fix(desktop): honour the system proxy in main-process requests (#3270)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-08 03:04:55 +02:00
1 parent 6de6be2358
commit 3071bc7525
16 files changed
+542 -19

No files matched your search

@@ -223,6 +223,7 @@ describe('AppImageUpdate in-place replacement', () => {
const progress = [];
const staged = await appImageUpdate.stageAppImageUpdate({
fetchImpl: fetch,
target: resolved.target,
url: `${baseUrl}/appimage`,
expectedSha256: NEW_SHA256,
@@ -249,6 +250,7 @@ describe('AppImageUpdate in-place replacement', () => {
const resolved = appImageUpdate.resolveAppImageTarget();
const staged = await appImageUpdate.stageAppImageUpdate({
fetchImpl: fetch,
target: resolved.target,
url: `${baseUrl}/appimage`,
expectedSha256: NEW_SHA256,
@@ -266,6 +268,7 @@ describe('AppImageUpdate in-place replacement', () => {
await assert.rejects(
appImageUpdate.stageAppImageUpdate({
fetchImpl: fetch,
target: resolved.target,
url: `${baseUrl}/corrupt`,
expectedSha256: NEW_SHA256,
@@ -289,6 +292,7 @@ describe('AppImageUpdate in-place replacement', () => {
await assert.rejects(
appImageUpdate.stageAppImageUpdate({
fetchImpl: fetch,
target: resolved.target,
url: `${baseUrl}/appimage`,
expectedSha256: '',
@@ -307,6 +311,7 @@ describe('AppImageUpdate in-place replacement', () => {
await assert.rejects(
appImageUpdate.stageAppImageUpdate({
fetchImpl: fetch,
target: resolved.target,
url: `${baseUrl}/nothing-here`,
expectedSha256: NEW_SHA256,
@@ -346,6 +351,7 @@ describe('AppImageUpdate in-place replacement', () => {
const resolved = appImageUpdate.resolveAppImageTarget();
const staged = await appImageUpdate.stageAppImageUpdate({
fetchImpl: fetch,
target: resolved.target,
url: `${baseUrl}/appimage`,
expectedSha256: NEW_SHA256,
@@ -387,6 +393,7 @@ describe('AppImageUpdate in-place replacement', () => {
await assert.rejects(
appImageUpdate.stageAppImageUpdate({
fetchImpl: fetch,
target: resolved.target,
url: `${baseUrl}/appimage`,
expectedSha256: NEW_SHA256,
@@ -428,6 +435,7 @@ describe('AppImageUpdate in-place replacement', () => {
const resolved = appImageUpdate.resolveAppImageTarget();
const staged = await appImageUpdate.stageAppImageUpdate({
fetchImpl: fetch,
target: resolved.target,
url: `${baseUrl}/appimage`,
expectedSha256: NEW_SHA256,
@@ -465,6 +473,7 @@ describe('AppImageUpdate in-place replacement', () => {
await assert.rejects(
appImageUpdate.stageAppImageUpdate({
fetchImpl: fetch,
target: resolved.target,
url: `${baseUrl}/appimage`,
expectedSha256: NEW_SHA256,
@@ -597,6 +606,7 @@ describe('AppImageUpdate in-place replacement', () => {
const appImageUpdate = loadAppImageUpdate({APPIMAGE: install.installedPath});
const resolved = appImageUpdate.resolveAppImageTarget();
const staged = await appImageUpdate.stageAppImageUpdate({
fetchImpl: fetch,
target: resolved.target,
url: `${baseUrl}/appimage`,
expectedSha256: NEW_SHA256,
@@ -619,6 +629,7 @@ describe('AppImageUpdate abandoned staging cleanup', () => {
const appImageUpdate = loadAppImageUpdate({APPIMAGE: install.installedPath});
const resolved = appImageUpdate.resolveAppImageTarget();
const abandoned = await appImageUpdate.stageAppImageUpdate({
fetchImpl: fetch,
target: resolved.target,
url: `${baseUrl}/appimage`,
expectedSha256: NEW_SHA256,
@@ -641,6 +652,7 @@ describe('AppImageUpdate abandoned staging cleanup', () => {
const resolved = appImageUpdate.resolveAppImageTarget();
const sibling = appImageUpdate.resolveAppImageTarget(siblingPath);
const siblingStaged = await appImageUpdate.stageAppImageUpdate({
fetchImpl: fetch,
target: sibling.target,
url: `${baseUrl}/appimage`,
expectedSha256: NEW_SHA256,
+2 -3
View File
@@ -166,14 +166,13 @@ export async function stageAppImageUpdate(options: {
url: string;
expectedSha256: string;
onProgress?: (progress: AppImageDownloadProgress) => void;
fetchImpl?: typeof fetch;
fetchImpl: typeof fetch;
}): Promise<StagedAppImageUpdate> {
const expected = options.expectedSha256.trim().toLowerCase();
if (!SHA256_PATTERN.test(expected)) {
throw new Error('Update checksum is missing or malformed.');
}
const fetchImpl = options.fetchImpl ?? fetch;
const response = await fetchImpl(options.url, {cache: 'no-store', redirect: 'follow'});
const response = await options.fetchImpl(options.url, {cache: 'no-store', redirect: 'follow'});
if (!response.ok || response.body == null) {
throw new Error(`Update download failed: ${response.status}`);
}
@@ -726,6 +726,116 @@ describe('DesktopSelectedInstanceClient', () => {
});
});
describe('DesktopOutboundHTTP system proxy routes', () => {
function proxiedOutboundHTTP() {
const sent = [];
const resolved = [];
const outboundHTTP = new DesktopOutboundHTTP({
resolveHostAddresses: async (hostname) => {
resolved.push(hostname);
throw Object.assign(new Error(`getaddrinfo ENOTFOUND ${hostname}`), {syscall: 'getaddrinfo'});
},
resolveProxy: async (url) =>
url.startsWith('https://direct.example') ? 'DIRECT' : 'PROXY 127.0.0.1:7890; DIRECT',
sendThroughSession: async (request) => {
sent.push({method: request.method, url: request.url.href, headers: request.headers});
return {
headers: {'content-type': 'image/png'},
message: Readable.from([PNG_BYTES]),
status: 200,
statusText: 'OK',
};
},
});
return {outboundHTTP, resolved, sent};
}
test('an instance behind the system proxy is anchored and fetched without a local lookup', async () => {
const {outboundHTTP, resolved, sent} = proxiedOutboundHTTP();
const requirement = await outboundHTTP.registerAnchoredOrigins({
anchorOrigin: 'https://api.blocked.example',
origins: ['https://media.blocked.example'],
});
assert.equal(requirement, 'public');
const message = await outboundHTTP.request({
body: null,
expectedOrigin: 'https://media.blocked.example',
headers: {Accept: 'image/png'},
method: 'GET',
originTrust: DesktopOriginTrust.REGISTERED,
serviceName: 'test',
signal: null,
timeoutMs: 500,
url: 'https://media.blocked.example/avatar.png',
});
assert.deepEqual(
await readBoundedMessage({
declaredBytes: null,
description: 'proxied',
maxBytes: 64,
maxChunks: 4,
message: message.message,
}),
PNG_BYTES,
);
assert.deepEqual(resolved, []);
assert.deepEqual(sent, [
{method: 'GET', url: 'https://media.blocked.example/avatar.png', headers: {Accept: 'image/png'}},
]);
outboundHTTP.cleanup();
});
test('a proxied download goes through the session and a direct one keeps the pinned lookup', async () => {
const {outboundHTTP, resolved, sent} = proxiedOutboundHTTP();
const message = await outboundHTTP.get({
context: 'test',
timeoutMs: 500,
url: new URL('https://cdn.blocked.example/icon.png'),
});
message.message.resume();
await assert.rejects(
outboundHTTP.get({context: 'test', timeoutMs: 500, url: new URL('https://direct.example/icon.png')}),
/could not be reached/,
);
assert.deepEqual(
sent.map((entry) => entry.url),
['https://cdn.blocked.example/icon.png'],
);
assert.deepEqual(resolved, ['direct.example']);
outboundHTTP.cleanup();
});
test('a proxied route keeps the cleartext and private address rules', async () => {
const {outboundHTTP, sent} = proxiedOutboundHTTP();
await outboundHTTP.registerAnchoredOrigins({
anchorOrigin: 'https://api.blocked.example',
origins: ['http://10.0.0.1'],
});
const request = (url, originTrust = DesktopOriginTrust.BOUND) => ({
body: null,
expectedOrigin: new URL(url).origin,
headers: null,
method: 'GET',
originTrust,
serviceName: 'test',
signal: null,
timeoutMs: 500,
url,
});
await assert.rejects(outboundHTTP.request(request('http://plain.blocked.example/x')), /requires https/);
await assert.rejects(
outboundHTTP.request(request('http://10.0.0.1/x', DesktopOriginTrust.REGISTERED)),
/cannot authorize the privately resolving origin/,
);
await assert.rejects(
outboundHTTP.get({context: 'test', timeoutMs: 500, url: new URL('https://10.0.0.1/x')}),
/could not be reached/,
);
assert.deepEqual(sent, []);
outboundHTTP.cleanup();
});
});
describe('desktop instance launch surfaces', () => {
test('--fluxer-instance is a development-only API endpoint flag', () => {
const originalDefaultApp = process.defaultApp;
+156 -1
View File
@@ -8,6 +8,13 @@ import {isIPv4, isIPv6, 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';
import {
type DesktopProxyResolver,
type DesktopSessionHTTPSender,
isDirectProxyRoute,
resolveDesktopSessionProxy,
sendThroughDesktopSession,
} from '@electron/main/DesktopSessionHTTP';
import {normalizeHTTPNetworkOrigin} from '@fluxer/instance_bootstrap/src/NetworkOrigin';
const logger = createChildLogger('DesktopOutboundHTTP');
@@ -120,6 +127,8 @@ interface DesktopAnchoredOriginRegistration {
interface DesktopOutboundHTTPOptions {
readonly resolveHostAddresses?: DesktopHostAddressResolver;
readonly resolveProxy?: DesktopProxyResolver;
readonly sendThroughSession?: DesktopSessionHTTPSender;
}
interface DesktopOutboundGETRequest {
@@ -130,7 +139,7 @@ interface DesktopOutboundGETRequest {
export interface DesktopOutboundHTTPMessage {
readonly headers: http.IncomingHttpHeaders;
readonly message: http.IncomingMessage;
readonly message: Readable;
readonly status: number;
readonly statusText: string;
readonly url: URL;
@@ -432,6 +441,11 @@ function isPublicPinnedAddress(pinned: PinnedAddress): boolean {
return pinned.family === 4 ? isPublicIPv4Address(pinned.address) : isPublicIPv6Address(pinned.address);
}
function proxiedHostScope(hostname: string): DesktopOriginAddressScope {
const literal = parseIPAddress(hostname);
return literal == null ? DesktopOriginAddressScope.PUBLIC : addressScope(literal);
}
export function requireDesktopHTTPOrigin(value: string): string {
const normalized = normalizeHTTPNetworkOrigin(value);
if (normalized == null) {
@@ -687,6 +701,17 @@ function createRequestBodyLimit(maximumBytes: number): Transform {
});
}
interface DesktopProxiedSend {
readonly body: Uint8Array | Readable | null;
readonly headers: Readonly<Record<string, string>>;
readonly method: string;
readonly release: () => void;
readonly signal: AbortSignal | null;
readonly target: URL;
readonly timeout: () => Error;
readonly timeoutMs: number;
}
export class DesktopOutboundHTTP {
private readonly httpAgent = new http.Agent({keepAlive: true, maxSockets: DESKTOP_OUTBOUND_HTTP_MAX_SOCKETS});
private readonly httpsAgent = new https.Agent({keepAlive: true, maxSockets: DESKTOP_OUTBOUND_HTTP_MAX_SOCKETS});
@@ -703,6 +728,8 @@ export class DesktopOutboundHTTP {
private readonly registeredRequirements = new Map<string, DesktopAddressRequirement>();
private readonly originRequestsInFlightByService = new Map<string, number>();
private readonly resolveHostAddresses: DesktopHostAddressResolver;
private readonly resolveProxy: DesktopProxyResolver;
private readonly sendThroughSession: DesktopSessionHTTPSender;
private inFlight = 0;
private originRequestsInFlight = 0;
private activeResolutions = 0;
@@ -710,6 +737,8 @@ export class DesktopOutboundHTTP {
public constructor(options: DesktopOutboundHTTPOptions = {}) {
this.resolveHostAddresses = options.resolveHostAddresses ?? lookupAllAddresses;
this.resolveProxy = options.resolveProxy ?? resolveDesktopSessionProxy;
this.sendThroughSession = options.sendThroughSession ?? sendThroughDesktopSession;
}
public async get(request: DesktopOutboundGETRequest): Promise<DesktopOutboundHTTPMessage> {
@@ -730,6 +759,15 @@ export class DesktopOutboundHTTP {
if (requirement === DesktopAddressRequirement.PUBLIC && request.url.protocol !== 'https:') {
throw blocked(DesktopOutboundBlockReason.INSECURE_TRANSPORT, request.context, request.url.hostname);
}
if (!(await this.routesDirect(request.url))) {
if (
requirement === DesktopAddressRequirement.PUBLIC &&
proxiedHostScope(request.url.hostname) !== DesktopOriginAddressScope.PUBLIC
) {
throw blocked(DesktopOutboundBlockReason.NON_PUBLIC_ADDRESS, request.context, request.url.hostname);
}
return await this.issueProxied(request, release);
}
const pinned = await this.pinAddress(request.url.hostname, requirement, request.context);
return await this.issue(request, pinned, release);
} catch (error) {
@@ -779,6 +817,11 @@ export class DesktopOutboundHTTP {
canonicalAnchor: string,
unresolvedAnchorRequirement: DesktopAddressRequirement | null,
): Promise<DesktopAddressRequirement> {
if (!(await this.routesDirect(new URL(canonicalAnchor)))) {
return proxiedHostScope(new URL(canonicalAnchor).hostname) === DesktopOriginAddressScope.PUBLIC
? DesktopAddressRequirement.PUBLIC
: DesktopAddressRequirement.ANY;
}
let anchor: DesktopOriginAddressBinding;
try {
anchor = await this.ensureBinding(canonicalAnchor, DesktopAddressRequirement.ANY);
@@ -825,6 +868,17 @@ export class DesktopOutboundHTTP {
throw new DesktopOutboundHTTPRequestBodyLimitError(maximumRequestBodyBytes);
}
const requirement = this.requirementFor(origin, request.originTrust);
if (!(await this.routesDirect(target))) {
const scope = proxiedHostScope(target.hostname);
if (requirement === DesktopAddressRequirement.PUBLIC && scope !== DesktopOriginAddressScope.PUBLIC) {
throw new DesktopOutboundHTTPPublicAddressRequiredError(origin);
}
if (scope === DesktopOriginAddressScope.PUBLIC && target.protocol !== 'https:') {
throw new DesktopOutboundHTTPInsecureTransportError(origin);
}
this.requireAdmission();
return await this.issueProxiedOriginRequest(request, target);
}
const binding = await this.ensureBinding(origin, requirement);
if (binding.scope === DesktopOriginAddressScope.PUBLIC && target.protocol !== 'https:') {
throw new DesktopOutboundHTTPInsecureTransportError(origin);
@@ -860,6 +914,107 @@ export class DesktopOutboundHTTP {
this.originRequestsInFlightByService.clear();
}
private async routesDirect(url: URL): Promise<boolean> {
return isDirectProxyRoute(await this.resolveProxy(url.href));
}
private issueProxied(request: DesktopOutboundGETRequest, release: () => void): Promise<DesktopOutboundHTTPMessage> {
return this.sendProxied({
body: null,
headers: {},
method: 'GET',
signal: null,
target: request.url,
timeout: () => new DesktopOutboundHTTPTimeoutError(),
timeoutMs: request.timeoutMs,
release,
}).catch((error: unknown) => {
if (error instanceof DesktopOutboundHTTPTimeoutError) {
throw error;
}
logger.warn('Outbound request failed', {context: request.context, hostname: request.url.hostname, error});
throw new DesktopOutboundHTTPTransportError();
});
}
private issueProxiedOriginRequest(
request: DesktopOutboundHTTPRequest,
target: URL,
): Promise<DesktopOutboundHTTPMessage> {
this.acquireSlot(request.serviceName);
let released = false;
const release = (): void => {
if (released) {
return;
}
released = true;
this.releaseSlot(request.serviceName);
};
return this.sendProxied({
body: this.proxiedRequestBody(request),
headers: requestHeaders(request.headers),
method: request.method,
signal: request.signal,
target,
timeout: () => new DesktopOutboundHTTPRequestTimeoutError(target.toString(), request.timeoutMs),
timeoutMs: request.timeoutMs,
release,
}).catch((error: unknown) => {
if (request.signal?.aborted === true) {
throw new DesktopOutboundHTTPRequestAbortedError(target.toString());
}
throw error;
});
}
private async sendProxied({
body,
headers,
method,
signal,
target,
timeout,
timeoutMs,
release,
}: DesktopProxiedSend): Promise<DesktopOutboundHTTPMessage> {
const deadline = new AbortController();
const timer = setTimeout(() => deadline.abort(timeout()), timeoutMs);
timer.unref();
const settle = (): void => {
clearTimeout(timer);
release();
};
try {
const response = await this.sendThroughSession({
body,
headers,
method,
signal: signal == null ? deadline.signal : AbortSignal.any([deadline.signal, signal]),
url: target,
});
response.message.on('end', settle);
response.message.on('close', settle);
response.message.on('error', settle);
return {...response, url: target};
} catch (error) {
settle();
throw error;
}
}
private proxiedRequestBody(request: DesktopOutboundHTTPRequest): Uint8Array | Readable | null {
const body = request.body;
if (body == null || body instanceof Uint8Array) {
return body;
}
const limit = createRequestBodyLimit(
request.maximumRequestBodyBytes ?? DESKTOP_OUTBOUND_HTTP_MAX_REQUEST_BODY_BYTES,
);
const source = Readable.fromWeb(body as unknown as NodeReadableStream<Uint8Array>);
source.on('error', (error: Error) => limit.destroy(error));
return source.pipe(limit);
}
private async pinAddress(
hostname: string,
requirement: DesktopAddressRequirement,
@@ -0,0 +1,193 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {Buffer} from 'node:buffer';
import type http from 'node:http';
import {Readable, type Writable} from 'node:stream';
import {net, session} from 'electron';
const CHROMIUM_REFUSED_REQUEST_HEADERS: ReadonlySet<string> = new Set([
'connection',
'content-length',
'cookie2',
'expect',
'host',
'keep-alive',
'referer',
'te',
'trailer',
'transfer-encoding',
'upgrade',
]);
const DIRECT_PROXY_ROUTE = 'DIRECT';
export interface DesktopSessionHTTPRequest {
readonly body: Uint8Array | Readable | null;
readonly headers: Readonly<Record<string, string>>;
readonly method: string;
readonly signal: AbortSignal;
readonly url: URL;
}
export interface DesktopSessionHTTPResponse {
readonly headers: http.IncomingHttpHeaders;
readonly message: Readable;
readonly status: number;
readonly statusText: string;
}
export type DesktopSessionHTTPSender = (request: DesktopSessionHTTPRequest) => Promise<DesktopSessionHTTPResponse>;
export type DesktopProxyResolver = (url: string) => Promise<string>;
class DesktopSessionHTTPAbortedError extends Error {
public constructor(url: string) {
super(`Desktop session request to ${url} was aborted`);
this.name = 'DesktopSessionHTTPAbortedError';
}
}
export function isDirectProxyRoute(route: string): boolean {
const first = route.split(';', 1)[0] ?? '';
return first.trim().toUpperCase() === DIRECT_PROXY_ROUTE;
}
export function resolveDesktopSessionProxy(url: string): Promise<string> {
return session.defaultSession.resolveProxy(url);
}
function isRefusedRequestHeader(name: string): boolean {
const normalized = name.toLowerCase();
return CHROMIUM_REFUSED_REQUEST_HEADERS.has(normalized) || normalized.startsWith('proxy-');
}
function decodedResponseHeaders(headers: Record<string, string | Array<string>>): http.IncomingHttpHeaders {
const result: http.IncomingHttpHeaders = {};
const decoded = headers['content-encoding'] != null;
for (const [name, value] of Object.entries(headers)) {
const normalized = name.toLowerCase();
if (decoded && (normalized === 'content-encoding' || normalized === 'content-length')) {
continue;
}
if (normalized === 'set-cookie') {
result['set-cookie'] = Array.isArray(value) ? value : [value];
continue;
}
result[normalized] = Array.isArray(value) ? value.join(', ') : value;
}
return result;
}
export function sendThroughDesktopSession(request: DesktopSessionHTTPRequest): Promise<DesktopSessionHTTPResponse> {
return new Promise<DesktopSessionHTTPResponse>((resolve, reject) => {
if (request.signal.aborted) {
reject(request.signal.reason);
return;
}
const clientRequest = net.request({
bypassCustomProtocolHandlers: true,
cache: 'no-store',
credentials: 'omit',
method: request.method,
redirect: 'manual',
session: session.defaultSession,
url: request.url.href,
useSessionCookies: false,
});
let responded = false;
let finished = false;
let body: Readable | null = null;
const fail = (error: unknown): void => {
if (finished) {
return;
}
finished = true;
request.signal.removeEventListener('abort', onAbort);
if (body == null) {
reject(error);
return;
}
body.destroy(error instanceof Error ? error : new DesktopSessionHTTPAbortedError(request.url.href));
};
const finish = (): void => {
finished = true;
request.signal.removeEventListener('abort', onAbort);
};
const onAbort = (): void => {
clientRequest.abort();
fail(request.signal.reason);
};
request.signal.addEventListener('abort', onAbort, {once: true});
for (const [name, value] of Object.entries(request.headers)) {
if (!isRefusedRequestHeader(name)) {
clientRequest.setHeader(name, value);
}
}
clientRequest.on('redirect', (status, _method, _location, headers) => {
responded = true;
finish();
clientRequest.abort();
resolve({
headers: decodedResponseHeaders(headers),
message: Readable.from([]),
status,
statusText: '',
});
});
clientRequest.on('response', (incoming) => {
responded = true;
const source = incoming as unknown as Readable;
const message = new Readable({
read() {
source.resume();
},
destroy(error, callback) {
if (!finished) {
finish();
clientRequest.abort();
}
callback(error);
},
});
body = message;
source.on('data', (chunk: Buffer) => {
if (!message.push(chunk)) {
source.pause();
}
});
source.on('end', () => {
finish();
message.push(null);
});
source.on('error', fail);
incoming.on('aborted', () => fail(new DesktopSessionHTTPAbortedError(request.url.href)));
resolve({
headers: decodedResponseHeaders(incoming.headers),
message,
status: incoming.statusCode,
statusText: incoming.statusMessage,
});
});
clientRequest.on('error', fail);
clientRequest.on('abort', () => {
if (!responded) {
fail(new DesktopSessionHTTPAbortedError(request.url.href));
}
});
const requestBody = request.body;
if (requestBody == null) {
clientRequest.end();
return;
}
if (requestBody instanceof Uint8Array) {
clientRequest.end(Buffer.from(requestBody));
return;
}
clientRequest.chunkedEncoding = true;
requestBody.on('error', (error: Error) => {
clientRequest.abort();
fail(error);
});
requestBody.pipe(clientRequest as unknown as Writable);
});
}
@@ -82,6 +82,13 @@ async function loadFileDownloads({registerOrigin = true} = {}) {
createChildLogger: () => ({debug() {}, info() {}, warn() {}, error() {}}),
},
'@fluxer/instance_bootstrap/src/NetworkOrigin': {normalizeHTTPNetworkOrigin: (value) => new URL(value).origin},
'@electron/main/DesktopSessionHTTP': {
isDirectProxyRoute: (route) => route === 'DIRECT',
resolveDesktopSessionProxy: async () => 'DIRECT',
sendThroughDesktopSession: async () => {
throw new Error('a direct route never reaches the session');
},
},
};
const sandbox = {
console,
@@ -414,7 +414,7 @@ function contentEncoding(incoming: http.IncomingHttpHeaders): string | null {
return normalized.length === 0 ? null : normalized;
}
function decodedMessageStream(message: http.IncomingMessage, encoding: string | null): Readable {
function decodedMessageStream(message: Readable, encoding: string | null): Readable {
if (encoding == null || encoding === 'identity') {
return message;
}
@@ -153,7 +153,8 @@ export function installElectronStub(extra = {}) {
}
}
const nativeTheme = {themeSource: 'system'};
state.electron = {nativeTheme, webContents, webFrameMain, ...extra, app};
const session = {defaultSession: {resolveProxy: async () => 'DIRECT'}};
state.electron = {nativeTheme, session, webContents, webFrameMain, ...extra, app};
return registry;
}
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {getDesktopOutboundHTTP} from '@electron/main/DesktopOutboundHTTP';
import {isDirectProxyRoute} from '@electron/main/DesktopSessionHTTP';
import type {GatewayOriginRegistry} from '@electron/main/GatewayOriginRegistry';
import {
createGatewaySocketBoundary,
@@ -28,6 +29,7 @@ import {
NATIVE_GATEWAY_TRANSPORT_CHANNELS,
NATIVE_GATEWAY_TRANSPORT_EVENT_CHANNEL,
NATIVE_GATEWAY_TRANSPORT_MAX_CONNECTIONS_PER_DOCUMENT,
NATIVE_GATEWAY_TRANSPORT_PROXIED_MESSAGE,
type NativeGatewayTransportCreateResult,
type NativeGatewayTransportEvent,
NativeGatewayTransportEventKind,
@@ -61,7 +63,7 @@ class NativeGatewayTransportUnavailableError extends Error {
class NativeGatewayTransportProxiedError extends Error {
public constructor() {
super('The native gateway transport cannot reach a gateway that resolves through a proxy');
super(NATIVE_GATEWAY_TRANSPORT_PROXIED_MESSAGE);
this.name = 'NativeGatewayTransportProxiedError';
}
}
@@ -263,7 +265,7 @@ export class DesktopNativeGatewayTransport {
private async requireDirectRoute(event: RendererDocumentIpcEvent, url: string): Promise<void> {
const route = await this.dependencies.resolveProxy(event, url.replace(/^ws/u, 'http'));
if (route.trim() !== 'DIRECT') {
if (!isDirectProxyRoute(route)) {
throw new NativeGatewayTransportProxiedError();
}
}
+4 -2
View File
@@ -10,6 +10,7 @@ import {
stageAppImageUpdate,
} from '@electron/main/AppImageUpdate';
import {relaunchStableLaunchPath} from '@electron/main/LinuxLaunchPath';
import {moduleNetworkFetch} from '@electron/main/ModuleNetworkFetch';
import {compareModuleVersions, parseModuleVersion} from '@electron/main/ModuleVersion';
import {getUpdateBaseUrl} from '@electron/main/ShellDownloadFormats';
import type {ShellUpdateCapability, ShellUpdatePlan} from '@electron/main/ShellUpdateCapability';
@@ -18,7 +19,7 @@ import {
readVelopackApplyAttempt,
recordVelopackApplyAttempt,
} from '@electron/main/UpdaterApplyState';
import {app, autoUpdater} from 'electron';
import {app, autoUpdater, net} from 'electron';
import type {UpdateInfo, VelopackAsset} from 'velopack';
const requireModule = createRequire(import.meta.url);
@@ -148,7 +149,7 @@ async function runVelopackSelfUpdate(control: SelfUpdateControl, hooks: ShellSel
}
async function fetchPublishedAppImage(): Promise<{version: string; sha256: string | null}> {
const response = await fetch(`${getUpdateBaseUrl()}/latest`, {
const response = await net.fetch(`${getUpdateBaseUrl()}/latest`, {
cache: 'no-store',
headers: {Accept: 'application/json', 'Cache-Control': 'no-cache'},
});
@@ -199,6 +200,7 @@ async function runAppImageSelfUpdate(
target,
url: `${getUpdateBaseUrl()}/${published.version}/appimage`,
expectedSha256: published.sha256,
fetchImpl: moduleNetworkFetch,
onProgress: ({transferred, total}) => {
if (control.isSettled() || total <= 0) return;
hooks.onDownloading(Math.min(100, (transferred / total) * 100));
+2 -2
View File
@@ -14,7 +14,7 @@ import {
DICTIONARY_SOURCES,
type SpellcheckDictionaryCatalogEntry,
} from '@electron/main/SpellcheckDictionaries';
import {app, ipcMain, type Session, type WebContents} from 'electron';
import {app, ipcMain, net, type Session, type WebContents} from 'electron';
import log from 'electron-log';
import {franc} from 'franc-min';
import {type Hunspell, type HunspellFactory, loadModule} from 'hunspell-asm';
@@ -280,7 +280,7 @@ const downloadDictionaryFile = async (
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), DICTIONARY_DOWNLOAD_TIMEOUT_MS);
try {
const response = await fetch(dictionaryFileUrl(dict, fileName), {
const response = await net.fetch(dictionaryFileUrl(dict, fileName), {
headers: {
Accept: 'application/octet-stream,*/*;q=0.8',
},
+2
View File
@@ -143,6 +143,7 @@ function loadUpdater({
'@electron/common/DesktopIdentity': {DESKTOP_ARTIFACT_PRODUCT_NAME: 'Fluxer-Canary'},
'@electron/common/UserDataPath': {isPortableMode: () => false},
'@electron/main/DesktopTray': {destroyDesktopTray() {}},
'@electron/main/ModuleNetworkFetch': {moduleNetworkFetch: (input, init) => sandbox.fetch(input, init)},
'@electron/main/LinuxSandbox': {isFlatpakRuntime: () => false},
'@electron/main/Troubleshooting': {
relaunchAndExit() {
@@ -164,6 +165,7 @@ function loadUpdater({
exit() {},
},
autoUpdater: {on() {}},
net: {fetch: (input, init) => sandbox.fetch(input, init)},
ipcMain: {
handle(channel, handler) {
handlers.set(channel, handler);
+4 -2
View File
@@ -13,6 +13,7 @@ import {
sweepAbandonedAppImageUpdates,
} from '@electron/main/AppImageUpdate';
import {destroyDesktopTray} from '@electron/main/DesktopTray';
import {moduleNetworkFetch} from '@electron/main/ModuleNetworkFetch';
import {MANUAL_DESKTOP_FORMATS, type ManualDesktopFormat} from '@electron/main/ShellDownloadFormats';
import {resolveShellUpdatePlan, ShellUpdateCapability} from '@electron/main/ShellUpdateCapability';
import {relaunchAndExit} from '@electron/main/Troubleshooting';
@@ -33,7 +34,7 @@ import {
type UpdaterDownloadOption,
} from '@electron/main/UpdaterDownloads';
import {setQuitting} from '@electron/main/Window';
import {app, autoUpdater, type BrowserWindow, ipcMain} from 'electron';
import {app, autoUpdater, type BrowserWindow, ipcMain, net} from 'electron';
import log from 'electron-log';
import type {UpdateInfo, VelopackAsset} from 'velopack';
@@ -552,7 +553,7 @@ async function fetchManualLatest(options: {forceRefresh?: boolean} = {}): Promis
if (!options.forceRefresh && manualLatestCache && now - manualLatestCache.at < MANUAL_CACHE_TTL_MS) {
return manualLatestCache.info;
}
const response = await fetch(`${UPDATE_BASE_URL}/latest`, {
const response = await net.fetch(`${UPDATE_BASE_URL}/latest`, {
cache: 'no-store',
headers: {
Accept: 'application/json',
@@ -628,6 +629,7 @@ async function downloadAppImageUpdate(
target,
url,
expectedSha256,
fetchImpl: moduleNetworkFetch,
onProgress: ({transferred, total}) => {
const now = Date.now();
const dtMs = now - lastSampleAt;