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,237 @@
|
|
1
|
+
import enum
|
2
|
+
import logging
|
3
|
+
import time
|
4
|
+
import typing as ta
|
5
|
+
import urllib.parse
|
6
|
+
|
7
|
+
from ..config import Config
|
8
|
+
from ..events import Body
|
9
|
+
from ..events import EndBody
|
10
|
+
from ..events import InformationalResponse
|
11
|
+
from ..events import ProtocolEvent
|
12
|
+
from ..events import Request
|
13
|
+
from ..events import Response
|
14
|
+
from ..events import StreamClosed
|
15
|
+
from ..taskspawner import TaskSpawner
|
16
|
+
from ..types import AppWrapper
|
17
|
+
from ..types import AsgiSendEvent
|
18
|
+
from ..types import HttpResponseStartEvent
|
19
|
+
from ..types import HttpScope
|
20
|
+
from ..types import UnexpectedMessageError
|
21
|
+
from ..workercontext import WorkerContext
|
22
|
+
from .utils import build_and_validate_headers
|
23
|
+
from .utils import log_access
|
24
|
+
from .utils import suppress_body
|
25
|
+
from .utils import valid_server_name
|
26
|
+
|
27
|
+
|
28
|
+
log = logging.getLogger(__name__)
|
29
|
+
|
30
|
+
|
31
|
+
PUSH_VERSIONS = {'2', '3'}
|
32
|
+
EARLY_HINTS_VERSIONS = {'2', '3'}
|
33
|
+
|
34
|
+
|
35
|
+
class AsgiHttpState(enum.Enum):
|
36
|
+
# The Asgi Spec is clear that a response should not start till the framework has sent at least one body message
|
37
|
+
# hence why this state tracking is required.
|
38
|
+
REQUEST = enum.auto()
|
39
|
+
RESPONSE = enum.auto()
|
40
|
+
CLOSED = enum.auto()
|
41
|
+
|
42
|
+
|
43
|
+
class HttpStream:
|
44
|
+
def __init__(
|
45
|
+
self,
|
46
|
+
app: AppWrapper,
|
47
|
+
config: Config,
|
48
|
+
context: WorkerContext,
|
49
|
+
task_spawner: TaskSpawner,
|
50
|
+
client: tuple[str, int] | None,
|
51
|
+
server: tuple[str, int] | None,
|
52
|
+
send: ta.Callable[[ProtocolEvent], ta.Awaitable[None]],
|
53
|
+
stream_id: int,
|
54
|
+
) -> None:
|
55
|
+
super().__init__()
|
56
|
+
self.app = app
|
57
|
+
self.client = client
|
58
|
+
self.closed = False
|
59
|
+
self.config = config
|
60
|
+
self.context = context
|
61
|
+
self.response: HttpResponseStartEvent
|
62
|
+
self.scope: HttpScope
|
63
|
+
self.send = send
|
64
|
+
self.scheme = 'http'
|
65
|
+
self.server = server
|
66
|
+
self.start_time: float
|
67
|
+
self.state = AsgiHttpState.REQUEST
|
68
|
+
self.stream_id = stream_id
|
69
|
+
self.task_spawner = task_spawner
|
70
|
+
|
71
|
+
@property
|
72
|
+
def idle(self) -> bool:
|
73
|
+
return False
|
74
|
+
|
75
|
+
async def handle(self, event: ProtocolEvent) -> None:
|
76
|
+
if self.closed:
|
77
|
+
return
|
78
|
+
|
79
|
+
elif isinstance(event, Request):
|
80
|
+
self.start_time = time.time()
|
81
|
+
|
82
|
+
path, _, query_string = event.raw_path.partition(b'?')
|
83
|
+
|
84
|
+
self.scope = {
|
85
|
+
'type': 'http',
|
86
|
+
'http_version': event.http_version,
|
87
|
+
'asgi': {'spec_version': '2.1', 'version': '3.0'},
|
88
|
+
'method': event.method,
|
89
|
+
'scheme': self.scheme,
|
90
|
+
'path': urllib.parse.unquote(path.decode('ascii')),
|
91
|
+
'raw_path': path,
|
92
|
+
'query_string': query_string,
|
93
|
+
'headers': event.headers,
|
94
|
+
'client': self.client,
|
95
|
+
'server': self.server,
|
96
|
+
'extensions': {},
|
97
|
+
}
|
98
|
+
|
99
|
+
if event.http_version in PUSH_VERSIONS:
|
100
|
+
self.scope['extensions']['http.response.push'] = {}
|
101
|
+
|
102
|
+
if event.http_version in EARLY_HINTS_VERSIONS:
|
103
|
+
self.scope['extensions']['http.response.early_hint'] = {}
|
104
|
+
|
105
|
+
if valid_server_name(self.config, event):
|
106
|
+
self.app_put = await self.task_spawner.spawn_app(self.app, self.config, self.scope, self.app_send)
|
107
|
+
|
108
|
+
else:
|
109
|
+
await self._send_error_response(404)
|
110
|
+
self.closed = True
|
111
|
+
|
112
|
+
elif isinstance(event, Body):
|
113
|
+
await self.app_put({'type': 'http.request', 'body': bytes(event.data), 'more_body': True})
|
114
|
+
|
115
|
+
elif isinstance(event, EndBody):
|
116
|
+
await self.app_put({'type': 'http.request', 'body': b'', 'more_body': False})
|
117
|
+
|
118
|
+
elif isinstance(event, StreamClosed):
|
119
|
+
self.closed = True
|
120
|
+
|
121
|
+
if self.state != AsgiHttpState.CLOSED:
|
122
|
+
log_access(self.config, self.scope, None, time.time() - self.start_time)
|
123
|
+
|
124
|
+
if self.app_put is not None:
|
125
|
+
await self.app_put({'type': 'http.disconnect'})
|
126
|
+
|
127
|
+
async def app_send(self, message: AsgiSendEvent | None) -> None:
|
128
|
+
if message is None: # Asgi App has finished sending messages
|
129
|
+
if not self.closed:
|
130
|
+
# Cleanup if required
|
131
|
+
if self.state == AsgiHttpState.REQUEST:
|
132
|
+
await self._send_error_response(500)
|
133
|
+
|
134
|
+
await self.send(StreamClosed(stream_id=self.stream_id))
|
135
|
+
|
136
|
+
elif message['type'] == 'http.response.start' and self.state == AsgiHttpState.REQUEST:
|
137
|
+
self.response = message
|
138
|
+
|
139
|
+
elif (
|
140
|
+
message['type'] == 'http.response.push' and
|
141
|
+
self.scope['http_version'] in PUSH_VERSIONS
|
142
|
+
):
|
143
|
+
if not isinstance(message['path'], str):
|
144
|
+
raise TypeError(f'{message["path"]} should be a str')
|
145
|
+
|
146
|
+
headers = [(b':scheme', self.scope['scheme'].encode())]
|
147
|
+
for name, value in self.scope['headers']:
|
148
|
+
if name == b'host':
|
149
|
+
headers.append((b':authority', value))
|
150
|
+
|
151
|
+
headers.extend(build_and_validate_headers(message['headers']))
|
152
|
+
|
153
|
+
await self.send(Request(
|
154
|
+
stream_id=self.stream_id,
|
155
|
+
headers=headers,
|
156
|
+
http_version=self.scope['http_version'],
|
157
|
+
method='GET',
|
158
|
+
raw_path=message['path'].encode(),
|
159
|
+
))
|
160
|
+
|
161
|
+
elif (
|
162
|
+
message['type'] == 'http.response.early_hint'
|
163
|
+
and self.scope['http_version'] in EARLY_HINTS_VERSIONS
|
164
|
+
and self.state == AsgiHttpState.REQUEST
|
165
|
+
):
|
166
|
+
headers = [(b'link', bytes(link).strip()) for link in message['links']]
|
167
|
+
|
168
|
+
await self.send(
|
169
|
+
InformationalResponse(
|
170
|
+
stream_id=self.stream_id,
|
171
|
+
headers=headers,
|
172
|
+
status_code=103,
|
173
|
+
),
|
174
|
+
)
|
175
|
+
|
176
|
+
elif message['type'] == 'http.response.body' and self.state in {
|
177
|
+
AsgiHttpState.REQUEST,
|
178
|
+
AsgiHttpState.RESPONSE,
|
179
|
+
}:
|
180
|
+
if self.state == AsgiHttpState.REQUEST:
|
181
|
+
headers = build_and_validate_headers(self.response.get('headers', []))
|
182
|
+
|
183
|
+
await self.send(Response(
|
184
|
+
stream_id=self.stream_id,
|
185
|
+
headers=headers,
|
186
|
+
status_code=int(self.response['status']),
|
187
|
+
))
|
188
|
+
|
189
|
+
self.state = AsgiHttpState.RESPONSE
|
190
|
+
|
191
|
+
if (
|
192
|
+
not suppress_body(self.scope['method'], int(self.response['status']))
|
193
|
+
and message.get('body', b'') != b''
|
194
|
+
):
|
195
|
+
await self.send(Body(stream_id=self.stream_id, data=bytes(message.get('body', b''))))
|
196
|
+
|
197
|
+
if not message.get('more_body', False):
|
198
|
+
if self.state != AsgiHttpState.CLOSED:
|
199
|
+
self.state = AsgiHttpState.CLOSED
|
200
|
+
|
201
|
+
log_access(
|
202
|
+
self.config,
|
203
|
+
self.scope,
|
204
|
+
self.response, # type: ignore # FIXME # noqa
|
205
|
+
time.time() - self.start_time,
|
206
|
+
)
|
207
|
+
|
208
|
+
await self.send(EndBody(stream_id=self.stream_id))
|
209
|
+
|
210
|
+
await self.send(StreamClosed(stream_id=self.stream_id))
|
211
|
+
|
212
|
+
else:
|
213
|
+
raise UnexpectedMessageError(self.state, message['type'])
|
214
|
+
|
215
|
+
async def _send_error_response(self, status_code: int) -> None:
|
216
|
+
await self.send(Response(
|
217
|
+
stream_id=self.stream_id,
|
218
|
+
headers=[
|
219
|
+
(b'content-length', b'0'),
|
220
|
+
(b'connection', b'close'),
|
221
|
+
],
|
222
|
+
status_code=status_code,
|
223
|
+
))
|
224
|
+
|
225
|
+
await self.send(EndBody(stream_id=self.stream_id))
|
226
|
+
|
227
|
+
self.state = AsgiHttpState.CLOSED
|
228
|
+
|
229
|
+
log_access(
|
230
|
+
self.config,
|
231
|
+
self.scope,
|
232
|
+
{
|
233
|
+
'status': status_code,
|
234
|
+
'headers': [],
|
235
|
+
},
|
236
|
+
time.time() - self.start_time,
|
237
|
+
)
|
@@ -0,0 +1,53 @@
|
|
1
|
+
import logging
|
2
|
+
import typing as ta
|
3
|
+
|
4
|
+
from ..config import Config
|
5
|
+
from ..events import Request
|
6
|
+
from ..types import Scope
|
7
|
+
|
8
|
+
|
9
|
+
log = logging.getLogger(__name__)
|
10
|
+
|
11
|
+
|
12
|
+
def valid_server_name(config: Config, request: Request) -> bool:
|
13
|
+
if not config.server_names:
|
14
|
+
return True
|
15
|
+
|
16
|
+
host = ''
|
17
|
+
for name, value in request.headers:
|
18
|
+
if name.lower() == b'host':
|
19
|
+
host = value.decode()
|
20
|
+
break
|
21
|
+
return host in config.server_names
|
22
|
+
|
23
|
+
|
24
|
+
def build_and_validate_headers(headers: ta.Iterable[tuple[bytes, bytes]]) -> list[tuple[bytes, bytes]]:
|
25
|
+
# Validates that the header name and value are bytes
|
26
|
+
validated_headers: list[tuple[bytes, bytes]] = []
|
27
|
+
for name, value in headers:
|
28
|
+
if name[0] == b':'[0]:
|
29
|
+
raise ValueError('Pseudo headers are not valid')
|
30
|
+
validated_headers.append((bytes(name).strip(), bytes(value).strip()))
|
31
|
+
return validated_headers
|
32
|
+
|
33
|
+
|
34
|
+
def suppress_body(method: str, status_code: int) -> bool:
|
35
|
+
return method == 'HEAD' or 100 <= status_code < 200 or status_code in {204, 304}
|
36
|
+
|
37
|
+
|
38
|
+
class ResponseSummary(ta.TypedDict):
|
39
|
+
status: int
|
40
|
+
headers: ta.Iterable[tuple[bytes, bytes]]
|
41
|
+
|
42
|
+
|
43
|
+
def log_access(
|
44
|
+
config: Config,
|
45
|
+
request: 'Scope',
|
46
|
+
response: ta.Optional['ResponseSummary'],
|
47
|
+
request_time: float,
|
48
|
+
) -> None:
|
49
|
+
# if self.access_logger is not None:
|
50
|
+
# self.access_logger.info(
|
51
|
+
# self.access_log_format, self.atoms(request, response, request_time)
|
52
|
+
# )
|
53
|
+
log.info('access: %r %r', request, response)
|