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
|
FROM python:3.12-slim
|
||||||
WORKDIR /app
|
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 server.py config.py database.py sessions.py proxy.py responses.py auth.py dashboard.py command.py ./
|
||||||
COPY integrations/ ./integrations/
|
COPY integrations/ ./integrations/
|
||||||
EXPOSE 8100
|
EXPOSE 8100
|
||||||
|
|||||||
@@ -2,10 +2,16 @@
|
|||||||
Platform Gateway — Auth handlers (login, logout, register).
|
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 json
|
||||||
import hashlib
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
|
import bcrypt
|
||||||
|
|
||||||
from database import get_db
|
from database import get_db
|
||||||
from sessions import create_session, delete_session
|
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)
|
handler._send_json({"error": "Username and password required"}, 400)
|
||||||
return
|
return
|
||||||
|
|
||||||
pw_hash = hashlib.sha256(password.encode()).hexdigest()
|
|
||||||
|
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
user = conn.execute("SELECT * FROM users WHERE username = ? AND password_hash = ?",
|
user = conn.execute("SELECT * FROM users WHERE username = ?",
|
||||||
(username, pw_hash)).fetchone()
|
(username,)).fetchone()
|
||||||
conn.close()
|
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)
|
handler._send_json({"error": "Invalid credentials"}, 401)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -76,7 +80,7 @@ def handle_register(handler, body):
|
|||||||
handler._send_json({"error": "Username and password required"}, 400)
|
handler._send_json({"error": "Username and password required"}, 400)
|
||||||
return
|
return
|
||||||
|
|
||||||
pw_hash = hashlib.sha256(password.encode()).hexdigest()
|
pw_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
|
||||||
|
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import json
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from config import (
|
from config import OPENAI_API_KEY, OPENAI_MODEL, TRIPS_URL
|
||||||
OPENAI_API_KEY, OPENAI_MODEL, TRIPS_URL, _ssl_ctx,
|
|
||||||
)
|
|
||||||
from sessions import get_service_token
|
from sessions import get_service_token
|
||||||
import proxy as _proxy_module
|
import proxy as _proxy_module
|
||||||
from proxy import proxy_request
|
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
|
# Get context: user's trips list and today's date
|
||||||
trips_token = get_service_token(user["id"], "trips")
|
trips_token = get_service_token(user["id"], "trips")
|
||||||
fitness_token = get_service_token(user["id"], "fitness")
|
|
||||||
|
|
||||||
trips_context = ""
|
trips_context = ""
|
||||||
if trips_token:
|
if trips_token:
|
||||||
@@ -116,7 +113,7 @@ Guidelines:
|
|||||||
method="POST"
|
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_result = json.loads(resp.read().decode())
|
||||||
ai_text = ai_result["choices"][0]["message"]["content"]
|
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_URL = os.environ.get("KARAKEEP_URL", "http://192.168.1.42:3005")
|
||||||
KARAKEEP_API_KEY = os.environ.get("KARAKEEP_API_KEY", "")
|
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 ──
|
# ── AI ──
|
||||||
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
|
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
|
||||||
OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-5.2")
|
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 config ──
|
||||||
SESSION_MAX_AGE = int(os.environ.get("SESSION_MAX_AGE", 30 * 86400)) # 30 days
|
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 ──
|
# ── Ensure data dir exists ──
|
||||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ def handle_dashboard(handler, user):
|
|||||||
data = json.loads(b)
|
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", "")}
|
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:
|
except Exception as e:
|
||||||
pass
|
print(f"[Dashboard] Budget fetch error: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def fetch_reader(app):
|
def fetch_reader(app):
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
Platform Gateway — Database initialization and access.
|
Platform Gateway — Database initialization and access.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
|
import bcrypt
|
||||||
|
|
||||||
from config import (
|
from config import (
|
||||||
DB_PATH, TRIPS_URL, FITNESS_URL, INVENTORY_URL,
|
DB_PATH, TRIPS_URL, FITNESS_URL, INVENTORY_URL,
|
||||||
MINIFLUX_URL, SHELFMARK_URL, SPOTIZERR_URL, BUDGET_URL,
|
MINIFLUX_URL, SHELFMARK_URL, SPOTIZERR_URL, BUDGET_URL,
|
||||||
@@ -124,7 +125,7 @@ def init_db():
|
|||||||
# Seed default admin user if empty
|
# Seed default admin user if empty
|
||||||
user_count = c.execute("SELECT COUNT(*) FROM users").fetchone()[0]
|
user_count = c.execute("SELECT COUNT(*) FROM users").fetchone()[0]
|
||||||
if user_count == 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 (?, ?, ?)",
|
c.execute("INSERT INTO users (username, password_hash, display_name) VALUES (?, ?, ?)",
|
||||||
("admin", pw_hash, "Yusuf"))
|
("admin", pw_hash, "Yusuf"))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ Platform Gateway — Booklore integration (book library manager).
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from config import (
|
from config import BOOKLORE_URL, BOOKLORE_USER, BOOKLORE_PASS, BOOKLORE_BOOKS_DIR
|
||||||
BOOKLORE_URL, BOOKLORE_USER, BOOKLORE_PASS,
|
|
||||||
BOOKLORE_BOOKS_DIR, _booklore_token,
|
|
||||||
)
|
|
||||||
from proxy import proxy_request
|
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():
|
def booklore_auth():
|
||||||
"""Get a valid Booklore JWT token, refreshing if needed."""
|
"""Get a valid Booklore JWT token, refreshing if needed."""
|
||||||
|
|||||||
@@ -3,21 +3,18 @@ Platform Gateway — qBittorrent integration (download status).
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
|
from config import QBITTORRENT_HOST, QBITTORRENT_PORT, QBITTORRENT_USERNAME, QBITTORRENT_PASSWORD
|
||||||
|
|
||||||
|
|
||||||
def handle_downloads_status(handler):
|
def handle_downloads_status(handler):
|
||||||
"""Get active downloads from qBittorrent."""
|
"""Get active downloads from qBittorrent."""
|
||||||
qbt_host = os.environ.get("QBITTORRENT_HOST", "192.168.1.42")
|
base = f"http://{QBITTORRENT_HOST}:{QBITTORRENT_PORT}"
|
||||||
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}"
|
|
||||||
try:
|
try:
|
||||||
# Login
|
# 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)
|
req = urllib.request.Request(f"{base}/api/v2/auth/login", data=login_data)
|
||||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||||
cookie = resp.headers.get("Set-Cookie", "").split(";")[0]
|
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
|
import json
|
||||||
from datetime import datetime
|
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 (
|
from config import (
|
||||||
PORT, TRIPS_API_TOKEN, KINDLE_EMAIL_1, KINDLE_EMAIL_2,
|
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] Services: {_proxy_module.SERVICE_MAP}")
|
||||||
print(f"[Gateway] Listening on port {PORT}")
|
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()
|
server.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user