Skip to content

Commit

Permalink
feat(request): add noProxy setting for configuration (yarnpkg#5048)
Browse files Browse the repository at this point in the history
  • Loading branch information
glausmichael committed Apr 26, 2018
1 parent 9e97f50 commit b6e0424
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 9 deletions.
54 changes: 48 additions & 6 deletions __tests__/util/request-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;

const net = require('net');
const https = require('https');
const http = require('http');
const path = require('path');

test('RequestManager.request with cafile', async () => {
Expand Down Expand Up @@ -119,6 +120,49 @@ test('RequestManager.request with mutual TLS', async () => {
expect(body).toBe('ok');
});

test('RequestManager.request with no_proxy option', async () => {
let bodyHttps;
let bodyHttp;
const options = {
key: await fs.readFile(path.join(__dirname, '..', 'fixtures', 'certificates', 'server-key.pem')),
cert: await fs.readFile(path.join(__dirname, '..', 'fixtures', 'certificates', 'server-cert.pem')),
};
const httpsServer = https.createServer(options, (req, res) => {
res.end('ok');
});
const httpServer = http.createServer((req, res) => {
res.end('ok');
});

try {
httpsServer.listen(0);
httpServer.listen(0);

const config = await Config.create({
cafile: path.join(__dirname, '..', 'fixtures', 'certificates', 'cacerts.pem'),
noProxy: 'localhost',
httpProxy: 'http://example-proxy.com',
httpsProxy : 'http://example-proxy.com',
});
const httpsPort = httpsServer.address().port;
bodyHttps = await config.requestManager.request({
url: `https://localhost:${httpsPort}/?nocache`,
headers: {Connection: 'close'},
});

const httpPort = httpServer.address().port;
bodyHttp = await config.requestManager.request({
url: `http://localhost:${httpPort}/?nocache`,
headers: {Connection: 'close'},
});
} finally {
httpsServer.close();
httpServer.close();
}
expect(bodyHttps).toBe('ok');
expect(bodyHttp).toBe('ok');
});

test('RequestManager.execute timeout error with maxRetryAttempts=1', async () => {
jest.useFakeTimers();

Expand Down Expand Up @@ -196,14 +240,12 @@ test('RequestManager.execute Request 403 error', async () => {
});
await config.requestManager.execute({
params: {
url: `https://localhost:port/?nocache`,
url: `https://localhost:80/?nocache`,
headers: {Connection: 'close'},
},
resolve: body => {},
reject: err => {
expect(err.message).toBe(
'https://localhost:port/?nocache: Request "https://localhost:port/?nocache" returned a 403',
);
expect(err.message).toBe('https://localhost:80/?nocache: Request "https://localhost:80/?nocache" returned a 403');
},
});
});
Expand All @@ -212,11 +254,11 @@ test('RequestManager.request with offlineNoRequests', async () => {
const config = await Config.create({offline: true}, new Reporter());
try {
await config.requestManager.request({
url: `https://localhost:port/?nocache`,
url: `https://localhost:80/?nocache`,
headers: {Connection: 'close'},
});
} catch (err) {
expect(err.message).toBe('Can\'t make a request in offline mode ("https://localhost:port/?nocache")');
expect(err.message).toBe('Can\'t make a request in offline mode ("https://localhost:80/?nocache")');
}
});

Expand Down
3 changes: 3 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type ConfigOptions = {

httpProxy?: ?string,
httpsProxy?: ?string,
noProxy?: ?string,

commandName?: ?string,
registry?: ?string,
Expand Down Expand Up @@ -282,10 +283,12 @@ export default class Config {

const httpProxy = opts.httpProxy || this.getOption('proxy');
const httpsProxy = opts.httpsProxy || this.getOption('https-proxy');
const noProxy = opts.noProxy || this.getOption('no_proxy');
this.requestManager.setOptions({
userAgent: String(this.getOption('user-agent')),
httpProxy: httpProxy === false ? false : String(httpProxy || ''),
httpsProxy: httpsProxy === false ? false : String(httpsProxy || ''),
noProxy: String(noProxy || ''),
strictSSL: Boolean(this.getOption('strict-ssl')),
ca: Array.prototype.concat(opts.ca || this.getOption('ca') || []).map(String),
cafile: String(opts.cafile || this.getOption('cafile', true) || ''),
Expand Down
57 changes: 57 additions & 0 deletions src/util/proxy-exclusion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* @flow */

import {URL} from 'url';

type Zone = {
hostname: string,
port: number,
hasPort: boolean,
};

function formatHostname(hostname: string): string {
// canonicalize the hostname, so that 'oogle.com' won't match 'google.com'
return hostname.replace(/^\.*/, '.').toLowerCase();
}

function parseNoProxyZone(zone: string): Zone {
zone = zone.trim().toLowerCase();

const zoneParts = zone.split(':', 2);
const hostname = formatHostname(zoneParts[0]);
const port: number = (zoneParts[1]: any);
const hasPort = zone.indexOf(':') > -1;

return {
hostname,
port,
hasPort,
};
}

export default function isExcludedFromProxy(url: string, noProxy: string): boolean {
if (!noProxy) {
return false;
}

if (noProxy === '*') {
return true;
}

const uri = new URL(url);

const port = uri.port || (uri.protocol === 'https:' ? '443' : '80');
const hostname = formatHostname(uri.hostname);
const noProxyList = noProxy.split(',');

// iterate through the noProxyList until it finds a match.
return noProxyList.map(parseNoProxyZone).some(noProxyZone => {
const isMatchedAt = hostname.indexOf(noProxyZone.hostname);
const hostnameMatched = isMatchedAt > -1 && isMatchedAt === hostname.length - noProxyZone.hostname.length;

if (noProxyZone.hasPort) {
return port === noProxyZone.port && hostnameMatched;
}

return hostnameMatched;
});
}
13 changes: 10 additions & 3 deletions src/util/request-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import BlockingQueue from './blocking-queue.js';
import * as constants from '../constants.js';
import * as network from './network.js';
import map from '../util/map.js';
import isExcludedFromProxy from './proxy-exclusion.js';

import typeof * as RequestModuleT from 'request';

Expand Down Expand Up @@ -97,6 +98,7 @@ export default class RequestManager {
userAgent: string;
reporter: Reporter;
running: number;
noProxy: string;
httpsProxy: string | boolean;
httpProxy: string | boolean;
strictSSL: boolean;
Expand All @@ -121,6 +123,7 @@ export default class RequestManager {
captureHar?: boolean,
httpProxy?: string | boolean,
httpsProxy?: string | boolean,
noProxy?: string,
strictSSL?: boolean,
ca?: Array<string>,
cafile?: string,
Expand Down Expand Up @@ -154,6 +157,10 @@ export default class RequestManager {
this.httpsProxy = opts.httpsProxy || '';
}

if (opts.noProxy != null) {
this.noProxy = opts.noProxy;
}

if (opts.strictSSL !== null && typeof opts.strictSSL !== 'undefined') {
this.strictSSL = opts.strictSSL;
}
Expand Down Expand Up @@ -410,9 +417,9 @@ export default class RequestManager {
params.encoding = null;
}

let proxy = this.httpProxy;
if (params.url.startsWith('https:')) {
proxy = this.httpsProxy;
let proxy;
if (!isExcludedFromProxy(params.url, this.noProxy)) {
proxy = params.url.startsWith('https:') ? this.httpsProxy : this.httpProxy;
}

if (proxy) {
Expand Down

0 comments on commit b6e0424

Please sign in to comment.