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 +7 -0
- xsocket/client.py +197 -0
- xsocket/data.py +169 -0
- xsocket/helpers.py +250 -0
- xsocket/server.py +227 -0
- xsocket/socket_object.py +255 -0
- xsocket-2.1.0.dist-info/METADATA +122 -0
- xsocket-2.1.0.dist-info/RECORD +11 -0
- xsocket-2.1.0.dist-info/WHEEL +5 -0
- xsocket-2.1.0.dist-info/licenses/LICENSE +24 -0
- xsocket-2.1.0.dist-info/top_level.txt +1 -0
xsocket/__init__.py
ADDED
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)
|
xsocket/socket_object.py
ADDED
|
@@ -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,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
|