Format: text/x-python
⬇ Download File (37.8 KB)
   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