1 // Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
2
3 package org.xbill.DNS;
4
5 import java.io.*;
6 import java.util.*;
7
8 /**
9 * A DNS master file parser. This incrementally parses the file, returning
10 * one record at a time. When directives are seen, they are added to the
11 * state and used when parsing future records.
12 *
13 * @author Brian Wellington
14 */
15
16 public class Master {
17
18 private Name origin;
19 private File file;
20 private Record last = null;
21 private long defaultTTL;
22 private Master included = null;
23 private Tokenizer st;
24 private int currentType;
25 private int currentDClass;
26 private long currentTTL;
27 private boolean needSOATTL;
28
29 private Generator generator;
30 private List generators;
31 private boolean noExpandGenerate;
32
Master(File file, Name origin, long initialTTL)33 Master(File file, Name origin, long initialTTL) throws IOException {
34 if (origin != null && !origin.isAbsolute()) {
35 throw new RelativeNameException(origin);
36 }
37 this.file = file;
38 st = new Tokenizer(file);
39 this.origin = origin;
40 defaultTTL = initialTTL;
41 }
42
43 /**
44 * Initializes the master file reader and opens the specified master file.
45 * @param filename The master file.
46 * @param origin The initial origin to append to relative names.
47 * @param ttl The initial default TTL.
48 * @throws IOException The master file could not be opened.
49 */
50 public
Master(String filename, Name origin, long ttl)51 Master(String filename, Name origin, long ttl) throws IOException {
52 this(new File(filename), origin, ttl);
53 }
54
55 /**
56 * Initializes the master file reader and opens the specified master file.
57 * @param filename The master file.
58 * @param origin The initial origin to append to relative names.
59 * @throws IOException The master file could not be opened.
60 */
61 public
Master(String filename, Name origin)62 Master(String filename, Name origin) throws IOException {
63 this(new File(filename), origin, -1);
64 }
65
66 /**
67 * Initializes the master file reader and opens the specified master file.
68 * @param filename The master file.
69 * @throws IOException The master file could not be opened.
70 */
71 public
Master(String filename)72 Master(String filename) throws IOException {
73 this(new File(filename), null, -1);
74 }
75
76 /**
77 * Initializes the master file reader.
78 * @param in The input stream containing a master file.
79 * @param origin The initial origin to append to relative names.
80 * @param ttl The initial default TTL.
81 */
82 public
Master(InputStream in, Name origin, long ttl)83 Master(InputStream in, Name origin, long ttl) {
84 if (origin != null && !origin.isAbsolute()) {
85 throw new RelativeNameException(origin);
86 }
87 st = new Tokenizer(in);
88 this.origin = origin;
89 defaultTTL = ttl;
90 }
91
92 /**
93 * Initializes the master file reader.
94 * @param in The input stream containing a master file.
95 * @param origin The initial origin to append to relative names.
96 */
97 public
Master(InputStream in, Name origin)98 Master(InputStream in, Name origin) {
99 this(in, origin, -1);
100 }
101
102 /**
103 * Initializes the master file reader.
104 * @param in The input stream containing a master file.
105 */
106 public
Master(InputStream in)107 Master(InputStream in) {
108 this(in, null, -1);
109 }
110
111 private Name
parseName(String s, Name origin)112 parseName(String s, Name origin) throws TextParseException {
113 try {
114 return Name.fromString(s, origin);
115 }
116 catch (TextParseException e) {
117 throw st.exception(e.getMessage());
118 }
119 }
120
121 private void
parseTTLClassAndType()122 parseTTLClassAndType() throws IOException {
123 String s;
124 boolean seen_class = false;
125
126
127 // This is a bit messy, since any of the following are legal:
128 // class ttl type
129 // ttl class type
130 // class type
131 // ttl type
132 // type
133 seen_class = false;
134 s = st.getString();
135 if ((currentDClass = DClass.value(s)) >= 0) {
136 s = st.getString();
137 seen_class = true;
138 }
139
140 currentTTL = -1;
141 try {
142 currentTTL = TTL.parseTTL(s);
143 s = st.getString();
144 }
145 catch (NumberFormatException e) {
146 if (defaultTTL >= 0)
147 currentTTL = defaultTTL;
148 else if (last != null)
149 currentTTL = last.getTTL();
150 }
151
152 if (!seen_class) {
153 if ((currentDClass = DClass.value(s)) >= 0) {
154 s = st.getString();
155 } else {
156 currentDClass = DClass.IN;
157 }
158 }
159
160 if ((currentType = Type.value(s)) < 0)
161 throw st.exception("Invalid type '" + s + "'");
162
163 // BIND allows a missing TTL for the initial SOA record, and uses
164 // the SOA minimum value. If the SOA is not the first record,
165 // this is an error.
166 if (currentTTL < 0) {
167 if (currentType != Type.SOA)
168 throw st.exception("missing TTL");
169 needSOATTL = true;
170 currentTTL = 0;
171 }
172 }
173
174 private long
parseUInt32(String s)175 parseUInt32(String s) {
176 if (!Character.isDigit(s.charAt(0)))
177 return -1;
178 try {
179 long l = Long.parseLong(s);
180 if (l < 0 || l > 0xFFFFFFFFL)
181 return -1;
182 return l;
183 }
184 catch (NumberFormatException e) {
185 return -1;
186 }
187 }
188
189 private void
startGenerate()190 startGenerate() throws IOException {
191 String s;
192 int n;
193
194 // The first field is of the form start-end[/step]
195 // Regexes would be useful here.
196 s = st.getIdentifier();
197 n = s.indexOf("-");
198 if (n < 0)
199 throw st.exception("Invalid $GENERATE range specifier: " + s);
200 String startstr = s.substring(0, n);
201 String endstr = s.substring(n + 1);
202 String stepstr = null;
203 n = endstr.indexOf("/");
204 if (n >= 0) {
205 stepstr = endstr.substring(n + 1);
206 endstr = endstr.substring(0, n);
207 }
208 long start = parseUInt32(startstr);
209 long end = parseUInt32(endstr);
210 long step;
211 if (stepstr != null)
212 step = parseUInt32(stepstr);
213 else
214 step = 1;
215 if (start < 0 || end < 0 || start > end || step <= 0)
216 throw st.exception("Invalid $GENERATE range specifier: " + s);
217
218 // The next field is the name specification.
219 String nameSpec = st.getIdentifier();
220
221 // Then the ttl/class/type, in the same form as a normal record.
222 // Only some types are supported.
223 parseTTLClassAndType();
224 if (!Generator.supportedType(currentType))
225 throw st.exception("$GENERATE does not support " +
226 Type.string(currentType) + " records");
227
228 // Next comes the rdata specification.
229 String rdataSpec = st.getIdentifier();
230
231 // That should be the end. However, we don't want to move past the
232 // line yet, so put back the EOL after reading it.
233 st.getEOL();
234 st.unget();
235
236 generator = new Generator(start, end, step, nameSpec,
237 currentType, currentDClass, currentTTL,
238 rdataSpec, origin);
239 if (generators == null)
240 generators = new ArrayList(1);
241 generators.add(generator);
242 }
243
244 private void
endGenerate()245 endGenerate() throws IOException {
246 // Read the EOL that we put back before.
247 st.getEOL();
248
249 generator = null;
250 }
251
252 private Record
nextGenerated()253 nextGenerated() throws IOException {
254 try {
255 return generator.nextRecord();
256 }
257 catch (Tokenizer.TokenizerException e) {
258 throw st.exception("Parsing $GENERATE: " + e.getBaseMessage());
259 }
260 catch (TextParseException e) {
261 throw st.exception("Parsing $GENERATE: " + e.getMessage());
262 }
263 }
264
265 /**
266 * Returns the next record in the master file. This will process any
267 * directives before the next record.
268 * @return The next record.
269 * @throws IOException The master file could not be read, or was syntactically
270 * invalid.
271 */
272 public Record
_nextRecord()273 _nextRecord() throws IOException {
274 Tokenizer.Token token;
275 String s;
276
277 if (included != null) {
278 Record rec = included.nextRecord();
279 if (rec != null)
280 return rec;
281 included = null;
282 }
283 if (generator != null) {
284 Record rec = nextGenerated();
285 if (rec != null)
286 return rec;
287 endGenerate();
288 }
289 while (true) {
290 Name name;
291
292 token = st.get(true, false);
293 if (token.type == Tokenizer.WHITESPACE) {
294 Tokenizer.Token next = st.get();
295 if (next.type == Tokenizer.EOL)
296 continue;
297 else if (next.type == Tokenizer.EOF)
298 return null;
299 else
300 st.unget();
301 if (last == null)
302 throw st.exception("no owner");
303 name = last.getName();
304 }
305 else if (token.type == Tokenizer.EOL)
306 continue;
307 else if (token.type == Tokenizer.EOF)
308 return null;
309 else if (((String) token.value).charAt(0) == '$') {
310 s = token.value;
311
312 if (s.equalsIgnoreCase("$ORIGIN")) {
313 origin = st.getName(Name.root);
314 st.getEOL();
315 continue;
316 } else if (s.equalsIgnoreCase("$TTL")) {
317 defaultTTL = st.getTTL();
318 st.getEOL();
319 continue;
320 } else if (s.equalsIgnoreCase("$INCLUDE")) {
321 String filename = st.getString();
322 File newfile;
323 if (file != null) {
324 String parent = file.getParent();
325 newfile = new File(parent, filename);
326 } else {
327 newfile = new File(filename);
328 }
329 Name incorigin = origin;
330 token = st.get();
331 if (token.isString()) {
332 incorigin = parseName(token.value,
333 Name.root);
334 st.getEOL();
335 }
336 included = new Master(newfile, incorigin,
337 defaultTTL);
338 /*
339 * If we continued, we wouldn't be looking in
340 * the new file. Recursing works better.
341 */
342 return nextRecord();
343 } else if (s.equalsIgnoreCase("$GENERATE")) {
344 if (generator != null)
345 throw new IllegalStateException
346 ("cannot nest $GENERATE");
347 startGenerate();
348 if (noExpandGenerate) {
349 endGenerate();
350 continue;
351 }
352 return nextGenerated();
353 } else {
354 throw st.exception("Invalid directive: " + s);
355 }
356 } else {
357 s = token.value;
358 name = parseName(s, origin);
359 if (last != null && name.equals(last.getName())) {
360 name = last.getName();
361 }
362 }
363
364 parseTTLClassAndType();
365 last = Record.fromString(name, currentType, currentDClass,
366 currentTTL, st, origin);
367 if (needSOATTL) {
368 long ttl = ((SOARecord)last).getMinimum();
369 last.setTTL(ttl);
370 defaultTTL = ttl;
371 needSOATTL = false;
372 }
373 return last;
374 }
375 }
376
377 /**
378 * Returns the next record in the master file. This will process any
379 * directives before the next record.
380 * @return The next record.
381 * @throws IOException The master file could not be read, or was syntactically
382 * invalid.
383 */
384 public Record
nextRecord()385 nextRecord() throws IOException {
386 Record rec = null;
387 try {
388 rec = _nextRecord();
389 }
390 finally {
391 if (rec == null) {
392 st.close();
393 }
394 }
395 return rec;
396 }
397
398 /**
399 * Specifies whether $GENERATE statements should be expanded. Whether
400 * expanded or not, the specifications for generated records are available
401 * by calling {@link #generators}. This must be called before a $GENERATE
402 * statement is seen during iteration to have an effect.
403 */
404 public void
expandGenerate(boolean wantExpand)405 expandGenerate(boolean wantExpand) {
406 noExpandGenerate = !wantExpand;
407 }
408
409 /**
410 * Returns an iterator over the generators specified in the master file; that
411 * is, the parsed contents of $GENERATE statements.
412 * @see Generator
413 */
414 public Iterator
generators()415 generators() {
416 if (generators != null)
417 return Collections.unmodifiableList(generators).iterator();
418 else
419 return Collections.EMPTY_LIST.iterator();
420 }
421
422 protected void
finalize()423 finalize() {
424 st.close();
425 }
426
427 }
428