| import { inTest, inDevelopment, Logger } from '@n8n/backend-common'; |
| import { GlobalConfig } from '@n8n/config'; |
| import { OnShutdown } from '@n8n/decorators'; |
| import { Container, Service } from '@n8n/di'; |
| import compression from 'compression'; |
| import express from 'express'; |
| import { engine as expressHandlebars } from 'express-handlebars'; |
| import { readFile } from 'fs/promises'; |
| import type { Server } from 'http'; |
| import isbot from 'isbot'; |
|
|
| import config from '@/config'; |
| import { N8N_VERSION, TEMPLATES_DIR } from '@/constants'; |
| import { DbConnection } from '@/databases/db-connection'; |
| import { ServiceUnavailableError } from '@/errors/response-errors/service-unavailable.error'; |
| import { ExternalHooks } from '@/external-hooks'; |
| import { rawBodyReader, bodyParser, corsMiddleware } from '@/middlewares'; |
| import { send, sendErrorResponse } from '@/response-helper'; |
| import { LiveWebhooks } from '@/webhooks/live-webhooks'; |
| import { TestWebhooks } from '@/webhooks/test-webhooks'; |
| import { WaitingForms } from '@/webhooks/waiting-forms'; |
| import { WaitingWebhooks } from '@/webhooks/waiting-webhooks'; |
| import { createWebhookHandlerFor } from '@/webhooks/webhook-request-handler'; |
|
|
| @Service() |
| export abstract class AbstractServer { |
| protected logger: Logger; |
|
|
| protected server: Server; |
|
|
| readonly app: express.Application; |
|
|
| protected externalHooks: ExternalHooks; |
|
|
| protected globalConfig = Container.get(GlobalConfig); |
|
|
| protected dbConnection = Container.get(DbConnection); |
|
|
| protected sslKey: string; |
|
|
| protected sslCert: string; |
|
|
| protected restEndpoint: string; |
|
|
| protected endpointForm: string; |
|
|
| protected endpointFormTest: string; |
|
|
| protected endpointFormWaiting: string; |
|
|
| protected endpointWebhook: string; |
|
|
| protected endpointWebhookTest: string; |
|
|
| protected endpointWebhookWaiting: string; |
|
|
| protected endpointMcp: string; |
|
|
| protected endpointMcpTest: string; |
|
|
| protected webhooksEnabled = true; |
|
|
| protected testWebhooksEnabled = false; |
|
|
| readonly uniqueInstanceId: string; |
|
|
| constructor() { |
| this.app = express(); |
| this.app.disable('x-powered-by'); |
| this.app.set('query parser', 'extended'); |
| this.app.engine('handlebars', expressHandlebars({ defaultLayout: false })); |
| this.app.set('view engine', 'handlebars'); |
| this.app.set('views', TEMPLATES_DIR); |
|
|
| const proxyHops = this.globalConfig.proxy_hops; |
| if (proxyHops > 0) this.app.set('trust proxy', proxyHops); |
|
|
| this.sslKey = config.getEnv('ssl_key'); |
| this.sslCert = config.getEnv('ssl_cert'); |
|
|
| const { endpoints } = this.globalConfig; |
| this.restEndpoint = endpoints.rest; |
|
|
| this.endpointForm = endpoints.form; |
| this.endpointFormTest = endpoints.formTest; |
| this.endpointFormWaiting = endpoints.formWaiting; |
|
|
| this.endpointWebhook = endpoints.webhook; |
| this.endpointWebhookTest = endpoints.webhookTest; |
| this.endpointWebhookWaiting = endpoints.webhookWaiting; |
|
|
| this.endpointMcp = endpoints.mcp; |
| this.endpointMcpTest = endpoints.mcpTest; |
|
|
| this.logger = Container.get(Logger); |
| } |
|
|
| async configure(): Promise<void> { |
| |
| } |
|
|
| private async setupErrorHandlers() { |
| const { app } = this; |
|
|
| |
| const { setupExpressErrorHandler } = await import('@sentry/node'); |
| setupExpressErrorHandler(app); |
| } |
|
|
| private setupCommonMiddlewares() { |
| |
| this.app.use(compression()); |
|
|
| |
| this.app.use(rawBodyReader); |
| } |
|
|
| private setupDevMiddlewares() { |
| this.app.use(corsMiddleware); |
| } |
|
|
| protected setupPushServer() {} |
|
|
| private async setupHealthCheck() { |
| |
| this.app.get('/healthz', (_req, res) => { |
| res.send({ status: 'ok' }); |
| }); |
|
|
| const { connectionState } = this.dbConnection; |
|
|
| this.app.get('/healthz/readiness', (_req, res) => { |
| const { connected, migrated } = connectionState; |
| if (connected && migrated) { |
| res.status(200).send({ status: 'ok' }); |
| } else { |
| res.status(503).send({ status: 'error' }); |
| } |
| }); |
|
|
| 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!')); |
| }); |
| } |
|
|
| async init(): Promise<void> { |
| const { app, sslKey, sslCert } = this; |
| const { protocol } = this.globalConfig; |
|
|
| if (protocol === 'https' && sslKey && sslCert) { |
| const https = await import('https'); |
| this.server = https.createServer( |
| { |
| key: await readFile(this.sslKey, 'utf8'), |
| cert: await readFile(this.sslCert, 'utf8'), |
| }, |
| app, |
| ); |
| } else { |
| const http = await import('http'); |
| this.server = http.createServer(app); |
| } |
|
|
| const { port, listen_address: address } = Container.get(GlobalConfig); |
|
|
| this.server.on('error', (error: Error & { code: string }) => { |
| if (error.code === 'EADDRINUSE') { |
| this.logger.info( |
| `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) => this.server.listen(port, address, () => resolve())); |
|
|
| this.externalHooks = Container.get(ExternalHooks); |
|
|
| await this.setupHealthCheck(); |
|
|
| this.logger.info(`n8n ready on ${address}, port ${port}`); |
| } |
|
|
| async start(): Promise<void> { |
| if (!inTest) { |
| await this.setupErrorHandlers(); |
| this.setupPushServer(); |
| } |
|
|
| this.setupCommonMiddlewares(); |
|
|
| |
| if (this.webhooksEnabled) { |
| const liveWebhooksRequestHandler = createWebhookHandlerFor(Container.get(LiveWebhooks)); |
| |
| this.app.all(`/${this.endpointForm}/*path`, liveWebhooksRequestHandler); |
|
|
| |
| this.app.all(`/${this.endpointWebhook}/*path`, liveWebhooksRequestHandler); |
|
|
| |
| this.app.all( |
| `/${this.endpointFormWaiting}/:path{/:suffix}`, |
| createWebhookHandlerFor(Container.get(WaitingForms)), |
| ); |
|
|
| |
| this.app.all( |
| `/${this.endpointWebhookWaiting}/:path{/:suffix}`, |
| createWebhookHandlerFor(Container.get(WaitingWebhooks)), |
| ); |
|
|
| |
| this.app.all(`/${this.endpointMcp}/*path`, liveWebhooksRequestHandler); |
| } |
|
|
| if (this.testWebhooksEnabled) { |
| const testWebhooksRequestHandler = createWebhookHandlerFor(Container.get(TestWebhooks)); |
|
|
| |
| this.app.all(`/${this.endpointFormTest}/*path`, testWebhooksRequestHandler); |
| this.app.all(`/${this.endpointWebhookTest}/*path`, testWebhooksRequestHandler); |
|
|
| |
| this.app.all(`/${this.endpointMcpTest}/*path`, testWebhooksRequestHandler); |
| } |
|
|
| |
| const checkIfBot = isbot.spawn(['bot']); |
| this.app.use((req, res, next) => { |
| const userAgent = req.headers['user-agent']; |
| if (userAgent && checkIfBot(userAgent)) { |
| this.logger.info(`Blocked ${req.method} ${req.url} for "${userAgent}"`); |
| res.status(204).end(); |
| } else next(); |
| }); |
|
|
| if (inDevelopment) { |
| this.setupDevMiddlewares(); |
| } |
|
|
| if (this.testWebhooksEnabled) { |
| const testWebhooks = Container.get(TestWebhooks); |
| |
| |
| this.app.delete( |
| `/${this.restEndpoint}/test-webhook/:id`, |
| send(async (req) => await testWebhooks.cancelWebhook(req.params.id)), |
| ); |
| } |
|
|
| |
| this.app.use(bodyParser); |
|
|
| await this.configure(); |
|
|
| if (!inTest) { |
| this.logger.info(`Version: ${N8N_VERSION}`); |
|
|
| const { defaultLocale } = this.globalConfig; |
| if (defaultLocale !== 'en') { |
| this.logger.info(`Locale: ${defaultLocale}`); |
| } |
|
|
| await this.externalHooks.run('n8n.ready', [this, config]); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| @OnShutdown() |
| async onShutdown(): Promise<void> { |
| if (!this.server) { |
| return; |
| } |
|
|
| const { protocol } = this.globalConfig; |
|
|
| this.logger.debug(`Shutting down ${protocol} server`); |
|
|
| this.server.close((error) => { |
| if (error) { |
| this.logger.error(`Error while shutting down ${protocol} server`, { error }); |
| } |
|
|
| this.logger.debug(`${protocol} server shut down`); |
| }); |
| } |
| } |
|
|