python-web-sig / wsgi-ng

Working group for wsgi-ng
39 stars 3 forks source link

protocol detection #10

Open rbtcollins opened 9 years ago

rbtcollins commented 9 years ago

We need to define how websockets, bosh, and other possible network protocols in future are accessed with WSGI-ng. (From #2).

Assuming they all handshake roughly like HTTP, we can either define one [python] protocol which is a superset of all protocols (and mandate that folk only use the relevant subset for a given request), or we can define a minimal common [python] protocol and then escape out of that for each thing, or we can define one [python] protocol (or perhaps a profile of the protocol) for each protocol.

python protocol as superset of all requirements (one-protocol)

In this approach there is one python protocol, and one app for a server. Middleware/apps needs to check what network protocol is in use and tailor what they do to match. I suspect this would tend to find bugs in middleware where e.g. HTTP features are assumed and then that middleware needs to be fixed, and this may happen post-creation-and-testing when new network protocols have a smaller subset. OTOH new network protocols are somewhat rare.

python protocol per network protocol, dispatched via a common protocol and middleware/app checks (per-protocol-escaped)

In this approach there is one common python protocol and then several different protocols that are escaped out into. There would be one app for a server. This may require some care by us to correctly identify the minimum set of features, but if opt-in is required for all network protocols, and pass-through is the default, it should be harder to accidentally write middleware that breaks on known protocols. It would however still suffer from the issue where new network protocols may have less common ground that we assumed.

python protocol per network protocol, dispatched by the server (per-protocol)

If we take the subset protocol to its extreme and instead of escaping out into dedicated python protocols from a common core we say that the server will identify the network protocol and dispatch to a separate app for each network protocol. We also then need to write (or at least instantiate) N separate middlewares for things like e.g. extended logging or whatever a middleware wants to do. This would be entirely safe from the new protocol issue leading to silent failures - since middleware would never encounter network protocols it doesn't understand. OTOH it would require explicit effort by deployers to enable new protocols. The benefit of this approach is that its typesafe: a websocket specific middleware will never be given non-websocket requests to handle. A further variation would be to specialise the protocols by revision (http 0.9, 1, 2) or even negotiated features. Experimentation may be needed to assess that.

Characteristic one-protocol per-protocol-escaped per-protocol
network-protocol-conditionals yes yes no
single app handles everying yes yes no
ionelmc commented 9 years ago

About the 3rd idea, Having the protocol dispatch in the server doesn't allow the app to chose whether to upgrade/switch the protocol. At least not without adding additional semantics that allow the app to do so (like a callback). The whole thing doesn't seem desirable.

rbtcollins commented 9 years ago

Apps don't get to choose whether to upgrade/switch protocols at all anyway, for any of: HTTP/2 BOSH Comet Websockets

That is, there's no way given an HTTP/1 request to make it into an HTTP/2 request. You can tell the client to retry with HTTP/2 under some draft RFC's, but you can't just change that one request across.

Thats because these are all initiated by the client requesting a specific protocol. The app within that protocol could choose to reject it, but they can't change it anyhow.

Lukasa commented 9 years ago

Apps do, however, may want to refuse to upgrade. That is not possible in the case of the 3rd idea without getting the server to re-dispatch the request.

rbtcollins commented 9 years ago

I think I understand your point. Here's what I think you mean. Lets say that we're dealing with the websocket handshake 'upgrade' (its an "UPGRADE" only because thats how they tunnelled it on HTTP/1.1 - with HTTP/2 it will be different (https://github.com/yutakahirano/ws-over-http2 and http://tools.ietf.org/html/draft-hirano-httpbis-websocket-over-http2-01 will give some context for that - its still draft today though - but basically it will be a request to open a stream with ws[s] as the scheme).

So you're saying, that request is an HTTP request, so it hits the HTTP app, where we can accept it, or reject it, and after that it would need to be resubmitted to the websocket app. Which is perhaps quite ugly.

But that isn't what I meant. Consider websockets: it looks like HTTP, but its not an HTTP handshake. And its not a normal HTTP/2 request either: its clearly distinguished in both cases. So the server can unambiguously dispatch just once to the websocket app, and if we don't want to accept that protocol, that app can raise an error. At the wire level, you'd see the HTTP-like handshake from websockets (or an HTTP/2 stream open request) and then an error response send back. That seems to fit the use case of rejecting that 'upgrade' (but its not really an upgrade, because its a distinct protocol).

Now, the case where an HTTP/1.1 & 2 server that supports the 1.1 Upgrade-to-2 pathway. Lets assume that that sends a request to the HTTP app, and we want to accept it there. Once accepted, the connection changes from 1.1 to become version 2- our 'request' can't use the input stream or write bytes - unless its implementing HTTP/2 itself on top of that one connection (and since the output stream may be chunked or TE encoded, we can't actually do that). So we can't implement HTTP/2 as a request on top of HTTP/1.1 with anything that presents HTTP/1.1 bodies and requests. But there's a much easier way: let the server handle the upgrade, then reject the first request sent on HTTP/2 if the client doesn't meet your policy.

GrahamDumpleton commented 9 years ago

It is likely that only the 3rd idea will be feasible under Apache as something like mod_wsgi itself can't or wouldn't want to take on the responsibility to implement web sockets or HTTP/2. Instead separate Apache modules such as mod_websocket for HTTP/1.1 would be relied on for the heavy lifting, with a new mod_websocket_python module providing the bridge between the mod_websocket hooking mechanism and Python code.

It would appear to be relatively easy to implement a mod_websocket_python module even now in conjunction with the current mod_wsgi code base. That is, even if mod_wsgi itself wasn't being used to host a WSGI application, one would still load it into Apache and mod_websocket_python would use the mod_wsgi machinery for managing Python interpreters.

I don't know enough about mod_spdy to know whether it has a hooking mechanism for dynamic web applications so as to be able to achieve the same thing. Apache developers are only now starting to discuss a path forward for HTTP/2, so not much else is known right now as what one could do.

One problem with relying on separate Apache modules for the basic support for the protocols is that you need to run in mod_wsgi embedded mode. That is, within the actual Apache child worker processes and you couldn't run within the more desirable daemon mode, which provides a much safer and more controlled environment for Python web applications.

To run in daemon mode process of mod_wsgi is a much more complicated exercise because of how proxying to them currently follows the HTTP model of send all the request content before reading the response. It is hard to get around that limitation right now with how Apache internals work, with a reliance on threading, and may require waiting to see what internal changes are done in Apache to support HTTP/2 proxying, something Apache can't do right now due to limitations which prevent full duplex connections when proxying.

Even if one can get around this limitation and run Python applications requiring web sockets of HTTP/2 in mod_wsgi daemon mode processes, it would still be more practical to use the 3rd approach, even if it ends up dispatching off to a web socket or HTTP/2 handler written in Python.

In other words I don't see any benefit surfacing a user driven dispatch point at this time.

rbtcollins commented 9 years ago

BOSH in particular is interesting to consider: each HTTP request that makes up part of the BOSH system is equivalent to a single message in websockets - so we'd be pushing a /lot/ of complexity down into the app stack if we exposed each thing as a WSGI request. Additionally the routing complexity for sessions - which BOSH is - would be a nightmare if the server isn't aware (e.g. mod_wsgi in daemon mode, each BOSH message has to end up on the /same/ daemon process, or the WSGI app has to have its own inter-request handoff cross-process - or even cross-machine).

GrahamDumpleton commented 9 years ago

Sticky sessions in HTTP are horrid things.

Although you might be able to do it in a load balancer, falls apart very quickly if the load balancer is then proxying to a web server using multiple processes to handle the WSGI application and where the exact same process has to handle the subsequent request.