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