DefEssential 0.1.6__19-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 +134 -0
- DefEssential/Adapter/Implements/Gunicorn/Transport.py +171 -0
- DefEssential/Adapter/Implements/Gunicorn/__init__.py +5 -0
- DefEssential/Adapter/Implements/Python/Server.py +245 -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 +102 -0
- DefEssential/Adapter/Implements/Uvicorn/WebSocket.py +220 -0
- DefEssential/Adapter/Implements/Uvicorn/__init__.py +5 -0
- DefEssential/Adapter/Implements/__init__.py +3 -0
- DefEssential/Adapter/RequestHandler.py +70 -0
- DefEssential/Adapter/Server.py +54 -0
- DefEssential/Adapter/WSGI.py +219 -0
- DefEssential/Adapter/__init__.py +5 -0
- DefEssential/Application.py +579 -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 +184 -0
- DefEssential/Extensions/File.py +89 -0
- DefEssential/Extensions/FileSystem.py +184 -0
- DefEssential/Extensions/Metrics.py +500 -0
- DefEssential/Extensions/Ping.py +89 -0
- DefEssential/Extensions/Specifications.py +328 -0
- DefEssential/Extensions/Status.html +207 -0
- DefEssential/Extensions/Status.py +104 -0
- DefEssential/Extensions/Swagger.html +77 -0
- DefEssential/Extensions/Swagger.py +111 -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 +241 -0
- DefEssential/IO/WebSocket/WebSocket.py +282 -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 +87 -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 +101 -0
- DefEssential/Runners/RunFactory.py +26 -0
- DefEssential/Runners/RunOptions.py +77 -0
- DefEssential/Runners/RunRequest.py +121 -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.6.dist-info/METADATA +41 -0
- defessential-0.1.6.dist-info/RECORD +104 -0
- defessential-0.1.6.dist-info/WHEEL +6 -0
- defessential-0.1.6.dist-info/licenses/LICENSE +674 -0
- defessential-0.1.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,282 @@
|
|
|
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
|
+
"""업그레이드된 HTTP 연결에서 WebSocket 메시지를 주고받는다.
|
|
25
|
+
|
|
26
|
+
receive()는 한 스레드에서만 호출한다. send(), ping(), close()는 다른 스레드에서도 호출할 수 있다.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
logger = getLogger('DefEssential.IO.WebSocket')
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
request: Request,
|
|
34
|
+
limit: int,
|
|
35
|
+
):
|
|
36
|
+
if limit <= 0:
|
|
37
|
+
raise ValueError('Message size limit must be positive')
|
|
38
|
+
self.request = request
|
|
39
|
+
self.limit = limit
|
|
40
|
+
self.handshake = WebSocketHandshake(request)
|
|
41
|
+
self.frames = WebSocketFrameReader(
|
|
42
|
+
request.reader,
|
|
43
|
+
limit,
|
|
44
|
+
)
|
|
45
|
+
self.lock = Lock()
|
|
46
|
+
self.accepted = False
|
|
47
|
+
self.close_sent = False
|
|
48
|
+
self.closed = False
|
|
49
|
+
self.code: Optional[int] = None
|
|
50
|
+
self.reason = ''
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def protocols(self) -> list[str]:
|
|
55
|
+
return self.handshake.protocols
|
|
56
|
+
|
|
57
|
+
def accept(
|
|
58
|
+
self,
|
|
59
|
+
protocol: Optional[str] = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""핸드셰이크를 검사하고 101 응답을 보낸다. 실패하면 101 전이므로 일반 오류 응답이 나간다."""
|
|
62
|
+
if self.accepted:
|
|
63
|
+
raise RuntimeError('WebSocket has already been accepted')
|
|
64
|
+
self.request.writer.upgrade(self.handshake.response(protocol))
|
|
65
|
+
self.accepted = True
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
def valid_code(
|
|
69
|
+
self,
|
|
70
|
+
code: int,
|
|
71
|
+
) -> bool:
|
|
72
|
+
return code in (
|
|
73
|
+
1000,
|
|
74
|
+
1001,
|
|
75
|
+
1002,
|
|
76
|
+
1003,
|
|
77
|
+
1007,
|
|
78
|
+
1008,
|
|
79
|
+
1009,
|
|
80
|
+
1010,
|
|
81
|
+
1011,
|
|
82
|
+
1012,
|
|
83
|
+
1013,
|
|
84
|
+
1014,
|
|
85
|
+
) or 3000 <= code <= 4999
|
|
86
|
+
|
|
87
|
+
def write(
|
|
88
|
+
self,
|
|
89
|
+
frame: WebSocketFrame,
|
|
90
|
+
) -> None:
|
|
91
|
+
with self.lock:
|
|
92
|
+
if not self.accepted:
|
|
93
|
+
raise RuntimeError('WebSocket must be accepted first')
|
|
94
|
+
if self.close_sent:
|
|
95
|
+
raise RuntimeError('Cannot send after the WebSocket close frame')
|
|
96
|
+
if frame.opcode == WebSocketFrame.CLOSE:
|
|
97
|
+
self.close_sent = True
|
|
98
|
+
self.request.writer.write(frame.encode())
|
|
99
|
+
self.request.writer.flush()
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
def send(
|
|
103
|
+
self,
|
|
104
|
+
message: Union[str, bytes],
|
|
105
|
+
) -> None:
|
|
106
|
+
"""str은 text 메시지로, bytes는 binary 메시지로 보낸다."""
|
|
107
|
+
if isinstance(message, str):
|
|
108
|
+
frame = WebSocketFrame(
|
|
109
|
+
WebSocketFrame.TEXT,
|
|
110
|
+
message.encode('utf-8'),
|
|
111
|
+
)
|
|
112
|
+
elif isinstance(message, bytes):
|
|
113
|
+
frame = WebSocketFrame(
|
|
114
|
+
WebSocketFrame.BINARY,
|
|
115
|
+
message,
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
raise TypeError('WebSocket messages must be str or bytes')
|
|
119
|
+
self.write(frame)
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
def ping(
|
|
123
|
+
self,
|
|
124
|
+
data: bytes = b'',
|
|
125
|
+
) -> None:
|
|
126
|
+
self.write(WebSocketFrame(
|
|
127
|
+
WebSocketFrame.PING,
|
|
128
|
+
data,
|
|
129
|
+
))
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
def close_frame(
|
|
133
|
+
self,
|
|
134
|
+
code: int,
|
|
135
|
+
reason: str,
|
|
136
|
+
) -> WebSocketFrame:
|
|
137
|
+
return WebSocketFrame(
|
|
138
|
+
WebSocketFrame.CLOSE,
|
|
139
|
+
pack('!H', code) + reason.encode('utf-8'),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def receive_close(
|
|
143
|
+
self,
|
|
144
|
+
frame: WebSocketFrame,
|
|
145
|
+
) -> None:
|
|
146
|
+
payload = frame.payload
|
|
147
|
+
if len(payload) == 1:
|
|
148
|
+
raise WebSocketError(
|
|
149
|
+
1002,
|
|
150
|
+
'Invalid close frame',
|
|
151
|
+
)
|
|
152
|
+
code = unpack('!H', payload[:2])[0] if payload else None
|
|
153
|
+
if code is not None and not self.valid_code(code):
|
|
154
|
+
raise WebSocketError(
|
|
155
|
+
1002,
|
|
156
|
+
'Invalid close code',
|
|
157
|
+
)
|
|
158
|
+
try:
|
|
159
|
+
reason = payload[2:].decode('utf-8')
|
|
160
|
+
except UnicodeDecodeError as error:
|
|
161
|
+
raise WebSocketError(
|
|
162
|
+
1007,
|
|
163
|
+
'Invalid close reason',
|
|
164
|
+
) from error
|
|
165
|
+
self.code = code
|
|
166
|
+
self.reason = reason
|
|
167
|
+
self.closed = True
|
|
168
|
+
with self.lock:
|
|
169
|
+
echo = not self.close_sent
|
|
170
|
+
if echo:
|
|
171
|
+
self.write(WebSocketFrame(
|
|
172
|
+
WebSocketFrame.CLOSE,
|
|
173
|
+
payload[:2],
|
|
174
|
+
))
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
def fail(
|
|
178
|
+
self,
|
|
179
|
+
error: WebSocketError,
|
|
180
|
+
) -> None:
|
|
181
|
+
self.logger.warning(
|
|
182
|
+
'WebSocket protocol error: %s',
|
|
183
|
+
error.reason,
|
|
184
|
+
)
|
|
185
|
+
self.code = error.code
|
|
186
|
+
self.reason = error.reason
|
|
187
|
+
self.closed = True
|
|
188
|
+
with self.lock:
|
|
189
|
+
send = not self.close_sent
|
|
190
|
+
if send:
|
|
191
|
+
self.write(self.close_frame(
|
|
192
|
+
error.code,
|
|
193
|
+
error.reason,
|
|
194
|
+
))
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
def receive(self) -> Union[str, bytes, None]:
|
|
198
|
+
"""다음 메시지를 반환한다. 연결이 닫히는 과정에 들어가면 None을 반환한다.
|
|
199
|
+
|
|
200
|
+
ping에는 자동으로 pong을 보낸다. 프로토콜 위반은 닫기 코드를 보낸 뒤 None을 반환하고,
|
|
201
|
+
TCP 연결이 끊기면 ClientDisconnected가 전파된다.
|
|
202
|
+
"""
|
|
203
|
+
if not self.accepted:
|
|
204
|
+
raise RuntimeError('WebSocket must be accepted first')
|
|
205
|
+
if self.closed:
|
|
206
|
+
return None
|
|
207
|
+
opcode: Optional[int] = None
|
|
208
|
+
parts = []
|
|
209
|
+
size = 0
|
|
210
|
+
try:
|
|
211
|
+
while True:
|
|
212
|
+
frame = self.frames.read()
|
|
213
|
+
if frame.opcode == WebSocketFrame.CLOSE:
|
|
214
|
+
self.receive_close(frame)
|
|
215
|
+
return None
|
|
216
|
+
if frame.opcode == WebSocketFrame.PING:
|
|
217
|
+
with self.lock:
|
|
218
|
+
reply = not self.close_sent
|
|
219
|
+
if reply:
|
|
220
|
+
self.write(WebSocketFrame(
|
|
221
|
+
WebSocketFrame.PONG,
|
|
222
|
+
frame.payload,
|
|
223
|
+
))
|
|
224
|
+
continue
|
|
225
|
+
if frame.opcode == WebSocketFrame.PONG:
|
|
226
|
+
continue
|
|
227
|
+
if frame.opcode == WebSocketFrame.CONTINUATION:
|
|
228
|
+
if opcode is None:
|
|
229
|
+
raise WebSocketError(
|
|
230
|
+
1002,
|
|
231
|
+
'Unexpected continuation frame',
|
|
232
|
+
)
|
|
233
|
+
elif opcode is not None:
|
|
234
|
+
raise WebSocketError(
|
|
235
|
+
1002,
|
|
236
|
+
'Expected a continuation frame',
|
|
237
|
+
)
|
|
238
|
+
else:
|
|
239
|
+
opcode = frame.opcode
|
|
240
|
+
size += len(frame.payload)
|
|
241
|
+
if size > self.limit:
|
|
242
|
+
raise WebSocketError(
|
|
243
|
+
1009,
|
|
244
|
+
'Message too big',
|
|
245
|
+
)
|
|
246
|
+
parts.append(frame.payload)
|
|
247
|
+
if frame.final:
|
|
248
|
+
break
|
|
249
|
+
data = b''.join(parts)
|
|
250
|
+
if opcode == WebSocketFrame.BINARY:
|
|
251
|
+
return data
|
|
252
|
+
try:
|
|
253
|
+
return data.decode('utf-8')
|
|
254
|
+
except UnicodeDecodeError as error:
|
|
255
|
+
raise WebSocketError(
|
|
256
|
+
1007,
|
|
257
|
+
'Invalid UTF-8 text',
|
|
258
|
+
) from error
|
|
259
|
+
except WebSocketError as error:
|
|
260
|
+
self.fail(error)
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
def close(
|
|
264
|
+
self,
|
|
265
|
+
code: int = 1000,
|
|
266
|
+
reason: str = '',
|
|
267
|
+
) -> None:
|
|
268
|
+
"""닫기 프레임을 보내고 클라이언트의 닫기 프레임을 받을 때까지 남은 메시지를 버린다."""
|
|
269
|
+
if not self.valid_code(code):
|
|
270
|
+
raise ValueError('Invalid WebSocket close code')
|
|
271
|
+
if len(reason.encode('utf-8')) > 123:
|
|
272
|
+
raise ValueError('WebSocket close reason must be at most 123 bytes')
|
|
273
|
+
with self.lock:
|
|
274
|
+
send = not self.close_sent
|
|
275
|
+
if send:
|
|
276
|
+
self.write(self.close_frame(
|
|
277
|
+
code,
|
|
278
|
+
reason,
|
|
279
|
+
))
|
|
280
|
+
while not self.closed:
|
|
281
|
+
self.receive()
|
|
282
|
+
return
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
__all__ = (
|
|
2
|
+
'WebSocketError',
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class WebSocketError(Exception):
|
|
7
|
+
"""클라이언트의 WebSocket 프로토콜 위반을 닫기 코드와 함께 표현한다."""
|
|
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
|
+
"""WebSocket 프레임 하나를 표현하고 서버가 보내는 마스크 없는 바이트로 인코딩한다."""
|
|
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
|
+
"""업그레이드된 연결에서 클라이언트 프레임을 하나씩 읽고 프레임 규칙을 검사한다."""
|
|
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()는 EOF를 본문 오류로 보고하지만, 업그레이드 뒤의 EOF는 연결 종료다.
|
|
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
|
+
"""WebSocket 열기 핸드셰이크 요청을 검증하고 101 응답을 만든다."""
|
|
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
|
+
"""클라이언트가 요청한 서브프로토콜을 요청 순서대로 반환한다."""
|
|
40
|
+
return self.tokens('Sec-WebSocket-Protocol')
|
|
41
|
+
|
|
42
|
+
def validate(self) -> str:
|
|
43
|
+
"""핸드셰이크 요청을 검사하고 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
|