|
| 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 <2.7.9 decide to use the old "http://$IP/ technique. If Python is |
| 71 | +>=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