Repository navigation
Expose socket connection interface in connection #273
Description
I've working on an custom httpx transport which uses Kerberos/NTLM/Negotiate authentication when talking to a peer. These authentication methods have a custom extension when talking over TLS called channel binding tokens (CBT) where you embed a structure that is based on information from the TLS context which in essence binds the authentication against the TLS context making it harder to achieve MitM attacks.
A version of CBT that I'm currently using is documented in RFC 5929 which is essentially tls-server-end-point:<cert hash>. In the past I've just send a request to the host, gotten the SSL object from the socket in the response but this has a few problems:
- You waste a round trip sending a request you know will fail
- Somewhat more annoying because in TLS there are more than 1 roundtrips to set up the TLS context
- You need to have set up the connection before you can build the authentication context and generate the first auth token
- If the connection is closed in the auth request then the CBT data you used might not be correct anymore
The last point is problematic because in my scenario I send a blank request which sets up a connection and the response I get back is a 401. Typically I've seen a Connection: close header which has the *ConnectionPool classes drop the connection from the pool. This means the next request is a brand new connection with a brand new negotiated TLS context.
For the tls-server-end-point I could still get the certificate hash as that should be static between the connections but with new protocols like TLS 1.3 the draft proposal for CBT seems to indicate it's based on the unique connection set up Breaking authentication.
What I'm hoping is for is to expose the _open_socket method publically in some shape or form. This would allow me to create the socket connection and set up the TLS request before I send a request through (s)request on the connection. I feel like this could potentially be done by setting up the socket myself and passing it into the __init__() for *HTTPConnection or by just making that method more public by removing the _. This also hinges on my other question around whether connections are actually meant to be publicly exposed in #272.
Very interested to hear your thoughts on this as I do have a working prototype for all this but it relies on being able to access AsyncHTTPConnection and calling _open_socket manually before sending the request through (a)request.
Activity
jborean93 commentedon Feb 22, 2021
When thinking about this a bit more, it sounds like having an
open/aopenmethod that is purely designed to open the connection. I'll see if I can create a PR to show you what I mean.jborean93 commentedon Feb 23, 2021
I've just opened a draft PR that exposes the functionality I need as a public interface #274 in what I think is a suitable implementation. Happy to hear your thoughts on the approach and whether there's anything you wish to change or another approach you recommend.
florimondmanca commentedon Feb 23, 2021
One thing that hits me looking at #274 and our code is that, although
AsyncHTTPConnectionis defined as a subclass ofAsyncHTTPTransport(it "is" a transport), we don't use it as such internally.See:
httpcore/httpcore/_async/connection_pool.py
Lines 201 to 226 in 12a25fd
In here we do a dance to acquire an existing connection from the pool, create one if needed, send a request, and then remove the connection from the pool.
If
AsyncHTTPConnectionis to be a proper transport, then we should use it like a transport, i.e. we should call into__aenter__(),arequest()and__aexit__()/aclose(). Right now we only doarequest()(andaclose()when the pool gets closed). That's a bit awkward, and worth looking into before looking into any new hooks, I think.florimondmanca commentedon Feb 23, 2021
@jborean93 About #274 and the "allow connections to open their sockets via a dedicated method" idea… Also, cc @tomchristie — happy to hear your thoughts about this?
Circling back to my last point above, actually transports are really thought of as classes with two methods:
arequest(), andaclose(). Transports aren't really designed to "open things upon creation", at least not within the transport class itself. This is close enough to Trio's "async resource" idea, which is basically just an object with anaclose()method. In Trio, these "async resources" are typically created using async context managers, likeopen_something(), which manage the creation of the underlying I/O resources and pass them to the class constructor. Like this…Which is used like this:
But although our transports expose the same kind of "constructor +
aclose()" style, we don't really use them with the proper semantics always.For example,
AsyncHTTPConnectiondeals with its own "open socket" internally by calling it upon firstarequest(). That's kind of clunky. Ideally, we'd open a socket beforehand, and pass it toAsyncHTTPConnection(socket, ...). We don't do this right now (except for tunnel proxying, in which we pass an existing socket), and I think it's a problem that begs for a small refactor.So, we could switch to a style in which we have
AsyncConnectionPoolmanage the opening of sockets, and pass them down to connections. It would work, and I have a local branch that passes tests to prove it. :-) See #275To customize "open socket" behavior, one would have to either override
AsyncConnectionPool._open_socket()(if using connection pooling), or — if a single connection transport is enough — copy that code to open a socket before passing it toAsyncHTTPConnection(socket=...).@jborean93 What would be the impact of this on your use case?
jborean93 commentedon Feb 23, 2021
Yea there seems to be a mixture of transport behaviours but ultimately it does things right. The reason why I thought adding
aopen()was because it can ensure that anything that opens a socket or some other resource for an endpoint can be controlled by both__aenter__() -> aopen()and__aexit__() -> aclose(). So if a transport uses a network resource like a socket it can open and close them as needed but if not then it just does nothing.I can't really question how things work here as I'm mostly fumbling around but this is what I see with the 3 classes of transports being used by
httpcorein a normal operationAsyncConnectionPoolAsyncHTTPTransportimplementation in httpcoreAsyncHTTPConnectionas neededaclose()will callaclose()on all the connections in the poolAsyncHTTPConnectionAsyncHTTP11Connection(and the other HTTP version specific connections)arequest()aclose()will callaclose()on the HTTP version specific connection it had createdAsyncHTTP11Connectionaclose()will close the H11 state machine and the socket that it is usingSo we already do have actual instances where we pass in a socket (ignoring tunneled proxies) I was mostly just trying to mirror the close method with the relevant open functionality that a transprot may or may not implement.
Whatever route you would like to go forward I'm happy to follow, all I need is a way to open the connection and do the TLS handshake before I prepare my actual HTTP request. Right now that's impossible to do without calling some private methods, or re implementing the whole shebang. Still what you have in that PR will work for my scenario, I can manually create and connect to the socket myself, the next step is to just make the connection class public :) (totally understand why the project is not prepared to do that right now).
The method you have chosen works for my use case and I will be happy to adopt it once the underlying connection is made public. For now I've just re implemented (shamelessly copied)
AsyncHTTP11Connectionand have a thing transport over the top that manages the connection in a pool-less scenario https://github.com/jborean93/pypsrp/blob/asyncio/psrp/_wsman/_async.py. I'll probably copy your create the socket and open method in the parent transport and use that socket in the HTTP11 connection rather than the aopen() method I went with.