1 /*
   2  * Copyright (c) 2016, 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.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 import java.io.*;
  25 import java.net.DatagramSocket;
  26 import java.net.ServerSocket;
  27 import java.nio.file.Files;
  28 import java.nio.file.Paths;
  29 import java.security.Security;
  30 import java.util.ArrayList;
  31 import java.util.List;
  32 import java.util.Random;
  33 import java.util.regex.Matcher;
  34 import java.util.regex.Pattern;
  35 import javax.security.auth.login.LoginException;
  36 import sun.security.krb5.Asn1Exception;
  37 import sun.security.krb5.Config;
  38 
  39 /*
  40  * @test
  41  * @bug 8164656
  42  * @run main/othervm KdcPolicy udp
  43  * @run main/othervm KdcPolicy tcp
  44  * @summary krb5.kdc.bad.policy test
  45  */
  46 public class KdcPolicy {
  47 
  48     // Is this test on UDP?
  49     static boolean udp;
  50 
  51     public static void main(String[] args) throws Exception {
  52 
  53         udp = args[0].equals("udp");
  54 
  55         try {
  56             main0();
  57         } catch (LoginException le) {
  58             Throwable cause = le.getCause();
  59             if (cause instanceof Asn1Exception) {
  60                 System.out.println("Another process sends a packet to " +
  61                         "this server. Ignored.");
  62                 return;
  63             }
  64             throw le;
  65         }
  66     }
  67 
  68     static DebugMatcher cm = new DebugMatcher();
  69 
  70     static void main0() throws Exception {
  71 
  72         System.setProperty("sun.security.krb5.debug", "true");
  73 
  74         // One real KDC. Must be created before fake KDCs
  75         // to read the TestHosts file.
  76         OneKDC kdc = new OneKDC(null);
  77 
  78         // Two fake KDCs, d1 and d2 only listen but do not respond.
  79 
  80         if (udp) {
  81             try (DatagramSocket d1 = new DatagramSocket();
  82                  DatagramSocket d2 = new DatagramSocket()) {
  83                 run(d1.getLocalPort(), d2.getLocalPort(), kdc.getPort());
  84             }
  85         } else {
  86             try (ServerSocket d1 = new ServerSocket(0);
  87                  ServerSocket d2 = new ServerSocket(0)) {
  88                 run(d1.getLocalPort(), d2.getLocalPort(), kdc.getPort());
  89             }
  90         }
  91     }
  92 
  93     static void run(int p1, int p2, int p3) throws Exception {
  94 
  95         // cm.kdc() will return a and b for fake KDCs, and c for real KDC.
  96         cm.addPort(-1).addPort(p1).addPort(p2).addPort(p3);
  97 
  98         System.setProperty("java.security.krb5.conf", "alternative-krb5.conf");
  99 
 100         // Check default timeout is 30s. Use real KDC only, otherwise too
 101         // slow to wait for timeout.
 102         writeConf(-1, -1, p3);
 103         test("c30000c30000");
 104 
 105         // 1. Default policy is tryLast
 106         //Security.setProperty("krb5.kdc.bad.policy", "tryLast");
 107 
 108         // Need a real KDC, otherwise there is no last good.
 109         // This test waste 3 seconds waiting for d1 to timeout.
 110         // It is possible the real KDC cannot fulfil the request
 111         // in 3s, so it might fail (either 1st time or 2nd time).
 112         writeConf(1, 3000, p1, p3);
 113         test("a3000c3000c3000|a3000c3000-|a3000c3000c3000-");
 114 
 115         // If a test case won't use a real KDC, it can be sped up.
 116         writeConf(3, 5, p1, p2);
 117         test("a5a5a5b5b5b5-");  // default max_retries == 3
 118         test("a5a5a5b5b5b5-");  // all bad means no bad
 119 
 120         // 2. No policy.
 121         Security.setProperty("krb5.kdc.bad.policy", "");
 122         Config.refresh();
 123 
 124         // This case needs a real KDC, otherwise, all bad means no
 125         // bad and we cannot tell the difference. This case waste 3
 126         // seconds on d1 to timeout twice. It is possible the real KDC
 127         // cannot fulfil the request within 3s, so it might fail
 128         // (either 1st time or 2nd time).
 129         writeConf(1, 3000, p1, p3);
 130         test("a3000c3000a3000c3000|a3000c3000-|a3000c3000a3000c3000-");
 131 
 132         // 3. tryLess with no argument means tryLess:1,5000
 133         Security.setProperty("krb5.kdc.bad.policy", "tryLess");
 134 
 135         // This case will waste 11s. We are checking that the default
 136         // value of 5000 in tryLess is only used if it's less than timeout
 137         // in krb5.conf
 138         writeConf(1, 6000, p1);
 139         test("a6000-"); // timeout in krb5.conf is 6s
 140         test("a5000-"); // tryLess to 5s. This line can be made faster if
 141                         // d1 is a read KDC, but we have no existing method
 142                         // to start KDC on an existing ServerSocket (port).
 143 
 144         writeConf(-1, 4, p1, p2);
 145         test("a4a4a4b4b4b4-");  // default max_retries == 3
 146         test("a4b4-");          // tryLess to 1. And since 4 < 5000, use 4.
 147         Config.refresh();
 148         test("a4a4a4b4b4b4-");
 149 
 150         writeConf(5, 4, p1, p2);
 151         test("a4a4a4a4a4b4b4b4b4b4-"); // user-provided max_retries == 5
 152         test("a4b4-");
 153         Config.refresh();
 154         test("a4a4a4a4a4b4b4b4b4b4-");
 155 
 156         // 3. tryLess with arguments
 157         Security.setProperty("krb5.kdc.bad.policy",
 158                 "tryLess:2,5");
 159 
 160         writeConf(-1, 6, p1, p2);
 161         test("a6a6a6b6b6b6-");  // default max_retries == 3
 162         test("a5a5b5b5-");      // tryLess to 2
 163         Config.refresh();
 164         test("a6a6a6b6b6b6-");
 165 
 166         writeConf(5, 4, p1, p2);
 167         test("a4a4a4a4a4b4b4b4b4b4-");  // user-provided max_retries == 5
 168         test("a4a4b4b4-");              // tryLess to 2
 169         Config.refresh();
 170         test("a4a4a4a4a4b4b4b4b4b4-");
 171     }
 172 
 173     /**
 174      * Writes a krb5.conf file.
 175      * @param max max_retries, -1 if not set
 176      * @param to kdc_timeout, -1 if not set
 177      * @param ports where KDCs listen on
 178      */
 179     static void writeConf(int max, int to, int... ports) throws Exception {
 180 
 181         // content of krb5.conf
 182         String conf = "";
 183 
 184         // Extra settings in [libdefaults]
 185         String inDefaults = "";
 186 
 187         // Extra settings in [realms]
 188         String inRealm = "";
 189 
 190         // We will randomly put extra settings only in [libdefaults],
 191         // or in [realms] but with different values in [libdefaults],
 192         // to prove that settings in [realms] override those in [libdefaults].
 193         Random r = new Random();
 194 
 195         if (max > 0) {
 196             if (r.nextBoolean()) {
 197                 inDefaults += "max_retries = " + max + "\n";
 198             } else {
 199                 inRealm += "   max_retries = " + max + "\n";
 200                 inDefaults += "max_retries = " + (max + 1) + "\n";
 201             }
 202         }
 203 
 204         if (to > 0) {
 205             if (r.nextBoolean()) {
 206                 inDefaults += "kdc_timeout = " + to + "\n";
 207             } else {
 208                 inRealm += "   kdc_timeout = " + to + "\n";
 209                 inDefaults += "kdc_timeout = " + (to + 1) + "\n";
 210             }
 211         }
 212 
 213         if (udp) {
 214             if (r.nextBoolean()) {
 215                 inDefaults += "udp_preference_limit = 10000\n";
 216             } else if (r.nextBoolean()) {
 217                 inRealm += "   udp_preference_limit = 10000\n";
 218                 inDefaults += "udp_preference_limit = 1\n";
 219             } // else no settings means UDP
 220         } else {
 221             if (r.nextBoolean()) {
 222                 inDefaults += "udp_preference_limit = 1\n";
 223             } else {
 224                 inRealm += "   udp_preference_limit = 1\n";
 225                 inDefaults += "udp_preference_limit = 10000\n";
 226             }
 227         }
 228 
 229         conf = "[libdefaults]\n" +
 230                 "default_realm = " + OneKDC.REALM + "\n" +
 231                 inDefaults +
 232                 "\n" +
 233                 "[realms]\n" +
 234                 OneKDC.REALM + " = {\n";
 235 
 236         for (int port : ports) {
 237             conf += "   kdc = " + OneKDC.KDCHOST + ":" + port + "\n" +
 238                     inRealm;
 239         }
 240 
 241         conf += "}\n";
 242 
 243         Files.write(Paths.get("alternative-krb5.conf"), conf.getBytes());
 244         Config.refresh();
 245     }
 246 
 247     /**
 248      * One call of krb5 login. As long as the result matches one of expected,
 249      * the test is considered as success. The grammar of expected is
 250      *
 251      *    kdc#, timeout, kdc#, timeout, ..., optional "-" for failure
 252      */
 253     static void test(String... expected) throws Exception {
 254 
 255         System.out.println("------------------TEST----------------------");
 256         PrintStream oldOut = System.out;
 257         boolean failed = false;
 258         ByteArrayOutputStream bo = new ByteArrayOutputStream();
 259         System.setOut(new PrintStream(bo));
 260         try {
 261             Context.fromUserPass(OneKDC.USER, OneKDC.PASS, false);
 262         } catch (Exception e) {
 263             failed = true;
 264         } finally {
 265             System.setOut(oldOut);
 266         }
 267 
 268         String[] lines = new String(bo.toByteArray()).split("\n");
 269         StringBuilder sb = new StringBuilder();
 270         for (String line: lines) {
 271             if (cm.match(line)) {
 272                 if (udp != cm.isUDP()) {
 273                     sb.append("x");
 274                 }
 275                 sb.append(cm.kdc()).append(cm.timeout());
 276             }
 277         }
 278         if (failed) sb.append('-');
 279 
 280         String output = sb.toString();
 281 
 282         boolean found = false;
 283         for (String ex : expected) {
 284             if (output.matches(ex)) {
 285                 System.out.println("Expected: " + ex + ", actual " + output);
 286                 found = true;
 287                 break;
 288             }
 289         }
 290 
 291         if (!found) {
 292             System.out.println("--------------- ERROR START -------------");
 293             System.out.println(new String(bo.toByteArray()));
 294             System.out.println("--------------- ERROR END ---------------");
 295             throw new Exception("Does not match. Output is " + output);
 296         }
 297     }
 298 
 299     /**
 300      * A helper class to match the krb5 debug output:
 301      * >>> KDCCommunication: kdc=host UDP:11555, timeout=200,Attempt =1, #bytes=138
 302      *
 303      * Example:
 304      *  DebugMatcher cm = new DebugMatcher();
 305      *  cm.addPort(12345).addPort(11555);
 306      *  for (String line : debugOutput) {
 307      *      if (cm.match(line)) {
 308      *          System.out.printf("%c%d\n", cm.kdc(), cm.timeout());
 309      *          // shows b200 for the example above
 310      *      }
 311      *  }
 312      */
 313     static class DebugMatcher {
 314 
 315         static final Pattern re = Pattern.compile(
 316                 ">>> KDCCommunication: kdc=\\S+ (TCP|UDP):(\\d+), " +
 317                         "timeout=(\\d+),Attempt\\s*=(\\d+)");
 318 
 319         List<Integer> kdcPorts = new ArrayList<>();
 320         Matcher matcher;
 321 
 322         /**
 323          * Add KDC ports one by one. See {@link #kdc()}.
 324          */
 325         DebugMatcher addPort(int port) {
 326             if (port > 0) {
 327                 kdcPorts.add(port);
 328             } else {
 329                 kdcPorts.clear();
 330             }
 331             return this;
 332         }
 333 
 334         /**
 335          * When a line matches the ">>> KDCCommunication:" pattern. After a
 336          * match, the getters below can be called on this match.
 337          */
 338         boolean match(String line) {
 339             matcher = re.matcher(line);
 340             return matcher.find();
 341         }
 342 
 343         /**
 344          * Protocol of this match, "UDP" or "TCP".
 345          */
 346         boolean isUDP() {
 347             return matcher.group(1).equals("UDP");
 348         }
 349 
 350         /**
 351          * KDC for this match, "a" for the one 1st added bt addPort(), "b"
 352          * for second, etc. Undefined for not added.
 353          */
 354         char kdc() {
 355             int port = Integer.parseInt(matcher.group(2));
 356             return (char) (kdcPorts.indexOf(port) + 'a');
 357         }
 358 
 359         /**
 360          * Timeout value for this match.
 361          */
 362         int timeout() {
 363             return Integer.parseInt(matcher.group(3));
 364         }
 365     }
 366 }