uhttp-server 2.3.1__tar.gz → 2.3.2__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 (44) hide show
  1. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/PKG-INFO +1 -1
  2. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_mpy_websocket.py +1 -1
  3. uhttp_server-2.3.2/tests/test_timeout_during_response.py +188 -0
  4. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_websocket.py +36 -0
  5. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_websocket_utils.py +1 -1
  6. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/uhttp/server.py +4 -1
  7. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/uhttp_server.egg-info/PKG-INFO +1 -1
  8. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/uhttp_server.egg-info/SOURCES.txt +1 -0
  9. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/.github/workflows/micropython.yml +0 -0
  10. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/.github/workflows/publish.yml +0 -0
  11. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/.github/workflows/tests.yml +0 -0
  12. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/.gitignore +0 -0
  13. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/LICENSE +0 -0
  14. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/README.md +0 -0
  15. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/examples/client_with_server.py +0 -0
  16. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/examples/http_to_https_redirect.py +0 -0
  17. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/examples/https_server.py +0 -0
  18. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/pyproject.toml +0 -0
  19. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/setup.cfg +0 -0
  20. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/__init__.py +0 -0
  21. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_100_continue.py +0 -0
  22. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_concurrent_connections.py +0 -0
  23. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_content_length_security.py +0 -0
  24. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_data_parsing.py +0 -0
  25. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_double_response.py +0 -0
  26. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_eagain.py +0 -0
  27. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_error_handling.py +0 -0
  28. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_event_mode.py +0 -0
  29. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_ipv6.py +0 -0
  30. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_keepalive.py +0 -0
  31. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_keepalive_basic.py +0 -0
  32. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_keepalive_http10.py +0 -0
  33. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_keepalive_limits.py +0 -0
  34. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_keepalive_simple.py +0 -0
  35. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_mpy_integration.py +0 -0
  36. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_multipart.py +0 -0
  37. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_pipelining.py +0 -0
  38. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_respond_falsy.py +0 -0
  39. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_respond_file.py +0 -0
  40. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_respond_file_race.py +0 -0
  41. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_ssl.py +0 -0
  42. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_utils.py +0 -0
  43. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/uhttp_server.egg-info/dependency_links.txt +0 -0
  44. {uhttp_server-2.3.1 → uhttp_server-2.3.2}/uhttp_server.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: uhttp-server
3
- Version: 2.3.1
3
+ Version: 2.3.2
4
4
  Summary: Micro HTTP server for Python and MicroPython
5
5
  Author-email: Pavel Revak <pavelrevak@gmail.com>
6
6
  License: MIT
@@ -67,7 +67,7 @@ WIFI_PASSWORD = _config['password']
67
67
 
68
68
  ESP32_SERVER_PORT = 8081 # Different from test_mpy_integration
69
69
 
70
- _WS_MAGIC = b'258EAFA5-E914-47DA-95CA-5AB9141B3175'
70
+ _WS_MAGIC = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
71
71
 
72
72
  WS_OPCODE_TEXT = 0x1
73
73
  WS_OPCODE_BINARY = 0x2
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test that connections with in-progress response are not timed out with 408.
4
+
5
+ When a connection is sending a large response (e.g., file streaming),
6
+ the keep-alive timeout should not interrupt it with a 408 response.
7
+ """
8
+ import unittest
9
+ import socket
10
+ import time
11
+ import select
12
+ import tempfile
13
+ import os
14
+ from uhttp import server as uhttp_server
15
+
16
+
17
+ class TestTimeoutDuringResponse(unittest.TestCase):
18
+ """Test that _cleanup_idle_connections skips connections with _response_started"""
19
+
20
+ PORT = 9998
21
+
22
+ def test_no_408_while_response_in_progress(self):
23
+ """Connection streaming file response should not get 408 timeout"""
24
+ temp_dir = tempfile.mkdtemp()
25
+ large_file = os.path.join(temp_dir, 'large.bin')
26
+ with open(large_file, 'wb') as f:
27
+ f.write(b'X' * 200_000)
28
+
29
+ server = uhttp_server.HttpServer(
30
+ port=self.PORT, keep_alive_timeout=0.3, file_chunk_size=512)
31
+ trigger_sock = None
32
+
33
+ try:
34
+ client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
35
+ client_sock.connect(('localhost', self.PORT))
36
+ client_sock.setblocking(False)
37
+ client_sock.send(
38
+ b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n")
39
+ time.sleep(0.1)
40
+
41
+ connection = None
42
+ for _ in range(10):
43
+ r, _, _ = select.select(server.read_sockets, [], [], 0.1)
44
+ if r:
45
+ connection = server.event_read(r)
46
+ if connection:
47
+ break
48
+ self.assertIsNotNone(connection)
49
+
50
+ connection.respond_file(large_file)
51
+ self.assertTrue(connection._response_started)
52
+
53
+ # Wait longer than keep_alive_timeout
54
+ time.sleep(0.5)
55
+
56
+ # Trigger cleanup via new connection
57
+ trigger_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
58
+ trigger_sock.connect(('localhost', self.PORT))
59
+ time.sleep(0.1)
60
+
61
+ r, _, _ = select.select(server.read_sockets, [], [], 0.2)
62
+ if r:
63
+ server.event_read(r)
64
+
65
+ self.assertIsNotNone(
66
+ connection.socket,
67
+ "Connection should not be closed while response is in progress")
68
+
69
+ # Drain the response
70
+ response = b""
71
+ for _ in range(200):
72
+ w = server.write_sockets
73
+ if w:
74
+ _, ww, _ = select.select([], w, [], 0.1)
75
+ if ww:
76
+ server.event_write(ww)
77
+ try:
78
+ chunk = client_sock.recv(65536)
79
+ if chunk:
80
+ response += chunk
81
+ else:
82
+ break
83
+ except BlockingIOError:
84
+ continue
85
+
86
+ self.assertIn(b'200 OK', response)
87
+ self.assertIn(b'X' * 100, response)
88
+
89
+ finally:
90
+ client_sock.close()
91
+ if trigger_sock:
92
+ trigger_sock.close()
93
+ # Close server first so file handles are released (Windows)
94
+ server.close()
95
+ time.sleep(0.1)
96
+ try:
97
+ os.unlink(large_file)
98
+ os.rmdir(temp_dir)
99
+ except OSError:
100
+ pass
101
+
102
+ def test_idle_keepalive_still_gets_408(self):
103
+ """Idle keep-alive connection (no response started) should still get 408"""
104
+ server = uhttp_server.HttpServer(
105
+ port=self.PORT + 1, keep_alive_timeout=0.3)
106
+ trigger_sock = None
107
+
108
+ try:
109
+ client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
110
+ client_sock.connect(('localhost', self.PORT + 1))
111
+ client_sock.setblocking(False)
112
+
113
+ client_sock.send(
114
+ b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n")
115
+ time.sleep(0.1)
116
+
117
+ # Process the request and respond
118
+ for _ in range(10):
119
+ r, _, _ = select.select(server.read_sockets, [], [], 0.1)
120
+ if r:
121
+ connection = server.event_read(r)
122
+ if connection:
123
+ connection.respond('ok')
124
+ break
125
+
126
+ # Flush send buffer
127
+ for _ in range(20):
128
+ w = server.write_sockets
129
+ if w:
130
+ _, ww, _ = select.select([], w, [], 0.1)
131
+ if ww:
132
+ server.event_write(ww)
133
+ else:
134
+ break
135
+
136
+ # Drain the first response
137
+ try:
138
+ while True:
139
+ client_sock.recv(4096)
140
+ except BlockingIOError:
141
+ pass
142
+
143
+ # Wait for keep-alive timeout
144
+ time.sleep(0.5)
145
+
146
+ # Trigger cleanup
147
+ trigger_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
148
+ trigger_sock.connect(('localhost', self.PORT + 1))
149
+ time.sleep(0.1)
150
+
151
+ r, _, _ = select.select(server.read_sockets, [], [], 0.2)
152
+ if r:
153
+ server.event_read(r)
154
+
155
+ # Flush 408 response
156
+ for _ in range(20):
157
+ w = server.write_sockets
158
+ if w:
159
+ _, ww, _ = select.select([], w, [], 0.1)
160
+ if ww:
161
+ server.event_write(ww)
162
+ else:
163
+ break
164
+
165
+ # Read the 408 response
166
+ time.sleep(0.1)
167
+ response = b""
168
+ try:
169
+ for _ in range(10):
170
+ chunk = client_sock.recv(4096)
171
+ if chunk:
172
+ response += chunk
173
+ else:
174
+ break
175
+ except (BlockingIOError, ConnectionResetError, BrokenPipeError):
176
+ pass
177
+
178
+ self.assertIn(b'408', response)
179
+
180
+ finally:
181
+ client_sock.close()
182
+ if trigger_sock:
183
+ trigger_sock.close()
184
+ server.close()
185
+
186
+
187
+ if __name__ == '__main__':
188
+ unittest.main()
@@ -471,6 +471,42 @@ class TestWebSocketEventMode(unittest.TestCase):
471
471
  finally:
472
472
  sock.close()
473
473
 
474
+ def test_http_keepalive_then_ws_upgrade(self):
475
+ """Test HTTP keep-alive requests followed by WebSocket upgrade on same connection"""
476
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
477
+ try:
478
+ sock.connect(('localhost', self.PORT))
479
+ sock.settimeout(3)
480
+
481
+ # Send HTTP request on keep-alive connection
482
+ sock.sendall(
483
+ b"GET /test HTTP/1.1\r\n"
484
+ b"Host: localhost\r\n\r\n")
485
+ response = b''
486
+ while b'\r\n\r\n' not in response:
487
+ response += sock.recv(1024)
488
+ self.assertIn(b'200 OK', response)
489
+
490
+ # Send second HTTP request on same connection
491
+ sock.sendall(
492
+ b"GET /test2 HTTP/1.1\r\n"
493
+ b"Host: localhost\r\n\r\n")
494
+ response = b''
495
+ while b'\r\n\r\n' not in response:
496
+ response += sock.recv(1024)
497
+ self.assertIn(b'200 OK', response)
498
+
499
+ # Now upgrade to WebSocket on the same connection
500
+ ws_upgrade(sock)
501
+
502
+ # WebSocket should work
503
+ sock.sendall(build_masked_frame(WS_OPCODE_TEXT, 'after keepalive'))
504
+ fin, opcode, payload = recv_frame(sock)
505
+ self.assertEqual(opcode, WS_OPCODE_TEXT)
506
+ self.assertEqual(payload.decode(), 'after keepalive')
507
+ finally:
508
+ sock.close()
509
+
474
510
  def test_empty_ping(self):
475
511
  """Test ping with empty payload"""
476
512
  sock = self._connect_ws()
@@ -16,7 +16,7 @@ class TestWsAcceptKey(unittest.TestCase):
16
16
  def test_rfc_example(self):
17
17
  """Test with RFC 6455 example key"""
18
18
  key = "dGhlIHNhbXBsZSBub25jZQ=="
19
- expected = "MWp/lNvnLAWD1TD0HICsGGCP88w="
19
+ expected = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
20
20
  self.assertEqual(_ws_accept_key(key), expected)
21
21
 
22
22
  def test_known_key(self):
@@ -103,7 +103,7 @@ WS_OPCODE_CLOSE = 0x8
103
103
  WS_OPCODE_PING = 0x9
104
104
  WS_OPCODE_PONG = 0xA
105
105
 
106
- _WS_MAGIC = b'258EAFA5-E914-47DA-95CA-5AB9141B3175'
106
+ _WS_MAGIC = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
107
107
 
108
108
  STATUS_CODES = {
109
109
  100: "Continue",
@@ -1602,6 +1602,7 @@ class HttpConnection(_WsFrameMixin):
1602
1602
  WS_OPCODE_TEXT, data.encode('utf-8')))
1603
1603
  else:
1604
1604
  self._send(_ws_build_frame(WS_OPCODE_BINARY, data))
1605
+ self.update_activity()
1605
1606
 
1606
1607
  def ws_ping(self, data=b''):
1607
1608
  """Send WebSocket ping (event mode)"""
@@ -1741,6 +1742,8 @@ class HttpServer():
1741
1742
  if connection.is_timed_out:
1742
1743
  connection.close()
1743
1744
  continue
1745
+ if connection._response_started:
1746
+ continue
1744
1747
  if not connection.is_loaded and connection.is_timed_out:
1745
1748
  connection.respond(
1746
1749
  'Request Timeout', status=408,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: uhttp-server
3
- Version: 2.3.1
3
+ Version: 2.3.2
4
4
  Summary: Micro HTTP server for Python and MicroPython
5
5
  Author-email: Pavel Revak <pavelrevak@gmail.com>
6
6
  License: MIT
@@ -31,6 +31,7 @@ tests/test_respond_falsy.py
31
31
  tests/test_respond_file.py
32
32
  tests/test_respond_file_race.py
33
33
  tests/test_ssl.py
34
+ tests/test_timeout_during_response.py
34
35
  tests/test_utils.py
35
36
  tests/test_websocket.py
36
37
  tests/test_websocket_utils.py
File without changes
File without changes
File without changes
File without changes