Fixed #12: Crash on receiving high byte in path

This commit is contained in:
Andrew Godwin 2016-05-18 10:08:15 -07:00
parent 81d99a34d3
commit d786329abb

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
import logging import logging
import six import six
import time import time
import traceback
from six.moves.urllib_parse import unquote from six.moves.urllib_parse import unquote
from twisted.protocols.policies import ProtocolWrapper from twisted.protocols.policies import ProtocolWrapper
@ -53,56 +54,12 @@ class WebRequest(http.Request):
self._got_response_start = False self._got_response_start = False
def process(self): def process(self):
self.request_start = time.time() try:
# Get upgrade header self.request_start = time.time()
upgrade_header = None # Get upgrade header
if self.requestHeaders.hasHeader(b"Upgrade"): upgrade_header = None
upgrade_header = self.requestHeaders.getRawHeaders(b"Upgrade")[0] if self.requestHeaders.hasHeader(b"Upgrade"):
# Calculate query string upgrade_header = self.requestHeaders.getRawHeaders(b"Upgrade")[0]
self.query_string = b""
if b"?" in self.uri:
self.query_string = self.uri.split(b"?", 1)[1]
# Is it WebSocket? IS IT?!
if upgrade_header and upgrade_header.lower() == b"websocket":
# Make WebSocket protocol to hand off to
protocol = self.factory.ws_factory.buildProtocol(self.transport.getPeer())
if not protocol:
# If protocol creation fails, we signal "internal server error"
self.setResponseCode(500)
logger.warn("Could not make WebSocket protocol")
self.finish()
# Port across transport
protocol.set_main_factory(self.factory)
transport, self.transport = self.transport, None
if isinstance(transport, ProtocolWrapper):
# i.e. TLS is a wrapping protocol
transport.wrappedProtocol = protocol
else:
transport.protocol = protocol
protocol.makeConnection(transport)
# Re-inject request
data = self.method + b' ' + self.uri + b' HTTP/1.1\x0d\x0a'
for h in self.requestHeaders.getAllRawHeaders():
data += h[0] + b': ' + b",".join(h[1]) + b'\x0d\x0a'
data += b"\x0d\x0a"
data += self.content.read()
protocol.dataReceived(data)
# Remove our HTTP reply channel association
logger.debug("Upgraded connection %s to WebSocket %s", self.reply_channel, protocol.reply_channel)
del self.factory.reply_protocols[self.reply_channel]
self.reply_channel = None
# Boring old HTTP.
else:
# Sanitize and decode headers
self.clean_headers = []
for name, values in self.requestHeaders.getAllRawHeaders():
# Prevent CVE-2015-0219
if b"_" in name:
continue
for value in values:
self.clean_headers.append((name.lower(), value))
logger.debug("HTTP %s request for %s", self.method, self.reply_channel)
self.content.seek(0, 0)
# Get client address if possible # Get client address if possible
if hasattr(self.client, "host") and hasattr(self.client, "port"): if hasattr(self.client, "host") and hasattr(self.client, "port"):
self.client_addr = [self.client.host, self.client.port] self.client_addr = [self.client.host, self.client.port]
@ -110,24 +67,79 @@ class WebRequest(http.Request):
else: else:
self.client_addr = None self.client_addr = None
self.server_addr = None self.server_addr = None
# Send message # Check for unicodeish path (or it'll crash when trying to parse)
try: try:
self.factory.channel_layer.send("http.request", { self.path.decode("ascii")
"reply_channel": self.reply_channel, except UnicodeDecodeError:
# TODO: Correctly say if it's 1.1 or 1.0 self.path = b"/"
"http_version": "1.1", self.basic_error(400, b"Bad Request", "Invalid characters in path")
"method": self.method.decode("ascii"), return
"path": self.unquote(self.path), # Calculate query string
"scheme": "http", self.query_string = b""
"query_string": self.unquote(self.query_string), if b"?" in self.uri:
"headers": self.clean_headers, self.query_string = self.uri.split(b"?", 1)[1]
"body": self.content.read(), # Is it WebSocket? IS IT?!
"client": self.client_addr, if upgrade_header and upgrade_header.lower() == b"websocket":
"server": self.server_addr, # Make WebSocket protocol to hand off to
}) protocol = self.factory.ws_factory.buildProtocol(self.transport.getPeer())
except self.factory.channel_layer.ChannelFull: if not protocol:
# Channel is too full; reject request with 503 # If protocol creation fails, we signal "internal server error"
self.basic_error(503, b"Service Unavailable", "Request queue full.") self.setResponseCode(500)
logger.warn("Could not make WebSocket protocol")
self.finish()
# Port across transport
protocol.set_main_factory(self.factory)
transport, self.transport = self.transport, None
if isinstance(transport, ProtocolWrapper):
# i.e. TLS is a wrapping protocol
transport.wrappedProtocol = protocol
else:
transport.protocol = protocol
protocol.makeConnection(transport)
# Re-inject request
data = self.method + b' ' + self.uri + b' HTTP/1.1\x0d\x0a'
for h in self.requestHeaders.getAllRawHeaders():
data += h[0] + b': ' + b",".join(h[1]) + b'\x0d\x0a'
data += b"\x0d\x0a"
data += self.content.read()
protocol.dataReceived(data)
# Remove our HTTP reply channel association
logger.debug("Upgraded connection %s to WebSocket %s", self.reply_channel, protocol.reply_channel)
del self.factory.reply_protocols[self.reply_channel]
self.reply_channel = None
# Boring old HTTP.
else:
# Sanitize and decode headers
self.clean_headers = []
for name, values in self.requestHeaders.getAllRawHeaders():
# Prevent CVE-2015-0219
if b"_" in name:
continue
for value in values:
self.clean_headers.append((name.lower(), value))
logger.debug("HTTP %s request for %s", self.method, self.reply_channel)
self.content.seek(0, 0)
# Send message
try:
self.factory.channel_layer.send("http.request", {
"reply_channel": self.reply_channel,
# TODO: Correctly say if it's 1.1 or 1.0
"http_version": "1.1",
"method": self.method.decode("ascii"),
"path": self.unquote(self.path),
"scheme": "http",
"query_string": self.unquote(self.query_string),
"headers": self.clean_headers,
"body": self.content.read(),
"client": self.client_addr,
"server": self.server_addr,
})
except self.factory.channel_layer.ChannelFull:
# Channel is too full; reject request with 503
self.basic_error(503, b"Service Unavailable", "Request queue full.")
except Exception as e:
logger.error(traceback.format_exc())
self.basic_error(500, b"Internal Server Error", "HTTP processing error")
@classmethod @classmethod
def unquote(cls, value): def unquote(cls, value):
@ -195,14 +207,17 @@ class WebRequest(http.Request):
if not message.get("more_content", False): if not message.get("more_content", False):
self.finish() self.finish()
logger.debug("HTTP response complete for %s", self.reply_channel) logger.debug("HTTP response complete for %s", self.reply_channel)
self.factory.log_action("http", "complete", { try:
"path": self.path.decode("ascii"), self.factory.log_action("http", "complete", {
"status": self.code, "path": self.path.decode("ascii"),
"method": self.method.decode("ascii"), "status": self.code,
"client": "%s:%s" % tuple(self.client_addr) if self.client_addr else None, "method": self.method.decode("ascii"),
"time_taken": self.duration(), "client": "%s:%s" % tuple(self.client_addr) if self.client_addr else None,
"size": self.sentLength, "time_taken": self.duration(),
}) "size": self.sentLength,
})
except Exception as e:
logging.error(traceback.format_exc())
else: else:
logger.debug("HTTP response chunk for %s", self.reply_channel) logger.debug("HTTP response chunk for %s", self.reply_channel)