From babe5ae1770f5d4d2f4c82996b695b58d1736165 Mon Sep 17 00:00:00 2001 From: mannilakash Date: Fri, 27 Mar 2026 08:48:16 -0400 Subject: [PATCH] feat: adding custom/default user agent support for http monitors --- client/src/Hooks/useMonitorForm.ts | 1 + client/src/Hooks/useSettingsForm.ts | 1 + client/src/Pages/CreateMonitor/index.tsx | 25 +++++++++++++++++++ client/src/Pages/Settings/index.tsx | 24 ++++++++++++++++++ client/src/Types/Monitor.ts | 1 + client/src/Types/Settings.ts | 1 + client/src/Validation/monitor.ts | 1 + client/src/Validation/settings.ts | 5 ++++ client/src/locales/en.json | 16 ++++++++++++ server/src/config/services.ts | 2 +- server/src/db/models/AppSettings.ts | 1 + server/src/db/models/Monitor.ts | 3 +++ .../monitors/MongoMonitorsRepository.ts | 2 ++ .../settings/MongoSettingsRepository.ts | 1 + .../infrastructure/network/HttpProvider.ts | 18 ++++++++++--- server/src/types/monitor.ts | 1 + server/src/types/settings.ts | 1 + server/src/validation/monitorValidation.ts | 3 +++ server/src/validation/settingsValidation.ts | 1 + 19 files changed, 104 insertions(+), 4 deletions(-) diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts index 963409fc8..1e8810940 100644 --- a/client/src/Hooks/useMonitorForm.ts +++ b/client/src/Hooks/useMonitorForm.ts @@ -40,6 +40,7 @@ export const useMonitorForm = ({ matchMethod: data?.matchMethod || "", expectedValue: data?.expectedValue || "", jsonPath: data?.jsonPath || "", + customUserAgent: data?.customUserAgent || "", }; break; case "ping": diff --git a/client/src/Hooks/useSettingsForm.ts b/client/src/Hooks/useSettingsForm.ts index 4b0768d79..a9672cf0e 100644 --- a/client/src/Hooks/useSettingsForm.ts +++ b/client/src/Hooks/useSettingsForm.ts @@ -40,6 +40,7 @@ export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) => : 80, }, checkTTL: data?.checkTTL ?? 30, + defaultUserAgent: data?.defaultUserAgent || "", pagespeedApiKey: "", systemEmailPassword: "", }; diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 15b76eab3..86aac8741 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -795,6 +795,31 @@ const CreateMonitorPage = () => { /> )} + {watchedType === "http" && ( + ( + + )} + /> + } + /> + )} + {watchedType === "http" && ( { } /> + {/* Default User-Agent - Admin Only */} + {isAdmin && ( + ( + + )} + /> + } + /> + )} + {/* Clear All Stats */} {isAdmin && ( (val.trim() === "" ? null : val.trim())) + .optional(), checkTTL: z .number() .int() diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 83ca4fa33..6f0574dff 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -527,6 +527,14 @@ }, "title": "TLS/SSL settings" }, + "userAgent": { + "title": "Custom User-Agent", + "description": "Override the User-Agent header sent with HTTP requests for this monitor. Useful for identifying Checkmate in WAF logs. Leave blank to use the global default.", + "option": { + "label": "User-Agent", + "placeholder": "Checkmate/X.X (uptime monitor)" + } + }, "incidents": { "description": "A sliding window is used to determine when a monitor goes down. The status of a monitor will only change when the percentage of checks in the sliding window meet the specified value.", "option": { @@ -1051,6 +1059,14 @@ } } }, + "userAgent": { + "title": "Default User-Agent", + "description": "Set a default User-Agent header sent with all HTTP uptime monitor requests. Individual monitors can override this. Useful for identifying Checkmate in WAF logs and whitelists.", + "option": { + "label": "Default User-Agent", + "placeholder": "Checkmate/X.X (uptime monitor; your-instance)" + } + }, "stats": { "title": "Monitor history", "description": "Clear all monitoring history and statistics for your team. This action is irreversible.", diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 8ed72862a..19c6b4ee7 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -248,7 +248,7 @@ export const initializeServices = async ({ // Network providers const pingProvider = new PingProvider(ping); - const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath)); + const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath), settingsService); const pageSpeedProvider = new PageSpeedProvider(httpProvider, settingsService, logger); const hardwareProvider = new HardwareProvider(httpProvider); const dockerProvider = new DockerProvider(logger, Docker); diff --git a/server/src/db/models/AppSettings.ts b/server/src/db/models/AppSettings.ts index 9eeaa640a..f46396c01 100644 --- a/server/src/db/models/AppSettings.ts +++ b/server/src/db/models/AppSettings.ts @@ -36,6 +36,7 @@ const AppSettingsSchema = new Schema( systemEmailRequireTLS: { type: Boolean, default: false }, systemEmailRejectUnauthorized: { type: Boolean, default: true }, showURL: { type: Boolean, default: false }, + defaultUserAgent: { type: String }, singleton: { type: Boolean, required: true, unique: true, default: true }, version: { type: Number, default: 1 }, globalThresholds: { type: thresholdsSchema }, diff --git a/server/src/db/models/Monitor.ts b/server/src/db/models/Monitor.ts index 036aeadad..7b4b0c178 100644 --- a/server/src/db/models/Monitor.ts +++ b/server/src/db/models/Monitor.ts @@ -339,6 +339,9 @@ const MonitorSchema = new Schema( return value && value.trim() ? value.trim() : null; }, }, + customUserAgent: { + type: String, + }, geoCheckEnabled: { type: Boolean, default: false, diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index b2d759448..1ee359ef0 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -387,6 +387,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { gameId: doc.gameId ?? undefined, grpcServiceName: doc.grpcServiceName ?? undefined, group: doc.group ?? null, + customUserAgent: doc.customUserAgent ?? undefined, recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)), geoCheckEnabled: doc.geoCheckEnabled ?? false, geoCheckLocations: doc.geoCheckLocations ?? [], @@ -446,6 +447,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { gameId: doc.gameId ?? undefined, grpcServiceName: doc.grpcServiceName ?? undefined, group: doc.group ?? null, + customUserAgent: doc.customUserAgent ?? undefined, recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)), geoCheckEnabled: doc.geoCheckEnabled ?? false, geoCheckLocations: doc.geoCheckLocations ?? [], diff --git a/server/src/repositories/settings/MongoSettingsRepository.ts b/server/src/repositories/settings/MongoSettingsRepository.ts index 3117a94ea..b2b604c4c 100644 --- a/server/src/repositories/settings/MongoSettingsRepository.ts +++ b/server/src/repositories/settings/MongoSettingsRepository.ts @@ -38,6 +38,7 @@ class MongoSettingsRepository implements ISettingsRepository { systemEmailRequireTLS: doc.systemEmailRequireTLS ?? false, systemEmailRejectUnauthorized: doc.systemEmailRejectUnauthorized ?? true, showURL: doc.showURL ?? false, + defaultUserAgent: doc.defaultUserAgent ?? undefined, singleton: doc.singleton, version: doc.version ?? 1, globalThresholds: doc.globalThresholds ?? undefined, diff --git a/server/src/service/infrastructure/network/HttpProvider.ts b/server/src/service/infrastructure/network/HttpProvider.ts index bbc0934f8..02e1f0e82 100644 --- a/server/src/service/infrastructure/network/HttpProvider.ts +++ b/server/src/service/infrastructure/network/HttpProvider.ts @@ -7,13 +7,15 @@ import { Agent as HttpsAgent } from "https"; import { Monitor, MonitorType } from "@/types/monitor.js"; import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js"; import CacheableLookup from "cacheable-lookup"; +import { ISettingsService } from "@/service/system/settingsService.js"; export class HttpProvider implements IStatusProvider { readonly type = "http"; constructor( private got: Got, - private advancedMatcher: IAdvancedMatcher + private advancedMatcher: IAdvancedMatcher, + private settingsService: ISettingsService ) { const cacheable = new CacheableLookup({ maxTtl: 300, errorTtl: 30 }); this.got = got.extend({ @@ -57,14 +59,24 @@ export class HttpProvider implements IStatusProvider { } async handle(monitor: Monitor): Promise> { - const { url, secret, jsonPath, ignoreTlsErrors } = monitor; + const { url, secret, jsonPath, ignoreTlsErrors, customUserAgent } = monitor; if (!url) { throw new Error("URL is required for HTTP monitor"); } + let userAgent: string | undefined = customUserAgent; + if (!userAgent && monitor.type === "http") { + const dbSettings = await this.settingsService.getDBSettings(); + userAgent = dbSettings?.defaultUserAgent ?? undefined; + } + + const headers: Record = {}; + if (secret) headers["Authorization"] = `Bearer ${secret}`; + if (userAgent) headers["User-Agent"] = userAgent; + const options: Record = { - headers: monitor.secret ? { Authorization: `Bearer ${secret}` } : undefined, + headers: Object.keys(headers).length > 0 ? headers : undefined, }; options.agent = { diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index f29ce75d7..cbe78aa4a 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -50,6 +50,7 @@ export interface Monitor { gameId?: string; grpcServiceName?: string; group: string | null; + customUserAgent?: string; geoCheckEnabled?: boolean; geoCheckLocations?: GeoContinent[]; geoCheckInterval?: number; diff --git a/server/src/types/settings.ts b/server/src/types/settings.ts index bcedfbe89..348c433b1 100644 --- a/server/src/types/settings.ts +++ b/server/src/types/settings.ts @@ -31,6 +31,7 @@ export interface Settings { systemEmailRequireTLS: boolean; systemEmailRejectUnauthorized: boolean; showURL: boolean; + defaultUserAgent?: string; singleton: boolean; version: number; globalThresholds?: SettingsThresholds; diff --git a/server/src/validation/monitorValidation.ts b/server/src/validation/monitorValidation.ts index df000ecef..521b5f72b 100644 --- a/server/src/validation/monitorValidation.ts +++ b/server/src/validation/monitorValidation.ts @@ -75,6 +75,7 @@ export const createMonitorBodyValidation = z.object({ grpcServiceName: z.union([z.string(), z.literal("")]).default(""), selectedDisks: z.array(z.string()).optional(), group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(), + customUserAgent: z.string().max(500).optional(), geoCheckEnabled: z.boolean().optional(), geoCheckLocations: z.array(z.enum(GeoContinents)).optional(), geoCheckInterval: z.number().min(300000).optional(), @@ -104,6 +105,7 @@ export const editMonitorBodyValidation = z.object({ grpcServiceName: z.union([z.string(), z.literal("")]).optional(), selectedDisks: z.array(z.string()).optional(), group: z.union([z.string().max(50).trim(), z.null(), z.literal("")]).optional(), + customUserAgent: z.string().max(500).optional(), geoCheckEnabled: z.boolean().optional(), geoCheckLocations: z.array(z.enum(GeoContinents)).optional(), geoCheckInterval: z.number().min(300000).optional(), @@ -157,6 +159,7 @@ const importedMonitorSchema = z.object({ gameId: z.union([z.string(), z.literal("")]).optional(), grpcServiceName: z.union([z.string(), z.literal("")]).default(""), group: z.union([z.string().max(50).trim(), z.null()]).default(null), + customUserAgent: z.string().max(500).optional(), geoCheckEnabled: z.boolean().default(false), geoCheckLocations: z.array(z.enum(GeoContinents)).default([]), geoCheckInterval: z.number().min(300000).default(300000), diff --git a/server/src/validation/settingsValidation.ts b/server/src/validation/settingsValidation.ts index f2c4e4c94..234037c8a 100644 --- a/server/src/validation/settingsValidation.ts +++ b/server/src/validation/settingsValidation.ts @@ -20,6 +20,7 @@ export const updateAppSettingsBodyValidation = z systemEmailTLSServername: z.string().nullable().optional(), showURL: z.boolean().optional(), + defaultUserAgent: z.string().max(500).nullable().optional(), systemEmailSecure: z.boolean().optional(), systemEmailPool: z.boolean().optional(), systemEmailIgnoreTLS: z.boolean().optional(),