import { core, primordials } from "ext:core/mod.js";
import { op_get_env_no_permission_check } from "ext:core/ops";
import * as net from "node:net";
import httpProxy from "node:_http_proxy";
const lazyTls = core.createLazyLoader("node:tls");
const { EventEmitter } = core.loadExtScript("ext:deno_node/_events.mjs");
const { debuglog } = core.loadExtScript(
"ext:deno_node/internal/util/debuglog.ts",
);
let debug = debuglog("http", (fn) => {
debug = fn;
});
const { AsyncResource } = core.loadExtScript("ext:deno_node/async_hooks.ts");
const {
emitDestroy,
emitInit,
executionAsyncId,
symbols,
} = core.loadExtScript("ext:deno_node/internal/async_hooks.ts");
const { async_id_symbol } = symbols;
const { once } = core.loadExtScript("ext:deno_node/internal/util.mjs");
const {
validateNumber,
validateOneOf,
validateString,
} = core.loadExtScript("ext:deno_node/internal/validators.mjs");
const {
ArrayPrototypeIncludes,
ArrayPrototypeIndexOf,
ArrayPrototypePop,
ArrayPrototypePush,
ArrayPrototypeShift,
ArrayPrototypeSome,
ArrayPrototypeSplice,
FunctionPrototypeCall,
NumberIsFinite,
NumberParseInt,
ObjectCreate,
ObjectKeys,
ObjectPrototypeIsPrototypeOf,
ObjectSetPrototypeOf,
ObjectValues,
RegExpPrototypeExec,
SafeRegExp,
StringPrototypeIndexOf,
StringPrototypeSlice,
StringPrototypeSplit,
StringPrototypeStartsWith,
Symbol,
} = primordials;
const KEEP_ALIVE_TIMEOUT_RE = new SafeRegExp("^timeout=(\\d+)");
const kOnKeylog = Symbol("onkeylog");
const kRequestOptions = Symbol("requestOptions");
const kRequestAsyncResource = Symbol("requestAsyncResource");
class ReusedHandle {
constructor(type, handle) {
this.type = type;
this.handle = handle;
}
}
const providerTypeNames = [
"NONE",
"DIRHANDLE",
"DNSCHANNEL",
"ELDHISTOGRAM",
"FILEHANDLE",
"FILEHANDLECLOSEREQ",
"FIXEDSIZEBLOBCOPY",
"FSEVENTWRAP",
"FSREQCALLBACK",
"FSREQPROMISE",
"GETADDRINFOREQWRAP",
"GETNAMEINFOREQWRAP",
"HEAPSNAPSHOT",
"HTTP2SESSION",
"HTTP2STREAM",
"HTTP2PING",
"HTTP2SETTINGS",
"HTTPINCOMINGMESSAGE",
"HTTPCLIENTREQUEST",
"JSSTREAM",
"JSUDPWRAP",
"MESSAGEPORT",
"PIPECONNECTWRAP",
"PIPESERVERWRAP",
"PIPEWRAP",
"PROCESSWRAP",
"PROMISE",
"QUERYWRAP",
"SHUTDOWNWRAP",
"SIGNALWRAP",
"STATWATCHER",
"STREAMPIPE",
"TCPCONNECTWRAP",
"TCPSERVERWRAP",
"TCPWRAP",
"TLSWRAP",
"TTYWRAP",
"UDPSENDWRAP",
"UDPWRAP",
"SIGINTWATCHDOG",
"WORKER",
"WORKERHEAPSNAPSHOT",
"WRITEWRAP",
"ZLIB",
];
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 (!ObjectPrototypeIsPrototypeOf(Agent.prototype, this)) {
return new Agent(options);
}
FunctionPrototypeCall(EventEmitter, 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 = ObjectCreate(null);
this.sockets = ObjectCreate(null);
this.freeSockets = ObjectCreate(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 &&
NumberIsFinite(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;
}
httpProxy.initAgentProxy(this, this.options);
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 = ArrayPrototypeShift(requests);
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);
ArrayPrototypePush(freeSockets, socket);
});
this.on("newListener", maybeEnableKeylog);
}
ObjectSetPrototypeOf(Agent.prototype, EventEmitter.prototype);
ObjectSetPrototypeOf(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 = ObjectValues(this.sockets);
for (let i = 0; i < sockets.length; i++) {
sockets[i].on("keylog", this[kOnKeylog]);
}
}
}
Agent.defaultMaxSockets = Infinity;
Agent.prototype.createConnection = function createConnection(options, cb) {
const proxy = options && options._proxy;
if (proxy && options._proxyProtocol === "http:") {
const connectOpts = {
__proto__: null,
...options,
host: proxy.hostname,
hostname: proxy.hostname,
port: proxy.port,
servername: undefined,
};
delete connectOpts._proxy;
delete connectOpts._proxyTargetHost;
delete connectOpts._proxyTargetPort;
delete connectOpts._proxyProtocol;
if (proxy.protocol === "https:") {
connectOpts.servername = proxy.hostname;
if (
connectOpts.ca === undefined &&
op_get_env_no_permission_check("NODE_EXTRA_CA_CERTS")
) {
connectOpts.ca = lazyTls().default.getCACertificates("default");
}
return lazyTls().default.connect(connectOpts, cb);
}
return net.createConnection(connectOpts, cb);
}
return net.createConnection(options, cb);
};
Agent.prototype.getName = function getName(options = { __proto__: null }) {
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) {
ArrayPrototypeShift(freeSockets);
}
socket = this.scheduling === "fifo"
? ArrayPrototypeShift(freeSockets)
: ArrayPrototypePop(freeSockets);
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);
ArrayPrototypePush(this.sockets[name], 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");
ArrayPrototypePush(this.requests[name], 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] = [];
}
ArrayPrototypePush(this.sockets[name], 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 (StringPrototypeStartsWith(hostHeader, "[")) {
const index = StringPrototypeIndexOf(hostHeader, "]");
if (index === -1) {
servername = hostHeader;
} else {
servername = StringPrototypeSlice(hostHeader, 1, index);
}
} else {
servername = StringPrototypeSplit(hostHeader, ":", 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 (
ArrayPrototypeSome(
ObjectKeys(sockets),
(name) => ArrayPrototypeIncludes(sockets[name], 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) {
ArrayPrototypePush(sets, this.freeSockets);
}
for (let sk = 0; sk < sets.length; sk++) {
const sockets = sets[sk];
if (sockets[name]) {
const index = ArrayPrototypeIndexOf(sockets[name], s);
if (index !== -1) {
ArrayPrototypeSplice(sockets[name], 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 = ObjectKeys(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 = RegExpPrototypeExec(KEEP_ALIVE_TIMEOUT_RE, keepAliveHint)
?.[1];
if (hint) {
let serverHintTimeout = (NumberParseInt(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 = ObjectKeys(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") {
const oldAsyncId = handle.getAsyncId();
const providerType = handle.getProviderType();
const reusedHandle = new ReusedHandle(providerType, handle);
if (oldAsyncId > 0) {
emitDestroy(oldAsyncId);
}
handle.asyncReset(reusedHandle);
socket[async_id_symbol] = handle.getAsyncId();
emitInit(
socket[async_id_symbol],
providerTypeNames[providerType] || "UNKNOWN",
executionAsyncId(),
reusedHandle,
);
}
}
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;
},
};