about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/websockets/extensions
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/websockets/extensions')
-rw-r--r--.venv/lib/python3.12/site-packages/websockets/extensions/__init__.py4
-rw-r--r--.venv/lib/python3.12/site-packages/websockets/extensions/base.py123
-rw-r--r--.venv/lib/python3.12/site-packages/websockets/extensions/permessage_deflate.py697
3 files changed, 824 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/websockets/extensions/__init__.py b/.venv/lib/python3.12/site-packages/websockets/extensions/__init__.py
new file mode 100644
index 00000000..02838b98
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/websockets/extensions/__init__.py
@@ -0,0 +1,4 @@
+from .base import *
+
+
+__all__ = ["Extension", "ClientExtensionFactory", "ServerExtensionFactory"]
diff --git a/.venv/lib/python3.12/site-packages/websockets/extensions/base.py b/.venv/lib/python3.12/site-packages/websockets/extensions/base.py
new file mode 100644
index 00000000..42dd6c5f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/websockets/extensions/base.py
@@ -0,0 +1,123 @@
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+from ..frames import Frame
+from ..typing import ExtensionName, ExtensionParameter
+
+
+__all__ = ["Extension", "ClientExtensionFactory", "ServerExtensionFactory"]
+
+
+class Extension:
+    """
+    Base class for extensions.
+
+    """
+
+    name: ExtensionName
+    """Extension identifier."""
+
+    def decode(self, frame: Frame, *, max_size: int | None = None) -> Frame:
+        """
+        Decode an incoming frame.
+
+        Args:
+            frame: Incoming frame.
+            max_size: Maximum payload size in bytes.
+
+        Returns:
+            Decoded frame.
+
+        Raises:
+            PayloadTooBig: If decoding the payload exceeds ``max_size``.
+
+        """
+        raise NotImplementedError
+
+    def encode(self, frame: Frame) -> Frame:
+        """
+        Encode an outgoing frame.
+
+        Args:
+            frame: Outgoing frame.
+
+        Returns:
+            Encoded frame.
+
+        """
+        raise NotImplementedError
+
+
+class ClientExtensionFactory:
+    """
+    Base class for client-side extension factories.
+
+    """
+
+    name: ExtensionName
+    """Extension identifier."""
+
+    def get_request_params(self) -> list[ExtensionParameter]:
+        """
+        Build parameters to send to the server for this extension.
+
+        Returns:
+            Parameters to send to the server.
+
+        """
+        raise NotImplementedError
+
+    def process_response_params(
+        self,
+        params: Sequence[ExtensionParameter],
+        accepted_extensions: Sequence[Extension],
+    ) -> Extension:
+        """
+        Process parameters received from the server.
+
+        Args:
+            params: Parameters received from the server for this extension.
+            accepted_extensions: List of previously accepted extensions.
+
+        Returns:
+            An extension instance.
+
+        Raises:
+            NegotiationError: If parameters aren't acceptable.
+
+        """
+        raise NotImplementedError
+
+
+class ServerExtensionFactory:
+    """
+    Base class for server-side extension factories.
+
+    """
+
+    name: ExtensionName
+    """Extension identifier."""
+
+    def process_request_params(
+        self,
+        params: Sequence[ExtensionParameter],
+        accepted_extensions: Sequence[Extension],
+    ) -> tuple[list[ExtensionParameter], Extension]:
+        """
+        Process parameters received from the client.
+
+        Args:
+            params: Parameters received from the client for this extension.
+            accepted_extensions: List of previously accepted extensions.
+
+        Returns:
+            To accept the offer, parameters to send to the client for this
+            extension and an extension instance.
+
+        Raises:
+            NegotiationError: To reject the offer, if parameters received from
+                the client aren't acceptable.
+
+        """
+        raise NotImplementedError
diff --git a/.venv/lib/python3.12/site-packages/websockets/extensions/permessage_deflate.py b/.venv/lib/python3.12/site-packages/websockets/extensions/permessage_deflate.py
new file mode 100644
index 00000000..cefad4f5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/websockets/extensions/permessage_deflate.py
@@ -0,0 +1,697 @@
+from __future__ import annotations
+
+import zlib
+from collections.abc import Sequence
+from typing import Any
+
+from .. import frames
+from ..exceptions import (
+    DuplicateParameter,
+    InvalidParameterName,
+    InvalidParameterValue,
+    NegotiationError,
+    PayloadTooBig,
+    ProtocolError,
+)
+from ..typing import ExtensionName, ExtensionParameter
+from .base import ClientExtensionFactory, Extension, ServerExtensionFactory
+
+
+__all__ = [
+    "PerMessageDeflate",
+    "ClientPerMessageDeflateFactory",
+    "enable_client_permessage_deflate",
+    "ServerPerMessageDeflateFactory",
+    "enable_server_permessage_deflate",
+]
+
+_EMPTY_UNCOMPRESSED_BLOCK = b"\x00\x00\xff\xff"
+
+_MAX_WINDOW_BITS_VALUES = [str(bits) for bits in range(8, 16)]
+
+
+class PerMessageDeflate(Extension):
+    """
+    Per-Message Deflate extension.
+
+    """
+
+    name = ExtensionName("permessage-deflate")
+
+    def __init__(
+        self,
+        remote_no_context_takeover: bool,
+        local_no_context_takeover: bool,
+        remote_max_window_bits: int,
+        local_max_window_bits: int,
+        compress_settings: dict[Any, Any] | None = None,
+    ) -> None:
+        """
+        Configure the Per-Message Deflate extension.
+
+        """
+        if compress_settings is None:
+            compress_settings = {}
+
+        assert remote_no_context_takeover in [False, True]
+        assert local_no_context_takeover in [False, True]
+        assert 8 <= remote_max_window_bits <= 15
+        assert 8 <= local_max_window_bits <= 15
+        assert "wbits" not in compress_settings
+
+        self.remote_no_context_takeover = remote_no_context_takeover
+        self.local_no_context_takeover = local_no_context_takeover
+        self.remote_max_window_bits = remote_max_window_bits
+        self.local_max_window_bits = local_max_window_bits
+        self.compress_settings = compress_settings
+
+        if not self.remote_no_context_takeover:
+            self.decoder = zlib.decompressobj(wbits=-self.remote_max_window_bits)
+
+        if not self.local_no_context_takeover:
+            self.encoder = zlib.compressobj(
+                wbits=-self.local_max_window_bits,
+                **self.compress_settings,
+            )
+
+        # To handle continuation frames properly, we must keep track of
+        # whether that initial frame was encoded.
+        self.decode_cont_data = False
+        # There's no need for self.encode_cont_data because we always encode
+        # outgoing frames, so it would always be True.
+
+    def __repr__(self) -> str:
+        return (
+            f"PerMessageDeflate("
+            f"remote_no_context_takeover={self.remote_no_context_takeover}, "
+            f"local_no_context_takeover={self.local_no_context_takeover}, "
+            f"remote_max_window_bits={self.remote_max_window_bits}, "
+            f"local_max_window_bits={self.local_max_window_bits})"
+        )
+
+    def decode(
+        self,
+        frame: frames.Frame,
+        *,
+        max_size: int | None = None,
+    ) -> frames.Frame:
+        """
+        Decode an incoming frame.
+
+        """
+        # Skip control frames.
+        if frame.opcode in frames.CTRL_OPCODES:
+            return frame
+
+        # Handle continuation data frames:
+        # - skip if the message isn't encoded
+        # - reset "decode continuation data" flag if it's a final frame
+        if frame.opcode is frames.OP_CONT:
+            if not self.decode_cont_data:
+                return frame
+            if frame.fin:
+                self.decode_cont_data = False
+
+        # Handle text and binary data frames:
+        # - skip if the message isn't encoded
+        # - unset the rsv1 flag on the first frame of a compressed message
+        # - set "decode continuation data" flag if it's a non-final frame
+        else:
+            if not frame.rsv1:
+                return frame
+            if not frame.fin:
+                self.decode_cont_data = True
+
+            # Re-initialize per-message decoder.
+            if self.remote_no_context_takeover:
+                self.decoder = zlib.decompressobj(wbits=-self.remote_max_window_bits)
+
+        # Uncompress data. Protect against zip bombs by preventing zlib from
+        # decompressing more than max_length bytes (except when the limit is
+        # disabled with max_size = None).
+        if frame.fin and len(frame.data) < 2044:
+            # Profiling shows that appending four bytes, which makes a copy, is
+            # faster than calling decompress() again when data is less than 2kB.
+            data = bytes(frame.data) + _EMPTY_UNCOMPRESSED_BLOCK
+        else:
+            data = frame.data
+        max_length = 0 if max_size is None else max_size
+        try:
+            data = self.decoder.decompress(data, max_length)
+            if self.decoder.unconsumed_tail:
+                assert max_size is not None  # help mypy
+                raise PayloadTooBig(None, max_size)
+            if frame.fin and len(frame.data) >= 2044:
+                # This cannot generate additional data.
+                self.decoder.decompress(_EMPTY_UNCOMPRESSED_BLOCK)
+        except zlib.error as exc:
+            raise ProtocolError("decompression failed") from exc
+
+        # Allow garbage collection of the decoder if it won't be reused.
+        if frame.fin and self.remote_no_context_takeover:
+            del self.decoder
+
+        return frames.Frame(
+            frame.opcode,
+            data,
+            frame.fin,
+            # Unset the rsv1 flag on the first frame of a compressed message.
+            False,
+            frame.rsv2,
+            frame.rsv3,
+        )
+
+    def encode(self, frame: frames.Frame) -> frames.Frame:
+        """
+        Encode an outgoing frame.
+
+        """
+        # Skip control frames.
+        if frame.opcode in frames.CTRL_OPCODES:
+            return frame
+
+        # Since we always encode messages, there's no "encode continuation
+        # data" flag similar to "decode continuation data" at this time.
+
+        if frame.opcode is not frames.OP_CONT:
+            # Re-initialize per-message decoder.
+            if self.local_no_context_takeover:
+                self.encoder = zlib.compressobj(
+                    wbits=-self.local_max_window_bits,
+                    **self.compress_settings,
+                )
+
+        # Compress data.
+        data = self.encoder.compress(frame.data) + self.encoder.flush(zlib.Z_SYNC_FLUSH)
+        if frame.fin:
+            # Sync flush generates between 5 or 6 bytes, ending with the bytes
+            # 0x00 0x00 0xff 0xff, which must be removed.
+            assert data[-4:] == _EMPTY_UNCOMPRESSED_BLOCK
+            # Making a copy is faster than memoryview(a)[:-4] until 2kB.
+            if len(data) < 2048:
+                data = data[:-4]
+            else:
+                data = memoryview(data)[:-4]
+
+        # Allow garbage collection of the encoder if it won't be reused.
+        if frame.fin and self.local_no_context_takeover:
+            del self.encoder
+
+        return frames.Frame(
+            frame.opcode,
+            data,
+            frame.fin,
+            # Set the rsv1 flag on the first frame of a compressed message.
+            frame.opcode is not frames.OP_CONT,
+            frame.rsv2,
+            frame.rsv3,
+        )
+
+
+def _build_parameters(
+    server_no_context_takeover: bool,
+    client_no_context_takeover: bool,
+    server_max_window_bits: int | None,
+    client_max_window_bits: int | bool | None,
+) -> list[ExtensionParameter]:
+    """
+    Build a list of ``(name, value)`` pairs for some compression parameters.
+
+    """
+    params: list[ExtensionParameter] = []
+    if server_no_context_takeover:
+        params.append(("server_no_context_takeover", None))
+    if client_no_context_takeover:
+        params.append(("client_no_context_takeover", None))
+    if server_max_window_bits:
+        params.append(("server_max_window_bits", str(server_max_window_bits)))
+    if client_max_window_bits is True:  # only in handshake requests
+        params.append(("client_max_window_bits", None))
+    elif client_max_window_bits:
+        params.append(("client_max_window_bits", str(client_max_window_bits)))
+    return params
+
+
+def _extract_parameters(
+    params: Sequence[ExtensionParameter], *, is_server: bool
+) -> tuple[bool, bool, int | None, int | bool | None]:
+    """
+    Extract compression parameters from a list of ``(name, value)`` pairs.
+
+    If ``is_server`` is :obj:`True`, ``client_max_window_bits`` may be
+    provided without a value. This is only allowed in handshake requests.
+
+    """
+    server_no_context_takeover: bool = False
+    client_no_context_takeover: bool = False
+    server_max_window_bits: int | None = None
+    client_max_window_bits: int | bool | None = None
+
+    for name, value in params:
+        if name == "server_no_context_takeover":
+            if server_no_context_takeover:
+                raise DuplicateParameter(name)
+            if value is None:
+                server_no_context_takeover = True
+            else:
+                raise InvalidParameterValue(name, value)
+
+        elif name == "client_no_context_takeover":
+            if client_no_context_takeover:
+                raise DuplicateParameter(name)
+            if value is None:
+                client_no_context_takeover = True
+            else:
+                raise InvalidParameterValue(name, value)
+
+        elif name == "server_max_window_bits":
+            if server_max_window_bits is not None:
+                raise DuplicateParameter(name)
+            if value in _MAX_WINDOW_BITS_VALUES:
+                server_max_window_bits = int(value)
+            else:
+                raise InvalidParameterValue(name, value)
+
+        elif name == "client_max_window_bits":
+            if client_max_window_bits is not None:
+                raise DuplicateParameter(name)
+            if is_server and value is None:  # only in handshake requests
+                client_max_window_bits = True
+            elif value in _MAX_WINDOW_BITS_VALUES:
+                client_max_window_bits = int(value)
+            else:
+                raise InvalidParameterValue(name, value)
+
+        else:
+            raise InvalidParameterName(name)
+
+    return (
+        server_no_context_takeover,
+        client_no_context_takeover,
+        server_max_window_bits,
+        client_max_window_bits,
+    )
+
+
+class ClientPerMessageDeflateFactory(ClientExtensionFactory):
+    """
+    Client-side extension factory for the Per-Message Deflate extension.
+
+    Parameters behave as described in `section 7.1 of RFC 7692`_.
+
+    .. _section 7.1 of RFC 7692: https://datatracker.ietf.org/doc/html/rfc7692#section-7.1
+
+    Set them to :obj:`True` to include them in the negotiation offer without a
+    value or to an integer value to include them with this value.
+
+    Args:
+        server_no_context_takeover: Prevent server from using context takeover.
+        client_no_context_takeover: Prevent client from using context takeover.
+        server_max_window_bits: Maximum size of the server's LZ77 sliding window
+            in bits, between 8 and 15.
+        client_max_window_bits: Maximum size of the client's LZ77 sliding window
+            in bits, between 8 and 15, or :obj:`True` to indicate support without
+            setting a limit.
+        compress_settings: Additional keyword arguments for :func:`zlib.compressobj`,
+            excluding ``wbits``.
+
+    """
+
+    name = ExtensionName("permessage-deflate")
+
+    def __init__(
+        self,
+        server_no_context_takeover: bool = False,
+        client_no_context_takeover: bool = False,
+        server_max_window_bits: int | None = None,
+        client_max_window_bits: int | bool | None = True,
+        compress_settings: dict[str, Any] | None = None,
+    ) -> None:
+        """
+        Configure the Per-Message Deflate extension factory.
+
+        """
+        if not (server_max_window_bits is None or 8 <= server_max_window_bits <= 15):
+            raise ValueError("server_max_window_bits must be between 8 and 15")
+        if not (
+            client_max_window_bits is None
+            or client_max_window_bits is True
+            or 8 <= client_max_window_bits <= 15
+        ):
+            raise ValueError("client_max_window_bits must be between 8 and 15")
+        if compress_settings is not None and "wbits" in compress_settings:
+            raise ValueError(
+                "compress_settings must not include wbits, "
+                "set client_max_window_bits instead"
+            )
+
+        self.server_no_context_takeover = server_no_context_takeover
+        self.client_no_context_takeover = client_no_context_takeover
+        self.server_max_window_bits = server_max_window_bits
+        self.client_max_window_bits = client_max_window_bits
+        self.compress_settings = compress_settings
+
+    def get_request_params(self) -> list[ExtensionParameter]:
+        """
+        Build request parameters.
+
+        """
+        return _build_parameters(
+            self.server_no_context_takeover,
+            self.client_no_context_takeover,
+            self.server_max_window_bits,
+            self.client_max_window_bits,
+        )
+
+    def process_response_params(
+        self,
+        params: Sequence[ExtensionParameter],
+        accepted_extensions: Sequence[Extension],
+    ) -> PerMessageDeflate:
+        """
+        Process response parameters.
+
+        Return an extension instance.
+
+        """
+        if any(other.name == self.name for other in accepted_extensions):
+            raise NegotiationError(f"received duplicate {self.name}")
+
+        # Request parameters are available in instance variables.
+
+        # Load response parameters in local variables.
+        (
+            server_no_context_takeover,
+            client_no_context_takeover,
+            server_max_window_bits,
+            client_max_window_bits,
+        ) = _extract_parameters(params, is_server=False)
+
+        # After comparing the request and the response, the final
+        # configuration must be available in the local variables.
+
+        # server_no_context_takeover
+        #
+        #   Req.    Resp.   Result
+        #   ------  ------  --------------------------------------------------
+        #   False   False   False
+        #   False   True    True
+        #   True    False   Error!
+        #   True    True    True
+
+        if self.server_no_context_takeover:
+            if not server_no_context_takeover:
+                raise NegotiationError("expected server_no_context_takeover")
+
+        # client_no_context_takeover
+        #
+        #   Req.    Resp.   Result
+        #   ------  ------  --------------------------------------------------
+        #   False   False   False
+        #   False   True    True
+        #   True    False   True - must change value
+        #   True    True    True
+
+        if self.client_no_context_takeover:
+            if not client_no_context_takeover:
+                client_no_context_takeover = True
+
+        # server_max_window_bits
+
+        #   Req.    Resp.   Result
+        #   ------  ------  --------------------------------------------------
+        #   None    None    None
+        #   None    8≤M≤15  M
+        #   8≤N≤15  None    Error!
+        #   8≤N≤15  8≤M≤N   M
+        #   8≤N≤15  N<M≤15  Error!
+
+        if self.server_max_window_bits is None:
+            pass
+
+        else:
+            if server_max_window_bits is None:
+                raise NegotiationError("expected server_max_window_bits")
+            elif server_max_window_bits > self.server_max_window_bits:
+                raise NegotiationError("unsupported server_max_window_bits")
+
+        # client_max_window_bits
+
+        #   Req.    Resp.   Result
+        #   ------  ------  --------------------------------------------------
+        #   None    None    None
+        #   None    8≤M≤15  Error!
+        #   True    None    None
+        #   True    8≤M≤15  M
+        #   8≤N≤15  None    N - must change value
+        #   8≤N≤15  8≤M≤N   M
+        #   8≤N≤15  N<M≤15  Error!
+
+        if self.client_max_window_bits is None:
+            if client_max_window_bits is not None:
+                raise NegotiationError("unexpected client_max_window_bits")
+
+        elif self.client_max_window_bits is True:
+            pass
+
+        else:
+            if client_max_window_bits is None:
+                client_max_window_bits = self.client_max_window_bits
+            elif client_max_window_bits > self.client_max_window_bits:
+                raise NegotiationError("unsupported client_max_window_bits")
+
+        return PerMessageDeflate(
+            server_no_context_takeover,  # remote_no_context_takeover
+            client_no_context_takeover,  # local_no_context_takeover
+            server_max_window_bits or 15,  # remote_max_window_bits
+            client_max_window_bits or 15,  # local_max_window_bits
+            self.compress_settings,
+        )
+
+
+def enable_client_permessage_deflate(
+    extensions: Sequence[ClientExtensionFactory] | None,
+) -> Sequence[ClientExtensionFactory]:
+    """
+    Enable Per-Message Deflate with default settings in client extensions.
+
+    If the extension is already present, perhaps with non-default settings,
+    the configuration isn't changed.
+
+    """
+    if extensions is None:
+        extensions = []
+    if not any(
+        extension_factory.name == ClientPerMessageDeflateFactory.name
+        for extension_factory in extensions
+    ):
+        extensions = list(extensions) + [
+            ClientPerMessageDeflateFactory(
+                compress_settings={"memLevel": 5},
+            )
+        ]
+    return extensions
+
+
+class ServerPerMessageDeflateFactory(ServerExtensionFactory):
+    """
+    Server-side extension factory for the Per-Message Deflate extension.
+
+    Parameters behave as described in `section 7.1 of RFC 7692`_.
+
+    .. _section 7.1 of RFC 7692: https://datatracker.ietf.org/doc/html/rfc7692#section-7.1
+
+    Set them to :obj:`True` to include them in the negotiation offer without a
+    value or to an integer value to include them with this value.
+
+    Args:
+        server_no_context_takeover: Prevent server from using context takeover.
+        client_no_context_takeover: Prevent client from using context takeover.
+        server_max_window_bits: Maximum size of the server's LZ77 sliding window
+            in bits, between 8 and 15.
+        client_max_window_bits: Maximum size of the client's LZ77 sliding window
+            in bits, between 8 and 15.
+        compress_settings: Additional keyword arguments for :func:`zlib.compressobj`,
+            excluding ``wbits``.
+        require_client_max_window_bits: Do not enable compression at all if
+            client doesn't advertise support for ``client_max_window_bits``;
+            the default behavior is to enable compression without enforcing
+            ``client_max_window_bits``.
+
+    """
+
+    name = ExtensionName("permessage-deflate")
+
+    def __init__(
+        self,
+        server_no_context_takeover: bool = False,
+        client_no_context_takeover: bool = False,
+        server_max_window_bits: int | None = None,
+        client_max_window_bits: int | None = None,
+        compress_settings: dict[str, Any] | None = None,
+        require_client_max_window_bits: bool = False,
+    ) -> None:
+        """
+        Configure the Per-Message Deflate extension factory.
+
+        """
+        if not (server_max_window_bits is None or 8 <= server_max_window_bits <= 15):
+            raise ValueError("server_max_window_bits must be between 8 and 15")
+        if not (client_max_window_bits is None or 8 <= client_max_window_bits <= 15):
+            raise ValueError("client_max_window_bits must be between 8 and 15")
+        if compress_settings is not None and "wbits" in compress_settings:
+            raise ValueError(
+                "compress_settings must not include wbits, "
+                "set server_max_window_bits instead"
+            )
+        if client_max_window_bits is None and require_client_max_window_bits:
+            raise ValueError(
+                "require_client_max_window_bits is enabled, "
+                "but client_max_window_bits isn't configured"
+            )
+
+        self.server_no_context_takeover = server_no_context_takeover
+        self.client_no_context_takeover = client_no_context_takeover
+        self.server_max_window_bits = server_max_window_bits
+        self.client_max_window_bits = client_max_window_bits
+        self.compress_settings = compress_settings
+        self.require_client_max_window_bits = require_client_max_window_bits
+
+    def process_request_params(
+        self,
+        params: Sequence[ExtensionParameter],
+        accepted_extensions: Sequence[Extension],
+    ) -> tuple[list[ExtensionParameter], PerMessageDeflate]:
+        """
+        Process request parameters.
+
+        Return response params and an extension instance.
+
+        """
+        if any(other.name == self.name for other in accepted_extensions):
+            raise NegotiationError(f"skipped duplicate {self.name}")
+
+        # Load request parameters in local variables.
+        (
+            server_no_context_takeover,
+            client_no_context_takeover,
+            server_max_window_bits,
+            client_max_window_bits,
+        ) = _extract_parameters(params, is_server=True)
+
+        # Configuration parameters are available in instance variables.
+
+        # After comparing the request and the configuration, the response must
+        # be available in the local variables.
+
+        # server_no_context_takeover
+        #
+        #   Config  Req.    Resp.
+        #   ------  ------  --------------------------------------------------
+        #   False   False   False
+        #   False   True    True
+        #   True    False   True - must change value to True
+        #   True    True    True
+
+        if self.server_no_context_takeover:
+            if not server_no_context_takeover:
+                server_no_context_takeover = True
+
+        # client_no_context_takeover
+        #
+        #   Config  Req.    Resp.
+        #   ------  ------  --------------------------------------------------
+        #   False   False   False
+        #   False   True    True (or False)
+        #   True    False   True - must change value to True
+        #   True    True    True (or False)
+
+        if self.client_no_context_takeover:
+            if not client_no_context_takeover:
+                client_no_context_takeover = True
+
+        # server_max_window_bits
+
+        #   Config  Req.    Resp.
+        #   ------  ------  --------------------------------------------------
+        #   None    None    None
+        #   None    8≤M≤15  M
+        #   8≤N≤15  None    N - must change value
+        #   8≤N≤15  8≤M≤N   M
+        #   8≤N≤15  N<M≤15  N - must change value
+
+        if self.server_max_window_bits is None:
+            pass
+
+        else:
+            if server_max_window_bits is None:
+                server_max_window_bits = self.server_max_window_bits
+            elif server_max_window_bits > self.server_max_window_bits:
+                server_max_window_bits = self.server_max_window_bits
+
+        # client_max_window_bits
+
+        #   Config  Req.    Resp.
+        #   ------  ------  --------------------------------------------------
+        #   None    None    None
+        #   None    True    None - must change value
+        #   None    8≤M≤15  M (or None)
+        #   8≤N≤15  None    None or Error!
+        #   8≤N≤15  True    N - must change value
+        #   8≤N≤15  8≤M≤N   M (or None)
+        #   8≤N≤15  N<M≤15  N
+
+        if self.client_max_window_bits is None:
+            if client_max_window_bits is True:
+                client_max_window_bits = self.client_max_window_bits
+
+        else:
+            if client_max_window_bits is None:
+                if self.require_client_max_window_bits:
+                    raise NegotiationError("required client_max_window_bits")
+            elif client_max_window_bits is True:
+                client_max_window_bits = self.client_max_window_bits
+            elif self.client_max_window_bits < client_max_window_bits:
+                client_max_window_bits = self.client_max_window_bits
+
+        return (
+            _build_parameters(
+                server_no_context_takeover,
+                client_no_context_takeover,
+                server_max_window_bits,
+                client_max_window_bits,
+            ),
+            PerMessageDeflate(
+                client_no_context_takeover,  # remote_no_context_takeover
+                server_no_context_takeover,  # local_no_context_takeover
+                client_max_window_bits or 15,  # remote_max_window_bits
+                server_max_window_bits or 15,  # local_max_window_bits
+                self.compress_settings,
+            ),
+        )
+
+
+def enable_server_permessage_deflate(
+    extensions: Sequence[ServerExtensionFactory] | None,
+) -> Sequence[ServerExtensionFactory]:
+    """
+    Enable Per-Message Deflate with default settings in server extensions.
+
+    If the extension is already present, perhaps with non-default settings,
+    the configuration isn't changed.
+
+    """
+    if extensions is None:
+        extensions = []
+    if not any(
+        ext_factory.name == ServerPerMessageDeflateFactory.name
+        for ext_factory in extensions
+    ):
+        extensions = list(extensions) + [
+            ServerPerMessageDeflateFactory(
+                server_max_window_bits=12,
+                client_max_window_bits=12,
+                compress_settings={"memLevel": 5},
+            )
+        ]
+    return extensions