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 | * Get zone |
40 | * Return the authoritative zone (zone_auth) that contains a DNS name: the zone named fqdn itself, or else its closest parent zone (app.prod.example.com → prod.example.com → example.com). Fails when Infoblox is authoritative for none of them. Forward names only: reverse zones are listed by CIDR in List zones. Set view when the zone exists in several DNS views. |
41 | */ |
42 | export async function main( |
43 | auth: RT.InfobloxNios, |
44 | fqdn: string, |
45 | view: string | undefined |
46 | ) { |
47 | const name = fqdn.trim().toLowerCase().replace(/\.$/, "") |
48 | if (!name) throw new Error("fqdn is required") |
49 | if (/\.(in-addr|ip6)\.arpa$/.test(name)) { |
50 | throw new Error( |
51 | "Reverse zones are named by CIDR in NIOS (e.g. 10.20.0.0/24): use List zones with zone_format IPV4 or IPV6" |
52 | ) |
53 | } |
54 | const labels = name.split(".") |
55 |
|
56 | |
57 | for (let i = 0; i < labels.length; i++) { |
58 | const candidate = labels.slice(i).join(".") |
59 | const url = wapiUrl(auth, "zone_auth") |
60 | url.searchParams.append("fqdn", candidate) |
61 | if (view) url.searchParams.append("view", view) |
62 | url.searchParams.append( |
63 | "_return_fields+", |
64 | "zone_format,comment,disable,ns_group,extattrs" |
65 | ) |
66 | url.searchParams.append("_return_as_object", "1") |
67 |
|
68 | const response = await fetch(url, { |
69 | headers: { |
70 | Authorization: basicAuth(auth), |
71 | Accept: "application/json", |
72 | }, |
73 | }) |
74 | if (!response.ok) { |
75 | throw await wapiError(response) |
76 | } |
77 | const { result } = (await response.json()) as { |
78 | result: { _ref: string; fqdn: string; view: string }[] |
79 | } |
80 | if (result.length > 1) { |
81 | throw new Error( |
82 | `Zone ${candidate} exists in several DNS views (${result.map((z) => z.view).join(", ")}); set view` |
83 | ) |
84 | } |
85 | if (result.length === 1) return result[0] |
86 | } |
87 |
|
88 | throw new Error( |
89 | `No authoritative zone found for ${fqdn}${view ? ` in view ${view}` : ""}` |
90 | ) |
91 | } |
92 |
|