Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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