From 62210c7550e79ee06dd6fae355dc995967d20069 Mon Sep 17 00:00:00 2001 From: gsd Date: Tue, 10 Feb 2026 21:20:44 +0300 Subject: [PATCH] auth --- authManager.py | 91 ++++++++++++++++++++++++++++++++ service.py | 20 +++---- utils.py | 18 +++++++ webExtensions/nodeList.py | 13 +++-- webExtensions/publicEndpoints.py | 58 ++++++++++++++++++++ webExtensions/webPing.py | 11 ---- 6 files changed, 186 insertions(+), 25 deletions(-) create mode 100644 authManager.py create mode 100644 utils.py create mode 100644 webExtensions/publicEndpoints.py delete mode 100644 webExtensions/webPing.py diff --git a/authManager.py b/authManager.py new file mode 100644 index 0000000..7b017a6 --- /dev/null +++ b/authManager.py @@ -0,0 +1,91 @@ +from functools import wraps +from fastapi.requests import Request +from fastapi.responses import Response +from fastapi.exceptions import HTTPException + +from utils import md5hash +from random import randint +from time import time + +import asyncio +from logger import logger + +class NotValidCode(Exception): + pass + +class AuthManager: + NUM = "NUM" + NUM_SECURED = "NUM_SECURED" + SECRET_KEY = "SECRET_KEY"#todo + MAX_CODE_LIFE = 60 + + def __init__(self, args): + self.salt = args.web_salt + self.code_store = {} + + async def storeCleaner(self): + logger.info("Code store cleaner working...") + run = True + while run: + try: + l = list(self.code_store.keys()) + for code in l: + if code in self.code_store.keys():#check mb is not exists one time + if time() - self.code_store[code]["ts"] > self.MAX_CODE_LIFE: + logger.info(f"Code {code} is ended") + del self.code_store[code] + await asyncio.sleep(1) + except asyncio.exceptions.CancelledError: + run = False + except: + logger.error("Cannot check code store") + pass + + def authRequest(self, method=[]):#todo cookie or secret_key + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + request: Request = kwargs.get("request", None) + if request is None: + raise HTTPException(status_code=500, detail="Authed endpoint need request arg, but is missing") + + if request.cookies.get(self.NUM, None) and request.cookies.get(self.NUM_SECURED, None): + #check cookie is valid + if md5hash(request.cookies[self.NUM] + self.salt) == request.cookies.get(self.NUM_SECURED): + return await func(*args, **kwargs) + + raise HTTPException(status_code=401) + return wrapper + return decorator + + def setAuth(self, response: Response, num:int, clear = False): + if clear: + response.set_cookie(self.NUM, "") + response.set_cookie(self.NUM_SECURED, "") + else: + response.set_cookie(self.NUM, str(num)) + response.set_cookie(self.NUM_SECURED, md5hash(str(num)+self.salt)) + return response + + def request_auth(self, num: int): + code = randint(1000, 9999) + while code in self.code_store.keys(): + code = randint(1000, 9999) + + self.code_store[code] = { + "code": code, + "num": num, + "ts": time() + } + #logger.info(code) + return code + + def accept_code(self, code:int): + if code in self.code_store.keys(): + num = self.code_store[code]["num"] + del self.code_store[code] + return num + + raise NotValidCode() + + \ No newline at end of file diff --git a/service.py b/service.py index 4738fc6..a22fef9 100644 --- a/service.py +++ b/service.py @@ -24,14 +24,7 @@ from pymongo import AsyncMongoClient #other from botManager import BotManager - -def isInt(any): - try: - int(any) - return True - except: - return False - +from utils import isInt, generate_random_string class MeshArgsParse: def __init__(self, args): @@ -42,6 +35,7 @@ class MeshListener(MeshArgsParse): super().__init__(args) self.meshState = NOT_CONNECTED self.PUB_CH = PUB_CH + self.last_packet_catch = time() if args.transport == "serial": from transport_serial import SerialTransport @@ -62,7 +56,7 @@ class MeshListener(MeshArgsParse): #task async def meshWorker(self, queue: asyncio.Queue): logger.info("Start mesh queue listener") - run = not self.args.disable_mesh + run = not self.args.disable_mesh or self.args.enable_mesh while run: try: await self.device.start() @@ -78,6 +72,7 @@ class MeshListener(MeshArgsParse): from_radio, _ = await self.device.recv() #logger.debug(from_radio) await queue.put(from_radio) + self.last_packet_catch = time() except asyncio.exceptions.CancelledError: logger.info("Kill mesh device") run = False @@ -99,6 +94,8 @@ class MeshApi(MeshArgsParse): super().__init__(args) self.app = FastAPI(lifespan=self.lifespan) self.tasks = [] + from authManager import AuthManager + self.authManager = AuthManager(args) @asynccontextmanager async def lifespan(self, app: FastAPI): @@ -213,6 +210,7 @@ class MeshCenter(MeshListener, MeshApi, MongoDriver, MeshArgsParse): asyncio.create_task(self.bot.handleMessage(from_radio)) self.tasks.append(handlerTask) + self.tasks.append(self.authManager.storeCleaner) def extensionLoader(self, search_paths = []): logger.info("Search fastapiExt") @@ -239,7 +237,8 @@ if __name__ == "__main__": parser = argparse.ArgumentParser() #mesh parser.add_argument("--transport", default="tcp") - parser.add_argument("--disable-mesh", action="store_true", default=False) + parser.add_argument("--disable-mesh", action="store_true", default=True) + parser.add_argument("--enable-mesh", action="store_true", default=False, help="Need to run in docker if git is bullshit updates") #serial transport parser.add_argument("--serial-port", default="/dev/tty.usbmodemD0CF1309DC141") parser.add_argument("--serial-baudrate", default=115200) @@ -251,6 +250,7 @@ if __name__ == "__main__": #fastapi parser.add_argument("--web-host", default="0.0.0.0") parser.add_argument("--web-port", default=8680) + parser.add_argument("--web-salt", default=generate_random_string(32)) #mongodb parser.add_argument("--mongo-url") parser.add_argument("--mongo-host", default="192.168.3.2") diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..46a631e --- /dev/null +++ b/utils.py @@ -0,0 +1,18 @@ +import random +import string +import hashlib + +def generate_random_string(length): + characters = string.ascii_letters + string.digits + random_string = ''.join(random.choices(characters, k=length)) + return random_string + +def isInt(any): + try: + int(any) + return True + except: + return False + +def md5hash(string: str): + return str(hashlib.md5(string.encode()).hexdigest()) \ No newline at end of file diff --git a/webExtensions/nodeList.py b/webExtensions/nodeList.py index 12211d5..37ffb99 100644 --- a/webExtensions/nodeList.py +++ b/webExtensions/nodeList.py @@ -1,5 +1,6 @@ from fastapi import FastAPI from fastapi.responses import HTMLResponse +from fastapi.requests import Request from fastapi import Query from pymongo.asynchronous.database import AsyncDatabase @@ -15,7 +16,8 @@ class WebExtension: self.dbStore = core.dbStore @self.app.get(f"{self.core.context}/nodes/list") - async def listOfNodes(): + @self.core.authManager.authRequest() + async def listOfNodes(request: Request): pipeline = [ #ai slooop # Сортировка для каждого num по убыванию ts {"$sort": {"num": 1, "ts": -1}}, @@ -36,7 +38,8 @@ class WebExtension: return [NodeDTO(node) for node in l] @self.app.get(f"{self.core.context}/nodes/direct") - async def listOfDirectNodes(): + @self.core.authManager.authRequest() + async def listOfDirectNodes(request: Request): pipeline = [ #ai slooop {"$match": {"hops_away": 0}}, # Фильтруем по списку # Сортировка для каждого num по убыванию ts @@ -58,7 +61,8 @@ class WebExtension: return [NodeDTO(node) for node in l] @self.app.get(self.core.context + "/nodes/{num}") - async def oneNode(num: int): + @self.core.authManager.authRequest() + async def oneNode(request: Request, num: int): collection = self.dbStore['node_info'] c = await collection.find_one( {"num":num}, @@ -70,7 +74,8 @@ class WebExtension: raise HTMLResponse(status_code=404) @self.app.get(self.core.context + "/nodes") - async def listOfSelectedNodes(nums: List[int] = Query(None)): + @self.core.authManager.authRequest() + async def listOfSelectedNodes(request: Request, nums: List[int] = Query(None)): pipeline = [ {"$match": {"num": {"$in": nums}}}, # Фильтруем по списку {"$sort": {"num": 1, "ts": -1}}, # Сортируем по num и ts (по убыванию) diff --git a/webExtensions/publicEndpoints.py b/webExtensions/publicEndpoints.py new file mode 100644 index 0000000..34423b7 --- /dev/null +++ b/webExtensions/publicEndpoints.py @@ -0,0 +1,58 @@ +from fastapi import FastAPI +from fastapi.requests import Request +from fastapi.responses import Response, JSONResponse +from fastapi.exceptions import HTTPException + +class WebExtension: + app: FastAPI + def __init__(self, core): + self.core = core + self.app = core.app + + @self.app.get(f"{self.core.context}/ping") + async def pong(request:Request): + return {"pong": True} + + @self.app.get(f"{self.core.context}/status") + async def status(request:Request): + return { + "status": self.core.meshState, + "last_packet_catch": self.core.last_packet_catch + } + + @self.app.get(f"{self.core.context}/auth/code") + async def sendCodeToNode(request:Request, num: int): + #в сообщение одноразовый код и юзер агент кто слал, так-же чтобы не спапили по фингерпринту один код в минуту + if num: + node = await self.getNodeInfo(num) + if node: + code = self.core.authManager.request_auth(num) + await self.core.device.sendMsgToDM(f"Auth code: {code}", num) + return {"status": f"Code sended to {node['long_name']}"} + + raise HTTPException(status_code=400) + + @self.app.get(f"{self.core.context}/auth/code/check") + async def acceptCodeFromNode(request:Request, code: int): + #если такой код есть то авторизуем пользователя, иначе шлем н***й + if code: + try: + num = self.core.authManager.accept_code(code) + respond = self.core.authManager.setAuth(JSONResponse({"status":True}), num) + return respond + except: + return {"status": False} + + return {"status": False} + + @self.app.get(f"{self.core.context}/auth/logout") + async def clearSession(request:Request): + return self.core.authManager.setAuth(JSONResponse({"status":True}), 0, True) + + async def getNodeInfo(self, num:int): + collection = self.core.dbStore['node_info'] + c = await collection.find_one( + {"num":num}, + sort=[("ts", -1)] + ) + return c diff --git a/webExtensions/webPing.py b/webExtensions/webPing.py deleted file mode 100644 index f5785b8..0000000 --- a/webExtensions/webPing.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi import FastAPI - -class WebExtension: - app: FastAPI - def __init__(self, core): - self.core = core - self.app = core.app - - @self.app.get(f"{self.core.context}/ping") - async def pong(): - return {"pong": True}