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.

_handshake.py 6.6KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. """
  2. _handshake.py
  3. websocket - WebSocket client library for Python
  4. Copyright 2023 engn33r
  5. Licensed under the Apache License, Version 2.0 (the "License");
  6. you may not use this file except in compliance with the License.
  7. You may obtain a copy of the License at
  8. http://www.apache.org/licenses/LICENSE-2.0
  9. Unless required by applicable law or agreed to in writing, software
  10. distributed under the License is distributed on an "AS IS" BASIS,
  11. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. See the License for the specific language governing permissions and
  13. limitations under the License.
  14. """
  15. import hashlib
  16. import hmac
  17. import os
  18. from base64 import encodebytes as base64encode
  19. from http import client as HTTPStatus
  20. from ._cookiejar import SimpleCookieJar
  21. from ._exceptions import *
  22. from ._http import *
  23. from ._logging import *
  24. from ._socket import *
  25. __all__ = ["handshake_response", "handshake", "SUPPORTED_REDIRECT_STATUSES"]
  26. # websocket supported version.
  27. VERSION = 13
  28. SUPPORTED_REDIRECT_STATUSES = (HTTPStatus.MOVED_PERMANENTLY, HTTPStatus.FOUND, HTTPStatus.SEE_OTHER,)
  29. SUCCESS_STATUSES = SUPPORTED_REDIRECT_STATUSES + (HTTPStatus.SWITCHING_PROTOCOLS,)
  30. CookieJar = SimpleCookieJar()
  31. class handshake_response:
  32. def __init__(self, status: int, headers: dict, subprotocol):
  33. self.status = status
  34. self.headers = headers
  35. self.subprotocol = subprotocol
  36. CookieJar.add(headers.get("set-cookie"))
  37. def handshake(sock, url: str, hostname: str, port: int, resource: str, **options):
  38. headers, key = _get_handshake_headers(resource, url, hostname, port, options)
  39. header_str = "\r\n".join(headers)
  40. send(sock, header_str)
  41. dump("request header", header_str)
  42. status, resp = _get_resp_headers(sock)
  43. if status in SUPPORTED_REDIRECT_STATUSES:
  44. return handshake_response(status, resp, None)
  45. success, subproto = _validate(resp, key, options.get("subprotocols"))
  46. if not success:
  47. raise WebSocketException("Invalid WebSocket Header")
  48. return handshake_response(status, resp, subproto)
  49. def _pack_hostname(hostname: str) -> str:
  50. # IPv6 address
  51. if ':' in hostname:
  52. return '[' + hostname + ']'
  53. return hostname
  54. def _get_handshake_headers(resource: str, url: str, host: str, port: int, options: dict):
  55. headers = [
  56. "GET {resource} HTTP/1.1".format(resource=resource),
  57. "Upgrade: websocket"
  58. ]
  59. if port == 80 or port == 443:
  60. hostport = _pack_hostname(host)
  61. else:
  62. hostport = "{h}:{p}".format(h=_pack_hostname(host), p=port)
  63. if options.get("host"):
  64. headers.append("Host: {h}".format(h=options["host"]))
  65. else:
  66. headers.append("Host: {hp}".format(hp=hostport))
  67. # scheme indicates whether http or https is used in Origin
  68. # The same approach is used in parse_url of _url.py to set default port
  69. scheme, url = url.split(":", 1)
  70. if not options.get("suppress_origin"):
  71. if "origin" in options and options["origin"] is not None:
  72. headers.append("Origin: {origin}".format(origin=options["origin"]))
  73. elif scheme == "wss":
  74. headers.append("Origin: https://{hp}".format(hp=hostport))
  75. else:
  76. headers.append("Origin: http://{hp}".format(hp=hostport))
  77. key = _create_sec_websocket_key()
  78. # Append Sec-WebSocket-Key & Sec-WebSocket-Version if not manually specified
  79. if not options.get('header') or 'Sec-WebSocket-Key' not in options['header']:
  80. headers.append("Sec-WebSocket-Key: {key}".format(key=key))
  81. else:
  82. key = options['header']['Sec-WebSocket-Key']
  83. if not options.get('header') or 'Sec-WebSocket-Version' not in options['header']:
  84. headers.append("Sec-WebSocket-Version: {version}".format(version=VERSION))
  85. if not options.get('connection'):
  86. headers.append('Connection: Upgrade')
  87. else:
  88. headers.append(options['connection'])
  89. subprotocols = options.get("subprotocols")
  90. if subprotocols:
  91. headers.append("Sec-WebSocket-Protocol: {protocols}".format(protocols=",".join(subprotocols)))
  92. header = options.get("header")
  93. if header:
  94. if isinstance(header, dict):
  95. header = [
  96. ": ".join([k, v])
  97. for k, v in header.items()
  98. if v is not None
  99. ]
  100. headers.extend(header)
  101. server_cookie = CookieJar.get(host)
  102. client_cookie = options.get("cookie", None)
  103. cookie = "; ".join(filter(None, [server_cookie, client_cookie]))
  104. if cookie:
  105. headers.append("Cookie: {cookie}".format(cookie=cookie))
  106. headers.extend(("", ""))
  107. return headers, key
  108. def _get_resp_headers(sock, success_statuses: tuple = SUCCESS_STATUSES) -> tuple:
  109. status, resp_headers, status_message = read_headers(sock)
  110. if status not in success_statuses:
  111. content_len = resp_headers.get('content-length')
  112. if content_len:
  113. response_body = sock.recv(int(content_len)) # read the body of the HTTP error message response and include it in the exception
  114. else:
  115. response_body = None
  116. raise WebSocketBadStatusException("Handshake status {status} {message} -+-+- {headers} -+-+- {body}".format(status=status, message=status_message, headers=resp_headers, body=response_body), status, status_message, resp_headers, response_body)
  117. return status, resp_headers
  118. _HEADERS_TO_CHECK = {
  119. "upgrade": "websocket",
  120. "connection": "upgrade",
  121. }
  122. def _validate(headers, key: str, subprotocols):
  123. subproto = None
  124. for k, v in _HEADERS_TO_CHECK.items():
  125. r = headers.get(k, None)
  126. if not r:
  127. return False, None
  128. r = [x.strip().lower() for x in r.split(',')]
  129. if v not in r:
  130. return False, None
  131. if subprotocols:
  132. subproto = headers.get("sec-websocket-protocol", None)
  133. if not subproto or subproto.lower() not in [s.lower() for s in subprotocols]:
  134. error("Invalid subprotocol: " + str(subprotocols))
  135. return False, None
  136. subproto = subproto.lower()
  137. result = headers.get("sec-websocket-accept", None)
  138. if not result:
  139. return False, None
  140. result = result.lower()
  141. if isinstance(result, str):
  142. result = result.encode('utf-8')
  143. value = (key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode('utf-8')
  144. hashed = base64encode(hashlib.sha1(value).digest()).strip().lower()
  145. success = hmac.compare_digest(hashed, result)
  146. if success:
  147. return True, subproto
  148. else:
  149. return False, None
  150. def _create_sec_websocket_key() -> str:
  151. randomness = os.urandom(16)
  152. return base64encode(randomness).decode('utf-8').strip()