From 00c0e073d15ed460669e8ad6c53e41c0441b4770 Mon Sep 17 00:00:00 2001 From: Leopere Date: Tue, 24 Feb 2026 16:16:02 -0500 Subject: [PATCH] WebDAV: fix 500, perms, bind path; drop broken depth middleware - Remove limit_propfind_depth middleware (was breaking response chain) - Entrypoint: mkdir/chmod /data, chmod -R a+rwX, umask 0000 - Compose: bind webdav data to ${HOME}/dev/piconfigurator/bin - Add __pycache__ to .gitignore Co-authored-by: Cursor --- .gitignore | 4 +++ docker-compose-macmini.yml | 10 ++++-- webdav/Dockerfile | 5 +-- webdav/app.py | 62 ++++++++++++++++++++++++++++++++++++-- webdav/entrypoint.sh | 9 ++++++ 5 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 webdav/entrypoint.sh diff --git a/.gitignore b/.gitignore index b4cfa37..7e757a5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,9 @@ macmini-tunnel macmini-tunnel.pub webdav-data/ +# Python +__pycache__/ +*.pyc + # OS .DS_Store diff --git a/docker-compose-macmini.yml b/docker-compose-macmini.yml index 147e8fa..9258826 100644 --- a/docker-compose-macmini.yml +++ b/docker-compose-macmini.yml @@ -2,7 +2,7 @@ # Server (ingress) is assumed configured for this domain and key. # # What works: -# - WebDAV: no auth in app; uploads go to ./webdav-data. Rebuild after app changes: --build. +# - WebDAV: no auth in app; uploads go to ~/dev/piconfigurator/bin. Rebuild after app changes: --build. # - Tunnel: uses ~/.ssh/ca-userkey (same key as all other tunnel clients). # - Auth: TUNNEL_AUTH_USER/PASS (genghis/genghis) = HTTP Basic at the tunnel; WebDAV behind it is open. # - network_mode: service:webdav so tunnel forwards to localhost:80 inside the webdav container. @@ -11,8 +11,14 @@ services: webdav: build: ./webdav restart: always + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:80/')"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s volumes: - - ./webdav-data:/data + - ${HOME}/dev/piconfigurator/bin:/data tunnel-client: image: git.nixc.us/colin/better-argo-tunnels:client-production-arm64 diff --git a/webdav/Dockerfile b/webdav/Dockerfile index 855bcef..1fa2612 100644 --- a/webdav/Dockerfile +++ b/webdav/Dockerfile @@ -4,7 +4,8 @@ WORKDIR /app RUN pip install --no-cache-dir wsgidav cheroot COPY app.py . -RUN mkdir -p /data +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh EXPOSE 80 -CMD ["python", "-u", "app.py"] +ENTRYPOINT ["/entrypoint.sh"] diff --git a/webdav/app.py b/webdav/app.py index 88aabc5..6f09320 100644 --- a/webdav/app.py +++ b/webdav/app.py @@ -1,25 +1,83 @@ -"""Minimal WebDAV server. Serves /data for uploads. Auth handled at reverse tunnel.""" +"""Minimal WebDAV server. Serves /data for uploads. Auth at reverse tunnel; app allows anonymous.""" +import os +import sys +import traceback + from wsgidav.fs_dav_provider import FilesystemProvider from wsgidav.wsgidav_app import WsgiDAVApp from wsgidav.dir_browser import WsgiDavDirBrowser from wsgidav.error_printer import ErrorPrinter from wsgidav.request_resolver import RequestResolver +from wsgidav.http_authenticator import HTTPAuthenticator from wsgidav.mw.cors import Cors from cheroot.wsgi import Server as WSGIServer ROOT = "/data" PORT = 80 +def ensure_data_dir_writable() -> None: + os.makedirs(ROOT, exist_ok=True) + probe = os.path.join(ROOT, ".write_probe") + try: + with open(probe, "w") as f: + f.write("") + os.remove(probe) + except OSError as e: + print(f"FATAL: cannot write to {ROOT}: {e}", file=sys.stderr) + sys.exit(1) + + +def catch_all(app): + """Catch uncaught exceptions so one bad request doesn't kill the server.""" + + def wrapper(environ, start_response): + try: + return app(environ, start_response) + except Exception: + traceback.print_exc() + start_response("500 Internal Server Error", [("Content-Type", "text/plain")]) + return [b"Internal Server Error"] + + return wrapper + + +ensure_data_dir_writable() config = { "host": "0.0.0.0", "port": PORT, "provider_mapping": {"/": FilesystemProvider(ROOT, readonly=False)}, - "middleware_stack": [Cors, ErrorPrinter, WsgiDavDirBrowser, RequestResolver], + "middleware_stack": [ + Cors, + ErrorPrinter, + HTTPAuthenticator, + WsgiDavDirBrowser, + RequestResolver, + ], + "http_authenticator": { + "domain_controller": None, + "accept_basic": True, + "accept_digest": True, + "default_to_digest": False, + }, + "simple_dc": { + "user_mapping": {"*": True}, + }, + "hotfixes": { + "emulate_win32_lastmod": True, + "treat_root_options_as_asterisk": True, + }, + "add_header_MS_Author_Via": True, "verbose": 3, } app = WsgiDAVApp(config) +app = catch_all(app) if __name__ == "__main__": server = WSGIServer((config["host"], config["port"]), app) + # Avoid Finder/hung clients holding connections forever + if hasattr(server, "connection_limit"): + server.connection_limit = 20 + if hasattr(server, "timeout"): + server.timeout = 30 server.start() diff --git a/webdav/entrypoint.sh b/webdav/entrypoint.sh new file mode 100644 index 0000000..c8f309f --- /dev/null +++ b/webdav/entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -e +mkdir -p /data +chmod 1777 /data +# Fix perms on existing content so host user can read/write +chmod -R a+rwX /data 2>/dev/null || true +# New files/dirs world-readable/writable so host user can manage them +umask 0000 +exec python -u app.py