chore: http.paths - access log, auth, metrics
This commit is contained in:
1 parent
b7c4eac93b
commit
2595ccfe7b
13 files changed
+218
-70
No files matched your search
@@ -11,8 +11,13 @@ import { UniqueAppResolver } from '@waha/apps/app_sdk/services/UniqueAppResolver
|
||||
import { Auth } from '@waha/core/auth/config';
|
||||
import { AppRuntimeConfig } from '@waha/apps/app_sdk/apps/AppRuntime';
|
||||
import { GetApps } from '@waha/apps/app_sdk/apps/registry';
|
||||
import { HttpPathsRegistration } from '@waha/plugins/http.paths.module';
|
||||
|
||||
const QUEUES_IMPORTS_REQUIRED = [
|
||||
HttpPathsRegistration(
|
||||
{ prefix: '/jobs', include: { authBasic: false } },
|
||||
{ prefix: '/jobs/', include: { authBasic: false, accessLog: false } },
|
||||
),
|
||||
BullModule.forRoot({
|
||||
connection: {
|
||||
url: process.env.REDIS_URL || 'redis://:redis@localhost:6379',
|
||||
|
||||
@@ -39,10 +39,15 @@ import { TaskContactsPullConsumer } from '@waha/apps/chatwoot/consumers/task/con
|
||||
import { TaskMessagesPullConsumer } from '@waha/apps/chatwoot/consumers/task/messages.pull';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { QueueManager } from '@waha/apps/chatwoot/services/QueueManager';
|
||||
import { HttpPathsRegistration } from '@waha/plugins/http.paths.module';
|
||||
|
||||
const CONTROLLERS = [ChatwootWebhookController, ChatwootLocalesController];
|
||||
|
||||
const IMPORTS = lodash.flatten([
|
||||
HttpPathsRegistration({
|
||||
prefix: '/webhooks/',
|
||||
include: { authBasic: false },
|
||||
}),
|
||||
BullModule.registerFlowProducer({
|
||||
name: FlowProducerName.MESSAGES_PULL_FLOW,
|
||||
}),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AppName } from '@waha/apps/app_sdk/apps/apps';
|
||||
import { McpController } from '@waha/apps/mcp/api/mcp.controller';
|
||||
import { McpService } from '@waha/apps/mcp/mcp.service';
|
||||
import { McpAppService } from '@waha/apps/mcp/services/McpAppService';
|
||||
import { HttpPathsRegistration } from '@waha/plugins/http.paths.module';
|
||||
|
||||
const McpAppModule: AppModule = {
|
||||
name: AppName.mcp,
|
||||
@@ -18,7 +19,12 @@ const McpAppModule: AppModule = {
|
||||
unique: false,
|
||||
},
|
||||
nestjs: {
|
||||
imports: [],
|
||||
imports: [
|
||||
HttpPathsRegistration(
|
||||
{ prefix: '/mcp', include: { authBasic: false } },
|
||||
{ prefix: 'mcp', include: { authApiKey: true } },
|
||||
),
|
||||
],
|
||||
controllers: [McpController],
|
||||
providers: [McpService, McpAppService],
|
||||
},
|
||||
|
||||
@@ -4,8 +4,7 @@ import { DocumentBuilder, OpenAPIObject, SwaggerModule } from '@nestjs/swagger';
|
||||
import { DECORATORS } from '@nestjs/swagger/dist/constants';
|
||||
import { GetAppsApiTags } from '@waha/apps/app_sdk/api/tags';
|
||||
import { BasicAuthFunction } from '@waha/core/auth/basicAuth';
|
||||
import { DashboardConfigServiceCore } from '@waha/core/config/DashboardConfigServiceCore';
|
||||
import { getPrometheusExcludePaths } from '@waha/modules/waha-prometheus/prometheus.config';
|
||||
import { HttpPathsService } from '@waha/plugins/HttpPathsService';
|
||||
import { Logger } from 'nestjs-pino';
|
||||
|
||||
import { WhatsappConfigService } from '../config.service';
|
||||
@@ -194,21 +193,8 @@ export class SwaggerConfiguratorCore {
|
||||
|
||||
setUpAuth(credentials: [string, string]): void {
|
||||
const [username, password] = credentials;
|
||||
const dashboardConfig = this.app.get(DashboardConfigServiceCore);
|
||||
const config = this.app.get(WhatsappConfigService);
|
||||
const exclude = lodash.uniq([
|
||||
'/api/',
|
||||
'/mcp',
|
||||
dashboardConfig.dashboardUri,
|
||||
'/health',
|
||||
'/ping',
|
||||
'/ws',
|
||||
'/webhooks/',
|
||||
'/jobs',
|
||||
'/jobs/',
|
||||
...config.getExcludedFullPaths(),
|
||||
...getPrometheusExcludePaths(),
|
||||
]);
|
||||
const httpPaths = this.app.get(HttpPathsService);
|
||||
const exclude = lodash.uniq(httpPaths.globalAuthExcludePrefixes());
|
||||
|
||||
const authFunction = BasicAuthFunction(username, password, exclude);
|
||||
this.app.use(authFunction);
|
||||
|
||||
+56
-38
@@ -29,15 +29,14 @@ import { MediaLocalStorageModule } from '@waha/core/media/local/media.local.stor
|
||||
import { MediaLocalStorageConfig } from '@waha/core/media/local/MediaLocalStorageConfig';
|
||||
import { MediaPsqlStorageModule } from '@waha/core/media/psql/media.psql.storage.module';
|
||||
import { MediaS3StorageModule } from '@waha/core/media/s3/media.s3.storage.module';
|
||||
import { HttpPathsModule } from '@waha/plugins/http.paths.module';
|
||||
import { HttpPathsService } from '@waha/plugins/HttpPathsService';
|
||||
import { SessionPluginsModule } from '@waha/plugins/session.plugins.module';
|
||||
import { isPresenceAutoOnlineEnabled } from '@waha/modules/waha-maintain-online-status/maintain-online-status.config';
|
||||
import { MaintainOnlineStatusModule } from '@waha/modules/waha-maintain-online-status/maintain-online-status.module';
|
||||
import { isJidEngine } from '@waha/modules/waha-wid-jid/wid-jid.plugins';
|
||||
import { MessageSourceModule } from '@waha/modules/waha-message-source/message-source.module';
|
||||
import {
|
||||
getPrometheusPath,
|
||||
isPrometheusEnabled,
|
||||
} from '@waha/modules/waha-prometheus/prometheus.config';
|
||||
import { isPrometheusEnabled } from '@waha/modules/waha-prometheus/prometheus.config';
|
||||
import { PrometheusModule } from '@waha/modules/waha-prometheus/prometheus.module';
|
||||
import { WebhookModule } from '@waha/modules/waha-webhook/webhook.module';
|
||||
import { SessionRuntimeInfoModule } from '@waha/modules/waha-session-runtime-info/session-runtime-info.module';
|
||||
@@ -94,40 +93,37 @@ import { SessionService } from '@waha/core/services/SessionService';
|
||||
|
||||
export const IMPORTS_CORE = [
|
||||
...AppsModuleExports.imports,
|
||||
LoggerModule.forRoot({
|
||||
renameContext: 'name',
|
||||
pinoHttp: {
|
||||
level: getPinoLogLevel(),
|
||||
useLevel: getPinoHttpUseLevel(),
|
||||
transport: getPinoTransport(),
|
||||
autoLogging: {
|
||||
ignore: (req) => {
|
||||
return (
|
||||
req.url.startsWith('/ping') ||
|
||||
req.url.startsWith(getPrometheusPath()) ||
|
||||
req.url.startsWith('/dashboard/') ||
|
||||
req.url.startsWith('/api/files/') ||
|
||||
req.url.startsWith('/api/s3/') ||
|
||||
req.url.startsWith('/jobs/')
|
||||
);
|
||||
LoggerModule.forRootAsync({
|
||||
imports: [HttpPathsModule],
|
||||
inject: [HttpPathsService],
|
||||
useFactory: (httpPaths: HttpPathsService) => {
|
||||
return {
|
||||
renameContext: 'name',
|
||||
pinoHttp: {
|
||||
level: getPinoLogLevel(),
|
||||
useLevel: getPinoHttpUseLevel(),
|
||||
transport: getPinoTransport(),
|
||||
autoLogging: {
|
||||
ignore: (req) => httpPaths.isAccessLogIgnored(req.url),
|
||||
},
|
||||
redact: {
|
||||
paths: ['req.query["x-api-key"]'],
|
||||
censor: '[REDACTED]',
|
||||
},
|
||||
customAttributeKeys: { req: 'req', res: 'res' },
|
||||
serializers: {
|
||||
req: (req) => ({
|
||||
method: req.method,
|
||||
url: redactUrlParams('x-api-key', req.url, req.query),
|
||||
query: req.query,
|
||||
params: req.params,
|
||||
}),
|
||||
res: (res) => ({
|
||||
statusCode: res.statusCode,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
redact: {
|
||||
paths: ['req.query["x-api-key"]'],
|
||||
censor: '[REDACTED]',
|
||||
},
|
||||
customAttributeKeys: { req: 'req', res: 'res' },
|
||||
serializers: {
|
||||
req: (req) => ({
|
||||
method: req.method,
|
||||
url: redactUrlParams('x-api-key', req.url, req.query),
|
||||
query: req.query,
|
||||
params: req.params,
|
||||
}),
|
||||
res: (res) => ({
|
||||
statusCode: res.statusCode,
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
ConfigModule.forRoot({
|
||||
@@ -154,6 +150,7 @@ export const IMPORTS_CORE = [
|
||||
}),
|
||||
PassportModule,
|
||||
TerminusModule,
|
||||
HttpPathsModule,
|
||||
SessionPluginsModule,
|
||||
SessionRuntimeInfoModule,
|
||||
WebhookModule,
|
||||
@@ -281,8 +278,29 @@ export class AppModuleCore {
|
||||
constructor(
|
||||
protected config: WhatsappConfigService,
|
||||
private dashboardConfig: DashboardConfigServiceCore,
|
||||
private httpPaths: HttpPathsService,
|
||||
) {
|
||||
this.startTimestamp = Date.now();
|
||||
this.httpPaths.register(
|
||||
{ prefix: '/ping', include: { accessLog: false, authBasic: false } },
|
||||
{ prefix: '/api/', include: { authBasic: false } },
|
||||
{ prefix: 'api', include: { authApiKey: true } },
|
||||
{ prefix: '/health', include: { authBasic: false } },
|
||||
{ prefix: 'health', include: { authApiKey: true } },
|
||||
{ prefix: '/ws', include: { authBasic: false } },
|
||||
{
|
||||
prefix: this.dashboardConfig.dashboardUri,
|
||||
include: { authBasic: false },
|
||||
},
|
||||
{
|
||||
prefix: this.dashboardConfig.dashboardUri + '/',
|
||||
include: { accessLog: false },
|
||||
},
|
||||
);
|
||||
// WHATSAPP_API_KEY_EXCLUDE_PATH - env-driven auth exclusions
|
||||
for (const path of this.config.getExcludedFullPaths()) {
|
||||
this.httpPaths.register({ prefix: path, include: { authBasic: false } });
|
||||
}
|
||||
}
|
||||
|
||||
static getHttpsOptions(logger: Logger) {
|
||||
@@ -311,7 +329,7 @@ export class AppModuleCore {
|
||||
consumer
|
||||
.apply(ApiKeyAuthMiddleware)
|
||||
.exclude(...exclude)
|
||||
.forRoutes('api', 'health', 'mcp');
|
||||
.forRoutes(...this.httpPaths.apiKeyRoutes());
|
||||
|
||||
// Dashboard
|
||||
const dashboardCredentials = this.dashboardConfig.credentials;
|
||||
|
||||
@@ -4,9 +4,12 @@ import { WhatsappConfigService } from '@waha/config.service';
|
||||
import { MediaLocalStorageConfig } from '@waha/core/media/local/MediaLocalStorageConfig';
|
||||
import { MediaLocalStorageFactory } from '@waha/core/media/local/MediaLocalStorageFactory';
|
||||
import { MediaStorageFactory } from '@waha/core/media/MediaStorageFactory';
|
||||
import { HttpPathsModule } from '@waha/plugins/http.paths.module';
|
||||
import { HttpPathsService } from '@waha/plugins/HttpPathsService';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
HttpPathsModule,
|
||||
ServeStaticModule.forRootAsync({
|
||||
imports: [],
|
||||
extraProviders: [MediaLocalStorageConfig, WhatsappConfigService],
|
||||
@@ -31,4 +34,11 @@ import { MediaStorageFactory } from '@waha/core/media/MediaStorageFactory';
|
||||
],
|
||||
exports: [MediaStorageFactory],
|
||||
})
|
||||
export class MediaLocalStorageModule {}
|
||||
export class MediaLocalStorageModule {
|
||||
constructor(config: MediaLocalStorageConfig, httpPaths: HttpPathsService) {
|
||||
httpPaths.register({
|
||||
prefix: config.filesUri + '/',
|
||||
include: { accessLog: false, metrics: false },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,14 @@ import { WhatsappConfigService } from '@waha/config.service';
|
||||
import { EngineConfigService } from '@waha/core/config/EngineConfigService';
|
||||
import { MediaStorageFactory } from '@waha/core/media/MediaStorageFactory';
|
||||
import { PsqlFilesController } from '@waha/core/media/psql/api/psql.files.controller';
|
||||
import { HttpPathsModule } from '@waha/plugins/http.paths.module';
|
||||
import { HttpPathsService } from '@waha/plugins/HttpPathsService';
|
||||
|
||||
import { MediaPsqlStorageConfig } from './MediaPsqlStorageConfig';
|
||||
import { MediaPsqlStorageFactory } from './MediaPsqlStorageFactory';
|
||||
|
||||
@Module({
|
||||
imports: [HttpPathsModule],
|
||||
providers: [
|
||||
{
|
||||
provide: MediaStorageFactory,
|
||||
@@ -21,4 +24,11 @@ import { MediaPsqlStorageFactory } from './MediaPsqlStorageFactory';
|
||||
exports: [MediaStorageFactory],
|
||||
controllers: [PsqlFilesController],
|
||||
})
|
||||
export class MediaPsqlStorageModule {}
|
||||
export class MediaPsqlStorageModule {
|
||||
constructor(config: MediaPsqlStorageConfig, httpPaths: HttpPathsService) {
|
||||
httpPaths.register({
|
||||
prefix: config.filesUri + '/',
|
||||
include: { accessLog: false, metrics: false },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,11 @@ import {
|
||||
S3ProxyUrl,
|
||||
S3Url,
|
||||
} from '@waha/core/media/s3/MediaS3UrlResolver';
|
||||
import { HttpPathsModule } from '@waha/plugins/http.paths.module';
|
||||
import { HttpPathsService } from '@waha/plugins/HttpPathsService';
|
||||
|
||||
@Module({
|
||||
imports: [HttpPathsModule],
|
||||
providers: [
|
||||
{
|
||||
provide: S3Client,
|
||||
@@ -44,4 +47,11 @@ import {
|
||||
exports: [MediaStorageFactory],
|
||||
controllers: [S3ProxyController],
|
||||
})
|
||||
export class MediaS3StorageModule {}
|
||||
export class MediaS3StorageModule {
|
||||
constructor(httpPaths: HttpPathsService) {
|
||||
httpPaths.register({
|
||||
prefix: '/api/s3/',
|
||||
include: { accessLog: false, metrics: false },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -46,13 +46,6 @@ export function getPrometheusPath(): string {
|
||||
return process.env[Env.WAHA_PROMETHEUS_PATH] || DEFAULT_PATH;
|
||||
}
|
||||
|
||||
export function getPrometheusExcludePaths(): string[] {
|
||||
if (!isPrometheusEnabled(process.env)) {
|
||||
return [];
|
||||
}
|
||||
return [getPrometheusPath()];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PrometheusConfigService {
|
||||
private eventUnmask = new EventWildUnmask(WAHAEvents, WAHAEventsWild);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { PrometheusConfigService } from '@waha/modules/waha-prometheus/prometheus.config';
|
||||
import { WahaMetrics } from '@waha/modules/waha-prometheus/prometheus.metrics';
|
||||
import { HttpPathsService } from '@waha/plugins/HttpPathsService';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class HttpMetricsMiddleware implements NestMiddleware {
|
||||
constructor(
|
||||
private metrics: WahaMetrics,
|
||||
private config: PrometheusConfigService,
|
||||
private httpPaths: HttpPathsService,
|
||||
) {}
|
||||
|
||||
use(req: Request, res: Response, next: NextFunction): void {
|
||||
const pathname = (req.originalUrl || req.url || '').split('?')[0];
|
||||
if (pathname.startsWith(this.config.path)) {
|
||||
if (this.httpPaths.isHttpMetricsIgnored(pathname)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -10,12 +10,15 @@ import { EventMetricsSubscriber } from '@waha/modules/waha-prometheus/prometheus
|
||||
import { HttpMetricsMiddleware } from '@waha/modules/waha-prometheus/prometheus.http.middleware';
|
||||
import { WahaMetrics } from '@waha/modules/waha-prometheus/prometheus.metrics';
|
||||
import { SessionMetricsCollector } from '@waha/modules/waha-prometheus/prometheus.sessions.collector';
|
||||
import { HttpPathsModule } from '@waha/plugins/http.paths.module';
|
||||
import { HttpPathsService } from '@waha/plugins/HttpPathsService';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
validationSchema: PrometheusEnvSchema,
|
||||
}),
|
||||
HttpPathsModule,
|
||||
],
|
||||
providers: [
|
||||
PrometheusConfigService,
|
||||
@@ -32,7 +35,15 @@ import { SessionMetricsCollector } from '@waha/modules/waha-prometheus/prometheu
|
||||
* optional basic auth via WAHA_PROMETHEUS_USERNAME and WAHA_PROMETHEUS_PASSWORD.
|
||||
*/
|
||||
export class PrometheusModule implements NestModule {
|
||||
constructor(private config: PrometheusConfigService) {}
|
||||
constructor(
|
||||
private config: PrometheusConfigService,
|
||||
httpPaths: HttpPathsService,
|
||||
) {
|
||||
httpPaths.register({
|
||||
prefix: this.config.path,
|
||||
include: { accessLog: false, authBasic: false, metrics: false },
|
||||
});
|
||||
}
|
||||
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
const credentials = this.config.credentials;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* What the path takes part in.
|
||||
*/
|
||||
export interface HttpPathInclude {
|
||||
/** Log requests to the path in the HTTP access log (default true). */
|
||||
accessLog?: boolean;
|
||||
/** Protect the path with the global (Swagger) basic auth (default true). */
|
||||
authBasic?: boolean;
|
||||
/** Protect the route with the api key middleware (default false). */
|
||||
authApiKey?: boolean;
|
||||
/** Count requests to the path in the HTTP metrics (default true). */
|
||||
metrics?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A URL prefix contributed by a module - matched with url.startsWith(prefix);
|
||||
* for authApiKey the prefix is passed verbatim to MiddlewareConsumer.forRoutes().
|
||||
*/
|
||||
export interface HttpPathContribution {
|
||||
prefix: string;
|
||||
include?: HttpPathInclude;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects HTTP path contributions from modules, so core auth, logging and metrics can honor them
|
||||
* without importing the modules.
|
||||
*/
|
||||
@Injectable()
|
||||
export class HttpPathsService {
|
||||
private contributions: HttpPathContribution[] = [];
|
||||
|
||||
register(...contributions: HttpPathContribution[]): void {
|
||||
this.contributions.push(...contributions);
|
||||
}
|
||||
|
||||
isAccessLogIgnored(url: string): boolean {
|
||||
return this.contributions.some(
|
||||
(contribution) =>
|
||||
contribution.include?.accessLog === false &&
|
||||
url.startsWith(contribution.prefix),
|
||||
);
|
||||
}
|
||||
|
||||
globalAuthExcludePrefixes(): string[] {
|
||||
return this.contributions
|
||||
.filter((contribution) => contribution.include?.authBasic === false)
|
||||
.map((contribution) => contribution.prefix);
|
||||
}
|
||||
|
||||
isHttpMetricsIgnored(pathname: string): boolean {
|
||||
return this.contributions.some(
|
||||
(contribution) =>
|
||||
contribution.include?.metrics === false &&
|
||||
pathname.startsWith(contribution.prefix),
|
||||
);
|
||||
}
|
||||
|
||||
apiKeyRoutes(): string[] {
|
||||
return this.contributions
|
||||
.filter((contribution) => contribution.include?.authApiKey === true)
|
||||
.map((contribution) => contribution.prefix);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Inject, Module, Type } from '@nestjs/common';
|
||||
import {
|
||||
HttpPathContribution,
|
||||
HttpPathsService,
|
||||
} from '@waha/plugins/HttpPathsService';
|
||||
|
||||
@Module({
|
||||
providers: [HttpPathsService],
|
||||
exports: [HttpPathsService],
|
||||
})
|
||||
export class HttpPathsModule {}
|
||||
|
||||
/**
|
||||
* Import-time registration for modules that have no class of their own (e.g. object-style apps) -
|
||||
* returns a module that registers the contributions in HttpPathsService at bootstrap.
|
||||
*/
|
||||
export function HttpPathsRegistration(
|
||||
...contributions: HttpPathContribution[]
|
||||
): Type<any> {
|
||||
@Module({
|
||||
imports: [HttpPathsModule],
|
||||
})
|
||||
class HttpPathsRegistrationModule {
|
||||
constructor(@Inject(HttpPathsService) httpPaths: HttpPathsService) {
|
||||
httpPaths.register(...contributions);
|
||||
}
|
||||
}
|
||||
return HttpPathsRegistrationModule;
|
||||
}
|
||||
Reference in new issue
Block a user