mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
refactor: Delete a lot of unused and duplicate code in Server and WebhookServer (#5080)
* store n8n version string in a const and use that everywhere * reduce code duplication between Server and WebhookServer * unify redis checks * fix linting
This commit is contained in:
parent
b67f803cbe
commit
8b19fdd5f0
|
@ -65,6 +65,7 @@
|
||||||
"@oclif/dev-cli": "^1.22.2",
|
"@oclif/dev-cli": "^1.22.2",
|
||||||
"@types/basic-auth": "^1.1.2",
|
"@types/basic-auth": "^1.1.2",
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
|
"@types/body-parser-xml": "^2.0.2",
|
||||||
"@types/compression": "1.0.1",
|
"@types/compression": "1.0.1",
|
||||||
"@types/connect-history-api-fallback": "^1.3.1",
|
"@types/connect-history-api-fallback": "^1.3.1",
|
||||||
"@types/convict": "^4.2.1",
|
"@types/convict": "^4.2.1",
|
||||||
|
|
453
packages/cli/src/AbstractServer.ts
Normal file
453
packages/cli/src/AbstractServer.ts
Normal file
|
@ -0,0 +1,453 @@
|
||||||
|
import { readFile } from 'fs/promises';
|
||||||
|
import type { Server } from 'http';
|
||||||
|
import type { Url } from 'url';
|
||||||
|
import express from 'express';
|
||||||
|
import bodyParser from 'body-parser';
|
||||||
|
import bodyParserXml from 'body-parser-xml';
|
||||||
|
import compression from 'compression';
|
||||||
|
import parseUrl from 'parseurl';
|
||||||
|
import { getConnectionManager } from 'typeorm';
|
||||||
|
import type { RedisOptions } from 'ioredis';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ErrorReporterProxy as ErrorReporter,
|
||||||
|
LoggerProxy as Logger,
|
||||||
|
WebhookHttpMethod,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import config from '@/config';
|
||||||
|
import { N8N_VERSION, inDevelopment } from '@/constants';
|
||||||
|
import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner';
|
||||||
|
import * as Db from '@/Db';
|
||||||
|
import type { IExternalHooksClass } from '@/Interfaces';
|
||||||
|
import { ExternalHooks } from '@/ExternalHooks';
|
||||||
|
import {
|
||||||
|
send,
|
||||||
|
sendErrorResponse,
|
||||||
|
sendSuccessResponse,
|
||||||
|
ServiceUnavailableError,
|
||||||
|
} from '@/ResponseHelper';
|
||||||
|
import { corsMiddleware } from '@/middlewares/cors';
|
||||||
|
import * as TestWebhooks from '@/TestWebhooks';
|
||||||
|
import { WaitingWebhooks } from '@/WaitingWebhooks';
|
||||||
|
import { WEBHOOK_METHODS } from '@/WebhookHelpers';
|
||||||
|
|
||||||
|
const emptyBuffer = Buffer.alloc(0);
|
||||||
|
|
||||||
|
export abstract class AbstractServer {
|
||||||
|
protected app: express.Application;
|
||||||
|
|
||||||
|
protected externalHooks: IExternalHooksClass;
|
||||||
|
|
||||||
|
protected activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner;
|
||||||
|
|
||||||
|
protected protocol: string;
|
||||||
|
|
||||||
|
protected sslKey: string;
|
||||||
|
|
||||||
|
protected sslCert: string;
|
||||||
|
|
||||||
|
protected timezone: string;
|
||||||
|
|
||||||
|
protected restEndpoint: string;
|
||||||
|
|
||||||
|
protected endpointWebhook: string;
|
||||||
|
|
||||||
|
protected endpointWebhookTest: string;
|
||||||
|
|
||||||
|
protected endpointWebhookWaiting: string;
|
||||||
|
|
||||||
|
abstract configure(): Promise<void>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.app = express();
|
||||||
|
this.app.disable('x-powered-by');
|
||||||
|
|
||||||
|
this.protocol = config.getEnv('protocol');
|
||||||
|
this.sslKey = config.getEnv('ssl_key');
|
||||||
|
this.sslCert = config.getEnv('ssl_cert');
|
||||||
|
|
||||||
|
this.timezone = config.getEnv('generic.timezone');
|
||||||
|
|
||||||
|
this.restEndpoint = config.getEnv('endpoints.rest');
|
||||||
|
this.endpointWebhook = config.getEnv('endpoints.webhook');
|
||||||
|
this.endpointWebhookTest = config.getEnv('endpoints.webhookTest');
|
||||||
|
this.endpointWebhookWaiting = config.getEnv('endpoints.webhookWaiting');
|
||||||
|
|
||||||
|
this.externalHooks = ExternalHooks();
|
||||||
|
this.activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setupCommonMiddlewares() {
|
||||||
|
const { app } = this;
|
||||||
|
|
||||||
|
// Augment errors sent to Sentry
|
||||||
|
const {
|
||||||
|
Handlers: { requestHandler, errorHandler },
|
||||||
|
} = await import('@sentry/node');
|
||||||
|
app.use(requestHandler());
|
||||||
|
app.use(errorHandler());
|
||||||
|
|
||||||
|
// Compress the response data
|
||||||
|
app.use(compression());
|
||||||
|
|
||||||
|
// Make sure that each request has the "parsedUrl" parameter
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
req.parsedUrl = parseUrl(req)!;
|
||||||
|
req.rawBody = emptyBuffer;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
const payloadSizeMax = config.getEnv('endpoints.payloadSizeMax');
|
||||||
|
|
||||||
|
// Support application/json type post data
|
||||||
|
app.use(
|
||||||
|
bodyParser.json({
|
||||||
|
limit: `${payloadSizeMax}mb`,
|
||||||
|
verify: (req, res, buf) => {
|
||||||
|
req.rawBody = buf;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Support application/xml type post data
|
||||||
|
bodyParserXml(bodyParser);
|
||||||
|
app.use(
|
||||||
|
bodyParser.xml({
|
||||||
|
limit: `${payloadSizeMax}mb`,
|
||||||
|
xmlParseOptions: {
|
||||||
|
normalize: true, // Trim whitespace inside text nodes
|
||||||
|
normalizeTags: true, // Transform tags to lowercase
|
||||||
|
explicitArray: false, // Only put properties in array if length > 1
|
||||||
|
},
|
||||||
|
verify: (req, res, buf) => {
|
||||||
|
req.rawBody = buf;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
bodyParser.text({
|
||||||
|
limit: `${payloadSizeMax}mb`,
|
||||||
|
verify: (req, res, buf) => {
|
||||||
|
req.rawBody = buf;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// support application/x-www-form-urlencoded post data
|
||||||
|
app.use(
|
||||||
|
bodyParser.urlencoded({
|
||||||
|
limit: `${payloadSizeMax}mb`,
|
||||||
|
extended: false,
|
||||||
|
verify: (req, res, buf) => {
|
||||||
|
req.rawBody = buf;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupDevMiddlewares() {
|
||||||
|
this.app.use(corsMiddleware);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setupHealthCheck() {
|
||||||
|
this.app.use((req, res, next) => {
|
||||||
|
if (!Db.isInitialized) {
|
||||||
|
sendErrorResponse(res, new ServiceUnavailableError('Database is not ready!'));
|
||||||
|
} else next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Does very basic health check
|
||||||
|
this.app.get('/healthz', async (req, res) => {
|
||||||
|
Logger.debug('Health check started!');
|
||||||
|
|
||||||
|
const connection = getConnectionManager().get();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!connection.isConnected) {
|
||||||
|
// Connection is not active
|
||||||
|
throw new ServiceUnavailableError('No active database connection!');
|
||||||
|
}
|
||||||
|
// DB ping
|
||||||
|
await connection.query('SELECT 1');
|
||||||
|
} catch (error) {
|
||||||
|
ErrorReporter.error(error);
|
||||||
|
Logger.error('No Database connection!');
|
||||||
|
return sendErrorResponse(res, new ServiceUnavailableError('No Database connection!'));
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.debug('Health check completed successfully!');
|
||||||
|
sendSuccessResponse(res, { status: 'ok' }, true, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (config.getEnv('executions.mode') === 'queue') {
|
||||||
|
await this.setupRedisChecks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This connection is going to be our heartbeat
|
||||||
|
// IORedis automatically pings redis and tries to reconnect
|
||||||
|
// We will be using a retryStrategy to control how and when to exit.
|
||||||
|
private async setupRedisChecks() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
const { default: Redis } = await import('ioredis');
|
||||||
|
|
||||||
|
let lastTimer = 0;
|
||||||
|
let cumulativeTimeout = 0;
|
||||||
|
const { host, port, username, password, db }: RedisOptions = config.getEnv('queue.bull.redis');
|
||||||
|
const redisConnectionTimeoutLimit = config.getEnv('queue.bull.redis.timeoutThreshold');
|
||||||
|
|
||||||
|
const redis = new Redis({
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
db,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
retryStrategy: (): number | null => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastTimer > 30000) {
|
||||||
|
// Means we had no timeout at all or last timeout was temporary and we recovered
|
||||||
|
lastTimer = now;
|
||||||
|
cumulativeTimeout = 0;
|
||||||
|
} else {
|
||||||
|
cumulativeTimeout += now - lastTimer;
|
||||||
|
lastTimer = now;
|
||||||
|
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
|
||||||
|
Logger.error(
|
||||||
|
`Unable to connect to Redis after ${redisConnectionTimeoutLimit}. Exiting process.`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 500;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
redis.on('close', () => {
|
||||||
|
Logger.warn('Redis unavailable - trying to reconnect...');
|
||||||
|
});
|
||||||
|
|
||||||
|
redis.on('error', (error) => {
|
||||||
|
if (!String(error).includes('ECONNREFUSED')) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
Logger.warn('Error with Redis: ', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Regular Webhooks
|
||||||
|
// ----------------------------------------
|
||||||
|
protected setupWebhookEndpoint() {
|
||||||
|
const endpoint = this.endpointWebhook;
|
||||||
|
const activeWorkflowRunner = this.activeWorkflowRunner;
|
||||||
|
|
||||||
|
// Register all webhook requests
|
||||||
|
this.app.all(`/${endpoint}/*`, async (req, res) => {
|
||||||
|
// Cut away the "/webhook/" to get the registered part of the url
|
||||||
|
const requestUrl = req.parsedUrl.pathname!.slice(endpoint.length + 2);
|
||||||
|
|
||||||
|
const method = req.method.toUpperCase() as WebhookHttpMethod;
|
||||||
|
if (method === 'OPTIONS') {
|
||||||
|
let allowedMethods: string[];
|
||||||
|
try {
|
||||||
|
allowedMethods = await activeWorkflowRunner.getWebhookMethods(requestUrl);
|
||||||
|
allowedMethods.push('OPTIONS');
|
||||||
|
|
||||||
|
// Add custom "Allow" header to satisfy OPTIONS response.
|
||||||
|
res.append('Allow', allowedMethods);
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
sendErrorResponse(res, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.header('Access-Control-Allow-Origin', '*');
|
||||||
|
|
||||||
|
sendSuccessResponse(res, {}, true, 204);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!WEBHOOK_METHODS.includes(method)) {
|
||||||
|
sendErrorResponse(res, new Error(`The method ${method} is not supported.`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await activeWorkflowRunner.executeWebhook(method, requestUrl, req, res);
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
sendErrorResponse(res, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.noWebhookResponse === true) {
|
||||||
|
// Nothing else to do as the response got already sent
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendSuccessResponse(res, response.data, true, response.responseCode, response.headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Waiting Webhooks
|
||||||
|
// ----------------------------------------
|
||||||
|
protected setupWaitingWebhookEndpoint() {
|
||||||
|
const endpoint = this.endpointWebhookWaiting;
|
||||||
|
const waitingWebhooks = new WaitingWebhooks();
|
||||||
|
|
||||||
|
// Register all webhook-waiting requests
|
||||||
|
this.app.all(`/${endpoint}/*`, async (req, res) => {
|
||||||
|
// Cut away the "/webhook-waiting/" to get the registered part of the url
|
||||||
|
const requestUrl = req.parsedUrl.pathname!.slice(endpoint.length + 2);
|
||||||
|
|
||||||
|
const method = req.method.toUpperCase() as WebhookHttpMethod;
|
||||||
|
|
||||||
|
if (!WEBHOOK_METHODS.includes(method)) {
|
||||||
|
sendErrorResponse(res, new Error(`The method ${method} is not supported.`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await waitingWebhooks.executeWebhook(method, requestUrl, req, res);
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
sendErrorResponse(res, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.noWebhookResponse === true) {
|
||||||
|
// Nothing else to do as the response got already sent
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendSuccessResponse(res, response.data, true, response.responseCode, response.headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Testing Webhooks
|
||||||
|
// ----------------------------------------
|
||||||
|
protected setupTestWebhookEndpoint() {
|
||||||
|
const endpoint = this.endpointWebhookTest;
|
||||||
|
const testWebhooks = TestWebhooks.getInstance();
|
||||||
|
|
||||||
|
// Register all test webhook requests (for testing via the UI)
|
||||||
|
this.app.all(`/${endpoint}/*`, async (req, res) => {
|
||||||
|
// Cut away the "/webhook-test/" to get the registered part of the url
|
||||||
|
const requestUrl = req.parsedUrl.pathname!.slice(endpoint.length + 2);
|
||||||
|
|
||||||
|
const method = req.method.toUpperCase() as WebhookHttpMethod;
|
||||||
|
|
||||||
|
if (method === 'OPTIONS') {
|
||||||
|
let allowedMethods: string[];
|
||||||
|
try {
|
||||||
|
allowedMethods = await testWebhooks.getWebhookMethods(requestUrl);
|
||||||
|
allowedMethods.push('OPTIONS');
|
||||||
|
|
||||||
|
// Add custom "Allow" header to satisfy OPTIONS response.
|
||||||
|
res.append('Allow', allowedMethods);
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
sendErrorResponse(res, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.header('Access-Control-Allow-Origin', '*');
|
||||||
|
|
||||||
|
sendSuccessResponse(res, {}, true, 204);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!WEBHOOK_METHODS.includes(method)) {
|
||||||
|
sendErrorResponse(res, new Error(`The method ${method} is not supported.`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await testWebhooks.callTestWebhook(method, requestUrl, req, res);
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
sendErrorResponse(res, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.noWebhookResponse === true) {
|
||||||
|
// Nothing else to do as the response got already sent
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendSuccessResponse(res, response.data, true, response.responseCode, response.headers);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Removes a test webhook
|
||||||
|
// TODO UM: check if this needs validation with user management.
|
||||||
|
this.app.delete(
|
||||||
|
`/${this.restEndpoint}/test-webhook/:id`,
|
||||||
|
send(async (req) => testWebhooks.cancelTestWebhook(req.params.id)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<void> {
|
||||||
|
const { app, externalHooks, protocol, sslKey, sslCert } = this;
|
||||||
|
|
||||||
|
let server: Server;
|
||||||
|
if (protocol === 'https' && sslKey && sslCert) {
|
||||||
|
const https = await import('https');
|
||||||
|
server = https.createServer(
|
||||||
|
{
|
||||||
|
key: await readFile(this.sslKey, 'utf8'),
|
||||||
|
cert: await readFile(this.sslCert, 'utf8'),
|
||||||
|
},
|
||||||
|
app,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const http = await import('http');
|
||||||
|
server = http.createServer(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PORT = config.getEnv('port');
|
||||||
|
const ADDRESS = config.getEnv('listen_address');
|
||||||
|
|
||||||
|
server.on('error', (error: Error & { code: string }) => {
|
||||||
|
if (error.code === 'EADDRINUSE') {
|
||||||
|
console.log(
|
||||||
|
`n8n's port ${PORT} is already in use. Do you have another instance of n8n running already?`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => server.listen(PORT, ADDRESS, () => resolve()));
|
||||||
|
|
||||||
|
await this.setupCommonMiddlewares();
|
||||||
|
if (inDevelopment) {
|
||||||
|
this.setupDevMiddlewares();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.setupHealthCheck();
|
||||||
|
|
||||||
|
await this.configure();
|
||||||
|
|
||||||
|
console.log(`n8n ready on ${ADDRESS}, port ${PORT}`);
|
||||||
|
console.log(`Version: ${N8N_VERSION}`);
|
||||||
|
|
||||||
|
const defaultLocale = config.getEnv('defaultLocale');
|
||||||
|
if (defaultLocale !== 'en') {
|
||||||
|
console.log(`Locale: ${defaultLocale}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await externalHooks.run('n8n.ready', [app, config]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'http' {
|
||||||
|
export interface IncomingMessage {
|
||||||
|
parsedUrl: Url;
|
||||||
|
}
|
||||||
|
}
|
|
@ -76,6 +76,10 @@ export class ActiveWorkflowRunner {
|
||||||
[key: string]: IQueuedWorkflowActivations;
|
[key: string]: IQueuedWorkflowActivations;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.activeWorkflows = new ActiveWorkflows();
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
async init() {
|
async init() {
|
||||||
// Get the active workflows from database
|
// Get the active workflows from database
|
||||||
|
@ -100,8 +104,6 @@ export class ActiveWorkflowRunner {
|
||||||
await Db.collections.Webhook.clear();
|
await Db.collections.Webhook.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.activeWorkflows = new ActiveWorkflows();
|
|
||||||
|
|
||||||
if (workflowsData.length !== 0) {
|
if (workflowsData.length !== 0) {
|
||||||
console.info(' ================================');
|
console.info(' ================================');
|
||||||
console.info(' Start Active Workflows:');
|
console.info(' Start Active Workflows:');
|
||||||
|
@ -147,11 +149,6 @@ export class ActiveWorkflowRunner {
|
||||||
await externalHooks.run('activeWorkflows.initialized', []);
|
await externalHooks.run('activeWorkflows.initialized', []);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
||||||
async initWebhooks() {
|
|
||||||
this.activeWorkflows = new ActiveWorkflows();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes all the currently active workflows
|
* Removes all the currently active workflows
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import type { Application } from 'express';
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { ErrorReporterProxy } from 'n8n-workflow';
|
import { ErrorReporterProxy } from 'n8n-workflow';
|
||||||
|
|
||||||
|
@ -44,11 +43,3 @@ export const initErrorHandling = async () => {
|
||||||
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setupErrorMiddleware = async (app: Application) => {
|
|
||||||
const {
|
|
||||||
Handlers: { requestHandler, errorHandler },
|
|
||||||
} = await import('@sentry/node');
|
|
||||||
app.use(requestHandler());
|
|
||||||
app.use(errorHandler());
|
|
||||||
};
|
|
||||||
|
|
|
@ -5,28 +5,19 @@
|
||||||
/* eslint-disable no-underscore-dangle */
|
/* eslint-disable no-underscore-dangle */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { join as pathJoin } from 'path';
|
|
||||||
import { readFile as fsReadFile } from 'fs/promises';
|
import { readFile as fsReadFile } from 'fs/promises';
|
||||||
import type { n8n } from 'n8n-core';
|
|
||||||
import {
|
import {
|
||||||
ExecutionError,
|
ExecutionError,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
INode,
|
INode,
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
jsonParse,
|
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { validate } from 'class-validator';
|
import { validate } from 'class-validator';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import {
|
import { ICredentialsDb, IExecutionDb, IExecutionFlattedDb, IWorkflowDb } from '@/Interfaces';
|
||||||
ICredentialsDb,
|
|
||||||
IExecutionDb,
|
|
||||||
IExecutionFlattedDb,
|
|
||||||
IPackageVersions,
|
|
||||||
IWorkflowDb,
|
|
||||||
} from '@/Interfaces';
|
|
||||||
import * as ResponseHelper from '@/ResponseHelper';
|
import * as ResponseHelper from '@/ResponseHelper';
|
||||||
// eslint-disable-next-line import/order
|
// eslint-disable-next-line import/order
|
||||||
import { Like } from 'typeorm';
|
import { Like } from 'typeorm';
|
||||||
|
@ -34,9 +25,6 @@ import { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||||
import { CredentialsEntity } from '@db/entities/CredentialsEntity';
|
import { CredentialsEntity } from '@db/entities/CredentialsEntity';
|
||||||
import { TagEntity } from '@db/entities/TagEntity';
|
import { TagEntity } from '@db/entities/TagEntity';
|
||||||
import { User } from '@db/entities/User';
|
import { User } from '@db/entities/User';
|
||||||
import { CLI_DIR } from '@/constants';
|
|
||||||
|
|
||||||
let versionCache: IPackageVersions | undefined;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the base URL n8n is reachable from
|
* Returns the base URL n8n is reachable from
|
||||||
|
@ -62,27 +50,8 @@ export function getSessionId(req: express.Request): string | undefined {
|
||||||
return req.headers.sessionid as string | undefined;
|
return req.headers.sessionid as string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns information which version of the packages are installed
|
|
||||||
*/
|
|
||||||
export async function getVersions(): Promise<IPackageVersions> {
|
|
||||||
if (versionCache !== undefined) {
|
|
||||||
return versionCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
const packageFile = await fsReadFile(pathJoin(CLI_DIR, 'package.json'), 'utf8');
|
|
||||||
const packageData = jsonParse<n8n.PackageJson>(packageFile);
|
|
||||||
|
|
||||||
versionCache = {
|
|
||||||
cli: packageData.version,
|
|
||||||
};
|
|
||||||
|
|
||||||
return versionCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts configuration schema for key
|
* Extracts configuration schema for key
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
function extractSchemaForKey(configKey: string, configSchema: IDataObject): IDataObject {
|
function extractSchemaForKey(configKey: string, configSchema: IDataObject): IDataObject {
|
||||||
const configKeyParts = configKey.split('.');
|
const configKeyParts = configKey.split('.');
|
||||||
|
|
|
@ -27,9 +27,7 @@ import PCancelable from 'p-cancelable';
|
||||||
import type { FindOperator, Repository } from 'typeorm';
|
import type { FindOperator, Repository } from 'typeorm';
|
||||||
|
|
||||||
import type { ChildProcess } from 'child_process';
|
import type { ChildProcess } from 'child_process';
|
||||||
import { Url } from 'url';
|
|
||||||
|
|
||||||
import type { Request } from 'express';
|
|
||||||
import type { InstalledNodes } from '@db/entities/InstalledNodes';
|
import type { InstalledNodes } from '@db/entities/InstalledNodes';
|
||||||
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||||
import type { Role } from '@db/entities/Role';
|
import type { Role } from '@db/entities/Role';
|
||||||
|
@ -57,10 +55,6 @@ export interface IQueuedWorkflowActivations {
|
||||||
workflowData: IWorkflowDb;
|
workflowData: IWorkflowDb;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICustomRequest extends Request {
|
|
||||||
parsedUrl: Url | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICredentialsTypeData {
|
export interface ICredentialsTypeData {
|
||||||
[key: string]: CredentialLoadingDetails;
|
[key: string]: CredentialLoadingDetails;
|
||||||
}
|
}
|
||||||
|
@ -498,8 +492,8 @@ export interface IVersionNotificationSettings {
|
||||||
export interface IN8nUISettings {
|
export interface IN8nUISettings {
|
||||||
endpointWebhook: string;
|
endpointWebhook: string;
|
||||||
endpointWebhookTest: string;
|
endpointWebhookTest: string;
|
||||||
saveDataErrorExecution: string;
|
saveDataErrorExecution: 'all' | 'none';
|
||||||
saveDataSuccessExecution: string;
|
saveDataSuccessExecution: 'all' | 'none';
|
||||||
saveManualExecutions: boolean;
|
saveManualExecutions: boolean;
|
||||||
executionTimeout: number;
|
executionTimeout: number;
|
||||||
maxExecutionTimeout: number;
|
maxExecutionTimeout: number;
|
||||||
|
|
|
@ -23,7 +23,8 @@ import {
|
||||||
import { Telemetry } from '@/telemetry';
|
import { Telemetry } from '@/telemetry';
|
||||||
import { RoleService } from './role/role.service';
|
import { RoleService } from './role/role.service';
|
||||||
import { eventBus } from './eventbus';
|
import { eventBus } from './eventbus';
|
||||||
import { User } from './databases/entities/User';
|
import type { User } from '@db/entities/User';
|
||||||
|
import { N8N_VERSION } from '@/constants';
|
||||||
|
|
||||||
function userToPayload(user: User): {
|
function userToPayload(user: User): {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
@ -42,19 +43,11 @@ function userToPayload(user: User): {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InternalHooksClass implements IInternalHooksClass {
|
export class InternalHooksClass implements IInternalHooksClass {
|
||||||
private versionCli: string;
|
|
||||||
|
|
||||||
private nodeTypes: INodeTypes;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private telemetry: Telemetry,
|
private telemetry: Telemetry,
|
||||||
private instanceId: string,
|
private instanceId: string,
|
||||||
versionCli: string,
|
private nodeTypes: INodeTypes,
|
||||||
nodeTypes: INodeTypes,
|
) {}
|
||||||
) {
|
|
||||||
this.versionCli = versionCli;
|
|
||||||
this.nodeTypes = nodeTypes;
|
|
||||||
}
|
|
||||||
|
|
||||||
async onServerStarted(
|
async onServerStarted(
|
||||||
diagnosticInfo: IDiagnosticInfo,
|
diagnosticInfo: IDiagnosticInfo,
|
||||||
|
@ -174,7 +167,7 @@ export class InternalHooksClass implements IInternalHooksClass {
|
||||||
node_graph_string: JSON.stringify(nodeGraph),
|
node_graph_string: JSON.stringify(nodeGraph),
|
||||||
notes_count_overlapping: overlappingCount,
|
notes_count_overlapping: overlappingCount,
|
||||||
notes_count_non_overlapping: notesCount - overlappingCount,
|
notes_count_non_overlapping: notesCount - overlappingCount,
|
||||||
version_cli: this.versionCli,
|
version_cli: N8N_VERSION,
|
||||||
num_tags: workflow.tags?.length ?? 0,
|
num_tags: workflow.tags?.length ?? 0,
|
||||||
public_api: publicApi,
|
public_api: publicApi,
|
||||||
sharing_role: userRole,
|
sharing_role: userRole,
|
||||||
|
@ -249,7 +242,7 @@ export class InternalHooksClass implements IInternalHooksClass {
|
||||||
const properties: IExecutionTrackProperties = {
|
const properties: IExecutionTrackProperties = {
|
||||||
workflow_id: workflow.id,
|
workflow_id: workflow.id,
|
||||||
is_manual: false,
|
is_manual: false,
|
||||||
version_cli: this.versionCli,
|
version_cli: N8N_VERSION,
|
||||||
success: false,
|
success: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,20 +13,11 @@ export class InternalHooksManager {
|
||||||
throw new Error('InternalHooks not initialized');
|
throw new Error('InternalHooks not initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
static async init(
|
static async init(instanceId: string, nodeTypes: INodeTypes): Promise<InternalHooksClass> {
|
||||||
instanceId: string,
|
|
||||||
versionCli: string,
|
|
||||||
nodeTypes: INodeTypes,
|
|
||||||
): Promise<InternalHooksClass> {
|
|
||||||
if (!this.internalHooksInstance) {
|
if (!this.internalHooksInstance) {
|
||||||
const telemetry = new Telemetry(instanceId, versionCli);
|
const telemetry = new Telemetry(instanceId);
|
||||||
await telemetry.init();
|
await telemetry.init();
|
||||||
this.internalHooksInstance = new InternalHooksClass(
|
this.internalHooksInstance = new InternalHooksClass(telemetry, instanceId, nodeTypes);
|
||||||
telemetry,
|
|
||||||
instanceId,
|
|
||||||
versionCli,
|
|
||||||
nodeTypes,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.internalHooksInstance;
|
return this.internalHooksInstance;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { ILogger } from 'n8n-workflow';
|
||||||
import { getLogger } from './Logger';
|
import { getLogger } from './Logger';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import { LICENSE_FEATURES, SETTINGS_LICENSE_CERT_KEY } from './constants';
|
import { LICENSE_FEATURES, N8N_VERSION, SETTINGS_LICENSE_CERT_KEY } from './constants';
|
||||||
|
|
||||||
async function loadCertStr(): Promise<TLicenseContainerStr> {
|
async function loadCertStr(): Promise<TLicenseContainerStr> {
|
||||||
const databaseSettings = await Db.collections.Settings.findOne({
|
const databaseSettings = await Db.collections.Settings.findOne({
|
||||||
|
@ -35,7 +35,7 @@ export class License {
|
||||||
this.logger = getLogger();
|
this.logger = getLogger();
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(instanceId: string, version: string) {
|
async init(instanceId: string) {
|
||||||
if (this.manager) {
|
if (this.manager) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ export class License {
|
||||||
this.manager = new LicenseManager({
|
this.manager = new LicenseManager({
|
||||||
server,
|
server,
|
||||||
tenantId: config.getEnv('license.tenantId'),
|
tenantId: config.getEnv('license.tenantId'),
|
||||||
productIdentifier: `n8n-${version}`,
|
productIdentifier: `n8n-${N8N_VERSION}`,
|
||||||
autoRenewEnabled,
|
autoRenewEnabled,
|
||||||
autoRenewOffset,
|
autoRenewOffset,
|
||||||
logger: this.logger,
|
logger: this.logger,
|
||||||
|
|
|
@ -28,7 +28,6 @@
|
||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
|
|
||||||
import { exec as callbackExec } from 'child_process';
|
import { exec as callbackExec } from 'child_process';
|
||||||
import { readFileSync } from 'fs';
|
|
||||||
import { access as fsAccess } from 'fs/promises';
|
import { access as fsAccess } from 'fs/promises';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { join as pathJoin, resolve as pathResolve } from 'path';
|
import { join as pathJoin, resolve as pathResolve } from 'path';
|
||||||
|
@ -36,7 +35,7 @@ import { createHmac } from 'crypto';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { FindManyOptions, getConnectionManager, In } from 'typeorm';
|
import { FindManyOptions, In } from 'typeorm';
|
||||||
import axios, { AxiosRequestConfig } from 'axios';
|
import axios, { AxiosRequestConfig } from 'axios';
|
||||||
import clientOAuth1, { RequestOptions } from 'oauth-1.0a';
|
import clientOAuth1, { RequestOptions } from 'oauth-1.0a';
|
||||||
// IMPORTANT! Do not switch to anther bcrypt library unless really necessary and
|
// IMPORTANT! Do not switch to anther bcrypt library unless really necessary and
|
||||||
|
@ -61,9 +60,7 @@ import {
|
||||||
ITelemetrySettings,
|
ITelemetrySettings,
|
||||||
LoggerProxy,
|
LoggerProxy,
|
||||||
jsonParse,
|
jsonParse,
|
||||||
WebhookHttpMethod,
|
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
ErrorReporterProxy as ErrorReporter,
|
|
||||||
INodeTypes,
|
INodeTypes,
|
||||||
ICredentialTypes,
|
ICredentialTypes,
|
||||||
INode,
|
INode,
|
||||||
|
@ -72,21 +69,17 @@ import {
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import basicAuth from 'basic-auth';
|
import basicAuth from 'basic-auth';
|
||||||
import compression from 'compression';
|
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import jwks from 'jwks-rsa';
|
import jwks from 'jwks-rsa';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import timezones from 'google-timezones-json';
|
import timezones from 'google-timezones-json';
|
||||||
import parseUrl from 'parseurl';
|
|
||||||
import promClient, { Registry } from 'prom-client';
|
import promClient, { Registry } from 'prom-client';
|
||||||
import history from 'connect-history-api-fallback';
|
import history from 'connect-history-api-fallback';
|
||||||
import bodyParser from 'body-parser';
|
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import * as Queue from '@/Queue';
|
import * as Queue from '@/Queue';
|
||||||
import { InternalHooksManager } from '@/InternalHooksManager';
|
import { InternalHooksManager } from '@/InternalHooksManager';
|
||||||
import { getCredentialTranslationPath } from '@/TranslationHelpers';
|
import { getCredentialTranslationPath } from '@/TranslationHelpers';
|
||||||
import { WEBHOOK_METHODS } from '@/WebhookHelpers';
|
|
||||||
import { getSharedWorkflowIds } from '@/WorkflowHelpers';
|
import { getSharedWorkflowIds } from '@/WorkflowHelpers';
|
||||||
|
|
||||||
import { nodesController } from '@/api/nodes.api';
|
import { nodesController } from '@/api/nodes.api';
|
||||||
|
@ -95,6 +88,7 @@ import {
|
||||||
AUTH_COOKIE_NAME,
|
AUTH_COOKIE_NAME,
|
||||||
EDITOR_UI_DIST_DIR,
|
EDITOR_UI_DIST_DIR,
|
||||||
GENERATED_STATIC_DIR,
|
GENERATED_STATIC_DIR,
|
||||||
|
N8N_VERSION,
|
||||||
NODES_BASE_DIR,
|
NODES_BASE_DIR,
|
||||||
RESPONSE_ERROR_MESSAGES,
|
RESPONSE_ERROR_MESSAGES,
|
||||||
TEMPLATES_DIR,
|
TEMPLATES_DIR,
|
||||||
|
@ -129,17 +123,13 @@ import {
|
||||||
DatabaseType,
|
DatabaseType,
|
||||||
ICredentialsDb,
|
ICredentialsDb,
|
||||||
ICredentialsOverwrite,
|
ICredentialsOverwrite,
|
||||||
ICustomRequest,
|
|
||||||
IDiagnosticInfo,
|
IDiagnosticInfo,
|
||||||
IExecutionFlattedDb,
|
IExecutionFlattedDb,
|
||||||
IExecutionsStopData,
|
IExecutionsStopData,
|
||||||
IExecutionsSummary,
|
IExecutionsSummary,
|
||||||
IExternalHooksClass,
|
|
||||||
IN8nUISettings,
|
IN8nUISettings,
|
||||||
IPackageVersions,
|
|
||||||
} from '@/Interfaces';
|
} from '@/Interfaces';
|
||||||
import * as ActiveExecutions from '@/ActiveExecutions';
|
import * as ActiveExecutions from '@/ActiveExecutions';
|
||||||
import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner';
|
|
||||||
import {
|
import {
|
||||||
CredentialsHelper,
|
CredentialsHelper,
|
||||||
getCredentialForUser,
|
getCredentialForUser,
|
||||||
|
@ -147,119 +137,42 @@ import {
|
||||||
} from '@/CredentialsHelper';
|
} from '@/CredentialsHelper';
|
||||||
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
||||||
import { CredentialTypes } from '@/CredentialTypes';
|
import { CredentialTypes } from '@/CredentialTypes';
|
||||||
import { ExternalHooks } from '@/ExternalHooks';
|
|
||||||
import * as GenericHelpers from '@/GenericHelpers';
|
import * as GenericHelpers from '@/GenericHelpers';
|
||||||
import { NodeTypes } from '@/NodeTypes';
|
import { NodeTypes } from '@/NodeTypes';
|
||||||
import * as Push from '@/Push';
|
import * as Push from '@/Push';
|
||||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
import * as ResponseHelper from '@/ResponseHelper';
|
import * as ResponseHelper from '@/ResponseHelper';
|
||||||
import * as TestWebhooks from '@/TestWebhooks';
|
|
||||||
import { WaitTracker, WaitTrackerClass } from '@/WaitTracker';
|
import { WaitTracker, WaitTrackerClass } from '@/WaitTracker';
|
||||||
import * as WebhookHelpers from '@/WebhookHelpers';
|
import * as WebhookHelpers from '@/WebhookHelpers';
|
||||||
import * as WebhookServer from '@/WebhookServer';
|
|
||||||
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
|
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
|
||||||
import { toHttpNodeParameters } from '@/CurlConverterHelper';
|
import { toHttpNodeParameters } from '@/CurlConverterHelper';
|
||||||
import { setupErrorMiddleware } from '@/ErrorReporting';
|
|
||||||
import { eventBus } from '@/eventbus';
|
import { eventBus } from '@/eventbus';
|
||||||
import { eventBusRouter } from '@/eventbus/eventBusRoutes';
|
import { eventBusRouter } from '@/eventbus/eventBusRoutes';
|
||||||
import { isLogStreamingEnabled } from '@/eventbus/MessageEventBus/MessageEventBusHelper';
|
import { isLogStreamingEnabled } from '@/eventbus/MessageEventBus/MessageEventBusHelper';
|
||||||
import { getLicense } from '@/License';
|
import { getLicense } from '@/License';
|
||||||
import { licenseController } from '@/license/license.controller';
|
import { licenseController } from './license/license.controller';
|
||||||
import { corsMiddleware } from '@/middlewares/cors';
|
import { corsMiddleware } from './middlewares/cors';
|
||||||
|
import { AbstractServer } from './AbstractServer';
|
||||||
require('body-parser-xml')(bodyParser);
|
|
||||||
|
|
||||||
const exec = promisify(callbackExec);
|
const exec = promisify(callbackExec);
|
||||||
|
|
||||||
const externalHooks: IExternalHooksClass = ExternalHooks();
|
class Server extends AbstractServer {
|
||||||
|
|
||||||
class App {
|
|
||||||
app: express.Application;
|
|
||||||
|
|
||||||
activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner;
|
|
||||||
|
|
||||||
testWebhooks: TestWebhooks.TestWebhooks;
|
|
||||||
|
|
||||||
endpointWebhook: string;
|
|
||||||
|
|
||||||
endpointWebhookWaiting: string;
|
|
||||||
|
|
||||||
endpointWebhookTest: string;
|
|
||||||
|
|
||||||
endpointPresetCredentials: string;
|
endpointPresetCredentials: string;
|
||||||
|
|
||||||
externalHooks: IExternalHooksClass;
|
|
||||||
|
|
||||||
waitTracker: WaitTrackerClass;
|
waitTracker: WaitTrackerClass;
|
||||||
|
|
||||||
defaultWorkflowName: string;
|
|
||||||
|
|
||||||
defaultCredentialsName: string;
|
|
||||||
|
|
||||||
saveDataErrorExecution: 'all' | 'none';
|
|
||||||
|
|
||||||
saveDataSuccessExecution: 'all' | 'none';
|
|
||||||
|
|
||||||
saveManualExecutions: boolean;
|
|
||||||
|
|
||||||
executionTimeout: number;
|
|
||||||
|
|
||||||
maxExecutionTimeout: number;
|
|
||||||
|
|
||||||
timezone: string;
|
|
||||||
|
|
||||||
activeExecutionsInstance: ActiveExecutions.ActiveExecutions;
|
activeExecutionsInstance: ActiveExecutions.ActiveExecutions;
|
||||||
|
|
||||||
push: Push.Push;
|
|
||||||
|
|
||||||
versions: IPackageVersions | undefined;
|
|
||||||
|
|
||||||
restEndpoint: string;
|
|
||||||
|
|
||||||
publicApiEndpoint: string;
|
|
||||||
|
|
||||||
frontendSettings: IN8nUISettings;
|
frontendSettings: IN8nUISettings;
|
||||||
|
|
||||||
protocol: string;
|
|
||||||
|
|
||||||
sslKey: string;
|
|
||||||
|
|
||||||
sslCert: string;
|
|
||||||
|
|
||||||
payloadSizeMax: number;
|
|
||||||
|
|
||||||
presetCredentialsLoaded: boolean;
|
presetCredentialsLoaded: boolean;
|
||||||
|
|
||||||
webhookMethods: WebhookHttpMethod[];
|
|
||||||
|
|
||||||
nodeTypes: INodeTypes;
|
nodeTypes: INodeTypes;
|
||||||
|
|
||||||
credentialTypes: ICredentialTypes;
|
credentialTypes: ICredentialTypes;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.app = express();
|
super();
|
||||||
this.app.disable('x-powered-by');
|
|
||||||
|
|
||||||
this.endpointWebhook = config.getEnv('endpoints.webhook');
|
|
||||||
this.endpointWebhookWaiting = config.getEnv('endpoints.webhookWaiting');
|
|
||||||
this.endpointWebhookTest = config.getEnv('endpoints.webhookTest');
|
|
||||||
|
|
||||||
this.defaultWorkflowName = config.getEnv('workflows.defaultName');
|
|
||||||
this.defaultCredentialsName = config.getEnv('credentials.defaultName');
|
|
||||||
|
|
||||||
this.saveDataErrorExecution = config.get('executions.saveDataOnError');
|
|
||||||
this.saveDataSuccessExecution = config.get('executions.saveDataOnSuccess');
|
|
||||||
this.saveManualExecutions = config.get('executions.saveDataManualExecutions');
|
|
||||||
this.executionTimeout = config.get('executions.timeout');
|
|
||||||
this.maxExecutionTimeout = config.get('executions.maxTimeout');
|
|
||||||
this.payloadSizeMax = config.get('endpoints.payloadSizeMax');
|
|
||||||
this.timezone = config.get('generic.timezone');
|
|
||||||
this.restEndpoint = config.get('endpoints.rest');
|
|
||||||
this.publicApiEndpoint = config.get('publicApi.path');
|
|
||||||
|
|
||||||
this.activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
|
|
||||||
this.testWebhooks = TestWebhooks.getInstance();
|
|
||||||
this.push = Push.getInstance();
|
|
||||||
|
|
||||||
this.nodeTypes = NodeTypes();
|
this.nodeTypes = NodeTypes();
|
||||||
this.credentialTypes = CredentialTypes();
|
this.credentialTypes = CredentialTypes();
|
||||||
|
@ -267,17 +180,9 @@ class App {
|
||||||
this.activeExecutionsInstance = ActiveExecutions.getInstance();
|
this.activeExecutionsInstance = ActiveExecutions.getInstance();
|
||||||
this.waitTracker = WaitTracker();
|
this.waitTracker = WaitTracker();
|
||||||
|
|
||||||
this.protocol = config.getEnv('protocol');
|
|
||||||
this.sslKey = config.getEnv('ssl_key');
|
|
||||||
this.sslCert = config.getEnv('ssl_cert');
|
|
||||||
|
|
||||||
this.externalHooks = externalHooks;
|
|
||||||
|
|
||||||
this.presetCredentialsLoaded = false;
|
this.presetCredentialsLoaded = false;
|
||||||
this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
|
this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
|
||||||
|
|
||||||
void setupErrorMiddleware(this.app);
|
|
||||||
|
|
||||||
if (process.env.E2E_TESTS === 'true') {
|
if (process.env.E2E_TESTS === 'true') {
|
||||||
this.app.use('/e2e', require('./api/e2e.api').e2eController);
|
this.app.use('/e2e', require('./api/e2e.api').e2eController);
|
||||||
}
|
}
|
||||||
|
@ -305,11 +210,11 @@ class App {
|
||||||
this.frontendSettings = {
|
this.frontendSettings = {
|
||||||
endpointWebhook: this.endpointWebhook,
|
endpointWebhook: this.endpointWebhook,
|
||||||
endpointWebhookTest: this.endpointWebhookTest,
|
endpointWebhookTest: this.endpointWebhookTest,
|
||||||
saveDataErrorExecution: this.saveDataErrorExecution,
|
saveDataErrorExecution: config.getEnv('executions.saveDataOnError'),
|
||||||
saveDataSuccessExecution: this.saveDataSuccessExecution,
|
saveDataSuccessExecution: config.getEnv('executions.saveDataOnSuccess'),
|
||||||
saveManualExecutions: this.saveManualExecutions,
|
saveManualExecutions: config.getEnv('executions.saveDataManualExecutions'),
|
||||||
executionTimeout: this.executionTimeout,
|
executionTimeout: config.getEnv('executions.timeout'),
|
||||||
maxExecutionTimeout: this.maxExecutionTimeout,
|
maxExecutionTimeout: config.getEnv('executions.maxTimeout'),
|
||||||
workflowCallerPolicyDefaultOption: config.getEnv('workflows.callerPolicyDefaultOption'),
|
workflowCallerPolicyDefaultOption: config.getEnv('workflows.callerPolicyDefaultOption'),
|
||||||
timezone: this.timezone,
|
timezone: this.timezone,
|
||||||
urlBaseWebhook,
|
urlBaseWebhook,
|
||||||
|
@ -374,14 +279,6 @@ class App {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current epoch time
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
getCurrentDate(): Date {
|
|
||||||
return new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current settings for the frontend
|
* Returns the current settings for the frontend
|
||||||
*/
|
*/
|
||||||
|
@ -410,7 +307,7 @@ class App {
|
||||||
|
|
||||||
async initLicense(): Promise<void> {
|
async initLicense(): Promise<void> {
|
||||||
const license = getLicense();
|
const license = getLicense();
|
||||||
await license.init(this.frontendSettings.instanceId, this.frontendSettings.versionCli);
|
await license.init(this.frontendSettings.instanceId);
|
||||||
|
|
||||||
const activationKey = config.getEnv('license.activationKey');
|
const activationKey = config.getEnv('license.activationKey');
|
||||||
if (activationKey) {
|
if (activationKey) {
|
||||||
|
@ -422,7 +319,7 @@ class App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async config(): Promise<void> {
|
async configure(): Promise<void> {
|
||||||
const enableMetrics = config.getEnv('endpoints.metrics.enable');
|
const enableMetrics = config.getEnv('endpoints.metrics.enable');
|
||||||
let register: Registry;
|
let register: Registry;
|
||||||
|
|
||||||
|
@ -437,8 +334,7 @@ class App {
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false);
|
.catch(() => false);
|
||||||
|
|
||||||
this.versions = await GenericHelpers.getVersions();
|
this.frontendSettings.versionCli = N8N_VERSION;
|
||||||
this.frontendSettings.versionCli = this.versions.cli;
|
|
||||||
|
|
||||||
this.frontendSettings.instanceId = await UserSettings.getInstanceId();
|
this.frontendSettings.instanceId = await UserSettings.getInstanceId();
|
||||||
|
|
||||||
|
@ -446,6 +342,7 @@ class App {
|
||||||
|
|
||||||
await this.initLicense();
|
await this.initLicense();
|
||||||
|
|
||||||
|
const publicApiEndpoint = config.getEnv('publicApi.path');
|
||||||
const excludeEndpoints = config.getEnv('security.excludeEndpoints');
|
const excludeEndpoints = config.getEnv('security.excludeEndpoints');
|
||||||
|
|
||||||
const ignoredEndpoints = [
|
const ignoredEndpoints = [
|
||||||
|
@ -458,7 +355,7 @@ class App {
|
||||||
this.endpointWebhook,
|
this.endpointWebhook,
|
||||||
this.endpointWebhookTest,
|
this.endpointWebhookTest,
|
||||||
this.endpointPresetCredentials,
|
this.endpointPresetCredentials,
|
||||||
config.getEnv('publicApi.disabled') ? this.publicApiEndpoint : '',
|
config.getEnv('publicApi.disabled') ? publicApiEndpoint : '',
|
||||||
...excludeEndpoints.split(':'),
|
...excludeEndpoints.split(':'),
|
||||||
].filter((u) => !!u);
|
].filter((u) => !!u);
|
||||||
|
|
||||||
|
@ -635,7 +532,7 @@ class App {
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
|
||||||
if (!config.getEnv('publicApi.disabled')) {
|
if (!config.getEnv('publicApi.disabled')) {
|
||||||
const { apiRouters, apiLatestVersion } = await loadPublicApiVersions(this.publicApiEndpoint);
|
const { apiRouters, apiLatestVersion } = await loadPublicApiVersions(publicApiEndpoint);
|
||||||
this.app.use(...apiRouters);
|
this.app.use(...apiRouters);
|
||||||
this.frontendSettings.publicApi.latestVersion = apiLatestVersion;
|
this.frontendSettings.publicApi.latestVersion = apiLatestVersion;
|
||||||
}
|
}
|
||||||
|
@ -643,6 +540,7 @@ class App {
|
||||||
this.app.use(cookieParser());
|
this.app.use(cookieParser());
|
||||||
|
|
||||||
// Get push connections
|
// Get push connections
|
||||||
|
const push = Push.getInstance();
|
||||||
this.app.use(`/${this.restEndpoint}/push`, corsMiddleware, async (req, res, next) => {
|
this.app.use(`/${this.restEndpoint}/push`, corsMiddleware, async (req, res, next) => {
|
||||||
const { sessionId } = req.query;
|
const { sessionId } = req.query;
|
||||||
if (sessionId === undefined) {
|
if (sessionId === undefined) {
|
||||||
|
@ -660,54 +558,9 @@ class App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.push.add(sessionId as string, req, res);
|
push.add(sessionId as string, req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Compress the response data
|
|
||||||
this.app.use(compression());
|
|
||||||
|
|
||||||
// Make sure that each request has the "parsedUrl" parameter
|
|
||||||
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
||||||
(req as ICustomRequest).parsedUrl = parseUrl(req);
|
|
||||||
req.rawBody = Buffer.from('', 'base64');
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Support application/json type post data
|
|
||||||
this.app.use(
|
|
||||||
bodyParser.json({
|
|
||||||
limit: `${this.payloadSizeMax}mb`,
|
|
||||||
verify: (req, res, buf) => {
|
|
||||||
req.rawBody = buf;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Support application/xml type post data
|
|
||||||
this.app.use(
|
|
||||||
// @ts-ignore
|
|
||||||
bodyParser.xml({
|
|
||||||
limit: `${this.payloadSizeMax}mb`,
|
|
||||||
xmlParseOptions: {
|
|
||||||
normalize: true, // Trim whitespace inside text nodes
|
|
||||||
normalizeTags: true, // Transform tags to lowercase
|
|
||||||
explicitArray: false, // Only put properties in array if length > 1
|
|
||||||
},
|
|
||||||
verify: (req: express.Request, res: any, buf: any) => {
|
|
||||||
req.rawBody = buf;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.app.use(
|
|
||||||
bodyParser.text({
|
|
||||||
limit: `${this.payloadSizeMax}mb`,
|
|
||||||
verify: (req, res, buf) => {
|
|
||||||
req.rawBody = buf;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Make sure that Vue history mode works properly
|
// Make sure that Vue history mode works properly
|
||||||
this.app.use(
|
this.app.use(
|
||||||
history({
|
history({
|
||||||
|
@ -722,29 +575,6 @@ class App {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// support application/x-www-form-urlencoded post data
|
|
||||||
this.app.use(
|
|
||||||
bodyParser.urlencoded({
|
|
||||||
limit: `${this.payloadSizeMax}mb`,
|
|
||||||
extended: false,
|
|
||||||
verify: (req, res, buf) => {
|
|
||||||
req.rawBody = buf;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.app.use(corsMiddleware);
|
|
||||||
|
|
||||||
// eslint-disable-next-line consistent-return
|
|
||||||
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
||||||
if (!Db.isInitialized) {
|
|
||||||
const error = new ResponseHelper.ServiceUnavailableError('Database is not ready!');
|
|
||||||
return ResponseHelper.sendErrorResponse(res, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// User Management
|
// User Management
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
@ -759,40 +589,6 @@ class App {
|
||||||
this.app.use(`/${this.restEndpoint}/nodes`, nodesController);
|
this.app.use(`/${this.restEndpoint}/nodes`, nodesController);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// Healthcheck
|
|
||||||
// ----------------------------------------
|
|
||||||
|
|
||||||
// Does very basic health check
|
|
||||||
this.app.get('/healthz', async (req: express.Request, res: express.Response) => {
|
|
||||||
LoggerProxy.debug('Health check started!');
|
|
||||||
|
|
||||||
const connection = getConnectionManager().get();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!connection.isConnected) {
|
|
||||||
// Connection is not active
|
|
||||||
throw new Error('No active database connection!');
|
|
||||||
}
|
|
||||||
// DB ping
|
|
||||||
await connection.query('SELECT 1');
|
|
||||||
} catch (err) {
|
|
||||||
ErrorReporter.error(err);
|
|
||||||
LoggerProxy.error('No Database connection!', err);
|
|
||||||
const error = new ResponseHelper.ServiceUnavailableError('No Database connection!');
|
|
||||||
return ResponseHelper.sendErrorResponse(res, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Everything fine
|
|
||||||
const responseData = {
|
|
||||||
status: 'ok',
|
|
||||||
};
|
|
||||||
|
|
||||||
LoggerProxy.debug('Health check completed successfully!');
|
|
||||||
|
|
||||||
ResponseHelper.sendSuccessResponse(res, responseData, true, 200);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// Metrics
|
// Metrics
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
@ -1155,7 +951,7 @@ class App {
|
||||||
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
||||||
|
|
||||||
// Add special database related data
|
// Add special database related data
|
||||||
newCredentialsData.updatedAt = this.getCurrentDate();
|
newCredentialsData.updatedAt = new Date();
|
||||||
|
|
||||||
// Update the credentials in DB
|
// Update the credentials in DB
|
||||||
await Db.collections.Credentials.update(credentialId, newCredentialsData);
|
await Db.collections.Credentials.update(credentialId, newCredentialsData);
|
||||||
|
@ -1267,7 +1063,7 @@ class App {
|
||||||
credentials.setData(decryptedDataOriginal, encryptionKey);
|
credentials.setData(decryptedDataOriginal, encryptionKey);
|
||||||
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
|
||||||
// Add special database related data
|
// Add special database related data
|
||||||
newCredentialsData.updatedAt = this.getCurrentDate();
|
newCredentialsData.updatedAt = new Date();
|
||||||
// Save the credentials in DB
|
// Save the credentials in DB
|
||||||
await Db.collections.Credentials.update(credentialId, newCredentialsData);
|
await Db.collections.Credentials.update(credentialId, newCredentialsData);
|
||||||
|
|
||||||
|
@ -1486,16 +1282,6 @@ class App {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Removes a test webhook
|
|
||||||
this.app.delete(
|
|
||||||
`/${this.restEndpoint}/test-webhook/:id`,
|
|
||||||
ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<boolean> => {
|
|
||||||
// TODO UM: check if this needs validation with user management.
|
|
||||||
const workflowId = req.params.id;
|
|
||||||
return this.testWebhooks.cancelTestWebhook(workflowId);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// Options
|
// Options
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
@ -1565,69 +1351,11 @@ class App {
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
|
||||||
if (!config.getEnv('endpoints.disableProductionWebhooksOnMainProcess')) {
|
if (!config.getEnv('endpoints.disableProductionWebhooksOnMainProcess')) {
|
||||||
WebhookServer.registerProductionWebhooks.apply(this);
|
this.setupWebhookEndpoint();
|
||||||
|
this.setupWaitingWebhookEndpoint();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register all webhook requests (test for UI)
|
this.setupTestWebhookEndpoint();
|
||||||
this.app.all(
|
|
||||||
`/${this.endpointWebhookTest}/*`,
|
|
||||||
async (req: express.Request, res: express.Response) => {
|
|
||||||
// Cut away the "/webhook-test/" to get the registered part of the url
|
|
||||||
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
|
|
||||||
this.endpointWebhookTest.length + 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
const method = req.method.toUpperCase() as WebhookHttpMethod;
|
|
||||||
|
|
||||||
if (method === 'OPTIONS') {
|
|
||||||
let allowedMethods: string[];
|
|
||||||
try {
|
|
||||||
allowedMethods = await this.testWebhooks.getWebhookMethods(requestUrl);
|
|
||||||
allowedMethods.push('OPTIONS');
|
|
||||||
|
|
||||||
// Add custom "Allow" header to satisfy OPTIONS response.
|
|
||||||
res.append('Allow', allowedMethods);
|
|
||||||
} catch (error) {
|
|
||||||
ResponseHelper.sendErrorResponse(res, error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.header('Access-Control-Allow-Origin', '*');
|
|
||||||
|
|
||||||
ResponseHelper.sendSuccessResponse(res, {}, true, 204);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!WEBHOOK_METHODS.includes(method)) {
|
|
||||||
ResponseHelper.sendErrorResponse(
|
|
||||||
res,
|
|
||||||
new Error(`The method ${method} is not supported.`),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let response;
|
|
||||||
try {
|
|
||||||
response = await this.testWebhooks.callTestWebhook(method, requestUrl, req, res);
|
|
||||||
} catch (error) {
|
|
||||||
ResponseHelper.sendErrorResponse(res, error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.noWebhookResponse === true) {
|
|
||||||
// Nothing else to do as the response got already sent
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ResponseHelper.sendSuccessResponse(
|
|
||||||
res,
|
|
||||||
response.data,
|
|
||||||
true,
|
|
||||||
response.responseCode,
|
|
||||||
response.headers,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.endpointPresetCredentials !== '') {
|
if (this.endpointPresetCredentials !== '') {
|
||||||
// POST endpoint to set preset credentials
|
// POST endpoint to set preset credentials
|
||||||
|
@ -1676,97 +1404,53 @@ class App {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function start(): Promise<void> {
|
export async function start(): Promise<void> {
|
||||||
const PORT = config.getEnv('port');
|
const app = new Server();
|
||||||
const ADDRESS = config.getEnv('listen_address');
|
await app.start();
|
||||||
|
|
||||||
const app = new App();
|
const cpus = os.cpus();
|
||||||
|
const binaryDataConfig = config.getEnv('binaryDataManager');
|
||||||
await app.config();
|
const diagnosticInfo: IDiagnosticInfo = {
|
||||||
|
basicAuthActive: config.getEnv('security.basicAuth.active'),
|
||||||
let server;
|
databaseType: (await GenericHelpers.getConfigValue('database.type')) as DatabaseType,
|
||||||
|
disableProductionWebhooksOnMainProcess: config.getEnv(
|
||||||
if (app.protocol === 'https' && app.sslKey && app.sslCert) {
|
'endpoints.disableProductionWebhooksOnMainProcess',
|
||||||
const https = require('https');
|
),
|
||||||
const privateKey = readFileSync(app.sslKey, 'utf8');
|
notificationsEnabled: config.getEnv('versionNotifications.enabled'),
|
||||||
const cert = readFileSync(app.sslCert, 'utf8');
|
versionCli: N8N_VERSION,
|
||||||
const credentials = { key: privateKey, cert };
|
systemInfo: {
|
||||||
server = https.createServer(credentials, app.app);
|
os: {
|
||||||
} else {
|
type: os.type(),
|
||||||
const http = require('http');
|
version: os.version(),
|
||||||
server = http.createServer(app.app);
|
|
||||||
}
|
|
||||||
|
|
||||||
server.listen(PORT, ADDRESS, async () => {
|
|
||||||
const versions = await GenericHelpers.getVersions();
|
|
||||||
console.log(`n8n ready on ${ADDRESS}, port ${PORT}`);
|
|
||||||
console.log(`Version: ${versions.cli}`);
|
|
||||||
|
|
||||||
const defaultLocale = config.getEnv('defaultLocale');
|
|
||||||
|
|
||||||
if (defaultLocale !== 'en') {
|
|
||||||
console.log(`Locale: ${defaultLocale}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.externalHooks.run('n8n.ready', [app, config]);
|
|
||||||
const cpus = os.cpus();
|
|
||||||
const binaryDataConfig = config.getEnv('binaryDataManager');
|
|
||||||
const diagnosticInfo: IDiagnosticInfo = {
|
|
||||||
basicAuthActive: config.getEnv('security.basicAuth.active'),
|
|
||||||
databaseType: (await GenericHelpers.getConfigValue('database.type')) as DatabaseType,
|
|
||||||
disableProductionWebhooksOnMainProcess: config.getEnv(
|
|
||||||
'endpoints.disableProductionWebhooksOnMainProcess',
|
|
||||||
),
|
|
||||||
notificationsEnabled: config.getEnv('versionNotifications.enabled'),
|
|
||||||
versionCli: versions.cli,
|
|
||||||
systemInfo: {
|
|
||||||
os: {
|
|
||||||
type: os.type(),
|
|
||||||
version: os.version(),
|
|
||||||
},
|
|
||||||
memory: os.totalmem() / 1024,
|
|
||||||
cpus: {
|
|
||||||
count: cpus.length,
|
|
||||||
model: cpus[0].model,
|
|
||||||
speed: cpus[0].speed,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
executionVariables: {
|
memory: os.totalmem() / 1024,
|
||||||
executions_process: config.getEnv('executions.process'),
|
cpus: {
|
||||||
executions_mode: config.getEnv('executions.mode'),
|
count: cpus.length,
|
||||||
executions_timeout: config.getEnv('executions.timeout'),
|
model: cpus[0].model,
|
||||||
executions_timeout_max: config.getEnv('executions.maxTimeout'),
|
speed: cpus[0].speed,
|
||||||
executions_data_save_on_error: config.getEnv('executions.saveDataOnError'),
|
|
||||||
executions_data_save_on_success: config.getEnv('executions.saveDataOnSuccess'),
|
|
||||||
executions_data_save_on_progress: config.getEnv('executions.saveExecutionProgress'),
|
|
||||||
executions_data_save_manual_executions: config.getEnv(
|
|
||||||
'executions.saveDataManualExecutions',
|
|
||||||
),
|
|
||||||
executions_data_prune: config.getEnv('executions.pruneData'),
|
|
||||||
executions_data_max_age: config.getEnv('executions.pruneDataMaxAge'),
|
|
||||||
executions_data_prune_timeout: config.getEnv('executions.pruneDataTimeout'),
|
|
||||||
},
|
},
|
||||||
deploymentType: config.getEnv('deployment.type'),
|
},
|
||||||
binaryDataMode: binaryDataConfig.mode,
|
executionVariables: {
|
||||||
n8n_multi_user_allowed: isUserManagementEnabled(),
|
executions_process: config.getEnv('executions.process'),
|
||||||
smtp_set_up: config.getEnv('userManagement.emails.mode') === 'smtp',
|
executions_mode: config.getEnv('executions.mode'),
|
||||||
};
|
executions_timeout: config.getEnv('executions.timeout'),
|
||||||
|
executions_timeout_max: config.getEnv('executions.maxTimeout'),
|
||||||
|
executions_data_save_on_error: config.getEnv('executions.saveDataOnError'),
|
||||||
|
executions_data_save_on_success: config.getEnv('executions.saveDataOnSuccess'),
|
||||||
|
executions_data_save_on_progress: config.getEnv('executions.saveExecutionProgress'),
|
||||||
|
executions_data_save_manual_executions: config.getEnv('executions.saveDataManualExecutions'),
|
||||||
|
executions_data_prune: config.getEnv('executions.pruneData'),
|
||||||
|
executions_data_max_age: config.getEnv('executions.pruneDataMaxAge'),
|
||||||
|
executions_data_prune_timeout: config.getEnv('executions.pruneDataTimeout'),
|
||||||
|
},
|
||||||
|
deploymentType: config.getEnv('deployment.type'),
|
||||||
|
binaryDataMode: binaryDataConfig.mode,
|
||||||
|
n8n_multi_user_allowed: isUserManagementEnabled(),
|
||||||
|
smtp_set_up: config.getEnv('userManagement.emails.mode') === 'smtp',
|
||||||
|
};
|
||||||
|
|
||||||
void Db.collections
|
const workflow = await Db.collections.Workflow!.findOne({
|
||||||
.Workflow!.findOne({
|
select: ['createdAt'],
|
||||||
select: ['createdAt'],
|
order: { createdAt: 'ASC' },
|
||||||
order: { createdAt: 'ASC' },
|
|
||||||
})
|
|
||||||
.then(async (workflow) =>
|
|
||||||
InternalHooksManager.getInstance().onServerStarted(diagnosticInfo, workflow?.createdAt),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
server.on('error', (error: Error & { code: string }) => {
|
|
||||||
if (error.code === 'EADDRINUSE') {
|
|
||||||
console.log(
|
|
||||||
`n8n's port ${PORT} is already in use. Do you have another instance of n8n running already?`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
await InternalHooksManager.getInstance().onServerStarted(diagnosticInfo, workflow?.createdAt);
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,5 @@ export interface N8nApp {
|
||||||
app: Application;
|
app: Application;
|
||||||
restEndpoint: string;
|
restEndpoint: string;
|
||||||
externalHooks: IExternalHooksClass;
|
externalHooks: IExternalHooksClass;
|
||||||
defaultCredentialsName: string;
|
|
||||||
activeWorkflowRunner: ActiveWorkflowRunner;
|
activeWorkflowRunner: ActiveWorkflowRunner;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,356 +1,8 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
import { AbstractServer } from '@/AbstractServer';
|
||||||
/* eslint-disable no-console */
|
|
||||||
/* eslint-disable consistent-return */
|
|
||||||
/* eslint-disable @typescript-eslint/restrict-plus-operands */
|
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
||||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
||||||
import express from 'express';
|
|
||||||
import { readFileSync } from 'fs';
|
|
||||||
import { getConnectionManager } from 'typeorm';
|
|
||||||
import bodyParser from 'body-parser';
|
|
||||||
|
|
||||||
import compression from 'compression';
|
export class WebhookServer extends AbstractServer {
|
||||||
import parseUrl from 'parseurl';
|
async configure() {
|
||||||
import { WebhookHttpMethod } from 'n8n-workflow';
|
this.setupWebhookEndpoint();
|
||||||
|
this.setupWaitingWebhookEndpoint();
|
||||||
import * as Db from '@/Db';
|
|
||||||
import * as ActiveExecutions from '@/ActiveExecutions';
|
|
||||||
import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner';
|
|
||||||
import { ExternalHooks } from '@/ExternalHooks';
|
|
||||||
import * as GenericHelpers from '@/GenericHelpers';
|
|
||||||
import * as ResponseHelper from '@/ResponseHelper';
|
|
||||||
import { WaitingWebhooks } from '@/WaitingWebhooks';
|
|
||||||
import type { ICustomRequest, IExternalHooksClass, IPackageVersions } from '@/Interfaces';
|
|
||||||
import config from '@/config';
|
|
||||||
import { WEBHOOK_METHODS } from '@/WebhookHelpers';
|
|
||||||
import { setupErrorMiddleware } from '@/ErrorReporting';
|
|
||||||
import { corsMiddleware } from './middlewares/cors';
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-call
|
|
||||||
require('body-parser-xml')(bodyParser);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
||||||
export function registerProductionWebhooks() {
|
|
||||||
// ----------------------------------------
|
|
||||||
// Regular Webhooks
|
|
||||||
// ----------------------------------------
|
|
||||||
|
|
||||||
// Register all webhook requests
|
|
||||||
this.app.all(
|
|
||||||
`/${this.endpointWebhook}/*`,
|
|
||||||
async (req: express.Request, res: express.Response) => {
|
|
||||||
// Cut away the "/webhook/" to get the registered part of the url
|
|
||||||
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
|
|
||||||
this.endpointWebhook.length + 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
const method = req.method.toUpperCase() as WebhookHttpMethod;
|
|
||||||
|
|
||||||
if (method === 'OPTIONS') {
|
|
||||||
let allowedMethods: string[];
|
|
||||||
try {
|
|
||||||
allowedMethods = await this.activeWorkflowRunner.getWebhookMethods(requestUrl);
|
|
||||||
allowedMethods.push('OPTIONS');
|
|
||||||
|
|
||||||
// Add custom "Allow" header to satisfy OPTIONS response.
|
|
||||||
res.append('Allow', allowedMethods);
|
|
||||||
} catch (error) {
|
|
||||||
ResponseHelper.sendErrorResponse(res, error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.header('Access-Control-Allow-Origin', '*');
|
|
||||||
|
|
||||||
ResponseHelper.sendSuccessResponse(res, {}, true, 204);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!WEBHOOK_METHODS.includes(method)) {
|
|
||||||
ResponseHelper.sendErrorResponse(res, new Error(`The method ${method} is not supported.`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let response;
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
||||||
response = await this.activeWorkflowRunner.executeWebhook(method, requestUrl, req, res);
|
|
||||||
} catch (error) {
|
|
||||||
ResponseHelper.sendErrorResponse(res, error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.noWebhookResponse === true) {
|
|
||||||
// Nothing else to do as the response got already sent
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ResponseHelper.sendSuccessResponse(
|
|
||||||
res,
|
|
||||||
response.data,
|
|
||||||
true,
|
|
||||||
response.responseCode,
|
|
||||||
response.headers,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// Waiting Webhooks
|
|
||||||
// ----------------------------------------
|
|
||||||
|
|
||||||
const waitingWebhooks = new WaitingWebhooks();
|
|
||||||
|
|
||||||
// Register all webhook-waiting requests
|
|
||||||
this.app.all(
|
|
||||||
`/${this.endpointWebhookWaiting}/*`,
|
|
||||||
async (req: express.Request, res: express.Response) => {
|
|
||||||
// Cut away the "/webhook-waiting/" to get the registered part of the url
|
|
||||||
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(
|
|
||||||
this.endpointWebhookWaiting.length + 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
const method = req.method.toUpperCase() as WebhookHttpMethod;
|
|
||||||
|
|
||||||
// TODO: Add support for OPTIONS in the future
|
|
||||||
// if (method === 'OPTIONS') {
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (!WEBHOOK_METHODS.includes(method)) {
|
|
||||||
ResponseHelper.sendErrorResponse(res, new Error(`The method ${method} is not supported.`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let response;
|
|
||||||
try {
|
|
||||||
response = await waitingWebhooks.executeWebhook(method, requestUrl, req, res);
|
|
||||||
} catch (error) {
|
|
||||||
ResponseHelper.sendErrorResponse(res, error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.noWebhookResponse === true) {
|
|
||||||
// Nothing else to do as the response got already sent
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ResponseHelper.sendSuccessResponse(
|
|
||||||
res,
|
|
||||||
response.data,
|
|
||||||
true,
|
|
||||||
response.responseCode,
|
|
||||||
response.headers,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class App {
|
|
||||||
app: express.Application;
|
|
||||||
|
|
||||||
activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner;
|
|
||||||
|
|
||||||
endpointWebhook: string;
|
|
||||||
|
|
||||||
endpointWebhookWaiting: string;
|
|
||||||
|
|
||||||
endpointPresetCredentials: string;
|
|
||||||
|
|
||||||
externalHooks: IExternalHooksClass;
|
|
||||||
|
|
||||||
saveDataErrorExecution: string;
|
|
||||||
|
|
||||||
saveDataSuccessExecution: string;
|
|
||||||
|
|
||||||
saveManualExecutions: boolean;
|
|
||||||
|
|
||||||
executionTimeout: number;
|
|
||||||
|
|
||||||
maxExecutionTimeout: number;
|
|
||||||
|
|
||||||
timezone: string;
|
|
||||||
|
|
||||||
activeExecutionsInstance: ActiveExecutions.ActiveExecutions;
|
|
||||||
|
|
||||||
versions: IPackageVersions | undefined;
|
|
||||||
|
|
||||||
restEndpoint: string;
|
|
||||||
|
|
||||||
protocol: string;
|
|
||||||
|
|
||||||
sslKey: string;
|
|
||||||
|
|
||||||
sslCert: string;
|
|
||||||
|
|
||||||
presetCredentialsLoaded: boolean;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.app = express();
|
|
||||||
this.app.disable('x-powered-by');
|
|
||||||
|
|
||||||
this.endpointWebhook = config.getEnv('endpoints.webhook');
|
|
||||||
this.endpointWebhookWaiting = config.getEnv('endpoints.webhookWaiting');
|
|
||||||
this.saveDataErrorExecution = config.getEnv('executions.saveDataOnError');
|
|
||||||
this.saveDataSuccessExecution = config.getEnv('executions.saveDataOnSuccess');
|
|
||||||
this.saveManualExecutions = config.getEnv('executions.saveDataManualExecutions');
|
|
||||||
this.executionTimeout = config.getEnv('executions.timeout');
|
|
||||||
this.maxExecutionTimeout = config.getEnv('executions.maxTimeout');
|
|
||||||
this.timezone = config.getEnv('generic.timezone');
|
|
||||||
this.restEndpoint = config.getEnv('endpoints.rest');
|
|
||||||
|
|
||||||
this.activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
|
|
||||||
|
|
||||||
this.activeExecutionsInstance = ActiveExecutions.getInstance();
|
|
||||||
|
|
||||||
this.protocol = config.getEnv('protocol');
|
|
||||||
this.sslKey = config.getEnv('ssl_key');
|
|
||||||
this.sslCert = config.getEnv('ssl_cert');
|
|
||||||
|
|
||||||
this.externalHooks = ExternalHooks();
|
|
||||||
|
|
||||||
this.presetCredentialsLoaded = false;
|
|
||||||
this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
|
|
||||||
|
|
||||||
void setupErrorMiddleware(this.app);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current epoch time
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
getCurrentDate(): Date {
|
|
||||||
return new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
async config(): Promise<void> {
|
|
||||||
this.versions = await GenericHelpers.getVersions();
|
|
||||||
|
|
||||||
// Compress the response data
|
|
||||||
this.app.use(compression());
|
|
||||||
|
|
||||||
// Make sure that each request has the "parsedUrl" parameter
|
|
||||||
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
||||||
(req as ICustomRequest).parsedUrl = parseUrl(req);
|
|
||||||
req.rawBody = Buffer.from('', 'base64');
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Support application/json type post data
|
|
||||||
this.app.use(
|
|
||||||
bodyParser.json({
|
|
||||||
limit: '16mb',
|
|
||||||
verify: (req, res, buf) => {
|
|
||||||
req.rawBody = buf;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Support application/xml type post data
|
|
||||||
this.app.use(
|
|
||||||
// @ts-ignore
|
|
||||||
bodyParser.xml({
|
|
||||||
limit: '16mb',
|
|
||||||
xmlParseOptions: {
|
|
||||||
normalize: true, // Trim whitespace inside text nodes
|
|
||||||
normalizeTags: true, // Transform tags to lowercase
|
|
||||||
explicitArray: false, // Only put properties in array if length > 1
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.app.use(
|
|
||||||
bodyParser.text({
|
|
||||||
limit: '16mb',
|
|
||||||
verify: (req, res, buf) => {
|
|
||||||
req.rawBody = buf;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// support application/x-www-form-urlencoded post data
|
|
||||||
this.app.use(
|
|
||||||
bodyParser.urlencoded({
|
|
||||||
extended: false,
|
|
||||||
verify: (req, res, buf) => {
|
|
||||||
req.rawBody = buf;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.app.use(corsMiddleware);
|
|
||||||
|
|
||||||
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
||||||
if (!Db.isInitialized) {
|
|
||||||
const error = new ResponseHelper.ServiceUnavailableError('Database is not ready!');
|
|
||||||
return ResponseHelper.sendErrorResponse(res, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// Healthcheck
|
|
||||||
// ----------------------------------------
|
|
||||||
|
|
||||||
// Does very basic health check
|
|
||||||
this.app.get('/healthz', async (req: express.Request, res: express.Response) => {
|
|
||||||
const connection = getConnectionManager().get();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!connection.isConnected) {
|
|
||||||
// Connection is not active
|
|
||||||
throw new Error('No active database connection!');
|
|
||||||
}
|
|
||||||
// DB ping
|
|
||||||
await connection.query('SELECT 1');
|
|
||||||
// eslint-disable-next-line id-denylist
|
|
||||||
} catch (err) {
|
|
||||||
const error = new ResponseHelper.ServiceUnavailableError('No Database connection!');
|
|
||||||
return ResponseHelper.sendErrorResponse(res, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Everything fine
|
|
||||||
const responseData = {
|
|
||||||
status: 'ok',
|
|
||||||
};
|
|
||||||
|
|
||||||
ResponseHelper.sendSuccessResponse(res, responseData, true, 200);
|
|
||||||
});
|
|
||||||
|
|
||||||
registerProductionWebhooks.apply(this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function start(): Promise<void> {
|
|
||||||
const PORT = config.getEnv('port');
|
|
||||||
const ADDRESS = config.getEnv('listen_address');
|
|
||||||
|
|
||||||
const app = new App();
|
|
||||||
|
|
||||||
await app.config();
|
|
||||||
|
|
||||||
let server;
|
|
||||||
|
|
||||||
if (app.protocol === 'https' && app.sslKey && app.sslCert) {
|
|
||||||
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
|
|
||||||
const https = require('https');
|
|
||||||
const privateKey = readFileSync(app.sslKey, 'utf8');
|
|
||||||
const cert = readFileSync(app.sslCert, 'utf8');
|
|
||||||
const credentials = { key: privateKey, cert };
|
|
||||||
server = https.createServer(credentials, app.app);
|
|
||||||
} else {
|
|
||||||
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
|
|
||||||
const http = require('http');
|
|
||||||
server = http.createServer(app.app);
|
|
||||||
}
|
|
||||||
|
|
||||||
server.listen(PORT, ADDRESS, async () => {
|
|
||||||
const versions = await GenericHelpers.getVersions();
|
|
||||||
console.log(`n8n ready on ${ADDRESS}, port ${PORT}`);
|
|
||||||
console.log(`Version: ${versions.cli}`);
|
|
||||||
|
|
||||||
await app.externalHooks.run('n8n.ready', [app, config]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -34,7 +34,6 @@ import { CredentialTypes } from '@/CredentialTypes';
|
||||||
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import { ExternalHooks } from '@/ExternalHooks';
|
import { ExternalHooks } from '@/ExternalHooks';
|
||||||
import * as GenericHelpers from '@/GenericHelpers';
|
|
||||||
import { IWorkflowExecuteProcess, IWorkflowExecutionDataProcessWithExecution } from '@/Interfaces';
|
import { IWorkflowExecuteProcess, IWorkflowExecutionDataProcessWithExecution } from '@/Interfaces';
|
||||||
import { NodeTypes } from '@/NodeTypes';
|
import { NodeTypes } from '@/NodeTypes';
|
||||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
|
@ -111,8 +110,7 @@ class WorkflowRunnerProcess {
|
||||||
await externalHooks.init();
|
await externalHooks.init();
|
||||||
|
|
||||||
const instanceId = (await UserSettings.prepareUserSettings()).instanceId ?? '';
|
const instanceId = (await UserSettings.prepareUserSettings()).instanceId ?? '';
|
||||||
const { cli } = await GenericHelpers.getVersions();
|
await InternalHooksManager.init(instanceId, nodeTypes);
|
||||||
await InternalHooksManager.init(instanceId, cli, nodeTypes);
|
|
||||||
|
|
||||||
const binaryDataConfig = config.getEnv('binaryDataManager');
|
const binaryDataConfig = config.getEnv('binaryDataManager');
|
||||||
await BinaryDataManager.init(binaryDataConfig);
|
await BinaryDataManager.init(binaryDataConfig);
|
||||||
|
@ -121,7 +119,7 @@ class WorkflowRunnerProcess {
|
||||||
await Db.init();
|
await Db.init();
|
||||||
|
|
||||||
const license = getLicense();
|
const license = getLicense();
|
||||||
await license.init(instanceId, cli);
|
await license.init(instanceId);
|
||||||
|
|
||||||
// Start timeout for the execution
|
// Start timeout for the execution
|
||||||
let workflowTimeout = config.getEnv('executions.timeout'); // initialize with default
|
let workflowTimeout = config.getEnv('executions.timeout'); // initialize with default
|
||||||
|
|
|
@ -11,7 +11,6 @@ import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
||||||
import { CredentialTypes } from '@/CredentialTypes';
|
import { CredentialTypes } from '@/CredentialTypes';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import { ExternalHooks } from '@/ExternalHooks';
|
import { ExternalHooks } from '@/ExternalHooks';
|
||||||
import * as GenericHelpers from '@/GenericHelpers';
|
|
||||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
import { NodeTypes } from '@/NodeTypes';
|
import { NodeTypes } from '@/NodeTypes';
|
||||||
import { InternalHooksManager } from '@/InternalHooksManager';
|
import { InternalHooksManager } from '@/InternalHooksManager';
|
||||||
|
@ -137,8 +136,7 @@ export class Execute extends Command {
|
||||||
CredentialTypes(loadNodesAndCredentials);
|
CredentialTypes(loadNodesAndCredentials);
|
||||||
|
|
||||||
const instanceId = await UserSettings.getInstanceId();
|
const instanceId = await UserSettings.getInstanceId();
|
||||||
const { cli } = await GenericHelpers.getVersions();
|
await InternalHooksManager.init(instanceId, nodeTypes);
|
||||||
await InternalHooksManager.init(instanceId, cli, nodeTypes);
|
|
||||||
|
|
||||||
if (!WorkflowHelpers.isWorkflowIdValid(workflowId)) {
|
if (!WorkflowHelpers.isWorkflowIdValid(workflowId)) {
|
||||||
workflowId = undefined;
|
workflowId = undefined;
|
||||||
|
|
|
@ -25,7 +25,6 @@ import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
||||||
import { CredentialTypes } from '@/CredentialTypes';
|
import { CredentialTypes } from '@/CredentialTypes';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import { ExternalHooks } from '@/ExternalHooks';
|
import { ExternalHooks } from '@/ExternalHooks';
|
||||||
import * as GenericHelpers from '@/GenericHelpers';
|
|
||||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
import { NodeTypes } from '@/NodeTypes';
|
import { NodeTypes } from '@/NodeTypes';
|
||||||
import { InternalHooksManager } from '@/InternalHooksManager';
|
import { InternalHooksManager } from '@/InternalHooksManager';
|
||||||
|
@ -325,8 +324,7 @@ export class ExecuteBatch extends Command {
|
||||||
CredentialTypes(loadNodesAndCredentials);
|
CredentialTypes(loadNodesAndCredentials);
|
||||||
|
|
||||||
const instanceId = await UserSettings.getInstanceId();
|
const instanceId = await UserSettings.getInstanceId();
|
||||||
const { cli } = await GenericHelpers.getVersions();
|
await InternalHooksManager.init(instanceId, nodeTypes);
|
||||||
await InternalHooksManager.init(instanceId, cli, nodeTypes);
|
|
||||||
|
|
||||||
// Send a shallow copy of allWorkflows so we still have all workflow data.
|
// Send a shallow copy of allWorkflows so we still have all workflow data.
|
||||||
const results = await this.runTests([...allWorkflows]);
|
const results = await this.runTests([...allWorkflows]);
|
||||||
|
|
|
@ -17,7 +17,7 @@ import replaceStream from 'replacestream';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import glob from 'fast-glob';
|
import glob from 'fast-glob';
|
||||||
|
|
||||||
import { IDataObject, LoggerProxy, sleep } from 'n8n-workflow';
|
import { LoggerProxy, sleep } from 'n8n-workflow';
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
|
||||||
|
@ -234,314 +234,231 @@ export class Start extends Command {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
const { flags } = this.parse(Start);
|
const { flags } = this.parse(Start);
|
||||||
|
|
||||||
// Wrap that the process does not close but we can still use async
|
try {
|
||||||
await (async () => {
|
// Start directly with the init of the database to improve startup time
|
||||||
try {
|
const startDbInitPromise = Db.init().catch((error: Error) => {
|
||||||
// Start directly with the init of the database to improve startup time
|
logger.error(`There was an error initializing DB: "${error.message}"`);
|
||||||
const startDbInitPromise = Db.init().catch((error: Error) => {
|
|
||||||
logger.error(`There was an error initializing DB: "${error.message}"`);
|
|
||||||
|
|
||||||
processExitCode = 1;
|
|
||||||
// @ts-ignore
|
|
||||||
process.emit('SIGINT');
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Make sure the settings exist
|
|
||||||
const userSettings = await UserSettings.prepareUserSettings();
|
|
||||||
|
|
||||||
if (!config.getEnv('userManagement.jwtSecret')) {
|
|
||||||
// If we don't have a JWT secret set, generate
|
|
||||||
// one based and save to config.
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
|
|
||||||
// For a key off every other letter from encryption key
|
|
||||||
// CAREFUL: do not change this or it breaks all existing tokens.
|
|
||||||
let baseKey = '';
|
|
||||||
for (let i = 0; i < encryptionKey.length; i += 2) {
|
|
||||||
baseKey += encryptionKey[i];
|
|
||||||
}
|
|
||||||
config.set(
|
|
||||||
'userManagement.jwtSecret',
|
|
||||||
createHash('sha256').update(baseKey).digest('hex'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config.getEnv('endpoints.disableUi')) {
|
|
||||||
await Start.generateStaticAssets();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load all node and credential types
|
|
||||||
const loadNodesAndCredentials = LoadNodesAndCredentials();
|
|
||||||
await loadNodesAndCredentials.init();
|
|
||||||
|
|
||||||
// Load all external hooks
|
|
||||||
const externalHooks = ExternalHooks();
|
|
||||||
await externalHooks.init();
|
|
||||||
|
|
||||||
// Add the found types to an instance other parts of the application can use
|
|
||||||
const nodeTypes = NodeTypes(loadNodesAndCredentials);
|
|
||||||
const credentialTypes = CredentialTypes(loadNodesAndCredentials);
|
|
||||||
|
|
||||||
// Load the credentials overwrites if any exist
|
|
||||||
await CredentialsOverwrites(credentialTypes).init();
|
|
||||||
|
|
||||||
await loadNodesAndCredentials.generateTypesForFrontend();
|
|
||||||
|
|
||||||
// Wait till the database is ready
|
|
||||||
await startDbInitPromise;
|
|
||||||
|
|
||||||
const installedPackages = await getAllInstalledPackages();
|
|
||||||
const missingPackages = new Set<{
|
|
||||||
packageName: string;
|
|
||||||
version: string;
|
|
||||||
}>();
|
|
||||||
installedPackages.forEach((installedPackage) => {
|
|
||||||
installedPackage.installedNodes.forEach((installedNode) => {
|
|
||||||
if (!loadNodesAndCredentials.known.nodes[installedNode.type]) {
|
|
||||||
// Leave the list ready for installing in case we need.
|
|
||||||
missingPackages.add({
|
|
||||||
packageName: installedPackage.packageName,
|
|
||||||
version: installedPackage.installedVersion,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await UserSettings.getEncryptionKey();
|
|
||||||
|
|
||||||
// Load settings from database and set them to config.
|
|
||||||
const databaseSettings = await Db.collections.Settings.find({ loadOnStartup: true });
|
|
||||||
databaseSettings.forEach((setting) => {
|
|
||||||
config.set(setting.key, JSON.parse(setting.value));
|
|
||||||
});
|
|
||||||
|
|
||||||
config.set('nodes.packagesMissing', '');
|
|
||||||
if (missingPackages.size) {
|
|
||||||
LoggerProxy.error(
|
|
||||||
'n8n detected that some packages are missing. For more information, visit https://docs.n8n.io/integrations/community-nodes/troubleshooting/',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (flags.reinstallMissingPackages || process.env.N8N_REINSTALL_MISSING_PACKAGES) {
|
|
||||||
LoggerProxy.info('Attempting to reinstall missing packages', { missingPackages });
|
|
||||||
try {
|
|
||||||
// Optimistic approach - stop if any installation fails
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
|
||||||
for (const missingPackage of missingPackages) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
void (await loadNodesAndCredentials.loadNpmModule(
|
|
||||||
missingPackage.packageName,
|
|
||||||
missingPackage.version,
|
|
||||||
));
|
|
||||||
missingPackages.delete(missingPackage);
|
|
||||||
}
|
|
||||||
LoggerProxy.info(
|
|
||||||
'Packages reinstalled successfully. Resuming regular initialization.',
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
LoggerProxy.error('n8n was unable to install the missing packages.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (missingPackages.size) {
|
|
||||||
config.set(
|
|
||||||
'nodes.packagesMissing',
|
|
||||||
Array.from(missingPackages)
|
|
||||||
.map((missingPackage) => `${missingPackage.packageName}@${missingPackage.version}`)
|
|
||||||
.join(' '),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.getEnv('executions.mode') === 'queue') {
|
|
||||||
const redisHost = config.getEnv('queue.bull.redis.host');
|
|
||||||
const redisUsername = config.getEnv('queue.bull.redis.username');
|
|
||||||
const redisPassword = config.getEnv('queue.bull.redis.password');
|
|
||||||
const redisPort = config.getEnv('queue.bull.redis.port');
|
|
||||||
const redisDB = config.getEnv('queue.bull.redis.db');
|
|
||||||
const redisConnectionTimeoutLimit = config.getEnv('queue.bull.redis.timeoutThreshold');
|
|
||||||
let lastTimer = 0;
|
|
||||||
let cumulativeTimeout = 0;
|
|
||||||
|
|
||||||
const settings = {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
retryStrategy: (times: number): number | null => {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - lastTimer > 30000) {
|
|
||||||
// Means we had no timeout at all or last timeout was temporary and we recovered
|
|
||||||
lastTimer = now;
|
|
||||||
cumulativeTimeout = 0;
|
|
||||||
} else {
|
|
||||||
cumulativeTimeout += now - lastTimer;
|
|
||||||
lastTimer = now;
|
|
||||||
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
|
|
||||||
logger.error(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
||||||
`Unable to connect to Redis after ${redisConnectionTimeoutLimit}. Exiting process.`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 500;
|
|
||||||
},
|
|
||||||
} as IDataObject;
|
|
||||||
|
|
||||||
if (redisHost) {
|
|
||||||
settings.host = redisHost;
|
|
||||||
}
|
|
||||||
if (redisUsername) {
|
|
||||||
settings.username = redisUsername;
|
|
||||||
}
|
|
||||||
if (redisPassword) {
|
|
||||||
settings.password = redisPassword;
|
|
||||||
}
|
|
||||||
if (redisPort) {
|
|
||||||
settings.port = redisPort;
|
|
||||||
}
|
|
||||||
if (redisDB) {
|
|
||||||
settings.db = redisDB;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
const { default: Redis } = await import('ioredis');
|
|
||||||
|
|
||||||
// This connection is going to be our heartbeat
|
|
||||||
// IORedis automatically pings redis and tries to reconnect
|
|
||||||
// We will be using the retryStrategy above
|
|
||||||
// to control how and when to exit.
|
|
||||||
const redis = new Redis(settings);
|
|
||||||
|
|
||||||
redis.on('error', (error) => {
|
|
||||||
if (error.toString().includes('ECONNREFUSED') === true) {
|
|
||||||
logger.warn('Redis unavailable - trying to reconnect...');
|
|
||||||
} else {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
||||||
logger.warn('Error with Redis: ', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const dbType = (await GenericHelpers.getConfigValue('database.type')) as DatabaseType;
|
|
||||||
|
|
||||||
if (dbType === 'sqlite') {
|
|
||||||
const shouldRunVacuum = config.getEnv('database.sqlite.executeVacuumOnStartup');
|
|
||||||
if (shouldRunVacuum) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
await Db.collections.Execution.query('VACUUM;');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flags.tunnel) {
|
|
||||||
this.log('\nWaiting for tunnel ...');
|
|
||||||
|
|
||||||
let tunnelSubdomain;
|
|
||||||
if (
|
|
||||||
process.env[TUNNEL_SUBDOMAIN_ENV] !== undefined &&
|
|
||||||
process.env[TUNNEL_SUBDOMAIN_ENV] !== ''
|
|
||||||
) {
|
|
||||||
tunnelSubdomain = process.env[TUNNEL_SUBDOMAIN_ENV];
|
|
||||||
} else if (userSettings.tunnelSubdomain !== undefined) {
|
|
||||||
tunnelSubdomain = userSettings.tunnelSubdomain;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tunnelSubdomain === undefined) {
|
|
||||||
// When no tunnel subdomain did exist yet create a new random one
|
|
||||||
const availableCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
||||||
userSettings.tunnelSubdomain = Array.from({ length: 24 })
|
|
||||||
.map(() => {
|
|
||||||
return availableCharacters.charAt(
|
|
||||||
Math.floor(Math.random() * availableCharacters.length),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
await UserSettings.writeUserSettings(userSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tunnelSettings: localtunnel.TunnelConfig = {
|
|
||||||
host: 'https://hooks.n8n.cloud',
|
|
||||||
subdomain: tunnelSubdomain,
|
|
||||||
};
|
|
||||||
|
|
||||||
const port = config.getEnv('port');
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const webhookTunnel = await localtunnel(port, tunnelSettings);
|
|
||||||
|
|
||||||
process.env.WEBHOOK_URL = `${webhookTunnel.url}/`;
|
|
||||||
this.log(`Tunnel URL: ${process.env.WEBHOOK_URL}\n`);
|
|
||||||
this.log(
|
|
||||||
'IMPORTANT! Do not share with anybody as it would give people access to your n8n instance!',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const instanceId = await UserSettings.getInstanceId();
|
|
||||||
const { cli } = await GenericHelpers.getVersions();
|
|
||||||
await InternalHooksManager.init(instanceId, cli, nodeTypes);
|
|
||||||
|
|
||||||
const binaryDataConfig = config.getEnv('binaryDataManager');
|
|
||||||
await BinaryDataManager.init(binaryDataConfig, true);
|
|
||||||
|
|
||||||
await Server.start();
|
|
||||||
|
|
||||||
// Start to get active workflows and run their triggers
|
|
||||||
activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
|
|
||||||
await activeWorkflowRunner.init();
|
|
||||||
|
|
||||||
WaitTracker();
|
|
||||||
|
|
||||||
const editorUrl = GenericHelpers.getBaseUrl();
|
|
||||||
this.log(`\nEditor is now accessible via:\n${editorUrl}`);
|
|
||||||
|
|
||||||
const saveManualExecutions = config.getEnv('executions.saveDataManualExecutions');
|
|
||||||
|
|
||||||
if (saveManualExecutions) {
|
|
||||||
this.log('\nManual executions will be visible only for the owner');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow to open n8n editor by pressing "o"
|
|
||||||
if (Boolean(process.stdout.isTTY) && process.stdin.setRawMode) {
|
|
||||||
process.stdin.setRawMode(true);
|
|
||||||
process.stdin.resume();
|
|
||||||
process.stdin.setEncoding('utf8');
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
let inputText = '';
|
|
||||||
|
|
||||||
if (flags.open) {
|
|
||||||
Start.openBrowser();
|
|
||||||
}
|
|
||||||
this.log('\nPress "o" to open in Browser.');
|
|
||||||
process.stdin.on('data', (key: string) => {
|
|
||||||
if (key === 'o') {
|
|
||||||
Start.openBrowser();
|
|
||||||
inputText = '';
|
|
||||||
} else if (key.charCodeAt(0) === 3) {
|
|
||||||
// Ctrl + c got pressed
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
Start.stopProcess();
|
|
||||||
} else {
|
|
||||||
// When anything else got pressed, record it and send it on enter into the child process
|
|
||||||
// eslint-disable-next-line no-lonely-if
|
|
||||||
if (key.charCodeAt(0) === 13) {
|
|
||||||
// send to child process and print in terminal
|
|
||||||
process.stdout.write('\n');
|
|
||||||
inputText = '';
|
|
||||||
} else {
|
|
||||||
// record it and write into terminal
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
inputText += key;
|
|
||||||
process.stdout.write(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
||||||
this.error(`There was an error: ${error.message}`);
|
|
||||||
|
|
||||||
processExitCode = 1;
|
processExitCode = 1;
|
||||||
// @ts-ignore
|
process.emit('exit', processExitCode);
|
||||||
process.emit('SIGINT');
|
});
|
||||||
|
|
||||||
|
// Make sure the settings exist
|
||||||
|
const userSettings = await UserSettings.prepareUserSettings();
|
||||||
|
|
||||||
|
if (!config.getEnv('userManagement.jwtSecret')) {
|
||||||
|
// If we don't have a JWT secret set, generate
|
||||||
|
// one based and save to config.
|
||||||
|
const encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
|
|
||||||
|
// For a key off every other letter from encryption key
|
||||||
|
// CAREFUL: do not change this or it breaks all existing tokens.
|
||||||
|
let baseKey = '';
|
||||||
|
for (let i = 0; i < encryptionKey.length; i += 2) {
|
||||||
|
baseKey += encryptionKey[i];
|
||||||
|
}
|
||||||
|
config.set('userManagement.jwtSecret', createHash('sha256').update(baseKey).digest('hex'));
|
||||||
}
|
}
|
||||||
})();
|
|
||||||
|
if (!config.getEnv('endpoints.disableUi')) {
|
||||||
|
await Start.generateStaticAssets();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load all node and credential types
|
||||||
|
const loadNodesAndCredentials = LoadNodesAndCredentials();
|
||||||
|
await loadNodesAndCredentials.init();
|
||||||
|
|
||||||
|
// Load all external hooks
|
||||||
|
const externalHooks = ExternalHooks();
|
||||||
|
await externalHooks.init();
|
||||||
|
|
||||||
|
// Add the found types to an instance other parts of the application can use
|
||||||
|
const nodeTypes = NodeTypes(loadNodesAndCredentials);
|
||||||
|
const credentialTypes = CredentialTypes(loadNodesAndCredentials);
|
||||||
|
|
||||||
|
// Load the credentials overwrites if any exist
|
||||||
|
await CredentialsOverwrites(credentialTypes).init();
|
||||||
|
|
||||||
|
await loadNodesAndCredentials.generateTypesForFrontend();
|
||||||
|
|
||||||
|
// Wait till the database is ready
|
||||||
|
await startDbInitPromise;
|
||||||
|
|
||||||
|
const installedPackages = await getAllInstalledPackages();
|
||||||
|
const missingPackages = new Set<{
|
||||||
|
packageName: string;
|
||||||
|
version: string;
|
||||||
|
}>();
|
||||||
|
installedPackages.forEach((installedPackage) => {
|
||||||
|
installedPackage.installedNodes.forEach((installedNode) => {
|
||||||
|
if (!loadNodesAndCredentials.known.nodes[installedNode.type]) {
|
||||||
|
// Leave the list ready for installing in case we need.
|
||||||
|
missingPackages.add({
|
||||||
|
packageName: installedPackage.packageName,
|
||||||
|
version: installedPackage.installedVersion,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await UserSettings.getEncryptionKey();
|
||||||
|
|
||||||
|
// Load settings from database and set them to config.
|
||||||
|
const databaseSettings = await Db.collections.Settings.find({ loadOnStartup: true });
|
||||||
|
databaseSettings.forEach((setting) => {
|
||||||
|
config.set(setting.key, JSON.parse(setting.value));
|
||||||
|
});
|
||||||
|
|
||||||
|
config.set('nodes.packagesMissing', '');
|
||||||
|
if (missingPackages.size) {
|
||||||
|
LoggerProxy.error(
|
||||||
|
'n8n detected that some packages are missing. For more information, visit https://docs.n8n.io/integrations/community-nodes/troubleshooting/',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (flags.reinstallMissingPackages || process.env.N8N_REINSTALL_MISSING_PACKAGES) {
|
||||||
|
LoggerProxy.info('Attempting to reinstall missing packages', { missingPackages });
|
||||||
|
try {
|
||||||
|
// Optimistic approach - stop if any installation fails
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const missingPackage of missingPackages) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
void (await loadNodesAndCredentials.loadNpmModule(
|
||||||
|
missingPackage.packageName,
|
||||||
|
missingPackage.version,
|
||||||
|
));
|
||||||
|
missingPackages.delete(missingPackage);
|
||||||
|
}
|
||||||
|
LoggerProxy.info('Packages reinstalled successfully. Resuming regular initialization.');
|
||||||
|
} catch (error) {
|
||||||
|
LoggerProxy.error('n8n was unable to install the missing packages.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.set(
|
||||||
|
'nodes.packagesMissing',
|
||||||
|
Array.from(missingPackages)
|
||||||
|
.map((missingPackage) => `${missingPackage.packageName}@${missingPackage.version}`)
|
||||||
|
.join(' '),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbType = (await GenericHelpers.getConfigValue('database.type')) as DatabaseType;
|
||||||
|
|
||||||
|
if (dbType === 'sqlite') {
|
||||||
|
const shouldRunVacuum = config.getEnv('database.sqlite.executeVacuumOnStartup');
|
||||||
|
if (shouldRunVacuum) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
await Db.collections.Execution.query('VACUUM;');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags.tunnel) {
|
||||||
|
this.log('\nWaiting for tunnel ...');
|
||||||
|
|
||||||
|
let tunnelSubdomain;
|
||||||
|
if (
|
||||||
|
process.env[TUNNEL_SUBDOMAIN_ENV] !== undefined &&
|
||||||
|
process.env[TUNNEL_SUBDOMAIN_ENV] !== ''
|
||||||
|
) {
|
||||||
|
tunnelSubdomain = process.env[TUNNEL_SUBDOMAIN_ENV];
|
||||||
|
} else if (userSettings.tunnelSubdomain !== undefined) {
|
||||||
|
tunnelSubdomain = userSettings.tunnelSubdomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tunnelSubdomain === undefined) {
|
||||||
|
// When no tunnel subdomain did exist yet create a new random one
|
||||||
|
const availableCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
userSettings.tunnelSubdomain = Array.from({ length: 24 })
|
||||||
|
.map(() => {
|
||||||
|
return availableCharacters.charAt(
|
||||||
|
Math.floor(Math.random() * availableCharacters.length),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
await UserSettings.writeUserSettings(userSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tunnelSettings: localtunnel.TunnelConfig = {
|
||||||
|
host: 'https://hooks.n8n.cloud',
|
||||||
|
subdomain: tunnelSubdomain,
|
||||||
|
};
|
||||||
|
|
||||||
|
const port = config.getEnv('port');
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const webhookTunnel = await localtunnel(port, tunnelSettings);
|
||||||
|
|
||||||
|
process.env.WEBHOOK_URL = `${webhookTunnel.url}/`;
|
||||||
|
this.log(`Tunnel URL: ${process.env.WEBHOOK_URL}\n`);
|
||||||
|
this.log(
|
||||||
|
'IMPORTANT! Do not share with anybody as it would give people access to your n8n instance!',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceId = await UserSettings.getInstanceId();
|
||||||
|
await InternalHooksManager.init(instanceId, nodeTypes);
|
||||||
|
|
||||||
|
const binaryDataConfig = config.getEnv('binaryDataManager');
|
||||||
|
await BinaryDataManager.init(binaryDataConfig, true);
|
||||||
|
|
||||||
|
await Server.start();
|
||||||
|
|
||||||
|
// Start to get active workflows and run their triggers
|
||||||
|
activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
|
||||||
|
await activeWorkflowRunner.init();
|
||||||
|
|
||||||
|
WaitTracker();
|
||||||
|
|
||||||
|
const editorUrl = GenericHelpers.getBaseUrl();
|
||||||
|
this.log(`\nEditor is now accessible via:\n${editorUrl}`);
|
||||||
|
|
||||||
|
const saveManualExecutions = config.getEnv('executions.saveDataManualExecutions');
|
||||||
|
|
||||||
|
if (saveManualExecutions) {
|
||||||
|
this.log('\nManual executions will be visible only for the owner');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow to open n8n editor by pressing "o"
|
||||||
|
if (Boolean(process.stdout.isTTY) && process.stdin.setRawMode) {
|
||||||
|
process.stdin.setRawMode(true);
|
||||||
|
process.stdin.resume();
|
||||||
|
process.stdin.setEncoding('utf8');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
let inputText = '';
|
||||||
|
|
||||||
|
if (flags.open) {
|
||||||
|
Start.openBrowser();
|
||||||
|
}
|
||||||
|
this.log('\nPress "o" to open in Browser.');
|
||||||
|
process.stdin.on('data', (key: string) => {
|
||||||
|
if (key === 'o') {
|
||||||
|
Start.openBrowser();
|
||||||
|
inputText = '';
|
||||||
|
} else if (key.charCodeAt(0) === 3) {
|
||||||
|
// Ctrl + c got pressed
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
Start.stopProcess();
|
||||||
|
} else {
|
||||||
|
// When anything else got pressed, record it and send it on enter into the child process
|
||||||
|
// eslint-disable-next-line no-lonely-if
|
||||||
|
if (key.charCodeAt(0) === 13) {
|
||||||
|
// send to child process and print in terminal
|
||||||
|
process.stdout.write('\n');
|
||||||
|
inputText = '';
|
||||||
|
} else {
|
||||||
|
// record it and write into terminal
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
inputText += key;
|
||||||
|
process.stdout.write(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('There was an error', error);
|
||||||
|
processExitCode = 1;
|
||||||
|
process.emit('exit', processExitCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,24 +7,21 @@
|
||||||
import { BinaryDataManager, UserSettings } from 'n8n-core';
|
import { BinaryDataManager, UserSettings } from 'n8n-core';
|
||||||
import { Command, flags } from '@oclif/command';
|
import { Command, flags } from '@oclif/command';
|
||||||
|
|
||||||
import { IDataObject, LoggerProxy, sleep } from 'n8n-workflow';
|
import { LoggerProxy, sleep } from 'n8n-workflow';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import * as ActiveExecutions from '@/ActiveExecutions';
|
import * as ActiveExecutions from '@/ActiveExecutions';
|
||||||
import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner';
|
|
||||||
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
||||||
import { CredentialTypes } from '@/CredentialTypes';
|
import { CredentialTypes } from '@/CredentialTypes';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import { ExternalHooks } from '@/ExternalHooks';
|
import { ExternalHooks } from '@/ExternalHooks';
|
||||||
import * as GenericHelpers from '@/GenericHelpers';
|
|
||||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
import { NodeTypes } from '@/NodeTypes';
|
import { NodeTypes } from '@/NodeTypes';
|
||||||
import { InternalHooksManager } from '@/InternalHooksManager';
|
import { InternalHooksManager } from '@/InternalHooksManager';
|
||||||
import * as WebhookServer from '@/WebhookServer';
|
import { WebhookServer } from '@/WebhookServer';
|
||||||
import { getLogger } from '@/Logger';
|
import { getLogger } from '@/Logger';
|
||||||
import { initErrorHandling } from '@/ErrorReporting';
|
import { initErrorHandling } from '@/ErrorReporting';
|
||||||
import * as CrashJournal from '@/CrashJournal';
|
import * as CrashJournal from '@/CrashJournal';
|
||||||
|
|
||||||
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
|
|
||||||
let processExitCode = 0;
|
let processExitCode = 0;
|
||||||
|
|
||||||
export class Webhook extends Command {
|
export class Webhook extends Command {
|
||||||
|
@ -85,6 +82,22 @@ export class Webhook extends Command {
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
async run() {
|
async run() {
|
||||||
|
if (config.getEnv('executions.mode') !== 'queue') {
|
||||||
|
/**
|
||||||
|
* It is technically possible to run without queues but
|
||||||
|
* there are 2 known bugs when running in this mode:
|
||||||
|
* - Executions list will be problematic as the main process
|
||||||
|
* is not aware of current executions in the webhook processes
|
||||||
|
* and therefore will display all current executions as error
|
||||||
|
* as it is unable to determine if it is still running or crashed
|
||||||
|
* - You cannot stop currently executing jobs from webhook processes
|
||||||
|
* when running without queues as the main process cannot talk to
|
||||||
|
* the webhook processes to communicate workflow execution interruption.
|
||||||
|
*/
|
||||||
|
|
||||||
|
this.error('Webhook processes can only run with execution mode as queue.');
|
||||||
|
}
|
||||||
|
|
||||||
const logger = getLogger();
|
const logger = getLogger();
|
||||||
LoggerProxy.init(logger);
|
LoggerProxy.init(logger);
|
||||||
|
|
||||||
|
@ -95,154 +108,52 @@ export class Webhook extends Command {
|
||||||
await initErrorHandling();
|
await initErrorHandling();
|
||||||
await CrashJournal.init();
|
await CrashJournal.init();
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-shadow
|
try {
|
||||||
const { flags } = this.parse(Webhook);
|
// Start directly with the init of the database to improve startup time
|
||||||
|
const startDbInitPromise = Db.init().catch((error) => {
|
||||||
// Wrap that the process does not close but we can still use async
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
|
||||||
await (async () => {
|
logger.error(`There was an error initializing DB: "${error.message}"`);
|
||||||
if (config.getEnv('executions.mode') !== 'queue') {
|
|
||||||
/**
|
|
||||||
* It is technically possible to run without queues but
|
|
||||||
* there are 2 known bugs when running in this mode:
|
|
||||||
* - Executions list will be problematic as the main process
|
|
||||||
* is not aware of current executions in the webhook processes
|
|
||||||
* and therefore will display all current executions as error
|
|
||||||
* as it is unable to determine if it is still running or crashed
|
|
||||||
* - You cannot stop currently executing jobs from webhook processes
|
|
||||||
* when running without queues as the main process cannot talk to
|
|
||||||
* the webhook processes to communicate workflow execution interruption.
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.error('Webhook processes can only run with execution mode as queue.');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Start directly with the init of the database to improve startup time
|
|
||||||
const startDbInitPromise = Db.init().catch((error) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
|
|
||||||
logger.error(`There was an error initializing DB: "${error.message}"`);
|
|
||||||
|
|
||||||
processExitCode = 1;
|
|
||||||
// @ts-ignore
|
|
||||||
process.emit('SIGINT');
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Make sure the settings exist
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
await UserSettings.prepareUserSettings();
|
|
||||||
|
|
||||||
// Load all node and credential types
|
|
||||||
const loadNodesAndCredentials = LoadNodesAndCredentials();
|
|
||||||
await loadNodesAndCredentials.init();
|
|
||||||
|
|
||||||
// Add the found types to an instance other parts of the application can use
|
|
||||||
const nodeTypes = NodeTypes(loadNodesAndCredentials);
|
|
||||||
const credentialTypes = CredentialTypes(loadNodesAndCredentials);
|
|
||||||
|
|
||||||
// Load the credentials overwrites if any exist
|
|
||||||
await CredentialsOverwrites(credentialTypes).init();
|
|
||||||
|
|
||||||
// Load all external hooks
|
|
||||||
const externalHooks = ExternalHooks();
|
|
||||||
await externalHooks.init();
|
|
||||||
|
|
||||||
// Wait till the database is ready
|
|
||||||
await startDbInitPromise;
|
|
||||||
|
|
||||||
const instanceId = await UserSettings.getInstanceId();
|
|
||||||
const { cli } = await GenericHelpers.getVersions();
|
|
||||||
await InternalHooksManager.init(instanceId, cli, nodeTypes);
|
|
||||||
|
|
||||||
const binaryDataConfig = config.getEnv('binaryDataManager');
|
|
||||||
await BinaryDataManager.init(binaryDataConfig);
|
|
||||||
|
|
||||||
if (config.getEnv('executions.mode') === 'queue') {
|
|
||||||
const redisHost = config.getEnv('queue.bull.redis.host');
|
|
||||||
const redisUsername = config.getEnv('queue.bull.redis.username');
|
|
||||||
const redisPassword = config.getEnv('queue.bull.redis.password');
|
|
||||||
const redisPort = config.getEnv('queue.bull.redis.port');
|
|
||||||
const redisDB = config.getEnv('queue.bull.redis.db');
|
|
||||||
const redisConnectionTimeoutLimit = config.getEnv('queue.bull.redis.timeoutThreshold');
|
|
||||||
let lastTimer = 0;
|
|
||||||
let cumulativeTimeout = 0;
|
|
||||||
|
|
||||||
const settings = {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
retryStrategy: (times: number): number | null => {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - lastTimer > 30000) {
|
|
||||||
// Means we had no timeout at all or last timeout was temporary and we recovered
|
|
||||||
lastTimer = now;
|
|
||||||
cumulativeTimeout = 0;
|
|
||||||
} else {
|
|
||||||
cumulativeTimeout += now - lastTimer;
|
|
||||||
lastTimer = now;
|
|
||||||
if (cumulativeTimeout > redisConnectionTimeoutLimit) {
|
|
||||||
logger.error(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
||||||
`Unable to connect to Redis after ${redisConnectionTimeoutLimit}. Exiting process.`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 500;
|
|
||||||
},
|
|
||||||
} as IDataObject;
|
|
||||||
|
|
||||||
if (redisHost) {
|
|
||||||
settings.host = redisHost;
|
|
||||||
}
|
|
||||||
if (redisUsername) {
|
|
||||||
settings.username = redisUsername;
|
|
||||||
}
|
|
||||||
if (redisPassword) {
|
|
||||||
settings.password = redisPassword;
|
|
||||||
}
|
|
||||||
if (redisPort) {
|
|
||||||
settings.port = redisPort;
|
|
||||||
}
|
|
||||||
if (redisDB) {
|
|
||||||
settings.db = redisDB;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
const { default: Redis } = await import('ioredis');
|
|
||||||
|
|
||||||
// This connection is going to be our heartbeat
|
|
||||||
// IORedis automatically pings redis and tries to reconnect
|
|
||||||
// We will be using the retryStrategy above
|
|
||||||
// to control how and when to exit.
|
|
||||||
const redis = new Redis(settings);
|
|
||||||
|
|
||||||
redis.on('error', (error) => {
|
|
||||||
if (error.toString().includes('ECONNREFUSED') === true) {
|
|
||||||
logger.warn('Redis unavailable - trying to reconnect...');
|
|
||||||
} else {
|
|
||||||
logger.warn('Error with Redis: ', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await WebhookServer.start();
|
|
||||||
|
|
||||||
// Start to get active workflows and run their triggers
|
|
||||||
activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
|
|
||||||
await activeWorkflowRunner.initWebhooks();
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const editorUrl = GenericHelpers.getBaseUrl();
|
|
||||||
console.info('Webhook listener waiting for requests.');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Exiting due to error. See log message for details.');
|
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
||||||
logger.error(`Webhook process cannot continue. "${error.message}"`);
|
|
||||||
|
|
||||||
processExitCode = 1;
|
processExitCode = 1;
|
||||||
// @ts-ignore
|
process.emit('exit', processExitCode);
|
||||||
process.emit('SIGINT');
|
});
|
||||||
process.exit(1);
|
|
||||||
}
|
// Make sure the settings exist
|
||||||
})();
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
await UserSettings.prepareUserSettings();
|
||||||
|
|
||||||
|
// Load all node and credential types
|
||||||
|
const loadNodesAndCredentials = LoadNodesAndCredentials();
|
||||||
|
await loadNodesAndCredentials.init();
|
||||||
|
|
||||||
|
// Add the found types to an instance other parts of the application can use
|
||||||
|
const nodeTypes = NodeTypes(loadNodesAndCredentials);
|
||||||
|
const credentialTypes = CredentialTypes(loadNodesAndCredentials);
|
||||||
|
|
||||||
|
// Load the credentials overwrites if any exist
|
||||||
|
await CredentialsOverwrites(credentialTypes).init();
|
||||||
|
|
||||||
|
// Load all external hooks
|
||||||
|
const externalHooks = ExternalHooks();
|
||||||
|
await externalHooks.init();
|
||||||
|
|
||||||
|
// Wait till the database is ready
|
||||||
|
await startDbInitPromise;
|
||||||
|
|
||||||
|
const instanceId = await UserSettings.getInstanceId();
|
||||||
|
await InternalHooksManager.init(instanceId, nodeTypes);
|
||||||
|
|
||||||
|
const binaryDataConfig = config.getEnv('binaryDataManager');
|
||||||
|
await BinaryDataManager.init(binaryDataConfig);
|
||||||
|
|
||||||
|
const server = new WebhookServer();
|
||||||
|
await server.start();
|
||||||
|
|
||||||
|
console.info('Webhook listener waiting for requests.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Exiting due to error.', error);
|
||||||
|
processExitCode = 1;
|
||||||
|
process.emit('exit', processExitCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ import config from '@/config';
|
||||||
import * as Queue from '@/Queue';
|
import * as Queue from '@/Queue';
|
||||||
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
|
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
|
||||||
import { generateFailedExecutionFromError } from '@/WorkflowHelpers';
|
import { generateFailedExecutionFromError } from '@/WorkflowHelpers';
|
||||||
|
import { N8N_VERSION } from '@/constants';
|
||||||
|
|
||||||
export class Worker extends Command {
|
export class Worker extends Command {
|
||||||
static description = '\nStarts a n8n worker';
|
static description = '\nStarts a n8n worker';
|
||||||
|
@ -304,16 +305,15 @@ export class Worker extends Command {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
Worker.jobQueue.process(flags.concurrency, async (job) => this.runJob(job, nodeTypes));
|
Worker.jobQueue.process(flags.concurrency, async (job) => this.runJob(job, nodeTypes));
|
||||||
|
|
||||||
const versions = await GenericHelpers.getVersions();
|
|
||||||
const instanceId = await UserSettings.getInstanceId();
|
const instanceId = await UserSettings.getInstanceId();
|
||||||
|
|
||||||
await InternalHooksManager.init(instanceId, versions.cli, nodeTypes);
|
await InternalHooksManager.init(instanceId, nodeTypes);
|
||||||
|
|
||||||
const binaryDataConfig = config.getEnv('binaryDataManager');
|
const binaryDataConfig = config.getEnv('binaryDataManager');
|
||||||
await BinaryDataManager.init(binaryDataConfig);
|
await BinaryDataManager.init(binaryDataConfig);
|
||||||
|
|
||||||
console.info('\nn8n worker is now ready');
|
console.info('\nn8n worker is now ready');
|
||||||
console.info(` * Version: ${versions.cli}`);
|
console.info(` * Version: ${N8N_VERSION}`);
|
||||||
console.info(` * Concurrency: ${flags.concurrency}`);
|
console.info(` * Concurrency: ${flags.concurrency}`);
|
||||||
console.info('');
|
console.info('');
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
import { resolve, join, dirname } from 'path';
|
import { resolve, join, dirname } from 'path';
|
||||||
import { RESPONSE_ERROR_MESSAGES as CORE_RESPONSE_ERROR_MESSAGES, UserSettings } from 'n8n-core';
|
import {
|
||||||
|
n8n,
|
||||||
|
RESPONSE_ERROR_MESSAGES as CORE_RESPONSE_ERROR_MESSAGES,
|
||||||
|
UserSettings,
|
||||||
|
} from 'n8n-core';
|
||||||
|
import { jsonParse } from 'n8n-workflow';
|
||||||
|
|
||||||
const { NODE_ENV, E2E_TESTS } = process.env;
|
const { NODE_ENV, E2E_TESTS } = process.env;
|
||||||
export const inProduction = NODE_ENV === 'production';
|
export const inProduction = NODE_ENV === 'production';
|
||||||
|
@ -16,6 +22,10 @@ export const NODES_BASE_DIR = join(CLI_DIR, '..', 'nodes-base');
|
||||||
export const GENERATED_STATIC_DIR = join(UserSettings.getUserHome(), '.cache/n8n/public');
|
export const GENERATED_STATIC_DIR = join(UserSettings.getUserHome(), '.cache/n8n/public');
|
||||||
export const EDITOR_UI_DIST_DIR = join(dirname(require.resolve('n8n-editor-ui')), 'dist');
|
export const EDITOR_UI_DIST_DIR = join(dirname(require.resolve('n8n-editor-ui')), 'dist');
|
||||||
|
|
||||||
|
export const N8N_VERSION = jsonParse<n8n.PackageJson>(
|
||||||
|
readFileSync(join(CLI_DIR, 'package.json'), 'utf8'),
|
||||||
|
).version;
|
||||||
|
|
||||||
export const NODE_PACKAGE_PREFIX = 'n8n-nodes-';
|
export const NODE_PACKAGE_PREFIX = 'n8n-nodes-';
|
||||||
|
|
||||||
export const STARTER_TEMPLATE_NAME = `${NODE_PACKAGE_PREFIX}starter`;
|
export const STARTER_TEMPLATE_NAME = `${NODE_PACKAGE_PREFIX}starter`;
|
||||||
|
|
|
@ -5,15 +5,14 @@ import { MessageEventBusDestination } from './MessageEventBusDestination.ee';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { eventBus } from '../MessageEventBus/MessageEventBus';
|
import { eventBus } from '../MessageEventBus/MessageEventBus';
|
||||||
import {
|
import {
|
||||||
LoggerProxy,
|
|
||||||
MessageEventBusDestinationOptions,
|
MessageEventBusDestinationOptions,
|
||||||
MessageEventBusDestinationSentryOptions,
|
MessageEventBusDestinationSentryOptions,
|
||||||
MessageEventBusDestinationTypeNames,
|
MessageEventBusDestinationTypeNames,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { GenericHelpers } from '../..';
|
|
||||||
import { isLogStreamingEnabled } from '../MessageEventBus/MessageEventBusHelper';
|
import { isLogStreamingEnabled } from '../MessageEventBus/MessageEventBusHelper';
|
||||||
import { EventMessageTypes } from '../EventMessageClasses';
|
import { EventMessageTypes } from '../EventMessageClasses';
|
||||||
import { eventMessageGenericDestinationTestEvent } from '../EventMessageClasses/EventMessageGeneric';
|
import { eventMessageGenericDestinationTestEvent } from '../EventMessageClasses/EventMessageGeneric';
|
||||||
|
import { N8N_VERSION } from '@/constants';
|
||||||
|
|
||||||
export const isMessageEventBusDestinationSentryOptions = (
|
export const isMessageEventBusDestinationSentryOptions = (
|
||||||
candidate: unknown,
|
candidate: unknown,
|
||||||
|
@ -45,22 +44,15 @@ export class MessageEventBusDestinationSentry
|
||||||
if (options.tracesSampleRate) this.tracesSampleRate = options.tracesSampleRate;
|
if (options.tracesSampleRate) this.tracesSampleRate = options.tracesSampleRate;
|
||||||
const { ENVIRONMENT: environment } = process.env;
|
const { ENVIRONMENT: environment } = process.env;
|
||||||
|
|
||||||
GenericHelpers.getVersions()
|
this.sentryClient = new Sentry.NodeClient({
|
||||||
.then((versions) => {
|
dsn: this.dsn,
|
||||||
this.sentryClient = new Sentry.NodeClient({
|
tracesSampleRate: this.tracesSampleRate,
|
||||||
dsn: this.dsn,
|
environment,
|
||||||
tracesSampleRate: this.tracesSampleRate,
|
release: N8N_VERSION,
|
||||||
environment,
|
transport: Sentry.makeNodeTransport,
|
||||||
release: versions.cli,
|
integrations: Sentry.defaultIntegrations,
|
||||||
transport: Sentry.makeNodeTransport,
|
stackParser: Sentry.defaultStackParser,
|
||||||
integrations: Sentry.defaultIntegrations,
|
});
|
||||||
stackParser: Sentry.defaultStackParser,
|
|
||||||
});
|
|
||||||
LoggerProxy.debug(`MessageEventBusDestinationSentry with id ${this.getId()} initialized`);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async receiveFromEventBus(msg: EventMessageTypes): Promise<boolean> {
|
async receiveFromEventBus(msg: EventMessageTypes): Promise<boolean> {
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { inDevelopment } from '@/constants';
|
|
||||||
import type { RequestHandler } from 'express';
|
import type { RequestHandler } from 'express';
|
||||||
|
|
||||||
export const corsMiddleware: RequestHandler = (req, res, next) => {
|
export const corsMiddleware: RequestHandler = (req, res, next) => {
|
||||||
if (inDevelopment && 'origin' in req.headers) {
|
if ('origin' in req.headers) {
|
||||||
// Allow access also from frontend when developing
|
// Allow access also from frontend when developing
|
||||||
res.header('Access-Control-Allow-Origin', req.headers.origin);
|
res.header('Access-Control-Allow-Origin', req.headers.origin);
|
||||||
res.header('Access-Control-Allow-Credentials', 'true');
|
res.header('Access-Control-Allow-Credentials', 'true');
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { IExecutionTrackProperties } from '@/Interfaces';
|
||||||
import { getLogger } from '@/Logger';
|
import { getLogger } from '@/Logger';
|
||||||
import { getLicense } from '@/License';
|
import { getLicense } from '@/License';
|
||||||
import { LicenseService } from '@/license/License.service';
|
import { LicenseService } from '@/license/License.service';
|
||||||
|
import { N8N_VERSION } from '@/constants';
|
||||||
|
|
||||||
type ExecutionTrackDataKey = 'manual_error' | 'manual_success' | 'prod_error' | 'prod_success';
|
type ExecutionTrackDataKey = 'manual_error' | 'manual_success' | 'prod_error' | 'prod_success';
|
||||||
|
|
||||||
|
@ -35,7 +36,7 @@ export class Telemetry {
|
||||||
|
|
||||||
private executionCountsBuffer: IExecutionsBuffer = {};
|
private executionCountsBuffer: IExecutionsBuffer = {};
|
||||||
|
|
||||||
constructor(private instanceId: string, private versionCli: string) {}
|
constructor(private instanceId: string) {}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
const enabled = config.getEnv('diagnostics.enabled');
|
const enabled = config.getEnv('diagnostics.enabled');
|
||||||
|
@ -179,7 +180,7 @@ export class Telemetry {
|
||||||
const updatedProperties: ITelemetryTrackProperties = {
|
const updatedProperties: ITelemetryTrackProperties = {
|
||||||
...properties,
|
...properties,
|
||||||
instance_id: this.instanceId,
|
instance_id: this.instanceId,
|
||||||
version_cli: this.versionCli,
|
version_cli: N8N_VERSION,
|
||||||
};
|
};
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|
|
@ -6,13 +6,11 @@ import * as testDb from './shared/testDb';
|
||||||
import type { AuthAgent } from './shared/types';
|
import type { AuthAgent } from './shared/types';
|
||||||
import * as utils from './shared/utils';
|
import * as utils from './shared/utils';
|
||||||
import { ILicensePostResponse, ILicenseReadResponse } from '@/Interfaces';
|
import { ILicensePostResponse, ILicenseReadResponse } from '@/Interfaces';
|
||||||
import { LicenseManager } from '@n8n_io/license-sdk';
|
|
||||||
import { License } from '@/License';
|
import { License } from '@/License';
|
||||||
|
|
||||||
const MOCK_SERVER_URL = 'https://server.com/v1';
|
const MOCK_SERVER_URL = 'https://server.com/v1';
|
||||||
const MOCK_RENEW_OFFSET = 259200;
|
const MOCK_RENEW_OFFSET = 259200;
|
||||||
const MOCK_INSTANCE_ID = 'instance-id';
|
const MOCK_INSTANCE_ID = 'instance-id';
|
||||||
const MOCK_N8N_VERSION = '0.27.0';
|
|
||||||
|
|
||||||
let app: express.Application;
|
let app: express.Application;
|
||||||
let testDbName = '';
|
let testDbName = '';
|
||||||
|
@ -41,7 +39,7 @@ beforeAll(async () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
license = new License();
|
license = new License();
|
||||||
await license.init(MOCK_INSTANCE_ID, MOCK_N8N_VERSION);
|
await license.init(MOCK_INSTANCE_ID);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { LicenseManager } from '@n8n_io/license-sdk';
|
import { LicenseManager } from '@n8n_io/license-sdk';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { License } from '@/License';
|
import { License } from '@/License';
|
||||||
|
import { N8N_VERSION } from '@/constants';
|
||||||
|
|
||||||
jest.mock('@n8n_io/license-sdk');
|
jest.mock('@n8n_io/license-sdk');
|
||||||
|
|
||||||
const MOCK_SERVER_URL = 'https://server.com/v1';
|
const MOCK_SERVER_URL = 'https://server.com/v1';
|
||||||
const MOCK_RENEW_OFFSET = 259200;
|
const MOCK_RENEW_OFFSET = 259200;
|
||||||
const MOCK_INSTANCE_ID = 'instance-id';
|
const MOCK_INSTANCE_ID = 'instance-id';
|
||||||
const MOCK_N8N_VERSION = '0.27.0';
|
|
||||||
const MOCK_ACTIVATION_KEY = 'activation-key';
|
const MOCK_ACTIVATION_KEY = 'activation-key';
|
||||||
const MOCK_FEATURE_FLAG = 'feat:mock';
|
const MOCK_FEATURE_FLAG = 'feat:mock';
|
||||||
const MOCK_MAIN_PLAN_ID = 1234;
|
const MOCK_MAIN_PLAN_ID = 1234;
|
||||||
|
@ -23,7 +23,7 @@ describe('License', () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
license = new License();
|
license = new License();
|
||||||
await license.init(MOCK_INSTANCE_ID, MOCK_N8N_VERSION);
|
await license.init(MOCK_INSTANCE_ID);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initializes license manager', async () => {
|
test('initializes license manager', async () => {
|
||||||
|
@ -31,7 +31,7 @@ describe('License', () => {
|
||||||
autoRenewEnabled: true,
|
autoRenewEnabled: true,
|
||||||
autoRenewOffset: MOCK_RENEW_OFFSET,
|
autoRenewOffset: MOCK_RENEW_OFFSET,
|
||||||
deviceFingerprint: expect.any(Function),
|
deviceFingerprint: expect.any(Function),
|
||||||
productIdentifier: `n8n-${MOCK_N8N_VERSION}`,
|
productIdentifier: `n8n-${N8N_VERSION}`,
|
||||||
logger: expect.anything(),
|
logger: expect.anything(),
|
||||||
loadCertStr: expect.any(Function),
|
loadCertStr: expect.any(Function),
|
||||||
saveCertStr: expect.any(Function),
|
saveCertStr: expect.any(Function),
|
||||||
|
|
|
@ -103,6 +103,7 @@ importers:
|
||||||
'@sentry/node': ^7.28.1
|
'@sentry/node': ^7.28.1
|
||||||
'@types/basic-auth': ^1.1.2
|
'@types/basic-auth': ^1.1.2
|
||||||
'@types/bcryptjs': ^2.4.2
|
'@types/bcryptjs': ^2.4.2
|
||||||
|
'@types/body-parser-xml': ^2.0.2
|
||||||
'@types/compression': 1.0.1
|
'@types/compression': 1.0.1
|
||||||
'@types/connect-history-api-fallback': ^1.3.1
|
'@types/connect-history-api-fallback': ^1.3.1
|
||||||
'@types/convict': ^4.2.1
|
'@types/convict': ^4.2.1
|
||||||
|
@ -312,6 +313,7 @@ importers:
|
||||||
'@oclif/dev-cli': 1.26.10
|
'@oclif/dev-cli': 1.26.10
|
||||||
'@types/basic-auth': 1.1.3
|
'@types/basic-auth': 1.1.3
|
||||||
'@types/bcryptjs': 2.4.2
|
'@types/bcryptjs': 2.4.2
|
||||||
|
'@types/body-parser-xml': 2.0.2
|
||||||
'@types/compression': 1.0.1
|
'@types/compression': 1.0.1
|
||||||
'@types/connect-history-api-fallback': 1.3.5
|
'@types/connect-history-api-fallback': 1.3.5
|
||||||
'@types/convict': 4.2.1
|
'@types/convict': 4.2.1
|
||||||
|
@ -5550,6 +5552,16 @@ packages:
|
||||||
resolution: {integrity: sha512-g2qEd+zkfkTEudA2SrMAeAvY7CrFqtbsLILm2dT2VIeKTqMqVzcdfURlvu6FU3srRgbmXN1Srm94pg34EIehww==}
|
resolution: {integrity: sha512-g2qEd+zkfkTEudA2SrMAeAvY7CrFqtbsLILm2dT2VIeKTqMqVzcdfURlvu6FU3srRgbmXN1Srm94pg34EIehww==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/body-parser-xml/2.0.2:
|
||||||
|
resolution: {integrity: sha512-LlmFkP3BTfacofFevInpM8iZ6+hALZ9URUt5JpSw76irhHCdbqbcBtbxbu2MO8HUGoIROQ5wuB55rLS99xNgCg==}
|
||||||
|
dependencies:
|
||||||
|
'@types/body-parser': 1.19.2
|
||||||
|
'@types/connect': 3.4.35
|
||||||
|
'@types/express-serve-static-core': 4.17.31
|
||||||
|
'@types/node': 16.11.65
|
||||||
|
'@types/xml2js': 0.4.11
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/body-parser/1.19.2:
|
/@types/body-parser/1.19.2:
|
||||||
resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==}
|
resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
Loading…
Reference in a new issue