2009-07-19 51 views
31

如何設置Python和urllib2的源IP /接口?Python和urllib2的源接口

+0

使用「請求」庫或pycurl是合理的。如果您將它用於非平凡的任務,您總會偶然發現urllib2的糟糕設計。 – HighCat 2012-09-09 11:49:07

回答

45

不幸的是,使用中的標準庫模塊(urllib2,httplib,socket)的堆棧設計有點糟糕 - 在操作的關鍵點,HTTPConnection.connect(在httplib中)代表socket.create_connection,這反過來給出你在創建套接字實例socksock.connect調用之間沒有任何「掛鉤」,因爲你需要在sock.connect之前插入sock.bind,這就是你需要設置源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的好答案是「複製/粘貼/編輯」替代方案中的一個變種,因此我不需要在這方面進行太多的擴展 - 但請注意,它並不完全在其connect方法中重現socket.create_connection,請參閱源here(位於頁面的最後),並決定如果您決定轉到該路由,您可能希望在複製/粘貼/編輯的版本中體現create_connection函數的其他功能。

24

這似乎工作。

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」。

+0

這個完美的作品!非常感謝。 – 2010-10-05 23:33:30

+0

@DaveRawks:你在哪個系統中獲得成功?我無法綁定Windows 7中的網絡接口。 – 2013-01-27 12:25:26

+0

代碼適用於Linux和OSX上的我。我從來沒有在Windows上編寫過套接字代碼,但我懷疑在用戶空間中缺少原始套接字可能會導致問題。 – 2013-02-04 22:39:35

2

我以爲我會跟進一個稍微好一點的猴子補丁版本。如果您需要能夠在某些套接字上設置不同的端口選項,或者使用類似套接字的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地址相同的過程Web服務器。

1

推理,我應該猴子補丁在可用的最高級別,這裏是亞歷克斯的答案替代哪些補httplib,而不是socket,趁着httplib.HTTPSConnection.__init__()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 
10

這裏有一個進一步的細化,使得使用的HTTPConnection's source_address argument(在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)