From 23e71843403a0dae84d839ce278c9520737378ec Mon Sep 17 00:00:00 2001 From: Orsiris de Jong Date: Tue, 31 Jan 2023 21:04:04 +0100 Subject: [PATCH] Upgrade server - initial work --- upgrade_server/requrements.txt | 4 + upgrade_server/upgrade_server.py | 66 +++++++++++ upgrade_server/upgrade_server/__init__.py | 0 upgrade_server/upgrade_server/api.py | 104 ++++++++++++++++++ .../upgrade_server/configuration.py | 39 +++++++ upgrade_server/upgrade_server/crud.py | 54 +++++++++ upgrade_server/upgrade_server/models/files.py | 39 +++++++ 7 files changed, 306 insertions(+) create mode 100644 upgrade_server/requrements.txt create mode 100644 upgrade_server/upgrade_server.py create mode 100644 upgrade_server/upgrade_server/__init__.py create mode 100644 upgrade_server/upgrade_server/api.py create mode 100644 upgrade_server/upgrade_server/configuration.py create mode 100644 upgrade_server/upgrade_server/crud.py create mode 100644 upgrade_server/upgrade_server/models/files.py diff --git a/upgrade_server/requrements.txt b/upgrade_server/requrements.txt new file mode 100644 index 0000000..5bda27a --- /dev/null +++ b/upgrade_server/requrements.txt @@ -0,0 +1,4 @@ +fastapi +fastapi-offline>=1.5.0 +uvicorn +pydantic diff --git a/upgrade_server/upgrade_server.py b/upgrade_server/upgrade_server.py new file mode 100644 index 0000000..7ff2be2 --- /dev/null +++ b/upgrade_server/upgrade_server.py @@ -0,0 +1,66 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# +# This file is part of npbackup + +__intname__ = "npbackup.upgrade_server.upgrade_server" +__author__ = "Orsiris de Jong" +__copyright__ = "Copyright (C) 2023 NetInvent" +__license__ = "GPL-3.0-only" +__build__ = "202303101" +__version__ = "0.0.1" + + +DEVEL=True + +import sys +import os +from upgrade_server import configuration +from ofunctions.logger_utils import logger_get_logger + + +#ROOT_DIR = os.path.abspath(os.path.dirname(__file__)) +#config_file = 'upgrade_server.conf' +#config_dict = configuration.load_config(os.path.join(ROOT_DIR, config_file) + +config_dict = configuration.load_config() +try: + listen = config_dict['http_server']['listen'] +except KeyError: + listen = None +try: + port = config_dict['http_server']['port'] +except KeyError: + listen = None + +if DEVEL: + import uvicorn as server + server_args = { + 'workers': 1, + 'log_level': "debug", + 'reload': True, + 'host': listen if listen else '0.0.0.0', + 'port': port if port else 8080 + } +else: + import gunicorn as server + server_args = { + 'workers': 8, + 'reload': False, + 'host': listen if listen else '0.0.0.0', + 'port': port if port else 8080 + } + +logger = logger_get_logger() + +if __name__ == "__main__": + try: + + server.run("upgrade_server.api:app", **server_args) + except KeyboardInterrupt as exc: + logger.error("Program interrupted by keyoard: {}".format(exc)) + sys.exit(200) + except Exception as exc: + logger.error("Program interrupted by error: {}".format(exc)) + logger.critical('Trace:', exc_info=True) + sys.exit(201) \ No newline at end of file diff --git a/upgrade_server/upgrade_server/__init__.py b/upgrade_server/upgrade_server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/upgrade_server/upgrade_server/api.py b/upgrade_server/upgrade_server/api.py new file mode 100644 index 0000000..2974ef3 --- /dev/null +++ b/upgrade_server/upgrade_server/api.py @@ -0,0 +1,104 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + +__appname__ = "npbackup.upgrader" +__author__ = "Alan Smithee" +__build__ = "2022112201" +__version__ = "1.0-beta" + +from typing import Literal +import logging +import secrets +from fastapi import FastAPI, HTTPException, Response, Depends, status +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from fastapi_offline import FastAPIOffline +from upgrade_server.models.files import FileGet, FileSend, Platform, Arch +import upgrade_server.crud as crud +import upgrade_server.configuration as configuration + + +config_dict = configuration.load_config() +logger = logging.getLogger() + +#### Create app +#app = FastAPI() # standard FastAPI initialization +app = FastAPIOffline() # Offline FastAPI initialization, allows /docs to not use online CDN +security = HTTPBasic() + + +def get_current_username(credentials: HTTPBasicCredentials = Depends(security)): + current_username_bytes = credentials.username.encode("utf8") + correct_username_bytes = config_dict['http_server']['username'].encode('utf-8') + is_correct_username = secrets.compare_digest( + current_username_bytes, correct_username_bytes + ) + current_password_bytes = credentials.password.encode("utf8") + correct_password_bytes = config_dict['http_server']['password'].encode('utf-8') + is_correct_password = secrets.compare_digest( + current_password_bytes, correct_password_bytes + ) + if not (is_correct_username and is_correct_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Basic"}, + ) + return credentials.username + + +@app.get("/") +async def api_root(): + if crud.is_enabled(): + return { + "app": __appname__, + } + else: + return { + "app": "Currently under maintenance" + } + + +@app.get("/upgrades/{platform}/{arch}", response_model=FileSend, status_code=200) +async def upgrades(platform: Platform, arch: Arch, auth = Depends(get_current_username)): + + file = FileGet(platform=platform, arch=arch) + try: + result = crud.get_file(file) + if not result: + raise HTTPException( + status_code=404, + detail="Not found" + ) + return result + except HTTPException: + raise + except Exception as exc: + logger.debug("Cannot get file: {}".format(exc), exc_info=True) + raise HTTPException( + status_code=400, + detail="Cannot get file: {}".format(exc), + ) + +@app.get("/upgrades/{platform}/{arch}/data", status_code=200) +async def download(platform: Platform, arch: Arch, auth = Depends(get_current_username)): + file = FileGet(platform=platform, arch=arch) + try: + result = crud.get_file(file, content=True) + if not result: + raise HTTPException( + status_code=404, + detail="Not found" + ) + headers = { + "Content-Disposition": 'attachment; filename="npbackup"' + } + return Response(content=result, media_type="application/dat", headers=headers) + except HTTPException: + raise + except Exception as exc: + logger.debug("Cannot get file: {}".format(exc), exc_info=True) + raise HTTPException( + status_code=400, + detail="Cannot get file: {}".format(exc), + ) + diff --git a/upgrade_server/upgrade_server/configuration.py b/upgrade_server/upgrade_server/configuration.py new file mode 100644 index 0000000..05d08b2 --- /dev/null +++ b/upgrade_server/upgrade_server/configuration.py @@ -0,0 +1,39 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# +# This file is part of npbackup + +__intname__ = "npbackup.upgrade_server.configuration" +__author__ = "Orsiris de Jong" +__copyright__ = "Copyright (C) 2023 NetInvent" +__license__ = "GPL-3.0-only" +__build__ = "202303101" +__version__ = "0.0.1" + +import os +from ruamel.yaml import YAML +from logging import getLogger + + +ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) +config_file = "upgrade_server.conf" +default_config_path = os.path.join(ROOT_DIR, config_file) +logger = getLogger(__intname__) + + +def load_config(config_file: str = default_config_path): + """ + Using ruamel.yaml preserves comments and order of yaml files + """ + logger.debug("Using configuration file {}".format(config_file)) + with open(config_file, "r", encoding="utf-8") as file_handle: + # RoundTrip loader is default and preserves comments and ordering + yaml = YAML(typ="rt") + config_dict = yaml.load(file_handle) + return config_dict + + +def save_config(config_file, config_dict): + with open(config_file, "w", encoding="utf-8") as file_handle: + yaml = YAML(typ="rt") + yaml.dump(config_dict, file_handle) diff --git a/upgrade_server/upgrade_server/crud.py b/upgrade_server/upgrade_server/crud.py new file mode 100644 index 0000000..8262e49 --- /dev/null +++ b/upgrade_server/upgrade_server/crud.py @@ -0,0 +1,54 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# +# This file is part of npbackup + +__intname__ = "npbackup.upgrade_server.crud" +__author__ = "Orsiris de Jong" +__copyright__ = "Copyright (C) 2023 NetInvent" +__license__ = "GPL-3.0-only" +__build__ = "202303101" +__version__ = "0.0.1" + +import os +from typing import Optional, Union +from logging import getLogger +import hashlib +from upgrade_server.models.files import FileGet, FileSend +import upgrade_server.configuration as configuration + + +config_dict = configuration.load_config() + +logger = getLogger(__intname__) + +def sha256sum_data(data): + # type: (bytes) -> str + """ + Returns sha256sum of some data + """ + sha256 = hashlib.sha256() + sha256.update(data) + return sha256.hexdigest() + + +def is_enabled() -> bool: + return not os.path.isfile("DISABLED") + + +def get_file(file: FileGet, content: bool = False) -> Optional[Union[FileSend, bytes]]: + possible_filename = 'npbackup{}'.format( + '.exe' if file.platform.value == 'windows' else '' + ) + path = os.path.join(config_dict['upgrades']['data_root'], file.platform.value, file.arch.value, possible_filename) + logger.info("Searching for %s", path) + if not os.path.isfile(path): + return None + with open(path, 'rb') as fh: + bytes = fh.read() + if content: + return bytes + length = len(bytes) + sha256 = sha256sum_data(bytes) + file_send = FileSend(arch=file.arch.value, platform=file.platform.value, sha256sum=sha256, filename=possible_filename, file_length=length) + return file_send \ No newline at end of file diff --git a/upgrade_server/upgrade_server/models/files.py b/upgrade_server/upgrade_server/models/files.py new file mode 100644 index 0000000..ac1e65d --- /dev/null +++ b/upgrade_server/upgrade_server/models/files.py @@ -0,0 +1,39 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# +# This file is part of npbackup + +__intname__ = "npbackup.upgrade_server.models.files" +__author__ = "Orsiris de Jong" +__copyright__ = "Copyright (C) 2023 NetInvent" +__license__ = "GPL-3.0-only" +__build__ = "202303101" +__version__ = "0.0.1" + + +from enum import Enum +from pydantic import BaseModel, constr + + +class Platform(Enum): + windows = "windows" + linux = "Linux" + + +class Arch(Enum): + x86 = "x86" + x64 = "x64" + + +class FileBase(BaseModel): + arch: Arch + platform: Platform + +class FileGet(FileBase): + pass + +class FileSend(FileBase): + sha256sum: constr(min_length=64, max_length=64) + filename: str + file_length: int +