Skip to content

feat: Improved proxy #122

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Help with preparing the key and certificate files for connection can be found in

### Connecting through an HTTP proxy

If you need to connect through an HTTP proxy, you simply need to provide the `proxy: {host, port}` option when creating the provider. For example:
The provider will retrieve HTTP proxy connection info from the system environment variables `apn_proxy`, `http_proxy`/`https_proxy`. If you for some reason need to connect through another specific HTTP proxy, you simply need to provide the `proxy: {host, port}` option when creating the provider. For example:

```javascript
var options = {
Expand All @@ -81,15 +81,20 @@ var options = {
},
proxy: {
host: "192.168.10.92",
port: 8080
}
port: 8080,
username: "user", // optional
password: "secretPassword" // optional
},
production: false
};

var apnProvider = new apn.Provider(options);
```

The provider will first send an HTTP CONNECT request to the specified proxy in order to establish an HTTP tunnel. Once established, it will create a new secure connection to the Apple Push Notification provider API through the tunnel.
To disable the default HTTP proxy behaviour, simply set the `proxy: false`.

When enabled, the provider will first send an HTTP CONNECT request to the specified proxy in order to establish an HTTP tunnel. Once established, it will create a new secure connection to the Apple Push Notification provider API through the tunnel.


### Using a pool of http/2 connections

Expand Down
2 changes: 1 addition & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export interface ProviderOptions {
/**
* Connect through an HTTP proxy
*/
proxy?: { host: string, port: number|string }
proxy?: { host: string, port: number|string, username?: string, password?: string }
}

export interface MultiProviderOptions extends ProviderOptions {
Expand Down
12 changes: 9 additions & 3 deletions lib/client.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const VError = require('verror');
const tls = require('tls');
const extend = require('./util/extend');
const createProxySocket = require('./util/proxy');
const { createProxySocket, shouldProxyHost, getSystemProxy } = require('./util/proxy');

module.exports = function (dependencies) {
// Used for routine logs such as HTTP status codes, etc.
Expand Down Expand Up @@ -96,8 +96,14 @@ module.exports = function (dependencies) {
Client.prototype.connect = function connect() {
if (this.sessionPromise) return this.sessionPromise;

const proxySocketPromise = this.config.proxy
? createProxySocket(this.config.proxy, {
const proxyOptions = this.config.proxy || getSystemProxy(this.config.port);
const shouldProxy =
this.config.proxy !== false &&
proxyOptions &&
shouldProxyHost(this.config.host, this.config.port);

const proxySocketPromise = shouldProxy
? createProxySocket(proxyOptions, {
host: this.config.address,
port: this.config.port,
})
Expand Down
9 changes: 9 additions & 0 deletions lib/util/getEnv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Get environment variable regardless of casing (upper/lowercase).
* @param {string} key - Name of the environment variable
* @returns {string} Value of the environment variable or empty string
*/
module.exports = function getEnv(key) {
if (!process || !process.env) return '';
return process.env[key.toUpperCase()] || process.env[key.toLowerCase()] || '';
};
138 changes: 125 additions & 13 deletions lib/util/proxy.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,130 @@
const http = require('http');
const getEnv = require('./getEnv');

module.exports = function createProxySocket(proxy, target) {
return new Promise((resolve, reject) => {
const req = http.request({
host: proxy.host,
port: proxy.port,
method: 'connect',
path: target.host + ':' + target.port,
headers: { Connection: 'Keep-Alive' },
module.exports = {
/**
* Connects to proxy and returns the socket
*
* @param {Object} proxy - Proxy connection object containing host, port, username and passord
* @param {Object} target - Proxy target containing host and port
* @returns {Socket} - HTTP socket
*/
createProxySocket: function (proxy, target) {
return new Promise((resolve, reject) => {
const proxyRequestOptions = {
host: proxy.host,
port: proxy.port,
method: 'CONNECT',
path: `${target.host || ''}${target.port ? `:${target.port}` : ''}`,
headers: { Connection: 'Keep-Alive' },
};

// Add proxy basic authentication header
if (proxy.username || proxy.password) {
const auth = `${proxy.username || ''}:${proxy.password || ''}`;
const base64 = Buffer.from(auth, 'utf8').toString('base64');

proxyRequestOptions.headers['Proxy-Authorization'] = 'Basic ' + base64;
}

const req = http.request(proxyRequestOptions);
req.on('error', reject);
req.on('connect', (res, socket, head) => resolve(socket));
req.end();
});
req.on('error', reject);
req.on('connect', (res, socket, head) => {
resolve(socket);
},

/**
* Get proxy connection info from the system environment variables
* Gathers connection info from environment variables in the following order:
* 1. apn_proxy
* 2. npm_config_http/https_proxy (https if targetPort: 443)
* 3. http/https_proxy (https if targetPort: 443)
* 4. all_proxy
* 5. npm_config_proxy
* 6. proxy
*
* @param {number} targetPort - Port number for the target host/webpage.
* @returns {Object} proxy - Object containing proxy information from the environment.
* @returns {string} proxy.host - Proxy hostname
* @returns {string} proxy.origin - Proxy port number
* @returns {string} proxy.port - Proxy port number
* @returns {string} proxy.protocol - Proxy connection protocol
* @returns {string} proxy.username - Username for connecting to the proxy
* @returns {string} proxy.password - Password for connecting to the proxy
*/
getSystemProxy: function (targetPort) {
const protocol = targetPort === 443 ? 'https' : 'http';
let proxy =
getEnv('apn_proxy') ||
getEnv(`npm_config_${protocol}_proxy`) ||
getEnv(`${protocol}_proxy`) ||
getEnv('all_proxy') ||
getEnv('npm_config_proxy') ||
getEnv('proxy');

// No proxy environment variable set
if (!proxy) return null;

// Append protocol scheme if missing from proxy url
if (proxy.indexOf('://') === -1) {
proxy = `${protocol}://${proxy}`;
}

// Parse proxy as Url to easier extract info
const parsedProxy = new URL(proxy);
return {
host: parsedProxy.hostname || parsedProxy.host,
origin: parsedProxy.origin,
port: parsedProxy.port,
protocol: parsedProxy.protocol,
username: parsedProxy.username,
password: parsedProxy.password,
};
},

/**
* Checks the `no_proxy` environment variable if a hostname (and port) should be proxied or not.
*
* @param {string} hostname - Hostname of the page we are connecting to (not the proxy itself)
* @param {string} port - Effective port number for the host
* @returns {boolean} Whether the hostname should be proxied or not
*/
shouldProxyHost: function (hostname, port) {
const noProxy = `${getEnv('no_proxy') || getEnv('npm_config_no_proxy')}`.toLowerCase();
if (!noProxy || noProxy === '*') return true; // No proxy restrictions are set or everything should be proxied

// Loop all excluded paths and check if host matches
return noProxy.split(/[,\s]+/).every(function (path) {
if (!path) return true;

// Parse path to separate host and port
const match = path.match(/^([^:]+)?(?::(\d+))?$/);
const proxyHost = match[1] || '';
const proxyPort = match[2] ? parseInt(match[2]) : '';

// If port is specified and it doesn't match
if (proxyPort && proxyPort !== port) return true;

// No hostname, but matching port is specified
if (proxyPort && !proxyHost) return false;

// If no wildcards or beginning with dot, return if exact match
if (!/^[.*]/.test(proxyHost)) {
if (hostname === proxyHost) return false;
}

// Escape any special characters in the hostname
const escapedProxyHost = proxyHost.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');

// Replace wildcard characters in the hostname with regular expression wildcards
const regexProxyHost = escapedProxyHost
.replace(/^\\\./, '\\*.') // Leading dot = wildcard
.replace(/\\\.$/, '\\*.') // Trailing dot = wildcard
.replace(/\\\*/g, '.*');

// Test the hostname against the regular expression
return !new RegExp(`^${regexProxyHost}$`).test(hostname);
});
req.end();
});
},
};