| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.squareup.okhttp.internal.tls; |
| |
| import javax.security.auth.x500.X500Principal; |
| |
| /** |
| * A distinguished name (DN) parser. This parser only supports extracting a |
| * string value from a DN. It doesn't support values in the hex-string style. |
| */ |
| final class DistinguishedNameParser { |
| private final String dn; |
| private final int length; |
| private int pos; |
| private int beg; |
| private int end; |
| |
| /** Temporary variable to store positions of the currently parsed item. */ |
| private int cur; |
| |
| /** Distinguished name characters. */ |
| private char[] chars; |
| |
| public DistinguishedNameParser(X500Principal principal) { |
| // RFC2253 is used to ensure we get attributes in the reverse |
| // order of the underlying ASN.1 encoding, so that the most |
| // significant values of repeated attributes occur first. |
| this.dn = principal.getName(X500Principal.RFC2253); |
| this.length = this.dn.length(); |
| } |
| |
| // gets next attribute type: (ALPHA 1*keychar) / oid |
| private String nextAT() { |
| // skip preceding space chars, they can present after |
| // comma or semicolon (compatibility with RFC 1779) |
| for (; pos < length && chars[pos] == ' '; pos++) { |
| } |
| if (pos == length) { |
| return null; // reached the end of DN |
| } |
| |
| // mark the beginning of attribute type |
| beg = pos; |
| |
| // attribute type chars |
| pos++; |
| for (; pos < length && chars[pos] != '=' && chars[pos] != ' '; pos++) { |
| // we don't follow exact BNF syntax here: |
| // accept any char except space and '=' |
| } |
| if (pos >= length) { |
| throw new IllegalStateException("Unexpected end of DN: " + dn); |
| } |
| |
| // mark the end of attribute type |
| end = pos; |
| |
| // skip trailing space chars between attribute type and '=' |
| // (compatibility with RFC 1779) |
| if (chars[pos] == ' ') { |
| for (; pos < length && chars[pos] != '=' && chars[pos] == ' '; pos++) { |
| } |
| |
| if (chars[pos] != '=' || pos == length) { |
| throw new IllegalStateException("Unexpected end of DN: " + dn); |
| } |
| } |
| |
| pos++; //skip '=' char |
| |
| // skip space chars between '=' and attribute value |
| // (compatibility with RFC 1779) |
| for (; pos < length && chars[pos] == ' '; pos++) { |
| } |
| |
| // in case of oid attribute type skip its prefix: "oid." or "OID." |
| // (compatibility with RFC 1779) |
| if ((end - beg > 4) && (chars[beg + 3] == '.') |
| && (chars[beg] == 'O' || chars[beg] == 'o') |
| && (chars[beg + 1] == 'I' || chars[beg + 1] == 'i') |
| && (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) { |
| beg += 4; |
| } |
| |
| return new String(chars, beg, end - beg); |
| } |
| |
| // gets quoted attribute value: QUOTATION *( quotechar / pair ) QUOTATION |
| private String quotedAV() { |
| pos++; |
| beg = pos; |
| end = beg; |
| while (true) { |
| |
| if (pos == length) { |
| throw new IllegalStateException("Unexpected end of DN: " + dn); |
| } |
| |
| if (chars[pos] == '"') { |
| // enclosing quotation was found |
| pos++; |
| break; |
| } else if (chars[pos] == '\\') { |
| chars[end] = getEscaped(); |
| } else { |
| // shift char: required for string with escaped chars |
| chars[end] = chars[pos]; |
| } |
| pos++; |
| end++; |
| } |
| |
| // skip trailing space chars before comma or semicolon. |
| // (compatibility with RFC 1779) |
| for (; pos < length && chars[pos] == ' '; pos++) { |
| } |
| |
| return new String(chars, beg, end - beg); |
| } |
| |
| // gets hex string attribute value: "#" hexstring |
| private String hexAV() { |
| if (pos + 4 >= length) { |
| // encoded byte array must be not less then 4 c |
| throw new IllegalStateException("Unexpected end of DN: " + dn); |
| } |
| |
| beg = pos; // store '#' position |
| pos++; |
| while (true) { |
| |
| // check for end of attribute value |
| // looks for space and component separators |
| if (pos == length || chars[pos] == '+' || chars[pos] == ',' |
| || chars[pos] == ';') { |
| end = pos; |
| break; |
| } |
| |
| if (chars[pos] == ' ') { |
| end = pos; |
| pos++; |
| // skip trailing space chars before comma or semicolon. |
| // (compatibility with RFC 1779) |
| for (; pos < length && chars[pos] == ' '; pos++) { |
| } |
| break; |
| } else if (chars[pos] >= 'A' && chars[pos] <= 'F') { |
| chars[pos] += 32; //to low case |
| } |
| |
| pos++; |
| } |
| |
| // verify length of hex string |
| // encoded byte array must be not less then 4 and must be even number |
| int hexLen = end - beg; // skip first '#' char |
| if (hexLen < 5 || (hexLen & 1) == 0) { |
| throw new IllegalStateException("Unexpected end of DN: " + dn); |
| } |
| |
| // get byte encoding from string representation |
| byte[] encoded = new byte[hexLen / 2]; |
| for (int i = 0, p = beg + 1; i < encoded.length; p += 2, i++) { |
| encoded[i] = (byte) getByte(p); |
| } |
| |
| return new String(chars, beg, hexLen); |
| } |
| |
| // gets string attribute value: *( stringchar / pair ) |
| private String escapedAV() { |
| beg = pos; |
| end = pos; |
| while (true) { |
| if (pos >= length) { |
| // the end of DN has been found |
| return new String(chars, beg, end - beg); |
| } |
| |
| switch (chars[pos]) { |
| case '+': |
| case ',': |
| case ';': |
| // separator char has been found |
| return new String(chars, beg, end - beg); |
| case '\\': |
| // escaped char |
| chars[end++] = getEscaped(); |
| pos++; |
| break; |
| case ' ': |
| // need to figure out whether space defines |
| // the end of attribute value or not |
| cur = end; |
| |
| pos++; |
| chars[end++] = ' '; |
| |
| for (; pos < length && chars[pos] == ' '; pos++) { |
| chars[end++] = ' '; |
| } |
| if (pos == length || chars[pos] == ',' || chars[pos] == '+' |
| || chars[pos] == ';') { |
| // separator char or the end of DN has been found |
| return new String(chars, beg, cur - beg); |
| } |
| break; |
| default: |
| chars[end++] = chars[pos]; |
| pos++; |
| } |
| } |
| } |
| |
| // returns escaped char |
| private char getEscaped() { |
| pos++; |
| if (pos == length) { |
| throw new IllegalStateException("Unexpected end of DN: " + dn); |
| } |
| |
| switch (chars[pos]) { |
| case '"': |
| case '\\': |
| case ',': |
| case '=': |
| case '+': |
| case '<': |
| case '>': |
| case '#': |
| case ';': |
| case ' ': |
| case '*': |
| case '%': |
| case '_': |
| //FIXME: escaping is allowed only for leading or trailing space char |
| return chars[pos]; |
| default: |
| // RFC doesn't explicitly say that escaped hex pair is |
| // interpreted as UTF-8 char. It only contains an example of such DN. |
| return getUTF8(); |
| } |
| } |
| |
| // decodes UTF-8 char |
| // see http://www.unicode.org for UTF-8 bit distribution table |
| private char getUTF8() { |
| int res = getByte(pos); |
| pos++; //FIXME tmp |
| |
| if (res < 128) { // one byte: 0-7F |
| return (char) res; |
| } else if (res >= 192 && res <= 247) { |
| |
| int count; |
| if (res <= 223) { // two bytes: C0-DF |
| count = 1; |
| res = res & 0x1F; |
| } else if (res <= 239) { // three bytes: E0-EF |
| count = 2; |
| res = res & 0x0F; |
| } else { // four bytes: F0-F7 |
| count = 3; |
| res = res & 0x07; |
| } |
| |
| int b; |
| for (int i = 0; i < count; i++) { |
| pos++; |
| if (pos == length || chars[pos] != '\\') { |
| return 0x3F; //FIXME failed to decode UTF-8 char - return '?' |
| } |
| pos++; |
| |
| b = getByte(pos); |
| pos++; //FIXME tmp |
| if ((b & 0xC0) != 0x80) { |
| return 0x3F; //FIXME failed to decode UTF-8 char - return '?' |
| } |
| |
| res = (res << 6) + (b & 0x3F); |
| } |
| return (char) res; |
| } else { |
| return 0x3F; //FIXME failed to decode UTF-8 char - return '?' |
| } |
| } |
| |
| // Returns byte representation of a char pair |
| // The char pair is composed of DN char in |
| // specified 'position' and the next char |
| // According to BNF syntax: |
| // hexchar = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" |
| // / "a" / "b" / "c" / "d" / "e" / "f" |
| private int getByte(int position) { |
| if (position + 1 >= length) { |
| throw new IllegalStateException("Malformed DN: " + dn); |
| } |
| |
| int b1, b2; |
| |
| b1 = chars[position]; |
| if (b1 >= '0' && b1 <= '9') { |
| b1 = b1 - '0'; |
| } else if (b1 >= 'a' && b1 <= 'f') { |
| b1 = b1 - 87; // 87 = 'a' - 10 |
| } else if (b1 >= 'A' && b1 <= 'F') { |
| b1 = b1 - 55; // 55 = 'A' - 10 |
| } else { |
| throw new IllegalStateException("Malformed DN: " + dn); |
| } |
| |
| b2 = chars[position + 1]; |
| if (b2 >= '0' && b2 <= '9') { |
| b2 = b2 - '0'; |
| } else if (b2 >= 'a' && b2 <= 'f') { |
| b2 = b2 - 87; // 87 = 'a' - 10 |
| } else if (b2 >= 'A' && b2 <= 'F') { |
| b2 = b2 - 55; // 55 = 'A' - 10 |
| } else { |
| throw new IllegalStateException("Malformed DN: " + dn); |
| } |
| |
| return (b1 << 4) + b2; |
| } |
| |
| /** |
| * Parses the DN and returns the most significant attribute value |
| * for an attribute type, or null if none found. |
| * |
| * @param attributeType attribute type to look for (e.g. "ca") |
| */ |
| public String findMostSpecific(String attributeType) { |
| // Initialize internal state. |
| pos = 0; |
| beg = 0; |
| end = 0; |
| cur = 0; |
| chars = dn.toCharArray(); |
| |
| String attType = nextAT(); |
| if (attType == null) { |
| return null; |
| } |
| while (true) { |
| String attValue = ""; |
| |
| if (pos == length) { |
| return null; |
| } |
| |
| switch (chars[pos]) { |
| case '"': |
| attValue = quotedAV(); |
| break; |
| case '#': |
| attValue = hexAV(); |
| break; |
| case '+': |
| case ',': |
| case ';': // compatibility with RFC 1779: semicolon can separate RDNs |
| //empty attribute value |
| break; |
| default: |
| attValue = escapedAV(); |
| } |
| |
| // Values are ordered from most specific to least specific |
| // due to the RFC2253 formatting. So take the first match |
| // we see. |
| if (attributeType.equalsIgnoreCase(attType)) { |
| return attValue; |
| } |
| |
| if (pos >= length) { |
| return null; |
| } |
| |
| if (chars[pos] == ',' || chars[pos] == ';') { |
| } else if (chars[pos] != '+') { |
| throw new IllegalStateException("Malformed DN: " + dn); |
| } |
| |
| pos++; |
| attType = nextAT(); |
| if (attType == null) { |
| throw new IllegalStateException("Malformed DN: " + dn); |
| } |
| } |
| } |
| } |