fix(p2p stats): prefer browser IPv4 for own endpoint

This commit is contained in:
Tommaso Casaburi
2026-05-24 23:18:42 +07:00
parent 271cb14ae6
commit 639c627a04
4 changed files with 240 additions and 93 deletions
@@ -62,7 +62,7 @@ describe('P2PStatsSettings', () => {
}; };
testState.rpcSettings = { state: 'disconnected' }; testState.rpcSettings = { state: 'disconnected' };
testState.setAccountMock.mockReset().mockResolvedValue(undefined); testState.setAccountMock.mockReset().mockResolvedValue(undefined);
// Default: own-IP country lookups (api.country.is) resolve offline so browser // Default: own-IP endpoint lookups resolve offline so browser
// stats tests never hit the network. Individual tests can override this stub. // stats tests never hit the network. Individual tests can override this stub.
vi.stubGlobal( vi.stubGlobal(
'fetch', 'fetch',
@@ -151,8 +151,9 @@ describe('P2PStatsSettings', () => {
expect(seederLink).not.toBeNull(); expect(seederLink).not.toBeNull();
expect(seederLink?.textContent).toBe('want to seed?'); expect(seederLink?.textContent).toBe('want to seed?');
expect(rows.get('Your IP')).toContain('147.75.84.175'); expect(rows.get('Your IP')).toContain('147.75.84.175');
// The own IP is geolocated accurately (per-IP lookup), not via the coarse peer guess. // Browser mode resolves "Your IP" from the browser's public endpoint, not
expect(fetch).toHaveBeenCalledWith('https://api.country.is/147.75.84.175', expect.objectContaining({ signal: expect.any(AbortSignal) })); // from libp2p observed/WebRTC addresses.
expect(fetch).toHaveBeenCalledWith('https://api.ipify.org?format=json', expect.objectContaining({ signal: expect.any(AbortSignal) }));
const yourIpRow = Array.from(container.querySelectorAll('tr')).find((row) => row.textContent?.includes('Your IP')); const yourIpRow = Array.from(container.querySelectorAll('tr')).find((row) => row.textContent?.includes('Your IP'));
expect(yourIpRow?.querySelector('[role="img"]')).not.toBeNull(); expect(yourIpRow?.querySelector('[role="img"]')).not.toBeNull();
expect(container.textContent).not.toContain('browser Helia'); expect(container.textContent).not.toContain('browser Helia');
@@ -254,6 +255,12 @@ describe('P2PStatsSettings', () => {
it('shows the own IP flag and a red precise map marker when leeching', async () => { it('shows the own IP flag and a red precise map marker when leeching', async () => {
const fetchMock = vi.fn(async (url: string | URL | Request) => { const fetchMock = vi.fn(async (url: string | URL | Request) => {
const requestUrl = String(url); const requestUrl = String(url);
if (requestUrl === 'https://api.ipify.org?format=json') {
return {
ok: true,
json: async () => ({ ip: '117.2.120.113' }),
};
}
if (requestUrl === 'https://api.country.is/117.2.120.113') { if (requestUrl === 'https://api.country.is/117.2.120.113') {
return { return {
ok: false, ok: false,
@@ -320,6 +327,98 @@ describe('P2PStatsSettings', () => {
expect(Number(marker?.getAttribute('y'))).toBeCloseTo(72.43, 1); expect(Number(marker?.getAttribute('y'))).toBeCloseTo(72.43, 1);
}); });
it('keeps browser "Your IP" on the browser public endpoint when libp2p exposes a different public address', async () => {
const fetchMock = vi.fn(async (url: string | URL | Request) => {
const requestUrl = String(url);
if (requestUrl === 'https://api.ipify.org?format=json') {
return {
ok: true,
json: async () => ({ ip: '104.28.68.171' }),
};
}
if (requestUrl === 'https://api.country.is/104.28.68.171') {
return {
ok: true,
json: async () => ({ country: 'VN', ip: '104.28.68.171' }),
};
}
if (requestUrl === 'https://free.freeipapi.com/api/json/104.28.68.171') {
return {
ok: true,
json: async () => ({
cityName: 'Toronto',
countryCode: 'CA',
ipAddress: '104.28.68.171',
latitude: 43.6532,
longitude: -79.3832,
regionName: 'Ontario',
}),
};
}
if (requestUrl === 'https://free.freeipapi.com/api/json/146.75.187.55') {
return {
ok: true,
json: async () => ({
cityName: 'Bandar Seri Begawan',
countryCode: 'BN',
ipAddress: '146.75.187.55',
latitude: 4.89234,
longitude: 114.942,
regionName: 'Brunei-Muara',
}),
};
}
return {
ok: true,
json: async () => ({ country: 'US', ip: '147.75.84.175' }),
};
});
vi.stubGlobal('fetch', fetchMock);
testState.account = {
...testState.account,
pkcOptions: {
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
},
pkc: {
clients: {
libp2pJsClients: {
libp2pjs: {
key: 'libp2pjs',
_helia: {
libp2p: {
getConnections: () => [
{
localAddr: { toString: () => '/ip4/146.75.187.55/udp/4001/webrtc-direct' },
},
],
getMultiaddrs: () => ['/ip4/146.75.187.55/udp/4001/webrtc-direct'],
getPeers: () => [],
peerId: { toString: () => 'self-peer' },
},
},
},
},
},
},
};
await renderSettings(false);
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
const rows = getStatRows();
const yourIpRow = Array.from(container.querySelectorAll('tr')).find((row) => row.textContent?.includes('Your IP'));
const marker = getMarkerByTitle('Your node - VN');
expect(rows.get('Your IP')).toContain('104.28.68.171');
expect(rows.get('Your IP')).not.toContain('146.75.187.55');
expect(yourIpRow?.querySelector('[role="img"]')?.getAttribute('aria-label')).toBe('Vietnam');
expect(marker).not.toBeNull();
expect(container.querySelector('svg rect title')?.textContent).not.toContain('Toronto');
expect(fetchMock).not.toHaveBeenCalledWith('https://free.freeipapi.com/api/json/146.75.187.55', expect.anything());
});
it('reads browser transfer counters from Helia bitswap ledgers', async () => { it('reads browser transfer counters from Helia bitswap ledgers', async () => {
testState.account = { testState.account = {
...testState.account, ...testState.account,
@@ -401,13 +500,20 @@ describe('P2PStatsSettings', () => {
}); });
it('falls back to the browser node public endpoint when Helia exposes no public address', async () => { it('falls back to the browser node public endpoint when Helia exposes no public address', async () => {
vi.stubGlobal( const fetchMock = vi.fn(async (url: string | URL | Request) => {
'fetch', const requestUrl = String(url);
vi.fn().mockResolvedValue({ if (requestUrl === 'https://api.ipify.org?format=json') {
return {
ok: true,
json: async () => ({ ip: '104.28.68.171' }),
};
}
return {
ok: true, ok: true,
json: async () => ({ country: 'US', ip: '2001:4860:4860::8888' }), json: async () => ({ country: 'VN', ip: '104.28.68.171' }),
}), };
); });
vi.stubGlobal('fetch', fetchMock);
testState.account = { testState.account = {
...testState.account, ...testState.account,
pkcOptions: { pkcOptions: {
@@ -442,11 +548,11 @@ describe('P2PStatsSettings', () => {
await act(async () => Promise.resolve()); await act(async () => Promise.resolve());
const rows = getStatRows(); const rows = getStatRows();
expect(rows.get('Your IP')).toContain('2001:4860:4860::8888'); expect(rows.get('Your IP')).toContain('104.28.68.171');
expect(rows.get('Your IP')).not.toContain('unknown'); expect(rows.get('Your IP')).not.toContain('unknown');
const yourIpRow = Array.from(container.querySelectorAll('tr')).find((row) => row.textContent?.includes('Your IP')); const yourIpRow = Array.from(container.querySelectorAll('tr')).find((row) => row.textContent?.includes('Your IP'));
expect(yourIpRow?.querySelector('[role="img"]')).not.toBeNull(); expect(yourIpRow?.querySelector('[role="img"]')).not.toBeNull();
expect(fetch).toHaveBeenCalledWith('https://api.country.is', expect.objectContaining({ signal: expect.any(AbortSignal) })); expect(fetch).toHaveBeenCalledWith('https://api.ipify.org?format=json', expect.objectContaining({ signal: expect.any(AbortSignal) }));
}); });
it('reports seeding only when browser Helia can add and publish provider records', async () => { it('reports seeding only when browser Helia can add and publish provider records', async () => {
@@ -8,6 +8,7 @@ import {
fetchOwnPublicEndpoint, fetchOwnPublicEndpoint,
fetchPeerMapLocation, fetchPeerMapLocation,
getApproximateCountryCode, getApproximateCountryCode,
getCountryConsistentLocation,
getFirstPublicIpFromAddresses, getFirstPublicIpFromAddresses,
isPrivateOrReservedIpv4, isPrivateOrReservedIpv4,
type PeerMapLocation, type PeerMapLocation,
@@ -90,11 +91,7 @@ type StatsAction =
type Libp2pClientShape = { type Libp2pClientShape = {
_helia?: { _helia?: {
libp2p?: { libp2p?: {
components?: {
addressManager?: Libp2pAddressManagerShape;
};
getConnections?: () => unknown[] | Promise<unknown[]>; getConnections?: () => unknown[] | Promise<unknown[]>;
getMultiaddrs?: () => unknown[] | Promise<unknown[]>;
getPeers?: () => unknown[] | Promise<unknown[]>; getPeers?: () => unknown[] | Promise<unknown[]>;
peerId?: { toString: () => string }; peerId?: { toString: () => string };
services?: { services?: {
@@ -115,13 +112,6 @@ type Libp2pClientShape = {
key?: string; key?: string;
}; };
type Libp2pAddressManagerShape = {
getAddressesWithMetadata?: () => unknown[] | Promise<unknown[]>;
getObservedAddrs?: () => unknown[] | Promise<unknown[]>;
};
type BrowserLibp2pShape = NonNullable<NonNullable<NonNullable<Libp2pClientShape['_helia']>['libp2p']>>;
type PkcRpcClientShape = { type PkcRpcClientShape = {
getPeers?: () => unknown | Promise<unknown>; getPeers?: () => unknown | Promise<unknown>;
getStats?: () => unknown | Promise<unknown>; getStats?: () => unknown | Promise<unknown>;
@@ -264,21 +254,6 @@ const getSafeArray = async (getValue?: () => unknown[] | Promise<unknown[]> | un
} }
}; };
const getAddressManagerAddresses = async (libp2p?: BrowserLibp2pShape): Promise<unknown[]> => {
const addressManager = isRecord(libp2p?.components) ? (libp2p.components.addressManager as Libp2pAddressManagerShape | undefined) : undefined;
const [observedAddrs, addressesWithMetadata] = await Promise.all([
getSafeArray(() => addressManager?.getObservedAddrs?.()),
getSafeArray(() => addressManager?.getAddressesWithMetadata?.()),
]);
return [
...observedAddrs,
...addressesWithMetadata.flatMap((entry) => {
const address = isRecord(entry) ? (entry.multiaddr ?? entry.address) : entry;
return address ? [address] : [];
}),
];
};
const getFirstPkcRpcClient = (account?: AccountShape) => getFirstObjectValue(account?.pkc?.clients?.pkcRpcClients) as PkcRpcClientShape | undefined; const getFirstPkcRpcClient = (account?: AccountShape) => getFirstObjectValue(account?.pkc?.clients?.pkcRpcClients) as PkcRpcClientShape | undefined;
const getPkcRpcUrls = (account?: AccountShape) => { const getPkcRpcUrls = (account?: AccountShape) => {
@@ -727,17 +702,18 @@ const resolveConnectedPeerLocations = async (row: ConnectedPeersStatRow, signal?
return { ...row, entries }; return { ...row, entries };
}; };
// Resolves the "Your IP" row from observed node addresses for browser/full-node // Resolves node-owned listen addresses for full-node and Kubo fallback paths.
// paths. Electron Kubo uses resolveKuboOwnEndpoint below because its address list // Browser libp2p observed addresses can be WebRTC relay/CDN endpoints, so browser
// can include relay/circuit endpoints owned by other peers. // mode uses fetchOwnPublicEndpoint instead.
const resolveOwnEndpoint = async (addresses: unknown[], signal?: AbortSignal): Promise<PublicEndpoint | undefined> => { const resolveOwnEndpoint = async (addresses: unknown[], signal?: AbortSignal): Promise<PublicEndpoint | undefined> => {
const ip = getFirstPublicIpFromAddresses(addresses); const ip = getFirstPublicIpFromAddresses(addresses);
if (ip) { if (ip) {
const [countryCode, location] = await Promise.all([fetchOwnIpCountryCode(ip, signal), fetchIpMapLocation(ip, signal)]); const [countryCode, location] = await Promise.all([fetchOwnIpCountryCode(ip, signal), fetchIpMapLocation(ip, signal)]);
const resolvedCountryCode = countryCode ?? location?.countryCode ?? getApproximateCountryCode(getEndpointAddress(ip));
return { return {
countryCode: location?.countryCode ?? countryCode ?? getApproximateCountryCode(getEndpointAddress(ip)), countryCode: resolvedCountryCode,
ip, ip,
location, location: getCountryConsistentLocation(resolvedCountryCode, location),
}; };
} }
return fetchOwnPublicEndpoint(signal); return fetchOwnPublicEndpoint(signal);
@@ -784,21 +760,12 @@ const getAddressListFromRecord = (record: unknown) =>
const getBrowserLibp2pStats = async (account?: AccountShape, signal?: AbortSignal): Promise<StatRow[]> => { const getBrowserLibp2pStats = async (account?: AccountShape, signal?: AbortSignal): Promise<StatRow[]> => {
const client = getFirstObjectValue(account?.pkc?.clients?.libp2pJsClients) as Libp2pClientShape | undefined; const client = getFirstObjectValue(account?.pkc?.clients?.libp2pJsClients) as Libp2pClientShape | undefined;
const libp2p = client?._helia?.libp2p; const libp2p = client?._helia?.libp2p;
const [peers, connections, multiaddrs, addressManagerAddresses] = await Promise.all([ const [peers, connections] = await Promise.all([getSafeArray(() => libp2p?.getPeers?.()), getSafeArray(() => libp2p?.getConnections?.())]);
getSafeArray(() => libp2p?.getPeers?.()),
getSafeArray(() => libp2p?.getConnections?.()),
getSafeArray(() => libp2p?.getMultiaddrs?.()),
getAddressManagerAddresses(libp2p),
]);
const localAddresses = connections.flatMap((connection) => {
const localAddr = isRecord(connection) ? connection.localAddr : undefined;
return localAddr ? [localAddr] : [];
});
const connectedPeersRow = getBrowserConnectedPeersRow(peers, connections); const connectedPeersRow = getBrowserConnectedPeersRow(peers, connections);
const mode = getBrowserMode(client); const mode = getBrowserMode(client);
const [transferStats, nodeEndpoint, connectedPeers] = await Promise.all([ const [transferStats, nodeEndpoint, connectedPeers] = await Promise.all([
getBrowserTransferStats(client, connections), getBrowserTransferStats(client, connections),
resolveOwnEndpoint([...multiaddrs, ...addressManagerAddresses, ...localAddresses], signal), fetchOwnPublicEndpoint(signal),
resolveConnectedPeerLocations(connectedPeersRow, signal), resolveConnectedPeerLocations(connectedPeersRow, signal),
]); ]);
const connectedPeersWithMapEntries = { const connectedPeersWithMapEntries = {
+60 -23
View File
@@ -9,6 +9,7 @@ import {
fetchPeerMapLocation, fetchPeerMapLocation,
getApproximateCountryCode, getApproximateCountryCode,
getApproximateLatLon, getApproximateLatLon,
getCountryConsistentLocation,
getFirstPublicIpFromAddresses, getFirstPublicIpFromAddresses,
isPrivateOrReservedIpv4, isPrivateOrReservedIpv4,
} from '../peer-geo'; } from '../peer-geo';
@@ -86,6 +87,25 @@ describe('getApproximateLatLon', () => {
}); });
}); });
describe('getCountryConsistentLocation', () => {
it('falls back to the selected country centroid when GeoIP databases disagree', () => {
expect(
getCountryConsistentLocation('VN', {
countryCode: 'ca',
label: 'Toronto, Ontario, CA',
lat: 43.6532,
lon: -79.3832,
source: 'geoip',
}),
).toEqual({
...COUNTRY_CENTROIDS.vn,
countryCode: 'vn',
label: 'VN',
source: 'coarse',
});
});
});
describe('getFirstPublicIpFromAddresses', () => { describe('getFirstPublicIpFromAddresses', () => {
it('returns the first public IP from multiaddrs', () => { it('returns the first public IP from multiaddrs', () => {
expect(getFirstPublicIpFromAddresses(['/ip4/127.0.0.1/tcp/4001', '/ip4/147.75.84.175/tcp/4001/ws'])).toBe('147.75.84.175'); expect(getFirstPublicIpFromAddresses(['/ip4/127.0.0.1/tcp/4001', '/ip4/147.75.84.175/tcp/4001/ws'])).toBe('147.75.84.175');
@@ -98,35 +118,52 @@ describe('getFirstPublicIpFromAddresses', () => {
}); });
describe('fetchOwnPublicEndpoint', () => { describe('fetchOwnPublicEndpoint', () => {
it('caches the fetched public endpoint', async () => { it('prefers and caches the fetched IPv4 public endpoint', async () => {
const fetchMock = vi.fn(async (url: string | URL | Request) => ({ const fetchMock = vi.fn(async (url: string | URL | Request) => {
ok: true, const requestUrl = String(url);
json: async () => if (requestUrl === 'https://api.ipify.org?format=json') {
String(url).startsWith('https://free.freeipapi.com/api/json/') return {
? { ok: true,
cityName: 'Mountain View', json: async () => ({ ip: '104.28.68.171' }),
countryCode: 'US', };
latitude: 37.422, }
longitude: -122.085, if (requestUrl === 'https://api.country.is/104.28.68.171') {
regionName: 'California', return {
} ok: true,
: { country: 'US', ip: '2001:4860:4860::8888' }, json: async () => ({ country: 'VN', ip: '104.28.68.171' }),
})); };
}
if (requestUrl === 'https://free.freeipapi.com/api/json/104.28.68.171') {
return {
ok: true,
json: async () => ({
cityName: 'Toronto',
countryCode: 'CA',
latitude: 43.6532,
longitude: -79.3832,
regionName: 'Ontario',
}),
};
}
return {
ok: true,
json: async () => ({ country: 'US', ip: '2001:4860:4860::8888' }),
};
});
vi.stubGlobal('fetch', fetchMock); vi.stubGlobal('fetch', fetchMock);
await expect(fetchOwnPublicEndpoint()).resolves.toMatchObject({ await expect(fetchOwnPublicEndpoint()).resolves.toMatchObject({
countryCode: 'us', countryCode: 'vn',
ip: '2001:4860:4860::8888', ip: '104.28.68.171',
location: { location: {
countryCode: 'us', countryCode: 'vn',
label: 'Mountain View, California, US', label: 'VN',
lat: 37.422, source: 'coarse',
lon: -122.085,
source: 'geoip',
}, },
}); });
await expect(fetchOwnPublicEndpoint()).resolves.toMatchObject({ countryCode: 'us', ip: '2001:4860:4860::8888' }); await expect(fetchOwnPublicEndpoint()).resolves.toMatchObject({ countryCode: 'vn', ip: '104.28.68.171' });
expect(fetchMock).toHaveBeenCalledTimes(2); expect(fetchMock).toHaveBeenCalledTimes(3);
expect(fetchMock).not.toHaveBeenCalledWith('https://api.country.is', expect.anything());
vi.unstubAllGlobals(); vi.unstubAllGlobals();
}); });
+54 -17
View File
@@ -33,7 +33,7 @@ export type PeerMapLocation = LatLon & {
}; };
const COUNTRY_LOOKUP_URL = 'https://api.country.is'; const COUNTRY_LOOKUP_URL = 'https://api.country.is';
const PUBLIC_IPV4_LOOKUP_URL = 'https://api64.ipify.org?format=json'; const PUBLIC_IPV4_LOOKUP_URL = 'https://api.ipify.org?format=json';
const PEER_LOCATION_LOOKUP_URL = 'https://free.freeipapi.com/api/json'; const PEER_LOCATION_LOOKUP_URL = 'https://free.freeipapi.com/api/json';
const PEER_LOCATION_CACHE_MS = 24 * 60 * 60_000; const PEER_LOCATION_CACHE_MS = 24 * 60 * 60_000;
const PEER_LOCATION_FAILURE_CACHE_MS = 10 * 60_000; const PEER_LOCATION_FAILURE_CACHE_MS = 10 * 60_000;
@@ -57,6 +57,32 @@ const normalizeLookupCountryCode = (value: unknown) => {
return /^[a-z]{2}$/.test(code) ? code : undefined; return /^[a-z]{2}$/.test(code) ? code : undefined;
}; };
const getCountryCentroidLocation = (countryCode: string | undefined): PeerMapLocation | undefined => {
const normalizedCountryCode = normalizeLookupCountryCode(countryCode);
const centroid = normalizedCountryCode ? COUNTRY_CENTROIDS[normalizedCountryCode] : undefined;
if (!normalizedCountryCode || !centroid) return undefined;
return {
...centroid,
countryCode: normalizedCountryCode,
label: normalizedCountryCode.toUpperCase(),
source: 'coarse',
};
};
export const getCountryConsistentLocation = (countryCode: string | undefined, location: PeerMapLocation | undefined): PeerMapLocation | undefined => {
const normalizedCountryCode = normalizeLookupCountryCode(countryCode);
if (!normalizedCountryCode) return location;
if (!location) return getCountryCentroidLocation(normalizedCountryCode);
const locationCountryCode = normalizeLookupCountryCode(location.countryCode);
if (!locationCountryCode || locationCountryCode === normalizedCountryCode) {
return {
...location,
countryCode: locationCountryCode ?? normalizedCountryCode,
};
}
return getCountryCentroidLocation(normalizedCountryCode);
};
const normalizePlaceName = (value: unknown) => { const normalizePlaceName = (value: unknown) => {
if (typeof value !== 'string') return undefined; if (typeof value !== 'string') return undefined;
const trimmed = value.trim(); const trimmed = value.trim();
@@ -88,6 +114,8 @@ const fetchPublicEndpoint = async (url: string, signal?: AbortSignal): Promise<P
} }
}; };
const getIpLookupAddress = (ip: string) => `/ip${ip.includes(':') ? '6' : '4'}/${ip}/tcp/0`;
const parsePeerLocation = (data: unknown): PeerMapLocation | undefined => { const parsePeerLocation = (data: unknown): PeerMapLocation | undefined => {
if (!data || typeof data !== 'object') return undefined; if (!data || typeof data !== 'object') return undefined;
const lat = getFiniteCoordinate((data as { latitude?: unknown }).latitude, -85, 85); const lat = getFiniteCoordinate((data as { latitude?: unknown }).latitude, -85, 85);
@@ -107,6 +135,19 @@ const parsePeerLocation = (data: unknown): PeerMapLocation | undefined => {
}; };
}; };
const resolveOwnPublicEndpointLocation = async (endpoint: PublicEndpoint, signal?: AbortSignal): Promise<PublicEndpoint> => {
const [countryCode, location] = await Promise.all([
endpoint.countryCode ? Promise.resolve(endpoint.countryCode) : fetchOwnIpCountryCode(endpoint.ip, signal),
fetchIpMapLocation(endpoint.ip, signal),
]);
const resolvedCountryCode = endpoint.countryCode ?? countryCode ?? location?.countryCode ?? getApproximateCountryCode(getIpLookupAddress(endpoint.ip));
return {
...endpoint,
countryCode: resolvedCountryCode,
location: getCountryConsistentLocation(resolvedCountryCode, location),
};
};
// Fetches the browser node's own public endpoint for the P2P stats panel when // Fetches the browser node's own public endpoint for the P2P stats panel when
// libp2p only advertises local/private listen addresses (common in browser nodes // libp2p only advertises local/private listen addresses (common in browser nodes
// and VPN setups). This only asks about the current browser's public endpoint; // and VPN setups). This only asks about the current browser's public endpoint;
@@ -114,28 +155,24 @@ const parsePeerLocation = (data: unknown): PeerMapLocation | undefined => {
export const fetchOwnPublicEndpoint = async (signal?: AbortSignal): Promise<PublicEndpoint | undefined> => { export const fetchOwnPublicEndpoint = async (signal?: AbortSignal): Promise<PublicEndpoint | undefined> => {
if (cachedOwnPublicEndpoint && Date.now() < cachedOwnPublicEndpoint.expiresAt) return cachedOwnPublicEndpoint.value; if (cachedOwnPublicEndpoint && Date.now() < cachedOwnPublicEndpoint.expiresAt) return cachedOwnPublicEndpoint.value;
const endpoint = await fetchPublicEndpoint(COUNTRY_LOOKUP_URL, signal); const ipv4Endpoint = await fetchPublicEndpoint(PUBLIC_IPV4_LOOKUP_URL, signal);
if (endpoint) { if (ipv4Endpoint) {
const location = await fetchIpMapLocation(endpoint.ip, signal); const value = await resolveOwnPublicEndpointLocation(ipv4Endpoint, signal);
const value = {
...endpoint,
countryCode: endpoint.countryCode ?? location?.countryCode,
location,
};
cachedOwnPublicEndpoint = { expiresAt: Date.now() + 60_000, value }; cachedOwnPublicEndpoint = { expiresAt: Date.now() + 60_000, value };
return value; return value;
} }
const ipv4Endpoint = await fetchPublicEndpoint(PUBLIC_IPV4_LOOKUP_URL, signal); const endpoint = await fetchPublicEndpoint(COUNTRY_LOOKUP_URL, signal);
const ipv4Location = ipv4Endpoint?.ip ? await fetchIpMapLocation(ipv4Endpoint.ip, signal) : undefined; if (endpoint) {
const fallback = ipv4Endpoint?.ip const value = await resolveOwnPublicEndpointLocation(endpoint, signal);
? { countryCode: ipv4Location?.countryCode ?? getApproximateCountryCode(`/ip4/${ipv4Endpoint.ip}/tcp/0`), ip: ipv4Endpoint.ip, location: ipv4Location } cachedOwnPublicEndpoint = { expiresAt: Date.now() + 60_000, value };
: undefined; return value;
}
// Don't cache an empty result produced by an aborted lookup (e.g. the panel was // Don't cache an empty result produced by an aborted lookup (e.g. the panel was
// closed mid-request): that is cancellation, not a real failure, so a later // closed mid-request): that is cancellation, not a real failure, so a later
// reopen should retry instead of being served a cached blank for 30s. // reopen should retry instead of being served a cached blank for 30s.
if (!signal?.aborted) cachedOwnPublicEndpoint = { expiresAt: Date.now() + 30_000, value: fallback }; if (!signal?.aborted) cachedOwnPublicEndpoint = { expiresAt: Date.now() + 30_000, value: undefined };
return fallback; return undefined;
}; };
const ownIpCountryCache = new Map<string, { expiresAt: number; value?: string }>(); const ownIpCountryCache = new Map<string, { expiresAt: number; value?: string }>();
@@ -147,7 +184,7 @@ export const fetchOwnIpCountryCode = async (ip: string, signal?: AbortSignal): P
const cached = ownIpCountryCache.get(ip); const cached = ownIpCountryCache.get(ip);
if (cached && Date.now() < cached.expiresAt) return cached.value; if (cached && Date.now() < cached.expiresAt) return cached.value;
const endpoint = await fetchPublicEndpoint(`${COUNTRY_LOOKUP_URL}/${ip}`, signal); const endpoint = await fetchPublicEndpoint(`${COUNTRY_LOOKUP_URL}/${ip}`, signal);
const value = endpoint?.countryCode ?? getApproximateCountryCode(`/ip4/${ip}/tcp/0`); const value = endpoint?.countryCode;
// See fetchOwnPublicEndpoint: skip caching when the lookup was aborted so a // See fetchOwnPublicEndpoint: skip caching when the lookup was aborted so a
// cancelled request does not blank the flag for 60s on the next open. // cancelled request does not blank the flag for 60s on the next open.
if (!signal?.aborted) ownIpCountryCache.set(ip, { expiresAt: Date.now() + 60_000, value }); if (!signal?.aborted) ownIpCountryCache.set(ip, { expiresAt: Date.now() + 60_000, value });