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)
- Admin panel
- Logging in to O2/Movistar (interactive VNC login)
- Manual session import
- Telegram session-expiry alerts
- Using the WebDAV share
- Simulated mode (no real cloud account)
- Secrets
- Environment variables
- Ports
- Local development
- Releasing
- 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.
The responsive administration panel provides a single view of the gateway's service, cloud session, quota, WebDAV URL, metadata cache, locks and most recent error. It also includes:
- Assisted login and manual session import.
- Connection tests, session removal and cache cleanup.
- Telegram notification status and a test-message action.
- Recent operation output and logs, with actions to refresh or clear the active log file.
- Gateway version and source commit in the login screen and footer, linked to the GitHub repository and the exact revision respectively.
The current redesign and immediate Telegram expiry alerts are available in the development image:
services:
o2cloud-webdav:
image: garanda21/o2cloud_gateway_webdav:developmentUpdate an existing development deployment with:
docker compose pull
docker compose up -d --force-recreateO2 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.
The gateway can notify one Telegram chat when O2/Movistar rejects the active
session and automatic renewal cannot recover it. Notifications are deduplicated:
the expiry event triggers delivery immediately and only one alert is sent for a
given expired session. Failed deliveries are retried after
TELEGRAM_ALERT_RETRY_SECONDS; no periodic alert polling is used.
-
Create a bot with Telegram's
@BotFatherand copy its token. -
Start a conversation with the bot. A bot cannot initiate a private conversation until the user has contacted it.
-
Obtain the destination chat id and set these values in the gitignored
.env:TELEGRAM_BOT_TOKEN=123456:replace-with-your-token TELEGRAM_CHAT_ID=123456789
-
Restart the container and click Notificaciones Telegram in the admin header to send a test message.
For a production deployment, prefer TELEGRAM_BOT_TOKEN_FILE with a read-only
Docker secret instead of exposing the token as a container environment variable.
The token and chat id are never returned by the admin API or written to logs.
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. |
APP_VERSION |
package version | Optional display-version override for the API and admin footer. |
APP_COMMIT |
local Git commit when available | Optional source revision shown in the admin footer; injected from GIT_COMMIT by the Docker build. |
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 |
|---|---|---|
TELEGRAM_BOT_TOKEN |
(unset) | Bot token issued by @BotFather. Prefer the file setting in production. |
TELEGRAM_BOT_TOKEN_FILE |
/run/secrets/telegram_bot_token |
Optional file containing the bot token. TELEGRAM_BOT_TOKEN takes precedence. |
TELEGRAM_CHAT_ID |
(unset) | Private chat, group, supergroup or channel id that receives alerts. |
TELEGRAM_ALERT_RETRY_SECONDS |
300 |
Minimum delay between retry attempts after Telegram rejects or cannot receive an alert. |
| 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. |
ADMIN_STATUS_POLL_SECONDS |
120 |
Refresh interval for admin status while the browser tab is visible. Set to 0 to disable periodic refreshes. |
| 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_QUOTA_TTL_SECONDS |
60 |
How long cloud quota results are reused across admin status requests. |
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 --reloadTo include the release version and source commit in the admin footer when building the container:
docker build \
--build-arg APP_VERSION="0.1.0" \
--build-arg GIT_COMMIT="$(git rev-parse HEAD)" \
-t o2cloud-webdav-gateway .The package version is always shown; the commit is appended when available.
The source targets Python 3.12+ and intentionally avoids newer syntax where practical, so local smoke tests can run on older macOS Python builds.
GitHub Releases and Docker Hub images are published together by
.github/workflows/release.yml. The workflow runs only for version tags matching
v*.*.*, verifies that the tagged commit belongs to main, runs the complete test
suite, builds the image for linux/amd64, pushes it to Docker Hub and finally creates
a GitHub Release with automatically generated notes.
Configure these GitHub Actions repository secrets before publishing the first release:
| Secret | Purpose |
|---|---|
DOCKERHUB_USERNAME |
Docker Hub account allowed to push garanda21/o2cloud_gateway_webdav. |
DOCKERHUB_TOKEN |
Docker Hub access token with read/write permission. Do not use the account password. |
Pull requests should be labelled enhancement, bug or documentation before
merging. .github/release.yml uses those labels to organize the generated changelog;
unlabelled changes appear under Other changes, and ignore-for-release excludes
internal-only work.
Stable releases publish version-specific tags plus latest:
git switch main
git pull --ff-only
git tag -a v0.1.0 -m "v0.1.0"
git push origin v0.1.0This publishes 0.1.0, v0.1.0 and latest. A prerelease tag such as
v0.2.0-rc.1 is marked as a GitHub prerelease and publishes 0.2.0-rc.1,
v0.2.0-rc.1 and development without changing latest.
The tag value is injected as APP_VERSION, while its full source revision is injected
as APP_COMMIT; both link back to this repository from the admin panel. Review the
generated release notes after publication, especially for the first release, because
commits made directly on main appear in the full comparison but not necessarily as
individual pull-request entries.
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.