如何强制
requests
库对 get 请求使用特定的互联网协议版本?或者用 Python 中的另一种方法可以更好地实现这一点?我可以,但我不想使用curl
…
阐明目的的示例:
import requests
r = requests.get('https://my-dyn-dns-service.domain/?hostname=my.domain',
auth = ('myUserName', 'my-password'))
我找到了一个简单的解决方案来强制 urrlib3 使用 ipv4 或 ipv6。 urrlib3 使用此方法为 Http 和 Https 创建新连接。您可以在其中指定您想要使用的任何 AF_FAMILY。
import socket
import requests.packages.urllib3.util.connection as urllib3_cn
def allowed_gai_family():
"""
https://github.com/shazow/urllib3/blob/master/urllib3/util/connection.py
"""
family = socket.AF_INET
if urllib3_cn.HAS_IPV6:
family = socket.AF_INET6 # force ipv6 only if it is available
return family
urllib3_cn.allowed_gai_family = allowed_gai_family
您可以使用此 hack 强制
requests
使用 IPv4:
requests.packages.urllib3.util.connection.HAS_IPV6 = False
(这是 Nulano 作为评论提交的,我认为它应该是一个正确的答案)。
这是一个 hack,但您可以对 getaddrinfo 进行猴子修补以仅过滤 IPv4 地址:
# Monkey patch to force IPv4, since FB seems to hang on IPv6
import socket
old_getaddrinfo = socket.getaddrinfo
def new_getaddrinfo(*args, **kwargs):
responses = old_getaddrinfo(*args, **kwargs)
return [response
for response in responses
if response[0] == socket.AF_INET]
socket.getaddrinfo = new_getaddrinfo
我为
requests
+urllib3
+socket
编写了一个运行时补丁,允许根据每个请求选择性地传递所需的地址族。
与其他解决方案不同,不涉及猴子补丁,而是用修补的文件替换导入的
requests
,它提供了一个request
兼容的接口,所有公开的类都被子类化和修补,并且所有“简单API”功能都被重新实现。唯一明显的区别应该是存在一个额外的 family
参数,您可以使用该参数将名称解析期间使用的地址族限制为 socket.AF_INET
或 socket.AF_INET6
。然后,使用一系列有点复杂(但大多只是 LoC 密集型)的策略方法覆盖来将该值一直传递到 urllib3
的底层,在那里它将用于 socket.create_connection
函数调用的替代实现.
TL;DR 用法如下:
import socket
from . import requests_wrapper as requests # Use this load the patch
# This will work (if IPv6 connectivity is available) …
requests.get("http://ip6only.me/", family=socket.AF_INET6)
# … but this won't
requests.get("http://ip6only.me/", family=socket.AF_INET)
# This one will fail as well
requests.get("http://127.0.0.1/", family=socket.AF_INET6)
# This one will work if you have IPv4 available
requests.get("http://ip6.me/", family=socket.AF_INET)
# This one will work on both IPv4 and IPv6 (the default)
requests.get("http://ip6.me/", family=socket.AF_UNSPEC)
补丁库的完整链接(~350 LoC):https://gitlab.com/snippets/1900824
我采用了与 https://stackoverflow.com/a/33046939/5059062 类似的方法,但是修补了
socket
中发出 DNS 请求的部分,因此它仅执行 IPv6 或 IPv4,对于每个 请求,这意味着它可以在 urllib
中使用,就像在
requests
中一样有效。
如果你的程序也使用unix管道和其他类似的东西,这可能会很糟糕,所以我强烈建议谨慎使用monkeypatching。
import requests
import socket
from unittest.mock import patch
import re
orig_getaddrinfo = socket.getaddrinfo
def getaddrinfoIPv6(host, port, family=0, type=0, proto=0, flags=0):
return orig_getaddrinfo(host=host, port=port, family=socket.AF_INET6, type=type, proto=proto, flags=flags)
def getaddrinfoIPv4(host, port, family=0, type=0, proto=0, flags=0):
return orig_getaddrinfo(host=host, port=port, family=socket.AF_INET, type=type, proto=proto, flags=flags)
with patch('socket.getaddrinfo', side_effect=getaddrinfoIPv6):
r = requests.get('http://ip6.me')
print('ipv6: '+re.search(r'\+3>(.*?)</',r.content.decode('utf-8')).group(1))
with patch('socket.getaddrinfo', side_effect=getaddrinfoIPv4):
r = requests.get('http://ip6.me')
print('ipv4: '+re.search(r'\+3>(.*?)</',r.content.decode('utf-8')).group(1))
并且没有
requests
:
import urllib.request
import socket
from unittest.mock import patch
import re
orig_getaddrinfo = socket.getaddrinfo
def getaddrinfoIPv6(host, port, family=0, type=0, proto=0, flags=0):
return orig_getaddrinfo(host=host, port=port, family=socket.AF_INET6, type=type, proto=proto, flags=flags)
def getaddrinfoIPv4(host, port, family=0, type=0, proto=0, flags=0):
return orig_getaddrinfo(host=host, port=port, family=socket.AF_INET, type=type, proto=proto, flags=flags)
with patch('socket.getaddrinfo', side_effect=getaddrinfoIPv6):
r = urllib.request.urlopen('http://ip6.me')
print('ipv6: '+re.search(r'\+3>(.*?)</',r.read().decode('utf-8')).group(1))
with patch('socket.getaddrinfo', side_effect=getaddrinfoIPv4):
r = urllib.request.urlopen('http://ip6.me')
print('ipv4: '+re.search(r'\+3>(.*?)</',r.read().decode('utf-8')).group(1))
在3.5.2中测试
使用Python“请求”与现有套接字连接和如何强制python httplib库仅使用A请求的答案,看起来你应该能够创建一个仅限 IPv6 的套接字,然后让请求将其用于其连接池,例如:
try:
from http.client import HTTPConnection
except ImportError:
from httplib import HTTPConnection
class MyHTTPConnection(HTTPConnection):
def connect(self):
print("This actually called called")
self.sock = socket.socket(socket.AF_INET6)
self.sock.connect((self.host, self.port,0,0))
if self._tunnel_host:
self._tunnel()
requests.packages.urllib3.connectionpool.HTTPConnection = MyHTTPConnection
我还需要重写 HTTP
SConnection,它与 HTTPConnection 有很大不同,因为 requests
包装了 httplib.HTTPSConnection 以验证证书
ssl
模块是否可用。
import socket
import ssl
try:
from http.client import HTTPConnection
except ImportError:
from httplib import HTTPConnection
from requests.packages.urllib3.connection import VerifiedHTTPSConnection
# HTTP
class MyHTTPConnection(HTTPConnection):
def connect(self):
self.sock = socket.socket(socket.AF_INET)
self.sock.connect((self.host, self.port))
if self._tunnel_host:
self._tunnel()
requests.packages.urllib3.connectionpool.HTTPConnection = MyHTTPConnection
requests.packages.urllib3.connectionpool.HTTPConnectionPool.ConnectionCls = MyHTTPConnection
# HTTPS
class MyHTTPSConnection(VerifiedHTTPSConnection):
def connect(self):
self.sock = socket.socket(socket.AF_INET)
self.sock.connect((self.host, self.port))
if self._tunnel_host:
self._tunnel()
self.sock = ssl.wrap_socket(self.sock, self.key_file, self.cert_file)
requests.packages.urllib3.connectionpool.HTTPSConnection = MyHTTPSConnection
requests.packages.urllib3.connectionpool.VerifiedHTTPSConnection = MyHTTPSConnection
requests.packages.urllib3.connectionpool.HTTPSConnectionPool.ConnectionCls = MyHTTPSConnection
用途:
import requests
set_ip_version(6)
requests.get(...)
set_ip_version(4)
来源:
import sockets
import requests.packages.urllib3.util.connection
def get_ip_version():
# type: () -> Optional[int]
ip_version = requests.packages.urllib3.util.connection.allowed_gai_family()
if ip_version == socket.AF_INET6:
return 6
elif ip_version == socket.AF_INET:
return 4
else:
return None
def set_ip_version(ip_version: int = 6):
# type: (str) -> bool
"""
Sets preferred IP protocol version to use in requests / ofunctions
Attention: this propagates
By default, AF_INET6 is used which preferes IPv6 but fallbacks to IPv4
"""
if ip_version == 4:
def allowed_gai_family():
return socket.AF_INET
elif ip_version == 6:
def allowed_gai_family():
if requests.packages.urllib3.util.connection.HAS_IPV6:
return socket.AF_INET6
return socket.AF_INET
else:
return False
requests.packages.urllib3.util.connection.allowed_gai_family = allowed_gai_family
return True
这些函数存在于 ofunctions.network 库中,随 pip install ofunctions.network
安装。然后您可以通过致电他们
from ofunctions.network import set_ip_version
set_ip_version(6)