diff --git a/package-lock.json b/package-lock.json index 96c0a173..9f44afd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30589,6 +30589,7 @@ "version": "1.7.4", "license": "Apache-2.0", "dependencies": { + "debug": "^4.4.0", "mongodb-connection-string-url": "^3.0.0" }, "devDependencies": { @@ -50739,6 +50740,7 @@ "@mongodb-js/tsconfig-devtools": "^1.0.4", "@types/mocha": "^9.1.1", "chai": "^4.5.0", + "debug": "^4.4.0", "depcheck": "^1.4.7", "eslint": "^7.25.0", "mocha": "^8.4.0", diff --git a/packages/mongodb-build-info/package.json b/packages/mongodb-build-info/package.json index 0c180a76..e804a73f 100644 --- a/packages/mongodb-build-info/package.json +++ b/packages/mongodb-build-info/package.json @@ -77,6 +77,7 @@ "typescript": "^5.0.4" }, "dependencies": { + "debug": "^4.4.0", "mongodb-connection-string-url": "^3.0.0" } } diff --git a/packages/mongodb-build-info/src/index.ts b/packages/mongodb-build-info/src/index.ts index ed75e9b6..dfde7a08 100644 --- a/packages/mongodb-build-info/src/index.ts +++ b/packages/mongodb-build-info/src/index.ts @@ -1,4 +1,9 @@ import ConnectionString from 'mongodb-connection-string-url'; +import { debug as createDebug } from 'debug'; + +const debug = createDebug('mongodb-build-info'); + +type Document = Record; const ATLAS_REGEX = /\.mongodb(-dev|-qa|-stage)?\.net$/i; const ATLAS_STREAM_REGEX = /^atlas-stream-.+/i; @@ -7,6 +12,7 @@ const LOCALHOST_REGEX = const DIGITAL_OCEAN_REGEX = /\.mongo\.ondigitalocean\.com$/i; const COSMOS_DB_REGEX = /\.cosmos\.azure\.com$/i; const DOCUMENT_DB_REGEX = /docdb(-elastic)?\.amazonaws\.com$/i; +const FIRESTORE_REGEX = /\.firestore.goog$/i; function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null; @@ -109,6 +115,9 @@ export function isDigitalOcean(uri: string): boolean { return !!getHostnameFromUrl(uri).match(DIGITAL_OCEAN_REGEX); } +/** + * @deprecated Use `identifyServerName` instead. + */ export function getGenuineMongoDB(uri: string): { isGenuine: boolean; serverName: string; @@ -134,6 +143,73 @@ export function getGenuineMongoDB(uri: string): { }; } +type IdentifyServerNameOptions = { + connectionString: string; + adminCommand: (command: Document) => Promise; +}; + +/** + * Identify the server name based on connection string and server responses. + * @returns A name of the server, "unknown" if we fail to identify it. + */ +export async function identifyServerName({ + connectionString, + adminCommand, +}: IdentifyServerNameOptions): Promise { + try { + const hostname = getHostnameFromUrl(connectionString); + if (hostname.match(COSMOS_DB_REGEX)) { + return 'cosmosdb'; + } + + if (hostname.match(DOCUMENT_DB_REGEX)) { + return 'documentdb'; + } + + if (hostname.match(FIRESTORE_REGEX)) { + return 'firestore'; + } + + const candidates = await Promise.all([ + adminCommand({ buildInfo: 1 }).then( + (response) => { + if ('ferretdb' in response) { + return ['ferretdb']; + } else { + return []; + } + }, + (error: unknown) => { + debug('buildInfo command failed %O', error); + return []; + }, + ), + adminCommand({ getParameter: 'foo' }).then( + // A successful response doesn't represent a signal + () => [], + (error: unknown) => { + if (error instanceof Error && /documentdb_api/.test(error.message)) { + return ['pg_documentdb']; + } else { + return []; + } + }, + ), + ]).then((results) => results.flat()); + + if (candidates.length === 0) { + return 'mongodb'; + } else if (candidates.length === 1) { + return candidates[0]; + } else { + return 'unknown'; + } + } catch (error) { + debug('Failed to identify server name', error); + return 'unknown'; + } +} + export function getBuildEnv(buildInfo: unknown): { serverOs: string | null; serverArch: string | null; diff --git a/packages/mongodb-build-info/test/fixtures.ts b/packages/mongodb-build-info/test/fixtures.ts index 775e010e..006610c6 100644 --- a/packages/mongodb-build-info/test/fixtures.ts +++ b/packages/mongodb-build-info/test/fixtures.ts @@ -80,6 +80,10 @@ export const DOCUMENT_DB_URIS = [ 'mongodb://x:y@elastic-docdb-123456789.eu-central-1.docdb-elastic.amazonaws.com:27017', ]; +export const FIRESTORE_URIS = [ + 'mongodb://x:y@bbccdaf5-527a-4be5-9881-b7073e92002b.europe-north2.firestore.goog:443/test-db?loadBalanced=true&tls=true&authMechanism=SCRAM-SHA-256&retryWrites=false', +]; + export const COSMOSDB_BUILD_INFO = { _t: 'BuildInfoResponse', ok: 1, diff --git a/packages/mongodb-build-info/test/index.spec.ts b/packages/mongodb-build-info/test/index.spec.ts index ca5989c6..f206b143 100644 --- a/packages/mongodb-build-info/test/index.spec.ts +++ b/packages/mongodb-build-info/test/index.spec.ts @@ -1,4 +1,5 @@ import { expect } from 'chai'; + import * as fixtures from './fixtures'; import { isAtlas, @@ -10,6 +11,7 @@ import { getBuildEnv, isEnterprise, getGenuineMongoDB, + identifyServerName, } from '../src/index'; describe('mongodb-build-info', function () { @@ -428,4 +430,74 @@ describe('mongodb-build-info', function () { expect(isGenuine.serverName).to.equal('mongodb'); }); }); + + context('identifyServerName', function () { + function fail() { + return Promise.reject(new Error('Should not be called')); + } + + it('reports CosmosDB', async function () { + for (const connectionString of fixtures.COSMOS_DB_URI) { + const result = await identifyServerName({ + connectionString, + adminCommand: fail, + }); + expect(result).to.equal('cosmosdb'); + } + }); + + it('reports DocumentDB', async function () { + for (const connectionString of fixtures.DOCUMENT_DB_URIS) { + const result = await identifyServerName({ + connectionString, + adminCommand: fail, + }); + expect(result).to.equal('documentdb'); + } + }); + + it('reports Firestore', async function () { + for (const connectionString of fixtures.FIRESTORE_URIS) { + const result = await identifyServerName({ + connectionString, + adminCommand: fail, + }); + expect(result).to.equal('firestore'); + } + }); + + it('reports FerretDB', async function () { + const result = await identifyServerName({ + connectionString: '', + adminCommand(req) { + if ('buildInfo' in req) { + return Promise.resolve({ + ferretdb: {}, + }); + } else { + return Promise.resolve({}); + } + }, + }); + expect(result).to.equal('ferretdb'); + }); + + it('reports PG DocumentDB', async function () { + const result = await identifyServerName({ + connectionString: '', + adminCommand(req) { + if ('getParameter' in req) { + return Promise.reject( + new Error( + 'function documentdb_api.get_parameter(boolean, boolean, text[]) does not exist', + ), + ); + } else { + return Promise.resolve({}); + } + }, + }); + expect(result).to.equal('pg_documentdb'); + }); + }); });