forked from request/request
-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4107a71
commit 5fd6a69
Showing
11 changed files
with
901 additions
and
891 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,186 +1,251 @@ | ||
import { Agent } from "https"; | ||
import { Http2Agent, HTTP2ClientRequestOptions } from "../http2/http2Agent"; | ||
import * as https from "https"; | ||
import * as http2 from "http2"; | ||
import * as http from "http"; | ||
import { AutoRequestOptions, MultiProtocolRequest } from "./request"; | ||
import * as tls from "tls"; | ||
import { EventEmitter } from "events"; | ||
import * as net from "net"; | ||
import { Agent } from 'https' | ||
import { Http2Agent, HTTP2ClientRequestOptions } from '../http2/http2Agent' | ||
import * as https from 'https' | ||
import * as http2 from 'http2' | ||
import * as http from 'http' | ||
import { AutoRequestOptions, MultiProtocolRequest } from './request' | ||
import * as tls from 'tls' | ||
import { EventEmitter } from 'events' | ||
import * as net from 'net' | ||
|
||
interface CreateConnectionCallback { | ||
(err: null | Error, proto: "h2", connection: http2.ClientHttp2Session); | ||
(err: null, proto: 'h2', connection: http2.ClientHttp2Session) | ||
|
||
(err: null | Error, proto: "http1", connection: http.ClientRequest); | ||
(err: null, proto: 'http1', connection: http.ClientRequest) | ||
|
||
(err: Error); | ||
(err: Error, proto: undefined, connection: undefined) | ||
} | ||
|
||
function calculateServerName(options: AutoRequestOptions) { | ||
|
||
let servername = options.host || ''; | ||
const hostHeader = options.headers?.["host"]; | ||
function calculateServerName (options: AutoRequestOptions) { | ||
let servername = options.host || '' | ||
const hostHeader = options.headers?.host | ||
|
||
if (hostHeader) { | ||
if (typeof hostHeader !== "string") { | ||
if (typeof hostHeader !== 'string') { | ||
throw new TypeError( | ||
"host header content must be a string, received" + hostHeader | ||
); | ||
'host header content must be a string, received' + hostHeader | ||
) | ||
} | ||
|
||
// abc => abc | ||
// abc:123 => abc | ||
// [::1] => ::1 | ||
// [::1]:123 => ::1 | ||
if (hostHeader.startsWith("[")) { | ||
const index = hostHeader.indexOf("]"); | ||
if (hostHeader.startsWith('[')) { | ||
const index = hostHeader.indexOf(']') | ||
if (index === -1) { | ||
// Leading '[', but no ']'. Need to do something... | ||
servername = hostHeader; | ||
servername = hostHeader | ||
} else { | ||
servername = hostHeader.substring(1, index); | ||
servername = hostHeader.substring(1, index) | ||
} | ||
} else { | ||
servername = hostHeader.split(":", 1)[0]; | ||
servername = hostHeader.split(':', 1)[0] | ||
} | ||
} | ||
// Don't implicitly set invalid (IP) servernames. | ||
if (net.isIP(servername)) servername = ""; | ||
return servername; | ||
if (net.isIP(servername)) servername = '' | ||
return servername | ||
} | ||
|
||
function httpOptionsToUri(options: AutoRequestOptions): URL { | ||
const url = new URL('https://'+ (options.host || 'localhost')); | ||
return url; | ||
function httpOptionsToUri (options: AutoRequestOptions): URL { | ||
const url = new URL('https://' + (options.host || 'localhost')) | ||
return url | ||
} | ||
|
||
// @ts-ignore | ||
// @ts-expect-error | ||
export class AutoHttp2Agent extends EventEmitter implements Agent { | ||
private http2Agent: Http2Agent; | ||
private httpsAgent: https.Agent; | ||
private ALPNCache: Map<string, Map<number, string>>; | ||
private options: https.AgentOptions; | ||
defaultPort = 443; | ||
|
||
constructor(options: https.AgentOptions) { | ||
super(); | ||
this.http2Agent = new Http2Agent(options); | ||
this.httpsAgent = new https.Agent(options); | ||
this.ALPNCache = new Map(); | ||
this.options = options; | ||
private readonly http2Agent: Http2Agent | ||
private readonly httpsAgent: https.Agent | ||
private readonly ALPNCache: Map<string, string> | ||
private readonly options: https.AgentOptions | ||
defaultPort = 443 | ||
|
||
constructor (options: https.AgentOptions) { | ||
super() | ||
this.http2Agent = new Http2Agent(options) | ||
this.httpsAgent = new https.Agent(options) | ||
this.ALPNCache = new Map() | ||
this.options = options | ||
} | ||
|
||
createConnection( | ||
createConnection ( | ||
req: MultiProtocolRequest, | ||
reqOptions: AutoRequestOptions, | ||
cb: CreateConnectionCallback, | ||
socketCb: (socket: tls.TLSSocket) => void | ||
) { | ||
const options = { ...reqOptions, ...this.options } | ||
const name = this.getName(options); | ||
|
||
const options = {...reqOptions, ...this.options}; | ||
|
||
const uri = httpOptionsToUri(options); | ||
const port = Number(options.port || this.defaultPort); | ||
const uri = httpOptionsToUri(options) | ||
const port = Number(options.port || this.defaultPort) | ||
|
||
// check if there is ALPN cached | ||
// TODO: Replace map of map cache with getName based cache | ||
const hostnameCache = | ||
this.ALPNCache.get(uri.hostname) ?? new Map<number, string>(); | ||
const protocol = hostnameCache.get(port); | ||
if (protocol === "h2") { | ||
const protocol = this.ALPNCache.get(name) | ||
if (protocol === 'h2') { | ||
const newOptions: HTTP2ClientRequestOptions = { | ||
...options, | ||
port, | ||
path: options.socketPath, | ||
host: options.hostname || options.host || "localhost", | ||
}; | ||
const connection = this.http2Agent.createConnection(req, uri, newOptions); | ||
cb(null, "h2", connection); | ||
socketCb(connection.socket as tls.TLSSocket); | ||
return; | ||
host: options.hostname || options.host || 'localhost' | ||
} | ||
const connection = this.http2Agent.createConnection(req, uri, newOptions) | ||
cb(null, 'h2', connection) | ||
socketCb(connection.socket as tls.TLSSocket) | ||
return | ||
} | ||
if (protocol === "http/1.1" || protocol === "http/1.0") { | ||
if (protocol === 'http/1.1' || protocol === 'http/1.0') { | ||
const requestOptions: https.RequestOptions = { | ||
...options, | ||
agent: this.httpsAgent, | ||
host: options.hostname || options.host || "localhost", | ||
}; | ||
host: options.hostname || options.host || 'localhost' | ||
} | ||
|
||
const request = https.request(requestOptions); | ||
request.on("socket", (socket) => socketCb(socket as tls.TLSSocket)); | ||
cb(null, "http1", request); | ||
return; | ||
const request = https.request(requestOptions) | ||
request.on('socket', (socket) => socketCb(socket as tls.TLSSocket)) | ||
cb(null, 'http1', request) | ||
return | ||
} | ||
|
||
const newOptions: tls.ConnectionOptions = { | ||
...options, | ||
port, | ||
path: options.socketPath, | ||
ALPNProtocols: ["h2", "http/1.1", "http/1.0"], | ||
ALPNProtocols: ['h2', 'http/1.1', 'http/1.0'], | ||
servername: options.servername || calculateServerName(options), | ||
host: options.hostname || options.host || "localhost", | ||
}; | ||
|
||
const socket = tls.connect(newOptions); | ||
socketCb(socket); | ||
socket.on("error", (e) => cb(e)); | ||
socket.once("secureConnect", () => { | ||
const protocol = socket.alpnProtocol; | ||
host: options.hostname || options.host || 'localhost' | ||
} | ||
|
||
const socket = tls.connect(newOptions) | ||
socketCb(socket) | ||
socket.on('error', (e: Error) => cb(e, undefined, undefined)) | ||
socket.once('secureConnect', () => { | ||
const protocol = socket.alpnProtocol | ||
if (!protocol) { | ||
cb(socket.authorizationError); | ||
socket.end(); | ||
return; | ||
cb(socket.authorizationError, undefined, undefined) | ||
socket.end() | ||
return | ||
} | ||
|
||
const hostnameCache = this.ALPNCache.get(uri.hostname); | ||
if (!hostnameCache) { | ||
const portMap = new Map<number, string>(); | ||
portMap.set(port, protocol); | ||
this.ALPNCache.set(uri.hostname, portMap); | ||
} else { | ||
hostnameCache.set(port, protocol); | ||
} | ||
this.ALPNCache.set(name, protocol) | ||
|
||
if (protocol === "h2") { | ||
if (protocol === 'h2') { | ||
const newOptions: HTTP2ClientRequestOptions = { | ||
...options, | ||
port, | ||
path: options.socketPath, | ||
host: options.hostname || options.host || "localhost", | ||
}; | ||
host: options.hostname || options.host || 'localhost' | ||
} | ||
|
||
const connection = this.http2Agent.createConnection( | ||
req, | ||
uri, | ||
newOptions, | ||
socket | ||
); | ||
cb(null, "h2", connection); | ||
} else if (protocol === "http/1.1") { | ||
) | ||
cb(null, 'h2', connection) | ||
} else if (protocol === 'http/1.1') { | ||
// Protocol is http1, using the built in | ||
// We need to release all free sockets so that new connection is created using the overriden createconnection forcing the agent to reuse the socket used for alpn | ||
|
||
// This reassignment works, since all code so far is sync, and happens in the same tick, hence there will be no race conditions | ||
// @ts-ignore | ||
const oldCreateConnection = this.httpsAgent.createConnection; | ||
// @ts-ignore | ||
// @ts-expect-error | ||
const oldCreateConnection = this.httpsAgent.createConnection | ||
// @ts-expect-error | ||
this.httpsAgent.createConnection = () => { | ||
return socket; | ||
}; | ||
return socket | ||
} | ||
|
||
const requestOptions: https.RequestOptions = { | ||
...options, | ||
agent: this.httpsAgent, | ||
host: options.hostname || options.host || "localhost", | ||
}; | ||
host: options.hostname || options.host || 'localhost' | ||
} | ||
|
||
const request = https.request(requestOptions); | ||
// @ts-ignore | ||
this.httpsAgent.createConnection = oldCreateConnection; | ||
cb(null, "http1", request); | ||
const request = https.request(requestOptions) | ||
// @ts-expect-error | ||
this.httpsAgent.createConnection = oldCreateConnection | ||
cb(null, 'http1', request) | ||
} else { | ||
cb(new Error("Unknown protocol" + protocol)); | ||
return; | ||
cb(new Error('Unknown protocol' + protocol), undefined, undefined) | ||
} | ||
}); | ||
}) | ||
} | ||
|
||
getName (options: AutoRequestOptions) { | ||
let name = options.host || 'localhost' | ||
|
||
name += ':' | ||
if (options.port) { name += options.port } | ||
|
||
name += ':' | ||
if (options.localAddress) { name += options.localAddress } | ||
|
||
if (options.path) { name += `:${options.path}` } | ||
|
||
name += ':' | ||
if (options.ca) { name += options.ca } | ||
|
||
name += ':' | ||
if (options.cert) { name += options.cert } | ||
|
||
name += ':' | ||
if (options.clientCertEngine) { name += options.clientCertEngine } | ||
|
||
name += ':' | ||
if (options.ciphers) { name += options.ciphers } | ||
|
||
name += ':' | ||
if (options.key) { name += options.key } | ||
|
||
name += ':' | ||
if (options.pfx) { name += options.pfx } | ||
|
||
name += ':' | ||
if (options.rejectUnauthorized !== undefined) { name += options.rejectUnauthorized } | ||
|
||
name += ':' | ||
if (options.servername && options.servername !== options.host) { name += options.servername } | ||
|
||
name += ':' | ||
if (options.minVersion) { name += options.minVersion } | ||
|
||
name += ':' | ||
if (options.maxVersion) { name += options.maxVersion } | ||
|
||
name += ':' | ||
if (options.secureProtocol) { name += options.secureProtocol } | ||
|
||
name += ':' | ||
if (options.crl) { name += options.crl } | ||
|
||
name += ':' | ||
if (options.honorCipherOrder !== undefined) { name += options.honorCipherOrder } | ||
|
||
name += ':' | ||
if (options.ecdhCurve) { name += options.ecdhCurve } | ||
|
||
name += ':' | ||
if (options.dhparam) { name += options.dhparam } | ||
|
||
name += ':' | ||
if (options.secureOptions !== undefined) { name += options.secureOptions } | ||
|
||
name += ':' | ||
if (options.sessionIdContext) { name += options.sessionIdContext } | ||
|
||
name += ':' | ||
if (options.sigalgs) { name += JSON.stringify(options.sigalgs) } | ||
|
||
name += ':' | ||
if (options.privateKeyIdentifier) { name += options.privateKeyIdentifier } | ||
|
||
name += ':' | ||
if (options.privateKeyEngine) { name += options.privateKeyEngine } | ||
|
||
return name | ||
} | ||
} | ||
|
||
export const globalAgent = new AutoHttp2Agent({}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,8 @@ | ||
import {AutoHttp2Agent} from "./agent"; | ||
import {request} from "./request"; | ||
import { AutoHttp2Agent, globalAgent } from './agent' | ||
import { request } from './request' | ||
|
||
export default { | ||
Agent: AutoHttp2Agent, | ||
request: request, | ||
globalAgent: new AutoHttp2Agent({}) | ||
export default { | ||
Agent: AutoHttp2Agent, | ||
request, | ||
globalAgent | ||
} |
Oops, something went wrong.