1 from typing import Any, cast, Dict, List, Optional, Tuple, Type 2 3 import pytest 4 5 from .._connection import _body_framing, _keep_alive, Connection, NEED_DATA, PAUSED 6 from .._events import ( 7 ConnectionClosed, 8 Data, 9 EndOfMessage, 10 Event, 11 InformationalResponse, 12 Request, 13 Response, 14 ) 15 from .._state import ( 16 CLIENT, 17 CLOSED, 18 DONE, 19 ERROR, 20 IDLE, 21 MIGHT_SWITCH_PROTOCOL, 22 MUST_CLOSE, 23 SEND_BODY, 24 SEND_RESPONSE, 25 SERVER, 26 SWITCHED_PROTOCOL, 27 ) 28 from .._util import LocalProtocolError, RemoteProtocolError, Sentinel 29 from .helpers import ConnectionPair, get_all_events, receive_and_get 30 31 32 def test__keep_alive() -> None: 33 assert _keep_alive( 34 Request(method="GET", target="/", headers=[("Host", "Example.com")]) 35 ) 36 assert not _keep_alive( 37 Request( 38 method="GET", 39 target="/", 40 headers=[("Host", "Example.com"), ("Connection", "close")], 41 ) 42 ) 43 assert not _keep_alive( 44 Request( 45 method="GET", 46 target="/", 47 headers=[("Host", "Example.com"), ("Connection", "a, b, cLOse, foo")], 48 ) 49 ) 50 assert not _keep_alive( 51 Request(method="GET", target="/", headers=[], http_version="1.0") # type: ignore[arg-type] 52 ) 53 54 assert _keep_alive(Response(status_code=200, headers=[])) # type: ignore[arg-type] 55 assert not _keep_alive(Response(status_code=200, headers=[("Connection", "close")])) 56 assert not _keep_alive( 57 Response(status_code=200, headers=[("Connection", "a, b, cLOse, foo")]) 58 ) 59 assert not _keep_alive(Response(status_code=200, headers=[], http_version="1.0")) # type: ignore[arg-type] 60 61 62 def test__body_framing() -> None: 63 def headers(cl: Optional[int], te: bool) -> List[Tuple[str, str]]: 64 headers = [] 65 if cl is not None: 66 headers.append(("Content-Length", str(cl))) 67 if te: 68 headers.append(("Transfer-Encoding", "chunked")) 69 return headers 70 71 def resp( 72 status_code: int = 200, cl: Optional[int] = None, te: bool = False 73 ) -> Response: 74 return Response(status_code=status_code, headers=headers(cl, te)) 75 76 def req(cl: Optional[int] = None, te: bool = False) -> Request: 77 h = headers(cl, te) 78 h += [("Host", "example.com")] 79 return Request(method="GET", target="/", headers=h) 80 81 # Special cases where the headers are ignored: 82 for kwargs in [{}, {"cl": 100}, {"te": True}, {"cl": 100, "te": True}]: 83 kwargs = cast(Dict[str, Any], kwargs) 84 for meth, r in [ 85 (b"HEAD", resp(**kwargs)), 86 (b"GET", resp(status_code=204, **kwargs)), 87 (b"GET", resp(status_code=304, **kwargs)), 88 ]: 89 assert _body_framing(meth, r) == ("content-length", (0,)) 90 91 # Transfer-encoding 92 for kwargs in [{"te": True}, {"cl": 100, "te": True}]: 93 kwargs = cast(Dict[str, Any], kwargs) 94 for meth, r in [(None, req(**kwargs)), (b"GET", resp(**kwargs))]: # type: ignore 95 assert _body_framing(meth, r) == ("chunked", ()) 96 97 # Content-Length 98 for meth, r in [(None, req(cl=100)), (b"GET", resp(cl=100))]: # type: ignore 99 assert _body_framing(meth, r) == ("content-length", (100,)) 100 101 # No headers 102 assert _body_framing(None, req()) == ("content-length", (0,)) # type: ignore 103 assert _body_framing(b"GET", resp()) == ("http/1.0", ()) 104 105 106 def test_Connection_basics_and_content_length() -> None: 107 with pytest.raises(ValueError): 108 Connection("CLIENT") # type: ignore 109 110 p = ConnectionPair() 111 assert p.conn[CLIENT].our_role is CLIENT 112 assert p.conn[CLIENT].their_role is SERVER 113 assert p.conn[SERVER].our_role is SERVER 114 assert p.conn[SERVER].their_role is CLIENT 115 116 data = p.send( 117 CLIENT, 118 Request( 119 method="GET", 120 target="/", 121 headers=[("Host", "example.com"), ("Content-Length", "10")], 122 ), 123 ) 124 assert data == ( 125 b"GET / HTTP/1.1\r\n" b"Host: example.com\r\n" b"Content-Length: 10\r\n\r\n" 126 ) 127 128 for conn in p.conns: 129 assert conn.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE} 130 assert p.conn[CLIENT].our_state is SEND_BODY 131 assert p.conn[CLIENT].their_state is SEND_RESPONSE 132 assert p.conn[SERVER].our_state is SEND_RESPONSE 133 assert p.conn[SERVER].their_state is SEND_BODY 134 135 assert p.conn[CLIENT].their_http_version is None 136 assert p.conn[SERVER].their_http_version == b"1.1" 137 138 data = p.send(SERVER, InformationalResponse(status_code=100, headers=[])) # type: ignore[arg-type] 139 assert data == b"HTTP/1.1 100 \r\n\r\n" 140 141 data = p.send(SERVER, Response(status_code=200, headers=[("Content-Length", "11")])) 142 assert data == b"HTTP/1.1 200 \r\nContent-Length: 11\r\n\r\n" 143 144 for conn in p.conns: 145 assert conn.states == {CLIENT: SEND_BODY, SERVER: SEND_BODY} 146 147 assert p.conn[CLIENT].their_http_version == b"1.1" 148 assert p.conn[SERVER].their_http_version == b"1.1" 149 150 data = p.send(CLIENT, Data(data=b"12345")) 151 assert data == b"12345" 152 data = p.send( 153 CLIENT, Data(data=b"67890"), expect=[Data(data=b"67890"), EndOfMessage()] 154 ) 155 assert data == b"67890" 156 data = p.send(CLIENT, EndOfMessage(), expect=[]) 157 assert data == b"" 158 159 for conn in p.conns: 160 assert conn.states == {CLIENT: DONE, SERVER: SEND_BODY} 161 162 data = p.send(SERVER, Data(data=b"1234567890")) 163 assert data == b"1234567890" 164 data = p.send(SERVER, Data(data=b"1"), expect=[Data(data=b"1"), EndOfMessage()]) 165 assert data == b"1" 166 data = p.send(SERVER, EndOfMessage(), expect=[]) 167 assert data == b"" 168 169 for conn in p.conns: 170 assert conn.states == {CLIENT: DONE, SERVER: DONE} 171 172 173 def test_chunked() -> None: 174 p = ConnectionPair() 175 176 p.send( 177 CLIENT, 178 Request( 179 method="GET", 180 target="/", 181 headers=[("Host", "example.com"), ("Transfer-Encoding", "chunked")], 182 ), 183 ) 184 data = p.send(CLIENT, Data(data=b"1234567890", chunk_start=True, chunk_end=True)) 185 assert data == b"a\r\n1234567890\r\n" 186 data = p.send(CLIENT, Data(data=b"abcde", chunk_start=True, chunk_end=True)) 187 assert data == b"5\r\nabcde\r\n" 188 data = p.send(CLIENT, Data(data=b""), expect=[]) 189 assert data == b"" 190 data = p.send(CLIENT, EndOfMessage(headers=[("hello", "there")])) 191 assert data == b"0\r\nhello: there\r\n\r\n" 192 193 p.send( 194 SERVER, Response(status_code=200, headers=[("Transfer-Encoding", "chunked")]) 195 ) 196 p.send(SERVER, Data(data=b"54321", chunk_start=True, chunk_end=True)) 197 p.send(SERVER, Data(data=b"12345", chunk_start=True, chunk_end=True)) 198 p.send(SERVER, EndOfMessage()) 199 200 for conn in p.conns: 201 assert conn.states == {CLIENT: DONE, SERVER: DONE} 202 203 204 def test_chunk_boundaries() -> None: 205 conn = Connection(our_role=SERVER) 206 207 request = ( 208 b"POST / HTTP/1.1\r\n" 209 b"Host: example.com\r\n" 210 b"Transfer-Encoding: chunked\r\n" 211 b"\r\n" 212 ) 213 conn.receive_data(request) 214 assert conn.next_event() == Request( 215 method="POST", 216 target="/", 217 headers=[("Host", "example.com"), ("Transfer-Encoding", "chunked")], 218 ) 219 assert conn.next_event() is NEED_DATA 220 221 conn.receive_data(b"5\r\nhello\r\n") 222 assert conn.next_event() == Data(data=b"hello", chunk_start=True, chunk_end=True) 223 224 conn.receive_data(b"5\r\nhel") 225 assert conn.next_event() == Data(data=b"hel", chunk_start=True, chunk_end=False) 226 227 conn.receive_data(b"l") 228 assert conn.next_event() == Data(data=b"l", chunk_start=False, chunk_end=False) 229 230 conn.receive_data(b"o\r\n") 231 assert conn.next_event() == Data(data=b"o", chunk_start=False, chunk_end=True) 232 233 conn.receive_data(b"5\r\nhello") 234 assert conn.next_event() == Data(data=b"hello", chunk_start=True, chunk_end=True) 235 236 conn.receive_data(b"\r\n") 237 assert conn.next_event() == NEED_DATA 238 239 conn.receive_data(b"0\r\n\r\n") 240 assert conn.next_event() == EndOfMessage() 241 242 243 def test_client_talking_to_http10_server() -> None: 244 c = Connection(CLIENT) 245 c.send(Request(method="GET", target="/", headers=[("Host", "example.com")])) 246 c.send(EndOfMessage()) 247 assert c.our_state is DONE 248 # No content-length, so Http10 framing for body 249 assert receive_and_get(c, b"HTTP/1.0 200 OK\r\n\r\n") == [ 250 Response(status_code=200, headers=[], http_version="1.0", reason=b"OK") # type: ignore[arg-type] 251 ] 252 assert c.our_state is MUST_CLOSE 253 assert receive_and_get(c, b"12345") == [Data(data=b"12345")] 254 assert receive_and_get(c, b"67890") == [Data(data=b"67890")] 255 assert receive_and_get(c, b"") == [EndOfMessage(), ConnectionClosed()] 256 assert c.their_state is CLOSED 257 258 259 def test_server_talking_to_http10_client() -> None: 260 c = Connection(SERVER) 261 # No content-length, so no body 262 # NB: no host header 263 assert receive_and_get(c, b"GET / HTTP/1.0\r\n\r\n") == [ 264 Request(method="GET", target="/", headers=[], http_version="1.0"), # type: ignore[arg-type] 265 EndOfMessage(), 266 ] 267 assert c.their_state is MUST_CLOSE 268 269 # We automatically Connection: close back at them 270 assert ( 271 c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type] 272 == b"HTTP/1.1 200 \r\nConnection: close\r\n\r\n" 273 ) 274 275 assert c.send(Data(data=b"12345")) == b"12345" 276 assert c.send(EndOfMessage()) == b"" 277 assert c.our_state is MUST_CLOSE 278 279 # Check that it works if they do send Content-Length 280 c = Connection(SERVER) 281 # NB: no host header 282 assert receive_and_get(c, b"POST / HTTP/1.0\r\nContent-Length: 10\r\n\r\n1") == [ 283 Request( 284 method="POST", 285 target="/", 286 headers=[("Content-Length", "10")], 287 http_version="1.0", 288 ), 289 Data(data=b"1"), 290 ] 291 assert receive_and_get(c, b"234567890") == [Data(data=b"234567890"), EndOfMessage()] 292 assert c.their_state is MUST_CLOSE 293 assert receive_and_get(c, b"") == [ConnectionClosed()] 294 295 296 def test_automatic_transfer_encoding_in_response() -> None: 297 # Check that in responses, the user can specify either Transfer-Encoding: 298 # chunked or no framing at all, and in both cases we automatically select 299 # the right option depending on whether the peer speaks HTTP/1.0 or 300 # HTTP/1.1 301 for user_headers in [ 302 [("Transfer-Encoding", "chunked")], 303 [], 304 # In fact, this even works if Content-Length is set, 305 # because if both are set then Transfer-Encoding wins 306 [("Transfer-Encoding", "chunked"), ("Content-Length", "100")], 307 ]: 308 user_headers = cast(List[Tuple[str, str]], user_headers) 309 p = ConnectionPair() 310 p.send( 311 CLIENT, 312 [ 313 Request(method="GET", target="/", headers=[("Host", "example.com")]), 314 EndOfMessage(), 315 ], 316 ) 317 # When speaking to HTTP/1.1 client, all of the above cases get 318 # normalized to Transfer-Encoding: chunked 319 p.send( 320 SERVER, 321 Response(status_code=200, headers=user_headers), 322 expect=Response( 323 status_code=200, headers=[("Transfer-Encoding", "chunked")] 324 ), 325 ) 326 327 # When speaking to HTTP/1.0 client, all of the above cases get 328 # normalized to no-framing-headers 329 c = Connection(SERVER) 330 receive_and_get(c, b"GET / HTTP/1.0\r\n\r\n") 331 assert ( 332 c.send(Response(status_code=200, headers=user_headers)) 333 == b"HTTP/1.1 200 \r\nConnection: close\r\n\r\n" 334 ) 335 assert c.send(Data(data=b"12345")) == b"12345" 336 337 338 def test_automagic_connection_close_handling() -> None: 339 p = ConnectionPair() 340 # If the user explicitly sets Connection: close, then we notice and 341 # respect it 342 p.send( 343 CLIENT, 344 [ 345 Request( 346 method="GET", 347 target="/", 348 headers=[("Host", "example.com"), ("Connection", "close")], 349 ), 350 EndOfMessage(), 351 ], 352 ) 353 for conn in p.conns: 354 assert conn.states[CLIENT] is MUST_CLOSE 355 # And if the client sets it, the server automatically echoes it back 356 p.send( 357 SERVER, 358 # no header here... 359 [Response(status_code=204, headers=[]), EndOfMessage()], # type: ignore[arg-type] 360 # ...but oh look, it arrived anyway 361 expect=[ 362 Response(status_code=204, headers=[("connection", "close")]), 363 EndOfMessage(), 364 ], 365 ) 366 for conn in p.conns: 367 assert conn.states == {CLIENT: MUST_CLOSE, SERVER: MUST_CLOSE} 368 369 370 def test_100_continue() -> None: 371 def setup() -> ConnectionPair: 372 p = ConnectionPair() 373 p.send( 374 CLIENT, 375 Request( 376 method="GET", 377 target="/", 378 headers=[ 379 ("Host", "example.com"), 380 ("Content-Length", "100"), 381 ("Expect", "100-continue"), 382 ], 383 ), 384 ) 385 for conn in p.conns: 386 assert conn.client_is_waiting_for_100_continue 387 assert not p.conn[CLIENT].they_are_waiting_for_100_continue 388 assert p.conn[SERVER].they_are_waiting_for_100_continue 389 return p 390 391 # Disabled by 100 Continue 392 p = setup() 393 p.send(SERVER, InformationalResponse(status_code=100, headers=[])) # type: ignore[arg-type] 394 for conn in p.conns: 395 assert not conn.client_is_waiting_for_100_continue 396 assert not conn.they_are_waiting_for_100_continue 397 398 # Disabled by a real response 399 p = setup() 400 p.send( 401 SERVER, Response(status_code=200, headers=[("Transfer-Encoding", "chunked")]) 402 ) 403 for conn in p.conns: 404 assert not conn.client_is_waiting_for_100_continue 405 assert not conn.they_are_waiting_for_100_continue 406 407 # Disabled by the client going ahead and sending stuff anyway 408 p = setup() 409 p.send(CLIENT, Data(data=b"12345")) 410 for conn in p.conns: 411 assert not conn.client_is_waiting_for_100_continue 412 assert not conn.they_are_waiting_for_100_continue 413 414 415 def test_max_incomplete_event_size_countermeasure() -> None: 416 # Infinitely long headers are definitely not okay 417 c = Connection(SERVER) 418 c.receive_data(b"GET / HTTP/1.0\r\nEndless: ") 419 assert c.next_event() is NEED_DATA 420 with pytest.raises(RemoteProtocolError): 421 while True: 422 c.receive_data(b"a" * 1024) 423 c.next_event() 424 425 # Checking that the same header is accepted / rejected depending on the 426 # max_incomplete_event_size setting: 427 c = Connection(SERVER, max_incomplete_event_size=5000) 428 c.receive_data(b"GET / HTTP/1.0\r\nBig: ") 429 c.receive_data(b"a" * 4000) 430 c.receive_data(b"\r\n\r\n") 431 assert get_all_events(c) == [ 432 Request( 433 method="GET", target="/", http_version="1.0", headers=[("big", "a" * 4000)] 434 ), 435 EndOfMessage(), 436 ] 437 438 c = Connection(SERVER, max_incomplete_event_size=4000) 439 c.receive_data(b"GET / HTTP/1.0\r\nBig: ") 440 c.receive_data(b"a" * 4000) 441 with pytest.raises(RemoteProtocolError): 442 c.next_event() 443 444 # Temporarily exceeding the size limit is fine, as long as its done with 445 # complete events: 446 c = Connection(SERVER, max_incomplete_event_size=5000) 447 c.receive_data(b"GET / HTTP/1.0\r\nContent-Length: 10000") 448 c.receive_data(b"\r\n\r\n" + b"a" * 10000) 449 assert get_all_events(c) == [ 450 Request( 451 method="GET", 452 target="/", 453 http_version="1.0", 454 headers=[("Content-Length", "10000")], 455 ), 456 Data(data=b"a" * 10000), 457 EndOfMessage(), 458 ] 459 460 c = Connection(SERVER, max_incomplete_event_size=100) 461 # Two pipelined requests to create a way-too-big receive buffer... but 462 # it's fine because we're not checking 463 c.receive_data( 464 b"GET /1 HTTP/1.1\r\nHost: a\r\n\r\n" 465 b"GET /2 HTTP/1.1\r\nHost: b\r\n\r\n" + b"X" * 1000 466 ) 467 assert get_all_events(c) == [ 468 Request(method="GET", target="/1", headers=[("host", "a")]), 469 EndOfMessage(), 470 ] 471 # Even more data comes in, still no problem 472 c.receive_data(b"X" * 1000) 473 # We can respond and reuse to get the second pipelined request 474 c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type] 475 c.send(EndOfMessage()) 476 c.start_next_cycle() 477 assert get_all_events(c) == [ 478 Request(method="GET", target="/2", headers=[("host", "b")]), 479 EndOfMessage(), 480 ] 481 # But once we unpause and try to read the next message, and find that it's 482 # incomplete and the buffer is *still* way too large, then *that's* a 483 # problem: 484 c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type] 485 c.send(EndOfMessage()) 486 c.start_next_cycle() 487 with pytest.raises(RemoteProtocolError): 488 c.next_event() 489 490 491 def test_reuse_simple() -> None: 492 p = ConnectionPair() 493 p.send( 494 CLIENT, 495 [Request(method="GET", target="/", headers=[("Host", "a")]), EndOfMessage()], 496 ) 497 p.send( 498 SERVER, 499 [ 500 Response(status_code=200, headers=[(b"transfer-encoding", b"chunked")]), 501 EndOfMessage(), 502 ], 503 ) 504 for conn in p.conns: 505 assert conn.states == {CLIENT: DONE, SERVER: DONE} 506 conn.start_next_cycle() 507 508 p.send( 509 CLIENT, 510 [ 511 Request(method="DELETE", target="/foo", headers=[("Host", "a")]), 512 EndOfMessage(), 513 ], 514 ) 515 p.send( 516 SERVER, 517 [ 518 Response(status_code=404, headers=[(b"transfer-encoding", b"chunked")]), 519 EndOfMessage(), 520 ], 521 ) 522 523 524 def test_pipelining() -> None: 525 # Client doesn't support pipelining, so we have to do this by hand 526 c = Connection(SERVER) 527 assert c.next_event() is NEED_DATA 528 # 3 requests all bunched up 529 c.receive_data( 530 b"GET /1 HTTP/1.1\r\nHost: a.com\r\nContent-Length: 5\r\n\r\n" 531 b"12345" 532 b"GET /2 HTTP/1.1\r\nHost: a.com\r\nContent-Length: 5\r\n\r\n" 533 b"67890" 534 b"GET /3 HTTP/1.1\r\nHost: a.com\r\n\r\n" 535 ) 536 assert get_all_events(c) == [ 537 Request( 538 method="GET", 539 target="/1", 540 headers=[("Host", "a.com"), ("Content-Length", "5")], 541 ), 542 Data(data=b"12345"), 543 EndOfMessage(), 544 ] 545 assert c.their_state is DONE 546 assert c.our_state is SEND_RESPONSE 547 548 assert c.next_event() is PAUSED 549 550 c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type] 551 c.send(EndOfMessage()) 552 assert c.their_state is DONE 553 assert c.our_state is DONE 554 555 c.start_next_cycle() 556 557 assert get_all_events(c) == [ 558 Request( 559 method="GET", 560 target="/2", 561 headers=[("Host", "a.com"), ("Content-Length", "5")], 562 ), 563 Data(data=b"67890"), 564 EndOfMessage(), 565 ] 566 assert c.next_event() is PAUSED 567 c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type] 568 c.send(EndOfMessage()) 569 c.start_next_cycle() 570 571 assert get_all_events(c) == [ 572 Request(method="GET", target="/3", headers=[("Host", "a.com")]), 573 EndOfMessage(), 574 ] 575 # Doesn't pause this time, no trailing data 576 assert c.next_event() is NEED_DATA 577 c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type] 578 c.send(EndOfMessage()) 579 580 # Arrival of more data triggers pause 581 assert c.next_event() is NEED_DATA 582 c.receive_data(b"SADF") 583 assert c.next_event() is PAUSED 584 assert c.trailing_data == (b"SADF", False) 585 # If EOF arrives while paused, we don't see that either: 586 c.receive_data(b"") 587 assert c.trailing_data == (b"SADF", True) 588 assert c.next_event() is PAUSED 589 c.receive_data(b"") 590 assert c.next_event() is PAUSED 591 # Can't call receive_data with non-empty buf after closing it 592 with pytest.raises(RuntimeError): 593 c.receive_data(b"FDSA") 594 595 596 def test_protocol_switch() -> None: 597 for (req, deny, accept) in [ 598 ( 599 Request( 600 method="CONNECT", 601 target="example.com:443", 602 headers=[("Host", "foo"), ("Content-Length", "1")], 603 ), 604 Response(status_code=404, headers=[(b"transfer-encoding", b"chunked")]), 605 Response(status_code=200, headers=[(b"transfer-encoding", b"chunked")]), 606 ), 607 ( 608 Request( 609 method="GET", 610 target="/", 611 headers=[("Host", "foo"), ("Content-Length", "1"), ("Upgrade", "a, b")], 612 ), 613 Response(status_code=200, headers=[(b"transfer-encoding", b"chunked")]), 614 InformationalResponse(status_code=101, headers=[("Upgrade", "a")]), 615 ), 616 ( 617 Request( 618 method="CONNECT", 619 target="example.com:443", 620 headers=[("Host", "foo"), ("Content-Length", "1"), ("Upgrade", "a, b")], 621 ), 622 Response(status_code=404, headers=[(b"transfer-encoding", b"chunked")]), 623 # Accept CONNECT, not upgrade 624 Response(status_code=200, headers=[(b"transfer-encoding", b"chunked")]), 625 ), 626 ( 627 Request( 628 method="CONNECT", 629 target="example.com:443", 630 headers=[("Host", "foo"), ("Content-Length", "1"), ("Upgrade", "a, b")], 631 ), 632 Response(status_code=404, headers=[(b"transfer-encoding", b"chunked")]), 633 # Accept Upgrade, not CONNECT 634 InformationalResponse(status_code=101, headers=[("Upgrade", "b")]), 635 ), 636 ]: 637 638 def setup() -> ConnectionPair: 639 p = ConnectionPair() 640 p.send(CLIENT, req) 641 # No switch-related state change stuff yet; the client has to 642 # finish the request before that kicks in 643 for conn in p.conns: 644 assert conn.states[CLIENT] is SEND_BODY 645 p.send(CLIENT, [Data(data=b"1"), EndOfMessage()]) 646 for conn in p.conns: 647 assert conn.states[CLIENT] is MIGHT_SWITCH_PROTOCOL 648 assert p.conn[SERVER].next_event() is PAUSED 649 return p 650 651 # Test deny case 652 p = setup() 653 p.send(SERVER, deny) 654 for conn in p.conns: 655 assert conn.states == {CLIENT: DONE, SERVER: SEND_BODY} 656 p.send(SERVER, EndOfMessage()) 657 # Check that re-use is still allowed after a denial 658 for conn in p.conns: 659 conn.start_next_cycle() 660 661 # Test accept case 662 p = setup() 663 p.send(SERVER, accept) 664 for conn in p.conns: 665 assert conn.states == {CLIENT: SWITCHED_PROTOCOL, SERVER: SWITCHED_PROTOCOL} 666 conn.receive_data(b"123") 667 assert conn.next_event() is PAUSED 668 conn.receive_data(b"456") 669 assert conn.next_event() is PAUSED 670 assert conn.trailing_data == (b"123456", False) 671 672 # Pausing in might-switch, then recovery 673 # (weird artificial case where the trailing data actually is valid 674 # HTTP for some reason, because this makes it easier to test the state 675 # logic) 676 p = setup() 677 sc = p.conn[SERVER] 678 sc.receive_data(b"GET / HTTP/1.0\r\n\r\n") 679 assert sc.next_event() is PAUSED 680 assert sc.trailing_data == (b"GET / HTTP/1.0\r\n\r\n", False) 681 sc.send(deny) 682 assert sc.next_event() is PAUSED 683 sc.send(EndOfMessage()) 684 sc.start_next_cycle() 685 assert get_all_events(sc) == [ 686 Request(method="GET", target="/", headers=[], http_version="1.0"), # type: ignore[arg-type] 687 EndOfMessage(), 688 ] 689 690 # When we're DONE, have no trailing data, and the connection gets 691 # closed, we report ConnectionClosed(). When we're in might-switch or 692 # switched, we don't. 693 p = setup() 694 sc = p.conn[SERVER] 695 sc.receive_data(b"") 696 assert sc.next_event() is PAUSED 697 assert sc.trailing_data == (b"", True) 698 p.send(SERVER, accept) 699 assert sc.next_event() is PAUSED 700 701 p = setup() 702 sc = p.conn[SERVER] 703 sc.receive_data(b"") 704 assert sc.next_event() is PAUSED 705 sc.send(deny) 706 assert sc.next_event() == ConnectionClosed() 707 708 # You can't send after switching protocols, or while waiting for a 709 # protocol switch 710 p = setup() 711 with pytest.raises(LocalProtocolError): 712 p.conn[CLIENT].send( 713 Request(method="GET", target="/", headers=[("Host", "a")]) 714 ) 715 p = setup() 716 p.send(SERVER, accept) 717 with pytest.raises(LocalProtocolError): 718 p.conn[SERVER].send(Data(data=b"123")) 719 720 721 def test_close_simple() -> None: 722 # Just immediately closing a new connection without anything having 723 # happened yet. 724 for (who_shot_first, who_shot_second) in [(CLIENT, SERVER), (SERVER, CLIENT)]: 725 726 def setup() -> ConnectionPair: 727 p = ConnectionPair() 728 p.send(who_shot_first, ConnectionClosed()) 729 for conn in p.conns: 730 assert conn.states == { 731 who_shot_first: CLOSED, 732 who_shot_second: MUST_CLOSE, 733 } 734 return p 735 736 # You can keep putting b"" into a closed connection, and you keep 737 # getting ConnectionClosed() out: 738 p = setup() 739 assert p.conn[who_shot_second].next_event() == ConnectionClosed() 740 assert p.conn[who_shot_second].next_event() == ConnectionClosed() 741 p.conn[who_shot_second].receive_data(b"") 742 assert p.conn[who_shot_second].next_event() == ConnectionClosed() 743 # Second party can close... 744 p = setup() 745 p.send(who_shot_second, ConnectionClosed()) 746 for conn in p.conns: 747 assert conn.our_state is CLOSED 748 assert conn.their_state is CLOSED 749 # But trying to receive new data on a closed connection is a 750 # RuntimeError (not ProtocolError, because the problem here isn't 751 # violation of HTTP, it's violation of physics) 752 p = setup() 753 with pytest.raises(RuntimeError): 754 p.conn[who_shot_second].receive_data(b"123") 755 # And receiving new data on a MUST_CLOSE connection is a ProtocolError 756 p = setup() 757 p.conn[who_shot_first].receive_data(b"GET") 758 with pytest.raises(RemoteProtocolError): 759 p.conn[who_shot_first].next_event() 760 761 762 def test_close_different_states() -> None: 763 req = [ 764 Request(method="GET", target="/foo", headers=[("Host", "a")]), 765 EndOfMessage(), 766 ] 767 resp = [ 768 Response(status_code=200, headers=[(b"transfer-encoding", b"chunked")]), 769 EndOfMessage(), 770 ] 771 772 # Client before request 773 p = ConnectionPair() 774 p.send(CLIENT, ConnectionClosed()) 775 for conn in p.conns: 776 assert conn.states == {CLIENT: CLOSED, SERVER: MUST_CLOSE} 777 778 # Client after request 779 p = ConnectionPair() 780 p.send(CLIENT, req) 781 p.send(CLIENT, ConnectionClosed()) 782 for conn in p.conns: 783 assert conn.states == {CLIENT: CLOSED, SERVER: SEND_RESPONSE} 784 785 # Server after request -> not allowed 786 p = ConnectionPair() 787 p.send(CLIENT, req) 788 with pytest.raises(LocalProtocolError): 789 p.conn[SERVER].send(ConnectionClosed()) 790 p.conn[CLIENT].receive_data(b"") 791 with pytest.raises(RemoteProtocolError): 792 p.conn[CLIENT].next_event() 793 794 # Server after response 795 p = ConnectionPair() 796 p.send(CLIENT, req) 797 p.send(SERVER, resp) 798 p.send(SERVER, ConnectionClosed()) 799 for conn in p.conns: 800 assert conn.states == {CLIENT: MUST_CLOSE, SERVER: CLOSED} 801 802 # Both after closing (ConnectionClosed() is idempotent) 803 p = ConnectionPair() 804 p.send(CLIENT, req) 805 p.send(SERVER, resp) 806 p.send(CLIENT, ConnectionClosed()) 807 p.send(SERVER, ConnectionClosed()) 808 p.send(CLIENT, ConnectionClosed()) 809 p.send(SERVER, ConnectionClosed()) 810 811 # In the middle of sending -> not allowed 812 p = ConnectionPair() 813 p.send( 814 CLIENT, 815 Request( 816 method="GET", target="/", headers=[("Host", "a"), ("Content-Length", "10")] 817 ), 818 ) 819 with pytest.raises(LocalProtocolError): 820 p.conn[CLIENT].send(ConnectionClosed()) 821 p.conn[SERVER].receive_data(b"") 822 with pytest.raises(RemoteProtocolError): 823 p.conn[SERVER].next_event() 824 825 826 # Receive several requests and then client shuts down their side of the 827 # connection; we can respond to each 828 def test_pipelined_close() -> None: 829 c = Connection(SERVER) 830 # 2 requests then a close 831 c.receive_data( 832 b"GET /1 HTTP/1.1\r\nHost: a.com\r\nContent-Length: 5\r\n\r\n" 833 b"12345" 834 b"GET /2 HTTP/1.1\r\nHost: a.com\r\nContent-Length: 5\r\n\r\n" 835 b"67890" 836 ) 837 c.receive_data(b"") 838 assert get_all_events(c) == [ 839 Request( 840 method="GET", 841 target="/1", 842 headers=[("host", "a.com"), ("content-length", "5")], 843 ), 844 Data(data=b"12345"), 845 EndOfMessage(), 846 ] 847 assert c.states[CLIENT] is DONE 848 c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type] 849 c.send(EndOfMessage()) 850 assert c.states[SERVER] is DONE 851 c.start_next_cycle() 852 assert get_all_events(c) == [ 853 Request( 854 method="GET", 855 target="/2", 856 headers=[("host", "a.com"), ("content-length", "5")], 857 ), 858 Data(data=b"67890"), 859 EndOfMessage(), 860 ConnectionClosed(), 861 ] 862 assert c.states == {CLIENT: CLOSED, SERVER: SEND_RESPONSE} 863 c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type] 864 c.send(EndOfMessage()) 865 assert c.states == {CLIENT: CLOSED, SERVER: MUST_CLOSE} 866 c.send(ConnectionClosed()) 867 assert c.states == {CLIENT: CLOSED, SERVER: CLOSED} 868 869 870 def test_sendfile() -> None: 871 class SendfilePlaceholder: 872 def __len__(self) -> int: 873 return 10 874 875 placeholder = SendfilePlaceholder() 876 877 def setup( 878 header: Tuple[str, str], http_version: str 879 ) -> Tuple[Connection, Optional[List[bytes]]]: 880 c = Connection(SERVER) 881 receive_and_get( 882 c, "GET / HTTP/{}\r\nHost: a\r\n\r\n".format(http_version).encode("ascii") 883 ) 884 headers = [] 885 if header: 886 headers.append(header) 887 c.send(Response(status_code=200, headers=headers)) 888 return c, c.send_with_data_passthrough(Data(data=placeholder)) # type: ignore 889 890 c, data = setup(("Content-Length", "10"), "1.1") 891 assert data == [placeholder] # type: ignore 892 # Raises an error if the connection object doesn't think we've sent 893 # exactly 10 bytes 894 c.send(EndOfMessage()) 895 896 _, data = setup(("Transfer-Encoding", "chunked"), "1.1") 897 assert placeholder in data # type: ignore 898 data[data.index(placeholder)] = b"x" * 10 # type: ignore 899 assert b"".join(data) == b"a\r\nxxxxxxxxxx\r\n" # type: ignore 900 901 c, data = setup(None, "1.0") # type: ignore 902 assert data == [placeholder] # type: ignore 903 assert c.our_state is SEND_BODY 904 905 906 def test_errors() -> None: 907 # After a receive error, you can't receive 908 for role in [CLIENT, SERVER]: 909 c = Connection(our_role=role) 910 c.receive_data(b"gibberish\r\n\r\n") 911 with pytest.raises(RemoteProtocolError): 912 c.next_event() 913 # Now any attempt to receive continues to raise 914 assert c.their_state is ERROR 915 assert c.our_state is not ERROR 916 print(c._cstate.states) 917 with pytest.raises(RemoteProtocolError): 918 c.next_event() 919 # But we can still yell at the client for sending us gibberish 920 if role is SERVER: 921 assert ( 922 c.send(Response(status_code=400, headers=[])) # type: ignore[arg-type] 923 == b"HTTP/1.1 400 \r\nConnection: close\r\n\r\n" 924 ) 925 926 # After an error sending, you can no longer send 927 # (This is especially important for things like content-length errors, 928 # where there's complex internal state being modified) 929 def conn(role: Type[Sentinel]) -> Connection: 930 c = Connection(our_role=role) 931 if role is SERVER: 932 # Put it into the state where it *could* send a response... 933 receive_and_get(c, b"GET / HTTP/1.0\r\n\r\n") 934 assert c.our_state is SEND_RESPONSE 935 return c 936 937 for role in [CLIENT, SERVER]: 938 if role is CLIENT: 939 # This HTTP/1.0 request won't be detected as bad until after we go 940 # through the state machine and hit the writing code 941 good = Request(method="GET", target="/", headers=[("Host", "example.com")]) 942 bad = Request( 943 method="GET", 944 target="/", 945 headers=[("Host", "example.com")], 946 http_version="1.0", 947 ) 948 elif role is SERVER: 949 good = Response(status_code=200, headers=[]) # type: ignore[arg-type,assignment] 950 bad = Response(status_code=200, headers=[], http_version="1.0") # type: ignore[arg-type,assignment] 951 # Make sure 'good' actually is good 952 c = conn(role) 953 c.send(good) 954 assert c.our_state is not ERROR 955 # Do that again, but this time sending 'bad' first 956 c = conn(role) 957 with pytest.raises(LocalProtocolError): 958 c.send(bad) 959 assert c.our_state is ERROR 960 assert c.their_state is not ERROR 961 # Now 'good' is not so good 962 with pytest.raises(LocalProtocolError): 963 c.send(good) 964 965 # And check send_failed() too 966 c = conn(role) 967 c.send_failed() 968 assert c.our_state is ERROR 969 assert c.their_state is not ERROR 970 # This is idempotent 971 c.send_failed() 972 assert c.our_state is ERROR 973 assert c.their_state is not ERROR 974 975 976 def test_idle_receive_nothing() -> None: 977 # At one point this incorrectly raised an error 978 for role in [CLIENT, SERVER]: 979 c = Connection(role) 980 assert c.next_event() is NEED_DATA 981 982 983 def test_connection_drop() -> None: 984 c = Connection(SERVER) 985 c.receive_data(b"GET /") 986 assert c.next_event() is NEED_DATA 987 c.receive_data(b"") 988 with pytest.raises(RemoteProtocolError): 989 c.next_event() 990 991 992 def test_408_request_timeout() -> None: 993 # Should be able to send this spontaneously as a server without seeing 994 # anything from client 995 p = ConnectionPair() 996 p.send(SERVER, Response(status_code=408, headers=[(b"connection", b"close")])) 997 998 999 # This used to raise IndexError 1000 def test_empty_request() -> None: 1001 c = Connection(SERVER) 1002 c.receive_data(b"\r\n") 1003 with pytest.raises(RemoteProtocolError): 1004 c.next_event() 1005 1006 1007 # This used to raise IndexError 1008 def test_empty_response() -> None: 1009 c = Connection(CLIENT) 1010 c.send(Request(method="GET", target="/", headers=[("Host", "a")])) 1011 c.receive_data(b"\r\n") 1012 with pytest.raises(RemoteProtocolError): 1013 c.next_event() 1014 1015 1016 @pytest.mark.parametrize( 1017 "data", 1018 [ 1019 b"\x00", 1020 b"\x20", 1021 b"\x16\x03\x01\x00\xa5", # Typical start of a TLS Client Hello 1022 ], 1023 ) 1024 def test_early_detection_of_invalid_request(data: bytes) -> None: 1025 c = Connection(SERVER) 1026 # Early detection should occur before even receiving a `\r\n` 1027 c.receive_data(data) 1028 with pytest.raises(RemoteProtocolError): 1029 c.next_event() 1030 1031 1032 @pytest.mark.parametrize( 1033 "data", 1034 [ 1035 b"\x00", 1036 b"\x20", 1037 b"\x16\x03\x03\x00\x31", # Typical start of a TLS Server Hello 1038 ], 1039 ) 1040 def test_early_detection_of_invalid_response(data: bytes) -> None: 1041 c = Connection(CLIENT) 1042 # Early detection should occur before even receiving a `\r\n` 1043 c.receive_data(data) 1044 with pytest.raises(RemoteProtocolError): 1045 c.next_event() 1046 1047 1048 # This used to give different headers for HEAD and GET. 1049 # The correct way to handle HEAD is to put whatever headers we *would* have 1050 # put if it were a GET -- even though we know that for HEAD, those headers 1051 # will be ignored. 1052 def test_HEAD_framing_headers() -> None: 1053 def setup(method: bytes, http_version: bytes) -> Connection: 1054 c = Connection(SERVER) 1055 c.receive_data( 1056 method + b" / HTTP/" + http_version + b"\r\n" + b"Host: example.com\r\n\r\n" 1057 ) 1058 assert type(c.next_event()) is Request 1059 assert type(c.next_event()) is EndOfMessage 1060 return c 1061 1062 for method in [b"GET", b"HEAD"]: 1063 # No Content-Length, HTTP/1.1 peer, should use chunked 1064 c = setup(method, b"1.1") 1065 assert ( 1066 c.send(Response(status_code=200, headers=[])) == b"HTTP/1.1 200 \r\n" # type: ignore[arg-type] 1067 b"Transfer-Encoding: chunked\r\n\r\n" 1068 ) 1069 1070 # No Content-Length, HTTP/1.0 peer, frame with connection: close 1071 c = setup(method, b"1.0") 1072 assert ( 1073 c.send(Response(status_code=200, headers=[])) == b"HTTP/1.1 200 \r\n" # type: ignore[arg-type] 1074 b"Connection: close\r\n\r\n" 1075 ) 1076 1077 # Content-Length + Transfer-Encoding, TE wins 1078 c = setup(method, b"1.1") 1079 assert ( 1080 c.send( 1081 Response( 1082 status_code=200, 1083 headers=[ 1084 ("Content-Length", "100"), 1085 ("Transfer-Encoding", "chunked"), 1086 ], 1087 ) 1088 ) 1089 == b"HTTP/1.1 200 \r\n" 1090 b"Transfer-Encoding: chunked\r\n\r\n" 1091 ) 1092 1093 1094 def test_special_exceptions_for_lost_connection_in_message_body() -> None: 1095 c = Connection(SERVER) 1096 c.receive_data( 1097 b"POST / HTTP/1.1\r\n" b"Host: example.com\r\n" b"Content-Length: 100\r\n\r\n" 1098 ) 1099 assert type(c.next_event()) is Request 1100 assert c.next_event() is NEED_DATA 1101 c.receive_data(b"12345") 1102 assert c.next_event() == Data(data=b"12345") 1103 c.receive_data(b"") 1104 with pytest.raises(RemoteProtocolError) as excinfo: 1105 c.next_event() 1106 assert "received 5 bytes" in str(excinfo.value) 1107 assert "expected 100" in str(excinfo.value) 1108 1109 c = Connection(SERVER) 1110 c.receive_data( 1111 b"POST / HTTP/1.1\r\n" 1112 b"Host: example.com\r\n" 1113 b"Transfer-Encoding: chunked\r\n\r\n" 1114 ) 1115 assert type(c.next_event()) is Request 1116 assert c.next_event() is NEED_DATA 1117 c.receive_data(b"8\r\n012345") 1118 assert c.next_event().data == b"012345" # type: ignore 1119 c.receive_data(b"") 1120 with pytest.raises(RemoteProtocolError) as excinfo: 1121 c.next_event() 1122 assert "incomplete chunked read" in str(excinfo.value) 1123