Add support for HTTP proxies

This commit is contained in:
Samuel Elliott 2023-07-12 23:55:05 +01:00
parent d7a64b9807
commit 0457746e5e
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
6 changed files with 147 additions and 5 deletions

View File

@ -30,6 +30,7 @@ export type Menu = import('electron').Menu;
export type MenuItem = import('electron').MenuItem;
export type MessageBoxOptions = import('electron').MessageBoxOptions;
export type Notification = import('electron').Notification;
export type Session = import('electron').Session;
export type Settings = import('electron').Settings;
export type ShareMenu = import('electron').ShareMenu;
export type SharingItem = import('electron').SharingItem;

View File

@ -1,14 +1,15 @@
import { app, BrowserWindow, ipcMain, LoginItemSettingsOptions } from './electron.js';
import { app, BrowserWindow, ipcMain, LoginItemSettingsOptions, session } from './electron.js';
import process from 'node:process';
import * as path from 'node:path';
import { EventEmitter } from 'node:events';
import { setGlobalDispatcher } from 'undici';
import * as persist from 'node-persist';
import MenuApp from './menu.js';
import { handleOpenWebServiceUri } from './webservices.js';
import { EmbeddedPresenceMonitor, PresenceMonitorManager } from './monitor.js';
import { createModalWindow, createWindow } from './windows.js';
import { setupIpc } from './ipc.js';
import { askUserForUri, showErrorDialog } from './util.js';
import { askUserForUri, buildElectronProxyAgent, showErrorDialog } from './util.js';
import { setAppInstance } from './app-menu.js';
import { handleAuthUri } from './na-auth.js';
import { DiscordPresenceConfiguration, LoginItem, LoginItemOptions, WindowType } from '../common/types.js';
@ -119,6 +120,11 @@ export async function init() {
initGlobals();
addUserAgent('nxapi-app (Chromium ' + process.versions.chrome + '; Electron ' + process.versions.electron + ')');
const agent = buildElectronProxyAgent({
session: session.defaultSession,
});
setGlobalDispatcher(agent);
app.setAboutPanelOptions({
applicationName: 'nxapi-app',
applicationVersion: process.platform === 'darwin' ? version : version +

View File

@ -1,11 +1,15 @@
import { BrowserWindow, dialog, Menu, MenuItem, MessageBoxOptions, nativeImage } from './electron.js';
import { BrowserWindow, dialog, Menu, MenuItem, MessageBoxOptions, nativeImage, Session } from './electron.js';
import path from 'node:path';
import { Buffer } from 'node:buffer';
import createDebug from '../../util/debug.js';
import { fetch } from 'undici';
import { dir } from '../../util/product.js';
import { App, Store } from './index.js';
import { SavedToken } from '../../common/auth/coral.js';
import { ErrorDescription } from '../../util/errors.js';
import { buildProxyAgent, ProxyAgentOptions } from '../../util/undici-proxy.js';
const debug = createDebug('app:main:util');
export const bundlepath = path.resolve(dir, 'dist', 'app', 'bundle');
@ -87,3 +91,35 @@ export function showErrorDialog(options: ErrorBoxOptions) {
dialog.showMessageBox(window, message_box_options) :
dialog.showMessageBox(message_box_options);
}
export function buildElectronProxyAgent(options: ProxyAgentOptions & {
session: Session;
}) {
let warned_proxy_unsupported: string | null = null;
return buildProxyAgent({
...options,
resolveProxy: async origin => {
// https://chromium.googlesource.com/chromium/src/+/HEAD/net/docs/proxy.md
const proxies = await options.session.resolveProxy(origin);
const proxy = proxies.split(';')[0].trim();
if (proxy === 'DIRECT') return null;
if (proxy.startsWith('PROXY ')) {
return new URL('http://' + proxy.substr(6));
}
if (proxy.startsWith('HTTPS ')) {
return new URL('https://' + proxy.substr(6));
}
if (warned_proxy_unsupported !== proxy) {
warned_proxy_unsupported = proxy;
debug('Unsupported proxy', proxy);
}
return null;
},
});
}

View File

@ -1,5 +1,6 @@
import process from 'node:process';
import Yargs from 'yargs';
import { setGlobalDispatcher } from 'undici';
import * as commands from './cli/index.js';
import { checkUpdates } from './common/update.js';
import createDebug from './util/debug.js';
@ -9,11 +10,15 @@ import { YargsArguments } from './util/yargs.js';
import { addUserAgent } from './util/useragent.js';
import { USER_AGENT_INFO_URL } from './common/constants.js';
import { init as initGlobals } from './common/globals.js';
import { buildEnvironmentProxyAgent } from './util/undici-proxy.js';
const debug = createDebug('cli');
initGlobals();
const agent = buildEnvironmentProxyAgent();
setGlobalDispatcher(agent);
export function createYargs(argv: string[]) {
const yargs = Yargs(argv).option('data-path', {
describe: 'Data storage path',

View File

@ -20,12 +20,12 @@ export class ErrorDescription {
if (description) {
return description.message +
(err instanceof Error ? '\n\n--\n\n' + (err.stack ?? err.message) : '');
(err instanceof Error ? '\n\n--\n\n' + util.inspect(err) : '');
}
}
if (err instanceof Error) {
return err.stack || err.message;
return util.inspect(err);
}
return util.inspect(err, {compact: true});

94
src/util/undici-proxy.ts Normal file
View File

@ -0,0 +1,94 @@
import { Agent, buildConnector, Dispatcher, errors } from 'undici';
import createDebug from './debug.js';
const debug = createDebug('nxapi:util:undici-proxy');
function defaultProtocolPort(protocol: string) {
return protocol === 'https:' ? 443 : 80;
}
export interface ProxyAgentOptions {
agent?: Agent;
requestTls?: buildConnector.BuildOptions;
}
export function buildProxyAgent(options: ProxyAgentOptions & {
resolveProxy: (origin: string) => Promise<URL | null>;
}) {
const agent = options.agent ?? new Agent();
const connectEndpoint = buildConnector(options.requestTls ?? {});
return new Agent({
connect: async (opts, callback) => {
let requestedHost = opts.host!;
if (!opts.port) {
requestedHost += `:${defaultProtocolPort(opts.protocol)}`;
}
try {
const request_origin = opts.protocol + '//' + opts.hostname +
(opts.port ? ':' + opts.port : '');
const proxy = await options.resolveProxy.call(null, request_origin);
debug('resolved proxy for %s as %s', request_origin, proxy?.toString());
if (!proxy) {
connectEndpoint(opts, callback);
return;
}
const { origin, port, host } = proxy;
const { socket, statusCode } = await agent.connect({
// @ts-expect-error
origin,
port,
path: requestedHost,
// @ts-expect-error
signal: opts.signal,
headers: {
host,
},
}) as unknown as Dispatcher.ConnectData;
if (statusCode !== 200) {
socket.on('error', () => {}).destroy();
callback(new errors.RequestAbortedError('Proxy response !== 200 when HTTP Tunneling'), null);
}
if (opts.protocol !== 'https:') {
// @ts-expect-error
callback(null, socket);
return;
}
// @ts-expect-error
connectEndpoint({ ...opts, httpSocket: socket }, callback);
} catch (err) {
callback(err as Error, null);
}
},
});
}
export function buildEnvironmentProxyAgent(options?: ProxyAgentOptions) {
return buildProxyAgent({
...options,
resolveProxy: resolveProxyFromEnvironment,
});
}
export async function resolveProxyFromEnvironment(origin: string) {
const { protocol } = new URL(origin);
if (protocol === 'http:' && process.env.HTTP_PROXY) {
return new URL(process.env.HTTP_PROXY);
}
if (protocol === 'https:' && process.env.HTTPS_PROXY) {
return new URL(process.env.HTTPS_PROXY);
}
return null;
}