wsbuilder 1.0.2__tar.gz → 1.0.3__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 (130) hide show
  1. {wsbuilder-1.0.2/src/wsbuilder.egg-info → wsbuilder-1.0.3}/PKG-INFO +1 -1
  2. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/pyproject.toml +1 -1
  3. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/__init__.py +1 -1
  4. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/app.py +1 -1
  5. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/cache.py +2 -2
  6. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/caches.py +2 -2
  7. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/http.py +1 -1
  8. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/metrics.py +2 -2
  9. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/proxyi.py +2 -2
  10. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/security.py +4 -4
  11. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/server.py +6 -6
  12. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/ws.py +1 -1
  13. {wsbuilder-1.0.2 → wsbuilder-1.0.3/src/wsbuilder.egg-info}/PKG-INFO +1 -1
  14. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_http.py +17 -0
  15. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_security.py +18 -0
  16. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_ws.py +7 -0
  17. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/LICENSE +0 -0
  18. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/MANIFEST.in +0 -0
  19. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/README.md +0 -0
  20. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/404.html +0 -0
  21. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/CNAME +0 -0
  22. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/advanced.md +0 -0
  23. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/ai-predict.md +0 -0
  24. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/api-map.md +0 -0
  25. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/app-http.md +0 -0
  26. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/architecture.md +0 -0
  27. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/assets/wsbuilder-mark.svg +0 -0
  28. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/beginners.md +0 -0
  29. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/cache-security.md +0 -0
  30. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/cache.md +0 -0
  31. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/capabilities.md +0 -0
  32. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/data-orm.md +0 -0
  33. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/dns.md +0 -0
  34. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/full-project.md +0 -0
  35. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/getting-started.md +0 -0
  36. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/help/index.md +0 -0
  37. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/http-versions.md +0 -0
  38. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/http.md +0 -0
  39. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/ia.md +0 -0
  40. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/index.md +0 -0
  41. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/install.md +0 -0
  42. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/intermediate.md +0 -0
  43. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/mdns.md +0 -0
  44. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/network-edge.md +0 -0
  45. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/observability-tasks.md +0 -0
  46. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/observability.md +0 -0
  47. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/persistence.md +0 -0
  48. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/proxy.md +0 -0
  49. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/reference/index.md +0 -0
  50. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/security.md +0 -0
  51. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/stylesheets/extra.css +0 -0
  52. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/tasks.md +0 -0
  53. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/tls-certificates.md +0 -0
  54. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/docs/websocket.md +0 -0
  55. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/examples/full-project/.gitignore +0 -0
  56. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/examples/full-project/README.md +0 -0
  57. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/examples/full-project/app.py +0 -0
  58. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/examples/full-project/pyproject.toml +0 -0
  59. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/examples/full-project/upstream.py +0 -0
  60. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/mkdocs.yml +0 -0
  61. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/setup.cfg +0 -0
  62. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/__main__.py +0 -0
  63. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/constants.py +0 -0
  64. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/cookies.py +0 -0
  65. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/db_replicas.py +0 -0
  66. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/dns.py +0 -0
  67. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/framework.py +0 -0
  68. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/headers.py +0 -0
  69. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/hpack.py +0 -0
  70. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/http1.py +0 -0
  71. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/http2.py +0 -0
  72. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/http3.py +0 -0
  73. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/http3_server.py +0 -0
  74. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/http3_websocket.py +0 -0
  75. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/ia.py +0 -0
  76. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/logs.py +0 -0
  77. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/mdns.py +0 -0
  78. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/orm.py +0 -0
  79. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/pki.py +0 -0
  80. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/predicts.py +0 -0
  81. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/qpack.py +0 -0
  82. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/quic/__init__.py +0 -0
  83. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/quic/address.py +0 -0
  84. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/quic/connection.py +0 -0
  85. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/quic/crypto.py +0 -0
  86. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/quic/frames.py +0 -0
  87. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/quic/packet.py +0 -0
  88. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/quic/recovery.py +0 -0
  89. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/quic/tls.py +0 -0
  90. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/quic/varint.py +0 -0
  91. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/tasks.py +0 -0
  92. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder/ws_demo.py +0 -0
  93. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder.egg-info/SOURCES.txt +0 -0
  94. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder.egg-info/dependency_links.txt +0 -0
  95. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder.egg-info/entry_points.txt +0 -0
  96. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder.egg-info/requires.txt +0 -0
  97. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/src/wsbuilder.egg-info/top_level.txt +0 -0
  98. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_app_threads.py +0 -0
  99. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_cache.py +0 -0
  100. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_caches.py +0 -0
  101. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_contributing_policy.py +0 -0
  102. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_demo_monitor.py +0 -0
  103. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_dns.py +0 -0
  104. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_docs.py +0 -0
  105. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_documentation_files.py +0 -0
  106. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_headers_cookies.py +0 -0
  107. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_hpack.py +0 -0
  108. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_http1.py +0 -0
  109. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_http2.py +0 -0
  110. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_http3.py +0 -0
  111. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_http3_server.py +0 -0
  112. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_http3_streaming.py +0 -0
  113. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_http3_streaming_udp.py +0 -0
  114. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_ia.py +0 -0
  115. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_logs.py +0 -0
  116. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_mdns.py +0 -0
  117. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_metrics.py +0 -0
  118. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_orm.py +0 -0
  119. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_orm_replicas.py +0 -0
  120. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_pki.py +0 -0
  121. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_pr_identity_policy.py +0 -0
  122. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_predicts.py +0 -0
  123. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_proxyi.py +0 -0
  124. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_quic_address.py +0 -0
  125. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_quic_crypto.py +0 -0
  126. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_quic_protocol.py +0 -0
  127. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_quic_recovery.py +0 -0
  128. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_quic_tls.py +0 -0
  129. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_server.py +0 -0
  130. {wsbuilder-1.0.2 → wsbuilder-1.0.3}/tests/test_tasks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wsbuilder
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: Lightweight HTTP + WebSocket framework for Python.
5
5
  Author: wsbuilder Authors
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wsbuilder"
7
- version = "1.0.2"
7
+ version = "1.0.3"
8
8
  description = "Lightweight HTTP + WebSocket framework for Python."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -126,7 +126,7 @@ def __getattr__(name):
126
126
  def __dir__():
127
127
  return sorted(set(globals()) | _PKI_EXPORTS)
128
128
 
129
- __version__ = "1.0.2"
129
+ __version__ = "1.0.3"
130
130
 
131
131
  __all__ = [
132
132
  "App",
@@ -455,7 +455,7 @@ class App:
455
455
  thread_id = parts[0]
456
456
  try:
457
457
  normalized = str(uuid.UUID(thread_id))
458
- except Exception:
458
+ except ValueError:
459
459
  return ""
460
460
 
461
461
  issued_text, sig = parts[1], parts[2]
@@ -16,14 +16,14 @@ DEFAULT_CLEANUP_INTERVAL_SECONDS = 30.0
16
16
  def _safe_float(value, default):
17
17
  try:
18
18
  return float(value)
19
- except Exception:
19
+ except (TypeError, ValueError, OverflowError):
20
20
  return default
21
21
 
22
22
 
23
23
  def _safe_int(value, default):
24
24
  try:
25
25
  return int(value)
26
- except Exception:
26
+ except (TypeError, ValueError, OverflowError):
27
27
  return default
28
28
 
29
29
 
@@ -39,14 +39,14 @@ _DROP_HEADERS = {
39
39
  def _safe_float(value, default):
40
40
  try:
41
41
  return float(value)
42
- except Exception:
42
+ except (TypeError, ValueError, OverflowError):
43
43
  return default
44
44
 
45
45
 
46
46
  def _safe_int(value, default):
47
47
  try:
48
48
  return int(value)
49
- except Exception:
49
+ except (TypeError, ValueError, OverflowError):
50
50
  return default
51
51
 
52
52
 
@@ -59,7 +59,7 @@ class Request:
59
59
  def json(self):
60
60
  try:
61
61
  return json.loads(self.text())
62
- except Exception:
62
+ except (ValueError, RecursionError):
63
63
  return None
64
64
 
65
65
 
@@ -19,14 +19,14 @@ def _iso_utc(ts):
19
19
  def _safe_int(value, default):
20
20
  try:
21
21
  return int(value)
22
- except Exception:
22
+ except (TypeError, ValueError, OverflowError):
23
23
  return default
24
24
 
25
25
 
26
26
  def _safe_float(value, default):
27
27
  try:
28
28
  return float(value)
29
- except Exception:
29
+ except (TypeError, ValueError, OverflowError):
30
30
  return default
31
31
 
32
32
 
@@ -99,14 +99,14 @@ _REDACTED_HEADER_VALUE = "[REDACTED]"
99
99
  def _safe_int(value, default):
100
100
  try:
101
101
  return int(value)
102
- except Exception:
102
+ except (TypeError, ValueError, OverflowError):
103
103
  return default
104
104
 
105
105
 
106
106
  def _safe_float(value, default):
107
107
  try:
108
108
  return float(value)
109
- except Exception:
109
+ except (TypeError, ValueError, OverflowError):
110
110
  return default
111
111
 
112
112
 
@@ -29,14 +29,14 @@ DEFAULT_MAX_TEMPORARY_BLOCKS = 20000
29
29
  def _safe_int(value, default):
30
30
  try:
31
31
  return int(value)
32
- except Exception:
32
+ except (TypeError, ValueError, OverflowError):
33
33
  return default
34
34
 
35
35
 
36
36
  def _safe_float(value, default):
37
37
  try:
38
38
  return float(value)
39
- except Exception:
39
+ except (TypeError, ValueError, OverflowError):
40
40
  return default
41
41
 
42
42
 
@@ -46,7 +46,7 @@ def _to_ip(value):
46
46
  return None
47
47
  try:
48
48
  return ipaddress.ip_address(text)
49
- except Exception:
49
+ except ValueError:
50
50
  return None
51
51
 
52
52
 
@@ -56,7 +56,7 @@ def _to_network(value):
56
56
  return None
57
57
  try:
58
58
  return ipaddress.ip_network(text, strict=False)
59
- except Exception:
59
+ except ValueError:
60
60
  ip_obj = _to_ip(text)
61
61
  if ip_obj is None:
62
62
  return None
@@ -105,7 +105,7 @@ class HTTPServer:
105
105
  if not acquired:
106
106
  try:
107
107
  conn.close()
108
- except Exception:
108
+ except OSError:
109
109
  pass
110
110
  continue
111
111
  t = threading.Thread(
@@ -172,7 +172,7 @@ class HTTPServer:
172
172
  metrics.error("tls_context", e)
173
173
  try:
174
174
  conn.close()
175
- except Exception:
175
+ except OSError:
176
176
  pass
177
177
  return
178
178
  tls_meta = {
@@ -192,14 +192,14 @@ class HTTPServer:
192
192
  print(f"[tls] handshake error from {addr}: {e}")
193
193
  try:
194
194
  conn.close()
195
- except Exception:
195
+ except OSError:
196
196
  pass
197
197
  return
198
198
  except Exception as e:
199
199
  print(f"[tls] wrap error from {addr}: {e}")
200
200
  try:
201
201
  conn.close()
202
- except Exception:
202
+ except OSError:
203
203
  pass
204
204
  return
205
205
  with conn:
@@ -214,7 +214,7 @@ class HTTPServer:
214
214
  # first read, so a reused connection waits less.
215
215
  try:
216
216
  conn.settimeout(self.KEEPALIVE_TIMEOUT_SECONDS)
217
- except Exception:
217
+ except OSError:
218
218
  pass
219
219
  try:
220
220
  keep_alive = self._serve_one_request(reader, conn, addr, tls_meta)
@@ -274,7 +274,7 @@ class HTTPServer:
274
274
  security = getattr(self.app, "security", None)
275
275
  try:
276
276
  conn.settimeout(self.REQUEST_READ_TIMEOUT_SECONDS)
277
- except Exception:
277
+ except OSError:
278
278
  pass
279
279
  try:
280
280
  req = parse_http_request(
@@ -69,7 +69,7 @@ def _is_valid_websocket_key(value):
69
69
  return False
70
70
  try:
71
71
  decoded = base64.b64decode(raw.encode("ascii"), validate=True)
72
- except Exception:
72
+ except ValueError:
73
73
  return False
74
74
  return len(decoded) == 16
75
75
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wsbuilder
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: Lightweight HTTP + WebSocket framework for Python.
5
5
  Author: wsbuilder Authors
6
6
  License-Expression: MIT
@@ -21,6 +21,23 @@ class BufferSocket:
21
21
 
22
22
 
23
23
  class TestHTTPCore(unittest.TestCase):
24
+ def test_request_json_returns_none_for_unparseable_bodies(self):
25
+ def request(body):
26
+ return Request(
27
+ method="POST",
28
+ path="/",
29
+ query_string="",
30
+ headers={},
31
+ body=body,
32
+ client=("127.0.0.1", 1234),
33
+ tls={},
34
+ )
35
+
36
+ self.assertEqual(request(b'{"ok": true}').json(), {"ok": True})
37
+ for body in (b"", b"{not json", b"[" * 100_000):
38
+ with self.subTest(body=body[:16]):
39
+ self.assertIsNone(request(body).json())
40
+
24
41
  def test_query_string_decodes_standard_url_encoding(self):
25
42
  parsed = parse_query_string(
26
43
  "name=Ana+Mar%C3%ADa&empty=&flag&name=final"
@@ -2,6 +2,7 @@ import json
2
2
  import unittest
3
3
 
4
4
  from wsbuilder import App, Request, SecurityPolicy
5
+ from wsbuilder.security import _safe_float, _safe_int, _to_ip, _to_network
5
6
 
6
7
 
7
8
  def _req(method, path, query_string="", client=("127.0.0.1", 1234), headers=None, tls=None):
@@ -152,6 +153,23 @@ if __name__ == "__main__":
152
153
  unittest.main()
153
154
 
154
155
 
156
+ class TestSecurityParsing(unittest.TestCase):
157
+ def test_numeric_helpers_fall_back_on_unconvertible_values(self):
158
+ for value in (None, "", "abc", float("inf"), float("nan"), object()):
159
+ with self.subTest(value=value):
160
+ self.assertEqual(_safe_int(value, 7), 7)
161
+ self.assertEqual(_safe_int("12", 7), 12)
162
+ self.assertEqual(_safe_float(None, 1.5), 1.5)
163
+ self.assertEqual(_safe_float(10**400, 1.5), 1.5)
164
+ self.assertEqual(_safe_float("2.5", 1.5), 2.5)
165
+
166
+ def test_address_helpers_return_none_for_invalid_text(self):
167
+ self.assertIsNone(_to_ip("999.1.1.1"))
168
+ self.assertIsNone(_to_network("not-a-network"))
169
+ self.assertEqual(str(_to_network("10.0.0.0/8")), "10.0.0.0/8")
170
+ self.assertEqual(str(_to_ip("::1")), "::1")
171
+
172
+
155
173
  class TestSecurityTrackingLimits(unittest.TestCase):
156
174
  def test_client_tracking_maps_stop_growing_at_the_configured_ceiling(self):
157
175
  policy = SecurityPolicy(max_tracked_clients=16)
@@ -8,6 +8,7 @@ from wsbuilder.ws import (
8
8
  WebSocketFrame,
9
9
  WebSocketProtocolError,
10
10
  WebSocketReadTimeoutError,
11
+ _is_valid_websocket_key,
11
12
  base64_encode,
12
13
  handshake_websocket_with_options,
13
14
  make_ws_frame_bytes,
@@ -332,6 +333,12 @@ if __name__ == "__main__":
332
333
 
333
334
 
334
335
  class TestWebSocketPrimitives(unittest.TestCase):
336
+ def test_websocket_key_must_be_sixteen_base64_octets(self):
337
+ self.assertTrue(_is_valid_websocket_key("dGhlIHNhbXBsZSBub25jZQ=="))
338
+ for key in (None, "", "not base64!", "ñandú", "dGhlIHNhbXBsZQ=="):
339
+ with self.subTest(key=key):
340
+ self.assertFalse(_is_valid_websocket_key(key))
341
+
335
342
  def test_unmask_payload_matches_the_per_byte_definition(self):
336
343
  mask = b"\x37\xfa\x21\x3d"
337
344
  for size in (0, 1, 2, 3, 4, 5, 7, 8, 1023, 4096):
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
File without changes
File without changes
File without changes
File without changes