mirror of
https://github.com/jason5ng32/MyIP.git
synced 2026-11-04 08:55:47 +00:00
Merge branch 'dev' of https://github.com/jason5ng32/MyIP into dev
This commit is contained in:
commit
4840c9ce01
21 files changed
+1446
-13
No files matched your search
+3
-2
@@ -22,7 +22,8 @@ Roughly one handler file per route: IP-geolocation sources (`ipinfo-io` /
|
||||
`maxmind`), tool backends (`get-whois` / `dns-resolver` / `mac-checker` /
|
||||
`cf-radar` / `asn-history` / `asn-connectivity` / `asn-profile` /
|
||||
`ooni-blocking` / `globalping-probes` / `service-status` / `google-map` /
|
||||
`github-stars` / `invisibility-test` / `dns-leak-test` / `persona`), user
|
||||
`github-stars` / `invisibility-test` / `dns-leak-test` / `persona` /
|
||||
`ip-blocklist`), 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.
|
||||
@@ -149,7 +150,7 @@ loading (`isStillLoading`) answers `error`, leaving the others to answer.
|
||||
|
||||
Handlers proxying our private IPCheck.ing API (`ipcheck-ing`,
|
||||
`invisibility-test`, `update-user-achievement`, `get-user-info`,
|
||||
`dns-leak-test`, `persona`, and `asn-profile`'s reputation via
|
||||
`dns-leak-test`, `persona`, `ip-blocklist`, and `asn-profile`'s reputation via
|
||||
`server/asn-reputation.js`) forward the caller's headers upstream (`headers: {
|
||||
...req.headers }`) — the upstream needs caller context (Accept-Language, auth
|
||||
tokens). `persona` strips the framing headers (`host` / `content-length` / …)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// GET /api/ipblocklist?ip= — which blocklists an IP is on, from the private
|
||||
// IPCheck.ing API (`/iphitlist`: live DNSBL lookups plus static threat lists).
|
||||
// `?ip` is validated by requirePublicIP(). Sign-in is enforced upstream, so
|
||||
// the caller's headers (auth token) are forwarded as-is.
|
||||
|
||||
import { fetchUpstream } from '../common/fetch-with-timeout.js';
|
||||
import logger from '../server/logger.js';
|
||||
|
||||
// A cold DNSBL pass queries dozens of zones under per-zone deadlines; well
|
||||
// past the default 8s, and the frontend waits a little longer than this.
|
||||
export const IP_BLOCKLIST_TIMEOUT_MS = 30000;
|
||||
|
||||
export default async (req, res) => {
|
||||
const apiKey = process.env.IPCHECKING_API_KEY;
|
||||
const apiEndpoint = process.env.IPCHECKING_API_ENDPOINT;
|
||||
if (!apiKey || !apiEndpoint) {
|
||||
return res.status(500).json({ error: 'API key is missing' });
|
||||
}
|
||||
|
||||
const url = new URL(`${apiEndpoint}/iphitlist`);
|
||||
url.searchParams.set('ip', req.query.ip);
|
||||
url.searchParams.set('key', apiKey);
|
||||
|
||||
try {
|
||||
const apiResponse = await fetchUpstream(url, {
|
||||
headers: { ...req.headers },
|
||||
timeoutMs: IP_BLOCKLIST_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
// Monthly quota exhausted: not a failure — pass status + code through
|
||||
// so the frontend shows the quota hint.
|
||||
if (apiResponse.status === 429) {
|
||||
const errorData = await apiResponse.json().catch(() => ({}));
|
||||
return res.status(429).json({ error: errorData.error || 'Monthly quota exceeded', code: 'quota_exceeded' });
|
||||
}
|
||||
|
||||
// Sign-in problems are the visitor's state, not a failure: pass the
|
||||
// status through so the frontend prompts sign-in.
|
||||
if (apiResponse.status === 401 || apiResponse.status === 403) {
|
||||
const errorData = await apiResponse.json().catch(() => ({}));
|
||||
return res.status(apiResponse.status).json({ error: errorData.message || 'Sign in required' });
|
||||
}
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
throw new Error(`API responded with status: ${apiResponse.status}`);
|
||||
}
|
||||
|
||||
res.json(await apiResponse.json());
|
||||
} catch (error) {
|
||||
logger.error({ err: error, ip: req.query.ip }, 'ip-blocklist upstream request failed');
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
@@ -37,6 +37,7 @@ import getWhois from './api/get-whois.js';
|
||||
import sentryTunnelHandler from './api/sentry-tunnel.js';
|
||||
import createReportHandler, { getReport as getReportHandler } from './api/share-report.js';
|
||||
import invisibilitytestHandler from './api/invisibility-test.js';
|
||||
import ipBlocklistHandler from './api/ip-blocklist.js';
|
||||
import macChecker from './api/mac-checker.js';
|
||||
import githubStarsHandler from './api/github-stars.js';
|
||||
import personaEvaluateHandler from './api/persona.js';
|
||||
@@ -294,6 +295,7 @@ app.get('/api/ipchecking', requirePublicIP(), withTimeZone(), ipCheckingHandler)
|
||||
app.get('/api/dnsresolver', requireValidDomain('hostname'), requireValidRecordType(), dnsResolver);
|
||||
app.get('/api/dnsleaktest/session/:token', dnsLeakGetResult);
|
||||
app.get('/api/invisibility', invisibilitytestHandler);
|
||||
app.get('/api/ipblocklist', requirePublicIP(), ipBlocklistHandler);
|
||||
app.get('/api/getuserinfo', getUserinfo);
|
||||
app.put('/api/updateuserachievement', updateUserAchievement);
|
||||
// Shared reports stay no-store: an edge cache could serve a report past its
|
||||
|
||||
@@ -167,6 +167,7 @@ const USAGE_LABEL_KEYS = {
|
||||
invisibility_test: 'invisibilitytest.Title',
|
||||
dns_leak_test: 'enhanceddnsleaktest.Title',
|
||||
persona_check: 'personacheck.Title',
|
||||
ip_hitlist: 'ipblocklist.Title',
|
||||
};
|
||||
|
||||
// Rows for the usage dialog. Empty until the (re)fetched user info lands or
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
<template>
|
||||
<div class="ip-blocklist-section my-4 space-y-4">
|
||||
<!-- Top note -->
|
||||
<div class="text-sm text-muted-foreground space-y-1.5 leading-relaxed">
|
||||
<p>{{ t('ipblocklist.Note') }}</p>
|
||||
<p>{{ t('ipblocklist.Note2') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Input area. With stored IPs (homepage drawer) the user can pick one
|
||||
OR switch to manual entry; on the standalone page allIPs is empty,
|
||||
so it's manual entry only. -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<Label :for="manualMode ? 'blocklistIPManual' : 'blocklistIP'" class="font-medium">
|
||||
{{ manualMode ? t('ipblocklist.EnterIPLabel') : t('ipblocklist.SelectIPLabel') }}
|
||||
</Label>
|
||||
<div v-if="allIPs.length" class="flex items-center gap-2 shrink-0">
|
||||
<Switch id="blocklistUseStored" v-model="useStored" :disabled="isRunning" />
|
||||
<Label for="blocklistUseStored" class="font-normal text-muted-foreground cursor-pointer">
|
||||
{{ t('ipblocklist.UseStored') }}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Select v-if="!manualMode" v-model="selectedIP" :disabled="isRunning">
|
||||
<SelectTrigger id="blocklistIP" aria-label="Select IP to check" class="flex-1 min-w-0">
|
||||
<SelectValue :placeholder="t('ipblocklist.SelectIP')" class="truncate font-mono" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="item in allIPs" :key="item.ip" :value="item.ip">
|
||||
<span class="flex items-center gap-1 min-w-0">
|
||||
<Icon v-if="item.country" :icon="'circle-flags:' + item.country.toLowerCase()"
|
||||
class="size-3.5 md:size-4 shrink-0" />
|
||||
<span class="font-mono truncate text-xs md:text-sm">{{ item.ip }}</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input v-else id="blocklistIPManual" v-model="manualIP" class="flex-1 font-mono"
|
||||
:placeholder="t('ipblocklist.EnterIPPlaceholder')" :disabled="isRunning"
|
||||
:aria-invalid="inputInvalid" autocomplete="off"
|
||||
autocorrect="off" autocapitalize="off" spellcheck="false" data-1p-ignore data-lpignore="true"
|
||||
@keyup.enter="runCheck" />
|
||||
<Button variant="action" class="cursor-pointer" :disabled="!canRun" @click="runCheck"
|
||||
aria-label="Run blocklist check">
|
||||
<Spinner v-if="isRunning" />
|
||||
<Play v-else class="size-4 shrink-0" />
|
||||
</Button>
|
||||
</div>
|
||||
<p v-if="inputInvalid" class="text-sm text-destructive">{{ t('ipblocklist.InvalidIP') }}</p>
|
||||
|
||||
<!-- Login hint -->
|
||||
<div v-if="!store.user"
|
||||
class="flex items-start gap-2 p-3 rounded-md border border-info/30 bg-info/10 text-sm text-info">
|
||||
<Info class="size-4 mt-0.5 shrink-0" />
|
||||
<span>{{ t('user.SignInToUse') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Running: a cold DNSBL pass can take a while -->
|
||||
<p v-if="isRunning" class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Spinner class="size-4" />{{ t('ipblocklist.Checking') }}
|
||||
</p>
|
||||
|
||||
<p v-if="errorMsg" class="text-sm text-destructive">{{ errorMsg }}</p>
|
||||
|
||||
<!-- Monthly quota exhausted: not an error — explain + usage / sponsor path -->
|
||||
<div v-if="quotaExceeded"
|
||||
class="flex items-start gap-2 p-3 rounded-md border border-warning/30 bg-warning/10 text-sm text-warning">
|
||||
<Hourglass class="size-4 mt-0.5 shrink-0" />
|
||||
<span>
|
||||
{{ t('user.QuotaExceeded') }}
|
||||
<button type="button" class="underline underline-offset-2 cursor-pointer"
|
||||
@click="openUsageDialog">{{ t('user.ViewUsage') }}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result -->
|
||||
<template v-if="result">
|
||||
<!-- Summary: IP, verdict, four counts -->
|
||||
<Card>
|
||||
<CardContent class="p-4 md:p-6 space-y-4">
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
|
||||
<span class="font-mono font-semibold text-lg break-all">{{ result.ip }}</span>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ t('ipblocklist.CheckedAt', { time: isoToDateTime(result.checkedAt, lang) }) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-2 text-sm font-medium" :class="TONE_TEXT[verdict]">
|
||||
<component :is="TONE_ICON[verdict]" class="size-4 mt-0.5 shrink-0" />
|
||||
<span>{{ t(`ipblocklist.verdict.${verdict}`) }}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<div v-for="tile in tiles" :key="tile.key" class="rounded-md bg-muted/50 px-3 py-2">
|
||||
<div class="text-xs text-muted-foreground">{{ t(`ipblocklist.count.${tile.key}`) }}</div>
|
||||
<div class="text-2xl font-semibold tabular-nums"
|
||||
:class="tile.value > 0 ? TONE_TEXT[tile.key] : 'text-muted-foreground'">
|
||||
{{ tile.value }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="result.summary?.error > 0" class="text-xs text-muted-foreground">
|
||||
{{ t('ipblocklist.SomeFailed', { n: result.summary.error }) }}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- One card per part: hits open, the rest folded -->
|
||||
<Card v-for="part in parts" :key="part.id">
|
||||
<CardContent class="p-0">
|
||||
<header class="flex flex-wrap items-start justify-between gap-x-4 gap-y-1 px-4 py-3 border-b">
|
||||
<div class="min-w-0">
|
||||
<h3 class="flex items-center gap-2 text-sm font-semibold m-0">
|
||||
<component :is="part.icon" class="size-4 text-muted-foreground shrink-0" />
|
||||
{{ t(`ipblocklist.part.${part.id}.title`) }}
|
||||
</h3>
|
||||
<p class="text-xs text-muted-foreground mt-0.5 mb-0">
|
||||
{{ t(`ipblocklist.part.${part.id}.desc`) }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-xs text-muted-foreground tabular-nums">
|
||||
{{ partCounts(part.summary) }}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<!-- Hits -->
|
||||
<ul v-if="part.hits.length" class="divide-y list-none p-0 m-0">
|
||||
<li v-for="item in part.hits" :key="item.id" class="flex items-start gap-3 px-4 py-3">
|
||||
<component :is="TONE_ICON[itemTone(item)]" class="size-4 mt-0.5 shrink-0"
|
||||
:class="TONE_TEXT[itemTone(item)]" />
|
||||
<div class="flex-1 min-w-0 space-y-1.5">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-medium">{{ item.name }}</span>
|
||||
<Badge variant="outline" class="font-normal text-muted-foreground">
|
||||
{{ categoryLabel(item.category) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div v-if="item.reasons.length" class="flex flex-wrap gap-1.5">
|
||||
<span v-for="reason in item.reasons" :key="reason"
|
||||
class="rounded-md px-2 py-0.5 text-xs" :class="TONE_CHIP[itemTone(item)]">
|
||||
{{ reasonLabel(reason) }}
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="item.lastSeen" class="text-xs text-muted-foreground m-0">
|
||||
{{ t('ipblocklist.LastSeen', { date: formatIsoDate(item.lastSeen, lang) }) }}
|
||||
</p>
|
||||
</div>
|
||||
<a v-if="item.removalUrl && item.status === 'listed'" :href="item.removalUrl"
|
||||
target="_blank" rel="noopener"
|
||||
class="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground whitespace-nowrap">
|
||||
{{ t('ipblocklist.RequestRemoval') }}
|
||||
<ExternalLink class="size-3" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="px-4 py-3 text-sm text-muted-foreground m-0">
|
||||
{{ t('ipblocklist.NoHits') }}
|
||||
</p>
|
||||
|
||||
<!-- Everything else, folded -->
|
||||
<Collapsible v-if="part.rest.length" v-model:open="openRest[part.id]">
|
||||
<CollapsibleTrigger as-child>
|
||||
<button type="button"
|
||||
class="flex w-full items-center justify-center gap-1.5 border-t px-4 py-2.5 text-sm text-muted-foreground hover:bg-muted/30 hover:text-foreground cursor-pointer">
|
||||
<ChevronDown class="size-4 transition-transform duration-200"
|
||||
:class="{ 'rotate-180': openRest[part.id] }" />
|
||||
{{ openRest[part.id] ? t('ipblocklist.HideRest') : t('ipblocklist.ShowRest', { n: part.rest.length }) }}
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<ul class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-4 gap-y-1.5 list-none m-0 px-4 py-3 border-t">
|
||||
<li v-for="item in part.rest" :key="item.id"
|
||||
class="flex items-center gap-2 min-w-0 text-sm">
|
||||
<component :is="TONE_ICON[itemTone(item)]" class="size-4 shrink-0"
|
||||
:class="TONE_TEXT[itemTone(item)]" />
|
||||
<span class="truncate" :class="{ 'text-muted-foreground': item.status !== 'clean' }"
|
||||
:title="item.name">{{ item.name }}</span>
|
||||
<span v-if="item.status === 'error'" class="shrink-0 text-xs text-muted-foreground">
|
||||
{{ t('ipblocklist.CheckFailed') }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Legend: what each mark means, for every row above -->
|
||||
<ul class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-1.5 list-none m-0 p-0 text-xs text-muted-foreground">
|
||||
<li v-for="tone in LEGEND" :key="tone" class="flex items-start gap-1.5">
|
||||
<component :is="TONE_ICON[tone]" class="size-3.5 mt-px shrink-0" :class="TONE_TEXT[tone]" />
|
||||
<span>
|
||||
<span class="font-medium text-foreground">{{ t(`ipblocklist.legend.${tone}.label`) }}</span>
|
||||
{{ t(`ipblocklist.legend.${tone}.desc`) }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// IP Blocklist Check: which blocklists an IP is on — live DNSBLs and static
|
||||
// threat lists — via /api/ipblocklist (the private API's /iphitlist; signed-in
|
||||
// users, original site only). Verdict / row split live in
|
||||
// utils/features/ip-blocklist.js.
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useMainStore } from '@/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { trackEvent } from '@/utils/analytics';
|
||||
import { authenticatedFetch } from '@/utils/authenticated-fetch';
|
||||
import { isoToDateTime, formatIsoDate } from '@/utils/time-utils.js';
|
||||
import { blocklistVerdict, listedCounts, splitPart, itemTone, reasonMessage } from '@/utils/features/ip-blocklist.js';
|
||||
import { selectableIPs, classifyTarget } from '@/composables/use-globalping-measurement';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import {
|
||||
ChevronDown, CircleAlert, CircleCheck, CircleHelp, CircleX, ExternalLink, Hourglass, Info, ListChecks, Play, Radar,
|
||||
} from '@lucide/vue';
|
||||
|
||||
const { t, te } = useI18n();
|
||||
const store = useMainStore();
|
||||
const lang = computed(() => store.lang);
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
// Open the Benefits & Usage dialog. Inside the Advanced Tools drawer the
|
||||
// dialog would stack on top of it — close the drawer first (it's driven by
|
||||
// the ?tool query; standalone pages don't carry it, nothing to close).
|
||||
const openUsageDialog = () => {
|
||||
if (route.query.tool) router.push({ path: '/', query: {} });
|
||||
store.setTriggerUserBenefits(true);
|
||||
};
|
||||
|
||||
// The backend waits up to 30s on the upstream; give it room to answer first.
|
||||
const CLIENT_TIMEOUT_MS = 35000;
|
||||
|
||||
// Tone → classes / icons, one circled-icon family for every row and the
|
||||
// verdict: abuse ✕, anonymity !, notice i, clean ✓, error ?.
|
||||
const TONE_TEXT = {
|
||||
abuse: 'text-destructive', anonymity: 'text-warning', notice: 'text-info', clean: 'text-success', error: 'text-muted-foreground',
|
||||
};
|
||||
const TONE_CHIP = { abuse: 'bg-destructive/10 text-destructive', anonymity: 'bg-warning/10 text-warning', notice: 'bg-info/10 text-info' };
|
||||
const TONE_ICON = { abuse: CircleX, anonymity: CircleAlert, notice: Info, clean: CircleCheck, error: CircleHelp };
|
||||
const LEGEND = ['abuse', 'anonymity', 'notice', 'clean', 'error'];
|
||||
|
||||
// ── Input ────────────────────────────────────────────────────────────────
|
||||
// Manual entry is forced when there are no stored IPs (the standalone page);
|
||||
// otherwise the switch (on by default) picks between dropdown and typing.
|
||||
const allIPs = computed(() => selectableIPs(store.allIPs));
|
||||
const useStored = ref(true);
|
||||
const manualMode = computed(() => allIPs.value.length === 0 || !useStored.value);
|
||||
const selectedIP = ref('');
|
||||
const manualIP = ref('');
|
||||
// The typed IP is validated on submit, not while typing (like Whois): the
|
||||
// button only needs something entered, and a rejected value flags the input
|
||||
// until the next run.
|
||||
const inputInvalid = ref(false);
|
||||
const targetIP = computed(() => (manualMode.value ? manualIP.value.trim() : selectedIP.value));
|
||||
|
||||
const status = ref('idle'); // 'idle' | 'running'
|
||||
const isRunning = computed(() => status.value === 'running');
|
||||
const canRun = computed(() => !isRunning.value && !!store.user && !!targetIP.value);
|
||||
|
||||
// ── Result ───────────────────────────────────────────────────────────────
|
||||
const result = ref(null);
|
||||
const errorMsg = ref('');
|
||||
const quotaExceeded = ref(false);
|
||||
const openRest = ref({});
|
||||
|
||||
const verdict = computed(() => blocklistVerdict(result.value?.summary));
|
||||
const tiles = computed(() => {
|
||||
const s = result.value?.summary ?? {};
|
||||
const listed = listedCounts(s);
|
||||
return [
|
||||
{ key: 'abuse', value: listed.abuse },
|
||||
{ key: 'anonymity', value: listed.anonymity },
|
||||
{ key: 'notice', value: s.notice ?? 0 },
|
||||
{ key: 'clean', value: s.clean ?? 0 },
|
||||
];
|
||||
});
|
||||
const parts = computed(() => [
|
||||
{ id: 'dnsbl', icon: Radar },
|
||||
{ id: 'static', icon: ListChecks },
|
||||
]
|
||||
.filter(({ id }) => result.value?.[id])
|
||||
.map((p) => ({ ...p, summary: result.value[p.id].summary, ...splitPart(result.value[p.id]) })));
|
||||
|
||||
// "2 abuse · 1 anonymity · 41 clean" — non-zero counts only.
|
||||
const partCounts = (s) => [
|
||||
['abuse', listedCounts(s).abuse],
|
||||
['anonymity', listedCounts(s).anonymity],
|
||||
['notice', s?.notice],
|
||||
['clean', s?.clean],
|
||||
['error', s?.error],
|
||||
]
|
||||
.filter(([, n]) => n > 0)
|
||||
.map(([key, n]) => `${t(`ipblocklist.count.${key}`)} ${n}`)
|
||||
.join(' · ');
|
||||
|
||||
// Unknown keys (added upstream before a translation lands) show as-is.
|
||||
const reasonLabel = (reason) => {
|
||||
const { key, params } = reasonMessage(reason);
|
||||
return te(key) ? t(key, params) : reason;
|
||||
};
|
||||
const categoryLabel = (category) => {
|
||||
const key = `ipblocklist.category.${category}`;
|
||||
return te(key) ? t(key) : category;
|
||||
};
|
||||
|
||||
// ── Run ──────────────────────────────────────────────────────────────────
|
||||
const runCheck = async () => {
|
||||
if (!canRun.value) return;
|
||||
// Public IPv4 / IPv6 only ('invalid' / 'unreachable' = not an IP / not public).
|
||||
inputInvalid.value = manualMode.value && classifyTarget(targetIP.value) !== 'ok';
|
||||
if (inputInvalid.value) return;
|
||||
// Frontend first line: the quota snapshot already says the month is used
|
||||
// up. The backend enforces the same limit (429) as the real guard.
|
||||
if (store.quotaExceeded.ip_hitlist) {
|
||||
quotaExceeded.value = true;
|
||||
return;
|
||||
}
|
||||
status.value = 'running';
|
||||
errorMsg.value = '';
|
||||
quotaExceeded.value = false;
|
||||
result.value = null;
|
||||
openRest.value = {};
|
||||
trackEvent('Section', 'StartClick', 'IpBlocklist');
|
||||
try {
|
||||
const ip = encodeURIComponent(targetIP.value);
|
||||
result.value = await authenticatedFetch(`/api/ipblocklist?ip=${ip}`, 'GET', null, CLIENT_TIMEOUT_MS);
|
||||
} catch (error) {
|
||||
// 429: monthly quota exhausted — show the quota hint, not an error.
|
||||
if (error.status === 429) {
|
||||
quotaExceeded.value = true;
|
||||
store.markQuotaExhausted('ip_hitlist');
|
||||
return;
|
||||
}
|
||||
console.error('IP blocklist check failed:', error);
|
||||
// 401/403: the visitor's sign-in state, not a failure to retry.
|
||||
if (error.status === 401 || error.status === 403) {
|
||||
errorMsg.value = error.message.includes('Invalid token') ? t('user.InvalidUserToken') : t('user.SignInToUse');
|
||||
} else {
|
||||
errorMsg.value = t('ipblocklist.FetchError');
|
||||
}
|
||||
} finally {
|
||||
status.value = 'idle';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -228,6 +228,12 @@ const buildShortcutConfig = ({ refs, store, t, configs, userPreferences }) => {
|
||||
action: () => goToAdvancedTool('enhanceddnsleaktest', 'EnhancedDnsLeakTest'),
|
||||
description: t('shortcutKeys.EnhancedDnsLeakTest'),
|
||||
});
|
||||
// Uppercase B: lowercase `b` belongs to Browser Info.
|
||||
config.push({
|
||||
keys: 'B',
|
||||
action: () => goToAdvancedTool('blocklist', 'IpBlocklist'),
|
||||
description: t('shortcutKeys.IpBlocklist'),
|
||||
});
|
||||
// Uppercase P: lowercase `p` belongs to Earth Online.
|
||||
config.push({
|
||||
keys: 'P',
|
||||
|
||||
@@ -2208,5 +2208,55 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "v7.8.0",
|
||||
"date": "Beta",
|
||||
"content": [
|
||||
{
|
||||
"type": "add",
|
||||
"change": {
|
||||
"en": "New tool: IP Blocklist Check — see whether an IP is on about 80 real-time DNSBLs and threat intelligence lists",
|
||||
"zh": "新增工具:IP 黑名单检查——查看 IP 是否出现在约 80 个实时 DNSBL 与威胁情报名单上",
|
||||
"zh-TW": "新增工具:IP 黑名單檢查——查看 IP 是否出現在約 80 個即時 DNSBL 與威脅情報名單上",
|
||||
"fr": "Nouvel outil : Vérification des listes noires IP — vérifiez si une IP figure sur environ 80 DNSBL en temps réel et listes de renseignement sur les menaces",
|
||||
"ru": "Новый инструмент: проверка IP по чёрным спискам — узнайте, есть ли IP примерно в 80 DNSBL реального времени и списках киберразведки",
|
||||
"pt-BR": "Nova ferramenta: Verificação de listas negras de IP — veja se um IP está em cerca de 80 DNSBLs em tempo real e listas de inteligência de ameaças"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "improve",
|
||||
"change": {
|
||||
"en": "Advanced Tools are now grouped into Network Diagnostics, Lookup & Calculation and In-depth Checks",
|
||||
"zh": "高级工具改为按网络诊断、查询与计算、深度检测分组展示",
|
||||
"zh-TW": "進階工具改為按網路診斷、查詢與計算、深度檢測分組展示",
|
||||
"fr": "Les Outils avancés sont désormais regroupés en Diagnostic réseau, Recherche et calcul et Contrôles approfondis",
|
||||
"ru": "Расширенные инструменты теперь разделены на группы: «Диагностика сети», «Поиск и расчёты» и «Углублённые проверки»",
|
||||
"pt-BR": "As Ferramentas avançadas agora são agrupadas em Diagnóstico de rede, Consulta e cálculo e Verificações aprofundadas"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "improve",
|
||||
"change": {
|
||||
"en": "Reorganized the project's directory structure for easier maintenance",
|
||||
"zh": "重构了程序的目录结构,提高可维护性",
|
||||
"zh-TW": "重構了程式的目錄結構,提高可維護性",
|
||||
"fr": "Réorganisation de la structure des répertoires du projet pour faciliter la maintenance",
|
||||
"ru": "Реорганизована структура каталогов проекта для упрощения поддержки",
|
||||
"pt-BR": "Reorganizada a estrutura de diretórios do projeto para facilitar a manutenção"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "fix",
|
||||
"change": {
|
||||
"en": "Fixed some issues",
|
||||
"zh": "修复了一些问题",
|
||||
"zh-TW": "修正了一些問題",
|
||||
"fr": "Correction de divers problèmes",
|
||||
"ru": "Исправлены некоторые проблемы",
|
||||
"pt-BR": "Corrigidos alguns problemas"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -34,6 +34,7 @@ export const ADVANCED_TOOLS = [
|
||||
{ slug: 'ipcalculator', emoji: '🔢', titleKey: 'ipcalculator.Title', noteKey: 'advancedtools.IpCalculator', category: 'lookup', component: () => import('@/components/advanced-tools/IpCalculator.vue') },
|
||||
{ slug: 'invisibilitytest', emoji: '🫣', titleKey: 'invisibilitytest.Title', noteKey: 'advancedtools.InvisibilityTest', category: 'deep', component: () => import('@/components/advanced-tools/InvisibilityTest.vue'), requiresOriginalSite: true },
|
||||
{ slug: 'enhanceddnsleaktest', emoji: '🌀', titleKey: 'enhanceddnsleaktest.Title', noteKey: 'advancedtools.EnhancedDnsLeakTest', category: 'deep', component: () => import('@/components/advanced-tools/EnhancedDnsLeakTest.vue'), requiresOriginalSite: true },
|
||||
{ slug: 'blocklist', emoji: '🚫', titleKey: 'ipblocklist.Title', noteKey: 'advancedtools.IpBlocklist', category: 'deep', component: () => import('@/components/advanced-tools/IpBlocklist.vue'), requiresOriginalSite: true },
|
||||
// noStandalone: the check requires the homepage tests' results, and running
|
||||
// them navigates home — a /tools/ page for it would immediately bounce away.
|
||||
{ slug: 'personacheck', emoji: '🎭', titleKey: 'personacheck.Title', noteKey: 'advancedtools.PersonaCheck', category: 'deep', component: () => import('@/components/advanced-tools/PersonaCheck.vue'), requiresOriginalSite: true, noStandalone: true },
|
||||
|
||||
+120
-1
@@ -857,6 +857,123 @@
|
||||
"no": "No"
|
||||
}
|
||||
},
|
||||
"ipblocklist": {
|
||||
"Title": "IP Blocklist Check",
|
||||
"Note": "Checks an IP against about 80 public blocklists in two groups. Real-time blocklists (DNSBLs) are queried live by mail servers, forums and online services to decide whether to accept a connection or a message; threat intelligence lists are published feeds of abusive addresses, open proxies, VPN servers and Tor exits that firewalls and security services load. Being listed is often why emails get rejected, sign-ups get blocked or a site keeps asking for CAPTCHAs.",
|
||||
"Note2": "Each listing falls into one of three kinds. Abuse means a list has recorded spam, attacks or a compromised device at this address. Anonymity means the address is known as a proxy, VPN, relay or Tor exit, which is expected when you use one and isn't a bad record by itself. Advisory entries flag the wider network, such as a dynamic range or a whole provider, rather than this address. Shared, cloud and VPN addresses often turn up somewhere, so treat a listing as a signal rather than a verdict; where a list offers a removal page, it's linked next to the listing.",
|
||||
"SelectIPLabel": "Pick an IP to check",
|
||||
"EnterIPLabel": "Enter an IP to check",
|
||||
"UseStored": "Use a detected IP",
|
||||
"SelectIP": "Select an IP",
|
||||
"EnterIPPlaceholder": "e.g. 1.1.1.1",
|
||||
"InvalidIP": "Enter a public IPv4 or IPv6 address.",
|
||||
"Checking": "Checking about 80 blocklists. This can take up to 30 seconds.",
|
||||
"FetchError": "Couldn't finish the check. Try again in a moment.",
|
||||
"CheckedAt": "Checked {time}",
|
||||
"verdict": {
|
||||
"abuse": "This IP has a record of abuse on some blocklists",
|
||||
"anonymity": "No abuse record, but this IP is marked as a proxy, VPN, relay or Tor exit",
|
||||
"notice": "Not on any blocklist, though some lists flag its wider network",
|
||||
"clean": "Not on any of the blocklists checked"
|
||||
},
|
||||
"count": {
|
||||
"abuse": "Abuse",
|
||||
"anonymity": "Anonymity",
|
||||
"notice": "Advisory",
|
||||
"clean": "Clean",
|
||||
"error": "Failed"
|
||||
},
|
||||
"SomeFailed": "{n} of the lists couldn't be checked this time, so the result may be incomplete.",
|
||||
"part": {
|
||||
"dnsbl": {
|
||||
"title": "Real-time blocklists (DNSBL)",
|
||||
"desc": "Queried live by mail servers and online services"
|
||||
},
|
||||
"static": {
|
||||
"title": "Threat intelligence lists",
|
||||
"desc": "Abuse, proxy and VPN lists used by firewalls and security services"
|
||||
}
|
||||
},
|
||||
"NoHits": "Not listed here.",
|
||||
"ShowRest": "Show the other {n} lists",
|
||||
"HideRest": "Hide",
|
||||
"CheckFailed": "Check failed",
|
||||
"LastSeen": "Last seen {date}",
|
||||
"RequestRemoval": "Request removal",
|
||||
"category": {
|
||||
"spam": "Spam",
|
||||
"abuse": "Abuse",
|
||||
"proxy": "Proxy",
|
||||
"vpn": "VPN",
|
||||
"relay": "Relay",
|
||||
"tor": "Tor",
|
||||
"scanner": "Scanner",
|
||||
"backscatter": "Backscatter",
|
||||
"mixed": "Mixed"
|
||||
},
|
||||
"reason": {
|
||||
"socksProxy": "SOCKS proxy",
|
||||
"httpProxy": "HTTP proxy",
|
||||
"proxyChain": "Proxy chain",
|
||||
"webProxy": "Web proxy",
|
||||
"wingateProxy": "WinGate proxy",
|
||||
"openProxy": "Open proxy",
|
||||
"torExit": "Tor exit node",
|
||||
"exploited": "Exploited host",
|
||||
"compromisedHost": "Compromised host",
|
||||
"compromisedRouter": "Compromised router",
|
||||
"botnet": "Botnet",
|
||||
"worm": "Worm infection",
|
||||
"ircDrone": "IRC drone",
|
||||
"ddosDrone": "DDoS drone",
|
||||
"bottler": "Bottler",
|
||||
"spambot": "Spambot",
|
||||
"bruteForce": "Brute-force attacks",
|
||||
"spamSource": "Spam source",
|
||||
"spamtrap": "Hit a spam trap",
|
||||
"badReputation": "Bad reputation",
|
||||
"hijackedNetwork": "Hijacked network",
|
||||
"endUserRange": "End-user address range",
|
||||
"dynamicRange": "Dynamic address range",
|
||||
"noMailRange": "Not meant to send mail",
|
||||
"missingPtr": "No reverse DNS",
|
||||
"poorReputation": "Poor reputation",
|
||||
"mixedReputation": "Mixed reputation",
|
||||
"rangeListed": "Address range listed",
|
||||
"networkListed": "Whole network listed",
|
||||
"unaccountableOperator": "Operator doesn't act on abuse",
|
||||
"noMailService": "No mail service",
|
||||
"unreliableAbuseDesk": "Unreliable abuse contact",
|
||||
"reported1d": "Reported in the last day",
|
||||
"reported7d": "Reported in the last 7 days",
|
||||
"reported30d": "Reported in the last 30 days",
|
||||
"reported90d": "Reported in the last 90 days",
|
||||
"feeds": "Reported by {n}+ threat feeds",
|
||||
"testEntry": "Test entry"
|
||||
},
|
||||
"legend": {
|
||||
"abuse": {
|
||||
"label": "Abuse",
|
||||
"desc": "the list recorded spam, attacks or a compromised device at this address"
|
||||
},
|
||||
"anonymity": {
|
||||
"label": "Anonymity",
|
||||
"desc": "a known proxy, VPN, relay or Tor exit, which isn't a bad record by itself"
|
||||
},
|
||||
"notice": {
|
||||
"label": "Advisory",
|
||||
"desc": "flags the wider network or provider, not this address"
|
||||
},
|
||||
"clean": {
|
||||
"label": "Not listed",
|
||||
"desc": "this IP isn't on the list"
|
||||
},
|
||||
"error": {
|
||||
"label": "Check failed",
|
||||
"desc": "the list couldn't be checked this time, which doesn't mean the IP isn't on it"
|
||||
}
|
||||
}
|
||||
},
|
||||
"advancedtools": {
|
||||
"OpenInNewTab": "Open in new tab",
|
||||
"BackToHome": "Back to Home",
|
||||
@@ -881,7 +998,8 @@
|
||||
"IpCalculator": "Subnet math, notation conversion and IPv6 insight for any IP, prefix or range",
|
||||
"BrowserInfo": "Check browser information and fingerprint",
|
||||
"SecurityChecklist": "Guide to securing your digital life",
|
||||
"PersonaCheck": "Compare what sites see against the country you want to look local to"
|
||||
"PersonaCheck": "Compare what sites see against the country you want to look local to",
|
||||
"IpBlocklist": "See whether an IP is on spam, abuse, proxy or VPN blocklists"
|
||||
},
|
||||
"macchecker": {
|
||||
"Title": "MAC Lookup",
|
||||
@@ -1847,6 +1965,7 @@
|
||||
"BrowserInfo": "Open Browser Info panel",
|
||||
"SecurityChecklist": "Open Security Checklist panel",
|
||||
"PersonaCheck": "Open In-depth Persona Check panel",
|
||||
"IpBlocklist": "Open IP Blocklist Check panel",
|
||||
"AsnProfile": "Open ASN Profile Lookup panel"
|
||||
},
|
||||
"page": {
|
||||
|
||||
+120
-1
@@ -857,6 +857,123 @@
|
||||
"no": "Non"
|
||||
}
|
||||
},
|
||||
"ipblocklist": {
|
||||
"Title": "Vérification des listes noires IP",
|
||||
"Note": "Vérifie une IP auprès d’environ 80 listes noires publiques, réparties en deux groupes. Les listes noires en temps réel (DNSBL) sont interrogées en direct par les serveurs de messagerie, les forums et les services en ligne pour décider d’accepter ou non une connexion ou un message ; les listes de renseignement sur les menaces sont des flux publics d’adresses abusives, de proxys ouverts, de serveurs VPN et de sorties Tor, chargés par les pare-feu et les services de sécurité. Figurer sur ces listes explique souvent des e-mails refusés, des inscriptions bloquées ou des CAPTCHA à répétition.",
|
||||
"Note2": "Chaque inscription relève de l’un de trois types. Abus signifie qu’une liste a enregistré du spam, des attaques ou un appareil compromis à cette adresse. Anonymat signifie que l’adresse est connue comme proxy, VPN, relais ou sortie Tor, ce qui est normal si vous en utilisez un et ne constitue pas un mauvais antécédent en soi. Les indications visent le réseau plus large, comme une plage dynamique ou un fournisseur entier, plutôt que cette adresse. Les adresses partagées, cloud et VPN apparaissent souvent quelque part : voyez une inscription comme un signal plutôt qu’un verdict ; lorsqu’une liste propose une page de retrait, le lien figure à côté de l’inscription.",
|
||||
"SelectIPLabel": "Choisissez une IP à vérifier",
|
||||
"EnterIPLabel": "Saisissez une IP à vérifier",
|
||||
"UseStored": "Utiliser une IP détectée",
|
||||
"SelectIP": "Choisissez une IP",
|
||||
"EnterIPPlaceholder": "ex. 1.1.1.1",
|
||||
"InvalidIP": "Saisissez une adresse IPv4 ou IPv6 publique.",
|
||||
"Checking": "Vérification d’environ 80 listes noires. Cela peut prendre jusqu’à 30 secondes.",
|
||||
"FetchError": "La vérification n’a pas abouti. Réessayez dans un instant.",
|
||||
"CheckedAt": "Vérifié le {time}",
|
||||
"verdict": {
|
||||
"abuse": "Cette IP a des antécédents d’abus sur certaines listes noires",
|
||||
"anonymity": "Aucun antécédent d’abus, mais cette IP est marquée comme proxy, VPN, relais ou sortie Tor",
|
||||
"notice": "Absente des listes noires, mais certaines listes signalent son réseau",
|
||||
"clean": "Absente de toutes les listes noires vérifiées"
|
||||
},
|
||||
"count": {
|
||||
"abuse": "Abus",
|
||||
"anonymity": "Anonymat",
|
||||
"notice": "Indication",
|
||||
"clean": "Propre",
|
||||
"error": "Échec"
|
||||
},
|
||||
"SomeFailed": "{n} listes n’ont pas pu être vérifiées cette fois ; le résultat peut être incomplet.",
|
||||
"part": {
|
||||
"dnsbl": {
|
||||
"title": "Listes noires en temps réel (DNSBL)",
|
||||
"desc": "Interrogées en direct par les serveurs de messagerie et les services en ligne"
|
||||
},
|
||||
"static": {
|
||||
"title": "Listes de renseignement sur les menaces",
|
||||
"desc": "Listes d’abus, de proxy et de VPN utilisées par les pare-feu et les services de sécurité"
|
||||
}
|
||||
},
|
||||
"NoHits": "Aucune inscription ici.",
|
||||
"ShowRest": "Afficher les {n} autres listes",
|
||||
"HideRest": "Masquer",
|
||||
"CheckFailed": "Échec de la vérification",
|
||||
"LastSeen": "Vue pour la dernière fois le {date}",
|
||||
"RequestRemoval": "Demander le retrait",
|
||||
"category": {
|
||||
"spam": "Spam",
|
||||
"abuse": "Abus",
|
||||
"proxy": "Proxy",
|
||||
"vpn": "VPN",
|
||||
"relay": "Relais",
|
||||
"tor": "Tor",
|
||||
"scanner": "Scanner",
|
||||
"backscatter": "Backscatter",
|
||||
"mixed": "Mixte"
|
||||
},
|
||||
"reason": {
|
||||
"socksProxy": "Proxy SOCKS",
|
||||
"httpProxy": "Proxy HTTP",
|
||||
"proxyChain": "Chaîne de proxys",
|
||||
"webProxy": "Proxy web",
|
||||
"wingateProxy": "Proxy WinGate",
|
||||
"openProxy": "Proxy ouvert",
|
||||
"torExit": "Nœud de sortie Tor",
|
||||
"exploited": "Hôte exploité",
|
||||
"compromisedHost": "Hôte compromis",
|
||||
"compromisedRouter": "Routeur compromis",
|
||||
"botnet": "Botnet",
|
||||
"worm": "Infection par un ver",
|
||||
"ircDrone": "Drone IRC",
|
||||
"ddosDrone": "Drone DDoS",
|
||||
"bottler": "Bottler",
|
||||
"spambot": "Spambot",
|
||||
"bruteForce": "Attaques par force brute",
|
||||
"spamSource": "Source de spam",
|
||||
"spamtrap": "Piège à spam déclenché",
|
||||
"badReputation": "Mauvaise réputation",
|
||||
"hijackedNetwork": "Réseau détourné",
|
||||
"endUserRange": "Plage d’utilisateurs finaux",
|
||||
"dynamicRange": "Plage d’adresses dynamiques",
|
||||
"noMailRange": "Plage non destinée à envoyer du courrier",
|
||||
"missingPtr": "Pas de DNS inverse",
|
||||
"poorReputation": "Réputation faible",
|
||||
"mixedReputation": "Réputation mitigée",
|
||||
"rangeListed": "Plage d’adresses listée",
|
||||
"networkListed": "Réseau entier listé",
|
||||
"unaccountableOperator": "L’opérateur ne traite pas les abus",
|
||||
"noMailService": "Pas de service de messagerie",
|
||||
"unreliableAbuseDesk": "Contact abus peu fiable",
|
||||
"reported1d": "Signalée au cours du dernier jour",
|
||||
"reported7d": "Signalée au cours des 7 derniers jours",
|
||||
"reported30d": "Signalée au cours des 30 derniers jours",
|
||||
"reported90d": "Signalée au cours des 90 derniers jours",
|
||||
"feeds": "Signalée par au moins {n} sources de menaces",
|
||||
"testEntry": "Entrée de test"
|
||||
},
|
||||
"legend": {
|
||||
"abuse": {
|
||||
"label": "Abus",
|
||||
"desc": "la liste a enregistré du spam, des attaques ou un appareil compromis à cette adresse"
|
||||
},
|
||||
"anonymity": {
|
||||
"label": "Anonymat",
|
||||
"desc": "proxy, VPN, relais ou sortie Tor connu, ce qui n’est pas un mauvais antécédent en soi"
|
||||
},
|
||||
"notice": {
|
||||
"label": "Indication",
|
||||
"desc": "vise le réseau ou le fournisseur plus large, pas cette adresse"
|
||||
},
|
||||
"clean": {
|
||||
"label": "Non listée",
|
||||
"desc": "cette IP ne figure pas sur la liste"
|
||||
},
|
||||
"error": {
|
||||
"label": "Échec de la vérification",
|
||||
"desc": "la liste n’a pas pu être vérifiée cette fois, ce qui ne signifie pas que l’IP n’y figure pas"
|
||||
}
|
||||
}
|
||||
},
|
||||
"advancedtools": {
|
||||
"OpenInNewTab": "Ouvrir dans un nouvel onglet",
|
||||
"BackToHome": "Retour à l'accueil",
|
||||
@@ -881,7 +998,8 @@
|
||||
"IpCalculator": "Calcul de sous-réseau, conversion de notation et analyse IPv6 pour toute IP, préfixe ou plage",
|
||||
"BrowserInfo": "Vérifier les informations du navigateur et l'empreinte digitale",
|
||||
"SecurityChecklist": "Guide pour sécuriser votre vie numérique",
|
||||
"PersonaCheck": "Comparez ce que voient les sites avec le pays dont vous voulez avoir l'air d'être un habitant"
|
||||
"PersonaCheck": "Comparez ce que voient les sites avec le pays dont vous voulez avoir l'air d'être un habitant",
|
||||
"IpBlocklist": "Vérifiez si une IP figure sur des listes noires de spam, d’abus, de proxy ou de VPN"
|
||||
},
|
||||
"macchecker": {
|
||||
"Title": "Recherche MAC",
|
||||
@@ -1847,6 +1965,7 @@
|
||||
"BrowserInfo": "Ouvrir l'Info du navigateur",
|
||||
"SecurityChecklist": "Ouvrir la Liste de sécurité",
|
||||
"PersonaCheck": "Ouvrir le panneau Vérification de persona approfondie",
|
||||
"IpBlocklist": "Ouvrir la Vérification des listes noires IP",
|
||||
"AsnProfile": "Ouvrir le panneau Recherche de profil ASN"
|
||||
},
|
||||
"page": {
|
||||
|
||||
+120
-1
@@ -857,6 +857,123 @@
|
||||
"no": "Não"
|
||||
}
|
||||
},
|
||||
"ipblocklist": {
|
||||
"Title": "Verificação de listas negras de IP",
|
||||
"Note": "Verifica um IP em cerca de 80 listas negras públicas, divididas em dois grupos. As listas negras em tempo real (DNSBLs) são consultadas ao vivo por servidores de e-mail, fóruns e serviços online para decidir se aceitam uma conexão ou mensagem; as listas de inteligência de ameaças são feeds públicos de endereços abusivos, proxies abertos, servidores VPN e saídas Tor, carregados por firewalls e serviços de segurança. Estar nessas listas costuma explicar e-mails recusados, cadastros bloqueados ou CAPTCHAs sem fim.",
|
||||
"Note2": "Cada ocorrência se encaixa em um de três tipos. Abuso significa que uma lista registrou spam, ataques ou um dispositivo comprometido neste endereço. Anonimato significa que o endereço é conhecido como proxy, VPN, relay ou saída Tor, o que é esperado se você usa um e não é, por si só, um registro ruim. Avisos apontam para a rede mais ampla, como uma faixa dinâmica ou uma operadora inteira, e não para este endereço. Endereços compartilhados, de nuvem e de VPN costumam aparecer em alguma lista, então trate uma ocorrência como um sinal, não como um veredito; quando a lista oferece uma página de remoção, o link aparece ao lado da ocorrência.",
|
||||
"SelectIPLabel": "Escolha um IP para verificar",
|
||||
"EnterIPLabel": "Digite um IP para verificar",
|
||||
"UseStored": "Usar um IP detectado",
|
||||
"SelectIP": "Selecionar um IP",
|
||||
"EnterIPPlaceholder": "ex.: 1.1.1.1",
|
||||
"InvalidIP": "Digite um endereço IPv4 ou IPv6 público.",
|
||||
"Checking": "Verificando cerca de 80 listas negras. Isso pode levar até 30 segundos.",
|
||||
"FetchError": "Não foi possível concluir a verificação. Tente novamente em instantes.",
|
||||
"CheckedAt": "Verificado em {time}",
|
||||
"verdict": {
|
||||
"abuse": "Este IP tem registro de abuso em algumas listas negras",
|
||||
"anonymity": "Sem registro de abuso, mas este IP está marcado como proxy, VPN, relay ou saída Tor",
|
||||
"notice": "Fora das listas negras, mas algumas listas sinalizam a rede dele",
|
||||
"clean": "Não está em nenhuma das listas negras verificadas"
|
||||
},
|
||||
"count": {
|
||||
"abuse": "Abuso",
|
||||
"anonymity": "Anonimato",
|
||||
"notice": "Aviso",
|
||||
"clean": "Limpo",
|
||||
"error": "Falhou"
|
||||
},
|
||||
"SomeFailed": "Não foi possível verificar {n} das listas desta vez; o resultado pode estar incompleto.",
|
||||
"part": {
|
||||
"dnsbl": {
|
||||
"title": "Listas negras em tempo real (DNSBL)",
|
||||
"desc": "Consultadas ao vivo por servidores de e-mail e serviços online"
|
||||
},
|
||||
"static": {
|
||||
"title": "Listas de inteligência de ameaças",
|
||||
"desc": "Listas de abuso, proxy e VPN usadas por firewalls e serviços de segurança"
|
||||
}
|
||||
},
|
||||
"NoHits": "Nenhuma ocorrência aqui.",
|
||||
"ShowRest": "Mostrar as outras {n} listas",
|
||||
"HideRest": "Ocultar",
|
||||
"CheckFailed": "Falha na verificação",
|
||||
"LastSeen": "Visto pela última vez em {date}",
|
||||
"RequestRemoval": "Pedir remoção",
|
||||
"category": {
|
||||
"spam": "Spam",
|
||||
"abuse": "Abuso",
|
||||
"proxy": "Proxy",
|
||||
"vpn": "VPN",
|
||||
"relay": "Relay",
|
||||
"tor": "Tor",
|
||||
"scanner": "Scanner",
|
||||
"backscatter": "Backscatter",
|
||||
"mixed": "Misto"
|
||||
},
|
||||
"reason": {
|
||||
"socksProxy": "Proxy SOCKS",
|
||||
"httpProxy": "Proxy HTTP",
|
||||
"proxyChain": "Cadeia de proxies",
|
||||
"webProxy": "Proxy web",
|
||||
"wingateProxy": "Proxy WinGate",
|
||||
"openProxy": "Proxy aberto",
|
||||
"torExit": "Nó de saída Tor",
|
||||
"exploited": "Host explorado",
|
||||
"compromisedHost": "Host comprometido",
|
||||
"compromisedRouter": "Roteador comprometido",
|
||||
"botnet": "Botnet",
|
||||
"worm": "Infecção por worm",
|
||||
"ircDrone": "Drone IRC",
|
||||
"ddosDrone": "Drone DDoS",
|
||||
"bottler": "Bottler",
|
||||
"spambot": "Spambot",
|
||||
"bruteForce": "Ataques de força bruta",
|
||||
"spamSource": "Origem de spam",
|
||||
"spamtrap": "Caiu em armadilha de spam",
|
||||
"badReputation": "Má reputação",
|
||||
"hijackedNetwork": "Rede sequestrada",
|
||||
"endUserRange": "Faixa de usuários finais",
|
||||
"dynamicRange": "Faixa de endereços dinâmicos",
|
||||
"noMailRange": "Não deveria enviar e-mail",
|
||||
"missingPtr": "Sem DNS reverso",
|
||||
"poorReputation": "Reputação baixa",
|
||||
"mixedReputation": "Reputação mista",
|
||||
"rangeListed": "Faixa de endereços listada",
|
||||
"networkListed": "Rede inteira listada",
|
||||
"unaccountableOperator": "Operadora não trata abusos",
|
||||
"noMailService": "Sem serviço de e-mail",
|
||||
"unreliableAbuseDesk": "Contato de abuso pouco confiável",
|
||||
"reported1d": "Denunciado no último dia",
|
||||
"reported7d": "Denunciado nos últimos 7 dias",
|
||||
"reported30d": "Denunciado nos últimos 30 dias",
|
||||
"reported90d": "Denunciado nos últimos 90 dias",
|
||||
"feeds": "Denunciado por pelo menos {n} fontes de ameaças",
|
||||
"testEntry": "Entrada de teste"
|
||||
},
|
||||
"legend": {
|
||||
"abuse": {
|
||||
"label": "Abuso",
|
||||
"desc": "a lista registrou spam, ataques ou um dispositivo comprometido neste endereço"
|
||||
},
|
||||
"anonymity": {
|
||||
"label": "Anonimato",
|
||||
"desc": "proxy, VPN, relay ou saída Tor conhecido, o que não é um registro ruim por si só"
|
||||
},
|
||||
"notice": {
|
||||
"label": "Aviso",
|
||||
"desc": "aponta para a rede ou operadora mais ampla, não para este endereço"
|
||||
},
|
||||
"clean": {
|
||||
"label": "Não listado",
|
||||
"desc": "este IP não está na lista"
|
||||
},
|
||||
"error": {
|
||||
"label": "Falha na verificação",
|
||||
"desc": "não foi possível verificar a lista desta vez, o que não significa que o IP não esteja nela"
|
||||
}
|
||||
}
|
||||
},
|
||||
"advancedtools": {
|
||||
"OpenInNewTab": "Abrir em nova aba",
|
||||
"BackToHome": "Voltar para o início",
|
||||
@@ -881,7 +998,8 @@
|
||||
"IpCalculator": "Cálculo de sub-rede, conversão de notação e análise IPv6 para qualquer IP, prefixo ou intervalo",
|
||||
"BrowserInfo": "Verificar informações e impressão digital do navegador",
|
||||
"SecurityChecklist": "Guia para proteger sua vida digital",
|
||||
"PersonaCheck": "Compare o que os sites veem com o país no qual você quer parecer local"
|
||||
"PersonaCheck": "Compare o que os sites veem com o país no qual você quer parecer local",
|
||||
"IpBlocklist": "Veja se um IP está em listas negras de spam, abuso, proxy ou VPN"
|
||||
},
|
||||
"macchecker": {
|
||||
"Title": "Consulta de MAC",
|
||||
@@ -1847,6 +1965,7 @@
|
||||
"BrowserInfo": "Abrir painel de informações do navegador",
|
||||
"SecurityChecklist": "Abrir painel de checklist de segurança",
|
||||
"PersonaCheck": "Abrir painel de verificação aprofundada de persona",
|
||||
"IpBlocklist": "Abrir painel de verificação de listas negras de IP",
|
||||
"AsnProfile": "Abrir painel de consulta de perfil de ASN"
|
||||
},
|
||||
"page": {
|
||||
|
||||
+120
-1
@@ -857,6 +857,123 @@
|
||||
"no": "Нет"
|
||||
}
|
||||
},
|
||||
"ipblocklist": {
|
||||
"Title": "Проверка IP по чёрным спискам",
|
||||
"Note": "Проверяет IP примерно по 80 публичным чёрным спискам двух видов. Чёрные списки реального времени (DNSBL) почтовые серверы, форумы и онлайн-сервисы запрашивают в момент подключения или получения письма, чтобы решить, принимать ли его; списки киберразведки — это публикуемые перечни адресов-нарушителей, открытых прокси, VPN-серверов и выходов Tor, которые загружают межсетевые экраны и службы безопасности. Именно попадание в такие списки часто объясняет отклонённые письма, заблокированные регистрации и бесконечные CAPTCHA.",
|
||||
"Note2": "Каждое совпадение относится к одному из трёх видов. «Злоупотребления» — список зафиксировал с этого адреса спам, атаки или взломанное устройство. «Анонимность» — адрес известен как прокси, VPN, ретранслятор или выход Tor; если вы пользуетесь таким сервисом, это ожидаемо и само по себе не плохая запись. «Справочно» — отметка касается более широкой сети, например динамического диапазона или целого провайдера, а не этого адреса. Общие, облачные и VPN-адреса часто где-нибудь встречаются, поэтому воспринимайте совпадение как сигнал, а не приговор; если у списка есть страница удаления, ссылка на неё стоит рядом с совпадением.",
|
||||
"SelectIPLabel": "Выберите IP для проверки",
|
||||
"EnterIPLabel": "Введите IP для проверки",
|
||||
"UseStored": "Обнаруженный IP",
|
||||
"SelectIP": "Выберите IP",
|
||||
"EnterIPPlaceholder": "например, 1.1.1.1",
|
||||
"InvalidIP": "Введите публичный IPv4 или IPv6.",
|
||||
"Checking": "Проверяем около 80 чёрных списков. Это может занять до 30 секунд.",
|
||||
"FetchError": "Не удалось завершить проверку. Попробуйте чуть позже.",
|
||||
"CheckedAt": "Проверено {time}",
|
||||
"verdict": {
|
||||
"abuse": "У этого IP есть записи о злоупотреблениях в некоторых чёрных списках",
|
||||
"anonymity": "Записей о злоупотреблениях нет, но IP отмечен как прокси, VPN, ретранслятор или выход Tor",
|
||||
"notice": "В чёрных списках нет, но некоторые списки отмечают его сеть",
|
||||
"clean": "Нет ни в одном из проверенных чёрных списков"
|
||||
},
|
||||
"count": {
|
||||
"abuse": "Злоупотребления",
|
||||
"anonymity": "Анонимность",
|
||||
"notice": "Справочно",
|
||||
"clean": "Чисто",
|
||||
"error": "Ошибка"
|
||||
},
|
||||
"SomeFailed": "Списков, которые не удалось проверить: {n}. Результат может быть неполным.",
|
||||
"part": {
|
||||
"dnsbl": {
|
||||
"title": "Чёрные списки реального времени (DNSBL)",
|
||||
"desc": "Их в реальном времени запрашивают почтовые серверы и онлайн-сервисы"
|
||||
},
|
||||
"static": {
|
||||
"title": "Списки киберразведки",
|
||||
"desc": "Списки злоупотреблений, прокси и VPN для межсетевых экранов и служб безопасности"
|
||||
}
|
||||
},
|
||||
"NoHits": "Здесь совпадений нет.",
|
||||
"ShowRest": "Показать остальные списки ({n})",
|
||||
"HideRest": "Скрыть",
|
||||
"CheckFailed": "Ошибка проверки",
|
||||
"LastSeen": "Последний раз замечен {date}",
|
||||
"RequestRemoval": "Запросить удаление",
|
||||
"category": {
|
||||
"spam": "Спам",
|
||||
"abuse": "Злоупотребления",
|
||||
"proxy": "Прокси",
|
||||
"vpn": "VPN",
|
||||
"relay": "Ретранслятор",
|
||||
"tor": "Tor",
|
||||
"scanner": "Сканер",
|
||||
"backscatter": "Backscatter",
|
||||
"mixed": "Смешанный"
|
||||
},
|
||||
"reason": {
|
||||
"socksProxy": "SOCKS-прокси",
|
||||
"httpProxy": "HTTP-прокси",
|
||||
"proxyChain": "Цепочка прокси",
|
||||
"webProxy": "Веб-прокси",
|
||||
"wingateProxy": "Прокси WinGate",
|
||||
"openProxy": "Открытый прокси",
|
||||
"torExit": "Выходной узел Tor",
|
||||
"exploited": "Эксплуатируемый хост",
|
||||
"compromisedHost": "Взломанный хост",
|
||||
"compromisedRouter": "Взломанный маршрутизатор",
|
||||
"botnet": "Ботнет",
|
||||
"worm": "Заражение червём",
|
||||
"ircDrone": "IRC-дрон",
|
||||
"ddosDrone": "DDoS-дрон",
|
||||
"bottler": "Bottler",
|
||||
"spambot": "Спам-бот",
|
||||
"bruteForce": "Атаки перебором",
|
||||
"spamSource": "Источник спама",
|
||||
"spamtrap": "Попал в спам-ловушку",
|
||||
"badReputation": "Плохая репутация",
|
||||
"hijackedNetwork": "Захваченная сеть",
|
||||
"endUserRange": "Диапазон конечных пользователей",
|
||||
"dynamicRange": "Динамический диапазон",
|
||||
"noMailRange": "Не предназначен для отправки почты",
|
||||
"missingPtr": "Нет обратной DNS-записи",
|
||||
"poorReputation": "Низкая репутация",
|
||||
"mixedReputation": "Неоднозначная репутация",
|
||||
"rangeListed": "Внесён диапазон адресов",
|
||||
"networkListed": "Внесена вся сеть",
|
||||
"unaccountableOperator": "Оператор не реагирует на жалобы",
|
||||
"noMailService": "Нет почтового сервиса",
|
||||
"unreliableAbuseDesk": "Ненадёжный контакт для жалоб",
|
||||
"reported1d": "Жалобы за последние сутки",
|
||||
"reported7d": "Жалобы за последние 7 дней",
|
||||
"reported30d": "Жалобы за последние 30 дней",
|
||||
"reported90d": "Жалобы за последние 90 дней",
|
||||
"feeds": "Отмечен минимум в {n} источниках угроз",
|
||||
"testEntry": "Тестовая запись"
|
||||
},
|
||||
"legend": {
|
||||
"abuse": {
|
||||
"label": "Злоупотребления",
|
||||
"desc": "список зафиксировал с этого адреса спам, атаки или взломанное устройство"
|
||||
},
|
||||
"anonymity": {
|
||||
"label": "Анонимность",
|
||||
"desc": "известный прокси, VPN, ретранслятор или выход Tor — само по себе не плохая запись"
|
||||
},
|
||||
"notice": {
|
||||
"label": "Справочно",
|
||||
"desc": "отметка касается более широкой сети или провайдера, а не этого адреса"
|
||||
},
|
||||
"clean": {
|
||||
"label": "Нет в списке",
|
||||
"desc": "этого IP в списке нет"
|
||||
},
|
||||
"error": {
|
||||
"label": "Ошибка проверки",
|
||||
"desc": "список не удалось проверить в этот раз — это не значит, что IP в нём нет"
|
||||
}
|
||||
}
|
||||
},
|
||||
"advancedtools": {
|
||||
"OpenInNewTab": "Открыть в новой вкладке",
|
||||
"BackToHome": "На главную",
|
||||
@@ -881,7 +998,8 @@
|
||||
"IpCalculator": "Расчёт подсетей, преобразование записей и анализ IPv6 для любого IP, префикса или диапазона",
|
||||
"BrowserInfo": "Проверка сведений и отпечатка браузера",
|
||||
"SecurityChecklist": "Руководство по защите цифровой жизни",
|
||||
"PersonaCheck": "Сравните то, что видят сайты, со страной, местным жителем которой вы хотите выглядеть"
|
||||
"PersonaCheck": "Сравните то, что видят сайты, со страной, местным жителем которой вы хотите выглядеть",
|
||||
"IpBlocklist": "Проверьте, есть ли IP в чёрных списках спама, злоупотреблений, прокси или VPN"
|
||||
},
|
||||
"macchecker": {
|
||||
"Title": "Поиск MAC-адреса",
|
||||
@@ -1847,6 +1965,7 @@
|
||||
"BrowserInfo": "Открыть панель сведений о браузере",
|
||||
"SecurityChecklist": "Открыть панель контрольного списка безопасности",
|
||||
"PersonaCheck": "Открыть панель углублённой проверки портрета",
|
||||
"IpBlocklist": "Открыть панель проверки IP по чёрным спискам",
|
||||
"AsnProfile": "Открыть панель поиска профиля ASN"
|
||||
},
|
||||
"page": {
|
||||
|
||||
+120
-1
@@ -857,6 +857,123 @@
|
||||
"no": "否"
|
||||
}
|
||||
},
|
||||
"ipblocklist": {
|
||||
"Title": "IP 黑名單檢查",
|
||||
"Note": "用約 80 個公開黑名單檢查一個 IP,分為兩類。即時黑名單(DNSBL)由郵件伺服器、論壇和各類網路服務在收到連線或郵件時即時查詢,用來決定是否接受;威脅情報名單則是公開發布的濫用位址、開放代理、VPN 伺服器和 Tor 出口清單,被防火牆和安全服務大量採用。郵件被拒收、註冊被攔截、網站反覆跳出驗證碼,常常就是因為 IP 出現在了這些名單上。",
|
||||
"Note2": "每一筆命中分為三種性質。「不良紀錄」表示名單記錄過這個位址發送垃圾郵件、發起攻擊或裝置被入侵;「匿名標記」表示這個位址是已知的代理、VPN、中繼或 Tor 出口,如果你正在使用這類服務,被標記是正常的,本身並不算不良紀錄;「參考」針對的是更大的網路範圍,例如動態位址段或整個營運商,而不是這個位址本身。共享、雲端伺服器和 VPN 的位址常常會出現在某些名單上,所以請把命中當作訊號而不是定論;如果名單提供移除申請頁面,會在命中條目旁給出連結。",
|
||||
"SelectIPLabel": "選擇要檢查的 IP",
|
||||
"EnterIPLabel": "輸入要檢查的 IP",
|
||||
"UseStored": "使用偵測到的 IP",
|
||||
"SelectIP": "選擇一個 IP",
|
||||
"EnterIPPlaceholder": "例如 1.1.1.1",
|
||||
"InvalidIP": "請輸入一個公網 IPv4 或 IPv6。",
|
||||
"Checking": "正在檢查約 80 個黑名單,最長可能需要 30 秒。",
|
||||
"FetchError": "檢查沒能完成,請稍後再試。",
|
||||
"CheckedAt": "檢查於 {time}",
|
||||
"verdict": {
|
||||
"abuse": "該 IP 在部分黑名單上有不良紀錄",
|
||||
"anonymity": "沒有不良紀錄,但被標記為代理、VPN、中繼或 Tor 出口",
|
||||
"notice": "未被列入黑名單,但部分名單標記了它所在的網路",
|
||||
"clean": "未出現在任何已檢查的黑名單上"
|
||||
},
|
||||
"count": {
|
||||
"abuse": "不良紀錄",
|
||||
"anonymity": "匿名標記",
|
||||
"notice": "參考",
|
||||
"clean": "乾淨",
|
||||
"error": "失敗"
|
||||
},
|
||||
"SomeFailed": "本次有 {n} 個名單查詢失敗,結果可能不完整。",
|
||||
"part": {
|
||||
"dnsbl": {
|
||||
"title": "即時黑名單(DNSBL)",
|
||||
"desc": "郵件伺服器和網路服務即時查詢的黑名單"
|
||||
},
|
||||
"static": {
|
||||
"title": "威脅情報名單",
|
||||
"desc": "防火牆和安全服務常用的濫用、代理與 VPN 名單"
|
||||
}
|
||||
},
|
||||
"NoHits": "這裡沒有命中。",
|
||||
"ShowRest": "查看其餘 {n} 個名單",
|
||||
"HideRest": "收起",
|
||||
"CheckFailed": "查詢失敗",
|
||||
"LastSeen": "最近出現:{date}",
|
||||
"RequestRemoval": "申請移除",
|
||||
"category": {
|
||||
"spam": "垃圾郵件",
|
||||
"abuse": "濫用",
|
||||
"proxy": "代理",
|
||||
"vpn": "VPN",
|
||||
"relay": "中繼",
|
||||
"tor": "Tor",
|
||||
"scanner": "掃描",
|
||||
"backscatter": "退信散射",
|
||||
"mixed": "綜合"
|
||||
},
|
||||
"reason": {
|
||||
"socksProxy": "SOCKS 代理",
|
||||
"httpProxy": "HTTP 代理",
|
||||
"proxyChain": "代理鏈",
|
||||
"webProxy": "Web 代理",
|
||||
"wingateProxy": "WinGate 代理",
|
||||
"openProxy": "開放代理",
|
||||
"torExit": "Tor 出口節點",
|
||||
"exploited": "被利用的主機",
|
||||
"compromisedHost": "被入侵的主機",
|
||||
"compromisedRouter": "被入侵的路由器",
|
||||
"botnet": "殭屍網路",
|
||||
"worm": "蠕蟲感染",
|
||||
"ircDrone": "IRC 傀儡機",
|
||||
"ddosDrone": "DDoS 傀儡機",
|
||||
"bottler": "Bottler 惡意程式",
|
||||
"spambot": "垃圾郵件機器人",
|
||||
"bruteForce": "暴力破解",
|
||||
"spamSource": "垃圾郵件來源",
|
||||
"spamtrap": "觸發垃圾郵件陷阱",
|
||||
"badReputation": "信譽差",
|
||||
"hijackedNetwork": "被劫持的網路",
|
||||
"endUserRange": "終端使用者位址段",
|
||||
"dynamicRange": "動態位址段",
|
||||
"noMailRange": "不應發送郵件的位址段",
|
||||
"missingPtr": "缺少反向解析",
|
||||
"poorReputation": "信譽偏低",
|
||||
"mixedReputation": "信譽參半",
|
||||
"rangeListed": "所在網段被列入",
|
||||
"networkListed": "所在網路整體被列入",
|
||||
"unaccountableOperator": "營運者不處理濫用投訴",
|
||||
"noMailService": "沒有郵件服務",
|
||||
"unreliableAbuseDesk": "濫用投訴管道不可靠",
|
||||
"reported1d": "近 1 天內被檢舉",
|
||||
"reported7d": "近 7 天內被檢舉",
|
||||
"reported30d": "近 30 天內被檢舉",
|
||||
"reported90d": "近 90 天內被檢舉",
|
||||
"feeds": "被至少 {n} 個威脅情報來源回報",
|
||||
"testEntry": "測試條目"
|
||||
},
|
||||
"legend": {
|
||||
"abuse": {
|
||||
"label": "不良紀錄",
|
||||
"desc": "名單記錄過這個位址的垃圾郵件、攻擊或入侵行為"
|
||||
},
|
||||
"anonymity": {
|
||||
"label": "匿名標記",
|
||||
"desc": "已知的代理、VPN、中繼或 Tor 出口,本身不算不良紀錄"
|
||||
},
|
||||
"notice": {
|
||||
"label": "參考",
|
||||
"desc": "標記的是所在網段或營運商,而不是這個位址"
|
||||
},
|
||||
"clean": {
|
||||
"label": "未列入",
|
||||
"desc": "名單上沒有這個 IP"
|
||||
},
|
||||
"error": {
|
||||
"label": "查詢失敗",
|
||||
"desc": "本次沒能查詢這個名單,不代表沒有列入"
|
||||
}
|
||||
}
|
||||
},
|
||||
"advancedtools": {
|
||||
"OpenInNewTab": "在新標籤頁開啟",
|
||||
"BackToHome": "回到首頁",
|
||||
@@ -881,7 +998,8 @@
|
||||
"IpCalculator": "任意 IP、前綴或範圍的子網路計算、進位轉換與 IPv6 解讀",
|
||||
"BrowserInfo": "檢閱瀏覽器資訊和指紋",
|
||||
"SecurityChecklist": "全面的數位生活安全檢查清單",
|
||||
"PersonaCheck": "把網站看到的你,和你想扮成的本地人做對照"
|
||||
"PersonaCheck": "把網站看到的你,和你想扮成的本地人做對照",
|
||||
"IpBlocklist": "查看 IP 是否出現在垃圾郵件、濫用、代理或 VPN 黑名單上"
|
||||
},
|
||||
"macchecker": {
|
||||
"Title": "MAC 位址查詢",
|
||||
@@ -1847,6 +1965,7 @@
|
||||
"BrowserInfo": "開啟瀏覽器資訊面板",
|
||||
"SecurityChecklist": "開啟安全檢查清單",
|
||||
"PersonaCheck": "開啟深度畫像檢測面板",
|
||||
"IpBlocklist": "開啟 IP 黑名單檢查面板",
|
||||
"AsnProfile": "開啟 ASN 檔案查詢面板"
|
||||
},
|
||||
"page": {
|
||||
|
||||
+120
-1
@@ -857,6 +857,123 @@
|
||||
"no": "否"
|
||||
}
|
||||
},
|
||||
"ipblocklist": {
|
||||
"Title": "IP 黑名单检查",
|
||||
"Note": "用约 80 个公开黑名单检查一个 IP,分为两类。实时黑名单(DNSBL)由邮件服务器、论坛和各类网络服务在收到连接或邮件时实时查询,用来决定是否接受;威胁情报名单则是公开发布的滥用地址、开放代理、VPN 服务器和 Tor 出口清单,被防火墙和安全服务大量采用。邮件被拒收、注册被拦截、网站反复弹出验证码,常常就是因为 IP 出现在了这些名单上。",
|
||||
"Note2": "每一条命中分为三种性质。「不良记录」表示名单记录过这个地址发送垃圾邮件、发起攻击或设备被入侵;「匿名标记」表示这个地址是已知的代理、VPN、中继或 Tor 出口,如果你正在使用这类服务,被标记是正常的,本身并不算不良记录;「参考」针对的是更大的网络范围,比如动态地址段或整个运营商,而不是这个地址本身。共享、云服务器和 VPN 的地址常常会出现在某些名单上,所以请把命中当作信号而不是定论;如果名单提供移除申请页面,会在命中条目旁给出链接。",
|
||||
"SelectIPLabel": "选择要检查的 IP",
|
||||
"EnterIPLabel": "输入要检查的 IP",
|
||||
"UseStored": "使用检测到的 IP",
|
||||
"SelectIP": "选择一个 IP",
|
||||
"EnterIPPlaceholder": "例如 1.1.1.1",
|
||||
"InvalidIP": "请输入一个公网 IPv4 或 IPv6。",
|
||||
"Checking": "正在检查约 80 个黑名单,最长可能需要 30 秒。",
|
||||
"FetchError": "检查没能完成,请稍后再试。",
|
||||
"CheckedAt": "检查于 {time}",
|
||||
"verdict": {
|
||||
"abuse": "该 IP 在部分黑名单上有不良记录",
|
||||
"anonymity": "没有不良记录,但被标记为代理、VPN、中继或 Tor 出口",
|
||||
"notice": "未被列入黑名单,但部分名单标记了它所在的网络",
|
||||
"clean": "未出现在任何已检查的黑名单上"
|
||||
},
|
||||
"count": {
|
||||
"abuse": "不良记录",
|
||||
"anonymity": "匿名标记",
|
||||
"notice": "参考",
|
||||
"clean": "干净",
|
||||
"error": "失败"
|
||||
},
|
||||
"SomeFailed": "本次有 {n} 个名单查询失败,结果可能不完整。",
|
||||
"part": {
|
||||
"dnsbl": {
|
||||
"title": "实时黑名单(DNSBL)",
|
||||
"desc": "邮件服务器和网络服务实时查询的黑名单"
|
||||
},
|
||||
"static": {
|
||||
"title": "威胁情报名单",
|
||||
"desc": "防火墙和安全服务常用的滥用、代理与 VPN 名单"
|
||||
}
|
||||
},
|
||||
"NoHits": "这里没有命中。",
|
||||
"ShowRest": "查看其余 {n} 个名单",
|
||||
"HideRest": "收起",
|
||||
"CheckFailed": "查询失败",
|
||||
"LastSeen": "最近出现:{date}",
|
||||
"RequestRemoval": "申请移除",
|
||||
"category": {
|
||||
"spam": "垃圾邮件",
|
||||
"abuse": "滥用",
|
||||
"proxy": "代理",
|
||||
"vpn": "VPN",
|
||||
"relay": "中继",
|
||||
"tor": "Tor",
|
||||
"scanner": "扫描",
|
||||
"backscatter": "退信散射",
|
||||
"mixed": "综合"
|
||||
},
|
||||
"reason": {
|
||||
"socksProxy": "SOCKS 代理",
|
||||
"httpProxy": "HTTP 代理",
|
||||
"proxyChain": "代理链",
|
||||
"webProxy": "Web 代理",
|
||||
"wingateProxy": "WinGate 代理",
|
||||
"openProxy": "开放代理",
|
||||
"torExit": "Tor 出口节点",
|
||||
"exploited": "被利用的主机",
|
||||
"compromisedHost": "被入侵的主机",
|
||||
"compromisedRouter": "被入侵的路由器",
|
||||
"botnet": "僵尸网络",
|
||||
"worm": "蠕虫感染",
|
||||
"ircDrone": "IRC 傀儡机",
|
||||
"ddosDrone": "DDoS 傀儡机",
|
||||
"bottler": "Bottler 恶意程序",
|
||||
"spambot": "垃圾邮件机器人",
|
||||
"bruteForce": "暴力破解",
|
||||
"spamSource": "垃圾邮件来源",
|
||||
"spamtrap": "触发垃圾邮件陷阱",
|
||||
"badReputation": "信誉差",
|
||||
"hijackedNetwork": "被劫持的网络",
|
||||
"endUserRange": "终端用户地址段",
|
||||
"dynamicRange": "动态地址段",
|
||||
"noMailRange": "不应发送邮件的地址段",
|
||||
"missingPtr": "缺少反向解析",
|
||||
"poorReputation": "信誉偏低",
|
||||
"mixedReputation": "信誉参半",
|
||||
"rangeListed": "所在网段被列入",
|
||||
"networkListed": "所在网络整体被列入",
|
||||
"unaccountableOperator": "运营者不处理滥用投诉",
|
||||
"noMailService": "没有邮件服务",
|
||||
"unreliableAbuseDesk": "滥用投诉渠道不可靠",
|
||||
"reported1d": "近 1 天内被举报",
|
||||
"reported7d": "近 7 天内被举报",
|
||||
"reported30d": "近 30 天内被举报",
|
||||
"reported90d": "近 90 天内被举报",
|
||||
"feeds": "被至少 {n} 个威胁情报源报告",
|
||||
"testEntry": "测试条目"
|
||||
},
|
||||
"legend": {
|
||||
"abuse": {
|
||||
"label": "不良记录",
|
||||
"desc": "名单记录过这个地址的垃圾邮件、攻击或入侵行为"
|
||||
},
|
||||
"anonymity": {
|
||||
"label": "匿名标记",
|
||||
"desc": "已知的代理、VPN、中继或 Tor 出口,本身不算不良记录"
|
||||
},
|
||||
"notice": {
|
||||
"label": "参考",
|
||||
"desc": "标记的是所在网段或运营商,而不是这个地址"
|
||||
},
|
||||
"clean": {
|
||||
"label": "未列入",
|
||||
"desc": "名单上没有这个 IP"
|
||||
},
|
||||
"error": {
|
||||
"label": "查询失败",
|
||||
"desc": "本次没能查询这个名单,不代表没有列入"
|
||||
}
|
||||
}
|
||||
},
|
||||
"advancedtools": {
|
||||
"OpenInNewTab": "在新标签页打开",
|
||||
"BackToHome": "回到首页",
|
||||
@@ -881,7 +998,8 @@
|
||||
"IpCalculator": "任意 IP、前缀或范围的子网计算、进制转换与 IPv6 解读",
|
||||
"BrowserInfo": "检阅浏览器信息和指纹",
|
||||
"SecurityChecklist": "全面的数字生活安全检查清单",
|
||||
"PersonaCheck": "把网站看到的你,和你想扮成的本地人做对照"
|
||||
"PersonaCheck": "把网站看到的你,和你想扮成的本地人做对照",
|
||||
"IpBlocklist": "查看 IP 是否出现在垃圾邮件、滥用、代理或 VPN 黑名单上"
|
||||
},
|
||||
"macchecker": {
|
||||
"Title": "MAC 地址查询",
|
||||
@@ -1847,6 +1965,7 @@
|
||||
"BrowserInfo": "打开浏览器信息面板",
|
||||
"SecurityChecklist": "打开安全检查清单",
|
||||
"PersonaCheck": "打开深度画像检测面板",
|
||||
"IpBlocklist": "打开 IP 黑名单检查面板",
|
||||
"AsnProfile": "打开 ASN 档案查询面板"
|
||||
},
|
||||
"page": {
|
||||
|
||||
+3
-2
@@ -115,8 +115,8 @@ export const useMainStore = defineStore('main', {
|
||||
// data (signed out, old backend, fetch pending) reads as not exceeded.
|
||||
//
|
||||
// Metering differs per feature: invisibility_test / dns_leak_test /
|
||||
// persona_check count requests, so exhausted means every further run is
|
||||
// blocked and their components use this as a pre-flight gate.
|
||||
// persona_check / ip_hitlist count requests, so exhausted means every
|
||||
// further run is blocked and their components use this as a pre-flight gate.
|
||||
quotaExceeded: (state) => {
|
||||
const features = state.remoteUserInfo?.quota?.features || {};
|
||||
const exceeded = (key) => features[key]?.exhausted === true;
|
||||
@@ -125,6 +125,7 @@ export const useMainStore = defineStore('main', {
|
||||
invisibility_test: exceeded('invisibility_test'),
|
||||
dns_leak_test: exceeded('dns_leak_test'),
|
||||
persona_check: exceeded('persona_check'),
|
||||
ip_hitlist: exceeded('ip_hitlist'),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Pure layer of the IP Blocklist Check (advanced-tools/IpBlocklist.vue) over
|
||||
// the /api/ipblocklist answer — the private API's /iphitlist: two parts
|
||||
// (`dnsbl`, `static`), each `{ checkedAt, summary, results }`, every item
|
||||
// carrying a status (listed / notice / clean / error) and a kind (abuse /
|
||||
// anonymity). The API decides kinds and order; this only picks what to show.
|
||||
|
||||
// Listed counts by kind. An answer without `listedBy` counts every listing as
|
||||
// abuse — never let a hit read as clean.
|
||||
export const listedCounts = (summary) => ({
|
||||
abuse: summary?.listedBy?.abuse ?? summary?.listed ?? 0,
|
||||
anonymity: summary?.listedBy?.anonymity ?? 0,
|
||||
});
|
||||
|
||||
// Headline verdict, worst first: a bad record outweighs an anonymizer mark,
|
||||
// which outweighs advisory notices.
|
||||
export const blocklistVerdict = (summary) => {
|
||||
const listedBy = listedCounts(summary);
|
||||
if (listedBy.abuse > 0) return 'abuse';
|
||||
if (listedBy.anonymity > 0) return 'anonymity';
|
||||
if (summary?.notice > 0) return 'notice';
|
||||
return 'clean';
|
||||
};
|
||||
|
||||
// One part's rows: hits (listed + notice) stay open, the rest (clean + error)
|
||||
// fold away. Keeps the API's order within each.
|
||||
export const splitPart = (part) => {
|
||||
const results = Array.isArray(part?.results) ? part.results : [];
|
||||
return {
|
||||
hits: results.filter((r) => r.status === 'listed' || r.status === 'notice'),
|
||||
rest: results.filter((r) => r.status !== 'listed' && r.status !== 'notice'),
|
||||
};
|
||||
};
|
||||
|
||||
// Tone of any row — one icon / color per tone: abuse | anonymity (listed, by
|
||||
// kind) | notice (advisory whatever its kind) | clean | error.
|
||||
export const itemTone = (item) => {
|
||||
if (item?.status === 'notice' || item?.status === 'clean' || item?.status === 'error') return item.status;
|
||||
return item?.kind === 'anonymity' ? 'anonymity' : 'abuse';
|
||||
};
|
||||
|
||||
// i18n lookup for a reason key. IPsum's `feeds1`…`feeds8` share one
|
||||
// parametrized string; any other key maps to its own entry.
|
||||
export const reasonMessage = (reason) => {
|
||||
const feeds = /^feeds(\d+)$/.exec(reason);
|
||||
if (feeds) return { key: 'ipblocklist.reason.feeds', params: { n: Number(feeds[1]) } };
|
||||
return { key: `ipblocklist.reason.${reason}`, params: {} };
|
||||
};
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "myip",
|
||||
"private": true,
|
||||
"version": "7.7.0",
|
||||
"version": "7.8.0",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@12.4.2",
|
||||
"engines": {
|
||||
|
||||
@@ -19,6 +19,7 @@ import getUserInfoHandler from '../api/get-user-info.js';
|
||||
import getWhoisHandler from '../api/get-whois.js';
|
||||
import cfRadarHandler from '../api/cf-radar.js';
|
||||
import invisibilityHandler from '../api/invisibility-test.js';
|
||||
import ipBlocklistHandler, { IP_BLOCKLIST_TIMEOUT_MS } from '../api/ip-blocklist.js';
|
||||
import macCheckerHandler, { createMacChecker } from '../api/mac-checker.js';
|
||||
import githubStarsHandler from '../api/github-stars.js';
|
||||
import personaEvaluateHandler from '../api/persona.js';
|
||||
@@ -572,6 +573,79 @@ describe('invisibility-test handler', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// -- ip-blocklist handler -------------------------------------------------
|
||||
|
||||
describe('ip-blocklist handler', () => {
|
||||
const configure = () => {
|
||||
process.env.IPCHECKING_API_KEY = 'test-key';
|
||||
process.env.IPCHECKING_API_ENDPOINT = 'https://upstream.invalid';
|
||||
};
|
||||
|
||||
it('reports a missing API key before any upstream call', async () => {
|
||||
delete process.env.IPCHECKING_API_KEY;
|
||||
globalThis.fetch = async () => assert.fail('no upstream call');
|
||||
const res = createResponse();
|
||||
await ipBlocklistHandler(createRequest({ query: { ip: '1.1.1.1' } }), res);
|
||||
assert.equal(res.statusCode, 500);
|
||||
assert.deepEqual(res.body, { error: 'API key is missing' });
|
||||
});
|
||||
|
||||
it('asks /iphitlist for the ip with the system key and forwards the caller headers', async () => {
|
||||
configure();
|
||||
let called;
|
||||
globalThis.fetch = async (url, init) => {
|
||||
called = { url: new URL(url), init };
|
||||
return { status: 200, ok: true, json: async () => ({ ip: '1.1.1.1', summary: { listed: 0 } }) };
|
||||
};
|
||||
const req = createRequest({ query: { ip: '1.1.1.1' } });
|
||||
req.headers.authorization = 'Bearer t';
|
||||
const res = createResponse();
|
||||
await ipBlocklistHandler(req, res);
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.deepEqual(res.body, { ip: '1.1.1.1', summary: { listed: 0 } });
|
||||
assert.equal(called.url.pathname, '/iphitlist');
|
||||
assert.equal(called.url.searchParams.get('ip'), '1.1.1.1');
|
||||
assert.equal(called.url.searchParams.get('key'), 'test-key');
|
||||
assert.equal(called.init.headers.authorization, 'Bearer t');
|
||||
});
|
||||
|
||||
it('passes an upstream sign-in rejection through as its own 4xx', async () => {
|
||||
configure();
|
||||
globalThis.fetch = async () => ({ status: 403, ok: false, json: async () => ({ message: 'Sign in required' }) });
|
||||
const res = createResponse();
|
||||
await ipBlocklistHandler(createRequest({ query: { ip: '1.1.1.1' } }), res);
|
||||
assert.equal(res.statusCode, 403);
|
||||
assert.deepEqual(res.body, { error: 'Sign in required' });
|
||||
});
|
||||
|
||||
it('passes an exhausted monthly quota through as 429 quota_exceeded', async () => {
|
||||
configure();
|
||||
globalThis.fetch = async () => ({ status: 429, ok: false, json: async () => ({ error: 'Monthly quota exceeded' }) });
|
||||
const res = createResponse();
|
||||
await ipBlocklistHandler(createRequest({ query: { ip: '1.1.1.1' } }), res);
|
||||
assert.equal(res.statusCode, 429);
|
||||
assert.deepEqual(res.body, { error: 'Monthly quota exceeded', code: 'quota_exceeded' });
|
||||
});
|
||||
|
||||
it('turns any other upstream failure into a 500', async () => {
|
||||
configure();
|
||||
globalThis.fetch = async () => ({ status: 502, ok: false, json: async () => ({}) });
|
||||
const res = createResponse();
|
||||
const restore = logger.level;
|
||||
logger.level = 'silent';
|
||||
try {
|
||||
await ipBlocklistHandler(createRequest({ query: { ip: '1.1.1.1' } }), res);
|
||||
} finally {
|
||||
logger.level = restore;
|
||||
}
|
||||
assert.equal(res.statusCode, 500);
|
||||
});
|
||||
|
||||
it('waits well past the default 8s upstream timeout', () => {
|
||||
assert.ok(IP_BLOCKLIST_TIMEOUT_MS >= 30000);
|
||||
});
|
||||
});
|
||||
|
||||
// -- mac-checker handler --------------------------------------------------
|
||||
|
||||
describe('mac-checker handler', () => {
|
||||
|
||||
@@ -156,13 +156,14 @@ describe('useShortcuts()', () => {
|
||||
assert.ok(distinctKeys.has('H'));
|
||||
assert.equal(distinctKeys.has('p'), false, 'pulse shortcut stays off without a pulse backend or cloudFlare key');
|
||||
assert.equal(distinctKeys.has('P'), false, 'persona shortcut stays off a self-hosted instance');
|
||||
assert.equal(distinctKeys.has('B'), false, 'blocklist shortcut stays off a self-hosted instance');
|
||||
assert.equal(distinctKeys.has('A'), false, 'ASN Profile shortcut stays off without a cloudFlare key');
|
||||
// Overlays take no keys at all (utils/shortcut.js), so there is no
|
||||
// drawer-only entry left to register.
|
||||
assert.equal(distinctKeys.has('f'), false, 'no shortcut acts on an open overlay');
|
||||
});
|
||||
|
||||
it('originalSite=true adds invisibility ("i"), enhanced DNS-leak ("D") and persona ("P") shortcuts', () => {
|
||||
it('originalSite=true adds invisibility ("i"), enhanced DNS-leak ("D"), blocklist ("B") and persona ("P") shortcuts', () => {
|
||||
const { keyMap, calls } = loadAndGetKeyMap({ originalSite: true });
|
||||
const hasInvisibility = keyMap.some((e) => e.keys === 'i');
|
||||
assert.ok(hasInvisibility, 'key "i" should be present on originalSite');
|
||||
@@ -170,11 +171,15 @@ describe('useShortcuts()', () => {
|
||||
assert.ok(D, 'key "D" should be present on originalSite');
|
||||
D.action();
|
||||
assert.deepEqual(calls.advancedNavigate, ['enhanceddnsleaktest']);
|
||||
// Uppercase B is its own key — lowercase b is Browser Info's.
|
||||
const B = keyMap.findLast((e) => e.keys === 'B');
|
||||
assert.ok(B, 'key "B" should be present on originalSite');
|
||||
B.action();
|
||||
// Uppercase P is its own key — lowercase p is Earth Online's.
|
||||
const P = keyMap.findLast((e) => e.keys === 'P');
|
||||
assert.ok(P, 'key "P" should be present on originalSite');
|
||||
P.action();
|
||||
assert.deepEqual(calls.advancedNavigate, ['enhanceddnsleaktest', 'personacheck']);
|
||||
assert.deepEqual(calls.advancedNavigate, ['enhanceddnsleaktest', 'blocklist', 'personacheck']);
|
||||
});
|
||||
|
||||
it('configs.cloudFlare adds the ASN Profile shortcut ("A")', () => {
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
// Tests for frontend/utils/features/ip-blocklist.js — the IP Blocklist Check's
|
||||
// verdict, row split, row tone and reason-key lookup.
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { blocklistVerdict, listedCounts, splitPart, itemTone, reasonMessage } from '../frontend/utils/features/ip-blocklist.js';
|
||||
|
||||
const en = JSON.parse(readFileSync(new URL('../frontend/locales/en.json', import.meta.url), 'utf8'));
|
||||
|
||||
// The reason keys and categories /iphitlist can send, as the private API's
|
||||
// docs list them (docs/ip-monitoring/hitlist.md §1.1-1.2 there).
|
||||
const API_REASONS = [
|
||||
'socksProxy', 'httpProxy', 'proxyChain', 'webProxy', 'wingateProxy', 'openProxy', 'torExit',
|
||||
'exploited', 'compromisedHost', 'compromisedRouter', 'botnet', 'worm', 'ircDrone', 'ddosDrone', 'bottler', 'spambot',
|
||||
'bruteForce', 'spamSource', 'spamtrap', 'badReputation', 'hijackedNetwork',
|
||||
'endUserRange', 'dynamicRange', 'noMailRange', 'missingPtr', 'poorReputation', 'mixedReputation',
|
||||
'rangeListed', 'networkListed', 'unaccountableOperator', 'noMailService', 'unreliableAbuseDesk',
|
||||
'reported1d', 'reported7d', 'reported30d', 'reported90d',
|
||||
...[1, 2, 3, 4, 5, 6, 7, 8].map((n) => `feeds${n}`),
|
||||
'testEntry',
|
||||
];
|
||||
const API_CATEGORIES = ['spam', 'abuse', 'proxy', 'vpn', 'relay', 'tor', 'scanner', 'backscatter', 'mixed'];
|
||||
const lookup = (key) => key.split('.').reduce((node, part) => node?.[part], en);
|
||||
|
||||
const summary = (listed, notice, abuse, anonymity) => ({
|
||||
listed, notice, clean: 0, error: 0, total: listed + notice, listedBy: { abuse, anonymity },
|
||||
});
|
||||
|
||||
describe('blocklistVerdict', () => {
|
||||
it('a bad record outweighs everything else', () => {
|
||||
assert.equal(blocklistVerdict(summary(3, 1, 1, 2)), 'abuse');
|
||||
});
|
||||
|
||||
it('anonymizer marks alone are not a bad record', () => {
|
||||
assert.equal(blocklistVerdict(summary(2, 1, 0, 2)), 'anonymity');
|
||||
});
|
||||
|
||||
it('notices alone are advisory', () => {
|
||||
assert.equal(blocklistVerdict(summary(0, 2, 0, 0)), 'notice');
|
||||
});
|
||||
|
||||
it('nothing listed is clean, as is a missing summary', () => {
|
||||
assert.equal(blocklistVerdict(summary(0, 0, 0, 0)), 'clean');
|
||||
assert.equal(blocklistVerdict(undefined), 'clean');
|
||||
});
|
||||
|
||||
it('an answer without listedBy never reads a hit as clean', () => {
|
||||
assert.equal(blocklistVerdict({ listed: 1, notice: 0 }), 'abuse');
|
||||
assert.deepEqual(listedCounts({ listed: 3 }), { abuse: 3, anonymity: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('splitPart', () => {
|
||||
it('keeps listed and notice rows open, folds clean and error, in order', () => {
|
||||
const results = [
|
||||
{ id: 'a', status: 'listed' },
|
||||
{ id: 'b', status: 'notice' },
|
||||
{ id: 'c', status: 'error' },
|
||||
{ id: 'd', status: 'clean' },
|
||||
];
|
||||
const { hits, rest } = splitPart({ results });
|
||||
assert.deepEqual(hits.map((r) => r.id), ['a', 'b']);
|
||||
assert.deepEqual(rest.map((r) => r.id), ['c', 'd']);
|
||||
});
|
||||
|
||||
it('a missing part has no rows', () => {
|
||||
assert.deepEqual(splitPart(null), { hits: [], rest: [] });
|
||||
assert.deepEqual(splitPart({ results: 'x' }), { hits: [], rest: [] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('itemTone', () => {
|
||||
it('follows kind for listings and stays advisory for notices', () => {
|
||||
assert.equal(itemTone({ status: 'listed', kind: 'abuse' }), 'abuse');
|
||||
assert.equal(itemTone({ status: 'listed', kind: 'anonymity' }), 'anonymity');
|
||||
assert.equal(itemTone({ status: 'notice', kind: 'abuse' }), 'notice');
|
||||
});
|
||||
|
||||
it('clean and failed rows keep their own tone whatever their kind', () => {
|
||||
assert.equal(itemTone({ status: 'clean', kind: 'anonymity' }), 'clean');
|
||||
assert.equal(itemTone({ status: 'error', kind: 'abuse' }), 'error');
|
||||
});
|
||||
|
||||
it('an unknown kind reads as abuse', () => {
|
||||
assert.equal(itemTone({ status: 'listed' }), 'abuse');
|
||||
});
|
||||
});
|
||||
|
||||
describe('reasonMessage', () => {
|
||||
it('maps IPsum levels onto one parametrized key', () => {
|
||||
assert.deepEqual(reasonMessage('feeds5'), { key: 'ipblocklist.reason.feeds', params: { n: 5 } });
|
||||
});
|
||||
|
||||
it('maps any other reason onto its own key', () => {
|
||||
assert.deepEqual(reasonMessage('socksProxy'), { key: 'ipblocklist.reason.socksProxy', params: {} });
|
||||
assert.deepEqual(reasonMessage('reported7d'), { key: 'ipblocklist.reason.reported7d', params: {} });
|
||||
});
|
||||
});
|
||||
|
||||
describe('ip-blocklist copy (en)', () => {
|
||||
it('every reason the API sends has a label', () => {
|
||||
for (const reason of API_REASONS) {
|
||||
assert.equal(typeof lookup(reasonMessage(reason).key), 'string', reason);
|
||||
}
|
||||
});
|
||||
|
||||
it('every list category has a label', () => {
|
||||
for (const category of API_CATEGORIES) {
|
||||
assert.equal(typeof en.ipblocklist.category[category], 'string', category);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -348,6 +348,12 @@ describe('store — quota snapshot', () => {
|
||||
assert.equal(s.quotaExceeded.invisibility_test, true);
|
||||
});
|
||||
|
||||
it('quotaExceeded covers the IP Blocklist Check (ip_hitlist)', () => {
|
||||
const s = useMainStore();
|
||||
s.remoteUserInfo = snapshot({ ip_hitlist: { percent: 100, exhausted: true } });
|
||||
assert.equal(s.quotaExceeded.ip_hitlist, true);
|
||||
});
|
||||
|
||||
it('markQuotaExhausted is a no-op without a snapshot for the feature', () => {
|
||||
const s = useMainStore();
|
||||
s.remoteUserInfo = snapshot({});
|
||||
|
||||
Reference in new issue
Block a user