test_io.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. from typing import Any, Callable, Generator, List
  2. import pytest
  3. from .._events import (
  4. ConnectionClosed,
  5. Data,
  6. EndOfMessage,
  7. Event,
  8. InformationalResponse,
  9. Request,
  10. Response,
  11. )
  12. from .._headers import Headers, normalize_and_validate
  13. from .._readers import (
  14. _obsolete_line_fold,
  15. ChunkedReader,
  16. ContentLengthReader,
  17. Http10Reader,
  18. READERS,
  19. )
  20. from .._receivebuffer import ReceiveBuffer
  21. from .._state import (
  22. CLIENT,
  23. CLOSED,
  24. DONE,
  25. IDLE,
  26. MIGHT_SWITCH_PROTOCOL,
  27. MUST_CLOSE,
  28. SEND_BODY,
  29. SEND_RESPONSE,
  30. SERVER,
  31. SWITCHED_PROTOCOL,
  32. )
  33. from .._util import LocalProtocolError
  34. from .._writers import (
  35. ChunkedWriter,
  36. ContentLengthWriter,
  37. Http10Writer,
  38. write_any_response,
  39. write_headers,
  40. write_request,
  41. WRITERS,
  42. )
  43. from .helpers import normalize_data_events
  44. SIMPLE_CASES = [
  45. (
  46. (CLIENT, IDLE),
  47. Request(
  48. method="GET",
  49. target="/a",
  50. headers=[("Host", "foo"), ("Connection", "close")],
  51. ),
  52. b"GET /a HTTP/1.1\r\nHost: foo\r\nConnection: close\r\n\r\n",
  53. ),
  54. (
  55. (SERVER, SEND_RESPONSE),
  56. Response(status_code=200, headers=[("Connection", "close")], reason=b"OK"),
  57. b"HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n",
  58. ),
  59. (
  60. (SERVER, SEND_RESPONSE),
  61. Response(status_code=200, headers=[], reason=b"OK"), # type: ignore[arg-type]
  62. b"HTTP/1.1 200 OK\r\n\r\n",
  63. ),
  64. (
  65. (SERVER, SEND_RESPONSE),
  66. InformationalResponse(
  67. status_code=101, headers=[("Upgrade", "websocket")], reason=b"Upgrade"
  68. ),
  69. b"HTTP/1.1 101 Upgrade\r\nUpgrade: websocket\r\n\r\n",
  70. ),
  71. (
  72. (SERVER, SEND_RESPONSE),
  73. InformationalResponse(status_code=101, headers=[], reason=b"Upgrade"), # type: ignore[arg-type]
  74. b"HTTP/1.1 101 Upgrade\r\n\r\n",
  75. ),
  76. ]
  77. def dowrite(writer: Callable[..., None], obj: Any) -> bytes:
  78. got_list: List[bytes] = []
  79. writer(obj, got_list.append)
  80. return b"".join(got_list)
  81. def tw(writer: Any, obj: Any, expected: Any) -> None:
  82. got = dowrite(writer, obj)
  83. assert got == expected
  84. def makebuf(data: bytes) -> ReceiveBuffer:
  85. buf = ReceiveBuffer()
  86. buf += data
  87. return buf
  88. def tr(reader: Any, data: bytes, expected: Any) -> None:
  89. def check(got: Any) -> None:
  90. assert got == expected
  91. # Headers should always be returned as bytes, not e.g. bytearray
  92. # https://github.com/python-hyper/wsproto/pull/54#issuecomment-377709478
  93. for name, value in getattr(got, "headers", []):
  94. assert type(name) is bytes
  95. assert type(value) is bytes
  96. # Simple: consume whole thing
  97. buf = makebuf(data)
  98. check(reader(buf))
  99. assert not buf
  100. # Incrementally growing buffer
  101. buf = ReceiveBuffer()
  102. for i in range(len(data)):
  103. assert reader(buf) is None
  104. buf += data[i : i + 1]
  105. check(reader(buf))
  106. # Trailing data
  107. buf = makebuf(data)
  108. buf += b"trailing"
  109. check(reader(buf))
  110. assert bytes(buf) == b"trailing"
  111. def test_writers_simple() -> None:
  112. for ((role, state), event, binary) in SIMPLE_CASES:
  113. tw(WRITERS[role, state], event, binary)
  114. def test_readers_simple() -> None:
  115. for ((role, state), event, binary) in SIMPLE_CASES:
  116. tr(READERS[role, state], binary, event)
  117. def test_writers_unusual() -> None:
  118. # Simple test of the write_headers utility routine
  119. tw(
  120. write_headers,
  121. normalize_and_validate([("foo", "bar"), ("baz", "quux")]),
  122. b"foo: bar\r\nbaz: quux\r\n\r\n",
  123. )
  124. tw(write_headers, Headers([]), b"\r\n")
  125. # We understand HTTP/1.0, but we don't speak it
  126. with pytest.raises(LocalProtocolError):
  127. tw(
  128. write_request,
  129. Request(
  130. method="GET",
  131. target="/",
  132. headers=[("Host", "foo"), ("Connection", "close")],
  133. http_version="1.0",
  134. ),
  135. None,
  136. )
  137. with pytest.raises(LocalProtocolError):
  138. tw(
  139. write_any_response,
  140. Response(
  141. status_code=200, headers=[("Connection", "close")], http_version="1.0"
  142. ),
  143. None,
  144. )
  145. def test_readers_unusual() -> None:
  146. # Reading HTTP/1.0
  147. tr(
  148. READERS[CLIENT, IDLE],
  149. b"HEAD /foo HTTP/1.0\r\nSome: header\r\n\r\n",
  150. Request(
  151. method="HEAD",
  152. target="/foo",
  153. headers=[("Some", "header")],
  154. http_version="1.0",
  155. ),
  156. )
  157. # check no-headers, since it's only legal with HTTP/1.0
  158. tr(
  159. READERS[CLIENT, IDLE],
  160. b"HEAD /foo HTTP/1.0\r\n\r\n",
  161. Request(method="HEAD", target="/foo", headers=[], http_version="1.0"), # type: ignore[arg-type]
  162. )
  163. tr(
  164. READERS[SERVER, SEND_RESPONSE],
  165. b"HTTP/1.0 200 OK\r\nSome: header\r\n\r\n",
  166. Response(
  167. status_code=200,
  168. headers=[("Some", "header")],
  169. http_version="1.0",
  170. reason=b"OK",
  171. ),
  172. )
  173. # single-character header values (actually disallowed by the ABNF in RFC
  174. # 7230 -- this is a bug in the standard that we originally copied...)
  175. tr(
  176. READERS[SERVER, SEND_RESPONSE],
  177. b"HTTP/1.0 200 OK\r\n" b"Foo: a a a a a \r\n\r\n",
  178. Response(
  179. status_code=200,
  180. headers=[("Foo", "a a a a a")],
  181. http_version="1.0",
  182. reason=b"OK",
  183. ),
  184. )
  185. # Empty headers -- also legal
  186. tr(
  187. READERS[SERVER, SEND_RESPONSE],
  188. b"HTTP/1.0 200 OK\r\n" b"Foo:\r\n\r\n",
  189. Response(
  190. status_code=200, headers=[("Foo", "")], http_version="1.0", reason=b"OK"
  191. ),
  192. )
  193. tr(
  194. READERS[SERVER, SEND_RESPONSE],
  195. b"HTTP/1.0 200 OK\r\n" b"Foo: \t \t \r\n\r\n",
  196. Response(
  197. status_code=200, headers=[("Foo", "")], http_version="1.0", reason=b"OK"
  198. ),
  199. )
  200. # Tolerate broken servers that leave off the response code
  201. tr(
  202. READERS[SERVER, SEND_RESPONSE],
  203. b"HTTP/1.0 200\r\n" b"Foo: bar\r\n\r\n",
  204. Response(
  205. status_code=200, headers=[("Foo", "bar")], http_version="1.0", reason=b""
  206. ),
  207. )
  208. # Tolerate headers line endings (\r\n and \n)
  209. # \n\r\b between headers and body
  210. tr(
  211. READERS[SERVER, SEND_RESPONSE],
  212. b"HTTP/1.1 200 OK\r\nSomeHeader: val\n\r\n",
  213. Response(
  214. status_code=200,
  215. headers=[("SomeHeader", "val")],
  216. http_version="1.1",
  217. reason="OK",
  218. ),
  219. )
  220. # delimited only with \n
  221. tr(
  222. READERS[SERVER, SEND_RESPONSE],
  223. b"HTTP/1.1 200 OK\nSomeHeader1: val1\nSomeHeader2: val2\n\n",
  224. Response(
  225. status_code=200,
  226. headers=[("SomeHeader1", "val1"), ("SomeHeader2", "val2")],
  227. http_version="1.1",
  228. reason="OK",
  229. ),
  230. )
  231. # mixed \r\n and \n
  232. tr(
  233. READERS[SERVER, SEND_RESPONSE],
  234. b"HTTP/1.1 200 OK\r\nSomeHeader1: val1\nSomeHeader2: val2\n\r\n",
  235. Response(
  236. status_code=200,
  237. headers=[("SomeHeader1", "val1"), ("SomeHeader2", "val2")],
  238. http_version="1.1",
  239. reason="OK",
  240. ),
  241. )
  242. # obsolete line folding
  243. tr(
  244. READERS[CLIENT, IDLE],
  245. b"HEAD /foo HTTP/1.1\r\n"
  246. b"Host: example.com\r\n"
  247. b"Some: multi-line\r\n"
  248. b" header\r\n"
  249. b"\tnonsense\r\n"
  250. b" \t \t\tI guess\r\n"
  251. b"Connection: close\r\n"
  252. b"More-nonsense: in the\r\n"
  253. b" last header \r\n\r\n",
  254. Request(
  255. method="HEAD",
  256. target="/foo",
  257. headers=[
  258. ("Host", "example.com"),
  259. ("Some", "multi-line header nonsense I guess"),
  260. ("Connection", "close"),
  261. ("More-nonsense", "in the last header"),
  262. ],
  263. ),
  264. )
  265. with pytest.raises(LocalProtocolError):
  266. tr(
  267. READERS[CLIENT, IDLE],
  268. b"HEAD /foo HTTP/1.1\r\n" b" folded: line\r\n\r\n",
  269. None,
  270. )
  271. with pytest.raises(LocalProtocolError):
  272. tr(
  273. READERS[CLIENT, IDLE],
  274. b"HEAD /foo HTTP/1.1\r\n" b"foo : line\r\n\r\n",
  275. None,
  276. )
  277. with pytest.raises(LocalProtocolError):
  278. tr(
  279. READERS[CLIENT, IDLE],
  280. b"HEAD /foo HTTP/1.1\r\n" b"foo\t: line\r\n\r\n",
  281. None,
  282. )
  283. with pytest.raises(LocalProtocolError):
  284. tr(
  285. READERS[CLIENT, IDLE],
  286. b"HEAD /foo HTTP/1.1\r\n" b"foo\t: line\r\n\r\n",
  287. None,
  288. )
  289. with pytest.raises(LocalProtocolError):
  290. tr(READERS[CLIENT, IDLE], b"HEAD /foo HTTP/1.1\r\n" b": line\r\n\r\n", None)
  291. def test__obsolete_line_fold_bytes() -> None:
  292. # _obsolete_line_fold has a defensive cast to bytearray, which is
  293. # necessary to protect against O(n^2) behavior in case anyone ever passes
  294. # in regular bytestrings... but right now we never pass in regular
  295. # bytestrings. so this test just exists to get some coverage on that
  296. # defensive cast.
  297. assert list(_obsolete_line_fold([b"aaa", b"bbb", b" ccc", b"ddd"])) == [
  298. b"aaa",
  299. bytearray(b"bbb ccc"),
  300. b"ddd",
  301. ]
  302. def _run_reader_iter(
  303. reader: Any, buf: bytes, do_eof: bool
  304. ) -> Generator[Any, None, None]:
  305. while True:
  306. event = reader(buf)
  307. if event is None:
  308. break
  309. yield event
  310. # body readers have undefined behavior after returning EndOfMessage,
  311. # because this changes the state so they don't get called again
  312. if type(event) is EndOfMessage:
  313. break
  314. if do_eof:
  315. assert not buf
  316. yield reader.read_eof()
  317. def _run_reader(*args: Any) -> List[Event]:
  318. events = list(_run_reader_iter(*args))
  319. return normalize_data_events(events)
  320. def t_body_reader(thunk: Any, data: bytes, expected: Any, do_eof: bool = False) -> None:
  321. # Simple: consume whole thing
  322. print("Test 1")
  323. buf = makebuf(data)
  324. assert _run_reader(thunk(), buf, do_eof) == expected
  325. # Incrementally growing buffer
  326. print("Test 2")
  327. reader = thunk()
  328. buf = ReceiveBuffer()
  329. events = []
  330. for i in range(len(data)):
  331. events += _run_reader(reader, buf, False)
  332. buf += data[i : i + 1]
  333. events += _run_reader(reader, buf, do_eof)
  334. assert normalize_data_events(events) == expected
  335. is_complete = any(type(event) is EndOfMessage for event in expected)
  336. if is_complete and not do_eof:
  337. buf = makebuf(data + b"trailing")
  338. assert _run_reader(thunk(), buf, False) == expected
  339. def test_ContentLengthReader() -> None:
  340. t_body_reader(lambda: ContentLengthReader(0), b"", [EndOfMessage()])
  341. t_body_reader(
  342. lambda: ContentLengthReader(10),
  343. b"0123456789",
  344. [Data(data=b"0123456789"), EndOfMessage()],
  345. )
  346. def test_Http10Reader() -> None:
  347. t_body_reader(Http10Reader, b"", [EndOfMessage()], do_eof=True)
  348. t_body_reader(Http10Reader, b"asdf", [Data(data=b"asdf")], do_eof=False)
  349. t_body_reader(
  350. Http10Reader, b"asdf", [Data(data=b"asdf"), EndOfMessage()], do_eof=True
  351. )
  352. def test_ChunkedReader() -> None:
  353. t_body_reader(ChunkedReader, b"0\r\n\r\n", [EndOfMessage()])
  354. t_body_reader(
  355. ChunkedReader,
  356. b"0\r\nSome: header\r\n\r\n",
  357. [EndOfMessage(headers=[("Some", "header")])],
  358. )
  359. t_body_reader(
  360. ChunkedReader,
  361. b"5\r\n01234\r\n"
  362. + b"10\r\n0123456789abcdef\r\n"
  363. + b"0\r\n"
  364. + b"Some: header\r\n\r\n",
  365. [
  366. Data(data=b"012340123456789abcdef"),
  367. EndOfMessage(headers=[("Some", "header")]),
  368. ],
  369. )
  370. t_body_reader(
  371. ChunkedReader,
  372. b"5\r\n01234\r\n" + b"10\r\n0123456789abcdef\r\n" + b"0\r\n\r\n",
  373. [Data(data=b"012340123456789abcdef"), EndOfMessage()],
  374. )
  375. # handles upper and lowercase hex
  376. t_body_reader(
  377. ChunkedReader,
  378. b"aA\r\n" + b"x" * 0xAA + b"\r\n" + b"0\r\n\r\n",
  379. [Data(data=b"x" * 0xAA), EndOfMessage()],
  380. )
  381. # refuses arbitrarily long chunk integers
  382. with pytest.raises(LocalProtocolError):
  383. # Technically this is legal HTTP/1.1, but we refuse to process chunk
  384. # sizes that don't fit into 20 characters of hex
  385. t_body_reader(ChunkedReader, b"9" * 100 + b"\r\nxxx", [Data(data=b"xxx")])
  386. # refuses garbage in the chunk count
  387. with pytest.raises(LocalProtocolError):
  388. t_body_reader(ChunkedReader, b"10\x00\r\nxxx", None)
  389. # handles (and discards) "chunk extensions" omg wtf
  390. t_body_reader(
  391. ChunkedReader,
  392. b"5; hello=there\r\n"
  393. + b"xxxxx"
  394. + b"\r\n"
  395. + b'0; random="junk"; some=more; canbe=lonnnnngg\r\n\r\n',
  396. [Data(data=b"xxxxx"), EndOfMessage()],
  397. )
  398. t_body_reader(
  399. ChunkedReader,
  400. b"5 \r\n01234\r\n" + b"0\r\n\r\n",
  401. [Data(data=b"01234"), EndOfMessage()],
  402. )
  403. def test_ContentLengthWriter() -> None:
  404. w = ContentLengthWriter(5)
  405. assert dowrite(w, Data(data=b"123")) == b"123"
  406. assert dowrite(w, Data(data=b"45")) == b"45"
  407. assert dowrite(w, EndOfMessage()) == b""
  408. w = ContentLengthWriter(5)
  409. with pytest.raises(LocalProtocolError):
  410. dowrite(w, Data(data=b"123456"))
  411. w = ContentLengthWriter(5)
  412. dowrite(w, Data(data=b"123"))
  413. with pytest.raises(LocalProtocolError):
  414. dowrite(w, Data(data=b"456"))
  415. w = ContentLengthWriter(5)
  416. dowrite(w, Data(data=b"123"))
  417. with pytest.raises(LocalProtocolError):
  418. dowrite(w, EndOfMessage())
  419. w = ContentLengthWriter(5)
  420. dowrite(w, Data(data=b"123")) == b"123"
  421. dowrite(w, Data(data=b"45")) == b"45"
  422. with pytest.raises(LocalProtocolError):
  423. dowrite(w, EndOfMessage(headers=[("Etag", "asdf")]))
  424. def test_ChunkedWriter() -> None:
  425. w = ChunkedWriter()
  426. assert dowrite(w, Data(data=b"aaa")) == b"3\r\naaa\r\n"
  427. assert dowrite(w, Data(data=b"a" * 20)) == b"14\r\n" + b"a" * 20 + b"\r\n"
  428. assert dowrite(w, Data(data=b"")) == b""
  429. assert dowrite(w, EndOfMessage()) == b"0\r\n\r\n"
  430. assert (
  431. dowrite(w, EndOfMessage(headers=[("Etag", "asdf"), ("a", "b")]))
  432. == b"0\r\nEtag: asdf\r\na: b\r\n\r\n"
  433. )
  434. def test_Http10Writer() -> None:
  435. w = Http10Writer()
  436. assert dowrite(w, Data(data=b"1234")) == b"1234"
  437. assert dowrite(w, EndOfMessage()) == b""
  438. with pytest.raises(LocalProtocolError):
  439. dowrite(w, EndOfMessage(headers=[("Etag", "asdf")]))
  440. def test_reject_garbage_after_request_line() -> None:
  441. with pytest.raises(LocalProtocolError):
  442. tr(READERS[SERVER, SEND_RESPONSE], b"HTTP/1.0 200 OK\x00xxxx\r\n\r\n", None)
  443. def test_reject_garbage_after_response_line() -> None:
  444. with pytest.raises(LocalProtocolError):
  445. tr(
  446. READERS[CLIENT, IDLE],
  447. b"HEAD /foo HTTP/1.1 xxxxxx\r\n" b"Host: a\r\n\r\n",
  448. None,
  449. )
  450. def test_reject_garbage_in_header_line() -> None:
  451. with pytest.raises(LocalProtocolError):
  452. tr(
  453. READERS[CLIENT, IDLE],
  454. b"HEAD /foo HTTP/1.1\r\n" b"Host: foo\x00bar\r\n\r\n",
  455. None,
  456. )
  457. def test_reject_non_vchar_in_path() -> None:
  458. for bad_char in b"\x00\x20\x7f\xee":
  459. message = bytearray(b"HEAD /")
  460. message.append(bad_char)
  461. message.extend(b" HTTP/1.1\r\nHost: foobar\r\n\r\n")
  462. with pytest.raises(LocalProtocolError):
  463. tr(READERS[CLIENT, IDLE], message, None)
  464. # https://github.com/python-hyper/h11/issues/57
  465. def test_allow_some_garbage_in_cookies() -> None:
  466. tr(
  467. READERS[CLIENT, IDLE],
  468. b"HEAD /foo HTTP/1.1\r\n"
  469. b"Host: foo\r\n"
  470. b"Set-Cookie: ___utmvafIumyLc=kUd\x01UpAt; path=/; Max-Age=900\r\n"
  471. b"\r\n",
  472. Request(
  473. method="HEAD",
  474. target="/foo",
  475. headers=[
  476. ("Host", "foo"),
  477. ("Set-Cookie", "___utmvafIumyLc=kUd\x01UpAt; path=/; Max-Age=900"),
  478. ],
  479. ),
  480. )
  481. def test_host_comes_first() -> None:
  482. tw(
  483. write_headers,
  484. normalize_and_validate([("foo", "bar"), ("Host", "example.com")]),
  485. b"Host: example.com\r\nfoo: bar\r\n\r\n",
  486. )