- Rust 70.7%
- TypeScript 14.4%
- Svelte 11%
- Dockerfile 1.2%
- CSS 1.2%
- Other 1.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
## Summary ```diff CookieManager (proxy/src/cookies.rs) parse_set_cookies + "_strava_idcf" => idcf = Some(value) // same parse style as the trio cookie_header() - "CloudFront-Key-Pair-Id=…; CloudFront-Policy=…; CloudFront-Signature=…" + … ; _strava_idcf=<jwt> // appended only when present get_tile (proxy/src/app.rs) - refresh+refetch on upstream 403 + refresh+refetch on upstream 403 or 401 ``` Strava's identified tile endpoint now also requires the `_strava_idcf` JWT (short-lived, athlete-bound) alongside the CloudFront trio — trio-only requests get **401**. That JWT is emitted as a `Set-Cookie` by the very exchange response `fetch_cookies` already parses; we were dropping it. This captures and forwards it, and teaches the cookie-recovery path the 401 signature (the 403-only trigger couldn't recover an idcf that expires or is invalidated inside the 12 h refresh window). ## Evidence - **Before** (live production probes, 2026-10-02): exchange yields a valid identified-scope trio (same policy + key-pair as a working browser session), yet `content-a.strava.com/identified/…` returns **401** for trio-only and **200 PNG** for trio + `_strava_idcf` → every zoom 12–15 request through canicule was a 502. - **After:** `cargo test -p canicule` — **44 passed (9 suites)**, incl. new: - `forwards_the_strava_idcf_cookie_on_authenticated_tile_requests` — exact wire-level Cookie-header assertion (red against pre-fix code) - `reauthenticates_after_an_upstream_401_when_the_idcf_cookie_is_missing` — mock tile endpoint returns 401 unless the Cookie header carries `_strava_idcf=`; asserts final 200 through refresh+refetch (red was 502 vs 200, the production shape) - unit: idcf captured with cookie attributes; absent idcf → byte-identical trio-only header, no trailing `"; "` - `cargo fmt --check` and `clippy --all-targets` clean. ## Merge Danger **Door:** two-way — revert restores current (hi-res-broken) behaviour; anonymous zoom ≤ 11 unaffected either way. **Blast Radius:** low — authenticated tile path only. Absent-idcf behaviour is byte-identical to today's, pinned by tests. No leak path: the idcf value only flows into the upstream request; visitor responses carry tile bytes only. Post-deploy check: one zoom 12–15 tile through the proxy after the next tofu-maison apply (Renovate bumps the pinned ref automatically). Related: findings also posted on #6 — the login/password flow must maintain `_strava4_session` **and** `_strava_idcf`. Co-authored-by: Mateo Greil <mateo@go-electra.com> Reviewed-on: #8 |
||
| .forgejo/workflows | ||
| frontend | ||
| proxy | ||
| .dockerignore | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| Makefile | ||
| README.md | ||
canicule 🔥
Explore Strava's Global Heatmap without a Strava account. canicule is a self-hostable, single-binary map viewer with a Rust tile proxy and a Svelte + MapLibre frontend. It works anonymously at zoom levels up to 11; an optional server-side Strava session cookie unlocks high-resolution tiles up to zoom 15. Visitors never receive that cookie.
This is an unofficial project, not affiliated with Strava.
Quick start
git clone ssh://git@git.greil.fr/mat/canicule.git
cd canicule
docker compose up -d --build
Open http://localhost:8080. No account or configuration is needed for low-resolution tiles. Change the host port with CANICULE_PORT=3000 docker compose up -d --build.
To enable high resolution, sign in to Strava in your browser, open developer tools → Application / Storage → Cookies → www.strava.com, and copy the value of _strava4_session. Keep it secret: it grants access to your Strava session.
STRAVA_SESSION_COOKIE='your-session-cookie' docker compose up -d
For a persistent installation, put STRAVA_SESSION_COOKIE=... in a local .env file (ignored by Git) and run docker compose up -d. The proxy obtains short-lived CloudFront cookies from that session and refreshes them automatically. If the Strava session expires, copy a new one. Never put the cookie in a committed file or a public issue.
To run the binary without Docker:
make build
CACHE_DIR=./cache ./target/release/canicule
Map controls
- Choose All, Ride, Run, Water, or Winter activities.
- Pick one of six heatmap palettes: Hot, Blue, Gray, Blue / Red, Purple, Orange.
- Adjust heatmap opacity; switch between Voyager, Light, and Dark Carto basemaps.
- Use browser geolocation to center the map on your position.
- Copy the URL to preserve map position, zoom, sport, and color. The heatmap remains visible beyond its native maximum zoom by overzooming existing tiles; no new detail is added.
Configuration
| Variable | Default | Description |
|---|---|---|
PORT |
8080 |
HTTP listening port inside the container |
STRAVA_SESSION_COOKIE |
unset | _strava4_session value; enables high-resolution mode |
CACHE_DIR |
unset | Persistent tile cache directory; Compose sets /var/cache/canicule in a named volume |
CACHE_TTL_DAYS |
14 |
Tile freshness period in days |
CACHE_MAX_ENTRIES |
10000 |
In-memory LRU capacity (tiles) |
UPSTREAM_TIMEOUT_SECS |
10 |
Timeout for requests to Strava |
CANICULE_PORT |
8080 |
Host port in Docker Compose only |
The cache keeps tiles fresh for 14 days to avoid repeatedly fetching unchanged heatmap data. Empty tiles have a one-hour negative cache. Concurrent requests for the same tile share one upstream fetch; no more than eight different upstream requests run at once. With CACHE_DIR set, an expired tile is served as a fallback when Strava is unavailable.
The Rust server serves both the frontend and /api/tiles/{sport}/{color}/{z}/{x}/{y}.png from the same origin. No CORS configuration or browser-side Strava credentials are needed.
Development
Requires Node.js 22+, Rust (stable), and optionally Docker and Google Chrome for browser tests.
npm --prefix frontend ci
make dev # Vite :5173 + Rust proxy :8080
make test # cargo tests, Vitest, typecheck, Playwright
make build # target/release/canicule with embedded frontend
In environments where port 8080 is occupied, run PORT=18080 VITE_PROXY_TARGET=http://127.0.0.1:18080 make dev.
Verification checklist
docker compose up -d --buildloads a map at http://localhost:8080 withoutSTRAVA_SESSION_COOKIE./api/configreportsauthenticated: false,maxZoom: 11; a real anonymous tile at/api/tiles/all/hot/6/32/21.pngreturns PNG.- A second request for the same tile returns
X-Cache: hit; zoom 12 in anonymous mode is rejected locally. - Five sports, six colors, opacity, three basemaps, geolocation, mobile controls, and URL hash restore work in the browser.
make testpasses Rust, Vitest, typechecking, and Playwright;make buildproduces a binary serving the actual frontend bundle.- If a valid
STRAVA_SESSION_COOKIEis available:/api/configreportsauthenticated: true,maxZoom: 15, and a high-resolution tile loads. This live authentication path has not yet been verified; the automated cookie-flow tests use a mock Strava server.
Attribution and fair use
Heatmap data © Strava · basemaps © CARTO · map data © OpenStreetMap contributors. Attribution is also shown on the map.
Please respect Strava's terms of use. Do not bulk-scrape tiles. This tool is intended for personal and small-scale use; a public instance needs its own legal and rate-limit assessment. Strava can change its tile authentication or block access at any time. The cache reduces requests but does not guarantee availability.