mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(Redis Node): Add option to disable TLS verification in Redis node (#19143)
Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
@@ -2,11 +2,8 @@ import type { ICredentialType, INodeProperties } from 'n8n-workflow';
|
|||||||
|
|
||||||
export class Redis implements ICredentialType {
|
export class Redis implements ICredentialType {
|
||||||
name = 'redis';
|
name = 'redis';
|
||||||
|
|
||||||
displayName = 'Redis';
|
displayName = 'Redis';
|
||||||
|
|
||||||
documentationUrl = 'redis';
|
documentationUrl = 'redis';
|
||||||
|
|
||||||
properties: INodeProperties[] = [
|
properties: INodeProperties[] = [
|
||||||
{
|
{
|
||||||
displayName: 'Password',
|
displayName: 'Password',
|
||||||
@@ -48,5 +45,18 @@ export class Redis implements ICredentialType {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Disable TLS Verification (insecure)',
|
||||||
|
name: 'disableTlsVerification',
|
||||||
|
type: 'boolean',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
ssl: [true],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: false,
|
||||||
|
description:
|
||||||
|
'Whether to disable TLS certificate verification. Enable this to use self-signed certificates. WARNING: This makes the connection less secure.',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -515,147 +515,159 @@ export class Redis implements INodeType {
|
|||||||
const credentials = await this.getCredentials<RedisCredential>('redis');
|
const credentials = await this.getCredentials<RedisCredential>('redis');
|
||||||
|
|
||||||
const client = setupRedisClient(credentials);
|
const client = setupRedisClient(credentials);
|
||||||
await client.connect();
|
|
||||||
await client.ping();
|
|
||||||
|
|
||||||
const operation = this.getNodeParameter('operation', 0);
|
try {
|
||||||
const returnItems: INodeExecutionData[] = [];
|
await client.connect();
|
||||||
|
await client.ping();
|
||||||
|
|
||||||
if (operation === 'info') {
|
const operation = this.getNodeParameter('operation', 0);
|
||||||
try {
|
const returnItems: INodeExecutionData[] = [];
|
||||||
const result = await client.info();
|
|
||||||
returnItems.push({ json: convertInfoToObject(result) });
|
|
||||||
} catch (error) {
|
|
||||||
if (this.continueOnFail()) {
|
|
||||||
returnItems.push({
|
|
||||||
json: {
|
|
||||||
error: error.message,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await client.quit();
|
|
||||||
throw new NodeOperationError(this.getNode(), error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
['delete', 'get', 'keys', 'set', 'incr', 'publish', 'push', 'pop'].includes(operation)
|
|
||||||
) {
|
|
||||||
const items = this.getInputData();
|
|
||||||
|
|
||||||
let item: INodeExecutionData;
|
if (operation === 'info') {
|
||||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
||||||
try {
|
try {
|
||||||
item = { json: {}, pairedItem: { item: itemIndex } };
|
const result = await client.info();
|
||||||
|
returnItems.push({ json: convertInfoToObject(result) });
|
||||||
if (operation === 'delete') {
|
|
||||||
const keyDelete = this.getNodeParameter('key', itemIndex) as string;
|
|
||||||
|
|
||||||
await client.del(keyDelete);
|
|
||||||
returnItems.push(items[itemIndex]);
|
|
||||||
} else if (operation === 'get') {
|
|
||||||
const propertyName = this.getNodeParameter('propertyName', itemIndex) as string;
|
|
||||||
const keyGet = this.getNodeParameter('key', itemIndex) as string;
|
|
||||||
const keyType = this.getNodeParameter('keyType', itemIndex) as string;
|
|
||||||
|
|
||||||
const value = (await getValue(client, keyGet, keyType)) ?? null;
|
|
||||||
|
|
||||||
const options = this.getNodeParameter('options', itemIndex, {});
|
|
||||||
|
|
||||||
if (options.dotNotation === false) {
|
|
||||||
item.json[propertyName] = value;
|
|
||||||
} else {
|
|
||||||
set(item.json, propertyName, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
returnItems.push(item);
|
|
||||||
} else if (operation === 'keys') {
|
|
||||||
const keyPattern = this.getNodeParameter('keyPattern', itemIndex) as string;
|
|
||||||
const getValues = this.getNodeParameter('getValues', itemIndex, true) as boolean;
|
|
||||||
|
|
||||||
const keys = await client.keys(keyPattern);
|
|
||||||
|
|
||||||
if (!getValues) {
|
|
||||||
returnItems.push({ json: { keys } });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const keyName of keys) {
|
|
||||||
item.json[keyName] = await getValue(client, keyName);
|
|
||||||
}
|
|
||||||
returnItems.push(item);
|
|
||||||
} else if (operation === 'set') {
|
|
||||||
const keySet = this.getNodeParameter('key', itemIndex) as string;
|
|
||||||
const value = this.getNodeParameter('value', itemIndex) as string;
|
|
||||||
const keyType = this.getNodeParameter('keyType', itemIndex) as string;
|
|
||||||
const valueIsJSON = this.getNodeParameter('valueIsJSON', itemIndex, true) as boolean;
|
|
||||||
const expire = this.getNodeParameter('expire', itemIndex, false) as boolean;
|
|
||||||
const ttl = this.getNodeParameter('ttl', itemIndex, -1) as number;
|
|
||||||
|
|
||||||
await setValue.call(this, client, keySet, value, expire, ttl, keyType, valueIsJSON);
|
|
||||||
returnItems.push(items[itemIndex]);
|
|
||||||
} else if (operation === 'incr') {
|
|
||||||
const keyIncr = this.getNodeParameter('key', itemIndex) as string;
|
|
||||||
const expire = this.getNodeParameter('expire', itemIndex, false) as boolean;
|
|
||||||
const ttl = this.getNodeParameter('ttl', itemIndex, -1) as number;
|
|
||||||
const incrementVal = await client.incr(keyIncr);
|
|
||||||
if (expire && ttl > 0) {
|
|
||||||
await client.expire(keyIncr, ttl);
|
|
||||||
}
|
|
||||||
returnItems.push({ json: { [keyIncr]: incrementVal } });
|
|
||||||
} else if (operation === 'publish') {
|
|
||||||
const channel = this.getNodeParameter('channel', itemIndex) as string;
|
|
||||||
const messageData = this.getNodeParameter('messageData', itemIndex) as string;
|
|
||||||
await client.publish(channel, messageData);
|
|
||||||
returnItems.push(items[itemIndex]);
|
|
||||||
} else if (operation === 'push') {
|
|
||||||
const redisList = this.getNodeParameter('list', itemIndex) as string;
|
|
||||||
const messageData = this.getNodeParameter('messageData', itemIndex) as string;
|
|
||||||
const tail = this.getNodeParameter('tail', itemIndex, false) as boolean;
|
|
||||||
await client[tail ? 'rPush' : 'lPush'](redisList, messageData);
|
|
||||||
returnItems.push(items[itemIndex]);
|
|
||||||
} else if (operation === 'pop') {
|
|
||||||
const redisList = this.getNodeParameter('list', itemIndex) as string;
|
|
||||||
const tail = this.getNodeParameter('tail', itemIndex, false) as boolean;
|
|
||||||
const propertyName = this.getNodeParameter(
|
|
||||||
'propertyName',
|
|
||||||
itemIndex,
|
|
||||||
'propertyName',
|
|
||||||
) as string;
|
|
||||||
|
|
||||||
const value = await client[tail ? 'rPop' : 'lPop'](redisList);
|
|
||||||
|
|
||||||
let outputValue;
|
|
||||||
try {
|
|
||||||
outputValue = value && JSON.parse(value);
|
|
||||||
} catch {
|
|
||||||
outputValue = value;
|
|
||||||
}
|
|
||||||
const options = this.getNodeParameter('options', itemIndex, {});
|
|
||||||
if (options.dotNotation === false) {
|
|
||||||
item.json[propertyName] = outputValue;
|
|
||||||
} else {
|
|
||||||
set(item.json, propertyName, outputValue);
|
|
||||||
}
|
|
||||||
returnItems.push(item);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.continueOnFail()) {
|
if (this.continueOnFail()) {
|
||||||
returnItems.push({
|
returnItems.push({
|
||||||
json: {
|
json: {
|
||||||
error: error.message,
|
error: error.message,
|
||||||
},
|
},
|
||||||
pairedItem: {
|
|
||||||
item: itemIndex,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
continue;
|
} else {
|
||||||
|
throw new NodeOperationError(this.getNode(), error);
|
||||||
}
|
}
|
||||||
await client.quit();
|
}
|
||||||
throw new NodeOperationError(this.getNode(), error, { itemIndex });
|
} else if (
|
||||||
|
['delete', 'get', 'keys', 'set', 'incr', 'publish', 'push', 'pop'].includes(operation)
|
||||||
|
) {
|
||||||
|
const items = this.getInputData();
|
||||||
|
|
||||||
|
let item: INodeExecutionData;
|
||||||
|
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||||
|
try {
|
||||||
|
item = { json: {}, pairedItem: { item: itemIndex } };
|
||||||
|
|
||||||
|
if (operation === 'delete') {
|
||||||
|
const keyDelete = this.getNodeParameter('key', itemIndex) as string;
|
||||||
|
|
||||||
|
await client.del(keyDelete);
|
||||||
|
returnItems.push(items[itemIndex]);
|
||||||
|
} else if (operation === 'get') {
|
||||||
|
const propertyName = this.getNodeParameter('propertyName', itemIndex) as string;
|
||||||
|
const keyGet = this.getNodeParameter('key', itemIndex) as string;
|
||||||
|
const keyType = this.getNodeParameter('keyType', itemIndex) as string;
|
||||||
|
|
||||||
|
const value = (await getValue(client, keyGet, keyType)) ?? null;
|
||||||
|
|
||||||
|
const options = this.getNodeParameter('options', itemIndex, {});
|
||||||
|
|
||||||
|
if (options.dotNotation === false) {
|
||||||
|
item.json[propertyName] = value;
|
||||||
|
} else {
|
||||||
|
set(item.json, propertyName, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
returnItems.push(item);
|
||||||
|
} else if (operation === 'keys') {
|
||||||
|
const keyPattern = this.getNodeParameter('keyPattern', itemIndex) as string;
|
||||||
|
const getValues = this.getNodeParameter('getValues', itemIndex, true) as boolean;
|
||||||
|
|
||||||
|
const keys = await client.keys(keyPattern);
|
||||||
|
|
||||||
|
if (!getValues) {
|
||||||
|
returnItems.push({ json: { keys } });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const keyName of keys) {
|
||||||
|
item.json[keyName] = await getValue(client, keyName);
|
||||||
|
}
|
||||||
|
returnItems.push(item);
|
||||||
|
} else if (operation === 'set') {
|
||||||
|
const keySet = this.getNodeParameter('key', itemIndex) as string;
|
||||||
|
const value = this.getNodeParameter('value', itemIndex) as string;
|
||||||
|
const keyType = this.getNodeParameter('keyType', itemIndex) as string;
|
||||||
|
const valueIsJSON = this.getNodeParameter('valueIsJSON', itemIndex, true) as boolean;
|
||||||
|
const expire = this.getNodeParameter('expire', itemIndex, false) as boolean;
|
||||||
|
const ttl = this.getNodeParameter('ttl', itemIndex, -1) as number;
|
||||||
|
|
||||||
|
await setValue.call(this, client, keySet, value, expire, ttl, keyType, valueIsJSON);
|
||||||
|
returnItems.push(items[itemIndex]);
|
||||||
|
} else if (operation === 'incr') {
|
||||||
|
const keyIncr = this.getNodeParameter('key', itemIndex) as string;
|
||||||
|
const expire = this.getNodeParameter('expire', itemIndex, false) as boolean;
|
||||||
|
const ttl = this.getNodeParameter('ttl', itemIndex, -1) as number;
|
||||||
|
const incrementVal = await client.incr(keyIncr);
|
||||||
|
if (expire && ttl > 0) {
|
||||||
|
await client.expire(keyIncr, ttl);
|
||||||
|
}
|
||||||
|
returnItems.push({ json: { [keyIncr]: incrementVal } });
|
||||||
|
} else if (operation === 'publish') {
|
||||||
|
const channel = this.getNodeParameter('channel', itemIndex) as string;
|
||||||
|
const messageData = this.getNodeParameter('messageData', itemIndex) as string;
|
||||||
|
await client.publish(channel, messageData);
|
||||||
|
returnItems.push(items[itemIndex]);
|
||||||
|
} else if (operation === 'push') {
|
||||||
|
const redisList = this.getNodeParameter('list', itemIndex) as string;
|
||||||
|
const messageData = this.getNodeParameter('messageData', itemIndex) as string;
|
||||||
|
const tail = this.getNodeParameter('tail', itemIndex, false) as boolean;
|
||||||
|
await client[tail ? 'rPush' : 'lPush'](redisList, messageData);
|
||||||
|
returnItems.push(items[itemIndex]);
|
||||||
|
} else if (operation === 'pop') {
|
||||||
|
const redisList = this.getNodeParameter('list', itemIndex) as string;
|
||||||
|
const tail = this.getNodeParameter('tail', itemIndex, false) as boolean;
|
||||||
|
const propertyName = this.getNodeParameter(
|
||||||
|
'propertyName',
|
||||||
|
itemIndex,
|
||||||
|
'propertyName',
|
||||||
|
) as string;
|
||||||
|
|
||||||
|
const value = await client[tail ? 'rPop' : 'lPop'](redisList);
|
||||||
|
|
||||||
|
let outputValue;
|
||||||
|
try {
|
||||||
|
outputValue = value && JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
outputValue = value;
|
||||||
|
}
|
||||||
|
const options = this.getNodeParameter('options', itemIndex, {});
|
||||||
|
if (options.dotNotation === false) {
|
||||||
|
item.json[propertyName] = outputValue;
|
||||||
|
} else {
|
||||||
|
set(item.json, propertyName, outputValue);
|
||||||
|
}
|
||||||
|
returnItems.push(item);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (this.continueOnFail()) {
|
||||||
|
returnItems.push({
|
||||||
|
json: {
|
||||||
|
error: error.message,
|
||||||
|
},
|
||||||
|
pairedItem: {
|
||||||
|
item: itemIndex,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new NodeOperationError(this.getNode(), error, { itemIndex });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [returnItems];
|
||||||
|
} finally {
|
||||||
|
// Ensure the Redis client is always closed to prevent leaked connections
|
||||||
|
try {
|
||||||
|
await client.quit();
|
||||||
|
} catch {
|
||||||
|
// If quit fails, forcefully disconnect
|
||||||
|
try {
|
||||||
|
await client.disconnect();
|
||||||
|
} catch {
|
||||||
|
// Ignore disconnect errors in cleanup
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await client.quit();
|
|
||||||
return [returnItems];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ describe('Redis Node', () => {
|
|||||||
host: 'redis.domain',
|
host: 'redis.domain',
|
||||||
port: 1234,
|
port: 1234,
|
||||||
tls: false,
|
tls: false,
|
||||||
|
connectTimeout: 10000,
|
||||||
|
reconnectStrategy: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -54,6 +56,69 @@ describe('Redis Node', () => {
|
|||||||
host: 'redis.domain',
|
host: 'redis.domain',
|
||||||
port: 1234,
|
port: 1234,
|
||||||
tls: true,
|
tls: true,
|
||||||
|
connectTimeout: 10000,
|
||||||
|
reconnectStrategy: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should configure TLS with verification disabled for self-signed certificates', () => {
|
||||||
|
setupRedisClient({
|
||||||
|
host: 'redis.domain',
|
||||||
|
port: 1234,
|
||||||
|
database: 0,
|
||||||
|
ssl: true,
|
||||||
|
disableTlsVerification: true,
|
||||||
|
});
|
||||||
|
expect(createClient).toHaveBeenCalledWith({
|
||||||
|
database: 0,
|
||||||
|
socket: {
|
||||||
|
host: 'redis.domain',
|
||||||
|
port: 1234,
|
||||||
|
tls: true,
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
connectTimeout: 10000,
|
||||||
|
reconnectStrategy: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set rejectUnauthorized when TLS verification is enabled', () => {
|
||||||
|
setupRedisClient({
|
||||||
|
host: 'redis.domain',
|
||||||
|
port: 1234,
|
||||||
|
database: 0,
|
||||||
|
ssl: true,
|
||||||
|
disableTlsVerification: false,
|
||||||
|
});
|
||||||
|
expect(createClient).toHaveBeenCalledWith({
|
||||||
|
database: 0,
|
||||||
|
socket: {
|
||||||
|
host: 'redis.domain',
|
||||||
|
port: 1234,
|
||||||
|
tls: true,
|
||||||
|
connectTimeout: 10000,
|
||||||
|
reconnectStrategy: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set rejectUnauthorized when SSL is disabled', () => {
|
||||||
|
setupRedisClient({
|
||||||
|
host: 'redis.domain',
|
||||||
|
port: 1234,
|
||||||
|
database: 0,
|
||||||
|
ssl: false,
|
||||||
|
disableTlsVerification: true,
|
||||||
|
});
|
||||||
|
expect(createClient).toHaveBeenCalledWith({
|
||||||
|
database: 0,
|
||||||
|
socket: {
|
||||||
|
host: 'redis.domain',
|
||||||
|
port: 1234,
|
||||||
|
tls: false,
|
||||||
|
connectTimeout: 10000,
|
||||||
|
reconnectStrategy: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -74,6 +139,33 @@ describe('Redis Node', () => {
|
|||||||
host: 'redis.domain',
|
host: 'redis.domain',
|
||||||
port: 1234,
|
port: 1234,
|
||||||
tls: false,
|
tls: false,
|
||||||
|
connectTimeout: 10000,
|
||||||
|
reconnectStrategy: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should configure TLS with disabled verification and auth', () => {
|
||||||
|
setupRedisClient({
|
||||||
|
host: 'redis.domain',
|
||||||
|
port: 1234,
|
||||||
|
database: 0,
|
||||||
|
ssl: true,
|
||||||
|
disableTlsVerification: true,
|
||||||
|
user: 'test_user',
|
||||||
|
password: 'test_password',
|
||||||
|
});
|
||||||
|
expect(createClient).toHaveBeenCalledWith({
|
||||||
|
database: 0,
|
||||||
|
username: 'test_user',
|
||||||
|
password: 'test_password',
|
||||||
|
socket: {
|
||||||
|
host: 'redis.domain',
|
||||||
|
port: 1234,
|
||||||
|
tls: true,
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
connectTimeout: 10000,
|
||||||
|
reconnectStrategy: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -95,10 +187,14 @@ describe('Redis Node', () => {
|
|||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 6379,
|
port: 6379,
|
||||||
tls: false,
|
tls: false,
|
||||||
|
connectTimeout: 10000,
|
||||||
|
reconnectStrategy: false,
|
||||||
},
|
},
|
||||||
database: 0,
|
database: 0,
|
||||||
username: 'username',
|
username: 'username',
|
||||||
password: 'password',
|
password: 'password',
|
||||||
|
disableOfflineQueue: true,
|
||||||
|
enableOfflineQueue: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should return success when connection is established', async () => {
|
it('should return success when connection is established', async () => {
|
||||||
@@ -126,6 +222,44 @@ describe('Redis Node', () => {
|
|||||||
expect(mockClient.connect).toHaveBeenCalled();
|
expect(mockClient.connect).toHaveBeenCalled();
|
||||||
expect(mockClient.ping).not.toHaveBeenCalled();
|
expect(mockClient.ping).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return success when connection is established with disabled TLS verification', async () => {
|
||||||
|
const credentialsWithTls = mock<ICredentialsDecrypted>({
|
||||||
|
data: {
|
||||||
|
host: 'localhost',
|
||||||
|
port: 6379,
|
||||||
|
ssl: true,
|
||||||
|
disableTlsVerification: true,
|
||||||
|
user: 'username',
|
||||||
|
password: 'password',
|
||||||
|
database: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await redisConnectionTest.call(thisArg, credentialsWithTls);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
status: 'OK',
|
||||||
|
message: 'Connection successful!',
|
||||||
|
});
|
||||||
|
expect(createClient).toHaveBeenCalledWith({
|
||||||
|
socket: {
|
||||||
|
host: 'localhost',
|
||||||
|
port: 6379,
|
||||||
|
tls: true,
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
connectTimeout: 10000,
|
||||||
|
reconnectStrategy: false,
|
||||||
|
},
|
||||||
|
database: 0,
|
||||||
|
username: 'username',
|
||||||
|
password: 'password',
|
||||||
|
disableOfflineQueue: true,
|
||||||
|
enableOfflineQueue: false,
|
||||||
|
});
|
||||||
|
expect(mockClient.connect).toHaveBeenCalled();
|
||||||
|
expect(mockClient.ping).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('operations', () => {
|
describe('operations', () => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type RedisCredential = {
|
|||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
ssl?: boolean;
|
ssl?: boolean;
|
||||||
|
disableTlsVerification?: boolean;
|
||||||
database: number;
|
database: number;
|
||||||
user?: string;
|
user?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
|||||||
@@ -10,16 +10,31 @@ import { createClient } from 'redis';
|
|||||||
|
|
||||||
import type { RedisCredential, RedisClient } from './types';
|
import type { RedisCredential, RedisClient } from './types';
|
||||||
|
|
||||||
export function setupRedisClient(credentials: RedisCredential): RedisClient {
|
export function setupRedisClient(credentials: RedisCredential, isTest = false): RedisClient {
|
||||||
|
const socketConfig: any = {
|
||||||
|
host: credentials.host,
|
||||||
|
port: credentials.port,
|
||||||
|
tls: credentials.ssl === true,
|
||||||
|
connectTimeout: 10000,
|
||||||
|
// Disable reconnection for tests to prevent hanging
|
||||||
|
reconnectStrategy: isTest ? false : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
// If SSL is enabled and TLS verification should be disabled
|
||||||
|
if (credentials.ssl === true && credentials.disableTlsVerification === true) {
|
||||||
|
socketConfig.rejectUnauthorized = false;
|
||||||
|
}
|
||||||
|
|
||||||
return createClient({
|
return createClient({
|
||||||
socket: {
|
socket: socketConfig,
|
||||||
host: credentials.host,
|
|
||||||
port: credentials.port,
|
|
||||||
tls: credentials.ssl === true,
|
|
||||||
},
|
|
||||||
database: credentials.database,
|
database: credentials.database,
|
||||||
username: credentials.user || undefined,
|
username: credentials.user ?? undefined,
|
||||||
password: credentials.password || undefined,
|
password: credentials.password ?? undefined,
|
||||||
|
// Disable automatic error retry for tests
|
||||||
|
...(isTest && {
|
||||||
|
disableOfflineQueue: true,
|
||||||
|
enableOfflineQueue: false,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,26 +43,68 @@ export async function redisConnectionTest(
|
|||||||
credential: ICredentialsDecrypted,
|
credential: ICredentialsDecrypted,
|
||||||
): Promise<INodeCredentialTestResult> {
|
): Promise<INodeCredentialTestResult> {
|
||||||
const credentials = credential.data as RedisCredential;
|
const credentials = credential.data as RedisCredential;
|
||||||
|
let client: RedisClient | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = setupRedisClient(credentials);
|
client = setupRedisClient(credentials, true);
|
||||||
await client.connect();
|
|
||||||
|
// Add error event handler to catch connection errors
|
||||||
|
const errorPromise = new Promise<never>((_, reject) => {
|
||||||
|
client!.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a timeout promise
|
||||||
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
reject(new Error('Connection timeout: Unable to connect to Redis server'));
|
||||||
|
}, 10000); // 10 seconds timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
// Race between connecting and error/timeout
|
||||||
|
await Promise.race([client.connect(), errorPromise, timeoutPromise]);
|
||||||
|
|
||||||
await client.ping();
|
await client.ping();
|
||||||
return {
|
return {
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
message: 'Connection successful!',
|
message: 'Connection successful!',
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Handle specific error types for better user feedback
|
||||||
|
let errorMessage = error.message;
|
||||||
|
if (error.code === 'ECONNRESET') {
|
||||||
|
errorMessage =
|
||||||
|
'Connection reset: The Redis server rejected the connection. This often happens when trying to connect without SSL to an SSL-only server.';
|
||||||
|
} else if (error.code === 'ECONNREFUSED') {
|
||||||
|
errorMessage =
|
||||||
|
'Connection refused: Unable to connect to the Redis server. Please check the host and port.';
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 'Error',
|
status: 'Error',
|
||||||
message: error.message,
|
message: errorMessage,
|
||||||
};
|
};
|
||||||
|
} finally {
|
||||||
|
// Ensure the Redis client is always closed to prevent leaked connections
|
||||||
|
if (client) {
|
||||||
|
try {
|
||||||
|
await client.quit();
|
||||||
|
} catch {
|
||||||
|
// If quit fails, forcefully disconnect
|
||||||
|
try {
|
||||||
|
await client.disconnect();
|
||||||
|
} catch {
|
||||||
|
// Ignore disconnect errors in cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Parses the given value in a number if it is one else returns a string */
|
/** Parses the given value in a number if it is one else returns a string */
|
||||||
function getParsedValue(value: string): string | number {
|
function getParsedValue(value: string): string | number {
|
||||||
if (value.match(/^[\d\.]+$/) === null) {
|
if (value.match(/^[\d.]+$/) === null) {
|
||||||
// Is a string
|
// Is a string
|
||||||
return value;
|
return value;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user