mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 04:55:42 +09:00
refactor(pkgs): share vitest config and dedupe helpers (#3384)
This commit is contained in:
1 parent
5695510980
commit
575a5adbef
43 files changed
+172
-874
No files matched your search
@@ -4,7 +4,7 @@ import {Buffer} from 'node:buffer';
|
||||
import dns from 'node:dns';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import {isIPv4, isIPv6, type LookupFunction} from 'node:net';
|
||||
import type {LookupFunction} from 'node:net';
|
||||
import {Readable, Transform} from 'node:stream';
|
||||
import type {ReadableStream as NodeReadableStream} from 'node:stream/web';
|
||||
import {createChildLogger} from '@electron/common/Logger';
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '@electron/main/DesktopSessionHTTP';
|
||||
import {resolveDesktopTrustedCertificates} from '@electron/main/DesktopTrustedCertificates';
|
||||
import {normalizeHTTPNetworkOrigin} from '@fluxer/instance_bootstrap/src/NetworkOrigin';
|
||||
import {isPublicIpAddress, parseIpAddress} from '@fluxer/ip_utils/src/IpAddress';
|
||||
|
||||
const logger = createChildLogger('DesktopOutboundHTTP');
|
||||
|
||||
@@ -54,8 +55,6 @@ const DESKTOP_OUTBOUND_HTTP_TRANSPORT_MESSAGE = 'The request could not be comple
|
||||
const DESKTOP_OUTBOUND_HTTP_TIMEOUT_MESSAGE = 'The request timed out';
|
||||
const DESKTOP_OUTBOUND_HTTP_CAPACITY_MESSAGE = 'Too many downloads are already in progress';
|
||||
|
||||
const IPV6_GROUP_COUNT = 8;
|
||||
|
||||
const DesktopOutboundBlockReason = Object.freeze({
|
||||
INSECURE_TRANSPORT: 'insecure-transport',
|
||||
INVALID_TARGET: 'invalid-target',
|
||||
@@ -299,143 +298,13 @@ function blocked(
|
||||
return new DesktopOutboundHTTPBlockedError();
|
||||
}
|
||||
|
||||
function stripIPv6ZoneIdentifier(value: string): string {
|
||||
const zoneIndex = value.indexOf('%');
|
||||
if (zoneIndex === -1) {
|
||||
return value;
|
||||
}
|
||||
const addressPart = value.slice(0, zoneIndex);
|
||||
return addressPart.includes(':') ? addressPart : value;
|
||||
}
|
||||
|
||||
function normalizeIPv6(value: string): string {
|
||||
try {
|
||||
const hostname = new URL(`http://[${value}]`).hostname;
|
||||
return hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function parseIPAddress(value: string): PinnedAddress | null {
|
||||
const trimmed = value.trim();
|
||||
const unbracketed = trimmed.startsWith('[') && trimmed.endsWith(']') ? trimmed.slice(1, -1) : trimmed;
|
||||
const unzoned = stripIPv6ZoneIdentifier(unbracketed);
|
||||
if (isIPv4(unzoned)) {
|
||||
return {address: unzoned, family: 4};
|
||||
}
|
||||
if (isIPv6(unzoned)) {
|
||||
return {address: normalizeIPv6(unzoned), family: 6};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseIPv4Octets(address: string): Array<number> | null {
|
||||
const parts = address.split('.');
|
||||
if (parts.length !== 4) {
|
||||
return null;
|
||||
}
|
||||
const octets = parts.map((part) => (/^\d{1,3}$/u.test(part) ? Number.parseInt(part, 10) : Number.NaN));
|
||||
if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) {
|
||||
return null;
|
||||
}
|
||||
return octets;
|
||||
}
|
||||
|
||||
function ipv4Value(octets: ReadonlyArray<number>): number {
|
||||
return ((octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]) >>> 0;
|
||||
}
|
||||
|
||||
function isIPv4InCIDR(value: number, base: number, prefixLength: number): boolean {
|
||||
const mask = prefixLength === 0 ? 0 : (0xffffffff << (32 - prefixLength)) >>> 0;
|
||||
return (value & mask) >>> 0 === (base & mask) >>> 0;
|
||||
}
|
||||
|
||||
const IPV4_NON_PUBLIC_RANGES: ReadonlyArray<readonly [base: number, prefixLength: number]> = Object.freeze([
|
||||
[0x00000000, 8],
|
||||
[0x0a000000, 8],
|
||||
[0x64400000, 10],
|
||||
[0x7f000000, 8],
|
||||
[0xa9fe0000, 16],
|
||||
[0xac100000, 12],
|
||||
[0xc0000000, 24],
|
||||
[0xc0000200, 24],
|
||||
[0xc0a80000, 16],
|
||||
[0xc6120000, 15],
|
||||
[0xc6336400, 24],
|
||||
[0xcb007100, 24],
|
||||
[0xe0000000, 4],
|
||||
[0xf0000000, 4],
|
||||
]);
|
||||
|
||||
function isPublicIPv4Address(address: string): boolean {
|
||||
const octets = parseIPv4Octets(address);
|
||||
if (octets == null) {
|
||||
return false;
|
||||
}
|
||||
const value = ipv4Value(octets);
|
||||
return !IPV4_NON_PUBLIC_RANGES.some(([base, prefixLength]) => isIPv4InCIDR(value, base, prefixLength));
|
||||
}
|
||||
|
||||
function expandIPv6Groups(address: string): Array<string> {
|
||||
const halves = address.split('::');
|
||||
if (halves.length === 2) {
|
||||
const left = halves[0].length > 0 ? halves[0].split(':') : [];
|
||||
const right = halves[1].length > 0 ? halves[1].split(':') : [];
|
||||
const missing = Math.max(IPV6_GROUP_COUNT - left.length - right.length, 0);
|
||||
return [...left, ...Array<string>(missing).fill('0'), ...right].map((group) => group.padStart(4, '0'));
|
||||
}
|
||||
return address.split(':').map((group) => group.padStart(4, '0'));
|
||||
}
|
||||
|
||||
function ipv4FromMappedIPv6(groups: ReadonlyArray<string>): string | null {
|
||||
const isMapped =
|
||||
groups[0] === '0000' &&
|
||||
groups[1] === '0000' &&
|
||||
groups[2] === '0000' &&
|
||||
groups[3] === '0000' &&
|
||||
groups[4] === '0000' &&
|
||||
groups[5] === 'ffff';
|
||||
if (!isMapped) {
|
||||
return null;
|
||||
}
|
||||
const high = Number.parseInt(groups[6], 16);
|
||||
const low = Number.parseInt(groups[7], 16);
|
||||
return `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;
|
||||
}
|
||||
|
||||
function isPublicIPv6Address(address: string): boolean {
|
||||
const groups = expandIPv6Groups(address);
|
||||
if (groups.length !== IPV6_GROUP_COUNT) {
|
||||
return false;
|
||||
}
|
||||
const mapped = ipv4FromMappedIPv6(groups);
|
||||
if (mapped != null) {
|
||||
return isPublicIPv4Address(mapped);
|
||||
}
|
||||
const first = Number.parseInt(groups[0], 16);
|
||||
const second = Number.parseInt(groups[1], 16);
|
||||
const last = Number.parseInt(groups[7], 16);
|
||||
if (groups.slice(0, 7).every((group) => group === '0000') && (last === 0 || last === 1)) {
|
||||
return false;
|
||||
}
|
||||
if ((first & 0xe000) !== 0x2000) {
|
||||
return false;
|
||||
}
|
||||
if ((first & 0xffc0) === 0xfe80) {
|
||||
return false;
|
||||
}
|
||||
if ((first & 0xfe00) === 0xfc00) {
|
||||
return false;
|
||||
}
|
||||
if ((first & 0xff00) === 0xff00) {
|
||||
return false;
|
||||
}
|
||||
return !(first === 0x2001 && second === 0x0db8);
|
||||
const parsed = parseIpAddress(value);
|
||||
return parsed == null ? null : {address: parsed.normalized, family: parsed.family === 'ipv4' ? 4 : 6};
|
||||
}
|
||||
|
||||
function isPublicPinnedAddress(pinned: PinnedAddress): boolean {
|
||||
return pinned.family === 4 ? isPublicIPv4Address(pinned.address) : isPublicIPv6Address(pinned.address);
|
||||
return isPublicIpAddress(pinned.address);
|
||||
}
|
||||
|
||||
function proxiedHostScope(hostname: string): DesktopOriginAddressScope {
|
||||
|
||||
@@ -30,6 +30,7 @@ function transform(name) {
|
||||
}
|
||||
|
||||
const happyEyeballsSource = transform('DesktopHappyEyeballs.ts');
|
||||
const ipAddressSource = transform('../../../packages/ip_utils/src/IpAddress.ts');
|
||||
const outboundSource = transform('DesktopOutboundHTTP.ts');
|
||||
const fileDownloadsSource = transform('FileDownloads.ts');
|
||||
|
||||
@@ -112,6 +113,11 @@ async function loadFileDownloads({registerOrigin = true} = {}) {
|
||||
vm.runInContext(happyEyeballsSource.code, happyEyeballsContext, {filename: happyEyeballsSource.path});
|
||||
stubs['@electron/main/DesktopHappyEyeballs'] = happyEyeballsModule.exports;
|
||||
|
||||
const ipAddressModule = {exports: {}};
|
||||
const ipAddressContext = vm.createContext({...sandbox, exports: ipAddressModule.exports, module: ipAddressModule});
|
||||
vm.runInContext(ipAddressSource.code, ipAddressContext, {filename: ipAddressSource.path});
|
||||
stubs['@fluxer/ip_utils/src/IpAddress'] = ipAddressModule.exports;
|
||||
|
||||
const outboundModule = {exports: {}};
|
||||
sandbox.module = outboundModule;
|
||||
sandbox.exports = outboundModule.exports;
|
||||
|
||||
@@ -11,9 +11,7 @@ const {GatewayOriginRegistry} = await import('./GatewayOriginRegistry.ts');
|
||||
const {DesktopOutboundHTTP} = await import('./DesktopOutboundHTTP.ts');
|
||||
const {RendererDocumentOwnerFactory} = await import('./RendererDocumentOwner.ts');
|
||||
const {parseNativeGatewayTransportEvent} = await import('./NativeGatewayTransportEventParser.ts');
|
||||
const {websocketHTTPOrigin, websocketOrigin} = await import('./WebSocketOrigin.ts');
|
||||
const {getNativeGatewayDisableReason} = await import('./LaunchOptions.ts');
|
||||
const {reconstructNativeGatewayTransportEvent} = await import('../preload/PreloadNativeGatewayEvent.ts');
|
||||
const {createNativeGatewayPreloadAPI} = await import('../preload/NativeGatewayPreload.ts');
|
||||
const {
|
||||
NATIVE_GATEWAY_TRANSPORT_AVAILABLE_RENDERER_ARG,
|
||||
@@ -874,18 +872,6 @@ describe('GatewayOriginRegistry', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('WebSocketOrigin', () => {
|
||||
test('derives both the ws and the http form with default ports collapsed', () => {
|
||||
assert.equal(websocketOrigin('wss://gateway.fluxer.app/socket?v=9'), 'wss://gateway.fluxer.app');
|
||||
assert.equal(websocketOrigin('https://gateway.fluxer.app'), 'wss://gateway.fluxer.app');
|
||||
assert.equal(websocketOrigin('ws://127.0.0.1:8080/'), 'ws://127.0.0.1:8080');
|
||||
assert.equal(websocketOrigin('file:///etc/passwd'), null);
|
||||
assert.equal(websocketHTTPOrigin('wss://gateway.fluxer.app/socket'), 'https://gateway.fluxer.app');
|
||||
assert.equal(websocketHTTPOrigin('ws://192.168.1.5:8080/'), 'http://192.168.1.5:8080');
|
||||
assert.equal(websocketHTTPOrigin('nope'), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NativeGatewayTransportEventParser', () => {
|
||||
test('accepts the addon exact seven field shape and normalises binary to ArrayBuffer', () => {
|
||||
const parsed = parseNativeGatewayTransportEvent(
|
||||
@@ -922,59 +908,6 @@ describe('NativeGatewayTransportEventParser', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('PreloadNativeGatewayEvent', () => {
|
||||
test('independently revalidates a well formed main event', () => {
|
||||
const event = {
|
||||
connectionId: nextConnectionId(),
|
||||
kind: 'message',
|
||||
data: '{"op":11}',
|
||||
binary: null,
|
||||
code: null,
|
||||
reason: null,
|
||||
wasClean: null,
|
||||
message: null,
|
||||
};
|
||||
assert.equal(reconstructNativeGatewayTransportEvent(event).data, '{"op":11}');
|
||||
});
|
||||
|
||||
test('rejects a forged connection id, a missing key and an unknown kind', () => {
|
||||
const base = {
|
||||
connectionId: nextConnectionId(),
|
||||
kind: 'open',
|
||||
data: null,
|
||||
binary: null,
|
||||
code: null,
|
||||
reason: null,
|
||||
wasClean: null,
|
||||
message: null,
|
||||
};
|
||||
assert.throws(
|
||||
() => reconstructNativeGatewayTransportEvent({...base, connectionId: 'gateway-renderer-x:y'}),
|
||||
/connection id has an invalid format/u,
|
||||
);
|
||||
const {message: _message, ...missing} = base;
|
||||
assert.throws(() => reconstructNativeGatewayTransportEvent(missing), /exactly the declared keys/u);
|
||||
assert.throws(() => reconstructNativeGatewayTransportEvent({...base, kind: 'voice'}), /event kind is unknown/u);
|
||||
});
|
||||
|
||||
test('normalises a byte view to a standalone ArrayBuffer', () => {
|
||||
const source = new Uint8Array([9, 8, 7, 6]);
|
||||
const event = {
|
||||
connectionId: nextConnectionId(),
|
||||
kind: 'binary',
|
||||
data: null,
|
||||
binary: source.subarray(1, 3),
|
||||
code: null,
|
||||
reason: null,
|
||||
wasClean: null,
|
||||
message: null,
|
||||
};
|
||||
const reconstructed = reconstructNativeGatewayTransportEvent(event);
|
||||
assert.ok(reconstructed.binary instanceof ArrayBuffer);
|
||||
assert.deepEqual([...new Uint8Array(reconstructed.binary)], [8, 7]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('native gateway kill switches', () => {
|
||||
test('names the launch flag, the environment variable and safe mode as disable reasons', () => {
|
||||
assert.equal(getNativeGatewayDisableReason([]), null);
|
||||
|
||||
@@ -69,11 +69,13 @@ describe('reconstructing a native gateway transport event in the preload', () =>
|
||||
});
|
||||
|
||||
test('a binary frame is copied out of a view so the sender cannot mutate it afterwards', () => {
|
||||
const view = new Uint8Array([1, 2, 3, 4]);
|
||||
const source = new Uint8Array([9, 1, 2, 3, 4, 9]);
|
||||
const view = source.subarray(1, 5);
|
||||
const event = reconstructNativeGatewayTransportEvent(
|
||||
frame({kind: NativeGatewayTransportEventKind.BINARY, binary: view}),
|
||||
);
|
||||
view[0] = 99;
|
||||
assert.ok(event.binary instanceof ArrayBuffer);
|
||||
assert.deepEqual(Array.from(new Uint8Array(event.binary)), [1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user