Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/src/Hooks/useMonitorForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const useMonitorForm = ({
matchMethod: data?.matchMethod || "",
expectedValue: data?.expectedValue || "",
jsonPath: data?.jsonPath || "",
customUserAgent: data?.customUserAgent || "",
};
break;
case "ping":
Expand Down
1 change: 1 addition & 0 deletions client/src/Hooks/useSettingsForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) =>
: 80,
},
checkTTL: data?.checkTTL ?? 30,
defaultUserAgent: data?.defaultUserAgent || "",
pagespeedApiKey: "",
systemEmailPassword: "",
};
Expand Down
34 changes: 34 additions & 0 deletions client/src/Pages/CreateMonitor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,40 @@ 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 ?? ""}
onChange={(e) => {
field.onChange(
e.target.value
.replace(/[<>]/g, "")
.replace(/(?:javascript|data|vbscript):/gi, "")
.replace(/[^\t\x20-\x7E\x80-\xFF]/g, "") // RFC 7230 header-safe chars only
Comment thread
akashmannil marked this conversation as resolved.
Fixed
.slice(0, 500)
);
}}
/>
)}
/>
}
/>
)}

{watchedType === "http" && (
<ConfigBox
title={t("pages.createMonitor.form.advanced.title")}
Expand Down
33 changes: 33 additions & 0 deletions client/src/Pages/Settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,39 @@ 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}
onChange={(e) => {
field.onChange(
e.target.value
.replace(/[<>]/g, "")
.replace(/(?:javascript|data|vbscript):/gi, "")
.replace(/[^\t\x20-\x7E\x80-\xFF]/g, "") // RFC 7230 header-safe chars only
Comment thread
akashmannil marked this conversation as resolved.
Fixed
.slice(0, 500)
);
}}
/>
)}
/>
}
/>
)}

{/* Clear All Stats */}
{isAdmin && (
<ConfigBox
Expand Down
1 change: 1 addition & 0 deletions client/src/Types/Monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export interface Monitor {
gameId?: string;
grpcServiceName?: string;
group: string | null;
customUserAgent?: string | null;
geoCheckEnabled?: boolean;
geoCheckLocations?: GeoContinent[];
geoCheckInterval?: number;
Expand Down
1 change: 1 addition & 0 deletions client/src/Types/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface Settings {
systemEmailRequireTLS: boolean;
systemEmailRejectUnauthorized: boolean;
showURL: boolean;
defaultUserAgent?: string;
singleton: boolean;
globalThresholds?: SettingsThresholds;
createdAt: string;
Expand Down
10 changes: 10 additions & 0 deletions client/src/Validation/monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ const httpSchema = baseSchema.extend({
matchMethod: z.enum(["equal", "include", "regex", ""]).optional(),
expectedValue: z.string().optional(),
jsonPath: z.string().optional(),
customUserAgent: z
.string()
.max(500)
.regex(
/^[\t\x20-\x7E\x80-\xFF]*$/,
"Only printable characters, spaces, and tabs are allowed (RFC 7230 §3.2.6)"
)
.transform((val) => (val.trim() === "" ? null : val.trim()))
.nullable()
.optional(),
});

// Ping monitor schema
Expand Down
9 changes: 9 additions & 0 deletions client/src/Validation/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ export const settingsSchema = z.object({
systemEmailSecure: z.boolean().optional(),
systemEmailPool: z.boolean().optional(),
showURL: z.boolean().optional(),
defaultUserAgent: z
.string()
.max(500)
.regex(
/^[\t\x20-\x7E\x80-\xFF]*$/,
"Only printable characters, spaces, and tabs are allowed (RFC 7230)"
)
.transform((val) => (val.trim() === "" ? null : val.trim()))
.optional(),
checkTTL: z
.number()
.int()
Expand Down
16 changes: 16 additions & 0 deletions client/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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)"
}
},
"stats": {
"title": "Monitor history",
"description": "Clear all monitoring history and statistics for your team. This action is irreversible.",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Pool } from "pg";

export const addUserAgentFields = async (pool: Pool) => {
await pool.query(`
ALTER TABLE monitors
ADD COLUMN IF NOT EXISTS custom_user_agent TEXT;

ALTER TABLE app_settings
ADD COLUMN IF NOT EXISTS default_user_agent TEXT;
`);
};

export const dropUserAgentFields = async (pool: Pool) => {
await pool.query(`
ALTER TABLE monitors
DROP COLUMN IF EXISTS custom_user_agent;

ALTER TABLE app_settings
DROP COLUMN IF EXISTS default_user_agent;
`);
};
2 changes: 2 additions & 0 deletions server/src/db/migration/timescaledb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { createStatusPages, dropStatusPages } from "./0018_create_status_pages.j
import { createAppSettings, dropAppSettings } from "./0019_create_app_settings.js";
import { createContinuousAggregates, dropContinuousAggregates } from "./0020_create_continuous_aggregates.js";
import { createRetentionCompression, dropRetentionCompression } from "./0021_create_retention_compression.js";
import { addUserAgentFields, dropUserAgentFields } from "./0022_add_user_agent_fields.js";

const SERVICE_NAME = "TimescaleDB Migrations";

Expand Down Expand Up @@ -52,6 +53,7 @@ const migrations: MigrationEntry[] = [
{ name: "0019_create_app_settings", up: createAppSettings, down: dropAppSettings },
{ name: "0020_create_continuous_aggregates", up: createContinuousAggregates, down: dropContinuousAggregates },
{ name: "0021_create_retention_compression", up: createRetentionCompression, down: dropRetentionCompression },
{ name: "0022_add_user_agent_fields", up: addUserAgentFields, down: dropUserAgentFields },
];

const ensureMigrationsTable = async (pool: Pool) => {
Expand Down
1 change: 1 addition & 0 deletions server/src/db/models/AppSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
3 changes: 3 additions & 0 deletions server/src/db/models/Monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,9 @@ const MonitorSchema = new Schema<MonitorDocument>(
return value && value.trim() ? value.trim() : null;
},
},
customUserAgent: {
type: String,
},
geoCheckEnabled: {
type: Boolean,
default: false,
Expand Down
2 changes: 2 additions & 0 deletions server/src/repositories/monitors/MongoMonitorsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? [],
Expand Down Expand Up @@ -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 ?? [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ interface MonitorRow {
game_id: string | null;
grpc_service_name: string | null;
monitor_group: string | null;
custom_user_agent: string | null;
geo_check_enabled: boolean;
geo_check_locations: GeoContinent[] | null;
geo_check_interval_ms: number;
Expand All @@ -49,7 +50,7 @@ const MONITOR_COLUMNS = `id, user_id, team_id, name, description, type, status,
interval_ms, is_active, status_window, status_window_size, status_window_threshold, uptime_percentage,
cpu_alert_threshold, cpu_alert_counter, memory_alert_threshold, memory_alert_counter,
disk_alert_threshold, disk_alert_counter, temp_alert_threshold, temp_alert_counter, selected_disks,
game_id, grpc_service_name, monitor_group, geo_check_enabled, geo_check_locations, geo_check_interval_ms,
game_id, grpc_service_name, monitor_group, custom_user_agent, geo_check_enabled, geo_check_locations, geo_check_interval_ms,
created_at, updated_at`;

export class TimescaleMonitorsRepository implements IMonitorsRepository {
Expand All @@ -62,8 +63,8 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository {
interval_ms, is_active, status_window, status_window_size, status_window_threshold,
cpu_alert_threshold, cpu_alert_counter, memory_alert_threshold, memory_alert_counter,
disk_alert_threshold, disk_alert_counter, temp_alert_threshold, temp_alert_counter, selected_disks,
game_id, grpc_service_name, monitor_group, geo_check_enabled, geo_check_locations, geo_check_interval_ms)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34)
game_id, grpc_service_name, monitor_group, custom_user_agent, geo_check_enabled, geo_check_locations, geo_check_interval_ms)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34,$35)
RETURNING ${MONITOR_COLUMNS}`,
[
userId,
Expand Down Expand Up @@ -97,6 +98,7 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository {
monitor.gameId ?? null,
monitor.grpcServiceName ?? null,
monitor.group ?? null,
monitor.customUserAgent ?? null,
monitor.geoCheckEnabled ?? false,
monitor.geoCheckLocations ?? [],
monitor.geoCheckInterval ?? 300000,
Expand Down Expand Up @@ -595,6 +597,7 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository {
["gameId", "game_id"],
["grpcServiceName", "grpc_service_name"],
["group", "monitor_group"],
["customUserAgent", "custom_user_agent"],
["geoCheckEnabled", "geo_check_enabled"],
["geoCheckLocations", "geo_check_locations"],
["geoCheckInterval", "geo_check_interval_ms"],
Expand Down Expand Up @@ -1034,6 +1037,7 @@ export class TimescaleMonitorsRepository implements IMonitorsRepository {
gameId: row.game_id ?? undefined,
grpcServiceName: row.grpc_service_name ?? undefined,
group: row.monitor_group,
customUserAgent: row.custom_user_agent ?? undefined,
geoCheckEnabled: row.geo_check_enabled,
geoCheckLocations: row.geo_check_locations ?? [],
geoCheckInterval: row.geo_check_interval_ms,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface SettingsRow {
system_email_require_tls: boolean | null;
system_email_reject_unauthorized: boolean | null;
show_url: boolean | null;
default_user_agent: string | null;
version: string | null;
threshold_cpu_usage: number | null;
threshold_memory_usage: number | null;
Expand All @@ -34,7 +35,7 @@ const COLUMNS = `id, check_ttl, language, jwt_secret, pagespeed_api_key,
system_email_host, system_email_port, system_email_address, system_email_password, system_email_user,
system_email_connection_host, system_email_tls_servername, system_email_secure, system_email_pool,
system_email_ignore_tls, system_email_require_tls, system_email_reject_unauthorized,
show_url, version, threshold_cpu_usage, threshold_memory_usage, threshold_disk_usage, threshold_temperature,
show_url, default_user_agent, version, threshold_cpu_usage, threshold_memory_usage, threshold_disk_usage, threshold_temperature,
created_at, updated_at`;

export class TimescaleSettingsRepository implements ISettingsRepository {
Expand All @@ -46,8 +47,8 @@ export class TimescaleSettingsRepository implements ISettingsRepository {
system_email_host, system_email_port, system_email_address, system_email_password, system_email_user,
system_email_connection_host, system_email_tls_servername, system_email_secure, system_email_pool,
system_email_ignore_tls, system_email_require_tls, system_email_reject_unauthorized,
show_url, version, threshold_cpu_usage, threshold_memory_usage, threshold_disk_usage, threshold_temperature)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22)
show_url, default_user_agent, version, threshold_cpu_usage, threshold_memory_usage, threshold_disk_usage, threshold_temperature)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23)
RETURNING ${COLUMNS}`,
[
settings.checkTTL ?? 30,
Expand All @@ -67,6 +68,7 @@ export class TimescaleSettingsRepository implements ISettingsRepository {
settings.systemEmailRequireTLS ?? false,
settings.systemEmailRejectUnauthorized ?? true,
settings.showURL ?? false,
settings.defaultUserAgent ?? null,
settings.version ?? 1,
settings.globalThresholds?.cpu ?? null,
settings.globalThresholds?.memory ?? null,
Expand Down Expand Up @@ -113,6 +115,7 @@ export class TimescaleSettingsRepository implements ISettingsRepository {
["systemEmailRequireTLS", "system_email_require_tls"],
["systemEmailRejectUnauthorized", "system_email_reject_unauthorized"],
["showURL", "show_url"],
["defaultUserAgent", "default_user_agent"],
["version", "version"],
];

Expand Down Expand Up @@ -197,6 +200,7 @@ export class TimescaleSettingsRepository implements ISettingsRepository {
systemEmailRequireTLS: row.system_email_require_tls ?? false,
systemEmailRejectUnauthorized: row.system_email_reject_unauthorized ?? true,
showURL: row.show_url ?? false,
defaultUserAgent: row.default_user_agent ?? undefined,
singleton: true,
version: Number(row.version ?? 1),
globalThresholds:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,15 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
}

// Step 2. Request monitor status
const status = await this.networkService.requestStatus(monitor);
// Resolve default user agent for HTTP monitors (cached, invalidated on settings update)
let effectiveMonitor: Monitor = monitor;
if (monitor.type === "http" && !monitor.customUserAgent) {
const defaultUserAgent = await this.settingsService.getDefaultUserAgent();
if (defaultUserAgent) {
effectiveMonitor = { ...monitor, customUserAgent: defaultUserAgent };
}
}
const status = await this.networkService.requestStatus(effectiveMonitor);
if (!status) {
throw new Error("No network response");
}
Expand Down
Loading
Loading