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