picows 2.2.0__tar.gz → 2.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {picows-2.2.0/picows.egg-info → picows-2.3.1}/PKG-INFO +1 -2
- {picows-2.2.0 → picows-2.3.1}/README.md +0 -1
- picows-2.3.1/examples/websocket_and_health_check.py +59 -0
- {picows-2.2.0 → picows-2.3.1}/picows/api.py +70 -136
- {picows-2.2.0 → picows-2.3.1}/picows/common.py +22 -0
- {picows-2.2.0 → picows-2.3.1}/picows/picows.pxd +3 -0
- {picows-2.2.0 → picows-2.3.1}/picows/picows.pyx +339 -220
- picows-2.3.1/picows/proxy.py +261 -0
- {picows-2.2.0 → picows-2.3.1}/picows/version.py +1 -1
- {picows-2.2.0 → picows-2.3.1/picows.egg-info}/PKG-INFO +1 -2
- {picows-2.2.0 → picows-2.3.1}/picows.egg-info/SOURCES.txt +4 -0
- {picows-2.2.0 → picows-2.3.1}/pyproject.toml +1 -1
- picows-2.3.1/tests/test_handshake_parsing.py +410 -0
- picows-2.3.1/tests/test_proxy.py +143 -0
- {picows-2.2.0 → picows-2.3.1}/tests/test_redirects_and_proxies.py +81 -8
- {picows-2.2.0 → picows-2.3.1}/tests/test_websockets_server_handshake.py +33 -0
- {picows-2.2.0 → picows-2.3.1}/tests/test_ws_logic.py +153 -8
- {picows-2.2.0 → picows-2.3.1}/tests/utils.py +11 -0
- {picows-2.2.0 → picows-2.3.1}/LICENSE +0 -0
- {picows-2.2.0 → picows-2.3.1}/MANIFEST.in +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/__init__.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/broadcast_client.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/broadcast_server.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/echo_client.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/echo_client_async_callbacks.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/echo_client_async_iteration.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/echo_client_cython.pyx +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/echo_client_cython_runner.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/echo_client_fragmented_msg.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/echo_client_threaded.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/echo_client_websockets.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/echo_server.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/echo_server_ssl.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/echo_server_threaded.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/echo_server_websockes.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/graceful_shutdown.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/okx_roundtrip_time.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/perf_test.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/reconnecting_client.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/slow_client_backpressure.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/examples/subprotocol_negotiation.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/llms.txt +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/__init__.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/compat.h +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/mask_avx2.c +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/mask_avx512.c +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/mask_dispatch.c +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/mask_neon.c +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/mask_sse2.c +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/picows.pyi +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/py.typed +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/url.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/websockets/__init__.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/websockets/asyncio/__init__.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/websockets/asyncio/client.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/websockets/asyncio/connection.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/websockets/asyncio/negotiation.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/websockets/asyncio/router.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/websockets/asyncio/server.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/websockets/asyncio/utils.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/websockets/compat.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/websockets/exceptions.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/websockets/protocol.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows/websockets/typing.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows.egg-info/dependency_links.txt +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows.egg-info/requires.txt +0 -0
- {picows-2.2.0 → picows-2.3.1}/picows.egg-info/top_level.txt +0 -0
- {picows-2.2.0 → picows-2.3.1}/requirements.txt +0 -0
- {picows-2.2.0 → picows-2.3.1}/setup.cfg +0 -0
- {picows-2.2.0 → picows-2.3.1}/setup.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/tests/__init__.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/tests/conftest.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/tests/fixtures.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/tests/picows_test.crt +0 -0
- {picows-2.2.0 → picows-2.3.1}/tests/picows_test.key +0 -0
- {picows-2.2.0 → picows-2.3.1}/tests/test_autoping.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/tests/test_basics.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/tests/test_benchmarks.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/tests/test_redirect_location.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/tests/test_websockets_client.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/tests/test_websockets_compat_types.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/tests/test_websockets_exceptions.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/tests/test_websockets_negotiation.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/tests/test_websockets_ping_pong.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/tests/test_websockets_recv.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/tests/test_websockets_send.py +0 -0
- {picows-2.2.0 → picows-2.3.1}/tests/test_websockets_server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: picows
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.1
|
|
4
4
|
Summary: Ultra-fast websocket client and server for asyncio
|
|
5
5
|
Author-email: Taras Kozlov <tarasko.projects@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/tarasko/picows
|
|
@@ -43,7 +43,6 @@ Dynamic: license-file
|
|
|
43
43
|
<a href='https://pypistats.org/packages/picows' target="_blank"><img alt='downloads' src='https://img.shields.io/pypi/dm/picows'></a>
|
|
44
44
|
<a href='https://picows.readthedocs.io/en/latest/' target="_blank"><img alt='docs' src='https://readthedocs.org/projects/picows/badge/?version=latest'></a>
|
|
45
45
|
<a href='https://codspeed.io/tarasko/picows?utm_source=badge' target="_blank"><img alt='codspeed' src='https://img.shields.io/endpoint?url=https://codspeed.io/badge.json'></a>
|
|
46
|
-
<a href='https://deepwiki.com/tarasko/picows' target="_blank"><img alt='codspeed' src='https://deepwiki.com/badge.svg'></a>
|
|
47
46
|
</p>
|
|
48
47
|
|
|
49
48
|
<div align="center">
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
<a href='https://pypistats.org/packages/picows' target="_blank"><img alt='downloads' src='https://img.shields.io/pypi/dm/picows'></a>
|
|
9
9
|
<a href='https://picows.readthedocs.io/en/latest/' target="_blank"><img alt='docs' src='https://readthedocs.org/projects/picows/badge/?version=latest'></a>
|
|
10
10
|
<a href='https://codspeed.io/tarasko/picows?utm_source=badge' target="_blank"><img alt='codspeed' src='https://img.shields.io/endpoint?url=https://codspeed.io/badge.json'></a>
|
|
11
|
-
<a href='https://deepwiki.com/tarasko/picows' target="_blank"><img alt='codspeed' src='https://deepwiki.com/badge.svg'></a>
|
|
12
11
|
</p>
|
|
13
12
|
|
|
14
13
|
<div align="center">
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# This example shows how to serve a WebSocket endpoint and an HTTP health check on the same port.
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from logging import basicConfig
|
|
5
|
+
|
|
6
|
+
import picows
|
|
7
|
+
from picows import (
|
|
8
|
+
WSFrame,
|
|
9
|
+
WSListener,
|
|
10
|
+
WSMsgType,
|
|
11
|
+
WSTransport,
|
|
12
|
+
WSUpgradeRequest,
|
|
13
|
+
WSUpgradeResponse,
|
|
14
|
+
WSUpgradeResponseWithListener,
|
|
15
|
+
ws_create_server,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EchoListener(WSListener):
|
|
20
|
+
def on_ws_frame(self, transport: WSTransport, frame: WSFrame):
|
|
21
|
+
if frame.msg_type == WSMsgType.CLOSE:
|
|
22
|
+
transport.send_close(frame.get_close_code(), frame.get_close_message())
|
|
23
|
+
transport.disconnect()
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
transport.send(
|
|
27
|
+
frame.msg_type,
|
|
28
|
+
frame.get_payload_as_memoryview(),
|
|
29
|
+
frame.fin,
|
|
30
|
+
frame.rsv1,
|
|
31
|
+
frame.rsv2,
|
|
32
|
+
frame.rsv3,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create_health_check_response() -> WSUpgradeResponseWithListener:
|
|
37
|
+
response = WSUpgradeResponse.create_ok_response(b"I'm healthy")
|
|
38
|
+
return WSUpgradeResponseWithListener(response, None)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def listener_factory(request: WSUpgradeRequest):
|
|
42
|
+
if request.path == b"/health":
|
|
43
|
+
return create_health_check_response()
|
|
44
|
+
if request.path == b"/ws":
|
|
45
|
+
return EchoListener()
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def main():
|
|
50
|
+
server = await ws_create_server(listener_factory, "127.0.0.1", 9001)
|
|
51
|
+
print("Health check: http://127.0.0.1:9001/health")
|
|
52
|
+
print("WebSocket endpoint: ws://127.0.0.1:9001/ws")
|
|
53
|
+
async with server:
|
|
54
|
+
await server.serve_forever()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
basicConfig(level=picows.PICOWS_DEBUG_LL)
|
|
59
|
+
asyncio.run(main())
|
|
@@ -1,28 +1,24 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import
|
|
2
|
+
import ssl as ssl_module
|
|
3
3
|
import urllib.parse
|
|
4
|
-
from dataclasses import dataclass
|
|
5
4
|
from functools import partial
|
|
6
|
-
from inspect import isawaitable
|
|
7
5
|
from logging import Logger, LoggerAdapter, getLogger
|
|
8
6
|
from ssl import SSLContext
|
|
9
|
-
from typing import
|
|
7
|
+
from typing import Any, Awaitable, Callable, Optional, Union, cast, TYPE_CHECKING
|
|
10
8
|
|
|
11
|
-
from
|
|
12
|
-
|
|
13
|
-
from .common import (WSHeadersLike, WSUpgradeRequest, WSUpgradeResponse,
|
|
9
|
+
from .proxy import (ConnectedTransport, WSSocketFactory,
|
|
10
|
+
connect_through_optional_proxy)
|
|
11
|
+
from .common import (WSHeadersLike, WSUpgradeRequest, WSUpgradeResponse,
|
|
14
12
|
WSUpgradeResponseWithListener, WSHandshakeError)
|
|
15
13
|
from .picows import (WSListener, WSTransport, WSAutoPingStrategy, # type: ignore [attr-defined]
|
|
16
14
|
WSProtocol)
|
|
17
|
-
from .url import parse_url,
|
|
18
|
-
|
|
15
|
+
from .url import parse_url, WSParsedURL
|
|
19
16
|
|
|
20
17
|
WSListenerFactory = Union[
|
|
21
18
|
Callable[[], WSListener],
|
|
22
19
|
Callable[[WSUpgradeRequest, WSUpgradeResponse], WSListener],
|
|
23
20
|
]
|
|
24
21
|
WSServerListenerFactory = Callable[[WSUpgradeRequest], Union[WSListener, WSUpgradeResponseWithListener, None]]
|
|
25
|
-
WSSocketFactory = Callable[[WSParsedURL], Union[Optional[socket.socket], Awaitable[Optional[socket.socket]]]]
|
|
26
22
|
|
|
27
23
|
if TYPE_CHECKING:
|
|
28
24
|
_WSLoggerAdapter = LoggerAdapter[Any]
|
|
@@ -64,14 +60,6 @@ def _maybe_handle_redirect(exc: WSHandshakeError, old_parsed_url: WSParsedURL, m
|
|
|
64
60
|
return parsed_url
|
|
65
61
|
|
|
66
62
|
|
|
67
|
-
def _is_connected(sock: socket.socket) -> bool:
|
|
68
|
-
try:
|
|
69
|
-
sock.getpeername()
|
|
70
|
-
return True
|
|
71
|
-
except OSError:
|
|
72
|
-
return False
|
|
73
|
-
|
|
74
|
-
|
|
75
63
|
def _resolve_logger(
|
|
76
64
|
logger_name: WSLoggerLike,
|
|
77
65
|
default_suffix: str,
|
|
@@ -85,109 +73,6 @@ def _resolve_logger(
|
|
|
85
73
|
|
|
86
74
|
return logger_name
|
|
87
75
|
|
|
88
|
-
@dataclass
|
|
89
|
-
class _ConnectedSocket:
|
|
90
|
-
sock: Optional[socket.socket]
|
|
91
|
-
host: Optional[WSHost]
|
|
92
|
-
port: Optional[WSPort]
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
async def _create_connected_socket(
|
|
96
|
-
loop: asyncio.AbstractEventLoop,
|
|
97
|
-
socket_factory: Optional[WSSocketFactory],
|
|
98
|
-
parsed_url: WSParsedURL
|
|
99
|
-
) -> Optional[socket.socket]:
|
|
100
|
-
if socket_factory is None:
|
|
101
|
-
return None
|
|
102
|
-
|
|
103
|
-
sock_or_awaitable = socket_factory(parsed_url)
|
|
104
|
-
sock: Optional[socket.socket]
|
|
105
|
-
if sock_or_awaitable is None or isinstance(sock_or_awaitable, socket.socket):
|
|
106
|
-
sock = sock_or_awaitable
|
|
107
|
-
elif isawaitable(sock_or_awaitable):
|
|
108
|
-
sock = await sock_or_awaitable
|
|
109
|
-
else:
|
|
110
|
-
raise TypeError(f"user socket_factory() returned invalid type: {type(sock_or_awaitable).__name__}")
|
|
111
|
-
|
|
112
|
-
if sock is not None:
|
|
113
|
-
sock.setblocking(False)
|
|
114
|
-
if sock is not None:
|
|
115
|
-
if not _is_connected(sock):
|
|
116
|
-
await loop.sock_connect(sock, (parsed_url.host, parsed_url.port))
|
|
117
|
-
|
|
118
|
-
return sock
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
async def _connect_through_optional_proxy(
|
|
122
|
-
loop: asyncio.AbstractEventLoop,
|
|
123
|
-
parsed_url: WSParsedURL,
|
|
124
|
-
proxy_parsed_url: Optional[WSParsedURL],
|
|
125
|
-
socket_factory: Optional[WSSocketFactory],
|
|
126
|
-
ssl_context: Optional[Union[SSLContext, bool]],
|
|
127
|
-
conn_kwargs: Dict[str, Any]
|
|
128
|
-
) -> _ConnectedSocket:
|
|
129
|
-
if proxy_parsed_url is not None and proxy_parsed_url.scheme == "https":
|
|
130
|
-
raise WSInvalidURL(proxy_parsed_url.url,
|
|
131
|
-
"HTTPS proxy URL scheme is not supported, use http://, socks4:// or socks5://")
|
|
132
|
-
|
|
133
|
-
if proxy_parsed_url is not None:
|
|
134
|
-
proxy_obj = Proxy.from_url(proxy_parsed_url.url, loop=loop)
|
|
135
|
-
proxy_socket = await _create_connected_socket(loop, socket_factory, proxy_parsed_url)
|
|
136
|
-
if proxy_socket is not None:
|
|
137
|
-
# It is so ugly that I have to use python_socks internals
|
|
138
|
-
# I could not figure out how to pass existing connected socket using public
|
|
139
|
-
# interface. Maybe I should just copy that part of the code?
|
|
140
|
-
|
|
141
|
-
# Import everthing as local as possible
|
|
142
|
-
# If imports will stop working, picows ws_connect will break but only
|
|
143
|
-
# if user has passed proxy together with socket_factory
|
|
144
|
-
|
|
145
|
-
from python_socks import ProxyError
|
|
146
|
-
from python_socks._connectors.factory_async import create_connector
|
|
147
|
-
from python_socks._protocols.errors import ReplyError
|
|
148
|
-
from python_socks.async_.asyncio._stream import AsyncioSocketStream
|
|
149
|
-
|
|
150
|
-
stream = AsyncioSocketStream(sock=proxy_socket, loop=loop)
|
|
151
|
-
|
|
152
|
-
try:
|
|
153
|
-
connector = create_connector(
|
|
154
|
-
proxy_type=proxy_obj._proxy_type,
|
|
155
|
-
username=proxy_obj._username,
|
|
156
|
-
password=proxy_obj._password,
|
|
157
|
-
rdns=proxy_obj._rdns,
|
|
158
|
-
resolver=proxy_obj._resolver,
|
|
159
|
-
)
|
|
160
|
-
await connector.connect(
|
|
161
|
-
stream=stream,
|
|
162
|
-
host=parsed_url.host,
|
|
163
|
-
port=parsed_url.port,
|
|
164
|
-
)
|
|
165
|
-
except ReplyError as e:
|
|
166
|
-
await stream.close()
|
|
167
|
-
raise ProxyError(e, error_code=e.error_code)
|
|
168
|
-
except (asyncio.CancelledError, Exception):
|
|
169
|
-
await stream.close()
|
|
170
|
-
raise
|
|
171
|
-
else:
|
|
172
|
-
proxy_socket = await proxy_obj.connect(
|
|
173
|
-
dest_host=parsed_url.host,
|
|
174
|
-
dest_port=parsed_url.port,
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
if ssl_context and "server_hostname" not in conn_kwargs:
|
|
178
|
-
conn_kwargs["server_hostname"] = parsed_url.host
|
|
179
|
-
|
|
180
|
-
return _ConnectedSocket(proxy_socket, None, None)
|
|
181
|
-
else:
|
|
182
|
-
sock = await _create_connected_socket(loop, socket_factory, parsed_url)
|
|
183
|
-
if sock is not None:
|
|
184
|
-
if ssl_context and "server_hostname" not in conn_kwargs:
|
|
185
|
-
conn_kwargs["server_hostname"] = parsed_url.host
|
|
186
|
-
|
|
187
|
-
return _ConnectedSocket(sock, None, None)
|
|
188
|
-
else:
|
|
189
|
-
return _ConnectedSocket(None, parsed_url.host, parsed_url.port)
|
|
190
|
-
|
|
191
76
|
|
|
192
77
|
async def ws_connect(ws_listener_factory: WSListenerFactory, # type: ignore [no-untyped-def]
|
|
193
78
|
url: str,
|
|
@@ -205,6 +90,7 @@ async def ws_connect(ws_listener_factory: WSListenerFactory, # type: ignore [no-
|
|
|
205
90
|
extra_headers: Optional[WSHeadersLike] = None,
|
|
206
91
|
max_redirects: int = 5,
|
|
207
92
|
proxy: Optional[str] = None,
|
|
93
|
+
proxy_ssl_context: Optional[SSLContext] = None,
|
|
208
94
|
read_buffer_init_size: int = 16 * 1024,
|
|
209
95
|
socket_factory: Optional[WSSocketFactory] = None,
|
|
210
96
|
use_aiofastnet: Optional[bool] = None,
|
|
@@ -265,9 +151,11 @@ async def ws_connect(ws_listener_factory: WSListenerFactory, # type: ignore [no-
|
|
|
265
151
|
:param max_redirects:
|
|
266
152
|
* How many times we can follow HTTP redirects. Set to 0 in order to disable redirects.
|
|
267
153
|
:param proxy:
|
|
268
|
-
Optional proxy URL. Supported schemes are ``http://``, ``
|
|
269
|
-
and ``socks5://`` (including authenticated variants).
|
|
270
|
-
|
|
154
|
+
Optional proxy URL. Supported schemes are ``http://``, ``https://``,
|
|
155
|
+
``socks4://`` and ``socks5://`` (including authenticated variants).
|
|
156
|
+
:param proxy_ssl_context:
|
|
157
|
+
Optional SSLContext for the TLS connection to an ``https://`` proxy.
|
|
158
|
+
A default client context is used when this isn't provided.
|
|
271
159
|
:param read_buffer_init_size:
|
|
272
160
|
Initial size (in bytes) of the internal read buffer.
|
|
273
161
|
The buffer grows exponentially when incoming data does not fit.
|
|
@@ -313,10 +201,13 @@ async def ws_connect(ws_listener_factory: WSListenerFactory, # type: ignore [no-
|
|
|
313
201
|
parsed_proxy_url = parse_url(proxy, False) if proxy is not None else None
|
|
314
202
|
loop = asyncio.get_running_loop()
|
|
315
203
|
|
|
204
|
+
start_tls: Callable[..., Awaitable[Optional[asyncio.Transport]]]
|
|
316
205
|
if use_aiofastnet:
|
|
317
206
|
create_connection = partial(aiofastnet.create_connection, loop)
|
|
207
|
+
start_tls = partial(aiofastnet.start_tls, loop)
|
|
318
208
|
else:
|
|
319
209
|
create_connection = loop.create_connection # type: ignore [assignment]
|
|
210
|
+
start_tls = loop.start_tls
|
|
320
211
|
|
|
321
212
|
ssl: Optional[Union[SSLContext, bool]]
|
|
322
213
|
while True:
|
|
@@ -349,17 +240,48 @@ async def ws_connect(ws_listener_factory: WSListenerFactory, # type: ignore [no-
|
|
|
349
240
|
|
|
350
241
|
try:
|
|
351
242
|
conn_kwargs = dict(kwargs)
|
|
352
|
-
|
|
353
|
-
loop, parsed_url, parsed_proxy_url, socket_factory, ssl,
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
243
|
+
connection = await connect_through_optional_proxy(
|
|
244
|
+
loop, parsed_url, parsed_proxy_url, socket_factory, ssl,
|
|
245
|
+
proxy_ssl_context, conn_kwargs, create_connection)
|
|
246
|
+
|
|
247
|
+
if isinstance(connection, ConnectedTransport):
|
|
248
|
+
transport = connection.transport
|
|
249
|
+
try:
|
|
250
|
+
if ssl is not None:
|
|
251
|
+
target_ssl_context = ssl if isinstance(ssl, SSLContext) \
|
|
252
|
+
else ssl_module.create_default_context()
|
|
253
|
+
server_hostname = conn_kwargs.get("server_hostname", parsed_url.host)
|
|
254
|
+
start_tls_kwargs = {
|
|
255
|
+
key: value for key, value in conn_kwargs.items()
|
|
256
|
+
if key.startswith("ssl_")
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
new_transport = await start_tls(
|
|
260
|
+
transport, transport.get_protocol(), target_ssl_context,
|
|
261
|
+
server_hostname=server_hostname, **start_tls_kwargs)
|
|
262
|
+
|
|
263
|
+
# asyncio docs says that start_tls can potentially return None if transport
|
|
264
|
+
# is already closing
|
|
265
|
+
if new_transport is None:
|
|
266
|
+
raise ConnectionError("connection closed while starting TLS")
|
|
267
|
+
|
|
268
|
+
transport = new_transport
|
|
269
|
+
|
|
270
|
+
ws_protocol = ws_protocol_factory()
|
|
271
|
+
transport.set_protocol(ws_protocol)
|
|
272
|
+
ws_protocol.connection_made(transport)
|
|
273
|
+
except (asyncio.CancelledError, Exception):
|
|
274
|
+
transport.abort()
|
|
275
|
+
raise
|
|
276
|
+
else:
|
|
277
|
+
(_, ws_protocol) = await create_connection(
|
|
278
|
+
ws_protocol_factory,
|
|
279
|
+
connection.host,
|
|
280
|
+
connection.port,
|
|
281
|
+
ssl=ssl,
|
|
282
|
+
sock=connection.sock,
|
|
283
|
+
**conn_kwargs
|
|
284
|
+
)
|
|
363
285
|
|
|
364
286
|
await ws_protocol.wait_until_handshake_complete()
|
|
365
287
|
return ws_protocol.transport, ws_protocol.listener
|
|
@@ -396,12 +318,24 @@ async def ws_create_server(ws_listener_factory: WSServerListenerFactory,
|
|
|
396
318
|
It has a few extra parameters to control WebSocket behavior.
|
|
397
319
|
|
|
398
320
|
:param ws_listener_factory:
|
|
399
|
-
A factory function that accepts WSUpgradeRequest object
|
|
321
|
+
A factory function that accepts a parsed WSUpgradeRequest object before
|
|
322
|
+
WebSocket upgrade headers are validated and returns one of:
|
|
400
323
|
|
|
401
324
|
* User handler object. A standard 101 response will be sent to the client.
|
|
402
325
|
* WSUpgradeResponseWithListener object. This allows to send a custom response with extra headers and an optional body.
|
|
403
326
|
* None. In such case 404 Not Found response will be sent and the client will be disconnected.
|
|
404
327
|
|
|
328
|
+
Returning a response whose status isn't 101 Switching Protocols sends
|
|
329
|
+
that response with ``Connection: close``, without validating WebSocket
|
|
330
|
+
upgrade headers, and then disconnects the client. Any user-provided
|
|
331
|
+
``Connection`` response header is overridden. This can be used for small
|
|
332
|
+
HTTP endpoints such as health checks. A 101 response is sent only after
|
|
333
|
+
validating the WebSocket upgrade request.
|
|
334
|
+
|
|
335
|
+
Incoming requests must be HTTP/1.1 GET requests without a body.
|
|
336
|
+
``Content-Length`` may be omitted or set to zero; ``Transfer-Encoding``
|
|
337
|
+
and non-zero content lengths are rejected.
|
|
338
|
+
|
|
405
339
|
The user handler must derive from WSListener and is responsible for
|
|
406
340
|
processing incoming data.
|
|
407
341
|
|
|
@@ -45,6 +45,28 @@ class WSUpgradeResponse:
|
|
|
45
45
|
headers : CIMultiDict[str]
|
|
46
46
|
body : Optional[bytes]
|
|
47
47
|
|
|
48
|
+
@staticmethod
|
|
49
|
+
def create_ok_response(body: Optional[bytes] = None,
|
|
50
|
+
extra_headers: Optional[WSHeadersLike] = None) -> Any:
|
|
51
|
+
"""
|
|
52
|
+
Create an HTTP 200 OK response.
|
|
53
|
+
|
|
54
|
+
:param body: optional response body
|
|
55
|
+
:param extra_headers: optional additional headers. Content-Type defaults
|
|
56
|
+
to ``text/plain; charset=utf-8`` when it isn't provided.
|
|
57
|
+
:return: a new WSUpgradeResponse object
|
|
58
|
+
"""
|
|
59
|
+
self = WSUpgradeResponse()
|
|
60
|
+
self.version = b"HTTP/1.1"
|
|
61
|
+
self.status = HTTPStatus.OK
|
|
62
|
+
self.headers = CIMultiDict()
|
|
63
|
+
add_extra_headers(self.headers, extra_headers)
|
|
64
|
+
if "Content-Type" not in self.headers:
|
|
65
|
+
self.headers["Content-Type"] = "text/plain; charset=utf-8"
|
|
66
|
+
self.body = body
|
|
67
|
+
|
|
68
|
+
return self
|
|
69
|
+
|
|
48
70
|
@staticmethod
|
|
49
71
|
def create_error_response(status: Union[int, HTTPStatus],
|
|
50
72
|
body: Optional[bytes] = None,
|
|
@@ -97,6 +97,9 @@ cdef class WSFrame:
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
cpdef WSFrame _make_test_ws_frame(WSMsgType msg_type, bytes payload, bint fin, bint rsv1)
|
|
100
|
+
cpdef object _parse_http_request(bytes raw_headers)
|
|
101
|
+
cpdef bytes _validate_upgrade_request(object upgrade_request)
|
|
102
|
+
cpdef object _parse_upgrade_response(bytes data, bytes websocket_key_b64)
|
|
100
103
|
|
|
101
104
|
|
|
102
105
|
cdef class WSTransport:
|