django-nativemojo 0.1.10__py3-none-any.whl → 0.1.15__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.
- django_nativemojo-0.1.15.dist-info/METADATA +136 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/RECORD +105 -65
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +531 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- mojo/apps/account/models/group.py +25 -7
- mojo/apps/account/models/member.py +15 -4
- mojo/apps/account/models/user.py +197 -20
- mojo/apps/account/rest/group.py +1 -0
- mojo/apps/account/rest/user.py +6 -2
- mojo/apps/aws/rest/__init__.py +1 -0
- mojo/apps/aws/rest/s3.py +64 -0
- mojo/apps/fileman/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +200 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +204 -58
- mojo/apps/fileman/models/manager.py +161 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- mojo/apps/incident/models/__init__.py +1 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +1 -1
- mojo/apps/incident/reporter.py +3 -1
- mojo/apps/incident/rest/event.py +7 -1
- mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +4 -1
- mojo/apps/metrics/utils.py +2 -2
- mojo/apps/notify/handlers/ses/message.py +1 -1
- mojo/apps/notify/providers/aws.py +2 -2
- mojo/apps/tasks/__init__.py +34 -1
- mojo/apps/tasks/manager.py +200 -45
- mojo/apps/tasks/rest/tasks.py +24 -10
- mojo/apps/tasks/runner.py +283 -18
- mojo/apps/tasks/task.py +99 -0
- mojo/apps/tasks/tq_handlers.py +118 -0
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +7 -2
- mojo/helpers/aws/__init__.py +41 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
- mojo/helpers/dates.py +18 -0
- mojo/helpers/response.py +6 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/logging.py +1 -1
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +261 -46
- mojo/models/secrets.py +13 -4
- mojo/serializers/__init__.py +100 -0
- mojo/serializers/advanced/README.md +363 -0
- mojo/serializers/advanced/__init__.py +247 -0
- mojo/serializers/advanced/formats/__init__.py +28 -0
- mojo/serializers/advanced/formats/csv.py +416 -0
- mojo/serializers/advanced/formats/excel.py +516 -0
- mojo/serializers/advanced/formats/json.py +239 -0
- mojo/serializers/advanced/formats/localizers.py +509 -0
- mojo/serializers/advanced/formats/response.py +485 -0
- mojo/serializers/advanced/serializer.py +568 -0
- mojo/serializers/manager.py +501 -0
- mojo/serializers/optimized.py +618 -0
- mojo/serializers/settings_example.py +322 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- testit/helpers.py +21 -4
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/WHEEL +0 -0
- /mojo/{ws4redis/servers → apps/aws}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → aws/models/__init__.py} +0 -0
- /mojo/apps/fileman/{rest/__init__ → migrations/__init__.py} +0 -0
mojo/ws4redis/connection.py
DELETED
@@ -1,327 +0,0 @@
|
|
1
|
-
import time
|
2
|
-
from objict import objict
|
3
|
-
from django.core.handlers.wsgi import WSGIRequest
|
4
|
-
from django.apps import apps
|
5
|
-
|
6
|
-
from mojo.ws4redis import settings as private_settings
|
7
|
-
from mojo.ws4redis.redis import RedisStore, RedisMessage
|
8
|
-
|
9
|
-
from mojo.apps.account.utils.jwtoken import JWToken
|
10
|
-
from mojo.helpers import request as rhelper
|
11
|
-
from mojo.helpers.logit import get_logger
|
12
|
-
logger = get_logger("async", filename="async.log")
|
13
|
-
|
14
|
-
MODEL_CACHE = dict() # caching of app.Model for faster access
|
15
|
-
|
16
|
-
ALLOW_ANY_FACILITY = not private_settings.WS4REDIS_FACILITIES
|
17
|
-
DISCONNECT_AFTER_NO_CREDS = 30
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
class WebsocketConnection():
|
22
|
-
def __init__(self, server, environ, start_response):
|
23
|
-
self.server = server
|
24
|
-
self.request = WSGIRequest(environ)
|
25
|
-
self.ip = rhelper.get_remote_ip(self.request)
|
26
|
-
self.ua = rhelper.get_user_agent(self.request)
|
27
|
-
self.facility = self.request.path_info.replace(private_settings.WEBSOCKET_URL, '', 1)
|
28
|
-
self.credentials = objict()
|
29
|
-
self.listening_fds = None
|
30
|
-
self.redis = RedisStore(server._redis_connection)
|
31
|
-
self.websocket = server.upgrade_websocket(environ, start_response)
|
32
|
-
self.last_beat = time.time()
|
33
|
-
self.conneted_time = time.time()
|
34
|
-
self.last_msg = None
|
35
|
-
self._heart_beat = private_settings.WS4REDIS_HEARTBEAT
|
36
|
-
self.debug = private_settings.WS4REDIS_LOG_DEBUG
|
37
|
-
|
38
|
-
@property
|
39
|
-
def elapsed_time(self):
|
40
|
-
return time.time() - self.conneted_time
|
41
|
-
|
42
|
-
def refreshFDs(self):
|
43
|
-
if len(self.listening_fds) == 1:
|
44
|
-
sub_sd = self.redis.get_file_descriptor()
|
45
|
-
if sub_sd:
|
46
|
-
self.listening_fds.append(sub_sd)
|
47
|
-
|
48
|
-
def on_auth(self, msg):
|
49
|
-
if msg.kind == "jwt":
|
50
|
-
self.on_auth_jwt(msg)
|
51
|
-
return
|
52
|
-
# check our other auth mechanisms
|
53
|
-
auther = self.getAuthenticator(msg.kind)
|
54
|
-
if auther is None or not hasattr(auther, "authWS4RedisConnection"):
|
55
|
-
logger.error(f"{self.ip} invalid auth", msg)
|
56
|
-
self.sendToWS(msg.channel, dict(error="invalid auth kind", code=500))
|
57
|
-
return
|
58
|
-
|
59
|
-
# logger.info(f"{self.ip} authenticating")
|
60
|
-
self.credentials = auther.authWS4RedisConnection(msg)
|
61
|
-
if self.credentials is None or self.credentials.pk is None:
|
62
|
-
logger.error(f"{self.ip} invalid credentials for", msg, self.credentials)
|
63
|
-
self.sendToWS(msg.channel, dict(error="invalid credentials", code=401))
|
64
|
-
return
|
65
|
-
self.on_authenticated()
|
66
|
-
|
67
|
-
def on_auth_jwt(self, msg):
|
68
|
-
token = JWToken()
|
69
|
-
if token.payload is None:
|
70
|
-
raise Exception("invalid auth token")
|
71
|
-
User = apps.get_model("account", "User")
|
72
|
-
user, error = User.validate_jwt(token)
|
73
|
-
if error is not None:
|
74
|
-
raise Exception(error)
|
75
|
-
self.credentials = objict(kind="user", instance=user, pk=user.pk, uuid=user.username)
|
76
|
-
self.on_authenticated()
|
77
|
-
|
78
|
-
def on_authenticated(self):
|
79
|
-
if self.debug:
|
80
|
-
logger.info(F"authenticated {self.credentials.kind}: {self.credentials.uuid}")
|
81
|
-
if self.credentials.kind == "user":
|
82
|
-
channel_key = self.redis.subscribe("user", self.facility, self.credentials.uuid)
|
83
|
-
self.forwardPending(channel_key)
|
84
|
-
else:
|
85
|
-
channel_key = self.redis.subscribe(self.credentials.kind, self.facility, self.credentials.uuid)
|
86
|
-
self.forwardPending(channel_key)
|
87
|
-
|
88
|
-
self.redis.publishModelOnline(self.credentials.kind, self.credentials.pk, only_one=self.credentials.only_one)
|
89
|
-
self.refreshFDs()
|
90
|
-
if self.credentials and self.credentials.instance:
|
91
|
-
if hasattr(self.credentials.instance, "on_ws_online"):
|
92
|
-
self.credentials.instance.on_ws_online()
|
93
|
-
|
94
|
-
def forwardPending(self, channel_key):
|
95
|
-
msg = self.redis.getPendingMessage(channel_key)
|
96
|
-
if msg:
|
97
|
-
if self.debug:
|
98
|
-
logger.info("pending messages", repr(msg))
|
99
|
-
channel, pk = self.parseChannel(channel_key)
|
100
|
-
self.sendToWS(channel, msg, pk)
|
101
|
-
|
102
|
-
def on_resubscribe(self, msg):
|
103
|
-
for ch in msg.channels:
|
104
|
-
self.redis.subscribe(ch.channel, self.facility, ch.pk)
|
105
|
-
|
106
|
-
def getAppModel(self, app_model):
|
107
|
-
if app_model not in MODEL_CACHE:
|
108
|
-
app_label, model_name = app_model.split('.')
|
109
|
-
MODEL_CACHE[app_model] = apps.get_model(app_label, model_name)
|
110
|
-
return MODEL_CACHE[app_model]
|
111
|
-
|
112
|
-
def getAuthenticator(self, channel):
|
113
|
-
auther_path = private_settings.WS4REDIS_AUTHENTICATORS.get(channel, None)
|
114
|
-
if auther_path is not None:
|
115
|
-
return self.getAppModel(auther_path)
|
116
|
-
return None
|
117
|
-
|
118
|
-
def on_subscribe(self, msg):
|
119
|
-
pks = self.canSubscribeTo(msg)
|
120
|
-
if bool(pks):
|
121
|
-
for pk in pks:
|
122
|
-
channel_key = self.redis.subscribe(msg.channel, self.facility, pk)
|
123
|
-
else:
|
124
|
-
logger.warning("subscribe permission denied", msg)
|
125
|
-
self.sendToWS(msg.channel, dict(error="subscribe permission denied"))
|
126
|
-
|
127
|
-
def canSubscribeTo(self, msg):
|
128
|
-
ch_model = private_settings.WS4REDIS_CHANNELS.get(msg.channel, None)
|
129
|
-
if not ch_model:
|
130
|
-
return None
|
131
|
-
if ch_model == "any":
|
132
|
-
# this allows anything to be sent
|
133
|
-
return [msg.pk]
|
134
|
-
Model = self.getAppModel(ch_model)
|
135
|
-
if not hasattr(Model, "can_ws_subscribe_to"):
|
136
|
-
return None
|
137
|
-
return Model.can_ws_subscribe_to(self.credentials, msg)
|
138
|
-
|
139
|
-
def on_unsubscribe(self, msg):
|
140
|
-
self.redis.unsubscribe(msg.channel, self.facility, msg.pk)
|
141
|
-
|
142
|
-
def on_publish(self, msg):
|
143
|
-
if self.canPublishTo(msg):
|
144
|
-
self.redis.publish(msg.message, channel=msg.channel, pk=msg.pk)
|
145
|
-
else:
|
146
|
-
logger.warning("publish permission denied", msg)
|
147
|
-
self.sendToWS(msg.channel, dict(error="publish permission denied"))
|
148
|
-
|
149
|
-
def canPublishTo(self, msg):
|
150
|
-
ch_model = private_settings.WS4REDIS_CHANNELS.get(msg.channel, None)
|
151
|
-
if not ch_model:
|
152
|
-
# you can publish to any channels you may create
|
153
|
-
return True
|
154
|
-
Model = self.getAppModel(ch_model)
|
155
|
-
if not hasattr(Model, "canPublishTo"):
|
156
|
-
logger.warning("canPublishTo", "no canPublishTo")
|
157
|
-
return False
|
158
|
-
return Model.can_ws_publish_to(self.credentials, msg)
|
159
|
-
|
160
|
-
def on_ws_msg(self, raw_data):
|
161
|
-
if isinstance(raw_data, bytes):
|
162
|
-
raw_data = raw_data.decode()
|
163
|
-
if raw_data == self._heart_beat:
|
164
|
-
# echo the heartbeat
|
165
|
-
self.websocket.send(self._heart_beat)
|
166
|
-
return
|
167
|
-
if self.debug:
|
168
|
-
logger.info("on_ws_msg", repr(raw_data))
|
169
|
-
dmsg = objict.from_json(raw_data, ignore_errors=True)
|
170
|
-
if dmsg.action:
|
171
|
-
# this is a special message that we want to handle directly
|
172
|
-
try:
|
173
|
-
if dmsg.action == "auth":
|
174
|
-
self.on_auth(dmsg)
|
175
|
-
elif dmsg.action == "subscribe":
|
176
|
-
self.on_subscribe(dmsg)
|
177
|
-
elif dmsg.action == "unsubscribe":
|
178
|
-
self.on_unsubscribe(dmsg)
|
179
|
-
elif dmsg.action == "resubscribe":
|
180
|
-
self.on_resubscribe(dmsg)
|
181
|
-
elif dmsg.action == "publish":
|
182
|
-
self.on_publish(dmsg)
|
183
|
-
elif dmsg.action == "save":
|
184
|
-
self.on_save(dmsg)
|
185
|
-
elif dmsg.channel:
|
186
|
-
self.on_channel_msg(dmsg)
|
187
|
-
except Exception as err:
|
188
|
-
self.sendToWS(dmsg.get("channel", "system"), dict(error=str(err)))
|
189
|
-
logger.exception(self.ip)
|
190
|
-
|
191
|
-
def on_save(self, msg):
|
192
|
-
if self.credentials and self.credentials.instance:
|
193
|
-
if hasattr(self.credentials.instance, "on_ws_save"):
|
194
|
-
resp = objict(id=msg.id, name="save")
|
195
|
-
if msg.echo:
|
196
|
-
resp.data = msg.data
|
197
|
-
resp.status = self.credentials.instance.on_ws_save(msg)
|
198
|
-
self.sendToWS(self.credentials.kind, resp)
|
199
|
-
|
200
|
-
def on_channel_msg(self, msg):
|
201
|
-
if not self.canPublishTo(msg):
|
202
|
-
logger.warning("on_channel_msg permission denied", msg, private_settings.WS4REDIS_CHANNELS)
|
203
|
-
self.sendToWS(msg.channel, dict(error="cannot publish to channel"))
|
204
|
-
return None
|
205
|
-
ch_model = private_settings.WS4REDIS_CHANNELS.get(msg.channel, None)
|
206
|
-
if ch_model is None:
|
207
|
-
return None
|
208
|
-
Model = self.getAppModel(ch_model)
|
209
|
-
if not hasattr(Model, "on_ws_message"):
|
210
|
-
logger.warning(f"{msg.channel} does not support on_ws_message")
|
211
|
-
self.sendToWS(msg.channel, dict(error="channel does not support on_ws_message"))
|
212
|
-
return None
|
213
|
-
Model.on_ws_message(self.credentials, msg, self)
|
214
|
-
|
215
|
-
def on_redis_pending(self):
|
216
|
-
sub_resp = self.redis.getSubMessage()
|
217
|
-
if sub_resp:
|
218
|
-
if self.debug:
|
219
|
-
logger.info("incoming redis msg", sub_resp)
|
220
|
-
self.on_redis_msg(sub_resp)
|
221
|
-
|
222
|
-
def sendToWS(self, channel, message, pk=None):
|
223
|
-
msg = objict(channel=channel)
|
224
|
-
if pk is not None:
|
225
|
-
msg.pk = pk
|
226
|
-
if isinstance(message, (str, bytes)):
|
227
|
-
msg.message = objict.from_json(message, ignore_errors=True)
|
228
|
-
if not msg.message:
|
229
|
-
msg.message = message
|
230
|
-
elif msg.message.name == "logout" and msg.message.pk == self.credentials.pk:
|
231
|
-
raise Exception("websocket is being logged out")
|
232
|
-
self.websocket.send(msg.toJSON(as_string=True))
|
233
|
-
|
234
|
-
def on_redis_msg(self, sub_resp):
|
235
|
-
if isinstance(sub_resp, list):
|
236
|
-
if sub_resp[0] == b'subscribe':
|
237
|
-
# this is succesfull subscriptions
|
238
|
-
# notify ws
|
239
|
-
channel, pk = self.parseChannel(sub_resp[1].decode())
|
240
|
-
msg = objict(name="subscribed", channel=channel, status=sub_resp[2])
|
241
|
-
if pk is not None:
|
242
|
-
msg.pk = pk
|
243
|
-
self.websocket.send(msg.toJSON(as_string=True))
|
244
|
-
return
|
245
|
-
elif sub_resp[0] == b'unsubscribe':
|
246
|
-
# this is succesfull subscriptions
|
247
|
-
# notify ws
|
248
|
-
channel, pk = self.parseChannel(sub_resp[1].decode())
|
249
|
-
msg = objict(name="unsubscribed", channel=channel, status=sub_resp[2])
|
250
|
-
if pk is not None:
|
251
|
-
msg.pk = pk
|
252
|
-
self.websocket.send(msg.toJSON(as_string=True))
|
253
|
-
return
|
254
|
-
elif sub_resp[0] == b'message':
|
255
|
-
channel, pk = self.parseChannel(sub_resp[1].decode())
|
256
|
-
self.sendToWS(channel, sub_resp[2].decode(), pk)
|
257
|
-
return
|
258
|
-
sendmsg = RedisMessage(sub_resp)
|
259
|
-
if self.debug:
|
260
|
-
logger.info(sub_resp)
|
261
|
-
if sendmsg:
|
262
|
-
if self.debug:
|
263
|
-
logger.info("pushing to websocket", sendmsg)
|
264
|
-
self.websocket.send(sendmsg)
|
265
|
-
|
266
|
-
def parseChannel(self, channel):
|
267
|
-
fields = channel.split(":")
|
268
|
-
fields.pop(0)
|
269
|
-
fields.pop()
|
270
|
-
if len(fields) == 1:
|
271
|
-
return fields[0], None
|
272
|
-
return fields[0], fields[1]
|
273
|
-
|
274
|
-
def on_ws_pending(self):
|
275
|
-
try:
|
276
|
-
self.last_msg = self.websocket.receive()
|
277
|
-
if bool(self.last_msg):
|
278
|
-
self.on_ws_msg(self.last_msg)
|
279
|
-
except Exception:
|
280
|
-
# logger.exception()
|
281
|
-
logger.error(f"{self.ip}: unable to recv on ws... flushing")
|
282
|
-
self.websocket.flush()
|
283
|
-
|
284
|
-
def checkHeartbeat(self):
|
285
|
-
beat_delta = time.time() - self.last_beat
|
286
|
-
if beat_delta > 30.0:
|
287
|
-
self.last_beat = time.time()
|
288
|
-
if private_settings.WS4REDIS_HEARTBEAT and not self.websocket.closed:
|
289
|
-
# logger.info("send heartbeat")
|
290
|
-
self.websocket.send(private_settings.WS4REDIS_HEARTBEAT)
|
291
|
-
|
292
|
-
def release(self):
|
293
|
-
self.redis.release()
|
294
|
-
self.listening_fds = None
|
295
|
-
if self.websocket:
|
296
|
-
self.websocket.close(code=1001, message='Websocket Closed')
|
297
|
-
if self.credentials and self.credentials.instance:
|
298
|
-
if hasattr(self.credentials.instance, "on_ws_offline"):
|
299
|
-
self.credentials.instance.on_ws_offline()
|
300
|
-
|
301
|
-
def handleComs(self):
|
302
|
-
# check if token is in url
|
303
|
-
if not ALLOW_ANY_FACILITY and self.facility not in private_settings.WS4REDIS_FACILITIES:
|
304
|
-
self.sendToWS("facility", dict(error=f"facility name not allowed: '{self.facility}'. Please check your connection URL."))
|
305
|
-
return
|
306
|
-
|
307
|
-
websocket_fd = self.websocket.get_file_descriptor()
|
308
|
-
self.listening_fds = [websocket_fd]
|
309
|
-
if callable(private_settings.URL_AUTHENTICATOR):
|
310
|
-
private_settings.URL_AUTHENTICATOR(self)
|
311
|
-
|
312
|
-
while self.websocket and not self.websocket.closed:
|
313
|
-
ready = self.server.select(self.listening_fds, [], [], 4.0)[0]
|
314
|
-
if not ready:
|
315
|
-
self.websocket.flush()
|
316
|
-
if not self.credentials:
|
317
|
-
if self.elapsed_time > 8 and self.elapsed_time < 30:
|
318
|
-
logger.error(f"{self.ip} has sent no credentials", self.ua)
|
319
|
-
self.sendToWS("user", dict(error="no credentials received"))
|
320
|
-
elif private_settings.WS4REDIS_NOAUTH_CLOSE and self.elapsed_time > 30:
|
321
|
-
self.release()
|
322
|
-
|
323
|
-
for fd in ready:
|
324
|
-
if fd == websocket_fd:
|
325
|
-
self.on_ws_pending()
|
326
|
-
else:
|
327
|
-
self.on_redis_pending()
|
mojo/ws4redis/exceptions.py
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
#-*- coding: utf-8 -*-
|
2
|
-
from socket import error as socket_error
|
3
|
-
from django.http import BadHeaderError
|
4
|
-
|
5
|
-
|
6
|
-
class WebSocketError(socket_error):
|
7
|
-
"""
|
8
|
-
Raised when an active websocket encounters a problem.
|
9
|
-
"""
|
10
|
-
|
11
|
-
|
12
|
-
class FrameTooLargeException(WebSocketError):
|
13
|
-
"""
|
14
|
-
Raised if a received frame is too large.
|
15
|
-
"""
|
16
|
-
|
17
|
-
|
18
|
-
class HandshakeError(BadHeaderError):
|
19
|
-
"""
|
20
|
-
Raised if an error occurs during protocol handshake.
|
21
|
-
"""
|
22
|
-
|
23
|
-
|
24
|
-
class UpgradeRequiredError(HandshakeError):
|
25
|
-
"""
|
26
|
-
Raised if protocol must be upgraded.
|
27
|
-
"""
|
28
|
-
|
29
|
-
class SSLRequiredError(socket_error):
|
30
|
-
"""
|
31
|
-
Raised if protocol must be upgraded.
|
32
|
-
"""
|
mojo/ws4redis/redis.py
DELETED
@@ -1,183 +0,0 @@
|
|
1
|
-
from redis import ConnectionPool, StrictRedis
|
2
|
-
|
3
|
-
from mojo.ws4redis import settings
|
4
|
-
import time
|
5
|
-
from objict import objict
|
6
|
-
|
7
|
-
from mojo.helpers.logit import get_logger
|
8
|
-
logger = get_logger("async", filename="async.log")
|
9
|
-
|
10
|
-
|
11
|
-
REDIS_CON_POOL = None
|
12
|
-
|
13
|
-
|
14
|
-
def getRedisClient():
|
15
|
-
global REDIS_CON_POOL
|
16
|
-
if REDIS_CON_POOL is None:
|
17
|
-
REDIS_CON_POOL = ConnectionPool(**settings.WS4REDIS_CONNECTION)
|
18
|
-
return StrictRedis(connection_pool=REDIS_CON_POOL)
|
19
|
-
|
20
|
-
|
21
|
-
# Function to check the number of connections in use and available
|
22
|
-
def getPoolStatus():
|
23
|
-
status = objict()
|
24
|
-
if REDIS_CON_POOL is None:
|
25
|
-
return status
|
26
|
-
status.max_size = REDIS_CON_POOL.max_connections
|
27
|
-
status.size = REDIS_CON_POOL._created_connections
|
28
|
-
status.in_use = len(REDIS_CON_POOL._in_use_connections)
|
29
|
-
status.available = len(REDIS_CON_POOL._available_connections)
|
30
|
-
return status
|
31
|
-
|
32
|
-
|
33
|
-
class RedisMessage(bytes):
|
34
|
-
def __new__(cls, value):
|
35
|
-
if isinstance(value, str):
|
36
|
-
if value != settings.WS4REDIS_HEARTBEAT:
|
37
|
-
return bytes(value, 'utf-8')
|
38
|
-
elif isinstance(value, list):
|
39
|
-
if len(value) >= 2 and value[0] == b'message':
|
40
|
-
return value[2]
|
41
|
-
elif isinstance(value, dict):
|
42
|
-
if not hasattr(value, "toJSON"):
|
43
|
-
value = objict(value)
|
44
|
-
return bytes(value.toJSON(as_string=True), 'utf-8')
|
45
|
-
elif isinstance(value, bytes):
|
46
|
-
return value
|
47
|
-
return None
|
48
|
-
|
49
|
-
|
50
|
-
class RedisStore():
|
51
|
-
def __init__(self, connection=None):
|
52
|
-
self.connection = connection
|
53
|
-
if self.connection is None:
|
54
|
-
self.connection = getRedisClient()
|
55
|
-
self.subscriptions = []
|
56
|
-
self.pubsub = None
|
57
|
-
self.online_pk = None
|
58
|
-
self.online_channel = None
|
59
|
-
self.only_one = False # don't count connections, only allows one instance
|
60
|
-
self.expire = settings.WS4REDIS_EXPIRE
|
61
|
-
|
62
|
-
def publish(self, message, channel, facility="events", pk=None, expire=None, prefix=settings.WS4REDIS_PREFIX):
|
63
|
-
if expire is None:
|
64
|
-
expire = self.expire
|
65
|
-
if not isinstance(message, RedisMessage):
|
66
|
-
message = RedisMessage(message)
|
67
|
-
|
68
|
-
if not isinstance(message, bytes):
|
69
|
-
raise ValueError('message is {} but should be bytes'.format(type(message)))
|
70
|
-
|
71
|
-
if isinstance(pk, list):
|
72
|
-
count = 0
|
73
|
-
for spk in pk:
|
74
|
-
count += self.publish(message, channel, facility, pk=spk, expire=expire, prefix=prefix)
|
75
|
-
return count
|
76
|
-
|
77
|
-
channel_key = self.channelToKey(channel, facility, pk, prefix)
|
78
|
-
if settings.WS4REDIS_LOG_DEBUG:
|
79
|
-
logger.info("publishing msg to: {0}".format(channel_key), message)
|
80
|
-
count = self.connection.publish(channel_key, message)
|
81
|
-
return count
|
82
|
-
|
83
|
-
def getSubMessage(self):
|
84
|
-
# get a message pending from subscription
|
85
|
-
if self.pubsub:
|
86
|
-
return self.pubsub.parse_response()
|
87
|
-
return None
|
88
|
-
|
89
|
-
def getPendingMessage(self, channel, facility="events", pk=None, prefix=settings.WS4REDIS_PREFIX):
|
90
|
-
# get a message from the connection channel
|
91
|
-
channel_key = self.channelToKey(channel, facility, pk, prefix)
|
92
|
-
return self.connection.get(channel_key)
|
93
|
-
|
94
|
-
def channelToKey(self, channel, facility="events", pk=None, prefix=settings.WS4REDIS_PREFIX):
|
95
|
-
if not pk:
|
96
|
-
key = F'{prefix}:{channel}:{facility}'
|
97
|
-
else:
|
98
|
-
key = F'{prefix}:{channel}:{pk}:{facility}'
|
99
|
-
return key
|
100
|
-
|
101
|
-
def subscribe(self, channel, facility="events", pk=None, prefix=settings.WS4REDIS_PREFIX):
|
102
|
-
if self.pubsub is None:
|
103
|
-
self.pubsub = self.connection.pubsub()
|
104
|
-
key = self.channelToKey(channel, facility, pk, prefix)
|
105
|
-
if key not in self.subscriptions:
|
106
|
-
if settings.WS4REDIS_LOG_DEBUG:
|
107
|
-
logger.info(F"subscribing to: {key}")
|
108
|
-
self.subscriptions.append(key)
|
109
|
-
self.pubsub.subscribe(key)
|
110
|
-
return key
|
111
|
-
|
112
|
-
def unsubscribe(self, channel, facility, pk=None, prefix=settings.WS4REDIS_PREFIX):
|
113
|
-
key = self.channelToKey(channel, facility, pk, prefix)
|
114
|
-
if key in self.subscriptions:
|
115
|
-
if settings.WS4REDIS_LOG_DEBUG:
|
116
|
-
logger.info(F"unsubscribing to: {key}")
|
117
|
-
self.subscriptions.remove(key)
|
118
|
-
self.pubsub.unsubscribe(key)
|
119
|
-
|
120
|
-
def publishModelOnline(self, name, pk, only_one=False):
|
121
|
-
if self.online_pk is None:
|
122
|
-
self.online_pk = pk
|
123
|
-
self.online_channel = name
|
124
|
-
self.only_one = only_one
|
125
|
-
if self.only_one:
|
126
|
-
self.connection.sadd(F"{name}:online", pk)
|
127
|
-
else:
|
128
|
-
count = self.connection.hincrby(F"{name}:online:connections", pk, 1)
|
129
|
-
if count == 1:
|
130
|
-
self.connection.sadd(F"{name}:online", pk)
|
131
|
-
|
132
|
-
def unpublishModelOnline(self):
|
133
|
-
if self.online_pk:
|
134
|
-
name = self.online_channel
|
135
|
-
pk = self.online_pk
|
136
|
-
self.online_channel = None
|
137
|
-
self.online_pk = None
|
138
|
-
if self.only_one:
|
139
|
-
self.connection.srem(F"{name}:online", pk)
|
140
|
-
else:
|
141
|
-
count = self.connection.hincrby(F"{name}:online:connections", pk, -1)
|
142
|
-
if count == 0:
|
143
|
-
self.connection.srem(F"{name}:online", pk)
|
144
|
-
|
145
|
-
def waitForMessage(self, muid=None, timeout=55):
|
146
|
-
timeout_at = time.time() + timeout
|
147
|
-
while time.time() < timeout_at:
|
148
|
-
message = self.pubsub.get_message()
|
149
|
-
if message is not None and message.get("type") == "message":
|
150
|
-
imsg = objict.from_json(message.get("data"))
|
151
|
-
if muid is not None and imsg.muid == muid:
|
152
|
-
return imsg
|
153
|
-
elif muid is None:
|
154
|
-
return imsg
|
155
|
-
time.sleep(1.0)
|
156
|
-
return None
|
157
|
-
|
158
|
-
def get_file_descriptor(self):
|
159
|
-
"""
|
160
|
-
Returns the file descriptor used for passing to the select call when listening
|
161
|
-
on the message queue.
|
162
|
-
"""
|
163
|
-
if self.pubsub.connection:
|
164
|
-
return self.pubsub.connection._sock.fileno()
|
165
|
-
return None
|
166
|
-
|
167
|
-
def release(self):
|
168
|
-
"""
|
169
|
-
New implementation to free up Redis subscriptions when websockets close. This prevents
|
170
|
-
memory sap when Redis Output Buffer and Output Lists build when websockets are abandoned.
|
171
|
-
"""
|
172
|
-
self.unpublishModelOnline()
|
173
|
-
if self.pubsub and self.pubsub.subscribed:
|
174
|
-
self.pubsub.unsubscribe()
|
175
|
-
self.pubsub.reset()
|
176
|
-
self.connection = None
|
177
|
-
|
178
|
-
def __enter__(self):
|
179
|
-
return self
|
180
|
-
|
181
|
-
def __exit__(self, exc_type, exc_value, traceback):
|
182
|
-
self.release()
|
183
|
-
return False
|
mojo/ws4redis/servers/base.py
DELETED
@@ -1,86 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
import sys
|
3
|
-
from redis import StrictRedis
|
4
|
-
from http import client as http_client
|
5
|
-
from mojo.ws4redis import settings as private_settings
|
6
|
-
from mojo.ws4redis.exceptions import WebSocketError, HandshakeError, UpgradeRequiredError, SSLRequiredError
|
7
|
-
from mojo.ws4redis.connection import WebsocketConnection
|
8
|
-
|
9
|
-
from django.core.exceptions import PermissionDenied
|
10
|
-
from django.utils.encoding import force_str
|
11
|
-
from django import http
|
12
|
-
|
13
|
-
from mojo.helpers.logit import get_logger
|
14
|
-
logger = get_logger("async", filename="async.log")
|
15
|
-
|
16
|
-
|
17
|
-
class WebsocketServerBase(object):
|
18
|
-
def __init__(self, redis_connection=None):
|
19
|
-
"""
|
20
|
-
redis_connection can be overriden by a mock object.
|
21
|
-
"""
|
22
|
-
self._websockets = set() # a list of currently active websockets
|
23
|
-
self._redis_connection = redis_connection
|
24
|
-
if redis_connection is None:
|
25
|
-
self._redis_connection = StrictRedis(**private_settings.WS4REDIS_CONNECTION)
|
26
|
-
|
27
|
-
# clear out all online user connections from redis
|
28
|
-
self._redis_connection.delete("users:online:connections")
|
29
|
-
self._redis_connection.delete("users:online")
|
30
|
-
|
31
|
-
def assure_protocol_requirements(self, environ):
|
32
|
-
if environ.get('REQUEST_METHOD') != 'GET':
|
33
|
-
raise HandshakeError('HTTP method must be a GET')
|
34
|
-
|
35
|
-
if environ.get('SERVER_PROTOCOL') != 'HTTP/1.1':
|
36
|
-
raise HandshakeError('HTTP server protocol must be 1.1')
|
37
|
-
|
38
|
-
if environ.get('HTTP_UPGRADE', '').lower() != 'websocket':
|
39
|
-
raise HandshakeError('Client does not wish to upgrade to a websocket')
|
40
|
-
|
41
|
-
@property
|
42
|
-
def websockets(self):
|
43
|
-
return self._websockets
|
44
|
-
|
45
|
-
def __call__(self, environ, start_response):
|
46
|
-
""" Hijack the main loop from the original thread and listen on events on Redis and Websockets"""
|
47
|
-
connection = None
|
48
|
-
try:
|
49
|
-
self.assure_protocol_requirements(environ)
|
50
|
-
connection = WebsocketConnection(self, environ, start_response)
|
51
|
-
connection.handleComs()
|
52
|
-
except WebSocketError:
|
53
|
-
logger.exception()
|
54
|
-
response = http.HttpResponse(status=1001, content='Websocket Closed')
|
55
|
-
except UpgradeRequiredError as excpt:
|
56
|
-
logger.exception()
|
57
|
-
response = http.HttpResponseBadRequest(status=426, content=excpt)
|
58
|
-
except HandshakeError as excpt:
|
59
|
-
logger.exception()
|
60
|
-
response = http.HttpResponseBadRequest(content=excpt)
|
61
|
-
except PermissionDenied as excpt:
|
62
|
-
logger.exception("PermissionDenied")
|
63
|
-
logger.warning('PermissionDenied: {}'.format(excpt), exc_info=sys.exc_info())
|
64
|
-
response = http.HttpResponseForbidden(content=excpt)
|
65
|
-
except SSLRequiredError as excpt:
|
66
|
-
logger.exception("SSLRequiredError")
|
67
|
-
response = http.HttpResponseServerError(content=excpt)
|
68
|
-
except Exception as excpt:
|
69
|
-
logger.exception()
|
70
|
-
response = http.HttpResponseServerError(content=excpt)
|
71
|
-
else:
|
72
|
-
response = http.HttpResponse()
|
73
|
-
finally:
|
74
|
-
logger.info("closing websocket")
|
75
|
-
if connection:
|
76
|
-
connection.release()
|
77
|
-
else:
|
78
|
-
logger.warning('Starting late response on websocket')
|
79
|
-
status_text = http_client.responses.get(response.status_code, 'UNKNOWN STATUS CODE')
|
80
|
-
status = '{0} {1}'.format(response.status_code, status_text)
|
81
|
-
# headers = list(response._headers.values())
|
82
|
-
# if six.PY3:
|
83
|
-
# headers = list(headers)
|
84
|
-
start_response(force_str(status), [])
|
85
|
-
logger.info('Finish non-websocket response with status code: {}'.format(response.status_code))
|
86
|
-
return response
|