proxy-from-env
Advanced tools
| on: [ push, pull_request, workflow_dispatch ] | ||
| name: Tests | ||
| jobs: | ||
| lint: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: actions/setup-node@v6 | ||
| - run: npm install # install eslint | ||
| - run: npm run lint | ||
| test-node: | ||
| runs-on: ubuntu-latest | ||
| strategy: | ||
| matrix: | ||
| node_version: | ||
| # The oldest Node.js version that we can still test. | ||
| # Node v20.11.0+ required for --test-reporter=lcov (test-coverage) | ||
| # Node v20.19.0 required for require(esm) require("proxy-from-env") | ||
| - 20 | ||
| # Other Node.js versions that have not reached End-of-life status per | ||
| # https://nodejs.org/en/about/previous-releases | ||
| - 24 | ||
| - 22 | ||
| - 25 | ||
| # ^ latest version may be an unstable (odd) version when the latest | ||
| # (even) version is not yet available. | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - name: Use Node.js ${{ matrix.node_version }} | ||
| uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: ${{ matrix.node_version }} | ||
| # Note: no npm ci / npm install: | ||
| # The package has no non-dev dependencies. | ||
| # We rely on Node.js's built-in test module and reporter, | ||
| # and do not require any dev dependencies either. | ||
| # test-coverage will also run the tests, but does not print helpful output upon test failure. | ||
| # So we also run the tests separately. | ||
| - run: npm test | ||
| # note: --experimental-test-coverage requires Node v18.15.0+ | ||
| # note: --test-reporter=lcov requires Node v20.11.0+ (https://github.com/nodejs/node/pull/50018) | ||
| - run: npm run test-coverage | ||
| - name: Send coverage for Node ${{ matrix.node_version }} to Coveralls | ||
| uses: coverallsapp/github-action@v2 | ||
| with: | ||
| parallel: true | ||
| file: lcov.info | ||
| flag-name: coverage-node-${{ matrix.node_version }} | ||
| coveralls: | ||
| name: Report to Coveralls | ||
| needs: [ test-node ] | ||
| if: ${{ github.repository == 'Rob--W/proxy-from-env' }} | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: coverallsapp/github-action@v2 | ||
| with: | ||
| parallel-finished: true |
| import {defineConfig, globalIgnores} from 'eslint/config'; | ||
| // These rules are from node-style-guide@1.0.0 to ensure some consistent style. | ||
| // Many of these rules are depecated and will be removed in eslint@11. At that | ||
| // point we should consider migration to @stylistic/eslint-plugin or prettier. | ||
| const rules = { | ||
| 'array-bracket-spacing': [2, 'never'], | ||
| 'block-scoped-var': 2, | ||
| 'brace-style': [2, '1tbs'], | ||
| 'camelcase': 1, | ||
| 'computed-property-spacing': [2, 'never'], | ||
| 'curly': 2, | ||
| 'eol-last': 2, | ||
| 'eqeqeq': [2, 'smart'], | ||
| 'max-depth': [1, 3], | ||
| 'max-len': [1, 80], | ||
| 'max-statements': [1, 15], | ||
| 'new-cap': 1, | ||
| 'no-extend-native': 2, | ||
| 'no-mixed-spaces-and-tabs': 2, | ||
| 'no-trailing-spaces': 2, | ||
| 'no-unused-vars': 1, | ||
| 'no-use-before-define': [2, 'nofunc'], | ||
| 'object-curly-spacing': [2, 'never'], | ||
| 'quotes': [2, 'single', 'avoid-escape'], | ||
| 'semi': [2, 'always'], | ||
| 'keyword-spacing': [2, {'before': true, 'after': true}], | ||
| 'space-unary-ops': 2 | ||
| }; | ||
| export default defineConfig([ | ||
| globalIgnores([ | ||
| 'coverage/', // Generated by: npm run test-coverage-as-html | ||
| ]), | ||
| { | ||
| languageOptions: { | ||
| globals: { | ||
| // Minimum set of globals, supported in Node. | ||
| process: 'readonly', | ||
| } | ||
| }, | ||
| rules, | ||
| }, | ||
| { | ||
| files: ['test.js'], | ||
| languageOptions: { | ||
| globals: { | ||
| // Minimum set of globals, supported in Node. | ||
| process: 'readonly', | ||
| } | ||
| }, | ||
| rules, | ||
| } | ||
| ]); |
+14
-19
| 'use strict'; | ||
| var parseUrl = require('url').parse; | ||
| var DEFAULT_PORTS = { | ||
@@ -14,14 +12,18 @@ ftp: 21, | ||
| var stringEndsWith = String.prototype.endsWith || function(s) { | ||
| return s.length <= this.length && | ||
| this.indexOf(s, this.length - s.length) !== -1; | ||
| }; | ||
| function parseUrl(urlString) { | ||
| try { | ||
| return new URL(urlString); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| /** | ||
| * @param {string|object} url - The URL, or the result from url.parse. | ||
| * @param {string|object|URL} url - The URL as a string or URL instance, or a | ||
| * compatible object (such as the result from legacy url.parse). | ||
| * @return {string} The URL of the proxy that should handle the request to the | ||
| * given URL. If no proxy is set, this will be an empty string. | ||
| */ | ||
| function getProxyForUrl(url) { | ||
| var parsedUrl = typeof url === 'string' ? parseUrl(url) : url || {}; | ||
| export function getProxyForUrl(url) { | ||
| var parsedUrl = (typeof url === 'string' ? parseUrl(url) : url) || {}; | ||
| var proto = parsedUrl.protocol; | ||
@@ -43,7 +45,3 @@ var hostname = parsedUrl.host; | ||
| var proxy = | ||
| getEnv('npm_config_' + proto + '_proxy') || | ||
| getEnv(proto + '_proxy') || | ||
| getEnv('npm_config_proxy') || | ||
| getEnv('all_proxy'); | ||
| var proxy = getEnv(proto + '_proxy') || getEnv('all_proxy'); | ||
| if (proxy && proxy.indexOf('://') === -1) { | ||
@@ -65,4 +63,3 @@ // Missing scheme in proxy, default to the requested URL's scheme. | ||
| function shouldProxy(hostname, port) { | ||
| var NO_PROXY = | ||
| (getEnv('npm_config_no_proxy') || getEnv('no_proxy')).toLowerCase(); | ||
| var NO_PROXY = getEnv('no_proxy').toLowerCase(); | ||
| if (!NO_PROXY) { | ||
@@ -96,3 +93,3 @@ return true; // Always proxy if NO_PROXY is not set. | ||
| // Stop proxying if the hostname ends with the no_proxy host. | ||
| return !stringEndsWith.call(hostname, parsedProxyHostname); | ||
| return !hostname.endsWith(parsedProxyHostname); | ||
| }); | ||
@@ -111,3 +108,1 @@ } | ||
| } | ||
| exports.getProxyForUrl = getProxyForUrl; |
+11
-10
| { | ||
| "name": "proxy-from-env", | ||
| "version": "1.1.0", | ||
| "version": "2.0.0", | ||
| "description": "Offers getProxyForUrl to get the proxy URL for a URL, respecting the *_PROXY (e.g. HTTP_PROXY) and NO_PROXY environment variables.", | ||
| "main": "index.js", | ||
| "exports": "./index.js", | ||
| "scripts": { | ||
| "lint": "eslint *.js", | ||
| "test": "mocha ./test.js --reporter spec", | ||
| "test-coverage": "istanbul cover ./node_modules/.bin/_mocha -- --reporter spec" | ||
| "lint": "eslint *.js *.mjs", | ||
| "test": "node --test ./test.js", | ||
| "test-coverage": "node --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info ./test.js", | ||
| "test-coverage-as-html": "npm run test-coverage && genhtml lcov.info -o coverage/" | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/Rob--W/proxy-from-env.git" | ||
| "url": "git+https://github.com/Rob--W/proxy-from-env.git" | ||
| }, | ||
@@ -29,7 +31,6 @@ "keywords": [ | ||
| "devDependencies": { | ||
| "coveralls": "^3.0.9", | ||
| "eslint": "^6.8.0", | ||
| "istanbul": "^0.4.5", | ||
| "mocha": "^7.1.0" | ||
| } | ||
| "eslint": "^9.39.2" | ||
| }, | ||
| "type": "module", | ||
| "sideEffects": false | ||
| } |
+32
-9
| # proxy-from-env | ||
| [](https://travis-ci.org/Rob--W/proxy-from-env) | ||
|  | ||
| [](https://coveralls.io/github/Rob--W/proxy-from-env?branch=master) | ||
| `proxy-from-env` is a Node.js package that exports a function (`getProxyForUrl`) | ||
| that takes an input URL (a string or | ||
| [`url.parse`](https://nodejs.org/docs/latest/api/url.html#url_url_parsing)'s | ||
| that takes an input URL (a string, an instance of | ||
| [`URL`](https://nodejs.org/docs/latest/api/url.html#the-whatwg-url-api), | ||
| or [`url.parse`](https://nodejs.org/docs/latest/api/url.html#url_url_parsing)'s | ||
| return value) and returns the desired proxy URL (also a string) based on | ||
@@ -25,6 +26,10 @@ standard proxy environment variables. If no proxy is set, an empty string is | ||
| warning: this simple example works for http requests only. To support https, | ||
| you must establish a proxy tunnel via the | ||
| [http `connect` method](https://developer.mozilla.org/en-us/docs/web/http/reference/methods/connect). | ||
| ```javascript | ||
| var http = require('http'); | ||
| var parseUrl = require('url').parse; | ||
| var getProxyForUrl = require('proxy-from-env').getProxyForUrl; | ||
| import http from 'node:test'; | ||
| import { getProxyForUrl } from 'proxy-from-env'; | ||
| // ^ or: var getProxyForUrl = require('proxy-from-env').getProxyForUrl; | ||
@@ -44,4 +49,4 @@ var some_url = 'http://example.com/something'; | ||
| // Should be proxied through proxy_url. | ||
| var parsed_some_url = parseUrl(some_url); | ||
| var parsed_proxy_url = parseUrl(proxy_url); | ||
| var parsed_some_url = new URL(some_url); | ||
| var parsed_proxy_url = new URL(proxy_url); | ||
| // A HTTP proxy is quite simple. It is similar to a normal request, except the | ||
@@ -68,5 +73,23 @@ // path is an absolute URL, and the proxied URL's host is put in the header | ||
| }); | ||
| ``` | ||
| ### Full proxy support | ||
| The simple example above works for http requests only. To support https, you | ||
| must establish a proxy tunnel via the | ||
| [http `connect` method](https://developer.mozilla.org/en-us/docs/web/http/reference/methods/connect). | ||
| An example of that is shown in the | ||
| [`https-proxy-agent` npm package](https://www.npmjs.com/package/https-proxy-agent). | ||
| The [`proxy-agent` npm package](https://www.npmjs.com/package/proxy-agent) | ||
| combines `https-proxy-agent` and `proxy-from-env` to offer a `http.Agent` that | ||
| supports proxies from environment variables. | ||
| ### Built-in proxy support | ||
| Node.js is working on built-in support for proxy environment variables, | ||
| currently behind `NODE_USE_ENV_PROXY=1` or `--use-env-proxy`. For details, see: | ||
| - https://github.com/nodejs/node/issues/57872 | ||
| - https://nodejs.org/api/http.html#built-in-proxy-support | ||
| ## Environment variables | ||
@@ -73,0 +96,0 @@ The environment variables can be specified in lowercase or uppercase, with the |
+52
-103
| /* eslint max-statements:0 */ | ||
| 'use strict'; | ||
| var assert = require('assert'); | ||
| var parseUrl = require('url').parse; | ||
| import {describe, it} from 'node:test'; | ||
| import assert from 'node:assert'; | ||
| import {parse as parseUrl} from 'node:url'; | ||
| var getProxyForUrl = require('./').getProxyForUrl; | ||
| import {getProxyForUrl} from 'proxy-from-env'; | ||
@@ -30,10 +31,2 @@ // Runs the callback with process.env temporarily set to env. | ||
| // Save call stack for later use. | ||
| var stack = {}; | ||
| Error.captureStackTrace(stack, testProxyUrl); | ||
| // Only use the last stack frame because that shows where this function is | ||
| // called, and that is sufficient for our purpose. No need to flood the logs | ||
| // with an uninteresting stack trace. | ||
| stack = stack.stack.split('\n', 2)[1]; | ||
| it(title, function() { | ||
@@ -44,14 +37,3 @@ var actual; | ||
| }); | ||
| if (expected === actual) { | ||
| return; // Good! | ||
| } | ||
| try { | ||
| assert.strictEqual(expected, actual); // Create a formatted error message. | ||
| // Should not happen because previously we determined expected !== actual. | ||
| throw new Error('assert.strictEqual passed. This is impossible!'); | ||
| } catch (e) { | ||
| // Use the original stack trace, so we can see a helpful line number. | ||
| e.stack = e.message + stack; | ||
| throw e; | ||
| } | ||
| assert.strictEqual(actual, expected); | ||
| }); | ||
@@ -83,2 +65,3 @@ } | ||
| testProxyUrl(env, '', '__proto__://'); | ||
| testProxyUrl(env, '', 'http://abc\x00/'); | ||
| testProxyUrl(env, '', undefined); | ||
@@ -89,2 +72,42 @@ testProxyUrl(env, '', null); | ||
| testProxyUrl(env, '', {host: 1, protocol: 'x'}); | ||
| describe('difference between url.parse and WHATWG URL', function() { | ||
| // Node 24 and later raise the following warning when url.parse is used: | ||
| // | ||
| // (node:11623) [DEP0169] DeprecationWarning: `url.parse()` behavior is | ||
| // not standardized and prone to errors that have security implications. | ||
| // Use the WHATWG URL API instead. CVEs are not issued for `url.parse()` | ||
| // vulnerabilities. | ||
| // | ||
| // The above refers to https://hackerone.com/reports/678487 which shows | ||
| // that a bare percentage sign is parsed inconsistently: | ||
| // - `url.parse` splits hosts. | ||
| // - WHATWG `URL` constructor raised an error. | ||
| // | ||
| // This test case shows the difference. | ||
| // | ||
| // For comparison: | ||
| // - curl (8.17.0) refuses to connect: | ||
| // $ http_proxy=http://localhost:1337 curl http://bad% | ||
| // curl:(3) URL rejected: Bad hostname | ||
| // - wget (GNU wget 1.25.0) passes "bad%" as Host header: | ||
| // $ http_proxy=http://localhost:1337 wget http://bad% | ||
| // (nc -l 1337 receives request with "bad% as Host header) | ||
| // - Python (3.13.11) passes "bad%" as Host header: | ||
| // $ http_proxy=http://localhost:1337 python3 -c \ | ||
| // 'import urllib.request;urllib.request.urlopen("http://bad%")' | ||
| // (nc -l 1337 receives request with "bad% as Host header) | ||
| // A canonical URL does not have a single "%". | ||
| var badUrl = 'http://bad%'; | ||
| // proxy-from-env@1.1.0 and earlier accepted bad URLs: | ||
| testProxyUrl(env, 'http://unexpected.proxy', parseUrl(badUrl)); | ||
| // Sanity check: WHATWG URL constructor rejects badUrl. | ||
| assert(!URL.canParse(badUrl)); | ||
| // Verify current proxy-from-env behavior. Should reject without throwing. | ||
| testProxyUrl(env, '', badUrl); | ||
| }); | ||
| }); | ||
@@ -395,94 +418,20 @@ | ||
| // Up until proxy-from-env@1.1.0, proxy-from-env had undocumented support for | ||
| // specifying proxies through npm_config_ prefixes. The historical reasons | ||
| // for them are no longer relevant: | ||
| // https://github.com/Rob--W/proxy-from-env/issues/13#issuecomment-3150256653 | ||
| describe('NPM proxy configuration', function() { | ||
| describe('npm_config_http_proxy should work', function() { | ||
| describe('npm_config_*_proxy variables are unsupported', function() { | ||
| var env = {}; | ||
| // eslint-disable-next-line camelcase | ||
| env.npm_config_http_proxy = 'http://http-proxy'; | ||
| testProxyUrl(env, '', 'https://example'); | ||
| testProxyUrl(env, 'http://http-proxy', 'http://example'); | ||
| // eslint-disable-next-line camelcase | ||
| env.npm_config_http_proxy = 'http://priority'; | ||
| testProxyUrl(env, 'http://priority', 'http://example'); | ||
| }); | ||
| // eslint-disable-next-line max-len | ||
| describe('npm_config_http_proxy should take precedence over HTTP_PROXY and npm_config_proxy', function() { | ||
| var env = {}; | ||
| // eslint-disable-next-line camelcase | ||
| env.npm_config_http_proxy = 'http://http-proxy'; | ||
| // eslint-disable-next-line camelcase | ||
| env.npm_config_proxy = 'http://unexpected-proxy'; | ||
| env.HTTP_PROXY = 'http://unexpected-proxy'; | ||
| testProxyUrl(env, 'http://http-proxy', 'http://example'); | ||
| }); | ||
| describe('npm_config_https_proxy should work', function() { | ||
| var env = {}; | ||
| // eslint-disable-next-line camelcase | ||
| env.npm_config_http_proxy = 'http://unexpected.proxy'; | ||
| testProxyUrl(env, '', 'https://example'); | ||
| // eslint-disable-next-line camelcase | ||
| env.npm_config_https_proxy = 'http://https-proxy'; | ||
| testProxyUrl(env, 'http://https-proxy', 'https://example'); | ||
| // eslint-disable-next-line camelcase | ||
| env.npm_config_https_proxy = 'http://priority'; | ||
| testProxyUrl(env, 'http://priority', 'https://example'); | ||
| }); | ||
| // eslint-disable-next-line max-len | ||
| describe('npm_config_https_proxy should take precedence over HTTPS_PROXY and npm_config_proxy', function() { | ||
| var env = {}; | ||
| // eslint-disable-next-line camelcase | ||
| env.npm_config_https_proxy = 'http://https-proxy'; | ||
| // eslint-disable-next-line camelcase | ||
| env.npm_config_proxy = 'http://unexpected-proxy'; | ||
| env.HTTPS_PROXY = 'http://unexpected-proxy'; | ||
| testProxyUrl(env, 'http://https-proxy', 'https://example'); | ||
| }); | ||
| describe('npm_config_proxy should work', function() { | ||
| var env = {}; | ||
| // eslint-disable-next-line camelcase | ||
| env.npm_config_proxy = 'http://http-proxy'; | ||
| testProxyUrl(env, 'http://http-proxy', 'http://example'); | ||
| testProxyUrl(env, 'http://http-proxy', 'https://example'); | ||
| // eslint-disable-next-line camelcase | ||
| env.npm_config_proxy = 'http://priority'; | ||
| testProxyUrl(env, 'http://priority', 'http://example'); | ||
| testProxyUrl(env, 'http://priority', 'https://example'); | ||
| }); | ||
| // eslint-disable-next-line max-len | ||
| describe('HTTP_PROXY and HTTPS_PROXY should take precedence over npm_config_proxy', function() { | ||
| var env = {}; | ||
| env.HTTP_PROXY = 'http://http-proxy'; | ||
| env.HTTPS_PROXY = 'http://https-proxy'; | ||
| // eslint-disable-next-line camelcase | ||
| env.npm_config_proxy = 'http://unexpected-proxy'; | ||
| testProxyUrl(env, 'http://http-proxy', 'http://example'); | ||
| testProxyUrl(env, 'http://https-proxy', 'https://example'); | ||
| }); | ||
| describe('npm_config_no_proxy should work', function() { | ||
| var env = {}; | ||
| env.HTTP_PROXY = 'http://proxy'; | ||
| // eslint-disable-next-line camelcase | ||
| env.npm_config_no_proxy = 'example'; | ||
| testProxyUrl(env, '', 'http://example'); | ||
| testProxyUrl(env, 'http://proxy', 'http://otherwebsite'); | ||
| testProxyUrl(env, '', 'https://example'); | ||
| }); | ||
| // eslint-disable-next-line max-len | ||
| describe('npm_config_no_proxy should take precedence over NO_PROXY', function() { | ||
| var env = {}; | ||
| env.HTTP_PROXY = 'http://proxy'; | ||
| env.NO_PROXY = 'otherwebsite'; | ||
| // eslint-disable-next-line camelcase | ||
| env.npm_config_no_proxy = 'example'; | ||
| testProxyUrl(env, '', 'http://example'); | ||
| testProxyUrl(env, 'http://proxy', 'http://otherwebsite'); | ||
| }); | ||
| }); | ||
| }); |
-29
| { | ||
| "env": { | ||
| "node": true | ||
| }, | ||
| "rules": { | ||
| "array-bracket-spacing": [2, "never"], | ||
| "block-scoped-var": 2, | ||
| "brace-style": [2, "1tbs"], | ||
| "camelcase": 1, | ||
| "computed-property-spacing": [2, "never"], | ||
| "curly": 2, | ||
| "eol-last": 2, | ||
| "eqeqeq": [2, "smart"], | ||
| "max-depth": [1, 3], | ||
| "max-len": [1, 80], | ||
| "max-statements": [1, 15], | ||
| "new-cap": 1, | ||
| "no-extend-native": 2, | ||
| "no-mixed-spaces-and-tabs": 2, | ||
| "no-trailing-spaces": 2, | ||
| "no-unused-vars": 1, | ||
| "no-use-before-define": [2, "nofunc"], | ||
| "object-curly-spacing": [2, "never"], | ||
| "quotes": [2, "single", "avoid-escape"], | ||
| "semi": [2, "always"], | ||
| "keyword-spacing": [2, {"before": true, "after": true}], | ||
| "space-unary-ops": 2 | ||
| } | ||
| } |
-10
| language: node_js | ||
| node_js: | ||
| - node | ||
| - lts/* | ||
| script: | ||
| - npm run lint | ||
| # test-coverage will also run the tests, but does not print helpful output upon test failure. | ||
| # So we also run the tests separately. | ||
| - npm run test | ||
| - npm run test-coverage && cat coverage/lcov.info | ./node_modules/.bin/coveralls && rm -rf coverage |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
31145
5.75%1
-75%508
0.4%155
17.42%Yes
NaN