From a8b81bdd01329252466eb1dd608b2a92b960c3ae Mon Sep 17 00:00:00 2001 From: Shaya Potter Date: Tue, 7 Nov 2023 14:43:20 +0200 Subject: [PATCH] Send client user-agent during connection, via CLIENT SETINFO (#2645) * Add SETINFO support to client connection, with the ability to disable sending the user agent if the end user desires. * Also enables modifying the user-agent with a tag to enable distinguishing different usages. --- packages/client/lib/client/index.spec.ts | 40 +++++++++++++++++++++ packages/client/lib/client/index.ts | 39 +++++++++++++++++++- packages/client/lib/commands/CLIENT_INFO.ts | 7 +++- packages/client/lib/test-utils.ts | 3 +- packages/client/tsconfig.json | 3 +- tsconfig.base.json | 3 +- 6 files changed, 90 insertions(+), 5 deletions(-) diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index 4c2899a9b73..3278d27775b 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -10,6 +10,8 @@ import { once } from 'events'; import { ClientKillFilters } from '../commands/CLIENT_KILL'; import { promisify } from 'util'; +import {version} from '../../package.json'; + export const SQUARE_SCRIPT = defineScript({ SCRIPT: 'return ARGV[1] * ARGV[1];', NUMBER_OF_KEYS: 0, @@ -118,6 +120,44 @@ describe('Client', () => { ...GLOBAL.SERVERS.PASSWORD, disableClientSetup: true }); + + testUtils.testWithClient('should set default lib name and version', async client => { + const clientInfo = await client.clientInfo(); + + assert.equal(clientInfo.libName, 'node-redis'); + assert.equal(clientInfo.libVer, version); + }, { + ...GLOBAL.SERVERS.PASSWORD, + minimumDockerVersion: [7, 2] + }); + + testUtils.testWithClient('disable sending lib name and version', async client => { + const clientInfo = await client.clientInfo(); + + assert.equal(clientInfo.libName, ''); + assert.equal(clientInfo.libVer, ''); + }, { + ...GLOBAL.SERVERS.PASSWORD, + clientOptions: { + ...GLOBAL.SERVERS.PASSWORD.clientOptions, + disableClientInfo: true + }, + minimumDockerVersion: [7, 2] + }); + + testUtils.testWithClient('send client name tag', async client => { + const clientInfo = await client.clientInfo(); + + assert.equal(clientInfo.libName, 'node-redis(test)'); + assert.equal(clientInfo.libVer, version); + }, { + ...GLOBAL.SERVERS.PASSWORD, + clientOptions: { + ...GLOBAL.SERVERS.PASSWORD.clientOptions, + clientInfoTag: "test" + }, + minimumDockerVersion: [7, 2] + }); }); describe('authentication', () => { diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index 800894e06bb..4c3964c7aa0 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -11,11 +11,13 @@ import { ScanCommandOptions } from '../commands/SCAN'; import { HScanTuple } from '../commands/HSCAN'; import { attachCommands, attachExtensions, fCallArguments, transformCommandArguments, transformCommandReply, transformLegacyCommandArguments } from '../commander'; import { Pool, Options as PoolOptions, createPool } from 'generic-pool'; -import { ClientClosedError, ClientOfflineError, DisconnectsClientError } from '../errors'; +import { ClientClosedError, ClientOfflineError, DisconnectsClientError, ErrorReply } from '../errors'; import { URL } from 'url'; import { TcpSocketConnectOpts } from 'net'; import { PubSubType, PubSubListener, PubSubTypeListeners, ChannelListeners } from './pub-sub'; +import {version} from '../../package.json'; + export interface RedisClientOptions< M extends RedisModules = RedisModules, F extends RedisFunctions = RedisFunctions, @@ -66,6 +68,14 @@ export interface RedisClientOptions< * Useful with Redis deployments that do not use TCP Keep-Alive. */ pingInterval?: number; + /** + * If set to true, disables sending client identifier (user-agent like message) to the redis server + */ + disableClientInfo?: boolean; + /** + * Tag to append to library name that is sent to the Redis server + */ + clientInfoTag?: string; } type WithCommands = { @@ -274,6 +284,33 @@ export default class RedisClient< ); } + if (!this.#options?.disableClientInfo) { + promises.push( + this.#queue.addCommand( + [ 'CLIENT', 'SETINFO', 'LIB-VER', version], + { asap: true } + ).catch(err => { + if (!(err instanceof ErrorReply)) { + throw err; + } + }) + ); + + promises.push( + this.#queue.addCommand( + [ + 'CLIENT', 'SETINFO', 'LIB-NAME', + this.#options?.clientInfoTag ? `node-redis(${this.#options.clientInfoTag})` : 'node-redis' + ], + { asap: true } + ).catch(err => { + if (!(err instanceof ErrorReply)) { + throw err; + } + }) + ); + } + if (this.#options?.name) { promises.push( this.#queue.addCommand( diff --git a/packages/client/lib/commands/CLIENT_INFO.ts b/packages/client/lib/commands/CLIENT_INFO.ts index 7f6b6e1884e..fd823542f86 100644 --- a/packages/client/lib/commands/CLIENT_INFO.ts +++ b/packages/client/lib/commands/CLIENT_INFO.ts @@ -31,6 +31,9 @@ export interface ClientInfoReply { user?: string; // 6.0 redir?: number; // 6.2 resp?: number; // 7.0 + // 7.2 + libName?: string; + libVer?: string; } const CLIENT_INFO_REGEX = /([^\s=]+)=([^\s]*)/g; @@ -62,7 +65,9 @@ export function transformReply(rawReply: string): ClientInfoReply { totMem: Number(map['tot-mem']), events: map.events, cmd: map.cmd, - user: map.user + user: map.user, + libName: map['lib-name'], + libVer: map['lib-ver'], }; if (map.laddr !== undefined) { diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index 65d526f6019..a9db70c860f 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -4,7 +4,8 @@ import { promiseTimeout } from './utils'; const utils = new TestUtils({ dockerImageName: 'redis', - dockerImageVersionArgument: 'redis-version' + dockerImageVersionArgument: 'redis-version', + defaultDockerVersion: '7.2' }); export default utils; diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index 3271cf400a2..c71595c5702 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -5,7 +5,8 @@ }, "include": [ "./index.ts", - "./lib/**/*.ts" + "./lib/**/*.ts", + "./package.json" ], "exclude": [ "./lib/test-utils.ts", diff --git a/tsconfig.base.json b/tsconfig.base.json index 68325e51dcc..1157be947b9 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -4,7 +4,8 @@ "declaration": true, "allowJs": true, "useDefineForClassFields": true, - "esModuleInterop": false + "esModuleInterop": false, + "resolveJsonModule": true }, "ts-node": { "files": true