diff --git a/README.md b/README.md index bcb1834a..6dfc95f9 100644 --- a/README.md +++ b/README.md @@ -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 = { @@ -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 diff --git a/index.d.ts b/index.d.ts index 331b8f2d..f56d78fe 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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 { diff --git a/lib/client.js b/lib/client.js index d62cbb5e..a9252a1b 100644 --- a/lib/client.js +++ b/lib/client.js @@ -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. @@ -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, }) diff --git a/lib/util/getEnv.js b/lib/util/getEnv.js new file mode 100644 index 00000000..9fa5067e --- /dev/null +++ b/lib/util/getEnv.js @@ -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()] || ''; +}; diff --git a/lib/util/proxy.js b/lib/util/proxy.js index f91e3b0a..cb2d9947 100644 --- a/lib/util/proxy.js +++ b/lib/util/proxy.js @@ -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(); - }); + }, };