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
mojo/helpers/aws/s3.py
ADDED
@@ -0,0 +1,268 @@
|
|
1
|
+
from rest import settings
|
2
|
+
from objict import objict
|
3
|
+
import boto3
|
4
|
+
import botocore
|
5
|
+
from urllib.parse import urlparse
|
6
|
+
import io
|
7
|
+
import sys
|
8
|
+
from medialib import utils
|
9
|
+
import threading
|
10
|
+
import tempfile
|
11
|
+
import logging
|
12
|
+
from typing import Optional, Union, BinaryIO, Dict, List, Any
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
class S3Config:
|
17
|
+
"""S3 configuration holder with lazy initialization of clients and resources."""
|
18
|
+
def __init__(self, key: str, secret: str):
|
19
|
+
self.key = key
|
20
|
+
self.secret = secret
|
21
|
+
self._resource = None
|
22
|
+
self._client = None
|
23
|
+
|
24
|
+
@property
|
25
|
+
def resource(self):
|
26
|
+
if self._resource is None:
|
27
|
+
self._resource = boto3.resource('s3',
|
28
|
+
aws_access_key_id=self.key,
|
29
|
+
aws_secret_access_key=self.secret)
|
30
|
+
return self._resource
|
31
|
+
|
32
|
+
@property
|
33
|
+
def client(self):
|
34
|
+
if self._client is None:
|
35
|
+
self._client = boto3.client('s3',
|
36
|
+
aws_access_key_id=self.key,
|
37
|
+
aws_secret_access_key=self.secret)
|
38
|
+
return self._client
|
39
|
+
|
40
|
+
# Initialize the global S3 configuration
|
41
|
+
S3 = S3Config(settings.AWS_KEY, settings.AWS_SECRET)
|
42
|
+
|
43
|
+
|
44
|
+
class S3Item:
|
45
|
+
"""Class representing an S3 object with operations for upload, download, and management."""
|
46
|
+
|
47
|
+
S3_HOST = "https://s3.amazonaws.com"
|
48
|
+
|
49
|
+
def __init__(self, url: str):
|
50
|
+
"""
|
51
|
+
Initialize an S3Item from a URL.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
url: S3 URL in the format s3://bucket-name/key
|
55
|
+
"""
|
56
|
+
self.url = url
|
57
|
+
parsed_url = urlparse(url)
|
58
|
+
self.bucket_name = parsed_url.netloc
|
59
|
+
self.key = parsed_url.path.lstrip('/')
|
60
|
+
self.object = S3.resource.Object(self.bucket_name, self.key)
|
61
|
+
self.exists = self._check_exists()
|
62
|
+
|
63
|
+
def _check_exists(self) -> bool:
|
64
|
+
"""Check if the S3 object exists."""
|
65
|
+
try:
|
66
|
+
self.object.load()
|
67
|
+
return True
|
68
|
+
except botocore.exceptions.ClientError as e:
|
69
|
+
if e.response['Error']['Code'] == "404":
|
70
|
+
return False
|
71
|
+
raise
|
72
|
+
|
73
|
+
def upload(self, file_obj: Union[str, BinaryIO], background: bool = False) -> None:
|
74
|
+
"""
|
75
|
+
Upload a file to S3.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
file_obj: File path or file-like object to upload
|
79
|
+
background: Currently unused, kept for backward compatibility
|
80
|
+
"""
|
81
|
+
prepared_file = self._prepare_file(file_obj)
|
82
|
+
self.object.upload_fileobj(prepared_file)
|
83
|
+
|
84
|
+
def create_multipart_upload(self) -> str:
|
85
|
+
"""
|
86
|
+
Initialize a multipart upload.
|
87
|
+
|
88
|
+
Returns:
|
89
|
+
Upload ID for the multipart upload
|
90
|
+
"""
|
91
|
+
self.part_num = 0
|
92
|
+
self.parts = []
|
93
|
+
response = S3.client.create_multipart_upload(
|
94
|
+
Bucket=self.bucket_name,
|
95
|
+
Key=self.key
|
96
|
+
)
|
97
|
+
self.upload_id = response["UploadId"]
|
98
|
+
return self.upload_id
|
99
|
+
|
100
|
+
def upload_part(self, chunk: bytes) -> Dict:
|
101
|
+
"""
|
102
|
+
Upload a part in a multipart upload.
|
103
|
+
|
104
|
+
Args:
|
105
|
+
chunk: Bytes to upload as part
|
106
|
+
|
107
|
+
Returns:
|
108
|
+
Dict with part information
|
109
|
+
"""
|
110
|
+
self.part_num += 1
|
111
|
+
response = S3.client.upload_part(
|
112
|
+
Bucket=self.bucket_name,
|
113
|
+
Key=self.key,
|
114
|
+
PartNumber=self.part_num,
|
115
|
+
UploadId=self.upload_id,
|
116
|
+
Body=chunk
|
117
|
+
)
|
118
|
+
part_info = {"PartNumber": self.part_num, "ETag": response["ETag"]}
|
119
|
+
self.parts.append(part_info)
|
120
|
+
return part_info
|
121
|
+
|
122
|
+
def complete_multipart_upload(self) -> Dict:
|
123
|
+
"""
|
124
|
+
Complete a multipart upload.
|
125
|
+
|
126
|
+
Returns:
|
127
|
+
S3 response
|
128
|
+
"""
|
129
|
+
return S3.client.complete_multipart_upload(
|
130
|
+
Bucket=self.bucket_name,
|
131
|
+
Key=self.key,
|
132
|
+
UploadId=self.upload_id,
|
133
|
+
MultipartUpload={"Parts": self.parts}
|
134
|
+
)
|
135
|
+
|
136
|
+
@property
|
137
|
+
def public_url(self) -> str:
|
138
|
+
"""Get the public URL for the S3 object."""
|
139
|
+
return f"{self.S3_HOST}/{self.bucket_name}/{self.key}"
|
140
|
+
|
141
|
+
def generate_presigned_url(self, expires: int = 600) -> str:
|
142
|
+
"""
|
143
|
+
Generate a presigned URL for the S3 object.
|
144
|
+
|
145
|
+
Args:
|
146
|
+
expires: Expiration time in seconds
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
Presigned URL
|
150
|
+
"""
|
151
|
+
return S3.client.generate_presigned_url(
|
152
|
+
'get_object',
|
153
|
+
ExpiresIn=expires,
|
154
|
+
Params={'Bucket': self.bucket_name, 'Key': self.key}
|
155
|
+
)
|
156
|
+
|
157
|
+
def download(self, file_obj: Optional[BinaryIO] = None) -> BinaryIO:
|
158
|
+
"""
|
159
|
+
Download the S3 object.
|
160
|
+
|
161
|
+
Args:
|
162
|
+
file_obj: Optional file-like object to download to
|
163
|
+
|
164
|
+
Returns:
|
165
|
+
File-like object containing the downloaded data
|
166
|
+
"""
|
167
|
+
if file_obj is None:
|
168
|
+
file_obj = tempfile.NamedTemporaryFile()
|
169
|
+
self.object.download_fileobj(file_obj)
|
170
|
+
if hasattr(file_obj, 'seek'):
|
171
|
+
file_obj.seek(0)
|
172
|
+
return file_obj
|
173
|
+
|
174
|
+
def delete(self) -> None:
|
175
|
+
"""Delete the S3 object."""
|
176
|
+
self.object.delete()
|
177
|
+
|
178
|
+
def _prepare_file(self, file_obj: Union[str, BinaryIO]) -> BinaryIO:
|
179
|
+
"""
|
180
|
+
Prepare a file object for upload.
|
181
|
+
|
182
|
+
Args:
|
183
|
+
file_obj: File path or file-like object
|
184
|
+
|
185
|
+
Returns:
|
186
|
+
A file-like object ready for upload
|
187
|
+
"""
|
188
|
+
if hasattr(file_obj, "read"):
|
189
|
+
return io.BytesIO(file_obj.read().encode() if isinstance(file_obj.read(), str) else file_obj.read())
|
190
|
+
|
191
|
+
try:
|
192
|
+
return open(str(file_obj), "rb")
|
193
|
+
except (IOError, TypeError):
|
194
|
+
pass
|
195
|
+
|
196
|
+
return file_obj
|
197
|
+
|
198
|
+
|
199
|
+
|
200
|
+
# Utility functions for common S3 operations
|
201
|
+
|
202
|
+
def upload(url: str, file_obj: Union[str, BinaryIO], background: bool = False) -> None:
|
203
|
+
"""Upload a file to S3."""
|
204
|
+
S3Item(url).upload(file_obj, background)
|
205
|
+
|
206
|
+
|
207
|
+
def view_url_noexpire(url: str, is_secure: bool = False) -> str:
|
208
|
+
"""Get a public URL for an S3 object."""
|
209
|
+
return S3Item(url).public_url
|
210
|
+
|
211
|
+
|
212
|
+
def view_url(url: str, expires: Optional[int] = 600, is_secure: bool = True) -> str:
|
213
|
+
"""
|
214
|
+
Get a URL for an S3 object.
|
215
|
+
|
216
|
+
Args:
|
217
|
+
url: S3 URL
|
218
|
+
expires: Expiration time in seconds, or None for a public URL
|
219
|
+
is_secure: Whether to use HTTPS (currently unused)
|
220
|
+
|
221
|
+
Returns:
|
222
|
+
URL for the S3 object
|
223
|
+
"""
|
224
|
+
if expires is None:
|
225
|
+
return view_url_noexpire(url, is_secure)
|
226
|
+
return S3Item(url).generate_presigned_url(expires)
|
227
|
+
|
228
|
+
|
229
|
+
def exists(url: str) -> bool:
|
230
|
+
"""Check if an S3 object exists."""
|
231
|
+
return S3Item(url).exists
|
232
|
+
|
233
|
+
|
234
|
+
def get_file(url: str, file_obj: Optional[BinaryIO] = None) -> BinaryIO:
|
235
|
+
"""Download an S3 object to a file."""
|
236
|
+
return S3Item(url).download(file_obj)
|
237
|
+
|
238
|
+
|
239
|
+
def delete(url: str) -> None:
|
240
|
+
"""
|
241
|
+
Delete an S3 object or prefix.
|
242
|
+
|
243
|
+
If the URL ends with /, all objects under that prefix are deleted.
|
244
|
+
"""
|
245
|
+
if url.endswith("/"):
|
246
|
+
parsed_url = urlparse(url)
|
247
|
+
prefix = parsed_url.path.lstrip("/")
|
248
|
+
bucket_name = parsed_url.netloc
|
249
|
+
|
250
|
+
response = S3.client.list_objects_v2(
|
251
|
+
Bucket=bucket_name,
|
252
|
+
Prefix=prefix,
|
253
|
+
MaxKeys=100
|
254
|
+
)
|
255
|
+
|
256
|
+
if 'Contents' in response:
|
257
|
+
for obj in response['Contents']:
|
258
|
+
S3.client.delete_object(
|
259
|
+
Bucket=bucket_name,
|
260
|
+
Key=obj['Key']
|
261
|
+
)
|
262
|
+
else:
|
263
|
+
S3Item(url).delete()
|
264
|
+
|
265
|
+
|
266
|
+
def path(url: str) -> str:
|
267
|
+
"""Extract the path component from a URL."""
|
268
|
+
return urlparse(url).path
|
File without changes
|
mojo/helpers/cron.py
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
import datetime
|
2
|
+
from typing import Callable, List, Dict, Any
|
3
|
+
from mojo.decorators.cron import schedule
|
4
|
+
|
5
|
+
def run_now() -> None:
|
6
|
+
"""
|
7
|
+
Execute the scheduled functions that match the current time.
|
8
|
+
|
9
|
+
Retrieves the list of functions scheduled to run at the current
|
10
|
+
date and time, and executes each of them.
|
11
|
+
"""
|
12
|
+
functions_to_run = find_scheduled_functions()
|
13
|
+
for func in functions_to_run:
|
14
|
+
func()
|
15
|
+
|
16
|
+
def find_scheduled_functions() -> List[Callable]:
|
17
|
+
"""
|
18
|
+
Find all functions that are scheduled to run at the current time.
|
19
|
+
|
20
|
+
Returns:
|
21
|
+
List[Callable]: A list of callable functions that match the
|
22
|
+
current date and time according to their cron specifications.
|
23
|
+
"""
|
24
|
+
if not hasattr(schedule, 'scheduled_functions'):
|
25
|
+
return []
|
26
|
+
|
27
|
+
now = datetime.datetime.now()
|
28
|
+
matching_funcs = []
|
29
|
+
|
30
|
+
for cron_spec in schedule.scheduled_functions:
|
31
|
+
if match_time(now, cron_spec):
|
32
|
+
matching_funcs.append(cron_spec['func'])
|
33
|
+
|
34
|
+
return matching_funcs
|
35
|
+
|
36
|
+
def match_time(current_time: datetime.datetime, cron_spec: Dict[str, Any]) -> bool:
|
37
|
+
"""
|
38
|
+
Determine if a given time matches a cron-like specification.
|
39
|
+
|
40
|
+
Args:
|
41
|
+
current_time (datetime.datetime): The current date and time.
|
42
|
+
cron_spec (Dict[str, Any]): A dictionary containing the cron
|
43
|
+
specifications to match against.
|
44
|
+
|
45
|
+
Returns:
|
46
|
+
bool: True if the current time matches the cron specification,
|
47
|
+
False otherwise.
|
48
|
+
"""
|
49
|
+
cron_field = {
|
50
|
+
'minutes': current_time.minute,
|
51
|
+
'hours': current_time.hour,
|
52
|
+
'days': current_time.day,
|
53
|
+
'months': current_time.month,
|
54
|
+
'weekdays': current_time.weekday()
|
55
|
+
}
|
56
|
+
|
57
|
+
for field, time_value in cron_field.items():
|
58
|
+
if not matches(cron_spec[field], time_value):
|
59
|
+
return False
|
60
|
+
return True
|
61
|
+
|
62
|
+
def matches(cron_value: str, actual_value: int) -> bool:
|
63
|
+
"""
|
64
|
+
Check if a specific time value matches the corresponding cron pattern.
|
65
|
+
|
66
|
+
Args:
|
67
|
+
cron_value (str): The cron pattern to match, which can be '*'
|
68
|
+
or a list of comma-separated values.
|
69
|
+
actual_value (int): The actual time value to compare.
|
70
|
+
|
71
|
+
Returns:
|
72
|
+
bool: True if the actual value matches the cron pattern,
|
73
|
+
False otherwise.
|
74
|
+
"""
|
75
|
+
if cron_value == '*':
|
76
|
+
return True
|
77
|
+
cron_values = cron_value.split(',')
|
78
|
+
|
79
|
+
return str(actual_value) in cron_values
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import json
|
2
|
+
from base64 import b64encode, b64decode
|
3
|
+
from Crypto.Cipher import AES
|
4
|
+
from Crypto.Protocol.KDF import PBKDF2
|
5
|
+
from Crypto.Random import get_random_bytes
|
6
|
+
from objict import objict
|
7
|
+
import mojo.errors
|
8
|
+
|
9
|
+
PBKDF2_ITERATIONS = 100_000
|
10
|
+
SALT_LENGTH = 16
|
11
|
+
NONCE_LENGTH = 12
|
12
|
+
TAG_LENGTH = 16
|
13
|
+
|
14
|
+
|
15
|
+
def encrypt(data, password):
|
16
|
+
if isinstance(data, dict):
|
17
|
+
data = json.dumps(data)
|
18
|
+
if not isinstance(data, str):
|
19
|
+
raise mojo.errors.ValueException("Data must be a string or dictionary")
|
20
|
+
|
21
|
+
data_bytes = data.encode('utf-8')
|
22
|
+
salt = get_random_bytes(SALT_LENGTH)
|
23
|
+
key = derive_key(password, salt)
|
24
|
+
cipher = AES.new(key, AES.MODE_GCM, nonce=get_random_bytes(NONCE_LENGTH))
|
25
|
+
|
26
|
+
ciphertext, tag = cipher.encrypt_and_digest(data_bytes)
|
27
|
+
|
28
|
+
# Final payload: [salt | nonce | tag | ciphertext]
|
29
|
+
payload = salt + cipher.nonce + tag + ciphertext
|
30
|
+
return b64encode(payload).decode('utf-8')
|
31
|
+
|
32
|
+
def decrypt(enc_data_b64, password, ignore_errors=True):
|
33
|
+
raw = b64decode(enc_data_b64)
|
34
|
+
|
35
|
+
salt = raw[:SALT_LENGTH]
|
36
|
+
nonce = raw[SALT_LENGTH:SALT_LENGTH + NONCE_LENGTH]
|
37
|
+
tag = raw[SALT_LENGTH + NONCE_LENGTH:SALT_LENGTH + NONCE_LENGTH + TAG_LENGTH]
|
38
|
+
ciphertext = raw[SALT_LENGTH + NONCE_LENGTH + TAG_LENGTH:]
|
39
|
+
|
40
|
+
key = derive_key(password, salt)
|
41
|
+
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
|
42
|
+
|
43
|
+
if ignore_errors:
|
44
|
+
try:
|
45
|
+
decrypted = cipher.decrypt_and_verify(ciphertext, tag)
|
46
|
+
except ValueError:
|
47
|
+
return None
|
48
|
+
else:
|
49
|
+
decrypted = cipher.decrypt_and_verify(ciphertext, tag)
|
50
|
+
|
51
|
+
decrypted_str = decrypted.decode('utf-8')
|
52
|
+
|
53
|
+
try:
|
54
|
+
return objict.from_json(decrypted_str)
|
55
|
+
except Exception:
|
56
|
+
return decrypted_str
|
57
|
+
|
58
|
+
|
59
|
+
def derive_key(password, salt, key_length=32):
|
60
|
+
return PBKDF2(password, salt, dkLen=key_length, count=PBKDF2_ITERATIONS)
|
@@ -0,0 +1,59 @@
|
|
1
|
+
from Crypto.Hash import SHA256
|
2
|
+
from Crypto.Random import get_random_bytes
|
3
|
+
import hmac
|
4
|
+
import hashlib
|
5
|
+
from django.conf import settings
|
6
|
+
|
7
|
+
|
8
|
+
def hash(value, salt=settings.SECRET_KEY):
|
9
|
+
"""
|
10
|
+
Returns a SHA-256 hash of the input value (string, int, or dict), optionally salted.
|
11
|
+
|
12
|
+
:param value: str, int, or dict - the input to be hashed
|
13
|
+
:param salt: Optional[str or bytes] - a salt to strengthen the hash
|
14
|
+
:return: str - the hex digest of the hash
|
15
|
+
"""
|
16
|
+
if isinstance(value, dict):
|
17
|
+
# Sort the dictionary and prepare a string representation
|
18
|
+
value_str = str(sorted(value.items())).encode('utf-8')
|
19
|
+
elif isinstance(value, (str, int)):
|
20
|
+
value_str = str(value).encode('utf-8')
|
21
|
+
else:
|
22
|
+
raise TypeError("Only strings, integers, or dictionaries are allowed.")
|
23
|
+
|
24
|
+
# Use provided salt or generate one
|
25
|
+
if salt is None:
|
26
|
+
salt = get_random_bytes(16)
|
27
|
+
elif isinstance(salt, str):
|
28
|
+
salt = salt.encode('utf-8')
|
29
|
+
|
30
|
+
# Combine salt and value
|
31
|
+
hasher = hashlib.sha256()
|
32
|
+
hasher.update(salt + value_str)
|
33
|
+
return hasher.hexdigest()
|
34
|
+
|
35
|
+
|
36
|
+
def hash_digits(digits, secret_key):
|
37
|
+
"""Hashes the digits using a derived salt without storing it."""
|
38
|
+
salt = derive_salt(digits, secret_key)
|
39
|
+
hash_obj = hashlib.sha256(salt + digits.encode())
|
40
|
+
return hash_obj.hexdigest()
|
41
|
+
|
42
|
+
|
43
|
+
def derive_salt(digits, secret_key):
|
44
|
+
"""Derives a salt from the last 8 digits of the DIGITs using HMAC."""
|
45
|
+
last_8_digits = digits[-8:]
|
46
|
+
if isinstance(secret_key, str): # Ensure secret_key is bytes
|
47
|
+
secret_key = secret_key.encode()
|
48
|
+
return hmac.new(secret_key, last_8_digits.encode(), hashlib.sha256).digest()[:16] # Use first 16 bytes
|
49
|
+
|
50
|
+
|
51
|
+
def hash_to_hex(input_string):
|
52
|
+
if not isinstance(input_string, str):
|
53
|
+
raise ValueError("Input must be a string")
|
54
|
+
# Create a new SHA-256 hasher
|
55
|
+
hasher = hashlib.sha256()
|
56
|
+
# Update the hasher with the input string encoded to bytes
|
57
|
+
hasher.update(input_string.encode('utf-8'))
|
58
|
+
# Return the hexadecimal representation of the hash
|
59
|
+
return hasher.hexdigest()
|
@@ -0,0 +1 @@
|
|
1
|
+
from .hybrid import PrivatePublicEncryption
|
@@ -0,0 +1,97 @@
|
|
1
|
+
import base64
|
2
|
+
import binascii
|
3
|
+
import json
|
4
|
+
from nacl.public import PrivateKey, PublicKey, SealedBox
|
5
|
+
from nacl.encoding import Base64Encoder
|
6
|
+
from nacl.exceptions import CryptoError
|
7
|
+
|
8
|
+
|
9
|
+
class PrivatePublicEncryption:
|
10
|
+
def __init__(self, private_key=None, public_key=None, private_key_file=None, public_key_file=None):
|
11
|
+
self.private_key = self._load_key(private_key, private_key_file, is_private=True)
|
12
|
+
self.public_key = self._load_key(public_key, public_key_file, is_private=False)
|
13
|
+
|
14
|
+
if self.private_key and not self.public_key:
|
15
|
+
self.public_key = self.private_key.public_key
|
16
|
+
|
17
|
+
def _load_key(self, key, key_file, is_private):
|
18
|
+
if key_file:
|
19
|
+
with open(key_file, 'r') as f:
|
20
|
+
key = f.read().strip()
|
21
|
+
|
22
|
+
if key:
|
23
|
+
if isinstance(key, str):
|
24
|
+
key_bytes = Base64Encoder.decode(key)
|
25
|
+
elif isinstance(key, bytes):
|
26
|
+
key_bytes = key
|
27
|
+
else:
|
28
|
+
raise ValueError("Key must be a base64 string or bytes")
|
29
|
+
|
30
|
+
return PrivateKey(key_bytes) if is_private else PublicKey(key_bytes)
|
31
|
+
return None
|
32
|
+
|
33
|
+
def generate_public_key(self, make_new=False):
|
34
|
+
if self.public_key is None or make_new:
|
35
|
+
if not self.private_key:
|
36
|
+
self.private_key = PrivateKey.generate()
|
37
|
+
self.public_key = self.private_key.public_key
|
38
|
+
return self.public_key
|
39
|
+
|
40
|
+
def encrypt(self, data):
|
41
|
+
self.generate_public_key()
|
42
|
+
return self.encrypt_to_b64(data)
|
43
|
+
|
44
|
+
def decrypt(self, data, as_string=True):
|
45
|
+
return self.decrypt_from_b64(data, as_string)
|
46
|
+
|
47
|
+
def encrypt_to_b64(self, data):
|
48
|
+
encrypted_bytes = encrypt_with_public_key(data, self.public_key)
|
49
|
+
return base64.b64encode(encrypted_bytes).decode('utf-8')
|
50
|
+
|
51
|
+
def decrypt_from_b64(self, data, as_string=True):
|
52
|
+
decoded = base64.b64decode(data)
|
53
|
+
return decrypt_with_private_key(decoded, self.private_key, as_string)
|
54
|
+
|
55
|
+
def encrypt_to_hex(self, data):
|
56
|
+
encrypted_bytes = encrypt_with_public_key(data, self.public_key)
|
57
|
+
return binascii.hexlify(encrypted_bytes).decode('utf-8')
|
58
|
+
|
59
|
+
def decrypt_from_hex(self, data, as_string=True):
|
60
|
+
decoded = binascii.unhexlify(data)
|
61
|
+
return decrypt_with_private_key(decoded, self.private_key, as_string)
|
62
|
+
|
63
|
+
|
64
|
+
def generate_private_key():
|
65
|
+
return PrivateKey.generate()
|
66
|
+
|
67
|
+
|
68
|
+
def generate_public_key(private_key):
|
69
|
+
if isinstance(private_key, str):
|
70
|
+
private_key = PrivateKey(Base64Encoder.decode(private_key))
|
71
|
+
return private_key.public_key
|
72
|
+
|
73
|
+
|
74
|
+
def encrypt_with_public_key(data, public_key):
|
75
|
+
if isinstance(public_key, str):
|
76
|
+
public_key = PublicKey(Base64Encoder.decode(public_key))
|
77
|
+
|
78
|
+
if isinstance(data, (dict, list)):
|
79
|
+
data = json.dumps(data)
|
80
|
+
if isinstance(data, str):
|
81
|
+
data = data.encode('utf-8')
|
82
|
+
|
83
|
+
sealed_box = SealedBox(public_key)
|
84
|
+
return sealed_box.encrypt(data)
|
85
|
+
|
86
|
+
|
87
|
+
def decrypt_with_private_key(data, private_key, as_string=True):
|
88
|
+
if isinstance(private_key, str):
|
89
|
+
private_key = PrivateKey(Base64Encoder.decode(private_key))
|
90
|
+
|
91
|
+
sealed_box = SealedBox(private_key)
|
92
|
+
try:
|
93
|
+
decrypted = sealed_box.decrypt(data)
|
94
|
+
decoded = decrypted.decode('utf-8')
|
95
|
+
return json.loads(decoded) if as_string else decrypted
|
96
|
+
except (CryptoError, json.JSONDecodeError):
|
97
|
+
return decrypted.decode('utf-8') if as_string else decrypted
|
@@ -0,0 +1,104 @@
|
|
1
|
+
from Crypto import Random
|
2
|
+
from Crypto.PublicKey import RSA
|
3
|
+
from Crypto.Cipher import AES, PKCS1_OAEP
|
4
|
+
import base64
|
5
|
+
import binascii
|
6
|
+
import json
|
7
|
+
from io import BytesIO
|
8
|
+
from contextlib import closing
|
9
|
+
|
10
|
+
|
11
|
+
class PrivatePublicEncryption:
|
12
|
+
def __init__(self, private_key=None, public_key=None, private_key_file=None, public_key_file=None):
|
13
|
+
self.private_key = self._load_key(private_key, private_key_file)
|
14
|
+
self.public_key = self._load_key(public_key, public_key_file)
|
15
|
+
|
16
|
+
def _load_key(self, key, key_file):
|
17
|
+
if key_file:
|
18
|
+
with open(key_file, 'r') as f:
|
19
|
+
key = f.read()
|
20
|
+
if isinstance(key, str):
|
21
|
+
return RSA.import_key(key)
|
22
|
+
return key
|
23
|
+
|
24
|
+
def generate_public_key(self, make_new=False):
|
25
|
+
if self.public_key is None or make_new:
|
26
|
+
self.public_key = generate_public_key(self.private_key)
|
27
|
+
return self.public_key
|
28
|
+
|
29
|
+
def encrypt(self, data):
|
30
|
+
self.generate_public_key()
|
31
|
+
return self.encrypt_to_b64(data)
|
32
|
+
|
33
|
+
def decrypt(self, data, as_string=True):
|
34
|
+
return self.decrypt_from_b64(data, as_string)
|
35
|
+
|
36
|
+
def encrypt_to_b64(self, data):
|
37
|
+
ebytes = encrypt_with_public_key(data, self.public_key)
|
38
|
+
return base64.b64encode(ebytes).decode('utf-8')
|
39
|
+
|
40
|
+
def decrypt_from_b64(self, data, as_string=True):
|
41
|
+
data = base64.b64decode(data)
|
42
|
+
return decrypt_with_private_key(data, self.private_key, as_string)
|
43
|
+
|
44
|
+
def encrypt_to_hex(self, data):
|
45
|
+
ebytes = encrypt_with_public_key(data, self.public_key)
|
46
|
+
return binascii.hexlify(ebytes).decode('utf-8')
|
47
|
+
|
48
|
+
def decrypt_from_hex(self, data, as_string=True):
|
49
|
+
data = binascii.unhexlify(data)
|
50
|
+
return decrypt_with_private_key(data, self.private_key, as_string)
|
51
|
+
|
52
|
+
|
53
|
+
def generate_private_key(size=2048):
|
54
|
+
return RSA.generate(size)
|
55
|
+
|
56
|
+
|
57
|
+
def generate_public_key(private_key):
|
58
|
+
if isinstance(private_key, str):
|
59
|
+
private_key = RSA.import_key(private_key)
|
60
|
+
return private_key.publickey()
|
61
|
+
|
62
|
+
|
63
|
+
def encrypt_with_public_key(data, public_key):
|
64
|
+
if isinstance(public_key, str):
|
65
|
+
public_key = RSA.import_key(public_key)
|
66
|
+
|
67
|
+
if isinstance(data, (dict, list)):
|
68
|
+
data = json.dumps(data)
|
69
|
+
|
70
|
+
if isinstance(data, str):
|
71
|
+
data = data.encode('utf-8')
|
72
|
+
|
73
|
+
session_key = Random.get_random_bytes(16)
|
74
|
+
cipher_rsa = PKCS1_OAEP.new(public_key)
|
75
|
+
enc_session_key = cipher_rsa.encrypt(session_key)
|
76
|
+
|
77
|
+
cipher_aes = AES.new(session_key, AES.MODE_EAX)
|
78
|
+
ciphertext, tag = cipher_aes.encrypt_and_digest(data)
|
79
|
+
|
80
|
+
with closing(BytesIO()) as output:
|
81
|
+
for x in (enc_session_key, cipher_aes.nonce, tag, ciphertext):
|
82
|
+
output.write(x)
|
83
|
+
return output.getvalue()
|
84
|
+
|
85
|
+
|
86
|
+
def decrypt_with_private_key(data, private_key, as_string=True):
|
87
|
+
if isinstance(private_key, str):
|
88
|
+
private_key = RSA.import_key(private_key)
|
89
|
+
if isinstance(data, str):
|
90
|
+
data = data.encode('utf-8')
|
91
|
+
if isinstance(data, bytes):
|
92
|
+
data = BytesIO(data)
|
93
|
+
|
94
|
+
enc_session_key, nonce, tag, ciphertext = (
|
95
|
+
data.read(x) for x in (private_key.size_in_bytes(), 16, 16, -1)
|
96
|
+
)
|
97
|
+
|
98
|
+
cipher_rsa = PKCS1_OAEP.new(private_key)
|
99
|
+
session_key = cipher_rsa.decrypt(enc_session_key)
|
100
|
+
|
101
|
+
cipher_aes = AES.new(session_key, AES.MODE_EAX, nonce)
|
102
|
+
decrypted_data = cipher_aes.decrypt_and_verify(ciphertext, tag)
|
103
|
+
|
104
|
+
return decrypted_data.decode('utf-8') if as_string else decrypted_data
|