import * as net from "node:net";
import EventEmitter from "node:events";
import { debuglog } from "ext:deno_node/internal/util/debuglog.ts";
let debug = debuglog("http", (fn) => {
debug = fn;
});
import { AsyncResource } from "node:async_hooks";
import { symbols } from "ext:deno_node/internal/async_hooks.ts";
const { async_id_symbol } = symbols;
import { once } from "ext:deno_node/internal/util.mjs";
import {
validateNumber,
validateOneOf,
validateString,
} from "ext:deno_node/internal/validators.mjs";
const kOnKeylog = Symbol("onkeylog");
const kRequestOptions = Symbol("requestOptions");
const kRequestAsyncResource = Symbol("requestAsyncResource");
class ReusedHandle {
constructor(type, handle) {
this.type = type;
this.handle = handle;
}
}
function freeSocketErrorListener(err) {
const socket = this;
debug("SOCKET ERROR on FREE socket:", err.message, err.stack);
socket.destroy();
socket.emit("agentRemove");
}
export function Agent(options) {
if (!(this instanceof Agent)) {
return new Agent(options);
}
EventEmitter.call(this);
this.options = { __proto__: null, ...options };
this.defaultPort = this.options.defaultPort || 80;
this.protocol = this.options.protocol || "http:";
if (this.options.noDelay === undefined) {
this.options.noDelay = true;
}
this.options.path = null;
this.requests = Object.create(null);
this.sockets = Object.create(null);
this.freeSockets = Object.create(null);
this.keepAliveMsecs = this.options.keepAliveMsecs || 1000;
this.keepAlive = this.options.keepAlive || false;
this.maxSockets = this.options.maxSockets || Agent.defaultMaxSockets;
this.maxFreeSockets = this.options.maxFreeSockets || 256;
this.scheduling = this.options.scheduling || "lifo";
this.maxTotalSockets = this.options.maxTotalSockets;
this.totalSocketCount = 0;
this.agentKeepAliveTimeoutBuffer =
typeof this.options.agentKeepAliveTimeoutBuffer === "number" &&
this.options.agentKeepAliveTimeoutBuffer >= 0 &&
Number.isFinite(this.options.agentKeepAliveTimeoutBuffer)
? this.options.agentKeepAliveTimeoutBuffer
: 1000;
validateOneOf(this.scheduling, "scheduling", ["fifo", "lifo"]);
if (this.maxTotalSockets !== undefined) {
validateNumber(this.maxTotalSockets, "maxTotalSockets", 1);
} else {
this.maxTotalSockets = Infinity;
}
this.on("free", (socket, options) => {
const name = this.getName(options);
debug("agent.on(free)", name);
if (socket.destroyed) {
return;
}
const requests = this.requests[name];
if (requests && requests.length && !socket.writable) {
socket.destroy();
return;
}
if (requests && requests.length) {
const req = requests.shift();
const reqAsyncRes = req[kRequestAsyncResource];
if (reqAsyncRes) {
reqAsyncRes.runInAsyncScope(() => {
asyncResetHandle(socket);
setRequestSocket(this, req, socket);
});
req[kRequestAsyncResource] = null;
} else {
setRequestSocket(this, req, socket);
}
if (requests.length === 0) {
delete this.requests[name];
}
return;
}
const req = socket._httpMessage;
if (!req || !req.shouldKeepAlive || !this.keepAlive) {
socket.destroy();
return;
}
const freeSockets = this.freeSockets[name] || [];
const freeLen = freeSockets.length;
let count = freeLen;
if (this.sockets[name]) {
count += this.sockets[name].length;
}
if (
this.totalSocketCount > this.maxTotalSockets ||
count > this.maxSockets ||
freeLen >= this.maxFreeSockets ||
!this.keepSocketAlive(socket)
) {
socket.destroy();
return;
}
this.freeSockets[name] = freeSockets;
socket[async_id_symbol] = -1;
socket._httpMessage = null;
this.removeSocket(socket, options);
socket.once("error", freeSocketErrorListener);
freeSockets.push(socket);
});
this.on("newListener", maybeEnableKeylog);
}
Object.setPrototypeOf(Agent.prototype, EventEmitter.prototype);
Object.setPrototypeOf(Agent, EventEmitter);
function maybeEnableKeylog(eventName) {
if (eventName === "keylog") {
this.removeListener("newListener", maybeEnableKeylog);
const agent = this;
this[kOnKeylog] = function onkeylog(keylog) {
agent.emit("keylog", keylog, this);
};
const sockets = Object.values(this.sockets);
for (let i = 0; i < sockets.length; i++) {
sockets[i].on("keylog", this[kOnKeylog]);
}
}
}
Agent.defaultMaxSockets = Infinity;
Agent.prototype.createConnection = net.createConnection;
Agent.prototype.getName = function getName(options = {}) {
let name = options.host || "localhost";
name += ":";
if (options.port) {
name += options.port;
}
name += ":";
if (options.localAddress) {
name += options.localAddress;
}
if (options.family === 4 || options.family === 6) {
name += `:${options.family}`;
}
if (options.socketPath) {
name += `:${options.socketPath}`;
}
return name;
};
Agent.prototype.addRequest = function addRequest(
req,
options,
port,
localAddress,
) {
if (typeof options === "string") {
options = {
__proto__: null,
host: options,
port,
localAddress,
};
}
options = { __proto__: null, ...options, ...this.options };
if (options.socketPath) {
options.path = options.socketPath;
}
if (!options.servername && options.servername !== "") {
options.servername = calculateServerName(options, req);
}
const name = this.getName(options);
if (!this.sockets[name]) {
this.sockets[name] = [];
}
const freeSockets = this.freeSockets[name];
let socket;
if (freeSockets) {
while (freeSockets.length && freeSockets[0].destroyed) {
freeSockets.shift();
}
socket = this.scheduling === "fifo"
? freeSockets.shift()
: freeSockets.pop();
if (!freeSockets.length) {
delete this.freeSockets[name];
}
}
if (socket && (socket.destroyed || !socket.writable)) {
socket.destroy();
socket = null;
}
const freeLen = freeSockets ? freeSockets.length : 0;
const sockLen = freeLen + this.sockets[name].length;
if (socket) {
asyncResetHandle(socket);
this.reuseSocket(socket, req);
setRequestSocket(this, req, socket);
this.sockets[name].push(socket);
} else if (
sockLen < this.maxSockets &&
this.totalSocketCount < this.maxTotalSockets
) {
debug("call onSocket", sockLen, freeLen);
this.createSocket(req, options, (err, socket) => {
if (err) {
req.onSocket(socket, err);
} else {
setRequestSocket(this, req, socket);
}
});
} else {
debug("wait for socket");
if (!this.requests[name]) {
this.requests[name] = [];
}
req[kRequestOptions] = options;
req[kRequestAsyncResource] = new AsyncResource("QueuedRequest");
this.requests[name].push(req);
}
};
Agent.prototype.createSocket = function createSocket(req, options, cb) {
options = { __proto__: null, ...options, ...this.options };
if (options.socketPath) {
options.path = options.socketPath;
}
if (!options.servername && options.servername !== "") {
options.servername = calculateServerName(options, req);
}
const timeout = req.timeout || this.options.timeout || undefined;
if (timeout) {
options.timeout = timeout;
}
const name = this.getName(options);
options._agentKey = name;
debug("createConnection", name, options);
options.encoding = null;
const oncreate = once((err, s) => {
if (err) {
return cb(err);
}
if (!this.sockets[name]) {
this.sockets[name] = [];
}
this.sockets[name].push(s);
this.totalSocketCount++;
debug("sockets", name, this.sockets[name].length, this.totalSocketCount);
installListeners(this, s, options);
cb(null, s);
});
if (this.keepAlive) {
options.keepAlive = this.keepAlive;
options.keepAliveInitialDelay = this.keepAliveMsecs;
}
const newSocket = this.createConnection(options, oncreate);
if (newSocket) {
oncreate(null, newSocket);
}
};
function calculateServerName(options, req) {
let servername = options.host;
const hostHeader = req.getHeader("host");
if (hostHeader) {
validateString(hostHeader, "options.headers.host");
if (hostHeader.startsWith("[")) {
const index = hostHeader.indexOf("]");
if (index === -1) {
servername = hostHeader;
} else {
servername = hostHeader.slice(1, index);
}
} else {
servername = hostHeader.split(":", 1)[0];
}
}
if (net.isIP(servername)) {
servername = "";
}
return servername;
}
function installListeners(agent, s, options) {
function onFree() {
debug("CLIENT socket onFree");
agent.emit("free", s, options);
}
s.on("free", onFree);
function onClose(_err) {
debug("CLIENT socket onClose");
agent.totalSocketCount--;
agent.removeSocket(s, options);
}
s.on("close", onClose);
function onTimeout() {
debug("CLIENT socket onTimeout");
const sockets = agent.freeSockets;
if (Object.keys(sockets).some((name) => sockets[name].includes(s))) {
return s.destroy();
}
}
s.on("timeout", onTimeout);
function onRemove() {
debug("CLIENT socket onRemove");
agent.totalSocketCount--;
agent.removeSocket(s, options);
s.removeListener("close", onClose);
s.removeListener("free", onFree);
s.removeListener("timeout", onTimeout);
s.removeListener("agentRemove", onRemove);
}
s.on("agentRemove", onRemove);
if (agent[kOnKeylog]) {
s.on("keylog", agent[kOnKeylog]);
}
}
Agent.prototype.removeSocket = function removeSocket(s, options) {
const name = this.getName(options);
debug("removeSocket", name, "writable:", s.writable);
const sets = [this.sockets];
if (!s.writable) {
sets.push(this.freeSockets);
}
for (let sk = 0; sk < sets.length; sk++) {
const sockets = sets[sk];
if (sockets[name]) {
const index = sockets[name].indexOf(s);
if (index !== -1) {
sockets[name].splice(index, 1);
if (sockets[name].length === 0) {
delete sockets[name];
}
}
}
}
let req;
if (this.requests[name] && this.requests[name].length) {
debug("removeSocket, have a request, make a socket");
req = this.requests[name][0];
} else {
const keys = Object.keys(this.requests);
for (let i = 0; i < keys.length; i++) {
const prop = keys[i];
if (this.sockets[prop] && this.sockets[prop].length) break;
debug(
"removeSocket, have a request with different origin," +
" make a socket",
);
req = this.requests[prop][0];
options = req[kRequestOptions];
break;
}
}
if (req && options) {
req[kRequestOptions] = undefined;
this.createSocket(req, options, (err, socket) => {
if (err) {
req.onSocket(socket, err);
} else {
socket.emit("free");
}
});
}
};
Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
socket.setKeepAlive(true, this.keepAliveMsecs);
socket.unref();
let agentTimeout = this.options.timeout || 0;
let canKeepSocketAlive = true;
if (socket._httpMessage?.res) {
const keepAliveHint = socket._httpMessage.res.headers["keep-alive"];
if (keepAliveHint) {
const hint = /^timeout=(\d+)/.exec(keepAliveHint)?.[1];
if (hint) {
let serverHintTimeout = (Number.parseInt(hint) * 1000) -
this.agentKeepAliveTimeoutBuffer;
serverHintTimeout = serverHintTimeout > 0 ? serverHintTimeout : 0;
if (serverHintTimeout === 0) {
canKeepSocketAlive = false;
} else if (serverHintTimeout < agentTimeout) {
agentTimeout = serverHintTimeout;
}
}
}
}
if (socket.timeout !== agentTimeout) {
socket.setTimeout(agentTimeout);
}
return canKeepSocketAlive;
};
Agent.prototype.reuseSocket = function reuseSocket(socket, req) {
debug("have free socket");
socket.removeListener("error", freeSocketErrorListener);
req.reusedSocket = true;
socket.ref();
};
Agent.prototype.destroy = function destroy() {
const sets = [this.freeSockets, this.sockets];
for (let s = 0; s < sets.length; s++) {
const set = sets[s];
const keys = Object.keys(set);
for (let v = 0; v < keys.length; v++) {
const setName = set[keys[v]];
for (let n = 0; n < setName.length; n++) {
setName[n].destroy();
}
}
}
};
function setRequestSocket(agent, req, socket) {
req.onSocket(socket);
const agentTimeout = agent.options.timeout || 0;
if (req.timeout === undefined || req.timeout === agentTimeout) {
return;
}
socket.setTimeout(req.timeout);
}
function asyncResetHandle(socket) {
const handle = socket._handle;
if (handle && typeof handle.asyncReset === "function") {
handle.asyncReset(new ReusedHandle(handle.getProviderType(), handle));
socket[async_id_symbol] = handle.getAsyncId();
}
}
export let globalAgent = new Agent({
keepAlive: true,
scheduling: "lifo",
timeout: 5000,
});
export function setGlobalAgent(agent) {
globalAgent = agent;
}
export default {
Agent,
get globalAgent() {
return globalAgent;
},
set globalAgent(agent) {
globalAgent = agent;
},
};