headers.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. """
  2. :mod:`websockets.headers` provides parsers and serializers for HTTP headers
  3. used in WebSocket handshake messages.
  4. """
  5. import base64
  6. import binascii
  7. import re
  8. from typing import Callable, List, Optional, Sequence, Tuple, TypeVar, cast
  9. from .exceptions import InvalidHeaderFormat, InvalidHeaderValue
  10. from .typing import (
  11. ConnectionOption,
  12. ExtensionHeader,
  13. ExtensionName,
  14. ExtensionParameter,
  15. Subprotocol,
  16. UpgradeProtocol,
  17. )
  18. __all__ = [
  19. "parse_connection",
  20. "parse_upgrade",
  21. "parse_extension",
  22. "build_extension",
  23. "parse_subprotocol",
  24. "build_subprotocol",
  25. "build_www_authenticate_basic",
  26. "parse_authorization_basic",
  27. "build_authorization_basic",
  28. ]
  29. T = TypeVar("T")
  30. # To avoid a dependency on a parsing library, we implement manually the ABNF
  31. # described in https://tools.ietf.org/html/rfc6455#section-9.1 with the
  32. # definitions from https://tools.ietf.org/html/rfc7230#appendix-B.
  33. def peek_ahead(header: str, pos: int) -> Optional[str]:
  34. """
  35. Return the next character from ``header`` at the given position.
  36. Return ``None`` at the end of ``header``.
  37. We never need to peek more than one character ahead.
  38. """
  39. return None if pos == len(header) else header[pos]
  40. _OWS_re = re.compile(r"[\t ]*")
  41. def parse_OWS(header: str, pos: int) -> int:
  42. """
  43. Parse optional whitespace from ``header`` at the given position.
  44. Return the new position.
  45. The whitespace itself isn't returned because it isn't significant.
  46. """
  47. # There's always a match, possibly empty, whose content doesn't matter.
  48. match = _OWS_re.match(header, pos)
  49. assert match is not None
  50. return match.end()
  51. _token_re = re.compile(r"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+")
  52. def parse_token(header: str, pos: int, header_name: str) -> Tuple[str, int]:
  53. """
  54. Parse a token from ``header`` at the given position.
  55. Return the token value and the new position.
  56. :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
  57. """
  58. match = _token_re.match(header, pos)
  59. if match is None:
  60. raise InvalidHeaderFormat(header_name, "expected token", header, pos)
  61. return match.group(), match.end()
  62. _quoted_string_re = re.compile(
  63. r'"(?:[\x09\x20-\x21\x23-\x5b\x5d-\x7e]|\\[\x09\x20-\x7e\x80-\xff])*"'
  64. )
  65. _unquote_re = re.compile(r"\\([\x09\x20-\x7e\x80-\xff])")
  66. def parse_quoted_string(header: str, pos: int, header_name: str) -> Tuple[str, int]:
  67. """
  68. Parse a quoted string from ``header`` at the given position.
  69. Return the unquoted value and the new position.
  70. :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
  71. """
  72. match = _quoted_string_re.match(header, pos)
  73. if match is None:
  74. raise InvalidHeaderFormat(header_name, "expected quoted string", header, pos)
  75. return _unquote_re.sub(r"\1", match.group()[1:-1]), match.end()
  76. _quotable_re = re.compile(r"[\x09\x20-\x7e\x80-\xff]*")
  77. _quote_re = re.compile(r"([\x22\x5c])")
  78. def build_quoted_string(value: str) -> str:
  79. """
  80. Format ``value`` as a quoted string.
  81. This is the reverse of :func:`parse_quoted_string`.
  82. """
  83. match = _quotable_re.fullmatch(value)
  84. if match is None:
  85. raise ValueError("invalid characters for quoted-string encoding")
  86. return '"' + _quote_re.sub(r"\\\1", value) + '"'
  87. def parse_list(
  88. parse_item: Callable[[str, int, str], Tuple[T, int]],
  89. header: str,
  90. pos: int,
  91. header_name: str,
  92. ) -> List[T]:
  93. """
  94. Parse a comma-separated list from ``header`` at the given position.
  95. This is appropriate for parsing values with the following grammar:
  96. 1#item
  97. ``parse_item`` parses one item.
  98. ``header`` is assumed not to start or end with whitespace.
  99. (This function is designed for parsing an entire header value and
  100. :func:`~websockets.http.read_headers` strips whitespace from values.)
  101. Return a list of items.
  102. :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
  103. """
  104. # Per https://tools.ietf.org/html/rfc7230#section-7, "a recipient MUST
  105. # parse and ignore a reasonable number of empty list elements"; hence
  106. # while loops that remove extra delimiters.
  107. # Remove extra delimiters before the first item.
  108. while peek_ahead(header, pos) == ",":
  109. pos = parse_OWS(header, pos + 1)
  110. items = []
  111. while True:
  112. # Loop invariant: a item starts at pos in header.
  113. item, pos = parse_item(header, pos, header_name)
  114. items.append(item)
  115. pos = parse_OWS(header, pos)
  116. # We may have reached the end of the header.
  117. if pos == len(header):
  118. break
  119. # There must be a delimiter after each element except the last one.
  120. if peek_ahead(header, pos) == ",":
  121. pos = parse_OWS(header, pos + 1)
  122. else:
  123. raise InvalidHeaderFormat(header_name, "expected comma", header, pos)
  124. # Remove extra delimiters before the next item.
  125. while peek_ahead(header, pos) == ",":
  126. pos = parse_OWS(header, pos + 1)
  127. # We may have reached the end of the header.
  128. if pos == len(header):
  129. break
  130. # Since we only advance in the header by one character with peek_ahead()
  131. # or with the end position of a regex match, we can't overshoot the end.
  132. assert pos == len(header)
  133. return items
  134. def parse_connection_option(
  135. header: str, pos: int, header_name: str
  136. ) -> Tuple[ConnectionOption, int]:
  137. """
  138. Parse a Connection option from ``header`` at the given position.
  139. Return the protocol value and the new position.
  140. :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
  141. """
  142. item, pos = parse_token(header, pos, header_name)
  143. return cast(ConnectionOption, item), pos
  144. def parse_connection(header: str) -> List[ConnectionOption]:
  145. """
  146. Parse a ``Connection`` header.
  147. Return a list of HTTP connection options.
  148. :param header: value of the ``Connection`` header
  149. :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
  150. """
  151. return parse_list(parse_connection_option, header, 0, "Connection")
  152. _protocol_re = re.compile(
  153. r"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+(?:/[-!#$%&\'*+.^_`|~0-9a-zA-Z]+)?"
  154. )
  155. def parse_upgrade_protocol(
  156. header: str, pos: int, header_name: str
  157. ) -> Tuple[UpgradeProtocol, int]:
  158. """
  159. Parse an Upgrade protocol from ``header`` at the given position.
  160. Return the protocol value and the new position.
  161. :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
  162. """
  163. match = _protocol_re.match(header, pos)
  164. if match is None:
  165. raise InvalidHeaderFormat(header_name, "expected protocol", header, pos)
  166. return cast(UpgradeProtocol, match.group()), match.end()
  167. def parse_upgrade(header: str) -> List[UpgradeProtocol]:
  168. """
  169. Parse an ``Upgrade`` header.
  170. Return a list of HTTP protocols.
  171. :param header: value of the ``Upgrade`` header
  172. :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
  173. """
  174. return parse_list(parse_upgrade_protocol, header, 0, "Upgrade")
  175. def parse_extension_item_param(
  176. header: str, pos: int, header_name: str
  177. ) -> Tuple[ExtensionParameter, int]:
  178. """
  179. Parse a single extension parameter from ``header`` at the given position.
  180. Return a ``(name, value)`` pair and the new position.
  181. :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
  182. """
  183. # Extract parameter name.
  184. name, pos = parse_token(header, pos, header_name)
  185. pos = parse_OWS(header, pos)
  186. # Extract parameter value, if there is one.
  187. value: Optional[str] = None
  188. if peek_ahead(header, pos) == "=":
  189. pos = parse_OWS(header, pos + 1)
  190. if peek_ahead(header, pos) == '"':
  191. pos_before = pos # for proper error reporting below
  192. value, pos = parse_quoted_string(header, pos, header_name)
  193. # https://tools.ietf.org/html/rfc6455#section-9.1 says: the value
  194. # after quoted-string unescaping MUST conform to the 'token' ABNF.
  195. if _token_re.fullmatch(value) is None:
  196. raise InvalidHeaderFormat(
  197. header_name, "invalid quoted header content", header, pos_before
  198. )
  199. else:
  200. value, pos = parse_token(header, pos, header_name)
  201. pos = parse_OWS(header, pos)
  202. return (name, value), pos
  203. def parse_extension_item(
  204. header: str, pos: int, header_name: str
  205. ) -> Tuple[ExtensionHeader, int]:
  206. """
  207. Parse an extension definition from ``header`` at the given position.
  208. Return an ``(extension name, parameters)`` pair, where ``parameters`` is a
  209. list of ``(name, value)`` pairs, and the new position.
  210. :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
  211. """
  212. # Extract extension name.
  213. name, pos = parse_token(header, pos, header_name)
  214. pos = parse_OWS(header, pos)
  215. # Extract all parameters.
  216. parameters = []
  217. while peek_ahead(header, pos) == ";":
  218. pos = parse_OWS(header, pos + 1)
  219. parameter, pos = parse_extension_item_param(header, pos, header_name)
  220. parameters.append(parameter)
  221. return (cast(ExtensionName, name), parameters), pos
  222. def parse_extension(header: str) -> List[ExtensionHeader]:
  223. """
  224. Parse a ``Sec-WebSocket-Extensions`` header.
  225. Return a list of WebSocket extensions and their parameters in this format::
  226. [
  227. (
  228. 'extension name',
  229. [
  230. ('parameter name', 'parameter value'),
  231. ....
  232. ]
  233. ),
  234. ...
  235. ]
  236. Parameter values are ``None`` when no value is provided.
  237. :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
  238. """
  239. return parse_list(parse_extension_item, header, 0, "Sec-WebSocket-Extensions")
  240. parse_extension_list = parse_extension # alias for backwards compatibility
  241. def build_extension_item(
  242. name: ExtensionName, parameters: List[ExtensionParameter]
  243. ) -> str:
  244. """
  245. Build an extension definition.
  246. This is the reverse of :func:`parse_extension_item`.
  247. """
  248. return "; ".join(
  249. [cast(str, name)]
  250. + [
  251. # Quoted strings aren't necessary because values are always tokens.
  252. name if value is None else f"{name}={value}"
  253. for name, value in parameters
  254. ]
  255. )
  256. def build_extension(extensions: Sequence[ExtensionHeader]) -> str:
  257. """
  258. Build a ``Sec-WebSocket-Extensions`` header.
  259. This is the reverse of :func:`parse_extension`.
  260. """
  261. return ", ".join(
  262. build_extension_item(name, parameters) for name, parameters in extensions
  263. )
  264. build_extension_list = build_extension # alias for backwards compatibility
  265. def parse_subprotocol_item(
  266. header: str, pos: int, header_name: str
  267. ) -> Tuple[Subprotocol, int]:
  268. """
  269. Parse a subprotocol from ``header`` at the given position.
  270. Return the subprotocol value and the new position.
  271. :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
  272. """
  273. item, pos = parse_token(header, pos, header_name)
  274. return cast(Subprotocol, item), pos
  275. def parse_subprotocol(header: str) -> List[Subprotocol]:
  276. """
  277. Parse a ``Sec-WebSocket-Protocol`` header.
  278. Return a list of WebSocket subprotocols.
  279. :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
  280. """
  281. return parse_list(parse_subprotocol_item, header, 0, "Sec-WebSocket-Protocol")
  282. parse_subprotocol_list = parse_subprotocol # alias for backwards compatibility
  283. def build_subprotocol(protocols: Sequence[Subprotocol]) -> str:
  284. """
  285. Build a ``Sec-WebSocket-Protocol`` header.
  286. This is the reverse of :func:`parse_subprotocol`.
  287. """
  288. return ", ".join(protocols)
  289. build_subprotocol_list = build_subprotocol # alias for backwards compatibility
  290. def build_www_authenticate_basic(realm: str) -> str:
  291. """
  292. Build a ``WWW-Authenticate`` header for HTTP Basic Auth.
  293. :param realm: authentication realm
  294. """
  295. # https://tools.ietf.org/html/rfc7617#section-2
  296. realm = build_quoted_string(realm)
  297. charset = build_quoted_string("UTF-8")
  298. return f"Basic realm={realm}, charset={charset}"
  299. _token68_re = re.compile(r"[A-Za-z0-9-._~+/]+=*")
  300. def parse_token68(header: str, pos: int, header_name: str) -> Tuple[str, int]:
  301. """
  302. Parse a token68 from ``header`` at the given position.
  303. Return the token value and the new position.
  304. :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs.
  305. """
  306. match = _token68_re.match(header, pos)
  307. if match is None:
  308. raise InvalidHeaderFormat(header_name, "expected token68", header, pos)
  309. return match.group(), match.end()
  310. def parse_end(header: str, pos: int, header_name: str) -> None:
  311. """
  312. Check that parsing reached the end of header.
  313. """
  314. if pos < len(header):
  315. raise InvalidHeaderFormat(header_name, "trailing data", header, pos)
  316. def parse_authorization_basic(header: str) -> Tuple[str, str]:
  317. """
  318. Parse an ``Authorization`` header for HTTP Basic Auth.
  319. Return a ``(username, password)`` tuple.
  320. :param header: value of the ``Authorization`` header
  321. :raises InvalidHeaderFormat: on invalid inputs
  322. :raises InvalidHeaderValue: on unsupported inputs
  323. """
  324. # https://tools.ietf.org/html/rfc7235#section-2.1
  325. # https://tools.ietf.org/html/rfc7617#section-2
  326. scheme, pos = parse_token(header, 0, "Authorization")
  327. if scheme.lower() != "basic":
  328. raise InvalidHeaderValue("Authorization", f"unsupported scheme: {scheme}")
  329. if peek_ahead(header, pos) != " ":
  330. raise InvalidHeaderFormat(
  331. "Authorization", "expected space after scheme", header, pos
  332. )
  333. pos += 1
  334. basic_credentials, pos = parse_token68(header, pos, "Authorization")
  335. parse_end(header, pos, "Authorization")
  336. try:
  337. user_pass = base64.b64decode(basic_credentials.encode()).decode()
  338. except binascii.Error:
  339. raise InvalidHeaderValue(
  340. "Authorization", "expected base64-encoded credentials"
  341. ) from None
  342. try:
  343. username, password = user_pass.split(":", 1)
  344. except ValueError:
  345. raise InvalidHeaderValue(
  346. "Authorization", "expected username:password credentials"
  347. ) from None
  348. return username, password
  349. def build_authorization_basic(username: str, password: str) -> str:
  350. """
  351. Build an ``Authorization`` header for HTTP Basic Auth.
  352. This is the reverse of :func:`parse_authorization_basic`.
  353. """
  354. # https://tools.ietf.org/html/rfc7617#section-2
  355. assert ":" not in username
  356. user_pass = f"{username}:{password}"
  357. basic_credentials = base64.b64encode(user_pass.encode()).decode()
  358. return "Basic " + basic_credentials