【问题标题】:Python 'requests' library - define specific DNS?Python“请求”库 - 定义特定的 DNS?
【发布时间】:2014-05-01 19:24:31
【问题描述】:

在我的项目中,我使用 python requests library 处理所有 HTTP 请求。

现在,我需要使用特定的 DNS 查询 http 服务器 - 有两个环境,每个环境都使用自己的 DNS,并且单独进行更改。

因此,当代码运行时,它应该使用特定于环境的 DNS,而不是我的互联网连接中指定的 DNS。

有没有人使用 python-requests 尝试过这个?我只找到了 urllib2 的解决方案:
https://stackoverflow.com/questions/4623090/python-set-custom-dns-server-for-urllib-requests

【问题讨论】:

    标签: python http dns urllib python-requests


    【解决方案1】:

    requests 使用urllib3,最终也使用httplib.HTTPConnection,因此来自https://stackoverflow.com/questions/4623090/python-set-custom-dns-server-for-urllib-requests 的技术(现已删除,它仅链接到Tell urllib2 to use custom DNS)仍然适用,一定程度上。

    urllib3.connection 模块以相同的名称继承 httplib.HTTPConnection,将 .connect() 方法替换为调用 self._new_conn 的方法。反过来,这代表urllib3.util.connection.create_connection()。修补那个函数可能是最简单的:

    from urllib3.util import connection
    
    
    _orig_create_connection = connection.create_connection
    
    
    def patched_create_connection(address, *args, **kwargs):
        """Wrap urllib3's create_connection to resolve the name elsewhere"""
        # resolve hostname to an ip address; use your own
        # resolver here, as otherwise the system resolver will be used.
        host, port = address
        hostname = your_dns_resolver(host)
    
        return _orig_create_connection((hostname, port), *args, **kwargs)
    
    
    connection.create_connection = patched_create_connection
    

    并且您需要提供自己的代码来将地址的 host 部分解析为 IP 地址,而不是依靠 connection.create_connection() 调用(包装 socket.create_connection())为您解析主机名。

    像所有猴子补丁一样,请注意代码在以后的版本中没有显着变化;这里的补丁是针对urllib3 1.21.1 版创建的。但应该适用于早至 1.9 的版本。


    请注意,此答案已重新编写以适用于较新的 urllib3 版本,其中添加了更方便的修补位置。请参阅旧方法的编辑历史记录,适用于版本 urllib3 版本的补丁,而不是独立安装。

    【讨论】:

    • 是的!完美运行。对于解析器,我使用了:def myResolver(host,dnssrv): r = dns.resolver.Resolver() r.nameservers = dnssrv answers = r.query(host, 'A') for rdata in answers: return str(rdata),现在对于每个请求,我都可以动态定义哪个 DNS 应该解析主机名。非常非常感谢。
    • FWIW,与此处的上下文管理器相同的解决方案 stackoverflow.com/a/61790319/253049 。尽管原来的 url 方案,它还增加了对强制 http 连接的支持。
    【解决方案2】:

    您应该查看TransportAdapters,包括源代码。它们的文档不是很好,但它们提供了对RFC 2818RFC 6125 中描述的许多功能的低级别访问。特别是,这些文档鼓励(要求?)客户端代码支持特定于应用程序的 DNS,以检查证书的 CommonName 和 SubjectAltName。您在这些调用中需要的关键字参数是“assert_hostname”。以下是使用 requests 库设置它的方法:

    from requests import Session, HTTPError
    from requests.adapters import HTTPAdapter, DEFAULT_POOLSIZE, DEFAULT_RETRIES, DEFAULT_POOLBLOCK
    
    
    class DNSResolverHTTPSAdapter(HTTPAdapter):
        def __init__(self, common_name, host, pool_connections=DEFAULT_POOLSIZE, pool_maxsize=DEFAULT_POOLSIZE,
            max_retries=DEFAULT_RETRIES, pool_block=DEFAULT_POOLBLOCK):
            self.__common_name = common_name
            self.__host = host
            super(DNSResolverHTTPSAdapter, self).__init__(pool_connections=pool_connections, pool_maxsize=pool_maxsize,
                max_retries=max_retries, pool_block=pool_block)
    
        def get_connection(self, url, proxies=None):
            redirected_url = url.replace(self.__common_name, self.__host)
            return super(DNSResolverHTTPSAdapter, self).get_connection(redirected_url, proxies=proxies)
    
        def init_poolmanager(self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs):
            pool_kwargs['assert_hostname'] = self.__common_name
            super(DNSResolverHTTPSAdapter, self).init_poolmanager(connections, maxsize, block=block, **pool_kwargs)
    
    common_name = 'SuperSecretSarahServer'
    host = '192.168.33.51'
    port = 666
    base_url = 'https://{}:{}/api/'.format(common_name, port)
    my_session = Session()
    my_session.mount(self.base_url.lower(), DNSResolverHTTPSAdapter(common_name, host))
    user_name = 'sarah'
    url = '{}users/{}'.format(self.base_url, user_name)
    default_response_kwargs = {
        'auth': (NAME, PASSWORD),
        'headers': {'Content-Type': 'application/json'},
        'verify': SSL_OPTIONS['ca_certs'],
        'cert': (SSL_OPTIONS['certfile'], SSL_OPTIONS['keyfile'])
    }
    response = my_session.get(url, **default_response_kwargs)
    

    我使用common_name 作为证书上预期的名称以及您的代码将如何引用所需的机器。我使用host 作为外部世界识别的名称——FQDN、IP、DNS 条目……当然,SSL_OPTIONS 字典(在我的示例中)必须列出您机器上的适当证书/密钥文件名。 (另外,NAME 和 PASSWORD 应该解析为正确的字符串。)

    【讨论】:

    • 我真的认为这应该是正确的答案,因为它是使用请求和 urllib3 的正确方法。
    【解决方案3】:

    定制的 HTTPAdapter 可以解决问题。

    不要忘记设置server_hostname 以启用SNI

    import requests
    
    
    class HostHeaderSSLAdapter(requests.adapters.HTTPAdapter):
        def resolve(self, hostname):
            # a dummy DNS resolver
            import random
            ips = [
                '104.16.89.20',  # CloudFlare
                '151.101.2.109',  # Fastly
            ]
            resolutions = {
                'cdn.jsdelivr.net': random.choice(ips),
            }
            return resolutions.get(hostname)
    
        def send(self, request, **kwargs):
            from urllib.parse import urlparse
    
            connection_pool_kwargs = self.poolmanager.connection_pool_kw
    
            result = urlparse(request.url)
            resolved_ip = self.resolve(result.hostname)
    
            if result.scheme == 'https' and resolved_ip:
                request.url = request.url.replace(
                    'https://' + result.hostname,
                    'https://' + resolved_ip,
                )
                connection_pool_kwargs['server_hostname'] = result.hostname  # SNI
                connection_pool_kwargs['assert_hostname'] = result.hostname
    
                # overwrite the host header
                request.headers['Host'] = result.hostname
            else:
                # theses headers from a previous request may have been left
                connection_pool_kwargs.pop('server_hostname', None)
                connection_pool_kwargs.pop('assert_hostname', None)
    
            return super(HostHeaderSSLAdapter, self).send(request, **kwargs)
    
    
    url = 'https://cdn.jsdelivr.net/npm/bootstrap/LICENSE'
    
    session = requests.Session()
    session.mount('https://', HostHeaderSSLAdapter())
    
    r = session.get(url)
    print(r.headers)
    
    r = session.get(url)
    print(r.headers)
    
    【解决方案4】:

    我知道这是一个旧线程,但这是我使用 tldextract 和 dnspython 的 python3 兼容解决方案。我留下了一些注释掉的代码来说明如何调试和设置额外的会话参数。

    #!/usr/bin/env python3
    
    import sys
    
    from pprint import pprint as pp
    
    import requests
    import dns.resolver # NOTE: dnspython package
    import tldextract
    
    class CustomAdapter(requests.adapters.HTTPAdapter):
        def __init__(self, nameservers):
            self.nameservers = nameservers
            super().__init__()
    
        def resolve(self, host, nameservers, record_type):
            dns_resolver = dns.resolver.Resolver()
            dns_resolver.nameservers = nameservers
            answers = dns_resolver.query(host, record_type)
            for rdata in answers:
                return str(rdata)
    
        def get_connection(self, url, proxies=None):
            ext = tldextract.extract(url)
            fqdn = ".".join([ ext.subdomain, ext.domain, ext.suffix ])
    
            print("FQDN: {}".format(fqdn))
            a_record = self.resolve(fqdn, nameservers, 'A')
            print("A record: {}".format(a_record))
    
            resolved_url = url.replace(fqdn, a_record) # NOTE: Replace first occurrence only
            print("Resolved URL: {}".format(resolved_url))
    
            return super().get_connection(resolved_url, proxies=proxies)
    
    if __name__ == "__main__":
    
        if len(sys.argv) != 2:
            print("Usage: {} <url>".format(sys.argv[0]))
            sys.exit(0)
    
        url = sys.argv[1]
    
        nameservers = [ 
            '208.67.222.222', # NOTE: OpenDNS
            '8.8.8.8'         # NOTE: Google
        ]
    
        session = requests.Session()
        session.mount(url, CustomAdapter(nameservers))
    
        parameters = {
            # "headers": {'Content-Type': 'application/json'},
            # "timeout" : 45,
            # "stream" : True
            # "proxies" : {
            #   "http": "http://your_http_proxy:8080/",
            #   "https": "http://your_https_proxy:8081/"
            # },
            # "auth": (name, password),
            # ...
        }
    
        response = session.get(url, **parameters)
        pp(response.__dict__)
    

    这里是控制台输出:

    $ ./run.py http://www.test.com
    FQDN: www.test.com
    A record: 69.172.200.235
    Resolved URL: http://69.172.200.235/
    {'_content': b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3'
                 b'.org/TR/html4/strict.dtd">\n<html>\n<head>\n<meta http-equiv="C'
                 b'ontent-Type" content="text/html; charset=iso-8859-1">\n<meta '
                 b'http-equiv="Content-Script-Type" content="text/javascript">\n'
                 b'<script type="text/javascript">\nfunction getCookie(c_name) {'
                 b' // Local function for getting a cookie value\n    if (docume'
                 b'nt.cookie.length > 0) {\n        c_start = document.cookie.in'
                 b'dexOf(c_name + "=");\n        if (c_start!=-1) {\n        c_st'
                 b'art=c_start + c_name.length + 1;\n        c_end=document.cook'
                 b'ie.indexOf(";", c_start);\n\n        if (c_end==-1) \n         '
                 b'   c_end = document.cookie.length;\n\n        return unescape('
                 b'document.cookie.substring(c_start,c_end));\n        }\n    }\n '
                 b'   return "";\n}\nfunction setCookie(c_name, value, expiredays'
                 b') { // Local function for setting a value of a cookie\n    va'
                 b'r exdate = new Date();\n    exdate.setDate(exdate.getDate()+e'
                 b'xpiredays);\n    document.cookie = c_name + "=" + escape(valu'
                 b'e) + ((expiredays==null) ? "" : ";expires=" + exdate.toGMTString'
                 b'()) + ";path=/";\n}\nfunction getHostUri() {\n    var loc = doc'
                 b"ument.location;\n    return loc.toString();\n}\nsetCookie('YPF8"
                 b"827340282Jdskjhfiw_928937459182JAX666', '171.68.244.56', 10)"
                 b';\ntry {  \n    location.reload(true);  \n} catch (err1) {  \n  '
                 b'  try {  \n        location.reload();  \n    } catch (err2) { '
                 b' \n    \tlocation.href = getHostUri();  \n    }  \n}\n</scrip'
                 b't>\n</head>\n<body>\n<noscript>This site requires JavaScript an'
                 b'd Cookies to be enabled. Please change your browser settings or '
                 b'upgrade your browser.</noscript>\n</body>\n</html>\n',
     '_content_consumed': True,
     '_next': None,
     'connection': <requests.adapters.HTTPAdapter object at 0x109130e48>,
     'cookies': <RequestsCookieJar[]>,
     'elapsed': datetime.timedelta(microseconds=992676),
     'encoding': 'ISO-8859-1',
     'headers': {'Server': 'nginx/1.14.2', 'Date': 'Wed, 01 May 2019 18:01:58 GMT', 'Content-Type': 'text/html', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', 'Keep-Alive': 'timeout=20', 'X-DIS-Request-ID': '2a5057a7c7b8a93dd700856c48fda74a', 'P3P': 'CP="NON DSP COR ADMa OUR IND UNI COM NAV INT"', 'Cache-Control': 'no-cache', 'Content-Encoding': 'gzip'},
     'history': [<Response [302]>],
     'raw': <urllib3.response.HTTPResponse object at 0x1095b90b8>,
     'reason': 'OK',
     'request': <PreparedRequest [GET]>,
     'status_code': 200,
     'url': 'https://www.test.com/'}
    

    希望这会有所帮助。

    【讨论】:

      猜你喜欢
      • 2017-03-16
      • 2015-06-10
      • 1970-01-01
      • 1970-01-01
      • 2017-12-25
      • 2020-08-05
      • 1970-01-01
      相关资源
      最近更新 更多