Files
send-mail/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.js
T

209 lines
8.6 KiB
JavaScript
Raw Normal View History

2026-09-15 13:00:32 +02:00
"use strict";
2020-02-29 23:01:03 +01:00
/**
* Minimal HTTP/S proxy client
*/
2026-09-15 13:00:32 +02:00
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_net_1 = __importDefault(require("node:net"));
const node_tls_1 = __importDefault(require("node:tls"));
const urllib = __importStar(require("../shared/url.js"));
const errors = __importStar(require("../errors.js"));
2026-08-07 08:08:22 +02:00
// Cap the CONNECT response we buffer before the header terminator, so a proxy that
// never sends \r\n\r\n cannot grow memory unboundedly before the socket times out.
const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
2026-10-10 17:27:57 +02:00
// URL hostnames keep the brackets around an IPv6 literal, socket options and net.isIPv6 take it without
const unbracket = (host) => typeof host === 'string' && host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
2026-06-15 07:32:52 +02:00
function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions, callback) {
if (typeof tlsOptions === 'function') {
callback = tlsOptions;
tlsOptions = {};
}
tlsOptions = tlsOptions || {};
2026-10-01 09:48:38 +02:00
// the error paths hand over the error alone
const done = callback;
2026-08-07 08:08:22 +02:00
// Reject CRLF in the destination before it reaches the CONNECT request line
// and Host header. A tainted host/port could otherwise inject additional
// request headers into the proxy connection (HTTP request splitting).
destinationPort = Number(destinationPort) || 0;
if (!destinationPort || /[\r\n]/.test(destinationHost)) {
const err = new Error('Invalid proxy destination');
err.code = errors.EPROXY;
2026-10-01 09:48:38 +02:00
setImmediate(() => done(err));
2026-09-15 13:00:32 +02:00
return;
2026-08-07 08:08:22 +02:00
}
2026-04-28 12:50:45 +02:00
const proxy = urllib.parse(proxyUrl);
2026-10-10 17:27:57 +02:00
// the CONNECT request line and the Host header take an IPv6 destination in brackets
const authority = (node_net_1.default.isIPv6(unbracket(destinationHost)) ? '[' + unbracket(destinationHost) + ']' : destinationHost) + ':' + destinationPort;
2026-06-15 07:32:52 +02:00
const connectOptions = {
2020-02-29 23:01:03 +01:00
host: proxy.hostname,
port: Number(proxy.port) ? Number(proxy.port) : proxy.protocol === 'https:' ? 443 : 80
};
2026-04-28 12:50:45 +02:00
let connect;
2020-02-29 23:01:03 +01:00
if (proxy.protocol === 'https:') {
2026-06-15 07:32:52 +02:00
// Validate the proxy's TLS certificate by default. A caller that uses a
// self-signed proxy (e.g. integration tests) opts out explicitly with
// tls.rejectUnauthorized === false.
connectOptions.rejectUnauthorized = tlsOptions.rejectUnauthorized !== false;
2026-09-15 13:00:32 +02:00
connect = node_tls_1.default.connect.bind(node_tls_1.default);
}
else {
connect = node_net_1.default.connect.bind(node_net_1.default);
2020-02-29 23:01:03 +01:00
}
2026-10-10 17:27:57 +02:00
// The handshake is bounded as a whole, a proxy that keeps sending a byte now and then can
// not hold the connection open past it
const timeout = Number(tlsOptions.timeout) || httpProxyClient.timeout || 30 * 1000;
2026-04-28 12:50:45 +02:00
let socket;
2026-10-10 17:27:57 +02:00
// Single settlement path for the handshake: every temporary listener and the timer are
// dropped exactly once. Once the tunnel is up, the responsibility to handle errors is passed
// to whoever uses this socket
2020-02-29 23:01:03 +01:00
let finished = false;
2026-10-10 17:27:57 +02:00
let timer;
const cleanup = () => {
clearTimeout(timer);
socket.removeListener('data', onSocketData);
socket.removeListener('error', fail);
socket.removeListener('close', onEarlyClose);
};
function fail(err) {
2020-02-29 23:01:03 +01:00
if (finished) {
return;
}
finished = true;
2026-10-10 17:27:57 +02:00
cleanup();
2020-02-29 23:01:03 +01:00
try {
socket.destroy();
2026-09-15 13:00:32 +02:00
}
catch (_E) {
2020-02-29 23:01:03 +01:00
// ignore
}
2026-10-01 09:48:38 +02:00
done(err);
2026-10-10 17:27:57 +02:00
}
function onEarlyClose() {
const err = new Error('Proxy closed the connection before the tunnel was established');
err.code = errors.EPROXY;
fail(err);
}
// The response is collected as chunks and only the bytes that just arrived, together
// with the three before them, are searched for the end of the headers. Appending to a
// string and searching all of it again re-read the whole response on every chunk.
const chunks = [];
let received = 0;
let tail = '';
function onSocketData(chunk) {
if (finished) {
return;
}
const window = tail + chunk.toString('binary');
const windowEnd = window.indexOf('\r\n\r\n');
chunks.push(chunk);
received += chunk.length;
tail = window.slice(-3);
if (windowEnd < 0) {
if (received > MAX_RESPONSE_HEADER_BYTES) {
const err = new Error('Proxy response headers too large');
err.code = errors.EPROXY;
fail(err);
}
return;
}
// Stop reading before anything is put back. A socket that keeps flowing would emit the
// bytes after the headers, a greeting the proxy sent together with its own response,
// before the next owner of the socket has a listener for them
socket.removeListener('data', onSocketData);
socket.pause();
const headerEnd = received - window.length + windowEnd;
const response = Buffer.concat(chunks, received);
if (response.length > headerEnd + 4) {
socket.unshift(response.subarray(headerEnd + 4));
}
// check response code
const match = response.toString('binary', 0, headerEnd).match(/^HTTP\/\d+\.\d+ (\d+)/i);
if (!match || (match[1] || '').charAt(0) !== '2') {
const err = new Error('Invalid response from proxy' + ((match && ': ' + match[1]) || ''));
err.code = errors.EPROXY;
return fail(err);
}
// proxy connection is now established
finished = true;
cleanup();
// A fresh socket starts flowing once something listens for 'data', a paused one would
// not. Keep that behaviour for the next owner of the socket
const resumeOnData = (event) => {
if (event === 'data') {
socket.removeListener('newListener', resumeOnData);
socket.resume();
}
};
socket.on('newListener', resumeOnData);
return done(null, socket);
}
2026-06-15 07:32:52 +02:00
socket = connect(connectOptions, () => {
2020-02-29 23:01:03 +01:00
if (finished) {
return;
}
2026-04-28 12:50:45 +02:00
const reqHeaders = {
2026-10-10 17:27:57 +02:00
Host: authority,
2020-02-29 23:01:03 +01:00
Connection: 'close'
};
if (proxy.auth) {
reqHeaders['Proxy-Authorization'] = 'Basic ' + Buffer.from(proxy.auth).toString('base64');
}
socket.write(
2026-09-15 13:00:32 +02:00
// HTTP method
'CONNECT ' +
2026-10-10 17:27:57 +02:00
authority +
2026-09-15 13:00:32 +02:00
' HTTP/1.1\r\n' +
// HTTP request headers
Object.keys(reqHeaders)
.map(key => key + ': ' + reqHeaders[key])
.join('\r\n') +
// End request
'\r\n\r\n');
2020-02-29 23:01:03 +01:00
socket.on('data', onSocketData);
});
2026-10-10 17:27:57 +02:00
timer = setTimeout(() => {
const err = new Error('Proxy socket timed out');
err.code = errors.ETIMEDOUT;
fail(err);
}, timeout);
socket.once('error', fail);
socket.once('close', onEarlyClose);
2020-02-29 23:01:03 +01:00
}
2026-09-15 13:00:32 +02:00
exports.default = httpProxyClient;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });