Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

http_protocol.py 16KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. import logging
  2. import time
  3. import traceback
  4. from urllib.parse import unquote
  5. from twisted.internet.defer import inlineCallbacks, maybeDeferred
  6. from twisted.internet.interfaces import IProtocolNegotiationFactory
  7. from twisted.protocols.policies import ProtocolWrapper
  8. from twisted.web import http
  9. from zope.interface import implementer
  10. from .utils import parse_x_forwarded_for
  11. logger = logging.getLogger(__name__)
  12. class WebRequest(http.Request):
  13. """
  14. Request that either hands off information to channels, or offloads
  15. to a WebSocket class.
  16. Does some extra processing over the normal Twisted Web request to separate
  17. GET and POST out.
  18. """
  19. error_template = (
  20. """
  21. <html>
  22. <head>
  23. <title>%(title)s</title>
  24. <style>
  25. body { font-family: sans-serif; margin: 0; padding: 0; }
  26. h1 { padding: 0.6em 0 0.2em 20px; color: #896868; margin: 0; }
  27. p { padding: 0 0 0.3em 20px; margin: 0; }
  28. footer { padding: 1em 0 0.3em 20px; color: #999; font-size: 80%%; font-style: italic; }
  29. </style>
  30. </head>
  31. <body>
  32. <h1>%(title)s</h1>
  33. <p>%(body)s</p>
  34. <footer>Daphne</footer>
  35. </body>
  36. </html>
  37. """.replace(
  38. "\n", ""
  39. )
  40. .replace(" ", " ")
  41. .replace(" ", " ")
  42. .replace(" ", " ")
  43. ) # Shorten it a bit, bytes wise
  44. def __init__(self, *args, **kwargs):
  45. self.client_addr = None
  46. self.server_addr = None
  47. try:
  48. http.Request.__init__(self, *args, **kwargs)
  49. # Easy server link
  50. self.server = self.channel.factory.server
  51. self.application_queue = None
  52. self._response_started = False
  53. self.server.protocol_connected(self)
  54. except Exception:
  55. logger.error(traceback.format_exc())
  56. raise
  57. ### Twisted progress callbacks
  58. @inlineCallbacks
  59. def process(self):
  60. try:
  61. self.request_start = time.time()
  62. # Get upgrade header
  63. upgrade_header = None
  64. if self.requestHeaders.hasHeader(b"Upgrade"):
  65. upgrade_header = self.requestHeaders.getRawHeaders(b"Upgrade")[0]
  66. # Get client address if possible
  67. if hasattr(self.client, "host") and hasattr(self.client, "port"):
  68. # client.host and host.host are byte strings in Python 2, but spec
  69. # requires unicode string.
  70. self.client_addr = [str(self.client.host), self.client.port]
  71. self.server_addr = [str(self.host.host), self.host.port]
  72. self.client_scheme = "https" if self.isSecure() else "http"
  73. # See if we need to get the address from a proxy header instead
  74. if self.server.proxy_forwarded_address_header:
  75. self.client_addr, self.client_scheme = parse_x_forwarded_for(
  76. self.requestHeaders,
  77. self.server.proxy_forwarded_address_header,
  78. self.server.proxy_forwarded_port_header,
  79. self.server.proxy_forwarded_proto_header,
  80. self.client_addr,
  81. self.client_scheme,
  82. )
  83. # Check for unicodeish path (or it'll crash when trying to parse)
  84. try:
  85. self.path.decode("ascii")
  86. except UnicodeDecodeError:
  87. self.path = b"/"
  88. self.basic_error(400, b"Bad Request", "Invalid characters in path")
  89. return
  90. # Calculate query string
  91. self.query_string = b""
  92. if b"?" in self.uri:
  93. self.query_string = self.uri.split(b"?", 1)[1]
  94. try:
  95. self.query_string.decode("ascii")
  96. except UnicodeDecodeError:
  97. self.basic_error(400, b"Bad Request", "Invalid query string")
  98. return
  99. # Is it WebSocket? IS IT?!
  100. if upgrade_header and upgrade_header.lower() == b"websocket":
  101. # Make WebSocket protocol to hand off to
  102. protocol = self.server.ws_factory.buildProtocol(
  103. self.transport.getPeer()
  104. )
  105. if not protocol:
  106. # If protocol creation fails, we signal "internal server error"
  107. self.setResponseCode(500)
  108. logger.warn("Could not make WebSocket protocol")
  109. self.finish()
  110. # Give it the raw query string
  111. protocol._raw_query_string = self.query_string
  112. # Port across transport
  113. transport, self.transport = self.transport, None
  114. if isinstance(transport, ProtocolWrapper):
  115. # i.e. TLS is a wrapping protocol
  116. transport.wrappedProtocol = protocol
  117. else:
  118. transport.protocol = protocol
  119. protocol.makeConnection(transport)
  120. # Re-inject request
  121. data = self.method + b" " + self.uri + b" HTTP/1.1\x0d\x0a"
  122. for h in self.requestHeaders.getAllRawHeaders():
  123. data += h[0] + b": " + b",".join(h[1]) + b"\x0d\x0a"
  124. data += b"\x0d\x0a"
  125. data += self.content.read()
  126. protocol.dataReceived(data)
  127. # Remove our HTTP reply channel association
  128. logger.debug("Upgraded connection %s to WebSocket", self.client_addr)
  129. self.server.protocol_disconnected(self)
  130. # Resume the producer so we keep getting data, if it's available as a method
  131. self.channel._networkProducer.resumeProducing()
  132. # Boring old HTTP.
  133. else:
  134. # Sanitize and decode headers, potentially extracting root path
  135. self.clean_headers = []
  136. self.root_path = self.server.root_path
  137. for name, values in self.requestHeaders.getAllRawHeaders():
  138. # Prevent CVE-2015-0219
  139. if b"_" in name:
  140. continue
  141. for value in values:
  142. if name.lower() == b"daphne-root-path":
  143. self.root_path = unquote(value.decode("ascii"))
  144. else:
  145. self.clean_headers.append((name.lower(), value))
  146. logger.debug("HTTP %s request for %s", self.method, self.client_addr)
  147. self.content.seek(0, 0)
  148. # Work out the application scope and create application
  149. self.application_queue = yield maybeDeferred(
  150. self.server.create_application,
  151. self,
  152. {
  153. "type": "http",
  154. # TODO: Correctly say if it's 1.1 or 1.0
  155. "http_version": self.clientproto.split(b"/")[-1].decode(
  156. "ascii"
  157. ),
  158. "method": self.method.decode("ascii"),
  159. "path": unquote(self.path.decode("ascii")),
  160. "raw_path": self.path,
  161. "root_path": self.root_path,
  162. "scheme": self.client_scheme,
  163. "query_string": self.query_string,
  164. "headers": self.clean_headers,
  165. "client": self.client_addr,
  166. "server": self.server_addr,
  167. },
  168. )
  169. # Check they didn't close an unfinished request
  170. if self.application_queue is None or self.content.closed:
  171. # Not much we can do, the request is prematurely abandoned.
  172. return
  173. # Run application against request
  174. buffer_size = self.server.request_buffer_size
  175. while True:
  176. chunk = self.content.read(buffer_size)
  177. more_body = not (len(chunk) < buffer_size)
  178. payload = {
  179. "type": "http.request",
  180. "body": chunk,
  181. "more_body": more_body,
  182. }
  183. self.application_queue.put_nowait(payload)
  184. if not more_body:
  185. break
  186. except Exception:
  187. logger.error(traceback.format_exc())
  188. self.basic_error(
  189. 500, b"Internal Server Error", "Daphne HTTP processing error"
  190. )
  191. def connectionLost(self, reason):
  192. """
  193. Cleans up reply channel on close.
  194. """
  195. if self.application_queue:
  196. self.send_disconnect()
  197. logger.debug("HTTP disconnect for %s", self.client_addr)
  198. http.Request.connectionLost(self, reason)
  199. self.server.protocol_disconnected(self)
  200. def finish(self):
  201. """
  202. Cleans up reply channel on close.
  203. """
  204. if self.application_queue:
  205. self.send_disconnect()
  206. logger.debug("HTTP close for %s", self.client_addr)
  207. http.Request.finish(self)
  208. self.server.protocol_disconnected(self)
  209. ### Server reply callbacks
  210. def handle_reply(self, message):
  211. """
  212. Handles a reply from the client
  213. """
  214. # Handle connections that are already closed
  215. if self.finished or self.channel is None:
  216. return
  217. # Check message validity
  218. if "type" not in message:
  219. raise ValueError("Message has no type defined")
  220. # Handle message
  221. if message["type"] == "http.response.start":
  222. if self._response_started:
  223. raise ValueError("HTTP response has already been started")
  224. self._response_started = True
  225. if "status" not in message:
  226. raise ValueError(
  227. "Specifying a status code is required for a Response message."
  228. )
  229. # Set HTTP status code
  230. self.setResponseCode(message["status"])
  231. # Write headers
  232. for header, value in message.get("headers", {}):
  233. self.responseHeaders.addRawHeader(header, value)
  234. if self.server.server_name and not self.responseHeaders.hasHeader("server"):
  235. self.setHeader(b"server", self.server.server_name.encode())
  236. logger.debug(
  237. "HTTP %s response started for %s", message["status"], self.client_addr
  238. )
  239. elif message["type"] == "http.response.body":
  240. if not self._response_started:
  241. raise ValueError(
  242. "HTTP response has not yet been started but got %s"
  243. % message["type"]
  244. )
  245. # Write out body
  246. http.Request.write(self, message.get("body", b""))
  247. # End if there's no more content
  248. if not message.get("more_body", False):
  249. self.finish()
  250. logger.debug("HTTP response complete for %s", self.client_addr)
  251. try:
  252. uri = self.uri.decode("ascii")
  253. except UnicodeDecodeError:
  254. # The path is malformed somehow - do our best to log something
  255. uri = repr(self.uri)
  256. try:
  257. self.server.log_action(
  258. "http",
  259. "complete",
  260. {
  261. "path": uri,
  262. "status": self.code,
  263. "method": self.method.decode("ascii", "replace"),
  264. "client": "%s:%s" % tuple(self.client_addr)
  265. if self.client_addr
  266. else None,
  267. "time_taken": self.duration(),
  268. "size": self.sentLength,
  269. },
  270. )
  271. except Exception:
  272. logger.error(traceback.format_exc())
  273. else:
  274. logger.debug("HTTP response chunk for %s", self.client_addr)
  275. else:
  276. raise ValueError("Cannot handle message type %s!" % message["type"])
  277. def handle_exception(self, exception):
  278. """
  279. Called by the server when our application tracebacks
  280. """
  281. self.basic_error(500, b"Internal Server Error", "Exception inside application.")
  282. def check_timeouts(self):
  283. """
  284. Called periodically to see if we should timeout something
  285. """
  286. # Web timeout checking
  287. if self.server.http_timeout and self.duration() > self.server.http_timeout:
  288. if self._response_started:
  289. logger.warning("Application timed out while sending response")
  290. self.finish()
  291. else:
  292. self.basic_error(
  293. 503,
  294. b"Service Unavailable",
  295. "Application failed to respond within time limit.",
  296. )
  297. ### Utility functions
  298. def send_disconnect(self):
  299. """
  300. Sends a http.disconnect message.
  301. Useful only really for long-polling.
  302. """
  303. # If we don't yet have a path, then don't send as we never opened.
  304. if self.path:
  305. self.application_queue.put_nowait({"type": "http.disconnect"})
  306. def duration(self):
  307. """
  308. Returns the time since the start of the request.
  309. """
  310. if not hasattr(self, "request_start"):
  311. return 0
  312. return time.time() - self.request_start
  313. def basic_error(self, status, status_text, body):
  314. """
  315. Responds with a server-level error page (very basic)
  316. """
  317. self.handle_reply(
  318. {
  319. "type": "http.response.start",
  320. "status": status,
  321. "headers": [(b"Content-Type", b"text/html; charset=utf-8")],
  322. }
  323. )
  324. self.handle_reply(
  325. {
  326. "type": "http.response.body",
  327. "body": (
  328. self.error_template
  329. % {
  330. "title": str(status) + " " + status_text.decode("ascii"),
  331. "body": body,
  332. }
  333. ).encode("utf8"),
  334. }
  335. )
  336. def __hash__(self):
  337. return hash(id(self))
  338. def __eq__(self, other):
  339. return id(self) == id(other)
  340. @implementer(IProtocolNegotiationFactory)
  341. class HTTPFactory(http.HTTPFactory):
  342. """
  343. Factory which takes care of tracking which protocol
  344. instances or request instances are responsible for which
  345. named response channels, so incoming messages can be
  346. routed appropriately.
  347. """
  348. def __init__(self, server):
  349. http.HTTPFactory.__init__(self)
  350. self.server = server
  351. def buildProtocol(self, addr):
  352. """
  353. Builds protocol instances. This override is used to ensure we use our
  354. own Request object instead of the default.
  355. """
  356. try:
  357. protocol = http.HTTPFactory.buildProtocol(self, addr)
  358. protocol.requestFactory = WebRequest
  359. return protocol
  360. except Exception:
  361. logger.error("Cannot build protocol: %s" % traceback.format_exc())
  362. raise
  363. # IProtocolNegotiationFactory
  364. def acceptableProtocols(self):
  365. """
  366. Protocols this server can speak after ALPN negotiation. Currently that
  367. is HTTP/1.1 and optionally HTTP/2. Websockets cannot be negotiated
  368. using ALPN, so that doesn't go here: anyone wanting websockets will
  369. negotiate HTTP/1.1 and then do the upgrade dance.
  370. """
  371. baseProtocols = [b"http/1.1"]
  372. if http.H2_ENABLED:
  373. baseProtocols.insert(0, b"h2")
  374. return baseProtocols