add validation, helpers, tests
This commit is contained in:
1 parent
131fd1a6b0
commit
5eb16fdbdb
13 files changed
+249
-13
No files matched your search
@@ -15,6 +15,7 @@ import {
|
||||
import { DateRanges, SortOrders } from "@/types/query.js";
|
||||
import { DockerContainerStates, DockerHealthStatuses, DockerLogStreams, DockerPortProtocols } from "@/domain/docker/docker.type.js";
|
||||
import { DOCKER_LOG_PAGE_DEFAULT, DOCKER_LOG_PAGE_MAX } from "@/domain/docker/docker-log.type.js";
|
||||
import { isDockerSocketUrl, isDockerTlsUrl } from "@/utils/dockerHost.js";
|
||||
|
||||
const httpStatusCode = z.number().refine((code) => HttpStatusCodeSet.has(code), { message: "Must be a valid HTTP status code" });
|
||||
|
||||
@@ -120,15 +121,13 @@ const refineProxySelection = (body: { proxyMode?: string; proxyId?: string }, ct
|
||||
}
|
||||
};
|
||||
|
||||
const dockerUrlRegex = /^(?:unix:\/\/\/\S+|\/\S+)$/;
|
||||
|
||||
const refineDockerUrl = (data: { type?: string; url?: string }, ctx: z.RefinementCtx) => {
|
||||
if (data.type !== "docker" || data.url === undefined) return;
|
||||
if (!dockerUrlRegex.test(data.url)) {
|
||||
if (!isDockerSocketUrl(data.url) && !isDockerTlsUrl(data.url)) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
path: ["url"],
|
||||
message: "Docker host must be unix:///path or an absolute socket path",
|
||||
message: "Docker host must be unix:///path, an absolute socket path, or tcp://host[:port]",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -170,6 +169,9 @@ export const createMonitorBodyValidation = z
|
||||
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(),
|
||||
geoCheckInterval: z.number().min(300000).optional(),
|
||||
dockerLogsEnabled: z.boolean().optional(),
|
||||
dockerTlsCa: z.union([z.string(), z.literal("")]).optional(),
|
||||
dockerTlsCert: z.union([z.string(), z.literal("")]).optional(),
|
||||
dockerTlsKey: z.union([z.string(), z.literal("")]).optional(),
|
||||
dnsServer: dnsServerValidation.optional(),
|
||||
dnsRecordType: z.enum(DnsRecordTypes).optional(),
|
||||
})
|
||||
@@ -215,6 +217,9 @@ export const editMonitorBodyValidation = z
|
||||
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(),
|
||||
geoCheckInterval: z.number().min(300000).optional(),
|
||||
dockerLogsEnabled: z.boolean().optional(),
|
||||
dockerTlsCa: z.union([z.string(), z.literal("")]).optional(),
|
||||
dockerTlsCert: z.union([z.string(), z.literal("")]).optional(),
|
||||
dockerTlsKey: z.union([z.string(), z.literal("")]).optional(),
|
||||
dnsServer: dnsServerValidation.optional(),
|
||||
dnsRecordType: z.enum(DnsRecordTypes).optional(),
|
||||
})
|
||||
@@ -291,7 +296,6 @@ const importedMonitorSchema = z
|
||||
geoCheckLocations: z.array(z.enum(GeoContinents)).default([]),
|
||||
geoCheckInterval: z.number().min(300000).default(300000),
|
||||
dockerLogsEnabled: z.boolean().default(false),
|
||||
|
||||
dnsServer: dnsServerValidation.optional(),
|
||||
dnsRecordType: z.enum(DnsRecordTypes).optional(),
|
||||
createdAt: z.string().optional(),
|
||||
@@ -381,6 +385,9 @@ export const monitorResponseSchema = z
|
||||
geoCheckLocations: z.array(z.enum(GeoContinents)),
|
||||
geoCheckInterval: z.number(),
|
||||
dockerLogsEnabled: z.boolean(),
|
||||
dockerTlsCa: z.string().optional(),
|
||||
dockerTlsCert: z.string().optional(),
|
||||
dockerTlsKeySet: z.boolean().optional(),
|
||||
dnsServer: z.string().optional(),
|
||||
dnsRecordType: z.enum(DnsRecordTypes).optional(),
|
||||
teamId: z.string(),
|
||||
|
||||
@@ -26,6 +26,8 @@ import Dockerode from "dockerode";
|
||||
import { timeRequest } from "@/service/network/utils.js";
|
||||
import { NETWORK_ERROR } from "@/types/network.js";
|
||||
import { IEncryptionService } from "@/service/encryptionService.js";
|
||||
import { DOCKER_TLS_URL, isDockerTlsUrl } from "@/utils/dockerHost.js";
|
||||
import { splitCertificateBundle } from "@/utils/pem.js";
|
||||
|
||||
type DockerodeType = typeof Dockerode;
|
||||
type DockerOptions = Dockerode.DockerOptions & { agent?: https.Agent };
|
||||
@@ -37,8 +39,6 @@ const DOCKER_LOG_MAX_LINE_BYTES = 4096;
|
||||
const DOCKER_LOG_TRUNCATION_MARKER = " …[truncated]";
|
||||
const DOCKER_LOG_TS_REGEX = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{1,9}))?Z /;
|
||||
const DOCKER_TLS_DEFAULT_PORT = 2376;
|
||||
const DOCKER_TLS_URL = /^(tcp|https):\/\/([^/\s:]+)(?::(\d+))?\/?$/;
|
||||
const PEM_CERT_BLOCK = /-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g;
|
||||
|
||||
export interface DockerError extends Error {
|
||||
statusCode?: number;
|
||||
@@ -60,8 +60,7 @@ export class DockerProvider implements IStatusProvider<DockerStatusPayload> {
|
||||
}
|
||||
|
||||
private resolveTlsCredentials = (monitor: Monitor, ctx?: CheckContext): TlsCredentials | undefined => {
|
||||
const url = monitor.url?.trim() ?? "";
|
||||
if (!DOCKER_TLS_URL.test(url)) return undefined;
|
||||
if (!isDockerTlsUrl(monitor.url)) return undefined;
|
||||
if (!ctx?.dockerTlsKey) return { ok: false, message: "Docker TLS key is missing" };
|
||||
try {
|
||||
return { ok: true, key: this.encryptionService.decrypt(ctx.dockerTlsKey) };
|
||||
@@ -79,9 +78,8 @@ export class DockerProvider implements IStatusProvider<DockerStatusPayload> {
|
||||
}
|
||||
|
||||
private splitCaBundle = (pem: string | undefined): string[] | undefined => {
|
||||
if (!pem) return undefined;
|
||||
const blocks = pem.match(PEM_CERT_BLOCK);
|
||||
return blocks && blocks.length > 0 ? blocks : undefined;
|
||||
const blocks = pem ? splitCertificateBundle(pem) : [];
|
||||
return blocks.length > 0 ? blocks : undefined;
|
||||
};
|
||||
|
||||
private invalidUrl = (message: string) => new AppError({ message, status: 422, service: SERVICE_NAME, method: "toDockerOptions" });
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export const DOCKER_TLS_URL = /^(tcp|https):\/\/([^/\s:]+)(?::(\d+))?\/?$/;
|
||||
export const DOCKER_SOCKET_URL = /^(?:unix:\/\/\/\S+|\/\S+)$/;
|
||||
|
||||
export const isDockerTlsUrl = (url: string | undefined): boolean => DOCKER_TLS_URL.test(url?.trim() ?? "");
|
||||
export const isDockerSocketUrl = (url: string | undefined): boolean => DOCKER_SOCKET_URL.test(url?.trim() ?? "");
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createPrivateKey, KeyObject, X509Certificate } from "node:crypto";
|
||||
|
||||
const PEM_CERT_BLOCK = /-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g;
|
||||
|
||||
export const splitCertificateBundle = (pem: string): string[] => pem.match(PEM_CERT_BLOCK) ?? [];
|
||||
|
||||
export const parseCertificates = (pem: string): X509Certificate[] => {
|
||||
const blocks = splitCertificateBundle(pem);
|
||||
if (blocks.length === 0) throw new Error("No certificate found; expected one or more PEM CERTIFICATE blocks");
|
||||
try {
|
||||
return blocks.map((block) => new X509Certificate(block));
|
||||
} catch {
|
||||
throw new Error("Certificate is not valid PEM");
|
||||
}
|
||||
};
|
||||
|
||||
export const parsePrivateKey = (pem: string): KeyObject => {
|
||||
if (/^-----BEGIN ENCRYPTED PRIVATE KEY-----/m.test(pem) || /^Proc-Type: 4,ENCRYPTED/m.test(pem)) {
|
||||
throw new Error("Encrypted private keys are not supported; remove the passphrase first");
|
||||
}
|
||||
try {
|
||||
return createPrivateKey(pem);
|
||||
} catch {
|
||||
throw new Error("Private key is not valid PEM");
|
||||
}
|
||||
};
|
||||
|
||||
export const keyMatchesCertificate = (key: KeyObject, certificate: X509Certificate): boolean => certificate.checkPrivateKey(key);
|
||||
Reference in new issue
Block a user