使用 asyncio/aiohttp 执行请求时指定 SNI server_hostname

问题描述 投票:0回答:1

各位开发者大家好!我被困在一个角落里,我开始手足无措......这是情节:

            load-balancer.example.com:443 (TCP passthrough)
                               /\
                              /  \
                             /    \
                            /      \
          s1.example.com:443        s2.example.com:443
              (SSL/SNI)                  (SSL/SNI)              

目标是直接使用启用证书验证的

s1
对上游
s2
aiohttp
进行压力测试。由于负载均衡器不属于我,我不想对其进行压力测试。

  • 该代码不应该在 GNU Linux 之外的其他平台上运行,至少具有 Python-v3.7(但如果需要,我可以使用任何最新版本)
  • 所有服务器都提供有效的证书
    load-balancer.example.com
  • openssl
    使用
    openssl s_connect s1.example.com:443 -servername load-balancer.example.com
  • 时验证来自上游的证书
  • cURL
    需要
    curl 'https://load-balancer.example.com/' --resolve s1.example.com:443:load-balancer.example.com
    并且也验证成功

我能够在两个上游并行启动大量异步

ClientSession.get
请求,但对于每个请求,我需要以某种方式告诉
asyncio
aiohttp
使用
load-balancer.example.com
作为
server_hostname
,否则 SSL 握手失败了。

有没有一种简单的方法可以在设置 SSL 套接字时将

ClientSession
设置为使用特定的
server_hostname
? 有人已经做过类似的事情了吗?

编辑:这是最简单的片段,只有一个请求:

import aiohttp
import asyncio


async def main_async(host, port, uri, params=[], headers={}, sni_hostname=None):
    if sni_hostname is not None:
        print('Setting SNI server_name field ')
        #
        # THIS IS WHERE I DON'T KNOW HOW TO TELL aiohttp
        # TO SET THE server_name FIELD TO sni_hostname
        # IN THE SSL SOCKET BEFORE PERFORMING THE SSL HANDSHAKE
        #
    try:
        async with aiohttp.ClientSession(raise_for_status=True) as session:
            async with session.get(f'https://{host}:{port}/{uri}', params=params, headers=headers) as r:
                body = await r.read()
                print(body)
    except Exception as e:
        print(f'Exception while requesting ({e}) ')


if __name__ == "__main__":
    asyncio.run(main_async(host='s1.example.com', port=443,
                           uri='/api/some/endpoint',
                           params={'apikey': '0123456789'},
                           headers={'Host': 'load-balancer.example.com'},
                           sni_hostname='load-balancer.example.com'))

当与真实主机一起运行时,它会抛出

Cannot connect to host s1.example.com:443 ssl:True 
[SSLCertVerificationError: (1, '[SSL: CERTIFICATE_VERIFY_FAILED] '
certificate verify failed: certificate has expired (_ssl.c:1131)')]) 

请注意,错误

certificate has expired
表示向客户端建议的证书是默认证书,因为 SNI 主机名是
s1.example.com
,运行在那里的 Web 服务器不知道该名称。

当针对负载均衡器运行它时,它工作得很好,SSL 握手发生在提供证书的上游,并且一切都是有效的。

还要注意的是

  • sni_callback
    没有帮助,因为它是在握手开始并收到证书后调用的(此时
    server_hostname
    无论如何都是只读属性)
  • 创建
    server_hostname
    时似乎无法设置
    SSLContext
    ,尽管SSLContext.wrap_socket确实支持
    server_hostname
    ,但我无法完成这项工作

我希望有人知道如何填写该片段中的评论块;-]

python-3.x python-asyncio aiohttp sni sslcontext
1个回答
0
投票

对于延迟回复表示歉意,但我最近遇到了同样的挑战。据我所知,流行的异步 HTTP 库

aiohttp
和任何其他当代异步 HTTP 库本身都不支持定义 SNI(服务器名称指示)字段。为了规避这一限制,我成功地使用了
urllib3
库并实现了类似于您提供的解决方法:

sni_proxy_ip_addr = "127.0.0.1"  # your SNI proxy server
server_hostname: str = "ifconfig.co"
server_hostname_uri: str = "/json"

pool = urllib3.HTTPSConnectionPool(
    sni_proxy_ip_addr,
    port=443,
    server_hostname=server_hostname
)

try:
    resp = pool.request(
        "GET",
        server_hostname_uri,
        headers={"Host": server_hostname},
        assert_same_host=False,
        retries=False
    )
except urllib3.exceptions.MaxRetryError:
    return {'original_ip_addr': sni_proxy_ip_addr}
except urllib3.exceptions.NewConnectionError:
    return {'original_ip_addr': sni_proxy_ip_addr}

PS。谷歌吟游诗人以更正式的方式重写你的回复真是太棒了:)))))

© www.soinside.com 2019 - 2024. All rights reserved.