From efe273f2feda3998c3b51f4de9b495037ae26935 Mon Sep 17 00:00:00 2001 From: Astronand Date: Wed, 14 Jan 2026 14:14:02 -0800 Subject: [PATCH] remove snip --- wip/SNIP/.idea/.gitignore | 3 - wip/SNIP/.idea/SNIP.iml | 8 - .../inspectionProfiles/profiles_settings.xml | 6 - wip/SNIP/.idea/misc.xml | 7 - wip/SNIP/.idea/modules.xml | 8 - wip/SNIP/client/chatServer.py | 122 -- wip/SNIP/client/load.py | 116 -- wip/SNIP/client/load2.py | 116 -- wip/SNIP/client/loadexp.py | 131 -- wip/SNIP/client/messenger.lua | 65 - wip/SNIP/client/messenger.py | 122 -- wip/SNIP/client/websockets/__init__.py | 236 --- wip/SNIP/client/websockets/__main__.py | 5 - .../__pycache__/__init__.cpython-313.pyc | Bin 4454 -> 0 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 5199 -> 0 bytes .../__pycache__/client.cpython-313.pyc | Bin 15968 -> 0 bytes .../__pycache__/client.cpython-314.pyc | Bin 17429 -> 0 bytes .../datastructures.cpython-313.pyc | Bin 8895 -> 0 bytes .../datastructures.cpython-314.pyc | Bin 11667 -> 0 bytes .../__pycache__/exceptions.cpython-313.pyc | Bin 18208 -> 0 bytes .../__pycache__/exceptions.cpython-314.pyc | Bin 21888 -> 0 bytes .../__pycache__/frames.cpython-313.pyc | Bin 15901 -> 0 bytes .../__pycache__/frames.cpython-314.pyc | Bin 17324 -> 0 bytes .../__pycache__/headers.cpython-313.pyc | Bin 17545 -> 0 bytes .../__pycache__/headers.cpython-314.pyc | Bin 20868 -> 0 bytes .../__pycache__/http11.cpython-313.pyc | Bin 15186 -> 0 bytes .../__pycache__/http11.cpython-314.pyc | Bin 16539 -> 0 bytes .../__pycache__/imports.cpython-313.pyc | Bin 3335 -> 0 bytes .../__pycache__/imports.cpython-314.pyc | Bin 3989 -> 0 bytes .../__pycache__/protocol.cpython-313.pyc | Bin 24671 -> 0 bytes .../__pycache__/protocol.cpython-314.pyc | Bin 27960 -> 0 bytes .../__pycache__/server.cpython-313.pyc | Bin 22309 -> 0 bytes .../__pycache__/streams.cpython-313.pyc | Bin 5231 -> 0 bytes .../__pycache__/streams.cpython-314.pyc | Bin 6278 -> 0 bytes .../__pycache__/typing.cpython-313.pyc | Bin 1229 -> 0 bytes .../__pycache__/typing.cpython-314.pyc | Bin 1305 -> 0 bytes .../__pycache__/uri.cpython-313.pyc | Bin 8117 -> 0 bytes .../__pycache__/uri.cpython-314.pyc | Bin 9077 -> 0 bytes .../__pycache__/utils.cpython-313.pyc | Bin 2163 -> 0 bytes .../__pycache__/utils.cpython-314.pyc | Bin 2621 -> 0 bytes .../__pycache__/version.cpython-313.pyc | Bin 2987 -> 0 bytes .../__pycache__/version.cpython-314.pyc | Bin 3303 -> 0 bytes .../client/websockets/asyncio/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 157 -> 0 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 159 -> 0 bytes .../__pycache__/client.cpython-313.pyc | Bin 33493 -> 0 bytes .../__pycache__/client.cpython-314.pyc | Bin 37436 -> 0 bytes .../__pycache__/compatibility.cpython-313.pyc | Bin 1034 -> 0 bytes .../__pycache__/compatibility.cpython-314.pyc | Bin 1044 -> 0 bytes .../__pycache__/connection.cpython-313.pyc | Bin 50817 -> 0 bytes .../__pycache__/connection.cpython-314.pyc | Bin 57131 -> 0 bytes .../__pycache__/messages.cpython-313.pyc | Bin 13540 -> 0 bytes .../__pycache__/messages.cpython-314.pyc | Bin 16195 -> 0 bytes .../__pycache__/server.cpython-313.pyc | Bin 38218 -> 0 bytes .../websockets/asyncio/async_timeout.py | 282 --- wip/SNIP/client/websockets/asyncio/client.py | 820 -------- .../websockets/asyncio/compatibility.py | 30 - .../client/websockets/asyncio/connection.py | 1237 ------------- .../client/websockets/asyncio/messages.py | 314 ---- wip/SNIP/client/websockets/asyncio/router.py | 198 -- wip/SNIP/client/websockets/asyncio/server.py | 981 ---------- wip/SNIP/client/websockets/auth.py | 18 - wip/SNIP/client/websockets/cli.py | 178 -- wip/SNIP/client/websockets/client.py | 389 ---- wip/SNIP/client/websockets/connection.py | 12 - wip/SNIP/client/websockets/datastructures.py | 187 -- wip/SNIP/client/websockets/exceptions.py | 473 ----- .../client/websockets/extensions/__init__.py | 4 - .../__pycache__/__init__.cpython-313.pyc | Bin 272 -> 0 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 274 -> 0 bytes .../__pycache__/base.cpython-313.pyc | Bin 3673 -> 0 bytes .../__pycache__/base.cpython-314.pyc | Bin 4410 -> 0 bytes .../permessage_deflate.cpython-313.pyc | Bin 19675 -> 0 bytes .../permessage_deflate.cpython-314.pyc | Bin 21921 -> 0 bytes wip/SNIP/client/websockets/extensions/base.py | 123 -- .../extensions/permessage_deflate.py | 697 ------- wip/SNIP/client/websockets/frames.py | 430 ----- wip/SNIP/client/websockets/headers.py | 586 ------ wip/SNIP/client/websockets/http.py | 20 - wip/SNIP/client/websockets/http11.py | 427 ----- wip/SNIP/client/websockets/imports.py | 100 - wip/SNIP/client/websockets/legacy/__init__.py | 11 - .../__pycache__/__init__.cpython-313.pyc | Bin 440 -> 0 bytes .../__pycache__/exceptions.cpython-313.pyc | Bin 3291 -> 0 bytes .../__pycache__/framing.cpython-313.pyc | Bin 8067 -> 0 bytes .../__pycache__/handshake.cpython-313.pyc | Bin 6240 -> 0 bytes .../legacy/__pycache__/http.cpython-313.pyc | Bin 7576 -> 0 bytes .../__pycache__/protocol.cpython-313.pyc | Bin 62750 -> 0 bytes .../legacy/__pycache__/server.cpython-313.pyc | Bin 45309 -> 0 bytes wip/SNIP/client/websockets/legacy/auth.py | 190 -- wip/SNIP/client/websockets/legacy/client.py | 705 ------- .../client/websockets/legacy/exceptions.py | 71 - wip/SNIP/client/websockets/legacy/framing.py | 225 --- .../client/websockets/legacy/handshake.py | 158 -- wip/SNIP/client/websockets/legacy/http.py | 201 -- wip/SNIP/client/websockets/legacy/protocol.py | 1641 ----------------- wip/SNIP/client/websockets/legacy/server.py | 1191 ------------ wip/SNIP/client/websockets/protocol.py | 758 -------- wip/SNIP/client/websockets/py.typed | 0 wip/SNIP/client/websockets/server.py | 587 ------ wip/SNIP/client/websockets/speedups.c | 222 --- wip/SNIP/client/websockets/speedups.pyi | 1 - wip/SNIP/client/websockets/streams.py | 151 -- wip/SNIP/client/websockets/sync/__init__.py | 0 wip/SNIP/client/websockets/sync/client.py | 648 ------- wip/SNIP/client/websockets/sync/connection.py | 1072 ----------- wip/SNIP/client/websockets/sync/messages.py | 345 ---- wip/SNIP/client/websockets/sync/router.py | 192 -- wip/SNIP/client/websockets/sync/server.py | 763 -------- wip/SNIP/client/websockets/sync/utils.py | 45 - wip/SNIP/client/websockets/typing.py | 74 - wip/SNIP/client/websockets/uri.py | 225 --- wip/SNIP/client/websockets/utils.py | 51 - wip/SNIP/client/websockets/version.py | 92 - wip/SNIP/docs/packetHeader | 7 - wip/SNIP/server/main.py | 167 -- wip/SNIP/server/registered.json | 6 - wip/SNIP/websockets/__init__.py | 236 --- wip/SNIP/websockets/__main__.py | 5 - .../__pycache__/__init__.cpython-313.pyc | Bin 4447 -> 0 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 5192 -> 0 bytes .../__pycache__/client.cpython-313.pyc | Bin 15961 -> 0 bytes .../datastructures.cpython-313.pyc | Bin 8888 -> 0 bytes .../datastructures.cpython-314.pyc | Bin 11660 -> 0 bytes .../__pycache__/exceptions.cpython-313.pyc | Bin 18201 -> 0 bytes .../__pycache__/exceptions.cpython-314.pyc | Bin 21881 -> 0 bytes .../__pycache__/frames.cpython-313.pyc | Bin 15894 -> 0 bytes .../__pycache__/frames.cpython-314.pyc | Bin 17317 -> 0 bytes .../__pycache__/headers.cpython-313.pyc | Bin 17538 -> 0 bytes .../__pycache__/headers.cpython-314.pyc | Bin 20861 -> 0 bytes .../__pycache__/http11.cpython-313.pyc | Bin 15179 -> 0 bytes .../__pycache__/http11.cpython-314.pyc | Bin 16532 -> 0 bytes .../__pycache__/imports.cpython-313.pyc | Bin 3328 -> 0 bytes .../__pycache__/imports.cpython-314.pyc | Bin 3982 -> 0 bytes .../__pycache__/protocol.cpython-313.pyc | Bin 24664 -> 0 bytes .../__pycache__/protocol.cpython-314.pyc | Bin 27953 -> 0 bytes .../__pycache__/server.cpython-313.pyc | Bin 22302 -> 0 bytes .../__pycache__/server.cpython-314.pyc | Bin 24143 -> 0 bytes .../__pycache__/streams.cpython-313.pyc | Bin 5224 -> 0 bytes .../__pycache__/streams.cpython-314.pyc | Bin 6271 -> 0 bytes .../__pycache__/typing.cpython-313.pyc | Bin 1222 -> 0 bytes .../__pycache__/typing.cpython-314.pyc | Bin 1298 -> 0 bytes .../__pycache__/uri.cpython-313.pyc | Bin 8110 -> 0 bytes .../__pycache__/utils.cpython-313.pyc | Bin 2156 -> 0 bytes .../__pycache__/utils.cpython-314.pyc | Bin 2614 -> 0 bytes .../__pycache__/version.cpython-313.pyc | Bin 2980 -> 0 bytes .../__pycache__/version.cpython-314.pyc | Bin 3296 -> 0 bytes wip/SNIP/websockets/asyncio/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 150 -> 0 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 152 -> 0 bytes .../__pycache__/client.cpython-313.pyc | Bin 33486 -> 0 bytes .../__pycache__/compatibility.cpython-313.pyc | Bin 1027 -> 0 bytes .../__pycache__/compatibility.cpython-314.pyc | Bin 1037 -> 0 bytes .../__pycache__/connection.cpython-313.pyc | Bin 50810 -> 0 bytes .../__pycache__/connection.cpython-314.pyc | Bin 57124 -> 0 bytes .../__pycache__/messages.cpython-313.pyc | Bin 13533 -> 0 bytes .../__pycache__/messages.cpython-314.pyc | Bin 16188 -> 0 bytes .../__pycache__/server.cpython-313.pyc | Bin 38211 -> 0 bytes .../__pycache__/server.cpython-314.pyc | Bin 43202 -> 0 bytes wip/SNIP/websockets/asyncio/async_timeout.py | 282 --- wip/SNIP/websockets/asyncio/client.py | 820 -------- wip/SNIP/websockets/asyncio/compatibility.py | 30 - wip/SNIP/websockets/asyncio/connection.py | 1237 ------------- wip/SNIP/websockets/asyncio/messages.py | 314 ---- wip/SNIP/websockets/asyncio/router.py | 198 -- wip/SNIP/websockets/asyncio/server.py | 981 ---------- wip/SNIP/websockets/auth.py | 18 - wip/SNIP/websockets/cli.py | 178 -- wip/SNIP/websockets/client.py | 389 ---- wip/SNIP/websockets/connection.py | 12 - wip/SNIP/websockets/datastructures.py | 187 -- wip/SNIP/websockets/exceptions.py | 473 ----- wip/SNIP/websockets/extensions/__init__.py | 4 - .../__pycache__/__init__.cpython-313.pyc | Bin 265 -> 0 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 267 -> 0 bytes .../__pycache__/base.cpython-313.pyc | Bin 3666 -> 0 bytes .../__pycache__/base.cpython-314.pyc | Bin 4403 -> 0 bytes .../permessage_deflate.cpython-313.pyc | Bin 19668 -> 0 bytes .../permessage_deflate.cpython-314.pyc | Bin 21914 -> 0 bytes wip/SNIP/websockets/extensions/base.py | 123 -- .../extensions/permessage_deflate.py | 697 ------- wip/SNIP/websockets/frames.py | 430 ----- wip/SNIP/websockets/headers.py | 586 ------ wip/SNIP/websockets/http.py | 20 - wip/SNIP/websockets/http11.py | 427 ----- wip/SNIP/websockets/imports.py | 100 - wip/SNIP/websockets/legacy/__init__.py | 11 - .../__pycache__/__init__.cpython-313.pyc | Bin 433 -> 0 bytes .../__pycache__/exceptions.cpython-313.pyc | Bin 3284 -> 0 bytes .../__pycache__/framing.cpython-313.pyc | Bin 8060 -> 0 bytes .../__pycache__/handshake.cpython-313.pyc | Bin 6233 -> 0 bytes .../legacy/__pycache__/http.cpython-313.pyc | Bin 7569 -> 0 bytes .../__pycache__/protocol.cpython-313.pyc | Bin 62743 -> 0 bytes .../legacy/__pycache__/server.cpython-313.pyc | Bin 45302 -> 0 bytes wip/SNIP/websockets/legacy/auth.py | 190 -- wip/SNIP/websockets/legacy/client.py | 705 ------- wip/SNIP/websockets/legacy/exceptions.py | 71 - wip/SNIP/websockets/legacy/framing.py | 225 --- wip/SNIP/websockets/legacy/handshake.py | 158 -- wip/SNIP/websockets/legacy/http.py | 201 -- wip/SNIP/websockets/legacy/protocol.py | 1641 ----------------- wip/SNIP/websockets/legacy/server.py | 1191 ------------ wip/SNIP/websockets/protocol.py | 758 -------- wip/SNIP/websockets/py.typed | 0 wip/SNIP/websockets/server.py | 587 ------ wip/SNIP/websockets/speedups.c | 222 --- wip/SNIP/websockets/speedups.pyi | 1 - wip/SNIP/websockets/streams.py | 151 -- wip/SNIP/websockets/sync/__init__.py | 0 wip/SNIP/websockets/sync/client.py | 648 ------- wip/SNIP/websockets/sync/connection.py | 1072 ----------- wip/SNIP/websockets/sync/messages.py | 345 ---- wip/SNIP/websockets/sync/router.py | 192 -- wip/SNIP/websockets/sync/server.py | 763 -------- wip/SNIP/websockets/sync/utils.py | 45 - wip/SNIP/websockets/typing.py | 74 - wip/SNIP/websockets/uri.py | 225 --- wip/SNIP/websockets/utils.py | 51 - wip/SNIP/websockets/version.py | 92 - 219 files changed, 35616 deletions(-) delete mode 100644 wip/SNIP/.idea/.gitignore delete mode 100644 wip/SNIP/.idea/SNIP.iml delete mode 100644 wip/SNIP/.idea/inspectionProfiles/profiles_settings.xml delete mode 100644 wip/SNIP/.idea/misc.xml delete mode 100644 wip/SNIP/.idea/modules.xml delete mode 100644 wip/SNIP/client/chatServer.py delete mode 100644 wip/SNIP/client/load.py delete mode 100644 wip/SNIP/client/load2.py delete mode 100644 wip/SNIP/client/loadexp.py delete mode 100644 wip/SNIP/client/messenger.lua delete mode 100644 wip/SNIP/client/messenger.py delete mode 100644 wip/SNIP/client/websockets/__init__.py delete mode 100644 wip/SNIP/client/websockets/__main__.py delete mode 100644 wip/SNIP/client/websockets/__pycache__/__init__.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/__init__.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/client.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/client.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/datastructures.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/datastructures.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/exceptions.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/exceptions.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/frames.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/frames.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/headers.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/headers.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/http11.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/http11.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/imports.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/imports.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/protocol.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/protocol.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/server.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/streams.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/streams.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/typing.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/typing.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/uri.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/uri.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/utils.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/utils.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/version.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/__pycache__/version.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/asyncio/__init__.py delete mode 100644 wip/SNIP/client/websockets/asyncio/__pycache__/__init__.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/asyncio/__pycache__/__init__.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/asyncio/__pycache__/client.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/asyncio/__pycache__/client.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/asyncio/__pycache__/compatibility.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/asyncio/__pycache__/compatibility.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/asyncio/__pycache__/connection.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/asyncio/__pycache__/connection.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/asyncio/__pycache__/messages.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/asyncio/__pycache__/messages.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/asyncio/__pycache__/server.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/asyncio/async_timeout.py delete mode 100644 wip/SNIP/client/websockets/asyncio/client.py delete mode 100644 wip/SNIP/client/websockets/asyncio/compatibility.py delete mode 100644 wip/SNIP/client/websockets/asyncio/connection.py delete mode 100644 wip/SNIP/client/websockets/asyncio/messages.py delete mode 100644 wip/SNIP/client/websockets/asyncio/router.py delete mode 100644 wip/SNIP/client/websockets/asyncio/server.py delete mode 100644 wip/SNIP/client/websockets/auth.py delete mode 100644 wip/SNIP/client/websockets/cli.py delete mode 100644 wip/SNIP/client/websockets/client.py delete mode 100644 wip/SNIP/client/websockets/connection.py delete mode 100644 wip/SNIP/client/websockets/datastructures.py delete mode 100644 wip/SNIP/client/websockets/exceptions.py delete mode 100644 wip/SNIP/client/websockets/extensions/__init__.py delete mode 100644 wip/SNIP/client/websockets/extensions/__pycache__/__init__.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/extensions/__pycache__/__init__.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/extensions/__pycache__/base.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/extensions/__pycache__/base.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/extensions/__pycache__/permessage_deflate.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/extensions/__pycache__/permessage_deflate.cpython-314.pyc delete mode 100644 wip/SNIP/client/websockets/extensions/base.py delete mode 100644 wip/SNIP/client/websockets/extensions/permessage_deflate.py delete mode 100644 wip/SNIP/client/websockets/frames.py delete mode 100644 wip/SNIP/client/websockets/headers.py delete mode 100644 wip/SNIP/client/websockets/http.py delete mode 100644 wip/SNIP/client/websockets/http11.py delete mode 100644 wip/SNIP/client/websockets/imports.py delete mode 100644 wip/SNIP/client/websockets/legacy/__init__.py delete mode 100644 wip/SNIP/client/websockets/legacy/__pycache__/__init__.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/legacy/__pycache__/exceptions.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/legacy/__pycache__/framing.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/legacy/__pycache__/handshake.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/legacy/__pycache__/http.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/legacy/__pycache__/protocol.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/legacy/__pycache__/server.cpython-313.pyc delete mode 100644 wip/SNIP/client/websockets/legacy/auth.py delete mode 100644 wip/SNIP/client/websockets/legacy/client.py delete mode 100644 wip/SNIP/client/websockets/legacy/exceptions.py delete mode 100644 wip/SNIP/client/websockets/legacy/framing.py delete mode 100644 wip/SNIP/client/websockets/legacy/handshake.py delete mode 100644 wip/SNIP/client/websockets/legacy/http.py delete mode 100644 wip/SNIP/client/websockets/legacy/protocol.py delete mode 100644 wip/SNIP/client/websockets/legacy/server.py delete mode 100644 wip/SNIP/client/websockets/protocol.py delete mode 100644 wip/SNIP/client/websockets/py.typed delete mode 100644 wip/SNIP/client/websockets/server.py delete mode 100644 wip/SNIP/client/websockets/speedups.c delete mode 100644 wip/SNIP/client/websockets/speedups.pyi delete mode 100644 wip/SNIP/client/websockets/streams.py delete mode 100644 wip/SNIP/client/websockets/sync/__init__.py delete mode 100644 wip/SNIP/client/websockets/sync/client.py delete mode 100644 wip/SNIP/client/websockets/sync/connection.py delete mode 100644 wip/SNIP/client/websockets/sync/messages.py delete mode 100644 wip/SNIP/client/websockets/sync/router.py delete mode 100644 wip/SNIP/client/websockets/sync/server.py delete mode 100644 wip/SNIP/client/websockets/sync/utils.py delete mode 100644 wip/SNIP/client/websockets/typing.py delete mode 100644 wip/SNIP/client/websockets/uri.py delete mode 100644 wip/SNIP/client/websockets/utils.py delete mode 100644 wip/SNIP/client/websockets/version.py delete mode 100644 wip/SNIP/docs/packetHeader delete mode 100644 wip/SNIP/server/main.py delete mode 100644 wip/SNIP/server/registered.json delete mode 100644 wip/SNIP/websockets/__init__.py delete mode 100644 wip/SNIP/websockets/__main__.py delete mode 100644 wip/SNIP/websockets/__pycache__/__init__.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/__init__.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/client.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/datastructures.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/datastructures.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/exceptions.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/exceptions.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/frames.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/frames.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/headers.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/headers.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/http11.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/http11.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/imports.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/imports.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/protocol.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/protocol.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/server.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/server.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/streams.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/streams.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/typing.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/typing.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/uri.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/utils.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/utils.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/version.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/__pycache__/version.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/asyncio/__init__.py delete mode 100644 wip/SNIP/websockets/asyncio/__pycache__/__init__.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/asyncio/__pycache__/__init__.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/asyncio/__pycache__/client.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/asyncio/__pycache__/compatibility.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/asyncio/__pycache__/compatibility.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/asyncio/__pycache__/connection.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/asyncio/__pycache__/connection.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/asyncio/__pycache__/messages.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/asyncio/__pycache__/messages.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/asyncio/__pycache__/server.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/asyncio/__pycache__/server.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/asyncio/async_timeout.py delete mode 100644 wip/SNIP/websockets/asyncio/client.py delete mode 100644 wip/SNIP/websockets/asyncio/compatibility.py delete mode 100644 wip/SNIP/websockets/asyncio/connection.py delete mode 100644 wip/SNIP/websockets/asyncio/messages.py delete mode 100644 wip/SNIP/websockets/asyncio/router.py delete mode 100644 wip/SNIP/websockets/asyncio/server.py delete mode 100644 wip/SNIP/websockets/auth.py delete mode 100644 wip/SNIP/websockets/cli.py delete mode 100644 wip/SNIP/websockets/client.py delete mode 100644 wip/SNIP/websockets/connection.py delete mode 100644 wip/SNIP/websockets/datastructures.py delete mode 100644 wip/SNIP/websockets/exceptions.py delete mode 100644 wip/SNIP/websockets/extensions/__init__.py delete mode 100644 wip/SNIP/websockets/extensions/__pycache__/__init__.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/extensions/__pycache__/__init__.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/extensions/__pycache__/base.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/extensions/__pycache__/base.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/extensions/__pycache__/permessage_deflate.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/extensions/__pycache__/permessage_deflate.cpython-314.pyc delete mode 100644 wip/SNIP/websockets/extensions/base.py delete mode 100644 wip/SNIP/websockets/extensions/permessage_deflate.py delete mode 100644 wip/SNIP/websockets/frames.py delete mode 100644 wip/SNIP/websockets/headers.py delete mode 100644 wip/SNIP/websockets/http.py delete mode 100644 wip/SNIP/websockets/http11.py delete mode 100644 wip/SNIP/websockets/imports.py delete mode 100644 wip/SNIP/websockets/legacy/__init__.py delete mode 100644 wip/SNIP/websockets/legacy/__pycache__/__init__.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/legacy/__pycache__/exceptions.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/legacy/__pycache__/framing.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/legacy/__pycache__/handshake.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/legacy/__pycache__/http.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/legacy/__pycache__/protocol.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/legacy/__pycache__/server.cpython-313.pyc delete mode 100644 wip/SNIP/websockets/legacy/auth.py delete mode 100644 wip/SNIP/websockets/legacy/client.py delete mode 100644 wip/SNIP/websockets/legacy/exceptions.py delete mode 100644 wip/SNIP/websockets/legacy/framing.py delete mode 100644 wip/SNIP/websockets/legacy/handshake.py delete mode 100644 wip/SNIP/websockets/legacy/http.py delete mode 100644 wip/SNIP/websockets/legacy/protocol.py delete mode 100644 wip/SNIP/websockets/legacy/server.py delete mode 100644 wip/SNIP/websockets/protocol.py delete mode 100644 wip/SNIP/websockets/py.typed delete mode 100644 wip/SNIP/websockets/server.py delete mode 100644 wip/SNIP/websockets/speedups.c delete mode 100644 wip/SNIP/websockets/speedups.pyi delete mode 100644 wip/SNIP/websockets/streams.py delete mode 100644 wip/SNIP/websockets/sync/__init__.py delete mode 100644 wip/SNIP/websockets/sync/client.py delete mode 100644 wip/SNIP/websockets/sync/connection.py delete mode 100644 wip/SNIP/websockets/sync/messages.py delete mode 100644 wip/SNIP/websockets/sync/router.py delete mode 100644 wip/SNIP/websockets/sync/server.py delete mode 100644 wip/SNIP/websockets/sync/utils.py delete mode 100644 wip/SNIP/websockets/typing.py delete mode 100644 wip/SNIP/websockets/uri.py delete mode 100644 wip/SNIP/websockets/utils.py delete mode 100644 wip/SNIP/websockets/version.py diff --git a/wip/SNIP/.idea/.gitignore b/wip/SNIP/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/wip/SNIP/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/wip/SNIP/.idea/SNIP.iml b/wip/SNIP/.idea/SNIP.iml deleted file mode 100644 index d8b3f6c..0000000 --- a/wip/SNIP/.idea/SNIP.iml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/wip/SNIP/.idea/inspectionProfiles/profiles_settings.xml b/wip/SNIP/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/wip/SNIP/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/wip/SNIP/.idea/misc.xml b/wip/SNIP/.idea/misc.xml deleted file mode 100644 index 1d3ce46..0000000 --- a/wip/SNIP/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/wip/SNIP/.idea/modules.xml b/wip/SNIP/.idea/modules.xml deleted file mode 100644 index c0f01cd..0000000 --- a/wip/SNIP/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/wip/SNIP/client/chatServer.py b/wip/SNIP/client/chatServer.py deleted file mode 100644 index aa68b9e..0000000 --- a/wip/SNIP/client/chatServer.py +++ /dev/null @@ -1,122 +0,0 @@ -import asyncio -import struct -import ipaddress -import websockets -import time - -SERVER = "wss://ip.astronand.dev" -SERVER_NAME = "alpha" - -OP_IP_REQUEST = 0x01 -OP_IP_ASSIGN = 0x02 -OP_DATA = 0x10 -ASSIGN_OK = 0x00 - -DATA_HEADER = "!4s4sI" -HEADER_SIZE = struct.calcsize(DATA_HEADER) - -clients = {} # ip -> username -rooms = {} # room -> set(ip) -federated_servers = set() - -def encode(src, dst, payload: bytes): - return ( - bytes([OP_DATA]) + - struct.pack(DATA_HEADER, src, dst, len(payload)) + - payload - ) - -def chat(tp, room, user, data=""): - return f"CHAT|{tp}|{room}|{user}|{data}".encode() - -async def main(): - async with websockets.connect(SERVER, max_size=2**20) as ws: - # DHCP - await ws.send(b"\x01\x00") - reply = await ws.recv() - my_ip = reply[2:6] - - print("🖥️ Chat server IP:", ipaddress.IPv4Address(my_ip)) - - async def announce(): - while True: - await ws.send( - encode(my_ip, my_ip, - chat("SERVER_HELLO", "*", "server", SERVER_NAME) - ) - ) - await asyncio.sleep(5) - - async def handler(): - async for msg in ws: - if not isinstance(msg, bytes) or msg[0] != OP_DATA: - continue - - src, _, length = struct.unpack( - DATA_HEADER, msg[1:1+HEADER_SIZE] - ) - payload = msg[1+HEADER_SIZE:] - if len(payload) != length: - continue - - try: - text = payload.decode() - except UnicodeDecodeError: - continue - - if not text.startswith("CHAT|"): - continue - - _, tp, room, user, data = text.split("|", 4) - - # ---- discovery / federation ---- - if tp == "SERVER_HELLO" and src != my_ip: - federated_servers.add(src) - print(f"🔗 Federated with {data} @ {ipaddress.IPv4Address(src)}") - - elif tp == "HELLO": - clients[src] = user - print(f"👤 {user} connected") - - elif tp == "ROOM_LIST": - await ws.send(encode( - my_ip, src, - chat("ROOM_LIST", "*", "server", ",".join(rooms.keys())) - )) - - # ---- room management ---- - elif tp == "JOIN": - rooms.setdefault(room, set()).add(src) - for ip in rooms[room]: - await ws.send(encode( - my_ip, ip, - chat("INFO", room, "server", f"{user} joined") - )) - - elif tp == "LEAVE": - rooms.get(room, set()).discard(src) - - # ---- messaging ---- - elif tp == "MSG": - for ip in rooms.get(room, []): - await ws.send(encode( - my_ip, ip, - chat("MSG", room, user, data) - )) - - for srv in federated_servers: - await ws.send(encode( - my_ip, srv, - chat("FED_MSG", room, user, data) - )) - - elif tp == "FED_MSG": - for ip in rooms.get(room, []): - await ws.send(encode( - my_ip, ip, - chat("MSG", room, user, data) - )) - - await asyncio.gather(announce(), handler()) - -asyncio.run(main()) diff --git a/wip/SNIP/client/load.py b/wip/SNIP/client/load.py deleted file mode 100644 index f27f00d..0000000 --- a/wip/SNIP/client/load.py +++ /dev/null @@ -1,116 +0,0 @@ -import asyncio -import struct -import ipaddress -import websockets -import random -import time - -SERVER = "ws://localhost:8765" -CLIENT_COUNT = 5000 -SEND_INTERVAL = (0.2, 0.5) # safer backpressure - -IP_PACKET_HEADER = "!4s4sI" -HEADER_SIZE = struct.calcsize(IP_PACKET_HEADER) - -# Opcodes -OP_IP_REQUEST = 0x01 -OP_IP_ASSIGN = 0x02 -OP_DATA = 0x10 -ASSIGN_OK = 0x00 - -STATIC_IP_BASE = ipaddress.IPv4Address("10.0.0.10") -assigned_ips: set[bytes] = set() -assigned_ips_lock = asyncio.Lock() - - -def random_payload(my_ip: bytes) -> bytes: - return f"hello from {ipaddress.IPv4Address(my_ip)} @ {time.time():.2f}".encode() - - -async def virtual_node(node_id: int): - my_ip = None - try: - async with websockets.connect(SERVER, max_size=2**20) as ws: - # ------------------------- - # CONTROL PLANE: IP REQUEST - # ------------------------- - role = random.random() - if role < 1: - await ws.send(bytes([OP_IP_REQUEST, 0x00])) - elif role < 0.90: - static_ip = (STATIC_IP_BASE + node_id).packed - await ws.send(bytes([OP_IP_REQUEST, 0x01]) + static_ip) - elif role < 0.95: - bad_ip = ipaddress.IPv4Address(random.getrandbits(32)).packed - await ws.send(bytes([OP_IP_REQUEST, 0x01]) + bad_ip) - else: - fake_packet = bytes([OP_DATA]) + b"\x00" * (HEADER_SIZE + 10) - await ws.send(fake_packet) - return - - reply = await ws.recv() - if not isinstance(reply, bytes) or len(reply) != 6: - return - if reply[0] != OP_IP_ASSIGN or reply[1] != ASSIGN_OK: - return - - my_ip = reply[2:6] - - async with assigned_ips_lock: - assigned_ips.add(my_ip) - - # ------------------------- - # RECEIVER - # ------------------------- - async def receiver(): - try: - async for msg in ws: - if not isinstance(msg, bytes): - continue - if msg[0] != OP_DATA: - continue - if len(msg) < 1 + HEADER_SIZE: - continue - src, dst, length = struct.unpack( - IP_PACKET_HEADER, msg[1:1 + HEADER_SIZE] - ) - payload = msg[1 + HEADER_SIZE:] - if len(payload) != length: - continue - except websockets.ConnectionClosed: - pass - - # ------------------------- - # SENDER - # ------------------------- - async def sender(): - while True: - await asyncio.sleep(random.uniform(*SEND_INTERVAL)) - async with assigned_ips_lock: - if not assigned_ips: - continue - dst = random.choice(list(assigned_ips)) - if dst == my_ip: - continue - payload = random_payload(my_ip) - packet = ( - bytes([OP_DATA]) - + struct.pack(IP_PACKET_HEADER, my_ip, dst, len(payload)) - + payload - ) - await ws.send(packet) - - await asyncio.gather(receiver(), sender()) - - finally: - async with assigned_ips_lock: - assigned_ips.discard(my_ip) - - -async def main(): - print(f"🚀 Starting {CLIENT_COUNT} virtual nodes") - tasks = [asyncio.create_task(virtual_node(i)) for i in range(CLIENT_COUNT)] - await asyncio.gather(*tasks) - - -asyncio.run(main()) diff --git a/wip/SNIP/client/load2.py b/wip/SNIP/client/load2.py deleted file mode 100644 index ddb79da..0000000 --- a/wip/SNIP/client/load2.py +++ /dev/null @@ -1,116 +0,0 @@ -import asyncio -import struct -import ipaddress -import websockets -import random -import time - -SERVER = "ws://localhost:8765" -CLIENT_COUNT = 5000 -SEND_INTERVAL = (0.2, 0.5) # safer backpressure - -IP_PACKET_HEADER = "!4s4sI" -HEADER_SIZE = struct.calcsize(IP_PACKET_HEADER) - -# Opcodes -OP_IP_REQUEST = 0x01 -OP_IP_ASSIGN = 0x02 -OP_DATA = 0x10 -ASSIGN_OK = 0x00 - -STATIC_IP_BASE = ipaddress.IPv4Address("10.0.0.10") -assigned_ips: set[bytes] = set() -assigned_ips_lock = asyncio.Lock() - - -def random_payload(my_ip: bytes) -> bytes: - return f"hello from {ipaddress.IPv4Address(my_ip)} @ {time.time():.2f}".encode() - - -async def virtual_node(node_id: int): - my_ip = None - try: - async with websockets.connect(SERVER, max_size=2**20) as ws: - # ------------------------- - # CONTROL PLANE: IP REQUEST - # ------------------------- - role = random.random() - if role < 0.70: - await ws.send(bytes([OP_IP_REQUEST, 0x00])) - elif role < 0.90: - static_ip = (STATIC_IP_BASE + node_id).packed - await ws.send(bytes([OP_IP_REQUEST, 0x01]) + static_ip) - elif role < 0.95: - bad_ip = ipaddress.IPv4Address(random.getrandbits(32)).packed - await ws.send(bytes([OP_IP_REQUEST, 0x01]) + bad_ip) - else: - fake_packet = bytes([OP_DATA]) + b"\x00" * (HEADER_SIZE + 10) - await ws.send(fake_packet) - return - - reply = await ws.recv() - if not isinstance(reply, bytes) or len(reply) != 6: - return - if reply[0] != OP_IP_ASSIGN or reply[1] != ASSIGN_OK: - return - - my_ip = reply[2:6] - - async with assigned_ips_lock: - assigned_ips.add(my_ip) - - # ------------------------- - # RECEIVER - # ------------------------- - async def receiver(): - try: - async for msg in ws: - if not isinstance(msg, bytes): - continue - if msg[0] != OP_DATA: - continue - if len(msg) < 1 + HEADER_SIZE: - continue - src, dst, length = struct.unpack( - IP_PACKET_HEADER, msg[1:1 + HEADER_SIZE] - ) - payload = msg[1 + HEADER_SIZE:] - if len(payload) != length: - continue - except websockets.ConnectionClosed: - pass - - # ------------------------- - # SENDER - # ------------------------- - async def sender(): - while True: - await asyncio.sleep(random.uniform(*SEND_INTERVAL)) - async with assigned_ips_lock: - if not assigned_ips: - continue - dst = random.choice(list(assigned_ips)) - if dst == my_ip: - continue - payload = random_payload(my_ip) - packet = ( - bytes([OP_DATA]) - + struct.pack(IP_PACKET_HEADER, my_ip, dst, len(payload)) - + payload - ) - await ws.send(packet) - - await asyncio.gather(receiver(), sender()) - - finally: - async with assigned_ips_lock: - assigned_ips.discard(my_ip) - - -async def main(): - print(f"🚀 Starting {CLIENT_COUNT} virtual nodes") - tasks = [asyncio.create_task(virtual_node(i)) for i in range(CLIENT_COUNT)] - await asyncio.gather(*tasks) - - -asyncio.run(main()) diff --git a/wip/SNIP/client/loadexp.py b/wip/SNIP/client/loadexp.py deleted file mode 100644 index 10f5c92..0000000 --- a/wip/SNIP/client/loadexp.py +++ /dev/null @@ -1,131 +0,0 @@ -import asyncio -import struct -import ipaddress -import websockets -import random -import time - -SERVER = "wss://ip.astronand.dev" -SEND_INTERVAL = (0.2, 0.5) # safer backpressure - -IP_PACKET_HEADER = "!4s4sI" -HEADER_SIZE = struct.calcsize(IP_PACKET_HEADER) - -# Opcodes -OP_IP_REQUEST = 0x01 -OP_IP_ASSIGN = 0x02 -OP_DATA = 0x10 -ASSIGN_OK = 0x00 - -STATIC_IP_BASE = ipaddress.IPv4Address("10.0.0.10") -assigned_ips: set[bytes] = set() -assigned_ips_lock = asyncio.Lock() - - -def random_payload(my_ip: bytes) -> bytes: - return f"hello from {ipaddress.IPv4Address(my_ip)} @ {time.time():.2f}".encode() - - -async def virtual_node(node_id: int): - my_ip = None - try: - async with websockets.connect(SERVER, max_size=2**20) as ws: - # ------------------------- - # CONTROL PLANE: IP REQUEST - # ------------------------- - role = random.random() - if role < 1: - await ws.send(bytes([OP_IP_REQUEST, 0x00])) - elif role < 0.90: - static_ip = (STATIC_IP_BASE + node_id).packed - await ws.send(bytes([OP_IP_REQUEST, 0x01]) + static_ip) - elif role < 0.95: - bad_ip = ipaddress.IPv4Address(random.getrandbits(32)).packed - await ws.send(bytes([OP_IP_REQUEST, 0x01]) + bad_ip) - else: - fake_packet = bytes([OP_DATA]) + b"\x00" * (HEADER_SIZE + 10) - await ws.send(fake_packet) - return - - reply = await ws.recv() - if not isinstance(reply, bytes) or len(reply) != 6: - return - if reply[0] != OP_IP_ASSIGN or reply[1] != ASSIGN_OK: - return - - my_ip = reply[2:6] - - async with assigned_ips_lock: - assigned_ips.add(my_ip) - - # ------------------------- - # RECEIVER - # ------------------------- - async def receiver(): - try: - async for msg in ws: - if not isinstance(msg, bytes): - continue - if msg[0] != OP_DATA: - continue - if len(msg) < 1 + HEADER_SIZE: - continue - src, dst, length = struct.unpack( - IP_PACKET_HEADER, msg[1:1 + HEADER_SIZE] - ) - payload = msg[1 + HEADER_SIZE:] - if len(payload) != length: - continue - except websockets.ConnectionClosed: - raise RuntimeError("WebSocket closed") # bubble up to stop node - - # ------------------------- - # SENDER - # ------------------------- - async def sender(): - while True: - await asyncio.sleep(random.uniform(*SEND_INTERVAL)) - async with assigned_ips_lock: - if not assigned_ips: - continue - dst = random.choice(list(assigned_ips)) - if dst == my_ip: - continue - payload = random_payload(my_ip) - packet = ( - bytes([OP_DATA]) - + struct.pack(IP_PACKET_HEADER, my_ip, dst, len(payload)) - + payload - ) - await ws.send(packet) - - await asyncio.gather(receiver(), sender()) - - finally: - async with assigned_ips_lock: - assigned_ips.discard(my_ip) - - -async def main(): - print("🚀 Gradually increasing virtual nodes...") - node_id = 0 - tasks = [] - - while True: - task = asyncio.create_task(virtual_node(node_id)) - - # Wait a bit before spawning the next node - await asyncio.sleep(0.01) - if task.done(): - try: - task.result() - except Exception as e: - print(f"💥 First crash at node {node_id}: {e}") - print(f"✅ Successfully running nodes before crash: {len(tasks)}") - break - - tasks.append(task) - node_id += 1 - - -asyncio.run(main()) diff --git a/wip/SNIP/client/messenger.lua b/wip/SNIP/client/messenger.lua deleted file mode 100644 index eb5df61..0000000 --- a/wip/SNIP/client/messenger.lua +++ /dev/null @@ -1,65 +0,0 @@ -local ws = http.websocket("wss://ip.astronand.dev") - -local OP_DATA = 0x10 - --- Example IPs (replace with assigned ones) -local my_ip = {10,0,0,2} -local peers = {} - -local function pack_u32(n) - return string.char( - bit32.rshift(n,24) & 0xFF, - bit32.rshift(n,16) & 0xFF, - bit32.rshift(n,8) & 0xFF, - n & 0xFF - ) -end - -local function pack_ip(ip) - return string.char(ip[1], ip[2], ip[3], ip[4]) -end - -local function send_chat(dst_ip, username, msg) - local payload = "CHAT|" .. username .. "|" .. msg - local packet = - string.char(OP_DATA) .. - pack_ip(my_ip) .. - pack_ip(dst_ip) .. - pack_u32(#payload) .. - payload - - ws.send(packet) -end - --- Receiver -local function receiver() - while true do - local msg = ws.receive() - if msg then - local op = string.byte(msg, 1) - if op == OP_DATA then - local payload = msg:sub(14) - if payload:sub(1,5) == "CHAT|" then - local _, _, user, text = - payload:find("CHAT|(.-)|(.*)") - print(user .. ": " .. text) - end - end - end - end -end - --- Sender -local function sender() - write("Username: ") - local username = read() - - while true do - local msg = read() - for _, ip in pairs(peers) do - send_chat(ip, username, msg) - end - end -end - -parallel.waitForAny(receiver, sender) diff --git a/wip/SNIP/client/messenger.py b/wip/SNIP/client/messenger.py deleted file mode 100644 index 59b11df..0000000 --- a/wip/SNIP/client/messenger.py +++ /dev/null @@ -1,122 +0,0 @@ -import asyncio -import struct -import ipaddress -import websockets -import sys - -SERVER = "wss://ip.astronand.dev" - -OP_IP_REQUEST = 0x01 -OP_IP_ASSIGN = 0x02 -OP_DATA = 0x10 -ASSIGN_OK = 0x00 - -DATA_HEADER = "!4s4sI" -HEADER_SIZE = struct.calcsize(DATA_HEADER) - -servers = {} # ip -> name -server_ip = "0.0.0.1" -room = "general" - -def encode(src, dst, payload: bytes): - return ( - bytes([OP_DATA]) + - struct.pack(DATA_HEADER, src, dst, len(payload)) + - payload - ) - -def chat(tp, room, user, data=""): - return f"CHAT|{tp}|{room}|{user}|{data}".encode() - -async def main(): - global server_ip, room - - username = input("Username: ").strip() - - async with websockets.connect(SERVER, max_size=2**20) as ws: - # DHCP - await ws.send(b"\x01\x00") - reply = await ws.recv() - my_ip = reply[2:6] - - print("Assigned IP:", ipaddress.IPv4Address(my_ip)) - - async def receiver(): - async for msg in ws: - if not isinstance(msg, bytes) or msg[0] != OP_DATA: - continue - - src, _, length = struct.unpack( - DATA_HEADER, msg[1:1+HEADER_SIZE] - ) - payload = msg[1+HEADER_SIZE:] - if len(payload) != length: - continue - - text = payload.decode(errors="ignore") - if not text.startswith("CHAT|"): - continue - - _, tp, rm, user, data = text.split("|", 4) - - if tp == "SERVER_HELLO": - servers[src] = data - print(f"🖥️ Server: {data} @ {ipaddress.IPv4Address(src)}") - - elif tp == "ROOM_LIST": - print("📂 Rooms:", data) - - elif tp in ("MSG", "INFO"): - print(f"[#{rm}] {user}: {data}") - - async def sender(): - global server_ip - loop = asyncio.get_event_loop() - - while server_ip is None: - await asyncio.sleep(1) - if servers: - server_ip = next(iter(servers)) - - await ws.send(encode( - my_ip, server_ip, - chat("HELLO", "*", username) - )) - await ws.send(encode( - my_ip, server_ip, - chat("JOIN", room, username) - )) - - while True: - line = await loop.run_in_executor(None, sys.stdin.readline) - line = line.strip() - if not line: - continue - - if line == "/servers": - for ip, name in servers.items(): - print(ipaddress.IPv4Address(ip), name) - - elif line == "/rooms": - await ws.send(encode( - my_ip, server_ip, - chat("ROOM_LIST", "*", username) - )) - - elif line.startswith("/join "): - room = line.split(" ", 1)[1] - await ws.send(encode( - my_ip, server_ip, - chat("JOIN", room, username) - )) - - else: - await ws.send(encode( - my_ip, server_ip, - chat("MSG", room, username, line) - )) - - print("Commands: /servers /rooms /join ") - await asyncio.gather(receiver(), sender()) - -asyncio.run(main()) diff --git a/wip/SNIP/client/websockets/__init__.py b/wip/SNIP/client/websockets/__init__.py deleted file mode 100644 index f90aff5..0000000 --- a/wip/SNIP/client/websockets/__init__.py +++ /dev/null @@ -1,236 +0,0 @@ -from __future__ import annotations - -# Importing the typing module would conflict with websockets.typing. -from typing import TYPE_CHECKING - -from .imports import lazy_import -from .version import version as __version__ # noqa: F401 - - -__all__ = [ - # .asyncio.client - "connect", - "unix_connect", - "ClientConnection", - # .asyncio.router - "route", - "unix_route", - "Router", - # .asyncio.server - "basic_auth", - "broadcast", - "serve", - "unix_serve", - "ServerConnection", - "Server", - # .client - "ClientProtocol", - # .datastructures - "Headers", - "HeadersLike", - "MultipleValuesError", - # .exceptions - "ConcurrencyError", - "ConnectionClosed", - "ConnectionClosedError", - "ConnectionClosedOK", - "DuplicateParameter", - "InvalidHandshake", - "InvalidHeader", - "InvalidHeaderFormat", - "InvalidHeaderValue", - "InvalidMessage", - "InvalidOrigin", - "InvalidParameterName", - "InvalidParameterValue", - "InvalidProxy", - "InvalidProxyMessage", - "InvalidProxyStatus", - "InvalidState", - "InvalidStatus", - "InvalidUpgrade", - "InvalidURI", - "NegotiationError", - "PayloadTooBig", - "ProtocolError", - "ProxyError", - "SecurityError", - "WebSocketException", - # .frames - "Close", - "CloseCode", - "Frame", - "Opcode", - # .http11 - "Request", - "Response", - # .protocol - "Protocol", - "Side", - "State", - # .server - "ServerProtocol", - # .typing - "Data", - "ExtensionName", - "ExtensionParameter", - "LoggerLike", - "StatusLike", - "Origin", - "Subprotocol", -] - -# When type checking, import non-deprecated aliases eagerly. Else, import on demand. -if TYPE_CHECKING: - from .asyncio.client import ClientConnection, connect, unix_connect - from .asyncio.router import Router, route, unix_route - from .asyncio.server import ( - Server, - ServerConnection, - basic_auth, - broadcast, - serve, - unix_serve, - ) - from .client import ClientProtocol - from .datastructures import Headers, HeadersLike, MultipleValuesError - from .exceptions import ( - ConcurrencyError, - ConnectionClosed, - ConnectionClosedError, - ConnectionClosedOK, - DuplicateParameter, - InvalidHandshake, - InvalidHeader, - InvalidHeaderFormat, - InvalidHeaderValue, - InvalidMessage, - InvalidOrigin, - InvalidParameterName, - InvalidParameterValue, - InvalidProxy, - InvalidProxyMessage, - InvalidProxyStatus, - InvalidState, - InvalidStatus, - InvalidUpgrade, - InvalidURI, - NegotiationError, - PayloadTooBig, - ProtocolError, - ProxyError, - SecurityError, - WebSocketException, - ) - from .frames import Close, CloseCode, Frame, Opcode - from .http11 import Request, Response - from .protocol import Protocol, Side, State - from .server import ServerProtocol - from .typing import ( - Data, - ExtensionName, - ExtensionParameter, - LoggerLike, - Origin, - StatusLike, - Subprotocol, - ) -else: - lazy_import( - globals(), - aliases={ - # .asyncio.client - "connect": ".asyncio.client", - "unix_connect": ".asyncio.client", - "ClientConnection": ".asyncio.client", - # .asyncio.router - "route": ".asyncio.router", - "unix_route": ".asyncio.router", - "Router": ".asyncio.router", - # .asyncio.server - "basic_auth": ".asyncio.server", - "broadcast": ".asyncio.server", - "serve": ".asyncio.server", - "unix_serve": ".asyncio.server", - "ServerConnection": ".asyncio.server", - "Server": ".asyncio.server", - # .client - "ClientProtocol": ".client", - # .datastructures - "Headers": ".datastructures", - "HeadersLike": ".datastructures", - "MultipleValuesError": ".datastructures", - # .exceptions - "ConcurrencyError": ".exceptions", - "ConnectionClosed": ".exceptions", - "ConnectionClosedError": ".exceptions", - "ConnectionClosedOK": ".exceptions", - "DuplicateParameter": ".exceptions", - "InvalidHandshake": ".exceptions", - "InvalidHeader": ".exceptions", - "InvalidHeaderFormat": ".exceptions", - "InvalidHeaderValue": ".exceptions", - "InvalidMessage": ".exceptions", - "InvalidOrigin": ".exceptions", - "InvalidParameterName": ".exceptions", - "InvalidParameterValue": ".exceptions", - "InvalidProxy": ".exceptions", - "InvalidProxyMessage": ".exceptions", - "InvalidProxyStatus": ".exceptions", - "InvalidState": ".exceptions", - "InvalidStatus": ".exceptions", - "InvalidUpgrade": ".exceptions", - "InvalidURI": ".exceptions", - "NegotiationError": ".exceptions", - "PayloadTooBig": ".exceptions", - "ProtocolError": ".exceptions", - "ProxyError": ".exceptions", - "SecurityError": ".exceptions", - "WebSocketException": ".exceptions", - # .frames - "Close": ".frames", - "CloseCode": ".frames", - "Frame": ".frames", - "Opcode": ".frames", - # .http11 - "Request": ".http11", - "Response": ".http11", - # .protocol - "Protocol": ".protocol", - "Side": ".protocol", - "State": ".protocol", - # .server - "ServerProtocol": ".server", - # .typing - "Data": ".typing", - "ExtensionName": ".typing", - "ExtensionParameter": ".typing", - "LoggerLike": ".typing", - "Origin": ".typing", - "StatusLike": ".typing", - "Subprotocol": ".typing", - }, - deprecated_aliases={ - # deprecated in 9.0 - 2021-09-01 - "framing": ".legacy", - "handshake": ".legacy", - "parse_uri": ".uri", - "WebSocketURI": ".uri", - # deprecated in 14.0 - 2024-11-09 - # .legacy.auth - "BasicAuthWebSocketServerProtocol": ".legacy.auth", - "basic_auth_protocol_factory": ".legacy.auth", - # .legacy.client - "WebSocketClientProtocol": ".legacy.client", - # .legacy.exceptions - "AbortHandshake": ".legacy.exceptions", - "InvalidStatusCode": ".legacy.exceptions", - "RedirectHandshake": ".legacy.exceptions", - "WebSocketProtocolError": ".legacy.exceptions", - # .legacy.protocol - "WebSocketCommonProtocol": ".legacy.protocol", - # .legacy.server - "WebSocketServer": ".legacy.server", - "WebSocketServerProtocol": ".legacy.server", - }, - ) diff --git a/wip/SNIP/client/websockets/__main__.py b/wip/SNIP/client/websockets/__main__.py deleted file mode 100644 index 2f05ddc..0000000 --- a/wip/SNIP/client/websockets/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .cli import main - - -if __name__ == "__main__": - main() diff --git a/wip/SNIP/client/websockets/__pycache__/__init__.cpython-313.pyc b/wip/SNIP/client/websockets/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index fb65dadadbaa1087c7ab9be808b66958a8cfa118..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4454 zcmey&%ge>Uz`#(r@pt2dh$TM@eggFZtrTOeZ*M+~zD#2|*m#S9?T?B1MSTt!@7 z+(q18JViWUT^!!LUVKG-Ui?M;UIIk|U@=Z_K`)^qAur(~VK0#)5iijqQ7^F~F)#5V zaj-m>w}h8uktCSS?JeacT_o)#QzYXhTOV zQYljLQY}*TQY%vPQZG{X(kRmK(k#;S(kjyO(k{~W(kasM(k;^U(ks&Q(l65YGAJ_e zGAuImGAc6iGA=UqGAT0gGA%OoGAlCkGA}asvM90u+sEr|>19=9)0F#nnQYu(Fo-tSkOv-{uIWQ>?CKbS>Vz5#?W3Y0tN<3q*DwtFQlj^}5@r(ux z!I~gCSPM*QgGn7QsS76cz@$EyGys!^VA2Ro8iPp_Flh=V&A_BNn6vJSAZNQ{0n6v|v_F&QhOge%|r(oxJ#$XpP=?W&@z@&SyM?7P&Cz$jK_Ks%^_5qW= zAkvjF*e}>01qSGGxH1L@1_uTQ1qTI#KuB;X6ov(d2Zsko=i+omSpDV6>Boy;th!maE*8NaCP?f^mBj7$iTn=Qk0WeRT-a|TaaH=qRDuR zy)3n;I5R&_)A|;Ba(-T3YI4afp3=O`ig*}Lz&R%~HLt`OBAJ<=cZ;#GpQ&)F(o;%xa1aVacWT+Ob3K55DXG3LNb;OB65ol zVqrj0eo20Ee$Fj+kJQAJ)S}{B+)##3W_Ic=Vc*i6lFWjf)Ud>y($r$tqN4nwTLRAc zdC8?kMX7nol?V}#r!w>NoOAMvQ&Vn7;*pq_Qk;>PoqCHGCIWWmEnyU{TYgb)V#zHb6hW|)Z}CCJd{c{y z6Vnmq_!niSXXf1!fl9-z_Djr7y(Nk+3NecZCR~(XQ3-cK0Eh=O9_9qFU~oxdNonyd zm^Kg>;Q$b?v>5Kb(1P@$#FW%qTu?hggFJ5u_@$=jmt=y|8YKL90}?B9@)J`+^7EZC z({J&@q7Nd=1-2E!<_%6wE-lI|fyR(fcxqB`esXqdiEBl2Y5^$q-eLvE%q>nZ<(!|A zdW+Q!6alx`{0ovnT=t;U!qU{@l3N@>sl^5PdBv%>IAAv3VhPR!=>T~n^%fr_RUsr@ z5=#ukXfb0U^W{xez=26lM0||RFk;~ zRErgHFfcF_ae^`$a}h6y%?%=WKmu$_CJ)h;RfE4j{q_M1VrAieE3WxH2y}GhZ(mT;70#5fZAP zKtz%UmpDa5piqSbGAKBaL!y-_`fTN{~ zQxBFWIjQN1$(2yR;9v`2WlM^3*iw`Y7q84xnX=rvtE$$G~km$r;_}yZIBo%NGc#97$ zFM@I?B17KdLL?(_<{Z%cc#92`NK=c!`4TNV-eQAhM0hcIivyPM!1)r98E>&cg9%h< z5|IIKv8U(cCne?-uVnZPYW&?Yaj}Z=s4Pe=%FNI64~{9%EQksA^9+cAglkNBYEm({ ziY<7N2EiVEDky$jEq?LFp?S1Eb+X> zujBR_G*1FzHtX`K(6V4fBOuhIo&lMmWpo*o0Q z`~u#O`e2R$nDfC9%rRo%Rap@G!5GXjVc=ClT%rS#-%)uNBh#pHY$BKbhW=8A> zYcR)#fmdmP?FU;h#}1;$9?WrI1UWYMgCirD>%<7r<_zJwFf#BeFYx`~3gNmjLLA}_ z;d(GK@Ji3{{oo1VdND$5@rH1H7$NrfLb!fV#r_a(0K}qCfe>yG)EU7LZU`d-ujCBf z51|ll7$d}I;Sg>FMDK@42saAKjfQYzpn78=+&HLgJcOG7byXsSo5IMzD>5PSLn?%u z#>l`cF+=i0I)s}6mCb~3vltn8MJHr_$cAw97#VoQCRBdNhj0rR8F6b1$c03sUMxBvhE diff --git a/wip/SNIP/client/websockets/__pycache__/__init__.cpython-314.pyc b/wip/SNIP/client/websockets/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index cede6927ff08888081c8011246aaaf5e2f9aeb1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5199 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 diff --git a/wip/SNIP/client/websockets/__pycache__/client.cpython-314.pyc b/wip/SNIP/client/websockets/__pycache__/client.cpython-314.pyc deleted file mode 100644 index f4927df5effd6f16ea0bc4c9656c80bd58be0a12..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17429 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 diff --git a/wip/SNIP/client/websockets/__pycache__/datastructures.cpython-313.pyc b/wip/SNIP/client/websockets/__pycache__/datastructures.cpython-313.pyc deleted file mode 100644 index 22646882c02dec62ba158d51b355887e2a56773a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8895 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 diff --git a/wip/SNIP/client/websockets/__pycache__/frames.cpython-313.pyc b/wip/SNIP/client/websockets/__pycache__/frames.cpython-313.pyc deleted file mode 100644 index ecb746e57cf2bfd9894c1a90cc921232b933cb17..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15901 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 diff --git a/wip/SNIP/client/websockets/__pycache__/frames.cpython-314.pyc b/wip/SNIP/client/websockets/__pycache__/frames.cpython-314.pyc deleted file mode 100644 index 39fe6a3685ccbb18d86d1c8f9472eb646f2adc06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17324 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 diff --git a/wip/SNIP/client/websockets/__pycache__/headers.cpython-314.pyc b/wip/SNIP/client/websockets/__pycache__/headers.cpython-314.pyc deleted file mode 100644 index 6387c190931e0301183b3647a991bb5f4f36317b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20868 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 Mz9%qpGeU}70MqOZqW}N^ diff --git a/wip/SNIP/client/websockets/__pycache__/http11.cpython-313.pyc b/wip/SNIP/client/websockets/__pycache__/http11.cpython-313.pyc deleted file mode 100644 index c339969bb1157fba4fb543ad41651b143727631e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15186 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 diff --git a/wip/SNIP/client/websockets/__pycache__/http11.cpython-314.pyc b/wip/SNIP/client/websockets/__pycache__/http11.cpython-314.pyc deleted file mode 100644 index b5cbd371b9eb5817e6ef0a05a62cb5ab90a63299..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16539 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 diff --git a/wip/SNIP/client/websockets/__pycache__/imports.cpython-313.pyc b/wip/SNIP/client/websockets/__pycache__/imports.cpython-313.pyc deleted file mode 100644 index 1b91a2145e92795c354f19ffb370b35e856b239c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3335 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 Te`OP5RQ#?5YVbjthhW116Y`aC diff --git a/wip/SNIP/client/websockets/__pycache__/protocol.cpython-313.pyc b/wip/SNIP/client/websockets/__pycache__/protocol.cpython-313.pyc deleted file mode 100644 index 7f414796e963a0f5c7485335ee8600cc8a3b260c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24671 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 diff --git a/wip/SNIP/client/websockets/__pycache__/streams.cpython-313.pyc b/wip/SNIP/client/websockets/__pycache__/streams.cpython-313.pyc deleted file mode 100644 index ce6d0e0b9ddf3efabb04a74da0208397f2f1a39d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5231 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 J3?LG0Eda_j$VC7E diff --git a/wip/SNIP/client/websockets/__pycache__/streams.cpython-314.pyc b/wip/SNIP/client/websockets/__pycache__/streams.cpython-314.pyc deleted file mode 100644 index 9544ba5927acccc1640fcdd0b7b0866b286f2ba4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6278 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 diff --git a/wip/SNIP/client/websockets/__pycache__/typing.cpython-314.pyc b/wip/SNIP/client/websockets/__pycache__/typing.cpython-314.pyc deleted file mode 100644 index 89c60307fd27335d4f79d74b74a297ed242cdf6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1305 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 bGHH-9X}B_JM!hdK`iz!eQy3K)Aps5mj=98B diff --git a/wip/SNIP/client/websockets/__pycache__/uri.cpython-314.pyc b/wip/SNIP/client/websockets/__pycache__/uri.cpython-314.pyc deleted file mode 100644 index 72f7141dda1be8de1f771138d47cea51c2f95b85..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9077 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 diff --git a/wip/SNIP/client/websockets/__pycache__/utils.cpython-313.pyc b/wip/SNIP/client/websockets/__pycache__/utils.cpython-313.pyc deleted file mode 100644 index 22e1a304873b02b551da5fe3572a962f7d4a463d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2163 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 P3UM-Of0qCi2w;N&X!QNc diff --git a/wip/SNIP/client/websockets/__pycache__/version.cpython-313.pyc b/wip/SNIP/client/websockets/__pycache__/version.cpython-313.pyc deleted file mode 100644 index 49a30f2c1d4af67de3f9822c7350ca9fe8e23894..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2987 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 diff --git a/wip/SNIP/client/websockets/asyncio/__pycache__/client.cpython-313.pyc b/wip/SNIP/client/websockets/asyncio/__pycache__/client.cpython-313.pyc deleted file mode 100644 index 87a1d8bc42c346c03e71a85f13ab55e818dcdec3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33493 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 diff --git a/wip/SNIP/client/websockets/asyncio/__pycache__/client.cpython-314.pyc b/wip/SNIP/client/websockets/asyncio/__pycache__/client.cpython-314.pyc deleted file mode 100644 index 663577a812a41b3601d361d5319f7993f21b7a16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37436 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 diff --git a/wip/SNIP/client/websockets/asyncio/__pycache__/compatibility.cpython-314.pyc b/wip/SNIP/client/websockets/asyncio/__pycache__/compatibility.cpython-314.pyc deleted file mode 100644 index e1180326d7db4e37164f4d4508e676ecb8686a20..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1044 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�}1odQUSeG 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 SJ_Il`NEkP8USkjeyBPo_!Qnpu diff --git a/wip/SNIP/client/websockets/asyncio/__pycache__/messages.cpython-313.pyc b/wip/SNIP/client/websockets/asyncio/__pycache__/messages.cpython-313.pyc deleted file mode 100644 index 4f06b5694781487b5c8da53e3a3ac299cfe4b053..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13540 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%0A5XjM(9C0z;zk&m>>4lH&`oP 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 B&By=% diff --git a/wip/SNIP/client/websockets/asyncio/__pycache__/server.cpython-313.pyc b/wip/SNIP/client/websockets/asyncio/__pycache__/server.cpython-313.pyc deleted file mode 100644 index adb6850f71a3d21f011f700659c6f61a0d0b6e97..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38218 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 diff --git a/wip/SNIP/client/websockets/asyncio/async_timeout.py b/wip/SNIP/client/websockets/asyncio/async_timeout.py deleted file mode 100644 index 6ffa899..0000000 --- a/wip/SNIP/client/websockets/asyncio/async_timeout.py +++ /dev/null @@ -1,282 +0,0 @@ -# From https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py -# Licensed under the Apache License (Apache-2.0) - -import asyncio -import enum -import sys -import warnings -from types import TracebackType -from typing import Optional, Type - - -if sys.version_info >= (3, 11): - from typing import final -else: - # From https://github.com/python/typing_extensions/blob/main/src/typing_extensions.py - # Licensed under the Python Software Foundation License (PSF-2.0) - - # @final exists in 3.8+, but we backport it for all versions - # before 3.11 to keep support for the __final__ attribute. - # See https://bugs.python.org/issue46342 - def final(f): - """This decorator can be used to indicate to type checkers that - the decorated method cannot be overridden, and decorated class - cannot be subclassed. For example: - - class Base: - @final - def done(self) -> None: - ... - class Sub(Base): - def done(self) -> None: # Error reported by type checker - ... - @final - class Leaf: - ... - class Other(Leaf): # Error reported by type checker - ... - - There is no runtime checking of these properties. The decorator - sets the ``__final__`` attribute to ``True`` on the decorated object - to allow runtime introspection. - """ - try: - f.__final__ = True - except (AttributeError, TypeError): - # Skip the attribute silently if it is not writable. - # AttributeError happens if the object has __slots__ or a - # read-only property, TypeError if it's a builtin class. - pass - return f - - # End https://github.com/python/typing_extensions/blob/main/src/typing_extensions.py - - -if sys.version_info >= (3, 11): - - def _uncancel_task(task: "asyncio.Task[object]") -> None: - task.uncancel() - -else: - - def _uncancel_task(task: "asyncio.Task[object]") -> None: - pass - - -__version__ = "4.0.3" - - -__all__ = ("timeout", "timeout_at", "Timeout") - - -def timeout(delay: Optional[float]) -> "Timeout": - """timeout context manager. - - Useful in cases when you want to apply timeout logic around block - of code or in cases when asyncio.wait_for is not suitable. For example: - - >>> async with timeout(0.001): - ... async with aiohttp.get('https://github.com') as r: - ... await r.text() - - - delay - value in seconds or None to disable timeout logic - """ - loop = asyncio.get_running_loop() - if delay is not None: - deadline = loop.time() + delay # type: Optional[float] - else: - deadline = None - return Timeout(deadline, loop) - - -def timeout_at(deadline: Optional[float]) -> "Timeout": - """Schedule the timeout at absolute time. - - deadline argument points on the time in the same clock system - as loop.time(). - - Please note: it is not POSIX time but a time with - undefined starting base, e.g. the time of the system power on. - - >>> async with timeout_at(loop.time() + 10): - ... async with aiohttp.get('https://github.com') as r: - ... await r.text() - - - """ - loop = asyncio.get_running_loop() - return Timeout(deadline, loop) - - -class _State(enum.Enum): - INIT = "INIT" - ENTER = "ENTER" - TIMEOUT = "TIMEOUT" - EXIT = "EXIT" - - -@final -class Timeout: - # Internal class, please don't instantiate it directly - # Use timeout() and timeout_at() public factories instead. - # - # Implementation note: `async with timeout()` is preferred - # over `with timeout()`. - # While technically the Timeout class implementation - # doesn't need to be async at all, - # the `async with` statement explicitly points that - # the context manager should be used from async function context. - # - # This design allows to avoid many silly misusages. - # - # TimeoutError is raised immediately when scheduled - # if the deadline is passed. - # The purpose is to time out as soon as possible - # without waiting for the next await expression. - - __slots__ = ("_deadline", "_loop", "_state", "_timeout_handler", "_task") - - def __init__( - self, deadline: Optional[float], loop: asyncio.AbstractEventLoop - ) -> None: - self._loop = loop - self._state = _State.INIT - - self._task: Optional["asyncio.Task[object]"] = None - self._timeout_handler = None # type: Optional[asyncio.Handle] - if deadline is None: - self._deadline = None # type: Optional[float] - else: - self.update(deadline) - - def __enter__(self) -> "Timeout": - warnings.warn( - "with timeout() is deprecated, use async with timeout() instead", - DeprecationWarning, - stacklevel=2, - ) - self._do_enter() - return self - - def __exit__( - self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], - ) -> Optional[bool]: - self._do_exit(exc_type) - return None - - async def __aenter__(self) -> "Timeout": - self._do_enter() - return self - - async def __aexit__( - self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], - ) -> Optional[bool]: - self._do_exit(exc_type) - return None - - @property - def expired(self) -> bool: - """Is timeout expired during execution?""" - return self._state == _State.TIMEOUT - - @property - def deadline(self) -> Optional[float]: - return self._deadline - - def reject(self) -> None: - """Reject scheduled timeout if any.""" - # cancel is maybe better name but - # task.cancel() raises CancelledError in asyncio world. - if self._state not in (_State.INIT, _State.ENTER): - raise RuntimeError(f"invalid state {self._state.value}") - self._reject() - - def _reject(self) -> None: - self._task = None - if self._timeout_handler is not None: - self._timeout_handler.cancel() - self._timeout_handler = None - - def shift(self, delay: float) -> None: - """Advance timeout on delay seconds. - - The delay can be negative. - - Raise RuntimeError if shift is called when deadline is not scheduled - """ - deadline = self._deadline - if deadline is None: - raise RuntimeError("cannot shift timeout if deadline is not scheduled") - self.update(deadline + delay) - - def update(self, deadline: float) -> None: - """Set deadline to absolute value. - - deadline argument points on the time in the same clock system - as loop.time(). - - If new deadline is in the past the timeout is raised immediately. - - Please note: it is not POSIX time but a time with - undefined starting base, e.g. the time of the system power on. - """ - if self._state == _State.EXIT: - raise RuntimeError("cannot reschedule after exit from context manager") - if self._state == _State.TIMEOUT: - raise RuntimeError("cannot reschedule expired timeout") - if self._timeout_handler is not None: - self._timeout_handler.cancel() - self._deadline = deadline - if self._state != _State.INIT: - self._reschedule() - - def _reschedule(self) -> None: - assert self._state == _State.ENTER - deadline = self._deadline - if deadline is None: - return - - now = self._loop.time() - if self._timeout_handler is not None: - self._timeout_handler.cancel() - - self._task = asyncio.current_task() - if deadline <= now: - self._timeout_handler = self._loop.call_soon(self._on_timeout) - else: - self._timeout_handler = self._loop.call_at(deadline, self._on_timeout) - - def _do_enter(self) -> None: - if self._state != _State.INIT: - raise RuntimeError(f"invalid state {self._state.value}") - self._state = _State.ENTER - self._reschedule() - - def _do_exit(self, exc_type: Optional[Type[BaseException]]) -> None: - if exc_type is asyncio.CancelledError and self._state == _State.TIMEOUT: - assert self._task is not None - _uncancel_task(self._task) - self._timeout_handler = None - self._task = None - raise asyncio.TimeoutError - # timeout has not expired - self._state = _State.EXIT - self._reject() - return None - - def _on_timeout(self) -> None: - assert self._task is not None - self._task.cancel() - self._state = _State.TIMEOUT - # drop the reference early - self._timeout_handler = None - - -# End https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py diff --git a/wip/SNIP/client/websockets/asyncio/client.py b/wip/SNIP/client/websockets/asyncio/client.py deleted file mode 100644 index 38a56dd..0000000 --- a/wip/SNIP/client/websockets/asyncio/client.py +++ /dev/null @@ -1,820 +0,0 @@ -from __future__ import annotations - -import asyncio -import logging -import os -import socket -import ssl as ssl_module -import traceback -import urllib.parse -from collections.abc import AsyncIterator, Generator, Sequence -from types import TracebackType -from typing import Any, Callable, Literal, cast - -from ..client import ClientProtocol, backoff -from ..datastructures import Headers, HeadersLike -from ..exceptions import ( - InvalidMessage, - InvalidProxyMessage, - InvalidProxyStatus, - InvalidStatus, - ProxyError, - SecurityError, -) -from ..extensions.base import ClientExtensionFactory -from ..extensions.permessage_deflate import enable_client_permessage_deflate -from ..headers import build_authorization_basic, build_host, validate_subprotocols -from ..http11 import USER_AGENT, Response -from ..protocol import CONNECTING, Event -from ..streams import StreamReader -from ..typing import LoggerLike, Origin, Subprotocol -from ..uri import Proxy, WebSocketURI, get_proxy, parse_proxy, parse_uri -from .compatibility import TimeoutError, asyncio_timeout -from .connection import Connection - - -__all__ = ["connect", "unix_connect", "ClientConnection"] - -MAX_REDIRECTS = int(os.environ.get("WEBSOCKETS_MAX_REDIRECTS", "10")) - - -class ClientConnection(Connection): - """ - :mod:`asyncio` implementation of a WebSocket client connection. - - :class:`ClientConnection` provides :meth:`recv` and :meth:`send` coroutines - for receiving and sending messages. - - It supports asynchronous iteration to receive messages:: - - async for message in websocket: - await process(message) - - The iterator exits normally when the connection is closed with close code - 1000 (OK) or 1001 (going away) or without a close code. It raises a - :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is - closed with any other code. - - The ``ping_interval``, ``ping_timeout``, ``close_timeout``, ``max_queue``, - and ``write_limit`` arguments have the same meaning as in :func:`connect`. - - Args: - protocol: Sans-I/O connection. - - """ - - def __init__( - self, - protocol: ClientProtocol, - *, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = 10, - max_queue: int | None | tuple[int | None, int | None] = 16, - write_limit: int | tuple[int, int | None] = 2**15, - ) -> None: - self.protocol: ClientProtocol - super().__init__( - protocol, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - close_timeout=close_timeout, - max_queue=max_queue, - write_limit=write_limit, - ) - self.response_rcvd: asyncio.Future[None] = self.loop.create_future() - - async def handshake( - self, - additional_headers: HeadersLike | None = None, - user_agent_header: str | None = USER_AGENT, - ) -> None: - """ - Perform the opening handshake. - - """ - async with self.send_context(expected_state=CONNECTING): - self.request = self.protocol.connect() - if additional_headers is not None: - self.request.headers.update(additional_headers) - if user_agent_header is not None: - self.request.headers.setdefault("User-Agent", user_agent_header) - self.protocol.send_request(self.request) - - await asyncio.wait( - [self.response_rcvd, self.connection_lost_waiter], - return_when=asyncio.FIRST_COMPLETED, - ) - - # self.protocol.handshake_exc is set when the connection is lost before - # receiving a response, when the response cannot be parsed, or when the - # response fails the handshake. - - if self.protocol.handshake_exc is not None: - raise self.protocol.handshake_exc - - def process_event(self, event: Event) -> None: - """ - Process one incoming event. - - """ - # First event - handshake response. - if self.response is None: - assert isinstance(event, Response) - self.response = event - self.response_rcvd.set_result(None) - # Later events - frames. - else: - super().process_event(event) - - -def process_exception(exc: Exception) -> Exception | None: - """ - Determine whether a connection error is retryable or fatal. - - When reconnecting automatically with ``async for ... in connect(...)``, if a - connection attempt fails, :func:`process_exception` is called to determine - whether to retry connecting or to raise the exception. - - This function defines the default behavior, which is to retry on: - - * :exc:`EOFError`, :exc:`OSError`, :exc:`asyncio.TimeoutError`: network - errors; - * :exc:`~websockets.exceptions.InvalidStatus` when the status code is 500, - 502, 503, or 504: server or proxy errors. - - All other exceptions are considered fatal. - - You can change this behavior with the ``process_exception`` argument of - :func:`connect`. - - Return :obj:`None` if the exception is retryable i.e. when the error could - be transient and trying to reconnect with the same parameters could succeed. - The exception will be logged at the ``INFO`` level. - - Return an exception, either ``exc`` or a new exception, if the exception is - fatal i.e. when trying to reconnect will most likely produce the same error. - That exception will be raised, breaking out of the retry loop. - - """ - # This catches python-socks' ProxyConnectionError and ProxyTimeoutError. - # Remove asyncio.TimeoutError when dropping Python < 3.11. - if isinstance(exc, (OSError, TimeoutError, asyncio.TimeoutError)): - return None - if isinstance(exc, InvalidMessage) and isinstance(exc.__cause__, EOFError): - return None - if isinstance(exc, InvalidStatus) and exc.response.status_code in [ - 500, # Internal Server Error - 502, # Bad Gateway - 503, # Service Unavailable - 504, # Gateway Timeout - ]: - return None - return exc - - -# This is spelled in lower case because it's exposed as a callable in the API. -class connect: - """ - Connect to the WebSocket server at ``uri``. - - This coroutine returns a :class:`ClientConnection` instance, which you can - use to send and receive messages. - - :func:`connect` may be used as an asynchronous context manager:: - - from websockets.asyncio.client import connect - - async with connect(...) as websocket: - ... - - The connection is closed automatically when exiting the context. - - :func:`connect` can be used as an infinite asynchronous iterator to - reconnect automatically on errors:: - - async for websocket in connect(...): - try: - ... - except websockets.exceptions.ConnectionClosed: - continue - - If the connection fails with a transient error, it is retried with - exponential backoff. If it fails with a fatal error, the exception is - raised, breaking out of the loop. - - The connection is closed automatically after each iteration of the loop. - - Args: - uri: URI of the WebSocket server. - origin: Value of the ``Origin`` header, for servers that require it. - extensions: List of supported extensions, in order in which they - should be negotiated and run. - subprotocols: List of supported subprotocols, in order of decreasing - preference. - compression: The "permessage-deflate" extension is enabled by default. - Set ``compression`` to :obj:`None` to disable it. See the - :doc:`compression guide <../../topics/compression>` for details. - additional_headers (HeadersLike | None): Arbitrary HTTP headers to add - to the handshake request. - user_agent_header: Value of the ``User-Agent`` request header. - It defaults to ``"Python/x.y.z websockets/X.Y"``. - Setting it to :obj:`None` removes the header. - proxy: If a proxy is configured, it is used by default. Set ``proxy`` - to :obj:`None` to disable the proxy or to the address of a proxy - to override the system configuration. See the :doc:`proxy docs - <../../topics/proxies>` for details. - process_exception: When reconnecting automatically, tell whether an - error is transient or fatal. The default behavior is defined by - :func:`process_exception`. Refer to its documentation for details. - open_timeout: Timeout for opening the connection in seconds. - :obj:`None` disables the timeout. - ping_interval: Interval between keepalive pings in seconds. - :obj:`None` disables keepalive. - ping_timeout: Timeout for keepalive pings in seconds. - :obj:`None` disables timeouts. - close_timeout: Timeout for closing the connection in seconds. - :obj:`None` disables the timeout. - max_size: Maximum size of incoming messages in bytes. - :obj:`None` disables the limit. - max_queue: High-water mark of the buffer where frames are received. - It defaults to 16 frames. The low-water mark defaults to ``max_queue - // 4``. You may pass a ``(high, low)`` tuple to set the high-water - and low-water marks. If you want to disable flow control entirely, - you may set it to ``None``, although that's a bad idea. - write_limit: High-water mark of write buffer in bytes. It is passed to - :meth:`~asyncio.WriteTransport.set_write_buffer_limits`. It defaults - to 32 KiB. You may pass a ``(high, low)`` tuple to set the - high-water and low-water marks. - logger: Logger for this client. - It defaults to ``logging.getLogger("websockets.client")``. - See the :doc:`logging guide <../../topics/logging>` for details. - create_connection: Factory for the :class:`ClientConnection` managing - the connection. Set it to a wrapper or a subclass to customize - connection handling. - - Any other keyword arguments are passed to the event loop's - :meth:`~asyncio.loop.create_connection` method. - - For example: - - * You can set ``ssl`` to a :class:`~ssl.SSLContext` to enforce TLS settings. - When connecting to a ``wss://`` URI, if ``ssl`` isn't provided, a TLS - context is created with :func:`~ssl.create_default_context`. - - * You can set ``server_hostname`` to override the host name from ``uri`` in - the TLS handshake. - - * You can set ``host`` and ``port`` to connect to a different host and port - from those found in ``uri``. This only changes the destination of the TCP - connection. The host name from ``uri`` is still used in the TLS handshake - for secure connections and in the ``Host`` header. - - * You can set ``sock`` to provide a preexisting TCP socket. You may call - :func:`socket.create_connection` (not to be confused with the event loop's - :meth:`~asyncio.loop.create_connection` method) to create a suitable - client socket and customize it. - - When using a proxy: - - * Prefix keyword arguments with ``proxy_`` for configuring TLS between the - client and an HTTPS proxy: ``proxy_ssl``, ``proxy_server_hostname``, - ``proxy_ssl_handshake_timeout``, and ``proxy_ssl_shutdown_timeout``. - * Use the standard keyword arguments for configuring TLS between the proxy - and the WebSocket server: ``ssl``, ``server_hostname``, - ``ssl_handshake_timeout``, and ``ssl_shutdown_timeout``. - * Other keyword arguments are used only for connecting to the proxy. - - Raises: - InvalidURI: If ``uri`` isn't a valid WebSocket URI. - InvalidProxy: If ``proxy`` isn't a valid proxy. - OSError: If the TCP connection fails. - InvalidHandshake: If the opening handshake fails. - TimeoutError: If the opening handshake times out. - - """ - - def __init__( - self, - uri: str, - *, - # WebSocket - origin: Origin | None = None, - extensions: Sequence[ClientExtensionFactory] | None = None, - subprotocols: Sequence[Subprotocol] | None = None, - compression: str | None = "deflate", - # HTTP - additional_headers: HeadersLike | None = None, - user_agent_header: str | None = USER_AGENT, - proxy: str | Literal[True] | None = True, - process_exception: Callable[[Exception], Exception | None] = process_exception, - # Timeouts - open_timeout: float | None = 10, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = 10, - # Limits - max_size: int | None = 2**20, - max_queue: int | None | tuple[int | None, int | None] = 16, - write_limit: int | tuple[int, int | None] = 2**15, - # Logging - logger: LoggerLike | None = None, - # Escape hatch for advanced customization - create_connection: type[ClientConnection] | None = None, - # Other keyword arguments are passed to loop.create_connection - **kwargs: Any, - ) -> None: - self.uri = uri - - if subprotocols is not None: - validate_subprotocols(subprotocols) - - if compression == "deflate": - extensions = enable_client_permessage_deflate(extensions) - elif compression is not None: - raise ValueError(f"unsupported compression: {compression}") - - if logger is None: - logger = logging.getLogger("websockets.client") - - if create_connection is None: - create_connection = ClientConnection - - def protocol_factory(uri: WebSocketURI) -> ClientConnection: - # This is a protocol in the Sans-I/O implementation of websockets. - protocol = ClientProtocol( - uri, - origin=origin, - extensions=extensions, - subprotocols=subprotocols, - max_size=max_size, - logger=logger, - ) - # This is a connection in websockets and a protocol in asyncio. - connection = create_connection( - protocol, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - close_timeout=close_timeout, - max_queue=max_queue, - write_limit=write_limit, - ) - return connection - - self.proxy = proxy - self.protocol_factory = protocol_factory - self.additional_headers = additional_headers - self.user_agent_header = user_agent_header - self.process_exception = process_exception - self.open_timeout = open_timeout - self.logger = logger - self.connection_kwargs = kwargs - - async def create_connection(self) -> ClientConnection: - """Create TCP or Unix connection.""" - loop = asyncio.get_running_loop() - kwargs = self.connection_kwargs.copy() - - ws_uri = parse_uri(self.uri) - - proxy = self.proxy - if kwargs.get("unix", False): - proxy = None - if kwargs.get("sock") is not None: - proxy = None - if proxy is True: - proxy = get_proxy(ws_uri) - - def factory() -> ClientConnection: - return self.protocol_factory(ws_uri) - - if ws_uri.secure: - kwargs.setdefault("ssl", True) - kwargs.setdefault("server_hostname", ws_uri.host) - if kwargs.get("ssl") is None: - raise ValueError("ssl=None is incompatible with a wss:// URI") - else: - if kwargs.get("ssl") is not None: - raise ValueError("ssl argument is incompatible with a ws:// URI") - - if kwargs.pop("unix", False): - _, connection = await loop.create_unix_connection(factory, **kwargs) - elif proxy is not None: - proxy_parsed = parse_proxy(proxy) - if proxy_parsed.scheme[:5] == "socks": - # Connect to the server through the proxy. - sock = await connect_socks_proxy( - proxy_parsed, - ws_uri, - local_addr=kwargs.pop("local_addr", None), - ) - # Initialize WebSocket connection via the proxy. - _, connection = await loop.create_connection( - factory, - sock=sock, - **kwargs, - ) - elif proxy_parsed.scheme[:4] == "http": - # Split keyword arguments between the proxy and the server. - all_kwargs, proxy_kwargs, kwargs = kwargs, {}, {} - for key, value in all_kwargs.items(): - if key.startswith("ssl") or key == "server_hostname": - kwargs[key] = value - elif key.startswith("proxy_"): - proxy_kwargs[key[6:]] = value - else: - proxy_kwargs[key] = value - # Validate the proxy_ssl argument. - if proxy_parsed.scheme == "https": - proxy_kwargs.setdefault("ssl", True) - if proxy_kwargs.get("ssl") is None: - raise ValueError( - "proxy_ssl=None is incompatible with an https:// proxy" - ) - else: - if proxy_kwargs.get("ssl") is not None: - raise ValueError( - "proxy_ssl argument is incompatible with an http:// proxy" - ) - # Connect to the server through the proxy. - transport = await connect_http_proxy( - proxy_parsed, - ws_uri, - user_agent_header=self.user_agent_header, - **proxy_kwargs, - ) - # Initialize WebSocket connection via the proxy. - connection = factory() - transport.set_protocol(connection) - ssl = kwargs.pop("ssl", None) - if ssl is True: - ssl = ssl_module.create_default_context() - if ssl is not None: - new_transport = await loop.start_tls( - transport, connection, ssl, **kwargs - ) - assert new_transport is not None # help mypy - transport = new_transport - connection.connection_made(transport) - else: - raise AssertionError("unsupported proxy") - else: - # Connect to the server directly. - if kwargs.get("sock") is None: - kwargs.setdefault("host", ws_uri.host) - kwargs.setdefault("port", ws_uri.port) - # Initialize WebSocket connection. - _, connection = await loop.create_connection(factory, **kwargs) - return connection - - def process_redirect(self, exc: Exception) -> Exception | str: - """ - Determine whether a connection error is a redirect that can be followed. - - Return the new URI if it's a valid redirect. Else, return an exception. - - """ - if not ( - isinstance(exc, InvalidStatus) - and exc.response.status_code - in [ - 300, # Multiple Choices - 301, # Moved Permanently - 302, # Found - 303, # See Other - 307, # Temporary Redirect - 308, # Permanent Redirect - ] - and "Location" in exc.response.headers - ): - return exc - - old_ws_uri = parse_uri(self.uri) - new_uri = urllib.parse.urljoin(self.uri, exc.response.headers["Location"]) - new_ws_uri = parse_uri(new_uri) - - # If connect() received a socket, it is closed and cannot be reused. - if self.connection_kwargs.get("sock") is not None: - return ValueError( - f"cannot follow redirect to {new_uri} with a preexisting socket" - ) - - # TLS downgrade is forbidden. - if old_ws_uri.secure and not new_ws_uri.secure: - return SecurityError(f"cannot follow redirect to non-secure URI {new_uri}") - - # Apply restrictions to cross-origin redirects. - if ( - old_ws_uri.secure != new_ws_uri.secure - or old_ws_uri.host != new_ws_uri.host - or old_ws_uri.port != new_ws_uri.port - ): - # Cross-origin redirects on Unix sockets don't quite make sense. - if self.connection_kwargs.get("unix", False): - return ValueError( - f"cannot follow cross-origin redirect to {new_uri} " - f"with a Unix socket" - ) - - # Cross-origin redirects when host and port are overridden are ill-defined. - if ( - self.connection_kwargs.get("host") is not None - or self.connection_kwargs.get("port") is not None - ): - return ValueError( - f"cannot follow cross-origin redirect to {new_uri} " - f"with an explicit host or port" - ) - - return new_uri - - # ... = await connect(...) - - def __await__(self) -> Generator[Any, None, ClientConnection]: - # Create a suitable iterator by calling __await__ on a coroutine. - return self.__await_impl__().__await__() - - async def __await_impl__(self) -> ClientConnection: - try: - async with asyncio_timeout(self.open_timeout): - for _ in range(MAX_REDIRECTS): - self.connection = await self.create_connection() - try: - await self.connection.handshake( - self.additional_headers, - self.user_agent_header, - ) - except asyncio.CancelledError: - self.connection.transport.abort() - raise - except Exception as exc: - # Always close the connection even though keep-alive is - # the default in HTTP/1.1 because create_connection ties - # opening the network connection with initializing the - # protocol. In the current design of connect(), there is - # no easy way to reuse the network connection that works - # in every case nor to reinitialize the protocol. - self.connection.transport.abort() - - uri_or_exc = self.process_redirect(exc) - # Response is a valid redirect; follow it. - if isinstance(uri_or_exc, str): - self.uri = uri_or_exc - continue - # Response isn't a valid redirect; raise the exception. - if uri_or_exc is exc: - raise - else: - raise uri_or_exc from exc - - else: - self.connection.start_keepalive() - return self.connection - else: - raise SecurityError(f"more than {MAX_REDIRECTS} redirects") - - except TimeoutError as exc: - # Re-raise exception with an informative error message. - raise TimeoutError("timed out during opening handshake") from exc - - # ... = yield from connect(...) - remove when dropping Python < 3.10 - - __iter__ = __await__ - - # async with connect(...) as ...: ... - - async def __aenter__(self) -> ClientConnection: - return await self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - await self.connection.close() - - # async for ... in connect(...): - - async def __aiter__(self) -> AsyncIterator[ClientConnection]: - delays: Generator[float] | None = None - while True: - try: - async with self as protocol: - yield protocol - except Exception as exc: - # Determine whether the exception is retryable or fatal. - # The API of process_exception is "return an exception or None"; - # "raise an exception" is also supported because it's a frequent - # mistake. It isn't documented in order to keep the API simple. - try: - new_exc = self.process_exception(exc) - except Exception as raised_exc: - new_exc = raised_exc - - # The connection failed with a fatal error. - # Raise the exception and exit the loop. - if new_exc is exc: - raise - if new_exc is not None: - raise new_exc from exc - - # The connection failed with a retryable error. - # Start or continue backoff and reconnect. - if delays is None: - delays = backoff() - delay = next(delays) - self.logger.info( - "connect failed; reconnecting in %.1f seconds: %s", - delay, - # Remove first argument when dropping Python 3.9. - traceback.format_exception_only(type(exc), exc)[0].strip(), - ) - await asyncio.sleep(delay) - continue - - else: - # The connection succeeded. Reset backoff. - delays = None - - -def unix_connect( - path: str | None = None, - uri: str | None = None, - **kwargs: Any, -) -> connect: - """ - Connect to a WebSocket server listening on a Unix socket. - - This function accepts the same keyword arguments as :func:`connect`. - - It's only available on Unix. - - It's mainly useful for debugging servers listening on Unix sockets. - - Args: - path: File system path to the Unix socket. - uri: URI of the WebSocket server. ``uri`` defaults to - ``ws://localhost/`` or, when a ``ssl`` argument is provided, to - ``wss://localhost/``. - - """ - if uri is None: - if kwargs.get("ssl") is None: - uri = "ws://localhost/" - else: - uri = "wss://localhost/" - return connect(uri=uri, unix=True, path=path, **kwargs) - - -try: - from python_socks import ProxyType - from python_socks.async_.asyncio import Proxy as SocksProxy - - SOCKS_PROXY_TYPES = { - "socks5h": ProxyType.SOCKS5, - "socks5": ProxyType.SOCKS5, - "socks4a": ProxyType.SOCKS4, - "socks4": ProxyType.SOCKS4, - } - - SOCKS_PROXY_RDNS = { - "socks5h": True, - "socks5": False, - "socks4a": True, - "socks4": False, - } - - async def connect_socks_proxy( - proxy: Proxy, - ws_uri: WebSocketURI, - **kwargs: Any, - ) -> socket.socket: - """Connect via a SOCKS proxy and return the socket.""" - socks_proxy = SocksProxy( - SOCKS_PROXY_TYPES[proxy.scheme], - proxy.host, - proxy.port, - proxy.username, - proxy.password, - SOCKS_PROXY_RDNS[proxy.scheme], - ) - # connect() is documented to raise OSError. - # socks_proxy.connect() doesn't raise TimeoutError; it gets canceled. - # Wrap other exceptions in ProxyError, a subclass of InvalidHandshake. - try: - return await socks_proxy.connect(ws_uri.host, ws_uri.port, **kwargs) - except OSError: - raise - except Exception as exc: - raise ProxyError("failed to connect to SOCKS proxy") from exc - -except ImportError: - - async def connect_socks_proxy( - proxy: Proxy, - ws_uri: WebSocketURI, - **kwargs: Any, - ) -> socket.socket: - raise ImportError("python-socks is required to use a SOCKS proxy") - - -def prepare_connect_request( - proxy: Proxy, - ws_uri: WebSocketURI, - user_agent_header: str | None = None, -) -> bytes: - host = build_host(ws_uri.host, ws_uri.port, ws_uri.secure, always_include_port=True) - headers = Headers() - headers["Host"] = build_host(ws_uri.host, ws_uri.port, ws_uri.secure) - if user_agent_header is not None: - headers["User-Agent"] = user_agent_header - if proxy.username is not None: - assert proxy.password is not None # enforced by parse_proxy() - headers["Proxy-Authorization"] = build_authorization_basic( - proxy.username, proxy.password - ) - # We cannot use the Request class because it supports only GET requests. - return f"CONNECT {host} HTTP/1.1\r\n".encode() + headers.serialize() - - -class HTTPProxyConnection(asyncio.Protocol): - def __init__( - self, - ws_uri: WebSocketURI, - proxy: Proxy, - user_agent_header: str | None = None, - ): - self.ws_uri = ws_uri - self.proxy = proxy - self.user_agent_header = user_agent_header - - self.reader = StreamReader() - self.parser = Response.parse( - self.reader.read_line, - self.reader.read_exact, - self.reader.read_to_eof, - include_body=False, - ) - - loop = asyncio.get_running_loop() - self.response: asyncio.Future[Response] = loop.create_future() - - def run_parser(self) -> None: - try: - next(self.parser) - except StopIteration as exc: - response = exc.value - if 200 <= response.status_code < 300: - self.response.set_result(response) - else: - self.response.set_exception(InvalidProxyStatus(response)) - except Exception as exc: - proxy_exc = InvalidProxyMessage( - "did not receive a valid HTTP response from proxy" - ) - proxy_exc.__cause__ = exc - self.response.set_exception(proxy_exc) - - def connection_made(self, transport: asyncio.BaseTransport) -> None: - transport = cast(asyncio.Transport, transport) - self.transport = transport - self.transport.write( - prepare_connect_request(self.proxy, self.ws_uri, self.user_agent_header) - ) - - def data_received(self, data: bytes) -> None: - self.reader.feed_data(data) - self.run_parser() - - def eof_received(self) -> None: - self.reader.feed_eof() - self.run_parser() - - def connection_lost(self, exc: Exception | None) -> None: - self.reader.feed_eof() - if exc is not None: - self.response.set_exception(exc) - - -async def connect_http_proxy( - proxy: Proxy, - ws_uri: WebSocketURI, - user_agent_header: str | None = None, - **kwargs: Any, -) -> asyncio.Transport: - transport, protocol = await asyncio.get_running_loop().create_connection( - lambda: HTTPProxyConnection(ws_uri, proxy, user_agent_header), - proxy.host, - proxy.port, - **kwargs, - ) - - try: - # This raises exceptions if the connection to the proxy fails. - await protocol.response - except Exception: - transport.close() - raise - - return transport diff --git a/wip/SNIP/client/websockets/asyncio/compatibility.py b/wip/SNIP/client/websockets/asyncio/compatibility.py deleted file mode 100644 index e170000..0000000 --- a/wip/SNIP/client/websockets/asyncio/compatibility.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import annotations - -import sys - - -__all__ = ["TimeoutError", "aiter", "anext", "asyncio_timeout", "asyncio_timeout_at"] - - -if sys.version_info[:2] >= (3, 11): - TimeoutError = TimeoutError - aiter = aiter - anext = anext - from asyncio import ( - timeout as asyncio_timeout, # noqa: F401 - timeout_at as asyncio_timeout_at, # noqa: F401 - ) - -else: # Python < 3.11 - from asyncio import TimeoutError - - def aiter(async_iterable): - return type(async_iterable).__aiter__(async_iterable) - - async def anext(async_iterator): - return await type(async_iterator).__anext__(async_iterator) - - from .async_timeout import ( - timeout as asyncio_timeout, # noqa: F401 - timeout_at as asyncio_timeout_at, # noqa: F401 - ) diff --git a/wip/SNIP/client/websockets/asyncio/connection.py b/wip/SNIP/client/websockets/asyncio/connection.py deleted file mode 100644 index 1b51e47..0000000 --- a/wip/SNIP/client/websockets/asyncio/connection.py +++ /dev/null @@ -1,1237 +0,0 @@ -from __future__ import annotations - -import asyncio -import collections -import contextlib -import logging -import random -import struct -import sys -import traceback -import uuid -from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Iterable, Mapping -from types import TracebackType -from typing import Any, Literal, cast, overload - -from ..exceptions import ( - ConcurrencyError, - ConnectionClosed, - ConnectionClosedOK, - ProtocolError, -) -from ..frames import DATA_OPCODES, BytesLike, CloseCode, Frame, Opcode -from ..http11 import Request, Response -from ..protocol import CLOSED, OPEN, Event, Protocol, State -from ..typing import Data, LoggerLike, Subprotocol -from .compatibility import ( - TimeoutError, - aiter, - anext, - asyncio_timeout, - asyncio_timeout_at, -) -from .messages import Assembler - - -__all__ = ["Connection"] - - -class Connection(asyncio.Protocol): - """ - :mod:`asyncio` implementation of a WebSocket connection. - - :class:`Connection` provides APIs shared between WebSocket servers and - clients. - - You shouldn't use it directly. Instead, use - :class:`~websockets.asyncio.client.ClientConnection` or - :class:`~websockets.asyncio.server.ServerConnection`. - - """ - - def __init__( - self, - protocol: Protocol, - *, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = 10, - max_queue: int | None | tuple[int | None, int | None] = 16, - write_limit: int | tuple[int, int | None] = 2**15, - ) -> None: - self.protocol = protocol - self.ping_interval = ping_interval - self.ping_timeout = ping_timeout - self.close_timeout = close_timeout - if isinstance(max_queue, int) or max_queue is None: - max_queue = (max_queue, None) - self.max_queue = max_queue - if isinstance(write_limit, int): - write_limit = (write_limit, None) - self.write_limit = write_limit - - # Inject reference to this instance in the protocol's logger. - self.protocol.logger = logging.LoggerAdapter( - self.protocol.logger, - {"websocket": self}, - ) - - # Copy attributes from the protocol for convenience. - self.id: uuid.UUID = self.protocol.id - """Unique identifier of the connection. Useful in logs.""" - self.logger: LoggerLike = self.protocol.logger - """Logger for this connection.""" - self.debug = self.protocol.debug - - # HTTP handshake request and response. - self.request: Request | None = None - """Opening handshake request.""" - self.response: Response | None = None - """Opening handshake response.""" - - # Event loop running this connection. - self.loop = asyncio.get_running_loop() - - # Assembler turning frames into messages and serializing reads. - self.recv_messages: Assembler # initialized in connection_made - - # Deadline for the closing handshake. - self.close_deadline: float | None = None - - # Protect sending fragmented messages. - self.fragmented_send_waiter: asyncio.Future[None] | None = None - - # Mapping of ping IDs to pong waiters, in chronological order. - self.pong_waiters: dict[bytes, tuple[asyncio.Future[float], float]] = {} - - self.latency: float = 0 - """ - Latency of the connection, in seconds. - - Latency is defined as the round-trip time of the connection. It is - measured by sending a Ping frame and waiting for a matching Pong frame. - Before the first measurement, :attr:`latency` is ``0``. - - By default, websockets enables a :ref:`keepalive ` mechanism - that sends Ping frames automatically at regular intervals. You can also - send Ping frames and measure latency with :meth:`ping`. - """ - - # Task that sends keepalive pings. None when ping_interval is None. - self.keepalive_task: asyncio.Task[None] | None = None - - # Exception raised while reading from the connection, to be chained to - # ConnectionClosed in order to show why the TCP connection dropped. - self.recv_exc: BaseException | None = None - - # Completed when the TCP connection is closed and the WebSocket - # connection state becomes CLOSED. - self.connection_lost_waiter: asyncio.Future[None] = self.loop.create_future() - - # Adapted from asyncio.FlowControlMixin - self.paused: bool = False - self.drain_waiters: collections.deque[asyncio.Future[None]] = ( - collections.deque() - ) - - # Public attributes - - @property - def local_address(self) -> Any: - """ - Local address of the connection. - - For IPv4 connections, this is a ``(host, port)`` tuple. - - The format of the address depends on the address family. - See :meth:`~socket.socket.getsockname`. - - """ - return self.transport.get_extra_info("sockname") - - @property - def remote_address(self) -> Any: - """ - Remote address of the connection. - - For IPv4 connections, this is a ``(host, port)`` tuple. - - The format of the address depends on the address family. - See :meth:`~socket.socket.getpeername`. - - """ - return self.transport.get_extra_info("peername") - - @property - def state(self) -> State: - """ - State of the WebSocket connection, defined in :rfc:`6455`. - - This attribute is provided for completeness. Typical applications - shouldn't check its value. Instead, they should call :meth:`~recv` or - :meth:`send` and handle :exc:`~websockets.exceptions.ConnectionClosed` - exceptions. - - """ - return self.protocol.state - - @property - def subprotocol(self) -> Subprotocol | None: - """ - Subprotocol negotiated during the opening handshake. - - :obj:`None` if no subprotocol was negotiated. - - """ - return self.protocol.subprotocol - - @property - def close_code(self) -> int | None: - """ - State of the WebSocket connection, defined in :rfc:`6455`. - - This attribute is provided for completeness. Typical applications - shouldn't check its value. Instead, they should inspect attributes - of :exc:`~websockets.exceptions.ConnectionClosed` exceptions. - - """ - return self.protocol.close_code - - @property - def close_reason(self) -> str | None: - """ - State of the WebSocket connection, defined in :rfc:`6455`. - - This attribute is provided for completeness. Typical applications - shouldn't check its value. Instead, they should inspect attributes - of :exc:`~websockets.exceptions.ConnectionClosed` exceptions. - - """ - return self.protocol.close_reason - - # Public methods - - async def __aenter__(self) -> Connection: - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - if exc_type is None: - await self.close() - else: - await self.close(CloseCode.INTERNAL_ERROR) - - async def __aiter__(self) -> AsyncIterator[Data]: - """ - Iterate on incoming messages. - - The iterator calls :meth:`recv` and yields messages asynchronously in an - infinite loop. - - It exits when the connection is closed normally. It raises a - :exc:`~websockets.exceptions.ConnectionClosedError` exception after a - protocol error or a network failure. - - """ - try: - while True: - yield await self.recv() - except ConnectionClosedOK: - return - - @overload - async def recv(self, decode: Literal[True]) -> str: ... - - @overload - async def recv(self, decode: Literal[False]) -> bytes: ... - - @overload - async def recv(self, decode: bool | None = None) -> Data: ... - - async def recv(self, decode: bool | None = None) -> Data: - """ - Receive the next message. - - When the connection is closed, :meth:`recv` raises - :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it raises - :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal closure - and :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol - error or a network failure. This is how you detect the end of the - message stream. - - Canceling :meth:`recv` is safe. There's no risk of losing data. The next - invocation of :meth:`recv` will return the next message. - - This makes it possible to enforce a timeout by wrapping :meth:`recv` in - :func:`~asyncio.timeout` or :func:`~asyncio.wait_for`. - - When the message is fragmented, :meth:`recv` waits until all fragments - are received, reassembles them, and returns the whole message. - - Args: - decode: Set this flag to override the default behavior of returning - :class:`str` or :class:`bytes`. See below for details. - - Returns: - A string (:class:`str`) for a Text_ frame or a bytestring - (:class:`bytes`) for a Binary_ frame. - - .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - .. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - You may override this behavior with the ``decode`` argument: - - * Set ``decode=False`` to disable UTF-8 decoding of Text_ frames and - return a bytestring (:class:`bytes`). This improves performance - when decoding isn't needed, for example if the message contains - JSON and you're using a JSON library that expects a bytestring. - * Set ``decode=True`` to force UTF-8 decoding of Binary_ frames - and return a string (:class:`str`). This may be useful for - servers that send binary frames instead of text frames. - - Raises: - ConnectionClosed: When the connection is closed. - ConcurrencyError: If two coroutines call :meth:`recv` or - :meth:`recv_streaming` concurrently. - - """ - try: - return await self.recv_messages.get(decode) - except EOFError: - pass - # fallthrough - except ConcurrencyError: - raise ConcurrencyError( - "cannot call recv while another coroutine " - "is already running recv or recv_streaming" - ) from None - except UnicodeDecodeError as exc: - async with self.send_context(): - self.protocol.fail( - CloseCode.INVALID_DATA, - f"{exc.reason} at position {exc.start}", - ) - # fallthrough - - # Wait for the protocol state to be CLOSED before accessing close_exc. - await asyncio.shield(self.connection_lost_waiter) - raise self.protocol.close_exc from self.recv_exc - - @overload - def recv_streaming(self, decode: Literal[True]) -> AsyncIterator[str]: ... - - @overload - def recv_streaming(self, decode: Literal[False]) -> AsyncIterator[bytes]: ... - - @overload - def recv_streaming(self, decode: bool | None = None) -> AsyncIterator[Data]: ... - - async def recv_streaming(self, decode: bool | None = None) -> AsyncIterator[Data]: - """ - Receive the next message frame by frame. - - This method is designed for receiving fragmented messages. It returns an - asynchronous iterator that yields each fragment as it is received. This - iterator must be fully consumed. Else, future calls to :meth:`recv` or - :meth:`recv_streaming` will raise - :exc:`~websockets.exceptions.ConcurrencyError`, making the connection - unusable. - - :meth:`recv_streaming` raises the same exceptions as :meth:`recv`. - - Canceling :meth:`recv_streaming` before receiving the first frame is - safe. Canceling it after receiving one or more frames leaves the - iterator in a partially consumed state, making the connection unusable. - Instead, you should close the connection with :meth:`close`. - - Args: - decode: Set this flag to override the default behavior of returning - :class:`str` or :class:`bytes`. See below for details. - - Returns: - An iterator of strings (:class:`str`) for a Text_ frame or - bytestrings (:class:`bytes`) for a Binary_ frame. - - .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - .. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - You may override this behavior with the ``decode`` argument: - - * Set ``decode=False`` to disable UTF-8 decoding of Text_ frames - and return bytestrings (:class:`bytes`). This may be useful to - optimize performance when decoding isn't needed. - * Set ``decode=True`` to force UTF-8 decoding of Binary_ frames - and return strings (:class:`str`). This is useful for servers - that send binary frames instead of text frames. - - Raises: - ConnectionClosed: When the connection is closed. - ConcurrencyError: If two coroutines call :meth:`recv` or - :meth:`recv_streaming` concurrently. - - """ - try: - async for frame in self.recv_messages.get_iter(decode): - yield frame - return - except EOFError: - pass - # fallthrough - except ConcurrencyError: - raise ConcurrencyError( - "cannot call recv_streaming while another coroutine " - "is already running recv or recv_streaming" - ) from None - except UnicodeDecodeError as exc: - async with self.send_context(): - self.protocol.fail( - CloseCode.INVALID_DATA, - f"{exc.reason} at position {exc.start}", - ) - # fallthrough - - # Wait for the protocol state to be CLOSED before accessing close_exc. - await asyncio.shield(self.connection_lost_waiter) - raise self.protocol.close_exc from self.recv_exc - - async def send( - self, - message: Data | Iterable[Data] | AsyncIterable[Data], - text: bool | None = None, - ) -> None: - """ - Send a message. - - A string (:class:`str`) is sent as a Text_ frame. A bytestring or - bytes-like object (:class:`bytes`, :class:`bytearray`, or - :class:`memoryview`) is sent as a Binary_ frame. - - .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - .. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - You may override this behavior with the ``text`` argument: - - * Set ``text=True`` to send a bytestring or bytes-like object - (:class:`bytes`, :class:`bytearray`, or :class:`memoryview`) as a - Text_ frame. This improves performance when the message is already - UTF-8 encoded, for example if the message contains JSON and you're - using a JSON library that produces a bytestring. - * Set ``text=False`` to send a string (:class:`str`) in a Binary_ - frame. This may be useful for servers that expect binary frames - instead of text frames. - - :meth:`send` also accepts an iterable or an asynchronous iterable of - strings, bytestrings, or bytes-like objects to enable fragmentation_. - Each item is treated as a message fragment and sent in its own frame. - All items must be of the same type, or else :meth:`send` will raise a - :exc:`TypeError` and the connection will be closed. - - .. _fragmentation: https://datatracker.ietf.org/doc/html/rfc6455#section-5.4 - - :meth:`send` rejects dict-like objects because this is often an error. - (If you really want to send the keys of a dict-like object as fragments, - call its :meth:`~dict.keys` method and pass the result to :meth:`send`.) - - Canceling :meth:`send` is discouraged. Instead, you should close the - connection with :meth:`close`. Indeed, there are only two situations - where :meth:`send` may yield control to the event loop and then get - canceled; in both cases, :meth:`close` has the same effect and is - more clear: - - 1. The write buffer is full. If you don't want to wait until enough - data is sent, your only alternative is to close the connection. - :meth:`close` will likely time out then abort the TCP connection. - 2. ``message`` is an asynchronous iterator that yields control. - Stopping in the middle of a fragmented message will cause a - protocol error and the connection will be closed. - - When the connection is closed, :meth:`send` raises - :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it - raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal - connection closure and - :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol - error or a network failure. - - Args: - message: Message to send. - - Raises: - ConnectionClosed: When the connection is closed. - TypeError: If ``message`` doesn't have a supported type. - - """ - # While sending a fragmented message, prevent sending other messages - # until all fragments are sent. - while self.fragmented_send_waiter is not None: - await asyncio.shield(self.fragmented_send_waiter) - - # Unfragmented message -- this case must be handled first because - # strings and bytes-like objects are iterable. - - if isinstance(message, str): - async with self.send_context(): - if text is False: - self.protocol.send_binary(message.encode()) - else: - self.protocol.send_text(message.encode()) - - elif isinstance(message, BytesLike): - async with self.send_context(): - if text is True: - self.protocol.send_text(message) - else: - self.protocol.send_binary(message) - - # Catch a common mistake -- passing a dict to send(). - - elif isinstance(message, Mapping): - raise TypeError("data is a dict-like object") - - # Fragmented message -- regular iterator. - - elif isinstance(message, Iterable): - chunks = iter(message) - try: - chunk = next(chunks) - except StopIteration: - return - - assert self.fragmented_send_waiter is None - self.fragmented_send_waiter = self.loop.create_future() - try: - # First fragment. - if isinstance(chunk, str): - async with self.send_context(): - if text is False: - self.protocol.send_binary(chunk.encode(), fin=False) - else: - self.protocol.send_text(chunk.encode(), fin=False) - encode = True - elif isinstance(chunk, BytesLike): - async with self.send_context(): - if text is True: - self.protocol.send_text(chunk, fin=False) - else: - self.protocol.send_binary(chunk, fin=False) - encode = False - else: - raise TypeError("iterable must contain bytes or str") - - # Other fragments - for chunk in chunks: - if isinstance(chunk, str) and encode: - async with self.send_context(): - self.protocol.send_continuation(chunk.encode(), fin=False) - elif isinstance(chunk, BytesLike) and not encode: - async with self.send_context(): - self.protocol.send_continuation(chunk, fin=False) - else: - raise TypeError("iterable must contain uniform types") - - # Final fragment. - async with self.send_context(): - self.protocol.send_continuation(b"", fin=True) - - except Exception: - # We're half-way through a fragmented message and we can't - # complete it. This makes the connection unusable. - async with self.send_context(): - self.protocol.fail( - CloseCode.INTERNAL_ERROR, - "error in fragmented message", - ) - raise - - finally: - self.fragmented_send_waiter.set_result(None) - self.fragmented_send_waiter = None - - # Fragmented message -- async iterator. - - elif isinstance(message, AsyncIterable): - achunks = aiter(message) - try: - chunk = await anext(achunks) - except StopAsyncIteration: - return - - assert self.fragmented_send_waiter is None - self.fragmented_send_waiter = self.loop.create_future() - try: - # First fragment. - if isinstance(chunk, str): - if text is False: - async with self.send_context(): - self.protocol.send_binary(chunk.encode(), fin=False) - else: - async with self.send_context(): - self.protocol.send_text(chunk.encode(), fin=False) - encode = True - elif isinstance(chunk, BytesLike): - if text is True: - async with self.send_context(): - self.protocol.send_text(chunk, fin=False) - else: - async with self.send_context(): - self.protocol.send_binary(chunk, fin=False) - encode = False - else: - raise TypeError("async iterable must contain bytes or str") - - # Other fragments - async for chunk in achunks: - if isinstance(chunk, str) and encode: - async with self.send_context(): - self.protocol.send_continuation(chunk.encode(), fin=False) - elif isinstance(chunk, BytesLike) and not encode: - async with self.send_context(): - self.protocol.send_continuation(chunk, fin=False) - else: - raise TypeError("async iterable must contain uniform types") - - # Final fragment. - async with self.send_context(): - self.protocol.send_continuation(b"", fin=True) - - except Exception: - # We're half-way through a fragmented message and we can't - # complete it. This makes the connection unusable. - async with self.send_context(): - self.protocol.fail( - CloseCode.INTERNAL_ERROR, - "error in fragmented message", - ) - raise - - finally: - self.fragmented_send_waiter.set_result(None) - self.fragmented_send_waiter = None - - else: - raise TypeError("data must be str, bytes, iterable, or async iterable") - - async def close(self, code: int = 1000, reason: str = "") -> None: - """ - Perform the closing handshake. - - :meth:`close` waits for the other end to complete the handshake and - for the TCP connection to terminate. - - :meth:`close` is idempotent: it doesn't do anything once the - connection is closed. - - Args: - code: WebSocket close code. - reason: WebSocket close reason. - - """ - try: - # The context manager takes care of waiting for the TCP connection - # to terminate after calling a method that sends a close frame. - async with self.send_context(): - if self.fragmented_send_waiter is not None: - self.protocol.fail( - CloseCode.INTERNAL_ERROR, - "close during fragmented message", - ) - else: - self.protocol.send_close(code, reason) - except ConnectionClosed: - # Ignore ConnectionClosed exceptions raised from send_context(). - # They mean that the connection is closed, which was the goal. - pass - - async def wait_closed(self) -> None: - """ - Wait until the connection is closed. - - :meth:`wait_closed` waits for the closing handshake to complete and for - the TCP connection to terminate. - - """ - await asyncio.shield(self.connection_lost_waiter) - - async def ping(self, data: Data | None = None) -> Awaitable[float]: - """ - Send a Ping_. - - .. _Ping: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2 - - A ping may serve as a keepalive or as a check that the remote endpoint - received all messages up to this point - - Args: - data: Payload of the ping. A :class:`str` will be encoded to UTF-8. - If ``data`` is :obj:`None`, the payload is four random bytes. - - Returns: - A future that will be completed when the corresponding pong is - received. You can ignore it if you don't intend to wait. The result - of the future is the latency of the connection in seconds. - - :: - - pong_waiter = await ws.ping() - # only if you want to wait for the corresponding pong - latency = await pong_waiter - - Raises: - ConnectionClosed: When the connection is closed. - ConcurrencyError: If another ping was sent with the same data and - the corresponding pong wasn't received yet. - - """ - if isinstance(data, BytesLike): - data = bytes(data) - elif isinstance(data, str): - data = data.encode() - elif data is not None: - raise TypeError("data must be str or bytes-like") - - async with self.send_context(): - # Protect against duplicates if a payload is explicitly set. - if data in self.pong_waiters: - raise ConcurrencyError("already waiting for a pong with the same data") - - # Generate a unique random payload otherwise. - while data is None or data in self.pong_waiters: - data = struct.pack("!I", random.getrandbits(32)) - - pong_waiter = self.loop.create_future() - # The event loop's default clock is time.monotonic(). Its resolution - # is a bit low on Windows (~16ms). This is improved in Python 3.13. - self.pong_waiters[data] = (pong_waiter, self.loop.time()) - self.protocol.send_ping(data) - return pong_waiter - - async def pong(self, data: Data = b"") -> None: - """ - Send a Pong_. - - .. _Pong: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.3 - - An unsolicited pong may serve as a unidirectional heartbeat. - - Args: - data: Payload of the pong. A :class:`str` will be encoded to UTF-8. - - Raises: - ConnectionClosed: When the connection is closed. - - """ - if isinstance(data, BytesLike): - data = bytes(data) - elif isinstance(data, str): - data = data.encode() - else: - raise TypeError("data must be str or bytes-like") - - async with self.send_context(): - self.protocol.send_pong(data) - - # Private methods - - def process_event(self, event: Event) -> None: - """ - Process one incoming event. - - This method is overridden in subclasses to handle the handshake. - - """ - assert isinstance(event, Frame) - if event.opcode in DATA_OPCODES: - self.recv_messages.put(event) - - if event.opcode is Opcode.PONG: - self.acknowledge_pings(bytes(event.data)) - - def acknowledge_pings(self, data: bytes) -> None: - """ - Acknowledge pings when receiving a pong. - - """ - # Ignore unsolicited pong. - if data not in self.pong_waiters: - return - - pong_timestamp = self.loop.time() - - # Sending a pong for only the most recent ping is legal. - # Acknowledge all previous pings too in that case. - ping_id = None - ping_ids = [] - for ping_id, (pong_waiter, ping_timestamp) in self.pong_waiters.items(): - ping_ids.append(ping_id) - latency = pong_timestamp - ping_timestamp - if not pong_waiter.done(): - pong_waiter.set_result(latency) - if ping_id == data: - self.latency = latency - break - else: - raise AssertionError("solicited pong not found in pings") - - # Remove acknowledged pings from self.pong_waiters. - for ping_id in ping_ids: - del self.pong_waiters[ping_id] - - def abort_pings(self) -> None: - """ - Raise ConnectionClosed in pending pings. - - They'll never receive a pong once the connection is closed. - - """ - assert self.protocol.state is CLOSED - exc = self.protocol.close_exc - - for pong_waiter, _ping_timestamp in self.pong_waiters.values(): - if not pong_waiter.done(): - pong_waiter.set_exception(exc) - # If the exception is never retrieved, it will be logged when ping - # is garbage-collected. This is confusing for users. - # Given that ping is done (with an exception), canceling it does - # nothing, but it prevents logging the exception. - pong_waiter.cancel() - - self.pong_waiters.clear() - - async def keepalive(self) -> None: - """ - Send a Ping frame and wait for a Pong frame at regular intervals. - - """ - assert self.ping_interval is not None - latency = 0.0 - try: - while True: - # If self.ping_timeout > latency > self.ping_interval, - # pings will be sent immediately after receiving pongs. - # The period will be longer than self.ping_interval. - await asyncio.sleep(self.ping_interval - latency) - - # This cannot raise ConnectionClosed when the connection is - # closing because ping(), via send_context(), waits for the - # connection to be closed before raising ConnectionClosed. - # However, connection_lost() cancels keepalive_task before - # it gets a chance to resume excuting. - pong_waiter = await self.ping() - if self.debug: - self.logger.debug("% sent keepalive ping") - - if self.ping_timeout is not None: - try: - async with asyncio_timeout(self.ping_timeout): - # connection_lost cancels keepalive immediately - # after setting a ConnectionClosed exception on - # pong_waiter. A CancelledError is raised here, - # not a ConnectionClosed exception. - latency = await pong_waiter - self.logger.debug("% received keepalive pong") - except asyncio.TimeoutError: - if self.debug: - self.logger.debug("- timed out waiting for keepalive pong") - async with self.send_context(): - self.protocol.fail( - CloseCode.INTERNAL_ERROR, - "keepalive ping timeout", - ) - raise AssertionError( - "send_context() should wait for connection_lost(), " - "which cancels keepalive()" - ) - except Exception: - self.logger.error("keepalive ping failed", exc_info=True) - - def start_keepalive(self) -> None: - """ - Run :meth:`keepalive` in a task, unless keepalive is disabled. - - """ - if self.ping_interval is not None: - self.keepalive_task = self.loop.create_task(self.keepalive()) - - @contextlib.asynccontextmanager - async def send_context( - self, - *, - expected_state: State = OPEN, # CONNECTING during the opening handshake - ) -> AsyncIterator[None]: - """ - Create a context for writing to the connection from user code. - - On entry, :meth:`send_context` checks that the connection is open; on - exit, it writes outgoing data to the socket:: - - async with self.send_context(): - self.protocol.send_text(message.encode()) - - When the connection isn't open on entry, when the connection is expected - to close on exit, or when an unexpected error happens, terminating the - connection, :meth:`send_context` waits until the connection is closed - then raises :exc:`~websockets.exceptions.ConnectionClosed`. - - """ - # Should we wait until the connection is closed? - wait_for_close = False - # Should we close the transport and raise ConnectionClosed? - raise_close_exc = False - # What exception should we chain ConnectionClosed to? - original_exc: BaseException | None = None - - if self.protocol.state is expected_state: - # Let the caller interact with the protocol. - try: - yield - except (ProtocolError, ConcurrencyError): - # The protocol state wasn't changed. Exit immediately. - raise - except Exception as exc: - self.logger.error("unexpected internal error", exc_info=True) - # This branch should never run. It's a safety net in case of - # bugs. Since we don't know what happened, we will close the - # connection and raise the exception to the caller. - wait_for_close = False - raise_close_exc = True - original_exc = exc - else: - # Check if the connection is expected to close soon. - if self.protocol.close_expected(): - wait_for_close = True - # If the connection is expected to close soon, set the - # close deadline based on the close timeout. - # Since we tested earlier that protocol.state was OPEN - # (or CONNECTING), self.close_deadline is still None. - if self.close_timeout is not None: - assert self.close_deadline is None - self.close_deadline = self.loop.time() + self.close_timeout - # Write outgoing data to the socket and enforce flow control. - try: - self.send_data() - await self.drain() - except Exception as exc: - if self.debug: - self.logger.debug("! error while sending data", exc_info=True) - # While the only expected exception here is OSError, - # other exceptions would be treated identically. - wait_for_close = False - raise_close_exc = True - original_exc = exc - - else: # self.protocol.state is not expected_state - # Minor layering violation: we assume that the connection - # will be closing soon if it isn't in the expected state. - wait_for_close = True - # Calculate close_deadline if it wasn't set yet. - if self.close_timeout is not None: - if self.close_deadline is None: - self.close_deadline = self.loop.time() + self.close_timeout - raise_close_exc = True - - # If the connection is expected to close soon and the close timeout - # elapses, close the socket to terminate the connection. - if wait_for_close: - try: - async with asyncio_timeout_at(self.close_deadline): - await asyncio.shield(self.connection_lost_waiter) - except TimeoutError: - # There's no risk to overwrite another error because - # original_exc is never set when wait_for_close is True. - assert original_exc is None - original_exc = TimeoutError("timed out while closing connection") - # Set recv_exc before closing the transport in order to get - # proper exception reporting. - raise_close_exc = True - self.set_recv_exc(original_exc) - - # If an error occurred, close the transport to terminate the connection and - # raise an exception. - if raise_close_exc: - self.transport.abort() - # Wait for the protocol state to be CLOSED before accessing close_exc. - await asyncio.shield(self.connection_lost_waiter) - raise self.protocol.close_exc from original_exc - - def send_data(self) -> None: - """ - Send outgoing data. - - Raises: - OSError: When a socket operations fails. - - """ - for data in self.protocol.data_to_send(): - if data: - self.transport.write(data) - else: - # Half-close the TCP connection when possible i.e. no TLS. - if self.transport.can_write_eof(): - if self.debug: - self.logger.debug("x half-closing TCP connection") - # write_eof() doesn't document which exceptions it raises. - # OSError is plausible. uvloop can raise RuntimeError here. - try: - self.transport.write_eof() - except (OSError, RuntimeError): # pragma: no cover - pass - # Else, close the TCP connection. - else: # pragma: no cover - if self.debug: - self.logger.debug("x closing TCP connection") - self.transport.close() - - def set_recv_exc(self, exc: BaseException | None) -> None: - """ - Set recv_exc, if not set yet. - - """ - if self.recv_exc is None: - self.recv_exc = exc - - # asyncio.Protocol methods - - # Connection callbacks - - def connection_made(self, transport: asyncio.BaseTransport) -> None: - transport = cast(asyncio.Transport, transport) - self.recv_messages = Assembler( - *self.max_queue, - pause=transport.pause_reading, - resume=transport.resume_reading, - ) - transport.set_write_buffer_limits(*self.write_limit) - self.transport = transport - - def connection_lost(self, exc: Exception | None) -> None: - # Calling protocol.receive_eof() is safe because it's idempotent. - # This guarantees that the protocol state becomes CLOSED. - self.protocol.receive_eof() - assert self.protocol.state is CLOSED - - self.set_recv_exc(exc) - - # Abort recv() and pending pings with a ConnectionClosed exception. - self.recv_messages.close() - self.abort_pings() - - if self.keepalive_task is not None: - self.keepalive_task.cancel() - - # If self.connection_lost_waiter isn't pending, that's a bug, because: - # - it's set only here in connection_lost() which is called only once; - # - it must never be canceled. - self.connection_lost_waiter.set_result(None) - - # Adapted from asyncio.streams.FlowControlMixin - if self.paused: # pragma: no cover - self.paused = False - for waiter in self.drain_waiters: - if not waiter.done(): - if exc is None: - waiter.set_result(None) - else: - waiter.set_exception(exc) - - # Flow control callbacks - - def pause_writing(self) -> None: # pragma: no cover - # Adapted from asyncio.streams.FlowControlMixin - assert not self.paused - self.paused = True - - def resume_writing(self) -> None: # pragma: no cover - # Adapted from asyncio.streams.FlowControlMixin - assert self.paused - self.paused = False - for waiter in self.drain_waiters: - if not waiter.done(): - waiter.set_result(None) - - async def drain(self) -> None: # pragma: no cover - # We don't check if the connection is closed because we call drain() - # immediately after write() and write() would fail in that case. - - # Adapted from asyncio.streams.StreamWriter - # Yield to the event loop so that connection_lost() may be called. - if self.transport.is_closing(): - await asyncio.sleep(0) - - # Adapted from asyncio.streams.FlowControlMixin - if self.paused: - waiter = self.loop.create_future() - self.drain_waiters.append(waiter) - try: - await waiter - finally: - self.drain_waiters.remove(waiter) - - # Streaming protocol callbacks - - def data_received(self, data: bytes) -> None: - # Feed incoming data to the protocol. - self.protocol.receive_data(data) - - # This isn't expected to raise an exception. - events = self.protocol.events_received() - - # Write outgoing data to the transport. - try: - self.send_data() - except Exception as exc: - if self.debug: - self.logger.debug("! error while sending data", exc_info=True) - self.set_recv_exc(exc) - - if self.protocol.close_expected(): - # If the connection is expected to close soon, set the - # close deadline based on the close timeout. - if self.close_timeout is not None: - if self.close_deadline is None: - self.close_deadline = self.loop.time() + self.close_timeout - - for event in events: - # This isn't expected to raise an exception. - self.process_event(event) - - def eof_received(self) -> None: - # Feed the end of the data stream to the connection. - self.protocol.receive_eof() - - # This isn't expected to raise an exception. - events = self.protocol.events_received() - - # There is no error handling because send_data() can only write - # the end of the data stream here and it shouldn't raise errors. - self.send_data() - - # This code path is triggered when receiving an HTTP response - # without a Content-Length header. This is the only case where - # reading until EOF generates an event; all other events have - # a known length. Ignore for coverage measurement because tests - # are in test_client.py rather than test_connection.py. - for event in events: # pragma: no cover - # This isn't expected to raise an exception. - self.process_event(event) - - # The WebSocket protocol has its own closing handshake: endpoints close - # the TCP or TLS connection after sending and receiving a close frame. - # As a consequence, they never need to write after receiving EOF, so - # there's no reason to keep the transport open by returning True. - # Besides, that doesn't work on TLS connections. - - -# broadcast() is defined in the connection module even though it's primarily -# used by servers and documented in the server module because it works with -# client connections too and because it's easier to test together with the -# Connection class. - - -def broadcast( - connections: Iterable[Connection], - message: Data, - raise_exceptions: bool = False, -) -> None: - """ - Broadcast a message to several WebSocket connections. - - A string (:class:`str`) is sent as a Text_ frame. A bytestring or bytes-like - object (:class:`bytes`, :class:`bytearray`, or :class:`memoryview`) is sent - as a Binary_ frame. - - .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - .. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - :func:`broadcast` pushes the message synchronously to all connections even - if their write buffers are overflowing. There's no backpressure. - - If you broadcast messages faster than a connection can handle them, messages - will pile up in its write buffer until the connection times out. Keep - ``ping_interval`` and ``ping_timeout`` low to prevent excessive memory usage - from slow connections. - - Unlike :meth:`~websockets.asyncio.connection.Connection.send`, - :func:`broadcast` doesn't support sending fragmented messages. Indeed, - fragmentation is useful for sending large messages without buffering them in - memory, while :func:`broadcast` buffers one copy per connection as fast as - possible. - - :func:`broadcast` skips connections that aren't open in order to avoid - errors on connections where the closing handshake is in progress. - - :func:`broadcast` ignores failures to write the message on some connections. - It continues writing to other connections. On Python 3.11 and above, you may - set ``raise_exceptions`` to :obj:`True` to record failures and raise all - exceptions in a :pep:`654` :exc:`ExceptionGroup`. - - While :func:`broadcast` makes more sense for servers, it works identically - with clients, if you have a use case for opening connections to many servers - and broadcasting a message to them. - - Args: - websockets: WebSocket connections to which the message will be sent. - message: Message to send. - raise_exceptions: Whether to raise an exception in case of failures. - - Raises: - TypeError: If ``message`` doesn't have a supported type. - - """ - if isinstance(message, str): - send_method = "send_text" - message = message.encode() - elif isinstance(message, BytesLike): - send_method = "send_binary" - else: - raise TypeError("data must be str or bytes") - - if raise_exceptions: - if sys.version_info[:2] < (3, 11): # pragma: no cover - raise ValueError("raise_exceptions requires at least Python 3.11") - exceptions: list[Exception] = [] - - for connection in connections: - exception: Exception - - if connection.protocol.state is not OPEN: - continue - - if connection.fragmented_send_waiter is not None: - if raise_exceptions: - exception = ConcurrencyError("sending a fragmented message") - exceptions.append(exception) - else: - connection.logger.warning( - "skipped broadcast: sending a fragmented message", - ) - continue - - try: - # Call connection.protocol.send_text or send_binary. - # Either way, message is already converted to bytes. - getattr(connection.protocol, send_method)(message) - connection.send_data() - except Exception as write_exception: - if raise_exceptions: - exception = RuntimeError("failed to write message") - exception.__cause__ = write_exception - exceptions.append(exception) - else: - connection.logger.warning( - "skipped broadcast: failed to write message: %s", - traceback.format_exception_only( - # Remove first argument when dropping Python 3.9. - type(write_exception), - write_exception, - )[0].strip(), - ) - - if raise_exceptions and exceptions: - raise ExceptionGroup("skipped broadcast", exceptions) - - -# Pretend that broadcast is actually defined in the server module. -broadcast.__module__ = "websockets.asyncio.server" diff --git a/wip/SNIP/client/websockets/asyncio/messages.py b/wip/SNIP/client/websockets/asyncio/messages.py deleted file mode 100644 index 1fd4181..0000000 --- a/wip/SNIP/client/websockets/asyncio/messages.py +++ /dev/null @@ -1,314 +0,0 @@ -from __future__ import annotations - -import asyncio -import codecs -import collections -from collections.abc import AsyncIterator, Iterable -from typing import Any, Callable, Generic, Literal, TypeVar, overload - -from ..exceptions import ConcurrencyError -from ..frames import OP_BINARY, OP_CONT, OP_TEXT, Frame -from ..typing import Data - - -__all__ = ["Assembler"] - -UTF8Decoder = codecs.getincrementaldecoder("utf-8") - -T = TypeVar("T") - - -class SimpleQueue(Generic[T]): - """ - Simplified version of :class:`asyncio.Queue`. - - Provides only the subset of functionality needed by :class:`Assembler`. - - """ - - def __init__(self) -> None: - self.loop = asyncio.get_running_loop() - self.get_waiter: asyncio.Future[None] | None = None - self.queue: collections.deque[T] = collections.deque() - - def __len__(self) -> int: - return len(self.queue) - - def put(self, item: T) -> None: - """Put an item into the queue without waiting.""" - self.queue.append(item) - if self.get_waiter is not None and not self.get_waiter.done(): - self.get_waiter.set_result(None) - - async def get(self, block: bool = True) -> T: - """Remove and return an item from the queue, waiting if necessary.""" - if not self.queue: - if not block: - raise EOFError("stream of frames ended") - assert self.get_waiter is None, "cannot call get() concurrently" - self.get_waiter = self.loop.create_future() - try: - await self.get_waiter - finally: - self.get_waiter.cancel() - self.get_waiter = None - return self.queue.popleft() - - def reset(self, items: Iterable[T]) -> None: - """Put back items into an empty, idle queue.""" - assert self.get_waiter is None, "cannot reset() while get() is running" - assert not self.queue, "cannot reset() while queue isn't empty" - self.queue.extend(items) - - def abort(self) -> None: - """Close the queue, raising EOFError in get() if necessary.""" - if self.get_waiter is not None and not self.get_waiter.done(): - self.get_waiter.set_exception(EOFError("stream of frames ended")) - - -class Assembler: - """ - Assemble messages from frames. - - :class:`Assembler` expects only data frames. The stream of frames must - respect the protocol; if it doesn't, the behavior is undefined. - - Args: - pause: Called when the buffer of frames goes above the high water mark; - should pause reading from the network. - resume: Called when the buffer of frames goes below the low water mark; - should resume reading from the network. - - """ - - # coverage reports incorrectly: "line NN didn't jump to the function exit" - def __init__( # pragma: no cover - self, - high: int | None = None, - low: int | None = None, - pause: Callable[[], Any] = lambda: None, - resume: Callable[[], Any] = lambda: None, - ) -> None: - # Queue of incoming frames. - self.frames: SimpleQueue[Frame] = SimpleQueue() - - # We cannot put a hard limit on the size of the queue because a single - # call to Protocol.data_received() could produce thousands of frames, - # which must be buffered. Instead, we pause reading when the buffer goes - # above the high limit and we resume when it goes under the low limit. - if high is not None and low is None: - low = high // 4 - if high is None and low is not None: - high = low * 4 - if high is not None and low is not None: - if low < 0: - raise ValueError("low must be positive or equal to zero") - if high < low: - raise ValueError("high must be greater than or equal to low") - self.high, self.low = high, low - self.pause = pause - self.resume = resume - self.paused = False - - # This flag prevents concurrent calls to get() by user code. - self.get_in_progress = False - - # This flag marks the end of the connection. - self.closed = False - - @overload - async def get(self, decode: Literal[True]) -> str: ... - - @overload - async def get(self, decode: Literal[False]) -> bytes: ... - - @overload - async def get(self, decode: bool | None = None) -> Data: ... - - async def get(self, decode: bool | None = None) -> Data: - """ - Read the next message. - - :meth:`get` returns a single :class:`str` or :class:`bytes`. - - If the message is fragmented, :meth:`get` waits until the last frame is - received, then it reassembles the message and returns it. To receive - messages frame by frame, use :meth:`get_iter` instead. - - Args: - decode: :obj:`False` disables UTF-8 decoding of text frames and - returns :class:`bytes`. :obj:`True` forces UTF-8 decoding of - binary frames and returns :class:`str`. - - Raises: - EOFError: If the stream of frames has ended. - UnicodeDecodeError: If a text frame contains invalid UTF-8. - ConcurrencyError: If two coroutines run :meth:`get` or - :meth:`get_iter` concurrently. - - """ - if self.get_in_progress: - raise ConcurrencyError("get() or get_iter() is already running") - self.get_in_progress = True - - # Locking with get_in_progress prevents concurrent execution - # until get() fetches a complete message or is canceled. - - try: - # First frame - frame = await self.frames.get(not self.closed) - self.maybe_resume() - assert frame.opcode is OP_TEXT or frame.opcode is OP_BINARY - if decode is None: - decode = frame.opcode is OP_TEXT - frames = [frame] - - # Following frames, for fragmented messages - while not frame.fin: - try: - frame = await self.frames.get(not self.closed) - except asyncio.CancelledError: - # Put frames already received back into the queue - # so that future calls to get() can return them. - self.frames.reset(frames) - raise - self.maybe_resume() - assert frame.opcode is OP_CONT - frames.append(frame) - - finally: - self.get_in_progress = False - - data = b"".join(frame.data for frame in frames) - if decode: - return data.decode() - else: - return data - - @overload - def get_iter(self, decode: Literal[True]) -> AsyncIterator[str]: ... - - @overload - def get_iter(self, decode: Literal[False]) -> AsyncIterator[bytes]: ... - - @overload - def get_iter(self, decode: bool | None = None) -> AsyncIterator[Data]: ... - - async def get_iter(self, decode: bool | None = None) -> AsyncIterator[Data]: - """ - Stream the next message. - - Iterating the return value of :meth:`get_iter` asynchronously yields a - :class:`str` or :class:`bytes` for each frame in the message. - - The iterator must be fully consumed before calling :meth:`get_iter` or - :meth:`get` again. Else, :exc:`ConcurrencyError` is raised. - - This method only makes sense for fragmented messages. If messages aren't - fragmented, use :meth:`get` instead. - - Args: - decode: :obj:`False` disables UTF-8 decoding of text frames and - returns :class:`bytes`. :obj:`True` forces UTF-8 decoding of - binary frames and returns :class:`str`. - - Raises: - EOFError: If the stream of frames has ended. - UnicodeDecodeError: If a text frame contains invalid UTF-8. - ConcurrencyError: If two coroutines run :meth:`get` or - :meth:`get_iter` concurrently. - - """ - if self.get_in_progress: - raise ConcurrencyError("get() or get_iter() is already running") - self.get_in_progress = True - - # Locking with get_in_progress prevents concurrent execution - # until get_iter() fetches a complete message or is canceled. - - # If get_iter() raises an exception e.g. in decoder.decode(), - # get_in_progress remains set and the connection becomes unusable. - - # First frame - try: - frame = await self.frames.get(not self.closed) - except asyncio.CancelledError: - self.get_in_progress = False - raise - self.maybe_resume() - assert frame.opcode is OP_TEXT or frame.opcode is OP_BINARY - if decode is None: - decode = frame.opcode is OP_TEXT - if decode: - decoder = UTF8Decoder() - yield decoder.decode(frame.data, frame.fin) - else: - yield frame.data - - # Following frames, for fragmented messages - while not frame.fin: - # We cannot handle asyncio.CancelledError because we don't buffer - # previous fragments — we're streaming them. Canceling get_iter() - # here will leave the assembler in a stuck state. Future calls to - # get() or get_iter() will raise ConcurrencyError. - frame = await self.frames.get(not self.closed) - self.maybe_resume() - assert frame.opcode is OP_CONT - if decode: - yield decoder.decode(frame.data, frame.fin) - else: - yield frame.data - - self.get_in_progress = False - - def put(self, frame: Frame) -> None: - """ - Add ``frame`` to the next message. - - Raises: - EOFError: If the stream of frames has ended. - - """ - if self.closed: - raise EOFError("stream of frames ended") - - self.frames.put(frame) - self.maybe_pause() - - def maybe_pause(self) -> None: - """Pause the writer if queue is above the high water mark.""" - # Skip if flow control is disabled - if self.high is None: - return - - # Check for "> high" to support high = 0 - if len(self.frames) > self.high and not self.paused: - self.paused = True - self.pause() - - def maybe_resume(self) -> None: - """Resume the writer if queue is below the low water mark.""" - # Skip if flow control is disabled - if self.low is None: - return - - # Check for "<= low" to support low = 0 - if len(self.frames) <= self.low and self.paused: - self.paused = False - self.resume() - - def close(self) -> None: - """ - End the stream of frames. - - Calling :meth:`close` concurrently with :meth:`get`, :meth:`get_iter`, - or :meth:`put` is safe. They will raise :exc:`EOFError`. - - """ - if self.closed: - return - - self.closed = True - - # Unblock get() or get_iter(). - self.frames.abort() diff --git a/wip/SNIP/client/websockets/asyncio/router.py b/wip/SNIP/client/websockets/asyncio/router.py deleted file mode 100644 index 047e7ef..0000000 --- a/wip/SNIP/client/websockets/asyncio/router.py +++ /dev/null @@ -1,198 +0,0 @@ -from __future__ import annotations - -import http -import ssl as ssl_module -import urllib.parse -from typing import Any, Awaitable, Callable, Literal - -from werkzeug.exceptions import NotFound -from werkzeug.routing import Map, RequestRedirect - -from ..http11 import Request, Response -from .server import Server, ServerConnection, serve - - -__all__ = ["route", "unix_route", "Router"] - - -class Router: - """WebSocket router supporting :func:`route`.""" - - def __init__( - self, - url_map: Map, - server_name: str | None = None, - url_scheme: str = "ws", - ) -> None: - self.url_map = url_map - self.server_name = server_name - self.url_scheme = url_scheme - for rule in self.url_map.iter_rules(): - rule.websocket = True - - def get_server_name(self, connection: ServerConnection, request: Request) -> str: - if self.server_name is None: - return request.headers["Host"] - else: - return self.server_name - - def redirect(self, connection: ServerConnection, url: str) -> Response: - response = connection.respond(http.HTTPStatus.FOUND, f"Found at {url}") - response.headers["Location"] = url - return response - - def not_found(self, connection: ServerConnection) -> Response: - return connection.respond(http.HTTPStatus.NOT_FOUND, "Not Found") - - def route_request( - self, connection: ServerConnection, request: Request - ) -> Response | None: - """Route incoming request.""" - url_map_adapter = self.url_map.bind( - server_name=self.get_server_name(connection, request), - url_scheme=self.url_scheme, - ) - try: - parsed = urllib.parse.urlparse(request.path) - handler, kwargs = url_map_adapter.match( - path_info=parsed.path, - query_args=parsed.query, - ) - except RequestRedirect as redirect: - return self.redirect(connection, redirect.new_url) - except NotFound: - return self.not_found(connection) - connection.handler, connection.handler_kwargs = handler, kwargs - return None - - async def handler(self, connection: ServerConnection) -> None: - """Handle a connection.""" - return await connection.handler(connection, **connection.handler_kwargs) - - -def route( - url_map: Map, - *args: Any, - server_name: str | None = None, - ssl: ssl_module.SSLContext | Literal[True] | None = None, - create_router: type[Router] | None = None, - **kwargs: Any, -) -> Awaitable[Server]: - """ - Create a WebSocket server dispatching connections to different handlers. - - This feature requires the third-party library `werkzeug`_: - - .. code-block:: console - - $ pip install werkzeug - - .. _werkzeug: https://werkzeug.palletsprojects.com/ - - :func:`route` accepts the same arguments as - :func:`~websockets.sync.server.serve`, except as described below. - - The first argument is a :class:`werkzeug.routing.Map` that maps URL patterns - to connection handlers. In addition to the connection, handlers receive - parameters captured in the URL as keyword arguments. - - Here's an example:: - - - from websockets.asyncio.router import route - from werkzeug.routing import Map, Rule - - async def channel_handler(websocket, channel_id): - ... - - url_map = Map([ - Rule("/channel/", endpoint=channel_handler), - ... - ]) - - # set this future to exit the server - stop = asyncio.get_running_loop().create_future() - - async with route(url_map, ...) as server: - await stop - - - Refer to the documentation of :mod:`werkzeug.routing` for details. - - If you define redirects with ``Rule(..., redirect_to=...)`` in the URL map, - when the server runs behind a reverse proxy that modifies the ``Host`` - header or terminates TLS, you need additional configuration: - - * Set ``server_name`` to the name of the server as seen by clients. When not - provided, websockets uses the value of the ``Host`` header. - - * Set ``ssl=True`` to generate ``wss://`` URIs without actually enabling - TLS. Under the hood, this bind the URL map with a ``url_scheme`` of - ``wss://`` instead of ``ws://``. - - There is no need to specify ``websocket=True`` in each rule. It is added - automatically. - - Args: - url_map: Mapping of URL patterns to connection handlers. - server_name: Name of the server as seen by clients. If :obj:`None`, - websockets uses the value of the ``Host`` header. - ssl: Configuration for enabling TLS on the connection. Set it to - :obj:`True` if a reverse proxy terminates TLS connections. - create_router: Factory for the :class:`Router` dispatching requests to - handlers. Set it to a wrapper or a subclass to customize routing. - - """ - url_scheme = "ws" if ssl is None else "wss" - if ssl is not True and ssl is not None: - kwargs["ssl"] = ssl - - if create_router is None: - create_router = Router - - router = create_router(url_map, server_name, url_scheme) - - _process_request: ( - Callable[ - [ServerConnection, Request], - Awaitable[Response | None] | Response | None, - ] - | None - ) = kwargs.pop("process_request", None) - if _process_request is None: - process_request: Callable[ - [ServerConnection, Request], - Awaitable[Response | None] | Response | None, - ] = router.route_request - else: - - async def process_request( - connection: ServerConnection, request: Request - ) -> Response | None: - response = _process_request(connection, request) - if isinstance(response, Awaitable): - response = await response - if response is not None: - return response - return router.route_request(connection, request) - - return serve(router.handler, *args, process_request=process_request, **kwargs) - - -def unix_route( - url_map: Map, - path: str | None = None, - **kwargs: Any, -) -> Awaitable[Server]: - """ - Create a WebSocket Unix server dispatching connections to different handlers. - - :func:`unix_route` combines the behaviors of :func:`route` and - :func:`~websockets.asyncio.server.unix_serve`. - - Args: - url_map: Mapping of URL patterns to connection handlers. - path: File system path to the Unix socket. - - """ - return route(url_map, unix=True, path=path, **kwargs) diff --git a/wip/SNIP/client/websockets/asyncio/server.py b/wip/SNIP/client/websockets/asyncio/server.py deleted file mode 100644 index ec7fc43..0000000 --- a/wip/SNIP/client/websockets/asyncio/server.py +++ /dev/null @@ -1,981 +0,0 @@ -from __future__ import annotations - -import asyncio -import hmac -import http -import logging -import re -import socket -import sys -from collections.abc import Awaitable, Generator, Iterable, Sequence -from types import TracebackType -from typing import Any, Callable, Mapping, cast - -from ..exceptions import InvalidHeader -from ..extensions.base import ServerExtensionFactory -from ..extensions.permessage_deflate import enable_server_permessage_deflate -from ..frames import CloseCode -from ..headers import ( - build_www_authenticate_basic, - parse_authorization_basic, - validate_subprotocols, -) -from ..http11 import SERVER, Request, Response -from ..protocol import CONNECTING, OPEN, Event -from ..server import ServerProtocol -from ..typing import LoggerLike, Origin, StatusLike, Subprotocol -from .compatibility import asyncio_timeout -from .connection import Connection, broadcast - - -__all__ = [ - "broadcast", - "serve", - "unix_serve", - "ServerConnection", - "Server", - "basic_auth", -] - - -class ServerConnection(Connection): - """ - :mod:`asyncio` implementation of a WebSocket server connection. - - :class:`ServerConnection` provides :meth:`recv` and :meth:`send` methods for - receiving and sending messages. - - It supports asynchronous iteration to receive messages:: - - async for message in websocket: - await process(message) - - The iterator exits normally when the connection is closed with close code - 1000 (OK) or 1001 (going away) or without a close code. It raises a - :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is - closed with any other code. - - The ``ping_interval``, ``ping_timeout``, ``close_timeout``, ``max_queue``, - and ``write_limit`` arguments have the same meaning as in :func:`serve`. - - Args: - protocol: Sans-I/O connection. - server: Server that manages this connection. - - """ - - def __init__( - self, - protocol: ServerProtocol, - server: Server, - *, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = 10, - max_queue: int | None | tuple[int | None, int | None] = 16, - write_limit: int | tuple[int, int | None] = 2**15, - ) -> None: - self.protocol: ServerProtocol - super().__init__( - protocol, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - close_timeout=close_timeout, - max_queue=max_queue, - write_limit=write_limit, - ) - self.server = server - self.request_rcvd: asyncio.Future[None] = self.loop.create_future() - self.username: str # see basic_auth() - self.handler: Callable[[ServerConnection], Awaitable[None]] # see route() - self.handler_kwargs: Mapping[str, Any] # see route() - - def respond(self, status: StatusLike, text: str) -> Response: - """ - Create a plain text HTTP response. - - ``process_request`` and ``process_response`` may call this method to - return an HTTP response instead of performing the WebSocket opening - handshake. - - You can modify the response before returning it, for example by changing - HTTP headers. - - Args: - status: HTTP status code. - text: HTTP response body; it will be encoded to UTF-8. - - Returns: - HTTP response to send to the client. - - """ - return self.protocol.reject(status, text) - - async def handshake( - self, - process_request: ( - Callable[ - [ServerConnection, Request], - Awaitable[Response | None] | Response | None, - ] - | None - ) = None, - process_response: ( - Callable[ - [ServerConnection, Request, Response], - Awaitable[Response | None] | Response | None, - ] - | None - ) = None, - server_header: str | None = SERVER, - ) -> None: - """ - Perform the opening handshake. - - """ - await asyncio.wait( - [self.request_rcvd, self.connection_lost_waiter], - return_when=asyncio.FIRST_COMPLETED, - ) - - if self.request is not None: - async with self.send_context(expected_state=CONNECTING): - response = None - - if process_request is not None: - try: - response = process_request(self, self.request) - if isinstance(response, Awaitable): - response = await response - except Exception as exc: - self.protocol.handshake_exc = exc - response = self.protocol.reject( - http.HTTPStatus.INTERNAL_SERVER_ERROR, - ( - "Failed to open a WebSocket connection.\n" - "See server log for more information.\n" - ), - ) - - if response is None: - if self.server.is_serving(): - self.response = self.protocol.accept(self.request) - else: - self.response = self.protocol.reject( - http.HTTPStatus.SERVICE_UNAVAILABLE, - "Server is shutting down.\n", - ) - else: - assert isinstance(response, Response) # help mypy - self.response = response - - if server_header: - self.response.headers["Server"] = server_header - - response = None - - if process_response is not None: - try: - response = process_response(self, self.request, self.response) - if isinstance(response, Awaitable): - response = await response - except Exception as exc: - self.protocol.handshake_exc = exc - response = self.protocol.reject( - http.HTTPStatus.INTERNAL_SERVER_ERROR, - ( - "Failed to open a WebSocket connection.\n" - "See server log for more information.\n" - ), - ) - - if response is not None: - assert isinstance(response, Response) # help mypy - self.response = response - - self.protocol.send_response(self.response) - - # self.protocol.handshake_exc is set when the connection is lost before - # receiving a request, when the request cannot be parsed, or when the - # handshake fails, including when process_request or process_response - # raises an exception. - - # It isn't set when process_request or process_response sends an HTTP - # response that rejects the handshake. - - if self.protocol.handshake_exc is not None: - raise self.protocol.handshake_exc - - def process_event(self, event: Event) -> None: - """ - Process one incoming event. - - """ - # First event - handshake request. - if self.request is None: - assert isinstance(event, Request) - self.request = event - self.request_rcvd.set_result(None) - # Later events - frames. - else: - super().process_event(event) - - def connection_made(self, transport: asyncio.BaseTransport) -> None: - super().connection_made(transport) - self.server.start_connection_handler(self) - - -class Server: - """ - WebSocket server returned by :func:`serve`. - - This class mirrors the API of :class:`asyncio.Server`. - - It keeps track of WebSocket connections in order to close them properly - when shutting down. - - Args: - handler: Connection handler. It receives the WebSocket connection, - which is a :class:`ServerConnection`, in argument. - process_request: Intercept the request during the opening handshake. - Return an HTTP response to force the response. Return :obj:`None` to - continue normally. When you force an HTTP 101 Continue response, the - handshake is successful. Else, the connection is aborted. - ``process_request`` may be a function or a coroutine. - process_response: Intercept the response during the opening handshake. - Modify the response or return a new HTTP response to force the - response. Return :obj:`None` to continue normally. When you force an - HTTP 101 Continue response, the handshake is successful. Else, the - connection is aborted. ``process_response`` may be a function or a - coroutine. - server_header: Value of the ``Server`` response header. - It defaults to ``"Python/x.y.z websockets/X.Y"``. Setting it to - :obj:`None` removes the header. - open_timeout: Timeout for opening connections in seconds. - :obj:`None` disables the timeout. - logger: Logger for this server. - It defaults to ``logging.getLogger("websockets.server")``. - See the :doc:`logging guide <../../topics/logging>` for details. - - """ - - def __init__( - self, - handler: Callable[[ServerConnection], Awaitable[None]], - *, - process_request: ( - Callable[ - [ServerConnection, Request], - Awaitable[Response | None] | Response | None, - ] - | None - ) = None, - process_response: ( - Callable[ - [ServerConnection, Request, Response], - Awaitable[Response | None] | Response | None, - ] - | None - ) = None, - server_header: str | None = SERVER, - open_timeout: float | None = 10, - logger: LoggerLike | None = None, - ) -> None: - self.loop = asyncio.get_running_loop() - self.handler = handler - self.process_request = process_request - self.process_response = process_response - self.server_header = server_header - self.open_timeout = open_timeout - if logger is None: - logger = logging.getLogger("websockets.server") - self.logger = logger - - # Keep track of active connections. - self.handlers: dict[ServerConnection, asyncio.Task[None]] = {} - - # Task responsible for closing the server and terminating connections. - self.close_task: asyncio.Task[None] | None = None - - # Completed when the server is closed and connections are terminated. - self.closed_waiter: asyncio.Future[None] = self.loop.create_future() - - @property - def connections(self) -> set[ServerConnection]: - """ - Set of active connections. - - This property contains all connections that completed the opening - handshake successfully and didn't start the closing handshake yet. - It can be useful in combination with :func:`~broadcast`. - - """ - return {connection for connection in self.handlers if connection.state is OPEN} - - def wrap(self, server: asyncio.Server) -> None: - """ - Attach to a given :class:`asyncio.Server`. - - Since :meth:`~asyncio.loop.create_server` doesn't support injecting a - custom ``Server`` class, the easiest solution that doesn't rely on - private :mod:`asyncio` APIs is to: - - - instantiate a :class:`Server` - - give the protocol factory a reference to that instance - - call :meth:`~asyncio.loop.create_server` with the factory - - attach the resulting :class:`asyncio.Server` with this method - - """ - self.server = server - for sock in server.sockets: - if sock.family == socket.AF_INET: - name = "%s:%d" % sock.getsockname() - elif sock.family == socket.AF_INET6: - name = "[%s]:%d" % sock.getsockname()[:2] - elif sock.family == socket.AF_UNIX: - name = sock.getsockname() - # In the unlikely event that someone runs websockets over a - # protocol other than IP or Unix sockets, avoid crashing. - else: # pragma: no cover - name = str(sock.getsockname()) - self.logger.info("server listening on %s", name) - - async def conn_handler(self, connection: ServerConnection) -> None: - """ - Handle the lifecycle of a WebSocket connection. - - Since this method doesn't have a caller that can handle exceptions, - it attempts to log relevant ones. - - It guarantees that the TCP connection is closed before exiting. - - """ - try: - async with asyncio_timeout(self.open_timeout): - try: - await connection.handshake( - self.process_request, - self.process_response, - self.server_header, - ) - except asyncio.CancelledError: - connection.transport.abort() - raise - except Exception: - connection.logger.error("opening handshake failed", exc_info=True) - connection.transport.abort() - return - - if connection.protocol.state is not OPEN: - # process_request or process_response rejected the handshake. - connection.transport.abort() - return - - try: - connection.start_keepalive() - await self.handler(connection) - except Exception: - connection.logger.error("connection handler failed", exc_info=True) - await connection.close(CloseCode.INTERNAL_ERROR) - else: - await connection.close() - - except TimeoutError: - # When the opening handshake times out, there's nothing to log. - pass - - except Exception: # pragma: no cover - # Don't leak connections on unexpected errors. - connection.transport.abort() - - finally: - # Registration is tied to the lifecycle of conn_handler() because - # the server waits for connection handlers to terminate, even if - # all connections are already closed. - del self.handlers[connection] - - def start_connection_handler(self, connection: ServerConnection) -> None: - """ - Register a connection with this server. - - """ - # The connection must be registered in self.handlers immediately. - # If it was registered in conn_handler(), a race condition could - # happen when closing the server after scheduling conn_handler() - # but before it starts executing. - self.handlers[connection] = self.loop.create_task(self.conn_handler(connection)) - - def close(self, close_connections: bool = True) -> None: - """ - Close the server. - - * Close the underlying :class:`asyncio.Server`. - * When ``close_connections`` is :obj:`True`, which is the default, - close existing connections. Specifically: - - * Reject opening WebSocket connections with an HTTP 503 (service - unavailable) error. This happens when the server accepted the TCP - connection but didn't complete the opening handshake before closing. - * Close open WebSocket connections with close code 1001 (going away). - - * Wait until all connection handlers terminate. - - :meth:`close` is idempotent. - - """ - if self.close_task is None: - self.close_task = self.get_loop().create_task( - self._close(close_connections) - ) - - async def _close(self, close_connections: bool) -> None: - """ - Implementation of :meth:`close`. - - This calls :meth:`~asyncio.Server.close` on the underlying - :class:`asyncio.Server` object to stop accepting new connections and - then closes open connections with close code 1001. - - """ - self.logger.info("server closing") - - # Stop accepting new connections. - self.server.close() - - # Wait until all accepted connections reach connection_made() and call - # register(). See https://github.com/python/cpython/issues/79033 for - # details. This workaround can be removed when dropping Python < 3.11. - await asyncio.sleep(0) - - if close_connections: - # Close OPEN connections with close code 1001. After server.close(), - # handshake() closes OPENING connections with an HTTP 503 error. - close_tasks = [ - asyncio.create_task(connection.close(1001)) - for connection in self.handlers - if connection.protocol.state is not CONNECTING - ] - # asyncio.wait doesn't accept an empty first argument. - if close_tasks: - await asyncio.wait(close_tasks) - - # Wait until all TCP connections are closed. - await self.server.wait_closed() - - # Wait until all connection handlers terminate. - # asyncio.wait doesn't accept an empty first argument. - if self.handlers: - await asyncio.wait(self.handlers.values()) - - # Tell wait_closed() to return. - self.closed_waiter.set_result(None) - - self.logger.info("server closed") - - async def wait_closed(self) -> None: - """ - Wait until the server is closed. - - When :meth:`wait_closed` returns, all TCP connections are closed and - all connection handlers have returned. - - To ensure a fast shutdown, a connection handler should always be - awaiting at least one of: - - * :meth:`~ServerConnection.recv`: when the connection is closed, - it raises :exc:`~websockets.exceptions.ConnectionClosedOK`; - * :meth:`~ServerConnection.wait_closed`: when the connection is - closed, it returns. - - Then the connection handler is immediately notified of the shutdown; - it can clean up and exit. - - """ - await asyncio.shield(self.closed_waiter) - - def get_loop(self) -> asyncio.AbstractEventLoop: - """ - See :meth:`asyncio.Server.get_loop`. - - """ - return self.server.get_loop() - - def is_serving(self) -> bool: # pragma: no cover - """ - See :meth:`asyncio.Server.is_serving`. - - """ - return self.server.is_serving() - - async def start_serving(self) -> None: # pragma: no cover - """ - See :meth:`asyncio.Server.start_serving`. - - Typical use:: - - server = await serve(..., start_serving=False) - # perform additional setup here... - # ... then start the server - await server.start_serving() - - """ - await self.server.start_serving() - - async def serve_forever(self) -> None: # pragma: no cover - """ - See :meth:`asyncio.Server.serve_forever`. - - Typical use:: - - server = await serve(...) - # this coroutine doesn't return - # canceling it stops the server - await server.serve_forever() - - This is an alternative to using :func:`serve` as an asynchronous context - manager. Shutdown is triggered by canceling :meth:`serve_forever` - instead of exiting a :func:`serve` context. - - """ - await self.server.serve_forever() - - @property - def sockets(self) -> Iterable[socket.socket]: - """ - See :attr:`asyncio.Server.sockets`. - - """ - return self.server.sockets - - async def __aenter__(self) -> Server: # pragma: no cover - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: # pragma: no cover - self.close() - await self.wait_closed() - - -# This is spelled in lower case because it's exposed as a callable in the API. -class serve: - """ - Create a WebSocket server listening on ``host`` and ``port``. - - Whenever a client connects, the server creates a :class:`ServerConnection`, - performs the opening handshake, and delegates to the ``handler`` coroutine. - - The handler receives the :class:`ServerConnection` instance, which you can - use to send and receive messages. - - Once the handler completes, either normally or with an exception, the server - performs the closing handshake and closes the connection. - - This coroutine returns a :class:`Server` whose API mirrors - :class:`asyncio.Server`. Treat it as an asynchronous context manager to - ensure that the server will be closed:: - - from websockets.asyncio.server import serve - - def handler(websocket): - ... - - # set this future to exit the server - stop = asyncio.get_running_loop().create_future() - - async with serve(handler, host, port): - await stop - - Alternatively, call :meth:`~Server.serve_forever` to serve requests and - cancel it to stop the server:: - - server = await serve(handler, host, port) - await server.serve_forever() - - Args: - handler: Connection handler. It receives the WebSocket connection, - which is a :class:`ServerConnection`, in argument. - host: Network interfaces the server binds to. - See :meth:`~asyncio.loop.create_server` for details. - port: TCP port the server listens on. - See :meth:`~asyncio.loop.create_server` for details. - origins: Acceptable values of the ``Origin`` header, for defending - against Cross-Site WebSocket Hijacking attacks. Values can be - :class:`str` to test for an exact match or regular expressions - compiled by :func:`re.compile` to test against a pattern. Include - :obj:`None` in the list if the lack of an origin is acceptable. - extensions: List of supported extensions, in order in which they - should be negotiated and run. - subprotocols: List of supported subprotocols, in order of decreasing - preference. - select_subprotocol: Callback for selecting a subprotocol among - those supported by the client and the server. It receives a - :class:`ServerConnection` (not a - :class:`~websockets.server.ServerProtocol`!) instance and a list of - subprotocols offered by the client. Other than the first argument, - it has the same behavior as the - :meth:`ServerProtocol.select_subprotocol - ` method. - compression: The "permessage-deflate" extension is enabled by default. - Set ``compression`` to :obj:`None` to disable it. See the - :doc:`compression guide <../../topics/compression>` for details. - process_request: Intercept the request during the opening handshake. - Return an HTTP response to force the response or :obj:`None` to - continue normally. When you force an HTTP 101 Continue response, the - handshake is successful. Else, the connection is aborted. - ``process_request`` may be a function or a coroutine. - process_response: Intercept the response during the opening handshake. - Return an HTTP response to force the response or :obj:`None` to - continue normally. When you force an HTTP 101 Continue response, the - handshake is successful. Else, the connection is aborted. - ``process_response`` may be a function or a coroutine. - server_header: Value of the ``Server`` response header. - It defaults to ``"Python/x.y.z websockets/X.Y"``. Setting it to - :obj:`None` removes the header. - open_timeout: Timeout for opening connections in seconds. - :obj:`None` disables the timeout. - ping_interval: Interval between keepalive pings in seconds. - :obj:`None` disables keepalive. - ping_timeout: Timeout for keepalive pings in seconds. - :obj:`None` disables timeouts. - close_timeout: Timeout for closing connections in seconds. - :obj:`None` disables the timeout. - max_size: Maximum size of incoming messages in bytes. - :obj:`None` disables the limit. - max_queue: High-water mark of the buffer where frames are received. - It defaults to 16 frames. The low-water mark defaults to ``max_queue - // 4``. You may pass a ``(high, low)`` tuple to set the high-water - and low-water marks. If you want to disable flow control entirely, - you may set it to ``None``, although that's a bad idea. - write_limit: High-water mark of write buffer in bytes. It is passed to - :meth:`~asyncio.WriteTransport.set_write_buffer_limits`. It defaults - to 32 KiB. You may pass a ``(high, low)`` tuple to set the - high-water and low-water marks. - logger: Logger for this server. - It defaults to ``logging.getLogger("websockets.server")``. See the - :doc:`logging guide <../../topics/logging>` for details. - create_connection: Factory for the :class:`ServerConnection` managing - the connection. Set it to a wrapper or a subclass to customize - connection handling. - - Any other keyword arguments are passed to the event loop's - :meth:`~asyncio.loop.create_server` method. - - For example: - - * You can set ``ssl`` to a :class:`~ssl.SSLContext` to enable TLS. - - * You can set ``sock`` to provide a preexisting TCP socket. You may call - :func:`socket.create_server` (not to be confused with the event loop's - :meth:`~asyncio.loop.create_server` method) to create a suitable server - socket and customize it. - - * You can set ``start_serving`` to ``False`` to start accepting connections - only after you call :meth:`~Server.start_serving()` or - :meth:`~Server.serve_forever()`. - - """ - - def __init__( - self, - handler: Callable[[ServerConnection], Awaitable[None]], - host: str | None = None, - port: int | None = None, - *, - # WebSocket - origins: Sequence[Origin | re.Pattern[str] | None] | None = None, - extensions: Sequence[ServerExtensionFactory] | None = None, - subprotocols: Sequence[Subprotocol] | None = None, - select_subprotocol: ( - Callable[ - [ServerConnection, Sequence[Subprotocol]], - Subprotocol | None, - ] - | None - ) = None, - compression: str | None = "deflate", - # HTTP - process_request: ( - Callable[ - [ServerConnection, Request], - Awaitable[Response | None] | Response | None, - ] - | None - ) = None, - process_response: ( - Callable[ - [ServerConnection, Request, Response], - Awaitable[Response | None] | Response | None, - ] - | None - ) = None, - server_header: str | None = SERVER, - # Timeouts - open_timeout: float | None = 10, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = 10, - # Limits - max_size: int | None = 2**20, - max_queue: int | None | tuple[int | None, int | None] = 16, - write_limit: int | tuple[int, int | None] = 2**15, - # Logging - logger: LoggerLike | None = None, - # Escape hatch for advanced customization - create_connection: type[ServerConnection] | None = None, - # Other keyword arguments are passed to loop.create_server - **kwargs: Any, - ) -> None: - if subprotocols is not None: - validate_subprotocols(subprotocols) - - if compression == "deflate": - extensions = enable_server_permessage_deflate(extensions) - elif compression is not None: - raise ValueError(f"unsupported compression: {compression}") - - if create_connection is None: - create_connection = ServerConnection - - self.server = Server( - handler, - process_request=process_request, - process_response=process_response, - server_header=server_header, - open_timeout=open_timeout, - logger=logger, - ) - - if kwargs.get("ssl") is not None: - kwargs.setdefault("ssl_handshake_timeout", open_timeout) - if sys.version_info[:2] >= (3, 11): # pragma: no branch - kwargs.setdefault("ssl_shutdown_timeout", close_timeout) - - def factory() -> ServerConnection: - """ - Create an asyncio protocol for managing a WebSocket connection. - - """ - # Create a closure to give select_subprotocol access to connection. - protocol_select_subprotocol: ( - Callable[ - [ServerProtocol, Sequence[Subprotocol]], - Subprotocol | None, - ] - | None - ) = None - if select_subprotocol is not None: - - def protocol_select_subprotocol( - protocol: ServerProtocol, - subprotocols: Sequence[Subprotocol], - ) -> Subprotocol | None: - # mypy doesn't know that select_subprotocol is immutable. - assert select_subprotocol is not None - # Ensure this function is only used in the intended context. - assert protocol is connection.protocol - return select_subprotocol(connection, subprotocols) - - # This is a protocol in the Sans-I/O implementation of websockets. - protocol = ServerProtocol( - origins=origins, - extensions=extensions, - subprotocols=subprotocols, - select_subprotocol=protocol_select_subprotocol, - max_size=max_size, - logger=logger, - ) - # This is a connection in websockets and a protocol in asyncio. - connection = create_connection( - protocol, - self.server, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - close_timeout=close_timeout, - max_queue=max_queue, - write_limit=write_limit, - ) - return connection - - loop = asyncio.get_running_loop() - if kwargs.pop("unix", False): - self.create_server = loop.create_unix_server(factory, **kwargs) - else: - # mypy cannot tell that kwargs must provide sock when port is None. - self.create_server = loop.create_server(factory, host, port, **kwargs) # type: ignore[arg-type] - - # async with serve(...) as ...: ... - - async def __aenter__(self) -> Server: - return await self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - self.server.close() - await self.server.wait_closed() - - # ... = await serve(...) - - def __await__(self) -> Generator[Any, None, Server]: - # Create a suitable iterator by calling __await__ on a coroutine. - return self.__await_impl__().__await__() - - async def __await_impl__(self) -> Server: - server = await self.create_server - self.server.wrap(server) - return self.server - - # ... = yield from serve(...) - remove when dropping Python < 3.10 - - __iter__ = __await__ - - -def unix_serve( - handler: Callable[[ServerConnection], Awaitable[None]], - path: str | None = None, - **kwargs: Any, -) -> Awaitable[Server]: - """ - Create a WebSocket server listening on a Unix socket. - - This function is identical to :func:`serve`, except the ``host`` and - ``port`` arguments are replaced by ``path``. It's only available on Unix. - - It's useful for deploying a server behind a reverse proxy such as nginx. - - Args: - handler: Connection handler. It receives the WebSocket connection, - which is a :class:`ServerConnection`, in argument. - path: File system path to the Unix socket. - - """ - return serve(handler, unix=True, path=path, **kwargs) - - -def is_credentials(credentials: Any) -> bool: - try: - username, password = credentials - except (TypeError, ValueError): - return False - else: - return isinstance(username, str) and isinstance(password, str) - - -def basic_auth( - realm: str = "", - credentials: tuple[str, str] | Iterable[tuple[str, str]] | None = None, - check_credentials: Callable[[str, str], Awaitable[bool] | bool] | None = None, -) -> Callable[[ServerConnection, Request], Awaitable[Response | None]]: - """ - Factory for ``process_request`` to enforce HTTP Basic Authentication. - - :func:`basic_auth` is designed to integrate with :func:`serve` as follows:: - - from websockets.asyncio.server import basic_auth, serve - - async with serve( - ..., - process_request=basic_auth( - realm="my dev server", - credentials=("hello", "iloveyou"), - ), - ): - - If authentication succeeds, the connection's ``username`` attribute is set. - If it fails, the server responds with an HTTP 401 Unauthorized status. - - One of ``credentials`` or ``check_credentials`` must be provided; not both. - - Args: - realm: Scope of protection. It should contain only ASCII characters - because the encoding of non-ASCII characters is undefined. Refer to - section 2.2 of :rfc:`7235` for details. - credentials: Hard coded authorized credentials. It can be a - ``(username, password)`` pair or a list of such pairs. - check_credentials: Function or coroutine that verifies credentials. - It receives ``username`` and ``password`` arguments and returns - whether they're valid. - Raises: - TypeError: If ``credentials`` or ``check_credentials`` is wrong. - ValueError: If ``credentials`` and ``check_credentials`` are both - provided or both not provided. - - """ - if (credentials is None) == (check_credentials is None): - raise ValueError("provide either credentials or check_credentials") - - if credentials is not None: - if is_credentials(credentials): - credentials_list = [cast(tuple[str, str], credentials)] - elif isinstance(credentials, Iterable): - credentials_list = list(cast(Iterable[tuple[str, str]], credentials)) - if not all(is_credentials(item) for item in credentials_list): - raise TypeError(f"invalid credentials argument: {credentials}") - else: - raise TypeError(f"invalid credentials argument: {credentials}") - - credentials_dict = dict(credentials_list) - - def check_credentials(username: str, password: str) -> bool: - try: - expected_password = credentials_dict[username] - except KeyError: - return False - return hmac.compare_digest(expected_password, password) - - assert check_credentials is not None # help mypy - - async def process_request( - connection: ServerConnection, - request: Request, - ) -> Response | None: - """ - Perform HTTP Basic Authentication. - - If it succeeds, set the connection's ``username`` attribute and return - :obj:`None`. If it fails, return an HTTP 401 Unauthorized responss. - - """ - try: - authorization = request.headers["Authorization"] - except KeyError: - response = connection.respond( - http.HTTPStatus.UNAUTHORIZED, - "Missing credentials\n", - ) - response.headers["WWW-Authenticate"] = build_www_authenticate_basic(realm) - return response - - try: - username, password = parse_authorization_basic(authorization) - except InvalidHeader: - response = connection.respond( - http.HTTPStatus.UNAUTHORIZED, - "Unsupported credentials\n", - ) - response.headers["WWW-Authenticate"] = build_www_authenticate_basic(realm) - return response - - valid_credentials = check_credentials(username, password) - if isinstance(valid_credentials, Awaitable): - valid_credentials = await valid_credentials - - if not valid_credentials: - response = connection.respond( - http.HTTPStatus.UNAUTHORIZED, - "Invalid credentials\n", - ) - response.headers["WWW-Authenticate"] = build_www_authenticate_basic(realm) - return response - - connection.username = username - return None - - return process_request diff --git a/wip/SNIP/client/websockets/auth.py b/wip/SNIP/client/websockets/auth.py deleted file mode 100644 index 15b70a3..0000000 --- a/wip/SNIP/client/websockets/auth.py +++ /dev/null @@ -1,18 +0,0 @@ -from __future__ import annotations - -import warnings - - -with warnings.catch_warnings(): - # Suppress redundant DeprecationWarning raised by websockets.legacy. - warnings.filterwarnings("ignore", category=DeprecationWarning) - from .legacy.auth import * - from .legacy.auth import __all__ # noqa: F401 - - -warnings.warn( # deprecated in 14.0 - 2024-11-09 - "websockets.auth, an alias for websockets.legacy.auth, is deprecated; " - "see https://websockets.readthedocs.io/en/stable/howto/upgrade.html " - "for upgrade instructions", - DeprecationWarning, -) diff --git a/wip/SNIP/client/websockets/cli.py b/wip/SNIP/client/websockets/cli.py deleted file mode 100644 index e084b62..0000000 --- a/wip/SNIP/client/websockets/cli.py +++ /dev/null @@ -1,178 +0,0 @@ -from __future__ import annotations - -import argparse -import asyncio -import os -import sys -from typing import Generator - -from .asyncio.client import ClientConnection, connect -from .asyncio.messages import SimpleQueue -from .exceptions import ConnectionClosed -from .frames import Close -from .streams import StreamReader -from .version import version as websockets_version - - -__all__ = ["main"] - - -def print_during_input(string: str) -> None: - sys.stdout.write( - # Save cursor position - "\N{ESC}7" - # Add a new line - "\N{LINE FEED}" - # Move cursor up - "\N{ESC}[A" - # Insert blank line, scroll last line down - "\N{ESC}[L" - # Print string in the inserted blank line - f"{string}\N{LINE FEED}" - # Restore cursor position - "\N{ESC}8" - # Move cursor down - "\N{ESC}[B" - ) - sys.stdout.flush() - - -def print_over_input(string: str) -> None: - sys.stdout.write( - # Move cursor to beginning of line - "\N{CARRIAGE RETURN}" - # Delete current line - "\N{ESC}[K" - # Print string - f"{string}\N{LINE FEED}" - ) - sys.stdout.flush() - - -class ReadLines(asyncio.Protocol): - def __init__(self) -> None: - self.reader = StreamReader() - self.messages: SimpleQueue[str] = SimpleQueue() - - def parse(self) -> Generator[None, None, None]: - while True: - sys.stdout.write("> ") - sys.stdout.flush() - line = yield from self.reader.read_line(sys.maxsize) - self.messages.put(line.decode().rstrip("\r\n")) - - def connection_made(self, transport: asyncio.BaseTransport) -> None: - self.parser = self.parse() - next(self.parser) - - def data_received(self, data: bytes) -> None: - self.reader.feed_data(data) - next(self.parser) - - def eof_received(self) -> None: - self.reader.feed_eof() - # next(self.parser) isn't useful and would raise EOFError. - - def connection_lost(self, exc: Exception | None) -> None: - self.reader.discard() - self.messages.abort() - - -async def print_incoming_messages(websocket: ClientConnection) -> None: - async for message in websocket: - if isinstance(message, str): - print_during_input("< " + message) - else: - print_during_input("< (binary) " + message.hex()) - - -async def send_outgoing_messages( - websocket: ClientConnection, - messages: SimpleQueue[str], -) -> None: - while True: - try: - message = await messages.get() - except EOFError: - break - try: - await websocket.send(message) - except ConnectionClosed: # pragma: no cover - break - - -async def interactive_client(uri: str) -> None: - try: - websocket = await connect(uri) - except Exception as exc: - print(f"Failed to connect to {uri}: {exc}.") - sys.exit(1) - else: - print(f"Connected to {uri}.") - - loop = asyncio.get_running_loop() - transport, protocol = await loop.connect_read_pipe(ReadLines, sys.stdin) - incoming = asyncio.create_task( - print_incoming_messages(websocket), - ) - outgoing = asyncio.create_task( - send_outgoing_messages(websocket, protocol.messages), - ) - try: - await asyncio.wait( - [incoming, outgoing], - # Clean up and exit when the server closes the connection - # or the user enters EOT (^D), whichever happens first. - return_when=asyncio.FIRST_COMPLETED, - ) - # asyncio.run() cancels the main task when the user triggers SIGINT (^C). - # https://docs.python.org/3/library/asyncio-runner.html#handling-keyboard-interruption - # Clean up and exit without re-raising CancelledError to prevent Python - # from raising KeyboardInterrupt and displaying a stack track. - except asyncio.CancelledError: # pragma: no cover - pass - finally: - incoming.cancel() - outgoing.cancel() - transport.close() - - await websocket.close() - assert websocket.close_code is not None and websocket.close_reason is not None - close_status = Close(websocket.close_code, websocket.close_reason) - print_over_input(f"Connection closed: {close_status}.") - - -def main(argv: list[str] | None = None) -> None: - parser = argparse.ArgumentParser( - prog="websockets", - description="Interactive WebSocket client.", - add_help=False, - ) - group = parser.add_mutually_exclusive_group() - group.add_argument("--version", action="store_true") - group.add_argument("uri", metavar="", nargs="?") - args = parser.parse_args(argv) - - if args.version: - print(f"websockets {websockets_version}") - return - - if args.uri is None: - parser.print_usage() - sys.exit(2) - - # Enable VT100 to support ANSI escape codes in Command Prompt on Windows. - # See https://github.com/python/cpython/issues/74261 for why this works. - if sys.platform == "win32": - os.system("") - - try: - import readline # noqa: F401 - except ImportError: # readline isn't available on all platforms - pass - - # Remove the try/except block when dropping Python < 3.11. - try: - asyncio.run(interactive_client(args.uri)) - except KeyboardInterrupt: # pragma: no cover - pass diff --git a/wip/SNIP/client/websockets/client.py b/wip/SNIP/client/websockets/client.py deleted file mode 100644 index 9ea21c3..0000000 --- a/wip/SNIP/client/websockets/client.py +++ /dev/null @@ -1,389 +0,0 @@ -from __future__ import annotations - -import os -import random -import warnings -from collections.abc import Generator, Sequence -from typing import Any - -from .datastructures import Headers, MultipleValuesError -from .exceptions import ( - InvalidHandshake, - InvalidHeader, - InvalidHeaderValue, - InvalidMessage, - InvalidStatus, - InvalidUpgrade, - NegotiationError, -) -from .extensions import ClientExtensionFactory, Extension -from .headers import ( - build_authorization_basic, - build_extension, - build_host, - build_subprotocol, - parse_connection, - parse_extension, - parse_subprotocol, - parse_upgrade, -) -from .http11 import Request, Response -from .imports import lazy_import -from .protocol import CLIENT, CONNECTING, OPEN, Protocol, State -from .typing import ( - ConnectionOption, - ExtensionHeader, - LoggerLike, - Origin, - Subprotocol, - UpgradeProtocol, -) -from .uri import WebSocketURI -from .utils import accept_key, generate_key - - -__all__ = ["ClientProtocol"] - - -class ClientProtocol(Protocol): - """ - Sans-I/O implementation of a WebSocket client connection. - - Args: - uri: URI of the WebSocket server, parsed - with :func:`~websockets.uri.parse_uri`. - origin: Value of the ``Origin`` header. This is useful when connecting - to a server that validates the ``Origin`` header to defend against - Cross-Site WebSocket Hijacking attacks. - extensions: List of supported extensions, in order in which they - should be tried. - subprotocols: List of supported subprotocols, in order of decreasing - preference. - state: Initial state of the WebSocket connection. - max_size: Maximum size of incoming messages in bytes; - :obj:`None` disables the limit. - logger: Logger for this connection; - defaults to ``logging.getLogger("websockets.client")``; - see the :doc:`logging guide <../../topics/logging>` for details. - - """ - - def __init__( - self, - uri: WebSocketURI, - *, - origin: Origin | None = None, - extensions: Sequence[ClientExtensionFactory] | None = None, - subprotocols: Sequence[Subprotocol] | None = None, - state: State = CONNECTING, - max_size: int | None = 2**20, - logger: LoggerLike | None = None, - ) -> None: - super().__init__( - side=CLIENT, - state=state, - max_size=max_size, - logger=logger, - ) - self.uri = uri - self.origin = origin - self.available_extensions = extensions - self.available_subprotocols = subprotocols - self.key = generate_key() - - def connect(self) -> Request: - """ - Create a handshake request to open a connection. - - You must send the handshake request with :meth:`send_request`. - - You can modify it before sending it, for example to add HTTP headers. - - Returns: - WebSocket handshake request event to send to the server. - - """ - headers = Headers() - headers["Host"] = build_host(self.uri.host, self.uri.port, self.uri.secure) - if self.uri.user_info: - headers["Authorization"] = build_authorization_basic(*self.uri.user_info) - if self.origin is not None: - headers["Origin"] = self.origin - headers["Upgrade"] = "websocket" - headers["Connection"] = "Upgrade" - headers["Sec-WebSocket-Key"] = self.key - headers["Sec-WebSocket-Version"] = "13" - if self.available_extensions is not None: - headers["Sec-WebSocket-Extensions"] = build_extension( - [ - (extension_factory.name, extension_factory.get_request_params()) - for extension_factory in self.available_extensions - ] - ) - if self.available_subprotocols is not None: - headers["Sec-WebSocket-Protocol"] = build_subprotocol( - self.available_subprotocols - ) - return Request(self.uri.resource_name, headers) - - def process_response(self, response: Response) -> None: - """ - Check a handshake response. - - Args: - request: WebSocket handshake response received from the server. - - Raises: - InvalidHandshake: If the handshake response is invalid. - - """ - - if response.status_code != 101: - raise InvalidStatus(response) - - headers = response.headers - - connection: list[ConnectionOption] = sum( - [parse_connection(value) for value in headers.get_all("Connection")], [] - ) - if not any(value.lower() == "upgrade" for value in connection): - raise InvalidUpgrade( - "Connection", ", ".join(connection) if connection else None - ) - - upgrade: list[UpgradeProtocol] = sum( - [parse_upgrade(value) for value in headers.get_all("Upgrade")], [] - ) - # For compatibility with non-strict implementations, ignore case when - # checking the Upgrade header. It's supposed to be 'WebSocket'. - if not (len(upgrade) == 1 and upgrade[0].lower() == "websocket"): - raise InvalidUpgrade("Upgrade", ", ".join(upgrade) if upgrade else None) - - try: - s_w_accept = headers["Sec-WebSocket-Accept"] - except KeyError: - raise InvalidHeader("Sec-WebSocket-Accept") from None - except MultipleValuesError: - raise InvalidHeader("Sec-WebSocket-Accept", "multiple values") from None - if s_w_accept != accept_key(self.key): - raise InvalidHeaderValue("Sec-WebSocket-Accept", s_w_accept) - - self.extensions = self.process_extensions(headers) - self.subprotocol = self.process_subprotocol(headers) - - def process_extensions(self, headers: Headers) -> list[Extension]: - """ - Handle the Sec-WebSocket-Extensions HTTP response header. - - Check that each extension is supported, as well as its parameters. - - :rfc:`6455` leaves the rules up to the specification of each - extension. - - To provide this level of flexibility, for each extension accepted by - the server, we check for a match with each extension available in the - client configuration. If no match is found, an exception is raised. - - If several variants of the same extension are accepted by the server, - it may be configured several times, which won't make sense in general. - Extensions must implement their own requirements. For this purpose, - the list of previously accepted extensions is provided. - - Other requirements, for example related to mandatory extensions or the - order of extensions, may be implemented by overriding this method. - - Args: - headers: WebSocket handshake response headers. - - Returns: - List of accepted extensions. - - Raises: - InvalidHandshake: To abort the handshake. - - """ - accepted_extensions: list[Extension] = [] - - extensions = headers.get_all("Sec-WebSocket-Extensions") - - if extensions: - if self.available_extensions is None: - raise NegotiationError("no extensions supported") - - parsed_extensions: list[ExtensionHeader] = sum( - [parse_extension(header_value) for header_value in extensions], [] - ) - - for name, response_params in parsed_extensions: - for extension_factory in self.available_extensions: - # Skip non-matching extensions based on their name. - if extension_factory.name != name: - continue - - # Skip non-matching extensions based on their params. - try: - extension = extension_factory.process_response_params( - response_params, accepted_extensions - ) - except NegotiationError: - continue - - # Add matching extension to the final list. - accepted_extensions.append(extension) - - # Break out of the loop once we have a match. - break - - # If we didn't break from the loop, no extension in our list - # matched what the server sent. Fail the connection. - else: - raise NegotiationError( - f"Unsupported extension: " - f"name = {name}, params = {response_params}" - ) - - return accepted_extensions - - def process_subprotocol(self, headers: Headers) -> Subprotocol | None: - """ - Handle the Sec-WebSocket-Protocol HTTP response header. - - If provided, check that it contains exactly one supported subprotocol. - - Args: - headers: WebSocket handshake response headers. - - Returns: - Subprotocol, if one was selected. - - """ - subprotocol: Subprotocol | None = None - - subprotocols = headers.get_all("Sec-WebSocket-Protocol") - - if subprotocols: - if self.available_subprotocols is None: - raise NegotiationError("no subprotocols supported") - - parsed_subprotocols: Sequence[Subprotocol] = sum( - [parse_subprotocol(header_value) for header_value in subprotocols], [] - ) - if len(parsed_subprotocols) > 1: - raise InvalidHeader( - "Sec-WebSocket-Protocol", - f"multiple values: {', '.join(parsed_subprotocols)}", - ) - - subprotocol = parsed_subprotocols[0] - if subprotocol not in self.available_subprotocols: - raise NegotiationError(f"unsupported subprotocol: {subprotocol}") - - return subprotocol - - def send_request(self, request: Request) -> None: - """ - Send a handshake request to the server. - - Args: - request: WebSocket handshake request event. - - """ - if self.debug: - self.logger.debug("> GET %s HTTP/1.1", request.path) - for key, value in request.headers.raw_items(): - self.logger.debug("> %s: %s", key, value) - - self.writes.append(request.serialize()) - - def parse(self) -> Generator[None]: - if self.state is CONNECTING: - try: - response = yield from Response.parse( - self.reader.read_line, - self.reader.read_exact, - self.reader.read_to_eof, - ) - except Exception as exc: - self.handshake_exc = InvalidMessage( - "did not receive a valid HTTP response" - ) - self.handshake_exc.__cause__ = exc - self.send_eof() - self.parser = self.discard() - next(self.parser) # start coroutine - yield - - if self.debug: - code, phrase = response.status_code, response.reason_phrase - self.logger.debug("< HTTP/1.1 %d %s", code, phrase) - for key, value in response.headers.raw_items(): - self.logger.debug("< %s: %s", key, value) - if response.body: - self.logger.debug("< [body] (%d bytes)", len(response.body)) - - try: - self.process_response(response) - except InvalidHandshake as exc: - response._exception = exc - self.events.append(response) - self.handshake_exc = exc - self.send_eof() - self.parser = self.discard() - next(self.parser) # start coroutine - yield - - assert self.state is CONNECTING - self.state = OPEN - self.events.append(response) - - yield from super().parse() - - -class ClientConnection(ClientProtocol): - def __init__(self, *args: Any, **kwargs: Any) -> None: - warnings.warn( # deprecated in 11.0 - 2023-04-02 - "ClientConnection was renamed to ClientProtocol", - DeprecationWarning, - ) - super().__init__(*args, **kwargs) - - -BACKOFF_INITIAL_DELAY = float(os.environ.get("WEBSOCKETS_BACKOFF_INITIAL_DELAY", "5")) -BACKOFF_MIN_DELAY = float(os.environ.get("WEBSOCKETS_BACKOFF_MIN_DELAY", "3.1")) -BACKOFF_MAX_DELAY = float(os.environ.get("WEBSOCKETS_BACKOFF_MAX_DELAY", "90.0")) -BACKOFF_FACTOR = float(os.environ.get("WEBSOCKETS_BACKOFF_FACTOR", "1.618")) - - -def backoff( - initial_delay: float = BACKOFF_INITIAL_DELAY, - min_delay: float = BACKOFF_MIN_DELAY, - max_delay: float = BACKOFF_MAX_DELAY, - factor: float = BACKOFF_FACTOR, -) -> Generator[float]: - """ - Generate a series of backoff delays between reconnection attempts. - - Yields: - How many seconds to wait before retrying to connect. - - """ - # Add a random initial delay between 0 and 5 seconds. - # See 7.2.3. Recovering from Abnormal Closure in RFC 6455. - yield random.random() * initial_delay - delay = min_delay - while delay < max_delay: - yield delay - delay *= factor - while True: - yield max_delay - - -lazy_import( - globals(), - deprecated_aliases={ - # deprecated in 14.0 - 2024-11-09 - "WebSocketClientProtocol": ".legacy.client", - "connect": ".legacy.client", - "unix_connect": ".legacy.client", - }, -) diff --git a/wip/SNIP/client/websockets/connection.py b/wip/SNIP/client/websockets/connection.py deleted file mode 100644 index 5e78e34..0000000 --- a/wip/SNIP/client/websockets/connection.py +++ /dev/null @@ -1,12 +0,0 @@ -from __future__ import annotations - -import warnings - -from .protocol import SEND_EOF, Protocol as Connection, Side, State # noqa: F401 - - -warnings.warn( # deprecated in 11.0 - 2023-04-02 - "websockets.connection was renamed to websockets.protocol " - "and Connection was renamed to Protocol", - DeprecationWarning, -) diff --git a/wip/SNIP/client/websockets/datastructures.py b/wip/SNIP/client/websockets/datastructures.py deleted file mode 100644 index 3c5dcbe..0000000 --- a/wip/SNIP/client/websockets/datastructures.py +++ /dev/null @@ -1,187 +0,0 @@ -from __future__ import annotations - -from collections.abc import Iterable, Iterator, Mapping, MutableMapping -from typing import Any, Protocol, Union - - -__all__ = [ - "Headers", - "HeadersLike", - "MultipleValuesError", -] - - -class MultipleValuesError(LookupError): - """ - Exception raised when :class:`Headers` has multiple values for a key. - - """ - - def __str__(self) -> str: - # Implement the same logic as KeyError_str in Objects/exceptions.c. - if len(self.args) == 1: - return repr(self.args[0]) - return super().__str__() - - -class Headers(MutableMapping[str, str]): - """ - Efficient data structure for manipulating HTTP headers. - - A :class:`list` of ``(name, values)`` is inefficient for lookups. - - A :class:`dict` doesn't suffice because header names are case-insensitive - and multiple occurrences of headers with the same name are possible. - - :class:`Headers` stores HTTP headers in a hybrid data structure to provide - efficient insertions and lookups while preserving the original data. - - In order to account for multiple values with minimal hassle, - :class:`Headers` follows this logic: - - - When getting a header with ``headers[name]``: - - if there's no value, :exc:`KeyError` is raised; - - if there's exactly one value, it's returned; - - if there's more than one value, :exc:`MultipleValuesError` is raised. - - - When setting a header with ``headers[name] = value``, the value is - appended to the list of values for that header. - - - When deleting a header with ``del headers[name]``, all values for that - header are removed (this is slow). - - Other methods for manipulating headers are consistent with this logic. - - As long as no header occurs multiple times, :class:`Headers` behaves like - :class:`dict`, except keys are lower-cased to provide case-insensitivity. - - Two methods support manipulating multiple values explicitly: - - - :meth:`get_all` returns a list of all values for a header; - - :meth:`raw_items` returns an iterator of ``(name, values)`` pairs. - - """ - - __slots__ = ["_dict", "_list"] - - # Like dict, Headers accepts an optional "mapping or iterable" argument. - def __init__(self, *args: HeadersLike, **kwargs: str) -> None: - self._dict: dict[str, list[str]] = {} - self._list: list[tuple[str, str]] = [] - self.update(*args, **kwargs) - - def __str__(self) -> str: - return "".join(f"{key}: {value}\r\n" for key, value in self._list) + "\r\n" - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self._list!r})" - - def copy(self) -> Headers: - copy = self.__class__() - copy._dict = self._dict.copy() - copy._list = self._list.copy() - return copy - - def serialize(self) -> bytes: - # Since headers only contain ASCII characters, we can keep this simple. - return str(self).encode() - - # Collection methods - - def __contains__(self, key: object) -> bool: - return isinstance(key, str) and key.lower() in self._dict - - def __iter__(self) -> Iterator[str]: - return iter(self._dict) - - def __len__(self) -> int: - return len(self._dict) - - # MutableMapping methods - - def __getitem__(self, key: str) -> str: - value = self._dict[key.lower()] - if len(value) == 1: - return value[0] - else: - raise MultipleValuesError(key) - - def __setitem__(self, key: str, value: str) -> None: - self._dict.setdefault(key.lower(), []).append(value) - self._list.append((key, value)) - - def __delitem__(self, key: str) -> None: - key_lower = key.lower() - self._dict.__delitem__(key_lower) - # This is inefficient. Fortunately deleting HTTP headers is uncommon. - self._list = [(k, v) for k, v in self._list if k.lower() != key_lower] - - def __eq__(self, other: Any) -> bool: - if not isinstance(other, Headers): - return NotImplemented - return self._dict == other._dict - - def clear(self) -> None: - """ - Remove all headers. - - """ - self._dict = {} - self._list = [] - - def update(self, *args: HeadersLike, **kwargs: str) -> None: - """ - Update from a :class:`Headers` instance and/or keyword arguments. - - """ - args = tuple( - arg.raw_items() if isinstance(arg, Headers) else arg for arg in args - ) - super().update(*args, **kwargs) - - # Methods for handling multiple values - - def get_all(self, key: str) -> list[str]: - """ - Return the (possibly empty) list of all values for a header. - - Args: - key: Header name. - - """ - return self._dict.get(key.lower(), []) - - def raw_items(self) -> Iterator[tuple[str, str]]: - """ - Return an iterator of all values as ``(name, value)`` pairs. - - """ - return iter(self._list) - - -# copy of _typeshed.SupportsKeysAndGetItem. -class SupportsKeysAndGetItem(Protocol): # pragma: no cover - """ - Dict-like types with ``keys() -> str`` and ``__getitem__(key: str) -> str`` methods. - - """ - - def keys(self) -> Iterable[str]: ... - - def __getitem__(self, key: str) -> str: ... - - -# Change to Headers | Mapping[str, str] | ... when dropping Python < 3.10. -HeadersLike = Union[ - Headers, - Mapping[str, str], - Iterable[tuple[str, str]], - SupportsKeysAndGetItem, -] -""" -Types accepted where :class:`Headers` is expected. - -In addition to :class:`Headers` itself, this includes dict-like types where both -keys and values are :class:`str`. - -""" diff --git a/wip/SNIP/client/websockets/exceptions.py b/wip/SNIP/client/websockets/exceptions.py deleted file mode 100644 index ab1a15c..0000000 --- a/wip/SNIP/client/websockets/exceptions.py +++ /dev/null @@ -1,473 +0,0 @@ -""" -:mod:`websockets.exceptions` defines the following hierarchy of exceptions. - -* :exc:`WebSocketException` - * :exc:`ConnectionClosed` - * :exc:`ConnectionClosedOK` - * :exc:`ConnectionClosedError` - * :exc:`InvalidURI` - * :exc:`InvalidProxy` - * :exc:`InvalidHandshake` - * :exc:`SecurityError` - * :exc:`ProxyError` - * :exc:`InvalidProxyMessage` - * :exc:`InvalidProxyStatus` - * :exc:`InvalidMessage` - * :exc:`InvalidStatus` - * :exc:`InvalidStatusCode` (legacy) - * :exc:`InvalidHeader` - * :exc:`InvalidHeaderFormat` - * :exc:`InvalidHeaderValue` - * :exc:`InvalidOrigin` - * :exc:`InvalidUpgrade` - * :exc:`NegotiationError` - * :exc:`DuplicateParameter` - * :exc:`InvalidParameterName` - * :exc:`InvalidParameterValue` - * :exc:`AbortHandshake` (legacy) - * :exc:`RedirectHandshake` (legacy) - * :exc:`ProtocolError` (Sans-I/O) - * :exc:`PayloadTooBig` (Sans-I/O) - * :exc:`InvalidState` (Sans-I/O) - * :exc:`ConcurrencyError` - -""" - -from __future__ import annotations - -import warnings - -from .imports import lazy_import - - -__all__ = [ - "WebSocketException", - "ConnectionClosed", - "ConnectionClosedOK", - "ConnectionClosedError", - "InvalidURI", - "InvalidProxy", - "InvalidHandshake", - "SecurityError", - "ProxyError", - "InvalidProxyMessage", - "InvalidProxyStatus", - "InvalidMessage", - "InvalidStatus", - "InvalidHeader", - "InvalidHeaderFormat", - "InvalidHeaderValue", - "InvalidOrigin", - "InvalidUpgrade", - "NegotiationError", - "DuplicateParameter", - "InvalidParameterName", - "InvalidParameterValue", - "ProtocolError", - "PayloadTooBig", - "InvalidState", - "ConcurrencyError", -] - - -class WebSocketException(Exception): - """ - Base class for all exceptions defined by websockets. - - """ - - -class ConnectionClosed(WebSocketException): - """ - Raised when trying to interact with a closed connection. - - Attributes: - rcvd: If a close frame was received, its code and reason are available - in ``rcvd.code`` and ``rcvd.reason``. - sent: If a close frame was sent, its code and reason are available - in ``sent.code`` and ``sent.reason``. - rcvd_then_sent: If close frames were received and sent, this attribute - tells in which order this happened, from the perspective of this - side of the connection. - - """ - - def __init__( - self, - rcvd: frames.Close | None, - sent: frames.Close | None, - rcvd_then_sent: bool | None = None, - ) -> None: - self.rcvd = rcvd - self.sent = sent - self.rcvd_then_sent = rcvd_then_sent - assert (self.rcvd_then_sent is None) == (self.rcvd is None or self.sent is None) - - def __str__(self) -> str: - if self.rcvd is None: - if self.sent is None: - return "no close frame received or sent" - else: - return f"sent {self.sent}; no close frame received" - else: - if self.sent is None: - return f"received {self.rcvd}; no close frame sent" - else: - if self.rcvd_then_sent: - return f"received {self.rcvd}; then sent {self.sent}" - else: - return f"sent {self.sent}; then received {self.rcvd}" - - # code and reason attributes are provided for backwards-compatibility - - @property - def code(self) -> int: - warnings.warn( # deprecated in 13.1 - 2024-09-21 - "ConnectionClosed.code is deprecated; " - "use Protocol.close_code or ConnectionClosed.rcvd.code", - DeprecationWarning, - ) - if self.rcvd is None: - return frames.CloseCode.ABNORMAL_CLOSURE - return self.rcvd.code - - @property - def reason(self) -> str: - warnings.warn( # deprecated in 13.1 - 2024-09-21 - "ConnectionClosed.reason is deprecated; " - "use Protocol.close_reason or ConnectionClosed.rcvd.reason", - DeprecationWarning, - ) - if self.rcvd is None: - return "" - return self.rcvd.reason - - -class ConnectionClosedOK(ConnectionClosed): - """ - Like :exc:`ConnectionClosed`, when the connection terminated properly. - - A close code with code 1000 (OK) or 1001 (going away) or without a code was - received and sent. - - """ - - -class ConnectionClosedError(ConnectionClosed): - """ - Like :exc:`ConnectionClosed`, when the connection terminated with an error. - - A close frame with a code other than 1000 (OK) or 1001 (going away) was - received or sent, or the closing handshake didn't complete properly. - - """ - - -class InvalidURI(WebSocketException): - """ - Raised when connecting to a URI that isn't a valid WebSocket URI. - - """ - - def __init__(self, uri: str, msg: str) -> None: - self.uri = uri - self.msg = msg - - def __str__(self) -> str: - return f"{self.uri} isn't a valid URI: {self.msg}" - - -class InvalidProxy(WebSocketException): - """ - Raised when connecting via a proxy that isn't valid. - - """ - - def __init__(self, proxy: str, msg: str) -> None: - self.proxy = proxy - self.msg = msg - - def __str__(self) -> str: - return f"{self.proxy} isn't a valid proxy: {self.msg}" - - -class InvalidHandshake(WebSocketException): - """ - Base class for exceptions raised when the opening handshake fails. - - """ - - -class SecurityError(InvalidHandshake): - """ - Raised when a handshake request or response breaks a security rule. - - Security limits can be configured with :doc:`environment variables - <../reference/variables>`. - - """ - - -class ProxyError(InvalidHandshake): - """ - Raised when failing to connect to a proxy. - - """ - - -class InvalidProxyMessage(ProxyError): - """ - Raised when an HTTP proxy response is malformed. - - """ - - -class InvalidProxyStatus(ProxyError): - """ - Raised when an HTTP proxy rejects the connection. - - """ - - def __init__(self, response: http11.Response) -> None: - self.response = response - - def __str__(self) -> str: - return f"proxy rejected connection: HTTP {self.response.status_code:d}" - - -class InvalidMessage(InvalidHandshake): - """ - Raised when a handshake request or response is malformed. - - """ - - -class InvalidStatus(InvalidHandshake): - """ - Raised when a handshake response rejects the WebSocket upgrade. - - """ - - def __init__(self, response: http11.Response) -> None: - self.response = response - - def __str__(self) -> str: - return ( - f"server rejected WebSocket connection: HTTP {self.response.status_code:d}" - ) - - -class InvalidHeader(InvalidHandshake): - """ - Raised when an HTTP header doesn't have a valid format or value. - - """ - - def __init__(self, name: str, value: str | None = None) -> None: - self.name = name - self.value = value - - def __str__(self) -> str: - if self.value is None: - return f"missing {self.name} header" - elif self.value == "": - return f"empty {self.name} header" - else: - return f"invalid {self.name} header: {self.value}" - - -class InvalidHeaderFormat(InvalidHeader): - """ - Raised when an HTTP header cannot be parsed. - - The format of the header doesn't match the grammar for that header. - - """ - - def __init__(self, name: str, error: str, header: str, pos: int) -> None: - super().__init__(name, f"{error} at {pos} in {header}") - - -class InvalidHeaderValue(InvalidHeader): - """ - Raised when an HTTP header has a wrong value. - - The format of the header is correct but the value isn't acceptable. - - """ - - -class InvalidOrigin(InvalidHeader): - """ - Raised when the Origin header in a request isn't allowed. - - """ - - def __init__(self, origin: str | None) -> None: - super().__init__("Origin", origin) - - -class InvalidUpgrade(InvalidHeader): - """ - Raised when the Upgrade or Connection header isn't correct. - - """ - - -class NegotiationError(InvalidHandshake): - """ - Raised when negotiating an extension or a subprotocol fails. - - """ - - -class DuplicateParameter(NegotiationError): - """ - Raised when a parameter name is repeated in an extension header. - - """ - - def __init__(self, name: str) -> None: - self.name = name - - def __str__(self) -> str: - return f"duplicate parameter: {self.name}" - - -class InvalidParameterName(NegotiationError): - """ - Raised when a parameter name in an extension header is invalid. - - """ - - def __init__(self, name: str) -> None: - self.name = name - - def __str__(self) -> str: - return f"invalid parameter name: {self.name}" - - -class InvalidParameterValue(NegotiationError): - """ - Raised when a parameter value in an extension header is invalid. - - """ - - def __init__(self, name: str, value: str | None) -> None: - self.name = name - self.value = value - - def __str__(self) -> str: - if self.value is None: - return f"missing value for parameter {self.name}" - elif self.value == "": - return f"empty value for parameter {self.name}" - else: - return f"invalid value for parameter {self.name}: {self.value}" - - -class ProtocolError(WebSocketException): - """ - Raised when receiving or sending a frame that breaks the protocol. - - The Sans-I/O implementation raises this exception when: - - * receiving or sending a frame that contains invalid data; - * receiving or sending an invalid sequence of frames. - - """ - - -class PayloadTooBig(WebSocketException): - """ - Raised when parsing a frame with a payload that exceeds the maximum size. - - The Sans-I/O layer uses this exception internally. It doesn't bubble up to - the I/O layer. - - The :meth:`~websockets.extensions.Extension.decode` method of extensions - must raise :exc:`PayloadTooBig` if decoding a frame would exceed the limit. - - """ - - def __init__( - self, - size_or_message: int | None | str, - max_size: int | None = None, - cur_size: int | None = None, - ) -> None: - if isinstance(size_or_message, str): - assert max_size is None - assert cur_size is None - warnings.warn( # deprecated in 14.0 - 2024-11-09 - "PayloadTooBig(message) is deprecated; " - "change to PayloadTooBig(size, max_size)", - DeprecationWarning, - ) - self.message: str | None = size_or_message - else: - self.message = None - self.size: int | None = size_or_message - assert max_size is not None - self.max_size: int = max_size - self.cur_size: int | None = None - self.set_current_size(cur_size) - - def __str__(self) -> str: - if self.message is not None: - return self.message - else: - message = "frame " - if self.size is not None: - message += f"with {self.size} bytes " - if self.cur_size is not None: - message += f"after reading {self.cur_size} bytes " - message += f"exceeds limit of {self.max_size} bytes" - return message - - def set_current_size(self, cur_size: int | None) -> None: - assert self.cur_size is None - if cur_size is not None: - self.max_size += cur_size - self.cur_size = cur_size - - -class InvalidState(WebSocketException, AssertionError): - """ - Raised when sending a frame is forbidden in the current state. - - Specifically, the Sans-I/O layer raises this exception when: - - * sending a data frame to a connection in a state other - :attr:`~websockets.protocol.State.OPEN`; - * sending a control frame to a connection in a state other than - :attr:`~websockets.protocol.State.OPEN` or - :attr:`~websockets.protocol.State.CLOSING`. - - """ - - -class ConcurrencyError(WebSocketException, RuntimeError): - """ - Raised when receiving or sending messages concurrently. - - WebSocket is a connection-oriented protocol. Reads must be serialized; so - must be writes. However, reading and writing concurrently is possible. - - """ - - -# At the bottom to break import cycles created by type annotations. -from . import frames, http11 # noqa: E402 - - -lazy_import( - globals(), - deprecated_aliases={ - # deprecated in 14.0 - 2024-11-09 - "AbortHandshake": ".legacy.exceptions", - "InvalidStatusCode": ".legacy.exceptions", - "RedirectHandshake": ".legacy.exceptions", - "WebSocketProtocolError": ".legacy.exceptions", - }, -) diff --git a/wip/SNIP/client/websockets/extensions/__init__.py b/wip/SNIP/client/websockets/extensions/__init__.py deleted file mode 100644 index 02838b9..0000000 --- a/wip/SNIP/client/websockets/extensions/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .base import * - - -__all__ = ["Extension", "ClientExtensionFactory", "ServerExtensionFactory"] diff --git a/wip/SNIP/client/websockets/extensions/__pycache__/__init__.cpython-313.pyc b/wip/SNIP/client/websockets/extensions/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index db48732d820e2fbb531d40bfc6bbea001626cc45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 272 zcmey&%ge>Uz`#(r@TZlX-=wL5f1|c1IW3>0t^fc YAD9^#8SgL%e_~_c_F-&fF9HPw08X?|TmS$7 diff --git a/wip/SNIP/client/websockets/extensions/__pycache__/__init__.cpython-314.pyc b/wip/SNIP/client/websockets/extensions/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index b4bd089a658b58f34084ff3248083653dad544bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 274 zcmdPql5=<`rk==iL%>&dE&8D}f8RB_@~T7ggR83r;O6 zOD#ed*JQfIl9X7SdW$_iJ~1aJK7J*`XOIhS*}GW9cvKdo7G>t=`3J|8XBNZ+`*{Y$ zB!i8QDNju*&QH!xEh&ykh1p#k6Ca2KczG$)vkz#fq?w1ZHnA6hf&u`94o;N- diff --git a/wip/SNIP/client/websockets/extensions/__pycache__/base.cpython-313.pyc b/wip/SNIP/client/websockets/extensions/__pycache__/base.cpython-313.pyc deleted file mode 100644 index c0edadb44bdff393e4168c63f9ea9f6eabee5d75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3673 zcmey&%ge>Uz`#(r@A(&l?A(+FI31Vsy z7g&r_iXoT_F2)TOy##5}WW2?ln3tDdl30?NpI5BOc#9)AwXifb zFFExk$W%?nTdZzHiMgqoOt*MlD@s!HiZk=`{1S6hZwbM<0f`_LC8VpiQj5^VlR@r*VP*(}@v{>+oMIS?7=sy#n1UILm{l2q znTl9~nPXUkS=1RA7~~j&S%ca17=yV$p^?Rs#Z<%=%pJ@j#gN5P#2(C9#F5Ub$y3D1 zz`*d5hk=2iN|wthu{c#BIVZ8WSRpOHNFf#GuVOtet|E2@28LTKd5O8HexTsUO{|D7 z&a6sJ1_cMmRg4S_3?R(Rz`*#K5A3{XkT@8pGHNnbUE*>{P0mkARY=TJ$jnR5&&|wB zS4abekscS9V^Mms6_{JO(@HW@ z;pV4SB&VjP6e}boz}%3KpvT3vlJS_^L>Pfq`KqgC-l;m?AcifuQiM;>ye`QK(Vy%g;-_#a0B4 zf3U133zAA%kaCb7P0m{!@$sPK93Ov+D?UCqKczG$H9r0pPkek~X<`mkhCe<&EwiY& zBquX3H7`Ft{uX%pD|=R7b0%6-N13=9nI3?G@8Svfv%F>p&bg+MYW$$^p{2!q(Ti-%k;r_#)v6ompH!2Ro(smL~QRTMtvIEmC?FKuWL4pv(^n0uTn-hBFuv6&M*9xB`mu zlT(X}k;9@WH90l2EESZc@^cjs!GV%DAr7`ufLA@m3b^!P4MtEQo0y!OT2PXj5|1c; ztQ34Qi%S&p(-aDdQp+;)ON(*>rstPr zg6mmu*$FO#!Tv}EM_^)6YMy!t)V9Q=9FTr+X|j?T9C^3cz>14+3Bw$OtD1p!0!Mm{sWm8-twT2X+vPgMmxkkjIWTGmjY_7Aq0rd#3A_#MX6b-$tB3n z$V@|a2};QXwis0WfJ!!~qi|;ZB5hC(1jVlctO+58$g72=sfe;$7kjH(54-X-kh!45 z-N5ig3o~br5^VWhM*&_4#A6CNHSzlU7<%T9g_eUjz!!Tl~rS zIXS7x;C_!@Vp4JusBdzW@Kc%%b@g>LFFlf)O`lCPizd_K8y|Q k-`P|dl|C}4GAex!U{nJ!)fkn&2QkVqDt%&*V}!Jb0DZgC9smFU diff --git a/wip/SNIP/client/websockets/extensions/__pycache__/base.cpython-314.pyc b/wip/SNIP/client/websockets/extensions/__pycache__/base.cpython-314.pyc deleted file mode 100644 index f46b395a66a263a11af953b4b4a2668944780898..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4410 zcmdPqAVyCH55^Kkc@Ue)lgWd* zgjt>;jwy)Qlf{FzgcZzY@nrL0FJbrKDB+N2&|?VV@n|5 zP0dSA)nvTI>Q_J>596_wg44T|U91IK$ zRkB=8iN&c3$vKI|#R_TpMGC1fZx!otaTT#KFfiO=$piV2BR8=kzBscg)i0Tmfq?;p zK@J0{U}j)o{Hy|YT^K_glSK;yLkcqkgA79;V+LaoV-QmiGss;*O!~~q44P~&LD6%I zH4PL_MXU@A3`M*kr*LKFl_=CG_~qxN-eN0CEh#O^Tgjlwc#AD9zbH4cq!?tQ(k*Kj zs~C^Ug4CkS{5=2QnDWemm|#E8fS6=(0LPT4CKcx=XQ!4F#~^$XlayGTs#j2XizhxF z8nvnM@wdd_!LElaF3P~bP^<{{&36WVR;7<1VutGn5Ve5|OdUuC3w{O#Kr+ZTAO^^N zAk56b!26jGtUm@M4#pXbnv7MKxLi_`^HWk467v)?^OEy(GxO3Fz`>x$#pPI(UTnps zpr8N|u~P6vQvs5K#etQAZ(>DeZfUMUL1JZ2eqxFOC}tEg^AwUQOHx5m7X%JKsEH73 zQy?bmad8DDW)`PHOT1iGK-2BvvUuj z^tiZIGTsvQ%P;ZFEyzjDP0cGwO>r$M$}iGnzQt0Unv+%}0E^yYP%>6iQz;SwB`G$r z9jUhj5K#aT0S7*)xM*PbBFDh5bb&*$NSuLz0VU0W3Jq|YQvioD&NRoQ&!o(t$x_4* zas=2V;M67v;tDY^FlaIs34%pV_riz_}pH$SB` z2b6Jm;^PZT6LX+4{PFQ=nMK7VIhlE>dHM12x7g$3Q}UDJ<8Sf9vlXZ$jgK#q0l5wo zX}3h<0HzyuE{nlu;~7}^;=GBLAqeBff> zmblC!euG8+0}DT^(vA>tR{gF7iz%=n*o#pIaHPVEQ4{RND5xF*7ny8`!nA}vh@*rf zh*OzCldFgmRAw2W7oU(~NVhmMB^6PWDx`sn`%G}g1ed8rAP=FWX;5^5(=;fI8W^yr zX?;dz22Ey2dIqK9Uox<&DH@)U;Pp=|I5csj@mtE+Yb2DC&=wTxp#0jv@STAJlm|Fi zl`g0_d<1bIAq-0Kpp*~7AU5vQpUdS`nwgWLPynkRixo=p6^c{yQb07Qs3`{5h2UDK zBqOsJ5q_W&3%NLhdl82bpqdMu9~i;;0bFxHbAnKMYDs(%s6H<)i3dBS_?9M)pukiL z4iIQ|5kSf=C^1P&jmQSB5!t~pdW#L>o+52f2`QYIoSa%vlA02asKtd3 zF{tDQ#V-isjpIZGMg|71fTH~5)Z${~h%QP^PR%S!1r=BMxeAE5Myd87zPD09id|g# zu%;1E{fH&dtQ34Qi%S&p(-aDdQp+;)ON(h32rEYt37b_3HC>7NorBCLSj*Bo_Yz?w#1|ykbZDUy^os zT0nuU1}m?{3m{<_UzA#0ke^qa3QMYbIFc$3_23AHrdKtj^a@TXewv&`N+A1{K?JDz zR-_7If$D@Jbr4GrM3{jHP@(~A0;dQ_;S4GO!KI)H$o-%O6K3K3m4$;<>AMIlt$}r8 zFO@-tG%&zRk28s+&bb<1?C@7DMLi4yNJdcA|91L70 zA2~%y&*RI9&f}1}kn}tb(MOLw9+D5uo8U4jKP@e_NC&yxLaU(A90qQz!%Js{#Jm(x z!yX|(bfFQFuTYelm6}|F?2ODbWS5|ngkXz7#T}?5g*pmnQB!0JN)af7_Qui6mKCv-y`!F`Je`ix=RQkxE%Bb``fKd&^RAW^79>gfesPvgZjuBGx F0stUUz`#(r@A!q zV1{63Z)Pu+A{MY1i#MwmTM?TVdl9<=LojPFn>U9SXAviu&F;W}R3rkb;l2bLAZZSLNRo-Iv$t+1NO3Zl)3J^`k zTWo1XiMgr8nryd(TuKXaGLsWaQUek}VkN0Xw?sVi$`W%jQ{W!+i`)_mPAw`+ErJU`m1wfw z5)4Q!@=Yx+PE1dANlnX1EJ?j32Qd#r0;=hjLTX-OQch}oGT7Sqg4Cj1hyn2_FcTow z5j3F~>?$lK6tjThjN$+PmmCZX3@-&37#NblB2WUF+896AF@SPa3_}rPFhdbjFk=yO zFjEmr4671DFf%OO7O}}O1areu2`U}T6U+)qGg*v9?7_UjY#??KM=(n;yB;G#WiVeb z2S_H1sfaU}KbRB5F5(L2D&kIO(G(~W1-Vg?fq|h)kt-mzNEaHO3eW&mNQK3u9v9ay zAw+oTLc_~18Cpt!eaFhcz`)GF!1(z9xWou$O=ee zs2EiK1i?*1$Z0VIvsp5RGUTws#6lT@;I4s*F)+YwGD5Kll%Nobz$_((V2)tcP=+An zxB$yAFlaFtForS&fzl#W1V*_qnt@!w3F8N|hce{AeU#3q$yue(<(!|FmYH5!l&VmY zk*Yu_h4^VQxrG!-fzlK|0|Ub?*7BsxlH!$&MW7Vswvs`U`<7HuYHof>YJ6UPd~$wX zNoqw&d`V(HI*0&8STQIH6u{t?r;AmLM`b~3QD%Oge{f8BWy-7K0T@dIgoYIO5|o^D;}~<8Mh}luUXsIZXxz zhT^Xb3=9nnA0!xfWG`~7+)z-uz$5#BUvow5Wqzv*999?jt#0sZtnj_eZ+U^k@&do* z4Ss*lUf?$X%V=KZ*IyBInP2|`hyFz{d4XU52EW(^Iem!W z1%CYpS}s>O)Ng1yUExr>p=NW1L-~fP)fEn<8;V9(IOHDi3t#8gyUwq7M_T^6wC*Ko z-3=kvO#?2P23#-+xF{Xi;nL#+R-=2JUl*)K`;xTwM+RmYz8m7w*ToGliW^=KGyK5J zEW`IeNbI_h#zi5GkF3lxd?1}4_!tz`I#{l9$bM!3m9+32&cMLH2`h~Xz$I-kC?9|n zpk zYQ`!nsiOeZjF#AKt0Z6=(PeHiYHG4zORFmxi}XRM1r%HgMFya}$`K!5lv+@PD~Unm zfT$ITTcefoqoz7EsL@%80Fej)s?{*y0UT2PHBwFoZIyFc8!a%F1BN5rhvz zq7Jxb0J#Mr3}%6{KZpUUXLG1;F#5{$} zyyX1c%)E33aQ{n>i_0yf2$Uu=|FAPK_!WT)rz#Ops{+zI$;{7FNK4GjNlnp|xW$%V z05YZsRJPpWaSjRciT4k1_IGg&zQyhz5bx~o7g7ZB+b!m_%)DDHMa5-?kg^@*)FM!r z7J)oeqzx+0nR8O}Zn1#cEVo#bDoav}Z%M|x`UZqV#)tYj`}+n2xdsQjy2LyA_&a;w z;zGFS7Jq48a(-TMX>Mvtd`V(vP7yE2IM!59&#wp+&9_+HK>bLt1B}3=u_hb1I=RIP z_H~gZs5RsW(#8cU$a7Nj(n~UKF{f4}BQ=j8wGJD|=9JW15*Ssb9z-SvRO75<1UG8L z7m(-DgwBte88useg~??}{V!|`B9b#i=S$6$ zx-6`6Mcv_|utTTI17V5jRuio{Ty97yU6j(A7`TC>gX;qmC#NIh4Po)=HWO{G3u|2z z)>>h5S=gq-|RA`b7ctD;kbh1f1Y*ogs2v(&DIYHsid_WN}D+~5%au{=9HAMgl*SYDl8Q0g;3hXB_H1`bZHA7A(x1Vs9)I;%c2 zFmgIF-Vu_&pkQ`c$b3ie1s;drADBUczluR+{1H_r0S_656EY&6jLattcsy*FPuQ?} zGBSh8UU0So5ulU~uF*A^2dqYCOmqg9U?8`E+=PVTB{ymh3~OI6m=)Z<1Z5B;6F^L8 z4-8)FfW#mey$2S`fUW)w0`<`$8sKCoV-S`S9bQghHwP3ia1$U5q#i7mx)Np{xJG3P zWek#m=t3ru!wA%?4`yazK-3~u5s1EcG`zkEWrCRut>@W5HDfSyFcS+y9+N*`v@*;F zB$ZHE0qD`Gf|06xo4{ zaRw0%kODt7H@~Q|EHkyd$Q>l=0U|s>&3a~V9Z}>35&)T33>tM&P=Izn*dTpwY!v}S zW(lYwFo#qGJn-@!S4B`ZziwvTC2n0r&k|IY>tB-B|H#0sAan!N7d5>oZF)h<^aC@q zg3tposq11o7sYfwvN9_OflGKEP+12qhE*=|t1Jjz98_k%a+zQLhPd<$=NUm0Dkri$5m#KGa#>t`1@j7_3*tHxSU#|Uyl2^O2`aD6uS=Q} zDz8BW_!73|d<*$5@aREHZ778(ydixnjv`Rn0yiVT?F4W{L*gBr7C`9{+^AT|=%>j8 zX5C^*%FoY%40c3-A`hhI7IS7^2}EBcNDO3Zm1s(4a!IsfUZswLV_s#fLXCo7eqQP= zwxZOM(xSXuEFhjHQ&AKsp)ePh6lt=7Q^PMFaI>LU58SQ*w;q0RW#*MY^@G(H#e$L$ z3s^mvjU>tg8Xn0@%uS7tzr__FpPQdjnge3<#K#wwCgwn8_~YZ#GK-2!ax(K$^YY{4 zZ?VV6r{pKc#}|Qzs#rkEiVQ)1F$WR8AR-jxQqlPMV$jfga(rS*Nl|7}X-R5vd^|W; zfx{HskO2>*f(cM5SKPk)4(6 zgA{{;%12H&Mpmu|{L&v-L5vR^48pP>z+xY`7`Vi~a)U&_2QcZdLb}#q2PVUNoD2*M zpms61237-)*y=Onv4p}$9ziYPP?jK2-y5kU0x^RbVI?@Ie*lVnkSHGY@Dg8vA(RQO zUWS1IhxyFWpwa7($teu$2wTR`?h^imfav z44Uj!-ds+lnK>y6i3&NH#U%>)X$lDm8lVKEqfiE#Gto>)P$)>uEGkw=%P&$W&d*I% zfEA^n7JmV3BBof6i%XN~mK1a{0n50LA4nQ9V2Ev8)bExUR4=HZjWjSCqRDhi45|-J z%C9I3l$V%pu_YE1q~@g*ftuKwtVQXd6rTYiGC>4L(JeNxQ;Um11N-1XO>knrB^aLs z@_#&7Hnk+RsJJMXfq~&NsEBTYj1Gw~XlOOK-D6k0!NJ$h+sS)D%<2kZEfgu`X79?G=!BSW%V<37g66>&L)= zNSSD6vVqMs0Oh14JZ441b3LdXkjTrx5RI6BfcJ3oSbSUX*u@UE%Z`B|logL%jPRVs z&cFZ<8CKtoARdwKVD&wL%N^06ab1iU0L|@&GKDgSvV^jlfO08E)nzW%ijtzlKUQ0+&$W5$N zNJ`}@D9SI(Oi4{qD9dI}*KnZ+Oj^Yd~l6%uoD^2<|G6f*M^G7|Gr ziZc?kQx%FplfA_ypwhz451da!icmAECTkHWREj`Daks?deH|m>!#(|6{KMm&JVS!x z!yJ7=U4x6jH8p4~N0XxnG%t3G6YeLlsc424rGTnGR&a?{3~Ew?O8|I_O*lRk61>R8 zKoJ82!)s760IE+K7(R$FXy{&Gk-MR;10pqbFR)16u(Z9vBK25Ywv!vwV&cCbtbdtX z{~o{Q9Wjj+BA3PVE(qw|;1}tvy`gJ(NkHp~nDz>n%VHK61S}r#3SJO3yv%Dzv|`=M zyt+5|RWI@zq8ffh0yKSbM^k4-=xV7;0&3`%VAXezUt&h+%-9>6x|amhZU~B9kutn2 zXo%I2FKi6L>I+IP3+Y_o(fR1f$RHv2!HUI9YkvJ}|JdvO$U} zaDs*s(Bgyfa~XKzg76eEY`Qm?6UIWNgSn8Vin(D^#mH)cd4k!&Q^TCW97SB|ESkJU zpbloH1|tJQ;w)wc1}^6u&`iBH%}6XuRY)vW zNJ%YDF3L=TB>RNo)MU^)2L*FILr~2cKQ`R_s_B1y%|HMX6<}c_j)EHIRa=v>4P2gifj| zz$R7oz-B>9C%k4s)DSicVrM*BtHMgbH?bl!w=`D)G;afnI8Z=A)T$RN_(YkTD-`Es zrhwcCX{Uf42J&cTo&sntS4SZ!wWK^XHBZ4pAu%sS!O#@N?L^rJ@dLT`>42gQsa%Id zR|y(98$w zIt)+&0&eR(0~a8n%t=xVpjl4D8VfcChG1s!8Vgr z!;k_o1F;4JpBcz)Bi1BA28LkvP*!lu1Eq};%z>~gLI7!fhC&W#s2p4#Mi?Ni&u~D@ zYefX)fZB9mHK5fBzsyNltpF>z^tibEz$+Gt6c`v7e(6I}EPCoz$So}{QAkR~NTMOX z^dV`EXdU3;nJOPx48s>FK&;5iF9FRVfJ+>36zeDyr11ClXhT>jG<6nY7Q2nBS-Uh`B z=Dt^rf-eLHUo{H8BP4S{*6*^A{{v=~=<^vO>py_93W?{Y$49vWIAGjE_T^T{E546B*5Fo1$Af%$v zRSr2w~sIv*V&jtTiz-Rnv7L4(4Hf-NvDA5L4mr0 ztl(}G)(A2KHL5`VfR2|5rKgs}LrsA+z>04f5{N2HElHqAGX+Q57cK??#Tz^V*Lk!r z@@S(n)GzX=gGE4$&z!8F`2bevbbT_Yuwj4#P?rK+@PKBz;uxkP6)T|(CXB(1QVdxf zp-jRIAUi>NVA&s(K0*9M3FKyfFjEMQc<@Fmo`AB~vL2XbL!wC66_a zEd=Z0C%An=3`j!*DWJj$?w?@z5JD%fNtLRUVtabSNlli&}u-1L6fJ-jw_%jKRLCySfMDjxFA2TI2ARI1%byRK?CRTMOO--Vl*)? zIaQB~3luvbSAj7o#Ee198ABKt80;98859_%GJppPWEmLp1+gtZ31Pw}YURPeP{uHo zRe`}*9sp202F{F02+BJfZ-bb!2}2)sUr(pQwD&eBkz<3TX)gwqjtQU^ zFKp6797jyuG9xdB^FYP97b641j|PS>VhlX|SNJ?Gnz&!)^SI9Kagp2Oj*!v?Rf`=W z`z3cu?%=+l>~dMi^#YIU7ZC$l+<`)8*D*^F}z!N0ku^BK6tSk;T zocc=&GOGbys~-&-Jc7-VfRoiP$P7wxNfF8n3S_P$8a!$XX8)3bt^bdPuZxHGtYbCV z!0EE6A7pPAs8uVBsXP{J5O_jBle;J$q%;9UB!Y+}5CK}X4xSr&4Gxra(vW7o&(`v&@sHBZE!=^@P@794P)~UymG9@ z;Q0_aR^tzB462$R*g-4~21V@;oFEn#gQE2ZZV(GH9|B@Q=0oIIjlVcBaO<7&60ptM^M8&9G^W`iq}V5VSZ1IAz$OGbT0UC$s zLP2U#Mq)uRxF4&Km!Ah($&X^X4i~8Bo(x_14c_*lkXf9kUZRkkk(igBnu4>M1WhC$ zRgy{!40)hAZm{=J38dL~R0%K-sk&6bs4g{`An6eli<(SDji50~L3mvr4;_#y26Z2y z#cWYCNR2U}tshvb$0wlj6tu7kR6BlPVCH9gq^Ny?NA?3V1E;d)C^8&Xv^Y3VTD;>bY<|cXI5r@w(ktgJZv99 z#1{?*F{v3^mqk_Y@Mzso)?HD!K>s5%6EEKfW)@AZj|?ngTpe7XbwOemSVY0O0+idq zfe#`;4Ic(j!v|CoPGul|08)-2m;-rO5tSXxi5P}NmI>y94M_&G7jdSuXmS@xFfcG= zegF+V2D30Qa6vZZj1E46a^~pZV-fmb%WxQcr2D`js38g(K*%hD?}tNcWm_q@D;t)^`37St!%}dKKO3utnhns`jF2txQs7Veg@jw_<>Vca9Lf{!< zibhRI87oB^W(b9hm9k+SD@7h6U`YlIT(E)Kq9CV(hXp`Y2Z+N!tx?s8gdEWN5U`n` z5!Rwo1_lPC(b6K&{8UjnsG0(;dLeaC6UR_1s5hRPm{JvnKXHOm1SC16=qMzW;vORf zr$$(FCN)Kqy{H1@VsLXE+)@WO(m~4|z%}$OIsEo&Lc4yr^cT$qjnskC6)1zjW=o-C zsgQb%;8-dz69Yr>3h=Pp2Pr~hsW$}F@Q$?-97E+`;8D8Bt$9t)2RguNN9+LW6%M%@ zdOlY{ChPzC}g76t~;!b8Yb&~;1<3`1fh5HyAdUzPzHg$icQVWt=XL&EGGSEm;<9wgA*A zNEAvD1CL?ku~C0L&~h0c2GF7s?C!{8^OwOc6UqkO=!3^~(ZVptKt>8f*^fU}K0TOyC7xfk>l>g{7&*C0GX$VdHBM$zt%V zWnvz3|0}P!WZ(uCi|Rq;Tmz^$0>ztx0%Rl+(jD?3P!u6XrojHkJ%nh@#K2J03bLt< zfq|h)s5m>bpa5ljFr=s*BqI+kS5t7zWG0A!F&bBW)LxdAgI2=yI|ag4e#f#w7a7=u|YnNddG*vK4tgEU(arTmB+c>^Ut*vK0<=l}t)$N`V$tfJf`AByg2A zC`10Bxi~~IR%Hn(ra+rGbQB;YWEujr9YaR}!p1fw5v$2kGz(O7tHY-cAk$LtIS1$( z5cnJhxL8ymq#Dy$NIgufu{cy~DuZ@^q?MMG7Ny3=7lE2Q;H^!!_>=QJpA%RttQS63*ImiOB2dt`JK>RNTAj`puK#UJ5 gtm05b;*20gUqSqzpFBkvr0u~=m<|dFIhnEm0EBX(4gdfE diff --git a/wip/SNIP/client/websockets/extensions/__pycache__/permessage_deflate.cpython-314.pyc b/wip/SNIP/client/websockets/extensions/__pycache__/permessage_deflate.cpython-314.pyc deleted file mode 100644 index 55dd607621f3798a9c491b776faeec6a9b7809c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21921 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 diff --git a/wip/SNIP/client/websockets/extensions/base.py b/wip/SNIP/client/websockets/extensions/base.py deleted file mode 100644 index 2fdc59f..0000000 --- a/wip/SNIP/client/websockets/extensions/base.py +++ /dev/null @@ -1,123 +0,0 @@ -from __future__ import annotations - -from collections.abc import Sequence - -from ..frames import Frame -from ..typing import ExtensionName, ExtensionParameter - - -__all__ = ["Extension", "ClientExtensionFactory", "ServerExtensionFactory"] - - -class Extension: - """ - Base class for extensions. - - """ - - name: ExtensionName - """Extension identifier.""" - - def decode(self, frame: Frame, *, max_size: int | None = None) -> Frame: - """ - Decode an incoming frame. - - Args: - frame: Incoming frame. - max_size: Maximum payload size in bytes. - - Returns: - Decoded frame. - - Raises: - PayloadTooBig: If decoding the payload exceeds ``max_size``. - - """ - raise NotImplementedError - - def encode(self, frame: Frame) -> Frame: - """ - Encode an outgoing frame. - - Args: - frame: Outgoing frame. - - Returns: - Encoded frame. - - """ - raise NotImplementedError - - -class ClientExtensionFactory: - """ - Base class for client-side extension factories. - - """ - - name: ExtensionName - """Extension identifier.""" - - def get_request_params(self) -> Sequence[ExtensionParameter]: - """ - Build parameters to send to the server for this extension. - - Returns: - Parameters to send to the server. - - """ - raise NotImplementedError - - def process_response_params( - self, - params: Sequence[ExtensionParameter], - accepted_extensions: Sequence[Extension], - ) -> Extension: - """ - Process parameters received from the server. - - Args: - params: Parameters received from the server for this extension. - accepted_extensions: List of previously accepted extensions. - - Returns: - An extension instance. - - Raises: - NegotiationError: If parameters aren't acceptable. - - """ - raise NotImplementedError - - -class ServerExtensionFactory: - """ - Base class for server-side extension factories. - - """ - - name: ExtensionName - """Extension identifier.""" - - def process_request_params( - self, - params: Sequence[ExtensionParameter], - accepted_extensions: Sequence[Extension], - ) -> tuple[list[ExtensionParameter], Extension]: - """ - Process parameters received from the client. - - Args: - params: Parameters received from the client for this extension. - accepted_extensions: List of previously accepted extensions. - - Returns: - To accept the offer, parameters to send to the client for this - extension and an extension instance. - - Raises: - NegotiationError: To reject the offer, if parameters received from - the client aren't acceptable. - - """ - raise NotImplementedError diff --git a/wip/SNIP/client/websockets/extensions/permessage_deflate.py b/wip/SNIP/client/websockets/extensions/permessage_deflate.py deleted file mode 100644 index 7e9e7a5..0000000 --- a/wip/SNIP/client/websockets/extensions/permessage_deflate.py +++ /dev/null @@ -1,697 +0,0 @@ -from __future__ import annotations - -import zlib -from collections.abc import Sequence -from typing import Any, Literal - -from .. import frames -from ..exceptions import ( - DuplicateParameter, - InvalidParameterName, - InvalidParameterValue, - NegotiationError, - PayloadTooBig, - ProtocolError, -) -from ..typing import ExtensionName, ExtensionParameter -from .base import ClientExtensionFactory, Extension, ServerExtensionFactory - - -__all__ = [ - "PerMessageDeflate", - "ClientPerMessageDeflateFactory", - "enable_client_permessage_deflate", - "ServerPerMessageDeflateFactory", - "enable_server_permessage_deflate", -] - -_EMPTY_UNCOMPRESSED_BLOCK = b"\x00\x00\xff\xff" - -_MAX_WINDOW_BITS_VALUES = [str(bits) for bits in range(8, 16)] - - -class PerMessageDeflate(Extension): - """ - Per-Message Deflate extension. - - """ - - name = ExtensionName("permessage-deflate") - - def __init__( - self, - remote_no_context_takeover: bool, - local_no_context_takeover: bool, - remote_max_window_bits: int, - local_max_window_bits: int, - compress_settings: dict[Any, Any] | None = None, - ) -> None: - """ - Configure the Per-Message Deflate extension. - - """ - if compress_settings is None: - compress_settings = {} - - assert remote_no_context_takeover in [False, True] - assert local_no_context_takeover in [False, True] - assert 8 <= remote_max_window_bits <= 15 - assert 8 <= local_max_window_bits <= 15 - assert "wbits" not in compress_settings - - self.remote_no_context_takeover = remote_no_context_takeover - self.local_no_context_takeover = local_no_context_takeover - self.remote_max_window_bits = remote_max_window_bits - self.local_max_window_bits = local_max_window_bits - self.compress_settings = compress_settings - - if not self.remote_no_context_takeover: - self.decoder = zlib.decompressobj(wbits=-self.remote_max_window_bits) - - if not self.local_no_context_takeover: - self.encoder = zlib.compressobj( - wbits=-self.local_max_window_bits, - **self.compress_settings, - ) - - # To handle continuation frames properly, we must keep track of - # whether that initial frame was encoded. - self.decode_cont_data = False - # There's no need for self.encode_cont_data because we always encode - # outgoing frames, so it would always be True. - - def __repr__(self) -> str: - return ( - f"PerMessageDeflate(" - f"remote_no_context_takeover={self.remote_no_context_takeover}, " - f"local_no_context_takeover={self.local_no_context_takeover}, " - f"remote_max_window_bits={self.remote_max_window_bits}, " - f"local_max_window_bits={self.local_max_window_bits})" - ) - - def decode( - self, - frame: frames.Frame, - *, - max_size: int | None = None, - ) -> frames.Frame: - """ - Decode an incoming frame. - - """ - # Skip control frames. - if frame.opcode in frames.CTRL_OPCODES: - return frame - - # Handle continuation data frames: - # - skip if the message isn't encoded - # - reset "decode continuation data" flag if it's a final frame - if frame.opcode is frames.OP_CONT: - if not self.decode_cont_data: - return frame - if frame.fin: - self.decode_cont_data = False - - # Handle text and binary data frames: - # - skip if the message isn't encoded - # - unset the rsv1 flag on the first frame of a compressed message - # - set "decode continuation data" flag if it's a non-final frame - else: - if not frame.rsv1: - return frame - if not frame.fin: - self.decode_cont_data = True - - # Re-initialize per-message decoder. - if self.remote_no_context_takeover: - self.decoder = zlib.decompressobj(wbits=-self.remote_max_window_bits) - - # Uncompress data. Protect against zip bombs by preventing zlib from - # decompressing more than max_length bytes (except when the limit is - # disabled with max_size = None). - if frame.fin and len(frame.data) < 2044: - # Profiling shows that appending four bytes, which makes a copy, is - # faster than calling decompress() again when data is less than 2kB. - data = bytes(frame.data) + _EMPTY_UNCOMPRESSED_BLOCK - else: - data = frame.data - max_length = 0 if max_size is None else max_size - try: - data = self.decoder.decompress(data, max_length) - if self.decoder.unconsumed_tail: - assert max_size is not None # help mypy - raise PayloadTooBig(None, max_size) - if frame.fin and len(frame.data) >= 2044: - # This cannot generate additional data. - self.decoder.decompress(_EMPTY_UNCOMPRESSED_BLOCK) - except zlib.error as exc: - raise ProtocolError("decompression failed") from exc - - # Allow garbage collection of the decoder if it won't be reused. - if frame.fin and self.remote_no_context_takeover: - del self.decoder - - return frames.Frame( - frame.opcode, - data, - frame.fin, - # Unset the rsv1 flag on the first frame of a compressed message. - False, - frame.rsv2, - frame.rsv3, - ) - - def encode(self, frame: frames.Frame) -> frames.Frame: - """ - Encode an outgoing frame. - - """ - # Skip control frames. - if frame.opcode in frames.CTRL_OPCODES: - return frame - - # Since we always encode messages, there's no "encode continuation - # data" flag similar to "decode continuation data" at this time. - - if frame.opcode is not frames.OP_CONT: - # Re-initialize per-message decoder. - if self.local_no_context_takeover: - self.encoder = zlib.compressobj( - wbits=-self.local_max_window_bits, - **self.compress_settings, - ) - - # Compress data. - data = self.encoder.compress(frame.data) + self.encoder.flush(zlib.Z_SYNC_FLUSH) - if frame.fin: - # Sync flush generates between 5 or 6 bytes, ending with the bytes - # 0x00 0x00 0xff 0xff, which must be removed. - assert data[-4:] == _EMPTY_UNCOMPRESSED_BLOCK - # Making a copy is faster than memoryview(a)[:-4] until 2kB. - if len(data) < 2048: - data = data[:-4] - else: - data = memoryview(data)[:-4] - - # Allow garbage collection of the encoder if it won't be reused. - if frame.fin and self.local_no_context_takeover: - del self.encoder - - return frames.Frame( - frame.opcode, - data, - frame.fin, - # Set the rsv1 flag on the first frame of a compressed message. - frame.opcode is not frames.OP_CONT, - frame.rsv2, - frame.rsv3, - ) - - -def _build_parameters( - server_no_context_takeover: bool, - client_no_context_takeover: bool, - server_max_window_bits: int | None, - client_max_window_bits: int | Literal[True] | None, -) -> list[ExtensionParameter]: - """ - Build a list of ``(name, value)`` pairs for some compression parameters. - - """ - params: list[ExtensionParameter] = [] - if server_no_context_takeover: - params.append(("server_no_context_takeover", None)) - if client_no_context_takeover: - params.append(("client_no_context_takeover", None)) - if server_max_window_bits: - params.append(("server_max_window_bits", str(server_max_window_bits))) - if client_max_window_bits is True: # only in handshake requests - params.append(("client_max_window_bits", None)) - elif client_max_window_bits: - params.append(("client_max_window_bits", str(client_max_window_bits))) - return params - - -def _extract_parameters( - params: Sequence[ExtensionParameter], *, is_server: bool -) -> tuple[bool, bool, int | None, int | Literal[True] | None]: - """ - Extract compression parameters from a list of ``(name, value)`` pairs. - - If ``is_server`` is :obj:`True`, ``client_max_window_bits`` may be - provided without a value. This is only allowed in handshake requests. - - """ - server_no_context_takeover: bool = False - client_no_context_takeover: bool = False - server_max_window_bits: int | None = None - client_max_window_bits: int | Literal[True] | None = None - - for name, value in params: - if name == "server_no_context_takeover": - if server_no_context_takeover: - raise DuplicateParameter(name) - if value is None: - server_no_context_takeover = True - else: - raise InvalidParameterValue(name, value) - - elif name == "client_no_context_takeover": - if client_no_context_takeover: - raise DuplicateParameter(name) - if value is None: - client_no_context_takeover = True - else: - raise InvalidParameterValue(name, value) - - elif name == "server_max_window_bits": - if server_max_window_bits is not None: - raise DuplicateParameter(name) - if value in _MAX_WINDOW_BITS_VALUES: - server_max_window_bits = int(value) - else: - raise InvalidParameterValue(name, value) - - elif name == "client_max_window_bits": - if client_max_window_bits is not None: - raise DuplicateParameter(name) - if is_server and value is None: # only in handshake requests - client_max_window_bits = True - elif value in _MAX_WINDOW_BITS_VALUES: - client_max_window_bits = int(value) - else: - raise InvalidParameterValue(name, value) - - else: - raise InvalidParameterName(name) - - return ( - server_no_context_takeover, - client_no_context_takeover, - server_max_window_bits, - client_max_window_bits, - ) - - -class ClientPerMessageDeflateFactory(ClientExtensionFactory): - """ - Client-side extension factory for the Per-Message Deflate extension. - - Parameters behave as described in `section 7.1 of RFC 7692`_. - - .. _section 7.1 of RFC 7692: https://datatracker.ietf.org/doc/html/rfc7692#section-7.1 - - Set them to :obj:`True` to include them in the negotiation offer without a - value or to an integer value to include them with this value. - - Args: - server_no_context_takeover: Prevent server from using context takeover. - client_no_context_takeover: Prevent client from using context takeover. - server_max_window_bits: Maximum size of the server's LZ77 sliding window - in bits, between 8 and 15. - client_max_window_bits: Maximum size of the client's LZ77 sliding window - in bits, between 8 and 15, or :obj:`True` to indicate support without - setting a limit. - compress_settings: Additional keyword arguments for :func:`zlib.compressobj`, - excluding ``wbits``. - - """ - - name = ExtensionName("permessage-deflate") - - def __init__( - self, - server_no_context_takeover: bool = False, - client_no_context_takeover: bool = False, - server_max_window_bits: int | None = None, - client_max_window_bits: int | Literal[True] | None = True, - compress_settings: dict[str, Any] | None = None, - ) -> None: - """ - Configure the Per-Message Deflate extension factory. - - """ - if not (server_max_window_bits is None or 8 <= server_max_window_bits <= 15): - raise ValueError("server_max_window_bits must be between 8 and 15") - if not ( - client_max_window_bits is None - or client_max_window_bits is True - or 8 <= client_max_window_bits <= 15 - ): - raise ValueError("client_max_window_bits must be between 8 and 15") - if compress_settings is not None and "wbits" in compress_settings: - raise ValueError( - "compress_settings must not include wbits, " - "set client_max_window_bits instead" - ) - - self.server_no_context_takeover = server_no_context_takeover - self.client_no_context_takeover = client_no_context_takeover - self.server_max_window_bits = server_max_window_bits - self.client_max_window_bits = client_max_window_bits - self.compress_settings = compress_settings - - def get_request_params(self) -> Sequence[ExtensionParameter]: - """ - Build request parameters. - - """ - return _build_parameters( - self.server_no_context_takeover, - self.client_no_context_takeover, - self.server_max_window_bits, - self.client_max_window_bits, - ) - - def process_response_params( - self, - params: Sequence[ExtensionParameter], - accepted_extensions: Sequence[Extension], - ) -> PerMessageDeflate: - """ - Process response parameters. - - Return an extension instance. - - """ - if any(other.name == self.name for other in accepted_extensions): - raise NegotiationError(f"received duplicate {self.name}") - - # Request parameters are available in instance variables. - - # Load response parameters in local variables. - ( - server_no_context_takeover, - client_no_context_takeover, - server_max_window_bits, - client_max_window_bits, - ) = _extract_parameters(params, is_server=False) - - # After comparing the request and the response, the final - # configuration must be available in the local variables. - - # server_no_context_takeover - # - # Req. Resp. Result - # ------ ------ -------------------------------------------------- - # False False False - # False True True - # True False Error! - # True True True - - if self.server_no_context_takeover: - if not server_no_context_takeover: - raise NegotiationError("expected server_no_context_takeover") - - # client_no_context_takeover - # - # Req. Resp. Result - # ------ ------ -------------------------------------------------- - # False False False - # False True True - # True False True - must change value - # True True True - - if self.client_no_context_takeover: - if not client_no_context_takeover: - client_no_context_takeover = True - - # server_max_window_bits - - # Req. Resp. Result - # ------ ------ -------------------------------------------------- - # None None None - # None 8≤M≤15 M - # 8≤N≤15 None Error! - # 8≤N≤15 8≤M≤N M - # 8≤N≤15 N self.server_max_window_bits: - raise NegotiationError("unsupported server_max_window_bits") - - # client_max_window_bits - - # Req. Resp. Result - # ------ ------ -------------------------------------------------- - # None None None - # None 8≤M≤15 Error! - # True None None - # True 8≤M≤15 M - # 8≤N≤15 None N - must change value - # 8≤N≤15 8≤M≤N M - # 8≤N≤15 N self.client_max_window_bits: - raise NegotiationError("unsupported client_max_window_bits") - - return PerMessageDeflate( - server_no_context_takeover, # remote_no_context_takeover - client_no_context_takeover, # local_no_context_takeover - server_max_window_bits or 15, # remote_max_window_bits - client_max_window_bits or 15, # local_max_window_bits - self.compress_settings, - ) - - -def enable_client_permessage_deflate( - extensions: Sequence[ClientExtensionFactory] | None, -) -> Sequence[ClientExtensionFactory]: - """ - Enable Per-Message Deflate with default settings in client extensions. - - If the extension is already present, perhaps with non-default settings, - the configuration isn't changed. - - """ - if extensions is None: - extensions = [] - if not any( - extension_factory.name == ClientPerMessageDeflateFactory.name - for extension_factory in extensions - ): - extensions = list(extensions) + [ - ClientPerMessageDeflateFactory( - compress_settings={"memLevel": 5}, - ) - ] - return extensions - - -class ServerPerMessageDeflateFactory(ServerExtensionFactory): - """ - Server-side extension factory for the Per-Message Deflate extension. - - Parameters behave as described in `section 7.1 of RFC 7692`_. - - .. _section 7.1 of RFC 7692: https://datatracker.ietf.org/doc/html/rfc7692#section-7.1 - - Set them to :obj:`True` to include them in the negotiation offer without a - value or to an integer value to include them with this value. - - Args: - server_no_context_takeover: Prevent server from using context takeover. - client_no_context_takeover: Prevent client from using context takeover. - server_max_window_bits: Maximum size of the server's LZ77 sliding window - in bits, between 8 and 15. - client_max_window_bits: Maximum size of the client's LZ77 sliding window - in bits, between 8 and 15. - compress_settings: Additional keyword arguments for :func:`zlib.compressobj`, - excluding ``wbits``. - require_client_max_window_bits: Do not enable compression at all if - client doesn't advertise support for ``client_max_window_bits``; - the default behavior is to enable compression without enforcing - ``client_max_window_bits``. - - """ - - name = ExtensionName("permessage-deflate") - - def __init__( - self, - server_no_context_takeover: bool = False, - client_no_context_takeover: bool = False, - server_max_window_bits: int | None = None, - client_max_window_bits: int | None = None, - compress_settings: dict[str, Any] | None = None, - require_client_max_window_bits: bool = False, - ) -> None: - """ - Configure the Per-Message Deflate extension factory. - - """ - if not (server_max_window_bits is None or 8 <= server_max_window_bits <= 15): - raise ValueError("server_max_window_bits must be between 8 and 15") - if not (client_max_window_bits is None or 8 <= client_max_window_bits <= 15): - raise ValueError("client_max_window_bits must be between 8 and 15") - if compress_settings is not None and "wbits" in compress_settings: - raise ValueError( - "compress_settings must not include wbits, " - "set server_max_window_bits instead" - ) - if client_max_window_bits is None and require_client_max_window_bits: - raise ValueError( - "require_client_max_window_bits is enabled, " - "but client_max_window_bits isn't configured" - ) - - self.server_no_context_takeover = server_no_context_takeover - self.client_no_context_takeover = client_no_context_takeover - self.server_max_window_bits = server_max_window_bits - self.client_max_window_bits = client_max_window_bits - self.compress_settings = compress_settings - self.require_client_max_window_bits = require_client_max_window_bits - - def process_request_params( - self, - params: Sequence[ExtensionParameter], - accepted_extensions: Sequence[Extension], - ) -> tuple[list[ExtensionParameter], PerMessageDeflate]: - """ - Process request parameters. - - Return response params and an extension instance. - - """ - if any(other.name == self.name for other in accepted_extensions): - raise NegotiationError(f"skipped duplicate {self.name}") - - # Load request parameters in local variables. - ( - server_no_context_takeover, - client_no_context_takeover, - server_max_window_bits, - client_max_window_bits, - ) = _extract_parameters(params, is_server=True) - - # Configuration parameters are available in instance variables. - - # After comparing the request and the configuration, the response must - # be available in the local variables. - - # server_no_context_takeover - # - # Config Req. Resp. - # ------ ------ -------------------------------------------------- - # False False False - # False True True - # True False True - must change value to True - # True True True - - if self.server_no_context_takeover: - if not server_no_context_takeover: - server_no_context_takeover = True - - # client_no_context_takeover - # - # Config Req. Resp. - # ------ ------ -------------------------------------------------- - # False False False - # False True True (or False) - # True False True - must change value to True - # True True True (or False) - - if self.client_no_context_takeover: - if not client_no_context_takeover: - client_no_context_takeover = True - - # server_max_window_bits - - # Config Req. Resp. - # ------ ------ -------------------------------------------------- - # None None None - # None 8≤M≤15 M - # 8≤N≤15 None N - must change value - # 8≤N≤15 8≤M≤N M - # 8≤N≤15 N self.server_max_window_bits: - server_max_window_bits = self.server_max_window_bits - - # client_max_window_bits - - # Config Req. Resp. - # ------ ------ -------------------------------------------------- - # None None None - # None True None - must change value - # None 8≤M≤15 M (or None) - # 8≤N≤15 None None or Error! - # 8≤N≤15 True N - must change value - # 8≤N≤15 8≤M≤N M (or None) - # 8≤N≤15 N Sequence[ServerExtensionFactory]: - """ - Enable Per-Message Deflate with default settings in server extensions. - - If the extension is already present, perhaps with non-default settings, - the configuration isn't changed. - - """ - if extensions is None: - extensions = [] - if not any( - ext_factory.name == ServerPerMessageDeflateFactory.name - for ext_factory in extensions - ): - extensions = list(extensions) + [ - ServerPerMessageDeflateFactory( - server_max_window_bits=12, - client_max_window_bits=12, - compress_settings={"memLevel": 5}, - ) - ] - return extensions diff --git a/wip/SNIP/client/websockets/frames.py b/wip/SNIP/client/websockets/frames.py deleted file mode 100644 index ab0869d..0000000 --- a/wip/SNIP/client/websockets/frames.py +++ /dev/null @@ -1,430 +0,0 @@ -from __future__ import annotations - -import dataclasses -import enum -import io -import os -import secrets -import struct -from collections.abc import Generator, Sequence -from typing import Callable, Union - -from .exceptions import PayloadTooBig, ProtocolError - - -try: - from .speedups import apply_mask -except ImportError: - from .utils import apply_mask - - -__all__ = [ - "Opcode", - "OP_CONT", - "OP_TEXT", - "OP_BINARY", - "OP_CLOSE", - "OP_PING", - "OP_PONG", - "DATA_OPCODES", - "CTRL_OPCODES", - "CloseCode", - "Frame", - "Close", -] - - -class Opcode(enum.IntEnum): - """Opcode values for WebSocket frames.""" - - CONT, TEXT, BINARY = 0x00, 0x01, 0x02 - CLOSE, PING, PONG = 0x08, 0x09, 0x0A - - -OP_CONT = Opcode.CONT -OP_TEXT = Opcode.TEXT -OP_BINARY = Opcode.BINARY -OP_CLOSE = Opcode.CLOSE -OP_PING = Opcode.PING -OP_PONG = Opcode.PONG - -DATA_OPCODES = OP_CONT, OP_TEXT, OP_BINARY -CTRL_OPCODES = OP_CLOSE, OP_PING, OP_PONG - - -class CloseCode(enum.IntEnum): - """Close code values for WebSocket close frames.""" - - NORMAL_CLOSURE = 1000 - GOING_AWAY = 1001 - PROTOCOL_ERROR = 1002 - UNSUPPORTED_DATA = 1003 - # 1004 is reserved - NO_STATUS_RCVD = 1005 - ABNORMAL_CLOSURE = 1006 - INVALID_DATA = 1007 - POLICY_VIOLATION = 1008 - MESSAGE_TOO_BIG = 1009 - MANDATORY_EXTENSION = 1010 - INTERNAL_ERROR = 1011 - SERVICE_RESTART = 1012 - TRY_AGAIN_LATER = 1013 - BAD_GATEWAY = 1014 - TLS_HANDSHAKE = 1015 - - -# See https://www.iana.org/assignments/websocket/websocket.xhtml -CLOSE_CODE_EXPLANATIONS: dict[int, str] = { - CloseCode.NORMAL_CLOSURE: "OK", - CloseCode.GOING_AWAY: "going away", - CloseCode.PROTOCOL_ERROR: "protocol error", - CloseCode.UNSUPPORTED_DATA: "unsupported data", - CloseCode.NO_STATUS_RCVD: "no status received [internal]", - CloseCode.ABNORMAL_CLOSURE: "abnormal closure [internal]", - CloseCode.INVALID_DATA: "invalid frame payload data", - CloseCode.POLICY_VIOLATION: "policy violation", - CloseCode.MESSAGE_TOO_BIG: "message too big", - CloseCode.MANDATORY_EXTENSION: "mandatory extension", - CloseCode.INTERNAL_ERROR: "internal error", - CloseCode.SERVICE_RESTART: "service restart", - CloseCode.TRY_AGAIN_LATER: "try again later", - CloseCode.BAD_GATEWAY: "bad gateway", - CloseCode.TLS_HANDSHAKE: "TLS handshake failure [internal]", -} - - -# Close code that are allowed in a close frame. -# Using a set optimizes `code in EXTERNAL_CLOSE_CODES`. -EXTERNAL_CLOSE_CODES = { - CloseCode.NORMAL_CLOSURE, - CloseCode.GOING_AWAY, - CloseCode.PROTOCOL_ERROR, - CloseCode.UNSUPPORTED_DATA, - CloseCode.INVALID_DATA, - CloseCode.POLICY_VIOLATION, - CloseCode.MESSAGE_TOO_BIG, - CloseCode.MANDATORY_EXTENSION, - CloseCode.INTERNAL_ERROR, - CloseCode.SERVICE_RESTART, - CloseCode.TRY_AGAIN_LATER, - CloseCode.BAD_GATEWAY, -} - - -OK_CLOSE_CODES = { - CloseCode.NORMAL_CLOSURE, - CloseCode.GOING_AWAY, - CloseCode.NO_STATUS_RCVD, -} - - -BytesLike = bytes, bytearray, memoryview - - -@dataclasses.dataclass -class Frame: - """ - WebSocket frame. - - Attributes: - opcode: Opcode. - data: Payload data. - fin: FIN bit. - rsv1: RSV1 bit. - rsv2: RSV2 bit. - rsv3: RSV3 bit. - - Only these fields are needed. The MASK bit, payload length and masking-key - are handled on the fly when parsing and serializing frames. - - """ - - opcode: Opcode - data: Union[bytes, bytearray, memoryview] - fin: bool = True - rsv1: bool = False - rsv2: bool = False - rsv3: bool = False - - # Configure if you want to see more in logs. Should be a multiple of 3. - MAX_LOG_SIZE = int(os.environ.get("WEBSOCKETS_MAX_LOG_SIZE", "75")) - - def __str__(self) -> str: - """ - Return a human-readable representation of a frame. - - """ - coding = None - length = f"{len(self.data)} byte{'' if len(self.data) == 1 else 's'}" - non_final = "" if self.fin else "continued" - - if self.opcode is OP_TEXT: - # Decoding only the beginning and the end is needlessly hard. - # Decode the entire payload then elide later if necessary. - data = repr(bytes(self.data).decode()) - elif self.opcode is OP_BINARY: - # We'll show at most the first 16 bytes and the last 8 bytes. - # Encode just what we need, plus two dummy bytes to elide later. - binary = self.data - if len(binary) > self.MAX_LOG_SIZE // 3: - cut = (self.MAX_LOG_SIZE // 3 - 1) // 3 # by default cut = 8 - binary = b"".join([binary[: 2 * cut], b"\x00\x00", binary[-cut:]]) - data = " ".join(f"{byte:02x}" for byte in binary) - elif self.opcode is OP_CLOSE: - data = str(Close.parse(self.data)) - elif self.data: - # We don't know if a Continuation frame contains text or binary. - # Ping and Pong frames could contain UTF-8. - # Attempt to decode as UTF-8 and display it as text; fallback to - # binary. If self.data is a memoryview, it has no decode() method, - # which raises AttributeError. - try: - data = repr(bytes(self.data).decode()) - coding = "text" - except (UnicodeDecodeError, AttributeError): - binary = self.data - if len(binary) > self.MAX_LOG_SIZE // 3: - cut = (self.MAX_LOG_SIZE // 3 - 1) // 3 # by default cut = 8 - binary = b"".join([binary[: 2 * cut], b"\x00\x00", binary[-cut:]]) - data = " ".join(f"{byte:02x}" for byte in binary) - coding = "binary" - else: - data = "''" - - if len(data) > self.MAX_LOG_SIZE: - cut = self.MAX_LOG_SIZE // 3 - 1 # by default cut = 24 - data = data[: 2 * cut] + "..." + data[-cut:] - - metadata = ", ".join(filter(None, [coding, length, non_final])) - - return f"{self.opcode.name} {data} [{metadata}]" - - @classmethod - def parse( - cls, - read_exact: Callable[[int], Generator[None, None, bytes]], - *, - mask: bool, - max_size: int | None = None, - extensions: Sequence[extensions.Extension] | None = None, - ) -> Generator[None, None, Frame]: - """ - Parse a WebSocket frame. - - This is a generator-based coroutine. - - Args: - read_exact: Generator-based coroutine that reads the requested - bytes or raises an exception if there isn't enough data. - mask: Whether the frame should be masked i.e. whether the read - happens on the server side. - max_size: Maximum payload size in bytes. - extensions: List of extensions, applied in reverse order. - - Raises: - EOFError: If the connection is closed without a full WebSocket frame. - PayloadTooBig: If the frame's payload size exceeds ``max_size``. - ProtocolError: If the frame contains incorrect values. - - """ - # Read the header. - data = yield from read_exact(2) - head1, head2 = struct.unpack("!BB", data) - - # While not Pythonic, this is marginally faster than calling bool(). - fin = True if head1 & 0b10000000 else False - rsv1 = True if head1 & 0b01000000 else False - rsv2 = True if head1 & 0b00100000 else False - rsv3 = True if head1 & 0b00010000 else False - - try: - opcode = Opcode(head1 & 0b00001111) - except ValueError as exc: - raise ProtocolError("invalid opcode") from exc - - if (True if head2 & 0b10000000 else False) != mask: - raise ProtocolError("incorrect masking") - - length = head2 & 0b01111111 - if length == 126: - data = yield from read_exact(2) - (length,) = struct.unpack("!H", data) - elif length == 127: - data = yield from read_exact(8) - (length,) = struct.unpack("!Q", data) - if max_size is not None and length > max_size: - raise PayloadTooBig(length, max_size) - if mask: - mask_bytes = yield from read_exact(4) - - # Read the data. - data = yield from read_exact(length) - if mask: - data = apply_mask(data, mask_bytes) - - frame = cls(opcode, data, fin, rsv1, rsv2, rsv3) - - if extensions is None: - extensions = [] - for extension in reversed(extensions): - frame = extension.decode(frame, max_size=max_size) - - frame.check() - - return frame - - def serialize( - self, - *, - mask: bool, - extensions: Sequence[extensions.Extension] | None = None, - ) -> bytes: - """ - Serialize a WebSocket frame. - - Args: - mask: Whether the frame should be masked i.e. whether the write - happens on the client side. - extensions: List of extensions, applied in order. - - Raises: - ProtocolError: If the frame contains incorrect values. - - """ - self.check() - - if extensions is None: - extensions = [] - for extension in extensions: - self = extension.encode(self) - - output = io.BytesIO() - - # Prepare the header. - head1 = ( - (0b10000000 if self.fin else 0) - | (0b01000000 if self.rsv1 else 0) - | (0b00100000 if self.rsv2 else 0) - | (0b00010000 if self.rsv3 else 0) - | self.opcode - ) - - head2 = 0b10000000 if mask else 0 - - length = len(self.data) - if length < 126: - output.write(struct.pack("!BB", head1, head2 | length)) - elif length < 65536: - output.write(struct.pack("!BBH", head1, head2 | 126, length)) - else: - output.write(struct.pack("!BBQ", head1, head2 | 127, length)) - - if mask: - mask_bytes = secrets.token_bytes(4) - output.write(mask_bytes) - - # Prepare the data. - if mask: - data = apply_mask(self.data, mask_bytes) - else: - data = self.data - output.write(data) - - return output.getvalue() - - def check(self) -> None: - """ - Check that reserved bits and opcode have acceptable values. - - Raises: - ProtocolError: If a reserved bit or the opcode is invalid. - - """ - if self.rsv1 or self.rsv2 or self.rsv3: - raise ProtocolError("reserved bits must be 0") - - if self.opcode in CTRL_OPCODES: - if len(self.data) > 125: - raise ProtocolError("control frame too long") - if not self.fin: - raise ProtocolError("fragmented control frame") - - -@dataclasses.dataclass -class Close: - """ - Code and reason for WebSocket close frames. - - Attributes: - code: Close code. - reason: Close reason. - - """ - - code: int - reason: str - - def __str__(self) -> str: - """ - Return a human-readable representation of a close code and reason. - - """ - if 3000 <= self.code < 4000: - explanation = "registered" - elif 4000 <= self.code < 5000: - explanation = "private use" - else: - explanation = CLOSE_CODE_EXPLANATIONS.get(self.code, "unknown") - result = f"{self.code} ({explanation})" - - if self.reason: - result = f"{result} {self.reason}" - - return result - - @classmethod - def parse(cls, data: bytes) -> Close: - """ - Parse the payload of a close frame. - - Args: - data: Payload of the close frame. - - Raises: - ProtocolError: If data is ill-formed. - UnicodeDecodeError: If the reason isn't valid UTF-8. - - """ - if len(data) >= 2: - (code,) = struct.unpack("!H", data[:2]) - reason = data[2:].decode() - close = cls(code, reason) - close.check() - return close - elif len(data) == 0: - return cls(CloseCode.NO_STATUS_RCVD, "") - else: - raise ProtocolError("close frame too short") - - def serialize(self) -> bytes: - """ - Serialize the payload of a close frame. - - """ - self.check() - return struct.pack("!H", self.code) + self.reason.encode() - - def check(self) -> None: - """ - Check that the close code has a valid value for a close frame. - - Raises: - ProtocolError: If the close code is invalid. - - """ - if not (self.code in EXTERNAL_CLOSE_CODES or 3000 <= self.code < 5000): - raise ProtocolError("invalid status code") - - -# At the bottom to break import cycles created by type annotations. -from . import extensions # noqa: E402 diff --git a/wip/SNIP/client/websockets/headers.py b/wip/SNIP/client/websockets/headers.py deleted file mode 100644 index e05ff5b..0000000 --- a/wip/SNIP/client/websockets/headers.py +++ /dev/null @@ -1,586 +0,0 @@ -from __future__ import annotations - -import base64 -import binascii -import ipaddress -import re -from collections.abc import Sequence -from typing import Callable, TypeVar, cast - -from .exceptions import InvalidHeaderFormat, InvalidHeaderValue -from .typing import ( - ConnectionOption, - ExtensionHeader, - ExtensionName, - ExtensionParameter, - Subprotocol, - UpgradeProtocol, -) - - -__all__ = [ - "build_host", - "parse_connection", - "parse_upgrade", - "parse_extension", - "build_extension", - "parse_subprotocol", - "build_subprotocol", - "validate_subprotocols", - "build_www_authenticate_basic", - "parse_authorization_basic", - "build_authorization_basic", -] - - -T = TypeVar("T") - - -def build_host( - host: str, - port: int, - secure: bool, - *, - always_include_port: bool = False, -) -> str: - """ - Build a ``Host`` header. - - """ - # https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2 - # IPv6 addresses must be enclosed in brackets. - try: - address = ipaddress.ip_address(host) - except ValueError: - # host is a hostname - pass - else: - # host is an IP address - if address.version == 6: - host = f"[{host}]" - - if always_include_port or port != (443 if secure else 80): - host = f"{host}:{port}" - - return host - - -# To avoid a dependency on a parsing library, we implement manually the ABNF -# described in https://datatracker.ietf.org/doc/html/rfc6455#section-9.1 and -# https://datatracker.ietf.org/doc/html/rfc7230#appendix-B. - - -def peek_ahead(header: str, pos: int) -> str | None: - """ - Return the next character from ``header`` at the given position. - - Return :obj:`None` at the end of ``header``. - - We never need to peek more than one character ahead. - - """ - return None if pos == len(header) else header[pos] - - -_OWS_re = re.compile(r"[\t ]*") - - -def parse_OWS(header: str, pos: int) -> int: - """ - Parse optional whitespace from ``header`` at the given position. - - Return the new position. - - The whitespace itself isn't returned because it isn't significant. - - """ - # There's always a match, possibly empty, whose content doesn't matter. - match = _OWS_re.match(header, pos) - assert match is not None - return match.end() - - -_token_re = re.compile(r"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+") - - -def parse_token(header: str, pos: int, header_name: str) -> tuple[str, int]: - """ - Parse a token from ``header`` at the given position. - - Return the token value and the new position. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - match = _token_re.match(header, pos) - if match is None: - raise InvalidHeaderFormat(header_name, "expected token", header, pos) - return match.group(), match.end() - - -_quoted_string_re = re.compile( - r'"(?:[\x09\x20-\x21\x23-\x5b\x5d-\x7e]|\\[\x09\x20-\x7e\x80-\xff])*"' -) - - -_unquote_re = re.compile(r"\\([\x09\x20-\x7e\x80-\xff])") - - -def parse_quoted_string(header: str, pos: int, header_name: str) -> tuple[str, int]: - """ - Parse a quoted string from ``header`` at the given position. - - Return the unquoted value and the new position. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - match = _quoted_string_re.match(header, pos) - if match is None: - raise InvalidHeaderFormat(header_name, "expected quoted string", header, pos) - return _unquote_re.sub(r"\1", match.group()[1:-1]), match.end() - - -_quotable_re = re.compile(r"[\x09\x20-\x7e\x80-\xff]*") - - -_quote_re = re.compile(r"([\x22\x5c])") - - -def build_quoted_string(value: str) -> str: - """ - Format ``value`` as a quoted string. - - This is the reverse of :func:`parse_quoted_string`. - - """ - match = _quotable_re.fullmatch(value) - if match is None: - raise ValueError("invalid characters for quoted-string encoding") - return '"' + _quote_re.sub(r"\\\1", value) + '"' - - -def parse_list( - parse_item: Callable[[str, int, str], tuple[T, int]], - header: str, - pos: int, - header_name: str, -) -> list[T]: - """ - Parse a comma-separated list from ``header`` at the given position. - - This is appropriate for parsing values with the following grammar: - - 1#item - - ``parse_item`` parses one item. - - ``header`` is assumed not to start or end with whitespace. - - (This function is designed for parsing an entire header value and - :func:`~websockets.http.read_headers` strips whitespace from values.) - - Return a list of items. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - # Per https://datatracker.ietf.org/doc/html/rfc7230#section-7, "a recipient - # MUST parse and ignore a reasonable number of empty list elements"; - # hence while loops that remove extra delimiters. - - # Remove extra delimiters before the first item. - while peek_ahead(header, pos) == ",": - pos = parse_OWS(header, pos + 1) - - items = [] - while True: - # Loop invariant: a item starts at pos in header. - item, pos = parse_item(header, pos, header_name) - items.append(item) - pos = parse_OWS(header, pos) - - # We may have reached the end of the header. - if pos == len(header): - break - - # There must be a delimiter after each element except the last one. - if peek_ahead(header, pos) == ",": - pos = parse_OWS(header, pos + 1) - else: - raise InvalidHeaderFormat(header_name, "expected comma", header, pos) - - # Remove extra delimiters before the next item. - while peek_ahead(header, pos) == ",": - pos = parse_OWS(header, pos + 1) - - # We may have reached the end of the header. - if pos == len(header): - break - - # Since we only advance in the header by one character with peek_ahead() - # or with the end position of a regex match, we can't overshoot the end. - assert pos == len(header) - - return items - - -def parse_connection_option( - header: str, pos: int, header_name: str -) -> tuple[ConnectionOption, int]: - """ - Parse a Connection option from ``header`` at the given position. - - Return the protocol value and the new position. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - item, pos = parse_token(header, pos, header_name) - return cast(ConnectionOption, item), pos - - -def parse_connection(header: str) -> list[ConnectionOption]: - """ - Parse a ``Connection`` header. - - Return a list of HTTP connection options. - - Args - header: value of the ``Connection`` header. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - return parse_list(parse_connection_option, header, 0, "Connection") - - -_protocol_re = re.compile( - r"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+(?:/[-!#$%&\'*+.^_`|~0-9a-zA-Z]+)?" -) - - -def parse_upgrade_protocol( - header: str, pos: int, header_name: str -) -> tuple[UpgradeProtocol, int]: - """ - Parse an Upgrade protocol from ``header`` at the given position. - - Return the protocol value and the new position. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - match = _protocol_re.match(header, pos) - if match is None: - raise InvalidHeaderFormat(header_name, "expected protocol", header, pos) - return cast(UpgradeProtocol, match.group()), match.end() - - -def parse_upgrade(header: str) -> list[UpgradeProtocol]: - """ - Parse an ``Upgrade`` header. - - Return a list of HTTP protocols. - - Args: - header: Value of the ``Upgrade`` header. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - return parse_list(parse_upgrade_protocol, header, 0, "Upgrade") - - -def parse_extension_item_param( - header: str, pos: int, header_name: str -) -> tuple[ExtensionParameter, int]: - """ - Parse a single extension parameter from ``header`` at the given position. - - Return a ``(name, value)`` pair and the new position. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - # Extract parameter name. - name, pos = parse_token(header, pos, header_name) - pos = parse_OWS(header, pos) - # Extract parameter value, if there is one. - value: str | None = None - if peek_ahead(header, pos) == "=": - pos = parse_OWS(header, pos + 1) - if peek_ahead(header, pos) == '"': - pos_before = pos # for proper error reporting below - value, pos = parse_quoted_string(header, pos, header_name) - # https://datatracker.ietf.org/doc/html/rfc6455#section-9.1 says: - # the value after quoted-string unescaping MUST conform to - # the 'token' ABNF. - if _token_re.fullmatch(value) is None: - raise InvalidHeaderFormat( - header_name, "invalid quoted header content", header, pos_before - ) - else: - value, pos = parse_token(header, pos, header_name) - pos = parse_OWS(header, pos) - - return (name, value), pos - - -def parse_extension_item( - header: str, pos: int, header_name: str -) -> tuple[ExtensionHeader, int]: - """ - Parse an extension definition from ``header`` at the given position. - - Return an ``(extension name, parameters)`` pair, where ``parameters`` is a - list of ``(name, value)`` pairs, and the new position. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - # Extract extension name. - name, pos = parse_token(header, pos, header_name) - pos = parse_OWS(header, pos) - # Extract all parameters. - parameters = [] - while peek_ahead(header, pos) == ";": - pos = parse_OWS(header, pos + 1) - parameter, pos = parse_extension_item_param(header, pos, header_name) - parameters.append(parameter) - return (cast(ExtensionName, name), parameters), pos - - -def parse_extension(header: str) -> list[ExtensionHeader]: - """ - Parse a ``Sec-WebSocket-Extensions`` header. - - Return a list of WebSocket extensions and their parameters in this format:: - - [ - ( - 'extension name', - [ - ('parameter name', 'parameter value'), - .... - ] - ), - ... - ] - - Parameter values are :obj:`None` when no value is provided. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - return parse_list(parse_extension_item, header, 0, "Sec-WebSocket-Extensions") - - -parse_extension_list = parse_extension # alias for backwards compatibility - - -def build_extension_item( - name: ExtensionName, parameters: Sequence[ExtensionParameter] -) -> str: - """ - Build an extension definition. - - This is the reverse of :func:`parse_extension_item`. - - """ - return "; ".join( - [cast(str, name)] - + [ - # Quoted strings aren't necessary because values are always tokens. - name if value is None else f"{name}={value}" - for name, value in parameters - ] - ) - - -def build_extension(extensions: Sequence[ExtensionHeader]) -> str: - """ - Build a ``Sec-WebSocket-Extensions`` header. - - This is the reverse of :func:`parse_extension`. - - """ - return ", ".join( - build_extension_item(name, parameters) for name, parameters in extensions - ) - - -build_extension_list = build_extension # alias for backwards compatibility - - -def parse_subprotocol_item( - header: str, pos: int, header_name: str -) -> tuple[Subprotocol, int]: - """ - Parse a subprotocol from ``header`` at the given position. - - Return the subprotocol value and the new position. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - item, pos = parse_token(header, pos, header_name) - return cast(Subprotocol, item), pos - - -def parse_subprotocol(header: str) -> list[Subprotocol]: - """ - Parse a ``Sec-WebSocket-Protocol`` header. - - Return a list of WebSocket subprotocols. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - return parse_list(parse_subprotocol_item, header, 0, "Sec-WebSocket-Protocol") - - -parse_subprotocol_list = parse_subprotocol # alias for backwards compatibility - - -def build_subprotocol(subprotocols: Sequence[Subprotocol]) -> str: - """ - Build a ``Sec-WebSocket-Protocol`` header. - - This is the reverse of :func:`parse_subprotocol`. - - """ - return ", ".join(subprotocols) - - -build_subprotocol_list = build_subprotocol # alias for backwards compatibility - - -def validate_subprotocols(subprotocols: Sequence[Subprotocol]) -> None: - """ - Validate that ``subprotocols`` is suitable for :func:`build_subprotocol`. - - """ - if not isinstance(subprotocols, Sequence): - raise TypeError("subprotocols must be a list") - if isinstance(subprotocols, str): - raise TypeError("subprotocols must be a list, not a str") - for subprotocol in subprotocols: - if not _token_re.fullmatch(subprotocol): - raise ValueError(f"invalid subprotocol: {subprotocol}") - - -def build_www_authenticate_basic(realm: str) -> str: - """ - Build a ``WWW-Authenticate`` header for HTTP Basic Auth. - - Args: - realm: Identifier of the protection space. - - """ - # https://datatracker.ietf.org/doc/html/rfc7617#section-2 - realm = build_quoted_string(realm) - charset = build_quoted_string("UTF-8") - return f"Basic realm={realm}, charset={charset}" - - -_token68_re = re.compile(r"[A-Za-z0-9-._~+/]+=*") - - -def parse_token68(header: str, pos: int, header_name: str) -> tuple[str, int]: - """ - Parse a token68 from ``header`` at the given position. - - Return the token value and the new position. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - match = _token68_re.match(header, pos) - if match is None: - raise InvalidHeaderFormat(header_name, "expected token68", header, pos) - return match.group(), match.end() - - -def parse_end(header: str, pos: int, header_name: str) -> None: - """ - Check that parsing reached the end of header. - - """ - if pos < len(header): - raise InvalidHeaderFormat(header_name, "trailing data", header, pos) - - -def parse_authorization_basic(header: str) -> tuple[str, str]: - """ - Parse an ``Authorization`` header for HTTP Basic Auth. - - Return a ``(username, password)`` tuple. - - Args: - header: Value of the ``Authorization`` header. - - Raises: - InvalidHeaderFormat: On invalid inputs. - InvalidHeaderValue: On unsupported inputs. - - """ - # https://datatracker.ietf.org/doc/html/rfc7235#section-2.1 - # https://datatracker.ietf.org/doc/html/rfc7617#section-2 - scheme, pos = parse_token(header, 0, "Authorization") - if scheme.lower() != "basic": - raise InvalidHeaderValue( - "Authorization", - f"unsupported scheme: {scheme}", - ) - if peek_ahead(header, pos) != " ": - raise InvalidHeaderFormat( - "Authorization", "expected space after scheme", header, pos - ) - pos += 1 - basic_credentials, pos = parse_token68(header, pos, "Authorization") - parse_end(header, pos, "Authorization") - - try: - user_pass = base64.b64decode(basic_credentials.encode()).decode() - except binascii.Error: - raise InvalidHeaderValue( - "Authorization", - "expected base64-encoded credentials", - ) from None - try: - username, password = user_pass.split(":", 1) - except ValueError: - raise InvalidHeaderValue( - "Authorization", - "expected username:password credentials", - ) from None - - return username, password - - -def build_authorization_basic(username: str, password: str) -> str: - """ - Build an ``Authorization`` header for HTTP Basic Auth. - - This is the reverse of :func:`parse_authorization_basic`. - - """ - # https://datatracker.ietf.org/doc/html/rfc7617#section-2 - assert ":" not in username - user_pass = f"{username}:{password}" - basic_credentials = base64.b64encode(user_pass.encode()).decode() - return "Basic " + basic_credentials diff --git a/wip/SNIP/client/websockets/http.py b/wip/SNIP/client/websockets/http.py deleted file mode 100644 index 0d860e5..0000000 --- a/wip/SNIP/client/websockets/http.py +++ /dev/null @@ -1,20 +0,0 @@ -from __future__ import annotations - -import warnings - -from .datastructures import Headers, MultipleValuesError # noqa: F401 - - -with warnings.catch_warnings(): - # Suppress redundant DeprecationWarning raised by websockets.legacy. - warnings.filterwarnings("ignore", category=DeprecationWarning) - from .legacy.http import read_request, read_response # noqa: F401 - - -warnings.warn( # deprecated in 9.0 - 2021-09-01 - "Headers and MultipleValuesError were moved " - "from websockets.http to websockets.datastructures" - "and read_request and read_response were moved " - "from websockets.http to websockets.legacy.http", - DeprecationWarning, -) diff --git a/wip/SNIP/client/websockets/http11.py b/wip/SNIP/client/websockets/http11.py deleted file mode 100644 index 530ac3d..0000000 --- a/wip/SNIP/client/websockets/http11.py +++ /dev/null @@ -1,427 +0,0 @@ -from __future__ import annotations - -import dataclasses -import os -import re -import sys -import warnings -from collections.abc import Generator -from typing import Callable - -from .datastructures import Headers -from .exceptions import SecurityError -from .version import version as websockets_version - - -__all__ = [ - "SERVER", - "USER_AGENT", - "Request", - "Response", -] - - -PYTHON_VERSION = "{}.{}".format(*sys.version_info) - -# User-Agent header for HTTP requests. -USER_AGENT = os.environ.get( - "WEBSOCKETS_USER_AGENT", - f"Python/{PYTHON_VERSION} websockets/{websockets_version}", -) - -# Server header for HTTP responses. -SERVER = os.environ.get( - "WEBSOCKETS_SERVER", - f"Python/{PYTHON_VERSION} websockets/{websockets_version}", -) - -# Maximum total size of headers is around 128 * 8 KiB = 1 MiB. -MAX_NUM_HEADERS = int(os.environ.get("WEBSOCKETS_MAX_NUM_HEADERS", "128")) - -# Limit request line and header lines. 8KiB is the most common default -# configuration of popular HTTP servers. -MAX_LINE_LENGTH = int(os.environ.get("WEBSOCKETS_MAX_LINE_LENGTH", "8192")) - -# Support for HTTP response bodies is intended to read an error message -# returned by a server. It isn't designed to perform large file transfers. -MAX_BODY_SIZE = int(os.environ.get("WEBSOCKETS_MAX_BODY_SIZE", "1_048_576")) # 1 MiB - - -def d(value: bytes) -> str: - """ - Decode a bytestring for interpolating into an error message. - - """ - return value.decode(errors="backslashreplace") - - -# See https://datatracker.ietf.org/doc/html/rfc7230#appendix-B. - -# Regex for validating header names. - -_token_re = re.compile(rb"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+") - -# Regex for validating header values. - -# We don't attempt to support obsolete line folding. - -# Include HTAB (\x09), SP (\x20), VCHAR (\x21-\x7e), obs-text (\x80-\xff). - -# The ABNF is complicated because it attempts to express that optional -# whitespace is ignored. We strip whitespace and don't revalidate that. - -# See also https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189 - -_value_re = re.compile(rb"[\x09\x20-\x7e\x80-\xff]*") - - -@dataclasses.dataclass -class Request: - """ - WebSocket handshake request. - - Attributes: - path: Request path, including optional query. - headers: Request headers. - """ - - path: str - headers: Headers - # body isn't useful is the context of this library. - - _exception: Exception | None = None - - @property - def exception(self) -> Exception | None: # pragma: no cover - warnings.warn( # deprecated in 10.3 - 2022-04-17 - "Request.exception is deprecated; use ServerProtocol.handshake_exc instead", - DeprecationWarning, - ) - return self._exception - - @classmethod - def parse( - cls, - read_line: Callable[[int], Generator[None, None, bytes]], - ) -> Generator[None, None, Request]: - """ - Parse a WebSocket handshake request. - - This is a generator-based coroutine. - - The request path isn't URL-decoded or validated in any way. - - The request path and headers are expected to contain only ASCII - characters. Other characters are represented with surrogate escapes. - - :meth:`parse` doesn't attempt to read the request body because - WebSocket handshake requests don't have one. If the request contains a - body, it may be read from the data stream after :meth:`parse` returns. - - Args: - read_line: Generator-based coroutine that reads a LF-terminated - line or raises an exception if there isn't enough data - - Raises: - EOFError: If the connection is closed without a full HTTP request. - SecurityError: If the request exceeds a security limit. - ValueError: If the request isn't well formatted. - - """ - # https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.1 - - # Parsing is simple because fixed values are expected for method and - # version and because path isn't checked. Since WebSocket software tends - # to implement HTTP/1.1 strictly, there's little need for lenient parsing. - - try: - request_line = yield from parse_line(read_line) - except EOFError as exc: - raise EOFError("connection closed while reading HTTP request line") from exc - - try: - method, raw_path, protocol = request_line.split(b" ", 2) - except ValueError: # not enough values to unpack (expected 3, got 1-2) - raise ValueError(f"invalid HTTP request line: {d(request_line)}") from None - if protocol != b"HTTP/1.1": - raise ValueError( - f"unsupported protocol; expected HTTP/1.1: {d(request_line)}" - ) - if method != b"GET": - raise ValueError(f"unsupported HTTP method; expected GET; got {d(method)}") - path = raw_path.decode("ascii", "surrogateescape") - - headers = yield from parse_headers(read_line) - - # https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3 - - if "Transfer-Encoding" in headers: - raise NotImplementedError("transfer codings aren't supported") - - if "Content-Length" in headers: - raise ValueError("unsupported request body") - - return cls(path, headers) - - def serialize(self) -> bytes: - """ - Serialize a WebSocket handshake request. - - """ - # Since the request line and headers only contain ASCII characters, - # we can keep this simple. - request = f"GET {self.path} HTTP/1.1\r\n".encode() - request += self.headers.serialize() - return request - - -@dataclasses.dataclass -class Response: - """ - WebSocket handshake response. - - Attributes: - status_code: Response code. - reason_phrase: Response reason. - headers: Response headers. - body: Response body. - - """ - - status_code: int - reason_phrase: str - headers: Headers - body: bytes = b"" - - _exception: Exception | None = None - - @property - def exception(self) -> Exception | None: # pragma: no cover - warnings.warn( # deprecated in 10.3 - 2022-04-17 - "Response.exception is deprecated; " - "use ClientProtocol.handshake_exc instead", - DeprecationWarning, - ) - return self._exception - - @classmethod - def parse( - cls, - read_line: Callable[[int], Generator[None, None, bytes]], - read_exact: Callable[[int], Generator[None, None, bytes]], - read_to_eof: Callable[[int], Generator[None, None, bytes]], - include_body: bool = True, - ) -> Generator[None, None, Response]: - """ - Parse a WebSocket handshake response. - - This is a generator-based coroutine. - - The reason phrase and headers are expected to contain only ASCII - characters. Other characters are represented with surrogate escapes. - - Args: - read_line: Generator-based coroutine that reads a LF-terminated - line or raises an exception if there isn't enough data. - read_exact: Generator-based coroutine that reads the requested - bytes or raises an exception if there isn't enough data. - read_to_eof: Generator-based coroutine that reads until the end - of the stream. - - Raises: - EOFError: If the connection is closed without a full HTTP response. - SecurityError: If the response exceeds a security limit. - LookupError: If the response isn't well formatted. - ValueError: If the response isn't well formatted. - - """ - # https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2 - - try: - status_line = yield from parse_line(read_line) - except EOFError as exc: - raise EOFError("connection closed while reading HTTP status line") from exc - - try: - protocol, raw_status_code, raw_reason = status_line.split(b" ", 2) - except ValueError: # not enough values to unpack (expected 3, got 1-2) - raise ValueError(f"invalid HTTP status line: {d(status_line)}") from None - if protocol != b"HTTP/1.1": - raise ValueError( - f"unsupported protocol; expected HTTP/1.1: {d(status_line)}" - ) - try: - status_code = int(raw_status_code) - except ValueError: # invalid literal for int() with base 10 - raise ValueError( - f"invalid status code; expected integer; got {d(raw_status_code)}" - ) from None - if not 100 <= status_code < 600: - raise ValueError( - f"invalid status code; expected 100–599; got {d(raw_status_code)}" - ) - if not _value_re.fullmatch(raw_reason): - raise ValueError(f"invalid HTTP reason phrase: {d(raw_reason)}") - reason = raw_reason.decode("ascii", "surrogateescape") - - headers = yield from parse_headers(read_line) - - if include_body: - body = yield from read_body( - status_code, headers, read_line, read_exact, read_to_eof - ) - else: - body = b"" - - return cls(status_code, reason, headers, body) - - def serialize(self) -> bytes: - """ - Serialize a WebSocket handshake response. - - """ - # Since the status line and headers only contain ASCII characters, - # we can keep this simple. - response = f"HTTP/1.1 {self.status_code} {self.reason_phrase}\r\n".encode() - response += self.headers.serialize() - response += self.body - return response - - -def parse_line( - read_line: Callable[[int], Generator[None, None, bytes]], -) -> Generator[None, None, bytes]: - """ - Parse a single line. - - CRLF is stripped from the return value. - - Args: - read_line: Generator-based coroutine that reads a LF-terminated line - or raises an exception if there isn't enough data. - - Raises: - EOFError: If the connection is closed without a CRLF. - SecurityError: If the response exceeds a security limit. - - """ - try: - line = yield from read_line(MAX_LINE_LENGTH) - except RuntimeError: - raise SecurityError("line too long") - # Not mandatory but safe - https://datatracker.ietf.org/doc/html/rfc7230#section-3.5 - if not line.endswith(b"\r\n"): - raise EOFError("line without CRLF") - return line[:-2] - - -def parse_headers( - read_line: Callable[[int], Generator[None, None, bytes]], -) -> Generator[None, None, Headers]: - """ - Parse HTTP headers. - - Non-ASCII characters are represented with surrogate escapes. - - Args: - read_line: Generator-based coroutine that reads a LF-terminated line - or raises an exception if there isn't enough data. - - Raises: - EOFError: If the connection is closed without complete headers. - SecurityError: If the request exceeds a security limit. - ValueError: If the request isn't well formatted. - - """ - # https://datatracker.ietf.org/doc/html/rfc7230#section-3.2 - - # We don't attempt to support obsolete line folding. - - headers = Headers() - for _ in range(MAX_NUM_HEADERS + 1): - try: - line = yield from parse_line(read_line) - except EOFError as exc: - raise EOFError("connection closed while reading HTTP headers") from exc - if line == b"": - break - - try: - raw_name, raw_value = line.split(b":", 1) - except ValueError: # not enough values to unpack (expected 2, got 1) - raise ValueError(f"invalid HTTP header line: {d(line)}") from None - if not _token_re.fullmatch(raw_name): - raise ValueError(f"invalid HTTP header name: {d(raw_name)}") - raw_value = raw_value.strip(b" \t") - if not _value_re.fullmatch(raw_value): - raise ValueError(f"invalid HTTP header value: {d(raw_value)}") - - name = raw_name.decode("ascii") # guaranteed to be ASCII at this point - value = raw_value.decode("ascii", "surrogateescape") - headers[name] = value - - else: - raise SecurityError("too many HTTP headers") - - return headers - - -def read_body( - status_code: int, - headers: Headers, - read_line: Callable[[int], Generator[None, None, bytes]], - read_exact: Callable[[int], Generator[None, None, bytes]], - read_to_eof: Callable[[int], Generator[None, None, bytes]], -) -> Generator[None, None, bytes]: - # https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3 - - # Since websockets only does GET requests (no HEAD, no CONNECT), all - # responses except 1xx, 204, and 304 include a message body. - if 100 <= status_code < 200 or status_code == 204 or status_code == 304: - return b"" - - # MultipleValuesError is sufficiently unlikely that we don't attempt to - # handle it when accessing headers. Instead we document that its parent - # class, LookupError, may be raised. - # Conversions from str to int are protected by sys.set_int_max_str_digits.. - - elif (coding := headers.get("Transfer-Encoding")) is not None: - if coding != "chunked": - raise NotImplementedError(f"transfer coding {coding} isn't supported") - - body = b"" - while True: - chunk_size_line = yield from parse_line(read_line) - raw_chunk_size = chunk_size_line.split(b";", 1)[0] - # Set a lower limit than default_max_str_digits; 1 EB is plenty. - if len(raw_chunk_size) > 15: - str_chunk_size = raw_chunk_size.decode(errors="backslashreplace") - raise SecurityError(f"chunk too large: 0x{str_chunk_size} bytes") - chunk_size = int(raw_chunk_size, 16) - if chunk_size == 0: - break - if len(body) + chunk_size > MAX_BODY_SIZE: - raise SecurityError( - f"chunk too large: {chunk_size} bytes after {len(body)} bytes" - ) - body += yield from read_exact(chunk_size) - if (yield from read_exact(2)) != b"\r\n": - raise ValueError("chunk without CRLF") - # Read the trailer. - yield from parse_headers(read_line) - return body - - elif (raw_content_length := headers.get("Content-Length")) is not None: - # Set a lower limit than default_max_str_digits; 1 EiB is plenty. - if len(raw_content_length) > 18: - raise SecurityError(f"body too large: {raw_content_length} bytes") - content_length = int(raw_content_length) - if content_length > MAX_BODY_SIZE: - raise SecurityError(f"body too large: {content_length} bytes") - return (yield from read_exact(content_length)) - - else: - try: - return (yield from read_to_eof(MAX_BODY_SIZE)) - except RuntimeError: - raise SecurityError(f"body too large: over {MAX_BODY_SIZE} bytes") diff --git a/wip/SNIP/client/websockets/imports.py b/wip/SNIP/client/websockets/imports.py deleted file mode 100644 index c63fb21..0000000 --- a/wip/SNIP/client/websockets/imports.py +++ /dev/null @@ -1,100 +0,0 @@ -from __future__ import annotations - -import warnings -from collections.abc import Iterable -from typing import Any - - -__all__ = ["lazy_import"] - - -def import_name(name: str, source: str, namespace: dict[str, Any]) -> Any: - """ - Import ``name`` from ``source`` in ``namespace``. - - There are two use cases: - - - ``name`` is an object defined in ``source``; - - ``name`` is a submodule of ``source``. - - Neither :func:`__import__` nor :func:`~importlib.import_module` does - exactly this. :func:`__import__` is closer to the intended behavior. - - """ - level = 0 - while source[level] == ".": - level += 1 - assert level < len(source), "importing from parent isn't supported" - module = __import__(source[level:], namespace, None, [name], level) - return getattr(module, name) - - -def lazy_import( - namespace: dict[str, Any], - aliases: dict[str, str] | None = None, - deprecated_aliases: dict[str, str] | None = None, -) -> None: - """ - Provide lazy, module-level imports. - - Typical use:: - - __getattr__, __dir__ = lazy_import( - globals(), - aliases={ - "": "", - ... - }, - deprecated_aliases={ - ..., - } - ) - - This function defines ``__getattr__`` and ``__dir__`` per :pep:`562`. - - """ - if aliases is None: - aliases = {} - if deprecated_aliases is None: - deprecated_aliases = {} - - namespace_set = set(namespace) - aliases_set = set(aliases) - deprecated_aliases_set = set(deprecated_aliases) - - assert not namespace_set & aliases_set, "namespace conflict" - assert not namespace_set & deprecated_aliases_set, "namespace conflict" - assert not aliases_set & deprecated_aliases_set, "namespace conflict" - - package = namespace["__name__"] - - def __getattr__(name: str) -> Any: - assert aliases is not None # mypy cannot figure this out - try: - source = aliases[name] - except KeyError: - pass - else: - return import_name(name, source, namespace) - - assert deprecated_aliases is not None # mypy cannot figure this out - try: - source = deprecated_aliases[name] - except KeyError: - pass - else: - warnings.warn( - f"{package}.{name} is deprecated", - DeprecationWarning, - stacklevel=2, - ) - return import_name(name, source, namespace) - - raise AttributeError(f"module {package!r} has no attribute {name!r}") - - namespace["__getattr__"] = __getattr__ - - def __dir__() -> Iterable[str]: - return sorted(namespace_set | aliases_set | deprecated_aliases_set) - - namespace["__dir__"] = __dir__ diff --git a/wip/SNIP/client/websockets/legacy/__init__.py b/wip/SNIP/client/websockets/legacy/__init__.py deleted file mode 100644 index ad9aa25..0000000 --- a/wip/SNIP/client/websockets/legacy/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import annotations - -import warnings - - -warnings.warn( # deprecated in 14.0 - 2024-11-09 - "websockets.legacy is deprecated; " - "see https://websockets.readthedocs.io/en/stable/howto/upgrade.html " - "for upgrade instructions", - DeprecationWarning, -) diff --git a/wip/SNIP/client/websockets/legacy/__pycache__/__init__.cpython-313.pyc b/wip/SNIP/client/websockets/legacy/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 1bcff1ba71e8a1929a8b5dd7791e310068c7d1e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 440 zcmey&%ge>Uz`#(r@A!q z7^WaLh$>`Ki6NLdhQ*W#BBsEQ&ZNom62#GDyv3cEmzQ6XSdy8aSL|0+Q=XbsoS&SX zT2ic+lbW8GT&a**tdNpgP?VaSSdyAztx%krs*q7qQc!HAuaBg-C^a#qBqKE?Ke<>h zGhaV7PrtY%F)1fiKO?`qBwxR@AiXFtB~>q@BsWJPEx$+sDxi>=S6ot5nhdsHll2x? ze0*AINoi4Pe0&iz0|Ub?j`GB!yv)4x;#({r)-53ys9Q4g^THugD;Yk6{Bq0M#VW?5 zvLLl6Ge6HiIHo+aAST$)Gax28Co?s#BnIKw7>Lhe;^Q;(GE3s)^$IF)aoFVMr<{9 diff --git a/wip/SNIP/client/websockets/legacy/__pycache__/exceptions.cpython-313.pyc b/wip/SNIP/client/websockets/legacy/__pycache__/exceptions.cpython-313.pyc deleted file mode 100644 index 9a98bb7d02b9ae9a1ef67d00e24fe0e7a2f016f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3291 zcmey&%ge>Uz`)?VDK68Koq^#ohy%l{P{!vX1_p+y48aV+jNS}I48csn%-)P%Ohrrz zAU2CPvlmMd3z*I7&FaNg#HPR?!Jxzt%qGPU%pSvH$_z25h#f4-A;l2Pi6qJa7Uhy+ z2B24#E>0DC5ep@=b7SS=clA9WELxALOi9% z#dV7tA{7sky2YN6nwXMWRD6rAD7B=tD9cEj8RZfxMk~N72{D^kXn?PpXVPOQ=VB6 z6YS?15R;sfnVMG;Q=XbsoS&SXT2dU7lbW8GTp5#Ek(^pkl9``ZtXEKZiz7ZhGcU6w zKK_;zM%d`VYpw)xdB=TI~vl#0_z|UY^e&rzXQ4&A`9__nrdSd%=vM3_&18 zXqb<|m`Q~}lgY12yEwI|EVW3XC^aiJxg<43Av`rH80=hy$LIM;t z8Nt3^$yme>at_E|g(3k428LVg@$tnaMe*^sB(Zq`EF})oBF4bL(7i%W`%Kn}Sj3UYN~ zNoI0RTMYH@u0N(Qi7z#c2&U|?VX6QHm!Hf3O7Xk+-u#LUX^fs2V%eu47` z1}0Yd4SLs&JT4k}fH@~ZzJSC&a4@n4FwPME&H$nzCV~t@$!wg^X!#rkjxF@eCdUxW zjX%T1a0Igjvzu^0vRM{m5oa)443`O0Fefpr%7+~41BwryBq(q@4BNZw0<>x6Blol1_7pE#D=A|e=a+E@19wb4)vZfvvS4c)? zu>w3{gYsu$o1HBq?ROR=71HaDio9^yS5a99?Jf`S4RwUDwEQdEgy zgd4a}1r=!6430@(xUS_4tWmU5aD&`xijwl~UIi7hi z^RV6p$G{7L;TL!!J}@(Ja)AR;lNn+)IO;%=tjP!w=Pi-|MYJS{kOC2)a-#@Tbryj_ z0aCnjL$e8}Epdw#>@{#nTO<$C0V-*WK($ekB8Uqr<{{}p8N>wF z263s6oSdwZ4+Nw>uyV3Weqdt|Qn)UteNj;R13QS%!N4!_0i@ys7XugHS8kA)7&9vu zsJvxng?Oq6WHw513yb8>7T_F)Ufe>8+F(`_7EqHci=~Jym?ehQgbCWBVGrgg;z(!J zM|R!6Sw zL6z`S2IR&JG#jsEEHVLQP$ozQ1+j}kCMZA?7dTT&VfY80FP%Y&ksX}CZU~F@a)6Ts zN}dCm2FgjFL2*2l0auj9eE)RlhLEGID)!VB}%cyda_Rm4Sy5;#2^1so(4X diff --git a/wip/SNIP/client/websockets/legacy/__pycache__/framing.cpython-313.pyc b/wip/SNIP/client/websockets/legacy/__pycache__/framing.cpython-313.pyc deleted file mode 100644 index 4804fc990199e3fea5ecfd0f6a0465fdc27ae137..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8067 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 ajQn4;Y#D{WI56=t8husJWOQW&n-2iqkGMGi diff --git a/wip/SNIP/client/websockets/legacy/__pycache__/http.cpython-313.pyc b/wip/SNIP/client/websockets/legacy/__pycache__/http.cpython-313.pyc deleted file mode 100644 index e9d925018185e9a4c4eec0282142181f6285df4e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7576 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 diff --git a/wip/SNIP/client/websockets/legacy/__pycache__/server.cpython-313.pyc b/wip/SNIP/client/websockets/legacy/__pycache__/server.cpython-313.pyc deleted file mode 100644 index 936b27d181e49358a326e1cb78203b5c2d8d1d96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45309 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 ME4M5o8@SN{05BwNJOBUy diff --git a/wip/SNIP/client/websockets/legacy/auth.py b/wip/SNIP/client/websockets/legacy/auth.py deleted file mode 100644 index a262fcd..0000000 --- a/wip/SNIP/client/websockets/legacy/auth.py +++ /dev/null @@ -1,190 +0,0 @@ -from __future__ import annotations - -import functools -import hmac -import http -from collections.abc import Awaitable, Iterable -from typing import Any, Callable, cast - -from ..datastructures import Headers -from ..exceptions import InvalidHeader -from ..headers import build_www_authenticate_basic, parse_authorization_basic -from .server import HTTPResponse, WebSocketServerProtocol - - -__all__ = ["BasicAuthWebSocketServerProtocol", "basic_auth_protocol_factory"] - -Credentials = tuple[str, str] - - -def is_credentials(value: Any) -> bool: - try: - username, password = value - except (TypeError, ValueError): - return False - else: - return isinstance(username, str) and isinstance(password, str) - - -class BasicAuthWebSocketServerProtocol(WebSocketServerProtocol): - """ - WebSocket server protocol that enforces HTTP Basic Auth. - - """ - - realm: str = "" - """ - Scope of protection. - - If provided, it should contain only ASCII characters because the - encoding of non-ASCII characters is undefined. - """ - - username: str | None = None - """Username of the authenticated user.""" - - def __init__( - self, - *args: Any, - realm: str | None = None, - check_credentials: Callable[[str, str], Awaitable[bool]] | None = None, - **kwargs: Any, - ) -> None: - if realm is not None: - self.realm = realm # shadow class attribute - self._check_credentials = check_credentials - super().__init__(*args, **kwargs) - - async def check_credentials(self, username: str, password: str) -> bool: - """ - Check whether credentials are authorized. - - This coroutine may be overridden in a subclass, for example to - authenticate against a database or an external service. - - Args: - username: HTTP Basic Auth username. - password: HTTP Basic Auth password. - - Returns: - :obj:`True` if the handshake should continue; - :obj:`False` if it should fail with an HTTP 401 error. - - """ - if self._check_credentials is not None: - return await self._check_credentials(username, password) - - return False - - async def process_request( - self, - path: str, - request_headers: Headers, - ) -> HTTPResponse | None: - """ - Check HTTP Basic Auth and return an HTTP 401 response if needed. - - """ - try: - authorization = request_headers["Authorization"] - except KeyError: - return ( - http.HTTPStatus.UNAUTHORIZED, - [("WWW-Authenticate", build_www_authenticate_basic(self.realm))], - b"Missing credentials\n", - ) - - try: - username, password = parse_authorization_basic(authorization) - except InvalidHeader: - return ( - http.HTTPStatus.UNAUTHORIZED, - [("WWW-Authenticate", build_www_authenticate_basic(self.realm))], - b"Unsupported credentials\n", - ) - - if not await self.check_credentials(username, password): - return ( - http.HTTPStatus.UNAUTHORIZED, - [("WWW-Authenticate", build_www_authenticate_basic(self.realm))], - b"Invalid credentials\n", - ) - - self.username = username - - return await super().process_request(path, request_headers) - - -def basic_auth_protocol_factory( - realm: str | None = None, - credentials: Credentials | Iterable[Credentials] | None = None, - check_credentials: Callable[[str, str], Awaitable[bool]] | None = None, - create_protocol: Callable[..., BasicAuthWebSocketServerProtocol] | None = None, -) -> Callable[..., BasicAuthWebSocketServerProtocol]: - """ - Protocol factory that enforces HTTP Basic Auth. - - :func:`basic_auth_protocol_factory` is designed to integrate with - :func:`~websockets.legacy.server.serve` like this:: - - serve( - ..., - create_protocol=basic_auth_protocol_factory( - realm="my dev server", - credentials=("hello", "iloveyou"), - ) - ) - - Args: - realm: Scope of protection. It should contain only ASCII characters - because the encoding of non-ASCII characters is undefined. - Refer to section 2.2 of :rfc:`7235` for details. - credentials: Hard coded authorized credentials. It can be a - ``(username, password)`` pair or a list of such pairs. - check_credentials: Coroutine that verifies credentials. - It receives ``username`` and ``password`` arguments - and returns a :class:`bool`. One of ``credentials`` or - ``check_credentials`` must be provided but not both. - create_protocol: Factory that creates the protocol. By default, this - is :class:`BasicAuthWebSocketServerProtocol`. It can be replaced - by a subclass. - Raises: - TypeError: If the ``credentials`` or ``check_credentials`` argument is - wrong. - - """ - if (credentials is None) == (check_credentials is None): - raise TypeError("provide either credentials or check_credentials") - - if credentials is not None: - if is_credentials(credentials): - credentials_list = [cast(Credentials, credentials)] - elif isinstance(credentials, Iterable): - credentials_list = list(cast(Iterable[Credentials], credentials)) - if not all(is_credentials(item) for item in credentials_list): - raise TypeError(f"invalid credentials argument: {credentials}") - else: - raise TypeError(f"invalid credentials argument: {credentials}") - - credentials_dict = dict(credentials_list) - - async def check_credentials(username: str, password: str) -> bool: - try: - expected_password = credentials_dict[username] - except KeyError: - return False - return hmac.compare_digest(expected_password, password) - - if create_protocol is None: - create_protocol = BasicAuthWebSocketServerProtocol - - # Help mypy and avoid this error: "type[BasicAuthWebSocketServerProtocol] | - # Callable[..., BasicAuthWebSocketServerProtocol]" not callable [misc] - create_protocol = cast( - Callable[..., BasicAuthWebSocketServerProtocol], create_protocol - ) - return functools.partial( - create_protocol, - realm=realm, - check_credentials=check_credentials, - ) diff --git a/wip/SNIP/client/websockets/legacy/client.py b/wip/SNIP/client/websockets/legacy/client.py deleted file mode 100644 index 29141f3..0000000 --- a/wip/SNIP/client/websockets/legacy/client.py +++ /dev/null @@ -1,705 +0,0 @@ -from __future__ import annotations - -import asyncio -import functools -import logging -import os -import random -import traceback -import urllib.parse -import warnings -from collections.abc import AsyncIterator, Generator, Sequence -from types import TracebackType -from typing import Any, Callable, cast - -from ..asyncio.compatibility import asyncio_timeout -from ..datastructures import Headers, HeadersLike -from ..exceptions import ( - InvalidHeader, - InvalidHeaderValue, - InvalidMessage, - NegotiationError, - SecurityError, -) -from ..extensions import ClientExtensionFactory, Extension -from ..extensions.permessage_deflate import enable_client_permessage_deflate -from ..headers import ( - build_authorization_basic, - build_extension, - build_host, - build_subprotocol, - parse_extension, - parse_subprotocol, - validate_subprotocols, -) -from ..http11 import USER_AGENT -from ..typing import ExtensionHeader, LoggerLike, Origin, Subprotocol -from ..uri import WebSocketURI, parse_uri -from .exceptions import InvalidStatusCode, RedirectHandshake -from .handshake import build_request, check_response -from .http import read_response -from .protocol import WebSocketCommonProtocol - - -__all__ = ["connect", "unix_connect", "WebSocketClientProtocol"] - - -class WebSocketClientProtocol(WebSocketCommonProtocol): - """ - WebSocket client connection. - - :class:`WebSocketClientProtocol` provides :meth:`recv` and :meth:`send` - coroutines for receiving and sending messages. - - It supports asynchronous iteration to receive messages:: - - async for message in websocket: - await process(message) - - The iterator exits normally when the connection is closed with close code - 1000 (OK) or 1001 (going away) or without a close code. It raises - a :exc:`~websockets.exceptions.ConnectionClosedError` when the connection - is closed with any other code. - - See :func:`connect` for the documentation of ``logger``, ``origin``, - ``extensions``, ``subprotocols``, ``extra_headers``, and - ``user_agent_header``. - - See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the - documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, - ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit``. - - """ - - is_client = True - side = "client" - - def __init__( - self, - *, - logger: LoggerLike | None = None, - origin: Origin | None = None, - extensions: Sequence[ClientExtensionFactory] | None = None, - subprotocols: Sequence[Subprotocol] | None = None, - extra_headers: HeadersLike | None = None, - user_agent_header: str | None = USER_AGENT, - **kwargs: Any, - ) -> None: - if logger is None: - logger = logging.getLogger("websockets.client") - super().__init__(logger=logger, **kwargs) - self.origin = origin - self.available_extensions = extensions - self.available_subprotocols = subprotocols - self.extra_headers = extra_headers - self.user_agent_header = user_agent_header - - def write_http_request(self, path: str, headers: Headers) -> None: - """ - Write request line and headers to the HTTP request. - - """ - self.path = path - self.request_headers = headers - - if self.debug: - self.logger.debug("> GET %s HTTP/1.1", path) - for key, value in headers.raw_items(): - self.logger.debug("> %s: %s", key, value) - - # Since the path and headers only contain ASCII characters, - # we can keep this simple. - request = f"GET {path} HTTP/1.1\r\n" - request += str(headers) - - self.transport.write(request.encode()) - - async def read_http_response(self) -> tuple[int, Headers]: - """ - Read status line and headers from the HTTP response. - - If the response contains a body, it may be read from ``self.reader`` - after this coroutine returns. - - Raises: - InvalidMessage: If the HTTP message is malformed or isn't an - HTTP/1.1 GET response. - - """ - try: - status_code, reason, headers = await read_response(self.reader) - except Exception as exc: - raise InvalidMessage("did not receive a valid HTTP response") from exc - - if self.debug: - self.logger.debug("< HTTP/1.1 %d %s", status_code, reason) - for key, value in headers.raw_items(): - self.logger.debug("< %s: %s", key, value) - - self.response_headers = headers - - return status_code, self.response_headers - - @staticmethod - def process_extensions( - headers: Headers, - available_extensions: Sequence[ClientExtensionFactory] | None, - ) -> list[Extension]: - """ - Handle the Sec-WebSocket-Extensions HTTP response header. - - Check that each extension is supported, as well as its parameters. - - Return the list of accepted extensions. - - Raise :exc:`~websockets.exceptions.InvalidHandshake` to abort the - connection. - - :rfc:`6455` leaves the rules up to the specification of each - :extension. - - To provide this level of flexibility, for each extension accepted by - the server, we check for a match with each extension available in the - client configuration. If no match is found, an exception is raised. - - If several variants of the same extension are accepted by the server, - it may be configured several times, which won't make sense in general. - Extensions must implement their own requirements. For this purpose, - the list of previously accepted extensions is provided. - - Other requirements, for example related to mandatory extensions or the - order of extensions, may be implemented by overriding this method. - - """ - accepted_extensions: list[Extension] = [] - - header_values = headers.get_all("Sec-WebSocket-Extensions") - - if header_values: - if available_extensions is None: - raise NegotiationError("no extensions supported") - - parsed_header_values: list[ExtensionHeader] = sum( - [parse_extension(header_value) for header_value in header_values], [] - ) - - for name, response_params in parsed_header_values: - for extension_factory in available_extensions: - # Skip non-matching extensions based on their name. - if extension_factory.name != name: - continue - - # Skip non-matching extensions based on their params. - try: - extension = extension_factory.process_response_params( - response_params, accepted_extensions - ) - except NegotiationError: - continue - - # Add matching extension to the final list. - accepted_extensions.append(extension) - - # Break out of the loop once we have a match. - break - - # If we didn't break from the loop, no extension in our list - # matched what the server sent. Fail the connection. - else: - raise NegotiationError( - f"Unsupported extension: " - f"name = {name}, params = {response_params}" - ) - - return accepted_extensions - - @staticmethod - def process_subprotocol( - headers: Headers, available_subprotocols: Sequence[Subprotocol] | None - ) -> Subprotocol | None: - """ - Handle the Sec-WebSocket-Protocol HTTP response header. - - Check that it contains exactly one supported subprotocol. - - Return the selected subprotocol. - - """ - subprotocol: Subprotocol | None = None - - header_values = headers.get_all("Sec-WebSocket-Protocol") - - if header_values: - if available_subprotocols is None: - raise NegotiationError("no subprotocols supported") - - parsed_header_values: Sequence[Subprotocol] = sum( - [parse_subprotocol(header_value) for header_value in header_values], [] - ) - - if len(parsed_header_values) > 1: - raise InvalidHeaderValue( - "Sec-WebSocket-Protocol", - f"multiple values: {', '.join(parsed_header_values)}", - ) - - subprotocol = parsed_header_values[0] - - if subprotocol not in available_subprotocols: - raise NegotiationError(f"unsupported subprotocol: {subprotocol}") - - return subprotocol - - async def handshake( - self, - wsuri: WebSocketURI, - origin: Origin | None = None, - available_extensions: Sequence[ClientExtensionFactory] | None = None, - available_subprotocols: Sequence[Subprotocol] | None = None, - extra_headers: HeadersLike | None = None, - ) -> None: - """ - Perform the client side of the opening handshake. - - Args: - wsuri: URI of the WebSocket server. - origin: Value of the ``Origin`` header. - extensions: List of supported extensions, in order in which they - should be negotiated and run. - subprotocols: List of supported subprotocols, in order of decreasing - preference. - extra_headers: Arbitrary HTTP headers to add to the handshake request. - - Raises: - InvalidHandshake: If the handshake fails. - - """ - request_headers = Headers() - - request_headers["Host"] = build_host(wsuri.host, wsuri.port, wsuri.secure) - - if wsuri.user_info: - request_headers["Authorization"] = build_authorization_basic( - *wsuri.user_info - ) - - if origin is not None: - request_headers["Origin"] = origin - - key = build_request(request_headers) - - if available_extensions is not None: - extensions_header = build_extension( - [ - (extension_factory.name, extension_factory.get_request_params()) - for extension_factory in available_extensions - ] - ) - request_headers["Sec-WebSocket-Extensions"] = extensions_header - - if available_subprotocols is not None: - protocol_header = build_subprotocol(available_subprotocols) - request_headers["Sec-WebSocket-Protocol"] = protocol_header - - if self.extra_headers is not None: - request_headers.update(self.extra_headers) - - if self.user_agent_header: - request_headers.setdefault("User-Agent", self.user_agent_header) - - self.write_http_request(wsuri.resource_name, request_headers) - - status_code, response_headers = await self.read_http_response() - if status_code in (301, 302, 303, 307, 308): - if "Location" not in response_headers: - raise InvalidHeader("Location") - raise RedirectHandshake(response_headers["Location"]) - elif status_code != 101: - raise InvalidStatusCode(status_code, response_headers) - - check_response(response_headers, key) - - self.extensions = self.process_extensions( - response_headers, available_extensions - ) - - self.subprotocol = self.process_subprotocol( - response_headers, available_subprotocols - ) - - self.connection_open() - - -class Connect: - """ - Connect to the WebSocket server at ``uri``. - - Awaiting :func:`connect` yields a :class:`WebSocketClientProtocol` which - can then be used to send and receive messages. - - :func:`connect` can be used as a asynchronous context manager:: - - async with connect(...) as websocket: - ... - - The connection is closed automatically when exiting the context. - - :func:`connect` can be used as an infinite asynchronous iterator to - reconnect automatically on errors:: - - async for websocket in connect(...): - try: - ... - except websockets.exceptions.ConnectionClosed: - continue - - The connection is closed automatically after each iteration of the loop. - - If an error occurs while establishing the connection, :func:`connect` - retries with exponential backoff. The backoff delay starts at three - seconds and increases up to one minute. - - If an error occurs in the body of the loop, you can handle the exception - and :func:`connect` will reconnect with the next iteration; or you can - let the exception bubble up and break out of the loop. This lets you - decide which errors trigger a reconnection and which errors are fatal. - - Args: - uri: URI of the WebSocket server. - create_protocol: Factory for the :class:`asyncio.Protocol` managing - the connection. It defaults to :class:`WebSocketClientProtocol`. - Set it to a wrapper or a subclass to customize connection handling. - logger: Logger for this client. - It defaults to ``logging.getLogger("websockets.client")``. - See the :doc:`logging guide <../../topics/logging>` for details. - compression: The "permessage-deflate" extension is enabled by default. - Set ``compression`` to :obj:`None` to disable it. See the - :doc:`compression guide <../../topics/compression>` for details. - origin: Value of the ``Origin`` header, for servers that require it. - extensions: List of supported extensions, in order in which they - should be negotiated and run. - subprotocols: List of supported subprotocols, in order of decreasing - preference. - extra_headers: Arbitrary HTTP headers to add to the handshake request. - user_agent_header: Value of the ``User-Agent`` request header. - It defaults to ``"Python/x.y.z websockets/X.Y"``. - Setting it to :obj:`None` removes the header. - open_timeout: Timeout for opening the connection in seconds. - :obj:`None` disables the timeout. - - See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the - documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, - ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit``. - - Any other keyword arguments are passed the event loop's - :meth:`~asyncio.loop.create_connection` method. - - For example: - - * You can set ``ssl`` to a :class:`~ssl.SSLContext` to enforce TLS - settings. When connecting to a ``wss://`` URI, if ``ssl`` isn't - provided, a TLS context is created - with :func:`~ssl.create_default_context`. - - * You can set ``host`` and ``port`` to connect to a different host and - port from those found in ``uri``. This only changes the destination of - the TCP connection. The host name from ``uri`` is still used in the TLS - handshake for secure connections and in the ``Host`` header. - - Raises: - InvalidURI: If ``uri`` isn't a valid WebSocket URI. - OSError: If the TCP connection fails. - InvalidHandshake: If the opening handshake fails. - ~asyncio.TimeoutError: If the opening handshake times out. - - """ - - MAX_REDIRECTS_ALLOWED = int(os.environ.get("WEBSOCKETS_MAX_REDIRECTS", "10")) - - def __init__( - self, - uri: str, - *, - create_protocol: Callable[..., WebSocketClientProtocol] | None = None, - logger: LoggerLike | None = None, - compression: str | None = "deflate", - origin: Origin | None = None, - extensions: Sequence[ClientExtensionFactory] | None = None, - subprotocols: Sequence[Subprotocol] | None = None, - extra_headers: HeadersLike | None = None, - user_agent_header: str | None = USER_AGENT, - open_timeout: float | None = 10, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = None, - max_size: int | None = 2**20, - max_queue: int | None = 2**5, - read_limit: int = 2**16, - write_limit: int = 2**16, - **kwargs: Any, - ) -> None: - # Backwards compatibility: close_timeout used to be called timeout. - timeout: float | None = kwargs.pop("timeout", None) - if timeout is None: - timeout = 10 - else: - warnings.warn("rename timeout to close_timeout", DeprecationWarning) - # If both are specified, timeout is ignored. - if close_timeout is None: - close_timeout = timeout - - # Backwards compatibility: create_protocol used to be called klass. - klass: type[WebSocketClientProtocol] | None = kwargs.pop("klass", None) - if klass is None: - klass = WebSocketClientProtocol - else: - warnings.warn("rename klass to create_protocol", DeprecationWarning) - # If both are specified, klass is ignored. - if create_protocol is None: - create_protocol = klass - - # Backwards compatibility: recv() used to return None on closed connections - legacy_recv: bool = kwargs.pop("legacy_recv", False) - - # Backwards compatibility: the loop parameter used to be supported. - _loop: asyncio.AbstractEventLoop | None = kwargs.pop("loop", None) - if _loop is None: - loop = asyncio.get_event_loop() - else: - loop = _loop - warnings.warn("remove loop argument", DeprecationWarning) - - wsuri = parse_uri(uri) - if wsuri.secure: - kwargs.setdefault("ssl", True) - elif kwargs.get("ssl") is not None: - raise ValueError( - "connect() received a ssl argument for a ws:// URI, " - "use a wss:// URI to enable TLS" - ) - - if compression == "deflate": - extensions = enable_client_permessage_deflate(extensions) - elif compression is not None: - raise ValueError(f"unsupported compression: {compression}") - - if subprotocols is not None: - validate_subprotocols(subprotocols) - - # Help mypy and avoid this error: "type[WebSocketClientProtocol] | - # Callable[..., WebSocketClientProtocol]" not callable [misc] - create_protocol = cast(Callable[..., WebSocketClientProtocol], create_protocol) - factory = functools.partial( - create_protocol, - logger=logger, - origin=origin, - extensions=extensions, - subprotocols=subprotocols, - extra_headers=extra_headers, - user_agent_header=user_agent_header, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - close_timeout=close_timeout, - max_size=max_size, - max_queue=max_queue, - read_limit=read_limit, - write_limit=write_limit, - host=wsuri.host, - port=wsuri.port, - secure=wsuri.secure, - legacy_recv=legacy_recv, - loop=_loop, - ) - - if kwargs.pop("unix", False): - path: str | None = kwargs.pop("path", None) - create_connection = functools.partial( - loop.create_unix_connection, factory, path, **kwargs - ) - else: - host: str | None - port: int | None - if kwargs.get("sock") is None: - host, port = wsuri.host, wsuri.port - else: - # If sock is given, host and port shouldn't be specified. - host, port = None, None - if kwargs.get("ssl"): - kwargs.setdefault("server_hostname", wsuri.host) - # If host and port are given, override values from the URI. - host = kwargs.pop("host", host) - port = kwargs.pop("port", port) - create_connection = functools.partial( - loop.create_connection, factory, host, port, **kwargs - ) - - self.open_timeout = open_timeout - if logger is None: - logger = logging.getLogger("websockets.client") - self.logger = logger - - # This is a coroutine function. - self._create_connection = create_connection - self._uri = uri - self._wsuri = wsuri - - def handle_redirect(self, uri: str) -> None: - # Update the state of this instance to connect to a new URI. - old_uri = self._uri - old_wsuri = self._wsuri - new_uri = urllib.parse.urljoin(old_uri, uri) - new_wsuri = parse_uri(new_uri) - - # Forbid TLS downgrade. - if old_wsuri.secure and not new_wsuri.secure: - raise SecurityError("redirect from WSS to WS") - - same_origin = ( - old_wsuri.secure == new_wsuri.secure - and old_wsuri.host == new_wsuri.host - and old_wsuri.port == new_wsuri.port - ) - - # Rewrite secure, host, and port for cross-origin redirects. - # This preserves connection overrides with the host and port - # arguments if the redirect points to the same host and port. - if not same_origin: - factory = self._create_connection.args[0] - # Support TLS upgrade. - if not old_wsuri.secure and new_wsuri.secure: - factory.keywords["secure"] = True - self._create_connection.keywords.setdefault("ssl", True) - # Replace secure, host, and port arguments of the protocol factory. - factory = functools.partial( - factory.func, - *factory.args, - **dict(factory.keywords, host=new_wsuri.host, port=new_wsuri.port), - ) - # Replace secure, host, and port arguments of create_connection. - self._create_connection = functools.partial( - self._create_connection.func, - *(factory, new_wsuri.host, new_wsuri.port), - **self._create_connection.keywords, - ) - - # Set the new WebSocket URI. This suffices for same-origin redirects. - self._uri = new_uri - self._wsuri = new_wsuri - - # async for ... in connect(...): - - BACKOFF_INITIAL = float(os.environ.get("WEBSOCKETS_BACKOFF_INITIAL_DELAY", "5")) - BACKOFF_MIN = float(os.environ.get("WEBSOCKETS_BACKOFF_MIN_DELAY", "3.1")) - BACKOFF_MAX = float(os.environ.get("WEBSOCKETS_BACKOFF_MAX_DELAY", "90.0")) - BACKOFF_FACTOR = float(os.environ.get("WEBSOCKETS_BACKOFF_FACTOR", "1.618")) - - async def __aiter__(self) -> AsyncIterator[WebSocketClientProtocol]: - backoff_delay = self.BACKOFF_MIN / self.BACKOFF_FACTOR - while True: - try: - async with self as protocol: - yield protocol - except Exception as exc: - # Add a random initial delay between 0 and 5 seconds. - # See 7.2.3. Recovering from Abnormal Closure in RFC 6455. - if backoff_delay == self.BACKOFF_MIN: - initial_delay = random.random() * self.BACKOFF_INITIAL - self.logger.info( - "connect failed; reconnecting in %.1f seconds: %s", - initial_delay, - # Remove first argument when dropping Python 3.9. - traceback.format_exception_only(type(exc), exc)[0].strip(), - ) - await asyncio.sleep(initial_delay) - else: - self.logger.info( - "connect failed again; retrying in %d seconds: %s", - int(backoff_delay), - # Remove first argument when dropping Python 3.9. - traceback.format_exception_only(type(exc), exc)[0].strip(), - ) - await asyncio.sleep(int(backoff_delay)) - # Increase delay with truncated exponential backoff. - backoff_delay = backoff_delay * self.BACKOFF_FACTOR - backoff_delay = min(backoff_delay, self.BACKOFF_MAX) - continue - else: - # Connection succeeded - reset backoff delay - backoff_delay = self.BACKOFF_MIN - - # async with connect(...) as ...: - - async def __aenter__(self) -> WebSocketClientProtocol: - return await self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - await self.protocol.close() - - # ... = await connect(...) - - def __await__(self) -> Generator[Any, None, WebSocketClientProtocol]: - # Create a suitable iterator by calling __await__ on a coroutine. - return self.__await_impl__().__await__() - - async def __await_impl__(self) -> WebSocketClientProtocol: - async with asyncio_timeout(self.open_timeout): - for _redirects in range(self.MAX_REDIRECTS_ALLOWED): - _transport, protocol = await self._create_connection() - try: - await protocol.handshake( - self._wsuri, - origin=protocol.origin, - available_extensions=protocol.available_extensions, - available_subprotocols=protocol.available_subprotocols, - extra_headers=protocol.extra_headers, - ) - except RedirectHandshake as exc: - protocol.fail_connection() - await protocol.wait_closed() - self.handle_redirect(exc.uri) - # Avoid leaking a connected socket when the handshake fails. - except (Exception, asyncio.CancelledError): - protocol.fail_connection() - await protocol.wait_closed() - raise - else: - self.protocol = protocol - return protocol - else: - raise SecurityError("too many redirects") - - # ... = yield from connect(...) - remove when dropping Python < 3.10 - - __iter__ = __await__ - - -connect = Connect - - -def unix_connect( - path: str | None = None, - uri: str = "ws://localhost/", - **kwargs: Any, -) -> Connect: - """ - Similar to :func:`connect`, but for connecting to a Unix socket. - - This function builds upon the event loop's - :meth:`~asyncio.loop.create_unix_connection` method. - - It is only available on Unix. - - It's mainly useful for debugging servers listening on Unix sockets. - - Args: - path: File system path to the Unix socket. - uri: URI of the WebSocket server; the host is used in the TLS - handshake for secure connections and in the ``Host`` header. - - """ - return connect(uri=uri, path=path, unix=True, **kwargs) diff --git a/wip/SNIP/client/websockets/legacy/exceptions.py b/wip/SNIP/client/websockets/legacy/exceptions.py deleted file mode 100644 index 6604a45..0000000 --- a/wip/SNIP/client/websockets/legacy/exceptions.py +++ /dev/null @@ -1,71 +0,0 @@ -import http - -from .. import datastructures -from ..exceptions import ( - InvalidHandshake, - # InvalidMessage was incorrectly moved here in versions 14.0 and 14.1. - # noqa: F401 - # noqa: F401 -) -from ..typing import StatusLike - - -class InvalidStatusCode(InvalidHandshake): - """ - Raised when a handshake response status code is invalid. - - """ - - def __init__(self, status_code: int, headers: datastructures.Headers) -> None: - self.status_code = status_code - self.headers = headers - - def __str__(self) -> str: - return f"server rejected WebSocket connection: HTTP {self.status_code}" - - -class AbortHandshake(InvalidHandshake): - """ - Raised to abort the handshake on purpose and return an HTTP response. - - This exception is an implementation detail. - - The public API is - :meth:`~websockets.legacy.server.WebSocketServerProtocol.process_request`. - - Attributes: - status (~http.HTTPStatus): HTTP status code. - headers (Headers): HTTP response headers. - body (bytes): HTTP response body. - """ - - def __init__( - self, - status: StatusLike, - headers: datastructures.HeadersLike, - body: bytes = b"", - ) -> None: - # If a user passes an int instead of an HTTPStatus, fix it automatically. - self.status = http.HTTPStatus(status) - self.headers = datastructures.Headers(headers) - self.body = body - - def __str__(self) -> str: - return ( - f"HTTP {self.status:d}, {len(self.headers)} headers, {len(self.body)} bytes" - ) - - -class RedirectHandshake(InvalidHandshake): - """ - Raised when a handshake gets redirected. - - This exception is an implementation detail. - - """ - - def __init__(self, uri: str) -> None: - self.uri = uri - - def __str__(self) -> str: - return f"redirect to {self.uri}" diff --git a/wip/SNIP/client/websockets/legacy/framing.py b/wip/SNIP/client/websockets/legacy/framing.py deleted file mode 100644 index add0c6e..0000000 --- a/wip/SNIP/client/websockets/legacy/framing.py +++ /dev/null @@ -1,225 +0,0 @@ -from __future__ import annotations - -import struct -from collections.abc import Awaitable, Sequence -from typing import Any, Callable, NamedTuple - -from .. import extensions, frames -from ..exceptions import PayloadTooBig, ProtocolError -from ..frames import BytesLike -from ..typing import Data - - -try: - from ..speedups import apply_mask -except ImportError: - from ..utils import apply_mask - - -class Frame(NamedTuple): - fin: bool - opcode: frames.Opcode - data: bytes - rsv1: bool = False - rsv2: bool = False - rsv3: bool = False - - @property - def new_frame(self) -> frames.Frame: - return frames.Frame( - self.opcode, - self.data, - self.fin, - self.rsv1, - self.rsv2, - self.rsv3, - ) - - def __str__(self) -> str: - return str(self.new_frame) - - def check(self) -> None: - return self.new_frame.check() - - @classmethod - async def read( - cls, - reader: Callable[[int], Awaitable[bytes]], - *, - mask: bool, - max_size: int | None = None, - extensions: Sequence[extensions.Extension] | None = None, - ) -> Frame: - """ - Read a WebSocket frame. - - Args: - reader: Coroutine that reads exactly the requested number of - bytes, unless the end of file is reached. - mask: Whether the frame should be masked i.e. whether the read - happens on the server side. - max_size: Maximum payload size in bytes. - extensions: List of extensions, applied in reverse order. - - Raises: - PayloadTooBig: If the frame exceeds ``max_size``. - ProtocolError: If the frame contains incorrect values. - - """ - - # Read the header. - data = await reader(2) - head1, head2 = struct.unpack("!BB", data) - - # While not Pythonic, this is marginally faster than calling bool(). - fin = True if head1 & 0b10000000 else False - rsv1 = True if head1 & 0b01000000 else False - rsv2 = True if head1 & 0b00100000 else False - rsv3 = True if head1 & 0b00010000 else False - - try: - opcode = frames.Opcode(head1 & 0b00001111) - except ValueError as exc: - raise ProtocolError("invalid opcode") from exc - - if (True if head2 & 0b10000000 else False) != mask: - raise ProtocolError("incorrect masking") - - length = head2 & 0b01111111 - if length == 126: - data = await reader(2) - (length,) = struct.unpack("!H", data) - elif length == 127: - data = await reader(8) - (length,) = struct.unpack("!Q", data) - if max_size is not None and length > max_size: - raise PayloadTooBig(length, max_size) - if mask: - mask_bits = await reader(4) - - # Read the data. - data = await reader(length) - if mask: - data = apply_mask(data, mask_bits) - - new_frame = frames.Frame(opcode, data, fin, rsv1, rsv2, rsv3) - - if extensions is None: - extensions = [] - for extension in reversed(extensions): - new_frame = extension.decode(new_frame, max_size=max_size) - - new_frame.check() - - return cls( - new_frame.fin, - new_frame.opcode, - new_frame.data, - new_frame.rsv1, - new_frame.rsv2, - new_frame.rsv3, - ) - - def write( - self, - write: Callable[[bytes], Any], - *, - mask: bool, - extensions: Sequence[extensions.Extension] | None = None, - ) -> None: - """ - Write a WebSocket frame. - - Args: - frame: Frame to write. - write: Function that writes bytes. - mask: Whether the frame should be masked i.e. whether the write - happens on the client side. - extensions: List of extensions, applied in order. - - Raises: - ProtocolError: If the frame contains incorrect values. - - """ - # The frame is written in a single call to write in order to prevent - # TCP fragmentation. See #68 for details. This also makes it safe to - # send frames concurrently from multiple coroutines. - write(self.new_frame.serialize(mask=mask, extensions=extensions)) - - -def prepare_data(data: Data) -> tuple[int, bytes]: - """ - Convert a string or byte-like object to an opcode and a bytes-like object. - - This function is designed for data frames. - - If ``data`` is a :class:`str`, return ``OP_TEXT`` and a :class:`bytes` - object encoding ``data`` in UTF-8. - - If ``data`` is a bytes-like object, return ``OP_BINARY`` and a bytes-like - object. - - Raises: - TypeError: If ``data`` doesn't have a supported type. - - """ - if isinstance(data, str): - return frames.Opcode.TEXT, data.encode() - elif isinstance(data, BytesLike): - return frames.Opcode.BINARY, data - else: - raise TypeError("data must be str or bytes-like") - - -def prepare_ctrl(data: Data) -> bytes: - """ - Convert a string or byte-like object to bytes. - - This function is designed for ping and pong frames. - - If ``data`` is a :class:`str`, return a :class:`bytes` object encoding - ``data`` in UTF-8. - - If ``data`` is a bytes-like object, return a :class:`bytes` object. - - Raises: - TypeError: If ``data`` doesn't have a supported type. - - """ - if isinstance(data, str): - return data.encode() - elif isinstance(data, BytesLike): - return bytes(data) - else: - raise TypeError("data must be str or bytes-like") - - -# Backwards compatibility with previously documented public APIs -encode_data = prepare_ctrl - -# Backwards compatibility with previously documented public APIs -from ..frames import Close # noqa: E402 F401, I001 - - -def parse_close(data: bytes) -> tuple[int, str]: - """ - Parse the payload from a close frame. - - Returns: - Close code and reason. - - Raises: - ProtocolError: If data is ill-formed. - UnicodeDecodeError: If the reason isn't valid UTF-8. - - """ - close = Close.parse(data) - return close.code, close.reason - - -def serialize_close(code: int, reason: str) -> bytes: - """ - Serialize the payload for a close frame. - - """ - return Close(code, reason).serialize() diff --git a/wip/SNIP/client/websockets/legacy/handshake.py b/wip/SNIP/client/websockets/legacy/handshake.py deleted file mode 100644 index 6a7157c..0000000 --- a/wip/SNIP/client/websockets/legacy/handshake.py +++ /dev/null @@ -1,158 +0,0 @@ -from __future__ import annotations - -import base64 -import binascii - -from ..datastructures import Headers, MultipleValuesError -from ..exceptions import InvalidHeader, InvalidHeaderValue, InvalidUpgrade -from ..headers import parse_connection, parse_upgrade -from ..typing import ConnectionOption, UpgradeProtocol -from ..utils import accept_key as accept, generate_key - - -__all__ = ["build_request", "check_request", "build_response", "check_response"] - - -def build_request(headers: Headers) -> str: - """ - Build a handshake request to send to the server. - - Update request headers passed in argument. - - Args: - headers: Handshake request headers. - - Returns: - ``key`` that must be passed to :func:`check_response`. - - """ - key = generate_key() - headers["Upgrade"] = "websocket" - headers["Connection"] = "Upgrade" - headers["Sec-WebSocket-Key"] = key - headers["Sec-WebSocket-Version"] = "13" - return key - - -def check_request(headers: Headers) -> str: - """ - Check a handshake request received from the client. - - This function doesn't verify that the request is an HTTP/1.1 or higher GET - request and doesn't perform ``Host`` and ``Origin`` checks. These controls - are usually performed earlier in the HTTP request handling code. They're - the responsibility of the caller. - - Args: - headers: Handshake request headers. - - Returns: - ``key`` that must be passed to :func:`build_response`. - - Raises: - InvalidHandshake: If the handshake request is invalid. - Then, the server must return a 400 Bad Request error. - - """ - connection: list[ConnectionOption] = sum( - [parse_connection(value) for value in headers.get_all("Connection")], [] - ) - - if not any(value.lower() == "upgrade" for value in connection): - raise InvalidUpgrade("Connection", ", ".join(connection)) - - upgrade: list[UpgradeProtocol] = sum( - [parse_upgrade(value) for value in headers.get_all("Upgrade")], [] - ) - - # For compatibility with non-strict implementations, ignore case when - # checking the Upgrade header. The RFC always uses "websocket", except - # in section 11.2. (IANA registration) where it uses "WebSocket". - if not (len(upgrade) == 1 and upgrade[0].lower() == "websocket"): - raise InvalidUpgrade("Upgrade", ", ".join(upgrade)) - - try: - s_w_key = headers["Sec-WebSocket-Key"] - except KeyError as exc: - raise InvalidHeader("Sec-WebSocket-Key") from exc - except MultipleValuesError as exc: - raise InvalidHeader("Sec-WebSocket-Key", "multiple values") from exc - - try: - raw_key = base64.b64decode(s_w_key.encode(), validate=True) - except binascii.Error as exc: - raise InvalidHeaderValue("Sec-WebSocket-Key", s_w_key) from exc - if len(raw_key) != 16: - raise InvalidHeaderValue("Sec-WebSocket-Key", s_w_key) - - try: - s_w_version = headers["Sec-WebSocket-Version"] - except KeyError as exc: - raise InvalidHeader("Sec-WebSocket-Version") from exc - except MultipleValuesError as exc: - raise InvalidHeader("Sec-WebSocket-Version", "multiple values") from exc - - if s_w_version != "13": - raise InvalidHeaderValue("Sec-WebSocket-Version", s_w_version) - - return s_w_key - - -def build_response(headers: Headers, key: str) -> None: - """ - Build a handshake response to send to the client. - - Update response headers passed in argument. - - Args: - headers: Handshake response headers. - key: Returned by :func:`check_request`. - - """ - headers["Upgrade"] = "websocket" - headers["Connection"] = "Upgrade" - headers["Sec-WebSocket-Accept"] = accept(key) - - -def check_response(headers: Headers, key: str) -> None: - """ - Check a handshake response received from the server. - - This function doesn't verify that the response is an HTTP/1.1 or higher - response with a 101 status code. These controls are the responsibility of - the caller. - - Args: - headers: Handshake response headers. - key: Returned by :func:`build_request`. - - Raises: - InvalidHandshake: If the handshake response is invalid. - - """ - connection: list[ConnectionOption] = sum( - [parse_connection(value) for value in headers.get_all("Connection")], [] - ) - - if not any(value.lower() == "upgrade" for value in connection): - raise InvalidUpgrade("Connection", " ".join(connection)) - - upgrade: list[UpgradeProtocol] = sum( - [parse_upgrade(value) for value in headers.get_all("Upgrade")], [] - ) - - # For compatibility with non-strict implementations, ignore case when - # checking the Upgrade header. The RFC always uses "websocket", except - # in section 11.2. (IANA registration) where it uses "WebSocket". - if not (len(upgrade) == 1 and upgrade[0].lower() == "websocket"): - raise InvalidUpgrade("Upgrade", ", ".join(upgrade)) - - try: - s_w_accept = headers["Sec-WebSocket-Accept"] - except KeyError as exc: - raise InvalidHeader("Sec-WebSocket-Accept") from exc - except MultipleValuesError as exc: - raise InvalidHeader("Sec-WebSocket-Accept", "multiple values") from exc - - if s_w_accept != accept(key): - raise InvalidHeaderValue("Sec-WebSocket-Accept", s_w_accept) diff --git a/wip/SNIP/client/websockets/legacy/http.py b/wip/SNIP/client/websockets/legacy/http.py deleted file mode 100644 index a7c8a92..0000000 --- a/wip/SNIP/client/websockets/legacy/http.py +++ /dev/null @@ -1,201 +0,0 @@ -from __future__ import annotations - -import asyncio -import os -import re - -from ..datastructures import Headers -from ..exceptions import SecurityError - - -__all__ = ["read_request", "read_response"] - -MAX_NUM_HEADERS = int(os.environ.get("WEBSOCKETS_MAX_NUM_HEADERS", "128")) -MAX_LINE_LENGTH = int(os.environ.get("WEBSOCKETS_MAX_LINE_LENGTH", "8192")) - - -def d(value: bytes) -> str: - """ - Decode a bytestring for interpolating into an error message. - - """ - return value.decode(errors="backslashreplace") - - -# See https://datatracker.ietf.org/doc/html/rfc7230#appendix-B. - -# Regex for validating header names. - -_token_re = re.compile(rb"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+") - -# Regex for validating header values. - -# We don't attempt to support obsolete line folding. - -# Include HTAB (\x09), SP (\x20), VCHAR (\x21-\x7e), obs-text (\x80-\xff). - -# The ABNF is complicated because it attempts to express that optional -# whitespace is ignored. We strip whitespace and don't revalidate that. - -# See also https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189 - -_value_re = re.compile(rb"[\x09\x20-\x7e\x80-\xff]*") - - -async def read_request(stream: asyncio.StreamReader) -> tuple[str, Headers]: - """ - Read an HTTP/1.1 GET request and return ``(path, headers)``. - - ``path`` isn't URL-decoded or validated in any way. - - ``path`` and ``headers`` are expected to contain only ASCII characters. - Other characters are represented with surrogate escapes. - - :func:`read_request` doesn't attempt to read the request body because - WebSocket handshake requests don't have one. If the request contains a - body, it may be read from ``stream`` after this coroutine returns. - - Args: - stream: Input to read the request from. - - Raises: - EOFError: If the connection is closed without a full HTTP request. - SecurityError: If the request exceeds a security limit. - ValueError: If the request isn't well formatted. - - """ - # https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.1 - - # Parsing is simple because fixed values are expected for method and - # version and because path isn't checked. Since WebSocket software tends - # to implement HTTP/1.1 strictly, there's little need for lenient parsing. - - try: - request_line = await read_line(stream) - except EOFError as exc: - raise EOFError("connection closed while reading HTTP request line") from exc - - try: - method, raw_path, version = request_line.split(b" ", 2) - except ValueError: # not enough values to unpack (expected 3, got 1-2) - raise ValueError(f"invalid HTTP request line: {d(request_line)}") from None - - if method != b"GET": - raise ValueError(f"unsupported HTTP method: {d(method)}") - if version != b"HTTP/1.1": - raise ValueError(f"unsupported HTTP version: {d(version)}") - path = raw_path.decode("ascii", "surrogateescape") - - headers = await read_headers(stream) - - return path, headers - - -async def read_response(stream: asyncio.StreamReader) -> tuple[int, str, Headers]: - """ - Read an HTTP/1.1 response and return ``(status_code, reason, headers)``. - - ``reason`` and ``headers`` are expected to contain only ASCII characters. - Other characters are represented with surrogate escapes. - - :func:`read_request` doesn't attempt to read the response body because - WebSocket handshake responses don't have one. If the response contains a - body, it may be read from ``stream`` after this coroutine returns. - - Args: - stream: Input to read the response from. - - Raises: - EOFError: If the connection is closed without a full HTTP response. - SecurityError: If the response exceeds a security limit. - ValueError: If the response isn't well formatted. - - """ - # https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2 - - # As in read_request, parsing is simple because a fixed value is expected - # for version, status_code is a 3-digit number, and reason can be ignored. - - try: - status_line = await read_line(stream) - except EOFError as exc: - raise EOFError("connection closed while reading HTTP status line") from exc - - try: - version, raw_status_code, raw_reason = status_line.split(b" ", 2) - except ValueError: # not enough values to unpack (expected 3, got 1-2) - raise ValueError(f"invalid HTTP status line: {d(status_line)}") from None - - if version != b"HTTP/1.1": - raise ValueError(f"unsupported HTTP version: {d(version)}") - try: - status_code = int(raw_status_code) - except ValueError: # invalid literal for int() with base 10 - raise ValueError(f"invalid HTTP status code: {d(raw_status_code)}") from None - if not 100 <= status_code < 1000: - raise ValueError(f"unsupported HTTP status code: {d(raw_status_code)}") - if not _value_re.fullmatch(raw_reason): - raise ValueError(f"invalid HTTP reason phrase: {d(raw_reason)}") - reason = raw_reason.decode() - - headers = await read_headers(stream) - - return status_code, reason, headers - - -async def read_headers(stream: asyncio.StreamReader) -> Headers: - """ - Read HTTP headers from ``stream``. - - Non-ASCII characters are represented with surrogate escapes. - - """ - # https://datatracker.ietf.org/doc/html/rfc7230#section-3.2 - - # We don't attempt to support obsolete line folding. - - headers = Headers() - for _ in range(MAX_NUM_HEADERS + 1): - try: - line = await read_line(stream) - except EOFError as exc: - raise EOFError("connection closed while reading HTTP headers") from exc - if line == b"": - break - - try: - raw_name, raw_value = line.split(b":", 1) - except ValueError: # not enough values to unpack (expected 2, got 1) - raise ValueError(f"invalid HTTP header line: {d(line)}") from None - if not _token_re.fullmatch(raw_name): - raise ValueError(f"invalid HTTP header name: {d(raw_name)}") - raw_value = raw_value.strip(b" \t") - if not _value_re.fullmatch(raw_value): - raise ValueError(f"invalid HTTP header value: {d(raw_value)}") - - name = raw_name.decode("ascii") # guaranteed to be ASCII at this point - value = raw_value.decode("ascii", "surrogateescape") - headers[name] = value - - else: - raise SecurityError("too many HTTP headers") - - return headers - - -async def read_line(stream: asyncio.StreamReader) -> bytes: - """ - Read a single line from ``stream``. - - CRLF is stripped from the return value. - - """ - # Security: this is bounded by the StreamReader's limit (default = 32 KiB). - line = await stream.readline() - # Security: this guarantees header values are small (hard-coded = 8 KiB) - if len(line) > MAX_LINE_LENGTH: - raise SecurityError("line too long") - # Not mandatory but safe - https://datatracker.ietf.org/doc/html/rfc7230#section-3.5 - if not line.endswith(b"\r\n"): - raise EOFError("line without CRLF") - return line[:-2] diff --git a/wip/SNIP/client/websockets/legacy/protocol.py b/wip/SNIP/client/websockets/legacy/protocol.py deleted file mode 100644 index db126c0..0000000 --- a/wip/SNIP/client/websockets/legacy/protocol.py +++ /dev/null @@ -1,1641 +0,0 @@ -from __future__ import annotations - -import asyncio -import codecs -import collections -import logging -import random -import ssl -import struct -import sys -import time -import traceback -import uuid -import warnings -from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Iterable, Mapping -from typing import Any, Callable, Deque, cast - -from ..asyncio.compatibility import asyncio_timeout -from ..datastructures import Headers -from ..exceptions import ( - ConnectionClosed, - ConnectionClosedError, - ConnectionClosedOK, - InvalidState, - PayloadTooBig, - ProtocolError, -) -from ..extensions import Extension -from ..frames import ( - OK_CLOSE_CODES, - OP_BINARY, - OP_CLOSE, - OP_CONT, - OP_PING, - OP_PONG, - OP_TEXT, - Close, - CloseCode, - Opcode, -) -from ..protocol import State -from ..typing import Data, LoggerLike, Subprotocol -from .framing import Frame, prepare_ctrl, prepare_data - - -__all__ = ["WebSocketCommonProtocol"] - - -# In order to ensure consistency, the code always checks the current value of -# WebSocketCommonProtocol.state before assigning a new value and never yields -# between the check and the assignment. - - -class WebSocketCommonProtocol(asyncio.Protocol): - """ - WebSocket connection. - - :class:`WebSocketCommonProtocol` provides APIs shared between WebSocket - servers and clients. You shouldn't use it directly. Instead, use - :class:`~websockets.legacy.client.WebSocketClientProtocol` or - :class:`~websockets.legacy.server.WebSocketServerProtocol`. - - This documentation focuses on low-level details that aren't covered in the - documentation of :class:`~websockets.legacy.client.WebSocketClientProtocol` - and :class:`~websockets.legacy.server.WebSocketServerProtocol` for the sake - of simplicity. - - Once the connection is open, a Ping_ frame is sent every ``ping_interval`` - seconds. This serves as a keepalive. It helps keeping the connection open, - especially in the presence of proxies with short timeouts on inactive - connections. Set ``ping_interval`` to :obj:`None` to disable this behavior. - - .. _Ping: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2 - - If the corresponding Pong_ frame isn't received within ``ping_timeout`` - seconds, the connection is considered unusable and is closed with code 1011. - This ensures that the remote endpoint remains responsive. Set - ``ping_timeout`` to :obj:`None` to disable this behavior. - - .. _Pong: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.3 - - See the discussion of :doc:`keepalive <../../topics/keepalive>` for details. - - The ``close_timeout`` parameter defines a maximum wait time for completing - the closing handshake and terminating the TCP connection. For legacy - reasons, :meth:`close` completes in at most ``5 * close_timeout`` seconds - for clients and ``4 * close_timeout`` for servers. - - ``close_timeout`` is a parameter of the protocol because websockets usually - calls :meth:`close` implicitly upon exit: - - * on the client side, when using :func:`~websockets.legacy.client.connect` - as a context manager; - * on the server side, when the connection handler terminates. - - To apply a timeout to any other API, wrap it in :func:`~asyncio.timeout` or - :func:`~asyncio.wait_for`. - - The ``max_size`` parameter enforces the maximum size for incoming messages - in bytes. The default value is 1 MiB. If a larger message is received, - :meth:`recv` will raise :exc:`~websockets.exceptions.ConnectionClosedError` - and the connection will be closed with code 1009. - - The ``max_queue`` parameter sets the maximum length of the queue that - holds incoming messages. The default value is ``32``. Messages are added - to an in-memory queue when they're received; then :meth:`recv` pops from - that queue. In order to prevent excessive memory consumption when - messages are received faster than they can be processed, the queue must - be bounded. If the queue fills up, the protocol stops processing incoming - data until :meth:`recv` is called. In this situation, various receive - buffers (at least in :mod:`asyncio` and in the OS) will fill up, then the - TCP receive window will shrink, slowing down transmission to avoid packet - loss. - - Since Python can use up to 4 bytes of memory to represent a single - character, each connection may use up to ``4 * max_size * max_queue`` - bytes of memory to store incoming messages. By default, this is 128 MiB. - You may want to lower the limits, depending on your application's - requirements. - - The ``read_limit`` argument sets the high-water limit of the buffer for - incoming bytes. The low-water limit is half the high-water limit. The - default value is 64 KiB, half of asyncio's default (based on the current - implementation of :class:`~asyncio.StreamReader`). - - The ``write_limit`` argument sets the high-water limit of the buffer for - outgoing bytes. The low-water limit is a quarter of the high-water limit. - The default value is 64 KiB, equal to asyncio's default (based on the - current implementation of ``FlowControlMixin``). - - See the discussion of :doc:`memory usage <../../topics/memory>` for details. - - Args: - logger: Logger for this server. - It defaults to ``logging.getLogger("websockets.protocol")``. - See the :doc:`logging guide <../../topics/logging>` for details. - ping_interval: Interval between keepalive pings in seconds. - :obj:`None` disables keepalive. - ping_timeout: Timeout for keepalive pings in seconds. - :obj:`None` disables timeouts. - close_timeout: Timeout for closing the connection in seconds. - For legacy reasons, the actual timeout is 4 or 5 times larger. - max_size: Maximum size of incoming messages in bytes. - :obj:`None` disables the limit. - max_queue: Maximum number of incoming messages in receive buffer. - :obj:`None` disables the limit. - read_limit: High-water mark of read buffer in bytes. - write_limit: High-water mark of write buffer in bytes. - - """ - - # There are only two differences between the client-side and server-side - # behavior: masking the payload and closing the underlying TCP connection. - # Set is_client = True/False and side = "client"/"server" to pick a side. - is_client: bool - side: str = "undefined" - - def __init__( - self, - *, - logger: LoggerLike | None = None, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = None, - max_size: int | None = 2**20, - max_queue: int | None = 2**5, - read_limit: int = 2**16, - write_limit: int = 2**16, - # The following arguments are kept only for backwards compatibility. - host: str | None = None, - port: int | None = None, - secure: bool | None = None, - legacy_recv: bool = False, - loop: asyncio.AbstractEventLoop | None = None, - timeout: float | None = None, - ) -> None: - if legacy_recv: # pragma: no cover - warnings.warn("legacy_recv is deprecated", DeprecationWarning) - - # Backwards compatibility: close_timeout used to be called timeout. - if timeout is None: - timeout = 10 - else: - warnings.warn("rename timeout to close_timeout", DeprecationWarning) - # If both are specified, timeout is ignored. - if close_timeout is None: - close_timeout = timeout - - # Backwards compatibility: the loop parameter used to be supported. - if loop is None: - loop = asyncio.get_event_loop() - else: - warnings.warn("remove loop argument", DeprecationWarning) - - self.ping_interval = ping_interval - self.ping_timeout = ping_timeout - self.close_timeout = close_timeout - self.max_size = max_size - self.max_queue = max_queue - self.read_limit = read_limit - self.write_limit = write_limit - - # Unique identifier. For logs. - self.id: uuid.UUID = uuid.uuid4() - """Unique identifier of the connection. Useful in logs.""" - - # Logger or LoggerAdapter for this connection. - if logger is None: - logger = logging.getLogger("websockets.protocol") - self.logger: LoggerLike = logging.LoggerAdapter(logger, {"websocket": self}) - """Logger for this connection.""" - - # Track if DEBUG is enabled. Shortcut logging calls if it isn't. - self.debug = logger.isEnabledFor(logging.DEBUG) - - self.loop = loop - - self._host = host - self._port = port - self._secure = secure - self.legacy_recv = legacy_recv - - # Configure read buffer limits. The high-water limit is defined by - # ``self.read_limit``. The ``limit`` argument controls the line length - # limit and half the buffer limit of :class:`~asyncio.StreamReader`. - # That's why it must be set to half of ``self.read_limit``. - self.reader = asyncio.StreamReader(limit=read_limit // 2, loop=loop) - - # Copied from asyncio.FlowControlMixin - self._paused = False - self._drain_waiter: asyncio.Future[None] | None = None - - self._drain_lock = asyncio.Lock() - - # This class implements the data transfer and closing handshake, which - # are shared between the client-side and the server-side. - # Subclasses implement the opening handshake and, on success, execute - # :meth:`connection_open` to change the state to OPEN. - self.state = State.CONNECTING - if self.debug: - self.logger.debug("= connection is CONNECTING") - - # HTTP protocol parameters. - self.path: str - """Path of the opening handshake request.""" - self.request_headers: Headers - """Opening handshake request headers.""" - self.response_headers: Headers - """Opening handshake response headers.""" - - # WebSocket protocol parameters. - self.extensions: list[Extension] = [] - self.subprotocol: Subprotocol | None = None - """Subprotocol, if one was negotiated.""" - - # Close code and reason, set when a close frame is sent or received. - self.close_rcvd: Close | None = None - self.close_sent: Close | None = None - self.close_rcvd_then_sent: bool | None = None - - # Completed when the connection state becomes CLOSED. Translates the - # :meth:`connection_lost` callback to a :class:`~asyncio.Future` - # that can be awaited. (Other :class:`~asyncio.Protocol` callbacks are - # translated by ``self.stream_reader``). - self.connection_lost_waiter: asyncio.Future[None] = loop.create_future() - - # Queue of received messages. - self.messages: Deque[Data] = collections.deque() - self._pop_message_waiter: asyncio.Future[None] | None = None - self._put_message_waiter: asyncio.Future[None] | None = None - - # Protect sending fragmented messages. - self._fragmented_message_waiter: asyncio.Future[None] | None = None - - # Mapping of ping IDs to pong waiters, in chronological order. - self.pings: dict[bytes, tuple[asyncio.Future[float], float]] = {} - - self.latency: float = 0 - """ - Latency of the connection, in seconds. - - Latency is defined as the round-trip time of the connection. It is - measured by sending a Ping frame and waiting for a matching Pong frame. - Before the first measurement, :attr:`latency` is ``0``. - - By default, websockets enables a :ref:`keepalive ` mechanism - that sends Ping frames automatically at regular intervals. You can also - send Ping frames and measure latency with :meth:`ping`. - """ - - # Task running the data transfer. - self.transfer_data_task: asyncio.Task[None] - - # Exception that occurred during data transfer, if any. - self.transfer_data_exc: BaseException | None = None - - # Task sending keepalive pings. - self.keepalive_ping_task: asyncio.Task[None] - - # Task closing the TCP connection. - self.close_connection_task: asyncio.Task[None] - - # Copied from asyncio.FlowControlMixin - async def _drain_helper(self) -> None: # pragma: no cover - if self.connection_lost_waiter.done(): - raise ConnectionResetError("Connection lost") - if not self._paused: - return - waiter = self._drain_waiter - assert waiter is None or waiter.cancelled() - waiter = self.loop.create_future() - self._drain_waiter = waiter - await waiter - - # Copied from asyncio.StreamWriter - async def _drain(self) -> None: # pragma: no cover - if self.reader is not None: - exc = self.reader.exception() - if exc is not None: - raise exc - if self.transport is not None: - if self.transport.is_closing(): - # Yield to the event loop so connection_lost() may be - # called. Without this, _drain_helper() would return - # immediately, and code that calls - # write(...); yield from drain() - # in a loop would never call connection_lost(), so it - # would not see an error when the socket is closed. - await asyncio.sleep(0) - await self._drain_helper() - - def connection_open(self) -> None: - """ - Callback when the WebSocket opening handshake completes. - - Enter the OPEN state and start the data transfer phase. - - """ - # 4.1. The WebSocket Connection is Established. - assert self.state is State.CONNECTING - self.state = State.OPEN - if self.debug: - self.logger.debug("= connection is OPEN") - # Start the task that receives incoming WebSocket messages. - self.transfer_data_task = self.loop.create_task(self.transfer_data()) - # Start the task that sends pings at regular intervals. - self.keepalive_ping_task = self.loop.create_task(self.keepalive_ping()) - # Start the task that eventually closes the TCP connection. - self.close_connection_task = self.loop.create_task(self.close_connection()) - - @property - def host(self) -> str | None: - alternative = "remote_address" if self.is_client else "local_address" - warnings.warn(f"use {alternative}[0] instead of host", DeprecationWarning) - return self._host - - @property - def port(self) -> int | None: - alternative = "remote_address" if self.is_client else "local_address" - warnings.warn(f"use {alternative}[1] instead of port", DeprecationWarning) - return self._port - - @property - def secure(self) -> bool | None: - warnings.warn("don't use secure", DeprecationWarning) - return self._secure - - # Public API - - @property - def local_address(self) -> Any: - """ - Local address of the connection. - - For IPv4 connections, this is a ``(host, port)`` tuple. - - The format of the address depends on the address family; - see :meth:`~socket.socket.getsockname`. - - :obj:`None` if the TCP connection isn't established yet. - - """ - try: - transport = self.transport - except AttributeError: - return None - else: - return transport.get_extra_info("sockname") - - @property - def remote_address(self) -> Any: - """ - Remote address of the connection. - - For IPv4 connections, this is a ``(host, port)`` tuple. - - The format of the address depends on the address family; - see :meth:`~socket.socket.getpeername`. - - :obj:`None` if the TCP connection isn't established yet. - - """ - try: - transport = self.transport - except AttributeError: - return None - else: - return transport.get_extra_info("peername") - - @property - def open(self) -> bool: - """ - :obj:`True` when the connection is open; :obj:`False` otherwise. - - This attribute may be used to detect disconnections. However, this - approach is discouraged per the EAFP_ principle. Instead, you should - handle :exc:`~websockets.exceptions.ConnectionClosed` exceptions. - - .. _EAFP: https://docs.python.org/3/glossary.html#term-eafp - - """ - return self.state is State.OPEN and not self.transfer_data_task.done() - - @property - def closed(self) -> bool: - """ - :obj:`True` when the connection is closed; :obj:`False` otherwise. - - Be aware that both :attr:`open` and :attr:`closed` are :obj:`False` - during the opening and closing sequences. - - """ - return self.state is State.CLOSED - - @property - def close_code(self) -> int | None: - """ - WebSocket close code, defined in `section 7.1.5 of RFC 6455`_. - - .. _section 7.1.5 of RFC 6455: - https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5 - - :obj:`None` if the connection isn't closed yet. - - """ - if self.state is not State.CLOSED: - return None - elif self.close_rcvd is None: - return CloseCode.ABNORMAL_CLOSURE - else: - return self.close_rcvd.code - - @property - def close_reason(self) -> str | None: - """ - WebSocket close reason, defined in `section 7.1.6 of RFC 6455`_. - - .. _section 7.1.6 of RFC 6455: - https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6 - - :obj:`None` if the connection isn't closed yet. - - """ - if self.state is not State.CLOSED: - return None - elif self.close_rcvd is None: - return "" - else: - return self.close_rcvd.reason - - async def __aiter__(self) -> AsyncIterator[Data]: - """ - Iterate on incoming messages. - - The iterator exits normally when the connection is closed with the close - code 1000 (OK) or 1001 (going away) or without a close code. - - It raises a :exc:`~websockets.exceptions.ConnectionClosedError` - exception when the connection is closed with any other code. - - """ - try: - while True: - yield await self.recv() - except ConnectionClosedOK: - return - - async def recv(self) -> Data: - """ - Receive the next message. - - When the connection is closed, :meth:`recv` raises - :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it raises - :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal - connection closure and - :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol - error or a network failure. This is how you detect the end of the - message stream. - - Canceling :meth:`recv` is safe. There's no risk of losing the next - message. The next invocation of :meth:`recv` will return it. - - This makes it possible to enforce a timeout by wrapping :meth:`recv` in - :func:`~asyncio.timeout` or :func:`~asyncio.wait_for`. - - Returns: - A string (:class:`str`) for a Text_ frame. A bytestring - (:class:`bytes`) for a Binary_ frame. - - .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - .. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - Raises: - ConnectionClosed: When the connection is closed. - RuntimeError: If two coroutines call :meth:`recv` concurrently. - - """ - if self._pop_message_waiter is not None: - raise RuntimeError( - "cannot call recv while another coroutine " - "is already waiting for the next message" - ) - - # Don't await self.ensure_open() here: - # - messages could be available in the queue even if the connection - # is closed; - # - messages could be received before the closing frame even if the - # connection is closing. - - # Wait until there's a message in the queue (if necessary) or the - # connection is closed. - while len(self.messages) <= 0: - pop_message_waiter: asyncio.Future[None] = self.loop.create_future() - self._pop_message_waiter = pop_message_waiter - try: - # If asyncio.wait() is canceled, it doesn't cancel - # pop_message_waiter and self.transfer_data_task. - await asyncio.wait( - [pop_message_waiter, self.transfer_data_task], - return_when=asyncio.FIRST_COMPLETED, - ) - finally: - self._pop_message_waiter = None - - # If asyncio.wait(...) exited because self.transfer_data_task - # completed before receiving a new message, raise a suitable - # exception (or return None if legacy_recv is enabled). - if not pop_message_waiter.done(): - if self.legacy_recv: - return None # type: ignore - else: - # Wait until the connection is closed to raise - # ConnectionClosed with the correct code and reason. - await self.ensure_open() - - # Pop a message from the queue. - message = self.messages.popleft() - - # Notify transfer_data(). - if self._put_message_waiter is not None: - self._put_message_waiter.set_result(None) - self._put_message_waiter = None - - return message - - async def send( - self, - message: Data | Iterable[Data] | AsyncIterable[Data], - ) -> None: - """ - Send a message. - - A string (:class:`str`) is sent as a Text_ frame. A bytestring or - bytes-like object (:class:`bytes`, :class:`bytearray`, or - :class:`memoryview`) is sent as a Binary_ frame. - - .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - .. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - :meth:`send` also accepts an iterable or an asynchronous iterable of - strings, bytestrings, or bytes-like objects to enable fragmentation_. - Each item is treated as a message fragment and sent in its own frame. - All items must be of the same type, or else :meth:`send` will raise a - :exc:`TypeError` and the connection will be closed. - - .. _fragmentation: https://datatracker.ietf.org/doc/html/rfc6455#section-5.4 - - :meth:`send` rejects dict-like objects because this is often an error. - (If you want to send the keys of a dict-like object as fragments, call - its :meth:`~dict.keys` method and pass the result to :meth:`send`.) - - Canceling :meth:`send` is discouraged. Instead, you should close the - connection with :meth:`close`. Indeed, there are only two situations - where :meth:`send` may yield control to the event loop and then get - canceled; in both cases, :meth:`close` has the same effect and is - more clear: - - 1. The write buffer is full. If you don't want to wait until enough - data is sent, your only alternative is to close the connection. - :meth:`close` will likely time out then abort the TCP connection. - 2. ``message`` is an asynchronous iterator that yields control. - Stopping in the middle of a fragmented message will cause a - protocol error and the connection will be closed. - - When the connection is closed, :meth:`send` raises - :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it - raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal - connection closure and - :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol - error or a network failure. - - Args: - message: Message to send. - - Raises: - ConnectionClosed: When the connection is closed. - TypeError: If ``message`` doesn't have a supported type. - - """ - await self.ensure_open() - - # While sending a fragmented message, prevent sending other messages - # until all fragments are sent. - while self._fragmented_message_waiter is not None: - await asyncio.shield(self._fragmented_message_waiter) - - # Unfragmented message -- this case must be handled first because - # strings and bytes-like objects are iterable. - - if isinstance(message, (str, bytes, bytearray, memoryview)): - opcode, data = prepare_data(message) - await self.write_frame(True, opcode, data) - - # Catch a common mistake -- passing a dict to send(). - - elif isinstance(message, Mapping): - raise TypeError("data is a dict-like object") - - # Fragmented message -- regular iterator. - - elif isinstance(message, Iterable): - # Work around https://github.com/python/mypy/issues/6227 - message = cast(Iterable[Data], message) - - iter_message = iter(message) - try: - fragment = next(iter_message) - except StopIteration: - return - opcode, data = prepare_data(fragment) - - self._fragmented_message_waiter = self.loop.create_future() - try: - # First fragment. - await self.write_frame(False, opcode, data) - - # Other fragments. - for fragment in iter_message: - confirm_opcode, data = prepare_data(fragment) - if confirm_opcode != opcode: - raise TypeError("data contains inconsistent types") - await self.write_frame(False, OP_CONT, data) - - # Final fragment. - await self.write_frame(True, OP_CONT, b"") - - except (Exception, asyncio.CancelledError): - # We're half-way through a fragmented message and we can't - # complete it. This makes the connection unusable. - self.fail_connection(CloseCode.INTERNAL_ERROR) - raise - - finally: - self._fragmented_message_waiter.set_result(None) - self._fragmented_message_waiter = None - - # Fragmented message -- asynchronous iterator - - elif isinstance(message, AsyncIterable): - # Implement aiter_message = aiter(message) without aiter - # Work around https://github.com/python/mypy/issues/5738 - aiter_message = cast( - Callable[[AsyncIterable[Data]], AsyncIterator[Data]], - type(message).__aiter__, - )(message) - try: - # Implement fragment = anext(aiter_message) without anext - # Work around https://github.com/python/mypy/issues/5738 - fragment = await cast( - Callable[[AsyncIterator[Data]], Awaitable[Data]], - type(aiter_message).__anext__, - )(aiter_message) - except StopAsyncIteration: - return - opcode, data = prepare_data(fragment) - - self._fragmented_message_waiter = self.loop.create_future() - try: - # First fragment. - await self.write_frame(False, opcode, data) - - # Other fragments. - async for fragment in aiter_message: - confirm_opcode, data = prepare_data(fragment) - if confirm_opcode != opcode: - raise TypeError("data contains inconsistent types") - await self.write_frame(False, OP_CONT, data) - - # Final fragment. - await self.write_frame(True, OP_CONT, b"") - - except (Exception, asyncio.CancelledError): - # We're half-way through a fragmented message and we can't - # complete it. This makes the connection unusable. - self.fail_connection(CloseCode.INTERNAL_ERROR) - raise - - finally: - self._fragmented_message_waiter.set_result(None) - self._fragmented_message_waiter = None - - else: - raise TypeError("data must be str, bytes-like, or iterable") - - async def close( - self, - code: int = CloseCode.NORMAL_CLOSURE, - reason: str = "", - ) -> None: - """ - Perform the closing handshake. - - :meth:`close` waits for the other end to complete the handshake and - for the TCP connection to terminate. As a consequence, there's no need - to await :meth:`wait_closed` after :meth:`close`. - - :meth:`close` is idempotent: it doesn't do anything once the - connection is closed. - - Wrapping :func:`close` in :func:`~asyncio.create_task` is safe, given - that errors during connection termination aren't particularly useful. - - Canceling :meth:`close` is discouraged. If it takes too long, you can - set a shorter ``close_timeout``. If you don't want to wait, let the - Python process exit, then the OS will take care of closing the TCP - connection. - - Args: - code: WebSocket close code. - reason: WebSocket close reason. - - """ - try: - async with asyncio_timeout(self.close_timeout): - await self.write_close_frame(Close(code, reason)) - except asyncio.TimeoutError: - # If the close frame cannot be sent because the send buffers - # are full, the closing handshake won't complete anyway. - # Fail the connection to shut down faster. - self.fail_connection() - - # If no close frame is received within the timeout, asyncio_timeout() - # cancels the data transfer task and raises TimeoutError. - - # If close() is called multiple times concurrently and one of these - # calls hits the timeout, the data transfer task will be canceled. - # Other calls will receive a CancelledError here. - - try: - # If close() is canceled during the wait, self.transfer_data_task - # is canceled before the timeout elapses. - async with asyncio_timeout(self.close_timeout): - await self.transfer_data_task - except (asyncio.TimeoutError, asyncio.CancelledError): - pass - - # Wait for the close connection task to close the TCP connection. - await asyncio.shield(self.close_connection_task) - - async def wait_closed(self) -> None: - """ - Wait until the connection is closed. - - This coroutine is identical to the :attr:`closed` attribute, except it - can be awaited. - - This can make it easier to detect connection termination, regardless - of its cause, in tasks that interact with the WebSocket connection. - - """ - await asyncio.shield(self.connection_lost_waiter) - - async def ping(self, data: Data | None = None) -> Awaitable[float]: - """ - Send a Ping_. - - .. _Ping: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2 - - A ping may serve as a keepalive, as a check that the remote endpoint - received all messages up to this point, or to measure :attr:`latency`. - - Canceling :meth:`ping` is discouraged. If :meth:`ping` doesn't return - immediately, it means the write buffer is full. If you don't want to - wait, you should close the connection. - - Canceling the :class:`~asyncio.Future` returned by :meth:`ping` has no - effect. - - Args: - data: Payload of the ping. A string will be encoded to UTF-8. - If ``data`` is :obj:`None`, the payload is four random bytes. - - Returns: - A future that will be completed when the corresponding pong is - received. You can ignore it if you don't intend to wait. The result - of the future is the latency of the connection in seconds. - - :: - - pong_waiter = await ws.ping() - # only if you want to wait for the corresponding pong - latency = await pong_waiter - - Raises: - ConnectionClosed: When the connection is closed. - RuntimeError: If another ping was sent with the same data and - the corresponding pong wasn't received yet. - - """ - await self.ensure_open() - - if data is not None: - data = prepare_ctrl(data) - - # Protect against duplicates if a payload is explicitly set. - if data in self.pings: - raise RuntimeError("already waiting for a pong with the same data") - - # Generate a unique random payload otherwise. - while data is None or data in self.pings: - data = struct.pack("!I", random.getrandbits(32)) - - pong_waiter = self.loop.create_future() - # Resolution of time.monotonic() may be too low on Windows. - ping_timestamp = time.perf_counter() - self.pings[data] = (pong_waiter, ping_timestamp) - - await self.write_frame(True, OP_PING, data) - - return asyncio.shield(pong_waiter) - - async def pong(self, data: Data = b"") -> None: - """ - Send a Pong_. - - .. _Pong: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.3 - - An unsolicited pong may serve as a unidirectional heartbeat. - - Canceling :meth:`pong` is discouraged. If :meth:`pong` doesn't return - immediately, it means the write buffer is full. If you don't want to - wait, you should close the connection. - - Args: - data: Payload of the pong. A string will be encoded to UTF-8. - - Raises: - ConnectionClosed: When the connection is closed. - - """ - await self.ensure_open() - - data = prepare_ctrl(data) - - await self.write_frame(True, OP_PONG, data) - - # Private methods - no guarantees. - - def connection_closed_exc(self) -> ConnectionClosed: - exc: ConnectionClosed - if ( - self.close_rcvd is not None - and self.close_rcvd.code in OK_CLOSE_CODES - and self.close_sent is not None - and self.close_sent.code in OK_CLOSE_CODES - ): - exc = ConnectionClosedOK( - self.close_rcvd, - self.close_sent, - self.close_rcvd_then_sent, - ) - else: - exc = ConnectionClosedError( - self.close_rcvd, - self.close_sent, - self.close_rcvd_then_sent, - ) - # Chain to the exception that terminated data transfer, if any. - exc.__cause__ = self.transfer_data_exc - return exc - - async def ensure_open(self) -> None: - """ - Check that the WebSocket connection is open. - - Raise :exc:`~websockets.exceptions.ConnectionClosed` if it isn't. - - """ - # Handle cases from most common to least common for performance. - if self.state is State.OPEN: - # If self.transfer_data_task exited without a closing handshake, - # self.close_connection_task may be closing the connection, going - # straight from OPEN to CLOSED. - if self.transfer_data_task.done(): - await asyncio.shield(self.close_connection_task) - raise self.connection_closed_exc() - else: - return - - if self.state is State.CLOSED: - raise self.connection_closed_exc() - - if self.state is State.CLOSING: - # If we started the closing handshake, wait for its completion to - # get the proper close code and reason. self.close_connection_task - # will complete within 4 or 5 * close_timeout after close(). The - # CLOSING state also occurs when failing the connection. In that - # case self.close_connection_task will complete even faster. - await asyncio.shield(self.close_connection_task) - raise self.connection_closed_exc() - - # Control may only reach this point in buggy third-party subclasses. - assert self.state is State.CONNECTING - raise InvalidState("WebSocket connection isn't established yet") - - async def transfer_data(self) -> None: - """ - Read incoming messages and put them in a queue. - - This coroutine runs in a task until the closing handshake is started. - - """ - try: - while True: - message = await self.read_message() - - # Exit the loop when receiving a close frame. - if message is None: - break - - # Wait until there's room in the queue (if necessary). - if self.max_queue is not None: - while len(self.messages) >= self.max_queue: - self._put_message_waiter = self.loop.create_future() - try: - await asyncio.shield(self._put_message_waiter) - finally: - self._put_message_waiter = None - - # Put the message in the queue. - self.messages.append(message) - - # Notify recv(). - if self._pop_message_waiter is not None: - self._pop_message_waiter.set_result(None) - self._pop_message_waiter = None - - except asyncio.CancelledError as exc: - self.transfer_data_exc = exc - # If fail_connection() cancels this task, avoid logging the error - # twice and failing the connection again. - raise - - except ProtocolError as exc: - self.transfer_data_exc = exc - self.fail_connection(CloseCode.PROTOCOL_ERROR) - - except (ConnectionError, TimeoutError, EOFError, ssl.SSLError) as exc: - # Reading data with self.reader.readexactly may raise: - # - most subclasses of ConnectionError if the TCP connection - # breaks, is reset, or is aborted; - # - TimeoutError if the TCP connection times out; - # - IncompleteReadError, a subclass of EOFError, if fewer - # bytes are available than requested; - # - ssl.SSLError if the other side infringes the TLS protocol. - self.transfer_data_exc = exc - self.fail_connection(CloseCode.ABNORMAL_CLOSURE) - - except UnicodeDecodeError as exc: - self.transfer_data_exc = exc - self.fail_connection(CloseCode.INVALID_DATA) - - except PayloadTooBig as exc: - self.transfer_data_exc = exc - self.fail_connection(CloseCode.MESSAGE_TOO_BIG) - - except Exception as exc: - # This shouldn't happen often because exceptions expected under - # regular circumstances are handled above. If it does, consider - # catching and handling more exceptions. - self.logger.error("data transfer failed", exc_info=True) - - self.transfer_data_exc = exc - self.fail_connection(CloseCode.INTERNAL_ERROR) - - async def read_message(self) -> Data | None: - """ - Read a single message from the connection. - - Re-assemble data frames if the message is fragmented. - - Return :obj:`None` when the closing handshake is started. - - """ - frame = await self.read_data_frame(max_size=self.max_size) - - # A close frame was received. - if frame is None: - return None - - if frame.opcode == OP_TEXT: - text = True - elif frame.opcode == OP_BINARY: - text = False - else: # frame.opcode == OP_CONT - raise ProtocolError("unexpected opcode") - - # Shortcut for the common case - no fragmentation - if frame.fin: - return frame.data.decode() if text else frame.data - - # 5.4. Fragmentation - fragments: list[Data] = [] - max_size = self.max_size - if text: - decoder_factory = codecs.getincrementaldecoder("utf-8") - decoder = decoder_factory(errors="strict") - if max_size is None: - - def append(frame: Frame) -> None: - nonlocal fragments - fragments.append(decoder.decode(frame.data, frame.fin)) - - else: - - def append(frame: Frame) -> None: - nonlocal fragments, max_size - fragments.append(decoder.decode(frame.data, frame.fin)) - assert isinstance(max_size, int) - max_size -= len(frame.data) - - else: - if max_size is None: - - def append(frame: Frame) -> None: - nonlocal fragments - fragments.append(frame.data) - - else: - - def append(frame: Frame) -> None: - nonlocal fragments, max_size - fragments.append(frame.data) - assert isinstance(max_size, int) - max_size -= len(frame.data) - - append(frame) - - while not frame.fin: - frame = await self.read_data_frame(max_size=max_size) - if frame is None: - raise ProtocolError("incomplete fragmented message") - if frame.opcode != OP_CONT: - raise ProtocolError("unexpected opcode") - append(frame) - - return ("" if text else b"").join(fragments) - - async def read_data_frame(self, max_size: int | None) -> Frame | None: - """ - Read a single data frame from the connection. - - Process control frames received before the next data frame. - - Return :obj:`None` if a close frame is encountered before any data frame. - - """ - # 6.2. Receiving Data - while True: - frame = await self.read_frame(max_size) - - # 5.5. Control Frames - if frame.opcode == OP_CLOSE: - # 7.1.5. The WebSocket Connection Close Code - # 7.1.6. The WebSocket Connection Close Reason - self.close_rcvd = Close.parse(frame.data) - if self.close_sent is not None: - self.close_rcvd_then_sent = False - try: - # Echo the original data instead of re-serializing it with - # Close.serialize() because that fails when the close frame - # is empty and Close.parse() synthesizes a 1005 close code. - await self.write_close_frame(self.close_rcvd, frame.data) - except ConnectionClosed: - # Connection closed before we could echo the close frame. - pass - return None - - elif frame.opcode == OP_PING: - # Answer pings, unless connection is CLOSING. - if self.state is State.OPEN: - try: - await self.pong(frame.data) - except ConnectionClosed: - # Connection closed while draining write buffer. - pass - - elif frame.opcode == OP_PONG: - if frame.data in self.pings: - pong_timestamp = time.perf_counter() - # Sending a pong for only the most recent ping is legal. - # Acknowledge all previous pings too in that case. - ping_id = None - ping_ids = [] - for ping_id, (pong_waiter, ping_timestamp) in self.pings.items(): - ping_ids.append(ping_id) - if not pong_waiter.done(): - pong_waiter.set_result(pong_timestamp - ping_timestamp) - if ping_id == frame.data: - self.latency = pong_timestamp - ping_timestamp - break - else: - raise AssertionError("solicited pong not found in pings") - # Remove acknowledged pings from self.pings. - for ping_id in ping_ids: - del self.pings[ping_id] - - # 5.6. Data Frames - else: - return frame - - async def read_frame(self, max_size: int | None) -> Frame: - """ - Read a single frame from the connection. - - """ - frame = await Frame.read( - self.reader.readexactly, - mask=not self.is_client, - max_size=max_size, - extensions=self.extensions, - ) - if self.debug: - self.logger.debug("< %s", frame) - return frame - - def write_frame_sync(self, fin: bool, opcode: int, data: bytes) -> None: - frame = Frame(fin, Opcode(opcode), data) - if self.debug: - self.logger.debug("> %s", frame) - frame.write( - self.transport.write, - mask=self.is_client, - extensions=self.extensions, - ) - - async def drain(self) -> None: - try: - # drain() cannot be called concurrently by multiple coroutines. - # See https://github.com/python/cpython/issues/74116 for details. - # This workaround can be removed when dropping Python < 3.10. - async with self._drain_lock: - # Handle flow control automatically. - await self._drain() - except ConnectionError: - # Terminate the connection if the socket died. - self.fail_connection() - # Wait until the connection is closed to raise ConnectionClosed - # with the correct code and reason. - await self.ensure_open() - - async def write_frame( - self, fin: bool, opcode: int, data: bytes, *, _state: int = State.OPEN - ) -> None: - # Defensive assertion for protocol compliance. - if self.state is not _state: # pragma: no cover - raise InvalidState( - f"Cannot write to a WebSocket in the {self.state.name} state" - ) - self.write_frame_sync(fin, opcode, data) - await self.drain() - - async def write_close_frame(self, close: Close, data: bytes | None = None) -> None: - """ - Write a close frame if and only if the connection state is OPEN. - - This dedicated coroutine must be used for writing close frames to - ensure that at most one close frame is sent on a given connection. - - """ - # Test and set the connection state before sending the close frame to - # avoid sending two frames in case of concurrent calls. - if self.state is State.OPEN: - # 7.1.3. The WebSocket Closing Handshake is Started - self.state = State.CLOSING - if self.debug: - self.logger.debug("= connection is CLOSING") - - self.close_sent = close - if self.close_rcvd is not None: - self.close_rcvd_then_sent = True - if data is None: - data = close.serialize() - - # 7.1.2. Start the WebSocket Closing Handshake - await self.write_frame(True, OP_CLOSE, data, _state=State.CLOSING) - - async def keepalive_ping(self) -> None: - """ - Send a Ping frame and wait for a Pong frame at regular intervals. - - This coroutine exits when the connection terminates and one of the - following happens: - - - :meth:`ping` raises :exc:`ConnectionClosed`, or - - :meth:`close_connection` cancels :attr:`keepalive_ping_task`. - - """ - if self.ping_interval is None: - return - - try: - while True: - await asyncio.sleep(self.ping_interval) - - self.logger.debug("% sending keepalive ping") - pong_waiter = await self.ping() - - if self.ping_timeout is not None: - try: - async with asyncio_timeout(self.ping_timeout): - # Raises CancelledError if the connection is closed, - # when close_connection() cancels keepalive_ping(). - # Raises ConnectionClosed if the connection is lost, - # when connection_lost() calls abort_pings(). - await pong_waiter - self.logger.debug("% received keepalive pong") - except asyncio.TimeoutError: - if self.debug: - self.logger.debug("- timed out waiting for keepalive pong") - self.fail_connection( - CloseCode.INTERNAL_ERROR, - "keepalive ping timeout", - ) - break - - except ConnectionClosed: - pass - - except Exception: - self.logger.error("keepalive ping failed", exc_info=True) - - async def close_connection(self) -> None: - """ - 7.1.1. Close the WebSocket Connection - - When the opening handshake succeeds, :meth:`connection_open` starts - this coroutine in a task. It waits for the data transfer phase to - complete then it closes the TCP connection cleanly. - - When the opening handshake fails, :meth:`fail_connection` does the - same. There's no data transfer phase in that case. - - """ - try: - # Wait for the data transfer phase to complete. - if hasattr(self, "transfer_data_task"): - try: - await self.transfer_data_task - except asyncio.CancelledError: - pass - - # Cancel the keepalive ping task. - if hasattr(self, "keepalive_ping_task"): - self.keepalive_ping_task.cancel() - - # A client should wait for a TCP close from the server. - if self.is_client and hasattr(self, "transfer_data_task"): - if await self.wait_for_connection_lost(): - return - if self.debug: - self.logger.debug("- timed out waiting for TCP close") - - # Half-close the TCP connection if possible (when there's no TLS). - if self.transport.can_write_eof(): - if self.debug: - self.logger.debug("x half-closing TCP connection") - # write_eof() doesn't document which exceptions it raises. - # "[Errno 107] Transport endpoint is not connected" happens - # but it isn't completely clear under which circumstances. - # uvloop can raise RuntimeError here. - try: - self.transport.write_eof() - except (OSError, RuntimeError): # pragma: no cover - pass - - if await self.wait_for_connection_lost(): - return - if self.debug: - self.logger.debug("- timed out waiting for TCP close") - - finally: - # The try/finally ensures that the transport never remains open, - # even if this coroutine is canceled (for example). - await self.close_transport() - - async def close_transport(self) -> None: - """ - Close the TCP connection. - - """ - # If connection_lost() was called, the TCP connection is closed. - # However, if TLS is enabled, the transport still needs closing. - # Else asyncio complains: ResourceWarning: unclosed transport. - if self.connection_lost_waiter.done() and self.transport.is_closing(): - return - - # Close the TCP connection. Buffers are flushed asynchronously. - if self.debug: - self.logger.debug("x closing TCP connection") - self.transport.close() - - if await self.wait_for_connection_lost(): - return - if self.debug: - self.logger.debug("- timed out waiting for TCP close") - - # Abort the TCP connection. Buffers are discarded. - if self.debug: - self.logger.debug("x aborting TCP connection") - self.transport.abort() - - # connection_lost() is called quickly after aborting. - await self.wait_for_connection_lost() - - async def wait_for_connection_lost(self) -> bool: - """ - Wait until the TCP connection is closed or ``self.close_timeout`` elapses. - - Return :obj:`True` if the connection is closed and :obj:`False` - otherwise. - - """ - if not self.connection_lost_waiter.done(): - try: - async with asyncio_timeout(self.close_timeout): - await asyncio.shield(self.connection_lost_waiter) - except asyncio.TimeoutError: - pass - # Re-check self.connection_lost_waiter.done() synchronously because - # connection_lost() could run between the moment the timeout occurs - # and the moment this coroutine resumes running. - return self.connection_lost_waiter.done() - - def fail_connection( - self, - code: int = CloseCode.ABNORMAL_CLOSURE, - reason: str = "", - ) -> None: - """ - 7.1.7. Fail the WebSocket Connection - - This requires: - - 1. Stopping all processing of incoming data, which means cancelling - :attr:`transfer_data_task`. The close code will be 1006 unless a - close frame was received earlier. - - 2. Sending a close frame with an appropriate code if the opening - handshake succeeded and the other side is likely to process it. - - 3. Closing the connection. :meth:`close_connection` takes care of - this once :attr:`transfer_data_task` exits after being canceled. - - (The specification describes these steps in the opposite order.) - - """ - if self.debug: - self.logger.debug("! failing connection with code %d", code) - - # Cancel transfer_data_task if the opening handshake succeeded. - # cancel() is idempotent and ignored if the task is done already. - if hasattr(self, "transfer_data_task"): - self.transfer_data_task.cancel() - - # Send a close frame when the state is OPEN (a close frame was already - # sent if it's CLOSING), except when failing the connection because of - # an error reading from or writing to the network. - # Don't send a close frame if the connection is broken. - if code != CloseCode.ABNORMAL_CLOSURE and self.state is State.OPEN: - close = Close(code, reason) - - # Write the close frame without draining the write buffer. - - # Keeping fail_connection() synchronous guarantees it can't - # get stuck and simplifies the implementation of the callers. - # Not drainig the write buffer is acceptable in this context. - - # This duplicates a few lines of code from write_close_frame(). - - self.state = State.CLOSING - if self.debug: - self.logger.debug("= connection is CLOSING") - - # If self.close_rcvd was set, the connection state would be - # CLOSING. Therefore self.close_rcvd isn't set and we don't - # have to set self.close_rcvd_then_sent. - assert self.close_rcvd is None - self.close_sent = close - - self.write_frame_sync(True, OP_CLOSE, close.serialize()) - - # Start close_connection_task if the opening handshake didn't succeed. - if not hasattr(self, "close_connection_task"): - self.close_connection_task = self.loop.create_task(self.close_connection()) - - def abort_pings(self) -> None: - """ - Raise ConnectionClosed in pending keepalive pings. - - They'll never receive a pong once the connection is closed. - - """ - assert self.state is State.CLOSED - exc = self.connection_closed_exc() - - for pong_waiter, _ping_timestamp in self.pings.values(): - pong_waiter.set_exception(exc) - # If the exception is never retrieved, it will be logged when ping - # is garbage-collected. This is confusing for users. - # Given that ping is done (with an exception), canceling it does - # nothing, but it prevents logging the exception. - pong_waiter.cancel() - - # asyncio.Protocol methods - - def connection_made(self, transport: asyncio.BaseTransport) -> None: - """ - Configure write buffer limits. - - The high-water limit is defined by ``self.write_limit``. - - The low-water limit currently defaults to ``self.write_limit // 4`` in - :meth:`~asyncio.WriteTransport.set_write_buffer_limits`, which should - be all right for reasonable use cases of this library. - - This is the earliest point where we can get hold of the transport, - which means it's the best point for configuring it. - - """ - transport = cast(asyncio.Transport, transport) - transport.set_write_buffer_limits(self.write_limit) - self.transport = transport - - # Copied from asyncio.StreamReaderProtocol - self.reader.set_transport(transport) - - def connection_lost(self, exc: Exception | None) -> None: - """ - 7.1.4. The WebSocket Connection is Closed. - - """ - self.state = State.CLOSED - self.logger.debug("= connection is CLOSED") - - self.abort_pings() - - # If self.connection_lost_waiter isn't pending, that's a bug, because: - # - it's set only here in connection_lost() which is called only once; - # - it must never be canceled. - self.connection_lost_waiter.set_result(None) - - if True: # pragma: no cover - # Copied from asyncio.StreamReaderProtocol - if self.reader is not None: - if exc is None: - self.reader.feed_eof() - else: - self.reader.set_exception(exc) - - # Copied from asyncio.FlowControlMixin - # Wake up the writer if currently paused. - if not self._paused: - return - waiter = self._drain_waiter - if waiter is None: - return - self._drain_waiter = None - if waiter.done(): - return - if exc is None: - waiter.set_result(None) - else: - waiter.set_exception(exc) - - def pause_writing(self) -> None: # pragma: no cover - assert not self._paused - self._paused = True - - def resume_writing(self) -> None: # pragma: no cover - assert self._paused - self._paused = False - - waiter = self._drain_waiter - if waiter is not None: - self._drain_waiter = None - if not waiter.done(): - waiter.set_result(None) - - def data_received(self, data: bytes) -> None: - self.reader.feed_data(data) - - def eof_received(self) -> None: - """ - Close the transport after receiving EOF. - - The WebSocket protocol has its own closing handshake: endpoints close - the TCP or TLS connection after sending and receiving a close frame. - - As a consequence, they never need to write after receiving EOF, so - there's no reason to keep the transport open by returning :obj:`True`. - - Besides, that doesn't work on TLS connections. - - """ - self.reader.feed_eof() - - -# broadcast() is defined in the protocol module even though it's primarily -# used by servers and documented in the server module because it works with -# client connections too and because it's easier to test together with the -# WebSocketCommonProtocol class. - - -def broadcast( - websockets: Iterable[WebSocketCommonProtocol], - message: Data, - raise_exceptions: bool = False, -) -> None: - """ - Broadcast a message to several WebSocket connections. - - A string (:class:`str`) is sent as a Text_ frame. A bytestring or bytes-like - object (:class:`bytes`, :class:`bytearray`, or :class:`memoryview`) is sent - as a Binary_ frame. - - .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - .. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - :func:`broadcast` pushes the message synchronously to all connections even - if their write buffers are overflowing. There's no backpressure. - - If you broadcast messages faster than a connection can handle them, messages - will pile up in its write buffer until the connection times out. Keep - ``ping_interval`` and ``ping_timeout`` low to prevent excessive memory usage - from slow connections. - - Unlike :meth:`~websockets.legacy.protocol.WebSocketCommonProtocol.send`, - :func:`broadcast` doesn't support sending fragmented messages. Indeed, - fragmentation is useful for sending large messages without buffering them in - memory, while :func:`broadcast` buffers one copy per connection as fast as - possible. - - :func:`broadcast` skips connections that aren't open in order to avoid - errors on connections where the closing handshake is in progress. - - :func:`broadcast` ignores failures to write the message on some connections. - It continues writing to other connections. On Python 3.11 and above, you may - set ``raise_exceptions`` to :obj:`True` to record failures and raise all - exceptions in a :pep:`654` :exc:`ExceptionGroup`. - - While :func:`broadcast` makes more sense for servers, it works identically - with clients, if you have a use case for opening connections to many servers - and broadcasting a message to them. - - Args: - websockets: WebSocket connections to which the message will be sent. - message: Message to send. - raise_exceptions: Whether to raise an exception in case of failures. - - Raises: - TypeError: If ``message`` doesn't have a supported type. - - """ - if not isinstance(message, (str, bytes, bytearray, memoryview)): - raise TypeError("data must be str or bytes-like") - - if raise_exceptions: - if sys.version_info[:2] < (3, 11): # pragma: no cover - raise ValueError("raise_exceptions requires at least Python 3.11") - exceptions = [] - - opcode, data = prepare_data(message) - - for websocket in websockets: - if websocket.state is not State.OPEN: - continue - - if websocket._fragmented_message_waiter is not None: - if raise_exceptions: - exception = RuntimeError("sending a fragmented message") - exceptions.append(exception) - else: - websocket.logger.warning( - "skipped broadcast: sending a fragmented message", - ) - continue - - try: - websocket.write_frame_sync(True, opcode, data) - except Exception as write_exception: - if raise_exceptions: - exception = RuntimeError("failed to write message") - exception.__cause__ = write_exception - exceptions.append(exception) - else: - websocket.logger.warning( - "skipped broadcast: failed to write message: %s", - traceback.format_exception_only( - # Remove first argument when dropping Python 3.9. - type(write_exception), - write_exception, - )[0].strip(), - ) - - if raise_exceptions and exceptions: - raise ExceptionGroup("skipped broadcast", exceptions) - - -# Pretend that broadcast is actually defined in the server module. -broadcast.__module__ = "websockets.legacy.server" diff --git a/wip/SNIP/client/websockets/legacy/server.py b/wip/SNIP/client/websockets/legacy/server.py deleted file mode 100644 index f9d57cb..0000000 --- a/wip/SNIP/client/websockets/legacy/server.py +++ /dev/null @@ -1,1191 +0,0 @@ -from __future__ import annotations - -import asyncio -import email.utils -import functools -import http -import inspect -import logging -import socket -import warnings -from collections.abc import Awaitable, Generator, Iterable, Sequence -from types import TracebackType -from typing import Any, Callable, Union, cast - -from ..asyncio.compatibility import asyncio_timeout -from ..datastructures import Headers, HeadersLike, MultipleValuesError -from ..exceptions import ( - InvalidHandshake, - InvalidHeader, - InvalidMessage, - InvalidOrigin, - InvalidUpgrade, - NegotiationError, -) -from ..extensions import Extension, ServerExtensionFactory -from ..extensions.permessage_deflate import enable_server_permessage_deflate -from ..headers import ( - build_extension, - parse_extension, - parse_subprotocol, - validate_subprotocols, -) -from ..http11 import SERVER -from ..protocol import State -from ..typing import ExtensionHeader, LoggerLike, Origin, StatusLike, Subprotocol -from .exceptions import AbortHandshake -from .handshake import build_response, check_request -from .http import read_request -from .protocol import WebSocketCommonProtocol, broadcast - - -__all__ = [ - "broadcast", - "serve", - "unix_serve", - "WebSocketServerProtocol", - "WebSocketServer", -] - - -# Change to HeadersLike | ... when dropping Python < 3.10. -HeadersLikeOrCallable = Union[HeadersLike, Callable[[str, Headers], HeadersLike]] - -HTTPResponse = tuple[StatusLike, HeadersLike, bytes] - - -class WebSocketServerProtocol(WebSocketCommonProtocol): - """ - WebSocket server connection. - - :class:`WebSocketServerProtocol` provides :meth:`recv` and :meth:`send` - coroutines for receiving and sending messages. - - It supports asynchronous iteration to receive messages:: - - async for message in websocket: - await process(message) - - The iterator exits normally when the connection is closed with close code - 1000 (OK) or 1001 (going away) or without a close code. It raises - a :exc:`~websockets.exceptions.ConnectionClosedError` when the connection - is closed with any other code. - - You may customize the opening handshake in a subclass by - overriding :meth:`process_request` or :meth:`select_subprotocol`. - - Args: - ws_server: WebSocket server that created this connection. - - See :func:`serve` for the documentation of ``ws_handler``, ``logger``, ``origins``, - ``extensions``, ``subprotocols``, ``extra_headers``, and ``server_header``. - - See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the - documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, - ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit``. - - """ - - is_client = False - side = "server" - - def __init__( - self, - # The version that accepts the path in the second argument is deprecated. - ws_handler: ( - Callable[[WebSocketServerProtocol], Awaitable[Any]] - | Callable[[WebSocketServerProtocol, str], Awaitable[Any]] - ), - ws_server: WebSocketServer, - *, - logger: LoggerLike | None = None, - origins: Sequence[Origin | None] | None = None, - extensions: Sequence[ServerExtensionFactory] | None = None, - subprotocols: Sequence[Subprotocol] | None = None, - extra_headers: HeadersLikeOrCallable | None = None, - server_header: str | None = SERVER, - process_request: ( - Callable[[str, Headers], Awaitable[HTTPResponse | None]] | None - ) = None, - select_subprotocol: ( - Callable[[Sequence[Subprotocol], Sequence[Subprotocol]], Subprotocol] | None - ) = None, - open_timeout: float | None = 10, - **kwargs: Any, - ) -> None: - if logger is None: - logger = logging.getLogger("websockets.server") - super().__init__(logger=logger, **kwargs) - # For backwards compatibility with 6.0 or earlier. - if origins is not None and "" in origins: - warnings.warn("use None instead of '' in origins", DeprecationWarning) - origins = [None if origin == "" else origin for origin in origins] - # For backwards compatibility with 10.0 or earlier. Done here in - # addition to serve to trigger the deprecation warning on direct - # use of WebSocketServerProtocol. - self.ws_handler = remove_path_argument(ws_handler) - self.ws_server = ws_server - self.origins = origins - self.available_extensions = extensions - self.available_subprotocols = subprotocols - self.extra_headers = extra_headers - self.server_header = server_header - self._process_request = process_request - self._select_subprotocol = select_subprotocol - self.open_timeout = open_timeout - - def connection_made(self, transport: asyncio.BaseTransport) -> None: - """ - Register connection and initialize a task to handle it. - - """ - super().connection_made(transport) - # Register the connection with the server before creating the handler - # task. Registering at the beginning of the handler coroutine would - # create a race condition between the creation of the task, which - # schedules its execution, and the moment the handler starts running. - self.ws_server.register(self) - self.handler_task = self.loop.create_task(self.handler()) - - async def handler(self) -> None: - """ - Handle the lifecycle of a WebSocket connection. - - Since this method doesn't have a caller able to handle exceptions, it - attempts to log relevant ones and guarantees that the TCP connection is - closed before exiting. - - """ - try: - try: - async with asyncio_timeout(self.open_timeout): - await self.handshake( - origins=self.origins, - available_extensions=self.available_extensions, - available_subprotocols=self.available_subprotocols, - extra_headers=self.extra_headers, - ) - except asyncio.TimeoutError: # pragma: no cover - raise - except ConnectionError: - raise - except Exception as exc: - if isinstance(exc, AbortHandshake): - status, headers, body = exc.status, exc.headers, exc.body - elif isinstance(exc, InvalidOrigin): - if self.debug: - self.logger.debug("! invalid origin", exc_info=True) - status, headers, body = ( - http.HTTPStatus.FORBIDDEN, - Headers(), - f"Failed to open a WebSocket connection: {exc}.\n".encode(), - ) - elif isinstance(exc, InvalidUpgrade): - if self.debug: - self.logger.debug("! invalid upgrade", exc_info=True) - status, headers, body = ( - http.HTTPStatus.UPGRADE_REQUIRED, - Headers([("Upgrade", "websocket")]), - ( - f"Failed to open a WebSocket connection: {exc}.\n" - f"\n" - f"You cannot access a WebSocket server directly " - f"with a browser. You need a WebSocket client.\n" - ).encode(), - ) - elif isinstance(exc, InvalidHandshake): - if self.debug: - self.logger.debug("! invalid handshake", exc_info=True) - exc_chain = cast(BaseException, exc) - exc_str = f"{exc_chain}" - while exc_chain.__cause__ is not None: - exc_chain = exc_chain.__cause__ - exc_str += f"; {exc_chain}" - status, headers, body = ( - http.HTTPStatus.BAD_REQUEST, - Headers(), - f"Failed to open a WebSocket connection: {exc_str}.\n".encode(), - ) - else: - self.logger.error("opening handshake failed", exc_info=True) - status, headers, body = ( - http.HTTPStatus.INTERNAL_SERVER_ERROR, - Headers(), - ( - b"Failed to open a WebSocket connection.\n" - b"See server log for more information.\n" - ), - ) - - headers.setdefault("Date", email.utils.formatdate(usegmt=True)) - if self.server_header: - headers.setdefault("Server", self.server_header) - - headers.setdefault("Content-Length", str(len(body))) - headers.setdefault("Content-Type", "text/plain") - headers.setdefault("Connection", "close") - - self.write_http_response(status, headers, body) - self.logger.info( - "connection rejected (%d %s)", status.value, status.phrase - ) - await self.close_transport() - return - - try: - await self.ws_handler(self) - except Exception: - self.logger.error("connection handler failed", exc_info=True) - if not self.closed: - self.fail_connection(1011) - raise - - try: - await self.close() - except ConnectionError: - raise - except Exception: - self.logger.error("closing handshake failed", exc_info=True) - raise - - except Exception: - # Last-ditch attempt to avoid leaking connections on errors. - try: - self.transport.close() - except Exception: # pragma: no cover - pass - - finally: - # Unregister the connection with the server when the handler task - # terminates. Registration is tied to the lifecycle of the handler - # task because the server waits for tasks attached to registered - # connections before terminating. - self.ws_server.unregister(self) - self.logger.info("connection closed") - - async def read_http_request(self) -> tuple[str, Headers]: - """ - Read request line and headers from the HTTP request. - - If the request contains a body, it may be read from ``self.reader`` - after this coroutine returns. - - Raises: - InvalidMessage: If the HTTP message is malformed or isn't an - HTTP/1.1 GET request. - - """ - try: - path, headers = await read_request(self.reader) - except asyncio.CancelledError: # pragma: no cover - raise - except Exception as exc: - raise InvalidMessage("did not receive a valid HTTP request") from exc - - if self.debug: - self.logger.debug("< GET %s HTTP/1.1", path) - for key, value in headers.raw_items(): - self.logger.debug("< %s: %s", key, value) - - self.path = path - self.request_headers = headers - - return path, headers - - def write_http_response( - self, status: http.HTTPStatus, headers: Headers, body: bytes | None = None - ) -> None: - """ - Write status line and headers to the HTTP response. - - This coroutine is also able to write a response body. - - """ - self.response_headers = headers - - if self.debug: - self.logger.debug("> HTTP/1.1 %d %s", status.value, status.phrase) - for key, value in headers.raw_items(): - self.logger.debug("> %s: %s", key, value) - if body is not None: - self.logger.debug("> [body] (%d bytes)", len(body)) - - # Since the status line and headers only contain ASCII characters, - # we can keep this simple. - response = f"HTTP/1.1 {status.value} {status.phrase}\r\n" - response += str(headers) - - self.transport.write(response.encode()) - - if body is not None: - self.transport.write(body) - - async def process_request( - self, path: str, request_headers: Headers - ) -> HTTPResponse | None: - """ - Intercept the HTTP request and return an HTTP response if appropriate. - - You may override this method in a :class:`WebSocketServerProtocol` - subclass, for example: - - * to return an HTTP 200 OK response on a given path; then a load - balancer can use this path for a health check; - * to authenticate the request and return an HTTP 401 Unauthorized or an - HTTP 403 Forbidden when authentication fails. - - You may also override this method with the ``process_request`` - argument of :func:`serve` and :class:`WebSocketServerProtocol`. This - is equivalent, except ``process_request`` won't have access to the - protocol instance, so it can't store information for later use. - - :meth:`process_request` is expected to complete quickly. If it may run - for a long time, then it should await :meth:`wait_closed` and exit if - :meth:`wait_closed` completes, or else it could prevent the server - from shutting down. - - Args: - path: Request path, including optional query string. - request_headers: Request headers. - - Returns: - tuple[StatusLike, HeadersLike, bytes] | None: :obj:`None` to - continue the WebSocket handshake normally. - - An HTTP response, represented by a 3-uple of the response status, - headers, and body, to abort the WebSocket handshake and return - that HTTP response instead. - - """ - if self._process_request is not None: - response = self._process_request(path, request_headers) - if isinstance(response, Awaitable): - return await response - else: - # For backwards compatibility with 7.0. - warnings.warn( - "declare process_request as a coroutine", DeprecationWarning - ) - return response - return None - - @staticmethod - def process_origin( - headers: Headers, origins: Sequence[Origin | None] | None = None - ) -> Origin | None: - """ - Handle the Origin HTTP request header. - - Args: - headers: Request headers. - origins: Optional list of acceptable origins. - - Raises: - InvalidOrigin: If the origin isn't acceptable. - - """ - # "The user agent MUST NOT include more than one Origin header field" - # per https://datatracker.ietf.org/doc/html/rfc6454#section-7.3. - try: - origin = headers.get("Origin") - except MultipleValuesError as exc: - raise InvalidHeader("Origin", "multiple values") from exc - if origin is not None: - origin = cast(Origin, origin) - if origins is not None: - if origin not in origins: - raise InvalidOrigin(origin) - return origin - - @staticmethod - def process_extensions( - headers: Headers, - available_extensions: Sequence[ServerExtensionFactory] | None, - ) -> tuple[str | None, list[Extension]]: - """ - Handle the Sec-WebSocket-Extensions HTTP request header. - - Accept or reject each extension proposed in the client request. - Negotiate parameters for accepted extensions. - - Return the Sec-WebSocket-Extensions HTTP response header and the list - of accepted extensions. - - :rfc:`6455` leaves the rules up to the specification of each - :extension. - - To provide this level of flexibility, for each extension proposed by - the client, we check for a match with each extension available in the - server configuration. If no match is found, the extension is ignored. - - If several variants of the same extension are proposed by the client, - it may be accepted several times, which won't make sense in general. - Extensions must implement their own requirements. For this purpose, - the list of previously accepted extensions is provided. - - This process doesn't allow the server to reorder extensions. It can - only select a subset of the extensions proposed by the client. - - Other requirements, for example related to mandatory extensions or the - order of extensions, may be implemented by overriding this method. - - Args: - headers: Request headers. - extensions: Optional list of supported extensions. - - Raises: - InvalidHandshake: To abort the handshake with an HTTP 400 error. - - """ - response_header_value: str | None = None - - extension_headers: list[ExtensionHeader] = [] - accepted_extensions: list[Extension] = [] - - header_values = headers.get_all("Sec-WebSocket-Extensions") - - if header_values and available_extensions: - parsed_header_values: list[ExtensionHeader] = sum( - [parse_extension(header_value) for header_value in header_values], [] - ) - - for name, request_params in parsed_header_values: - for ext_factory in available_extensions: - # Skip non-matching extensions based on their name. - if ext_factory.name != name: - continue - - # Skip non-matching extensions based on their params. - try: - response_params, extension = ext_factory.process_request_params( - request_params, accepted_extensions - ) - except NegotiationError: - continue - - # Add matching extension to the final list. - extension_headers.append((name, response_params)) - accepted_extensions.append(extension) - - # Break out of the loop once we have a match. - break - - # If we didn't break from the loop, no extension in our list - # matched what the client sent. The extension is declined. - - # Serialize extension header. - if extension_headers: - response_header_value = build_extension(extension_headers) - - return response_header_value, accepted_extensions - - # Not @staticmethod because it calls self.select_subprotocol() - def process_subprotocol( - self, headers: Headers, available_subprotocols: Sequence[Subprotocol] | None - ) -> Subprotocol | None: - """ - Handle the Sec-WebSocket-Protocol HTTP request header. - - Return Sec-WebSocket-Protocol HTTP response header, which is the same - as the selected subprotocol. - - Args: - headers: Request headers. - available_subprotocols: Optional list of supported subprotocols. - - Raises: - InvalidHandshake: To abort the handshake with an HTTP 400 error. - - """ - subprotocol: Subprotocol | None = None - - header_values = headers.get_all("Sec-WebSocket-Protocol") - - if header_values and available_subprotocols: - parsed_header_values: list[Subprotocol] = sum( - [parse_subprotocol(header_value) for header_value in header_values], [] - ) - - subprotocol = self.select_subprotocol( - parsed_header_values, available_subprotocols - ) - - return subprotocol - - def select_subprotocol( - self, - client_subprotocols: Sequence[Subprotocol], - server_subprotocols: Sequence[Subprotocol], - ) -> Subprotocol | None: - """ - Pick a subprotocol among those supported by the client and the server. - - If several subprotocols are available, select the preferred subprotocol - by giving equal weight to the preferences of the client and the server. - - If no subprotocol is available, proceed without a subprotocol. - - You may provide a ``select_subprotocol`` argument to :func:`serve` or - :class:`WebSocketServerProtocol` to override this logic. For example, - you could reject the handshake if the client doesn't support a - particular subprotocol, rather than accept the handshake without that - subprotocol. - - Args: - client_subprotocols: List of subprotocols offered by the client. - server_subprotocols: List of subprotocols available on the server. - - Returns: - Selected subprotocol, if a common subprotocol was found. - - :obj:`None` to continue without a subprotocol. - - """ - if self._select_subprotocol is not None: - return self._select_subprotocol(client_subprotocols, server_subprotocols) - - subprotocols = set(client_subprotocols) & set(server_subprotocols) - if not subprotocols: - return None - return sorted( - subprotocols, - key=lambda p: client_subprotocols.index(p) + server_subprotocols.index(p), - )[0] - - async def handshake( - self, - origins: Sequence[Origin | None] | None = None, - available_extensions: Sequence[ServerExtensionFactory] | None = None, - available_subprotocols: Sequence[Subprotocol] | None = None, - extra_headers: HeadersLikeOrCallable | None = None, - ) -> str: - """ - Perform the server side of the opening handshake. - - Args: - origins: List of acceptable values of the Origin HTTP header; - include :obj:`None` if the lack of an origin is acceptable. - extensions: List of supported extensions, in order in which they - should be tried. - subprotocols: List of supported subprotocols, in order of - decreasing preference. - extra_headers: Arbitrary HTTP headers to add to the response when - the handshake succeeds. - - Returns: - path of the URI of the request. - - Raises: - InvalidHandshake: If the handshake fails. - - """ - path, request_headers = await self.read_http_request() - - # Hook for customizing request handling, for example checking - # authentication or treating some paths as plain HTTP endpoints. - early_response_awaitable = self.process_request(path, request_headers) - if isinstance(early_response_awaitable, Awaitable): - early_response = await early_response_awaitable - else: - # For backwards compatibility with 7.0. - warnings.warn("declare process_request as a coroutine", DeprecationWarning) - early_response = early_response_awaitable - - # The connection may drop while process_request is running. - if self.state is State.CLOSED: - # This subclass of ConnectionError is silently ignored in handler(). - raise BrokenPipeError("connection closed during opening handshake") - - # Change the response to a 503 error if the server is shutting down. - if not self.ws_server.is_serving(): - early_response = ( - http.HTTPStatus.SERVICE_UNAVAILABLE, - [], - b"Server is shutting down.\n", - ) - - if early_response is not None: - raise AbortHandshake(*early_response) - - key = check_request(request_headers) - - self.origin = self.process_origin(request_headers, origins) - - extensions_header, self.extensions = self.process_extensions( - request_headers, available_extensions - ) - - protocol_header = self.subprotocol = self.process_subprotocol( - request_headers, available_subprotocols - ) - - response_headers = Headers() - - build_response(response_headers, key) - - if extensions_header is not None: - response_headers["Sec-WebSocket-Extensions"] = extensions_header - - if protocol_header is not None: - response_headers["Sec-WebSocket-Protocol"] = protocol_header - - if callable(extra_headers): - extra_headers = extra_headers(path, self.request_headers) - if extra_headers is not None: - response_headers.update(extra_headers) - - response_headers.setdefault("Date", email.utils.formatdate(usegmt=True)) - if self.server_header is not None: - response_headers.setdefault("Server", self.server_header) - - self.write_http_response(http.HTTPStatus.SWITCHING_PROTOCOLS, response_headers) - - self.logger.info("connection open") - - self.connection_open() - - return path - - -class WebSocketServer: - """ - WebSocket server returned by :func:`serve`. - - This class mirrors the API of :class:`~asyncio.Server`. - - It keeps track of WebSocket connections in order to close them properly - when shutting down. - - Args: - logger: Logger for this server. - It defaults to ``logging.getLogger("websockets.server")``. - See the :doc:`logging guide <../../topics/logging>` for details. - - """ - - def __init__(self, logger: LoggerLike | None = None) -> None: - if logger is None: - logger = logging.getLogger("websockets.server") - self.logger = logger - - # Keep track of active connections. - self.websockets: set[WebSocketServerProtocol] = set() - - # Task responsible for closing the server and terminating connections. - self.close_task: asyncio.Task[None] | None = None - - # Completed when the server is closed and connections are terminated. - self.closed_waiter: asyncio.Future[None] - - def wrap(self, server: asyncio.base_events.Server) -> None: - """ - Attach to a given :class:`~asyncio.Server`. - - Since :meth:`~asyncio.loop.create_server` doesn't support injecting a - custom ``Server`` class, the easiest solution that doesn't rely on - private :mod:`asyncio` APIs is to: - - - instantiate a :class:`WebSocketServer` - - give the protocol factory a reference to that instance - - call :meth:`~asyncio.loop.create_server` with the factory - - attach the resulting :class:`~asyncio.Server` with this method - - """ - self.server = server - for sock in server.sockets: - if sock.family == socket.AF_INET: - name = "%s:%d" % sock.getsockname() - elif sock.family == socket.AF_INET6: - name = "[%s]:%d" % sock.getsockname()[:2] - elif sock.family == socket.AF_UNIX: - name = sock.getsockname() - # In the unlikely event that someone runs websockets over a - # protocol other than IP or Unix sockets, avoid crashing. - else: # pragma: no cover - name = str(sock.getsockname()) - self.logger.info("server listening on %s", name) - - # Initialized here because we need a reference to the event loop. - # This should be moved back to __init__ when dropping Python < 3.10. - self.closed_waiter = server.get_loop().create_future() - - def register(self, protocol: WebSocketServerProtocol) -> None: - """ - Register a connection with this server. - - """ - self.websockets.add(protocol) - - def unregister(self, protocol: WebSocketServerProtocol) -> None: - """ - Unregister a connection with this server. - - """ - self.websockets.remove(protocol) - - def close(self, close_connections: bool = True) -> None: - """ - Close the server. - - * Close the underlying :class:`~asyncio.Server`. - * When ``close_connections`` is :obj:`True`, which is the default, - close existing connections. Specifically: - - * Reject opening WebSocket connections with an HTTP 503 (service - unavailable) error. This happens when the server accepted the TCP - connection but didn't complete the opening handshake before closing. - * Close open WebSocket connections with close code 1001 (going away). - - * Wait until all connection handlers terminate. - - :meth:`close` is idempotent. - - """ - if self.close_task is None: - self.close_task = self.get_loop().create_task( - self._close(close_connections) - ) - - async def _close(self, close_connections: bool) -> None: - """ - Implementation of :meth:`close`. - - This calls :meth:`~asyncio.Server.close` on the underlying - :class:`~asyncio.Server` object to stop accepting new connections and - then closes open connections with close code 1001. - - """ - self.logger.info("server closing") - - # Stop accepting new connections. - self.server.close() - - # Wait until all accepted connections reach connection_made() and call - # register(). See https://github.com/python/cpython/issues/79033 for - # details. This workaround can be removed when dropping Python < 3.11. - await asyncio.sleep(0) - - if close_connections: - # Close OPEN connections with close code 1001. After server.close(), - # handshake() closes OPENING connections with an HTTP 503 error. - close_tasks = [ - asyncio.create_task(websocket.close(1001)) - for websocket in self.websockets - if websocket.state is not State.CONNECTING - ] - # asyncio.wait doesn't accept an empty first argument. - if close_tasks: - await asyncio.wait(close_tasks) - - # Wait until all TCP connections are closed. - await self.server.wait_closed() - - # Wait until all connection handlers terminate. - # asyncio.wait doesn't accept an empty first argument. - if self.websockets: - await asyncio.wait( - [websocket.handler_task for websocket in self.websockets] - ) - - # Tell wait_closed() to return. - self.closed_waiter.set_result(None) - - self.logger.info("server closed") - - async def wait_closed(self) -> None: - """ - Wait until the server is closed. - - When :meth:`wait_closed` returns, all TCP connections are closed and - all connection handlers have returned. - - To ensure a fast shutdown, a connection handler should always be - awaiting at least one of: - - * :meth:`~WebSocketServerProtocol.recv`: when the connection is closed, - it raises :exc:`~websockets.exceptions.ConnectionClosedOK`; - * :meth:`~WebSocketServerProtocol.wait_closed`: when the connection is - closed, it returns. - - Then the connection handler is immediately notified of the shutdown; - it can clean up and exit. - - """ - await asyncio.shield(self.closed_waiter) - - def get_loop(self) -> asyncio.AbstractEventLoop: - """ - See :meth:`asyncio.Server.get_loop`. - - """ - return self.server.get_loop() - - def is_serving(self) -> bool: - """ - See :meth:`asyncio.Server.is_serving`. - - """ - return self.server.is_serving() - - async def start_serving(self) -> None: # pragma: no cover - """ - See :meth:`asyncio.Server.start_serving`. - - Typical use:: - - server = await serve(..., start_serving=False) - # perform additional setup here... - # ... then start the server - await server.start_serving() - - """ - await self.server.start_serving() - - async def serve_forever(self) -> None: # pragma: no cover - """ - See :meth:`asyncio.Server.serve_forever`. - - Typical use:: - - server = await serve(...) - # this coroutine doesn't return - # canceling it stops the server - await server.serve_forever() - - This is an alternative to using :func:`serve` as an asynchronous context - manager. Shutdown is triggered by canceling :meth:`serve_forever` - instead of exiting a :func:`serve` context. - - """ - await self.server.serve_forever() - - @property - def sockets(self) -> Iterable[socket.socket]: - """ - See :attr:`asyncio.Server.sockets`. - - """ - return self.server.sockets - - async def __aenter__(self) -> WebSocketServer: # pragma: no cover - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: # pragma: no cover - self.close() - await self.wait_closed() - - -class Serve: - """ - Start a WebSocket server listening on ``host`` and ``port``. - - Whenever a client connects, the server creates a - :class:`WebSocketServerProtocol`, performs the opening handshake, and - delegates to the connection handler, ``ws_handler``. - - The handler receives the :class:`WebSocketServerProtocol` and uses it to - send and receive messages. - - Once the handler completes, either normally or with an exception, the - server performs the closing handshake and closes the connection. - - Awaiting :func:`serve` yields a :class:`WebSocketServer`. This object - provides a :meth:`~WebSocketServer.close` method to shut down the server:: - - # set this future to exit the server - stop = asyncio.get_running_loop().create_future() - - server = await serve(...) - await stop - server.close() - await server.wait_closed() - - :func:`serve` can be used as an asynchronous context manager. Then, the - server is shut down automatically when exiting the context:: - - # set this future to exit the server - stop = asyncio.get_running_loop().create_future() - - async with serve(...): - await stop - - Args: - ws_handler: Connection handler. It receives the WebSocket connection, - which is a :class:`WebSocketServerProtocol`, in argument. - host: Network interfaces the server binds to. - See :meth:`~asyncio.loop.create_server` for details. - port: TCP port the server listens on. - See :meth:`~asyncio.loop.create_server` for details. - create_protocol: Factory for the :class:`asyncio.Protocol` managing - the connection. It defaults to :class:`WebSocketServerProtocol`. - Set it to a wrapper or a subclass to customize connection handling. - logger: Logger for this server. - It defaults to ``logging.getLogger("websockets.server")``. - See the :doc:`logging guide <../../topics/logging>` for details. - compression: The "permessage-deflate" extension is enabled by default. - Set ``compression`` to :obj:`None` to disable it. See the - :doc:`compression guide <../../topics/compression>` for details. - origins: Acceptable values of the ``Origin`` header, for defending - against Cross-Site WebSocket Hijacking attacks. Include :obj:`None` - in the list if the lack of an origin is acceptable. - extensions: List of supported extensions, in order in which they - should be negotiated and run. - subprotocols: List of supported subprotocols, in order of decreasing - preference. - extra_headers (HeadersLike | Callable[[str, Headers] | HeadersLike]): - Arbitrary HTTP headers to add to the response. This can be - a :data:`~websockets.datastructures.HeadersLike` or a callable - taking the request path and headers in arguments and returning - a :data:`~websockets.datastructures.HeadersLike`. - server_header: Value of the ``Server`` response header. - It defaults to ``"Python/x.y.z websockets/X.Y"``. - Setting it to :obj:`None` removes the header. - process_request (Callable[[str, Headers], \ - Awaitable[tuple[StatusLike, HeadersLike, bytes] | None]] | None): - Intercept HTTP request before the opening handshake. - See :meth:`~WebSocketServerProtocol.process_request` for details. - select_subprotocol: Select a subprotocol supported by the client. - See :meth:`~WebSocketServerProtocol.select_subprotocol` for details. - open_timeout: Timeout for opening connections in seconds. - :obj:`None` disables the timeout. - - See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the - documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, - ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit``. - - Any other keyword arguments are passed the event loop's - :meth:`~asyncio.loop.create_server` method. - - For example: - - * You can set ``ssl`` to a :class:`~ssl.SSLContext` to enable TLS. - - * You can set ``sock`` to a :obj:`~socket.socket` that you created - outside of websockets. - - Returns: - WebSocket server. - - """ - - def __init__( - self, - # The version that accepts the path in the second argument is deprecated. - ws_handler: ( - Callable[[WebSocketServerProtocol], Awaitable[Any]] - | Callable[[WebSocketServerProtocol, str], Awaitable[Any]] - ), - host: str | Sequence[str] | None = None, - port: int | None = None, - *, - create_protocol: Callable[..., WebSocketServerProtocol] | None = None, - logger: LoggerLike | None = None, - compression: str | None = "deflate", - origins: Sequence[Origin | None] | None = None, - extensions: Sequence[ServerExtensionFactory] | None = None, - subprotocols: Sequence[Subprotocol] | None = None, - extra_headers: HeadersLikeOrCallable | None = None, - server_header: str | None = SERVER, - process_request: ( - Callable[[str, Headers], Awaitable[HTTPResponse | None]] | None - ) = None, - select_subprotocol: ( - Callable[[Sequence[Subprotocol], Sequence[Subprotocol]], Subprotocol] | None - ) = None, - open_timeout: float | None = 10, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = None, - max_size: int | None = 2**20, - max_queue: int | None = 2**5, - read_limit: int = 2**16, - write_limit: int = 2**16, - **kwargs: Any, - ) -> None: - # Backwards compatibility: close_timeout used to be called timeout. - timeout: float | None = kwargs.pop("timeout", None) - if timeout is None: - timeout = 10 - else: - warnings.warn("rename timeout to close_timeout", DeprecationWarning) - # If both are specified, timeout is ignored. - if close_timeout is None: - close_timeout = timeout - - # Backwards compatibility: create_protocol used to be called klass. - klass: type[WebSocketServerProtocol] | None = kwargs.pop("klass", None) - if klass is None: - klass = WebSocketServerProtocol - else: - warnings.warn("rename klass to create_protocol", DeprecationWarning) - # If both are specified, klass is ignored. - if create_protocol is None: - create_protocol = klass - - # Backwards compatibility: recv() used to return None on closed connections - legacy_recv: bool = kwargs.pop("legacy_recv", False) - - # Backwards compatibility: the loop parameter used to be supported. - _loop: asyncio.AbstractEventLoop | None = kwargs.pop("loop", None) - if _loop is None: - loop = asyncio.get_event_loop() - else: - loop = _loop - warnings.warn("remove loop argument", DeprecationWarning) - - ws_server = WebSocketServer(logger=logger) - - secure = kwargs.get("ssl") is not None - - if compression == "deflate": - extensions = enable_server_permessage_deflate(extensions) - elif compression is not None: - raise ValueError(f"unsupported compression: {compression}") - - if subprotocols is not None: - validate_subprotocols(subprotocols) - - # Help mypy and avoid this error: "type[WebSocketServerProtocol] | - # Callable[..., WebSocketServerProtocol]" not callable [misc] - create_protocol = cast(Callable[..., WebSocketServerProtocol], create_protocol) - factory = functools.partial( - create_protocol, - # For backwards compatibility with 10.0 or earlier. Done here in - # addition to WebSocketServerProtocol to trigger the deprecation - # warning once per serve() call rather than once per connection. - remove_path_argument(ws_handler), - ws_server, - host=host, - port=port, - secure=secure, - open_timeout=open_timeout, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - close_timeout=close_timeout, - max_size=max_size, - max_queue=max_queue, - read_limit=read_limit, - write_limit=write_limit, - loop=_loop, - legacy_recv=legacy_recv, - origins=origins, - extensions=extensions, - subprotocols=subprotocols, - extra_headers=extra_headers, - server_header=server_header, - process_request=process_request, - select_subprotocol=select_subprotocol, - logger=logger, - ) - - if kwargs.pop("unix", False): - path: str | None = kwargs.pop("path", None) - # unix_serve(path) must not specify host and port parameters. - assert host is None and port is None - create_server = functools.partial( - loop.create_unix_server, factory, path, **kwargs - ) - else: - create_server = functools.partial( - loop.create_server, factory, host, port, **kwargs - ) - - # This is a coroutine function. - self._create_server = create_server - self.ws_server = ws_server - - # async with serve(...) - - async def __aenter__(self) -> WebSocketServer: - return await self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - self.ws_server.close() - await self.ws_server.wait_closed() - - # await serve(...) - - def __await__(self) -> Generator[Any, None, WebSocketServer]: - # Create a suitable iterator by calling __await__ on a coroutine. - return self.__await_impl__().__await__() - - async def __await_impl__(self) -> WebSocketServer: - server = await self._create_server() - self.ws_server.wrap(server) - return self.ws_server - - # yield from serve(...) - remove when dropping Python < 3.10 - - __iter__ = __await__ - - -serve = Serve - - -def unix_serve( - # The version that accepts the path in the second argument is deprecated. - ws_handler: ( - Callable[[WebSocketServerProtocol], Awaitable[Any]] - | Callable[[WebSocketServerProtocol, str], Awaitable[Any]] - ), - path: str | None = None, - **kwargs: Any, -) -> Serve: - """ - Start a WebSocket server listening on a Unix socket. - - This function is identical to :func:`serve`, except the ``host`` and - ``port`` arguments are replaced by ``path``. It is only available on Unix. - - Unrecognized keyword arguments are passed the event loop's - :meth:`~asyncio.loop.create_unix_server` method. - - It's useful for deploying a server behind a reverse proxy such as nginx. - - Args: - path: File system path to the Unix socket. - - """ - return serve(ws_handler, path=path, unix=True, **kwargs) - - -def remove_path_argument( - ws_handler: ( - Callable[[WebSocketServerProtocol], Awaitable[Any]] - | Callable[[WebSocketServerProtocol, str], Awaitable[Any]] - ), -) -> Callable[[WebSocketServerProtocol], Awaitable[Any]]: - try: - inspect.signature(ws_handler).bind(None) - except TypeError: - try: - inspect.signature(ws_handler).bind(None, "") - except TypeError: # pragma: no cover - # ws_handler accepts neither one nor two arguments; leave it alone. - pass - else: - # ws_handler accepts two arguments; activate backwards compatibility. - warnings.warn("remove second argument of ws_handler", DeprecationWarning) - - async def _ws_handler(websocket: WebSocketServerProtocol) -> Any: - return await cast( - Callable[[WebSocketServerProtocol, str], Awaitable[Any]], - ws_handler, - )(websocket, websocket.path) - - return _ws_handler - - return cast( - Callable[[WebSocketServerProtocol], Awaitable[Any]], - ws_handler, - ) diff --git a/wip/SNIP/client/websockets/protocol.py b/wip/SNIP/client/websockets/protocol.py deleted file mode 100644 index bc64a21..0000000 --- a/wip/SNIP/client/websockets/protocol.py +++ /dev/null @@ -1,758 +0,0 @@ -from __future__ import annotations - -import enum -import logging -import uuid -from collections.abc import Generator -from typing import Union - -from .exceptions import ( - ConnectionClosed, - ConnectionClosedError, - ConnectionClosedOK, - InvalidState, - PayloadTooBig, - ProtocolError, -) -from .extensions import Extension -from .frames import ( - OK_CLOSE_CODES, - OP_BINARY, - OP_CLOSE, - OP_CONT, - OP_PING, - OP_PONG, - OP_TEXT, - Close, - CloseCode, - Frame, -) -from .http11 import Request, Response -from .streams import StreamReader -from .typing import LoggerLike, Origin, Subprotocol - - -__all__ = [ - "Protocol", - "Side", - "State", - "SEND_EOF", -] - -# Change to Request | Response | Frame when dropping Python < 3.10. -Event = Union[Request, Response, Frame] -"""Events that :meth:`~Protocol.events_received` may return.""" - - -class Side(enum.IntEnum): - """A WebSocket connection is either a server or a client.""" - - SERVER, CLIENT = range(2) - - -SERVER = Side.SERVER -CLIENT = Side.CLIENT - - -class State(enum.IntEnum): - """A WebSocket connection is in one of these four states.""" - - CONNECTING, OPEN, CLOSING, CLOSED = range(4) - - -CONNECTING = State.CONNECTING -OPEN = State.OPEN -CLOSING = State.CLOSING -CLOSED = State.CLOSED - - -SEND_EOF = b"" -"""Sentinel signaling that the TCP connection must be half-closed.""" - - -class Protocol: - """ - Sans-I/O implementation of a WebSocket connection. - - Args: - side: :attr:`~Side.CLIENT` or :attr:`~Side.SERVER`. - state: Initial state of the WebSocket connection. - max_size: Maximum size of incoming messages in bytes; - :obj:`None` disables the limit. - logger: Logger for this connection; depending on ``side``, - defaults to ``logging.getLogger("websockets.client")`` - or ``logging.getLogger("websockets.server")``; - see the :doc:`logging guide <../../topics/logging>` for details. - - """ - - def __init__( - self, - side: Side, - *, - state: State = OPEN, - max_size: int | None = 2**20, - logger: LoggerLike | None = None, - ) -> None: - # Unique identifier. For logs. - self.id: uuid.UUID = uuid.uuid4() - """Unique identifier of the connection. Useful in logs.""" - - # Logger or LoggerAdapter for this connection. - if logger is None: - logger = logging.getLogger(f"websockets.{side.name.lower()}") - self.logger: LoggerLike = logger - """Logger for this connection.""" - - # Track if DEBUG is enabled. Shortcut logging calls if it isn't. - self.debug = logger.isEnabledFor(logging.DEBUG) - - # Connection side. CLIENT or SERVER. - self.side = side - - # Connection state. Initially OPEN because subclasses handle CONNECTING. - self.state = state - - # Maximum size of incoming messages in bytes. - self.max_size = max_size - - # Current size of incoming message in bytes. Only set while reading a - # fragmented message i.e. a data frames with the FIN bit not set. - self.cur_size: int | None = None - - # True while sending a fragmented message i.e. a data frames with the - # FIN bit not set. - self.expect_continuation_frame = False - - # WebSocket protocol parameters. - self.origin: Origin | None = None - self.extensions: list[Extension] = [] - self.subprotocol: Subprotocol | None = None - - # Close code and reason, set when a close frame is sent or received. - self.close_rcvd: Close | None = None - self.close_sent: Close | None = None - self.close_rcvd_then_sent: bool | None = None - - # Track if an exception happened during the handshake. - self.handshake_exc: Exception | None = None - """ - Exception to raise if the opening handshake failed. - - :obj:`None` if the opening handshake succeeded. - - """ - - # Track if send_eof() was called. - self.eof_sent = False - - # Parser state. - self.reader = StreamReader() - self.events: list[Event] = [] - self.writes: list[bytes] = [] - self.parser = self.parse() - next(self.parser) # start coroutine - self.parser_exc: Exception | None = None - - @property - def state(self) -> State: - """ - State of the WebSocket connection. - - Defined in 4.1_, 4.2_, 7.1.3_, and 7.1.4_ of :rfc:`6455`. - - .. _4.1: https://datatracker.ietf.org/doc/html/rfc6455#section-4.1 - .. _4.2: https://datatracker.ietf.org/doc/html/rfc6455#section-4.2 - .. _7.1.3: https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.3 - .. _7.1.4: https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.4 - - """ - return self._state - - @state.setter - def state(self, state: State) -> None: - if self.debug: - self.logger.debug("= connection is %s", state.name) - self._state = state - - @property - def close_code(self) -> int | None: - """ - WebSocket close code received from the remote endpoint. - - Defined in 7.1.5_ of :rfc:`6455`. - - .. _7.1.5: https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5 - - :obj:`None` if the connection isn't closed yet. - - """ - if self.state is not CLOSED: - return None - elif self.close_rcvd is None: - return CloseCode.ABNORMAL_CLOSURE - else: - return self.close_rcvd.code - - @property - def close_reason(self) -> str | None: - """ - WebSocket close reason received from the remote endpoint. - - Defined in 7.1.6_ of :rfc:`6455`. - - .. _7.1.6: https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6 - - :obj:`None` if the connection isn't closed yet. - - """ - if self.state is not CLOSED: - return None - elif self.close_rcvd is None: - return "" - else: - return self.close_rcvd.reason - - @property - def close_exc(self) -> ConnectionClosed: - """ - Exception to raise when trying to interact with a closed connection. - - Don't raise this exception while the connection :attr:`state` - is :attr:`~websockets.protocol.State.CLOSING`; wait until - it's :attr:`~websockets.protocol.State.CLOSED`. - - Indeed, the exception includes the close code and reason, which are - known only once the connection is closed. - - Raises: - AssertionError: If the connection isn't closed yet. - - """ - assert self.state is CLOSED, "connection isn't closed yet" - exc_type: type[ConnectionClosed] - if ( - self.close_rcvd is not None - and self.close_sent is not None - and self.close_rcvd.code in OK_CLOSE_CODES - and self.close_sent.code in OK_CLOSE_CODES - ): - exc_type = ConnectionClosedOK - else: - exc_type = ConnectionClosedError - exc: ConnectionClosed = exc_type( - self.close_rcvd, - self.close_sent, - self.close_rcvd_then_sent, - ) - # Chain to the exception raised in the parser, if any. - exc.__cause__ = self.parser_exc - return exc - - # Public methods for receiving data. - - def receive_data(self, data: bytes) -> None: - """ - Receive data from the network. - - After calling this method: - - - You must call :meth:`data_to_send` and send this data to the network. - - You should call :meth:`events_received` and process resulting events. - - Raises: - EOFError: If :meth:`receive_eof` was called earlier. - - """ - self.reader.feed_data(data) - next(self.parser) - - def receive_eof(self) -> None: - """ - Receive the end of the data stream from the network. - - After calling this method: - - - You must call :meth:`data_to_send` and send this data to the network; - it will return ``[b""]``, signaling the end of the stream, or ``[]``. - - You aren't expected to call :meth:`events_received`; it won't return - any new events. - - :meth:`receive_eof` is idempotent. - - """ - if self.reader.eof: - return - self.reader.feed_eof() - next(self.parser) - - # Public methods for sending events. - - def send_continuation(self, data: bytes, fin: bool) -> None: - """ - Send a `Continuation frame`_. - - .. _Continuation frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - Parameters: - data: payload containing the same kind of data - as the initial frame. - fin: FIN bit; set it to :obj:`True` if this is the last frame - of a fragmented message and to :obj:`False` otherwise. - - Raises: - ProtocolError: If a fragmented message isn't in progress. - - """ - if not self.expect_continuation_frame: - raise ProtocolError("unexpected continuation frame") - if self._state is not OPEN: - raise InvalidState(f"connection is {self.state.name.lower()}") - self.expect_continuation_frame = not fin - self.send_frame(Frame(OP_CONT, data, fin)) - - def send_text(self, data: bytes, fin: bool = True) -> None: - """ - Send a `Text frame`_. - - .. _Text frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - Parameters: - data: payload containing text encoded with UTF-8. - fin: FIN bit; set it to :obj:`False` if this is the first frame of - a fragmented message. - - Raises: - ProtocolError: If a fragmented message is in progress. - - """ - if self.expect_continuation_frame: - raise ProtocolError("expected a continuation frame") - if self._state is not OPEN: - raise InvalidState(f"connection is {self.state.name.lower()}") - self.expect_continuation_frame = not fin - self.send_frame(Frame(OP_TEXT, data, fin)) - - def send_binary(self, data: bytes, fin: bool = True) -> None: - """ - Send a `Binary frame`_. - - .. _Binary frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - Parameters: - data: payload containing arbitrary binary data. - fin: FIN bit; set it to :obj:`False` if this is the first frame of - a fragmented message. - - Raises: - ProtocolError: If a fragmented message is in progress. - - """ - if self.expect_continuation_frame: - raise ProtocolError("expected a continuation frame") - if self._state is not OPEN: - raise InvalidState(f"connection is {self.state.name.lower()}") - self.expect_continuation_frame = not fin - self.send_frame(Frame(OP_BINARY, data, fin)) - - def send_close(self, code: int | None = None, reason: str = "") -> None: - """ - Send a `Close frame`_. - - .. _Close frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.1 - - Parameters: - code: close code. - reason: close reason. - - Raises: - ProtocolError: If the code isn't valid or if a reason is provided - without a code. - - """ - # While RFC 6455 doesn't rule out sending more than one close Frame, - # websockets is conservative in what it sends and doesn't allow that. - if self._state is not OPEN: - raise InvalidState(f"connection is {self.state.name.lower()}") - if code is None: - if reason != "": - raise ProtocolError("cannot send a reason without a code") - close = Close(CloseCode.NO_STATUS_RCVD, "") - data = b"" - else: - close = Close(code, reason) - data = close.serialize() - # 7.1.3. The WebSocket Closing Handshake is Started - self.send_frame(Frame(OP_CLOSE, data)) - # Since the state is OPEN, no close frame was received yet. - # As a consequence, self.close_rcvd_then_sent remains None. - assert self.close_rcvd is None - self.close_sent = close - self.state = CLOSING - - def send_ping(self, data: bytes) -> None: - """ - Send a `Ping frame`_. - - .. _Ping frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2 - - Parameters: - data: payload containing arbitrary binary data. - - """ - # RFC 6455 allows control frames after starting the closing handshake. - if self._state is not OPEN and self._state is not CLOSING: - raise InvalidState(f"connection is {self.state.name.lower()}") - self.send_frame(Frame(OP_PING, data)) - - def send_pong(self, data: bytes) -> None: - """ - Send a `Pong frame`_. - - .. _Pong frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.3 - - Parameters: - data: payload containing arbitrary binary data. - - """ - # RFC 6455 allows control frames after starting the closing handshake. - if self._state is not OPEN and self._state is not CLOSING: - raise InvalidState(f"connection is {self.state.name.lower()}") - self.send_frame(Frame(OP_PONG, data)) - - def fail(self, code: int, reason: str = "") -> None: - """ - `Fail the WebSocket connection`_. - - .. _Fail the WebSocket connection: - https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.7 - - Parameters: - code: close code - reason: close reason - - Raises: - ProtocolError: If the code isn't valid. - """ - # 7.1.7. Fail the WebSocket Connection - - # Send a close frame when the state is OPEN (a close frame was already - # sent if it's CLOSING), except when failing the connection because - # of an error reading from or writing to the network. - if self.state is OPEN: - if code != CloseCode.ABNORMAL_CLOSURE: - close = Close(code, reason) - data = close.serialize() - self.send_frame(Frame(OP_CLOSE, data)) - self.close_sent = close - # If recv_messages() raised an exception upon receiving a close - # frame but before echoing it, then close_rcvd is not None even - # though the state is OPEN. This happens when the connection is - # closed while receiving a fragmented message. - if self.close_rcvd is not None: - self.close_rcvd_then_sent = True - self.state = CLOSING - - # When failing the connection, a server closes the TCP connection - # without waiting for the client to complete the handshake, while a - # client waits for the server to close the TCP connection, possibly - # after sending a close frame that the client will ignore. - if self.side is SERVER and not self.eof_sent: - self.send_eof() - - # 7.1.7. Fail the WebSocket Connection "An endpoint MUST NOT continue - # to attempt to process data(including a responding Close frame) from - # the remote endpoint after being instructed to _Fail the WebSocket - # Connection_." - self.parser = self.discard() - next(self.parser) # start coroutine - - # Public method for getting incoming events after receiving data. - - def events_received(self) -> list[Event]: - """ - Fetch events generated from data received from the network. - - Call this method immediately after any of the ``receive_*()`` methods. - - Process resulting events, likely by passing them to the application. - - Returns: - Events read from the connection. - """ - events, self.events = self.events, [] - return events - - # Public method for getting outgoing data after receiving data or sending events. - - def data_to_send(self) -> list[bytes]: - """ - Obtain data to send to the network. - - Call this method immediately after any of the ``receive_*()``, - ``send_*()``, or :meth:`fail` methods. - - Write resulting data to the connection. - - The empty bytestring :data:`~websockets.protocol.SEND_EOF` signals - the end of the data stream. When you receive it, half-close the TCP - connection. - - Returns: - Data to write to the connection. - - """ - writes, self.writes = self.writes, [] - return writes - - def close_expected(self) -> bool: - """ - Tell if the TCP connection is expected to close soon. - - Call this method immediately after any of the ``receive_*()``, - ``send_close()``, or :meth:`fail` methods. - - If it returns :obj:`True`, schedule closing the TCP connection after a - short timeout if the other side hasn't already closed it. - - Returns: - Whether the TCP connection is expected to close soon. - - """ - # During the opening handshake, when our state is CONNECTING, we expect - # a TCP close if and only if the hansdake fails. When it does, we start - # the TCP closing handshake by sending EOF with send_eof(). - - # Once the opening handshake completes successfully, we expect a TCP - # close if and only if we sent a close frame, meaning that our state - # progressed to CLOSING: - - # * Normal closure: once we send a close frame, we expect a TCP close: - # server waits for client to complete the TCP closing handshake; - # client waits for server to initiate the TCP closing handshake. - - # * Abnormal closure: we always send a close frame and the same logic - # applies, except on EOFError where we don't send a close frame - # because we already received the TCP close, so we don't expect it. - - # If our state is CLOSED, we already received a TCP close so we don't - # expect it anymore. - - # Micro-optimization: put the most common case first - if self.state is OPEN: - return False - if self.state is CLOSING: - return True - if self.state is CLOSED: - return False - assert self.state is CONNECTING - return self.eof_sent - - # Private methods for receiving data. - - def parse(self) -> Generator[None]: - """ - Parse incoming data into frames. - - :meth:`receive_data` and :meth:`receive_eof` run this generator - coroutine until it needs more data or reaches EOF. - - :meth:`parse` never raises an exception. Instead, it sets the - :attr:`parser_exc` and yields control. - - """ - try: - while True: - if (yield from self.reader.at_eof()): - if self.debug: - self.logger.debug("< EOF") - # If the WebSocket connection is closed cleanly, with a - # closing handhshake, recv_frame() substitutes parse() - # with discard(). This branch is reached only when the - # connection isn't closed cleanly. - raise EOFError("unexpected end of stream") - - if self.max_size is None: - max_size = None - elif self.cur_size is None: - max_size = self.max_size - else: - max_size = self.max_size - self.cur_size - - # During a normal closure, execution ends here on the next - # iteration of the loop after receiving a close frame. At - # this point, recv_frame() replaced parse() by discard(). - frame = yield from Frame.parse( - self.reader.read_exact, - mask=self.side is SERVER, - max_size=max_size, - extensions=self.extensions, - ) - - if self.debug: - self.logger.debug("< %s", frame) - - self.recv_frame(frame) - - except ProtocolError as exc: - self.fail(CloseCode.PROTOCOL_ERROR, str(exc)) - self.parser_exc = exc - - except EOFError as exc: - self.fail(CloseCode.ABNORMAL_CLOSURE, str(exc)) - self.parser_exc = exc - - except UnicodeDecodeError as exc: - self.fail(CloseCode.INVALID_DATA, f"{exc.reason} at position {exc.start}") - self.parser_exc = exc - - except PayloadTooBig as exc: - exc.set_current_size(self.cur_size) - self.fail(CloseCode.MESSAGE_TOO_BIG, str(exc)) - self.parser_exc = exc - - except Exception as exc: - self.logger.error("parser failed", exc_info=True) - # Don't include exception details, which may be security-sensitive. - self.fail(CloseCode.INTERNAL_ERROR) - self.parser_exc = exc - - # During an abnormal closure, execution ends here after catching an - # exception. At this point, fail() replaced parse() by discard(). - yield - raise AssertionError("parse() shouldn't step after error") - - def discard(self) -> Generator[None]: - """ - Discard incoming data. - - This coroutine replaces :meth:`parse`: - - - after receiving a close frame, during a normal closure (1.4); - - after sending a close frame, during an abnormal closure (7.1.7). - - """ - # After the opening handshake completes, the server closes the TCP - # connection in the same circumstances where discard() replaces parse(). - # The client closes it when it receives EOF from the server or times - # out. (The latter case cannot be handled in this Sans-I/O layer.) - assert (self.side is SERVER or self.state is CONNECTING) == (self.eof_sent) - while not (yield from self.reader.at_eof()): - self.reader.discard() - if self.debug: - self.logger.debug("< EOF") - # A server closes the TCP connection immediately, while a client - # waits for the server to close the TCP connection. - if self.side is CLIENT and self.state is not CONNECTING: - self.send_eof() - self.state = CLOSED - # If discard() completes normally, execution ends here. - yield - # Once the reader reaches EOF, its feed_data/eof() methods raise an - # error, so our receive_data/eof() methods don't step the generator. - raise AssertionError("discard() shouldn't step after EOF") - - def recv_frame(self, frame: Frame) -> None: - """ - Process an incoming frame. - - """ - if frame.opcode is OP_TEXT or frame.opcode is OP_BINARY: - if self.cur_size is not None: - raise ProtocolError("expected a continuation frame") - if not frame.fin: - self.cur_size = len(frame.data) - - elif frame.opcode is OP_CONT: - if self.cur_size is None: - raise ProtocolError("unexpected continuation frame") - if frame.fin: - self.cur_size = None - else: - self.cur_size += len(frame.data) - - elif frame.opcode is OP_PING: - # 5.5.2. Ping: "Upon receipt of a Ping frame, an endpoint MUST - # send a Pong frame in response" - pong_frame = Frame(OP_PONG, frame.data) - self.send_frame(pong_frame) - - elif frame.opcode is OP_PONG: - # 5.5.3 Pong: "A response to an unsolicited Pong frame is not - # expected." - pass - - elif frame.opcode is OP_CLOSE: - # 7.1.5. The WebSocket Connection Close Code - # 7.1.6. The WebSocket Connection Close Reason - self.close_rcvd = Close.parse(frame.data) - if self.state is CLOSING: - assert self.close_sent is not None - self.close_rcvd_then_sent = False - - if self.cur_size is not None: - raise ProtocolError("incomplete fragmented message") - - # 5.5.1 Close: "If an endpoint receives a Close frame and did - # not previously send a Close frame, the endpoint MUST send a - # Close frame in response. (When sending a Close frame in - # response, the endpoint typically echos the status code it - # received.)" - - if self.state is OPEN: - # Echo the original data instead of re-serializing it with - # Close.serialize() because that fails when the close frame - # is empty and Close.parse() synthesizes a 1005 close code. - # The rest is identical to send_close(). - self.send_frame(Frame(OP_CLOSE, frame.data)) - self.close_sent = self.close_rcvd - self.close_rcvd_then_sent = True - self.state = CLOSING - - # 7.1.2. Start the WebSocket Closing Handshake: "Once an - # endpoint has both sent and received a Close control frame, - # that endpoint SHOULD _Close the WebSocket Connection_" - - # A server closes the TCP connection immediately, while a client - # waits for the server to close the TCP connection. - if self.side is SERVER: - self.send_eof() - - # 1.4. Closing Handshake: "after receiving a control frame - # indicating the connection should be closed, a peer discards - # any further data received." - # RFC 6455 allows reading Ping and Pong frames after a Close frame. - # However, that doesn't seem useful; websockets doesn't support it. - self.parser = self.discard() - next(self.parser) # start coroutine - - else: - # This can't happen because Frame.parse() validates opcodes. - raise AssertionError(f"unexpected opcode: {frame.opcode:02x}") - - self.events.append(frame) - - # Private methods for sending events. - - def send_frame(self, frame: Frame) -> None: - if self.debug: - self.logger.debug("> %s", frame) - self.writes.append( - frame.serialize( - mask=self.side is CLIENT, - extensions=self.extensions, - ) - ) - - def send_eof(self) -> None: - assert not self.eof_sent - self.eof_sent = True - if self.debug: - self.logger.debug("> EOF") - self.writes.append(SEND_EOF) diff --git a/wip/SNIP/client/websockets/py.typed b/wip/SNIP/client/websockets/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/wip/SNIP/client/websockets/server.py b/wip/SNIP/client/websockets/server.py deleted file mode 100644 index 1744412..0000000 --- a/wip/SNIP/client/websockets/server.py +++ /dev/null @@ -1,587 +0,0 @@ -from __future__ import annotations - -import base64 -import binascii -import email.utils -import http -import re -import warnings -from collections.abc import Generator, Sequence -from typing import Any, Callable, cast - -from .datastructures import Headers, MultipleValuesError -from .exceptions import ( - InvalidHandshake, - InvalidHeader, - InvalidHeaderValue, - InvalidMessage, - InvalidOrigin, - InvalidUpgrade, - NegotiationError, -) -from .extensions import Extension, ServerExtensionFactory -from .headers import ( - build_extension, - parse_connection, - parse_extension, - parse_subprotocol, - parse_upgrade, -) -from .http11 import Request, Response -from .imports import lazy_import -from .protocol import CONNECTING, OPEN, SERVER, Protocol, State -from .typing import ( - ConnectionOption, - ExtensionHeader, - LoggerLike, - Origin, - StatusLike, - Subprotocol, - UpgradeProtocol, -) -from .utils import accept_key - - -__all__ = ["ServerProtocol"] - - -class ServerProtocol(Protocol): - """ - Sans-I/O implementation of a WebSocket server connection. - - Args: - origins: Acceptable values of the ``Origin`` header. Values can be - :class:`str` to test for an exact match or regular expressions - compiled by :func:`re.compile` to test against a pattern. Include - :obj:`None` in the list if the lack of an origin is acceptable. - This is useful for defending against Cross-Site WebSocket - Hijacking attacks. - extensions: List of supported extensions, in order in which they - should be tried. - subprotocols: List of supported subprotocols, in order of decreasing - preference. - select_subprotocol: Callback for selecting a subprotocol among - those supported by the client and the server. It has the same - signature as the :meth:`select_subprotocol` method, including a - :class:`ServerProtocol` instance as first argument. - state: Initial state of the WebSocket connection. - max_size: Maximum size of incoming messages in bytes; - :obj:`None` disables the limit. - logger: Logger for this connection; - defaults to ``logging.getLogger("websockets.server")``; - see the :doc:`logging guide <../../topics/logging>` for details. - - """ - - def __init__( - self, - *, - origins: Sequence[Origin | re.Pattern[str] | None] | None = None, - extensions: Sequence[ServerExtensionFactory] | None = None, - subprotocols: Sequence[Subprotocol] | None = None, - select_subprotocol: ( - Callable[ - [ServerProtocol, Sequence[Subprotocol]], - Subprotocol | None, - ] - | None - ) = None, - state: State = CONNECTING, - max_size: int | None = 2**20, - logger: LoggerLike | None = None, - ) -> None: - super().__init__( - side=SERVER, - state=state, - max_size=max_size, - logger=logger, - ) - self.origins = origins - self.available_extensions = extensions - self.available_subprotocols = subprotocols - if select_subprotocol is not None: - # Bind select_subprotocol then shadow self.select_subprotocol. - # Use setattr to work around https://github.com/python/mypy/issues/2427. - setattr( - self, - "select_subprotocol", - select_subprotocol.__get__(self, self.__class__), - ) - - def accept(self, request: Request) -> Response: - """ - Create a handshake response to accept the connection. - - If the handshake request is valid and the handshake successful, - :meth:`accept` returns an HTTP response with status code 101. - - Else, it returns an HTTP response with another status code. This rejects - the connection, like :meth:`reject` would. - - You must send the handshake response with :meth:`send_response`. - - You may modify the response before sending it, typically by adding HTTP - headers. - - Args: - request: WebSocket handshake request received from the client. - - Returns: - WebSocket handshake response or HTTP response to send to the client. - - """ - try: - ( - accept_header, - extensions_header, - protocol_header, - ) = self.process_request(request) - except InvalidOrigin as exc: - request._exception = exc - self.handshake_exc = exc - if self.debug: - self.logger.debug("! invalid origin", exc_info=True) - return self.reject( - http.HTTPStatus.FORBIDDEN, - f"Failed to open a WebSocket connection: {exc}.\n", - ) - except InvalidUpgrade as exc: - request._exception = exc - self.handshake_exc = exc - if self.debug: - self.logger.debug("! invalid upgrade", exc_info=True) - response = self.reject( - http.HTTPStatus.UPGRADE_REQUIRED, - ( - f"Failed to open a WebSocket connection: {exc}.\n" - f"\n" - f"You cannot access a WebSocket server directly " - f"with a browser. You need a WebSocket client.\n" - ), - ) - response.headers["Upgrade"] = "websocket" - return response - except InvalidHandshake as exc: - request._exception = exc - self.handshake_exc = exc - if self.debug: - self.logger.debug("! invalid handshake", exc_info=True) - exc_chain = cast(BaseException, exc) - exc_str = f"{exc_chain}" - while exc_chain.__cause__ is not None: - exc_chain = exc_chain.__cause__ - exc_str += f"; {exc_chain}" - return self.reject( - http.HTTPStatus.BAD_REQUEST, - f"Failed to open a WebSocket connection: {exc_str}.\n", - ) - except Exception as exc: - # Handle exceptions raised by user-provided select_subprotocol and - # unexpected errors. - request._exception = exc - self.handshake_exc = exc - self.logger.error("opening handshake failed", exc_info=True) - return self.reject( - http.HTTPStatus.INTERNAL_SERVER_ERROR, - ( - "Failed to open a WebSocket connection.\n" - "See server log for more information.\n" - ), - ) - - headers = Headers() - headers["Date"] = email.utils.formatdate(usegmt=True) - headers["Upgrade"] = "websocket" - headers["Connection"] = "Upgrade" - headers["Sec-WebSocket-Accept"] = accept_header - if extensions_header is not None: - headers["Sec-WebSocket-Extensions"] = extensions_header - if protocol_header is not None: - headers["Sec-WebSocket-Protocol"] = protocol_header - return Response(101, "Switching Protocols", headers) - - def process_request( - self, - request: Request, - ) -> tuple[str, str | None, str | None]: - """ - Check a handshake request and negotiate extensions and subprotocol. - - This function doesn't verify that the request is an HTTP/1.1 or higher - GET request and doesn't check the ``Host`` header. These controls are - usually performed earlier in the HTTP request handling code. They're - the responsibility of the caller. - - Args: - request: WebSocket handshake request received from the client. - - Returns: - ``Sec-WebSocket-Accept``, ``Sec-WebSocket-Extensions``, and - ``Sec-WebSocket-Protocol`` headers for the handshake response. - - Raises: - InvalidHandshake: If the handshake request is invalid; - then the server must return 400 Bad Request error. - - """ - headers = request.headers - - connection: list[ConnectionOption] = sum( - [parse_connection(value) for value in headers.get_all("Connection")], [] - ) - if not any(value.lower() == "upgrade" for value in connection): - raise InvalidUpgrade( - "Connection", ", ".join(connection) if connection else None - ) - - upgrade: list[UpgradeProtocol] = sum( - [parse_upgrade(value) for value in headers.get_all("Upgrade")], [] - ) - # For compatibility with non-strict implementations, ignore case when - # checking the Upgrade header. The RFC always uses "websocket", except - # in section 11.2. (IANA registration) where it uses "WebSocket". - if not (len(upgrade) == 1 and upgrade[0].lower() == "websocket"): - raise InvalidUpgrade("Upgrade", ", ".join(upgrade) if upgrade else None) - - try: - key = headers["Sec-WebSocket-Key"] - except KeyError: - raise InvalidHeader("Sec-WebSocket-Key") from None - except MultipleValuesError: - raise InvalidHeader("Sec-WebSocket-Key", "multiple values") from None - try: - raw_key = base64.b64decode(key.encode(), validate=True) - except binascii.Error as exc: - raise InvalidHeaderValue("Sec-WebSocket-Key", key) from exc - if len(raw_key) != 16: - raise InvalidHeaderValue("Sec-WebSocket-Key", key) - accept_header = accept_key(key) - - try: - version = headers["Sec-WebSocket-Version"] - except KeyError: - raise InvalidHeader("Sec-WebSocket-Version") from None - except MultipleValuesError: - raise InvalidHeader("Sec-WebSocket-Version", "multiple values") from None - if version != "13": - raise InvalidHeaderValue("Sec-WebSocket-Version", version) - - self.origin = self.process_origin(headers) - extensions_header, self.extensions = self.process_extensions(headers) - protocol_header = self.subprotocol = self.process_subprotocol(headers) - - return (accept_header, extensions_header, protocol_header) - - def process_origin(self, headers: Headers) -> Origin | None: - """ - Handle the Origin HTTP request header. - - Args: - headers: WebSocket handshake request headers. - - Returns: - origin, if it is acceptable. - - Raises: - InvalidHandshake: If the Origin header is invalid. - InvalidOrigin: If the origin isn't acceptable. - - """ - # "The user agent MUST NOT include more than one Origin header field" - # per https://datatracker.ietf.org/doc/html/rfc6454#section-7.3. - try: - origin = headers.get("Origin") - except MultipleValuesError: - raise InvalidHeader("Origin", "multiple values") from None - if origin is not None: - origin = cast(Origin, origin) - if self.origins is not None: - for origin_or_regex in self.origins: - if origin_or_regex == origin or ( - isinstance(origin_or_regex, re.Pattern) - and origin is not None - and origin_or_regex.fullmatch(origin) is not None - ): - break - else: - raise InvalidOrigin(origin) - return origin - - def process_extensions( - self, - headers: Headers, - ) -> tuple[str | None, list[Extension]]: - """ - Handle the Sec-WebSocket-Extensions HTTP request header. - - Accept or reject each extension proposed in the client request. - Negotiate parameters for accepted extensions. - - Per :rfc:`6455`, negotiation rules are defined by the specification of - each extension. - - To provide this level of flexibility, for each extension proposed by - the client, we check for a match with each extension available in the - server configuration. If no match is found, the extension is ignored. - - If several variants of the same extension are proposed by the client, - it may be accepted several times, which won't make sense in general. - Extensions must implement their own requirements. For this purpose, - the list of previously accepted extensions is provided. - - This process doesn't allow the server to reorder extensions. It can - only select a subset of the extensions proposed by the client. - - Other requirements, for example related to mandatory extensions or the - order of extensions, may be implemented by overriding this method. - - Args: - headers: WebSocket handshake request headers. - - Returns: - ``Sec-WebSocket-Extensions`` HTTP response header and list of - accepted extensions. - - Raises: - InvalidHandshake: If the Sec-WebSocket-Extensions header is invalid. - - """ - response_header_value: str | None = None - - extension_headers: list[ExtensionHeader] = [] - accepted_extensions: list[Extension] = [] - - header_values = headers.get_all("Sec-WebSocket-Extensions") - - if header_values and self.available_extensions: - parsed_header_values: list[ExtensionHeader] = sum( - [parse_extension(header_value) for header_value in header_values], [] - ) - - for name, request_params in parsed_header_values: - for ext_factory in self.available_extensions: - # Skip non-matching extensions based on their name. - if ext_factory.name != name: - continue - - # Skip non-matching extensions based on their params. - try: - response_params, extension = ext_factory.process_request_params( - request_params, accepted_extensions - ) - except NegotiationError: - continue - - # Add matching extension to the final list. - extension_headers.append((name, response_params)) - accepted_extensions.append(extension) - - # Break out of the loop once we have a match. - break - - # If we didn't break from the loop, no extension in our list - # matched what the client sent. The extension is declined. - - # Serialize extension header. - if extension_headers: - response_header_value = build_extension(extension_headers) - - return response_header_value, accepted_extensions - - def process_subprotocol(self, headers: Headers) -> Subprotocol | None: - """ - Handle the Sec-WebSocket-Protocol HTTP request header. - - Args: - headers: WebSocket handshake request headers. - - Returns: - Subprotocol, if one was selected; this is also the value of the - ``Sec-WebSocket-Protocol`` response header. - - Raises: - InvalidHandshake: If the Sec-WebSocket-Subprotocol header is invalid. - - """ - subprotocols: Sequence[Subprotocol] = sum( - [ - parse_subprotocol(header_value) - for header_value in headers.get_all("Sec-WebSocket-Protocol") - ], - [], - ) - return self.select_subprotocol(subprotocols) - - def select_subprotocol( - self, - subprotocols: Sequence[Subprotocol], - ) -> Subprotocol | None: - """ - Pick a subprotocol among those offered by the client. - - If several subprotocols are supported by both the client and the server, - pick the first one in the list declared the server. - - If the server doesn't support any subprotocols, continue without a - subprotocol, regardless of what the client offers. - - If the server supports at least one subprotocol and the client doesn't - offer any, abort the handshake with an HTTP 400 error. - - You provide a ``select_subprotocol`` argument to :class:`ServerProtocol` - to override this logic. For example, you could accept the connection - even if client doesn't offer a subprotocol, rather than reject it. - - Here's how to negotiate the ``chat`` subprotocol if the client supports - it and continue without a subprotocol otherwise:: - - def select_subprotocol(protocol, subprotocols): - if "chat" in subprotocols: - return "chat" - - Args: - subprotocols: List of subprotocols offered by the client. - - Returns: - Selected subprotocol, if a common subprotocol was found. - - :obj:`None` to continue without a subprotocol. - - Raises: - NegotiationError: Custom implementations may raise this exception - to abort the handshake with an HTTP 400 error. - - """ - # Server doesn't offer any subprotocols. - if not self.available_subprotocols: # None or empty list - return None - - # Server offers at least one subprotocol but client doesn't offer any. - if not subprotocols: - raise NegotiationError("missing subprotocol") - - # Server and client both offer subprotocols. Look for a shared one. - proposed_subprotocols = set(subprotocols) - for subprotocol in self.available_subprotocols: - if subprotocol in proposed_subprotocols: - return subprotocol - - # No common subprotocol was found. - raise NegotiationError( - "invalid subprotocol; expected one of " - + ", ".join(self.available_subprotocols) - ) - - def reject(self, status: StatusLike, text: str) -> Response: - """ - Create a handshake response to reject the connection. - - A short plain text response is the best fallback when failing to - establish a WebSocket connection. - - You must send the handshake response with :meth:`send_response`. - - You may modify the response before sending it, for example by changing - HTTP headers. - - Args: - status: HTTP status code. - text: HTTP response body; it will be encoded to UTF-8. - - Returns: - HTTP response to send to the client. - - """ - # If status is an int instead of an HTTPStatus, fix it automatically. - status = http.HTTPStatus(status) - body = text.encode() - headers = Headers( - [ - ("Date", email.utils.formatdate(usegmt=True)), - ("Connection", "close"), - ("Content-Length", str(len(body))), - ("Content-Type", "text/plain; charset=utf-8"), - ] - ) - return Response(status.value, status.phrase, headers, body) - - def send_response(self, response: Response) -> None: - """ - Send a handshake response to the client. - - Args: - response: WebSocket handshake response event to send. - - """ - if self.debug: - code, phrase = response.status_code, response.reason_phrase - self.logger.debug("> HTTP/1.1 %d %s", code, phrase) - for key, value in response.headers.raw_items(): - self.logger.debug("> %s: %s", key, value) - if response.body: - self.logger.debug("> [body] (%d bytes)", len(response.body)) - - self.writes.append(response.serialize()) - - if response.status_code == 101: - assert self.state is CONNECTING - self.state = OPEN - self.logger.info("connection open") - - else: - self.logger.info( - "connection rejected (%d %s)", - response.status_code, - response.reason_phrase, - ) - - self.send_eof() - self.parser = self.discard() - next(self.parser) # start coroutine - - def parse(self) -> Generator[None]: - if self.state is CONNECTING: - try: - request = yield from Request.parse( - self.reader.read_line, - ) - except Exception as exc: - self.handshake_exc = InvalidMessage( - "did not receive a valid HTTP request" - ) - self.handshake_exc.__cause__ = exc - self.send_eof() - self.parser = self.discard() - next(self.parser) # start coroutine - yield - - if self.debug: - self.logger.debug("< GET %s HTTP/1.1", request.path) - for key, value in request.headers.raw_items(): - self.logger.debug("< %s: %s", key, value) - - self.events.append(request) - - yield from super().parse() - - -class ServerConnection(ServerProtocol): - def __init__(self, *args: Any, **kwargs: Any) -> None: - warnings.warn( # deprecated in 11.0 - 2023-04-02 - "ServerConnection was renamed to ServerProtocol", - DeprecationWarning, - ) - super().__init__(*args, **kwargs) - - -lazy_import( - globals(), - deprecated_aliases={ - # deprecated in 14.0 - 2024-11-09 - "WebSocketServer": ".legacy.server", - "WebSocketServerProtocol": ".legacy.server", - "broadcast": ".legacy.server", - "serve": ".legacy.server", - "unix_serve": ".legacy.server", - }, -) diff --git a/wip/SNIP/client/websockets/speedups.c b/wip/SNIP/client/websockets/speedups.c deleted file mode 100644 index cb10ded..0000000 --- a/wip/SNIP/client/websockets/speedups.c +++ /dev/null @@ -1,222 +0,0 @@ -/* C implementation of performance sensitive functions. */ - -#define PY_SSIZE_T_CLEAN -#include -#include /* uint8_t, uint32_t, uint64_t */ - -#if __ARM_NEON -#include -#elif __SSE2__ -#include -#endif - -static const Py_ssize_t MASK_LEN = 4; - -/* Similar to PyBytes_AsStringAndSize, but accepts more types */ - -static int -_PyBytesLike_AsStringAndSize(PyObject *obj, PyObject **tmp, char **buffer, Py_ssize_t *length) -{ - // This supports bytes, bytearrays, and memoryview objects, - // which are common data structures for handling byte streams. - // If *tmp isn't NULL, the caller gets a new reference. - if (PyBytes_Check(obj)) - { - *tmp = NULL; - *buffer = PyBytes_AS_STRING(obj); - *length = PyBytes_GET_SIZE(obj); - } - else if (PyByteArray_Check(obj)) - { - *tmp = NULL; - *buffer = PyByteArray_AS_STRING(obj); - *length = PyByteArray_GET_SIZE(obj); - } - else if (PyMemoryView_Check(obj)) - { - *tmp = PyMemoryView_GetContiguous(obj, PyBUF_READ, 'C'); - if (*tmp == NULL) - { - return -1; - } - Py_buffer *mv_buf; - mv_buf = PyMemoryView_GET_BUFFER(*tmp); - *buffer = mv_buf->buf; - *length = mv_buf->len; - } - else - { - PyErr_Format( - PyExc_TypeError, - "expected a bytes-like object, %.200s found", - Py_TYPE(obj)->tp_name); - return -1; - } - - return 0; -} - -/* C implementation of websockets.utils.apply_mask */ - -static PyObject * -apply_mask(PyObject *self, PyObject *args, PyObject *kwds) -{ - - // In order to support various bytes-like types, accept any Python object. - - static char *kwlist[] = {"data", "mask", NULL}; - PyObject *input_obj; - PyObject *mask_obj; - - // A pointer to a char * + length will be extracted from the data and mask - // arguments, possibly via a Py_buffer. - - PyObject *input_tmp = NULL; - char *input; - Py_ssize_t input_len; - PyObject *mask_tmp = NULL; - char *mask; - Py_ssize_t mask_len; - - // Initialize a PyBytesObject then get a pointer to the underlying char * - // in order to avoid an extra memory copy in PyBytes_FromStringAndSize. - - PyObject *result = NULL; - char *output; - - // Other variables. - - Py_ssize_t i = 0; - - // Parse inputs. - - if (!PyArg_ParseTupleAndKeywords( - args, kwds, "OO", kwlist, &input_obj, &mask_obj)) - { - goto exit; - } - - if (_PyBytesLike_AsStringAndSize(input_obj, &input_tmp, &input, &input_len) == -1) - { - goto exit; - } - - if (_PyBytesLike_AsStringAndSize(mask_obj, &mask_tmp, &mask, &mask_len) == -1) - { - goto exit; - } - - if (mask_len != MASK_LEN) - { - PyErr_SetString(PyExc_ValueError, "mask must contain 4 bytes"); - goto exit; - } - - // Create output. - - result = PyBytes_FromStringAndSize(NULL, input_len); - if (result == NULL) - { - goto exit; - } - - // Since we just created result, we don't need error checks. - output = PyBytes_AS_STRING(result); - - // Perform the masking operation. - - // Apparently GCC cannot figure out the following optimizations by itself. - - // We need a new scope for MSVC 2010 (non C99 friendly) - { -#if __ARM_NEON - - // With NEON support, XOR by blocks of 16 bytes = 128 bits. - - Py_ssize_t input_len_128 = input_len & ~15; - uint8x16_t mask_128 = vreinterpretq_u8_u32(vdupq_n_u32(*(uint32_t *)mask)); - - for (; i < input_len_128; i += 16) - { - uint8x16_t in_128 = vld1q_u8((uint8_t *)(input + i)); - uint8x16_t out_128 = veorq_u8(in_128, mask_128); - vst1q_u8((uint8_t *)(output + i), out_128); - } - -#elif __SSE2__ - - // With SSE2 support, XOR by blocks of 16 bytes = 128 bits. - - // Since we cannot control the 16-bytes alignment of input and output - // buffers, we rely on loadu/storeu rather than load/store. - - Py_ssize_t input_len_128 = input_len & ~15; - __m128i mask_128 = _mm_set1_epi32(*(uint32_t *)mask); - - for (; i < input_len_128; i += 16) - { - __m128i in_128 = _mm_loadu_si128((__m128i *)(input + i)); - __m128i out_128 = _mm_xor_si128(in_128, mask_128); - _mm_storeu_si128((__m128i *)(output + i), out_128); - } - -#else - - // Without SSE2 support, XOR by blocks of 8 bytes = 64 bits. - - // We assume the memory allocator aligns everything on 8 bytes boundaries. - - Py_ssize_t input_len_64 = input_len & ~7; - uint32_t mask_32 = *(uint32_t *)mask; - uint64_t mask_64 = ((uint64_t)mask_32 << 32) | (uint64_t)mask_32; - - for (; i < input_len_64; i += 8) - { - *(uint64_t *)(output + i) = *(uint64_t *)(input + i) ^ mask_64; - } - -#endif - } - - // XOR the remainder of the input byte by byte. - - for (; i < input_len; i++) - { - output[i] = input[i] ^ mask[i & (MASK_LEN - 1)]; - } - -exit: - Py_XDECREF(input_tmp); - Py_XDECREF(mask_tmp); - return result; - -} - -static PyMethodDef speedups_methods[] = { - { - "apply_mask", - (PyCFunction)apply_mask, - METH_VARARGS | METH_KEYWORDS, - "Apply masking to the data of a WebSocket message.", - }, - {NULL, NULL, 0, NULL}, /* Sentinel */ -}; - -static struct PyModuleDef speedups_module = { - PyModuleDef_HEAD_INIT, - "websocket.speedups", /* m_name */ - "C implementation of performance sensitive functions.", - /* m_doc */ - -1, /* m_size */ - speedups_methods, /* m_methods */ - NULL, - NULL, - NULL, - NULL -}; - -PyMODINIT_FUNC -PyInit_speedups(void) -{ - return PyModule_Create(&speedups_module); -} diff --git a/wip/SNIP/client/websockets/speedups.pyi b/wip/SNIP/client/websockets/speedups.pyi deleted file mode 100644 index 821438a..0000000 --- a/wip/SNIP/client/websockets/speedups.pyi +++ /dev/null @@ -1 +0,0 @@ -def apply_mask(data: bytes, mask: bytes) -> bytes: ... diff --git a/wip/SNIP/client/websockets/streams.py b/wip/SNIP/client/websockets/streams.py deleted file mode 100644 index f52e619..0000000 --- a/wip/SNIP/client/websockets/streams.py +++ /dev/null @@ -1,151 +0,0 @@ -from __future__ import annotations - -from collections.abc import Generator - - -class StreamReader: - """ - Generator-based stream reader. - - This class doesn't support concurrent calls to :meth:`read_line`, - :meth:`read_exact`, or :meth:`read_to_eof`. Make sure calls are - serialized. - - """ - - def __init__(self) -> None: - self.buffer = bytearray() - self.eof = False - - def read_line(self, m: int) -> Generator[None, None, bytes]: - """ - Read a LF-terminated line from the stream. - - This is a generator-based coroutine. - - The return value includes the LF character. - - Args: - m: Maximum number bytes to read; this is a security limit. - - Raises: - EOFError: If the stream ends without a LF. - RuntimeError: If the stream ends in more than ``m`` bytes. - - """ - n = 0 # number of bytes to read - p = 0 # number of bytes without a newline - while True: - n = self.buffer.find(b"\n", p) + 1 - if n > 0: - break - p = len(self.buffer) - if p > m: - raise RuntimeError(f"read {p} bytes, expected no more than {m} bytes") - if self.eof: - raise EOFError(f"stream ends after {p} bytes, before end of line") - yield - if n > m: - raise RuntimeError(f"read {n} bytes, expected no more than {m} bytes") - r = self.buffer[:n] - del self.buffer[:n] - return r - - def read_exact(self, n: int) -> Generator[None, None, bytes]: - """ - Read a given number of bytes from the stream. - - This is a generator-based coroutine. - - Args: - n: How many bytes to read. - - Raises: - EOFError: If the stream ends in less than ``n`` bytes. - - """ - assert n >= 0 - while len(self.buffer) < n: - if self.eof: - p = len(self.buffer) - raise EOFError(f"stream ends after {p} bytes, expected {n} bytes") - yield - r = self.buffer[:n] - del self.buffer[:n] - return r - - def read_to_eof(self, m: int) -> Generator[None, None, bytes]: - """ - Read all bytes from the stream. - - This is a generator-based coroutine. - - Args: - m: Maximum number bytes to read; this is a security limit. - - Raises: - RuntimeError: If the stream ends in more than ``m`` bytes. - - """ - while not self.eof: - p = len(self.buffer) - if p > m: - raise RuntimeError(f"read {p} bytes, expected no more than {m} bytes") - yield - r = self.buffer[:] - del self.buffer[:] - return r - - def at_eof(self) -> Generator[None, None, bool]: - """ - Tell whether the stream has ended and all data was read. - - This is a generator-based coroutine. - - """ - while True: - if self.buffer: - return False - if self.eof: - return True - # When all data was read but the stream hasn't ended, we can't - # tell if until either feed_data() or feed_eof() is called. - yield - - def feed_data(self, data: bytes) -> None: - """ - Write data to the stream. - - :meth:`feed_data` cannot be called after :meth:`feed_eof`. - - Args: - data: Data to write. - - Raises: - EOFError: If the stream has ended. - - """ - if self.eof: - raise EOFError("stream ended") - self.buffer += data - - def feed_eof(self) -> None: - """ - End the stream. - - :meth:`feed_eof` cannot be called more than once. - - Raises: - EOFError: If the stream has ended. - - """ - if self.eof: - raise EOFError("stream ended") - self.eof = True - - def discard(self) -> None: - """ - Discard all buffered data, but don't end the stream. - - """ - del self.buffer[:] diff --git a/wip/SNIP/client/websockets/sync/__init__.py b/wip/SNIP/client/websockets/sync/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/wip/SNIP/client/websockets/sync/client.py b/wip/SNIP/client/websockets/sync/client.py deleted file mode 100644 index 58cb847..0000000 --- a/wip/SNIP/client/websockets/sync/client.py +++ /dev/null @@ -1,648 +0,0 @@ -from __future__ import annotations - -import socket -import ssl as ssl_module -import threading -import warnings -from collections.abc import Sequence -from typing import Any, Callable, Literal, TypeVar, cast - -from ..client import ClientProtocol -from ..datastructures import Headers, HeadersLike -from ..exceptions import InvalidProxyMessage, InvalidProxyStatus, ProxyError -from ..extensions.base import ClientExtensionFactory -from ..extensions.permessage_deflate import enable_client_permessage_deflate -from ..headers import build_authorization_basic, build_host, validate_subprotocols -from ..http11 import USER_AGENT, Response -from ..protocol import CONNECTING, Event -from ..streams import StreamReader -from ..typing import LoggerLike, Origin, Subprotocol -from ..uri import Proxy, WebSocketURI, get_proxy, parse_proxy, parse_uri -from .connection import Connection -from .utils import Deadline - - -__all__ = ["connect", "unix_connect", "ClientConnection"] - - -class ClientConnection(Connection): - """ - :mod:`threading` implementation of a WebSocket client connection. - - :class:`ClientConnection` provides :meth:`recv` and :meth:`send` methods for - receiving and sending messages. - - It supports iteration to receive messages:: - - for message in websocket: - process(message) - - The iterator exits normally when the connection is closed with close code - 1000 (OK) or 1001 (going away) or without a close code. It raises a - :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is - closed with any other code. - - The ``ping_interval``, ``ping_timeout``, ``close_timeout``, and - ``max_queue`` arguments have the same meaning as in :func:`connect`. - - Args: - socket: Socket connected to a WebSocket server. - protocol: Sans-I/O connection. - - """ - - def __init__( - self, - socket: socket.socket, - protocol: ClientProtocol, - *, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = 10, - max_queue: int | None | tuple[int | None, int | None] = 16, - ) -> None: - self.protocol: ClientProtocol - self.response_rcvd = threading.Event() - super().__init__( - socket, - protocol, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - close_timeout=close_timeout, - max_queue=max_queue, - ) - - def handshake( - self, - additional_headers: HeadersLike | None = None, - user_agent_header: str | None = USER_AGENT, - timeout: float | None = None, - ) -> None: - """ - Perform the opening handshake. - - """ - with self.send_context(expected_state=CONNECTING): - self.request = self.protocol.connect() - if additional_headers is not None: - self.request.headers.update(additional_headers) - if user_agent_header is not None: - self.request.headers.setdefault("User-Agent", user_agent_header) - self.protocol.send_request(self.request) - - if not self.response_rcvd.wait(timeout): - raise TimeoutError("timed out while waiting for handshake response") - - # self.protocol.handshake_exc is set when the connection is lost before - # receiving a response, when the response cannot be parsed, or when the - # response fails the handshake. - - if self.protocol.handshake_exc is not None: - raise self.protocol.handshake_exc - - def process_event(self, event: Event) -> None: - """ - Process one incoming event. - - """ - # First event - handshake response. - if self.response is None: - assert isinstance(event, Response) - self.response = event - self.response_rcvd.set() - # Later events - frames. - else: - super().process_event(event) - - def recv_events(self) -> None: - """ - Read incoming data from the socket and process events. - - """ - try: - super().recv_events() - finally: - # If the connection is closed during the handshake, unblock it. - self.response_rcvd.set() - - -def connect( - uri: str, - *, - # TCP/TLS - sock: socket.socket | None = None, - ssl: ssl_module.SSLContext | None = None, - server_hostname: str | None = None, - # WebSocket - origin: Origin | None = None, - extensions: Sequence[ClientExtensionFactory] | None = None, - subprotocols: Sequence[Subprotocol] | None = None, - compression: str | None = "deflate", - # HTTP - additional_headers: HeadersLike | None = None, - user_agent_header: str | None = USER_AGENT, - proxy: str | Literal[True] | None = True, - proxy_ssl: ssl_module.SSLContext | None = None, - proxy_server_hostname: str | None = None, - # Timeouts - open_timeout: float | None = 10, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = 10, - # Limits - max_size: int | None = 2**20, - max_queue: int | None | tuple[int | None, int | None] = 16, - # Logging - logger: LoggerLike | None = None, - # Escape hatch for advanced customization - create_connection: type[ClientConnection] | None = None, - **kwargs: Any, -) -> ClientConnection: - """ - Connect to the WebSocket server at ``uri``. - - This function returns a :class:`ClientConnection` instance, which you can - use to send and receive messages. - - :func:`connect` may be used as a context manager:: - - from websockets.sync.client import connect - - with connect(...) as websocket: - ... - - The connection is closed automatically when exiting the context. - - Args: - uri: URI of the WebSocket server. - sock: Preexisting TCP socket. ``sock`` overrides the host and port - from ``uri``. You may call :func:`socket.create_connection` to - create a suitable TCP socket. - ssl: Configuration for enabling TLS on the connection. - server_hostname: Host name for the TLS handshake. ``server_hostname`` - overrides the host name from ``uri``. - origin: Value of the ``Origin`` header, for servers that require it. - extensions: List of supported extensions, in order in which they - should be negotiated and run. - subprotocols: List of supported subprotocols, in order of decreasing - preference. - compression: The "permessage-deflate" extension is enabled by default. - Set ``compression`` to :obj:`None` to disable it. See the - :doc:`compression guide <../../topics/compression>` for details. - additional_headers (HeadersLike | None): Arbitrary HTTP headers to add - to the handshake request. - user_agent_header: Value of the ``User-Agent`` request header. - It defaults to ``"Python/x.y.z websockets/X.Y"``. - Setting it to :obj:`None` removes the header. - proxy: If a proxy is configured, it is used by default. Set ``proxy`` - to :obj:`None` to disable the proxy or to the address of a proxy - to override the system configuration. See the :doc:`proxy docs - <../../topics/proxies>` for details. - proxy_ssl: Configuration for enabling TLS on the proxy connection. - proxy_server_hostname: Host name for the TLS handshake with the proxy. - ``proxy_server_hostname`` overrides the host name from ``proxy``. - open_timeout: Timeout for opening the connection in seconds. - :obj:`None` disables the timeout. - ping_interval: Interval between keepalive pings in seconds. - :obj:`None` disables keepalive. - ping_timeout: Timeout for keepalive pings in seconds. - :obj:`None` disables timeouts. - close_timeout: Timeout for closing the connection in seconds. - :obj:`None` disables the timeout. - max_size: Maximum size of incoming messages in bytes. - :obj:`None` disables the limit. - max_queue: High-water mark of the buffer where frames are received. - It defaults to 16 frames. The low-water mark defaults to ``max_queue - // 4``. You may pass a ``(high, low)`` tuple to set the high-water - and low-water marks. If you want to disable flow control entirely, - you may set it to ``None``, although that's a bad idea. - logger: Logger for this client. - It defaults to ``logging.getLogger("websockets.client")``. - See the :doc:`logging guide <../../topics/logging>` for details. - create_connection: Factory for the :class:`ClientConnection` managing - the connection. Set it to a wrapper or a subclass to customize - connection handling. - - Any other keyword arguments are passed to :func:`~socket.create_connection`. - - Raises: - InvalidURI: If ``uri`` isn't a valid WebSocket URI. - OSError: If the TCP connection fails. - InvalidHandshake: If the opening handshake fails. - TimeoutError: If the opening handshake times out. - - """ - - # Process parameters - - # Backwards compatibility: ssl used to be called ssl_context. - if ssl is None and "ssl_context" in kwargs: - ssl = kwargs.pop("ssl_context") - warnings.warn( # deprecated in 13.0 - 2024-08-20 - "ssl_context was renamed to ssl", - DeprecationWarning, - ) - - ws_uri = parse_uri(uri) - if not ws_uri.secure and ssl is not None: - raise ValueError("ssl argument is incompatible with a ws:// URI") - - # Private APIs for unix_connect() - unix: bool = kwargs.pop("unix", False) - path: str | None = kwargs.pop("path", None) - - if unix: - if path is None and sock is None: - raise ValueError("missing path argument") - elif path is not None and sock is not None: - raise ValueError("path and sock arguments are incompatible") - - if subprotocols is not None: - validate_subprotocols(subprotocols) - - if compression == "deflate": - extensions = enable_client_permessage_deflate(extensions) - elif compression is not None: - raise ValueError(f"unsupported compression: {compression}") - - if unix: - proxy = None - if sock is not None: - proxy = None - if proxy is True: - proxy = get_proxy(ws_uri) - - # Calculate timeouts on the TCP, TLS, and WebSocket handshakes. - # The TCP and TLS timeouts must be set on the socket, then removed - # to avoid conflicting with the WebSocket timeout in handshake(). - deadline = Deadline(open_timeout) - - if create_connection is None: - create_connection = ClientConnection - - try: - # Connect socket - - if sock is None: - if unix: - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.settimeout(deadline.timeout()) - assert path is not None # mypy cannot figure this out - sock.connect(path) - elif proxy is not None: - proxy_parsed = parse_proxy(proxy) - if proxy_parsed.scheme[:5] == "socks": - # Connect to the server through the proxy. - sock = connect_socks_proxy( - proxy_parsed, - ws_uri, - deadline, - # websockets is consistent with the socket module while - # python_socks is consistent across implementations. - local_addr=kwargs.pop("source_address", None), - ) - elif proxy_parsed.scheme[:4] == "http": - # Validate the proxy_ssl argument. - if proxy_parsed.scheme != "https" and proxy_ssl is not None: - raise ValueError( - "proxy_ssl argument is incompatible with an http:// proxy" - ) - # Connect to the server through the proxy. - sock = connect_http_proxy( - proxy_parsed, - ws_uri, - deadline, - user_agent_header=user_agent_header, - ssl=proxy_ssl, - server_hostname=proxy_server_hostname, - **kwargs, - ) - else: - raise AssertionError("unsupported proxy") - else: - kwargs.setdefault("timeout", deadline.timeout()) - sock = socket.create_connection( - (ws_uri.host, ws_uri.port), - **kwargs, - ) - sock.settimeout(None) - - # Disable Nagle algorithm - - if not unix: - sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True) - - # Initialize TLS wrapper and perform TLS handshake - - if ws_uri.secure: - if ssl is None: - ssl = ssl_module.create_default_context() - if server_hostname is None: - server_hostname = ws_uri.host - sock.settimeout(deadline.timeout()) - if proxy_ssl is None: - sock = ssl.wrap_socket(sock, server_hostname=server_hostname) - else: - sock_2 = SSLSSLSocket(sock, ssl, server_hostname=server_hostname) - # Let's pretend that sock is a socket, even though it isn't. - sock = cast(socket.socket, sock_2) - sock.settimeout(None) - - # Initialize WebSocket protocol - - protocol = ClientProtocol( - ws_uri, - origin=origin, - extensions=extensions, - subprotocols=subprotocols, - max_size=max_size, - logger=logger, - ) - - # Initialize WebSocket connection - - connection = create_connection( - sock, - protocol, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - close_timeout=close_timeout, - max_queue=max_queue, - ) - except Exception: - if sock is not None: - sock.close() - raise - - try: - connection.handshake( - additional_headers, - user_agent_header, - deadline.timeout(), - ) - except Exception: - connection.close_socket() - connection.recv_events_thread.join() - raise - - connection.start_keepalive() - return connection - - -def unix_connect( - path: str | None = None, - uri: str | None = None, - **kwargs: Any, -) -> ClientConnection: - """ - Connect to a WebSocket server listening on a Unix socket. - - This function accepts the same keyword arguments as :func:`connect`. - - It's only available on Unix. - - It's mainly useful for debugging servers listening on Unix sockets. - - Args: - path: File system path to the Unix socket. - uri: URI of the WebSocket server. ``uri`` defaults to - ``ws://localhost/`` or, when a ``ssl`` is provided, to - ``wss://localhost/``. - - """ - if uri is None: - # Backwards compatibility: ssl used to be called ssl_context. - if kwargs.get("ssl") is None and kwargs.get("ssl_context") is None: - uri = "ws://localhost/" - else: - uri = "wss://localhost/" - return connect(uri=uri, unix=True, path=path, **kwargs) - - -try: - from python_socks import ProxyType - from python_socks.sync import Proxy as SocksProxy - - SOCKS_PROXY_TYPES = { - "socks5h": ProxyType.SOCKS5, - "socks5": ProxyType.SOCKS5, - "socks4a": ProxyType.SOCKS4, - "socks4": ProxyType.SOCKS4, - } - - SOCKS_PROXY_RDNS = { - "socks5h": True, - "socks5": False, - "socks4a": True, - "socks4": False, - } - - def connect_socks_proxy( - proxy: Proxy, - ws_uri: WebSocketURI, - deadline: Deadline, - **kwargs: Any, - ) -> socket.socket: - """Connect via a SOCKS proxy and return the socket.""" - socks_proxy = SocksProxy( - SOCKS_PROXY_TYPES[proxy.scheme], - proxy.host, - proxy.port, - proxy.username, - proxy.password, - SOCKS_PROXY_RDNS[proxy.scheme], - ) - kwargs.setdefault("timeout", deadline.timeout()) - # connect() is documented to raise OSError and TimeoutError. - # Wrap other exceptions in ProxyError, a subclass of InvalidHandshake. - try: - return socks_proxy.connect(ws_uri.host, ws_uri.port, **kwargs) - except (OSError, TimeoutError, socket.timeout): - raise - except Exception as exc: - raise ProxyError("failed to connect to SOCKS proxy") from exc - -except ImportError: - - def connect_socks_proxy( - proxy: Proxy, - ws_uri: WebSocketURI, - deadline: Deadline, - **kwargs: Any, - ) -> socket.socket: - raise ImportError("python-socks is required to use a SOCKS proxy") - - -def prepare_connect_request( - proxy: Proxy, - ws_uri: WebSocketURI, - user_agent_header: str | None = None, -) -> bytes: - host = build_host(ws_uri.host, ws_uri.port, ws_uri.secure, always_include_port=True) - headers = Headers() - headers["Host"] = build_host(ws_uri.host, ws_uri.port, ws_uri.secure) - if user_agent_header is not None: - headers["User-Agent"] = user_agent_header - if proxy.username is not None: - assert proxy.password is not None # enforced by parse_proxy() - headers["Proxy-Authorization"] = build_authorization_basic( - proxy.username, proxy.password - ) - # We cannot use the Request class because it supports only GET requests. - return f"CONNECT {host} HTTP/1.1\r\n".encode() + headers.serialize() - - -def read_connect_response(sock: socket.socket, deadline: Deadline) -> Response: - reader = StreamReader() - parser = Response.parse( - reader.read_line, - reader.read_exact, - reader.read_to_eof, - include_body=False, - ) - try: - while True: - sock.settimeout(deadline.timeout()) - data = sock.recv(4096) - if data: - reader.feed_data(data) - else: - reader.feed_eof() - next(parser) - except StopIteration as exc: - assert isinstance(exc.value, Response) # help mypy - response = exc.value - if 200 <= response.status_code < 300: - return response - else: - raise InvalidProxyStatus(response) - except socket.timeout: - raise TimeoutError("timed out while connecting to HTTP proxy") - except Exception as exc: - raise InvalidProxyMessage( - "did not receive a valid HTTP response from proxy" - ) from exc - finally: - sock.settimeout(None) - - -def connect_http_proxy( - proxy: Proxy, - ws_uri: WebSocketURI, - deadline: Deadline, - *, - user_agent_header: str | None = None, - ssl: ssl_module.SSLContext | None = None, - server_hostname: str | None = None, - **kwargs: Any, -) -> socket.socket: - # Connect socket - - kwargs.setdefault("timeout", deadline.timeout()) - sock = socket.create_connection((proxy.host, proxy.port), **kwargs) - - # Initialize TLS wrapper and perform TLS handshake - - if proxy.scheme == "https": - if ssl is None: - ssl = ssl_module.create_default_context() - if server_hostname is None: - server_hostname = proxy.host - sock.settimeout(deadline.timeout()) - sock = ssl.wrap_socket(sock, server_hostname=server_hostname) - sock.settimeout(None) - - # Send CONNECT request to the proxy and read response. - - sock.sendall(prepare_connect_request(proxy, ws_uri, user_agent_header)) - try: - read_connect_response(sock, deadline) - except Exception: - sock.close() - raise - - return sock - - -T = TypeVar("T") -F = TypeVar("F", bound=Callable[..., T]) - - -class SSLSSLSocket: - """ - Socket-like object providing TLS-in-TLS. - - Only methods that are used by websockets are implemented. - - """ - - recv_bufsize = 65536 - - def __init__( - self, - sock: socket.socket, - ssl_context: ssl_module.SSLContext, - server_hostname: str | None = None, - ) -> None: - self.incoming = ssl_module.MemoryBIO() - self.outgoing = ssl_module.MemoryBIO() - self.ssl_socket = sock - self.ssl_object = ssl_context.wrap_bio( - self.incoming, - self.outgoing, - server_hostname=server_hostname, - ) - self.run_io(self.ssl_object.do_handshake) - - def run_io(self, func: Callable[..., T], *args: Any) -> T: - while True: - want_read = False - want_write = False - try: - result = func(*args) - except ssl_module.SSLWantReadError: - want_read = True - except ssl_module.SSLWantWriteError: # pragma: no cover - want_write = True - - # Write outgoing data in all cases. - data = self.outgoing.read() - if data: - self.ssl_socket.sendall(data) - - # Read incoming data and retry on SSLWantReadError. - if want_read: - data = self.ssl_socket.recv(self.recv_bufsize) - if data: - self.incoming.write(data) - else: - self.incoming.write_eof() - continue - # Retry after writing outgoing data on SSLWantWriteError. - if want_write: # pragma: no cover - continue - # Return result if no error happened. - return result - - def recv(self, buflen: int) -> bytes: - try: - return self.run_io(self.ssl_object.read, buflen) - except ssl_module.SSLEOFError: - return b"" # always ignore ragged EOFs - - def send(self, data: bytes) -> int: - return self.run_io(self.ssl_object.write, data) - - def sendall(self, data: bytes) -> None: - # adapted from ssl_module.SSLSocket.sendall() - count = 0 - with memoryview(data) as view, view.cast("B") as byte_view: - amount = len(byte_view) - while count < amount: - count += self.send(byte_view[count:]) - - # recv_into(), recvfrom(), recvfrom_into(), sendto(), unwrap(), and the - # flags argument aren't implemented because websockets doesn't need them. - - def __getattr__(self, name: str) -> Any: - return getattr(self.ssl_socket, name) diff --git a/wip/SNIP/client/websockets/sync/connection.py b/wip/SNIP/client/websockets/sync/connection.py deleted file mode 100644 index 8b9e062..0000000 --- a/wip/SNIP/client/websockets/sync/connection.py +++ /dev/null @@ -1,1072 +0,0 @@ -from __future__ import annotations - -import contextlib -import logging -import random -import socket -import struct -import threading -import time -import uuid -from collections.abc import Iterable, Iterator, Mapping -from types import TracebackType -from typing import Any, Literal, overload - -from ..exceptions import ( - ConcurrencyError, - ConnectionClosed, - ConnectionClosedOK, - ProtocolError, -) -from ..frames import DATA_OPCODES, BytesLike, CloseCode, Frame, Opcode -from ..http11 import Request, Response -from ..protocol import CLOSED, OPEN, Event, Protocol, State -from ..typing import Data, LoggerLike, Subprotocol -from .messages import Assembler -from .utils import Deadline - - -__all__ = ["Connection"] - - -class Connection: - """ - :mod:`threading` implementation of a WebSocket connection. - - :class:`Connection` provides APIs shared between WebSocket servers and - clients. - - You shouldn't use it directly. Instead, use - :class:`~websockets.sync.client.ClientConnection` or - :class:`~websockets.sync.server.ServerConnection`. - - """ - - recv_bufsize = 65536 - - def __init__( - self, - socket: socket.socket, - protocol: Protocol, - *, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = 10, - max_queue: int | None | tuple[int | None, int | None] = 16, - ) -> None: - self.socket = socket - self.protocol = protocol - self.ping_interval = ping_interval - self.ping_timeout = ping_timeout - self.close_timeout = close_timeout - if isinstance(max_queue, int) or max_queue is None: - max_queue = (max_queue, None) - self.max_queue = max_queue - - # Inject reference to this instance in the protocol's logger. - self.protocol.logger = logging.LoggerAdapter( - self.protocol.logger, - {"websocket": self}, - ) - - # Copy attributes from the protocol for convenience. - self.id: uuid.UUID = self.protocol.id - """Unique identifier of the connection. Useful in logs.""" - self.logger: LoggerLike = self.protocol.logger - """Logger for this connection.""" - self.debug = self.protocol.debug - - # HTTP handshake request and response. - self.request: Request | None = None - """Opening handshake request.""" - self.response: Response | None = None - """Opening handshake response.""" - - # Mutex serializing interactions with the protocol. - self.protocol_mutex = threading.Lock() - - # Lock stopping reads when the assembler buffer is full. - self.recv_flow_control = threading.Lock() - - # Assembler turning frames into messages and serializing reads. - self.recv_messages = Assembler( - *self.max_queue, - pause=self.recv_flow_control.acquire, - resume=self.recv_flow_control.release, - ) - - # Deadline for the closing handshake. - self.close_deadline: Deadline | None = None - - # Whether we are busy sending a fragmented message. - self.send_in_progress = False - - # Mapping of ping IDs to pong waiters, in chronological order. - self.pong_waiters: dict[bytes, tuple[threading.Event, float, bool]] = {} - - self.latency: float = 0 - """ - Latency of the connection, in seconds. - - Latency is defined as the round-trip time of the connection. It is - measured by sending a Ping frame and waiting for a matching Pong frame. - Before the first measurement, :attr:`latency` is ``0``. - - By default, websockets enables a :ref:`keepalive ` mechanism - that sends Ping frames automatically at regular intervals. You can also - send Ping frames and measure latency with :meth:`ping`. - """ - - # Thread that sends keepalive pings. None when ping_interval is None. - self.keepalive_thread: threading.Thread | None = None - - # Exception raised in recv_events, to be chained to ConnectionClosed - # in the user thread in order to show why the TCP connection dropped. - self.recv_exc: BaseException | None = None - - # Receiving events from the socket. This thread is marked as daemon to - # allow creating a connection in a non-daemon thread and using it in a - # daemon thread. This mustn't prevent the interpreter from exiting. - self.recv_events_thread = threading.Thread( - target=self.recv_events, - daemon=True, - ) - - # Start recv_events only after all attributes are initialized. - self.recv_events_thread.start() - - # Public attributes - - @property - def local_address(self) -> Any: - """ - Local address of the connection. - - For IPv4 connections, this is a ``(host, port)`` tuple. - - The format of the address depends on the address family. - See :meth:`~socket.socket.getsockname`. - - """ - return self.socket.getsockname() - - @property - def remote_address(self) -> Any: - """ - Remote address of the connection. - - For IPv4 connections, this is a ``(host, port)`` tuple. - - The format of the address depends on the address family. - See :meth:`~socket.socket.getpeername`. - - """ - return self.socket.getpeername() - - @property - def state(self) -> State: - """ - State of the WebSocket connection, defined in :rfc:`6455`. - - This attribute is provided for completeness. Typical applications - shouldn't check its value. Instead, they should call :meth:`~recv` or - :meth:`send` and handle :exc:`~websockets.exceptions.ConnectionClosed` - exceptions. - - """ - return self.protocol.state - - @property - def subprotocol(self) -> Subprotocol | None: - """ - Subprotocol negotiated during the opening handshake. - - :obj:`None` if no subprotocol was negotiated. - - """ - return self.protocol.subprotocol - - @property - def close_code(self) -> int | None: - """ - State of the WebSocket connection, defined in :rfc:`6455`. - - This attribute is provided for completeness. Typical applications - shouldn't check its value. Instead, they should inspect attributes - of :exc:`~websockets.exceptions.ConnectionClosed` exceptions. - - """ - return self.protocol.close_code - - @property - def close_reason(self) -> str | None: - """ - State of the WebSocket connection, defined in :rfc:`6455`. - - This attribute is provided for completeness. Typical applications - shouldn't check its value. Instead, they should inspect attributes - of :exc:`~websockets.exceptions.ConnectionClosed` exceptions. - - """ - return self.protocol.close_reason - - # Public methods - - def __enter__(self) -> Connection: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - if exc_type is None: - self.close() - else: - self.close(CloseCode.INTERNAL_ERROR) - - def __iter__(self) -> Iterator[Data]: - """ - Iterate on incoming messages. - - The iterator calls :meth:`recv` and yields messages in an infinite loop. - - It exits when the connection is closed normally. It raises a - :exc:`~websockets.exceptions.ConnectionClosedError` exception after a - protocol error or a network failure. - - """ - try: - while True: - yield self.recv() - except ConnectionClosedOK: - return - - # This overload structure is required to avoid the error: - # "parameter without a default follows parameter with a default" - - @overload - def recv(self, timeout: float | None, decode: Literal[True]) -> str: ... - - @overload - def recv(self, timeout: float | None, decode: Literal[False]) -> bytes: ... - - @overload - def recv(self, timeout: float | None = None, *, decode: Literal[True]) -> str: ... - - @overload - def recv( - self, timeout: float | None = None, *, decode: Literal[False] - ) -> bytes: ... - - @overload - def recv( - self, timeout: float | None = None, decode: bool | None = None - ) -> Data: ... - - def recv(self, timeout: float | None = None, decode: bool | None = None) -> Data: - """ - Receive the next message. - - When the connection is closed, :meth:`recv` raises - :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it raises - :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal closure - and :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol - error or a network failure. This is how you detect the end of the - message stream. - - If ``timeout`` is :obj:`None`, block until a message is received. If - ``timeout`` is set, wait up to ``timeout`` seconds for a message to be - received and return it, else raise :exc:`TimeoutError`. If ``timeout`` - is ``0`` or negative, check if a message has been received already and - return it, else raise :exc:`TimeoutError`. - - If the message is fragmented, wait until all fragments are received, - reassemble them, and return the whole message. - - Args: - timeout: Timeout for receiving a message in seconds. - decode: Set this flag to override the default behavior of returning - :class:`str` or :class:`bytes`. See below for details. - - Returns: - A string (:class:`str`) for a Text_ frame or a bytestring - (:class:`bytes`) for a Binary_ frame. - - .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - .. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - You may override this behavior with the ``decode`` argument: - - * Set ``decode=False`` to disable UTF-8 decoding of Text_ frames and - return a bytestring (:class:`bytes`). This improves performance - when decoding isn't needed, for example if the message contains - JSON and you're using a JSON library that expects a bytestring. - * Set ``decode=True`` to force UTF-8 decoding of Binary_ frames - and return a string (:class:`str`). This may be useful for - servers that send binary frames instead of text frames. - - Raises: - ConnectionClosed: When the connection is closed. - ConcurrencyError: If two threads call :meth:`recv` or - :meth:`recv_streaming` concurrently. - - """ - try: - return self.recv_messages.get(timeout, decode) - except EOFError: - pass - # fallthrough - except ConcurrencyError: - raise ConcurrencyError( - "cannot call recv while another thread " - "is already running recv or recv_streaming" - ) from None - except UnicodeDecodeError as exc: - with self.send_context(): - self.protocol.fail( - CloseCode.INVALID_DATA, - f"{exc.reason} at position {exc.start}", - ) - # fallthrough - - # Wait for the protocol state to be CLOSED before accessing close_exc. - self.recv_events_thread.join() - raise self.protocol.close_exc from self.recv_exc - - @overload - def recv_streaming(self, decode: Literal[True]) -> Iterator[str]: ... - - @overload - def recv_streaming(self, decode: Literal[False]) -> Iterator[bytes]: ... - - @overload - def recv_streaming(self, decode: bool | None = None) -> Iterator[Data]: ... - - def recv_streaming(self, decode: bool | None = None) -> Iterator[Data]: - """ - Receive the next message frame by frame. - - This method is designed for receiving fragmented messages. It returns an - iterator that yields each fragment as it is received. This iterator must - be fully consumed. Else, future calls to :meth:`recv` or - :meth:`recv_streaming` will raise - :exc:`~websockets.exceptions.ConcurrencyError`, making the connection - unusable. - - :meth:`recv_streaming` raises the same exceptions as :meth:`recv`. - - Args: - decode: Set this flag to override the default behavior of returning - :class:`str` or :class:`bytes`. See below for details. - - Returns: - An iterator of strings (:class:`str`) for a Text_ frame or - bytestrings (:class:`bytes`) for a Binary_ frame. - - .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - .. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - You may override this behavior with the ``decode`` argument: - - * Set ``decode=False`` to disable UTF-8 decoding of Text_ frames - and return bytestrings (:class:`bytes`). This may be useful to - optimize performance when decoding isn't needed. - * Set ``decode=True`` to force UTF-8 decoding of Binary_ frames - and return strings (:class:`str`). This is useful for servers - that send binary frames instead of text frames. - - Raises: - ConnectionClosed: When the connection is closed. - ConcurrencyError: If two threads call :meth:`recv` or - :meth:`recv_streaming` concurrently. - - """ - try: - yield from self.recv_messages.get_iter(decode) - return - except EOFError: - pass - # fallthrough - except ConcurrencyError: - raise ConcurrencyError( - "cannot call recv_streaming while another thread " - "is already running recv or recv_streaming" - ) from None - except UnicodeDecodeError as exc: - with self.send_context(): - self.protocol.fail( - CloseCode.INVALID_DATA, - f"{exc.reason} at position {exc.start}", - ) - # fallthrough - - # Wait for the protocol state to be CLOSED before accessing close_exc. - self.recv_events_thread.join() - raise self.protocol.close_exc from self.recv_exc - - def send( - self, - message: Data | Iterable[Data], - text: bool | None = None, - ) -> None: - """ - Send a message. - - A string (:class:`str`) is sent as a Text_ frame. A bytestring or - bytes-like object (:class:`bytes`, :class:`bytearray`, or - :class:`memoryview`) is sent as a Binary_ frame. - - .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - .. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - You may override this behavior with the ``text`` argument: - - * Set ``text=True`` to send a bytestring or bytes-like object - (:class:`bytes`, :class:`bytearray`, or :class:`memoryview`) as a - Text_ frame. This improves performance when the message is already - UTF-8 encoded, for example if the message contains JSON and you're - using a JSON library that produces a bytestring. - * Set ``text=False`` to send a string (:class:`str`) in a Binary_ - frame. This may be useful for servers that expect binary frames - instead of text frames. - - :meth:`send` also accepts an iterable of strings, bytestrings, or - bytes-like objects to enable fragmentation_. Each item is treated as a - message fragment and sent in its own frame. All items must be of the - same type, or else :meth:`send` will raise a :exc:`TypeError` and the - connection will be closed. - - .. _fragmentation: https://datatracker.ietf.org/doc/html/rfc6455#section-5.4 - - :meth:`send` rejects dict-like objects because this is often an error. - (If you really want to send the keys of a dict-like object as fragments, - call its :meth:`~dict.keys` method and pass the result to :meth:`send`.) - - When the connection is closed, :meth:`send` raises - :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it - raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal - connection closure and - :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol - error or a network failure. - - Args: - message: Message to send. - - Raises: - ConnectionClosed: When the connection is closed. - ConcurrencyError: If the connection is sending a fragmented message. - TypeError: If ``message`` doesn't have a supported type. - - """ - # Unfragmented message -- this case must be handled first because - # strings and bytes-like objects are iterable. - - if isinstance(message, str): - with self.send_context(): - if self.send_in_progress: - raise ConcurrencyError( - "cannot call send while another thread is already running send" - ) - if text is False: - self.protocol.send_binary(message.encode()) - else: - self.protocol.send_text(message.encode()) - - elif isinstance(message, BytesLike): - with self.send_context(): - if self.send_in_progress: - raise ConcurrencyError( - "cannot call send while another thread is already running send" - ) - if text is True: - self.protocol.send_text(message) - else: - self.protocol.send_binary(message) - - # Catch a common mistake -- passing a dict to send(). - - elif isinstance(message, Mapping): - raise TypeError("data is a dict-like object") - - # Fragmented message -- regular iterator. - - elif isinstance(message, Iterable): - chunks = iter(message) - try: - chunk = next(chunks) - except StopIteration: - return - - try: - # First fragment. - if isinstance(chunk, str): - with self.send_context(): - if self.send_in_progress: - raise ConcurrencyError( - "cannot call send while another thread " - "is already running send" - ) - self.send_in_progress = True - if text is False: - self.protocol.send_binary(chunk.encode(), fin=False) - else: - self.protocol.send_text(chunk.encode(), fin=False) - encode = True - elif isinstance(chunk, BytesLike): - with self.send_context(): - if self.send_in_progress: - raise ConcurrencyError( - "cannot call send while another thread " - "is already running send" - ) - self.send_in_progress = True - if text is True: - self.protocol.send_text(chunk, fin=False) - else: - self.protocol.send_binary(chunk, fin=False) - encode = False - else: - raise TypeError("data iterable must contain bytes or str") - - # Other fragments - for chunk in chunks: - if isinstance(chunk, str) and encode: - with self.send_context(): - assert self.send_in_progress - self.protocol.send_continuation(chunk.encode(), fin=False) - elif isinstance(chunk, BytesLike) and not encode: - with self.send_context(): - assert self.send_in_progress - self.protocol.send_continuation(chunk, fin=False) - else: - raise TypeError("data iterable must contain uniform types") - - # Final fragment. - with self.send_context(): - self.protocol.send_continuation(b"", fin=True) - self.send_in_progress = False - - except ConcurrencyError: - # We didn't start sending a fragmented message. - # The connection is still usable. - raise - - except Exception: - # We're half-way through a fragmented message and we can't - # complete it. This makes the connection unusable. - with self.send_context(): - self.protocol.fail( - CloseCode.INTERNAL_ERROR, - "error in fragmented message", - ) - raise - - else: - raise TypeError("data must be str, bytes, or iterable") - - def close(self, code: int = CloseCode.NORMAL_CLOSURE, reason: str = "") -> None: - """ - Perform the closing handshake. - - :meth:`close` waits for the other end to complete the handshake, for the - TCP connection to terminate, and for all incoming messages to be read - with :meth:`recv`. - - :meth:`close` is idempotent: it doesn't do anything once the - connection is closed. - - Args: - code: WebSocket close code. - reason: WebSocket close reason. - - """ - try: - # The context manager takes care of waiting for the TCP connection - # to terminate after calling a method that sends a close frame. - with self.send_context(): - if self.send_in_progress: - self.protocol.fail( - CloseCode.INTERNAL_ERROR, - "close during fragmented message", - ) - else: - self.protocol.send_close(code, reason) - except ConnectionClosed: - # Ignore ConnectionClosed exceptions raised from send_context(). - # They mean that the connection is closed, which was the goal. - pass - - def ping( - self, - data: Data | None = None, - ack_on_close: bool = False, - ) -> threading.Event: - """ - Send a Ping_. - - .. _Ping: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2 - - A ping may serve as a keepalive or as a check that the remote endpoint - received all messages up to this point - - Args: - data: Payload of the ping. A :class:`str` will be encoded to UTF-8. - If ``data`` is :obj:`None`, the payload is four random bytes. - ack_on_close: when this option is :obj:`True`, the event will also - be set when the connection is closed. While this avoids getting - stuck waiting for a pong that will never arrive, it requires - checking that the state of the connection is still ``OPEN`` to - confirm that a pong was received, rather than the connection - being closed. - - Returns: - An event that will be set when the corresponding pong is received. - You can ignore it if you don't intend to wait. - - :: - - pong_event = ws.ping() - pong_event.wait() # only if you want to wait for the pong - - Raises: - ConnectionClosed: When the connection is closed. - ConcurrencyError: If another ping was sent with the same data and - the corresponding pong wasn't received yet. - - """ - if isinstance(data, BytesLike): - data = bytes(data) - elif isinstance(data, str): - data = data.encode() - elif data is not None: - raise TypeError("data must be str or bytes-like") - - with self.send_context(): - # Protect against duplicates if a payload is explicitly set. - if data in self.pong_waiters: - raise ConcurrencyError("already waiting for a pong with the same data") - - # Generate a unique random payload otherwise. - while data is None or data in self.pong_waiters: - data = struct.pack("!I", random.getrandbits(32)) - - pong_waiter = threading.Event() - self.pong_waiters[data] = (pong_waiter, time.monotonic(), ack_on_close) - self.protocol.send_ping(data) - return pong_waiter - - def pong(self, data: Data = b"") -> None: - """ - Send a Pong_. - - .. _Pong: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.3 - - An unsolicited pong may serve as a unidirectional heartbeat. - - Args: - data: Payload of the pong. A :class:`str` will be encoded to UTF-8. - - Raises: - ConnectionClosed: When the connection is closed. - - """ - if isinstance(data, BytesLike): - data = bytes(data) - elif isinstance(data, str): - data = data.encode() - else: - raise TypeError("data must be str or bytes-like") - - with self.send_context(): - self.protocol.send_pong(data) - - # Private methods - - def process_event(self, event: Event) -> None: - """ - Process one incoming event. - - This method is overridden in subclasses to handle the handshake. - - """ - assert isinstance(event, Frame) - if event.opcode in DATA_OPCODES: - self.recv_messages.put(event) - - if event.opcode is Opcode.PONG: - self.acknowledge_pings(bytes(event.data)) - - def acknowledge_pings(self, data: bytes) -> None: - """ - Acknowledge pings when receiving a pong. - - """ - with self.protocol_mutex: - # Ignore unsolicited pong. - if data not in self.pong_waiters: - return - - pong_timestamp = time.monotonic() - - # Sending a pong for only the most recent ping is legal. - # Acknowledge all previous pings too in that case. - ping_id = None - ping_ids = [] - for ping_id, ( - pong_waiter, - ping_timestamp, - _ack_on_close, - ) in self.pong_waiters.items(): - ping_ids.append(ping_id) - pong_waiter.set() - if ping_id == data: - self.latency = pong_timestamp - ping_timestamp - break - else: - raise AssertionError("solicited pong not found in pings") - - # Remove acknowledged pings from self.pong_waiters. - for ping_id in ping_ids: - del self.pong_waiters[ping_id] - - def acknowledge_pending_pings(self) -> None: - """ - Acknowledge pending pings when the connection is closed. - - """ - assert self.protocol.state is CLOSED - - for pong_waiter, _ping_timestamp, ack_on_close in self.pong_waiters.values(): - if ack_on_close: - pong_waiter.set() - - self.pong_waiters.clear() - - def keepalive(self) -> None: - """ - Send a Ping frame and wait for a Pong frame at regular intervals. - - """ - assert self.ping_interval is not None - try: - while True: - # If self.ping_timeout > self.latency > self.ping_interval, - # pings will be sent immediately after receiving pongs. - # The period will be longer than self.ping_interval. - self.recv_events_thread.join(self.ping_interval - self.latency) - if not self.recv_events_thread.is_alive(): - break - - try: - pong_waiter = self.ping(ack_on_close=True) - except ConnectionClosed: - break - if self.debug: - self.logger.debug("% sent keepalive ping") - - if self.ping_timeout is not None: - # - if pong_waiter.wait(self.ping_timeout): - if self.debug: - self.logger.debug("% received keepalive pong") - else: - if self.debug: - self.logger.debug("- timed out waiting for keepalive pong") - with self.send_context(): - self.protocol.fail( - CloseCode.INTERNAL_ERROR, - "keepalive ping timeout", - ) - break - except Exception: - self.logger.error("keepalive ping failed", exc_info=True) - - def start_keepalive(self) -> None: - """ - Run :meth:`keepalive` in a thread, unless keepalive is disabled. - - """ - if self.ping_interval is not None: - # This thread is marked as daemon like self.recv_events_thread. - self.keepalive_thread = threading.Thread( - target=self.keepalive, - daemon=True, - ) - self.keepalive_thread.start() - - def recv_events(self) -> None: - """ - Read incoming data from the socket and process events. - - Run this method in a thread as long as the connection is alive. - - ``recv_events()`` exits immediately when the ``self.socket`` is closed. - - """ - try: - while True: - try: - with self.recv_flow_control: - if self.close_deadline is not None: - self.socket.settimeout(self.close_deadline.timeout()) - data = self.socket.recv(self.recv_bufsize) - except Exception as exc: - if self.debug: - self.logger.debug( - "! error while receiving data", - exc_info=True, - ) - # When the closing handshake is initiated by our side, - # recv() may block until send_context() closes the socket. - # In that case, send_context() already set recv_exc. - # Calling set_recv_exc() avoids overwriting it. - with self.protocol_mutex: - self.set_recv_exc(exc) - break - - if data == b"": - break - - # Acquire the connection lock. - with self.protocol_mutex: - # Feed incoming data to the protocol. - self.protocol.receive_data(data) - - # This isn't expected to raise an exception. - events = self.protocol.events_received() - - # Write outgoing data to the socket. - try: - self.send_data() - except Exception as exc: - if self.debug: - self.logger.debug( - "! error while sending data", - exc_info=True, - ) - # Similarly to the above, avoid overriding an exception - # set by send_context(), in case of a race condition - # i.e. send_context() closes the socket after recv() - # returns above but before send_data() calls send(). - self.set_recv_exc(exc) - break - - if self.protocol.close_expected(): - # If the connection is expected to close soon, set the - # close deadline based on the close timeout. - if self.close_deadline is None: - self.close_deadline = Deadline(self.close_timeout) - - # Unlock conn_mutex before processing events. Else, the - # application can't send messages in response to events. - - # If self.send_data raised an exception, then events are lost. - # Given that automatic responses write small amounts of data, - # this should be uncommon, so we don't handle the edge case. - - for event in events: - # This isn't expected to raise an exception. - self.process_event(event) - - # Breaking out of the while True: ... loop means that we believe - # that the socket doesn't work anymore. - with self.protocol_mutex: - # Feed the end of the data stream to the protocol. - self.protocol.receive_eof() - - # This isn't expected to raise an exception. - events = self.protocol.events_received() - - # There is no error handling because send_data() can only write - # the end of the data stream here and it handles errors itself. - self.send_data() - - # This code path is triggered when receiving an HTTP response - # without a Content-Length header. This is the only case where - # reading until EOF generates an event; all other events have - # a known length. Ignore for coverage measurement because tests - # are in test_client.py rather than test_connection.py. - for event in events: # pragma: no cover - # This isn't expected to raise an exception. - self.process_event(event) - - except Exception as exc: - # This branch should never run. It's a safety net in case of bugs. - self.logger.error("unexpected internal error", exc_info=True) - with self.protocol_mutex: - self.set_recv_exc(exc) - finally: - # This isn't expected to raise an exception. - self.close_socket() - - @contextlib.contextmanager - def send_context( - self, - *, - expected_state: State = OPEN, # CONNECTING during the opening handshake - ) -> Iterator[None]: - """ - Create a context for writing to the connection from user code. - - On entry, :meth:`send_context` acquires the connection lock and checks - that the connection is open; on exit, it writes outgoing data to the - socket:: - - with self.send_context(): - self.protocol.send_text(message.encode()) - - When the connection isn't open on entry, when the connection is expected - to close on exit, or when an unexpected error happens, terminating the - connection, :meth:`send_context` waits until the connection is closed - then raises :exc:`~websockets.exceptions.ConnectionClosed`. - - """ - # Should we wait until the connection is closed? - wait_for_close = False - # Should we close the socket and raise ConnectionClosed? - raise_close_exc = False - # What exception should we chain ConnectionClosed to? - original_exc: BaseException | None = None - - # Acquire the protocol lock. - with self.protocol_mutex: - if self.protocol.state is expected_state: - # Let the caller interact with the protocol. - try: - yield - except (ProtocolError, ConcurrencyError): - # The protocol state wasn't changed. Exit immediately. - raise - except Exception as exc: - self.logger.error("unexpected internal error", exc_info=True) - # This branch should never run. It's a safety net in case of - # bugs. Since we don't know what happened, we will close the - # connection and raise the exception to the caller. - wait_for_close = False - raise_close_exc = True - original_exc = exc - else: - # Check if the connection is expected to close soon. - if self.protocol.close_expected(): - wait_for_close = True - # If the connection is expected to close soon, set the - # close deadline based on the close timeout. - # Since we tested earlier that protocol.state was OPEN - # (or CONNECTING) and we didn't release protocol_mutex, - # it is certain that self.close_deadline is still None. - assert self.close_deadline is None - self.close_deadline = Deadline(self.close_timeout) - # Write outgoing data to the socket. - try: - self.send_data() - except Exception as exc: - if self.debug: - self.logger.debug( - "! error while sending data", - exc_info=True, - ) - # While the only expected exception here is OSError, - # other exceptions would be treated identically. - wait_for_close = False - raise_close_exc = True - original_exc = exc - - else: # self.protocol.state is not expected_state - # Minor layering violation: we assume that the connection - # will be closing soon if it isn't in the expected state. - wait_for_close = True - raise_close_exc = True - - # To avoid a deadlock, release the connection lock by exiting the - # context manager before waiting for recv_events() to terminate. - - # If the connection is expected to close soon and the close timeout - # elapses, close the socket to terminate the connection. - if wait_for_close: - if self.close_deadline is None: - timeout = self.close_timeout - else: - # Thread.join() returns immediately if timeout is negative. - timeout = self.close_deadline.timeout(raise_if_elapsed=False) - self.recv_events_thread.join(timeout) - - if self.recv_events_thread.is_alive(): - # There's no risk to overwrite another error because - # original_exc is never set when wait_for_close is True. - assert original_exc is None - original_exc = TimeoutError("timed out while closing connection") - # Set recv_exc before closing the socket in order to get - # proper exception reporting. - raise_close_exc = True - with self.protocol_mutex: - self.set_recv_exc(original_exc) - - # If an error occurred, close the socket to terminate the connection and - # raise an exception. - if raise_close_exc: - self.close_socket() - # Wait for the protocol state to be CLOSED before accessing close_exc. - self.recv_events_thread.join() - raise self.protocol.close_exc from original_exc - - def send_data(self) -> None: - """ - Send outgoing data. - - This method requires holding protocol_mutex. - - Raises: - OSError: When a socket operations fails. - - """ - assert self.protocol_mutex.locked() - for data in self.protocol.data_to_send(): - if data: - if self.close_deadline is not None: - self.socket.settimeout(self.close_deadline.timeout()) - self.socket.sendall(data) - else: - try: - self.socket.shutdown(socket.SHUT_WR) - except OSError: # socket already closed - pass - - def set_recv_exc(self, exc: BaseException | None) -> None: - """ - Set recv_exc, if not set yet. - - This method requires holding protocol_mutex. - - """ - assert self.protocol_mutex.locked() - if self.recv_exc is None: # pragma: no branch - self.recv_exc = exc - - def close_socket(self) -> None: - """ - Shutdown and close socket. Close message assembler. - - Calling close_socket() guarantees that recv_events() terminates. Indeed, - recv_events() may block only on socket.recv() or on recv_messages.put(). - - """ - # shutdown() is required to interrupt recv() on Linux. - try: - self.socket.shutdown(socket.SHUT_RDWR) - except OSError: - pass # socket is already closed - self.socket.close() - - # Calling protocol.receive_eof() is safe because it's idempotent. - # This guarantees that the protocol state becomes CLOSED. - with self.protocol_mutex: - self.protocol.receive_eof() - assert self.protocol.state is CLOSED - - # Abort recv() with a ConnectionClosed exception. - self.recv_messages.close() - - # Acknowledge pings sent with the ack_on_close option. - self.acknowledge_pending_pings() diff --git a/wip/SNIP/client/websockets/sync/messages.py b/wip/SNIP/client/websockets/sync/messages.py deleted file mode 100644 index c619e78..0000000 --- a/wip/SNIP/client/websockets/sync/messages.py +++ /dev/null @@ -1,345 +0,0 @@ -from __future__ import annotations - -import codecs -import queue -import threading -from typing import Any, Callable, Iterable, Iterator, Literal, overload - -from ..exceptions import ConcurrencyError -from ..frames import OP_BINARY, OP_CONT, OP_TEXT, Frame -from ..typing import Data -from .utils import Deadline - - -__all__ = ["Assembler"] - -UTF8Decoder = codecs.getincrementaldecoder("utf-8") - - -class Assembler: - """ - Assemble messages from frames. - - :class:`Assembler` expects only data frames. The stream of frames must - respect the protocol; if it doesn't, the behavior is undefined. - - Args: - pause: Called when the buffer of frames goes above the high water mark; - should pause reading from the network. - resume: Called when the buffer of frames goes below the low water mark; - should resume reading from the network. - - """ - - def __init__( - self, - high: int | None = None, - low: int | None = None, - pause: Callable[[], Any] = lambda: None, - resume: Callable[[], Any] = lambda: None, - ) -> None: - # Serialize reads and writes -- except for reads via synchronization - # primitives provided by the threading and queue modules. - self.mutex = threading.Lock() - - # Queue of incoming frames. - self.frames: queue.SimpleQueue[Frame | None] = queue.SimpleQueue() - - # We cannot put a hard limit on the size of the queue because a single - # call to Protocol.data_received() could produce thousands of frames, - # which must be buffered. Instead, we pause reading when the buffer goes - # above the high limit and we resume when it goes under the low limit. - if high is not None and low is None: - low = high // 4 - if high is None and low is not None: - high = low * 4 - if high is not None and low is not None: - if low < 0: - raise ValueError("low must be positive or equal to zero") - if high < low: - raise ValueError("high must be greater than or equal to low") - self.high, self.low = high, low - self.pause = pause - self.resume = resume - self.paused = False - - # This flag prevents concurrent calls to get() by user code. - self.get_in_progress = False - - # This flag marks the end of the connection. - self.closed = False - - def get_next_frame(self, timeout: float | None = None) -> Frame: - # Helper to factor out the logic for getting the next frame from the - # queue, while handling timeouts and reaching the end of the stream. - if self.closed: - try: - frame = self.frames.get(block=False) - except queue.Empty: - raise EOFError("stream of frames ended") from None - else: - try: - # Check for a frame that's already received if timeout <= 0. - # SimpleQueue.get() doesn't support negative timeout values. - if timeout is not None and timeout <= 0: - frame = self.frames.get(block=False) - else: - frame = self.frames.get(block=True, timeout=timeout) - except queue.Empty: - raise TimeoutError(f"timed out in {timeout:.1f}s") from None - if frame is None: - raise EOFError("stream of frames ended") - return frame - - def reset_queue(self, frames: Iterable[Frame]) -> None: - # Helper to put frames back into the queue after they were fetched. - # This happens only when the queue is empty. However, by the time - # we acquire self.mutex, put() may have added items in the queue. - # Therefore, we must handle the case where the queue is not empty. - frame: Frame | None - with self.mutex: - queued = [] - try: - while True: - queued.append(self.frames.get(block=False)) - except queue.Empty: - pass - for frame in frames: - self.frames.put(frame) - # This loop runs only when a race condition occurs. - for frame in queued: # pragma: no cover - self.frames.put(frame) - - # This overload structure is required to avoid the error: - # "parameter without a default follows parameter with a default" - - @overload - def get(self, timeout: float | None, decode: Literal[True]) -> str: ... - - @overload - def get(self, timeout: float | None, decode: Literal[False]) -> bytes: ... - - @overload - def get(self, timeout: float | None = None, *, decode: Literal[True]) -> str: ... - - @overload - def get(self, timeout: float | None = None, *, decode: Literal[False]) -> bytes: ... - - @overload - def get(self, timeout: float | None = None, decode: bool | None = None) -> Data: ... - - def get(self, timeout: float | None = None, decode: bool | None = None) -> Data: - """ - Read the next message. - - :meth:`get` returns a single :class:`str` or :class:`bytes`. - - If the message is fragmented, :meth:`get` waits until the last frame is - received, then it reassembles the message and returns it. To receive - messages frame by frame, use :meth:`get_iter` instead. - - Args: - timeout: If a timeout is provided and elapses before a complete - message is received, :meth:`get` raises :exc:`TimeoutError`. - decode: :obj:`False` disables UTF-8 decoding of text frames and - returns :class:`bytes`. :obj:`True` forces UTF-8 decoding of - binary frames and returns :class:`str`. - - Raises: - EOFError: If the stream of frames has ended. - UnicodeDecodeError: If a text frame contains invalid UTF-8. - ConcurrencyError: If two coroutines run :meth:`get` or - :meth:`get_iter` concurrently. - TimeoutError: If a timeout is provided and elapses before a - complete message is received. - - """ - with self.mutex: - if self.get_in_progress: - raise ConcurrencyError("get() or get_iter() is already running") - self.get_in_progress = True - - # Locking with get_in_progress prevents concurrent execution - # until get() fetches a complete message or times out. - - try: - deadline = Deadline(timeout) - - # First frame - frame = self.get_next_frame(deadline.timeout(raise_if_elapsed=False)) - with self.mutex: - self.maybe_resume() - assert frame.opcode is OP_TEXT or frame.opcode is OP_BINARY - if decode is None: - decode = frame.opcode is OP_TEXT - frames = [frame] - - # Following frames, for fragmented messages - while not frame.fin: - try: - frame = self.get_next_frame( - deadline.timeout(raise_if_elapsed=False) - ) - except TimeoutError: - # Put frames already received back into the queue - # so that future calls to get() can return them. - self.reset_queue(frames) - raise - with self.mutex: - self.maybe_resume() - assert frame.opcode is OP_CONT - frames.append(frame) - - finally: - self.get_in_progress = False - - data = b"".join(frame.data for frame in frames) - if decode: - return data.decode() - else: - return data - - @overload - def get_iter(self, decode: Literal[True]) -> Iterator[str]: ... - - @overload - def get_iter(self, decode: Literal[False]) -> Iterator[bytes]: ... - - @overload - def get_iter(self, decode: bool | None = None) -> Iterator[Data]: ... - - def get_iter(self, decode: bool | None = None) -> Iterator[Data]: - """ - Stream the next message. - - Iterating the return value of :meth:`get_iter` yields a :class:`str` or - :class:`bytes` for each frame in the message. - - The iterator must be fully consumed before calling :meth:`get_iter` or - :meth:`get` again. Else, :exc:`ConcurrencyError` is raised. - - This method only makes sense for fragmented messages. If messages aren't - fragmented, use :meth:`get` instead. - - Args: - decode: :obj:`False` disables UTF-8 decoding of text frames and - returns :class:`bytes`. :obj:`True` forces UTF-8 decoding of - binary frames and returns :class:`str`. - - Raises: - EOFError: If the stream of frames has ended. - UnicodeDecodeError: If a text frame contains invalid UTF-8. - ConcurrencyError: If two coroutines run :meth:`get` or - :meth:`get_iter` concurrently. - - """ - with self.mutex: - if self.get_in_progress: - raise ConcurrencyError("get() or get_iter() is already running") - self.get_in_progress = True - - # Locking with get_in_progress prevents concurrent execution - # until get_iter() fetches a complete message or times out. - - # If get_iter() raises an exception e.g. in decoder.decode(), - # get_in_progress remains set and the connection becomes unusable. - - # First frame - frame = self.get_next_frame() - with self.mutex: - self.maybe_resume() - assert frame.opcode is OP_TEXT or frame.opcode is OP_BINARY - if decode is None: - decode = frame.opcode is OP_TEXT - if decode: - decoder = UTF8Decoder() - yield decoder.decode(frame.data, frame.fin) - else: - yield frame.data - - # Following frames, for fragmented messages - while not frame.fin: - frame = self.get_next_frame() - with self.mutex: - self.maybe_resume() - assert frame.opcode is OP_CONT - if decode: - yield decoder.decode(frame.data, frame.fin) - else: - yield frame.data - - self.get_in_progress = False - - def put(self, frame: Frame) -> None: - """ - Add ``frame`` to the next message. - - Raises: - EOFError: If the stream of frames has ended. - - """ - with self.mutex: - if self.closed: - raise EOFError("stream of frames ended") - - self.frames.put(frame) - self.maybe_pause() - - # put() and get/get_iter() call maybe_pause() and maybe_resume() while - # holding self.mutex. This guarantees that the calls interleave properly. - # Specifically, it prevents a race condition where maybe_resume() would - # run before maybe_pause(), leaving the connection incorrectly paused. - - # A race condition is possible when get/get_iter() call self.frames.get() - # without holding self.mutex. However, it's harmless — and even beneficial! - # It can only result in popping an item from the queue before maybe_resume() - # runs and skipping a pause() - resume() cycle that would otherwise occur. - - def maybe_pause(self) -> None: - """Pause the writer if queue is above the high water mark.""" - # Skip if flow control is disabled - if self.high is None: - return - - assert self.mutex.locked() - - # Check for "> high" to support high = 0 - if self.frames.qsize() > self.high and not self.paused: - self.paused = True - self.pause() - - def maybe_resume(self) -> None: - """Resume the writer if queue is below the low water mark.""" - # Skip if flow control is disabled - if self.low is None: - return - - assert self.mutex.locked() - - # Check for "<= low" to support low = 0 - if self.frames.qsize() <= self.low and self.paused: - self.paused = False - self.resume() - - def close(self) -> None: - """ - End the stream of frames. - - Calling :meth:`close` concurrently with :meth:`get`, :meth:`get_iter`, - or :meth:`put` is safe. They will raise :exc:`EOFError`. - - """ - with self.mutex: - if self.closed: - return - - self.closed = True - - if self.get_in_progress: - # Unblock get() or get_iter(). - self.frames.put(None) - - if self.paused: - # Unblock recv_events(). - self.paused = False - self.resume() diff --git a/wip/SNIP/client/websockets/sync/router.py b/wip/SNIP/client/websockets/sync/router.py deleted file mode 100644 index 5572c42..0000000 --- a/wip/SNIP/client/websockets/sync/router.py +++ /dev/null @@ -1,192 +0,0 @@ -from __future__ import annotations - -import http -import ssl as ssl_module -import urllib.parse -from typing import Any, Callable, Literal - -from werkzeug.exceptions import NotFound -from werkzeug.routing import Map, RequestRedirect - -from ..http11 import Request, Response -from .server import Server, ServerConnection, serve - - -__all__ = ["route", "unix_route", "Router"] - - -class Router: - """WebSocket router supporting :func:`route`.""" - - def __init__( - self, - url_map: Map, - server_name: str | None = None, - url_scheme: str = "ws", - ) -> None: - self.url_map = url_map - self.server_name = server_name - self.url_scheme = url_scheme - for rule in self.url_map.iter_rules(): - rule.websocket = True - - def get_server_name(self, connection: ServerConnection, request: Request) -> str: - if self.server_name is None: - return request.headers["Host"] - else: - return self.server_name - - def redirect(self, connection: ServerConnection, url: str) -> Response: - response = connection.respond(http.HTTPStatus.FOUND, f"Found at {url}") - response.headers["Location"] = url - return response - - def not_found(self, connection: ServerConnection) -> Response: - return connection.respond(http.HTTPStatus.NOT_FOUND, "Not Found") - - def route_request( - self, connection: ServerConnection, request: Request - ) -> Response | None: - """Route incoming request.""" - url_map_adapter = self.url_map.bind( - server_name=self.get_server_name(connection, request), - url_scheme=self.url_scheme, - ) - try: - parsed = urllib.parse.urlparse(request.path) - handler, kwargs = url_map_adapter.match( - path_info=parsed.path, - query_args=parsed.query, - ) - except RequestRedirect as redirect: - return self.redirect(connection, redirect.new_url) - except NotFound: - return self.not_found(connection) - connection.handler, connection.handler_kwargs = handler, kwargs - return None - - def handler(self, connection: ServerConnection) -> None: - """Handle a connection.""" - return connection.handler(connection, **connection.handler_kwargs) - - -def route( - url_map: Map, - *args: Any, - server_name: str | None = None, - ssl: ssl_module.SSLContext | Literal[True] | None = None, - create_router: type[Router] | None = None, - **kwargs: Any, -) -> Server: - """ - Create a WebSocket server dispatching connections to different handlers. - - This feature requires the third-party library `werkzeug`_: - - .. code-block:: console - - $ pip install werkzeug - - .. _werkzeug: https://werkzeug.palletsprojects.com/ - - :func:`route` accepts the same arguments as - :func:`~websockets.sync.server.serve`, except as described below. - - The first argument is a :class:`werkzeug.routing.Map` that maps URL patterns - to connection handlers. In addition to the connection, handlers receive - parameters captured in the URL as keyword arguments. - - Here's an example:: - - - from websockets.sync.router import route - from werkzeug.routing import Map, Rule - - def channel_handler(websocket, channel_id): - ... - - url_map = Map([ - Rule("/channel/", endpoint=channel_handler), - ... - ]) - - with route(url_map, ...) as server: - server.serve_forever() - - Refer to the documentation of :mod:`werkzeug.routing` for details. - - If you define redirects with ``Rule(..., redirect_to=...)`` in the URL map, - when the server runs behind a reverse proxy that modifies the ``Host`` - header or terminates TLS, you need additional configuration: - - * Set ``server_name`` to the name of the server as seen by clients. When not - provided, websockets uses the value of the ``Host`` header. - - * Set ``ssl=True`` to generate ``wss://`` URIs without actually enabling - TLS. Under the hood, this bind the URL map with a ``url_scheme`` of - ``wss://`` instead of ``ws://``. - - There is no need to specify ``websocket=True`` in each rule. It is added - automatically. - - Args: - url_map: Mapping of URL patterns to connection handlers. - server_name: Name of the server as seen by clients. If :obj:`None`, - websockets uses the value of the ``Host`` header. - ssl: Configuration for enabling TLS on the connection. Set it to - :obj:`True` if a reverse proxy terminates TLS connections. - create_router: Factory for the :class:`Router` dispatching requests to - handlers. Set it to a wrapper or a subclass to customize routing. - - """ - url_scheme = "ws" if ssl is None else "wss" - if ssl is not True and ssl is not None: - kwargs["ssl"] = ssl - - if create_router is None: - create_router = Router - - router = create_router(url_map, server_name, url_scheme) - - _process_request: ( - Callable[ - [ServerConnection, Request], - Response | None, - ] - | None - ) = kwargs.pop("process_request", None) - if _process_request is None: - process_request: Callable[ - [ServerConnection, Request], - Response | None, - ] = router.route_request - else: - - def process_request( - connection: ServerConnection, request: Request - ) -> Response | None: - response = _process_request(connection, request) - if response is not None: - return response - return router.route_request(connection, request) - - return serve(router.handler, *args, process_request=process_request, **kwargs) - - -def unix_route( - url_map: Map, - path: str | None = None, - **kwargs: Any, -) -> Server: - """ - Create a WebSocket Unix server dispatching connections to different handlers. - - :func:`unix_route` combines the behaviors of :func:`route` and - :func:`~websockets.sync.server.unix_serve`. - - Args: - url_map: Mapping of URL patterns to connection handlers. - path: File system path to the Unix socket. - - """ - return route(url_map, unix=True, path=path, **kwargs) diff --git a/wip/SNIP/client/websockets/sync/server.py b/wip/SNIP/client/websockets/sync/server.py deleted file mode 100644 index efb40a7..0000000 --- a/wip/SNIP/client/websockets/sync/server.py +++ /dev/null @@ -1,763 +0,0 @@ -from __future__ import annotations - -import hmac -import http -import logging -import os -import re -import selectors -import socket -import ssl as ssl_module -import sys -import threading -import warnings -from collections.abc import Iterable, Sequence -from types import TracebackType -from typing import Any, Callable, Mapping, cast - -from ..exceptions import InvalidHeader -from ..extensions.base import ServerExtensionFactory -from ..extensions.permessage_deflate import enable_server_permessage_deflate -from ..frames import CloseCode -from ..headers import ( - build_www_authenticate_basic, - parse_authorization_basic, - validate_subprotocols, -) -from ..http11 import SERVER, Request, Response -from ..protocol import CONNECTING, OPEN, Event -from ..server import ServerProtocol -from ..typing import LoggerLike, Origin, StatusLike, Subprotocol -from .connection import Connection -from .utils import Deadline - - -__all__ = ["serve", "unix_serve", "ServerConnection", "Server", "basic_auth"] - - -class ServerConnection(Connection): - """ - :mod:`threading` implementation of a WebSocket server connection. - - :class:`ServerConnection` provides :meth:`recv` and :meth:`send` methods for - receiving and sending messages. - - It supports iteration to receive messages:: - - for message in websocket: - process(message) - - The iterator exits normally when the connection is closed with close code - 1000 (OK) or 1001 (going away) or without a close code. It raises a - :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is - closed with any other code. - - The ``ping_interval``, ``ping_timeout``, ``close_timeout``, and - ``max_queue`` arguments have the same meaning as in :func:`serve`. - - Args: - socket: Socket connected to a WebSocket client. - protocol: Sans-I/O connection. - - """ - - def __init__( - self, - socket: socket.socket, - protocol: ServerProtocol, - *, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = 10, - max_queue: int | None | tuple[int | None, int | None] = 16, - ) -> None: - self.protocol: ServerProtocol - self.request_rcvd = threading.Event() - super().__init__( - socket, - protocol, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - close_timeout=close_timeout, - max_queue=max_queue, - ) - self.username: str # see basic_auth() - self.handler: Callable[[ServerConnection], None] # see route() - self.handler_kwargs: Mapping[str, Any] # see route() - - def respond(self, status: StatusLike, text: str) -> Response: - """ - Create a plain text HTTP response. - - ``process_request`` and ``process_response`` may call this method to - return an HTTP response instead of performing the WebSocket opening - handshake. - - You can modify the response before returning it, for example by changing - HTTP headers. - - Args: - status: HTTP status code. - text: HTTP response body; it will be encoded to UTF-8. - - Returns: - HTTP response to send to the client. - - """ - return self.protocol.reject(status, text) - - def handshake( - self, - process_request: ( - Callable[ - [ServerConnection, Request], - Response | None, - ] - | None - ) = None, - process_response: ( - Callable[ - [ServerConnection, Request, Response], - Response | None, - ] - | None - ) = None, - server_header: str | None = SERVER, - timeout: float | None = None, - ) -> None: - """ - Perform the opening handshake. - - """ - if not self.request_rcvd.wait(timeout): - raise TimeoutError("timed out while waiting for handshake request") - - if self.request is not None: - with self.send_context(expected_state=CONNECTING): - response = None - - if process_request is not None: - try: - response = process_request(self, self.request) - except Exception as exc: - self.protocol.handshake_exc = exc - response = self.protocol.reject( - http.HTTPStatus.INTERNAL_SERVER_ERROR, - ( - "Failed to open a WebSocket connection.\n" - "See server log for more information.\n" - ), - ) - - if response is None: - self.response = self.protocol.accept(self.request) - else: - self.response = response - - if server_header: - self.response.headers["Server"] = server_header - - response = None - - if process_response is not None: - try: - response = process_response(self, self.request, self.response) - except Exception as exc: - self.protocol.handshake_exc = exc - response = self.protocol.reject( - http.HTTPStatus.INTERNAL_SERVER_ERROR, - ( - "Failed to open a WebSocket connection.\n" - "See server log for more information.\n" - ), - ) - - if response is not None: - self.response = response - - self.protocol.send_response(self.response) - - # self.protocol.handshake_exc is set when the connection is lost before - # receiving a request, when the request cannot be parsed, or when the - # handshake fails, including when process_request or process_response - # raises an exception. - - # It isn't set when process_request or process_response sends an HTTP - # response that rejects the handshake. - - if self.protocol.handshake_exc is not None: - raise self.protocol.handshake_exc - - def process_event(self, event: Event) -> None: - """ - Process one incoming event. - - """ - # First event - handshake request. - if self.request is None: - assert isinstance(event, Request) - self.request = event - self.request_rcvd.set() - # Later events - frames. - else: - super().process_event(event) - - def recv_events(self) -> None: - """ - Read incoming data from the socket and process events. - - """ - try: - super().recv_events() - finally: - # If the connection is closed during the handshake, unblock it. - self.request_rcvd.set() - - -class Server: - """ - WebSocket server returned by :func:`serve`. - - This class mirrors the API of :class:`~socketserver.BaseServer`, notably the - :meth:`~socketserver.BaseServer.serve_forever` and - :meth:`~socketserver.BaseServer.shutdown` methods, as well as the context - manager protocol. - - Args: - socket: Server socket listening for new connections. - handler: Handler for one connection. Receives the socket and address - returned by :meth:`~socket.socket.accept`. - logger: Logger for this server. - It defaults to ``logging.getLogger("websockets.server")``. - See the :doc:`logging guide <../../topics/logging>` for details. - - """ - - def __init__( - self, - socket: socket.socket, - handler: Callable[[socket.socket, Any], None], - logger: LoggerLike | None = None, - ) -> None: - self.socket = socket - self.handler = handler - if logger is None: - logger = logging.getLogger("websockets.server") - self.logger = logger - if sys.platform != "win32": - self.shutdown_watcher, self.shutdown_notifier = os.pipe() - - def serve_forever(self) -> None: - """ - See :meth:`socketserver.BaseServer.serve_forever`. - - This method doesn't return. Calling :meth:`shutdown` from another thread - stops the server. - - Typical use:: - - with serve(...) as server: - server.serve_forever() - - """ - poller = selectors.DefaultSelector() - try: - poller.register(self.socket, selectors.EVENT_READ) - except ValueError: # pragma: no cover - # If shutdown() is called before poller.register(), - # the socket is closed and poller.register() raises - # ValueError: Invalid file descriptor: -1 - return - if sys.platform != "win32": - poller.register(self.shutdown_watcher, selectors.EVENT_READ) - - while True: - poller.select() - try: - # If the socket is closed, this will raise an exception and exit - # the loop. So we don't need to check the return value of select(). - sock, addr = self.socket.accept() - except OSError: - break - # Since there isn't a mechanism for tracking connections and waiting - # for them to terminate, we cannot use daemon threads, or else all - # connections would be terminate brutally when closing the server. - thread = threading.Thread(target=self.handler, args=(sock, addr)) - thread.start() - - def shutdown(self) -> None: - """ - See :meth:`socketserver.BaseServer.shutdown`. - - """ - self.socket.close() - if sys.platform != "win32": - os.write(self.shutdown_notifier, b"x") - - def fileno(self) -> int: - """ - See :meth:`socketserver.BaseServer.fileno`. - - """ - return self.socket.fileno() - - def __enter__(self) -> Server: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - self.shutdown() - - -def __getattr__(name: str) -> Any: - if name == "WebSocketServer": - warnings.warn( # deprecated in 13.0 - 2024-08-20 - "WebSocketServer was renamed to Server", - DeprecationWarning, - ) - return Server - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - - -def serve( - handler: Callable[[ServerConnection], None], - host: str | None = None, - port: int | None = None, - *, - # TCP/TLS - sock: socket.socket | None = None, - ssl: ssl_module.SSLContext | None = None, - # WebSocket - origins: Sequence[Origin | re.Pattern[str] | None] | None = None, - extensions: Sequence[ServerExtensionFactory] | None = None, - subprotocols: Sequence[Subprotocol] | None = None, - select_subprotocol: ( - Callable[ - [ServerConnection, Sequence[Subprotocol]], - Subprotocol | None, - ] - | None - ) = None, - compression: str | None = "deflate", - # HTTP - process_request: ( - Callable[ - [ServerConnection, Request], - Response | None, - ] - | None - ) = None, - process_response: ( - Callable[ - [ServerConnection, Request, Response], - Response | None, - ] - | None - ) = None, - server_header: str | None = SERVER, - # Timeouts - open_timeout: float | None = 10, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = 10, - # Limits - max_size: int | None = 2**20, - max_queue: int | None | tuple[int | None, int | None] = 16, - # Logging - logger: LoggerLike | None = None, - # Escape hatch for advanced customization - create_connection: type[ServerConnection] | None = None, - **kwargs: Any, -) -> Server: - """ - Create a WebSocket server listening on ``host`` and ``port``. - - Whenever a client connects, the server creates a :class:`ServerConnection`, - performs the opening handshake, and delegates to the ``handler``. - - The handler receives the :class:`ServerConnection` instance, which you can - use to send and receive messages. - - Once the handler completes, either normally or with an exception, the server - performs the closing handshake and closes the connection. - - This function returns a :class:`Server` whose API mirrors - :class:`~socketserver.BaseServer`. Treat it as a context manager to ensure - that it will be closed and call :meth:`~Server.serve_forever` to serve - requests:: - - from websockets.sync.server import serve - - def handler(websocket): - ... - - with serve(handler, ...) as server: - server.serve_forever() - - Args: - handler: Connection handler. It receives the WebSocket connection, - which is a :class:`ServerConnection`, in argument. - host: Network interfaces the server binds to. - See :func:`~socket.create_server` for details. - port: TCP port the server listens on. - See :func:`~socket.create_server` for details. - sock: Preexisting TCP socket. ``sock`` replaces ``host`` and ``port``. - You may call :func:`socket.create_server` to create a suitable TCP - socket. - ssl: Configuration for enabling TLS on the connection. - origins: Acceptable values of the ``Origin`` header, for defending - against Cross-Site WebSocket Hijacking attacks. Values can be - :class:`str` to test for an exact match or regular expressions - compiled by :func:`re.compile` to test against a pattern. Include - :obj:`None` in the list if the lack of an origin is acceptable. - extensions: List of supported extensions, in order in which they - should be negotiated and run. - subprotocols: List of supported subprotocols, in order of decreasing - preference. - select_subprotocol: Callback for selecting a subprotocol among - those supported by the client and the server. It receives a - :class:`ServerConnection` (not a - :class:`~websockets.server.ServerProtocol`!) instance and a list of - subprotocols offered by the client. Other than the first argument, - it has the same behavior as the - :meth:`ServerProtocol.select_subprotocol - ` method. - compression: The "permessage-deflate" extension is enabled by default. - Set ``compression`` to :obj:`None` to disable it. See the - :doc:`compression guide <../../topics/compression>` for details. - process_request: Intercept the request during the opening handshake. - Return an HTTP response to force the response. Return :obj:`None` to - continue normally. When you force an HTTP 101 Continue response, the - handshake is successful. Else, the connection is aborted. - process_response: Intercept the response during the opening handshake. - Modify the response or return a new HTTP response to force the - response. Return :obj:`None` to continue normally. When you force an - HTTP 101 Continue response, the handshake is successful. Else, the - connection is aborted. - server_header: Value of the ``Server`` response header. - It defaults to ``"Python/x.y.z websockets/X.Y"``. Setting it to - :obj:`None` removes the header. - open_timeout: Timeout for opening connections in seconds. - :obj:`None` disables the timeout. - ping_interval: Interval between keepalive pings in seconds. - :obj:`None` disables keepalive. - ping_timeout: Timeout for keepalive pings in seconds. - :obj:`None` disables timeouts. - close_timeout: Timeout for closing connections in seconds. - :obj:`None` disables the timeout. - max_size: Maximum size of incoming messages in bytes. - :obj:`None` disables the limit. - max_queue: High-water mark of the buffer where frames are received. - It defaults to 16 frames. The low-water mark defaults to ``max_queue - // 4``. You may pass a ``(high, low)`` tuple to set the high-water - and low-water marks. If you want to disable flow control entirely, - you may set it to ``None``, although that's a bad idea. - logger: Logger for this server. - It defaults to ``logging.getLogger("websockets.server")``. See the - :doc:`logging guide <../../topics/logging>` for details. - create_connection: Factory for the :class:`ServerConnection` managing - the connection. Set it to a wrapper or a subclass to customize - connection handling. - - Any other keyword arguments are passed to :func:`~socket.create_server`. - - """ - - # Process parameters - - # Backwards compatibility: ssl used to be called ssl_context. - if ssl is None and "ssl_context" in kwargs: - ssl = kwargs.pop("ssl_context") - warnings.warn( # deprecated in 13.0 - 2024-08-20 - "ssl_context was renamed to ssl", - DeprecationWarning, - ) - - if subprotocols is not None: - validate_subprotocols(subprotocols) - - if compression == "deflate": - extensions = enable_server_permessage_deflate(extensions) - elif compression is not None: - raise ValueError(f"unsupported compression: {compression}") - - if create_connection is None: - create_connection = ServerConnection - - # Bind socket and listen - - # Private APIs for unix_connect() - unix: bool = kwargs.pop("unix", False) - path: str | None = kwargs.pop("path", None) - - if sock is None: - if unix: - if path is None: - raise ValueError("missing path argument") - kwargs.setdefault("family", socket.AF_UNIX) - sock = socket.create_server(path, **kwargs) - else: - sock = socket.create_server((host, port), **kwargs) - else: - if path is not None: - raise ValueError("path and sock arguments are incompatible") - - # Initialize TLS wrapper - - if ssl is not None: - sock = ssl.wrap_socket( - sock, - server_side=True, - # Delay TLS handshake until after we set a timeout on the socket. - do_handshake_on_connect=False, - ) - - # Define request handler - - def conn_handler(sock: socket.socket, addr: Any) -> None: - # Calculate timeouts on the TLS and WebSocket handshakes. - # The TLS timeout must be set on the socket, then removed - # to avoid conflicting with the WebSocket timeout in handshake(). - deadline = Deadline(open_timeout) - - try: - # Disable Nagle algorithm - - if not unix: - sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True) - - # Perform TLS handshake - - if ssl is not None: - sock.settimeout(deadline.timeout()) - # mypy cannot figure this out - assert isinstance(sock, ssl_module.SSLSocket) - sock.do_handshake() - sock.settimeout(None) - - # Create a closure to give select_subprotocol access to connection. - protocol_select_subprotocol: ( - Callable[ - [ServerProtocol, Sequence[Subprotocol]], - Subprotocol | None, - ] - | None - ) = None - if select_subprotocol is not None: - - def protocol_select_subprotocol( - protocol: ServerProtocol, - subprotocols: Sequence[Subprotocol], - ) -> Subprotocol | None: - # mypy doesn't know that select_subprotocol is immutable. - assert select_subprotocol is not None - # Ensure this function is only used in the intended context. - assert protocol is connection.protocol - return select_subprotocol(connection, subprotocols) - - # Initialize WebSocket protocol - - protocol = ServerProtocol( - origins=origins, - extensions=extensions, - subprotocols=subprotocols, - select_subprotocol=protocol_select_subprotocol, - max_size=max_size, - logger=logger, - ) - - # Initialize WebSocket connection - - assert create_connection is not None # help mypy - connection = create_connection( - sock, - protocol, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - close_timeout=close_timeout, - max_queue=max_queue, - ) - except Exception: - sock.close() - return - - try: - try: - connection.handshake( - process_request, - process_response, - server_header, - deadline.timeout(), - ) - except TimeoutError: - connection.close_socket() - connection.recv_events_thread.join() - return - except Exception: - connection.logger.error("opening handshake failed", exc_info=True) - connection.close_socket() - connection.recv_events_thread.join() - return - - assert connection.protocol.state is OPEN - try: - connection.start_keepalive() - handler(connection) - except Exception: - connection.logger.error("connection handler failed", exc_info=True) - connection.close(CloseCode.INTERNAL_ERROR) - else: - connection.close() - - except Exception: # pragma: no cover - # Don't leak sockets on unexpected errors. - sock.close() - - # Initialize server - - return Server(sock, conn_handler, logger) - - -def unix_serve( - handler: Callable[[ServerConnection], None], - path: str | None = None, - **kwargs: Any, -) -> Server: - """ - Create a WebSocket server listening on a Unix socket. - - This function accepts the same keyword arguments as :func:`serve`. - - It's only available on Unix. - - It's useful for deploying a server behind a reverse proxy such as nginx. - - Args: - handler: Connection handler. It receives the WebSocket connection, - which is a :class:`ServerConnection`, in argument. - path: File system path to the Unix socket. - - """ - return serve(handler, unix=True, path=path, **kwargs) - - -def is_credentials(credentials: Any) -> bool: - try: - username, password = credentials - except (TypeError, ValueError): - return False - else: - return isinstance(username, str) and isinstance(password, str) - - -def basic_auth( - realm: str = "", - credentials: tuple[str, str] | Iterable[tuple[str, str]] | None = None, - check_credentials: Callable[[str, str], bool] | None = None, -) -> Callable[[ServerConnection, Request], Response | None]: - """ - Factory for ``process_request`` to enforce HTTP Basic Authentication. - - :func:`basic_auth` is designed to integrate with :func:`serve` as follows:: - - from websockets.sync.server import basic_auth, serve - - with serve( - ..., - process_request=basic_auth( - realm="my dev server", - credentials=("hello", "iloveyou"), - ), - ): - - If authentication succeeds, the connection's ``username`` attribute is set. - If it fails, the server responds with an HTTP 401 Unauthorized status. - - One of ``credentials`` or ``check_credentials`` must be provided; not both. - - Args: - realm: Scope of protection. It should contain only ASCII characters - because the encoding of non-ASCII characters is undefined. Refer to - section 2.2 of :rfc:`7235` for details. - credentials: Hard coded authorized credentials. It can be a - ``(username, password)`` pair or a list of such pairs. - check_credentials: Function that verifies credentials. - It receives ``username`` and ``password`` arguments and returns - whether they're valid. - Raises: - TypeError: If ``credentials`` or ``check_credentials`` is wrong. - ValueError: If ``credentials`` and ``check_credentials`` are both - provided or both not provided. - - """ - if (credentials is None) == (check_credentials is None): - raise ValueError("provide either credentials or check_credentials") - - if credentials is not None: - if is_credentials(credentials): - credentials_list = [cast(tuple[str, str], credentials)] - elif isinstance(credentials, Iterable): - credentials_list = list(cast(Iterable[tuple[str, str]], credentials)) - if not all(is_credentials(item) for item in credentials_list): - raise TypeError(f"invalid credentials argument: {credentials}") - else: - raise TypeError(f"invalid credentials argument: {credentials}") - - credentials_dict = dict(credentials_list) - - def check_credentials(username: str, password: str) -> bool: - try: - expected_password = credentials_dict[username] - except KeyError: - return False - return hmac.compare_digest(expected_password, password) - - assert check_credentials is not None # help mypy - - def process_request( - connection: ServerConnection, - request: Request, - ) -> Response | None: - """ - Perform HTTP Basic Authentication. - - If it succeeds, set the connection's ``username`` attribute and return - :obj:`None`. If it fails, return an HTTP 401 Unauthorized responss. - - """ - try: - authorization = request.headers["Authorization"] - except KeyError: - response = connection.respond( - http.HTTPStatus.UNAUTHORIZED, - "Missing credentials\n", - ) - response.headers["WWW-Authenticate"] = build_www_authenticate_basic(realm) - return response - - try: - username, password = parse_authorization_basic(authorization) - except InvalidHeader: - response = connection.respond( - http.HTTPStatus.UNAUTHORIZED, - "Unsupported credentials\n", - ) - response.headers["WWW-Authenticate"] = build_www_authenticate_basic(realm) - return response - - if not check_credentials(username, password): - response = connection.respond( - http.HTTPStatus.UNAUTHORIZED, - "Invalid credentials\n", - ) - response.headers["WWW-Authenticate"] = build_www_authenticate_basic(realm) - return response - - connection.username = username - return None - - return process_request diff --git a/wip/SNIP/client/websockets/sync/utils.py b/wip/SNIP/client/websockets/sync/utils.py deleted file mode 100644 index 00bce2c..0000000 --- a/wip/SNIP/client/websockets/sync/utils.py +++ /dev/null @@ -1,45 +0,0 @@ -from __future__ import annotations - -import time - - -__all__ = ["Deadline"] - - -class Deadline: - """ - Manage timeouts across multiple steps. - - Args: - timeout: Time available in seconds or :obj:`None` if there is no limit. - - """ - - def __init__(self, timeout: float | None) -> None: - self.deadline: float | None - if timeout is None: - self.deadline = None - else: - self.deadline = time.monotonic() + timeout - - def timeout(self, *, raise_if_elapsed: bool = True) -> float | None: - """ - Calculate a timeout from a deadline. - - Args: - raise_if_elapsed: Whether to raise :exc:`TimeoutError` - if the deadline lapsed. - - Raises: - TimeoutError: If the deadline lapsed. - - Returns: - Time left in seconds or :obj:`None` if there is no limit. - - """ - if self.deadline is None: - return None - timeout = self.deadline - time.monotonic() - if raise_if_elapsed and timeout <= 0: - raise TimeoutError("timed out") - return timeout diff --git a/wip/SNIP/client/websockets/typing.py b/wip/SNIP/client/websockets/typing.py deleted file mode 100644 index ab7ddd3..0000000 --- a/wip/SNIP/client/websockets/typing.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import annotations - -import http -import logging -from typing import TYPE_CHECKING, Any, NewType, Optional, Sequence, Union - - -__all__ = [ - "Data", - "LoggerLike", - "StatusLike", - "Origin", - "Subprotocol", - "ExtensionName", - "ExtensionParameter", -] - - -# Public types used in the signature of public APIs - -# Change to str | bytes when dropping Python < 3.10. -Data = Union[str, bytes] -"""Types supported in a WebSocket message: -:class:`str` for a Text_ frame, :class:`bytes` for a Binary_. - -.. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 -.. _Binary : https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - -""" - - -# Change to logging.Logger | ... when dropping Python < 3.10. -if TYPE_CHECKING: - LoggerLike = Union[logging.Logger, logging.LoggerAdapter[Any]] - """Types accepted where a :class:`~logging.Logger` is expected.""" -else: # remove this branch when dropping support for Python < 3.11 - LoggerLike = Union[logging.Logger, logging.LoggerAdapter] - """Types accepted where a :class:`~logging.Logger` is expected.""" - - -# Change to http.HTTPStatus | int when dropping Python < 3.10. -StatusLike = Union[http.HTTPStatus, int] -""" -Types accepted where an :class:`~http.HTTPStatus` is expected.""" - - -Origin = NewType("Origin", str) -"""Value of a ``Origin`` header.""" - - -Subprotocol = NewType("Subprotocol", str) -"""Subprotocol in a ``Sec-WebSocket-Protocol`` header.""" - - -ExtensionName = NewType("ExtensionName", str) -"""Name of a WebSocket extension.""" - -# Change to tuple[str, str | None] when dropping Python < 3.10. -ExtensionParameter = tuple[str, Optional[str]] -"""Parameter of a WebSocket extension.""" - - -# Private types - -ExtensionHeader = tuple[ExtensionName, Sequence[ExtensionParameter]] -"""Extension in a ``Sec-WebSocket-Extensions`` header.""" - - -ConnectionOption = NewType("ConnectionOption", str) -"""Connection option in a ``Connection`` header.""" - - -UpgradeProtocol = NewType("UpgradeProtocol", str) -"""Upgrade protocol in an ``Upgrade`` header.""" diff --git a/wip/SNIP/client/websockets/uri.py b/wip/SNIP/client/websockets/uri.py deleted file mode 100644 index b925b99..0000000 --- a/wip/SNIP/client/websockets/uri.py +++ /dev/null @@ -1,225 +0,0 @@ -from __future__ import annotations - -import dataclasses -import urllib.parse -import urllib.request - -from .exceptions import InvalidProxy, InvalidURI - - -__all__ = ["parse_uri", "WebSocketURI"] - - -# All characters from the gen-delims and sub-delims sets in RFC 3987. -DELIMS = ":/?#[]@!$&'()*+,;=" - - -@dataclasses.dataclass -class WebSocketURI: - """ - WebSocket URI. - - Attributes: - secure: :obj:`True` for a ``wss`` URI, :obj:`False` for a ``ws`` URI. - host: Normalized to lower case. - port: Always set even if it's the default. - path: May be empty. - query: May be empty if the URI doesn't include a query component. - username: Available when the URI contains `User Information`_. - password: Available when the URI contains `User Information`_. - - .. _User Information: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1 - - """ - - secure: bool - host: str - port: int - path: str - query: str - username: str | None = None - password: str | None = None - - @property - def resource_name(self) -> str: - if self.path: - resource_name = self.path - else: - resource_name = "/" - if self.query: - resource_name += "?" + self.query - return resource_name - - @property - def user_info(self) -> tuple[str, str] | None: - if self.username is None: - return None - assert self.password is not None - return (self.username, self.password) - - -def parse_uri(uri: str) -> WebSocketURI: - """ - Parse and validate a WebSocket URI. - - Args: - uri: WebSocket URI. - - Returns: - Parsed WebSocket URI. - - Raises: - InvalidURI: If ``uri`` isn't a valid WebSocket URI. - - """ - parsed = urllib.parse.urlparse(uri) - if parsed.scheme not in ["ws", "wss"]: - raise InvalidURI(uri, "scheme isn't ws or wss") - if parsed.hostname is None: - raise InvalidURI(uri, "hostname isn't provided") - if parsed.fragment != "": - raise InvalidURI(uri, "fragment identifier is meaningless") - - secure = parsed.scheme == "wss" - host = parsed.hostname - port = parsed.port or (443 if secure else 80) - path = parsed.path - query = parsed.query - username = parsed.username - password = parsed.password - # urllib.parse.urlparse accepts URLs with a username but without a - # password. This doesn't make sense for HTTP Basic Auth credentials. - if username is not None and password is None: - raise InvalidURI(uri, "username provided without password") - - try: - uri.encode("ascii") - except UnicodeEncodeError: - # Input contains non-ASCII characters. - # It must be an IRI. Convert it to a URI. - host = host.encode("idna").decode() - path = urllib.parse.quote(path, safe=DELIMS) - query = urllib.parse.quote(query, safe=DELIMS) - if username is not None: - assert password is not None - username = urllib.parse.quote(username, safe=DELIMS) - password = urllib.parse.quote(password, safe=DELIMS) - - return WebSocketURI(secure, host, port, path, query, username, password) - - -@dataclasses.dataclass -class Proxy: - """ - Proxy. - - Attributes: - scheme: ``"socks5h"``, ``"socks5"``, ``"socks4a"``, ``"socks4"``, - ``"https"``, or ``"http"``. - host: Normalized to lower case. - port: Always set even if it's the default. - username: Available when the proxy address contains `User Information`_. - password: Available when the proxy address contains `User Information`_. - - .. _User Information: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1 - - """ - - scheme: str - host: str - port: int - username: str | None = None - password: str | None = None - - @property - def user_info(self) -> tuple[str, str] | None: - if self.username is None: - return None - assert self.password is not None - return (self.username, self.password) - - -def parse_proxy(proxy: str) -> Proxy: - """ - Parse and validate a proxy. - - Args: - proxy: proxy. - - Returns: - Parsed proxy. - - Raises: - InvalidProxy: If ``proxy`` isn't a valid proxy. - - """ - parsed = urllib.parse.urlparse(proxy) - if parsed.scheme not in ["socks5h", "socks5", "socks4a", "socks4", "https", "http"]: - raise InvalidProxy(proxy, f"scheme {parsed.scheme} isn't supported") - if parsed.hostname is None: - raise InvalidProxy(proxy, "hostname isn't provided") - if parsed.path not in ["", "/"]: - raise InvalidProxy(proxy, "path is meaningless") - if parsed.query != "": - raise InvalidProxy(proxy, "query is meaningless") - if parsed.fragment != "": - raise InvalidProxy(proxy, "fragment is meaningless") - - scheme = parsed.scheme - host = parsed.hostname - port = parsed.port or (443 if parsed.scheme == "https" else 80) - username = parsed.username - password = parsed.password - # urllib.parse.urlparse accepts URLs with a username but without a - # password. This doesn't make sense for HTTP Basic Auth credentials. - if username is not None and password is None: - raise InvalidProxy(proxy, "username provided without password") - - try: - proxy.encode("ascii") - except UnicodeEncodeError: - # Input contains non-ASCII characters. - # It must be an IRI. Convert it to a URI. - host = host.encode("idna").decode() - if username is not None: - assert password is not None - username = urllib.parse.quote(username, safe=DELIMS) - password = urllib.parse.quote(password, safe=DELIMS) - - return Proxy(scheme, host, port, username, password) - - -def get_proxy(uri: WebSocketURI) -> str | None: - """ - Return the proxy to use for connecting to the given WebSocket URI, if any. - - """ - if urllib.request.proxy_bypass(f"{uri.host}:{uri.port}"): - return None - - # According to the _Proxy Usage_ section of RFC 6455, use a SOCKS5 proxy if - # available, else favor the proxy for HTTPS connections over the proxy for - # HTTP connections. - - # The priority of a proxy for WebSocket connections is unspecified. We give - # it the highest priority. This makes it easy to configure a specific proxy - # for websockets. - - # getproxies() may return SOCKS proxies as {"socks": "http://host:port"} or - # as {"https": "socks5h://host:port"} depending on whether they're declared - # in the operating system or in environment variables. - - proxies = urllib.request.getproxies() - if uri.secure: - schemes = ["wss", "socks", "https"] - else: - schemes = ["ws", "socks", "https", "http"] - - for scheme in schemes: - proxy = proxies.get(scheme) - if proxy is not None: - if scheme == "socks" and proxy.startswith("http://"): - proxy = "socks5h://" + proxy[7:] - return proxy - else: - return None diff --git a/wip/SNIP/client/websockets/utils.py b/wip/SNIP/client/websockets/utils.py deleted file mode 100644 index 62d2dc1..0000000 --- a/wip/SNIP/client/websockets/utils.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import annotations - -import base64 -import hashlib -import secrets -import sys - - -__all__ = ["accept_key", "apply_mask"] - - -GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - - -def generate_key() -> str: - """ - Generate a random key for the Sec-WebSocket-Key header. - - """ - key = secrets.token_bytes(16) - return base64.b64encode(key).decode() - - -def accept_key(key: str) -> str: - """ - Compute the value of the Sec-WebSocket-Accept header. - - Args: - key: Value of the Sec-WebSocket-Key header. - - """ - sha1 = hashlib.sha1((key + GUID).encode()).digest() - return base64.b64encode(sha1).decode() - - -def apply_mask(data: bytes, mask: bytes) -> bytes: - """ - Apply masking to the data of a WebSocket message. - - Args: - data: Data to mask. - mask: 4-bytes mask. - - """ - if len(mask) != 4: - raise ValueError("mask must contain 4 bytes") - - data_int = int.from_bytes(data, sys.byteorder) - mask_repeated = mask * (len(data) // 4) + mask[: len(data) % 4] - mask_int = int.from_bytes(mask_repeated, sys.byteorder) - return (data_int ^ mask_int).to_bytes(len(data), sys.byteorder) diff --git a/wip/SNIP/client/websockets/version.py b/wip/SNIP/client/websockets/version.py deleted file mode 100644 index 8d22f4e..0000000 --- a/wip/SNIP/client/websockets/version.py +++ /dev/null @@ -1,92 +0,0 @@ -from __future__ import annotations - -import importlib.metadata - - -__all__ = ["tag", "version", "commit"] - - -# ========= =========== =================== -# release development -# ========= =========== =================== -# tag X.Y X.Y (upcoming) -# version X.Y X.Y.dev1+g5678cde -# commit X.Y 5678cde -# ========= =========== =================== - - -# When tagging a release, set `released = True`. -# After tagging a release, set `released = False` and increment `tag`. - -released = True - -tag = version = commit = "15.0.1" - - -if not released: # pragma: no cover - import pathlib - import re - import subprocess - - def get_version(tag: str) -> str: - # Since setup.py executes the contents of src/websockets/version.py, - # __file__ can point to either of these two files. - file_path = pathlib.Path(__file__) - root_dir = file_path.parents[0 if file_path.name == "setup.py" else 2] - - # Read version from package metadata if it is installed. - try: - version = importlib.metadata.version("websockets") - except ImportError: - pass - else: - # Check that this file belongs to the installed package. - files = importlib.metadata.files("websockets") - if files: - version_files = [f for f in files if f.name == file_path.name] - if version_files: - version_file = version_files[0] - if version_file.locate() == file_path: - return version - - # Read version from git if available. - try: - description = subprocess.run( - ["git", "describe", "--dirty", "--tags", "--long"], - capture_output=True, - cwd=root_dir, - timeout=1, - check=True, - text=True, - ).stdout.strip() - # subprocess.run raises FileNotFoundError if git isn't on $PATH. - except ( - FileNotFoundError, - subprocess.CalledProcessError, - subprocess.TimeoutExpired, - ): - pass - else: - description_re = r"[0-9.]+-([0-9]+)-(g[0-9a-f]{7,}(?:-dirty)?)" - match = re.fullmatch(description_re, description) - if match is None: - raise ValueError(f"Unexpected git description: {description}") - distance, remainder = match.groups() - remainder = remainder.replace("-", ".") # required by PEP 440 - return f"{tag}.dev{distance}+{remainder}" - - # Avoid crashing if the development version cannot be determined. - return f"{tag}.dev0+gunknown" - - version = get_version(tag) - - def get_commit(tag: str, version: str) -> str: - # Extract commit from version, falling back to tag if not available. - version_re = r"[0-9.]+\.dev[0-9]+\+g([0-9a-f]{7,}|unknown)(?:\.dirty)?" - match = re.fullmatch(version_re, version) - if match is None: - raise ValueError(f"Unexpected version: {version}") - (commit,) = match.groups() - return tag if commit == "unknown" else commit - - commit = get_commit(tag, version) diff --git a/wip/SNIP/docs/packetHeader b/wip/SNIP/docs/packetHeader deleted file mode 100644 index 32b13e9..0000000 --- a/wip/SNIP/docs/packetHeader +++ /dev/null @@ -1,7 +0,0 @@ -packet length = 65535 - -SNIP = 4 bytes -Sender = 4 bytes -Receiver = 4 bytes - -Content = 65513 bytes \ No newline at end of file diff --git a/wip/SNIP/server/main.py b/wip/SNIP/server/main.py deleted file mode 100644 index c8f74fe..0000000 --- a/wip/SNIP/server/main.py +++ /dev/null @@ -1,167 +0,0 @@ -import asyncio -import struct -import ipaddress -import websockets - -HOST = "0.0.0.0" -PORT = 8765 -ENABLE_PACKET_LOGGING = True -ENABLE_IP_INFO_LOGGING = True - -NETWORK = ipaddress.IPv4Network("0.0.0.0/0") -STATIC_IPS: set[int] = set() - -clients: dict[int, websockets.WebSocketServerProtocol] = {} -leases: dict[websockets.WebSocketServerProtocol, int] = {} - -clients_lock = asyncio.Lock() -leases_lock = asyncio.Lock() - -DATA_HEADER = "!4s4sI" -DATA_HEADER_SIZE = struct.calcsize(DATA_HEADER) - -NET_START = int(NETWORK.network_address) + 1 -NET_END = int(NETWORK.broadcast_address) - 1 - - -def int_to_bytes(ip: int) -> bytes: - return ip.to_bytes(4, "big") - - -def bytes_to_int(b: bytes) -> int: - return int.from_bytes(b, "big") - - -def allocate_dhcp_ip() -> int | None: - """Lazy allocation of first free IP.""" - for ip in range(NET_START, NET_END + 1): - if ip not in clients and ip not in STATIC_IPS: - return ip - return None - - -async def safe_send(ws: websockets.WebSocketServerProtocol, msg: bytes): - """Fire-and-forget safe send with cleanup.""" - try: - await ws.send(msg) - except websockets.ConnectionClosed: - async with leases_lock: - ip = leases.pop(ws, None) - if ip is not None: - async with clients_lock: - clients.pop(ip, None) - if ENABLE_IP_INFO_LOGGING: - print(f"Released {ipaddress.IPv4Address(ip)} (peer closed)") - -async def debug(): - while True: - print(len(clients)) - await asyncio.sleep(1) - -async def handle_websocket(ws: websockets.WebSocketServerProtocol): - try: - async for msg in ws: - if not msg: - continue - - if not isinstance(msg, bytes): - msg = msg.encode() - - opcode = msg[0] - - # -------------------- - # IP REQUEST - # -------------------- - if opcode == 0x01: - async with leases_lock: - if ws in leases: - continue - - flags = msg[1] - static = False - - if flags == 0x00: # DHCP - ip = allocate_dhcp_ip() - if ip is None: - await ws.send(b"\x02\x02\x00\x00\x00\x00") - continue - - elif flags == 0x01 and len(msg) == 6: - requested = bytes_to_int(msg[2:6]) - if requested not in STATIC_IPS: - await ws.send(b"\x02\x01\x00\x00\x00\x00") - continue - async with clients_lock: - if requested in clients: - await ws.send(b"\x02\x01\x00\x00\x00\x00") - continue - ip = requested - static = True - else: - continue - - async with leases_lock: - leases[ws] = ip - async with clients_lock: - clients[ip] = ws - - await ws.send(b"\x02\x00" + int_to_bytes(ip)) - label = "static" if static else "dynamic" - if ENABLE_IP_INFO_LOGGING: - print(f"Assigned {label} {ipaddress.IPv4Address(ip)}") - - # -------------------- - # DATA PLANE - # -------------------- - elif opcode == 0x10: - async with leases_lock: - if ws not in leases: - continue - - if len(msg) < 1 + DATA_HEADER_SIZE: - continue - - try: - src_b, dst_b, length = struct.unpack( - DATA_HEADER, msg[1:1 + DATA_HEADER_SIZE] - ) - except struct.error: - continue - - payload = msg[1 + DATA_HEADER_SIZE:] - if len(payload) != length: - continue - - src = bytes_to_int(src_b) - dst = bytes_to_int(dst_b) - - if ENABLE_PACKET_LOGGING: - print(f"{ipaddress.IPv4Address(src)} > {ipaddress.IPv4Address(dst)}: {payload}") - - async with clients_lock: - receiver = clients.get(dst) - if receiver: - # Fire-and-forget send - asyncio.create_task(safe_send(receiver, msg)) - - except websockets.ConnectionClosed: - pass - - finally: - async with leases_lock: - ip = leases.pop(ws, None) - if ip is not None: - async with clients_lock: - clients.pop(ip, None) - if ENABLE_IP_INFO_LOGGING: - print(f"Released {ipaddress.IPv4Address(ip)}") - - -async def main(): - async with websockets.serve(handle_websocket, HOST, PORT, max_size=2**20, max_queue=65535): - print(f"IPoW router listening on {PORT}") - await asyncio.Future() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/wip/SNIP/server/registered.json b/wip/SNIP/server/registered.json deleted file mode 100644 index 432f4fd..0000000 --- a/wip/SNIP/server/registered.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "1.1.1.1": { - "salt": "", - "sha256": "" - } -} \ No newline at end of file diff --git a/wip/SNIP/websockets/__init__.py b/wip/SNIP/websockets/__init__.py deleted file mode 100644 index f90aff5..0000000 --- a/wip/SNIP/websockets/__init__.py +++ /dev/null @@ -1,236 +0,0 @@ -from __future__ import annotations - -# Importing the typing module would conflict with websockets.typing. -from typing import TYPE_CHECKING - -from .imports import lazy_import -from .version import version as __version__ # noqa: F401 - - -__all__ = [ - # .asyncio.client - "connect", - "unix_connect", - "ClientConnection", - # .asyncio.router - "route", - "unix_route", - "Router", - # .asyncio.server - "basic_auth", - "broadcast", - "serve", - "unix_serve", - "ServerConnection", - "Server", - # .client - "ClientProtocol", - # .datastructures - "Headers", - "HeadersLike", - "MultipleValuesError", - # .exceptions - "ConcurrencyError", - "ConnectionClosed", - "ConnectionClosedError", - "ConnectionClosedOK", - "DuplicateParameter", - "InvalidHandshake", - "InvalidHeader", - "InvalidHeaderFormat", - "InvalidHeaderValue", - "InvalidMessage", - "InvalidOrigin", - "InvalidParameterName", - "InvalidParameterValue", - "InvalidProxy", - "InvalidProxyMessage", - "InvalidProxyStatus", - "InvalidState", - "InvalidStatus", - "InvalidUpgrade", - "InvalidURI", - "NegotiationError", - "PayloadTooBig", - "ProtocolError", - "ProxyError", - "SecurityError", - "WebSocketException", - # .frames - "Close", - "CloseCode", - "Frame", - "Opcode", - # .http11 - "Request", - "Response", - # .protocol - "Protocol", - "Side", - "State", - # .server - "ServerProtocol", - # .typing - "Data", - "ExtensionName", - "ExtensionParameter", - "LoggerLike", - "StatusLike", - "Origin", - "Subprotocol", -] - -# When type checking, import non-deprecated aliases eagerly. Else, import on demand. -if TYPE_CHECKING: - from .asyncio.client import ClientConnection, connect, unix_connect - from .asyncio.router import Router, route, unix_route - from .asyncio.server import ( - Server, - ServerConnection, - basic_auth, - broadcast, - serve, - unix_serve, - ) - from .client import ClientProtocol - from .datastructures import Headers, HeadersLike, MultipleValuesError - from .exceptions import ( - ConcurrencyError, - ConnectionClosed, - ConnectionClosedError, - ConnectionClosedOK, - DuplicateParameter, - InvalidHandshake, - InvalidHeader, - InvalidHeaderFormat, - InvalidHeaderValue, - InvalidMessage, - InvalidOrigin, - InvalidParameterName, - InvalidParameterValue, - InvalidProxy, - InvalidProxyMessage, - InvalidProxyStatus, - InvalidState, - InvalidStatus, - InvalidUpgrade, - InvalidURI, - NegotiationError, - PayloadTooBig, - ProtocolError, - ProxyError, - SecurityError, - WebSocketException, - ) - from .frames import Close, CloseCode, Frame, Opcode - from .http11 import Request, Response - from .protocol import Protocol, Side, State - from .server import ServerProtocol - from .typing import ( - Data, - ExtensionName, - ExtensionParameter, - LoggerLike, - Origin, - StatusLike, - Subprotocol, - ) -else: - lazy_import( - globals(), - aliases={ - # .asyncio.client - "connect": ".asyncio.client", - "unix_connect": ".asyncio.client", - "ClientConnection": ".asyncio.client", - # .asyncio.router - "route": ".asyncio.router", - "unix_route": ".asyncio.router", - "Router": ".asyncio.router", - # .asyncio.server - "basic_auth": ".asyncio.server", - "broadcast": ".asyncio.server", - "serve": ".asyncio.server", - "unix_serve": ".asyncio.server", - "ServerConnection": ".asyncio.server", - "Server": ".asyncio.server", - # .client - "ClientProtocol": ".client", - # .datastructures - "Headers": ".datastructures", - "HeadersLike": ".datastructures", - "MultipleValuesError": ".datastructures", - # .exceptions - "ConcurrencyError": ".exceptions", - "ConnectionClosed": ".exceptions", - "ConnectionClosedError": ".exceptions", - "ConnectionClosedOK": ".exceptions", - "DuplicateParameter": ".exceptions", - "InvalidHandshake": ".exceptions", - "InvalidHeader": ".exceptions", - "InvalidHeaderFormat": ".exceptions", - "InvalidHeaderValue": ".exceptions", - "InvalidMessage": ".exceptions", - "InvalidOrigin": ".exceptions", - "InvalidParameterName": ".exceptions", - "InvalidParameterValue": ".exceptions", - "InvalidProxy": ".exceptions", - "InvalidProxyMessage": ".exceptions", - "InvalidProxyStatus": ".exceptions", - "InvalidState": ".exceptions", - "InvalidStatus": ".exceptions", - "InvalidUpgrade": ".exceptions", - "InvalidURI": ".exceptions", - "NegotiationError": ".exceptions", - "PayloadTooBig": ".exceptions", - "ProtocolError": ".exceptions", - "ProxyError": ".exceptions", - "SecurityError": ".exceptions", - "WebSocketException": ".exceptions", - # .frames - "Close": ".frames", - "CloseCode": ".frames", - "Frame": ".frames", - "Opcode": ".frames", - # .http11 - "Request": ".http11", - "Response": ".http11", - # .protocol - "Protocol": ".protocol", - "Side": ".protocol", - "State": ".protocol", - # .server - "ServerProtocol": ".server", - # .typing - "Data": ".typing", - "ExtensionName": ".typing", - "ExtensionParameter": ".typing", - "LoggerLike": ".typing", - "Origin": ".typing", - "StatusLike": ".typing", - "Subprotocol": ".typing", - }, - deprecated_aliases={ - # deprecated in 9.0 - 2021-09-01 - "framing": ".legacy", - "handshake": ".legacy", - "parse_uri": ".uri", - "WebSocketURI": ".uri", - # deprecated in 14.0 - 2024-11-09 - # .legacy.auth - "BasicAuthWebSocketServerProtocol": ".legacy.auth", - "basic_auth_protocol_factory": ".legacy.auth", - # .legacy.client - "WebSocketClientProtocol": ".legacy.client", - # .legacy.exceptions - "AbortHandshake": ".legacy.exceptions", - "InvalidStatusCode": ".legacy.exceptions", - "RedirectHandshake": ".legacy.exceptions", - "WebSocketProtocolError": ".legacy.exceptions", - # .legacy.protocol - "WebSocketCommonProtocol": ".legacy.protocol", - # .legacy.server - "WebSocketServer": ".legacy.server", - "WebSocketServerProtocol": ".legacy.server", - }, - ) diff --git a/wip/SNIP/websockets/__main__.py b/wip/SNIP/websockets/__main__.py deleted file mode 100644 index 2f05ddc..0000000 --- a/wip/SNIP/websockets/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .cli import main - - -if __name__ == "__main__": - main() diff --git a/wip/SNIP/websockets/__pycache__/__init__.cpython-313.pyc b/wip/SNIP/websockets/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 08d9ca3e8873e2fa5690302070858e1964e484f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4447 zcmey&%ge>Uz`#(r@pt2dh$TM@eggFZtrTOeZ*M+~zD#2|*m#S9?T?B1MSTt!@7 z+(q18JViWUT^!!LUVKG-Ui?M;UIIk|U@=Z_K`)^qAur(~VK0#)5iijqQ7^F~F)#5V zaj-m>w}h8uktCSS?JeacT_o)#QzYXhTOV zQYljLQY}*TQY%vPQZG{X(kRmK(k#;S(kjyO(k{~W(kasM(k;^U(ks&Q(l65YGAJ_e zGAuImGAc6iGA=UqGAT0gGA%OoGAlCkGA}asvM90u+sEr|>19=9)0F#nnQYu(Fo-tSkOv-{uIWQ>?CKbS>Vz5#?W3Y0tN<3q*DwtFQlj^}5@r(ux z!I~gCSPM*QgGn7QsS76cz@$EyGys!^VA2Ro8iPp_Flh=V&A_BNn6vJSAZNQ{0n6v|v_F&QhOge%|r(oxJ#$XpP=?W&@z@&SyM?7P&Cz$jK_Ks%^_5qW= zAkvjF*e}>01qSGGxH1L@1_uTQ1qTI#KuB;X6ov(d2Zsko=i+omSpDV6>Boy;th!maE*8NaCP?f^mBj7$iTn=Qk0WeRT-a|TaaH=qRDuR zy)3n;I5R&_)A|;Ba(-T3YI4afp3=O`ig*}Lz&R%~HLt`OBAJ<=cZ;#GpQ&)F(o;%xa1aVacWT+Ob3K55DXG3LNb;OB65ol zVqrj0eo20Ee$Fj+kJQAJ)S}{B+)##3W_Ic=Vc*i6lFWjf)Ud>y($r$tqN4nwTLRAc zdC8?kMX7nol?V}#r!w>NoOAMvQ&Vn7;*pq_Qk;>PoqCHGCIWWmEnyU{TYgb)V#zHb6hW|)Z}CCJd{c{y z6Vnmq_!niSXXf1!fl9-z_Djr7y(Nk+3NecZCR~(XQ3-cK0Eh=O9_9qFU~oxdNonyd zm^Kg>;Q$b?v>5Kb(1P@$#FW%qTu?hggFJ5u_@$=jmt=y|8YKL90}?B9@)J`+^7EZC z({J&@q7Nd=1-2E!<_%6wE-lI|fyR(fcxqB`esXqdiEBl2Y5^$q-eLvE%q>nZ<(!|A zdW+Q!6alx`{0ovnT=t;U!qU{@l3N@>sl^5PdBv%>IAAv3VhPR!=>T~n^%fr_RUsr@ z5=#ukXfb0U^W{xez=26lM0||RFk;~ zRErgHFfcF_ae^`$a}h6y%?%=WKmu$_CJ)h;RfE4j{q_M1VrAieE3WxH2y}GhZ(mT;70#5fZAP zKtz%UmpDa5piqSbGAKBaL!y-_`fTN{~ zQxBFWIjQN1$(2yR;9v`2WlM^3*iw`Y7q84xnX=rvtE$$G~km$r;_}yZIBo%NGc#97$ zFM@I?B17KdLL?(_<{Z%cc#92`NK=c!`4TNV-eQAhM0hcIivyPM!1)r98E>&cg9%h< z5|IIKv8U(cCne?-uVnZPYW&^Ob+L-^s4Pe=%FNI64~{9%EQksA^9+b7PfaQY*R91d z@$s2?nI-Y@dIgoYIBatBQ%ZAE?TXSE7#Kh;!{W2d3=AKb85tSxGAMmzV_;N#%AjzU zLHiS%J0mw!1N*lSCI%M4`z-DkS={flSYBkY{LaMA%J!Xsot5pY00XP~eHPD)ES}$) zq(Ops2@3{Ri_0wL_gOUWv*=%B(Z9>0_MM3Xq>Y1>?GpnBE89mA23CQa9Q@s!ADG-3 z1-TyZh+Yua_`nKce_&(amA)XO`+*(I<6z*Gzo20FffLN*Vc-?HAgcC(7tG^h;FY`} zrTu{)%oAYX6}=#){y`AT6M@Q!f_Y*Lyb>2AwLXZ0c@j`lB*8o>s7h%tPX=nAESM(; zHB%nUQ)1wioss!L8O%{*;1vToS{=;OVBnRyAg%L36U@_M;8nVyZ1O=H%+q7wm0!U7 zQ6J1P0CPSVf;mPEyebP~KNy2KCJela3v@r2f;naojyafP0nuX#=2$WC%FKxUU=8Nj zFz_lZu>D{Q=GZ~>*n>F^j3CG6esE+2bDbC=+MFR=7e)qNXDp)E1NB`W(uX N$jHi8l)}Kk003ru*46+3 diff --git a/wip/SNIP/websockets/__pycache__/__init__.cpython-314.pyc b/wip/SNIP/websockets/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index b24d696ce5254fa15e5b335352e41051c3f225a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5192 zcmdPqqU0R>CgNpvMrz>c<$w=EoSs?#CF!;l~)n>Bktv<;NJr z?Z+6z&FQ>y>cBkr(<;NH#?Z+4-&F-*=f@Z%@5dOV;KvxG=*JkOQ@I z>c<$Q=EoSM?#CFU;l~)H>BktP<;NJL?Z+6T&FQ?d>c<#l=EoRh?#CEp;l~(c>Bksk<;NIg?Z+5o&F;m=f@ai z@5fle5zDL&2{49~r3?%*pm1^UbK#2fY z%*j*GL#RZ^L%2lPL!?B+L$pNHL##y1L%c*BEbr_o;UQTf31+)^N_j|^NPEbX$au(> z$b!XOJ>@*)OXNKiN)$X4OB6knN|Zd5OO!oSN>n^lOH@77O4K~mOVm9yN;Et)OEf*S zO0+z*OSC<7N_0GQOLRT-O7uMROY}VqN(?*=ON>2CN=!UVOH4h?O3XaW zOUykiN-V(kxp`W8Se00LSeICP*p%3K*p}FW#oayaJnT#CJse6Lzlosi5r;h<;mpXUgGZIQR3m@S>oy8RpRC0UE=NGQ{v;{TjDFv5R{|L z5X-8_5bLMNU;+(YeTE>`Anzo`AT}_`4kkH*e3BT0IKdm# zi806kOd5hoBQR+k6qLjmWCA8lgMyP7gUrCBIf!&-46+Cc39<|d4YCRf4YCdj4YCOe z4YCUg4YCai4YCgk)#q?#3m^Ye-| z8E^51L-C|G9&&x|qF1f{1 znwMD-590|q=VYell{iBrK?+%m@=HrnZ*hT@LD+0TAfcjLTuF(=naS~qr6n1+IFpL< z6H}5Ci%V{?7N-`K!E`{_0>L1mA|zwkAR@Q;AQlD`<(K3q=jYsF_ef1lNi8bA#SLZn zWM-${680_4DakCzNexTPDNQYQEh@?{x+UP8pO;)(RFs;RT!|0?c`7qM&p9W*I5p*# zD8fpJ2t=U}y0E|ZEg_fEf}G6c#FEs2#G=I9)RNSqTLPYWWr;bNDISS=Da9Fy*{Qd9 zVIp8>-V#RPy5$$;CYIb1LJ1&5vVlWYQMzX)LWwH zq7buqV8TWD6_s!&1b}!j<6%w!3kH`YmXsFXf@uSB5e@+HN{iv{3oS@5N=!+;#Rat^ zG|2OofM05Qen}=ckwL2+oCv_AM@- z{PgtHB5-2k0-04>3}&-II2jliir7H}2Ll6xCUX%Fhz+Xni?~4o zY(+vK0YMPK3nKVH1V4xn0Le2J345q2QL z0Yun?2uB76hAMu&#Nx`li@^U|x>^>R|v z6O$`%ac01?IcGs)QE_T~X;Eesi(Y9_<}Dt0!Ug3B1t(C+=U7^j0T)J1c~v}6L-oL= zqcozVjE6ZXJ}oi1B)_QgmN?uD)UvRO4`v1=q;By!Cgm5Ez{{d4A(%WO5N-)V^DQI| zgL9K$P-;qMQEDl}Nu<3^G7=*1-{OEJ0JJjT78^8} zKqUbY<-jfW^ql;p#GK-l44*+wnOnLpRxuux1*t`u`FZ}qG3A*BF~NSG0WsyNNyXp_ zqc|o$J~J<~BtBlRpz;=nO>TZlX-=wLQ7Qui10w?iL-83l28IvJjEszT8I-=VF)%7V zWl*@wp#6c(ospaA0SoU3RuJO@8w0EG2X-)%gMn5211Fft!@$b(ffvl=V_+5hzz=2$ zFtGA|5Ck(tK%$~xrWgaOzz1}%#lR}{K^x4}1DT)?W*UH)hG3=<1FOsjV=&W%fmQN@DVS*n zVw!`Q79h2jV5SuVtH=jyFw+KPt}U2p2U2SfW;!r}+~ddy<~T8elsiK>E{qJU(jQzQ z95+UgrS1@p2O}u(JRuw}Mv%GQ5RMNc$Yfs##}BN`AHoR$8yyJY1c5CLhHyd{K|vM@ z;e;`Q+#U|$M1b`~LO4-iPBerQ1J)A@;lzPu;vt*_unQ6)oD@b-#H2zvX^fxW30K*}7MAi>K74qgbyg$Wc?t`LqJ6DSPaAsi1d#}mTwVgk9%8^ZBn0woh)FsFh2 zdk7N)i{O10_lqp<_gO42vRHm+VrOOh&cM#f_Ems^RsBAT=S3FJ?@ZDlLA-$54|qf`h--i{ zlOWdzHU?hl3o^Q({3OWrfrEip{(^!bC`$=)ec)l>6}cd)2Fg`}Tp#!tcqK1LX@fGB zAlC;0242w%V(Os0CCK$b1S%&AGXH}Z1FysdNi9$g6Xg0J0X0PuEGGq3DGlbyK<$$S z^W>mr%7b}I47{>4GC}!Gkn4jQ1Fsm!(du9+4F+DR3(`8ETqnr&L5qP`>4LHeDB}ro zeb8gzm0!U7Q6H?(0L=Md2<8|u@Tx3`1!X@$t`8;*yow8SK^ahx>w_7DV-8kk0nuX# z=2$WC%FKubWkW%(4>k6`*M|^B242Y-x*tFpRFLaK7$d}Ipe!oL z^&tYH_X8-C3UYmjf^tFGRFLaK3{)>DqY83;h=a<4vZ^50hXkmrK$%sL>q8171Fy)0 z$Pb|GD#-OAjgf&@Vus`gP=*!c`j7#Y1!Y-5t`Av^47{QfGCzPatsvKjJVpjyu?dwQ zK-pH1>q7w}1F!fDPH@H*q7?_Fi zp$6>z%m+L|6FhJ5NY9A8!K1K1^9GN^49y!nA`>#9EXf&`A3+%r6hcg3%Wv?A&yWNW zVmElCX1IYeh5iP<8$6mT>>lu_UQjoGz$0@(&gce@`~^i*7^!kW%?d1Q`G80Eg1r6> Xu*whGVlrIcLzxm8S=ow`85kG<7t^Yv diff --git a/wip/SNIP/websockets/__pycache__/client.cpython-313.pyc b/wip/SNIP/websockets/__pycache__/client.cpython-313.pyc deleted file mode 100644 index 81190d19c29772930cea331f434ddd64fb6fc53d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15961 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 diff --git a/wip/SNIP/websockets/__pycache__/datastructures.cpython-313.pyc b/wip/SNIP/websockets/__pycache__/datastructures.cpython-313.pyc deleted file mode 100644 index eb7a862b756ee378f1e3b0b18dbd1c1670b26ff7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8888 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 diff --git a/wip/SNIP/websockets/__pycache__/datastructures.cpython-314.pyc b/wip/SNIP/websockets/__pycache__/datastructures.cpython-314.pyc deleted file mode 100644 index 394157341faf78ed745ac452f39c97f0c984578f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11660 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 aV3GR3&de(Fm6?^Fse$_=4;v#3I3NK7=0LCj diff --git a/wip/SNIP/websockets/__pycache__/exceptions.cpython-313.pyc b/wip/SNIP/websockets/__pycache__/exceptions.cpython-313.pyc deleted file mode 100644 index 5bdec9c699ce8420110175e1d5a171b841ccca9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18201 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 diff --git a/wip/SNIP/websockets/__pycache__/exceptions.cpython-314.pyc b/wip/SNIP/websockets/__pycache__/exceptions.cpython-314.pyc deleted file mode 100644 index c065116e9caa51fc7f6bf7777aac2e6c595ef00d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21881 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 V;ewLc2W~bAw(qQ*j7;E&1OQ;e#rgmM diff --git a/wip/SNIP/websockets/__pycache__/frames.cpython-313.pyc b/wip/SNIP/websockets/__pycache__/frames.cpython-313.pyc deleted file mode 100644 index e42f5b9cef7dc24c6e198d8012e8688b7dcd99fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15894 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 diff --git a/wip/SNIP/websockets/__pycache__/frames.cpython-314.pyc b/wip/SNIP/websockets/__pycache__/frames.cpython-314.pyc deleted file mode 100644 index 64f2210037e31c4f76fc3d7ba7bee09a956abb48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17317 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 diff --git a/wip/SNIP/websockets/__pycache__/headers.cpython-314.pyc b/wip/SNIP/websockets/__pycache__/headers.cpython-314.pyc deleted file mode 100644 index ac3aa9d522e9833ff49d49c1ab2107e48d27bfac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20861 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 FTmY?N3c&yX diff --git a/wip/SNIP/websockets/__pycache__/http11.cpython-313.pyc b/wip/SNIP/websockets/__pycache__/http11.cpython-313.pyc deleted file mode 100644 index 391f26a91e647d28efeab9a687dca7efbe508c19..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15179 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 diff --git a/wip/SNIP/websockets/__pycache__/http11.cpython-314.pyc b/wip/SNIP/websockets/__pycache__/http11.cpython-314.pyc deleted file mode 100644 index 9a6667ab611d270c1f484260847b8014ecc85caa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16532 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 N0i*EuG)8#C8URhY^`QU& diff --git a/wip/SNIP/websockets/__pycache__/imports.cpython-313.pyc b/wip/SNIP/websockets/__pycache__/imports.cpython-313.pyc deleted file mode 100644 index 099b1140c0a0f00f20f171243631382df7aea694..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3328 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 M0yX#`%|ozZ0O22!jsO4v diff --git a/wip/SNIP/websockets/__pycache__/protocol.cpython-313.pyc b/wip/SNIP/websockets/__pycache__/protocol.cpython-313.pyc deleted file mode 100644 index 22cddda2e86974c6640378dc95ec9eaf38eac45c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24664 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 Hg8d8tAZK5M diff --git a/wip/SNIP/websockets/__pycache__/server.cpython-313.pyc b/wip/SNIP/websockets/__pycache__/server.cpython-313.pyc deleted file mode 100644 index ac3dde9f7dbf52d63dc46b70fb40274b3c055696..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22302 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 diff --git a/wip/SNIP/websockets/__pycache__/server.cpython-314.pyc b/wip/SNIP/websockets/__pycache__/server.cpython-314.pyc deleted file mode 100644 index 14a6abb2550183fc2de6c924b584252c2546b69f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24143 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 diff --git a/wip/SNIP/websockets/__pycache__/streams.cpython-313.pyc b/wip/SNIP/websockets/__pycache__/streams.cpython-313.pyc deleted file mode 100644 index 1bd3162dbd609d42288be8693ab47d8cdd0b9138..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5224 zcmey&%ge>Uz`#(r@8zS;FF_nl##`Kpd3pIIi6xo&dBvKHw>aHX^HPfv zOY)17K?-0Ps+I9`2H3I~h9bsbh9ah5#v0{UZ!i~_#S_e3#GB5eDRhe`xTGjG zF*hhRF(tL=B^v_+L)8W@xD$1g5{pw)6pF!W6pFx#^tiY}GBS%5l5-M^ixpDxQ;YM| zOB9Mr3kvd!N)(dw^O8%8ic<4R6p|Bja*7p7@)fLdQ%f?e5kG6!MEuRF>q&r{+RhFbC78NB{-eOBC zO-oBHy2YHDpSF_m7E5tzPFgWYzrrmY7poYL%7WCQ%=|q6;F$8vf|y`G&w!Zn)THA4 zNle0Tmdbn>0 zOLVaGa(o84E*b7j1_lOp1_lOYkSB{6K@0^@@?^|o3TDh>mSYHI2m)z9!$Hhw0t^fc z!AzmdutXcotjoZ_kjE6t6wGKfg@GZFk%7TKD4G?f3%jYNpr8QhV~#Kc>k?vMhz7Y1 ztSy)&l$nnqm^GArpF zWeihU6&QRg7#JcHvD(ap(`J~#NOUk~C=&}q9t(C~f@F=DI2l4&R2a;e(-<|m5^Wh7 z7`Q;WMj=tb$4$2+wJ0|;FR>&wMFEt;71E0Ga}`Q5QlVJ~mS-}H6%rNFVFds*cO~Z+ z<(HOZ=B0vFr79GqmXsFdDU>DVl%^_V<|XHprlb~w4fAnRNX|$sN=zPAi9xW$-zi!tvOW5F%PqGC{90EMD_ML>D_A_D`%ONIu94+;#td>xE8cz8P)Z}17-5D=NpJ(2smfWk!q zg$1H37%vNGtq8m*px5DegI{2RVRz{b0ns}G!V_Gkdr$PfET8}u?=HO~rnDe*h2aY2 z3%cGX98VaZNV}jMdRZ*&f3xuM%ozNj|uWC#6it%u~ooEe4fN;0&IJp23SGKrISEhy_S_ zx=5UXfx%CcrAUf_fdQN`!TGO9hJk@WlLeghisV32@*qM1l#f730F?V6nU4#c$-qsA zTjH3x4=iB@($){leo_nqf)gCO%Q|>F%I@$9PEeVyJyH8IpWFh$%X~_(tRo^mgJnMV zOzz7fDiauyGZIoJfn}#JoDBRzNXA^{Q~JQgz$-{N^YAb*Ff=)uJ4rJhv{rDE#*u|S zfh&1X-HyFhf@OddX9k97c%a8_VO$qFoxtU$m8 zprr4NE29Rp1hYVsJ{FgOWKohnYt=Pq($C3(q#G(Ec4}3S$TbGmN)lgaG+oZH`t)PK(In^Uk|xW3({$&-~x)5l6-}7kaa}X zOz_y%tFrubYJ}@4(Ii38OXd;Nr!g{G|f! zvVD+Z;1TGz=(o5dBsIhCvXIsV9<3W3JpJ5V+!IPJa!B`a7YQ;jFrZ`yc+7cG6m!8$ zp$s|DVxEB^omrEms+7w$F9mm;fPxw`N)Qe8{JdmPUxb*B0di0m=`k=cg!pMPAq6pd zhTs4PUTS_CS}zzT+yV+xP|?@G0BWdW1gWq%s6&h=ES2ztWiVqfBdDrLXVhe>GUsy1 zEKW`=f@FV4FF!Q}oS=0Sl1fVyQu0CL8lWx&N*3#7$l1*$ke%8RT))d>?=Yn3z(t@-&mAf5W44h<{VAh7x(OOReZ z(5OXTVs2`D{4K8d_}u)I(i{+*CqBNgG%*J%!yg}?mRVF>0_ynY<;Tb0Vvmnc$xn`t zF9P|uND<^7P%bG_2eGt41ZYI9$QZ-|wb5>g#>W?fM#z%m6H7{pGLuS6Qj6o`S292X z2h`ar0`)h*1c*@#(%;7Lk%^s^0~5)gJpZVU_zpuTG{s6PL|%*e=imqF<%gX(v-Kqf}%PYfUuY%Kt& Czr|w! diff --git a/wip/SNIP/websockets/__pycache__/streams.cpython-314.pyc b/wip/SNIP/websockets/__pycache__/streams.cpython-314.pyc deleted file mode 100644 index ae244647338878383b1411cbba8109fd735e84b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6271 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 A%>V!Z diff --git a/wip/SNIP/websockets/__pycache__/typing.cpython-313.pyc b/wip/SNIP/websockets/__pycache__/typing.cpython-313.pyc deleted file mode 100644 index 349ddb640b4d47a27538d9fc37f6f225ae2af520..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1222 zcmey&%ge>Uz`#(r@R4iQ}dEjZ?T5v zW#;E;vfpBHNi0dc#pRQqo}OCdlbM}*iz~P!v81#Z%x3d1%1qD9yTu(`np9AfUy`4k zpL2`XwW1_7uQ)S5&o41I^_CEv8<1F(n44OXT2#afj%g6V&%nTNOTallFE2G2$rlb}X34$~T zF)%Q!WcUnw6;$5h zu*uC&Da}c>D^g})U|?imU?`SgWMKHf%*e=imqF<<1NUtPzWWTG*BLxNv2k!SHL!n> zVc_8H2%ezY5p|JWqJiZD6B{GTHxULF!5e~N6UwL8O{`l`eqG(>qPoqF;EU>x7sMSe z3OY4--C%KhfUe+zxXndDTd>>*4hBBq2`1C+C)zJ?z96iAkyoRE>jM)bqZ8vt4hAm4 z2@*4mFLTN_u-#x#{>a9_FE$}}LGWch%?9op43;0+7z8C|@UIZQETG%KdxJsc0~>>w z^bF$#94mwu@?MbEy(p@8fkEg7gXRY=24RUAybCHW7&u*2cfKIud{M}yfgfUqi1Y<% m-4(@`g^e#T2;5*W{>a84EOkLjdqwbNAw!TNgCa!+1_l6i&oL_i diff --git a/wip/SNIP/websockets/__pycache__/typing.cpython-314.pyc b/wip/SNIP/websockets/__pycache__/typing.cpython-314.pyc deleted file mode 100644 index 16849466c1f17b6277fd33a540107ffdd6ff8689..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1298 zcmdPq5>^kk5;hO^5_S)c5)Kc}5>5}U5-xcLJ%%7of5sB-Sgu%} zSYBf$9Vv*}AhLu{njw}|9mHf{U`WwmV31*m<%(qvkb?>#QL)Sc3P@ZiGnNh6JT%h< zeHBPE4PC!9$Tds>Dp1Ris90e%wIb3CvFwTrK`gO6CQJ~8C8AIDvfbhhi41U!clK~~_V)C1zs2mBS9y!wFSR_RvLN*qhkpS`VPeiLj^Nb7 z($u`<)LX2fd71fnx7hrPGSf5jZgB^fCKVLrm*gks=iK6Tttd&&E6&W%^GnQ4y(QqB zpO=@K3^D>@&@KMZg7l)ql+*yI4o&u3EG~&9iMP0X^3&5(i+nP(Q*Utvmn4>y7K7MD zpp;w0&A`A=#KXYAa7zeoK|o?rVs2_lYLTYwEw1?Zw9=B&qSW~KB4&^>mW+~;f?Mo4 z`RVDIdFi*(C8w|F76V@hH{NorA%07#C@BP1jM;^14%nRz8ef*=FYoXuKNT9A`kBm~mH z5BGvcYGO)iQ4udl10Mqe!%BwFph&uQ{L+ z8n`|%F)}(ae&k@_5}Y70!}v0%d;{AJ2IY@z4E$mfau)<&=F@E8zQJJmk&Qu6Vg~;T z;mZQL4ZJrPR6ejVh)K^dUcj+Jcq#7%Y2B-$dKVakZZK$m;9?M#n8CZC;(~$GRdweJ z63$nJTpIWxW{5~%kk(yMd|BA|0)xN}2IG%x48l?uq_kHAUluZK;J?XWP^8GfzyJUP CurpKu diff --git a/wip/SNIP/websockets/__pycache__/uri.cpython-313.pyc b/wip/SNIP/websockets/__pycache__/uri.cpython-313.pyc deleted file mode 100644 index 955e5d328be0ffa0459114391d365c00e108426a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8110 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 UX-2&-Hu{W~UsD(r86g1<0GYtSaR2}S diff --git a/wip/SNIP/websockets/__pycache__/utils.cpython-313.pyc b/wip/SNIP/websockets/__pycache__/utils.cpython-313.pyc deleted file mode 100644 index 342586cff32cf9b6d6d3633b21d5b636dbe6f029..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2156 zcmey&%ge>Uz`#(r@A!q zV1{5OZ{{LqFwIiL0;XAuSc92^S@fBT*n(M$*n>HP*`yeg zE1gM`=OxG@O~zZ?iFtYXC5a`O`FX{Dx406MlT!;y;ROsQJL)={IyxD+I9r%H85$;ojD%rk1_lOZ1_s8@Wnhm) z!^9bapk9OvFfcGEF$6Q3GD1Z17@}DqECz-kHW&**D=~yJz|;nDBE(@VQz%1)L6fP< zipxDUFSRJKBvm0%p(rsgB|ldoJGD|FEx$;iBqLQJI5k-}JT)meKRG+KMAtjDQXwNX zF(tJ~kBjT200RSqChINs;?(4#)RN*`+$H(hsd@29l_jagx7d;ri&M=^ZgD1=nWW|= z=clCJVoON{(VC36n6pzWi$UH|xTWP{72{D^kXn?PpXVPOQ=VB66YS?15L2F-R1CJX zIHt5DGpAUupz;<^I@GP8;4k85U|;~@Vm$^1h6aW&TnyYo*STabammg|y3D26;CzQ$ z@H)5jC2r{%&X>6ru5+tjpBE^9&0|P@4 zJID}J3=3ys;v$bR8l2=oCIs<-NK~xE5DJg~Adsg~r5Rv$sW50VR^@X!=jRrbmZXAX zuq-jBG*ux#4a`A}PDgNBM#QLNQF^fzmx6)2iw=9`Bjysl_ElARiWSGcYg|fqbaRRKyEn3o|e<6oZnyfM9PL(qCb=&nggr_lsrIQhjE~^DbzK2OreZOw!^b-FrzLL1A{Ir14Afd zFf$GnYzz!644?#<$4tOA%tX3|B|=4)hk+po6gNj3c01lB?`&;c_oRNc?u>9V0C_)?6;V6QuA(cffKoFQBi); zE#}O;l3QG9MfthVGK{&nviKG!NJD;6N@~$9j*@(cj3(xv4Cv2#Q|c+gLLzP z`0+)l1*wT8sVTQOz}(Edl44c{1_n^Zf~Ij0w+KY2fYNw7B#jF(aBz2+G*?b=Y_7Y* zCvid21rF0E{6hV8opm!(mN+l>UFdt6->AXkhKR%*=LIaYJy!_M^t&jcy&`3U z{g&VzBAX)*2yTqMpyzy1#JR!u4pe9En&9=ZD`PM7TQ_*z;1RjVBY&M+^CGwA8s!x! zt2MU-Z;#y=dzst$0*f;^qi8bSVk=55DJ{yo#avucq{#xwscN9ysR|-MEKRl|1qKF& zTdbh)DK1h3iK~MMunG`MllvA|e0*AINoi4Pe0&io4?)TRA&@dq<|vW{nZO<&pO}*q zA73N_5(kxT;Oq|$pOp+?7lBKLTO2mI`6;D2sdh!$3=9mQ99wM8z`*cy??Y|ZP diff --git a/wip/SNIP/websockets/__pycache__/utils.cpython-314.pyc b/wip/SNIP/websockets/__pycache__/utils.cpython-314.pyc deleted file mode 100644 index 1216aac34c859e29a301818b807d8c67caed741b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2614 zcmdPqAVyCH55^Kkc@Ue)lc|I$ zjvNKh9Guvh8%$sjvx+kh9FKbn=^<@ zoFRxC%;pMWQfAQPc?q&mlkpaJVqRW;Nn%N6eqOQPEw04m0hlvt9gkf>0Un3s~DtB{>qsgRamq)?KPst}x-tQ(%1 z6b$ygu6JssLPlz0N@|fF7ndgME%xHnsl~V0k`jwk%}j1_ zCYhO}<|XH+q~2mnNd?iGjJKGxQ!Bxttx&|nz`$^eCmm`(C^-~~!Gci_94KG77`TP5 zbID%flAV!snM<+3`3|?>b#CcP+|o0gFLNtg=T^VSt$vwX^8$-z5g!8s14`-ug%UV* zD1Z&ao;reU&NEAeXLam4c!~$g|O~xW_P}G9LUkU7LaCCzS36O*o z*!=GdoQ&EZLBs^-4;fmGd@kqw+=9}QRB*B>OUx-v zRme{Rb5PTeBRKOTl96Lkda)Ikf`S4l-B~Gw;nsQJ98i#faw|ARs(>xPog#x+f|&G~l^HZy!Kv~VOG;u%;w@H4{$c@@mc%7b zkUhm}V9UNU@H4tFegu;fLO+1%1*#Bwg)2lH;$l#^fLsg0P#3d)?qLK~88I-QASI1B z7K?HQh7=x9nHvKt)j;|fg5XIfgAu8`!jfde7-B$K4Xi5wo*=7W{|Dx1p@RCy|Zl0#-*xbFh1}HQ;>7e+JuWUpb_1!i zQgBHuNd)Nx8KMWyh9H)ef{89Tv4IWO0-V&_!Abp# z5CaEyhe>ng1jm-TJA4usBuy^!nO@*9eZnu)U)NbTBV~#6a^Ho%m-&qvJZ^|c%yC}8 zGTU>7;9S3}BHAlbHrQ_o-XXF%@_^vh*b92jS4Er~eD6SY=B^1|AG4n@&wp4aNTo@!zMRBr8Fniu1Fu+6e>1n zU|{&b%*e=imqF<<1Mh7HJ}~0?z{bEMF+=78ms$h+2Q~%{y#|)AY;25LUxhdswZBV% I3Iwph0KCWg=l}o! diff --git a/wip/SNIP/websockets/__pycache__/version.cpython-313.pyc b/wip/SNIP/websockets/__pycache__/version.cpython-313.pyc deleted file mode 100644 index 05feff8fc89ac2a735976b8984b560f6f1d62ec3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2980 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 VVqs*p`^3h`>caS)4JHG&8~_a2=lTEu diff --git a/wip/SNIP/websockets/asyncio/__pycache__/connection.cpython-313.pyc b/wip/SNIP/websockets/asyncio/__pycache__/connection.cpython-313.pyc deleted file mode 100644 index af3f96a21b7ffdfb441e1698ec0655d3e7699b59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50810 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 J3?kqG9{`JzVru{Z diff --git a/wip/SNIP/websockets/asyncio/__pycache__/connection.cpython-314.pyc b/wip/SNIP/websockets/asyncio/__pycache__/connection.cpython-314.pyc deleted file mode 100644 index bd54f4fbabc2a9b80969614ae74e8a24257dfc04..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57124 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 OgM@Jd=QRcqu$uuo%idi8 diff --git a/wip/SNIP/websockets/asyncio/__pycache__/messages.cpython-313.pyc b/wip/SNIP/websockets/asyncio/__pycache__/messages.cpython-313.pyc deleted file mode 100644 index ca5f452763ed6774b4ac19ad8919a9e7b7756ae6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13533 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 Ml=;K}BEew`0P^xs6aWAK diff --git a/wip/SNIP/websockets/asyncio/__pycache__/messages.cpython-314.pyc b/wip/SNIP/websockets/asyncio/__pycache__/messages.cpython-314.pyc deleted file mode 100644 index a6c437f5bf73552466b6e92ba6b902c1c4d789b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16188 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 z zEY0Y~!E#te5yZA-bmL+aS+h0%?NUL6mZ@{omWIO9>Aj@Mc_@H zpd~n5A^8fadBvqg;H5T+py`Z^(h|@f6CKnhJG8s5P@IupnvnVORVE|OLR*)AzszTFox|WVhr!R!pvtjHMUQbAqYle*Ha$j^ya6h9!JS9YVsuhEkFXjY zv}e0YoeQ)w1)2&_>k`oFmUvK;6jW1bGJ)r3iq?Tj-(8>qBP5T41mTMhU=7(1AYX!( zA2%?3-~w$3uI{XcYzV&0uX=$)71D_VnF7hq3=GuqktQA=WfsHsGa>sa9Gjm&g0}=A z*+LJY?>kmssb1t)z040Pi7|ZzFT+CMWf<}8W>{R|FSn|$o}Lb5!2&GMY~2!bic>Yg-7;kb=caNHE8}EcUA;PP(0`mg0j>i5+e~7U*I8k`0`Bns5_+G1V@H4=m3P| z)ErP{kqKI?k`G!0oB`Vi308v|VW7jDV9o+X7`Q>332Iy_B<7T)7J*i+fX+(ES12t8 znF?LG0`^XVLLx*Vs0Et=JstSz zPKB0UU_)V!N#Fv_u|v;+fDY;_Br2fV54B7WC7~gE2_vB$LrG}JN_m(V7>eNuO#w$j zD>}l!z<^SWg9<=!F%H^Cji9uAsCNf$A%gv)$pq=ffNJ-Z zkS+*hGN`1oAoUg}Xj%x|Q@I7&z63c^4eWHpN(N4lKPxgz;^Scldg;Z-CxQ}be7q*e z2Na|xtZ1mz??m=t1{l6VnlMQ2e3D7-5{L=}jr1`(hoTacBYpfioY>llkB zfF$OE2=GunXiy%KenCBAa1#~MrUSKiia_;f5vYtS0u>5HXF%F6gNPasaT-K`4sii5 z$N_s8On|nh6#rphU;yn)l4s@k5XOkKRS2{}Nk!wMraG%H;{!pv53C@@2QJWdA#N~J z0>qRAGgUx4hg88#HPB8abud$dK~U)fSpNqt(0(FquxJn?0~_CGCQjA}#?Q2jW2vWtXf}W z1X#5`sDO=PWmSfn#LBAtg^!U{BN-9Si}}A4BhL*j&4)12z2igN3bY#?-OURFm&$|R}5zncd$Sa zPdc-vSdj_?14E_>69Ypjn;2*U1a=}6Viys(sxCrZVV01PkzWixXAN=^RDMwjXaPOA zvz=N7njKJp9AE?+q$-B2@`IWMnUpF9)$!mZfLIRt)8T?PSs_~iKr;cLHVLR}2%g>7 z0eA9JQgc$%6H7pg2J*qJ)r16)ik#GE#O1;pfa$7{2&Iy=5ZkHQAD3QK>^ez z0ZsP9W@$j%d6E6MPIF zY_kezBMx}AU}7F()dX^2p@svfFOL!q;9vlWg2Djhpg`~;d!UgJ*d`z7&Kt~N2c6)T zUkur72HkvyYsgYhAq12{Kz(@J4MONKctxPCB+%gy@JdQhh(J>S?2Ih1%Tr)oo3x_* zT%_G8dayDdswp!Uw7(eABY~W~n39?X8;sF_?+ws|9N`D5!$Cb8a7$Pj)ZI}4x2hrM z%YmX0REvXC4a9t~LhwMWE#y$g%zSXSx(I1ofrch*#Vf=ZP&*oA5!j`WDK@ZS8qk>6 zQ2=EW9R-lXk=zAsn3v=iaB(>z+TJ;pI^Y#GXhSWKO)Bu#GRWy*OJJMcKrQnWu4GVR z%>l&>=qNGJcrqfqVF3)*hTeC?=2UQ4KwSxHITxY20pxAOS!qxM25@sAD3Glb{8CHG z^NX@U`?FJv(h`%QSpw?mq|Ce&&~_+DUl$Z6r@FL~xVm-*7vtm#e z6ErFbG695Pg=KL`5oiabLJ4Tac3OTBXd`86MPf4O0L+r)3{aU?l$u_elL$J{w4f-p z7<9aFF*r?ui~*H~;B$SFDq)?xqEtPoD8gWvn-diZ5=%h+Q#}RGyyTqH6v&J<$Rx;Q zI;g1zDzCv20a^*IkeLQyL$|~x<|#nJ64Wt;Ok9Hk6B0$Ku=9_LtrUDRi@|$NpvS?c zrYIm}!Fy66duTxiyn=VJmSm(>!o39@%uPyF$V*MnF9D5OLTiXpXlg({;~1M|$nr?m z<)9P`4azT>*TlI>aBvsX3|1CGkjRSSf(|3!rnN!KnhG036KVnS(fbTN~ zjgW&9l>(>?%}9i_55T9!CZ%R1mSuv{Iam;GC3G+Y)v0=ra7K;@xCR?|B~=V5P>Y}g z05E@I(QF4h%@`8RplkxoQC13|vxAio$8+mKkLOlGBwJ95PX(Xm4Gs+G8WV_7AkTs} zStTSO8JYmjfXErEBwqo#EmI-01hiEYyn+<&C&(^IBoh>{?UY24N8c?8at31Sabam{ zaS7(mW3aFS4R&{f3M*9K!hHrBa>^Xt*bP5+6%50u}n;?bNUh+Msp`eET+N zlMDE`_MsWRkd5xp6KyefZX@?CKp_GtvqpDpqwLrQpJfd@+#8ys%Mw9Hra=DcEfQNr7rq#Ox&r{s7fX zP@5n=0UhvNoLQA>rQn-bk(pbXs{rDG8hh}Qub}6~gKbHwEJ=kr5tIm#OEGAH4X&;f zz=xtEYycmeZl&OnnVz9ro>-Dvq>!6f1U@(VlL=dc<_13 zpi}fgv)mxNKp5?m0YfvWa?rxTRE3=Ua%9t?6%VKZu8@!b@+jCD@Um84U%@0HK~Dj6 zvNPzkih{&qP)9W(K_eqGJwpd%oF=Gh0v*N#DrkyRA$tTsvJiXVMuFNWsJ0jDDR`!V z&*LZu?K(iJ(9&}9%RyrlB}Mr;3ZQE%KsyZ)4FOP}ALJU4ZQ!F36cQlke9)2>C9_YX`u!->y6Co$z7bk$5Xt2PC8&;C9U~F_~fp?}8$srCm0}=Az z1{r#YgVlj{*ryiJ^3VnaP$#Y=1F1_u?x78!Bn@h0m!u-C-Lz7G-iiWlL4)!K?zSy> z8VtG91yYVE7(gwj5>V5!Bwry>0kkKwAhifoJ0^l!iYn0@J zj$%*F&r2&UPK92X0g4xt08xMsxPZrTGV}Ft2Z<&)l0buCphZ5#rI1T|AcIn%X>y2L zz*zzwm7sPrXaNtJzrjc9fm{ok><5QaLIQZ!9ugYhxpAaLfrxYf>PqM5flez(0~Oz( zQVJ9xa9=<=BrU*2=0~=e?&;-pu`rTrOb|k>Zm=tM)#)ugh1Q?2R zL9+^=$xlteTkMc7b@44OL?8JU4{|T;mJnuF=oU9pPXIDQ1)iJ&&r5-)q)_j4xWxgg zD?rs?5$J3_@OgXS39DOz(C9`43w*K>w2z990kjsEf$_5$6X-IXIF>lpIJP+UINmtE zIQ}?+IKeofIN>;vsSF`(N!$!bdw4_GU<=R`7(zLM8Nu86d63V?41q5%&1Ll$4CP4T zV_=8|?P>te9|SXna_})2!xuTHGKPpGvoSCrjzGv2i3VLt0ahK#7|I#S70MmT6CxbU zY{?dae3OP~5NMM(SRH~uEIdxKW?%>v4dO@0!C1kpp**l1n+gPua|vb$W@BLpX1ChI zz>p|}(?pICeuRm^JfZxdBB89I!l8np0--FSyrF!d?4b;yY@tFXyx>FHII>vs#1K0k zgRm}phE3$=iHC|}ThJWL70eB@6~0&*bWW%^wjIMt454Bsj0Oy$qArYPj3Ep;pfmsu zl5~bpacp~O2YV#%J%6vCv!pvhL{1ivCA4>lyOfH*WEAJlTpO9Wk{f%9l?E=Pq42%PDh!ZGen#*n zOHCFqRRlUl6f)oZ0JQWCWTS#Y1LR=WThcJs$73m)TtH=UD*QwqJ)4~T*`h))vYc`TU`*bx*%1c-wVG^2cR{7>JrDu*3&hhbX1N&I8>^ z0(J3m=YF&-$He_#bMKCm%}X?$k~GZ{eS7cmA2wHp%hH>6c>$ZFnD zw!0yzctc$FBQLuIUx&v>AqFY!8v+v0%{4dVv_XpGv~Nf$-;h!N$j>3g*WvX+h(Sc| z0*@>>dTz0Ru6T2UEjj=l_^HWX1X_Jl1oHGPF7Rn>kiEJfN$@N)czwhz=JeE(TU^Dd zCD7*hE#~6N;#)jrsYRgSaq#|R@KV(x(A`b9m<#d?ZV5r_IFOy-g?*_-@OzTL;a8+7 z0ba@qULSIc1vC$Giv=_(fi)351VwW!h`?LtLJ}J|!c9Pm!SN4D4!78{%M**zi;LcY zgh3~46@zvGBq%5-BqSt&CX-;mM1A*&0za7j}2hOp#EUUqS=4wfG74?+w)Lj6{q zRufV$gEVQ};1PnH$9zG~bVJP*9+x}(Lf84_FY?PTU|Jw}fnRw+`DK2+8v#14yW8_5b-63kwHNGI=}iQe)Y@znjI`RxCOzsq|HdZ&aHBh zTjhqL>UBknONtgd#4jp(A8@;<=-uIRl}GLa8-tSCbw%S#ipCq7pqfE-0%*MvY}FR}`X?38>L(WXO0FXCA`tZTm!O3rw*`KO!h9GE3s)^+6#4 zYUqMO0Fr6DTA^;BgKP2}t1cUQuP8L z6QR+?B}K3`YA_mHRadFQwL!Xf=$E^~RfAdu!?zZ0`g`l1pxY-0=epqw^Bz6;NX`u}KcHU^myhtV=N=s5b)AJFzW*w1Ha@4b_kmT#D-fVZAZqhL5W*H>5I6oH3}K6b zRtt(l*h&my3LlgqYz>gOCWLJUVw*$QPK=;6h0YL1d~jm~Eh}_~@;n$BBvd|lLU~?b zId3S>2Q23ceMp4LB{4E^@qJ5%@V>aPFtSR4)*EU-*BffU*BfelfUP&w_yAgO_=TIDRpW~o z6RY7D4rW$qQ2SM~*#lgxf+G~v`+*m2phFV5L6tn{Sgol@ojLI3yNscX!AyEgp-iCO ztqOxCbK)t`Mg;gJj`)}CB`SpGWmYIahNoa_!{En3gSJ|wfX2U*6LUZVTqtYrb)YNL zz$1ysOaDNZLxPw7p^ZHjr55BQCPS9~CnOXkmSiM=2cp$M3u8dTR7@Kt&v=fI_+Ju?TcQAeis~RbHU(U<1P!H3lBR3E~&HEoYRhP`Su$xk2qB zx8(&EOK_?~X;p$c$KXZ@SQ z)hY~%42T0@5yN06OzaGa%pm>p4C(M&(^fM1xoNT#fiH9c9X@!AGo-R071E*l2r7q} zZ-EXq0VmePoZ?#?rNya5pd$!xae#)rK_lTPL3E2Rvp7DvC>5rmNRW|%Ar2H;pnEtP z7(OtuvbrqI_A`?E;6}k1y;Dykb+T zE^sS-W?*C$y~!`~ftit2^cSS+XVGO?E8xh$yr0pLff?n>R?u+>;6T0tvY8={DUNk2 zLnvb~qYpy}6Nm(lFrth>B92H%WejCZ+JQX!h-e$ZN5tfj#~%>^91XG=T(}@uCQP7# zO2in7K4TuUe_1qWWD2Y@lo4@`9W1)x_5?F9M1$J7V3mjwPlOI)>_)X8HrATQj5rK6 zm|2P;lvx;l(hE!t%pED9@m685BS6PYvxG9Dj<@>rMdOi;2CcjYYepQpkjom(9?ELM z7|aS8NCORkfl6l3sFNH60tR!0GKDgjFo7?u0F7|6gtDkGXmTbBfi8SNo+`q23V#V` zcQ9myga_!BU8lt2%wz?}Qqa!+lFVfA$pfIv&q1ZKRRZFYc<@mqDXGPo>3QHeLGS{{ z^divlrr@Q+D1|ksBuvZC$;mG-hOPP}ZdonDJ{?Gb3t3_do)JM_*$ST615K9c>FFV6 zO3@bZ*dk2RfNKD0DoRbv$+cC=tyD-!ErZxoq=YaAqy#Cm+G;3eq~_%0E9odGW#;6U zrB>#bDrq8=Lz$XZTwI=M3W+Eo391*8Q&Ur*OV1J01nR{K2??-L19Y1)?1~rATB&02 z!Om%*c{#Z0%#gWAt50-BQ-f2xnKe7+NU= zC+8P{%?FJ}LQZM|&-OysA%a#ffo^pMPZT)@J9~O6BxfWRB_@}o7J(*^KtZjbkd&I7 z2wu^i0llyoRO9BSDdgqn>7r`_g%9Xdi?qzV)D+Nl44{PqC5S2NVu&LYjP#5^YZk1E z(vqzb%#Dmqu`HoQ3K1&>kHn%B1;}N|NWqAt1ne)+tRrYC4m>?2Bxt~*Q3pI@4l4XX zs{{%XGodr9;MJh{X`r-_p#Tzwu9v_FeJcgG(!6BQ)>zP#EMn&+c;R|kY7uDXd9ebr zli<$qEP?F-Ku=hZ%@K*k#UO`)QX2eLde9mL$YxGNRFr3=LYF6|R;m}Jf|mMbrs#16 zC1w_<7DLvw!>d9o1S03-!VDe)3=DaUNNo=I z@m^&NQ&|-le1$+Wf1nW|#P9$kxZ2lby2YetaEm3gBsCX#P~aA)O?qlxYDGbj-7Q%} z$%Pp2hf7ZfH5@=gCZI8b3u2y^EIc-_OsQLta$Q{eg1Gi23lHetM+qs$D!I(Ovc#Os z6y#(BZz)?TfGQYpJp>{^Eiv%KNDOFVgmEf^9770mDEJKiJft=iwzJih7$Ov5RW-P+ z1u_qev9+&?Ko?7BGT-9xPOSvDs&27lwVnppNRcs-Pp==%~gh5xj{Q$Min0aMysF-Ys zxTs=xK|t{f8-s|%bz!wj!fF@PEiMZ?U*K{6@r9K^MCLOCBd_dDahVUyjJ&eHz=M{} zta1$VxU`v9F=~V7n7|J%5u`kiNe-#y6AGW( z1vi#mL)euVB3xlnkjI8-eT1?hWY}DK6k6us{w# za}}bUh3!0OY{zZ|v%&1oV~&Q;Du-~u+E$^=*oJL`nL(!yhBB)#L^vQW_DE+{WI#0Y z*%=buL5+Cq=NyvZQYK1VTFfNRpu&&_y@FYh0dbWH)!oTLJ$G_2`27PFR+{Wp8@XT? zC*x})dV*Uh;F~MJEgaBl98iyvm{ty`fQQsQTu4g>!As;aOTee$7VALwiow_TVzhCJ zAU6wxS~<6PK}}dt+Xvi&trGFgEC%gVKrZ&UsszHr!*xLlA)^c>sZ|o8$g}E*nvBb@ zO2iYhBOo%Je~QaH1=c1s$FOGABVHK|uj}A0?45iTm4M6gffsmW?g)!r6!W}j;c;2a^SZF-MPbi7V)7T191chy z;Ju*WcUjE;f&l0|OA#-|FKi6LG8g1*c0}*+y&&UtS;+eWkM|R4`3njLm!%Caa7cV- zVdZoIjdkVP(dGjK_MX9uwuKtv29 z1CR6t7RYT!kkum`kO4OEtPFUR4Lmym8fq&7WpMCZK$RfkqE*QB0qE2NaGb!^Kn@^a zzQy3@r^x|{G|>1}5lB^)5je&{^ERMkdcc!6@FNP)q~OyLpxCZ5M$EqymlQ!XBVYRp zIynKP14e^qkif~%5up>}WWqN$*Hk}sg5m_Io`2ecC{Ge56bFEJ?@eBpf&s1Z{HN}ffavovq9 zf@Wfhi$DWZx7bQ5K}+}{p$%I5R0JB0g5(q~#7Xktviuf5VzHrKQetsx5okQ>mMoGe zVmnkkY!xGTWd<8$F+2E>bx1{ms3t(`H*T?Il#~=08Ww@ZhalNm9ONnRvKY`Z2kV12ZEd<6Q=&#|(V88MwiS?=}PHZ3f=k3|#jaTrM-X++~oz&!B&qLH{X(+Fb^d zy9_>e8RWh*u`n`yWME-r`p(6`sDF_`?>>XxMFzdQ49fQz94<3B+-1jh&mR zf&Gg}1rww3gsLwLAo_b5s}C!q;ztG$`8|OvkcCn5BLj&1ki)?sA@hM%n9=G6uhj=; zVMeP5Tq0mTXf{dc1G_Mz)dvm+Hu0~V!i-j5bl4c(89#_H2#MTq^Sa?1_<=o-kx}{v kH{SXPOBfj#8W`djf*3s+JQzzDoa3_;AEoE}^yTprvd+#Wn7JRZCy zykPTKJo!BMOZdTTR!;#B!4g3ap%Nhv;SymFkrEMkhB&4mHcwFxu@W&bo842~L!v|i z%;xZv^pGl%0<$?ir9EUyWWa1LPgxJS5;+g~5_u1W5(Tgrx2K|qQi+m>a*48sN{I?s zjK@>eL#;&3L%l@ZL!(3kEXM1p>7iAk1!nVk3V3LjXnW|C=y>Rs=z8dt=*ct0F$VE_ z>U$WJ7=YOVo`xPqB}N{`CC2g$dJI9b{){Ciq6~@*K_cP|K|--cCd`oVEHMR(ii$G? z37arM#mvBBV&V)zf^aeOAh{rMafToXafTd$9L5rhAW3nCASn>9#4<=eNLrjBNCw2q zVJxu%eo10UW`16=pC-#KPRH`Z%#y^UoYY&K?x}gHMTsT( zMYlLSOHzx#f*irAg{7%^$*G!*w|GN}5|dMt5|gt-DhpCIS#B{q=2hO}a8At00jp*A zO)MzL%uBz;lAKsv0@C1_SC*KQnc|U}n37tg$#_dFIJKxOwaB%iBsH%%Ge6HQF}Wna zs8W;hmO^SC$c*@6u!8u4)S}$f;^M^g)cBOtw4B5eko}y_Ir+t@&iN^+n#{Lkl1ejk zQsT?Y%i|MEOEOaPN-~oZOH$*L5{omFZ%Gy;78R$0W%7$MtHAz+N{ND<1X5aDnp9Af zUy`4kpHr;Ke2XpEH7LwA=oWhr$jikgw>W}QiwpAeib2+KIs5zhxjKh<`nlg?@egqI zyT$5SmYN6hB_G670Z^kfS#ELp^gX-dk=qlk15`c;bLPfZOgi5%>7_1pfc$675MT(R`&Jtl}VBoUK%}=pP zfQCYXLS}A3PHJvy9weD6;M0pHi%lmS4nGl$xBH zS(cfX4ps(|2eF~au~?6b%dqe9 z1(-%FD=sbt1qFx>kVP;Vh0HvK^3(*!O9cmEabhkg@)GmFAyN#AC9Aa3ykx5ca0*P&o}Nk(FcLT+MSVtQ(^LP-WFmXK5Q zEnXDA-r@msq5izZi{i&yoN)i$;zsgUG9v>60|RL4sBPBZzn)r0{`Nf>r+m8-t+u2X+vPi$Ox=12>4p z!yvBwffvN$1Bvm2SON?pb{_;mEFlIlqYuI$mIwnE-$zjiR{hVQC`^X76Bt14FHT5& za(?b%U|_IeY+wjukY|ViDFfqRjxdGZwlMY}26HyBB!fIdxI!K% zCc)e=h5)EIm0u4oGAicIA3nj6{Da1kG_zYxV zVEEC%@KKC`llKk>Zzuaj4v8B)A~&SeZ%C?wNHO^v63QQV88{`sFtBrSeGp&}m+j%c zVPv*lbED>UBhQOQo|lchJ6Nu8NZ#b&xyiwEn}eswl7WE%C0l|59-J*fc?T4U__C!w zvoeDwdyx*v`E14D_C%2^h{IBnT2XR~xwxdL$Oz;?F%SWYEKSBDQ&8#z=LCr7%|UXY zLLe3-p$JX3!mLIg8H8DlW@LQ;Q5%@S)D8}?AjH|Q)CY=n7D(#j{R}b)>DC>jnx z)&Z3Z7lO57l^HY{6PGeFFmO48<6I$8p&%zQGfx5J7X^=ykN|}uX!}c#iz^|apeP?y z6UIZ61gOx?O952?aB)auEg?Z6H?dM7IWZ>(Qm#Vkc7>9BE=WP4keG*Ryh3JPaY<@o z3aF(~kXi&PJV0eWs2)W$JMs%sK{WwaMq*w{aYkY`s8Wi|FI7lR%u~qCPsvQH1lJlc zCo3eSrsWrb-iH>64dg%nf{VQ+p)rL{t4i9&g1PL4uSszPcWsIpH1HAzB4+;lDUxVVDA zj)GQQV7uYkOY#*!Egq0+kpGi&GE?(P^tiY*nZVf&RM=*vCYNY37de8`kt>LB10^I- zT&t<6fz#M6c8GZ?ws`Yg%}nw*df!;hALenr7LF458GuGCS0_5osb6yP1C zXjpduTCK$@)F@yP0@Xsd1Q3=%Qsu7{B5VU$3sFw4y}XbbEFO}UesL9-6hU1JDHx1F z@kOX$C<4W&6*z9bGsH5o>U{*0AJ|xUg+H)^1U`s>n4(~&0)vFo2SqSb2~=GvgP7pR zO9qt^(0tDTio0e82HwvxYzz!a4Dt*ONVQb}ye@(%XJAld2xAC9HUTPyy|&V0LezZ$ zAiJQdVN?*KIn)k!h9IUaW(I}|#s+2|RxSp4kUh}85==FSc4bUG3~C#NF$C~}_^3FB z55^B-HV2ii3=9m`jPcB+Or^~6OrQ`3$-6V6+ZDuAz!JuoY63D@njwr)ks+KR0E_Lg zQnG+0Mi68Y2#2x62!Uu2hRK7PRv>-~H^{^=7Ifbgu!OOswSnar0+c{1QE`AODj&py zxgKt|4pcv=S^+6Q!T~TvNMd2weFP1qForx4m}1|EFopnZrb@$9Am|tw7&|}?#zN2m zpoSuX3uXne!a@nfPC1ki1y!bC)iAX%52Zs|#ta#Z;SLBFhcUzCQzSv55^fLxDvZD; zAcz1^>jc3Cvx3+_En{%}Go{*t^hz^?G2x2;Fa~Vq!bXaykt$NJWr1c+-ug9yu}Xfgx+ES4g3^2fZFw92#ex#@{3F2K~kwj zxA@&WgMvfio&9|Sd|X3ZT|f=4!qU{@l3P5W)?0jXejcc8SR@WIge$Wc)WA*5OHM5U z)u*>OU138skTxAJJY?ciE0T*mL7G@HN=gcDfg7fffy-N>o_-;&L4J-t@sM%Sc-NpH z{~$=`jVrSlJaP*f{AEi_1`Pn+;()aDic@b1gEV+LyT*t5IfglU`ZzlIxE6s*{afsi zc1rOrUa+%aIyBiJtxI1}YsDW#fO=y^DIkY2r&c5vgL+MBYHDf<3Xt|DC)@|Oq|n=P zdT=>#yC#u|f#D@X1H%^`27a;roUWW3yn@$xV0}@+`T~#59SQmC68aY< z^uMq&^K*S?VCLld04BH?cm(=wI&Cg*CJW#htH;JKvC& zpTO}zSm~0m-UlWgQK_5CMjK4_%j}f7tn4;}rBAP2SE{YgGfVf@xlCVD1?Zz8i_Ur7_Il*$C_ayIS1*q)3OHwBGY*%@?ICvz9^u*!ezbhO5ZC2R^M|O86;d7uZuf>a9{$3t8J(4b#Aqb z+-jG&jcy2uP1l{MdtFHPqLA(d9^H@33_Ql4e3_UzxjylLw0+`Z&@lUPM_T0z1Ba;8 z4KvI6yfb;PNEqKxR$p$r(Du5r`9)=Oc)&PblyUsP!l>xU_?eweMQR?$?+>gTqEf#; z+EK%l?}0F%To-q~EbjcL_yq$4Lz5$ej}GHOCPq&VmV?ZuAhrdgCpX7IOJ)$;hTVss z^$?4$ry$EALq<!b(JuWUkP4*(th-49{S5zblDnh{( z3RiJzNqkXiacNFTkqxLg<%M-r!8&2x`W%o3Cs6o;LO@LoI&PK^(jkXYz@iudjukJ+ z;MoTrP`0+|w7JABe@9q$M(UFA>%zL1gh3hlg0SulX@wr28$1Hnd1Np0$X@5sxX7ci zLgX5c-WN6oUZGC+i#&=8RIl-9Kw=RTMj#9dTu@w)oXR*$iW2jRLDM!>qR_fl&ndAu zH3TM1LfX3z3L8+f4wUwoSdBk2FtHk6(6sviq7RsWxe#X~Cq7VMGBYr6eg?$~Xv`Kw zgZ#t50FG+#cr7BTvG;!i*daQQ$uI_lt{?_whLwzdnk>0U zr=Po%`#OitMGl=6A=fyJz=5R60!gf(BnqzO!L15VEzO|G3mPcSOUwlg!*j*Q=jNxB z=788d@$rSFi8)Xi{`mN`%%b9woXot`y!`n1TkP@iDf!9q@kOAq-Xc(XECRLXi!wp} z0;S_3uvbLm!L7Z_1sfaTKse;*}L24ypL7;JR4RFIc@eeBl0~cZ#7cxEu9hOZ^0gb|<&+3JM zC*wd9$_lxepczZh{9LMnV}K`UfEYH_3@bq(r6FiG(6dA#J2kbSSfQjSF*zHg7GW(S zgMcRz^NUhai$G(n;CVbyFC`Z=(_4^QlvBwC>X0a)brO)K8lf50N&%7oz+Ic1)FQ~# zEO`1FJgJz9G;NM>fesgFdKrYvGcuDiKwXkV1(>%W-a#0h0GexqPme-|onhrYbR^zN z!4oti3u^R(e4+pqR!Au=0#!+%VPsGT3d9Dv9XfapvLA#YW6_vn@+JA8u2eE)E*91? z&{Kdax5`h-vPu9AwI+ZD_CW@LFsR>?nOB+$pApwn2+v5(Q>e@@g_;H%`!_T&1O;FT zST)Re9gzFsp#yg;D1?ejlR>i&X{9-O3a&ZDsbHl@bG#rmiAniIC8;TpKms`j+oT0( zZXzjFAyEM|mkgRF2Ti^wDkSF@<(HOZ=A}Y(BZ3$*9*h~r5OE>{*%!+MMSc-97bqku zIi;zfoDE7S3JK8lCJ71fnJNWH zhZYivpd{c~qL7lBmROooQVa^ygaoC4%94!yJpBs2O1&z?x{qT02)#(9gakc>;8e)y zKxPSY%0^D`MX9;@WzY!PY%kDTF{46M@pb0!Rjw{}2HME{2Lzlk@XZiV?0w zwk0LA7_=e_Qusj^9zay)fLAwJDL_^^fh_EfRJL7C|R6=<=sU?&&(fm$kHXM;K@Mc@&TAdpxih|mWSO`ydQ zf{>sDEi5Bxs^^w4q^w6-%D~3Rz)&0sZajQvP-pdJ{0Jr=2-F0O}q<*anDo3gHfUpkX73ct9S!6&=Qw1|Bnx0j-b$ z%Lc$_S-@N{p~!&frWLS-C*;99Y5ELd40+sOMGOoKK_DJ{JQzm{9@KGwX@Js5z5*pb zuqdb{ugTSSQDg^77cC$Hl zJ7|!O6P^W&K#AxU2Q;M?--0Z|iZ4kl&MvYC*#TK=lmZ=k)MSSY&Ec{FY+NxYaG^^s zAnj#PEBqEeJi$TMS%4E;IXJO>(P0pkxh|q}Nkr$eh+YRv59bYWxgMSy@_JV|WNs)J zT;Y(tA**$TL+XZ%#uX098{C3jc6UT&u8XQ&5>>mPZgpAIy2I^;m`o4%2Q~&*Mt8;z zmLA>@Yz$^r9V|WkAJ`a7EVf&2w7hQYbOmM3msz+eE%YoHNYP#=<$HG=UY1BkpJ zYXo5+oCk6m2t#uV?`P031IT%xk^v-vj0G4%8B3WA7)qJkq!<_q1+nEK#9)RO17x1d zTLLu9kj>;R!@$7FAjeS3RKTdrP|73^W@R#HGFIK;3QjEnRep)dC7ET&YYaioJm26-tX!(@Jwdi}sT9bCWXjz~k_c z6_C*OejR*SEvWIX$y@|lNOg-9JosM(nps-O1WxL=xRBfrN+4>`L1%6x@moUBw2dUD z4~kzV5iVwB70#3F?1lIz95~(%F6R<%xDOTH$ z3{tGNGweQqs1+$-9weQDLI#!$;py}ZxI_;}9u{N7mL3Hd-0m_k6flJ`VVm&>tr&n9 zjcsP%B7%VdG8Y@f7{(L;FNq;KKqM$5f@mFh`Om;m!0g*j&^CBE4$=+5VA~WxgM&dJ z*W$4)h*<{|ZV+t@Xm)!Nv>RRvKn-AEU;x`K1hN~yKMRS zH6em>UT|h!aw=>!b{$L}RGH~P7Xv~Xh6xHO`KiU=VgR!ES|KwJv=j+c3?y^_I5{2UYoKkR?3tS4pj4MjbsZ_|%<0>f1EK4i_IR#}kx?_N6 zF{oQnl5fSurK>; z#I)p8E?vliHRvjJyukoklnu54YA~0sLLxN0Gg3i4kN2SDOK#zs>QKZswuntz_puSl^7)G6>>6*OTf$D6!P;FREzyIxxwYi zEq2JF_2OG>X^FX+IhD8AAOg489o^zR{aizCai^!2fW$!KbhkL5qGn+EP(ROzB6m6K-0Zo`{vVhB6i0LdKjkj1pT8lwtA7Z|brMxJy;1)N;M|vO*xSU!AZcBWSfvm(7 zk(%x|#jnHV2ETBBb!YXQwE1~6^RDpg+z^*rAaY&N@S>vOWkutw;>HtMZU~A_m!2rS zgl#$hLjEg)`ZvTC7MNUDHomBAd|BCaL()}c%d6s+$m;kO@?8(bg6rL`|h>s}Vuo4|5|N1)%k)B8G)%taoV3$msgO0MzPd=_UARfd$C zpr!{1gK`$glSCKrDc_2--_QLa81OD%C-)P*BsugjMS!g9)qF1zp<@Ao_p` zm5ozfOntTXj0H`qta|)Ekwgf2#R4s$$y%%xA{OnqGWu9R z7rroNp_l=mXT-MfB>=MUg`L6YDLaGDd9;N;UL=PKD`NGOHHm!&H)epA@4>*wDnbP6PECJQ#sksG^0YlJ|qN3EC)Uw3963|pHsH^2!qL5yi zSd^Gok_sLLOe_I62SS_!K>aT0gdV8L0_r7!<~Sfr(LsB~K%;M5Tz*v&pg}ND%L(KH zNCzqnyxb{7lkpZuYDIE9s5Y*WM0gK89-EU|qySN;DPANGs(3)d3Xn-PwDAV;_ynIb zXw^|pPAX*UR}pAd@fIt1fVK#<0vtSAaEmn+G|K|+e}NYy6-@$}4C;d2;sDwFM>D)h#ZnTU-~nyefQav`3=&q?#VtR`F$hRq=U2MO zuXLSX<08Mt7M9EWCJzLpt_!GK6i~S?pmk9|>xzKR4FRzadJKZ1p!G8|vMvc|e$Zy; z)w}^x2U8&?H9coy&H}5;VtUsF^ezbKec)mcSG+E!c2P|2vY5to0gVd+8Xshk4fz<% z$RJ_$k%dK6?8g@_1_^n{qN6ViOoCE3MC7iEXj~N0xGti1QAF>Gh`|jJ$vff-Ul^DL zrNNS_7e!RBi)ddI(Y~PLb|EnIf{6ANkub0nnm(!T(h%=n5V!mg#E9&K5BAK0(!aio zLgcTDTV4^j{Pj_XK~VoA7lWYYk1w1IBC=l?n0Or-!M;_zD4=*ebC`UMT=6Db!2 z)UOBx-w;!P_;X3+b#?oT>h_n#9IgvEToG{izzWj&`wI_)fXo*LCQiXeJOWSnB|uEg z2Rvfec@!@4C|u`JyU3$7Q3(Vdg!uoVl4Z3j|$gqP7niQx!8ql+NR5kU(# z751ZCAo3U!$iidHAoGv$F}ew|9OGy8P+~YH0@8I%nbA#!{g?`~n-=Rab&v_iwAkEP zIFA`Hx*78wGvorXZJ0o$9jAv9!*O;-cW#E`oFJ9QxgEJY%}a+-V47z&4hKZ3j~r1J))gym za?uU#;bj&>7nXt-^ss=(c5iV*JH_Au+Kr$v0*zZ?6m&&pps`zdXb%Zeih@cx$ewao z0SFF0P!-(3@Ii<{Rdc!LLeJ|emKRklcc@&q^1f*0eZuRqRrqC_Jc5}JwQ&s<(4405Jy^qQhbXgDL+4lgl^0XQ0#+RqM+CpW##+GAj-~OGS3%MRGONgqX1t80GeonPF8}}2!Qp1c9<7~hZ7K~r&v!RxF9t-Gc6Oe$*vN- zuU$(aC>1ms0~^i7y7HhHGWGzQgEuuWR?qg_*_S0~H#~ zuz?EV%Xh>)A9!aNsBDAeQ#gq&i^B>9d4?E}?O?Uo_H`*T#KDI|(D!P|GlVh1dRr;2 z&;khFg$5f5CIlElnM#@TKr=iZ&{1%hV7i28BfVqGPX{Qvp`-T)QSi0zn3}JBdu!IZNoq*W7^{{xrY6h0D zgon2|D7*=}ks$z!83CY?G(>=bSul6N!Un5*unjDM8Z%%M5JV6Y%oK2nL`c9{*s7@@ z7KAS79w$a0W9Wp42A@851|JV_mzTBbI+rJCVh(f=24uc0KMgi84=yr5 z!}Z|u7*y`Vk5qvVRwI`#380Z;P=$i1uDEawNarVkYYx!XrzcT3>*qGV{{?GG8(?FjPS|%R)>` z(G&ppMvE4L+Ek!vJ8+o^nP+D$&Ph!zK<}Y~=5E2IZ&5g?bmazZ&VdxeDYw|lKud*+ zi~2yOf$DnL$Tp~C29Md^;s&`U6+F{kj5@wuv=?L^FEl(MMuH1M(3B!*S?mWP22Nhc zN?M7F91<5KHLq}JePDwNN?zoUyuu;gXT@+TkF06A=SZ9mZ1!21{tPFxSQ`oNyC|?p#Uf^|I z#pI%j$%c^2DpuD7tZypme_&rB#_Q8K@FX6ctp(5O^ER*JENNb%TW$nHzCGj>}(*C zj}b%)fJl_dT2PY{JeCj2C!~m-d2`#a~zH9|noPfvmP&QD5TCeO3K5XD(geg%8)QUxF z^?-^2Xa)owa|3GLrs#2TfmdciTRTWu2egDG4!F65I;vHykXV!o&9LC&0Cjx< zqD2KBa4Q0za{yYA$rX~XkeXLqS_EEsnFyK-%qT4ZZNSq(?Rr5+h7^i3@=J446cTgF z6Dx}ql2W-6K~4k}N{J;3IjJBMKpQXe)2u+PbEq%sAUPJXdR7m7+>{lf-HIs9K+SM) z0gwV}YiE|gj#MJ)KqY_g1ZyrWSQ(9JJ;=?F;v*#imn*mwpy8QR7BlzSqP)V29l{X{gx~%?1S^dj=2G=shMePZnP2q+hbqEHAd^6Dg8GQ@ zGogYJImC;0fm{Ug6TZ=)@1U@S6^yK`8lW)MnBfAZ5Do!30)(Lsp)hDQ@dPbmS1fYi zg393{^uPrP-V%gH2|^#(M;O7YdXZoCGC!y)Mfi&3;4L~%OpyLV4pJUa|BHuJYXNw7 z9kk;GvltYl@VW*x`VTqegZPdYD0V@H5UO9QuHy-E$VeBcw#m#(2Zee_WdUfI0<<6u zb^;$rHnm8>Rsme5L&n%N^z`&}APdZ3JDF|W5_5`EHNj&F$_mhfffN!`QZhknND^}t zic?EU3luU^i&FLUAWN2&74-DBC{UUBL4pC+{8GQjr+%4F^E!v-6%I{ki72PZxPVceWg)vJ znI$4|35bV@fuR_dCj~$WNC1+61YkXPx+kE-YS0=b+$AD7eZ+%~Zz@YIA~6lYk_&he z5WcAcJ`V_~UcqTW8FX}1a%v8!O_2#&+n5j9I+OuBOaQC~HNAk2Muj;GlwQDHu}o06 zNFgz&B((^%$PsjSVZK6XG00TtLPxN75)=|43PBYY?7T|QcA3QrbY0yY%pm;^4+Iwt4|TF_}mg+v8Z`=OTUp`>$UFJYwf zTPW!qSt&SC!PB_{j&xph9sRU5Xpv7^-BTqFI}JA)v>++9L=Qp{Ug0YbO8bbqUKvyo zDPNE@0Mm$~8&v3lFeoE{rzAkVPn>l<$PXaZNElqlt3gUd&^|`^S*m)lLIQlYXfY@) z!>agMpwb_`76J+0f}Up!^<^<=Ou882i4Q#R8Xh!pd4WR-;R%rAK~4fWkMw>I==3{+ zO(!)_aKefbMpluJ42-NI3)H|A!XY3>fH24*&7jS;ps_JnU8Kwa>Z25a=6t}`p{Kc9 zT=DUVsi5s_@$vBL7$F390chnvsC>P_FWkZX6WnueVrE4xL_uSK;7|aqlmJB~w%#9T zsRe8c3FxGeokWD|}{XU@V58m&i$rz$3UQ`KkViky}1`#zNq83De zG9`F_Gx$7K@WT6|sUV3ZAObu$2$}_i_#8B(1nx^g8uy^EF9J0ria@1o5h&vqJpyTa z4I=751SojH*$NyY;N7%f;t8l;o|WT+6C-G2&j%6Eu4Pd$QwhXW1~XL{ zRJA|Awg#zbe1L5YQq}wp*%}nY$iT+;g$cCv;WIM>8{Y>o^Q)K?tL;|{6IQLSIgFs0 z-0xzdtbAYfv{<#iMlcGq8h+JcWmWsi$I7blm5+y2>#K|atJVh4q^`CiDeDqjpGX9ixUarj}r+Jh~o+p3{nabiW3MD4pI&h31SWs4Pw?8MC^dh zVaj1FVME#l%^oBr&H&v7&4IKFniFXkG*>KV33re}2@iO0vq}*t`DKC@VVAOrf#wWh zM`$9p9D{4q+zj+LTV=XIBM9=R?x0p@HBm59%A_xa$uo`18AfN zB^Aw(7sdXU>SIkIw(Y-DFAk;DA?sGuntvPQGPDcR#rXO2m@48W-jQ^4M;x- zat3-zY8q^)P6NKVO%rmIHfZN4s6Pd604sw!Sqk9BG~`HNP!xj7WpJv2m=9K144OHx zg&eM)nGYV+C_>s7r=bbkOaL(k)O-e61a>K8RvK)W1~le%6hIk8M*-w;BzHmE<0bh8 zTwIQbCU;Jy4tQxF+CUs+M=QLM401Zy64(xTP=h>$D;bnnb3iczn*c$CH!Og`+R%Hf z*qjOu3#cnW4dx%q=yZPG^CyzeC`P}f(46Wh(@%Hz#xsF*tAjrjVOayNJ$7CMW9m`QY#XZK?l*7Bxiuiw4&7X(ws!l#R3IIsl}i>I*P$*3S@&x>Xzy$c;+SNl%_xyd4NoUtUm!YV?pILI3hq>OcXNHAZ+OQ z8Hsrckgx>xMHjn9qLa?Rme+C&o2RuX{UgT=b}<*YCygy1e;~Z@<`U@rzxbQg6d3AZyau20elw_ z#2>|}IjPAd@knM^DS)<%gYH=YrwWJya5O6*sZvPH&Cg4R8(WeAstOPRkOVo88d{x$ z?mPe`JV@3+Jtrg)ZU!u!U{2n~;EHXaYC`B4@0Ud6_Di7k5&LV1946X`1nc8V@klnM4nOtJ=+DgJPdZ) z2&mqKpG5-N-v+*u2UJ+0`WEgp&`49}=qWJpbLv461;QgBd?BaeK(7G6Jmv+tZvhGs zNSQTyybH?lF5sJHV3+Jbb97lE=q`bh^3>Ek@F7>=%@_(Gl_VS_1K-5~DaJs$Vdt=* z9#;cbNU9A`e?U*P0e4cc+W?XR)u@QMZ4&$es+piRL3{$bey2FID%DECH?bl!w=`D) z!~-?<;0H58?{5Ozl2loe3UwkV5h9mj&;lD=T`7PsCqmc&zQD*z!6P$0L$^G!B(+E( zH?atO&KSr+Nu_CNsYRfb21Tg~X+?>-pzE2y7f^t1vjVMw0NDk?Xh#wmnn9I=*4m~j z1@stE}i8JX!BIw0dT zK~)pzk{?h(UYrWqtOSyU*aJ5T)J8$Iy;x7dGYx!rS2<|=6H6nJKA>kf1I{ zTnvRZu)*>Q(BRI@Q-H)K=u)iAVg-=*LCcZz;l6^lHQ*hUaFFrvBPaDh=W&8fjE9&A zxsj?k0o+7`1wP!cl6(bYqeBb4Go45dakv?XkOw!&&_f)o4s=XRY7s3@cv1j$;z}}* zx&-8&@B~WIphk8{D$-s8D+TD)g5VZ3D1YE?+k$7mkUL!<<%og-)N(2TH7!f>6%rLd zd(8_{i$JZ9L{MuSJPrm**pLGVGOG|iLKp!V>j1AS=W>MKY?hr`S)N~%0&mAc);7UY z5V$7##W|o{ z3GxvvVCssCbM%6PeVp^bXNQBEld0eqs6vQOFnA?9ni=`Y*$_jZm+XOV@F_}7t;mFI z7y$LNAv0x�pwC%B2A7Jwj!nu?MM)6BNLGjgoxO-8ISid1y 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 diff --git a/wip/SNIP/websockets/asyncio/async_timeout.py b/wip/SNIP/websockets/asyncio/async_timeout.py deleted file mode 100644 index 6ffa899..0000000 --- a/wip/SNIP/websockets/asyncio/async_timeout.py +++ /dev/null @@ -1,282 +0,0 @@ -# From https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py -# Licensed under the Apache License (Apache-2.0) - -import asyncio -import enum -import sys -import warnings -from types import TracebackType -from typing import Optional, Type - - -if sys.version_info >= (3, 11): - from typing import final -else: - # From https://github.com/python/typing_extensions/blob/main/src/typing_extensions.py - # Licensed under the Python Software Foundation License (PSF-2.0) - - # @final exists in 3.8+, but we backport it for all versions - # before 3.11 to keep support for the __final__ attribute. - # See https://bugs.python.org/issue46342 - def final(f): - """This decorator can be used to indicate to type checkers that - the decorated method cannot be overridden, and decorated class - cannot be subclassed. For example: - - class Base: - @final - def done(self) -> None: - ... - class Sub(Base): - def done(self) -> None: # Error reported by type checker - ... - @final - class Leaf: - ... - class Other(Leaf): # Error reported by type checker - ... - - There is no runtime checking of these properties. The decorator - sets the ``__final__`` attribute to ``True`` on the decorated object - to allow runtime introspection. - """ - try: - f.__final__ = True - except (AttributeError, TypeError): - # Skip the attribute silently if it is not writable. - # AttributeError happens if the object has __slots__ or a - # read-only property, TypeError if it's a builtin class. - pass - return f - - # End https://github.com/python/typing_extensions/blob/main/src/typing_extensions.py - - -if sys.version_info >= (3, 11): - - def _uncancel_task(task: "asyncio.Task[object]") -> None: - task.uncancel() - -else: - - def _uncancel_task(task: "asyncio.Task[object]") -> None: - pass - - -__version__ = "4.0.3" - - -__all__ = ("timeout", "timeout_at", "Timeout") - - -def timeout(delay: Optional[float]) -> "Timeout": - """timeout context manager. - - Useful in cases when you want to apply timeout logic around block - of code or in cases when asyncio.wait_for is not suitable. For example: - - >>> async with timeout(0.001): - ... async with aiohttp.get('https://github.com') as r: - ... await r.text() - - - delay - value in seconds or None to disable timeout logic - """ - loop = asyncio.get_running_loop() - if delay is not None: - deadline = loop.time() + delay # type: Optional[float] - else: - deadline = None - return Timeout(deadline, loop) - - -def timeout_at(deadline: Optional[float]) -> "Timeout": - """Schedule the timeout at absolute time. - - deadline argument points on the time in the same clock system - as loop.time(). - - Please note: it is not POSIX time but a time with - undefined starting base, e.g. the time of the system power on. - - >>> async with timeout_at(loop.time() + 10): - ... async with aiohttp.get('https://github.com') as r: - ... await r.text() - - - """ - loop = asyncio.get_running_loop() - return Timeout(deadline, loop) - - -class _State(enum.Enum): - INIT = "INIT" - ENTER = "ENTER" - TIMEOUT = "TIMEOUT" - EXIT = "EXIT" - - -@final -class Timeout: - # Internal class, please don't instantiate it directly - # Use timeout() and timeout_at() public factories instead. - # - # Implementation note: `async with timeout()` is preferred - # over `with timeout()`. - # While technically the Timeout class implementation - # doesn't need to be async at all, - # the `async with` statement explicitly points that - # the context manager should be used from async function context. - # - # This design allows to avoid many silly misusages. - # - # TimeoutError is raised immediately when scheduled - # if the deadline is passed. - # The purpose is to time out as soon as possible - # without waiting for the next await expression. - - __slots__ = ("_deadline", "_loop", "_state", "_timeout_handler", "_task") - - def __init__( - self, deadline: Optional[float], loop: asyncio.AbstractEventLoop - ) -> None: - self._loop = loop - self._state = _State.INIT - - self._task: Optional["asyncio.Task[object]"] = None - self._timeout_handler = None # type: Optional[asyncio.Handle] - if deadline is None: - self._deadline = None # type: Optional[float] - else: - self.update(deadline) - - def __enter__(self) -> "Timeout": - warnings.warn( - "with timeout() is deprecated, use async with timeout() instead", - DeprecationWarning, - stacklevel=2, - ) - self._do_enter() - return self - - def __exit__( - self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], - ) -> Optional[bool]: - self._do_exit(exc_type) - return None - - async def __aenter__(self) -> "Timeout": - self._do_enter() - return self - - async def __aexit__( - self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], - ) -> Optional[bool]: - self._do_exit(exc_type) - return None - - @property - def expired(self) -> bool: - """Is timeout expired during execution?""" - return self._state == _State.TIMEOUT - - @property - def deadline(self) -> Optional[float]: - return self._deadline - - def reject(self) -> None: - """Reject scheduled timeout if any.""" - # cancel is maybe better name but - # task.cancel() raises CancelledError in asyncio world. - if self._state not in (_State.INIT, _State.ENTER): - raise RuntimeError(f"invalid state {self._state.value}") - self._reject() - - def _reject(self) -> None: - self._task = None - if self._timeout_handler is not None: - self._timeout_handler.cancel() - self._timeout_handler = None - - def shift(self, delay: float) -> None: - """Advance timeout on delay seconds. - - The delay can be negative. - - Raise RuntimeError if shift is called when deadline is not scheduled - """ - deadline = self._deadline - if deadline is None: - raise RuntimeError("cannot shift timeout if deadline is not scheduled") - self.update(deadline + delay) - - def update(self, deadline: float) -> None: - """Set deadline to absolute value. - - deadline argument points on the time in the same clock system - as loop.time(). - - If new deadline is in the past the timeout is raised immediately. - - Please note: it is not POSIX time but a time with - undefined starting base, e.g. the time of the system power on. - """ - if self._state == _State.EXIT: - raise RuntimeError("cannot reschedule after exit from context manager") - if self._state == _State.TIMEOUT: - raise RuntimeError("cannot reschedule expired timeout") - if self._timeout_handler is not None: - self._timeout_handler.cancel() - self._deadline = deadline - if self._state != _State.INIT: - self._reschedule() - - def _reschedule(self) -> None: - assert self._state == _State.ENTER - deadline = self._deadline - if deadline is None: - return - - now = self._loop.time() - if self._timeout_handler is not None: - self._timeout_handler.cancel() - - self._task = asyncio.current_task() - if deadline <= now: - self._timeout_handler = self._loop.call_soon(self._on_timeout) - else: - self._timeout_handler = self._loop.call_at(deadline, self._on_timeout) - - def _do_enter(self) -> None: - if self._state != _State.INIT: - raise RuntimeError(f"invalid state {self._state.value}") - self._state = _State.ENTER - self._reschedule() - - def _do_exit(self, exc_type: Optional[Type[BaseException]]) -> None: - if exc_type is asyncio.CancelledError and self._state == _State.TIMEOUT: - assert self._task is not None - _uncancel_task(self._task) - self._timeout_handler = None - self._task = None - raise asyncio.TimeoutError - # timeout has not expired - self._state = _State.EXIT - self._reject() - return None - - def _on_timeout(self) -> None: - assert self._task is not None - self._task.cancel() - self._state = _State.TIMEOUT - # drop the reference early - self._timeout_handler = None - - -# End https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py diff --git a/wip/SNIP/websockets/asyncio/client.py b/wip/SNIP/websockets/asyncio/client.py deleted file mode 100644 index 38a56dd..0000000 --- a/wip/SNIP/websockets/asyncio/client.py +++ /dev/null @@ -1,820 +0,0 @@ -from __future__ import annotations - -import asyncio -import logging -import os -import socket -import ssl as ssl_module -import traceback -import urllib.parse -from collections.abc import AsyncIterator, Generator, Sequence -from types import TracebackType -from typing import Any, Callable, Literal, cast - -from ..client import ClientProtocol, backoff -from ..datastructures import Headers, HeadersLike -from ..exceptions import ( - InvalidMessage, - InvalidProxyMessage, - InvalidProxyStatus, - InvalidStatus, - ProxyError, - SecurityError, -) -from ..extensions.base import ClientExtensionFactory -from ..extensions.permessage_deflate import enable_client_permessage_deflate -from ..headers import build_authorization_basic, build_host, validate_subprotocols -from ..http11 import USER_AGENT, Response -from ..protocol import CONNECTING, Event -from ..streams import StreamReader -from ..typing import LoggerLike, Origin, Subprotocol -from ..uri import Proxy, WebSocketURI, get_proxy, parse_proxy, parse_uri -from .compatibility import TimeoutError, asyncio_timeout -from .connection import Connection - - -__all__ = ["connect", "unix_connect", "ClientConnection"] - -MAX_REDIRECTS = int(os.environ.get("WEBSOCKETS_MAX_REDIRECTS", "10")) - - -class ClientConnection(Connection): - """ - :mod:`asyncio` implementation of a WebSocket client connection. - - :class:`ClientConnection` provides :meth:`recv` and :meth:`send` coroutines - for receiving and sending messages. - - It supports asynchronous iteration to receive messages:: - - async for message in websocket: - await process(message) - - The iterator exits normally when the connection is closed with close code - 1000 (OK) or 1001 (going away) or without a close code. It raises a - :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is - closed with any other code. - - The ``ping_interval``, ``ping_timeout``, ``close_timeout``, ``max_queue``, - and ``write_limit`` arguments have the same meaning as in :func:`connect`. - - Args: - protocol: Sans-I/O connection. - - """ - - def __init__( - self, - protocol: ClientProtocol, - *, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = 10, - max_queue: int | None | tuple[int | None, int | None] = 16, - write_limit: int | tuple[int, int | None] = 2**15, - ) -> None: - self.protocol: ClientProtocol - super().__init__( - protocol, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - close_timeout=close_timeout, - max_queue=max_queue, - write_limit=write_limit, - ) - self.response_rcvd: asyncio.Future[None] = self.loop.create_future() - - async def handshake( - self, - additional_headers: HeadersLike | None = None, - user_agent_header: str | None = USER_AGENT, - ) -> None: - """ - Perform the opening handshake. - - """ - async with self.send_context(expected_state=CONNECTING): - self.request = self.protocol.connect() - if additional_headers is not None: - self.request.headers.update(additional_headers) - if user_agent_header is not None: - self.request.headers.setdefault("User-Agent", user_agent_header) - self.protocol.send_request(self.request) - - await asyncio.wait( - [self.response_rcvd, self.connection_lost_waiter], - return_when=asyncio.FIRST_COMPLETED, - ) - - # self.protocol.handshake_exc is set when the connection is lost before - # receiving a response, when the response cannot be parsed, or when the - # response fails the handshake. - - if self.protocol.handshake_exc is not None: - raise self.protocol.handshake_exc - - def process_event(self, event: Event) -> None: - """ - Process one incoming event. - - """ - # First event - handshake response. - if self.response is None: - assert isinstance(event, Response) - self.response = event - self.response_rcvd.set_result(None) - # Later events - frames. - else: - super().process_event(event) - - -def process_exception(exc: Exception) -> Exception | None: - """ - Determine whether a connection error is retryable or fatal. - - When reconnecting automatically with ``async for ... in connect(...)``, if a - connection attempt fails, :func:`process_exception` is called to determine - whether to retry connecting or to raise the exception. - - This function defines the default behavior, which is to retry on: - - * :exc:`EOFError`, :exc:`OSError`, :exc:`asyncio.TimeoutError`: network - errors; - * :exc:`~websockets.exceptions.InvalidStatus` when the status code is 500, - 502, 503, or 504: server or proxy errors. - - All other exceptions are considered fatal. - - You can change this behavior with the ``process_exception`` argument of - :func:`connect`. - - Return :obj:`None` if the exception is retryable i.e. when the error could - be transient and trying to reconnect with the same parameters could succeed. - The exception will be logged at the ``INFO`` level. - - Return an exception, either ``exc`` or a new exception, if the exception is - fatal i.e. when trying to reconnect will most likely produce the same error. - That exception will be raised, breaking out of the retry loop. - - """ - # This catches python-socks' ProxyConnectionError and ProxyTimeoutError. - # Remove asyncio.TimeoutError when dropping Python < 3.11. - if isinstance(exc, (OSError, TimeoutError, asyncio.TimeoutError)): - return None - if isinstance(exc, InvalidMessage) and isinstance(exc.__cause__, EOFError): - return None - if isinstance(exc, InvalidStatus) and exc.response.status_code in [ - 500, # Internal Server Error - 502, # Bad Gateway - 503, # Service Unavailable - 504, # Gateway Timeout - ]: - return None - return exc - - -# This is spelled in lower case because it's exposed as a callable in the API. -class connect: - """ - Connect to the WebSocket server at ``uri``. - - This coroutine returns a :class:`ClientConnection` instance, which you can - use to send and receive messages. - - :func:`connect` may be used as an asynchronous context manager:: - - from websockets.asyncio.client import connect - - async with connect(...) as websocket: - ... - - The connection is closed automatically when exiting the context. - - :func:`connect` can be used as an infinite asynchronous iterator to - reconnect automatically on errors:: - - async for websocket in connect(...): - try: - ... - except websockets.exceptions.ConnectionClosed: - continue - - If the connection fails with a transient error, it is retried with - exponential backoff. If it fails with a fatal error, the exception is - raised, breaking out of the loop. - - The connection is closed automatically after each iteration of the loop. - - Args: - uri: URI of the WebSocket server. - origin: Value of the ``Origin`` header, for servers that require it. - extensions: List of supported extensions, in order in which they - should be negotiated and run. - subprotocols: List of supported subprotocols, in order of decreasing - preference. - compression: The "permessage-deflate" extension is enabled by default. - Set ``compression`` to :obj:`None` to disable it. See the - :doc:`compression guide <../../topics/compression>` for details. - additional_headers (HeadersLike | None): Arbitrary HTTP headers to add - to the handshake request. - user_agent_header: Value of the ``User-Agent`` request header. - It defaults to ``"Python/x.y.z websockets/X.Y"``. - Setting it to :obj:`None` removes the header. - proxy: If a proxy is configured, it is used by default. Set ``proxy`` - to :obj:`None` to disable the proxy or to the address of a proxy - to override the system configuration. See the :doc:`proxy docs - <../../topics/proxies>` for details. - process_exception: When reconnecting automatically, tell whether an - error is transient or fatal. The default behavior is defined by - :func:`process_exception`. Refer to its documentation for details. - open_timeout: Timeout for opening the connection in seconds. - :obj:`None` disables the timeout. - ping_interval: Interval between keepalive pings in seconds. - :obj:`None` disables keepalive. - ping_timeout: Timeout for keepalive pings in seconds. - :obj:`None` disables timeouts. - close_timeout: Timeout for closing the connection in seconds. - :obj:`None` disables the timeout. - max_size: Maximum size of incoming messages in bytes. - :obj:`None` disables the limit. - max_queue: High-water mark of the buffer where frames are received. - It defaults to 16 frames. The low-water mark defaults to ``max_queue - // 4``. You may pass a ``(high, low)`` tuple to set the high-water - and low-water marks. If you want to disable flow control entirely, - you may set it to ``None``, although that's a bad idea. - write_limit: High-water mark of write buffer in bytes. It is passed to - :meth:`~asyncio.WriteTransport.set_write_buffer_limits`. It defaults - to 32 KiB. You may pass a ``(high, low)`` tuple to set the - high-water and low-water marks. - logger: Logger for this client. - It defaults to ``logging.getLogger("websockets.client")``. - See the :doc:`logging guide <../../topics/logging>` for details. - create_connection: Factory for the :class:`ClientConnection` managing - the connection. Set it to a wrapper or a subclass to customize - connection handling. - - Any other keyword arguments are passed to the event loop's - :meth:`~asyncio.loop.create_connection` method. - - For example: - - * You can set ``ssl`` to a :class:`~ssl.SSLContext` to enforce TLS settings. - When connecting to a ``wss://`` URI, if ``ssl`` isn't provided, a TLS - context is created with :func:`~ssl.create_default_context`. - - * You can set ``server_hostname`` to override the host name from ``uri`` in - the TLS handshake. - - * You can set ``host`` and ``port`` to connect to a different host and port - from those found in ``uri``. This only changes the destination of the TCP - connection. The host name from ``uri`` is still used in the TLS handshake - for secure connections and in the ``Host`` header. - - * You can set ``sock`` to provide a preexisting TCP socket. You may call - :func:`socket.create_connection` (not to be confused with the event loop's - :meth:`~asyncio.loop.create_connection` method) to create a suitable - client socket and customize it. - - When using a proxy: - - * Prefix keyword arguments with ``proxy_`` for configuring TLS between the - client and an HTTPS proxy: ``proxy_ssl``, ``proxy_server_hostname``, - ``proxy_ssl_handshake_timeout``, and ``proxy_ssl_shutdown_timeout``. - * Use the standard keyword arguments for configuring TLS between the proxy - and the WebSocket server: ``ssl``, ``server_hostname``, - ``ssl_handshake_timeout``, and ``ssl_shutdown_timeout``. - * Other keyword arguments are used only for connecting to the proxy. - - Raises: - InvalidURI: If ``uri`` isn't a valid WebSocket URI. - InvalidProxy: If ``proxy`` isn't a valid proxy. - OSError: If the TCP connection fails. - InvalidHandshake: If the opening handshake fails. - TimeoutError: If the opening handshake times out. - - """ - - def __init__( - self, - uri: str, - *, - # WebSocket - origin: Origin | None = None, - extensions: Sequence[ClientExtensionFactory] | None = None, - subprotocols: Sequence[Subprotocol] | None = None, - compression: str | None = "deflate", - # HTTP - additional_headers: HeadersLike | None = None, - user_agent_header: str | None = USER_AGENT, - proxy: str | Literal[True] | None = True, - process_exception: Callable[[Exception], Exception | None] = process_exception, - # Timeouts - open_timeout: float | None = 10, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = 10, - # Limits - max_size: int | None = 2**20, - max_queue: int | None | tuple[int | None, int | None] = 16, - write_limit: int | tuple[int, int | None] = 2**15, - # Logging - logger: LoggerLike | None = None, - # Escape hatch for advanced customization - create_connection: type[ClientConnection] | None = None, - # Other keyword arguments are passed to loop.create_connection - **kwargs: Any, - ) -> None: - self.uri = uri - - if subprotocols is not None: - validate_subprotocols(subprotocols) - - if compression == "deflate": - extensions = enable_client_permessage_deflate(extensions) - elif compression is not None: - raise ValueError(f"unsupported compression: {compression}") - - if logger is None: - logger = logging.getLogger("websockets.client") - - if create_connection is None: - create_connection = ClientConnection - - def protocol_factory(uri: WebSocketURI) -> ClientConnection: - # This is a protocol in the Sans-I/O implementation of websockets. - protocol = ClientProtocol( - uri, - origin=origin, - extensions=extensions, - subprotocols=subprotocols, - max_size=max_size, - logger=logger, - ) - # This is a connection in websockets and a protocol in asyncio. - connection = create_connection( - protocol, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - close_timeout=close_timeout, - max_queue=max_queue, - write_limit=write_limit, - ) - return connection - - self.proxy = proxy - self.protocol_factory = protocol_factory - self.additional_headers = additional_headers - self.user_agent_header = user_agent_header - self.process_exception = process_exception - self.open_timeout = open_timeout - self.logger = logger - self.connection_kwargs = kwargs - - async def create_connection(self) -> ClientConnection: - """Create TCP or Unix connection.""" - loop = asyncio.get_running_loop() - kwargs = self.connection_kwargs.copy() - - ws_uri = parse_uri(self.uri) - - proxy = self.proxy - if kwargs.get("unix", False): - proxy = None - if kwargs.get("sock") is not None: - proxy = None - if proxy is True: - proxy = get_proxy(ws_uri) - - def factory() -> ClientConnection: - return self.protocol_factory(ws_uri) - - if ws_uri.secure: - kwargs.setdefault("ssl", True) - kwargs.setdefault("server_hostname", ws_uri.host) - if kwargs.get("ssl") is None: - raise ValueError("ssl=None is incompatible with a wss:// URI") - else: - if kwargs.get("ssl") is not None: - raise ValueError("ssl argument is incompatible with a ws:// URI") - - if kwargs.pop("unix", False): - _, connection = await loop.create_unix_connection(factory, **kwargs) - elif proxy is not None: - proxy_parsed = parse_proxy(proxy) - if proxy_parsed.scheme[:5] == "socks": - # Connect to the server through the proxy. - sock = await connect_socks_proxy( - proxy_parsed, - ws_uri, - local_addr=kwargs.pop("local_addr", None), - ) - # Initialize WebSocket connection via the proxy. - _, connection = await loop.create_connection( - factory, - sock=sock, - **kwargs, - ) - elif proxy_parsed.scheme[:4] == "http": - # Split keyword arguments between the proxy and the server. - all_kwargs, proxy_kwargs, kwargs = kwargs, {}, {} - for key, value in all_kwargs.items(): - if key.startswith("ssl") or key == "server_hostname": - kwargs[key] = value - elif key.startswith("proxy_"): - proxy_kwargs[key[6:]] = value - else: - proxy_kwargs[key] = value - # Validate the proxy_ssl argument. - if proxy_parsed.scheme == "https": - proxy_kwargs.setdefault("ssl", True) - if proxy_kwargs.get("ssl") is None: - raise ValueError( - "proxy_ssl=None is incompatible with an https:// proxy" - ) - else: - if proxy_kwargs.get("ssl") is not None: - raise ValueError( - "proxy_ssl argument is incompatible with an http:// proxy" - ) - # Connect to the server through the proxy. - transport = await connect_http_proxy( - proxy_parsed, - ws_uri, - user_agent_header=self.user_agent_header, - **proxy_kwargs, - ) - # Initialize WebSocket connection via the proxy. - connection = factory() - transport.set_protocol(connection) - ssl = kwargs.pop("ssl", None) - if ssl is True: - ssl = ssl_module.create_default_context() - if ssl is not None: - new_transport = await loop.start_tls( - transport, connection, ssl, **kwargs - ) - assert new_transport is not None # help mypy - transport = new_transport - connection.connection_made(transport) - else: - raise AssertionError("unsupported proxy") - else: - # Connect to the server directly. - if kwargs.get("sock") is None: - kwargs.setdefault("host", ws_uri.host) - kwargs.setdefault("port", ws_uri.port) - # Initialize WebSocket connection. - _, connection = await loop.create_connection(factory, **kwargs) - return connection - - def process_redirect(self, exc: Exception) -> Exception | str: - """ - Determine whether a connection error is a redirect that can be followed. - - Return the new URI if it's a valid redirect. Else, return an exception. - - """ - if not ( - isinstance(exc, InvalidStatus) - and exc.response.status_code - in [ - 300, # Multiple Choices - 301, # Moved Permanently - 302, # Found - 303, # See Other - 307, # Temporary Redirect - 308, # Permanent Redirect - ] - and "Location" in exc.response.headers - ): - return exc - - old_ws_uri = parse_uri(self.uri) - new_uri = urllib.parse.urljoin(self.uri, exc.response.headers["Location"]) - new_ws_uri = parse_uri(new_uri) - - # If connect() received a socket, it is closed and cannot be reused. - if self.connection_kwargs.get("sock") is not None: - return ValueError( - f"cannot follow redirect to {new_uri} with a preexisting socket" - ) - - # TLS downgrade is forbidden. - if old_ws_uri.secure and not new_ws_uri.secure: - return SecurityError(f"cannot follow redirect to non-secure URI {new_uri}") - - # Apply restrictions to cross-origin redirects. - if ( - old_ws_uri.secure != new_ws_uri.secure - or old_ws_uri.host != new_ws_uri.host - or old_ws_uri.port != new_ws_uri.port - ): - # Cross-origin redirects on Unix sockets don't quite make sense. - if self.connection_kwargs.get("unix", False): - return ValueError( - f"cannot follow cross-origin redirect to {new_uri} " - f"with a Unix socket" - ) - - # Cross-origin redirects when host and port are overridden are ill-defined. - if ( - self.connection_kwargs.get("host") is not None - or self.connection_kwargs.get("port") is not None - ): - return ValueError( - f"cannot follow cross-origin redirect to {new_uri} " - f"with an explicit host or port" - ) - - return new_uri - - # ... = await connect(...) - - def __await__(self) -> Generator[Any, None, ClientConnection]: - # Create a suitable iterator by calling __await__ on a coroutine. - return self.__await_impl__().__await__() - - async def __await_impl__(self) -> ClientConnection: - try: - async with asyncio_timeout(self.open_timeout): - for _ in range(MAX_REDIRECTS): - self.connection = await self.create_connection() - try: - await self.connection.handshake( - self.additional_headers, - self.user_agent_header, - ) - except asyncio.CancelledError: - self.connection.transport.abort() - raise - except Exception as exc: - # Always close the connection even though keep-alive is - # the default in HTTP/1.1 because create_connection ties - # opening the network connection with initializing the - # protocol. In the current design of connect(), there is - # no easy way to reuse the network connection that works - # in every case nor to reinitialize the protocol. - self.connection.transport.abort() - - uri_or_exc = self.process_redirect(exc) - # Response is a valid redirect; follow it. - if isinstance(uri_or_exc, str): - self.uri = uri_or_exc - continue - # Response isn't a valid redirect; raise the exception. - if uri_or_exc is exc: - raise - else: - raise uri_or_exc from exc - - else: - self.connection.start_keepalive() - return self.connection - else: - raise SecurityError(f"more than {MAX_REDIRECTS} redirects") - - except TimeoutError as exc: - # Re-raise exception with an informative error message. - raise TimeoutError("timed out during opening handshake") from exc - - # ... = yield from connect(...) - remove when dropping Python < 3.10 - - __iter__ = __await__ - - # async with connect(...) as ...: ... - - async def __aenter__(self) -> ClientConnection: - return await self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - await self.connection.close() - - # async for ... in connect(...): - - async def __aiter__(self) -> AsyncIterator[ClientConnection]: - delays: Generator[float] | None = None - while True: - try: - async with self as protocol: - yield protocol - except Exception as exc: - # Determine whether the exception is retryable or fatal. - # The API of process_exception is "return an exception or None"; - # "raise an exception" is also supported because it's a frequent - # mistake. It isn't documented in order to keep the API simple. - try: - new_exc = self.process_exception(exc) - except Exception as raised_exc: - new_exc = raised_exc - - # The connection failed with a fatal error. - # Raise the exception and exit the loop. - if new_exc is exc: - raise - if new_exc is not None: - raise new_exc from exc - - # The connection failed with a retryable error. - # Start or continue backoff and reconnect. - if delays is None: - delays = backoff() - delay = next(delays) - self.logger.info( - "connect failed; reconnecting in %.1f seconds: %s", - delay, - # Remove first argument when dropping Python 3.9. - traceback.format_exception_only(type(exc), exc)[0].strip(), - ) - await asyncio.sleep(delay) - continue - - else: - # The connection succeeded. Reset backoff. - delays = None - - -def unix_connect( - path: str | None = None, - uri: str | None = None, - **kwargs: Any, -) -> connect: - """ - Connect to a WebSocket server listening on a Unix socket. - - This function accepts the same keyword arguments as :func:`connect`. - - It's only available on Unix. - - It's mainly useful for debugging servers listening on Unix sockets. - - Args: - path: File system path to the Unix socket. - uri: URI of the WebSocket server. ``uri`` defaults to - ``ws://localhost/`` or, when a ``ssl`` argument is provided, to - ``wss://localhost/``. - - """ - if uri is None: - if kwargs.get("ssl") is None: - uri = "ws://localhost/" - else: - uri = "wss://localhost/" - return connect(uri=uri, unix=True, path=path, **kwargs) - - -try: - from python_socks import ProxyType - from python_socks.async_.asyncio import Proxy as SocksProxy - - SOCKS_PROXY_TYPES = { - "socks5h": ProxyType.SOCKS5, - "socks5": ProxyType.SOCKS5, - "socks4a": ProxyType.SOCKS4, - "socks4": ProxyType.SOCKS4, - } - - SOCKS_PROXY_RDNS = { - "socks5h": True, - "socks5": False, - "socks4a": True, - "socks4": False, - } - - async def connect_socks_proxy( - proxy: Proxy, - ws_uri: WebSocketURI, - **kwargs: Any, - ) -> socket.socket: - """Connect via a SOCKS proxy and return the socket.""" - socks_proxy = SocksProxy( - SOCKS_PROXY_TYPES[proxy.scheme], - proxy.host, - proxy.port, - proxy.username, - proxy.password, - SOCKS_PROXY_RDNS[proxy.scheme], - ) - # connect() is documented to raise OSError. - # socks_proxy.connect() doesn't raise TimeoutError; it gets canceled. - # Wrap other exceptions in ProxyError, a subclass of InvalidHandshake. - try: - return await socks_proxy.connect(ws_uri.host, ws_uri.port, **kwargs) - except OSError: - raise - except Exception as exc: - raise ProxyError("failed to connect to SOCKS proxy") from exc - -except ImportError: - - async def connect_socks_proxy( - proxy: Proxy, - ws_uri: WebSocketURI, - **kwargs: Any, - ) -> socket.socket: - raise ImportError("python-socks is required to use a SOCKS proxy") - - -def prepare_connect_request( - proxy: Proxy, - ws_uri: WebSocketURI, - user_agent_header: str | None = None, -) -> bytes: - host = build_host(ws_uri.host, ws_uri.port, ws_uri.secure, always_include_port=True) - headers = Headers() - headers["Host"] = build_host(ws_uri.host, ws_uri.port, ws_uri.secure) - if user_agent_header is not None: - headers["User-Agent"] = user_agent_header - if proxy.username is not None: - assert proxy.password is not None # enforced by parse_proxy() - headers["Proxy-Authorization"] = build_authorization_basic( - proxy.username, proxy.password - ) - # We cannot use the Request class because it supports only GET requests. - return f"CONNECT {host} HTTP/1.1\r\n".encode() + headers.serialize() - - -class HTTPProxyConnection(asyncio.Protocol): - def __init__( - self, - ws_uri: WebSocketURI, - proxy: Proxy, - user_agent_header: str | None = None, - ): - self.ws_uri = ws_uri - self.proxy = proxy - self.user_agent_header = user_agent_header - - self.reader = StreamReader() - self.parser = Response.parse( - self.reader.read_line, - self.reader.read_exact, - self.reader.read_to_eof, - include_body=False, - ) - - loop = asyncio.get_running_loop() - self.response: asyncio.Future[Response] = loop.create_future() - - def run_parser(self) -> None: - try: - next(self.parser) - except StopIteration as exc: - response = exc.value - if 200 <= response.status_code < 300: - self.response.set_result(response) - else: - self.response.set_exception(InvalidProxyStatus(response)) - except Exception as exc: - proxy_exc = InvalidProxyMessage( - "did not receive a valid HTTP response from proxy" - ) - proxy_exc.__cause__ = exc - self.response.set_exception(proxy_exc) - - def connection_made(self, transport: asyncio.BaseTransport) -> None: - transport = cast(asyncio.Transport, transport) - self.transport = transport - self.transport.write( - prepare_connect_request(self.proxy, self.ws_uri, self.user_agent_header) - ) - - def data_received(self, data: bytes) -> None: - self.reader.feed_data(data) - self.run_parser() - - def eof_received(self) -> None: - self.reader.feed_eof() - self.run_parser() - - def connection_lost(self, exc: Exception | None) -> None: - self.reader.feed_eof() - if exc is not None: - self.response.set_exception(exc) - - -async def connect_http_proxy( - proxy: Proxy, - ws_uri: WebSocketURI, - user_agent_header: str | None = None, - **kwargs: Any, -) -> asyncio.Transport: - transport, protocol = await asyncio.get_running_loop().create_connection( - lambda: HTTPProxyConnection(ws_uri, proxy, user_agent_header), - proxy.host, - proxy.port, - **kwargs, - ) - - try: - # This raises exceptions if the connection to the proxy fails. - await protocol.response - except Exception: - transport.close() - raise - - return transport diff --git a/wip/SNIP/websockets/asyncio/compatibility.py b/wip/SNIP/websockets/asyncio/compatibility.py deleted file mode 100644 index e170000..0000000 --- a/wip/SNIP/websockets/asyncio/compatibility.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import annotations - -import sys - - -__all__ = ["TimeoutError", "aiter", "anext", "asyncio_timeout", "asyncio_timeout_at"] - - -if sys.version_info[:2] >= (3, 11): - TimeoutError = TimeoutError - aiter = aiter - anext = anext - from asyncio import ( - timeout as asyncio_timeout, # noqa: F401 - timeout_at as asyncio_timeout_at, # noqa: F401 - ) - -else: # Python < 3.11 - from asyncio import TimeoutError - - def aiter(async_iterable): - return type(async_iterable).__aiter__(async_iterable) - - async def anext(async_iterator): - return await type(async_iterator).__anext__(async_iterator) - - from .async_timeout import ( - timeout as asyncio_timeout, # noqa: F401 - timeout_at as asyncio_timeout_at, # noqa: F401 - ) diff --git a/wip/SNIP/websockets/asyncio/connection.py b/wip/SNIP/websockets/asyncio/connection.py deleted file mode 100644 index 1b51e47..0000000 --- a/wip/SNIP/websockets/asyncio/connection.py +++ /dev/null @@ -1,1237 +0,0 @@ -from __future__ import annotations - -import asyncio -import collections -import contextlib -import logging -import random -import struct -import sys -import traceback -import uuid -from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Iterable, Mapping -from types import TracebackType -from typing import Any, Literal, cast, overload - -from ..exceptions import ( - ConcurrencyError, - ConnectionClosed, - ConnectionClosedOK, - ProtocolError, -) -from ..frames import DATA_OPCODES, BytesLike, CloseCode, Frame, Opcode -from ..http11 import Request, Response -from ..protocol import CLOSED, OPEN, Event, Protocol, State -from ..typing import Data, LoggerLike, Subprotocol -from .compatibility import ( - TimeoutError, - aiter, - anext, - asyncio_timeout, - asyncio_timeout_at, -) -from .messages import Assembler - - -__all__ = ["Connection"] - - -class Connection(asyncio.Protocol): - """ - :mod:`asyncio` implementation of a WebSocket connection. - - :class:`Connection` provides APIs shared between WebSocket servers and - clients. - - You shouldn't use it directly. Instead, use - :class:`~websockets.asyncio.client.ClientConnection` or - :class:`~websockets.asyncio.server.ServerConnection`. - - """ - - def __init__( - self, - protocol: Protocol, - *, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = 10, - max_queue: int | None | tuple[int | None, int | None] = 16, - write_limit: int | tuple[int, int | None] = 2**15, - ) -> None: - self.protocol = protocol - self.ping_interval = ping_interval - self.ping_timeout = ping_timeout - self.close_timeout = close_timeout - if isinstance(max_queue, int) or max_queue is None: - max_queue = (max_queue, None) - self.max_queue = max_queue - if isinstance(write_limit, int): - write_limit = (write_limit, None) - self.write_limit = write_limit - - # Inject reference to this instance in the protocol's logger. - self.protocol.logger = logging.LoggerAdapter( - self.protocol.logger, - {"websocket": self}, - ) - - # Copy attributes from the protocol for convenience. - self.id: uuid.UUID = self.protocol.id - """Unique identifier of the connection. Useful in logs.""" - self.logger: LoggerLike = self.protocol.logger - """Logger for this connection.""" - self.debug = self.protocol.debug - - # HTTP handshake request and response. - self.request: Request | None = None - """Opening handshake request.""" - self.response: Response | None = None - """Opening handshake response.""" - - # Event loop running this connection. - self.loop = asyncio.get_running_loop() - - # Assembler turning frames into messages and serializing reads. - self.recv_messages: Assembler # initialized in connection_made - - # Deadline for the closing handshake. - self.close_deadline: float | None = None - - # Protect sending fragmented messages. - self.fragmented_send_waiter: asyncio.Future[None] | None = None - - # Mapping of ping IDs to pong waiters, in chronological order. - self.pong_waiters: dict[bytes, tuple[asyncio.Future[float], float]] = {} - - self.latency: float = 0 - """ - Latency of the connection, in seconds. - - Latency is defined as the round-trip time of the connection. It is - measured by sending a Ping frame and waiting for a matching Pong frame. - Before the first measurement, :attr:`latency` is ``0``. - - By default, websockets enables a :ref:`keepalive ` mechanism - that sends Ping frames automatically at regular intervals. You can also - send Ping frames and measure latency with :meth:`ping`. - """ - - # Task that sends keepalive pings. None when ping_interval is None. - self.keepalive_task: asyncio.Task[None] | None = None - - # Exception raised while reading from the connection, to be chained to - # ConnectionClosed in order to show why the TCP connection dropped. - self.recv_exc: BaseException | None = None - - # Completed when the TCP connection is closed and the WebSocket - # connection state becomes CLOSED. - self.connection_lost_waiter: asyncio.Future[None] = self.loop.create_future() - - # Adapted from asyncio.FlowControlMixin - self.paused: bool = False - self.drain_waiters: collections.deque[asyncio.Future[None]] = ( - collections.deque() - ) - - # Public attributes - - @property - def local_address(self) -> Any: - """ - Local address of the connection. - - For IPv4 connections, this is a ``(host, port)`` tuple. - - The format of the address depends on the address family. - See :meth:`~socket.socket.getsockname`. - - """ - return self.transport.get_extra_info("sockname") - - @property - def remote_address(self) -> Any: - """ - Remote address of the connection. - - For IPv4 connections, this is a ``(host, port)`` tuple. - - The format of the address depends on the address family. - See :meth:`~socket.socket.getpeername`. - - """ - return self.transport.get_extra_info("peername") - - @property - def state(self) -> State: - """ - State of the WebSocket connection, defined in :rfc:`6455`. - - This attribute is provided for completeness. Typical applications - shouldn't check its value. Instead, they should call :meth:`~recv` or - :meth:`send` and handle :exc:`~websockets.exceptions.ConnectionClosed` - exceptions. - - """ - return self.protocol.state - - @property - def subprotocol(self) -> Subprotocol | None: - """ - Subprotocol negotiated during the opening handshake. - - :obj:`None` if no subprotocol was negotiated. - - """ - return self.protocol.subprotocol - - @property - def close_code(self) -> int | None: - """ - State of the WebSocket connection, defined in :rfc:`6455`. - - This attribute is provided for completeness. Typical applications - shouldn't check its value. Instead, they should inspect attributes - of :exc:`~websockets.exceptions.ConnectionClosed` exceptions. - - """ - return self.protocol.close_code - - @property - def close_reason(self) -> str | None: - """ - State of the WebSocket connection, defined in :rfc:`6455`. - - This attribute is provided for completeness. Typical applications - shouldn't check its value. Instead, they should inspect attributes - of :exc:`~websockets.exceptions.ConnectionClosed` exceptions. - - """ - return self.protocol.close_reason - - # Public methods - - async def __aenter__(self) -> Connection: - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - if exc_type is None: - await self.close() - else: - await self.close(CloseCode.INTERNAL_ERROR) - - async def __aiter__(self) -> AsyncIterator[Data]: - """ - Iterate on incoming messages. - - The iterator calls :meth:`recv` and yields messages asynchronously in an - infinite loop. - - It exits when the connection is closed normally. It raises a - :exc:`~websockets.exceptions.ConnectionClosedError` exception after a - protocol error or a network failure. - - """ - try: - while True: - yield await self.recv() - except ConnectionClosedOK: - return - - @overload - async def recv(self, decode: Literal[True]) -> str: ... - - @overload - async def recv(self, decode: Literal[False]) -> bytes: ... - - @overload - async def recv(self, decode: bool | None = None) -> Data: ... - - async def recv(self, decode: bool | None = None) -> Data: - """ - Receive the next message. - - When the connection is closed, :meth:`recv` raises - :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it raises - :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal closure - and :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol - error or a network failure. This is how you detect the end of the - message stream. - - Canceling :meth:`recv` is safe. There's no risk of losing data. The next - invocation of :meth:`recv` will return the next message. - - This makes it possible to enforce a timeout by wrapping :meth:`recv` in - :func:`~asyncio.timeout` or :func:`~asyncio.wait_for`. - - When the message is fragmented, :meth:`recv` waits until all fragments - are received, reassembles them, and returns the whole message. - - Args: - decode: Set this flag to override the default behavior of returning - :class:`str` or :class:`bytes`. See below for details. - - Returns: - A string (:class:`str`) for a Text_ frame or a bytestring - (:class:`bytes`) for a Binary_ frame. - - .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - .. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - You may override this behavior with the ``decode`` argument: - - * Set ``decode=False`` to disable UTF-8 decoding of Text_ frames and - return a bytestring (:class:`bytes`). This improves performance - when decoding isn't needed, for example if the message contains - JSON and you're using a JSON library that expects a bytestring. - * Set ``decode=True`` to force UTF-8 decoding of Binary_ frames - and return a string (:class:`str`). This may be useful for - servers that send binary frames instead of text frames. - - Raises: - ConnectionClosed: When the connection is closed. - ConcurrencyError: If two coroutines call :meth:`recv` or - :meth:`recv_streaming` concurrently. - - """ - try: - return await self.recv_messages.get(decode) - except EOFError: - pass - # fallthrough - except ConcurrencyError: - raise ConcurrencyError( - "cannot call recv while another coroutine " - "is already running recv or recv_streaming" - ) from None - except UnicodeDecodeError as exc: - async with self.send_context(): - self.protocol.fail( - CloseCode.INVALID_DATA, - f"{exc.reason} at position {exc.start}", - ) - # fallthrough - - # Wait for the protocol state to be CLOSED before accessing close_exc. - await asyncio.shield(self.connection_lost_waiter) - raise self.protocol.close_exc from self.recv_exc - - @overload - def recv_streaming(self, decode: Literal[True]) -> AsyncIterator[str]: ... - - @overload - def recv_streaming(self, decode: Literal[False]) -> AsyncIterator[bytes]: ... - - @overload - def recv_streaming(self, decode: bool | None = None) -> AsyncIterator[Data]: ... - - async def recv_streaming(self, decode: bool | None = None) -> AsyncIterator[Data]: - """ - Receive the next message frame by frame. - - This method is designed for receiving fragmented messages. It returns an - asynchronous iterator that yields each fragment as it is received. This - iterator must be fully consumed. Else, future calls to :meth:`recv` or - :meth:`recv_streaming` will raise - :exc:`~websockets.exceptions.ConcurrencyError`, making the connection - unusable. - - :meth:`recv_streaming` raises the same exceptions as :meth:`recv`. - - Canceling :meth:`recv_streaming` before receiving the first frame is - safe. Canceling it after receiving one or more frames leaves the - iterator in a partially consumed state, making the connection unusable. - Instead, you should close the connection with :meth:`close`. - - Args: - decode: Set this flag to override the default behavior of returning - :class:`str` or :class:`bytes`. See below for details. - - Returns: - An iterator of strings (:class:`str`) for a Text_ frame or - bytestrings (:class:`bytes`) for a Binary_ frame. - - .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - .. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - You may override this behavior with the ``decode`` argument: - - * Set ``decode=False`` to disable UTF-8 decoding of Text_ frames - and return bytestrings (:class:`bytes`). This may be useful to - optimize performance when decoding isn't needed. - * Set ``decode=True`` to force UTF-8 decoding of Binary_ frames - and return strings (:class:`str`). This is useful for servers - that send binary frames instead of text frames. - - Raises: - ConnectionClosed: When the connection is closed. - ConcurrencyError: If two coroutines call :meth:`recv` or - :meth:`recv_streaming` concurrently. - - """ - try: - async for frame in self.recv_messages.get_iter(decode): - yield frame - return - except EOFError: - pass - # fallthrough - except ConcurrencyError: - raise ConcurrencyError( - "cannot call recv_streaming while another coroutine " - "is already running recv or recv_streaming" - ) from None - except UnicodeDecodeError as exc: - async with self.send_context(): - self.protocol.fail( - CloseCode.INVALID_DATA, - f"{exc.reason} at position {exc.start}", - ) - # fallthrough - - # Wait for the protocol state to be CLOSED before accessing close_exc. - await asyncio.shield(self.connection_lost_waiter) - raise self.protocol.close_exc from self.recv_exc - - async def send( - self, - message: Data | Iterable[Data] | AsyncIterable[Data], - text: bool | None = None, - ) -> None: - """ - Send a message. - - A string (:class:`str`) is sent as a Text_ frame. A bytestring or - bytes-like object (:class:`bytes`, :class:`bytearray`, or - :class:`memoryview`) is sent as a Binary_ frame. - - .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - .. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - You may override this behavior with the ``text`` argument: - - * Set ``text=True`` to send a bytestring or bytes-like object - (:class:`bytes`, :class:`bytearray`, or :class:`memoryview`) as a - Text_ frame. This improves performance when the message is already - UTF-8 encoded, for example if the message contains JSON and you're - using a JSON library that produces a bytestring. - * Set ``text=False`` to send a string (:class:`str`) in a Binary_ - frame. This may be useful for servers that expect binary frames - instead of text frames. - - :meth:`send` also accepts an iterable or an asynchronous iterable of - strings, bytestrings, or bytes-like objects to enable fragmentation_. - Each item is treated as a message fragment and sent in its own frame. - All items must be of the same type, or else :meth:`send` will raise a - :exc:`TypeError` and the connection will be closed. - - .. _fragmentation: https://datatracker.ietf.org/doc/html/rfc6455#section-5.4 - - :meth:`send` rejects dict-like objects because this is often an error. - (If you really want to send the keys of a dict-like object as fragments, - call its :meth:`~dict.keys` method and pass the result to :meth:`send`.) - - Canceling :meth:`send` is discouraged. Instead, you should close the - connection with :meth:`close`. Indeed, there are only two situations - where :meth:`send` may yield control to the event loop and then get - canceled; in both cases, :meth:`close` has the same effect and is - more clear: - - 1. The write buffer is full. If you don't want to wait until enough - data is sent, your only alternative is to close the connection. - :meth:`close` will likely time out then abort the TCP connection. - 2. ``message`` is an asynchronous iterator that yields control. - Stopping in the middle of a fragmented message will cause a - protocol error and the connection will be closed. - - When the connection is closed, :meth:`send` raises - :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it - raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal - connection closure and - :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol - error or a network failure. - - Args: - message: Message to send. - - Raises: - ConnectionClosed: When the connection is closed. - TypeError: If ``message`` doesn't have a supported type. - - """ - # While sending a fragmented message, prevent sending other messages - # until all fragments are sent. - while self.fragmented_send_waiter is not None: - await asyncio.shield(self.fragmented_send_waiter) - - # Unfragmented message -- this case must be handled first because - # strings and bytes-like objects are iterable. - - if isinstance(message, str): - async with self.send_context(): - if text is False: - self.protocol.send_binary(message.encode()) - else: - self.protocol.send_text(message.encode()) - - elif isinstance(message, BytesLike): - async with self.send_context(): - if text is True: - self.protocol.send_text(message) - else: - self.protocol.send_binary(message) - - # Catch a common mistake -- passing a dict to send(). - - elif isinstance(message, Mapping): - raise TypeError("data is a dict-like object") - - # Fragmented message -- regular iterator. - - elif isinstance(message, Iterable): - chunks = iter(message) - try: - chunk = next(chunks) - except StopIteration: - return - - assert self.fragmented_send_waiter is None - self.fragmented_send_waiter = self.loop.create_future() - try: - # First fragment. - if isinstance(chunk, str): - async with self.send_context(): - if text is False: - self.protocol.send_binary(chunk.encode(), fin=False) - else: - self.protocol.send_text(chunk.encode(), fin=False) - encode = True - elif isinstance(chunk, BytesLike): - async with self.send_context(): - if text is True: - self.protocol.send_text(chunk, fin=False) - else: - self.protocol.send_binary(chunk, fin=False) - encode = False - else: - raise TypeError("iterable must contain bytes or str") - - # Other fragments - for chunk in chunks: - if isinstance(chunk, str) and encode: - async with self.send_context(): - self.protocol.send_continuation(chunk.encode(), fin=False) - elif isinstance(chunk, BytesLike) and not encode: - async with self.send_context(): - self.protocol.send_continuation(chunk, fin=False) - else: - raise TypeError("iterable must contain uniform types") - - # Final fragment. - async with self.send_context(): - self.protocol.send_continuation(b"", fin=True) - - except Exception: - # We're half-way through a fragmented message and we can't - # complete it. This makes the connection unusable. - async with self.send_context(): - self.protocol.fail( - CloseCode.INTERNAL_ERROR, - "error in fragmented message", - ) - raise - - finally: - self.fragmented_send_waiter.set_result(None) - self.fragmented_send_waiter = None - - # Fragmented message -- async iterator. - - elif isinstance(message, AsyncIterable): - achunks = aiter(message) - try: - chunk = await anext(achunks) - except StopAsyncIteration: - return - - assert self.fragmented_send_waiter is None - self.fragmented_send_waiter = self.loop.create_future() - try: - # First fragment. - if isinstance(chunk, str): - if text is False: - async with self.send_context(): - self.protocol.send_binary(chunk.encode(), fin=False) - else: - async with self.send_context(): - self.protocol.send_text(chunk.encode(), fin=False) - encode = True - elif isinstance(chunk, BytesLike): - if text is True: - async with self.send_context(): - self.protocol.send_text(chunk, fin=False) - else: - async with self.send_context(): - self.protocol.send_binary(chunk, fin=False) - encode = False - else: - raise TypeError("async iterable must contain bytes or str") - - # Other fragments - async for chunk in achunks: - if isinstance(chunk, str) and encode: - async with self.send_context(): - self.protocol.send_continuation(chunk.encode(), fin=False) - elif isinstance(chunk, BytesLike) and not encode: - async with self.send_context(): - self.protocol.send_continuation(chunk, fin=False) - else: - raise TypeError("async iterable must contain uniform types") - - # Final fragment. - async with self.send_context(): - self.protocol.send_continuation(b"", fin=True) - - except Exception: - # We're half-way through a fragmented message and we can't - # complete it. This makes the connection unusable. - async with self.send_context(): - self.protocol.fail( - CloseCode.INTERNAL_ERROR, - "error in fragmented message", - ) - raise - - finally: - self.fragmented_send_waiter.set_result(None) - self.fragmented_send_waiter = None - - else: - raise TypeError("data must be str, bytes, iterable, or async iterable") - - async def close(self, code: int = 1000, reason: str = "") -> None: - """ - Perform the closing handshake. - - :meth:`close` waits for the other end to complete the handshake and - for the TCP connection to terminate. - - :meth:`close` is idempotent: it doesn't do anything once the - connection is closed. - - Args: - code: WebSocket close code. - reason: WebSocket close reason. - - """ - try: - # The context manager takes care of waiting for the TCP connection - # to terminate after calling a method that sends a close frame. - async with self.send_context(): - if self.fragmented_send_waiter is not None: - self.protocol.fail( - CloseCode.INTERNAL_ERROR, - "close during fragmented message", - ) - else: - self.protocol.send_close(code, reason) - except ConnectionClosed: - # Ignore ConnectionClosed exceptions raised from send_context(). - # They mean that the connection is closed, which was the goal. - pass - - async def wait_closed(self) -> None: - """ - Wait until the connection is closed. - - :meth:`wait_closed` waits for the closing handshake to complete and for - the TCP connection to terminate. - - """ - await asyncio.shield(self.connection_lost_waiter) - - async def ping(self, data: Data | None = None) -> Awaitable[float]: - """ - Send a Ping_. - - .. _Ping: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2 - - A ping may serve as a keepalive or as a check that the remote endpoint - received all messages up to this point - - Args: - data: Payload of the ping. A :class:`str` will be encoded to UTF-8. - If ``data`` is :obj:`None`, the payload is four random bytes. - - Returns: - A future that will be completed when the corresponding pong is - received. You can ignore it if you don't intend to wait. The result - of the future is the latency of the connection in seconds. - - :: - - pong_waiter = await ws.ping() - # only if you want to wait for the corresponding pong - latency = await pong_waiter - - Raises: - ConnectionClosed: When the connection is closed. - ConcurrencyError: If another ping was sent with the same data and - the corresponding pong wasn't received yet. - - """ - if isinstance(data, BytesLike): - data = bytes(data) - elif isinstance(data, str): - data = data.encode() - elif data is not None: - raise TypeError("data must be str or bytes-like") - - async with self.send_context(): - # Protect against duplicates if a payload is explicitly set. - if data in self.pong_waiters: - raise ConcurrencyError("already waiting for a pong with the same data") - - # Generate a unique random payload otherwise. - while data is None or data in self.pong_waiters: - data = struct.pack("!I", random.getrandbits(32)) - - pong_waiter = self.loop.create_future() - # The event loop's default clock is time.monotonic(). Its resolution - # is a bit low on Windows (~16ms). This is improved in Python 3.13. - self.pong_waiters[data] = (pong_waiter, self.loop.time()) - self.protocol.send_ping(data) - return pong_waiter - - async def pong(self, data: Data = b"") -> None: - """ - Send a Pong_. - - .. _Pong: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.3 - - An unsolicited pong may serve as a unidirectional heartbeat. - - Args: - data: Payload of the pong. A :class:`str` will be encoded to UTF-8. - - Raises: - ConnectionClosed: When the connection is closed. - - """ - if isinstance(data, BytesLike): - data = bytes(data) - elif isinstance(data, str): - data = data.encode() - else: - raise TypeError("data must be str or bytes-like") - - async with self.send_context(): - self.protocol.send_pong(data) - - # Private methods - - def process_event(self, event: Event) -> None: - """ - Process one incoming event. - - This method is overridden in subclasses to handle the handshake. - - """ - assert isinstance(event, Frame) - if event.opcode in DATA_OPCODES: - self.recv_messages.put(event) - - if event.opcode is Opcode.PONG: - self.acknowledge_pings(bytes(event.data)) - - def acknowledge_pings(self, data: bytes) -> None: - """ - Acknowledge pings when receiving a pong. - - """ - # Ignore unsolicited pong. - if data not in self.pong_waiters: - return - - pong_timestamp = self.loop.time() - - # Sending a pong for only the most recent ping is legal. - # Acknowledge all previous pings too in that case. - ping_id = None - ping_ids = [] - for ping_id, (pong_waiter, ping_timestamp) in self.pong_waiters.items(): - ping_ids.append(ping_id) - latency = pong_timestamp - ping_timestamp - if not pong_waiter.done(): - pong_waiter.set_result(latency) - if ping_id == data: - self.latency = latency - break - else: - raise AssertionError("solicited pong not found in pings") - - # Remove acknowledged pings from self.pong_waiters. - for ping_id in ping_ids: - del self.pong_waiters[ping_id] - - def abort_pings(self) -> None: - """ - Raise ConnectionClosed in pending pings. - - They'll never receive a pong once the connection is closed. - - """ - assert self.protocol.state is CLOSED - exc = self.protocol.close_exc - - for pong_waiter, _ping_timestamp in self.pong_waiters.values(): - if not pong_waiter.done(): - pong_waiter.set_exception(exc) - # If the exception is never retrieved, it will be logged when ping - # is garbage-collected. This is confusing for users. - # Given that ping is done (with an exception), canceling it does - # nothing, but it prevents logging the exception. - pong_waiter.cancel() - - self.pong_waiters.clear() - - async def keepalive(self) -> None: - """ - Send a Ping frame and wait for a Pong frame at regular intervals. - - """ - assert self.ping_interval is not None - latency = 0.0 - try: - while True: - # If self.ping_timeout > latency > self.ping_interval, - # pings will be sent immediately after receiving pongs. - # The period will be longer than self.ping_interval. - await asyncio.sleep(self.ping_interval - latency) - - # This cannot raise ConnectionClosed when the connection is - # closing because ping(), via send_context(), waits for the - # connection to be closed before raising ConnectionClosed. - # However, connection_lost() cancels keepalive_task before - # it gets a chance to resume excuting. - pong_waiter = await self.ping() - if self.debug: - self.logger.debug("% sent keepalive ping") - - if self.ping_timeout is not None: - try: - async with asyncio_timeout(self.ping_timeout): - # connection_lost cancels keepalive immediately - # after setting a ConnectionClosed exception on - # pong_waiter. A CancelledError is raised here, - # not a ConnectionClosed exception. - latency = await pong_waiter - self.logger.debug("% received keepalive pong") - except asyncio.TimeoutError: - if self.debug: - self.logger.debug("- timed out waiting for keepalive pong") - async with self.send_context(): - self.protocol.fail( - CloseCode.INTERNAL_ERROR, - "keepalive ping timeout", - ) - raise AssertionError( - "send_context() should wait for connection_lost(), " - "which cancels keepalive()" - ) - except Exception: - self.logger.error("keepalive ping failed", exc_info=True) - - def start_keepalive(self) -> None: - """ - Run :meth:`keepalive` in a task, unless keepalive is disabled. - - """ - if self.ping_interval is not None: - self.keepalive_task = self.loop.create_task(self.keepalive()) - - @contextlib.asynccontextmanager - async def send_context( - self, - *, - expected_state: State = OPEN, # CONNECTING during the opening handshake - ) -> AsyncIterator[None]: - """ - Create a context for writing to the connection from user code. - - On entry, :meth:`send_context` checks that the connection is open; on - exit, it writes outgoing data to the socket:: - - async with self.send_context(): - self.protocol.send_text(message.encode()) - - When the connection isn't open on entry, when the connection is expected - to close on exit, or when an unexpected error happens, terminating the - connection, :meth:`send_context` waits until the connection is closed - then raises :exc:`~websockets.exceptions.ConnectionClosed`. - - """ - # Should we wait until the connection is closed? - wait_for_close = False - # Should we close the transport and raise ConnectionClosed? - raise_close_exc = False - # What exception should we chain ConnectionClosed to? - original_exc: BaseException | None = None - - if self.protocol.state is expected_state: - # Let the caller interact with the protocol. - try: - yield - except (ProtocolError, ConcurrencyError): - # The protocol state wasn't changed. Exit immediately. - raise - except Exception as exc: - self.logger.error("unexpected internal error", exc_info=True) - # This branch should never run. It's a safety net in case of - # bugs. Since we don't know what happened, we will close the - # connection and raise the exception to the caller. - wait_for_close = False - raise_close_exc = True - original_exc = exc - else: - # Check if the connection is expected to close soon. - if self.protocol.close_expected(): - wait_for_close = True - # If the connection is expected to close soon, set the - # close deadline based on the close timeout. - # Since we tested earlier that protocol.state was OPEN - # (or CONNECTING), self.close_deadline is still None. - if self.close_timeout is not None: - assert self.close_deadline is None - self.close_deadline = self.loop.time() + self.close_timeout - # Write outgoing data to the socket and enforce flow control. - try: - self.send_data() - await self.drain() - except Exception as exc: - if self.debug: - self.logger.debug("! error while sending data", exc_info=True) - # While the only expected exception here is OSError, - # other exceptions would be treated identically. - wait_for_close = False - raise_close_exc = True - original_exc = exc - - else: # self.protocol.state is not expected_state - # Minor layering violation: we assume that the connection - # will be closing soon if it isn't in the expected state. - wait_for_close = True - # Calculate close_deadline if it wasn't set yet. - if self.close_timeout is not None: - if self.close_deadline is None: - self.close_deadline = self.loop.time() + self.close_timeout - raise_close_exc = True - - # If the connection is expected to close soon and the close timeout - # elapses, close the socket to terminate the connection. - if wait_for_close: - try: - async with asyncio_timeout_at(self.close_deadline): - await asyncio.shield(self.connection_lost_waiter) - except TimeoutError: - # There's no risk to overwrite another error because - # original_exc is never set when wait_for_close is True. - assert original_exc is None - original_exc = TimeoutError("timed out while closing connection") - # Set recv_exc before closing the transport in order to get - # proper exception reporting. - raise_close_exc = True - self.set_recv_exc(original_exc) - - # If an error occurred, close the transport to terminate the connection and - # raise an exception. - if raise_close_exc: - self.transport.abort() - # Wait for the protocol state to be CLOSED before accessing close_exc. - await asyncio.shield(self.connection_lost_waiter) - raise self.protocol.close_exc from original_exc - - def send_data(self) -> None: - """ - Send outgoing data. - - Raises: - OSError: When a socket operations fails. - - """ - for data in self.protocol.data_to_send(): - if data: - self.transport.write(data) - else: - # Half-close the TCP connection when possible i.e. no TLS. - if self.transport.can_write_eof(): - if self.debug: - self.logger.debug("x half-closing TCP connection") - # write_eof() doesn't document which exceptions it raises. - # OSError is plausible. uvloop can raise RuntimeError here. - try: - self.transport.write_eof() - except (OSError, RuntimeError): # pragma: no cover - pass - # Else, close the TCP connection. - else: # pragma: no cover - if self.debug: - self.logger.debug("x closing TCP connection") - self.transport.close() - - def set_recv_exc(self, exc: BaseException | None) -> None: - """ - Set recv_exc, if not set yet. - - """ - if self.recv_exc is None: - self.recv_exc = exc - - # asyncio.Protocol methods - - # Connection callbacks - - def connection_made(self, transport: asyncio.BaseTransport) -> None: - transport = cast(asyncio.Transport, transport) - self.recv_messages = Assembler( - *self.max_queue, - pause=transport.pause_reading, - resume=transport.resume_reading, - ) - transport.set_write_buffer_limits(*self.write_limit) - self.transport = transport - - def connection_lost(self, exc: Exception | None) -> None: - # Calling protocol.receive_eof() is safe because it's idempotent. - # This guarantees that the protocol state becomes CLOSED. - self.protocol.receive_eof() - assert self.protocol.state is CLOSED - - self.set_recv_exc(exc) - - # Abort recv() and pending pings with a ConnectionClosed exception. - self.recv_messages.close() - self.abort_pings() - - if self.keepalive_task is not None: - self.keepalive_task.cancel() - - # If self.connection_lost_waiter isn't pending, that's a bug, because: - # - it's set only here in connection_lost() which is called only once; - # - it must never be canceled. - self.connection_lost_waiter.set_result(None) - - # Adapted from asyncio.streams.FlowControlMixin - if self.paused: # pragma: no cover - self.paused = False - for waiter in self.drain_waiters: - if not waiter.done(): - if exc is None: - waiter.set_result(None) - else: - waiter.set_exception(exc) - - # Flow control callbacks - - def pause_writing(self) -> None: # pragma: no cover - # Adapted from asyncio.streams.FlowControlMixin - assert not self.paused - self.paused = True - - def resume_writing(self) -> None: # pragma: no cover - # Adapted from asyncio.streams.FlowControlMixin - assert self.paused - self.paused = False - for waiter in self.drain_waiters: - if not waiter.done(): - waiter.set_result(None) - - async def drain(self) -> None: # pragma: no cover - # We don't check if the connection is closed because we call drain() - # immediately after write() and write() would fail in that case. - - # Adapted from asyncio.streams.StreamWriter - # Yield to the event loop so that connection_lost() may be called. - if self.transport.is_closing(): - await asyncio.sleep(0) - - # Adapted from asyncio.streams.FlowControlMixin - if self.paused: - waiter = self.loop.create_future() - self.drain_waiters.append(waiter) - try: - await waiter - finally: - self.drain_waiters.remove(waiter) - - # Streaming protocol callbacks - - def data_received(self, data: bytes) -> None: - # Feed incoming data to the protocol. - self.protocol.receive_data(data) - - # This isn't expected to raise an exception. - events = self.protocol.events_received() - - # Write outgoing data to the transport. - try: - self.send_data() - except Exception as exc: - if self.debug: - self.logger.debug("! error while sending data", exc_info=True) - self.set_recv_exc(exc) - - if self.protocol.close_expected(): - # If the connection is expected to close soon, set the - # close deadline based on the close timeout. - if self.close_timeout is not None: - if self.close_deadline is None: - self.close_deadline = self.loop.time() + self.close_timeout - - for event in events: - # This isn't expected to raise an exception. - self.process_event(event) - - def eof_received(self) -> None: - # Feed the end of the data stream to the connection. - self.protocol.receive_eof() - - # This isn't expected to raise an exception. - events = self.protocol.events_received() - - # There is no error handling because send_data() can only write - # the end of the data stream here and it shouldn't raise errors. - self.send_data() - - # This code path is triggered when receiving an HTTP response - # without a Content-Length header. This is the only case where - # reading until EOF generates an event; all other events have - # a known length. Ignore for coverage measurement because tests - # are in test_client.py rather than test_connection.py. - for event in events: # pragma: no cover - # This isn't expected to raise an exception. - self.process_event(event) - - # The WebSocket protocol has its own closing handshake: endpoints close - # the TCP or TLS connection after sending and receiving a close frame. - # As a consequence, they never need to write after receiving EOF, so - # there's no reason to keep the transport open by returning True. - # Besides, that doesn't work on TLS connections. - - -# broadcast() is defined in the connection module even though it's primarily -# used by servers and documented in the server module because it works with -# client connections too and because it's easier to test together with the -# Connection class. - - -def broadcast( - connections: Iterable[Connection], - message: Data, - raise_exceptions: bool = False, -) -> None: - """ - Broadcast a message to several WebSocket connections. - - A string (:class:`str`) is sent as a Text_ frame. A bytestring or bytes-like - object (:class:`bytes`, :class:`bytearray`, or :class:`memoryview`) is sent - as a Binary_ frame. - - .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - .. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - :func:`broadcast` pushes the message synchronously to all connections even - if their write buffers are overflowing. There's no backpressure. - - If you broadcast messages faster than a connection can handle them, messages - will pile up in its write buffer until the connection times out. Keep - ``ping_interval`` and ``ping_timeout`` low to prevent excessive memory usage - from slow connections. - - Unlike :meth:`~websockets.asyncio.connection.Connection.send`, - :func:`broadcast` doesn't support sending fragmented messages. Indeed, - fragmentation is useful for sending large messages without buffering them in - memory, while :func:`broadcast` buffers one copy per connection as fast as - possible. - - :func:`broadcast` skips connections that aren't open in order to avoid - errors on connections where the closing handshake is in progress. - - :func:`broadcast` ignores failures to write the message on some connections. - It continues writing to other connections. On Python 3.11 and above, you may - set ``raise_exceptions`` to :obj:`True` to record failures and raise all - exceptions in a :pep:`654` :exc:`ExceptionGroup`. - - While :func:`broadcast` makes more sense for servers, it works identically - with clients, if you have a use case for opening connections to many servers - and broadcasting a message to them. - - Args: - websockets: WebSocket connections to which the message will be sent. - message: Message to send. - raise_exceptions: Whether to raise an exception in case of failures. - - Raises: - TypeError: If ``message`` doesn't have a supported type. - - """ - if isinstance(message, str): - send_method = "send_text" - message = message.encode() - elif isinstance(message, BytesLike): - send_method = "send_binary" - else: - raise TypeError("data must be str or bytes") - - if raise_exceptions: - if sys.version_info[:2] < (3, 11): # pragma: no cover - raise ValueError("raise_exceptions requires at least Python 3.11") - exceptions: list[Exception] = [] - - for connection in connections: - exception: Exception - - if connection.protocol.state is not OPEN: - continue - - if connection.fragmented_send_waiter is not None: - if raise_exceptions: - exception = ConcurrencyError("sending a fragmented message") - exceptions.append(exception) - else: - connection.logger.warning( - "skipped broadcast: sending a fragmented message", - ) - continue - - try: - # Call connection.protocol.send_text or send_binary. - # Either way, message is already converted to bytes. - getattr(connection.protocol, send_method)(message) - connection.send_data() - except Exception as write_exception: - if raise_exceptions: - exception = RuntimeError("failed to write message") - exception.__cause__ = write_exception - exceptions.append(exception) - else: - connection.logger.warning( - "skipped broadcast: failed to write message: %s", - traceback.format_exception_only( - # Remove first argument when dropping Python 3.9. - type(write_exception), - write_exception, - )[0].strip(), - ) - - if raise_exceptions and exceptions: - raise ExceptionGroup("skipped broadcast", exceptions) - - -# Pretend that broadcast is actually defined in the server module. -broadcast.__module__ = "websockets.asyncio.server" diff --git a/wip/SNIP/websockets/asyncio/messages.py b/wip/SNIP/websockets/asyncio/messages.py deleted file mode 100644 index 1fd4181..0000000 --- a/wip/SNIP/websockets/asyncio/messages.py +++ /dev/null @@ -1,314 +0,0 @@ -from __future__ import annotations - -import asyncio -import codecs -import collections -from collections.abc import AsyncIterator, Iterable -from typing import Any, Callable, Generic, Literal, TypeVar, overload - -from ..exceptions import ConcurrencyError -from ..frames import OP_BINARY, OP_CONT, OP_TEXT, Frame -from ..typing import Data - - -__all__ = ["Assembler"] - -UTF8Decoder = codecs.getincrementaldecoder("utf-8") - -T = TypeVar("T") - - -class SimpleQueue(Generic[T]): - """ - Simplified version of :class:`asyncio.Queue`. - - Provides only the subset of functionality needed by :class:`Assembler`. - - """ - - def __init__(self) -> None: - self.loop = asyncio.get_running_loop() - self.get_waiter: asyncio.Future[None] | None = None - self.queue: collections.deque[T] = collections.deque() - - def __len__(self) -> int: - return len(self.queue) - - def put(self, item: T) -> None: - """Put an item into the queue without waiting.""" - self.queue.append(item) - if self.get_waiter is not None and not self.get_waiter.done(): - self.get_waiter.set_result(None) - - async def get(self, block: bool = True) -> T: - """Remove and return an item from the queue, waiting if necessary.""" - if not self.queue: - if not block: - raise EOFError("stream of frames ended") - assert self.get_waiter is None, "cannot call get() concurrently" - self.get_waiter = self.loop.create_future() - try: - await self.get_waiter - finally: - self.get_waiter.cancel() - self.get_waiter = None - return self.queue.popleft() - - def reset(self, items: Iterable[T]) -> None: - """Put back items into an empty, idle queue.""" - assert self.get_waiter is None, "cannot reset() while get() is running" - assert not self.queue, "cannot reset() while queue isn't empty" - self.queue.extend(items) - - def abort(self) -> None: - """Close the queue, raising EOFError in get() if necessary.""" - if self.get_waiter is not None and not self.get_waiter.done(): - self.get_waiter.set_exception(EOFError("stream of frames ended")) - - -class Assembler: - """ - Assemble messages from frames. - - :class:`Assembler` expects only data frames. The stream of frames must - respect the protocol; if it doesn't, the behavior is undefined. - - Args: - pause: Called when the buffer of frames goes above the high water mark; - should pause reading from the network. - resume: Called when the buffer of frames goes below the low water mark; - should resume reading from the network. - - """ - - # coverage reports incorrectly: "line NN didn't jump to the function exit" - def __init__( # pragma: no cover - self, - high: int | None = None, - low: int | None = None, - pause: Callable[[], Any] = lambda: None, - resume: Callable[[], Any] = lambda: None, - ) -> None: - # Queue of incoming frames. - self.frames: SimpleQueue[Frame] = SimpleQueue() - - # We cannot put a hard limit on the size of the queue because a single - # call to Protocol.data_received() could produce thousands of frames, - # which must be buffered. Instead, we pause reading when the buffer goes - # above the high limit and we resume when it goes under the low limit. - if high is not None and low is None: - low = high // 4 - if high is None and low is not None: - high = low * 4 - if high is not None and low is not None: - if low < 0: - raise ValueError("low must be positive or equal to zero") - if high < low: - raise ValueError("high must be greater than or equal to low") - self.high, self.low = high, low - self.pause = pause - self.resume = resume - self.paused = False - - # This flag prevents concurrent calls to get() by user code. - self.get_in_progress = False - - # This flag marks the end of the connection. - self.closed = False - - @overload - async def get(self, decode: Literal[True]) -> str: ... - - @overload - async def get(self, decode: Literal[False]) -> bytes: ... - - @overload - async def get(self, decode: bool | None = None) -> Data: ... - - async def get(self, decode: bool | None = None) -> Data: - """ - Read the next message. - - :meth:`get` returns a single :class:`str` or :class:`bytes`. - - If the message is fragmented, :meth:`get` waits until the last frame is - received, then it reassembles the message and returns it. To receive - messages frame by frame, use :meth:`get_iter` instead. - - Args: - decode: :obj:`False` disables UTF-8 decoding of text frames and - returns :class:`bytes`. :obj:`True` forces UTF-8 decoding of - binary frames and returns :class:`str`. - - Raises: - EOFError: If the stream of frames has ended. - UnicodeDecodeError: If a text frame contains invalid UTF-8. - ConcurrencyError: If two coroutines run :meth:`get` or - :meth:`get_iter` concurrently. - - """ - if self.get_in_progress: - raise ConcurrencyError("get() or get_iter() is already running") - self.get_in_progress = True - - # Locking with get_in_progress prevents concurrent execution - # until get() fetches a complete message or is canceled. - - try: - # First frame - frame = await self.frames.get(not self.closed) - self.maybe_resume() - assert frame.opcode is OP_TEXT or frame.opcode is OP_BINARY - if decode is None: - decode = frame.opcode is OP_TEXT - frames = [frame] - - # Following frames, for fragmented messages - while not frame.fin: - try: - frame = await self.frames.get(not self.closed) - except asyncio.CancelledError: - # Put frames already received back into the queue - # so that future calls to get() can return them. - self.frames.reset(frames) - raise - self.maybe_resume() - assert frame.opcode is OP_CONT - frames.append(frame) - - finally: - self.get_in_progress = False - - data = b"".join(frame.data for frame in frames) - if decode: - return data.decode() - else: - return data - - @overload - def get_iter(self, decode: Literal[True]) -> AsyncIterator[str]: ... - - @overload - def get_iter(self, decode: Literal[False]) -> AsyncIterator[bytes]: ... - - @overload - def get_iter(self, decode: bool | None = None) -> AsyncIterator[Data]: ... - - async def get_iter(self, decode: bool | None = None) -> AsyncIterator[Data]: - """ - Stream the next message. - - Iterating the return value of :meth:`get_iter` asynchronously yields a - :class:`str` or :class:`bytes` for each frame in the message. - - The iterator must be fully consumed before calling :meth:`get_iter` or - :meth:`get` again. Else, :exc:`ConcurrencyError` is raised. - - This method only makes sense for fragmented messages. If messages aren't - fragmented, use :meth:`get` instead. - - Args: - decode: :obj:`False` disables UTF-8 decoding of text frames and - returns :class:`bytes`. :obj:`True` forces UTF-8 decoding of - binary frames and returns :class:`str`. - - Raises: - EOFError: If the stream of frames has ended. - UnicodeDecodeError: If a text frame contains invalid UTF-8. - ConcurrencyError: If two coroutines run :meth:`get` or - :meth:`get_iter` concurrently. - - """ - if self.get_in_progress: - raise ConcurrencyError("get() or get_iter() is already running") - self.get_in_progress = True - - # Locking with get_in_progress prevents concurrent execution - # until get_iter() fetches a complete message or is canceled. - - # If get_iter() raises an exception e.g. in decoder.decode(), - # get_in_progress remains set and the connection becomes unusable. - - # First frame - try: - frame = await self.frames.get(not self.closed) - except asyncio.CancelledError: - self.get_in_progress = False - raise - self.maybe_resume() - assert frame.opcode is OP_TEXT or frame.opcode is OP_BINARY - if decode is None: - decode = frame.opcode is OP_TEXT - if decode: - decoder = UTF8Decoder() - yield decoder.decode(frame.data, frame.fin) - else: - yield frame.data - - # Following frames, for fragmented messages - while not frame.fin: - # We cannot handle asyncio.CancelledError because we don't buffer - # previous fragments — we're streaming them. Canceling get_iter() - # here will leave the assembler in a stuck state. Future calls to - # get() or get_iter() will raise ConcurrencyError. - frame = await self.frames.get(not self.closed) - self.maybe_resume() - assert frame.opcode is OP_CONT - if decode: - yield decoder.decode(frame.data, frame.fin) - else: - yield frame.data - - self.get_in_progress = False - - def put(self, frame: Frame) -> None: - """ - Add ``frame`` to the next message. - - Raises: - EOFError: If the stream of frames has ended. - - """ - if self.closed: - raise EOFError("stream of frames ended") - - self.frames.put(frame) - self.maybe_pause() - - def maybe_pause(self) -> None: - """Pause the writer if queue is above the high water mark.""" - # Skip if flow control is disabled - if self.high is None: - return - - # Check for "> high" to support high = 0 - if len(self.frames) > self.high and not self.paused: - self.paused = True - self.pause() - - def maybe_resume(self) -> None: - """Resume the writer if queue is below the low water mark.""" - # Skip if flow control is disabled - if self.low is None: - return - - # Check for "<= low" to support low = 0 - if len(self.frames) <= self.low and self.paused: - self.paused = False - self.resume() - - def close(self) -> None: - """ - End the stream of frames. - - Calling :meth:`close` concurrently with :meth:`get`, :meth:`get_iter`, - or :meth:`put` is safe. They will raise :exc:`EOFError`. - - """ - if self.closed: - return - - self.closed = True - - # Unblock get() or get_iter(). - self.frames.abort() diff --git a/wip/SNIP/websockets/asyncio/router.py b/wip/SNIP/websockets/asyncio/router.py deleted file mode 100644 index 047e7ef..0000000 --- a/wip/SNIP/websockets/asyncio/router.py +++ /dev/null @@ -1,198 +0,0 @@ -from __future__ import annotations - -import http -import ssl as ssl_module -import urllib.parse -from typing import Any, Awaitable, Callable, Literal - -from werkzeug.exceptions import NotFound -from werkzeug.routing import Map, RequestRedirect - -from ..http11 import Request, Response -from .server import Server, ServerConnection, serve - - -__all__ = ["route", "unix_route", "Router"] - - -class Router: - """WebSocket router supporting :func:`route`.""" - - def __init__( - self, - url_map: Map, - server_name: str | None = None, - url_scheme: str = "ws", - ) -> None: - self.url_map = url_map - self.server_name = server_name - self.url_scheme = url_scheme - for rule in self.url_map.iter_rules(): - rule.websocket = True - - def get_server_name(self, connection: ServerConnection, request: Request) -> str: - if self.server_name is None: - return request.headers["Host"] - else: - return self.server_name - - def redirect(self, connection: ServerConnection, url: str) -> Response: - response = connection.respond(http.HTTPStatus.FOUND, f"Found at {url}") - response.headers["Location"] = url - return response - - def not_found(self, connection: ServerConnection) -> Response: - return connection.respond(http.HTTPStatus.NOT_FOUND, "Not Found") - - def route_request( - self, connection: ServerConnection, request: Request - ) -> Response | None: - """Route incoming request.""" - url_map_adapter = self.url_map.bind( - server_name=self.get_server_name(connection, request), - url_scheme=self.url_scheme, - ) - try: - parsed = urllib.parse.urlparse(request.path) - handler, kwargs = url_map_adapter.match( - path_info=parsed.path, - query_args=parsed.query, - ) - except RequestRedirect as redirect: - return self.redirect(connection, redirect.new_url) - except NotFound: - return self.not_found(connection) - connection.handler, connection.handler_kwargs = handler, kwargs - return None - - async def handler(self, connection: ServerConnection) -> None: - """Handle a connection.""" - return await connection.handler(connection, **connection.handler_kwargs) - - -def route( - url_map: Map, - *args: Any, - server_name: str | None = None, - ssl: ssl_module.SSLContext | Literal[True] | None = None, - create_router: type[Router] | None = None, - **kwargs: Any, -) -> Awaitable[Server]: - """ - Create a WebSocket server dispatching connections to different handlers. - - This feature requires the third-party library `werkzeug`_: - - .. code-block:: console - - $ pip install werkzeug - - .. _werkzeug: https://werkzeug.palletsprojects.com/ - - :func:`route` accepts the same arguments as - :func:`~websockets.sync.server.serve`, except as described below. - - The first argument is a :class:`werkzeug.routing.Map` that maps URL patterns - to connection handlers. In addition to the connection, handlers receive - parameters captured in the URL as keyword arguments. - - Here's an example:: - - - from websockets.asyncio.router import route - from werkzeug.routing import Map, Rule - - async def channel_handler(websocket, channel_id): - ... - - url_map = Map([ - Rule("/channel/", endpoint=channel_handler), - ... - ]) - - # set this future to exit the server - stop = asyncio.get_running_loop().create_future() - - async with route(url_map, ...) as server: - await stop - - - Refer to the documentation of :mod:`werkzeug.routing` for details. - - If you define redirects with ``Rule(..., redirect_to=...)`` in the URL map, - when the server runs behind a reverse proxy that modifies the ``Host`` - header or terminates TLS, you need additional configuration: - - * Set ``server_name`` to the name of the server as seen by clients. When not - provided, websockets uses the value of the ``Host`` header. - - * Set ``ssl=True`` to generate ``wss://`` URIs without actually enabling - TLS. Under the hood, this bind the URL map with a ``url_scheme`` of - ``wss://`` instead of ``ws://``. - - There is no need to specify ``websocket=True`` in each rule. It is added - automatically. - - Args: - url_map: Mapping of URL patterns to connection handlers. - server_name: Name of the server as seen by clients. If :obj:`None`, - websockets uses the value of the ``Host`` header. - ssl: Configuration for enabling TLS on the connection. Set it to - :obj:`True` if a reverse proxy terminates TLS connections. - create_router: Factory for the :class:`Router` dispatching requests to - handlers. Set it to a wrapper or a subclass to customize routing. - - """ - url_scheme = "ws" if ssl is None else "wss" - if ssl is not True and ssl is not None: - kwargs["ssl"] = ssl - - if create_router is None: - create_router = Router - - router = create_router(url_map, server_name, url_scheme) - - _process_request: ( - Callable[ - [ServerConnection, Request], - Awaitable[Response | None] | Response | None, - ] - | None - ) = kwargs.pop("process_request", None) - if _process_request is None: - process_request: Callable[ - [ServerConnection, Request], - Awaitable[Response | None] | Response | None, - ] = router.route_request - else: - - async def process_request( - connection: ServerConnection, request: Request - ) -> Response | None: - response = _process_request(connection, request) - if isinstance(response, Awaitable): - response = await response - if response is not None: - return response - return router.route_request(connection, request) - - return serve(router.handler, *args, process_request=process_request, **kwargs) - - -def unix_route( - url_map: Map, - path: str | None = None, - **kwargs: Any, -) -> Awaitable[Server]: - """ - Create a WebSocket Unix server dispatching connections to different handlers. - - :func:`unix_route` combines the behaviors of :func:`route` and - :func:`~websockets.asyncio.server.unix_serve`. - - Args: - url_map: Mapping of URL patterns to connection handlers. - path: File system path to the Unix socket. - - """ - return route(url_map, unix=True, path=path, **kwargs) diff --git a/wip/SNIP/websockets/asyncio/server.py b/wip/SNIP/websockets/asyncio/server.py deleted file mode 100644 index ec7fc43..0000000 --- a/wip/SNIP/websockets/asyncio/server.py +++ /dev/null @@ -1,981 +0,0 @@ -from __future__ import annotations - -import asyncio -import hmac -import http -import logging -import re -import socket -import sys -from collections.abc import Awaitable, Generator, Iterable, Sequence -from types import TracebackType -from typing import Any, Callable, Mapping, cast - -from ..exceptions import InvalidHeader -from ..extensions.base import ServerExtensionFactory -from ..extensions.permessage_deflate import enable_server_permessage_deflate -from ..frames import CloseCode -from ..headers import ( - build_www_authenticate_basic, - parse_authorization_basic, - validate_subprotocols, -) -from ..http11 import SERVER, Request, Response -from ..protocol import CONNECTING, OPEN, Event -from ..server import ServerProtocol -from ..typing import LoggerLike, Origin, StatusLike, Subprotocol -from .compatibility import asyncio_timeout -from .connection import Connection, broadcast - - -__all__ = [ - "broadcast", - "serve", - "unix_serve", - "ServerConnection", - "Server", - "basic_auth", -] - - -class ServerConnection(Connection): - """ - :mod:`asyncio` implementation of a WebSocket server connection. - - :class:`ServerConnection` provides :meth:`recv` and :meth:`send` methods for - receiving and sending messages. - - It supports asynchronous iteration to receive messages:: - - async for message in websocket: - await process(message) - - The iterator exits normally when the connection is closed with close code - 1000 (OK) or 1001 (going away) or without a close code. It raises a - :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is - closed with any other code. - - The ``ping_interval``, ``ping_timeout``, ``close_timeout``, ``max_queue``, - and ``write_limit`` arguments have the same meaning as in :func:`serve`. - - Args: - protocol: Sans-I/O connection. - server: Server that manages this connection. - - """ - - def __init__( - self, - protocol: ServerProtocol, - server: Server, - *, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = 10, - max_queue: int | None | tuple[int | None, int | None] = 16, - write_limit: int | tuple[int, int | None] = 2**15, - ) -> None: - self.protocol: ServerProtocol - super().__init__( - protocol, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - close_timeout=close_timeout, - max_queue=max_queue, - write_limit=write_limit, - ) - self.server = server - self.request_rcvd: asyncio.Future[None] = self.loop.create_future() - self.username: str # see basic_auth() - self.handler: Callable[[ServerConnection], Awaitable[None]] # see route() - self.handler_kwargs: Mapping[str, Any] # see route() - - def respond(self, status: StatusLike, text: str) -> Response: - """ - Create a plain text HTTP response. - - ``process_request`` and ``process_response`` may call this method to - return an HTTP response instead of performing the WebSocket opening - handshake. - - You can modify the response before returning it, for example by changing - HTTP headers. - - Args: - status: HTTP status code. - text: HTTP response body; it will be encoded to UTF-8. - - Returns: - HTTP response to send to the client. - - """ - return self.protocol.reject(status, text) - - async def handshake( - self, - process_request: ( - Callable[ - [ServerConnection, Request], - Awaitable[Response | None] | Response | None, - ] - | None - ) = None, - process_response: ( - Callable[ - [ServerConnection, Request, Response], - Awaitable[Response | None] | Response | None, - ] - | None - ) = None, - server_header: str | None = SERVER, - ) -> None: - """ - Perform the opening handshake. - - """ - await asyncio.wait( - [self.request_rcvd, self.connection_lost_waiter], - return_when=asyncio.FIRST_COMPLETED, - ) - - if self.request is not None: - async with self.send_context(expected_state=CONNECTING): - response = None - - if process_request is not None: - try: - response = process_request(self, self.request) - if isinstance(response, Awaitable): - response = await response - except Exception as exc: - self.protocol.handshake_exc = exc - response = self.protocol.reject( - http.HTTPStatus.INTERNAL_SERVER_ERROR, - ( - "Failed to open a WebSocket connection.\n" - "See server log for more information.\n" - ), - ) - - if response is None: - if self.server.is_serving(): - self.response = self.protocol.accept(self.request) - else: - self.response = self.protocol.reject( - http.HTTPStatus.SERVICE_UNAVAILABLE, - "Server is shutting down.\n", - ) - else: - assert isinstance(response, Response) # help mypy - self.response = response - - if server_header: - self.response.headers["Server"] = server_header - - response = None - - if process_response is not None: - try: - response = process_response(self, self.request, self.response) - if isinstance(response, Awaitable): - response = await response - except Exception as exc: - self.protocol.handshake_exc = exc - response = self.protocol.reject( - http.HTTPStatus.INTERNAL_SERVER_ERROR, - ( - "Failed to open a WebSocket connection.\n" - "See server log for more information.\n" - ), - ) - - if response is not None: - assert isinstance(response, Response) # help mypy - self.response = response - - self.protocol.send_response(self.response) - - # self.protocol.handshake_exc is set when the connection is lost before - # receiving a request, when the request cannot be parsed, or when the - # handshake fails, including when process_request or process_response - # raises an exception. - - # It isn't set when process_request or process_response sends an HTTP - # response that rejects the handshake. - - if self.protocol.handshake_exc is not None: - raise self.protocol.handshake_exc - - def process_event(self, event: Event) -> None: - """ - Process one incoming event. - - """ - # First event - handshake request. - if self.request is None: - assert isinstance(event, Request) - self.request = event - self.request_rcvd.set_result(None) - # Later events - frames. - else: - super().process_event(event) - - def connection_made(self, transport: asyncio.BaseTransport) -> None: - super().connection_made(transport) - self.server.start_connection_handler(self) - - -class Server: - """ - WebSocket server returned by :func:`serve`. - - This class mirrors the API of :class:`asyncio.Server`. - - It keeps track of WebSocket connections in order to close them properly - when shutting down. - - Args: - handler: Connection handler. It receives the WebSocket connection, - which is a :class:`ServerConnection`, in argument. - process_request: Intercept the request during the opening handshake. - Return an HTTP response to force the response. Return :obj:`None` to - continue normally. When you force an HTTP 101 Continue response, the - handshake is successful. Else, the connection is aborted. - ``process_request`` may be a function or a coroutine. - process_response: Intercept the response during the opening handshake. - Modify the response or return a new HTTP response to force the - response. Return :obj:`None` to continue normally. When you force an - HTTP 101 Continue response, the handshake is successful. Else, the - connection is aborted. ``process_response`` may be a function or a - coroutine. - server_header: Value of the ``Server`` response header. - It defaults to ``"Python/x.y.z websockets/X.Y"``. Setting it to - :obj:`None` removes the header. - open_timeout: Timeout for opening connections in seconds. - :obj:`None` disables the timeout. - logger: Logger for this server. - It defaults to ``logging.getLogger("websockets.server")``. - See the :doc:`logging guide <../../topics/logging>` for details. - - """ - - def __init__( - self, - handler: Callable[[ServerConnection], Awaitable[None]], - *, - process_request: ( - Callable[ - [ServerConnection, Request], - Awaitable[Response | None] | Response | None, - ] - | None - ) = None, - process_response: ( - Callable[ - [ServerConnection, Request, Response], - Awaitable[Response | None] | Response | None, - ] - | None - ) = None, - server_header: str | None = SERVER, - open_timeout: float | None = 10, - logger: LoggerLike | None = None, - ) -> None: - self.loop = asyncio.get_running_loop() - self.handler = handler - self.process_request = process_request - self.process_response = process_response - self.server_header = server_header - self.open_timeout = open_timeout - if logger is None: - logger = logging.getLogger("websockets.server") - self.logger = logger - - # Keep track of active connections. - self.handlers: dict[ServerConnection, asyncio.Task[None]] = {} - - # Task responsible for closing the server and terminating connections. - self.close_task: asyncio.Task[None] | None = None - - # Completed when the server is closed and connections are terminated. - self.closed_waiter: asyncio.Future[None] = self.loop.create_future() - - @property - def connections(self) -> set[ServerConnection]: - """ - Set of active connections. - - This property contains all connections that completed the opening - handshake successfully and didn't start the closing handshake yet. - It can be useful in combination with :func:`~broadcast`. - - """ - return {connection for connection in self.handlers if connection.state is OPEN} - - def wrap(self, server: asyncio.Server) -> None: - """ - Attach to a given :class:`asyncio.Server`. - - Since :meth:`~asyncio.loop.create_server` doesn't support injecting a - custom ``Server`` class, the easiest solution that doesn't rely on - private :mod:`asyncio` APIs is to: - - - instantiate a :class:`Server` - - give the protocol factory a reference to that instance - - call :meth:`~asyncio.loop.create_server` with the factory - - attach the resulting :class:`asyncio.Server` with this method - - """ - self.server = server - for sock in server.sockets: - if sock.family == socket.AF_INET: - name = "%s:%d" % sock.getsockname() - elif sock.family == socket.AF_INET6: - name = "[%s]:%d" % sock.getsockname()[:2] - elif sock.family == socket.AF_UNIX: - name = sock.getsockname() - # In the unlikely event that someone runs websockets over a - # protocol other than IP or Unix sockets, avoid crashing. - else: # pragma: no cover - name = str(sock.getsockname()) - self.logger.info("server listening on %s", name) - - async def conn_handler(self, connection: ServerConnection) -> None: - """ - Handle the lifecycle of a WebSocket connection. - - Since this method doesn't have a caller that can handle exceptions, - it attempts to log relevant ones. - - It guarantees that the TCP connection is closed before exiting. - - """ - try: - async with asyncio_timeout(self.open_timeout): - try: - await connection.handshake( - self.process_request, - self.process_response, - self.server_header, - ) - except asyncio.CancelledError: - connection.transport.abort() - raise - except Exception: - connection.logger.error("opening handshake failed", exc_info=True) - connection.transport.abort() - return - - if connection.protocol.state is not OPEN: - # process_request or process_response rejected the handshake. - connection.transport.abort() - return - - try: - connection.start_keepalive() - await self.handler(connection) - except Exception: - connection.logger.error("connection handler failed", exc_info=True) - await connection.close(CloseCode.INTERNAL_ERROR) - else: - await connection.close() - - except TimeoutError: - # When the opening handshake times out, there's nothing to log. - pass - - except Exception: # pragma: no cover - # Don't leak connections on unexpected errors. - connection.transport.abort() - - finally: - # Registration is tied to the lifecycle of conn_handler() because - # the server waits for connection handlers to terminate, even if - # all connections are already closed. - del self.handlers[connection] - - def start_connection_handler(self, connection: ServerConnection) -> None: - """ - Register a connection with this server. - - """ - # The connection must be registered in self.handlers immediately. - # If it was registered in conn_handler(), a race condition could - # happen when closing the server after scheduling conn_handler() - # but before it starts executing. - self.handlers[connection] = self.loop.create_task(self.conn_handler(connection)) - - def close(self, close_connections: bool = True) -> None: - """ - Close the server. - - * Close the underlying :class:`asyncio.Server`. - * When ``close_connections`` is :obj:`True`, which is the default, - close existing connections. Specifically: - - * Reject opening WebSocket connections with an HTTP 503 (service - unavailable) error. This happens when the server accepted the TCP - connection but didn't complete the opening handshake before closing. - * Close open WebSocket connections with close code 1001 (going away). - - * Wait until all connection handlers terminate. - - :meth:`close` is idempotent. - - """ - if self.close_task is None: - self.close_task = self.get_loop().create_task( - self._close(close_connections) - ) - - async def _close(self, close_connections: bool) -> None: - """ - Implementation of :meth:`close`. - - This calls :meth:`~asyncio.Server.close` on the underlying - :class:`asyncio.Server` object to stop accepting new connections and - then closes open connections with close code 1001. - - """ - self.logger.info("server closing") - - # Stop accepting new connections. - self.server.close() - - # Wait until all accepted connections reach connection_made() and call - # register(). See https://github.com/python/cpython/issues/79033 for - # details. This workaround can be removed when dropping Python < 3.11. - await asyncio.sleep(0) - - if close_connections: - # Close OPEN connections with close code 1001. After server.close(), - # handshake() closes OPENING connections with an HTTP 503 error. - close_tasks = [ - asyncio.create_task(connection.close(1001)) - for connection in self.handlers - if connection.protocol.state is not CONNECTING - ] - # asyncio.wait doesn't accept an empty first argument. - if close_tasks: - await asyncio.wait(close_tasks) - - # Wait until all TCP connections are closed. - await self.server.wait_closed() - - # Wait until all connection handlers terminate. - # asyncio.wait doesn't accept an empty first argument. - if self.handlers: - await asyncio.wait(self.handlers.values()) - - # Tell wait_closed() to return. - self.closed_waiter.set_result(None) - - self.logger.info("server closed") - - async def wait_closed(self) -> None: - """ - Wait until the server is closed. - - When :meth:`wait_closed` returns, all TCP connections are closed and - all connection handlers have returned. - - To ensure a fast shutdown, a connection handler should always be - awaiting at least one of: - - * :meth:`~ServerConnection.recv`: when the connection is closed, - it raises :exc:`~websockets.exceptions.ConnectionClosedOK`; - * :meth:`~ServerConnection.wait_closed`: when the connection is - closed, it returns. - - Then the connection handler is immediately notified of the shutdown; - it can clean up and exit. - - """ - await asyncio.shield(self.closed_waiter) - - def get_loop(self) -> asyncio.AbstractEventLoop: - """ - See :meth:`asyncio.Server.get_loop`. - - """ - return self.server.get_loop() - - def is_serving(self) -> bool: # pragma: no cover - """ - See :meth:`asyncio.Server.is_serving`. - - """ - return self.server.is_serving() - - async def start_serving(self) -> None: # pragma: no cover - """ - See :meth:`asyncio.Server.start_serving`. - - Typical use:: - - server = await serve(..., start_serving=False) - # perform additional setup here... - # ... then start the server - await server.start_serving() - - """ - await self.server.start_serving() - - async def serve_forever(self) -> None: # pragma: no cover - """ - See :meth:`asyncio.Server.serve_forever`. - - Typical use:: - - server = await serve(...) - # this coroutine doesn't return - # canceling it stops the server - await server.serve_forever() - - This is an alternative to using :func:`serve` as an asynchronous context - manager. Shutdown is triggered by canceling :meth:`serve_forever` - instead of exiting a :func:`serve` context. - - """ - await self.server.serve_forever() - - @property - def sockets(self) -> Iterable[socket.socket]: - """ - See :attr:`asyncio.Server.sockets`. - - """ - return self.server.sockets - - async def __aenter__(self) -> Server: # pragma: no cover - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: # pragma: no cover - self.close() - await self.wait_closed() - - -# This is spelled in lower case because it's exposed as a callable in the API. -class serve: - """ - Create a WebSocket server listening on ``host`` and ``port``. - - Whenever a client connects, the server creates a :class:`ServerConnection`, - performs the opening handshake, and delegates to the ``handler`` coroutine. - - The handler receives the :class:`ServerConnection` instance, which you can - use to send and receive messages. - - Once the handler completes, either normally or with an exception, the server - performs the closing handshake and closes the connection. - - This coroutine returns a :class:`Server` whose API mirrors - :class:`asyncio.Server`. Treat it as an asynchronous context manager to - ensure that the server will be closed:: - - from websockets.asyncio.server import serve - - def handler(websocket): - ... - - # set this future to exit the server - stop = asyncio.get_running_loop().create_future() - - async with serve(handler, host, port): - await stop - - Alternatively, call :meth:`~Server.serve_forever` to serve requests and - cancel it to stop the server:: - - server = await serve(handler, host, port) - await server.serve_forever() - - Args: - handler: Connection handler. It receives the WebSocket connection, - which is a :class:`ServerConnection`, in argument. - host: Network interfaces the server binds to. - See :meth:`~asyncio.loop.create_server` for details. - port: TCP port the server listens on. - See :meth:`~asyncio.loop.create_server` for details. - origins: Acceptable values of the ``Origin`` header, for defending - against Cross-Site WebSocket Hijacking attacks. Values can be - :class:`str` to test for an exact match or regular expressions - compiled by :func:`re.compile` to test against a pattern. Include - :obj:`None` in the list if the lack of an origin is acceptable. - extensions: List of supported extensions, in order in which they - should be negotiated and run. - subprotocols: List of supported subprotocols, in order of decreasing - preference. - select_subprotocol: Callback for selecting a subprotocol among - those supported by the client and the server. It receives a - :class:`ServerConnection` (not a - :class:`~websockets.server.ServerProtocol`!) instance and a list of - subprotocols offered by the client. Other than the first argument, - it has the same behavior as the - :meth:`ServerProtocol.select_subprotocol - ` method. - compression: The "permessage-deflate" extension is enabled by default. - Set ``compression`` to :obj:`None` to disable it. See the - :doc:`compression guide <../../topics/compression>` for details. - process_request: Intercept the request during the opening handshake. - Return an HTTP response to force the response or :obj:`None` to - continue normally. When you force an HTTP 101 Continue response, the - handshake is successful. Else, the connection is aborted. - ``process_request`` may be a function or a coroutine. - process_response: Intercept the response during the opening handshake. - Return an HTTP response to force the response or :obj:`None` to - continue normally. When you force an HTTP 101 Continue response, the - handshake is successful. Else, the connection is aborted. - ``process_response`` may be a function or a coroutine. - server_header: Value of the ``Server`` response header. - It defaults to ``"Python/x.y.z websockets/X.Y"``. Setting it to - :obj:`None` removes the header. - open_timeout: Timeout for opening connections in seconds. - :obj:`None` disables the timeout. - ping_interval: Interval between keepalive pings in seconds. - :obj:`None` disables keepalive. - ping_timeout: Timeout for keepalive pings in seconds. - :obj:`None` disables timeouts. - close_timeout: Timeout for closing connections in seconds. - :obj:`None` disables the timeout. - max_size: Maximum size of incoming messages in bytes. - :obj:`None` disables the limit. - max_queue: High-water mark of the buffer where frames are received. - It defaults to 16 frames. The low-water mark defaults to ``max_queue - // 4``. You may pass a ``(high, low)`` tuple to set the high-water - and low-water marks. If you want to disable flow control entirely, - you may set it to ``None``, although that's a bad idea. - write_limit: High-water mark of write buffer in bytes. It is passed to - :meth:`~asyncio.WriteTransport.set_write_buffer_limits`. It defaults - to 32 KiB. You may pass a ``(high, low)`` tuple to set the - high-water and low-water marks. - logger: Logger for this server. - It defaults to ``logging.getLogger("websockets.server")``. See the - :doc:`logging guide <../../topics/logging>` for details. - create_connection: Factory for the :class:`ServerConnection` managing - the connection. Set it to a wrapper or a subclass to customize - connection handling. - - Any other keyword arguments are passed to the event loop's - :meth:`~asyncio.loop.create_server` method. - - For example: - - * You can set ``ssl`` to a :class:`~ssl.SSLContext` to enable TLS. - - * You can set ``sock`` to provide a preexisting TCP socket. You may call - :func:`socket.create_server` (not to be confused with the event loop's - :meth:`~asyncio.loop.create_server` method) to create a suitable server - socket and customize it. - - * You can set ``start_serving`` to ``False`` to start accepting connections - only after you call :meth:`~Server.start_serving()` or - :meth:`~Server.serve_forever()`. - - """ - - def __init__( - self, - handler: Callable[[ServerConnection], Awaitable[None]], - host: str | None = None, - port: int | None = None, - *, - # WebSocket - origins: Sequence[Origin | re.Pattern[str] | None] | None = None, - extensions: Sequence[ServerExtensionFactory] | None = None, - subprotocols: Sequence[Subprotocol] | None = None, - select_subprotocol: ( - Callable[ - [ServerConnection, Sequence[Subprotocol]], - Subprotocol | None, - ] - | None - ) = None, - compression: str | None = "deflate", - # HTTP - process_request: ( - Callable[ - [ServerConnection, Request], - Awaitable[Response | None] | Response | None, - ] - | None - ) = None, - process_response: ( - Callable[ - [ServerConnection, Request, Response], - Awaitable[Response | None] | Response | None, - ] - | None - ) = None, - server_header: str | None = SERVER, - # Timeouts - open_timeout: float | None = 10, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = 10, - # Limits - max_size: int | None = 2**20, - max_queue: int | None | tuple[int | None, int | None] = 16, - write_limit: int | tuple[int, int | None] = 2**15, - # Logging - logger: LoggerLike | None = None, - # Escape hatch for advanced customization - create_connection: type[ServerConnection] | None = None, - # Other keyword arguments are passed to loop.create_server - **kwargs: Any, - ) -> None: - if subprotocols is not None: - validate_subprotocols(subprotocols) - - if compression == "deflate": - extensions = enable_server_permessage_deflate(extensions) - elif compression is not None: - raise ValueError(f"unsupported compression: {compression}") - - if create_connection is None: - create_connection = ServerConnection - - self.server = Server( - handler, - process_request=process_request, - process_response=process_response, - server_header=server_header, - open_timeout=open_timeout, - logger=logger, - ) - - if kwargs.get("ssl") is not None: - kwargs.setdefault("ssl_handshake_timeout", open_timeout) - if sys.version_info[:2] >= (3, 11): # pragma: no branch - kwargs.setdefault("ssl_shutdown_timeout", close_timeout) - - def factory() -> ServerConnection: - """ - Create an asyncio protocol for managing a WebSocket connection. - - """ - # Create a closure to give select_subprotocol access to connection. - protocol_select_subprotocol: ( - Callable[ - [ServerProtocol, Sequence[Subprotocol]], - Subprotocol | None, - ] - | None - ) = None - if select_subprotocol is not None: - - def protocol_select_subprotocol( - protocol: ServerProtocol, - subprotocols: Sequence[Subprotocol], - ) -> Subprotocol | None: - # mypy doesn't know that select_subprotocol is immutable. - assert select_subprotocol is not None - # Ensure this function is only used in the intended context. - assert protocol is connection.protocol - return select_subprotocol(connection, subprotocols) - - # This is a protocol in the Sans-I/O implementation of websockets. - protocol = ServerProtocol( - origins=origins, - extensions=extensions, - subprotocols=subprotocols, - select_subprotocol=protocol_select_subprotocol, - max_size=max_size, - logger=logger, - ) - # This is a connection in websockets and a protocol in asyncio. - connection = create_connection( - protocol, - self.server, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - close_timeout=close_timeout, - max_queue=max_queue, - write_limit=write_limit, - ) - return connection - - loop = asyncio.get_running_loop() - if kwargs.pop("unix", False): - self.create_server = loop.create_unix_server(factory, **kwargs) - else: - # mypy cannot tell that kwargs must provide sock when port is None. - self.create_server = loop.create_server(factory, host, port, **kwargs) # type: ignore[arg-type] - - # async with serve(...) as ...: ... - - async def __aenter__(self) -> Server: - return await self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - self.server.close() - await self.server.wait_closed() - - # ... = await serve(...) - - def __await__(self) -> Generator[Any, None, Server]: - # Create a suitable iterator by calling __await__ on a coroutine. - return self.__await_impl__().__await__() - - async def __await_impl__(self) -> Server: - server = await self.create_server - self.server.wrap(server) - return self.server - - # ... = yield from serve(...) - remove when dropping Python < 3.10 - - __iter__ = __await__ - - -def unix_serve( - handler: Callable[[ServerConnection], Awaitable[None]], - path: str | None = None, - **kwargs: Any, -) -> Awaitable[Server]: - """ - Create a WebSocket server listening on a Unix socket. - - This function is identical to :func:`serve`, except the ``host`` and - ``port`` arguments are replaced by ``path``. It's only available on Unix. - - It's useful for deploying a server behind a reverse proxy such as nginx. - - Args: - handler: Connection handler. It receives the WebSocket connection, - which is a :class:`ServerConnection`, in argument. - path: File system path to the Unix socket. - - """ - return serve(handler, unix=True, path=path, **kwargs) - - -def is_credentials(credentials: Any) -> bool: - try: - username, password = credentials - except (TypeError, ValueError): - return False - else: - return isinstance(username, str) and isinstance(password, str) - - -def basic_auth( - realm: str = "", - credentials: tuple[str, str] | Iterable[tuple[str, str]] | None = None, - check_credentials: Callable[[str, str], Awaitable[bool] | bool] | None = None, -) -> Callable[[ServerConnection, Request], Awaitable[Response | None]]: - """ - Factory for ``process_request`` to enforce HTTP Basic Authentication. - - :func:`basic_auth` is designed to integrate with :func:`serve` as follows:: - - from websockets.asyncio.server import basic_auth, serve - - async with serve( - ..., - process_request=basic_auth( - realm="my dev server", - credentials=("hello", "iloveyou"), - ), - ): - - If authentication succeeds, the connection's ``username`` attribute is set. - If it fails, the server responds with an HTTP 401 Unauthorized status. - - One of ``credentials`` or ``check_credentials`` must be provided; not both. - - Args: - realm: Scope of protection. It should contain only ASCII characters - because the encoding of non-ASCII characters is undefined. Refer to - section 2.2 of :rfc:`7235` for details. - credentials: Hard coded authorized credentials. It can be a - ``(username, password)`` pair or a list of such pairs. - check_credentials: Function or coroutine that verifies credentials. - It receives ``username`` and ``password`` arguments and returns - whether they're valid. - Raises: - TypeError: If ``credentials`` or ``check_credentials`` is wrong. - ValueError: If ``credentials`` and ``check_credentials`` are both - provided or both not provided. - - """ - if (credentials is None) == (check_credentials is None): - raise ValueError("provide either credentials or check_credentials") - - if credentials is not None: - if is_credentials(credentials): - credentials_list = [cast(tuple[str, str], credentials)] - elif isinstance(credentials, Iterable): - credentials_list = list(cast(Iterable[tuple[str, str]], credentials)) - if not all(is_credentials(item) for item in credentials_list): - raise TypeError(f"invalid credentials argument: {credentials}") - else: - raise TypeError(f"invalid credentials argument: {credentials}") - - credentials_dict = dict(credentials_list) - - def check_credentials(username: str, password: str) -> bool: - try: - expected_password = credentials_dict[username] - except KeyError: - return False - return hmac.compare_digest(expected_password, password) - - assert check_credentials is not None # help mypy - - async def process_request( - connection: ServerConnection, - request: Request, - ) -> Response | None: - """ - Perform HTTP Basic Authentication. - - If it succeeds, set the connection's ``username`` attribute and return - :obj:`None`. If it fails, return an HTTP 401 Unauthorized responss. - - """ - try: - authorization = request.headers["Authorization"] - except KeyError: - response = connection.respond( - http.HTTPStatus.UNAUTHORIZED, - "Missing credentials\n", - ) - response.headers["WWW-Authenticate"] = build_www_authenticate_basic(realm) - return response - - try: - username, password = parse_authorization_basic(authorization) - except InvalidHeader: - response = connection.respond( - http.HTTPStatus.UNAUTHORIZED, - "Unsupported credentials\n", - ) - response.headers["WWW-Authenticate"] = build_www_authenticate_basic(realm) - return response - - valid_credentials = check_credentials(username, password) - if isinstance(valid_credentials, Awaitable): - valid_credentials = await valid_credentials - - if not valid_credentials: - response = connection.respond( - http.HTTPStatus.UNAUTHORIZED, - "Invalid credentials\n", - ) - response.headers["WWW-Authenticate"] = build_www_authenticate_basic(realm) - return response - - connection.username = username - return None - - return process_request diff --git a/wip/SNIP/websockets/auth.py b/wip/SNIP/websockets/auth.py deleted file mode 100644 index 15b70a3..0000000 --- a/wip/SNIP/websockets/auth.py +++ /dev/null @@ -1,18 +0,0 @@ -from __future__ import annotations - -import warnings - - -with warnings.catch_warnings(): - # Suppress redundant DeprecationWarning raised by websockets.legacy. - warnings.filterwarnings("ignore", category=DeprecationWarning) - from .legacy.auth import * - from .legacy.auth import __all__ # noqa: F401 - - -warnings.warn( # deprecated in 14.0 - 2024-11-09 - "websockets.auth, an alias for websockets.legacy.auth, is deprecated; " - "see https://websockets.readthedocs.io/en/stable/howto/upgrade.html " - "for upgrade instructions", - DeprecationWarning, -) diff --git a/wip/SNIP/websockets/cli.py b/wip/SNIP/websockets/cli.py deleted file mode 100644 index e084b62..0000000 --- a/wip/SNIP/websockets/cli.py +++ /dev/null @@ -1,178 +0,0 @@ -from __future__ import annotations - -import argparse -import asyncio -import os -import sys -from typing import Generator - -from .asyncio.client import ClientConnection, connect -from .asyncio.messages import SimpleQueue -from .exceptions import ConnectionClosed -from .frames import Close -from .streams import StreamReader -from .version import version as websockets_version - - -__all__ = ["main"] - - -def print_during_input(string: str) -> None: - sys.stdout.write( - # Save cursor position - "\N{ESC}7" - # Add a new line - "\N{LINE FEED}" - # Move cursor up - "\N{ESC}[A" - # Insert blank line, scroll last line down - "\N{ESC}[L" - # Print string in the inserted blank line - f"{string}\N{LINE FEED}" - # Restore cursor position - "\N{ESC}8" - # Move cursor down - "\N{ESC}[B" - ) - sys.stdout.flush() - - -def print_over_input(string: str) -> None: - sys.stdout.write( - # Move cursor to beginning of line - "\N{CARRIAGE RETURN}" - # Delete current line - "\N{ESC}[K" - # Print string - f"{string}\N{LINE FEED}" - ) - sys.stdout.flush() - - -class ReadLines(asyncio.Protocol): - def __init__(self) -> None: - self.reader = StreamReader() - self.messages: SimpleQueue[str] = SimpleQueue() - - def parse(self) -> Generator[None, None, None]: - while True: - sys.stdout.write("> ") - sys.stdout.flush() - line = yield from self.reader.read_line(sys.maxsize) - self.messages.put(line.decode().rstrip("\r\n")) - - def connection_made(self, transport: asyncio.BaseTransport) -> None: - self.parser = self.parse() - next(self.parser) - - def data_received(self, data: bytes) -> None: - self.reader.feed_data(data) - next(self.parser) - - def eof_received(self) -> None: - self.reader.feed_eof() - # next(self.parser) isn't useful and would raise EOFError. - - def connection_lost(self, exc: Exception | None) -> None: - self.reader.discard() - self.messages.abort() - - -async def print_incoming_messages(websocket: ClientConnection) -> None: - async for message in websocket: - if isinstance(message, str): - print_during_input("< " + message) - else: - print_during_input("< (binary) " + message.hex()) - - -async def send_outgoing_messages( - websocket: ClientConnection, - messages: SimpleQueue[str], -) -> None: - while True: - try: - message = await messages.get() - except EOFError: - break - try: - await websocket.send(message) - except ConnectionClosed: # pragma: no cover - break - - -async def interactive_client(uri: str) -> None: - try: - websocket = await connect(uri) - except Exception as exc: - print(f"Failed to connect to {uri}: {exc}.") - sys.exit(1) - else: - print(f"Connected to {uri}.") - - loop = asyncio.get_running_loop() - transport, protocol = await loop.connect_read_pipe(ReadLines, sys.stdin) - incoming = asyncio.create_task( - print_incoming_messages(websocket), - ) - outgoing = asyncio.create_task( - send_outgoing_messages(websocket, protocol.messages), - ) - try: - await asyncio.wait( - [incoming, outgoing], - # Clean up and exit when the server closes the connection - # or the user enters EOT (^D), whichever happens first. - return_when=asyncio.FIRST_COMPLETED, - ) - # asyncio.run() cancels the main task when the user triggers SIGINT (^C). - # https://docs.python.org/3/library/asyncio-runner.html#handling-keyboard-interruption - # Clean up and exit without re-raising CancelledError to prevent Python - # from raising KeyboardInterrupt and displaying a stack track. - except asyncio.CancelledError: # pragma: no cover - pass - finally: - incoming.cancel() - outgoing.cancel() - transport.close() - - await websocket.close() - assert websocket.close_code is not None and websocket.close_reason is not None - close_status = Close(websocket.close_code, websocket.close_reason) - print_over_input(f"Connection closed: {close_status}.") - - -def main(argv: list[str] | None = None) -> None: - parser = argparse.ArgumentParser( - prog="websockets", - description="Interactive WebSocket client.", - add_help=False, - ) - group = parser.add_mutually_exclusive_group() - group.add_argument("--version", action="store_true") - group.add_argument("uri", metavar="", nargs="?") - args = parser.parse_args(argv) - - if args.version: - print(f"websockets {websockets_version}") - return - - if args.uri is None: - parser.print_usage() - sys.exit(2) - - # Enable VT100 to support ANSI escape codes in Command Prompt on Windows. - # See https://github.com/python/cpython/issues/74261 for why this works. - if sys.platform == "win32": - os.system("") - - try: - import readline # noqa: F401 - except ImportError: # readline isn't available on all platforms - pass - - # Remove the try/except block when dropping Python < 3.11. - try: - asyncio.run(interactive_client(args.uri)) - except KeyboardInterrupt: # pragma: no cover - pass diff --git a/wip/SNIP/websockets/client.py b/wip/SNIP/websockets/client.py deleted file mode 100644 index 9ea21c3..0000000 --- a/wip/SNIP/websockets/client.py +++ /dev/null @@ -1,389 +0,0 @@ -from __future__ import annotations - -import os -import random -import warnings -from collections.abc import Generator, Sequence -from typing import Any - -from .datastructures import Headers, MultipleValuesError -from .exceptions import ( - InvalidHandshake, - InvalidHeader, - InvalidHeaderValue, - InvalidMessage, - InvalidStatus, - InvalidUpgrade, - NegotiationError, -) -from .extensions import ClientExtensionFactory, Extension -from .headers import ( - build_authorization_basic, - build_extension, - build_host, - build_subprotocol, - parse_connection, - parse_extension, - parse_subprotocol, - parse_upgrade, -) -from .http11 import Request, Response -from .imports import lazy_import -from .protocol import CLIENT, CONNECTING, OPEN, Protocol, State -from .typing import ( - ConnectionOption, - ExtensionHeader, - LoggerLike, - Origin, - Subprotocol, - UpgradeProtocol, -) -from .uri import WebSocketURI -from .utils import accept_key, generate_key - - -__all__ = ["ClientProtocol"] - - -class ClientProtocol(Protocol): - """ - Sans-I/O implementation of a WebSocket client connection. - - Args: - uri: URI of the WebSocket server, parsed - with :func:`~websockets.uri.parse_uri`. - origin: Value of the ``Origin`` header. This is useful when connecting - to a server that validates the ``Origin`` header to defend against - Cross-Site WebSocket Hijacking attacks. - extensions: List of supported extensions, in order in which they - should be tried. - subprotocols: List of supported subprotocols, in order of decreasing - preference. - state: Initial state of the WebSocket connection. - max_size: Maximum size of incoming messages in bytes; - :obj:`None` disables the limit. - logger: Logger for this connection; - defaults to ``logging.getLogger("websockets.client")``; - see the :doc:`logging guide <../../topics/logging>` for details. - - """ - - def __init__( - self, - uri: WebSocketURI, - *, - origin: Origin | None = None, - extensions: Sequence[ClientExtensionFactory] | None = None, - subprotocols: Sequence[Subprotocol] | None = None, - state: State = CONNECTING, - max_size: int | None = 2**20, - logger: LoggerLike | None = None, - ) -> None: - super().__init__( - side=CLIENT, - state=state, - max_size=max_size, - logger=logger, - ) - self.uri = uri - self.origin = origin - self.available_extensions = extensions - self.available_subprotocols = subprotocols - self.key = generate_key() - - def connect(self) -> Request: - """ - Create a handshake request to open a connection. - - You must send the handshake request with :meth:`send_request`. - - You can modify it before sending it, for example to add HTTP headers. - - Returns: - WebSocket handshake request event to send to the server. - - """ - headers = Headers() - headers["Host"] = build_host(self.uri.host, self.uri.port, self.uri.secure) - if self.uri.user_info: - headers["Authorization"] = build_authorization_basic(*self.uri.user_info) - if self.origin is not None: - headers["Origin"] = self.origin - headers["Upgrade"] = "websocket" - headers["Connection"] = "Upgrade" - headers["Sec-WebSocket-Key"] = self.key - headers["Sec-WebSocket-Version"] = "13" - if self.available_extensions is not None: - headers["Sec-WebSocket-Extensions"] = build_extension( - [ - (extension_factory.name, extension_factory.get_request_params()) - for extension_factory in self.available_extensions - ] - ) - if self.available_subprotocols is not None: - headers["Sec-WebSocket-Protocol"] = build_subprotocol( - self.available_subprotocols - ) - return Request(self.uri.resource_name, headers) - - def process_response(self, response: Response) -> None: - """ - Check a handshake response. - - Args: - request: WebSocket handshake response received from the server. - - Raises: - InvalidHandshake: If the handshake response is invalid. - - """ - - if response.status_code != 101: - raise InvalidStatus(response) - - headers = response.headers - - connection: list[ConnectionOption] = sum( - [parse_connection(value) for value in headers.get_all("Connection")], [] - ) - if not any(value.lower() == "upgrade" for value in connection): - raise InvalidUpgrade( - "Connection", ", ".join(connection) if connection else None - ) - - upgrade: list[UpgradeProtocol] = sum( - [parse_upgrade(value) for value in headers.get_all("Upgrade")], [] - ) - # For compatibility with non-strict implementations, ignore case when - # checking the Upgrade header. It's supposed to be 'WebSocket'. - if not (len(upgrade) == 1 and upgrade[0].lower() == "websocket"): - raise InvalidUpgrade("Upgrade", ", ".join(upgrade) if upgrade else None) - - try: - s_w_accept = headers["Sec-WebSocket-Accept"] - except KeyError: - raise InvalidHeader("Sec-WebSocket-Accept") from None - except MultipleValuesError: - raise InvalidHeader("Sec-WebSocket-Accept", "multiple values") from None - if s_w_accept != accept_key(self.key): - raise InvalidHeaderValue("Sec-WebSocket-Accept", s_w_accept) - - self.extensions = self.process_extensions(headers) - self.subprotocol = self.process_subprotocol(headers) - - def process_extensions(self, headers: Headers) -> list[Extension]: - """ - Handle the Sec-WebSocket-Extensions HTTP response header. - - Check that each extension is supported, as well as its parameters. - - :rfc:`6455` leaves the rules up to the specification of each - extension. - - To provide this level of flexibility, for each extension accepted by - the server, we check for a match with each extension available in the - client configuration. If no match is found, an exception is raised. - - If several variants of the same extension are accepted by the server, - it may be configured several times, which won't make sense in general. - Extensions must implement their own requirements. For this purpose, - the list of previously accepted extensions is provided. - - Other requirements, for example related to mandatory extensions or the - order of extensions, may be implemented by overriding this method. - - Args: - headers: WebSocket handshake response headers. - - Returns: - List of accepted extensions. - - Raises: - InvalidHandshake: To abort the handshake. - - """ - accepted_extensions: list[Extension] = [] - - extensions = headers.get_all("Sec-WebSocket-Extensions") - - if extensions: - if self.available_extensions is None: - raise NegotiationError("no extensions supported") - - parsed_extensions: list[ExtensionHeader] = sum( - [parse_extension(header_value) for header_value in extensions], [] - ) - - for name, response_params in parsed_extensions: - for extension_factory in self.available_extensions: - # Skip non-matching extensions based on their name. - if extension_factory.name != name: - continue - - # Skip non-matching extensions based on their params. - try: - extension = extension_factory.process_response_params( - response_params, accepted_extensions - ) - except NegotiationError: - continue - - # Add matching extension to the final list. - accepted_extensions.append(extension) - - # Break out of the loop once we have a match. - break - - # If we didn't break from the loop, no extension in our list - # matched what the server sent. Fail the connection. - else: - raise NegotiationError( - f"Unsupported extension: " - f"name = {name}, params = {response_params}" - ) - - return accepted_extensions - - def process_subprotocol(self, headers: Headers) -> Subprotocol | None: - """ - Handle the Sec-WebSocket-Protocol HTTP response header. - - If provided, check that it contains exactly one supported subprotocol. - - Args: - headers: WebSocket handshake response headers. - - Returns: - Subprotocol, if one was selected. - - """ - subprotocol: Subprotocol | None = None - - subprotocols = headers.get_all("Sec-WebSocket-Protocol") - - if subprotocols: - if self.available_subprotocols is None: - raise NegotiationError("no subprotocols supported") - - parsed_subprotocols: Sequence[Subprotocol] = sum( - [parse_subprotocol(header_value) for header_value in subprotocols], [] - ) - if len(parsed_subprotocols) > 1: - raise InvalidHeader( - "Sec-WebSocket-Protocol", - f"multiple values: {', '.join(parsed_subprotocols)}", - ) - - subprotocol = parsed_subprotocols[0] - if subprotocol not in self.available_subprotocols: - raise NegotiationError(f"unsupported subprotocol: {subprotocol}") - - return subprotocol - - def send_request(self, request: Request) -> None: - """ - Send a handshake request to the server. - - Args: - request: WebSocket handshake request event. - - """ - if self.debug: - self.logger.debug("> GET %s HTTP/1.1", request.path) - for key, value in request.headers.raw_items(): - self.logger.debug("> %s: %s", key, value) - - self.writes.append(request.serialize()) - - def parse(self) -> Generator[None]: - if self.state is CONNECTING: - try: - response = yield from Response.parse( - self.reader.read_line, - self.reader.read_exact, - self.reader.read_to_eof, - ) - except Exception as exc: - self.handshake_exc = InvalidMessage( - "did not receive a valid HTTP response" - ) - self.handshake_exc.__cause__ = exc - self.send_eof() - self.parser = self.discard() - next(self.parser) # start coroutine - yield - - if self.debug: - code, phrase = response.status_code, response.reason_phrase - self.logger.debug("< HTTP/1.1 %d %s", code, phrase) - for key, value in response.headers.raw_items(): - self.logger.debug("< %s: %s", key, value) - if response.body: - self.logger.debug("< [body] (%d bytes)", len(response.body)) - - try: - self.process_response(response) - except InvalidHandshake as exc: - response._exception = exc - self.events.append(response) - self.handshake_exc = exc - self.send_eof() - self.parser = self.discard() - next(self.parser) # start coroutine - yield - - assert self.state is CONNECTING - self.state = OPEN - self.events.append(response) - - yield from super().parse() - - -class ClientConnection(ClientProtocol): - def __init__(self, *args: Any, **kwargs: Any) -> None: - warnings.warn( # deprecated in 11.0 - 2023-04-02 - "ClientConnection was renamed to ClientProtocol", - DeprecationWarning, - ) - super().__init__(*args, **kwargs) - - -BACKOFF_INITIAL_DELAY = float(os.environ.get("WEBSOCKETS_BACKOFF_INITIAL_DELAY", "5")) -BACKOFF_MIN_DELAY = float(os.environ.get("WEBSOCKETS_BACKOFF_MIN_DELAY", "3.1")) -BACKOFF_MAX_DELAY = float(os.environ.get("WEBSOCKETS_BACKOFF_MAX_DELAY", "90.0")) -BACKOFF_FACTOR = float(os.environ.get("WEBSOCKETS_BACKOFF_FACTOR", "1.618")) - - -def backoff( - initial_delay: float = BACKOFF_INITIAL_DELAY, - min_delay: float = BACKOFF_MIN_DELAY, - max_delay: float = BACKOFF_MAX_DELAY, - factor: float = BACKOFF_FACTOR, -) -> Generator[float]: - """ - Generate a series of backoff delays between reconnection attempts. - - Yields: - How many seconds to wait before retrying to connect. - - """ - # Add a random initial delay between 0 and 5 seconds. - # See 7.2.3. Recovering from Abnormal Closure in RFC 6455. - yield random.random() * initial_delay - delay = min_delay - while delay < max_delay: - yield delay - delay *= factor - while True: - yield max_delay - - -lazy_import( - globals(), - deprecated_aliases={ - # deprecated in 14.0 - 2024-11-09 - "WebSocketClientProtocol": ".legacy.client", - "connect": ".legacy.client", - "unix_connect": ".legacy.client", - }, -) diff --git a/wip/SNIP/websockets/connection.py b/wip/SNIP/websockets/connection.py deleted file mode 100644 index 5e78e34..0000000 --- a/wip/SNIP/websockets/connection.py +++ /dev/null @@ -1,12 +0,0 @@ -from __future__ import annotations - -import warnings - -from .protocol import SEND_EOF, Protocol as Connection, Side, State # noqa: F401 - - -warnings.warn( # deprecated in 11.0 - 2023-04-02 - "websockets.connection was renamed to websockets.protocol " - "and Connection was renamed to Protocol", - DeprecationWarning, -) diff --git a/wip/SNIP/websockets/datastructures.py b/wip/SNIP/websockets/datastructures.py deleted file mode 100644 index 3c5dcbe..0000000 --- a/wip/SNIP/websockets/datastructures.py +++ /dev/null @@ -1,187 +0,0 @@ -from __future__ import annotations - -from collections.abc import Iterable, Iterator, Mapping, MutableMapping -from typing import Any, Protocol, Union - - -__all__ = [ - "Headers", - "HeadersLike", - "MultipleValuesError", -] - - -class MultipleValuesError(LookupError): - """ - Exception raised when :class:`Headers` has multiple values for a key. - - """ - - def __str__(self) -> str: - # Implement the same logic as KeyError_str in Objects/exceptions.c. - if len(self.args) == 1: - return repr(self.args[0]) - return super().__str__() - - -class Headers(MutableMapping[str, str]): - """ - Efficient data structure for manipulating HTTP headers. - - A :class:`list` of ``(name, values)`` is inefficient for lookups. - - A :class:`dict` doesn't suffice because header names are case-insensitive - and multiple occurrences of headers with the same name are possible. - - :class:`Headers` stores HTTP headers in a hybrid data structure to provide - efficient insertions and lookups while preserving the original data. - - In order to account for multiple values with minimal hassle, - :class:`Headers` follows this logic: - - - When getting a header with ``headers[name]``: - - if there's no value, :exc:`KeyError` is raised; - - if there's exactly one value, it's returned; - - if there's more than one value, :exc:`MultipleValuesError` is raised. - - - When setting a header with ``headers[name] = value``, the value is - appended to the list of values for that header. - - - When deleting a header with ``del headers[name]``, all values for that - header are removed (this is slow). - - Other methods for manipulating headers are consistent with this logic. - - As long as no header occurs multiple times, :class:`Headers` behaves like - :class:`dict`, except keys are lower-cased to provide case-insensitivity. - - Two methods support manipulating multiple values explicitly: - - - :meth:`get_all` returns a list of all values for a header; - - :meth:`raw_items` returns an iterator of ``(name, values)`` pairs. - - """ - - __slots__ = ["_dict", "_list"] - - # Like dict, Headers accepts an optional "mapping or iterable" argument. - def __init__(self, *args: HeadersLike, **kwargs: str) -> None: - self._dict: dict[str, list[str]] = {} - self._list: list[tuple[str, str]] = [] - self.update(*args, **kwargs) - - def __str__(self) -> str: - return "".join(f"{key}: {value}\r\n" for key, value in self._list) + "\r\n" - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self._list!r})" - - def copy(self) -> Headers: - copy = self.__class__() - copy._dict = self._dict.copy() - copy._list = self._list.copy() - return copy - - def serialize(self) -> bytes: - # Since headers only contain ASCII characters, we can keep this simple. - return str(self).encode() - - # Collection methods - - def __contains__(self, key: object) -> bool: - return isinstance(key, str) and key.lower() in self._dict - - def __iter__(self) -> Iterator[str]: - return iter(self._dict) - - def __len__(self) -> int: - return len(self._dict) - - # MutableMapping methods - - def __getitem__(self, key: str) -> str: - value = self._dict[key.lower()] - if len(value) == 1: - return value[0] - else: - raise MultipleValuesError(key) - - def __setitem__(self, key: str, value: str) -> None: - self._dict.setdefault(key.lower(), []).append(value) - self._list.append((key, value)) - - def __delitem__(self, key: str) -> None: - key_lower = key.lower() - self._dict.__delitem__(key_lower) - # This is inefficient. Fortunately deleting HTTP headers is uncommon. - self._list = [(k, v) for k, v in self._list if k.lower() != key_lower] - - def __eq__(self, other: Any) -> bool: - if not isinstance(other, Headers): - return NotImplemented - return self._dict == other._dict - - def clear(self) -> None: - """ - Remove all headers. - - """ - self._dict = {} - self._list = [] - - def update(self, *args: HeadersLike, **kwargs: str) -> None: - """ - Update from a :class:`Headers` instance and/or keyword arguments. - - """ - args = tuple( - arg.raw_items() if isinstance(arg, Headers) else arg for arg in args - ) - super().update(*args, **kwargs) - - # Methods for handling multiple values - - def get_all(self, key: str) -> list[str]: - """ - Return the (possibly empty) list of all values for a header. - - Args: - key: Header name. - - """ - return self._dict.get(key.lower(), []) - - def raw_items(self) -> Iterator[tuple[str, str]]: - """ - Return an iterator of all values as ``(name, value)`` pairs. - - """ - return iter(self._list) - - -# copy of _typeshed.SupportsKeysAndGetItem. -class SupportsKeysAndGetItem(Protocol): # pragma: no cover - """ - Dict-like types with ``keys() -> str`` and ``__getitem__(key: str) -> str`` methods. - - """ - - def keys(self) -> Iterable[str]: ... - - def __getitem__(self, key: str) -> str: ... - - -# Change to Headers | Mapping[str, str] | ... when dropping Python < 3.10. -HeadersLike = Union[ - Headers, - Mapping[str, str], - Iterable[tuple[str, str]], - SupportsKeysAndGetItem, -] -""" -Types accepted where :class:`Headers` is expected. - -In addition to :class:`Headers` itself, this includes dict-like types where both -keys and values are :class:`str`. - -""" diff --git a/wip/SNIP/websockets/exceptions.py b/wip/SNIP/websockets/exceptions.py deleted file mode 100644 index ab1a15c..0000000 --- a/wip/SNIP/websockets/exceptions.py +++ /dev/null @@ -1,473 +0,0 @@ -""" -:mod:`websockets.exceptions` defines the following hierarchy of exceptions. - -* :exc:`WebSocketException` - * :exc:`ConnectionClosed` - * :exc:`ConnectionClosedOK` - * :exc:`ConnectionClosedError` - * :exc:`InvalidURI` - * :exc:`InvalidProxy` - * :exc:`InvalidHandshake` - * :exc:`SecurityError` - * :exc:`ProxyError` - * :exc:`InvalidProxyMessage` - * :exc:`InvalidProxyStatus` - * :exc:`InvalidMessage` - * :exc:`InvalidStatus` - * :exc:`InvalidStatusCode` (legacy) - * :exc:`InvalidHeader` - * :exc:`InvalidHeaderFormat` - * :exc:`InvalidHeaderValue` - * :exc:`InvalidOrigin` - * :exc:`InvalidUpgrade` - * :exc:`NegotiationError` - * :exc:`DuplicateParameter` - * :exc:`InvalidParameterName` - * :exc:`InvalidParameterValue` - * :exc:`AbortHandshake` (legacy) - * :exc:`RedirectHandshake` (legacy) - * :exc:`ProtocolError` (Sans-I/O) - * :exc:`PayloadTooBig` (Sans-I/O) - * :exc:`InvalidState` (Sans-I/O) - * :exc:`ConcurrencyError` - -""" - -from __future__ import annotations - -import warnings - -from .imports import lazy_import - - -__all__ = [ - "WebSocketException", - "ConnectionClosed", - "ConnectionClosedOK", - "ConnectionClosedError", - "InvalidURI", - "InvalidProxy", - "InvalidHandshake", - "SecurityError", - "ProxyError", - "InvalidProxyMessage", - "InvalidProxyStatus", - "InvalidMessage", - "InvalidStatus", - "InvalidHeader", - "InvalidHeaderFormat", - "InvalidHeaderValue", - "InvalidOrigin", - "InvalidUpgrade", - "NegotiationError", - "DuplicateParameter", - "InvalidParameterName", - "InvalidParameterValue", - "ProtocolError", - "PayloadTooBig", - "InvalidState", - "ConcurrencyError", -] - - -class WebSocketException(Exception): - """ - Base class for all exceptions defined by websockets. - - """ - - -class ConnectionClosed(WebSocketException): - """ - Raised when trying to interact with a closed connection. - - Attributes: - rcvd: If a close frame was received, its code and reason are available - in ``rcvd.code`` and ``rcvd.reason``. - sent: If a close frame was sent, its code and reason are available - in ``sent.code`` and ``sent.reason``. - rcvd_then_sent: If close frames were received and sent, this attribute - tells in which order this happened, from the perspective of this - side of the connection. - - """ - - def __init__( - self, - rcvd: frames.Close | None, - sent: frames.Close | None, - rcvd_then_sent: bool | None = None, - ) -> None: - self.rcvd = rcvd - self.sent = sent - self.rcvd_then_sent = rcvd_then_sent - assert (self.rcvd_then_sent is None) == (self.rcvd is None or self.sent is None) - - def __str__(self) -> str: - if self.rcvd is None: - if self.sent is None: - return "no close frame received or sent" - else: - return f"sent {self.sent}; no close frame received" - else: - if self.sent is None: - return f"received {self.rcvd}; no close frame sent" - else: - if self.rcvd_then_sent: - return f"received {self.rcvd}; then sent {self.sent}" - else: - return f"sent {self.sent}; then received {self.rcvd}" - - # code and reason attributes are provided for backwards-compatibility - - @property - def code(self) -> int: - warnings.warn( # deprecated in 13.1 - 2024-09-21 - "ConnectionClosed.code is deprecated; " - "use Protocol.close_code or ConnectionClosed.rcvd.code", - DeprecationWarning, - ) - if self.rcvd is None: - return frames.CloseCode.ABNORMAL_CLOSURE - return self.rcvd.code - - @property - def reason(self) -> str: - warnings.warn( # deprecated in 13.1 - 2024-09-21 - "ConnectionClosed.reason is deprecated; " - "use Protocol.close_reason or ConnectionClosed.rcvd.reason", - DeprecationWarning, - ) - if self.rcvd is None: - return "" - return self.rcvd.reason - - -class ConnectionClosedOK(ConnectionClosed): - """ - Like :exc:`ConnectionClosed`, when the connection terminated properly. - - A close code with code 1000 (OK) or 1001 (going away) or without a code was - received and sent. - - """ - - -class ConnectionClosedError(ConnectionClosed): - """ - Like :exc:`ConnectionClosed`, when the connection terminated with an error. - - A close frame with a code other than 1000 (OK) or 1001 (going away) was - received or sent, or the closing handshake didn't complete properly. - - """ - - -class InvalidURI(WebSocketException): - """ - Raised when connecting to a URI that isn't a valid WebSocket URI. - - """ - - def __init__(self, uri: str, msg: str) -> None: - self.uri = uri - self.msg = msg - - def __str__(self) -> str: - return f"{self.uri} isn't a valid URI: {self.msg}" - - -class InvalidProxy(WebSocketException): - """ - Raised when connecting via a proxy that isn't valid. - - """ - - def __init__(self, proxy: str, msg: str) -> None: - self.proxy = proxy - self.msg = msg - - def __str__(self) -> str: - return f"{self.proxy} isn't a valid proxy: {self.msg}" - - -class InvalidHandshake(WebSocketException): - """ - Base class for exceptions raised when the opening handshake fails. - - """ - - -class SecurityError(InvalidHandshake): - """ - Raised when a handshake request or response breaks a security rule. - - Security limits can be configured with :doc:`environment variables - <../reference/variables>`. - - """ - - -class ProxyError(InvalidHandshake): - """ - Raised when failing to connect to a proxy. - - """ - - -class InvalidProxyMessage(ProxyError): - """ - Raised when an HTTP proxy response is malformed. - - """ - - -class InvalidProxyStatus(ProxyError): - """ - Raised when an HTTP proxy rejects the connection. - - """ - - def __init__(self, response: http11.Response) -> None: - self.response = response - - def __str__(self) -> str: - return f"proxy rejected connection: HTTP {self.response.status_code:d}" - - -class InvalidMessage(InvalidHandshake): - """ - Raised when a handshake request or response is malformed. - - """ - - -class InvalidStatus(InvalidHandshake): - """ - Raised when a handshake response rejects the WebSocket upgrade. - - """ - - def __init__(self, response: http11.Response) -> None: - self.response = response - - def __str__(self) -> str: - return ( - f"server rejected WebSocket connection: HTTP {self.response.status_code:d}" - ) - - -class InvalidHeader(InvalidHandshake): - """ - Raised when an HTTP header doesn't have a valid format or value. - - """ - - def __init__(self, name: str, value: str | None = None) -> None: - self.name = name - self.value = value - - def __str__(self) -> str: - if self.value is None: - return f"missing {self.name} header" - elif self.value == "": - return f"empty {self.name} header" - else: - return f"invalid {self.name} header: {self.value}" - - -class InvalidHeaderFormat(InvalidHeader): - """ - Raised when an HTTP header cannot be parsed. - - The format of the header doesn't match the grammar for that header. - - """ - - def __init__(self, name: str, error: str, header: str, pos: int) -> None: - super().__init__(name, f"{error} at {pos} in {header}") - - -class InvalidHeaderValue(InvalidHeader): - """ - Raised when an HTTP header has a wrong value. - - The format of the header is correct but the value isn't acceptable. - - """ - - -class InvalidOrigin(InvalidHeader): - """ - Raised when the Origin header in a request isn't allowed. - - """ - - def __init__(self, origin: str | None) -> None: - super().__init__("Origin", origin) - - -class InvalidUpgrade(InvalidHeader): - """ - Raised when the Upgrade or Connection header isn't correct. - - """ - - -class NegotiationError(InvalidHandshake): - """ - Raised when negotiating an extension or a subprotocol fails. - - """ - - -class DuplicateParameter(NegotiationError): - """ - Raised when a parameter name is repeated in an extension header. - - """ - - def __init__(self, name: str) -> None: - self.name = name - - def __str__(self) -> str: - return f"duplicate parameter: {self.name}" - - -class InvalidParameterName(NegotiationError): - """ - Raised when a parameter name in an extension header is invalid. - - """ - - def __init__(self, name: str) -> None: - self.name = name - - def __str__(self) -> str: - return f"invalid parameter name: {self.name}" - - -class InvalidParameterValue(NegotiationError): - """ - Raised when a parameter value in an extension header is invalid. - - """ - - def __init__(self, name: str, value: str | None) -> None: - self.name = name - self.value = value - - def __str__(self) -> str: - if self.value is None: - return f"missing value for parameter {self.name}" - elif self.value == "": - return f"empty value for parameter {self.name}" - else: - return f"invalid value for parameter {self.name}: {self.value}" - - -class ProtocolError(WebSocketException): - """ - Raised when receiving or sending a frame that breaks the protocol. - - The Sans-I/O implementation raises this exception when: - - * receiving or sending a frame that contains invalid data; - * receiving or sending an invalid sequence of frames. - - """ - - -class PayloadTooBig(WebSocketException): - """ - Raised when parsing a frame with a payload that exceeds the maximum size. - - The Sans-I/O layer uses this exception internally. It doesn't bubble up to - the I/O layer. - - The :meth:`~websockets.extensions.Extension.decode` method of extensions - must raise :exc:`PayloadTooBig` if decoding a frame would exceed the limit. - - """ - - def __init__( - self, - size_or_message: int | None | str, - max_size: int | None = None, - cur_size: int | None = None, - ) -> None: - if isinstance(size_or_message, str): - assert max_size is None - assert cur_size is None - warnings.warn( # deprecated in 14.0 - 2024-11-09 - "PayloadTooBig(message) is deprecated; " - "change to PayloadTooBig(size, max_size)", - DeprecationWarning, - ) - self.message: str | None = size_or_message - else: - self.message = None - self.size: int | None = size_or_message - assert max_size is not None - self.max_size: int = max_size - self.cur_size: int | None = None - self.set_current_size(cur_size) - - def __str__(self) -> str: - if self.message is not None: - return self.message - else: - message = "frame " - if self.size is not None: - message += f"with {self.size} bytes " - if self.cur_size is not None: - message += f"after reading {self.cur_size} bytes " - message += f"exceeds limit of {self.max_size} bytes" - return message - - def set_current_size(self, cur_size: int | None) -> None: - assert self.cur_size is None - if cur_size is not None: - self.max_size += cur_size - self.cur_size = cur_size - - -class InvalidState(WebSocketException, AssertionError): - """ - Raised when sending a frame is forbidden in the current state. - - Specifically, the Sans-I/O layer raises this exception when: - - * sending a data frame to a connection in a state other - :attr:`~websockets.protocol.State.OPEN`; - * sending a control frame to a connection in a state other than - :attr:`~websockets.protocol.State.OPEN` or - :attr:`~websockets.protocol.State.CLOSING`. - - """ - - -class ConcurrencyError(WebSocketException, RuntimeError): - """ - Raised when receiving or sending messages concurrently. - - WebSocket is a connection-oriented protocol. Reads must be serialized; so - must be writes. However, reading and writing concurrently is possible. - - """ - - -# At the bottom to break import cycles created by type annotations. -from . import frames, http11 # noqa: E402 - - -lazy_import( - globals(), - deprecated_aliases={ - # deprecated in 14.0 - 2024-11-09 - "AbortHandshake": ".legacy.exceptions", - "InvalidStatusCode": ".legacy.exceptions", - "RedirectHandshake": ".legacy.exceptions", - "WebSocketProtocolError": ".legacy.exceptions", - }, -) diff --git a/wip/SNIP/websockets/extensions/__init__.py b/wip/SNIP/websockets/extensions/__init__.py deleted file mode 100644 index 02838b9..0000000 --- a/wip/SNIP/websockets/extensions/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .base import * - - -__all__ = ["Extension", "ClientExtensionFactory", "ServerExtensionFactory"] diff --git a/wip/SNIP/websockets/extensions/__pycache__/__init__.cpython-313.pyc b/wip/SNIP/websockets/extensions/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index cab597b37d47164081a0eec9ffa5559d61b03de4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 265 zcmey&%ge>Uz`#(r@Kd~`z`!F`L7lA?m0O*=bdH?_b diff --git a/wip/SNIP/websockets/extensions/__pycache__/__init__.cpython-314.pyc b/wip/SNIP/websockets/extensions/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 2315cb2208d99ffcbd93060c882644e42a03ca44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 267 zcmdPql5=<`rk==iL%>&dE&8D}f8RB_@~T7ggR83r;O6 zOD#ed*JQfIl9X7SdW$_iJ~1aJK7J*`XOIhSS-4omcvKdo7G>t=`3J|8XBNZ+`*{Y$ zl&2;Y=O<^UmK4XN!YnP0iI30B%PfhH*DI*J#bJ}1pHiBWYFEU=z`y`Uz`#(r@A(&l?A(+FI31Vsy z7g&r_iXoT_F2)TOy##5}WW2?ln3tDdl30?NpI5BOc#9)AwXifb zFFExk$W%?nTdZzHiMgqoOt*MlD@s!HiZk=`{1S6hZwbM<0f`_LC8VpiQj5^VlR@r*VP*(}@v{>+oMIS?7=sy#n1UILm{l2q znTl9~nPXUkS=1RA7~~j&S%ca17=yV$p^?Rs#Z<%=%pJ@j#gN5P#2(C9#F5Ub$y3D1 zz`*d5hk=2iN|wthu{c#BIVZ8WSRpOHNFf#GuVOtet|E2@28LTKd5O8HexTsUO{|D7 z&a6sJ1_cMmRg4S_3?R(Rz`*#K5A3{XkT@8pGHNnbUE*>{P0mkARY=TJ$jnR5&&|wB zS4abekscS9V^Mms6_{JO(@HW@ z;pV4SB&VjP6e}boz}%3KpvT3vlJSrRxuux1*t`u`FZ}qG3A*BF~NSG0WsyNNyYie*{LPPF^G_l zNlGkE)hnpH#Rhg(>Ma3y`p|=jh=R-p<*WvVFLDh0N*6d3KZC*?9x)&e{)ma@a?OK9 zOnzxedOpbaV3%Wy7=LsXC}9mT2@%#sf(#4{noLE)3=9lKA|SVd5{!aE5y;)Q*iyk3 zBl`*>q6Sh4^_3XJS49#G3=At7G}*w$6tRH}1O;prS7u&`LXCo7eqQP=wjyxEgJm^Y zkW|Wol!NqWa^B*Ij|ZjW`1o5~@$tF&DWy57@$t8K;^PZT6LX+4{PFQ=nMK7VIhlE> zdHM12x7g$3Q}UDJ<8SfD$0HRD@$p4sAfu%~*+Vowz8F+wB*!O~loVwqm6oIy$Hy1R zfE0jS5B4-T%Yg|{wkwurU|?ux_{hY}%JG4Vfm`A-i}(!|`3JU+-x>H>mA)%+vnqjo z4%UIaFmMCsHUfnKXD}-`>$Ax*AYd?8FdJBuJ(#_SBb`H&y9gBMFHJxNf)RRwkPI%k zbc-`nQW1rNLK?XI$^>USa4As)%KgcpBnL`*APi#TE*EmSoJuouQWOeczId-RLIOLE=kNwP6b64QpxL=nx0>h39ewl zg(kT01^Xiv9D#{Nsd?%pP}>rdazOgQrO8TWaOB-$11m1RB@A;AlEY!8EjY*B5-)%R zYkW~^aY24waVji=^>9Qm4)yLxv8;v^%itXDr^!?VDrJ7jz?urt@Uj{nEwP$xMLMA3 zzyL((GB7aw62?>>3pNOx<25;p4Jsi?l&G5EQ=(uqK2UBCi&frXtF2UF^+iJ?zTUK<0uHcLT#0 zEzF#ao@t8=L3s_7+pDxnN(*vQqjNHgOR(j49R+wH5Ua^SATOJOOvRRsK{*4QJwUmm z*cfXz{=gy%&B3DZ91LP{FmRcC=M;tIV31LMn!KQZ}BJR=j5a& zgF8HWiAl*tpibE>HgG$z7~G(`#a2>TkeQbbspUC9Egtsx_{5x?_;_$x1ukg7&IA{T zkY)?GoyTDVX=d3Kfr<%GJy>kXz`*c7_Qui6mKCv-y`!F`Je`ix= hRQkxE%Bb``fKd&^RAW^79>gfesPu_JjuFx#0swu;&pZGC diff --git a/wip/SNIP/websockets/extensions/__pycache__/base.cpython-314.pyc b/wip/SNIP/websockets/extensions/__pycache__/base.cpython-314.pyc deleted file mode 100644 index a01cf32e011d2f859db81911d9467c9686fbfc71..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4403 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 diff --git a/wip/SNIP/websockets/extensions/base.py b/wip/SNIP/websockets/extensions/base.py deleted file mode 100644 index 2fdc59f..0000000 --- a/wip/SNIP/websockets/extensions/base.py +++ /dev/null @@ -1,123 +0,0 @@ -from __future__ import annotations - -from collections.abc import Sequence - -from ..frames import Frame -from ..typing import ExtensionName, ExtensionParameter - - -__all__ = ["Extension", "ClientExtensionFactory", "ServerExtensionFactory"] - - -class Extension: - """ - Base class for extensions. - - """ - - name: ExtensionName - """Extension identifier.""" - - def decode(self, frame: Frame, *, max_size: int | None = None) -> Frame: - """ - Decode an incoming frame. - - Args: - frame: Incoming frame. - max_size: Maximum payload size in bytes. - - Returns: - Decoded frame. - - Raises: - PayloadTooBig: If decoding the payload exceeds ``max_size``. - - """ - raise NotImplementedError - - def encode(self, frame: Frame) -> Frame: - """ - Encode an outgoing frame. - - Args: - frame: Outgoing frame. - - Returns: - Encoded frame. - - """ - raise NotImplementedError - - -class ClientExtensionFactory: - """ - Base class for client-side extension factories. - - """ - - name: ExtensionName - """Extension identifier.""" - - def get_request_params(self) -> Sequence[ExtensionParameter]: - """ - Build parameters to send to the server for this extension. - - Returns: - Parameters to send to the server. - - """ - raise NotImplementedError - - def process_response_params( - self, - params: Sequence[ExtensionParameter], - accepted_extensions: Sequence[Extension], - ) -> Extension: - """ - Process parameters received from the server. - - Args: - params: Parameters received from the server for this extension. - accepted_extensions: List of previously accepted extensions. - - Returns: - An extension instance. - - Raises: - NegotiationError: If parameters aren't acceptable. - - """ - raise NotImplementedError - - -class ServerExtensionFactory: - """ - Base class for server-side extension factories. - - """ - - name: ExtensionName - """Extension identifier.""" - - def process_request_params( - self, - params: Sequence[ExtensionParameter], - accepted_extensions: Sequence[Extension], - ) -> tuple[list[ExtensionParameter], Extension]: - """ - Process parameters received from the client. - - Args: - params: Parameters received from the client for this extension. - accepted_extensions: List of previously accepted extensions. - - Returns: - To accept the offer, parameters to send to the client for this - extension and an extension instance. - - Raises: - NegotiationError: To reject the offer, if parameters received from - the client aren't acceptable. - - """ - raise NotImplementedError diff --git a/wip/SNIP/websockets/extensions/permessage_deflate.py b/wip/SNIP/websockets/extensions/permessage_deflate.py deleted file mode 100644 index 7e9e7a5..0000000 --- a/wip/SNIP/websockets/extensions/permessage_deflate.py +++ /dev/null @@ -1,697 +0,0 @@ -from __future__ import annotations - -import zlib -from collections.abc import Sequence -from typing import Any, Literal - -from .. import frames -from ..exceptions import ( - DuplicateParameter, - InvalidParameterName, - InvalidParameterValue, - NegotiationError, - PayloadTooBig, - ProtocolError, -) -from ..typing import ExtensionName, ExtensionParameter -from .base import ClientExtensionFactory, Extension, ServerExtensionFactory - - -__all__ = [ - "PerMessageDeflate", - "ClientPerMessageDeflateFactory", - "enable_client_permessage_deflate", - "ServerPerMessageDeflateFactory", - "enable_server_permessage_deflate", -] - -_EMPTY_UNCOMPRESSED_BLOCK = b"\x00\x00\xff\xff" - -_MAX_WINDOW_BITS_VALUES = [str(bits) for bits in range(8, 16)] - - -class PerMessageDeflate(Extension): - """ - Per-Message Deflate extension. - - """ - - name = ExtensionName("permessage-deflate") - - def __init__( - self, - remote_no_context_takeover: bool, - local_no_context_takeover: bool, - remote_max_window_bits: int, - local_max_window_bits: int, - compress_settings: dict[Any, Any] | None = None, - ) -> None: - """ - Configure the Per-Message Deflate extension. - - """ - if compress_settings is None: - compress_settings = {} - - assert remote_no_context_takeover in [False, True] - assert local_no_context_takeover in [False, True] - assert 8 <= remote_max_window_bits <= 15 - assert 8 <= local_max_window_bits <= 15 - assert "wbits" not in compress_settings - - self.remote_no_context_takeover = remote_no_context_takeover - self.local_no_context_takeover = local_no_context_takeover - self.remote_max_window_bits = remote_max_window_bits - self.local_max_window_bits = local_max_window_bits - self.compress_settings = compress_settings - - if not self.remote_no_context_takeover: - self.decoder = zlib.decompressobj(wbits=-self.remote_max_window_bits) - - if not self.local_no_context_takeover: - self.encoder = zlib.compressobj( - wbits=-self.local_max_window_bits, - **self.compress_settings, - ) - - # To handle continuation frames properly, we must keep track of - # whether that initial frame was encoded. - self.decode_cont_data = False - # There's no need for self.encode_cont_data because we always encode - # outgoing frames, so it would always be True. - - def __repr__(self) -> str: - return ( - f"PerMessageDeflate(" - f"remote_no_context_takeover={self.remote_no_context_takeover}, " - f"local_no_context_takeover={self.local_no_context_takeover}, " - f"remote_max_window_bits={self.remote_max_window_bits}, " - f"local_max_window_bits={self.local_max_window_bits})" - ) - - def decode( - self, - frame: frames.Frame, - *, - max_size: int | None = None, - ) -> frames.Frame: - """ - Decode an incoming frame. - - """ - # Skip control frames. - if frame.opcode in frames.CTRL_OPCODES: - return frame - - # Handle continuation data frames: - # - skip if the message isn't encoded - # - reset "decode continuation data" flag if it's a final frame - if frame.opcode is frames.OP_CONT: - if not self.decode_cont_data: - return frame - if frame.fin: - self.decode_cont_data = False - - # Handle text and binary data frames: - # - skip if the message isn't encoded - # - unset the rsv1 flag on the first frame of a compressed message - # - set "decode continuation data" flag if it's a non-final frame - else: - if not frame.rsv1: - return frame - if not frame.fin: - self.decode_cont_data = True - - # Re-initialize per-message decoder. - if self.remote_no_context_takeover: - self.decoder = zlib.decompressobj(wbits=-self.remote_max_window_bits) - - # Uncompress data. Protect against zip bombs by preventing zlib from - # decompressing more than max_length bytes (except when the limit is - # disabled with max_size = None). - if frame.fin and len(frame.data) < 2044: - # Profiling shows that appending four bytes, which makes a copy, is - # faster than calling decompress() again when data is less than 2kB. - data = bytes(frame.data) + _EMPTY_UNCOMPRESSED_BLOCK - else: - data = frame.data - max_length = 0 if max_size is None else max_size - try: - data = self.decoder.decompress(data, max_length) - if self.decoder.unconsumed_tail: - assert max_size is not None # help mypy - raise PayloadTooBig(None, max_size) - if frame.fin and len(frame.data) >= 2044: - # This cannot generate additional data. - self.decoder.decompress(_EMPTY_UNCOMPRESSED_BLOCK) - except zlib.error as exc: - raise ProtocolError("decompression failed") from exc - - # Allow garbage collection of the decoder if it won't be reused. - if frame.fin and self.remote_no_context_takeover: - del self.decoder - - return frames.Frame( - frame.opcode, - data, - frame.fin, - # Unset the rsv1 flag on the first frame of a compressed message. - False, - frame.rsv2, - frame.rsv3, - ) - - def encode(self, frame: frames.Frame) -> frames.Frame: - """ - Encode an outgoing frame. - - """ - # Skip control frames. - if frame.opcode in frames.CTRL_OPCODES: - return frame - - # Since we always encode messages, there's no "encode continuation - # data" flag similar to "decode continuation data" at this time. - - if frame.opcode is not frames.OP_CONT: - # Re-initialize per-message decoder. - if self.local_no_context_takeover: - self.encoder = zlib.compressobj( - wbits=-self.local_max_window_bits, - **self.compress_settings, - ) - - # Compress data. - data = self.encoder.compress(frame.data) + self.encoder.flush(zlib.Z_SYNC_FLUSH) - if frame.fin: - # Sync flush generates between 5 or 6 bytes, ending with the bytes - # 0x00 0x00 0xff 0xff, which must be removed. - assert data[-4:] == _EMPTY_UNCOMPRESSED_BLOCK - # Making a copy is faster than memoryview(a)[:-4] until 2kB. - if len(data) < 2048: - data = data[:-4] - else: - data = memoryview(data)[:-4] - - # Allow garbage collection of the encoder if it won't be reused. - if frame.fin and self.local_no_context_takeover: - del self.encoder - - return frames.Frame( - frame.opcode, - data, - frame.fin, - # Set the rsv1 flag on the first frame of a compressed message. - frame.opcode is not frames.OP_CONT, - frame.rsv2, - frame.rsv3, - ) - - -def _build_parameters( - server_no_context_takeover: bool, - client_no_context_takeover: bool, - server_max_window_bits: int | None, - client_max_window_bits: int | Literal[True] | None, -) -> list[ExtensionParameter]: - """ - Build a list of ``(name, value)`` pairs for some compression parameters. - - """ - params: list[ExtensionParameter] = [] - if server_no_context_takeover: - params.append(("server_no_context_takeover", None)) - if client_no_context_takeover: - params.append(("client_no_context_takeover", None)) - if server_max_window_bits: - params.append(("server_max_window_bits", str(server_max_window_bits))) - if client_max_window_bits is True: # only in handshake requests - params.append(("client_max_window_bits", None)) - elif client_max_window_bits: - params.append(("client_max_window_bits", str(client_max_window_bits))) - return params - - -def _extract_parameters( - params: Sequence[ExtensionParameter], *, is_server: bool -) -> tuple[bool, bool, int | None, int | Literal[True] | None]: - """ - Extract compression parameters from a list of ``(name, value)`` pairs. - - If ``is_server`` is :obj:`True`, ``client_max_window_bits`` may be - provided without a value. This is only allowed in handshake requests. - - """ - server_no_context_takeover: bool = False - client_no_context_takeover: bool = False - server_max_window_bits: int | None = None - client_max_window_bits: int | Literal[True] | None = None - - for name, value in params: - if name == "server_no_context_takeover": - if server_no_context_takeover: - raise DuplicateParameter(name) - if value is None: - server_no_context_takeover = True - else: - raise InvalidParameterValue(name, value) - - elif name == "client_no_context_takeover": - if client_no_context_takeover: - raise DuplicateParameter(name) - if value is None: - client_no_context_takeover = True - else: - raise InvalidParameterValue(name, value) - - elif name == "server_max_window_bits": - if server_max_window_bits is not None: - raise DuplicateParameter(name) - if value in _MAX_WINDOW_BITS_VALUES: - server_max_window_bits = int(value) - else: - raise InvalidParameterValue(name, value) - - elif name == "client_max_window_bits": - if client_max_window_bits is not None: - raise DuplicateParameter(name) - if is_server and value is None: # only in handshake requests - client_max_window_bits = True - elif value in _MAX_WINDOW_BITS_VALUES: - client_max_window_bits = int(value) - else: - raise InvalidParameterValue(name, value) - - else: - raise InvalidParameterName(name) - - return ( - server_no_context_takeover, - client_no_context_takeover, - server_max_window_bits, - client_max_window_bits, - ) - - -class ClientPerMessageDeflateFactory(ClientExtensionFactory): - """ - Client-side extension factory for the Per-Message Deflate extension. - - Parameters behave as described in `section 7.1 of RFC 7692`_. - - .. _section 7.1 of RFC 7692: https://datatracker.ietf.org/doc/html/rfc7692#section-7.1 - - Set them to :obj:`True` to include them in the negotiation offer without a - value or to an integer value to include them with this value. - - Args: - server_no_context_takeover: Prevent server from using context takeover. - client_no_context_takeover: Prevent client from using context takeover. - server_max_window_bits: Maximum size of the server's LZ77 sliding window - in bits, between 8 and 15. - client_max_window_bits: Maximum size of the client's LZ77 sliding window - in bits, between 8 and 15, or :obj:`True` to indicate support without - setting a limit. - compress_settings: Additional keyword arguments for :func:`zlib.compressobj`, - excluding ``wbits``. - - """ - - name = ExtensionName("permessage-deflate") - - def __init__( - self, - server_no_context_takeover: bool = False, - client_no_context_takeover: bool = False, - server_max_window_bits: int | None = None, - client_max_window_bits: int | Literal[True] | None = True, - compress_settings: dict[str, Any] | None = None, - ) -> None: - """ - Configure the Per-Message Deflate extension factory. - - """ - if not (server_max_window_bits is None or 8 <= server_max_window_bits <= 15): - raise ValueError("server_max_window_bits must be between 8 and 15") - if not ( - client_max_window_bits is None - or client_max_window_bits is True - or 8 <= client_max_window_bits <= 15 - ): - raise ValueError("client_max_window_bits must be between 8 and 15") - if compress_settings is not None and "wbits" in compress_settings: - raise ValueError( - "compress_settings must not include wbits, " - "set client_max_window_bits instead" - ) - - self.server_no_context_takeover = server_no_context_takeover - self.client_no_context_takeover = client_no_context_takeover - self.server_max_window_bits = server_max_window_bits - self.client_max_window_bits = client_max_window_bits - self.compress_settings = compress_settings - - def get_request_params(self) -> Sequence[ExtensionParameter]: - """ - Build request parameters. - - """ - return _build_parameters( - self.server_no_context_takeover, - self.client_no_context_takeover, - self.server_max_window_bits, - self.client_max_window_bits, - ) - - def process_response_params( - self, - params: Sequence[ExtensionParameter], - accepted_extensions: Sequence[Extension], - ) -> PerMessageDeflate: - """ - Process response parameters. - - Return an extension instance. - - """ - if any(other.name == self.name for other in accepted_extensions): - raise NegotiationError(f"received duplicate {self.name}") - - # Request parameters are available in instance variables. - - # Load response parameters in local variables. - ( - server_no_context_takeover, - client_no_context_takeover, - server_max_window_bits, - client_max_window_bits, - ) = _extract_parameters(params, is_server=False) - - # After comparing the request and the response, the final - # configuration must be available in the local variables. - - # server_no_context_takeover - # - # Req. Resp. Result - # ------ ------ -------------------------------------------------- - # False False False - # False True True - # True False Error! - # True True True - - if self.server_no_context_takeover: - if not server_no_context_takeover: - raise NegotiationError("expected server_no_context_takeover") - - # client_no_context_takeover - # - # Req. Resp. Result - # ------ ------ -------------------------------------------------- - # False False False - # False True True - # True False True - must change value - # True True True - - if self.client_no_context_takeover: - if not client_no_context_takeover: - client_no_context_takeover = True - - # server_max_window_bits - - # Req. Resp. Result - # ------ ------ -------------------------------------------------- - # None None None - # None 8≤M≤15 M - # 8≤N≤15 None Error! - # 8≤N≤15 8≤M≤N M - # 8≤N≤15 N self.server_max_window_bits: - raise NegotiationError("unsupported server_max_window_bits") - - # client_max_window_bits - - # Req. Resp. Result - # ------ ------ -------------------------------------------------- - # None None None - # None 8≤M≤15 Error! - # True None None - # True 8≤M≤15 M - # 8≤N≤15 None N - must change value - # 8≤N≤15 8≤M≤N M - # 8≤N≤15 N self.client_max_window_bits: - raise NegotiationError("unsupported client_max_window_bits") - - return PerMessageDeflate( - server_no_context_takeover, # remote_no_context_takeover - client_no_context_takeover, # local_no_context_takeover - server_max_window_bits or 15, # remote_max_window_bits - client_max_window_bits or 15, # local_max_window_bits - self.compress_settings, - ) - - -def enable_client_permessage_deflate( - extensions: Sequence[ClientExtensionFactory] | None, -) -> Sequence[ClientExtensionFactory]: - """ - Enable Per-Message Deflate with default settings in client extensions. - - If the extension is already present, perhaps with non-default settings, - the configuration isn't changed. - - """ - if extensions is None: - extensions = [] - if not any( - extension_factory.name == ClientPerMessageDeflateFactory.name - for extension_factory in extensions - ): - extensions = list(extensions) + [ - ClientPerMessageDeflateFactory( - compress_settings={"memLevel": 5}, - ) - ] - return extensions - - -class ServerPerMessageDeflateFactory(ServerExtensionFactory): - """ - Server-side extension factory for the Per-Message Deflate extension. - - Parameters behave as described in `section 7.1 of RFC 7692`_. - - .. _section 7.1 of RFC 7692: https://datatracker.ietf.org/doc/html/rfc7692#section-7.1 - - Set them to :obj:`True` to include them in the negotiation offer without a - value or to an integer value to include them with this value. - - Args: - server_no_context_takeover: Prevent server from using context takeover. - client_no_context_takeover: Prevent client from using context takeover. - server_max_window_bits: Maximum size of the server's LZ77 sliding window - in bits, between 8 and 15. - client_max_window_bits: Maximum size of the client's LZ77 sliding window - in bits, between 8 and 15. - compress_settings: Additional keyword arguments for :func:`zlib.compressobj`, - excluding ``wbits``. - require_client_max_window_bits: Do not enable compression at all if - client doesn't advertise support for ``client_max_window_bits``; - the default behavior is to enable compression without enforcing - ``client_max_window_bits``. - - """ - - name = ExtensionName("permessage-deflate") - - def __init__( - self, - server_no_context_takeover: bool = False, - client_no_context_takeover: bool = False, - server_max_window_bits: int | None = None, - client_max_window_bits: int | None = None, - compress_settings: dict[str, Any] | None = None, - require_client_max_window_bits: bool = False, - ) -> None: - """ - Configure the Per-Message Deflate extension factory. - - """ - if not (server_max_window_bits is None or 8 <= server_max_window_bits <= 15): - raise ValueError("server_max_window_bits must be between 8 and 15") - if not (client_max_window_bits is None or 8 <= client_max_window_bits <= 15): - raise ValueError("client_max_window_bits must be between 8 and 15") - if compress_settings is not None and "wbits" in compress_settings: - raise ValueError( - "compress_settings must not include wbits, " - "set server_max_window_bits instead" - ) - if client_max_window_bits is None and require_client_max_window_bits: - raise ValueError( - "require_client_max_window_bits is enabled, " - "but client_max_window_bits isn't configured" - ) - - self.server_no_context_takeover = server_no_context_takeover - self.client_no_context_takeover = client_no_context_takeover - self.server_max_window_bits = server_max_window_bits - self.client_max_window_bits = client_max_window_bits - self.compress_settings = compress_settings - self.require_client_max_window_bits = require_client_max_window_bits - - def process_request_params( - self, - params: Sequence[ExtensionParameter], - accepted_extensions: Sequence[Extension], - ) -> tuple[list[ExtensionParameter], PerMessageDeflate]: - """ - Process request parameters. - - Return response params and an extension instance. - - """ - if any(other.name == self.name for other in accepted_extensions): - raise NegotiationError(f"skipped duplicate {self.name}") - - # Load request parameters in local variables. - ( - server_no_context_takeover, - client_no_context_takeover, - server_max_window_bits, - client_max_window_bits, - ) = _extract_parameters(params, is_server=True) - - # Configuration parameters are available in instance variables. - - # After comparing the request and the configuration, the response must - # be available in the local variables. - - # server_no_context_takeover - # - # Config Req. Resp. - # ------ ------ -------------------------------------------------- - # False False False - # False True True - # True False True - must change value to True - # True True True - - if self.server_no_context_takeover: - if not server_no_context_takeover: - server_no_context_takeover = True - - # client_no_context_takeover - # - # Config Req. Resp. - # ------ ------ -------------------------------------------------- - # False False False - # False True True (or False) - # True False True - must change value to True - # True True True (or False) - - if self.client_no_context_takeover: - if not client_no_context_takeover: - client_no_context_takeover = True - - # server_max_window_bits - - # Config Req. Resp. - # ------ ------ -------------------------------------------------- - # None None None - # None 8≤M≤15 M - # 8≤N≤15 None N - must change value - # 8≤N≤15 8≤M≤N M - # 8≤N≤15 N self.server_max_window_bits: - server_max_window_bits = self.server_max_window_bits - - # client_max_window_bits - - # Config Req. Resp. - # ------ ------ -------------------------------------------------- - # None None None - # None True None - must change value - # None 8≤M≤15 M (or None) - # 8≤N≤15 None None or Error! - # 8≤N≤15 True N - must change value - # 8≤N≤15 8≤M≤N M (or None) - # 8≤N≤15 N Sequence[ServerExtensionFactory]: - """ - Enable Per-Message Deflate with default settings in server extensions. - - If the extension is already present, perhaps with non-default settings, - the configuration isn't changed. - - """ - if extensions is None: - extensions = [] - if not any( - ext_factory.name == ServerPerMessageDeflateFactory.name - for ext_factory in extensions - ): - extensions = list(extensions) + [ - ServerPerMessageDeflateFactory( - server_max_window_bits=12, - client_max_window_bits=12, - compress_settings={"memLevel": 5}, - ) - ] - return extensions diff --git a/wip/SNIP/websockets/frames.py b/wip/SNIP/websockets/frames.py deleted file mode 100644 index ab0869d..0000000 --- a/wip/SNIP/websockets/frames.py +++ /dev/null @@ -1,430 +0,0 @@ -from __future__ import annotations - -import dataclasses -import enum -import io -import os -import secrets -import struct -from collections.abc import Generator, Sequence -from typing import Callable, Union - -from .exceptions import PayloadTooBig, ProtocolError - - -try: - from .speedups import apply_mask -except ImportError: - from .utils import apply_mask - - -__all__ = [ - "Opcode", - "OP_CONT", - "OP_TEXT", - "OP_BINARY", - "OP_CLOSE", - "OP_PING", - "OP_PONG", - "DATA_OPCODES", - "CTRL_OPCODES", - "CloseCode", - "Frame", - "Close", -] - - -class Opcode(enum.IntEnum): - """Opcode values for WebSocket frames.""" - - CONT, TEXT, BINARY = 0x00, 0x01, 0x02 - CLOSE, PING, PONG = 0x08, 0x09, 0x0A - - -OP_CONT = Opcode.CONT -OP_TEXT = Opcode.TEXT -OP_BINARY = Opcode.BINARY -OP_CLOSE = Opcode.CLOSE -OP_PING = Opcode.PING -OP_PONG = Opcode.PONG - -DATA_OPCODES = OP_CONT, OP_TEXT, OP_BINARY -CTRL_OPCODES = OP_CLOSE, OP_PING, OP_PONG - - -class CloseCode(enum.IntEnum): - """Close code values for WebSocket close frames.""" - - NORMAL_CLOSURE = 1000 - GOING_AWAY = 1001 - PROTOCOL_ERROR = 1002 - UNSUPPORTED_DATA = 1003 - # 1004 is reserved - NO_STATUS_RCVD = 1005 - ABNORMAL_CLOSURE = 1006 - INVALID_DATA = 1007 - POLICY_VIOLATION = 1008 - MESSAGE_TOO_BIG = 1009 - MANDATORY_EXTENSION = 1010 - INTERNAL_ERROR = 1011 - SERVICE_RESTART = 1012 - TRY_AGAIN_LATER = 1013 - BAD_GATEWAY = 1014 - TLS_HANDSHAKE = 1015 - - -# See https://www.iana.org/assignments/websocket/websocket.xhtml -CLOSE_CODE_EXPLANATIONS: dict[int, str] = { - CloseCode.NORMAL_CLOSURE: "OK", - CloseCode.GOING_AWAY: "going away", - CloseCode.PROTOCOL_ERROR: "protocol error", - CloseCode.UNSUPPORTED_DATA: "unsupported data", - CloseCode.NO_STATUS_RCVD: "no status received [internal]", - CloseCode.ABNORMAL_CLOSURE: "abnormal closure [internal]", - CloseCode.INVALID_DATA: "invalid frame payload data", - CloseCode.POLICY_VIOLATION: "policy violation", - CloseCode.MESSAGE_TOO_BIG: "message too big", - CloseCode.MANDATORY_EXTENSION: "mandatory extension", - CloseCode.INTERNAL_ERROR: "internal error", - CloseCode.SERVICE_RESTART: "service restart", - CloseCode.TRY_AGAIN_LATER: "try again later", - CloseCode.BAD_GATEWAY: "bad gateway", - CloseCode.TLS_HANDSHAKE: "TLS handshake failure [internal]", -} - - -# Close code that are allowed in a close frame. -# Using a set optimizes `code in EXTERNAL_CLOSE_CODES`. -EXTERNAL_CLOSE_CODES = { - CloseCode.NORMAL_CLOSURE, - CloseCode.GOING_AWAY, - CloseCode.PROTOCOL_ERROR, - CloseCode.UNSUPPORTED_DATA, - CloseCode.INVALID_DATA, - CloseCode.POLICY_VIOLATION, - CloseCode.MESSAGE_TOO_BIG, - CloseCode.MANDATORY_EXTENSION, - CloseCode.INTERNAL_ERROR, - CloseCode.SERVICE_RESTART, - CloseCode.TRY_AGAIN_LATER, - CloseCode.BAD_GATEWAY, -} - - -OK_CLOSE_CODES = { - CloseCode.NORMAL_CLOSURE, - CloseCode.GOING_AWAY, - CloseCode.NO_STATUS_RCVD, -} - - -BytesLike = bytes, bytearray, memoryview - - -@dataclasses.dataclass -class Frame: - """ - WebSocket frame. - - Attributes: - opcode: Opcode. - data: Payload data. - fin: FIN bit. - rsv1: RSV1 bit. - rsv2: RSV2 bit. - rsv3: RSV3 bit. - - Only these fields are needed. The MASK bit, payload length and masking-key - are handled on the fly when parsing and serializing frames. - - """ - - opcode: Opcode - data: Union[bytes, bytearray, memoryview] - fin: bool = True - rsv1: bool = False - rsv2: bool = False - rsv3: bool = False - - # Configure if you want to see more in logs. Should be a multiple of 3. - MAX_LOG_SIZE = int(os.environ.get("WEBSOCKETS_MAX_LOG_SIZE", "75")) - - def __str__(self) -> str: - """ - Return a human-readable representation of a frame. - - """ - coding = None - length = f"{len(self.data)} byte{'' if len(self.data) == 1 else 's'}" - non_final = "" if self.fin else "continued" - - if self.opcode is OP_TEXT: - # Decoding only the beginning and the end is needlessly hard. - # Decode the entire payload then elide later if necessary. - data = repr(bytes(self.data).decode()) - elif self.opcode is OP_BINARY: - # We'll show at most the first 16 bytes and the last 8 bytes. - # Encode just what we need, plus two dummy bytes to elide later. - binary = self.data - if len(binary) > self.MAX_LOG_SIZE // 3: - cut = (self.MAX_LOG_SIZE // 3 - 1) // 3 # by default cut = 8 - binary = b"".join([binary[: 2 * cut], b"\x00\x00", binary[-cut:]]) - data = " ".join(f"{byte:02x}" for byte in binary) - elif self.opcode is OP_CLOSE: - data = str(Close.parse(self.data)) - elif self.data: - # We don't know if a Continuation frame contains text or binary. - # Ping and Pong frames could contain UTF-8. - # Attempt to decode as UTF-8 and display it as text; fallback to - # binary. If self.data is a memoryview, it has no decode() method, - # which raises AttributeError. - try: - data = repr(bytes(self.data).decode()) - coding = "text" - except (UnicodeDecodeError, AttributeError): - binary = self.data - if len(binary) > self.MAX_LOG_SIZE // 3: - cut = (self.MAX_LOG_SIZE // 3 - 1) // 3 # by default cut = 8 - binary = b"".join([binary[: 2 * cut], b"\x00\x00", binary[-cut:]]) - data = " ".join(f"{byte:02x}" for byte in binary) - coding = "binary" - else: - data = "''" - - if len(data) > self.MAX_LOG_SIZE: - cut = self.MAX_LOG_SIZE // 3 - 1 # by default cut = 24 - data = data[: 2 * cut] + "..." + data[-cut:] - - metadata = ", ".join(filter(None, [coding, length, non_final])) - - return f"{self.opcode.name} {data} [{metadata}]" - - @classmethod - def parse( - cls, - read_exact: Callable[[int], Generator[None, None, bytes]], - *, - mask: bool, - max_size: int | None = None, - extensions: Sequence[extensions.Extension] | None = None, - ) -> Generator[None, None, Frame]: - """ - Parse a WebSocket frame. - - This is a generator-based coroutine. - - Args: - read_exact: Generator-based coroutine that reads the requested - bytes or raises an exception if there isn't enough data. - mask: Whether the frame should be masked i.e. whether the read - happens on the server side. - max_size: Maximum payload size in bytes. - extensions: List of extensions, applied in reverse order. - - Raises: - EOFError: If the connection is closed without a full WebSocket frame. - PayloadTooBig: If the frame's payload size exceeds ``max_size``. - ProtocolError: If the frame contains incorrect values. - - """ - # Read the header. - data = yield from read_exact(2) - head1, head2 = struct.unpack("!BB", data) - - # While not Pythonic, this is marginally faster than calling bool(). - fin = True if head1 & 0b10000000 else False - rsv1 = True if head1 & 0b01000000 else False - rsv2 = True if head1 & 0b00100000 else False - rsv3 = True if head1 & 0b00010000 else False - - try: - opcode = Opcode(head1 & 0b00001111) - except ValueError as exc: - raise ProtocolError("invalid opcode") from exc - - if (True if head2 & 0b10000000 else False) != mask: - raise ProtocolError("incorrect masking") - - length = head2 & 0b01111111 - if length == 126: - data = yield from read_exact(2) - (length,) = struct.unpack("!H", data) - elif length == 127: - data = yield from read_exact(8) - (length,) = struct.unpack("!Q", data) - if max_size is not None and length > max_size: - raise PayloadTooBig(length, max_size) - if mask: - mask_bytes = yield from read_exact(4) - - # Read the data. - data = yield from read_exact(length) - if mask: - data = apply_mask(data, mask_bytes) - - frame = cls(opcode, data, fin, rsv1, rsv2, rsv3) - - if extensions is None: - extensions = [] - for extension in reversed(extensions): - frame = extension.decode(frame, max_size=max_size) - - frame.check() - - return frame - - def serialize( - self, - *, - mask: bool, - extensions: Sequence[extensions.Extension] | None = None, - ) -> bytes: - """ - Serialize a WebSocket frame. - - Args: - mask: Whether the frame should be masked i.e. whether the write - happens on the client side. - extensions: List of extensions, applied in order. - - Raises: - ProtocolError: If the frame contains incorrect values. - - """ - self.check() - - if extensions is None: - extensions = [] - for extension in extensions: - self = extension.encode(self) - - output = io.BytesIO() - - # Prepare the header. - head1 = ( - (0b10000000 if self.fin else 0) - | (0b01000000 if self.rsv1 else 0) - | (0b00100000 if self.rsv2 else 0) - | (0b00010000 if self.rsv3 else 0) - | self.opcode - ) - - head2 = 0b10000000 if mask else 0 - - length = len(self.data) - if length < 126: - output.write(struct.pack("!BB", head1, head2 | length)) - elif length < 65536: - output.write(struct.pack("!BBH", head1, head2 | 126, length)) - else: - output.write(struct.pack("!BBQ", head1, head2 | 127, length)) - - if mask: - mask_bytes = secrets.token_bytes(4) - output.write(mask_bytes) - - # Prepare the data. - if mask: - data = apply_mask(self.data, mask_bytes) - else: - data = self.data - output.write(data) - - return output.getvalue() - - def check(self) -> None: - """ - Check that reserved bits and opcode have acceptable values. - - Raises: - ProtocolError: If a reserved bit or the opcode is invalid. - - """ - if self.rsv1 or self.rsv2 or self.rsv3: - raise ProtocolError("reserved bits must be 0") - - if self.opcode in CTRL_OPCODES: - if len(self.data) > 125: - raise ProtocolError("control frame too long") - if not self.fin: - raise ProtocolError("fragmented control frame") - - -@dataclasses.dataclass -class Close: - """ - Code and reason for WebSocket close frames. - - Attributes: - code: Close code. - reason: Close reason. - - """ - - code: int - reason: str - - def __str__(self) -> str: - """ - Return a human-readable representation of a close code and reason. - - """ - if 3000 <= self.code < 4000: - explanation = "registered" - elif 4000 <= self.code < 5000: - explanation = "private use" - else: - explanation = CLOSE_CODE_EXPLANATIONS.get(self.code, "unknown") - result = f"{self.code} ({explanation})" - - if self.reason: - result = f"{result} {self.reason}" - - return result - - @classmethod - def parse(cls, data: bytes) -> Close: - """ - Parse the payload of a close frame. - - Args: - data: Payload of the close frame. - - Raises: - ProtocolError: If data is ill-formed. - UnicodeDecodeError: If the reason isn't valid UTF-8. - - """ - if len(data) >= 2: - (code,) = struct.unpack("!H", data[:2]) - reason = data[2:].decode() - close = cls(code, reason) - close.check() - return close - elif len(data) == 0: - return cls(CloseCode.NO_STATUS_RCVD, "") - else: - raise ProtocolError("close frame too short") - - def serialize(self) -> bytes: - """ - Serialize the payload of a close frame. - - """ - self.check() - return struct.pack("!H", self.code) + self.reason.encode() - - def check(self) -> None: - """ - Check that the close code has a valid value for a close frame. - - Raises: - ProtocolError: If the close code is invalid. - - """ - if not (self.code in EXTERNAL_CLOSE_CODES or 3000 <= self.code < 5000): - raise ProtocolError("invalid status code") - - -# At the bottom to break import cycles created by type annotations. -from . import extensions # noqa: E402 diff --git a/wip/SNIP/websockets/headers.py b/wip/SNIP/websockets/headers.py deleted file mode 100644 index e05ff5b..0000000 --- a/wip/SNIP/websockets/headers.py +++ /dev/null @@ -1,586 +0,0 @@ -from __future__ import annotations - -import base64 -import binascii -import ipaddress -import re -from collections.abc import Sequence -from typing import Callable, TypeVar, cast - -from .exceptions import InvalidHeaderFormat, InvalidHeaderValue -from .typing import ( - ConnectionOption, - ExtensionHeader, - ExtensionName, - ExtensionParameter, - Subprotocol, - UpgradeProtocol, -) - - -__all__ = [ - "build_host", - "parse_connection", - "parse_upgrade", - "parse_extension", - "build_extension", - "parse_subprotocol", - "build_subprotocol", - "validate_subprotocols", - "build_www_authenticate_basic", - "parse_authorization_basic", - "build_authorization_basic", -] - - -T = TypeVar("T") - - -def build_host( - host: str, - port: int, - secure: bool, - *, - always_include_port: bool = False, -) -> str: - """ - Build a ``Host`` header. - - """ - # https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2 - # IPv6 addresses must be enclosed in brackets. - try: - address = ipaddress.ip_address(host) - except ValueError: - # host is a hostname - pass - else: - # host is an IP address - if address.version == 6: - host = f"[{host}]" - - if always_include_port or port != (443 if secure else 80): - host = f"{host}:{port}" - - return host - - -# To avoid a dependency on a parsing library, we implement manually the ABNF -# described in https://datatracker.ietf.org/doc/html/rfc6455#section-9.1 and -# https://datatracker.ietf.org/doc/html/rfc7230#appendix-B. - - -def peek_ahead(header: str, pos: int) -> str | None: - """ - Return the next character from ``header`` at the given position. - - Return :obj:`None` at the end of ``header``. - - We never need to peek more than one character ahead. - - """ - return None if pos == len(header) else header[pos] - - -_OWS_re = re.compile(r"[\t ]*") - - -def parse_OWS(header: str, pos: int) -> int: - """ - Parse optional whitespace from ``header`` at the given position. - - Return the new position. - - The whitespace itself isn't returned because it isn't significant. - - """ - # There's always a match, possibly empty, whose content doesn't matter. - match = _OWS_re.match(header, pos) - assert match is not None - return match.end() - - -_token_re = re.compile(r"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+") - - -def parse_token(header: str, pos: int, header_name: str) -> tuple[str, int]: - """ - Parse a token from ``header`` at the given position. - - Return the token value and the new position. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - match = _token_re.match(header, pos) - if match is None: - raise InvalidHeaderFormat(header_name, "expected token", header, pos) - return match.group(), match.end() - - -_quoted_string_re = re.compile( - r'"(?:[\x09\x20-\x21\x23-\x5b\x5d-\x7e]|\\[\x09\x20-\x7e\x80-\xff])*"' -) - - -_unquote_re = re.compile(r"\\([\x09\x20-\x7e\x80-\xff])") - - -def parse_quoted_string(header: str, pos: int, header_name: str) -> tuple[str, int]: - """ - Parse a quoted string from ``header`` at the given position. - - Return the unquoted value and the new position. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - match = _quoted_string_re.match(header, pos) - if match is None: - raise InvalidHeaderFormat(header_name, "expected quoted string", header, pos) - return _unquote_re.sub(r"\1", match.group()[1:-1]), match.end() - - -_quotable_re = re.compile(r"[\x09\x20-\x7e\x80-\xff]*") - - -_quote_re = re.compile(r"([\x22\x5c])") - - -def build_quoted_string(value: str) -> str: - """ - Format ``value`` as a quoted string. - - This is the reverse of :func:`parse_quoted_string`. - - """ - match = _quotable_re.fullmatch(value) - if match is None: - raise ValueError("invalid characters for quoted-string encoding") - return '"' + _quote_re.sub(r"\\\1", value) + '"' - - -def parse_list( - parse_item: Callable[[str, int, str], tuple[T, int]], - header: str, - pos: int, - header_name: str, -) -> list[T]: - """ - Parse a comma-separated list from ``header`` at the given position. - - This is appropriate for parsing values with the following grammar: - - 1#item - - ``parse_item`` parses one item. - - ``header`` is assumed not to start or end with whitespace. - - (This function is designed for parsing an entire header value and - :func:`~websockets.http.read_headers` strips whitespace from values.) - - Return a list of items. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - # Per https://datatracker.ietf.org/doc/html/rfc7230#section-7, "a recipient - # MUST parse and ignore a reasonable number of empty list elements"; - # hence while loops that remove extra delimiters. - - # Remove extra delimiters before the first item. - while peek_ahead(header, pos) == ",": - pos = parse_OWS(header, pos + 1) - - items = [] - while True: - # Loop invariant: a item starts at pos in header. - item, pos = parse_item(header, pos, header_name) - items.append(item) - pos = parse_OWS(header, pos) - - # We may have reached the end of the header. - if pos == len(header): - break - - # There must be a delimiter after each element except the last one. - if peek_ahead(header, pos) == ",": - pos = parse_OWS(header, pos + 1) - else: - raise InvalidHeaderFormat(header_name, "expected comma", header, pos) - - # Remove extra delimiters before the next item. - while peek_ahead(header, pos) == ",": - pos = parse_OWS(header, pos + 1) - - # We may have reached the end of the header. - if pos == len(header): - break - - # Since we only advance in the header by one character with peek_ahead() - # or with the end position of a regex match, we can't overshoot the end. - assert pos == len(header) - - return items - - -def parse_connection_option( - header: str, pos: int, header_name: str -) -> tuple[ConnectionOption, int]: - """ - Parse a Connection option from ``header`` at the given position. - - Return the protocol value and the new position. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - item, pos = parse_token(header, pos, header_name) - return cast(ConnectionOption, item), pos - - -def parse_connection(header: str) -> list[ConnectionOption]: - """ - Parse a ``Connection`` header. - - Return a list of HTTP connection options. - - Args - header: value of the ``Connection`` header. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - return parse_list(parse_connection_option, header, 0, "Connection") - - -_protocol_re = re.compile( - r"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+(?:/[-!#$%&\'*+.^_`|~0-9a-zA-Z]+)?" -) - - -def parse_upgrade_protocol( - header: str, pos: int, header_name: str -) -> tuple[UpgradeProtocol, int]: - """ - Parse an Upgrade protocol from ``header`` at the given position. - - Return the protocol value and the new position. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - match = _protocol_re.match(header, pos) - if match is None: - raise InvalidHeaderFormat(header_name, "expected protocol", header, pos) - return cast(UpgradeProtocol, match.group()), match.end() - - -def parse_upgrade(header: str) -> list[UpgradeProtocol]: - """ - Parse an ``Upgrade`` header. - - Return a list of HTTP protocols. - - Args: - header: Value of the ``Upgrade`` header. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - return parse_list(parse_upgrade_protocol, header, 0, "Upgrade") - - -def parse_extension_item_param( - header: str, pos: int, header_name: str -) -> tuple[ExtensionParameter, int]: - """ - Parse a single extension parameter from ``header`` at the given position. - - Return a ``(name, value)`` pair and the new position. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - # Extract parameter name. - name, pos = parse_token(header, pos, header_name) - pos = parse_OWS(header, pos) - # Extract parameter value, if there is one. - value: str | None = None - if peek_ahead(header, pos) == "=": - pos = parse_OWS(header, pos + 1) - if peek_ahead(header, pos) == '"': - pos_before = pos # for proper error reporting below - value, pos = parse_quoted_string(header, pos, header_name) - # https://datatracker.ietf.org/doc/html/rfc6455#section-9.1 says: - # the value after quoted-string unescaping MUST conform to - # the 'token' ABNF. - if _token_re.fullmatch(value) is None: - raise InvalidHeaderFormat( - header_name, "invalid quoted header content", header, pos_before - ) - else: - value, pos = parse_token(header, pos, header_name) - pos = parse_OWS(header, pos) - - return (name, value), pos - - -def parse_extension_item( - header: str, pos: int, header_name: str -) -> tuple[ExtensionHeader, int]: - """ - Parse an extension definition from ``header`` at the given position. - - Return an ``(extension name, parameters)`` pair, where ``parameters`` is a - list of ``(name, value)`` pairs, and the new position. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - # Extract extension name. - name, pos = parse_token(header, pos, header_name) - pos = parse_OWS(header, pos) - # Extract all parameters. - parameters = [] - while peek_ahead(header, pos) == ";": - pos = parse_OWS(header, pos + 1) - parameter, pos = parse_extension_item_param(header, pos, header_name) - parameters.append(parameter) - return (cast(ExtensionName, name), parameters), pos - - -def parse_extension(header: str) -> list[ExtensionHeader]: - """ - Parse a ``Sec-WebSocket-Extensions`` header. - - Return a list of WebSocket extensions and their parameters in this format:: - - [ - ( - 'extension name', - [ - ('parameter name', 'parameter value'), - .... - ] - ), - ... - ] - - Parameter values are :obj:`None` when no value is provided. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - return parse_list(parse_extension_item, header, 0, "Sec-WebSocket-Extensions") - - -parse_extension_list = parse_extension # alias for backwards compatibility - - -def build_extension_item( - name: ExtensionName, parameters: Sequence[ExtensionParameter] -) -> str: - """ - Build an extension definition. - - This is the reverse of :func:`parse_extension_item`. - - """ - return "; ".join( - [cast(str, name)] - + [ - # Quoted strings aren't necessary because values are always tokens. - name if value is None else f"{name}={value}" - for name, value in parameters - ] - ) - - -def build_extension(extensions: Sequence[ExtensionHeader]) -> str: - """ - Build a ``Sec-WebSocket-Extensions`` header. - - This is the reverse of :func:`parse_extension`. - - """ - return ", ".join( - build_extension_item(name, parameters) for name, parameters in extensions - ) - - -build_extension_list = build_extension # alias for backwards compatibility - - -def parse_subprotocol_item( - header: str, pos: int, header_name: str -) -> tuple[Subprotocol, int]: - """ - Parse a subprotocol from ``header`` at the given position. - - Return the subprotocol value and the new position. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - item, pos = parse_token(header, pos, header_name) - return cast(Subprotocol, item), pos - - -def parse_subprotocol(header: str) -> list[Subprotocol]: - """ - Parse a ``Sec-WebSocket-Protocol`` header. - - Return a list of WebSocket subprotocols. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - return parse_list(parse_subprotocol_item, header, 0, "Sec-WebSocket-Protocol") - - -parse_subprotocol_list = parse_subprotocol # alias for backwards compatibility - - -def build_subprotocol(subprotocols: Sequence[Subprotocol]) -> str: - """ - Build a ``Sec-WebSocket-Protocol`` header. - - This is the reverse of :func:`parse_subprotocol`. - - """ - return ", ".join(subprotocols) - - -build_subprotocol_list = build_subprotocol # alias for backwards compatibility - - -def validate_subprotocols(subprotocols: Sequence[Subprotocol]) -> None: - """ - Validate that ``subprotocols`` is suitable for :func:`build_subprotocol`. - - """ - if not isinstance(subprotocols, Sequence): - raise TypeError("subprotocols must be a list") - if isinstance(subprotocols, str): - raise TypeError("subprotocols must be a list, not a str") - for subprotocol in subprotocols: - if not _token_re.fullmatch(subprotocol): - raise ValueError(f"invalid subprotocol: {subprotocol}") - - -def build_www_authenticate_basic(realm: str) -> str: - """ - Build a ``WWW-Authenticate`` header for HTTP Basic Auth. - - Args: - realm: Identifier of the protection space. - - """ - # https://datatracker.ietf.org/doc/html/rfc7617#section-2 - realm = build_quoted_string(realm) - charset = build_quoted_string("UTF-8") - return f"Basic realm={realm}, charset={charset}" - - -_token68_re = re.compile(r"[A-Za-z0-9-._~+/]+=*") - - -def parse_token68(header: str, pos: int, header_name: str) -> tuple[str, int]: - """ - Parse a token68 from ``header`` at the given position. - - Return the token value and the new position. - - Raises: - InvalidHeaderFormat: On invalid inputs. - - """ - match = _token68_re.match(header, pos) - if match is None: - raise InvalidHeaderFormat(header_name, "expected token68", header, pos) - return match.group(), match.end() - - -def parse_end(header: str, pos: int, header_name: str) -> None: - """ - Check that parsing reached the end of header. - - """ - if pos < len(header): - raise InvalidHeaderFormat(header_name, "trailing data", header, pos) - - -def parse_authorization_basic(header: str) -> tuple[str, str]: - """ - Parse an ``Authorization`` header for HTTP Basic Auth. - - Return a ``(username, password)`` tuple. - - Args: - header: Value of the ``Authorization`` header. - - Raises: - InvalidHeaderFormat: On invalid inputs. - InvalidHeaderValue: On unsupported inputs. - - """ - # https://datatracker.ietf.org/doc/html/rfc7235#section-2.1 - # https://datatracker.ietf.org/doc/html/rfc7617#section-2 - scheme, pos = parse_token(header, 0, "Authorization") - if scheme.lower() != "basic": - raise InvalidHeaderValue( - "Authorization", - f"unsupported scheme: {scheme}", - ) - if peek_ahead(header, pos) != " ": - raise InvalidHeaderFormat( - "Authorization", "expected space after scheme", header, pos - ) - pos += 1 - basic_credentials, pos = parse_token68(header, pos, "Authorization") - parse_end(header, pos, "Authorization") - - try: - user_pass = base64.b64decode(basic_credentials.encode()).decode() - except binascii.Error: - raise InvalidHeaderValue( - "Authorization", - "expected base64-encoded credentials", - ) from None - try: - username, password = user_pass.split(":", 1) - except ValueError: - raise InvalidHeaderValue( - "Authorization", - "expected username:password credentials", - ) from None - - return username, password - - -def build_authorization_basic(username: str, password: str) -> str: - """ - Build an ``Authorization`` header for HTTP Basic Auth. - - This is the reverse of :func:`parse_authorization_basic`. - - """ - # https://datatracker.ietf.org/doc/html/rfc7617#section-2 - assert ":" not in username - user_pass = f"{username}:{password}" - basic_credentials = base64.b64encode(user_pass.encode()).decode() - return "Basic " + basic_credentials diff --git a/wip/SNIP/websockets/http.py b/wip/SNIP/websockets/http.py deleted file mode 100644 index 0d860e5..0000000 --- a/wip/SNIP/websockets/http.py +++ /dev/null @@ -1,20 +0,0 @@ -from __future__ import annotations - -import warnings - -from .datastructures import Headers, MultipleValuesError # noqa: F401 - - -with warnings.catch_warnings(): - # Suppress redundant DeprecationWarning raised by websockets.legacy. - warnings.filterwarnings("ignore", category=DeprecationWarning) - from .legacy.http import read_request, read_response # noqa: F401 - - -warnings.warn( # deprecated in 9.0 - 2021-09-01 - "Headers and MultipleValuesError were moved " - "from websockets.http to websockets.datastructures" - "and read_request and read_response were moved " - "from websockets.http to websockets.legacy.http", - DeprecationWarning, -) diff --git a/wip/SNIP/websockets/http11.py b/wip/SNIP/websockets/http11.py deleted file mode 100644 index 530ac3d..0000000 --- a/wip/SNIP/websockets/http11.py +++ /dev/null @@ -1,427 +0,0 @@ -from __future__ import annotations - -import dataclasses -import os -import re -import sys -import warnings -from collections.abc import Generator -from typing import Callable - -from .datastructures import Headers -from .exceptions import SecurityError -from .version import version as websockets_version - - -__all__ = [ - "SERVER", - "USER_AGENT", - "Request", - "Response", -] - - -PYTHON_VERSION = "{}.{}".format(*sys.version_info) - -# User-Agent header for HTTP requests. -USER_AGENT = os.environ.get( - "WEBSOCKETS_USER_AGENT", - f"Python/{PYTHON_VERSION} websockets/{websockets_version}", -) - -# Server header for HTTP responses. -SERVER = os.environ.get( - "WEBSOCKETS_SERVER", - f"Python/{PYTHON_VERSION} websockets/{websockets_version}", -) - -# Maximum total size of headers is around 128 * 8 KiB = 1 MiB. -MAX_NUM_HEADERS = int(os.environ.get("WEBSOCKETS_MAX_NUM_HEADERS", "128")) - -# Limit request line and header lines. 8KiB is the most common default -# configuration of popular HTTP servers. -MAX_LINE_LENGTH = int(os.environ.get("WEBSOCKETS_MAX_LINE_LENGTH", "8192")) - -# Support for HTTP response bodies is intended to read an error message -# returned by a server. It isn't designed to perform large file transfers. -MAX_BODY_SIZE = int(os.environ.get("WEBSOCKETS_MAX_BODY_SIZE", "1_048_576")) # 1 MiB - - -def d(value: bytes) -> str: - """ - Decode a bytestring for interpolating into an error message. - - """ - return value.decode(errors="backslashreplace") - - -# See https://datatracker.ietf.org/doc/html/rfc7230#appendix-B. - -# Regex for validating header names. - -_token_re = re.compile(rb"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+") - -# Regex for validating header values. - -# We don't attempt to support obsolete line folding. - -# Include HTAB (\x09), SP (\x20), VCHAR (\x21-\x7e), obs-text (\x80-\xff). - -# The ABNF is complicated because it attempts to express that optional -# whitespace is ignored. We strip whitespace and don't revalidate that. - -# See also https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189 - -_value_re = re.compile(rb"[\x09\x20-\x7e\x80-\xff]*") - - -@dataclasses.dataclass -class Request: - """ - WebSocket handshake request. - - Attributes: - path: Request path, including optional query. - headers: Request headers. - """ - - path: str - headers: Headers - # body isn't useful is the context of this library. - - _exception: Exception | None = None - - @property - def exception(self) -> Exception | None: # pragma: no cover - warnings.warn( # deprecated in 10.3 - 2022-04-17 - "Request.exception is deprecated; use ServerProtocol.handshake_exc instead", - DeprecationWarning, - ) - return self._exception - - @classmethod - def parse( - cls, - read_line: Callable[[int], Generator[None, None, bytes]], - ) -> Generator[None, None, Request]: - """ - Parse a WebSocket handshake request. - - This is a generator-based coroutine. - - The request path isn't URL-decoded or validated in any way. - - The request path and headers are expected to contain only ASCII - characters. Other characters are represented with surrogate escapes. - - :meth:`parse` doesn't attempt to read the request body because - WebSocket handshake requests don't have one. If the request contains a - body, it may be read from the data stream after :meth:`parse` returns. - - Args: - read_line: Generator-based coroutine that reads a LF-terminated - line or raises an exception if there isn't enough data - - Raises: - EOFError: If the connection is closed without a full HTTP request. - SecurityError: If the request exceeds a security limit. - ValueError: If the request isn't well formatted. - - """ - # https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.1 - - # Parsing is simple because fixed values are expected for method and - # version and because path isn't checked. Since WebSocket software tends - # to implement HTTP/1.1 strictly, there's little need for lenient parsing. - - try: - request_line = yield from parse_line(read_line) - except EOFError as exc: - raise EOFError("connection closed while reading HTTP request line") from exc - - try: - method, raw_path, protocol = request_line.split(b" ", 2) - except ValueError: # not enough values to unpack (expected 3, got 1-2) - raise ValueError(f"invalid HTTP request line: {d(request_line)}") from None - if protocol != b"HTTP/1.1": - raise ValueError( - f"unsupported protocol; expected HTTP/1.1: {d(request_line)}" - ) - if method != b"GET": - raise ValueError(f"unsupported HTTP method; expected GET; got {d(method)}") - path = raw_path.decode("ascii", "surrogateescape") - - headers = yield from parse_headers(read_line) - - # https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3 - - if "Transfer-Encoding" in headers: - raise NotImplementedError("transfer codings aren't supported") - - if "Content-Length" in headers: - raise ValueError("unsupported request body") - - return cls(path, headers) - - def serialize(self) -> bytes: - """ - Serialize a WebSocket handshake request. - - """ - # Since the request line and headers only contain ASCII characters, - # we can keep this simple. - request = f"GET {self.path} HTTP/1.1\r\n".encode() - request += self.headers.serialize() - return request - - -@dataclasses.dataclass -class Response: - """ - WebSocket handshake response. - - Attributes: - status_code: Response code. - reason_phrase: Response reason. - headers: Response headers. - body: Response body. - - """ - - status_code: int - reason_phrase: str - headers: Headers - body: bytes = b"" - - _exception: Exception | None = None - - @property - def exception(self) -> Exception | None: # pragma: no cover - warnings.warn( # deprecated in 10.3 - 2022-04-17 - "Response.exception is deprecated; " - "use ClientProtocol.handshake_exc instead", - DeprecationWarning, - ) - return self._exception - - @classmethod - def parse( - cls, - read_line: Callable[[int], Generator[None, None, bytes]], - read_exact: Callable[[int], Generator[None, None, bytes]], - read_to_eof: Callable[[int], Generator[None, None, bytes]], - include_body: bool = True, - ) -> Generator[None, None, Response]: - """ - Parse a WebSocket handshake response. - - This is a generator-based coroutine. - - The reason phrase and headers are expected to contain only ASCII - characters. Other characters are represented with surrogate escapes. - - Args: - read_line: Generator-based coroutine that reads a LF-terminated - line or raises an exception if there isn't enough data. - read_exact: Generator-based coroutine that reads the requested - bytes or raises an exception if there isn't enough data. - read_to_eof: Generator-based coroutine that reads until the end - of the stream. - - Raises: - EOFError: If the connection is closed without a full HTTP response. - SecurityError: If the response exceeds a security limit. - LookupError: If the response isn't well formatted. - ValueError: If the response isn't well formatted. - - """ - # https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2 - - try: - status_line = yield from parse_line(read_line) - except EOFError as exc: - raise EOFError("connection closed while reading HTTP status line") from exc - - try: - protocol, raw_status_code, raw_reason = status_line.split(b" ", 2) - except ValueError: # not enough values to unpack (expected 3, got 1-2) - raise ValueError(f"invalid HTTP status line: {d(status_line)}") from None - if protocol != b"HTTP/1.1": - raise ValueError( - f"unsupported protocol; expected HTTP/1.1: {d(status_line)}" - ) - try: - status_code = int(raw_status_code) - except ValueError: # invalid literal for int() with base 10 - raise ValueError( - f"invalid status code; expected integer; got {d(raw_status_code)}" - ) from None - if not 100 <= status_code < 600: - raise ValueError( - f"invalid status code; expected 100–599; got {d(raw_status_code)}" - ) - if not _value_re.fullmatch(raw_reason): - raise ValueError(f"invalid HTTP reason phrase: {d(raw_reason)}") - reason = raw_reason.decode("ascii", "surrogateescape") - - headers = yield from parse_headers(read_line) - - if include_body: - body = yield from read_body( - status_code, headers, read_line, read_exact, read_to_eof - ) - else: - body = b"" - - return cls(status_code, reason, headers, body) - - def serialize(self) -> bytes: - """ - Serialize a WebSocket handshake response. - - """ - # Since the status line and headers only contain ASCII characters, - # we can keep this simple. - response = f"HTTP/1.1 {self.status_code} {self.reason_phrase}\r\n".encode() - response += self.headers.serialize() - response += self.body - return response - - -def parse_line( - read_line: Callable[[int], Generator[None, None, bytes]], -) -> Generator[None, None, bytes]: - """ - Parse a single line. - - CRLF is stripped from the return value. - - Args: - read_line: Generator-based coroutine that reads a LF-terminated line - or raises an exception if there isn't enough data. - - Raises: - EOFError: If the connection is closed without a CRLF. - SecurityError: If the response exceeds a security limit. - - """ - try: - line = yield from read_line(MAX_LINE_LENGTH) - except RuntimeError: - raise SecurityError("line too long") - # Not mandatory but safe - https://datatracker.ietf.org/doc/html/rfc7230#section-3.5 - if not line.endswith(b"\r\n"): - raise EOFError("line without CRLF") - return line[:-2] - - -def parse_headers( - read_line: Callable[[int], Generator[None, None, bytes]], -) -> Generator[None, None, Headers]: - """ - Parse HTTP headers. - - Non-ASCII characters are represented with surrogate escapes. - - Args: - read_line: Generator-based coroutine that reads a LF-terminated line - or raises an exception if there isn't enough data. - - Raises: - EOFError: If the connection is closed without complete headers. - SecurityError: If the request exceeds a security limit. - ValueError: If the request isn't well formatted. - - """ - # https://datatracker.ietf.org/doc/html/rfc7230#section-3.2 - - # We don't attempt to support obsolete line folding. - - headers = Headers() - for _ in range(MAX_NUM_HEADERS + 1): - try: - line = yield from parse_line(read_line) - except EOFError as exc: - raise EOFError("connection closed while reading HTTP headers") from exc - if line == b"": - break - - try: - raw_name, raw_value = line.split(b":", 1) - except ValueError: # not enough values to unpack (expected 2, got 1) - raise ValueError(f"invalid HTTP header line: {d(line)}") from None - if not _token_re.fullmatch(raw_name): - raise ValueError(f"invalid HTTP header name: {d(raw_name)}") - raw_value = raw_value.strip(b" \t") - if not _value_re.fullmatch(raw_value): - raise ValueError(f"invalid HTTP header value: {d(raw_value)}") - - name = raw_name.decode("ascii") # guaranteed to be ASCII at this point - value = raw_value.decode("ascii", "surrogateescape") - headers[name] = value - - else: - raise SecurityError("too many HTTP headers") - - return headers - - -def read_body( - status_code: int, - headers: Headers, - read_line: Callable[[int], Generator[None, None, bytes]], - read_exact: Callable[[int], Generator[None, None, bytes]], - read_to_eof: Callable[[int], Generator[None, None, bytes]], -) -> Generator[None, None, bytes]: - # https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3 - - # Since websockets only does GET requests (no HEAD, no CONNECT), all - # responses except 1xx, 204, and 304 include a message body. - if 100 <= status_code < 200 or status_code == 204 or status_code == 304: - return b"" - - # MultipleValuesError is sufficiently unlikely that we don't attempt to - # handle it when accessing headers. Instead we document that its parent - # class, LookupError, may be raised. - # Conversions from str to int are protected by sys.set_int_max_str_digits.. - - elif (coding := headers.get("Transfer-Encoding")) is not None: - if coding != "chunked": - raise NotImplementedError(f"transfer coding {coding} isn't supported") - - body = b"" - while True: - chunk_size_line = yield from parse_line(read_line) - raw_chunk_size = chunk_size_line.split(b";", 1)[0] - # Set a lower limit than default_max_str_digits; 1 EB is plenty. - if len(raw_chunk_size) > 15: - str_chunk_size = raw_chunk_size.decode(errors="backslashreplace") - raise SecurityError(f"chunk too large: 0x{str_chunk_size} bytes") - chunk_size = int(raw_chunk_size, 16) - if chunk_size == 0: - break - if len(body) + chunk_size > MAX_BODY_SIZE: - raise SecurityError( - f"chunk too large: {chunk_size} bytes after {len(body)} bytes" - ) - body += yield from read_exact(chunk_size) - if (yield from read_exact(2)) != b"\r\n": - raise ValueError("chunk without CRLF") - # Read the trailer. - yield from parse_headers(read_line) - return body - - elif (raw_content_length := headers.get("Content-Length")) is not None: - # Set a lower limit than default_max_str_digits; 1 EiB is plenty. - if len(raw_content_length) > 18: - raise SecurityError(f"body too large: {raw_content_length} bytes") - content_length = int(raw_content_length) - if content_length > MAX_BODY_SIZE: - raise SecurityError(f"body too large: {content_length} bytes") - return (yield from read_exact(content_length)) - - else: - try: - return (yield from read_to_eof(MAX_BODY_SIZE)) - except RuntimeError: - raise SecurityError(f"body too large: over {MAX_BODY_SIZE} bytes") diff --git a/wip/SNIP/websockets/imports.py b/wip/SNIP/websockets/imports.py deleted file mode 100644 index c63fb21..0000000 --- a/wip/SNIP/websockets/imports.py +++ /dev/null @@ -1,100 +0,0 @@ -from __future__ import annotations - -import warnings -from collections.abc import Iterable -from typing import Any - - -__all__ = ["lazy_import"] - - -def import_name(name: str, source: str, namespace: dict[str, Any]) -> Any: - """ - Import ``name`` from ``source`` in ``namespace``. - - There are two use cases: - - - ``name`` is an object defined in ``source``; - - ``name`` is a submodule of ``source``. - - Neither :func:`__import__` nor :func:`~importlib.import_module` does - exactly this. :func:`__import__` is closer to the intended behavior. - - """ - level = 0 - while source[level] == ".": - level += 1 - assert level < len(source), "importing from parent isn't supported" - module = __import__(source[level:], namespace, None, [name], level) - return getattr(module, name) - - -def lazy_import( - namespace: dict[str, Any], - aliases: dict[str, str] | None = None, - deprecated_aliases: dict[str, str] | None = None, -) -> None: - """ - Provide lazy, module-level imports. - - Typical use:: - - __getattr__, __dir__ = lazy_import( - globals(), - aliases={ - "": "", - ... - }, - deprecated_aliases={ - ..., - } - ) - - This function defines ``__getattr__`` and ``__dir__`` per :pep:`562`. - - """ - if aliases is None: - aliases = {} - if deprecated_aliases is None: - deprecated_aliases = {} - - namespace_set = set(namespace) - aliases_set = set(aliases) - deprecated_aliases_set = set(deprecated_aliases) - - assert not namespace_set & aliases_set, "namespace conflict" - assert not namespace_set & deprecated_aliases_set, "namespace conflict" - assert not aliases_set & deprecated_aliases_set, "namespace conflict" - - package = namespace["__name__"] - - def __getattr__(name: str) -> Any: - assert aliases is not None # mypy cannot figure this out - try: - source = aliases[name] - except KeyError: - pass - else: - return import_name(name, source, namespace) - - assert deprecated_aliases is not None # mypy cannot figure this out - try: - source = deprecated_aliases[name] - except KeyError: - pass - else: - warnings.warn( - f"{package}.{name} is deprecated", - DeprecationWarning, - stacklevel=2, - ) - return import_name(name, source, namespace) - - raise AttributeError(f"module {package!r} has no attribute {name!r}") - - namespace["__getattr__"] = __getattr__ - - def __dir__() -> Iterable[str]: - return sorted(namespace_set | aliases_set | deprecated_aliases_set) - - namespace["__dir__"] = __dir__ diff --git a/wip/SNIP/websockets/legacy/__init__.py b/wip/SNIP/websockets/legacy/__init__.py deleted file mode 100644 index ad9aa25..0000000 --- a/wip/SNIP/websockets/legacy/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import annotations - -import warnings - - -warnings.warn( # deprecated in 14.0 - 2024-11-09 - "websockets.legacy is deprecated; " - "see https://websockets.readthedocs.io/en/stable/howto/upgrade.html " - "for upgrade instructions", - DeprecationWarning, -) diff --git a/wip/SNIP/websockets/legacy/__pycache__/__init__.cpython-313.pyc b/wip/SNIP/websockets/legacy/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 477435bd43f0ac3d14782ed2cd54c738b54c3125..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 433 zcmey&%ge>Uz`#(r@A!q z7^WaLh$>`Ki6NLdhQ*W#BBsEQ&ZNom62#GDyv3cEmzQ6XSdy8aSL|0+Q=XbsoS&SX zT2ic+lbW8GT&a**tdNpgP?VaSSdyAztx%krs*q7qQc!HAuaBg-C^a#qBqKE?Ke<>h zGhaV7PrtY%F)1fiKO?`qBwxR@AiXFtB~>q@BsWJPEx$+sDxi>=S6ot5nhdsHll2x? ze0*AINoi4Pe0&iz0|Ub?j`GB!yv)4x;#({r)-53ys9Q4g^THugD;Yk6{Bq00#VW?5 zvLLl6Ge6HiIHo+aAST$)Gav@x${2{3V&da7^D;}~E=Kd|+l|WW39u^q7JF0~-S)XFG2b?*}F(R{0NX46Nc`IawK*iZ~e<7yu0s Bd?x?^ diff --git a/wip/SNIP/websockets/legacy/__pycache__/exceptions.cpython-313.pyc b/wip/SNIP/websockets/legacy/__pycache__/exceptions.cpython-313.pyc deleted file mode 100644 index 5c88da4d4e684ac6aaae3d96375b3aa7b1a90b0e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3284 zcmey&%ge>Uz`)?VDK68Koq^#ohy%l{P{!vX1_p+y48aV+jNS}I48csn%-)P%Ohrrz zAU2CPvlmMd3z*I7&FaNg#HPR?!Jxzt%qGPU%pSvH$_z25h#f4-A;l2Pi6qJa7Uhy+ z2B24#E>0DC5ep@=b7SS=clA9WELxALOi9% z#dV7tA{7sky2YN6nwXMWRD6rAD7B=tD9cEj8RZfxMk*I72{D^kXn?PpXVPOQ=VB6 z6YS?15L2F-RGgojomx^HlardBm|Pi?T9KSuP?DLSSFBf1d5a@HJ~J<~BtHI@6h@%v z!Q_M)7#NB{A=JQdLt5<$hr|tWxn7>nAU7t%UCY400QZ*y*k8enp$tJFMQE6h!I(*f zL6ga^O1n6OL(pbN$aP!ai?+TOZ2T_p z`(NPjF9P{clL;KiU|TeKKp~Ttn420Oe~T+VJ~uz5GzY}yiH|QVP0WGH@W;oeWfm2e ziq`3-v4 zjXW+Id4M@5LcV~+K5#Iy1~ASL{>}iRAtr(hL&;^F&}jJ_1&%HBTqeg5%#A;%#c%|( z1+$xQK=M}>V-aUCTMU;8Q!pnuzi|h174f9AYx3OUb43=FXR zR+6uf2vVX@l97s(>+`1*igu>ZJUXN(GIi%97M# z422*$J+5195G%lm`xXmGu!x6&fx#~slsutnfB{sFfbi!UaAJsti8BO&GBunBVJI<# zGMa)iAOizKC_@fhMKrA7W(WeMHi%+4sl*VXkf|;X1D#1Y$$`Ax{DwvHRU;)L)5X^$279Lbs)v_A1t1xJ?`Bkxk0#4x; zV@eg1jzSd|EYavFRIw>QQVR<>z-}?;q~?K>1~@E>L_t9Tidsn73Ms0@Fv1O7sDcWl zVoPwaJ>V0b;4V0qMWEWKND;&Z74wkvpbX-I2#}e@p3tI|hn3@l3WK=RM@~*w z$p->bA6PkAB|oq+2q|0_)V?UF{ed0C=V0I$`2bS!fs28Q?<+S*OpKY83sl}RvqC&o z1Tq_?xP?XXXA5u+LoaS2MQt#v2@9w#mc>%U7R(aEYQh9<%CHA>6mg`pYI5EZ3`$MO zEJ{sADo?FJjh6^mVT#dsNlz^)Rw#n%N=*S5fh3fdx0p+dGNDBVJgXzu_Ml35Dg$yW z2AYjmG8UPDGAI)ygM!$_AQKdzi3^-5r7-*h&zH`i#K;a#U^j%tdO5(!0wvFZOatYl z&!9M-%781+AzBc1Em72NnR40tH7z=4df_LLIO1C%vZ!>T|Wsv>K#LCF@m4|^*;R_o# rV*uj?35_odAjTINSw^l4qN-mQWEr`>I56@sYF?1g_{zY;2yrR^ivQgT diff --git a/wip/SNIP/websockets/legacy/__pycache__/framing.cpython-313.pyc b/wip/SNIP/websockets/legacy/__pycache__/framing.cpython-313.pyc deleted file mode 100644 index f6d41fff3322c033a25d906e4e253c30e5eec57d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8060 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 diff --git a/wip/SNIP/websockets/legacy/__pycache__/handshake.cpython-313.pyc b/wip/SNIP/websockets/legacy/__pycache__/handshake.cpython-313.pyc deleted file mode 100644 index 71171b0e13c2490310be53fdec633c5a9c275a69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6233 zcmey&%ge>Uz`#(r@?KkpqQIcf5X>3KSR@+E7tAHaki}9Y7R(pS z4Q7c4^9S>QSrWng!MtFWWIB_kz)O&mH5qSlC+6klmn4>C=I0gry##qglj#<_M`~h9 zYEkhmVc*i6lFWjf)Ud>y($r$tqN4mFP3Bv?o_S@7IhiRCWw(S-xL~EX_@H8;1?feJ zDXE%Fw*(3ji;7d@lk@ZPQjTk!zKL-z)JX``U8scOY)QRb2OQ5aU~`v zrxujNXQx));z>`I@7F!AzkH!OS35 zgg`J0hz@2AWe8>iu|gSD7&O_dE^|47oTiYdkdc^|Qk;>PovHwJrb0=+LUC$d3WzSr zNL46KEhMv>i5?f1V^Mms6_;1=)JYjD0HsnX!y z;dzlo?1qeLgL{Y1MHa~$%4!#Q_}xK=o=Q89xT4$g_%u+ZFrjEW4=axmRN3NRLehMS1h-#ARf=L0^HeZYs`2buzjdQ1!q zT+W~>18Xr|l$xBHS(cijkXDqRs{ksglXEg5rEf?^X0ZaOm;_Z?3Mu)i#d+!_3T3H9 znQ4`fVilwuR^De8DFP$sNpK<-x#E#xRvt zfx%Z2QtqcSnlq;{f@(x)8_91a<1N;l{PNVIl}xvo^bBsXmVp~)pqfEJp$L=(Z*kgy z8buWaMRvC!2?N&t(X+|PPfpA!w$p>Fuw`Ii_$+M(C@36e0?RuHR?UJ_Z0@WVb38mLKWiM*G zUgU5)!StD#NsX<+{ROlWeuy#eAX8@tUs`@vidUK5R$wgrF&6GuY>&q4>PL=;~gQ1>q1JGgp?MT zFSlE0cUjqZN6JMZ=MMHSYz!RS9p=q76N)agOWxsAxS(jVA^kF+-31OikXeC@H^gKw z$eCUgGwa~~z{9{HJR#*WyA)WV#SZPud~O#w+#YZXHTeDf6v)WH>C5=@6CVSoJL8Wp zVhlVYpBWfgt?uwiUy#wi%xAD6`2vUK?>pRbAYordi1-CVr^|fKCxkC>`27CB%*g7? z`1=bN1CL0*U8fyLod+Y>GKi}VIA7p!2P^Yn{QZRwYJ(lvB;(6`COecbaM;3Z2!z;R ze8JT1GN1d2;0qi9V6B0SzlyUN7#L=Xy4Z3YVi0v@;y5hN>dMY=SXJ58p5cfJtD6SH zQFbm@8|I^eJg&0LM`d|IY%N|_bLOMwtRS|%h?^?&F=1Xe1?FQ4tRS`qm%A+UF(V!~ zJLY3{yza8h$GLgkC76#(u)51KCxfyatmFU{GvMxt3AlTLtt*1mfP*(FLm435lXO;1 zwyM2YyCmRJJ{8m#LGF{lYdUzJ1gZ|&REKpJNa~cJ8UZPcvr{Xr6d=8m)D(rJO7z}G zA*f>qD*PdZ52!!_7s9_pP&*Zl;7+2SCgUwONPn?Nnt_1<+L?glTu}3(2t=fUWI%nj z28It}3{s$;0BWD$j+)Mj;KkXX?ts!I7Ud#&P^pg`9iU?Qhml)@V>|19&#E(Mqmg+a!Eh*FTW4W!wnK&57v@p_Au78|@SYujJubGX3aK(}UB0OJi& znG3SU7e!4vxIj%Mp($ZAoF_+LW>)~WV9a+&UFLJSz~S&k15hUS(oAuzt2<|qpl~fjF=A{?$fGh@8LAO{-OEPnci}*kS z;3Nndbh^bJ59)u!$Ad=*!L4|3gP;hMuvRjF>v?cn;TDHYZhlH>PO4o|2m=EHs0UPR z$H2hwftit!@h*eXV+P*a4E)a-EblXTU1adO&tQ6=!R$VR*(Wvz?m)%{_AhKgjQn4; WY#D{WI56=t8husJWOQW&n-2hll(km? diff --git a/wip/SNIP/websockets/legacy/__pycache__/http.cpython-313.pyc b/wip/SNIP/websockets/legacy/__pycache__/http.cpython-313.pyc deleted file mode 100644 index c5bff4a69f94646c7e390c2906f223dc69bb6f33..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7569 zcmey&%ge>Uz`#(r@cOG*|%45(yRy76h|IgT;e|z$~$JCQXT#AgeSPZ*eE) z<>i+omSpDV75lvec~q0}7Q07kVoGXJu_og!-r&^a(xS|gO4p*I{GwYtMX8A?@kObH zrK!aww|Jqv;)49V;?!GG;jT`>{?6X6A;Iy!juG*Gp}z4Ru8uCQLBY3}4UH@?6!>`h zxyJjr`niXA++wjXv@}YFhA_x+3@i)`49pA+jGsY)GnFBfAqZI(tb~Cfm@$~il2L_0 zlex;C%Oy2AKP6QmQ6Z_aB(=DtC^IizAuYd1Av3QewWuIJC$R)1l9^YMuaKChkP7mV zLT+ktabkL^9v9awfuzLb?Bbln;*6ryf}F(UR87WPY+zNOkYGyzo36=ti?u8Yruuz}$N7Xy!AzfGsj1$n~`n+x1V7g&rwgFIL)!@$50t*fZ4 zqN*07uBELP7oSj5XP|4Ds9WWz8x^Zv3`(-mF%<@uF%?Dzx-k{zsWBB6AVykRtQJae zvNJF+G=qY36C(qIGJ^ucR0cVQXpkpBHZg=Uz!H)tLoj0oGXp~z<5XrpRvv~tMmdI1 z#voR(G86)o)nEy`l$n8{z$A|~lsTBqs+@r#QJjGx8e}I}M=*OdCz!#&z!1s|vx$$v zm?uSSs} z8k8Hrn%Ef<1sNFfn4+1%Tm}Y)V7R+OnNpd88S|JNnbH{37;Ks48PXUP8KR*D7Xt%3 zL!u-Dg936`up;@05u1;K8B>{HW^pk1`I3N8?q*_O;0j7jOaY}{kC2c6eM3D% z1$WmF1!#s;NX$!7C`v6UEy`0!NYE%qEXmMO$bjbMgakbDVWTqsRq^2lj<|!oRRVtJxRw8Qx8J~~^QBi(+Vo9n(YH@O6L29uc7nfC9X_9K)POwXSd?CD#igL2pa9WfrQn%YP>S6TAfxrTxPlThi&LSR zT>ag^<*^manaTNid8x^uQd|LKV@`f?YKj6VIP*(O6cQEEN^^1)Kq&?mFnVCSQOb6> zS+K;ET9KTZno_Kgs89^80~B&Hb2CdIdc#0vAgUu^8X;+`JT)f=RQl$E;wMFqi_5Rd z5XqUzP&Z~|=70hwF$GjKBRf$cCo?bgGpKx0C<0XlRnnPx-~@`|5wI#NgS!z*nW`3TP!Y$Us;^fTC zTm0~p2uX;V?6){U2`nDuuv;9kIJm`HT#%Dla*GQQBH;49NPvNX0a{@~vy3LkEjCDe zgT%qf7-AT6YDMxbHi%1aaTFz%$AhxzEq17zZm~ePP-BWgJ^`g25H1p8U|=W$6|`VN z7gVc!Vqjo+$2R6P@dAhA4R*c>+Lzg7 z!P2H%5_hm~&e{-jLDK0mpYsI{=NsI@{q~*qGt4ijm|o^KYjAlYqOd^hvWUiYK8*(V z2i*J}wij3=J}NUv=>NFGE&Q2*5#&5Ru?yl}m-)Oe1cqGT2>Jbi86^7q3l{^A7)V73 zBiJ^xErmN|HrHP;bH5!PAr@zGM(p)y2h-rJN97oz;k98X3#@+0V+-aCWh-W~WwNqjU`S+R zUGD3DS4Lv`Y;{SAn!{UIq6T&)*@ zdPk7DUkat}M^a;@P$Ukj03{e07^<+U0;O>)g_kJ|3=Eksm>C$VQ-!sr%rW?soH&UkR|Fuo}D7AL6Lm77?S zoB?hN6v;C%FlcfXfx5G|xFL3etN$V;kP)h&<^n&c?uSZ&oO6o{Bmi*(Tn|`+4I)^i z4yy1$eJfC70E{6m0#F;F2t>qz6eKc2TLea+>i>?6>VmY(GP>8rbUU~~ErKA%8~Rq) z^&KwiI~))>PX;cUzPuXiJ3Kw@s60}4E@VuS{>XU*cf>DCum>g zR$0Jum0JZ|bzAOH+~I%O%IAWm?*&P}%Y6P9IQ;K$i(KcHyT~m!qv|@h)Wb>%4LodF3wiDm1vEwI{?DuwUR-zrdpYQJ+Cv57wjr zH5kM$hF*=K| zOlENwW0)c8Y{IZs)|H9zAUC&*6vII=M^jfO#>3oJNRAjh6$p353e{G z5uMm*P_r6bg$6T%+KsTLf-r-}c~Ey(4yn_MQpqx*3=c4eG6%C*MKCZVGJ`t0p^PdF zatxu&Si9t`!Hn<$111KBg0N^%D;eyDAW$9xvk(N1h6KDTAIu&NZ+;-uqBSJgfRa=}c_V-ID+t^zS~n8y*( z7|IUH?!Ffp7`Q=03ZRk*?CxM5m`SOO91MP^6&a#IsR1kp9lT&cG%JGP12Ca1sZ7C4 zc`S}BD1#U9#vOd{0^wUWB;Uf#LH2Dh6JqcJ(ln_`hKyQ(I$yBi2nA5phSXsO^$+v& zbkPS5h#w+&3F;~ORp}5_$v`_&MIsCg4B#%N6c4{P97tCaBF+BIJF z4#p2m%$y;NcO;b-m|d3Cx-P8M;R3251SS}FTi@Uj{Gi6bBi!KkL7AD;pYev0+H$Rh zS}Q^}7+zK~y~rcq;qiryK|u04ztTm1r3Dt(`E@Sx>)a8NxgZa!Q9O2Q?~uJ9>wa0t z;{uPz4H4Pt{uBKdxLp>}?%=$`F9FqMd7WSPBEK$Jm(?D_9g(|jcIaJ@^}HEeP{iQ^2_|{9V|BlB(Do7Toh1PAbvs1`m%sc2kQ+9*$FJ&ydO0g_%y!gG4RV= zkTJf@XL5nV={NkS(7(reXlDZ%bHZ>sl0uQ*#;RIDVd<+6oAQk?MU?+ea z?y_5bhvWrW=gUGaCsZ!*_I-KK@lfSS7ye;wv5j9Oo#2cT%=f!aPqi_vK|p-1+k@=U6~n=$SS)supVUq zu~B-ipfM6~1#yf4)^o)^s1BrZR>y27%HO zxKKh6h{6b7yn;-C_h5tJeISqt!HmJIEDYfOoCmmYVXN|o6fTJh#hH2OIjP`0i=}jN z4)SpWjg5hGNI_}}Lb2u+ol^|FYHl_wL z?&ZJ#|Nm>U-Qoa^3V~#AG3TV_L9#E>)CM>c-r`8jODP8FE&??jHJQN8z#=P9CIS~B z3XooTAA1rF^S-24-mCkS;|USJXV$j`u|{IeKT zLNtjuDl$%Ebd+bA?#St=$f(Hz&iS`k!BZ%=*owfT$+wt`ONumEz;6B}l2}}smzhTvAk;3{qAM zHtrS|sBcpM8Y?Y^L=Ags0?LK%`3|+%FnySoSs@zWCn7z6NmscPj0a% z=jRq==A;&xgM`54EO_V*RNI0}MR44$WB}JJ;26EdVUwGmQks)$R}{>^zyPYHil;I# zFnnNUWMsU{p!Ar5?=}PHZ3fP#3@UdS)Ig=9)C~IzT-pun4~0Z0Bu&qnm^D3bV%`N+ z#{;I9#Xv<&1OH8Q8MDh`UYCWu8~DGlu`(KekYM2D@AvBRx*%?Tna84m^Me#zz~nNI hX#?jMHa$kkFG-Asj2d4u7*!dCzXmY!Gm3(p4FD%=Ewlgt diff --git a/wip/SNIP/websockets/legacy/__pycache__/protocol.cpython-313.pyc b/wip/SNIP/websockets/legacy/__pycache__/protocol.cpython-313.pyc deleted file mode 100644 index 6b982e113bb3586e3146014e616c695a701401d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 62743 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 Yz{d5J6U=mAW{@yy;JnHp0v@RW0I#t*lmGw# diff --git a/wip/SNIP/websockets/legacy/__pycache__/server.cpython-313.pyc b/wip/SNIP/websockets/legacy/__pycache__/server.cpython-313.pyc deleted file mode 100644 index 7a826f58ae8d19c53a62f1e37aee57215db03efb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45302 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 I4czDe0NEXCS^xk5 diff --git a/wip/SNIP/websockets/legacy/auth.py b/wip/SNIP/websockets/legacy/auth.py deleted file mode 100644 index a262fcd..0000000 --- a/wip/SNIP/websockets/legacy/auth.py +++ /dev/null @@ -1,190 +0,0 @@ -from __future__ import annotations - -import functools -import hmac -import http -from collections.abc import Awaitable, Iterable -from typing import Any, Callable, cast - -from ..datastructures import Headers -from ..exceptions import InvalidHeader -from ..headers import build_www_authenticate_basic, parse_authorization_basic -from .server import HTTPResponse, WebSocketServerProtocol - - -__all__ = ["BasicAuthWebSocketServerProtocol", "basic_auth_protocol_factory"] - -Credentials = tuple[str, str] - - -def is_credentials(value: Any) -> bool: - try: - username, password = value - except (TypeError, ValueError): - return False - else: - return isinstance(username, str) and isinstance(password, str) - - -class BasicAuthWebSocketServerProtocol(WebSocketServerProtocol): - """ - WebSocket server protocol that enforces HTTP Basic Auth. - - """ - - realm: str = "" - """ - Scope of protection. - - If provided, it should contain only ASCII characters because the - encoding of non-ASCII characters is undefined. - """ - - username: str | None = None - """Username of the authenticated user.""" - - def __init__( - self, - *args: Any, - realm: str | None = None, - check_credentials: Callable[[str, str], Awaitable[bool]] | None = None, - **kwargs: Any, - ) -> None: - if realm is not None: - self.realm = realm # shadow class attribute - self._check_credentials = check_credentials - super().__init__(*args, **kwargs) - - async def check_credentials(self, username: str, password: str) -> bool: - """ - Check whether credentials are authorized. - - This coroutine may be overridden in a subclass, for example to - authenticate against a database or an external service. - - Args: - username: HTTP Basic Auth username. - password: HTTP Basic Auth password. - - Returns: - :obj:`True` if the handshake should continue; - :obj:`False` if it should fail with an HTTP 401 error. - - """ - if self._check_credentials is not None: - return await self._check_credentials(username, password) - - return False - - async def process_request( - self, - path: str, - request_headers: Headers, - ) -> HTTPResponse | None: - """ - Check HTTP Basic Auth and return an HTTP 401 response if needed. - - """ - try: - authorization = request_headers["Authorization"] - except KeyError: - return ( - http.HTTPStatus.UNAUTHORIZED, - [("WWW-Authenticate", build_www_authenticate_basic(self.realm))], - b"Missing credentials\n", - ) - - try: - username, password = parse_authorization_basic(authorization) - except InvalidHeader: - return ( - http.HTTPStatus.UNAUTHORIZED, - [("WWW-Authenticate", build_www_authenticate_basic(self.realm))], - b"Unsupported credentials\n", - ) - - if not await self.check_credentials(username, password): - return ( - http.HTTPStatus.UNAUTHORIZED, - [("WWW-Authenticate", build_www_authenticate_basic(self.realm))], - b"Invalid credentials\n", - ) - - self.username = username - - return await super().process_request(path, request_headers) - - -def basic_auth_protocol_factory( - realm: str | None = None, - credentials: Credentials | Iterable[Credentials] | None = None, - check_credentials: Callable[[str, str], Awaitable[bool]] | None = None, - create_protocol: Callable[..., BasicAuthWebSocketServerProtocol] | None = None, -) -> Callable[..., BasicAuthWebSocketServerProtocol]: - """ - Protocol factory that enforces HTTP Basic Auth. - - :func:`basic_auth_protocol_factory` is designed to integrate with - :func:`~websockets.legacy.server.serve` like this:: - - serve( - ..., - create_protocol=basic_auth_protocol_factory( - realm="my dev server", - credentials=("hello", "iloveyou"), - ) - ) - - Args: - realm: Scope of protection. It should contain only ASCII characters - because the encoding of non-ASCII characters is undefined. - Refer to section 2.2 of :rfc:`7235` for details. - credentials: Hard coded authorized credentials. It can be a - ``(username, password)`` pair or a list of such pairs. - check_credentials: Coroutine that verifies credentials. - It receives ``username`` and ``password`` arguments - and returns a :class:`bool`. One of ``credentials`` or - ``check_credentials`` must be provided but not both. - create_protocol: Factory that creates the protocol. By default, this - is :class:`BasicAuthWebSocketServerProtocol`. It can be replaced - by a subclass. - Raises: - TypeError: If the ``credentials`` or ``check_credentials`` argument is - wrong. - - """ - if (credentials is None) == (check_credentials is None): - raise TypeError("provide either credentials or check_credentials") - - if credentials is not None: - if is_credentials(credentials): - credentials_list = [cast(Credentials, credentials)] - elif isinstance(credentials, Iterable): - credentials_list = list(cast(Iterable[Credentials], credentials)) - if not all(is_credentials(item) for item in credentials_list): - raise TypeError(f"invalid credentials argument: {credentials}") - else: - raise TypeError(f"invalid credentials argument: {credentials}") - - credentials_dict = dict(credentials_list) - - async def check_credentials(username: str, password: str) -> bool: - try: - expected_password = credentials_dict[username] - except KeyError: - return False - return hmac.compare_digest(expected_password, password) - - if create_protocol is None: - create_protocol = BasicAuthWebSocketServerProtocol - - # Help mypy and avoid this error: "type[BasicAuthWebSocketServerProtocol] | - # Callable[..., BasicAuthWebSocketServerProtocol]" not callable [misc] - create_protocol = cast( - Callable[..., BasicAuthWebSocketServerProtocol], create_protocol - ) - return functools.partial( - create_protocol, - realm=realm, - check_credentials=check_credentials, - ) diff --git a/wip/SNIP/websockets/legacy/client.py b/wip/SNIP/websockets/legacy/client.py deleted file mode 100644 index 29141f3..0000000 --- a/wip/SNIP/websockets/legacy/client.py +++ /dev/null @@ -1,705 +0,0 @@ -from __future__ import annotations - -import asyncio -import functools -import logging -import os -import random -import traceback -import urllib.parse -import warnings -from collections.abc import AsyncIterator, Generator, Sequence -from types import TracebackType -from typing import Any, Callable, cast - -from ..asyncio.compatibility import asyncio_timeout -from ..datastructures import Headers, HeadersLike -from ..exceptions import ( - InvalidHeader, - InvalidHeaderValue, - InvalidMessage, - NegotiationError, - SecurityError, -) -from ..extensions import ClientExtensionFactory, Extension -from ..extensions.permessage_deflate import enable_client_permessage_deflate -from ..headers import ( - build_authorization_basic, - build_extension, - build_host, - build_subprotocol, - parse_extension, - parse_subprotocol, - validate_subprotocols, -) -from ..http11 import USER_AGENT -from ..typing import ExtensionHeader, LoggerLike, Origin, Subprotocol -from ..uri import WebSocketURI, parse_uri -from .exceptions import InvalidStatusCode, RedirectHandshake -from .handshake import build_request, check_response -from .http import read_response -from .protocol import WebSocketCommonProtocol - - -__all__ = ["connect", "unix_connect", "WebSocketClientProtocol"] - - -class WebSocketClientProtocol(WebSocketCommonProtocol): - """ - WebSocket client connection. - - :class:`WebSocketClientProtocol` provides :meth:`recv` and :meth:`send` - coroutines for receiving and sending messages. - - It supports asynchronous iteration to receive messages:: - - async for message in websocket: - await process(message) - - The iterator exits normally when the connection is closed with close code - 1000 (OK) or 1001 (going away) or without a close code. It raises - a :exc:`~websockets.exceptions.ConnectionClosedError` when the connection - is closed with any other code. - - See :func:`connect` for the documentation of ``logger``, ``origin``, - ``extensions``, ``subprotocols``, ``extra_headers``, and - ``user_agent_header``. - - See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the - documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, - ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit``. - - """ - - is_client = True - side = "client" - - def __init__( - self, - *, - logger: LoggerLike | None = None, - origin: Origin | None = None, - extensions: Sequence[ClientExtensionFactory] | None = None, - subprotocols: Sequence[Subprotocol] | None = None, - extra_headers: HeadersLike | None = None, - user_agent_header: str | None = USER_AGENT, - **kwargs: Any, - ) -> None: - if logger is None: - logger = logging.getLogger("websockets.client") - super().__init__(logger=logger, **kwargs) - self.origin = origin - self.available_extensions = extensions - self.available_subprotocols = subprotocols - self.extra_headers = extra_headers - self.user_agent_header = user_agent_header - - def write_http_request(self, path: str, headers: Headers) -> None: - """ - Write request line and headers to the HTTP request. - - """ - self.path = path - self.request_headers = headers - - if self.debug: - self.logger.debug("> GET %s HTTP/1.1", path) - for key, value in headers.raw_items(): - self.logger.debug("> %s: %s", key, value) - - # Since the path and headers only contain ASCII characters, - # we can keep this simple. - request = f"GET {path} HTTP/1.1\r\n" - request += str(headers) - - self.transport.write(request.encode()) - - async def read_http_response(self) -> tuple[int, Headers]: - """ - Read status line and headers from the HTTP response. - - If the response contains a body, it may be read from ``self.reader`` - after this coroutine returns. - - Raises: - InvalidMessage: If the HTTP message is malformed or isn't an - HTTP/1.1 GET response. - - """ - try: - status_code, reason, headers = await read_response(self.reader) - except Exception as exc: - raise InvalidMessage("did not receive a valid HTTP response") from exc - - if self.debug: - self.logger.debug("< HTTP/1.1 %d %s", status_code, reason) - for key, value in headers.raw_items(): - self.logger.debug("< %s: %s", key, value) - - self.response_headers = headers - - return status_code, self.response_headers - - @staticmethod - def process_extensions( - headers: Headers, - available_extensions: Sequence[ClientExtensionFactory] | None, - ) -> list[Extension]: - """ - Handle the Sec-WebSocket-Extensions HTTP response header. - - Check that each extension is supported, as well as its parameters. - - Return the list of accepted extensions. - - Raise :exc:`~websockets.exceptions.InvalidHandshake` to abort the - connection. - - :rfc:`6455` leaves the rules up to the specification of each - :extension. - - To provide this level of flexibility, for each extension accepted by - the server, we check for a match with each extension available in the - client configuration. If no match is found, an exception is raised. - - If several variants of the same extension are accepted by the server, - it may be configured several times, which won't make sense in general. - Extensions must implement their own requirements. For this purpose, - the list of previously accepted extensions is provided. - - Other requirements, for example related to mandatory extensions or the - order of extensions, may be implemented by overriding this method. - - """ - accepted_extensions: list[Extension] = [] - - header_values = headers.get_all("Sec-WebSocket-Extensions") - - if header_values: - if available_extensions is None: - raise NegotiationError("no extensions supported") - - parsed_header_values: list[ExtensionHeader] = sum( - [parse_extension(header_value) for header_value in header_values], [] - ) - - for name, response_params in parsed_header_values: - for extension_factory in available_extensions: - # Skip non-matching extensions based on their name. - if extension_factory.name != name: - continue - - # Skip non-matching extensions based on their params. - try: - extension = extension_factory.process_response_params( - response_params, accepted_extensions - ) - except NegotiationError: - continue - - # Add matching extension to the final list. - accepted_extensions.append(extension) - - # Break out of the loop once we have a match. - break - - # If we didn't break from the loop, no extension in our list - # matched what the server sent. Fail the connection. - else: - raise NegotiationError( - f"Unsupported extension: " - f"name = {name}, params = {response_params}" - ) - - return accepted_extensions - - @staticmethod - def process_subprotocol( - headers: Headers, available_subprotocols: Sequence[Subprotocol] | None - ) -> Subprotocol | None: - """ - Handle the Sec-WebSocket-Protocol HTTP response header. - - Check that it contains exactly one supported subprotocol. - - Return the selected subprotocol. - - """ - subprotocol: Subprotocol | None = None - - header_values = headers.get_all("Sec-WebSocket-Protocol") - - if header_values: - if available_subprotocols is None: - raise NegotiationError("no subprotocols supported") - - parsed_header_values: Sequence[Subprotocol] = sum( - [parse_subprotocol(header_value) for header_value in header_values], [] - ) - - if len(parsed_header_values) > 1: - raise InvalidHeaderValue( - "Sec-WebSocket-Protocol", - f"multiple values: {', '.join(parsed_header_values)}", - ) - - subprotocol = parsed_header_values[0] - - if subprotocol not in available_subprotocols: - raise NegotiationError(f"unsupported subprotocol: {subprotocol}") - - return subprotocol - - async def handshake( - self, - wsuri: WebSocketURI, - origin: Origin | None = None, - available_extensions: Sequence[ClientExtensionFactory] | None = None, - available_subprotocols: Sequence[Subprotocol] | None = None, - extra_headers: HeadersLike | None = None, - ) -> None: - """ - Perform the client side of the opening handshake. - - Args: - wsuri: URI of the WebSocket server. - origin: Value of the ``Origin`` header. - extensions: List of supported extensions, in order in which they - should be negotiated and run. - subprotocols: List of supported subprotocols, in order of decreasing - preference. - extra_headers: Arbitrary HTTP headers to add to the handshake request. - - Raises: - InvalidHandshake: If the handshake fails. - - """ - request_headers = Headers() - - request_headers["Host"] = build_host(wsuri.host, wsuri.port, wsuri.secure) - - if wsuri.user_info: - request_headers["Authorization"] = build_authorization_basic( - *wsuri.user_info - ) - - if origin is not None: - request_headers["Origin"] = origin - - key = build_request(request_headers) - - if available_extensions is not None: - extensions_header = build_extension( - [ - (extension_factory.name, extension_factory.get_request_params()) - for extension_factory in available_extensions - ] - ) - request_headers["Sec-WebSocket-Extensions"] = extensions_header - - if available_subprotocols is not None: - protocol_header = build_subprotocol(available_subprotocols) - request_headers["Sec-WebSocket-Protocol"] = protocol_header - - if self.extra_headers is not None: - request_headers.update(self.extra_headers) - - if self.user_agent_header: - request_headers.setdefault("User-Agent", self.user_agent_header) - - self.write_http_request(wsuri.resource_name, request_headers) - - status_code, response_headers = await self.read_http_response() - if status_code in (301, 302, 303, 307, 308): - if "Location" not in response_headers: - raise InvalidHeader("Location") - raise RedirectHandshake(response_headers["Location"]) - elif status_code != 101: - raise InvalidStatusCode(status_code, response_headers) - - check_response(response_headers, key) - - self.extensions = self.process_extensions( - response_headers, available_extensions - ) - - self.subprotocol = self.process_subprotocol( - response_headers, available_subprotocols - ) - - self.connection_open() - - -class Connect: - """ - Connect to the WebSocket server at ``uri``. - - Awaiting :func:`connect` yields a :class:`WebSocketClientProtocol` which - can then be used to send and receive messages. - - :func:`connect` can be used as a asynchronous context manager:: - - async with connect(...) as websocket: - ... - - The connection is closed automatically when exiting the context. - - :func:`connect` can be used as an infinite asynchronous iterator to - reconnect automatically on errors:: - - async for websocket in connect(...): - try: - ... - except websockets.exceptions.ConnectionClosed: - continue - - The connection is closed automatically after each iteration of the loop. - - If an error occurs while establishing the connection, :func:`connect` - retries with exponential backoff. The backoff delay starts at three - seconds and increases up to one minute. - - If an error occurs in the body of the loop, you can handle the exception - and :func:`connect` will reconnect with the next iteration; or you can - let the exception bubble up and break out of the loop. This lets you - decide which errors trigger a reconnection and which errors are fatal. - - Args: - uri: URI of the WebSocket server. - create_protocol: Factory for the :class:`asyncio.Protocol` managing - the connection. It defaults to :class:`WebSocketClientProtocol`. - Set it to a wrapper or a subclass to customize connection handling. - logger: Logger for this client. - It defaults to ``logging.getLogger("websockets.client")``. - See the :doc:`logging guide <../../topics/logging>` for details. - compression: The "permessage-deflate" extension is enabled by default. - Set ``compression`` to :obj:`None` to disable it. See the - :doc:`compression guide <../../topics/compression>` for details. - origin: Value of the ``Origin`` header, for servers that require it. - extensions: List of supported extensions, in order in which they - should be negotiated and run. - subprotocols: List of supported subprotocols, in order of decreasing - preference. - extra_headers: Arbitrary HTTP headers to add to the handshake request. - user_agent_header: Value of the ``User-Agent`` request header. - It defaults to ``"Python/x.y.z websockets/X.Y"``. - Setting it to :obj:`None` removes the header. - open_timeout: Timeout for opening the connection in seconds. - :obj:`None` disables the timeout. - - See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the - documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, - ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit``. - - Any other keyword arguments are passed the event loop's - :meth:`~asyncio.loop.create_connection` method. - - For example: - - * You can set ``ssl`` to a :class:`~ssl.SSLContext` to enforce TLS - settings. When connecting to a ``wss://`` URI, if ``ssl`` isn't - provided, a TLS context is created - with :func:`~ssl.create_default_context`. - - * You can set ``host`` and ``port`` to connect to a different host and - port from those found in ``uri``. This only changes the destination of - the TCP connection. The host name from ``uri`` is still used in the TLS - handshake for secure connections and in the ``Host`` header. - - Raises: - InvalidURI: If ``uri`` isn't a valid WebSocket URI. - OSError: If the TCP connection fails. - InvalidHandshake: If the opening handshake fails. - ~asyncio.TimeoutError: If the opening handshake times out. - - """ - - MAX_REDIRECTS_ALLOWED = int(os.environ.get("WEBSOCKETS_MAX_REDIRECTS", "10")) - - def __init__( - self, - uri: str, - *, - create_protocol: Callable[..., WebSocketClientProtocol] | None = None, - logger: LoggerLike | None = None, - compression: str | None = "deflate", - origin: Origin | None = None, - extensions: Sequence[ClientExtensionFactory] | None = None, - subprotocols: Sequence[Subprotocol] | None = None, - extra_headers: HeadersLike | None = None, - user_agent_header: str | None = USER_AGENT, - open_timeout: float | None = 10, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = None, - max_size: int | None = 2**20, - max_queue: int | None = 2**5, - read_limit: int = 2**16, - write_limit: int = 2**16, - **kwargs: Any, - ) -> None: - # Backwards compatibility: close_timeout used to be called timeout. - timeout: float | None = kwargs.pop("timeout", None) - if timeout is None: - timeout = 10 - else: - warnings.warn("rename timeout to close_timeout", DeprecationWarning) - # If both are specified, timeout is ignored. - if close_timeout is None: - close_timeout = timeout - - # Backwards compatibility: create_protocol used to be called klass. - klass: type[WebSocketClientProtocol] | None = kwargs.pop("klass", None) - if klass is None: - klass = WebSocketClientProtocol - else: - warnings.warn("rename klass to create_protocol", DeprecationWarning) - # If both are specified, klass is ignored. - if create_protocol is None: - create_protocol = klass - - # Backwards compatibility: recv() used to return None on closed connections - legacy_recv: bool = kwargs.pop("legacy_recv", False) - - # Backwards compatibility: the loop parameter used to be supported. - _loop: asyncio.AbstractEventLoop | None = kwargs.pop("loop", None) - if _loop is None: - loop = asyncio.get_event_loop() - else: - loop = _loop - warnings.warn("remove loop argument", DeprecationWarning) - - wsuri = parse_uri(uri) - if wsuri.secure: - kwargs.setdefault("ssl", True) - elif kwargs.get("ssl") is not None: - raise ValueError( - "connect() received a ssl argument for a ws:// URI, " - "use a wss:// URI to enable TLS" - ) - - if compression == "deflate": - extensions = enable_client_permessage_deflate(extensions) - elif compression is not None: - raise ValueError(f"unsupported compression: {compression}") - - if subprotocols is not None: - validate_subprotocols(subprotocols) - - # Help mypy and avoid this error: "type[WebSocketClientProtocol] | - # Callable[..., WebSocketClientProtocol]" not callable [misc] - create_protocol = cast(Callable[..., WebSocketClientProtocol], create_protocol) - factory = functools.partial( - create_protocol, - logger=logger, - origin=origin, - extensions=extensions, - subprotocols=subprotocols, - extra_headers=extra_headers, - user_agent_header=user_agent_header, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - close_timeout=close_timeout, - max_size=max_size, - max_queue=max_queue, - read_limit=read_limit, - write_limit=write_limit, - host=wsuri.host, - port=wsuri.port, - secure=wsuri.secure, - legacy_recv=legacy_recv, - loop=_loop, - ) - - if kwargs.pop("unix", False): - path: str | None = kwargs.pop("path", None) - create_connection = functools.partial( - loop.create_unix_connection, factory, path, **kwargs - ) - else: - host: str | None - port: int | None - if kwargs.get("sock") is None: - host, port = wsuri.host, wsuri.port - else: - # If sock is given, host and port shouldn't be specified. - host, port = None, None - if kwargs.get("ssl"): - kwargs.setdefault("server_hostname", wsuri.host) - # If host and port are given, override values from the URI. - host = kwargs.pop("host", host) - port = kwargs.pop("port", port) - create_connection = functools.partial( - loop.create_connection, factory, host, port, **kwargs - ) - - self.open_timeout = open_timeout - if logger is None: - logger = logging.getLogger("websockets.client") - self.logger = logger - - # This is a coroutine function. - self._create_connection = create_connection - self._uri = uri - self._wsuri = wsuri - - def handle_redirect(self, uri: str) -> None: - # Update the state of this instance to connect to a new URI. - old_uri = self._uri - old_wsuri = self._wsuri - new_uri = urllib.parse.urljoin(old_uri, uri) - new_wsuri = parse_uri(new_uri) - - # Forbid TLS downgrade. - if old_wsuri.secure and not new_wsuri.secure: - raise SecurityError("redirect from WSS to WS") - - same_origin = ( - old_wsuri.secure == new_wsuri.secure - and old_wsuri.host == new_wsuri.host - and old_wsuri.port == new_wsuri.port - ) - - # Rewrite secure, host, and port for cross-origin redirects. - # This preserves connection overrides with the host and port - # arguments if the redirect points to the same host and port. - if not same_origin: - factory = self._create_connection.args[0] - # Support TLS upgrade. - if not old_wsuri.secure and new_wsuri.secure: - factory.keywords["secure"] = True - self._create_connection.keywords.setdefault("ssl", True) - # Replace secure, host, and port arguments of the protocol factory. - factory = functools.partial( - factory.func, - *factory.args, - **dict(factory.keywords, host=new_wsuri.host, port=new_wsuri.port), - ) - # Replace secure, host, and port arguments of create_connection. - self._create_connection = functools.partial( - self._create_connection.func, - *(factory, new_wsuri.host, new_wsuri.port), - **self._create_connection.keywords, - ) - - # Set the new WebSocket URI. This suffices for same-origin redirects. - self._uri = new_uri - self._wsuri = new_wsuri - - # async for ... in connect(...): - - BACKOFF_INITIAL = float(os.environ.get("WEBSOCKETS_BACKOFF_INITIAL_DELAY", "5")) - BACKOFF_MIN = float(os.environ.get("WEBSOCKETS_BACKOFF_MIN_DELAY", "3.1")) - BACKOFF_MAX = float(os.environ.get("WEBSOCKETS_BACKOFF_MAX_DELAY", "90.0")) - BACKOFF_FACTOR = float(os.environ.get("WEBSOCKETS_BACKOFF_FACTOR", "1.618")) - - async def __aiter__(self) -> AsyncIterator[WebSocketClientProtocol]: - backoff_delay = self.BACKOFF_MIN / self.BACKOFF_FACTOR - while True: - try: - async with self as protocol: - yield protocol - except Exception as exc: - # Add a random initial delay between 0 and 5 seconds. - # See 7.2.3. Recovering from Abnormal Closure in RFC 6455. - if backoff_delay == self.BACKOFF_MIN: - initial_delay = random.random() * self.BACKOFF_INITIAL - self.logger.info( - "connect failed; reconnecting in %.1f seconds: %s", - initial_delay, - # Remove first argument when dropping Python 3.9. - traceback.format_exception_only(type(exc), exc)[0].strip(), - ) - await asyncio.sleep(initial_delay) - else: - self.logger.info( - "connect failed again; retrying in %d seconds: %s", - int(backoff_delay), - # Remove first argument when dropping Python 3.9. - traceback.format_exception_only(type(exc), exc)[0].strip(), - ) - await asyncio.sleep(int(backoff_delay)) - # Increase delay with truncated exponential backoff. - backoff_delay = backoff_delay * self.BACKOFF_FACTOR - backoff_delay = min(backoff_delay, self.BACKOFF_MAX) - continue - else: - # Connection succeeded - reset backoff delay - backoff_delay = self.BACKOFF_MIN - - # async with connect(...) as ...: - - async def __aenter__(self) -> WebSocketClientProtocol: - return await self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - await self.protocol.close() - - # ... = await connect(...) - - def __await__(self) -> Generator[Any, None, WebSocketClientProtocol]: - # Create a suitable iterator by calling __await__ on a coroutine. - return self.__await_impl__().__await__() - - async def __await_impl__(self) -> WebSocketClientProtocol: - async with asyncio_timeout(self.open_timeout): - for _redirects in range(self.MAX_REDIRECTS_ALLOWED): - _transport, protocol = await self._create_connection() - try: - await protocol.handshake( - self._wsuri, - origin=protocol.origin, - available_extensions=protocol.available_extensions, - available_subprotocols=protocol.available_subprotocols, - extra_headers=protocol.extra_headers, - ) - except RedirectHandshake as exc: - protocol.fail_connection() - await protocol.wait_closed() - self.handle_redirect(exc.uri) - # Avoid leaking a connected socket when the handshake fails. - except (Exception, asyncio.CancelledError): - protocol.fail_connection() - await protocol.wait_closed() - raise - else: - self.protocol = protocol - return protocol - else: - raise SecurityError("too many redirects") - - # ... = yield from connect(...) - remove when dropping Python < 3.10 - - __iter__ = __await__ - - -connect = Connect - - -def unix_connect( - path: str | None = None, - uri: str = "ws://localhost/", - **kwargs: Any, -) -> Connect: - """ - Similar to :func:`connect`, but for connecting to a Unix socket. - - This function builds upon the event loop's - :meth:`~asyncio.loop.create_unix_connection` method. - - It is only available on Unix. - - It's mainly useful for debugging servers listening on Unix sockets. - - Args: - path: File system path to the Unix socket. - uri: URI of the WebSocket server; the host is used in the TLS - handshake for secure connections and in the ``Host`` header. - - """ - return connect(uri=uri, path=path, unix=True, **kwargs) diff --git a/wip/SNIP/websockets/legacy/exceptions.py b/wip/SNIP/websockets/legacy/exceptions.py deleted file mode 100644 index 6604a45..0000000 --- a/wip/SNIP/websockets/legacy/exceptions.py +++ /dev/null @@ -1,71 +0,0 @@ -import http - -from .. import datastructures -from ..exceptions import ( - InvalidHandshake, - # InvalidMessage was incorrectly moved here in versions 14.0 and 14.1. - # noqa: F401 - # noqa: F401 -) -from ..typing import StatusLike - - -class InvalidStatusCode(InvalidHandshake): - """ - Raised when a handshake response status code is invalid. - - """ - - def __init__(self, status_code: int, headers: datastructures.Headers) -> None: - self.status_code = status_code - self.headers = headers - - def __str__(self) -> str: - return f"server rejected WebSocket connection: HTTP {self.status_code}" - - -class AbortHandshake(InvalidHandshake): - """ - Raised to abort the handshake on purpose and return an HTTP response. - - This exception is an implementation detail. - - The public API is - :meth:`~websockets.legacy.server.WebSocketServerProtocol.process_request`. - - Attributes: - status (~http.HTTPStatus): HTTP status code. - headers (Headers): HTTP response headers. - body (bytes): HTTP response body. - """ - - def __init__( - self, - status: StatusLike, - headers: datastructures.HeadersLike, - body: bytes = b"", - ) -> None: - # If a user passes an int instead of an HTTPStatus, fix it automatically. - self.status = http.HTTPStatus(status) - self.headers = datastructures.Headers(headers) - self.body = body - - def __str__(self) -> str: - return ( - f"HTTP {self.status:d}, {len(self.headers)} headers, {len(self.body)} bytes" - ) - - -class RedirectHandshake(InvalidHandshake): - """ - Raised when a handshake gets redirected. - - This exception is an implementation detail. - - """ - - def __init__(self, uri: str) -> None: - self.uri = uri - - def __str__(self) -> str: - return f"redirect to {self.uri}" diff --git a/wip/SNIP/websockets/legacy/framing.py b/wip/SNIP/websockets/legacy/framing.py deleted file mode 100644 index add0c6e..0000000 --- a/wip/SNIP/websockets/legacy/framing.py +++ /dev/null @@ -1,225 +0,0 @@ -from __future__ import annotations - -import struct -from collections.abc import Awaitable, Sequence -from typing import Any, Callable, NamedTuple - -from .. import extensions, frames -from ..exceptions import PayloadTooBig, ProtocolError -from ..frames import BytesLike -from ..typing import Data - - -try: - from ..speedups import apply_mask -except ImportError: - from ..utils import apply_mask - - -class Frame(NamedTuple): - fin: bool - opcode: frames.Opcode - data: bytes - rsv1: bool = False - rsv2: bool = False - rsv3: bool = False - - @property - def new_frame(self) -> frames.Frame: - return frames.Frame( - self.opcode, - self.data, - self.fin, - self.rsv1, - self.rsv2, - self.rsv3, - ) - - def __str__(self) -> str: - return str(self.new_frame) - - def check(self) -> None: - return self.new_frame.check() - - @classmethod - async def read( - cls, - reader: Callable[[int], Awaitable[bytes]], - *, - mask: bool, - max_size: int | None = None, - extensions: Sequence[extensions.Extension] | None = None, - ) -> Frame: - """ - Read a WebSocket frame. - - Args: - reader: Coroutine that reads exactly the requested number of - bytes, unless the end of file is reached. - mask: Whether the frame should be masked i.e. whether the read - happens on the server side. - max_size: Maximum payload size in bytes. - extensions: List of extensions, applied in reverse order. - - Raises: - PayloadTooBig: If the frame exceeds ``max_size``. - ProtocolError: If the frame contains incorrect values. - - """ - - # Read the header. - data = await reader(2) - head1, head2 = struct.unpack("!BB", data) - - # While not Pythonic, this is marginally faster than calling bool(). - fin = True if head1 & 0b10000000 else False - rsv1 = True if head1 & 0b01000000 else False - rsv2 = True if head1 & 0b00100000 else False - rsv3 = True if head1 & 0b00010000 else False - - try: - opcode = frames.Opcode(head1 & 0b00001111) - except ValueError as exc: - raise ProtocolError("invalid opcode") from exc - - if (True if head2 & 0b10000000 else False) != mask: - raise ProtocolError("incorrect masking") - - length = head2 & 0b01111111 - if length == 126: - data = await reader(2) - (length,) = struct.unpack("!H", data) - elif length == 127: - data = await reader(8) - (length,) = struct.unpack("!Q", data) - if max_size is not None and length > max_size: - raise PayloadTooBig(length, max_size) - if mask: - mask_bits = await reader(4) - - # Read the data. - data = await reader(length) - if mask: - data = apply_mask(data, mask_bits) - - new_frame = frames.Frame(opcode, data, fin, rsv1, rsv2, rsv3) - - if extensions is None: - extensions = [] - for extension in reversed(extensions): - new_frame = extension.decode(new_frame, max_size=max_size) - - new_frame.check() - - return cls( - new_frame.fin, - new_frame.opcode, - new_frame.data, - new_frame.rsv1, - new_frame.rsv2, - new_frame.rsv3, - ) - - def write( - self, - write: Callable[[bytes], Any], - *, - mask: bool, - extensions: Sequence[extensions.Extension] | None = None, - ) -> None: - """ - Write a WebSocket frame. - - Args: - frame: Frame to write. - write: Function that writes bytes. - mask: Whether the frame should be masked i.e. whether the write - happens on the client side. - extensions: List of extensions, applied in order. - - Raises: - ProtocolError: If the frame contains incorrect values. - - """ - # The frame is written in a single call to write in order to prevent - # TCP fragmentation. See #68 for details. This also makes it safe to - # send frames concurrently from multiple coroutines. - write(self.new_frame.serialize(mask=mask, extensions=extensions)) - - -def prepare_data(data: Data) -> tuple[int, bytes]: - """ - Convert a string or byte-like object to an opcode and a bytes-like object. - - This function is designed for data frames. - - If ``data`` is a :class:`str`, return ``OP_TEXT`` and a :class:`bytes` - object encoding ``data`` in UTF-8. - - If ``data`` is a bytes-like object, return ``OP_BINARY`` and a bytes-like - object. - - Raises: - TypeError: If ``data`` doesn't have a supported type. - - """ - if isinstance(data, str): - return frames.Opcode.TEXT, data.encode() - elif isinstance(data, BytesLike): - return frames.Opcode.BINARY, data - else: - raise TypeError("data must be str or bytes-like") - - -def prepare_ctrl(data: Data) -> bytes: - """ - Convert a string or byte-like object to bytes. - - This function is designed for ping and pong frames. - - If ``data`` is a :class:`str`, return a :class:`bytes` object encoding - ``data`` in UTF-8. - - If ``data`` is a bytes-like object, return a :class:`bytes` object. - - Raises: - TypeError: If ``data`` doesn't have a supported type. - - """ - if isinstance(data, str): - return data.encode() - elif isinstance(data, BytesLike): - return bytes(data) - else: - raise TypeError("data must be str or bytes-like") - - -# Backwards compatibility with previously documented public APIs -encode_data = prepare_ctrl - -# Backwards compatibility with previously documented public APIs -from ..frames import Close # noqa: E402 F401, I001 - - -def parse_close(data: bytes) -> tuple[int, str]: - """ - Parse the payload from a close frame. - - Returns: - Close code and reason. - - Raises: - ProtocolError: If data is ill-formed. - UnicodeDecodeError: If the reason isn't valid UTF-8. - - """ - close = Close.parse(data) - return close.code, close.reason - - -def serialize_close(code: int, reason: str) -> bytes: - """ - Serialize the payload for a close frame. - - """ - return Close(code, reason).serialize() diff --git a/wip/SNIP/websockets/legacy/handshake.py b/wip/SNIP/websockets/legacy/handshake.py deleted file mode 100644 index 6a7157c..0000000 --- a/wip/SNIP/websockets/legacy/handshake.py +++ /dev/null @@ -1,158 +0,0 @@ -from __future__ import annotations - -import base64 -import binascii - -from ..datastructures import Headers, MultipleValuesError -from ..exceptions import InvalidHeader, InvalidHeaderValue, InvalidUpgrade -from ..headers import parse_connection, parse_upgrade -from ..typing import ConnectionOption, UpgradeProtocol -from ..utils import accept_key as accept, generate_key - - -__all__ = ["build_request", "check_request", "build_response", "check_response"] - - -def build_request(headers: Headers) -> str: - """ - Build a handshake request to send to the server. - - Update request headers passed in argument. - - Args: - headers: Handshake request headers. - - Returns: - ``key`` that must be passed to :func:`check_response`. - - """ - key = generate_key() - headers["Upgrade"] = "websocket" - headers["Connection"] = "Upgrade" - headers["Sec-WebSocket-Key"] = key - headers["Sec-WebSocket-Version"] = "13" - return key - - -def check_request(headers: Headers) -> str: - """ - Check a handshake request received from the client. - - This function doesn't verify that the request is an HTTP/1.1 or higher GET - request and doesn't perform ``Host`` and ``Origin`` checks. These controls - are usually performed earlier in the HTTP request handling code. They're - the responsibility of the caller. - - Args: - headers: Handshake request headers. - - Returns: - ``key`` that must be passed to :func:`build_response`. - - Raises: - InvalidHandshake: If the handshake request is invalid. - Then, the server must return a 400 Bad Request error. - - """ - connection: list[ConnectionOption] = sum( - [parse_connection(value) for value in headers.get_all("Connection")], [] - ) - - if not any(value.lower() == "upgrade" for value in connection): - raise InvalidUpgrade("Connection", ", ".join(connection)) - - upgrade: list[UpgradeProtocol] = sum( - [parse_upgrade(value) for value in headers.get_all("Upgrade")], [] - ) - - # For compatibility with non-strict implementations, ignore case when - # checking the Upgrade header. The RFC always uses "websocket", except - # in section 11.2. (IANA registration) where it uses "WebSocket". - if not (len(upgrade) == 1 and upgrade[0].lower() == "websocket"): - raise InvalidUpgrade("Upgrade", ", ".join(upgrade)) - - try: - s_w_key = headers["Sec-WebSocket-Key"] - except KeyError as exc: - raise InvalidHeader("Sec-WebSocket-Key") from exc - except MultipleValuesError as exc: - raise InvalidHeader("Sec-WebSocket-Key", "multiple values") from exc - - try: - raw_key = base64.b64decode(s_w_key.encode(), validate=True) - except binascii.Error as exc: - raise InvalidHeaderValue("Sec-WebSocket-Key", s_w_key) from exc - if len(raw_key) != 16: - raise InvalidHeaderValue("Sec-WebSocket-Key", s_w_key) - - try: - s_w_version = headers["Sec-WebSocket-Version"] - except KeyError as exc: - raise InvalidHeader("Sec-WebSocket-Version") from exc - except MultipleValuesError as exc: - raise InvalidHeader("Sec-WebSocket-Version", "multiple values") from exc - - if s_w_version != "13": - raise InvalidHeaderValue("Sec-WebSocket-Version", s_w_version) - - return s_w_key - - -def build_response(headers: Headers, key: str) -> None: - """ - Build a handshake response to send to the client. - - Update response headers passed in argument. - - Args: - headers: Handshake response headers. - key: Returned by :func:`check_request`. - - """ - headers["Upgrade"] = "websocket" - headers["Connection"] = "Upgrade" - headers["Sec-WebSocket-Accept"] = accept(key) - - -def check_response(headers: Headers, key: str) -> None: - """ - Check a handshake response received from the server. - - This function doesn't verify that the response is an HTTP/1.1 or higher - response with a 101 status code. These controls are the responsibility of - the caller. - - Args: - headers: Handshake response headers. - key: Returned by :func:`build_request`. - - Raises: - InvalidHandshake: If the handshake response is invalid. - - """ - connection: list[ConnectionOption] = sum( - [parse_connection(value) for value in headers.get_all("Connection")], [] - ) - - if not any(value.lower() == "upgrade" for value in connection): - raise InvalidUpgrade("Connection", " ".join(connection)) - - upgrade: list[UpgradeProtocol] = sum( - [parse_upgrade(value) for value in headers.get_all("Upgrade")], [] - ) - - # For compatibility with non-strict implementations, ignore case when - # checking the Upgrade header. The RFC always uses "websocket", except - # in section 11.2. (IANA registration) where it uses "WebSocket". - if not (len(upgrade) == 1 and upgrade[0].lower() == "websocket"): - raise InvalidUpgrade("Upgrade", ", ".join(upgrade)) - - try: - s_w_accept = headers["Sec-WebSocket-Accept"] - except KeyError as exc: - raise InvalidHeader("Sec-WebSocket-Accept") from exc - except MultipleValuesError as exc: - raise InvalidHeader("Sec-WebSocket-Accept", "multiple values") from exc - - if s_w_accept != accept(key): - raise InvalidHeaderValue("Sec-WebSocket-Accept", s_w_accept) diff --git a/wip/SNIP/websockets/legacy/http.py b/wip/SNIP/websockets/legacy/http.py deleted file mode 100644 index a7c8a92..0000000 --- a/wip/SNIP/websockets/legacy/http.py +++ /dev/null @@ -1,201 +0,0 @@ -from __future__ import annotations - -import asyncio -import os -import re - -from ..datastructures import Headers -from ..exceptions import SecurityError - - -__all__ = ["read_request", "read_response"] - -MAX_NUM_HEADERS = int(os.environ.get("WEBSOCKETS_MAX_NUM_HEADERS", "128")) -MAX_LINE_LENGTH = int(os.environ.get("WEBSOCKETS_MAX_LINE_LENGTH", "8192")) - - -def d(value: bytes) -> str: - """ - Decode a bytestring for interpolating into an error message. - - """ - return value.decode(errors="backslashreplace") - - -# See https://datatracker.ietf.org/doc/html/rfc7230#appendix-B. - -# Regex for validating header names. - -_token_re = re.compile(rb"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+") - -# Regex for validating header values. - -# We don't attempt to support obsolete line folding. - -# Include HTAB (\x09), SP (\x20), VCHAR (\x21-\x7e), obs-text (\x80-\xff). - -# The ABNF is complicated because it attempts to express that optional -# whitespace is ignored. We strip whitespace and don't revalidate that. - -# See also https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189 - -_value_re = re.compile(rb"[\x09\x20-\x7e\x80-\xff]*") - - -async def read_request(stream: asyncio.StreamReader) -> tuple[str, Headers]: - """ - Read an HTTP/1.1 GET request and return ``(path, headers)``. - - ``path`` isn't URL-decoded or validated in any way. - - ``path`` and ``headers`` are expected to contain only ASCII characters. - Other characters are represented with surrogate escapes. - - :func:`read_request` doesn't attempt to read the request body because - WebSocket handshake requests don't have one. If the request contains a - body, it may be read from ``stream`` after this coroutine returns. - - Args: - stream: Input to read the request from. - - Raises: - EOFError: If the connection is closed without a full HTTP request. - SecurityError: If the request exceeds a security limit. - ValueError: If the request isn't well formatted. - - """ - # https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.1 - - # Parsing is simple because fixed values are expected for method and - # version and because path isn't checked. Since WebSocket software tends - # to implement HTTP/1.1 strictly, there's little need for lenient parsing. - - try: - request_line = await read_line(stream) - except EOFError as exc: - raise EOFError("connection closed while reading HTTP request line") from exc - - try: - method, raw_path, version = request_line.split(b" ", 2) - except ValueError: # not enough values to unpack (expected 3, got 1-2) - raise ValueError(f"invalid HTTP request line: {d(request_line)}") from None - - if method != b"GET": - raise ValueError(f"unsupported HTTP method: {d(method)}") - if version != b"HTTP/1.1": - raise ValueError(f"unsupported HTTP version: {d(version)}") - path = raw_path.decode("ascii", "surrogateescape") - - headers = await read_headers(stream) - - return path, headers - - -async def read_response(stream: asyncio.StreamReader) -> tuple[int, str, Headers]: - """ - Read an HTTP/1.1 response and return ``(status_code, reason, headers)``. - - ``reason`` and ``headers`` are expected to contain only ASCII characters. - Other characters are represented with surrogate escapes. - - :func:`read_request` doesn't attempt to read the response body because - WebSocket handshake responses don't have one. If the response contains a - body, it may be read from ``stream`` after this coroutine returns. - - Args: - stream: Input to read the response from. - - Raises: - EOFError: If the connection is closed without a full HTTP response. - SecurityError: If the response exceeds a security limit. - ValueError: If the response isn't well formatted. - - """ - # https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2 - - # As in read_request, parsing is simple because a fixed value is expected - # for version, status_code is a 3-digit number, and reason can be ignored. - - try: - status_line = await read_line(stream) - except EOFError as exc: - raise EOFError("connection closed while reading HTTP status line") from exc - - try: - version, raw_status_code, raw_reason = status_line.split(b" ", 2) - except ValueError: # not enough values to unpack (expected 3, got 1-2) - raise ValueError(f"invalid HTTP status line: {d(status_line)}") from None - - if version != b"HTTP/1.1": - raise ValueError(f"unsupported HTTP version: {d(version)}") - try: - status_code = int(raw_status_code) - except ValueError: # invalid literal for int() with base 10 - raise ValueError(f"invalid HTTP status code: {d(raw_status_code)}") from None - if not 100 <= status_code < 1000: - raise ValueError(f"unsupported HTTP status code: {d(raw_status_code)}") - if not _value_re.fullmatch(raw_reason): - raise ValueError(f"invalid HTTP reason phrase: {d(raw_reason)}") - reason = raw_reason.decode() - - headers = await read_headers(stream) - - return status_code, reason, headers - - -async def read_headers(stream: asyncio.StreamReader) -> Headers: - """ - Read HTTP headers from ``stream``. - - Non-ASCII characters are represented with surrogate escapes. - - """ - # https://datatracker.ietf.org/doc/html/rfc7230#section-3.2 - - # We don't attempt to support obsolete line folding. - - headers = Headers() - for _ in range(MAX_NUM_HEADERS + 1): - try: - line = await read_line(stream) - except EOFError as exc: - raise EOFError("connection closed while reading HTTP headers") from exc - if line == b"": - break - - try: - raw_name, raw_value = line.split(b":", 1) - except ValueError: # not enough values to unpack (expected 2, got 1) - raise ValueError(f"invalid HTTP header line: {d(line)}") from None - if not _token_re.fullmatch(raw_name): - raise ValueError(f"invalid HTTP header name: {d(raw_name)}") - raw_value = raw_value.strip(b" \t") - if not _value_re.fullmatch(raw_value): - raise ValueError(f"invalid HTTP header value: {d(raw_value)}") - - name = raw_name.decode("ascii") # guaranteed to be ASCII at this point - value = raw_value.decode("ascii", "surrogateescape") - headers[name] = value - - else: - raise SecurityError("too many HTTP headers") - - return headers - - -async def read_line(stream: asyncio.StreamReader) -> bytes: - """ - Read a single line from ``stream``. - - CRLF is stripped from the return value. - - """ - # Security: this is bounded by the StreamReader's limit (default = 32 KiB). - line = await stream.readline() - # Security: this guarantees header values are small (hard-coded = 8 KiB) - if len(line) > MAX_LINE_LENGTH: - raise SecurityError("line too long") - # Not mandatory but safe - https://datatracker.ietf.org/doc/html/rfc7230#section-3.5 - if not line.endswith(b"\r\n"): - raise EOFError("line without CRLF") - return line[:-2] diff --git a/wip/SNIP/websockets/legacy/protocol.py b/wip/SNIP/websockets/legacy/protocol.py deleted file mode 100644 index db126c0..0000000 --- a/wip/SNIP/websockets/legacy/protocol.py +++ /dev/null @@ -1,1641 +0,0 @@ -from __future__ import annotations - -import asyncio -import codecs -import collections -import logging -import random -import ssl -import struct -import sys -import time -import traceback -import uuid -import warnings -from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Iterable, Mapping -from typing import Any, Callable, Deque, cast - -from ..asyncio.compatibility import asyncio_timeout -from ..datastructures import Headers -from ..exceptions import ( - ConnectionClosed, - ConnectionClosedError, - ConnectionClosedOK, - InvalidState, - PayloadTooBig, - ProtocolError, -) -from ..extensions import Extension -from ..frames import ( - OK_CLOSE_CODES, - OP_BINARY, - OP_CLOSE, - OP_CONT, - OP_PING, - OP_PONG, - OP_TEXT, - Close, - CloseCode, - Opcode, -) -from ..protocol import State -from ..typing import Data, LoggerLike, Subprotocol -from .framing import Frame, prepare_ctrl, prepare_data - - -__all__ = ["WebSocketCommonProtocol"] - - -# In order to ensure consistency, the code always checks the current value of -# WebSocketCommonProtocol.state before assigning a new value and never yields -# between the check and the assignment. - - -class WebSocketCommonProtocol(asyncio.Protocol): - """ - WebSocket connection. - - :class:`WebSocketCommonProtocol` provides APIs shared between WebSocket - servers and clients. You shouldn't use it directly. Instead, use - :class:`~websockets.legacy.client.WebSocketClientProtocol` or - :class:`~websockets.legacy.server.WebSocketServerProtocol`. - - This documentation focuses on low-level details that aren't covered in the - documentation of :class:`~websockets.legacy.client.WebSocketClientProtocol` - and :class:`~websockets.legacy.server.WebSocketServerProtocol` for the sake - of simplicity. - - Once the connection is open, a Ping_ frame is sent every ``ping_interval`` - seconds. This serves as a keepalive. It helps keeping the connection open, - especially in the presence of proxies with short timeouts on inactive - connections. Set ``ping_interval`` to :obj:`None` to disable this behavior. - - .. _Ping: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2 - - If the corresponding Pong_ frame isn't received within ``ping_timeout`` - seconds, the connection is considered unusable and is closed with code 1011. - This ensures that the remote endpoint remains responsive. Set - ``ping_timeout`` to :obj:`None` to disable this behavior. - - .. _Pong: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.3 - - See the discussion of :doc:`keepalive <../../topics/keepalive>` for details. - - The ``close_timeout`` parameter defines a maximum wait time for completing - the closing handshake and terminating the TCP connection. For legacy - reasons, :meth:`close` completes in at most ``5 * close_timeout`` seconds - for clients and ``4 * close_timeout`` for servers. - - ``close_timeout`` is a parameter of the protocol because websockets usually - calls :meth:`close` implicitly upon exit: - - * on the client side, when using :func:`~websockets.legacy.client.connect` - as a context manager; - * on the server side, when the connection handler terminates. - - To apply a timeout to any other API, wrap it in :func:`~asyncio.timeout` or - :func:`~asyncio.wait_for`. - - The ``max_size`` parameter enforces the maximum size for incoming messages - in bytes. The default value is 1 MiB. If a larger message is received, - :meth:`recv` will raise :exc:`~websockets.exceptions.ConnectionClosedError` - and the connection will be closed with code 1009. - - The ``max_queue`` parameter sets the maximum length of the queue that - holds incoming messages. The default value is ``32``. Messages are added - to an in-memory queue when they're received; then :meth:`recv` pops from - that queue. In order to prevent excessive memory consumption when - messages are received faster than they can be processed, the queue must - be bounded. If the queue fills up, the protocol stops processing incoming - data until :meth:`recv` is called. In this situation, various receive - buffers (at least in :mod:`asyncio` and in the OS) will fill up, then the - TCP receive window will shrink, slowing down transmission to avoid packet - loss. - - Since Python can use up to 4 bytes of memory to represent a single - character, each connection may use up to ``4 * max_size * max_queue`` - bytes of memory to store incoming messages. By default, this is 128 MiB. - You may want to lower the limits, depending on your application's - requirements. - - The ``read_limit`` argument sets the high-water limit of the buffer for - incoming bytes. The low-water limit is half the high-water limit. The - default value is 64 KiB, half of asyncio's default (based on the current - implementation of :class:`~asyncio.StreamReader`). - - The ``write_limit`` argument sets the high-water limit of the buffer for - outgoing bytes. The low-water limit is a quarter of the high-water limit. - The default value is 64 KiB, equal to asyncio's default (based on the - current implementation of ``FlowControlMixin``). - - See the discussion of :doc:`memory usage <../../topics/memory>` for details. - - Args: - logger: Logger for this server. - It defaults to ``logging.getLogger("websockets.protocol")``. - See the :doc:`logging guide <../../topics/logging>` for details. - ping_interval: Interval between keepalive pings in seconds. - :obj:`None` disables keepalive. - ping_timeout: Timeout for keepalive pings in seconds. - :obj:`None` disables timeouts. - close_timeout: Timeout for closing the connection in seconds. - For legacy reasons, the actual timeout is 4 or 5 times larger. - max_size: Maximum size of incoming messages in bytes. - :obj:`None` disables the limit. - max_queue: Maximum number of incoming messages in receive buffer. - :obj:`None` disables the limit. - read_limit: High-water mark of read buffer in bytes. - write_limit: High-water mark of write buffer in bytes. - - """ - - # There are only two differences between the client-side and server-side - # behavior: masking the payload and closing the underlying TCP connection. - # Set is_client = True/False and side = "client"/"server" to pick a side. - is_client: bool - side: str = "undefined" - - def __init__( - self, - *, - logger: LoggerLike | None = None, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = None, - max_size: int | None = 2**20, - max_queue: int | None = 2**5, - read_limit: int = 2**16, - write_limit: int = 2**16, - # The following arguments are kept only for backwards compatibility. - host: str | None = None, - port: int | None = None, - secure: bool | None = None, - legacy_recv: bool = False, - loop: asyncio.AbstractEventLoop | None = None, - timeout: float | None = None, - ) -> None: - if legacy_recv: # pragma: no cover - warnings.warn("legacy_recv is deprecated", DeprecationWarning) - - # Backwards compatibility: close_timeout used to be called timeout. - if timeout is None: - timeout = 10 - else: - warnings.warn("rename timeout to close_timeout", DeprecationWarning) - # If both are specified, timeout is ignored. - if close_timeout is None: - close_timeout = timeout - - # Backwards compatibility: the loop parameter used to be supported. - if loop is None: - loop = asyncio.get_event_loop() - else: - warnings.warn("remove loop argument", DeprecationWarning) - - self.ping_interval = ping_interval - self.ping_timeout = ping_timeout - self.close_timeout = close_timeout - self.max_size = max_size - self.max_queue = max_queue - self.read_limit = read_limit - self.write_limit = write_limit - - # Unique identifier. For logs. - self.id: uuid.UUID = uuid.uuid4() - """Unique identifier of the connection. Useful in logs.""" - - # Logger or LoggerAdapter for this connection. - if logger is None: - logger = logging.getLogger("websockets.protocol") - self.logger: LoggerLike = logging.LoggerAdapter(logger, {"websocket": self}) - """Logger for this connection.""" - - # Track if DEBUG is enabled. Shortcut logging calls if it isn't. - self.debug = logger.isEnabledFor(logging.DEBUG) - - self.loop = loop - - self._host = host - self._port = port - self._secure = secure - self.legacy_recv = legacy_recv - - # Configure read buffer limits. The high-water limit is defined by - # ``self.read_limit``. The ``limit`` argument controls the line length - # limit and half the buffer limit of :class:`~asyncio.StreamReader`. - # That's why it must be set to half of ``self.read_limit``. - self.reader = asyncio.StreamReader(limit=read_limit // 2, loop=loop) - - # Copied from asyncio.FlowControlMixin - self._paused = False - self._drain_waiter: asyncio.Future[None] | None = None - - self._drain_lock = asyncio.Lock() - - # This class implements the data transfer and closing handshake, which - # are shared between the client-side and the server-side. - # Subclasses implement the opening handshake and, on success, execute - # :meth:`connection_open` to change the state to OPEN. - self.state = State.CONNECTING - if self.debug: - self.logger.debug("= connection is CONNECTING") - - # HTTP protocol parameters. - self.path: str - """Path of the opening handshake request.""" - self.request_headers: Headers - """Opening handshake request headers.""" - self.response_headers: Headers - """Opening handshake response headers.""" - - # WebSocket protocol parameters. - self.extensions: list[Extension] = [] - self.subprotocol: Subprotocol | None = None - """Subprotocol, if one was negotiated.""" - - # Close code and reason, set when a close frame is sent or received. - self.close_rcvd: Close | None = None - self.close_sent: Close | None = None - self.close_rcvd_then_sent: bool | None = None - - # Completed when the connection state becomes CLOSED. Translates the - # :meth:`connection_lost` callback to a :class:`~asyncio.Future` - # that can be awaited. (Other :class:`~asyncio.Protocol` callbacks are - # translated by ``self.stream_reader``). - self.connection_lost_waiter: asyncio.Future[None] = loop.create_future() - - # Queue of received messages. - self.messages: Deque[Data] = collections.deque() - self._pop_message_waiter: asyncio.Future[None] | None = None - self._put_message_waiter: asyncio.Future[None] | None = None - - # Protect sending fragmented messages. - self._fragmented_message_waiter: asyncio.Future[None] | None = None - - # Mapping of ping IDs to pong waiters, in chronological order. - self.pings: dict[bytes, tuple[asyncio.Future[float], float]] = {} - - self.latency: float = 0 - """ - Latency of the connection, in seconds. - - Latency is defined as the round-trip time of the connection. It is - measured by sending a Ping frame and waiting for a matching Pong frame. - Before the first measurement, :attr:`latency` is ``0``. - - By default, websockets enables a :ref:`keepalive ` mechanism - that sends Ping frames automatically at regular intervals. You can also - send Ping frames and measure latency with :meth:`ping`. - """ - - # Task running the data transfer. - self.transfer_data_task: asyncio.Task[None] - - # Exception that occurred during data transfer, if any. - self.transfer_data_exc: BaseException | None = None - - # Task sending keepalive pings. - self.keepalive_ping_task: asyncio.Task[None] - - # Task closing the TCP connection. - self.close_connection_task: asyncio.Task[None] - - # Copied from asyncio.FlowControlMixin - async def _drain_helper(self) -> None: # pragma: no cover - if self.connection_lost_waiter.done(): - raise ConnectionResetError("Connection lost") - if not self._paused: - return - waiter = self._drain_waiter - assert waiter is None or waiter.cancelled() - waiter = self.loop.create_future() - self._drain_waiter = waiter - await waiter - - # Copied from asyncio.StreamWriter - async def _drain(self) -> None: # pragma: no cover - if self.reader is not None: - exc = self.reader.exception() - if exc is not None: - raise exc - if self.transport is not None: - if self.transport.is_closing(): - # Yield to the event loop so connection_lost() may be - # called. Without this, _drain_helper() would return - # immediately, and code that calls - # write(...); yield from drain() - # in a loop would never call connection_lost(), so it - # would not see an error when the socket is closed. - await asyncio.sleep(0) - await self._drain_helper() - - def connection_open(self) -> None: - """ - Callback when the WebSocket opening handshake completes. - - Enter the OPEN state and start the data transfer phase. - - """ - # 4.1. The WebSocket Connection is Established. - assert self.state is State.CONNECTING - self.state = State.OPEN - if self.debug: - self.logger.debug("= connection is OPEN") - # Start the task that receives incoming WebSocket messages. - self.transfer_data_task = self.loop.create_task(self.transfer_data()) - # Start the task that sends pings at regular intervals. - self.keepalive_ping_task = self.loop.create_task(self.keepalive_ping()) - # Start the task that eventually closes the TCP connection. - self.close_connection_task = self.loop.create_task(self.close_connection()) - - @property - def host(self) -> str | None: - alternative = "remote_address" if self.is_client else "local_address" - warnings.warn(f"use {alternative}[0] instead of host", DeprecationWarning) - return self._host - - @property - def port(self) -> int | None: - alternative = "remote_address" if self.is_client else "local_address" - warnings.warn(f"use {alternative}[1] instead of port", DeprecationWarning) - return self._port - - @property - def secure(self) -> bool | None: - warnings.warn("don't use secure", DeprecationWarning) - return self._secure - - # Public API - - @property - def local_address(self) -> Any: - """ - Local address of the connection. - - For IPv4 connections, this is a ``(host, port)`` tuple. - - The format of the address depends on the address family; - see :meth:`~socket.socket.getsockname`. - - :obj:`None` if the TCP connection isn't established yet. - - """ - try: - transport = self.transport - except AttributeError: - return None - else: - return transport.get_extra_info("sockname") - - @property - def remote_address(self) -> Any: - """ - Remote address of the connection. - - For IPv4 connections, this is a ``(host, port)`` tuple. - - The format of the address depends on the address family; - see :meth:`~socket.socket.getpeername`. - - :obj:`None` if the TCP connection isn't established yet. - - """ - try: - transport = self.transport - except AttributeError: - return None - else: - return transport.get_extra_info("peername") - - @property - def open(self) -> bool: - """ - :obj:`True` when the connection is open; :obj:`False` otherwise. - - This attribute may be used to detect disconnections. However, this - approach is discouraged per the EAFP_ principle. Instead, you should - handle :exc:`~websockets.exceptions.ConnectionClosed` exceptions. - - .. _EAFP: https://docs.python.org/3/glossary.html#term-eafp - - """ - return self.state is State.OPEN and not self.transfer_data_task.done() - - @property - def closed(self) -> bool: - """ - :obj:`True` when the connection is closed; :obj:`False` otherwise. - - Be aware that both :attr:`open` and :attr:`closed` are :obj:`False` - during the opening and closing sequences. - - """ - return self.state is State.CLOSED - - @property - def close_code(self) -> int | None: - """ - WebSocket close code, defined in `section 7.1.5 of RFC 6455`_. - - .. _section 7.1.5 of RFC 6455: - https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5 - - :obj:`None` if the connection isn't closed yet. - - """ - if self.state is not State.CLOSED: - return None - elif self.close_rcvd is None: - return CloseCode.ABNORMAL_CLOSURE - else: - return self.close_rcvd.code - - @property - def close_reason(self) -> str | None: - """ - WebSocket close reason, defined in `section 7.1.6 of RFC 6455`_. - - .. _section 7.1.6 of RFC 6455: - https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6 - - :obj:`None` if the connection isn't closed yet. - - """ - if self.state is not State.CLOSED: - return None - elif self.close_rcvd is None: - return "" - else: - return self.close_rcvd.reason - - async def __aiter__(self) -> AsyncIterator[Data]: - """ - Iterate on incoming messages. - - The iterator exits normally when the connection is closed with the close - code 1000 (OK) or 1001 (going away) or without a close code. - - It raises a :exc:`~websockets.exceptions.ConnectionClosedError` - exception when the connection is closed with any other code. - - """ - try: - while True: - yield await self.recv() - except ConnectionClosedOK: - return - - async def recv(self) -> Data: - """ - Receive the next message. - - When the connection is closed, :meth:`recv` raises - :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it raises - :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal - connection closure and - :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol - error or a network failure. This is how you detect the end of the - message stream. - - Canceling :meth:`recv` is safe. There's no risk of losing the next - message. The next invocation of :meth:`recv` will return it. - - This makes it possible to enforce a timeout by wrapping :meth:`recv` in - :func:`~asyncio.timeout` or :func:`~asyncio.wait_for`. - - Returns: - A string (:class:`str`) for a Text_ frame. A bytestring - (:class:`bytes`) for a Binary_ frame. - - .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - .. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - Raises: - ConnectionClosed: When the connection is closed. - RuntimeError: If two coroutines call :meth:`recv` concurrently. - - """ - if self._pop_message_waiter is not None: - raise RuntimeError( - "cannot call recv while another coroutine " - "is already waiting for the next message" - ) - - # Don't await self.ensure_open() here: - # - messages could be available in the queue even if the connection - # is closed; - # - messages could be received before the closing frame even if the - # connection is closing. - - # Wait until there's a message in the queue (if necessary) or the - # connection is closed. - while len(self.messages) <= 0: - pop_message_waiter: asyncio.Future[None] = self.loop.create_future() - self._pop_message_waiter = pop_message_waiter - try: - # If asyncio.wait() is canceled, it doesn't cancel - # pop_message_waiter and self.transfer_data_task. - await asyncio.wait( - [pop_message_waiter, self.transfer_data_task], - return_when=asyncio.FIRST_COMPLETED, - ) - finally: - self._pop_message_waiter = None - - # If asyncio.wait(...) exited because self.transfer_data_task - # completed before receiving a new message, raise a suitable - # exception (or return None if legacy_recv is enabled). - if not pop_message_waiter.done(): - if self.legacy_recv: - return None # type: ignore - else: - # Wait until the connection is closed to raise - # ConnectionClosed with the correct code and reason. - await self.ensure_open() - - # Pop a message from the queue. - message = self.messages.popleft() - - # Notify transfer_data(). - if self._put_message_waiter is not None: - self._put_message_waiter.set_result(None) - self._put_message_waiter = None - - return message - - async def send( - self, - message: Data | Iterable[Data] | AsyncIterable[Data], - ) -> None: - """ - Send a message. - - A string (:class:`str`) is sent as a Text_ frame. A bytestring or - bytes-like object (:class:`bytes`, :class:`bytearray`, or - :class:`memoryview`) is sent as a Binary_ frame. - - .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - .. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - :meth:`send` also accepts an iterable or an asynchronous iterable of - strings, bytestrings, or bytes-like objects to enable fragmentation_. - Each item is treated as a message fragment and sent in its own frame. - All items must be of the same type, or else :meth:`send` will raise a - :exc:`TypeError` and the connection will be closed. - - .. _fragmentation: https://datatracker.ietf.org/doc/html/rfc6455#section-5.4 - - :meth:`send` rejects dict-like objects because this is often an error. - (If you want to send the keys of a dict-like object as fragments, call - its :meth:`~dict.keys` method and pass the result to :meth:`send`.) - - Canceling :meth:`send` is discouraged. Instead, you should close the - connection with :meth:`close`. Indeed, there are only two situations - where :meth:`send` may yield control to the event loop and then get - canceled; in both cases, :meth:`close` has the same effect and is - more clear: - - 1. The write buffer is full. If you don't want to wait until enough - data is sent, your only alternative is to close the connection. - :meth:`close` will likely time out then abort the TCP connection. - 2. ``message`` is an asynchronous iterator that yields control. - Stopping in the middle of a fragmented message will cause a - protocol error and the connection will be closed. - - When the connection is closed, :meth:`send` raises - :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it - raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal - connection closure and - :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol - error or a network failure. - - Args: - message: Message to send. - - Raises: - ConnectionClosed: When the connection is closed. - TypeError: If ``message`` doesn't have a supported type. - - """ - await self.ensure_open() - - # While sending a fragmented message, prevent sending other messages - # until all fragments are sent. - while self._fragmented_message_waiter is not None: - await asyncio.shield(self._fragmented_message_waiter) - - # Unfragmented message -- this case must be handled first because - # strings and bytes-like objects are iterable. - - if isinstance(message, (str, bytes, bytearray, memoryview)): - opcode, data = prepare_data(message) - await self.write_frame(True, opcode, data) - - # Catch a common mistake -- passing a dict to send(). - - elif isinstance(message, Mapping): - raise TypeError("data is a dict-like object") - - # Fragmented message -- regular iterator. - - elif isinstance(message, Iterable): - # Work around https://github.com/python/mypy/issues/6227 - message = cast(Iterable[Data], message) - - iter_message = iter(message) - try: - fragment = next(iter_message) - except StopIteration: - return - opcode, data = prepare_data(fragment) - - self._fragmented_message_waiter = self.loop.create_future() - try: - # First fragment. - await self.write_frame(False, opcode, data) - - # Other fragments. - for fragment in iter_message: - confirm_opcode, data = prepare_data(fragment) - if confirm_opcode != opcode: - raise TypeError("data contains inconsistent types") - await self.write_frame(False, OP_CONT, data) - - # Final fragment. - await self.write_frame(True, OP_CONT, b"") - - except (Exception, asyncio.CancelledError): - # We're half-way through a fragmented message and we can't - # complete it. This makes the connection unusable. - self.fail_connection(CloseCode.INTERNAL_ERROR) - raise - - finally: - self._fragmented_message_waiter.set_result(None) - self._fragmented_message_waiter = None - - # Fragmented message -- asynchronous iterator - - elif isinstance(message, AsyncIterable): - # Implement aiter_message = aiter(message) without aiter - # Work around https://github.com/python/mypy/issues/5738 - aiter_message = cast( - Callable[[AsyncIterable[Data]], AsyncIterator[Data]], - type(message).__aiter__, - )(message) - try: - # Implement fragment = anext(aiter_message) without anext - # Work around https://github.com/python/mypy/issues/5738 - fragment = await cast( - Callable[[AsyncIterator[Data]], Awaitable[Data]], - type(aiter_message).__anext__, - )(aiter_message) - except StopAsyncIteration: - return - opcode, data = prepare_data(fragment) - - self._fragmented_message_waiter = self.loop.create_future() - try: - # First fragment. - await self.write_frame(False, opcode, data) - - # Other fragments. - async for fragment in aiter_message: - confirm_opcode, data = prepare_data(fragment) - if confirm_opcode != opcode: - raise TypeError("data contains inconsistent types") - await self.write_frame(False, OP_CONT, data) - - # Final fragment. - await self.write_frame(True, OP_CONT, b"") - - except (Exception, asyncio.CancelledError): - # We're half-way through a fragmented message and we can't - # complete it. This makes the connection unusable. - self.fail_connection(CloseCode.INTERNAL_ERROR) - raise - - finally: - self._fragmented_message_waiter.set_result(None) - self._fragmented_message_waiter = None - - else: - raise TypeError("data must be str, bytes-like, or iterable") - - async def close( - self, - code: int = CloseCode.NORMAL_CLOSURE, - reason: str = "", - ) -> None: - """ - Perform the closing handshake. - - :meth:`close` waits for the other end to complete the handshake and - for the TCP connection to terminate. As a consequence, there's no need - to await :meth:`wait_closed` after :meth:`close`. - - :meth:`close` is idempotent: it doesn't do anything once the - connection is closed. - - Wrapping :func:`close` in :func:`~asyncio.create_task` is safe, given - that errors during connection termination aren't particularly useful. - - Canceling :meth:`close` is discouraged. If it takes too long, you can - set a shorter ``close_timeout``. If you don't want to wait, let the - Python process exit, then the OS will take care of closing the TCP - connection. - - Args: - code: WebSocket close code. - reason: WebSocket close reason. - - """ - try: - async with asyncio_timeout(self.close_timeout): - await self.write_close_frame(Close(code, reason)) - except asyncio.TimeoutError: - # If the close frame cannot be sent because the send buffers - # are full, the closing handshake won't complete anyway. - # Fail the connection to shut down faster. - self.fail_connection() - - # If no close frame is received within the timeout, asyncio_timeout() - # cancels the data transfer task and raises TimeoutError. - - # If close() is called multiple times concurrently and one of these - # calls hits the timeout, the data transfer task will be canceled. - # Other calls will receive a CancelledError here. - - try: - # If close() is canceled during the wait, self.transfer_data_task - # is canceled before the timeout elapses. - async with asyncio_timeout(self.close_timeout): - await self.transfer_data_task - except (asyncio.TimeoutError, asyncio.CancelledError): - pass - - # Wait for the close connection task to close the TCP connection. - await asyncio.shield(self.close_connection_task) - - async def wait_closed(self) -> None: - """ - Wait until the connection is closed. - - This coroutine is identical to the :attr:`closed` attribute, except it - can be awaited. - - This can make it easier to detect connection termination, regardless - of its cause, in tasks that interact with the WebSocket connection. - - """ - await asyncio.shield(self.connection_lost_waiter) - - async def ping(self, data: Data | None = None) -> Awaitable[float]: - """ - Send a Ping_. - - .. _Ping: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2 - - A ping may serve as a keepalive, as a check that the remote endpoint - received all messages up to this point, or to measure :attr:`latency`. - - Canceling :meth:`ping` is discouraged. If :meth:`ping` doesn't return - immediately, it means the write buffer is full. If you don't want to - wait, you should close the connection. - - Canceling the :class:`~asyncio.Future` returned by :meth:`ping` has no - effect. - - Args: - data: Payload of the ping. A string will be encoded to UTF-8. - If ``data`` is :obj:`None`, the payload is four random bytes. - - Returns: - A future that will be completed when the corresponding pong is - received. You can ignore it if you don't intend to wait. The result - of the future is the latency of the connection in seconds. - - :: - - pong_waiter = await ws.ping() - # only if you want to wait for the corresponding pong - latency = await pong_waiter - - Raises: - ConnectionClosed: When the connection is closed. - RuntimeError: If another ping was sent with the same data and - the corresponding pong wasn't received yet. - - """ - await self.ensure_open() - - if data is not None: - data = prepare_ctrl(data) - - # Protect against duplicates if a payload is explicitly set. - if data in self.pings: - raise RuntimeError("already waiting for a pong with the same data") - - # Generate a unique random payload otherwise. - while data is None or data in self.pings: - data = struct.pack("!I", random.getrandbits(32)) - - pong_waiter = self.loop.create_future() - # Resolution of time.monotonic() may be too low on Windows. - ping_timestamp = time.perf_counter() - self.pings[data] = (pong_waiter, ping_timestamp) - - await self.write_frame(True, OP_PING, data) - - return asyncio.shield(pong_waiter) - - async def pong(self, data: Data = b"") -> None: - """ - Send a Pong_. - - .. _Pong: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.3 - - An unsolicited pong may serve as a unidirectional heartbeat. - - Canceling :meth:`pong` is discouraged. If :meth:`pong` doesn't return - immediately, it means the write buffer is full. If you don't want to - wait, you should close the connection. - - Args: - data: Payload of the pong. A string will be encoded to UTF-8. - - Raises: - ConnectionClosed: When the connection is closed. - - """ - await self.ensure_open() - - data = prepare_ctrl(data) - - await self.write_frame(True, OP_PONG, data) - - # Private methods - no guarantees. - - def connection_closed_exc(self) -> ConnectionClosed: - exc: ConnectionClosed - if ( - self.close_rcvd is not None - and self.close_rcvd.code in OK_CLOSE_CODES - and self.close_sent is not None - and self.close_sent.code in OK_CLOSE_CODES - ): - exc = ConnectionClosedOK( - self.close_rcvd, - self.close_sent, - self.close_rcvd_then_sent, - ) - else: - exc = ConnectionClosedError( - self.close_rcvd, - self.close_sent, - self.close_rcvd_then_sent, - ) - # Chain to the exception that terminated data transfer, if any. - exc.__cause__ = self.transfer_data_exc - return exc - - async def ensure_open(self) -> None: - """ - Check that the WebSocket connection is open. - - Raise :exc:`~websockets.exceptions.ConnectionClosed` if it isn't. - - """ - # Handle cases from most common to least common for performance. - if self.state is State.OPEN: - # If self.transfer_data_task exited without a closing handshake, - # self.close_connection_task may be closing the connection, going - # straight from OPEN to CLOSED. - if self.transfer_data_task.done(): - await asyncio.shield(self.close_connection_task) - raise self.connection_closed_exc() - else: - return - - if self.state is State.CLOSED: - raise self.connection_closed_exc() - - if self.state is State.CLOSING: - # If we started the closing handshake, wait for its completion to - # get the proper close code and reason. self.close_connection_task - # will complete within 4 or 5 * close_timeout after close(). The - # CLOSING state also occurs when failing the connection. In that - # case self.close_connection_task will complete even faster. - await asyncio.shield(self.close_connection_task) - raise self.connection_closed_exc() - - # Control may only reach this point in buggy third-party subclasses. - assert self.state is State.CONNECTING - raise InvalidState("WebSocket connection isn't established yet") - - async def transfer_data(self) -> None: - """ - Read incoming messages and put them in a queue. - - This coroutine runs in a task until the closing handshake is started. - - """ - try: - while True: - message = await self.read_message() - - # Exit the loop when receiving a close frame. - if message is None: - break - - # Wait until there's room in the queue (if necessary). - if self.max_queue is not None: - while len(self.messages) >= self.max_queue: - self._put_message_waiter = self.loop.create_future() - try: - await asyncio.shield(self._put_message_waiter) - finally: - self._put_message_waiter = None - - # Put the message in the queue. - self.messages.append(message) - - # Notify recv(). - if self._pop_message_waiter is not None: - self._pop_message_waiter.set_result(None) - self._pop_message_waiter = None - - except asyncio.CancelledError as exc: - self.transfer_data_exc = exc - # If fail_connection() cancels this task, avoid logging the error - # twice and failing the connection again. - raise - - except ProtocolError as exc: - self.transfer_data_exc = exc - self.fail_connection(CloseCode.PROTOCOL_ERROR) - - except (ConnectionError, TimeoutError, EOFError, ssl.SSLError) as exc: - # Reading data with self.reader.readexactly may raise: - # - most subclasses of ConnectionError if the TCP connection - # breaks, is reset, or is aborted; - # - TimeoutError if the TCP connection times out; - # - IncompleteReadError, a subclass of EOFError, if fewer - # bytes are available than requested; - # - ssl.SSLError if the other side infringes the TLS protocol. - self.transfer_data_exc = exc - self.fail_connection(CloseCode.ABNORMAL_CLOSURE) - - except UnicodeDecodeError as exc: - self.transfer_data_exc = exc - self.fail_connection(CloseCode.INVALID_DATA) - - except PayloadTooBig as exc: - self.transfer_data_exc = exc - self.fail_connection(CloseCode.MESSAGE_TOO_BIG) - - except Exception as exc: - # This shouldn't happen often because exceptions expected under - # regular circumstances are handled above. If it does, consider - # catching and handling more exceptions. - self.logger.error("data transfer failed", exc_info=True) - - self.transfer_data_exc = exc - self.fail_connection(CloseCode.INTERNAL_ERROR) - - async def read_message(self) -> Data | None: - """ - Read a single message from the connection. - - Re-assemble data frames if the message is fragmented. - - Return :obj:`None` when the closing handshake is started. - - """ - frame = await self.read_data_frame(max_size=self.max_size) - - # A close frame was received. - if frame is None: - return None - - if frame.opcode == OP_TEXT: - text = True - elif frame.opcode == OP_BINARY: - text = False - else: # frame.opcode == OP_CONT - raise ProtocolError("unexpected opcode") - - # Shortcut for the common case - no fragmentation - if frame.fin: - return frame.data.decode() if text else frame.data - - # 5.4. Fragmentation - fragments: list[Data] = [] - max_size = self.max_size - if text: - decoder_factory = codecs.getincrementaldecoder("utf-8") - decoder = decoder_factory(errors="strict") - if max_size is None: - - def append(frame: Frame) -> None: - nonlocal fragments - fragments.append(decoder.decode(frame.data, frame.fin)) - - else: - - def append(frame: Frame) -> None: - nonlocal fragments, max_size - fragments.append(decoder.decode(frame.data, frame.fin)) - assert isinstance(max_size, int) - max_size -= len(frame.data) - - else: - if max_size is None: - - def append(frame: Frame) -> None: - nonlocal fragments - fragments.append(frame.data) - - else: - - def append(frame: Frame) -> None: - nonlocal fragments, max_size - fragments.append(frame.data) - assert isinstance(max_size, int) - max_size -= len(frame.data) - - append(frame) - - while not frame.fin: - frame = await self.read_data_frame(max_size=max_size) - if frame is None: - raise ProtocolError("incomplete fragmented message") - if frame.opcode != OP_CONT: - raise ProtocolError("unexpected opcode") - append(frame) - - return ("" if text else b"").join(fragments) - - async def read_data_frame(self, max_size: int | None) -> Frame | None: - """ - Read a single data frame from the connection. - - Process control frames received before the next data frame. - - Return :obj:`None` if a close frame is encountered before any data frame. - - """ - # 6.2. Receiving Data - while True: - frame = await self.read_frame(max_size) - - # 5.5. Control Frames - if frame.opcode == OP_CLOSE: - # 7.1.5. The WebSocket Connection Close Code - # 7.1.6. The WebSocket Connection Close Reason - self.close_rcvd = Close.parse(frame.data) - if self.close_sent is not None: - self.close_rcvd_then_sent = False - try: - # Echo the original data instead of re-serializing it with - # Close.serialize() because that fails when the close frame - # is empty and Close.parse() synthesizes a 1005 close code. - await self.write_close_frame(self.close_rcvd, frame.data) - except ConnectionClosed: - # Connection closed before we could echo the close frame. - pass - return None - - elif frame.opcode == OP_PING: - # Answer pings, unless connection is CLOSING. - if self.state is State.OPEN: - try: - await self.pong(frame.data) - except ConnectionClosed: - # Connection closed while draining write buffer. - pass - - elif frame.opcode == OP_PONG: - if frame.data in self.pings: - pong_timestamp = time.perf_counter() - # Sending a pong for only the most recent ping is legal. - # Acknowledge all previous pings too in that case. - ping_id = None - ping_ids = [] - for ping_id, (pong_waiter, ping_timestamp) in self.pings.items(): - ping_ids.append(ping_id) - if not pong_waiter.done(): - pong_waiter.set_result(pong_timestamp - ping_timestamp) - if ping_id == frame.data: - self.latency = pong_timestamp - ping_timestamp - break - else: - raise AssertionError("solicited pong not found in pings") - # Remove acknowledged pings from self.pings. - for ping_id in ping_ids: - del self.pings[ping_id] - - # 5.6. Data Frames - else: - return frame - - async def read_frame(self, max_size: int | None) -> Frame: - """ - Read a single frame from the connection. - - """ - frame = await Frame.read( - self.reader.readexactly, - mask=not self.is_client, - max_size=max_size, - extensions=self.extensions, - ) - if self.debug: - self.logger.debug("< %s", frame) - return frame - - def write_frame_sync(self, fin: bool, opcode: int, data: bytes) -> None: - frame = Frame(fin, Opcode(opcode), data) - if self.debug: - self.logger.debug("> %s", frame) - frame.write( - self.transport.write, - mask=self.is_client, - extensions=self.extensions, - ) - - async def drain(self) -> None: - try: - # drain() cannot be called concurrently by multiple coroutines. - # See https://github.com/python/cpython/issues/74116 for details. - # This workaround can be removed when dropping Python < 3.10. - async with self._drain_lock: - # Handle flow control automatically. - await self._drain() - except ConnectionError: - # Terminate the connection if the socket died. - self.fail_connection() - # Wait until the connection is closed to raise ConnectionClosed - # with the correct code and reason. - await self.ensure_open() - - async def write_frame( - self, fin: bool, opcode: int, data: bytes, *, _state: int = State.OPEN - ) -> None: - # Defensive assertion for protocol compliance. - if self.state is not _state: # pragma: no cover - raise InvalidState( - f"Cannot write to a WebSocket in the {self.state.name} state" - ) - self.write_frame_sync(fin, opcode, data) - await self.drain() - - async def write_close_frame(self, close: Close, data: bytes | None = None) -> None: - """ - Write a close frame if and only if the connection state is OPEN. - - This dedicated coroutine must be used for writing close frames to - ensure that at most one close frame is sent on a given connection. - - """ - # Test and set the connection state before sending the close frame to - # avoid sending two frames in case of concurrent calls. - if self.state is State.OPEN: - # 7.1.3. The WebSocket Closing Handshake is Started - self.state = State.CLOSING - if self.debug: - self.logger.debug("= connection is CLOSING") - - self.close_sent = close - if self.close_rcvd is not None: - self.close_rcvd_then_sent = True - if data is None: - data = close.serialize() - - # 7.1.2. Start the WebSocket Closing Handshake - await self.write_frame(True, OP_CLOSE, data, _state=State.CLOSING) - - async def keepalive_ping(self) -> None: - """ - Send a Ping frame and wait for a Pong frame at regular intervals. - - This coroutine exits when the connection terminates and one of the - following happens: - - - :meth:`ping` raises :exc:`ConnectionClosed`, or - - :meth:`close_connection` cancels :attr:`keepalive_ping_task`. - - """ - if self.ping_interval is None: - return - - try: - while True: - await asyncio.sleep(self.ping_interval) - - self.logger.debug("% sending keepalive ping") - pong_waiter = await self.ping() - - if self.ping_timeout is not None: - try: - async with asyncio_timeout(self.ping_timeout): - # Raises CancelledError if the connection is closed, - # when close_connection() cancels keepalive_ping(). - # Raises ConnectionClosed if the connection is lost, - # when connection_lost() calls abort_pings(). - await pong_waiter - self.logger.debug("% received keepalive pong") - except asyncio.TimeoutError: - if self.debug: - self.logger.debug("- timed out waiting for keepalive pong") - self.fail_connection( - CloseCode.INTERNAL_ERROR, - "keepalive ping timeout", - ) - break - - except ConnectionClosed: - pass - - except Exception: - self.logger.error("keepalive ping failed", exc_info=True) - - async def close_connection(self) -> None: - """ - 7.1.1. Close the WebSocket Connection - - When the opening handshake succeeds, :meth:`connection_open` starts - this coroutine in a task. It waits for the data transfer phase to - complete then it closes the TCP connection cleanly. - - When the opening handshake fails, :meth:`fail_connection` does the - same. There's no data transfer phase in that case. - - """ - try: - # Wait for the data transfer phase to complete. - if hasattr(self, "transfer_data_task"): - try: - await self.transfer_data_task - except asyncio.CancelledError: - pass - - # Cancel the keepalive ping task. - if hasattr(self, "keepalive_ping_task"): - self.keepalive_ping_task.cancel() - - # A client should wait for a TCP close from the server. - if self.is_client and hasattr(self, "transfer_data_task"): - if await self.wait_for_connection_lost(): - return - if self.debug: - self.logger.debug("- timed out waiting for TCP close") - - # Half-close the TCP connection if possible (when there's no TLS). - if self.transport.can_write_eof(): - if self.debug: - self.logger.debug("x half-closing TCP connection") - # write_eof() doesn't document which exceptions it raises. - # "[Errno 107] Transport endpoint is not connected" happens - # but it isn't completely clear under which circumstances. - # uvloop can raise RuntimeError here. - try: - self.transport.write_eof() - except (OSError, RuntimeError): # pragma: no cover - pass - - if await self.wait_for_connection_lost(): - return - if self.debug: - self.logger.debug("- timed out waiting for TCP close") - - finally: - # The try/finally ensures that the transport never remains open, - # even if this coroutine is canceled (for example). - await self.close_transport() - - async def close_transport(self) -> None: - """ - Close the TCP connection. - - """ - # If connection_lost() was called, the TCP connection is closed. - # However, if TLS is enabled, the transport still needs closing. - # Else asyncio complains: ResourceWarning: unclosed transport. - if self.connection_lost_waiter.done() and self.transport.is_closing(): - return - - # Close the TCP connection. Buffers are flushed asynchronously. - if self.debug: - self.logger.debug("x closing TCP connection") - self.transport.close() - - if await self.wait_for_connection_lost(): - return - if self.debug: - self.logger.debug("- timed out waiting for TCP close") - - # Abort the TCP connection. Buffers are discarded. - if self.debug: - self.logger.debug("x aborting TCP connection") - self.transport.abort() - - # connection_lost() is called quickly after aborting. - await self.wait_for_connection_lost() - - async def wait_for_connection_lost(self) -> bool: - """ - Wait until the TCP connection is closed or ``self.close_timeout`` elapses. - - Return :obj:`True` if the connection is closed and :obj:`False` - otherwise. - - """ - if not self.connection_lost_waiter.done(): - try: - async with asyncio_timeout(self.close_timeout): - await asyncio.shield(self.connection_lost_waiter) - except asyncio.TimeoutError: - pass - # Re-check self.connection_lost_waiter.done() synchronously because - # connection_lost() could run between the moment the timeout occurs - # and the moment this coroutine resumes running. - return self.connection_lost_waiter.done() - - def fail_connection( - self, - code: int = CloseCode.ABNORMAL_CLOSURE, - reason: str = "", - ) -> None: - """ - 7.1.7. Fail the WebSocket Connection - - This requires: - - 1. Stopping all processing of incoming data, which means cancelling - :attr:`transfer_data_task`. The close code will be 1006 unless a - close frame was received earlier. - - 2. Sending a close frame with an appropriate code if the opening - handshake succeeded and the other side is likely to process it. - - 3. Closing the connection. :meth:`close_connection` takes care of - this once :attr:`transfer_data_task` exits after being canceled. - - (The specification describes these steps in the opposite order.) - - """ - if self.debug: - self.logger.debug("! failing connection with code %d", code) - - # Cancel transfer_data_task if the opening handshake succeeded. - # cancel() is idempotent and ignored if the task is done already. - if hasattr(self, "transfer_data_task"): - self.transfer_data_task.cancel() - - # Send a close frame when the state is OPEN (a close frame was already - # sent if it's CLOSING), except when failing the connection because of - # an error reading from or writing to the network. - # Don't send a close frame if the connection is broken. - if code != CloseCode.ABNORMAL_CLOSURE and self.state is State.OPEN: - close = Close(code, reason) - - # Write the close frame without draining the write buffer. - - # Keeping fail_connection() synchronous guarantees it can't - # get stuck and simplifies the implementation of the callers. - # Not drainig the write buffer is acceptable in this context. - - # This duplicates a few lines of code from write_close_frame(). - - self.state = State.CLOSING - if self.debug: - self.logger.debug("= connection is CLOSING") - - # If self.close_rcvd was set, the connection state would be - # CLOSING. Therefore self.close_rcvd isn't set and we don't - # have to set self.close_rcvd_then_sent. - assert self.close_rcvd is None - self.close_sent = close - - self.write_frame_sync(True, OP_CLOSE, close.serialize()) - - # Start close_connection_task if the opening handshake didn't succeed. - if not hasattr(self, "close_connection_task"): - self.close_connection_task = self.loop.create_task(self.close_connection()) - - def abort_pings(self) -> None: - """ - Raise ConnectionClosed in pending keepalive pings. - - They'll never receive a pong once the connection is closed. - - """ - assert self.state is State.CLOSED - exc = self.connection_closed_exc() - - for pong_waiter, _ping_timestamp in self.pings.values(): - pong_waiter.set_exception(exc) - # If the exception is never retrieved, it will be logged when ping - # is garbage-collected. This is confusing for users. - # Given that ping is done (with an exception), canceling it does - # nothing, but it prevents logging the exception. - pong_waiter.cancel() - - # asyncio.Protocol methods - - def connection_made(self, transport: asyncio.BaseTransport) -> None: - """ - Configure write buffer limits. - - The high-water limit is defined by ``self.write_limit``. - - The low-water limit currently defaults to ``self.write_limit // 4`` in - :meth:`~asyncio.WriteTransport.set_write_buffer_limits`, which should - be all right for reasonable use cases of this library. - - This is the earliest point where we can get hold of the transport, - which means it's the best point for configuring it. - - """ - transport = cast(asyncio.Transport, transport) - transport.set_write_buffer_limits(self.write_limit) - self.transport = transport - - # Copied from asyncio.StreamReaderProtocol - self.reader.set_transport(transport) - - def connection_lost(self, exc: Exception | None) -> None: - """ - 7.1.4. The WebSocket Connection is Closed. - - """ - self.state = State.CLOSED - self.logger.debug("= connection is CLOSED") - - self.abort_pings() - - # If self.connection_lost_waiter isn't pending, that's a bug, because: - # - it's set only here in connection_lost() which is called only once; - # - it must never be canceled. - self.connection_lost_waiter.set_result(None) - - if True: # pragma: no cover - # Copied from asyncio.StreamReaderProtocol - if self.reader is not None: - if exc is None: - self.reader.feed_eof() - else: - self.reader.set_exception(exc) - - # Copied from asyncio.FlowControlMixin - # Wake up the writer if currently paused. - if not self._paused: - return - waiter = self._drain_waiter - if waiter is None: - return - self._drain_waiter = None - if waiter.done(): - return - if exc is None: - waiter.set_result(None) - else: - waiter.set_exception(exc) - - def pause_writing(self) -> None: # pragma: no cover - assert not self._paused - self._paused = True - - def resume_writing(self) -> None: # pragma: no cover - assert self._paused - self._paused = False - - waiter = self._drain_waiter - if waiter is not None: - self._drain_waiter = None - if not waiter.done(): - waiter.set_result(None) - - def data_received(self, data: bytes) -> None: - self.reader.feed_data(data) - - def eof_received(self) -> None: - """ - Close the transport after receiving EOF. - - The WebSocket protocol has its own closing handshake: endpoints close - the TCP or TLS connection after sending and receiving a close frame. - - As a consequence, they never need to write after receiving EOF, so - there's no reason to keep the transport open by returning :obj:`True`. - - Besides, that doesn't work on TLS connections. - - """ - self.reader.feed_eof() - - -# broadcast() is defined in the protocol module even though it's primarily -# used by servers and documented in the server module because it works with -# client connections too and because it's easier to test together with the -# WebSocketCommonProtocol class. - - -def broadcast( - websockets: Iterable[WebSocketCommonProtocol], - message: Data, - raise_exceptions: bool = False, -) -> None: - """ - Broadcast a message to several WebSocket connections. - - A string (:class:`str`) is sent as a Text_ frame. A bytestring or bytes-like - object (:class:`bytes`, :class:`bytearray`, or :class:`memoryview`) is sent - as a Binary_ frame. - - .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - .. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - :func:`broadcast` pushes the message synchronously to all connections even - if their write buffers are overflowing. There's no backpressure. - - If you broadcast messages faster than a connection can handle them, messages - will pile up in its write buffer until the connection times out. Keep - ``ping_interval`` and ``ping_timeout`` low to prevent excessive memory usage - from slow connections. - - Unlike :meth:`~websockets.legacy.protocol.WebSocketCommonProtocol.send`, - :func:`broadcast` doesn't support sending fragmented messages. Indeed, - fragmentation is useful for sending large messages without buffering them in - memory, while :func:`broadcast` buffers one copy per connection as fast as - possible. - - :func:`broadcast` skips connections that aren't open in order to avoid - errors on connections where the closing handshake is in progress. - - :func:`broadcast` ignores failures to write the message on some connections. - It continues writing to other connections. On Python 3.11 and above, you may - set ``raise_exceptions`` to :obj:`True` to record failures and raise all - exceptions in a :pep:`654` :exc:`ExceptionGroup`. - - While :func:`broadcast` makes more sense for servers, it works identically - with clients, if you have a use case for opening connections to many servers - and broadcasting a message to them. - - Args: - websockets: WebSocket connections to which the message will be sent. - message: Message to send. - raise_exceptions: Whether to raise an exception in case of failures. - - Raises: - TypeError: If ``message`` doesn't have a supported type. - - """ - if not isinstance(message, (str, bytes, bytearray, memoryview)): - raise TypeError("data must be str or bytes-like") - - if raise_exceptions: - if sys.version_info[:2] < (3, 11): # pragma: no cover - raise ValueError("raise_exceptions requires at least Python 3.11") - exceptions = [] - - opcode, data = prepare_data(message) - - for websocket in websockets: - if websocket.state is not State.OPEN: - continue - - if websocket._fragmented_message_waiter is not None: - if raise_exceptions: - exception = RuntimeError("sending a fragmented message") - exceptions.append(exception) - else: - websocket.logger.warning( - "skipped broadcast: sending a fragmented message", - ) - continue - - try: - websocket.write_frame_sync(True, opcode, data) - except Exception as write_exception: - if raise_exceptions: - exception = RuntimeError("failed to write message") - exception.__cause__ = write_exception - exceptions.append(exception) - else: - websocket.logger.warning( - "skipped broadcast: failed to write message: %s", - traceback.format_exception_only( - # Remove first argument when dropping Python 3.9. - type(write_exception), - write_exception, - )[0].strip(), - ) - - if raise_exceptions and exceptions: - raise ExceptionGroup("skipped broadcast", exceptions) - - -# Pretend that broadcast is actually defined in the server module. -broadcast.__module__ = "websockets.legacy.server" diff --git a/wip/SNIP/websockets/legacy/server.py b/wip/SNIP/websockets/legacy/server.py deleted file mode 100644 index f9d57cb..0000000 --- a/wip/SNIP/websockets/legacy/server.py +++ /dev/null @@ -1,1191 +0,0 @@ -from __future__ import annotations - -import asyncio -import email.utils -import functools -import http -import inspect -import logging -import socket -import warnings -from collections.abc import Awaitable, Generator, Iterable, Sequence -from types import TracebackType -from typing import Any, Callable, Union, cast - -from ..asyncio.compatibility import asyncio_timeout -from ..datastructures import Headers, HeadersLike, MultipleValuesError -from ..exceptions import ( - InvalidHandshake, - InvalidHeader, - InvalidMessage, - InvalidOrigin, - InvalidUpgrade, - NegotiationError, -) -from ..extensions import Extension, ServerExtensionFactory -from ..extensions.permessage_deflate import enable_server_permessage_deflate -from ..headers import ( - build_extension, - parse_extension, - parse_subprotocol, - validate_subprotocols, -) -from ..http11 import SERVER -from ..protocol import State -from ..typing import ExtensionHeader, LoggerLike, Origin, StatusLike, Subprotocol -from .exceptions import AbortHandshake -from .handshake import build_response, check_request -from .http import read_request -from .protocol import WebSocketCommonProtocol, broadcast - - -__all__ = [ - "broadcast", - "serve", - "unix_serve", - "WebSocketServerProtocol", - "WebSocketServer", -] - - -# Change to HeadersLike | ... when dropping Python < 3.10. -HeadersLikeOrCallable = Union[HeadersLike, Callable[[str, Headers], HeadersLike]] - -HTTPResponse = tuple[StatusLike, HeadersLike, bytes] - - -class WebSocketServerProtocol(WebSocketCommonProtocol): - """ - WebSocket server connection. - - :class:`WebSocketServerProtocol` provides :meth:`recv` and :meth:`send` - coroutines for receiving and sending messages. - - It supports asynchronous iteration to receive messages:: - - async for message in websocket: - await process(message) - - The iterator exits normally when the connection is closed with close code - 1000 (OK) or 1001 (going away) or without a close code. It raises - a :exc:`~websockets.exceptions.ConnectionClosedError` when the connection - is closed with any other code. - - You may customize the opening handshake in a subclass by - overriding :meth:`process_request` or :meth:`select_subprotocol`. - - Args: - ws_server: WebSocket server that created this connection. - - See :func:`serve` for the documentation of ``ws_handler``, ``logger``, ``origins``, - ``extensions``, ``subprotocols``, ``extra_headers``, and ``server_header``. - - See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the - documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, - ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit``. - - """ - - is_client = False - side = "server" - - def __init__( - self, - # The version that accepts the path in the second argument is deprecated. - ws_handler: ( - Callable[[WebSocketServerProtocol], Awaitable[Any]] - | Callable[[WebSocketServerProtocol, str], Awaitable[Any]] - ), - ws_server: WebSocketServer, - *, - logger: LoggerLike | None = None, - origins: Sequence[Origin | None] | None = None, - extensions: Sequence[ServerExtensionFactory] | None = None, - subprotocols: Sequence[Subprotocol] | None = None, - extra_headers: HeadersLikeOrCallable | None = None, - server_header: str | None = SERVER, - process_request: ( - Callable[[str, Headers], Awaitable[HTTPResponse | None]] | None - ) = None, - select_subprotocol: ( - Callable[[Sequence[Subprotocol], Sequence[Subprotocol]], Subprotocol] | None - ) = None, - open_timeout: float | None = 10, - **kwargs: Any, - ) -> None: - if logger is None: - logger = logging.getLogger("websockets.server") - super().__init__(logger=logger, **kwargs) - # For backwards compatibility with 6.0 or earlier. - if origins is not None and "" in origins: - warnings.warn("use None instead of '' in origins", DeprecationWarning) - origins = [None if origin == "" else origin for origin in origins] - # For backwards compatibility with 10.0 or earlier. Done here in - # addition to serve to trigger the deprecation warning on direct - # use of WebSocketServerProtocol. - self.ws_handler = remove_path_argument(ws_handler) - self.ws_server = ws_server - self.origins = origins - self.available_extensions = extensions - self.available_subprotocols = subprotocols - self.extra_headers = extra_headers - self.server_header = server_header - self._process_request = process_request - self._select_subprotocol = select_subprotocol - self.open_timeout = open_timeout - - def connection_made(self, transport: asyncio.BaseTransport) -> None: - """ - Register connection and initialize a task to handle it. - - """ - super().connection_made(transport) - # Register the connection with the server before creating the handler - # task. Registering at the beginning of the handler coroutine would - # create a race condition between the creation of the task, which - # schedules its execution, and the moment the handler starts running. - self.ws_server.register(self) - self.handler_task = self.loop.create_task(self.handler()) - - async def handler(self) -> None: - """ - Handle the lifecycle of a WebSocket connection. - - Since this method doesn't have a caller able to handle exceptions, it - attempts to log relevant ones and guarantees that the TCP connection is - closed before exiting. - - """ - try: - try: - async with asyncio_timeout(self.open_timeout): - await self.handshake( - origins=self.origins, - available_extensions=self.available_extensions, - available_subprotocols=self.available_subprotocols, - extra_headers=self.extra_headers, - ) - except asyncio.TimeoutError: # pragma: no cover - raise - except ConnectionError: - raise - except Exception as exc: - if isinstance(exc, AbortHandshake): - status, headers, body = exc.status, exc.headers, exc.body - elif isinstance(exc, InvalidOrigin): - if self.debug: - self.logger.debug("! invalid origin", exc_info=True) - status, headers, body = ( - http.HTTPStatus.FORBIDDEN, - Headers(), - f"Failed to open a WebSocket connection: {exc}.\n".encode(), - ) - elif isinstance(exc, InvalidUpgrade): - if self.debug: - self.logger.debug("! invalid upgrade", exc_info=True) - status, headers, body = ( - http.HTTPStatus.UPGRADE_REQUIRED, - Headers([("Upgrade", "websocket")]), - ( - f"Failed to open a WebSocket connection: {exc}.\n" - f"\n" - f"You cannot access a WebSocket server directly " - f"with a browser. You need a WebSocket client.\n" - ).encode(), - ) - elif isinstance(exc, InvalidHandshake): - if self.debug: - self.logger.debug("! invalid handshake", exc_info=True) - exc_chain = cast(BaseException, exc) - exc_str = f"{exc_chain}" - while exc_chain.__cause__ is not None: - exc_chain = exc_chain.__cause__ - exc_str += f"; {exc_chain}" - status, headers, body = ( - http.HTTPStatus.BAD_REQUEST, - Headers(), - f"Failed to open a WebSocket connection: {exc_str}.\n".encode(), - ) - else: - self.logger.error("opening handshake failed", exc_info=True) - status, headers, body = ( - http.HTTPStatus.INTERNAL_SERVER_ERROR, - Headers(), - ( - b"Failed to open a WebSocket connection.\n" - b"See server log for more information.\n" - ), - ) - - headers.setdefault("Date", email.utils.formatdate(usegmt=True)) - if self.server_header: - headers.setdefault("Server", self.server_header) - - headers.setdefault("Content-Length", str(len(body))) - headers.setdefault("Content-Type", "text/plain") - headers.setdefault("Connection", "close") - - self.write_http_response(status, headers, body) - self.logger.info( - "connection rejected (%d %s)", status.value, status.phrase - ) - await self.close_transport() - return - - try: - await self.ws_handler(self) - except Exception: - self.logger.error("connection handler failed", exc_info=True) - if not self.closed: - self.fail_connection(1011) - raise - - try: - await self.close() - except ConnectionError: - raise - except Exception: - self.logger.error("closing handshake failed", exc_info=True) - raise - - except Exception: - # Last-ditch attempt to avoid leaking connections on errors. - try: - self.transport.close() - except Exception: # pragma: no cover - pass - - finally: - # Unregister the connection with the server when the handler task - # terminates. Registration is tied to the lifecycle of the handler - # task because the server waits for tasks attached to registered - # connections before terminating. - self.ws_server.unregister(self) - self.logger.info("connection closed") - - async def read_http_request(self) -> tuple[str, Headers]: - """ - Read request line and headers from the HTTP request. - - If the request contains a body, it may be read from ``self.reader`` - after this coroutine returns. - - Raises: - InvalidMessage: If the HTTP message is malformed or isn't an - HTTP/1.1 GET request. - - """ - try: - path, headers = await read_request(self.reader) - except asyncio.CancelledError: # pragma: no cover - raise - except Exception as exc: - raise InvalidMessage("did not receive a valid HTTP request") from exc - - if self.debug: - self.logger.debug("< GET %s HTTP/1.1", path) - for key, value in headers.raw_items(): - self.logger.debug("< %s: %s", key, value) - - self.path = path - self.request_headers = headers - - return path, headers - - def write_http_response( - self, status: http.HTTPStatus, headers: Headers, body: bytes | None = None - ) -> None: - """ - Write status line and headers to the HTTP response. - - This coroutine is also able to write a response body. - - """ - self.response_headers = headers - - if self.debug: - self.logger.debug("> HTTP/1.1 %d %s", status.value, status.phrase) - for key, value in headers.raw_items(): - self.logger.debug("> %s: %s", key, value) - if body is not None: - self.logger.debug("> [body] (%d bytes)", len(body)) - - # Since the status line and headers only contain ASCII characters, - # we can keep this simple. - response = f"HTTP/1.1 {status.value} {status.phrase}\r\n" - response += str(headers) - - self.transport.write(response.encode()) - - if body is not None: - self.transport.write(body) - - async def process_request( - self, path: str, request_headers: Headers - ) -> HTTPResponse | None: - """ - Intercept the HTTP request and return an HTTP response if appropriate. - - You may override this method in a :class:`WebSocketServerProtocol` - subclass, for example: - - * to return an HTTP 200 OK response on a given path; then a load - balancer can use this path for a health check; - * to authenticate the request and return an HTTP 401 Unauthorized or an - HTTP 403 Forbidden when authentication fails. - - You may also override this method with the ``process_request`` - argument of :func:`serve` and :class:`WebSocketServerProtocol`. This - is equivalent, except ``process_request`` won't have access to the - protocol instance, so it can't store information for later use. - - :meth:`process_request` is expected to complete quickly. If it may run - for a long time, then it should await :meth:`wait_closed` and exit if - :meth:`wait_closed` completes, or else it could prevent the server - from shutting down. - - Args: - path: Request path, including optional query string. - request_headers: Request headers. - - Returns: - tuple[StatusLike, HeadersLike, bytes] | None: :obj:`None` to - continue the WebSocket handshake normally. - - An HTTP response, represented by a 3-uple of the response status, - headers, and body, to abort the WebSocket handshake and return - that HTTP response instead. - - """ - if self._process_request is not None: - response = self._process_request(path, request_headers) - if isinstance(response, Awaitable): - return await response - else: - # For backwards compatibility with 7.0. - warnings.warn( - "declare process_request as a coroutine", DeprecationWarning - ) - return response - return None - - @staticmethod - def process_origin( - headers: Headers, origins: Sequence[Origin | None] | None = None - ) -> Origin | None: - """ - Handle the Origin HTTP request header. - - Args: - headers: Request headers. - origins: Optional list of acceptable origins. - - Raises: - InvalidOrigin: If the origin isn't acceptable. - - """ - # "The user agent MUST NOT include more than one Origin header field" - # per https://datatracker.ietf.org/doc/html/rfc6454#section-7.3. - try: - origin = headers.get("Origin") - except MultipleValuesError as exc: - raise InvalidHeader("Origin", "multiple values") from exc - if origin is not None: - origin = cast(Origin, origin) - if origins is not None: - if origin not in origins: - raise InvalidOrigin(origin) - return origin - - @staticmethod - def process_extensions( - headers: Headers, - available_extensions: Sequence[ServerExtensionFactory] | None, - ) -> tuple[str | None, list[Extension]]: - """ - Handle the Sec-WebSocket-Extensions HTTP request header. - - Accept or reject each extension proposed in the client request. - Negotiate parameters for accepted extensions. - - Return the Sec-WebSocket-Extensions HTTP response header and the list - of accepted extensions. - - :rfc:`6455` leaves the rules up to the specification of each - :extension. - - To provide this level of flexibility, for each extension proposed by - the client, we check for a match with each extension available in the - server configuration. If no match is found, the extension is ignored. - - If several variants of the same extension are proposed by the client, - it may be accepted several times, which won't make sense in general. - Extensions must implement their own requirements. For this purpose, - the list of previously accepted extensions is provided. - - This process doesn't allow the server to reorder extensions. It can - only select a subset of the extensions proposed by the client. - - Other requirements, for example related to mandatory extensions or the - order of extensions, may be implemented by overriding this method. - - Args: - headers: Request headers. - extensions: Optional list of supported extensions. - - Raises: - InvalidHandshake: To abort the handshake with an HTTP 400 error. - - """ - response_header_value: str | None = None - - extension_headers: list[ExtensionHeader] = [] - accepted_extensions: list[Extension] = [] - - header_values = headers.get_all("Sec-WebSocket-Extensions") - - if header_values and available_extensions: - parsed_header_values: list[ExtensionHeader] = sum( - [parse_extension(header_value) for header_value in header_values], [] - ) - - for name, request_params in parsed_header_values: - for ext_factory in available_extensions: - # Skip non-matching extensions based on their name. - if ext_factory.name != name: - continue - - # Skip non-matching extensions based on their params. - try: - response_params, extension = ext_factory.process_request_params( - request_params, accepted_extensions - ) - except NegotiationError: - continue - - # Add matching extension to the final list. - extension_headers.append((name, response_params)) - accepted_extensions.append(extension) - - # Break out of the loop once we have a match. - break - - # If we didn't break from the loop, no extension in our list - # matched what the client sent. The extension is declined. - - # Serialize extension header. - if extension_headers: - response_header_value = build_extension(extension_headers) - - return response_header_value, accepted_extensions - - # Not @staticmethod because it calls self.select_subprotocol() - def process_subprotocol( - self, headers: Headers, available_subprotocols: Sequence[Subprotocol] | None - ) -> Subprotocol | None: - """ - Handle the Sec-WebSocket-Protocol HTTP request header. - - Return Sec-WebSocket-Protocol HTTP response header, which is the same - as the selected subprotocol. - - Args: - headers: Request headers. - available_subprotocols: Optional list of supported subprotocols. - - Raises: - InvalidHandshake: To abort the handshake with an HTTP 400 error. - - """ - subprotocol: Subprotocol | None = None - - header_values = headers.get_all("Sec-WebSocket-Protocol") - - if header_values and available_subprotocols: - parsed_header_values: list[Subprotocol] = sum( - [parse_subprotocol(header_value) for header_value in header_values], [] - ) - - subprotocol = self.select_subprotocol( - parsed_header_values, available_subprotocols - ) - - return subprotocol - - def select_subprotocol( - self, - client_subprotocols: Sequence[Subprotocol], - server_subprotocols: Sequence[Subprotocol], - ) -> Subprotocol | None: - """ - Pick a subprotocol among those supported by the client and the server. - - If several subprotocols are available, select the preferred subprotocol - by giving equal weight to the preferences of the client and the server. - - If no subprotocol is available, proceed without a subprotocol. - - You may provide a ``select_subprotocol`` argument to :func:`serve` or - :class:`WebSocketServerProtocol` to override this logic. For example, - you could reject the handshake if the client doesn't support a - particular subprotocol, rather than accept the handshake without that - subprotocol. - - Args: - client_subprotocols: List of subprotocols offered by the client. - server_subprotocols: List of subprotocols available on the server. - - Returns: - Selected subprotocol, if a common subprotocol was found. - - :obj:`None` to continue without a subprotocol. - - """ - if self._select_subprotocol is not None: - return self._select_subprotocol(client_subprotocols, server_subprotocols) - - subprotocols = set(client_subprotocols) & set(server_subprotocols) - if not subprotocols: - return None - return sorted( - subprotocols, - key=lambda p: client_subprotocols.index(p) + server_subprotocols.index(p), - )[0] - - async def handshake( - self, - origins: Sequence[Origin | None] | None = None, - available_extensions: Sequence[ServerExtensionFactory] | None = None, - available_subprotocols: Sequence[Subprotocol] | None = None, - extra_headers: HeadersLikeOrCallable | None = None, - ) -> str: - """ - Perform the server side of the opening handshake. - - Args: - origins: List of acceptable values of the Origin HTTP header; - include :obj:`None` if the lack of an origin is acceptable. - extensions: List of supported extensions, in order in which they - should be tried. - subprotocols: List of supported subprotocols, in order of - decreasing preference. - extra_headers: Arbitrary HTTP headers to add to the response when - the handshake succeeds. - - Returns: - path of the URI of the request. - - Raises: - InvalidHandshake: If the handshake fails. - - """ - path, request_headers = await self.read_http_request() - - # Hook for customizing request handling, for example checking - # authentication or treating some paths as plain HTTP endpoints. - early_response_awaitable = self.process_request(path, request_headers) - if isinstance(early_response_awaitable, Awaitable): - early_response = await early_response_awaitable - else: - # For backwards compatibility with 7.0. - warnings.warn("declare process_request as a coroutine", DeprecationWarning) - early_response = early_response_awaitable - - # The connection may drop while process_request is running. - if self.state is State.CLOSED: - # This subclass of ConnectionError is silently ignored in handler(). - raise BrokenPipeError("connection closed during opening handshake") - - # Change the response to a 503 error if the server is shutting down. - if not self.ws_server.is_serving(): - early_response = ( - http.HTTPStatus.SERVICE_UNAVAILABLE, - [], - b"Server is shutting down.\n", - ) - - if early_response is not None: - raise AbortHandshake(*early_response) - - key = check_request(request_headers) - - self.origin = self.process_origin(request_headers, origins) - - extensions_header, self.extensions = self.process_extensions( - request_headers, available_extensions - ) - - protocol_header = self.subprotocol = self.process_subprotocol( - request_headers, available_subprotocols - ) - - response_headers = Headers() - - build_response(response_headers, key) - - if extensions_header is not None: - response_headers["Sec-WebSocket-Extensions"] = extensions_header - - if protocol_header is not None: - response_headers["Sec-WebSocket-Protocol"] = protocol_header - - if callable(extra_headers): - extra_headers = extra_headers(path, self.request_headers) - if extra_headers is not None: - response_headers.update(extra_headers) - - response_headers.setdefault("Date", email.utils.formatdate(usegmt=True)) - if self.server_header is not None: - response_headers.setdefault("Server", self.server_header) - - self.write_http_response(http.HTTPStatus.SWITCHING_PROTOCOLS, response_headers) - - self.logger.info("connection open") - - self.connection_open() - - return path - - -class WebSocketServer: - """ - WebSocket server returned by :func:`serve`. - - This class mirrors the API of :class:`~asyncio.Server`. - - It keeps track of WebSocket connections in order to close them properly - when shutting down. - - Args: - logger: Logger for this server. - It defaults to ``logging.getLogger("websockets.server")``. - See the :doc:`logging guide <../../topics/logging>` for details. - - """ - - def __init__(self, logger: LoggerLike | None = None) -> None: - if logger is None: - logger = logging.getLogger("websockets.server") - self.logger = logger - - # Keep track of active connections. - self.websockets: set[WebSocketServerProtocol] = set() - - # Task responsible for closing the server and terminating connections. - self.close_task: asyncio.Task[None] | None = None - - # Completed when the server is closed and connections are terminated. - self.closed_waiter: asyncio.Future[None] - - def wrap(self, server: asyncio.base_events.Server) -> None: - """ - Attach to a given :class:`~asyncio.Server`. - - Since :meth:`~asyncio.loop.create_server` doesn't support injecting a - custom ``Server`` class, the easiest solution that doesn't rely on - private :mod:`asyncio` APIs is to: - - - instantiate a :class:`WebSocketServer` - - give the protocol factory a reference to that instance - - call :meth:`~asyncio.loop.create_server` with the factory - - attach the resulting :class:`~asyncio.Server` with this method - - """ - self.server = server - for sock in server.sockets: - if sock.family == socket.AF_INET: - name = "%s:%d" % sock.getsockname() - elif sock.family == socket.AF_INET6: - name = "[%s]:%d" % sock.getsockname()[:2] - elif sock.family == socket.AF_UNIX: - name = sock.getsockname() - # In the unlikely event that someone runs websockets over a - # protocol other than IP or Unix sockets, avoid crashing. - else: # pragma: no cover - name = str(sock.getsockname()) - self.logger.info("server listening on %s", name) - - # Initialized here because we need a reference to the event loop. - # This should be moved back to __init__ when dropping Python < 3.10. - self.closed_waiter = server.get_loop().create_future() - - def register(self, protocol: WebSocketServerProtocol) -> None: - """ - Register a connection with this server. - - """ - self.websockets.add(protocol) - - def unregister(self, protocol: WebSocketServerProtocol) -> None: - """ - Unregister a connection with this server. - - """ - self.websockets.remove(protocol) - - def close(self, close_connections: bool = True) -> None: - """ - Close the server. - - * Close the underlying :class:`~asyncio.Server`. - * When ``close_connections`` is :obj:`True`, which is the default, - close existing connections. Specifically: - - * Reject opening WebSocket connections with an HTTP 503 (service - unavailable) error. This happens when the server accepted the TCP - connection but didn't complete the opening handshake before closing. - * Close open WebSocket connections with close code 1001 (going away). - - * Wait until all connection handlers terminate. - - :meth:`close` is idempotent. - - """ - if self.close_task is None: - self.close_task = self.get_loop().create_task( - self._close(close_connections) - ) - - async def _close(self, close_connections: bool) -> None: - """ - Implementation of :meth:`close`. - - This calls :meth:`~asyncio.Server.close` on the underlying - :class:`~asyncio.Server` object to stop accepting new connections and - then closes open connections with close code 1001. - - """ - self.logger.info("server closing") - - # Stop accepting new connections. - self.server.close() - - # Wait until all accepted connections reach connection_made() and call - # register(). See https://github.com/python/cpython/issues/79033 for - # details. This workaround can be removed when dropping Python < 3.11. - await asyncio.sleep(0) - - if close_connections: - # Close OPEN connections with close code 1001. After server.close(), - # handshake() closes OPENING connections with an HTTP 503 error. - close_tasks = [ - asyncio.create_task(websocket.close(1001)) - for websocket in self.websockets - if websocket.state is not State.CONNECTING - ] - # asyncio.wait doesn't accept an empty first argument. - if close_tasks: - await asyncio.wait(close_tasks) - - # Wait until all TCP connections are closed. - await self.server.wait_closed() - - # Wait until all connection handlers terminate. - # asyncio.wait doesn't accept an empty first argument. - if self.websockets: - await asyncio.wait( - [websocket.handler_task for websocket in self.websockets] - ) - - # Tell wait_closed() to return. - self.closed_waiter.set_result(None) - - self.logger.info("server closed") - - async def wait_closed(self) -> None: - """ - Wait until the server is closed. - - When :meth:`wait_closed` returns, all TCP connections are closed and - all connection handlers have returned. - - To ensure a fast shutdown, a connection handler should always be - awaiting at least one of: - - * :meth:`~WebSocketServerProtocol.recv`: when the connection is closed, - it raises :exc:`~websockets.exceptions.ConnectionClosedOK`; - * :meth:`~WebSocketServerProtocol.wait_closed`: when the connection is - closed, it returns. - - Then the connection handler is immediately notified of the shutdown; - it can clean up and exit. - - """ - await asyncio.shield(self.closed_waiter) - - def get_loop(self) -> asyncio.AbstractEventLoop: - """ - See :meth:`asyncio.Server.get_loop`. - - """ - return self.server.get_loop() - - def is_serving(self) -> bool: - """ - See :meth:`asyncio.Server.is_serving`. - - """ - return self.server.is_serving() - - async def start_serving(self) -> None: # pragma: no cover - """ - See :meth:`asyncio.Server.start_serving`. - - Typical use:: - - server = await serve(..., start_serving=False) - # perform additional setup here... - # ... then start the server - await server.start_serving() - - """ - await self.server.start_serving() - - async def serve_forever(self) -> None: # pragma: no cover - """ - See :meth:`asyncio.Server.serve_forever`. - - Typical use:: - - server = await serve(...) - # this coroutine doesn't return - # canceling it stops the server - await server.serve_forever() - - This is an alternative to using :func:`serve` as an asynchronous context - manager. Shutdown is triggered by canceling :meth:`serve_forever` - instead of exiting a :func:`serve` context. - - """ - await self.server.serve_forever() - - @property - def sockets(self) -> Iterable[socket.socket]: - """ - See :attr:`asyncio.Server.sockets`. - - """ - return self.server.sockets - - async def __aenter__(self) -> WebSocketServer: # pragma: no cover - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: # pragma: no cover - self.close() - await self.wait_closed() - - -class Serve: - """ - Start a WebSocket server listening on ``host`` and ``port``. - - Whenever a client connects, the server creates a - :class:`WebSocketServerProtocol`, performs the opening handshake, and - delegates to the connection handler, ``ws_handler``. - - The handler receives the :class:`WebSocketServerProtocol` and uses it to - send and receive messages. - - Once the handler completes, either normally or with an exception, the - server performs the closing handshake and closes the connection. - - Awaiting :func:`serve` yields a :class:`WebSocketServer`. This object - provides a :meth:`~WebSocketServer.close` method to shut down the server:: - - # set this future to exit the server - stop = asyncio.get_running_loop().create_future() - - server = await serve(...) - await stop - server.close() - await server.wait_closed() - - :func:`serve` can be used as an asynchronous context manager. Then, the - server is shut down automatically when exiting the context:: - - # set this future to exit the server - stop = asyncio.get_running_loop().create_future() - - async with serve(...): - await stop - - Args: - ws_handler: Connection handler. It receives the WebSocket connection, - which is a :class:`WebSocketServerProtocol`, in argument. - host: Network interfaces the server binds to. - See :meth:`~asyncio.loop.create_server` for details. - port: TCP port the server listens on. - See :meth:`~asyncio.loop.create_server` for details. - create_protocol: Factory for the :class:`asyncio.Protocol` managing - the connection. It defaults to :class:`WebSocketServerProtocol`. - Set it to a wrapper or a subclass to customize connection handling. - logger: Logger for this server. - It defaults to ``logging.getLogger("websockets.server")``. - See the :doc:`logging guide <../../topics/logging>` for details. - compression: The "permessage-deflate" extension is enabled by default. - Set ``compression`` to :obj:`None` to disable it. See the - :doc:`compression guide <../../topics/compression>` for details. - origins: Acceptable values of the ``Origin`` header, for defending - against Cross-Site WebSocket Hijacking attacks. Include :obj:`None` - in the list if the lack of an origin is acceptable. - extensions: List of supported extensions, in order in which they - should be negotiated and run. - subprotocols: List of supported subprotocols, in order of decreasing - preference. - extra_headers (HeadersLike | Callable[[str, Headers] | HeadersLike]): - Arbitrary HTTP headers to add to the response. This can be - a :data:`~websockets.datastructures.HeadersLike` or a callable - taking the request path and headers in arguments and returning - a :data:`~websockets.datastructures.HeadersLike`. - server_header: Value of the ``Server`` response header. - It defaults to ``"Python/x.y.z websockets/X.Y"``. - Setting it to :obj:`None` removes the header. - process_request (Callable[[str, Headers], \ - Awaitable[tuple[StatusLike, HeadersLike, bytes] | None]] | None): - Intercept HTTP request before the opening handshake. - See :meth:`~WebSocketServerProtocol.process_request` for details. - select_subprotocol: Select a subprotocol supported by the client. - See :meth:`~WebSocketServerProtocol.select_subprotocol` for details. - open_timeout: Timeout for opening connections in seconds. - :obj:`None` disables the timeout. - - See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the - documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, - ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit``. - - Any other keyword arguments are passed the event loop's - :meth:`~asyncio.loop.create_server` method. - - For example: - - * You can set ``ssl`` to a :class:`~ssl.SSLContext` to enable TLS. - - * You can set ``sock`` to a :obj:`~socket.socket` that you created - outside of websockets. - - Returns: - WebSocket server. - - """ - - def __init__( - self, - # The version that accepts the path in the second argument is deprecated. - ws_handler: ( - Callable[[WebSocketServerProtocol], Awaitable[Any]] - | Callable[[WebSocketServerProtocol, str], Awaitable[Any]] - ), - host: str | Sequence[str] | None = None, - port: int | None = None, - *, - create_protocol: Callable[..., WebSocketServerProtocol] | None = None, - logger: LoggerLike | None = None, - compression: str | None = "deflate", - origins: Sequence[Origin | None] | None = None, - extensions: Sequence[ServerExtensionFactory] | None = None, - subprotocols: Sequence[Subprotocol] | None = None, - extra_headers: HeadersLikeOrCallable | None = None, - server_header: str | None = SERVER, - process_request: ( - Callable[[str, Headers], Awaitable[HTTPResponse | None]] | None - ) = None, - select_subprotocol: ( - Callable[[Sequence[Subprotocol], Sequence[Subprotocol]], Subprotocol] | None - ) = None, - open_timeout: float | None = 10, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = None, - max_size: int | None = 2**20, - max_queue: int | None = 2**5, - read_limit: int = 2**16, - write_limit: int = 2**16, - **kwargs: Any, - ) -> None: - # Backwards compatibility: close_timeout used to be called timeout. - timeout: float | None = kwargs.pop("timeout", None) - if timeout is None: - timeout = 10 - else: - warnings.warn("rename timeout to close_timeout", DeprecationWarning) - # If both are specified, timeout is ignored. - if close_timeout is None: - close_timeout = timeout - - # Backwards compatibility: create_protocol used to be called klass. - klass: type[WebSocketServerProtocol] | None = kwargs.pop("klass", None) - if klass is None: - klass = WebSocketServerProtocol - else: - warnings.warn("rename klass to create_protocol", DeprecationWarning) - # If both are specified, klass is ignored. - if create_protocol is None: - create_protocol = klass - - # Backwards compatibility: recv() used to return None on closed connections - legacy_recv: bool = kwargs.pop("legacy_recv", False) - - # Backwards compatibility: the loop parameter used to be supported. - _loop: asyncio.AbstractEventLoop | None = kwargs.pop("loop", None) - if _loop is None: - loop = asyncio.get_event_loop() - else: - loop = _loop - warnings.warn("remove loop argument", DeprecationWarning) - - ws_server = WebSocketServer(logger=logger) - - secure = kwargs.get("ssl") is not None - - if compression == "deflate": - extensions = enable_server_permessage_deflate(extensions) - elif compression is not None: - raise ValueError(f"unsupported compression: {compression}") - - if subprotocols is not None: - validate_subprotocols(subprotocols) - - # Help mypy and avoid this error: "type[WebSocketServerProtocol] | - # Callable[..., WebSocketServerProtocol]" not callable [misc] - create_protocol = cast(Callable[..., WebSocketServerProtocol], create_protocol) - factory = functools.partial( - create_protocol, - # For backwards compatibility with 10.0 or earlier. Done here in - # addition to WebSocketServerProtocol to trigger the deprecation - # warning once per serve() call rather than once per connection. - remove_path_argument(ws_handler), - ws_server, - host=host, - port=port, - secure=secure, - open_timeout=open_timeout, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - close_timeout=close_timeout, - max_size=max_size, - max_queue=max_queue, - read_limit=read_limit, - write_limit=write_limit, - loop=_loop, - legacy_recv=legacy_recv, - origins=origins, - extensions=extensions, - subprotocols=subprotocols, - extra_headers=extra_headers, - server_header=server_header, - process_request=process_request, - select_subprotocol=select_subprotocol, - logger=logger, - ) - - if kwargs.pop("unix", False): - path: str | None = kwargs.pop("path", None) - # unix_serve(path) must not specify host and port parameters. - assert host is None and port is None - create_server = functools.partial( - loop.create_unix_server, factory, path, **kwargs - ) - else: - create_server = functools.partial( - loop.create_server, factory, host, port, **kwargs - ) - - # This is a coroutine function. - self._create_server = create_server - self.ws_server = ws_server - - # async with serve(...) - - async def __aenter__(self) -> WebSocketServer: - return await self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - self.ws_server.close() - await self.ws_server.wait_closed() - - # await serve(...) - - def __await__(self) -> Generator[Any, None, WebSocketServer]: - # Create a suitable iterator by calling __await__ on a coroutine. - return self.__await_impl__().__await__() - - async def __await_impl__(self) -> WebSocketServer: - server = await self._create_server() - self.ws_server.wrap(server) - return self.ws_server - - # yield from serve(...) - remove when dropping Python < 3.10 - - __iter__ = __await__ - - -serve = Serve - - -def unix_serve( - # The version that accepts the path in the second argument is deprecated. - ws_handler: ( - Callable[[WebSocketServerProtocol], Awaitable[Any]] - | Callable[[WebSocketServerProtocol, str], Awaitable[Any]] - ), - path: str | None = None, - **kwargs: Any, -) -> Serve: - """ - Start a WebSocket server listening on a Unix socket. - - This function is identical to :func:`serve`, except the ``host`` and - ``port`` arguments are replaced by ``path``. It is only available on Unix. - - Unrecognized keyword arguments are passed the event loop's - :meth:`~asyncio.loop.create_unix_server` method. - - It's useful for deploying a server behind a reverse proxy such as nginx. - - Args: - path: File system path to the Unix socket. - - """ - return serve(ws_handler, path=path, unix=True, **kwargs) - - -def remove_path_argument( - ws_handler: ( - Callable[[WebSocketServerProtocol], Awaitable[Any]] - | Callable[[WebSocketServerProtocol, str], Awaitable[Any]] - ), -) -> Callable[[WebSocketServerProtocol], Awaitable[Any]]: - try: - inspect.signature(ws_handler).bind(None) - except TypeError: - try: - inspect.signature(ws_handler).bind(None, "") - except TypeError: # pragma: no cover - # ws_handler accepts neither one nor two arguments; leave it alone. - pass - else: - # ws_handler accepts two arguments; activate backwards compatibility. - warnings.warn("remove second argument of ws_handler", DeprecationWarning) - - async def _ws_handler(websocket: WebSocketServerProtocol) -> Any: - return await cast( - Callable[[WebSocketServerProtocol, str], Awaitable[Any]], - ws_handler, - )(websocket, websocket.path) - - return _ws_handler - - return cast( - Callable[[WebSocketServerProtocol], Awaitable[Any]], - ws_handler, - ) diff --git a/wip/SNIP/websockets/protocol.py b/wip/SNIP/websockets/protocol.py deleted file mode 100644 index bc64a21..0000000 --- a/wip/SNIP/websockets/protocol.py +++ /dev/null @@ -1,758 +0,0 @@ -from __future__ import annotations - -import enum -import logging -import uuid -from collections.abc import Generator -from typing import Union - -from .exceptions import ( - ConnectionClosed, - ConnectionClosedError, - ConnectionClosedOK, - InvalidState, - PayloadTooBig, - ProtocolError, -) -from .extensions import Extension -from .frames import ( - OK_CLOSE_CODES, - OP_BINARY, - OP_CLOSE, - OP_CONT, - OP_PING, - OP_PONG, - OP_TEXT, - Close, - CloseCode, - Frame, -) -from .http11 import Request, Response -from .streams import StreamReader -from .typing import LoggerLike, Origin, Subprotocol - - -__all__ = [ - "Protocol", - "Side", - "State", - "SEND_EOF", -] - -# Change to Request | Response | Frame when dropping Python < 3.10. -Event = Union[Request, Response, Frame] -"""Events that :meth:`~Protocol.events_received` may return.""" - - -class Side(enum.IntEnum): - """A WebSocket connection is either a server or a client.""" - - SERVER, CLIENT = range(2) - - -SERVER = Side.SERVER -CLIENT = Side.CLIENT - - -class State(enum.IntEnum): - """A WebSocket connection is in one of these four states.""" - - CONNECTING, OPEN, CLOSING, CLOSED = range(4) - - -CONNECTING = State.CONNECTING -OPEN = State.OPEN -CLOSING = State.CLOSING -CLOSED = State.CLOSED - - -SEND_EOF = b"" -"""Sentinel signaling that the TCP connection must be half-closed.""" - - -class Protocol: - """ - Sans-I/O implementation of a WebSocket connection. - - Args: - side: :attr:`~Side.CLIENT` or :attr:`~Side.SERVER`. - state: Initial state of the WebSocket connection. - max_size: Maximum size of incoming messages in bytes; - :obj:`None` disables the limit. - logger: Logger for this connection; depending on ``side``, - defaults to ``logging.getLogger("websockets.client")`` - or ``logging.getLogger("websockets.server")``; - see the :doc:`logging guide <../../topics/logging>` for details. - - """ - - def __init__( - self, - side: Side, - *, - state: State = OPEN, - max_size: int | None = 2**20, - logger: LoggerLike | None = None, - ) -> None: - # Unique identifier. For logs. - self.id: uuid.UUID = uuid.uuid4() - """Unique identifier of the connection. Useful in logs.""" - - # Logger or LoggerAdapter for this connection. - if logger is None: - logger = logging.getLogger(f"websockets.{side.name.lower()}") - self.logger: LoggerLike = logger - """Logger for this connection.""" - - # Track if DEBUG is enabled. Shortcut logging calls if it isn't. - self.debug = logger.isEnabledFor(logging.DEBUG) - - # Connection side. CLIENT or SERVER. - self.side = side - - # Connection state. Initially OPEN because subclasses handle CONNECTING. - self.state = state - - # Maximum size of incoming messages in bytes. - self.max_size = max_size - - # Current size of incoming message in bytes. Only set while reading a - # fragmented message i.e. a data frames with the FIN bit not set. - self.cur_size: int | None = None - - # True while sending a fragmented message i.e. a data frames with the - # FIN bit not set. - self.expect_continuation_frame = False - - # WebSocket protocol parameters. - self.origin: Origin | None = None - self.extensions: list[Extension] = [] - self.subprotocol: Subprotocol | None = None - - # Close code and reason, set when a close frame is sent or received. - self.close_rcvd: Close | None = None - self.close_sent: Close | None = None - self.close_rcvd_then_sent: bool | None = None - - # Track if an exception happened during the handshake. - self.handshake_exc: Exception | None = None - """ - Exception to raise if the opening handshake failed. - - :obj:`None` if the opening handshake succeeded. - - """ - - # Track if send_eof() was called. - self.eof_sent = False - - # Parser state. - self.reader = StreamReader() - self.events: list[Event] = [] - self.writes: list[bytes] = [] - self.parser = self.parse() - next(self.parser) # start coroutine - self.parser_exc: Exception | None = None - - @property - def state(self) -> State: - """ - State of the WebSocket connection. - - Defined in 4.1_, 4.2_, 7.1.3_, and 7.1.4_ of :rfc:`6455`. - - .. _4.1: https://datatracker.ietf.org/doc/html/rfc6455#section-4.1 - .. _4.2: https://datatracker.ietf.org/doc/html/rfc6455#section-4.2 - .. _7.1.3: https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.3 - .. _7.1.4: https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.4 - - """ - return self._state - - @state.setter - def state(self, state: State) -> None: - if self.debug: - self.logger.debug("= connection is %s", state.name) - self._state = state - - @property - def close_code(self) -> int | None: - """ - WebSocket close code received from the remote endpoint. - - Defined in 7.1.5_ of :rfc:`6455`. - - .. _7.1.5: https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5 - - :obj:`None` if the connection isn't closed yet. - - """ - if self.state is not CLOSED: - return None - elif self.close_rcvd is None: - return CloseCode.ABNORMAL_CLOSURE - else: - return self.close_rcvd.code - - @property - def close_reason(self) -> str | None: - """ - WebSocket close reason received from the remote endpoint. - - Defined in 7.1.6_ of :rfc:`6455`. - - .. _7.1.6: https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6 - - :obj:`None` if the connection isn't closed yet. - - """ - if self.state is not CLOSED: - return None - elif self.close_rcvd is None: - return "" - else: - return self.close_rcvd.reason - - @property - def close_exc(self) -> ConnectionClosed: - """ - Exception to raise when trying to interact with a closed connection. - - Don't raise this exception while the connection :attr:`state` - is :attr:`~websockets.protocol.State.CLOSING`; wait until - it's :attr:`~websockets.protocol.State.CLOSED`. - - Indeed, the exception includes the close code and reason, which are - known only once the connection is closed. - - Raises: - AssertionError: If the connection isn't closed yet. - - """ - assert self.state is CLOSED, "connection isn't closed yet" - exc_type: type[ConnectionClosed] - if ( - self.close_rcvd is not None - and self.close_sent is not None - and self.close_rcvd.code in OK_CLOSE_CODES - and self.close_sent.code in OK_CLOSE_CODES - ): - exc_type = ConnectionClosedOK - else: - exc_type = ConnectionClosedError - exc: ConnectionClosed = exc_type( - self.close_rcvd, - self.close_sent, - self.close_rcvd_then_sent, - ) - # Chain to the exception raised in the parser, if any. - exc.__cause__ = self.parser_exc - return exc - - # Public methods for receiving data. - - def receive_data(self, data: bytes) -> None: - """ - Receive data from the network. - - After calling this method: - - - You must call :meth:`data_to_send` and send this data to the network. - - You should call :meth:`events_received` and process resulting events. - - Raises: - EOFError: If :meth:`receive_eof` was called earlier. - - """ - self.reader.feed_data(data) - next(self.parser) - - def receive_eof(self) -> None: - """ - Receive the end of the data stream from the network. - - After calling this method: - - - You must call :meth:`data_to_send` and send this data to the network; - it will return ``[b""]``, signaling the end of the stream, or ``[]``. - - You aren't expected to call :meth:`events_received`; it won't return - any new events. - - :meth:`receive_eof` is idempotent. - - """ - if self.reader.eof: - return - self.reader.feed_eof() - next(self.parser) - - # Public methods for sending events. - - def send_continuation(self, data: bytes, fin: bool) -> None: - """ - Send a `Continuation frame`_. - - .. _Continuation frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - Parameters: - data: payload containing the same kind of data - as the initial frame. - fin: FIN bit; set it to :obj:`True` if this is the last frame - of a fragmented message and to :obj:`False` otherwise. - - Raises: - ProtocolError: If a fragmented message isn't in progress. - - """ - if not self.expect_continuation_frame: - raise ProtocolError("unexpected continuation frame") - if self._state is not OPEN: - raise InvalidState(f"connection is {self.state.name.lower()}") - self.expect_continuation_frame = not fin - self.send_frame(Frame(OP_CONT, data, fin)) - - def send_text(self, data: bytes, fin: bool = True) -> None: - """ - Send a `Text frame`_. - - .. _Text frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - Parameters: - data: payload containing text encoded with UTF-8. - fin: FIN bit; set it to :obj:`False` if this is the first frame of - a fragmented message. - - Raises: - ProtocolError: If a fragmented message is in progress. - - """ - if self.expect_continuation_frame: - raise ProtocolError("expected a continuation frame") - if self._state is not OPEN: - raise InvalidState(f"connection is {self.state.name.lower()}") - self.expect_continuation_frame = not fin - self.send_frame(Frame(OP_TEXT, data, fin)) - - def send_binary(self, data: bytes, fin: bool = True) -> None: - """ - Send a `Binary frame`_. - - .. _Binary frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - Parameters: - data: payload containing arbitrary binary data. - fin: FIN bit; set it to :obj:`False` if this is the first frame of - a fragmented message. - - Raises: - ProtocolError: If a fragmented message is in progress. - - """ - if self.expect_continuation_frame: - raise ProtocolError("expected a continuation frame") - if self._state is not OPEN: - raise InvalidState(f"connection is {self.state.name.lower()}") - self.expect_continuation_frame = not fin - self.send_frame(Frame(OP_BINARY, data, fin)) - - def send_close(self, code: int | None = None, reason: str = "") -> None: - """ - Send a `Close frame`_. - - .. _Close frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.1 - - Parameters: - code: close code. - reason: close reason. - - Raises: - ProtocolError: If the code isn't valid or if a reason is provided - without a code. - - """ - # While RFC 6455 doesn't rule out sending more than one close Frame, - # websockets is conservative in what it sends and doesn't allow that. - if self._state is not OPEN: - raise InvalidState(f"connection is {self.state.name.lower()}") - if code is None: - if reason != "": - raise ProtocolError("cannot send a reason without a code") - close = Close(CloseCode.NO_STATUS_RCVD, "") - data = b"" - else: - close = Close(code, reason) - data = close.serialize() - # 7.1.3. The WebSocket Closing Handshake is Started - self.send_frame(Frame(OP_CLOSE, data)) - # Since the state is OPEN, no close frame was received yet. - # As a consequence, self.close_rcvd_then_sent remains None. - assert self.close_rcvd is None - self.close_sent = close - self.state = CLOSING - - def send_ping(self, data: bytes) -> None: - """ - Send a `Ping frame`_. - - .. _Ping frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2 - - Parameters: - data: payload containing arbitrary binary data. - - """ - # RFC 6455 allows control frames after starting the closing handshake. - if self._state is not OPEN and self._state is not CLOSING: - raise InvalidState(f"connection is {self.state.name.lower()}") - self.send_frame(Frame(OP_PING, data)) - - def send_pong(self, data: bytes) -> None: - """ - Send a `Pong frame`_. - - .. _Pong frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.3 - - Parameters: - data: payload containing arbitrary binary data. - - """ - # RFC 6455 allows control frames after starting the closing handshake. - if self._state is not OPEN and self._state is not CLOSING: - raise InvalidState(f"connection is {self.state.name.lower()}") - self.send_frame(Frame(OP_PONG, data)) - - def fail(self, code: int, reason: str = "") -> None: - """ - `Fail the WebSocket connection`_. - - .. _Fail the WebSocket connection: - https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.7 - - Parameters: - code: close code - reason: close reason - - Raises: - ProtocolError: If the code isn't valid. - """ - # 7.1.7. Fail the WebSocket Connection - - # Send a close frame when the state is OPEN (a close frame was already - # sent if it's CLOSING), except when failing the connection because - # of an error reading from or writing to the network. - if self.state is OPEN: - if code != CloseCode.ABNORMAL_CLOSURE: - close = Close(code, reason) - data = close.serialize() - self.send_frame(Frame(OP_CLOSE, data)) - self.close_sent = close - # If recv_messages() raised an exception upon receiving a close - # frame but before echoing it, then close_rcvd is not None even - # though the state is OPEN. This happens when the connection is - # closed while receiving a fragmented message. - if self.close_rcvd is not None: - self.close_rcvd_then_sent = True - self.state = CLOSING - - # When failing the connection, a server closes the TCP connection - # without waiting for the client to complete the handshake, while a - # client waits for the server to close the TCP connection, possibly - # after sending a close frame that the client will ignore. - if self.side is SERVER and not self.eof_sent: - self.send_eof() - - # 7.1.7. Fail the WebSocket Connection "An endpoint MUST NOT continue - # to attempt to process data(including a responding Close frame) from - # the remote endpoint after being instructed to _Fail the WebSocket - # Connection_." - self.parser = self.discard() - next(self.parser) # start coroutine - - # Public method for getting incoming events after receiving data. - - def events_received(self) -> list[Event]: - """ - Fetch events generated from data received from the network. - - Call this method immediately after any of the ``receive_*()`` methods. - - Process resulting events, likely by passing them to the application. - - Returns: - Events read from the connection. - """ - events, self.events = self.events, [] - return events - - # Public method for getting outgoing data after receiving data or sending events. - - def data_to_send(self) -> list[bytes]: - """ - Obtain data to send to the network. - - Call this method immediately after any of the ``receive_*()``, - ``send_*()``, or :meth:`fail` methods. - - Write resulting data to the connection. - - The empty bytestring :data:`~websockets.protocol.SEND_EOF` signals - the end of the data stream. When you receive it, half-close the TCP - connection. - - Returns: - Data to write to the connection. - - """ - writes, self.writes = self.writes, [] - return writes - - def close_expected(self) -> bool: - """ - Tell if the TCP connection is expected to close soon. - - Call this method immediately after any of the ``receive_*()``, - ``send_close()``, or :meth:`fail` methods. - - If it returns :obj:`True`, schedule closing the TCP connection after a - short timeout if the other side hasn't already closed it. - - Returns: - Whether the TCP connection is expected to close soon. - - """ - # During the opening handshake, when our state is CONNECTING, we expect - # a TCP close if and only if the hansdake fails. When it does, we start - # the TCP closing handshake by sending EOF with send_eof(). - - # Once the opening handshake completes successfully, we expect a TCP - # close if and only if we sent a close frame, meaning that our state - # progressed to CLOSING: - - # * Normal closure: once we send a close frame, we expect a TCP close: - # server waits for client to complete the TCP closing handshake; - # client waits for server to initiate the TCP closing handshake. - - # * Abnormal closure: we always send a close frame and the same logic - # applies, except on EOFError where we don't send a close frame - # because we already received the TCP close, so we don't expect it. - - # If our state is CLOSED, we already received a TCP close so we don't - # expect it anymore. - - # Micro-optimization: put the most common case first - if self.state is OPEN: - return False - if self.state is CLOSING: - return True - if self.state is CLOSED: - return False - assert self.state is CONNECTING - return self.eof_sent - - # Private methods for receiving data. - - def parse(self) -> Generator[None]: - """ - Parse incoming data into frames. - - :meth:`receive_data` and :meth:`receive_eof` run this generator - coroutine until it needs more data or reaches EOF. - - :meth:`parse` never raises an exception. Instead, it sets the - :attr:`parser_exc` and yields control. - - """ - try: - while True: - if (yield from self.reader.at_eof()): - if self.debug: - self.logger.debug("< EOF") - # If the WebSocket connection is closed cleanly, with a - # closing handhshake, recv_frame() substitutes parse() - # with discard(). This branch is reached only when the - # connection isn't closed cleanly. - raise EOFError("unexpected end of stream") - - if self.max_size is None: - max_size = None - elif self.cur_size is None: - max_size = self.max_size - else: - max_size = self.max_size - self.cur_size - - # During a normal closure, execution ends here on the next - # iteration of the loop after receiving a close frame. At - # this point, recv_frame() replaced parse() by discard(). - frame = yield from Frame.parse( - self.reader.read_exact, - mask=self.side is SERVER, - max_size=max_size, - extensions=self.extensions, - ) - - if self.debug: - self.logger.debug("< %s", frame) - - self.recv_frame(frame) - - except ProtocolError as exc: - self.fail(CloseCode.PROTOCOL_ERROR, str(exc)) - self.parser_exc = exc - - except EOFError as exc: - self.fail(CloseCode.ABNORMAL_CLOSURE, str(exc)) - self.parser_exc = exc - - except UnicodeDecodeError as exc: - self.fail(CloseCode.INVALID_DATA, f"{exc.reason} at position {exc.start}") - self.parser_exc = exc - - except PayloadTooBig as exc: - exc.set_current_size(self.cur_size) - self.fail(CloseCode.MESSAGE_TOO_BIG, str(exc)) - self.parser_exc = exc - - except Exception as exc: - self.logger.error("parser failed", exc_info=True) - # Don't include exception details, which may be security-sensitive. - self.fail(CloseCode.INTERNAL_ERROR) - self.parser_exc = exc - - # During an abnormal closure, execution ends here after catching an - # exception. At this point, fail() replaced parse() by discard(). - yield - raise AssertionError("parse() shouldn't step after error") - - def discard(self) -> Generator[None]: - """ - Discard incoming data. - - This coroutine replaces :meth:`parse`: - - - after receiving a close frame, during a normal closure (1.4); - - after sending a close frame, during an abnormal closure (7.1.7). - - """ - # After the opening handshake completes, the server closes the TCP - # connection in the same circumstances where discard() replaces parse(). - # The client closes it when it receives EOF from the server or times - # out. (The latter case cannot be handled in this Sans-I/O layer.) - assert (self.side is SERVER or self.state is CONNECTING) == (self.eof_sent) - while not (yield from self.reader.at_eof()): - self.reader.discard() - if self.debug: - self.logger.debug("< EOF") - # A server closes the TCP connection immediately, while a client - # waits for the server to close the TCP connection. - if self.side is CLIENT and self.state is not CONNECTING: - self.send_eof() - self.state = CLOSED - # If discard() completes normally, execution ends here. - yield - # Once the reader reaches EOF, its feed_data/eof() methods raise an - # error, so our receive_data/eof() methods don't step the generator. - raise AssertionError("discard() shouldn't step after EOF") - - def recv_frame(self, frame: Frame) -> None: - """ - Process an incoming frame. - - """ - if frame.opcode is OP_TEXT or frame.opcode is OP_BINARY: - if self.cur_size is not None: - raise ProtocolError("expected a continuation frame") - if not frame.fin: - self.cur_size = len(frame.data) - - elif frame.opcode is OP_CONT: - if self.cur_size is None: - raise ProtocolError("unexpected continuation frame") - if frame.fin: - self.cur_size = None - else: - self.cur_size += len(frame.data) - - elif frame.opcode is OP_PING: - # 5.5.2. Ping: "Upon receipt of a Ping frame, an endpoint MUST - # send a Pong frame in response" - pong_frame = Frame(OP_PONG, frame.data) - self.send_frame(pong_frame) - - elif frame.opcode is OP_PONG: - # 5.5.3 Pong: "A response to an unsolicited Pong frame is not - # expected." - pass - - elif frame.opcode is OP_CLOSE: - # 7.1.5. The WebSocket Connection Close Code - # 7.1.6. The WebSocket Connection Close Reason - self.close_rcvd = Close.parse(frame.data) - if self.state is CLOSING: - assert self.close_sent is not None - self.close_rcvd_then_sent = False - - if self.cur_size is not None: - raise ProtocolError("incomplete fragmented message") - - # 5.5.1 Close: "If an endpoint receives a Close frame and did - # not previously send a Close frame, the endpoint MUST send a - # Close frame in response. (When sending a Close frame in - # response, the endpoint typically echos the status code it - # received.)" - - if self.state is OPEN: - # Echo the original data instead of re-serializing it with - # Close.serialize() because that fails when the close frame - # is empty and Close.parse() synthesizes a 1005 close code. - # The rest is identical to send_close(). - self.send_frame(Frame(OP_CLOSE, frame.data)) - self.close_sent = self.close_rcvd - self.close_rcvd_then_sent = True - self.state = CLOSING - - # 7.1.2. Start the WebSocket Closing Handshake: "Once an - # endpoint has both sent and received a Close control frame, - # that endpoint SHOULD _Close the WebSocket Connection_" - - # A server closes the TCP connection immediately, while a client - # waits for the server to close the TCP connection. - if self.side is SERVER: - self.send_eof() - - # 1.4. Closing Handshake: "after receiving a control frame - # indicating the connection should be closed, a peer discards - # any further data received." - # RFC 6455 allows reading Ping and Pong frames after a Close frame. - # However, that doesn't seem useful; websockets doesn't support it. - self.parser = self.discard() - next(self.parser) # start coroutine - - else: - # This can't happen because Frame.parse() validates opcodes. - raise AssertionError(f"unexpected opcode: {frame.opcode:02x}") - - self.events.append(frame) - - # Private methods for sending events. - - def send_frame(self, frame: Frame) -> None: - if self.debug: - self.logger.debug("> %s", frame) - self.writes.append( - frame.serialize( - mask=self.side is CLIENT, - extensions=self.extensions, - ) - ) - - def send_eof(self) -> None: - assert not self.eof_sent - self.eof_sent = True - if self.debug: - self.logger.debug("> EOF") - self.writes.append(SEND_EOF) diff --git a/wip/SNIP/websockets/py.typed b/wip/SNIP/websockets/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/wip/SNIP/websockets/server.py b/wip/SNIP/websockets/server.py deleted file mode 100644 index 1744412..0000000 --- a/wip/SNIP/websockets/server.py +++ /dev/null @@ -1,587 +0,0 @@ -from __future__ import annotations - -import base64 -import binascii -import email.utils -import http -import re -import warnings -from collections.abc import Generator, Sequence -from typing import Any, Callable, cast - -from .datastructures import Headers, MultipleValuesError -from .exceptions import ( - InvalidHandshake, - InvalidHeader, - InvalidHeaderValue, - InvalidMessage, - InvalidOrigin, - InvalidUpgrade, - NegotiationError, -) -from .extensions import Extension, ServerExtensionFactory -from .headers import ( - build_extension, - parse_connection, - parse_extension, - parse_subprotocol, - parse_upgrade, -) -from .http11 import Request, Response -from .imports import lazy_import -from .protocol import CONNECTING, OPEN, SERVER, Protocol, State -from .typing import ( - ConnectionOption, - ExtensionHeader, - LoggerLike, - Origin, - StatusLike, - Subprotocol, - UpgradeProtocol, -) -from .utils import accept_key - - -__all__ = ["ServerProtocol"] - - -class ServerProtocol(Protocol): - """ - Sans-I/O implementation of a WebSocket server connection. - - Args: - origins: Acceptable values of the ``Origin`` header. Values can be - :class:`str` to test for an exact match or regular expressions - compiled by :func:`re.compile` to test against a pattern. Include - :obj:`None` in the list if the lack of an origin is acceptable. - This is useful for defending against Cross-Site WebSocket - Hijacking attacks. - extensions: List of supported extensions, in order in which they - should be tried. - subprotocols: List of supported subprotocols, in order of decreasing - preference. - select_subprotocol: Callback for selecting a subprotocol among - those supported by the client and the server. It has the same - signature as the :meth:`select_subprotocol` method, including a - :class:`ServerProtocol` instance as first argument. - state: Initial state of the WebSocket connection. - max_size: Maximum size of incoming messages in bytes; - :obj:`None` disables the limit. - logger: Logger for this connection; - defaults to ``logging.getLogger("websockets.server")``; - see the :doc:`logging guide <../../topics/logging>` for details. - - """ - - def __init__( - self, - *, - origins: Sequence[Origin | re.Pattern[str] | None] | None = None, - extensions: Sequence[ServerExtensionFactory] | None = None, - subprotocols: Sequence[Subprotocol] | None = None, - select_subprotocol: ( - Callable[ - [ServerProtocol, Sequence[Subprotocol]], - Subprotocol | None, - ] - | None - ) = None, - state: State = CONNECTING, - max_size: int | None = 2**20, - logger: LoggerLike | None = None, - ) -> None: - super().__init__( - side=SERVER, - state=state, - max_size=max_size, - logger=logger, - ) - self.origins = origins - self.available_extensions = extensions - self.available_subprotocols = subprotocols - if select_subprotocol is not None: - # Bind select_subprotocol then shadow self.select_subprotocol. - # Use setattr to work around https://github.com/python/mypy/issues/2427. - setattr( - self, - "select_subprotocol", - select_subprotocol.__get__(self, self.__class__), - ) - - def accept(self, request: Request) -> Response: - """ - Create a handshake response to accept the connection. - - If the handshake request is valid and the handshake successful, - :meth:`accept` returns an HTTP response with status code 101. - - Else, it returns an HTTP response with another status code. This rejects - the connection, like :meth:`reject` would. - - You must send the handshake response with :meth:`send_response`. - - You may modify the response before sending it, typically by adding HTTP - headers. - - Args: - request: WebSocket handshake request received from the client. - - Returns: - WebSocket handshake response or HTTP response to send to the client. - - """ - try: - ( - accept_header, - extensions_header, - protocol_header, - ) = self.process_request(request) - except InvalidOrigin as exc: - request._exception = exc - self.handshake_exc = exc - if self.debug: - self.logger.debug("! invalid origin", exc_info=True) - return self.reject( - http.HTTPStatus.FORBIDDEN, - f"Failed to open a WebSocket connection: {exc}.\n", - ) - except InvalidUpgrade as exc: - request._exception = exc - self.handshake_exc = exc - if self.debug: - self.logger.debug("! invalid upgrade", exc_info=True) - response = self.reject( - http.HTTPStatus.UPGRADE_REQUIRED, - ( - f"Failed to open a WebSocket connection: {exc}.\n" - f"\n" - f"You cannot access a WebSocket server directly " - f"with a browser. You need a WebSocket client.\n" - ), - ) - response.headers["Upgrade"] = "websocket" - return response - except InvalidHandshake as exc: - request._exception = exc - self.handshake_exc = exc - if self.debug: - self.logger.debug("! invalid handshake", exc_info=True) - exc_chain = cast(BaseException, exc) - exc_str = f"{exc_chain}" - while exc_chain.__cause__ is not None: - exc_chain = exc_chain.__cause__ - exc_str += f"; {exc_chain}" - return self.reject( - http.HTTPStatus.BAD_REQUEST, - f"Failed to open a WebSocket connection: {exc_str}.\n", - ) - except Exception as exc: - # Handle exceptions raised by user-provided select_subprotocol and - # unexpected errors. - request._exception = exc - self.handshake_exc = exc - self.logger.error("opening handshake failed", exc_info=True) - return self.reject( - http.HTTPStatus.INTERNAL_SERVER_ERROR, - ( - "Failed to open a WebSocket connection.\n" - "See server log for more information.\n" - ), - ) - - headers = Headers() - headers["Date"] = email.utils.formatdate(usegmt=True) - headers["Upgrade"] = "websocket" - headers["Connection"] = "Upgrade" - headers["Sec-WebSocket-Accept"] = accept_header - if extensions_header is not None: - headers["Sec-WebSocket-Extensions"] = extensions_header - if protocol_header is not None: - headers["Sec-WebSocket-Protocol"] = protocol_header - return Response(101, "Switching Protocols", headers) - - def process_request( - self, - request: Request, - ) -> tuple[str, str | None, str | None]: - """ - Check a handshake request and negotiate extensions and subprotocol. - - This function doesn't verify that the request is an HTTP/1.1 or higher - GET request and doesn't check the ``Host`` header. These controls are - usually performed earlier in the HTTP request handling code. They're - the responsibility of the caller. - - Args: - request: WebSocket handshake request received from the client. - - Returns: - ``Sec-WebSocket-Accept``, ``Sec-WebSocket-Extensions``, and - ``Sec-WebSocket-Protocol`` headers for the handshake response. - - Raises: - InvalidHandshake: If the handshake request is invalid; - then the server must return 400 Bad Request error. - - """ - headers = request.headers - - connection: list[ConnectionOption] = sum( - [parse_connection(value) for value in headers.get_all("Connection")], [] - ) - if not any(value.lower() == "upgrade" for value in connection): - raise InvalidUpgrade( - "Connection", ", ".join(connection) if connection else None - ) - - upgrade: list[UpgradeProtocol] = sum( - [parse_upgrade(value) for value in headers.get_all("Upgrade")], [] - ) - # For compatibility with non-strict implementations, ignore case when - # checking the Upgrade header. The RFC always uses "websocket", except - # in section 11.2. (IANA registration) where it uses "WebSocket". - if not (len(upgrade) == 1 and upgrade[0].lower() == "websocket"): - raise InvalidUpgrade("Upgrade", ", ".join(upgrade) if upgrade else None) - - try: - key = headers["Sec-WebSocket-Key"] - except KeyError: - raise InvalidHeader("Sec-WebSocket-Key") from None - except MultipleValuesError: - raise InvalidHeader("Sec-WebSocket-Key", "multiple values") from None - try: - raw_key = base64.b64decode(key.encode(), validate=True) - except binascii.Error as exc: - raise InvalidHeaderValue("Sec-WebSocket-Key", key) from exc - if len(raw_key) != 16: - raise InvalidHeaderValue("Sec-WebSocket-Key", key) - accept_header = accept_key(key) - - try: - version = headers["Sec-WebSocket-Version"] - except KeyError: - raise InvalidHeader("Sec-WebSocket-Version") from None - except MultipleValuesError: - raise InvalidHeader("Sec-WebSocket-Version", "multiple values") from None - if version != "13": - raise InvalidHeaderValue("Sec-WebSocket-Version", version) - - self.origin = self.process_origin(headers) - extensions_header, self.extensions = self.process_extensions(headers) - protocol_header = self.subprotocol = self.process_subprotocol(headers) - - return (accept_header, extensions_header, protocol_header) - - def process_origin(self, headers: Headers) -> Origin | None: - """ - Handle the Origin HTTP request header. - - Args: - headers: WebSocket handshake request headers. - - Returns: - origin, if it is acceptable. - - Raises: - InvalidHandshake: If the Origin header is invalid. - InvalidOrigin: If the origin isn't acceptable. - - """ - # "The user agent MUST NOT include more than one Origin header field" - # per https://datatracker.ietf.org/doc/html/rfc6454#section-7.3. - try: - origin = headers.get("Origin") - except MultipleValuesError: - raise InvalidHeader("Origin", "multiple values") from None - if origin is not None: - origin = cast(Origin, origin) - if self.origins is not None: - for origin_or_regex in self.origins: - if origin_or_regex == origin or ( - isinstance(origin_or_regex, re.Pattern) - and origin is not None - and origin_or_regex.fullmatch(origin) is not None - ): - break - else: - raise InvalidOrigin(origin) - return origin - - def process_extensions( - self, - headers: Headers, - ) -> tuple[str | None, list[Extension]]: - """ - Handle the Sec-WebSocket-Extensions HTTP request header. - - Accept or reject each extension proposed in the client request. - Negotiate parameters for accepted extensions. - - Per :rfc:`6455`, negotiation rules are defined by the specification of - each extension. - - To provide this level of flexibility, for each extension proposed by - the client, we check for a match with each extension available in the - server configuration. If no match is found, the extension is ignored. - - If several variants of the same extension are proposed by the client, - it may be accepted several times, which won't make sense in general. - Extensions must implement their own requirements. For this purpose, - the list of previously accepted extensions is provided. - - This process doesn't allow the server to reorder extensions. It can - only select a subset of the extensions proposed by the client. - - Other requirements, for example related to mandatory extensions or the - order of extensions, may be implemented by overriding this method. - - Args: - headers: WebSocket handshake request headers. - - Returns: - ``Sec-WebSocket-Extensions`` HTTP response header and list of - accepted extensions. - - Raises: - InvalidHandshake: If the Sec-WebSocket-Extensions header is invalid. - - """ - response_header_value: str | None = None - - extension_headers: list[ExtensionHeader] = [] - accepted_extensions: list[Extension] = [] - - header_values = headers.get_all("Sec-WebSocket-Extensions") - - if header_values and self.available_extensions: - parsed_header_values: list[ExtensionHeader] = sum( - [parse_extension(header_value) for header_value in header_values], [] - ) - - for name, request_params in parsed_header_values: - for ext_factory in self.available_extensions: - # Skip non-matching extensions based on their name. - if ext_factory.name != name: - continue - - # Skip non-matching extensions based on their params. - try: - response_params, extension = ext_factory.process_request_params( - request_params, accepted_extensions - ) - except NegotiationError: - continue - - # Add matching extension to the final list. - extension_headers.append((name, response_params)) - accepted_extensions.append(extension) - - # Break out of the loop once we have a match. - break - - # If we didn't break from the loop, no extension in our list - # matched what the client sent. The extension is declined. - - # Serialize extension header. - if extension_headers: - response_header_value = build_extension(extension_headers) - - return response_header_value, accepted_extensions - - def process_subprotocol(self, headers: Headers) -> Subprotocol | None: - """ - Handle the Sec-WebSocket-Protocol HTTP request header. - - Args: - headers: WebSocket handshake request headers. - - Returns: - Subprotocol, if one was selected; this is also the value of the - ``Sec-WebSocket-Protocol`` response header. - - Raises: - InvalidHandshake: If the Sec-WebSocket-Subprotocol header is invalid. - - """ - subprotocols: Sequence[Subprotocol] = sum( - [ - parse_subprotocol(header_value) - for header_value in headers.get_all("Sec-WebSocket-Protocol") - ], - [], - ) - return self.select_subprotocol(subprotocols) - - def select_subprotocol( - self, - subprotocols: Sequence[Subprotocol], - ) -> Subprotocol | None: - """ - Pick a subprotocol among those offered by the client. - - If several subprotocols are supported by both the client and the server, - pick the first one in the list declared the server. - - If the server doesn't support any subprotocols, continue without a - subprotocol, regardless of what the client offers. - - If the server supports at least one subprotocol and the client doesn't - offer any, abort the handshake with an HTTP 400 error. - - You provide a ``select_subprotocol`` argument to :class:`ServerProtocol` - to override this logic. For example, you could accept the connection - even if client doesn't offer a subprotocol, rather than reject it. - - Here's how to negotiate the ``chat`` subprotocol if the client supports - it and continue without a subprotocol otherwise:: - - def select_subprotocol(protocol, subprotocols): - if "chat" in subprotocols: - return "chat" - - Args: - subprotocols: List of subprotocols offered by the client. - - Returns: - Selected subprotocol, if a common subprotocol was found. - - :obj:`None` to continue without a subprotocol. - - Raises: - NegotiationError: Custom implementations may raise this exception - to abort the handshake with an HTTP 400 error. - - """ - # Server doesn't offer any subprotocols. - if not self.available_subprotocols: # None or empty list - return None - - # Server offers at least one subprotocol but client doesn't offer any. - if not subprotocols: - raise NegotiationError("missing subprotocol") - - # Server and client both offer subprotocols. Look for a shared one. - proposed_subprotocols = set(subprotocols) - for subprotocol in self.available_subprotocols: - if subprotocol in proposed_subprotocols: - return subprotocol - - # No common subprotocol was found. - raise NegotiationError( - "invalid subprotocol; expected one of " - + ", ".join(self.available_subprotocols) - ) - - def reject(self, status: StatusLike, text: str) -> Response: - """ - Create a handshake response to reject the connection. - - A short plain text response is the best fallback when failing to - establish a WebSocket connection. - - You must send the handshake response with :meth:`send_response`. - - You may modify the response before sending it, for example by changing - HTTP headers. - - Args: - status: HTTP status code. - text: HTTP response body; it will be encoded to UTF-8. - - Returns: - HTTP response to send to the client. - - """ - # If status is an int instead of an HTTPStatus, fix it automatically. - status = http.HTTPStatus(status) - body = text.encode() - headers = Headers( - [ - ("Date", email.utils.formatdate(usegmt=True)), - ("Connection", "close"), - ("Content-Length", str(len(body))), - ("Content-Type", "text/plain; charset=utf-8"), - ] - ) - return Response(status.value, status.phrase, headers, body) - - def send_response(self, response: Response) -> None: - """ - Send a handshake response to the client. - - Args: - response: WebSocket handshake response event to send. - - """ - if self.debug: - code, phrase = response.status_code, response.reason_phrase - self.logger.debug("> HTTP/1.1 %d %s", code, phrase) - for key, value in response.headers.raw_items(): - self.logger.debug("> %s: %s", key, value) - if response.body: - self.logger.debug("> [body] (%d bytes)", len(response.body)) - - self.writes.append(response.serialize()) - - if response.status_code == 101: - assert self.state is CONNECTING - self.state = OPEN - self.logger.info("connection open") - - else: - self.logger.info( - "connection rejected (%d %s)", - response.status_code, - response.reason_phrase, - ) - - self.send_eof() - self.parser = self.discard() - next(self.parser) # start coroutine - - def parse(self) -> Generator[None]: - if self.state is CONNECTING: - try: - request = yield from Request.parse( - self.reader.read_line, - ) - except Exception as exc: - self.handshake_exc = InvalidMessage( - "did not receive a valid HTTP request" - ) - self.handshake_exc.__cause__ = exc - self.send_eof() - self.parser = self.discard() - next(self.parser) # start coroutine - yield - - if self.debug: - self.logger.debug("< GET %s HTTP/1.1", request.path) - for key, value in request.headers.raw_items(): - self.logger.debug("< %s: %s", key, value) - - self.events.append(request) - - yield from super().parse() - - -class ServerConnection(ServerProtocol): - def __init__(self, *args: Any, **kwargs: Any) -> None: - warnings.warn( # deprecated in 11.0 - 2023-04-02 - "ServerConnection was renamed to ServerProtocol", - DeprecationWarning, - ) - super().__init__(*args, **kwargs) - - -lazy_import( - globals(), - deprecated_aliases={ - # deprecated in 14.0 - 2024-11-09 - "WebSocketServer": ".legacy.server", - "WebSocketServerProtocol": ".legacy.server", - "broadcast": ".legacy.server", - "serve": ".legacy.server", - "unix_serve": ".legacy.server", - }, -) diff --git a/wip/SNIP/websockets/speedups.c b/wip/SNIP/websockets/speedups.c deleted file mode 100644 index cb10ded..0000000 --- a/wip/SNIP/websockets/speedups.c +++ /dev/null @@ -1,222 +0,0 @@ -/* C implementation of performance sensitive functions. */ - -#define PY_SSIZE_T_CLEAN -#include -#include /* uint8_t, uint32_t, uint64_t */ - -#if __ARM_NEON -#include -#elif __SSE2__ -#include -#endif - -static const Py_ssize_t MASK_LEN = 4; - -/* Similar to PyBytes_AsStringAndSize, but accepts more types */ - -static int -_PyBytesLike_AsStringAndSize(PyObject *obj, PyObject **tmp, char **buffer, Py_ssize_t *length) -{ - // This supports bytes, bytearrays, and memoryview objects, - // which are common data structures for handling byte streams. - // If *tmp isn't NULL, the caller gets a new reference. - if (PyBytes_Check(obj)) - { - *tmp = NULL; - *buffer = PyBytes_AS_STRING(obj); - *length = PyBytes_GET_SIZE(obj); - } - else if (PyByteArray_Check(obj)) - { - *tmp = NULL; - *buffer = PyByteArray_AS_STRING(obj); - *length = PyByteArray_GET_SIZE(obj); - } - else if (PyMemoryView_Check(obj)) - { - *tmp = PyMemoryView_GetContiguous(obj, PyBUF_READ, 'C'); - if (*tmp == NULL) - { - return -1; - } - Py_buffer *mv_buf; - mv_buf = PyMemoryView_GET_BUFFER(*tmp); - *buffer = mv_buf->buf; - *length = mv_buf->len; - } - else - { - PyErr_Format( - PyExc_TypeError, - "expected a bytes-like object, %.200s found", - Py_TYPE(obj)->tp_name); - return -1; - } - - return 0; -} - -/* C implementation of websockets.utils.apply_mask */ - -static PyObject * -apply_mask(PyObject *self, PyObject *args, PyObject *kwds) -{ - - // In order to support various bytes-like types, accept any Python object. - - static char *kwlist[] = {"data", "mask", NULL}; - PyObject *input_obj; - PyObject *mask_obj; - - // A pointer to a char * + length will be extracted from the data and mask - // arguments, possibly via a Py_buffer. - - PyObject *input_tmp = NULL; - char *input; - Py_ssize_t input_len; - PyObject *mask_tmp = NULL; - char *mask; - Py_ssize_t mask_len; - - // Initialize a PyBytesObject then get a pointer to the underlying char * - // in order to avoid an extra memory copy in PyBytes_FromStringAndSize. - - PyObject *result = NULL; - char *output; - - // Other variables. - - Py_ssize_t i = 0; - - // Parse inputs. - - if (!PyArg_ParseTupleAndKeywords( - args, kwds, "OO", kwlist, &input_obj, &mask_obj)) - { - goto exit; - } - - if (_PyBytesLike_AsStringAndSize(input_obj, &input_tmp, &input, &input_len) == -1) - { - goto exit; - } - - if (_PyBytesLike_AsStringAndSize(mask_obj, &mask_tmp, &mask, &mask_len) == -1) - { - goto exit; - } - - if (mask_len != MASK_LEN) - { - PyErr_SetString(PyExc_ValueError, "mask must contain 4 bytes"); - goto exit; - } - - // Create output. - - result = PyBytes_FromStringAndSize(NULL, input_len); - if (result == NULL) - { - goto exit; - } - - // Since we just created result, we don't need error checks. - output = PyBytes_AS_STRING(result); - - // Perform the masking operation. - - // Apparently GCC cannot figure out the following optimizations by itself. - - // We need a new scope for MSVC 2010 (non C99 friendly) - { -#if __ARM_NEON - - // With NEON support, XOR by blocks of 16 bytes = 128 bits. - - Py_ssize_t input_len_128 = input_len & ~15; - uint8x16_t mask_128 = vreinterpretq_u8_u32(vdupq_n_u32(*(uint32_t *)mask)); - - for (; i < input_len_128; i += 16) - { - uint8x16_t in_128 = vld1q_u8((uint8_t *)(input + i)); - uint8x16_t out_128 = veorq_u8(in_128, mask_128); - vst1q_u8((uint8_t *)(output + i), out_128); - } - -#elif __SSE2__ - - // With SSE2 support, XOR by blocks of 16 bytes = 128 bits. - - // Since we cannot control the 16-bytes alignment of input and output - // buffers, we rely on loadu/storeu rather than load/store. - - Py_ssize_t input_len_128 = input_len & ~15; - __m128i mask_128 = _mm_set1_epi32(*(uint32_t *)mask); - - for (; i < input_len_128; i += 16) - { - __m128i in_128 = _mm_loadu_si128((__m128i *)(input + i)); - __m128i out_128 = _mm_xor_si128(in_128, mask_128); - _mm_storeu_si128((__m128i *)(output + i), out_128); - } - -#else - - // Without SSE2 support, XOR by blocks of 8 bytes = 64 bits. - - // We assume the memory allocator aligns everything on 8 bytes boundaries. - - Py_ssize_t input_len_64 = input_len & ~7; - uint32_t mask_32 = *(uint32_t *)mask; - uint64_t mask_64 = ((uint64_t)mask_32 << 32) | (uint64_t)mask_32; - - for (; i < input_len_64; i += 8) - { - *(uint64_t *)(output + i) = *(uint64_t *)(input + i) ^ mask_64; - } - -#endif - } - - // XOR the remainder of the input byte by byte. - - for (; i < input_len; i++) - { - output[i] = input[i] ^ mask[i & (MASK_LEN - 1)]; - } - -exit: - Py_XDECREF(input_tmp); - Py_XDECREF(mask_tmp); - return result; - -} - -static PyMethodDef speedups_methods[] = { - { - "apply_mask", - (PyCFunction)apply_mask, - METH_VARARGS | METH_KEYWORDS, - "Apply masking to the data of a WebSocket message.", - }, - {NULL, NULL, 0, NULL}, /* Sentinel */ -}; - -static struct PyModuleDef speedups_module = { - PyModuleDef_HEAD_INIT, - "websocket.speedups", /* m_name */ - "C implementation of performance sensitive functions.", - /* m_doc */ - -1, /* m_size */ - speedups_methods, /* m_methods */ - NULL, - NULL, - NULL, - NULL -}; - -PyMODINIT_FUNC -PyInit_speedups(void) -{ - return PyModule_Create(&speedups_module); -} diff --git a/wip/SNIP/websockets/speedups.pyi b/wip/SNIP/websockets/speedups.pyi deleted file mode 100644 index 821438a..0000000 --- a/wip/SNIP/websockets/speedups.pyi +++ /dev/null @@ -1 +0,0 @@ -def apply_mask(data: bytes, mask: bytes) -> bytes: ... diff --git a/wip/SNIP/websockets/streams.py b/wip/SNIP/websockets/streams.py deleted file mode 100644 index f52e619..0000000 --- a/wip/SNIP/websockets/streams.py +++ /dev/null @@ -1,151 +0,0 @@ -from __future__ import annotations - -from collections.abc import Generator - - -class StreamReader: - """ - Generator-based stream reader. - - This class doesn't support concurrent calls to :meth:`read_line`, - :meth:`read_exact`, or :meth:`read_to_eof`. Make sure calls are - serialized. - - """ - - def __init__(self) -> None: - self.buffer = bytearray() - self.eof = False - - def read_line(self, m: int) -> Generator[None, None, bytes]: - """ - Read a LF-terminated line from the stream. - - This is a generator-based coroutine. - - The return value includes the LF character. - - Args: - m: Maximum number bytes to read; this is a security limit. - - Raises: - EOFError: If the stream ends without a LF. - RuntimeError: If the stream ends in more than ``m`` bytes. - - """ - n = 0 # number of bytes to read - p = 0 # number of bytes without a newline - while True: - n = self.buffer.find(b"\n", p) + 1 - if n > 0: - break - p = len(self.buffer) - if p > m: - raise RuntimeError(f"read {p} bytes, expected no more than {m} bytes") - if self.eof: - raise EOFError(f"stream ends after {p} bytes, before end of line") - yield - if n > m: - raise RuntimeError(f"read {n} bytes, expected no more than {m} bytes") - r = self.buffer[:n] - del self.buffer[:n] - return r - - def read_exact(self, n: int) -> Generator[None, None, bytes]: - """ - Read a given number of bytes from the stream. - - This is a generator-based coroutine. - - Args: - n: How many bytes to read. - - Raises: - EOFError: If the stream ends in less than ``n`` bytes. - - """ - assert n >= 0 - while len(self.buffer) < n: - if self.eof: - p = len(self.buffer) - raise EOFError(f"stream ends after {p} bytes, expected {n} bytes") - yield - r = self.buffer[:n] - del self.buffer[:n] - return r - - def read_to_eof(self, m: int) -> Generator[None, None, bytes]: - """ - Read all bytes from the stream. - - This is a generator-based coroutine. - - Args: - m: Maximum number bytes to read; this is a security limit. - - Raises: - RuntimeError: If the stream ends in more than ``m`` bytes. - - """ - while not self.eof: - p = len(self.buffer) - if p > m: - raise RuntimeError(f"read {p} bytes, expected no more than {m} bytes") - yield - r = self.buffer[:] - del self.buffer[:] - return r - - def at_eof(self) -> Generator[None, None, bool]: - """ - Tell whether the stream has ended and all data was read. - - This is a generator-based coroutine. - - """ - while True: - if self.buffer: - return False - if self.eof: - return True - # When all data was read but the stream hasn't ended, we can't - # tell if until either feed_data() or feed_eof() is called. - yield - - def feed_data(self, data: bytes) -> None: - """ - Write data to the stream. - - :meth:`feed_data` cannot be called after :meth:`feed_eof`. - - Args: - data: Data to write. - - Raises: - EOFError: If the stream has ended. - - """ - if self.eof: - raise EOFError("stream ended") - self.buffer += data - - def feed_eof(self) -> None: - """ - End the stream. - - :meth:`feed_eof` cannot be called more than once. - - Raises: - EOFError: If the stream has ended. - - """ - if self.eof: - raise EOFError("stream ended") - self.eof = True - - def discard(self) -> None: - """ - Discard all buffered data, but don't end the stream. - - """ - del self.buffer[:] diff --git a/wip/SNIP/websockets/sync/__init__.py b/wip/SNIP/websockets/sync/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/wip/SNIP/websockets/sync/client.py b/wip/SNIP/websockets/sync/client.py deleted file mode 100644 index 58cb847..0000000 --- a/wip/SNIP/websockets/sync/client.py +++ /dev/null @@ -1,648 +0,0 @@ -from __future__ import annotations - -import socket -import ssl as ssl_module -import threading -import warnings -from collections.abc import Sequence -from typing import Any, Callable, Literal, TypeVar, cast - -from ..client import ClientProtocol -from ..datastructures import Headers, HeadersLike -from ..exceptions import InvalidProxyMessage, InvalidProxyStatus, ProxyError -from ..extensions.base import ClientExtensionFactory -from ..extensions.permessage_deflate import enable_client_permessage_deflate -from ..headers import build_authorization_basic, build_host, validate_subprotocols -from ..http11 import USER_AGENT, Response -from ..protocol import CONNECTING, Event -from ..streams import StreamReader -from ..typing import LoggerLike, Origin, Subprotocol -from ..uri import Proxy, WebSocketURI, get_proxy, parse_proxy, parse_uri -from .connection import Connection -from .utils import Deadline - - -__all__ = ["connect", "unix_connect", "ClientConnection"] - - -class ClientConnection(Connection): - """ - :mod:`threading` implementation of a WebSocket client connection. - - :class:`ClientConnection` provides :meth:`recv` and :meth:`send` methods for - receiving and sending messages. - - It supports iteration to receive messages:: - - for message in websocket: - process(message) - - The iterator exits normally when the connection is closed with close code - 1000 (OK) or 1001 (going away) or without a close code. It raises a - :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is - closed with any other code. - - The ``ping_interval``, ``ping_timeout``, ``close_timeout``, and - ``max_queue`` arguments have the same meaning as in :func:`connect`. - - Args: - socket: Socket connected to a WebSocket server. - protocol: Sans-I/O connection. - - """ - - def __init__( - self, - socket: socket.socket, - protocol: ClientProtocol, - *, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = 10, - max_queue: int | None | tuple[int | None, int | None] = 16, - ) -> None: - self.protocol: ClientProtocol - self.response_rcvd = threading.Event() - super().__init__( - socket, - protocol, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - close_timeout=close_timeout, - max_queue=max_queue, - ) - - def handshake( - self, - additional_headers: HeadersLike | None = None, - user_agent_header: str | None = USER_AGENT, - timeout: float | None = None, - ) -> None: - """ - Perform the opening handshake. - - """ - with self.send_context(expected_state=CONNECTING): - self.request = self.protocol.connect() - if additional_headers is not None: - self.request.headers.update(additional_headers) - if user_agent_header is not None: - self.request.headers.setdefault("User-Agent", user_agent_header) - self.protocol.send_request(self.request) - - if not self.response_rcvd.wait(timeout): - raise TimeoutError("timed out while waiting for handshake response") - - # self.protocol.handshake_exc is set when the connection is lost before - # receiving a response, when the response cannot be parsed, or when the - # response fails the handshake. - - if self.protocol.handshake_exc is not None: - raise self.protocol.handshake_exc - - def process_event(self, event: Event) -> None: - """ - Process one incoming event. - - """ - # First event - handshake response. - if self.response is None: - assert isinstance(event, Response) - self.response = event - self.response_rcvd.set() - # Later events - frames. - else: - super().process_event(event) - - def recv_events(self) -> None: - """ - Read incoming data from the socket and process events. - - """ - try: - super().recv_events() - finally: - # If the connection is closed during the handshake, unblock it. - self.response_rcvd.set() - - -def connect( - uri: str, - *, - # TCP/TLS - sock: socket.socket | None = None, - ssl: ssl_module.SSLContext | None = None, - server_hostname: str | None = None, - # WebSocket - origin: Origin | None = None, - extensions: Sequence[ClientExtensionFactory] | None = None, - subprotocols: Sequence[Subprotocol] | None = None, - compression: str | None = "deflate", - # HTTP - additional_headers: HeadersLike | None = None, - user_agent_header: str | None = USER_AGENT, - proxy: str | Literal[True] | None = True, - proxy_ssl: ssl_module.SSLContext | None = None, - proxy_server_hostname: str | None = None, - # Timeouts - open_timeout: float | None = 10, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = 10, - # Limits - max_size: int | None = 2**20, - max_queue: int | None | tuple[int | None, int | None] = 16, - # Logging - logger: LoggerLike | None = None, - # Escape hatch for advanced customization - create_connection: type[ClientConnection] | None = None, - **kwargs: Any, -) -> ClientConnection: - """ - Connect to the WebSocket server at ``uri``. - - This function returns a :class:`ClientConnection` instance, which you can - use to send and receive messages. - - :func:`connect` may be used as a context manager:: - - from websockets.sync.client import connect - - with connect(...) as websocket: - ... - - The connection is closed automatically when exiting the context. - - Args: - uri: URI of the WebSocket server. - sock: Preexisting TCP socket. ``sock`` overrides the host and port - from ``uri``. You may call :func:`socket.create_connection` to - create a suitable TCP socket. - ssl: Configuration for enabling TLS on the connection. - server_hostname: Host name for the TLS handshake. ``server_hostname`` - overrides the host name from ``uri``. - origin: Value of the ``Origin`` header, for servers that require it. - extensions: List of supported extensions, in order in which they - should be negotiated and run. - subprotocols: List of supported subprotocols, in order of decreasing - preference. - compression: The "permessage-deflate" extension is enabled by default. - Set ``compression`` to :obj:`None` to disable it. See the - :doc:`compression guide <../../topics/compression>` for details. - additional_headers (HeadersLike | None): Arbitrary HTTP headers to add - to the handshake request. - user_agent_header: Value of the ``User-Agent`` request header. - It defaults to ``"Python/x.y.z websockets/X.Y"``. - Setting it to :obj:`None` removes the header. - proxy: If a proxy is configured, it is used by default. Set ``proxy`` - to :obj:`None` to disable the proxy or to the address of a proxy - to override the system configuration. See the :doc:`proxy docs - <../../topics/proxies>` for details. - proxy_ssl: Configuration for enabling TLS on the proxy connection. - proxy_server_hostname: Host name for the TLS handshake with the proxy. - ``proxy_server_hostname`` overrides the host name from ``proxy``. - open_timeout: Timeout for opening the connection in seconds. - :obj:`None` disables the timeout. - ping_interval: Interval between keepalive pings in seconds. - :obj:`None` disables keepalive. - ping_timeout: Timeout for keepalive pings in seconds. - :obj:`None` disables timeouts. - close_timeout: Timeout for closing the connection in seconds. - :obj:`None` disables the timeout. - max_size: Maximum size of incoming messages in bytes. - :obj:`None` disables the limit. - max_queue: High-water mark of the buffer where frames are received. - It defaults to 16 frames. The low-water mark defaults to ``max_queue - // 4``. You may pass a ``(high, low)`` tuple to set the high-water - and low-water marks. If you want to disable flow control entirely, - you may set it to ``None``, although that's a bad idea. - logger: Logger for this client. - It defaults to ``logging.getLogger("websockets.client")``. - See the :doc:`logging guide <../../topics/logging>` for details. - create_connection: Factory for the :class:`ClientConnection` managing - the connection. Set it to a wrapper or a subclass to customize - connection handling. - - Any other keyword arguments are passed to :func:`~socket.create_connection`. - - Raises: - InvalidURI: If ``uri`` isn't a valid WebSocket URI. - OSError: If the TCP connection fails. - InvalidHandshake: If the opening handshake fails. - TimeoutError: If the opening handshake times out. - - """ - - # Process parameters - - # Backwards compatibility: ssl used to be called ssl_context. - if ssl is None and "ssl_context" in kwargs: - ssl = kwargs.pop("ssl_context") - warnings.warn( # deprecated in 13.0 - 2024-08-20 - "ssl_context was renamed to ssl", - DeprecationWarning, - ) - - ws_uri = parse_uri(uri) - if not ws_uri.secure and ssl is not None: - raise ValueError("ssl argument is incompatible with a ws:// URI") - - # Private APIs for unix_connect() - unix: bool = kwargs.pop("unix", False) - path: str | None = kwargs.pop("path", None) - - if unix: - if path is None and sock is None: - raise ValueError("missing path argument") - elif path is not None and sock is not None: - raise ValueError("path and sock arguments are incompatible") - - if subprotocols is not None: - validate_subprotocols(subprotocols) - - if compression == "deflate": - extensions = enable_client_permessage_deflate(extensions) - elif compression is not None: - raise ValueError(f"unsupported compression: {compression}") - - if unix: - proxy = None - if sock is not None: - proxy = None - if proxy is True: - proxy = get_proxy(ws_uri) - - # Calculate timeouts on the TCP, TLS, and WebSocket handshakes. - # The TCP and TLS timeouts must be set on the socket, then removed - # to avoid conflicting with the WebSocket timeout in handshake(). - deadline = Deadline(open_timeout) - - if create_connection is None: - create_connection = ClientConnection - - try: - # Connect socket - - if sock is None: - if unix: - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.settimeout(deadline.timeout()) - assert path is not None # mypy cannot figure this out - sock.connect(path) - elif proxy is not None: - proxy_parsed = parse_proxy(proxy) - if proxy_parsed.scheme[:5] == "socks": - # Connect to the server through the proxy. - sock = connect_socks_proxy( - proxy_parsed, - ws_uri, - deadline, - # websockets is consistent with the socket module while - # python_socks is consistent across implementations. - local_addr=kwargs.pop("source_address", None), - ) - elif proxy_parsed.scheme[:4] == "http": - # Validate the proxy_ssl argument. - if proxy_parsed.scheme != "https" and proxy_ssl is not None: - raise ValueError( - "proxy_ssl argument is incompatible with an http:// proxy" - ) - # Connect to the server through the proxy. - sock = connect_http_proxy( - proxy_parsed, - ws_uri, - deadline, - user_agent_header=user_agent_header, - ssl=proxy_ssl, - server_hostname=proxy_server_hostname, - **kwargs, - ) - else: - raise AssertionError("unsupported proxy") - else: - kwargs.setdefault("timeout", deadline.timeout()) - sock = socket.create_connection( - (ws_uri.host, ws_uri.port), - **kwargs, - ) - sock.settimeout(None) - - # Disable Nagle algorithm - - if not unix: - sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True) - - # Initialize TLS wrapper and perform TLS handshake - - if ws_uri.secure: - if ssl is None: - ssl = ssl_module.create_default_context() - if server_hostname is None: - server_hostname = ws_uri.host - sock.settimeout(deadline.timeout()) - if proxy_ssl is None: - sock = ssl.wrap_socket(sock, server_hostname=server_hostname) - else: - sock_2 = SSLSSLSocket(sock, ssl, server_hostname=server_hostname) - # Let's pretend that sock is a socket, even though it isn't. - sock = cast(socket.socket, sock_2) - sock.settimeout(None) - - # Initialize WebSocket protocol - - protocol = ClientProtocol( - ws_uri, - origin=origin, - extensions=extensions, - subprotocols=subprotocols, - max_size=max_size, - logger=logger, - ) - - # Initialize WebSocket connection - - connection = create_connection( - sock, - protocol, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - close_timeout=close_timeout, - max_queue=max_queue, - ) - except Exception: - if sock is not None: - sock.close() - raise - - try: - connection.handshake( - additional_headers, - user_agent_header, - deadline.timeout(), - ) - except Exception: - connection.close_socket() - connection.recv_events_thread.join() - raise - - connection.start_keepalive() - return connection - - -def unix_connect( - path: str | None = None, - uri: str | None = None, - **kwargs: Any, -) -> ClientConnection: - """ - Connect to a WebSocket server listening on a Unix socket. - - This function accepts the same keyword arguments as :func:`connect`. - - It's only available on Unix. - - It's mainly useful for debugging servers listening on Unix sockets. - - Args: - path: File system path to the Unix socket. - uri: URI of the WebSocket server. ``uri`` defaults to - ``ws://localhost/`` or, when a ``ssl`` is provided, to - ``wss://localhost/``. - - """ - if uri is None: - # Backwards compatibility: ssl used to be called ssl_context. - if kwargs.get("ssl") is None and kwargs.get("ssl_context") is None: - uri = "ws://localhost/" - else: - uri = "wss://localhost/" - return connect(uri=uri, unix=True, path=path, **kwargs) - - -try: - from python_socks import ProxyType - from python_socks.sync import Proxy as SocksProxy - - SOCKS_PROXY_TYPES = { - "socks5h": ProxyType.SOCKS5, - "socks5": ProxyType.SOCKS5, - "socks4a": ProxyType.SOCKS4, - "socks4": ProxyType.SOCKS4, - } - - SOCKS_PROXY_RDNS = { - "socks5h": True, - "socks5": False, - "socks4a": True, - "socks4": False, - } - - def connect_socks_proxy( - proxy: Proxy, - ws_uri: WebSocketURI, - deadline: Deadline, - **kwargs: Any, - ) -> socket.socket: - """Connect via a SOCKS proxy and return the socket.""" - socks_proxy = SocksProxy( - SOCKS_PROXY_TYPES[proxy.scheme], - proxy.host, - proxy.port, - proxy.username, - proxy.password, - SOCKS_PROXY_RDNS[proxy.scheme], - ) - kwargs.setdefault("timeout", deadline.timeout()) - # connect() is documented to raise OSError and TimeoutError. - # Wrap other exceptions in ProxyError, a subclass of InvalidHandshake. - try: - return socks_proxy.connect(ws_uri.host, ws_uri.port, **kwargs) - except (OSError, TimeoutError, socket.timeout): - raise - except Exception as exc: - raise ProxyError("failed to connect to SOCKS proxy") from exc - -except ImportError: - - def connect_socks_proxy( - proxy: Proxy, - ws_uri: WebSocketURI, - deadline: Deadline, - **kwargs: Any, - ) -> socket.socket: - raise ImportError("python-socks is required to use a SOCKS proxy") - - -def prepare_connect_request( - proxy: Proxy, - ws_uri: WebSocketURI, - user_agent_header: str | None = None, -) -> bytes: - host = build_host(ws_uri.host, ws_uri.port, ws_uri.secure, always_include_port=True) - headers = Headers() - headers["Host"] = build_host(ws_uri.host, ws_uri.port, ws_uri.secure) - if user_agent_header is not None: - headers["User-Agent"] = user_agent_header - if proxy.username is not None: - assert proxy.password is not None # enforced by parse_proxy() - headers["Proxy-Authorization"] = build_authorization_basic( - proxy.username, proxy.password - ) - # We cannot use the Request class because it supports only GET requests. - return f"CONNECT {host} HTTP/1.1\r\n".encode() + headers.serialize() - - -def read_connect_response(sock: socket.socket, deadline: Deadline) -> Response: - reader = StreamReader() - parser = Response.parse( - reader.read_line, - reader.read_exact, - reader.read_to_eof, - include_body=False, - ) - try: - while True: - sock.settimeout(deadline.timeout()) - data = sock.recv(4096) - if data: - reader.feed_data(data) - else: - reader.feed_eof() - next(parser) - except StopIteration as exc: - assert isinstance(exc.value, Response) # help mypy - response = exc.value - if 200 <= response.status_code < 300: - return response - else: - raise InvalidProxyStatus(response) - except socket.timeout: - raise TimeoutError("timed out while connecting to HTTP proxy") - except Exception as exc: - raise InvalidProxyMessage( - "did not receive a valid HTTP response from proxy" - ) from exc - finally: - sock.settimeout(None) - - -def connect_http_proxy( - proxy: Proxy, - ws_uri: WebSocketURI, - deadline: Deadline, - *, - user_agent_header: str | None = None, - ssl: ssl_module.SSLContext | None = None, - server_hostname: str | None = None, - **kwargs: Any, -) -> socket.socket: - # Connect socket - - kwargs.setdefault("timeout", deadline.timeout()) - sock = socket.create_connection((proxy.host, proxy.port), **kwargs) - - # Initialize TLS wrapper and perform TLS handshake - - if proxy.scheme == "https": - if ssl is None: - ssl = ssl_module.create_default_context() - if server_hostname is None: - server_hostname = proxy.host - sock.settimeout(deadline.timeout()) - sock = ssl.wrap_socket(sock, server_hostname=server_hostname) - sock.settimeout(None) - - # Send CONNECT request to the proxy and read response. - - sock.sendall(prepare_connect_request(proxy, ws_uri, user_agent_header)) - try: - read_connect_response(sock, deadline) - except Exception: - sock.close() - raise - - return sock - - -T = TypeVar("T") -F = TypeVar("F", bound=Callable[..., T]) - - -class SSLSSLSocket: - """ - Socket-like object providing TLS-in-TLS. - - Only methods that are used by websockets are implemented. - - """ - - recv_bufsize = 65536 - - def __init__( - self, - sock: socket.socket, - ssl_context: ssl_module.SSLContext, - server_hostname: str | None = None, - ) -> None: - self.incoming = ssl_module.MemoryBIO() - self.outgoing = ssl_module.MemoryBIO() - self.ssl_socket = sock - self.ssl_object = ssl_context.wrap_bio( - self.incoming, - self.outgoing, - server_hostname=server_hostname, - ) - self.run_io(self.ssl_object.do_handshake) - - def run_io(self, func: Callable[..., T], *args: Any) -> T: - while True: - want_read = False - want_write = False - try: - result = func(*args) - except ssl_module.SSLWantReadError: - want_read = True - except ssl_module.SSLWantWriteError: # pragma: no cover - want_write = True - - # Write outgoing data in all cases. - data = self.outgoing.read() - if data: - self.ssl_socket.sendall(data) - - # Read incoming data and retry on SSLWantReadError. - if want_read: - data = self.ssl_socket.recv(self.recv_bufsize) - if data: - self.incoming.write(data) - else: - self.incoming.write_eof() - continue - # Retry after writing outgoing data on SSLWantWriteError. - if want_write: # pragma: no cover - continue - # Return result if no error happened. - return result - - def recv(self, buflen: int) -> bytes: - try: - return self.run_io(self.ssl_object.read, buflen) - except ssl_module.SSLEOFError: - return b"" # always ignore ragged EOFs - - def send(self, data: bytes) -> int: - return self.run_io(self.ssl_object.write, data) - - def sendall(self, data: bytes) -> None: - # adapted from ssl_module.SSLSocket.sendall() - count = 0 - with memoryview(data) as view, view.cast("B") as byte_view: - amount = len(byte_view) - while count < amount: - count += self.send(byte_view[count:]) - - # recv_into(), recvfrom(), recvfrom_into(), sendto(), unwrap(), and the - # flags argument aren't implemented because websockets doesn't need them. - - def __getattr__(self, name: str) -> Any: - return getattr(self.ssl_socket, name) diff --git a/wip/SNIP/websockets/sync/connection.py b/wip/SNIP/websockets/sync/connection.py deleted file mode 100644 index 8b9e062..0000000 --- a/wip/SNIP/websockets/sync/connection.py +++ /dev/null @@ -1,1072 +0,0 @@ -from __future__ import annotations - -import contextlib -import logging -import random -import socket -import struct -import threading -import time -import uuid -from collections.abc import Iterable, Iterator, Mapping -from types import TracebackType -from typing import Any, Literal, overload - -from ..exceptions import ( - ConcurrencyError, - ConnectionClosed, - ConnectionClosedOK, - ProtocolError, -) -from ..frames import DATA_OPCODES, BytesLike, CloseCode, Frame, Opcode -from ..http11 import Request, Response -from ..protocol import CLOSED, OPEN, Event, Protocol, State -from ..typing import Data, LoggerLike, Subprotocol -from .messages import Assembler -from .utils import Deadline - - -__all__ = ["Connection"] - - -class Connection: - """ - :mod:`threading` implementation of a WebSocket connection. - - :class:`Connection` provides APIs shared between WebSocket servers and - clients. - - You shouldn't use it directly. Instead, use - :class:`~websockets.sync.client.ClientConnection` or - :class:`~websockets.sync.server.ServerConnection`. - - """ - - recv_bufsize = 65536 - - def __init__( - self, - socket: socket.socket, - protocol: Protocol, - *, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = 10, - max_queue: int | None | tuple[int | None, int | None] = 16, - ) -> None: - self.socket = socket - self.protocol = protocol - self.ping_interval = ping_interval - self.ping_timeout = ping_timeout - self.close_timeout = close_timeout - if isinstance(max_queue, int) or max_queue is None: - max_queue = (max_queue, None) - self.max_queue = max_queue - - # Inject reference to this instance in the protocol's logger. - self.protocol.logger = logging.LoggerAdapter( - self.protocol.logger, - {"websocket": self}, - ) - - # Copy attributes from the protocol for convenience. - self.id: uuid.UUID = self.protocol.id - """Unique identifier of the connection. Useful in logs.""" - self.logger: LoggerLike = self.protocol.logger - """Logger for this connection.""" - self.debug = self.protocol.debug - - # HTTP handshake request and response. - self.request: Request | None = None - """Opening handshake request.""" - self.response: Response | None = None - """Opening handshake response.""" - - # Mutex serializing interactions with the protocol. - self.protocol_mutex = threading.Lock() - - # Lock stopping reads when the assembler buffer is full. - self.recv_flow_control = threading.Lock() - - # Assembler turning frames into messages and serializing reads. - self.recv_messages = Assembler( - *self.max_queue, - pause=self.recv_flow_control.acquire, - resume=self.recv_flow_control.release, - ) - - # Deadline for the closing handshake. - self.close_deadline: Deadline | None = None - - # Whether we are busy sending a fragmented message. - self.send_in_progress = False - - # Mapping of ping IDs to pong waiters, in chronological order. - self.pong_waiters: dict[bytes, tuple[threading.Event, float, bool]] = {} - - self.latency: float = 0 - """ - Latency of the connection, in seconds. - - Latency is defined as the round-trip time of the connection. It is - measured by sending a Ping frame and waiting for a matching Pong frame. - Before the first measurement, :attr:`latency` is ``0``. - - By default, websockets enables a :ref:`keepalive ` mechanism - that sends Ping frames automatically at regular intervals. You can also - send Ping frames and measure latency with :meth:`ping`. - """ - - # Thread that sends keepalive pings. None when ping_interval is None. - self.keepalive_thread: threading.Thread | None = None - - # Exception raised in recv_events, to be chained to ConnectionClosed - # in the user thread in order to show why the TCP connection dropped. - self.recv_exc: BaseException | None = None - - # Receiving events from the socket. This thread is marked as daemon to - # allow creating a connection in a non-daemon thread and using it in a - # daemon thread. This mustn't prevent the interpreter from exiting. - self.recv_events_thread = threading.Thread( - target=self.recv_events, - daemon=True, - ) - - # Start recv_events only after all attributes are initialized. - self.recv_events_thread.start() - - # Public attributes - - @property - def local_address(self) -> Any: - """ - Local address of the connection. - - For IPv4 connections, this is a ``(host, port)`` tuple. - - The format of the address depends on the address family. - See :meth:`~socket.socket.getsockname`. - - """ - return self.socket.getsockname() - - @property - def remote_address(self) -> Any: - """ - Remote address of the connection. - - For IPv4 connections, this is a ``(host, port)`` tuple. - - The format of the address depends on the address family. - See :meth:`~socket.socket.getpeername`. - - """ - return self.socket.getpeername() - - @property - def state(self) -> State: - """ - State of the WebSocket connection, defined in :rfc:`6455`. - - This attribute is provided for completeness. Typical applications - shouldn't check its value. Instead, they should call :meth:`~recv` or - :meth:`send` and handle :exc:`~websockets.exceptions.ConnectionClosed` - exceptions. - - """ - return self.protocol.state - - @property - def subprotocol(self) -> Subprotocol | None: - """ - Subprotocol negotiated during the opening handshake. - - :obj:`None` if no subprotocol was negotiated. - - """ - return self.protocol.subprotocol - - @property - def close_code(self) -> int | None: - """ - State of the WebSocket connection, defined in :rfc:`6455`. - - This attribute is provided for completeness. Typical applications - shouldn't check its value. Instead, they should inspect attributes - of :exc:`~websockets.exceptions.ConnectionClosed` exceptions. - - """ - return self.protocol.close_code - - @property - def close_reason(self) -> str | None: - """ - State of the WebSocket connection, defined in :rfc:`6455`. - - This attribute is provided for completeness. Typical applications - shouldn't check its value. Instead, they should inspect attributes - of :exc:`~websockets.exceptions.ConnectionClosed` exceptions. - - """ - return self.protocol.close_reason - - # Public methods - - def __enter__(self) -> Connection: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - if exc_type is None: - self.close() - else: - self.close(CloseCode.INTERNAL_ERROR) - - def __iter__(self) -> Iterator[Data]: - """ - Iterate on incoming messages. - - The iterator calls :meth:`recv` and yields messages in an infinite loop. - - It exits when the connection is closed normally. It raises a - :exc:`~websockets.exceptions.ConnectionClosedError` exception after a - protocol error or a network failure. - - """ - try: - while True: - yield self.recv() - except ConnectionClosedOK: - return - - # This overload structure is required to avoid the error: - # "parameter without a default follows parameter with a default" - - @overload - def recv(self, timeout: float | None, decode: Literal[True]) -> str: ... - - @overload - def recv(self, timeout: float | None, decode: Literal[False]) -> bytes: ... - - @overload - def recv(self, timeout: float | None = None, *, decode: Literal[True]) -> str: ... - - @overload - def recv( - self, timeout: float | None = None, *, decode: Literal[False] - ) -> bytes: ... - - @overload - def recv( - self, timeout: float | None = None, decode: bool | None = None - ) -> Data: ... - - def recv(self, timeout: float | None = None, decode: bool | None = None) -> Data: - """ - Receive the next message. - - When the connection is closed, :meth:`recv` raises - :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it raises - :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal closure - and :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol - error or a network failure. This is how you detect the end of the - message stream. - - If ``timeout`` is :obj:`None`, block until a message is received. If - ``timeout`` is set, wait up to ``timeout`` seconds for a message to be - received and return it, else raise :exc:`TimeoutError`. If ``timeout`` - is ``0`` or negative, check if a message has been received already and - return it, else raise :exc:`TimeoutError`. - - If the message is fragmented, wait until all fragments are received, - reassemble them, and return the whole message. - - Args: - timeout: Timeout for receiving a message in seconds. - decode: Set this flag to override the default behavior of returning - :class:`str` or :class:`bytes`. See below for details. - - Returns: - A string (:class:`str`) for a Text_ frame or a bytestring - (:class:`bytes`) for a Binary_ frame. - - .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - .. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - You may override this behavior with the ``decode`` argument: - - * Set ``decode=False`` to disable UTF-8 decoding of Text_ frames and - return a bytestring (:class:`bytes`). This improves performance - when decoding isn't needed, for example if the message contains - JSON and you're using a JSON library that expects a bytestring. - * Set ``decode=True`` to force UTF-8 decoding of Binary_ frames - and return a string (:class:`str`). This may be useful for - servers that send binary frames instead of text frames. - - Raises: - ConnectionClosed: When the connection is closed. - ConcurrencyError: If two threads call :meth:`recv` or - :meth:`recv_streaming` concurrently. - - """ - try: - return self.recv_messages.get(timeout, decode) - except EOFError: - pass - # fallthrough - except ConcurrencyError: - raise ConcurrencyError( - "cannot call recv while another thread " - "is already running recv or recv_streaming" - ) from None - except UnicodeDecodeError as exc: - with self.send_context(): - self.protocol.fail( - CloseCode.INVALID_DATA, - f"{exc.reason} at position {exc.start}", - ) - # fallthrough - - # Wait for the protocol state to be CLOSED before accessing close_exc. - self.recv_events_thread.join() - raise self.protocol.close_exc from self.recv_exc - - @overload - def recv_streaming(self, decode: Literal[True]) -> Iterator[str]: ... - - @overload - def recv_streaming(self, decode: Literal[False]) -> Iterator[bytes]: ... - - @overload - def recv_streaming(self, decode: bool | None = None) -> Iterator[Data]: ... - - def recv_streaming(self, decode: bool | None = None) -> Iterator[Data]: - """ - Receive the next message frame by frame. - - This method is designed for receiving fragmented messages. It returns an - iterator that yields each fragment as it is received. This iterator must - be fully consumed. Else, future calls to :meth:`recv` or - :meth:`recv_streaming` will raise - :exc:`~websockets.exceptions.ConcurrencyError`, making the connection - unusable. - - :meth:`recv_streaming` raises the same exceptions as :meth:`recv`. - - Args: - decode: Set this flag to override the default behavior of returning - :class:`str` or :class:`bytes`. See below for details. - - Returns: - An iterator of strings (:class:`str`) for a Text_ frame or - bytestrings (:class:`bytes`) for a Binary_ frame. - - .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - .. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - You may override this behavior with the ``decode`` argument: - - * Set ``decode=False`` to disable UTF-8 decoding of Text_ frames - and return bytestrings (:class:`bytes`). This may be useful to - optimize performance when decoding isn't needed. - * Set ``decode=True`` to force UTF-8 decoding of Binary_ frames - and return strings (:class:`str`). This is useful for servers - that send binary frames instead of text frames. - - Raises: - ConnectionClosed: When the connection is closed. - ConcurrencyError: If two threads call :meth:`recv` or - :meth:`recv_streaming` concurrently. - - """ - try: - yield from self.recv_messages.get_iter(decode) - return - except EOFError: - pass - # fallthrough - except ConcurrencyError: - raise ConcurrencyError( - "cannot call recv_streaming while another thread " - "is already running recv or recv_streaming" - ) from None - except UnicodeDecodeError as exc: - with self.send_context(): - self.protocol.fail( - CloseCode.INVALID_DATA, - f"{exc.reason} at position {exc.start}", - ) - # fallthrough - - # Wait for the protocol state to be CLOSED before accessing close_exc. - self.recv_events_thread.join() - raise self.protocol.close_exc from self.recv_exc - - def send( - self, - message: Data | Iterable[Data], - text: bool | None = None, - ) -> None: - """ - Send a message. - - A string (:class:`str`) is sent as a Text_ frame. A bytestring or - bytes-like object (:class:`bytes`, :class:`bytearray`, or - :class:`memoryview`) is sent as a Binary_ frame. - - .. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - .. _Binary: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - You may override this behavior with the ``text`` argument: - - * Set ``text=True`` to send a bytestring or bytes-like object - (:class:`bytes`, :class:`bytearray`, or :class:`memoryview`) as a - Text_ frame. This improves performance when the message is already - UTF-8 encoded, for example if the message contains JSON and you're - using a JSON library that produces a bytestring. - * Set ``text=False`` to send a string (:class:`str`) in a Binary_ - frame. This may be useful for servers that expect binary frames - instead of text frames. - - :meth:`send` also accepts an iterable of strings, bytestrings, or - bytes-like objects to enable fragmentation_. Each item is treated as a - message fragment and sent in its own frame. All items must be of the - same type, or else :meth:`send` will raise a :exc:`TypeError` and the - connection will be closed. - - .. _fragmentation: https://datatracker.ietf.org/doc/html/rfc6455#section-5.4 - - :meth:`send` rejects dict-like objects because this is often an error. - (If you really want to send the keys of a dict-like object as fragments, - call its :meth:`~dict.keys` method and pass the result to :meth:`send`.) - - When the connection is closed, :meth:`send` raises - :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it - raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal - connection closure and - :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol - error or a network failure. - - Args: - message: Message to send. - - Raises: - ConnectionClosed: When the connection is closed. - ConcurrencyError: If the connection is sending a fragmented message. - TypeError: If ``message`` doesn't have a supported type. - - """ - # Unfragmented message -- this case must be handled first because - # strings and bytes-like objects are iterable. - - if isinstance(message, str): - with self.send_context(): - if self.send_in_progress: - raise ConcurrencyError( - "cannot call send while another thread is already running send" - ) - if text is False: - self.protocol.send_binary(message.encode()) - else: - self.protocol.send_text(message.encode()) - - elif isinstance(message, BytesLike): - with self.send_context(): - if self.send_in_progress: - raise ConcurrencyError( - "cannot call send while another thread is already running send" - ) - if text is True: - self.protocol.send_text(message) - else: - self.protocol.send_binary(message) - - # Catch a common mistake -- passing a dict to send(). - - elif isinstance(message, Mapping): - raise TypeError("data is a dict-like object") - - # Fragmented message -- regular iterator. - - elif isinstance(message, Iterable): - chunks = iter(message) - try: - chunk = next(chunks) - except StopIteration: - return - - try: - # First fragment. - if isinstance(chunk, str): - with self.send_context(): - if self.send_in_progress: - raise ConcurrencyError( - "cannot call send while another thread " - "is already running send" - ) - self.send_in_progress = True - if text is False: - self.protocol.send_binary(chunk.encode(), fin=False) - else: - self.protocol.send_text(chunk.encode(), fin=False) - encode = True - elif isinstance(chunk, BytesLike): - with self.send_context(): - if self.send_in_progress: - raise ConcurrencyError( - "cannot call send while another thread " - "is already running send" - ) - self.send_in_progress = True - if text is True: - self.protocol.send_text(chunk, fin=False) - else: - self.protocol.send_binary(chunk, fin=False) - encode = False - else: - raise TypeError("data iterable must contain bytes or str") - - # Other fragments - for chunk in chunks: - if isinstance(chunk, str) and encode: - with self.send_context(): - assert self.send_in_progress - self.protocol.send_continuation(chunk.encode(), fin=False) - elif isinstance(chunk, BytesLike) and not encode: - with self.send_context(): - assert self.send_in_progress - self.protocol.send_continuation(chunk, fin=False) - else: - raise TypeError("data iterable must contain uniform types") - - # Final fragment. - with self.send_context(): - self.protocol.send_continuation(b"", fin=True) - self.send_in_progress = False - - except ConcurrencyError: - # We didn't start sending a fragmented message. - # The connection is still usable. - raise - - except Exception: - # We're half-way through a fragmented message and we can't - # complete it. This makes the connection unusable. - with self.send_context(): - self.protocol.fail( - CloseCode.INTERNAL_ERROR, - "error in fragmented message", - ) - raise - - else: - raise TypeError("data must be str, bytes, or iterable") - - def close(self, code: int = CloseCode.NORMAL_CLOSURE, reason: str = "") -> None: - """ - Perform the closing handshake. - - :meth:`close` waits for the other end to complete the handshake, for the - TCP connection to terminate, and for all incoming messages to be read - with :meth:`recv`. - - :meth:`close` is idempotent: it doesn't do anything once the - connection is closed. - - Args: - code: WebSocket close code. - reason: WebSocket close reason. - - """ - try: - # The context manager takes care of waiting for the TCP connection - # to terminate after calling a method that sends a close frame. - with self.send_context(): - if self.send_in_progress: - self.protocol.fail( - CloseCode.INTERNAL_ERROR, - "close during fragmented message", - ) - else: - self.protocol.send_close(code, reason) - except ConnectionClosed: - # Ignore ConnectionClosed exceptions raised from send_context(). - # They mean that the connection is closed, which was the goal. - pass - - def ping( - self, - data: Data | None = None, - ack_on_close: bool = False, - ) -> threading.Event: - """ - Send a Ping_. - - .. _Ping: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2 - - A ping may serve as a keepalive or as a check that the remote endpoint - received all messages up to this point - - Args: - data: Payload of the ping. A :class:`str` will be encoded to UTF-8. - If ``data`` is :obj:`None`, the payload is four random bytes. - ack_on_close: when this option is :obj:`True`, the event will also - be set when the connection is closed. While this avoids getting - stuck waiting for a pong that will never arrive, it requires - checking that the state of the connection is still ``OPEN`` to - confirm that a pong was received, rather than the connection - being closed. - - Returns: - An event that will be set when the corresponding pong is received. - You can ignore it if you don't intend to wait. - - :: - - pong_event = ws.ping() - pong_event.wait() # only if you want to wait for the pong - - Raises: - ConnectionClosed: When the connection is closed. - ConcurrencyError: If another ping was sent with the same data and - the corresponding pong wasn't received yet. - - """ - if isinstance(data, BytesLike): - data = bytes(data) - elif isinstance(data, str): - data = data.encode() - elif data is not None: - raise TypeError("data must be str or bytes-like") - - with self.send_context(): - # Protect against duplicates if a payload is explicitly set. - if data in self.pong_waiters: - raise ConcurrencyError("already waiting for a pong with the same data") - - # Generate a unique random payload otherwise. - while data is None or data in self.pong_waiters: - data = struct.pack("!I", random.getrandbits(32)) - - pong_waiter = threading.Event() - self.pong_waiters[data] = (pong_waiter, time.monotonic(), ack_on_close) - self.protocol.send_ping(data) - return pong_waiter - - def pong(self, data: Data = b"") -> None: - """ - Send a Pong_. - - .. _Pong: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.3 - - An unsolicited pong may serve as a unidirectional heartbeat. - - Args: - data: Payload of the pong. A :class:`str` will be encoded to UTF-8. - - Raises: - ConnectionClosed: When the connection is closed. - - """ - if isinstance(data, BytesLike): - data = bytes(data) - elif isinstance(data, str): - data = data.encode() - else: - raise TypeError("data must be str or bytes-like") - - with self.send_context(): - self.protocol.send_pong(data) - - # Private methods - - def process_event(self, event: Event) -> None: - """ - Process one incoming event. - - This method is overridden in subclasses to handle the handshake. - - """ - assert isinstance(event, Frame) - if event.opcode in DATA_OPCODES: - self.recv_messages.put(event) - - if event.opcode is Opcode.PONG: - self.acknowledge_pings(bytes(event.data)) - - def acknowledge_pings(self, data: bytes) -> None: - """ - Acknowledge pings when receiving a pong. - - """ - with self.protocol_mutex: - # Ignore unsolicited pong. - if data not in self.pong_waiters: - return - - pong_timestamp = time.monotonic() - - # Sending a pong for only the most recent ping is legal. - # Acknowledge all previous pings too in that case. - ping_id = None - ping_ids = [] - for ping_id, ( - pong_waiter, - ping_timestamp, - _ack_on_close, - ) in self.pong_waiters.items(): - ping_ids.append(ping_id) - pong_waiter.set() - if ping_id == data: - self.latency = pong_timestamp - ping_timestamp - break - else: - raise AssertionError("solicited pong not found in pings") - - # Remove acknowledged pings from self.pong_waiters. - for ping_id in ping_ids: - del self.pong_waiters[ping_id] - - def acknowledge_pending_pings(self) -> None: - """ - Acknowledge pending pings when the connection is closed. - - """ - assert self.protocol.state is CLOSED - - for pong_waiter, _ping_timestamp, ack_on_close in self.pong_waiters.values(): - if ack_on_close: - pong_waiter.set() - - self.pong_waiters.clear() - - def keepalive(self) -> None: - """ - Send a Ping frame and wait for a Pong frame at regular intervals. - - """ - assert self.ping_interval is not None - try: - while True: - # If self.ping_timeout > self.latency > self.ping_interval, - # pings will be sent immediately after receiving pongs. - # The period will be longer than self.ping_interval. - self.recv_events_thread.join(self.ping_interval - self.latency) - if not self.recv_events_thread.is_alive(): - break - - try: - pong_waiter = self.ping(ack_on_close=True) - except ConnectionClosed: - break - if self.debug: - self.logger.debug("% sent keepalive ping") - - if self.ping_timeout is not None: - # - if pong_waiter.wait(self.ping_timeout): - if self.debug: - self.logger.debug("% received keepalive pong") - else: - if self.debug: - self.logger.debug("- timed out waiting for keepalive pong") - with self.send_context(): - self.protocol.fail( - CloseCode.INTERNAL_ERROR, - "keepalive ping timeout", - ) - break - except Exception: - self.logger.error("keepalive ping failed", exc_info=True) - - def start_keepalive(self) -> None: - """ - Run :meth:`keepalive` in a thread, unless keepalive is disabled. - - """ - if self.ping_interval is not None: - # This thread is marked as daemon like self.recv_events_thread. - self.keepalive_thread = threading.Thread( - target=self.keepalive, - daemon=True, - ) - self.keepalive_thread.start() - - def recv_events(self) -> None: - """ - Read incoming data from the socket and process events. - - Run this method in a thread as long as the connection is alive. - - ``recv_events()`` exits immediately when the ``self.socket`` is closed. - - """ - try: - while True: - try: - with self.recv_flow_control: - if self.close_deadline is not None: - self.socket.settimeout(self.close_deadline.timeout()) - data = self.socket.recv(self.recv_bufsize) - except Exception as exc: - if self.debug: - self.logger.debug( - "! error while receiving data", - exc_info=True, - ) - # When the closing handshake is initiated by our side, - # recv() may block until send_context() closes the socket. - # In that case, send_context() already set recv_exc. - # Calling set_recv_exc() avoids overwriting it. - with self.protocol_mutex: - self.set_recv_exc(exc) - break - - if data == b"": - break - - # Acquire the connection lock. - with self.protocol_mutex: - # Feed incoming data to the protocol. - self.protocol.receive_data(data) - - # This isn't expected to raise an exception. - events = self.protocol.events_received() - - # Write outgoing data to the socket. - try: - self.send_data() - except Exception as exc: - if self.debug: - self.logger.debug( - "! error while sending data", - exc_info=True, - ) - # Similarly to the above, avoid overriding an exception - # set by send_context(), in case of a race condition - # i.e. send_context() closes the socket after recv() - # returns above but before send_data() calls send(). - self.set_recv_exc(exc) - break - - if self.protocol.close_expected(): - # If the connection is expected to close soon, set the - # close deadline based on the close timeout. - if self.close_deadline is None: - self.close_deadline = Deadline(self.close_timeout) - - # Unlock conn_mutex before processing events. Else, the - # application can't send messages in response to events. - - # If self.send_data raised an exception, then events are lost. - # Given that automatic responses write small amounts of data, - # this should be uncommon, so we don't handle the edge case. - - for event in events: - # This isn't expected to raise an exception. - self.process_event(event) - - # Breaking out of the while True: ... loop means that we believe - # that the socket doesn't work anymore. - with self.protocol_mutex: - # Feed the end of the data stream to the protocol. - self.protocol.receive_eof() - - # This isn't expected to raise an exception. - events = self.protocol.events_received() - - # There is no error handling because send_data() can only write - # the end of the data stream here and it handles errors itself. - self.send_data() - - # This code path is triggered when receiving an HTTP response - # without a Content-Length header. This is the only case where - # reading until EOF generates an event; all other events have - # a known length. Ignore for coverage measurement because tests - # are in test_client.py rather than test_connection.py. - for event in events: # pragma: no cover - # This isn't expected to raise an exception. - self.process_event(event) - - except Exception as exc: - # This branch should never run. It's a safety net in case of bugs. - self.logger.error("unexpected internal error", exc_info=True) - with self.protocol_mutex: - self.set_recv_exc(exc) - finally: - # This isn't expected to raise an exception. - self.close_socket() - - @contextlib.contextmanager - def send_context( - self, - *, - expected_state: State = OPEN, # CONNECTING during the opening handshake - ) -> Iterator[None]: - """ - Create a context for writing to the connection from user code. - - On entry, :meth:`send_context` acquires the connection lock and checks - that the connection is open; on exit, it writes outgoing data to the - socket:: - - with self.send_context(): - self.protocol.send_text(message.encode()) - - When the connection isn't open on entry, when the connection is expected - to close on exit, or when an unexpected error happens, terminating the - connection, :meth:`send_context` waits until the connection is closed - then raises :exc:`~websockets.exceptions.ConnectionClosed`. - - """ - # Should we wait until the connection is closed? - wait_for_close = False - # Should we close the socket and raise ConnectionClosed? - raise_close_exc = False - # What exception should we chain ConnectionClosed to? - original_exc: BaseException | None = None - - # Acquire the protocol lock. - with self.protocol_mutex: - if self.protocol.state is expected_state: - # Let the caller interact with the protocol. - try: - yield - except (ProtocolError, ConcurrencyError): - # The protocol state wasn't changed. Exit immediately. - raise - except Exception as exc: - self.logger.error("unexpected internal error", exc_info=True) - # This branch should never run. It's a safety net in case of - # bugs. Since we don't know what happened, we will close the - # connection and raise the exception to the caller. - wait_for_close = False - raise_close_exc = True - original_exc = exc - else: - # Check if the connection is expected to close soon. - if self.protocol.close_expected(): - wait_for_close = True - # If the connection is expected to close soon, set the - # close deadline based on the close timeout. - # Since we tested earlier that protocol.state was OPEN - # (or CONNECTING) and we didn't release protocol_mutex, - # it is certain that self.close_deadline is still None. - assert self.close_deadline is None - self.close_deadline = Deadline(self.close_timeout) - # Write outgoing data to the socket. - try: - self.send_data() - except Exception as exc: - if self.debug: - self.logger.debug( - "! error while sending data", - exc_info=True, - ) - # While the only expected exception here is OSError, - # other exceptions would be treated identically. - wait_for_close = False - raise_close_exc = True - original_exc = exc - - else: # self.protocol.state is not expected_state - # Minor layering violation: we assume that the connection - # will be closing soon if it isn't in the expected state. - wait_for_close = True - raise_close_exc = True - - # To avoid a deadlock, release the connection lock by exiting the - # context manager before waiting for recv_events() to terminate. - - # If the connection is expected to close soon and the close timeout - # elapses, close the socket to terminate the connection. - if wait_for_close: - if self.close_deadline is None: - timeout = self.close_timeout - else: - # Thread.join() returns immediately if timeout is negative. - timeout = self.close_deadline.timeout(raise_if_elapsed=False) - self.recv_events_thread.join(timeout) - - if self.recv_events_thread.is_alive(): - # There's no risk to overwrite another error because - # original_exc is never set when wait_for_close is True. - assert original_exc is None - original_exc = TimeoutError("timed out while closing connection") - # Set recv_exc before closing the socket in order to get - # proper exception reporting. - raise_close_exc = True - with self.protocol_mutex: - self.set_recv_exc(original_exc) - - # If an error occurred, close the socket to terminate the connection and - # raise an exception. - if raise_close_exc: - self.close_socket() - # Wait for the protocol state to be CLOSED before accessing close_exc. - self.recv_events_thread.join() - raise self.protocol.close_exc from original_exc - - def send_data(self) -> None: - """ - Send outgoing data. - - This method requires holding protocol_mutex. - - Raises: - OSError: When a socket operations fails. - - """ - assert self.protocol_mutex.locked() - for data in self.protocol.data_to_send(): - if data: - if self.close_deadline is not None: - self.socket.settimeout(self.close_deadline.timeout()) - self.socket.sendall(data) - else: - try: - self.socket.shutdown(socket.SHUT_WR) - except OSError: # socket already closed - pass - - def set_recv_exc(self, exc: BaseException | None) -> None: - """ - Set recv_exc, if not set yet. - - This method requires holding protocol_mutex. - - """ - assert self.protocol_mutex.locked() - if self.recv_exc is None: # pragma: no branch - self.recv_exc = exc - - def close_socket(self) -> None: - """ - Shutdown and close socket. Close message assembler. - - Calling close_socket() guarantees that recv_events() terminates. Indeed, - recv_events() may block only on socket.recv() or on recv_messages.put(). - - """ - # shutdown() is required to interrupt recv() on Linux. - try: - self.socket.shutdown(socket.SHUT_RDWR) - except OSError: - pass # socket is already closed - self.socket.close() - - # Calling protocol.receive_eof() is safe because it's idempotent. - # This guarantees that the protocol state becomes CLOSED. - with self.protocol_mutex: - self.protocol.receive_eof() - assert self.protocol.state is CLOSED - - # Abort recv() with a ConnectionClosed exception. - self.recv_messages.close() - - # Acknowledge pings sent with the ack_on_close option. - self.acknowledge_pending_pings() diff --git a/wip/SNIP/websockets/sync/messages.py b/wip/SNIP/websockets/sync/messages.py deleted file mode 100644 index c619e78..0000000 --- a/wip/SNIP/websockets/sync/messages.py +++ /dev/null @@ -1,345 +0,0 @@ -from __future__ import annotations - -import codecs -import queue -import threading -from typing import Any, Callable, Iterable, Iterator, Literal, overload - -from ..exceptions import ConcurrencyError -from ..frames import OP_BINARY, OP_CONT, OP_TEXT, Frame -from ..typing import Data -from .utils import Deadline - - -__all__ = ["Assembler"] - -UTF8Decoder = codecs.getincrementaldecoder("utf-8") - - -class Assembler: - """ - Assemble messages from frames. - - :class:`Assembler` expects only data frames. The stream of frames must - respect the protocol; if it doesn't, the behavior is undefined. - - Args: - pause: Called when the buffer of frames goes above the high water mark; - should pause reading from the network. - resume: Called when the buffer of frames goes below the low water mark; - should resume reading from the network. - - """ - - def __init__( - self, - high: int | None = None, - low: int | None = None, - pause: Callable[[], Any] = lambda: None, - resume: Callable[[], Any] = lambda: None, - ) -> None: - # Serialize reads and writes -- except for reads via synchronization - # primitives provided by the threading and queue modules. - self.mutex = threading.Lock() - - # Queue of incoming frames. - self.frames: queue.SimpleQueue[Frame | None] = queue.SimpleQueue() - - # We cannot put a hard limit on the size of the queue because a single - # call to Protocol.data_received() could produce thousands of frames, - # which must be buffered. Instead, we pause reading when the buffer goes - # above the high limit and we resume when it goes under the low limit. - if high is not None and low is None: - low = high // 4 - if high is None and low is not None: - high = low * 4 - if high is not None and low is not None: - if low < 0: - raise ValueError("low must be positive or equal to zero") - if high < low: - raise ValueError("high must be greater than or equal to low") - self.high, self.low = high, low - self.pause = pause - self.resume = resume - self.paused = False - - # This flag prevents concurrent calls to get() by user code. - self.get_in_progress = False - - # This flag marks the end of the connection. - self.closed = False - - def get_next_frame(self, timeout: float | None = None) -> Frame: - # Helper to factor out the logic for getting the next frame from the - # queue, while handling timeouts and reaching the end of the stream. - if self.closed: - try: - frame = self.frames.get(block=False) - except queue.Empty: - raise EOFError("stream of frames ended") from None - else: - try: - # Check for a frame that's already received if timeout <= 0. - # SimpleQueue.get() doesn't support negative timeout values. - if timeout is not None and timeout <= 0: - frame = self.frames.get(block=False) - else: - frame = self.frames.get(block=True, timeout=timeout) - except queue.Empty: - raise TimeoutError(f"timed out in {timeout:.1f}s") from None - if frame is None: - raise EOFError("stream of frames ended") - return frame - - def reset_queue(self, frames: Iterable[Frame]) -> None: - # Helper to put frames back into the queue after they were fetched. - # This happens only when the queue is empty. However, by the time - # we acquire self.mutex, put() may have added items in the queue. - # Therefore, we must handle the case where the queue is not empty. - frame: Frame | None - with self.mutex: - queued = [] - try: - while True: - queued.append(self.frames.get(block=False)) - except queue.Empty: - pass - for frame in frames: - self.frames.put(frame) - # This loop runs only when a race condition occurs. - for frame in queued: # pragma: no cover - self.frames.put(frame) - - # This overload structure is required to avoid the error: - # "parameter without a default follows parameter with a default" - - @overload - def get(self, timeout: float | None, decode: Literal[True]) -> str: ... - - @overload - def get(self, timeout: float | None, decode: Literal[False]) -> bytes: ... - - @overload - def get(self, timeout: float | None = None, *, decode: Literal[True]) -> str: ... - - @overload - def get(self, timeout: float | None = None, *, decode: Literal[False]) -> bytes: ... - - @overload - def get(self, timeout: float | None = None, decode: bool | None = None) -> Data: ... - - def get(self, timeout: float | None = None, decode: bool | None = None) -> Data: - """ - Read the next message. - - :meth:`get` returns a single :class:`str` or :class:`bytes`. - - If the message is fragmented, :meth:`get` waits until the last frame is - received, then it reassembles the message and returns it. To receive - messages frame by frame, use :meth:`get_iter` instead. - - Args: - timeout: If a timeout is provided and elapses before a complete - message is received, :meth:`get` raises :exc:`TimeoutError`. - decode: :obj:`False` disables UTF-8 decoding of text frames and - returns :class:`bytes`. :obj:`True` forces UTF-8 decoding of - binary frames and returns :class:`str`. - - Raises: - EOFError: If the stream of frames has ended. - UnicodeDecodeError: If a text frame contains invalid UTF-8. - ConcurrencyError: If two coroutines run :meth:`get` or - :meth:`get_iter` concurrently. - TimeoutError: If a timeout is provided and elapses before a - complete message is received. - - """ - with self.mutex: - if self.get_in_progress: - raise ConcurrencyError("get() or get_iter() is already running") - self.get_in_progress = True - - # Locking with get_in_progress prevents concurrent execution - # until get() fetches a complete message or times out. - - try: - deadline = Deadline(timeout) - - # First frame - frame = self.get_next_frame(deadline.timeout(raise_if_elapsed=False)) - with self.mutex: - self.maybe_resume() - assert frame.opcode is OP_TEXT or frame.opcode is OP_BINARY - if decode is None: - decode = frame.opcode is OP_TEXT - frames = [frame] - - # Following frames, for fragmented messages - while not frame.fin: - try: - frame = self.get_next_frame( - deadline.timeout(raise_if_elapsed=False) - ) - except TimeoutError: - # Put frames already received back into the queue - # so that future calls to get() can return them. - self.reset_queue(frames) - raise - with self.mutex: - self.maybe_resume() - assert frame.opcode is OP_CONT - frames.append(frame) - - finally: - self.get_in_progress = False - - data = b"".join(frame.data for frame in frames) - if decode: - return data.decode() - else: - return data - - @overload - def get_iter(self, decode: Literal[True]) -> Iterator[str]: ... - - @overload - def get_iter(self, decode: Literal[False]) -> Iterator[bytes]: ... - - @overload - def get_iter(self, decode: bool | None = None) -> Iterator[Data]: ... - - def get_iter(self, decode: bool | None = None) -> Iterator[Data]: - """ - Stream the next message. - - Iterating the return value of :meth:`get_iter` yields a :class:`str` or - :class:`bytes` for each frame in the message. - - The iterator must be fully consumed before calling :meth:`get_iter` or - :meth:`get` again. Else, :exc:`ConcurrencyError` is raised. - - This method only makes sense for fragmented messages. If messages aren't - fragmented, use :meth:`get` instead. - - Args: - decode: :obj:`False` disables UTF-8 decoding of text frames and - returns :class:`bytes`. :obj:`True` forces UTF-8 decoding of - binary frames and returns :class:`str`. - - Raises: - EOFError: If the stream of frames has ended. - UnicodeDecodeError: If a text frame contains invalid UTF-8. - ConcurrencyError: If two coroutines run :meth:`get` or - :meth:`get_iter` concurrently. - - """ - with self.mutex: - if self.get_in_progress: - raise ConcurrencyError("get() or get_iter() is already running") - self.get_in_progress = True - - # Locking with get_in_progress prevents concurrent execution - # until get_iter() fetches a complete message or times out. - - # If get_iter() raises an exception e.g. in decoder.decode(), - # get_in_progress remains set and the connection becomes unusable. - - # First frame - frame = self.get_next_frame() - with self.mutex: - self.maybe_resume() - assert frame.opcode is OP_TEXT or frame.opcode is OP_BINARY - if decode is None: - decode = frame.opcode is OP_TEXT - if decode: - decoder = UTF8Decoder() - yield decoder.decode(frame.data, frame.fin) - else: - yield frame.data - - # Following frames, for fragmented messages - while not frame.fin: - frame = self.get_next_frame() - with self.mutex: - self.maybe_resume() - assert frame.opcode is OP_CONT - if decode: - yield decoder.decode(frame.data, frame.fin) - else: - yield frame.data - - self.get_in_progress = False - - def put(self, frame: Frame) -> None: - """ - Add ``frame`` to the next message. - - Raises: - EOFError: If the stream of frames has ended. - - """ - with self.mutex: - if self.closed: - raise EOFError("stream of frames ended") - - self.frames.put(frame) - self.maybe_pause() - - # put() and get/get_iter() call maybe_pause() and maybe_resume() while - # holding self.mutex. This guarantees that the calls interleave properly. - # Specifically, it prevents a race condition where maybe_resume() would - # run before maybe_pause(), leaving the connection incorrectly paused. - - # A race condition is possible when get/get_iter() call self.frames.get() - # without holding self.mutex. However, it's harmless — and even beneficial! - # It can only result in popping an item from the queue before maybe_resume() - # runs and skipping a pause() - resume() cycle that would otherwise occur. - - def maybe_pause(self) -> None: - """Pause the writer if queue is above the high water mark.""" - # Skip if flow control is disabled - if self.high is None: - return - - assert self.mutex.locked() - - # Check for "> high" to support high = 0 - if self.frames.qsize() > self.high and not self.paused: - self.paused = True - self.pause() - - def maybe_resume(self) -> None: - """Resume the writer if queue is below the low water mark.""" - # Skip if flow control is disabled - if self.low is None: - return - - assert self.mutex.locked() - - # Check for "<= low" to support low = 0 - if self.frames.qsize() <= self.low and self.paused: - self.paused = False - self.resume() - - def close(self) -> None: - """ - End the stream of frames. - - Calling :meth:`close` concurrently with :meth:`get`, :meth:`get_iter`, - or :meth:`put` is safe. They will raise :exc:`EOFError`. - - """ - with self.mutex: - if self.closed: - return - - self.closed = True - - if self.get_in_progress: - # Unblock get() or get_iter(). - self.frames.put(None) - - if self.paused: - # Unblock recv_events(). - self.paused = False - self.resume() diff --git a/wip/SNIP/websockets/sync/router.py b/wip/SNIP/websockets/sync/router.py deleted file mode 100644 index 5572c42..0000000 --- a/wip/SNIP/websockets/sync/router.py +++ /dev/null @@ -1,192 +0,0 @@ -from __future__ import annotations - -import http -import ssl as ssl_module -import urllib.parse -from typing import Any, Callable, Literal - -from werkzeug.exceptions import NotFound -from werkzeug.routing import Map, RequestRedirect - -from ..http11 import Request, Response -from .server import Server, ServerConnection, serve - - -__all__ = ["route", "unix_route", "Router"] - - -class Router: - """WebSocket router supporting :func:`route`.""" - - def __init__( - self, - url_map: Map, - server_name: str | None = None, - url_scheme: str = "ws", - ) -> None: - self.url_map = url_map - self.server_name = server_name - self.url_scheme = url_scheme - for rule in self.url_map.iter_rules(): - rule.websocket = True - - def get_server_name(self, connection: ServerConnection, request: Request) -> str: - if self.server_name is None: - return request.headers["Host"] - else: - return self.server_name - - def redirect(self, connection: ServerConnection, url: str) -> Response: - response = connection.respond(http.HTTPStatus.FOUND, f"Found at {url}") - response.headers["Location"] = url - return response - - def not_found(self, connection: ServerConnection) -> Response: - return connection.respond(http.HTTPStatus.NOT_FOUND, "Not Found") - - def route_request( - self, connection: ServerConnection, request: Request - ) -> Response | None: - """Route incoming request.""" - url_map_adapter = self.url_map.bind( - server_name=self.get_server_name(connection, request), - url_scheme=self.url_scheme, - ) - try: - parsed = urllib.parse.urlparse(request.path) - handler, kwargs = url_map_adapter.match( - path_info=parsed.path, - query_args=parsed.query, - ) - except RequestRedirect as redirect: - return self.redirect(connection, redirect.new_url) - except NotFound: - return self.not_found(connection) - connection.handler, connection.handler_kwargs = handler, kwargs - return None - - def handler(self, connection: ServerConnection) -> None: - """Handle a connection.""" - return connection.handler(connection, **connection.handler_kwargs) - - -def route( - url_map: Map, - *args: Any, - server_name: str | None = None, - ssl: ssl_module.SSLContext | Literal[True] | None = None, - create_router: type[Router] | None = None, - **kwargs: Any, -) -> Server: - """ - Create a WebSocket server dispatching connections to different handlers. - - This feature requires the third-party library `werkzeug`_: - - .. code-block:: console - - $ pip install werkzeug - - .. _werkzeug: https://werkzeug.palletsprojects.com/ - - :func:`route` accepts the same arguments as - :func:`~websockets.sync.server.serve`, except as described below. - - The first argument is a :class:`werkzeug.routing.Map` that maps URL patterns - to connection handlers. In addition to the connection, handlers receive - parameters captured in the URL as keyword arguments. - - Here's an example:: - - - from websockets.sync.router import route - from werkzeug.routing import Map, Rule - - def channel_handler(websocket, channel_id): - ... - - url_map = Map([ - Rule("/channel/", endpoint=channel_handler), - ... - ]) - - with route(url_map, ...) as server: - server.serve_forever() - - Refer to the documentation of :mod:`werkzeug.routing` for details. - - If you define redirects with ``Rule(..., redirect_to=...)`` in the URL map, - when the server runs behind a reverse proxy that modifies the ``Host`` - header or terminates TLS, you need additional configuration: - - * Set ``server_name`` to the name of the server as seen by clients. When not - provided, websockets uses the value of the ``Host`` header. - - * Set ``ssl=True`` to generate ``wss://`` URIs without actually enabling - TLS. Under the hood, this bind the URL map with a ``url_scheme`` of - ``wss://`` instead of ``ws://``. - - There is no need to specify ``websocket=True`` in each rule. It is added - automatically. - - Args: - url_map: Mapping of URL patterns to connection handlers. - server_name: Name of the server as seen by clients. If :obj:`None`, - websockets uses the value of the ``Host`` header. - ssl: Configuration for enabling TLS on the connection. Set it to - :obj:`True` if a reverse proxy terminates TLS connections. - create_router: Factory for the :class:`Router` dispatching requests to - handlers. Set it to a wrapper or a subclass to customize routing. - - """ - url_scheme = "ws" if ssl is None else "wss" - if ssl is not True and ssl is not None: - kwargs["ssl"] = ssl - - if create_router is None: - create_router = Router - - router = create_router(url_map, server_name, url_scheme) - - _process_request: ( - Callable[ - [ServerConnection, Request], - Response | None, - ] - | None - ) = kwargs.pop("process_request", None) - if _process_request is None: - process_request: Callable[ - [ServerConnection, Request], - Response | None, - ] = router.route_request - else: - - def process_request( - connection: ServerConnection, request: Request - ) -> Response | None: - response = _process_request(connection, request) - if response is not None: - return response - return router.route_request(connection, request) - - return serve(router.handler, *args, process_request=process_request, **kwargs) - - -def unix_route( - url_map: Map, - path: str | None = None, - **kwargs: Any, -) -> Server: - """ - Create a WebSocket Unix server dispatching connections to different handlers. - - :func:`unix_route` combines the behaviors of :func:`route` and - :func:`~websockets.sync.server.unix_serve`. - - Args: - url_map: Mapping of URL patterns to connection handlers. - path: File system path to the Unix socket. - - """ - return route(url_map, unix=True, path=path, **kwargs) diff --git a/wip/SNIP/websockets/sync/server.py b/wip/SNIP/websockets/sync/server.py deleted file mode 100644 index efb40a7..0000000 --- a/wip/SNIP/websockets/sync/server.py +++ /dev/null @@ -1,763 +0,0 @@ -from __future__ import annotations - -import hmac -import http -import logging -import os -import re -import selectors -import socket -import ssl as ssl_module -import sys -import threading -import warnings -from collections.abc import Iterable, Sequence -from types import TracebackType -from typing import Any, Callable, Mapping, cast - -from ..exceptions import InvalidHeader -from ..extensions.base import ServerExtensionFactory -from ..extensions.permessage_deflate import enable_server_permessage_deflate -from ..frames import CloseCode -from ..headers import ( - build_www_authenticate_basic, - parse_authorization_basic, - validate_subprotocols, -) -from ..http11 import SERVER, Request, Response -from ..protocol import CONNECTING, OPEN, Event -from ..server import ServerProtocol -from ..typing import LoggerLike, Origin, StatusLike, Subprotocol -from .connection import Connection -from .utils import Deadline - - -__all__ = ["serve", "unix_serve", "ServerConnection", "Server", "basic_auth"] - - -class ServerConnection(Connection): - """ - :mod:`threading` implementation of a WebSocket server connection. - - :class:`ServerConnection` provides :meth:`recv` and :meth:`send` methods for - receiving and sending messages. - - It supports iteration to receive messages:: - - for message in websocket: - process(message) - - The iterator exits normally when the connection is closed with close code - 1000 (OK) or 1001 (going away) or without a close code. It raises a - :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is - closed with any other code. - - The ``ping_interval``, ``ping_timeout``, ``close_timeout``, and - ``max_queue`` arguments have the same meaning as in :func:`serve`. - - Args: - socket: Socket connected to a WebSocket client. - protocol: Sans-I/O connection. - - """ - - def __init__( - self, - socket: socket.socket, - protocol: ServerProtocol, - *, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = 10, - max_queue: int | None | tuple[int | None, int | None] = 16, - ) -> None: - self.protocol: ServerProtocol - self.request_rcvd = threading.Event() - super().__init__( - socket, - protocol, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - close_timeout=close_timeout, - max_queue=max_queue, - ) - self.username: str # see basic_auth() - self.handler: Callable[[ServerConnection], None] # see route() - self.handler_kwargs: Mapping[str, Any] # see route() - - def respond(self, status: StatusLike, text: str) -> Response: - """ - Create a plain text HTTP response. - - ``process_request`` and ``process_response`` may call this method to - return an HTTP response instead of performing the WebSocket opening - handshake. - - You can modify the response before returning it, for example by changing - HTTP headers. - - Args: - status: HTTP status code. - text: HTTP response body; it will be encoded to UTF-8. - - Returns: - HTTP response to send to the client. - - """ - return self.protocol.reject(status, text) - - def handshake( - self, - process_request: ( - Callable[ - [ServerConnection, Request], - Response | None, - ] - | None - ) = None, - process_response: ( - Callable[ - [ServerConnection, Request, Response], - Response | None, - ] - | None - ) = None, - server_header: str | None = SERVER, - timeout: float | None = None, - ) -> None: - """ - Perform the opening handshake. - - """ - if not self.request_rcvd.wait(timeout): - raise TimeoutError("timed out while waiting for handshake request") - - if self.request is not None: - with self.send_context(expected_state=CONNECTING): - response = None - - if process_request is not None: - try: - response = process_request(self, self.request) - except Exception as exc: - self.protocol.handshake_exc = exc - response = self.protocol.reject( - http.HTTPStatus.INTERNAL_SERVER_ERROR, - ( - "Failed to open a WebSocket connection.\n" - "See server log for more information.\n" - ), - ) - - if response is None: - self.response = self.protocol.accept(self.request) - else: - self.response = response - - if server_header: - self.response.headers["Server"] = server_header - - response = None - - if process_response is not None: - try: - response = process_response(self, self.request, self.response) - except Exception as exc: - self.protocol.handshake_exc = exc - response = self.protocol.reject( - http.HTTPStatus.INTERNAL_SERVER_ERROR, - ( - "Failed to open a WebSocket connection.\n" - "See server log for more information.\n" - ), - ) - - if response is not None: - self.response = response - - self.protocol.send_response(self.response) - - # self.protocol.handshake_exc is set when the connection is lost before - # receiving a request, when the request cannot be parsed, or when the - # handshake fails, including when process_request or process_response - # raises an exception. - - # It isn't set when process_request or process_response sends an HTTP - # response that rejects the handshake. - - if self.protocol.handshake_exc is not None: - raise self.protocol.handshake_exc - - def process_event(self, event: Event) -> None: - """ - Process one incoming event. - - """ - # First event - handshake request. - if self.request is None: - assert isinstance(event, Request) - self.request = event - self.request_rcvd.set() - # Later events - frames. - else: - super().process_event(event) - - def recv_events(self) -> None: - """ - Read incoming data from the socket and process events. - - """ - try: - super().recv_events() - finally: - # If the connection is closed during the handshake, unblock it. - self.request_rcvd.set() - - -class Server: - """ - WebSocket server returned by :func:`serve`. - - This class mirrors the API of :class:`~socketserver.BaseServer`, notably the - :meth:`~socketserver.BaseServer.serve_forever` and - :meth:`~socketserver.BaseServer.shutdown` methods, as well as the context - manager protocol. - - Args: - socket: Server socket listening for new connections. - handler: Handler for one connection. Receives the socket and address - returned by :meth:`~socket.socket.accept`. - logger: Logger for this server. - It defaults to ``logging.getLogger("websockets.server")``. - See the :doc:`logging guide <../../topics/logging>` for details. - - """ - - def __init__( - self, - socket: socket.socket, - handler: Callable[[socket.socket, Any], None], - logger: LoggerLike | None = None, - ) -> None: - self.socket = socket - self.handler = handler - if logger is None: - logger = logging.getLogger("websockets.server") - self.logger = logger - if sys.platform != "win32": - self.shutdown_watcher, self.shutdown_notifier = os.pipe() - - def serve_forever(self) -> None: - """ - See :meth:`socketserver.BaseServer.serve_forever`. - - This method doesn't return. Calling :meth:`shutdown` from another thread - stops the server. - - Typical use:: - - with serve(...) as server: - server.serve_forever() - - """ - poller = selectors.DefaultSelector() - try: - poller.register(self.socket, selectors.EVENT_READ) - except ValueError: # pragma: no cover - # If shutdown() is called before poller.register(), - # the socket is closed and poller.register() raises - # ValueError: Invalid file descriptor: -1 - return - if sys.platform != "win32": - poller.register(self.shutdown_watcher, selectors.EVENT_READ) - - while True: - poller.select() - try: - # If the socket is closed, this will raise an exception and exit - # the loop. So we don't need to check the return value of select(). - sock, addr = self.socket.accept() - except OSError: - break - # Since there isn't a mechanism for tracking connections and waiting - # for them to terminate, we cannot use daemon threads, or else all - # connections would be terminate brutally when closing the server. - thread = threading.Thread(target=self.handler, args=(sock, addr)) - thread.start() - - def shutdown(self) -> None: - """ - See :meth:`socketserver.BaseServer.shutdown`. - - """ - self.socket.close() - if sys.platform != "win32": - os.write(self.shutdown_notifier, b"x") - - def fileno(self) -> int: - """ - See :meth:`socketserver.BaseServer.fileno`. - - """ - return self.socket.fileno() - - def __enter__(self) -> Server: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - self.shutdown() - - -def __getattr__(name: str) -> Any: - if name == "WebSocketServer": - warnings.warn( # deprecated in 13.0 - 2024-08-20 - "WebSocketServer was renamed to Server", - DeprecationWarning, - ) - return Server - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - - -def serve( - handler: Callable[[ServerConnection], None], - host: str | None = None, - port: int | None = None, - *, - # TCP/TLS - sock: socket.socket | None = None, - ssl: ssl_module.SSLContext | None = None, - # WebSocket - origins: Sequence[Origin | re.Pattern[str] | None] | None = None, - extensions: Sequence[ServerExtensionFactory] | None = None, - subprotocols: Sequence[Subprotocol] | None = None, - select_subprotocol: ( - Callable[ - [ServerConnection, Sequence[Subprotocol]], - Subprotocol | None, - ] - | None - ) = None, - compression: str | None = "deflate", - # HTTP - process_request: ( - Callable[ - [ServerConnection, Request], - Response | None, - ] - | None - ) = None, - process_response: ( - Callable[ - [ServerConnection, Request, Response], - Response | None, - ] - | None - ) = None, - server_header: str | None = SERVER, - # Timeouts - open_timeout: float | None = 10, - ping_interval: float | None = 20, - ping_timeout: float | None = 20, - close_timeout: float | None = 10, - # Limits - max_size: int | None = 2**20, - max_queue: int | None | tuple[int | None, int | None] = 16, - # Logging - logger: LoggerLike | None = None, - # Escape hatch for advanced customization - create_connection: type[ServerConnection] | None = None, - **kwargs: Any, -) -> Server: - """ - Create a WebSocket server listening on ``host`` and ``port``. - - Whenever a client connects, the server creates a :class:`ServerConnection`, - performs the opening handshake, and delegates to the ``handler``. - - The handler receives the :class:`ServerConnection` instance, which you can - use to send and receive messages. - - Once the handler completes, either normally or with an exception, the server - performs the closing handshake and closes the connection. - - This function returns a :class:`Server` whose API mirrors - :class:`~socketserver.BaseServer`. Treat it as a context manager to ensure - that it will be closed and call :meth:`~Server.serve_forever` to serve - requests:: - - from websockets.sync.server import serve - - def handler(websocket): - ... - - with serve(handler, ...) as server: - server.serve_forever() - - Args: - handler: Connection handler. It receives the WebSocket connection, - which is a :class:`ServerConnection`, in argument. - host: Network interfaces the server binds to. - See :func:`~socket.create_server` for details. - port: TCP port the server listens on. - See :func:`~socket.create_server` for details. - sock: Preexisting TCP socket. ``sock`` replaces ``host`` and ``port``. - You may call :func:`socket.create_server` to create a suitable TCP - socket. - ssl: Configuration for enabling TLS on the connection. - origins: Acceptable values of the ``Origin`` header, for defending - against Cross-Site WebSocket Hijacking attacks. Values can be - :class:`str` to test for an exact match or regular expressions - compiled by :func:`re.compile` to test against a pattern. Include - :obj:`None` in the list if the lack of an origin is acceptable. - extensions: List of supported extensions, in order in which they - should be negotiated and run. - subprotocols: List of supported subprotocols, in order of decreasing - preference. - select_subprotocol: Callback for selecting a subprotocol among - those supported by the client and the server. It receives a - :class:`ServerConnection` (not a - :class:`~websockets.server.ServerProtocol`!) instance and a list of - subprotocols offered by the client. Other than the first argument, - it has the same behavior as the - :meth:`ServerProtocol.select_subprotocol - ` method. - compression: The "permessage-deflate" extension is enabled by default. - Set ``compression`` to :obj:`None` to disable it. See the - :doc:`compression guide <../../topics/compression>` for details. - process_request: Intercept the request during the opening handshake. - Return an HTTP response to force the response. Return :obj:`None` to - continue normally. When you force an HTTP 101 Continue response, the - handshake is successful. Else, the connection is aborted. - process_response: Intercept the response during the opening handshake. - Modify the response or return a new HTTP response to force the - response. Return :obj:`None` to continue normally. When you force an - HTTP 101 Continue response, the handshake is successful. Else, the - connection is aborted. - server_header: Value of the ``Server`` response header. - It defaults to ``"Python/x.y.z websockets/X.Y"``. Setting it to - :obj:`None` removes the header. - open_timeout: Timeout for opening connections in seconds. - :obj:`None` disables the timeout. - ping_interval: Interval between keepalive pings in seconds. - :obj:`None` disables keepalive. - ping_timeout: Timeout for keepalive pings in seconds. - :obj:`None` disables timeouts. - close_timeout: Timeout for closing connections in seconds. - :obj:`None` disables the timeout. - max_size: Maximum size of incoming messages in bytes. - :obj:`None` disables the limit. - max_queue: High-water mark of the buffer where frames are received. - It defaults to 16 frames. The low-water mark defaults to ``max_queue - // 4``. You may pass a ``(high, low)`` tuple to set the high-water - and low-water marks. If you want to disable flow control entirely, - you may set it to ``None``, although that's a bad idea. - logger: Logger for this server. - It defaults to ``logging.getLogger("websockets.server")``. See the - :doc:`logging guide <../../topics/logging>` for details. - create_connection: Factory for the :class:`ServerConnection` managing - the connection. Set it to a wrapper or a subclass to customize - connection handling. - - Any other keyword arguments are passed to :func:`~socket.create_server`. - - """ - - # Process parameters - - # Backwards compatibility: ssl used to be called ssl_context. - if ssl is None and "ssl_context" in kwargs: - ssl = kwargs.pop("ssl_context") - warnings.warn( # deprecated in 13.0 - 2024-08-20 - "ssl_context was renamed to ssl", - DeprecationWarning, - ) - - if subprotocols is not None: - validate_subprotocols(subprotocols) - - if compression == "deflate": - extensions = enable_server_permessage_deflate(extensions) - elif compression is not None: - raise ValueError(f"unsupported compression: {compression}") - - if create_connection is None: - create_connection = ServerConnection - - # Bind socket and listen - - # Private APIs for unix_connect() - unix: bool = kwargs.pop("unix", False) - path: str | None = kwargs.pop("path", None) - - if sock is None: - if unix: - if path is None: - raise ValueError("missing path argument") - kwargs.setdefault("family", socket.AF_UNIX) - sock = socket.create_server(path, **kwargs) - else: - sock = socket.create_server((host, port), **kwargs) - else: - if path is not None: - raise ValueError("path and sock arguments are incompatible") - - # Initialize TLS wrapper - - if ssl is not None: - sock = ssl.wrap_socket( - sock, - server_side=True, - # Delay TLS handshake until after we set a timeout on the socket. - do_handshake_on_connect=False, - ) - - # Define request handler - - def conn_handler(sock: socket.socket, addr: Any) -> None: - # Calculate timeouts on the TLS and WebSocket handshakes. - # The TLS timeout must be set on the socket, then removed - # to avoid conflicting with the WebSocket timeout in handshake(). - deadline = Deadline(open_timeout) - - try: - # Disable Nagle algorithm - - if not unix: - sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True) - - # Perform TLS handshake - - if ssl is not None: - sock.settimeout(deadline.timeout()) - # mypy cannot figure this out - assert isinstance(sock, ssl_module.SSLSocket) - sock.do_handshake() - sock.settimeout(None) - - # Create a closure to give select_subprotocol access to connection. - protocol_select_subprotocol: ( - Callable[ - [ServerProtocol, Sequence[Subprotocol]], - Subprotocol | None, - ] - | None - ) = None - if select_subprotocol is not None: - - def protocol_select_subprotocol( - protocol: ServerProtocol, - subprotocols: Sequence[Subprotocol], - ) -> Subprotocol | None: - # mypy doesn't know that select_subprotocol is immutable. - assert select_subprotocol is not None - # Ensure this function is only used in the intended context. - assert protocol is connection.protocol - return select_subprotocol(connection, subprotocols) - - # Initialize WebSocket protocol - - protocol = ServerProtocol( - origins=origins, - extensions=extensions, - subprotocols=subprotocols, - select_subprotocol=protocol_select_subprotocol, - max_size=max_size, - logger=logger, - ) - - # Initialize WebSocket connection - - assert create_connection is not None # help mypy - connection = create_connection( - sock, - protocol, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - close_timeout=close_timeout, - max_queue=max_queue, - ) - except Exception: - sock.close() - return - - try: - try: - connection.handshake( - process_request, - process_response, - server_header, - deadline.timeout(), - ) - except TimeoutError: - connection.close_socket() - connection.recv_events_thread.join() - return - except Exception: - connection.logger.error("opening handshake failed", exc_info=True) - connection.close_socket() - connection.recv_events_thread.join() - return - - assert connection.protocol.state is OPEN - try: - connection.start_keepalive() - handler(connection) - except Exception: - connection.logger.error("connection handler failed", exc_info=True) - connection.close(CloseCode.INTERNAL_ERROR) - else: - connection.close() - - except Exception: # pragma: no cover - # Don't leak sockets on unexpected errors. - sock.close() - - # Initialize server - - return Server(sock, conn_handler, logger) - - -def unix_serve( - handler: Callable[[ServerConnection], None], - path: str | None = None, - **kwargs: Any, -) -> Server: - """ - Create a WebSocket server listening on a Unix socket. - - This function accepts the same keyword arguments as :func:`serve`. - - It's only available on Unix. - - It's useful for deploying a server behind a reverse proxy such as nginx. - - Args: - handler: Connection handler. It receives the WebSocket connection, - which is a :class:`ServerConnection`, in argument. - path: File system path to the Unix socket. - - """ - return serve(handler, unix=True, path=path, **kwargs) - - -def is_credentials(credentials: Any) -> bool: - try: - username, password = credentials - except (TypeError, ValueError): - return False - else: - return isinstance(username, str) and isinstance(password, str) - - -def basic_auth( - realm: str = "", - credentials: tuple[str, str] | Iterable[tuple[str, str]] | None = None, - check_credentials: Callable[[str, str], bool] | None = None, -) -> Callable[[ServerConnection, Request], Response | None]: - """ - Factory for ``process_request`` to enforce HTTP Basic Authentication. - - :func:`basic_auth` is designed to integrate with :func:`serve` as follows:: - - from websockets.sync.server import basic_auth, serve - - with serve( - ..., - process_request=basic_auth( - realm="my dev server", - credentials=("hello", "iloveyou"), - ), - ): - - If authentication succeeds, the connection's ``username`` attribute is set. - If it fails, the server responds with an HTTP 401 Unauthorized status. - - One of ``credentials`` or ``check_credentials`` must be provided; not both. - - Args: - realm: Scope of protection. It should contain only ASCII characters - because the encoding of non-ASCII characters is undefined. Refer to - section 2.2 of :rfc:`7235` for details. - credentials: Hard coded authorized credentials. It can be a - ``(username, password)`` pair or a list of such pairs. - check_credentials: Function that verifies credentials. - It receives ``username`` and ``password`` arguments and returns - whether they're valid. - Raises: - TypeError: If ``credentials`` or ``check_credentials`` is wrong. - ValueError: If ``credentials`` and ``check_credentials`` are both - provided or both not provided. - - """ - if (credentials is None) == (check_credentials is None): - raise ValueError("provide either credentials or check_credentials") - - if credentials is not None: - if is_credentials(credentials): - credentials_list = [cast(tuple[str, str], credentials)] - elif isinstance(credentials, Iterable): - credentials_list = list(cast(Iterable[tuple[str, str]], credentials)) - if not all(is_credentials(item) for item in credentials_list): - raise TypeError(f"invalid credentials argument: {credentials}") - else: - raise TypeError(f"invalid credentials argument: {credentials}") - - credentials_dict = dict(credentials_list) - - def check_credentials(username: str, password: str) -> bool: - try: - expected_password = credentials_dict[username] - except KeyError: - return False - return hmac.compare_digest(expected_password, password) - - assert check_credentials is not None # help mypy - - def process_request( - connection: ServerConnection, - request: Request, - ) -> Response | None: - """ - Perform HTTP Basic Authentication. - - If it succeeds, set the connection's ``username`` attribute and return - :obj:`None`. If it fails, return an HTTP 401 Unauthorized responss. - - """ - try: - authorization = request.headers["Authorization"] - except KeyError: - response = connection.respond( - http.HTTPStatus.UNAUTHORIZED, - "Missing credentials\n", - ) - response.headers["WWW-Authenticate"] = build_www_authenticate_basic(realm) - return response - - try: - username, password = parse_authorization_basic(authorization) - except InvalidHeader: - response = connection.respond( - http.HTTPStatus.UNAUTHORIZED, - "Unsupported credentials\n", - ) - response.headers["WWW-Authenticate"] = build_www_authenticate_basic(realm) - return response - - if not check_credentials(username, password): - response = connection.respond( - http.HTTPStatus.UNAUTHORIZED, - "Invalid credentials\n", - ) - response.headers["WWW-Authenticate"] = build_www_authenticate_basic(realm) - return response - - connection.username = username - return None - - return process_request diff --git a/wip/SNIP/websockets/sync/utils.py b/wip/SNIP/websockets/sync/utils.py deleted file mode 100644 index 00bce2c..0000000 --- a/wip/SNIP/websockets/sync/utils.py +++ /dev/null @@ -1,45 +0,0 @@ -from __future__ import annotations - -import time - - -__all__ = ["Deadline"] - - -class Deadline: - """ - Manage timeouts across multiple steps. - - Args: - timeout: Time available in seconds or :obj:`None` if there is no limit. - - """ - - def __init__(self, timeout: float | None) -> None: - self.deadline: float | None - if timeout is None: - self.deadline = None - else: - self.deadline = time.monotonic() + timeout - - def timeout(self, *, raise_if_elapsed: bool = True) -> float | None: - """ - Calculate a timeout from a deadline. - - Args: - raise_if_elapsed: Whether to raise :exc:`TimeoutError` - if the deadline lapsed. - - Raises: - TimeoutError: If the deadline lapsed. - - Returns: - Time left in seconds or :obj:`None` if there is no limit. - - """ - if self.deadline is None: - return None - timeout = self.deadline - time.monotonic() - if raise_if_elapsed and timeout <= 0: - raise TimeoutError("timed out") - return timeout diff --git a/wip/SNIP/websockets/typing.py b/wip/SNIP/websockets/typing.py deleted file mode 100644 index ab7ddd3..0000000 --- a/wip/SNIP/websockets/typing.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import annotations - -import http -import logging -from typing import TYPE_CHECKING, Any, NewType, Optional, Sequence, Union - - -__all__ = [ - "Data", - "LoggerLike", - "StatusLike", - "Origin", - "Subprotocol", - "ExtensionName", - "ExtensionParameter", -] - - -# Public types used in the signature of public APIs - -# Change to str | bytes when dropping Python < 3.10. -Data = Union[str, bytes] -"""Types supported in a WebSocket message: -:class:`str` for a Text_ frame, :class:`bytes` for a Binary_. - -.. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 -.. _Binary : https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - -""" - - -# Change to logging.Logger | ... when dropping Python < 3.10. -if TYPE_CHECKING: - LoggerLike = Union[logging.Logger, logging.LoggerAdapter[Any]] - """Types accepted where a :class:`~logging.Logger` is expected.""" -else: # remove this branch when dropping support for Python < 3.11 - LoggerLike = Union[logging.Logger, logging.LoggerAdapter] - """Types accepted where a :class:`~logging.Logger` is expected.""" - - -# Change to http.HTTPStatus | int when dropping Python < 3.10. -StatusLike = Union[http.HTTPStatus, int] -""" -Types accepted where an :class:`~http.HTTPStatus` is expected.""" - - -Origin = NewType("Origin", str) -"""Value of a ``Origin`` header.""" - - -Subprotocol = NewType("Subprotocol", str) -"""Subprotocol in a ``Sec-WebSocket-Protocol`` header.""" - - -ExtensionName = NewType("ExtensionName", str) -"""Name of a WebSocket extension.""" - -# Change to tuple[str, str | None] when dropping Python < 3.10. -ExtensionParameter = tuple[str, Optional[str]] -"""Parameter of a WebSocket extension.""" - - -# Private types - -ExtensionHeader = tuple[ExtensionName, Sequence[ExtensionParameter]] -"""Extension in a ``Sec-WebSocket-Extensions`` header.""" - - -ConnectionOption = NewType("ConnectionOption", str) -"""Connection option in a ``Connection`` header.""" - - -UpgradeProtocol = NewType("UpgradeProtocol", str) -"""Upgrade protocol in an ``Upgrade`` header.""" diff --git a/wip/SNIP/websockets/uri.py b/wip/SNIP/websockets/uri.py deleted file mode 100644 index b925b99..0000000 --- a/wip/SNIP/websockets/uri.py +++ /dev/null @@ -1,225 +0,0 @@ -from __future__ import annotations - -import dataclasses -import urllib.parse -import urllib.request - -from .exceptions import InvalidProxy, InvalidURI - - -__all__ = ["parse_uri", "WebSocketURI"] - - -# All characters from the gen-delims and sub-delims sets in RFC 3987. -DELIMS = ":/?#[]@!$&'()*+,;=" - - -@dataclasses.dataclass -class WebSocketURI: - """ - WebSocket URI. - - Attributes: - secure: :obj:`True` for a ``wss`` URI, :obj:`False` for a ``ws`` URI. - host: Normalized to lower case. - port: Always set even if it's the default. - path: May be empty. - query: May be empty if the URI doesn't include a query component. - username: Available when the URI contains `User Information`_. - password: Available when the URI contains `User Information`_. - - .. _User Information: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1 - - """ - - secure: bool - host: str - port: int - path: str - query: str - username: str | None = None - password: str | None = None - - @property - def resource_name(self) -> str: - if self.path: - resource_name = self.path - else: - resource_name = "/" - if self.query: - resource_name += "?" + self.query - return resource_name - - @property - def user_info(self) -> tuple[str, str] | None: - if self.username is None: - return None - assert self.password is not None - return (self.username, self.password) - - -def parse_uri(uri: str) -> WebSocketURI: - """ - Parse and validate a WebSocket URI. - - Args: - uri: WebSocket URI. - - Returns: - Parsed WebSocket URI. - - Raises: - InvalidURI: If ``uri`` isn't a valid WebSocket URI. - - """ - parsed = urllib.parse.urlparse(uri) - if parsed.scheme not in ["ws", "wss"]: - raise InvalidURI(uri, "scheme isn't ws or wss") - if parsed.hostname is None: - raise InvalidURI(uri, "hostname isn't provided") - if parsed.fragment != "": - raise InvalidURI(uri, "fragment identifier is meaningless") - - secure = parsed.scheme == "wss" - host = parsed.hostname - port = parsed.port or (443 if secure else 80) - path = parsed.path - query = parsed.query - username = parsed.username - password = parsed.password - # urllib.parse.urlparse accepts URLs with a username but without a - # password. This doesn't make sense for HTTP Basic Auth credentials. - if username is not None and password is None: - raise InvalidURI(uri, "username provided without password") - - try: - uri.encode("ascii") - except UnicodeEncodeError: - # Input contains non-ASCII characters. - # It must be an IRI. Convert it to a URI. - host = host.encode("idna").decode() - path = urllib.parse.quote(path, safe=DELIMS) - query = urllib.parse.quote(query, safe=DELIMS) - if username is not None: - assert password is not None - username = urllib.parse.quote(username, safe=DELIMS) - password = urllib.parse.quote(password, safe=DELIMS) - - return WebSocketURI(secure, host, port, path, query, username, password) - - -@dataclasses.dataclass -class Proxy: - """ - Proxy. - - Attributes: - scheme: ``"socks5h"``, ``"socks5"``, ``"socks4a"``, ``"socks4"``, - ``"https"``, or ``"http"``. - host: Normalized to lower case. - port: Always set even if it's the default. - username: Available when the proxy address contains `User Information`_. - password: Available when the proxy address contains `User Information`_. - - .. _User Information: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1 - - """ - - scheme: str - host: str - port: int - username: str | None = None - password: str | None = None - - @property - def user_info(self) -> tuple[str, str] | None: - if self.username is None: - return None - assert self.password is not None - return (self.username, self.password) - - -def parse_proxy(proxy: str) -> Proxy: - """ - Parse and validate a proxy. - - Args: - proxy: proxy. - - Returns: - Parsed proxy. - - Raises: - InvalidProxy: If ``proxy`` isn't a valid proxy. - - """ - parsed = urllib.parse.urlparse(proxy) - if parsed.scheme not in ["socks5h", "socks5", "socks4a", "socks4", "https", "http"]: - raise InvalidProxy(proxy, f"scheme {parsed.scheme} isn't supported") - if parsed.hostname is None: - raise InvalidProxy(proxy, "hostname isn't provided") - if parsed.path not in ["", "/"]: - raise InvalidProxy(proxy, "path is meaningless") - if parsed.query != "": - raise InvalidProxy(proxy, "query is meaningless") - if parsed.fragment != "": - raise InvalidProxy(proxy, "fragment is meaningless") - - scheme = parsed.scheme - host = parsed.hostname - port = parsed.port or (443 if parsed.scheme == "https" else 80) - username = parsed.username - password = parsed.password - # urllib.parse.urlparse accepts URLs with a username but without a - # password. This doesn't make sense for HTTP Basic Auth credentials. - if username is not None and password is None: - raise InvalidProxy(proxy, "username provided without password") - - try: - proxy.encode("ascii") - except UnicodeEncodeError: - # Input contains non-ASCII characters. - # It must be an IRI. Convert it to a URI. - host = host.encode("idna").decode() - if username is not None: - assert password is not None - username = urllib.parse.quote(username, safe=DELIMS) - password = urllib.parse.quote(password, safe=DELIMS) - - return Proxy(scheme, host, port, username, password) - - -def get_proxy(uri: WebSocketURI) -> str | None: - """ - Return the proxy to use for connecting to the given WebSocket URI, if any. - - """ - if urllib.request.proxy_bypass(f"{uri.host}:{uri.port}"): - return None - - # According to the _Proxy Usage_ section of RFC 6455, use a SOCKS5 proxy if - # available, else favor the proxy for HTTPS connections over the proxy for - # HTTP connections. - - # The priority of a proxy for WebSocket connections is unspecified. We give - # it the highest priority. This makes it easy to configure a specific proxy - # for websockets. - - # getproxies() may return SOCKS proxies as {"socks": "http://host:port"} or - # as {"https": "socks5h://host:port"} depending on whether they're declared - # in the operating system or in environment variables. - - proxies = urllib.request.getproxies() - if uri.secure: - schemes = ["wss", "socks", "https"] - else: - schemes = ["ws", "socks", "https", "http"] - - for scheme in schemes: - proxy = proxies.get(scheme) - if proxy is not None: - if scheme == "socks" and proxy.startswith("http://"): - proxy = "socks5h://" + proxy[7:] - return proxy - else: - return None diff --git a/wip/SNIP/websockets/utils.py b/wip/SNIP/websockets/utils.py deleted file mode 100644 index 62d2dc1..0000000 --- a/wip/SNIP/websockets/utils.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import annotations - -import base64 -import hashlib -import secrets -import sys - - -__all__ = ["accept_key", "apply_mask"] - - -GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - - -def generate_key() -> str: - """ - Generate a random key for the Sec-WebSocket-Key header. - - """ - key = secrets.token_bytes(16) - return base64.b64encode(key).decode() - - -def accept_key(key: str) -> str: - """ - Compute the value of the Sec-WebSocket-Accept header. - - Args: - key: Value of the Sec-WebSocket-Key header. - - """ - sha1 = hashlib.sha1((key + GUID).encode()).digest() - return base64.b64encode(sha1).decode() - - -def apply_mask(data: bytes, mask: bytes) -> bytes: - """ - Apply masking to the data of a WebSocket message. - - Args: - data: Data to mask. - mask: 4-bytes mask. - - """ - if len(mask) != 4: - raise ValueError("mask must contain 4 bytes") - - data_int = int.from_bytes(data, sys.byteorder) - mask_repeated = mask * (len(data) // 4) + mask[: len(data) % 4] - mask_int = int.from_bytes(mask_repeated, sys.byteorder) - return (data_int ^ mask_int).to_bytes(len(data), sys.byteorder) diff --git a/wip/SNIP/websockets/version.py b/wip/SNIP/websockets/version.py deleted file mode 100644 index 8d22f4e..0000000 --- a/wip/SNIP/websockets/version.py +++ /dev/null @@ -1,92 +0,0 @@ -from __future__ import annotations - -import importlib.metadata - - -__all__ = ["tag", "version", "commit"] - - -# ========= =========== =================== -# release development -# ========= =========== =================== -# tag X.Y X.Y (upcoming) -# version X.Y X.Y.dev1+g5678cde -# commit X.Y 5678cde -# ========= =========== =================== - - -# When tagging a release, set `released = True`. -# After tagging a release, set `released = False` and increment `tag`. - -released = True - -tag = version = commit = "15.0.1" - - -if not released: # pragma: no cover - import pathlib - import re - import subprocess - - def get_version(tag: str) -> str: - # Since setup.py executes the contents of src/websockets/version.py, - # __file__ can point to either of these two files. - file_path = pathlib.Path(__file__) - root_dir = file_path.parents[0 if file_path.name == "setup.py" else 2] - - # Read version from package metadata if it is installed. - try: - version = importlib.metadata.version("websockets") - except ImportError: - pass - else: - # Check that this file belongs to the installed package. - files = importlib.metadata.files("websockets") - if files: - version_files = [f for f in files if f.name == file_path.name] - if version_files: - version_file = version_files[0] - if version_file.locate() == file_path: - return version - - # Read version from git if available. - try: - description = subprocess.run( - ["git", "describe", "--dirty", "--tags", "--long"], - capture_output=True, - cwd=root_dir, - timeout=1, - check=True, - text=True, - ).stdout.strip() - # subprocess.run raises FileNotFoundError if git isn't on $PATH. - except ( - FileNotFoundError, - subprocess.CalledProcessError, - subprocess.TimeoutExpired, - ): - pass - else: - description_re = r"[0-9.]+-([0-9]+)-(g[0-9a-f]{7,}(?:-dirty)?)" - match = re.fullmatch(description_re, description) - if match is None: - raise ValueError(f"Unexpected git description: {description}") - distance, remainder = match.groups() - remainder = remainder.replace("-", ".") # required by PEP 440 - return f"{tag}.dev{distance}+{remainder}" - - # Avoid crashing if the development version cannot be determined. - return f"{tag}.dev0+gunknown" - - version = get_version(tag) - - def get_commit(tag: str, version: str) -> str: - # Extract commit from version, falling back to tag if not available. - version_re = r"[0-9.]+\.dev[0-9]+\+g([0-9a-f]{7,}|unknown)(?:\.dirty)?" - match = re.fullmatch(version_re, version) - if match is None: - raise ValueError(f"Unexpected version: {version}") - (commit,) = match.groups() - return tag if commit == "unknown" else commit - - commit = get_commit(tag, version)