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.
- django_nativemojo-0.1.10.dist-info/LICENSE +19 -0
- django_nativemojo-0.1.10.dist-info/METADATA +96 -0
- django_nativemojo-0.1.10.dist-info/NOTICE +8 -0
- django_nativemojo-0.1.10.dist-info/RECORD +194 -0
- django_nativemojo-0.1.10.dist-info/WHEEL +4 -0
- mojo/__init__.py +3 -0
- mojo/apps/account/__init__.py +1 -0
- mojo/apps/account/admin.py +91 -0
- mojo/apps/account/apps.py +16 -0
- mojo/apps/account/migrations/0001_initial.py +77 -0
- mojo/apps/account/migrations/0002_user_is_email_verified_user_is_phone_verified.py +23 -0
- mojo/apps/account/migrations/0003_group_mojo_secrets_user_mojo_secrets.py +23 -0
- mojo/apps/account/migrations/__init__.py +0 -0
- mojo/apps/account/models/__init__.py +3 -0
- mojo/apps/account/models/group.py +98 -0
- mojo/apps/account/models/member.py +95 -0
- mojo/apps/account/models/pkey.py +18 -0
- mojo/apps/account/models/user.py +211 -0
- mojo/apps/account/rest/__init__.py +3 -0
- mojo/apps/account/rest/group.py +25 -0
- mojo/apps/account/rest/user.py +47 -0
- mojo/apps/account/utils/__init__.py +0 -0
- mojo/apps/account/utils/jwtoken.py +72 -0
- mojo/apps/account/utils/passkeys.py +54 -0
- mojo/apps/fileman/README.md +549 -0
- mojo/apps/fileman/__init__.py +0 -0
- mojo/apps/fileman/apps.py +15 -0
- mojo/apps/fileman/backends/__init__.py +117 -0
- mojo/apps/fileman/backends/base.py +319 -0
- mojo/apps/fileman/backends/filesystem.py +397 -0
- mojo/apps/fileman/backends/s3.py +398 -0
- mojo/apps/fileman/examples/configurations.py +378 -0
- mojo/apps/fileman/examples/usage_example.py +665 -0
- mojo/apps/fileman/management/__init__.py +1 -0
- mojo/apps/fileman/management/commands/__init__.py +1 -0
- mojo/apps/fileman/management/commands/cleanup_expired_uploads.py +222 -0
- mojo/apps/fileman/models/__init__.py +7 -0
- mojo/apps/fileman/models/file.py +292 -0
- mojo/apps/fileman/models/manager.py +227 -0
- mojo/apps/fileman/models/render.py +0 -0
- mojo/apps/fileman/rest/__init__ +0 -0
- mojo/apps/fileman/rest/__init__.py +23 -0
- mojo/apps/fileman/rest/fileman.py +13 -0
- mojo/apps/fileman/rest/upload.py +92 -0
- mojo/apps/fileman/utils/__init__.py +19 -0
- mojo/apps/fileman/utils/upload.py +616 -0
- mojo/apps/incident/__init__.py +1 -0
- mojo/apps/incident/handlers/__init__.py +3 -0
- mojo/apps/incident/handlers/event_handlers.py +142 -0
- mojo/apps/incident/migrations/0001_initial.py +83 -0
- mojo/apps/incident/migrations/0002_rename_bundle_ruleset_bundle_minutes_event_hostname_and_more.py +44 -0
- mojo/apps/incident/migrations/0003_alter_event_model_id.py +18 -0
- mojo/apps/incident/migrations/0004_alter_incident_model_id.py +18 -0
- mojo/apps/incident/migrations/__init__.py +0 -0
- mojo/apps/incident/models/__init__.py +3 -0
- mojo/apps/incident/models/event.py +135 -0
- mojo/apps/incident/models/incident.py +33 -0
- mojo/apps/incident/models/rule.py +247 -0
- mojo/apps/incident/parsers/__init__.py +0 -0
- mojo/apps/incident/parsers/ossec/__init__.py +1 -0
- mojo/apps/incident/parsers/ossec/core.py +82 -0
- mojo/apps/incident/parsers/ossec/parsed.py +23 -0
- mojo/apps/incident/parsers/ossec/rules.py +124 -0
- mojo/apps/incident/parsers/ossec/utils.py +169 -0
- mojo/apps/incident/reporter.py +42 -0
- mojo/apps/incident/rest/__init__.py +2 -0
- mojo/apps/incident/rest/event.py +23 -0
- mojo/apps/incident/rest/ossec.py +22 -0
- mojo/apps/logit/__init__.py +0 -0
- mojo/apps/logit/admin.py +37 -0
- mojo/apps/logit/migrations/0001_initial.py +32 -0
- mojo/apps/logit/migrations/0002_log_duid_log_payload_log_username.py +28 -0
- mojo/apps/logit/migrations/0003_log_level.py +18 -0
- mojo/apps/logit/migrations/__init__.py +0 -0
- mojo/apps/logit/models/__init__.py +1 -0
- mojo/apps/logit/models/log.py +57 -0
- mojo/apps/logit/rest.py +9 -0
- mojo/apps/metrics/README.md +79 -0
- mojo/apps/metrics/__init__.py +12 -0
- mojo/apps/metrics/redis_metrics.py +331 -0
- mojo/apps/metrics/rest/__init__.py +1 -0
- mojo/apps/metrics/rest/base.py +152 -0
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/apps/metrics/utils.py +227 -0
- mojo/apps/notify/README.md +91 -0
- mojo/apps/notify/README_NOTIFICATIONS.md +566 -0
- mojo/apps/notify/__init__.py +0 -0
- mojo/apps/notify/admin.py +52 -0
- mojo/apps/notify/handlers/__init__.py +0 -0
- mojo/apps/notify/handlers/example_handlers.py +516 -0
- mojo/apps/notify/handlers/ses/__init__.py +25 -0
- mojo/apps/notify/handlers/ses/bounce.py +0 -0
- mojo/apps/notify/handlers/ses/complaint.py +25 -0
- mojo/apps/notify/handlers/ses/message.py +86 -0
- mojo/apps/notify/management/__init__.py +0 -0
- mojo/apps/notify/management/commands/__init__.py +1 -0
- mojo/apps/notify/management/commands/process_notifications.py +370 -0
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +12 -0
- mojo/apps/notify/models/account.py +128 -0
- mojo/apps/notify/models/attachment.py +24 -0
- mojo/apps/notify/models/bounce.py +68 -0
- mojo/apps/notify/models/complaint.py +40 -0
- mojo/apps/notify/models/inbox.py +113 -0
- mojo/apps/notify/models/inbox_message.py +173 -0
- mojo/apps/notify/models/outbox.py +129 -0
- mojo/apps/notify/models/outbox_message.py +288 -0
- mojo/apps/notify/models/template.py +30 -0
- mojo/apps/notify/providers/__init__.py +0 -0
- mojo/apps/notify/providers/aws.py +73 -0
- mojo/apps/notify/rest/__init__.py +0 -0
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +2 -0
- mojo/apps/notify/utils/notifications.py +404 -0
- mojo/apps/notify/utils/parsing.py +202 -0
- mojo/apps/notify/utils/render.py +144 -0
- mojo/apps/tasks/README.md +118 -0
- mojo/apps/tasks/__init__.py +11 -0
- mojo/apps/tasks/manager.py +489 -0
- mojo/apps/tasks/rest/__init__.py +2 -0
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +62 -0
- mojo/apps/tasks/runner.py +174 -0
- mojo/apps/tasks/tq_handlers.py +14 -0
- mojo/decorators/__init__.py +3 -0
- mojo/decorators/auth.py +25 -0
- mojo/decorators/cron.py +31 -0
- mojo/decorators/http.py +132 -0
- mojo/decorators/validate.py +14 -0
- mojo/errors.py +88 -0
- mojo/helpers/__init__.py +0 -0
- mojo/helpers/aws/__init__.py +0 -0
- mojo/helpers/aws/client.py +8 -0
- mojo/helpers/aws/s3.py +268 -0
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/cron.py +79 -0
- mojo/helpers/crypto/__init__.py +4 -0
- mojo/helpers/crypto/aes.py +60 -0
- mojo/helpers/crypto/hash.py +59 -0
- mojo/helpers/crypto/privpub/__init__.py +1 -0
- mojo/helpers/crypto/privpub/hybrid.py +97 -0
- mojo/helpers/crypto/privpub/rsa.py +104 -0
- mojo/helpers/crypto/sign.py +36 -0
- mojo/helpers/crypto/too.l.py +25 -0
- mojo/helpers/crypto/utils.py +26 -0
- mojo/helpers/daemon.py +94 -0
- mojo/helpers/dates.py +69 -0
- mojo/helpers/dns/__init__.py +0 -0
- mojo/helpers/dns/godaddy.py +62 -0
- mojo/helpers/filetypes.py +128 -0
- mojo/helpers/logit.py +310 -0
- mojo/helpers/modules.py +95 -0
- mojo/helpers/paths.py +63 -0
- mojo/helpers/redis.py +10 -0
- mojo/helpers/request.py +89 -0
- mojo/helpers/request_parser.py +269 -0
- mojo/helpers/response.py +14 -0
- mojo/helpers/settings.py +146 -0
- mojo/helpers/sysinfo.py +140 -0
- mojo/helpers/ua.py +0 -0
- mojo/middleware/__init__.py +0 -0
- mojo/middleware/auth.py +26 -0
- mojo/middleware/logging.py +55 -0
- mojo/middleware/mojo.py +21 -0
- mojo/migrations/0001_initial.py +32 -0
- mojo/migrations/__init__.py +0 -0
- mojo/models/__init__.py +2 -0
- mojo/models/meta.py +262 -0
- mojo/models/rest.py +538 -0
- mojo/models/secrets.py +59 -0
- mojo/rest/__init__.py +1 -0
- mojo/rest/info.py +26 -0
- mojo/serializers/__init__.py +0 -0
- mojo/serializers/models.py +165 -0
- mojo/serializers/openapi.py +188 -0
- mojo/urls.py +38 -0
- mojo/ws4redis/README.md +174 -0
- mojo/ws4redis/__init__.py +2 -0
- mojo/ws4redis/client.py +283 -0
- mojo/ws4redis/connection.py +327 -0
- mojo/ws4redis/exceptions.py +32 -0
- mojo/ws4redis/redis.py +183 -0
- mojo/ws4redis/servers/__init__.py +0 -0
- mojo/ws4redis/servers/base.py +86 -0
- mojo/ws4redis/servers/django.py +171 -0
- mojo/ws4redis/servers/uwsgi.py +63 -0
- mojo/ws4redis/settings.py +45 -0
- mojo/ws4redis/utf8validator.py +128 -0
- mojo/ws4redis/websocket.py +403 -0
- testit/__init__.py +0 -0
- testit/client.py +147 -0
- testit/faker.py +20 -0
- testit/helpers.py +198 -0
- testit/runner.py +262 -0
@@ -0,0 +1,403 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# This code was generously pilfered from https://bitbucket.org/Jeffrey/gevent-websocket
|
3
|
+
# written by Jeffrey Gelens (http://noppo.pro/) and licensed under the Apache License, Version 2.0
|
4
|
+
import struct
|
5
|
+
from socket import error as socket_error
|
6
|
+
from mojo.ws4redis.utf8validator import Utf8Validator
|
7
|
+
from mojo.ws4redis.exceptions import WebSocketError, FrameTooLargeException
|
8
|
+
|
9
|
+
from mojo.helpers.logit import get_logger
|
10
|
+
logger = get_logger("async", filename="async.log")
|
11
|
+
|
12
|
+
|
13
|
+
class WebSocket(object):
|
14
|
+
__slots__ = ('_closed', 'stream', 'utf8validator', 'utf8validate_last')
|
15
|
+
|
16
|
+
OPCODE_CONTINUATION = 0x00
|
17
|
+
OPCODE_TEXT = 0x01
|
18
|
+
OPCODE_BINARY = 0x02
|
19
|
+
OPCODE_CLOSE = 0x08
|
20
|
+
OPCODE_PING = 0x09
|
21
|
+
OPCODE_PONG = 0x0a
|
22
|
+
|
23
|
+
def __init__(self, wsgi_input):
|
24
|
+
self._closed = False
|
25
|
+
self.stream = Stream(wsgi_input)
|
26
|
+
self.utf8validator = Utf8Validator()
|
27
|
+
self.utf8validate_last = None
|
28
|
+
|
29
|
+
def __del__(self):
|
30
|
+
try:
|
31
|
+
self.close()
|
32
|
+
except:
|
33
|
+
# close() may fail if __init__ didn't complete
|
34
|
+
pass
|
35
|
+
|
36
|
+
def _decode_bytes(self, bytestring):
|
37
|
+
"""
|
38
|
+
Internal method used to convert the utf-8 encoded bytestring into unicode.
|
39
|
+
If the conversion fails, the socket will be closed.
|
40
|
+
"""
|
41
|
+
if not bytestring:
|
42
|
+
return u''
|
43
|
+
try:
|
44
|
+
return bytestring.decode('utf-8')
|
45
|
+
except UnicodeDecodeError:
|
46
|
+
self.close(1007)
|
47
|
+
raise
|
48
|
+
|
49
|
+
def _encode_bytes(self, text):
|
50
|
+
"""
|
51
|
+
:returns: The utf-8 byte string equivalent of `text`.
|
52
|
+
"""
|
53
|
+
if isinstance(text, bytes):
|
54
|
+
return text
|
55
|
+
if not isinstance(text, bytes):
|
56
|
+
text = str(text or '')
|
57
|
+
return text.encode('utf-8')
|
58
|
+
|
59
|
+
def _is_valid_close_code(self, code):
|
60
|
+
"""
|
61
|
+
:returns: Whether the returned close code is a valid hybi return code.
|
62
|
+
"""
|
63
|
+
if code < 1000:
|
64
|
+
return False
|
65
|
+
if 1004 <= code <= 1006:
|
66
|
+
return False
|
67
|
+
if 1012 <= code <= 1016:
|
68
|
+
return False
|
69
|
+
if code == 1100:
|
70
|
+
# not sure about this one but the autobahn fuzzer requires it.
|
71
|
+
return False
|
72
|
+
if 2000 <= code <= 2999:
|
73
|
+
return False
|
74
|
+
return True
|
75
|
+
|
76
|
+
def get_file_descriptor(self):
|
77
|
+
"""Return the file descriptor for the given websocket"""
|
78
|
+
return self.stream.fileno
|
79
|
+
|
80
|
+
@property
|
81
|
+
def closed(self):
|
82
|
+
return self._closed
|
83
|
+
|
84
|
+
def handle_close(self, header, payload):
|
85
|
+
"""
|
86
|
+
Called when a close frame has been decoded from the stream.
|
87
|
+
|
88
|
+
:param header: The decoded `Header`.
|
89
|
+
:param payload: The bytestring payload associated with the close frame.
|
90
|
+
"""
|
91
|
+
if not payload:
|
92
|
+
self.close(1000, None)
|
93
|
+
return
|
94
|
+
if len(payload) < 2:
|
95
|
+
raise WebSocketError('Invalid close frame: {0} {1}'.format(header, payload))
|
96
|
+
rv = payload[:2]
|
97
|
+
code = struct.unpack('!H', bytes(rv))[0]
|
98
|
+
payload = payload[2:]
|
99
|
+
if payload:
|
100
|
+
validator = Utf8Validator()
|
101
|
+
val = validator.validate(payload)
|
102
|
+
if not val[0]:
|
103
|
+
raise UnicodeError
|
104
|
+
if not self._is_valid_close_code(code):
|
105
|
+
raise WebSocketError('Invalid close code {0}'.format(code))
|
106
|
+
self.close(code, payload)
|
107
|
+
|
108
|
+
def handle_ping(self, header, payload):
|
109
|
+
self.send_frame(payload, self.OPCODE_PONG)
|
110
|
+
|
111
|
+
def handle_pong(self, header, payload):
|
112
|
+
pass
|
113
|
+
|
114
|
+
def read_frame(self):
|
115
|
+
"""
|
116
|
+
Block until a full frame has been read from the socket.
|
117
|
+
|
118
|
+
This is an internal method as calling this will not cleanup correctly
|
119
|
+
if an exception is called. Use `receive` instead.
|
120
|
+
|
121
|
+
:return: The header and payload as a tuple.
|
122
|
+
"""
|
123
|
+
header = Header.decode_header(self.stream)
|
124
|
+
if header.flags:
|
125
|
+
raise WebSocketError
|
126
|
+
if not header.length:
|
127
|
+
return header, ''
|
128
|
+
try:
|
129
|
+
payload = self.stream.read(header.length)
|
130
|
+
except socket_error:
|
131
|
+
payload = ''
|
132
|
+
except Exception as e:
|
133
|
+
logger.debug("{}: {}".format(type(e), str(e)))
|
134
|
+
payload = ''
|
135
|
+
if len(payload) != header.length:
|
136
|
+
raise WebSocketError('Unexpected EOF reading frame payload')
|
137
|
+
if header.mask:
|
138
|
+
payload = header.unmask_payload(payload)
|
139
|
+
return header, payload
|
140
|
+
|
141
|
+
def validate_utf8(self, payload):
|
142
|
+
# Make sure the frames are decodable independently
|
143
|
+
self.utf8validate_last = self.utf8validator.validate(payload)
|
144
|
+
|
145
|
+
if not self.utf8validate_last[0]:
|
146
|
+
raise UnicodeError("Encountered invalid UTF-8 while processing "
|
147
|
+
"text message at payload octet index "
|
148
|
+
"{0:d}".format(self.utf8validate_last[3]))
|
149
|
+
|
150
|
+
def read_message(self):
|
151
|
+
"""
|
152
|
+
Return the next text or binary message from the socket.
|
153
|
+
|
154
|
+
This is an internal method as calling this will not cleanup correctly
|
155
|
+
if an exception is called. Use `receive` instead.
|
156
|
+
"""
|
157
|
+
opcode = None
|
158
|
+
message = None
|
159
|
+
while True:
|
160
|
+
header, payload = self.read_frame()
|
161
|
+
f_opcode = header.opcode
|
162
|
+
if f_opcode in (self.OPCODE_TEXT, self.OPCODE_BINARY):
|
163
|
+
# a new frame
|
164
|
+
if opcode:
|
165
|
+
raise WebSocketError("The opcode in non-fin frame is expected to be zero, got {0!r}".format(f_opcode))
|
166
|
+
# Start reading a new message, reset the validator
|
167
|
+
self.utf8validator.reset()
|
168
|
+
self.utf8validate_last = (True, True, 0, 0)
|
169
|
+
opcode = f_opcode
|
170
|
+
elif f_opcode == self.OPCODE_CONTINUATION:
|
171
|
+
if not opcode:
|
172
|
+
raise WebSocketError("Unexpected frame with opcode=0")
|
173
|
+
elif f_opcode == self.OPCODE_PING:
|
174
|
+
self.handle_ping(header, payload)
|
175
|
+
continue
|
176
|
+
elif f_opcode == self.OPCODE_PONG:
|
177
|
+
self.handle_pong(header, payload)
|
178
|
+
continue
|
179
|
+
elif f_opcode == self.OPCODE_CLOSE:
|
180
|
+
self.handle_close(header, payload)
|
181
|
+
return
|
182
|
+
else:
|
183
|
+
raise WebSocketError("Unexpected opcode={0!r}".format(f_opcode))
|
184
|
+
if opcode == self.OPCODE_TEXT:
|
185
|
+
self.validate_utf8(payload)
|
186
|
+
payload = payload.decode()
|
187
|
+
if message is None:
|
188
|
+
message = str() if opcode == self.OPCODE_TEXT else bytes()
|
189
|
+
message += payload
|
190
|
+
if header.fin:
|
191
|
+
break
|
192
|
+
if opcode == self.OPCODE_TEXT:
|
193
|
+
self.validate_utf8(message.encode())
|
194
|
+
return message
|
195
|
+
else:
|
196
|
+
return bytearray(message)
|
197
|
+
|
198
|
+
def receive(self):
|
199
|
+
"""
|
200
|
+
Read and return a message from the stream. If `None` is returned, then
|
201
|
+
the socket is considered closed/errored.
|
202
|
+
"""
|
203
|
+
if self._closed:
|
204
|
+
raise WebSocketError("Connection is already closed")
|
205
|
+
try:
|
206
|
+
return self.read_message()
|
207
|
+
except UnicodeError as e:
|
208
|
+
logger.info('websocket.receive: UnicodeError {}'.format(e))
|
209
|
+
self.close(1007)
|
210
|
+
except WebSocketError as e:
|
211
|
+
logger.info('websocket.receive: WebSocketError {}'.format(e))
|
212
|
+
self.close(1002)
|
213
|
+
except Exception as e:
|
214
|
+
logger.info('websocket.receive: Unknown error {}'.format(e))
|
215
|
+
raise e
|
216
|
+
|
217
|
+
def flush(self):
|
218
|
+
"""
|
219
|
+
Flush a websocket. In this implementation intentionally it does nothing.
|
220
|
+
"""
|
221
|
+
pass
|
222
|
+
|
223
|
+
def send_frame(self, message, opcode):
|
224
|
+
"""
|
225
|
+
Send a frame over the websocket with message as its payload
|
226
|
+
"""
|
227
|
+
if self._closed:
|
228
|
+
raise WebSocketError("Connection is already closed")
|
229
|
+
if opcode == self.OPCODE_TEXT:
|
230
|
+
message = self._encode_bytes(message)
|
231
|
+
elif opcode == self.OPCODE_BINARY:
|
232
|
+
message = bytes(message)
|
233
|
+
header = Header.encode_header(True, opcode, '', len(message), 0)
|
234
|
+
try:
|
235
|
+
self.stream.write(header + message)
|
236
|
+
except socket_error:
|
237
|
+
raise WebSocketError("Socket is dead")
|
238
|
+
|
239
|
+
def send(self, message, binary=False):
|
240
|
+
"""
|
241
|
+
Send a frame over the websocket with message as its payload
|
242
|
+
"""
|
243
|
+
if binary is None:
|
244
|
+
binary = not isinstance(message, str)
|
245
|
+
opcode = self.OPCODE_BINARY if binary else self.OPCODE_TEXT
|
246
|
+
try:
|
247
|
+
self.send_frame(message, opcode)
|
248
|
+
except WebSocketError:
|
249
|
+
raise WebSocketError("Socket is dead")
|
250
|
+
|
251
|
+
def close(self, code=1000, message=''):
|
252
|
+
"""
|
253
|
+
Close the websocket and connection, sending the specified code and
|
254
|
+
message. The underlying socket object is _not_ closed, that is the
|
255
|
+
responsibility of the initiator.
|
256
|
+
"""
|
257
|
+
try:
|
258
|
+
message = self._encode_bytes(message)
|
259
|
+
self.send_frame(
|
260
|
+
struct.pack('!H%ds' % len(message), code, message),
|
261
|
+
opcode=self.OPCODE_CLOSE)
|
262
|
+
except WebSocketError:
|
263
|
+
# Failed to write the closing frame but it's ok because we're
|
264
|
+
# closing the socket anyway.
|
265
|
+
logger.debug("Failed to write closing frame -> closing socket")
|
266
|
+
finally:
|
267
|
+
logger.debug("Closed WebSocket")
|
268
|
+
self._closed = True
|
269
|
+
self.stream = None
|
270
|
+
|
271
|
+
|
272
|
+
class Stream(object):
|
273
|
+
"""
|
274
|
+
Wraps the handler's socket/rfile attributes and makes it in to a file like
|
275
|
+
object that can be read from/written to by the lower level websocket api.
|
276
|
+
"""
|
277
|
+
|
278
|
+
__slots__ = ('read', 'write', 'fileno')
|
279
|
+
|
280
|
+
def __init__(self, wsgi_input):
|
281
|
+
self.read = wsgi_input.raw._sock.recv
|
282
|
+
self.write = wsgi_input.raw._sock.sendall
|
283
|
+
self.fileno = wsgi_input.fileno()
|
284
|
+
|
285
|
+
|
286
|
+
class Header(object):
|
287
|
+
__slots__ = ('fin', 'mask', 'opcode', 'flags', 'length')
|
288
|
+
|
289
|
+
FIN_MASK = 0x80
|
290
|
+
OPCODE_MASK = 0x0f
|
291
|
+
MASK_MASK = 0x80
|
292
|
+
LENGTH_MASK = 0x7f
|
293
|
+
RSV0_MASK = 0x40
|
294
|
+
RSV1_MASK = 0x20
|
295
|
+
RSV2_MASK = 0x10
|
296
|
+
|
297
|
+
# bitwise mask that will determine the reserved bits for a frame header
|
298
|
+
HEADER_FLAG_MASK = RSV0_MASK | RSV1_MASK | RSV2_MASK
|
299
|
+
|
300
|
+
def __init__(self, fin=0, opcode=0, flags=0, length=0):
|
301
|
+
self.mask = ''
|
302
|
+
self.fin = fin
|
303
|
+
self.opcode = opcode
|
304
|
+
self.flags = flags
|
305
|
+
self.length = length
|
306
|
+
|
307
|
+
def mask_payload(self, payload):
|
308
|
+
payload = bytearray(payload)
|
309
|
+
mask = bytearray(self.mask)
|
310
|
+
for i in range(self.length):
|
311
|
+
payload[i] ^= mask[i % 4]
|
312
|
+
return bytes(payload)
|
313
|
+
|
314
|
+
# it's the same operation
|
315
|
+
unmask_payload = mask_payload
|
316
|
+
|
317
|
+
def __repr__(self):
|
318
|
+
return ("<Header fin={0} opcode={1} length={2} flags={3} at "
|
319
|
+
"0x{4:x}>").format(self.fin, self.opcode, self.length,
|
320
|
+
self.flags, id(self))
|
321
|
+
|
322
|
+
@classmethod
|
323
|
+
def decode_header(cls, stream):
|
324
|
+
"""
|
325
|
+
Decode a WebSocket header.
|
326
|
+
|
327
|
+
:param stream: A file like object that can be 'read' from.
|
328
|
+
:returns: A `Header` instance.
|
329
|
+
"""
|
330
|
+
read = stream.read
|
331
|
+
data = read(2)
|
332
|
+
if len(data) != 2:
|
333
|
+
raise WebSocketError("Unexpected EOF while decoding header")
|
334
|
+
first_byte, second_byte = struct.unpack('!BB', data)
|
335
|
+
header = cls(
|
336
|
+
fin=first_byte & cls.FIN_MASK == cls.FIN_MASK,
|
337
|
+
opcode=first_byte & cls.OPCODE_MASK,
|
338
|
+
flags=first_byte & cls.HEADER_FLAG_MASK,
|
339
|
+
length=second_byte & cls.LENGTH_MASK)
|
340
|
+
has_mask = second_byte & cls.MASK_MASK == cls.MASK_MASK
|
341
|
+
if header.opcode > 0x07:
|
342
|
+
if not header.fin:
|
343
|
+
raise WebSocketError('Received fragmented control frame: {0!r}'.format(data))
|
344
|
+
# Control frames MUST have a payload length of 125 bytes or less
|
345
|
+
if header.length > 125:
|
346
|
+
raise FrameTooLargeException('Control frame cannot be larger than 125 bytes: {0!r}'.format(data))
|
347
|
+
if header.length == 126:
|
348
|
+
# 16 bit length
|
349
|
+
data = read(2)
|
350
|
+
if len(data) != 2:
|
351
|
+
raise WebSocketError('Unexpected EOF while decoding header')
|
352
|
+
header.length = struct.unpack('!H', data)[0]
|
353
|
+
elif header.length == 127:
|
354
|
+
# 64 bit length
|
355
|
+
data = read(8)
|
356
|
+
if len(data) != 8:
|
357
|
+
raise WebSocketError('Unexpected EOF while decoding header')
|
358
|
+
header.length = struct.unpack('!Q', data)[0]
|
359
|
+
if has_mask:
|
360
|
+
mask = read(4)
|
361
|
+
if len(mask) != 4:
|
362
|
+
raise WebSocketError('Unexpected EOF while decoding header')
|
363
|
+
header.mask = mask
|
364
|
+
return header
|
365
|
+
|
366
|
+
@classmethod
|
367
|
+
def encode_header(cls, fin, opcode, mask, length, flags):
|
368
|
+
"""
|
369
|
+
Encodes a WebSocket header.
|
370
|
+
|
371
|
+
:param fin: Whether this is the final frame for this opcode.
|
372
|
+
:param opcode: The opcode of the payload, see `OPCODE_*`
|
373
|
+
:param mask: Whether the payload is masked.
|
374
|
+
:param length: The length of the frame.
|
375
|
+
:param flags: The RSV* flags.
|
376
|
+
:return: A bytestring encoded header.
|
377
|
+
"""
|
378
|
+
first_byte = opcode
|
379
|
+
second_byte = 0
|
380
|
+
extra = b''
|
381
|
+
if fin:
|
382
|
+
first_byte |= cls.FIN_MASK
|
383
|
+
if flags & cls.RSV0_MASK:
|
384
|
+
first_byte |= cls.RSV0_MASK
|
385
|
+
if flags & cls.RSV1_MASK:
|
386
|
+
first_byte |= cls.RSV1_MASK
|
387
|
+
if flags & cls.RSV2_MASK:
|
388
|
+
first_byte |= cls.RSV2_MASK
|
389
|
+
# now deal with length complexities
|
390
|
+
if length < 126:
|
391
|
+
second_byte += length
|
392
|
+
elif length <= 0xffff:
|
393
|
+
second_byte += 126
|
394
|
+
extra = struct.pack('!H', length)
|
395
|
+
elif length <= 0xffffffffffffffff:
|
396
|
+
second_byte += 127
|
397
|
+
extra = struct.pack('!Q', length)
|
398
|
+
else:
|
399
|
+
raise FrameTooLargeException
|
400
|
+
if mask:
|
401
|
+
second_byte |= cls.MASK_MASK
|
402
|
+
extra += mask
|
403
|
+
return bytes([first_byte, second_byte]) + extra
|
testit/__init__.py
ADDED
File without changes
|
testit/client.py
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
import requests
|
2
|
+
import base64
|
3
|
+
from objict import objict
|
4
|
+
|
5
|
+
|
6
|
+
class RestClient:
|
7
|
+
"""
|
8
|
+
A simple REST client for making HTTP requests to a specified host.
|
9
|
+
"""
|
10
|
+
|
11
|
+
def __init__(self, host, logger=None):
|
12
|
+
"""
|
13
|
+
Initializes the SimpleRestClient with a host URL.
|
14
|
+
|
15
|
+
Args:
|
16
|
+
host (str): The base URL of the host for making requests.
|
17
|
+
"""
|
18
|
+
self.host = host if host[-1] == "/" else f"{host}/"
|
19
|
+
self.logger = logger
|
20
|
+
self.access_token = None
|
21
|
+
self.is_authenticated = False
|
22
|
+
self.bearer = "bearer"
|
23
|
+
self.access_token = None
|
24
|
+
|
25
|
+
def login(self, username, password):
|
26
|
+
self.logout()
|
27
|
+
resp = self.post("/api/login", dict(username=username, password=password))
|
28
|
+
if resp.response.data and resp.response.data.access_token:
|
29
|
+
self.is_authenticated = True
|
30
|
+
self.access_token = resp.response.data.access_token
|
31
|
+
junk, self.jwt_data = decode_jwt(self.access_token)
|
32
|
+
return self.is_authenticated
|
33
|
+
|
34
|
+
def logout(self):
|
35
|
+
self.is_authenticated = False
|
36
|
+
self.bearer = "bearer"
|
37
|
+
self.access_token = None
|
38
|
+
|
39
|
+
def get_headers(self):
|
40
|
+
if self.is_authenticated:
|
41
|
+
return dict(Authorization=f"{self.bearer} {self.access_token}")
|
42
|
+
return {}
|
43
|
+
|
44
|
+
def _make_request(self, method, path, **kwargs):
|
45
|
+
"""
|
46
|
+
Makes an HTTP request using the specified method and path.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
method (str): The HTTP method to use for the request (e.g., 'GET', 'POST').
|
50
|
+
path (str): The endpoint path to append to the base host URL.
|
51
|
+
**kwargs: Additional arguments to pass to the request (e.g., headers, params).
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
dict: A dictionary containing the response data and status code. If an error occurs,
|
55
|
+
returns a dictionary with an error message instead.
|
56
|
+
"""
|
57
|
+
if path[0] == "/":
|
58
|
+
path = path[1:]
|
59
|
+
url = f"{self.host}{path}"
|
60
|
+
headers = self.get_headers()
|
61
|
+
response = requests.request(method, url, headers=headers, **kwargs)
|
62
|
+
if self.logger:
|
63
|
+
self.logger.info("REQUEST", f"{method}:{url}", headers)
|
64
|
+
self.logger.info(kwargs.get("json", ""))
|
65
|
+
try:
|
66
|
+
data = objict.fromdict(response.json()) if response.content else None
|
67
|
+
response_data = objict(response=data, status_code=response.status_code)
|
68
|
+
if not response.ok:
|
69
|
+
response_data['error_reason'] = response.reason
|
70
|
+
if self.logger:
|
71
|
+
self.logger.info("RESPONSE", f"{method}:{url}")
|
72
|
+
self.logger.info(response_data)
|
73
|
+
return response_data
|
74
|
+
except Exception as e:
|
75
|
+
if self.logger:
|
76
|
+
self.logger.error("RESPONSE", f"{method}:{url}")
|
77
|
+
self.logger.exception(str(e), response.text)
|
78
|
+
return objict(error=str(e), text=response.text)
|
79
|
+
|
80
|
+
def get(self, path, **kwargs):
|
81
|
+
"""
|
82
|
+
Sends a GET request to the specified path.
|
83
|
+
|
84
|
+
Args:
|
85
|
+
path (str): The endpoint path to append to the base host URL.
|
86
|
+
**kwargs: Additional arguments to pass to the request (e.g., headers, params).
|
87
|
+
|
88
|
+
Returns:
|
89
|
+
dict: A dictionary containing the response data and status code.
|
90
|
+
"""
|
91
|
+
return self._make_request('GET', path, **kwargs)
|
92
|
+
|
93
|
+
def post(self, path, json=None, **kwargs):
|
94
|
+
"""
|
95
|
+
Sends a POST request to the specified path.
|
96
|
+
|
97
|
+
Args:
|
98
|
+
path (str): The endpoint path to append to the base host URL.
|
99
|
+
json (dict, optional): The JSON data to include in the request body.
|
100
|
+
**kwargs: Additional arguments to pass to the request (e.g., headers).
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
dict: A dictionary containing the response data and status code.
|
104
|
+
"""
|
105
|
+
return self._make_request('POST', path, json=json, **kwargs)
|
106
|
+
|
107
|
+
def put(self, path, json=None, **kwargs):
|
108
|
+
"""
|
109
|
+
Sends a PUT request to the specified path.
|
110
|
+
|
111
|
+
Args:
|
112
|
+
path (str): The endpoint path to append to the base host URL.
|
113
|
+
json (dict, optional): The JSON data to include in the request body.
|
114
|
+
**kwargs: Additional arguments to pass to the request (e.g., headers).
|
115
|
+
|
116
|
+
Returns:
|
117
|
+
dict: A dictionary containing the response data and status code.
|
118
|
+
"""
|
119
|
+
return self._make_request('PUT', path, json=json, **kwargs)
|
120
|
+
|
121
|
+
def delete(self, path, **kwargs):
|
122
|
+
"""
|
123
|
+
Sends a DELETE request to the specified path.
|
124
|
+
|
125
|
+
Args:
|
126
|
+
path (str): The endpoint path to append to the base host URL.
|
127
|
+
**kwargs: Additional arguments to pass to the request (e.g., headers).
|
128
|
+
|
129
|
+
Returns:
|
130
|
+
dict: A dictionary containing the response data and status code.
|
131
|
+
"""
|
132
|
+
return self._make_request('DELETE', path, **kwargs)
|
133
|
+
|
134
|
+
|
135
|
+
|
136
|
+
def base64_decode(data):
|
137
|
+
"""Decode base64-encoded data."""
|
138
|
+
padding = '=' * (-len(data) % 4)
|
139
|
+
return base64.urlsafe_b64decode(data + padding)
|
140
|
+
|
141
|
+
|
142
|
+
def decode_jwt(token):
|
143
|
+
"""Decode a JWT token using base64 decoding."""
|
144
|
+
headers, payload, signature = token.split('.')
|
145
|
+
decoded_headers = objict.fromJSON(base64_decode(headers))
|
146
|
+
decoded_payload = objict.fromJSON(base64_decode(payload))
|
147
|
+
return decoded_headers, decoded_payload
|
testit/faker.py
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
from faker import Faker
|
2
|
+
|
3
|
+
fake = Faker()
|
4
|
+
|
5
|
+
def generate_person():
|
6
|
+
return {
|
7
|
+
'first_name': fake.first_name(),
|
8
|
+
'last_name': fake.last_name(),
|
9
|
+
'dob': fake.date_of_birth(),
|
10
|
+
'city': fake.city(),
|
11
|
+
'state': fake.state(),
|
12
|
+
'zipcode': fake.zipcode()
|
13
|
+
}
|
14
|
+
|
15
|
+
def generate_name():
|
16
|
+
return fake.catch_phrase()
|
17
|
+
|
18
|
+
|
19
|
+
def generate_text():
|
20
|
+
return fake.text()
|