0
# Extensions
1
2
WebSocket extensions support including RFC 7692 permessage-deflate compression with configurable parameters and negotiation. The extension system is designed to be pluggable and allows for custom extensions while providing a complete implementation of the standard compression extension.
3
4
## Capabilities
5
6
### Base Extension Class
7
8
Abstract base class for implementing WebSocket extensions with the standard negotiation and processing lifecycle.
9
10
```python { .api }
11
class Extension:
12
"""
13
Base class for WebSocket extensions.
14
"""
15
name: str # Extension name for negotiation
16
17
def enabled(self) -> bool:
18
"""
19
Check if the extension is enabled.
20
21
Returns:
22
True if extension is enabled, False otherwise
23
"""
24
25
def offer(self) -> Union[bool, str]:
26
"""
27
Generate an extension offer for the client to send.
28
29
Returns:
30
Extension parameters as string, or boolean indicating simple offer
31
"""
32
33
def accept(self, offer: str) -> Optional[Union[bool, str]]:
34
"""
35
Accept an extension offer from the client (server-side).
36
37
Args:
38
offer: The extension offer string from client
39
40
Returns:
41
Extension response parameters, True for simple accept,
42
None to reject the offer
43
"""
44
45
def finalize(self, offer: str) -> None:
46
"""
47
Finalize extension negotiation (client-side).
48
49
Args:
50
offer: The accepted extension parameters from server
51
"""
52
53
def frame_inbound_header(
54
self,
55
proto: Union[FrameDecoder, FrameProtocol],
56
opcode: Opcode,
57
rsv: RsvBits,
58
payload_length: int,
59
) -> Union[CloseReason, RsvBits]:
60
"""
61
Process inbound frame header.
62
63
Args:
64
proto: The frame protocol instance
65
opcode: Frame opcode
66
rsv: Reserved bits from frame header
67
payload_length: Length of frame payload
68
69
Returns:
70
Modified RSV bits or CloseReason for error
71
"""
72
73
def frame_inbound_payload_data(
74
self, proto: Union[FrameDecoder, FrameProtocol], data: bytes
75
) -> Union[bytes, CloseReason]:
76
"""
77
Process inbound frame payload data.
78
79
Args:
80
proto: The frame protocol instance
81
data: Payload data chunk
82
83
Returns:
84
Processed payload data or CloseReason for error
85
"""
86
87
def frame_inbound_complete(
88
self, proto: Union[FrameDecoder, FrameProtocol], fin: bool
89
) -> Union[bytes, CloseReason, None]:
90
"""
91
Process inbound frame completion.
92
93
Args:
94
proto: The frame protocol instance
95
fin: Whether this completes the frame
96
97
Returns:
98
Additional payload data, CloseReason for error, or None
99
"""
100
101
def frame_outbound(
102
self,
103
proto: Union[FrameDecoder, FrameProtocol],
104
opcode: Opcode,
105
rsv: RsvBits,
106
data: bytes,
107
fin: bool,
108
) -> Tuple[RsvBits, bytes]:
109
"""
110
Process outbound frame.
111
112
Args:
113
proto: The frame protocol instance
114
opcode: Frame opcode
115
rsv: Reserved bits for frame
116
data: Frame payload data
117
fin: Whether this completes the frame
118
119
Returns:
120
Tuple of (modified_rsv_bits, processed_payload_data)
121
"""
122
```
123
124
### PerMessage-Deflate Extension
125
126
Complete implementation of RFC 7692 permessage-deflate compression extension with configurable compression parameters.
127
128
```python { .api }
129
class PerMessageDeflate(Extension):
130
"""
131
RFC 7692 permessage-deflate WebSocket compression extension.
132
"""
133
name = "permessage-deflate" # Standard extension name
134
135
DEFAULT_CLIENT_MAX_WINDOW_BITS = 15 # Default client compression window size
136
DEFAULT_SERVER_MAX_WINDOW_BITS = 15 # Default server compression window size
137
138
def __init__(
139
self,
140
client_no_context_takeover: bool = False,
141
client_max_window_bits: Optional[int] = None,
142
server_no_context_takeover: bool = False,
143
server_max_window_bits: Optional[int] = None,
144
) -> None:
145
"""
146
Initialize permessage-deflate extension.
147
148
Args:
149
client_no_context_takeover: Client resets compression context each message
150
client_max_window_bits: Client compression window size (9-15)
151
server_no_context_takeover: Server resets compression context each message
152
server_max_window_bits: Server compression window size (9-15)
153
"""
154
155
@property
156
def client_max_window_bits(self) -> int:
157
"""
158
Get client maximum window bits.
159
160
Returns:
161
Window size between 9 and 15 inclusive
162
"""
163
164
@client_max_window_bits.setter
165
def client_max_window_bits(self, value: int) -> None:
166
"""
167
Set client maximum window bits.
168
169
Args:
170
value: Window size between 9 and 15 inclusive
171
172
Raises:
173
ValueError: If value is not between 9 and 15
174
"""
175
176
@property
177
def server_max_window_bits(self) -> int:
178
"""
179
Get server maximum window bits.
180
181
Returns:
182
Window size between 9 and 15 inclusive
183
"""
184
185
@server_max_window_bits.setter
186
def server_max_window_bits(self, value: int) -> None:
187
"""
188
Set server maximum window bits.
189
190
Args:
191
value: Window size between 9 and 15 inclusive
192
193
Raises:
194
ValueError: If value is not between 9 and 15
195
"""
196
```
197
198
### Extension Support Constants
199
200
```python { .api }
201
# Dictionary mapping all supported extension names to their class
202
SUPPORTED_EXTENSIONS = {PerMessageDeflate.name: PerMessageDeflate}
203
```
204
205
## Usage Examples
206
207
### Basic Compression Setup
208
209
```python
210
from wsproto import WSConnection, ConnectionType
211
from wsproto.extensions import PerMessageDeflate
212
from wsproto.events import Request, AcceptConnection
213
214
# Client with compression
215
compression = PerMessageDeflate()
216
ws = WSConnection(ConnectionType.CLIENT)
217
218
# Send request with compression offered
219
request_data = ws.send(Request(
220
host='example.com',
221
target='/ws',
222
extensions=[compression]
223
))
224
225
# Server accepting compression
226
ws_server = WSConnection(ConnectionType.SERVER)
227
ws_server.receive_data(request_data)
228
229
for event in ws_server.events():
230
if isinstance(event, Request):
231
# Accept with compression
232
server_compression = PerMessageDeflate()
233
response_data = ws_server.send(AcceptConnection(
234
extensions=[server_compression]
235
))
236
```
237
238
### Advanced Compression Configuration
239
240
```python
241
from wsproto.extensions import PerMessageDeflate
242
243
# Configure compression parameters
244
compression = PerMessageDeflate(
245
client_no_context_takeover=True, # Reset client context each message
246
client_max_window_bits=12, # Smaller window for less memory
247
server_no_context_takeover=False, # Keep server context between messages
248
server_max_window_bits=15, # Maximum compression for server
249
)
250
251
print(f"Client window bits: {compression.client_max_window_bits}")
252
print(f"Server window bits: {compression.server_max_window_bits}")
253
print(f"Extension enabled: {compression.enabled()}")
254
255
# Use in connection
256
ws = WSConnection(ConnectionType.CLIENT)
257
request_data = ws.send(Request(
258
host='example.com',
259
target='/ws',
260
extensions=[compression]
261
))
262
```
263
264
### Server-Side Extension Negotiation
265
266
```python
267
from wsproto import WSConnection, ConnectionType
268
from wsproto.extensions import PerMessageDeflate, SUPPORTED_EXTENSIONS
269
from wsproto.events import Request, AcceptConnection
270
271
def handle_extensions(requested_extensions):
272
"""Handle extension negotiation on server side."""
273
accepted_extensions = []
274
275
for ext_offer in requested_extensions:
276
ext_name = ext_offer.split(';')[0].strip()
277
278
if ext_name in SUPPORTED_EXTENSIONS:
279
ext_class = SUPPORTED_EXTENSIONS[ext_name]
280
extension = ext_class()
281
282
# Try to accept the offer
283
result = extension.accept(ext_offer)
284
if result is not None:
285
extension.finalize(ext_offer)
286
accepted_extensions.append(extension)
287
print(f"Accepted extension: {ext_name}")
288
else:
289
print(f"Rejected extension: {ext_name}")
290
else:
291
print(f"Unsupported extension: {ext_name}")
292
293
return accepted_extensions
294
295
# Server handling extensions
296
ws = WSConnection(ConnectionType.SERVER)
297
ws.receive_data(handshake_data)
298
299
for event in ws.events():
300
if isinstance(event, Request):
301
print(f"Requested extensions: {event.extensions}")
302
303
# Negotiate extensions
304
accepted_extensions = handle_extensions(event.extensions)
305
306
# Accept connection with negotiated extensions
307
response_data = ws.send(AcceptConnection(
308
extensions=accepted_extensions
309
))
310
```
311
312
### Custom Extension Implementation
313
314
```python
315
from wsproto.extensions import Extension
316
from wsproto.frame_protocol import Opcode, RsvBits, CloseReason
317
318
class SimpleLoggingExtension(Extension):
319
"""Example custom extension that logs frame information."""
320
321
name = "simple-logging"
322
323
def __init__(self):
324
self._enabled = False
325
326
def enabled(self) -> bool:
327
return self._enabled
328
329
def offer(self) -> Union[bool, str]:
330
return True # Simple offer with no parameters
331
332
def accept(self, offer: str) -> Optional[Union[bool, str]]:
333
self._enabled = True
334
return True # Accept the offer
335
336
def finalize(self, offer: str) -> None:
337
self._enabled = True
338
339
def frame_inbound_header(self, proto, opcode, rsv, payload_length):
340
print(f"Inbound frame: opcode={opcode}, length={payload_length}")
341
return RsvBits(False, False, False) # Don't modify RSV bits
342
343
def frame_inbound_payload_data(self, proto, data):
344
print(f"Inbound payload: {len(data)} bytes")
345
return data # Pass through unchanged
346
347
def frame_outbound(self, proto, opcode, rsv, data, fin):
348
print(f"Outbound frame: opcode={opcode}, length={len(data)}, fin={fin}")
349
return (rsv, data) # Pass through unchanged
350
351
# Use custom extension
352
custom_ext = SimpleLoggingExtension()
353
ws = WSConnection(ConnectionType.CLIENT)
354
request_data = ws.send(Request(
355
host='example.com',
356
target='/ws',
357
extensions=[custom_ext]
358
))
359
```
360
361
### Compression with Different Window Sizes
362
363
```python
364
from wsproto.extensions import PerMessageDeflate
365
366
# Memory-conscious compression (smaller windows)
367
low_memory_compression = PerMessageDeflate(
368
client_max_window_bits=9, # Minimum window size
369
server_max_window_bits=9,
370
client_no_context_takeover=True, # Reset context to save memory
371
server_no_context_takeover=True,
372
)
373
374
# High-compression setup (larger windows)
375
high_compression = PerMessageDeflate(
376
client_max_window_bits=15, # Maximum window size
377
server_max_window_bits=15,
378
client_no_context_takeover=False, # Keep context for better compression
379
server_no_context_takeover=False,
380
)
381
382
# Test different configurations
383
extensions_to_test = [
384
("Low Memory", low_memory_compression),
385
("High Compression", high_compression),
386
]
387
388
for name, ext in extensions_to_test:
389
ws = WSConnection(ConnectionType.CLIENT)
390
print(f"Testing {name} configuration:")
391
print(f" Client window: {ext.client_max_window_bits}")
392
print(f" Server window: {ext.server_max_window_bits}")
393
394
request_data = ws.send(Request(
395
host='example.com',
396
target='/ws',
397
extensions=[ext]
398
))
399
```
400
401
### Extension Error Handling
402
403
```python
404
from wsproto.extensions import PerMessageDeflate
405
406
try:
407
# Invalid window size
408
bad_compression = PerMessageDeflate(client_max_window_bits=16)
409
except ValueError as e:
410
print(f"Configuration error: {e}")
411
412
try:
413
# Another invalid configuration
414
bad_compression = PerMessageDeflate(server_max_window_bits=8)
415
except ValueError as e:
416
print(f"Configuration error: {e}")
417
418
# Valid configuration
419
good_compression = PerMessageDeflate(
420
client_max_window_bits=12,
421
server_max_window_bits=14,
422
)
423
print(f"Valid compression created: {good_compression}")
424
```