uhttp-server 2.2.0__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.
- uhttp_server-2.2.0/LICENSE +21 -0
- uhttp_server-2.2.0/PKG-INFO +15 -0
- uhttp_server-2.2.0/README.md +528 -0
- uhttp_server-2.2.0/pyproject.toml +26 -0
- uhttp_server-2.2.0/setup.cfg +4 -0
- uhttp_server-2.2.0/tests/test_100_continue.py +349 -0
- uhttp_server-2.2.0/tests/test_concurrent_connections.py +236 -0
- uhttp_server-2.2.0/tests/test_content_length_security.py +243 -0
- uhttp_server-2.2.0/tests/test_data_parsing.py +472 -0
- uhttp_server-2.2.0/tests/test_double_response.py +192 -0
- uhttp_server-2.2.0/tests/test_eagain.py +267 -0
- uhttp_server-2.2.0/tests/test_error_handling.py +210 -0
- uhttp_server-2.2.0/tests/test_event_mode.py +615 -0
- uhttp_server-2.2.0/tests/test_ipv6.py +175 -0
- uhttp_server-2.2.0/tests/test_keepalive.py +346 -0
- uhttp_server-2.2.0/tests/test_keepalive_basic.py +257 -0
- uhttp_server-2.2.0/tests/test_keepalive_http10.py +271 -0
- uhttp_server-2.2.0/tests/test_keepalive_limits.py +325 -0
- uhttp_server-2.2.0/tests/test_keepalive_simple.py +253 -0
- uhttp_server-2.2.0/tests/test_multipart.py +235 -0
- uhttp_server-2.2.0/tests/test_pipelining.py +138 -0
- uhttp_server-2.2.0/tests/test_respond_file.py +324 -0
- uhttp_server-2.2.0/tests/test_respond_file_race.py +164 -0
- uhttp_server-2.2.0/tests/test_ssl.py +372 -0
- uhttp_server-2.2.0/tests/test_utils.py +158 -0
- uhttp_server-2.2.0/uhttp/server.py +1334 -0
- uhttp_server-2.2.0/uhttp_server.egg-info/PKG-INFO +15 -0
- uhttp_server-2.2.0/uhttp_server.egg-info/SOURCES.txt +28 -0
- uhttp_server-2.2.0/uhttp_server.egg-info/dependency_links.txt +1 -0
- uhttp_server-2.2.0/uhttp_server.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pavel Revak
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uhttp-server
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: Micro HTTP server for Python and MicroPython
|
|
5
|
+
Author-email: Pavel Revak <pavelrevak@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/pavelrevak/uhttp
|
|
8
|
+
Project-URL: Repository, https://github.com/pavelrevak/uhttp
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: Implementation :: MicroPython
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.7
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
# uHTTP Server
|
|
2
|
+
|
|
3
|
+
Micro HTTP server for MicroPython and CPython.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- MicroPython and CPython compatible
|
|
8
|
+
- Low-level POSIX socket implementation
|
|
9
|
+
- Fully synchronous but handles multiple connections
|
|
10
|
+
- Delayed response support (hold client, reply later)
|
|
11
|
+
- Raw data (HTML, binary) and JSON support
|
|
12
|
+
- SSL/TLS for HTTPS connections
|
|
13
|
+
- IPv6 and dual-stack support
|
|
14
|
+
- Event mode for streaming large uploads
|
|
15
|
+
- Memory-efficient (~32KB RAM minimum)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install uhttp-server
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For MicroPython, copy `uhttp/server.py` to your device.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import uhttp.server
|
|
30
|
+
|
|
31
|
+
server = uhttp.server.HttpServer(port=9980)
|
|
32
|
+
|
|
33
|
+
while True:
|
|
34
|
+
client = server.wait()
|
|
35
|
+
if client:
|
|
36
|
+
if client.path == '/':
|
|
37
|
+
# result is html
|
|
38
|
+
client.respond("<h1>hello</h1><p>uHTTP</p>")
|
|
39
|
+
elif client.path == '/rpc':
|
|
40
|
+
# result is json
|
|
41
|
+
client.respond({'message': 'hello', 'success': True, 'headers': client.headers, 'query': client.query})
|
|
42
|
+
else:
|
|
43
|
+
client.respond("Not found", status=404)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## SSL/HTTPS Support
|
|
48
|
+
|
|
49
|
+
uHTTP supports SSL/TLS encryption for HTTPS connections on both CPython and MicroPython.
|
|
50
|
+
|
|
51
|
+
### Basic HTTPS Server
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import ssl
|
|
55
|
+
import uhttp.server
|
|
56
|
+
|
|
57
|
+
# Create SSL context
|
|
58
|
+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
59
|
+
context.load_cert_chain(certfile='cert.pem', keyfile='key.pem')
|
|
60
|
+
|
|
61
|
+
# Create HTTPS server
|
|
62
|
+
server = uhttp.server.HttpServer(port=443, ssl_context=context)
|
|
63
|
+
|
|
64
|
+
while True:
|
|
65
|
+
client = server.wait()
|
|
66
|
+
if client:
|
|
67
|
+
# Check if connection is secure
|
|
68
|
+
if client.is_secure:
|
|
69
|
+
client.respond({'message': 'Secure HTTPS connection!'})
|
|
70
|
+
else:
|
|
71
|
+
client.respond({'message': 'Insecure HTTP connection'})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Using Let's Encrypt / Certbot Certificates
|
|
75
|
+
|
|
76
|
+
[Certbot](https://certbot.eff.org/) creates certificates in `/etc/letsencrypt/live/your-domain/` with these files:
|
|
77
|
+
|
|
78
|
+
- `cert.pem` - Your domain certificate only
|
|
79
|
+
- `chain.pem` - Certificate authority chain
|
|
80
|
+
- **`fullchain.pem`** - Your certificate + CA chain (use this for `certfile`)
|
|
81
|
+
- **`privkey.pem`** - Private key (use this for `keyfile`)
|
|
82
|
+
|
|
83
|
+
**Important:** Always use `fullchain.pem` (not `cert.pem`) as the certificate file. Without the full chain, clients will get "certificate verification failed" errors.
|
|
84
|
+
|
|
85
|
+
#### Example with Certbot Certificates
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import ssl
|
|
89
|
+
import uhttp.server
|
|
90
|
+
|
|
91
|
+
# Create SSL context with Let's Encrypt certificates
|
|
92
|
+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
93
|
+
context.load_cert_chain(
|
|
94
|
+
certfile='/etc/letsencrypt/live/example.com/fullchain.pem',
|
|
95
|
+
keyfile='/etc/letsencrypt/live/example.com/privkey.pem'
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Create HTTPS server
|
|
99
|
+
server = uhttp.server.HttpServer(
|
|
100
|
+
address='0.0.0.0',
|
|
101
|
+
port=443,
|
|
102
|
+
ssl_context=context
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
while True:
|
|
106
|
+
client = server.wait()
|
|
107
|
+
if client:
|
|
108
|
+
client.respond({'message': 'Hello from HTTPS!'})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### Permissions Note
|
|
112
|
+
|
|
113
|
+
The `/etc/letsencrypt/` directory requires root access. You have two options:
|
|
114
|
+
|
|
115
|
+
1. **Run as root** (not recommended for production):
|
|
116
|
+
```bash
|
|
117
|
+
sudo python3 your_server.py
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
2. **Copy certificates to accessible location** (recommended):
|
|
121
|
+
```bash
|
|
122
|
+
# Copy certificates to your application directory
|
|
123
|
+
sudo cp /etc/letsencrypt/live/example.com/fullchain.pem ~/myapp/
|
|
124
|
+
sudo cp /etc/letsencrypt/live/example.com/privkey.pem ~/myapp/
|
|
125
|
+
sudo chown youruser:youruser ~/myapp/*.pem
|
|
126
|
+
sudo chmod 600 ~/myapp/privkey.pem
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Then use the copied files:
|
|
130
|
+
```python
|
|
131
|
+
context.load_cert_chain(
|
|
132
|
+
certfile='/home/youruser/myapp/fullchain.pem',
|
|
133
|
+
keyfile='/home/youruser/myapp/privkey.pem'
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### Certificate Renewal
|
|
138
|
+
|
|
139
|
+
Let's Encrypt certificates expire every 90 days. After renewal with `certbot renew`, restart your server to load the new certificates, or implement a reload mechanism:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# Renew certificates
|
|
143
|
+
sudo certbot renew
|
|
144
|
+
|
|
145
|
+
# Restart your application
|
|
146
|
+
sudo systemctl restart your-app
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### HTTP to HTTPS Redirect
|
|
150
|
+
|
|
151
|
+
Run both HTTP and HTTPS servers to redirect HTTP traffic:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
import ssl
|
|
155
|
+
import select
|
|
156
|
+
import uhttp.server
|
|
157
|
+
|
|
158
|
+
# SSL context for HTTPS
|
|
159
|
+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
160
|
+
context.load_cert_chain(
|
|
161
|
+
certfile='/etc/letsencrypt/live/example.com/fullchain.pem',
|
|
162
|
+
keyfile='/etc/letsencrypt/live/example.com/privkey.pem'
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# HTTP server (redirects)
|
|
166
|
+
http_server = uhttp.server.HttpServer(port=80)
|
|
167
|
+
|
|
168
|
+
# HTTPS server (serves content)
|
|
169
|
+
https_server = uhttp.server.HttpServer(port=443, ssl_context=context)
|
|
170
|
+
|
|
171
|
+
while True:
|
|
172
|
+
r, w, _ = select.select(
|
|
173
|
+
http_server.read_sockets + https_server.read_sockets,
|
|
174
|
+
http_server.write_sockets + https_server.write_sockets,
|
|
175
|
+
[], 1.0
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Redirect HTTP to HTTPS
|
|
179
|
+
http_client = http_server.process_events(r, w)
|
|
180
|
+
if http_client:
|
|
181
|
+
https_url = f"https://{http_client.host}{http_client.url}"
|
|
182
|
+
http_client.respond_redirect(https_url)
|
|
183
|
+
|
|
184
|
+
# Serve HTTPS content
|
|
185
|
+
https_client = https_server.process_events(r, w)
|
|
186
|
+
if https_client:
|
|
187
|
+
https_client.respond({'message': 'Secure content'})
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Testing SSL Locally
|
|
191
|
+
|
|
192
|
+
For local development, create self-signed certificates:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=localhost"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Then use them:
|
|
199
|
+
```python
|
|
200
|
+
context.load_cert_chain(certfile='cert.pem', keyfile='key.pem')
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Test with curl (use `-k` to accept self-signed certificates):
|
|
204
|
+
```bash
|
|
205
|
+
curl -k https://localhost:8443/
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
See [examples/](../examples/) directory for complete working examples.
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
## API
|
|
212
|
+
|
|
213
|
+
### General methods:
|
|
214
|
+
|
|
215
|
+
**`import uhttp.server`**
|
|
216
|
+
|
|
217
|
+
**`uhttp.server.decode_percent_encoding(data)`**
|
|
218
|
+
|
|
219
|
+
- Decode percent encoded data (bytes)
|
|
220
|
+
|
|
221
|
+
**`uhttp.server.parse_header_parameters(value)`**
|
|
222
|
+
|
|
223
|
+
- Parse parameters/directives from header value, returns dict
|
|
224
|
+
|
|
225
|
+
**`uhttp.server.parse_query(raw_query, query=None)`**
|
|
226
|
+
|
|
227
|
+
- Parse raw_query from URL, append it to existing query, returns dict
|
|
228
|
+
|
|
229
|
+
**`uhttp.server.parse_url(url)`**
|
|
230
|
+
|
|
231
|
+
- Parse URL to path and query
|
|
232
|
+
|
|
233
|
+
**`uhttp.server.parse_header_line(line)`**
|
|
234
|
+
|
|
235
|
+
- Parse header line to key and value
|
|
236
|
+
|
|
237
|
+
**`uhttp.server.encode_response_data(headers, data)`**
|
|
238
|
+
|
|
239
|
+
- Encode response data by its type
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
### Class `HttpServer`:
|
|
243
|
+
|
|
244
|
+
**`HttpServer(address='0.0.0.0', port=80, ssl_context=None, event_mode=False, **kwargs)`**
|
|
245
|
+
|
|
246
|
+
Parameters:
|
|
247
|
+
- `address` - IP address to bind to (default: '0.0.0.0')
|
|
248
|
+
- `port` - Port to listen on (default: 80)
|
|
249
|
+
- `ssl_context` - Optional `ssl.SSLContext` for HTTPS connections (default: None)
|
|
250
|
+
- `event_mode` - Enable event mode for streaming uploads (default: False)
|
|
251
|
+
- `**kwargs` - Additional options:
|
|
252
|
+
- `max_waiting_clients` - Maximum concurrent connections (default: 5)
|
|
253
|
+
- `keep_alive_timeout` - Keep-alive timeout in seconds (default: 30)
|
|
254
|
+
- `keep_alive_max_requests` - Max requests per connection (default: 100)
|
|
255
|
+
- `max_headers_length` - Maximum header size in bytes (default: 4KB)
|
|
256
|
+
- `max_content_length` - Maximum body size in bytes (default: 512KB, only enforced when event_mode=False)
|
|
257
|
+
|
|
258
|
+
#### Properties:
|
|
259
|
+
|
|
260
|
+
**`socket(self)`**
|
|
261
|
+
|
|
262
|
+
- Server socket
|
|
263
|
+
|
|
264
|
+
**`read_sockets(self)`**
|
|
265
|
+
|
|
266
|
+
- All sockets waiting for read, used for select
|
|
267
|
+
|
|
268
|
+
**`write_sockets(self)`**
|
|
269
|
+
|
|
270
|
+
- All sockets with data to send, used for select
|
|
271
|
+
|
|
272
|
+
**`is_secure(self)`**
|
|
273
|
+
|
|
274
|
+
- Returns `True` if server uses SSL/TLS, `False` otherwise
|
|
275
|
+
|
|
276
|
+
**`event_mode(self)`**
|
|
277
|
+
|
|
278
|
+
- Returns `True` if event mode is enabled
|
|
279
|
+
|
|
280
|
+
#### Methods:
|
|
281
|
+
|
|
282
|
+
**`event_write(self, sockets)`**
|
|
283
|
+
|
|
284
|
+
- Send buffered data for sockets in list. Called internally by `process_events()`.
|
|
285
|
+
|
|
286
|
+
**`event_read(self, sockets)`**
|
|
287
|
+
|
|
288
|
+
- Process sockets with read event, returns None or instance of HttpConnection with established connection.
|
|
289
|
+
|
|
290
|
+
**`process_events(self, read_sockets, write_sockets)`**
|
|
291
|
+
|
|
292
|
+
- Process select results, returns None or instance of HttpConnection with established connection.
|
|
293
|
+
|
|
294
|
+
**`wait(self, timeout=1)`**
|
|
295
|
+
|
|
296
|
+
- Wait for new clients with specified timeout, returns None or instance of HttpConnection with established connection.
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
### Class `HttpConnection`:
|
|
300
|
+
|
|
301
|
+
**`HttpConnection(server, sock, addr, **kwargs)`**
|
|
302
|
+
|
|
303
|
+
#### Properties:
|
|
304
|
+
|
|
305
|
+
**`addr(self)`**
|
|
306
|
+
|
|
307
|
+
- Client address
|
|
308
|
+
|
|
309
|
+
**`method(self)`**
|
|
310
|
+
|
|
311
|
+
- HTTP method
|
|
312
|
+
|
|
313
|
+
**`url(self)`**
|
|
314
|
+
|
|
315
|
+
- URL address
|
|
316
|
+
|
|
317
|
+
**`host(self)`**
|
|
318
|
+
|
|
319
|
+
- URL address
|
|
320
|
+
|
|
321
|
+
**`full_url(self)`**
|
|
322
|
+
|
|
323
|
+
- URL address
|
|
324
|
+
|
|
325
|
+
**`protocol(self)`**
|
|
326
|
+
|
|
327
|
+
- Protocol
|
|
328
|
+
|
|
329
|
+
**`headers(self)`**
|
|
330
|
+
|
|
331
|
+
- headers dict
|
|
332
|
+
|
|
333
|
+
**`data(self)`**
|
|
334
|
+
|
|
335
|
+
- Content data
|
|
336
|
+
|
|
337
|
+
**`path(self)`**
|
|
338
|
+
|
|
339
|
+
- Path
|
|
340
|
+
|
|
341
|
+
**`query(self)`**
|
|
342
|
+
|
|
343
|
+
- Query dict
|
|
344
|
+
|
|
345
|
+
**`cookies(self)`**
|
|
346
|
+
|
|
347
|
+
- Cookies dict
|
|
348
|
+
|
|
349
|
+
**`is_secure(self)`**
|
|
350
|
+
|
|
351
|
+
- Returns `True` if connection is using SSL/TLS, `False` otherwise
|
|
352
|
+
|
|
353
|
+
**`socket(self)`**
|
|
354
|
+
|
|
355
|
+
- This socket
|
|
356
|
+
|
|
357
|
+
**`is_loaded(self)`**
|
|
358
|
+
|
|
359
|
+
- Returns `True` when request is fully loaded and ready for response
|
|
360
|
+
|
|
361
|
+
**`content_length(self)`**
|
|
362
|
+
|
|
363
|
+
- Content length
|
|
364
|
+
|
|
365
|
+
**`event(self)`** (event mode only)
|
|
366
|
+
|
|
367
|
+
- Current event type: `EVENT_REQUEST`, `EVENT_HEADERS`, `EVENT_DATA`, `EVENT_COMPLETE`, or `EVENT_ERROR`
|
|
368
|
+
|
|
369
|
+
**`bytes_received(self)`** (event mode only)
|
|
370
|
+
|
|
371
|
+
- Number of body bytes received so far
|
|
372
|
+
|
|
373
|
+
**`error(self)`** (event mode only)
|
|
374
|
+
|
|
375
|
+
- Error message when event is `EVENT_ERROR`
|
|
376
|
+
|
|
377
|
+
**`context`** (event mode only)
|
|
378
|
+
|
|
379
|
+
- Application storage attribute for request state (read-write)
|
|
380
|
+
|
|
381
|
+
#### Methods:
|
|
382
|
+
|
|
383
|
+
**`headers_get(self, key, default=None)`**
|
|
384
|
+
|
|
385
|
+
- Return value from headers by key, or default if key not found
|
|
386
|
+
|
|
387
|
+
**`process_request(self)`**
|
|
388
|
+
|
|
389
|
+
- Process HTTP request when read event on client socket
|
|
390
|
+
|
|
391
|
+
**`respond(self, data=None, status=200, headers=None, cookies=None)`**
|
|
392
|
+
|
|
393
|
+
- Create general response with data, status and headers as dict
|
|
394
|
+
|
|
395
|
+
**`respond_redirect(self, url, status=302, cookies=None)`**
|
|
396
|
+
|
|
397
|
+
- Create redirect response to URL
|
|
398
|
+
|
|
399
|
+
**`respond_file(self, file_name, headers=None)`**
|
|
400
|
+
|
|
401
|
+
- Respond with file content, streaming asynchronously to minimize memory usage
|
|
402
|
+
|
|
403
|
+
**`response_multipart(self, headers=None)`**
|
|
404
|
+
|
|
405
|
+
- Create multipart response with headers as dict (for MJPEG streams etc.)
|
|
406
|
+
|
|
407
|
+
**`response_multipart_frame(self, data, headers=None, boundary=None)`**
|
|
408
|
+
|
|
409
|
+
- Send multipart frame with data and headers
|
|
410
|
+
|
|
411
|
+
**`response_multipart_end(self, boundary=None)`**
|
|
412
|
+
|
|
413
|
+
- Finish multipart stream
|
|
414
|
+
|
|
415
|
+
**`accept_body(self, streaming=False, to_file=None)`** (event mode only)
|
|
416
|
+
|
|
417
|
+
- Accept body after `EVENT_HEADERS`. Call this to start receiving body data.
|
|
418
|
+
- `streaming=False` (default) - Buffer all data, receive only `EVENT_COMPLETE`
|
|
419
|
+
- `streaming=True` - Receive `EVENT_DATA` for each chunk, read with `read_buffer()`
|
|
420
|
+
- `to_file="/path"` - Save body directly to file
|
|
421
|
+
- Returns: Number of bytes already waiting in buffer
|
|
422
|
+
|
|
423
|
+
**`read_buffer(self)`** (event mode only)
|
|
424
|
+
|
|
425
|
+
- Read available data from buffer
|
|
426
|
+
- Returns: bytes or None if no data available
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
## Event Mode
|
|
430
|
+
|
|
431
|
+
Event mode enables streaming large uploads without buffering entire body in memory.
|
|
432
|
+
|
|
433
|
+
```python
|
|
434
|
+
from uhttp.server import (
|
|
435
|
+
HttpServer, EVENT_REQUEST, EVENT_HEADERS,
|
|
436
|
+
EVENT_DATA, EVENT_COMPLETE, EVENT_ERROR
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
server = HttpServer(port=8080, event_mode=True)
|
|
440
|
+
|
|
441
|
+
while True:
|
|
442
|
+
client = server.wait()
|
|
443
|
+
if not client:
|
|
444
|
+
continue
|
|
445
|
+
|
|
446
|
+
if client.event == EVENT_REQUEST:
|
|
447
|
+
# Small request or GET - handle normally
|
|
448
|
+
client.respond({'status': 'ok'})
|
|
449
|
+
|
|
450
|
+
elif client.event == EVENT_HEADERS:
|
|
451
|
+
# Large upload starting - decide how to handle
|
|
452
|
+
client.context = {'total': 0}
|
|
453
|
+
client.accept_body()
|
|
454
|
+
# Read any data that arrived with headers
|
|
455
|
+
data = client.read_buffer()
|
|
456
|
+
if data:
|
|
457
|
+
client.context['total'] += len(data)
|
|
458
|
+
|
|
459
|
+
elif client.event == EVENT_DATA:
|
|
460
|
+
# More data arrived
|
|
461
|
+
data = client.read_buffer()
|
|
462
|
+
if data:
|
|
463
|
+
client.context['total'] += len(data)
|
|
464
|
+
|
|
465
|
+
elif client.event == EVENT_COMPLETE:
|
|
466
|
+
# Upload finished
|
|
467
|
+
data = client.read_buffer()
|
|
468
|
+
if data:
|
|
469
|
+
client.context['total'] += len(data)
|
|
470
|
+
client.respond({'received': client.context['total']})
|
|
471
|
+
|
|
472
|
+
elif client.event == EVENT_ERROR:
|
|
473
|
+
print(f"Error: {client.error}")
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
For file uploads, use `to_file` parameter:
|
|
477
|
+
|
|
478
|
+
```python
|
|
479
|
+
elif client.event == EVENT_HEADERS:
|
|
480
|
+
client.accept_body(to_file=f"/uploads/{uuid}.bin")
|
|
481
|
+
|
|
482
|
+
elif client.event == EVENT_COMPLETE:
|
|
483
|
+
client.respond({'status': 'uploaded'})
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
**Note:** Small POST requests where headers and body arrive in the same TCP packet will receive `EVENT_REQUEST` (not `EVENT_HEADERS`), since the complete request is already available.
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
## IPv6 Support
|
|
490
|
+
|
|
491
|
+
Server supports both IPv4 and IPv6:
|
|
492
|
+
|
|
493
|
+
```python
|
|
494
|
+
import uhttp.server
|
|
495
|
+
|
|
496
|
+
# IPv4 only (default)
|
|
497
|
+
server = uhttp.server.HttpServer(address='0.0.0.0', port=80)
|
|
498
|
+
|
|
499
|
+
# Dual-stack (IPv4 + IPv6)
|
|
500
|
+
server = uhttp.server.HttpServer(address='::', port=80)
|
|
501
|
+
|
|
502
|
+
# IPv6 only
|
|
503
|
+
server = uhttp.server.HttpServer(address='::1', port=80)
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
## Development
|
|
508
|
+
|
|
509
|
+
### Running tests
|
|
510
|
+
|
|
511
|
+
```bash
|
|
512
|
+
../.venv/bin/pip install -e .
|
|
513
|
+
../.venv/bin/python -m unittest discover -v tests/
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
For running tests from meta-repo, see [uhttp README](https://github.com/pavelrevak/uhttp#testing).
|
|
517
|
+
|
|
518
|
+
### CI
|
|
519
|
+
|
|
520
|
+
Tests run automatically on push/PR via GitHub Actions (Ubuntu + Windows, Python 3.10 + 3.14).
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
## TODO
|
|
524
|
+
|
|
525
|
+
- Cookie attributes support (Path, Domain, Secure, HttpOnly, SameSite, Expires)
|
|
526
|
+
- Expect: 100-continue support - currently causes deadlock (client waits for 100, server waits for body)
|
|
527
|
+
- Streaming API for sending large responses (handle EAGAIN)
|
|
528
|
+
- Chunked transfer encoding support (receiving and sending)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "uhttp-server"
|
|
7
|
+
version = "2.2.0"
|
|
8
|
+
description = "Micro HTTP server for Python and MicroPython"
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Pavel Revak", email = "pavelrevak@gmail.com"}
|
|
11
|
+
]
|
|
12
|
+
license = {text = "MIT"}
|
|
13
|
+
requires-python = ">=3.7"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: Implementation :: MicroPython",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/pavelrevak/uhttp"
|
|
23
|
+
Repository = "https://github.com/pavelrevak/uhttp"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
include = ["uhttp*"]
|