@tehw0lf/n8n-nodes-unix-socket-bridge 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -212,6 +212,34 @@ Run any custom script or command:
|
|
|
212
212
|
}
|
|
213
213
|
```
|
|
214
214
|
|
|
215
|
+
## Parameter Validation
|
|
216
|
+
|
|
217
|
+
The server supports comprehensive parameter validation for security and correctness:
|
|
218
|
+
|
|
219
|
+
### Parameter Types and Styles
|
|
220
|
+
|
|
221
|
+
| Type | Description | Validation |
|
|
222
|
+
|------|-------------|------------|
|
|
223
|
+
| `string` | Text values | Regex pattern, length limits |
|
|
224
|
+
| `number` | Numeric values | Min/max value validation |
|
|
225
|
+
| `boolean` | True/false | Converted to command flags |
|
|
226
|
+
| `json` | Complex objects | JSON string validation |
|
|
227
|
+
|
|
228
|
+
| Style | Format | Example |
|
|
229
|
+
|-------|--------|---------|
|
|
230
|
+
| `argument` | Positional argument | `command value` |
|
|
231
|
+
| `flag` | `--name value` | `command --verbose true` |
|
|
232
|
+
| `single_flag` | `--name=value` | `command --player=spotify` |
|
|
233
|
+
|
|
234
|
+
### Security Features
|
|
235
|
+
|
|
236
|
+
- **Regex Validation**: String parameters enforce pattern matching
|
|
237
|
+
- **Length Limits**: `max_length` prevents oversized inputs
|
|
238
|
+
- **Type Safety**: Parameters validated before execution
|
|
239
|
+
- **Required Fields**: Missing required parameters are rejected
|
|
240
|
+
|
|
241
|
+
See the [main repository](https://github.com/tehw0lf/n8n-nodes-unix-socket-bridge) examples for complete parameter configuration.
|
|
242
|
+
|
|
215
243
|
## Server Component
|
|
216
244
|
|
|
217
245
|
This node requires the Unix Socket Bridge server to be running. The server:
|
package/package.json
CHANGED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { IExecuteFunctions, ILoadOptionsFunctions, INodeExecutionData, INodePropertyOptions, INodeType, INodeTypeDescription } from "n8n-workflow";
|
|
2
|
-
/**
|
|
3
|
-
* Unix Socket Bridge Node for n8n
|
|
4
|
-
*
|
|
5
|
-
* This node provides a generic interface for communicating with Unix domain socket servers.
|
|
6
|
-
* It supports auto-discovery of available commands, parameter validation, and multiple
|
|
7
|
-
* response formats. Security: All authentication tokens are hashed using SHA-256 before transmission.
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* // Basic usage with auto-discovery
|
|
11
|
-
* const node = new UnixSocketBridge();
|
|
12
|
-
* // Configure socket path: /tmp/socket-bridge/playerctl.sock
|
|
13
|
-
* // Enable auto-discovery to see available commands
|
|
14
|
-
* // Select command from dropdown and execute
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* // Manual command execution
|
|
18
|
-
* const node = new UnixSocketBridge();
|
|
19
|
-
* // Set autoDiscover to false
|
|
20
|
-
* // Manually specify command and parameters
|
|
21
|
-
*
|
|
22
|
-
* @since 1.0.0
|
|
23
|
-
*/
|
|
24
|
-
export declare class UnixSocketBridge implements INodeType {
|
|
25
|
-
description: INodeTypeDescription;
|
|
26
|
-
/**
|
|
27
|
-
* Methods available on this node for dynamic option loading
|
|
28
|
-
*/
|
|
29
|
-
methods: {
|
|
30
|
-
loadOptions: {
|
|
31
|
-
/**
|
|
32
|
-
* Discovers available commands from a Unix socket server
|
|
33
|
-
*
|
|
34
|
-
* This method connects to the specified socket and sends an introspection
|
|
35
|
-
* request to discover what commands are available. It provides user-friendly
|
|
36
|
-
* error messages and fallback options when the server is unavailable.
|
|
37
|
-
*
|
|
38
|
-
* @param this - The load options function context from n8n
|
|
39
|
-
* @returns Promise resolving to an array of command options for the dropdown
|
|
40
|
-
*/
|
|
41
|
-
getAvailableCommands(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
42
|
-
};
|
|
43
|
-
};
|
|
44
|
-
/**
|
|
45
|
-
* Executes the Unix Socket Bridge node
|
|
46
|
-
*
|
|
47
|
-
* This method handles the main execution logic, including:
|
|
48
|
-
* - Parameter validation and processing
|
|
49
|
-
* - Socket communication
|
|
50
|
-
* - Response parsing and formatting
|
|
51
|
-
* - Error handling with optional continuation
|
|
52
|
-
*
|
|
53
|
-
* @param this - The execution function context from n8n
|
|
54
|
-
* @returns Promise resolving to node execution data
|
|
55
|
-
*/
|
|
56
|
-
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Helper function for Unix socket communication with improved robustness
|
|
60
|
-
*
|
|
61
|
-
* Establishes a connection to a Unix domain socket, sends a message,
|
|
62
|
-
* and returns the response. Includes timeout handling, chunked reading
|
|
63
|
-
* for large responses, and proper cleanup of socket resources.
|
|
64
|
-
*
|
|
65
|
-
* @param socketPath - Path to the Unix domain socket file
|
|
66
|
-
* @param message - Message to send (typically JSON-formatted command)
|
|
67
|
-
* @param timeoutMs - Connection timeout in milliseconds
|
|
68
|
-
* @param maxResponseSize - Maximum response size in bytes
|
|
69
|
-
* @returns Promise resolving to the server response as a string
|
|
70
|
-
*
|
|
71
|
-
* @throws {Error} When socket connection fails, times out, or response too large
|
|
72
|
-
*/
|
|
73
|
-
export declare function sendToUnixSocket(socketPath: string, message: string, timeoutMs: number, maxResponseSize?: number): Promise<string>;
|
|
@@ -1,717 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
-
if (mod && mod.__esModule) return mod;
|
|
20
|
-
var result = {};
|
|
21
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
-
__setModuleDefault(result, mod);
|
|
23
|
-
return result;
|
|
24
|
-
};
|
|
25
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.sendToUnixSocket = exports.UnixSocketBridge = void 0;
|
|
27
|
-
const n8n_workflow_1 = require("n8n-workflow");
|
|
28
|
-
const net = __importStar(require("net"));
|
|
29
|
-
const crypto = __importStar(require("crypto"));
|
|
30
|
-
/**
|
|
31
|
-
* Securely hash a token using SHA-256
|
|
32
|
-
*
|
|
33
|
-
* @param token - The plain text token to hash
|
|
34
|
-
* @returns SHA-256 hash of the token in hexadecimal format
|
|
35
|
-
*/
|
|
36
|
-
function hashToken(token) {
|
|
37
|
-
return crypto.createHash('sha256').update(token).digest('hex');
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Unix Socket Bridge Node for n8n
|
|
41
|
-
*
|
|
42
|
-
* This node provides a generic interface for communicating with Unix domain socket servers.
|
|
43
|
-
* It supports auto-discovery of available commands, parameter validation, and multiple
|
|
44
|
-
* response formats. Security: All authentication tokens are hashed using SHA-256 before transmission.
|
|
45
|
-
*
|
|
46
|
-
* @example
|
|
47
|
-
* // Basic usage with auto-discovery
|
|
48
|
-
* const node = new UnixSocketBridge();
|
|
49
|
-
* // Configure socket path: /tmp/socket-bridge/playerctl.sock
|
|
50
|
-
* // Enable auto-discovery to see available commands
|
|
51
|
-
* // Select command from dropdown and execute
|
|
52
|
-
*
|
|
53
|
-
* @example
|
|
54
|
-
* // Manual command execution
|
|
55
|
-
* const node = new UnixSocketBridge();
|
|
56
|
-
* // Set autoDiscover to false
|
|
57
|
-
* // Manually specify command and parameters
|
|
58
|
-
*
|
|
59
|
-
* @since 1.0.0
|
|
60
|
-
*/
|
|
61
|
-
class UnixSocketBridge {
|
|
62
|
-
constructor() {
|
|
63
|
-
this.description = {
|
|
64
|
-
displayName: "Unix Socket Bridge",
|
|
65
|
-
name: "unixSocketBridge",
|
|
66
|
-
icon: "fa:plug",
|
|
67
|
-
group: ["communication"],
|
|
68
|
-
version: 1,
|
|
69
|
-
subtitle: '={{$parameter["autoDiscover"] ? $parameter["discoveredCommand"] : $parameter["command"]}} @ {{$parameter["socketPath"]}}',
|
|
70
|
-
description: "Unix domain socket communication with configurable servers",
|
|
71
|
-
defaults: {
|
|
72
|
-
name: "Unix Socket Bridge",
|
|
73
|
-
},
|
|
74
|
-
inputs: ["main" /* NodeConnectionType.Main */],
|
|
75
|
-
outputs: ["main" /* NodeConnectionType.Main */],
|
|
76
|
-
credentials: [
|
|
77
|
-
{
|
|
78
|
-
name: "httpHeaderAuth",
|
|
79
|
-
required: false,
|
|
80
|
-
},
|
|
81
|
-
],
|
|
82
|
-
properties: [
|
|
83
|
-
{
|
|
84
|
-
displayName: "Socket Path",
|
|
85
|
-
name: "socketPath",
|
|
86
|
-
type: "string",
|
|
87
|
-
default: "/tmp/socket-bridge/service.sock",
|
|
88
|
-
placeholder: "/tmp/socket-bridge/service.sock",
|
|
89
|
-
description: "Path to the Unix domain socket file",
|
|
90
|
-
required: true,
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
displayName: "Auto-Discover Commands",
|
|
94
|
-
name: "autoDiscover",
|
|
95
|
-
type: "boolean",
|
|
96
|
-
default: true,
|
|
97
|
-
description: "Automatically discover available commands from the server (recommended)",
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
displayName: "Available Commands",
|
|
101
|
-
name: "discoveredCommand",
|
|
102
|
-
type: "options",
|
|
103
|
-
typeOptions: {
|
|
104
|
-
loadOptionsMethod: "getAvailableCommands",
|
|
105
|
-
loadOptionsDependsOn: ["socketPath"], // Force reload when socket path changes
|
|
106
|
-
},
|
|
107
|
-
displayOptions: {
|
|
108
|
-
show: {
|
|
109
|
-
autoDiscover: [true],
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
default: "",
|
|
113
|
-
description: "Choose from available commands on the server",
|
|
114
|
-
placeholder: "Select a command...",
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
displayName: "Refresh Commands",
|
|
118
|
-
name: "refreshCommands",
|
|
119
|
-
type: "notice",
|
|
120
|
-
displayOptions: {
|
|
121
|
-
show: {
|
|
122
|
-
autoDiscover: [true],
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
default: "",
|
|
126
|
-
description: "If commands don't load, try changing the socket path slightly and changing it back, or toggle auto-discovery off/on",
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
displayName: "Manual Command",
|
|
130
|
-
name: "command",
|
|
131
|
-
type: "string",
|
|
132
|
-
displayOptions: {
|
|
133
|
-
show: {
|
|
134
|
-
autoDiscover: [false],
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
default: '={{ $json.command || "status" }}',
|
|
138
|
-
description: "Command to send to the server (manual mode)",
|
|
139
|
-
placeholder: "status",
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
displayName: "Parameters",
|
|
143
|
-
name: "parameters",
|
|
144
|
-
type: "fixedCollection",
|
|
145
|
-
placeholder: "Add Parameter",
|
|
146
|
-
typeOptions: {
|
|
147
|
-
multipleValues: true,
|
|
148
|
-
},
|
|
149
|
-
default: {},
|
|
150
|
-
options: [
|
|
151
|
-
{
|
|
152
|
-
name: "parameter",
|
|
153
|
-
displayName: "Parameter",
|
|
154
|
-
values: [
|
|
155
|
-
{
|
|
156
|
-
displayName: "Name",
|
|
157
|
-
name: "name",
|
|
158
|
-
type: "string",
|
|
159
|
-
default: "",
|
|
160
|
-
description: 'Parameter name (e.g., "player")',
|
|
161
|
-
placeholder: "player",
|
|
162
|
-
required: false,
|
|
163
|
-
},
|
|
164
|
-
{
|
|
165
|
-
displayName: "Value",
|
|
166
|
-
name: "value",
|
|
167
|
-
type: "string",
|
|
168
|
-
default: "",
|
|
169
|
-
description: 'Parameter value (e.g., "spotify")',
|
|
170
|
-
placeholder: "spotify",
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
displayName: "Type",
|
|
174
|
-
name: "type",
|
|
175
|
-
type: "options",
|
|
176
|
-
options: [
|
|
177
|
-
{
|
|
178
|
-
name: "Auto-Detect",
|
|
179
|
-
value: "auto",
|
|
180
|
-
description: "Automatically detect the type",
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
name: "String",
|
|
184
|
-
value: "string",
|
|
185
|
-
description: "Text value",
|
|
186
|
-
},
|
|
187
|
-
{
|
|
188
|
-
name: "Number",
|
|
189
|
-
value: "number",
|
|
190
|
-
description: "Numeric value",
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
name: "Boolean",
|
|
194
|
-
value: "boolean",
|
|
195
|
-
description: "True/False value",
|
|
196
|
-
},
|
|
197
|
-
{
|
|
198
|
-
name: "JSON",
|
|
199
|
-
value: "json",
|
|
200
|
-
description: "Complex JSON value",
|
|
201
|
-
},
|
|
202
|
-
],
|
|
203
|
-
default: "auto",
|
|
204
|
-
description: "Parameter value type",
|
|
205
|
-
},
|
|
206
|
-
],
|
|
207
|
-
},
|
|
208
|
-
],
|
|
209
|
-
description: "Additional parameters to pass with the command",
|
|
210
|
-
},
|
|
211
|
-
{
|
|
212
|
-
displayName: "Timeout (ms)",
|
|
213
|
-
name: "timeout",
|
|
214
|
-
type: "number",
|
|
215
|
-
default: 5000,
|
|
216
|
-
description: "Connection timeout in milliseconds",
|
|
217
|
-
typeOptions: {
|
|
218
|
-
minValue: 1000,
|
|
219
|
-
maxValue: 30000,
|
|
220
|
-
},
|
|
221
|
-
},
|
|
222
|
-
{
|
|
223
|
-
displayName: "Response Format",
|
|
224
|
-
name: "responseFormat",
|
|
225
|
-
type: "options",
|
|
226
|
-
options: [
|
|
227
|
-
{
|
|
228
|
-
name: "Auto-Detect",
|
|
229
|
-
value: "auto",
|
|
230
|
-
description: "Automatically detect JSON or return as text",
|
|
231
|
-
},
|
|
232
|
-
{
|
|
233
|
-
name: "JSON",
|
|
234
|
-
value: "json",
|
|
235
|
-
description: "Parse response as JSON (will error if not valid JSON)",
|
|
236
|
-
},
|
|
237
|
-
{
|
|
238
|
-
name: "Text",
|
|
239
|
-
value: "text",
|
|
240
|
-
description: "Return response as plain text",
|
|
241
|
-
},
|
|
242
|
-
],
|
|
243
|
-
default: "auto",
|
|
244
|
-
description: "How to interpret the server response",
|
|
245
|
-
},
|
|
246
|
-
{
|
|
247
|
-
displayName: "Options",
|
|
248
|
-
name: "options",
|
|
249
|
-
type: "collection",
|
|
250
|
-
placeholder: "Add Option",
|
|
251
|
-
default: {},
|
|
252
|
-
options: [
|
|
253
|
-
{
|
|
254
|
-
displayName: "Continue On Fail",
|
|
255
|
-
name: "continueOnFail",
|
|
256
|
-
type: "boolean",
|
|
257
|
-
default: false,
|
|
258
|
-
description: "Whether to continue workflow execution on error",
|
|
259
|
-
},
|
|
260
|
-
{
|
|
261
|
-
displayName: "Max Response Size",
|
|
262
|
-
name: "maxResponseSize",
|
|
263
|
-
type: "number",
|
|
264
|
-
default: 1048576,
|
|
265
|
-
description: "Maximum response size in bytes (default: 1MB)",
|
|
266
|
-
typeOptions: {
|
|
267
|
-
minValue: 1024,
|
|
268
|
-
maxValue: 10485760,
|
|
269
|
-
},
|
|
270
|
-
},
|
|
271
|
-
{
|
|
272
|
-
displayName: "Include Metadata",
|
|
273
|
-
name: "includeMetadata",
|
|
274
|
-
type: "boolean",
|
|
275
|
-
default: true,
|
|
276
|
-
description: "Include metadata like timestamp and socket path in response",
|
|
277
|
-
},
|
|
278
|
-
],
|
|
279
|
-
},
|
|
280
|
-
],
|
|
281
|
-
};
|
|
282
|
-
/**
|
|
283
|
-
* Methods available on this node for dynamic option loading
|
|
284
|
-
*/
|
|
285
|
-
this.methods = {
|
|
286
|
-
loadOptions: {
|
|
287
|
-
/**
|
|
288
|
-
* Discovers available commands from a Unix socket server
|
|
289
|
-
*
|
|
290
|
-
* This method connects to the specified socket and sends an introspection
|
|
291
|
-
* request to discover what commands are available. It provides user-friendly
|
|
292
|
-
* error messages and fallback options when the server is unavailable.
|
|
293
|
-
*
|
|
294
|
-
* @param this - The load options function context from n8n
|
|
295
|
-
* @returns Promise resolving to an array of command options for the dropdown
|
|
296
|
-
*/
|
|
297
|
-
async getAvailableCommands() {
|
|
298
|
-
const socketPath = this.getNodeParameter("socketPath");
|
|
299
|
-
const timeout = 5000;
|
|
300
|
-
// Get authentication token from credentials (secure approach only)
|
|
301
|
-
let authToken;
|
|
302
|
-
try {
|
|
303
|
-
const credentials = await this.getCredentials('httpHeaderAuth');
|
|
304
|
-
if (credentials && credentials.value) {
|
|
305
|
-
authToken = credentials.value;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
catch (error) {
|
|
309
|
-
// Credentials not configured - authentication will be skipped
|
|
310
|
-
}
|
|
311
|
-
// Provide immediate feedback
|
|
312
|
-
if (!socketPath || socketPath.trim() === "") {
|
|
313
|
-
return [{ name: "⚠️ Please set a socket path first", value: "" }];
|
|
314
|
-
}
|
|
315
|
-
try {
|
|
316
|
-
const introspectionRequest = {
|
|
317
|
-
command: "__introspect__",
|
|
318
|
-
request_id: `discovery-${Date.now()}`
|
|
319
|
-
};
|
|
320
|
-
// Add auth token hash if provided (security: never send plain text tokens)
|
|
321
|
-
if (authToken && authToken.trim() !== "") {
|
|
322
|
-
introspectionRequest.auth_token_hash = hashToken(authToken.trim());
|
|
323
|
-
}
|
|
324
|
-
const response = await sendToUnixSocket(socketPath, JSON.stringify(introspectionRequest), timeout);
|
|
325
|
-
const serverInfo = JSON.parse(response);
|
|
326
|
-
if (serverInfo.success &&
|
|
327
|
-
serverInfo.server_info &&
|
|
328
|
-
serverInfo.server_info.commands) {
|
|
329
|
-
const commands = serverInfo.server_info.commands;
|
|
330
|
-
const options = [];
|
|
331
|
-
// Add server info as header
|
|
332
|
-
options.push({
|
|
333
|
-
name: `📡 ${serverInfo.server_info.name} (${Object.keys(commands).length} commands available)`,
|
|
334
|
-
value: "__server_info__",
|
|
335
|
-
});
|
|
336
|
-
// Add separator
|
|
337
|
-
options.push({
|
|
338
|
-
name: "──────────────────────────",
|
|
339
|
-
value: "",
|
|
340
|
-
});
|
|
341
|
-
// Add commands with better formatting
|
|
342
|
-
for (const [commandName, commandInfo] of Object.entries(commands)) {
|
|
343
|
-
const description = commandInfo.description || "No description";
|
|
344
|
-
const hasParams = commandInfo.parameters &&
|
|
345
|
-
Object.keys(commandInfo.parameters).length > 0;
|
|
346
|
-
const paramIndicator = hasParams ? " 🔧" : "";
|
|
347
|
-
options.push({
|
|
348
|
-
name: `${commandName}${paramIndicator} - ${description}`,
|
|
349
|
-
value: commandName,
|
|
350
|
-
description: hasParams
|
|
351
|
-
? "This command accepts parameters"
|
|
352
|
-
: undefined,
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
return options;
|
|
356
|
-
}
|
|
357
|
-
else if (serverInfo.error) {
|
|
358
|
-
return [
|
|
359
|
-
{ name: `❌ Server error: ${serverInfo.error}`, value: "" },
|
|
360
|
-
{ name: "Check server logs for details", value: "" },
|
|
361
|
-
];
|
|
362
|
-
}
|
|
363
|
-
else {
|
|
364
|
-
return [
|
|
365
|
-
{ name: "⚠️ Server responded but no commands found", value: "" },
|
|
366
|
-
{ name: "Check server configuration", value: "" },
|
|
367
|
-
];
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
catch (error) {
|
|
371
|
-
// More specific error messages
|
|
372
|
-
if (error.message.includes("timeout")) {
|
|
373
|
-
return [
|
|
374
|
-
{
|
|
375
|
-
name: "⏱️ Connection timeout - is the server running?",
|
|
376
|
-
value: "",
|
|
377
|
-
},
|
|
378
|
-
{
|
|
379
|
-
name: "Try: python3 socket-server.py config.json",
|
|
380
|
-
value: "",
|
|
381
|
-
},
|
|
382
|
-
];
|
|
383
|
-
}
|
|
384
|
-
else if (error.message.includes("ENOENT") ||
|
|
385
|
-
error.message.includes("not found")) {
|
|
386
|
-
return [
|
|
387
|
-
{ name: "🔍 Socket file not found", value: "" },
|
|
388
|
-
{ name: `Path: ${socketPath}`, value: "" },
|
|
389
|
-
{ name: "Check if server is running", value: "" },
|
|
390
|
-
];
|
|
391
|
-
}
|
|
392
|
-
else if (error.message.includes("ECONNREFUSED")) {
|
|
393
|
-
return [
|
|
394
|
-
{ name: "🚫 Connection refused", value: "" },
|
|
395
|
-
{ name: "Server may not be listening on this socket", value: "" },
|
|
396
|
-
];
|
|
397
|
-
}
|
|
398
|
-
else if (error.message.includes("EACCES")) {
|
|
399
|
-
return [
|
|
400
|
-
{ name: "🔒 Permission denied", value: "" },
|
|
401
|
-
{ name: "Check socket file permissions", value: "" },
|
|
402
|
-
];
|
|
403
|
-
}
|
|
404
|
-
else {
|
|
405
|
-
const shortError = error.message.substring(0, 50);
|
|
406
|
-
return [
|
|
407
|
-
{
|
|
408
|
-
name: `❌ Error: ${shortError}${error.message.length > 50 ? "..." : ""}`,
|
|
409
|
-
value: "",
|
|
410
|
-
},
|
|
411
|
-
{ name: "You can still use manual mode", value: "" },
|
|
412
|
-
];
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
},
|
|
416
|
-
},
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
/**
|
|
420
|
-
* Executes the Unix Socket Bridge node
|
|
421
|
-
*
|
|
422
|
-
* This method handles the main execution logic, including:
|
|
423
|
-
* - Parameter validation and processing
|
|
424
|
-
* - Socket communication
|
|
425
|
-
* - Response parsing and formatting
|
|
426
|
-
* - Error handling with optional continuation
|
|
427
|
-
*
|
|
428
|
-
* @param this - The execution function context from n8n
|
|
429
|
-
* @returns Promise resolving to node execution data
|
|
430
|
-
*/
|
|
431
|
-
async execute() {
|
|
432
|
-
const items = this.getInputData();
|
|
433
|
-
const returnData = [];
|
|
434
|
-
for (let i = 0; i < items.length; i++) {
|
|
435
|
-
try {
|
|
436
|
-
const socketPath = this.getNodeParameter("socketPath", i);
|
|
437
|
-
const autoDiscover = this.getNodeParameter("autoDiscover", i);
|
|
438
|
-
const timeout = this.getNodeParameter("timeout", i);
|
|
439
|
-
const responseFormat = this.getNodeParameter("responseFormat", i);
|
|
440
|
-
// Get options
|
|
441
|
-
const options = this.getNodeParameter("options", i, {});
|
|
442
|
-
const maxResponseSize = options.maxResponseSize || 1048576;
|
|
443
|
-
const includeMetadata = options.includeMetadata !== false;
|
|
444
|
-
let command;
|
|
445
|
-
if (autoDiscover) {
|
|
446
|
-
command = this.getNodeParameter("discoveredCommand", i);
|
|
447
|
-
// Validate command selection
|
|
448
|
-
if (command === "__server_info__" || command === "" || !command) {
|
|
449
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), "Please select a valid command from the dropdown", {
|
|
450
|
-
itemIndex: i,
|
|
451
|
-
description: "Use the dropdown to select an available command",
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
else {
|
|
456
|
-
command = this.getNodeParameter("command", i);
|
|
457
|
-
if (!command) {
|
|
458
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), "Command is required", { itemIndex: i });
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
// Get authentication token from credentials (secure approach only)
|
|
462
|
-
let authToken;
|
|
463
|
-
try {
|
|
464
|
-
const credentials = await this.getCredentials('httpHeaderAuth');
|
|
465
|
-
if (credentials && credentials.value) {
|
|
466
|
-
authToken = credentials.value;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
catch (error) {
|
|
470
|
-
// Credentials not configured - authentication will be skipped
|
|
471
|
-
}
|
|
472
|
-
// Build the request
|
|
473
|
-
const jsonMessage = {
|
|
474
|
-
command,
|
|
475
|
-
request_id: `n8n-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
476
|
-
};
|
|
477
|
-
// Add auth token hash if provided (security: never send plain text tokens)
|
|
478
|
-
if (authToken && authToken.trim() !== "") {
|
|
479
|
-
jsonMessage.auth_token_hash = hashToken(authToken.trim());
|
|
480
|
-
}
|
|
481
|
-
// Process parameters with type handling
|
|
482
|
-
const parameters = this.getNodeParameter("parameters", i, {});
|
|
483
|
-
if (parameters &&
|
|
484
|
-
parameters.parameter &&
|
|
485
|
-
Array.isArray(parameters.parameter) &&
|
|
486
|
-
parameters.parameter.length > 0) {
|
|
487
|
-
jsonMessage.parameters = {};
|
|
488
|
-
for (const param of parameters.parameter) {
|
|
489
|
-
if (param.name && param.value !== undefined && param.value !== "") {
|
|
490
|
-
const paramType = param.type || "auto";
|
|
491
|
-
let processedValue = param.value;
|
|
492
|
-
try {
|
|
493
|
-
switch (paramType) {
|
|
494
|
-
case "number":
|
|
495
|
-
processedValue = Number(param.value);
|
|
496
|
-
if (isNaN(processedValue)) {
|
|
497
|
-
throw new Error(`Invalid number: ${param.value}`);
|
|
498
|
-
}
|
|
499
|
-
break;
|
|
500
|
-
case "boolean":
|
|
501
|
-
if (typeof param.value === "string") {
|
|
502
|
-
processedValue = ["true", "yes", "1", "on"].includes(param.value.toLowerCase());
|
|
503
|
-
}
|
|
504
|
-
else {
|
|
505
|
-
processedValue = Boolean(param.value);
|
|
506
|
-
}
|
|
507
|
-
break;
|
|
508
|
-
case "json":
|
|
509
|
-
processedValue = JSON.parse(param.value);
|
|
510
|
-
break;
|
|
511
|
-
case "string":
|
|
512
|
-
processedValue = String(param.value);
|
|
513
|
-
break;
|
|
514
|
-
case "auto":
|
|
515
|
-
default:
|
|
516
|
-
// Try to parse as JSON first
|
|
517
|
-
try {
|
|
518
|
-
processedValue = JSON.parse(param.value);
|
|
519
|
-
}
|
|
520
|
-
catch (_a) {
|
|
521
|
-
// If not JSON, keep as string
|
|
522
|
-
processedValue = param.value;
|
|
523
|
-
}
|
|
524
|
-
break;
|
|
525
|
-
}
|
|
526
|
-
jsonMessage.parameters[param.name] = processedValue;
|
|
527
|
-
}
|
|
528
|
-
catch (error) {
|
|
529
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to process parameter "${param.name}": ${error.message}`, { itemIndex: i });
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
const messageToSend = JSON.stringify(jsonMessage);
|
|
535
|
-
// Send message to Unix socket with size limit
|
|
536
|
-
const response = await sendToUnixSocket(socketPath, messageToSend, timeout, maxResponseSize);
|
|
537
|
-
// Parse response based on format setting
|
|
538
|
-
let parsedResponse;
|
|
539
|
-
if (responseFormat === "auto") {
|
|
540
|
-
try {
|
|
541
|
-
parsedResponse = JSON.parse(response);
|
|
542
|
-
}
|
|
543
|
-
catch (_b) {
|
|
544
|
-
parsedResponse = response;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
else if (responseFormat === "json") {
|
|
548
|
-
try {
|
|
549
|
-
parsedResponse = JSON.parse(response);
|
|
550
|
-
}
|
|
551
|
-
catch (error) {
|
|
552
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to parse response as JSON: ${error.message}`, {
|
|
553
|
-
itemIndex: i,
|
|
554
|
-
description: "Server response is not valid JSON. Try using 'Text' response format.",
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
else {
|
|
559
|
-
parsedResponse = response;
|
|
560
|
-
}
|
|
561
|
-
// Build response data
|
|
562
|
-
let responseData;
|
|
563
|
-
if (includeMetadata) {
|
|
564
|
-
responseData = {
|
|
565
|
-
socketPath,
|
|
566
|
-
command,
|
|
567
|
-
request: jsonMessage,
|
|
568
|
-
response: parsedResponse,
|
|
569
|
-
success: true,
|
|
570
|
-
timestamp: new Date().toISOString(),
|
|
571
|
-
};
|
|
572
|
-
// Extract structured response fields if available
|
|
573
|
-
if (typeof parsedResponse === "object" &&
|
|
574
|
-
parsedResponse !== null &&
|
|
575
|
-
"success" in parsedResponse) {
|
|
576
|
-
const cmdResponse = parsedResponse;
|
|
577
|
-
responseData.success = cmdResponse.success;
|
|
578
|
-
responseData.output =
|
|
579
|
-
cmdResponse.stdout || cmdResponse.output || "";
|
|
580
|
-
responseData.error = cmdResponse.error || cmdResponse.stderr || "";
|
|
581
|
-
responseData.returncode = cmdResponse.returncode;
|
|
582
|
-
// Include parsed output if available
|
|
583
|
-
if (cmdResponse.parsed_output) {
|
|
584
|
-
responseData.parsedOutput = cmdResponse.parsed_output;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
else {
|
|
589
|
-
// Return just the response without metadata
|
|
590
|
-
if (typeof parsedResponse === "object" &&
|
|
591
|
-
parsedResponse !== null &&
|
|
592
|
-
"success" in parsedResponse) {
|
|
593
|
-
responseData = parsedResponse;
|
|
594
|
-
}
|
|
595
|
-
else {
|
|
596
|
-
responseData = { response: parsedResponse };
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
returnData.push({
|
|
600
|
-
json: responseData,
|
|
601
|
-
pairedItem: i,
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
catch (error) {
|
|
605
|
-
if (this.continueOnFail()) {
|
|
606
|
-
returnData.push({
|
|
607
|
-
json: {
|
|
608
|
-
error: error.message,
|
|
609
|
-
success: false,
|
|
610
|
-
timestamp: new Date().toISOString(),
|
|
611
|
-
itemIndex: i,
|
|
612
|
-
},
|
|
613
|
-
pairedItem: i,
|
|
614
|
-
});
|
|
615
|
-
continue;
|
|
616
|
-
}
|
|
617
|
-
throw error;
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
return [returnData];
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
exports.UnixSocketBridge = UnixSocketBridge;
|
|
624
|
-
/**
|
|
625
|
-
* Helper function for Unix socket communication with improved robustness
|
|
626
|
-
*
|
|
627
|
-
* Establishes a connection to a Unix domain socket, sends a message,
|
|
628
|
-
* and returns the response. Includes timeout handling, chunked reading
|
|
629
|
-
* for large responses, and proper cleanup of socket resources.
|
|
630
|
-
*
|
|
631
|
-
* @param socketPath - Path to the Unix domain socket file
|
|
632
|
-
* @param message - Message to send (typically JSON-formatted command)
|
|
633
|
-
* @param timeoutMs - Connection timeout in milliseconds
|
|
634
|
-
* @param maxResponseSize - Maximum response size in bytes
|
|
635
|
-
* @returns Promise resolving to the server response as a string
|
|
636
|
-
*
|
|
637
|
-
* @throws {Error} When socket connection fails, times out, or response too large
|
|
638
|
-
*/
|
|
639
|
-
async function sendToUnixSocket(socketPath, message, timeoutMs, maxResponseSize = 1048576) {
|
|
640
|
-
return new Promise((resolve, reject) => {
|
|
641
|
-
const socket = new net.Socket();
|
|
642
|
-
let response = "";
|
|
643
|
-
let responseBuffer = Buffer.alloc(0);
|
|
644
|
-
const timeoutHandle = setTimeout(() => {
|
|
645
|
-
socket.destroy();
|
|
646
|
-
reject(new Error(`Socket connection timeout after ${timeoutMs}ms`));
|
|
647
|
-
}, timeoutMs);
|
|
648
|
-
socket.on("connect", () => {
|
|
649
|
-
socket.write(message);
|
|
650
|
-
});
|
|
651
|
-
socket.on("data", (data) => {
|
|
652
|
-
// Use buffer concatenation for binary safety
|
|
653
|
-
responseBuffer = Buffer.concat([responseBuffer, data]);
|
|
654
|
-
// Check size limit
|
|
655
|
-
if (responseBuffer.length > maxResponseSize) {
|
|
656
|
-
socket.destroy();
|
|
657
|
-
clearTimeout(timeoutHandle);
|
|
658
|
-
reject(new Error(`Response too large (max ${maxResponseSize} bytes)`));
|
|
659
|
-
}
|
|
660
|
-
});
|
|
661
|
-
socket.on("end", () => {
|
|
662
|
-
clearTimeout(timeoutHandle);
|
|
663
|
-
try {
|
|
664
|
-
response = responseBuffer.toString("utf-8");
|
|
665
|
-
resolve(response);
|
|
666
|
-
}
|
|
667
|
-
catch (error) {
|
|
668
|
-
reject(new Error(`Failed to decode response: ${error.message}`));
|
|
669
|
-
}
|
|
670
|
-
});
|
|
671
|
-
socket.on("close", (hadError) => {
|
|
672
|
-
clearTimeout(timeoutHandle);
|
|
673
|
-
if (!hadError && responseBuffer.length > 0) {
|
|
674
|
-
try {
|
|
675
|
-
response = responseBuffer.toString("utf-8");
|
|
676
|
-
resolve(response);
|
|
677
|
-
}
|
|
678
|
-
catch (error) {
|
|
679
|
-
reject(new Error(`Failed to decode response: ${error.message}`));
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
else if (!hadError && response) {
|
|
683
|
-
resolve(response);
|
|
684
|
-
}
|
|
685
|
-
else if (!hadError) {
|
|
686
|
-
reject(new Error("Socket closed without response"));
|
|
687
|
-
}
|
|
688
|
-
});
|
|
689
|
-
socket.on("error", (error) => {
|
|
690
|
-
clearTimeout(timeoutHandle);
|
|
691
|
-
// Provide more helpful error messages
|
|
692
|
-
let errorMessage = error.message;
|
|
693
|
-
if (error.message.includes("ENOENT")) {
|
|
694
|
-
errorMessage = `Socket not found at ${socketPath}`;
|
|
695
|
-
}
|
|
696
|
-
else if (error.message.includes("ECONNREFUSED")) {
|
|
697
|
-
errorMessage = `Connection refused at ${socketPath} - is the server running?`;
|
|
698
|
-
}
|
|
699
|
-
else if (error.message.includes("EACCES")) {
|
|
700
|
-
errorMessage = `Permission denied accessing ${socketPath}`;
|
|
701
|
-
}
|
|
702
|
-
else if (error.message.includes("ETIMEDOUT")) {
|
|
703
|
-
errorMessage = `Connection timed out to ${socketPath}`;
|
|
704
|
-
}
|
|
705
|
-
reject(new Error(`Socket error: ${errorMessage}`));
|
|
706
|
-
});
|
|
707
|
-
// Connect to socket
|
|
708
|
-
try {
|
|
709
|
-
socket.connect(socketPath);
|
|
710
|
-
}
|
|
711
|
-
catch (error) {
|
|
712
|
-
clearTimeout(timeoutHandle);
|
|
713
|
-
reject(new Error(`Failed to connect: ${error.message}`));
|
|
714
|
-
}
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
exports.sendToUnixSocket = sendToUnixSocket;
|