Skip to content

Commit

Permalink
fix(NODE-5161): metadata duplication in handshake (#3628)
Browse files Browse the repository at this point in the history
Co-authored-by: Bailey Pearson <bailey.pearson@mongodb.com>
  • Loading branch information
nbbeeken and baileympearson committed Apr 7, 2023
1 parent 49fa638 commit b790142
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 84 deletions.
9 changes: 6 additions & 3 deletions src/cmap/auth/auth_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ import type { MongoCredentials } from './mongo_credentials';

export type AuthContextOptions = ConnectionOptions & ClientMetadataOptions;

/** Context used during authentication */
/**
* Context used during authentication
* @internal
*/
export class AuthContext {
/** The connection to authenticate */
connection: Connection;
/** The credentials to use for authentication */
credentials?: MongoCredentials;
/** The options passed to the `connect` method */
options: AuthContextOptions;
options: ConnectionOptions;

/** A response from an initial auth attempt, only some mechanisms use this (e.g, SCRAM) */
response?: Document;
Expand All @@ -24,7 +27,7 @@ export class AuthContext {
constructor(
connection: Connection,
credentials: MongoCredentials | undefined,
options: AuthContextOptions
options: ConnectionOptions
) {
this.connection = connection;
this.credentials = credentials;
Expand Down
4 changes: 2 additions & 2 deletions src/cmap/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
MongoServerError,
needsRetryableWriteLabel
} from '../error';
import { Callback, ClientMetadata, HostAddress, makeClientMetadata, ns } from '../utils';
import { Callback, ClientMetadata, HostAddress, ns } from '../utils';
import { AuthContext, AuthProvider } from './auth/auth_provider';
import { GSSAPI } from './auth/gssapi';
import { MongoCR } from './auth/mongocr';
Expand Down Expand Up @@ -233,7 +233,7 @@ export function prepareHandshakeDocument(
const handshakeDoc: HandshakeDocument = {
[serverApi?.version ? 'hello' : LEGACY_HELLO_COMMAND]: true,
helloOk: true,
client: options.metadata || makeClientMetadata(options),
client: options.metadata,
compression: compressors
};

Expand Down
19 changes: 5 additions & 14 deletions src/connection_string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
} from './error';
import { Logger as LegacyLogger, LoggerLevel as LegacyLoggerLevel } from './logger';
import {
DriverInfo,
MongoClient,
MongoClientOptions,
MongoOptions,
Expand Down Expand Up @@ -534,6 +533,8 @@ export function parseOptions(
loggerClientOptions
);

mongoOptions.metadata = makeClientMetadata(mongoOptions);

return mongoOptions;
}

Expand Down Expand Up @@ -635,10 +636,7 @@ interface OptionDescriptor {

export const OPTIONS = {
appName: {
target: 'metadata',
transform({ options, values: [value] }): DriverInfo {
return makeClientMetadata({ ...options.driverInfo, appName: String(value) });
}
type: 'string'
},
auth: {
target: 'credentials',
Expand Down Expand Up @@ -784,15 +782,8 @@ export const OPTIONS = {
type: 'boolean'
},
driverInfo: {
target: 'metadata',
default: makeClientMetadata(),
transform({ options, values: [value] }) {
if (!isRecord(value)) throw new MongoParseError('DriverInfo must be an object');
return makeClientMetadata({
driverInfo: value,
appName: options.metadata?.application?.name
});
}
default: {},
type: 'record'
},
enableUtf8Validation: { type: 'boolean', default: true },
family: {
Expand Down
1 change: 1 addition & 0 deletions src/mongo_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,7 @@ export interface MongoOptions
>
>,
SupportedNodeConnectionOptions {
appName?: string;
hosts: HostAddress[];
srvHost?: string;
credentials?: MongoCredentials;
Expand Down
50 changes: 23 additions & 27 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
MongoRuntimeError
} from './error';
import type { Explain } from './explain';
import type { MongoClient } from './mongo_client';
import type { MongoClient, MongoOptions } from './mongo_client';
import type { CommandOperationOptions, OperationParent } from './operations/command';
import type { Hint, OperationOptions } from './operations/operation';
import { PromiseProvider } from './promise_provider';
Expand Down Expand Up @@ -657,7 +657,10 @@ export function makeStateMachine(stateTable: StateTable): StateTransitionFunctio
};
}

/** @public */
/**
* @public
* @see https://github.com/mongodb/specifications/blob/master/source/mongodb-handshake/handshake.rst#hello-command
*/
export interface ClientMetadata {
driver: {
name: string;
Expand All @@ -670,7 +673,6 @@ export interface ClientMetadata {
version: string;
};
platform: string;
version?: string;
application?: {
name: string;
};
Expand All @@ -689,44 +691,38 @@ export interface ClientMetadataOptions {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const NODE_DRIVER_VERSION = require('../package.json').version;

export function makeClientMetadata(options?: ClientMetadataOptions): ClientMetadata {
options = options ?? {};
export function makeClientMetadata(
options: Pick<MongoOptions, 'appName' | 'driverInfo'>
): ClientMetadata {
const name = options.driverInfo.name ? `nodejs|${options.driverInfo.name}` : 'nodejs';
const version = options.driverInfo.version
? `${NODE_DRIVER_VERSION}|${options.driverInfo.version}`
: NODE_DRIVER_VERSION;
const platform = options.driverInfo.platform
? `Node.js ${process.version}, ${os.endianness()}|${options.driverInfo.platform}`
: `Node.js ${process.version}, ${os.endianness()}`;

const metadata: ClientMetadata = {
driver: {
name: 'nodejs',
version: NODE_DRIVER_VERSION
name,
version
},
os: {
type: os.type(),
name: process.platform,
architecture: process.arch,
version: os.release()
},
platform: `Node.js ${process.version}, ${os.endianness()} (unified)`
platform
};

// support optionally provided wrapping driver info
if (options.driverInfo) {
if (options.driverInfo.name) {
metadata.driver.name = `${metadata.driver.name}|${options.driverInfo.name}`;
}

if (options.driverInfo.version) {
metadata.version = `${metadata.driver.version}|${options.driverInfo.version}`;
}

if (options.driverInfo.platform) {
metadata.platform = `${metadata.platform}|${options.driverInfo.platform}`;
}
}

if (options.appName) {
// MongoDB requires the appName not exceed a byte length of 128
const buffer = Buffer.from(options.appName);
metadata.application = {
name: buffer.byteLength > 128 ? buffer.slice(0, 128).toString('utf8') : options.appName
};
const name =
Buffer.byteLength(options.appName, 'utf8') <= 128
? options.appName
: Buffer.from(options.appName, 'utf8').subarray(0, 128).toString('utf8');
metadata.application = { name };
}

return metadata;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { connect } from '../../../src/cmap/connect';
import { Connection } from '../../../src/cmap/connection';
import { LEGACY_HELLO_COMMAND } from '../../../src/constants';
import { Topology } from '../../../src/sdam/topology';
import { ns } from '../../../src/utils';
import { makeClientMetadata, ns } from '../../../src/utils';
import { skipBrokenAuthTestBeforeEachHook } from '../../tools/runner/hooks/configuration';
import { assert as test, setupDatabase } from '../shared';

Expand All @@ -27,12 +27,13 @@ describe('Connection', function () {
it('should execute a command against a server', {
metadata: { requires: { apiVersion: false, topology: '!load-balanced' } },
test: function (done) {
const connectOptions = Object.assign(
{ connectionType: Connection },
this.configuration.options
);
const connectOptions: Partial<ConnectionOptions> = {
connectionType: Connection,
...this.configuration.options,
metadata: makeClientMetadata({ driverInfo: {} })
};

connect(connectOptions, (err, conn) => {
connect(connectOptions as any as ConnectionOptions, (err, conn) => {
expect(err).to.not.exist;
this.defer(_done => conn.destroy(_done));

Expand All @@ -49,12 +50,14 @@ describe('Connection', function () {
it('should emit command monitoring events', {
metadata: { requires: { apiVersion: false, topology: '!load-balanced' } },
test: function (done) {
const connectOptions = Object.assign(
{ connectionType: Connection, monitorCommands: true },
this.configuration.options
);

connect(connectOptions, (err, conn) => {
const connectOptions: Partial<ConnectionOptions> = {
connectionType: Connection,
monitorCommands: true,
...this.configuration.options,
metadata: makeClientMetadata({ driverInfo: {} })
};

connect(connectOptions as any as ConnectionOptions, (err, conn) => {
expect(err).to.not.exist;
this.defer(_done => conn.destroy(_done));

Expand All @@ -80,12 +83,13 @@ describe('Connection', function () {
},
test: function (done) {
const namespace = ns(`${this.configuration.db}.$cmd`);
const connectOptions = Object.assign(
{ connectionType: Connection },
this.configuration.options
);
const connectOptions: Partial<ConnectionOptions> = {
connectionType: Connection,
...this.configuration.options,
metadata: makeClientMetadata({ driverInfo: {} })
};

connect(connectOptions, (err, conn) => {
connect(connectOptions as any as ConnectionOptions, (err, conn) => {
expect(err).to.not.exist;
this.defer(_done => conn.destroy(_done));

Expand Down
5 changes: 4 additions & 1 deletion test/integration/node-specific/topology.test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
'use strict';
const { expect } = require('chai');
const { makeClientMetadata } = require('../../../src/utils');

describe('Topology', function () {
it('should correctly track states of a topology', {
metadata: { requires: { apiVersion: false, topology: '!load-balanced' } }, // apiVersion not supported by newTopology()
test: function (done) {
const topology = this.configuration.newTopology();
const topology = this.configuration.newTopology({
metadata: makeClientMetadata({ driverInfo: {} })
});

const states = [];
topology.on('stateChanged', (_, newState) => states.push(newState));
Expand Down
7 changes: 2 additions & 5 deletions test/tools/cmap_spec_runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,11 +363,8 @@ async function runCmapTest(test: CmapTest, threadContext: ThreadContext) {
delete poolOptions.backgroundThreadIntervalMS;
}

let metadata;
if (poolOptions.appName) {
metadata = makeClientMetadata({ appName: poolOptions.appName });
delete poolOptions.appName;
}
const metadata = makeClientMetadata({ appName: poolOptions.appName, driverInfo: {} });
delete poolOptions.appName;

const operations = test.operations;
const expectedError = test.error;
Expand Down

0 comments on commit b790142

Please sign in to comment.