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.
 
 
 
 

395 lines
13 KiB

  1. import pytest
  2. from .._util import LocalProtocolError
  3. from .._receivebuffer import ReceiveBuffer
  4. from .._headers import normalize_and_validate
  5. from .._state import *
  6. from .._events import *
  7. from .._writers import (
  8. WRITERS,
  9. write_headers, write_request, write_any_response,
  10. ContentLengthWriter, ChunkedWriter, Http10Writer,
  11. )
  12. from .._readers import (
  13. READERS,
  14. ContentLengthReader, ChunkedReader, Http10Reader,
  15. _obsolete_line_fold,
  16. )
  17. from .helpers import normalize_data_events
  18. SIMPLE_CASES = [
  19. ((CLIENT, IDLE),
  20. Request(method="GET", target="/a",
  21. headers=[("Host", "foo"), ("Connection", "close")]),
  22. b"GET /a HTTP/1.1\r\nhost: foo\r\nconnection: close\r\n\r\n"),
  23. ((SERVER, SEND_RESPONSE),
  24. Response(status_code=200, headers=[("Connection", "close")], reason=b"OK"),
  25. b"HTTP/1.1 200 OK\r\nconnection: close\r\n\r\n"),
  26. ((SERVER, SEND_RESPONSE),
  27. Response(status_code=200, headers=[], reason=b"OK"),
  28. b"HTTP/1.1 200 OK\r\n\r\n"),
  29. ((SERVER, SEND_RESPONSE),
  30. InformationalResponse(status_code=101,
  31. headers=[("Upgrade", "websocket")], reason=b"Upgrade"),
  32. b"HTTP/1.1 101 Upgrade\r\nupgrade: websocket\r\n\r\n"),
  33. ((SERVER, SEND_RESPONSE),
  34. InformationalResponse(status_code=101, headers=[], reason=b"Upgrade"),
  35. b"HTTP/1.1 101 Upgrade\r\n\r\n"),
  36. ]
  37. def dowrite(writer, obj):
  38. got_list = []
  39. writer(obj, got_list.append)
  40. return b"".join(got_list)
  41. def tw(writer, obj, expected):
  42. got = dowrite(writer, obj)
  43. assert got == expected
  44. def makebuf(data):
  45. buf = ReceiveBuffer()
  46. buf += data
  47. return buf
  48. def tr(reader, data, expected):
  49. def check(got):
  50. assert got == expected
  51. # Headers should always be returned as bytes, not e.g. bytearray
  52. # https://github.com/python-hyper/wsproto/pull/54#issuecomment-377709478
  53. for name, value in getattr(got, "headers", []):
  54. print(name, value)
  55. assert type(name) is bytes
  56. assert type(value) is bytes
  57. # Simple: consume whole thing
  58. buf = makebuf(data)
  59. check(reader(buf))
  60. assert not buf
  61. # Incrementally growing buffer
  62. buf = ReceiveBuffer()
  63. for i in range(len(data)):
  64. assert reader(buf) is None
  65. buf += data[i:i + 1]
  66. check(reader(buf))
  67. # Trailing data
  68. buf = makebuf(data)
  69. buf += b"trailing"
  70. check(reader(buf))
  71. assert bytes(buf) == b"trailing"
  72. def test_writers_simple():
  73. for ((role, state), event, binary) in SIMPLE_CASES:
  74. tw(WRITERS[role, state], event, binary)
  75. def test_readers_simple():
  76. for ((role, state), event, binary) in SIMPLE_CASES:
  77. tr(READERS[role, state], binary, event)
  78. def test_writers_unusual():
  79. # Simple test of the write_headers utility routine
  80. tw(write_headers,
  81. normalize_and_validate([("foo", "bar"), ("baz", "quux")]),
  82. b"foo: bar\r\nbaz: quux\r\n\r\n")
  83. tw(write_headers, [], b"\r\n")
  84. # We understand HTTP/1.0, but we don't speak it
  85. with pytest.raises(LocalProtocolError):
  86. tw(write_request,
  87. Request(method="GET", target="/",
  88. headers=[("Host", "foo"), ("Connection", "close")],
  89. http_version="1.0"),
  90. None)
  91. with pytest.raises(LocalProtocolError):
  92. tw(write_any_response,
  93. Response(status_code=200, headers=[("Connection", "close")],
  94. http_version="1.0"),
  95. None)
  96. def test_readers_unusual():
  97. # Reading HTTP/1.0
  98. tr(READERS[CLIENT, IDLE],
  99. b"HEAD /foo HTTP/1.0\r\nSome: header\r\n\r\n",
  100. Request(method="HEAD", target="/foo", headers=[("Some", "header")],
  101. http_version="1.0"))
  102. # check no-headers, since it's only legal with HTTP/1.0
  103. tr(READERS[CLIENT, IDLE],
  104. b"HEAD /foo HTTP/1.0\r\n\r\n",
  105. Request(method="HEAD", target="/foo", headers=[], http_version="1.0"))
  106. tr(READERS[SERVER, SEND_RESPONSE],
  107. b"HTTP/1.0 200 OK\r\nSome: header\r\n\r\n",
  108. Response(status_code=200, headers=[("Some", "header")],
  109. http_version="1.0", reason=b"OK"))
  110. # single-character header values (actually disallowed by the ABNF in RFC
  111. # 7230 -- this is a bug in the standard that we originally copied...)
  112. tr(READERS[SERVER, SEND_RESPONSE],
  113. b"HTTP/1.0 200 OK\r\n"
  114. b"Foo: a a a a a \r\n\r\n",
  115. Response(status_code=200, headers=[("Foo", "a a a a a")],
  116. http_version="1.0", reason=b"OK"))
  117. # Empty headers -- also legal
  118. tr(READERS[SERVER, SEND_RESPONSE],
  119. b"HTTP/1.0 200 OK\r\n"
  120. b"Foo:\r\n\r\n",
  121. Response(status_code=200, headers=[("Foo", "")],
  122. http_version="1.0", reason=b"OK"))
  123. tr(READERS[SERVER, SEND_RESPONSE],
  124. b"HTTP/1.0 200 OK\r\n"
  125. b"Foo: \t \t \r\n\r\n",
  126. Response(status_code=200, headers=[("Foo", "")],
  127. http_version="1.0", reason=b"OK"))
  128. # Tolerate broken servers that leave off the response code
  129. tr(READERS[SERVER, SEND_RESPONSE],
  130. b"HTTP/1.0 200\r\n"
  131. b"Foo: bar\r\n\r\n",
  132. Response(status_code=200, headers=[("Foo", "bar")],
  133. http_version="1.0", reason=b""))
  134. # obsolete line folding
  135. tr(READERS[CLIENT, IDLE],
  136. b"HEAD /foo HTTP/1.1\r\n"
  137. b"Host: example.com\r\n"
  138. b"Some: multi-line\r\n"
  139. b" header\r\n"
  140. b"\tnonsense\r\n"
  141. b" \t \t\tI guess\r\n"
  142. b"Connection: close\r\n"
  143. b"More-nonsense: in the\r\n"
  144. b" last header \r\n\r\n",
  145. Request(method="HEAD", target="/foo",
  146. headers=[
  147. ("Host", "example.com"),
  148. ("Some", "multi-line header nonsense I guess"),
  149. ("Connection", "close"),
  150. ("More-nonsense", "in the last header"),
  151. ]))
  152. with pytest.raises(LocalProtocolError):
  153. tr(READERS[CLIENT, IDLE],
  154. b"HEAD /foo HTTP/1.1\r\n"
  155. b" folded: line\r\n\r\n",
  156. None)
  157. with pytest.raises(LocalProtocolError):
  158. tr(READERS[CLIENT, IDLE],
  159. b"HEAD /foo HTTP/1.1\r\n"
  160. b"foo : line\r\n\r\n",
  161. None)
  162. with pytest.raises(LocalProtocolError):
  163. tr(READERS[CLIENT, IDLE],
  164. b"HEAD /foo HTTP/1.1\r\n"
  165. b"foo\t: line\r\n\r\n",
  166. None)
  167. with pytest.raises(LocalProtocolError):
  168. tr(READERS[CLIENT, IDLE],
  169. b"HEAD /foo HTTP/1.1\r\n"
  170. b"foo\t: line\r\n\r\n",
  171. None)
  172. with pytest.raises(LocalProtocolError):
  173. tr(READERS[CLIENT, IDLE],
  174. b"HEAD /foo HTTP/1.1\r\n"
  175. b": line\r\n\r\n",
  176. None)
  177. def test__obsolete_line_fold_bytes():
  178. # _obsolete_line_fold has a defensive cast to bytearray, which is
  179. # necessary to protect against O(n^2) behavior in case anyone ever passes
  180. # in regular bytestrings... but right now we never pass in regular
  181. # bytestrings. so this test just exists to get some coverage on that
  182. # defensive cast.
  183. assert (list(_obsolete_line_fold([b"aaa", b"bbb", b" ccc", b"ddd"]))
  184. == [b"aaa", bytearray(b"bbb ccc"), b"ddd"])
  185. def _run_reader_iter(reader, buf, do_eof):
  186. while True:
  187. event = reader(buf)
  188. if event is None:
  189. break
  190. yield event
  191. # body readers have undefined behavior after returning EndOfMessage,
  192. # because this changes the state so they don't get called again
  193. if type(event) is EndOfMessage:
  194. break
  195. if do_eof:
  196. assert not buf
  197. yield reader.read_eof()
  198. def _run_reader(*args):
  199. events = list(_run_reader_iter(*args))
  200. return normalize_data_events(events)
  201. def t_body_reader(thunk, data, expected, do_eof=False):
  202. # Simple: consume whole thing
  203. print("Test 1")
  204. buf = makebuf(data)
  205. assert _run_reader(thunk(), buf, do_eof) == expected
  206. # Incrementally growing buffer
  207. print("Test 2")
  208. reader = thunk()
  209. buf = ReceiveBuffer()
  210. events = []
  211. for i in range(len(data)):
  212. events += _run_reader(reader, buf, False)
  213. buf += data[i:i + 1]
  214. events += _run_reader(reader, buf, do_eof)
  215. assert normalize_data_events(events) == expected
  216. is_complete = any(type(event) is EndOfMessage for event in expected)
  217. if is_complete and not do_eof:
  218. buf = makebuf(data + b"trailing")
  219. assert _run_reader(thunk(), buf, False) == expected
  220. def test_ContentLengthReader():
  221. t_body_reader(lambda: ContentLengthReader(0),
  222. b"",
  223. [EndOfMessage()])
  224. t_body_reader(lambda: ContentLengthReader(10),
  225. b"0123456789",
  226. [Data(data=b"0123456789"), EndOfMessage()])
  227. def test_Http10Reader():
  228. t_body_reader(Http10Reader, b"", [EndOfMessage()], do_eof=True)
  229. t_body_reader(Http10Reader, b"asdf",
  230. [Data(data=b"asdf")], do_eof=False)
  231. t_body_reader(Http10Reader, b"asdf",
  232. [Data(data=b"asdf"), EndOfMessage()], do_eof=True)
  233. def test_ChunkedReader():
  234. t_body_reader(ChunkedReader, b"0\r\n\r\n", [EndOfMessage()])
  235. t_body_reader(ChunkedReader,
  236. b"0\r\nSome: header\r\n\r\n",
  237. [EndOfMessage(headers=[("Some", "header")])])
  238. t_body_reader(ChunkedReader,
  239. b"5\r\n01234\r\n"
  240. + b"10\r\n0123456789abcdef\r\n"
  241. + b"0\r\n"
  242. + b"Some: header\r\n\r\n",
  243. [Data(data=b"012340123456789abcdef"),
  244. EndOfMessage(headers=[("Some", "header")])])
  245. t_body_reader(ChunkedReader,
  246. b"5\r\n01234\r\n"
  247. + b"10\r\n0123456789abcdef\r\n"
  248. + b"0\r\n\r\n",
  249. [Data(data=b"012340123456789abcdef"), EndOfMessage()])
  250. # handles upper and lowercase hex
  251. t_body_reader(ChunkedReader,
  252. b"aA\r\n"
  253. + b"x" * 0xaa + b"\r\n"
  254. + b"0\r\n\r\n",
  255. [Data(data=b"x" * 0xaa), EndOfMessage()])
  256. # refuses arbitrarily long chunk integers
  257. with pytest.raises(LocalProtocolError):
  258. # Technically this is legal HTTP/1.1, but we refuse to process chunk
  259. # sizes that don't fit into 20 characters of hex
  260. t_body_reader(ChunkedReader,
  261. b"9" * 100
  262. + b"\r\nxxx",
  263. [Data(data=b"xxx")])
  264. # refuses garbage in the chunk count
  265. with pytest.raises(LocalProtocolError):
  266. t_body_reader(ChunkedReader,
  267. b"10\x00\r\nxxx",
  268. None)
  269. # handles (and discards) "chunk extensions" omg wtf
  270. t_body_reader(ChunkedReader,
  271. b"5; hello=there\r\n"
  272. + b"xxxxx" + b"\r\n"
  273. + b"0; random=\"junk\"; some=more; canbe=lonnnnngg\r\n\r\n",
  274. [Data(data=b"xxxxx"), EndOfMessage()])
  275. def test_ContentLengthWriter():
  276. w = ContentLengthWriter(5)
  277. assert dowrite(w, Data(data=b"123")) == b"123"
  278. assert dowrite(w, Data(data=b"45")) == b"45"
  279. assert dowrite(w, EndOfMessage()) == b""
  280. w = ContentLengthWriter(5)
  281. with pytest.raises(LocalProtocolError):
  282. dowrite(w, Data(data=b"123456"))
  283. w = ContentLengthWriter(5)
  284. dowrite(w, Data(data=b"123"))
  285. with pytest.raises(LocalProtocolError):
  286. dowrite(w, Data(data=b"456"))
  287. w = ContentLengthWriter(5)
  288. dowrite(w, Data(data=b"123"))
  289. with pytest.raises(LocalProtocolError):
  290. dowrite(w, EndOfMessage())
  291. w = ContentLengthWriter(5)
  292. dowrite(w, Data(data=b"123")) == b"123"
  293. dowrite(w, Data(data=b"45")) == b"45"
  294. with pytest.raises(LocalProtocolError):
  295. dowrite(w, EndOfMessage(headers=[("Etag", "asdf")]))
  296. def test_ChunkedWriter():
  297. w = ChunkedWriter()
  298. assert dowrite(w, Data(data=b"aaa")) == b"3\r\naaa\r\n"
  299. assert dowrite(w, Data(data=b"a" * 20)) == b"14\r\n" + b"a" * 20 + b"\r\n"
  300. assert dowrite(w, Data(data=b"")) == b""
  301. assert dowrite(w, EndOfMessage()) == b"0\r\n\r\n"
  302. assert (dowrite(w, EndOfMessage(headers=[("Etag", "asdf"), ("a", "b")]))
  303. == b"0\r\netag: asdf\r\na: b\r\n\r\n")
  304. def test_Http10Writer():
  305. w = Http10Writer()
  306. assert dowrite(w, Data(data=b"1234")) == b"1234"
  307. assert dowrite(w, EndOfMessage()) == b""
  308. with pytest.raises(LocalProtocolError):
  309. dowrite(w, EndOfMessage(headers=[("Etag", "asdf")]))
  310. def test_reject_garbage_after_request_line():
  311. with pytest.raises(LocalProtocolError):
  312. tr(READERS[SERVER, SEND_RESPONSE],
  313. b"HTTP/1.0 200 OK\x00xxxx\r\n\r\n",
  314. None)
  315. def test_reject_garbage_after_response_line():
  316. with pytest.raises(LocalProtocolError):
  317. tr(READERS[CLIENT, IDLE],
  318. b"HEAD /foo HTTP/1.1 xxxxxx\r\n"
  319. b"Host: a\r\n\r\n",
  320. None)
  321. def test_reject_garbage_in_header_line():
  322. with pytest.raises(LocalProtocolError):
  323. tr(READERS[CLIENT, IDLE],
  324. b"HEAD /foo HTTP/1.1\r\n"
  325. b"Host: foo\x00bar\r\n\r\n",
  326. None)
  327. def test_host_comes_first():
  328. tw(write_headers,
  329. normalize_and_validate([("foo", "bar"), ("Host", "example.com")]),
  330. b"host: example.com\r\nfoo: bar\r\n\r\n")