1 | |
2 |
|
3 | function wapiUrl(auth: RT.InfobloxNios, path: string) { |
4 | const host = auth.host |
5 | .trim() |
6 | .replace(/^https?:\/\//, "") |
7 | .replace(/\/+$/, "") |
8 | const version = auth.wapi_version.trim().replace(/^v/, "") |
9 | return new URL(`https://${host}/wapi/v${version}/${path}`) |
10 | } |
11 |
|
12 | function basicAuth(auth: RT.InfobloxNios) { |
13 | const bytes = new TextEncoder().encode(`${auth.username}:${auth.password}`) |
14 | return `Basic ${btoa(String.fromCharCode(...bytes))}` |
15 | } |
16 |
|
17 | async function wapiError(response: Response) { |
18 | const text = await response.text() |
19 | if (response.status === 401) { |
20 | return new Error( |
21 | "401 Unauthorized: wrong username or password, or the user's admin group has no API access" |
22 | ) |
23 | } |
24 | try { |
25 | |
26 | const { Error: error, code, text: detail } = JSON.parse(text) |
27 | if (error) { |
28 | const message = |
29 | detail && !error.includes(detail) ? `${error}: ${detail}` : error |
30 | return new Error( |
31 | `${response.status} ${message}${code ? ` (${code})` : ""}` |
32 | ) |
33 | } |
34 | } catch {} |
35 | return new Error(`${response.status} ${text}`) |
36 | } |
37 |
|
38 | |
39 | |
40 | |
41 | const LOOKUPS: [string, string, string][] = [ |
42 | ["record:host", "record:host", "name:"], |
43 | ["record:host (alias)", "record:host", "alias:"], |
44 | ["record:a", "record:a", "name:"], |
45 | ["record:aaaa", "record:aaaa", "name:"], |
46 | ["record:cname", "record:cname", "name:"], |
47 | ["record:mx", "record:mx", "name:"], |
48 | ["record:txt", "record:txt", "name:"], |
49 | ["record:srv", "record:srv", "name:"], |
50 | ["record:ptr", "record:ptr", "name:"], |
51 | ["zone_auth", "zone_auth", "fqdn"], |
52 | ["zone_delegated", "zone_delegated", "fqdn"], |
53 | ["zone_forward", "zone_forward", "fqdn"], |
54 | ] |
55 |
|
56 | |
57 | * Check FQDN exists |
58 | * Tell whether a DNS name exists, and as what: one case-insensitive name lookup per record type (host, host alias, A, AAAA, CNAME, MX, TXT, SRV, PTR) plus authoritative, delegated and forward zone lookups (for a zone apex), run in parallel. Other record types (NS, CAA, NAPTR, DNAME…) are not checked. Returns exists, the matching record_types and the matching records by type. |
59 | */ |
60 | export async function main( |
61 | auth: RT.InfobloxNios, |
62 | fqdn: string, |
63 | view: string | undefined |
64 | ) { |
65 | const name = fqdn.trim().replace(/\.$/, "") |
66 | if (!name) throw new Error("fqdn is required") |
67 |
|
68 | const lookups = await Promise.all( |
69 | LOOKUPS.map(async ([key, type, arg]) => { |
70 | const url = wapiUrl(auth, type) |
71 | url.searchParams.append(arg, arg === "fqdn" ? name.toLowerCase() : name) |
72 | if (view) url.searchParams.append("view", view) |
73 | url.searchParams.append("_return_as_object", "1") |
74 |
|
75 | const response = await fetch(url, { |
76 | headers: { |
77 | Authorization: basicAuth(auth), |
78 | Accept: "application/json", |
79 | }, |
80 | }) |
81 | if (!response.ok) { |
82 | throw await wapiError(response) |
83 | } |
84 | const { result } = (await response.json()) as { result: any[] } |
85 | return [key, result] as const |
86 | }) |
87 | ) |
88 |
|
89 | const records = Object.fromEntries(lookups.filter(([, r]) => r.length > 0)) |
90 | const record_types = Object.keys(records) |
91 | return { fqdn: name, exists: record_types.length > 0, record_types, records } |
92 | } |
93 |
|