r46999 - Merge httpchannel-proxy-8193-3: Add proxy HTTPChannel object to allow swapping of channel after connection establishment
hawkowl-TA+aISz0psMTMxyoc4vAAJOcrHinNvQL0E9HWUfgJXw@public.gmane.org Tue, 15 Mar 2016 09:18:36 -0600 (MDT)
| Newsgroups | gmane.comp.python.twisted.commits |
|---|---|
| Message-ID | <[email protected]> |
Author: hawkowl
Date: Tue Mar 15 09:18:31 2016
New Revision: 46999
Added:
trunk/twisted/topfiles/8193.misc
Modified:
trunk/twisted/web/http.py
trunk/twisted/web/test/test_http.py
trunk/twisted/web/test/test_web.py
Log:
Merge httpchannel-proxy-8193-3: Add proxy HTTPChannel object to allow swapping of channel after connection establishment
Author: lukasa
Reviewers: glyph, adiroiban, hawkowl
Fixes: #8193
Modified: trunk/twisted/web/http.py
==============================================================================
--- trunk/twisted/web/http.py (original)
+++ trunk/twisted/web/http.py Tue Mar 15 09:18:31 2016
@@ -97,11 +97,14 @@
from twisted.python.components import proxyForInterface
from twisted.internet import interfaces, reactor, protocol, address
from twisted.internet.defer import Deferred
+from twisted.internet.interfaces import IProtocol
from twisted.protocols import policies, basic
from twisted.web.iweb import IRequest, IAccessLogFormatter
from twisted.web.http_headers import Headers
+H2_ENABLED = False
+
from twisted.web._responses import (
SWITCHING,
@@ -1978,6 +1981,92 @@
+class _GenericHTTPChannelProtocol(proxyForInterface(IProtocol, "_channel")):
+ """
+ A proxy object that wraps one of the HTTP protocol objects, and switches
+ between them depending on TLS negotiated protocol.
+
+ @ivar _negotiatedProtocol: The protocol negotiated with ALPN or NPN, if
+ any.
+ @type _negotiatedProtocol: Either a bytestring containing the ALPN token
+ for the negotiated protocol, or C{None} if no protocol has yet been
+ negotiated.
+
+ @ivar _channel: The object capable of behaving like a L{HTTPChannel} that
+ is backing this object. By default this is a L{HTTPChannel}, but if a
+ HTTP protocol upgrade takes place this may be a different channel
+ object. Must implement L{IProtocol}.
+ @type _channel: L{HTTPChannel}
+
+ @ivar _requestFactory: A callable to use to build L{IRequest} objects.
+ @type _requestFactory: L{IRequest}
+
+ @ivar _site: A reference to the creating L{twisted.web.server.Site} object.
+ @type _site: L{twisted.web.server.Site}
+ """
+ _negotiatedProtocol = None
+ _requestFactory = Request
+ _site = None
+
+
+ @property
+ def requestFactory(self):
+ return self._channel.requestFactory
+
+
+ @requestFactory.setter
+ def requestFactory(self, value):
+ self._requestFactory = value
+ self._channel.requestFactory = value
+
+
+ @property
+ def site(self):
+ return self._channel.site
+
+
+ @site.setter
+ def site(self, value):
+ self._site = value
+ self._channel.site = value
+
+
+ def dataReceived(self, data):
+ """
+ A override of L{IProtocol.dataReceived} that checks what protocol we're
+ using.
+ """
+ if self._negotiatedProtocol is None:
+ try:
+ negotiatedProtocol = self._channel.transport.negotiatedProtocol
+ except AttributeError:
+ # Plaintext HTTP, always HTTP/1.1
+ negotiatedProtocol = b'http/1.1'
+
+ if negotiatedProtocol is None:
+ negotiatedProtocol = b'http/1.1'
+
+ if negotiatedProtocol == b'h2':
+ assert H2_ENABLED, "Cannot negotiate HTTP/2 without support."
+ else:
+ # Only HTTP/2 and HTTP/1.1 are supported right now.
+ assert negotiatedProtocol == b'http/1.1', \
+ "Unsupported protocol negotiated"
+
+ self._negotiatedProtocol = negotiatedProtocol
+
+ return self._channel.dataReceived(data)
+
+
+
+def _genericHTTPChannelProtocolFactory(self):
+ """
+ Returns an appropriately initialized _GenericHTTPChannelProtocol.
+ """
+ return _GenericHTTPChannelProtocol(HTTPChannel())
+
+
+
class HTTPFactory(protocol.ServerFactory):
"""
Factory for HTTP server.
@@ -2001,7 +2090,7 @@
timestamps.
"""
- protocol = HTTPChannel
+ protocol = _genericHTTPChannelProtocolFactory
logPath = None
Modified: trunk/twisted/web/test/test_http.py
==============================================================================
--- trunk/twisted/web/test/test_http.py (original)
+++ trunk/twisted/web/test/test_http.py Tue Mar 15 09:18:31 2016
@@ -235,6 +235,94 @@
self.assertEqual(response, expectedResponse)
+
+class ProtocolNegotiationTests(unittest.TestCase):
+ requests = (
+ b"GET / HTTP/1.1\r\n"
+ b"Accept: text/html\r\n"
+ b"Connection: close\r\n"
+ b"\r\n"
+ b"GET / HTTP/1.0\r\n"
+ b"\r\n")
+
+
+ def _negotiatedProtocolForTransportInstance(self, t):
+ """
+ Run a request using the specific instance of a transport. Returns the
+ negotiated protocol string.
+ """
+ a = http._genericHTTPChannelProtocolFactory(b'')
+ a.requestFactory = DummyHTTPHandler
+ a.makeConnection(t)
+ # one byte at a time, to stress it.
+ for byte in iterbytes(self.requests):
+ a.dataReceived(byte)
+ a.connectionLost(IOError("all done"))
+ return a._negotiatedProtocol
+
+
+ def test_protocolUnspecified(self):
+ """
+ If the transport has no support for protocol negotiation (no
+ negotiatedProtocol attribute), HTTP/1.1 is assumed.
+ """
+ b = StringTransport()
+ negotiatedProtocol = self._negotiatedProtocolForTransportInstance(b)
+ self.assertEqual(negotiatedProtocol, b'http/1.1')
+
+
+ def test_protocolNone(self):
+ """
+ If the transport has no support for protocol negotiation (returns None
+ for negotiatedProtocol), HTTP/1.1 is assumed.
+ """
+ b = StringTransport()
+ b.negotiatedProtocol = None
+ negotiatedProtocol = self._negotiatedProtocolForTransportInstance(b)
+ self.assertEqual(negotiatedProtocol, b'http/1.1')
+
+
+ def test_http11(self):
+ """
+ If the transport reports that HTTP/1.1 is negotiated, that's what's
+ negotiated.
+ """
+ b = StringTransport()
+ b.negotiatedProtocol = b'http/1.1'
+ negotiatedProtocol = self._negotiatedProtocolForTransportInstance(b)
+ self.assertEqual(negotiatedProtocol, b'http/1.1')
+
+
+ def test_http2(self):
+ """
+ If the transport reports that HTTP/2 is negotiated, that's what's
+ negotiated. Currently HTTP/2 is unsupported, so this raises an
+ AssertionError.
+ """
+ b = StringTransport()
+ b.negotiatedProtocol = b'h2'
+ self.assertRaises(
+ AssertionError,
+ self._negotiatedProtocolForTransportInstance,
+ b,
+ )
+
+
+ def test_unknownProtocol(self):
+ """
+ If the transport reports that a protocol other than HTTP/1.1 or HTTP/2
+ is negotiated, an error occurs.
+ """
+ b = StringTransport()
+ b.negotiatedProtocol = b'smtp'
+ self.assertRaises(
+ AssertionError,
+ self._negotiatedProtocolForTransportInstance,
+ b,
+ )
+
+
+
class HTTPLoopbackTests(unittest.TestCase):
expectedHeaders = {b'request': b'/foo/bar',
Modified: trunk/twisted/web/test/test_web.py
==============================================================================
--- trunk/twisted/web/test/test_web.py (original)
+++ trunk/twisted/web/test/test_web.py Tue Mar 15 09:18:31 2016
@@ -320,7 +320,7 @@
else:
validator = b"If-Not-Match: " + etag
for line in [b"GET / HTTP/1.1", validator, b""]:
- self.channel.lineReceived(line)
+ self.channel.dataReceived(line + b'\r\n')
result = self.transport.getvalue()
self.assertEqual(httpCode(result), http.OK)
self.assertEqual(httpBody(result), b"correct")
@@ -346,7 +346,7 @@
"""
for line in [b"GET / HTTP/1.1",
b"If-Modified-Since: " + http.datetimeToString(100), b""]:
- self.channel.lineReceived(line)
+ self.channel.dataReceived(line + b'\r\n')
result = self.transport.getvalue()
self.assertEqual(httpCode(result), http.NOT_MODIFIED)
self.assertEqual(httpBody(result), b"")
@@ -414,7 +414,7 @@
with an empty response body.
"""
for line in [b"GET / HTTP/1.1", b"If-None-Match: MatchingTag", b""]:
- self.channel.lineReceived(line)
+ self.channel.dataReceived(line + b'\r\n')
result = self.transport.getvalue()
self.assertEqual(httpHeader(result, b"ETag"), b"MatchingTag")
self.assertEqual(httpCode(result), http.NOT_MODIFIED)
@@ -432,7 +432,7 @@
"""
for line in [b"GET /with-content-type HTTP/1.1",
b"If-None-Match: MatchingTag", b""]:
- self.channel.lineReceived(line)
+ self.channel.dataReceived(line + b'\r\n')
result = self.transport.getvalue()
self.assertEqual(httpCode(result), http.NOT_MODIFIED)
self.assertEqual(httpBody(result), b"")
@@ -1311,7 +1311,7 @@
"""
self.site._logDateTime = "[%02d/%3s/%4d:%02d:%02d:%02d +0000]" % (
25, 'Oct', 2004, 12, 31, 59)
- self.request.requestHeaders.addRawHeader(b'user-agent',
+ self.request.requestHeaders.addRawHeader(b'user-agent',
b'Malicious Web" Evil')
self.assertLogs(
b'"1.2.3.4" - - [25/Oct/2004:12:31:59 +0000] '