Skip to content

Commit

Permalink
Fixed host header settings
Browse files Browse the repository at this point in the history
  • Loading branch information
parthverma1 committed Jun 13, 2024
1 parent 4107a71 commit 5fd6a69
Show file tree
Hide file tree
Showing 11 changed files with 901 additions and 891 deletions.
4 changes: 2 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ function initParams (uri, options, callback) {
params.callback = callback || params.callback

// Disable http/2 when using custom agents that don't handle different versions separately
if (params.agents && !(params.agents.http1 || params.agents.auto || params.agents.h2)){
if (params.agents && !(params.agents.http1 || params.agents.auto || params.agents.h2)) {
params.protocolVersion = 'http1'
}

// Disable http/2 when using proxy or tunnels
if (params.tunnel || params.proxy){
if (params.tunnel || params.proxy) {
params.protocolVersion = 'http1'
}

Expand Down
275 changes: 170 additions & 105 deletions lib/autohttp/agent.ts
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({})
12 changes: 6 additions & 6 deletions lib/autohttp/index.ts
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
}
Loading

0 comments on commit 5fd6a69

Please sign in to comment.