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.
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/PKG-INFO +1 -1
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_mpy_websocket.py +1 -1
- uhttp_server-2.3.2/tests/test_timeout_during_response.py +188 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_websocket.py +36 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_websocket_utils.py +1 -1
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/uhttp/server.py +4 -1
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/uhttp_server.egg-info/PKG-INFO +1 -1
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/uhttp_server.egg-info/SOURCES.txt +1 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/.github/workflows/micropython.yml +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/.github/workflows/publish.yml +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/.github/workflows/tests.yml +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/.gitignore +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/LICENSE +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/README.md +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/examples/client_with_server.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/examples/http_to_https_redirect.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/examples/https_server.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/pyproject.toml +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/setup.cfg +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/__init__.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_100_continue.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_concurrent_connections.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_content_length_security.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_data_parsing.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_double_response.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_eagain.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_error_handling.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_event_mode.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_ipv6.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_keepalive.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_keepalive_basic.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_keepalive_http10.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_keepalive_limits.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_keepalive_simple.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_mpy_integration.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_multipart.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_pipelining.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_respond_falsy.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_respond_file.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_respond_file_race.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_ssl.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/tests/test_utils.py +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/uhttp_server.egg-info/dependency_links.txt +0 -0
- {uhttp_server-2.3.1 → uhttp_server-2.3.2}/uhttp_server.egg-info/top_level.txt +0 -0
|
@@ -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-
|
|
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 = "
|
|
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-
|
|
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,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|