A Docker-first, Linux-native gateway that exposes O2 Cloud and Movistar Cloud (Telefónica personal cloud storage services) as a standard WebDAV share, plus a small web admin panel for authentication and status.
Point any WebDAV client — Finder, Windows Explorer, rclone, Nautilus, mobile
apps — at the gateway and browse, upload, download, move, delete and lock your
O2/Movistar Cloud files as if they were a normal network drive. The gateway handles
each provider's session cookies, validation keys, upload/download quirks and metadata
caching behind the scenes.
- WebDAV endpoint:
/dav - Admin panel:
/admin - Interactive O2/Movistar login: a real Chromium session running inside the
container,
streamed to your browser over noVNC (default port
6080).
- How it works
- Requirements
- Quick start (Docker)
- Logging in to O2/Movistar (interactive VNC login)
- Manual session import
- Using the WebDAV share
- Simulated mode (no real cloud account)
- Secrets
- Environment variables
- Ports
- Local development
- Project layout
WebDAV client ──HTTP──> :8088 /dav ─┐
├─> o2gateway (FastAPI / ASGI) ─> O2/Movistar Cloud API
Browser ──────HTTP──> :8088 /admin ─┘ │
Browser ──────HTTP──> :6080 noVNC ────────────┘ (interactive login only)
The gateway is a single FastAPI/ASGI application (served by Uvicorn) that:
- Serves a WebDAV router at
/dav(PROPFIND, GET, PUT, DELETE, MKCOL, MOVE, COPY, LOCK/UNLOCK) backed by a pluggable cloud provider. - Serves an admin panel at
/adminfor O2/Movistar login, session status and logout. - Translates WebDAV operations into O2/Movistar Cloud API calls, caching metadata in a local SQLite database to keep listings fast.
- Stores the selected provider's session (cookies + validation key) encrypted on disk.
The container also runs a headless X server (Xvfb) + fluxbox + x11vnc +
websockify/noVNC so the interactive Chromium login can be driven from your
own browser — see below.
- Docker and Docker Compose (the supported path — bundles Chromium, Playwright and the VNC stack).
- A modern browser to reach
/adminand the noVNC login screen. - An O2 Cloud or Movistar Cloud account (unless you only run the
simulatedprovider).
For local (non-Docker) development you need Python 3.12+.
# 1. Configuration
cp .env.example .env
# 2. Create host directories mounted into the container
mkdir -p secrets config cache data
# 3. Create the secret files (see the Secrets section)
printf 'change-me-webdav\n' > secrets/webdav_password.txt
printf 'change-me-admin\n' > secrets/admin_password.txt
openssl rand -base64 32 > secrets/app_encryption_key.txt
# 4. Start
docker compose up -dBy default docker-compose.yml pulls the prebuilt image
garanda21/o2cloud_gateway_webdav:latest
from Docker Hub — no local build needed. To build from source instead, replace the
image: line with build: . and run docker compose up --build.
Then open:
- Admin panel: http://localhost:8088/admin
- WebDAV root: http://localhost:8088/dav
The shipped docker-compose.yml sets CLOUD_PROVIDER=o2. Set
CLOUD_PROVIDER=movistar to use Movistar Cloud, or CLOUD_PROVIDER=simulated to
try the gateway without an account (see Simulated mode).
Log in to the admin panel with ADMIN_USERNAME / the admin password, then
authenticate to the selected cloud provider.
O2 Cloud and Movistar Cloud have no public API token flow, so the gateway logs in the same way a human does: it drives a real Chromium browser inside the container using Playwright, and lets you complete the selected provider's login form (including any 2FA / captcha / consent screens) with your own eyes and hands.
Because the container has no physical display, that Chromium instance runs on a
virtual X server (Xvfb on DISPLAY=:99) and is exposed to you as a web-based
VNC session:
Xvfb (:99) ──> Chromium (Playwright) ──> x11vnc ──> websockify + noVNC ──> your browser :6080
Flow:
- Open the admin panel at
/adminand start Assisted O2/Movistar login. This callsPOST /api/admin/o2/login, which launches Chromium on the virtual display and navigates toO2_LOGIN_URL. - The panel shows (or links to) the noVNC screen. By default that is
http://localhost:6080/vnc.html?autoconnect=true&resize=scale&reconnect=true(host/port derived fromAPP_BASE_URL+NOVNC_PORT, or overridden withO2_LOGIN_NOVNC_URL). - Complete the provider login in the VNC window — type your credentials, pass any 2FA/captcha, accept consent dialogs.
- Once you land on the logged-in cloud page, the gateway captures the session
cookies and provider
validationKey. If that login flow exposes a renewable OAuth bundle and device identity, those are captured too. The resulting session is encrypted atO2_SESSION_FILEand Chromium is then closed. - Use Session status / test / logout in the admin panel to verify or clear the session.
The Chromium profile is persisted under /config/playwright-o2-profile/<provider>
so a later assisted login can reuse the provider session. An explicit logout from
the admin panel removes both the encrypted gateway session and this browser profile.
The gateway supports two renewal paths:
- If a
401response contains a rotated OAuth bundle andJSESSIONID, the gateway performsPOST /sapi/login/oauth, stores the newvalidationKeyand retries the original operation once. - Web logins normally expose only cookies and
validationKey. In that case the gateway reopens the persisted Chromium profile headlessly, reuses its SSO session and captures a fresh provider session without showing noVNC.
Sessions are also kept active preventively before the provider's idle window expires.
The gateway makes a lightweight authenticated API request every 5 minutes by default.
Set O2_SESSION_KEEPALIVE_SECONDS=0 to disable this behavior.
Concurrent WebDAV requests share a single renewal, avoiding parallel token rotation or multiple Chromium processes. Human intervention is required only when both the API credentials and the persisted browser SSO session have expired.
Notes:
O2_PLAYWRIGHT_HEADLESS=falseis required for the interactive flow — the browser must be rendered so you can see and drive it over VNC. Setting it totruedisables the visible session.- The bundled
x11vncruns with-nopw(no VNC password) and is intended to be reached only over your own trusted network / port mapping. Do not expose port6080to the public internet.
If you cannot use the VNC flow (headless host, restricted network), you can import a session captured elsewhere. Provide the same JSON shape the gateway stores for the selected provider:
{
"validationKey": "...",
"cookies": [
{"name": "...", "value": "...", "domain": "<provider-host>", "path": "/"}
],
"userAgent": "Mozilla/5.0 ...",
"oauthBundle": "...",
"deviceId": "...",
"deviceName": "...",
"encryptionToken": "...",
"createdAt": "2026-07-04T12:00:00Z"
}Import it through the admin panel's manual import action (or place it at
O2_SESSION_FILE). The gateway validates and encrypts it the same way as the
assisted flow.
Legacy imports containing only cookies and validationKey remain compatible. They can
renew silently only when the gateway also has a persistent browser profile created by
an assisted login; a standalone JSON import has no browser SSO session to reuse.
Authenticate with WEBDAV_USERNAME and the WebDAV password.
- macOS (Finder): Go → Connect to Server →
http://localhost:8088/dav - Windows: Map network drive →
http://localhost:8088/dav - rclone:
rclone config create cloud webdav \ url=http://localhost:8088/dav vendor=other \ user=webdav-user pass="$(rclone obscure change-me-webdav)" rclone ls cloud:
Set WEBDAV_READ_ONLY=true to block all writes (PUT/DELETE/MOVE/MKCOL). LOCK
support is implemented for clients that require it (macOS Finder, Office).
Set CLOUD_PROVIDER=simulated to run the whole WebDAV/admin stack against a fake
in-container filesystem rooted at SIMULATED_ROOT (default /data/simulated).
No O2/Movistar login is needed. Useful for testing WebDAV clients and the gateway itself.
Sensitive values are read from files (Docker secrets), not inline env vars where
possible. Each *_FILE variable points at a file whose contents are the secret;
the corresponding non-file variable (if set) takes precedence.
| File (host) | Mounted as | Purpose |
|---|---|---|
secrets/webdav_password.txt |
/run/secrets/webdav_password |
WebDAV client password (WEBDAV_PASSWORD_FILE) |
secrets/admin_password.txt |
/run/secrets/admin_password |
Admin panel password (ADMIN_PASSWORD_FILE) |
secrets/app_encryption_key.txt |
/run/secrets/app_encryption_key |
Encrypts the stored provider session and signs admin sessions (APP_ENCRYPTION_KEY_FILE, ADMIN_SESSION_SECRET_FILE) |
Generate the encryption key with openssl rand -base64 32. Keep these files out
of version control (secrets/ should be git-ignored).
All settings come from environment variables (or .env). Defaults below are the
in-code defaults; docker-compose.yml overrides several of them.
| Variable | Default | Description |
|---|---|---|
APP_HOST |
0.0.0.0 |
Interface the ASGI server binds to inside the container. |
APP_PORT |
8080 |
Port the app listens on inside the container (mapped to 8088 on the host by compose). |
APP_BASE_URL |
http://localhost:8080 |
Public base URL of the gateway. Used to build absolute links, including the default noVNC URL. |
APP_ENCRYPTION_KEY_FILE |
(unset) | File containing the encryption key used to encrypt the stored O2/Movistar session. |
PUID |
10001 |
Host user id the container runs as. Set to the owner of the mounted volumes so the app can write /config, /cache, /data. |
PGID |
10001 |
Host group id the container runs as (see PUID). |
The container starts as root only to apply PUID/PGID, fix volume ownership and
prepare the X11 socket, then drops privileges via gosu. No user: override or
HOME variable is needed in compose.
| Variable | Default | Description |
|---|---|---|
CLOUD_PROVIDER |
simulated |
Backend to serve: o2 (O2 Cloud), movistar (Movistar Cloud), or simulated (fake local filesystem). |
SIMULATED_ROOT |
/data/simulated |
Root directory for the simulated provider's fake files. |
| Variable | Default | Description |
|---|---|---|
O2_API_BASE_URL |
o2: https://cloud.o2online.es/sapi/movistar: https://micloud.movistar.es/sapi/ |
Base URL for metadata/API calls. The O2_* variable names are shared internal names: the default is selected from CLOUD_PROVIDER, unless you set an explicit custom URL. |
O2_UPLOAD_BASE_URL |
o2: https://upload.cloud.o2online.es/sapi/movistar: https://upload.micloud.movistar.es/sapi/ |
Base URL for uploads (separate host), selected from CLOUD_PROVIDER unless overridden. |
O2_LOGIN_URL |
o2: https://cloud.o2online.es/movistar: https://micloud.movistar.es/ |
Page Chromium opens for the interactive login, selected from CLOUD_PROVIDER unless overridden. |
O2_SESSION_FILE |
/config/secrets/o2-session.json |
Where the encrypted O2/Movistar session (cookies, validation key and renewable OAuth bundle) is stored. |
O2_PLAYWRIGHT_HEADLESS |
false |
Run the login Chromium headless. Must be false for the interactive VNC login to be visible. |
O2_SESSION_KEEPALIVE_SECONDS |
300 |
Preventive authenticated API keepalive interval for O2/Movistar sessions. Set to 0 to disable. |
O2_HTTP_TIMEOUT_SECONDS |
120 |
Timeout for O2/Movistar API HTTP requests. |
For Movistar Cloud, setting the provider is enough. The gateway automatically uses the Movistar API, upload, and login URLs unless you explicitly set custom URL values:
CLOUD_PROVIDER=movistar| Variable | Default | Description |
|---|---|---|
O2_LOGIN_NOVNC_URL |
(unset) | Explicit URL to the noVNC login screen. Despite its internal name, it applies to O2 and Movistar. If unset, it is derived from APP_BASE_URL, NOVNC_PORT and NOVNC_PATH. Set this whenever the published host port differs from the internal 6080 (e.g. you remapped it because 6080 was taken), or the gateway is reached by IP/hostname — otherwise the login link points at the wrong port. |
NOVNC_PORT |
6080 |
Host/container port serving the noVNC web client. |
NOVNC_PATH |
/vnc.html?autoconnect=true&resize=scale&reconnect=true |
Path + query appended when building the noVNC URL. |
DISPLAY |
:99 |
Virtual X display Chromium and VNC use (set in Dockerfile/compose). |
XVFB_SCREEN |
1280x900x24 |
Virtual framebuffer geometry for the login browser. |
VNC_PORT |
5900 |
Internal RFB port x11vnc listens on (proxied by websockify). |
NOVNC_HOST |
0.0.0.0 |
Interface websockify binds the noVNC web server to. |
| Variable | Default | Description |
|---|---|---|
WEBDAV_ENABLED |
true |
Enable the /dav WebDAV router. |
WEBDAV_PATH_BASE |
/dav |
URL path prefix the WebDAV share is mounted at. |
WEBDAV_USERNAME |
o2dav |
Username WebDAV clients authenticate with. |
WEBDAV_PASSWORD |
(unset) | Inline WebDAV password. Prefer WEBDAV_PASSWORD_FILE. |
WEBDAV_PASSWORD_FILE |
/run/secrets/webdav_password |
File containing the WebDAV password. |
WEBDAV_READ_ONLY |
false |
When true, reject all write methods (PUT/DELETE/MOVE/MKCOL/COPY). |
WEBDAV_ALLOW_DOTFILES |
true |
Allow listing/serving dotfiles (names starting with .). |
WEBDAV_DEPTH_INFINITY |
false |
Allow Depth: infinity PROPFIND requests (can be expensive). |
WEBDAV_IGNORE_APPLEDOUBLE |
true |
Ignore macOS AppleDouble sidecar files (._*) instead of uploading them to the cloud. |
| Variable | Default | Description |
|---|---|---|
ADMIN_ENABLED |
true |
Enable the /admin panel. |
ADMIN_PATH_BASE |
/admin |
URL path prefix for the admin panel. |
ADMIN_USERNAME |
admin |
Admin login username. |
ADMIN_PASSWORD |
(unset) | Inline admin password. Prefer ADMIN_PASSWORD_FILE. |
ADMIN_PASSWORD_FILE |
/run/secrets/admin_password |
File containing the admin password. |
ADMIN_SESSION_SECRET_FILE |
/run/secrets/app_encryption_key |
File whose contents sign admin session cookies. |
| Variable | Default | Description |
|---|---|---|
CONFIG_DIR |
/config |
Config directory (mounted volume). |
CACHE_DIR |
/cache |
Cache directory (mounted volume). |
DATA_DIR |
/data |
Data directory (mounted volume). |
SQLITE_PATH |
/data/o2cloud-gateway.db |
SQLite database file for metadata cache and locks. |
UPLOAD_SPOOL_DIR |
/cache/uploads |
Temporary spool directory for in-progress uploads. |
| Variable | Default | Description |
|---|---|---|
CACHE_METADATA_TTL_SECONDS |
20 |
How long directory/file metadata is cached before refetching from O2/Movistar. |
CACHE_NEGATIVE_TTL_SECONDS |
5 |
How long "not found" results are cached. |
CACHE_MAX_SIZE_MB |
4096 |
Max on-disk cache size in MB. |
UPLOAD_MAX_FILE_MB |
10240 |
Max single-file upload size in MB. |
DOWNLOAD_TIMEOUT_SECONDS |
3600 |
Timeout for a single file download from O2/Movistar. |
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL |
INFO |
Log verbosity (DEBUG, INFO, WARNING, ERROR). |
LOG_FILE |
/data/logs/gateway.log |
Path to the log file. |
| Container | Host (compose) | Purpose |
|---|---|---|
8080 |
8088 |
HTTP: WebDAV (/dav), admin (/admin), health (/health). |
6080 |
6080 |
noVNC web client for the interactive O2/Movistar login. |
GET /health returns the app health status and backs the Docker HEALTHCHECK.
Docker is the supported runtime, but you can run the app directly for development.
The interactive VNC login requires the container's X/VNC stack, so use the
simulated provider (or manual session import) when running on the host.
python3 -m venv .venv
. .venv/bin/activate
pip install -e ".[test,login]"
# Run tests
pytest
# Run the app with autoreload
uvicorn o2gateway.main:create_app --factory --reloadThe source targets Python 3.12+ and intentionally avoids newer syntax where practical, so local smoke tests can run on older macOS Python builds.
src/o2gateway/
main.py # ASGI app factory / entrypoint
settings.py # All environment-driven settings
webdav/ # WebDAV router, XML, PROPFIND parsing, locks
admin/ # Admin panel router + templates
o2/ # O2/Movistar API clients, session store, Playwright login
cloud/ # Provider interface + simulated provider
persistence/ # SQLite DB, metadata cache
security/ # Auth, session encryption
docker/entrypoint.sh # Starts Xvfb, fluxbox, x11vnc, websockify, then the app
Dockerfile # Runtime image (Python + Chromium + VNC stack)
docker-compose.yml # Reference deployment
MIT.