Skip to content
10 changes: 5 additions & 5 deletions README_FR.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,15 @@ docker run -d -p 18966:18966 --name myip --restart always jason5ng32/myip:latest

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)

* [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
* [Guide du développeur](https://docs.ipcheck.ing/developer/fr) — déploiement, configuration, architecture et contribution
* [Base de connaissances](https://docs.ipcheck.ing/knowledge-base/fr) — utilisation de chaque outil, diagnostic réseau pas à pas, concepts réseau

## ⚙️ Configuration

Deux réglages comptent avant tout :

* **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)
* **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/fr/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/fr/reverse-proxy-and-domains)

```bash
docker run -d -p 18966:18966 \
Expand All @@ -116,7 +116,7 @@ docker run -d -p 18966:18966 \
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).
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/fr/environment-variables).


## 👩🏻‍💻 Utilisation avancée
Expand Down
10 changes: 5 additions & 5 deletions README_RU.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,15 @@ docker run -d -p 18966:18966 --name myip --restart always jason5ng32/myip:latest

Полные руководства — в центре документации MyIP: **[docs.ipcheck.ing](https://docs.ipcheck.ing)** (переключатель языка в правом верхнем углу)

* [Руководство разработчика](https://docs.ipcheck.ing/developer) — развёртывание, настройка, архитектура и участие в разработке
* [База знаний](https://docs.ipcheck.ing/knowledge-base) — как пользоваться каждым инструментом, пошаговая диагностика сети, сетевые концепции
* [Руководство разработчика](https://docs.ipcheck.ing/developer/ru) — развёртывание, настройка, архитектура и участие в разработке
* [База знаний](https://docs.ipcheck.ing/knowledge-base/ru) — как пользоваться каждым инструментом, пошаговая диагностика сети, сетевые концепции

## ⚙️ Конфигурация

Прежде всего важны две настройки:

* **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)
* **MaxMind GeoLite2 (обязательно)** — бесплатные учётные данные для геолокации IP и запросов ASN. Без них источник MaxMind возвращает 503. → [Настройка MaxMind](https://docs.ipcheck.ing/developer/getting-started/ru/maxmind-setup)
* **`ALLOWED_DOMAINS` (обязательно на реальном домене)** — белый список хостов для backend API. Без него любой запрос с домена, отличного от localhost, получает 403. → [Обратный прокси и домены](https://docs.ipcheck.ing/developer/getting-started/ru/reverse-proxy-and-domains)

```bash
docker run -d -p 18966:18966 \
Expand All @@ -116,7 +116,7 @@ docker run -d -p 18966:18966 \
jason5ng32/myip:latest
```

Всё остальное — необязательные ключи API, безопасность и ограничение частоты запросов, логирование, Sentry, домены curl API — описано в [справочнике переменных окружения](https://docs.ipcheck.ing/developer/reference/environment-variables).
Всё остальное — необязательные ключи API, безопасность и ограничение частоты запросов, логирование, Sentry, домены curl API — описано в [справочнике переменных окружения](https://docs.ipcheck.ing/developer/reference/ru/environment-variables).


## 👩🏻‍💻 Расширенное использование
Expand Down
10 changes: 5 additions & 5 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,15 @@ docker run -d -p 18966:18966 --name myip --restart always jason5ng32/myip:latest

完整文档在 MyIP 文档中心:**[docs.ipcheck.ing](https://docs.ipcheck.ing)**(右上角可切换中文)

* [开发者指南](https://docs.ipcheck.ing/developer) —— 部署、配置、架构说明与参与贡献
* [知识库](https://docs.ipcheck.ing/knowledge-base) —— 每个工具的使用说明、网络问题排查指南、网络概念科普
* [开发者指南](https://docs.ipcheck.ing/developer/zh) —— 部署、配置、架构说明与参与贡献
* [知识库](https://docs.ipcheck.ing/knowledge-base/zh) —— 每个工具的使用说明、网络问题排查指南、网络概念科普

## ⚙️ 配置

开始之前,有两项配置最重要:

* **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)
* **MaxMind GeoLite2(必须)** —— 免费凭证,为 IP 地理位置与 ASN 查询提供数据。不配置时 MaxMind 数据源会返回 503。→ [MaxMind 配置指南](https://docs.ipcheck.ing/developer/getting-started/zh/maxmind-setup)
* **`ALLOWED_DOMAINS`(使用真实域名时必须)** —— 后端 API 的域名白名单。不配置时,来自非 localhost 域名的请求都会收到 403。→ [反向代理与域名](https://docs.ipcheck.ing/developer/getting-started/zh/reverse-proxy-and-domains)

```bash
docker run -d -p 18966:18966 \
Expand All @@ -116,7 +116,7 @@ docker run -d -p 18966:18966 \
jason5ng32/myip:latest
```

其余全部为可选配置 —— 第三方 API Key、安全与限流、日志、Sentry、CURL API 域名等,详见[环境变量参考](https://docs.ipcheck.ing/developer/reference/environment-variables)。
其余全部为可选配置 —— 第三方 API Key、安全与限流、日志、Sentry、CURL API 域名等,详见[环境变量参考](https://docs.ipcheck.ing/developer/reference/zh/environment-variables)。


## 👩🏻‍💻 高级用法
Expand Down
10 changes: 7 additions & 3 deletions api/ipapi-is.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ function buildUrl(req) {
return `https://api.ipapi.is?q=${ipAddress}&key=${key}`;
}

function modifyJsonForIPAPI(json) {
let asn = json.asn || {};
const { ip, location, is_datacenter, is_proxy, is_vpn, is_tor } = json;
// Both nested objects are optional: the upstream answers 200 with a null
// `location` (and no `asn`) for addresses it can't place — anycast ranges,
// bogons — so neither may be dereferenced directly.
export function modifyJsonForIPAPI(json) {
const asn = json.asn || {};
const location = json.location || {};
const { ip, is_datacenter, is_proxy, is_vpn, is_tor } = json;

return {
ip: ip,
Expand Down
5 changes: 4 additions & 1 deletion common/rdap.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,10 @@ export const rdapIp = async (ip, { timeoutMs = 5000 } = {}) => {
throw new Error(`No RDAP endpoint for ${ip}`);
}

const url = `${trimSlash(base)}/ip/${encodeURIComponent(ip)}`;
// Literal IP, not encodeURIComponent: colons are legal in a URL path,
// the guard layer already ensures a well-formed IP, and some RIR
// delegates (e.g. IDNIC behind APNIC's redirect) reject %3A with a 400.
const url = `${trimSlash(base)}/ip/${ip}`;
const res = await fetchUpstream(url, { timeoutMs });
if (res.status === 404) {
throw new Error(`IP not found: ${ip}`);
Expand Down
3 changes: 0 additions & 3 deletions frontend/components/Additional.vue
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,6 @@ const openCurlModal = () => {
trackEvent('Additional', 'AdditionalClick', 'Curl');
};

defineExpose({
openCurlModal,
});
</script>

<style scoped>
Expand Down
5 changes: 1 addition & 4 deletions frontend/components/Footer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
</RouterLink>
</Button>
<Button v-if="isDocsConfigured" variant="link" size="default" as-child class="cursor-pointer">
<a :href="DOCS_URL" target="_blank" rel="noopener"
<a :href="DOCS_URL+'/'+(locale === 'en' ? '' : locale)" target="_blank" rel="noopener"
@click="trackEvent('Footer', 'FooterClick', 'HelpCenter')">
{{ t('about.HelpCenter') }}
</a>
Expand Down Expand Up @@ -250,7 +250,4 @@ watch(content, () => {
});
});

defineExpose({
openAbout,
});
</script>
17 changes: 7 additions & 10 deletions frontend/components/Home.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<template>
<NavBar ref="navBarRef" />
<NavBar />
<User ref="userRef" />
<Achievements ref="achievementsRef" />
<Preferences ref="preferencesRef" />
<Preferences />
<main id="mainpart" class="mx-auto w-full px-4 jn-container">
<div class="rounded-md" tabindex="0">
<IPCheck ref="IPCheckRef" />
Expand All @@ -16,13 +16,13 @@
<FloatingDock :ready="showMaskButton" :mask-active="infoMaskLevel > 0">
<InfoMask :showMaskButton.value="showMaskButton" :infoMaskLevel.value="infoMaskLevel"
:toggleInfoMask="toggleInfoMask" />
<ShareReport />
<ShareReport ref="shareReportRef" />
<IPHistory />
<QueryIP ref="queryIPRef" />
</FloatingDock>
<HelpModal ref="helpModalRef" />
<Additional ref="additionalRef" />
<Footer ref="footerRef" />
<Additional />
<Footer />
</template>

<script setup>
Expand Down Expand Up @@ -81,14 +81,11 @@ const userPreferences = computed(() => store.userPreferences);
const isToolOpen = computed(() => !!route.query.tool);

// Template refs
const navBarRef = ref(null);
const userRef = ref(null);
const achievementsRef = ref(null);
const preferencesRef = ref(null);
const queryIPRef = ref(null);
const helpModalRef = ref(null);
const additionalRef = ref(null);
const footerRef = ref(null);
const shareReportRef = ref(null);
const speedTestRef = ref(null);
const advancedToolsRef = ref(null);
const IPCheckRef = ref(null);
Expand All @@ -114,7 +111,7 @@ const { loadingControl } = useRefreshOrchestrator({
// Shortcuts
const { loadShortcuts } = useShortcuts({
refs: {
navBarRef, preferencesRef, queryIPRef, helpModalRef, additionalRef, footerRef,
queryIPRef, helpModalRef, shareReportRef,
speedTestRef, advancedToolsRef, IPCheckRef, connectivityRef, webRTCRef, dnsLeaksRef,
isInfosLoaded, isToolOpen, toggleInfoMask,
},
Expand Down
16 changes: 11 additions & 5 deletions frontend/components/IpInfos.vue
Original file line number Diff line number Diff line change
Expand Up @@ -297,9 +297,8 @@ const fetchIPDetails = async (cardIndex, ip, sourceID = null) => {
const fetchPromise = (async () => {
const sources = store.ipDBs.filter(source => source.enabled);

// The preferred source may no longer be in the enabled list (disabled by
// a concurrent card's failure, or a stale stored preference); findIndex
// then yields -1 and sources[-1] is undefined — start from the first one.
// The requested source may be absent from the enabled list (config flag
// off, or configs not loaded yet); findIndex yields -1 — start from 0.
let currentSourceIndex = sourceID !== null ? sources.findIndex(source => source.id === sourceID) : 0;
if (currentSourceIndex === -1) currentSourceIndex = 0;
let attempts = 0;
Expand All @@ -312,16 +311,23 @@ const fetchIPDetails = async (cardIndex, ip, sourceID = null) => {
const cardData = transformDataFromIPapi(response, source.id, t, lang.value);

if (cardData) {
// Fallback landed off the requested source: toast once (parallel
// cards dedupe via usingSource), drift the runtime refs, but never
// rewrite the user's stored preference.
if (source.id !== sourceID && usingSource.value !== source.id) {
const from = store.ipDBs.find(db => db.id === sourceID)?.text || `#${sourceID}`;
store.setAlert(true, 'text-warning',
t('alert.IpGeoSourceFallbackMessage', { from, to: source.text }),
t('alert.IpGeoSourceFallbackTitle'), 5000);
}
ipGeoSource.value = source.id;
usingSource.value = source.id;
store.updatePreference('ipGeoSource', source.id);
Object.assign(card, cardData);
ipDataCache.set(ip, cardData);
return;
}
} catch (error) {
console.error("Error fetching IP details from source " + source.id + ":", error);
store.updateIPDBs({ id: source.id, enabled: false });
currentSourceIndex = (currentSourceIndex + 1) % sources.length;
attempts++;
}
Expand Down
2 changes: 0 additions & 2 deletions frontend/components/Nav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -455,8 +455,6 @@ onBeforeUnmount(() => {
window.removeEventListener('scroll', onScroll);
clearTimeout(openToolTimer);
});

defineExpose({ OpenPreferences });
</script>

<style scoped>
Expand Down
19 changes: 13 additions & 6 deletions frontend/components/SpeedTest.vue
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ import { trackEvent } from '@/utils/analytics';
import { emitAppEvent } from '@/utils/app-events.js';
import { fetchWithTimeout } from '@/utils/fetch-with-timeout.js';
import { isValidIP } from '@/utils/valid-ip.js';
import { parseTrace } from '@/utils/parse-trace.js';
import getCountryName from '@/data/country-name.js';
import SpeedTestEngine from '@cloudflare/speedtest';
import useSpeedTestCharts from '@/composables/use-speedtest-charts.js';
Expand Down Expand Up @@ -294,12 +295,15 @@ const connectionMethods = {
try {
const response = await fetchWithTimeout('https://speed.cloudflare.com/cdn-cgi/trace');
const data = await response.text();
const lines = data.split('\n');
const ip = lines.find((l) => l.startsWith('ip='))?.split('=')[1];
const colo = lines.find((l) => l.startsWith('colo='))?.split('=')[1];
const loc = lines.find((l) => l.startsWith('loc='))?.split('=')[1];
const { ip, colo, loc } = parseTrace(data);

if (!isValidIP(ip)) throw new Error('Invalid IP from SpeedTest Server');
if (!isValidIP(ip)) {
// Self-describing diagnostic.
const detail = ip == null ? 'no-ip-line'
: ip === '' ? 'empty-value'
: `len=${ip.length} tail=0x${ip.charCodeAt(ip.length - 1).toString(16)}`;
throw new Error(`Invalid IP from SpeedTest Server (${detail})`);
}

const { default: getColoCountry } = await import('@/data/speedtest-colos.js');

Expand Down Expand Up @@ -348,8 +352,11 @@ const engineMethods = {
});
},

// The engine's queued timers deliver one last measurement after onFinish /
// onUnmounted dropped the reference, so the engine may already be gone.
updateProgress() {
const rawData = testEngine.results.raw;
const rawData = testEngine?.results?.raw;
if (!rawData) return;
// 3 stages: latency / download / upload.
const perStage = 100 / 3;
let progress = 0;
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/User.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<!-- Benefits List -->
<ul class="rounded-lg border bg-card divide-y">
<li v-for="n in 5" :key="n" class="flex items-start gap-3 p-3 text-sm">
<li v-for="n in 6" :key="n" class="flex items-start gap-3 p-3 text-sm">
<span
class="shrink-0 inline-flex items-center justify-center size-5 rounded-full bg-success/15 text-success mt-0.5">
<CircleCheck class="size-3.5" />
Expand Down
12 changes: 4 additions & 8 deletions frontend/components/advanced-tools/RuleTest.vue
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import { ref, computed, onMounted, watch } from 'vue';
import { useMainStore } from '@/store';
import { useI18n } from 'vue-i18n';
import { emitAppEvent } from '@/utils/app-events.js';
import { parseTrace } from '@/utils/parse-trace.js';
import getCountryName from '@/data/country-name.js';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
Expand Down Expand Up @@ -138,12 +139,8 @@ const fetchTrace = async (id, url) => {
try {
const response = await fetch(`https://${url}/cdn-cgi/trace`);
const data = await response.text();
const lines = data.split('\n');
const ipLine = lines.find((line) => line.startsWith('ip='));
const countryLine = lines.find((line) => line.startsWith('loc='));
const country = countryLine ? countryLine.split('=')[1] : '';
if (ipLine) {
const ip = ipLine.split('=')[1];
const { ip, loc: country = '' } = parseTrace(data);
if (ip) {
ruleTests.value[id].ip = ip;
IPArray.value = [...IPArray.value, { ip, country }];
}
Expand All @@ -155,8 +152,7 @@ const fetchTrace = async (id, url) => {
// no org — MaxMind fills that gap. Country resolution stays on
// trace (authoritative for each ptest worker's egress), so a
// MaxMind miss leaves the existing country untouched.
if (ipLine) {
const ip = ruleTests.value[id].ip;
if (ip) {
const geo = await lookupMaxmind(ip);
ruleTests.value[id].org = geo ? geo.org : t('ruletest.StatusError');
if (geo) {
Expand Down
3 changes: 3 additions & 0 deletions frontend/components/report/ShareReportDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,7 @@ const onDownloadJson = () => {

const expiresAtDisplay = computed(() =>
expiresAt.value ? new Date(expiresAt.value).toLocaleString() : '');

// For the `e` keyboard shortcut (use-shortcuts.js).
defineExpose({ openDialog });
</script>
Loading
Loading