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
2 changes: 2 additions & 0 deletions client/src/Hooks/useMonitorForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const getMonitorDefaults = (
expectedValue: data?.expectedValue || "",
jsonPath: data?.jsonPath || "",
customUpCodes: data?.customUpCodes || [],
headers: data?.headers || [],
};
break;
case "ping":
Expand Down Expand Up @@ -151,6 +152,7 @@ export const getMonitorDefaults = (
expectedValue: "",
jsonPath: "",
customUpCodes: [],
headers: [],
};
}

Expand Down
70 changes: 69 additions & 1 deletion client/src/Pages/CreateMonitor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { useEffect } from "react";
import { logger } from "@/Utils/logger";
import { ALL_HTTP_STATUS_CODES } from "@/Utils/statusCode";
import { useParams, useLocation, useNavigate } from "react-router";
import { useForm, FormProvider } from "react-hook-form";
import { useForm, useFieldArray, FormProvider } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTheme } from "@mui/material";
import Stack from "@mui/material/Stack";
import IconButton from "@mui/material/IconButton";
import { Trans, useTranslation } from "react-i18next";
import Typography from "@mui/material/Typography";
import Link from "@mui/material/Link";
import { Trash2 } from "lucide-react";
import { HeaderDeleteControls } from "@/Components/monitors";
import { GeoContinents } from "@/Types/GeoCheck";

Expand Down Expand Up @@ -286,6 +288,14 @@ const CreateMonitorPage = () => {
defaultValues: defaults,
});
const { watch, handleSubmit, clearErrors, trigger, reset, setValue } = form;
const {
fields: headerFields,
append: appendHeader,
remove: removeHeader,
} = useFieldArray({ control: form.control, name: "headers" });
// Cross-field errors (duplicate names) live on the array root, which
// getFieldState surfaces without reaching into the union-typed error object.
const headersError = form.getFieldState("headers", form.formState).error?.root?.message;

useEffect(() => {
reset(defaults);
Expand Down Expand Up @@ -939,6 +949,64 @@ const CreateMonitorPage = () => {
/>
)}

{showStep(2) && watchedType === "http" && (
<ConfigBox
title={t("pages.createMonitor.form.headers.title")}
subtitle={t("pages.createMonitor.form.headers.description")}
rightContent={
<Stack spacing={theme.spacing(LAYOUT.MD)}>
{headerFields.map((field, index) => (
<Stack
key={field.id}
direction={{ xs: "column", md: "row" }}
alignItems={{ xs: "stretch", md: "flex-start" }}
spacing={theme.spacing(LAYOUT.SM)}
>
<FormTextField
name={`headers.${index}.key`}
placeholder={t(
"pages.createMonitor.form.headers.option.name.placeholder"
)}
/>
<FormTextField
name={`headers.${index}.value`}
placeholder={t(
"pages.createMonitor.form.headers.option.value.placeholder"
)}
/>
<IconButton
size="small"
onClick={() => removeHeader(index)}
aria-label={t(
"pages.createMonitor.form.headers.option.removeAriaLabel"
)}
>
<Trash2 size={16} />
</IconButton>
</Stack>
))}
<Button
type="button"
variant="outlined"
color="secondary"
onClick={() => appendHeader({ key: "", value: "" })}
sx={{ alignSelf: "flex-start" }}
>
{t("pages.createMonitor.form.headers.option.addButton")}
</Button>
{headersError && (
<Typography
variant="caption"
color={theme.palette.error.main}
>
{headersError}
</Typography>
)}
</Stack>
}
/>
)}

{showStep(2) && supportsGeoCheck(watchedType) && (
<ConfigBox
title={t("pages.createMonitor.form.geoChecks.title")}
Expand Down
6 changes: 6 additions & 0 deletions client/src/Types/Monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ export const ProxyModes = ["inherit", "none", "custom"] as const;
export type ProxyMode = (typeof ProxyModes)[number];
export const DefaultProxyMode: ProxyMode = "inherit";

export interface MonitorHeader {
key: string;
value: string;
}

export interface Monitor {
id: string;
userId: string;
Expand Down Expand Up @@ -141,6 +146,7 @@ export interface Monitor {
tags: string[];
customUpCodes?: number[];
secret?: string;
headers?: MonitorHeader[];
cpuAlertThreshold: number;
cpuAlertCounter: number;
memoryAlertThreshold: number;
Expand Down
47 changes: 47 additions & 0 deletions client/src/Validation/monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,52 @@ const httpStatusCode = z.number().refine((code) => httpStatusCodeSet.has(code),
message: "Must be a valid HTTP status code",
});

// Request headers sent with every check. Names are RFC 9110 tokens and values
// are printable ASCII, so a malformed header is caught in the form instead of
// failing at request time and reporting the monitor as down. Connection-level
// headers are managed by the HTTP client and would break the request if
// overridden. Kept in sync with server/src/api/validation/monitorValidation.ts.
const headerNameRegex = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
const headerValueRegex = /^[\x21-\x7e](?:[\x20-\x7e\t]*[\x21-\x7e])?$/;
const reservedHeaderNames = new Set([
"host",
"content-length",
"transfer-encoding",
"connection",
"upgrade",
"keep-alive",
]);

const headersSchema = z
.array(
z.object({
key: z
.string()
.min(1, "Header name is required")
.regex(
headerNameRegex,
"Header name may only contain letters, digits and !#$%&'*+-.^_`|~"
)
.refine((key) => !reservedHeaderNames.has(key.toLowerCase()), {
message: "This header is set automatically and cannot be overridden",
}),
value: z
.string()
.min(1, "Header value is required")
.regex(
headerValueRegex,
"Header value must be printable ASCII without leading or trailing whitespace"
),
})
)
.refine(
(headers) => {
const keys = headers.map((h) => h.key.toLowerCase());
return new Set(keys).size === keys.length;
},
{ message: "Duplicate header names are not allowed" }
);

const httpSchema = baseSchema.extend({
type: z.literal("http"),
url: urlSchema,
Expand All @@ -88,6 +134,7 @@ const httpSchema = baseSchema.extend({
.array(httpStatusCode)
.optional()
.register(monitorStepRegistry, { step: 2 }),
headers: headersSchema.optional().register(monitorStepRegistry, { step: 2 }),
...geoCheckFields,
});

Expand Down
14 changes: 14 additions & 0 deletions client/src/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,20 @@
"dns": "راقب ما إذا كان خادم DNS محدد يستجيب."
}
},
"headers": {
"title": "ترويسات الطلب",
"description": "ترويسات مخصصة تُرسل مع كل طلب لهذا المراقب.",
"option": {
"name": {
"placeholder": "اسم الترويسة"
},
"value": {
"placeholder": "قيمة الترويسة"
},
"addButton": "إضافة ترويسة",
"removeAriaLabel": "إزالة الترويسة"
}
},
"ignoreTls": {
"description": "تهيئة التحقق من شهادة TLS/SSL لاتصالات HTTPS.",
"option": {
Expand Down
14 changes: 14 additions & 0 deletions client/src/locales/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,20 @@
"dns": "Monitoritza si un servidor DNS específic respon."
}
},
"headers": {
"title": "Capçaleres de la sol·licitud",
"description": "Capçaleres personalitzades que s'envien amb cada sol·licitud d'aquest monitor.",
"option": {
"name": {
"placeholder": "Nom de la capçalera"
},
"value": {
"placeholder": "Valor de la capçalera"
},
"addButton": "Afegeix una capçalera",
"removeAriaLabel": "Elimina la capçalera"
}
},
"ignoreTls": {
"description": "Configura la validació del certificat TLS/SSL per a connexions HTTPS.",
"option": {
Expand Down
14 changes: 14 additions & 0 deletions client/src/locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,20 @@
"dns": "Sledujte, zda konkrétní DNS server odpovídá."
}
},
"headers": {
"title": "Hlavičky požadavku",
"description": "Vlastní hlavičky odesílané s každým požadavkem tohoto monitoru.",
"option": {
"name": {
"placeholder": "Název hlavičky"
},
"value": {
"placeholder": "Hodnota hlavičky"
},
"addButton": "Přidat hlavičku",
"removeAriaLabel": "Odebrat hlavičku"
}
},
"ignoreTls": {
"description": "Nastavte ověřování TLS/SSL certifikátů pro HTTPS připojení.",
"option": {
Expand Down
14 changes: 14 additions & 0 deletions client/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,20 @@
"dns": "Überwachen Sie, ob ein bestimmter DNS-Server antwortet."
}
},
"headers": {
"title": "Anfrage-Header",
"description": "Benutzerdefinierte Header, die mit jeder Anfrage dieses Monitors gesendet werden.",
"option": {
"name": {
"placeholder": "Header-Name"
},
"value": {
"placeholder": "Header-Wert"
},
"addButton": "Header hinzufügen",
"removeAriaLabel": "Header entfernen"
}
},
"ignoreTls": {
"description": "Konfigurieren Sie die TLS/SSL-Zertifikatsvalidierung für HTTPS-Verbindungen.",
"option": {
Expand Down
14 changes: 14 additions & 0 deletions client/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,20 @@
"dns": "Monitor if a specific DNS server is responding."
}
},
"headers": {
"title": "Request headers",
"description": "Custom headers sent with every request for this monitor.",
"option": {
"name": {
"placeholder": "Header name"
},
"value": {
"placeholder": "Header value"
},
"addButton": "Add header",
"removeAriaLabel": "Remove header"
}
},
"ignoreTls": {
"description": "Configure TLS/SSL certificate validation for HTTPS connections.",
"option": {
Expand Down
14 changes: 14 additions & 0 deletions client/src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,20 @@
"dns": "Monitorea si un servidor DNS específico responde."
}
},
"headers": {
"title": "Cabeceras de la solicitud",
"description": "Cabeceras personalizadas que se envían con cada solicitud de este monitor.",
"option": {
"name": {
"placeholder": "Nombre de la cabecera"
},
"value": {
"placeholder": "Valor de la cabecera"
},
"addButton": "Añadir cabecera",
"removeAriaLabel": "Eliminar cabecera"
}
},
"ignoreTls": {
"description": "Configure la validación de certificados TLS/SSL para conexiones HTTPS.",
"option": {
Expand Down
14 changes: 14 additions & 0 deletions client/src/locales/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,20 @@
"dns": "Tarkkaile, vastaako tietty DNS-palvelin."
}
},
"headers": {
"title": "Pyynnön otsakkeet",
"description": "Mukautetut otsakkeet, jotka lähetetään tämän monitorin jokaisen pyynnön mukana.",
"option": {
"name": {
"placeholder": "Otsakkeen nimi"
},
"value": {
"placeholder": "Otsakkeen arvo"
},
"addButton": "Lisää otsake",
"removeAriaLabel": "Poista otsake"
}
},
"ignoreTls": {
"description": "Määritä TLS/SSL-varmenteen vahvistus HTTPS-yhteyksiä varten.",
"option": {
Expand Down
14 changes: 14 additions & 0 deletions client/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,20 @@
"dns": "Surveillez si un serveur DNS spécifique répond."
}
},
"headers": {
"title": "En-têtes de requête",
"description": "En-têtes personnalisés envoyés avec chaque requête de ce moniteur.",
"option": {
"name": {
"placeholder": "Nom de l'en-tête"
},
"value": {
"placeholder": "Valeur de l'en-tête"
},
"addButton": "Ajouter un en-tête",
"removeAriaLabel": "Supprimer l'en-tête"
}
},
"ignoreTls": {
"description": "Configurez la validation des certificats TLS/SSL pour les connexions HTTPS.",
"option": {
Expand Down
14 changes: 14 additions & 0 deletions client/src/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,20 @@
"dns": "Monitora se uno specifico server DNS sta rispondendo."
}
},
"headers": {
"title": "Intestazioni della richiesta",
"description": "Intestazioni personalizzate inviate con ogni richiesta di questo monitor.",
"option": {
"name": {
"placeholder": "Nome dell'intestazione"
},
"value": {
"placeholder": "Valore dell'intestazione"
},
"addButton": "Aggiungi intestazione",
"removeAriaLabel": "Rimuovi intestazione"
}
},
"ignoreTls": {
"description": "Configura la validazione del certificato TLS/SSL per le connessioni HTTPS.",
"option": {
Expand Down
Loading
Loading