diff --git a/backend/app.py b/backend/app.py index 753f350..e673214 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,10 +1,7 @@ from base64 import b64decode -from datetime import datetime, timedelta import sqlite3, uvicorn, sys, secrets, re, os, asyncio, httpx, urllib, websockets -from tabnanny import check -from typing import Union -from fastapi import FastAPI, Request, HTTPException, WebSocket, Depends -from pydantic import BaseModel +from fastapi import FastAPI, HTTPException, WebSocket, Depends +from pydantic import BaseModel, BaseSettings from fastapi.responses import FileResponse, StreamingResponse from utils import SQLite, KeyValueStorage, gen_internal_port, ProxyManager, from_name_get_id, STATUS from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm @@ -20,16 +17,22 @@ db = SQLite('db/firegex.db') conf = KeyValueStorage(db) firewall = ProxyManager(db) -JWT_ALGORITHM="HS256" -JWT_SECRET = secrets.token_hex(32) -APP_STATUS = "init" -REACT_BUILD_DIR = "../frontend/build/" if not ON_DOCKER else "frontend/" -REACT_HTML_PATH = os.path.join(REACT_BUILD_DIR,"index.html") + +class Settings(BaseSettings): + JWT_ALGORITHM: str = "HS256" + REACT_BUILD_DIR: str = "../frontend/build/" if not ON_DOCKER else "frontend/" + REACT_HTML_PATH: str = os.path.join(REACT_BUILD_DIR,"index.html") + + +settings = Settings() oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/login", auto_error=False) crypto = CryptContext(schemes=["bcrypt"], deprecated="auto") app = FastAPI(debug=DEBUG) +def APP_STATUS(): return "init" if conf.get("password") is None else "run" +def JWT_SECRET(): return conf.get("secret") + @app.on_event("shutdown") async def shutdown_event(): await firewall.close() @@ -37,50 +40,21 @@ async def shutdown_event(): @app.on_event("startup") async def startup_event(): - global APP_STATUS - db.connect() - db.create_schema({ - 'services': { - 'status': 'VARCHAR(100) NOT NULL', - 'service_id': 'VARCHAR(100) PRIMARY KEY', - 'internal_port': 'INT NOT NULL CHECK(internal_port > 0 and internal_port < 65536) UNIQUE', - 'public_port': 'INT NOT NULL CHECK(internal_port > 0 and internal_port < 65536) UNIQUE', - 'name': 'VARCHAR(100) NOT NULL' - }, - 'regexes': { - 'regex': 'TEXT NOT NULL', - 'mode': 'VARCHAR(1) NOT NULL', - 'service_id': 'VARCHAR(100) NOT NULL', - 'is_blacklist': 'BOOLEAN NOT NULL CHECK (is_blacklist IN (0, 1))', - 'blocked_packets': 'INTEGER UNSIGNED NOT NULL DEFAULT 0', - 'regex_id': 'INTEGER PRIMARY KEY', - 'is_case_sensitive' : 'BOOLEAN NOT NULL CHECK (is_case_sensitive IN (0, 1))', - 'FOREIGN KEY (service_id)':'REFERENCES services (service_id)', - }, - 'keys_values': { - 'key': 'VARCHAR(100) PRIMARY KEY', - 'value': 'VARCHAR(100) NOT NULL', - }, - }) - db.query("CREATE UNIQUE INDEX IF NOT EXISTS unique_regex_service ON regexes (regex,service_id,is_blacklist,mode,is_case_sensitive);") - - if not conf.get("password") is None: - APP_STATUS = "run" - + db.init() + if not JWT_SECRET(): conf.put("secret", secrets.token_hex(32)) await firewall.reload() + def create_access_token(data: dict): - global JWT_SECRET to_encode = data.copy() - encoded_jwt = jwt.encode(to_encode, JWT_SECRET, algorithm=JWT_ALGORITHM) + encoded_jwt = jwt.encode(to_encode, JWT_SECRET(), algorithm=settings.JWT_ALGORITHM) return encoded_jwt async def check_login(token: str = Depends(oauth2_scheme)): - global JWT_SECRET if not token: return False try: - payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) + payload = jwt.decode(token, JWT_SECRET(), algorithms=[settings.JWT_ALGORITHM]) logged_in: bool = payload.get("logged_in") except JWTError: return False @@ -97,9 +71,8 @@ async def is_loggined(auth: bool = Depends(check_login)): @app.get("/api/status") async def get_status(auth: bool = Depends(check_login)): - global APP_STATUS return { - "status":APP_STATUS, + "status": APP_STATUS(), "loggined": auth } @@ -112,29 +85,23 @@ class PasswordChangeForm(BaseModel): @app.post("/api/login") async def login_api(form: OAuth2PasswordRequestForm = Depends()): - global APP_STATUS, JWT_SECRET - - if APP_STATUS != "run": raise HTTPException(status_code=400) - + if APP_STATUS() != "run": raise HTTPException(status_code=400) if form.password == "": return {"status":"Cannot insert an empty password!"} await asyncio.sleep(0.3) # No bruteforce :) if crypto.verify(form.password, conf.get("password")): - print("access granted, good job") return {"access_token": create_access_token({"logged_in": True}), "token_type": "bearer"} raise HTTPException(406,"Wrong password!") @app.post('/api/change-password') async def change_password(form: PasswordChangeForm, auth: bool = Depends(is_loggined)): - - global APP_STATUS, JWT_SECRET - if APP_STATUS != "run": raise HTTPException(status_code=400) + if APP_STATUS() != "run": raise HTTPException(status_code=400) if form.password == "": return {"status":"Cannot insert an empty password!"} if form.expire: - JWT_SECRET = secrets.token_hex(32) + conf.put("secret", secrets.token_hex(32)) hash_psw = crypto.hash(form.password) conf.put("password",hash_psw) @@ -143,14 +110,11 @@ async def change_password(form: PasswordChangeForm, auth: bool = Depends(is_logg @app.post('/api/set-password') async def set_password(form: PasswordForm): - global APP_STATUS, JWT_SECRET - if APP_STATUS != "init": raise HTTPException(status_code=400) + if APP_STATUS() != "init": raise HTTPException(status_code=400) if form.password == "": return {"status":"Cannot insert an empty password!"} - hash_psw = crypto.hash(form.password) conf.put("password",hash_psw) - APP_STATUS = "run" return {"status":"ok", "access_token": create_access_token({"logged_in": True})} @app.get('/api/general-stats') @@ -165,7 +129,6 @@ async def get_general_stats(auth: bool = Depends(is_loggined)): @app.get('/api/services') async def get_services(auth: bool = Depends(is_loggined)): - return db.query(""" SELECT s.service_id `id`, @@ -199,25 +162,21 @@ async def get_service(service_id: str, auth: bool = Depends(is_loggined)): @app.get('/api/service/{service_id}/stop') async def get_service_stop(service_id: str, auth: bool = Depends(is_loggined)): - await firewall.get(service_id).next(STATUS.STOP) return {'status': 'ok'} @app.get('/api/service/{service_id}/pause') async def get_service_pause(service_id: str, auth: bool = Depends(is_loggined)): - await firewall.get(service_id).next(STATUS.PAUSE) return {'status': 'ok'} @app.get('/api/service/{service_id}/start') async def get_service_start(service_id: str, auth: bool = Depends(is_loggined)): - await firewall.get(service_id).next(STATUS.ACTIVE) return {'status': 'ok'} @app.get('/api/service/{service_id}/delete') async def get_service_delete(service_id: str, auth: bool = Depends(is_loggined)): - db.query('DELETE FROM services WHERE service_id = ?;', service_id) db.query('DELETE FROM regexes WHERE service_id = ?;', service_id) await firewall.remove(service_id) @@ -226,7 +185,6 @@ async def get_service_delete(service_id: str, auth: bool = Depends(is_loggined)) @app.get('/api/service/{service_id}/regen-port') async def get_regen_port(service_id: str, auth: bool = Depends(is_loggined)): - db.query('UPDATE services SET internal_port = ? WHERE service_id = ?;', gen_internal_port(db), service_id) await firewall.get(service_id).update_port() return {'status': 'ok'} @@ -234,7 +192,6 @@ async def get_regen_port(service_id: str, auth: bool = Depends(is_loggined)): @app.get('/api/service/{service_id}/regexes') async def get_service_regexes(service_id: str, auth: bool = Depends(is_loggined)): - return db.query(""" SELECT regex, mode, regex_id `id`, service_id, is_blacklist, @@ -244,7 +201,6 @@ async def get_service_regexes(service_id: str, auth: bool = Depends(is_loggined) @app.get('/api/regex/{regex_id}') async def get_regex_id(regex_id: int, auth: bool = Depends(is_loggined)): - res = db.query(""" SELECT regex, mode, regex_id `id`, service_id, is_blacklist, @@ -256,9 +212,7 @@ async def get_regex_id(regex_id: int, auth: bool = Depends(is_loggined)): @app.get('/api/regex/{regex_id}/delete') async def get_regex_delete(regex_id: int, auth: bool = Depends(is_loggined)): - res = db.query('SELECT * FROM regexes WHERE regex_id = ?;', regex_id) - if len(res) != 0: db.query('DELETE FROM regexes WHERE regex_id = ?;', regex_id) await firewall.get(res[0]["service_id"]).update_filters() @@ -274,7 +228,6 @@ class RegexAddForm(BaseModel): @app.post('/api/regexes/add') async def post_regexes_add(form: RegexAddForm, auth: bool = Depends(is_loggined)): - try: re.compile(b64decode(form.regex)) except Exception: @@ -294,7 +247,6 @@ class ServiceAddForm(BaseModel): @app.post('/api/services/add') async def post_services_add(form: ServiceAddForm, auth: bool = Depends(is_loggined)): - serv_id = from_name_get_id(form.name) try: db.query("INSERT INTO services (name, service_id, internal_port, public_port, status) VALUES (?, ?, ?, ?, ?)", @@ -312,9 +264,9 @@ async def frontend_debug_proxy(path): return StreamingResponse(resp.aiter_bytes(),status_code=resp.status_code) async def react_deploy(path): - file_request = os.path.join(REACT_BUILD_DIR, path) + file_request = os.path.join(settings.REACT_BUILD_DIR, path) if not os.path.isfile(file_request): - return FileResponse(REACT_HTML_PATH, media_type='text/html') + return FileResponse(settings.REACT_HTML_PATH, media_type='text/html') else: return FileResponse(file_request) @@ -356,5 +308,5 @@ if __name__ == '__main__': port=int(os.getenv("PORT","4444")), reload=DEBUG, access_log=DEBUG, - workers=2 + workers=1 ) diff --git a/backend/proxy/__init__.py b/backend/proxy/__init__.py index bf782d2..3df3060 100755 --- a/backend/proxy/__init__.py +++ b/backend/proxy/__init__.py @@ -69,7 +69,6 @@ class Proxy: async with self.status_change: if self.isactive(): self.process.kill() - self.process = None return False return True @@ -92,9 +91,7 @@ class Proxy: await self.update_config(filters_codes) def isactive(self): - if self.process and not self.process.returncode is None: - self.process = None - return True if self.process else False + return self.process and self.process.returncode is None async def pause(self): if self.isactive(): diff --git a/backend/proxy/proxy.cpp b/backend/proxy/proxy.cpp index 9987bb3..15b52ac 100644 --- a/backend/proxy/proxy.cpp +++ b/backend/proxy/proxy.cpp @@ -443,7 +443,6 @@ private: }; - int main(int argc, char* argv[]) { if (argc < 5) @@ -500,4 +499,4 @@ int main(int argc, char* argv[]) #endif return 0; -} \ No newline at end of file +} diff --git a/backend/utils.py b/backend/utils.py index 1cd6f0b..aeb57d3 100755 --- a/backend/utils.py +++ b/backend/utils.py @@ -46,6 +46,33 @@ class SQLite(): cur.close() try: self.conn.commit() except Exception: pass + + def init(self): + self.connect() + self.create_schema({ + 'services': { + 'status': 'VARCHAR(100) NOT NULL', + 'service_id': 'VARCHAR(100) PRIMARY KEY', + 'internal_port': 'INT NOT NULL CHECK(internal_port > 0 and internal_port < 65536) UNIQUE', + 'public_port': 'INT NOT NULL CHECK(internal_port > 0 and internal_port < 65536) UNIQUE', + 'name': 'VARCHAR(100) NOT NULL' + }, + 'regexes': { + 'regex': 'TEXT NOT NULL', + 'mode': 'VARCHAR(1) NOT NULL', + 'service_id': 'VARCHAR(100) NOT NULL', + 'is_blacklist': 'BOOLEAN NOT NULL CHECK (is_blacklist IN (0, 1))', + 'blocked_packets': 'INTEGER UNSIGNED NOT NULL DEFAULT 0', + 'regex_id': 'INTEGER PRIMARY KEY', + 'is_case_sensitive' : 'BOOLEAN NOT NULL CHECK (is_case_sensitive IN (0, 1))', + 'FOREIGN KEY (service_id)':'REFERENCES services (service_id)', + }, + 'keys_values': { + 'key': 'VARCHAR(100) PRIMARY KEY', + 'value': 'VARCHAR(100) NOT NULL', + }, + }) + self.query("CREATE UNIQUE INDEX IF NOT EXISTS unique_regex_service ON regexes (regex,service_id,is_blacklist,mode,is_case_sensitive);") class KeyValueStorage: def __init__(self, db): @@ -70,8 +97,7 @@ class STATUS: PAUSE = "pause" ACTIVE = "active" -class ServiceNotFoundException(Exception): - pass +class ServiceNotFoundException(Exception): pass class ServiceManager: def __init__(self, id, db): @@ -83,7 +109,8 @@ class ServiceManager: ) self.status = STATUS.STOP self.filters = {} - self._proxy_update() + self._update_port_from_db() + self._update_filters_from_db() self.lock = asyncio.Lock() self.starter = None @@ -97,11 +124,7 @@ class ServiceManager: if len(res) == 0: raise ServiceNotFoundException() self.proxy.internal_port = res[0]["internal_port"] self.proxy.public_port = res[0]["public_port"] - - def _proxy_update(self): - self._update_port_from_db() - self._update_filters_from_db() - + def _update_filters_from_db(self): res = self.db.query(""" SELECT @@ -133,8 +156,8 @@ class ServiceManager: ) self.proxy.filters = list(self.filters.values()) - def __update_status_db(self, id, status): - self.db.query("UPDATE services SET status = ? WHERE service_id = ?;", status, id) + def __update_status_db(self, status): + self.db.query("UPDATE services SET status = ? WHERE service_id = ?;", status, self.id) async def next(self,to): async with self.lock: @@ -171,7 +194,7 @@ class ServiceManager: def _set_status(self,status): self.status = status - self.__update_status_db(self.id,status) + self.__update_status_db(status) async def update_filters(self): @@ -222,7 +245,10 @@ class ProxyManager: await self.proxy_table[srv_id].next(req_status) def get(self,id): - return self.proxy_table[id] + if id in self.proxy_table: + return self.proxy_table[id] + else: + raise ServiceNotFoundException() def check_port_is_open(port): try: diff --git a/frontend/build/asset-manifest.json b/frontend/build/asset-manifest.json old mode 100644 new mode 100755 index bcf2b47..7672e7d --- a/frontend/build/asset-manifest.json +++ b/frontend/build/asset-manifest.json @@ -1,13 +1,13 @@ { "files": { - "main.css": "/static/css/main.0efd334b.css", - "main.js": "/static/js/main.dbb30aee.js", + "main.css": "/static/css/main.c375ae17.css", + "main.js": "/static/js/main.e6c85636.js", "index.html": "/index.html", - "main.0efd334b.css.map": "/static/css/main.0efd334b.css.map", - "main.dbb30aee.js.map": "/static/js/main.dbb30aee.js.map" + "main.c375ae17.css.map": "/static/css/main.c375ae17.css.map", + "main.e6c85636.js.map": "/static/js/main.e6c85636.js.map" }, "entrypoints": [ - "static/css/main.0efd334b.css", - "static/js/main.dbb30aee.js" + "static/css/main.c375ae17.css", + "static/js/main.e6c85636.js" ] } \ No newline at end of file diff --git a/frontend/build/index.html b/frontend/build/index.html old mode 100644 new mode 100755 index aed712f..103ab3b --- a/frontend/build/index.html +++ b/frontend/build/index.html @@ -1 +1 @@ -
a||125d?(a.sortIndex=c,f(t,a),null===h(r)&&a===h(t)&&(B?(E(L),L=-1):B=!0,K(H,c-d))):(a.sortIndex=e,f(r,a),A||z||(A=!0,I(J)));return a};\nexports.unstable_shouldYield=M;exports.unstable_wrapCallback=function(a){var b=y;return function(){var c=y;y=b;try{return a.apply(this,arguments)}finally{y=c}}};\n","'use strict';\n\nif (process.env.NODE_ENV === 'production') {\n module.exports = require('./cjs/scheduler.production.min.js');\n} else {\n module.exports = require('./cjs/scheduler.development.js');\n}\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = function(module) {\n\tvar getter = module && module.__esModule ?\n\t\tfunction() { return module['default']; } :\n\t\tfunction() { return module; };\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = function(exports, definition) {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }","export default function _arrayLikeToArray(arr, len) {\n if (len == null || len > arr.length) len = arr.length;\n\n for (var i = 0, arr2 = new Array(len); i < len; i++) {\n arr2[i] = arr[i];\n }\n\n return arr2;\n}","import arrayLikeToArray from \"./arrayLikeToArray.js\";\nexport default function _unsupportedIterableToArray(o, minLen) {\n if (!o) return;\n if (typeof o === \"string\") return arrayLikeToArray(o, minLen);\n var n = Object.prototype.toString.call(o).slice(8, -1);\n if (n === \"Object\" && o.constructor) n = o.constructor.name;\n if (n === \"Map\" || n === \"Set\") return Array.from(o);\n if (n === \"Arguments\" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return arrayLikeToArray(o, minLen);\n}","import arrayWithHoles from \"./arrayWithHoles.js\";\nimport iterableToArrayLimit from \"./iterableToArrayLimit.js\";\nimport unsupportedIterableToArray from \"./unsupportedIterableToArray.js\";\nimport nonIterableRest from \"./nonIterableRest.js\";\nexport default function _slicedToArray(arr, i) {\n return arrayWithHoles(arr) || iterableToArrayLimit(arr, i) || unsupportedIterableToArray(arr, i) || nonIterableRest();\n}","export default function _arrayWithHoles(arr) {\n if (Array.isArray(arr)) return arr;\n}","export default function _iterableToArrayLimit(arr, i) {\n var _i = arr == null ? null : typeof Symbol !== \"undefined\" && arr[Symbol.iterator] || arr[\"@@iterator\"];\n\n if (_i == null) return;\n var _arr = [];\n var _n = true;\n var _d = false;\n\n var _s, _e;\n\n try {\n for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) {\n _arr.push(_s.value);\n\n if (i && _arr.length === i) break;\n }\n } catch (err) {\n _d = true;\n _e = err;\n } finally {\n try {\n if (!_n && _i[\"return\"] != null) _i[\"return\"]();\n } finally {\n if (_d) throw _e;\n }\n }\n\n return _arr;\n}","export default function _nonIterableRest() {\n throw new TypeError(\"Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\");\n}","export default function _extends() {\n _extends = Object.assign ? Object.assign.bind() : function (target) {\n for (var i = 1; i < arguments.length; i++) {\n var source = arguments[i];\n\n for (var key in source) {\n if (Object.prototype.hasOwnProperty.call(source, key)) {\n target[key] = source[key];\n }\n }\n }\n\n return target;\n };\n return _extends.apply(this, arguments);\n}","import * as React from \"react\";\nimport type { History, Location } from \"history\";\nimport { Action as NavigationType } from \"history\";\n\nimport type { RouteMatch } from \"./router\";\n\n/**\n * A Navigator is a \"location changer\"; it's how you get to different locations.\n *\n * Every history instance conforms to the Navigator interface, but the\n * distinction is useful primarily when it comes to the low-level