|
| 1 | +# Calendar-Proxy |
| 2 | + |
| 3 | +A tiny proxy to make browser-only calendar links (for example Office 365 `reachcalendar.ics`) usable by calendar clients that can't authenticate or set custom User-Agent headers. |
| 4 | + |
| 5 | +## Self-hosting |
| 6 | + |
| 7 | +Run a single container (secure defaults): |
| 8 | + |
| 9 | +```bash |
| 10 | +docker run -d --restart=unless-stopped \ |
| 11 | + --name calendar-proxy \ |
| 12 | + -p 8000:8000 \ |
| 13 | + --read-only \ |
| 14 | + --cap-drop=ALL \ |
| 15 | + --security-opt=no-new-privileges:true \ |
| 16 | + --memory=512m \ |
| 17 | + --cpus=0.5 \ |
| 18 | + --tmpfs=/tmp:noexec,nosuid,size=100m \ |
| 19 | + --tmpfs=/var/tmp:noexec,nosuid,size=50m \ |
| 20 | + --env-file .env \ |
| 21 | + calendar-proxy |
| 22 | +``` |
| 23 | + |
| 24 | +`.env` (example, keep this file private): |
| 25 | + |
| 26 | +``` |
| 27 | +PROXY_TOKENS=longtoken123 |
| 28 | +ALLOWED_HOSTS=example.com,calendar.example.org |
| 29 | +LOG_LEVEL=WARNING |
| 30 | +DISABLE_ACCESS_LOG=true |
| 31 | +``` |
| 32 | + |
| 33 | +Notes: |
| 34 | +- Prefer `--env-file` over inline `-e` to avoid leaking secrets in process lists or shell history. |
| 35 | +- Container runs as non-root user (UID 1000) for enhanced security. |
| 36 | +- Hardened with `--read-only`, capability drops, `no-new-privileges`, resource limits, and temporary filesystems. |
| 37 | +- Use `REDIS_URL` when running multiple containers for shared rate-limiting. |
| 38 | +- For production, consider using `docker-compose.production.yml` which includes additional security measures. |
| 39 | + |
| 40 | +## Required / recommended env vars |
| 41 | + |
| 42 | +- `PROXY_TOKENS` (required): comma-separated secret tokens used in subscription URLs. |
| 43 | +- `ALLOWED_HOSTS` (recommended): comma-separated allowed upstream hostnames. |
| 44 | +- `REDIS_URL` (optional): `redis://...` for cross-process rate limiting. |
| 45 | +- `RATE_LIMIT_PER_MIN` (default `60`): per-token request limit per minute. |
| 46 | +- `UPSTREAM_USER_AGENT` (default: Safari on macOS): User-Agent header sent to upstream servers. |
| 47 | +- `MAX_RESPONSE_BYTES` (default: `5242880` bytes / 5MB): maximum response size to prevent memory exhaustion. |
| 48 | +- `CONNECT_TIMEOUT` (default: `10` seconds): timeout for establishing upstream connections. |
| 49 | +- `READ_TIMEOUT` (default: `20` seconds): timeout for reading upstream response data. |
| 50 | +- `ALLOWED_CONTENT_TYPES` (default: `text/calendar,text/plain,application/octet-stream`): comma-separated allowed upstream content types. |
| 51 | +- `LOG_LEVEL` (default: `INFO`): application log level (DEBUG, INFO, WARNING, ERROR). |
| 52 | +- `DISABLE_ACCESS_LOG` (default: `true`): set to `false` to enable logging of URLs and accesses. |
| 53 | + |
| 54 | +## Build from source |
| 55 | + |
| 56 | +```bash |
| 57 | +docker build -t calendar-proxy . |
| 58 | +``` |
| 59 | + |
| 60 | +## Production deployment |
| 61 | + |
| 62 | +For production use, a hardened `docker-compose.production.yml` is provided: |
| 63 | + |
| 64 | +```bash |
| 65 | +# Copy and customize the production compose file |
| 66 | +cp docker-compose.production.yml docker-compose.yml |
| 67 | + |
| 68 | +# Create secure environment file |
| 69 | +echo "PROXY_TOKENS=$(openssl rand -hex 40)" > .env |
| 70 | + |
| 71 | +# Deploy with enhanced security |
| 72 | +docker-compose up -d |
| 73 | +``` |
| 74 | + |
| 75 | +The production configuration includes: |
| 76 | + |
| 77 | +- **Non-root execution**: Runs as UID/GID 1000 |
| 78 | +- **Read-only filesystem**: Prevents runtime modifications |
| 79 | +- **Capability restrictions**: Drops all capabilities except essential ones |
| 80 | +- **Resource limits**: Memory (512MB) and CPU (0.5 cores) constraints |
| 81 | +- **No new privileges**: Prevents privilege escalation |
| 82 | +- **Temporary filesystems**: Secure `/tmp` and `/var/tmp` mounts |
| 83 | +- **Enhanced logging**: Production-ready log levels |
| 84 | + |
| 85 | +## How the proxy works (short) |
| 86 | + |
| 87 | +- Endpoint: `GET /sub/{token}/{b64url}/{name}.ics` |
| 88 | + - `token`: secret in the path. |
| 89 | + - `b64url`: URL-safe base64 (no padding) of the full upstream URL. |
| 90 | + - `name.ics`: final filename for clients. |
| 91 | + - Optional `ua` query param: override User-Agent used to fetch upstream. |
| 92 | + |
| 93 | +Security & runtime behaviour: |
| 94 | +- **Container security**: Runs as non-root user, read-only filesystem, dropped capabilities, resource limits. |
| 95 | +- Validates `token` against `PROXY_TOKENS`. |
| 96 | +- Resolves `b64url` hostname and refuses private/loopback/link-local/multicast/reserved IPs (SSRF protection). |
| 97 | +- Enforces `ALLOWED_HOSTS` when set. |
| 98 | +- Validates upstream content types against `ALLOWED_CONTENT_TYPES`. |
| 99 | +- Per-token rate limiting (Redis if `REDIS_URL` set, otherwise in-process fallback with automatic cleanup). |
| 100 | +- Streams upstream response, enforces timeouts and a maximum response byte cap. |
| 101 | +- Strips client cookies and Authorization; forwards only safe upstream headers (e.g. `Content-Type`, `Content-Disposition`). |
| 102 | +- Sanitizes URLs in logs to prevent exposure of sensitive information (when `DISABLE_ACCESS_LOG=true`). |
| 103 | + |
| 104 | +## Why macOS Calendar can't directly subscribe to Microsoft Office 365 links (very short) |
| 105 | + |
| 106 | +1) Office 365 shared URLs are browser-centric |
| 107 | +- Shared `reachcalendar.ics` links are designed to be opened by a browser, which may provide cookies or other auth automatically; macOS Calendar does not. |
| 108 | + |
| 109 | +2) User-Agent restrictions |
| 110 | +- Microsoft checks User-Agent and rejects unsupported clients. Requests from macOS `CalendarAgent` often get a "Outlook is not supported on this browser" page instead of an ICS. |
| 111 | + |
| 112 | +3) No auth flow support |
| 113 | +- Some shared calendars require Microsoft authentication (cookies/OAuth). macOS Calendar cannot perform those interactive browser flows. |
| 114 | + |
| 115 | +Workarounds: use this proxy (fetch with a browser-like UA), import into a service that fetches server-side, or download & import manually. |
| 116 | + |
| 117 | +## Quick examples |
| 118 | + |
| 119 | +Generate a URL-safe base64 without padding (shell): |
| 120 | + |
| 121 | +```bash |
| 122 | +python - <<'PY' |
| 123 | +import base64 |
| 124 | +u='https://example.com/cal.ics' |
| 125 | +print(base64.urlsafe_b64encode(u.encode()).decode().rstrip('=')) |
| 126 | +PY |
| 127 | +``` |
| 128 | + |
| 129 | +Or without Python (using base64 + tr): |
| 130 | + |
| 131 | +```bash |
| 132 | +echo -n 'https://example.com/cal.ics' | base64 -w0 | tr '+/' '-_' | tr -d '=' |
| 133 | +``` |
| 134 | + |
| 135 | +Subscription URL: |
| 136 | + |
| 137 | +``` |
| 138 | +https://proxy.example.com/sub/<token>/<b64url>/Work.ics |
| 139 | +``` |
| 140 | + |
| 141 | +Open `http://<host>:8000/` in a browser to use the included `index.html` UI for building links. |
| 142 | + |
| 143 | +## Endpoints |
| 144 | + |
| 145 | +- `GET /sub/{token}/{b64url}/{name}.ics` — subscription proxy endpoint. |
| 146 | +- `GET /healthz` — health check JSON response. |
| 147 | +- `GET /` — web UI for building calendar subscription URLs. |
| 148 | +- `GET /static/{path}` — static files (CSS, etc.) for the web UI. |
| 149 | + |
| 150 | +If you want an admin UI for token management, Redis-backed caching, or a `docker-compose.yml` with Redis, tell me which and I'll add it. |
0 commit comments