Skip to content

Commit 4f8d8dd

Browse files
committed
Added readme
1 parent 444beb9 commit 4f8d8dd

1 file changed

Lines changed: 77 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
This module implements a set of requests TransportAdapter, PoolManager,
2+
ConnectionPool and HTTPSConnection with one goal only:
3+
4+
* to use a specific IP address when connecting via SSL to a web service without
5+
running into SNI trouble.
6+
7+
The usual technique to force an IP address on an HTTP connection with Requests
8+
is (assuming I want http://example.com/some/path on IP 1.2.3.4):
9+
10+
```
11+
requests.get("http://1.2.3.4/some/path", headers={'Host': 'example.com'})
12+
```
13+
14+
this is useful if I want to specifically test how 1.2.3.4 is responding; for
15+
instance, if example.com is DNS round-robined to several IP
16+
addresses and I want to hit one of them specifically.
17+
18+
This also works for https requests if using Python <2.7.9 because older
19+
versions don't do SNI and thus don't pass the requested hostname as part of the
20+
SSL handshake.
21+
22+
However, Python >=2.7.9 and >=3.4.x conveniently added SNI support, breaking
23+
this way of connecting to the IP, because the IP address embedded in the URL
24+
*is* passed as part of the SSL handshake, causing errors (mainly, the server
25+
returns a 400 Bad Request because the SNI host 1.2.3.4 doesn't match the one in
26+
the HTTP headers example.com).
27+
28+
The "easiest" way to achieve this is to force the IP address at the lowest
29+
possible level, namely when we do socket.create_connection. The rest of the
30+
"stack" is given the actual hostname. So the sequence is:
31+
32+
1. Open a socket to 1.2.3.4
33+
2. SSL wrap this socket using the hostname.
34+
3. Do the rest of the HTTPS traffic, headers and all over this socket.
35+
36+
Unfortunately Requests hides the socket.create_connection call in the deep
37+
recesses of urllib3, so the specified chain of classes is needed to propagate
38+
the given dest_ip value all the way down the stack.
39+
40+
Because this applies to a very limited set of circumstances, the overridden
41+
code is very simplistic and eschews many of the nice checks Requests does for
42+
you.
43+
44+
Specifically:
45+
46+
- It ONLY handles HTTPS.
47+
- It does NO certificate verification (which would be pointless)
48+
- Only tested with Requests 2.2.1 and 2.9.1.
49+
- Does NOT work with the ancient urllib3 (1.7.1) shipped with Ubuntu 14.04.
50+
Should not be an issue because Ubunt 14.04 has older Python which doesn't do
51+
SNI.
52+
53+
54+
How to use it
55+
=============
56+
57+
It's like any other transport adapter. Just pass the IP address that
58+
connections to the given URL prefix should use.
59+
60+
```
61+
session = requests.Session()
62+
session.mount("https://example.com", ForcedIPHTTPSAdapter(dest_ip='1.2.3.4'))
63+
response = session.get(
64+
'/some/path', headers={'Host': 'example.com'}, verify=False)
65+
```
66+
67+
Note this module will ImportError if there's no sane requests/urllib
68+
combination available so the adapter won't work, and it's up to the caller to
69+
decide what to do. The caller can, for instance, check the Python version and
70+
if it's &lt;2.7.9 decide to use the old "http://$IP/ technique. If Python is
71+
&gt;=2.7.9 and the adapter doesn't work, unfortunately, there's nothing that can
72+
be done :(
73+
74+
An example.py script is provided, it illustrates how to
75+
import the module, how to decide whether to use the adapter
76+
or the old technique, and how to define the IP and mount
77+
the adapter on a session.

0 commit comments

Comments
 (0)