mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-09-14 11:06:15 +05:00
fix(dns): send SRV and CAA values to Cloudflare as structured data
Cloudflare treats content as read-only for SRV and CAA and expects a data object instead, so both types were rejected on write. The client now parses the inline value into the fields Cloudflare wants, and builds the payload before the lookup request so a malformed value fails without spending an API call. The form rejects a malformed SRV or CAA value up front and shows the expected shape, so the error lands on the field instead of coming back from the provider.
This commit is contained in:
parent
9c4ad14c70
commit
db2eb4f60a
@ -164,6 +164,76 @@ describe("cloudflareClient MX priority", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("cloudflareClient structured records", () => {
|
||||
const stubCreate = () =>
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(cfSuccess([]))
|
||||
.mockResolvedValueOnce(cfSuccess({ id: "rec-1" }));
|
||||
|
||||
it("sends SRV values as structured data", async () => {
|
||||
stubCreate();
|
||||
|
||||
await cloudflareClient.upsertRecord(config, {
|
||||
zoneId: "zone-1",
|
||||
type: "SRV",
|
||||
name: "_sip._tcp.example.com",
|
||||
content: "1 10 5269 talk.example.com",
|
||||
});
|
||||
|
||||
const [, init] = mockFetch.mock.calls[1] as [string, RequestInit];
|
||||
const body = JSON.parse(init.body as string);
|
||||
expect(body.data).toEqual({
|
||||
priority: 1,
|
||||
weight: 10,
|
||||
port: 5269,
|
||||
target: "talk.example.com",
|
||||
});
|
||||
expect(body.content).toBeUndefined();
|
||||
});
|
||||
|
||||
it("sends CAA values as structured data and drops the quotes", async () => {
|
||||
stubCreate();
|
||||
|
||||
await cloudflareClient.upsertRecord(config, {
|
||||
zoneId: "zone-1",
|
||||
type: "CAA",
|
||||
name: "example.com",
|
||||
content: '0 issue "letsencrypt.org"',
|
||||
});
|
||||
|
||||
const [, init] = mockFetch.mock.calls[1] as [string, RequestInit];
|
||||
const body = JSON.parse(init.body as string);
|
||||
expect(body.data).toEqual({
|
||||
flags: 0,
|
||||
tag: "issue",
|
||||
value: "letsencrypt.org",
|
||||
});
|
||||
expect(body.content).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects an SRV value that is missing a part", async () => {
|
||||
await expect(
|
||||
cloudflareClient.upsertRecord(config, {
|
||||
zoneId: "zone-1",
|
||||
type: "SRV",
|
||||
name: "_sip._tcp.example.com",
|
||||
content: "1 10 5269",
|
||||
}),
|
||||
).rejects.toThrow(/SRV value/);
|
||||
});
|
||||
|
||||
it("rejects a CAA value that has no tag", async () => {
|
||||
await expect(
|
||||
cloudflareClient.upsertRecord(config, {
|
||||
zoneId: "zone-1",
|
||||
type: "CAA",
|
||||
name: "example.com",
|
||||
content: "0",
|
||||
}),
|
||||
).rejects.toThrow(/CAA value/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cloudflareClient proxy status", () => {
|
||||
it("sends the proxy status for proxiable types", async () => {
|
||||
mockFetch
|
||||
|
||||
@ -68,13 +68,29 @@ const valueFields: Record<
|
||||
PTR: { label: "Target", placeholder: "host.example.com" },
|
||||
};
|
||||
|
||||
const DnsRecordSchema = z.object({
|
||||
type: z.enum(DNS_RECORD_TYPES),
|
||||
name: z.string().min(1, { message: "Name is required" }),
|
||||
content: z.string().min(1, { message: "Content is required" }),
|
||||
ttl: z.string(),
|
||||
proxied: z.boolean(),
|
||||
});
|
||||
const structuredValuePatterns: Partial<Record<RecordType, RegExp>> = {
|
||||
SRV: /^\d+\s+\d+\s+\d+\s+\S+$/,
|
||||
CAA: /^\d+\s+\S+\s+.+$/,
|
||||
};
|
||||
|
||||
const DnsRecordSchema = z
|
||||
.object({
|
||||
type: z.enum(DNS_RECORD_TYPES),
|
||||
name: z.string().min(1, { message: "Name is required" }),
|
||||
content: z.string().min(1, { message: "Content is required" }),
|
||||
ttl: z.string(),
|
||||
proxied: z.boolean(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
const pattern = structuredValuePatterns[data.type];
|
||||
if (pattern && !pattern.test(data.content.trim())) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["content"],
|
||||
message: `Expected ${valueFields[data.type].placeholder}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
type DnsRecordForm = z.infer<typeof DnsRecordSchema>;
|
||||
|
||||
|
||||
@ -31,14 +31,51 @@ const proxySettings = (record: { type: string; proxied?: boolean }) =>
|
||||
? { proxied: record.proxied }
|
||||
: {};
|
||||
|
||||
const splitPriority = (record: { type: string; content: string }) => {
|
||||
if (record.type !== "MX") {
|
||||
return { content: record.content };
|
||||
const buildValue = (record: { type: string; content: string }) => {
|
||||
const value = record.content.trim();
|
||||
|
||||
if (record.type === "MX") {
|
||||
const match = /^(\d+)\s+(\S.*)$/.exec(value);
|
||||
return match
|
||||
? { content: match[2] as string, priority: Number(match[1]) }
|
||||
: { content: value, priority: 10 };
|
||||
}
|
||||
const match = /^\s*(\d+)\s+(\S.*)$/.exec(record.content);
|
||||
return match
|
||||
? { content: match[2] as string, priority: Number(match[1]) }
|
||||
: { content: record.content.trim(), priority: 10 };
|
||||
|
||||
if (record.type === "SRV") {
|
||||
const parts = value.split(/\s+/);
|
||||
const [priority, weight, port, target] = parts;
|
||||
if (parts.length !== 4 || !target) {
|
||||
throw new Error(
|
||||
`Cloudflare: an SRV value must be "priority weight port target", got "${value}"`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
data: {
|
||||
priority: Number(priority),
|
||||
weight: Number(weight),
|
||||
port: Number(port),
|
||||
target,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (record.type === "CAA") {
|
||||
const match = /^(\d+)\s+(\S+)\s+"?([^"]+)"?$/.exec(value);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`Cloudflare: a CAA value must be \`flags tag "value"\`, got "${value}"`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
data: {
|
||||
flags: Number(match[1]),
|
||||
tag: match[2] as string,
|
||||
value: match[3] as string,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { content: value };
|
||||
};
|
||||
|
||||
const cfFetch = async <T>(
|
||||
@ -123,19 +160,19 @@ export const cloudflareClient: DnsClient<CloudflareConfig> = {
|
||||
},
|
||||
|
||||
async upsertRecord(config, record) {
|
||||
const payload = {
|
||||
type: record.type,
|
||||
name: record.name,
|
||||
...buildValue(record),
|
||||
...proxySettings(record),
|
||||
ttl: record.ttl ?? 1,
|
||||
};
|
||||
|
||||
const existing = await cfFetch<{ id: string }[]>(
|
||||
config,
|
||||
`/zones/${record.zoneId}/dns_records?type=${record.type}&name=${encodeURIComponent(record.name)}`,
|
||||
);
|
||||
|
||||
const payload = {
|
||||
type: record.type,
|
||||
name: record.name,
|
||||
...splitPriority(record),
|
||||
...proxySettings(record),
|
||||
ttl: record.ttl ?? 1,
|
||||
};
|
||||
|
||||
const existingRecord = existing[0];
|
||||
if (existingRecord) {
|
||||
const updated = await cfFetch<{ id: string }>(
|
||||
@ -163,7 +200,7 @@ export const cloudflareClient: DnsClient<CloudflareConfig> = {
|
||||
body: JSON.stringify({
|
||||
type: record.type,
|
||||
name: record.name,
|
||||
...splitPriority(record),
|
||||
...buildValue(record),
|
||||
...proxySettings(record),
|
||||
ttl: record.ttl ?? 1,
|
||||
}),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user