diff options
| -rw-r--r-- | README | 23 | ||||
| -rw-r--r-- | server.py | 2 | ||||
| -rw-r--r-- | shim_app.py | 96 |
3 files changed, 47 insertions, 74 deletions
@@ -14,21 +14,18 @@ quick start (assumes POSIX) - python3 server.py - open http://127.0.0.1:8585/app -production configuration (gunicorn) +production service (gunicorn) - gunicorn server:app --bind 0.0.0.0:8585 --workers 4 --threads 8 --timeout 60 --graceful-timeout 30 --keep-alive 5 --access-logfile - --error-logfile - config - - SHIM_APP_NAME: ui/app name (default: shim) - - SHIM_BIND: bind address (default: 0.0.0.0) - - SHIM_PORT: port (default: 8585) - - SHIM_MOJICRYPT_BIN: mojicrypt path (default: ./vendor/mojicrypt) - - SHIM_COOKIE_SECURE: auto|true|false (default: auto) - - SHIM_ENFORCE_APP_REQUEST_GUARDS: false by default (set true only when proxy/host headers are correct) - - SHIM_SQLITE_TIMEOUT_SECONDS (default: 30.0) - - SHIM_SQLITE_BUSY_TIMEOUT_MS (default: 30000) - - SHIM_SQLITE_CACHE_SIZE_KIB (default: 32768) - - SHIM_SQLITE_MMAP_SIZE_BYTES (default: 268435456) - - SHIM_SQLITE_WAL_AUTOCHECKPOINT_PAGES (default: 1000) + - edit values directly in shim_app.py constants + - common constants: APP_NAME, BIND_HOST, PORT + - upload/session limits: SESSION_TTL_SECONDS, MAX_UPLOAD_BYTES, MAX_EXTRACTED_BYTES, MAX_EXTRACTED_FILES, MAX_FORM_MEMORY_SIZE + - sqlite tuning: SQLITE_TIMEOUT_SECONDS, SQLITE_BUSY_TIMEOUT_MS, SQLITE_CACHE_SIZE_KIB, SQLITE_MMAP_SIZE_BYTES, SQLITE_WAL_AUTOCHECKPOINT_PAGES + +env vars + - SECRET_KEY: external secret injection for production + - ENFORCE_APP_REQUEST_GUARDS: optional same-origin/csrf enforcement toggle for authenticated mutating /app/ requests security hardening - archive handling is restricted to common archive suffixes and secure extraction checks (no traversal, no symlinks/devices, file count cap, extracted size cap) @@ -42,7 +39,7 @@ security hardening - app shell responses include nonce-based CSP and additional security headers (frame/referrer/permissions/content-type protections) - sqlite hardening includes foreign key enforcement, extension loading disabled, trusted_schema off, and WAL-based runtime tuning - cookie handling uses httponly + samesite for session and active-site cookies, with secure flag applied automatically on https/proxied-https - - SHIM_ENFORCE_APP_REQUEST_GUARDS behavior + - ENFORCE_APP_REQUEST_GUARDS behavior - this is the only security toggle - when true: authenticated mutating requests under /app/ must pass same-origin verification and csrf token verification - when false: those same-origin/csrf request guards are skipped (useful behind some proxy/kubernetes setups) @@ -7,4 +7,4 @@ app = create_app() if __name__ == "__main__": - app.run(host=app.config["SHIM_BIND"], port=app.config["SHIM_PORT"])
\ No newline at end of file + app.run(host=app.config["BIND"], port=app.config["PORT"])
\ No newline at end of file diff --git a/shim_app.py b/shim_app.py index 0e8297b..7723233 100644 --- a/shim_app.py +++ b/shim_app.py @@ -35,12 +35,23 @@ from flask import ( from auth_backend import AuthBackend, LocalMojicryptAuthBackend -SESSION_TTL_SECONDS = int(os.getenv("SHIM_SESSION_TTL_SECONDS", "86400")) -MAX_UPLOAD_BYTES = int(os.getenv("SHIM_MAX_UPLOAD_BYTES", str(1024 * 1024 * 1024))) -MAX_EXTRACTED_BYTES = int( - os.getenv("SHIM_MAX_EXTRACTED_BYTES", str(2 * 1024 * 1024 * 1024)) -) -MAX_EXTRACTED_FILES = int(os.getenv("SHIM_MAX_EXTRACTED_FILES", "20000")) +# config +APP_NAME = "shim" +BIND_HOST = "0.0.0.0" +PORT = 8585 + +SESSION_TTL_SECONDS = 86400 +MAX_UPLOAD_BYTES = 1024 * 1024 * 1024 +MAX_EXTRACTED_BYTES = 2 * 1024 * 1024 * 1024 +MAX_EXTRACTED_FILES = 20000 +MAX_FORM_MEMORY_SIZE = 2 * 1024 * 1024 + +SQLITE_TIMEOUT_SECONDS = 30.0 +SQLITE_BUSY_TIMEOUT_MS = 30000 +SQLITE_CACHE_SIZE_KIB = 32768 +SQLITE_MMAP_SIZE_BYTES = 256 * 1024 * 1024 +SQLITE_WAL_AUTOCHECKPOINT_PAGES = 1000 + SESSION_COOKIE = "shim_session" ACTIVE_SITE_COOKIE = "shim_active_site" MUTATING_METHODS = {"POST", "PUT", "PATCH", "DELETE"} @@ -62,6 +73,7 @@ ROOT_ATTR_RE = re.compile(r"(?i)\b(href|src|action|poster)=([\"'])/([^\"']*)\2") CSS_URL_RE = re.compile(r"(?i)url\(\s*([\"']?)/([^\)'\"\s]+)\1\s*\)") +# template configs SHELL_TEMPLATE = """<!doctype html> <html lang="en"> <head> @@ -389,7 +401,7 @@ DASHBOARD_BODY_TEMPLATE = """ {% endif %} """ - +# code and server logic @dataclass(frozen=True) class AppConfig: base_dir: Path @@ -681,24 +693,6 @@ def find_site_root(extracted_dir: Path) -> Path: return candidates[0].parent -def env_int(name: str, default: int, minimum: int) -> int: - raw = os.getenv(name, str(default)).strip() - try: - value = int(raw) - except ValueError: - value = default - return max(value, minimum) - - -def env_float(name: str, default: float, minimum: float) -> float: - raw = os.getenv(name, str(default)).strip() - try: - value = float(raw) - except ValueError: - value = default - return max(value, minimum) - - def env_bool(name: str, default: bool) -> bool: raw = os.getenv(name, "true" if default else "false").strip().lower() if raw in {"1", "true", "yes", "on"}: @@ -710,14 +704,10 @@ def env_bool(name: str, default: bool) -> bool: def create_app(base_dir: Optional[Path] = None) -> Flask: project_dir = Path(base_dir or Path(__file__).parent).resolve() - app_name = os.getenv("SHIM_APP_NAME", "shim").strip() or "shim" + app_name = APP_NAME db_path = project_dir / "data" / "shim.db" sites_dir = project_dir / "data" / "sites" - default_mojicrypt = project_dir / "vendor" / "mojicrypt" - mojicrypt_env = os.getenv("SHIM_MOJICRYPT_BIN", str(default_mojicrypt)) - mojicrypt_bin = Path(mojicrypt_env).expanduser() - if not mojicrypt_bin.is_absolute(): - mojicrypt_bin = (project_dir / mojicrypt_bin).resolve() + mojicrypt_bin = (project_dir / "vendor" / "mojicrypt").resolve() cfg = AppConfig( base_dir=project_dir, @@ -725,8 +715,8 @@ def create_app(base_dir: Optional[Path] = None) -> Flask: db_path=db_path, sites_dir=sites_dir, mojicrypt_bin=mojicrypt_bin, - bind=os.getenv("SHIM_BIND", "0.0.0.0"), - port=int(os.getenv("SHIM_PORT", "8585")), + bind=BIND_HOST, + port=PORT, ) cfg.db_path.parent.mkdir(parents=True, exist_ok=True) @@ -734,29 +724,19 @@ def create_app(base_dir: Optional[Path] = None) -> Flask: app = Flask(__name__, static_folder=None) app.config["MAX_CONTENT_LENGTH"] = MAX_UPLOAD_BYTES - app.config["MAX_FORM_MEMORY_SIZE"] = int( - os.getenv("SHIM_MAX_FORM_MEMORY_SIZE", str(2 * 1024 * 1024)) - ) - app.config["SECRET_KEY"] = os.getenv("SHIM_SECRET_KEY", secrets.token_hex(32)) - app.config["SHIM_PORT"] = cfg.port - app.config["SHIM_BIND"] = cfg.bind - app.config["SHIM_APP_NAME"] = cfg.app_name - app.config["SHIM_MOJICRYPT_BIN"] = str(cfg.mojicrypt_bin) - - sqlite_timeout_seconds = env_float("SHIM_SQLITE_TIMEOUT_SECONDS", 30.0, 1.0) - sqlite_busy_timeout_ms = env_int("SHIM_SQLITE_BUSY_TIMEOUT_MS", 30000, 1000) - sqlite_cache_size_kib = env_int("SHIM_SQLITE_CACHE_SIZE_KIB", 32768, 4096) - sqlite_mmap_size_bytes = env_int( - "SHIM_SQLITE_MMAP_SIZE_BYTES", 256 * 1024 * 1024, 0 - ) - sqlite_wal_autocheckpoint_pages = env_int( - "SHIM_SQLITE_WAL_AUTOCHECKPOINT_PAGES", 1000, 100 - ) - enforce_app_request_guards = env_bool("SHIM_ENFORCE_APP_REQUEST_GUARDS", False) - - cookie_secure_mode = os.getenv("SHIM_COOKIE_SECURE", "auto").strip().lower() - if cookie_secure_mode not in {"auto", "true", "false"}: - cookie_secure_mode = "auto" + app.config["MAX_FORM_MEMORY_SIZE"] = MAX_FORM_MEMORY_SIZE + app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", secrets.token_hex(32)) + app.config["PORT"] = cfg.port + app.config["BIND"] = cfg.bind + app.config["APP_NAME"] = cfg.app_name + app.config["MOJICRYPT_BIN"] = str(cfg.mojicrypt_bin) + + sqlite_timeout_seconds = SQLITE_TIMEOUT_SECONDS + sqlite_busy_timeout_ms = SQLITE_BUSY_TIMEOUT_MS + sqlite_cache_size_kib = SQLITE_CACHE_SIZE_KIB + sqlite_mmap_size_bytes = SQLITE_MMAP_SIZE_BYTES + sqlite_wal_autocheckpoint_pages = SQLITE_WAL_AUTOCHECKPOINT_PAGES + enforce_app_request_guards = env_bool("ENFORCE_APP_REQUEST_GUARDS", False) def connect_db() -> sqlite3.Connection: conn = sqlite3.connect(str(cfg.db_path), timeout=sqlite_timeout_seconds) @@ -848,10 +828,6 @@ def create_app(base_dir: Optional[Path] = None) -> Flask: ) def cookie_secure_enabled() -> bool: - if cookie_secure_mode == "true": - return True - if cookie_secure_mode == "false": - return False if request.is_secure: return True xfp = request.headers.get("X-Forwarded-Proto", "") |
