From e4dd93c245ab1299f3a9ec2a1c7eb63be372d60e Mon Sep 17 00:00:00 2001 From: Mats Andreassen Date: Thu, 22 Dec 2022 20:46:58 +0100 Subject: [PATCH 1/4] feat: Added support for system proxy envs and proxy auth --- lib/client.js | 10 +++- lib/util/getEnv.js | 9 +++ lib/util/proxy.js | 133 ++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 136 insertions(+), 16 deletions(-) create mode 100644 lib/util/getEnv.js diff --git a/lib/client.js b/lib/client.js index d62cbb5e..7bfad5c5 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,12 @@ 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..7481727c 100644 --- a/lib/util/proxy.js +++ b/lib/util/proxy.js @@ -1,18 +1,125 @@ 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(); - }); + }, }; From 07059205fb21a9e7c186bf7c50e96b132ea7a336 Mon Sep 17 00:00:00 2001 From: Mats Andreassen Date: Thu, 22 Dec 2022 21:04:50 +0100 Subject: [PATCH 2/4] docs: Updated README with updated proxy options --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) 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 From c4cc25fb82813cd6b7cf8f4a9e627b956000bf42 Mon Sep 17 00:00:00 2001 From: Mats Andreassen Date: Thu, 22 Dec 2022 21:17:44 +0100 Subject: [PATCH 3/4] refactor: lint --- lib/client.js | 8 ++-- lib/util/proxy.js | 101 ++++++++++++++++++++++++---------------------- 2 files changed, 58 insertions(+), 51 deletions(-) diff --git a/lib/client.js b/lib/client.js index 7bfad5c5..a9252a1b 100644 --- a/lib/client.js +++ b/lib/client.js @@ -97,9 +97,11 @@ module.exports = function (dependencies) { if (this.sessionPromise) return this.sessionPromise; const proxyOptions = this.config.proxy || getSystemProxy(this.config.port); - const shouldProxy = this.config.proxy !== false && - proxyOptions && shouldProxyHost(this.config.host, 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, diff --git a/lib/util/proxy.js b/lib/util/proxy.js index 7481727c..cb2d9947 100644 --- a/lib/util/proxy.js +++ b/lib/util/proxy.js @@ -4,12 +4,12 @@ const getEnv = require('./getEnv'); 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) { + createProxySocket: function (proxy, target) { return new Promise((resolve, reject) => { const proxyRequestOptions = { host: proxy.host, @@ -35,69 +35,74 @@ module.exports = { }, /** - * 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'); - + * 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}`; + 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 - } + 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 + 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) { + 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]) : "" - + 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; @@ -106,20 +111,20 @@ module.exports = { // If no wildcards or beginning with dot, return if exact match if (!/^[.*]/.test(proxyHost)) { - if (hostname === proxyHost) return false; + if (hostname === proxyHost) return false; } // Escape any special characters in the hostname - const escapedProxyHost = proxyHost.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); + 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, ".*"); + .replace(/^\\\./, '\\*.') // Leading dot = wildcard + .replace(/\\\.$/, '\\*.') // Trailing dot = wildcard + .replace(/\\\*/g, '.*'); // Test the hostname against the regular expression - return !(new RegExp(`^${regexProxyHost}$`).test(hostname)); + return !new RegExp(`^${regexProxyHost}$`).test(hostname); }); }, }; From 9e3783862781ecbc169bd8e06c8b0868ec3b6958 Mon Sep 17 00:00:00 2001 From: Mats Andreassen Date: Thu, 22 Dec 2022 21:20:03 +0100 Subject: [PATCH 4/4] fix: updated provider options type --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 {