Added libs

This commit is contained in:
Lucas
2026-01-25 13:55:46 +10:00
parent 575c682afc
commit f70af3c4ea
229 changed files with 26983 additions and 0 deletions

2
api/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
__pycache__
api.zip

1
api/.mtimes.json Normal file
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,6 @@
{
"server": {
"host": "127.0.0.1",
"port": "5050"
}
}

51
api/eventsSocketio.py Normal file
View 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
View File

@@ -0,0 +1,10 @@
{
"id": "api",
"version": 0.042,
"modules": [
{
"id": "api",
"version": 0
}
]
}

4
api/requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
flask
flask-socketio
python-socketio[client]
flask_cors

4
app/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
__pycache__
.venv
*.log
app.zip

1
app/.mtimes.json Normal file
View 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
View File

@@ -0,0 +1,4 @@
__pycache__
.venv
*.log
app.zip

75
app/BKP/config.json Normal file
View 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
View File

@@ -0,0 +1,5 @@
{
"id":"app",
"version":0,
"modules":[]
}

70
app/BKP/main.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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")

View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,5 @@
{
"id": "app",
"version": 0.111,
"modules": []
}

117
app/main.py Normal file
View 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
View File

@@ -0,0 +1,4 @@
requests
Flask
flask-socketio
pywebview

142
app/start.py Normal file
View 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
View 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);
});

View 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
View 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
View 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
View 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

View File

@@ -0,0 +1 @@
{"file.py": 1757577571.3943994, "fileTransfer.py": 1756117680.0193572}

109
fileTransfer/file.py Normal file
View 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)

View 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

Binary file not shown.

10
fileTransfer/info.json Normal file
View File

@@ -0,0 +1,10 @@
{
"id": "fileTransfer",
"version": 0.007,
"modules": [
{
"id": "fileTransfer",
"version": 0
}
]
}

2
fspn/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
__pycache__
fspn.zip

1
fspn/.mtimes.json Normal file
View 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
View File

@@ -0,0 +1,5 @@
{
"id": "fspn",
"version": 0.007,
"modules": []
}

264
fspn/protocol/connection.py Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1,3 @@
pycryptodome
cryptography
ecdsa

78
fspn/test.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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

View 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
View File

@@ -0,0 +1,2 @@
__pycache__
lockbox.zip

1
lockbox/.mtimes.json Normal file
View 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
View 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"}

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 B

View 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);

View 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"
}
}

File diff suppressed because it is too large Load Diff

6
lockbox/config.json Normal file
View File

@@ -0,0 +1,6 @@
{
"server": {
"host": "https://localhost",
"port": "5001"
}
}

30
lockbox/frontend/.gitignore vendored Normal file
View 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

View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

View 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
View 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-----

View 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
View 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-----

View 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>

View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

27
lockbox/frontend/key.pem Normal file
View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View 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>

View 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

View File

@@ -0,0 +1 @@
@import "tailwindcss";

View 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>

View 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>

View File

@@ -0,0 +1,12 @@
<script setup>
defineOptions({ inheritAttrs: false })
</script>
<template>
<div
class="rounded-xl border"
:class="$attrs.class"
>
<slot></slot>
</div>
</template>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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')

View File

@@ -0,0 +1,3 @@
import { io } from 'socket.io-client'
const socket = io('https://localhost:5001')
export default socket

View 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
}
},
})

View 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>

View 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))
},
},
})

View 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
View 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
View 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