Skip to content

Commit 023cf57

Browse files
author
mannilakash
committed
fix: add caching for defaultUA db call and code fixes
1 parent babe5ae commit 023cf57

13 files changed

Lines changed: 89 additions & 19 deletions

File tree

.claude/settings.local.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(gh pr:*)",
5+
"Bash(gh api:*)",
6+
"WebFetch(domain:github.com)",
7+
"Bash(xargs grep:*)",
8+
"Bash(npx tsc:*)"
9+
]
10+
}
11+
}

client/src/Pages/CreateMonitor/index.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,15 @@ const CreateMonitorPage = () => {
813813
fullWidth
814814
error={!!fieldState.error}
815815
helperText={fieldState.error?.message ?? ""}
816+
onChange={(e) => {
817+
field.onChange(
818+
e.target.value
819+
.replace(/[<>]/g, "")
820+
.replace(/javascript:/gi, "")
821+
.replace(/[^\t\x20-\x7E\x80-\xFF]/g, "") // RFC 7230 header-safe chars only
822+
.slice(0, 500)
823+
);
824+
}}
816825
/>
817826
)}
818827
/>

client/src/Pages/Settings/index.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,15 @@ export const SettingsPage = () => {
442442
placeholder={t("pages.settings.form.userAgent.option.placeholder")}
443443
error={!!fieldState.error}
444444
helperText={fieldState.error?.message}
445+
onChange={(e) => {
446+
field.onChange(
447+
e.target.value
448+
.replace(/[<>]/g, "")
449+
.replace(/javascript:/gi, "")
450+
.replace(/[^\t\x20-\x7E\x80-\xFF]/g, "") // RFC 7230 header-safe chars only
451+
.slice(0, 500)
452+
);
453+
}}
445454
/>
446455
)}
447456
/>

client/src/Validation/monitor.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,14 @@ const httpSchema = baseSchema.extend({
3838
matchMethod: z.enum(["equal", "include", "regex", ""]).optional(),
3939
expectedValue: z.string().optional(),
4040
jsonPath: z.string().optional(),
41-
customUserAgent: z.string().max(500).optional(),
41+
customUserAgent: z
42+
.string()
43+
.max(500)
44+
.regex(
45+
/^[\t\x20-\x7E\x80-\xFF]*$/,
46+
"Only printable characters, spaces, and tabs are allowed (RFC 7230 §3.2.6)"
47+
)
48+
.optional(),
4249
});
4350

4451
// Ping monitor schema

client/src/Validation/settings.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ export const settingsSchema = z.object({
1515
defaultUserAgent: z
1616
.string()
1717
.max(500)
18+
.regex(
19+
/^[\t\x20-\x7E\x80-\xFF]*$/,
20+
"Only printable characters, spaces, and tabs are allowed (RFC 7230)"
21+
)
1822
.transform((val) => (val.trim() === "" ? null : val.trim()))
1923
.optional(),
2024
checkTTL: z

client/src/locales/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1064,7 +1064,7 @@
10641064
"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.",
10651065
"option": {
10661066
"label": "Default User-Agent",
1067-
"placeholder": "Checkmate/X.X (uptime monitor; your-instance)"
1067+
"placeholder": "Checkmate/X.X (uptime monitor)"
10681068
}
10691069
},
10701070
"stats": {

server/src/config/services.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ export const initializeServices = async ({
248248

249249
// Network providers
250250
const pingProvider = new PingProvider(ping);
251-
const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath), settingsService);
251+
const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath));
252252
const pageSpeedProvider = new PageSpeedProvider(httpProvider, settingsService, logger);
253253
const hardwareProvider = new HardwareProvider(httpProvider);
254254
const dockerProvider = new DockerProvider(logger, Docker);

server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,15 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
132132
}
133133

134134
// Step 2. Request monitor status
135-
const status = await this.networkService.requestStatus(monitor);
135+
// Resolve default user agent for HTTP monitors (cached, invalidated on settings update)
136+
let effectiveMonitor: Monitor = monitor;
137+
if (monitor.type === "http" && !monitor.customUserAgent) {
138+
const defaultUserAgent = await this.settingsService.getDefaultUserAgent();
139+
if (defaultUserAgent) {
140+
effectiveMonitor = { ...monitor, customUserAgent: defaultUserAgent };
141+
}
142+
}
143+
const status = await this.networkService.requestStatus(effectiveMonitor);
136144
if (!status) {
137145
throw new Error("No network response");
138146
}

server/src/service/infrastructure/network/HttpProvider.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,13 @@ import { Agent as HttpsAgent } from "https";
77
import { Monitor, MonitorType } from "@/types/monitor.js";
88
import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js";
99
import CacheableLookup from "cacheable-lookup";
10-
import { ISettingsService } from "@/service/system/settingsService.js";
1110

1211
export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
1312
readonly type = "http";
1413

1514
constructor(
1615
private got: Got,
17-
private advancedMatcher: IAdvancedMatcher,
18-
private settingsService: ISettingsService
16+
private advancedMatcher: IAdvancedMatcher
1917
) {
2018
const cacheable = new CacheableLookup({ maxTtl: 300, errorTtl: 30 });
2119
this.got = got.extend({
@@ -58,22 +56,27 @@ export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
5856
};
5957
}
6058

59+
private sanitizeHeaderValue(value: string): string {
60+
if (!value) return "";
61+
62+
return value
63+
.replace(/[<>]/g, "")
64+
.replace(/javascript:/gi, "")
65+
.replace(/[^\t\x20-\x7E\x80-\xFF]/g, "") // allow only RFC 7230 header-safe characters
66+
.trim()
67+
.slice(0, 500);
68+
}
69+
6170
async handle<T>(monitor: Monitor): Promise<MonitorStatusResponse<T>> {
6271
const { url, secret, jsonPath, ignoreTlsErrors, customUserAgent } = monitor;
6372

6473
if (!url) {
6574
throw new Error("URL is required for HTTP monitor");
6675
}
6776

68-
let userAgent: string | undefined = customUserAgent;
69-
if (!userAgent && monitor.type === "http") {
70-
const dbSettings = await this.settingsService.getDBSettings();
71-
userAgent = dbSettings?.defaultUserAgent ?? undefined;
72-
}
73-
7477
const headers: Record<string, string> = {};
7578
if (secret) headers["Authorization"] = `Bearer ${secret}`;
76-
if (userAgent) headers["User-Agent"] = userAgent;
79+
if (customUserAgent) headers["User-Agent"] = this.sanitizeHeaderValue(customUserAgent);
7780

7881
const options: Record<string, unknown> = {
7982
headers: Object.keys(headers).length > 0 ? headers : undefined,

server/src/service/system/settingsService.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ export interface ISettingsService {
2020
loadSettings(): EnvConfig;
2121
getSettings(): EnvConfig;
2222
getDBSettings(): Promise<Settings>;
23+
getDefaultUserAgent(): Promise<string | undefined>;
2324
updateDbSettings(newSettings: SettingsUpdate): Promise<Settings>;
2425
}
2526

2627
export class SettingsService implements ISettingsService {
2728
static SERVICE_NAME = SERVICE_NAME;
2829
private settings: EnvConfig;
2930
private settingsRepository: ISettingsRepository | null = null;
31+
private cachedDefaultUserAgent: string | null | undefined = undefined;
3032

3133
constructor(env: ValidatedEnv) {
3234
this.settings = {
@@ -70,7 +72,17 @@ export class SettingsService implements ISettingsService {
7072
return this.settingsRepository;
7173
}
7274

75+
getDefaultUserAgent = async (): Promise<string | undefined> => {
76+
if (this.cachedDefaultUserAgent !== undefined) {
77+
return this.cachedDefaultUserAgent ?? undefined;
78+
}
79+
const settings = await this.getDBSettings();
80+
this.cachedDefaultUserAgent = settings.defaultUserAgent ?? null;
81+
return this.cachedDefaultUserAgent ?? undefined;
82+
};
83+
7384
updateDbSettings = async (newSettings: SettingsUpdate) => {
85+
this.cachedDefaultUserAgent = newSettings.defaultUserAgent ?? null;
7486
return await this.getRepository().update(newSettings);
7587
};
7688

0 commit comments

Comments
 (0)