wslink 1.12.4__py3-none-any.whl → 2.0.1__py3-none-any.whl

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.
wslink/chunking.py ADDED
@@ -0,0 +1,214 @@
1
+ import sys
2
+ import secrets
3
+ import msgpack
4
+ from typing import Dict, Tuple, Union
5
+ if sys.version_info >= (3, 8):
6
+ from typing import TypedDict # pylint: disable=no-name-in-module
7
+ else:
8
+ from typing_extensions import TypedDict
9
+
10
+ UINT32_LENGTH = 4
11
+ ID_LOCATION = 0
12
+ ID_LENGTH = UINT32_LENGTH
13
+ MESSAGE_OFFSET_LOCATION = ID_LOCATION + ID_LENGTH
14
+ MESSAGE_OFFSET_LENGTH = UINT32_LENGTH
15
+ MESSAGE_SIZE_LOCATION = MESSAGE_OFFSET_LOCATION + MESSAGE_OFFSET_LENGTH
16
+ MESSAGE_SIZE_LENGTH = UINT32_LENGTH
17
+
18
+ HEADER_LENGTH = ID_LENGTH + MESSAGE_OFFSET_LENGTH + MESSAGE_SIZE_LENGTH
19
+
20
+
21
+ def _encode_header(id: bytes, offset: int, size: int) -> bytes:
22
+ return (
23
+ id
24
+ + offset.to_bytes(MESSAGE_OFFSET_LENGTH, "little", signed=False)
25
+ + size.to_bytes(MESSAGE_SIZE_LENGTH, "little", signed=False)
26
+ )
27
+
28
+
29
+ def _decode_header(header: bytes) -> Tuple[bytes, int, int]:
30
+ id = header[ID_LOCATION:ID_LENGTH]
31
+ offset = int.from_bytes(
32
+ header[
33
+ MESSAGE_OFFSET_LOCATION : MESSAGE_OFFSET_LOCATION + MESSAGE_OFFSET_LENGTH
34
+ ],
35
+ "little",
36
+ signed=False,
37
+ )
38
+ size = int.from_bytes(
39
+ header[MESSAGE_SIZE_LOCATION : MESSAGE_SIZE_LOCATION + MESSAGE_SIZE_LENGTH],
40
+ "little",
41
+ signed=False,
42
+ )
43
+ return id, offset, size
44
+
45
+
46
+ def generate_chunks(message: bytes, max_size: int):
47
+ total_size = len(message)
48
+
49
+ if max_size == 0:
50
+ max_content_size = total_size
51
+ else:
52
+ max_content_size = max(max_size - HEADER_LENGTH, 1)
53
+
54
+ id = secrets.token_bytes(ID_LENGTH)
55
+
56
+ offset = 0
57
+
58
+ while offset < total_size:
59
+ header = _encode_header(id, offset, total_size)
60
+ chunk_content = message[offset : offset + max_content_size]
61
+
62
+ yield header + chunk_content
63
+
64
+ offset += max_content_size
65
+
66
+ return
67
+
68
+
69
+ class PendingMessage(TypedDict):
70
+ received_size: int
71
+ content: bytearray
72
+
73
+
74
+ # This un-chunker is vulnerable to DOS.
75
+ # If it receives a message with a header claiming a large incoming message
76
+ # it will allocate the memory blindly even without actually receiving the content
77
+ # Chunks for a given message can come in any order
78
+ # Chunks across messages can be interleaved.
79
+ class UnChunker:
80
+ pending_messages: Dict[bytes, PendingMessage]
81
+ max_message_size: int
82
+
83
+ def __init__(self):
84
+ self.pending_messages = {}
85
+ self.max_message_size = 512
86
+
87
+ def set_max_message_size(self, size):
88
+ self.max_message_size = size
89
+
90
+ def release_pending_messages(self):
91
+ self.pending_messages = {}
92
+
93
+ def process_chunk(self, chunk: bytes) -> Union[bytes, None]:
94
+ header, chunk_content = chunk[:HEADER_LENGTH], chunk[HEADER_LENGTH:]
95
+ id, offset, total_size = _decode_header(header)
96
+
97
+ pending_message = self.pending_messages.get(id, None)
98
+
99
+ if pending_message is None:
100
+ if total_size > self.max_message_size:
101
+ raise ValueError(
102
+ f"""Total size for message {id} exceeds the allocation limit allowed.
103
+ Maximum size = {self.max_message_size},
104
+ Received size = {total_size}."""
105
+ )
106
+
107
+ pending_message = PendingMessage(
108
+ received_size=0, content=bytearray(total_size)
109
+ )
110
+ self.pending_messages[id] = pending_message
111
+
112
+ # This should never happen, but still check it
113
+ if total_size != len(pending_message["content"]):
114
+ del self.pending_messages[id]
115
+ raise ValueError(
116
+ f"Total size in chunk header for message {id} does not match total size declared by previous chunk."
117
+ )
118
+
119
+ content_size = len(chunk_content)
120
+ content_view = memoryview(pending_message["content"])
121
+ content_view[offset : offset + content_size] = chunk_content
122
+ pending_message["received_size"] += content_size
123
+
124
+ if pending_message["received_size"] >= total_size:
125
+ full_message = pending_message["content"]
126
+ del self.pending_messages[id]
127
+ return msgpack.unpackb(bytes(full_message))
128
+
129
+ return None
130
+
131
+
132
+ class StreamPendingMessage(TypedDict):
133
+ received_size: int
134
+ total_size: int
135
+ unpacker: msgpack.Unpacker
136
+
137
+
138
+ # This un-chunker is more memory efficient
139
+ # (each chunk is passed immediately to msgpack)
140
+ # and it will only allocate memory when it receives content.
141
+ # Chunks for a given message are expected to come sequentially
142
+ # Chunks across messages can be interleaved.
143
+ class StreamUnChunker:
144
+ pending_messages: Dict[bytes, StreamPendingMessage]
145
+
146
+ def __init__(self):
147
+ self.pending_messages = {}
148
+
149
+ def set_max_message_size(self, _size):
150
+ pass
151
+
152
+ def release_pending_messages(self):
153
+ self.pending_messages = {}
154
+
155
+ def process_chunk(self, chunk: bytes) -> Union[bytes, None]:
156
+ header, chunk_content = chunk[:HEADER_LENGTH], chunk[HEADER_LENGTH:]
157
+ id, offset, total_size = _decode_header(header)
158
+
159
+ pending_message = self.pending_messages.get(id, None)
160
+
161
+ if pending_message is None:
162
+ pending_message = StreamPendingMessage(
163
+ received_size=0,
164
+ total_size=total_size,
165
+ unpacker=msgpack.Unpacker(max_buffer_size=total_size),
166
+ )
167
+ self.pending_messages[id] = pending_message
168
+
169
+ # This should never happen, but still check it
170
+ if offset != pending_message["received_size"]:
171
+ del self.pending_messages[id]
172
+ raise ValueError(
173
+ f"""Received an unexpected chunk for message {id}.
174
+ Expected offset = {pending_message['received_size']},
175
+ Received offset = {offset}."""
176
+ )
177
+
178
+ # This should never happen, but still check it
179
+ if total_size != pending_message["total_size"]:
180
+ del self.pending_messages[id]
181
+ raise ValueError(
182
+ f"""Received an unexpected total size in chunk header for message {id}.
183
+ Expected size = {pending_message['total_size']},
184
+ Received size = {total_size}."""
185
+ )
186
+
187
+ content_size = len(chunk_content)
188
+ pending_message["received_size"] += content_size
189
+
190
+ unpacker = pending_message["unpacker"]
191
+ unpacker.feed(chunk_content)
192
+
193
+ full_message = None
194
+
195
+ try:
196
+ full_message = unpacker.unpack()
197
+ except msgpack.OutOfData:
198
+ pass # message is incomplete, keep ingesting chunks
199
+
200
+ if full_message is not None:
201
+ del self.pending_messages[id]
202
+
203
+ if pending_message["received_size"] < total_size:
204
+ # In principle feeding a stream to the unpacker could yield multiple outputs
205
+ # for example unpacker.feed(b'0123') would yield b'0', b'1', ect
206
+ # or concatenated packed payloads would yield two or more unpacked objects
207
+ # but in our use case we expect a full message to be mapped to a single object
208
+ raise ValueError(
209
+ f"""Received a parsable payload shorter than expected for message {id}.
210
+ Expected size = {total_size},
211
+ Received size = {pending_message['received_size']}."""
212
+ )
213
+
214
+ return full_message
wslink/protocol.py CHANGED
@@ -1,13 +1,14 @@
1
1
  import asyncio
2
2
  import copy
3
3
  import inspect
4
- import json
5
4
  import logging
6
- import re
5
+ import msgpack
6
+ import os
7
7
  import traceback
8
8
 
9
9
  from wslink import schedule_coroutine
10
10
  from wslink.publish import PublishManager
11
+ from wslink.chunking import generate_chunks, UnChunker
11
12
 
12
13
  # from http://www.jsonrpc.org/specification, section 5.1
13
14
  METHOD_NOT_FOUND = -32601
@@ -17,6 +18,9 @@ RESULT_SERIALIZE_ERROR = -32002
17
18
  # used in client JS code:
18
19
  CLIENT_ERROR = -32099
19
20
 
21
+ # 4MB is the default inside aiohttp
22
+ MAX_MSG_SIZE = int(os.environ.get("WSLINK_MAX_MSG_SIZE", 4194304))
23
+
20
24
  logger = logging.getLogger(__name__)
21
25
 
22
26
 
@@ -147,6 +151,7 @@ class WslinkHandler(object):
147
151
  self.authentified_client_ids = set()
148
152
  self.attachment_atomic = asyncio.Lock()
149
153
  self.pub_manager = PublishManager()
154
+ self.unchunkers = {}
150
155
 
151
156
  # Build the rpc method dictionary, assuming we were given a serverprotocol
152
157
  if self.getServerProtocol():
@@ -183,6 +188,8 @@ class WslinkHandler(object):
183
188
  return "reverse_connection_client_id"
184
189
 
185
190
  async def onConnect(self, request, client_id):
191
+ self.unchunkers[client_id] = UnChunker()
192
+
186
193
  if not self.serverProtocol:
187
194
  return
188
195
  if hasattr(self.serverProtocol, "onConnect"):
@@ -192,6 +199,8 @@ class WslinkHandler(object):
192
199
  linkProtocol.onConnect(request, client_id)
193
200
 
194
201
  async def onClose(self, client_id):
202
+ del self.unchunkers[client_id]
203
+
195
204
  if not self.serverProtocol:
196
205
  return
197
206
  if hasattr(self.serverProtocol, "onClose"):
@@ -212,9 +221,16 @@ class WslinkHandler(object):
212
221
  and await self.validateToken(args[0]["secret"], client_id)
213
222
  ):
214
223
  self.authentified_client_ids.add(client_id)
224
+ # Once a client is authenticated let the unchunker allocate memory unrestricted
225
+ self.unchunkers[client_id].set_max_message_size(
226
+ 4 * 1024 * 1024 * 1024
227
+ ) # 4GB
215
228
  await self.sendWrappedMessage(
216
229
  rpcid,
217
- {"clientID": "c{0}".format(client_id)},
230
+ {
231
+ "clientID": "c{0}".format(client_id),
232
+ "maxMsgSize": MAX_MSG_SIZE,
233
+ },
218
234
  client_id=client_id,
219
235
  )
220
236
  else:
@@ -235,34 +251,16 @@ class WslinkHandler(object):
235
251
  return False
236
252
 
237
253
  async def onMessage(self, is_binary, msg, client_id):
238
- payload = msg.data
239
-
240
- if is_binary:
241
- if self.isClientAuthenticated(client_id):
242
- # assume all binary messages are attachments
243
- try:
244
- key = self.attachmentsRecvQueue.pop(0)
245
- self.attachmentsReceived[key] = payload
246
- except:
247
- pass
248
- return
254
+ if not is_binary:
255
+ return
249
256
 
250
- # handles issue https://bugs.python.org/issue10976
251
- # `payload` is type bytes in Python 3. Unfortunately, json.loads
252
- # doesn't support taking bytes until Python 3.6.
253
- if type(payload) is bytes:
254
- payload = payload.decode("utf-8")
257
+ full_message = self.unchunkers[client_id].process_chunk(msg.data)
258
+ if full_message is not None:
259
+ await self.onCompleteMessage(full_message, client_id)
255
260
 
256
- rpc = json.loads(payload)
261
+ async def onCompleteMessage(self, rpc, client_id):
257
262
  logger.debug("wslink incoming msg %s", self.payloadWithSecretStripped(rpc))
258
263
  if "id" not in rpc:
259
- # should be a binary attachment header
260
- if rpc.get("method") == "wslink.binary.attachment":
261
- keys = rpc.get("args", [])
262
- if isinstance(keys, list):
263
- for k in keys:
264
- # wait for an attachment by it's order
265
- self.attachmentsRecvQueue.append(k)
266
264
  return
267
265
 
268
266
  # TODO validate
@@ -303,73 +301,37 @@ class WslinkHandler(object):
303
301
  return
304
302
 
305
303
  obj, func = self.functionMap[methodName]
304
+ args.insert(0, obj)
305
+
306
306
  try:
307
- # get any attachments
308
- def findAttachments(o):
309
- if (
310
- isinstance(o, str)
311
- and re.match(r"^wslink_bin\d+$", o)
312
- and o in self.attachmentsReceived
313
- ):
314
- attachment = self.attachmentsReceived[o]
315
- del self.attachmentsReceived[o]
316
- return attachment
317
- elif isinstance(o, list):
318
- for i, v in enumerate(o):
319
- o[i] = findAttachments(v)
320
- elif isinstance(o, dict):
321
- for k in o:
322
- o[k] = findAttachments(o[k])
323
- return o
324
-
325
- args = findAttachments(args)
326
- kwargs = findAttachments(kwargs)
327
-
328
- args.insert(0, obj)
329
-
330
- try:
331
- self.web_app.last_active_client_id = client_id
332
- results = func(*args, **kwargs)
333
- if inspect.isawaitable(results):
334
- results = await results
335
-
336
- if self.connections[client_id].closed:
337
- # Connection was closed during RPC call.
338
- return
339
-
340
- await self.sendWrappedMessage(
341
- rpcid, results, method=methodName, client_id=client_id
342
- )
343
- except Exception as e_inst:
344
- captured_trace = traceback.format_exc()
345
- logger.error("Exception raised")
346
- logger.error(repr(e_inst))
347
- logger.error(captured_trace)
348
- await self.sendWrappedError(
349
- rpcid,
350
- EXCEPTION_ERROR,
351
- "Exception raised",
352
- {
353
- "method": methodName,
354
- "exception": repr(e_inst),
355
- "trace": captured_trace,
356
- },
357
- client_id=client_id,
358
- )
307
+ self.web_app.last_active_client_id = client_id
308
+ results = func(*args, **kwargs)
309
+ if inspect.isawaitable(results):
310
+ results = await results
359
311
 
360
- except Exception as e:
312
+ if self.connections[client_id].closed:
313
+ # Connection was closed during RPC call.
314
+ return
315
+
316
+ await self.sendWrappedMessage(
317
+ rpcid, results, method=methodName, client_id=client_id
318
+ )
319
+ except Exception as e_inst:
320
+ captured_trace = traceback.format_exc()
321
+ logger.error("Exception raised")
322
+ logger.error(repr(e_inst))
323
+ logger.error(captured_trace)
361
324
  await self.sendWrappedError(
362
325
  rpcid,
363
326
  EXCEPTION_ERROR,
364
327
  "Exception raised",
365
328
  {
366
329
  "method": methodName,
367
- "exception": repr(e),
368
- "trace": traceback.format_exc(),
330
+ "exception": repr(e_inst),
331
+ "trace": captured_trace,
369
332
  },
370
333
  client_id=client_id,
371
334
  )
372
- return
373
335
 
374
336
  def payloadWithSecretStripped(self, payload):
375
337
  payload = copy.deepcopy(payload)
@@ -428,9 +390,10 @@ class WslinkHandler(object):
428
390
  "id": rpcid,
429
391
  "result": content,
430
392
  }
393
+
431
394
  try:
432
- encMsg = json.dumps(wrapper, ensure_ascii=False)
433
- except TypeError as e:
395
+ packed_wrapper = msgpack.packb(wrapper)
396
+ except Exception:
434
397
  # the content which is not serializable might be arbitrarily large, don't include.
435
398
  # repr(content) would do that...
436
399
  await self.sendWrappedError(
@@ -444,47 +407,14 @@ class WslinkHandler(object):
444
407
 
445
408
  websockets = self.getAuthenticatedWebsockets(client_id, skip_last_active_client)
446
409
 
447
- # Check if any attachments in the map go with this message
448
- attachments = self.pub_manager.getAttachmentMap()
449
- found_keys = []
450
- if attachments:
451
- for key in attachments:
452
- # string match the encoded attachment key
453
- if key in encMsg:
454
- if key not in found_keys:
455
- found_keys.append(key)
456
- # increment for key
457
- self.pub_manager.registerAttachment(key)
458
-
459
- for key in found_keys:
460
- # send header
461
- header = {
462
- "wslink": "1.0",
463
- "method": "wslink.binary.attachment",
464
- "args": [key],
465
- }
466
- json_header = json.dumps(header, ensure_ascii=False)
467
-
468
- # aiohttp can not handle pending ws.send_bytes()
469
- # tried with semaphore but got exception with >1
470
- # https://github.com/aio-libs/aiohttp/issues/2934
471
- async with self.attachment_atomic:
472
- for ws in websockets:
473
- if ws is not None:
474
- # Send binary header
475
- await ws.send_str(json_header)
476
- # Send binary message
477
- await ws.send_bytes(attachments[key])
478
-
479
- # decrement for key
480
- self.pub_manager.unregisterAttachment(key)
481
-
482
- for ws in websockets:
483
- if ws is not None:
484
- await ws.send_str(encMsg)
485
-
486
- loop = asyncio.get_event_loop()
487
- loop.call_soon(self.pub_manager.freeAttachments, found_keys)
410
+ # aiohttp can not handle pending ws.send_bytes()
411
+ # tried with semaphore but got exception with >1
412
+ # https://github.com/aio-libs/aiohttp/issues/2934
413
+ async with self.attachment_atomic:
414
+ for chunk in generate_chunks(packed_wrapper, MAX_MSG_SIZE):
415
+ for ws in websockets:
416
+ if ws is not None:
417
+ await ws.send_bytes(chunk)
488
418
 
489
419
  async def sendWrappedError(self, rpcid, code, message, data=None, client_id=None):
490
420
  wrapper = {
@@ -497,15 +427,26 @@ class WslinkHandler(object):
497
427
  }
498
428
  if data:
499
429
  wrapper["error"]["data"] = data
500
- encMsg = json.dumps(wrapper, ensure_ascii=False)
430
+
431
+ try:
432
+ packed_wrapper = msgpack.packb(wrapper)
433
+ except Exception:
434
+ del wrapper["error"]["data"]
435
+ packed_wrapper = msgpack.packb(wrapper)
436
+
501
437
  websockets = (
502
438
  [self.connections[client_id]]
503
439
  if client_id
504
440
  else [self.connections[c] for c in self.connections]
505
441
  )
506
- for ws in websockets:
507
- if ws is not None:
508
- await ws.send_str(encMsg)
442
+ # aiohttp can not handle pending ws.send_bytes()
443
+ # tried with semaphore but got exception with >1
444
+ # https://github.com/aio-libs/aiohttp/issues/2934
445
+ async with self.attachment_atomic:
446
+ for chunk in generate_chunks(packed_wrapper, MAX_MSG_SIZE):
447
+ for ws in websockets:
448
+ if ws is not None:
449
+ await ws.send_bytes(chunk)
509
450
 
510
451
  def publish(self, topic, data, client_id=None, skip_last_active_client=False):
511
452
  client_list = [client_id] if client_id else [c_id for c_id in self.connections]
wslink/publish.py CHANGED
@@ -7,9 +7,6 @@ from . import schedule_coroutine
7
7
  class PublishManager(object):
8
8
  def __init__(self):
9
9
  self.protocols = []
10
- self.attachmentMap = {}
11
- self.attachmentRefCounts = {} # keyed same as attachment map
12
- self.attachmentId = 0
13
10
  self.publishCount = 0
14
11
 
15
12
  def registerProtocol(self, protocol):
@@ -19,38 +16,12 @@ class PublishManager(object):
19
16
  if protocol in self.protocols:
20
17
  self.protocols.remove(protocol)
21
18
 
22
- def getAttachmentMap(self):
23
- return self.attachmentMap
24
-
25
- def clearAttachmentMap(self):
26
- self.attachmentMap.clear()
27
-
28
- def registerAttachment(self, attachKey):
29
- self.attachmentRefCounts[attachKey] += 1
30
-
31
- def unregisterAttachment(self, attachKey):
32
- self.attachmentRefCounts[attachKey] -= 1
33
-
34
- def freeAttachments(self, keys=None):
35
- keys_to_delete = []
36
- keys_to_check = keys if keys is not None else [k for k in self.attachmentMap]
37
-
38
- for key in keys_to_check:
39
- if self.attachmentRefCounts.get(key) == 0:
40
- keys_to_delete.append(key)
41
-
42
- for key in keys_to_delete:
43
- self.attachmentMap.pop(key)
44
- self.attachmentRefCounts.pop(key)
45
-
46
19
  def addAttachment(self, payload):
47
- # print("attachment", self, self.attachmentId)
48
- # use a string flag in place of the binary attachment.
49
- binaryId = "wslink_bin{0}".format(self.attachmentId)
50
- self.attachmentMap[binaryId] = payload
51
- self.attachmentRefCounts[binaryId] = 0
52
- self.attachmentId += 1
53
- return binaryId
20
+ """Deprecated method, keeping it to avoid breaking compatibility
21
+ Now that we use msgpack to pack/unpack messages,
22
+ We can have binary data directly in the object itself,
23
+ without needing to transfer it separately from the rest."""
24
+ return payload
54
25
 
55
26
  def publish(self, topic, data, client_id=None, skip_last_active_client=False):
56
27
  for protocol in self.protocols:
wslink/server.py CHANGED
@@ -18,13 +18,13 @@ ws_server = None
18
18
 
19
19
  # =============================================================================
20
20
  # Setup default arguments to be parsed
21
- # -s, --nosignalhandlers
22
- # -d, --debug
23
- # -i, --host localhost
21
+ # --nosignalhandlers
22
+ # --debug
23
+ # --host localhost
24
24
  # -p, --port 8080
25
- # -t, --timeout 300 (seconds)
26
- # -c, --content '/www' (No content means WebSocket only)
27
- # -a, --authKey vtkweb-secret
25
+ # --timeout 300 (seconds)
26
+ # --content '/www' (No content means WebSocket only)
27
+ # --authKey vtkweb-secret
28
28
  # =============================================================================
29
29
 
30
30
 
@@ -35,16 +35,14 @@ def add_arguments(parser):
35
35
  """
36
36
 
37
37
  parser.add_argument(
38
- "-d", "--debug", help="log debugging messages to stdout", action="store_true"
38
+ "--debug", help="log debugging messages to stdout", action="store_true"
39
39
  )
40
40
  parser.add_argument(
41
- "-s",
42
41
  "--nosignalhandlers",
43
42
  help="Prevent installation of signal handlers so server can be started inside a thread.",
44
43
  action="store_true",
45
44
  )
46
45
  parser.add_argument(
47
- "-i",
48
46
  "--host",
49
47
  type=str,
50
48
  default="localhost",
@@ -58,26 +56,22 @@ def add_arguments(parser):
58
56
  help="port number for the web-server to listen on (default: 8080)",
59
57
  )
60
58
  parser.add_argument(
61
- "-t",
62
59
  "--timeout",
63
60
  type=int,
64
61
  default=300,
65
62
  help="timeout for reaping process on idle in seconds (default: 300s, 0 to disable)",
66
63
  )
67
64
  parser.add_argument(
68
- "-c",
69
65
  "--content",
70
66
  default="",
71
67
  help="root for web-pages to serve (default: none)",
72
68
  )
73
69
  parser.add_argument(
74
- "-a",
75
70
  "--authKey",
76
71
  default="wslink-secret",
77
72
  help="Authentication key for clients to connect to the WebSocket.",
78
73
  )
79
74
  parser.add_argument(
80
- "-ws",
81
75
  "--ws-endpoint",
82
76
  type=str,
83
77
  default="ws",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wslink
3
- Version: 1.12.4
3
+ Version: 2.0.1
4
4
  Summary: Python/JavaScript library for communicating over WebSocket
5
5
  Home-page: https://github.com/kitware/wslink
6
6
  Author: Kitware, Inc.
@@ -22,6 +22,7 @@ Classifier: Programming Language :: Python :: 3.8
22
22
  Classifier: Programming Language :: Python :: 3.9
23
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
24
  Requires-Dist: aiohttp <4
25
+ Requires-Dist: msgpack <2,>=1
25
26
  Provides-Extra: ssl
26
27
  Requires-Dist: cryptography ; extra == 'ssl'
27
28
 
@@ -1,10 +1,11 @@
1
1
  wslink/LICENSE,sha256=I44UH7kDVqxDLnnlOWw_hFL2Fz7RjQ_4vPzZv9NYgTU,1483
2
2
  wslink/__init__.py,sha256=AbEm-sUSoGL-uLpnbK1rSSjHSvyW-bMsGHWie7FgMHw,2708
3
+ wslink/chunking.py,sha256=BZZ0YAlh6PNI8rQe80NfdxU8pAvn_Klxew47AkvUJow,7130
3
4
  wslink/launcher.py,sha256=8VMs3juObLkyGYQFNLjMoo4qFpKIcxWz0kS-af-DKO4,21170
4
- wslink/protocol.py,sha256=lI_xPZl3DsEbqdeyHzvHIvdfK-AlpiUqzc5vN8t6Ak8,18164
5
- wslink/publish.py,sha256=6xLr7tGkvbq5LbXbAvPQ6OplEBbs192WR-uhiTJMPdg,2354
5
+ wslink/protocol.py,sha256=zdf4QthFHpAgEw3hTUyyaOuN76jzHeOJBpvekPbk7aY,15886
6
+ wslink/publish.py,sha256=9G5TXqyGr-LCo_LwHYhzif6lhG2iXDvEBmEgwR8fh1M,1437
6
7
  wslink/relay.py,sha256=E8Lzu2Ay7KbOheN1-ArAZawo8lLqdDgJXOZSBuMknYs,86
7
- wslink/server.py,sha256=FKSJAKHDyfkNVM45-M-y1Zn8hh2TTYto1hTCIJx1pp8,9440
8
+ wslink/server.py,sha256=kS0v17uidq6auF4k2kD4L_FHU6i-OdxvG2tH1KeJOpk,9325
8
9
  wslink/ssl_context.py,sha256=hNOJJCdrStws1Qf6vPvY4vTk9Bf8J5d90W3fS0cRv8o,2290
9
10
  wslink/uri.py,sha256=woCQ4yChUqTMg9IT6YYDtUYeKmCg7OUCEgeBGA-19DY,384
10
11
  wslink/websocket.py,sha256=pBiWqkL8Zn8LuSJ9nv3yA-KjEynbolOQ2gLHtQFJ2Ic,4611
@@ -18,7 +19,7 @@ wslink/backends/jupyter/__init__.py,sha256=Qu65gWsd2xCSsxybnDtEDI5vMjHN-F5jgPZOy
18
19
  wslink/backends/jupyter/core.py,sha256=H73IEEHyom3TsbhkyI5O88bFBbUIDzHVuvqbIF6PAIM,3858
19
20
  wslink/backends/tornado/__init__.py,sha256=Qu65gWsd2xCSsxybnDtEDI5vMjHN-F5jgPZOyNIxnGs,112
20
21
  wslink/backends/tornado/core.py,sha256=tPMkkhWuO_ovkisVim0zcegwZKEAG4IRUdd_O_0a_R0,2157
21
- wslink-1.12.4.dist-info/METADATA,sha256=nd9iR5_IF8-flORecGE1-Gwibs3no1fPwj_1qaLPkdE,3016
22
- wslink-1.12.4.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
23
- wslink-1.12.4.dist-info/top_level.txt,sha256=N0d8eqvhwhfW1p1yPTmvxlbzhjz7ZyhBfysNvaFqpQY,7
24
- wslink-1.12.4.dist-info/RECORD,,
22
+ wslink-2.0.1.dist-info/METADATA,sha256=0qH0MmMTMLg0ABAdOzng9T0tCbUvQF3NDuJs-zpe6NA,3045
23
+ wslink-2.0.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
24
+ wslink-2.0.1.dist-info/top_level.txt,sha256=N0d8eqvhwhfW1p1yPTmvxlbzhjz7ZyhBfysNvaFqpQY,7
25
+ wslink-2.0.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.2)
2
+ Generator: bdist_wheel (0.43.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5