UNPKG

n8n-nodes-websocket

Version:

Enhanced WebSocket nodes for n8n with bidirectional communication support

538 lines 25.9 kB
"use strict"; 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