Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix: support Node.js 20 #16

Merged
merged 14 commits into from
Oct 21, 2023
11 changes: 5 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
name: ci

on: [push]

on: [push, pull_request]
permissions:
contents: read
jobs:
build:
name: Test on node ${{ matrix.node_version }}
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
strategy:
matrix:
node_version: [ 12, 14, 16 ]
node_version: [ 14, 16, 18, 20 ]

steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node_version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node_version }}

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ This library block the request to these IP addresses by default.

So, This library block the request to non-`unicast` IP addresses.

:warning: Node.js's built-in `fetch` does not support `http.Agent`.

- [Passing a custom agent to `fetch()` · Issue #1489 · nodejs/undici](https://github.com/nodejs/undici/issues/1489)

## Support `http.Agent` libraries

This library provides Node.js's [http.Agent](https://nodejs.org/api/http.html#http_class_http_agent) implementation.
Expand Down
214 changes: 208 additions & 6 deletions src/request-filtering-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { TcpNetConnectOpts } from "net";
import * as http from "http";
import * as https from "https";
import ipaddr from "ipaddr.js";
import { Socket } from "net";

export interface RequestFilteringAgentOptions {
// Allow to connect private IP address
Expand All @@ -19,7 +20,7 @@ export interface RequestFilteringAgentOptions {
// Default: false
allowMetaIPAddress?: boolean;
// Allow address list
// This values are preferred than denyAddressList
// These values are preferred than denyAddressList
// Default: []
allowIPAddressList?: string[];
// Deny address list
Expand Down Expand Up @@ -74,7 +75,7 @@ const validateIPAddress = (
);
}
} catch (error) {
return error as Error; // if can not parsed IP address, throw error
return error as Error; // if can not parse IP address, throw error
}
return;
};
Expand Down Expand Up @@ -167,19 +168,220 @@ export function applyRequestFilter<T extends http.Agent | https.Agent>(
* A subclass of http.Agent with request filtering
*/
export class RequestFilteringHttpAgent extends http.Agent {
private requestFilterOptions: Required<RequestFilteringAgentOptions>;

constructor(options?: http.AgentOptions & RequestFilteringAgentOptions) {
super(options);
applyRequestFilter(this, options);
this.requestFilterOptions = {
allowPrivateIPAddress:
options && options.allowPrivateIPAddress !== undefined ? options.allowPrivateIPAddress : false,
allowMetaIPAddress:
options && options.allowMetaIPAddress !== undefined ? options.allowMetaIPAddress : false,
allowIPAddressList: options && options.allowIPAddressList ? options.allowIPAddressList : [],
denyIPAddressList: options && options.denyIPAddressList ? options.denyIPAddressList : [],
stopPortScanningByUrlRedirection:
options && options.stopPortScanningByUrlRedirection !== undefined
? options.stopPortScanningByUrlRedirection
: false
};
}

createConnection(options: TcpNetConnectOpts, connectionListener?: (error: Error | null, socket: Socket) => void) {
console.log("createConnectio !!!!n");
let validationError: Error | null | undefined = null;
if (this.requestFilterOptions.stopPortScanningByUrlRedirection) {
// Prevents malicious user from identifying which ports are open
const { host, family } = options;
if (host && net.isIP(host)) {
const addr = ipaddr.parse(host);
const range = addr.range();
if (range !== "unicast") {
throw new Error(
`DNS lookup ${host}(family:${family}, host:${host}) is not allowed. Because, It is private IP address.`
);
}
}
}
// console.log("createConnection", options);
const { host } = options;
if (host) {
// Direct ip address request without dns-lookup
// Example: http://127.0.0.1
// https://nodejs.org/api/net.html#net_socket_connect_options_connectlistener
validationError = validateIPAddress({ address: host }, this.requestFilterOptions);
}
// https://nodejs.org/api/net.html#net_socket_connect_options_connectlistener
let isSocketEnding = false;
// @ts-expect-error - @types/node does not defined createConnection
const socket: Socket = super.createConnection(options, connectionListener);
const onReady = () => {
if (validationError) {
if (isSocketEnding) {
return;
}
if (socket.destroyed) {
return;
}
isSocketEnding = true;
socket.removeListener("ready", onReady);
socket.end(() => {
console.log("end 1");
if (validationError) {
if (socket.destroyed) {
return;
}
socket.destroy(validationError);
}
});
}
};
socket.addListener("ready", onReady);
// Request with domain name
// Example: http://127.0.0.1.nip.io/
const weakSet = new WeakSet<Socket>();
const onLookup = (err: Error, address: string, family: string | number, host: string): void => {
if (err) {
return;
}
console.log({
family,
host
});
const error = validateIPAddress({ address, family, host }, this.requestFilterOptions);
console.log("validation 2 ", error);
if (error) {
if (weakSet.has(socket)) {
return;
}
weakSet.add(socket);
socket.removeListener("lookup", onLookup);
if (socket.destroyed) {
return;
}
socket.end(() => {
console.log("end 2");
if (socket.destroyed) {
return;
}
console.log("end 3 ", socket.destroyed);
console.log("end errr", validationError?.message);
socket.destroy(error);
});
}
};
socket.addListener("lookup", onLookup);
return socket;
}
}

/**
* A subclass of https.Agent with request filtering
*/
export class RequestFilteringHttpsAgent extends https.Agent {
private requestFilterOptions: Required<RequestFilteringAgentOptions>;

constructor(options?: https.AgentOptions & RequestFilteringAgentOptions) {
super(options);
applyRequestFilter(this, options);
this.requestFilterOptions = {
allowPrivateIPAddress:
options && options.allowPrivateIPAddress !== undefined ? options.allowPrivateIPAddress : false,
allowMetaIPAddress:
options && options.allowMetaIPAddress !== undefined ? options.allowMetaIPAddress : false,
allowIPAddressList: options && options.allowIPAddressList ? options.allowIPAddressList : [],
denyIPAddressList: options && options.denyIPAddressList ? options.denyIPAddressList : [],
stopPortScanningByUrlRedirection:
options && options.stopPortScanningByUrlRedirection !== undefined
? options.stopPortScanningByUrlRedirection
: false
};
}

createConnection(options: TcpNetConnectOpts, connectionListener?: (error: Error | null, socket: Socket) => void) {
console.log("createConnectio !!!!n");
let validationError: Error | null | undefined = null;
if (this.requestFilterOptions.stopPortScanningByUrlRedirection) {
// Prevents malicious user from identifying which ports are open
const { host, family } = options;
if (host && net.isIP(host)) {
const addr = ipaddr.parse(host);
const range = addr.range();
if (range !== "unicast") {
throw new Error(
`DNS lookup ${host}(family:${family}, host:${host}) is not allowed. Because, It is private IP address.`
);
}
}
}
// console.log("createConnection", options);
const { host } = options;
if (host) {
// Direct ip address request without dns-lookup
// Example: http://127.0.0.1
// https://nodejs.org/api/net.html#net_socket_connect_options_connectlistener
validationError = validateIPAddress({ address: host }, this.requestFilterOptions);
}
// https://nodejs.org/api/net.html#net_socket_connect_options_connectlistener
let isSocketEnding = false;
// @ts-expect-error - @types/node does not defined createConnection
const socket: Socket = super.createConnection(options, connectionListener);
const onReady = () => {
if (validationError) {
if (isSocketEnding) {
return;
}
if (socket.destroyed) {
return;
}
isSocketEnding = true;
socket.removeListener("ready", onReady);
socket.end(() => {
console.log("end 1");
if (validationError) {
if (socket.destroyed) {
return;
}
socket.destroy(validationError);
}
});
}
};
socket.addListener("ready", onReady);
// Request with domain name
// Example: http://127.0.0.1.nip.io/
let isEndong: boolean = false;
const weakSet = new WeakSet<Socket>();
const onLookup = (err: Error, address: string, family: string | number, host: string): void => {
if (err) {
return;
}
const error = validateIPAddress({ address, family, host }, this.requestFilterOptions);
console.log("validation 2 ", error);
if (error) {
if (weakSet.has(socket)) {
return;
}
if (isEndong) {
return;
}
weakSet.add(socket);
isEndong = true;
socket.removeListener("lookup", onLookup);
if (socket.destroyed) {
return;
}
socket.end(() => {
console.log("end 2");
if (socket.destroyed) {
return;
}
console.log("end 3 ", socket.destroyed);
console.log("end errr", validationError?.message);
socket.destroy(error);
Copy link
Owner Author

@azu azu Oct 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid Node.js ERR_INTERNAL_ASSERTION: end -> destory.

Just call destory and Node.js throws ERR_INTERNAL_ASSERTION like #15

});
}
};
socket.addListener("lookup", onLookup);
return socket;
}
}

Expand All @@ -191,8 +393,8 @@ export const globalHttpsAgent = new RequestFilteringHttpsAgent();
* @param url
*/
export const useAgent = (url: string, options?: https.AgentOptions & RequestFilteringAgentOptions) => {
if(!options) {
return url.startsWith("https") ? globalHttpsAgent : globalHttpAgent;
if (!options) {
return url.startsWith("https") ? globalHttpsAgent : globalHttpAgent;
}
return url.startsWith("https") ? new RequestFilteringHttpsAgent(options) : new RequestFilteringHttpAgent(options);
};
11 changes: 9 additions & 2 deletions test/request-filtering-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,18 +86,22 @@ describe("request-filtering-agent", function () {
agent,
timeout: 2000
});
console.log("???ss");
throw new ReferenceError("SHOULD NOT BE CALLED:" + ipAddress);
} catch (error) {
console.log("??" + error);
if (error instanceof ReferenceError) {
assert.fail(error);
}
} finally {
console.log("??");
}
}
});

it("should allow http://127.0.0.1, but other private ip is disallowed", async () => {
const agent = new RequestFilteringHttpAgent({
allowIPAddressList: ["127.0.0.1"],
allowIPAddressList: ["127.0.0.1", "::1"],
Copy link
Owner Author

@azu azu Oct 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

localhost will be ::1 in some env.

allowPrivateIPAddress: false
});
const privateIPs = [`http://127.0.0.1:${TEST_PORT}`, `http://localhost:${TEST_PORT}`];
Expand Down Expand Up @@ -204,7 +208,10 @@ describe("request-filtering-agent", function () {
if (error instanceof ReferenceError) {
assert.fail(error);
}
assert.ok(/It is private IP address/i.test(error.message), `Failed at ${ipAddress}, error: ${error}`);
assert.ok(
/Because, It is private IP address./i.test(error.message),
`Failed at ${ipAddress}, error: ${error}`
);
}
}
});
Expand Down
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"esModuleInterop": true,
"newLine": "LF",
"outDir": "./lib/",
"target": "es5",
"target": "ES2020",
"sourceMap": true,
"declaration": true,
"jsx": "preserve",
Expand All @@ -33,4 +33,4 @@
".git",
"node_modules"
]
}
}
Loading