Skip to content

Commit

Permalink
Fix #254: Handle IPv6 loopback ::1 in NO_PROXY
Browse files Browse the repository at this point in the history
  • Loading branch information
bjowes committed Apr 16, 2024
1 parent f36939b commit 427be7a
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 16 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 4.1.7 - released 2024-04-16

- Fix #254: Handle IPv6 loopback ::1 in NO_PROXY
- Improved error reporting on upstream proxy settings

## 4.1.6 - released 2024-04-16

- Fix #253: Improved TypeScript typings, updated docs for typings
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ If your network environment enforces proxy usage for internet access (quite like
- `HTTPS_PROXY` - (optional) The URL to the proxy for accessing external HTTPS resources. Overrides `HTTP_PROXY` for HTTPS resources. Example: `http://proxy.acme.com:8080`
- `NO_PROXY` - A comma separated list of internal hosts to exclude from proxying. Add the host you are testing, and other local network resources used from the browser when accessing the host you are testing. Note that hosts that are located on the internet (not your intranet) must not be added, they should pass through the upstream proxy.

Include only the hostname (or IP), not the protocol or port. Wildcards are supported. Example: \*.acme.com
Include only the hostname (or IP), not the protocol or port. Wildcards are supported. Example: \*.acme.com. IPv6 addresses shall be quoted in brackets like `[::1]`.

Since the plugin requires traffic to localhost to be excluded from the corporate proxy, the plugin adds `localhost` and `127.0.0.1` to the `NO_PROXY` setting automatically unless they are already there. To disable this behavior (if you require an additional custom proxy), add `<-loopback>` to `NO_PROXY`.

Expand Down
53 changes: 38 additions & 15 deletions src/proxy/upstream.proxy.manager.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,77 @@
import { injectable } from "inversify";
import { inject, injectable } from "inversify";
import { URLExt } from "../util/url.ext";

import {
HttpHeaders,
IUpstreamProxyManager,
} from "./interfaces/i.upstream.proxy.manager";
import { TYPES } from "./dependency.injection.types";
import { IDebugLogger } from "../util/interfaces/i.debug.logger";

@injectable()
export class UpstreamProxyManager implements IUpstreamProxyManager {
private _httpProxyUrl?: URL;
private _httpsProxyUrl?: URL;
private _noProxyUrls?: string[];
private _debug: IDebugLogger;

constructor(@inject(TYPES.IDebugLogger) debug: IDebugLogger) {
this._debug = debug;
}

init(httpProxy?: string, httpsProxy?: string, noProxy?: string) {
if (httpProxy && this.validateUpstreamProxy(httpProxy, "HTTP_PROXY")) {
this._httpProxyUrl = new URL(httpProxy);
this._debug.log('Detected HTTP_PROXY:', httpProxy);
}
if (httpsProxy && this.validateUpstreamProxy(httpsProxy, "HTTPS_PROXY")) {
this._httpsProxyUrl = new URL(httpsProxy);
this._debug.log('Detected HTTPS_PROXY:', httpsProxy);
}
if (noProxy) {
// Might be a comma separated list of hosts
this._noProxyUrls = noProxy.split(",").map((item) => {
item = item.trim();
if (item.indexOf("*") === -1) {
if (item === '::1') item = '[::1]'; // Add quoting brackets for IPv6 loopback
if (item.indexOf("*") === -1 && this.validateNoProxyPart(item)) {
item = new URL(`http://${item}`).host; // Trim away default ports
}
return item;
});
this._debug.log('Detected NO_PROXY, parsed:', this._noProxyUrls.join(','));
}
}

private validateUpstreamProxy(proxyUrl: string, parameterName: string) {
const proxyParsed = new URL(proxyUrl);
if (
!proxyParsed.protocol ||
!proxyParsed.hostname ||
proxyParsed.pathname !== "/"
) {
throw new Error(
"Invalid " +
parameterName +
" argument. " +
"It must be a complete URL without path. Example: http://proxy.acme.com:8080"
);
try {
const proxyParsed = new URL(proxyUrl);
if (proxyParsed.protocol && proxyParsed.hostname && proxyParsed.pathname === "/") return true;
} catch (err) {
this._debug.log("Cannot parse upstream proxy URL", err);
}
return true;
throw new Error(
"Invalid " +
parameterName +
" argument '" + proxyUrl + "'. " +
"It must be a complete URL without path. Example: http://proxy.acme.com:8080"
);
}

private validateNoProxyPart(noProxyPart: string) {
try {
const proxyParsed = new URL(`http://${noProxyPart}`);
if (proxyParsed.hostname && proxyParsed.pathname === "/") return true;
} catch (err) {
this._debug.log("Cannot parse upstream proxy URL", err);
}
throw new Error(
"Invalid NO_PROXY argument part '" + noProxyPart + "'. " +
"It must be a comma separated list of: valid IP, hostname[:port] or a wildcard prefixed hostname. Protocol shall not be included. IPv6 addresses must be quoted in []. Examples: localhost,127.0.0.1,[::1],noproxy.acme.com:8080,*.noproxy.com"
);
}

private matchWithWildcardRule(str: string, rule: string): boolean {
if (rule.indexOf('*') === -1) return str === rule;
return new RegExp("^" + rule.split("*").join(".*") + "$").test(str);
}

Expand Down
91 changes: 91 additions & 0 deletions test/unittest/proxy/upstream.proxy.manager.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import assert from 'assert';
import { UpstreamProxyManager } from '../../../src/proxy/upstream.proxy.manager';
import Substitute, { Arg, SubstituteOf } from '@fluffy-spoon/substitute';
import { IDebugLogger } from '../../../src/util/interfaces/i.debug.logger';
import { DebugLogger } from '../../../src/util/debug.logger';

describe('Upstream proxy manager', function () {
let debugMock: SubstituteOf<IDebugLogger>;
let debugLogger = new DebugLogger();
let manager: UpstreamProxyManager;

before(function () {
debugMock = Substitute.for<IDebugLogger>();
debugMock.log(Arg.all()).mimicks(debugLogger.log);
manager = new UpstreamProxyManager(debugMock);
});

describe('hasHttpsUpstreamProxy', function () {
it('shall return false when no proxy is set', function () {
let res = manager.hasHttpsUpstreamProxy(new URL('http://localhost'));
assert.equal(res, false);
});

it('shall return true when HTTP_PROXY is set', function () {
manager.init('http://localhost:8080', undefined, undefined);
let res = manager.hasHttpsUpstreamProxy(new URL('http://localhost'));
assert.equal(res, true);
});

it('shall return true when HTTPS_PROXY is set', function () {
manager.init(undefined, 'http://localhost:8080', undefined);
let res = manager.hasHttpsUpstreamProxy(new URL('http://localhost'));
assert.equal(res, true);
});

it('shall return false when target matches NO_PROXY', function () {
manager.init('http://localhost:8080', undefined, 'localhost');
let res = manager.hasHttpsUpstreamProxy(new URL('http://localhost'));
assert.equal(res, false);
});

it('shall return true when target does not match NO_PROXY', function () {
manager.init('http://localhost:8080', undefined, 'localhost');
let res = manager.hasHttpsUpstreamProxy(new URL('http://akami.nu'));
assert.equal(res, true);
});

it('shall handle ::1 in NO_PROXY', function () {
manager.init('http://localhost:8080', undefined, 'localhost,::1');
let res = manager.hasHttpsUpstreamProxy(new URL('http://akami.nu'));
assert.equal(res, true);
});

it('shall return false when match on ::1 in NO_PROXY', function () {
manager.init('http://localhost:8080', undefined, 'localhost,::1');
let res = manager.hasHttpsUpstreamProxy(new URL('http://[::1]:9002'));
assert.equal(res, false);
});
});

describe('init', function () {
it('shall throw on invalid url (format) in NO_PROXY', function () {
const expextedErrorMessage = "Invalid NO_PROXY argument part 'local:::host'. " +
"It must be a comma separated list of: valid IP, hostname[:port] or a wildcard prefixed hostname. Protocol shall not be included. IPv6 addresses must be quoted in []. Examples: localhost,127.0.0.1,[::1],noproxy.acme.com:8080,*.noproxy.com";

assert.throws(() => manager.init('http://localhost:8080', undefined, 'localhost,local:::host,::1'),
{ message: expextedErrorMessage});
});

it('shall throw on invalid url (protocol) in NO_PROXY', function () {
const expextedErrorMessage = "Invalid NO_PROXY argument part 'http://localhost'. " +
"It must be a comma separated list of: valid IP, hostname[:port] or a wildcard prefixed hostname. Protocol shall not be included. IPv6 addresses must be quoted in []. Examples: localhost,127.0.0.1,[::1],noproxy.acme.com:8080,*.noproxy.com";

assert.throws(() => manager.init('http://localhost:8080', undefined, 'localhost,http://localhost,::1'),
{ message: expextedErrorMessage});
});

it('shall throw on invalid url in HTTP_PROXY', function () {
const expextedErrorMessage = "Invalid HTTP_PROXY argument 'http://local:::host:8080'. It must be a complete URL without path. Example: http://proxy.acme.com:8080";
assert.throws(() => manager.init('http://local:::host:8080', undefined, 'localhost,::1'),
{ message: expextedErrorMessage});
});

it('shall throw on invalid url in HTTPS_PROXY', function () {
const expextedErrorMessage = "Invalid HTTPS_PROXY argument 'http://local:::host:8080'. It must be a complete URL without path. Example: http://proxy.acme.com:8080";

assert.throws(() => manager.init('http://localhost:8080', 'http://local:::host:8080', 'localhost,::1'),
{ message: expextedErrorMessage});
});
});
});

0 comments on commit 427be7a

Please sign in to comment.