Add IPv6 DNS mappings
[hcoop/domtool2.git] / src / plugins / bind.sml
1 (* HCoop Domtool (http://hcoop.sourceforge.net/)
2 * Copyright (c) 2006, Adam Chlipala
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 *)
18
19 (* BIND DNS *)
20
21 structure Bind :> BIND = struct
22
23 open Ast
24
25 val namedChanged = ref false
26 val zoneChanged = ref false
27
28 val didDomain = ref ""
29
30 val () = Slave.registerPreHandler (fn () => (namedChanged := false;
31 zoneChanged := false;
32 didDomain := ""))
33
34 val dns : Domain.files option ref = ref NONE
35
36 val _ = Domain.registerBefore
37 (fn _ => dns := Option.map (fn node => Domain.domainsFile {node = node,
38 name = "dns"})
39 (Domain.dnsMaster ()))
40
41 val _ = Domain.registerAfter
42 (fn _ => (Option.app (fn files => #close files ()) (!dns);
43 dns := NONE))
44
45 val dl = ErrorMsg.dummyLoc
46
47 datatype dns_record =
48 A of string * string
49 | CNAME of string * string
50 | MX of int * string
51 | NS of string
52 | DefaultA of string
53 | AAAA of string * string
54 | DefaultAAAA of string
55
56 val record = fn (EApp ((EApp ((EVar "dnsA", _), e1), _), e2), _) =>
57 (case (Env.string e1, Domain.ip e2) of
58 (SOME v1, SOME v2) => SOME (A (v1, v2))
59 | _ => NONE)
60 | (EApp ((EApp ((EVar "dnsCNAME", _), e1), _), e2), _) =>
61 (case (Env.string e1, Env.string e2) of
62 (SOME v1, SOME v2) => SOME (CNAME (v1, v2))
63 | _ => NONE)
64 | (EApp ((EApp ((EVar "dnsMX", _), e1), _), e2), _) =>
65 (case (Env.int e1, Env.string e2) of
66 (SOME v1, SOME v2) => SOME (MX (v1, v2))
67 | _ => NONE)
68 | (EApp ((EVar "dnsNS", _), e), _) =>
69 Option.map NS (Env.string e)
70 | (EApp ((EVar "dnsDefaultA", _), e), _) =>
71 Option.map DefaultA (Domain.ip e)
72 | (EApp ((EApp ((EVar "dnsAAAA", _), e1), _), e2), _) =>
73 (case (Env.string e1, Env.string e2) of
74 (SOME v1, SOME v2) => SOME (AAAA (v1, v2))
75 | _ => NONE)
76 | (EApp ((EVar "dnsDefaultAAAA", _), e), _) =>
77 Option.map DefaultAAAA (Env.string e)
78 | _ => NONE
79
80 fun writeRecord (evs, r) =
81 case !dns of
82 NONE => print ("Warning: DNS directive for " ^ Domain.currentDomain ()
83 ^ " ignored because no master DNS server is configured for this domain\n")
84 | SOME files =>
85 let
86 fun write s = #write files s
87 fun writeDom () = #writeDom files ()
88 val ttl = Env.env Env.int (evs, "TTL")
89 in
90 case r of
91 A (from, to) => (write from;
92 write ".";
93 writeDom ();
94 write ".\t";
95 write (Int.toString ttl);
96 write "\tIN\tA\t";
97 write to;
98 write "\n")
99 | DefaultA to => (writeDom ();
100 write ".\t";
101 write (Int.toString ttl);
102 write "\tIN\tA\t";
103 write to;
104 write "\n")
105 | CNAME (from, to) => (write from;
106 write ".";
107 writeDom ();
108 write ".\t";
109 write (Int.toString ttl);
110 write "\tIN\tCNAME\t";
111 write to;
112 write ".\n")
113 | MX (num, host) => (writeDom ();
114 write ".\t";
115 write (Int.toString ttl);
116 write "\tIN\tMX\t";
117 write (Int.toString num);
118 write "\t";
119 write host;
120 write ".\n")
121 | NS host => (writeDom ();
122 write ".\t";
123 write (Int.toString ttl);
124 write "\tIN\tNS\t";
125 write host;
126 write ".\n")
127 | AAAA (from, to) => (write from;
128 write ".";
129 writeDom ();
130 write ".\t";
131 write (Int.toString ttl);
132 write "\tIN\tAAAA\t";
133 write to;
134 write "\n")
135 | DefaultAAAA to => (writeDom ();
136 write ".\t";
137 write (Int.toString ttl);
138 write "\tIN\tAAAA\t";
139 write to;
140 write "\n")
141 end
142
143 val () = Env.actionV_one "dns"
144 ("record", record)
145 writeRecord
146
147 fun readLine inf =
148 case TextIO.inputLine inf of
149 NONE => raise Fail "Expected a line for BIND"
150 | SOME s => String.substring (s, 0, size s - 1)
151
152 fun readILine inf = valOf (Int.fromString (readLine inf))
153
154 val monthToInt = fn Date.Jan => 1
155 | Date.Feb => 2
156 | Date.Mar => 3
157 | Date.Apr => 4
158 | Date.May => 5
159 | Date.Jun => 6
160 | Date.Jul => 7
161 | Date.Aug => 8
162 | Date.Sep => 9
163 | Date.Oct => 10
164 | Date.Nov => 11
165 | Date.Dec => 12
166
167 fun padBy ch amt s =
168 if size s < amt then
169 CharVector.tabulate (amt - size s, fn _ => ch) ^ s
170 else
171 s
172
173 fun dateString () =
174 let
175 val date = Date.fromTimeUniv (Time.now ())
176 in
177 padBy #"0" 4 (Int.toString (Date.year date))
178 ^ padBy #"0" 2 (Int.toString (monthToInt (Date.month date)))
179 ^ padBy #"0" 2 (Int.toString (Date.day date))
180 end
181
182 val () = Slave.registerFileHandler (fn fs =>
183 let
184 val {dir, file} = OS.Path.splitDirFile (#file fs)
185
186 fun dnsChanged () =
187 if #domain fs = !didDomain then
188 ()
189 else if Slave.isDelete (#action fs) then
190 let
191 val fname = OS.Path.joinBaseExt {base = #domain fs,
192 ext = SOME "zone"}
193 val fname = OS.Path.joinDirFile {dir = Config.Bind.zonePath,
194 file = fname}
195 in
196 Slave.shellF ([Config.rm, " -f ", fname],
197 fn cl => "Error deleting file: " ^ cl)
198 end
199 else
200 let
201 val inf = TextIO.openIn (OS.Path.joinDirFile {dir = #dir fs,
202 file = "soa"})
203 val kind = readLine inf
204 val ttl = readILine inf
205 val ns = readLine inf
206 val serial = case readLine inf of
207 "" => NONE
208 | s => Int.fromString s
209 val rf = readILine inf
210 val ret = readILine inf
211 val exp = readILine inf
212 val min = readILine inf
213 val () = TextIO.closeIn inf
214
215 val serialPath = OS.Path.joinDirFile {dir = Config.serialDir,
216 file = #domain fs}
217
218 val oldSerial = let
219 val inf = TextIO.openIn serialPath
220 in
221 SOME (readLine inf)
222 before TextIO.closeIn inf
223 end handle IO.Io {name, ...} => NONE
224
225 val newSerial =
226 case serial of
227 SOME n => Int.toString n
228 | NONE =>
229 let
230 val prefix = dateString ()
231 in
232 prefix
233 ^ (case oldSerial of
234 NONE => "00"
235 | SOME old =>
236 if size old >= 8 andalso
237 String.substring (old, 0, 8) = prefix then
238 case Int.fromString (String.extract (old, 8, NONE)) of
239 NONE => "00"
240 | SOME old => padBy #"0" 2 (Int.toString (old+1))
241 else
242 "00")
243 end
244
245 val outf = TextIO.openOut serialPath
246 val _ = TextIO.output (outf, newSerial)
247 val _ = TextIO.closeOut outf
248
249 val dns = OS.Path.joinDirFile {dir = #dir fs,
250 file = "dns"}
251
252 val fname = OS.Path.joinBaseExt {base = #domain fs,
253 ext = SOME "zone"}
254 val fname = OS.Path.joinDirFile {dir = Config.Bind.zonePath,
255 file = fname}
256
257 val outf = TextIO.openOut fname
258 in
259 zoneChanged := true;
260 TextIO.output (outf, "$TTL ");
261 TextIO.output (outf, Int.toString ttl);
262 TextIO.output (outf, "\n\n@\tIN\tSOA\t");
263 TextIO.output (outf, ns);
264 TextIO.output (outf, ".\thostmaster.");
265 TextIO.output (outf, #domain fs);
266 TextIO.output (outf, ". ( ");
267 TextIO.output (outf, newSerial);
268 TextIO.output (outf, " ");
269 TextIO.output (outf, Int.toString rf);
270 TextIO.output (outf, " ");
271 TextIO.output (outf, Int.toString ret);
272 TextIO.output (outf, " ");
273 TextIO.output (outf, Int.toString exp);
274 TextIO.output (outf, " ");
275 TextIO.output (outf, Int.toString min);
276 TextIO.output (outf, " )\n\n");
277 TextIO.closeOut outf;
278 if Posix.FileSys.access (dns, []) then
279 Slave.shellF ([Config.cat, " ", dns, " >>", fname],
280 fn cl => "Error concatenating file: " ^ cl)
281 else
282 ();
283 didDomain := #domain fs
284 end
285 in
286 case file of
287 "soa" => dnsChanged ()
288 | "dns" => dnsChanged ()
289 | "named.conf" => namedChanged := true
290 | _ => ()
291 end)
292
293 val () = Slave.registerPostHandler
294 (fn () =>
295 (if !namedChanged then
296 Slave.concatTo (fn s => s = "named.conf") Config.Bind.namedConf
297 else
298 ();
299 if !namedChanged orelse !zoneChanged then
300 Slave.shellF ([Config.Bind.reload],
301 fn cl => "Error reloading bind with " ^ cl)
302 else
303 ()))
304
305 val () = Domain.registerResetLocal (fn () =>
306 ignore (OS.Process.system (Config.rm ^ " -rf /var/domtool/zones/*")))
307
308 val () = Domain.registerDescriber (Domain.considerAll
309 [Domain.Filename {filename = "named.conf",
310 heading = "named.conf addition:",
311 showEmpty = false},
312 Domain.Filename {filename = "dns",
313 heading = "DNS zonefile contents:",
314 showEmpty = false}])
315
316 end