fix: security and reliability improvements
- Switch HTTPServer to ThreadingHTTPServer (concurrent request handling) - Replace SHA-256 password hashing with bcrypt (auth.py, database.py) - Add bcrypt to Dockerfile - Move qBittorrent env vars to config.py - Move _booklore_token state out of config into booklore.py - Remove dead fitness_token variable in command.py - Fix OpenAI call to use default SSL context instead of no-verify ctx - Log swallowed budget fetch error in dashboard.py
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
FROM python:3.12-slim
|
||||
WORKDIR /app
|
||||
RUN pip install bcrypt
|
||||
COPY server.py config.py database.py sessions.py proxy.py responses.py auth.py dashboard.py command.py ./
|
||||
COPY integrations/ ./integrations/
|
||||
EXPOSE 8100
|
||||
|
||||
@@ -2,10 +2,16 @@
|
||||
Platform Gateway — Auth handlers (login, logout, register).
|
||||
"""
|
||||
|
||||
"""
|
||||
NOTE: Passwords are hashed with bcrypt. Any existing SHA-256 hashed passwords
|
||||
in the database will no longer work. The admin user is re-seeded on first boot
|
||||
if no users exist. Other users need manual password reset.
|
||||
"""
|
||||
import json
|
||||
import hashlib
|
||||
import sqlite3
|
||||
|
||||
import bcrypt
|
||||
|
||||
from database import get_db
|
||||
from sessions import create_session, delete_session
|
||||
|
||||
@@ -24,14 +30,12 @@ def handle_login(handler, body):
|
||||
handler._send_json({"error": "Username and password required"}, 400)
|
||||
return
|
||||
|
||||
pw_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||
|
||||
conn = get_db()
|
||||
user = conn.execute("SELECT * FROM users WHERE username = ? AND password_hash = ?",
|
||||
(username, pw_hash)).fetchone()
|
||||
user = conn.execute("SELECT * FROM users WHERE username = ?",
|
||||
(username,)).fetchone()
|
||||
conn.close()
|
||||
|
||||
if not user:
|
||||
if not user or not bcrypt.checkpw(password.encode(), user["password_hash"].encode()):
|
||||
handler._send_json({"error": "Invalid credentials"}, 401)
|
||||
return
|
||||
|
||||
@@ -76,7 +80,7 @@ def handle_register(handler, body):
|
||||
handler._send_json({"error": "Username and password required"}, 400)
|
||||
return
|
||||
|
||||
pw_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||
pw_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
|
||||
|
||||
conn = get_db()
|
||||
try:
|
||||
|
||||
@@ -6,9 +6,7 @@ import json
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
|
||||
from config import (
|
||||
OPENAI_API_KEY, OPENAI_MODEL, TRIPS_URL, _ssl_ctx,
|
||||
)
|
||||
from config import OPENAI_API_KEY, OPENAI_MODEL, TRIPS_URL
|
||||
from sessions import get_service_token
|
||||
import proxy as _proxy_module
|
||||
from proxy import proxy_request
|
||||
@@ -33,7 +31,6 @@ def handle_command(handler, user, body):
|
||||
|
||||
# Get context: user's trips list and today's date
|
||||
trips_token = get_service_token(user["id"], "trips")
|
||||
fitness_token = get_service_token(user["id"], "fitness")
|
||||
|
||||
trips_context = ""
|
||||
if trips_token:
|
||||
@@ -116,7 +113,7 @@ Guidelines:
|
||||
method="POST"
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req, context=_ssl_ctx, timeout=30) as resp:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
ai_result = json.loads(resp.read().decode())
|
||||
ai_text = ai_result["choices"][0]["message"]["content"]
|
||||
|
||||
|
||||
@@ -42,6 +42,12 @@ KINDLE_LABELS = os.environ.get("KINDLE_LABELS", "Kindle 1,Kindle 2")
|
||||
KARAKEEP_URL = os.environ.get("KARAKEEP_URL", "http://192.168.1.42:3005")
|
||||
KARAKEEP_API_KEY = os.environ.get("KARAKEEP_API_KEY", "")
|
||||
|
||||
# ── qBittorrent ──
|
||||
QBITTORRENT_HOST = os.environ.get("QBITTORRENT_HOST", "192.168.1.42")
|
||||
QBITTORRENT_PORT = os.environ.get("QBITTORRENT_PORT", "8080")
|
||||
QBITTORRENT_USERNAME = os.environ.get("QBITTORRENT_USERNAME", "admin")
|
||||
QBITTORRENT_PASSWORD = os.environ.get("QBITTORRENT_PASSWORD", "")
|
||||
|
||||
# ── AI ──
|
||||
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
|
||||
OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-5.2")
|
||||
@@ -49,9 +55,6 @@ OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-5.2")
|
||||
# ── Session config ──
|
||||
SESSION_MAX_AGE = int(os.environ.get("SESSION_MAX_AGE", 30 * 86400)) # 30 days
|
||||
|
||||
# ── Shared state ──
|
||||
_booklore_token = {"access": "", "refresh": "", "expires": 0}
|
||||
|
||||
# ── Ensure data dir exists ──
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ def handle_dashboard(handler, user):
|
||||
data = json.loads(b)
|
||||
return {"count": data.get("transactionCount", 0), "totalBalance": data.get("totalBalanceDollars", 0), "spending": data.get("spendingDollars", 0), "income": data.get("incomeDollars", 0), "topCategories": data.get("topCategories", [])[:5], "month": data.get("month", "")}
|
||||
except Exception as e:
|
||||
pass
|
||||
print(f"[Dashboard] Budget fetch error: {e}")
|
||||
return None
|
||||
|
||||
def fetch_reader(app):
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
Platform Gateway — Database initialization and access.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import sqlite3
|
||||
|
||||
import bcrypt
|
||||
|
||||
from config import (
|
||||
DB_PATH, TRIPS_URL, FITNESS_URL, INVENTORY_URL,
|
||||
MINIFLUX_URL, SHELFMARK_URL, SPOTIZERR_URL, BUDGET_URL,
|
||||
@@ -124,7 +125,7 @@ def init_db():
|
||||
# Seed default admin user if empty
|
||||
user_count = c.execute("SELECT COUNT(*) FROM users").fetchone()[0]
|
||||
if user_count == 0:
|
||||
pw_hash = hashlib.sha256("admin".encode()).hexdigest()
|
||||
pw_hash = bcrypt.hashpw("admin".encode(), bcrypt.gensalt()).decode()
|
||||
c.execute("INSERT INTO users (username, password_hash, display_name) VALUES (?, ?, ?)",
|
||||
("admin", pw_hash, "Yusuf"))
|
||||
conn.commit()
|
||||
|
||||
@@ -5,12 +5,12 @@ Platform Gateway — Booklore integration (book library manager).
|
||||
import json
|
||||
import time
|
||||
|
||||
from config import (
|
||||
BOOKLORE_URL, BOOKLORE_USER, BOOKLORE_PASS,
|
||||
BOOKLORE_BOOKS_DIR, _booklore_token,
|
||||
)
|
||||
from config import BOOKLORE_URL, BOOKLORE_USER, BOOKLORE_PASS, BOOKLORE_BOOKS_DIR
|
||||
from proxy import proxy_request
|
||||
|
||||
# Mutable auth token state (not in config — config is for immutable values)
|
||||
_booklore_token = {"access": "", "refresh": "", "expires": 0}
|
||||
|
||||
|
||||
def booklore_auth():
|
||||
"""Get a valid Booklore JWT token, refreshing if needed."""
|
||||
|
||||
@@ -3,21 +3,18 @@ Platform Gateway — qBittorrent integration (download status).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
from config import QBITTORRENT_HOST, QBITTORRENT_PORT, QBITTORRENT_USERNAME, QBITTORRENT_PASSWORD
|
||||
|
||||
|
||||
def handle_downloads_status(handler):
|
||||
"""Get active downloads from qBittorrent."""
|
||||
qbt_host = os.environ.get("QBITTORRENT_HOST", "192.168.1.42")
|
||||
qbt_port = os.environ.get("QBITTORRENT_PORT", "8080")
|
||||
qbt_user = os.environ.get("QBITTORRENT_USERNAME", "admin")
|
||||
qbt_pass = os.environ.get("QBITTORRENT_PASSWORD", "")
|
||||
base = f"http://{qbt_host}:{qbt_port}"
|
||||
base = f"http://{QBITTORRENT_HOST}:{QBITTORRENT_PORT}"
|
||||
try:
|
||||
# Login
|
||||
login_data = urllib.parse.urlencode({"username": qbt_user, "password": qbt_pass}).encode()
|
||||
login_data = urllib.parse.urlencode({"username": QBITTORRENT_USERNAME, "password": QBITTORRENT_PASSWORD}).encode()
|
||||
req = urllib.request.Request(f"{base}/api/v2/auth/login", data=login_data)
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
cookie = resp.headers.get("Set-Cookie", "").split(";")[0]
|
||||
|
||||
@@ -8,7 +8,9 @@ This file is thin routing only. All logic lives in submodules.
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
from socketserver import ThreadingMixIn
|
||||
from http.server import HTTPServer
|
||||
|
||||
from config import (
|
||||
PORT, TRIPS_API_TOKEN, KINDLE_EMAIL_1, KINDLE_EMAIL_2,
|
||||
@@ -337,7 +339,10 @@ def main():
|
||||
print(f"[Gateway] Services: {_proxy_module.SERVICE_MAP}")
|
||||
print(f"[Gateway] Listening on port {PORT}")
|
||||
|
||||
server = HTTPServer(("0.0.0.0", PORT), GatewayHandler)
|
||||
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
daemon_threads = True
|
||||
|
||||
server = ThreadingHTTPServer(("0.0.0.0", PORT), GatewayHandler)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user