diff --git a/src/core/app.module.core.ts b/src/core/app.module.core.ts index 76215a31..df51bb9d 100644 --- a/src/core/app.module.core.ts +++ b/src/core/app.module.core.ts @@ -1,4 +1,6 @@ -import { INestApplication, Module } from '@nestjs/common'; +import * as process from 'node:process'; + +import { INestApplication, MiddlewareConsumer, Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { PassportModule } from '@nestjs/passport'; @@ -12,17 +14,22 @@ import { ServerDebugController, } from '@waha/api/server.controller'; import { WebsocketGatewayCore } from '@waha/core/api/websocket.gateway.core'; +import { AuthMiddleware } from '@waha/core/auth/auth.middleware'; +import { BasicAuthFunction } from '@waha/core/auth/basicAuth'; import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService'; import { WebJSEngineConfigService } from '@waha/core/config/WebJSEngineConfigService'; import { MediaLocalStorageModule } from '@waha/core/media/local/media.local.storage.module'; import { MediaLocalStorageConfig } from '@waha/core/media/local/MediaLocalStorageConfig'; import { ChannelsInfoServiceCore } from '@waha/core/services/ChannelsInfoServiceCore'; +import { parseBool } from '@waha/helpers'; import { BufferJsonReplacerInterceptor } from '@waha/nestjs/BufferJsonReplacerInterceptor'; +import { HttpsExpress } from '@waha/nestjs/HttpsExpress'; import { getPinoHttpUseLevel, getPinoLogLevel, getPinoTransport, } from '@waha/utils/logging'; +import { noSlashAtTheEnd } from '@waha/utils/string'; import * as Joi from 'joi'; import { LoggerModule } from 'nestjs-pino'; import { join } from 'path'; @@ -176,15 +183,44 @@ const PROVIDERS = [ export class AppModuleCore { public startTimestamp: number; - constructor(protected config: WhatsappConfigService) { + constructor( + protected config: WhatsappConfigService, + private dashboardConfig: DashboardConfigServiceCore, + ) { this.startTimestamp = Date.now(); } static getHttpsOptions(logger: Logger) { - return undefined; + const httpsEnabled = parseBool(process.env.WAHA_HTTPS_ENABLED); + if (!httpsEnabled) { + return undefined; + } + const httpsExpress = new HttpsExpress(logger); + return httpsExpress.readSync(); } static appReady(app: INestApplication, logger: Logger) { - return; + const httpsEnabled = parseBool(process.env.WAHA_HTTPS_ENABLED); + if (!httpsEnabled) { + return; + } + const httpd = app.getHttpServer(); + const httpsExpress = new HttpsExpress(logger); + httpsExpress.watchCertChanges(httpd); + } + + configure(consumer: MiddlewareConsumer) { + const exclude = this.config.getExcludedPaths(); + consumer + .apply(AuthMiddleware) + .exclude(...exclude) + .forRoutes('api', 'health', 'ws'); + const dashboardCredentials = this.dashboardConfig.credentials; + if (dashboardCredentials) { + const username = dashboardCredentials[0]; + const password = dashboardCredentials[1]; + const route = noSlashAtTheEnd(this.dashboardConfig.dashboardUri); + consumer.apply(BasicAuthFunction(username, password)).forRoutes(route); + } } } diff --git a/src/nestjs/HttpsExpress.ts b/src/nestjs/HttpsExpress.ts new file mode 100644 index 00000000..95656abb --- /dev/null +++ b/src/nestjs/HttpsExpress.ts @@ -0,0 +1,73 @@ +import * as fs from 'node:fs'; + +import { LoggerBuilder } from '@waha/utils/logging'; +import { Logger } from 'pino'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const chokidar = require('chokidar'); + +export class HttpsExpress { + private readonly keyPath: string; + private readonly certPath: string; + private readonly caPath: string; + + constructor(private logger: Logger) { + // + // Let's encrypt certificates default paths + // cert.pem chain.pem fullchain.pem privkey.pem + // + this.keyPath = process.env.WAHA_HTTPS_PATH_KEY || './.secrets/privkey.pem'; + this.certPath = process.env.WAHA_HTTPS_PATH_CERT || './.secrets/cert.pem'; + this.caPath = process.env.WAHA_HTTPS_PATH_CA; + if (this.caPath == null) { + this.caPath = './.secrets/chain.pem'; + } + } + + readSync() { + this.logger.info('Reading HTTPS certificates...'); + this.logger.info('HTTPS Key Path:', this.keyPath); + const key = fs.readFileSync(this.keyPath); + + this.logger.info('HTTPS Cert Path:', this.certPath); + const cert = fs.readFileSync(this.certPath); + + this.logger.info('HTTPS CA Path:', this.caPath); + const ca = this.caPath ? fs.readFileSync(this.caPath) : undefined; + + this.logger.info('HTTPS certificates read successfully'); + return { key: key, cert: cert, ca: ca }; + } + + /** + * https://stackoverflow.com/a/74076392 + */ + watchCertChanges(httpd) { + let waitForCertAndFullChainToGetUpdatedTooTimeout: any; + const paths = [this.keyPath, this.certPath, this.caPath].filter( + (path) => !!path, + ); + const watcher = chokidar.watch(paths, { + followSymlinks: false, + persistent: true, + ignoreInitial: true, + disableGlobbing: true, + }); + // IDK why, but it has few bugs: + // 1. It issues 'add' event at the start, even tho ignoreInitial is set to true + // 2. It issues additional 'add' for the same file, but without full path + watcher.on('all', (eventName, path, stats) => { + this.logger.info(`HTTPS file '${path}' has been '${eventName}'...`); + clearTimeout(waitForCertAndFullChainToGetUpdatedTooTimeout); + waitForCertAndFullChainToGetUpdatedTooTimeout = setTimeout(() => { + this.logger.info('Updating HTTPS configuration...'); + httpd.setSecureContext(this.readSync()); + }, 1000); + }); + process.on('SIGTERM', () => { + this.logger.info('SIGTERM received, closing HTTP file watchers'); + clearTimeout(waitForCertAndFullChainToGetUpdatedTooTimeout); + watcher.close(); + }); + } +}