Skip to content

Commit bbb3161

Browse files
committed
Add env vars for configuring constants.
1 parent e35c15a commit bbb3161

File tree

17 files changed

+149
-69
lines changed

17 files changed

+149
-69
lines changed

docs/project/changelog.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ New features
7373

7474
* Validated compatibility with Python 3.12.
7575

76+
* Added :doc:`environment variables <../reference/variables>` to configure debug
77+
logs, the ``Server`` and ``User-Agent`` headers, as well as security limits.
78+
79+
If you were monkey-patching constants, be aware that they were renamed, which
80+
will break your configuration. You must switch to the environment variables.
81+
7682
12.0
7783
----
7884

docs/reference/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ These low-level APIs are shared by all implementations.
8585
datastructures
8686
exceptions
8787
types
88+
variables
8889

8990
API stability
9091
-------------

docs/reference/variables.rst

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
Environment variables
2+
=====================
3+
4+
Logging
5+
-------
6+
7+
.. envvar:: WEBSOCKETS_MAX_LOG_SIZE
8+
9+
How much of each frame to show in debug logs.
10+
11+
The default value is ``75``.
12+
13+
See the :doc:`logging guide <../topics/logging>` for details.
14+
15+
Security
16+
........
17+
18+
.. envvar:: WEBSOCKETS_SERVER
19+
20+
Server header sent by websockets.
21+
22+
The default value uses the format ``"Python/x.y.z websockets/X.Y"``.
23+
24+
.. envvar:: WEBSOCKETS_USER_AGENT
25+
26+
User-Agent header sent by websockets.
27+
28+
The default value uses the format ``"Python/x.y.z websockets/X.Y"``.
29+
30+
.. envvar:: WEBSOCKETS_MAX_LINE_LENGTH
31+
32+
Maximum length of the request or status line in the opening handshake.
33+
34+
The default value is ``8192``.
35+
36+
.. envvar:: WEBSOCKETS_MAX_NUM_HEADERS
37+
38+
Maximum number of HTTP headers in the opening handshake.
39+
40+
The default value is ``128``.
41+
42+
.. envvar:: WEBSOCKETS_MAX_BODY_SIZE
43+
44+
Maximum size of the body of an HTTP response in the opening handshake.
45+
46+
The default value is ``1_048_576`` (1 MiB).
47+
48+
See the :doc:`security guide <../topics/security>` for details.

docs/topics/logging.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ Here's how to enable debug logs for development::
7676
level=logging.DEBUG,
7777
)
7878

79+
By default, websockets elides the content of messages to improve readability.
80+
If you want to see more, you can increase the :envvar:`WEBSOCKETS_MAX_LOG_SIZE`
81+
environment variable. The default value is 75.
82+
7983
Furthermore, websockets adds a ``websocket`` attribute to log records, so you
8084
can include additional information about the current connection in logs.
8185

docs/topics/security.rst

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
Security
22
========
33

4+
.. currentmodule:: websockets
5+
46
Encryption
57
----------
68

@@ -27,15 +29,33 @@ an amplification factor of 1000 between network traffic and memory usage.
2729
Configuring a server to :doc:`optimize memory usage <memory>` will improve
2830
security in addition to improving performance.
2931

30-
Other limits
31-
------------
32+
HTTP limits
33+
-----------
34+
35+
In the opening handshake, websockets applies limits to the amount of data that
36+
it accepts in order to minimize exposure to denial of service attacks.
37+
38+
The request or status line is limited to 8192 bytes. Each header line, including
39+
the name and value, is limited to 8192 bytes too. No more than 128 HTTP headers
40+
are allowed. When the HTTP response includes a body, it is limited to 1 MiB.
41+
42+
You may change these limits by setting the :envvar:`WEBSOCKETS_MAX_LINE_LENGTH`,
43+
:envvar:`WEBSOCKETS_MAX_NUM_HEADERS`, and :envvar:`WEBSOCKETS_MAX_BODY_SIZE`
44+
environment variables respectively.
45+
46+
Identification
47+
--------------
48+
49+
By default, websockets identifies itself with a ``Server`` or ``User-Agent``
50+
header in the format ``"Python/x.y.z websockets/X.Y"``.
3251

33-
websockets implements additional limits on the amount of data it accepts in
34-
order to minimize exposure to security vulnerabilities.
52+
You can set the ``server_header`` argument of :func:`~server.serve` or the
53+
``user_agent_header`` argument of :func:`~client.connect` to configure another
54+
value. Setting them to :obj:`None` removes the header.
3555

36-
In the opening handshake, websockets limits the number of HTTP headers to 256
37-
and the size of an individual header to 4096 bytes. These limits are 10 to 20
38-
times larger than what's expected in standard use cases. They're hard-coded.
56+
Alternatively, you can set the :envvar:`WEBSOCKETS_SERVER` and
57+
:envvar:`WEBSOCKETS_USER_AGENT` environment variables respectively. Setting them
58+
to an empty string removes the header.
3959

40-
If you need to change these limits, you can monkey-patch the constants in
41-
``websockets.http11``.
60+
If both the argument and the environment variable are set, the argument takes
61+
precedence.

src/websockets/asyncio/client.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
from ..extensions.base import ClientExtensionFactory
1010
from ..extensions.permessage_deflate import enable_client_permessage_deflate
1111
from ..headers import validate_subprotocols
12-
from ..http import USER_AGENT
13-
from ..http11 import Response
12+
from ..http11 import USER_AGENT, Response
1413
from ..protocol import CONNECTING, Event
1514
from ..typing import LoggerLike, Origin, Subprotocol
1615
from ..uri import parse_uri
@@ -71,7 +70,7 @@ async def handshake(
7170
self.request = self.protocol.connect()
7271
if additional_headers is not None:
7372
self.request.headers.update(additional_headers)
74-
if user_agent_header is not None:
73+
if user_agent_header:
7574
self.request.headers["User-Agent"] = user_agent_header
7675
self.protocol.send_request(self.request)
7776

src/websockets/asyncio/server.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020
from ..extensions.base import ServerExtensionFactory
2121
from ..extensions.permessage_deflate import enable_server_permessage_deflate
2222
from ..headers import validate_subprotocols
23-
from ..http import USER_AGENT
24-
from ..http11 import Request, Response
23+
from ..http11 import SERVER, Request, Response
2524
from ..protocol import CONNECTING, Event
2625
from ..server import ServerProtocol
2726
from ..typing import LoggerLike, Origin, Subprotocol
@@ -88,7 +87,7 @@ async def handshake(
8887
]
8988
| None
9089
) = None,
91-
server_header: str | None = USER_AGENT,
90+
server_header: str | None = SERVER,
9291
) -> None:
9392
"""
9493
Perform the opening handshake.
@@ -131,7 +130,7 @@ async def handshake(
131130
assert isinstance(response, Response) # help mypy
132131
self.response = response
133132

134-
if server_header is not None:
133+
if server_header:
135134
self.response.headers["Server"] = server_header
136135

137136
response = None
@@ -243,7 +242,7 @@ def __init__(
243242
]
244243
| None
245244
) = None,
246-
server_header: str | None = USER_AGENT,
245+
server_header: str | None = SERVER,
247246
open_timeout: float | None = 10,
248247
logger: LoggerLike | None = None,
249248
) -> None:
@@ -631,7 +630,7 @@ def __init__(
631630
]
632631
| None
633632
) = None,
634-
server_header: str | None = USER_AGENT,
633+
server_header: str | None = SERVER,
635634
compression: str | None = "deflate",
636635
# Timeouts
637636
open_timeout: float | None = 10,

src/websockets/frames.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import dataclasses
44
import enum
55
import io
6+
import os
67
import secrets
78
import struct
89
from typing import Callable, Generator, Sequence
@@ -146,8 +147,8 @@ class Frame:
146147
rsv2: bool = False
147148
rsv3: bool = False
148149

149-
# Monkey-patch if you want to see more in logs. Should be a multiple of 3.
150-
MAX_LOG = 75
150+
# Configure if you want to see more in logs. Should be a multiple of 3.
151+
MAX_LOG_SIZE = int(os.environ.get("WEBSOCKETS_MAX_LOG_SIZE", "75"))
151152

152153
def __str__(self) -> str:
153154
"""
@@ -166,8 +167,8 @@ def __str__(self) -> str:
166167
# We'll show at most the first 16 bytes and the last 8 bytes.
167168
# Encode just what we need, plus two dummy bytes to elide later.
168169
binary = self.data
169-
if len(binary) > self.MAX_LOG // 3:
170-
cut = (self.MAX_LOG // 3 - 1) // 3 # by default cut = 8
170+
if len(binary) > self.MAX_LOG_SIZE // 3:
171+
cut = (self.MAX_LOG_SIZE // 3 - 1) // 3 # by default cut = 8
171172
binary = b"".join([binary[: 2 * cut], b"\x00\x00", binary[-cut:]])
172173
data = " ".join(f"{byte:02x}" for byte in binary)
173174
elif self.opcode is OP_CLOSE:
@@ -183,16 +184,16 @@ def __str__(self) -> str:
183184
coding = "text"
184185
except (UnicodeDecodeError, AttributeError):
185186
binary = self.data
186-
if len(binary) > self.MAX_LOG // 3:
187-
cut = (self.MAX_LOG // 3 - 1) // 3 # by default cut = 8
187+
if len(binary) > self.MAX_LOG_SIZE // 3:
188+
cut = (self.MAX_LOG_SIZE // 3 - 1) // 3 # by default cut = 8
188189
binary = b"".join([binary[: 2 * cut], b"\x00\x00", binary[-cut:]])
189190
data = " ".join(f"{byte:02x}" for byte in binary)
190191
coding = "binary"
191192
else:
192193
data = "''"
193194

194-
if len(data) > self.MAX_LOG:
195-
cut = self.MAX_LOG // 3 - 1 # by default cut = 24
195+
if len(data) > self.MAX_LOG_SIZE:
196+
cut = self.MAX_LOG_SIZE // 3 - 1 # by default cut = 24
196197
data = data[: 2 * cut] + "..." + data[-cut:]
197198

198199
metadata = ", ".join(filter(None, [coding, length, non_final]))

src/websockets/http.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
from __future__ import annotations
22

3-
import sys
43
import typing
54

65
from .imports import lazy_import
7-
from .version import version as websockets_version
86

97

108
# For backwards compatibility:
@@ -26,10 +24,3 @@
2624
"read_response": ".legacy.http",
2725
},
2826
)
29-
30-
31-
__all__ = ["USER_AGENT"]
32-
33-
34-
PYTHON_VERSION = "{}.{}".format(*sys.version_info)
35-
USER_AGENT = f"Python/{PYTHON_VERSION} websockets/{websockets_version}"

src/websockets/http11.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,43 @@
11
from __future__ import annotations
22

33
import dataclasses
4+
import os
45
import re
6+
import sys
57
import warnings
68
from typing import Callable, Generator
79

810
from . import datastructures, exceptions
11+
from .version import version as websockets_version
912

1013

14+
__all__ = ["SERVER", "USER_AGENT", "Request", "Response"]
15+
16+
17+
PYTHON_VERSION = "{}.{}".format(*sys.version_info)
18+
19+
# User-Agent header for HTTP requests.
20+
USER_AGENT = os.environ.get(
21+
"WEBSOCKETS_USER_AGENT",
22+
f"Python/{PYTHON_VERSION} websockets/{websockets_version}",
23+
)
24+
25+
# Server header for HTTP responses.
26+
SERVER = os.environ.get(
27+
"WEBSOCKETS_SERVER",
28+
f"Python/{PYTHON_VERSION} websockets/{websockets_version}",
29+
)
30+
1131
# Maximum total size of headers is around 128 * 8 KiB = 1 MiB.
12-
MAX_HEADERS = 128
32+
MAX_NUM_HEADERS = int(os.environ.get("WEBSOCKETS_MAX_NUM_HEADERS", "128"))
1333

1434
# Limit request line and header lines. 8KiB is the most common default
1535
# configuration of popular HTTP servers.
16-
MAX_LINE = 8192
36+
MAX_LINE_LENGTH = int(os.environ.get("WEBSOCKETS_MAX_LINE_LENGTH", "8192"))
1737

1838
# Support for HTTP response bodies is intended to read an error message
1939
# returned by a server. It isn't designed to perform large file transfers.
20-
MAX_BODY = 2**20 # 1 MiB
40+
MAX_BODY_SIZE = int(os.environ.get("WEBSOCKETS_MAX_BODY_SIZE", "1_048_576")) # 1 MiB
2141

2242

2343
def d(value: bytes) -> str:
@@ -258,12 +278,12 @@ def parse(
258278

259279
if content_length is None:
260280
try:
261-
body = yield from read_to_eof(MAX_BODY)
281+
body = yield from read_to_eof(MAX_BODY_SIZE)
262282
except RuntimeError:
263283
raise exceptions.SecurityError(
264-
f"body too large: over {MAX_BODY} bytes"
284+
f"body too large: over {MAX_BODY_SIZE} bytes"
265285
)
266-
elif content_length > MAX_BODY:
286+
elif content_length > MAX_BODY_SIZE:
267287
raise exceptions.SecurityError(
268288
f"body too large: {content_length} bytes"
269289
)
@@ -309,7 +329,7 @@ def parse_headers(
309329
# We don't attempt to support obsolete line folding.
310330

311331
headers = datastructures.Headers()
312-
for _ in range(MAX_HEADERS + 1):
332+
for _ in range(MAX_NUM_HEADERS + 1):
313333
try:
314334
line = yield from parse_line(read_line)
315335
except EOFError as exc:
@@ -355,7 +375,7 @@ def parse_line(
355375
356376
"""
357377
try:
358-
line = yield from read_line(MAX_LINE)
378+
line = yield from read_line(MAX_LINE_LENGTH)
359379
except RuntimeError:
360380
raise exceptions.SecurityError("line too long")
361381
# Not mandatory but safe - https://www.rfc-editor.org/rfc/rfc7230.html#section-3.5

0 commit comments

Comments
 (0)