2009-07-19 15 views
31

¿Cómo configuro la IP/interfaz de origen con Python y urllib2?Interfaz de origen con Python y urllib2

+0

Es razonable utilizar "peticiones" biblioteca o pycurl. Siempre tropiezas con el mal diseño de urllib2, si lo usas para tareas no triviales. – HighCat

Respuesta

45

Desafortunadamente la pila de módulos de biblioteca estándar en uso (urllib2, httplib, socket) es algo mal diseñado para el propósito - en el punto clave en el funcionamiento, HTTPConnection.connect (en httplib) delegados a socket.create_connection, que a su vez da no se "engancha" en absoluto entre la creación de la instancia de socket sock y la llamada sock.connect, para que inserte el sock.bind justo antes de sock.connect que es lo que necesita para configurar el IP de origen (estoy evangelizando ampliamente por NO diseñar abstracciones en tales una forma hermética, excesivamente encapsulada - Voy a estar hablando de eso en OSCON este jueves bajo el título "Zen y el arte del mantenimiento de la abstracción" - pero aquí su problema es cómo lidiar con una pila de abstracciones que fueron diseñadas de esta manera, suspiro).

Cuando enfrenta estos problemas, solo tiene dos soluciones no tan buenas: copiar, pegar y editar el código mal diseñado en el que necesita colocar un "gancho" que el diseñador original no atendió ; o, "mono-parche" ese código. Ninguno de los dos es BUENO, pero ambos pueden funcionar, así que al menos agradezcamos que tengamos esas opciones (usando un lenguaje de código abierto y dinámico). En este caso, creo que me gustaría ir para el mono-parches (que es malo, pero copiar y pegar codificación es aún peor) - un fragmento de código como:

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 

En función de sus necesidades específicas (ver necesita todos los enchufes para estar vinculados a la misma fuente IP, o ...?) simplemente puede ejecutar esto antes de usar urllib2 normalmente, o (en formas más complejas, por supuesto) ejecutarlo en caso necesario solo para los enchufes salientes que necesita para enlazar de cierta manera (luego, cada vez restaure socket.socket = true_socket para salir del camino para futuras tomas que aún no se han creado). La segunda alternativa agrega sus propias complicaciones para orquestar correctamente, por lo que espero que aclare si necesita complicaciones antes de explicarlas todas.

La buena respuesta de AKX es una variante de la alternativa "copiar/pegar/editar", así que no necesito ampliar mucho sobre eso - tenga en cuenta que no reproduce exactamente socket.create_connection en su método connect, vea el fuente here (al final de la página) y decida qué otra funcionalidad de la función create_connection puede incluir en su versión copiada/pegada/editada si decide seguir esa ruta.

+5

no solo una respuesta completa, sino, tal vez, el primer ejemplo de buen uso de parches de mono que he visto –

+1

Tx @Roberto - es más un "mal menor", pero, sí, cuando se enfrenta a una abstracción eso está sellado en contra de sus necesidades (faltando los ganchos/"fugas" requeridos), el parcheo puede ser "bueno" (en el mismo sentido que tirar un diente enfermo sin remedio es "bueno" ;-). –

+0

@Alex Martelli: Gracias. ¡El parche de mono funcionó bien! :) – jonasl

24

Esto parece funcionar.

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. 

Sin embargo, tendrá que encontrar la manera de parametrizar "127.0.0.1".

+0

¡Esto funciona perfectamente! Muchas gracias. –

+0

@DaveRawks: ¿En qué sistema obtuviste el éxito? No puedo vincular la interfaz de red en Windows 7 –

+0

El código funciona para mí en Linux y OSX. Nunca he escrito código de socket en Windows, pero sospecho que la falta de tomas sin procesar en el espacio de usuario de windows podría causar problemas. –

2

Pensé en seguir con una versión ligeramente mejor del parche de mono. Si necesita poder establecer diferentes opciones de puerto en algunos de los sockets o está usando algo como SSL, el código siguiente funciona un poco mejor.

_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 

Sólo tienes que obligar a la toma de conexión si es necesario ejecutar algo así como un servidor web en el mismo proceso que necesita para unirse a una dirección IP diferente.

1

Razonamiento esté de mí mono-parche en el nivel más alto disponible, aquí es una alternativa a la respuesta de Alex qué parches httplib en lugar de socket, aprovechando argumento source_address palabra clave httplib.HTTPSConnection.__init__() 's (que no está expuesta por urllib2, AFAICT). Probado y trabajando en 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 
10

Aquí hay un refinamiento adicional que hace uso de HTTPConnection's source_address argument (introducido en 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) 

Esto nos da una costumbre urllib2.HTTPHandler aplicación que es source_address conscientes. Podemos añadir a una nueva urllib2.OpenerDirector e instalarlo como el abridor por defecto (para futuras llamadas urlopen()) con el siguiente código:

handler = BoundHTTPHandler(source_address=("192.168.1.10", 0)) 
opener = urllib2.build_opener(handler) 
urllib2.install_opener(opener) 
+2

+1. Este, creo, debería ser el método recomendado con el módulo 'httplib' actualizado de Python 2.7. Por cierto, la cadena de direcciones IPv4 debe ponerse entre comillas. Intenté editar tu publicación, pero un cambio tan pequeño no pudo pasar el filtro de trivialidad de stackoverflow. –