1/*
2 * Copyright (c) 2006, 2011, 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
26package sun.security.krb5;
27
28import sun.security.krb5.internal.Krb5;
29
30import java.security.AccessController;
31import java.security.PrivilegedActionException;
32import java.security.PrivilegedExceptionAction;
33import java.util.Arrays;
34import java.util.Hashtable;
35import java.util.Random;
36import java.util.StringTokenizer;
37
38import javax.naming.*;
39import javax.naming.directory.*;
40import javax.naming.spi.NamingManager;
41
42/**
43 * This class discovers the location of Kerberos services by querying DNS,
44 * as defined in RFC 4120.
45 *
46 * @author Seema Malkani
47 * @since 1.7
48 */
49
50class KrbServiceLocator {
51
52    private static final String SRV_RR = "SRV";
53    private static final String[] SRV_RR_ATTR = new String[] {SRV_RR};
54
55    private static final String SRV_TXT = "TXT";
56    private static final String[] SRV_TXT_ATTR = new String[] {SRV_TXT};
57
58    private static final Random random = new Random();
59
60    private static final boolean DEBUG = Krb5.DEBUG;
61
62    private KrbServiceLocator() {
63    }
64
65    /**
66     * Locates the KERBEROS service for a given domain.
67     * Queries DNS for a list of KERBEROS Service Text Records (TXT) for a
68     * given domain name.
69     * Information on the mapping of DNS hostnames and domain names
70     * to Kerberos realms is stored using DNS TXT records
71     *
72     * @param realmName A string realm name.
73     * @return An ordered list of hostports for the Kerberos service or null if
74     *          the service has not been located.
75     */
76    static String[] getKerberosService(String realmName) {
77
78        // search realm in SRV TXT records
79        String dnsUrl = "dns:///_kerberos." + realmName;
80        String[] records = null;
81        try {
82            // Create the DNS context using NamingManager rather than using
83            // the initial context constructor. This avoids having the initial
84            // context constructor call itself (when processing the URL
85            // argument in the getAttributes call).
86            Context ctx = NamingManager.getURLContext("dns", new Hashtable<>(0));
87            if (!(ctx instanceof DirContext)) {
88                return null; // cannot create a DNS context
89            }
90            Attributes attrs = null;
91            try {
92                // both connect and accept are needed since DNS is thru UDP
93                attrs = AccessController.doPrivileged(
94                        (PrivilegedExceptionAction<Attributes>)
95                                () -> ((DirContext)ctx).getAttributes(
96                                        dnsUrl, SRV_TXT_ATTR),
97                        null,
98                        new java.net.SocketPermission("*", "connect,accept"));
99            } catch (PrivilegedActionException e) {
100                throw (NamingException)e.getCause();
101            }
102            Attribute attr;
103
104            if (attrs != null && ((attr = attrs.get(SRV_TXT)) != null)) {
105                int numValues = attr.size();
106                int numRecords = 0;
107                String[] txtRecords = new String[numValues];
108
109                // gather the text records
110                int i = 0;
111                int j = 0;
112                while (i < numValues) {
113                    try {
114                        txtRecords[j] = (String)attr.get(i);
115                        j++;
116                    } catch (Exception e) {
117                        // ignore bad value
118                    }
119                    i++;
120                }
121                numRecords = j;
122
123                // trim
124                if (numRecords < numValues) {
125                    String[] trimmed = new String[numRecords];
126                    System.arraycopy(txtRecords, 0, trimmed, 0, numRecords);
127                    records = trimmed;
128                } else {
129                    records = txtRecords;
130                }
131            }
132        } catch (NamingException e) {
133            // ignore
134        }
135        return records;
136    }
137
138    /**
139     * Locates the KERBEROS service for a given domain.
140     * Queries DNS for a list of KERBEROS Service Location Records (SRV) for a
141     * given domain name.
142     *
143     * @param realmName A string realm name.
144     * @param protocol the protocol string, can be "_udp" or "_tcp"
145     * @return An ordered list of hostports for the Kerberos service or null if
146     *          the service has not been located.
147     */
148    static String[] getKerberosService(String realmName, String protocol) {
149
150        String dnsUrl = "dns:///_kerberos." + protocol + "." + realmName;
151        String[] hostports = null;
152
153        try {
154            // Create the DNS context using NamingManager rather than using
155            // the initial context constructor. This avoids having the initial
156            // context constructor call itself (when processing the URL
157            // argument in the getAttributes call).
158            Context ctx = NamingManager.getURLContext("dns", new Hashtable<>(0));
159            if (!(ctx instanceof DirContext)) {
160                return null; // cannot create a DNS context
161            }
162
163            Attributes attrs = null;
164            try {
165                // both connect and accept are needed since DNS is thru UDP
166                attrs = AccessController.doPrivileged(
167                        (PrivilegedExceptionAction<Attributes>)
168                                () -> ((DirContext)ctx).getAttributes(
169                                        dnsUrl, SRV_RR_ATTR),
170                        null,
171                        new java.net.SocketPermission("*", "connect,accept"));
172            } catch (PrivilegedActionException e) {
173                throw (NamingException)e.getCause();
174            }
175
176            Attribute attr;
177
178            if (attrs != null && ((attr = attrs.get(SRV_RR)) != null)) {
179                int numValues = attr.size();
180                int numRecords = 0;
181                SrvRecord[] srvRecords = new SrvRecord[numValues];
182
183                // create the service records
184                int i = 0;
185                int j = 0;
186                while (i < numValues) {
187                    try {
188                        srvRecords[j] = new SrvRecord((String) attr.get(i));
189                        j++;
190                    } catch (Exception e) {
191                        // ignore bad value
192                    }
193                    i++;
194                }
195                numRecords = j;
196
197                // trim
198                if (numRecords < numValues) {
199                    SrvRecord[] trimmed = new SrvRecord[numRecords];
200                    System.arraycopy(srvRecords, 0, trimmed, 0, numRecords);
201                    srvRecords = trimmed;
202                }
203
204                // Sort the service records in ascending order of their
205                // priority value. For records with equal priority, move
206                // those with weight 0 to the top of the list.
207                if (numRecords > 1) {
208                    Arrays.sort(srvRecords);
209                }
210
211                // extract the host and port number from each service record
212                hostports = extractHostports(srvRecords);
213            }
214        } catch (NamingException e) {
215            // e.printStackTrace();
216            // ignore
217        }
218        return hostports;
219    }
220
221    /**
222     * Extract hosts and port numbers from a list of SRV records.
223     * An array of hostports is returned or null if none were found.
224     */
225    private static String[] extractHostports(SrvRecord[] srvRecords) {
226        String[] hostports = null;
227
228        int head = 0;
229        int tail = 0;
230        int sublistLength = 0;
231        int k = 0;
232        for (int i = 0; i < srvRecords.length; i++) {
233            if (hostports == null) {
234                hostports = new String[srvRecords.length];
235            }
236            // find the head and tail of the list of records having the same
237            // priority value.
238            head = i;
239            while (i < srvRecords.length - 1 &&
240                srvRecords[i].priority == srvRecords[i + 1].priority) {
241                i++;
242            }
243            tail = i;
244
245            // select hostports from the sublist
246            sublistLength = (tail - head) + 1;
247            for (int j = 0; j < sublistLength; j++) {
248                hostports[k++] = selectHostport(srvRecords, head, tail);
249            }
250        }
251        return hostports;
252    }
253
254    /*
255     * Randomly select a service record in the range [head, tail] and return
256     * its hostport value. Follows the algorithm in RFC 2782.
257     */
258    private static String selectHostport(SrvRecord[] srvRecords, int head,
259            int tail) {
260        if (head == tail) {
261            return srvRecords[head].hostport;
262        }
263
264        // compute the running sum for records between head and tail
265        int sum = 0;
266        for (int i = head; i <= tail; i++) {
267            if (srvRecords[i] != null) {
268                sum += srvRecords[i].weight;
269                srvRecords[i].sum = sum;
270            }
271        }
272        String hostport = null;
273
274        // If all records have zero weight, select first available one;
275        // otherwise, randomly select a record according to its weight
276        int target = (sum == 0 ? 0 : random.nextInt(sum + 1));
277        for (int i = head; i <= tail; i++) {
278            if (srvRecords[i] != null && srvRecords[i].sum >= target) {
279                hostport = srvRecords[i].hostport;
280                srvRecords[i] = null; // make this record unavailable
281                break;
282            }
283        }
284        return hostport;
285    }
286
287/**
288 * This class holds a DNS service (SRV) record.
289 * See http://www.ietf.org/rfc/rfc2782.txt
290 */
291
292static class SrvRecord implements Comparable<SrvRecord> {
293
294    int priority;
295    int weight;
296    int sum;
297    String hostport;
298
299    /**
300     * Creates a service record object from a string record.
301     * DNS supplies the string record in the following format:
302     * <pre>
303     *          <Priority> " " <Weight> " " <Port> " " <Host>
304     * </pre>
305     */
306    SrvRecord(String srvRecord) throws Exception {
307        StringTokenizer tokenizer = new StringTokenizer(srvRecord, " ");
308        String port;
309
310        if (tokenizer.countTokens() == 4) {
311            priority = Integer.parseInt(tokenizer.nextToken());
312            weight = Integer.parseInt(tokenizer.nextToken());
313            port = tokenizer.nextToken();
314            hostport = tokenizer.nextToken() + ":" + port;
315        } else {
316            throw new IllegalArgumentException();
317        }
318    }
319
320    /*
321     * Sort records in ascending order of priority value. For records with
322     * equal priority move those with weight 0 to the top of the list.
323     */
324    public int compareTo(SrvRecord that) {
325        if (priority > that.priority) {
326            return 1; // this > that
327        } else if (priority < that.priority) {
328            return -1; // this < that
329        } else if (weight == 0 && that.weight != 0) {
330            return -1; // this < that
331        } else if (weight != 0 && that.weight == 0) {
332            return 1; // this > that
333        } else {
334            return 0; // this == that
335        }
336    }
337}
338}
339