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 	if (st != null)
425 		st.close();
426 }
427 
428 }
429