DefEssential 0.1.8__23-py3-none-any.whl
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.
- DefEssential/Adapter/Implements/Gunicorn/Server.py +152 -0
- DefEssential/Adapter/Implements/Gunicorn/Transport.py +171 -0
- DefEssential/Adapter/Implements/Gunicorn/__init__.py +5 -0
- DefEssential/Adapter/Implements/Python/Server.py +273 -0
- DefEssential/Adapter/Implements/Python/__init__.py +5 -0
- DefEssential/Adapter/Implements/Uvicorn/HTTP.py +256 -0
- DefEssential/Adapter/Implements/Uvicorn/Portal.py +65 -0
- DefEssential/Adapter/Implements/Uvicorn/Server.py +137 -0
- DefEssential/Adapter/Implements/Uvicorn/WebSocket.py +222 -0
- DefEssential/Adapter/Implements/Uvicorn/__init__.py +5 -0
- DefEssential/Adapter/Implements/__init__.py +3 -0
- DefEssential/Adapter/RequestHandler.py +109 -0
- DefEssential/Adapter/Server.py +54 -0
- DefEssential/Adapter/WSGI.py +219 -0
- DefEssential/Adapter/__init__.py +5 -0
- DefEssential/Application.py +657 -0
- DefEssential/Body.py +50 -0
- DefEssential/Content.py +18 -0
- DefEssential/Credentials.py +245 -0
- DefEssential/CrossOriginResourceSharing.py +107 -0
- DefEssential/Descriptor/DataObject.py +409 -0
- DefEssential/Descriptor/Types.py +248 -0
- DefEssential/Descriptor/Value.py +188 -0
- DefEssential/Descriptor/__init__.py +172 -0
- DefEssential/Error.py +66 -0
- DefEssential/Extension.py +191 -0
- DefEssential/Extensions/File.py +95 -0
- DefEssential/Extensions/FileSystem.py +195 -0
- DefEssential/Extensions/Metrics.py +511 -0
- DefEssential/Extensions/Ping.py +93 -0
- DefEssential/Extensions/Specifications.py +342 -0
- DefEssential/Extensions/Status.html +207 -0
- DefEssential/Extensions/Status.py +110 -0
- DefEssential/Extensions/Swagger.html +77 -0
- DefEssential/Extensions/Swagger.py +117 -0
- DefEssential/Extensions/Utils/Routes.py +42 -0
- DefEssential/Extensions/Utils/__init__.py +43 -0
- DefEssential/Extensions/__init__.py +21 -0
- DefEssential/IO/Readers/ChunkedStream.py +106 -0
- DefEssential/IO/Readers/ContentReader.py +36 -0
- DefEssential/IO/Readers/ContentReaders.py +241 -0
- DefEssential/IO/Readers/__init__.py +25 -0
- DefEssential/IO/RequestReader.py +125 -0
- DefEssential/IO/ResponseWriter.py +242 -0
- DefEssential/IO/WebSocket/WebSocket.py +288 -0
- DefEssential/IO/WebSocket/WebSocketError.py +17 -0
- DefEssential/IO/WebSocket/WebSocketFrame.py +68 -0
- DefEssential/IO/WebSocket/WebSocketFrameReader.py +98 -0
- DefEssential/IO/WebSocket/WebSocketHandshake.py +94 -0
- DefEssential/IO/WebSocket/__init__.py +13 -0
- DefEssential/IO/Writers/ChunkedStreamWriter.py +19 -0
- DefEssential/IO/Writers/ServerSentEvents.py +41 -0
- DefEssential/IO/Writers/ServerSentEventsWriter.py +67 -0
- DefEssential/IO/Writers/StreamWriter.py +57 -0
- DefEssential/IO/Writers/__init__.py +11 -0
- DefEssential/IO/__init__.py +7 -0
- DefEssential/Request.py +110 -0
- DefEssential/RequestRunner.py +261 -0
- DefEssential/Response.py +89 -0
- DefEssential/ResponseSpecification.py +23 -0
- DefEssential/Responses/ResponseFile.py +108 -0
- DefEssential/Responses/__init__.py +63 -0
- DefEssential/Route.py +55 -0
- DefEssential/Router.py +48 -0
- DefEssential/Runners/RunExtension.py +116 -0
- DefEssential/Runners/RunFactory.py +26 -0
- DefEssential/Runners/RunOptions.py +77 -0
- DefEssential/Runners/RunRequest.py +198 -0
- DefEssential/Runners/__init__.py +5 -0
- DefEssential/Startup.py +104 -0
- DefEssential/Utils/FileSystemObject/Configuration.py +15 -0
- DefEssential/Utils/FileSystemObject/Connection.py +57 -0
- DefEssential/Utils/FileSystemObject/File.py +41 -0
- DefEssential/Utils/FileSystemObject/Implements/FileSystem/Configuration.py +15 -0
- DefEssential/Utils/FileSystemObject/Implements/FileSystem/Connection.py +115 -0
- DefEssential/Utils/FileSystemObject/Implements/FileSystem/File.py +77 -0
- DefEssential/Utils/FileSystemObject/Implements/FileSystem/__init__.py +11 -0
- DefEssential/Utils/FileSystemObject/Implements/__init__.py +3 -0
- DefEssential/Utils/FileSystemObject/__init__.py +11 -0
- DefEssential/Utils/Loader.py +81 -0
- DefEssential/Utils/__init__.py +3 -0
- DefEssential/Validator/Pattern.py +36 -0
- DefEssential/Validator/Patterns/Array.py +30 -0
- DefEssential/Validator/Patterns/Common.py +92 -0
- DefEssential/Validator/Patterns/Compare.py +180 -0
- DefEssential/Validator/Patterns/Condition.py +60 -0
- DefEssential/Validator/Patterns/DataObject.py +85 -0
- DefEssential/Validator/Patterns/DateTime.py +111 -0
- DefEssential/Validator/Patterns/Number.py +43 -0
- DefEssential/Validator/Patterns/Object.py +150 -0
- DefEssential/Validator/Patterns/Size.py +110 -0
- DefEssential/Validator/Patterns/String.py +85 -0
- DefEssential/Validator/Patterns/Type.py +350 -0
- DefEssential/Validator/Patterns/__init__.py +137 -0
- DefEssential/Validator/Validate.py +29 -0
- DefEssential/Validator/Validator.py +30 -0
- DefEssential/Validator/__init__.py +11 -0
- DefEssential/Validators.py +255 -0
- DefEssential/__init__.py +83 -0
- defessential-0.1.8.dist-info/METADATA +82 -0
- defessential-0.1.8.dist-info/RECORD +104 -0
- defessential-0.1.8.dist-info/WHEEL +6 -0
- defessential-0.1.8.dist-info/licenses/LICENSE +674 -0
- defessential-0.1.8.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
from logging import getLogger
|
|
2
|
+
from struct import (
|
|
3
|
+
pack,
|
|
4
|
+
unpack,
|
|
5
|
+
)
|
|
6
|
+
from threading import Lock
|
|
7
|
+
from typing import (
|
|
8
|
+
Optional,
|
|
9
|
+
Union,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from ...Request import Request
|
|
13
|
+
from .WebSocketError import WebSocketError
|
|
14
|
+
from .WebSocketFrame import WebSocketFrame
|
|
15
|
+
from .WebSocketFrameReader import WebSocketFrameReader
|
|
16
|
+
from .WebSocketHandshake import WebSocketHandshake
|
|
17
|
+
|
|
18
|
+
__all__ = (
|
|
19
|
+
'WebSocket',
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class WebSocket:
|
|
24
|
+
"""Exchange WebSocket messages over an upgraded HTTP connection.
|
|
25
|
+
|
|
26
|
+
Call receive() from a single thread. send(), ping(), and close() may also be
|
|
27
|
+
called from other threads.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
logger = getLogger('DefEssential.IO.WebSocket')
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
request: Request,
|
|
35
|
+
limit: int,
|
|
36
|
+
):
|
|
37
|
+
if limit <= 0:
|
|
38
|
+
raise ValueError('Message size limit must be positive')
|
|
39
|
+
self.request = request
|
|
40
|
+
self.limit = limit
|
|
41
|
+
self.handshake = WebSocketHandshake(request)
|
|
42
|
+
self.frames = WebSocketFrameReader(
|
|
43
|
+
request.reader,
|
|
44
|
+
limit,
|
|
45
|
+
)
|
|
46
|
+
self.lock = Lock()
|
|
47
|
+
self.accepted = False
|
|
48
|
+
self.close_sent = False
|
|
49
|
+
self.closed = False
|
|
50
|
+
self.code: Optional[int] = None
|
|
51
|
+
self.reason = ''
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def protocols(self) -> list[str]:
|
|
56
|
+
return self.handshake.protocols
|
|
57
|
+
|
|
58
|
+
def accept(
|
|
59
|
+
self,
|
|
60
|
+
protocol: Optional[str] = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Validate the handshake and send a 101 response. On failure, a regular error response is sent because the connection has not been upgraded."""
|
|
63
|
+
if self.accepted:
|
|
64
|
+
raise RuntimeError('WebSocket has already been accepted')
|
|
65
|
+
self.request.writer.upgrade(self.handshake.response(protocol))
|
|
66
|
+
self.accepted = True
|
|
67
|
+
self.logger.debug('WebSocket upgraded: request_id=%s status=101', self.request.id)
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
def valid_code(
|
|
71
|
+
self,
|
|
72
|
+
code: int,
|
|
73
|
+
) -> bool:
|
|
74
|
+
return code in (
|
|
75
|
+
1000,
|
|
76
|
+
1001,
|
|
77
|
+
1002,
|
|
78
|
+
1003,
|
|
79
|
+
1007,
|
|
80
|
+
1008,
|
|
81
|
+
1009,
|
|
82
|
+
1010,
|
|
83
|
+
1011,
|
|
84
|
+
1012,
|
|
85
|
+
1013,
|
|
86
|
+
1014,
|
|
87
|
+
) or 3000 <= code <= 4999
|
|
88
|
+
|
|
89
|
+
def write(
|
|
90
|
+
self,
|
|
91
|
+
frame: WebSocketFrame,
|
|
92
|
+
) -> None:
|
|
93
|
+
with self.lock:
|
|
94
|
+
if not self.accepted:
|
|
95
|
+
raise RuntimeError('WebSocket must be accepted first')
|
|
96
|
+
if self.close_sent:
|
|
97
|
+
raise RuntimeError('Cannot send after the WebSocket close frame')
|
|
98
|
+
if frame.opcode == WebSocketFrame.CLOSE:
|
|
99
|
+
self.close_sent = True
|
|
100
|
+
self.request.writer.write(frame.encode())
|
|
101
|
+
self.request.writer.flush()
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
def send(
|
|
105
|
+
self,
|
|
106
|
+
message: Union[str, bytes],
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Send str values as text messages and bytes values as binary messages."""
|
|
109
|
+
if isinstance(message, str):
|
|
110
|
+
frame = WebSocketFrame(
|
|
111
|
+
WebSocketFrame.TEXT,
|
|
112
|
+
message.encode('utf-8'),
|
|
113
|
+
)
|
|
114
|
+
elif isinstance(message, bytes):
|
|
115
|
+
frame = WebSocketFrame(
|
|
116
|
+
WebSocketFrame.BINARY,
|
|
117
|
+
message,
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
raise TypeError('WebSocket messages must be str or bytes')
|
|
121
|
+
self.write(frame)
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
def ping(
|
|
125
|
+
self,
|
|
126
|
+
data: bytes = b'',
|
|
127
|
+
) -> None:
|
|
128
|
+
self.write(WebSocketFrame(
|
|
129
|
+
WebSocketFrame.PING,
|
|
130
|
+
data,
|
|
131
|
+
))
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
def close_frame(
|
|
135
|
+
self,
|
|
136
|
+
code: int,
|
|
137
|
+
reason: str,
|
|
138
|
+
) -> WebSocketFrame:
|
|
139
|
+
return WebSocketFrame(
|
|
140
|
+
WebSocketFrame.CLOSE,
|
|
141
|
+
pack('!H', code) + reason.encode('utf-8'),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def receive_close(
|
|
145
|
+
self,
|
|
146
|
+
frame: WebSocketFrame,
|
|
147
|
+
) -> None:
|
|
148
|
+
payload = frame.payload
|
|
149
|
+
if len(payload) == 1:
|
|
150
|
+
raise WebSocketError(
|
|
151
|
+
1002,
|
|
152
|
+
'Invalid close frame',
|
|
153
|
+
)
|
|
154
|
+
code = unpack('!H', payload[:2])[0] if payload else None
|
|
155
|
+
if code is not None and not self.valid_code(code):
|
|
156
|
+
raise WebSocketError(
|
|
157
|
+
1002,
|
|
158
|
+
'Invalid close code',
|
|
159
|
+
)
|
|
160
|
+
try:
|
|
161
|
+
reason = payload[2:].decode('utf-8')
|
|
162
|
+
except UnicodeDecodeError as error:
|
|
163
|
+
raise WebSocketError(
|
|
164
|
+
1007,
|
|
165
|
+
'Invalid close reason',
|
|
166
|
+
) from error
|
|
167
|
+
self.code = code
|
|
168
|
+
self.reason = reason
|
|
169
|
+
self.closed = True
|
|
170
|
+
with self.lock:
|
|
171
|
+
echo = not self.close_sent
|
|
172
|
+
if echo:
|
|
173
|
+
self.write(WebSocketFrame(
|
|
174
|
+
WebSocketFrame.CLOSE,
|
|
175
|
+
payload[:2],
|
|
176
|
+
))
|
|
177
|
+
self.logger.debug('WebSocket closed: request_id=%s code=%s', self.request.id, self.code)
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
def fail(
|
|
181
|
+
self,
|
|
182
|
+
error: WebSocketError,
|
|
183
|
+
) -> None:
|
|
184
|
+
self.logger.warning(
|
|
185
|
+
'WebSocket protocol error: request_id=%s code=%s',
|
|
186
|
+
self.request.id,
|
|
187
|
+
error.code,
|
|
188
|
+
)
|
|
189
|
+
self.code = error.code
|
|
190
|
+
self.reason = error.reason
|
|
191
|
+
self.closed = True
|
|
192
|
+
with self.lock:
|
|
193
|
+
send = not self.close_sent
|
|
194
|
+
if send:
|
|
195
|
+
self.write(self.close_frame(
|
|
196
|
+
error.code,
|
|
197
|
+
error.reason,
|
|
198
|
+
))
|
|
199
|
+
self.logger.debug('WebSocket failed: request_id=%s code=%s', self.request.id, error.code)
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
def receive(self) -> Union[str, bytes, None]:
|
|
203
|
+
"""Return the next message, or None once the connection starts closing.
|
|
204
|
+
|
|
205
|
+
Automatically reply to ping frames with pong. On a protocol violation,
|
|
206
|
+
send a close code and return None. Propagate ClientDisconnected if the
|
|
207
|
+
TCP connection is lost.
|
|
208
|
+
"""
|
|
209
|
+
if not self.accepted:
|
|
210
|
+
raise RuntimeError('WebSocket must be accepted first')
|
|
211
|
+
if self.closed:
|
|
212
|
+
return None
|
|
213
|
+
opcode: Optional[int] = None
|
|
214
|
+
parts = []
|
|
215
|
+
size = 0
|
|
216
|
+
try:
|
|
217
|
+
while True:
|
|
218
|
+
frame = self.frames.read()
|
|
219
|
+
if frame.opcode == WebSocketFrame.CLOSE:
|
|
220
|
+
self.receive_close(frame)
|
|
221
|
+
return None
|
|
222
|
+
if frame.opcode == WebSocketFrame.PING:
|
|
223
|
+
with self.lock:
|
|
224
|
+
reply = not self.close_sent
|
|
225
|
+
if reply:
|
|
226
|
+
self.write(WebSocketFrame(
|
|
227
|
+
WebSocketFrame.PONG,
|
|
228
|
+
frame.payload,
|
|
229
|
+
))
|
|
230
|
+
continue
|
|
231
|
+
if frame.opcode == WebSocketFrame.PONG:
|
|
232
|
+
continue
|
|
233
|
+
if frame.opcode == WebSocketFrame.CONTINUATION:
|
|
234
|
+
if opcode is None:
|
|
235
|
+
raise WebSocketError(
|
|
236
|
+
1002,
|
|
237
|
+
'Unexpected continuation frame',
|
|
238
|
+
)
|
|
239
|
+
elif opcode is not None:
|
|
240
|
+
raise WebSocketError(
|
|
241
|
+
1002,
|
|
242
|
+
'Expected a continuation frame',
|
|
243
|
+
)
|
|
244
|
+
else:
|
|
245
|
+
opcode = frame.opcode
|
|
246
|
+
size += len(frame.payload)
|
|
247
|
+
if size > self.limit:
|
|
248
|
+
raise WebSocketError(
|
|
249
|
+
1009,
|
|
250
|
+
'Message too big',
|
|
251
|
+
)
|
|
252
|
+
parts.append(frame.payload)
|
|
253
|
+
if frame.final:
|
|
254
|
+
break
|
|
255
|
+
data = b''.join(parts)
|
|
256
|
+
if opcode == WebSocketFrame.BINARY:
|
|
257
|
+
return data
|
|
258
|
+
try:
|
|
259
|
+
return data.decode('utf-8')
|
|
260
|
+
except UnicodeDecodeError as error:
|
|
261
|
+
raise WebSocketError(
|
|
262
|
+
1007,
|
|
263
|
+
'Invalid UTF-8 text',
|
|
264
|
+
) from error
|
|
265
|
+
except WebSocketError as error:
|
|
266
|
+
self.fail(error)
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
def close(
|
|
270
|
+
self,
|
|
271
|
+
code: int = 1000,
|
|
272
|
+
reason: str = '',
|
|
273
|
+
) -> None:
|
|
274
|
+
"""Send a close frame and discard messages until the client's close frame arrives."""
|
|
275
|
+
if not self.valid_code(code):
|
|
276
|
+
raise ValueError('Invalid WebSocket close code')
|
|
277
|
+
if len(reason.encode('utf-8')) > 123:
|
|
278
|
+
raise ValueError('WebSocket close reason must be at most 123 bytes')
|
|
279
|
+
with self.lock:
|
|
280
|
+
send = not self.close_sent
|
|
281
|
+
if send:
|
|
282
|
+
self.write(self.close_frame(
|
|
283
|
+
code,
|
|
284
|
+
reason,
|
|
285
|
+
))
|
|
286
|
+
while not self.closed:
|
|
287
|
+
self.receive()
|
|
288
|
+
return
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
__all__ = (
|
|
2
|
+
'WebSocketError',
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class WebSocketError(Exception):
|
|
7
|
+
"""Represent a client WebSocket protocol violation with a close code."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
code: int,
|
|
12
|
+
reason: str,
|
|
13
|
+
):
|
|
14
|
+
super().__init__(reason)
|
|
15
|
+
self.code = code
|
|
16
|
+
self.reason = reason
|
|
17
|
+
return
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from struct import pack
|
|
2
|
+
|
|
3
|
+
__all__ = (
|
|
4
|
+
'WebSocketFrame',
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WebSocketFrame:
|
|
9
|
+
"""Represent a WebSocket frame and encode it as unmasked bytes for server transmission."""
|
|
10
|
+
|
|
11
|
+
CONTINUATION = 0x0
|
|
12
|
+
TEXT = 0x1
|
|
13
|
+
BINARY = 0x2
|
|
14
|
+
CLOSE = 0x8
|
|
15
|
+
PING = 0x9
|
|
16
|
+
PONG = 0xA
|
|
17
|
+
OPCODES = (
|
|
18
|
+
CONTINUATION,
|
|
19
|
+
TEXT,
|
|
20
|
+
BINARY,
|
|
21
|
+
CLOSE,
|
|
22
|
+
PING,
|
|
23
|
+
PONG,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
opcode: int,
|
|
29
|
+
payload: bytes = b'',
|
|
30
|
+
final: bool = True,
|
|
31
|
+
):
|
|
32
|
+
if opcode not in self.OPCODES:
|
|
33
|
+
raise ValueError('Unknown WebSocket opcode')
|
|
34
|
+
if opcode >= self.CLOSE and (not final or len(payload) > 125):
|
|
35
|
+
raise ValueError('Control frames must be final and at most 125 bytes')
|
|
36
|
+
self.opcode = opcode
|
|
37
|
+
self.payload = payload
|
|
38
|
+
self.final = final
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def control(self) -> bool:
|
|
43
|
+
return self.opcode >= self.CLOSE
|
|
44
|
+
|
|
45
|
+
def encode(self) -> bytes:
|
|
46
|
+
first = (0x80 if self.final else 0) | self.opcode
|
|
47
|
+
size = len(self.payload)
|
|
48
|
+
if size < 126:
|
|
49
|
+
header = pack(
|
|
50
|
+
'!BB',
|
|
51
|
+
first,
|
|
52
|
+
size,
|
|
53
|
+
)
|
|
54
|
+
elif size < 65536:
|
|
55
|
+
header = pack(
|
|
56
|
+
'!BBH',
|
|
57
|
+
first,
|
|
58
|
+
126,
|
|
59
|
+
size,
|
|
60
|
+
)
|
|
61
|
+
else:
|
|
62
|
+
header = pack(
|
|
63
|
+
'!BBQ',
|
|
64
|
+
first,
|
|
65
|
+
127,
|
|
66
|
+
size,
|
|
67
|
+
)
|
|
68
|
+
return header + self.payload
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from struct import unpack
|
|
2
|
+
|
|
3
|
+
from ...Error import ClientError
|
|
4
|
+
from ..RequestReader import (
|
|
5
|
+
ClientDisconnected,
|
|
6
|
+
RequestReader,
|
|
7
|
+
)
|
|
8
|
+
from .WebSocketError import WebSocketError
|
|
9
|
+
from .WebSocketFrame import WebSocketFrame
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
'WebSocketFrameReader',
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WebSocketFrameReader:
|
|
17
|
+
"""Read client frames one at a time from an upgraded connection and validate frame rules."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
reader: RequestReader,
|
|
22
|
+
limit: int,
|
|
23
|
+
):
|
|
24
|
+
self.reader = reader
|
|
25
|
+
self.limit = limit
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
def exact(
|
|
29
|
+
self,
|
|
30
|
+
size: int,
|
|
31
|
+
) -> bytes:
|
|
32
|
+
try:
|
|
33
|
+
return self.reader.exact(size)
|
|
34
|
+
except ClientError as error:
|
|
35
|
+
# RequestReader.exact() treats EOF as a body error, but EOF after an upgrade means disconnection.
|
|
36
|
+
raise ClientDisconnected(error.reason) from error
|
|
37
|
+
|
|
38
|
+
def unmask(
|
|
39
|
+
self,
|
|
40
|
+
data: bytes,
|
|
41
|
+
mask: bytes,
|
|
42
|
+
) -> bytes:
|
|
43
|
+
size = len(data)
|
|
44
|
+
key = (mask * (size // 4 + 1))[:size]
|
|
45
|
+
return (int.from_bytes(data, 'big') ^ int.from_bytes(key, 'big')).to_bytes(
|
|
46
|
+
size,
|
|
47
|
+
'big',
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def read(self) -> WebSocketFrame:
|
|
51
|
+
first, second = self.exact(2)
|
|
52
|
+
if first & 0x70:
|
|
53
|
+
raise WebSocketError(
|
|
54
|
+
1002,
|
|
55
|
+
'Reserved bits must be zero',
|
|
56
|
+
)
|
|
57
|
+
final = bool(first & 0x80)
|
|
58
|
+
opcode = first & 0x0F
|
|
59
|
+
if opcode not in WebSocketFrame.OPCODES:
|
|
60
|
+
raise WebSocketError(
|
|
61
|
+
1002,
|
|
62
|
+
'Unknown opcode',
|
|
63
|
+
)
|
|
64
|
+
if not second & 0x80:
|
|
65
|
+
raise WebSocketError(
|
|
66
|
+
1002,
|
|
67
|
+
'Client frames must be masked',
|
|
68
|
+
)
|
|
69
|
+
size = second & 0x7F
|
|
70
|
+
if size == 126:
|
|
71
|
+
size = unpack('!H', self.exact(2))[0]
|
|
72
|
+
elif size == 127:
|
|
73
|
+
size = unpack('!Q', self.exact(8))[0]
|
|
74
|
+
if size >> 63:
|
|
75
|
+
raise WebSocketError(
|
|
76
|
+
1002,
|
|
77
|
+
'Invalid payload length',
|
|
78
|
+
)
|
|
79
|
+
control = opcode >= WebSocketFrame.CLOSE
|
|
80
|
+
if control and (not final or size > 125):
|
|
81
|
+
raise WebSocketError(
|
|
82
|
+
1002,
|
|
83
|
+
'Control frames must be final and at most 125 bytes',
|
|
84
|
+
)
|
|
85
|
+
if not control and size > self.limit:
|
|
86
|
+
raise WebSocketError(
|
|
87
|
+
1009,
|
|
88
|
+
'Message too big',
|
|
89
|
+
)
|
|
90
|
+
mask = self.exact(4)
|
|
91
|
+
return WebSocketFrame(
|
|
92
|
+
opcode,
|
|
93
|
+
self.unmask(
|
|
94
|
+
self.exact(size),
|
|
95
|
+
mask,
|
|
96
|
+
),
|
|
97
|
+
final,
|
|
98
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from base64 import (
|
|
2
|
+
b64decode,
|
|
3
|
+
b64encode,
|
|
4
|
+
)
|
|
5
|
+
from hashlib import sha1
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from ...Error import ClientError
|
|
9
|
+
from ...Request import Request
|
|
10
|
+
from ...Response import Response
|
|
11
|
+
|
|
12
|
+
__all__ = (
|
|
13
|
+
'WebSocketHandshake',
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WebSocketHandshake:
|
|
18
|
+
"""Validate a WebSocket opening handshake request and build a 101 response."""
|
|
19
|
+
|
|
20
|
+
GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
|
21
|
+
VERSION = '13'
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
request: Request,
|
|
26
|
+
):
|
|
27
|
+
self.request = request
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
def tokens(
|
|
31
|
+
self,
|
|
32
|
+
name: str,
|
|
33
|
+
) -> list[str]:
|
|
34
|
+
value = self.request.header(name) or ''
|
|
35
|
+
return [token.strip() for token in value.split(',') if token.strip()]
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def protocols(self) -> list[str]:
|
|
39
|
+
"""Return the subprotocols requested by the client in request order."""
|
|
40
|
+
return self.tokens('Sec-WebSocket-Protocol')
|
|
41
|
+
|
|
42
|
+
def validate(self) -> str:
|
|
43
|
+
"""Validate the handshake request and return the Sec-WebSocket-Key."""
|
|
44
|
+
if self.request.method != 'GET':
|
|
45
|
+
raise ClientError('WebSocket handshake requires GET')
|
|
46
|
+
if self.request.writer.version != 'HTTP/1.1':
|
|
47
|
+
raise ClientError('WebSocket handshake requires HTTP/1.1')
|
|
48
|
+
if 'websocket' not in (token.lower() for token in self.tokens('Upgrade')):
|
|
49
|
+
raise ClientError(
|
|
50
|
+
'WebSocket upgrade is required',
|
|
51
|
+
status=426,
|
|
52
|
+
headers={'Upgrade': 'websocket'},
|
|
53
|
+
)
|
|
54
|
+
if 'upgrade' not in (token.lower() for token in self.tokens('Connection')):
|
|
55
|
+
raise ClientError('Connection: Upgrade is required')
|
|
56
|
+
if self.request.header('Sec-WebSocket-Version') != self.VERSION:
|
|
57
|
+
raise ClientError(
|
|
58
|
+
'Unsupported WebSocket version',
|
|
59
|
+
status=426,
|
|
60
|
+
headers={'Sec-WebSocket-Version': self.VERSION},
|
|
61
|
+
)
|
|
62
|
+
key = self.request.header('Sec-WebSocket-Key')
|
|
63
|
+
try:
|
|
64
|
+
valid = key is not None and len(b64decode(key, validate=True)) == 16
|
|
65
|
+
except ValueError:
|
|
66
|
+
valid = False
|
|
67
|
+
if key is None or not valid:
|
|
68
|
+
raise ClientError('Invalid Sec-WebSocket-Key')
|
|
69
|
+
return key
|
|
70
|
+
|
|
71
|
+
def accept_key(
|
|
72
|
+
self,
|
|
73
|
+
key: str,
|
|
74
|
+
) -> str:
|
|
75
|
+
return b64encode(sha1((key + self.GUID).encode('ascii')).digest()).decode('ascii')
|
|
76
|
+
|
|
77
|
+
def response(
|
|
78
|
+
self,
|
|
79
|
+
protocol: Optional[str] = None,
|
|
80
|
+
) -> Response:
|
|
81
|
+
key = self.validate()
|
|
82
|
+
if protocol is not None and protocol not in self.protocols:
|
|
83
|
+
raise ValueError('Selected subprotocol was not requested by the client')
|
|
84
|
+
headers = {
|
|
85
|
+
'Upgrade': 'websocket',
|
|
86
|
+
'Connection': 'Upgrade',
|
|
87
|
+
'Sec-WebSocket-Accept': self.accept_key(key),
|
|
88
|
+
}
|
|
89
|
+
if protocol is not None:
|
|
90
|
+
headers['Sec-WebSocket-Protocol'] = protocol
|
|
91
|
+
return Response(
|
|
92
|
+
status=101,
|
|
93
|
+
headers=headers,
|
|
94
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .WebSocket import WebSocket
|
|
2
|
+
from .WebSocketError import WebSocketError
|
|
3
|
+
from .WebSocketFrame import WebSocketFrame
|
|
4
|
+
from .WebSocketFrameReader import WebSocketFrameReader
|
|
5
|
+
from .WebSocketHandshake import WebSocketHandshake
|
|
6
|
+
|
|
7
|
+
__all__ = (
|
|
8
|
+
'WebSocket',
|
|
9
|
+
'WebSocketError',
|
|
10
|
+
'WebSocketFrame',
|
|
11
|
+
'WebSocketFrameReader',
|
|
12
|
+
'WebSocketHandshake',
|
|
13
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .StreamWriter import StreamWriter
|
|
2
|
+
|
|
3
|
+
__all__ = (
|
|
4
|
+
'ChunkedStreamWriter',
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ChunkedStreamWriter(StreamWriter):
|
|
9
|
+
"""Write directly using HTTP/1.1 chunked framing."""
|
|
10
|
+
|
|
11
|
+
chunked = True
|
|
12
|
+
|
|
13
|
+
def chunk(
|
|
14
|
+
self,
|
|
15
|
+
data: bytes,
|
|
16
|
+
) -> None:
|
|
17
|
+
self.write(data)
|
|
18
|
+
self.flush()
|
|
19
|
+
return
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
__all__ = (
|
|
5
|
+
'ServerSentEvents',
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class ServerSentEvents:
|
|
11
|
+
"""Encode UTF-8 SSE events for both direct writes and iterable output."""
|
|
12
|
+
|
|
13
|
+
data: str = ''
|
|
14
|
+
event: Optional[str] = None
|
|
15
|
+
id: Optional[str] = None
|
|
16
|
+
retry: Optional[int] = None
|
|
17
|
+
|
|
18
|
+
def retry_line(self) -> str:
|
|
19
|
+
"""Return a validated reconnection delay field."""
|
|
20
|
+
if self.retry is None:
|
|
21
|
+
return ''
|
|
22
|
+
if isinstance(self.retry, bool) or not isinstance(self.retry, int) or self.retry < 0:
|
|
23
|
+
raise ValueError('SSE retry must be a non-negative integer')
|
|
24
|
+
return 'retry: ' + str(self.retry) + '\n'
|
|
25
|
+
|
|
26
|
+
def encode(self) -> bytes:
|
|
27
|
+
lines = []
|
|
28
|
+
for name, value in (
|
|
29
|
+
('event', self.event),
|
|
30
|
+
('id', self.id),
|
|
31
|
+
):
|
|
32
|
+
if value is None:
|
|
33
|
+
continue
|
|
34
|
+
if '\r' in value or '\n' in value or (name == 'id' and '\x00' in value):
|
|
35
|
+
raise ValueError('Invalid SSE ' + name)
|
|
36
|
+
lines.append(name + ': ' + value)
|
|
37
|
+
if self.retry is not None:
|
|
38
|
+
lines.append(self.retry_line().rstrip('\n'))
|
|
39
|
+
data = self.data.replace('\r\n', '\n').replace('\r', '\n')
|
|
40
|
+
lines.extend('data: ' + line for line in data.split('\n'))
|
|
41
|
+
return ('\n'.join(lines) + '\n\n').encode('utf-8')
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Mapping,
|
|
3
|
+
Optional,
|
|
4
|
+
Union,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
from ..ResponseWriter import ResponseWriter
|
|
8
|
+
from .ServerSentEvents import ServerSentEvents
|
|
9
|
+
from .StreamWriter import StreamWriter
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
'ServerSentEventsWriter',
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ServerSentEventsWriter:
|
|
17
|
+
"""Wrap a plain or chunked stream to send SSE events immediately."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
writer: Union[ResponseWriter, StreamWriter],
|
|
22
|
+
):
|
|
23
|
+
self.writer = StreamWriter(writer) if isinstance(writer, ResponseWriter) else writer
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
def begin(
|
|
27
|
+
self,
|
|
28
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
properties = dict(headers) if headers is not None else {}
|
|
31
|
+
if not any(key.lower() == 'cache-control' for key in properties):
|
|
32
|
+
properties['Cache-Control'] = 'no-cache'
|
|
33
|
+
self.writer.begin(
|
|
34
|
+
headers=properties,
|
|
35
|
+
format='text/event-stream',
|
|
36
|
+
charset='utf-8',
|
|
37
|
+
)
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
def emit(
|
|
41
|
+
self,
|
|
42
|
+
data: str,
|
|
43
|
+
event: Optional[str] = None,
|
|
44
|
+
id: Optional[str] = None,
|
|
45
|
+
retry: Optional[int] = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
self.writer.write(ServerSentEvents(
|
|
48
|
+
data=data,
|
|
49
|
+
event=event,
|
|
50
|
+
id=id,
|
|
51
|
+
retry=retry,
|
|
52
|
+
).encode())
|
|
53
|
+
self.writer.flush()
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
def retry(
|
|
57
|
+
self,
|
|
58
|
+
ms: int,
|
|
59
|
+
) -> None:
|
|
60
|
+
frame = ServerSentEvents(retry=ms).retry_line()
|
|
61
|
+
self.writer.write((frame + '\n').encode('utf-8'))
|
|
62
|
+
self.writer.flush()
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
def end(self) -> None:
|
|
66
|
+
self.writer.end()
|
|
67
|
+
return
|