Added libs
This commit is contained in:
2
api/.gitignore
vendored
Normal file
2
api/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
__pycache__
|
||||
api.zip
|
||||
1
api/.mtimes.json
Normal file
1
api/.mtimes.json
Normal file
@@ -0,0 +1 @@
|
||||
{".gitignore": 1741162450.0110252, "api.py": 1757749878.4958198, "apiBlueprint.py": 1757238077.9332085, "certs.py": 1757228708.4552765, "config.json": 1756001769.9660056, "eventsSocketio.py": 1757328543.5948038, "requirements.txt": 1766641471.4877045, "__pycache__\\api.cpython-311.pyc": 1739790368.2421517}
|
||||
110
api/api.py
Normal file
110
api/api.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask, Blueprint, request, send_from_directory, jsonify
|
||||
from flask_cors import CORS, cross_origin
|
||||
from flask_socketio import SocketIO, Namespace
|
||||
|
||||
from libs.app.common.logging import get_logger
|
||||
from libs.app.common.paths import ROOT_DIR
|
||||
from libs.fspn.utils.wrapper_util import threaded
|
||||
from .apiBlueprint import ApiBlueprint
|
||||
from .eventsSocketio import EventsSocketio
|
||||
from libs.noSys.noSysModule import NoSysModule
|
||||
from .certs import generate_ca_and_cert, add_ca_os
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
class Api(NoSysModule):
|
||||
def __init__(self, nosys_core):
|
||||
super().__init__(nosys_core)
|
||||
|
||||
self.dist_dir = os.path.join(ROOT_DIR, "libs/vueNoSys/dist")
|
||||
self.assets_dir = os.path.join(self.dist_dir, "assets")
|
||||
self.app = Flask(__name__, static_folder=self.assets_dir, template_folder=self.dist_dir)
|
||||
CORS(self.app, resources={r"/*": {"origins": "*"}}, supports_credentials=True)
|
||||
self.socketio = SocketIO(self.app, cors_allowed_origins="*")
|
||||
self.server = None
|
||||
|
||||
self.host = self.config["server"]["host"]
|
||||
self.port = self.config["server"]["port"]
|
||||
|
||||
self.register_blueprint(BasicBlueprint(self).blueprint)
|
||||
self.register_socketio(BasicEventSocketIo(self))
|
||||
|
||||
def setup(self):
|
||||
self.nosys_core.modules.api = self
|
||||
|
||||
certs_path = os.path.join(ROOT_DIR, "libs", "api", "certs")
|
||||
self.ca_path = os.path.join(certs_path , "ca.pem")
|
||||
self.ca_key_path = os.path.join(certs_path, "ca_key.pem")
|
||||
self.cert_path = os.path.join(certs_path, "cert.pem")
|
||||
self.key_path = os.path.join(certs_path, "key.pem")
|
||||
|
||||
if not os.path.exists(self.cert_path) or not os.path.exists(self.key_path) or not os.path.exists(self.ca_path) or not os.path.exists(self.ca_key_path):
|
||||
Path(certs_path).mkdir(parents=True, exist_ok=True)
|
||||
logger.debug("Generating certs")
|
||||
ca, cert, key = generate_ca_and_cert(self.ca_path, self.ca_key_path, self.cert_path, self.key_path)
|
||||
logger.debug("Adding ca to operational system")
|
||||
add_ca_os(self.ca_path)
|
||||
logger.debug("Cert installed")
|
||||
else:
|
||||
logger.debug("Certs already exists")
|
||||
|
||||
def register_blueprint(self, blueprint:Blueprint):
|
||||
try:
|
||||
self.app.register_blueprint(blueprint)
|
||||
logger.debug(f"Registered blueprint {blueprint.url_prefix}")
|
||||
except Exception:
|
||||
logger.exception(f"Failed registering blueprint {blueprint.url_prefix}")
|
||||
|
||||
def register_socketio(self, handler:EventsSocketio):
|
||||
try:
|
||||
handler.register_events(self.socketio)
|
||||
logger.debug(f"Registered socketio {handler.namespace}")
|
||||
except Exception:
|
||||
logger.exception(f"Failed registering socketio {handler.namespace}")
|
||||
|
||||
def on_nosys_ready(self, event):
|
||||
self.run()
|
||||
|
||||
@threaded
|
||||
def run(self):
|
||||
self.routes()
|
||||
logger.debug(f'Running Flask API ({self.host}:{self.port}) with urls: {self.app.url_map}')
|
||||
try:
|
||||
self.socketio.run(app=self.app, host=self.host, port=self.port, allow_unsafe_werkzeug=True, ssl_context=(self.cert_path, self.key_path))
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
def routes(self):
|
||||
@self.app.route("/", defaults={"path": ""})
|
||||
@self.app.route("/<path:path>")
|
||||
def index(path):
|
||||
if path != "" and os.path.exists(os.path.join(self.dist_dir, path)):
|
||||
return send_from_directory(self.dist_dir, path)
|
||||
return send_from_directory(self.dist_dir, "index.html")
|
||||
|
||||
class BasicBlueprint(ApiBlueprint):
|
||||
def routes(self):
|
||||
self.api:Api = self.module
|
||||
|
||||
@self.blueprint.route('/')
|
||||
def show():
|
||||
return "API"
|
||||
|
||||
class BasicEventSocketIo(EventsSocketio):
|
||||
def events(self):
|
||||
@self.on("connect")
|
||||
def on_connect(*args, **kwargs):
|
||||
print('API connected',args, kwargs, request.sid)
|
||||
self.emit("welcome", {"msg": f"Your id {request.sid}"})
|
||||
|
||||
@self.on("disconnect")
|
||||
def on_disconnect(*args, **kwargs):
|
||||
print('API disconnected',args, kwargs, request.sid)
|
||||
|
||||
@self.on("message")
|
||||
def on_message(*args, **kwargs):
|
||||
print('API Message',args, kwargs)
|
||||
self.emit("message", f"Message received {args[0]}")
|
||||
26
api/apiBlueprint.py
Normal file
26
api/apiBlueprint.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from flask import Blueprint, jsonify
|
||||
from libs.noSys.noSysModule import NoSysModule
|
||||
from libs.noSys.events import Events
|
||||
from flask_socketio import SocketIO
|
||||
|
||||
class ApiBlueprint():
|
||||
def __init__(self, nosys_module:NoSysModule):
|
||||
self.module = nosys_module
|
||||
self.blueprint = Blueprint(self.module.name, __name__, url_prefix='/api/'+self.module.package_id)
|
||||
|
||||
self.default_routes()
|
||||
self.routes()
|
||||
|
||||
def default_routes(self):
|
||||
@self.blueprint.route('/health')
|
||||
def health_check():
|
||||
body = {"package":self.module.package_id, "moduleName":self.module.module_id}
|
||||
return jsonify(body)
|
||||
|
||||
@self.blueprint.route('/config')
|
||||
def config():
|
||||
body = self.module.nosys_core.config.get(self.module.package_id)
|
||||
return jsonify(body)
|
||||
|
||||
def routes(self):
|
||||
pass
|
||||
115
api/certs.py
Normal file
115
api/certs.py
Normal file
@@ -0,0 +1,115 @@
|
||||
import os
|
||||
import webview
|
||||
import ssl
|
||||
import ipaddress
|
||||
import pathlib
|
||||
from cryptography import x509
|
||||
from cryptography.x509.oid import NameOID
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from datetime import datetime, timedelta
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
def generate_ca_and_cert(ca_path="ca.pem", ca_key_path="ca_key.pem",
|
||||
cert_path="cert.pem", key_path="key.pem"):
|
||||
|
||||
ca_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
ca_subject = x509.Name([
|
||||
x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"),
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"NoSys-CA"),
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, u"NoSys Local CA"),
|
||||
])
|
||||
ca_cert = (
|
||||
x509.CertificateBuilder()
|
||||
.subject_name(ca_subject)
|
||||
.issuer_name(ca_subject)
|
||||
.public_key(ca_key.public_key())
|
||||
.serial_number(x509.random_serial_number())
|
||||
.not_valid_before(datetime.utcnow())
|
||||
.not_valid_after(datetime.utcnow() + timedelta(days=3650))
|
||||
.add_extension(
|
||||
x509.BasicConstraints(ca=True, path_length=None), critical=True,
|
||||
)
|
||||
.sign(ca_key, hashes.SHA256())
|
||||
)
|
||||
|
||||
with open(ca_path, "wb") as f:
|
||||
f.write(ca_cert.public_bytes(serialization.Encoding.PEM))
|
||||
with open(ca_key_path, "wb") as f:
|
||||
f.write(ca_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
))
|
||||
|
||||
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
subject = x509.Name([
|
||||
x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"),
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"NoSys"),
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, u"localhost"),
|
||||
])
|
||||
cert = (
|
||||
x509.CertificateBuilder()
|
||||
.subject_name(subject)
|
||||
.issuer_name(ca_subject)
|
||||
.public_key(key.public_key())
|
||||
.serial_number(x509.random_serial_number())
|
||||
.not_valid_before(datetime.utcnow())
|
||||
.not_valid_after(datetime.utcnow() + timedelta(days=3650))
|
||||
.add_extension(
|
||||
x509.SubjectAlternativeName([
|
||||
x509.DNSName(u"localhost"),
|
||||
x509.IPAddress(ipaddress.IPv4Address("127.0.0.1"))]),
|
||||
critical=False,
|
||||
)
|
||||
.sign(ca_key, hashes.SHA256())
|
||||
)
|
||||
|
||||
with open(cert_path, "wb") as f:
|
||||
f.write(cert.public_bytes(serialization.Encoding.PEM))
|
||||
with open(key_path, "wb") as f:
|
||||
f.write(key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
))
|
||||
|
||||
return ca_path, cert_path, key_path
|
||||
|
||||
def add_ca_os(ca_path="ca.pem"):
|
||||
system = platform.system()
|
||||
if system == "Windows":
|
||||
add_ca_windows(ca_path)
|
||||
elif system == "Darwin":
|
||||
add_ca_macos(ca_path)
|
||||
elif system == "Linux":
|
||||
add_ca_linux(ca_path)
|
||||
else:
|
||||
raise Exception("Operational system not supported")
|
||||
|
||||
def add_ca_windows(ca_path="ca.pem"):
|
||||
subprocess.run([
|
||||
"powershell",
|
||||
"-Command",
|
||||
f'Import-Certificate -FilePath "{os.path.abspath(ca_path)}" -CertStoreLocation Cert:\\CurrentUser\\Root'
|
||||
], check=True)
|
||||
|
||||
def add_ca_macos(ca_path="ca.pem"):
|
||||
subprocess.run([
|
||||
"sudo",
|
||||
"security",
|
||||
"add-trusted-cert",
|
||||
"-d",
|
||||
"-r", "trustRoot",
|
||||
"-k", "/Library/Keychains/System.keychain",
|
||||
os.path.abspath(ca_path)
|
||||
], check=True)
|
||||
|
||||
def add_ca_linux(ca_path="ca.pem"):
|
||||
import shutil
|
||||
dest = "/usr/local/share/ca-certificates/zecho-ca.crt"
|
||||
shutil.copy(os.path.abspath(ca_path), dest)
|
||||
subprocess.run(["sudo", "update-ca-certificates"], check=True)
|
||||
|
||||
6
api/config.json
Normal file
6
api/config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"server": {
|
||||
"host": "127.0.0.1",
|
||||
"port": "5050"
|
||||
}
|
||||
}
|
||||
51
api/eventsSocketio.py
Normal file
51
api/eventsSocketio.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
from flask import Blueprint, make_response, request, jsonify, session, has_request_context
|
||||
from flask_socketio import SocketIO, Namespace, emit, send, join_room, leave_room
|
||||
|
||||
from libs.noSys.noSysModule import NoSysModule
|
||||
|
||||
class EventsSocketio():
|
||||
def __init__(self, module:NoSysModule):
|
||||
self.module = module
|
||||
self.namespace = f"/ws/{self.module.name}"
|
||||
self.socketio:SocketIO = None
|
||||
|
||||
def register_events(self, socketio:SocketIO):
|
||||
self.socketio = socketio
|
||||
self.default_events()
|
||||
self.events()
|
||||
|
||||
def default_events(self):
|
||||
@self.socketio.on("health", namespace=self.namespace)
|
||||
def on_health(*args, **kwargs):
|
||||
self.emit("health", {"status": "ok"})
|
||||
|
||||
@self.socketio.on("ping", namespace=self.namespace)
|
||||
def on_ping(data=None):
|
||||
self.emit("pong", {"ts": time.time(), "echo": data})
|
||||
|
||||
|
||||
def emit(self, event:str, data=None, room=None, **kwargs):
|
||||
target = None
|
||||
if room:
|
||||
target = room
|
||||
elif has_request_context():
|
||||
target = request.sid
|
||||
|
||||
self.socketio.emit(event, data, to=target, namespace=self.namespace, **kwargs)
|
||||
|
||||
def on(self, event: str):
|
||||
def decorator(handler):
|
||||
@self.socketio.on(event, namespace=self.namespace)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return handler(*args, **kwargs)
|
||||
except Exception as e:
|
||||
self.error(str(e))
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def events(self):
|
||||
pass
|
||||
10
api/info.json
Normal file
10
api/info.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"id": "api",
|
||||
"version": 0.042,
|
||||
"modules": [
|
||||
{
|
||||
"id": "api",
|
||||
"version": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
4
api/requirements.txt
Normal file
4
api/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
flask
|
||||
flask-socketio
|
||||
python-socketio[client]
|
||||
flask_cors
|
||||
4
app/.gitignore
vendored
Normal file
4
app/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
__pycache__
|
||||
.venv
|
||||
*.log
|
||||
app.zip
|
||||
1
app/.mtimes.json
Normal file
1
app/.mtimes.json
Normal file
@@ -0,0 +1 @@
|
||||
{".gitignore": 1765697926.2944038, "config.json": 1766641092.7797186, "main.py": 1757275665.3581765, "requirements.txt": 1757272908.932603, "start.py": 1767345275.3860576, "updater.py": 1756702920.6384296, "utilss.py": 1755864608.0, "BKP\\.gitignore": 1741162465.633492, "BKP\\config.json": 1755418661.1762836, "BKP\\main.py": 1756113491.7506, "BKP\\start.py": 1752932509.1119049, "BKP\\updater.py": 1752758398.9098537, "BKP\\utils.py": 1752930900.679567, "common\\args.py": 1755902334.0, "common\\config.py": 1756632053.520828, "common\\logging.py": 1756701444.0262084, "common\\network_utils.py": 1756111641.1188223, "common\\paths.py": 1756118190.209198, "common\\process.py": 1757143858.1563985, "common\\store.py": 1757582915.4349658, "common\\__pycache__\\args.cpython-311.pyc": 1755902456.0, "common\\__pycache__\\config.cpython-311.pyc": 1755909410.0, "common\\__pycache__\\logging.cpython-311.pyc": 1756014259.7862537, "common\\__pycache__\\paths.cpython-311.pyc": 1756014259.7917824, "common\\__pycache__\\process.cpython-311.pyc": 1755865508.0, "ui\\ui_server.py": 1757275891.3673892, "ui\\static\\main.js": 1757241125.1477273, "ui\\templates\\index.html": 1756627053.1326618, "__pycache__\\updater.cpython-311.pyc": 1755912104.0, "__pycache__\\utils.cpython-311.pyc": 1753049185.946803}
|
||||
4
app/BKP/.gitignore
vendored
Normal file
4
app/BKP/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
__pycache__
|
||||
.venv
|
||||
*.log
|
||||
app.zip
|
||||
75
app/BKP/config.json
Normal file
75
app/BKP/config.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"app":{
|
||||
"id":"app",
|
||||
"checkUpdates": true,
|
||||
"repositories":["http://error/data"],
|
||||
"version": 0
|
||||
},
|
||||
"main":{
|
||||
"terminalWindow": true
|
||||
},
|
||||
"passwordManager":{
|
||||
"package": "lockbox",
|
||||
"client": "lockboxClient"
|
||||
},
|
||||
"updater":{
|
||||
"autoUpdate": true,
|
||||
"checkUpdates": true,
|
||||
"repositories":[
|
||||
"http://n0sys.duckdns.org:40404/libs"
|
||||
]
|
||||
},
|
||||
"packages":[
|
||||
{
|
||||
"id":"fspn",
|
||||
"checkUpdates": true,
|
||||
"repositories":["http://error/data"],
|
||||
"version": 0
|
||||
},
|
||||
{
|
||||
"id":"noSys",
|
||||
"checkUpdates": true
|
||||
},
|
||||
{
|
||||
"id":"fileTransfer",
|
||||
"checkUpdates": true
|
||||
},
|
||||
{
|
||||
"id":"api",
|
||||
"checkUpdates": true,
|
||||
"server": {
|
||||
"host": "127.0.0.1",
|
||||
"port": "5050"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id":"vueNoSys",
|
||||
"checkUpdates": true,
|
||||
"server": {
|
||||
"host": "127.0.0.1",
|
||||
"port": "3001"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id":"lockbox",
|
||||
"checkUpdates": true,
|
||||
"servers": {
|
||||
"api": {
|
||||
"port": "5001"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id":"rendezvous",
|
||||
"checkUpdates": true,
|
||||
"server": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 40441
|
||||
}
|
||||
},
|
||||
{
|
||||
"id":"p2post",
|
||||
"checkUpdates": true
|
||||
}
|
||||
]
|
||||
}
|
||||
5
app/BKP/info.json
Normal file
5
app/BKP/info.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"id":"app",
|
||||
"version":0,
|
||||
"modules":[]
|
||||
}
|
||||
70
app/BKP/main.py
Normal file
70
app/BKP/main.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import os, sys
|
||||
import time
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
# TODO NOT SURE IF SYS PATH IS A GOOD PRACTICE
|
||||
root_dir = os.path.normpath(__file__.split("libs")[0])
|
||||
sys.path.append(root_dir)
|
||||
|
||||
from libs.app.utils import Utils, kargs_to_array, get_logger, is_http_running, config_root_logger
|
||||
|
||||
logger = None
|
||||
try:
|
||||
config_root_logger()
|
||||
logger = get_logger()
|
||||
utils = Utils()
|
||||
except Exception:
|
||||
logging.exception("ERROR")
|
||||
|
||||
from libs.app.updater import Updater
|
||||
|
||||
def main():
|
||||
log_ascii_art()
|
||||
logger.debug(f"Readable params: {kargs_to_array()}")
|
||||
|
||||
if not is_noSys_running():
|
||||
updater = Updater(utils)
|
||||
utils.configs.read_packages()
|
||||
|
||||
from libs.noSys.noSysCore import NoSysCore
|
||||
noSys = NoSysCore(updater)
|
||||
else:
|
||||
logger.debug(f"NoSys already running")
|
||||
# TODO open url frontend
|
||||
|
||||
logger.info(f"---------------- Main ended ----------------")
|
||||
|
||||
def is_noSys_running():
|
||||
try:
|
||||
api_config = utils.configs.packages["api"].config
|
||||
api_port = api_config["server"]["port"]
|
||||
api_health_check_url = f"http://localhost:{api_port}/api/health"
|
||||
logger.debug(f"Checking url: {api_health_check_url}")
|
||||
return is_http_running(api_health_check_url)
|
||||
except Exception:
|
||||
logger.exception("ERROR")
|
||||
|
||||
def log_ascii_art():
|
||||
logger.info("""
|
||||
_ __ ____
|
||||
/ |/ /__ / __/_ _____
|
||||
/ / _ \ _\ \/ // (_-<
|
||||
/_/|_/\___/ /___/\_, /___/
|
||||
/___/
|
||||
|
||||
Every man must have freedom, must have the scope to form, test, and act upon his own choices, for any sort of development of his own personality to take place. He must, in short, be free in order that he may be fully human.
|
||||
- Murray Rothbard
|
||||
|
||||
""")
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
time.sleep(30)
|
||||
except Exception:
|
||||
logger.exception("ERROR")
|
||||
for sec in range(30):
|
||||
logger.info(f"Closing in {30-sec}")
|
||||
time.sleep(1)
|
||||
|
||||
59
app/BKP/start.py
Normal file
59
app/BKP/start.py
Normal file
@@ -0,0 +1,59 @@
|
||||
import os, sys
|
||||
import logging
|
||||
import shutil
|
||||
import pathlib
|
||||
import urllib.request
|
||||
import time
|
||||
from pathlib import Path
|
||||
import zipfile
|
||||
|
||||
root_dir = pathlib.Path(__file__).parent.resolve()
|
||||
logger = logging.getLogger("start")
|
||||
|
||||
def start():
|
||||
global logger
|
||||
logger.debug(f"---------------- START ----------------")
|
||||
logger.debug(f"Root path: {root_dir}")
|
||||
|
||||
pathlib.Path(os.path.join(root_dir, "libs")).mkdir(parents=True, exist_ok=True)
|
||||
download_app()
|
||||
from libs.app.utils import Utils, get_logger, create_venv, download_file, kargs_to_array
|
||||
create_venv()
|
||||
args = kargs_to_array()
|
||||
# args.append("updateApp=False") # NEVER SET TRUE
|
||||
# args.append("updateLibs=True")
|
||||
utils = Utils()
|
||||
|
||||
main_file_path = os.path.join(root_dir, "libs","app", "main.py")
|
||||
pid = utils.new_python_process(main_file_path, args)
|
||||
logger.info(f"Main process running. PID {pid} - Args: {args}")
|
||||
logger.info(f"---------------- END ----------------")
|
||||
|
||||
def download_app():
|
||||
url = f"{default_repository}/app/app.zip"
|
||||
path = os.path.join(root_dir, "libs", "app.zip")
|
||||
if(not os.path.exists(path)):
|
||||
logger.debug(f"Downloading from URL: {url} to {path}")
|
||||
urllib.request.urlretrieve(url, path)
|
||||
logger.debug(f"Extracting all from {path}")
|
||||
with zipfile.ZipFile(path, 'r') as zip_ref:
|
||||
zip_ref.extractall(Path(path).parent.absolute())
|
||||
|
||||
|
||||
|
||||
default_repository = "http://n0sys.duckdns.org:30303/libs"
|
||||
|
||||
logger.addHandler(logging.StreamHandler())
|
||||
app_logs_path = os.path.join(root_dir, "logs", "app")
|
||||
shutil.rmtree(os.path.join(root_dir, "logs"), ignore_errors=True)
|
||||
pathlib.Path(app_logs_path).mkdir(parents=True, exist_ok=True)
|
||||
logger.addHandler(logging.FileHandler(os.path.join(app_logs_path,'start.log'), mode="a", encoding="utf-8"))
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
start()
|
||||
except Exception as e:
|
||||
logger.exception("ERROR")
|
||||
time.sleep(30)
|
||||
|
||||
124
app/BKP/updater.py
Normal file
124
app/BKP/updater.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import sys
|
||||
import subprocess
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import os
|
||||
from pathlib import Path
|
||||
import pathlib
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
import zipfile
|
||||
from threading import Thread
|
||||
|
||||
from libs.app.utils import Utils, Package, get_logger, root_dir
|
||||
|
||||
logger = get_logger("updater")
|
||||
|
||||
class Updater():
|
||||
def __init__(self, utils:Utils):
|
||||
self.utils = utils
|
||||
self.libs_path = os.path.join(root_dir, 'libs')
|
||||
self.start()
|
||||
|
||||
def start(self):
|
||||
logger.info(f"---------------- Updater Started ----------------")
|
||||
# Create better validation code
|
||||
if("updateApp" in self.utils.kargs):
|
||||
if(self.utils.kargs["updateApp"]):
|
||||
self.update_app()
|
||||
elif(self.utils.configs.data["updater"]["autoUpdate"]):
|
||||
self.update_app()
|
||||
|
||||
if("restart" in self.utils.flags and self.utils.flags["restart"]):
|
||||
self.utils.kargs["updateApp"] = False
|
||||
self.utils.restart_app()
|
||||
|
||||
if("updateLibs" in self.utils.kargs):
|
||||
if(self.utils.kargs["updateLibs"]):
|
||||
self.update_libs()
|
||||
elif(self.utils.configs.data["updater"]["checkUpdates"]):
|
||||
self.update_libs()
|
||||
|
||||
logger.info(f"---------------- Updater Ended ----------------")
|
||||
|
||||
def update_app(self):
|
||||
latest_version_repository = (self.utils.configs.data["app"]["version"],None)
|
||||
logger.info(f"App current version: {latest_version_repository[0]}")
|
||||
|
||||
logger.info(f"Updating app files")
|
||||
# TODO FIX THIS
|
||||
self.check_package_update(Package(config=self.utils.configs.data["app"]))
|
||||
|
||||
logger.info(f"App updated to version {latest_version_repository[0]}")
|
||||
self.utils.flags["restart"] = True
|
||||
|
||||
def update_libs(self):
|
||||
logger.info(f"Checking packages update")
|
||||
threads:list[Thread] = []
|
||||
for package in self.utils.configs.packages.values():
|
||||
# Default is check updates
|
||||
if(not "checkUpdates" in package.config or package.config["checkUpdates"]):
|
||||
thread = Thread(target=self.check_package_update, args=(package,))
|
||||
thread.start()
|
||||
threads.append(thread)
|
||||
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
def check_package_update(self, package:Package):
|
||||
logger.info(f"Updating package {package.config['id']}")
|
||||
package_id = package.config['id']
|
||||
|
||||
package_path = os.path.join(self.libs_path, package_id)
|
||||
if(package.info):
|
||||
latest_version_repository = (package.info["version"],None)
|
||||
else:
|
||||
logger.info(f"Creating package directory: {package_path}")
|
||||
Path(package_path).mkdir(parents=True, exist_ok=True)
|
||||
latest_version_repository = (-1,None)
|
||||
|
||||
repositories = self.utils.configs.data["updater"]["repositories"][:]
|
||||
if "repositories" in package.config:
|
||||
for repository in package.config["repositories"]:
|
||||
repositories.append(repository)
|
||||
|
||||
for repository in repositories:
|
||||
try:
|
||||
package_url = f"{repository}/{package_id}"
|
||||
info_url = f'{package_url}/info.json'
|
||||
logger.info(f"Reading remote info of {package_id} from repository {repository}")
|
||||
with urllib.request.urlopen(info_url, timeout=2) as data:
|
||||
remote_info = json.load(data)
|
||||
logger.debug(f"{package_id} remote info: {remote_info}")
|
||||
|
||||
# TODO Remove 'or' condition -> "or remote_info["version"]==0"
|
||||
if remote_info["version"] > latest_version_repository[0] or remote_info["version"]==0:
|
||||
latest_version_repository = (remote_info["version"], package_url)
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading remote info of {package_id} from repository {repository}: {e}")
|
||||
|
||||
if latest_version_repository[1]:
|
||||
package_zip = f"{latest_version_repository[1]}/{package_id}.zip"
|
||||
package_local_file = os.path.join(self.libs_path, f"{package_id}.zip")
|
||||
logger.debug(f"Downloading package from {package_zip} to local file {package_local_file}")
|
||||
self.download_package(package_zip, package_local_file)
|
||||
self.pip_install_requirements(package_path)
|
||||
|
||||
|
||||
def download_package(self, url, path):
|
||||
urllib.request.urlretrieve(url, path)
|
||||
logger.info(f"Extracting all from {path}")
|
||||
with zipfile.ZipFile(path, 'r') as zip_ref:
|
||||
zip_ref.extractall(Path(path).parent.absolute())
|
||||
|
||||
def pip_install_requirements(self, package_path):
|
||||
path = os.path.join(package_path, "requirements.txt")
|
||||
if(os.path.exists(path)):
|
||||
logger.info(f"Pip installing requirements in {path}")
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", path])
|
||||
else:
|
||||
logger.info(f"{package_path} no requirements.txt")
|
||||
|
||||
def check_module_requirements(self, package_path):
|
||||
pass
|
||||
256
app/BKP/utils.py
Normal file
256
app/BKP/utils.py
Normal file
@@ -0,0 +1,256 @@
|
||||
import os, sys
|
||||
import uuid
|
||||
import pathlib
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
root_dir = os.path.normpath(__file__.split("libs")[0])
|
||||
node_id = str(uuid.uuid4())
|
||||
|
||||
### LOGGER ###
|
||||
import logging, inspect
|
||||
|
||||
class CustomLoggingFormatter(logging.Formatter):
|
||||
grey = "\x1b[0;37m"
|
||||
green = "\x1b[1;32m"
|
||||
yellow = "\x1b[1;33m"
|
||||
red = "\x1b[1;31m"
|
||||
purple = "\x1b[1;35m"
|
||||
blue = "\x1b[1;34m"
|
||||
light_blue = "\x1b[1;36m"
|
||||
bold_red = "\x1b[31;1m"
|
||||
blink_red = "\x1b[5m\x1b[1;31m"
|
||||
reset = "\x1b[0m"
|
||||
prefix = light_blue + '%(asctime)s' + reset + ' |'
|
||||
colored_level = '%(levelname)-8s'
|
||||
message = '| %(message)s' + ''
|
||||
suffix = purple + ' (%(name)s %(filename)s:%(lineno)d)' + reset
|
||||
|
||||
FORMATS = {
|
||||
logging.DEBUG: prefix + grey + colored_level + reset + message + suffix,
|
||||
logging.INFO: prefix + blue + colored_level + reset + message + suffix,
|
||||
logging.WARNING: prefix + yellow + colored_level + reset + message + suffix,
|
||||
logging.ERROR: prefix + red + colored_level + reset + message + suffix,
|
||||
logging.CRITICAL: prefix + bold_red + colored_level + reset + message + suffix
|
||||
}
|
||||
|
||||
file_format = '%(asctime)s | %(levelname)-8s | %(message)s | %(name)s (%(filename)s:%(lineno)d)'
|
||||
|
||||
def format(self, record):
|
||||
log_fmt = self.FORMATS.get(record.levelno)
|
||||
formatter = logging.Formatter(log_fmt)
|
||||
return formatter.format(record)
|
||||
|
||||
# class FileFilter(logging.Filter):
|
||||
# def filter(self, record):
|
||||
# file_path = os.path.normpath(inspect.stack()[1].filename)
|
||||
# head = os.path.split(file_path)[0]
|
||||
# tail = os.path.split(file_path)[1]
|
||||
# if head == os.path.normpath(root_dir):
|
||||
# name = "app"
|
||||
# else:
|
||||
# after_lib = head.replace(os.path.join(root_dir, "libs"), "")
|
||||
# name = after_lib.split(os.sep)[1]
|
||||
# record.name = name
|
||||
# print(name)
|
||||
# return True
|
||||
|
||||
|
||||
def add_logging_console_handler(logger:logging.Logger, logging_level=logging.DEBUG):
|
||||
sh = logging.StreamHandler()
|
||||
sh.setLevel(logging_level)
|
||||
sh.setFormatter(CustomLoggingFormatter())
|
||||
logger.addHandler(sh)
|
||||
|
||||
def add_logging_file_handler(logger:logging.Logger, directory, file_name, logging_level=logging.DEBUG):
|
||||
file_path = os.path.join(root_dir, "logs", directory, f"{file_name}.log")
|
||||
pathlib.Path(file_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
fh = logging.FileHandler(file_path)
|
||||
fh.setLevel(logging_level)
|
||||
fh.setFormatter(logging.Formatter(CustomLoggingFormatter().file_format))
|
||||
logger.addHandler(fh)
|
||||
|
||||
def config_root_logger():
|
||||
logger = get_logger(name="root")
|
||||
# TODO Get lib that is calling logging
|
||||
# logger.addFilter(FileFilter())
|
||||
|
||||
def get_logger(name=""):
|
||||
file_path = os.path.normpath(inspect.stack()[1].filename)
|
||||
head = os.path.split(file_path)[0]
|
||||
tail = os.path.split(file_path)[1]
|
||||
if head == os.path.normpath(root_dir):
|
||||
lib = "root"
|
||||
else:
|
||||
after_lib = head.replace(os.path.join(root_dir, "libs"), "")
|
||||
lib = after_lib.split(os.sep)[1]
|
||||
|
||||
if name == "root":
|
||||
logger_name = name
|
||||
elif name:
|
||||
logger_name = f"{lib}.{name}"
|
||||
else:
|
||||
logger_name = lib
|
||||
|
||||
file_name = name if name else lib
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.propagate = False
|
||||
if not logger.hasHandlers():
|
||||
logger.setLevel(logging.DEBUG)
|
||||
add_logging_console_handler(logger=logger, logging_level=logging.DEBUG)
|
||||
add_logging_file_handler(logger=logger, directory=lib, file_name=file_name, logging_level=logging.DEBUG)
|
||||
|
||||
return logger
|
||||
|
||||
logger = get_logger()
|
||||
logger.debug(f"Root dir: {root_dir}")
|
||||
|
||||
### VENV ###
|
||||
import venv
|
||||
|
||||
def create_venv():
|
||||
venv_dir = os.path.join(root_dir, ".venv")
|
||||
if not os.path.exists(venv_dir):
|
||||
logger.debug(f"Creating python venv: {venv_dir}")
|
||||
venv.create(venv_dir, with_pip=True)
|
||||
|
||||
### DOWNLOADS ###
|
||||
import urllib.request
|
||||
|
||||
def download_file(url, path):
|
||||
head = os.path.split(path)[0]
|
||||
if not os.path.exists(head):
|
||||
logger.debug(f"Creating repository {head}")
|
||||
pathlib.Path(head).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logger.debug(f"Downloading from URL: {url} to {path}")
|
||||
urllib.request.urlretrieve(url, path)
|
||||
|
||||
### KARGS ###
|
||||
def read_kargs():
|
||||
kargs = {}
|
||||
params = sys.argv
|
||||
for param in params:
|
||||
if "=" in param:
|
||||
split = param.split("=")
|
||||
key = split[0]
|
||||
value = split[1]
|
||||
|
||||
if value == "true" or value == "True":
|
||||
value = True
|
||||
elif value == "false" or value == "False":
|
||||
value = False
|
||||
|
||||
kargs[key] = value
|
||||
kargs["rootDir"] = root_dir
|
||||
return kargs
|
||||
|
||||
def kargs_to_array(kargs={}):
|
||||
if not kargs:
|
||||
kargs = read_kargs()
|
||||
array = []
|
||||
for key, value in kargs.items():
|
||||
array.append(f"{key}={value}")
|
||||
return array
|
||||
|
||||
### HTTP UTILS ###
|
||||
def is_http_running(url):
|
||||
try:
|
||||
u:urllib.request.URLopener = urllib.request.urlopen(url)
|
||||
u.close()
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
### UTILS ###
|
||||
|
||||
class Utils:
|
||||
def __init__(self):
|
||||
self.kargs = read_kargs()
|
||||
self.configs = Config()
|
||||
self.flags = {}
|
||||
|
||||
# def get_env():
|
||||
# return os.environ.get('NOSYS_ENV', "PROD")
|
||||
|
||||
# def download_file(self, package, file, destination, repository=None):
|
||||
# if not repository:
|
||||
# repository = self.default_repository
|
||||
|
||||
# url = f"{repository}/{package}/{file}"
|
||||
# logger.debug(f"Downloading {file} from URL: {url}")
|
||||
# urllib.request.urlretrieve(url, destination)
|
||||
|
||||
# def download_main_file(self, ignoreIfExists=True):
|
||||
# main_path = os.path.join(self.root_dir, "libs","app", "main.py")
|
||||
# if (not os.path.exists(main_path)) or (os.path.exists(main_path) and not ignoreIfExists):
|
||||
# self.download_file(package="app", file="main.py", destination=main_path)
|
||||
|
||||
def is_terminal_visible(self):
|
||||
try:
|
||||
return self.configs.data["main"]["terminalWindow"]
|
||||
except Exception as e:
|
||||
return True
|
||||
|
||||
# def _create_log_dir(self):
|
||||
# logs_path = os.path.join(self.root_dir, "logs")
|
||||
# if not os.path.exists(logs_path):
|
||||
# logger.debug(f"Creating logs directory: {logs_path}")
|
||||
# pathlib.Path(logs_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def new_python_process(self, file_path, args):
|
||||
python_executable = "python" if self.is_terminal_visible() else "pythonw"
|
||||
args = [f"{root_dir}/.venv/scripts/{python_executable}", file_path] + args
|
||||
logger.debug(f"Starting a new process: {args}")
|
||||
process = subprocess.Popen(args, cwd=root_dir, creationflags=subprocess.CREATE_NEW_CONSOLE)
|
||||
logger.debug(f"Process PID {process.pid} - {args}")
|
||||
return process.pid
|
||||
|
||||
def restart_app(self):
|
||||
args = [sys.executable, "start.py"] + kargs_to_array(self.kargs)
|
||||
subprocess.Popen(args, cwd=root_dir, creationflags=subprocess.CREATE_NEW_CONSOLE)
|
||||
logger.info(f"Restarting app: {args}")
|
||||
self.exit_app()
|
||||
|
||||
def exit_app(self, exit_code=1):
|
||||
os._exit(exit_code)
|
||||
# sys.exit(exit_code)
|
||||
|
||||
class Config:
|
||||
def __init__(self):
|
||||
self.data = None
|
||||
self.packages:dict[str, Package] = {}
|
||||
|
||||
self.read_config()
|
||||
|
||||
def read_config(self):
|
||||
config_path = os.path.join(root_dir, "libs", "app", "config.json")
|
||||
if(os.path.exists(config_path)):
|
||||
with open(config_path) as f:
|
||||
data = json.load(f)
|
||||
self.data = data
|
||||
self.read_packages()
|
||||
else:
|
||||
raise Exception(f"Config file {config_path} not exist")
|
||||
|
||||
def read_packages(self):
|
||||
for package in self.data["packages"]:
|
||||
try:
|
||||
with open(os.path.join(root_dir,'libs',package["id"],'info.json')) as f:
|
||||
self.packages[package["id"]] = Package(config=package, info=json.load(f))
|
||||
except Exception as e:
|
||||
self.packages[package["id"]] = Package(config=package, info={})
|
||||
logger.error(e)
|
||||
|
||||
class Package:
|
||||
def __init__(self, config={}, info={}):
|
||||
self.config = config
|
||||
self.info = info
|
||||
self.modules:dict[str, any] = {}
|
||||
|
||||
self.read_modules()
|
||||
|
||||
def read_modules(self):
|
||||
if "modules" in self.info:
|
||||
for module in self.info["modules"]:
|
||||
self.modules[module["id"]] = module
|
||||
51
app/common/args.py
Normal file
51
app/common/args.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import sys
|
||||
from typing import Dict, List, Any
|
||||
|
||||
def read_kargs(argv: List[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Parse command-line arguments in the format key=value.
|
||||
|
||||
Example:
|
||||
python start.py updateApp=True port=8080
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
"updateApp": True,
|
||||
"port": "8080"
|
||||
}
|
||||
"""
|
||||
if argv is None:
|
||||
argv = sys.argv
|
||||
|
||||
kargs: Dict[str, Any] = {}
|
||||
for param in argv:
|
||||
if "=" in param:
|
||||
key, value = param.split("=", 1)
|
||||
|
||||
# Normalize booleans
|
||||
if value.lower() == "true":
|
||||
value = True
|
||||
elif value.lower() == "false":
|
||||
value = False
|
||||
|
||||
kargs[key] = value
|
||||
|
||||
return kargs
|
||||
|
||||
|
||||
def kargs_to_array(kargs: Dict[str, Any] = None) -> List[str]:
|
||||
"""
|
||||
Convert a dictionary of arguments back to an array of key=value.
|
||||
|
||||
Example:
|
||||
{"updateApp": True, "port": 8080}
|
||||
Returns:
|
||||
["updateApp=True", "port=8080"]
|
||||
"""
|
||||
if not kargs:
|
||||
kargs = read_kargs()
|
||||
|
||||
array: List[str] = []
|
||||
for key, value in kargs.items():
|
||||
array.append(f"{key}={value}")
|
||||
return array
|
||||
56
app/common/config.py
Normal file
56
app/common/config.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
from collections import OrderedDict
|
||||
|
||||
from .paths import LIBS_DIR
|
||||
|
||||
class Config:
|
||||
def __init__(self):
|
||||
self.libs: Dict[str, Dict[str, Any]] = {}
|
||||
self.load_libs_config()
|
||||
|
||||
def load_libs_config(self):
|
||||
for lib_path in LIBS_DIR.iterdir():
|
||||
if lib_path.is_dir():
|
||||
info_path = lib_path / "info.json"
|
||||
config_path = lib_path / "config.json"
|
||||
lib_id = lib_path.name
|
||||
|
||||
info_data = {}
|
||||
config_data = {}
|
||||
|
||||
if info_path.exists():
|
||||
with open(info_path) as f:
|
||||
info_data = json.load(f, object_pairs_hook=OrderedDict)
|
||||
|
||||
if config_path.exists():
|
||||
with open(config_path) as f:
|
||||
config_data = json.load(f, object_pairs_hook=OrderedDict)
|
||||
|
||||
config_data["info"] = info_data
|
||||
self.libs[lib_id] = config_data
|
||||
|
||||
def get(self, lib: str, *keys, default=None):
|
||||
"""
|
||||
Retrieves values from the config.
|
||||
|
||||
Example:
|
||||
config.get("app", "libs", "app", "update", "version", default="1.0")
|
||||
|
||||
Parameters:
|
||||
- lib: the name of the library
|
||||
- *keys: a sequence of keys to navigate through nested dictionaries
|
||||
- default: the value to return if any key in the chain does not exist
|
||||
|
||||
Returns:
|
||||
- The value found at the nested key path, or `default` if a key is missing.
|
||||
"""
|
||||
value = self.libs.get(lib, default)
|
||||
for key in keys:
|
||||
if isinstance(value, dict) and key in value:
|
||||
value = value[key]
|
||||
else:
|
||||
return default
|
||||
return value
|
||||
173
app/common/logging.py
Normal file
173
app/common/logging.py
Normal file
@@ -0,0 +1,173 @@
|
||||
import os
|
||||
import logging
|
||||
import inspect
|
||||
from collections import deque
|
||||
from typing import Deque
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from .paths import ROOT_DIR, LOGS_DIR
|
||||
|
||||
|
||||
class CustomLoggingFormatter(logging.Formatter):
|
||||
"""
|
||||
Custom logging formatter with ANSI colors for console output
|
||||
and standard format for file output.
|
||||
"""
|
||||
grey = "\x1b[0;37m"
|
||||
green = "\x1b[1;32m"
|
||||
yellow = "\x1b[1;33m"
|
||||
red = "\x1b[1;31m"
|
||||
purple = "\x1b[1;35m"
|
||||
blue = "\x1b[1;34m"
|
||||
light_blue = "\x1b[1;36m"
|
||||
bold_red = "\x1b[31;1m"
|
||||
reset = "\x1b[0m"
|
||||
|
||||
prefix = light_blue + '%(asctime)s' + reset + ' |'
|
||||
colored_level = '%(levelname)-8s'
|
||||
message = '| %(message)s'
|
||||
suffix = purple + ' (%(name)s %(filename)s:%(lineno)d)' + reset
|
||||
|
||||
FORMATS = {
|
||||
logging.DEBUG: prefix + grey + colored_level + reset + message + suffix,
|
||||
logging.INFO: prefix + blue + colored_level + reset + message + suffix,
|
||||
logging.WARNING: prefix + yellow + colored_level + reset + message + suffix,
|
||||
logging.ERROR: prefix + red + colored_level + reset + message + suffix,
|
||||
logging.CRITICAL: prefix + bold_red + colored_level + reset + message + suffix,
|
||||
}
|
||||
|
||||
file_format = '%(asctime)s | %(levelname)-8s | %(message)s | %(name)s (%(filename)s:%(lineno)d)'
|
||||
|
||||
def format(self, record):
|
||||
log_fmt = self.FORMATS.get(record.levelno, self.file_format)
|
||||
formatter = logging.Formatter(log_fmt)
|
||||
return formatter.format(record)
|
||||
|
||||
def add_logging_console_handler(logger: logging.Logger, logging_level=logging.DEBUG):
|
||||
"""
|
||||
Add console handler with colored formatter to a logger.
|
||||
"""
|
||||
sh = logging.StreamHandler()
|
||||
sh.setLevel(logging_level)
|
||||
sh.setFormatter(CustomLoggingFormatter())
|
||||
logger.addHandler(sh)
|
||||
|
||||
def add_logging_file_handler(logger: logging.Logger, directory: str, file_name: str, logging_level=logging.DEBUG):
|
||||
"""
|
||||
Add file handler with plain formatter to a logger.
|
||||
"""
|
||||
file_path = LOGS_DIR / directory / f"{file_name}.log"
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fh = logging.FileHandler(file_path, encoding="utf-8")
|
||||
fh.setLevel(logging_level)
|
||||
fh.setFormatter(logging.Formatter(CustomLoggingFormatter().file_format))
|
||||
logger.addHandler(fh)
|
||||
|
||||
class BufferHandler(logging.Handler):
|
||||
"""
|
||||
Logging handler that stores log records in a limited deque.
|
||||
Allows multiple listeners to be notified when a new log line is added.
|
||||
"""
|
||||
def __init__(self, maxlen: int = 200):
|
||||
super().__init__()
|
||||
self.buffer: Deque[str] = deque(maxlen=maxlen)
|
||||
self.formatter = CustomLoggingFormatter()
|
||||
self._listeners: list[callable] = []
|
||||
|
||||
def emit(self, record: logging.LogRecord):
|
||||
for listener in self._listeners:
|
||||
try:
|
||||
listener(record)
|
||||
except Exception as e:
|
||||
print(f"BufferHandler listener error: {e}")
|
||||
|
||||
def get_logs(self):
|
||||
return list(self.buffer)
|
||||
|
||||
def clear(self):
|
||||
self.buffer.clear()
|
||||
|
||||
def register_listener(self, listener: callable):
|
||||
"""
|
||||
Register a listener callback that will be called with
|
||||
each new log line.
|
||||
"""
|
||||
if listener not in self._listeners:
|
||||
self._listeners.append(listener)
|
||||
|
||||
def unregister_listener(self, listener: callable):
|
||||
"""Remove a previously registered listener."""
|
||||
if listener in self._listeners:
|
||||
self._listeners.remove(listener)
|
||||
|
||||
_LOGGER_BUFFERS: dict[str, BufferHandler] = {}
|
||||
|
||||
def add_logging_buffer_handler(logger: logging.Logger, logging_level=logging.DEBUG, maxlen: int = 300) -> BufferHandler:
|
||||
"""
|
||||
Add buffer handler with plain formatter to a logger.
|
||||
Returns the buffer so it can be accessed later.
|
||||
"""
|
||||
bh = BufferHandler(maxlen=maxlen)
|
||||
logger.addHandler(bh)
|
||||
_LOGGER_BUFFERS[logger.name] = bh
|
||||
|
||||
def get_logger(name: str = "", buffer: bool = False) -> logging.Logger:
|
||||
"""
|
||||
Returns a logger instance with console, file, and optional buffer handlers.
|
||||
Logger name is automatically derived from caller module path.
|
||||
"""
|
||||
file_path = os.path.normpath(inspect.stack()[1].filename)
|
||||
head = os.path.split(file_path)[0]
|
||||
|
||||
# Determine library name
|
||||
if head == os.path.normpath(ROOT_DIR):
|
||||
lib = "root"
|
||||
else:
|
||||
after_lib = head.replace(os.path.join(ROOT_DIR, "libs"), "")
|
||||
lib = after_lib.split(os.sep)[1] if len(after_lib.split(os.sep)) > 1 else "unknown"
|
||||
|
||||
# Build logger name
|
||||
if name == "root":
|
||||
logger_name = name
|
||||
elif name:
|
||||
logger_name = f"{lib}.{name}"
|
||||
else:
|
||||
logger_name = lib
|
||||
|
||||
file_name = name if name else lib
|
||||
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.propagate = False
|
||||
|
||||
if not logger.hasHandlers():
|
||||
logger.setLevel(logging.DEBUG)
|
||||
add_logging_console_handler(logger=logger, logging_level=logging.DEBUG)
|
||||
add_logging_file_handler(logger=logger, directory=lib, file_name=file_name, logging_level=logging.DEBUG)
|
||||
if buffer and logger_name not in _LOGGER_BUFFERS:
|
||||
add_logging_buffer_handler(logger, logging_level=logging.DEBUG)
|
||||
|
||||
return logger
|
||||
|
||||
def get_logger_buffer(logger_name: str):
|
||||
"""
|
||||
Returns the buffer of a previously configured logger.
|
||||
Raises KeyError if the logger does not have a buffer handler configured or does not exist.
|
||||
"""
|
||||
if logger_name in _LOGGER_BUFFERS:
|
||||
return _LOGGER_BUFFERS[logger_name]
|
||||
raise KeyError(f"Logger '{logger_name}' does not have a buffer handler configured or does not exist.")
|
||||
|
||||
def list_loggers() -> list[str]:
|
||||
"""
|
||||
Returns a list of all currently registered logger names.
|
||||
"""
|
||||
return sorted([
|
||||
name for name, obj in logging.Logger.manager.loggerDict.items()
|
||||
if isinstance(obj, logging.Logger)
|
||||
])
|
||||
|
||||
def config_root_logger() -> logging.Logger:
|
||||
"""
|
||||
Configure and return the root logger.
|
||||
"""
|
||||
return get_logger(name="root")
|
||||
25
app/common/network_utils.py
Normal file
25
app/common/network_utils.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import requests
|
||||
from requests.exceptions import RequestException, Timeout
|
||||
|
||||
|
||||
def check_url(url: str, timeout: int = 10, retries: int = 3, verify_ssl: bool = False) -> bool:
|
||||
"""
|
||||
Check if a given URL is reachable.
|
||||
|
||||
Args:
|
||||
url (str): The URL to check.
|
||||
timeout (int): Timeout for each request in seconds.
|
||||
retries (int): Number of retry attempts before failing.
|
||||
verify_ssl (bool): Verify SSL.
|
||||
|
||||
Returns:
|
||||
bool: True if the URL is reachable, False otherwise.
|
||||
"""
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
response = requests.head(url, timeout=timeout, allow_redirects=True, verify=verify_ssl)
|
||||
if 200 <= response.status_code < 400:
|
||||
return True
|
||||
except (RequestException, Timeout):
|
||||
continue
|
||||
return False
|
||||
8
app/common/paths.py
Normal file
8
app/common/paths.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from pathlib import Path
|
||||
|
||||
# Root directory of the project
|
||||
ROOT_DIR = Path(__file__).resolve().parents[3]
|
||||
|
||||
LOGS_DIR = ROOT_DIR / "logs"
|
||||
LIBS_DIR = ROOT_DIR / "libs"
|
||||
FILES_DIR = ROOT_DIR / "files"
|
||||
97
app/common/process.py
Normal file
97
app/common/process.py
Normal file
@@ -0,0 +1,97 @@
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
from typing import List, Optional
|
||||
from .paths import ROOT_DIR # centralized project paths
|
||||
|
||||
|
||||
def _find_terminal_emulator() -> Optional[str]:
|
||||
"""
|
||||
Try to find an available terminal emulator on Linux.
|
||||
Returns the command name if found, otherwise None.
|
||||
"""
|
||||
candidates = [
|
||||
"x-terminal-emulator",
|
||||
"gnome-terminal",
|
||||
"konsole",
|
||||
"xfce4-terminal",
|
||||
"lxterminal",
|
||||
"mate-terminal",
|
||||
"tilix",
|
||||
"terminator",
|
||||
"xterm"
|
||||
]
|
||||
for term in candidates:
|
||||
if shutil.which(term):
|
||||
return term
|
||||
return None
|
||||
|
||||
|
||||
def new_python_process(
|
||||
script_path: str,
|
||||
python_exec: str = sys.executable,
|
||||
args: Optional[List[str]] = None,
|
||||
cwd: str = str(ROOT_DIR),
|
||||
wait: bool = False,
|
||||
new_console: bool = False
|
||||
) -> int:
|
||||
"""
|
||||
Start a new Python process running the given script.
|
||||
|
||||
Args:
|
||||
script_path (str): Path to the Python script to execute.
|
||||
python_exec (str): Path to the Python executable.
|
||||
args (List[str], optional): Arguments to pass to the script. Defaults to [].
|
||||
cwd (str): Working directory for the process. Defaults to project root.
|
||||
wait (bool): If True, waits for process completion. Defaults to False.
|
||||
new_console (bool): If True, tries to open process in a new console window.
|
||||
|
||||
Returns:
|
||||
int: PID of the created process.
|
||||
"""
|
||||
if not os.path.isabs(python_exec):
|
||||
python_exec = os.path.normpath(os.path.join(cwd, python_exec))
|
||||
|
||||
if not os.path.isabs(script_path):
|
||||
script_path = os.path.normpath(os.path.join(cwd, script_path))
|
||||
|
||||
if args is None:
|
||||
args = []
|
||||
|
||||
command = [python_exec, script_path] + args
|
||||
|
||||
creation_flags = 0
|
||||
shell = False
|
||||
|
||||
system = platform.system().lower()
|
||||
|
||||
if new_console:
|
||||
if system == "windows":
|
||||
creation_flags = subprocess.CREATE_NEW_CONSOLE
|
||||
elif system == "linux":
|
||||
term = _find_terminal_emulator()
|
||||
if term:
|
||||
command = [term, "-e"] + command
|
||||
else:
|
||||
# fallback: run in same terminal
|
||||
pass
|
||||
elif system == "darwin": # MacOS
|
||||
osa_cmd = f'tell app "Terminal" to do script "{python_exec} {script_path} {" ".join(args)}"'
|
||||
process = subprocess.Popen(["osascript", "-e", osa_cmd], cwd=cwd)
|
||||
if wait:
|
||||
process.wait()
|
||||
return process.pid
|
||||
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
cwd=cwd,
|
||||
creationflags=creation_flags,
|
||||
shell=shell
|
||||
)
|
||||
|
||||
if wait:
|
||||
process.wait()
|
||||
|
||||
return process.pid
|
||||
75
app/common/store.py
Normal file
75
app/common/store.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from libs.app.common.paths import FILES_DIR
|
||||
|
||||
class DataStore:
|
||||
def __init__(self, path: str = "data/data.json", default_data:Optional[Dict[str, List[Any]]] = None, autosave:bool = True):
|
||||
if default_data is None:
|
||||
default_data = {"data": []}
|
||||
if not os.path.isabs(path):
|
||||
path = os.path.normpath(os.path.join(FILES_DIR, path))
|
||||
self.file_path = Path(path)
|
||||
self.default_data = default_data
|
||||
self.data = default_data.copy()
|
||||
self.autosave = autosave
|
||||
|
||||
self.load()
|
||||
|
||||
def load(self):
|
||||
if self.file_path.exists():
|
||||
with open(self.file_path, "r", encoding="utf-8") as f:
|
||||
self.data = json.load(f)
|
||||
else:
|
||||
os.makedirs(self.file_path.parent, exist_ok=True)
|
||||
self._save()
|
||||
|
||||
def save(self):
|
||||
with open(self.file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(self.data, f, indent=2)
|
||||
|
||||
def _save(self):
|
||||
if not self.autosave:
|
||||
return
|
||||
self.save()
|
||||
|
||||
def clear(self):
|
||||
self.data = self.default_data.copy()
|
||||
self._save()
|
||||
|
||||
|
||||
def add_item(self, parent:str, data: Dict[str, Any], unique: bool = False, id_field:Optional[str]=None, id: Optional[str]=None):
|
||||
if unique and id and id_field:
|
||||
if any(item.get(id_field) == id for item in self.data.get(parent, [])):
|
||||
raise ValueError(f"Item already exists in {parent}. {id_field}:{id}")
|
||||
|
||||
self.data.setdefault(parent, []).append(data)
|
||||
self._save()
|
||||
|
||||
def get_item(self, parent:str, id_field:str, id:str) -> Optional[Dict[str, Any]]:
|
||||
for item in self.data.get(parent, []):
|
||||
if item.get(id_field) == id:
|
||||
return item
|
||||
return None
|
||||
|
||||
def update_item(self, parent: str, id_field: str, id: str, updates: Dict[str, Any]) -> bool:
|
||||
for item in self.data.get(parent, []):
|
||||
if item.get(id_field) == id:
|
||||
item.update(updates)
|
||||
self._save()
|
||||
return True
|
||||
return False
|
||||
|
||||
def remove_item(self, parent:str, id_field:str, id:str) -> bool:
|
||||
items = self.data.get(parent, [])
|
||||
new_items = [item for item in items if item.get(id_field) != id]
|
||||
if len(new_items) != len(items):
|
||||
self.data[parent] = new_items
|
||||
self._save()
|
||||
return True
|
||||
return False
|
||||
|
||||
def list_items(self, parent:str) -> List[Dict[str, Any]]:
|
||||
return self.data.get(parent, [])
|
||||
44
app/config.json
Normal file
44
app/config.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"terminalWindow": true,
|
||||
|
||||
"repositories":["https://n0sys.duckdns.org/downloads/libs"],
|
||||
|
||||
"libs":[
|
||||
{
|
||||
"id":"app",
|
||||
"update": {
|
||||
"checkUpdates": true,
|
||||
"version": 0,
|
||||
"repositories": ["http://custom.repo/rendezvous"]
|
||||
},
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id":"fspn"
|
||||
},
|
||||
{
|
||||
"id":"api"
|
||||
},
|
||||
{
|
||||
"id":"noSys"
|
||||
},
|
||||
{
|
||||
"id":"fileTransfer"
|
||||
},
|
||||
{
|
||||
"id":"vueNoSys"
|
||||
},
|
||||
{
|
||||
"id":"lockbox"
|
||||
},
|
||||
{
|
||||
"id":"rendezvous"
|
||||
},
|
||||
{
|
||||
"id":"p2post"
|
||||
},
|
||||
{
|
||||
"id":"p2private"
|
||||
}
|
||||
]
|
||||
}
|
||||
5
app/info.json
Normal file
5
app/info.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"id": "app",
|
||||
"version": 0.111,
|
||||
"modules": []
|
||||
}
|
||||
117
app/main.py
Normal file
117
app/main.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import os, sys
|
||||
import time
|
||||
import logging
|
||||
import traceback
|
||||
import webbrowser
|
||||
import webview
|
||||
import threading
|
||||
|
||||
from common.paths import ROOT_DIR
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
from libs.app.common.logging import get_logger, get_logger_buffer, list_loggers
|
||||
from libs.app.common.args import read_kargs, kargs_to_array
|
||||
from libs.app.common.config import Config
|
||||
from libs.app.common.network_utils import check_url
|
||||
from libs.app.updater import Updater
|
||||
from libs.app.ui.ui_server import UIServer
|
||||
|
||||
logger = get_logger(buffer=True)
|
||||
logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)
|
||||
|
||||
|
||||
class App:
|
||||
def __init__(self):
|
||||
self.config = Config()
|
||||
|
||||
self.nosys_api_host = self.config.get("api", "server", "host")
|
||||
self.nosys_api_port = self.config.get("api", "server", "port")
|
||||
|
||||
self.ui_server = UIServer()
|
||||
self.ui_server.start()
|
||||
|
||||
self.updater = Updater(self.config)
|
||||
self.updater.register_listener(self.on_updater_event)
|
||||
|
||||
get_logger_buffer("app").register_listener(self.on_log)
|
||||
get_logger_buffer("app.updater").register_listener(self.on_log)
|
||||
|
||||
def start_frontend(self):
|
||||
url = "http://127.0.0.1:5000/"
|
||||
logger.debug("Opening browser...")
|
||||
webbrowser.open(url)
|
||||
|
||||
# logger.debug("Starting webview ...")
|
||||
# webview.create_window(
|
||||
# "NoSys",
|
||||
# url,
|
||||
# width=800,
|
||||
# height=800,
|
||||
# resizable=True,
|
||||
# confirm_close=False,
|
||||
# text_select=True,
|
||||
# frameless=False,
|
||||
# background_color="#000000",
|
||||
# )
|
||||
|
||||
# webview.start(debug=True)
|
||||
|
||||
def update_libraries(self):
|
||||
"""Update libs and restart if app itself was updated."""
|
||||
updated = self.updater.update_libs()
|
||||
if updated.get("app") == "Success updated":
|
||||
logger.warning("App updated, restarting...")
|
||||
self.ui_server.emit_event("restarting")
|
||||
self.ui_server.stop()
|
||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||
|
||||
def start_nosys(self):
|
||||
url = f"https://{self.nosys_api_host}:{self.nosys_api_port}/api/api/health"
|
||||
|
||||
logger.debug(f"Checking NoSys Server {url} ...")
|
||||
if check_url(url, 3, 1, verify_ssl=True):
|
||||
logger.debug("NoSys already running")
|
||||
self.ui_server.emit_event("redirect", {"url": f"https://{self.nosys_api_host}:{self.nosys_api_port}"})
|
||||
return
|
||||
|
||||
logger.debug("Starting NoSys")
|
||||
self.ui_server.emit_event("nosys_starting")
|
||||
from libs.noSys.noSysCore import NoSysCore
|
||||
from libs.noSys.events import Events
|
||||
|
||||
self.nosys_core = NoSysCore()
|
||||
get_logger_buffer("noSys").register_listener(self.on_log)
|
||||
get_logger_buffer("noSys").register_listener(self.on_log)
|
||||
get_logger_buffer("noSys.moduleManager").register_listener(self.on_log)
|
||||
self.nosys_core.subscribe_event(Events.READY, self.on_nosys_ready)
|
||||
self.nosys_core.start()
|
||||
|
||||
def start(self):
|
||||
self.start_frontend()
|
||||
self.update_libraries()
|
||||
self.config.load_libs_config()
|
||||
self.start_nosys()
|
||||
|
||||
def on_updater_event(self, event):
|
||||
if event.name == "status_lib":
|
||||
self.ui_server.emit_event(event.name, {"lib":event.lib, "status": event.status})
|
||||
|
||||
def on_log(self, record):
|
||||
self.ui_server.emit_event("log", {"levelname":record.levelname, "message":record.message})
|
||||
|
||||
def on_nosys_ready(self, event):
|
||||
logger.debug(f"Stoping startup frontend server and redirecting to nosys server {self.nosys_api_host}:{self.nosys_api_port} ...")
|
||||
self.ui_server.emit_event("redirect", {"url": f"https://{self.nosys_api_host}:{self.nosys_api_port}"})
|
||||
self.ui_server.stop()
|
||||
|
||||
def main():
|
||||
try:
|
||||
app = App()
|
||||
app.start()
|
||||
input("Done\nPress any key to close\n")
|
||||
except Exception:
|
||||
logger.exception("Error")
|
||||
input("Error\nPress any key to close\n")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
4
app/requirements.txt
Normal file
4
app/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
requests
|
||||
Flask
|
||||
flask-socketio
|
||||
pywebview
|
||||
142
app/start.py
Normal file
142
app/start.py
Normal file
@@ -0,0 +1,142 @@
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import shutil
|
||||
import pathlib
|
||||
import urllib.request
|
||||
import subprocess
|
||||
import zipfile
|
||||
import venv
|
||||
from pathlib import Path
|
||||
|
||||
# ==============================
|
||||
# Configuration constants
|
||||
# ==============================
|
||||
ROOT_DIR = Path(__file__).parent.resolve()
|
||||
LIBS_DIR = ROOT_DIR / "libs"
|
||||
LOGS_DIR = ROOT_DIR / "logs"
|
||||
APP_MAIN = LIBS_DIR / "app" / "main.py"
|
||||
APP_ZIP = LIBS_DIR / "app.zip"
|
||||
DEFAULT_REPOSITORY = "https://n0sys.duckdns.org/downloads/libs"
|
||||
ARGS_LIST = ["updateApp=False", "updateLibs=True", "repack=True"]
|
||||
|
||||
# ==============================
|
||||
# Logger setup
|
||||
# ==============================
|
||||
def setup_logger() -> logging.Logger:
|
||||
"""Configure application logger with console and rotating file handlers."""
|
||||
logger = logging.getLogger("start")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Log format
|
||||
formatter = logging.Formatter(
|
||||
fmt="%(asctime)s | %(levelname)-8s | %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
|
||||
# Console handler
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
console_handler.setFormatter(formatter)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
# File handler
|
||||
file_handler = RotatingFileHandler(LOGS_DIR / "start.log", maxBytes=5_000_000, backupCount=3, encoding="utf-8")
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setFormatter(formatter)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
return logger
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
# ==============================
|
||||
# Functions
|
||||
# ==============================
|
||||
def start_app():
|
||||
"""Entry point for the launcher"""
|
||||
logger.info("--------- START ---------")
|
||||
logger.debug(f"Root path: {ROOT_DIR}")
|
||||
|
||||
LIBS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# TODO: remove in production
|
||||
# update_libs()
|
||||
|
||||
create_venv()
|
||||
ensure_app()
|
||||
|
||||
# Import after app is available
|
||||
from libs.app.common.process import new_python_process
|
||||
from libs.app.common.args import read_kargs, kargs_to_array
|
||||
|
||||
args = kargs_to_array()
|
||||
pid = new_python_process(APP_MAIN, str(get_venv_python()), args=args, wait=True, new_console=True)
|
||||
logger.info(f"Main process running. PID {pid} - Args: {args}")
|
||||
logger.info("--------- END ---------")
|
||||
|
||||
|
||||
def get_venv_python():
|
||||
"""Return the path to the venv's Python executable in a cross-platform way"""
|
||||
if os.name == "nt":
|
||||
return ROOT_DIR / ".venv" / "Scripts" / "python.exe"
|
||||
else:
|
||||
return ROOT_DIR / ".venv" / "bin" / "python"
|
||||
|
||||
def update_libs():
|
||||
"""Development only: repack local libs"""
|
||||
if ("updateApp=True" in ARGS_LIST or "updateLibs=True" in ARGS_LIST) and "repack=True" in ARGS_LIST:
|
||||
update_version_script = r"C:\Workspace\utils\updateLibsVersion.py"
|
||||
try:
|
||||
subprocess.run(
|
||||
[str(get_venv_python()), update_version_script],
|
||||
cwd=ROOT_DIR,
|
||||
creationflags=subprocess.CREATE_NEW_CONSOLE,
|
||||
check=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Error running update libs: {e}")
|
||||
|
||||
def ensure_app():
|
||||
"""Download, extract and install requirements of the application if not already present"""
|
||||
if APP_MAIN.exists():
|
||||
logger.debug("App already present, skipping download.")
|
||||
return
|
||||
|
||||
url = f"{DEFAULT_REPOSITORY}/app/app.zip"
|
||||
logger.info(f"Downloading app from {url} ...")
|
||||
try:
|
||||
urllib.request.urlretrieve(url, APP_ZIP)
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to download app")
|
||||
raise
|
||||
|
||||
logger.info(f"Extracting {APP_ZIP}...")
|
||||
with zipfile.ZipFile(APP_ZIP, 'r') as zip_ref:
|
||||
zip_ref.extractall(LIBS_DIR)
|
||||
|
||||
logger.info(f"Installing app requirements ...")
|
||||
python_exec = str(get_venv_python())
|
||||
requirements_path = os.path.join(ROOT_DIR, "libs/app/requirements.txt")
|
||||
subprocess.check_call([python_exec, "-m", "pip", "install", "-r", requirements_path])
|
||||
|
||||
|
||||
def create_venv():
|
||||
"""Ensure a local virtual environment exists."""
|
||||
venv_dir = os.path.join(ROOT_DIR, ".venv")
|
||||
if not os.path.exists(venv_dir):
|
||||
logger.debug(f"Creating python venv: {venv_dir}")
|
||||
venv.create(venv_dir, with_pip=True)
|
||||
|
||||
# ==============================
|
||||
# Main
|
||||
# ==============================
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
start_app()
|
||||
except Exception:
|
||||
logger.exception("Error starting application")
|
||||
sys.exit(1)
|
||||
65
app/ui/static/main.js
Normal file
65
app/ui/static/main.js
Normal file
@@ -0,0 +1,65 @@
|
||||
const socket = io();
|
||||
// const socket = io("http://127.0.0.1:5000");
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log("Connected...");
|
||||
socket.emit("frontend_ready");
|
||||
});
|
||||
|
||||
const logContainer = document.getElementById("log-container");
|
||||
|
||||
function appendLog(data) {
|
||||
const line = document.createElement("div");
|
||||
line.textContent = data.levelname + " : " + data.message;
|
||||
logContainer.appendChild(line);
|
||||
logContainer.scrollTop = logContainer.scrollHeight;
|
||||
}
|
||||
|
||||
const statusTree = document.getElementById("updater-tree");
|
||||
let statusTreeData = {};
|
||||
|
||||
function renderStatusTable() {
|
||||
let html = `
|
||||
<table border="1" cellpadding="6">
|
||||
<tr>
|
||||
<th>Módulo</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
`;
|
||||
for (const [module, status] of Object.entries(statusTreeData)) {
|
||||
html += `
|
||||
<tr>
|
||||
<td>${module}</td>
|
||||
<td>${status}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
html += `</table>`;
|
||||
statusTree.innerHTML = html;
|
||||
}
|
||||
|
||||
socket.on("status_lib", (data) => {
|
||||
console.log("lib status", data);
|
||||
|
||||
statusTreeData[data.lib] = data.status;
|
||||
|
||||
renderStatusTable();
|
||||
});
|
||||
|
||||
socket.on("redirect", (data) => {
|
||||
console.log("redirect", data);
|
||||
location.replace(data.url);
|
||||
});
|
||||
|
||||
socket.on("restarting", (data) => {
|
||||
console.log("restarting", data);
|
||||
location.reload()
|
||||
});
|
||||
|
||||
socket.on("log", (data) => {
|
||||
appendLog(data);
|
||||
});
|
||||
|
||||
socket.on("error", (data) => {
|
||||
appendLog(data);
|
||||
});
|
||||
22
app/ui/templates/index.html
Normal file
22
app/ui/templates/index.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>NoSys Startup</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 20px; }
|
||||
ul { list-style-type: none; padding-left: 20px; }
|
||||
li { margin: 5px 0; }
|
||||
.status { font-weight: bold; }
|
||||
</style>
|
||||
</head>
|
||||
<body style="background-color:black; color: white;">
|
||||
<h1>NoSys Startup Status</h1>
|
||||
<ul id="updater-tree"></ul>
|
||||
<div id="log-container"></div>
|
||||
|
||||
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
|
||||
<!-- <script src="/static/main.js"></script> -->
|
||||
<script src="static/main.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
76
app/ui/ui_server.py
Normal file
76
app/ui/ui_server.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from libs.app.common.logging import get_logger
|
||||
from flask import Flask, render_template, request
|
||||
from flask_socketio import SocketIO
|
||||
import multiprocessing
|
||||
import threading
|
||||
import time
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
class UIServer:
|
||||
def __init__(self, host="127.0.0.1", port=5000):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.process = None
|
||||
self.queue = multiprocessing.Queue()
|
||||
self.client_ready = multiprocessing.Value("b", False)
|
||||
|
||||
@staticmethod
|
||||
def run_server(host, port, queue, client_ready):
|
||||
app = Flask(__name__)
|
||||
socketio = SocketIO(app, cors_allowed_origins="*")
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html")
|
||||
|
||||
@socketio.on("frontend_ready")
|
||||
def frontend_ready():
|
||||
client_ready.value = True
|
||||
|
||||
def queue_listener():
|
||||
while True:
|
||||
event, data = queue.get()
|
||||
while not client_ready.value:
|
||||
time.sleep(1)
|
||||
|
||||
socketio.emit(event, data)
|
||||
|
||||
threading.Thread(target=queue_listener, daemon=True).start()
|
||||
|
||||
socketio.run(app, host=host, port=port, debug=False, use_reloader=False)
|
||||
|
||||
def start(self):
|
||||
if self.process and self.process.is_alive():
|
||||
logger.debug("Server is already running")
|
||||
return
|
||||
self.process = multiprocessing.Process(
|
||||
target=UIServer.run_server,
|
||||
args=(self.host, self.port, self.queue, self.client_ready),
|
||||
)
|
||||
self.process.start()
|
||||
logger.debug(f"Server started on http://{self.host}:{self.port}")
|
||||
|
||||
def stop(self, wait_queue=True, time_limit=5):
|
||||
while wait_queue and not self.queue.empty():
|
||||
logger.debug(f"Waiting server. {self.queue.qsize()} items in queue")
|
||||
time.sleep(1)
|
||||
time_limit-=1
|
||||
if time_limit <= 0:
|
||||
break
|
||||
|
||||
if self.process and self.process.is_alive():
|
||||
self.process.terminate()
|
||||
self.process.join()
|
||||
logger.debug("Server stopped")
|
||||
self.process = None
|
||||
|
||||
def restart(self):
|
||||
logger.debug("Restarting server...")
|
||||
self.stop()
|
||||
self.start()
|
||||
|
||||
def emit_event(self, event, data={}):
|
||||
self.queue.put((event, data))
|
||||
|
||||
|
||||
135
app/updater.py
Normal file
135
app/updater.py
Normal file
@@ -0,0 +1,135 @@
|
||||
import sys
|
||||
import subprocess
|
||||
import urllib.request
|
||||
import os
|
||||
import json
|
||||
import types
|
||||
import logging
|
||||
import time
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
|
||||
from libs.app.common.config import Config
|
||||
from libs.app.common.logging import get_logger
|
||||
from libs.app.common.paths import LIBS_DIR
|
||||
|
||||
logger = get_logger("updater", buffer=True)
|
||||
|
||||
class Updater:
|
||||
"""
|
||||
Updater class responsible for managing library updates.
|
||||
"""
|
||||
def __init__(self, config:Config):
|
||||
self.config = config
|
||||
self.repositories = self.config.get("app", "repositories", default=[])
|
||||
self.updated_libs: dict[str, any] = {}
|
||||
|
||||
self._event_listeners: list[callable] = []
|
||||
|
||||
def update_libs(self):
|
||||
"""Check and update all configured libraries in parallel."""
|
||||
logger.debug("Checking libraries updates...")
|
||||
threads: list[Thread] = []
|
||||
|
||||
for lib in self.config.get("app", "libs", default=[]):
|
||||
lib_id = lib.get("id")
|
||||
self._update_lib_status(lib_id, "Reading configuration")
|
||||
if lib.get("update", {"checkUpdates":True}).get("checkUpdates", True):
|
||||
self._update_lib_status(lib_id, "Checking updates")
|
||||
thread = Thread(target=self.check_lib_update, args=(lib,))
|
||||
thread.start()
|
||||
threads.append(thread)
|
||||
else:
|
||||
self._update_lib_status(lib_id, "Checking updates disabled")
|
||||
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
return self.updated_libs
|
||||
|
||||
def check_lib_update(self, lib:dict):
|
||||
"""
|
||||
Check for a library update in repositories and install if newer version is available.
|
||||
"""
|
||||
lib_id = lib.get("id")
|
||||
lib_config = self.config.get(lib_id)
|
||||
lib_repositories = lib.get("update", {"repositories":[]}).get("repositories", [])
|
||||
lib_path = LIBS_DIR / lib_id
|
||||
|
||||
if not lib_config:
|
||||
Path(lib_path).mkdir(parents=True, exist_ok=True)
|
||||
current_version = 0
|
||||
else:
|
||||
current_version = lib_config.get("info").get("version")
|
||||
|
||||
logger.debug(f"Checking updates of lib {lib_id} with current version {current_version}")
|
||||
|
||||
repositories = list(self.repositories)
|
||||
repositories.extend(lib_repositories)
|
||||
latest_version = (current_version, None)
|
||||
|
||||
for repo in repositories:
|
||||
try:
|
||||
info_url = f"{repo}/{lib_id}/info.json"
|
||||
logger.debug(f"Reading remote info of {lib_id} from repository {repo}")
|
||||
|
||||
with urllib.request.urlopen(info_url, timeout=2) as response:
|
||||
remote_info = json.load(response)
|
||||
logger.debug(f"{lib_id} remote info: {remote_info}")
|
||||
|
||||
remote_version = remote_info.get("version")
|
||||
if remote_version > latest_version[0]:
|
||||
latest_version = (remote_version, f"{repo}/{lib_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading remote info of {lib_id} from {repo}: {e}")
|
||||
|
||||
if latest_version[1]:
|
||||
lib_url = f"{latest_version[1]}/{lib_id}.zip"
|
||||
lib_file = LIBS_DIR / f"{lib_id}.zip"
|
||||
|
||||
logger.debug(f"Downloading lib from {lib_url} to {lib_file}")
|
||||
self._update_lib_status(lib_id, f"Downloading lastest lib version {latest_version[0]}")
|
||||
self.download_lib(lib_url, lib_file)
|
||||
|
||||
self._update_lib_status(lib_id, f"Installing lib requirements")
|
||||
self.pip_install_requirements(lib_path)
|
||||
self._update_lib_status(lib_id, f"Success updated")
|
||||
else:
|
||||
self._update_lib_status(lib_id, f"Current version is the latest")
|
||||
|
||||
def download_lib(self, url: str, path: str) -> None:
|
||||
"""Download and extract a lib from repository."""
|
||||
urllib.request.urlretrieve(url, path)
|
||||
logger.info(f"Extracting all from {path}")
|
||||
with zipfile.ZipFile(path, "r") as zip_ref:
|
||||
zip_ref.extractall(Path(path).parent.absolute())
|
||||
|
||||
def pip_install_requirements(self, lib_path: str) -> None:
|
||||
"""Run pip install on requirements.txt if present."""
|
||||
requirements_path = os.path.join(lib_path, "requirements.txt")
|
||||
if os.path.exists(requirements_path):
|
||||
logger.info(f"Installing requirements from {requirements_path}")
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", requirements_path])
|
||||
else:
|
||||
logger.info(f"No requirements.txt found for {lib_path}")
|
||||
|
||||
def register_listener(self, callback: callable):
|
||||
"""
|
||||
Register a listener callback that will be called with
|
||||
each new event.
|
||||
"""
|
||||
if callback not in self._event_listeners:
|
||||
self._event_listeners.append(callback)
|
||||
|
||||
def emit_event(self, **kwargs):
|
||||
event = types.SimpleNamespace()
|
||||
for k, v in kwargs.items():
|
||||
setattr(event, k, v)
|
||||
for listener in self._event_listeners:
|
||||
listener(event)
|
||||
|
||||
def _update_lib_status(self, lib, status):
|
||||
self.updated_libs[lib] = status
|
||||
self.emit_event(name="status_lib", lib=lib, status=status)
|
||||
257
app/utilss.py
Normal file
257
app/utilss.py
Normal file
@@ -0,0 +1,257 @@
|
||||
import os, sys
|
||||
import uuid
|
||||
import pathlib
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
root_dir = os.path.normpath(__file__.split("libs")[0])
|
||||
node_id = str(uuid.uuid4())
|
||||
|
||||
### LOGGER ###
|
||||
import logging, inspect
|
||||
|
||||
class CustomLoggingFormatter(logging.Formatter):
|
||||
grey = "\x1b[0;37m"
|
||||
green = "\x1b[1;32m"
|
||||
yellow = "\x1b[1;33m"
|
||||
red = "\x1b[1;31m"
|
||||
purple = "\x1b[1;35m"
|
||||
blue = "\x1b[1;34m"
|
||||
light_blue = "\x1b[1;36m"
|
||||
bold_red = "\x1b[31;1m"
|
||||
blink_red = "\x1b[5m\x1b[1;31m"
|
||||
reset = "\x1b[0m"
|
||||
prefix = light_blue + '%(asctime)s' + reset + ' |'
|
||||
colored_level = '%(levelname)-8s'
|
||||
message = '| %(message)s' + ''
|
||||
suffix = purple + ' (%(name)s %(filename)s:%(lineno)d)' + reset
|
||||
|
||||
FORMATS = {
|
||||
logging.DEBUG: prefix + grey + colored_level + reset + message + suffix,
|
||||
logging.INFO: prefix + blue + colored_level + reset + message + suffix,
|
||||
logging.WARNING: prefix + yellow + colored_level + reset + message + suffix,
|
||||
logging.ERROR: prefix + red + colored_level + reset + message + suffix,
|
||||
logging.CRITICAL: prefix + bold_red + colored_level + reset + message + suffix
|
||||
}
|
||||
|
||||
file_format = '%(asctime)s | %(levelname)-8s | %(message)s | %(name)s (%(filename)s:%(lineno)d)'
|
||||
|
||||
def format(self, record):
|
||||
log_fmt = self.FORMATS.get(record.levelno)
|
||||
formatter = logging.Formatter(log_fmt)
|
||||
return formatter.format(record)
|
||||
|
||||
# TODO Dinamicaly get caller lib path
|
||||
# class FileFilter(logging.Filter):
|
||||
# def filter(self, record):
|
||||
# file_path = os.path.normpath(inspect.stack()[1].filename)
|
||||
# head = os.path.split(file_path)[0]
|
||||
# tail = os.path.split(file_path)[1]
|
||||
# if head == os.path.normpath(root_dir):
|
||||
# name = "app"
|
||||
# else:
|
||||
# after_lib = head.replace(os.path.join(root_dir, "libs"), "")
|
||||
# name = after_lib.split(os.sep)[1]
|
||||
# record.name = name
|
||||
# print(name)
|
||||
# return True
|
||||
|
||||
|
||||
def add_logging_console_handler(logger:logging.Logger, logging_level=logging.DEBUG):
|
||||
sh = logging.StreamHandler()
|
||||
sh.setLevel(logging_level)
|
||||
sh.setFormatter(CustomLoggingFormatter())
|
||||
logger.addHandler(sh)
|
||||
|
||||
def add_logging_file_handler(logger:logging.Logger, directory, file_name, logging_level=logging.DEBUG):
|
||||
file_path = os.path.join(root_dir, "logs", directory, f"{file_name}.log")
|
||||
pathlib.Path(file_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
fh = logging.FileHandler(file_path)
|
||||
fh.setLevel(logging_level)
|
||||
fh.setFormatter(logging.Formatter(CustomLoggingFormatter().file_format))
|
||||
logger.addHandler(fh)
|
||||
|
||||
def config_root_logger():
|
||||
logger = get_logger(name="root")
|
||||
# TODO Get lib that is calling logging
|
||||
# logger.addFilter(FileFilter())
|
||||
|
||||
def get_logger(name=""):
|
||||
file_path = os.path.normpath(inspect.stack()[1].filename)
|
||||
head = os.path.split(file_path)[0]
|
||||
tail = os.path.split(file_path)[1]
|
||||
if head == os.path.normpath(root_dir):
|
||||
lib = "root"
|
||||
else:
|
||||
after_lib = head.replace(os.path.join(root_dir, "libs"), "")
|
||||
lib = after_lib.split(os.sep)[1]
|
||||
|
||||
if name == "root":
|
||||
logger_name = name
|
||||
elif name:
|
||||
logger_name = f"{lib}.{name}"
|
||||
else:
|
||||
logger_name = lib
|
||||
|
||||
file_name = name if name else lib
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.propagate = False
|
||||
if not logger.hasHandlers():
|
||||
logger.setLevel(logging.DEBUG)
|
||||
add_logging_console_handler(logger=logger, logging_level=logging.DEBUG)
|
||||
add_logging_file_handler(logger=logger, directory=lib, file_name=file_name, logging_level=logging.DEBUG)
|
||||
|
||||
return logger
|
||||
|
||||
logger = get_logger()
|
||||
logger.debug(f"Root dir: {root_dir}")
|
||||
|
||||
### VENV ###
|
||||
import venv
|
||||
|
||||
def create_venv():
|
||||
venv_dir = os.path.join(root_dir, ".venv")
|
||||
if not os.path.exists(venv_dir):
|
||||
logger.debug(f"Creating python venv: {venv_dir}")
|
||||
venv.create(venv_dir, with_pip=True)
|
||||
|
||||
### DOWNLOADS ###
|
||||
import urllib.request
|
||||
|
||||
def download_file(url, path):
|
||||
head = os.path.split(path)[0]
|
||||
if not os.path.exists(head):
|
||||
logger.debug(f"Creating repository {head}")
|
||||
pathlib.Path(head).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logger.debug(f"Downloading from URL: {url} to {path}")
|
||||
urllib.request.urlretrieve(url, path)
|
||||
|
||||
### KARGS ###
|
||||
def read_kargs():
|
||||
kargs = {}
|
||||
params = sys.argv
|
||||
for param in params:
|
||||
if "=" in param:
|
||||
split = param.split("=")
|
||||
key = split[0]
|
||||
value = split[1]
|
||||
|
||||
if value == "true" or value == "True":
|
||||
value = True
|
||||
elif value == "false" or value == "False":
|
||||
value = False
|
||||
|
||||
kargs[key] = value
|
||||
kargs["rootDir"] = root_dir
|
||||
return kargs
|
||||
|
||||
def kargs_to_array(kargs={}):
|
||||
if not kargs:
|
||||
kargs = read_kargs()
|
||||
array = []
|
||||
for key, value in kargs.items():
|
||||
array.append(f"{key}={value}")
|
||||
return array
|
||||
|
||||
### HTTP UTILS ###
|
||||
def is_http_running(url):
|
||||
try:
|
||||
u:urllib.request.URLopener = urllib.request.urlopen(url)
|
||||
u.close()
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
### UTILS ###
|
||||
|
||||
class Utils:
|
||||
def __init__(self):
|
||||
self.kargs = read_kargs()
|
||||
self.configs = Config()
|
||||
self.flags = {}
|
||||
|
||||
# def get_env():
|
||||
# return os.environ.get('NOSYS_ENV', "PROD")
|
||||
|
||||
# def download_file(self, package, file, destination, repository=None):
|
||||
# if not repository:
|
||||
# repository = self.default_repository
|
||||
|
||||
# url = f"{repository}/{package}/{file}"
|
||||
# logger.debug(f"Downloading {file} from URL: {url}")
|
||||
# urllib.request.urlretrieve(url, destination)
|
||||
|
||||
# def download_main_file(self, ignoreIfExists=True):
|
||||
# main_path = os.path.join(self.root_dir, "libs","app", "main.py")
|
||||
# if (not os.path.exists(main_path)) or (os.path.exists(main_path) and not ignoreIfExists):
|
||||
# self.download_file(package="app", file="main.py", destination=main_path)
|
||||
|
||||
def is_terminal_visible(self):
|
||||
try:
|
||||
return self.configs.data["main"]["terminalWindow"]
|
||||
except Exception as e:
|
||||
return True
|
||||
|
||||
# def _create_log_dir(self):
|
||||
# logs_path = os.path.join(self.root_dir, "logs")
|
||||
# if not os.path.exists(logs_path):
|
||||
# logger.debug(f"Creating logs directory: {logs_path}")
|
||||
# pathlib.Path(logs_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def new_python_process(self, file_path, args):
|
||||
python_executable = "python" if self.is_terminal_visible() else "pythonw"
|
||||
args = [f"{root_dir}/.venv/scripts/{python_executable}", file_path] + args
|
||||
logger.debug(f"Starting a new process: {args}")
|
||||
process = subprocess.Popen(args, cwd=root_dir, creationflags=subprocess.CREATE_NEW_CONSOLE)
|
||||
logger.debug(f"Process PID {process.pid} - {args}")
|
||||
return process.pid
|
||||
|
||||
def restart_app(self):
|
||||
args = [sys.executable, "start.py"] + kargs_to_array(self.kargs)
|
||||
subprocess.Popen(args, cwd=root_dir, creationflags=subprocess.CREATE_NEW_CONSOLE)
|
||||
logger.info(f"Restarting app: {args}")
|
||||
self.exit_app()
|
||||
|
||||
def exit_app(self, exit_code=1):
|
||||
os._exit(exit_code)
|
||||
# sys.exit(exit_code)
|
||||
|
||||
class Config:
|
||||
def __init__(self):
|
||||
self.data = None
|
||||
self.packages:dict[str, Package] = {}
|
||||
|
||||
self.read_config()
|
||||
|
||||
def read_config(self):
|
||||
config_path = os.path.join(root_dir, "libs", "app", "config.json")
|
||||
if(os.path.exists(config_path)):
|
||||
with open(config_path) as f:
|
||||
data = json.load(f)
|
||||
self.data = data
|
||||
self.read_packages()
|
||||
else:
|
||||
raise Exception(f"Config file {config_path} not exist")
|
||||
|
||||
def read_packages(self):
|
||||
for package in self.data["packages"]:
|
||||
try:
|
||||
with open(os.path.join(root_dir,'libs',package["id"],'info.json')) as f:
|
||||
self.packages[package["id"]] = Package(config=package, info=json.load(f))
|
||||
except Exception as e:
|
||||
self.packages[package["id"]] = Package(config=package, info={})
|
||||
logger.error(e)
|
||||
|
||||
class Package:
|
||||
def __init__(self, config={}, info={}):
|
||||
self.config = config
|
||||
self.info = info
|
||||
self.modules:dict[str, any] = {}
|
||||
|
||||
self.read_modules()
|
||||
|
||||
def read_modules(self):
|
||||
if "modules" in self.info:
|
||||
for module in self.info["modules"]:
|
||||
self.modules[module["id"]] = module
|
||||
1
fileTransfer/.mtimes.json
Normal file
1
fileTransfer/.mtimes.json
Normal file
@@ -0,0 +1 @@
|
||||
{"file.py": 1757577571.3943994, "fileTransfer.py": 1756117680.0193572}
|
||||
109
fileTransfer/file.py
Normal file
109
fileTransfer/file.py
Normal file
@@ -0,0 +1,109 @@
|
||||
import sys, os
|
||||
import math
|
||||
from enum import Enum, auto
|
||||
|
||||
from libs.app.common.logging import get_logger
|
||||
from libs.fspn.utils.observable import Observable
|
||||
from libs.fspn.utils.wrapper_util import threaded
|
||||
from libs.fspn.utils.sha256_util import hash_bytes, hash_file
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
class FileEvents(Enum):
|
||||
ON_FILE_COMPLETED = auto()
|
||||
ON_FILE_ERROR = auto()
|
||||
ON_FILE_UPDATE = auto()
|
||||
ON_FILE_APPROVED = auto()
|
||||
|
||||
# TODO Add try except and retries
|
||||
class File(Observable):
|
||||
def __init__(self, folder, name, size, chunk_size, hash, connection_id, sending, file_transfer, to_module) -> None:
|
||||
self.id = (hash, connection_id)
|
||||
self.name = name
|
||||
self.folder = folder
|
||||
self.size = size
|
||||
self.chunk_size = chunk_size
|
||||
self.parts = [None] * math.ceil(size/chunk_size)
|
||||
self.hash = hash
|
||||
self.status = "WAITING"
|
||||
self.sending = sending
|
||||
self.connection_id = connection_id
|
||||
from libs.fileTransfer.fileTransfer import FileTransfer
|
||||
self.file_transfer:FileTransfer = file_transfer
|
||||
self.to_module = to_module
|
||||
|
||||
self.output_path = os.path.join(self.folder, self.name + '.download')
|
||||
self.final_path = self.get_final_path()
|
||||
|
||||
def get_final_path(self):
|
||||
base_name, ext = os.path.splitext(self.name)
|
||||
count = 0
|
||||
while True:
|
||||
if count == 0:
|
||||
filename = f"{base_name}{ext}"
|
||||
else:
|
||||
filename = f"{base_name}({count}){ext}"
|
||||
final_path = os.path.join(self.folder, filename)
|
||||
if not os.path.exists(final_path):
|
||||
return final_path
|
||||
count += 1
|
||||
|
||||
def approve_transfer(self, approved):
|
||||
if approved:
|
||||
self.status = "TRANSFERING"
|
||||
if self.sending:
|
||||
self.start_send()
|
||||
else:
|
||||
# TODO Check if file exists and same hash
|
||||
if not os.path.exists(self.output_path):
|
||||
os.makedirs(self.folder, exist_ok=True)
|
||||
with open(self.output_path, 'wb') as f:
|
||||
f.truncate(self.size)
|
||||
else:
|
||||
self.status = "CANCELED"
|
||||
|
||||
self.fire_event(FileEvents.ON_FILE_APPROVED.name, approved=approved)
|
||||
|
||||
@threaded
|
||||
def start_send(self):
|
||||
logger.warning("Start Sending")
|
||||
f = open(os.path.join(self.folder, self.name), 'rb')
|
||||
for part in range(len(self.parts)):
|
||||
f.seek(part * self.chunk_size)
|
||||
data = f.read(self.chunk_size)
|
||||
part_hash = hash_bytes(data)
|
||||
logger.debug(f"Sending part {part}")
|
||||
# time.sleep(0.5)
|
||||
self.file_transfer.send_file_part(self, part, part_hash, data)
|
||||
f.close()
|
||||
|
||||
@threaded
|
||||
def write_part(self, data, part, hash):
|
||||
check_hash = hash_bytes(data)
|
||||
logger.debug(f"Writing part {part}")
|
||||
if check_hash == hash:
|
||||
part_offset = part * self.chunk_size
|
||||
with open(self.output_path, 'r+b') as f:
|
||||
f.seek(part_offset)
|
||||
f.write(data)
|
||||
|
||||
self.parts[part] = True
|
||||
# TODO Send ack
|
||||
self.update_status()
|
||||
else:
|
||||
# TODO Request part again
|
||||
logger.error("HASH PART ERROR")
|
||||
|
||||
def update_status(self):
|
||||
if all(x for x in self.parts):
|
||||
os.rename(self.output_path, self.final_path)
|
||||
if hash_file(self.final_path) == self.hash:
|
||||
logger.info(f'File {self.name} downloaded!')
|
||||
self.status = "COMPLETED"
|
||||
self.fire_event(FileEvents.ON_FILE_COMPLETED.name, final_path=self.final_path)
|
||||
else:
|
||||
self.status = "CORRUPTED"
|
||||
logger.warning(f'File {self.name} corrupted')
|
||||
self.fire_event(FileEvents.ON_FILE_ERROR.name)
|
||||
else:
|
||||
self.fire_event(FileEvents.ON_FILE_UPDATE.name)
|
||||
100
fileTransfer/fileTransfer.py
Normal file
100
fileTransfer/fileTransfer.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import sys, os
|
||||
import math
|
||||
from enum import Enum, auto
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
from libs.noSys.noSysModule import NoSysModule
|
||||
from libs.app.common.logging import get_logger
|
||||
from libs.app.common.paths import ROOT_DIR
|
||||
from libs.fspn.utils.observable import Observable
|
||||
from libs.fspn.utils.wrapper_util import threaded
|
||||
from libs.fspn.utils.sha256_util import hash_bytes, hash_file
|
||||
|
||||
from .file import File, FileEvents
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
class FileTransferEvents(Enum):
|
||||
ON_RECEIVING_ = auto()
|
||||
|
||||
class FileTransfer(NoSysModule):
|
||||
def __init__(self, nosys_core):
|
||||
super().__init__(nosys_core)
|
||||
self.files:dict[tuple[str,str], File] = {}
|
||||
|
||||
def send_file(self, file_path, connection_id, to_module):
|
||||
path = Path(file_path)
|
||||
file_folder = path.parent
|
||||
file_name = path.name
|
||||
file_size = path.stat().st_size
|
||||
chunk_size = self.get_dynamic_chunk_size(file_size)
|
||||
file_hash = hash_file(file_path)
|
||||
file = File(file_folder, file_name, file_size, chunk_size, file_hash, connection_id, True, self, to_module)
|
||||
file.subscribe_event(FileEvents.ON_FILE_APPROVED.name, self.on_file_approved)
|
||||
self.files[file.id] = file
|
||||
self.send_file_info(connection_id, file_name, file_size, chunk_size, file_hash, to_module)
|
||||
|
||||
def get_dynamic_chunk_size(self, file_size: int) -> int:
|
||||
min_chunk = 64 * 1024 # 64 KB
|
||||
max_chunk = 2 * 1024 * 1024 # 2 MB
|
||||
target_chunks = 500
|
||||
ideal_chunk = file_size // target_chunks
|
||||
return max(min_chunk, min(ideal_chunk, max_chunk))
|
||||
|
||||
def on_module_message(self, event):
|
||||
if event.meta:
|
||||
action = event.meta["action"]
|
||||
else:
|
||||
action = event.data['action']
|
||||
handler_action = getattr(self, 'on_'+action)
|
||||
handler_action(event)
|
||||
|
||||
def send_file_info(self, connection_id, name, size, chunk_size, hash, to_module):
|
||||
body = {"action":"file_info", "name":name, "size":size, "chunk_size":chunk_size, "hash":hash, "module":{"package": to_module[0], "name":to_module[1]}}
|
||||
self.nosys_core.dispatcher.send_message(body, connection_id, self.id)
|
||||
|
||||
def on_file_info(self, event):
|
||||
payload = event.data
|
||||
folder = os.path.join(ROOT_DIR, "files")
|
||||
to_module = (payload['module']['package'], payload['module']['name'])
|
||||
file = File(folder, payload["name"], payload["size"], payload["chunk_size"], payload["hash"], event.connection.id, False, self, to_module)
|
||||
file.subscribe_event(FileEvents.ON_FILE_APPROVED.name, self.on_file_approved)
|
||||
self.files[file.id] = file
|
||||
self.fire_event(f"{FileTransferEvents.ON_RECEIVING_.name}{to_module[0]}_{to_module[1]}", file=file)
|
||||
|
||||
def subscribe_module_file_events(self, module:tuple[str, str], callback):
|
||||
self.subscribe_event(f"{FileTransferEvents.ON_RECEIVING_.name}{module[0]}_{module[1]}", callback)
|
||||
|
||||
def on_file_approved(self, event):
|
||||
file:File = event.source
|
||||
approved = event.approved
|
||||
|
||||
if file.sending:
|
||||
logger.debug(f"Peer file approved {approved}")
|
||||
else:
|
||||
self.send_file_transfer_approved(file, approved)
|
||||
|
||||
def send_file_transfer_approved(self, file:File, approved):
|
||||
body = {"action":"file_transfer_approved", "file_hash":file.hash, "approved":approved}
|
||||
self.nosys_core.dispatcher.send_message(body, file.connection_id, self.id)
|
||||
|
||||
def on_file_transfer_approved(self, event):
|
||||
payload = event.data
|
||||
file_id = (payload["file_hash"], event.connection.id)
|
||||
file:File = self.files[file_id]
|
||||
file.approve_transfer(payload["approved"])
|
||||
|
||||
def send_file_part(self, file:File, part, part_hash, data):
|
||||
meta = {"action":"file_part", "part":part, "part_hash":part_hash, "file_hash": file.hash}
|
||||
self.nosys_core.dispatcher.send_binary(data, file.connection_id, self.id, meta)
|
||||
|
||||
def on_file_part(self, event):
|
||||
meta = event.meta
|
||||
data = event.data
|
||||
file_id = (meta["file_hash"], event.connection.id)
|
||||
file:File = self.files[file_id]
|
||||
file.write_part(data, meta["part"], meta["part_hash"])
|
||||
|
||||
# TODO get_file_part, ack_file_part
|
||||
|
||||
BIN
fileTransfer/fileTransfer.zip
Normal file
BIN
fileTransfer/fileTransfer.zip
Normal file
Binary file not shown.
10
fileTransfer/info.json
Normal file
10
fileTransfer/info.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"id": "fileTransfer",
|
||||
"version": 0.007,
|
||||
"modules": [
|
||||
{
|
||||
"id": "fileTransfer",
|
||||
"version": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
2
fspn/.gitignore
vendored
Normal file
2
fspn/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
__pycache__
|
||||
fspn.zip
|
||||
1
fspn/.mtimes.json
Normal file
1
fspn/.mtimes.json
Normal file
@@ -0,0 +1 @@
|
||||
{".gitignore": 1741162475.4773676, "requirements.txt": 1740930861.0004706, "test.py": 1753436830.9918232, "protocol\\connection.py": 1757413256.9977374, "protocol\\security.py": 1757413451.9272285, "protocol\\server.py": 1757831912.1189482, "protocol\\__pycache__\\connection.cpython-311.pyc": 1753509446.7233407, "protocol\\__pycache__\\security.cpython-311.pyc": 1753436646.3835695, "protocol\\__pycache__\\server.cpython-311.pyc": 1739790368.10843, "utils\\aes_util.py": 1739790367.99551, "utils\\ecdh_util.py": 1739790367.9960146, "utils\\ecdsa_util.py": 1741151848.8236935, "utils\\observable.py": 1756021286.9012282, "utils\\sha256_util.py": 1752738996.6411796, "utils\\wrapper_util.py": 1739790368.001054, "utils\\__pycache__\\aes_util.cpython-311.pyc": 1739790368.1195576, "utils\\__pycache__\\aes_util.cpython-313.pyc": 1740069706.9769707, "utils\\__pycache__\\ecdh_util.cpython-311.pyc": 1739790368.1672213, "utils\\__pycache__\\ecdh_util.cpython-313.pyc": 1741381590.6166677, "utils\\__pycache__\\ecdsa_util.cpython-311.pyc": 1741380658.4493158, "utils\\__pycache__\\ecdsa_util.cpython-313.pyc": 1741381591.9633517, "utils\\__pycache__\\observable.cpython-311.pyc": 1753436646.3665586, "utils\\__pycache__\\sha256_util.cpython-311.pyc": 1752742523.9286432, "utils\\__pycache__\\sha256_util.cpython-313.pyc": 1744026927.3028462, "utils\\__pycache__\\wrappers.cpython-311.pyc": 1739790368.1048622, "utils\\__pycache__\\wrapper_util.cpython-311.pyc": 1740064619.98263, "utils\\__pycache__\\wrapper_util.cpython-313.pyc": 1741381591.983756}
|
||||
5
fspn/info.json
Normal file
5
fspn/info.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"id": "fspn",
|
||||
"version": 0.007,
|
||||
"modules": []
|
||||
}
|
||||
264
fspn/protocol/connection.py
Normal file
264
fspn/protocol/connection.py
Normal file
@@ -0,0 +1,264 @@
|
||||
from ..utils.observable import Observable
|
||||
from ..utils.wrapper_util import threaded
|
||||
from .security import Security
|
||||
|
||||
from enum import Enum
|
||||
import socket
|
||||
import ipaddress
|
||||
import struct
|
||||
import time, datetime
|
||||
import logging, traceback
|
||||
import json
|
||||
import uuid
|
||||
|
||||
# TODO Impossible: Hiding ip in a p2p connection hahahahahaha
|
||||
|
||||
HEADER_STRUCTURE = '!I?IIb' # size, encrypted, nonce_len, mac_len, is_binary
|
||||
HEADER_SIZE = struct.calcsize(HEADER_STRUCTURE) # 14 bytes
|
||||
|
||||
MAX_CONNECTION_TRIES = 3
|
||||
|
||||
class EVENTS(Enum):
|
||||
ON_CONNECTION = 0
|
||||
ON_CONNECTION_ERROR = 1
|
||||
ON_DISCONNECTION = 2
|
||||
ON_MESSAGE = 3
|
||||
|
||||
events = [EVENTS.ON_CONNECTION, EVENTS.ON_CONNECTION_ERROR, EVENTS.ON_DISCONNECTION, EVENTS.ON_MESSAGE]
|
||||
|
||||
class STATUS(Enum):
|
||||
DISCONNECTED = 0
|
||||
CONNECTED = 1
|
||||
CONNECTING = 2
|
||||
HANDSHAKING = 3
|
||||
ERROR = -1
|
||||
|
||||
class Connection(Observable):
|
||||
def __init__(self, user, pmc, conn=None):
|
||||
super().__init__(events)
|
||||
self.security = Security(user, pmc)
|
||||
self.status = STATUS.DISCONNECTED
|
||||
self.address = None
|
||||
self.hostname = None
|
||||
self.bind_address = None
|
||||
self.conn = conn
|
||||
if conn:
|
||||
self.set_addresses()
|
||||
self.id = str(uuid.uuid4())
|
||||
self.handshake_payload = None
|
||||
|
||||
def set_addresses(self, address=None):
|
||||
if address:
|
||||
self.address = address
|
||||
host, port = address
|
||||
try:
|
||||
ipaddress.ip_address(host)
|
||||
except ValueError:
|
||||
self.hostname = host
|
||||
else:
|
||||
self.address = self.conn.getpeername()
|
||||
self.bind_address = self.conn.getsockname()
|
||||
|
||||
|
||||
@threaded
|
||||
def connect(self, address, bind_address=('0.0.0.0', 0)):
|
||||
self.set_addresses(address)
|
||||
self.bind_address = bind_address
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
#s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
s.bind(bind_address)
|
||||
s.settimeout(10)
|
||||
self.conn = s
|
||||
except Exception:
|
||||
self.status = STATUS.ERROR
|
||||
self.fire_event(EVENTS.ON_CONNECTION_ERROR, error="Error to setup connection")
|
||||
raise
|
||||
|
||||
self.status = STATUS.CONNECTING
|
||||
logging.info(f'Socket trying to connect: {self.bind_address} -> {address}')
|
||||
|
||||
for i in range(MAX_CONNECTION_TRIES):
|
||||
try:
|
||||
s.settimeout(None)
|
||||
self.conn = s
|
||||
self.bind_address = s.getsockname()
|
||||
s.connect(address)
|
||||
self.address = s.getpeername()
|
||||
break
|
||||
except Exception as e:
|
||||
logging.exception("ERROR")
|
||||
if i < MAX_CONNECTION_TRIES - 1:
|
||||
continue
|
||||
else:
|
||||
self.status = STATUS.ERROR
|
||||
self.fire_event(EVENTS.ON_CONNECTION_ERROR, error=f"No connection could be made in {MAX_CONNECTION_TRIES} retries: {e}")
|
||||
raise
|
||||
|
||||
self.new_connection()
|
||||
|
||||
def new_connection(self):
|
||||
self.handshake_create_payload()
|
||||
|
||||
def handshake_create_payload(self):
|
||||
self.status = STATUS.HANDSHAKING
|
||||
logging.info(f'Socket handshaking: {self.bind_address} -> {self.address}')
|
||||
my_ecdsa_str = self.security.user
|
||||
my_proof_of_work = self.security.proof_of_work
|
||||
my_ecdh_pk = self.security.ecdh.public_key_to_str()
|
||||
date = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
self.handshake_payload = {'ecdsa':my_ecdsa_str, 'pof':my_proof_of_work, 'ecdh':my_ecdh_pk, 'date':date}
|
||||
payload = {'ecdsa':my_ecdsa_str, 'pof':my_proof_of_work, 'ecdh':my_ecdh_pk, 'date':date}
|
||||
request_id = self.security.sign_message_ecdsa(json.dumps(payload), self.payload_signature_callback, "Handshake Connection")
|
||||
self.wait_signature()
|
||||
|
||||
@threaded
|
||||
def wait_signature(self):
|
||||
# TODO Config this time as security time validation
|
||||
time.sleep(120)
|
||||
if self.status == STATUS.HANDSHAKING:
|
||||
logging.info(f"Closing connection, payload signature time exceded")
|
||||
self.status = STATUS.DISCONNECTED
|
||||
self.fire_event(EVENTS.ON_CONNECTION_ERROR, error="Payload signature time exceded")
|
||||
self.close_connection()
|
||||
|
||||
def payload_signature_callback(self, request_id, signature):
|
||||
if self.status != STATUS.HANDSHAKING:
|
||||
logging.info(f"Not using signature, connection is closed")
|
||||
return None
|
||||
if signature:
|
||||
self.handshake_payload['signature'] = signature
|
||||
self.send_message(json.dumps(self.handshake_payload), False)
|
||||
|
||||
while self.status == STATUS.HANDSHAKING:
|
||||
message = self.message_reader()["data"]
|
||||
logging.error
|
||||
if(message):
|
||||
payload = json.loads(message)
|
||||
payload_signed = payload.copy()
|
||||
payload_signed.pop('signature')
|
||||
self.security.handshake_validation(payload['ecdsa'], json.dumps(payload_signed), payload['signature'], payload['ecdh'], payload['pof'], payload['date'])
|
||||
|
||||
self.status = STATUS.CONNECTED
|
||||
logging.info(f'Ready: {self.bind_address} -> {self.address}')
|
||||
self.fire_event(EVENTS.ON_CONNECTION)
|
||||
self.wait_message()
|
||||
else:
|
||||
self.status = STATUS.DISCONNECTED
|
||||
self.fire_event(EVENTS.ON_CONNECTION_ERROR, error="Payload signature is None")
|
||||
self.close_connection()
|
||||
|
||||
@threaded
|
||||
def wait_message(self):
|
||||
while self.status == STATUS.CONNECTED:
|
||||
try:
|
||||
message = self.message_reader()
|
||||
except socket.timeout as to:
|
||||
logging.exception("ERROR")
|
||||
continue
|
||||
except (ConnectionAbortedError, EOFError, ConnectionResetError, OSError):
|
||||
# TODO Maybe try to reconnect
|
||||
self.status = STATUS.DISCONNECTED
|
||||
self.fire_event(EVENTS.ON_DISCONNECTION)
|
||||
break
|
||||
except Exception as e:
|
||||
logging.exception("ERROR")
|
||||
self.status = STATUS.DISCONNECTED
|
||||
self.fire_event(EVENTS.ON_DISCONNECTION)
|
||||
break
|
||||
|
||||
try:
|
||||
if(message):
|
||||
# logging.debug(message)
|
||||
self.fire_event(EVENTS.ON_MESSAGE, message=message)
|
||||
except Exception as e:
|
||||
logging.error(traceback.format_exc())
|
||||
|
||||
def close_connection(self):
|
||||
logging.info(f'Socket closed: {self.address}')
|
||||
self.conn.close()
|
||||
self.status = STATUS.DISCONNECTED
|
||||
self.fire_event(EVENTS.ON_DISCONNECTION)
|
||||
|
||||
@threaded
|
||||
def send_binary(self, data: bytes, meta: dict = None, encrypted=True):
|
||||
meta_json = json.dumps(meta or {})
|
||||
meta_encoded = meta_json.encode('utf-8')
|
||||
meta_len_bytes = struct.pack('!I', len(meta_encoded))
|
||||
full_payload = meta_len_bytes + meta_encoded + data
|
||||
|
||||
nonce = b''
|
||||
mac = b''
|
||||
|
||||
if encrypted:
|
||||
nonce, full_payload, mac = self.security.encrypt_message(full_payload)
|
||||
|
||||
message_header = struct.pack(
|
||||
HEADER_STRUCTURE,
|
||||
len(nonce) + len(mac) + len(full_payload),
|
||||
encrypted,
|
||||
len(nonce),
|
||||
len(mac),
|
||||
1 # is_binary = True
|
||||
)
|
||||
|
||||
self.conn.sendall(message_header + nonce + mac + full_payload)
|
||||
|
||||
@threaded
|
||||
def send_message(self, message: str, encrypted=True):
|
||||
nonce = b''
|
||||
mac = b''
|
||||
meta = b''
|
||||
encoded_msg = message.encode('utf-8')
|
||||
|
||||
if encrypted:
|
||||
nonce, encoded_msg, mac = self.security.encrypt_message(encoded_msg)
|
||||
|
||||
message_header = struct.pack(
|
||||
HEADER_STRUCTURE,
|
||||
len(nonce) + len(mac) + len(meta) + len(encoded_msg),
|
||||
encrypted,
|
||||
len(nonce),
|
||||
len(mac),
|
||||
0 # is_binary = False
|
||||
)
|
||||
|
||||
self.conn.sendall(message_header + nonce + mac + encoded_msg)
|
||||
|
||||
|
||||
def message_reader(self):
|
||||
message_header = self.recv_all(HEADER_SIZE)
|
||||
if not message_header:
|
||||
self.close_connection()
|
||||
return None
|
||||
|
||||
total_len, encrypted, nonce_len, mac_len, is_binary = struct.unpack(HEADER_STRUCTURE, message_header)
|
||||
data = self.recv_all(total_len)
|
||||
|
||||
if encrypted:
|
||||
nonce = data[:nonce_len]
|
||||
mac = data[nonce_len:nonce_len + mac_len]
|
||||
payload = data[nonce_len + mac_len:]
|
||||
decrypted = self.security.decrypt_message(nonce, payload, mac)
|
||||
else:
|
||||
decrypted = data
|
||||
|
||||
if is_binary:
|
||||
meta_length = struct.unpack('!I', decrypted[:4])[0]
|
||||
meta_raw = decrypted[4:4 + meta_length]
|
||||
meta = json.loads(meta_raw)
|
||||
file_data = decrypted[4 + meta_length:]
|
||||
return {"meta": meta, "data": file_data}
|
||||
else:
|
||||
return {"data": decrypted.decode('utf-8')}
|
||||
|
||||
|
||||
def recv_all(self, n: int) -> bytes:
|
||||
buffer = b''
|
||||
while len(buffer) < n:
|
||||
chunk = self.conn.recv(n - len(buffer))
|
||||
if not chunk:
|
||||
raise ConnectionError("Connection closed before receive all bytes")
|
||||
buffer += chunk
|
||||
return buffer
|
||||
112
fspn/protocol/security.py
Normal file
112
fspn/protocol/security.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from ..utils.observable import Observable
|
||||
from ..utils.wrapper_util import singleton
|
||||
from ..utils import sha256_util, aes_util, ecdh_util, ecdsa_util
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import importlib
|
||||
|
||||
# class EcdsaKey:
|
||||
# def __init__(self) -> None:
|
||||
# self.verifying:ecdsa_util.VerifyingKey = None
|
||||
# self.signing:ecdsa_util.SigningKey = None
|
||||
|
||||
# def create_key_from_string(self, password:str):
|
||||
# self.verifying, self.signing = ecdsa_util.create_keys(password.encode())
|
||||
|
||||
# def create_key_from_bytes(self, password:bytes):
|
||||
# self.verifying, self.signing = ecdsa_util.create_keys(password)
|
||||
|
||||
# def load_verifying(self, key:str):
|
||||
# self.verifying = ecdsa_util.load_verifying_key(base64.b64decode(key.encode()))
|
||||
|
||||
# def verifying_key_to_str(self):
|
||||
# return base64.b64encode(self.verifying.to_string('compressed')).decode()
|
||||
|
||||
class UserData:
|
||||
def __init__(self):
|
||||
self.proof_of_work = None
|
||||
|
||||
# Password Manager Client
|
||||
class Pmc:
|
||||
def raiseException(self):
|
||||
raise Exception("Missing Password Manager Client")
|
||||
|
||||
def get(self, user) -> UserData:
|
||||
self.raiseException()
|
||||
|
||||
# Returns a request_id. Callback receives str:request_id str:signature
|
||||
def sign(self, data, user, callback, info=None) -> str:
|
||||
self.raiseException()
|
||||
|
||||
class EcdhKey:
|
||||
def __init__(self):
|
||||
self.public, self.private = ecdh_util.generate_keys()
|
||||
self.derived_key = None
|
||||
|
||||
def generate_derived_key(self, peer_key:str):
|
||||
ecdh_pk = ecdh_util.load_public_key_str(peer_key, True)
|
||||
shared_key = ecdh_util.generate_shared_key(self.private, ecdh_pk)
|
||||
self.derived_key = ecdh_util.generate_derived_key(shared_key)
|
||||
|
||||
def update_derived_key(self):
|
||||
self.derived_key = ecdh_util.generate_derived_key(self.derived_key)
|
||||
|
||||
def public_key_to_str(self):
|
||||
return ecdh_util.public_key_to_str(self.public, True)
|
||||
|
||||
class Security():
|
||||
def __init__(self, user, pmc:Pmc):
|
||||
self.pmc = pmc
|
||||
self.user = user
|
||||
self.proof_of_work = self.pmc.get(user).proof_of_work
|
||||
|
||||
self.peer_user = None
|
||||
self.peer_ecdsa = None
|
||||
self.ecdh = EcdhKey()
|
||||
|
||||
self.peer_pof_level = None
|
||||
self.min_proof_of_work_level = 4
|
||||
|
||||
def encrypt_message(self, message:bytes):
|
||||
return aes_util.encrypt(message, self.ecdh.derived_key)
|
||||
|
||||
def decrypt_message(self, nonce:bytes, message:bytes, mac:bytes):
|
||||
return aes_util.decrypt_and_verify(nonce, message, mac, self.ecdh.derived_key)
|
||||
|
||||
def sign_message_ecdsa(self, message:str, callback, info=None):
|
||||
hash_message = sha256_util.hash_string(message)
|
||||
return self.pmc.sign(hash_message, self.user, callback, info)
|
||||
# return base64.b64encode(ecdsa_util.sign_message(message, self.my_ecdsa.signing)).decode()
|
||||
|
||||
def check_signature_ecdsa(self, message:str, signature:str):
|
||||
hash_message = sha256_util.hash_string(message)
|
||||
return ecdsa_util.verify_message(hash_message.encode(), base64.b64decode(signature.encode()), self.peer_ecdsa)
|
||||
|
||||
def check_signature_ecdsa_vk(self, verifying_key:str, message:str, signature:str):
|
||||
hash_message = sha256_util.hash_string(message)
|
||||
return ecdsa_util.verify_message(hash_message.encode(), base64.b64decode(signature.encode()), ecdsa_util.load_verifying_key(base64.b64decode(verifying_key.encode())))
|
||||
|
||||
# def encrypt_message_ecdsa(self, message:str):
|
||||
# encrypted = ecdsa_util.encrypt_message(base64.b64decode(self.peer_ecdsa.verifying_key_to_str().encode()), message.encode())
|
||||
# return base64.b64encode(encrypted).decode()
|
||||
|
||||
# def decrypt_message_ecdsa(self, message:str):
|
||||
# return ecdsa_util.decrypt_message(self.my_ecdsa.signing.to_string(), base64.b64decode(message.encode()))
|
||||
|
||||
|
||||
def verify_proof_of_work(self, ecdsa:str, proof_of_work:str):
|
||||
hash = sha256_util.hash_string(f'{ecdsa}{proof_of_work}')
|
||||
logging.debug(f'ECDSA {ecdsa}, POF {proof_of_work}, HASH {hash}')
|
||||
if hash[0:self.min_proof_of_work_level] != "0"*self.min_proof_of_work_level:
|
||||
raise Exception(f'Proof of work below minimum level {self.min_proof_of_work_level}')
|
||||
|
||||
# TODO validate date/time
|
||||
def handshake_validation(self, peer_ecdsa, payload, payload_signature, ecdh, proof_of_work, date):
|
||||
self.peer_user = peer_ecdsa
|
||||
self.peer_ecdsa = ecdsa_util.load_verifying_key(base64.b64decode(peer_ecdsa.encode()))
|
||||
self.check_signature_ecdsa(payload, payload_signature)
|
||||
|
||||
self.verify_proof_of_work(peer_ecdsa, proof_of_work)
|
||||
self.ecdh.generate_derived_key(ecdh)
|
||||
|
||||
74
fspn/protocol/server.py
Normal file
74
fspn/protocol/server.py
Normal file
@@ -0,0 +1,74 @@
|
||||
|
||||
from ..utils.observable import Observable, Event as ObservableEvent
|
||||
from ..utils.wrapper_util import threaded
|
||||
from .connection import Connection, EVENTS as CONNECTION_EVENTS
|
||||
|
||||
from enum import Enum
|
||||
import logging, traceback
|
||||
import socket
|
||||
import random
|
||||
|
||||
class EVENTS(Enum):
|
||||
ON_START = 0
|
||||
ON_START_ERROR = 1
|
||||
ON_CONNECTION = 2
|
||||
ON_CONNECTION_ERROR = 3
|
||||
ON_DISCONNECTION = 4
|
||||
ON_MESSAGE = 5
|
||||
|
||||
class Server(Observable):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.connections:dict[tuple[str,int],Connection] = {}
|
||||
self.bind_address = None
|
||||
self.running = False
|
||||
self.user = None
|
||||
|
||||
@threaded
|
||||
def run(self, user, pmc, bind_address = ('127.0.0.1', random.randint(5000, 5999))):
|
||||
try:
|
||||
self.user = user
|
||||
if not bind_address:
|
||||
self.bind_address = ('127.0.0.1', random.randint(5000, 5999))
|
||||
else:
|
||||
self.bind_address = bind_address
|
||||
logging.info(f"Starting server on address {self.bind_address}")
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
s.bind(self.bind_address)
|
||||
s.settimeout(10)
|
||||
s.listen(5)
|
||||
self.running = True
|
||||
self.fire_event(EVENTS.ON_START)
|
||||
logging.info(f"Listening on {self.bind_address}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
conn, addr = s.accept()
|
||||
logging.info(f"Incoming connection: {addr}")
|
||||
connection = Connection(user, pmc, conn)
|
||||
connection.subscribe_event(CONNECTION_EVENTS.ON_CONNECTION, self.on_server_connection)
|
||||
connection.subscribe_event(CONNECTION_EVENTS.ON_MESSAGE, self.on_server_message)
|
||||
connection.subscribe_event(CONNECTION_EVENTS.ON_DISCONNECTION, self.on_server_disconnection)
|
||||
self.connections[addr] = connection
|
||||
self.connections[addr].new_connection()
|
||||
except socket.timeout:
|
||||
continue
|
||||
except Exception:
|
||||
logging.error("ERROR")
|
||||
conn.close()
|
||||
|
||||
except Exception as e:
|
||||
logging.error("ERROR")
|
||||
self.fire_event(EVENTS.ON_START_ERROR, error=e)
|
||||
|
||||
|
||||
def on_server_connection(self, event:ObservableEvent):
|
||||
pass
|
||||
|
||||
def on_server_message(self, event):
|
||||
pass
|
||||
|
||||
def on_server_disconnection(self, event):
|
||||
pass
|
||||
3
fspn/requirements.txt
Normal file
3
fspn/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
pycryptodome
|
||||
cryptography
|
||||
ecdsa
|
||||
78
fspn/test.py
Normal file
78
fspn/test.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import os, sys
|
||||
root_dir = os.path.normpath(__file__.split("libs")[0])
|
||||
sys.path.append(root_dir)
|
||||
|
||||
from libs.fspn.protocol.connection import Connection, EVENTS
|
||||
from libs.fspn.utils import sha256_util, aes_util, ecdh_util, ecdsa_util
|
||||
from libs.fspn.utils.wrapper_util import singleton, threaded
|
||||
import base64, time
|
||||
|
||||
class UserData:
|
||||
def __init__(self):
|
||||
self.proof_of_work = None
|
||||
|
||||
class Pmc:
|
||||
def __init__(self):
|
||||
self.verifying_key, self.signing_key = ecdsa_util.create_keys(base64.b64decode("I0x1Y2FzR2FicmllbFZhekRvc1NhbnRvc0luYWNpbyE="))
|
||||
|
||||
def get(self, user) -> UserData:
|
||||
user_data = UserData()
|
||||
user_data.proof_of_work = "eYnU*@"
|
||||
return user_data
|
||||
|
||||
# Returns a request_id. Callback receives str:request_id str:signature
|
||||
def sign(self, data, user, callback) -> str:
|
||||
self.send_callback("test", callback, data)
|
||||
return "test"
|
||||
|
||||
def send_callback(self, request_id, callback, data):
|
||||
signature = ecdsa_util.sign_message(data, self.signing_key)
|
||||
signature = base64.b64encode(signature).decode()
|
||||
callback(request_id, signature)
|
||||
|
||||
|
||||
class Test:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def test(self):
|
||||
pmc = Pmc()
|
||||
|
||||
c1 = Connection("A4DZSk+TlR+4w39MbiIAQbti+N0H1QlJEhRH2DI6Iubj", pmc)
|
||||
c2 = Connection("A4DZSk+TlR+4w39MbiIAQbti+N0H1QlJEhRH2DI6Iubj", pmc)
|
||||
|
||||
p1= 5676
|
||||
p2 = 5686
|
||||
c1.connect(("127.0.0.1", p1), ("127.0.0.1", p2))
|
||||
c2.connect(("127.0.0.1", p2), ("127.0.0.1", p1))
|
||||
|
||||
c1.subscribe_event(EVENTS.ON_MESSAGE, self.on_message_1)
|
||||
c2.subscribe_event(EVENTS.ON_MESSAGE, self.on_message_2)
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
c1.send_message("Testing", True)
|
||||
c1.send_binary(data=b"Test", meta={"test":"test"}, encrypted=True)
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
c1.close_connection()
|
||||
c2.close_connection()
|
||||
|
||||
def on_message_1(self, event):
|
||||
print("C1 got message: ",event.__dict__)
|
||||
|
||||
def on_message_2(self, event):
|
||||
print("C2 got message: ",event.__dict__)
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format='[%(levelname)s] %(asctime)s - %(message)s',
|
||||
stream=sys.stdout
|
||||
)
|
||||
|
||||
t = Test()
|
||||
t.test()
|
||||
17
fspn/utils/aes_util.py
Normal file
17
fspn/utils/aes_util.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
def encrypt(data:bytes, key:bytes):
|
||||
cipher = AES.new(key, AES.MODE_EAX)
|
||||
nonce = cipher.nonce
|
||||
ciphertext, mac = cipher.encrypt_and_digest(data)
|
||||
return nonce, ciphertext, mac
|
||||
|
||||
def decrypt_and_verify(nonce:bytes, data:bytes, mac:bytes, key:bytes):
|
||||
cipher = AES.new(key, AES.MODE_EAX, nonce=nonce)
|
||||
plaintext = cipher.decrypt(data)
|
||||
try:
|
||||
cipher.verify(mac)
|
||||
except ValueError:
|
||||
return None
|
||||
return plaintext
|
||||
|
||||
36
fspn/utils/ecdh_util.py
Normal file
36
fspn/utils/ecdh_util.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||
from cryptography.hazmat.primitives.serialization import PublicFormat, Encoding, load_pem_public_key
|
||||
|
||||
def generate_keys():
|
||||
private_key = ec.generate_private_key(
|
||||
ec.SECP384R1()
|
||||
)
|
||||
public_key = private_key.public_key()
|
||||
return public_key, private_key
|
||||
|
||||
def generate_shared_key(private_key:ec.EllipticCurvePrivateKey, public_key:ec.EllipticCurvePublicKey):
|
||||
shared_key = private_key.exchange(ec.ECDH(), public_key)
|
||||
return shared_key
|
||||
|
||||
def generate_derived_key(shared_key:bytes):
|
||||
derived_key = HKDF(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=None,
|
||||
info=None,
|
||||
).derive(shared_key)
|
||||
return derived_key
|
||||
|
||||
def public_key_to_str(public_key:ec.EllipticCurvePublicKey, remove_header_and_footer=False):
|
||||
public_key_str = public_key.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo).decode()
|
||||
if(remove_header_and_footer):
|
||||
public_key_str = public_key_str.replace('-----BEGIN PUBLIC KEY-----\n','')
|
||||
public_key_str = public_key_str.replace('\n-----END PUBLIC KEY-----\n','')
|
||||
return public_key_str
|
||||
|
||||
def load_public_key_str(public_key_str:str, removed_header_and_footer=False):
|
||||
if(removed_header_and_footer):
|
||||
public_key_str = f'-----BEGIN PUBLIC KEY-----\n{public_key_str}\n-----END PUBLIC KEY-----\n'
|
||||
return load_pem_public_key(public_key_str.encode())
|
||||
62
fspn/utils/ecdsa_util.py
Normal file
62
fspn/utils/ecdsa_util.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from ecdsa import SigningKey, VerifyingKey, SECP256k1, keys
|
||||
from hashlib import sha256
|
||||
# from ecies import encrypt, decrypt
|
||||
|
||||
def create_keys(password:bytes) -> tuple[VerifyingKey, SigningKey]: # type: ignore
|
||||
privateKey = SigningKey.from_string(password, curve=SECP256k1)
|
||||
publicKey:VerifyingKey = privateKey.get_verifying_key()
|
||||
return (publicKey, privateKey)
|
||||
|
||||
def create_pem(fullpath, privateKey:SigningKey, publicKey:VerifyingKey):
|
||||
with open(fullpath+"privateKey.pem", "wb") as f:
|
||||
f.write(privateKey.to_pem(format="pkcs8"))
|
||||
with open(fullpath+"publicKey.pem", "wb") as f:
|
||||
f.write(publicKey.to_pem())
|
||||
|
||||
def read_pem(fullpath):
|
||||
with open(fullpath+"privateKey.pem") as f:
|
||||
privateKey = SigningKey.from_pem(f.read())
|
||||
with open(fullpath+"publicKey.pem") as f:
|
||||
publicKey = VerifyingKey.from_pem(f.read())
|
||||
return (privateKey, publicKey)
|
||||
|
||||
def sign_message(message:str, privateKey:SigningKey) -> bytes:
|
||||
return privateKey.sign(message.encode('utf-8'), hashfunc=sha256)
|
||||
|
||||
def load_signing_key(signing_key:bytes) -> SigningKey:
|
||||
return SigningKey.from_string(signing_key, curve=SECP256k1, hashfunc=sha256)
|
||||
|
||||
def get_verifying_key(signing_key:SigningKey) -> VerifyingKey:
|
||||
return signing_key.get_verifying_key()
|
||||
|
||||
def load_verifying_key(verifying_key:bytes) -> VerifyingKey:
|
||||
return VerifyingKey.from_string(verifying_key, curve=SECP256k1, hashfunc=sha256)
|
||||
|
||||
def verify_message(message:bytes, signature:bytes, publicKey:VerifyingKey) -> bool:
|
||||
try:
|
||||
return publicKey.verify(signature, message, hashfunc=sha256)
|
||||
except keys.BadSignatureError:
|
||||
return False
|
||||
|
||||
# ecdsa_vk.to_string('compressed') or vk_bytes
|
||||
# def encrypt_message(verifying_key:bytes, message:bytes):
|
||||
# return encrypt(verifying_key, message)
|
||||
|
||||
# ecdsa_sk.to_string() or sk_bytes
|
||||
# def decrypt_message(signing_key:bytes, message:bytes):
|
||||
# return decrypt(signing_key, message)
|
||||
|
||||
# password = b'#LucasGabrielVazDosSantosInacio!'
|
||||
|
||||
# vk,sk = create_keys(password)
|
||||
# import base64
|
||||
|
||||
# print(base64.b64encode(vk.to_string('compressed')).decode())
|
||||
|
||||
# pk ='AuWgGLOi4VUxYQnZzcXqtzl1nA4H4MAL+fzLgjf+TX8C'
|
||||
# message= '{"text":"Hello","public_key":"AuWgGLOi4VUxYQnZzcXqtzl1nA4H4MAL+fzLgjf+TX8C","pof":"69201","signature":null,"files":[],"networks":["000"],"datetime":"2024-10-06T07:08:30.419000Z","parents":[]}'
|
||||
# sig = 'dRAyr67oZXblKEqS9EpghhS7mbl1DOoqCF8n8krQ2KsTcV9VRK6Hc4O2A27WkdjW2ZEaalp2PbPd1ZamAMJJ/A=='
|
||||
|
||||
# pk = load_verifying_key(base64.b64decode(pk))
|
||||
# sig = base64.b64decode(sig)
|
||||
# print(verify_message(message.encode(), sig, pk))
|
||||
44
fspn/utils/observable.py
Normal file
44
fspn/utils/observable.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from .wrapper_util import threaded
|
||||
import logging, traceback
|
||||
|
||||
class Event(object):
|
||||
def __init__(self):
|
||||
self.source = self
|
||||
|
||||
class Observable(object):
|
||||
"""
|
||||
A simple publish-subscribe system.
|
||||
"""
|
||||
|
||||
def __init__(self, events = []):
|
||||
self.events: dict[str, list] = {}
|
||||
for event in events:
|
||||
self.register_event(event)
|
||||
|
||||
def register_event(self, event):
|
||||
if(event not in self.events):
|
||||
self.events[event] = []
|
||||
|
||||
def subscribe_event(self, event, callback):
|
||||
if(event not in self.events):
|
||||
self.register_event(event)
|
||||
self.events[event].append(callback)
|
||||
|
||||
@threaded
|
||||
def fire_event(self, event, **kwargs):
|
||||
e = Event()
|
||||
e.source = self
|
||||
for k, v in kwargs.items():
|
||||
setattr(e, k, v)
|
||||
if(event in self.events):
|
||||
for fn in self.events[event]:
|
||||
self.call_observer(fn, e)
|
||||
else:
|
||||
logging.warning(f"Event {event} without callback")
|
||||
|
||||
@threaded
|
||||
def call_observer(self, function, event):
|
||||
try:
|
||||
function(event)
|
||||
except Exception as ex:
|
||||
logging.exception(f'Error in event {event} to function {function.__name__}')
|
||||
67
fspn/utils/sha256_util.py
Normal file
67
fspn/utils/sha256_util.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import hashlib
|
||||
|
||||
def hash_file(filepath):
|
||||
BUF_SIZE = 65 * 1024
|
||||
|
||||
sha = hashlib.sha256()
|
||||
|
||||
with open(filepath, 'rb') as f:
|
||||
while True:
|
||||
data = f.read(BUF_SIZE)
|
||||
if not data:
|
||||
break
|
||||
sha.update(data)
|
||||
return sha.hexdigest()
|
||||
|
||||
def hash_bytes(data):
|
||||
sha = hashlib.sha256()
|
||||
sha.update(data)
|
||||
return sha.hexdigest()
|
||||
|
||||
def hash_string(data:str):
|
||||
sha = hashlib.sha256()
|
||||
sha.update(str(data).encode('utf-8'))
|
||||
return sha.hexdigest()
|
||||
|
||||
# ---------------
|
||||
import datetime
|
||||
import string
|
||||
import itertools
|
||||
import random
|
||||
|
||||
def get_nonce(characters, lenght):
|
||||
yield from itertools.product(*([characters] * lenght))
|
||||
|
||||
def count_leading_zeros(text):
|
||||
n = 0
|
||||
for i in range(len(text)):
|
||||
if text[:i] == "0" * i:
|
||||
n = i
|
||||
else:
|
||||
break
|
||||
return n
|
||||
|
||||
def mine_user(data, force=4, nonce_lenght=4):
|
||||
characters = '[@_!#$%^&*()<>?/\|}{~:]'+string.ascii_letters+string.digits
|
||||
while True:
|
||||
for x in get_nonce(characters, nonce_lenght):
|
||||
nonce = ''.join(x)+random.choice(characters)
|
||||
hash = hash_string(data+ nonce)
|
||||
leading_zeros = count_leading_zeros(hash)
|
||||
if leading_zeros >= force:
|
||||
return nonce
|
||||
|
||||
|
||||
def test():
|
||||
print(datetime.datetime.now(), "EXECUTING HASH TEST - MINE USER")
|
||||
public_key_str = "A4DZSk+TlR+4w39MbiIAQbti+N0H1QlJEhRH2DI6Iubj"
|
||||
nonce, hash = mine_user(public_key_str)
|
||||
|
||||
print(nonce, hash)
|
||||
|
||||
# test()
|
||||
|
||||
# A4DZSk+TlR+4w39MbiIAQbti+N0H1QlJEhRH2DI6Iubj
|
||||
# 8
|
||||
# eYnU*@
|
||||
# [/Q#7r
|
||||
19
fspn/utils/wrapper_util.py
Normal file
19
fspn/utils/wrapper_util.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import functools
|
||||
from threading import Thread
|
||||
|
||||
def threaded(fn):
|
||||
"""Decorator to automatically launch a function in a thread"""
|
||||
@functools.wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
thread = Thread(target=fn, args=args, kwargs=kwargs)
|
||||
thread.start()
|
||||
return thread
|
||||
return wrapper
|
||||
|
||||
def singleton(cls):
|
||||
instances = {}
|
||||
def getinstance(*args, **kwargs):
|
||||
if cls not in instances:
|
||||
instances[cls] = cls(*args, **kwargs)
|
||||
return instances[cls]
|
||||
return getinstance
|
||||
2
lockbox/.gitignore
vendored
Normal file
2
lockbox/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
__pycache__
|
||||
lockbox.zip
|
||||
1
lockbox/.mtimes.json
Normal file
1
lockbox/.mtimes.json
Normal file
@@ -0,0 +1 @@
|
||||
{".gitignore": 1741162489.7420642, "api.http": 1750139941.0187607, "config.json": 1756106460.8736105, "lockboxClient.py": 1757415645.911955, "lockboxService.py": 1757490968.0438733, "lockboxServiceApi.py": 1757415119.6281, "miner.py": 1757325231.7886086, "minerApiBlueprint.py": 1757323989.6872394, "minerSocketio.py": 1757322038.733255, "requirements.txt": 1756036846.2401123, "userHistory.json": 1756014413.6882465, "utils.py": 1756014180.2178173, "browserExtension\\hello.html": 1740309144.9421954, "browserExtension\\hello_extensions.png": 1740305089.7709863, "browserExtension\\main.js": 1740309132.1250446, "browserExtension\\manifest.json": 1740308869.3939943, "browserExtension\\socketio.js": 1740305420.5022602, "frontend\\.gitignore": 1753127603.0183902, "frontend\\ca.pem": 1757146987.2444186, "frontend\\ca_key.pem": 1757146987.2449198, "frontend\\cert.pem": 1757146987.2669685, "frontend\\index.html": 1753127949.1451695, "frontend\\jsconfig.json": 1753127603.072253, "frontend\\key.pem": 1757146987.2669685, "frontend\\package-lock.json": 1753577901.326157, "frontend\\package.json": 1753264154.9925382, "frontend\\README.md": 1753127749.0499122, "frontend\\vite.config.js": 1753133376.4389508, "frontend\\webView.py": 1757273055.978937, "frontend\\.vscode\\extensions.json": 1753127603.0647097, "frontend\\.vscode\\settings.json": 1753127603.0858867, "frontend\\dist\\favicon.ico": 1753127603.0304024, "frontend\\dist\\index.html": 1757196785.0428426, "frontend\\dist\\assets\\index-BZcXFX0l.css": 1757196785.0428426, "frontend\\dist\\assets\\index-DLC-DzCB.js": 1757196785.043342, "frontend\\public\\favicon.ico": 1753127603.0304024, "frontend\\src\\App.vue": 1753578911.9599397, "frontend\\src\\main.js": 1753571177.8232467, "frontend\\src\\assets\\logo.svg": 1753127603.100169, "frontend\\src\\assets\\main.css": 1753134037.7614193, "frontend\\src\\components\\buttons\\Button.vue": 1754224356.4003878, "frontend\\src\\components\\buttons\\ToogleSwitch.vue": 1754224437.5005052, "frontend\\src\\components\\cards\\Card.vue": 1754222510.193816, "frontend\\src\\components\\cards\\CardContent.vue": 1753192461.7694707, "frontend\\src\\components\\cards\\CardDescription.vue": 1753138683.593848, "frontend\\src\\components\\cards\\CardFooter.vue": 1753138707.6460035, "frontend\\src\\components\\cards\\CardHeader.vue": 1753190956.6568618, "frontend\\src\\components\\cards\\CardTitle.vue": 1753147372.2295625, "frontend\\src\\components\\inputs\\InputText.vue": 1754224553.033484, "frontend\\src\\components\\labels\\Label.vue": 1753141147.9629083, "frontend\\src\\components\\tabs\\TabCreateUser.vue": 1754224498.9855795, "frontend\\src\\components\\tabs\\TabProofOfWork.vue": 1754224221.950291, "frontend\\src\\components\\tabs\\TabSigningRequests.vue": 1754224256.0454957, "frontend\\src\\components\\tabs\\TabUsers.vue": 1754224245.3672867, "frontend\\src\\plugins\\socketio.js": 1756116610.3355243, "frontend\\src\\stores\\auth.js": 1753260869.1848066, "frontend\\src\\views\\Home.vue": 1754224669.0649729, "vue\\dependencies.txt": 1757200759.1109006, "vue\\router.js": 1757197583.1419137, "vue\\api\\lockboxClientApi.js": 1757244004.148773, "vue\\api\\lockboxServiceApi.js": 1757198065.150491, "vue\\api\\socketEvents.js": 1757198163.0826175, "vue\\components\\tabs\\TabCreateUser.vue": 1757197963.0401194, "vue\\components\\tabs\\TabProofOfWork.vue": 1757496275.300655, "vue\\components\\tabs\\TabSigningRequests.vue": 1757829169.3566494, "vue\\components\\tabs\\TabUsers.vue": 1757496293.6970932, "vue\\stores\\auth.js": 1757202108.6815858, "vue\\views\\HomeView.vue": 1757243879.6857555, "__pycache__\\lockboxService.cpython-311.pyc": 1756014397.4503593, "__pycache__\\lockboxServiceApi.cpython-311.pyc": 1756014397.199929, "__pycache__\\miner.cpython-311.pyc": 1753400645.8070076, "__pycache__\\minerBlueprint.cpython-311.pyc": 1753578561.8273892, "__pycache__\\utils.cpython-311.pyc": 1756014259.7933002}
|
||||
24
lockbox/api.http
Normal file
24
lockbox/api.http
Normal file
@@ -0,0 +1,24 @@
|
||||
@hostname = http://127.0.0.1
|
||||
@port = 5001
|
||||
@hostServer = {{hostname}}:{{port}}
|
||||
|
||||
###
|
||||
POST {{hostServer}}/users HTTP/1.1
|
||||
content-type: application/json
|
||||
|
||||
{"password":"MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MTI=", "data":{"proof_of_work":"eYnU*@"}}
|
||||
###{"password":"I0x1Y2FzR2FicmllbFZhekRvc1NhbnRvc0luYWNpbyE=", "data":{"proof_of_work":"eYnU*@"}}
|
||||
### A4DZSk+TlR+4w39MbiIAQbti+N0H1QlJEhRH2DI6Iubj
|
||||
|
||||
|
||||
###
|
||||
GET {{hostServer}}/users
|
||||
|
||||
###
|
||||
GET {{hostServer}}/users/A14%2FRek5z78U1rYC%2BWLvU%2FifnsX43o0tjnexmYdlXsjY
|
||||
|
||||
###
|
||||
POST {{hostServer}}/users/A14%2FRek5z78U1rYC%2BWLvU%2FifnsX43o0tjnexmYdlXsjY/sign HTTP/1.1
|
||||
content-type: application/json
|
||||
|
||||
{"data":"Test"}
|
||||
16
lockbox/browserExtension/hello.html
Normal file
16
lockbox/browserExtension/hello.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Flask WebSocket Example</title>
|
||||
</head>
|
||||
|
||||
<body style="background: #000;">
|
||||
<input type="text" id="message">
|
||||
<button id="btn">Send</button>
|
||||
<span id="result" style="color: aliceblue;"></span>
|
||||
</body>
|
||||
|
||||
<script src="socketio.js"></script>
|
||||
<script src="main.js"></script>
|
||||
|
||||
</html>
|
||||
BIN
lockbox/browserExtension/hello_extensions.png
Normal file
BIN
lockbox/browserExtension/hello_extensions.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 826 B |
19
lockbox/browserExtension/main.js
Normal file
19
lockbox/browserExtension/main.js
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
var socket = io("http://127.0.0.1:5000");
|
||||
socket.on('connect', function() {
|
||||
console.log('Connected to server');
|
||||
});
|
||||
socket.on('message', function(msg) {
|
||||
console.log('Message received: ' + msg);
|
||||
document.getElementById("result").innerHTML = msg
|
||||
});
|
||||
socket.on('disconnect', function() {
|
||||
console.log('Disconnected from server');
|
||||
});
|
||||
function sendMessage() {
|
||||
var msg = document.getElementById('message').value;
|
||||
socket.send(msg);
|
||||
socket.emit('ola',msg);
|
||||
}
|
||||
|
||||
document.getElementById("btn").addEventListener("click", sendMessage);
|
||||
10
lockbox/browserExtension/manifest.json
Normal file
10
lockbox/browserExtension/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "Hello Extensions",
|
||||
"description": "Base Level Extension",
|
||||
"version": "1.0",
|
||||
"manifest_version": 3,
|
||||
"action": {
|
||||
"default_popup": "hello.html",
|
||||
"default_icon": "hello_extensions.png"
|
||||
}
|
||||
}
|
||||
6042
lockbox/browserExtension/socketio.js
Normal file
6042
lockbox/browserExtension/socketio.js
Normal file
File diff suppressed because it is too large
Load Diff
6
lockbox/config.json
Normal file
6
lockbox/config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"server": {
|
||||
"host": "https://localhost",
|
||||
"port": "5001"
|
||||
}
|
||||
}
|
||||
30
lockbox/frontend/.gitignore
vendored
Normal file
30
lockbox/frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
3
lockbox/frontend/.vscode/extensions.json
vendored
Normal file
3
lockbox/frontend/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
29
lockbox/frontend/README.md
Normal file
29
lockbox/frontend/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# vue
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
19
lockbox/frontend/ca.pem
Normal file
19
lockbox/frontend/ca.pem
Normal file
@@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDEzCCAfugAwIBAgIULnlTx9ReaTKzpVwe2WsLQUVqDiIwDQYJKoZIhvcNAQEL
|
||||
BQAwOTELMAkGA1UEBhMCVVMxETAPBgNVBAoMCE5vU3lzLUNBMRcwFQYDVQQDDA5O
|
||||
b1N5cyBMb2NhbCBDQTAeFw0yNTA5MDYwODIzMDdaFw0zNTA5MDQwODIzMDdaMDkx
|
||||
CzAJBgNVBAYTAlVTMREwDwYDVQQKDAhOb1N5cy1DQTEXMBUGA1UEAwwOTm9TeXMg
|
||||
TG9jYWwgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9RQvCGPLZ
|
||||
z8V4qhM1IsUmSP3/05jEd9oBN5AmXIdiFHjlzBGVwgaqvhX7dWMAlaodN5s3moCu
|
||||
His2NyhCikXv/MepkguWChsljmfqiVEUUN/ZVYaUzYwYMQ7KY1Pz1+qaB6dsOzsi
|
||||
RFWv0AOZ5er3AIenpRF/3Y5yL3yqwzNcgEr5ULppCvIJdtyt9Da43p0zFnphGQL0
|
||||
IIAUnp4mEyBypghMWsNn2RtnD7gxGFrcHWgilC+EG44MUGnDZ2jAdxOc/x8Mac/n
|
||||
/VGCAYEGWIK/3lsAK1vdZMOQAXNpZC8zsPJFwNUXWQoJmZoqb9xyYmHU9VH/oc24
|
||||
wt5Q6S1/MYJlAgMBAAGjEzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
|
||||
BQADggEBAKEm4Jv1KGkZJHl/nnalpbxoc6Rj2KsaWvsGgW4NyGk+rBKqfy9uhd9e
|
||||
5DHoJ/KpLqQBL56+rU4oWu54/0CD0943oWoy6qroAphkJ3yhuRndKJgWvZHAyoIp
|
||||
+ESzHkiYaf7UXMqO6zdC6CP1fJL6ap4eRokEhp4+y+g63KJowuZl6HKqNh6wQaW9
|
||||
MqflZOP0rLBqDvbOPpSWUDZ77UKE4ZIRxarIas3KYyQtSBGBQjNXTysa7aCljGGv
|
||||
yNhl3gQ7HZzchigPUtsdLgQRDI/wDcrwPPhlHfyy76M8gcP5Nsodn++rh1O+L+Ff
|
||||
FCc+hSyCF6bajrkPWq/8WDCzC/XQs8k=
|
||||
-----END CERTIFICATE-----
|
||||
27
lockbox/frontend/ca_key.pem
Normal file
27
lockbox/frontend/ca_key.pem
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEAvUULwhjy2c/FeKoTNSLFJkj9/9OYxHfaATeQJlyHYhR45cwR
|
||||
lcIGqr4V+3VjAJWqHTebN5qArh4rNjcoQopF7/zHqZILlgobJY5n6olRFFDf2VWG
|
||||
lM2MGDEOymNT89fqmgenbDs7IkRVr9ADmeXq9wCHp6URf92Oci98qsMzXIBK+VC6
|
||||
aQryCXbcrfQ2uN6dMxZ6YRkC9CCAFJ6eJhMgcqYITFrDZ9kbZw+4MRha3B1oIpQv
|
||||
hBuODFBpw2dowHcTnP8fDGnP5/1RggGBBliCv95bACtb3WTDkAFzaWQvM7DyRcDV
|
||||
F1kKCZmaKm/ccmJh1PVR/6HNuMLeUOktfzGCZQIDAQABAoIBAAZ2GB5IHcGoujt0
|
||||
Y4dSUANJu7PvjO0FBwlgD63PlIkjBGVCEQsAdMMxvyKFVDmD4nmryoe6G7NCTrVX
|
||||
VmWq0k60DwBFplGbVVSHvQYgoEHjhiR+F1ex4ySybs/UQys1W7ZmlUKus+OO5OH2
|
||||
yDPEZzYRdZAYGr+tk02PY3OZO25U2HbS7xZWz68xWQJPuEJLm4kckWX+ylv0vlEM
|
||||
H7XcdNlAizGBp8wazQDL7kdfhnxZKUDTyaR4iWqC4qDT5bObPU4t7xiE43W8DH6C
|
||||
DI22zxriSeAkjzRbFAv67ws+nEzDvm7Oshz0kD9WqK0U0nkcibrCLXckZHni4ewQ
|
||||
HKzCAq0CgYEA4Tcpve20GhyCngEnk+MpUs7FP686+Ytrr7P65yRXEO8H1LPInD6J
|
||||
w/KRP5cRtCgV/Q+pp6yTr5YQlSSR7evQy04pquEJ6423n4zQTjROpmGxIVNzeAv+
|
||||
JwWpInLbEe5iafds+EXlAEo1buB5J47/LLP7LLcTOTtmLzjZzIPczgMCgYEA1yQN
|
||||
k6yPx5AHqjXSSH3UK0QC/lIMDBHbKtk+eB6lobN/skY9Ggw41Ph5wOZlJU4VQ9q9
|
||||
CsPtHr6izuxezmzbV0ae6a7dxM5fCSVs6V7IGNGIKF1xk6J0dqRbQxjv9Cid4mZM
|
||||
ORDNMdDWnfiB+/hjJ8KbLxZVJeYG7rXzkZvilXcCgYAUTXjB2m/l+rP7snby6gOL
|
||||
p4A4oX9bh6oJiNwRgkEnEaVPE3X+P9UDiRZ2+RNrfkGdMpBEwVX++jQ8fbN6E0wb
|
||||
R8yRzv+p8HihNXyB0E1Wym/BZVh/dfVPZz88D8aX8zmD+/4i04o1YHs4p5vEaSuv
|
||||
x/nYqhhdjHFFyIY53ZlGKQKBgGItRrDUN4y3Mng/NWX1XeQclk0effboEx77omFI
|
||||
gwdGlYhyOyHu3+R4O3+G5DNg7Z1YbZpDDKtSDZPmE+GZlYK/bHdxYCyWjJHs7UWW
|
||||
BjQlMkBRXComIYWevTLiZ2YBPwN48WG9RBZumfHe3NpyYDVdvll/lH3F+fXnPG5/
|
||||
N6hDAoGARq6gWGk4IiZj9J0P8ZgKGZzWwnxcAxPm5JbbkhTGNtrkJMOAok6j/vDy
|
||||
dq5i6XWXQg6jXlC8q6sNQfRjCZEzpdxlzsjpmeSMoKaqzTyQVTUQcdUtHedSPUDI
|
||||
YyXJScUDIfGaRgC8UwF8c4vpYPmy1s+jGD3CyhMLM8kgeM+85Yc=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
19
lockbox/frontend/cert.pem
Normal file
19
lockbox/frontend/cert.pem
Normal file
@@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDFjCCAf6gAwIBAgIUSdT3UOQDfikwJWhlc2sxUE6Vsi4wDQYJKoZIhvcNAQEL
|
||||
BQAwOTELMAkGA1UEBhMCVVMxETAPBgNVBAoMCE5vU3lzLUNBMRcwFQYDVQQDDA5O
|
||||
b1N5cyBMb2NhbCBDQTAeFw0yNTA5MDYwODIzMDdaFw0zNTA5MDQwODIzMDdaMDEx
|
||||
CzAJBgNVBAYTAlVTMQ4wDAYDVQQKDAVOb1N5czESMBAGA1UEAwwJbG9jYWxob3N0
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArg8PlencKCgepNGaRDx0
|
||||
lr+2QdAzILSaPt8WNAYK9+cNHfUAhoWB2c47nZezKtE5tInrw/6Ubx0wIvm2IaWR
|
||||
dN6TKUpBzrPru7hyxAGxKbefRVLhxt/O8rXxkJqBeCf5YByVZ4oGiEyeKyCt6umd
|
||||
faLQMB3g7ZKGS6khjWayVCMNh2BMwdYZ8pEDvdms91hdtS6rQntM9foOA1vYmwBv
|
||||
Z735NbymNGDEDHPx9lgBc6ZCL3gwNGvwEo/NNXubuYdZhwDtDytZTRfal1z1gE+7
|
||||
4BcX01ZdkUgo8bs6Gp9b3JEY59ctHv2K9HdMRJ8imfVq11HwUaVMrF17YOt43j7Z
|
||||
NwIDAQABox4wHDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcN
|
||||
AQELBQADggEBAAC0HUlG0udKDtiZUHXQklDDGCw/07ohnoRwVQxba6FzIPe74ca6
|
||||
j6Y7dKw9pXFy1h5scUyjS7Sl0AEkAalDh0wnedUwXPBtfrz5tzBUyOLJPtyFMCAV
|
||||
JhXpCivwd++f0p3TtpDvDR6VQy79rqPqCwO6wQ4dx/62g9f2NKtTztNV5w74TEV0
|
||||
Dx4OyFPAVoiEpqpTEuJ1v7HPYM8I4EcvE3jZSJDmzO1EhcxuJvUcJ1o1QBa5HW34
|
||||
/hTvDDlg2w+++Zc8/VYVgt/kGpf7jjFaUml3jzNNpxm+KGYudb+pGNVyb/w6schj
|
||||
FfKloHvLD2pAsCXo8VQ28VcaH42DEqeF10I=
|
||||
-----END CERTIFICATE-----
|
||||
13
lockbox/frontend/index.html
Normal file
13
lockbox/frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Lockbox</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
8
lockbox/frontend/jsconfig.json
Normal file
8
lockbox/frontend/jsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
27
lockbox/frontend/key.pem
Normal file
27
lockbox/frontend/key.pem
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEArg8PlencKCgepNGaRDx0lr+2QdAzILSaPt8WNAYK9+cNHfUA
|
||||
hoWB2c47nZezKtE5tInrw/6Ubx0wIvm2IaWRdN6TKUpBzrPru7hyxAGxKbefRVLh
|
||||
xt/O8rXxkJqBeCf5YByVZ4oGiEyeKyCt6umdfaLQMB3g7ZKGS6khjWayVCMNh2BM
|
||||
wdYZ8pEDvdms91hdtS6rQntM9foOA1vYmwBvZ735NbymNGDEDHPx9lgBc6ZCL3gw
|
||||
NGvwEo/NNXubuYdZhwDtDytZTRfal1z1gE+74BcX01ZdkUgo8bs6Gp9b3JEY59ct
|
||||
Hv2K9HdMRJ8imfVq11HwUaVMrF17YOt43j7ZNwIDAQABAoIBABoZyREGagydg4bc
|
||||
pYDw/dyzL93rnhcb7ftakaZId7GX9KgW2rbRY1jpa5gkrOnRSRFxEyknTlPhMRw1
|
||||
jOG7xbWcQL4S1A5ufX1/WbpZtJrYXapUFOYxHoPX07sG6D4/5E3My3ykvnkG4DsA
|
||||
YgQVdxflZ8mnWVjWvYuv94eQLFKgVSIr6OtJYAhpofVY/qRHYjBg8RLgjZbmyp9R
|
||||
QShlza0XHj4XZV5pXRrBWPWlF5ws7/OXHnCDxVq2xV1EQ9652HUTVtjPWiyfW4Ih
|
||||
pZV9kuDU8HT54tFjoVhWrVY3+EFrV63Y+6uUWwKoB/P8jMtvo5QwSgl8pZL61YQD
|
||||
RTo01oUCgYEA8TZ09fxkNgdH4XyQp+8FjdiBSlRx7XyXiE7dOfzxLaZ34fi85sgZ
|
||||
tI0Aai8ouskJci9QoKk2WkcX8vY0cyiXbgXehCbPBdRnPSM7zDRtvURhaL40w/+G
|
||||
VTcgJ/WIv/SwrmqL2jl56/ywrf/n+g1cN2bkCpNDIyXYY5wj34/e9mMCgYEAuLq0
|
||||
X1lzGzbgZ99XRNinCxq8hw7KVZpGd21bZsopgz7tnlZ4v347iIUhnNN93DwffFr8
|
||||
nhoVo5acW9DP8/xb7cXU9D3jdR1mLPW1PwtGuxc9kBYFYXpaZ7CngT+B5NjKn4Vd
|
||||
hNGIm2ltl3trRPGAmNCRXusG+ZC4ea3coKBqUB0CgYAOrobd9hfPZhAM/Hz9i8Hl
|
||||
yVjNQmiQ0PWUOWCjx+6SHcDMQ0yUK3fNEowE6ovrGpN1nMWmkcYaJpuhkTTOEZlt
|
||||
+/N4TbhqHWyPPxbDrilDzOa07mbdyy7M/wb5B6vkKyuZ4ihTBw6Ru5axcJMZGDkV
|
||||
sjCNKDt85y/NmFJiqColCwKBgQCvERnxpxcEOoyPREUzVNNyHaN/p0+vsqaHdhcC
|
||||
IiMXY+LThQWoDRykc+737iLAPiZktuHjf7r0Lr798LWzd30zqKH52lEe4366qx1a
|
||||
ovgkRJEuZQAycj8NN4h3X9VdKOtWJJENV3pMNq0Ku4dcbjc+G6M5Pil9CF8byd5m
|
||||
R8CZLQKBgQCieaqXoXI3PwuchtOp6rXmeMCmC669ukJsbqnlVQpD5Lr9gbNeyIZT
|
||||
mjx643a5zAgyQ8u/EH9aXKB/iUUgAbNrY0dx09tlRqg361XSZDjzORIJRYqfaS6R
|
||||
rX3PFqkkQ8ioCiS6TL2f+wFSCgNIzsZv1B+ScGQYzrvB7n/idVY/4A==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
3778
lockbox/frontend/package-lock.json
generated
Normal file
3778
lockbox/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
lockbox/frontend/package.json
Normal file
28
lockbox/frontend/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "vue",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.2.0",
|
||||
"@noble/secp256k1": "^2.3.0",
|
||||
"pinia": "^3.0.3",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"vue": "^3.5.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-vue-devtools": "^7.7.7"
|
||||
}
|
||||
}
|
||||
BIN
lockbox/frontend/public/favicon.ico
Normal file
BIN
lockbox/frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
17
lockbox/frontend/src/App.vue
Normal file
17
lockbox/frontend/src/App.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup>
|
||||
import Home from './views/Home.vue';
|
||||
</script>
|
||||
|
||||
|
||||
<!-- TODO Create API folder -->
|
||||
<template>
|
||||
<div class="flex h-screen">
|
||||
<Home/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:global(body) {
|
||||
background-color: black;
|
||||
}
|
||||
</style>
|
||||
1
lockbox/frontend/src/assets/logo.svg
Normal file
1
lockbox/frontend/src/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
1
lockbox/frontend/src/assets/main.css
Normal file
1
lockbox/frontend/src/assets/main.css
Normal file
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
29
lockbox/frontend/src/components/buttons/Button.vue
Normal file
29
lockbox/frontend/src/components/buttons/Button.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<button
|
||||
:type="type"
|
||||
:disabled="disabled"
|
||||
:class="[
|
||||
'inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2',
|
||||
disabled
|
||||
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
: 'bg-white text-black hover:bg-yellow-400',
|
||||
className
|
||||
]"
|
||||
@click="$emit('click', $event)"
|
||||
>
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button'
|
||||
},
|
||||
disabled: Boolean,
|
||||
className: String
|
||||
})
|
||||
|
||||
defineEmits(['click'])
|
||||
</script>
|
||||
37
lockbox/frontend/src/components/buttons/ToogleSwitch.vue
Normal file
37
lockbox/frontend/src/components/buttons/ToogleSwitch.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<button
|
||||
:aria-pressed="modelValue"
|
||||
role="switch"
|
||||
:aria-checked="modelValue"
|
||||
@click="toggle"
|
||||
:class="[
|
||||
'relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
|
||||
modelValue ? 'bg-yellow-400' : 'bg-gray-300 dark:bg-gray-600',
|
||||
disabled ? 'opacity-50 cursor-not-allowed' : ''
|
||||
]"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
:class="[
|
||||
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||
modelValue ? 'translate-x-5' : 'translate-x-0'
|
||||
]"
|
||||
></span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
disabled: Boolean
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const toggle = () => {
|
||||
if (!props.disabled) {
|
||||
emit('update:modelValue', !props.modelValue)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
12
lockbox/frontend/src/components/cards/Card.vue
Normal file
12
lockbox/frontend/src/components/cards/Card.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<script setup>
|
||||
defineOptions({ inheritAttrs: false })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="rounded-xl border"
|
||||
:class="$attrs.class"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
9
lockbox/frontend/src/components/cards/CardContent.vue
Normal file
9
lockbox/frontend/src/components/cards/CardContent.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div class="p-4 pt-0" :class="$attrs.class">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({ inheritAttrs: false })
|
||||
</script>
|
||||
12
lockbox/frontend/src/components/cards/CardDescription.vue
Normal file
12
lockbox/frontend/src/components/cards/CardDescription.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<p
|
||||
class="text-sm text-muted-foreground text-gray-500 dark:text-gray-400"
|
||||
:class="$attrs.class"
|
||||
>
|
||||
<slot></slot>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({ inheritAttrs: false })
|
||||
</script>
|
||||
9
lockbox/frontend/src/components/cards/CardFooter.vue
Normal file
9
lockbox/frontend/src/components/cards/CardFooter.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div class="flex items-center p-4 pt-0" :class="$attrs.class">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({ inheritAttrs: false })
|
||||
</script>
|
||||
9
lockbox/frontend/src/components/cards/CardHeader.vue
Normal file
9
lockbox/frontend/src/components/cards/CardHeader.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
defineOptions({ inheritAttrs: false })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col space-y-1.5 p-4 mb-3" :class="$attrs.class">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
12
lockbox/frontend/src/components/cards/CardTitle.vue
Normal file
12
lockbox/frontend/src/components/cards/CardTitle.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<h3
|
||||
class="font-semibold text-2xl leading-none tracking-tight"
|
||||
:class="$attrs.class"
|
||||
>
|
||||
<slot></slot>
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({ inheritAttrs: false })
|
||||
</script>
|
||||
50
lockbox/frontend/src/components/inputs/InputText.vue
Normal file
50
lockbox/frontend/src/components/inputs/InputText.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="relative w-full">
|
||||
<input
|
||||
v-bind="$attrs"
|
||||
:id="id"
|
||||
:type="showPassword ? 'text' : type"
|
||||
:value="modelValue"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:class="[
|
||||
'w-full pr-10 rounded-md border border-gray-300 px-3 py-2 text-sm text-black shadow-sm focus:outline-none focus:ring-1 focus:ring-yellow-400 focus:border-yellow-400 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
error ? 'border-red-500 focus:ring-red-500 focus:border-red-500' : ''
|
||||
]"
|
||||
/>
|
||||
|
||||
<button
|
||||
v-if="type === 'password'"
|
||||
type="button"
|
||||
@click="showPassword = !showPassword"
|
||||
class="absolute inset-y-0 right-0 flex items-center px-2 text-gray-500"
|
||||
tabindex="-1"
|
||||
>
|
||||
<EyeIcon v-if="showPassword" class="h-4 w-4"></EyeIcon>
|
||||
<EyeSlashIcon v-else class="h-4 w-4"></EyeSlashIcon>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { EyeIcon, EyeSlashIcon } from '@heroicons/vue/24/solid'
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
modelValue: String,
|
||||
id: String,
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
placeholder: String,
|
||||
disabled: Boolean,
|
||||
error: Boolean
|
||||
})
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const showPassword = ref(false)
|
||||
</script>
|
||||
12
lockbox/frontend/src/components/labels/Label.vue
Normal file
12
lockbox/frontend/src/components/labels/Label.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<label :for="forId" :class="['block text-sm font-medium text-gray-700 dark:text-gray-300', className]">
|
||||
<slot></slot>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
forId: String,
|
||||
className: String
|
||||
})
|
||||
</script>
|
||||
125
lockbox/frontend/src/components/tabs/TabCreateUser.vue
Normal file
125
lockbox/frontend/src/components/tabs/TabCreateUser.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { TabPanel} from '@headlessui/vue'
|
||||
import * as secp from '@noble/secp256k1'
|
||||
import { KeyIcon, ArrowPathIcon, ClipboardDocumentIcon } from '@heroicons/vue/24/solid'
|
||||
|
||||
import Card from '../cards/Card.vue';
|
||||
import CardHeader from '../cards/CardHeader.vue';
|
||||
import CardTitle from '../cards/CardTitle.vue';
|
||||
import CardDescription from '../cards/CardDescription.vue';
|
||||
import CardContent from '../cards/CardContent.vue';
|
||||
import Label from '../labels/Label.vue';
|
||||
import InputText from '../inputs/InputText.vue';
|
||||
import Button from '../buttons/Button.vue';
|
||||
import ToogleSwitch from '../buttons/ToogleSwitch.vue';
|
||||
|
||||
const privateKeyInput = ref("")
|
||||
const privateKeyB64 = ref("")
|
||||
const publicKeyB64 = ref("")
|
||||
const isTextEnabled = ref(true)
|
||||
|
||||
async function generate() {
|
||||
var array = null
|
||||
if (isTextEnabled.value){
|
||||
const encoder = new TextEncoder();
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(privateKeyInput.value));
|
||||
array = new Uint8Array(hashBuffer);
|
||||
|
||||
privateKeyB64.value = btoa(String.fromCharCode(...array))
|
||||
}else{
|
||||
try {
|
||||
const decoded = atob(privateKeyInput.value)
|
||||
const bytes = new Uint8Array(decoded.length)
|
||||
for (let i = 0; i < decoded.length; i++) {
|
||||
bytes[i] = decoded.charCodeAt(i)
|
||||
}
|
||||
if (bytes.length !== 32) {
|
||||
throw new Error('Tamanho inválido')
|
||||
}
|
||||
array = bytes
|
||||
} catch (e) {
|
||||
array = new Uint8Array(32)
|
||||
crypto.getRandomValues(array)
|
||||
privateKeyInput.value = btoa(String.fromCharCode(...array))
|
||||
}
|
||||
}
|
||||
generatePublicKey(array)
|
||||
|
||||
}
|
||||
|
||||
function generatePublicKey(privateKeyBytes) {
|
||||
const publicKeyBytes = secp.getPublicKey(privateKeyBytes)
|
||||
publicKeyB64.value = btoa(String.fromCharCode(...publicKeyBytes))
|
||||
}
|
||||
|
||||
function onSwitchChanged(newValue) {
|
||||
privateKeyInput.value = ""
|
||||
privateKeyB64.value = ""
|
||||
publicKeyB64.value = ""
|
||||
}
|
||||
|
||||
function copy(text){
|
||||
navigator.clipboard.writeText(text)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabPanel>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2 text-yellow-400">
|
||||
<KeyIcon class="h-5 w-5" />
|
||||
Create New User
|
||||
</CardTitle>
|
||||
<CardDescription class="text-slate-200">
|
||||
Generate a new ECDSA keypair for user authentication
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class=" space-y-2">
|
||||
<!-- Private key Characteres input -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex flex-row gap-4">
|
||||
<Label>Private Key</Label>
|
||||
<ToogleSwitch v-model="isTextEnabled" @update:modelValue="onSwitchChanged"/>
|
||||
<Label>Text Password</Label>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<InputText id="privateKey" type="password" v-model="privateKeyInput" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
|
||||
<Button @click="generate()" class="gap-2">
|
||||
<ArrowPathIcon class="h-4 w-4"/>
|
||||
<span v-if="!privateKeyInput">Generate</span>
|
||||
</Button>
|
||||
<Button v-if="privateKeyInput" @click="copy(privateKeyInput)" class="flex items-center gap-2">
|
||||
<ClipboardDocumentIcon class="h-4 w-4"/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Public Key -->
|
||||
<div class="space-y-2">
|
||||
<Label forId="publicKey" class="text-slate-300">Public Key</Label>
|
||||
<div class="flex gap-2">
|
||||
<InputText id="publicKey" type="text" v-model="publicKeyB64" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
|
||||
<Button @click="copy(publicKeyB64)" class="flex items-center gap-2">
|
||||
<ClipboardDocumentIcon class="h-4 w-4"/>
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Private Key -->
|
||||
<div class="space-y-2" v-if="isTextEnabled">
|
||||
<Label forId="privateKeyB64" class="text-slate-300">Private Key</Label>
|
||||
<div class="flex gap-2">
|
||||
<InputText id="privateKeyB64" type="password" v-model="privateKeyB64" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
|
||||
<Button @click="copy(privateKeyB64)" class="flex items-center gap-2">
|
||||
<ClipboardDocumentIcon class="h-4 w-4"/>
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</template>
|
||||
245
lockbox/frontend/src/components/tabs/TabProofOfWork.vue
Normal file
245
lockbox/frontend/src/components/tabs/TabProofOfWork.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<script setup>
|
||||
import { TabGroup, TabList, Tab, TabPanels, TabPanel} from '@headlessui/vue'
|
||||
import { KeyIcon, BoltIcon, ShieldCheckIcon, UserPlusIcon, UsersIcon, ClipboardDocumentListIcon} from '@heroicons/vue/24/solid'
|
||||
|
||||
import Card from '../cards/Card.vue';
|
||||
import CardHeader from '../cards/CardHeader.vue';
|
||||
import CardTitle from '../cards/CardTitle.vue';
|
||||
import CardDescription from '../cards/CardDescription.vue';
|
||||
import CardContent from '../cards/CardContent.vue';
|
||||
import Label from '../labels/Label.vue';
|
||||
import InputText from '../inputs/InputText.vue';
|
||||
import Button from '../buttons/Button.vue';
|
||||
import CardFooter from '../cards/CardFooter.vue';
|
||||
import { ref } from 'vue';
|
||||
import socket from '@/plugins/socketio'
|
||||
|
||||
const publicKey = ref("")
|
||||
const targetDifficulty = ref("4")
|
||||
|
||||
const lockboxServiceApiMineUrl = "http://127.0.0.1:5001/mine";
|
||||
const tasks = ref(null)
|
||||
|
||||
// TODO Create api file
|
||||
|
||||
function start_mining(){
|
||||
const data = {"public_key":publicKey.value, "force":targetDifficulty.value};
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
fetch(lockboxServiceApiMineUrl+"/start", requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
list_tasks()
|
||||
})
|
||||
.catch(error => {
|
||||
console.error
|
||||
|
||||
('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function list_tasks(){
|
||||
fetch(lockboxServiceApiMineUrl+"/list")
|
||||
.then(response => {
|
||||
if (!response.ok){
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
tasks.value = data;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function pause_task(taskId){
|
||||
const data = {};
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
fetch(lockboxServiceApiMineUrl+"/pause/"+taskId, requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
list_tasks()
|
||||
})
|
||||
.catch(error => {
|
||||
console.error
|
||||
|
||||
('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function resume_task(taskId){
|
||||
const data = {};
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
fetch(lockboxServiceApiMineUrl+"/resume/"+taskId, requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
list_tasks()
|
||||
})
|
||||
.catch(error => {
|
||||
console.error
|
||||
|
||||
('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function cancel_task(taskId){
|
||||
const data = {};
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
fetch(lockboxServiceApiMineUrl+"/cancel/"+taskId, requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
list_tasks()
|
||||
})
|
||||
.catch(error => {
|
||||
console.error
|
||||
|
||||
('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
socket.on('taskUpdated', function(msg) {
|
||||
tasks.value[msg.task_id] = msg.result
|
||||
});
|
||||
|
||||
list_tasks()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabPanel class=" space-y-5">
|
||||
<!-- Mining user -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<BoltIcon class="h-5 w-5" />
|
||||
Proof of Work
|
||||
</CardTitle>
|
||||
<CardDescription class="text-slate-200">
|
||||
Generate proof of work for a public key
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div class="flex flex-row w-full space-x-5 items-center justify-center text-center">
|
||||
<div class="flex flex-col space-y-2 w-full">
|
||||
<Label forId="publicKey" class="text-slate-300">Public Key</Label>
|
||||
<div class="flex gap-2">
|
||||
<InputText id="publicKey" v-model="publicKey" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2 w-full">
|
||||
<Label forId="targetDifficulty" class="text-slate-300">Target Difficulty (Leading Zeros)</Label>
|
||||
<div class="flex gap-2">
|
||||
<InputText id="targetDifficulty" v-model="targetDifficulty" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button @click="start_mining()" class="flex items-center gap-2 w-full">
|
||||
Start Mining
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<!-- Active jobs -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2 text-yellow-400">
|
||||
Active Jobs
|
||||
</CardTitle>
|
||||
<CardDescription class="text-slate-200">
|
||||
Monitor and control proof of work jobs
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="overflow-y-auto rounded-md p-4">
|
||||
<CardContent v-for="([taskId, task]) in Object.entries(tasks)" :key="taskId" class="border border-slate-300 rounded mb-2 pt-2">
|
||||
<div class="grid md:grid-cols-5 gap-4 items-center">
|
||||
<div class="flex flex-col">
|
||||
<p class="text-sm text-gray-300 w-full">Public Key</p>
|
||||
<p class=" text-gray-300">{{task.public_key}}</p>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<p class="text-sm text-gray-300 w-full">Difficulty</p>
|
||||
<p class=" text-gray-300">{{task.best_force}}/{{task.target_force}}</p>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<p class="text-sm text-gray-300 w-full">Nonce</p>
|
||||
<p class=" text-gray-300">{{task.best_nonce}}</p>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<p class="text-sm text-gray-300 w-full">Status</p>
|
||||
<p :class="{
|
||||
'text-yellow-400': task.status === 'paused',
|
||||
'text-blue-400': task.status === 'running',
|
||||
'text-green-400': task.status === 'completed',
|
||||
'text-red-400': task.status === 'cancelled',
|
||||
'text-gray-300': !['paused', 'running', 'completed', 'cancelled'].includes(task.status)
|
||||
}">
|
||||
{{ task.status }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-row space-x-3 items-center mt-2">
|
||||
<div v-if="!['completed', 'cancelled', 'error'].includes(task.status)" class="space-x-3">
|
||||
<!-- TODO Change to Icons -->
|
||||
<Button v-if="['running'].includes(task.status)" @click="pause_task(taskId)">Pause</Button>
|
||||
<Button v-if="['paused'].includes(task.status)" @click="resume_task(taskId)">Playy</Button>
|
||||
<Button @click="cancel_task(taskId)">Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</TabPanel>
|
||||
</template>
|
||||
126
lockbox/frontend/src/components/tabs/TabSigningRequests.vue
Normal file
126
lockbox/frontend/src/components/tabs/TabSigningRequests.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<script setup>
|
||||
import { TabGroup, TabList, Tab, TabPanels, TabPanel} from '@headlessui/vue'
|
||||
import { KeyIcon, BoltIcon, ShieldCheckIcon, UserPlusIcon, UsersIcon, ClipboardDocumentListIcon} from '@heroicons/vue/24/solid'
|
||||
|
||||
import Card from '../cards/Card.vue';
|
||||
import CardHeader from '../cards/CardHeader.vue';
|
||||
import CardTitle from '../cards/CardTitle.vue';
|
||||
import CardDescription from '../cards/CardDescription.vue';
|
||||
import CardContent from '../cards/CardContent.vue';
|
||||
import { ref } from 'vue';
|
||||
import Button from '../buttons/Button.vue';
|
||||
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import socket from '@/plugins/socketio'
|
||||
|
||||
const lockboxServiceApiUrl = "http://127.0.0.1:5001";
|
||||
const auth = useAuthStore()
|
||||
|
||||
const requestedSignatures = ref([])
|
||||
|
||||
function listSignatureRequests(user){
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: {'Authorization': 'Bearer ' + auth.tokens[user]}
|
||||
};
|
||||
fetch(lockboxServiceApiUrl+"/users/"+user+"/signatures", requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok){
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
requestedSignatures.value = data
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function approveSignature(requestId, user, approved){
|
||||
const data = {"approved":approved};
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + auth.tokens[user]},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
fetch(lockboxServiceApiUrl+"/users/"+user+"/signatures/"+requestId, requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
updateSignatures()
|
||||
})
|
||||
.catch(error => {
|
||||
console.error
|
||||
|
||||
('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
socket.on('userAdded', function(msg) {
|
||||
updateSignatures()
|
||||
});
|
||||
|
||||
socket.on('signatureWaiting', function(msg) {
|
||||
updateSignatures()
|
||||
});
|
||||
|
||||
function updateSignatures(){
|
||||
Object.entries(auth.tokens).forEach(([key, token]) => {
|
||||
listSignatureRequests(key)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabPanel>
|
||||
<!-- Head -->
|
||||
<Card >
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2 text-yellow-400">
|
||||
<ClipboardDocumentListIcon class="h-5 w-5" />
|
||||
Pending Signing Requests
|
||||
</CardTitle>
|
||||
<CardDescription class="text-slate-200">
|
||||
Review and approve signing requests from logged-in users
|
||||
</CardDescription>
|
||||
<Button @click="updateSignatures()">Refresh</Button>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<CardContent v-for="request in requestedSignatures" class="border border-slate-300 rounded mb-2 pt-2">
|
||||
<div class="flex flex-col ">
|
||||
<div class="flex flex-col w-full">
|
||||
<p class="font-mono text-sm text-gray-300">{{request.requestId}}</p>
|
||||
<!-- <div class="flex flex-row space-y-1 gap-2">
|
||||
<p class="font-mono text-sm text-gray-300">{{request.moduleRequesting}}</p>
|
||||
<p class="font-mono text-sm text-gray-300">{{request.requestedAt}}</p>
|
||||
</div> -->
|
||||
<p class="font-mono text-sm text-gray-300">{{request.publicKey}}</p>
|
||||
<div class="flex items-center gap-4 text-sm text-gray-400">
|
||||
<span>{{request.data}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col space-x-3 items-center">
|
||||
<p class="font-mono text-sm text-gray-300">{{request.action}}</p>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Button @click="approveSignature(request.requestId, request.user, true)">Approve</Button>
|
||||
<Button @click="approveSignature(request.requestId, request.user, false)">Reject</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</TabPanel>
|
||||
</template>
|
||||
201
lockbox/frontend/src/components/tabs/TabUsers.vue
Normal file
201
lockbox/frontend/src/components/tabs/TabUsers.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<script setup>
|
||||
import { TabGroup, TabList, Tab, TabPanels, TabPanel} from '@headlessui/vue'
|
||||
import { KeyIcon, BoltIcon, ShieldCheckIcon, UserPlusIcon, UsersIcon, ClipboardDocumentListIcon} from '@heroicons/vue/24/solid'
|
||||
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
import Card from '../cards/Card.vue';
|
||||
import CardHeader from '../cards/CardHeader.vue';
|
||||
import CardTitle from '../cards/CardTitle.vue';
|
||||
import CardDescription from '../cards/CardDescription.vue';
|
||||
import CardContent from '../cards/CardContent.vue';
|
||||
import Label from '../labels/Label.vue';
|
||||
import ToogleSwitch from '../buttons/ToogleSwitch.vue';
|
||||
import { ref } from 'vue';
|
||||
import InputText from '../inputs/InputText.vue';
|
||||
import CardFooter from '../cards/CardFooter.vue';
|
||||
import Button from '../buttons/Button.vue';
|
||||
|
||||
const privateKey = ref("")
|
||||
const publicKey = ref("")
|
||||
const proofOfWork = ref("")
|
||||
const credentialPassword = ref("")
|
||||
const isLoginEnabled = ref(false)
|
||||
|
||||
const lockboxServiceApiUrl = "http://127.0.0.1:5001";
|
||||
const auth = useAuthStore()
|
||||
|
||||
const users = ref({})
|
||||
|
||||
function onSwitchChanged(newValue) {
|
||||
// privateKeyInput.value = ""
|
||||
// privateKeyB64.value = ""
|
||||
// publicKeyB64.value = ""
|
||||
}
|
||||
|
||||
function set_login_user(userId){
|
||||
if(!isLoginEnabled.value){
|
||||
isLoginEnabled.value = true
|
||||
}
|
||||
publicKey.value = userId
|
||||
}
|
||||
|
||||
function add_user(){
|
||||
const data = {password:privateKey.value, data:{proof_of_work:proofOfWork.value}, credentialPassword:credentialPassword.value};
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
fetch(lockboxServiceApiUrl+"/users", requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
auth.setToken(data.verifying_key, data.token);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error
|
||||
|
||||
('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function login_user(){
|
||||
const data = {credentialPassword:credentialPassword.value};
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
fetch(lockboxServiceApiUrl+"/users/"+publicKey.value+"/login", requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
auth.setToken(data.verifying_key, data.token);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error
|
||||
|
||||
('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function logout_user(userId){
|
||||
delete auth.tokens[userId]
|
||||
list_users()
|
||||
}
|
||||
|
||||
function list_users(){
|
||||
fetch(lockboxServiceApiUrl+"/users")
|
||||
.then(response => {
|
||||
if (!response.ok){
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
users.value = data;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
list_users()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabPanel class="flex flex-col space-y-3">
|
||||
<Card class="">
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2 text-yellow-400">
|
||||
<UserPlusIcon class="h-5 w-5" />
|
||||
Add User
|
||||
</CardTitle>
|
||||
<CardDescription class="text-slate-200">
|
||||
Add a user with private key or login with public key
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class=" space-y-2">
|
||||
<div class="flex flex-row gap-4">
|
||||
<Label>Add with Private Key</Label>
|
||||
<ToogleSwitch v-model="isLoginEnabled" @update:modelValue="onSwitchChanged"/>
|
||||
<Label>Login with Public Key</Label>
|
||||
</div>
|
||||
<div v-if="!isLoginEnabled" class="flex flex-col space-y-2 w-full">
|
||||
<Label forId="privateKey" class="text-slate-300">Private Key</Label>
|
||||
<InputText id="privateKey" type="password" v-model="privateKey" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
|
||||
</div>
|
||||
<div v-if="!isLoginEnabled" class="flex flex-col space-y-2 w-full">
|
||||
<Label forId="proofOfWork" class="text-slate-300">Proof of Work</Label>
|
||||
<InputText id="proofOfWork" v-model="proofOfWork" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
|
||||
</div>
|
||||
<div v-if="isLoginEnabled" class="flex flex-col space-y-2 w-full">
|
||||
<Label forId="publicKey" class="text-slate-300">Public Key</Label>
|
||||
<InputText id="publicKey" v-model="publicKey" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2 w-full">
|
||||
<Label forId="credentialPassword" class="text-slate-300">Credential Password</Label>
|
||||
<InputText id="credentialPassword" type="password" v-model="credentialPassword" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button v-if="!isLoginEnabled" @click="add_user()" class="flex items-center gap-2 w-full">
|
||||
Save
|
||||
</Button>
|
||||
<Button v-if="isLoginEnabled" @click="login_user()" class="flex items-center gap-2 w-full">
|
||||
Login
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Card >
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2 text-yellow-400">
|
||||
<UsersIcon class="h-5 w-5" />
|
||||
User Management
|
||||
</CardTitle>
|
||||
<CardDescription class="text-slate-200">
|
||||
View and manage your users in the system
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent v-for="user in users" class="border border-slate-300 rounded mb-2 pt-2">
|
||||
<div class="flex flex-col ">
|
||||
<div class="space-y-1 w-full">
|
||||
<p class="font-mono text-sm text-gray-300">{{user.id}}</p>
|
||||
<div class="flex items-center gap-4 text-sm text-gray-400">
|
||||
<span>Added At: {{user.added_at}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row space-x-3 items-center">
|
||||
<div v-if="user.id in auth.tokens">
|
||||
<p class="">Logged In</p>
|
||||
<Button @click="logout_user(user.id)">Logout</Button>
|
||||
</div>
|
||||
<div v-if="!(user.id in auth.tokens)">
|
||||
<p class="">Logged Out</p>
|
||||
<Button @click="set_login_user(user.id)">Login</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</template>
|
||||
12
lockbox/frontend/src/main.js
Normal file
12
lockbox/frontend/src/main.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
|
||||
import socket from './plugins/socketio'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.config.globalProperties.$socket = socket
|
||||
app.mount('#app')
|
||||
3
lockbox/frontend/src/plugins/socketio.js
Normal file
3
lockbox/frontend/src/plugins/socketio.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { io } from 'socket.io-client'
|
||||
const socket = io('https://localhost:5001')
|
||||
export default socket
|
||||
21
lockbox/frontend/src/stores/auth.js
Normal file
21
lockbox/frontend/src/stores/auth.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
tokens: {},
|
||||
}),
|
||||
actions: {
|
||||
setToken(publicKey, token) {
|
||||
this.tokens[publicKey] = token
|
||||
localStorage.setItem(publicKey, token)
|
||||
},
|
||||
clearToken(publicKey) {
|
||||
this.tokens[publicKey] = null
|
||||
localStorage.removeItem(publicKey)
|
||||
},
|
||||
loadTokenFromStorage(publicKey) {
|
||||
const token = localStorage.getItem(publicKey)
|
||||
if (token) this.tokens[publicKey] = token
|
||||
}
|
||||
},
|
||||
})
|
||||
67
lockbox/frontend/src/views/Home.vue
Normal file
67
lockbox/frontend/src/views/Home.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<script setup>
|
||||
import { TabGroup, TabList, Tab, TabPanels, TabPanel} from '@headlessui/vue'
|
||||
import { KeyIcon, BoltIcon, ShieldCheckIcon, UserPlusIcon, UsersIcon, ClipboardDocumentListIcon} from '@heroicons/vue/24/solid'
|
||||
|
||||
import { ref } from 'vue'
|
||||
|
||||
import Card from '@/components/cards/Card.vue'
|
||||
import CardHeader from '@/components/cards/CardHeader.vue'
|
||||
import CardTitle from '@/components/cards/CardTitle.vue'
|
||||
import CardDescription from '@/components/cards/CardDescription.vue'
|
||||
import CardContent from '@/components/cards/CardContent.vue'
|
||||
import CardFooter from '@/components/cards/CardFooter.vue'
|
||||
import TabCreateUser from '@/components/tabs/TabCreateUser.vue'
|
||||
import TabProofOfWork from '@/components/tabs/TabProofOfWork.vue'
|
||||
import TabAddUser from '@/components/tabs/TabUsers.vue'
|
||||
import TabSigningRequests from '@/components/tabs/TabSigningRequests.vue'
|
||||
|
||||
const tabItems = [
|
||||
{label:"Create User", icon:KeyIcon, tabComponent:TabCreateUser},
|
||||
{label:"Proof of Work", icon:BoltIcon, tabComponent:TabProofOfWork},
|
||||
{label:"Users", icon:UserPlusIcon, tabComponent:TabAddUser},
|
||||
{label:"Signing Requests", icon:ClipboardDocumentListIcon, tabComponent:TabSigningRequests},
|
||||
]
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-black w-screen text-yellow-400">
|
||||
<header className="border-b border-yellow-400/20 py-6">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="text-3xl font-black">
|
||||
<span className="text-yellow-400">LOCK</span>
|
||||
<span className="text-white">BOX</span>
|
||||
</div>
|
||||
<div className="w-1 h-8 bg-yellow-400"></div>
|
||||
<span className="text-gray-400">Secure ECDSA Keypair Management</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-6 py-8">
|
||||
<TabGroup>
|
||||
<TabList class="flex flex-wrap gap-2 mb-8 border-b border-yellow-400/20">
|
||||
<Tab v-for="tab in tabItems" as="template" :key="tab" v-slot="{ selected }">
|
||||
<button class="flex items-center space-x-2 px-6 py-3 font-semibold transition-all duration-300 border-b-2"
|
||||
:class="{ 'text-yellow-400 border-yellow-400': selected, 'text-gray-400 border-transparent hover:text-yellow-400 hover:border-yellow-400/50': !selected }">
|
||||
<component :is="tab.icon" class="h-4 w-4"></component>
|
||||
<span>{{ tab.label }}</span>
|
||||
</button>
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels class="mt-6 space-y-6 px-40" v-for="tab in tabItems" :key="tab.label">
|
||||
<component :is="tab.tabComponent"></component>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* button {
|
||||
transition: background 0.2s, color 0.2s;
|
||||
} */
|
||||
</style>
|
||||
23
lockbox/frontend/vite.config.js
Normal file
23
lockbox/frontend/vite.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
tailwindcss(),
|
||||
],
|
||||
server: {
|
||||
port: 3001,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
})
|
||||
52
lockbox/frontend/webView.py
Normal file
52
lockbox/frontend/webView.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import os
|
||||
import webview
|
||||
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
def run_webview():
|
||||
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
ssl_context.load_cert_chain(certfile=cert_path, keyfile=key_path)
|
||||
|
||||
url = "https://localhost:5001/"
|
||||
|
||||
webview.create_window(
|
||||
"Unlock Key",
|
||||
url,
|
||||
width=800,
|
||||
height=800,
|
||||
resizable=True,
|
||||
confirm_close=True,
|
||||
text_select=True,
|
||||
frameless=False,
|
||||
background_color="#FFFFFF",
|
||||
)
|
||||
|
||||
webview.start()
|
||||
|
||||
if __name__ == "__main__":
|
||||
dir = os.path.dirname(os.path.abspath(__file__))
|
||||
ca_path = os.path.join(dir, "ca.pem")
|
||||
ca_key_path = os.path.join(dir, "ca_key.pem")
|
||||
cert_path = os.path.join(dir, "cert.pem")
|
||||
key_path = os.path.join(dir, "key.pem")
|
||||
|
||||
if not os.path.exists(cert_path) or not os.path.exists(key_path) or not os.path.exists(ca_path) or not os.path.exists(ca_key_path) :
|
||||
ca, cert, key = generate_ca_and_cert(ca_path, ca_key_path, cert_path, key_path)
|
||||
|
||||
system = platform.system()
|
||||
if system == "Windows":
|
||||
add_ca_windows(ca)
|
||||
elif system == "Darwin":
|
||||
add_ca_macos(ca)
|
||||
elif system == "Linux":
|
||||
add_ca_linux(ca)
|
||||
else:
|
||||
raise Exception("OS not supported")
|
||||
|
||||
print("Cert installed")
|
||||
else:
|
||||
print("Cert already exists")
|
||||
|
||||
run_webview()
|
||||
15
lockbox/info.json
Normal file
15
lockbox/info.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"id": "lockbox",
|
||||
"version": 0.097,
|
||||
"modules": [
|
||||
{
|
||||
"id": "lockboxClient",
|
||||
"version": 0
|
||||
},
|
||||
{
|
||||
"id": "miner",
|
||||
"version": 0
|
||||
}
|
||||
],
|
||||
"frontend": "vue"
|
||||
}
|
||||
154
lockbox/lockboxClient.py
Normal file
154
lockbox/lockboxClient.py
Normal file
@@ -0,0 +1,154 @@
|
||||
import requests
|
||||
import urllib.parse
|
||||
from threading import Thread
|
||||
|
||||
import os
|
||||
import ssl
|
||||
import socketio
|
||||
import logging, traceback
|
||||
import json
|
||||
import time
|
||||
|
||||
from libs.app.common.paths import ROOT_DIR
|
||||
from libs.app.common.logging import get_logger
|
||||
from libs.app.common.network_utils import check_url
|
||||
from libs.app.common.process import new_python_process
|
||||
from libs.noSys.noSysModule import NoSysModule
|
||||
from libs.noSys.users import User
|
||||
from .lockboxService import UserData
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
class LockboxClient(NoSysModule):
|
||||
def __init__(self, nosys_core):
|
||||
super().__init__(nosys_core)
|
||||
|
||||
self.api_host = self.config.get("server").get("host")
|
||||
self.api_port = self.config.get("server").get("port")
|
||||
self.service_url = f'{self.api_host}:{self.api_port}'
|
||||
self.session = requests.Session()
|
||||
certs_path = os.path.join(ROOT_DIR, "libs", "api", "certs")
|
||||
ca_path = os.path.join(certs_path, "ca.pem")
|
||||
self.session.verify = ca_path
|
||||
session_socketio = requests.Session()
|
||||
session_socketio.verify = ca_path
|
||||
self.socketio = socketio.Client(http_session=session_socketio)
|
||||
self.sio_events()
|
||||
|
||||
self.users = {} # UserData
|
||||
self.user_tokens = {}
|
||||
self.signature_requests = {} # request_id | callback
|
||||
|
||||
def setup(self):
|
||||
self.nosys_core.modules.pmc = self
|
||||
|
||||
def on_nosys_ready(self, event):
|
||||
logger.debug("LOCKBOX READY")
|
||||
self.starts_lockbox_service()
|
||||
self.connect()
|
||||
# TODO Remove it
|
||||
# self.add("I0x1Y2FzR2FicmllbFZhekRvc1NhbnRvc0luYWNpbyE=", UserData(proof_of_work="eYnU*@"))
|
||||
self.users = self.user_list()
|
||||
for user in self.users:
|
||||
u = User(user)
|
||||
self.nosys_core.users.add_user(u)
|
||||
|
||||
def starts_lockbox_service(self):
|
||||
try:
|
||||
health_check_url = f"{self.service_url}/healthCheck"
|
||||
if not check_url(health_check_url, timeout=3):
|
||||
logger.debug(f"Starting lockbox Service in a new process")
|
||||
process_pid = new_python_process("libs/lockbox/lockboxService.py", args=[self.api_port], new_console=True)
|
||||
logger.debug(f"Lockbox process PID {process_pid}")
|
||||
else:
|
||||
logger.debug(f"Lockbox service already running in {health_check_url}")
|
||||
except Exception:
|
||||
logger.exception("ERROR")
|
||||
|
||||
def add(self, password, data = UserData()):
|
||||
add_url = f"{self.service_url}/users"
|
||||
body = {'password': password, 'data':data.__dict__}
|
||||
result = self.session.post(add_url, json = body)
|
||||
content = result.json()
|
||||
user_token = content["token"]
|
||||
verifying_key = content["verifying_key"]
|
||||
# self.users[verifying_key] = data
|
||||
self.user_tokens[verifying_key] = user_token
|
||||
return (verifying_key, user_token)
|
||||
|
||||
def user_list(self):
|
||||
users_list_url = f"{self.service_url}/users"
|
||||
result = self.session.get(users_list_url)
|
||||
for user in result.json():
|
||||
print("LOGGED", user["logged"])
|
||||
if user["logged"]:
|
||||
verifying_key = user["id"]
|
||||
self.users[verifying_key] = UserData(id=user["id"],proof_of_work=user["proof_of_work"], time_to_live=user["time_to_live"], added_at=user["added_at"], logged=user["logged"])
|
||||
return self.users
|
||||
|
||||
def set_authorization_header(self, user, headers={}):
|
||||
if user in self.user_tokens:
|
||||
headers["Authorization"] = f'Bearer {self.user_tokens[user]}'
|
||||
return headers
|
||||
|
||||
def get(self, user):
|
||||
# users_data_url = f"{self.service_url}/users/{urllib.parse.quote(user, safe='')}"
|
||||
# headers = self.set_authorization_header(user)
|
||||
# result = self.session.get(users_data_url, headers=headers)
|
||||
# content = result.json()
|
||||
# data = UserData(proof_of_work=content["proof_of_work"])
|
||||
data = self.users[user]
|
||||
return data
|
||||
|
||||
def sign(self, data, user, callback, info=None):
|
||||
sign_url = f"{self.service_url}/users/{urllib.parse.quote(user, safe='')}/signatures"
|
||||
headers = self.set_authorization_header(user)
|
||||
body = {'data': data, "info":info}
|
||||
result = self.session.post(sign_url, json = body, headers=headers)
|
||||
request_id = result.json()["requestId"]
|
||||
self.signature_requests[request_id] = callback
|
||||
return request_id
|
||||
|
||||
def connect(self):
|
||||
while True:
|
||||
try:
|
||||
logger.debug(f"Trying to connect to lockbox service {self.service_url}")
|
||||
self.socketio.connect(self.service_url, wait=True ,wait_timeout=60, transports=["websocket"])
|
||||
logger.debug(f"Lockbox service connected {self.service_url}")
|
||||
break
|
||||
except Exception:
|
||||
logger.exception(f"Failed to connect to lockbox service {self.service_url} - Retrying in 10 seconds")
|
||||
time.sleep(10)
|
||||
# self.socketio.emit('message', {'from': 'client'})
|
||||
|
||||
|
||||
def sio_events(self):
|
||||
@self.socketio.on('message')
|
||||
def message(*args, **kwargs):
|
||||
logger.debug("Message",args, kwargs)
|
||||
|
||||
@self.socketio.on("userAdded")
|
||||
def on_user_added(*args, **kwargs):
|
||||
user_id = args[0]
|
||||
logger.debug(f"User added {user_id}")
|
||||
self.user_list()
|
||||
if user_id in self.users:
|
||||
u = User(user_id)
|
||||
self.nosys_core.users.add_user(u)
|
||||
|
||||
@self.socketio.on("signatureWaiting")
|
||||
def on_signature_waiting(*args, **kwargs):
|
||||
logger.debug(f"Signature Waiting {args[0]}")
|
||||
|
||||
@self.socketio.on("signatureResponse")
|
||||
def on_signature_response(*args, **kwargs):
|
||||
logger.debug(f"Signature response {args[0]}")
|
||||
request_id = args[0]["requestId"]
|
||||
signature = args[0]["signature"]
|
||||
if request_id in self.signature_requests:
|
||||
self.signature_requests[request_id](request_id, signature)
|
||||
# signature = result.json()["signature"]
|
||||
# if signature:
|
||||
# return signature
|
||||
# else:
|
||||
# raise Exception("Sign data failed")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user