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.

ws_protocol.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. import logging
  2. import time
  3. import traceback
  4. from urllib.parse import unquote
  5. from autobahn.twisted.websocket import (
  6. ConnectionDeny,
  7. WebSocketServerFactory,
  8. WebSocketServerProtocol,
  9. )
  10. from twisted.internet import defer
  11. from .utils import parse_x_forwarded_for
  12. logger = logging.getLogger(__name__)
  13. class WebSocketProtocol(WebSocketServerProtocol):
  14. """
  15. Protocol which supports WebSockets and forwards incoming messages to
  16. the websocket channels.
  17. """
  18. application_type = "websocket"
  19. # If we should send no more messages (e.g. we error-closed the socket)
  20. muted = False
  21. def onConnect(self, request):
  22. self.server = self.factory.server_class
  23. self.server.protocol_connected(self)
  24. self.request = request
  25. self.protocol_to_accept = None
  26. self.socket_opened = time.time()
  27. self.last_ping = time.time()
  28. try:
  29. # Sanitize and decode headers
  30. self.clean_headers = []
  31. for name, value in request.headers.items():
  32. name = name.encode("ascii")
  33. # Prevent CVE-2015-0219
  34. if b"_" in name:
  35. continue
  36. self.clean_headers.append((name.lower(), value.encode("latin1")))
  37. # Get client address if possible
  38. peer = self.transport.getPeer()
  39. host = self.transport.getHost()
  40. if hasattr(peer, "host") and hasattr(peer, "port"):
  41. self.client_addr = [str(peer.host), peer.port]
  42. self.server_addr = [str(host.host), host.port]
  43. else:
  44. self.client_addr = None
  45. self.server_addr = None
  46. if self.server.proxy_forwarded_address_header:
  47. self.client_addr, self.client_scheme = parse_x_forwarded_for(
  48. dict(self.clean_headers),
  49. self.server.proxy_forwarded_address_header,
  50. self.server.proxy_forwarded_port_header,
  51. self.server.proxy_forwarded_proto_header,
  52. self.client_addr,
  53. )
  54. # Decode websocket subprotocol options
  55. subprotocols = []
  56. for header, value in self.clean_headers:
  57. if header == b"sec-websocket-protocol":
  58. subprotocols = [
  59. x.strip() for x in unquote(value.decode("ascii")).split(",")
  60. ]
  61. # Make new application instance with scope
  62. self.path = request.path.encode("ascii")
  63. self.application_deferred = defer.maybeDeferred(
  64. self.server.create_application,
  65. self,
  66. {
  67. "type": "websocket",
  68. "path": unquote(self.path.decode("ascii")),
  69. "raw_path": self.path,
  70. "headers": self.clean_headers,
  71. "query_string": self._raw_query_string, # Passed by HTTP protocol
  72. "client": self.client_addr,
  73. "server": self.server_addr,
  74. "subprotocols": subprotocols,
  75. },
  76. )
  77. if self.application_deferred is not None:
  78. self.application_deferred.addCallback(self.applicationCreateWorked)
  79. self.application_deferred.addErrback(self.applicationCreateFailed)
  80. except Exception:
  81. # Exceptions here are not displayed right, just 500.
  82. # Turn them into an ERROR log.
  83. logger.error(traceback.format_exc())
  84. raise
  85. # Make a deferred and return it - we'll either call it or err it later on
  86. self.handshake_deferred = defer.Deferred()
  87. return self.handshake_deferred
  88. def applicationCreateWorked(self, application_queue):
  89. """
  90. Called when the background thread has successfully made the application
  91. instance.
  92. """
  93. # Store the application's queue
  94. self.application_queue = application_queue
  95. # Send over the connect message
  96. self.application_queue.put_nowait({"type": "websocket.connect"})
  97. self.server.log_action(
  98. "websocket",
  99. "connecting",
  100. {
  101. "path": self.request.path,
  102. "client": "%s:%s" % tuple(self.client_addr)
  103. if self.client_addr
  104. else None,
  105. },
  106. )
  107. def applicationCreateFailed(self, failure):
  108. """
  109. Called when application creation fails.
  110. """
  111. logger.error(failure)
  112. return failure
  113. ### Twisted event handling
  114. def onOpen(self):
  115. # Send news that this channel is open
  116. logger.debug("WebSocket %s open and established", self.client_addr)
  117. self.server.log_action(
  118. "websocket",
  119. "connected",
  120. {
  121. "path": self.request.path,
  122. "client": "%s:%s" % tuple(self.client_addr)
  123. if self.client_addr
  124. else None,
  125. },
  126. )
  127. def onMessage(self, payload, isBinary):
  128. # If we're muted, do nothing.
  129. if self.muted:
  130. logger.debug("Muting incoming frame on %s", self.client_addr)
  131. return
  132. logger.debug("WebSocket incoming frame on %s", self.client_addr)
  133. self.last_ping = time.time()
  134. if isBinary:
  135. self.application_queue.put_nowait(
  136. {"type": "websocket.receive", "bytes": payload}
  137. )
  138. else:
  139. self.application_queue.put_nowait(
  140. {"type": "websocket.receive", "text": payload.decode("utf8")}
  141. )
  142. def onClose(self, wasClean, code, reason):
  143. """
  144. Called when Twisted closes the socket.
  145. """
  146. self.server.protocol_disconnected(self)
  147. logger.debug("WebSocket closed for %s", self.client_addr)
  148. if not self.muted and hasattr(self, "application_queue"):
  149. self.application_queue.put_nowait(
  150. {"type": "websocket.disconnect", "code": code}
  151. )
  152. self.server.log_action(
  153. "websocket",
  154. "disconnected",
  155. {
  156. "path": self.request.path,
  157. "client": "%s:%s" % tuple(self.client_addr)
  158. if self.client_addr
  159. else None,
  160. },
  161. )
  162. ### Internal event handling
  163. def handle_reply(self, message):
  164. if "type" not in message:
  165. raise ValueError("Message has no type defined")
  166. if message["type"] == "websocket.accept":
  167. self.serverAccept(message.get("subprotocol", None))
  168. elif message["type"] == "websocket.close":
  169. if self.state == self.STATE_CONNECTING:
  170. self.serverReject()
  171. else:
  172. self.serverClose(code=message.get("code", None))
  173. elif message["type"] == "websocket.send":
  174. if self.state == self.STATE_CONNECTING:
  175. raise ValueError("Socket has not been accepted, so cannot send over it")
  176. if message.get("bytes", None) and message.get("text", None):
  177. raise ValueError(
  178. "Got invalid WebSocket reply message on %s - contains both bytes and text keys"
  179. % (message,)
  180. )
  181. if message.get("bytes", None):
  182. self.serverSend(message["bytes"], True)
  183. if message.get("text", None):
  184. self.serverSend(message["text"], False)
  185. def handle_exception(self, exception):
  186. """
  187. Called by the server when our application tracebacks
  188. """
  189. if hasattr(self, "handshake_deferred"):
  190. # If the handshake is still ongoing, we need to emit a HTTP error
  191. # code rather than a WebSocket one.
  192. self.handshake_deferred.errback(
  193. ConnectionDeny(code=500, reason="Internal server error")
  194. )
  195. else:
  196. self.sendCloseFrame(code=1011)
  197. def serverAccept(self, subprotocol=None):
  198. """
  199. Called when we get a message saying to accept the connection.
  200. """
  201. self.handshake_deferred.callback(subprotocol)
  202. del self.handshake_deferred
  203. logger.debug("WebSocket %s accepted by application", self.client_addr)
  204. def serverReject(self):
  205. """
  206. Called when we get a message saying to reject the connection.
  207. """
  208. self.handshake_deferred.errback(
  209. ConnectionDeny(code=403, reason="Access denied")
  210. )
  211. del self.handshake_deferred
  212. self.server.protocol_disconnected(self)
  213. logger.debug("WebSocket %s rejected by application", self.client_addr)
  214. self.server.log_action(
  215. "websocket",
  216. "rejected",
  217. {
  218. "path": self.request.path,
  219. "client": "%s:%s" % tuple(self.client_addr)
  220. if self.client_addr
  221. else None,
  222. },
  223. )
  224. def serverSend(self, content, binary=False):
  225. """
  226. Server-side channel message to send a message.
  227. """
  228. if self.state == self.STATE_CONNECTING:
  229. self.serverAccept()
  230. logger.debug("Sent WebSocket packet to client for %s", self.client_addr)
  231. if binary:
  232. self.sendMessage(content, binary)
  233. else:
  234. self.sendMessage(content.encode("utf8"), binary)
  235. def serverClose(self, code=None):
  236. """
  237. Server-side channel message to close the socket
  238. """
  239. code = 1000 if code is None else code
  240. self.sendClose(code=code)
  241. ### Utils
  242. def duration(self):
  243. """
  244. Returns the time since the socket was opened
  245. """
  246. return time.time() - self.socket_opened
  247. def check_timeouts(self):
  248. """
  249. Called periodically to see if we should timeout something
  250. """
  251. # Web timeout checking
  252. if (
  253. self.duration() > self.server.websocket_timeout
  254. and self.server.websocket_timeout >= 0
  255. ):
  256. self.serverClose()
  257. # Ping check
  258. # If we're still connecting, deny the connection
  259. if self.state == self.STATE_CONNECTING:
  260. if self.duration() > self.server.websocket_connect_timeout:
  261. self.serverReject()
  262. elif self.state == self.STATE_OPEN:
  263. if (time.time() - self.last_ping) > self.server.ping_interval:
  264. self._sendAutoPing()
  265. self.last_ping = time.time()
  266. def __hash__(self):
  267. return hash(id(self))
  268. def __eq__(self, other):
  269. return id(self) == id(other)
  270. def __repr__(self):
  271. return f"<WebSocketProtocol client={self.client_addr!r} path={self.path!r}>"
  272. class WebSocketFactory(WebSocketServerFactory):
  273. """
  274. Factory subclass that remembers what the "main"
  275. factory is, so WebSocket protocols can access it
  276. to get reply ID info.
  277. """
  278. protocol = WebSocketProtocol
  279. def __init__(self, server_class, *args, **kwargs):
  280. self.server_class = server_class
  281. WebSocketServerFactory.__init__(self, *args, **kwargs)
  282. def buildProtocol(self, addr):
  283. """
  284. Builds protocol instances. We use this to inject the factory object into the protocol.
  285. """
  286. try:
  287. protocol = super().buildProtocol(addr)
  288. protocol.factory = self
  289. return protocol
  290. except Exception:
  291. logger.error("Cannot build protocol: %s" % traceback.format_exc())
  292. raise