omserv 0.0.0.dev7__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.
- omserv/__about__.py +28 -0
- omserv/__init__.py +0 -0
- omserv/apps/__init__.py +0 -0
- omserv/apps/base.py +23 -0
- omserv/apps/inject.py +89 -0
- omserv/apps/markers.py +41 -0
- omserv/apps/routes.py +139 -0
- omserv/apps/sessions.py +57 -0
- omserv/apps/templates.py +90 -0
- omserv/dbs.py +24 -0
- omserv/node/__init__.py +0 -0
- omserv/node/models.py +53 -0
- omserv/node/registry.py +124 -0
- omserv/node/sql.py +131 -0
- omserv/secrets.py +12 -0
- omserv/server/__init__.py +18 -0
- omserv/server/config.py +51 -0
- omserv/server/debug.py +14 -0
- omserv/server/events.py +83 -0
- omserv/server/headers.py +36 -0
- omserv/server/lifespans.py +132 -0
- omserv/server/multiprocess.py +157 -0
- omserv/server/protocols/__init__.py +1 -0
- omserv/server/protocols/h11.py +334 -0
- omserv/server/protocols/h2.py +407 -0
- omserv/server/protocols/protocols.py +91 -0
- omserv/server/protocols/types.py +18 -0
- omserv/server/resources/__init__.py +8 -0
- omserv/server/sockets.py +111 -0
- omserv/server/ssl.py +47 -0
- omserv/server/streams/__init__.py +0 -0
- omserv/server/streams/httpstream.py +237 -0
- omserv/server/streams/utils.py +53 -0
- omserv/server/streams/wsstream.py +447 -0
- omserv/server/taskspawner.py +111 -0
- omserv/server/tcpserver.py +173 -0
- omserv/server/types.py +94 -0
- omserv/server/workercontext.py +52 -0
- omserv/server/workers.py +193 -0
- omserv-0.0.0.dev7.dist-info/LICENSE +21 -0
- omserv-0.0.0.dev7.dist-info/METADATA +21 -0
- omserv-0.0.0.dev7.dist-info/RECORD +44 -0
- omserv-0.0.0.dev7.dist-info/WHEEL +5 -0
- omserv-0.0.0.dev7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,334 @@
|
|
1
|
+
import itertools
|
2
|
+
import typing as ta
|
3
|
+
|
4
|
+
import h11
|
5
|
+
|
6
|
+
from ..config import Config
|
7
|
+
from ..events import Body
|
8
|
+
from ..events import Closed
|
9
|
+
from ..events import Data
|
10
|
+
from ..events import EndBody
|
11
|
+
from ..events import EndData
|
12
|
+
from ..events import InformationalResponse
|
13
|
+
from ..events import ProtocolEvent
|
14
|
+
from ..events import RawData
|
15
|
+
from ..events import Request
|
16
|
+
from ..events import Response
|
17
|
+
from ..events import ServerEvent
|
18
|
+
from ..events import StreamClosed
|
19
|
+
from ..events import Updated
|
20
|
+
from ..headers import response_headers
|
21
|
+
from ..streams.httpstream import HttpStream
|
22
|
+
from ..streams.wsstream import WsStream
|
23
|
+
from ..taskspawner import TaskSpawner
|
24
|
+
from ..types import AppWrapper
|
25
|
+
from ..workercontext import WorkerContext
|
26
|
+
from .types import Protocol
|
27
|
+
|
28
|
+
|
29
|
+
H11SendableEvent: ta.TypeAlias = ta.Union[ # noqa
|
30
|
+
h11.Data,
|
31
|
+
h11.EndOfMessage,
|
32
|
+
h11.InformationalResponse,
|
33
|
+
h11.Response,
|
34
|
+
]
|
35
|
+
|
36
|
+
|
37
|
+
STREAM_ID = 1
|
38
|
+
|
39
|
+
|
40
|
+
class H2CProtocolRequiredError(Exception):
|
41
|
+
def __init__(self, data: bytes, request: h11.Request) -> None:
|
42
|
+
super().__init__()
|
43
|
+
settings = ''
|
44
|
+
headers = [(b':method', request.method), (b':path', request.target)]
|
45
|
+
for name, value in request.headers:
|
46
|
+
if name.lower() == b'http2-settings':
|
47
|
+
settings = value.decode()
|
48
|
+
elif name.lower() == b'host':
|
49
|
+
headers.append((b':authority', value))
|
50
|
+
headers.append((name, value))
|
51
|
+
|
52
|
+
self.data = data
|
53
|
+
self.headers = headers
|
54
|
+
self.settings = settings
|
55
|
+
|
56
|
+
|
57
|
+
class H2ProtocolAssumedError(Exception):
|
58
|
+
def __init__(self, data: bytes) -> None:
|
59
|
+
super().__init__()
|
60
|
+
self.data = data
|
61
|
+
|
62
|
+
|
63
|
+
class H11WsConnection:
|
64
|
+
# This class matches the h11 interface, and either passes data
|
65
|
+
# through without altering it (for Data, EndData) or sends h11
|
66
|
+
# events (Response, Body, EndBody).
|
67
|
+
our_state = None # Prevents recycling the connection
|
68
|
+
they_are_waiting_for_100_continue = False
|
69
|
+
their_state = None
|
70
|
+
trailing_data = (b'', False)
|
71
|
+
|
72
|
+
def __init__(self, h11_connection: h11.Connection) -> None:
|
73
|
+
self.buffer = bytearray(h11_connection.trailing_data[0])
|
74
|
+
self.h11_connection = h11_connection
|
75
|
+
|
76
|
+
def receive_data(self, data: bytes) -> None:
|
77
|
+
self.buffer.extend(data)
|
78
|
+
|
79
|
+
def next_event(self) -> Data | type[h11.NEED_DATA]:
|
80
|
+
if self.buffer:
|
81
|
+
event = Data(stream_id=STREAM_ID, data=bytes(self.buffer))
|
82
|
+
self.buffer = bytearray()
|
83
|
+
return event
|
84
|
+
else:
|
85
|
+
return h11.NEED_DATA
|
86
|
+
|
87
|
+
def send(self, event: H11SendableEvent) -> bytes | None:
|
88
|
+
return self.h11_connection.send(event)
|
89
|
+
|
90
|
+
def start_next_cycle(self) -> None:
|
91
|
+
pass
|
92
|
+
|
93
|
+
|
94
|
+
class H11Protocol(Protocol):
|
95
|
+
def __init__(
|
96
|
+
self,
|
97
|
+
app: AppWrapper,
|
98
|
+
config: Config,
|
99
|
+
context: WorkerContext,
|
100
|
+
task_spawner: TaskSpawner,
|
101
|
+
client: tuple[str, int] | None,
|
102
|
+
server: tuple[str, int] | None,
|
103
|
+
send: ta.Callable[[ServerEvent], ta.Awaitable[None]],
|
104
|
+
) -> None:
|
105
|
+
self.app = app
|
106
|
+
self.can_read = context.event_class()
|
107
|
+
self.client = client
|
108
|
+
self.config = config
|
109
|
+
self.connection: h11.Connection | H11WsConnection = h11.Connection(
|
110
|
+
h11.SERVER, max_incomplete_event_size=self.config.h11_max_incomplete_size,
|
111
|
+
)
|
112
|
+
self.context = context
|
113
|
+
self.keep_alive_requests = 0
|
114
|
+
self.send = send
|
115
|
+
self.server = server
|
116
|
+
self.stream: HttpStream | WsStream | None = None
|
117
|
+
self.task_spawner = task_spawner
|
118
|
+
|
119
|
+
async def initiate(self) -> None:
|
120
|
+
pass
|
121
|
+
|
122
|
+
async def handle(self, event: ServerEvent) -> None:
|
123
|
+
if isinstance(event, RawData):
|
124
|
+
self.connection.receive_data(event.data)
|
125
|
+
await self._handle_events()
|
126
|
+
elif isinstance(event, Closed):
|
127
|
+
if self.stream is not None:
|
128
|
+
await self._close_stream()
|
129
|
+
|
130
|
+
async def stream_send(self, event: ProtocolEvent) -> None:
|
131
|
+
if isinstance(event, Response):
|
132
|
+
if event.status_code >= 200:
|
133
|
+
headers = list(itertools.chain(event.headers, response_headers(self.config, 'h11')))
|
134
|
+
if self.keep_alive_requests >= self.config.keep_alive_max_requests:
|
135
|
+
headers.append((b'connection', b'close'))
|
136
|
+
await self._send_h11_event(h11.Response(
|
137
|
+
headers=headers,
|
138
|
+
status_code=event.status_code,
|
139
|
+
))
|
140
|
+
else:
|
141
|
+
await self._send_h11_event(h11.InformationalResponse(
|
142
|
+
headers=list(itertools.chain(event.headers, response_headers(self.config, 'h11'))),
|
143
|
+
status_code=event.status_code,
|
144
|
+
))
|
145
|
+
|
146
|
+
elif isinstance(event, InformationalResponse):
|
147
|
+
pass # Ignore for HTTP/1
|
148
|
+
|
149
|
+
elif isinstance(event, Body):
|
150
|
+
await self._send_h11_event(h11.Data(data=event.data))
|
151
|
+
|
152
|
+
elif isinstance(event, EndBody):
|
153
|
+
await self._send_h11_event(h11.EndOfMessage())
|
154
|
+
|
155
|
+
elif isinstance(event, Data):
|
156
|
+
await self.send(RawData(data=event.data))
|
157
|
+
|
158
|
+
elif isinstance(event, EndData):
|
159
|
+
pass
|
160
|
+
|
161
|
+
elif isinstance(event, StreamClosed):
|
162
|
+
await self._maybe_recycle()
|
163
|
+
|
164
|
+
async def _handle_events(self) -> None:
|
165
|
+
while True:
|
166
|
+
if self.connection.they_are_waiting_for_100_continue:
|
167
|
+
await self._send_h11_event(h11.InformationalResponse(
|
168
|
+
status_code=100, headers=response_headers(self.config, 'h11'),
|
169
|
+
))
|
170
|
+
|
171
|
+
try:
|
172
|
+
event = self.connection.next_event()
|
173
|
+
except h11.RemoteProtocolError as error:
|
174
|
+
if self.connection.our_state in {h11.IDLE, h11.SEND_RESPONSE}:
|
175
|
+
await self._send_error_response(error.error_status_hint)
|
176
|
+
await self.send(Closed())
|
177
|
+
break
|
178
|
+
|
179
|
+
else:
|
180
|
+
if isinstance(event, h11.Request):
|
181
|
+
await self.send(Updated(idle=False))
|
182
|
+
await self._check_protocol(event)
|
183
|
+
await self._create_stream(event)
|
184
|
+
|
185
|
+
elif event is h11.PAUSED:
|
186
|
+
await self.can_read.clear()
|
187
|
+
await self.can_read.wait()
|
188
|
+
|
189
|
+
elif isinstance(event, h11.ConnectionClosed) or event is h11.NEED_DATA:
|
190
|
+
break
|
191
|
+
|
192
|
+
elif self.stream is None:
|
193
|
+
break
|
194
|
+
|
195
|
+
elif isinstance(event, h11.Data):
|
196
|
+
await self.stream.handle(Body(stream_id=STREAM_ID, data=event.data))
|
197
|
+
|
198
|
+
elif isinstance(event, h11.EndOfMessage):
|
199
|
+
await self.stream.handle(EndBody(stream_id=STREAM_ID))
|
200
|
+
|
201
|
+
elif isinstance(event, Data):
|
202
|
+
# WebSocket pass through
|
203
|
+
await self.stream.handle(event)
|
204
|
+
|
205
|
+
async def _create_stream(self, request: h11.Request) -> None:
|
206
|
+
upgrade_value = ''
|
207
|
+
connection_value = ''
|
208
|
+
for name, value in request.headers:
|
209
|
+
sanitised_name = name.decode('latin1').strip().lower()
|
210
|
+
if sanitised_name == 'upgrade':
|
211
|
+
upgrade_value = value.decode('latin1').strip()
|
212
|
+
elif sanitised_name == 'connection':
|
213
|
+
connection_value = value.decode('latin1').strip()
|
214
|
+
|
215
|
+
connection_tokens = connection_value.lower().split(',')
|
216
|
+
if (
|
217
|
+
any(token.strip() == 'upgrade' for token in connection_tokens)
|
218
|
+
and upgrade_value.lower() == 'websocket'
|
219
|
+
and request.method.decode('ascii').upper() == 'GET'
|
220
|
+
):
|
221
|
+
self.stream = WsStream(
|
222
|
+
self.app,
|
223
|
+
self.config,
|
224
|
+
self.context,
|
225
|
+
self.task_spawner,
|
226
|
+
self.client,
|
227
|
+
self.server,
|
228
|
+
self.stream_send,
|
229
|
+
STREAM_ID,
|
230
|
+
)
|
231
|
+
self.connection = H11WsConnection(ta.cast(h11.Connection, self.connection))
|
232
|
+
else:
|
233
|
+
self.stream = HttpStream(
|
234
|
+
self.app,
|
235
|
+
self.config,
|
236
|
+
self.context,
|
237
|
+
self.task_spawner,
|
238
|
+
self.client,
|
239
|
+
self.server,
|
240
|
+
self.stream_send,
|
241
|
+
STREAM_ID,
|
242
|
+
)
|
243
|
+
|
244
|
+
if self.config.h11_pass_raw_headers:
|
245
|
+
headers = request.headers.raw_items()
|
246
|
+
else:
|
247
|
+
headers = list(request.headers)
|
248
|
+
|
249
|
+
await self.stream.handle( # type: ignore
|
250
|
+
Request(
|
251
|
+
stream_id=STREAM_ID,
|
252
|
+
headers=headers,
|
253
|
+
http_version=request.http_version.decode(),
|
254
|
+
method=request.method.decode('ascii').upper(),
|
255
|
+
raw_path=request.target,
|
256
|
+
),
|
257
|
+
)
|
258
|
+
self.keep_alive_requests += 1
|
259
|
+
await self.context.mark_request()
|
260
|
+
|
261
|
+
async def _send_h11_event(self, event: H11SendableEvent) -> None:
|
262
|
+
try:
|
263
|
+
data = self.connection.send(event)
|
264
|
+
except h11.LocalProtocolError:
|
265
|
+
if self.connection.their_state != h11.ERROR:
|
266
|
+
raise
|
267
|
+
else:
|
268
|
+
if data is None:
|
269
|
+
raise Exception('closed') # FIXME
|
270
|
+
await self.send(RawData(data=data))
|
271
|
+
|
272
|
+
async def _send_error_response(self, status_code: int) -> None:
|
273
|
+
await self._send_h11_event(h11.Response(
|
274
|
+
status_code=status_code,
|
275
|
+
headers=list(
|
276
|
+
itertools.chain(
|
277
|
+
[(b'content-length', b'0'), (b'connection', b'close')],
|
278
|
+
response_headers(self.config, 'h11'),
|
279
|
+
),
|
280
|
+
),
|
281
|
+
))
|
282
|
+
await self._send_h11_event(h11.EndOfMessage())
|
283
|
+
|
284
|
+
async def _maybe_recycle(self) -> None:
|
285
|
+
await self._close_stream()
|
286
|
+
if (
|
287
|
+
not self.context.terminated.is_set()
|
288
|
+
and self.connection.our_state is h11.DONE
|
289
|
+
and self.connection.their_state is h11.DONE
|
290
|
+
):
|
291
|
+
try:
|
292
|
+
self.connection.start_next_cycle()
|
293
|
+
except h11.LocalProtocolError:
|
294
|
+
await self.send(Closed())
|
295
|
+
else:
|
296
|
+
self.response = None
|
297
|
+
self.scope = None
|
298
|
+
await self.can_read.set()
|
299
|
+
await self.send(Updated(idle=True))
|
300
|
+
else:
|
301
|
+
await self.can_read.set()
|
302
|
+
await self.send(Closed())
|
303
|
+
|
304
|
+
async def _close_stream(self) -> None:
|
305
|
+
if self.stream is not None:
|
306
|
+
await self.stream.handle(StreamClosed(stream_id=STREAM_ID))
|
307
|
+
self.stream = None
|
308
|
+
|
309
|
+
async def _check_protocol(self, event: h11.Request) -> None:
|
310
|
+
upgrade_value = ''
|
311
|
+
has_body = False
|
312
|
+
for name, value in event.headers:
|
313
|
+
sanitised_name = name.decode('latin1').strip().lower()
|
314
|
+
if sanitised_name == 'upgrade':
|
315
|
+
upgrade_value = value.decode('latin1').strip()
|
316
|
+
elif sanitised_name in {'content-length', 'transfer-encoding'}:
|
317
|
+
has_body = True
|
318
|
+
|
319
|
+
# h2c Upgrade requests with a body are a pain as the body must be fully recieved in HTTP/1.1 before the upgrade
|
320
|
+
# response and HTTP/2 takes over, so Hypercorn ignores the upgrade and responds in HTTP/1.1. Use a preflight
|
321
|
+
# OPTIONS request to initiate the upgrade if really required (or just use h2).
|
322
|
+
if upgrade_value.lower() == 'h2c' and not has_body:
|
323
|
+
await self._send_h11_event(h11.InformationalResponse(
|
324
|
+
status_code=101,
|
325
|
+
headers=[
|
326
|
+
*response_headers(self.config, 'h11'),
|
327
|
+
(b'connection', b'upgrade'),
|
328
|
+
(b'upgrade', b'h2c'),
|
329
|
+
],
|
330
|
+
))
|
331
|
+
raise H2CProtocolRequiredError(self.connection.trailing_data[0], event)
|
332
|
+
|
333
|
+
elif event.method == b'PRI' and event.target == b'*' and event.http_version == b'2.0':
|
334
|
+
raise H2ProtocolAssumedError(b'PRI * HTTP/2.0\r\n\r\n' + self.connection.trailing_data[0])
|