mirror of
https://github.com/jason5ng32/MyIP.git
synced 2026-11-04 08:55:47 +00:00
Improvements
This commit is contained in:
1 parent
238ded1e1f
commit
05f837df27
3 files changed
+133
-28
No files matched your search
@@ -22,7 +22,7 @@
|
||||
|
||||
<!-- Card grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<Card v-for="(leak, index) in leakTest" :key="leak.id"
|
||||
<Card v-for="(leak, index) in leakTest" :key="leak.slot"
|
||||
class="keyboard-shortcut-card jn-card min-w-0 overflow-hidden transition-transform duration-300 ease-out hover:-translate-y-1.5 data-[keyboard-hover=true]:ring-2 data-[keyboard-hover=true]:ring-green-500/50">
|
||||
<CardContent class="p-4 min-w-0">
|
||||
<!-- Top: heartbeat icon + name + index -->
|
||||
@@ -33,8 +33,7 @@
|
||||
|
||||
<span class="font-mono text-muted-foreground shrink-0">#{{ index + 1 }}</span>
|
||||
</div>
|
||||
<!-- Provider name (secondary information) — fixed per card, sourced
|
||||
from each provider's `name` export in utils/dnsleaks. -->
|
||||
<!-- Provider that actually answered (changes on fallback). -->
|
||||
<p class="w-full min-w-0 mb-1 text-xs font-mono text-muted-foreground truncate" :title="leak.providerName">
|
||||
{{ leak.providerName }}
|
||||
</p>
|
||||
@@ -108,13 +107,15 @@ 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, fastly, runWithRetry,
|
||||
ipApi, surfshark, ipleak, fastly, browserleaks,
|
||||
buildFallbackChain, runWithFallback,
|
||||
} from '@/utils/dnsleaks';
|
||||
|
||||
// One source of truth for which providers to run and in what order. Adding
|
||||
// a new provider = create the file under utils/dnsleaks and append it here.
|
||||
// (browserleaks stays available in utils/dnsleaks but is benched for now.)
|
||||
const PROVIDERS = [ipApi, surfshark, ipleak, fastly];
|
||||
// All providers in priority order: the first SLOT_COUNT hold a card each,
|
||||
// the rest are standbys. New provider = file under utils/dnsleaks + append.
|
||||
const PROVIDERS = [ipApi, surfshark, ipleak, fastly, browserleaks];
|
||||
const SLOT_COUNT = 4;
|
||||
const ACTIVE = PROVIDERS.slice(0, SLOT_COUNT);
|
||||
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -151,8 +152,11 @@ const createDefaultCard = () => ({
|
||||
org: t('dnsleaktest.StatusWait'),
|
||||
});
|
||||
|
||||
const leakTest = reactive(PROVIDERS.map((p) => ({
|
||||
// `slot` is the card's stable key; `id` / `providerName` follow the
|
||||
// provider that actually answered.
|
||||
const leakTest = reactive(ACTIVE.map((p) => ({
|
||||
...createDefaultCard(),
|
||||
slot: p.id,
|
||||
id: p.id,
|
||||
providerName: p.name,
|
||||
})));
|
||||
@@ -178,12 +182,14 @@ const markLeakCardError = (index) => {
|
||||
leakTest[index].org = t('dnsleaktest.StatusError');
|
||||
};
|
||||
|
||||
// Run one provider against its card slot, with retry (3 attempts, fresh
|
||||
// prefix per attempt — see `runWithRetry` in utils/dnsleaks). On full
|
||||
// failure (all attempts threw) the card is flipped to the error state.
|
||||
const runProvider = async (index, provider) => {
|
||||
// Run one slot through its fallback chain (see utils/dnsleaks); the card
|
||||
// flips to error only when the whole chain fails.
|
||||
const runProvider = async (index) => {
|
||||
const chain = buildFallbackChain(index, PROVIDERS, SLOT_COUNT);
|
||||
try {
|
||||
const { ip } = await runWithRetry(provider);
|
||||
const { ip, provider } = await runWithFallback(chain);
|
||||
leakTest[index].id = provider.id;
|
||||
leakTest[index].providerName = provider.name;
|
||||
leakTest[index].ip = ip;
|
||||
await applyMaxMindGeo(index, ip);
|
||||
} catch (error) {
|
||||
@@ -198,7 +204,9 @@ const checkAllDNSLeakTest = async (isRefresh) => {
|
||||
isStarted.value = true;
|
||||
if (isRefresh) {
|
||||
trackEvent('Section', 'RefreshClick', 'DNSLeakTest');
|
||||
leakTest.forEach((server) => {
|
||||
leakTest.forEach((server, index) => {
|
||||
server.id = ACTIVE[index].id;
|
||||
server.providerName = ACTIVE[index].name;
|
||||
server.ip = t('dnsleaktest.StatusWait');
|
||||
server.country = t('dnsleaktest.StatusWait');
|
||||
server.country_code = '';
|
||||
@@ -206,15 +214,13 @@ const checkAllDNSLeakTest = async (isRefresh) => {
|
||||
});
|
||||
}
|
||||
|
||||
const delayedRun = (provider, index, delay) => new Promise((resolve) => {
|
||||
const delayedRun = (index, delay) => new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
runProvider(index, provider).then(resolve, resolve);
|
||||
runProvider(index).then(resolve, resolve);
|
||||
}, delay);
|
||||
});
|
||||
|
||||
const promises = PROVIDERS.map((provider, index) =>
|
||||
delayedRun(provider, index, index * 200),
|
||||
);
|
||||
const promises = ACTIVE.map((_, index) => delayedRun(index, index * 200));
|
||||
|
||||
const allSettledPromise = Promise.allSettled(promises);
|
||||
const timeoutPromise = new Promise((resolve) => setTimeout(resolve, 6000));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Barrel + retry runner for the homepage DNS leak test providers.
|
||||
// Barrel + retry / fallback runners for the homepage DNS leak test providers.
|
||||
//
|
||||
// Each provider in this directory is a self-describing object:
|
||||
// { id, name, run() }
|
||||
@@ -21,7 +21,7 @@ export { fastly } from './fastly.js';
|
||||
// whose `ip` fails `isValidIP` counts as a failed attempt too — garbage
|
||||
// must never reach the MaxMind lookup downstream. Returns the first valid
|
||||
// result; throws the last error if every attempt failed.
|
||||
export async function runWithRetry(provider, attempts = 3) {
|
||||
export async function runWithRetry(provider, attempts = 2) {
|
||||
let lastError;
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
try {
|
||||
@@ -36,3 +36,30 @@ export async function runWithRetry(provider, attempts = 3) {
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
// Try order for card slot `index`: own provider → standbys (index ≥
|
||||
// slotCount) → the other slots' providers from 0. Each call derives a fresh
|
||||
// hostname, so a neighbour's provider still yields an independent lookup.
|
||||
export const buildFallbackChain = (index, providers, slotCount) => {
|
||||
const primary = providers[index];
|
||||
if (!primary) return [];
|
||||
const standbys = providers.slice(slotCount);
|
||||
const neighbours = providers.slice(0, slotCount).filter((p) => p !== primary);
|
||||
return [primary, ...standbys, ...neighbours];
|
||||
};
|
||||
|
||||
// Walk the chain once, each provider through `runWithRetry`; resolves
|
||||
// `{ ip, provider }` so the caller can name the upstream that answered.
|
||||
// Throws the last error.
|
||||
export const runWithFallback = async (chain) => {
|
||||
let lastError = new Error('dnsleak: empty provider chain');
|
||||
for (const provider of chain) {
|
||||
try {
|
||||
const { ip } = await runWithRetry(provider);
|
||||
return { ip, provider };
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
};
|
||||
+78
-6
@@ -1,12 +1,14 @@
|
||||
// Exercises `runWithRetry` from frontend/utils/dnsleaks — the single entry
|
||||
// point every DNS leak provider result flows through. Providers are stubbed;
|
||||
// the behaviors under test are the retry loop and the isValidIP gate that
|
||||
// keeps garbage upstream responses away from the MaxMind lookup.
|
||||
// Exercises the runners in frontend/utils/dnsleaks with stubbed providers:
|
||||
// the retry loop and its isValidIP gate (`runWithRetry`), the slot →
|
||||
// standby → neighbour ordering (`buildFallbackChain`) and the chain walk
|
||||
// that reports which provider answered (`runWithFallback`).
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import { runWithRetry } from '../frontend/utils/dnsleaks/index.js';
|
||||
import {
|
||||
runWithRetry, buildFallbackChain, runWithFallback,
|
||||
} from '../frontend/utils/dnsleaks/index.js';
|
||||
|
||||
const providerReturning = (results) => {
|
||||
let call = 0;
|
||||
@@ -63,7 +65,7 @@ describe('runWithRetry', () => {
|
||||
() => runWithRetry(provider),
|
||||
/stub: invalid IP in response/,
|
||||
);
|
||||
assert.equal(provider.calls(), 3);
|
||||
assert.equal(provider.calls(), 2);
|
||||
});
|
||||
|
||||
it('rejects a result missing the ip field entirely', async () => {
|
||||
@@ -80,3 +82,73 @@ describe('runWithRetry', () => {
|
||||
assert.equal(provider.calls(), 5);
|
||||
});
|
||||
});
|
||||
|
||||
const provider = (id, results) => ({ ...providerReturning(results), id, name: `${id}.example` });
|
||||
|
||||
describe('buildFallbackChain', () => {
|
||||
const a = provider('a', []);
|
||||
const b = provider('b', []);
|
||||
const c = provider('c', []);
|
||||
const d = provider('d', []);
|
||||
const e = provider('e', []);
|
||||
const registry = [a, b, c, d, e];
|
||||
|
||||
it('orders own provider, then the standbys beyond the slots, then the other slots from 0', () => {
|
||||
assert.deepEqual(buildFallbackChain(1, registry, 3), [b, d, e, a, c]);
|
||||
});
|
||||
|
||||
it('keeps registry order among several standbys', () => {
|
||||
assert.deepEqual(buildFallbackChain(0, registry, 2), [a, c, d, e, b]);
|
||||
});
|
||||
|
||||
it('is just the neighbours when there are no standbys', () => {
|
||||
assert.deepEqual(buildFallbackChain(2, [a, b, c], 3), [c, a, b]);
|
||||
});
|
||||
|
||||
it('is only the provider itself when it is the whole registry', () => {
|
||||
assert.deepEqual(buildFallbackChain(0, [a], 1), [a]);
|
||||
});
|
||||
|
||||
it('returns an empty chain for an index outside the registry', () => {
|
||||
assert.deepEqual(buildFallbackChain(5, registry, 4), []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('runWithFallback', () => {
|
||||
it('resolves with the primary and never touches the rest of the chain', async () => {
|
||||
const primary = provider('primary', [{ ip: '8.8.8.8' }]);
|
||||
const spare = provider('spare', [{ ip: '1.1.1.1' }]);
|
||||
const result = await runWithFallback([primary, spare]);
|
||||
assert.deepEqual(result, { ip: '8.8.8.8', provider: primary });
|
||||
assert.equal(spare.calls(), 0);
|
||||
});
|
||||
|
||||
it('falls through to the next provider once the primary exhausts its attempts', async () => {
|
||||
const dead = provider('dead', [new Error('down')]);
|
||||
const spare = provider('spare', [{ ip: '1.1.1.1' }]);
|
||||
const result = await runWithFallback([dead, spare]);
|
||||
assert.equal(result.ip, '1.1.1.1');
|
||||
assert.equal(result.provider, spare);
|
||||
assert.equal(dead.calls(), 2);
|
||||
assert.equal(spare.calls(), 1);
|
||||
});
|
||||
|
||||
it('gives every provider in the chain the default retry budget', async () => {
|
||||
const dead = provider('dead', [new Error('down')]);
|
||||
const flaky = provider('flaky', [{ ip: 'garbage' }, { ip: '9.9.9.9' }]);
|
||||
const result = await runWithFallback([dead, flaky]);
|
||||
assert.equal(result.provider, flaky);
|
||||
assert.equal(dead.calls(), 2);
|
||||
assert.equal(flaky.calls(), 2);
|
||||
});
|
||||
|
||||
it('throws the last error when the whole chain fails', async () => {
|
||||
const first = provider('first', [new Error('first down')]);
|
||||
const last = provider('last', [new Error('last down')]);
|
||||
await assert.rejects(() => runWithFallback([first, last]), /last down/);
|
||||
});
|
||||
|
||||
it('rejects an empty chain', async () => {
|
||||
await assert.rejects(() => runWithFallback([]), /empty provider chain/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user