Import Upstream version 5.0.0+~cs8.0.0

This commit is contained in:
zhouganqing 2023-02-17 16:50:44 +08:00
commit bcfbfc69a2
39 changed files with 2622 additions and 0 deletions

37
.editorconfig Normal file
View File

@ -0,0 +1,37 @@
root = true
[*]
indent_style = tab
indent_size = 4
tab_width = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{*.json,*.json.example,*.gyp,*.yml,*.yaml,*.workflow}]
indent_style = space
indent_size = 2
[{*.py,*.asm}]
indent_style = space
[*.py]
indent_size = 4
[*.asm]
indent_size = 8
[*.md]
trim_trailing_whitespace = false
# Ideal settings - some plugins might support these.
[*.js]
quote_type = single
[{*.c,*.cc,*.h,*.hh,*.cpp,*.hpp,*.m,*.mm,*.mpp,*.js,*.java,*.go,*.rs,*.php,*.ng,*.jsx,*.ts,*.d,*.cs,*.swift}]
curly_bracket_next_line = false
spaces_around_operators = true
spaces_around_brackets = outside
# close enough to 1TB
indent_brace_style = K&R

86
.eslintrc.js Normal file
View File

@ -0,0 +1,86 @@
module.exports = {
'extends': [
'airbnb',
'prettier'
],
'parser': '@typescript-eslint/parser',
'parserOptions': {
'ecmaVersion': 2018,
'sourceType': 'module',
'modules': true
},
'plugins': [
'@typescript-eslint'
],
'settings': {
'import/resolver': {
'typescript': {
}
}
},
'rules': {
'quotes': [
2,
'single',
{
'allowTemplateLiterals': true
}
],
'class-methods-use-this': 0,
'consistent-return': 0,
'func-names': 0,
'global-require': 0,
'guard-for-in': 0,
'import/no-duplicates': 0,
'import/no-dynamic-require': 0,
'import/no-extraneous-dependencies': 0,
'import/prefer-default-export': 0,
'lines-between-class-members': 0,
'no-await-in-loop': 0,
'no-bitwise': 0,
'no-console': 0,
'no-continue': 0,
'no-control-regex': 0,
'no-empty': 0,
'no-loop-func': 0,
'no-nested-ternary': 0,
'no-param-reassign': 0,
'no-plusplus': 0,
'no-restricted-globals': 0,
'no-restricted-syntax': 0,
'no-shadow': 0,
'no-underscore-dangle': 0,
'no-use-before-define': 0,
'prefer-const': 0,
'prefer-destructuring': 0,
'camelcase': 0,
'no-unused-vars': 0, // in favor of '@typescript-eslint/no-unused-vars'
// 'indent': 0 // in favor of '@typescript-eslint/indent'
'@typescript-eslint/no-unused-vars': 'warn',
// '@typescript-eslint/indent': ['error', 2] // this might conflict with a lot ongoing changes
'@typescript-eslint/no-array-constructor': 'error',
'@typescript-eslint/adjacent-overload-signatures': 'error',
'@typescript-eslint/class-name-casing': 'error',
'@typescript-eslint/interface-name-prefix': 'error',
'@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/no-inferrable-types': 'error',
'@typescript-eslint/no-misused-new': 'error',
'@typescript-eslint/no-namespace': 'error',
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/no-parameter-properties': 'error',
'@typescript-eslint/no-triple-slash-reference': 'error',
'@typescript-eslint/prefer-namespace-keyword': 'error',
'@typescript-eslint/type-annotation-spacing': 'error',
// '@typescript-eslint/array-type': 'error',
// '@typescript-eslint/ban-types': 'error',
// '@typescript-eslint/explicit-function-return-type': 'warn',
// '@typescript-eslint/explicit-member-accessibility': 'error',
// '@typescript-eslint/member-delimiter-style': 'error',
// '@typescript-eslint/no-angle-bracket-type-assertion': 'error',
// '@typescript-eslint/no-explicit-any': 'warn',
// '@typescript-eslint/no-object-literal-type-assertion': 'error',
// '@typescript-eslint/no-use-before-define': 'error',
// '@typescript-eslint/no-var-requires': 'error',
// '@typescript-eslint/prefer-interface': 'error'
}
}

46
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,46 @@
name: Node CI
on:
push:
branches:
- master
tags:
- '!*'
pull_request:
jobs:
build:
name: Test Node.js ${{ matrix.node-version }} on ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: [6.x, 8.x, 10.x, 12.x]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Print Node.js Version
run: node --version
- name: Install Dependencies
run: npm install
env:
CI: true
- name: Run "build" step
run: npm run build --if-present
env:
CI: true
- name: Run tests
run: npm test
env:
CI: true

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/node_modules
/yarn.lock
/?.?s
/dist

3
.npmignore Normal file
View File

@ -0,0 +1,3 @@
History.md
test
.travis.yml

137
README.md Normal file
View File

@ -0,0 +1,137 @@
https-proxy-agent
================
### An HTTP(s) proxy `http.Agent` implementation for HTTPS
[![Build Status](https://github.com/TooTallNate/node-https-proxy-agent/workflows/Node%20CI/badge.svg)](https://github.com/TooTallNate/node-https-proxy-agent/actions?workflow=Node+CI)
This module provides an `http.Agent` implementation that connects to a specified
HTTP or HTTPS proxy server, and can be used with the built-in `https` module.
Specifically, this `Agent` implementation connects to an intermediary "proxy"
server and issues the [CONNECT HTTP method][CONNECT], which tells the proxy to
open a direct TCP connection to the destination server.
Since this agent implements the CONNECT HTTP method, it also works with other
protocols that use this method when connecting over proxies (i.e. WebSockets).
See the "Examples" section below for more.
Installation
------------
Install with `npm`:
``` bash
$ npm install https-proxy-agent
```
Examples
--------
#### `https` module example
``` js
var url = require('url');
var https = require('https');
var HttpsProxyAgent = require('https-proxy-agent');
// HTTP/HTTPS proxy to connect to
var proxy = process.env.http_proxy || 'http://168.63.76.32:3128';
console.log('using proxy server %j', proxy);
// HTTPS endpoint for the proxy to connect to
var endpoint = process.argv[2] || 'https://graph.facebook.com/tootallnate';
console.log('attempting to GET %j', endpoint);
var options = url.parse(endpoint);
// create an instance of the `HttpsProxyAgent` class with the proxy server information
var agent = new HttpsProxyAgent(proxy);
options.agent = agent;
https.get(options, function (res) {
console.log('"response" event!', res.headers);
res.pipe(process.stdout);
});
```
#### `ws` WebSocket connection example
``` js
var url = require('url');
var WebSocket = require('ws');
var HttpsProxyAgent = require('https-proxy-agent');
// HTTP/HTTPS proxy to connect to
var proxy = process.env.http_proxy || 'http://168.63.76.32:3128';
console.log('using proxy server %j', proxy);
// WebSocket endpoint for the proxy to connect to
var endpoint = process.argv[2] || 'ws://echo.websocket.org';
var parsed = url.parse(endpoint);
console.log('attempting to connect to WebSocket %j', endpoint);
// create an instance of the `HttpsProxyAgent` class with the proxy server information
var options = url.parse(proxy);
var agent = new HttpsProxyAgent(options);
// finally, initiate the WebSocket connection
var socket = new WebSocket(endpoint, { agent: agent });
socket.on('open', function () {
console.log('"open" event!');
socket.send('hello world');
});
socket.on('message', function (data, flags) {
console.log('"message" event! %j %j', data, flags);
socket.close();
});
```
API
---
### new HttpsProxyAgent(Object options)
The `HttpsProxyAgent` class implements an `http.Agent` subclass that connects
to the specified "HTTP(s) proxy server" in order to proxy HTTPS and/or WebSocket
requests. This is achieved by using the [HTTP `CONNECT` method][CONNECT].
The `options` argument may either be a string URI of the proxy server to use, or an
"options" object with more specific properties:
* `host` - String - Proxy host to connect to (may use `hostname` as well). Required.
* `port` - Number - Proxy port to connect to. Required.
* `protocol` - String - If `https:`, then use TLS to connect to the proxy.
* `headers` - Object - Additional HTTP headers to be sent on the HTTP CONNECT method.
* Any other options given are passed to the `net.connect()`/`tls.connect()` functions.
License
-------
(The MIT License)
Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
[CONNECT]: http://en.wikipedia.org/wiki/HTTP_tunnel#HTTP_CONNECT_Tunneling

View File

@ -0,0 +1,37 @@
root = true
[*]
indent_style = tab
indent_size = 4
tab_width = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{*.json,*.json.example,*.gyp,*.yml,*.yaml,*.workflow}]
indent_style = space
indent_size = 2
[{*.py,*.asm}]
indent_style = space
[*.py]
indent_size = 4
[*.asm]
indent_size = 8
[*.md]
trim_trailing_whitespace = false
# Ideal settings - some plugins might support these.
[*.js]
quote_type = single
[{*.c,*.cc,*.h,*.hh,*.cpp,*.hpp,*.m,*.mm,*.mpp,*.js,*.java,*.go,*.rs,*.php,*.ng,*.jsx,*.ts,*.d,*.cs,*.swift}]
curly_bracket_next_line = false
spaces_around_operators = true
spaces_around_brackets = outside
# close enough to 1TB
indent_brace_style = K&R

View File

@ -0,0 +1,86 @@
module.exports = {
'extends': [
'airbnb',
'prettier'
],
'parser': '@typescript-eslint/parser',
'parserOptions': {
'ecmaVersion': 2018,
'sourceType': 'module',
'modules': true
},
'plugins': [
'@typescript-eslint'
],
'settings': {
'import/resolver': {
'typescript': {
}
}
},
'rules': {
'quotes': [
2,
'single',
{
'allowTemplateLiterals': true
}
],
'class-methods-use-this': 0,
'consistent-return': 0,
'func-names': 0,
'global-require': 0,
'guard-for-in': 0,
'import/no-duplicates': 0,
'import/no-dynamic-require': 0,
'import/no-extraneous-dependencies': 0,
'import/prefer-default-export': 0,
'lines-between-class-members': 0,
'no-await-in-loop': 0,
'no-bitwise': 0,
'no-console': 0,
'no-continue': 0,
'no-control-regex': 0,
'no-empty': 0,
'no-loop-func': 0,
'no-nested-ternary': 0,
'no-param-reassign': 0,
'no-plusplus': 0,
'no-restricted-globals': 0,
'no-restricted-syntax': 0,
'no-shadow': 0,
'no-underscore-dangle': 0,
'no-use-before-define': 0,
'prefer-const': 0,
'prefer-destructuring': 0,
'camelcase': 0,
'no-unused-vars': 0, // in favor of '@typescript-eslint/no-unused-vars'
// 'indent': 0 // in favor of '@typescript-eslint/indent'
'@typescript-eslint/no-unused-vars': 'warn',
// '@typescript-eslint/indent': ['error', 2] // this might conflict with a lot ongoing changes
'@typescript-eslint/no-array-constructor': 'error',
'@typescript-eslint/adjacent-overload-signatures': 'error',
'@typescript-eslint/class-name-casing': 'error',
'@typescript-eslint/interface-name-prefix': 'error',
'@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/no-inferrable-types': 'error',
'@typescript-eslint/no-misused-new': 'error',
'@typescript-eslint/no-namespace': 'error',
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/no-parameter-properties': 'error',
'@typescript-eslint/no-triple-slash-reference': 'error',
'@typescript-eslint/prefer-namespace-keyword': 'error',
'@typescript-eslint/type-annotation-spacing': 'error',
// '@typescript-eslint/array-type': 'error',
// '@typescript-eslint/ban-types': 'error',
// '@typescript-eslint/explicit-function-return-type': 'warn',
// '@typescript-eslint/explicit-member-accessibility': 'error',
// '@typescript-eslint/member-delimiter-style': 'error',
// '@typescript-eslint/no-angle-bracket-type-assertion': 'error',
// '@typescript-eslint/no-explicit-any': 'warn',
// '@typescript-eslint/no-object-literal-type-assertion': 'error',
// '@typescript-eslint/no-use-before-define': 'error',
// '@typescript-eslint/no-var-requires': 'error',
// '@typescript-eslint/prefer-interface': 'error'
}
}

View File

@ -0,0 +1,46 @@
name: Node CI
on:
push:
branches:
- master
tags:
- '!*'
pull_request:
jobs:
build:
name: Test Node.js ${{ matrix.node-version }} on ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: [6.x, 8.x, 10.x, 12.x]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Print Node.js Version
run: node --version
- name: Install Dependencies
run: npm install
env:
CI: true
- name: Run "build" step
run: npm run build --if-present
env:
CI: true
- name: Run tests
run: npm test
env:
CI: true

4
http-proxy-agent/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/dist
/yarn.lock
/node_modules
/?.?s

View File

@ -0,0 +1,74 @@
http-proxy-agent
================
### An HTTP(s) proxy `http.Agent` implementation for HTTP
[![Build Status](https://github.com/TooTallNate/node-http-proxy-agent/workflows/Node%20CI/badge.svg)](https://github.com/TooTallNate/node-http-proxy-agent/actions?workflow=Node+CI)
This module provides an `http.Agent` implementation that connects to a specified
HTTP or HTTPS proxy server, and can be used with the built-in `http` module.
__Note:__ For HTTP proxy usage with the `https` module, check out
[`node-https-proxy-agent`](https://github.com/TooTallNate/node-https-proxy-agent).
Installation
------------
Install with `npm`:
``` bash
$ npm install http-proxy-agent
```
Example
-------
``` js
var url = require('url');
var http = require('http');
var HttpProxyAgent = require('http-proxy-agent');
// HTTP/HTTPS proxy to connect to
var proxy = process.env.http_proxy || 'http://168.63.76.32:3128';
console.log('using proxy server %j', proxy);
// HTTP endpoint for the proxy to connect to
var endpoint = process.argv[2] || 'http://nodejs.org/api/';
console.log('attempting to GET %j', endpoint);
var opts = url.parse(endpoint);
// create an instance of the `HttpProxyAgent` class with the proxy server information
var agent = new HttpProxyAgent(proxy);
opts.agent = agent;
http.get(opts, function (res) {
console.log('"response" event!', res.headers);
res.pipe(process.stdout);
});
```
License
-------
(The MIT License)
Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,57 @@
{
"name": "http-proxy-agent",
"version": "5.0.0",
"description": "An HTTP(s) proxy `http.Agent` implementation for HTTP",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"prebuild": "rimraf dist",
"build": "tsc",
"test": "mocha",
"test-lint": "eslint src --ext .js,.ts",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git://github.com/TooTallNate/node-http-proxy-agent.git"
},
"keywords": [
"http",
"proxy",
"endpoint",
"agent"
],
"author": "Nathan Rajlich <nathan@tootallnate.net> (http://n8.io/)",
"license": "MIT",
"bugs": {
"url": "https://github.com/TooTallNate/node-http-proxy-agent/issues"
},
"dependencies": {
"@tootallnate/once": "2",
"agent-base": "6",
"debug": "4"
},
"devDependencies": {
"@types/debug": "4",
"@types/node": "^12.19.2",
"@typescript-eslint/eslint-plugin": "1.6.0",
"@typescript-eslint/parser": "1.1.0",
"eslint": "5.16.0",
"eslint-config-airbnb": "17.1.0",
"eslint-config-prettier": "4.1.0",
"eslint-import-resolver-typescript": "1.1.1",
"eslint-plugin-import": "2.16.0",
"eslint-plugin-jsx-a11y": "6.2.1",
"eslint-plugin-react": "7.12.4",
"mocha": "^6.2.2",
"proxy": "1",
"rimraf": "^3.0.0",
"typescript": "^4.4.3"
},
"engines": {
"node": ">= 6"
}
}

View File

@ -0,0 +1,169 @@
import net from 'net';
import tls from 'tls';
import url from 'url';
import createDebug from 'debug';
import once from '@tootallnate/once';
import { Agent, ClientRequest, RequestOptions } from 'agent-base';
import { HttpProxyAgentOptions } from '.';
const debug = createDebug('http-proxy-agent');
interface HttpProxyAgentClientRequest extends ClientRequest {
path: string;
output?: string[];
outputData?: {
data: string;
}[];
_header?: string | null;
_implicitHeader(): void;
}
function isHTTPS(protocol?: string | null): boolean {
return typeof protocol === 'string' ? /^https:?$/i.test(protocol) : false;
}
/**
* The `HttpProxyAgent` implements an HTTP Agent subclass that connects
* to the specified "HTTP proxy server" in order to proxy HTTP requests.
*
* @api public
*/
export default class HttpProxyAgent extends Agent {
private secureProxy: boolean;
private proxy: HttpProxyAgentOptions;
constructor(_opts: string | HttpProxyAgentOptions) {
let opts: HttpProxyAgentOptions;
if (typeof _opts === 'string') {
opts = url.parse(_opts);
} else {
opts = _opts;
}
if (!opts) {
throw new Error(
'an HTTP(S) proxy server `host` and `port` must be specified!'
);
}
debug('Creating new HttpProxyAgent instance: %o', opts);
super(opts);
const proxy: HttpProxyAgentOptions = { ...opts };
// If `true`, then connect to the proxy server over TLS.
// Defaults to `false`.
this.secureProxy = opts.secureProxy || isHTTPS(proxy.protocol);
// Prefer `hostname` over `host`, and set the `port` if needed.
proxy.host = proxy.hostname || proxy.host;
if (typeof proxy.port === 'string') {
proxy.port = parseInt(proxy.port, 10);
}
if (!proxy.port && proxy.host) {
proxy.port = this.secureProxy ? 443 : 80;
}
if (proxy.host && proxy.path) {
// If both a `host` and `path` are specified then it's most likely
// the result of a `url.parse()` call... we need to remove the
// `path` portion so that `net.connect()` doesn't attempt to open
// that as a Unix socket file.
delete proxy.path;
delete proxy.pathname;
}
this.proxy = proxy;
}
/**
* Called when the node-core HTTP client library is creating a
* new HTTP request.
*
* @api protected
*/
async callback(
req: HttpProxyAgentClientRequest,
opts: RequestOptions
): Promise<net.Socket> {
const { proxy, secureProxy } = this;
const parsed = url.parse(req.path);
if (!parsed.protocol) {
parsed.protocol = 'http:';
}
if (!parsed.hostname) {
parsed.hostname = opts.hostname || opts.host || null;
}
if (parsed.port == null && typeof opts.port) {
parsed.port = String(opts.port);
}
if (parsed.port === '80') {
// if port is 80, then we can remove the port so that the
// ":80" portion is not on the produced URL
parsed.port = '';
}
// Change the `http.ClientRequest` instance's "path" field
// to the absolute path of the URL that will be requested.
req.path = url.format(parsed);
// Inject the `Proxy-Authorization` header if necessary.
if (proxy.auth) {
req.setHeader(
'Proxy-Authorization',
`Basic ${Buffer.from(proxy.auth).toString('base64')}`
);
}
// Create a socket connection to the proxy server.
let socket: net.Socket;
if (secureProxy) {
debug('Creating `tls.Socket`: %o', proxy);
socket = tls.connect(proxy as tls.ConnectionOptions);
} else {
debug('Creating `net.Socket`: %o', proxy);
socket = net.connect(proxy as net.NetConnectOpts);
}
// At this point, the http ClientRequest's internal `_header` field
// might have already been set. If this is the case then we'll need
// to re-generate the string since we just changed the `req.path`.
if (req._header) {
let first: string;
let endOfHeaders: number;
debug('Regenerating stored HTTP header string for request');
req._header = null;
req._implicitHeader();
if (req.output && req.output.length > 0) {
// Node < 12
debug(
'Patching connection write() output buffer with updated header'
);
first = req.output[0];
endOfHeaders = first.indexOf('\r\n\r\n') + 4;
req.output[0] = req._header + first.substring(endOfHeaders);
debug('Output buffer: %o', req.output);
} else if (req.outputData && req.outputData.length > 0) {
// Node >= 12
debug(
'Patching connection write() output buffer with updated header'
);
first = req.outputData[0].data;
endOfHeaders = first.indexOf('\r\n\r\n') + 4;
req.outputData[0].data =
req._header + first.substring(endOfHeaders);
debug('Output buffer: %o', req.outputData[0].data);
}
}
// Wait for the socket's `connect` event, so that this `callback()`
// function throws instead of the `http` request machinery. This is
// important for i.e. `PacProxyAgent` which determines a failed proxy
// connection via the `callback()` function throwing.
await once(socket, 'connect');
return socket;
}
}

View File

@ -0,0 +1,37 @@
import net from 'net';
import tls from 'tls';
import { Url } from 'url';
import { AgentOptions } from 'agent-base';
import _HttpProxyAgent from './agent';
function createHttpProxyAgent(
opts: string | createHttpProxyAgent.HttpProxyAgentOptions
): _HttpProxyAgent {
return new _HttpProxyAgent(opts);
}
namespace createHttpProxyAgent {
interface BaseHttpProxyAgentOptions {
secureProxy?: boolean;
host?: string | null;
path?: string | null;
port?: string | number | null;
}
export interface HttpProxyAgentOptions
extends AgentOptions,
BaseHttpProxyAgentOptions,
Partial<
Omit<
Url & net.NetConnectOpts & tls.ConnectionOptions,
keyof BaseHttpProxyAgentOptions
>
> {}
export type HttpProxyAgent = _HttpProxyAgent;
export const HttpProxyAgent = _HttpProxyAgent;
createHttpProxyAgent.prototype = _HttpProxyAgent.prototype;
}
export = createHttpProxyAgent;

View File

@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQCzURxIqzer0ACAbX/lHdsn4Gd9PLKrf7EeDYfIdV0HZKPD8WDr
bBx2/fBu0OW2sjnzv/SVZbJ0DAuPE/p0+eT0qb2qC10iz9iTD7ribd7gxhirVb8y
b3fBjXsxc8V8p4Ny1LcvNSqCjwUbJqdRogfoJeTiqPM58z5sNzuv5iq7iwIDAQAB
AoGAPMQy4olrP0UotlzlJ36bowLP70ffgHCwU+/f4NWs5fF78c3du0oSx1w820Dd
Z7E0JF8bgnlJJTxjumPZz0RUCugrEHBKJmzEz3cxF5E3+7NvteZcjKn9D67RrM5x
1/uSZ9cqKE9cYvY4fSuHx18diyZ4axR/wB1Pea2utjjDM+ECQQDb9ZbmmaWMiRpQ
5Up+loxP7BZNPsEVsm+DVJmEFbaFgGfncWBqSIqnPNjMwTwj0OigTwCAEGPkfRVW
T0pbYWCxAkEA0LK7SCTwzyDmhASUalk0x+3uCAA6ryFdwJf/wd8TRAvVOmkTEldX
uJ7ldLvfrONYO3v56uKTU/SoNdZYzKtO+wJAX2KM4ctXYy5BXztPpr2acz4qHa1N
Bh+vBAC34fOYhyQ76r3b1btHhWZ5jbFuZwm9F2erC94Ps5IaoqcX07DSwQJAPKGw
h2U0EPkd/3zVIZCJJQya+vgWFIs9EZcXVtvYXQyTBkVApTN66MhBIYjzkub5205J
bVQmOV37AKklY1DhwQJAA1wos0cYxro02edzatxd0DIR2r4qqOqLkw6BhYHhq6HJ
ZvIcQkHqdSXzdETFc01I1znDGGIrJHcnvKWgBPoEUg==
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIB1TCCAT4CCQDV5mPlzm9+izANBgkqhkiG9w0BAQUFADAvMS0wKwYDVQQDEyQ3
NTI3YmQ3Ny1hYjNlLTQ3NGItYWNlNy1lZWQ2MDUzOTMxZTcwHhcNMTUwNzA2MjI0
NTA3WhcNMjUwNzAzMjI0NTA3WjAvMS0wKwYDVQQDEyQ3NTI3YmQ3Ny1hYjNlLTQ3
NGItYWNlNy1lZWQ2MDUzOTMxZTcwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGB
ALNRHEirN6vQAIBtf+Ud2yfgZ308sqt/sR4Nh8h1XQdko8PxYOtsHHb98G7Q5bay
OfO/9JVlsnQMC48T+nT55PSpvaoLXSLP2JMPuuJt3uDGGKtVvzJvd8GNezFzxXyn
g3LUty81KoKPBRsmp1GiB+gl5OKo8znzPmw3O6/mKruLAgMBAAEwDQYJKoZIhvcN
AQEFBQADgYEACzoHUF8UV2Z6541Q2wKEA0UFUzmUjf/E1XwBO+1P15ZZ64uw34B4
1RwMPtAo9RY/PmICTWtNxWGxkzwb2JtDWtnxVER/lF8k2XcXPE76fxTHJF/BKk9J
QU8OTD1dd9gHCBviQB9TqntRZ5X7axjtuWjb2umY+owBYzAHZkp1HKI=
-----END CERTIFICATE-----

View File

@ -0,0 +1,389 @@
/**
* Module dependencies.
*/
const fs = require('fs');
const net = require('net');
const url = require('url');
const http = require('http');
const https = require('https');
const assert = require('assert');
const Proxy = require('proxy');
const { Agent } = require('agent-base');
const { HttpProxyAgent } = require('../');
const sleep = n => new Promise(r => setTimeout(r, n));
describe('HttpProxyAgent', function() {
let server;
let serverPort;
let proxy;
let proxyPort;
let sslProxy;
let sslProxyPort;
before(function(done) {
// setup HTTP proxy server
proxy = Proxy();
proxy.listen(function() {
proxyPort = proxy.address().port;
done();
});
});
before(function(done) {
// setup target HTTP server
server = http.createServer();
server.listen(function() {
serverPort = server.address().port;
done();
});
});
beforeEach(function() {
server.removeAllListeners('request');
});
before(function(done) {
// setup SSL HTTP proxy server
let options = {
key: fs.readFileSync(`${__dirname}/ssl-cert-snakeoil.key`),
cert: fs.readFileSync(`${__dirname}/ssl-cert-snakeoil.pem`)
};
sslProxy = Proxy(https.createServer(options));
sslProxy.listen(function() {
sslProxyPort = sslProxy.address().port;
done();
});
});
// shut down test HTTP server
after(function(done) {
proxy.once('close', function() {
done();
});
proxy.close();
});
after(function(done) {
server.once('close', function() {
done();
});
server.close();
});
after(function(done) {
sslProxy.once('close', function() {
done();
});
sslProxy.close();
});
describe('constructor', function() {
it('should throw an Error if no "proxy" argument is given', function() {
assert.throws(function() {
new HttpProxyAgent();
});
});
it('should accept a "string" proxy argument', function() {
let agent = new HttpProxyAgent(`http://127.0.0.1:${proxyPort}`);
assert.equal('127.0.0.1', agent.proxy.host);
assert.equal(proxyPort, agent.proxy.port);
});
it('should accept a `url.parse()` result object argument', function() {
let opts = url.parse(`http://127.0.0.1:${proxyPort}`);
let agent = new HttpProxyAgent(opts);
assert.equal('127.0.0.1', agent.proxy.host);
assert.equal(proxyPort, agent.proxy.port);
});
it('should set a `defaultPort` property', function() {
let opts = url.parse(`http://127.0.0.1:${proxyPort}`);
let agent = new HttpProxyAgent(opts);
assert.equal(80, agent.defaultPort);
});
describe('secureProxy', function() {
it('should default to `false`', function() {
let agent = new HttpProxyAgent({ port: proxyPort });
assert.equal(false, agent.secureProxy);
});
it('should be `false` when "http:" protocol is used', function() {
let agent = new HttpProxyAgent({
port: proxyPort,
protocol: 'http:'
});
assert.equal(false, agent.secureProxy);
});
it('should be `true` when "https:" protocol is used', function() {
let agent = new HttpProxyAgent({
port: proxyPort,
protocol: 'https:'
});
assert.equal(true, agent.secureProxy);
});
it('should be `true` when "https" protocol is used', function() {
let agent = new HttpProxyAgent({
port: proxyPort,
protocol: 'https'
});
assert.equal(true, agent.secureProxy);
});
it('should support a `defaultPort` option', function() {
let agent = new HttpProxyAgent({
port: proxyPort,
secureProxy: true
});
assert.equal(true, agent.secureProxy);
});
});
});
describe('"http" module', function() {
it('should work over an HTTP proxy', function(done) {
// set HTTP "request" event handler for this test
server.once('request', function(req, res) {
res.end(JSON.stringify(req.headers));
});
let proxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
`http://127.0.0.1:${proxyPort}`;
let agent = new HttpProxyAgent(proxy);
let opts = url.parse(`http://127.0.0.1:${serverPort}`);
opts.agent = agent;
http.get(opts, function(res) {
let data = '';
res.setEncoding('utf8');
res.on('data', function(b) {
data += b;
});
res.on('end', function() {
data = JSON.parse(data);
assert.equal(`127.0.0.1:${serverPort}`, data.host);
assert('via' in data);
done();
});
});
});
it('should work over an HTTPS proxy', function(done) {
// set HTTP "request" event handler for this test
server.once('request', function(req, res) {
res.end(JSON.stringify(req.headers));
});
let proxy =
process.env.HTTPS_PROXY ||
process.env.https_proxy ||
`https://127.0.0.1:${sslProxyPort}`;
proxy = url.parse(proxy);
proxy.rejectUnauthorized = false;
let agent = new HttpProxyAgent(proxy);
assert.equal(true, agent.secureProxy);
let opts = url.parse(`http://127.0.0.1:${serverPort}`);
opts.agent = agent;
http.get(opts, function(res) {
let data = '';
res.setEncoding('utf8');
res.on('data', function(b) {
data += b;
});
res.on('end', function() {
data = JSON.parse(data);
assert.equal(`127.0.0.1:${serverPort}`, data.host);
assert('via' in data);
done();
});
});
});
it('should proxy the query string of the request path', function(done) {
// set HTTP "request" event handler for this test
server.once('request', function(req, res) {
res.end(
JSON.stringify({
url: req.url
})
);
});
let proxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
`http://127.0.0.1:${proxyPort}`;
let agent = new HttpProxyAgent(proxy);
let opts = url.parse(
`http://127.0.0.1:${serverPort}/test?foo=bar&1=2`
);
opts.agent = agent;
http.get(opts, function(res) {
let data = '';
res.setEncoding('utf8');
res.on('data', function(b) {
data += b;
});
res.on('end', function() {
data = JSON.parse(data);
assert.equal('/test?foo=bar&1=2', data.url);
done();
});
});
});
it('should receive the 407 authorization code on the `http.ClientResponse`', function(done) {
// set a proxy authentication function for this test
proxy.authenticate = function(req, fn) {
// reject all requests
fn(null, false);
};
let proxyUri =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
`http://127.0.0.1:${proxyPort}`;
let agent = new HttpProxyAgent(proxyUri);
let opts = {};
// `host` and `port` don't really matter since the proxy will reject anyways
opts.host = '127.0.0.1';
opts.port = 80;
opts.agent = agent;
http.get(opts, function(res) {
assert.equal(407, res.statusCode);
assert('proxy-authenticate' in res.headers);
delete proxy.authenticate;
done();
});
});
it('should send the "Proxy-Authorization" request header', function(done) {
// set a proxy authentication function for this test
proxy.authenticate = function(req, fn) {
// username:password is "foo:bar"
fn(
null,
req.headers['proxy-authorization'] == 'Basic Zm9vOmJhcg=='
);
};
// set HTTP "request" event handler for this test
server.once('request', function(req, res) {
res.end(JSON.stringify(req.headers));
});
let proxyUri =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
`http://127.0.0.1:${proxyPort}`;
let proxyOpts = url.parse(proxyUri);
proxyOpts.auth = 'foo:bar';
let agent = new HttpProxyAgent(proxyOpts);
let opts = url.parse(`http://127.0.0.1:${serverPort}`);
opts.agent = agent;
http.get(opts, function(res) {
let data = '';
res.setEncoding('utf8');
res.on('data', function(b) {
data += b;
});
res.on('end', function() {
data = JSON.parse(data);
assert.equal(`127.0.0.1:${serverPort}`, data.host);
assert('via' in data);
delete proxy.authenticate;
done();
});
});
});
it('should emit an "error" event on the `http.ClientRequest` if the proxy does not exist', function(done) {
// port 4 is a reserved, but "unassigned" port
let proxyUri = 'http://127.0.0.1:4';
let agent = new HttpProxyAgent(proxyUri);
let opts = url.parse('http://nodejs.org');
opts.agent = agent;
let req = http.get(opts);
req.once('error', function(err) {
assert.equal('ECONNREFUSED', err.code);
req.abort();
done();
});
});
it('should work after the first tick of the `http.ClientRequest` instance', function(done) {
// set HTTP "request" event handler for this test
server.once('request', function(req, res) {
res.end(JSON.stringify(req.url));
});
let proxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
`http://127.0.0.1:${proxyPort}`;
let httpProxyAgent = new HttpProxyAgent(proxy);
// Defer the "connect()" function logic, since calling `req.end()`
// before the socket is returned causes the HTTP header to be
// generated *before* `HttpProxyAgent` can patch the `req.path`
// property, making the header incorrect.
const sleepAgent = new Agent((req, opts) => {
assert.equal(opts.secureEndpoint, false);
assert.equal(opts.protocol, 'http:');
assert(!req._header);
return sleep(10).then(() => {
assert.equal(typeof req._header, 'string');
return httpProxyAgent;
});
});
const opts = url.parse(`http://127.0.0.1:${serverPort}/test`);
opts.agent = sleepAgent;
http.get(opts, function(res) {
let data = '';
res.setEncoding('utf8');
res.on('data', function(b) {
data += b;
});
res.on('end', function() {
data = JSON.parse(data);
assert.equal('/test', data);
done();
});
});
});
it('should not send a port number for the default port', function(done) {
server.once('request', function(req, res) {
res.end(JSON.stringify(req.headers));
});
let proxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
`http://127.0.0.1:${proxyPort}`;
proxy = url.parse(proxy);
let agent = new HttpProxyAgent(proxy);
agent.defaultPort = serverPort;
let opts = url.parse(`http://127.0.0.1:${serverPort}`);
opts.agent = agent;
http.get(opts, function(res) {
let data = '';
res.setEncoding('utf8');
res.on('data', function(b) {
data += b;
});
res.on('end', function() {
data = JSON.parse(data);
assert.equal('127.0.0.1', data.host);
done();
});
});
});
});
});

View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"strict": true,
"module": "CommonJS",
"target": "es2015",
"esModuleInterop": true,
"lib": ["esnext"],
"outDir": "dist",
"sourceMap": true,
"declaration": true,
"typeRoots": [
"./@types",
"./node_modules/@types"
]
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

56
package.json Normal file
View File

@ -0,0 +1,56 @@
{
"name": "https-proxy-agent",
"version": "5.0.0",
"description": "An HTTP(s) proxy `http.Agent` implementation for HTTPS",
"main": "dist/index",
"types": "dist/index",
"files": [
"dist"
],
"scripts": {
"prebuild": "rimraf dist",
"build": "tsc",
"test": "mocha --reporter spec",
"test-lint": "eslint src --ext .js,.ts",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git://github.com/TooTallNate/node-https-proxy-agent.git"
},
"keywords": [
"https",
"proxy",
"endpoint",
"agent"
],
"author": "Nathan Rajlich <nathan@tootallnate.net> (http://n8.io/)",
"license": "MIT",
"bugs": {
"url": "https://github.com/TooTallNate/node-https-proxy-agent/issues"
},
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"devDependencies": {
"@types/debug": "4",
"@types/node": "^12.12.11",
"@typescript-eslint/eslint-plugin": "1.6.0",
"@typescript-eslint/parser": "1.1.0",
"eslint": "5.16.0",
"eslint-config-airbnb": "17.1.0",
"eslint-config-prettier": "4.1.0",
"eslint-import-resolver-typescript": "1.1.1",
"eslint-plugin-import": "2.16.0",
"eslint-plugin-jsx-a11y": "6.2.1",
"eslint-plugin-react": "7.12.4",
"mocha": "^6.2.2",
"proxy": "1",
"rimraf": "^3.0.0",
"typescript": "^3.5.3"
},
"engines": {
"node": ">= 6"
}
}

215
src/agent.ts Normal file
View File

@ -0,0 +1,215 @@
import net from 'net';
import tls from 'tls';
import url from 'url';
import assert from 'assert';
import createDebug from 'debug';
import { OutgoingHttpHeaders } from 'http';
import { Agent, ClientRequest, RequestOptions } from 'agent-base';
import { HttpsProxyAgentOptions } from '.';
import parseProxyResponse from './parse-proxy-response';
const debug = createDebug('https-proxy-agent:agent');
/**
* The `HttpsProxyAgent` implements an HTTP Agent subclass that connects to
* the specified "HTTP(s) proxy server" in order to proxy HTTPS requests.
*
* Outgoing HTTP requests are first tunneled through the proxy server using the
* `CONNECT` HTTP request method to establish a connection to the proxy server,
* and then the proxy server connects to the destination target and issues the
* HTTP request from the proxy server.
*
* `https:` requests have their socket connection upgraded to TLS once
* the connection to the proxy server has been established.
*
* @api public
*/
export default class HttpsProxyAgent extends Agent {
private secureProxy: boolean;
private proxy: HttpsProxyAgentOptions;
constructor(_opts: string | HttpsProxyAgentOptions) {
let opts: HttpsProxyAgentOptions;
if (typeof _opts === 'string') {
opts = url.parse(_opts);
} else {
opts = _opts;
}
if (!opts) {
throw new Error(
'an HTTP(S) proxy server `host` and `port` must be specified!'
);
}
debug('creating new HttpsProxyAgent instance: %o', opts);
super(opts);
const proxy: HttpsProxyAgentOptions = { ...opts };
// If `true`, then connect to the proxy server over TLS.
// Defaults to `false`.
this.secureProxy = opts.secureProxy || isHTTPS(proxy.protocol);
// Prefer `hostname` over `host`, and set the `port` if needed.
proxy.host = proxy.hostname || proxy.host;
if (typeof proxy.port === 'string') {
proxy.port = parseInt(proxy.port, 10);
}
if (!proxy.port && proxy.host) {
proxy.port = this.secureProxy ? 443 : 80;
}
// ALPN is supported by Node.js >= v5.
// attempt to negotiate http/1.1 for proxy servers that support http/2
if (this.secureProxy && !('ALPNProtocols' in proxy)) {
proxy.ALPNProtocols = ['http 1.1'];
}
if (proxy.host && proxy.path) {
// If both a `host` and `path` are specified then it's most likely
// the result of a `url.parse()` call... we need to remove the
// `path` portion so that `net.connect()` doesn't attempt to open
// that as a Unix socket file.
delete proxy.path;
delete proxy.pathname;
}
this.proxy = proxy;
}
/**
* Called when the node-core HTTP client library is creating a
* new HTTP request.
*
* @api protected
*/
async callback(
req: ClientRequest,
opts: RequestOptions
): Promise<net.Socket> {
const { proxy, secureProxy } = this;
// Create a socket connection to the proxy server.
let socket: net.Socket;
if (secureProxy) {
debug('Creating `tls.Socket`: %o', proxy);
socket = tls.connect(proxy as tls.ConnectionOptions);
} else {
debug('Creating `net.Socket`: %o', proxy);
socket = net.connect(proxy as net.NetConnectOpts);
}
const headers: OutgoingHttpHeaders = { ...proxy.headers };
const hostname = `${opts.host}:${opts.port}`;
let payload = `CONNECT ${hostname} HTTP/1.1\r\n`;
// Inject the `Proxy-Authorization` header if necessary.
if (proxy.auth) {
headers['Proxy-Authorization'] = `Basic ${Buffer.from(
proxy.auth
).toString('base64')}`;
}
// The `Host` header should only include the port
// number when it is not the default port.
let { host, port, secureEndpoint } = opts;
if (!isDefaultPort(port, secureEndpoint)) {
host += `:${port}`;
}
headers.Host = host;
headers.Connection = 'close';
for (const name of Object.keys(headers)) {
payload += `${name}: ${headers[name]}\r\n`;
}
const proxyResponsePromise = parseProxyResponse(socket);
socket.write(`${payload}\r\n`);
const {
statusCode,
buffered
} = await proxyResponsePromise;
if (statusCode === 200) {
req.once('socket', resume);
if (opts.secureEndpoint) {
const servername = opts.servername || opts.host;
if (!servername) {
throw new Error('Could not determine "servername"');
}
// The proxy is connecting to a TLS server, so upgrade
// this socket connection to a TLS connection.
debug('Upgrading socket connection to TLS');
return tls.connect({
...omit(opts, 'host', 'hostname', 'path', 'port'),
socket,
servername
});
}
return socket;
}
// Some other status code that's not 200... need to re-play the HTTP
// header "data" events onto the socket once the HTTP machinery is
// attached so that the node core `http` can parse and handle the
// error status code.
// Close the original socket, and a new "fake" socket is returned
// instead, so that the proxy doesn't get the HTTP request
// written to it (which may contain `Authorization` headers or other
// sensitive data).
//
// See: https://hackerone.com/reports/541502
socket.destroy();
const fakeSocket = new net.Socket();
fakeSocket.readable = true;
// Need to wait for the "socket" event to re-play the "data" events.
req.once('socket', (s: net.Socket) => {
debug('replaying proxy buffer for failed request');
assert(s.listenerCount('data') > 0);
// Replay the "buffered" Buffer onto the fake `socket`, since at
// this point the HTTP module machinery has been hooked up for
// the user.
s.push(buffered);
s.push(null);
});
return fakeSocket;
}
}
function resume(socket: net.Socket | tls.TLSSocket): void {
socket.resume();
}
function isDefaultPort(port: number, secure: boolean): boolean {
return Boolean((!secure && port === 80) || (secure && port === 443));
}
function isHTTPS(protocol?: string | null): boolean {
return typeof protocol === 'string' ? /^https:?$/i.test(protocol) : false;
}
function omit<T extends object, K extends [...(keyof T)[]]>(
obj: T,
...keys: K
): {
[K2 in Exclude<keyof T, K[number]>]: T[K2];
} {
const ret = {} as {
[K in keyof typeof obj]: (typeof obj)[K];
};
let key: keyof typeof obj;
for (key in obj) {
if (!keys.includes(key)) {
ret[key] = obj[key];
}
}
return ret;
}

39
src/index.ts Normal file
View File

@ -0,0 +1,39 @@
import net from 'net';
import tls from 'tls';
import { Url } from 'url';
import { AgentOptions } from 'agent-base';
import { OutgoingHttpHeaders } from 'http';
import _HttpsProxyAgent from './agent';
function createHttpsProxyAgent(
opts: string | createHttpsProxyAgent.HttpsProxyAgentOptions
): _HttpsProxyAgent {
return new _HttpsProxyAgent(opts);
}
namespace createHttpsProxyAgent {
interface BaseHttpsProxyAgentOptions {
headers?: OutgoingHttpHeaders;
secureProxy?: boolean;
host?: string | null;
path?: string | null;
port?: string | number | null;
}
export interface HttpsProxyAgentOptions
extends AgentOptions,
BaseHttpsProxyAgentOptions,
Partial<
Omit<
Url & net.NetConnectOpts & tls.ConnectionOptions,
keyof BaseHttpsProxyAgentOptions
>
> {}
export type HttpsProxyAgent = _HttpsProxyAgent;
export const HttpsProxyAgent = _HttpsProxyAgent;
createHttpsProxyAgent.prototype = _HttpsProxyAgent.prototype;
}
export = createHttpsProxyAgent;

View File

@ -0,0 +1,82 @@
import createDebug from 'debug';
import { Readable } from 'stream';
const debug = createDebug('https-proxy-agent:parse-proxy-response');
export interface ProxyResponse {
statusCode: number;
buffered: Buffer;
}
export default function parseProxyResponse(
socket: Readable
): Promise<ProxyResponse> {
return new Promise((resolve, reject) => {
// we need to buffer any HTTP traffic that happens with the proxy before we get
// the CONNECT response, so that if the response is anything other than an "200"
// response code, then we can re-play the "data" events on the socket once the
// HTTP parser is hooked up...
let buffersLength = 0;
const buffers: Buffer[] = [];
function read() {
const b = socket.read();
if (b) ondata(b);
else socket.once('readable', read);
}
function cleanup() {
socket.removeListener('end', onend);
socket.removeListener('error', onerror);
socket.removeListener('close', onclose);
socket.removeListener('readable', read);
}
function onclose(err?: Error) {
debug('onclose had error %o', err);
}
function onend() {
debug('onend');
}
function onerror(err: Error) {
cleanup();
debug('onerror %o', err);
reject(err);
}
function ondata(b: Buffer) {
buffers.push(b);
buffersLength += b.length;
const buffered = Buffer.concat(buffers, buffersLength);
const endOfHeaders = buffered.indexOf('\r\n\r\n');
if (endOfHeaders === -1) {
// keep buffering
debug('have not received end of HTTP headers yet...');
read();
return;
}
const firstLine = buffered.toString(
'ascii',
0,
buffered.indexOf('\r\n')
);
const statusCode = +firstLine.split(' ')[1];
debug('got proxy server response: %o', firstLine);
resolve({
statusCode,
buffered
});
}
socket.on('error', onerror);
socket.on('close', onclose);
socket.on('end', onend);
read();
});
}

View File

@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQCzURxIqzer0ACAbX/lHdsn4Gd9PLKrf7EeDYfIdV0HZKPD8WDr
bBx2/fBu0OW2sjnzv/SVZbJ0DAuPE/p0+eT0qb2qC10iz9iTD7ribd7gxhirVb8y
b3fBjXsxc8V8p4Ny1LcvNSqCjwUbJqdRogfoJeTiqPM58z5sNzuv5iq7iwIDAQAB
AoGAPMQy4olrP0UotlzlJ36bowLP70ffgHCwU+/f4NWs5fF78c3du0oSx1w820Dd
Z7E0JF8bgnlJJTxjumPZz0RUCugrEHBKJmzEz3cxF5E3+7NvteZcjKn9D67RrM5x
1/uSZ9cqKE9cYvY4fSuHx18diyZ4axR/wB1Pea2utjjDM+ECQQDb9ZbmmaWMiRpQ
5Up+loxP7BZNPsEVsm+DVJmEFbaFgGfncWBqSIqnPNjMwTwj0OigTwCAEGPkfRVW
T0pbYWCxAkEA0LK7SCTwzyDmhASUalk0x+3uCAA6ryFdwJf/wd8TRAvVOmkTEldX
uJ7ldLvfrONYO3v56uKTU/SoNdZYzKtO+wJAX2KM4ctXYy5BXztPpr2acz4qHa1N
Bh+vBAC34fOYhyQ76r3b1btHhWZ5jbFuZwm9F2erC94Ps5IaoqcX07DSwQJAPKGw
h2U0EPkd/3zVIZCJJQya+vgWFIs9EZcXVtvYXQyTBkVApTN66MhBIYjzkub5205J
bVQmOV37AKklY1DhwQJAA1wos0cYxro02edzatxd0DIR2r4qqOqLkw6BhYHhq6HJ
ZvIcQkHqdSXzdETFc01I1znDGGIrJHcnvKWgBPoEUg==
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIB1TCCAT4CCQDV5mPlzm9+izANBgkqhkiG9w0BAQUFADAvMS0wKwYDVQQDEyQ3
NTI3YmQ3Ny1hYjNlLTQ3NGItYWNlNy1lZWQ2MDUzOTMxZTcwHhcNMTUwNzA2MjI0
NTA3WhcNMjUwNzAzMjI0NTA3WjAvMS0wKwYDVQQDEyQ3NTI3YmQ3Ny1hYjNlLTQ3
NGItYWNlNy1lZWQ2MDUzOTMxZTcwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGB
ALNRHEirN6vQAIBtf+Ud2yfgZ308sqt/sR4Nh8h1XQdko8PxYOtsHHb98G7Q5bay
OfO/9JVlsnQMC48T+nT55PSpvaoLXSLP2JMPuuJt3uDGGKtVvzJvd8GNezFzxXyn
g3LUty81KoKPBRsmp1GiB+gl5OKo8znzPmw3O6/mKruLAgMBAAEwDQYJKoZIhvcN
AQEFBQADgYEACzoHUF8UV2Z6541Q2wKEA0UFUzmUjf/E1XwBO+1P15ZZ64uw34B4
1RwMPtAo9RY/PmICTWtNxWGxkzwb2JtDWtnxVER/lF8k2XcXPE76fxTHJF/BKk9J
QU8OTD1dd9gHCBviQB9TqntRZ5X7axjtuWjb2umY+owBYzAHZkp1HKI=
-----END CERTIFICATE-----

410
test/test.js Normal file
View File

@ -0,0 +1,410 @@
/**
* Module dependencies.
*/
let fs = require('fs');
let url = require('url');
let http = require('http');
let https = require('https');
let assert = require('assert');
let Proxy = require('proxy');
let HttpsProxyAgent = require('../');
describe('HttpsProxyAgent', function() {
let server;
let serverPort;
let sslServer;
let sslServerPort;
let proxy;
let proxyPort;
let sslProxy;
let sslProxyPort;
before(function(done) {
// setup target HTTP server
server = http.createServer();
server.listen(function() {
serverPort = server.address().port;
done();
});
});
before(function(done) {
// setup HTTP proxy server
proxy = Proxy();
proxy.listen(function() {
proxyPort = proxy.address().port;
done();
});
});
before(function(done) {
// setup target HTTPS server
let options = {
key: fs.readFileSync(`${__dirname}/ssl-cert-snakeoil.key`),
cert: fs.readFileSync(`${__dirname}/ssl-cert-snakeoil.pem`)
};
sslServer = https.createServer(options);
sslServer.listen(function() {
sslServerPort = sslServer.address().port;
done();
});
});
before(function(done) {
// setup SSL HTTP proxy server
let options = {
key: fs.readFileSync(`${__dirname}/ssl-cert-snakeoil.key`),
cert: fs.readFileSync(`${__dirname}/ssl-cert-snakeoil.pem`)
};
sslProxy = Proxy(https.createServer(options));
sslProxy.listen(function() {
sslProxyPort = sslProxy.address().port;
done();
});
});
// shut down test HTTP server
after(function(done) {
server.once('close', function() {
done();
});
server.close();
});
after(function(done) {
proxy.once('close', function() {
done();
});
proxy.close();
});
after(function(done) {
sslServer.once('close', function() {
done();
});
sslServer.close();
});
after(function(done) {
sslProxy.once('close', function() {
done();
});
sslProxy.close();
});
describe('constructor', function() {
it('should throw an Error if no "proxy" argument is given', function() {
assert.throws(function() {
new HttpsProxyAgent();
});
});
it('should accept a "string" proxy argument', function() {
let agent = new HttpsProxyAgent(`http://localhost:${proxyPort}`);
assert.equal('localhost', agent.proxy.host);
assert.equal(proxyPort, agent.proxy.port);
});
it('should accept a `url.parse()` result object argument', function() {
let opts = url.parse(`http://localhost:${proxyPort}`);
let agent = new HttpsProxyAgent(opts);
assert.equal('localhost', agent.proxy.host);
assert.equal(proxyPort, agent.proxy.port);
});
describe('secureProxy', function() {
it('should default to `false`', function() {
let agent = new HttpsProxyAgent({ port: proxyPort });
assert.equal(false, agent.secureProxy);
});
it('should be `false` when "http:" protocol is used', function() {
let agent = new HttpsProxyAgent({
port: proxyPort,
protocol: 'http:'
});
assert.equal(false, agent.secureProxy);
});
it('should be `true` when "https:" protocol is used', function() {
let agent = new HttpsProxyAgent({
port: proxyPort,
protocol: 'https:'
});
assert.equal(true, agent.secureProxy);
});
it('should be `true` when "https" protocol is used', function() {
let agent = new HttpsProxyAgent({
port: proxyPort,
protocol: 'https'
});
assert.equal(true, agent.secureProxy);
});
});
});
describe('"http" module', function() {
beforeEach(function() {
delete proxy.authenticate;
});
it('should work over an HTTP proxy', function(done) {
server.once('request', function(req, res) {
res.end(JSON.stringify(req.headers));
});
let proxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
`http://localhost:${proxyPort}`;
let agent = new HttpsProxyAgent(proxy);
let opts = url.parse(`http://localhost:${serverPort}`);
opts.agent = agent;
let req = http.get(opts, function(res) {
let data = '';
res.setEncoding('utf8');
res.on('data', function(b) {
data += b;
});
res.on('end', function() {
data = JSON.parse(data);
assert.equal(`localhost:${serverPort}`, data.host);
done();
});
});
req.once('error', done);
});
it('should work over an HTTPS proxy', function(done) {
server.once('request', function(req, res) {
res.end(JSON.stringify(req.headers));
});
let proxy =
process.env.HTTPS_PROXY ||
process.env.https_proxy ||
`https://localhost:${sslProxyPort}`;
proxy = url.parse(proxy);
proxy.rejectUnauthorized = false;
let agent = new HttpsProxyAgent(proxy);
let opts = url.parse(`http://localhost:${serverPort}`);
opts.agent = agent;
http.get(opts, function(res) {
let data = '';
res.setEncoding('utf8');
res.on('data', function(b) {
data += b;
});
res.on('end', function() {
data = JSON.parse(data);
assert.equal(`localhost:${serverPort}`, data.host);
done();
});
});
});
it('should receive the 407 authorization code on the `http.ClientResponse`', function(done) {
// set a proxy authentication function for this test
proxy.authenticate = function(req, fn) {
// reject all requests
fn(null, false);
};
let proxyUri =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
`http://localhost:${proxyPort}`;
let agent = new HttpsProxyAgent(proxyUri);
let opts = {};
// `host` and `port` don't really matter since the proxy will reject anyways
opts.host = 'localhost';
opts.port = 80;
opts.agent = agent;
let req = http.get(opts, function(res) {
assert.equal(407, res.statusCode);
assert('proxy-authenticate' in res.headers);
done();
});
});
it('should not error if the proxy responds with 407 and the request is aborted', function(done) {
proxy.authenticate = function(req, fn) {
fn(null, false);
};
const proxyUri =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
`http://localhost:${proxyPort}`;
const req = http.get(
{
agent: new HttpsProxyAgent(proxyUri)
},
function(res) {
assert.equal(407, res.statusCode);
req.abort();
}
);
req.on('abort', done);
});
it('should emit an "end" event on the `http.IncomingMessage` if the proxy responds with non-200 status code', function(done) {
proxy.authenticate = function(req, fn) {
fn(null, false);
};
const proxyUri =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
`http://localhost:${proxyPort}`;
const req = http.get(
{
agent: new HttpsProxyAgent(proxyUri)
},
function(res) {
assert.equal(407, res.statusCode);
res.resume();
res.on('end', done);
}
);
});
it('should emit an "error" event on the `http.ClientRequest` if the proxy does not exist', function(done) {
// port 4 is a reserved, but "unassigned" port
let proxyUri = 'http://localhost:4';
let agent = new HttpsProxyAgent(proxyUri);
let opts = url.parse('http://nodejs.org');
opts.agent = agent;
let req = http.get(opts);
req.once('error', function(err) {
assert.equal('ECONNREFUSED', err.code);
req.abort();
done();
});
});
it('should allow custom proxy "headers"', function(done) {
server.once('connect', function(req, socket, head) {
assert.equal('CONNECT', req.method);
assert.equal('bar', req.headers.foo);
socket.destroy();
done();
});
let uri = `http://localhost:${serverPort}`;
let proxyOpts = url.parse(uri);
proxyOpts.headers = {
Foo: 'bar'
};
let agent = new HttpsProxyAgent(proxyOpts);
let opts = {};
// `host` and `port` don't really matter since the proxy will reject anyways
opts.host = 'localhost';
opts.port = 80;
opts.agent = agent;
http.get(opts);
});
});
describe('"https" module', function() {
it('should work over an HTTP proxy', function(done) {
sslServer.once('request', function(req, res) {
res.end(JSON.stringify(req.headers));
});
let proxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
`http://localhost:${proxyPort}`;
let agent = new HttpsProxyAgent(proxy);
let opts = url.parse(`https://localhost:${sslServerPort}`);
opts.rejectUnauthorized = false;
opts.agent = agent;
https.get(opts, function(res) {
let data = '';
res.setEncoding('utf8');
res.on('data', function(b) {
data += b;
});
res.on('end', function() {
data = JSON.parse(data);
assert.equal(`localhost:${sslServerPort}`, data.host);
done();
});
});
});
it('should work over an HTTPS proxy', function(done) {
sslServer.once('request', function(req, res) {
res.end(JSON.stringify(req.headers));
});
let proxy =
process.env.HTTPS_PROXY ||
process.env.https_proxy ||
`https://localhost:${sslProxyPort}`;
proxy = url.parse(proxy);
proxy.rejectUnauthorized = false;
let agent = new HttpsProxyAgent(proxy);
let opts = url.parse(`https://localhost:${sslServerPort}`);
opts.agent = agent;
opts.rejectUnauthorized = false;
https.get(opts, function(res) {
let data = '';
res.setEncoding('utf8');
res.on('data', function(b) {
data += b;
});
res.on('end', function() {
data = JSON.parse(data);
assert.equal(`localhost:${sslServerPort}`, data.host);
done();
});
});
});
it('should not send a port number for the default port', function(done) {
sslServer.once('request', function(req, res) {
res.end(JSON.stringify(req.headers));
});
let proxy =
process.env.HTTPS_PROXY ||
process.env.https_proxy ||
`https://localhost:${sslProxyPort}`;
proxy = url.parse(proxy);
proxy.rejectUnauthorized = false;
let agent = new HttpsProxyAgent(proxy);
agent.defaultPort = sslServerPort;
let opts = url.parse(`https://localhost:${sslServerPort}`);
opts.agent = agent;
opts.rejectUnauthorized = false;
https.get(opts, function(res) {
let data = '';
res.setEncoding('utf8');
res.on('data', function(b) {
data += b;
});
res.on('end', function() {
data = JSON.parse(data);
assert.equal('localhost', data.host);
done();
});
});
});
});
});

View File

@ -0,0 +1,37 @@
root = true
[*]
indent_style = tab
indent_size = 4
tab_width = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{*.json,*.json.example,*.gyp,*.yml,*.yaml,*.workflow}]
indent_style = space
indent_size = 2
[{*.py,*.asm}]
indent_style = space
[*.py]
indent_size = 4
[*.asm]
indent_size = 8
[*.md]
trim_trailing_whitespace = false
# Ideal settings - some plugins might support these.
[*.js]
quote_type = single
[{*.c,*.cc,*.h,*.hh,*.cpp,*.hpp,*.m,*.mm,*.mpp,*.js,*.java,*.go,*.rs,*.php,*.ng,*.jsx,*.ts,*.d,*.cs,*.swift}]
curly_bracket_next_line = false
spaces_around_operators = true
spaces_around_brackets = outside
# close enough to 1TB
indent_brace_style = K&R

View File

@ -0,0 +1,40 @@
name: Node CI
on: [push]
jobs:
build:
name: Test Node.js ${{ matrix.node-version }} on ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: [10.x, 12.x, 14.x, 16.x]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Print Node.js Version
run: node --version
- name: Install Dependencies
run: npm install
env:
CI: true
- name: Run "build" step
run: npm run build --if-present
env:
CI: true
- name: Run tests
run: npm test
env:
CI: true

6
tootallnate-once/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/yarn.lock
/node_modules
/src/?.?s
/dist
/.vscode
/*.tgz

21
tootallnate-once/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Nathan Rajlich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,93 @@
# @tootallnate/once
### Creates a Promise that waits for a single event
## Installation
Install with `npm`:
```bash
$ npm install @tootallnate/once
```
## API
### once(emitter: EventEmitter, name: string, opts?: OnceOptions): Promise&lt;[...Args]&gt;
Creates a Promise that waits for event `name` to occur on `emitter`, and resolves
the promise with an array of the values provided to the event handler. If an
`error` event occurs before the event specified by `name`, then the Promise is
rejected with the error argument.
```typescript
import once from '@tootallnate/once';
import { EventEmitter } from 'events';
const emitter = new EventEmitter();
setTimeout(() => {
emitter.emit('foo', 'bar');
}, 100);
const [result] = await once(emitter, 'foo');
console.log({ result });
// { result: 'bar' }
```
#### Promise Strong Typing
The main feature that this module provides over other "once" implementations is that
the Promise that is returned is _**strongly typed**_ based on the type of `emitter`
and the `name` of the event. Some examples are shown below.
_The process "exit" event contains a single number for exit code:_
```typescript
const [code] = await once(process, 'exit');
// ^ number
```
_A child process "exit" event contains either an exit code or a signal:_
```typescript
const child = spawn('echo', []);
const [code, signal] = await once(child, 'exit');
// ^ number | null
// ^ string | null
```
_A forked child process "message" event is type `any`, so you can cast the Promise directly:_
```typescript
const child = fork('file.js');
// With `await`
const [message, _]: [WorkerPayload, unknown] = await once(child, 'message');
// With Promise
const messagePromise: Promise<[WorkerPayload, unknown]> = once(child, 'message');
// Better yet would be to leave it as `any`, and validate the payload
// at runtime with i.e. `ajv` + `json-schema-to-typescript`
```
_If the TypeScript definition does not contain an overload for the specified event name, then the Promise will have type `unknown[]` and your code will need to narrow the result manually:_
```typescript
interface CustomEmitter extends EventEmitter {
on(name: 'foo', listener: (a: string, b: number) => void): this;
}
const emitter: CustomEmitter = new EventEmitter();
// "foo" event is a defined overload, so it's properly typed
const fooPromise = once(emitter, 'foo');
// ^ Promise<[a: string, b: number]>
// "bar" event in not a defined overload, so it gets `unknown[]`
const barPromise = once(emitter, 'bar');
// ^ Promise<unknown[]>
```
### OnceOptions
- `signal` - `AbortSignal` instance to unbind event handlers before the Promise has been fulfilled.

View File

@ -0,0 +1,54 @@
{
"name": "@tootallnate/once",
"version": "3.0.0",
"description": "Creates a Promise that waits for a single event",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist",
"src"
],
"scripts": {
"prebuild": "rimraf dist",
"build": "tsc",
"test": "jest",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git://github.com/TooTallNate/once.git"
},
"keywords": [],
"author": "Nathan Rajlich <nathan@tootallnate.net> (http://n8.io/)",
"license": "MIT",
"bugs": {
"url": "https://github.com/TooTallNate/once/issues"
},
"devDependencies": {
"@types/jest": "^27.0.2",
"@types/node": "^12.12.11",
"abort-controller": "^3.0.0",
"jest": "^27.2.1",
"rimraf": "^3.0.0",
"ts-jest": "^27.0.5",
"typescript": "^4.4.3"
},
"engines": {
"node": ">= 10"
},
"jest": {
"preset": "ts-jest",
"globals": {
"ts-jest": {
"diagnostics": false,
"isolatedModules": true
}
},
"verbose": false,
"testEnvironment": "node",
"testMatch": [
"<rootDir>/test/**/*.test.ts"
]
}
}

View File

@ -0,0 +1,28 @@
/**
* This script generated a TypeScript file that defines
* a type based on this StackOverflow answer:
* https://stackoverflow.com/a/52761156/376773
*/
process.stdout.write('export type OverloadedParameters<T> = \n');
const overloads = parseInt(process.argv[2], 10) || 5;
for (let i = overloads; i > 0; i--) {
process.stdout.write(`\tT extends { `);
for (let j = 1; j <= i; j++) {
process.stdout.write(
`(...args: infer A${j}): any; `
);
}
process.stdout.write(`} ? `);
for (let j = 1; j <= i; j++) {
process.stdout.write(`A${j} `);
if (j < i) {
process.stdout.write(`| `);
}
}
process.stdout.write(`:\n`);
}
process.stdout.write(`\tany;\n`);

View File

@ -0,0 +1,34 @@
import { EventEmitter } from 'events';
import { EventNames, EventListenerParameters, AbortSignal } from './types.js';
export interface OnceOptions {
signal?: AbortSignal;
}
export default function once<
Emitter extends EventEmitter,
Event extends EventNames<Emitter>
>(
emitter: Emitter,
name: Event,
{ signal }: OnceOptions = {}
): Promise<EventListenerParameters<Emitter, Event>> {
return new Promise((resolve, reject) => {
function cleanup() {
signal?.removeEventListener('abort', cleanup);
emitter.removeListener(name, onEvent);
emitter.removeListener('error', onError);
}
function onEvent(...args: EventListenerParameters<Emitter, Event>) {
cleanup();
resolve(args);
}
function onError(err: Error) {
cleanup();
reject(err);
}
signal?.addEventListener('abort', cleanup);
emitter.on(name, onEvent);
emitter.on('error', onError);
});
}

View File

@ -0,0 +1,22 @@
export type OverloadedParameters<T> =
T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; (...args: infer A11): any; (...args: infer A12): any; (...args: infer A13): any; (...args: infer A14): any; (...args: infer A15): any; (...args: infer A16): any; (...args: infer A17): any; (...args: infer A18): any; (...args: infer A19): any; (...args: infer A20): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 | A12 | A13 | A14 | A15 | A16 | A17 | A18 | A19 | A20 :
T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; (...args: infer A11): any; (...args: infer A12): any; (...args: infer A13): any; (...args: infer A14): any; (...args: infer A15): any; (...args: infer A16): any; (...args: infer A17): any; (...args: infer A18): any; (...args: infer A19): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 | A12 | A13 | A14 | A15 | A16 | A17 | A18 | A19 :
T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; (...args: infer A11): any; (...args: infer A12): any; (...args: infer A13): any; (...args: infer A14): any; (...args: infer A15): any; (...args: infer A16): any; (...args: infer A17): any; (...args: infer A18): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 | A12 | A13 | A14 | A15 | A16 | A17 | A18 :
T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; (...args: infer A11): any; (...args: infer A12): any; (...args: infer A13): any; (...args: infer A14): any; (...args: infer A15): any; (...args: infer A16): any; (...args: infer A17): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 | A12 | A13 | A14 | A15 | A16 | A17 :
T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; (...args: infer A11): any; (...args: infer A12): any; (...args: infer A13): any; (...args: infer A14): any; (...args: infer A15): any; (...args: infer A16): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 | A12 | A13 | A14 | A15 | A16 :
T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; (...args: infer A11): any; (...args: infer A12): any; (...args: infer A13): any; (...args: infer A14): any; (...args: infer A15): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 | A12 | A13 | A14 | A15 :
T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; (...args: infer A11): any; (...args: infer A12): any; (...args: infer A13): any; (...args: infer A14): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 | A12 | A13 | A14 :
T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; (...args: infer A11): any; (...args: infer A12): any; (...args: infer A13): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 | A12 | A13 :
T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; (...args: infer A11): any; (...args: infer A12): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 | A12 :
T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; (...args: infer A11): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 :
T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 :
T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 :
T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 :
T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 :
T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; } ? A1 | A2 | A3 | A4 | A5 | A6 :
T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; } ? A1 | A2 | A3 | A4 | A5 :
T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; } ? A1 | A2 | A3 | A4 :
T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; } ? A1 | A2 | A3 :
T extends { (...args: infer A1): any; (...args: infer A2): any; } ? A1 | A2 :
T extends { (...args: infer A1): any; } ? A1 :
any;

View File

@ -0,0 +1,38 @@
import { EventEmitter } from 'events';
import { OverloadedParameters } from './overloaded-parameters';
export type FirstParameter<T> = T extends [infer R, ...any[]] ? R : never;
export type EventListener<F, T extends string | symbol> = F extends [
T,
infer R,
...any[]
]
? R
: never;
export type EventParameters<
Emitter extends EventEmitter
> = OverloadedParameters<Emitter['on']>;
export type EventNames<Emitter extends EventEmitter> = FirstParameter<
EventParameters<Emitter>
>;
export type EventListenerParameters<
Emitter extends EventEmitter,
Event extends EventNames<Emitter>
> = WithDefault<
Parameters<EventListener<EventParameters<Emitter>, Event>>,
unknown[]
>;
export type WithDefault<T, D> = [T] extends [never] ? D : T;
export interface AbortSignal {
addEventListener: (name: string, listener: (...args: any[]) => any) => void;
removeEventListener: (
name: string,
listener: (...args: any[]) => any
) => void;
}

View File

@ -0,0 +1,117 @@
import { spawn } from 'child_process';
import { EventEmitter } from 'events';
import { AbortController } from 'abort-controller';
import once from '../src';
describe('once()', () => {
it('should work with vanilla EventEmitter', async () => {
const emitter = new EventEmitter();
const promise = once(emitter, 'foo');
emitter.emit('foo', 'bar');
const [foo] = await promise;
expect(foo).toEqual('bar');
});
it('should work with vanilla EventEmitter - nextTick', async () => {
const ee = new EventEmitter();
process.nextTick(() => {
ee.emit('myevent', 42);
});
const [value] = await once(ee, 'myevent');
expect(value).toEqual(42);
});
it('should reject Promise upon "error" event', async () => {
let err: Error | null = null;
const emitter = new EventEmitter();
const promise = once(emitter, 'foo').catch((_err) => {
err = _err;
});
emitter.emit('error', new Error('test'));
await promise;
expect(err!.message).toEqual('test');
});
it('should reject Promise upon "error" event - nextTick', async () => {
const ee = new EventEmitter();
const err = new Error('kaboom');
process.nextTick(() => {
ee.emit('error', err);
});
try {
await once(ee, 'myevent');
throw new Error('Should not happen');
} catch (err: any) {
expect(err.message).toEqual('kaboom');
}
});
it('should work with interface extending EventEmitter with overload', async () => {
interface TestEmitter extends EventEmitter {
on(name: 'foo', listener: (a: string, b: number) => void): this;
}
const emitter: TestEmitter = new EventEmitter();
const promise = once(emitter, 'foo');
emitter.emit('foo', 'bar', 4);
const [a, b] = await promise;
expect(a).toEqual('bar');
expect(b).toEqual(4);
});
it('should allow casting from an `any` param', async () => {
interface TestEmitter extends EventEmitter {
on(name: 'foo', listener: (a: any, b: number) => void): this;
}
const emitter: TestEmitter = new EventEmitter();
const promise: Promise<[string, unknown]> = once(emitter, 'foo');
emitter.emit('foo', 'bar', 4);
const [a] = await promise;
// TypeScript will fail if `a` is not `string` type
expect(a.toUpperCase()).toEqual('BAR');
});
it('should work with ChildProcess "exit" event', async () => {
const child = spawn('echo', ['hi']);
const [code, signal] = await once(child, 'exit');
expect(code).toEqual(0);
expect(signal).toBeNull();
});
it('should be abortable with `AbortController`', async () => {
let wasResolved = false;
const emitter = new EventEmitter();
const controller = new AbortController();
const { signal } = controller;
const onResolve = () => {
wasResolved = true;
};
once(emitter, 'foo', { signal }).then(onResolve, onResolve);
// First time without `abort()`, so it will be fulfilled
emitter.emit('foo');
// Promise is fulfilled on next tick, so wait a bit
await new Promise((r) => process.nextTick(r));
expect(wasResolved).toEqual(true);
// Reset
wasResolved = false;
once(emitter, 'foo', { signal }).then(onResolve, onResolve);
// This time abort
controller.abort();
emitter.emit('foo');
// Promise is fulfilled on next tick, so wait a bit
await new Promise((r) => process.nextTick(r));
expect(wasResolved).toEqual(false);
});
});

View File

@ -0,0 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["**/*.test.ts"]
}

View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"module": "ES2020",
"target": "ES2019",
"esModuleInterop": true,
"lib": ["ES2019"],
"outDir": "dist",
"sourceMap": true,
"declaration": true,
"skipLibCheck": true,
"moduleResolution": "node",
"typeRoots": [
"./@types",
"./node_modules/@types"
]
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

18
tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"strict": true,
"module": "CommonJS",
"target": "es2015",
"esModuleInterop": true,
"lib": ["esnext"],
"outDir": "dist",
"sourceMap": true,
"declaration": true,
"typeRoots": [
"./@types",
"./node_modules/@types"
]
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}