Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
df91967
Style(toast): move toasts to bottom-right, left of the FAB column
jason5ng32 Apr 19, 2026
4fbb38c
Feat(connectivity): let users add custom availability tests
jason5ng32 Apr 19, 2026
c01e984
Fix(a11y): silence Dialog description warning + extend aria-invalid t…
jason5ng32 Apr 19, 2026
b683248
Chore(store): bump userPreferences localStorage key to v6 to reset cl…
jason5ng32 Apr 19, 2026
9ca6ba5
Docs(release): v6.1.0
jason5ng32 Apr 19, 2026
d878cff
Improvements
jason5ng32 Apr 19, 2026
b1e8c29
Update changelog.json
jason5ng32 Apr 19, 2026
7f3d26a
Bug Fixed
jason5ng32 Apr 19, 2026
df9c7e0
UI Improvement
jason5ng32 Apr 19, 2026
1406a35
Fix UI bug
jason5ng32 Apr 19, 2026
4a54deb
IPCard UI Improvement
jason5ng32 Apr 19, 2026
dabd25a
Fix(security-checklist): scroll the drawer's own container, not the page
jason5ng32 Apr 19, 2026
7ef85ab
Fix(advanced-tools): swap raw <label> for the shadcn Label primitive
jason5ng32 Apr 19, 2026
7131428
UI Improvement
jason5ng32 Apr 19, 2026
158b03d
Add Coordinates on map
jason5ng32 Apr 20, 2026
9b11d8e
Improve UI of Toggle Group
jason5ng32 Apr 20, 2026
41a4fda
UI Improvements
jason5ng32 Apr 20, 2026
e78fa8b
Fix(forms): harden free-form inputs against iOS keyboard quirks
jason5ng32 Apr 20, 2026
b2a9929
Chore(vite): allow dev/test subdomain hosts on the Vite dev server
jason5ng32 Apr 20, 2026
320f230
Refactor(ip-display): fit-to-width IP text across all IP rows
jason5ng32 Apr 20, 2026
03101e6
Perf(dns-resolver): bound every upstream lookup with a per-call timeout
jason5ng32 Apr 20, 2026
356849d
Feat(whois): RDAP fallback for gTLDs whoiser can't reach
jason5ng32 Apr 20, 2026
2e5e400
Style(browser-info): unify Copy UA icon with other copy buttons
jason5ng32 Apr 20, 2026
8e46f6b
Style(ip-info): drop muted tint from Native IP status icon
jason5ng32 Apr 20, 2026
ce23795
Docs(agents): record branch discipline — dev in, dev out
jason5ng32 Apr 20, 2026
8dc92eb
Refactor(connectivity): probe via fetch HEAD no-cors instead of <img>
jason5ng32 Apr 20, 2026
53d45d7
Chore(changelog): record v6.1.0 DNS / Whois / Connectivity entries
jason5ng32 Apr 20, 2026
09e52e5
Update changelog.json
jason5ng32 Apr 20, 2026
5b295b0
Update changelog.json
jason5ng32 Apr 20, 2026
c206371
Improve UI
jason5ng32 Apr 20, 2026
66fb9ff
UI bug fixed
jason5ng32 Apr 20, 2026
0867893
Fixed Shortcut bug
jason5ng32 Apr 20, 2026
0f03f2c
Refactor(advanced-tools): extract useGlobalpingMeasurement composable
claude Apr 20, 2026
af0f7cc
Refactor(status-tone): consolidate 4 toneOf implementations into ipFi…
claude Apr 20, 2026
9eacbd9
Refactor(fetch): route remaining bare fetch() through fetchWithTimeout
claude Apr 20, 2026
fadd861
Refactor(valid-ip): share isValidDomain via common/valid-ip.js
claude Apr 20, 2026
6653720
Fix(ui): clear timers and detach engine callbacks on unmount
claude Apr 20, 2026
87b1e94
Style(store): route signIn failure alerts through setAlert
claude Apr 20, 2026
c448fae
Fix(a11y): silence Drawer aria-hidden warning and add DialogContent d…
claude Apr 21, 2026
3a9ba2a
Fix(a11y): mark #mainpart inert while any overlay is open
claude Apr 21, 2026
a401f78
Fix(webrtc): correct STUN test semantics and distinguish failure modes
claude Apr 21, 2026
c7bb130
Refactor(webrtc): fetchCountryCode returns null on miss instead of un…
claude Apr 21, 2026
e311974
Fix(webrtc): close pending RTCPeerConnections on unmount
claude Apr 21, 2026
fcc9de6
Revert Drawer a11y fixes
claude Apr 21, 2026
d62b388
Refactor(webrtc): drop the timeout / mDNS-privacy split, keep StatusE…
claude Apr 21, 2026
209fee9
Merge pull request #326 from jason5ng32/claude/optimize-vue-scripts-k…
jason5ng32 Apr 21, 2026
76dfefb
Improvements
jason5ng32 Apr 21, 2026
a939b07
Update .gitignore
jason5ng32 Apr 21, 2026
89fc828
Feat(advanced-tools): add in-depth DNS Leak Test
jason5ng32 Apr 21, 2026
d2ce03f
UI Improvement
jason5ng32 Apr 21, 2026
1458572
Improvements
jason5ng32 Apr 21, 2026
5eea710
UI Improvement
jason5ng32 Apr 21, 2026
71e14c2
Fix(connectivity): use GET instead of HEAD for reachability probe
jason5ng32 Apr 22, 2026
08e20be
Feat(backend): bootstrap MaxMind databases on startup if missing
jason5ng32 Apr 22, 2026
833f66c
Feat(backend): introduce pino logger + pino-http request logs
jason5ng32 Apr 22, 2026
0251601
Improvements
jason5ng32 Apr 23, 2026
98b9181
Improvements
jason5ng32 Apr 23, 2026
bff8466
Feat(boot): staged pre-Vue overlay hand-off with scale animation
jason5ng32 Apr 24, 2026
2951bfc
Merge pull request #328 from jason5ng32/dev
jason5ng32 Apr 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,9 @@ VITE_GOOGLE_ANALYTICS_ID=""
MAXMIND_ACCOUNT_ID=""
MAXMIND_LICENSE_KEY=""
MAXMIND_AUTO_UPDATE="false"
# Logging — LOG_LEVEL: debug/info/warn/error (default info)
# LOG_FORMAT: "json" for log shippers, anything else = pretty
# LOG_HTTP: "true" to enable per-request /api logging (off by default)
LOG_LEVEL=""
LOG_FORMAT=""
LOG_HTTP=""
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ common/maxmind-db/*.bak
common/maxmind-db/*.next
common/maxmind-db/*.mmdb
.learnings/
docs/
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Single repo, two halves: a Vue 3 SPA front-end and an Express 5 back-end API, se
| Bottom drawer | `vaul-vue` |
| Toast | `vue-sonner` |
| Backend | Express 5 |
| Logger | `pino` + `pino-pretty` (dev) + `pino-http` (request logs) — singleton at `common/logger.js` |
| Auth | Firebase Auth (optional, env-gated) |
| PWA | Serwist |
| Tests | Node built-in test runner (`node --test`) |
Expand Down Expand Up @@ -84,6 +85,14 @@ Single repo, two halves: a Vue 3 SPA front-end and an Express 5 back-end API, se
- Any feature that surfaces copy must land in **all four locales** (`en` / `zh` / `fr` / `tr`) in the same change. No English-only or Chinese-only keys slipping through.
- Same rule applies to `frontend/data/changelog.json` — every entry's `change` object must have all four languages. `tests/changelog.test.js` enforces this.

### Logging (backend)

- **Use the shared logger from `common/logger.js`** in every backend file (`backend-server.js`, `frontend-server.js`, `api/*`, `common/*`). It's a `pino` singleton — pretty-printed via `pino-pretty` by default; set `LOG_FORMAT=json` in `.env` to emit raw JSON for log aggregators. Log level defaults to `warn`; override with `LOG_LEVEL` env in `.env` (`debug` / `info` / `warn` / `error`). At the default `warn`, pino-http's 2xx/3xx request lines are filtered out (they log at level `info`); 4xx become visible warns and 5xx errors. No `NODE_ENV` dependency — the project doesn't use that variable anywhere else.
- **Bare `console.*` is banned** in backend code — it bypasses level filtering and dumps unstructured text into the prod log stream. Frontend code (`frontend/`) is unaffected; browser code keeps using `console.*`.
- **Pino's first-arg-is-context convention.** Errors: `logger.error({ err: error, ip, ... }, 'short message')`. Pino has a built-in serializer for the `err` key that formats stack traces nicely.
- **Startup-only lines (called once at boot)** lead with an emoji for at-a-glance scanning when the dev terminal is busy: 🚀 listening, 📦 ready, 📥 downloading, 🛡️ security on, 🐢 throttling on, 🗓️ schedule, ⚠️ recoverable warning, ❌ failure. Per-request and per-handler logs stay plain.
- **HTTP request logging** is **off by default** to keep pm2 logs from bloating. Set `LOG_HTTP=true` in `.env` to mount `pino-http` on `/api`; when on, 2xx/3xx log as `info`, 4xx as `warn`, 5xx as `error`. Handlers never log incoming requests themselves — they log domain-specific events / errors only, regardless of this flag.

## Testing

- **Test runner:** Node built-in (`node --test`), no third-party framework. Specs live in `tests/*.test.js`.
Expand All @@ -102,6 +111,7 @@ The backend enforces access control and timeouts through shared middleware rathe

## Workflow

- **Branch discipline — `dev` in, `dev` out.** All work starts from `dev` and lands on `dev`. `main` is only updated via PRs that merge `dev` → `main`; never base a branch on `main`, never push directly to `main`. When an AI assistant operates from a worktree and needs to fast-forward `dev`, use `git push . HEAD:dev` (the repo has `receive.denyCurrentBranch=updateInstead` set, so git syncs the main worktree's files too when it's clean) rather than `git update-ref`, which leaves the main worktree's files out of sync with HEAD.
- **Do not commit without explicit user approval.** The flow is: AI edits → user reviews → user tests → user says "commit" → AI commits. Silent commits are a breach of trust.
- **One concern per commit.** Don't mix unrelated changes into a single commit. Split at the right seam.
- **Self-test before handing off.** Run `npm run check` (or at least `npm test`) for every change. If the change is visual (UI layout, styling, interactions) and can't be verified headless, say so explicitly so the user can test it in `npm run dev`.
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ Download `GeoLite2-City.mmdb` and `GeoLite2-ASN.mmdb` from your MaxMind account
| `SECURITY_RATE_LIMIT` | No | `"0"` | Controls the number of requests an IP can make to the backend server every 60 minutes (set to 0 for no limit) |
| `SECURITY_DELAY_AFTER` | No | `"0"` | Controls the first X requests from an IP every 20 minutes that are not subject to speed limits, and after X requests, the delay will increase |
| `SECURITY_BLACKLIST_LOG_FILE_PATH` | No | `"logs/blacklist-ip.log"` | Path setting. Records the list of IPs that triggered the limit after SECURITY_RATE_LIMIT is enabled |
| `LOG_LEVEL` | No | `"info"` | Minimum log level (`debug` / `info` / `warn` / `error`). Lower-level messages are suppressed. |
| `LOG_FORMAT` | No | pretty | Set to `"json"` to emit one JSON event per line (for log aggregators / jq). Any other value (or unset) keeps the colored pretty output used in dev and pm2 log tails. |
| `LOG_HTTP` | No | `"false"` | Set to `"true"` to enable per-request HTTP logging on `/api/*` (method, URL, status, response time). Off by default to keep pm2 logs lean. Handler-level 4xx/5xx errors are always logged regardless of this flag. |
| `ALLOWED_DOMAINS` | No | `""` | Allowed domains for access, separated by commas, used to prevent misuse of the backend API |
| `GOOGLE_MAP_API_KEY` | No | `""` | API Key for Google Maps, used to display the location of the IP on a map |
| `IPCHECKING_API_ENDPOINT` | No | `""` | API endpoint for IPCheck.ing database, used to obtain accurate IP geolocation information |
Expand Down
3 changes: 3 additions & 0 deletions README_FR.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ Téléchargez `GeoLite2-City.mmdb` et `GeoLite2-ASN.mmdb` depuis votre compte Ma
| `SECURITY_RATE_LIMIT` | Non | `"0"` | Contrôle le nombre de requêtes qu'une adresse IP peut faire au serveur backend toutes les 60 minutes (réglé sur 0 pour aucune limite) |
| `SECURITY_DELAY_AFTER` | Non | `"0"` | Contrôle les premières X requêtes d'une adresse IP toutes les 20 minutes qui ne sont pas soumises à des limites de vitesse, et après X requêtes, le délai augmentera |
| `SECURITY_BLACKLIST_LOG_FILE_PATH` | Non | `"logs/blacklist-ip.log"` | Paramètre de chemin. Enregistre la liste des adresses IP qui ont déclenché la limite après que `SECURITY_RATE_LIMIT` soit activé |
| `LOG_LEVEL` | Non | `"info"` | Niveau minimum des journaux (`debug` / `info` / `warn` / `error`). Les messages de niveau inférieur sont supprimés. |
| `LOG_FORMAT` | Non | pretty | Définir sur `"json"` pour émettre un événement JSON par ligne (agrégateurs de logs / jq). Toute autre valeur (ou non défini) conserve la sortie colorée lisible utilisée en dev et lors du tail des logs pm2. |
| `LOG_HTTP` | Non | `"false"` | Définir sur `"true"` pour activer la journalisation par requête HTTP sur `/api/*` (méthode, URL, statut, temps de réponse). Désactivé par défaut pour garder les logs pm2 légers. Les erreurs 4xx/5xx côté handler sont toujours loguées, que ce drapeau soit activé ou non. |
| `ALLOWED_DOMAINS` | Non | `""` | Domaines autorisés pour l'accès, séparés par des virgules, utilisés pour empêcher une utilisation abusive de l'API backend |
| `GOOGLE_MAP_API_KEY` | Non | `""` | Clé API pour Google Maps, utilisée pour afficher l'emplacement de l'adresse IP sur une carte |
| `IPCHECKING_API_ENDPOINT` | Non | `""` | endpoint de l'API pour IPCheck.ing database, utilisée pour obtenir des informations de géolocalisation précises sur l'adresse IP |
Expand Down
3 changes: 3 additions & 0 deletions README_TR.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ MaxMind hesabınızdan `GeoLite2-City.mmdb` ve `GeoLite2-ASN.mmdb` dosyalarını
| `SECURITY_RATE_LIMIT` | Hayır | `"0"` | Bir IP'nin backend sunucusuna 60 dakikada yapabileceği istek sayısını kontrol eder (sınır yok için 0) |
| `SECURITY_DELAY_AFTER` | Hayır | `"0"` | 20 dakikada bir IP'den gelen ilk X isteğin hız sınırına tabi olmadığını kontrol eder; X'ten sonra gecikme artar |
| `SECURITY_BLACKLIST_LOG_FILE_PATH` | Hayır | `"logs/blacklist-ip.log"` | Yol ayarı. SECURITY_RATE_LIMIT etkinleştirildiğinde limit tetikleyen IP'leri kaydeder |
| `LOG_LEVEL` | Hayır | `"info"` | Minimum log seviyesi (`debug` / `info` / `warn` / `error`). Daha düşük seviyedeki mesajlar bastırılır. |
| `LOG_FORMAT` | Hayır | pretty | `"json"` olarak ayarlandığında satır başına bir JSON olayı çıkarır (log toplayıcılar / jq için). Diğer değerler (veya ayarlanmamışsa) dev ortamında ve pm2 log tail sırasında kullanılan renkli güzel biçimli çıktıyı korur. |
| `LOG_HTTP` | Hayır | `"false"` | `"true"` yapıldığında `/api/*` üzerinde istek başı HTTP loglamasını etkinleştirir (metod, URL, durum, yanıt süresi). pm2 loglarını küçük tutmak için varsayılan olarak kapalıdır. Bu bayrak kapalı olsa bile handler düzeyindeki 4xx/5xx hataları her zaman loglanır. |
| `ALLOWED_DOMAINS` | Hayır | `""` | Erişime izin verilen alan adları, virgülle ayrılmış; backend API kötüye kullanımını önlemek için kullanılır |
| `GOOGLE_MAP_API_KEY` | Hayır | `""` | IP'nin konumunu haritada göstermek için Google Maps API Anahtarı |
| `IPCHECKING_API_ENDPOINT` | Hayır | `""` | IPCheck.ing veritabanı API uç noktası, doğru IP konum bilgisi almak için |
Expand Down
3 changes: 3 additions & 0 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ MyIP 依赖 MaxMind 提供的免费 **GeoLite2** 数据库(City + ASN)来进
| `SECURITY_RATE_LIMIT` | 否 | `"0"` | 控制每 60 分钟一个 IP 可以对后端服务器请求的次数(设置为 0 则为不限制) |
| `SECURITY_DELAY_AFTER` | 否 | `"0"` | 控制每 20 分钟一个 IP 的前 X 次请求不受速度限制,超过 X 次后会逐次增加延迟 |
| `SECURITY_BLACKLIST_LOG_FILE_PATH` | 否 | `"logs/blacklist-ip.log"` | 路径设置。记录由 SECURITY_RATE_LIMIT 开启后,触发限制的 IP 列表 |
| `LOG_LEVEL` | 否 | `"info"` | 日志最低级别(`debug` / `info` / `warn` / `error`),低于该级别的日志会被过滤 |
| `LOG_FORMAT` | 否 | pretty | 设置为 `"json"` 时每行输出一个 JSON 事件(给日志聚合器 / jq 使用);其它值(或未设置)则使用带颜色的 pretty 格式,适合开发及 pm2 log tail |
| `LOG_HTTP` | 否 | `"false"` | 设置为 `"true"` 时启用 `/api/*` 的按请求日志(方法、URL、状态码、响应时间)。默认关闭以控制 pm2 日志体积。即使关闭,handler 层的 4xx/5xx 错误日志依然会被记录 |
| `ALLOWED_DOMAINS` | 否 | `""` | 允许访问的域名,用逗号分隔,用于防止后端 API 被滥用 |
| `GOOGLE_MAP_API_KEY` | 否 | `""` | Google 地图的 API Key,用于展示 IP 所在地的地图 |
| `IPCHECKING_API_ENDPOINT` | 否 | `""` | IPCheck.ing 数据库的 API 端点 URL |
Expand Down
13 changes: 10 additions & 3 deletions api/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,25 @@ api/
│ ipcheck-ing.js, maxmind.js ← IP geolocation source handlers (route per source)
├── invisibility-test.js ← /api/invisibility — proxy to private IPCheck.ing endpoint
├── mac-checker.js ← /api/macchecker — MAC vendor lookup
├── get-whois.js ← /api/whois — whoiser wrapper
├── get-whois.js ← /api/whois — whoiser primary + RDAP fallback for new gTLDs
├── cf-radar.js ← /api/cfradar — ASN details via Cloudflare Radar
├── dns-resolver.js ← /api/dnsresolver — DNS + DoH parallel query
├── dns-leak-test.js ← /api/dnsleaktest/session/:token — proxy to private
│ IPCheck.ing endpoint (Firebase-gated) that drives the
│ in-depth DNS Leak Test advanced tool
├── get-user-info.js ← /api/getuserinfo — user-profile proxy
└── update-user-achievement.js ← /api/updateuserachievement — user-achievement proxy

common/
├── fetch-with-timeout.js ← fetchWithTimeout (5s default) + fetchUpstream (8s preset)
├── guards.js ← requireReferer + requireValidIP Express middleware
├── logger.js ← shared pino logger (pretty in dev, JSON in prod)
├── referer-check.js ← low-level referer allow-list check
├── valid-ip.js ← IPv4 / IPv6 validator (also re-exported from frontend)
├── rdap.js ← RDAP client (domain fallback when whoiser returns no __raw)
├── maxmind-service.js ← mmdb reader + lookup
├── maxmind-updater.js ← scheduled mmdb auto-update
├── maxmind-updater.js ← mmdb bootstrap download at boot +
│ scheduled auto-update
└── maxmind-db/ ← GeoLite2-ASN.mmdb + GeoLite2-City.mmdb
```

Expand All @@ -54,6 +60,7 @@ common/
Default timeout is 8s. Never add a bare `fetch()` or `https.get()` — if a provider hangs, the Express connection should time out, not pin indefinitely.
- **Error shape.** `res.status(500).json({ error: error.message })` on upstream failures; `res.status(400).json({ error: '…' })` on bad input. Be terse — the frontend doesn't display these error strings verbatim.
- **Response shape.** IP-geolocation handlers normalize their upstream's response into the canonical shape consumed by the frontend (`ip` / `country` / `country_name` / `country_code` / `latitude` / `longitude` / `asn` / `org` / …). If you add a new source, match the existing shape.
- **Logging.** `import logger from '../common/logger.js'` and use `logger.error({ err: error, ...context }, 'short message')` on upstream failures, never bare `console.*` (banned project-wide for backend code — see root AGENTS.md "Logging"). The `pino-http` middleware mounted on `/api` already records the request line + status + response time, so handlers should only log domain-specific events / errors, not "received request" lines.

## Security & Boundaries

Expand All @@ -65,7 +72,7 @@ common/

### Private-API header pass-through (intentional exception)

Handlers that call our own private IPCheck.ing API (`ipcheck-ing.js`, `invisibility-test.js`, `update-user-achievement.js`, `get-user-info.js`) forward the caller's request headers to the upstream:
Handlers that call our own private IPCheck.ing API (`ipcheck-ing.js`, `invisibility-test.js`, `update-user-achievement.js`, `get-user-info.js`, `dns-leak-test.js`) forward the caller's request headers to the upstream:

```js
const apiResponse = await fetchUpstream(url, { headers: { ...req.headers } });
Expand Down
15 changes: 8 additions & 7 deletions api/cf-radar.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fetchUpstream } from '../common/fetch-with-timeout.js';
import logger from '../common/logger.js';

// Common fetch request function
async function fetchFromCloudflare(endpoint) {
Expand All @@ -17,7 +18,7 @@ async function getASNInfo(asn) {
try {
return await fetchFromCloudflare(`/radar/entities/asns/${asn}`);
} catch (error) {
console.error(error);
logger.error({ err: error }, 'Failed to fetch ASN info');
throw new Error('Failed to fetch ASN info');
}
};
Expand All @@ -27,7 +28,7 @@ async function getASNIPVersion(asn) {
try {
return await fetchFromCloudflare(`/radar/http/summary/ip_version?asn=${asn}&dateRange=7d`);
} catch (error) {
console.error(error);
logger.error({ err: error }, 'Failed to fetch ASN IP version');
throw new Error('Failed to fetch ASN IP version');
}
};
Expand All @@ -37,7 +38,7 @@ async function getASNHTTPProtocol(asn) {
try {
return await fetchFromCloudflare(`/radar/http/summary/http_protocol?asn=${asn}&dateRange=7d`);
} catch (error) {
console.error(error);
logger.error({ err: error }, 'Failed to fetch ASN HTTP protocol');
throw new Error('Failed to fetch ASN HTTP protocol');
}
};
Expand All @@ -47,7 +48,7 @@ async function getASNDeviceType(asn) {
try {
return await fetchFromCloudflare(`/radar/http/summary/device_type?asn=${asn}&dateRange=7d`);
} catch (error) {
console.error(error);
logger.error({ err: error }, 'Failed to fetch ASN device type');
throw new Error('Failed to fetch ASN device type');
}
};
Expand All @@ -57,7 +58,7 @@ async function getASNBotType(asn) {
try {
return await fetchFromCloudflare(`/radar/http/summary/bot_class?asn=${asn}&dateRange=7d`);
} catch (error) {
console.error(error);
logger.error({ err: error }, 'Failed to fetch ASN bot type');
throw new Error('Failed to fetch ASN bot type');
}
};
Expand All @@ -74,7 +75,7 @@ async function getAllASNData(asn) {
]);
return { asnInfo, ipVersion, httpProtocol, deviceType, botType };
} catch (error) {
console.error(error);
logger.error({ err: error }, 'Failed to fetch all ASN data');
throw new Error('Failed to fetch all ASN data');
}
}
Expand Down Expand Up @@ -155,7 +156,7 @@ export default async (req, res) => {

res.json(finalResponse);
} catch (error) {
console.error(error);
logger.error({ err: error, asn }, 'cf-radar handler failed');
res.status(500).json({ error: 'Internal server error' });
}
}
54 changes: 54 additions & 0 deletions api/dns-leak-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Enhanced DNS leak detection — thin proxy to the main IPCheck.ing API.
//
// - validates the 32-hex token locally (fail fast before any network hop),
// - forwards request headers (notably Authorization: Bearer <Firebase ID>),
// - attaches the apikey query param,
// - passes the upstream status + JSON back to the caller verbatim so the
// frontend can surface "Sign in required" / "Invalid token" etc.
//
// GET /api/dnsleaktest/session/:token

import { fetchUpstream } from '../common/fetch-with-timeout.js';
import logger from '../common/logger.js';

const TOKEN_RE = /^[0-9a-f]{32}$/;
const SUPPORTED_LANGS = ['zh-CN', 'en', 'fr', 'tr'];

export async function getSessionResult(req, res) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method Not Allowed' });
}

const token = req.params?.token;
if (!token || !TOKEN_RE.test(token)) {
return res.status(400).json({ error: 'Invalid token' });
}

const apiKey = process.env.IPCHECKING_API_KEY;
const apiEndpoint = process.env.IPCHECKING_API_ENDPOINT;
if (!apiKey || !apiEndpoint) {
return res.status(500).json({ error: 'API key is missing' });
}

const lang = SUPPORTED_LANGS.includes(req.query.lang) ? req.query.lang : 'zh-CN';

const url = new URL(`${apiEndpoint}/dnsleaktest/session/${token}`);
url.searchParams.set('apikey', apiKey);
url.searchParams.set('lang', lang);

try {
const apiResponse = await fetchUpstream(url, {
headers: { ...req.headers },
});

// Parse as JSON if we can, otherwise just keep an empty object so the
// client still gets a proper status code.
const data = await apiResponse.json().catch(() => ({}));

res.set('Cache-Control', 'no-store');
res.status(apiResponse.status).json(data);
} catch (error) {
logger.error({ err: error }, 'dnsleaktest upstream fetch failed');
res.status(502).json({ error: 'Upstream fetch failed', detail: error.message });
}
}
Loading
Loading