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