diff --git a/.env.example b/.env.example index 7e6a6d441..f8eca55d0 100644 --- a/.env.example +++ b/.env.example @@ -25,6 +25,9 @@ MAXMIND_AUTO_UPDATE="false" CAIDA_AUTO_UPDATE="false" # SITE VITE_SITE_URL="https://ipcheck.ing" +# DOCS SITE — origin of the GitBook docs site powering the in-app +# assistant and the Help Center link. Empty disables both. +VITE_DOCS_URL="" # CURL API VITE_CURL_IPV4_DOMAIN="" VITE_CURL_IPV6_DOMAIN="" diff --git a/README.md b/README.md index fc6f4f5e5..8e113501f 100644 --- a/README.md +++ b/README.md @@ -92,111 +92,33 @@ Click the 'Deploy to Docker' button at the top to complete the deployment. Or, u docker run -d -p 18966:18966 --name myip --restart always jason5ng32/myip:latest ``` -## 📚 Environment Variable - -Variables marked **Yes** below must be set for the backend to function correctly. The MaxMind credentials in particular are required — read the setup notes in the next subsection before filling in the table. - -### MaxMind Databases (Required) - -MyIP relies on the free **GeoLite2** databases from MaxMind (City + ASN) for IP geolocation, ASN / organization lookup, and the country-code badges that appear throughout the app (IP cards, WebRTC ICE candidates, and more). A working MaxMind setup is required for the backend to serve a complete experience. - -The `.mmdb` files are **not checked into this repository** because MaxMind's GeoLite2 license does not allow redistribution. You need to provide them yourself. There are two paths: - -**Option A — Automatic (recommended, required for Docker)** - -1. Create a free account at [maxmind.com/en/geolite2/signup](https://www.maxmind.com/en/geolite2/signup). -2. Generate a license key from your account's "Manage License Keys" page. -3. Set these three environment variables: - ```bash - MAXMIND_ACCOUNT_ID="your-account-id" - MAXMIND_LICENSE_KEY="your-license-key" - MAXMIND_AUTO_UPDATE="true" - ``` -4. Start the backend. Within about 60 seconds of the first startup, the updater will download both databases. They are then refreshed every 24 hours automatically. - -> ⚠️ **Docker deployers must use Option A.** A fresh container ships with an empty `common/maxmind-db/` directory — without the three variables above the backend starts, but the MaxMind-powered IP source and WebRTC country badges will not work, and you'll see `MaxMind API will return 503...` in the logs on every boot. - -**Option B — Manual (for air-gapped or non-Docker setups)** - -Download `GeoLite2-City.mmdb` and `GeoLite2-ASN.mmdb` from your MaxMind account and drop them into `common/maxmind-db/` before starting the backend. With this approach `MAXMIND_AUTO_UPDATE` can stay `"false"`, but you'll need to refresh the files manually as MaxMind publishes new versions. - -### Environment variables list - -| Variable Name | Required | Default Value | Description | -| --- | --- | --- | --- | -| `MAXMIND_ACCOUNT_ID` | **Yes** | `""` | MaxMind account ID, paired with `MAXMIND_LICENSE_KEY` to download GeoLite2 databases. See the MaxMind section above. | -| `MAXMIND_LICENSE_KEY` | **Yes** | `""` | MaxMind license key, paired with `MAXMIND_ACCOUNT_ID`. See the MaxMind section above. | -| `MAXMIND_AUTO_UPDATE` | **Yes** | `"false"` | Set to `"true"` to auto-download GeoLite2 databases ~60s after startup and refresh every 24h. **Required for Docker.** Can stay `"false"` only if you've pre-seeded the `.mmdb` files manually. | -| `CAIDA_AUTO_UPDATE` | No | `"false"` | Set to `"true"` to refresh the CAIDA datasets daily (as2org for ASN org-name lookup, as-rel2 for the ASN connectivity graph). When `"false"`, missing snapshots are still downloaded at startup but never refreshed afterwards. | -| `VITE_GOOGLE_ANALYTICS_ID` | **Yes** | `""` | Google Analytics ID, used to track user behavior | -| `BACKEND_PORT` | No | `"11966"` | The running port of the backend part of the program | -| `FRONTEND_PORT` | No | `"18966"` | The running port of the frontend part of the program | -| `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 | `""` | Opt-in on-disk ledger of rate-limited IPs (e.g. `"logs/blacklist-ip.log"`). Empty = no file is written; the event is always logged via the shared logger either way | -| `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. | -| `VITE_SENTRY_DSN_FRONTEND` | No | `""` | Sentry DSN for the frontend (build-time). When empty, no Sentry code is included in the bundle at all. Also read by the backend at runtime as the allowlist for `/api/monitoring`, the first-party tunnel that relays Sentry envelopes past ad blockers. If you bake it into a self-built Docker image at build time, pass the same value to the container at runtime too — otherwise the tunnel route stays disabled | -| `SENTRY_DSN_BACKEND` | No | `""` | Sentry DSN for the backend (runtime). When empty, the Sentry SDK is never loaded | -| `SENTRY_ENVIRONMENT` | No | `"production"` | Environment tag on backend Sentry events. Set to `"development"` on dev machines; the frontend tags itself automatically | -| `SENTRY_ORG` | No | `""` | Sentry organization slug, used with `SENTRY_PROJECT_FRONTEND` and `SENTRY_AUTH_TOKEN` to upload source maps at build time | -| `SENTRY_PROJECT_FRONTEND` | No | `""` | Sentry project slug of the frontend project, for build-time source map upload | -| `SENTRY_AUTH_TOKEN` | No | `""` | Sentry auth token enabling source map upload at build time. Build-time secret only — never exposed to the browser | -| `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 | -| `IPINFO_API_KEY` | No | `""` | API Token for IPInfo.io, used to obtain IP geolocation information through IPInfo.io | -| `IPAPIIS_API_KEY` | No | `""` | API Key for IPAPI.is, used to obtain IP geolocation information through IPAPI.is | -| `IP2LOCATION_API_KEY` | No | `""` | API Key for IP2Location.io, used to obtain IP geolocation information through IP2Location.io | -| `CLOUDFLARE_API_KEY` | No | `""` | API Key for Cloudflare, used to obtain AS system information; with the two KV variables below (and the "Workers KV Storage: Edit" token permission) it also powers shareable diagnostic reports | -| `CLOUDFLARE_ACCOUNT_ID` | No | `""` | Cloudflare account ID, required for storing shareable diagnostic reports in Workers KV | -| `CLOUDFLARE_KV_NAMESPACE_ID` | No | `""` | Hex ID (not the name) of the Workers KV namespace that stores shareable diagnostic reports | -| `RIPESTAT_SOURCE_APP` | No | `""` | Source app name for RIPE.net, used to obtain ASN history information through RIPE.net | -| `MAC_LOOKUP_API_KEY` | No | `""` | API Key for MAC Lookup, used to obtain MAC address information | -| `VITE_CURL_IPV4_DOMAIN` | No | `""` | Provides the IPv4 domain for the CURL API to users | -| `VITE_CURL_IPV6_DOMAIN` | No | `""` | Provides the IPv6 domain for the CURL API to users | -| `VITE_CURL_IPV64_DOMAIN` | No | `""` | Provides the dual-stack domain for the CURL API to users | - -Note that if any of the CURL series environment variables are missing, the CURL API will not be enabled. - -### Using Environment Variables in a Node Environment - -Create environment variables: +## 📖 Documentation -```bash -cp .env.example .env -``` - -Modify `.env`, and for example, add the following: +Full guides live in the MyIP Docs Center: **[docs.ipcheck.ing](https://docs.ipcheck.ing)** -```bash -BACKEND_PORT=11966 -FRONTEND_PORT=18966 -MAXMIND_ACCOUNT_ID="YOUR_ACCOUNT_ID" -MAXMIND_LICENSE_KEY="YOUR_LICENSE_KEY" -MAXMIND_AUTO_UPDATE="true" -GOOGLE_MAP_API_KEY="YOUR_KEY_HERE" -ALLOWED_DOMAINS="example.com,example.org" -``` +* [Developer Guide](https://docs.ipcheck.ing/developer) — deployment, configuration, architecture, and contributing +* [Knowledge Base](https://docs.ipcheck.ing/knowledge-base) — how to use every tool, step-by-step network diagnosis, and networking concepts -Then restart the backend service. +## ⚙️ Configuration -### Using Environment Variables in Docker +Two settings matter before anything else: -You can add environment variables when running Docker, for example: +* **MaxMind GeoLite2 (required)** — free credentials that power IP geolocation and ASN lookups. Without them, the MaxMind source returns 503. → [MaxMind Setup](https://docs.ipcheck.ing/developer/getting-started/maxmind-setup) +* **`ALLOWED_DOMAINS` (required on a real domain)** — hostname allowlist for the backend API. Without it, every request from a non-localhost domain gets 403. → [Reverse Proxy & Domains](https://docs.ipcheck.ing/developer/getting-started/reverse-proxy-and-domains) ```bash docker run -d -p 18966:18966 \ -e MAXMIND_ACCOUNT_ID="YOUR_ACCOUNT_ID" \ -e MAXMIND_LICENSE_KEY="YOUR_LICENSE_KEY" \ -e MAXMIND_AUTO_UPDATE="true" \ - -e GOOGLE_MAP_API_KEY="YOUR_KEY_HERE" \ - -e ALLOWED_DOMAINS="example.com,example.org" \ - --name myip \ + -e ALLOWED_DOMAINS="your-domain.com" \ + --name myip --restart always \ jason5ng32/myip:latest - ``` +Everything else — optional API keys, security & rate limiting, logging, Sentry, the curl API domains — is documented in the [Environment Variables reference](https://docs.ipcheck.ing/developer/reference/environment-variables). + + ## 👩🏻‍💻 Advanced Usage If you're using a proxy for internet access, consider adding this rule to your proxy configuration (modify it according to your client). This setup lets you check both your real IP and the IP when using the proxy: @@ -230,4 +152,6 @@ As a open source project, I'm very grateful to the following sponsors for their Sentry +GitBook + Cloudflare Project Alexandria diff --git a/README_FR.md b/README_FR.md index 40b2eb533..3d28e6a75 100644 --- a/README_FR.md +++ b/README_FR.md @@ -92,111 +92,33 @@ Cliquez sur le bouton 'Déployer sur Docker' en haut pour terminer le déploieme docker run -d -p 18966:18966 --name myip --restart always jason5ng32/myip:latest ``` -## 📚 Variables d'environnement - -Les variables marquées **Oui** ci-dessous doivent être définies pour que le backend fonctionne correctement. Les identifiants MaxMind sont particulièrement importants — lisez les notes de configuration ci-dessous avant de remplir le tableau. - -### Bases de données MaxMind (requis) - -MyIP s'appuie sur les bases **GeoLite2** gratuites de MaxMind (City + ASN) pour la géolocalisation IP, la recherche ASN / organisation, et les badges de code pays qui apparaissent partout dans l'application (cartes IP, candidats ICE WebRTC, etc.). Une configuration MaxMind fonctionnelle est nécessaire pour que le backend offre une expérience complète. - -Les fichiers `.mmdb` ne sont **pas inclus dans ce dépôt** car la licence GeoLite2 de MaxMind interdit la redistribution. Vous devez les fournir vous-même. Deux options : - -**Option A — Automatique (recommandée, obligatoire pour Docker)** - -1. Créez un compte gratuit sur [maxmind.com/en/geolite2/signup](https://www.maxmind.com/en/geolite2/signup). -2. Générez une clé de licence depuis la page « Manage License Keys » de votre compte. -3. Définissez ces trois variables d'environnement : - ```bash - MAXMIND_ACCOUNT_ID="your-account-id" - MAXMIND_LICENSE_KEY="your-license-key" - MAXMIND_AUTO_UPDATE="true" - ``` -4. Démarrez le backend. Environ 60 secondes après le premier démarrage, l'updater téléchargera les deux bases, puis les rafraîchira automatiquement toutes les 24 heures. - -> ⚠️ **Les déploiements Docker doivent utiliser l'option A.** Un conteneur neuf est livré avec un répertoire `common/maxmind-db/` vide — sans les trois variables ci-dessus, le backend démarre mais la source IP basée sur MaxMind et les badges de pays WebRTC ne fonctionneront pas, et vous verrez `MaxMind API will return 503...` dans les journaux à chaque démarrage. - -**Option B — Manuelle (environnements isolés ou non-Docker)** - -Téléchargez `GeoLite2-City.mmdb` et `GeoLite2-ASN.mmdb` depuis votre compte MaxMind et placez-les dans `common/maxmind-db/` avant de démarrer le backend. Dans ce cas, `MAXMIND_AUTO_UPDATE` peut rester à `"false"`, mais vous devrez rafraîchir les fichiers manuellement à chaque nouvelle version publiée par MaxMind. - -### Liste des variables d'environnement - -| Nom de la variable | Requis | Valeur par défaut | Description | -| --- | --- | --- | --- | -| `MAXMIND_ACCOUNT_ID` | **Oui** | `""` | ID de compte MaxMind, associé à `MAXMIND_LICENSE_KEY` pour télécharger les bases GeoLite2. Voir la section MaxMind ci-dessus. | -| `MAXMIND_LICENSE_KEY` | **Oui** | `""` | Clé de licence MaxMind, associée à `MAXMIND_ACCOUNT_ID`. Voir la section MaxMind ci-dessus. | -| `MAXMIND_AUTO_UPDATE` | **Oui** | `"false"` | Définissez sur `"true"` pour télécharger automatiquement les bases GeoLite2 environ 60 s après le démarrage et les rafraîchir toutes les 24 h. **Obligatoire pour Docker.** Peut rester à `"false"` uniquement si vous avez pré-déposé les fichiers `.mmdb` manuellement. | -| `CAIDA_AUTO_UPDATE` | Non | `"false"` | Définissez sur `"true"` pour rafraîchir quotidiennement les jeux de données CAIDA (as2org pour la résolution du nom d'organisation par ASN, as-rel2 pour le graphe de connectivité ASN). Lorsque `"false"`, les snapshots manquants sont quand même téléchargés au démarrage mais ne sont jamais rafraîchis ensuite. | -| `VITE_GOOGLE_ANALYTICS_ID` | **Oui** | `""` | Identifiant Google Analytics, utilisé pour l'analyse des utilisateurs | -| `BACKEND_PORT` | Non | `"11966"` | Le port d'exécution de la partie backend du programme | -| `FRONTEND_PORT` | Non | `"18966"` | Le port d'exécution de la partie frontend du programme | -| `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 | `""` | Registre local optionnel des IP limitées (ex. `"logs/blacklist-ip.log"`). Vide = aucun fichier n'est écrit ; l'événement est de toute façon toujours journalisé via le logger partagé | -| `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. | -| `VITE_SENTRY_DSN_FRONTEND` | Non | `""` | DSN Sentry du frontend (au moment du build). Si vide, aucun code Sentry n'est inclus dans le bundle. Également lu par le backend à l'exécution comme liste blanche pour `/api/monitoring`, le tunnel first-party qui fait passer les enveloppes Sentry malgré les bloqueurs de publicité. Si vous l'intégrez dans une image Docker auto-construite au moment du build, passez aussi la même valeur au conteneur à l'exécution — sinon la route du tunnel reste désactivée | -| `SENTRY_DSN_BACKEND` | Non | `""` | DSN Sentry du backend (à l'exécution). Si vide, le SDK Sentry n'est jamais chargé | -| `SENTRY_ENVIRONMENT` | Non | `"production"` | Étiquette d'environnement des événements Sentry du backend. Définir sur `"development"` sur les machines de développement ; le frontend s'étiquette automatiquement | -| `SENTRY_ORG` | Non | `""` | Slug de l'organisation Sentry, utilisé avec `SENTRY_PROJECT_FRONTEND` et `SENTRY_AUTH_TOKEN` pour téléverser les source maps au moment du build | -| `SENTRY_PROJECT_FRONTEND` | Non | `""` | Slug du projet Sentry du frontend, pour le téléversement des source maps au moment du build | -| `SENTRY_AUTH_TOKEN` | Non | `""` | Jeton Sentry activant le téléversement des source maps au moment du build. Secret de build uniquement — jamais exposé au navigateur | -| `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 | -| `IPINFO_API_KEY` | Non | `""` | Jeton API pour IPInfo.io, utilisé pour obtenir des informations de géolocalisation sur l'adresse IP via IPInfo.io | -| `IPAPIIS_API_KEY` | Non | `""` | Clé API pour IPAPI.is, utilisée pour obtenir des informations de géolocalisation sur l'adresse IP via IPAPI.is | -| `IP2LOCATION_API_KEY` | Non | `""` | Clé API pour IP2Location.io, utilisée pour obtenir des informations de géolocalisation sur l'adresse IP via IP2Location.io | -| `CLOUDFLARE_API_KEY` | Non | `""` | Clé API pour Cloudflare, utilisée pour les informations AS ; avec les deux variables KV ci-dessous (et la permission « Workers KV Storage: Edit »), elle alimente aussi le partage de rapports de diagnostic | -| `CLOUDFLARE_ACCOUNT_ID` | Non | `""` | ID du compte Cloudflare, requis pour stocker les rapports de diagnostic partageables dans Workers KV | -| `CLOUDFLARE_KV_NAMESPACE_ID` | Non | `""` | ID hexadécimal (pas le nom) de l'espace de noms Workers KV qui stocke les rapports de diagnostic partageables | -| `RIPESTAT_SOURCE_APP` | Non | `""` | Nom de l'application source pour RIPE.net, utilisé pour obtenir des informations sur l'historique ASN via RIPE.net | -| `MAC_LOOKUP_API_KEY` | Non | `""` | Clé API pour MAC Lookup, utilisée pour obtenir des informations sur l'adresse MAC via MAC Lookup | -| `VITE_CURL_IPV4_DOMAIN` | Non | `""` | Fournit aux utilisateurs le domaine IPv4 pour l'API CURL | -| `VITE_CURL_IPV6_DOMAIN` | Non | `""` | Fournit aux utilisateurs le domaine IPv6 pour l'API CURL | -| `VITE_CURL_IPV64_DOMAIN` | Non | `""` | Fournit aux utilisateurs le domaine à pile double pour l'API CURL | - -Il est à noter que si l'une quelconque des variables d'environnement de la série CURL est manquante, l'API CURL ne sera pas activée. - -### Utilisation des variables d'environnement dans un environnement Node - -Créez les variables d'environnement : +## 📖 Documentation -```bash -cp .env.example .env -``` - -Modifiez le fichier `.env`, et par exemple, ajoutez ce qui suit : +Les guides complets se trouvent dans le centre de documentation MyIP : **[docs.ipcheck.ing](https://docs.ipcheck.ing)** (sélecteur de langue en haut à droite) -```bash -BACKEND_PORT=11966 -FRONTEND_PORT=18966 -MAXMIND_ACCOUNT_ID="YOUR_ACCOUNT_ID" -MAXMIND_LICENSE_KEY="YOUR_LICENSE_KEY" -MAXMIND_AUTO_UPDATE="true" -GOOGLE_MAP_API_KEY="YOUR_KEY_HERE" -ALLOWED_DOMAINS="example.com,example.org" -``` +* [Guide du développeur](https://docs.ipcheck.ing/developer) — déploiement, configuration, architecture et contribution +* [Base de connaissances](https://docs.ipcheck.ing/knowledge-base) — utilisation de chaque outil, diagnostic réseau pas à pas, concepts réseau -Ensuite, redémarrez le service backend. +## ⚙️ Configuration -### Utilisation des variables d'environnement dans Docker +Deux réglages comptent avant tout : -Vous pouvez ajouter des variables d'environnement lors de l'exécution de Docker, par exemple : +* **MaxMind GeoLite2 (requis)** — identifiants gratuits pour la géolocalisation IP et les recherches ASN. Sans eux, la source MaxMind renvoie 503. → [Configuration MaxMind](https://docs.ipcheck.ing/developer/getting-started/maxmind-setup) +* **`ALLOWED_DOMAINS` (requis sur un vrai domaine)** — liste blanche de noms d'hôte pour l'API backend. Sans elle, toute requête venant d'un domaine autre que localhost reçoit un 403. → [Reverse proxy et domaines](https://docs.ipcheck.ing/developer/getting-started/reverse-proxy-and-domains) ```bash docker run -d -p 18966:18966 \ -e MAXMIND_ACCOUNT_ID="YOUR_ACCOUNT_ID" \ -e MAXMIND_LICENSE_KEY="YOUR_LICENSE_KEY" \ -e MAXMIND_AUTO_UPDATE="true" \ - -e GOOGLE_MAP_API_KEY="YOUR_KEY_HERE" \ - -e ALLOWED_DOMAINS="example.com,example.org" \ - --name myip \ + -e ALLOWED_DOMAINS="your-domain.com" \ + --name myip --restart always \ jason5ng32/myip:latest - ``` +Tout le reste — clés API optionnelles, sécurité et limitation de débit, journalisation, Sentry, domaines de l'API curl — est documenté dans la [référence des variables d'environnement](https://docs.ipcheck.ing/developer/reference/environment-variables). + + ## 👩🏻‍💻 Utilisation avancée Si vous utilisez un proxy pour accéder à Internet, envisagez d'ajouter cette règle à votre configuration de proxy (modifiez-la en fonction de votre client). Cette configuration vous permet de vérifier à la fois votre véritable adresse IP et l'adresse IP lorsque vous utilisez le proxy : @@ -230,4 +152,6 @@ En tant que projet open source, je suis très reconnaissant aux sponsors suivant Sentry +GitBook + Cloudflare Project Alexandria diff --git a/README_RU.md b/README_RU.md index 5638a7485..460341867 100644 --- a/README_RU.md +++ b/README_RU.md @@ -92,111 +92,33 @@ pnpm start docker run -d -p 18966:18966 --name myip --restart always jason5ng32/myip:latest ``` -## 📚 Переменные окружения - -Переменные, отмеченные как **Да**, необходимо задать для корректной работы серверной части. В частности, обязательны учётные данные MaxMind — перед заполнением таблицы прочитайте инструкции в следующем разделе. - -### Базы данных MaxMind (обязательно) - -MyIP использует бесплатные базы данных **GeoLite2** от MaxMind (City + ASN) для геолокации IP, поиска ASN и организаций, а также отображения значков стран во всём приложении (в карточках IP, кандидатах WebRTC ICE и других местах). Для полноценной работы серверной части требуется настроенный MaxMind. - -Файлы `.mmdb` **не включены в репозиторий**, поскольку лицензия MaxMind GeoLite2 запрещает их распространение. Их необходимо получить самостоятельно. Доступны два способа: - -**Вариант A — автоматически (рекомендуется, обязателен для Docker)** - -1. Создайте бесплатную учётную запись на [maxmind.com/en/geolite2/signup](https://www.maxmind.com/en/geolite2/signup). -2. Создайте лицензионный ключ на странице «Manage License Keys» своей учётной записи. -3. Задайте три переменные окружения: - ```bash - MAXMIND_ACCOUNT_ID="your-account-id" - MAXMIND_LICENSE_KEY="your-license-key" - MAXMIND_AUTO_UPDATE="true" - ``` -4. Запустите серверную часть. Примерно через 60 секунд после первого запуска модуль обновления загрузит обе базы данных. Затем они будут автоматически обновляться каждые 24 часа. - -> ⚠️ **При развёртывании через Docker необходимо использовать вариант A.** Новый контейнер содержит пустой каталог `common/maxmind-db/`. Без трёх указанных переменных сервер запустится, но источник IP-данных MaxMind и значки стран WebRTC работать не будут, а при каждом запуске в журнале будет появляться сообщение `MaxMind API will return 503...`. - -**Вариант B — вручную (для изолированных сред и установок без Docker)** - -Загрузите `GeoLite2-City.mmdb` и `GeoLite2-ASN.mmdb` из своей учётной записи MaxMind и поместите их в `common/maxmind-db/` до запуска серверной части. При таком способе `MAXMIND_AUTO_UPDATE` может оставаться равной `"false"`, однако новые версии файлов MaxMind придётся загружать вручную. - -### Список переменных окружения - -| Имя переменной | Обязательно | Значение по умолчанию | Описание | -| --- | --- | --- | --- | -| `MAXMIND_ACCOUNT_ID` | **Да** | `""` | Идентификатор учётной записи MaxMind, используемый вместе с `MAXMIND_LICENSE_KEY` для загрузки баз GeoLite2. См. раздел MaxMind выше. | -| `MAXMIND_LICENSE_KEY` | **Да** | `""` | Лицензионный ключ MaxMind, используемый вместе с `MAXMIND_ACCOUNT_ID`. См. раздел MaxMind выше. | -| `MAXMIND_AUTO_UPDATE` | **Да** | `"false"` | Установите `"true"`, чтобы автоматически загрузить базы GeoLite2 примерно через 60 секунд после запуска и обновлять их каждые 24 часа. **Обязательно для Docker.** Значение `"false"` допустимо только при ручном предварительном размещении файлов `.mmdb`. | -| `CAIDA_AUTO_UPDATE` | Нет | `"false"` | Установите `"true"`, чтобы ежедневно обновлять наборы данных CAIDA (as2org для поиска названия организации ASN и as-rel2 для графа связности ASN). При `"false"` отсутствующие снимки всё равно загружаются при запуске, но в дальнейшем не обновляются. | -| `VITE_GOOGLE_ANALYTICS_ID` | **Да** | `""` | Идентификатор Google Analytics для анализа использования сайта. | -| `BACKEND_PORT` | Нет | `"11966"` | Порт серверной части приложения. | -| `FRONTEND_PORT` | Нет | `"18966"` | Порт клиентской части приложения. | -| `SECURITY_RATE_LIMIT` | Нет | `"0"` | Максимальное количество запросов от одного IP к серверу за 60 минут; `0` отключает ограничение. | -| `SECURITY_DELAY_AFTER` | Нет | `"0"` | Количество первых запросов от одного IP за 20 минут, для которых не применяется замедление; после превышения задержка увеличивается. | -| `SECURITY_BLACKLIST_LOG_FILE_PATH` | Нет | `""` | Необязательный файл для записи IP, ограниченных по частоте запросов (например, `"logs/blacklist-ip.log"`). Пустое значение отключает запись в файл; событие в любом случае попадает в общий журнал. | -| `LOG_LEVEL` | Нет | `"info"` | Минимальный уровень журналирования (`debug` / `info` / `warn` / `error`). Сообщения более низких уровней подавляются. | -| `LOG_FORMAT` | Нет | pretty | Установите `"json"`, чтобы выводить по одному JSON-событию на строку для систем сбора журналов или jq. Любое другое значение или отсутствие переменной сохраняет цветной формат, используемый при разработке и просмотре журналов pm2. | -| `LOG_HTTP` | Нет | `"false"` | Установите `"true"`, чтобы включить журналирование HTTP-запросов к `/api/*` (метод, URL, статус и время ответа). По умолчанию отключено, чтобы не перегружать журналы pm2. Ошибки 4xx/5xx обработчиков журналируются независимо от этого параметра. | -| `VITE_SENTRY_DSN_FRONTEND` | Нет | `""` | DSN Sentry для клиентской части на этапе сборки. При пустом значении код Sentry вообще не включается в пакет. Переменная также используется серверной частью во время выполнения как список разрешённых адресов для `/api/monitoring` — собственного туннеля, передающего пакеты Sentry в обход блокировщиков. Если значение встраивается в самостоятельно собранный Docker-образ, передайте его контейнеру и во время выполнения, иначе маршрут туннеля останется отключённым. | -| `SENTRY_DSN_BACKEND` | Нет | `""` | DSN Sentry для серверной части во время выполнения. При пустом значении SDK Sentry не загружается. | -| `SENTRY_ENVIRONMENT` | Нет | `"production"` | Метка окружения в событиях Sentry серверной части. На компьютерах разработчиков используйте `"development"`; клиентская часть определяет окружение автоматически. | -| `SENTRY_ORG` | Нет | `""` | Slug организации Sentry, используемый вместе с `SENTRY_PROJECT_FRONTEND` и `SENTRY_AUTH_TOKEN` для загрузки карт исходного кода при сборке. | -| `SENTRY_PROJECT_FRONTEND` | Нет | `""` | Slug проекта клиентской части в Sentry, используемый при загрузке карт исходного кода во время сборки. | -| `SENTRY_AUTH_TOKEN` | Нет | `""` | Токен Sentry, разрешающий загрузку карт исходного кода во время сборки. Это секрет только для этапа сборки — он никогда не передаётся браузеру. | -| `ALLOWED_DOMAINS` | Нет | `""` | Разрешённые домены, разделённые запятыми; используются для предотвращения злоупотребления серверным API. | -| `GOOGLE_MAP_API_KEY` | Нет | `""` | Ключ API Google Maps для отображения местоположения IP на карте. | -| `IPINFO_API_KEY` | Нет | `""` | Токен API IPInfo.io для получения геолокационных данных IP. | -| `IPAPIIS_API_KEY` | Нет | `""` | Ключ API IPAPI.is для получения геолокационных данных IP. | -| `IP2LOCATION_API_KEY` | Нет | `""` | Ключ API IP2Location.io для получения геолокационных данных IP. | -| `CLOUDFLARE_API_KEY` | Нет | `""` | Ключ API Cloudflare для получения сведений об AS. Вместе с двумя переменными KV ниже и разрешением токена «Workers KV Storage: Edit» он также обеспечивает работу общедоступных диагностических отчётов. | -| `CLOUDFLARE_ACCOUNT_ID` | Нет | `""` | Идентификатор учётной записи Cloudflare, необходимый для хранения общедоступных диагностических отчётов в Workers KV. | -| `CLOUDFLARE_KV_NAMESPACE_ID` | Нет | `""` | Шестнадцатеричный идентификатор пространства имён Workers KV (не его название), где хранятся общедоступные диагностические отчёты. | -| `RIPESTAT_SOURCE_APP` | Нет | `""` | Имя приложения-источника для RIPE.net, используемое для получения истории ASN через RIPE.net. | -| `MAC_LOOKUP_API_KEY` | Нет | `""` | Ключ API MAC Lookup для получения сведений о MAC-адресах. | -| `VITE_CURL_IPV4_DOMAIN` | Нет | `""` | Домен IPv4 для CURL API, предоставляемого пользователям. | -| `VITE_CURL_IPV6_DOMAIN` | Нет | `""` | Домен IPv6 для CURL API, предоставляемого пользователям. | -| `VITE_CURL_IPV64_DOMAIN` | Нет | `""` | Домен с двойным стеком для CURL API, предоставляемого пользователям. | - -Если отсутствует хотя бы одна из переменных окружения серии CURL, соответствующий API не будет включён. - -### Использование переменных окружения в Node.js - -Создайте файл переменных окружения: +## 📖 Документация -```bash -cp .env.example .env -``` - -Измените `.env`, например добавив следующее: +Полные руководства — в центре документации MyIP: **[docs.ipcheck.ing](https://docs.ipcheck.ing)** (переключатель языка в правом верхнем углу) -```bash -BACKEND_PORT=11966 -FRONTEND_PORT=18966 -MAXMIND_ACCOUNT_ID="YOUR_ACCOUNT_ID" -MAXMIND_LICENSE_KEY="YOUR_LICENSE_KEY" -MAXMIND_AUTO_UPDATE="true" -GOOGLE_MAP_API_KEY="YOUR_KEY_HERE" -ALLOWED_DOMAINS="example.com,example.org" -``` +* [Руководство разработчика](https://docs.ipcheck.ing/developer) — развёртывание, настройка, архитектура и участие в разработке +* [База знаний](https://docs.ipcheck.ing/knowledge-base) — как пользоваться каждым инструментом, пошаговая диагностика сети, сетевые концепции -Затем перезапустите серверную часть. +## ⚙️ Конфигурация -### Использование переменных окружения в Docker +Прежде всего важны две настройки: -Переменные окружения можно передать при запуске Docker, например: +* **MaxMind GeoLite2 (обязательно)** — бесплатные учётные данные для геолокации IP и запросов ASN. Без них источник MaxMind возвращает 503. → [Настройка MaxMind](https://docs.ipcheck.ing/developer/getting-started/maxmind-setup) +* **`ALLOWED_DOMAINS` (обязательно на реальном домене)** — белый список хостов для backend API. Без него любой запрос с домена, отличного от localhost, получает 403. → [Обратный прокси и домены](https://docs.ipcheck.ing/developer/getting-started/reverse-proxy-and-domains) ```bash docker run -d -p 18966:18966 \ -e MAXMIND_ACCOUNT_ID="YOUR_ACCOUNT_ID" \ -e MAXMIND_LICENSE_KEY="YOUR_LICENSE_KEY" \ -e MAXMIND_AUTO_UPDATE="true" \ - -e GOOGLE_MAP_API_KEY="YOUR_KEY_HERE" \ - -e ALLOWED_DOMAINS="example.com,example.org" \ - --name myip \ + -e ALLOWED_DOMAINS="your-domain.com" \ + --name myip --restart always \ jason5ng32/myip:latest - ``` +Всё остальное — необязательные ключи API, безопасность и ограничение частоты запросов, логирование, Sentry, домены curl API — описано в [справочнике переменных окружения](https://docs.ipcheck.ing/developer/reference/environment-variables). + + ## 👩🏻‍💻 Расширенное использование Если для доступа в интернет используется прокси, добавьте следующее правило в конфигурацию прокси-клиента, изменив его под своё приложение. Это позволит проверять как настоящий IP-адрес, так и адрес, используемый через прокси: @@ -230,4 +152,6 @@ DOMAIN,ptest-8.ipcheck.ing,Proxy8 Sentry +GitBook + Cloudflare Project Alexandria diff --git a/README_ZH.md b/README_ZH.md index 3ae698ffc..d9228d0e5 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -92,111 +92,33 @@ pnpm start docker run -d -p 18966:18966 --name myip --restart always jason5ng32/myip:latest ``` -## 📚 环境变量 - -下表中标记为 **是** 的变量必须配置,后端才能正常工作。其中 MaxMind 相关的三项尤其重要——填写环境变量之前请先阅读下面的 MaxMind 配置说明。 - -### MaxMind 数据库(必须配置) - -MyIP 依赖 MaxMind 提供的免费 **GeoLite2** 数据库(City + ASN)来进行 IP 地理位置查询、ASN / 组织归属查询,以及全站各处(IP 卡片、WebRTC ICE candidate 等)的国家/地区标识。MaxMind 配置是后端完整运行的前提。 - -由于 MaxMind GeoLite2 协议不允许再分发,`.mmdb` 文件**没有被包含在本仓库里**,你需要自己准备。有两种做法: - -**方案 A —— 自动下载(推荐,Docker 部署必选)** - -1. 去 [maxmind.com/en/geolite2/signup](https://www.maxmind.com/en/geolite2/signup) 注册一个免费账号。 -2. 在账号的 "Manage License Keys" 页面生成一个 License Key。 -3. 配置这三个环境变量: - ```bash - MAXMIND_ACCOUNT_ID="your-account-id" - MAXMIND_LICENSE_KEY="your-license-key" - MAXMIND_AUTO_UPDATE="true" - ``` -4. 启动后端。首次启动后约 60 秒内,程序会自动下载两个数据库,之后每 24 小时自动检查更新。 - -> ⚠️ **Docker 部署必须使用方案 A。** 一个全新的容器里 `common/maxmind-db/` 目录是空的——如果不配置这三个变量,后端虽然能起来,但 MaxMind 相关的 IP 查询源和 WebRTC 国家标识将无法工作,并且每次启动日志里都会刷出 `MaxMind API will return 503...` 的报错。 - -**方案 B —— 手动放置(离线 / 非 Docker 场景)** - -从你的 MaxMind 账号下载 `GeoLite2-City.mmdb` 和 `GeoLite2-ASN.mmdb`,在启动后端前手动放入 `common/maxmind-db/` 目录。这种情况下 `MAXMIND_AUTO_UPDATE` 可以保持 `"false"`,但每次 MaxMind 发布新版本时你需要自己手动更新文件。 - -### 环境变量一览 - -| 变量名 | 是否必须 | 默认值 | 说明 | -| --- | --- | --- | --- | -| `MAXMIND_ACCOUNT_ID` | **是** | `""` | MaxMind 账号 ID,和 `MAXMIND_LICENSE_KEY` 一起用于下载 GeoLite2 数据库。详见上方 MaxMind 配置说明。 | -| `MAXMIND_LICENSE_KEY` | **是** | `""` | MaxMind License Key,和 `MAXMIND_ACCOUNT_ID` 配合使用。详见上方 MaxMind 配置说明。 | -| `MAXMIND_AUTO_UPDATE` | **是** | `"false"` | 设置为 `"true"` 时,程序会在启动后 60 秒左右自动下载 GeoLite2 数据库,之后每 24 小时刷新一次。**Docker 部署必须设置为 `"true"`。** 只有当你已经手动放置了 `.mmdb` 文件时,才能保持为 `"false"`。 | -| `CAIDA_AUTO_UPDATE` | 否 | `"false"` | 设置为 `"true"` 时,每天自动刷新 CAIDA 数据集(as2org 用于 ASN 组织名查询、as-rel2 用于 ASN 连接图)。设置为 `"false"` 时仍会在启动时下载缺失的快照,之后保持不变。 | -| `VITE_GOOGLE_ANALYTICS_ID` | **是** | `""` | Google Analytics 的 ID,用于统计访问量 | -| `BACKEND_PORT` | 否 | `"11966"` | 程序后端部分的运行端口 | -| `FRONTEND_PORT` | 否 | `"18966"` | 程序前端部分的运行端口 | -| `SECURITY_RATE_LIMIT` | 否 | `"0"` | 控制每 60 分钟一个 IP 可以对后端服务器请求的次数(设置为 0 则为不限制) | -| `SECURITY_DELAY_AFTER` | 否 | `"0"` | 控制每 20 分钟一个 IP 的前 X 次请求不受速度限制,超过 X 次后会逐次增加延迟 | -| `SECURITY_BLACKLIST_LOG_FILE_PATH` | 否 | `""` | 可选的本地黑名单文件(如 `"logs/blacklist-ip.log"`),记录触发限流的 IP。留空则不写文件;无论是否设置,事件都会通过共享 logger 记录 | -| `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 错误日志依然会被记录 | -| `VITE_SENTRY_DSN_FRONTEND` | 否 | `""` | 前端 Sentry DSN(构建时)。留空则构建产物中完全不包含 Sentry 代码。后端运行时也会读取它,作为 `/api/monitoring`(绕过广告拦截器的第一方转发隧道)的白名单。如果你自建 Docker 镜像并在构建时注入了它,运行容器时也要传入同一个值,否则隧道路由不会启用 | -| `SENTRY_DSN_BACKEND` | 否 | `""` | 后端 Sentry DSN(运行时)。留空则完全不加载 Sentry SDK | -| `SENTRY_ENVIRONMENT` | 否 | `"production"` | 后端 Sentry 事件的环境标签。开发机上设为 `"development"`;前端会自动打标签 | -| `SENTRY_ORG` | 否 | `""` | Sentry 组织 slug,与 `SENTRY_PROJECT_FRONTEND`、`SENTRY_AUTH_TOKEN` 配合,在构建时上传 source map | -| `SENTRY_PROJECT_FRONTEND` | 否 | `""` | 前端项目的 Sentry 项目 slug,用于构建时上传 source map | -| `SENTRY_AUTH_TOKEN` | 否 | `""` | 用于构建时上传 source map 的 Sentry 令牌。仅构建时使用的机密,绝不会暴露给浏览器 | -| `ALLOWED_DOMAINS` | 否 | `""` | 允许访问的域名,用逗号分隔,用于防止后端 API 被滥用 | -| `GOOGLE_MAP_API_KEY` | 否 | `""` | Google 地图的 API Key,用于展示 IP 所在地的地图 | -| `IPINFO_API_KEY` | 否 | `""` | IPInfo.io 的 API Token,用于通过 IPInfo.io 获取 IP 归属地信息 | -| `IPAPIIS_API_KEY` | 否 | `""` | IPAPI.is 的 API Key,用于通过 IPAPI.is 获取 IP 归属地信息 | -| `IP2LOCATION_API_KEY` | 否 | `""` | IP2Location.io 的 API Key,用于通过 IP2Location.io 获取 IP 归属地信息 | -| `CLOUDFLARE_API_KEY` | 否 | `""` | Cloudflare 的 API Key,用于获取 AS 系统信息;配合下方两个 KV 变量(token 需追加 "Workers KV Storage: Edit" 权限)还用于诊断报告分享 | -| `CLOUDFLARE_ACCOUNT_ID` | 否 | `""` | Cloudflare 账号 ID,诊断报告分享(Workers KV 存储)所需 | -| `CLOUDFLARE_KV_NAMESPACE_ID` | 否 | `""` | 存储诊断分享报告的 Workers KV namespace 的十六进制 ID(不是名字) | -| `RIPESTAT_SOURCE_APP` | 否 | `""` | RIPE.net 的源应用名称,用于通过 RIPE.net 获取 ASN 的历史信息 | -| `MAC_LOOKUP_API_KEY` | 否 | `""` | MAC 查询的 API Key,用于通过 MAC Lookup 获取 MAC 地址的归属信息 | -| `VITE_CURL_IPV4_DOMAIN` | 否 | `""` | 为用户提供 CURL API 的 IPv4 域名 | -| `VITE_CURL_IPV6_DOMAIN` | 否 | `""` | 为用户提供 CURL API 的 IPv6 域名 | -| `VITE_CURL_IPV64_DOMAIN` | 否 | `""` | 为用户提供 CURL API 的双网络栈域名 | - -需要注意的是,如果 CURL 系列的环境变量任意一个缺失,都不会启用 CURL API。 - -### 在 Node 环境里使用环境变量 - -创建环境变量: +## 📖 官方文档 -```bash -cp .env.example .env -``` - -修改 `.env` 里的内容,比如: +完整文档在 MyIP 文档中心:**[docs.ipcheck.ing](https://docs.ipcheck.ing)**(右上角可切换中文) -```bash -BACKEND_PORT=11966 -FRONTEND_PORT=18966 -MAXMIND_ACCOUNT_ID="YOUR_ACCOUNT_ID" -MAXMIND_LICENSE_KEY="YOUR_LICENSE_KEY" -MAXMIND_AUTO_UPDATE="true" -GOOGLE_MAP_API_KEY="YOUR_KEY_HERE" -ALLOWED_DOMAINS="example.com,example.org" -``` +* [开发者指南](https://docs.ipcheck.ing/developer) —— 部署、配置、架构说明与参与贡献 +* [知识库](https://docs.ipcheck.ing/knowledge-base) —— 每个工具的使用说明、网络问题排查指南、网络概念科普 -然后重新启动后端服务。 +## ⚙️ 配置 -### 在 Docker 里使用环境变量 +开始之前,有两项配置最重要: -你可以在运行 Docker 的时候,添加环境变量,比如: +* **MaxMind GeoLite2(必须)** —— 免费凭证,为 IP 地理位置与 ASN 查询提供数据。不配置时 MaxMind 数据源会返回 503。→ [MaxMind 配置指南](https://docs.ipcheck.ing/developer/getting-started/maxmind-setup) +* **`ALLOWED_DOMAINS`(使用真实域名时必须)** —— 后端 API 的域名白名单。不配置时,来自非 localhost 域名的请求都会收到 403。→ [反向代理与域名](https://docs.ipcheck.ing/developer/getting-started/reverse-proxy-and-domains) ```bash docker run -d -p 18966:18966 \ -e MAXMIND_ACCOUNT_ID="YOUR_ACCOUNT_ID" \ -e MAXMIND_LICENSE_KEY="YOUR_LICENSE_KEY" \ -e MAXMIND_AUTO_UPDATE="true" \ - -e GOOGLE_MAP_API_KEY="YOUR_KEY_HERE" \ - -e ALLOWED_DOMAINS="example.com,example.org" \ - --name myip \ + -e ALLOWED_DOMAINS="your-domain.com" \ + --name myip --restart always \ jason5ng32/myip:latest - ``` +其余全部为可选配置 —— 第三方 API Key、安全与限流、日志、Sentry、CURL API 域名等,详见[环境变量参考](https://docs.ipcheck.ing/developer/reference/environment-variables)。 + + ## 👩🏻‍💻 高级用法 如果你在通过代理上网,可以考虑在你的代理配置里,增加下面的规则(请根据你使用的客户端进行修改),这样就可以实现同时查询真实 IP 和代理后的 IP: @@ -230,4 +152,6 @@ DOMAIN,ptest-8.ipcheck.ing,Proxy8 Sentry +GitBook + Cloudflare Project Alexandria diff --git a/api/AGENTS.md b/api/AGENTS.md index 534ebcd15..2479c8712 100644 --- a/api/AGENTS.md +++ b/api/AGENTS.md @@ -14,8 +14,9 @@ services, service-status poller), parts of which the frontend also imports Roughly one handler file per route: IP-geolocation sources (`ipinfo-io` / `ipapi-com` / `ipapi-is` / `ip2location-io` / `ip-sb` / `ipcheck-ing` / `maxmind`), tool backends (`get-whois` / `dns-resolver` / `mac-checker` / -`cf-radar` / `asn-history` / `asn-connectivity` / `service-status` / -`google-map` / `github-stars` / `invisibility-test` / `dns-leak-test`), user +`cf-radar` / `asn-history` / `asn-connectivity` / `ooni-blocking` / +`globalping-probes` / `service-status` / `google-map` / `github-stars` / +`invisibility-test` / `dns-leak-test`), user proxies (`get-user-info` / `update-user-achievement`), platform (`configs` / `sentry-tunnel` / `share-report`). Each file's header comment states its route and purpose — read those for specifics. @@ -27,6 +28,10 @@ states its route and purpose — read those for specifics. - **Every upstream call uses `fetchUpstream`** from `common/fetch-with-timeout.js` (8s timeout). Never a bare `fetch()` / `https.get()` — a hanging provider must time out, not pin the connection. + It also injects a default `User-Agent` of `MyIP/v/` + (registered at boot by `common/upstream-ua.js` — some upstream WAFs block + undici's default `node` UA); caller-supplied `User-Agent` headers, including + the private-API `{ ...req.headers }` pass-through, always win. - **Error shape.** `res.status(500).json({ error: error.message })` on upstream failure, `400` on bad input. Terse — the frontend doesn't display these verbatim. @@ -59,6 +64,8 @@ these checks: - `requireReferer` — global on `/api/*` (ALLOWED_DOMAINS + localhost). - `requireValidIP()` — per-route for `?ip=`; handler sees a well-formed IP. +- `requireValidDomain()` — `?domain=`, lowercases in place so the edge cache + sees one canonical key. - `requireValidPrefix()` — `?prefix=` (CIDR); lets the frontend quantize to the BGP DFZ floor (/24 v4, /48 v6) for maximal CF edge-cache reuse. - `requireValidASN()` — `?asn=`, strips `AS`, rewrites to numeric diff --git a/api/github-stars.js b/api/github-stars.js index 6ec847fc8..4a2e5aca6 100644 --- a/api/github-stars.js +++ b/api/github-stars.js @@ -6,7 +6,8 @@ import { fetchUpstream } from '../common/fetch-with-timeout.js'; import logger from '../common/logger.js'; -// The project's own repository. GitHub requires a User-Agent on every request. +// The project's own repository. GitHub requires a User-Agent on every +// request; fetchUpstream's default project UA satisfies that. const REPO = 'jason5ng32/MyIP'; export default async (req, res) => { @@ -20,7 +21,6 @@ export default async (req, res) => { const apiRes = await fetchUpstream(`https://api.github.com/repos/${REPO}`, { headers: { 'Accept': 'application/vnd.github+json', - 'User-Agent': 'MyIP-IPCheck.ing', }, }); if (!apiRes.ok) { diff --git a/api/globalping-probes.js b/api/globalping-probes.js new file mode 100644 index 000000000..5968bb259 --- /dev/null +++ b/api/globalping-probes.js @@ -0,0 +1,28 @@ +// /api/globalping-probes — compact country inventory of online Globalping +// probes for the frontend country pickers (MtrTest / GlobalLatencyTest / +// CensorshipCheck). Proxying instead of hitting api.globalping.io from the +// browser trades the multi-hundred-KB raw probe list for a few hundred +// bytes, probe-country coverage changes slowly. + +import { fetchUpstream } from '../common/fetch-with-timeout.js'; +import { buildProbeInventory } from '../common/globalping-inventory.js'; +import logger from '../common/logger.js'; + +const GLOBALPING_PROBES_URL = 'https://api.globalping.io/v1/probes'; + +export default async (req, res) => { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method Not Allowed' }); + } + + try { + const response = await fetchUpstream(GLOBALPING_PROBES_URL); + if (!response.ok) { + throw new Error(`Globalping probes responded with status ${response.status}`); + } + res.json(buildProbeInventory(await response.json())); + } catch (error) { + logger.error({ err: error }, 'globalping-probes handler failed'); + res.status(500).json({ error: error.message }); + } +}; diff --git a/api/ooni-blocking.js b/api/ooni-blocking.js new file mode 100644 index 000000000..25f53fbda --- /dev/null +++ b/api/ooni-blocking.js @@ -0,0 +1,85 @@ +// /api/ooni-blocking — per-country blocking view for a domain over the last +// 30 days, built from OONI's open aggregation API (web_connectivity test, +// probe_cc × blocking_type axes). Powers the default view of the frontend +// CensorshipCheck tool; classification lives in common/ooni-blocking.js. +// +// OONI matches `domain` against the exact tested hostname, so a single query +// misses most of the dataset (e.g. `bbc.com` has ~6 rows while `www.bbc.com` +// has 152 countries). We query the apex and www. variants in parallel and +// merge; one variant failing only degrades the merge, both failing is a 500. +// +// The date window is computed server-side and aligned to UTC days so the +// cache key (the URL) stays stable within a day — the route is wrapped in +// `cacheable()` and CF absorbs repeat lookups of popular domains. + +import { fetchUpstream } from '../common/fetch-with-timeout.js'; +import { classifyOoniCountries, OONI_WINDOW_DAYS } from '../common/ooni-blocking.js'; +import logger from '../common/logger.js'; + +const OONI_AGGREGATION_URL = 'https://api.ooni.io/api/v1/aggregation'; + +const utcDay = (date) => date.toISOString().slice(0, 10); + +const fetchAggregationOnce = async (hostname, since, until) => { + const params = new URLSearchParams({ + domain: hostname, + test_name: 'web_connectivity', + since, + until, + axis_x: 'probe_cc', + axis_y: 'blocking_type', + }); + const response = await fetchUpstream(`${OONI_AGGREGATION_URL}?${params}`); + if (!response.ok) { + throw new Error(`OONI aggregation responded with status ${response.status}`); + } + const data = await response.json(); + return Array.isArray(data.result) ? data.result : []; +}; + +// OONI's API intermittently throws 5xx under load. Those fail fast, so one +// immediate retry usually rides out the blip. Timeouts (AbortError) are NOT +// retried — a saturated upstream would just cost the visitor another 8s. +const fetchAggregation = async (hostname, since, until) => { + try { + return await fetchAggregationOnce(hostname, since, until); + } catch (err) { + if (err.name === 'AbortError') throw err; + return await fetchAggregationOnce(hostname, since, until); + } +}; + +export default async (req, res) => { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method Not Allowed' }); + } + + // Presence, shape and lowercasing guaranteed by requireValidDomain. + const domain = req.query.domain; + const apex = domain.replace(/^www\./, ''); + const variants = [...new Set([apex, `www.${apex}`])]; + + const now = new Date(); + const until = utcDay(now); + const since = utcDay(new Date(now.getTime() - OONI_WINDOW_DAYS * 24 * 60 * 60 * 1000)); + + try { + const settled = await Promise.allSettled(variants.map((v) => fetchAggregation(v, since, until))); + const fulfilled = settled.filter((s) => s.status === 'fulfilled').map((s) => s.value); + if (fulfilled.length === 0) { + throw settled[0].reason; + } + if (fulfilled.length < settled.length) { + logger.warn({ domain }, 'ooni-blocking: one hostname variant query failed, serving partial merge'); + } + res.json({ + domain: apex, + since, + until, + countries: classifyOoniCountries(fulfilled.flat()), + }); + } catch (error) { + logger.error({ err: error, domain }, 'ooni-blocking handler failed'); + res.status(500).json({ error: error.message }); + } +}; diff --git a/backend-server.js b/backend-server.js index c133d3f43..343894629 100644 --- a/backend-server.js +++ b/backend-server.js @@ -7,7 +7,7 @@ import { slowDown } from 'express-slow-down' import rateLimit from 'express-rate-limit'; import pinoHttp from 'pino-http'; import logger from './common/logger.js'; -import { requireReferer, requireValidIP, requireValidPrefix, requireValidASN, requireValidProviderId, requireValidReportId } from './common/guards.js'; +import { requireReferer, requireValidIP, requireValidPrefix, requireValidASN, requireValidDomain, requireValidProviderId, requireValidReportId } from './common/guards.js'; // Backend APIs import mapHandler from './api/google-map.js'; @@ -23,6 +23,8 @@ import maxmindHandler from './api/maxmind.js'; import cfHander from './api/cf-radar.js'; import asnHistoryHandler from './api/asn-history.js'; import asnConnectivityHandler from './api/asn-connectivity.js'; +import ooniBlockingHandler from './api/ooni-blocking.js'; +import globalpingProbesHandler from './api/globalping-probes.js'; import dnsResolver from './api/dns-resolver.js'; import serviceStatusHandler, { detailHandler as serviceStatusDetailHandler, @@ -42,8 +44,10 @@ import { reloadMaxMindDatabases, startMaxMindFileWatcher } from './common/maxmin import { startMaxMindAutoUpdate, bootstrapMaxMindIfMissing } from './common/maxmind-updater.js'; import { startCaidaAutoUpdate, bootstrapCaidaIfMissing } from './common/caida-updater.js'; import { bootstrapServiceStatus, startServiceStatusPolling } from './common/service-status-store.js'; +import { initUpstreamUserAgent } from './common/upstream-ua.js'; dotenv.config({ quiet: true }); +initUpstreamUserAgent(); const app = express(); const backEndPort = parseInt(process.env.BACKEND_PORT || 11966, 10); @@ -227,6 +231,7 @@ app.use('/api', requireReferer); const FIVE_MIN_CACHE = 5 * 60; const ONE_HOUR_CACHE = 60 * 60; const ONE_DAY_CACHE = 24 * 60 * 60; +const SEVEN_DAYS_CACHE = 7 * 24 * 60 * 60; const THIRTY_DAYS_CACHE = 30 * 24 * 60 * 60; const ONE_YEAR_CACHE = 365 * 24 * 60 * 60; @@ -246,6 +251,13 @@ app.get('/api/github-stars', cacheable(ONE_DAY_CACHE), githubStarsHandler); // Feature flags derived from env vars — they only change on a redeploy, so // an hour of caching is safe. app.get('/api/configs', cacheable(ONE_HOUR_CACHE), validateConfigs); +// OONI aggregates cover a 30-day window aligned to UTC days — the payload +// only drifts as new measurements land, so 1 day of edge cache keeps us polite +// to OONI's free API without the view going meaningfully stale. +app.get('/api/ooni-blocking', requireValidDomain(), cacheable(ONE_DAY_CACHE), ooniBlockingHandler); +// Which countries have online Globalping probes — coverage changes slowly, +// and the pickers fail open anyway, so a week of edge cache is fine. +app.get('/api/globalping-probes', cacheable(SEVEN_DAYS_CACHE), globalpingProbesHandler); // Cache for 30 days — registry / historical data that changes on a monthly // (or slower) cadence: IEEE OUI assignments, ASN metadata, ASN interconnection, // and append-only BGP routing history. diff --git a/common/fetch-with-timeout.js b/common/fetch-with-timeout.js index ea4639a25..bdf146a5b 100644 --- a/common/fetch-with-timeout.js +++ b/common/fetch-with-timeout.js @@ -35,6 +35,33 @@ export async function fetchWithTimeout(url, init = {}) { } } -// Back-end preset: 8s default for server-to-server upstream calls. -export const fetchUpstream = (url, init = {}) => - fetchWithTimeout(url, { timeoutMs: 8000, ...init }); +// Optional User-Agent for fetchUpstream calls. Backend boot injects a +// project-identifying UA (see common/upstream-ua.js) because some upstream +// WAFs hard-block undici's default `User-Agent: node`. Injection keeps this +// file browser-safe: the frontend bundle never touches fs / process. +let upstreamUserAgent = null; + +export const setUpstreamUserAgent = (ua) => { + upstreamUserAgent = ua || null; +}; + +// True when caller-supplied headers already carry a User-Agent (any casing; +// plain object or Headers instance) — pass-through handlers forward the +// visitor's headers and must not have them overridden or duplicated. The +// array-of-pairs headers form isn't used in this repo; treat it as opting +// out of injection rather than attempting a merge. +const headersCarryUA = (h) => { + if (!h) return false; + if (typeof h.has === 'function') return h.has('user-agent'); + if (Array.isArray(h)) return true; + return Object.keys(h).some((k) => k.toLowerCase() === 'user-agent'); +}; + +// Back-end preset: 8s default for server-to-server upstream calls, plus the +// default User-Agent above unless the caller already set one. +export const fetchUpstream = (url, init = {}) => { + const headers = upstreamUserAgent && !headersCarryUA(init.headers) + ? { ...init.headers, 'User-Agent': upstreamUserAgent } + : init.headers; + return fetchWithTimeout(url, { timeoutMs: 8000, ...init, headers }); +}; diff --git a/common/globalping-inventory.js b/common/globalping-inventory.js new file mode 100644 index 000000000..330c6da8c --- /dev/null +++ b/common/globalping-inventory.js @@ -0,0 +1,35 @@ +// Pure aggregation for the Globalping probe inventory: turns the raw +// /v1/probes payload (one entry per online probe) into the compact shape +// the frontend country pickers consume — which countries have a probe at +// all, grouped into the five continent buckets the UI displays (NA + SA +// merge into "americas"). Serving this from our backend keeps the multi- +// hundred-KB probe list off the visitor's connection. + +// Globalping continent codes → our display buckets. +const CONTINENT_BUCKETS = { AS: 'asia', EU: 'europe', AF: 'africa', NA: 'americas', SA: 'americas', OC: 'oceania' }; +const BUCKET_ORDER = ['asia', 'europe', 'africa', 'americas', 'oceania']; + +// probes: raw array from Globalping. Returns +// { countries: [cc], continents: [{ key, countries: [cc] }] } with +// continents in display order and country codes sorted alphabetically. +export const buildProbeInventory = (probes) => { + const countries = new Set(); + const buckets = new Map(); + + for (const probe of (Array.isArray(probes) ? probes : [])) { + const cc = probe?.location?.country; + if (!cc) continue; + countries.add(cc); + const bucket = CONTINENT_BUCKETS[probe?.location?.continent]; + if (!bucket) continue; + if (!buckets.has(bucket)) buckets.set(bucket, new Set()); + buckets.get(bucket).add(cc); + } + + return { + countries: [...countries].sort(), + continents: BUCKET_ORDER + .filter((key) => buckets.has(key)) + .map((key) => ({ key, countries: [...buckets.get(key)].sort() })), + }; +}; diff --git a/common/guards.js b/common/guards.js index 4f285b66d..3f76c4429 100644 --- a/common/guards.js +++ b/common/guards.js @@ -4,7 +4,7 @@ // carrying the defensive checks and can't accidentally forget them. import { refererCheck } from './referer-check.js'; -import { isValidIP } from './valid-ip.js'; +import { isValidIP, isValidDomain } from './valid-ip.js'; import { isValidBgpPrefix } from './bgp-prefix.js'; import { STATUS_PROVIDER_IDS } from './service-status-providers.js'; @@ -34,6 +34,23 @@ export const requireValidIP = (paramName = 'ip') => (req, res, next) => { next(); }; +// Reject requests without a syntactically valid domain in the specified +// query param. Lowercases the value in place so handlers and the edge cache +// see one canonical form (domains are case-insensitive; a mixed-case query +// must not become a distinct CF cache entry). +export const requireValidDomain = (paramName = 'domain') => (req, res, next) => { + const raw = req.query[paramName]; + if (!raw) { + return res.status(400).json({ error: 'No domain provided' }); + } + const domain = String(raw).toLowerCase(); + if (!isValidDomain(domain)) { + return res.status(400).json({ error: 'Invalid domain' }); + } + req.query[paramName] = domain; + next(); +}; + // Reject requests without a valid CIDR prefix in the specified query param. // Accepts any well-formed CIDR — the quantization policy (e.g. /24 for v4, // /48 for v6) is the frontend's job, not the guard's. diff --git a/common/ooni-blocking.js b/common/ooni-blocking.js new file mode 100644 index 000000000..5b93e194f --- /dev/null +++ b/common/ooni-blocking.js @@ -0,0 +1,83 @@ +// Pure classification logic for the OONI-backed censorship view: turns raw +// rows from OONI's aggregation API (probe_cc × blocking_type, see +// api/ooni-blocking.js) into one tiered entry per country. +// +// Tier semantics — calibrated against real 30-day data for known-blocked +// domains (twitter/facebook/bbc/pornhub, 2026-07): +// confirmed OONI fingerprint-confirmed blocking (blockpage served). Only +// exists in countries that serve identifiable blockpages. +// likely High anomaly share. Countries that block via DNS poisoning / +// RST injection (e.g. CN) never produce `confirmed`, so anomaly +// share is the primary signal, not a fallback. +// signs Moderate anomaly share — intermittent interference or noisy +// CDN geo-variance. Requires a larger sample to avoid false +// positives from low-traffic countries. +// ok Everything else, including under-sampled countries. + +export const OONI_WINDOW_DAYS = 30; + +// Minimum measurements for a country to be classifiable at all. Kept low on +// purpose: for rarely-tested domains a handful of measurements is all OONI +// has, and 7-of-8 anomalous is still a real signal (the ratio thresholds do +// the actual gatekeeping). Below this, one noisy probe would decide the tier. +const MIN_SAMPLES = 5; +// `signs` is the noisiest tier; demand a larger sample before flagging. +const SIGNS_MIN_SAMPLES = 30; + +const CONFIRMED_RATIO = 0.05; +const LIKELY_RATIO = 0.5; +const SIGNS_RATIO = 0.2; + +const TIER_ORDER = { confirmed: 0, likely: 1, signs: 2, ok: 3 }; + +// OONI's blocking_type values for web_connectivity anomalies. Rows with an +// empty blocking_type carry the ok / failure counts and never contribute to +// the per-method breakdown. +const BLOCKING_TYPES = new Set(['dns', 'tcp_ip', 'http-failure', 'http-diff']); + +const classifyTier = ({ measurements, anomaly, confirmed }) => { + if (measurements < MIN_SAMPLES) return 'ok'; + const blockedRatio = (anomaly + confirmed) / measurements; + if (confirmed / measurements >= CONFIRMED_RATIO) return 'confirmed'; + if (blockedRatio >= LIKELY_RATIO) return 'likely'; + if (measurements >= SIGNS_MIN_SAMPLES && blockedRatio >= SIGNS_RATIO) return 'signs'; + return 'ok'; +}; + +// rows: concatenated OONI result rows (both hostname variants are fine — the +// accumulator merges by probe_cc). Returns one entry per country, flagged +// tiers first, each tier sorted by blocked share descending. +export const classifyOoniCountries = (rows) => { + const byCountry = new Map(); + + for (const row of rows) { + const cc = row?.probe_cc; + if (!cc) continue; + let entry = byCountry.get(cc); + if (!entry) { + entry = { country: cc, measurements: 0, ok: 0, anomaly: 0, confirmed: 0, failure: 0, methods: {} }; + byCountry.set(cc, entry); + } + entry.measurements += row.measurement_count || 0; + entry.ok += row.ok_count || 0; + entry.anomaly += row.anomaly_count || 0; + entry.confirmed += row.confirmed_count || 0; + entry.failure += row.failure_count || 0; + + const bt = row.blocking_type; + if (BLOCKING_TYPES.has(bt)) { + const hits = (row.anomaly_count || 0) + (row.confirmed_count || 0); + if (hits > 0) entry.methods[bt] = (entry.methods[bt] || 0) + hits; + } + } + + const blockedShare = (e) => e.measurements ? (e.anomaly + e.confirmed) / e.measurements : 0; + + return [...byCountry.values()] + .map((e) => ({ ...e, tier: classifyTier(e) })) + .sort((a, b) => + (TIER_ORDER[a.tier] - TIER_ORDER[b.tier]) + || (blockedShare(b) - blockedShare(a)) + || (b.measurements - a.measurements) + ); +}; diff --git a/common/service-status-providers.js b/common/service-status-providers.js index 86650715e..331d09bdc 100644 --- a/common/service-status-providers.js +++ b/common/service-status-providers.js @@ -27,8 +27,7 @@ export const STATUS_PROVIDERS = [ // categories, leaving one "Cloudflare Sites and Services" rollup + 7 // continents — which keeps the card light consistent with this list. { id: 'cloudflare', name: 'Cloudflare', api: 'https://new.cloudflarestatus.com', page: 'https://new.cloudflarestatus.com' }, - // LangSmith is the status page for the LangChain platform. - { id: 'langchain', name: 'LangChain', api: 'https://status.smith.langchain.com', page: 'https://status.smith.langchain.com' }, + { id: 'groq', name: 'Groq', api: 'https://groqstatus.com', page: 'https://groqstatus.com' }, { id: 'notion', name: 'Notion', api: 'https://www.notion-status.com', page: 'https://www.notion-status.com' }, { id: 'vercel', name: 'Vercel', api: 'https://www.vercel-status.com', page: 'https://www.vercel-status.com' }, { id: 'netlify', name: 'Netlify', api: 'https://www.netlifystatus.com', page: 'https://www.netlifystatus.com' }, diff --git a/common/upstream-ua.js b/common/upstream-ua.js new file mode 100644 index 000000000..19df49b88 --- /dev/null +++ b/common/upstream-ua.js @@ -0,0 +1,30 @@ +// Backend-only User-Agent bootstrap for fetchUpstream. +// +// Some upstream WAFs (e.g. Cloudflare's status page) hard-block undici's +// default `User-Agent: node`, so server-to-server calls identify themselves +// as `MyIP/v/` instead. Version comes from package.json; +// the site segment from VITE_SITE_URL, so forks advertise their own +// deployment rather than ipcheck.ing. Lives outside fetch-with-timeout.js on +// purpose: that module is shared with the browser bundle and must stay free +// of fs / process access. + +import { readFileSync } from 'node:fs'; + +import { setUpstreamUserAgent } from './fetch-with-timeout.js'; + +// Build and register the UA. Called from backend-server.js after +// dotenv.config() so VITE_SITE_URL from .env is visible — module import +// time would be too early (imports are hoisted above the config call). +// Missing pieces degrade gracefully: `MyIP/v7.1.0`, or just `MyIP`. +export const initUpstreamUserAgent = () => { + let version = ''; + try { + version = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version || ''; + } catch { + // Unreadable package.json → versionless UA. + } + const site = (process.env.VITE_SITE_URL || '').trim(); + const ua = ['MyIP', version && `v${version}`, site].filter(Boolean).join('/'); + setUpstreamUserAgent(ua); + return ua; +}; diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index a41b8822b..df98cd5b5 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -86,10 +86,11 @@ philosophy as `firebase-init.js`). Two rules: SDK back into the main bundle. All Sentry config lives in `sentry-init.js`. - **Explicit signals go through the app-events bus**, like achievements: the component emits, `sentry-init.js` subscribes. One signal is captured: - `ip-source:exhausted` (an IP card's whole source chain failed). v4 cards - report directly; v6 cards report only when the `ipinfo:finished` snapshot - shows some card resolved a valid IPv6 — otherwise "our v6 chain failed" is - indistinguishable from "visitor has no IPv6", which is routine noise. + `ip-source:exhausted` (an IP card's whole source chain failed). Cards + report only when the `ipinfo:finished` snapshot shows some card resolved a + valid IP of the same version — otherwise "our chain failed" is + indistinguishable from visitor-side conditions (no IPv6 / dead network), + which is routine noise. Capture surface: uncaught errors; `console.error` (fingerprinted per message prefix so each source stays a distinct issue; individual `utils/getips/` diff --git a/frontend/App.vue b/frontend/App.vue index d0b7712c0..c73431a26 100644 --- a/frontend/App.vue +++ b/frontend/App.vue @@ -5,6 +5,7 @@ + @@ -12,15 +13,16 @@ - - diff --git a/frontend/components/DnsLeaksTest.vue b/frontend/components/DnsLeaksTest.vue index 99879e925..ae4690e56 100644 --- a/frontend/components/DnsLeaksTest.vue +++ b/frontend/components/DnsLeaksTest.vue @@ -85,29 +85,15 @@ - -
- -
-

- - {{ t('dnsleaktest.EnhancedBanner.Title') }} -

-

- {{ t('dnsleaktest.EnhancedBanner.Note') }} -

-
-
- -
-
-
+ homepage test has resolved (success or timeout). --> + @@ -125,9 +111,10 @@ import { Card, CardContent } from '@/components/ui/card'; import { useStatusTone, ipFieldTone, isFieldPending as isFieldPendingShared } from '@/composables/use-status-tone.js'; import { createMaskGate } from '@/composables/use-info-mask.js'; import { useMaxmind } from '@/composables/use-maxmind.js'; -import { EthernetPort, Play, MapPin, RotateCw, Sparkles, ArrowRight, DoorOpen } from '@lucide/vue'; +import { EthernetPort, Play, MapPin, RotateCw, Sparkles, DoorOpen } from '@lucide/vue'; import { Icon } from '@iconify/vue'; import FitText from '@/components/widgets/FitText.vue'; +import InfoBanner from '@/components/widgets/InfoBanner.vue'; import { INLINE_TIERS } from '@/composables/use-fit-text.js'; import { ipApi, surfshark, ipleak, browserleaks, runWithRetry, @@ -279,23 +266,3 @@ defineExpose({ leakTest, }); - - diff --git a/frontend/components/Footer.vue b/frontend/components/Footer.vue index cd20b5632..8d3b83624 100644 --- a/frontend/components/Footer.vue +++ b/frontend/components/Footer.vue @@ -14,6 +14,12 @@ {{ t('about.Privacy') }} + @@ -159,6 +165,7 @@ import { useMainStore } from '@/store'; import { useI18n } from 'vue-i18n'; import changelogData from '@/data/changelog.json'; import { trackEvent } from '@/utils/analytics'; +import { DOCS_URL, isDocsConfigured } from '@/composables/use-docs-assistant'; import { Sheet, SheetContent, SheetClose } from '@/components/ui/sheet'; import { JnTooltip } from '@/components/ui/tooltip'; import { Button } from '@/components/ui/button'; @@ -197,10 +204,12 @@ const acknowledgementsList = [ { name: 'Sentry', link: 'https://www.sentry.io/' }, { name: '1Password', link: 'https://www.1password.com/' }, { name: 'Greptile', link: 'https://www.greptile.com/' }, + { name: 'GitBook', link: 'https://www.gitbook.com/' }, { name: 'Globalping by jsDelivr', link: 'https://globalping.io/' }, { name: 'ProxyCheck.io', link: 'https://proxycheck.io/' }, { name: 'Digital Defense', link: 'https://digital-defense.io/' }, { name: 'RIPE NCC', link: 'https://stat.ripe.net/' }, + { name: 'OONI', link: 'https://ooni.org/' }, { name: 'CAIDA', link: 'https://www.caida.org/' }, { name: 'ChatGPT', link: 'https://chatgpt.com/' }, { name: 'Claude', link: 'https://claude.ai/' }, diff --git a/frontend/components/IpInfos.vue b/frontend/components/IpInfos.vue index cd6981447..0caebdba7 100644 --- a/frontend/components/IpInfos.vue +++ b/frontend/components/IpInfos.vue @@ -172,8 +172,8 @@ const resolveIP = async (cardID, getFromSource) => { : t('ipInfos.IPv4Error'); // Domain event: this source AND all its internal fallbacks failed to // produce an IP. Emitted unconditionally (bus semantics); subscribers - // decide what matters — sentry-init.js reports v4 exhaustion directly - // and gates v6 exhaustion on evidence the visitor has working IPv6. + // decide what matters — sentry-init.js gates exhaustion on evidence the + // visitor's network works for that IP version (some card resolved one). emitAppEvent('ip-source:exhausted', { source: CARD_SOURCE_SLUGS[cardID] ?? `card-${cardID}`, ipVersion: isV6Card ? 'v6' : 'v4', diff --git a/frontend/components/Nav.vue b/frontend/components/Nav.vue index be508a927..306e65087 100644 --- a/frontend/components/Nav.vue +++ b/frontend/components/Nav.vue @@ -81,6 +81,9 @@
+ + +
- -
- - - -
- -

{{ t('censorshipcheck.TestGroup') }}

-
- -
-
+ + + + +
+ + {{ t('censorshipcheck.OoniError') }} +
+ + +
- - diff --git a/frontend/components/advanced-tools/Empty.vue b/frontend/components/advanced-tools/Empty.vue deleted file mode 100644 index 361001605..000000000 --- a/frontend/components/advanced-tools/Empty.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - \ No newline at end of file diff --git a/frontend/components/advanced-tools/GlobalLatencyTest.vue b/frontend/components/advanced-tools/GlobalLatencyTest.vue index 2fbc96277..6b1655a9a 100644 --- a/frontend/components/advanced-tools/GlobalLatencyTest.vue +++ b/frontend/components/advanced-tools/GlobalLatencyTest.vue @@ -43,8 +43,10 @@ :aria-invalid="manualIP.trim() !== '' && !isValidManualIP" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" data-1p-ignore data-lpignore="true" @keyup.enter="startPingCheck" /> -