Skip to content

Commit ce7df0f

Browse files
feat(NODE-3689): require hello command for connection handshake to use OP_MSG disallowing OP_QUERY (#3938)
Co-authored-by: Durran Jordan <[email protected]>
1 parent 130691d commit ce7df0f

File tree

9 files changed

+108
-123
lines changed

9 files changed

+108
-123
lines changed

src/sdam/topology.ts

+4
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,10 @@ export class Topology extends TypedEventEmitter<TopologyEvents> {
382382
return this.s.options.loadBalanced;
383383
}
384384

385+
get serverApi(): ServerApi | undefined {
386+
return this.s.options.serverApi;
387+
}
388+
385389
get capabilities(): ServerCapabilities {
386390
return new ServerCapabilities(this.lastHello());
387391
}

src/utils.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -371,11 +371,13 @@ export function uuidV4(): Buffer {
371371
*/
372372
export function maxWireVersion(topologyOrServer?: Connection | Topology | Server): number {
373373
if (topologyOrServer) {
374-
if (topologyOrServer.loadBalanced) {
375-
// Since we do not have a monitor, we assume the load balanced server is always
376-
// pointed at the latest mongodb version. There is a risk that for on-prem
377-
// deployments that don't upgrade immediately that this could alert to the
374+
if (topologyOrServer.loadBalanced || topologyOrServer.serverApi?.version) {
375+
// Since we do not have a monitor in the load balanced mode,
376+
// we assume the load-balanced server is always pointed at the latest mongodb version.
377+
// There is a risk that for on-prem deployments
378+
// that don't upgrade immediately that this could alert to the
378379
// application that a feature is available that is actually not.
380+
// We also return the max supported wire version for serverAPI.
379381
return MAX_SUPPORTED_WIRE_VERSION;
380382
}
381383
if (topologyOrServer.hello) {

test/integration/change-streams/change_stream.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1262,7 +1262,9 @@ describe('Change Streams', function () {
12621262
}
12631263
req.reply({ ok: 1 });
12641264
});
1265-
const client = this.configuration.newClient(`mongodb://${mockServer.uri()}/`);
1265+
const client = this.configuration.newClient(`mongodb://${mockServer.uri()}/`, {
1266+
serverApi: null // TODO(NODE-3807): remove resetting serverApi when the usage of mongodb mock server is removed
1267+
});
12661268
client.connect(err => {
12671269
expect(err).to.not.exist;
12681270
const collection = client.db('cs').collection('test');

test/integration/change-streams/change_streams.prose.test.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,10 @@ describe('Change Stream prose tests', function () {
332332
}
333333
request.reply(this.applyOpTime(response));
334334
});
335-
this.client = this.config.newClient(this.mongodbURI, { monitorCommands: true });
335+
this.client = this.config.newClient(this.mongodbURI, {
336+
monitorCommands: true,
337+
serverApi: null // TODO(NODE-3807): remove resetting serverApi when the usage of mongodb mock server is removed
338+
});
336339
this.apm = { started: [], succeeded: [], failed: [] };
337340

338341
(

test/integration/collection-management/collection.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,9 @@ describe('Collection', function () {
474474
afterEach(() => mock.cleanup());
475475

476476
function testCountDocMock(testConfiguration, config, done) {
477-
const client = testConfiguration.newClient(`mongodb://${server.uri()}/test`);
477+
const client = testConfiguration.newClient(`mongodb://${server.uri()}/test`, {
478+
serverApi: null // TODO(NODE-3807): remove resetting serverApi when the usage of mongodb mock server is removed
479+
});
478480
const close = e => client.close(() => done(e));
479481

480482
server.setMessageHandler(request => {

test/integration/max-staleness/max_staleness.test.js

+11-4
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ describe('Max Staleness', function () {
5454
var self = this;
5555
const configuration = this.configuration;
5656
const client = configuration.newClient(
57-
`mongodb://${test.server.uri()}/test?readPreference=secondary&maxStalenessSeconds=250`
57+
`mongodb://${test.server.uri()}/test?readPreference=secondary&maxStalenessSeconds=250`,
58+
{ serverApi: null } // TODO(NODE-3807): remove resetting serverApi when the usage of mongodb mock server is removed
5859
);
5960

6061
client.connect(function (err, client) {
@@ -86,7 +87,9 @@ describe('Max Staleness', function () {
8687

8788
test: function (done) {
8889
const configuration = this.configuration;
89-
const client = configuration.newClient(`mongodb://${test.server.uri()}/test`);
90+
const client = configuration.newClient(`mongodb://${test.server.uri()}/test`, {
91+
serverApi: null // TODO(NODE-3807): remove resetting serverApi when the usage of mongodb mock server is removed
92+
});
9093
client.connect(function (err, client) {
9194
expect(err).to.not.exist;
9295

@@ -124,7 +127,9 @@ describe('Max Staleness', function () {
124127
test: function (done) {
125128
var self = this;
126129
const configuration = this.configuration;
127-
const client = configuration.newClient(`mongodb://${test.server.uri()}/test`);
130+
const client = configuration.newClient(`mongodb://${test.server.uri()}/test`, {
131+
serverApi: null // TODO(NODE-3807): remove resetting serverApi when the usage of mongodb mock server is removed
132+
});
128133
client.connect(function (err, client) {
129134
expect(err).to.not.exist;
130135
var db = client.db(self.configuration.db);
@@ -159,7 +164,9 @@ describe('Max Staleness', function () {
159164
test: function (done) {
160165
var self = this;
161166
const configuration = this.configuration;
162-
const client = configuration.newClient(`mongodb://${test.server.uri()}/test`);
167+
const client = configuration.newClient(`mongodb://${test.server.uri()}/test`, {
168+
serverApi: null // TODO(NODE-3807): remove resetting serverApi when the usage of mongodb mock server is removed
169+
});
163170
client.connect(function (err, client) {
164171
expect(err).to.not.exist;
165172
var db = client.db(self.configuration.db);

test/integration/mongodb-handshake/mongodb-handshake.test.ts

+74-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import {
66
Connection,
77
LEGACY_HELLO_COMMAND,
88
MongoServerError,
9-
MongoServerSelectionError
9+
MongoServerSelectionError,
10+
OpMsgRequest,
11+
OpQueryRequest,
12+
ServerApiVersion
1013
} from '../../mongodb';
1114

1215
describe('MongoDB Handshake', () => {
@@ -48,6 +51,7 @@ describe('MongoDB Handshake', () => {
4851

4952
context('when compressors are provided on the mongo client', () => {
5053
let spy: Sinon.SinonSpy;
54+
5155
before(() => {
5256
spy = sinon.spy(Connection.prototype, 'command');
5357
});
@@ -56,10 +60,78 @@ describe('MongoDB Handshake', () => {
5660

5761
it('constructs a handshake with the specified compressors', async function () {
5862
client = this.configuration.newClient({ compressors: ['snappy'] });
59-
await client.connect();
63+
// The load-balanced mode doesn’t perform SDAM,
64+
// so `connect` doesn’t do anything unless authentication is enabled.
65+
// Force the driver to send a command to the server in the noauth mode.
66+
await client.db('admin').command({ ping: 1 });
6067
expect(spy.called).to.be.true;
6168
const handshakeDoc = spy.getCall(0).args[1];
6269
expect(handshakeDoc).to.have.property('compression').to.deep.equal(['snappy']);
6370
});
6471
});
72+
73+
context('when load-balanced', function () {
74+
let opMsgRequestToBinSpy: Sinon.SinonSpy;
75+
76+
beforeEach(() => {
77+
opMsgRequestToBinSpy = sinon.spy(OpMsgRequest.prototype, 'toBin');
78+
});
79+
80+
afterEach(() => sinon.restore());
81+
82+
it('sends the hello command as OP_MSG', {
83+
metadata: { requires: { topology: 'load-balanced' } },
84+
test: async function () {
85+
client = this.configuration.newClient({ loadBalanced: true });
86+
await client.db('admin').command({ ping: 1 });
87+
expect(opMsgRequestToBinSpy).to.have.been.called;
88+
}
89+
});
90+
});
91+
92+
context('when serverApi version is present', function () {
93+
let opMsgRequestToBinSpy: Sinon.SinonSpy;
94+
95+
beforeEach(() => {
96+
opMsgRequestToBinSpy = sinon.spy(OpMsgRequest.prototype, 'toBin');
97+
});
98+
99+
afterEach(() => sinon.restore());
100+
101+
it('sends the hello command as OP_MSG', {
102+
metadata: { requires: { topology: '!load-balanced', mongodb: '>=5.0' } },
103+
test: async function () {
104+
client = this.configuration.newClient({}, { serverApi: { version: ServerApiVersion.v1 } });
105+
await client.connect();
106+
expect(opMsgRequestToBinSpy).to.have.been.called;
107+
}
108+
});
109+
});
110+
111+
context('when not load-balanced and serverApi version is not present', function () {
112+
let opQueryRequestToBinSpy: Sinon.SinonSpy;
113+
let opMsgRequestToBinSpy: Sinon.SinonSpy;
114+
115+
beforeEach(() => {
116+
opQueryRequestToBinSpy = sinon.spy(OpQueryRequest.prototype, 'toBin');
117+
opMsgRequestToBinSpy = sinon.spy(OpMsgRequest.prototype, 'toBin');
118+
});
119+
120+
afterEach(() => sinon.restore());
121+
122+
it('sends the hello command as OP_MSG', {
123+
metadata: { requires: { topology: '!load-balanced', mongodb: '>=5.0' } },
124+
test: async function () {
125+
if (this.configuration.serverApi) {
126+
this.skipReason = 'Test requires serverApi to NOT be enabled';
127+
return this.skip();
128+
}
129+
client = this.configuration.newClient();
130+
await client.db('admin').command({ ping: 1 });
131+
expect(opQueryRequestToBinSpy).to.have.been.called;
132+
expect(opMsgRequestToBinSpy).to.have.been.called;
133+
opMsgRequestToBinSpy.calledAfter(opQueryRequestToBinSpy);
134+
}
135+
});
136+
});
65137
});

test/readme.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ The following steps will walk you through how to start and test a load balancer.
330330
A new file name `lb.env` is automatically created.
331331
1. Source the environment variables using a command like `source lb.env`.
332332
1. Export **each** of the environment variables that were created in `lb.env`. For example: `export SINGLE_MONGOS_LB_URI`.
333-
1. Export the `LOAD_BALANCED` environment variable to 'true': `export LOAD_BALANCED='true'`
333+
1. Export the `LOAD_BALANCER` environment variable to 'true': `export LOAD_BALANCER='true'`
334334
1. Disable auth for tests: `export AUTH='noauth'`
335335
1. Run the test suite as you normally would:
336336
```sh

test/unit/cmap/connection.test.ts

+2-109
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,13 @@ import {
1414
type HostAddress,
1515
isHello,
1616
type MessageHeader,
17-
MessageStream,
17+
type MessageStream,
1818
MongoNetworkError,
1919
MongoNetworkTimeoutError,
2020
MongoRuntimeError,
2121
ns,
2222
type OperationDescription,
23-
OpMsgRequest,
24-
OpMsgResponse,
25-
OpQueryRequest
23+
OpMsgResponse
2624
} from '../../mongodb';
2725
import * as mock from '../../tools/mongodb-mock/index';
2826
import { generateOpMsgBuffer, getSymbolFrom } from '../../tools/utils';
@@ -1030,109 +1028,4 @@ describe('new Connection()', function () {
10301028
});
10311029
});
10321030
});
1033-
1034-
describe('when load-balanced', () => {
1035-
const CONNECT_DEFAULTS = {
1036-
id: 1,
1037-
tls: false,
1038-
generation: 1,
1039-
monitorCommands: false,
1040-
metadata: {} as ClientMetadata
1041-
};
1042-
let server;
1043-
let connectOptions;
1044-
let connection: Connection;
1045-
let writeCommandSpy;
1046-
1047-
beforeEach(async () => {
1048-
server = await mock.createServer();
1049-
server.setMessageHandler(request => {
1050-
request.reply(mock.HELLO);
1051-
});
1052-
writeCommandSpy = sinon.spy(MessageStream.prototype, 'writeCommand');
1053-
});
1054-
1055-
afterEach(async () => {
1056-
connection?.destroy({ force: true });
1057-
sinon.restore();
1058-
await mock.cleanup();
1059-
});
1060-
1061-
it('sends the first command as OP_MSG', async () => {
1062-
try {
1063-
connectOptions = {
1064-
...CONNECT_DEFAULTS,
1065-
hostAddress: server.hostAddress() as HostAddress,
1066-
socketTimeoutMS: 100,
1067-
loadBalanced: true
1068-
};
1069-
1070-
connection = await promisify<Connection>(callback =>
1071-
//@ts-expect-error: Callbacks do not have mutual exclusion for error/result existence
1072-
connect(connectOptions, callback)
1073-
)();
1074-
1075-
await promisify(callback =>
1076-
connection.command(ns('admin.$cmd'), { hello: 1 }, {}, callback)
1077-
)();
1078-
} catch (error) {
1079-
/** Connection timeouts, but the handshake message is sent. */
1080-
}
1081-
1082-
expect(writeCommandSpy).to.have.been.called;
1083-
expect(writeCommandSpy.firstCall.args[0] instanceof OpMsgRequest).to.equal(true);
1084-
});
1085-
});
1086-
1087-
describe('when not load-balanced', () => {
1088-
const CONNECT_DEFAULTS = {
1089-
id: 1,
1090-
tls: false,
1091-
generation: 1,
1092-
monitorCommands: false,
1093-
metadata: {} as ClientMetadata
1094-
};
1095-
let server;
1096-
let connectOptions;
1097-
let connection: Connection;
1098-
let writeCommandSpy;
1099-
1100-
beforeEach(async () => {
1101-
server = await mock.createServer();
1102-
server.setMessageHandler(request => {
1103-
request.reply(mock.HELLO);
1104-
});
1105-
writeCommandSpy = sinon.spy(MessageStream.prototype, 'writeCommand');
1106-
});
1107-
1108-
afterEach(async () => {
1109-
connection?.destroy({ force: true });
1110-
sinon.restore();
1111-
await mock.cleanup();
1112-
});
1113-
1114-
it('sends the first command as OP_QUERY', async () => {
1115-
try {
1116-
connectOptions = {
1117-
...CONNECT_DEFAULTS,
1118-
hostAddress: server.hostAddress() as HostAddress,
1119-
socketTimeoutMS: 100
1120-
};
1121-
1122-
connection = await promisify<Connection>(callback =>
1123-
//@ts-expect-error: Callbacks do not have mutual exclusion for error/result existence
1124-
connect(connectOptions, callback)
1125-
)();
1126-
1127-
await promisify(callback =>
1128-
connection.command(ns('admin.$cmd'), { hello: 1 }, {}, callback)
1129-
)();
1130-
} catch (error) {
1131-
/** Connection timeouts, but the handshake message is sent. */
1132-
}
1133-
1134-
expect(writeCommandSpy).to.have.been.called;
1135-
expect(writeCommandSpy.firstCall.args[0] instanceof OpQueryRequest).to.equal(true);
1136-
});
1137-
});
11381031
});

0 commit comments

Comments
 (0)