Исходный интерфейс с Python и urllib2 - PullRequest
31 голосов
/ 19 июля 2009

Как установить исходный IP / интерфейс с Python и urllib2?

Ответы [ 6 ]

46 голосов
/ 19 июля 2009

К сожалению, стек используемых стандартных библиотечных модулей (urllib2, httplib, socket) несколько плохо спроектирован для этой цели - в ключевой момент операции HTTPConnection.connect (в httplib) делегирует socket.create_connection, что в свою очередь, между созданием экземпляра сокета sock и вызовом sock.connect нет никакой «зацепки», чтобы вставить sock.bind непосредственно перед sock.connect, то есть то, что вам нужно для установки исходного IP-адреса ( Я широко проповедую за то, что НЕ проектирую абстракции таким воздухонепроницаемым, чрезмерно инкапсулированным способом - я буду говорить об этом в OSCON в этот четверг под названием «Дзен и поддержание искусства абстракции» - но здесь ваша проблема как работать со стеком абстракций, которые были разработаны таким образом, вздох).

Когда вы сталкиваетесь с такими проблемами, у вас есть только два не очень хороших решения: либо скопируйте, вставьте и отредактируйте неправильно разработанный код, в который вам нужно поместить «зацепку», которой оригинальный дизайнер не воспользовался; или «обезьяна-патч» этот код. Ни то, ни другое ХОРОШО, но оба могут работать, так что, по крайней мере, давайте будем благодарны, что у нас есть такие варианты (с использованием открытого исходного кода и динамического языка). В этом случае, я думаю, я бы пошел на исправление обезьян (что плохо, но копирование и вставка кодирования еще хуже) - фрагмент кода, такой как:

import socket
true_socket = socket.socket
def bound_socket(*a, **k):
    sock = true_socket(*a, **k)
    sock.bind((sourceIP, 0))
    return sock
socket.socket = bound_socket

В зависимости от ваших точных потребностей (вам нужно, чтобы все сокеты были привязаны к одному и тому же исходному IP-адресу, или ...?), Вы можете просто запустить это перед использованием urllib2 как обычно, или (конечно, более сложными способами) при необходимости запускайте его только для тех исходящих сокетов, которые вам НЕОБХОДИМО связать определенным образом (затем каждый раз восстанавливайте socket.socket = true_socket, чтобы убрать путь с будущих сокетов, которые еще предстоит создать). Второй вариант добавляет свои сложности к правильной организации, поэтому я жду, чтобы вы выяснили, нужны ли вам такие сложности, прежде чем объяснять их все.

Хороший ответ AKX - это вариант альтернативы «копировать / вставить / редактировать», поэтому мне не нужно особо на этом разбираться - однако учтите, что он не совсем точно воспроизводит socket.create_connection в методе connect посмотрите исходный код здесь (в самом конце страницы) и решите, какие другие функции функции create_connection вы, возможно, захотите включить в свою скопированную / вставленную / отредактированную версию, если решите перейти к этому маршрут.

24 голосов
/ 19 июля 2009

Это похоже на работу.

import urllib2, httplib, socket

class BindableHTTPConnection(httplib.HTTPConnection):
    def connect(self):
        """Connect to the host and port specified in __init__."""
        self.sock = socket.socket()
        self.sock.bind((self.source_ip, 0))
        if isinstance(self.timeout, float):
            self.sock.settimeout(self.timeout)
        self.sock.connect((self.host,self.port))

def BindableHTTPConnectionFactory(source_ip):
    def _get(host, port=None, strict=None, timeout=0):
        bhc=BindableHTTPConnection(host, port=port, strict=strict, timeout=timeout)
        bhc.source_ip=source_ip
        return bhc
    return _get

class BindableHTTPHandler(urllib2.HTTPHandler):
    def http_open(self, req):
        return self.do_open(BindableHTTPConnectionFactory('127.0.0.1'), req)

opener = urllib2.build_opener(BindableHTTPHandler)
opener.open("http://google.com/").read() # Will fail, 127.0.0.1 can't reach google.com.

Вам нужно будет найти какой-нибудь способ для параметризации "127.0.0.1", однако.

11 голосов
/ 03 февраля 2013

Вот еще одно уточнение, в котором используется аргумент source_address HTTPConnection (введен в Python 2.7):

import functools
import httplib
import urllib2

class BoundHTTPHandler(urllib2.HTTPHandler):

    def __init__(self, source_address=None, debuglevel=0):
        urllib2.HTTPHandler.__init__(self, debuglevel)
        self.http_class = functools.partial(httplib.HTTPConnection,
                source_address=source_address)

    def http_open(self, req):
        return self.do_open(self.http_class, req)

Это дает нам пользовательскую urllib2.HTTPHandler реализацию, которая поддерживает source_address. Мы можем добавить его в новый urllib2.OpenerDirector и установить его как средство открывания по умолчанию (для будущих вызовов urlopen () ) со следующим кодом:

handler = BoundHTTPHandler(source_address=("192.168.1.10", 0))
opener = urllib2.build_opener(handler)
urllib2.install_opener(opener)
2 голосов
/ 23 июля 2010

Я думал, что получу чуть лучшую версию патча обезьяны. Если вам необходимо установить различные параметры порта для некоторых сокетов или использовать что-то вроде SSL для сокета подклассов, следующий код работает немного лучше.

_ip_address = None
def bind_outgoing_sockets_to_ip(ip_address):
    """This binds all python sockets to the passed in ip address"""
    global _ip_address
    _ip_address = ip_address

import socket
from socket import socket as s

class bound_socket(s):
    def connect(self, *args, **kwargs):
        if self.family == socket.AF_INET:
            if self.getsockname()[0] == "0.0.0.0" and _ip_address:                
                self.bind((_ip_address, 0))
        s.connect(self, *args, **kwargs)
socket.socket = bound_socket

Вы должны связывать сокет при подключении только в том случае, если вам нужно запустить что-то вроде веб-сервера в том же процессе, который нужно связать с другим IP-адресом.

1 голос
/ 18 июня 2013

Начиная с Python 2.7 к httplib.HTTPConnection был добавлен исходный_адрес, позволяющий предоставить пару IP-портов для привязки.

См .: http://docs.python.org/2/library/httplib.html#httplib.HTTPConnection

1 голос
/ 02 апреля 2012

Принимая во внимание, что я должен выполнить обезьяну-патч на самом высоком доступном уровне, вот альтернатива ответу Алекса, который исправляет httplib вместо socket, используя аргумент httplib.HTTPSConnection.__init__() s source_address ключевого слова (который не является выставлено urllib2, AFAICT). Протестировано и работает на Python 2.7.2.

import httplib
HTTPSConnection_real = httplib.HTTPSConnection
class HTTPSConnection_monkey(HTTPSConnection_real):
   def __init__(*a, **kw):
      HTTPSConnection_real.__init__(*a, source_address=(SOURCE_IP, 0), **kw)
httplib.HTTPSConnection = HTTPSConnection_monkey
...