django-nativemojo 0.1.10__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.
Files changed (194) hide show
  1. django_nativemojo-0.1.10.dist-info/LICENSE +19 -0
  2. django_nativemojo-0.1.10.dist-info/METADATA +96 -0
  3. django_nativemojo-0.1.10.dist-info/NOTICE +8 -0
  4. django_nativemojo-0.1.10.dist-info/RECORD +194 -0
  5. django_nativemojo-0.1.10.dist-info/WHEEL +4 -0
  6. mojo/__init__.py +3 -0
  7. mojo/apps/account/__init__.py +1 -0
  8. mojo/apps/account/admin.py +91 -0
  9. mojo/apps/account/apps.py +16 -0
  10. mojo/apps/account/migrations/0001_initial.py +77 -0
  11. mojo/apps/account/migrations/0002_user_is_email_verified_user_is_phone_verified.py +23 -0
  12. mojo/apps/account/migrations/0003_group_mojo_secrets_user_mojo_secrets.py +23 -0
  13. mojo/apps/account/migrations/__init__.py +0 -0
  14. mojo/apps/account/models/__init__.py +3 -0
  15. mojo/apps/account/models/group.py +98 -0
  16. mojo/apps/account/models/member.py +95 -0
  17. mojo/apps/account/models/pkey.py +18 -0
  18. mojo/apps/account/models/user.py +211 -0
  19. mojo/apps/account/rest/__init__.py +3 -0
  20. mojo/apps/account/rest/group.py +25 -0
  21. mojo/apps/account/rest/user.py +47 -0
  22. mojo/apps/account/utils/__init__.py +0 -0
  23. mojo/apps/account/utils/jwtoken.py +72 -0
  24. mojo/apps/account/utils/passkeys.py +54 -0
  25. mojo/apps/fileman/README.md +549 -0
  26. mojo/apps/fileman/__init__.py +0 -0
  27. mojo/apps/fileman/apps.py +15 -0
  28. mojo/apps/fileman/backends/__init__.py +117 -0
  29. mojo/apps/fileman/backends/base.py +319 -0
  30. mojo/apps/fileman/backends/filesystem.py +397 -0
  31. mojo/apps/fileman/backends/s3.py +398 -0
  32. mojo/apps/fileman/examples/configurations.py +378 -0
  33. mojo/apps/fileman/examples/usage_example.py +665 -0
  34. mojo/apps/fileman/management/__init__.py +1 -0
  35. mojo/apps/fileman/management/commands/__init__.py +1 -0
  36. mojo/apps/fileman/management/commands/cleanup_expired_uploads.py +222 -0
  37. mojo/apps/fileman/models/__init__.py +7 -0
  38. mojo/apps/fileman/models/file.py +292 -0
  39. mojo/apps/fileman/models/manager.py +227 -0
  40. mojo/apps/fileman/models/render.py +0 -0
  41. mojo/apps/fileman/rest/__init__ +0 -0
  42. mojo/apps/fileman/rest/__init__.py +23 -0
  43. mojo/apps/fileman/rest/fileman.py +13 -0
  44. mojo/apps/fileman/rest/upload.py +92 -0
  45. mojo/apps/fileman/utils/__init__.py +19 -0
  46. mojo/apps/fileman/utils/upload.py +616 -0
  47. mojo/apps/incident/__init__.py +1 -0
  48. mojo/apps/incident/handlers/__init__.py +3 -0
  49. mojo/apps/incident/handlers/event_handlers.py +142 -0
  50. mojo/apps/incident/migrations/0001_initial.py +83 -0
  51. mojo/apps/incident/migrations/0002_rename_bundle_ruleset_bundle_minutes_event_hostname_and_more.py +44 -0
  52. mojo/apps/incident/migrations/0003_alter_event_model_id.py +18 -0
  53. mojo/apps/incident/migrations/0004_alter_incident_model_id.py +18 -0
  54. mojo/apps/incident/migrations/__init__.py +0 -0
  55. mojo/apps/incident/models/__init__.py +3 -0
  56. mojo/apps/incident/models/event.py +135 -0
  57. mojo/apps/incident/models/incident.py +33 -0
  58. mojo/apps/incident/models/rule.py +247 -0
  59. mojo/apps/incident/parsers/__init__.py +0 -0
  60. mojo/apps/incident/parsers/ossec/__init__.py +1 -0
  61. mojo/apps/incident/parsers/ossec/core.py +82 -0
  62. mojo/apps/incident/parsers/ossec/parsed.py +23 -0
  63. mojo/apps/incident/parsers/ossec/rules.py +124 -0
  64. mojo/apps/incident/parsers/ossec/utils.py +169 -0
  65. mojo/apps/incident/reporter.py +42 -0
  66. mojo/apps/incident/rest/__init__.py +2 -0
  67. mojo/apps/incident/rest/event.py +23 -0
  68. mojo/apps/incident/rest/ossec.py +22 -0
  69. mojo/apps/logit/__init__.py +0 -0
  70. mojo/apps/logit/admin.py +37 -0
  71. mojo/apps/logit/migrations/0001_initial.py +32 -0
  72. mojo/apps/logit/migrations/0002_log_duid_log_payload_log_username.py +28 -0
  73. mojo/apps/logit/migrations/0003_log_level.py +18 -0
  74. mojo/apps/logit/migrations/__init__.py +0 -0
  75. mojo/apps/logit/models/__init__.py +1 -0
  76. mojo/apps/logit/models/log.py +57 -0
  77. mojo/apps/logit/rest.py +9 -0
  78. mojo/apps/metrics/README.md +79 -0
  79. mojo/apps/metrics/__init__.py +12 -0
  80. mojo/apps/metrics/redis_metrics.py +331 -0
  81. mojo/apps/metrics/rest/__init__.py +1 -0
  82. mojo/apps/metrics/rest/base.py +152 -0
  83. mojo/apps/metrics/rest/db.py +0 -0
  84. mojo/apps/metrics/utils.py +227 -0
  85. mojo/apps/notify/README.md +91 -0
  86. mojo/apps/notify/README_NOTIFICATIONS.md +566 -0
  87. mojo/apps/notify/__init__.py +0 -0
  88. mojo/apps/notify/admin.py +52 -0
  89. mojo/apps/notify/handlers/__init__.py +0 -0
  90. mojo/apps/notify/handlers/example_handlers.py +516 -0
  91. mojo/apps/notify/handlers/ses/__init__.py +25 -0
  92. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  93. mojo/apps/notify/handlers/ses/complaint.py +25 -0
  94. mojo/apps/notify/handlers/ses/message.py +86 -0
  95. mojo/apps/notify/management/__init__.py +0 -0
  96. mojo/apps/notify/management/commands/__init__.py +1 -0
  97. mojo/apps/notify/management/commands/process_notifications.py +370 -0
  98. mojo/apps/notify/mod +0 -0
  99. mojo/apps/notify/models/__init__.py +12 -0
  100. mojo/apps/notify/models/account.py +128 -0
  101. mojo/apps/notify/models/attachment.py +24 -0
  102. mojo/apps/notify/models/bounce.py +68 -0
  103. mojo/apps/notify/models/complaint.py +40 -0
  104. mojo/apps/notify/models/inbox.py +113 -0
  105. mojo/apps/notify/models/inbox_message.py +173 -0
  106. mojo/apps/notify/models/outbox.py +129 -0
  107. mojo/apps/notify/models/outbox_message.py +288 -0
  108. mojo/apps/notify/models/template.py +30 -0
  109. mojo/apps/notify/providers/__init__.py +0 -0
  110. mojo/apps/notify/providers/aws.py +73 -0
  111. mojo/apps/notify/rest/__init__.py +0 -0
  112. mojo/apps/notify/rest/ses.py +0 -0
  113. mojo/apps/notify/utils/__init__.py +2 -0
  114. mojo/apps/notify/utils/notifications.py +404 -0
  115. mojo/apps/notify/utils/parsing.py +202 -0
  116. mojo/apps/notify/utils/render.py +144 -0
  117. mojo/apps/tasks/README.md +118 -0
  118. mojo/apps/tasks/__init__.py +11 -0
  119. mojo/apps/tasks/manager.py +489 -0
  120. mojo/apps/tasks/rest/__init__.py +2 -0
  121. mojo/apps/tasks/rest/hooks.py +0 -0
  122. mojo/apps/tasks/rest/tasks.py +62 -0
  123. mojo/apps/tasks/runner.py +174 -0
  124. mojo/apps/tasks/tq_handlers.py +14 -0
  125. mojo/decorators/__init__.py +3 -0
  126. mojo/decorators/auth.py +25 -0
  127. mojo/decorators/cron.py +31 -0
  128. mojo/decorators/http.py +132 -0
  129. mojo/decorators/validate.py +14 -0
  130. mojo/errors.py +88 -0
  131. mojo/helpers/__init__.py +0 -0
  132. mojo/helpers/aws/__init__.py +0 -0
  133. mojo/helpers/aws/client.py +8 -0
  134. mojo/helpers/aws/s3.py +268 -0
  135. mojo/helpers/aws/setup_email.py +0 -0
  136. mojo/helpers/cron.py +79 -0
  137. mojo/helpers/crypto/__init__.py +4 -0
  138. mojo/helpers/crypto/aes.py +60 -0
  139. mojo/helpers/crypto/hash.py +59 -0
  140. mojo/helpers/crypto/privpub/__init__.py +1 -0
  141. mojo/helpers/crypto/privpub/hybrid.py +97 -0
  142. mojo/helpers/crypto/privpub/rsa.py +104 -0
  143. mojo/helpers/crypto/sign.py +36 -0
  144. mojo/helpers/crypto/too.l.py +25 -0
  145. mojo/helpers/crypto/utils.py +26 -0
  146. mojo/helpers/daemon.py +94 -0
  147. mojo/helpers/dates.py +69 -0
  148. mojo/helpers/dns/__init__.py +0 -0
  149. mojo/helpers/dns/godaddy.py +62 -0
  150. mojo/helpers/filetypes.py +128 -0
  151. mojo/helpers/logit.py +310 -0
  152. mojo/helpers/modules.py +95 -0
  153. mojo/helpers/paths.py +63 -0
  154. mojo/helpers/redis.py +10 -0
  155. mojo/helpers/request.py +89 -0
  156. mojo/helpers/request_parser.py +269 -0
  157. mojo/helpers/response.py +14 -0
  158. mojo/helpers/settings.py +146 -0
  159. mojo/helpers/sysinfo.py +140 -0
  160. mojo/helpers/ua.py +0 -0
  161. mojo/middleware/__init__.py +0 -0
  162. mojo/middleware/auth.py +26 -0
  163. mojo/middleware/logging.py +55 -0
  164. mojo/middleware/mojo.py +21 -0
  165. mojo/migrations/0001_initial.py +32 -0
  166. mojo/migrations/__init__.py +0 -0
  167. mojo/models/__init__.py +2 -0
  168. mojo/models/meta.py +262 -0
  169. mojo/models/rest.py +538 -0
  170. mojo/models/secrets.py +59 -0
  171. mojo/rest/__init__.py +1 -0
  172. mojo/rest/info.py +26 -0
  173. mojo/serializers/__init__.py +0 -0
  174. mojo/serializers/models.py +165 -0
  175. mojo/serializers/openapi.py +188 -0
  176. mojo/urls.py +38 -0
  177. mojo/ws4redis/README.md +174 -0
  178. mojo/ws4redis/__init__.py +2 -0
  179. mojo/ws4redis/client.py +283 -0
  180. mojo/ws4redis/connection.py +327 -0
  181. mojo/ws4redis/exceptions.py +32 -0
  182. mojo/ws4redis/redis.py +183 -0
  183. mojo/ws4redis/servers/__init__.py +0 -0
  184. mojo/ws4redis/servers/base.py +86 -0
  185. mojo/ws4redis/servers/django.py +171 -0
  186. mojo/ws4redis/servers/uwsgi.py +63 -0
  187. mojo/ws4redis/settings.py +45 -0
  188. mojo/ws4redis/utf8validator.py +128 -0
  189. mojo/ws4redis/websocket.py +403 -0
  190. testit/__init__.py +0 -0
  191. testit/client.py +147 -0
  192. testit/faker.py +20 -0
  193. testit/helpers.py +198 -0
  194. testit/runner.py +262 -0
@@ -0,0 +1,283 @@
1
+ import time
2
+
3
+ from objict import objict
4
+
5
+ from mojo.ws4redis.redis import RedisMessage, RedisStore, getRedisClient, getPoolStatus
6
+
7
+
8
+ def buildEventMessage(name=None, message=None, priority=0, model=None, model_pk=None, custom=None):
9
+ msg = objict(priority=priority)
10
+ if name:
11
+ msg["name"] = name
12
+
13
+ if message:
14
+ msg["message"] = message
15
+
16
+ if model:
17
+ msg["component"] = objict(pk=model_pk, model=model)
18
+
19
+ if custom:
20
+ msg.update(custom)
21
+ return msg.toJSON(as_string=True)
22
+
23
+
24
+ def ping():
25
+ return getRedisClient().ping()
26
+
27
+
28
+ def exists(key, default=None):
29
+ c = getRedisClient()
30
+ return c.exists(key)
31
+
32
+
33
+ def keys(keys):
34
+ c = getRedisClient()
35
+ return [v.decode() for v in c.keys(keys)]
36
+
37
+
38
+ def get(key, default=None, field_type=None):
39
+ c = getRedisClient()
40
+ v = c.get(key)
41
+ if v is None:
42
+ return default
43
+ if field_type is not None:
44
+ if field_type == "json":
45
+ return objict.fromJSON(v, ignore_errors=True)
46
+ if field_type in [str, "str"]:
47
+ v = v.decode()
48
+ return field_type(v)
49
+ return v
50
+
51
+
52
+ def set(key, value, expire=None):
53
+ c = getRedisClient()
54
+ v = c.set(key, value)
55
+ if expire:
56
+ c.expire(key, expire)
57
+ return v
58
+
59
+
60
+ def incr(key, amount=1, expire=None):
61
+ c = getRedisClient()
62
+ v = c.incr(key, amount)
63
+ if expire:
64
+ c.expire(key, expire)
65
+ return v
66
+
67
+
68
+ def expire(key, expire):
69
+ c = getRedisClient()
70
+ return c.expire(key, expire)
71
+
72
+
73
+ def decr(key, amount=1):
74
+ c = getRedisClient()
75
+ return c.decr(key, amount)
76
+
77
+
78
+ def delete(key):
79
+ c = getRedisClient()
80
+ return c.delete(key)
81
+
82
+
83
+ # SET FUNCTIONS
84
+ def sadd(name, *values):
85
+ # add value to set
86
+ c = getRedisClient()
87
+ return c.sadd(name, *values)
88
+
89
+
90
+ def srem(name, *values):
91
+ # remove value from set
92
+ c = getRedisClient()
93
+ return c.srem(name, *values)
94
+
95
+
96
+ def sismember(name, value):
97
+ # return items in set
98
+ c = getRedisClient()
99
+ return c.sismember(name, value)
100
+
101
+
102
+ def scard(name):
103
+ # count items in set
104
+ c = getRedisClient()
105
+ return c.scard(name)
106
+
107
+
108
+ def smembers(name):
109
+ # return items in set
110
+ c = getRedisClient()
111
+ return c.smembers(name)
112
+
113
+
114
+ # HASH FUNCTIONS
115
+ def hget(name, field, default=None):
116
+ c = getRedisClient()
117
+ v = c.hget(name, field)
118
+ if v is None:
119
+ return default
120
+ return v
121
+
122
+
123
+ def hgetall(name):
124
+ c = getRedisClient()
125
+ return c.hgetall(name)
126
+
127
+
128
+ def hset(name, field, value):
129
+ c = getRedisClient()
130
+ return c.hset(name, field, value)
131
+
132
+
133
+ def hdel(name, field):
134
+ c = getRedisClient()
135
+ return c.hdel(name, field)
136
+
137
+
138
+ def hincrby(name, field, inc=1):
139
+ c = getRedisClient()
140
+ return c.hincrby(name, field, inc)
141
+
142
+
143
+ def lpush(name, value, unique=False):
144
+ c = getRedisClient()
145
+ if isinstance(value, list):
146
+ for v in value:
147
+ if unique and value.encode() in c.lrange(name, 0, -1):
148
+ return 0
149
+ c.lpush(name, v)
150
+ return len(value)
151
+ if unique and value.encode() in c.lrange(name, 0, -1):
152
+ return 0
153
+ return c.lpush(name, value)
154
+
155
+
156
+ def rpush(name, value, unique=False):
157
+ c = getRedisClient()
158
+ if isinstance(value, list):
159
+ for v in value:
160
+ if unique and value.encode() in c.lrange(name, 0, -1):
161
+ return 0
162
+ c.rpush(name, v)
163
+ return len(value)
164
+ if unique and value.encode() in c.lrange(name, 0, -1):
165
+ return 0
166
+ return c.rpush(name, value)
167
+
168
+
169
+ def lpop(name, timeout=None):
170
+ c = getRedisClient()
171
+ if timeout is None:
172
+ return c.lpop(name)
173
+ r = c.blpop(name, timeout=timeout)
174
+ if isinstance(r, tuple):
175
+ return r[1].decode()
176
+
177
+
178
+ def rpop(name, timeout=None):
179
+ c = getRedisClient()
180
+ if timeout is None:
181
+ return c.rpop(name)
182
+ r = c.brpop(name, timeout=timeout)
183
+ if isinstance(r, tuple):
184
+ return r[1].decode()
185
+
186
+
187
+ def lrem(name, value, occurences=1):
188
+ c = getRedisClient()
189
+ return c.lrem(name, occurences, value.encode())
190
+
191
+
192
+ def vpop(name, value, timeout=None):
193
+ c = getRedisClient()
194
+ start = time.time()
195
+ while c.lrem(name, 1, value) == 0:
196
+ if timeout is None:
197
+ return 0
198
+ time.sleep(1.0)
199
+ elapsed = time.time() - start
200
+ if elapsed > timeout:
201
+ return 0
202
+ return 1
203
+
204
+
205
+ def lrange(name, start, end):
206
+ c = getRedisClient()
207
+ return [v.decode() for v in c.lrange(name, start, end)]
208
+
209
+
210
+ def sendToUser(user, name, message=None, priority=0, model=None, model_pk=None, custom=None):
211
+ return sendMessageToUsers([user], buildEventMessage(name, message, priority, model, model_pk, custom))
212
+
213
+
214
+ def sendToUsers(users, name, message=None, priority=0, model=None, model_pk=None, custom=None):
215
+ return sendMessageToUsers(users, buildEventMessage(name, message, priority, model, model_pk, custom))
216
+
217
+
218
+ def sendMessageToUsers(users, msg):
219
+ return RedisStore().publish(RedisMessage(msg), channel="user", pk=[u.username for u in users])
220
+
221
+
222
+ def sendToGroup(group, name, message=None, priority=0, model=None, model_pk=None, custom=None):
223
+ return sendMessageToModels("group", [group], buildEventMessage(name, message, priority, model, model_pk, custom))
224
+
225
+
226
+ def sendToGroups(groups, name, message=None, priority=0, model=None, model_pk=None, custom=None):
227
+ return sendMessageToModels("group", groups, buildEventMessage(name, message, priority, model, model_pk, custom))
228
+
229
+
230
+ def sendToModels(channel, models, name, message=None, priority=0, model=None, model_pk=None, custom=None):
231
+ return sendMessageToModels(channel, models, buildEventMessage(name, message, priority, model, model_pk, custom))
232
+
233
+
234
+ def sendMessageToModels(channel, models, msg):
235
+ return RedisStore().publish(RedisMessage(msg), channel=channel, pk=[g.pk for g in models])
236
+
237
+
238
+ def sendMessageToPK(channel, pk, msg):
239
+ return RedisStore().publish(RedisMessage(msg), channel=channel, pk=pk)
240
+
241
+
242
+ def broadcast(name, message=None, priority=0, model=None, model_pk=None, custom=None):
243
+ return broadcastMessage(buildEventMessage(name, message, priority, model, model_pk, custom))
244
+
245
+
246
+ def broadcastMessage(msg):
247
+ return RedisStore().publish(RedisMessage(msg), channel="broadcast")
248
+
249
+
250
+ def publish(key, data, c=None):
251
+ if c is None:
252
+ c = getRedisClient()
253
+ if isinstance(data, dict):
254
+ if not isinstance(data, objict):
255
+ data = objict(data)
256
+ data = data.toJSON(as_string=True)
257
+ return c.publish(key, data)
258
+
259
+
260
+ def subscribe(channel):
261
+ c = getRedisClient()
262
+ pubsub = c.pubsub()
263
+ pubsub.subscribe(channel)
264
+ return pubsub
265
+
266
+
267
+ def waitForMessage(pubsub, msg_filter, timeout=55):
268
+ timeout_at = time.time() + timeout
269
+ while time.time() < timeout_at:
270
+ message = pubsub.get_message()
271
+ if message is not None:
272
+ if message.get("type") == "message":
273
+ msg = objict.fromJSON(message.get("data"))
274
+ if msg_filter(msg):
275
+ pubsub.unsubscribe()
276
+ return msg
277
+ time.sleep(1.0)
278
+ pubsub.unsubscribe()
279
+ return None
280
+
281
+
282
+ def isOnline(name, pk):
283
+ return sismember(f"{name}:online", pk)
@@ -0,0 +1,327 @@
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()
@@ -0,0 +1,32 @@
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
+ """