From e320a1bdbe56e3a3e852da4e77ae5d5bd0c771ea Mon Sep 17 00:00:00 2001 From: profitroll Date: Mon, 16 Jan 2023 13:23:18 +0100 Subject: [PATCH] Initial --- config_example.json | 16 ++++++++ data/api_keys.json | 1 + data/expired_keys.json | 1 + favicon.ico | Bin 0 -> 30015 bytes modules/app.py | 75 +++++++++++++++++++++++++++++++++++ modules/extensions_loader.py | 47 ++++++++++++++++++++++ modules/utils.py | 61 ++++++++++++++++++++++++++++ requirements.txt | 4 ++ sync_api.py | 25 ++++++++++++ 9 files changed, 230 insertions(+) create mode 100644 config_example.json create mode 100644 data/api_keys.json create mode 100644 data/expired_keys.json create mode 100644 favicon.ico create mode 100644 modules/app.py create mode 100644 modules/extensions_loader.py create mode 100644 modules/utils.py create mode 100644 requirements.txt create mode 100644 sync_api.py diff --git a/config_example.json b/config_example.json new file mode 100644 index 0000000..049814b --- /dev/null +++ b/config_example.json @@ -0,0 +1,16 @@ +{ + "locations": { + "data": "data" + }, + "messages": { + "key_expired": "API key expired", + "key_invalid": "Invalid API key", + "key_valid": "Valid API key", + "bad_request": "Bad request. Read the docs at photos.end-play.xyz/docs", + "ip_blacklisted": "Your IP is blacklisted. Make sure you are using correct API address.", + "credentials_invalid": "Incorrect user or password", + "user_already_exists": "User with this username already exists.", + "email_confirmed": "Email confirmed. You can now log in.", + "email_code_invalid": "Confirmation code is invalid." + } +} \ No newline at end of file diff --git a/data/api_keys.json b/data/api_keys.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/data/api_keys.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/data/expired_keys.json b/data/expired_keys.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/data/expired_keys.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..ecea69336a25868b30c5b52cfc2f5400ba475613 GIT binary patch literal 30015 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 literal 0 HcmV?d00001 diff --git a/modules/app.py b/modules/app.py new file mode 100644 index 0000000..02184d0 --- /dev/null +++ b/modules/app.py @@ -0,0 +1,75 @@ +from os import path +from fastapi import FastAPI, Security, HTTPException +from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN +from fastapi.security import APIKeyQuery, APIKeyHeader, APIKeyCookie +from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html +from starlette.status import HTTP_401_UNAUTHORIZED + +from modules.utils import configGet, jsonLoad + +app = FastAPI(title="Stardew Sync", docs_url=None, redoc_url=None, version="0.1") + +api_key_query = APIKeyQuery(name="apikey", auto_error=False) +api_key_header = APIKeyHeader(name="apikey", auto_error=False) +api_key_cookie = APIKeyCookie(name="apikey", auto_error=False) + + +def get_all_api_keys() -> list: + return jsonLoad(path.join(configGet("data", "locations"), "api_keys.json")) + +def get_all_expired_keys() -> list: + return jsonLoad(path.join(configGet("data", "locations"), "expired_keys.json")) + +# def check_project_key(project: str, apikey: APIKey) -> bool: +# keys = jsonLoad(path.join(configGet("data", "locations"), "api_keys.json")) +# if apikey in keys: +# if keys[apikey] != []: +# if project in keys[apikey]: +# return True +# else: +# return False +# else: +# return False +# else: +# return False + + +async def get_api_key( + api_key_query: str = Security(api_key_query), + api_key_header: str = Security(api_key_header), + api_key_cookie: str = Security(api_key_cookie), +) -> str: + + keys = get_all_api_keys() + expired = get_all_expired_keys() + + def is_valid(key): + return True if key in keys else False + + if is_valid(api_key_query): + return api_key_query + elif is_valid(api_key_header): + return api_key_header + elif is_valid(api_key_cookie): + return api_key_cookie + else: + if (api_key_query in expired) or (api_key_header in expired) or (api_key_cookie in expired): + raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail=configGet("key_expired", "messages")) + else: + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=configGet("key_invalid", "messages")) + +@app.get("/docs", include_in_schema=False) +async def custom_swagger_ui_html(): + return get_swagger_ui_html( + openapi_url=app.openapi_url, # type: ignore + title=app.title + " - Documentation", + swagger_favicon_url="/favicon.ico" + ) + +@app.get("/redoc", include_in_schema=False) +async def custom_redoc_html(): + return get_redoc_html( + openapi_url=app.openapi_url, # type: ignore + title=app.title + " - Documentation", + redoc_favicon_url="/favicon.ico" + ) \ No newline at end of file diff --git a/modules/extensions_loader.py b/modules/extensions_loader.py new file mode 100644 index 0000000..49f8159 --- /dev/null +++ b/modules/extensions_loader.py @@ -0,0 +1,47 @@ +from importlib.util import module_from_spec, spec_from_file_location +from os import getcwd, path, walk + +#================================================================================= + +# Import functions +# Took from https://stackoverflow.com/a/57892961 +def get_py_files(src): + cwd = getcwd() + py_files = [] + for root, dirs, files in walk(src): + for file in files: + if file.endswith(".py"): + py_files.append(path.join(cwd, root, file)) + return py_files + + +def dynamic_import(module_name, py_path): + try: + module_spec = spec_from_file_location(module_name, py_path) + module = module_from_spec(module_spec) # type: ignore + module_spec.loader.exec_module(module) # type: ignore + return module + except SyntaxError: + print(f"Could not load extension {module_name} due to invalid syntax. Check logs/errors.log for details.", flush=True) + return + except Exception as exp: + print(f"Could not load extension {module_name} due to {exp}", flush=True) + return + + +def dynamic_import_from_src(src, star_import = False): + my_py_files = get_py_files(src) + for py_file in my_py_files: + module_name = path.split(py_file)[-1][:-3] + print(f"Importing {module_name} extension...", flush=True) + imported_module = dynamic_import(module_name, py_file) + if imported_module != None: + if star_import: + for obj in dir(imported_module): + globals()[obj] = imported_module.__dict__[obj] + else: + globals()[module_name] = imported_module + print(f"Successfully loaded {module_name} extension", flush=True) + return + +#================================================================================= \ No newline at end of file diff --git a/modules/utils.py b/modules/utils.py new file mode 100644 index 0000000..e50fcf9 --- /dev/null +++ b/modules/utils.py @@ -0,0 +1,61 @@ +from typing import Any, Union +from ujson import loads, dumps, JSONDecodeError +from traceback import print_exc + +# Print to stdout and then to log +def logWrite(message: str, debug: bool = False) -> None: + # save to log file and rotation is to be done + # logAppend(f'{message}', debug=debug) + print(f"{message}", flush=True) + +def jsonLoad(filepath: str) -> Any: + """Load json file + + ### Args: + * filepath (`str`): Path to input file + + ### Returns: + * `Any`: Some json deserializable + """ + with open(filepath, "r", encoding='utf8') as file: + try: + output = loads(file.read()) + except JSONDecodeError: + logWrite(f"Could not load json file {filepath}: file seems to be incorrect!\n{print_exc()}") + raise + except FileNotFoundError: + logWrite(f"Could not load json file {filepath}: file does not seem to exist!\n{print_exc()}") + raise + file.close() + return output + +def jsonSave(contents: Union[list, dict], filepath: str) -> None: + """Save contents into json file + + ### Args: + * contents (`Union[list, dict]`): Some json serializable + * filepath (`str`): Path to output file + """ + try: + with open(filepath, "w", encoding='utf8') as file: + file.write(dumps(contents, ensure_ascii=False, indent=4)) + file.close() + except Exception as exp: + logWrite(f"Could not save json file {filepath}: {exp}\n{print_exc()}") + return + +def configGet(key: str, *args: str) -> Any: + """Get value of the config key + + ### Args: + * key (`str`): The last key of the keys path. + * *args (`str`): Path to key like: dict[args][key]. + + ### Returns: + * `Any`: Value of provided key + """ + this_dict = jsonLoad("config.json") + this_key = this_dict + for dict_key in args: + this_key = this_key[dict_key] + return this_key[key] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..076a6cd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +fastapi[all] +ujson~=5.7.0 +pydantic~=1.10.4 +apscheduler~=3.9.1.post1 \ No newline at end of file diff --git a/sync_api.py b/sync_api.py new file mode 100644 index 0000000..9a65c4a --- /dev/null +++ b/sync_api.py @@ -0,0 +1,25 @@ +from os import makedirs, path +from modules.app import app +from modules.utils import configGet +from modules.extensions_loader import dynamic_import_from_src +from fastapi.responses import FileResponse + +makedirs(configGet("data", "locations"), exist_ok=True) + +for entry in [path.join(configGet("data", "locations"), "api_keys.json"), path.join(configGet("data", "locations"), "expired_keys.json")]: + mode = 'r' if path.exists(entry) else 'w' + with open(entry, mode) as f: + try: + f.write("[]") + except: + pass + + +@app.get("/favicon.ico", response_class=FileResponse, include_in_schema=False) +async def favicon(): + return FileResponse("favicon.ico") + + +#================================================================================= +dynamic_import_from_src("extensions", star_import = True) +#================================================================================= \ No newline at end of file