/*
* 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 okhttp3.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;
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);
}
}
}
}