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.
Files changed (30) hide show
  1. uhttp_server-2.2.0/LICENSE +21 -0
  2. uhttp_server-2.2.0/PKG-INFO +15 -0
  3. uhttp_server-2.2.0/README.md +528 -0
  4. uhttp_server-2.2.0/pyproject.toml +26 -0
  5. uhttp_server-2.2.0/setup.cfg +4 -0
  6. uhttp_server-2.2.0/tests/test_100_continue.py +349 -0
  7. uhttp_server-2.2.0/tests/test_concurrent_connections.py +236 -0
  8. uhttp_server-2.2.0/tests/test_content_length_security.py +243 -0
  9. uhttp_server-2.2.0/tests/test_data_parsing.py +472 -0
  10. uhttp_server-2.2.0/tests/test_double_response.py +192 -0
  11. uhttp_server-2.2.0/tests/test_eagain.py +267 -0
  12. uhttp_server-2.2.0/tests/test_error_handling.py +210 -0
  13. uhttp_server-2.2.0/tests/test_event_mode.py +615 -0
  14. uhttp_server-2.2.0/tests/test_ipv6.py +175 -0
  15. uhttp_server-2.2.0/tests/test_keepalive.py +346 -0
  16. uhttp_server-2.2.0/tests/test_keepalive_basic.py +257 -0
  17. uhttp_server-2.2.0/tests/test_keepalive_http10.py +271 -0
  18. uhttp_server-2.2.0/tests/test_keepalive_limits.py +325 -0
  19. uhttp_server-2.2.0/tests/test_keepalive_simple.py +253 -0
  20. uhttp_server-2.2.0/tests/test_multipart.py +235 -0
  21. uhttp_server-2.2.0/tests/test_pipelining.py +138 -0
  22. uhttp_server-2.2.0/tests/test_respond_file.py +324 -0
  23. uhttp_server-2.2.0/tests/test_respond_file_race.py +164 -0
  24. uhttp_server-2.2.0/tests/test_ssl.py +372 -0
  25. uhttp_server-2.2.0/tests/test_utils.py +158 -0
  26. uhttp_server-2.2.0/uhttp/server.py +1334 -0
  27. uhttp_server-2.2.0/uhttp_server.egg-info/PKG-INFO +15 -0
  28. uhttp_server-2.2.0/uhttp_server.egg-info/SOURCES.txt +28 -0
  29. uhttp_server-2.2.0/uhttp_server.egg-info/dependency_links.txt +1 -0
  30. 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*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+