2023-02-21 10:21:56 -08:00
|
|
|
import { Container } from 'typedi';
|
2023-01-04 02:38:48 -08:00
|
|
|
import { readFile } from 'fs/promises';
|
|
|
|
import type { Server } from 'http';
|
|
|
|
import express from 'express';
|
|
|
|
import compression from 'compression';
|
|
|
|
import config from '@/config';
|
2023-08-01 08:32:30 -07:00
|
|
|
import { N8N_VERSION, inDevelopment, inTest } from '@/constants';
|
2023-02-21 10:21:56 -08:00
|
|
|
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
2023-01-04 02:38:48 -08:00
|
|
|
import * as Db from '@/Db';
|
|
|
|
import type { IExternalHooksClass } from '@/Interfaces';
|
|
|
|
import { ExternalHooks } from '@/ExternalHooks';
|
2023-08-01 08:32:30 -07:00
|
|
|
import { send, sendErrorResponse, ServiceUnavailableError } from '@/ResponseHelper';
|
|
|
|
import { rawBody, jsonParser, corsMiddleware } from '@/middlewares';
|
2023-02-21 10:21:56 -08:00
|
|
|
import { TestWebhooks } from '@/TestWebhooks';
|
2023-01-04 02:38:48 -08:00
|
|
|
import { WaitingWebhooks } from '@/WaitingWebhooks';
|
2023-08-01 08:32:30 -07:00
|
|
|
import { webhookRequestHandler } from '@/WebhookHelpers';
|
2023-08-02 03:51:25 -07:00
|
|
|
import { RedisService } from '@/services/redis.service';
|
|
|
|
import { jsonParse } from 'n8n-workflow';
|
|
|
|
import { eventBus } from './eventbus';
|
|
|
|
import type { AbstractEventMessageOptions } from './eventbus/EventMessageClasses/AbstractEventMessageOptions';
|
|
|
|
import { getEventMessageObjectByType } from './eventbus/EventMessageClasses/Helpers';
|
|
|
|
import type { RedisServiceWorkerResponseObject } from './services/redis/RedisServiceCommands';
|
|
|
|
import {
|
|
|
|
EVENT_BUS_REDIS_CHANNEL,
|
|
|
|
WORKER_RESPONSE_REDIS_CHANNEL,
|
|
|
|
} from './services/redis/RedisServiceHelper';
|
2023-01-04 02:38:48 -08:00
|
|
|
|
|
|
|
export abstract class AbstractServer {
|
2023-02-10 06:02:47 -08:00
|
|
|
protected server: Server;
|
|
|
|
|
2023-08-01 08:32:30 -07:00
|
|
|
readonly app: express.Application;
|
2023-01-04 02:38:48 -08:00
|
|
|
|
|
|
|
protected externalHooks: IExternalHooksClass;
|
|
|
|
|
2023-02-21 10:21:56 -08:00
|
|
|
protected activeWorkflowRunner: ActiveWorkflowRunner;
|
2023-01-04 02:38:48 -08:00
|
|
|
|
|
|
|
protected protocol: string;
|
|
|
|
|
|
|
|
protected sslKey: string;
|
|
|
|
|
|
|
|
protected sslCert: string;
|
|
|
|
|
|
|
|
protected timezone: string;
|
|
|
|
|
|
|
|
protected restEndpoint: string;
|
|
|
|
|
|
|
|
protected endpointWebhook: string;
|
|
|
|
|
|
|
|
protected endpointWebhookTest: string;
|
|
|
|
|
|
|
|
protected endpointWebhookWaiting: string;
|
|
|
|
|
2023-04-24 06:12:00 -07:00
|
|
|
protected instanceId = '';
|
|
|
|
|
2023-08-01 08:32:30 -07:00
|
|
|
protected webhooksEnabled = true;
|
|
|
|
|
|
|
|
protected testWebhooksEnabled = false;
|
2023-01-04 02:38:48 -08:00
|
|
|
|
|
|
|
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');
|
|
|
|
}
|
|
|
|
|
2023-08-01 08:32:30 -07:00
|
|
|
async configure(): Promise<void> {
|
|
|
|
// Additional configuration in derived classes
|
|
|
|
}
|
|
|
|
|
2023-02-10 06:02:47 -08:00
|
|
|
private async setupErrorHandlers() {
|
2023-01-04 02:38:48 -08:00
|
|
|
const { app } = this;
|
|
|
|
|
|
|
|
// Augment errors sent to Sentry
|
|
|
|
const {
|
|
|
|
Handlers: { requestHandler, errorHandler },
|
|
|
|
} = await import('@sentry/node');
|
|
|
|
app.use(requestHandler());
|
|
|
|
app.use(errorHandler());
|
2023-02-10 06:02:47 -08:00
|
|
|
}
|
|
|
|
|
2023-08-01 08:32:30 -07:00
|
|
|
private setupCommonMiddlewares() {
|
2023-01-04 02:38:48 -08:00
|
|
|
// Compress the response data
|
2023-08-01 08:32:30 -07:00
|
|
|
this.app.use(compression());
|
2023-01-04 02:38:48 -08:00
|
|
|
|
2023-08-01 08:32:30 -07:00
|
|
|
// Read incoming data into `rawBody`
|
|
|
|
this.app.use(rawBody);
|
2023-01-04 02:38:48 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
private setupDevMiddlewares() {
|
|
|
|
this.app.use(corsMiddleware);
|
|
|
|
}
|
|
|
|
|
2023-02-10 06:02:47 -08:00
|
|
|
protected setupPushServer() {}
|
|
|
|
|
2023-01-04 02:38:48 -08:00
|
|
|
private async setupHealthCheck() {
|
2023-05-10 01:27:04 -07:00
|
|
|
// health check should not care about DB connections
|
2023-01-04 02:38:48 -08:00
|
|
|
this.app.get('/healthz', async (req, res) => {
|
2023-05-10 01:27:04 -07:00
|
|
|
res.send({ status: 'ok' });
|
|
|
|
});
|
2023-01-04 02:38:48 -08:00
|
|
|
|
2023-05-10 01:27:04 -07:00
|
|
|
const { connectionState } = Db;
|
|
|
|
this.app.use((req, res, next) => {
|
|
|
|
if (connectionState.connected) {
|
|
|
|
if (connectionState.migrated) next();
|
|
|
|
else res.send('n8n is starting up. Please wait');
|
|
|
|
} else sendErrorResponse(res, new ServiceUnavailableError('Database is not ready!'));
|
2023-01-04 02:38:48 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
if (config.getEnv('executions.mode') === 'queue') {
|
2023-08-02 03:51:25 -07:00
|
|
|
await this.setupRedis();
|
2023-01-04 02:38:48 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2023-08-02 03:51:25 -07:00
|
|
|
// We are also subscribing to the event log channel to receive events from workers
|
|
|
|
private async setupRedis() {
|
|
|
|
const redisService = Container.get(RedisService);
|
|
|
|
const redisSubscriber = await redisService.getPubSubSubscriber();
|
|
|
|
|
|
|
|
// TODO: these are all proof of concept implementations for the moment
|
|
|
|
// until worker communication is implemented
|
|
|
|
// #region proof of concept
|
|
|
|
await redisSubscriber.subscribeToEventLog();
|
|
|
|
await redisSubscriber.subscribeToWorkerResponseChannel();
|
|
|
|
redisSubscriber.addMessageHandler(
|
|
|
|
'AbstractServerReceiver',
|
|
|
|
async (channel: string, message: string) => {
|
|
|
|
// TODO: this is a proof of concept implementation to forward events to the main instance's event bus
|
|
|
|
// Events are arriving through a pub/sub channel and are forwarded to the eventBus
|
|
|
|
// In the future, a stream should probably replace this implementation entirely
|
|
|
|
if (channel === EVENT_BUS_REDIS_CHANNEL) {
|
|
|
|
const eventData = jsonParse<AbstractEventMessageOptions>(message);
|
|
|
|
if (eventData) {
|
|
|
|
const eventMessage = getEventMessageObjectByType(eventData);
|
|
|
|
if (eventMessage) {
|
|
|
|
await eventBus.send(eventMessage);
|
2023-07-21 14:31:52 -07:00
|
|
|
}
|
2023-08-02 03:51:25 -07:00
|
|
|
}
|
|
|
|
} else if (channel === WORKER_RESPONSE_REDIS_CHANNEL) {
|
|
|
|
// The back channel from the workers as a pub/sub channel
|
|
|
|
const workerResponse = jsonParse<RedisServiceWorkerResponseObject>(message);
|
|
|
|
if (workerResponse) {
|
|
|
|
// TODO: Handle worker response
|
|
|
|
console.log('Received worker response', workerResponse);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
// TODO: Leave comments for now as implementation example
|
|
|
|
// const redisStreamListener = await redisService.getStreamConsumer();
|
|
|
|
// void redisStreamListener.listenToStream('teststream');
|
|
|
|
// redisStreamListener.addMessageHandler(
|
|
|
|
// 'MessageLogger',
|
|
|
|
// async (stream: string, id: string, message: string[]) => {
|
|
|
|
// // TODO: this is a proof of concept implementation of a stream consumer
|
|
|
|
// switch (stream) {
|
|
|
|
// case EVENT_BUS_REDIS_STREAM:
|
|
|
|
// case COMMAND_REDIS_STREAM:
|
|
|
|
// case WORKER_RESPONSE_REDIS_STREAM:
|
|
|
|
// default:
|
|
|
|
// LoggerProxy.debug(
|
|
|
|
// `Received message from stream ${stream} with id ${id} and message ${message.join(
|
|
|
|
// ',',
|
|
|
|
// )}`,
|
|
|
|
// );
|
|
|
|
// break;
|
|
|
|
// }
|
|
|
|
// },
|
|
|
|
// );
|
|
|
|
|
|
|
|
// const redisListReceiver = await redisService.getListReceiver();
|
|
|
|
// await redisListReceiver.init();
|
|
|
|
|
|
|
|
// setInterval(async () => {
|
|
|
|
// await redisListReceiver.popLatestWorkerResponse();
|
|
|
|
// }, 1000);
|
|
|
|
// #endregion
|
2023-01-04 02:38:48 -08:00
|
|
|
}
|
|
|
|
|
2023-05-10 01:27:04 -07:00
|
|
|
async init(): Promise<void> {
|
|
|
|
const { app, protocol, sslKey, sslCert } = this;
|
2023-01-04 02:38:48 -08:00
|
|
|
|
|
|
|
if (protocol === 'https' && sslKey && sslCert) {
|
|
|
|
const https = await import('https');
|
2023-02-10 06:02:47 -08:00
|
|
|
this.server = https.createServer(
|
2023-01-04 02:38:48 -08:00
|
|
|
{
|
|
|
|
key: await readFile(this.sslKey, 'utf8'),
|
|
|
|
cert: await readFile(this.sslCert, 'utf8'),
|
|
|
|
},
|
|
|
|
app,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
const http = await import('http');
|
2023-02-10 06:02:47 -08:00
|
|
|
this.server = http.createServer(app);
|
2023-01-04 02:38:48 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
const PORT = config.getEnv('port');
|
|
|
|
const ADDRESS = config.getEnv('listen_address');
|
|
|
|
|
2023-02-10 06:02:47 -08:00
|
|
|
this.server.on('error', (error: Error & { code: string }) => {
|
2023-01-04 02:38:48 -08:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-02-10 06:02:47 -08:00
|
|
|
await new Promise<void>((resolve) => this.server.listen(PORT, ADDRESS, () => resolve()));
|
2023-01-04 02:38:48 -08:00
|
|
|
|
2023-07-25 09:17:34 -07:00
|
|
|
this.externalHooks = Container.get(ExternalHooks);
|
|
|
|
this.activeWorkflowRunner = Container.get(ActiveWorkflowRunner);
|
|
|
|
|
2023-05-10 01:27:04 -07:00
|
|
|
await this.setupHealthCheck();
|
|
|
|
|
|
|
|
console.log(`n8n ready on ${ADDRESS}, port ${PORT}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
async start(): Promise<void> {
|
2023-08-01 08:32:30 -07:00
|
|
|
if (!inTest) {
|
|
|
|
await this.setupErrorHandlers();
|
|
|
|
this.setupPushServer();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setupCommonMiddlewares();
|
|
|
|
|
|
|
|
// Setup webhook handlers before bodyParser, to let the Webhook node handle binary data in requests
|
|
|
|
if (this.webhooksEnabled) {
|
|
|
|
// Register a handler for active webhooks
|
|
|
|
this.app.all(
|
|
|
|
`/${this.endpointWebhook}/:path(*)`,
|
|
|
|
webhookRequestHandler(Container.get(ActiveWorkflowRunner)),
|
|
|
|
);
|
|
|
|
|
|
|
|
// Register a handler for waiting webhooks
|
|
|
|
this.app.all(
|
|
|
|
`/${this.endpointWebhookWaiting}/:path/:suffix?`,
|
|
|
|
webhookRequestHandler(Container.get(WaitingWebhooks)),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.testWebhooksEnabled) {
|
|
|
|
const testWebhooks = Container.get(TestWebhooks);
|
|
|
|
|
|
|
|
// Register a handler for test webhooks
|
|
|
|
this.app.all(`/${this.endpointWebhookTest}/:path(*)`, webhookRequestHandler(testWebhooks));
|
|
|
|
|
|
|
|
// 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)),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-01-04 02:38:48 -08:00
|
|
|
if (inDevelopment) {
|
|
|
|
this.setupDevMiddlewares();
|
|
|
|
}
|
|
|
|
|
2023-08-01 08:32:30 -07:00
|
|
|
// Setup JSON parsing middleware after the webhook handlers are setup
|
|
|
|
this.app.use(jsonParser);
|
|
|
|
|
2023-01-04 02:38:48 -08:00
|
|
|
await this.configure();
|
|
|
|
|
2023-08-01 08:32:30 -07:00
|
|
|
if (!inTest) {
|
|
|
|
console.log(`Version: ${N8N_VERSION}`);
|
2023-01-04 02:38:48 -08:00
|
|
|
|
2023-08-01 08:32:30 -07:00
|
|
|
const defaultLocale = config.getEnv('defaultLocale');
|
|
|
|
if (defaultLocale !== 'en') {
|
|
|
|
console.log(`Locale: ${defaultLocale}`);
|
|
|
|
}
|
2023-01-04 02:38:48 -08:00
|
|
|
|
2023-08-01 08:32:30 -07:00
|
|
|
await this.externalHooks.run('n8n.ready', [this, config]);
|
|
|
|
}
|
2023-01-04 02:38:48 -08:00
|
|
|
}
|
|
|
|
}
|