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