Compare commits

4 Commits

Author SHA1 Message Date
dm
b8e7860970 added session manager, but in local dict 2025-09-28 21:30:17 +03:00
dm
10ce121448 update readme 2025-09-10 14:13:03 +03:00
dm
ba48ff5c1c Merge branch 'security' 2025-09-04 11:47:29 +03:00
dm
1032c08a40 added cache and FileResponse for range requests to seek meia 2025-08-31 19:16:15 +03:00
3 changed files with 88 additions and 14 deletions

View File

@@ -9,8 +9,9 @@ Working mpv
- [x] security, using tokens - [x] security, using tokens
- [x] works in docker - [x] works in docker
- [x] make it use ftp server - [x] make it use ftp server
- [ ] using cookies and temporary tokens
- [ ] deploy on gogs webhooks
- [ ] crypt files - [ ] crypt files
- [ ] make it connect to db
- [ ] multiuser system - [ ] multiuser system
- [ ] files ownhership, owner and groups, roles - [ ] files ownhership, owner and groups, roles
- [ ] archive - [ ] archive

View File

@@ -1,4 +1,4 @@
from fastapi import FastAPI, File, UploadFile, Depends, HTTPException, Security from fastapi import FastAPI, File, UploadFile, Depends, HTTPException, Security, Response, Cookie
from fastapi.responses import FileResponse, PlainTextResponse, StreamingResponse from fastapi.responses import FileResponse, PlainTextResponse, StreamingResponse
from fastapi.security import APIKeyHeader from fastapi.security import APIKeyHeader
from sqlalchemy import exists from sqlalchemy import exists
@@ -6,6 +6,7 @@ import hashlib
from ftplib import FTP from ftplib import FTP
from io import BytesIO from io import BytesIO
from . import db from . import db
from .session_manager import SessionManager
from dotenv import load_dotenv from dotenv import load_dotenv
import os import os
import hmac import hmac
@@ -19,6 +20,7 @@ api_key_header = APIKeyHeader(name="X-API-Key")
FTP_URL = os.getenv("FTP_URL") FTP_URL = os.getenv("FTP_URL")
FTP_LOGIN = os.getenv("FTP_LOGIN") FTP_LOGIN = os.getenv("FTP_LOGIN")
FTP_PASSWORD = os.getenv("FTP_PASSWORD") FTP_PASSWORD = os.getenv("FTP_PASSWORD")
CACHE_DIR = "cache"
def verify_api_key(api_key: str = Security(api_key_header)): def verify_api_key(api_key: str = Security(api_key_header)):
api_key_hashed = hashlib.sha256(api_key.encode()).hexdigest() api_key_hashed = hashlib.sha256(api_key.encode()).hexdigest()
@@ -32,13 +34,40 @@ def compute_hash(data: bytes, algorithm="sha256") -> str:
h.update(data) h.update(data)
return h.hexdigest() return h.hexdigest()
app = FastAPI() def set_cookie(response: Response, name: str, value: str, max_age: int) -> None:
response.set_cookie(
key=name,
value=value,
httponly=True,
secure=True,
samesite="Strict",
max_age=max_age,
)
TOKEN_TTL = 60*15
session_manager = SessionManager(TOKEN_TTL)
app = FastAPI()
@app.get("/") @app.get("/")
async def root(): async def root():
return {"message": "hiii from sfs"} return {"message": "hiii from sfs"}
@app.post("/login")
def login(response: Response):
user_id = "user123"
token = session_manager.create(user_id)
set_cookie(response, "token", token, TOKEN_TTL)
return {"message": "logged in"}
def get_current_user(token: str = Cookie(None)) -> str:
return session_manager.validate(token)
@app.get("/protected")
def protected_route(user: str = Depends(get_current_user)):
return {"message": f"Hello {user}, you are authenticated!"}
@app.post("/file") @app.post("/file")
async def save_file(file: UploadFile = File(...), api_key: str = Depends(verify_api_key)): async def save_file(file: UploadFile = File(...), api_key: str = Depends(verify_api_key)):
contents = await file.read() contents = await file.read()
@@ -61,29 +90,38 @@ async def save_file(file: UploadFile = File(...), api_key: str = Depends(verify_
ftp.storbinary(f"STOR {FILES_DIR}/{file_url}", buffer) ftp.storbinary(f"STOR {FILES_DIR}/{file_url}", buffer)
ftp.quit() ftp.quit()
cached = os.path.join(CACHE_DIR, file_url)
if os.path.exists(cached):
os.remove(cached)
return {"status": "saved", "filename": file_url} return {"status": "saved", "filename": file_url}
except Exception as e: except Exception as e:
db.remove_file(file_url) db.remove_file(file_url)
return {"status": "error", "message": f"Could not save file {file_url}: {e}"} return {"status": "error", "message": f"Could not save file {file_url}: {e}"}
else: else:
return {"status": "file_exists", "filename": existed_url} return {"status": "file_exists", "filename": existed_url}
@app.get("/file/{filename}") @app.get("/file/{filename}")
async def get_file(filename: str, raw: bool = False, api_key: str = Depends(verify_api_key)): async def get_file(filename: str, raw: bool = False, api_key: str = Depends(verify_api_key)):
try: local_path = os.path.join(CACHE_DIR, filename)
ftp = FTP(FTP_URL)
ftp.login(FTP_LOGIN, FTP_PASSWORD) if not os.path.exists(local_path):
buffer = BytesIO() try:
ftp.retrbinary(f"RETR {FILES_DIR}/{filename}", buffer.write) ftp = FTP(FTP_URL)
ftp.quit() ftp.login(FTP_LOGIN, FTP_PASSWORD)
buffer.seek(0) with open(local_path, "wb") as f:
except Exception as e: ftp.retrbinary(f"RETR {FILES_DIR}/{filename}", f.write)
raise HTTPException(status_code=404, detail=f"File not found: {e}") ftp.quit()
except Exception as e:
raise HTTPException(status_code=404, detail=f"File not found: {e}")
if raw: if raw:
return PlainTextResponse(buffer.getvalue().decode("utf-8", errors="ignore")) with open(local_path, "r", encoding="utf-8", errors="ignore") as f:
return PlainTextResponse(f.read())
return StreamingResponse(buffer, media_type="application/octet-stream", headers={"Content-Disposition": f'attachment; filename="{filename}"'}) return FileResponse(local_path, filename=filename)
from ftplib import FTP from ftplib import FTP

35
app/session_manager.py Normal file
View File

@@ -0,0 +1,35 @@
from fastapi import HTTPException
from datetime import datetime, timedelta
import secrets
from typing import Dict, Optional
class SessionManager:
def __init__(self, ttl: int):
self.ttl = ttl
self._tokens: Dict[str, Dict[str, datetime]] = {}
def create(self, user_id: str) -> str:
token = secrets.token_urlsafe(32)
self._tokens[token] = {
"user": user_id,
"expires": datetime.utcnow() + timedelta(seconds=self.ttl),
}
return token
def validate(self, token: Optional[str]) -> str:
self.cleanup()
if not token or token not in self._tokens:
raise HTTPException(status_code=401, detail="Not authenticated")
token_data = self._tokens[token]
if token_data["expires"] < datetime.utcnow():
del self._tokens[token]
raise HTTPException(status_code=401, detail="Session expired")
return token_data["user"]
def cleanup(self) -> None:
now = datetime.utcnow()
expired = [t for t, data in self._tokens.items() if data["expires"] < now]
for t in expired:
del self._tokens[t]