-
-
Notifications
You must be signed in to change notification settings - Fork 7
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
Changes from 12 commits
f636e6c
6fad4f0
efa6f8e
5b951e9
67128bf
0c53b42
3dfbb3d
6ae95db
a0a348e
55cd37d
622720f
1088aff
9d6e555
0e0e01c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,34 +3,37 @@ 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 | ||
// Allow to connect private IP address if allowPrivateIPAddress is true | ||
// This includes Private IP addresses and Reserved IP addresses. | ||
// https://en.wikipedia.org/wiki/Private_network | ||
// https://en.wikipedia.org/wiki/Reserved_IP_addresses | ||
// Example, http://127.0.0.1/, http://localhost/, https://169.254.169.254/ | ||
// Default: false | ||
allowPrivateIPAddress?: boolean; | ||
// Allow to connect meta address 0.0.0.0 | ||
// 0.0.0.0 (IPv4) and :: (IPv6) a meta address that routing another address | ||
// Allow to connect meta address 0.0.0.0 if allowPrivateIPAddress is true | ||
// 0.0.0.0 (IPv4) and :: (IPv6) a meta/unspecified address that routing another address | ||
// https://en.wikipedia.org/wiki/Reserved_IP_addresses | ||
// https://tools.ietf.org/html/rfc6890 | ||
// 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 | ||
// Default: [] | ||
denyIPAddressList?: string[]; | ||
// prevent url redirection attack | ||
// connection not made to private IP adresses where the port is closed | ||
// Default: false | ||
stopPortScanningByUrlRedirection?: boolean; | ||
} | ||
|
||
export const DefaultRequestFilteringAgentOptions: Required<RequestFilteringAgentOptions> = { | ||
allowPrivateIPAddress: false, | ||
allowMetaIPAddress: false, | ||
allowIPAddressList: [], | ||
denyIPAddressList: [] | ||
}; | ||
/** | ||
* validate the address that is matched the validation options | ||
* @param address ip address | ||
|
@@ -41,7 +44,7 @@ export interface RequestFilteringAgentOptions { | |
const validateIPAddress = ( | ||
{ address, host, family }: { address: string; host?: string; family?: string | number }, | ||
options: Required<RequestFilteringAgentOptions> | ||
): null | undefined | Error => { | ||
): undefined | Error => { | ||
// if it is not IP address, skip it | ||
if (net.isIP(address) === 0) { | ||
return; | ||
|
@@ -74,112 +77,126 @@ 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; | ||
}; | ||
|
||
// dns lookup -> check | ||
const addDropFilterSocket = (options: Required<RequestFilteringAgentOptions>, socket: net.Socket) => { | ||
socket.addListener("lookup", (err, address, family, host) => { | ||
if (err) { | ||
return; | ||
} | ||
const error = validateIPAddress({ address, family, host }, options); | ||
if (error) { | ||
socket.destroy(error); | ||
} | ||
}); | ||
}; | ||
|
||
// public | ||
// prevent twice apply | ||
const appliedAgentSet = new WeakSet<http.Agent | https.Agent>(); | ||
|
||
/** | ||
* Apply request filter to http(s).Agent instance | ||
* A subclass of http.Agent with request filtering | ||
*/ | ||
export function applyRequestFilter<T extends http.Agent | https.Agent>( | ||
agent: T, | ||
options?: RequestFilteringAgentOptions | ||
): T { | ||
if (appliedAgentSet.has(agent)) { | ||
return agent; | ||
export class RequestFilteringHttpAgent extends http.Agent { | ||
private requestFilterOptions: Required<RequestFilteringAgentOptions>; | ||
|
||
constructor(options?: http.AgentOptions & RequestFilteringAgentOptions) { | ||
super(options); | ||
this.requestFilterOptions = { | ||
allowPrivateIPAddress: | ||
options && options.allowPrivateIPAddress !== undefined | ||
? options.allowPrivateIPAddress | ||
: DefaultRequestFilteringAgentOptions.allowPrivateIPAddress, | ||
allowMetaIPAddress: | ||
options && options.allowMetaIPAddress !== undefined | ||
? options.allowMetaIPAddress | ||
: DefaultRequestFilteringAgentOptions.allowMetaIPAddress, | ||
allowIPAddressList: | ||
options && options.allowIPAddressList | ||
? options.allowIPAddressList | ||
: DefaultRequestFilteringAgentOptions.allowIPAddressList, | ||
denyIPAddressList: | ||
options && options.denyIPAddressList | ||
? options.denyIPAddressList | ||
: DefaultRequestFilteringAgentOptions.denyIPAddressList | ||
}; | ||
} | ||
appliedAgentSet.add(agent); | ||
const requestFilterOptions: Required<RequestFilteringAgentOptions> = { | ||
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 | ||
}; | ||
|
||
// override http.Agent#createConnection | ||
// https://nodejs.org/api/http.html#http_agent_createconnection_options_callback | ||
// https://nodejs.org/api/net.html#net_net_createconnection_options_connectlistener | ||
// @ts-expect-error - @types/node does not defined createConnection | ||
const createConnection = agent.createConnection; | ||
// @ts-expect-error - @types/node does not defined createConnection | ||
agent.createConnection = (options: TcpNetConnectOpts, connectionListener?: (error?: Error) => void) => { | ||
if (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.` | ||
); | ||
} | ||
} | ||
} | ||
|
||
const socket = createConnection.call(agent, options, () => { | ||
createConnection(options: TcpNetConnectOpts, connectionListener?: (error: Error | null, socket: Socket) => void) { | ||
const { host } = options; | ||
if (host !== undefined) { | ||
// Direct ip address request without dns-lookup | ||
// Example: http://127.0.0.1 | ||
// https://nodejs.org/api/net.html#net_socket_connect_options_connectlistener | ||
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 | ||
const error = validateIPAddress({ address: host }, requestFilterOptions); | ||
if (error) { | ||
socket.destroy(error); | ||
} | ||
const validationError = validateIPAddress({ address: host }, this.requestFilterOptions); | ||
if (validationError) { | ||
throw validationError; | ||
} | ||
if (typeof connectionListener === "function") { | ||
connectionListener(); | ||
} | ||
}); | ||
} | ||
// https://nodejs.org/api/net.html#net_socket_connect_options_connectlistener | ||
// @ts-expect-error - @types/node does not defined createConnection | ||
const socket: Socket = super.createConnection(options, connectionListener); | ||
// Request with domain name | ||
// Example: http://127.0.0.1.nip.io/ | ||
addDropFilterSocket(requestFilterOptions, socket); | ||
const onLookup = (err: Error, address: string, family: string | number, host: string): void => { | ||
if (err) { | ||
return; | ||
} | ||
const validationError = validateIPAddress({ address, family, host }, this.requestFilterOptions); | ||
if (validationError) { | ||
socket.removeListener("lookup", onLookup); | ||
socket.end(() => { | ||
socket.destroy(validationError); | ||
}); | ||
} | ||
}; | ||
socket.addListener("lookup", onLookup); | ||
return socket; | ||
}; | ||
return agent; | ||
} | ||
|
||
/** | ||
* A subclass of http.Agent with request filtering | ||
*/ | ||
export class RequestFilteringHttpAgent extends http.Agent { | ||
constructor(options?: http.AgentOptions & RequestFilteringAgentOptions) { | ||
super(options); | ||
applyRequestFilter(this, options); | ||
} | ||
} | ||
|
||
/** | ||
* 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 : [] | ||
}; | ||
} | ||
|
||
// override http.Agent#createConnection | ||
// https://nodejs.org/api/http.html#http_agent_createconnection_options_callback | ||
// https://nodejs.org/api/net.html#net_net_createconnection_options_connectlistener | ||
createConnection(options: TcpNetConnectOpts, connectionListener?: (error: Error | null, socket: Socket) => void) { | ||
const { host } = options; | ||
if (host !== undefined) { | ||
// Direct ip address request without dns-lookup | ||
// Example: http://127.0.0.1 | ||
// https://nodejs.org/api/net.html#net_socket_connect_options_connectlistener | ||
const validationError = validateIPAddress({ address: host }, this.requestFilterOptions); | ||
if (validationError) { | ||
throw validationError; | ||
} | ||
} | ||
// https://nodejs.org/api/net.html#net_socket_connect_options_connectlistener | ||
// @ts-expect-error - @types/node does not defined createConnection | ||
const socket: Socket = super.createConnection(options, connectionListener); | ||
// Request with domain name | ||
// Example: http://127.0.0.1.nip.io/ | ||
const onLookup = (err: Error, address: string, family: string | number, host: string): void => { | ||
if (err) { | ||
return; | ||
} | ||
const validationError = validateIPAddress({ address, family, host }, this.requestFilterOptions); | ||
if (validationError) { | ||
socket.removeListener("lookup", onLookup); | ||
socket.end(() => { | ||
socket.destroy(validationError); | ||
}); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to call When just call socket.destroy(validationError);
|
||
}; | ||
socket.addListener("lookup", onLookup); | ||
return socket; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Implement validation as Agent subclass. We need to avoid this error. |
||
} | ||
} | ||
|
||
|
@@ -189,10 +206,11 @@ export const globalHttpsAgent = new RequestFilteringHttpsAgent(); | |
* Get an agent for the url | ||
* return http or https agent | ||
* @param url | ||
* @param options | ||
*/ | ||
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); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
allowMetaIPAddress and allowPrivateIPAddress has same logics.