You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
365 lines
15 KiB
365 lines
15 KiB
3 months ago
|
"use strict";
|
||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||
|
exports.LEGAL_TCP_SOCKET_OPTIONS = exports.LEGAL_TLS_SOCKET_OPTIONS = void 0;
|
||
|
exports.connect = connect;
|
||
|
exports.makeConnection = makeConnection;
|
||
|
exports.performInitialHandshake = performInitialHandshake;
|
||
|
exports.prepareHandshakeDocument = prepareHandshakeDocument;
|
||
|
exports.makeSocket = makeSocket;
|
||
|
const net = require("net");
|
||
|
const tls = require("tls");
|
||
|
const constants_1 = require("../constants");
|
||
|
const deps_1 = require("../deps");
|
||
|
const error_1 = require("../error");
|
||
|
const utils_1 = require("../utils");
|
||
|
const auth_provider_1 = require("./auth/auth_provider");
|
||
|
const providers_1 = require("./auth/providers");
|
||
|
const connection_1 = require("./connection");
|
||
|
const constants_2 = require("./wire_protocol/constants");
|
||
|
async function connect(options) {
|
||
|
let connection = null;
|
||
|
try {
|
||
|
const socket = await makeSocket(options);
|
||
|
connection = makeConnection(options, socket);
|
||
|
await performInitialHandshake(connection, options);
|
||
|
return connection;
|
||
|
}
|
||
|
catch (error) {
|
||
|
connection?.destroy();
|
||
|
throw error;
|
||
|
}
|
||
|
}
|
||
|
function makeConnection(options, socket) {
|
||
|
let ConnectionType = options.connectionType ?? connection_1.Connection;
|
||
|
if (options.autoEncrypter) {
|
||
|
ConnectionType = connection_1.CryptoConnection;
|
||
|
}
|
||
|
return new ConnectionType(socket, options);
|
||
|
}
|
||
|
function checkSupportedServer(hello, options) {
|
||
|
const maxWireVersion = Number(hello.maxWireVersion);
|
||
|
const minWireVersion = Number(hello.minWireVersion);
|
||
|
const serverVersionHighEnough = !Number.isNaN(maxWireVersion) && maxWireVersion >= constants_2.MIN_SUPPORTED_WIRE_VERSION;
|
||
|
const serverVersionLowEnough = !Number.isNaN(minWireVersion) && minWireVersion <= constants_2.MAX_SUPPORTED_WIRE_VERSION;
|
||
|
if (serverVersionHighEnough) {
|
||
|
if (serverVersionLowEnough) {
|
||
|
return null;
|
||
|
}
|
||
|
const message = `Server at ${options.hostAddress} reports minimum wire version ${JSON.stringify(hello.minWireVersion)}, but this version of the Node.js Driver requires at most ${constants_2.MAX_SUPPORTED_WIRE_VERSION} (MongoDB ${constants_2.MAX_SUPPORTED_SERVER_VERSION})`;
|
||
|
return new error_1.MongoCompatibilityError(message);
|
||
|
}
|
||
|
const message = `Server at ${options.hostAddress} reports maximum wire version ${JSON.stringify(hello.maxWireVersion) ?? 0}, but this version of the Node.js Driver requires at least ${constants_2.MIN_SUPPORTED_WIRE_VERSION} (MongoDB ${constants_2.MIN_SUPPORTED_SERVER_VERSION})`;
|
||
|
return new error_1.MongoCompatibilityError(message);
|
||
|
}
|
||
|
async function performInitialHandshake(conn, options) {
|
||
|
const credentials = options.credentials;
|
||
|
if (credentials) {
|
||
|
if (!(credentials.mechanism === providers_1.AuthMechanism.MONGODB_DEFAULT) &&
|
||
|
!options.authProviders.getOrCreateProvider(credentials.mechanism, credentials.mechanismProperties)) {
|
||
|
throw new error_1.MongoInvalidArgumentError(`AuthMechanism '${credentials.mechanism}' not supported`);
|
||
|
}
|
||
|
}
|
||
|
const authContext = new auth_provider_1.AuthContext(conn, credentials, options);
|
||
|
conn.authContext = authContext;
|
||
|
const handshakeDoc = await prepareHandshakeDocument(authContext);
|
||
|
// @ts-expect-error: TODO(NODE-5141): The options need to be filtered properly, Connection options differ from Command options
|
||
|
const handshakeOptions = { ...options, raw: false };
|
||
|
if (typeof options.connectTimeoutMS === 'number') {
|
||
|
// The handshake technically is a monitoring check, so its socket timeout should be connectTimeoutMS
|
||
|
handshakeOptions.socketTimeoutMS = options.connectTimeoutMS;
|
||
|
}
|
||
|
const start = new Date().getTime();
|
||
|
const response = await executeHandshake(handshakeDoc, handshakeOptions);
|
||
|
if (!('isWritablePrimary' in response)) {
|
||
|
// Provide hello-style response document.
|
||
|
response.isWritablePrimary = response[constants_1.LEGACY_HELLO_COMMAND];
|
||
|
}
|
||
|
if (response.helloOk) {
|
||
|
conn.helloOk = true;
|
||
|
}
|
||
|
const supportedServerErr = checkSupportedServer(response, options);
|
||
|
if (supportedServerErr) {
|
||
|
throw supportedServerErr;
|
||
|
}
|
||
|
if (options.loadBalanced) {
|
||
|
if (!response.serviceId) {
|
||
|
throw new error_1.MongoCompatibilityError('Driver attempted to initialize in load balancing mode, ' +
|
||
|
'but the server does not support this mode.');
|
||
|
}
|
||
|
}
|
||
|
// NOTE: This is metadata attached to the connection while porting away from
|
||
|
// handshake being done in the `Server` class. Likely, it should be
|
||
|
// relocated, or at very least restructured.
|
||
|
conn.hello = response;
|
||
|
conn.lastHelloMS = new Date().getTime() - start;
|
||
|
if (!response.arbiterOnly && credentials) {
|
||
|
// store the response on auth context
|
||
|
authContext.response = response;
|
||
|
const resolvedCredentials = credentials.resolveAuthMechanism(response);
|
||
|
const provider = options.authProviders.getOrCreateProvider(resolvedCredentials.mechanism, resolvedCredentials.mechanismProperties);
|
||
|
if (!provider) {
|
||
|
throw new error_1.MongoInvalidArgumentError(`No AuthProvider for ${resolvedCredentials.mechanism} defined.`);
|
||
|
}
|
||
|
try {
|
||
|
await provider.auth(authContext);
|
||
|
}
|
||
|
catch (error) {
|
||
|
if (error instanceof error_1.MongoError) {
|
||
|
error.addErrorLabel(error_1.MongoErrorLabel.HandshakeError);
|
||
|
if ((0, error_1.needsRetryableWriteLabel)(error, response.maxWireVersion, conn.description.type)) {
|
||
|
error.addErrorLabel(error_1.MongoErrorLabel.RetryableWriteError);
|
||
|
}
|
||
|
}
|
||
|
throw error;
|
||
|
}
|
||
|
}
|
||
|
// Connection establishment is socket creation (tcp handshake, tls handshake, MongoDB handshake (saslStart, saslContinue))
|
||
|
// Once connection is established, command logging can log events (if enabled)
|
||
|
conn.established = true;
|
||
|
async function executeHandshake(handshakeDoc, handshakeOptions) {
|
||
|
try {
|
||
|
const handshakeResponse = await conn.command((0, utils_1.ns)('admin.$cmd'), handshakeDoc, handshakeOptions);
|
||
|
return handshakeResponse;
|
||
|
}
|
||
|
catch (error) {
|
||
|
if (error instanceof error_1.MongoError) {
|
||
|
error.addErrorLabel(error_1.MongoErrorLabel.HandshakeError);
|
||
|
}
|
||
|
throw error;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* @internal
|
||
|
*
|
||
|
* This function is only exposed for testing purposes.
|
||
|
*/
|
||
|
async function prepareHandshakeDocument(authContext) {
|
||
|
const options = authContext.options;
|
||
|
const compressors = options.compressors ? options.compressors : [];
|
||
|
const { serverApi } = authContext.connection;
|
||
|
const clientMetadata = await options.extendedMetadata;
|
||
|
const handshakeDoc = {
|
||
|
[serverApi?.version || options.loadBalanced === true ? 'hello' : constants_1.LEGACY_HELLO_COMMAND]: 1,
|
||
|
helloOk: true,
|
||
|
client: clientMetadata,
|
||
|
compression: compressors
|
||
|
};
|
||
|
if (options.loadBalanced === true) {
|
||
|
handshakeDoc.loadBalanced = true;
|
||
|
}
|
||
|
const credentials = authContext.credentials;
|
||
|
if (credentials) {
|
||
|
if (credentials.mechanism === providers_1.AuthMechanism.MONGODB_DEFAULT && credentials.username) {
|
||
|
handshakeDoc.saslSupportedMechs = `${credentials.source}.${credentials.username}`;
|
||
|
const provider = authContext.options.authProviders.getOrCreateProvider(providers_1.AuthMechanism.MONGODB_SCRAM_SHA256, credentials.mechanismProperties);
|
||
|
if (!provider) {
|
||
|
// This auth mechanism is always present.
|
||
|
throw new error_1.MongoInvalidArgumentError(`No AuthProvider for ${providers_1.AuthMechanism.MONGODB_SCRAM_SHA256} defined.`);
|
||
|
}
|
||
|
return await provider.prepare(handshakeDoc, authContext);
|
||
|
}
|
||
|
const provider = authContext.options.authProviders.getOrCreateProvider(credentials.mechanism, credentials.mechanismProperties);
|
||
|
if (!provider) {
|
||
|
throw new error_1.MongoInvalidArgumentError(`No AuthProvider for ${credentials.mechanism} defined.`);
|
||
|
}
|
||
|
return await provider.prepare(handshakeDoc, authContext);
|
||
|
}
|
||
|
return handshakeDoc;
|
||
|
}
|
||
|
/** @public */
|
||
|
exports.LEGAL_TLS_SOCKET_OPTIONS = [
|
||
|
'allowPartialTrustChain',
|
||
|
'ALPNProtocols',
|
||
|
'ca',
|
||
|
'cert',
|
||
|
'checkServerIdentity',
|
||
|
'ciphers',
|
||
|
'crl',
|
||
|
'ecdhCurve',
|
||
|
'key',
|
||
|
'minDHSize',
|
||
|
'passphrase',
|
||
|
'pfx',
|
||
|
'rejectUnauthorized',
|
||
|
'secureContext',
|
||
|
'secureProtocol',
|
||
|
'servername',
|
||
|
'session'
|
||
|
];
|
||
|
/** @public */
|
||
|
exports.LEGAL_TCP_SOCKET_OPTIONS = [
|
||
|
'autoSelectFamily',
|
||
|
'autoSelectFamilyAttemptTimeout',
|
||
|
'family',
|
||
|
'hints',
|
||
|
'localAddress',
|
||
|
'localPort',
|
||
|
'lookup'
|
||
|
];
|
||
|
function parseConnectOptions(options) {
|
||
|
const hostAddress = options.hostAddress;
|
||
|
if (!hostAddress)
|
||
|
throw new error_1.MongoInvalidArgumentError('Option "hostAddress" is required');
|
||
|
const result = {};
|
||
|
for (const name of exports.LEGAL_TCP_SOCKET_OPTIONS) {
|
||
|
if (options[name] != null) {
|
||
|
result[name] = options[name];
|
||
|
}
|
||
|
}
|
||
|
if (typeof hostAddress.socketPath === 'string') {
|
||
|
result.path = hostAddress.socketPath;
|
||
|
return result;
|
||
|
}
|
||
|
else if (typeof hostAddress.host === 'string') {
|
||
|
result.host = hostAddress.host;
|
||
|
result.port = hostAddress.port;
|
||
|
return result;
|
||
|
}
|
||
|
else {
|
||
|
// This should never happen since we set up HostAddresses
|
||
|
// But if we don't throw here the socket could hang until timeout
|
||
|
// TODO(NODE-3483)
|
||
|
throw new error_1.MongoRuntimeError(`Unexpected HostAddress ${JSON.stringify(hostAddress)}`);
|
||
|
}
|
||
|
}
|
||
|
function parseSslOptions(options) {
|
||
|
const result = parseConnectOptions(options);
|
||
|
// Merge in valid SSL options
|
||
|
for (const name of exports.LEGAL_TLS_SOCKET_OPTIONS) {
|
||
|
if (options[name] != null) {
|
||
|
result[name] = options[name];
|
||
|
}
|
||
|
}
|
||
|
if (options.existingSocket) {
|
||
|
result.socket = options.existingSocket;
|
||
|
}
|
||
|
// Set default sni servername to be the same as host
|
||
|
if (result.servername == null && result.host && !net.isIP(result.host)) {
|
||
|
result.servername = result.host;
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
async function makeSocket(options) {
|
||
|
const useTLS = options.tls ?? false;
|
||
|
const noDelay = options.noDelay ?? true;
|
||
|
const connectTimeoutMS = options.connectTimeoutMS ?? 30000;
|
||
|
const existingSocket = options.existingSocket;
|
||
|
let socket;
|
||
|
if (options.proxyHost != null) {
|
||
|
// Currently, only Socks5 is supported.
|
||
|
return await makeSocks5Connection({
|
||
|
...options,
|
||
|
connectTimeoutMS // Should always be present for Socks5
|
||
|
});
|
||
|
}
|
||
|
if (useTLS) {
|
||
|
const tlsSocket = tls.connect(parseSslOptions(options));
|
||
|
if (typeof tlsSocket.disableRenegotiation === 'function') {
|
||
|
tlsSocket.disableRenegotiation();
|
||
|
}
|
||
|
socket = tlsSocket;
|
||
|
}
|
||
|
else if (existingSocket) {
|
||
|
// In the TLS case, parseSslOptions() sets options.socket to existingSocket,
|
||
|
// so we only need to handle the non-TLS case here (where existingSocket
|
||
|
// gives us all we need out of the box).
|
||
|
socket = existingSocket;
|
||
|
}
|
||
|
else {
|
||
|
socket = net.createConnection(parseConnectOptions(options));
|
||
|
}
|
||
|
socket.setKeepAlive(true, 300000);
|
||
|
socket.setTimeout(connectTimeoutMS);
|
||
|
socket.setNoDelay(noDelay);
|
||
|
let cancellationHandler = null;
|
||
|
const { promise: connectedSocket, resolve, reject } = (0, utils_1.promiseWithResolvers)();
|
||
|
if (existingSocket) {
|
||
|
resolve(socket);
|
||
|
}
|
||
|
else {
|
||
|
const start = performance.now();
|
||
|
const connectEvent = useTLS ? 'secureConnect' : 'connect';
|
||
|
socket
|
||
|
.once(connectEvent, () => resolve(socket))
|
||
|
.once('error', cause => reject(new error_1.MongoNetworkError(error_1.MongoError.buildErrorMessage(cause), { cause })))
|
||
|
.once('timeout', () => {
|
||
|
reject(new error_1.MongoNetworkTimeoutError(`Socket '${connectEvent}' timed out after ${(performance.now() - start) | 0}ms (connectTimeoutMS: ${connectTimeoutMS})`));
|
||
|
})
|
||
|
.once('close', () => reject(new error_1.MongoNetworkError(`Socket closed after ${(performance.now() - start) | 0} during connection establishment`)));
|
||
|
if (options.cancellationToken != null) {
|
||
|
cancellationHandler = () => reject(new error_1.MongoNetworkError(`Socket connection establishment was cancelled after ${(performance.now() - start) | 0}`));
|
||
|
options.cancellationToken.once('cancel', cancellationHandler);
|
||
|
}
|
||
|
}
|
||
|
try {
|
||
|
socket = await connectedSocket;
|
||
|
return socket;
|
||
|
}
|
||
|
catch (error) {
|
||
|
socket.destroy();
|
||
|
throw error;
|
||
|
}
|
||
|
finally {
|
||
|
socket.setTimeout(0);
|
||
|
if (cancellationHandler != null) {
|
||
|
options.cancellationToken?.removeListener('cancel', cancellationHandler);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
let socks = null;
|
||
|
function loadSocks() {
|
||
|
if (socks == null) {
|
||
|
const socksImport = (0, deps_1.getSocks)();
|
||
|
if ('kModuleError' in socksImport) {
|
||
|
throw socksImport.kModuleError;
|
||
|
}
|
||
|
socks = socksImport;
|
||
|
}
|
||
|
return socks;
|
||
|
}
|
||
|
async function makeSocks5Connection(options) {
|
||
|
const hostAddress = utils_1.HostAddress.fromHostPort(options.proxyHost ?? '', // proxyHost is guaranteed to set here
|
||
|
options.proxyPort ?? 1080);
|
||
|
// First, connect to the proxy server itself:
|
||
|
const rawSocket = await makeSocket({
|
||
|
...options,
|
||
|
hostAddress,
|
||
|
tls: false,
|
||
|
proxyHost: undefined
|
||
|
});
|
||
|
const destination = parseConnectOptions(options);
|
||
|
if (typeof destination.host !== 'string' || typeof destination.port !== 'number') {
|
||
|
throw new error_1.MongoInvalidArgumentError('Can only make Socks5 connections to TCP hosts');
|
||
|
}
|
||
|
socks ??= loadSocks();
|
||
|
let existingSocket;
|
||
|
try {
|
||
|
// Then, establish the Socks5 proxy connection:
|
||
|
const connection = await socks.SocksClient.createConnection({
|
||
|
existing_socket: rawSocket,
|
||
|
timeout: options.connectTimeoutMS,
|
||
|
command: 'connect',
|
||
|
destination: {
|
||
|
host: destination.host,
|
||
|
port: destination.port
|
||
|
},
|
||
|
proxy: {
|
||
|
// host and port are ignored because we pass existing_socket
|
||
|
host: 'iLoveJavaScript',
|
||
|
port: 0,
|
||
|
type: 5,
|
||
|
userId: options.proxyUsername || undefined,
|
||
|
password: options.proxyPassword || undefined
|
||
|
}
|
||
|
});
|
||
|
existingSocket = connection.socket;
|
||
|
}
|
||
|
catch (cause) {
|
||
|
throw new error_1.MongoNetworkError(error_1.MongoError.buildErrorMessage(cause), { cause });
|
||
|
}
|
||
|
// Finally, now treat the resulting duplex stream as the
|
||
|
// socket over which we send and receive wire protocol messages:
|
||
|
return await makeSocket({ ...options, existingSocket, proxyHost: undefined });
|
||
|
}
|
||
|
//# sourceMappingURL=connect.js.map
|