1 /*
   2  * Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 /*
  27  *
  28  *  (C) Copyright IBM Corp. 1999 All Rights Reserved.
  29  *  Copyright 1997 The Open Group Research Institute.  All rights reserved.
  30  */
  31 package sun.security.krb5;
  32 
  33 import java.io.*;
  34 import java.nio.file.DirectoryStream;
  35 import java.nio.file.Files;
  36 import java.nio.file.Paths;
  37 import java.nio.file.Path;
  38 import java.security.PrivilegedAction;
  39 import java.util.*;
  40 import java.net.InetAddress;
  41 import java.net.UnknownHostException;
  42 import java.security.AccessController;
  43 import java.security.PrivilegedExceptionAction;
  44 import java.util.regex.Matcher;
  45 import java.util.regex.Pattern;
  46 
  47 import sun.net.dns.ResolverConfiguration;
  48 import sun.security.action.GetPropertyAction;
  49 import sun.security.krb5.internal.crypto.EType;
  50 import sun.security.krb5.internal.Krb5;
  51 
  52 /**
  53  * This class maintains key-value pairs of Kerberos configurable constants
  54  * from configuration file or from user specified system properties.
  55  */
  56 
  57 public class Config {
  58 
  59     /*
  60      * Only allow a single instance of Config.
  61      */
  62     private static Config singleton = null;
  63 
  64     /*
  65      * Hashtable used to store configuration information.
  66      */
  67     private Hashtable<String,Object> stanzaTable = new Hashtable<>();
  68 
  69     private static boolean DEBUG = sun.security.krb5.internal.Krb5.DEBUG;
  70 
  71     // these are used for hexdecimal calculation.
  72     private static final int BASE16_0 = 1;
  73     private static final int BASE16_1 = 16;
  74     private static final int BASE16_2 = 16 * 16;
  75     private static final int BASE16_3 = 16 * 16 * 16;
  76 
  77     /**
  78      * Specified by system properties. Must be both null or non-null.
  79      */
  80     private final String defaultRealm;
  81     private final String defaultKDC;
  82 
  83     // used for native interface
  84     private static native String getWindowsDirectory(boolean isSystem);
  85 
  86 
  87     /**
  88      * Gets an instance of Config class. One and only one instance (the
  89      * singleton) is returned.
  90      *
  91      * @exception KrbException if error occurs when constructing a Config
  92      * instance. Possible causes would be either of java.security.krb5.realm or
  93      * java.security.krb5.kdc not specified, error reading configuration file.
  94      */
  95     public static synchronized Config getInstance() throws KrbException {
  96         if (singleton == null) {
  97             singleton = new Config();
  98         }
  99         return singleton;
 100     }
 101 
 102     /**
 103      * Refresh and reload the Configuration. This could involve,
 104      * for example reading the Configuration file again or getting
 105      * the java.security.krb5.* system properties again. This method
 106      * also tries its best to update static fields in other classes
 107      * that depend on the configuration.
 108      *
 109      * @exception KrbException if error occurs when constructing a Config
 110      * instance. Possible causes would be either of java.security.krb5.realm or
 111      * java.security.krb5.kdc not specified, error reading configuration file.
 112      */
 113 
 114     public static void refresh() throws KrbException {
 115         synchronized (Config.class) {
 116             singleton = new Config();
 117         }
 118         KdcComm.initStatic();
 119         EType.initStatic();
 120         Checksum.initStatic();
 121     }
 122 
 123 
 124     private static boolean isMacosLionOrBetter() {
 125         // split the "10.x.y" version number
 126         String osname = GetPropertyAction.privilegedGetProperty("os.name");
 127         if (!osname.contains("OS X")) {
 128             return false;
 129         }
 130 
 131         String osVersion = GetPropertyAction.privilegedGetProperty("os.version");
 132         String[] fragments = osVersion.split("\\.");
 133 
 134         // sanity check the "10." part of the version
 135         if (!fragments[0].equals("10")) return false;
 136         if (fragments.length < 2) return false;
 137 
 138         // check if Mac OS X 10.7(.y)
 139         try {
 140             int minorVers = Integer.parseInt(fragments[1]);
 141             if (minorVers >= 7) return true;
 142         } catch (NumberFormatException e) {
 143             // was not an integer
 144         }
 145 
 146         return false;
 147     }
 148 
 149     /**
 150      * Private constructor - can not be instantiated externally.
 151      */
 152     private Config() throws KrbException {
 153         /*
 154          * If either one system property is specified, we throw exception.
 155          */
 156         String tmp = GetPropertyAction
 157                 .privilegedGetProperty("java.security.krb5.kdc");
 158         if (tmp != null) {
 159             // The user can specify a list of kdc hosts separated by ":"
 160             defaultKDC = tmp.replace(':', ' ');
 161         } else {
 162             defaultKDC = null;
 163         }
 164         defaultRealm = GetPropertyAction
 165                 .privilegedGetProperty("java.security.krb5.realm");
 166         if ((defaultKDC == null && defaultRealm != null) ||
 167             (defaultRealm == null && defaultKDC != null)) {
 168             throw new KrbException
 169                 ("System property java.security.krb5.kdc and " +
 170                  "java.security.krb5.realm both must be set or " +
 171                  "neither must be set.");
 172         }
 173 
 174         // Always read the Kerberos configuration file
 175         try {
 176             List<String> configFile;
 177             String fileName = getJavaFileName();
 178             if (fileName != null) {
 179                 configFile = loadConfigFile(fileName);
 180                 stanzaTable = parseStanzaTable(configFile);
 181                 if (DEBUG) {
 182                     System.out.println("Loaded from Java config");
 183                 }
 184             } else {
 185                 boolean found = false;
 186                 if (isMacosLionOrBetter()) {
 187                     try {
 188                         stanzaTable = SCDynamicStoreConfig.getConfig();
 189                         if (DEBUG) {
 190                             System.out.println("Loaded from SCDynamicStoreConfig");
 191                         }
 192                         found = true;
 193                     } catch (IOException ioe) {
 194                         // OK. Will go on with file
 195                     }
 196                 }
 197                 if (!found) {
 198                     fileName = getNativeFileName();
 199                     configFile = loadConfigFile(fileName);
 200                     stanzaTable = parseStanzaTable(configFile);
 201                     if (DEBUG) {
 202                         System.out.println("Loaded from native config");
 203                     }
 204                 }
 205             }
 206         } catch (IOException ioe) {
 207             if (DEBUG) {
 208                 System.out.println("Exception thrown in loading config:");
 209                 ioe.printStackTrace(System.out);
 210             }
 211             throw new KrbException("krb5.conf loading failed");
 212         }
 213     }
 214 
 215     /**
 216      * Gets the last-defined string value for the specified keys.
 217      * @param keys the keys, as an array from section name, sub-section names
 218      * (if any), to value name.
 219      * @return the value. When there are multiple values for the same key,
 220      * returns the first one. {@code null} is returned if not all the keys are
 221      * defined. For example, {@code get("libdefaults", "forwardable")} will
 222      * return null if "forwardable" is not defined in [libdefaults], and
 223      * {@code get("realms", "R", "kdc")} will return null if "R" is not
 224      * defined in [realms] or "kdc" is not defined for "R".
 225      * @throws IllegalArgumentException if any of the keys is illegal, either
 226      * because a key not the last one is not a (sub)section name or the last
 227      * key is still a section name. For example, {@code get("libdefaults")}
 228      * throws this exception because [libdefaults] is a section name instead of
 229      * a value name, and {@code get("libdefaults", "forwardable", "tail")}
 230      * also throws this exception because "forwardable" is already a value name
 231      * and has no sub-key at all (given "forwardable" is defined, otherwise,
 232      * this method has no knowledge if it's a value name or a section name),
 233      */
 234     public String get(String... keys) {
 235         Vector<String> v = getString0(keys);
 236         if (v == null) return null;
 237         return v.firstElement();
 238     }
 239 
 240     /**
 241      * Gets the boolean value for the specified keys. Returns TRUE if the
 242      * string value is "yes", or "true", FALSE if "no", or "false", or null
 243      * if otherwise or not defined. The comparision is case-insensitive.
 244      *
 245      * @param keys the keys, see {@link #get(String...)}
 246      * @return the boolean value, or null if there is no value defined or the
 247      * value does not look like a boolean value.
 248      * @throws IllegalArgumentException see {@link #get(String...)}
 249      */
 250     public Boolean getBooleanObject(String... keys) {
 251         String s = get(keys);
 252         if (s == null) {
 253             return null;
 254         }
 255         switch (s.toLowerCase(Locale.US)) {
 256             case "yes": case "true":
 257                 return Boolean.TRUE;
 258             case "no": case "false":
 259                 return Boolean.FALSE;
 260             default:
 261                 return null;
 262         }
 263     }
 264 
 265     /**
 266      * Gets all values (at least one) for the specified keys separated by
 267      * a whitespace, or null if there is no such keys.
 268      * The values can either be provided on a single line, or on multiple lines
 269      * using the same key. When provided on a single line, the value can be
 270      * comma or space separated.
 271      * @throws IllegalArgumentException if any of the keys is illegal
 272      *         (See {@link #get})
 273      */
 274     public String getAll(String... keys) {
 275         Vector<String> v = getString0(keys);
 276         if (v == null) return null;
 277         StringBuilder sb = new StringBuilder();
 278         boolean first = true;
 279         for (String s: v) {
 280             s = s.replaceAll("[\\s,]+", " ");
 281             if (first) {
 282                 sb.append(s);
 283                 first = false;
 284             } else {
 285                 sb.append(' ').append(s);
 286             }
 287         }
 288         return sb.toString();
 289     }
 290 
 291     /**
 292      * Returns true if keys exists, can be final string(s) or a sub-section
 293      * @throws IllegalArgumentException if any of the keys is illegal
 294      *         (See {@link #get})
 295      */
 296     public boolean exists(String... keys) {
 297         return get0(keys) != null;
 298     }
 299 
 300     // Returns final string value(s) for given keys.
 301     @SuppressWarnings("unchecked")
 302     private Vector<String> getString0(String... keys) {
 303         try {
 304             return (Vector<String>)get0(keys);
 305         } catch (ClassCastException cce) {
 306             throw new IllegalArgumentException(cce);
 307         }
 308     }
 309 
 310     // Internal method. Returns the value for keys, which can be a sub-section
 311     // (as a Hashtable) or final string value(s) (as a Vector). This is the
 312     // only method (except for toString) that reads stanzaTable directly.
 313     @SuppressWarnings("unchecked")
 314     private Object get0(String... keys) {
 315         Object current = stanzaTable;
 316         try {
 317             for (String key: keys) {
 318                 current = ((Hashtable<String,Object>)current).get(key);
 319                 if (current == null) return null;
 320             }
 321             return current;
 322         } catch (ClassCastException cce) {
 323             throw new IllegalArgumentException(cce);
 324         }
 325     }
 326 
 327     /**
 328      * Translates a duration value into seconds.
 329      *
 330      * The format can be one of "h:m[:s]", "NdNhNmNs", and "N". See
 331      * http://web.mit.edu/kerberos/krb5-devel/doc/basic/date_format.html#duration
 332      * for definitions.
 333      *
 334      * @param s the string duration
 335      * @return time in seconds
 336      * @throws KrbException if format is illegal
 337      */
 338     public static int duration(String s) throws KrbException {
 339 
 340         if (s.isEmpty()) {
 341             throw new KrbException("Duration cannot be empty");
 342         }
 343 
 344         // N
 345         if (s.matches("\\d+")) {
 346             return Integer.parseInt(s);
 347         }
 348 
 349         // h:m[:s]
 350         Matcher m = Pattern.compile("(\\d+):(\\d+)(:(\\d+))?").matcher(s);
 351         if (m.matches()) {
 352             int hr = Integer.parseInt(m.group(1));
 353             int min = Integer.parseInt(m.group(2));
 354             if (min >= 60) {
 355                 throw new KrbException("Illegal duration format " + s);
 356             }
 357             int result = hr * 3600 + min * 60;
 358             if (m.group(4) != null) {
 359                 int sec = Integer.parseInt(m.group(4));
 360                 if (sec >= 60) {
 361                     throw new KrbException("Illegal duration format " + s);
 362                 }
 363                 result += sec;
 364             }
 365             return result;
 366         }
 367 
 368         // NdNhNmNs
 369         // 120m allowed. Maybe 1h120m is not good, but still allowed
 370         m = Pattern.compile(
 371                     "((\\d+)d)?\\s*((\\d+)h)?\\s*((\\d+)m)?\\s*((\\d+)s)?",
 372                 Pattern.CASE_INSENSITIVE).matcher(s);
 373         if (m.matches()) {
 374             int result = 0;
 375             if (m.group(2) != null) {
 376                 result += 86400 * Integer.parseInt(m.group(2));
 377             }
 378             if (m.group(4) != null) {
 379                 result += 3600 * Integer.parseInt(m.group(4));
 380             }
 381             if (m.group(6) != null) {
 382                 result += 60 * Integer.parseInt(m.group(6));
 383             }
 384             if (m.group(8) != null) {
 385                 result += Integer.parseInt(m.group(8));
 386             }
 387             return result;
 388         }
 389 
 390         throw new KrbException("Illegal duration format " + s);
 391     }
 392 
 393     /**
 394      * Gets the int value for the specified keys.
 395      * @param keys the keys
 396      * @return the int value, Integer.MIN_VALUE is returned if it cannot be
 397      * found or the value is not a legal integer.
 398      * @throws IllegalArgumentException if any of the keys is illegal
 399      * @see #get(java.lang.String[])
 400      */
 401     public int getIntValue(String... keys) {
 402         String result = get(keys);
 403         int value = Integer.MIN_VALUE;
 404         if (result != null) {
 405             try {
 406                 value = parseIntValue(result);
 407             } catch (NumberFormatException e) {
 408                 if (DEBUG) {
 409                     System.out.println("Exception in getting value of " +
 410                                        Arrays.toString(keys) + " " +
 411                                        e.getMessage());
 412                     System.out.println("Setting " + Arrays.toString(keys) +
 413                                        " to minimum value");
 414                 }
 415                 value = Integer.MIN_VALUE;
 416             }
 417         }
 418         return value;
 419     }
 420 
 421     /**
 422      * Parses a string to an integer. The convertible strings include the
 423      * string representations of positive integers, negative integers, and
 424      * hex decimal integers.  Valid inputs are, e.g., -1234, +1234,
 425      * 0x40000.
 426      *
 427      * @param input the String to be converted to an Integer.
 428      * @return an numeric value represented by the string
 429      * @exception NumberFormatException if the String does not contain a
 430      * parsable integer.
 431      */
 432     private int parseIntValue(String input) throws NumberFormatException {
 433         int value = 0;
 434         if (input.startsWith("+")) {
 435             String temp = input.substring(1);
 436             return Integer.parseInt(temp);
 437         } else if (input.startsWith("0x")) {
 438             String temp = input.substring(2);
 439             char[] chars = temp.toCharArray();
 440             if (chars.length > 8) {
 441                 throw new NumberFormatException();
 442             } else {
 443                 for (int i = 0; i < chars.length; i++) {
 444                     int index = chars.length - i - 1;
 445                     switch (chars[i]) {
 446                     case '0':
 447                         value += 0;
 448                         break;
 449                     case '1':
 450                         value += 1 * getBase(index);
 451                         break;
 452                     case '2':
 453                         value += 2 * getBase(index);
 454                         break;
 455                     case '3':
 456                         value += 3 * getBase(index);
 457                         break;
 458                     case '4':
 459                         value += 4 * getBase(index);
 460                         break;
 461                     case '5':
 462                         value += 5 * getBase(index);
 463                         break;
 464                     case '6':
 465                         value += 6 * getBase(index);
 466                         break;
 467                     case '7':
 468                         value += 7 * getBase(index);
 469                         break;
 470                     case '8':
 471                         value += 8 * getBase(index);
 472                         break;
 473                     case '9':
 474                         value += 9 * getBase(index);
 475                         break;
 476                     case 'a':
 477                     case 'A':
 478                         value += 10 * getBase(index);
 479                         break;
 480                     case 'b':
 481                     case 'B':
 482                         value += 11 * getBase(index);
 483                         break;
 484                     case 'c':
 485                     case 'C':
 486                         value += 12 * getBase(index);
 487                         break;
 488                     case 'd':
 489                     case 'D':
 490                         value += 13 * getBase(index);
 491                         break;
 492                     case 'e':
 493                     case 'E':
 494                         value += 14 * getBase(index);
 495                         break;
 496                     case 'f':
 497                     case 'F':
 498                         value += 15 * getBase(index);
 499                         break;
 500                     default:
 501                         throw new NumberFormatException("Invalid numerical format");
 502                     }
 503                 }
 504             }
 505             if (value < 0) {
 506                 throw new NumberFormatException("Data overflow.");
 507             }
 508         } else {
 509             value = Integer.parseInt(input);
 510         }
 511         return value;
 512     }
 513 
 514     private int getBase(int i) {
 515         int result = 16;
 516         switch (i) {
 517         case 0:
 518             result = BASE16_0;
 519             break;
 520         case 1:
 521             result = BASE16_1;
 522             break;
 523         case 2:
 524             result = BASE16_2;
 525             break;
 526         case 3:
 527             result = BASE16_3;
 528             break;
 529         default:
 530             for (int j = 1; j < i; j++) {
 531                 result *= 16;
 532             }
 533         }
 534         return result;
 535     }
 536 
 537     /**
 538      * Reads the lines of the configuration file. All include and includedir
 539      * directives are resolved by calling this method recursively.
 540      *
 541      * @param file the krb5.conf file, must be absolute
 542      * @param content the lines. Comment and empty lines are removed,
 543      *                all lines trimmed, include and includedir
 544      *                directives resolved, unknown directives ignored
 545      * @param dups a set of Paths to check for possible infinite loop
 546      * @throws IOException if there is an I/O error
 547      */
 548     private static Void readConfigFileLines(
 549             Path file, List<String> content, Set<Path> dups)
 550             throws IOException {
 551 
 552         if (DEBUG) {
 553             System.out.println("Loading krb5 profile at " + file);
 554         }
 555         if (!file.isAbsolute()) {
 556             throw new IOException("Profile path not absolute");
 557         }
 558 
 559         if (!dups.add(file)) {
 560             throw new IOException("Profile path included more than once");
 561         }
 562 
 563         List<String> lines = Files.readAllLines(file);
 564 
 565         boolean inDirectives = true;
 566         for (String line: lines) {
 567             line = line.trim();
 568             if (line.isEmpty() || line.startsWith("#") || line.startsWith(";")) {
 569                 continue;
 570             }
 571             if (inDirectives) {
 572                 if (line.charAt(0) == '[') {
 573                     inDirectives = false;
 574                     content.add(line);
 575                 } else if (line.startsWith("includedir ")) {
 576                     Path dir = Paths.get(
 577                             line.substring("includedir ".length()).trim());
 578                     try (DirectoryStream<Path> files =
 579                                  Files.newDirectoryStream(dir)) {
 580                         for (Path p: files) {
 581                             if (Files.isDirectory(p)) continue;
 582                             String name = p.getFileName().toString();
 583                             if (name.matches("[a-zA-Z0-9_-]+") ||
 584                                     (!name.startsWith(".") &&
 585                                             name.endsWith(".conf"))) {
 586                                 // if dir is absolute, so is p
 587                                 readConfigFileLines(p, content, dups);
 588                             }
 589                         }
 590                     }
 591                 } else if (line.startsWith("include ")) {
 592                     readConfigFileLines(
 593                             Paths.get(line.substring("include ".length()).trim()),
 594                             content, dups);
 595                 } else {
 596                     // Unsupported directives
 597                     if (DEBUG) {
 598                         System.out.println("Unknown directive: " + line);
 599                     }
 600                 }
 601             } else {
 602                 content.add(line);
 603             }
 604         }
 605         return null;
 606     }
 607 
 608     /**
 609      * Reads the configuration file and return normalized lines.
 610      * If the original file is:
 611      *
 612      *     [realms]
 613      *     EXAMPLE.COM =
 614      *     {
 615      *         kdc = kerberos.example.com
 616      *         ...
 617      *     }
 618      *     ...
 619      *
 620      * The result will be (no indentations):
 621      *
 622      *     {
 623      *         realms = {
 624      *             EXAMPLE.COM = {
 625      *                 kdc = kerberos.example.com
 626      *                 ...
 627      *             }
 628      *         }
 629      *         ...
 630      *     }
 631      *
 632      * @param fileName the configuration file
 633      * @return normalized lines
 634      */
 635     private List<String> loadConfigFile(final String fileName)
 636             throws IOException, KrbException {
 637 
 638         List<String> result = new ArrayList<>();
 639         List<String> raw = new ArrayList<>();
 640         Set<Path> dupsCheck = new HashSet<>();
 641 
 642         try {
 643             Path fullp = AccessController.doPrivileged((PrivilegedAction<Path>)
 644                         () -> Paths.get(fileName).toAbsolutePath(),
 645                     null,
 646                     new PropertyPermission("user.dir", "read"));
 647             AccessController.doPrivileged(
 648                     new PrivilegedExceptionAction<Void>() {
 649                         @Override
 650                         public Void run() throws IOException {
 651                             Path path = Paths.get(fileName);
 652                             if (!Files.exists(path)) {
 653                                 // This is OK. There are other ways to get
 654                                 // Kerberos 5 settings
 655                                 return null;
 656                             } else {
 657                                 return readConfigFileLines(
 658                                         fullp, raw, dupsCheck);
 659                             }
 660                         }
 661                     },
 662                     null,
 663                     // include/includedir can go anywhere
 664                     new FilePermission("<<ALL FILES>>", "read"));
 665         } catch (java.security.PrivilegedActionException pe) {
 666             throw (IOException)pe.getException();
 667         }
 668         String previous = null;
 669         for (String line: raw) {
 670             if (line.startsWith("[")) {
 671                 if (!line.endsWith("]")) {
 672                     throw new KrbException("Illegal config content:"
 673                             + line);
 674                 }
 675                 if (previous != null) {
 676                     result.add(previous);
 677                     result.add("}");
 678                 }
 679                 String title = line.substring(
 680                         1, line.length()-1).trim();
 681                 if (title.isEmpty()) {
 682                     throw new KrbException("Illegal config content:"
 683                             + line);
 684                 }
 685                 previous = title + " = {";
 686             } else if (line.startsWith("{")) {
 687                 if (previous == null) {
 688                     throw new KrbException(
 689                         "Config file should not start with \"{\"");
 690                 }
 691                 previous += " {";
 692                 if (line.length() > 1) {
 693                     // { and content on the same line
 694                     result.add(previous);
 695                     previous = line.substring(1).trim();
 696                 }
 697             } else {
 698                 if (previous == null) {
 699                     // This won't happen, because before a section
 700                     // all directives have been resolved
 701                     throw new KrbException(
 702                         "Config file must starts with a section");
 703                 }
 704                 result.add(previous);
 705                 previous = line;
 706             }
 707         }
 708         if (previous != null) {
 709             result.add(previous);
 710             result.add("}");
 711         }
 712         return result;
 713     }
 714 
 715     /**
 716      * Parses the input lines to a hashtable. The key would be section names
 717      * (libdefaults, realms, domain_realms, etc), and the value would be
 718      * another hashtable which contains the key-value pairs inside the section.
 719      * The value of this sub-hashtable can be another hashtable containing
 720      * another sub-sub-section or a non-empty vector of strings for final values
 721      * (even if there is only one value defined).
 722      * <p>
 723      * For top-level sections with duplicates names, their contents are merged.
 724      * For sub-sections the former overwrites the latter. For final values,
 725      * they are stored in a vector in their appearing order. Please note these
 726      * values must appear in the same sub-section. Otherwise, the sub-section
 727      * appears first should have already overridden the others.
 728      * <p>
 729      * As a corner case, if the same name is used as both a section name and a
 730      * value name, the first appearance decides the type. That is to say, if the
 731      * first one is for a section, all latter appearances are ignored. If it's
 732      * a value, latter appearances as sections are ignored, but those as values
 733      * are added to the vector.
 734      * <p>
 735      * The behavior described above is compatible to other krb5 implementations
 736      * but it's not decumented publicly anywhere. the best practice is not to
 737      * assume any kind of override functionality and only specify values for
 738      * a particular key in one place.
 739      *
 740      * @param v the normalized input as return by loadConfigFile
 741      * @throws KrbException if there is a file format error
 742      */
 743     @SuppressWarnings("unchecked")
 744     private Hashtable<String,Object> parseStanzaTable(List<String> v)
 745             throws KrbException {
 746         Hashtable<String,Object> current = stanzaTable;
 747         for (String line: v) {
 748             // There are only 3 kinds of lines
 749             // 1. a = b
 750             // 2. a = {
 751             // 3. }
 752             if (line.equals("}")) {
 753                 // Go back to parent, see below
 754                 current = (Hashtable<String,Object>)current.remove(" PARENT ");
 755                 if (current == null) {
 756                     throw new KrbException("Unmatched close brace");
 757                 }
 758             } else {
 759                 int pos = line.indexOf('=');
 760                 if (pos < 0) {
 761                     throw new KrbException("Illegal config content:" + line);
 762                 }
 763                 String key = line.substring(0, pos).trim();
 764                 String value = unquote(line.substring(pos + 1));
 765                 if (value.equals("{")) {
 766                     Hashtable<String,Object> subTable;
 767                     if (current == stanzaTable) {
 768                         key = key.toLowerCase(Locale.US);
 769                     }
 770                     // When there are dup names for sections
 771                     if (current.containsKey(key)) {
 772                         if (current == stanzaTable) {   // top-level, merge
 773                             // The value at top-level must be another Hashtable
 774                             subTable = (Hashtable<String,Object>)current.get(key);
 775                         } else {                        // otherwise, ignored
 776                             // read and ignore it (do not put into current)
 777                             subTable = new Hashtable<>();
 778                         }
 779                     } else {
 780                         subTable = new Hashtable<>();
 781                         current.put(key, subTable);
 782                     }
 783                     // A special entry for its parent. Put whitespaces around,
 784                     // so will never be confused with a normal key
 785                     subTable.put(" PARENT ", current);
 786                     current = subTable;
 787                 } else {
 788                     Vector<String> values;
 789                     if (current.containsKey(key)) {
 790                         Object obj = current.get(key);
 791                         if (obj instanceof Vector) {
 792                             // String values are merged
 793                             values = (Vector<String>)obj;
 794                             values.add(value);
 795                         } else {
 796                             // If a key shows as section first and then a value,
 797                             // ignore the value.
 798                         }
 799                     } else {
 800                         values = new Vector<String>();
 801                         values.add(value);
 802                         current.put(key, values);
 803                     }
 804                 }
 805             }
 806         }
 807         if (current != stanzaTable) {
 808             throw new KrbException("Not closed");
 809         }
 810         return current;
 811     }
 812 
 813     /**
 814      * Gets the default Java configuration file name.
 815      *
 816      * If the system property "java.security.krb5.conf" is defined, we'll
 817      * use its value, no matter if the file exists or not. Otherwise, we
 818      * will look at $JAVA_HOME/conf/security directory with "krb5.conf" name,
 819      * and return it if the file exists.
 820      *
 821      * The method returns null if it cannot find a Java config file.
 822      */
 823     private String getJavaFileName() {
 824         String name = GetPropertyAction
 825                 .privilegedGetProperty("java.security.krb5.conf");
 826         if (name == null) {
 827             name = GetPropertyAction.privilegedGetProperty("java.home")
 828                     + File.separator + "conf" + File.separator + "security"
 829                     + File.separator + "krb5.conf";
 830             if (!fileExists(name)) {
 831                 name = null;
 832             }
 833         }
 834         if (DEBUG) {
 835             System.out.println("Java config name: " + name);
 836         }
 837         return name;
 838     }
 839 
 840     /**
 841      * Gets the default native configuration file name.
 842      *
 843      * Depending on the OS type, the method returns the default native
 844      * kerberos config file name, which is at windows directory with
 845      * the name of "krb5.ini" for Windows, /etc/krb5/krb5.conf for Solaris,
 846      * /etc/krb5.conf otherwise. Mac OSX X has a different file name.
 847      *
 848      * Note: When the Terminal Service is started in Windows (from 2003),
 849      * there are two kinds of Windows directories: A system one (say,
 850      * C:\Windows), and a user-private one (say, C:\Users\Me\Windows).
 851      * We will first look for krb5.ini in the user-private one. If not
 852      * found, try the system one instead.
 853      *
 854      * This method will always return a non-null non-empty file name,
 855      * even if that file does not exist.
 856      */
 857     private String getNativeFileName() {
 858         String name = null;
 859         String osname = GetPropertyAction.privilegedGetProperty("os.name");
 860         if (osname.startsWith("Windows")) {
 861             try {
 862                 Credentials.ensureLoaded();
 863             } catch (Exception e) {
 864                 // ignore exceptions
 865             }
 866             if (Credentials.alreadyLoaded) {
 867                 String path = getWindowsDirectory(false);
 868                 if (path != null) {
 869                     if (path.endsWith("\\")) {
 870                         path = path + "krb5.ini";
 871                     } else {
 872                         path = path + "\\krb5.ini";
 873                     }
 874                     if (fileExists(path)) {
 875                         name = path;
 876                     }
 877                 }
 878                 if (name == null) {
 879                     path = getWindowsDirectory(true);
 880                     if (path != null) {
 881                         if (path.endsWith("\\")) {
 882                             path = path + "krb5.ini";
 883                         } else {
 884                             path = path + "\\krb5.ini";
 885                         }
 886                         name = path;
 887                     }
 888                 }
 889             }
 890             if (name == null) {
 891                 name = "c:\\winnt\\krb5.ini";
 892             }
 893         } else if (osname.startsWith("SunOS")) {
 894             name =  "/etc/krb5/krb5.conf";
 895         } else if (osname.contains("OS X")) {
 896             name = findMacosConfigFile();
 897         } else {
 898             name =  "/etc/krb5.conf";
 899         }
 900         if (DEBUG) {
 901             System.out.println("Native config name: " + name);
 902         }
 903         return name;
 904     }
 905 
 906     private String findMacosConfigFile() {
 907         String userHome = GetPropertyAction.privilegedGetProperty("user.home");
 908         final String PREF_FILE = "/Library/Preferences/edu.mit.Kerberos";
 909         String userPrefs = userHome + PREF_FILE;
 910 
 911         if (fileExists(userPrefs)) {
 912             return userPrefs;
 913         }
 914 
 915         if (fileExists(PREF_FILE)) {
 916             return PREF_FILE;
 917         }
 918 
 919         return "/etc/krb5.conf";
 920     }
 921 
 922     private static String unquote(String s) {
 923         s = s.trim();
 924         if (s.length() >= 2 &&
 925                 ((s.charAt(0) == '"' && s.charAt(s.length()-1) == '"') ||
 926                  (s.charAt(0) == '\'' && s.charAt(s.length()-1) == '\''))) {
 927             s = s.substring(1, s.length()-1).trim();
 928         }
 929         return s;
 930     }
 931 
 932     /**
 933      * For testing purpose. This method lists all information being parsed from
 934      * the configuration file to the hashtable.
 935      */
 936     public void listTable() {
 937         System.out.println(this);
 938     }
 939 
 940     /**
 941      * Returns all etypes specified in krb5.conf for the given configName,
 942      * or all the builtin defaults. This result is always non-empty.
 943      * If no etypes are found, an exception is thrown.
 944      */
 945     public int[] defaultEtype(String configName) throws KrbException {
 946         String default_enctypes;
 947         default_enctypes = get("libdefaults", configName);
 948         int[] etype;
 949         if (default_enctypes == null) {
 950             if (DEBUG) {
 951                 System.out.println("Using builtin default etypes for " +
 952                     configName);
 953             }
 954             etype = EType.getBuiltInDefaults();
 955         } else {
 956             String delim = " ";
 957             StringTokenizer st;
 958             for (int j = 0; j < default_enctypes.length(); j++) {
 959                 if (default_enctypes.substring(j, j + 1).equals(",")) {
 960                     // only two delimiters are allowed to use
 961                     // according to Kerberos DCE doc.
 962                     delim = ",";
 963                     break;
 964                 }
 965             }
 966             st = new StringTokenizer(default_enctypes, delim);
 967             int len = st.countTokens();
 968             ArrayList<Integer> ls = new ArrayList<>(len);
 969             int type;
 970             for (int i = 0; i < len; i++) {
 971                 type = Config.getType(st.nextToken());
 972                 if (type != -1 && EType.isSupported(type)) {
 973                     ls.add(type);
 974                 }
 975             }
 976             if (ls.isEmpty()) {
 977                 throw new KrbException("no supported default etypes for "
 978                         + configName);
 979             } else {
 980                 etype = new int[ls.size()];
 981                 for (int i = 0; i < etype.length; i++) {
 982                     etype[i] = ls.get(i);
 983                 }
 984             }
 985         }
 986 
 987         if (DEBUG) {
 988             System.out.print("default etypes for " + configName + ":");
 989             for (int i = 0; i < etype.length; i++) {
 990                 System.out.print(" " + etype[i]);
 991             }
 992             System.out.println(".");
 993         }
 994         return etype;
 995     }
 996 
 997 
 998     /**
 999      * Get the etype and checksum value for the specified encryption and
1000      * checksum type.
1001      *
1002      */
1003     /*
1004      * This method converts the string representation of encryption type and
1005      * checksum type to int value that can be later used by EType and
1006      * Checksum classes.
1007      */
1008     public static int getType(String input) {
1009         int result = -1;
1010         if (input == null) {
1011             return result;
1012         }
1013         if (input.startsWith("d") || (input.startsWith("D"))) {
1014             if (input.equalsIgnoreCase("des-cbc-crc")) {
1015                 result = EncryptedData.ETYPE_DES_CBC_CRC;
1016             } else if (input.equalsIgnoreCase("des-cbc-md5")) {
1017                 result = EncryptedData.ETYPE_DES_CBC_MD5;
1018             } else if (input.equalsIgnoreCase("des-mac")) {
1019                 result = Checksum.CKSUMTYPE_DES_MAC;
1020             } else if (input.equalsIgnoreCase("des-mac-k")) {
1021                 result = Checksum.CKSUMTYPE_DES_MAC_K;
1022             } else if (input.equalsIgnoreCase("des-cbc-md4")) {
1023                 result = EncryptedData.ETYPE_DES_CBC_MD4;
1024             } else if (input.equalsIgnoreCase("des3-cbc-sha1") ||
1025                 input.equalsIgnoreCase("des3-hmac-sha1") ||
1026                 input.equalsIgnoreCase("des3-cbc-sha1-kd") ||
1027                 input.equalsIgnoreCase("des3-cbc-hmac-sha1-kd")) {
1028                 result = EncryptedData.ETYPE_DES3_CBC_HMAC_SHA1_KD;
1029             }
1030         } else if (input.startsWith("a") || (input.startsWith("A"))) {
1031             // AES
1032             if (input.equalsIgnoreCase("aes128-cts") ||
1033                     input.equalsIgnoreCase("aes128-sha1") ||
1034                     input.equalsIgnoreCase("aes128-cts-hmac-sha1-96")) {
1035                 result = EncryptedData.ETYPE_AES128_CTS_HMAC_SHA1_96;
1036             } else if (input.equalsIgnoreCase("aes256-cts") ||
1037                     input.equalsIgnoreCase("aes256-sha1") ||
1038                     input.equalsIgnoreCase("aes256-cts-hmac-sha1-96")) {
1039                 result = EncryptedData.ETYPE_AES256_CTS_HMAC_SHA1_96;
1040             } else if (input.equalsIgnoreCase("aes128-sha2") ||
1041                     input.equalsIgnoreCase("aes128-cts-hmac-sha256-128")) {
1042                 result = EncryptedData.ETYPE_AES128_CTS_HMAC_SHA256_128;
1043             } else if (input.equalsIgnoreCase("aes256-sha2") ||
1044                     input.equalsIgnoreCase("aes256-cts-hmac-sha384-192")) {
1045                 result = EncryptedData.ETYPE_AES256_CTS_HMAC_SHA384_192;
1046             // ARCFOUR-HMAC
1047             } else if (input.equalsIgnoreCase("arcfour-hmac") ||
1048                    input.equalsIgnoreCase("arcfour-hmac-md5")) {
1049                 result = EncryptedData.ETYPE_ARCFOUR_HMAC;
1050             }
1051         // RC4-HMAC
1052         } else if (input.equalsIgnoreCase("rc4-hmac")) {
1053             result = EncryptedData.ETYPE_ARCFOUR_HMAC;
1054         } else if (input.equalsIgnoreCase("CRC32")) {
1055             result = Checksum.CKSUMTYPE_CRC32;
1056         } else if (input.startsWith("r") || (input.startsWith("R"))) {
1057             if (input.equalsIgnoreCase("rsa-md5")) {
1058                 result = Checksum.CKSUMTYPE_RSA_MD5;
1059             } else if (input.equalsIgnoreCase("rsa-md5-des")) {
1060                 result = Checksum.CKSUMTYPE_RSA_MD5_DES;
1061             }
1062         } else if (input.equalsIgnoreCase("hmac-sha1-des3-kd")) {
1063             result = Checksum.CKSUMTYPE_HMAC_SHA1_DES3_KD;
1064         } else if (input.equalsIgnoreCase("hmac-sha1-96-aes128")) {
1065             result = Checksum.CKSUMTYPE_HMAC_SHA1_96_AES128;
1066         } else if (input.equalsIgnoreCase("hmac-sha1-96-aes256")) {
1067             result = Checksum.CKSUMTYPE_HMAC_SHA1_96_AES256;
1068         } else if (input.equalsIgnoreCase("hmac-sha256-128-aes128")) {
1069             result = Checksum.CKSUMTYPE_HMAC_SHA256_128_AES128;
1070         } else if (input.equalsIgnoreCase("hmac-sha384-192-aes256")) {
1071             result = Checksum.CKSUMTYPE_HMAC_SHA384_192_AES256;
1072         } else if (input.equalsIgnoreCase("hmac-md5-rc4") ||
1073                 input.equalsIgnoreCase("hmac-md5-arcfour") ||
1074                 input.equalsIgnoreCase("hmac-md5-enc")) {
1075             result = Checksum.CKSUMTYPE_HMAC_MD5_ARCFOUR;
1076         } else if (input.equalsIgnoreCase("NULL")) {
1077             result = EncryptedData.ETYPE_NULL;
1078         }
1079 
1080         return result;
1081     }
1082 
1083     /**
1084      * Resets the default kdc realm.
1085      * We do not need to synchronize these methods since assignments are atomic
1086      *
1087      * This method was useless. Kept here in case some class still calls it.
1088      */
1089     public void resetDefaultRealm(String realm) {
1090         if (DEBUG) {
1091             System.out.println(">>> Config try resetting default kdc " + realm);
1092         }
1093     }
1094 
1095     /**
1096      * Check to use addresses in tickets
1097      * use addresses if "no_addresses" or "noaddresses" is set to false
1098      */
1099     public boolean useAddresses() {
1100         return getBooleanObject("libdefaults", "no_addresses") == Boolean.FALSE ||
1101                 getBooleanObject("libdefaults", "noaddresses") == Boolean.FALSE;
1102     }
1103 
1104     /**
1105      * Check if need to use DNS to locate Kerberos services for name. If not
1106      * defined, check dns_fallback, whose default value is true.
1107      */
1108     private boolean useDNS(String name, boolean defaultValue) {
1109         Boolean value = getBooleanObject("libdefaults", name);
1110         if (value != null) {
1111             return value.booleanValue();
1112         }
1113         value = getBooleanObject("libdefaults", "dns_fallback");
1114         if (value != null) {
1115             return value.booleanValue();
1116         }
1117         return defaultValue;
1118     }
1119 
1120     /**
1121      * Check if need to use DNS to locate the KDC
1122      */
1123     private boolean useDNS_KDC() {
1124         return useDNS("dns_lookup_kdc", true);
1125     }
1126 
1127     /*
1128      * Check if need to use DNS to locate the Realm
1129      */
1130     private boolean useDNS_Realm() {
1131         return useDNS("dns_lookup_realm", false);
1132     }
1133 
1134     /**
1135      * Gets default realm.
1136      * @throws KrbException where no realm can be located
1137      * @return the default realm, always non null
1138      */
1139     public String getDefaultRealm() throws KrbException {
1140         if (defaultRealm != null) {
1141             return defaultRealm;
1142         }
1143         Exception cause = null;
1144         String realm = get("libdefaults", "default_realm");
1145         if ((realm == null) && useDNS_Realm()) {
1146             // use DNS to locate Kerberos realm
1147             try {
1148                 realm = getRealmFromDNS();
1149             } catch (KrbException ke) {
1150                 cause = ke;
1151             }
1152         }
1153         if (realm == null) {
1154             realm = java.security.AccessController.doPrivileged(
1155                     new java.security.PrivilegedAction<String>() {
1156                 @Override
1157                 public String run() {
1158                     String osname = System.getProperty("os.name");
1159                     if (osname.startsWith("Windows")) {
1160                         return System.getenv("USERDNSDOMAIN");
1161                     }
1162                     return null;
1163                 }
1164             });
1165         }
1166         if (realm == null) {
1167             KrbException ke = new KrbException("Cannot locate default realm");
1168             if (cause != null) {
1169                 ke.initCause(cause);
1170             }
1171             throw ke;
1172         }
1173         return realm;
1174     }
1175 
1176     /**
1177      * Returns a list of KDC's with each KDC separated by a space
1178      *
1179      * @param realm the realm for which the KDC list is desired
1180      * @throws KrbException if there's no way to find KDC for the realm
1181      * @return the list of KDCs separated by a space, always non null
1182      */
1183     public String getKDCList(String realm) throws KrbException {
1184         if (realm == null) {
1185             realm = getDefaultRealm();
1186         }
1187         if (realm.equalsIgnoreCase(defaultRealm)) {
1188             return defaultKDC;
1189         }
1190         Exception cause = null;
1191         String kdcs = getAll("realms", realm, "kdc");
1192         if ((kdcs == null) && useDNS_KDC()) {
1193             // use DNS to locate KDC
1194             try {
1195                 kdcs = getKDCFromDNS(realm);
1196             } catch (KrbException ke) {
1197                 cause = ke;
1198             }
1199         }
1200         if (kdcs == null) {
1201             kdcs = java.security.AccessController.doPrivileged(
1202                     new java.security.PrivilegedAction<String>() {
1203                 @Override
1204                 public String run() {
1205                     String osname = System.getProperty("os.name");
1206                     if (osname.startsWith("Windows")) {
1207                         String logonServer = System.getenv("LOGONSERVER");
1208                         if (logonServer != null
1209                                 && logonServer.startsWith("\\\\")) {
1210                             logonServer = logonServer.substring(2);
1211                         }
1212                         return logonServer;
1213                     }
1214                     return null;
1215                 }
1216             });
1217         }
1218         if (kdcs == null) {
1219             if (defaultKDC != null) {
1220                 return defaultKDC;
1221             }
1222             KrbException ke = new KrbException("Cannot locate KDC");
1223             if (cause != null) {
1224                 ke.initCause(cause);
1225             }
1226             throw ke;
1227         }
1228         return kdcs;
1229     }
1230 
1231     /**
1232      * Locate Kerberos realm using DNS
1233      *
1234      * @return the Kerberos realm
1235      */
1236     private String getRealmFromDNS() throws KrbException {
1237         // use DNS to locate Kerberos realm
1238         String realm = null;
1239         String hostName = null;
1240         try {
1241             hostName = InetAddress.getLocalHost().getCanonicalHostName();
1242         } catch (UnknownHostException e) {
1243             KrbException ke = new KrbException(Krb5.KRB_ERR_GENERIC,
1244                 "Unable to locate Kerberos realm: " + e.getMessage());
1245             ke.initCause(e);
1246             throw (ke);
1247         }
1248         // get the domain realm mapping from the configuration
1249         String mapRealm = PrincipalName.mapHostToRealm(hostName);
1250         if (mapRealm == null) {
1251             // No match. Try search and/or domain in /etc/resolv.conf
1252             List<String> srchlist = ResolverConfiguration.open().searchlist();
1253             for (String domain: srchlist) {
1254                 realm = checkRealm(domain);
1255                 if (realm != null) {
1256                     break;
1257                 }
1258             }
1259         } else {
1260             realm = checkRealm(mapRealm);
1261         }
1262         if (realm == null) {
1263             throw new KrbException(Krb5.KRB_ERR_GENERIC,
1264                                 "Unable to locate Kerberos realm");
1265         }
1266         return realm;
1267     }
1268 
1269     /**
1270      * Check if the provided realm is the correct realm
1271      * @return the realm if correct, or null otherwise
1272      */
1273     private static String checkRealm(String mapRealm) {
1274         if (DEBUG) {
1275             System.out.println("getRealmFromDNS: trying " + mapRealm);
1276         }
1277         String[] records = null;
1278         String newRealm = mapRealm;
1279         while ((records == null) && (newRealm != null)) {
1280             // locate DNS TXT record
1281             records = KrbServiceLocator.getKerberosService(newRealm);
1282             newRealm = Realm.parseRealmComponent(newRealm);
1283             // if no DNS TXT records found, try again using sub-realm
1284         }
1285         if (records != null) {
1286             for (int i = 0; i < records.length; i++) {
1287                 if (records[i].equalsIgnoreCase(mapRealm)) {
1288                     return records[i];
1289                 }
1290             }
1291         }
1292         return null;
1293     }
1294 
1295     /**
1296      * Locate KDC using DNS
1297      *
1298      * @param realm the realm for which the master KDC is desired
1299      * @return the KDC
1300      */
1301     private String getKDCFromDNS(String realm) throws KrbException {
1302         // use DNS to locate KDC
1303         String kdcs = "";
1304         String[] srvs = null;
1305         // locate DNS SRV record using UDP
1306         if (DEBUG) {
1307             System.out.println("getKDCFromDNS using UDP");
1308         }
1309         srvs = KrbServiceLocator.getKerberosService(realm, "_udp");
1310         if (srvs == null) {
1311             // locate DNS SRV record using TCP
1312             if (DEBUG) {
1313                 System.out.println("getKDCFromDNS using TCP");
1314             }
1315             srvs = KrbServiceLocator.getKerberosService(realm, "_tcp");
1316         }
1317         if (srvs == null) {
1318             // no DNS SRV records
1319             throw new KrbException(Krb5.KRB_ERR_GENERIC,
1320                 "Unable to locate KDC for realm " + realm);
1321         }
1322         if (srvs.length == 0) {
1323             return null;
1324         }
1325         for (int i = 0; i < srvs.length; i++) {
1326             kdcs += srvs[i].trim() + " ";
1327         }
1328         kdcs = kdcs.trim();
1329         if (kdcs.equals("")) {
1330             return null;
1331         }
1332         return kdcs;
1333     }
1334 
1335     private boolean fileExists(String name) {
1336         return java.security.AccessController.doPrivileged(
1337                                 new FileExistsAction(name));
1338     }
1339 
1340     static class FileExistsAction
1341         implements java.security.PrivilegedAction<Boolean> {
1342 
1343         private String fileName;
1344 
1345         public FileExistsAction(String fileName) {
1346             this.fileName = fileName;
1347         }
1348 
1349         public Boolean run() {
1350             return new File(fileName).exists();
1351         }
1352     }
1353 
1354     // Shows the content of the Config object for debug purpose.
1355     //
1356     // {
1357     //      libdefaults = {
1358     //          default_realm = R
1359     //      }
1360     //      realms = {
1361     //          R = {
1362     //              kdc = [k1,k2]
1363     //          }
1364     //      }
1365     // }
1366 
1367     @Override
1368     public String toString() {
1369         StringBuffer sb = new StringBuffer();
1370         toStringInternal("", stanzaTable, sb);
1371         return sb.toString();
1372     }
1373     private static void toStringInternal(String prefix, Object obj,
1374             StringBuffer sb) {
1375         if (obj instanceof String) {
1376             // A string value, just print it
1377             sb.append(obj).append('\n');
1378         } else if (obj instanceof Hashtable) {
1379             // A table, start a new sub-section...
1380             Hashtable<?, ?> tab = (Hashtable<?, ?>)obj;
1381             sb.append("{\n");
1382             for (Object o: tab.keySet()) {
1383                 // ...indent, print "key = ", and
1384                 sb.append(prefix).append("    ").append(o).append(" = ");
1385                 // ...go recursively into value
1386                 toStringInternal(prefix + "    ", tab.get(o), sb);
1387             }
1388             sb.append(prefix).append("}\n");
1389         } else if (obj instanceof Vector) {
1390             // A vector of strings, print them inside [ and ]
1391             Vector<?> v = (Vector<?>)obj;
1392             sb.append("[");
1393             boolean first = true;
1394             for (Object o: v.toArray()) {
1395                 if (!first) sb.append(",");
1396                 sb.append(o);
1397                 first = false;
1398             }
1399             sb.append("]\n");
1400         }
1401     }
1402 }