Skip to content

Instantly share code, notes, and snippets.

@jobec
Last active October 16, 2023 10:29
Show Gist options
  • Save jobec/0a797ca165c725417ab1229fc33d8265 to your computer and use it in GitHub Desktop.
Save jobec/0a797ca165c725417ab1229fc33d8265 to your computer and use it in GitHub Desktop.
How to do domain fronting in Python with Requests. Send a request to an arbitrary IP address and force the SNI field and Host HTTP header to a certain value.
#
# How to do domain fronting in Python with Requests.
#
# Send a request to an arbitrary IP address and force the
# SNI field and Host HTTP header to a certain value.
#
import http.client
import requests
import urllib3
from requests.adapters import HTTPAdapter
http.client.HTTPConnection.debuglevel = 5
urllib3.add_stderr_logger()
class FrontingAdapter(HTTPAdapter):
""""Transport adapter" that allows us to use SSLv3."""
def __init__(self, fronted_domain=None, **kwargs):
self.fronted_domain = fronted_domain
super(FrontingAdapter, self).__init__(**kwargs)
def send(self, request, **kwargs):
connection_pool_kwargs = self.poolmanager.connection_pool_kw
if self.fronted_domain:
connection_pool_kwargs["assert_hostname"] = self.fronted_domain
elif "assert_hostname" in connection_pool_kwargs:
connection_pool_kwargs.pop("assert_hostname", None)
return super(FrontingAdapter, self).send(request, **kwargs)
def init_poolmanager(self, *args, **kwargs):
server_hostname = None
if self.fronted_domain:
server_hostname = self.fronted_domain
super(FrontingAdapter, self).init_poolmanager(server_hostname=server_hostname, *args, **kwargs)
# Based on the domain fronting example at https://digi.ninja/blog/cloudfront_example.php
s = requests.Session()
s.mount('https://', FrontingAdapter(fronted_domain="fronted.digi.ninja"))
r = s.get("https://54.230.14.90/", headers={"Host": "d1sdh26o090vk5.cloudfront.net"})
print()
print(r.content)
@davidjmemmett
Copy link

After a small amount of experimentation, it seems that this can be trimmed down to a one-liner (as of June 2022):

session.get_adapter('https://').poolmanager.connection_pool_kw['assert_hostname'] = 'my-fake-hostname-here'

@alpaycetin74
Copy link

session.get_adapter('https://').poolmanager.connection_pool_kw['assert_hostname'] = 'my-fake-hostname-here'

Could you explain why since June 2022? Which package/version was introduced ?

@wanghuizzz
Copy link

wanghuizzz commented Jan 17, 2023

After a small amount of experimentation, it seems that this can be trimmed down to a one-liner (as of June 2022):

session.get_adapter('https://').poolmanager.connection_pool_kw['assert_hostname'] = 'my-fake-hostname-here'

It's great! But I also need to set server_hostname for it to take effect. (Python3.7, requests == 2.28.1)

s = requests.Session()
s.get_adapter('https://').poolmanager.connection_pool_kw['server_hostname'] = "mytest.com"
s.get_adapter('https://').poolmanager.connection_pool_kw['assert_hostname'] = "mytest.com"

r = s.get("https://93.184.216.34", headers= {"Host": "mytest.com"})
print(r.content)

@tempookian
Copy link

It's awesome, but is there a way not to expose mytest.com? What we are experiencing is that the request is rejected based on the domain visible in the client hello.

After a small amount of experimentation, it seems that this can be trimmed down to a one-liner (as of June 2022):

session.get_adapter('https://').poolmanager.connection_pool_kw['assert_hostname'] = 'my-fake-hostname-here'

It's great! But I also need to set server_hostname for it to take effect. (Python3.7, requests == 2.28.1)

s = requests.Session()
s.get_adapter('https://').poolmanager.connection_pool_kw['server_hostname'] = "mytest.com"
s.get_adapter('https://').poolmanager.connection_pool_kw['assert_hostname'] = "mytest.com"

r = s.get("https://93.184.216.34", headers= {"Host": "mytest.com"})
print(r.content)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment