Skip to content

Commit 974b502

Browse files
Copexitclaude
andcommitted
fix: improve self-hosted mempool diagnostics and Tor detection
- Add pre-flight URL diagnostics that detect mixed content and CORS barriers before the health check, with actionable guidance per scenario (SSH tunnel, HTTPS proxy, Tor Browser + .onion) - Improve API settings with collapsible setup guide, nginx CORS snippet, and scenario-specific error messages for Umbrel/Start9 users - Replace hostname-only Tor detection with check.torproject.org API so Brave Tor and other Tor-routed browsers are correctly identified - Fix service worker to bypass all cross-origin requests instead of a hardcoded hostname list, preventing misleading 503 errors for custom API endpoints - Add onion.md with VPS setup instructions for deploying a .onion mirror - Show custom-API-aware error messages in analysis when endpoint fails Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 29a4bf0 commit 974b502

File tree

7 files changed

+547
-44
lines changed

7 files changed

+547
-44
lines changed

onion.md

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
# Deploying a .onion Mirror of am-i.exposed
2+
3+
## Why a .onion mirror is needed
4+
5+
When users run their own mempool.space instance (Umbrel, Start9, or a custom node),
6+
the API is typically accessible via a `.onion` Tor hidden service address. However,
7+
the main site at `https://am-i.exposed` is served over HTTPS, and browsers enforce
8+
**mixed content blocking** - an HTTPS page cannot make `fetch()` requests to plain
9+
HTTP URLs (except `localhost`).
10+
11+
This means a user on Tor Browser visiting `https://am-i.exposed` cannot connect to
12+
their mempool at `http://xyz.onion/api` - the browser silently blocks the request.
13+
14+
A `.onion` mirror of our site solves this: the mirror is served over HTTP (Tor
15+
provides encryption and authentication at the network layer), so requests from
16+
`http://our-mirror.onion` to `http://user-mempool.onion/api` are HTTP-to-HTTP
17+
with no mixed content barrier.
18+
19+
**Note:** CORS headers are still required on the user's mempool nginx config
20+
regardless of the connection method. See the in-app help section for the nginx
21+
snippet.
22+
23+
## Why Cloudflare Onion Routing doesn't solve this
24+
25+
Cloudflare offers an "Onion Routing" toggle (available on all plans) that gives
26+
Tor Browser users a `.onion` route to your site via `alt-svc` headers. However:
27+
28+
- The visible URL stays `https://am-i.exposed` (not a `.onion` address)
29+
- The page is still served over HTTPS
30+
- Mixed content restrictions still apply to outgoing `fetch()` calls
31+
- `window.location.hostname` remains `am-i.exposed`, not `.onion`
32+
33+
Cloudflare's feature is useful for avoiding exit-node exposure when Tor users
34+
access our site, but it does **not** enable HTTP-to-HTTP API calls to self-hosted
35+
`.onion` mempool instances. A true `.onion` hidden service is required for that.
36+
37+
## VPS setup instructions
38+
39+
### Prerequisites
40+
41+
- A VPS (any small instance - the site is fully static, minimal resources needed)
42+
- Root/sudo access
43+
- The static export files (`pnpm build` produces the `out/` directory)
44+
45+
### 1. Install Tor
46+
47+
```bash
48+
# Debian/Ubuntu
49+
sudo apt update
50+
sudo apt install tor
51+
52+
# Verify it's running
53+
sudo systemctl status tor
54+
```
55+
56+
### 2. Configure the Tor hidden service
57+
58+
Edit `/etc/tor/torrc`:
59+
60+
```
61+
HiddenServiceDir /var/lib/tor/am-i-exposed/
62+
HiddenServicePort 80 127.0.0.1:8080
63+
```
64+
65+
Restart Tor:
66+
67+
```bash
68+
sudo systemctl restart tor
69+
```
70+
71+
Get your `.onion` address:
72+
73+
```bash
74+
sudo cat /var/lib/tor/am-i-exposed/hostname
75+
```
76+
77+
This outputs something like `abc123xyz456.onion` - this is your mirror address.
78+
79+
### 3. Serve the static site
80+
81+
Install a lightweight web server (nginx, caddy, or even Python's http.server for
82+
testing):
83+
84+
**Option A: nginx**
85+
86+
```bash
87+
sudo apt install nginx
88+
```
89+
90+
Create `/etc/nginx/sites-available/am-i-exposed`:
91+
92+
```nginx
93+
server {
94+
listen 127.0.0.1:8080;
95+
server_name _;
96+
97+
root /var/www/am-i-exposed;
98+
index index.html;
99+
100+
# Next.js static export uses trailing slashes
101+
location / {
102+
try_files $uri $uri/ $uri/index.html =404;
103+
}
104+
105+
# Security headers
106+
add_header X-Content-Type-Options nosniff always;
107+
add_header X-Frame-Options DENY always;
108+
add_header Referrer-Policy no-referrer always;
109+
110+
# No need for HTTPS headers - Tor provides encryption
111+
}
112+
```
113+
114+
Enable and start:
115+
116+
```bash
117+
sudo ln -s /etc/nginx/sites-available/am-i-exposed /etc/nginx/sites-enabled/
118+
sudo nginx -t
119+
sudo systemctl reload nginx
120+
```
121+
122+
**Option B: Caddy (simpler config)**
123+
124+
```bash
125+
sudo apt install caddy
126+
```
127+
128+
Create `/etc/caddy/Caddyfile`:
129+
130+
```
131+
:8080 {
132+
root * /var/www/am-i-exposed
133+
file_server
134+
try_files {path} {path}/ {path}/index.html
135+
}
136+
```
137+
138+
### 4. Deploy the static files
139+
140+
From your development machine:
141+
142+
```bash
143+
# Build the static export
144+
pnpm build
145+
146+
# Copy to VPS
147+
rsync -avz out/ user@your-vps:/var/www/am-i-exposed/
148+
```
149+
150+
Or set up a simple CI/CD pipeline that builds and deploys on push to main.
151+
152+
### 5. Verify
153+
154+
Open Tor Browser and navigate to `http://your-onion-address.onion`. The site
155+
should load identically to the HTTPS version.
156+
157+
Test the privacy-optimal flow:
158+
1. Open Tor Browser
159+
2. Visit `http://your-onion-address.onion`
160+
3. Open API settings (gear icon)
161+
4. Enter your mempool's `.onion` API address (e.g., `http://mempool-onion.onion/api`)
162+
5. The health check should succeed (no mixed content barrier)
163+
164+
### 6. Keep it updated
165+
166+
Set up a cron job or deploy hook to sync the `out/` directory whenever the site
167+
is updated:
168+
169+
```bash
170+
# Example cron job (runs every 6 hours)
171+
0 */6 * * * rsync -avz /path/to/out/ /var/www/am-i-exposed/ && sudo systemctl reload nginx
172+
```
173+
174+
Or use a GitHub Actions workflow that SSHs into the VPS after a successful build.
175+
176+
## Architecture overview
177+
178+
```
179+
Tor Browser user
180+
|
181+
| (Tor circuit - encrypted, authenticated)
182+
v
183+
[.onion hidden service on VPS]
184+
|
185+
| (localhost:8080)
186+
v
187+
[nginx serving static files]
188+
|
189+
| HTTP fetch() from the page
190+
| (no mixed content - both HTTP)
191+
v
192+
[User's mempool .onion hidden service]
193+
|
194+
| (Tor circuit)
195+
v
196+
[User's Bitcoin node + mempool backend]
197+
```
198+
199+
## CORS reminder
200+
201+
Even with the `.onion` mirror eliminating mixed content, the user's mempool
202+
instance still needs CORS headers because the browser origins differ
203+
(`http://our-mirror.onion` vs `http://user-mempool.onion`).
204+
205+
Add to the mempool nginx config on the user's node:
206+
207+
```nginx
208+
location /api/ {
209+
add_header 'Access-Control-Allow-Origin' '*' always;
210+
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
211+
add_header 'Access-Control-Allow-Headers' 'Content-Type' always;
212+
213+
if ($request_method = 'OPTIONS') {
214+
return 204;
215+
}
216+
217+
# ... existing proxy/upstream config ...
218+
}
219+
```

public/sw.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,9 @@ self.addEventListener("fetch", (event) => {
3131
// Only handle GET requests
3232
if (event.request.method !== "GET") return;
3333

34-
// Don't cache API calls
34+
// Skip all cross-origin requests (API calls, custom endpoints, etc.)
3535
const url = new URL(event.request.url);
36-
if (
37-
url.hostname === "mempool.space" ||
38-
url.hostname === "blockstream.info" ||
39-
url.hostname.endsWith(".onion")
40-
) {
36+
if (url.origin !== self.location.origin) {
4137
return;
4238
}
4339

0 commit comments

Comments
 (0)