feat: adding custom/default user agent support for http monitors
This commit is contained in:
1 parent
4222d39a8b
commit
babe5ae177
19 files changed
+104
-4
No files matched your search
@@ -40,6 +40,7 @@ export const useMonitorForm = ({
|
||||
matchMethod: data?.matchMethod || "",
|
||||
expectedValue: data?.expectedValue || "",
|
||||
jsonPath: data?.jsonPath || "",
|
||||
customUserAgent: data?.customUserAgent || "",
|
||||
};
|
||||
break;
|
||||
case "ping":
|
||||
|
||||
@@ -40,6 +40,7 @@ export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) =>
|
||||
: 80,
|
||||
},
|
||||
checkTTL: data?.checkTTL ?? 30,
|
||||
defaultUserAgent: data?.defaultUserAgent || "",
|
||||
pagespeedApiKey: "",
|
||||
systemEmailPassword: "",
|
||||
};
|
||||
|
||||
@@ -795,6 +795,31 @@ const CreateMonitorPage = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{watchedType === "http" && (
|
||||
<ConfigBox
|
||||
title={t("pages.createMonitor.form.userAgent.title")}
|
||||
subtitle={t("pages.createMonitor.form.userAgent.description")}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="customUserAgent"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
type="text"
|
||||
fieldLabel={t("pages.createMonitor.form.userAgent.option.label")}
|
||||
placeholder={t("pages.createMonitor.form.userAgent.option.placeholder")}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{watchedType === "http" && (
|
||||
<ConfigBox
|
||||
title={t("pages.createMonitor.form.advanced.title")}
|
||||
|
||||
@@ -425,6 +425,30 @@ export const SettingsPage = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Default User-Agent - Admin Only */}
|
||||
{isAdmin && (
|
||||
<ConfigBox
|
||||
title={t("pages.settings.form.userAgent.title")}
|
||||
subtitle={t("pages.settings.form.userAgent.description")}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="defaultUserAgent"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
fieldLabel={t("pages.settings.form.userAgent.option.label")}
|
||||
placeholder={t("pages.settings.form.userAgent.option.placeholder")}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Clear All Stats */}
|
||||
{isAdmin && (
|
||||
<ConfigBox
|
||||
|
||||
@@ -73,6 +73,7 @@ export interface Monitor {
|
||||
gameId?: string;
|
||||
grpcServiceName?: string;
|
||||
group: string | null;
|
||||
customUserAgent?: string;
|
||||
geoCheckEnabled?: boolean;
|
||||
geoCheckLocations?: GeoContinent[];
|
||||
geoCheckInterval?: number;
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface Settings {
|
||||
systemEmailRequireTLS: boolean;
|
||||
systemEmailRejectUnauthorized: boolean;
|
||||
showURL: boolean;
|
||||
defaultUserAgent?: string;
|
||||
singleton: boolean;
|
||||
globalThresholds?: SettingsThresholds;
|
||||
createdAt: string;
|
||||
|
||||
@@ -38,6 +38,7 @@ const httpSchema = baseSchema.extend({
|
||||
matchMethod: z.enum(["equal", "include", "regex", ""]).optional(),
|
||||
expectedValue: z.string().optional(),
|
||||
jsonPath: z.string().optional(),
|
||||
customUserAgent: z.string().max(500).optional(),
|
||||
});
|
||||
|
||||
// Ping monitor schema
|
||||
|
||||
@@ -12,6 +12,11 @@ export const settingsSchema = z.object({
|
||||
systemEmailSecure: z.boolean().optional(),
|
||||
systemEmailPool: z.boolean().optional(),
|
||||
showURL: z.boolean().optional(),
|
||||
defaultUserAgent: z
|
||||
.string()
|
||||
.max(500)
|
||||
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||
.optional(),
|
||||
checkTTL: z
|
||||
.number()
|
||||
.int()
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -36,6 +36,7 @@ const AppSettingsSchema = new Schema<AppSettingsDocument>(
|
||||
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 },
|
||||
|
||||
@@ -339,6 +339,9 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
||||
return value && value.trim() ? value.trim() : null;
|
||||
},
|
||||
},
|
||||
customUserAgent: {
|
||||
type: String,
|
||||
},
|
||||
geoCheckEnabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
||||
@@ -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 ?? [],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<HttpStatusPayload> {
|
||||
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<HttpStatusPayload> {
|
||||
}
|
||||
|
||||
async handle<T>(monitor: Monitor): Promise<MonitorStatusResponse<T>> {
|
||||
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<string, string> = {};
|
||||
if (secret) headers["Authorization"] = `Bearer ${secret}`;
|
||||
if (userAgent) headers["User-Agent"] = userAgent;
|
||||
|
||||
const options: Record<string, unknown> = {
|
||||
headers: monitor.secret ? { Authorization: `Bearer ${secret}` } : undefined,
|
||||
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
||||
};
|
||||
|
||||
options.agent = {
|
||||
|
||||
@@ -50,6 +50,7 @@ export interface Monitor {
|
||||
gameId?: string;
|
||||
grpcServiceName?: string;
|
||||
group: string | null;
|
||||
customUserAgent?: string;
|
||||
geoCheckEnabled?: boolean;
|
||||
geoCheckLocations?: GeoContinent[];
|
||||
geoCheckInterval?: number;
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface Settings {
|
||||
systemEmailRequireTLS: boolean;
|
||||
systemEmailRejectUnauthorized: boolean;
|
||||
showURL: boolean;
|
||||
defaultUserAgent?: string;
|
||||
singleton: boolean;
|
||||
version: number;
|
||||
globalThresholds?: SettingsThresholds;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in new issue
Block a user