vesta-web 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vesta/__init__.py +486 -0
- vesta/db/UNIAUTH.sql +58 -0
- vesta/db/db_service.py +253 -0
- vesta/emptyProject/.gitignore +16 -0
- vesta/emptyProject/.gitlab-ci.yml +12 -0
- vesta/emptyProject/CONTRIBUTING.md +45 -0
- vesta/emptyProject/LICENSE.md +3 -0
- vesta/emptyProject/README.md +44 -0
- vesta/emptyProject/crons/exemple.py +13 -0
- vesta/emptyProject/db/schema.sql +0 -0
- vesta/emptyProject/install.sh +17 -0
- vesta/emptyProject/mailing/mailReset.html +1 -0
- vesta/emptyProject/mailing/mailVerif.html +1 -0
- vesta/emptyProject/misc/nginx_local +15 -0
- vesta/emptyProject/misc/nginx_prod +29 -0
- vesta/emptyProject/misc/nginx_prod_ws +29 -0
- vesta/emptyProject/misc/vesta.service +11 -0
- vesta/emptyProject/requirements.txt +0 -0
- vesta/emptyProject/server_static.ini +6 -0
- vesta/emptyProject/server_static.py +15 -0
- vesta/emptyProject/server_static_orm.ini +13 -0
- vesta/emptyProject/server_static_orm.py +15 -0
- vesta/emptyProject/server_vesta.ini +27 -0
- vesta/emptyProject/server_vesta.py +17 -0
- vesta/emptyProject/server_vesta_ws.ini +31 -0
- vesta/emptyProject/server_vesta_ws.py +56 -0
- vesta/emptyProject/static/home/auth.css +101 -0
- vesta/emptyProject/static/home/auth.html +38 -0
- vesta/emptyProject/static/home/auth.js +159 -0
- vesta/emptyProject/static/home/reset.html +33 -0
- vesta/emptyProject/static/home/verif.html +33 -0
- vesta/emptyProject/static/main.html +14 -0
- vesta/emptyProject/static/main.mjs +9 -0
- vesta/emptyProject/static/mobileUiManifest.mjs +3 -0
- vesta/emptyProject/static/style.css +0 -0
- vesta/emptyProject/static/translations/en.mjs +2 -0
- vesta/emptyProject/static/translations/fr.mjs +2 -0
- vesta/emptyProject/static/translations/translation.mjs +51 -0
- vesta/emptyProject/static/ws/onMessage.mjs +21 -0
- vesta/emptyProject/tests/example/foo.py +7 -0
- vesta/http/baseServer.py +257 -0
- vesta/http/error.py +5 -0
- vesta/http/redirect.py +5 -0
- vesta/http/response.py +85 -0
- vesta/mailing/mailing_service.py +126 -0
- vesta/scripts/initDB.py +52 -0
- vesta/scripts/install.py +76 -0
- vesta/scripts/testsRun.py +83 -0
- vesta/scripts/utils.py +84 -0
- vesta/scripts/vesta.py +225 -0
- vesta_web-1.1.0.dist-info/METADATA +55 -0
- vesta_web-1.1.0.dist-info/RECORD +55 -0
- vesta_web-1.1.0.dist-info/WHEEL +4 -0
- vesta_web-1.1.0.dist-info/entry_points.txt +2 -0
- vesta_web-1.1.0.dist-info/licenses/LICENSE.md +5 -0
vesta/http/baseServer.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import fastwsgi
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
import json
|
|
7
|
+
import hashlib
|
|
8
|
+
import base64
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import urllib
|
|
12
|
+
|
|
13
|
+
#requests modules
|
|
14
|
+
import multipart as mp
|
|
15
|
+
from io import BytesIO
|
|
16
|
+
|
|
17
|
+
from configparser import ConfigParser
|
|
18
|
+
|
|
19
|
+
from vesta.http import response
|
|
20
|
+
from vesta.http import error
|
|
21
|
+
from vesta.http import redirect
|
|
22
|
+
from vesta.db import db_service as db
|
|
23
|
+
Response = response.Response
|
|
24
|
+
HTTPRedirect = redirect.HTTPRedirect
|
|
25
|
+
HTTPError = error.HTTPError
|
|
26
|
+
|
|
27
|
+
RE_URL = re.compile(r"[\&]")
|
|
28
|
+
RE_PARAM = re.compile(r"[\=]")
|
|
29
|
+
|
|
30
|
+
routes = {}
|
|
31
|
+
|
|
32
|
+
from colorama import Fore, Style
|
|
33
|
+
from colorama import init as colorama_init
|
|
34
|
+
colorama_init()
|
|
35
|
+
|
|
36
|
+
class BaseServer:
|
|
37
|
+
features = {}
|
|
38
|
+
|
|
39
|
+
def __init__(self, path, configFile, noStart=False):
|
|
40
|
+
print(Fore.GREEN,"[INFO] starting Vesta server...")
|
|
41
|
+
self.path = path
|
|
42
|
+
|
|
43
|
+
self.importConf(configFile)
|
|
44
|
+
|
|
45
|
+
if noStart:
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
self.start()
|
|
49
|
+
|
|
50
|
+
#----------------------------HTTP SERVER------------------------------------
|
|
51
|
+
def expose(func):
|
|
52
|
+
def wrapper(self, *args, **kwargs):
|
|
53
|
+
|
|
54
|
+
res = func(self, *args, **kwargs)
|
|
55
|
+
# print("[DEBUG] res : ", res)
|
|
56
|
+
|
|
57
|
+
self.response.ok()
|
|
58
|
+
if res:
|
|
59
|
+
|
|
60
|
+
return res.encode()
|
|
61
|
+
else:
|
|
62
|
+
return "".encode()
|
|
63
|
+
|
|
64
|
+
name = func.__name__
|
|
65
|
+
if func.__name__ == "index":
|
|
66
|
+
name = "/"
|
|
67
|
+
elif func.__name__ == "default":
|
|
68
|
+
name = "default"
|
|
69
|
+
else:
|
|
70
|
+
name = "/" + func.__name__
|
|
71
|
+
|
|
72
|
+
routes[name] = {"params": inspect.signature(func).parameters, "target": wrapper}
|
|
73
|
+
return wrapper
|
|
74
|
+
|
|
75
|
+
def saveFile(self, content, name="",ext=None, category=None):
|
|
76
|
+
content = content.split(",")
|
|
77
|
+
extension = content[0].split("/")[1].split(";")[0]
|
|
78
|
+
content = base64.b64decode(content[1])
|
|
79
|
+
|
|
80
|
+
if not name :
|
|
81
|
+
hash_object = hashlib.sha256(content)
|
|
82
|
+
hex_dig = hash_object.hexdigest()
|
|
83
|
+
|
|
84
|
+
name = hex_dig
|
|
85
|
+
|
|
86
|
+
prefix = self.path + "/static/attachments/"
|
|
87
|
+
if category:
|
|
88
|
+
name = category + "/" + name
|
|
89
|
+
if ext :
|
|
90
|
+
name = name + "." + ext
|
|
91
|
+
|
|
92
|
+
with open(prefix+name, 'wb') as f:
|
|
93
|
+
f.write(content)
|
|
94
|
+
return name
|
|
95
|
+
|
|
96
|
+
def parseCookies(self, cookieStr):
|
|
97
|
+
if not cookieStr:
|
|
98
|
+
return
|
|
99
|
+
cookies = {}
|
|
100
|
+
for cookie in cookieStr.split(';'):
|
|
101
|
+
key, value = cookie.split('=')
|
|
102
|
+
cookies[key.strip()] = value
|
|
103
|
+
return cookies
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def parseRequest(self,environ):
|
|
107
|
+
self.response.cookies = self.parseCookies(environ.get('HTTP_COOKIE'))
|
|
108
|
+
|
|
109
|
+
if environ.get('CONTENT_TYPE'):
|
|
110
|
+
content_type = environ.get('CONTENT_TYPE').strip().split(";")
|
|
111
|
+
else:
|
|
112
|
+
content_type = ["text/html"]
|
|
113
|
+
|
|
114
|
+
args = {}
|
|
115
|
+
if environ.get('QUERY_STRING'):
|
|
116
|
+
query = re.split(RE_URL, environ['QUERY_STRING'])
|
|
117
|
+
for i in range(0, len(query)):
|
|
118
|
+
query[i] = re.split(RE_PARAM, query[i])
|
|
119
|
+
args[query[i][0]] = urllib.parse.unquote(query[i][1])
|
|
120
|
+
if content_type[0] == "multipart/form-data":
|
|
121
|
+
length = int(environ.get('CONTENT_LENGTH'))
|
|
122
|
+
body = environ['wsgi.input'].read(length)
|
|
123
|
+
sep = content_type[1].split("=")[1]
|
|
124
|
+
body = mp.MultipartParser(BytesIO(body), sep.encode('utf-8'))
|
|
125
|
+
for part in body.parts():
|
|
126
|
+
args[part.name] = part.value
|
|
127
|
+
if content_type[0] == "application/json":
|
|
128
|
+
length = int(environ.get('CONTENT_LENGTH'))
|
|
129
|
+
body = environ['wsgi.input'].read(length)
|
|
130
|
+
body = json.loads(body)
|
|
131
|
+
for key in body:
|
|
132
|
+
args[key] = body[key]
|
|
133
|
+
|
|
134
|
+
return args
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def tryDefault(self, environ, target):
|
|
138
|
+
print(Fore.WHITE,"[INFO] Vesta - using default route")
|
|
139
|
+
|
|
140
|
+
args = self.parseRequest(environ)
|
|
141
|
+
args["target"] = target
|
|
142
|
+
try:
|
|
143
|
+
return routes["default"]["target"](self, **args)
|
|
144
|
+
except (HTTPError, HTTPRedirect):
|
|
145
|
+
return self.response.encode()
|
|
146
|
+
except Exception as e:
|
|
147
|
+
return self.handleUnexpected(e)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def onrequest(self, environ, start_response):
|
|
151
|
+
self.response = Response(start_response=start_response)
|
|
152
|
+
print(Fore.WHITE,"[INFO] Vesta - request received :'", str(environ['PATH_INFO']) + "'" + " with "+ str(environ.get('QUERY_STRING')))
|
|
153
|
+
target = environ['PATH_INFO']
|
|
154
|
+
|
|
155
|
+
if routes.get(target):
|
|
156
|
+
args = self.parseRequest(environ)
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
if len(args) == 0:
|
|
160
|
+
return routes[target]["target"](self)
|
|
161
|
+
return routes[target]["target"](self, **args)
|
|
162
|
+
except (HTTPError, HTTPRedirect):
|
|
163
|
+
return self.response.encode()
|
|
164
|
+
except Exception as e:
|
|
165
|
+
return self.handleUnexpected(e)
|
|
166
|
+
else:
|
|
167
|
+
if routes.get("default"):
|
|
168
|
+
return self.tryDefault(environ, target)
|
|
169
|
+
self.response.code = 404
|
|
170
|
+
self.response.ok()
|
|
171
|
+
return self.response.encode()
|
|
172
|
+
|
|
173
|
+
def handleUnexpected(self, e):
|
|
174
|
+
print(Fore.RED,"[ERROR] Vesta - UNEXPECTED ERROR :", e)
|
|
175
|
+
self.response.code = 500
|
|
176
|
+
self.response.ok()
|
|
177
|
+
self.response.content = str(e)
|
|
178
|
+
tb = e.__traceback__
|
|
179
|
+
while tb is not None:
|
|
180
|
+
frame = tb.tb_frame
|
|
181
|
+
lineno = tb.tb_lineno
|
|
182
|
+
filename = frame.f_code.co_filename
|
|
183
|
+
funcname = frame.f_code.co_name
|
|
184
|
+
print(f' File "{filename}", line {lineno}, in {funcname}')
|
|
185
|
+
if self.features.get("debug"):
|
|
186
|
+
self.response.content += f' File "{filename}", line {lineno}, in {funcname}'
|
|
187
|
+
tb = tb.tb_next
|
|
188
|
+
return self.response.encode()
|
|
189
|
+
|
|
190
|
+
def onStart(self):
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
#--------------------------GENERAL USE METHODS------------------------------
|
|
194
|
+
|
|
195
|
+
def importConf(self, configFile):
|
|
196
|
+
self.config = ConfigParser()
|
|
197
|
+
try:
|
|
198
|
+
self.config.read(self.path + configFile)
|
|
199
|
+
print(Fore.GREEN,"[INFO] Vesta - config at " + self.path + configFile + " loaded")
|
|
200
|
+
except Exception:
|
|
201
|
+
print(Fore.RED,"[ERROR] Vesta - Please create a config file")
|
|
202
|
+
|
|
203
|
+
def start(self):
|
|
204
|
+
self.fileCache = {}
|
|
205
|
+
|
|
206
|
+
if self.features.get("errors"):
|
|
207
|
+
for code, page in self.features["errors"].items():
|
|
208
|
+
Response.ERROR_PAGES[code] = self.path + page
|
|
209
|
+
|
|
210
|
+
if self.features.get("orm") == True:
|
|
211
|
+
self.db = db.DB(user=self.config.get('DB', 'DB_USER'), password=self.config.get('DB', 'DB_PASSWORD'),
|
|
212
|
+
host=self.config.get('DB', 'DB_HOST'), port=int(self.config.get('DB', 'DB_PORT')),
|
|
213
|
+
db=self.config.get('DB', 'DB_NAME'))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
self.onStart()
|
|
217
|
+
|
|
218
|
+
fastwsgi.server.nowait = 1
|
|
219
|
+
fastwsgi.server.hook_sigint = 1
|
|
220
|
+
|
|
221
|
+
print(Fore.GREEN,"[INFO] Vesta - server running on PID:", os.getpid())
|
|
222
|
+
fastwsgi.server.init(app=self.onrequest, host=self.config.get('server', 'IP'),
|
|
223
|
+
port=int(self.config.get('server', 'PORT')))
|
|
224
|
+
while True:
|
|
225
|
+
code = fastwsgi.server.run()
|
|
226
|
+
if code != 0:
|
|
227
|
+
break
|
|
228
|
+
time.sleep(0)
|
|
229
|
+
self.close()
|
|
230
|
+
|
|
231
|
+
def close(self):
|
|
232
|
+
print(Fore.GREEN,"[INFO] SIGTERM/SIGINT received")
|
|
233
|
+
fastwsgi.server.close()
|
|
234
|
+
print(Fore.GREEN,"[INFO] SERVER STOPPED")
|
|
235
|
+
exit()
|
|
236
|
+
|
|
237
|
+
def file(self, path, responseFile=True):
|
|
238
|
+
if responseFile:
|
|
239
|
+
self.response.type = "html"
|
|
240
|
+
self.response.headers = [('Content-Type', 'text/html')]
|
|
241
|
+
file = self.fileCache.get(path)
|
|
242
|
+
if file:
|
|
243
|
+
return file
|
|
244
|
+
else:
|
|
245
|
+
file = open(path)
|
|
246
|
+
content = file.read()
|
|
247
|
+
file.close()
|
|
248
|
+
self.fileCache[path] = content
|
|
249
|
+
return content
|
|
250
|
+
|
|
251
|
+
def start_ORM(self):
|
|
252
|
+
if self.features.get("orm") == True:
|
|
253
|
+
self.db = db.DB(user=self.config.get('DB', 'DB_USER'), password=self.config.get('DB', 'DB_PASSWORD'),
|
|
254
|
+
host=self.config.get('DB', 'DB_HOST'), port=int(self.config.get('DB', 'DB_PORT')),
|
|
255
|
+
db=self.config.get('DB', 'DB_NAME'))
|
|
256
|
+
else:
|
|
257
|
+
raise Exception("ORM not enabled")
|
vesta/http/error.py
ADDED
vesta/http/redirect.py
ADDED
vesta/http/response.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
class Response:
|
|
4
|
+
CODES = {200: "200 OK", 400: "400 Bad Request", 403: "403 Forbidden", 404: "404 Not Found", 500: "500 Server Error", 302: "302 Redirect"}
|
|
5
|
+
ERROR_PAGES = {}
|
|
6
|
+
|
|
7
|
+
def __init__(self, start_response, code=200, type="html"):
|
|
8
|
+
self.cookies = {}
|
|
9
|
+
self.type = type
|
|
10
|
+
self.headers = [('Content-Type', 'text/' + type),('Cache-Control', 'no-cache'), ('Server', 'mag v1 Harpie')]
|
|
11
|
+
self.code = code
|
|
12
|
+
self.start_response = start_response
|
|
13
|
+
self.content = ""
|
|
14
|
+
|
|
15
|
+
def ok(self):
|
|
16
|
+
if self.code != 200 and self.code != 302:
|
|
17
|
+
if self.code in self.ERROR_PAGES.keys():
|
|
18
|
+
self.type = "html"
|
|
19
|
+
self.headers = [('Content-Type', 'text/html')]
|
|
20
|
+
file = open(self.ERROR_PAGES[self.code])
|
|
21
|
+
self.content = file.read()
|
|
22
|
+
file.close()
|
|
23
|
+
else:
|
|
24
|
+
self.type = "plain"
|
|
25
|
+
self.headers = [('Content-Type', 'text/plain')]
|
|
26
|
+
self.start_response(self.CODES.get(self.code, "500 UNEXPECTED"), self.headers)
|
|
27
|
+
|
|
28
|
+
def encode(self):
|
|
29
|
+
# print("[INFO] encoding response : ", self.content)
|
|
30
|
+
|
|
31
|
+
if self.type == "plain":
|
|
32
|
+
return (self.CODES[self.code] + " " + self.content).encode()
|
|
33
|
+
return str(self.content).encode()
|
|
34
|
+
|
|
35
|
+
def set_cookie(self, name, value, exp=None, samesite=None, secure=False, httponly=False):
|
|
36
|
+
"""Set a response cookie for the client.
|
|
37
|
+
name
|
|
38
|
+
the name of the cookie.
|
|
39
|
+
|
|
40
|
+
exp
|
|
41
|
+
the expiration timeout for the cookie. If 0 or other boolean
|
|
42
|
+
False, no 'expires' param will be set, and the cookie will be a
|
|
43
|
+
"session cookie" which expires when the browser is closed.
|
|
44
|
+
|
|
45
|
+
samesite
|
|
46
|
+
The 'SameSite' attribute of the cookie. If None (the default)
|
|
47
|
+
the cookie 'samesite' value will not be set. If 'Strict' or
|
|
48
|
+
'Lax', the cookie 'samesite' value will be set to the given value.
|
|
49
|
+
|
|
50
|
+
secure
|
|
51
|
+
if False (the default) the cookie 'secure' value will not
|
|
52
|
+
be set. If True, the cookie 'secure' value will be set (to 1).
|
|
53
|
+
|
|
54
|
+
httponly
|
|
55
|
+
If False (the default) the cookie 'httponly' value will not be set.
|
|
56
|
+
If True, the cookie 'httponly' value will be set (to 1).
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
# Calculate expiration time
|
|
61
|
+
expires = None
|
|
62
|
+
if exp:
|
|
63
|
+
if exp['unit'] == "days":
|
|
64
|
+
expires = datetime.datetime.now() + datetime.timedelta(days=exp['value'])
|
|
65
|
+
elif exp['unit'] == "minutes":
|
|
66
|
+
expires = datetime.datetime.now() + datetime.timedelta(minutes=exp['value'])
|
|
67
|
+
expires = expires.strftime("%a, %d-%b-%Y %H:%M:%S GMT")
|
|
68
|
+
|
|
69
|
+
# Construct cookie string
|
|
70
|
+
cookie_parts = [f"{name}={value}"]
|
|
71
|
+
if expires:
|
|
72
|
+
cookie_parts.append(f"Expires={expires}")
|
|
73
|
+
if samesite:
|
|
74
|
+
cookie_parts.append(f"SameSite={samesite}")
|
|
75
|
+
if secure:
|
|
76
|
+
cookie_parts.append("Secure")
|
|
77
|
+
if httponly:
|
|
78
|
+
cookie_parts.append("HttpOnly")
|
|
79
|
+
cookie_string = "; ".join(cookie_parts)
|
|
80
|
+
|
|
81
|
+
# Add cookie to headers
|
|
82
|
+
self.headers.append(('Set-Cookie', cookie_string))
|
|
83
|
+
|
|
84
|
+
def del_cookie(self, name):
|
|
85
|
+
self.set_cookie(name, "", exp={"value": 0, "unit": "days"})
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import smtplib
|
|
2
|
+
from email.mime.multipart import MIMEMultipart
|
|
3
|
+
from email.mime.text import MIMEText
|
|
4
|
+
import email.utils
|
|
5
|
+
import dkim
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Mailing:
|
|
9
|
+
def __init__(self, host, port, address, password, serviceName, path):
|
|
10
|
+
try:
|
|
11
|
+
self.name = serviceName
|
|
12
|
+
self.host = host
|
|
13
|
+
self.port = port
|
|
14
|
+
self.password = password
|
|
15
|
+
self.smtp = smtplib.SMTP(host, port)
|
|
16
|
+
self.smtp.ehlo() # send the extended hello to our server
|
|
17
|
+
self.smtp.starttls()
|
|
18
|
+
self.address = address
|
|
19
|
+
self.smtp.login(address, password)
|
|
20
|
+
self.templates = {}
|
|
21
|
+
self.path = path
|
|
22
|
+
with open(self.path + "/mailing/dkim.txt") as fh:
|
|
23
|
+
self.private = fh.read()
|
|
24
|
+
print("[Vesta - mails] server connected")
|
|
25
|
+
except smtplib.SMTPException as e:
|
|
26
|
+
print(f"Error connecting to mailing server : {e}")
|
|
27
|
+
self.smtp.quit()
|
|
28
|
+
|
|
29
|
+
def restart(self):
|
|
30
|
+
self.smtp = smtplib.SMTP(self.host, self.port)
|
|
31
|
+
self.smtp.ehlo() # send the extended hello to our server
|
|
32
|
+
self.smtp.starttls()
|
|
33
|
+
self.smtp.login(self.address, self.password)
|
|
34
|
+
print("[Vesta - mails] server reconnected")
|
|
35
|
+
|
|
36
|
+
def is_connected(conn):
|
|
37
|
+
try:
|
|
38
|
+
status = conn.noop()[0]
|
|
39
|
+
except: # smtplib.SMTPServerDisconnected
|
|
40
|
+
status = -1
|
|
41
|
+
return True if status == 250 else False
|
|
42
|
+
|
|
43
|
+
def sendMail(self, content):
|
|
44
|
+
if not self.is_connected():
|
|
45
|
+
self.restart()
|
|
46
|
+
try:
|
|
47
|
+
self.smtp.send_message(content)
|
|
48
|
+
print("[Vesta - mails] mail sent")
|
|
49
|
+
except smtplib.SMTPException as e:
|
|
50
|
+
print(f"Error sending a mail : {e}")
|
|
51
|
+
|
|
52
|
+
def sendTemplate(self,template,target,subject,text,values):
|
|
53
|
+
try:
|
|
54
|
+
self.templates[template]
|
|
55
|
+
except:
|
|
56
|
+
f = open(self.path + "/mailing/" + template, "r")
|
|
57
|
+
self.templates[template] = f.read()
|
|
58
|
+
f.close()
|
|
59
|
+
|
|
60
|
+
mail_confirmation = MIMEMultipart('alternative')
|
|
61
|
+
mail_confirmation['Subject'] = subject
|
|
62
|
+
mail_confirmation['From'] = self.name + " <" + self.address + ">"
|
|
63
|
+
mail_confirmation['Message-ID'] = email.utils.make_msgid(domain='carbonlab.dev')
|
|
64
|
+
mail_confirmation['Date'] = email.utils.formatdate()
|
|
65
|
+
part1 = MIMEText(text, 'plain')
|
|
66
|
+
part2 = MIMEText(self.templates[template].format(values), 'html')
|
|
67
|
+
mail_confirmation.attach(part1)
|
|
68
|
+
mail_confirmation.attach(part2)
|
|
69
|
+
mail_confirmation['To'] = target # ATTENTION CE N'EST PAS UNE COPIE CACHEE
|
|
70
|
+
self.signMail(mail_confirmation)
|
|
71
|
+
self.sendMail(mail_confirmation)
|
|
72
|
+
|
|
73
|
+
def signMail(self, mail):
|
|
74
|
+
headers = ["To", "From", "Subject"]
|
|
75
|
+
sig = dkim.sign(
|
|
76
|
+
message=mail.as_bytes(),
|
|
77
|
+
selector="1686741263.carbonlab".encode(),
|
|
78
|
+
domain="carbonlab.dev".encode(),
|
|
79
|
+
privkey=self.private.encode(),
|
|
80
|
+
include_headers=headers, )
|
|
81
|
+
mail["DKIM-Signature"] = sig[len("DKIM-Signature: "):].decode()
|
|
82
|
+
|
|
83
|
+
def sendConfirmation(self, target, OTP):
|
|
84
|
+
try:
|
|
85
|
+
self.template_confirmation
|
|
86
|
+
except:
|
|
87
|
+
f = open(self.path + "/mailing/mailVerif.html", "r")
|
|
88
|
+
self.template_confirmation = f.read()
|
|
89
|
+
f.close()
|
|
90
|
+
|
|
91
|
+
mail_confirmation = MIMEMultipart('alternative')
|
|
92
|
+
mail_confirmation['Subject'] = "🌱 Confirmez votre adresse ! 📨"
|
|
93
|
+
mail_confirmation['From'] = self.name + " <" + self.address + ">"
|
|
94
|
+
mail_confirmation['Message-ID'] = email.utils.make_msgid(domain='carbonlab.dev')
|
|
95
|
+
mail_confirmation['Date'] = email.utils.formatdate()
|
|
96
|
+
text = "Confirm your " + self.name + " account : " + OTP
|
|
97
|
+
part1 = MIMEText(text, 'plain')
|
|
98
|
+
part2 = MIMEText(self.template_confirmation.format(OTP), 'html')
|
|
99
|
+
mail_confirmation.attach(part1)
|
|
100
|
+
mail_confirmation.attach(part2)
|
|
101
|
+
mail_confirmation['To'] = target # ATTENTION CE N'EST PAS UNE COPIE CACHEE
|
|
102
|
+
self.signMail(mail_confirmation)
|
|
103
|
+
self.sendMail(mail_confirmation)
|
|
104
|
+
|
|
105
|
+
def sendOrgInvite(self, target, company):
|
|
106
|
+
try:
|
|
107
|
+
self.template_org_invitation
|
|
108
|
+
except:
|
|
109
|
+
f = open(self.path + "/mailing/mailInvite.html", "r")
|
|
110
|
+
self.template_org_invitation = f.read()
|
|
111
|
+
f.close()
|
|
112
|
+
self.mail_invitation = MIMEMultipart('alternative')
|
|
113
|
+
self.mail_invitation['Subject'] = "🔔 Rejoignez " + company + " 🔔"
|
|
114
|
+
self.mail_invitation['From'] = self.name + " <" + self.address + ">"
|
|
115
|
+
|
|
116
|
+
self.mail_invitation['Message-ID'] = email.utils.make_msgid(domain='carbonlab.dev')
|
|
117
|
+
self.mail_invitation['Date'] = email.utils.formatdate()
|
|
118
|
+
text = "Inscrivez vous sur " + self.name + " pour rejoindre " + company
|
|
119
|
+
part1 = MIMEText(text, 'plain')
|
|
120
|
+
part2 = MIMEText(self.template_org_invitation.format(company,company), 'html')
|
|
121
|
+
self.mail_invitation.attach(part1)
|
|
122
|
+
self.mail_invitation.attach(part2)
|
|
123
|
+
self.mail_invitation['To'] = target # ATTENTION CE N'EST PAS UNE COPIE CACHEE
|
|
124
|
+
self.signMail(self.mail_invitation)
|
|
125
|
+
|
|
126
|
+
self.sendMail(self.mail_invitation)
|
vesta/scripts/initDB.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from vesta import Server
|
|
2
|
+
from os.path import abspath, dirname
|
|
3
|
+
import sys
|
|
4
|
+
from psycopg import sql
|
|
5
|
+
|
|
6
|
+
PATH = dirname(abspath(__file__))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DBInitializer(Server):
|
|
10
|
+
def initUniauth(self):
|
|
11
|
+
if sys.argv[1].upper() == 'Y':
|
|
12
|
+
self.uniauth.initUniauth()
|
|
13
|
+
|
|
14
|
+
def initDB(self):
|
|
15
|
+
self.referenceUniauth()
|
|
16
|
+
self.db.cur.execute(open(self.path + "/db/schema.sql", "r").read())
|
|
17
|
+
self.db.conn.commit()
|
|
18
|
+
|
|
19
|
+
def referenceUniauth(self):
|
|
20
|
+
self.db.cur.execute("CREATE EXTENSION if not exists postgres_fdw;")
|
|
21
|
+
self.db.cur.execute(
|
|
22
|
+
sql.SQL("""
|
|
23
|
+
CREATE SERVER if not exists uniauth
|
|
24
|
+
FOREIGN DATA WRAPPER postgres_fdw
|
|
25
|
+
OPTIONS (host {}, port {}, dbname {});
|
|
26
|
+
""").format(
|
|
27
|
+
sql.Literal(self.config.get('UNIAUTH', 'DB_HOST')),
|
|
28
|
+
sql.Literal(self.config.get('UNIAUTH', 'DB_PORT')),
|
|
29
|
+
sql.Literal(self.config.get('UNIAUTH', 'DB_NAME'))
|
|
30
|
+
))
|
|
31
|
+
|
|
32
|
+
self.db.cur.execute(
|
|
33
|
+
sql.SQL("""
|
|
34
|
+
CREATE USER MAPPING if not exists FOR CURRENT_USER SERVER uniauth
|
|
35
|
+
OPTIONS (user '%s', password '%s');
|
|
36
|
+
""").format(
|
|
37
|
+
sql.Literal(self.config.get('DB', 'DB_USER')),
|
|
38
|
+
sql.Literal(self.config.get('DB', 'DB_PASSWORD'))
|
|
39
|
+
))
|
|
40
|
+
self.db.cur.execute(
|
|
41
|
+
"""
|
|
42
|
+
CREATE FOREIGN TABLE if not exists account (id bigserial NOT NULL)
|
|
43
|
+
SERVER uniauth
|
|
44
|
+
OPTIONS (schema_name 'public', table_name 'account');
|
|
45
|
+
""")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
initializer = DBInitializer(path=PATH[:-3], configFile="/server.ini", noStart=True)
|
|
50
|
+
initializer.initUniauth()
|
|
51
|
+
initializer.initDB()
|
|
52
|
+
print("DB ready")
|
vesta/scripts/install.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from configparser import ConfigParser
|
|
3
|
+
from os.path import abspath, dirname
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
PATH = dirname(abspath(__file__))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Installer:
|
|
10
|
+
def __init__(self, configFile, arg="all"):
|
|
11
|
+
self.importConf(configFile)
|
|
12
|
+
self.uniauth = "N"
|
|
13
|
+
self.name = self.config.get("server", "SERVICE_NAME").replace(" ", "_").upper()
|
|
14
|
+
|
|
15
|
+
if arg == "all" or arg == "a":
|
|
16
|
+
self.installAll()
|
|
17
|
+
elif arg == "db":
|
|
18
|
+
self.installDB()
|
|
19
|
+
elif arg == "uniauth":
|
|
20
|
+
self.installUniauth()
|
|
21
|
+
elif arg == "vesta":
|
|
22
|
+
self.resetVesta()
|
|
23
|
+
elif arg == "reset":
|
|
24
|
+
self.resetDB()
|
|
25
|
+
elif arg == "service":
|
|
26
|
+
self.installService()
|
|
27
|
+
elif arg == "cron":
|
|
28
|
+
self.setupCrons()
|
|
29
|
+
|
|
30
|
+
def ex(self, command):
|
|
31
|
+
subprocess.run(command, shell=True, check=True)
|
|
32
|
+
|
|
33
|
+
def installDB(self):
|
|
34
|
+
print("----DB----")
|
|
35
|
+
self.ex("sudo -u postgres createdb " + self.config.get("DB", "DB_NAME"))
|
|
36
|
+
self.ex("sudo -u postgres createuser " + self.config.get("DB", "DB_USER") + " -s --pwprompt ")
|
|
37
|
+
|
|
38
|
+
def resetVesta(self):
|
|
39
|
+
self.ex("pip uninstall vesta")
|
|
40
|
+
self.ex("pip install git+https://gitlab.com/Louciole/vesta.git/")
|
|
41
|
+
|
|
42
|
+
def installUniauth(self):
|
|
43
|
+
print("----UNIAUTH----")
|
|
44
|
+
while True:
|
|
45
|
+
uniauth = input("Do you want to create a uniauth database? (y/n)")
|
|
46
|
+
if uniauth.upper() == 'Y' or uniauth.upper() == 'N':
|
|
47
|
+
self.uniauth = uniauth.upper()
|
|
48
|
+
break
|
|
49
|
+
|
|
50
|
+
if self.uniauth == 'Y':
|
|
51
|
+
self.ex("sudo -u postgres createdb " + self.config.get("UNIAUTH", "DB_NAME"))
|
|
52
|
+
|
|
53
|
+
def initDB(self):
|
|
54
|
+
self.ex("python3 ./db/initDB.py " + self.uniauth)
|
|
55
|
+
|
|
56
|
+
def resetDB(self):
|
|
57
|
+
self.ex("sudo -u postgres dropdb " + self.config.get("DB", "DB_NAME"))
|
|
58
|
+
self.initDB()
|
|
59
|
+
|
|
60
|
+
def installAll(self):
|
|
61
|
+
self.installNginx()
|
|
62
|
+
self.installDB()
|
|
63
|
+
self.installUniauth()
|
|
64
|
+
self.initDB()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if len(sys.argv) > 1:
|
|
74
|
+
Installer(PATH + "/server.ini", sys.argv[1])
|
|
75
|
+
else:
|
|
76
|
+
Installer(PATH + "/server.ini")
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import importlib
|
|
4
|
+
from colorama import Fore, Style, init
|
|
5
|
+
|
|
6
|
+
def run_file(file_path):
|
|
7
|
+
"""Runs a single Python test file."""
|
|
8
|
+
# Extract the module name from the file path
|
|
9
|
+
readable_path = file_path.strip(test_dir)
|
|
10
|
+
|
|
11
|
+
# Import the test module
|
|
12
|
+
try:
|
|
13
|
+
module_path = os.path.splitext(file_path[2:])[0]
|
|
14
|
+
module_name = module_path.replace(os.sep, '.')
|
|
15
|
+
test_module = importlib.import_module(module_name)
|
|
16
|
+
except ModuleNotFoundError as e:
|
|
17
|
+
print(f"Failed to import test module '{module_name}': {e}")
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
# Look for a 'run' function within the module (assuming tests are run from this function)
|
|
21
|
+
if hasattr(test_module, 'run'):
|
|
22
|
+
try:
|
|
23
|
+
result = test_module.run()
|
|
24
|
+
for res in result:
|
|
25
|
+
if res[1] == False:
|
|
26
|
+
print(Fore.RED +f"FAILED: '{res[0]}' @{readable_path}")
|
|
27
|
+
return False
|
|
28
|
+
else:
|
|
29
|
+
print(Fore.GREEN +f"PASSED: '{res[0]}' @{readable_path}")
|
|
30
|
+
return True
|
|
31
|
+
except Exception as e:
|
|
32
|
+
print(f"Error running test file '{readable_path}': {e}")
|
|
33
|
+
else:
|
|
34
|
+
print(f"Test file '{readable_path}' doesn't have a 'run' function.")
|
|
35
|
+
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def run_folder(folder):
|
|
40
|
+
"""Runs all Python test files in a directory."""
|
|
41
|
+
counter = (0,0)
|
|
42
|
+
|
|
43
|
+
for root, _, files in os.walk(folder):
|
|
44
|
+
for file in files:
|
|
45
|
+
if file.endswith('.py'):
|
|
46
|
+
file_path = os.path.join(root, file)
|
|
47
|
+
res = run_file(file_path)
|
|
48
|
+
if res == True:
|
|
49
|
+
counter = (counter[0]+1,counter[1]+1)
|
|
50
|
+
elif res == False:
|
|
51
|
+
counter = (counter[0],counter[1]+1)
|
|
52
|
+
|
|
53
|
+
return counter
|
|
54
|
+
|
|
55
|
+
test_dir = "./tests"
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
init()# Initialize colorama
|
|
59
|
+
|
|
60
|
+
counter = 0
|
|
61
|
+
total = 0
|
|
62
|
+
|
|
63
|
+
cwd = os.getcwd()
|
|
64
|
+
if cwd not in sys.path:
|
|
65
|
+
sys.path.insert(0, cwd)
|
|
66
|
+
|
|
67
|
+
for _, dirs, _ in os.walk(test_dir):
|
|
68
|
+
for folder in dirs:
|
|
69
|
+
if folder == '__pycache__':
|
|
70
|
+
continue
|
|
71
|
+
print("Running folder "+folder)
|
|
72
|
+
count = run_folder(test_dir+'/'+folder)
|
|
73
|
+
counter += count[0]
|
|
74
|
+
total += count[1]
|
|
75
|
+
|
|
76
|
+
if counter == total:
|
|
77
|
+
color = Fore.GREEN
|
|
78
|
+
exCode = 0
|
|
79
|
+
else:
|
|
80
|
+
color = Fore.RED
|
|
81
|
+
exCode = 1
|
|
82
|
+
print(color + f"{counter}/{total} tests passed")
|
|
83
|
+
exit(exCode)
|