XSocket 2.1.0__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.
xsocket/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ from .helpers import VERSION, EventEmitter, XSocketError
2
+ from .data import Data
3
+ from .socket_object import SocketObject
4
+ from .client import Client
5
+ from .server import Server
6
+
7
+ __version__ = VERSION
xsocket/client.py ADDED
@@ -0,0 +1,197 @@
1
+ import asyncio
2
+ from urllib.parse import quote, urlencode
3
+
4
+ from websockets.asyncio.client import connect
5
+ from websockets.exceptions import ConnectionClosed
6
+
7
+ from . import helpers
8
+ from .helpers import Connection, EventEmitter, XSocketError, logger
9
+ from .socket_object import SocketObject
10
+
11
+
12
+ class Client(EventEmitter):
13
+
14
+ def __init__(self, serverUrl, query=None, settings=None):
15
+ super().__init__()
16
+ self.setMaxListeners(0)
17
+
18
+ if isinstance(serverUrl, str):
19
+ self._urlList = [serverUrl]
20
+ elif isinstance(serverUrl, (list, tuple)):
21
+ self._urlList = list(serverUrl)
22
+ else:
23
+ self._urlList = []
24
+ if not self._urlList:
25
+ raise XSocketError('Invalid urlList')
26
+ self._query = dict(query) if isinstance(query, dict) else {}
27
+ self._settings = dict(settings) if isinstance(settings, dict) else {}
28
+ self._ws = None
29
+ self._socketObject = None
30
+ self._isVersionWarned = False
31
+ self._isClose = False
32
+
33
+ self.on('connect', self.__onConnect)
34
+ self._runTask = helpers.promise(self.__run())
35
+
36
+ def getSettings(self):
37
+ return self._settings
38
+
39
+ def getSettingsVal(self, key, default=None):
40
+ return self.getSettings().get(key) or default
41
+
42
+ def getSocketObject(self):
43
+ return self._socketObject or False
44
+
45
+ async def getSocketObjectReady(self):
46
+ if self.getSocketObject():
47
+ return self.getSocketObject()
48
+ future = asyncio.get_running_loop().create_future()
49
+ self.once('connect', lambda socketObject: future.done() or future.set_result(socketObject))
50
+ return await future
51
+
52
+ def send(self, type, data=None, ttl=None):
53
+ return helpers.promise(self.__send(type, data, ttl))
54
+
55
+ async def __send(self, type, data, ttl):
56
+ socketObject = await self.getSocketObjectReady()
57
+ return await socketObject.send(type, data, ttl)
58
+
59
+ def sendReady(self, type, data=None, ttl=None):
60
+ return helpers.promise(self.__sendReady(type, data, ttl))
61
+
62
+ sendReadyResponse = sendReady
63
+
64
+ async def __sendReady(self, type, data, ttl):
65
+ socketObject = await self.getSocketObjectReady()
66
+ return await socketObject.sendReady(type, data, ttl)
67
+
68
+ def disconnect(self, msg=None):
69
+ socketObject = self.getSocketObject()
70
+ if not socketObject:
71
+ return False
72
+ return socketObject.disconnect(msg)
73
+
74
+ async def close(self, msg='end'):
75
+ self._isClose = True
76
+ if not self._ws or not self._ws.closeConnection(msg):
77
+ self._runTask.cancel()
78
+ await asyncio.gather(self._runTask, return_exceptions=True)
79
+ if self._socketObject:
80
+ self._socketObject.destroy(msg)
81
+
82
+ def __getUrl(self):
83
+ url = self._urlList[helpers.getRandInt(0, len(self._urlList) - 1)]
84
+ query = dict(self._query)
85
+ query['xV'] = helpers.VERSION
86
+ if self._socketObject and self._socketObject.getID() and self._socketObject.getSign():
87
+ query['xSOId'] = self._socketObject.getID()
88
+ query['xSOSign'] = self._socketObject.getSign()
89
+ return url + ('&' if '?' in url else '?') + urlencode({str(key): str(val) for key, val in query.items()}, quote_via=quote)
90
+
91
+ async def __run(self):
92
+ while not self._isClose:
93
+ try:
94
+ websocket = await connect(self.__getUrl(), ssl=self.getSettingsVal('ssl'), max_size=helpers.MAX_MESSAGE_SIZE)
95
+ except Exception as e:
96
+ self.emit('ws|error', str(e))
97
+ await asyncio.sleep(1)
98
+ continue
99
+ await self.__handleConnection(websocket)
100
+ if not self._isClose:
101
+ await asyncio.sleep(1)
102
+
103
+ async def __handleConnection(self, websocket):
104
+ ws = Connection(websocket)
105
+ self._ws = ws
106
+ if self._isClose:
107
+ ws.closeConnection('end')
108
+ self.emit('ws|connect', ws)
109
+ msgInc = 0
110
+
111
+ async def pongCheck():
112
+ nonlocal msgInc
113
+ while True:
114
+ await asyncio.sleep(21)
115
+ if not msgInc:
116
+ ws.closeConnection('pingTimeout')
117
+ msgInc = 0
118
+
119
+ pongTask = helpers.promise(pongCheck())
120
+ try:
121
+ async for message in websocket:
122
+ msgInc += 1
123
+ if message == 'ping':
124
+ ws.sendMessage('pong')
125
+ continue
126
+ try:
127
+ self.__onMessage(ws, message)
128
+ except Exception:
129
+ logger.exception('Message handler error')
130
+ except ConnectionClosed:
131
+ pass
132
+ finally:
133
+ pongTask.cancel()
134
+ await websocket.wait_closed()
135
+ ws.stop()
136
+ self._ws = None
137
+ msg = websocket.close_reason or 'close'
138
+ self.emit('ws|disconnect', msg)
139
+ if self._socketObject:
140
+ self._socketObject.emit('ws|disconnect', msg)
141
+
142
+ def __onMessage(self, ws, message):
143
+ body = helpers.decodeMessage(message)
144
+ if not body:
145
+ return
146
+ if body[0] != helpers.MSG_SO_UPDATE:
147
+ if self._socketObject:
148
+ self._socketObject.emit('ws|message', body)
149
+ return
150
+ body = body + [None] * (6 - len(body))
151
+ if body[4] != helpers.VERSION and not self._isVersionWarned:
152
+ self._isVersionWarned = True
153
+ logger.warning('Version mismatch client %s, server %s', helpers.VERSION, body[4] or 'unknown')
154
+ ws.setTimeOffset(body[5] - helpers.getTime() if helpers.isNumber(body[5]) else 0)
155
+ ws.setQuery({
156
+ 'xSOId' : str(body[1]),
157
+ 'xSOSign' : str(body[2]),
158
+ 'configure' : body[3] if isinstance(body[3], dict) else {}
159
+ })
160
+ if self._socketObject:
161
+ query = ws.getQuery()
162
+ if query['xSOId'] == self._socketObject.getID() and query['xSOSign'] == self._socketObject.getSign() and self._socketObject.update(ws):
163
+ return
164
+ self._socketObject.destroy('update')
165
+ self._socketObject = None
166
+ socketObject = self._socketObject = SocketObject()
167
+ socketObject.once('connect', lambda socketObject: self.emit('connect', socketObject))
168
+ socketObject.on('destroy', self.__onSocketObjectDestroy)
169
+ socketObject.on('disconnect', lambda socketObject, msg: self.emit('disconnect', socketObject, msg))
170
+ socketObject.on('reconnect', lambda socketObject: self.emit('reconnect', socketObject))
171
+ socketObject.on('data', lambda socketData: self.emit('data', socketData))
172
+ if not socketObject.update(ws):
173
+ ws.closeConnection('update')
174
+
175
+ def __onSocketObjectDestroy(self, socketObject, msg):
176
+ if self._socketObject is socketObject:
177
+ self._socketObject = None
178
+ self.emit('destroy', socketObject, msg)
179
+
180
+ def __onConnect(self, socketObject):
181
+ loop = asyncio.get_running_loop()
182
+ timerMap = {'destroy' : None}
183
+
184
+ def setDestroyTimeout(action, ttl=0):
185
+ if timerMap['destroy']:
186
+ timerMap['destroy'].cancel()
187
+ timerMap['destroy'] = None
188
+ if action:
189
+ timerMap['destroy'] = loop.call_later(max(ttl, 0) / 1000, socketObject.destroy, 'socketObjectTimeout')
190
+
191
+ def disconnectHandler(socketObject, msg):
192
+ configure = socketObject.getQueryVal('configure', {})
193
+ setDestroyTimeout(True, helpers.toInt(configure.get('socketObjectTimeout') if isinstance(configure, dict) else 0))
194
+
195
+ socketObject.on('reconnect', lambda socketObject: setDestroyTimeout(False))
196
+ socketObject.on('disconnect', disconnectHandler)
197
+ socketObject.once('destroy', lambda socketObject, msg: setDestroyTimeout(False))
xsocket/data.py ADDED
@@ -0,0 +1,169 @@
1
+ import asyncio
2
+
3
+ from . import helpers
4
+ from .helpers import EventEmitter, XSocketError
5
+
6
+
7
+ class Data(EventEmitter):
8
+
9
+ STATUS_NEW = 0
10
+ STATUS_SEND = 1
11
+ STATUS_RESPONSE = 2
12
+ DEFAULT_TTL = 3000
13
+
14
+ def __init__(self, isOutput, dataObject=None, socketObject=None):
15
+ super().__init__()
16
+ self.setMaxListeners(0)
17
+ dataObject = dataObject if isinstance(dataObject, dict) else {}
18
+
19
+ ID = dataObject.get('ID')
20
+ type = dataObject.get('type')
21
+ data = dataObject.get('data')
22
+ ttl = dataObject.get('ttl')
23
+ time = dataObject.get('time')
24
+
25
+ self._isOutput = bool(isOutput)
26
+ self._socketObject = socketObject
27
+ self._ID = ID if isinstance(ID, str) else ''
28
+ self._type = type if isinstance(type, str) else ''
29
+ self._data = data if helpers.isObject(data) else {}
30
+ self._ttl = ttl if helpers.isInt(ttl) and ttl >= 0 else Data.DEFAULT_TTL
31
+ now = socketObject.getTime() if socketObject else helpers.getTime()
32
+ self._time = time if helpers.isNumber(time) and time > 0 else now
33
+ self._resData = {}
34
+ self._error = False
35
+ self._status = Data.STATUS_NEW
36
+ self._destroy = False
37
+ self._ttlTimeout = asyncio.get_running_loop().call_later(
38
+ max(0, min(self._ttl, self._ttl - (now - self._time))) / 1000,
39
+ self.destroy, 'ttl', True
40
+ )
41
+
42
+ def getID(self):
43
+ return str(self._ID)
44
+
45
+ def getType(self):
46
+ return str(self._type)
47
+
48
+ def getTTL(self):
49
+ return self._ttl
50
+
51
+ def getTime(self):
52
+ return self._time
53
+
54
+ def getData(self):
55
+ return self._data
56
+
57
+ def getDataVal(self, key, default=None):
58
+ try:
59
+ return self.getData()[key] or default
60
+ except (KeyError, IndexError, TypeError):
61
+ return default
62
+
63
+ def getResData(self):
64
+ return self._resData
65
+
66
+ def getResDataVal(self, key, default=None):
67
+ try:
68
+ return self.getResData()[key] or default
69
+ except (KeyError, IndexError, TypeError):
70
+ return default
71
+
72
+ def getStatus(self):
73
+ return self._status
74
+
75
+ def getDestroy(self):
76
+ return self._destroy or False
77
+
78
+ def getSocketObject(self):
79
+ return self._socketObject
80
+
81
+ def getError(self):
82
+ return self._error or False
83
+
84
+ def isNew(self):
85
+ return self._status == Data.STATUS_NEW
86
+
87
+ def isSend(self):
88
+ return self._status == Data.STATUS_SEND
89
+
90
+ def isResponse(self):
91
+ return self._status == Data.STATUS_RESPONSE
92
+
93
+ def response(self, data=None, error=None):
94
+ return helpers.promise(self.__response(data, error))
95
+
96
+ async def __response(self, data, error):
97
+ if self._isOutput:
98
+ raise XSocketError('Output data')
99
+ if self._destroy or self.isResponse() or not self._ttl:
100
+ raise XSocketError('destroy')
101
+ self.setResData(data)
102
+ self.setError(error)
103
+ future = asyncio.get_running_loop().create_future()
104
+
105
+ def responseHandler(resData, error):
106
+ if not future.done():
107
+ future.set_result(self)
108
+
109
+ def destroyHandler(msg):
110
+ if not future.done():
111
+ future.set_exception(XSocketError(msg))
112
+
113
+ self.once('response', responseHandler)
114
+ self.once('destroy', destroyHandler)
115
+ try:
116
+ ws = await self._socketObject.getWsReady()
117
+ if ws.sendObject([
118
+ helpers.MSG_SD_UPDATE,
119
+ int(self._ID),
120
+ self.getResData(),
121
+ self.getError()
122
+ ]):
123
+ self.setStatus(Data.STATUS_RESPONSE)
124
+ else:
125
+ self.destroy('Send failed')
126
+ except Exception as e:
127
+ self.destroy(str(e) or 'Send failed')
128
+ return await future
129
+
130
+ def setStatus(self, status):
131
+ if (
132
+ status not in (Data.STATUS_SEND, Data.STATUS_RESPONSE) or
133
+ self._status == Data.STATUS_RESPONSE or
134
+ self._status == status
135
+ ):
136
+ return False
137
+ self._status = status
138
+ self.emit('setStatus', status)
139
+ if status == Data.STATUS_RESPONSE:
140
+ self.emit('response', self.getResData(), self.getError())
141
+ self.destroy('response')
142
+ return True
143
+
144
+ def setError(self, errorMsg):
145
+ if not isinstance(errorMsg, str):
146
+ return False
147
+ self._error = errorMsg
148
+ return True
149
+
150
+ def setResData(self, data):
151
+ if data is None:
152
+ data = {}
153
+ if not helpers.isObject(data):
154
+ return False
155
+ self._resData = data
156
+ return True
157
+
158
+ def destroy(self, msg=None, isSetError=False):
159
+ if self._destroy:
160
+ return False
161
+ self._destroy = msg if isinstance(msg, str) and msg else 'destroy'
162
+ self._ttlTimeout.cancel()
163
+ if isSetError and not self._error:
164
+ self.setError(self._destroy)
165
+ if self._status != Data.STATUS_RESPONSE:
166
+ self.setStatus(Data.STATUS_RESPONSE)
167
+ self.emit('destroy', self._destroy)
168
+ self.clearAllListeners()
169
+ return True
xsocket/helpers.py ADDED
@@ -0,0 +1,250 @@
1
+ import asyncio
2
+ import inspect
3
+ import json
4
+ import logging
5
+ import random
6
+ import struct
7
+ import time
8
+
9
+ from websockets.protocol import State
10
+
11
+ VERSION = '2.1.0'
12
+
13
+ MSG_SO_UPDATE = 0
14
+ MSG_SD_CREATE = 1
15
+ MSG_SD_UPDATE = 2
16
+ MSG_SO_TIME = 3
17
+
18
+ MAX_MESSAGE_SIZE = 100 * 1024 * 1024
19
+
20
+ logger = logging.getLogger('xsocket')
21
+
22
+ _taskSet = set()
23
+
24
+
25
+ class XSocketError(Exception):
26
+ pass
27
+
28
+
29
+ def getTime():
30
+ return int(time.time() * 1000)
31
+
32
+
33
+ def getRandInt(min, max):
34
+ return random.randint(min, max)
35
+
36
+
37
+ def isNumber(value):
38
+ return isinstance(value, (int, float)) and not isinstance(value, bool) and value == value
39
+
40
+
41
+ def isInt(value):
42
+ return isNumber(value) and value % 1 == 0
43
+
44
+
45
+ def isBinary(data):
46
+ return isinstance(data, (bytes, bytearray, memoryview))
47
+
48
+
49
+ def isObject(data):
50
+ return isinstance(data, (dict, list)) or isBinary(data)
51
+
52
+
53
+ def toInt(value, default=0):
54
+ try:
55
+ return int(float(value))
56
+ except (TypeError, ValueError):
57
+ return default
58
+
59
+
60
+ def promise(awaitable):
61
+ task = asyncio.ensure_future(awaitable)
62
+ _taskSet.add(task)
63
+ task.add_done_callback(_onTaskDone)
64
+ return task
65
+
66
+
67
+ def resolve(value):
68
+ future = asyncio.get_running_loop().create_future()
69
+ future.set_result(value)
70
+ return future
71
+
72
+
73
+ def _onTaskDone(task):
74
+ _taskSet.discard(task)
75
+ if not task.cancelled():
76
+ task.exception()
77
+
78
+
79
+ def getMessageDataIndex(msgType):
80
+ return 4 if msgType == MSG_SD_CREATE else 2
81
+
82
+
83
+ def _dumps(body):
84
+ return json.dumps(body, separators=(',', ':'), ensure_ascii=False)
85
+
86
+
87
+ def encodeMessage(body):
88
+ dataIndex = getMessageDataIndex(body[0])
89
+ data = body[dataIndex] if len(body) > dataIndex else None
90
+ if not isBinary(data):
91
+ return _dumps(body)
92
+ body = list(body)
93
+ body[dataIndex] = None
94
+ headerBytes = _dumps(body).encode()
95
+ return struct.pack('>I', len(headerBytes)) + headerBytes + bytes(data)
96
+
97
+
98
+ def decodeMessage(raw):
99
+ try:
100
+ if isinstance(raw, str):
101
+ body = json.loads(raw)
102
+ else:
103
+ headerLength = struct.unpack_from('>I', raw)[0]
104
+ body = json.loads(bytes(raw[4:4 + headerLength]).decode())
105
+ if isinstance(body, list) and body:
106
+ dataIndex = getMessageDataIndex(body[0])
107
+ body.extend([None] * (dataIndex + 1 - len(body)))
108
+ body[dataIndex] = bytes(raw[4 + headerLength:])
109
+ except Exception:
110
+ return False
111
+ return body if isinstance(body, list) and body and isNumber(body[0]) else False
112
+
113
+
114
+ class Listener:
115
+
116
+ def __init__(self, callback, once):
117
+ self.callback = callback
118
+ self.once = once
119
+
120
+
121
+ class EventEmitter:
122
+
123
+ def __init__(self):
124
+ self._listenerMap = {}
125
+ self._maxListeners = 10
126
+
127
+ def on(self, name, callback, once=False):
128
+ if not isinstance(name, str) or not callable(callback):
129
+ return self
130
+ listenerList = self._listenerMap.setdefault(name, [])
131
+ listenerList.append(Listener(callback, once is True))
132
+ if self._maxListeners > 0 and len(listenerList) > self._maxListeners:
133
+ logger.warning('EventEmitter %s, exceeded maxListeners: %s/%s. Use setMaxListeners(n)', name, self._maxListeners, len(listenerList))
134
+ return self
135
+
136
+ def once(self, name, callback):
137
+ return self.on(name, callback, True)
138
+
139
+ def getListenerNameList(self):
140
+ return list(self._listenerMap.keys())
141
+
142
+ def clearAllListeners(self):
143
+ self._listenerMap = {}
144
+ return self._listenerMap
145
+
146
+ def listenerCount(self, name):
147
+ return len(self._listenerMap.get(name, []))
148
+
149
+ def setMaxListeners(self, count):
150
+ if not isNumber(count):
151
+ return False
152
+ self._maxListeners = int(count)
153
+ return True
154
+
155
+ def off(self, name, callback):
156
+ listenerList = self._listenerMap.get(name)
157
+ if not listenerList or not callable(callback):
158
+ return self
159
+ listenerList[:] = [listener for listener in listenerList if listener.callback != callback]
160
+ if not listenerList:
161
+ del self._listenerMap[name]
162
+ return self
163
+
164
+ def offAll(self, name):
165
+ self._listenerMap.pop(name, None)
166
+ return self
167
+
168
+ def emit(self, name, *args):
169
+ listenerList = self._listenerMap.get(name)
170
+ if not listenerList:
171
+ return self
172
+ for listener in list(listenerList):
173
+ currentList = self._listenerMap.get(name)
174
+ if not currentList or not any(item is listener for item in currentList):
175
+ continue
176
+ if listener.once:
177
+ currentList.remove(listener)
178
+ if not currentList:
179
+ del self._listenerMap[name]
180
+ result = listener.callback(*args)
181
+ if inspect.isawaitable(result):
182
+ promise(result)
183
+ return self
184
+
185
+
186
+ class Connection:
187
+
188
+ def __init__(self, websocket, query=None):
189
+ self._websocket = websocket
190
+ self._query = query if isinstance(query, dict) else {}
191
+ self._timeOffset = 0
192
+ self._isClose = False
193
+ self._outputQueue = asyncio.Queue()
194
+ self._writerTask = promise(self.__writer())
195
+
196
+ def getWebsocket(self):
197
+ return self._websocket
198
+
199
+ def getQuery(self):
200
+ return self._query
201
+
202
+ def setQuery(self, query):
203
+ self._query = query if isinstance(query, dict) else {}
204
+
205
+ def getTimeOffset(self):
206
+ return self._timeOffset
207
+
208
+ def setTimeOffset(self, timeOffset):
209
+ self._timeOffset = timeOffset
210
+
211
+ def isConnection(self):
212
+ return not self._isClose and self._websocket.state is State.OPEN
213
+
214
+ def sendMessage(self, msg):
215
+ if not self.isConnection():
216
+ return False
217
+ self._outputQueue.put_nowait(('send', str(msg)))
218
+ return True
219
+
220
+ def sendObject(self, body):
221
+ if not self.isConnection():
222
+ return False
223
+ try:
224
+ message = encodeMessage(body)
225
+ except Exception:
226
+ return False
227
+ self._outputQueue.put_nowait(('send', message))
228
+ return True
229
+
230
+ def closeConnection(self, msg='close'):
231
+ if not self.isConnection():
232
+ return False
233
+ self._isClose = True
234
+ reason = str(msg or 'close').encode()[:123].decode(errors='ignore')
235
+ self._outputQueue.put_nowait(('close', reason))
236
+ return True
237
+
238
+ def stop(self):
239
+ self._isClose = True
240
+ self._writerTask.cancel()
241
+
242
+ async def __writer(self):
243
+ while True:
244
+ action, payload = await self._outputQueue.get()
245
+ try:
246
+ if action == 'close':
247
+ return await self._websocket.close(1000, payload)
248
+ await self._websocket.send(payload)
249
+ except Exception:
250
+ return
xsocket/server.py ADDED
@@ -0,0 +1,227 @@
1
+ import asyncio
2
+ import inspect
3
+ import ssl
4
+ import uuid
5
+ from urllib.parse import parse_qsl
6
+
7
+ from websockets.asyncio.server import serve
8
+ from websockets.exceptions import ConnectionClosed
9
+
10
+ from . import helpers
11
+ from .helpers import Connection, EventEmitter, XSocketError, logger
12
+ from .socket_object import SocketObject
13
+
14
+
15
+ class Server(EventEmitter):
16
+
17
+ def __init__(self, settings=None):
18
+ super().__init__()
19
+ self.setMaxListeners(0)
20
+ self._settings = dict(settings or {})
21
+ self._server = None
22
+ self._isListen = False
23
+ self._useCallbackMap = {}
24
+ self._socketObjectMap = {}
25
+ self._versionWarnMap = {}
26
+ self._sslContext = None
27
+
28
+ options = self.getSettingsVal('https', {})
29
+ if options.get('cert'):
30
+ self._sslContext = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
31
+ self._sslContext.load_cert_chain(options['cert'], options.get('key'))
32
+
33
+ self._socketObjectTimeout = helpers.toInt(self.getSettingsVal('socketObjectTimeout', 0), -1)
34
+ if self._socketObjectTimeout < 0:
35
+ self._socketObjectTimeout = 0
36
+ logger.warning('socketObjectTimeout invalid value, set default is 0')
37
+
38
+ def getSocketObjectList(self):
39
+ return self._socketObjectMap
40
+
41
+ def getSocketObject(self, ID):
42
+ return self._socketObjectMap.get(ID, False)
43
+
44
+ def getPort(self):
45
+ return self.getSettings().get('port') or (443 if self._sslContext else 80)
46
+
47
+ def getSettings(self):
48
+ return self._settings
49
+
50
+ def getSettingsVal(self, key, default=None):
51
+ return self.getSettings().get(key) or default
52
+
53
+ async def useEmit(self, target, data):
54
+ callback = self._useCallbackMap.get(target)
55
+ if not callable(callback):
56
+ raise XSocketError('callbackNotFound')
57
+ future = asyncio.get_running_loop().create_future()
58
+
59
+ def successCallback(*args):
60
+ if not future.done():
61
+ future.set_result(args[0] if args else None)
62
+
63
+ def failedCallback(message=None):
64
+ if not future.done():
65
+ future.set_exception(XSocketError(message if isinstance(message, str) else ''))
66
+
67
+ def onCallbackDone(task):
68
+ if not task.cancelled() and task.exception():
69
+ failedCallback(str(task.exception()))
70
+
71
+ try:
72
+ result = callback(data, successCallback, failedCallback)
73
+ if inspect.isawaitable(result):
74
+ helpers.promise(result).add_done_callback(onCallbackDone)
75
+ except Exception as e:
76
+ failedCallback(str(e))
77
+ try:
78
+ return await asyncio.wait_for(future, 10)
79
+ except asyncio.TimeoutError:
80
+ raise XSocketError('timeout')
81
+
82
+ def use(self, target, callback):
83
+ if not isinstance(target, str) or not target:
84
+ raise XSocketError('Invalid target')
85
+ if not callable(callback):
86
+ raise XSocketError('Invalid callback')
87
+ self._useCallbackMap[target] = callback
88
+ self.emit('use|append', target, callback)
89
+ return self
90
+
91
+ async def listen(self):
92
+ if self._isListen:
93
+ raise XSocketError('repeat')
94
+ if self._isListen is None:
95
+ future = asyncio.get_running_loop().create_future()
96
+ self.once('listen', lambda: future.done() or future.set_result(None))
97
+ self.once('error', lambda e: future.done() or future.set_exception(e))
98
+ return await future
99
+ self._isListen = None
100
+ perMessageDeflate = self.getSettingsVal('perMessageDeflate', False)
101
+ try:
102
+ self._server = await serve(
103
+ self.__onConnection,
104
+ None,
105
+ self.getPort(),
106
+ ssl=self._sslContext,
107
+ compression='deflate' if perMessageDeflate else None,
108
+ max_size=helpers.MAX_MESSAGE_SIZE
109
+ )
110
+ except Exception as e:
111
+ self._isListen = False
112
+ self.emit('error', e)
113
+ raise
114
+ self._isListen = True
115
+ self.emit('listen')
116
+
117
+ async def close(self):
118
+ for socketObject in list(self._socketObjectMap.values()):
119
+ socketObject.destroy('close')
120
+ if self._server:
121
+ self._server.close()
122
+ await self._server.wait_closed()
123
+ self._server = None
124
+ self._isListen = False
125
+
126
+ async def __onConnection(self, websocket):
127
+ request = websocket.request
128
+ query = dict(parse_qsl(request.path.partition('?')[2], keep_blank_values=True))
129
+ ws = Connection(websocket, query)
130
+ remoteAddress = websocket.remote_address
131
+ req = {
132
+ 'path' : request.path,
133
+ 'headers' : {key.lower(): val for key, val in request.headers.raw_items()},
134
+ 'remoteAddress' : remoteAddress[0] if isinstance(remoteAddress, tuple) else remoteAddress
135
+ }
136
+
137
+ clientVersion = str(query.get('xV') or 'unknown')
138
+ if clientVersion != helpers.VERSION and clientVersion not in self._versionWarnMap:
139
+ self._versionWarnMap[clientVersion] = True
140
+ logger.warning('Version mismatch server %s, client %s', helpers.VERSION, clientVersion)
141
+
142
+ socketObject = None
143
+ query['xSOId'] = str(query.get('xSOId') or '')
144
+ query['xSOSign'] = str(query.get('xSOSign') or '')
145
+ if query['xSOId'] and query['xSOSign']:
146
+ s = self.getSocketObject(query['xSOId'])
147
+ if s and s.getSign() == query['xSOSign'] and s.update(ws):
148
+ socketObject = s
149
+ if not socketObject:
150
+ query['xSOId'] = str(uuid.uuid4())
151
+ query['xSOSign'] = str(uuid.uuid4())
152
+ socketObject = SocketObject({'socketObjectTimeout' : self._socketObjectTimeout}, req)
153
+ if not self.getSettingsVal('auth', False) and not callable(self._useCallbackMap.get('auth')):
154
+ self.use('auth', lambda socketObject, successCallback, failedCallback: successCallback())
155
+ if socketObject.update(ws):
156
+ helpers.promise(self.__auth(socketObject))
157
+ else:
158
+ ws.closeConnection('update')
159
+
160
+ msgInc = 0
161
+
162
+ async def ping():
163
+ nonlocal msgInc
164
+ countPing = 0
165
+ while True:
166
+ await asyncio.sleep(5)
167
+ if countPing > 4:
168
+ if not msgInc:
169
+ ws.closeConnection('pingTimeout')
170
+ return
171
+ msgInc = 0
172
+ countPing = 0
173
+ ws.sendMessage('ping')
174
+ countPing += 1
175
+
176
+ pingTask = helpers.promise(ping())
177
+ self.emit('ws|connect', ws)
178
+ try:
179
+ async for message in websocket:
180
+ msgInc += 1
181
+ if message == 'pong' or not socketObject:
182
+ continue
183
+ try:
184
+ body = helpers.decodeMessage(message)
185
+ if body:
186
+ socketObject.emit('ws|message', body)
187
+ except Exception:
188
+ logger.exception('Message handler error')
189
+ except ConnectionClosed:
190
+ pass
191
+ finally:
192
+ pingTask.cancel()
193
+ await websocket.wait_closed()
194
+ ws.stop()
195
+ msg = websocket.close_reason or 'close'
196
+ socketObject.emit('ws|disconnect', msg)
197
+ self.emit('ws|disconnect', msg)
198
+
199
+ async def __auth(self, socketObject):
200
+ try:
201
+ await self.useEmit('auth', socketObject)
202
+ except Exception as e:
203
+ socketObject.destroy(str(e) or 'deniedAuth')
204
+ return
205
+ if socketObject.isDestroy():
206
+ return
207
+ self._socketObjectMap[socketObject.getID()] = socketObject
208
+ self.emit('connect', socketObject)
209
+ loop = asyncio.get_running_loop()
210
+ timerMap = {'destroy' : None}
211
+
212
+ def cancelTimeout(*args):
213
+ if timerMap['destroy']:
214
+ timerMap['destroy'].cancel()
215
+ timerMap['destroy'] = None
216
+
217
+ def disconnectHandler(socketObject, msg):
218
+ cancelTimeout()
219
+ timerMap['destroy'] = loop.call_later(self._socketObjectTimeout / 1000, socketObject.destroy, 'socketObjectTimeout')
220
+ socketObject.once('connect', cancelTimeout)
221
+
222
+ def destroyHandler(socketObject, msg):
223
+ self._socketObjectMap.pop(socketObject.getID(), None)
224
+ cancelTimeout()
225
+
226
+ socketObject.on('disconnect', disconnectHandler)
227
+ socketObject.once('destroy', destroyHandler)
@@ -0,0 +1,255 @@
1
+ import asyncio
2
+
3
+ from . import helpers
4
+ from .data import Data
5
+ from .helpers import EventEmitter, XSocketError
6
+
7
+
8
+ class SocketObject(EventEmitter):
9
+
10
+ def __init__(self, serverConfigure=None, req=None):
11
+ super().__init__()
12
+ self.setMaxListeners(0)
13
+
14
+ self._serverConfigure = serverConfigure
15
+ self._req = req if isinstance(req, dict) else {}
16
+ self._ws = None
17
+ self._ID = ''
18
+ self._sign = ''
19
+ self._destroy = None
20
+ self._isNewConnection = True
21
+ self._socketDataMap = {}
22
+ self._socketDataInc = 0
23
+ self._query = {}
24
+ self._timeOffset = 0
25
+
26
+ self.on('destroy', self.__onDestroy)
27
+ self.on('ws|disconnect', self.__onWsDisconnect)
28
+ self.on('ws|connect', self.__onWsConnect)
29
+ self.on('ws|message', self.__onWsMessage)
30
+
31
+ def getID(self):
32
+ return self._ID or ''
33
+
34
+ def getSign(self):
35
+ return self._sign or ''
36
+
37
+ def getTime(self):
38
+ return helpers.getTime() + self._timeOffset
39
+
40
+ def getTimeOffset(self):
41
+ return self._timeOffset
42
+
43
+ def setTimeOffset(self, timeOffset):
44
+ if not helpers.isNumber(timeOffset):
45
+ return False
46
+ self._timeOffset = round(timeOffset)
47
+ return True
48
+
49
+ def getReq(self):
50
+ return self._req
51
+
52
+ def getQuery(self):
53
+ return self._query
54
+
55
+ def getQueryVal(self, key, default=None):
56
+ return self.getQuery().get(key) or default
57
+
58
+ def getReqHeaders(self):
59
+ return self.getReq().get('headers') or {}
60
+
61
+ def getOrigin(self):
62
+ return self.getReqHeaders().get('origin') or 'unknown'
63
+
64
+ def getIP(self):
65
+ return str(self.getReq().get('remoteAddress') or 'unknown')
66
+
67
+ def getSocketDataList(self):
68
+ return self._socketDataMap
69
+
70
+ def getSocketData(self, ID):
71
+ return self._socketDataMap.get(ID, False)
72
+
73
+ def isConnection(self):
74
+ return self._ws.isConnection() if self._ws else False
75
+
76
+ def getConnection(self):
77
+ return self._ws if self.isConnection() else False
78
+
79
+ def getDestroy(self):
80
+ return self._destroy or False
81
+
82
+ def isDestroy(self):
83
+ return bool(self.getDestroy())
84
+
85
+ def isServer(self):
86
+ return self._serverConfigure is not None
87
+
88
+ async def getWsReady(self):
89
+ if self.getConnection():
90
+ return self.getConnection()
91
+ future = asyncio.get_running_loop().create_future()
92
+
93
+ def destroyHandler(socketObject, msg):
94
+ self.off('connect', connectHandler)
95
+ if not future.done():
96
+ future.set_exception(XSocketError('destroy'))
97
+
98
+ def connectHandler(socketObject):
99
+ self.off('destroy', destroyHandler)
100
+ if future.done():
101
+ return
102
+ if self.getConnection():
103
+ future.set_result(self.getConnection())
104
+ else:
105
+ future.set_exception(XSocketError('Connection failed'))
106
+
107
+ self.once('connect', connectHandler)
108
+ self.once('destroy', destroyHandler)
109
+ return await future
110
+
111
+ def send(self, type, data=None, ttl=None):
112
+ self._socketDataInc += 1
113
+ socketData = Data(True, {
114
+ 'ID' : str(self._socketDataInc),
115
+ 'type' : type,
116
+ 'data' : data,
117
+ 'ttl' : ttl,
118
+ 'time' : self.getTime()
119
+ }, self)
120
+ if socketData.getTTL():
121
+ self._socketDataMap[socketData.getID()] = socketData
122
+ socketData.once('destroy', lambda msg: self._socketDataMap.pop(socketData.getID(), None))
123
+ helpers.promise(self.__sendData(socketData))
124
+ return helpers.resolve(socketData)
125
+
126
+ async def __sendData(self, socketData):
127
+ try:
128
+ ws = await self.getWsReady()
129
+ except Exception as e:
130
+ socketData.destroy(str(e) or 'Send failed')
131
+ return
132
+ if not ws.sendObject([
133
+ helpers.MSG_SD_CREATE,
134
+ int(socketData.getID()),
135
+ socketData.getType(),
136
+ socketData.getTTL(),
137
+ socketData.getData(),
138
+ socketData.getTime()
139
+ ]):
140
+ socketData.destroy('Send failed')
141
+ return
142
+ socketData.setStatus(Data.STATUS_SEND if socketData.getTTL() else Data.STATUS_RESPONSE)
143
+
144
+ def sendReadyResponse(self, type, data=None, ttl=None):
145
+ return helpers.promise(self.__sendReady(type, data, ttl))
146
+
147
+ sendReady = sendReadyResponse
148
+
149
+ async def __sendReady(self, type, data, ttl):
150
+ socketData = await self.send(type, data, ttl)
151
+ if not socketData.isResponse() and not socketData.getDestroy():
152
+ future = asyncio.get_running_loop().create_future()
153
+ socketData.once('response', lambda resData, error: future.done() or future.set_result(None))
154
+ await future
155
+ if socketData.getError():
156
+ raise XSocketError(socketData.getError())
157
+ return socketData
158
+
159
+ def destroy(self, msg=None):
160
+ if self.isDestroy():
161
+ return False
162
+ self._destroy = str(msg or 'destroy')
163
+ self.disconnect(self._destroy)
164
+ self.emit('destroy', self, self._destroy)
165
+ self.clearAllListeners()
166
+ return True
167
+
168
+ def disconnect(self, msg=None):
169
+ ws = self.getConnection()
170
+ if not ws:
171
+ return False
172
+ return ws.closeConnection(msg)
173
+
174
+ def update(self, ws):
175
+ if not isinstance(ws, helpers.Connection):
176
+ return False
177
+ if not ws.isConnection() or self._destroy:
178
+ return False
179
+ query = ws.getQuery()
180
+ if self._ID != query.get('xSOId') or self._sign != query.get('xSOSign'):
181
+ self._ID = query.get('xSOId')
182
+ self._sign = query.get('xSOSign')
183
+ self._query = {key: val for key, val in query.items() if key not in ('xSOId', 'xSOSign', 'xV')}
184
+ self.emit('update', self)
185
+ if self.isServer():
186
+ ws.sendObject([
187
+ helpers.MSG_SO_UPDATE,
188
+ self.getID(),
189
+ self.getSign(),
190
+ self._serverConfigure if isinstance(self._serverConfigure, dict) else {},
191
+ helpers.VERSION,
192
+ helpers.getTime()
193
+ ])
194
+ else:
195
+ self.setTimeOffset(ws.getTimeOffset())
196
+ ws.sendObject([helpers.MSG_SO_TIME, helpers.getTime()])
197
+ self._ws = ws
198
+ self.emit('ws|connect', ws)
199
+ return True
200
+
201
+ def __onDestroy(self, socketObject, msg):
202
+ for socketData in list(self._socketDataMap.values()):
203
+ socketData.destroy('SocketObject|destroy')
204
+
205
+ def __onWsDisconnect(self, msg):
206
+ msg = msg if isinstance(msg, str) else 'unknown'
207
+ self.emit('disconnect', self, msg)
208
+ if msg == 'end':
209
+ self.destroy('end')
210
+
211
+ def __onWsConnect(self, ws):
212
+ self.emit('connect', self)
213
+ if not self._isNewConnection:
214
+ self.emit('reconnect', self)
215
+ else:
216
+ self._isNewConnection = False
217
+
218
+ def __onWsMessage(self, body):
219
+ body = list(body) + [None] * (6 - len(body))
220
+ if body[0] == helpers.MSG_SO_TIME:
221
+ return self.__onWsTime(body)
222
+ ID = body[1]
223
+ if not helpers.isInt(ID) or ID < 1:
224
+ return
225
+ ID = int(ID)
226
+ if body[0] == helpers.MSG_SD_CREATE:
227
+ if not isinstance(body[2], str) or not body[2] or not (body[4] is None or helpers.isObject(body[4])):
228
+ return
229
+ socketData = Data(False, {
230
+ 'ID' : str(ID),
231
+ 'type' : body[2],
232
+ 'ttl' : body[3],
233
+ 'data' : body[4],
234
+ 'time' : body[5]
235
+ }, self)
236
+ self.emit('data', socketData)
237
+ self.emit('data|' + socketData.getType(), socketData)
238
+ if not socketData.getTTL():
239
+ socketData.setStatus(Data.STATUS_RESPONSE)
240
+ elif body[0] == helpers.MSG_SD_UPDATE:
241
+ socketData = self.getSocketData(str(ID))
242
+ if socketData:
243
+ socketData.setResData(body[2])
244
+ socketData.setError(body[3] or False)
245
+ socketData.setStatus(Data.STATUS_RESPONSE)
246
+
247
+ def __onWsTime(self, body):
248
+ ws = self.getConnection()
249
+ if not ws or not helpers.isNumber(body[1]):
250
+ return
251
+ if self.isServer():
252
+ ws.sendObject([helpers.MSG_SO_TIME, body[1], helpers.getTime()])
253
+ elif helpers.isNumber(body[2]):
254
+ now = helpers.getTime()
255
+ self.setTimeOffset(body[2] + (now - body[1]) / 2 - now)
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: xsocket
3
+ Version: 2.1.0
4
+ Summary: WebSocket client and server for Python (asyncio), compatible with xSocket for Node.js and the browser
5
+ Author: pr!de
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/pride-oO/xsocket
8
+ Keywords: socket,ws,wss,websocket,message,client,server,asyncio
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: websockets>=13
13
+ Dynamic: license-file
14
+
15
+ rfr# xSocket for Python
16
+
17
+ WebSocket client and server for Python (asyncio). Same protocol as [xSocket](https://github.com/pride-oO/xsocket) for Node.js and the browser: a Python server works with JS clients, a Python client works with a Node.js server.
18
+
19
+ - Request-response with TTL (`sendReady`), clock synchronization with the server
20
+ - Automatic reconnect and session recovery within `socketObjectTimeout`
21
+ - Auth hook on the server (`use('auth')`)
22
+ - `ws://` or `wss://` depending on certificate, optional `perMessageDeflate` compression
23
+ - Binary messages: `bytes`, `bytearray` or `memoryview` as data, received as `bytes`
24
+ - Python 3.9+, depends on [websockets](https://pypi.org/project/websockets/)
25
+
26
+ ```
27
+ pip install xsocket
28
+ ```
29
+
30
+ The API mirrors the JS version. Methods that return a `Promise` in JS (`send`, `sendReady`, `response`) return an awaitable task here: the message is sent even without `await`. Event handlers can be regular functions or coroutines and receive the same arguments as in JS. `Client` and `Server` must be created inside a running event loop.
31
+
32
+ ## Server:
33
+ ```python
34
+ import asyncio
35
+ import xsocket
36
+
37
+ async def main():
38
+ xServer = xsocket.Server({
39
+ 'port' : 8752, # (Not necessary) Port listen (Default: 443 with certificate, 80 without)
40
+ 'socketObjectTimeout' : 30000, # (Not necessary) Allows the user to reconnect within 30 seconds (Default: 0)
41
+ 'perMessageDeflate' : True, # (Not necessary) Compress messages (Default: False)
42
+ 'https' : { # (Not necessary) Without certificate server uses ws:// instead of wss://
43
+ 'cert' : '<path>/cert.pem',
44
+ 'key' : '<path>/private.key'
45
+ }
46
+ })
47
+
48
+ # (Not necessary) Auth, by default all connections are allowed. Can be async
49
+ def auth(xSocketObject, success, failed):
50
+ if xSocketObject.getQueryVal('name'):
51
+ return success()
52
+ failed('Access denied')
53
+
54
+ xServer.use('auth', auth)
55
+
56
+ def onConnect(xSocketObject):
57
+ print('Connect event:', xSocketObject.getID(), xSocketObject.getIP(), xSocketObject.getOrigin(), xSocketObject.getQuery(), xSocketObject.getReqHeaders())
58
+
59
+ async def onData(xSocketData):
60
+ print('Data event', xSocketData.getID(), xSocketData.getType(), xSocketData.getData())
61
+ await asyncio.sleep(1)
62
+ try:
63
+ await xSocketData.response({'welcome' : 'Hello ' + xSocketObject.getQueryVal('name')})
64
+ # OR
65
+ # await xSocketData.response({}, 'Permision denied')
66
+ except xsocket.XSocketError as e:
67
+ print('Response send failed', xSocketData.getID(), e)
68
+
69
+ xSocketObject.on('data', onData)
70
+ xSocketObject.on('disconnect', lambda xSocketObject, msg: print('Disconnect event:', xSocketObject.getID(), msg))
71
+ xSocketObject.on('reconnect', lambda xSocketObject: print('Reconnect event:', xSocketObject.getID()))
72
+ xSocketObject.on('destroy', lambda xSocketObject, msg: print('Destroy event:', xSocketObject.getID(), msg))
73
+
74
+ xServer.on('connect', onConnect)
75
+ xServer.on('listen', lambda: print('Listen event', xServer.getPort()))
76
+
77
+ await xServer.listen()
78
+ await asyncio.Future()
79
+
80
+ asyncio.run(main())
81
+ ```
82
+
83
+ ## Client:
84
+ ```python
85
+ import asyncio
86
+ import xsocket
87
+
88
+ async def main():
89
+ xClient = xsocket.Client(['wss://server_domain.com:8752'], {'name' : 'Bob'}) # ws:// if server without certificate
90
+ # Self-signed certificate: xsocket.Client(url, query, {'ssl' : ssl.SSLContext})
91
+
92
+ xClient.on('reconnect', lambda xSocketObject: print('reconnect event', xSocketObject.getID()))
93
+ xClient.on('disconnect', lambda xSocketObject, msg: print('disconnect', xSocketObject.getID(), msg))
94
+ xClient.on('destroy', lambda xSocketObject, msg: print('destroy', xSocketObject.getID(), msg))
95
+
96
+ # Messages from server
97
+ xClient.on('data', lambda xSocketData: xSocketData.response({'ok' : True}))
98
+
99
+ xSocketObject = await xClient.getSocketObjectReady()
100
+ print('connect event:', xSocketObject.getID())
101
+
102
+ try:
103
+ xSocketData = await xClient.sendReady(
104
+ 'helloServer', # Name message
105
+ {'message' : 'Hello server'}, # Data message
106
+ 2000 # TTL ready response (Default: 3s)
107
+ )
108
+ print('Server responsed #' + xSocketData.getID(), xSocketData.getType(), xSocketData.getResData())
109
+ except xsocket.XSocketError as e:
110
+ print('Server responsed error', e)
111
+
112
+ # Binary message, response can be binary too
113
+ # xSocketData = await xClient.sendReady('file', b'\x01\x02\x03')
114
+
115
+ # Without waiting for response
116
+ # await xClient.send('log', {'message' : 'Hello'}, 0)
117
+
118
+ # Close connection and destroy session on server
119
+ await xClient.close()
120
+
121
+ asyncio.run(main())
122
+ ```
@@ -0,0 +1,11 @@
1
+ xsocket/__init__.py,sha256=UMeD4__Z7tKEqY7w3f6HVpQC7ECgKUWdVLaX-70G0Ko,197
2
+ xsocket/client.py,sha256=dLbvVwSD9QKlaDUel7ujbgp0MHCBze5JpoZC7ld7W_w,7859
3
+ xsocket/data.py,sha256=WBJfCWR5x6UmLptufXmw1aTJ5wUsy1JG7qHDW4mhj4o,5225
4
+ xsocket/helpers.py,sha256=pPQ5CC7BArZfnpJK2f3R6wQWon4FIp1cTiKJOqEStd0,6751
5
+ xsocket/server.py,sha256=2_mATDSgvCvrjByFU2oeQSnpeZcmyoY2Q5OZamv7oKk,8625
6
+ xsocket/socket_object.py,sha256=9MKLdRjrvWQM2SjagaHW8UTFFL7x9EmObJybJ13qgJA,8603
7
+ xsocket-2.1.0.dist-info/licenses/LICENSE,sha256=9ynRS34b-K3KDcw4uTuSnRTVP81dtWw9HJstqnqmk5Y,1169
8
+ xsocket-2.1.0.dist-info/METADATA,sha256=93DmkMTiQivn4NnckvbrRQ1fSavZfZtq6sVaSBpI2dU,5191
9
+ xsocket-2.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
10
+ xsocket-2.1.0.dist-info/top_level.txt,sha256=j4AoRIuXoYD9KDZmDGVT74dUZ704pp5EyH1InIw_q2M,8
11
+ xsocket-2.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,24 @@
1
+ # License
2
+
3
+ [The MIT License (MIT)](http://opensource.org/licenses/MIT)
4
+
5
+ Copyright (c) 2009-2013 Jeff Mott
6
+ Copyright (c) 2013-2016 Evan Vosberg
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in
16
+ all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
+ THE SOFTWARE.
@@ -0,0 +1 @@
1
+ xsocket