Home | History | Annotate | Download | only in DNS
      1 // Copyright (c) 1999-2004 Brian Wellington (bwelling (at) 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 
     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
     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
     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
     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
     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
     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
    107 Master(InputStream in) {
    108 	this(in, null, -1);
    109 }
    110 
    111 private Name
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    423 finalize() {
    424 	st.close();
    425 }
    426 
    427 }
    428