diff --git a/app/auth/auth_handler.py b/app/auth/auth_handler.py index b18df83..cf2794b 100644 --- a/app/auth/auth_handler.py +++ b/app/auth/auth_handler.py @@ -11,7 +11,7 @@ JWT_ALGORITHM = config("algorithm") JWT_EXPIRY_TIME = config("jwt_expiry_time", default=300, cast=int) -def signJWT() -> Dict[str, str]: +def sign_jwt() -> Dict[str, str]: payload = { "user_id": "admin", "expires": int(round(time.time() * 1000) + JWT_EXPIRY_TIME), @@ -45,7 +45,7 @@ def handle_local_cookie(): if enabled: f = open(full_cookie_file_path, "w") - f.write(signJWT()["access_token"]) + f.write(sign_jwt()["access_token"]) f.close() diff --git a/app/external/sse_startlette/LICENSE.md b/app/external/sse_starlette/LICENSE.md similarity index 100% rename from app/external/sse_startlette/LICENSE.md rename to app/external/sse_starlette/LICENSE.md diff --git a/app/external/sse_startlette/__init__.py b/app/external/sse_starlette/__init__.py similarity index 100% rename from app/external/sse_startlette/__init__.py rename to app/external/sse_starlette/__init__.py diff --git a/app/external/sse_startlette/sse_starlette.py b/app/external/sse_starlette/sse_starlette.py similarity index 100% rename from app/external/sse_startlette/sse_starlette.py rename to app/external/sse_starlette/sse_starlette.py diff --git a/app/main.py b/app/main.py index 243d650..2b13eee 100644 --- a/app/main.py +++ b/app/main.py @@ -22,7 +22,7 @@ from app.auth.auth_handler import ( remove_local_cookie, ) from app.external.fastapi_versioning import VersionedFastAPI -from app.external.sse_startlette import EventSourceResponse +from app.external.sse_starlette import EventSourceResponse from app.models.system import APIPlatform from app.repositories.bitcoin import ( register_bitcoin_status_gatherer, diff --git a/app/repositories/system.py b/app/repositories/system.py index 33b5790..ca3ed30 100644 --- a/app/repositories/system.py +++ b/app/repositories/system.py @@ -1,7 +1,7 @@ import asyncio -from os import path import logging import re +from os import path from decouple import config from fastapi import HTTPException, Request, status @@ -39,7 +39,8 @@ def _check_shell_scripts_status(): _check_shell_scripts_status() -async def callScript(scriptPath) -> str: + +async def call_script(scriptPath) -> str: cmd = f"bash {scriptPath}" logging.warning(f"running script: {cmd}") proc = await asyncio.create_subprocess_shell( @@ -54,30 +55,42 @@ async def callScript(scriptPath) -> str: logging.error(stderr.decode()) return "" -def parseKeyValueLines(lines:list) -> dict: + +def parse_key_value_lines(lines: list) -> dict: Dict = {} for line in lines: logging.warning(f"line({line})") - if len(line.strip()) == 0: continue - if line.strip().startswith('#'): continue - if line.find('=') <=0: continue - key, value = line.strip().split('=',1) + if len(line.strip()) == 0: + continue + if line.strip().startswith("#"): + continue + if line.find("=") <= 0: + continue + key, value = line.strip().split("=", 1) Dict[key] = value.strip('"').strip("'") return Dict -def parseKeyValueText(text:str) -> dict: - return parseKeyValueLines(text.splitlines()) -def passwordValid(password : str): - if len(password) < 8: return False - if password.find(' ') >= 0: return False - return re.match('^[\.a-zA-Z0-9-]*$', password) +def parse_key_value_text(text: str) -> dict: + return parse_key_value_lines(text.splitlines()) -def nameValid(password : str): - if len(password) < 3: return False - if password.find(' ') >= 0: return False + +def password_valid(password: str): + if len(password) < 8: + return False + if password.find(" ") >= 0: + return False + return re.match("^[a-zA-Z0-9]*$", password) + + +def name_valid(password : str): + if len(password) < 3: + return False + if password.find(' ') >= 0: + return False return re.match('^[\.a-zA-Z0-9-_]*$', password) + async def get_system_info() -> SystemInfo: try: return await get_system_info_impl() diff --git a/app/repositories/system_impl/raspiblitz.py b/app/repositories/system_impl/raspiblitz.py index 339d5f8..59d2a8a 100644 --- a/app/repositories/system_impl/raspiblitz.py +++ b/app/repositories/system_impl/raspiblitz.py @@ -1,3 +1,8 @@ +import asyncio +import os + +from decouple import config + from app.constants import API_VERSION from app.models.system import ( APIPlatform, @@ -9,6 +14,8 @@ from app.models.system import ( from app.repositories.lightning import get_ln_info from app.utils import redis_get +SHELL_SCRIPT_PATH = config("shell_script_path") + async def get_system_info_impl() -> SystemInfo: lninfo = await get_ln_info() @@ -34,3 +41,26 @@ async def get_system_info_impl() -> SystemInfo: ssh_address=f"admin@{lan}", chain=lninfo.chains[0].network, ) + + +async def shutdown(reboot: bool): + params = "" + if reboot: + params = "reboot" + + script = os.path.join(SHELL_SCRIPT_PATH, "config.scripts", "blitz.shutdown.sh") + cmd = f"sudo bash {script} {params}" + + proc = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + stdout, stderr = await proc.communicate() + + print(f"[{cmd!r} exited with {proc.returncode}]") + if stdout: + print(f"[stdout]\n{stdout.decode()}") + if stderr: + print(f"[stderr]\n{stderr.decode()}") diff --git a/app/routers/apps.py b/app/routers/apps.py index 8335237..3f93e63 100644 --- a/app/routers/apps.py +++ b/app/routers/apps.py @@ -4,7 +4,7 @@ from fastapi.params import Depends import app.repositories.apps as repo import app.routers.apps_docs as docs from app.auth.auth_bearer import JWTBearer -from app.external.sse_startlette import EventSourceResponse +from app.external.sse_starlette import EventSourceResponse _PREFIX = "apps" diff --git a/app/routers/bitcoin.py b/app/routers/bitcoin.py index 64deb44..5775831 100644 --- a/app/routers/bitcoin.py +++ b/app/routers/bitcoin.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, HTTPException, Request, status from fastapi.params import Depends, Query from app.auth.auth_bearer import JWTBearer -from app.external.sse_startlette import EventSourceResponse +from app.external.sse_starlette import EventSourceResponse from app.models.bitcoind import BlockchainInfo, BtcInfo, FeeEstimationMode, NetworkInfo from app.repositories.bitcoin import ( estimate_fee, diff --git a/app/routers/setup.py b/app/routers/setup.py index b697197..0ff624e 100644 --- a/app/routers/setup.py +++ b/app/routers/setup.py @@ -1,5 +1,6 @@ import asyncio -#from asyncio.windows_events import NULL + +# from asyncio.windows_events import NULL import logging from pickle import FALSE import re @@ -10,28 +11,22 @@ from fastapi.params import Depends from fastapi_plugins import depends_redis from setuptools import setup -from app.repositories.system import ( - callScript, - parseKeyValueLines, - nameValid, - passwordValid -) - from app.auth.auth_bearer import JWTBearer -from app.auth.auth_handler import signJWT +from app.auth.auth_handler import sign_jwt +from app.repositories.system import call_script, parse_key_value_lines, password_valid, name_valid, parse_key_value_lines from app.utils import redis_get router = APIRouter(prefix="/setup", tags=["Setup"]) -setupFilePath="/var/cache/raspiblitz/temp/raspiblitz.setup" -configFilePath="/mnt/hdd/raspiblitz.conf" +setupFilePath = "/var/cache/raspiblitz/temp/raspiblitz.setup" +configFilePath = "/mnt/hdd/raspiblitz.conf" # can always be called without credentials to check if # the system needs or is in setup (setupPhase!="done") # for example in the beginning setupPhase can be (see controlSetupDialog.sh) # 1) recovery = same version on fresh sd card # 2) update = updated version on fresh sd card -# 3) migration = hdd got data from another node projcect +# 3) migration = hdd got data from another node project # 4) setup = a fresh blitz to setup @router.get("/status") async def get_status(): @@ -46,6 +41,7 @@ async def get_status(): # We can do the "MIGRATION" option later - because it would need an additional step after formatting hdd # People that need to migrate can do for now by SSH option + @router.get("/setup-start-info") async def setup_start_info(): @@ -53,9 +49,7 @@ async def setup_start_info(): setupPhase = await redis_get("setupPhase") state = await redis_get("state") if state != "waitsetup": - logging.warning( - f"/setup-start-info can only be called when nodes awaits setup" - ) + logging.warning(f"/setup-start-info can only be called when nodes awaits setup") return HTTPException(status.status.HTTP_405_METHOD_NOT_ALLOWED) # get all the additional info needed to do setup dialog @@ -75,10 +69,11 @@ async def setup_start_info(): } -def writeTextFile(filename: str, arrayOfLines): +def write_text_file(filename: str, arrayOfLines): logging.warning(f"writing {filename}") - with open(filename, 'w', encoding='utf-8') as f: - f.write('\n'.join(arrayOfLines)) + with open(filename, "w", encoding="utf-8") as f: + f.write("\n".join(arrayOfLines)) + # With all this info the WebUi can run its own runs its dialogs and in the end makes a call to @router.post("/setup-start-done") @@ -102,18 +97,18 @@ async def setup_start_done( #### SETUP #### if setupPhase == "setup": - if nameValid(hostname) == False: + if name_valid(hostname) == False: logging.warning(f"hostname is not valid") return HTTPException(status.HTTP_400_BAD_REQUEST) if lightning!="lnd" and lightning!="cl" and lightning!="none": logging.warning(f"lightning is not valid") - if passwordValid(passwordA) == False: + if password_valid(passwordA) == False: logging.warning(f"passwordA is not valid") return HTTPException(status.HTTP_400_BAD_REQUEST) - if passwordValid(passwordB) == False: + if password_valid(passwordB) == False: logging.warning(f"passwordB is not valid") return HTTPException(status.HTTP_400_BAD_REQUEST) - if lightning!="none" and passwordValid(passwordC)==False: + if lightning!="none" and password_valid(passwordC)==False: logging.warning(f"passwordC is not valid") return HTTPException(status.HTTP_400_BAD_REQUEST) if hddGotBlockchain!="1" and keepBlockchain: @@ -125,7 +120,7 @@ async def setup_start_done( else: formatHDD=1 cleanHDD=0 - writeTextFile(setupFilePath,[ + write_text_file(setupFilePath,[ f"formatHDD={formatHDD}", f"cleanHDD={cleanHDD}", "network=bitcoin", @@ -143,10 +138,10 @@ async def setup_start_done( #### RECOVERY #### elif setupPhase == "recovery": logging.warning(f"check recovery data") - if passwordValid(passwordA) == False: + if password_valid(passwordA) == False: logging.warning(f"passwordA is not valid") return HTTPException(status.HTTP_400_BAD_REQUEST) - writeTextFile(setupFilePath,[ + write_text_file(setupFilePath,[ "setPasswordA=1", f"passwordA='{passwordA}'" ]) @@ -156,7 +151,7 @@ async def setup_start_done( return HTTPException(status.HTTP_405_METHOD_NOT_ALLOWED) logging.warning(f"kicking off recovery") - await callScript("/home/admin/_cache.sh set state waitprovision") + await call_script("/home/admin/_cache.sh set state waitprovision") # TODO: Following input parameters: # lightning='lnd', 'cl' or 'none' @@ -174,7 +169,8 @@ async def setup_start_done( # those values get stored in: /var/cache/raspiblitz/temp/raspiblitz.setup # also a skeleton raspiblitz.conf gets created (see controlSetupDialog.sh Line 318) # and then API sets state to `waitprovision` to kick-off provision - return signJWT() + + return sign_jwt() # WebUI now loops getting status until state=`waitfinal` then calls: @@ -190,13 +186,15 @@ async def setup_final_info(): setupPhase = await redis_get("setupPhase") state = await redis_get("state") if state != "waitfinal": - logging.warning(f"/setup-final-info can only be called when nodes awaits final ({state})") + logging.warning( + f"/setup-final-info can only be called when nodes awaits final ({state})" + ) return HTTPException(status.HTTP_405_METHOD_NOT_ALLOWED) - resultlines=[] - with open (setupFilePath, "r") as setupfile: - resultlines=setupfile.readlines() - data=parseKeyValueLines(resultlines) + resultlines = [] + with open(setupFilePath, "r") as setupfile: + resultlines = setupfile.readlines() + data = parse_key_value_lines(resultlines) logging.warning(f"data({data})") try: seedwordsNEW = data["seedwordsNEW"] @@ -218,7 +216,5 @@ async def setup_final_done(): logging.warning(f"/setup-final-done can only be called when nodes awaits final") return HTTPException(status.HTTP_405_METHOD_NOT_ALLOWED) - await callScript("/home/admin/_cache.sh set state donefinal") - return { - "state": "donefinal" - } + await call_script("/home/admin/_cache.sh set state donefinal") + return {"state": "donefinal"} diff --git a/app/routers/system.py b/app/routers/system.py index 5168e0d..8bdf686 100644 --- a/app/routers/system.py +++ b/app/routers/system.py @@ -1,24 +1,26 @@ -import secrets import logging +import secrets from decouple import config from fastapi import APIRouter, HTTPException, Request, status from fastapi.params import Depends from app.auth.auth_bearer import JWTBearer -from app.auth.auth_handler import signJWT -from app.external.sse_startlette import EventSourceResponse -from app.models.system import LoginInput, RawDebugLogData, SystemInfo +from app.auth.auth_handler import sign_jwt +from app.external.sse_starlette import EventSourceResponse +from app.models.system import APIPlatform, LoginInput, RawDebugLogData, SystemInfo from app.repositories.system import ( HW_INFO_YIELD_TIME, + PLATFORM, + call_script, get_debug_logs_raw, get_hardware_info, get_system_info, + parse_key_value_text, + password_valid, subscribe_hardware_info, - callScript, - parseKeyValueText, - passwordValid ) +from app.repositories.system_impl.raspiblitz import shutdown from app.routers.system_docs import ( get_debug_logs_raw_desc, get_debug_logs_raw_resp_desc, @@ -48,16 +50,21 @@ async def login(i: LoginInput): if platform == "raspiblitz": # script does not work when called from api yet - if passwordValid(i.password): - result = await callScript(f"/home/admin/config.scripts/blitz.setpassword.sh check-a {i.password}") - data = parseKeyValueText(result) - if data["correct"] == "1": return signJWT() + if password_valid(i.password): + result = await call_script( + f"/home/admin/config.scripts/blitz.setpassword.sh check-a {i.password}" + ) + data = parse_key_value_text(result) + if data["correct"] == "1": + return sign_jwt() raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail="Password is wrong") else: match = secrets.compare_digest(i.password, config("login_password", cast=str)) - if match: return signJWT() + if match: + return sign_jwt() raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail="Password is wrong") + @router.post( "/refresh-token", name=f"{_PREFIX}.refresh-token", @@ -66,7 +73,7 @@ async def login(i: LoginInput): dependencies=[Depends(JWTBearer())], ) def refresh_token(): - return signJWT() + return sign_jwt() @router.get( @@ -132,8 +139,14 @@ async def hw_info_sub(request: Request): summary="Reboots the system", dependencies=[Depends(JWTBearer())], ) -def reboot_system(): - return HTTPException(status.HTTP_501_NOT_IMPLEMENTED) +async def reboot_system() -> bool: + if PLATFORM == APIPlatform.RASPIBLITZ: + await shutdown(True) + return True + else: + raise HTTPException( + status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented on native" + ) @router.post( @@ -142,5 +155,11 @@ def reboot_system(): summary="Shuts the system down", dependencies=[Depends(JWTBearer())], ) -def reboot_system(): - return HTTPException(status.HTTP_501_NOT_IMPLEMENTED) +async def shutdown() -> bool: + if PLATFORM == APIPlatform.RASPIBLITZ: + await shutdown(False) + return True + else: + raise HTTPException( + status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented on native" + )