n8n-nodes-websocket
Version:
Enhanced WebSocket nodes for n8n with bidirectional communication support
538 lines • 25.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebsocketTrigger = exports.WebSocketConnectionManager = exports.Websocket = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const ws_1 = require("ws");
const url_1 = require("url");
const WebSocketConnections_1 = require("../Shared/WebSocketConnections");
Object.defineProperty(exports, "WebSocketConnectionManager", { enumerable: true, get: function () { return WebSocketConnections_1.WebSocketConnectionManager; } });
class Websocket {
constructor() {
this.description = {
displayName: 'WebSocket Trigger',
name: 'websocketTrigger',
icon: 'file:websocket_icon.svg',
group: ['trigger'],
version: 1,
subtitle: '=Port: {{$parameter["port"]}}{{$parameter["path"]}}',
description: 'Starts a WebSocket server with bidirectional communication and triggers workflow on incoming connections/messages',
defaults: {
name: 'WebSocket Trigger',
},
inputs: [],
outputs: ["main"],
credentials: [],
webhooks: [],
triggerPanel: {
header: 'WebSocket Server',
executionsHelp: {
inactive: 'WebSocket server is inactive. Workflows will not be triggered.',
active: 'WebSocket server is running on 0.0.0.0:{{$parameter["port"]}}{{$parameter["path"]}}. Accessible from external networks.',
},
activationHint: 'WebSocket server will start when you activate this workflow and listen on all network interfaces (0.0.0.0).',
},
properties: [
{
displayName: 'Port',
name: 'port',
type: 'number',
default: 8080,
placeholder: '8080',
description: 'Port number for the WebSocket server to listen on (accessible from 0.0.0.0)',
required: true,
},
{
displayName: 'Path',
name: 'path',
type: 'string',
default: '/websocket',
placeholder: '/websocket',
description: 'Path for WebSocket connections (e.g., /websocket)',
required: true,
},
{
displayName: 'Bind Address',
name: 'bindAddress',
type: 'options',
noDataExpression: true,
options: [
{
name: 'All Interfaces (0.0.0.0) - External Access',
value: '0.0.0.0',
description: 'Listen on all network interfaces - accessible from external networks',
action: 'Bind to all interfaces',
},
{
name: 'Localhost Only (127.0.0.1) - Local Access',
value: '127.0.0.1',
description: 'Listen only on localhost - accessible only from this machine',
action: 'Bind to localhost only',
},
],
default: '0.0.0.0',
description: 'Network interface to bind the WebSocket server to',
},
{
displayName: 'Trigger On',
name: 'triggerOn',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Connection',
value: 'connection',
description: 'Trigger when client connects',
action: 'Trigger on connection',
},
{
name: 'Message',
value: 'message',
description: 'Trigger when message is received',
action: 'Trigger on message',
},
{
name: 'Both',
value: 'both',
description: 'Trigger on both connection and message',
action: 'Trigger on connection and message',
},
],
default: 'message',
description: 'When to trigger the workflow',
},
{
displayName: 'Response Mode',
name: 'responseMode',
type: 'options',
noDataExpression: true,
options: [
{
name: 'No Response',
value: 'noResponse',
description: 'Do not send any response back',
},
{
name: 'Echo Message',
value: 'echo',
description: 'Echo the received message back to client',
},
{
name: 'Custom Response',
value: 'custom',
description: 'Send custom response back to client',
},
],
default: 'noResponse',
description: 'How to respond to incoming messages',
},
{
displayName: 'Custom Response',
name: 'customResponse',
type: 'string',
default: '{"status": "received"}',
placeholder: '{"status": "received"}',
description: 'Custom response to send back to client',
displayOptions: {
show: {
responseMode: ['custom'],
},
},
},
{
displayName: 'Auto Messages',
name: 'autoMessages',
type: 'collection',
placeholder: 'Add Auto Message',
default: {},
options: [
{
displayName: 'Send Welcome Message',
name: 'sendWelcome',
type: 'boolean',
default: false,
description: 'Send welcome message when client connects',
},
{
displayName: 'Welcome Message',
name: 'welcomeMessage',
type: 'string',
default: '{"type": "welcome", "message": "Connected to WebSocket server", "timestamp": "{{timestamp}}"}',
description: 'Welcome message to send (use {{timestamp}} for current time)',
displayOptions: {
show: {
sendWelcome: [true],
},
},
},
{
displayName: 'Send Periodic Messages',
name: 'sendPeriodic',
type: 'boolean',
default: false,
description: 'Send periodic messages to all connected clients',
},
{
displayName: 'Periodic Interval (seconds)',
name: 'periodicInterval',
type: 'number',
default: 30,
description: 'Interval in seconds for periodic messages',
displayOptions: {
show: {
sendPeriodic: [true],
},
},
},
{
displayName: 'Periodic Message',
name: 'periodicMessage',
type: 'string',
default: '{"type": "heartbeat", "message": "Server is alive", "timestamp": "{{timestamp}}", "connections": {{connections}}}',
description: 'Periodic message to send (use {{timestamp}} and {{connections}})',
displayOptions: {
show: {
sendPeriodic: [true],
},
},
},
{
displayName: 'Auto Reply After Message',
name: 'autoReplyAfterMessage',
type: 'boolean',
default: false,
description: 'Automatically send reply after receiving message',
},
{
displayName: 'Auto Reply Delay (seconds)',
name: 'autoReplyDelay',
type: 'number',
default: 5,
description: 'Delay in seconds before sending auto reply',
displayOptions: {
show: {
autoReplyAfterMessage: [true],
},
},
},
{
displayName: 'Auto Reply Message',
name: 'autoReplyMessage',
type: 'string',
default: '{"type": "auto_reply", "message": "Thank you for your message", "timestamp": "{{timestamp}}"}',
description: 'Auto reply message (use {{timestamp}})',
displayOptions: {
show: {
autoReplyAfterMessage: [true],
},
},
},
],
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
noDataExpression: true,
options: [
{
name: 'None',
value: 'none',
description: 'No authentication required',
},
{
name: 'Header Token',
value: 'headerToken',
description: 'Require token in connection headers',
},
{
name: 'Query Parameter',
value: 'queryParam',
description: 'Require token as query parameter',
},
],
default: 'none',
description: 'Authentication method for WebSocket connections',
},
{
displayName: 'Token Header Name',
name: 'tokenHeaderName',
type: 'string',
default: 'Authorization',
placeholder: 'Authorization',
description: 'Header name for authentication token',
displayOptions: {
show: {
authentication: ['headerToken'],
},
},
},
{
displayName: 'Token Parameter Name',
name: 'tokenParamName',
type: 'string',
default: 'token',
placeholder: 'token',
description: 'Query parameter name for authentication token',
displayOptions: {
show: {
authentication: ['queryParam'],
},
},
},
{
displayName: 'Expected Token',
name: 'expectedToken',
type: 'string',
typeOptions: {
password: true,
},
default: '',
placeholder: 'your-secret-token',
description: 'The expected token value for authentication',
displayOptions: {
show: {
authentication: ['headerToken', 'queryParam'],
},
},
},
{
displayName: 'Advanced Options',
name: 'advancedOptions',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Max Connections',
name: 'maxConnections',
type: 'number',
default: 100,
description: 'Maximum number of concurrent connections',
},
{
displayName: 'Ping Interval (ms)',
name: 'pingInterval',
type: 'number',
default: 30000,
description: 'Interval for sending ping frames (0 to disable)',
},
{
displayName: 'Parse JSON Messages',
name: 'parseJson',
type: 'boolean',
default: true,
description: 'Whether to try parsing incoming messages as JSON',
},
{
displayName: 'Include Connection Info',
name: 'includeConnectionInfo',
type: 'boolean',
default: true,
description: 'Whether to include connection metadata in trigger data',
},
{
displayName: 'Message Size Limit (KB)',
name: 'messageSizeLimit',
type: 'number',
default: 1024,
description: 'Maximum size of incoming messages in kilobytes',
},
{
displayName: 'Connection Cleanup Interval (seconds)',
name: 'cleanupInterval',
type: 'number',
default: 300,
description: 'How often to clean up dead connections (seconds)',
},
],
},
],
};
}
async trigger() {
const port = this.getNodeParameter('port');
const path = this.getNodeParameter('path');
const bindAddress = this.getNodeParameter('bindAddress', '0.0.0.0');
const triggerOn = this.getNodeParameter('triggerOn');
const responseMode = this.getNodeParameter('responseMode');
const customResponse = this.getNodeParameter('customResponse', '');
const authentication = this.getNodeParameter('authentication');
const expectedToken = this.getNodeParameter('expectedToken', '');
const tokenHeaderName = this.getNodeParameter('tokenHeaderName', 'Authorization');
const tokenParamName = this.getNodeParameter('tokenParamName', 'token');
const advancedOptions = this.getNodeParameter('advancedOptions', {});
const autoMessages = this.getNodeParameter('autoMessages', {});
const maxConnections = advancedOptions.maxConnections || 100;
const pingInterval = advancedOptions.pingInterval || 30000;
const parseJson = advancedOptions.parseJson !== false;
const includeConnectionInfo = advancedOptions.includeConnectionInfo !== false;
const messageSizeLimit = (advancedOptions.messageSizeLimit || 1024) * 1024;
const cleanupInterval = (advancedOptions.cleanupInterval || 300) * 1000;
let activeConnections = 0;
const wss = new ws_1.Server({
host: bindAddress,
port,
path,
maxPayload: messageSizeLimit,
verifyClient: (info) => {
if (activeConnections >= maxConnections) {
return false;
}
if (authentication !== 'none' && expectedToken) {
if (authentication === 'headerToken') {
const token = info.req.headers[tokenHeaderName.toLowerCase()];
if (token !== expectedToken) {
return false;
}
}
else if (authentication === 'queryParam') {
const url = new url_1.URL(info.req.url || '', `http://${info.req.headers.host}`);
const token = url.searchParams.get(tokenParamName);
if (token !== expectedToken) {
return false;
}
}
}
return true;
},
});
let pingIntervalId;
if (pingInterval > 0) {
pingIntervalId = setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.readyState === ws_1.WebSocket.OPEN) {
ws.ping();
}
});
}, pingInterval);
}
let periodicIntervalId;
if (autoMessages.sendPeriodic) {
const interval = (autoMessages.periodicInterval || 30) * 1000;
const template = autoMessages.periodicMessage || '{"type": "heartbeat", "timestamp": "{{timestamp}}"}';
periodicIntervalId = setInterval(() => {
const message = WebSocketConnections_1.WebSocketConnectionManager.processMessageTemplate(template);
const sent = WebSocketConnections_1.WebSocketConnectionManager.broadcastToAll(message);
console.log(`Sent periodic message to ${sent} clients`);
}, interval);
}
let cleanupIntervalId;
cleanupIntervalId = setInterval(() => {
WebSocketConnections_1.WebSocketConnectionManager.cleanupDeadConnections();
}, cleanupInterval);
wss.on('connection', (ws, req) => {
activeConnections++;
const connectionId = Math.random().toString(36).substring(7);
const connectionInfo = includeConnectionInfo ? {
remoteAddress: req.socket.remoteAddress,
remotePort: req.socket.remotePort,
userAgent: req.headers['user-agent'],
origin: req.headers.origin,
timestamp: new Date().toISOString(),
connectionId,
} : {
connectionId,
timestamp: new Date().toISOString(),
};
WebSocketConnections_1.WebSocketConnectionManager.addConnection(connectionId, ws, connectionInfo);
if (autoMessages.sendWelcome && autoMessages.welcomeMessage) {
const welcomeMsg = WebSocketConnections_1.WebSocketConnectionManager.processMessageTemplate(autoMessages.welcomeMessage, connectionInfo);
setTimeout(() => {
WebSocketConnections_1.WebSocketConnectionManager.sendToConnection(connectionId, welcomeMsg);
}, 100);
}
if (triggerOn === 'connection' || triggerOn === 'both') {
const executionData = [{
json: {
event: 'connection',
data: null,
connectionInfo,
meta: {
totalConnections: WebSocketConnections_1.WebSocketConnectionManager.getConnectionCount(),
serverInfo: {
host: bindAddress,
port,
path,
},
},
},
}];
this.emit([executionData]);
}
ws.on('message', (data) => {
let messageData = data.toString();
WebSocketConnections_1.WebSocketConnectionManager.updateActivity(connectionId);
if (parseJson) {
try {
messageData = JSON.parse(messageData);
}
catch (error) {
}
}
if (triggerOn === 'message' || triggerOn === 'both') {
const executionData = [{
json: {
event: 'message',
data: messageData,
connectionInfo,
meta: {
totalConnections: WebSocketConnections_1.WebSocketConnectionManager.getConnectionCount(),
messageSize: data.length,
timestamp: new Date().toISOString(),
},
},
}];
this.emit([executionData]);
}
if (ws.readyState === ws_1.WebSocket.OPEN) {
if (responseMode === 'echo') {
ws.send(data);
}
else if (responseMode === 'custom' && customResponse) {
const response = WebSocketConnections_1.WebSocketConnectionManager.processMessageTemplate(customResponse, connectionInfo);
ws.send(response);
}
}
if (autoMessages.autoReplyAfterMessage && autoMessages.autoReplyMessage) {
const delay = (autoMessages.autoReplyDelay || 5) * 1000;
const replyMsg = WebSocketConnections_1.WebSocketConnectionManager.processMessageTemplate(autoMessages.autoReplyMessage, connectionInfo);
setTimeout(() => {
WebSocketConnections_1.WebSocketConnectionManager.sendToConnection(connectionId, replyMsg);
}, delay);
}
});
ws.on('close', () => {
activeConnections--;
WebSocketConnections_1.WebSocketConnectionManager.removeConnection(connectionId);
});
ws.on('error', (error) => {
console.error(`WebSocket error for ${connectionId}:`, error);
activeConnections--;
WebSocketConnections_1.WebSocketConnectionManager.removeConnection(connectionId);
});
});
wss.on('error', (error) => {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `WebSocket server error: ${error.message}`);
});
console.log(`WebSocket server started on ${bindAddress}:${port}${path}`);
console.log(`Features enabled: Welcome=${autoMessages.sendWelcome}, Periodic=${autoMessages.sendPeriodic}, AutoReply=${autoMessages.autoReplyAfterMessage}`);
return {
closeFunction: async () => {
if (pingIntervalId) {
clearInterval(pingIntervalId);
}
if (periodicIntervalId) {
clearInterval(periodicIntervalId);
}
if (cleanupIntervalId) {
clearInterval(cleanupIntervalId);
}
wss.close();
console.log(`WebSocket server stopped on ${bindAddress}:${port}${path}`);
},
};
}
}
exports.Websocket = Websocket;
exports.WebsocketTrigger = Websocket;
//# sourceMappingURL=Websocket.node.js.map