1/*
2 *  Licensed to the Apache Software Foundation (ASF) under one or more
3 *  contributor license agreements.  See the NOTICE file distributed with
4 *  this work for additional information regarding copyright ownership.
5 *  The ASF licenses this file to You under the Apache License, Version 2.0
6 *  (the "License"); you may not use this file except in compliance with
7 *  the License.  You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *  Unless required by applicable law or agreed to in writing, software
12 *  distributed under the License is distributed on an "AS IS" BASIS,
13 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  See the License for the specific language governing permissions and
15 *  limitations under the License.
16 */
17
18package javax.net.ssl;
19
20import java.util.ArrayList;
21import java.util.Collections;
22import java.util.List;
23import javax.security.auth.x500.X500Principal;
24
25/**
26 * A distinguished name (DN) parser. This parser only supports extracting a
27 * string value from a DN. It doesn't support values in the hex-string style.
28 *
29 * @hide
30 */
31public final class DistinguishedNameParser {
32    private final String dn;
33    private final int length;
34    private int pos;
35    private int beg;
36    private int end;
37
38    /** tmp vars to store positions of the currently parsed item */
39    private int cur;
40
41    /** distinguished name chars */
42    private char[] chars;
43
44    public DistinguishedNameParser(X500Principal principal) {
45        // RFC2253 is used to ensure we get attributes in the reverse
46        // order of the underlying ASN.1 encoding, so that the most
47        // significant values of repeated attributes occur first.
48        this.dn = principal.getName(X500Principal.RFC2253);
49        this.length = this.dn.length();
50    }
51
52    // gets next attribute type: (ALPHA 1*keychar) / oid
53    private String nextAT() {
54        // skip preceding space chars, they can present after
55        // comma or semicolon (compatibility with RFC 1779)
56        for (; pos < length && chars[pos] == ' '; pos++) {
57        }
58        if (pos == length) {
59            return null; // reached the end of DN
60        }
61
62        // mark the beginning of attribute type
63        beg = pos;
64
65        // attribute type chars
66        pos++;
67        for (; pos < length && chars[pos] != '=' && chars[pos] != ' '; pos++) {
68            // we don't follow exact BNF syntax here:
69            // accept any char except space and '='
70        }
71        if (pos >= length) {
72            throw new IllegalStateException("Unexpected end of DN: " + dn);
73        }
74
75        // mark the end of attribute type
76        end = pos;
77
78        // skip trailing space chars between attribute type and '='
79        // (compatibility with RFC 1779)
80        if (chars[pos] == ' ') {
81            for (; pos < length && chars[pos] != '=' && chars[pos] == ' '; pos++) {
82            }
83
84            if (chars[pos] != '=' || pos == length) {
85                throw new IllegalStateException("Unexpected end of DN: " + dn);
86            }
87        }
88
89        pos++; //skip '=' char
90
91        // skip space chars between '=' and attribute value
92        // (compatibility with RFC 1779)
93        for (; pos < length && chars[pos] == ' '; pos++) {
94        }
95
96        // in case of oid attribute type skip its prefix: "oid." or "OID."
97        // (compatibility with RFC 1779)
98        if ((end - beg > 4) && (chars[beg + 3] == '.')
99                && (chars[beg] == 'O' || chars[beg] == 'o')
100                && (chars[beg + 1] == 'I' || chars[beg + 1] == 'i')
101                && (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) {
102            beg += 4;
103        }
104
105        return new String(chars, beg, end - beg);
106    }
107
108    // gets quoted attribute value: QUOTATION *( quotechar / pair ) QUOTATION
109    private String quotedAV() {
110        pos++;
111        beg = pos;
112        end = beg;
113        while (true) {
114
115            if (pos == length) {
116                throw new IllegalStateException("Unexpected end of DN: " + dn);
117            }
118
119            if (chars[pos] == '"') {
120                // enclosing quotation was found
121                pos++;
122                break;
123            } else if (chars[pos] == '\\') {
124                chars[end] = getEscaped();
125            } else {
126                // shift char: required for string with escaped chars
127                chars[end] = chars[pos];
128            }
129            pos++;
130            end++;
131        }
132
133        // skip trailing space chars before comma or semicolon.
134        // (compatibility with RFC 1779)
135        for (; pos < length && chars[pos] == ' '; pos++) {
136        }
137
138        return new String(chars, beg, end - beg);
139    }
140
141    // gets hex string attribute value: "#" hexstring
142    private String hexAV() {
143        if (pos + 4 >= length) {
144            // encoded byte array  must be not less then 4 c
145            throw new IllegalStateException("Unexpected end of DN: " + dn);
146        }
147
148        beg = pos; // store '#' position
149        pos++;
150        while (true) {
151
152            // check for end of attribute value
153            // looks for space and component separators
154            if (pos == length || chars[pos] == '+' || chars[pos] == ','
155                    || chars[pos] == ';') {
156                end = pos;
157                break;
158            }
159
160            if (chars[pos] == ' ') {
161                end = pos;
162                pos++;
163                // skip trailing space chars before comma or semicolon.
164                // (compatibility with RFC 1779)
165                for (; pos < length && chars[pos] == ' '; pos++) {
166                }
167                break;
168            } else if (chars[pos] >= 'A' && chars[pos] <= 'F') {
169                chars[pos] += 32; //to low case
170            }
171
172            pos++;
173        }
174
175        // verify length of hex string
176        // encoded byte array  must be not less then 4 and must be even number
177        int hexLen = end - beg; // skip first '#' char
178        if (hexLen < 5 || (hexLen & 1) == 0) {
179            throw new IllegalStateException("Unexpected end of DN: " + dn);
180        }
181
182        // get byte encoding from string representation
183        byte[] encoded = new byte[hexLen / 2];
184        for (int i = 0, p = beg + 1; i < encoded.length; p += 2, i++) {
185            encoded[i] = (byte) getByte(p);
186        }
187
188        return new String(chars, beg, hexLen);
189    }
190
191    // gets string attribute value: *( stringchar / pair )
192    private String escapedAV() {
193        beg = pos;
194        end = pos;
195        while (true) {
196            if (pos >= length) {
197                // the end of DN has been found
198                return new String(chars, beg, end - beg);
199            }
200
201            switch (chars[pos]) {
202            case '+':
203            case ',':
204            case ';':
205                // separator char has been found
206                return new String(chars, beg, end - beg);
207            case '\\':
208                // escaped char
209                chars[end++] = getEscaped();
210                pos++;
211                break;
212            case ' ':
213                // need to figure out whether space defines
214                // the end of attribute value or not
215                cur = end;
216
217                pos++;
218                chars[end++] = ' ';
219
220                for (; pos < length && chars[pos] == ' '; pos++) {
221                    chars[end++] = ' ';
222                }
223                if (pos == length || chars[pos] == ',' || chars[pos] == '+'
224                        || chars[pos] == ';') {
225                    // separator char or the end of DN has been found
226                    return new String(chars, beg, cur - beg);
227                }
228                break;
229            default:
230                chars[end++] = chars[pos];
231                pos++;
232            }
233        }
234    }
235
236    // returns escaped char
237    private char getEscaped() {
238        pos++;
239        if (pos == length) {
240            throw new IllegalStateException("Unexpected end of DN: " + dn);
241        }
242
243        switch (chars[pos]) {
244        case '"':
245        case '\\':
246        case ',':
247        case '=':
248        case '+':
249        case '<':
250        case '>':
251        case '#':
252        case ';':
253        case ' ':
254        case '*':
255        case '%':
256        case '_':
257            //FIXME: escaping is allowed only for leading or trailing space char
258            return chars[pos];
259        default:
260            // RFC doesn't explicitly say that escaped hex pair is
261            // interpreted as UTF-8 char. It only contains an example of such DN.
262            return getUTF8();
263        }
264    }
265
266    // decodes UTF-8 char
267    // see http://www.unicode.org for UTF-8 bit distribution table
268    private char getUTF8() {
269        int res = getByte(pos);
270        pos++; //FIXME tmp
271
272        if (res < 128) { // one byte: 0-7F
273            return (char) res;
274        } else if (res >= 192 && res <= 247) {
275
276            int count;
277            if (res <= 223) { // two bytes: C0-DF
278                count = 1;
279                res = res & 0x1F;
280            } else if (res <= 239) { // three bytes: E0-EF
281                count = 2;
282                res = res & 0x0F;
283            } else { // four bytes: F0-F7
284                count = 3;
285                res = res & 0x07;
286            }
287
288            int b;
289            for (int i = 0; i < count; i++) {
290                pos++;
291                if (pos == length || chars[pos] != '\\') {
292                    return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
293                }
294                pos++;
295
296                b = getByte(pos);
297                pos++; //FIXME tmp
298                if ((b & 0xC0) != 0x80) {
299                    return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
300                }
301
302                res = (res << 6) + (b & 0x3F);
303            }
304            return (char) res;
305        } else {
306            return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
307        }
308    }
309
310    // Returns byte representation of a char pair
311    // The char pair is composed of DN char in
312    // specified 'position' and the next char
313    // According to BNF syntax:
314    // hexchar    = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
315    //                    / "a" / "b" / "c" / "d" / "e" / "f"
316    private int getByte(int position) {
317        if (position + 1 >= length) {
318            throw new IllegalStateException("Malformed DN: " + dn);
319        }
320
321        int b1, b2;
322
323        b1 = chars[position];
324        if (b1 >= '0' && b1 <= '9') {
325            b1 = b1 - '0';
326        } else if (b1 >= 'a' && b1 <= 'f') {
327            b1 = b1 - 87; // 87 = 'a' - 10
328        } else if (b1 >= 'A' && b1 <= 'F') {
329            b1 = b1 - 55; // 55 = 'A' - 10
330        } else {
331            throw new IllegalStateException("Malformed DN: " + dn);
332        }
333
334        b2 = chars[position + 1];
335        if (b2 >= '0' && b2 <= '9') {
336            b2 = b2 - '0';
337        } else if (b2 >= 'a' && b2 <= 'f') {
338            b2 = b2 - 87; // 87 = 'a' - 10
339        } else if (b2 >= 'A' && b2 <= 'F') {
340            b2 = b2 - 55; // 55 = 'A' - 10
341        } else {
342            throw new IllegalStateException("Malformed DN: " + dn);
343        }
344
345        return (b1 << 4) + b2;
346    }
347
348    /**
349     * Parses the DN and returns the most significant attribute value
350     * for an attribute type, or null if none found.
351     *
352     * @param attributeType attribute type to look for (e.g. "ca")
353     */
354    public String findMostSpecific(String attributeType) {
355        // Initialize internal state.
356        pos = 0;
357        beg = 0;
358        end = 0;
359        cur = 0;
360        chars = dn.toCharArray();
361
362        String attType = nextAT();
363        if (attType == null) {
364            return null;
365        }
366        while (true) {
367            String attValue = "";
368
369            if (pos == length) {
370                return null;
371            }
372
373            switch (chars[pos]) {
374            case '"':
375                attValue = quotedAV();
376                break;
377            case '#':
378                attValue = hexAV();
379                break;
380            case '+':
381            case ',':
382            case ';': // compatibility with RFC 1779: semicolon can separate RDNs
383                //empty attribute value
384                break;
385            default:
386                attValue = escapedAV();
387            }
388
389            // Values are ordered from most specific to least specific
390            // due to the RFC2253 formatting. So take the first match
391            // we see.
392            if (attributeType.equalsIgnoreCase(attType)) {
393                return attValue;
394            }
395
396            if (pos >= length) {
397                return null;
398            }
399
400            if (chars[pos] == ',' || chars[pos] == ';') {
401            } else if (chars[pos] != '+') {
402                throw new IllegalStateException("Malformed DN: " + dn);
403            }
404
405            pos++;
406            attType = nextAT();
407            if (attType == null) {
408                throw new IllegalStateException("Malformed DN: " + dn);
409            }
410        }
411    }
412
413    /**
414     * Parses the DN and returns all values for an attribute type, in
415     * the order of decreasing significance (most significant first).
416     *
417     * @param attributeType attribute type to look for (e.g. "ca")
418     */
419    public List<String> getAllMostSpecificFirst(String attributeType) {
420        // Initialize internal state.
421        pos = 0;
422        beg = 0;
423        end = 0;
424        cur = 0;
425        chars = dn.toCharArray();
426        List<String> result = Collections.emptyList();
427
428        String attType = nextAT();
429        if (attType == null) {
430            return result;
431        }
432        while (pos < length) {
433            String attValue = "";
434
435            switch (chars[pos]) {
436            case '"':
437                attValue = quotedAV();
438                break;
439            case '#':
440                attValue = hexAV();
441                break;
442            case '+':
443            case ',':
444            case ';': // compatibility with RFC 1779: semicolon can separate RDNs
445                //empty attribute value
446                break;
447            default:
448                attValue = escapedAV();
449            }
450
451            // Values are ordered from most specific to least specific
452            // due to the RFC2253 formatting. So take the first match
453            // we see.
454            if (attributeType.equalsIgnoreCase(attType)) {
455                if (result.isEmpty()) {
456                    result = new ArrayList<String>();
457                }
458                result.add(attValue);
459            }
460
461            if (pos >= length) {
462                break;
463            }
464
465            if (chars[pos] == ',' || chars[pos] == ';') {
466            } else if (chars[pos] != '+') {
467                throw new IllegalStateException("Malformed DN: " + dn);
468            }
469
470            pos++;
471            attType = nextAT();
472            if (attType == null) {
473                throw new IllegalStateException("Malformed DN: " + dn);
474            }
475        }
476
477        return result;
478    }
479}
480