blob: 5254207cd78e4f745e4c6d01ac70c05a83e14adb [file] [log] [blame]
Makoto Onuki8f028a92010-01-08 13:34:57 -08001/*
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
Dianne Hackborn2269d1572010-02-24 19:54:22 -080018package com.android.internal.net;
19
Makoto Onuki8f028a92010-01-08 13:34:57 -080020
21import android.util.Log;
22
23import java.io.IOException;
24
25import javax.security.auth.x500.X500Principal;
26
27/**
28 * A simple distinguished name(DN) parser.
29 *
30 * <p>This class is based on org.apache.harmony.security.x509.DNParser. It's customized to remove
31 * external references which are unnecessary for our requirements.
32 *
33 * <p>This class is only meant for extracting a string value from a DN. e.g. it doesn't support
34 * values in the hex-string style.
35 *
36 * <p>This class is used by {@link DomainNameValidator} only. However, in order to make this
37 * class visible from unit tests, it's made public.
Dianne Hackborn2269d1572010-02-24 19:54:22 -080038 *
39 * @hide
Makoto Onuki8f028a92010-01-08 13:34:57 -080040 */
41public final class DNParser {
42 private static final String TAG = "DNParser";
43
44 /** DN to be parsed. */
45 private final String dn;
46
47 // length of distinguished name string
48 private final int length;
49
50 private int pos, beg, end;
51
52 // tmp vars to store positions of the currently parsed item
53 private int cur;
54
55 // distinguished name chars
56 private char[] chars;
57
58 /**
59 * Exception message thrown when we failed to parse DN, which shouldn't happen because we
60 * only handle DNs that {@link X500Principal#getName} returns, which shouldn't be malformed.
61 */
62 private static final String ERROR_PARSE_ERROR = "Failed to parse DN";
63
64 /**
65 * Constructor.
66 *
67 * @param principal - {@link X500Principal} to be parsed
68 */
69 public DNParser(X500Principal principal) {
70 this.dn = principal.getName(X500Principal.RFC2253);
71 this.length = dn.length();
72 }
73
74 // gets next attribute type: (ALPHA 1*keychar) / oid
75 private String nextAT() throws IOException {
76
77 // skip preceding space chars, they can present after
78 // comma or semicolon (compatibility with RFC 1779)
79 for (; pos < length && chars[pos] == ' '; pos++) {
80 }
81 if (pos == length) {
82 return null; // reached the end of DN
83 }
84
85 // mark the beginning of attribute type
86 beg = pos;
87
88 // attribute type chars
89 pos++;
90 for (; pos < length && chars[pos] != '=' && chars[pos] != ' '; pos++) {
91 // we don't follow exact BNF syntax here:
92 // accept any char except space and '='
93 }
94 if (pos >= length) {
95 // unexpected end of DN
96 throw new IOException(ERROR_PARSE_ERROR);
97 }
98
99 // mark the end of attribute type
100 end = pos;
101
102 // skip trailing space chars between attribute type and '='
103 // (compatibility with RFC 1779)
104 if (chars[pos] == ' ') {
105 for (; pos < length && chars[pos] != '=' && chars[pos] == ' '; pos++) {
106 }
107
108 if (chars[pos] != '=' || pos == length) {
109 // unexpected end of DN
110 throw new IOException(ERROR_PARSE_ERROR);
111 }
112 }
113
114 pos++; //skip '=' char
115
116 // skip space chars between '=' and attribute value
117 // (compatibility with RFC 1779)
118 for (; pos < length && chars[pos] == ' '; pos++) {
119 }
120
121 // in case of oid attribute type skip its prefix: "oid." or "OID."
122 // (compatibility with RFC 1779)
123 if ((end - beg > 4) && (chars[beg + 3] == '.')
124 && (chars[beg] == 'O' || chars[beg] == 'o')
125 && (chars[beg + 1] == 'I' || chars[beg + 1] == 'i')
126 && (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) {
127 beg += 4;
128 }
129
130 return new String(chars, beg, end - beg);
131 }
132
133 // gets quoted attribute value: QUOTATION *( quotechar / pair ) QUOTATION
134 private String quotedAV() throws IOException {
135
136 pos++;
137 beg = pos;
138 end = beg;
139 while (true) {
140
141 if (pos == length) {
142 // unexpected end of DN
143 throw new IOException(ERROR_PARSE_ERROR);
144 }
145
146 if (chars[pos] == '"') {
147 // enclosing quotation was found
148 pos++;
149 break;
150 } else if (chars[pos] == '\\') {
151 chars[end] = getEscaped();
152 } else {
153 // shift char: required for string with escaped chars
154 chars[end] = chars[pos];
155 }
156 pos++;
157 end++;
158 }
159
160 // skip trailing space chars before comma or semicolon.
161 // (compatibility with RFC 1779)
162 for (; pos < length && chars[pos] == ' '; pos++) {
163 }
164
165 return new String(chars, beg, end - beg);
166 }
167
168 // gets hex string attribute value: "#" hexstring
169 private String hexAV() throws IOException {
170
171 if (pos + 4 >= length) {
172 // encoded byte array must be not less then 4 c
173 throw new IOException(ERROR_PARSE_ERROR);
174 }
175
176 beg = pos; // store '#' position
177 pos++;
178 while (true) {
179
180 // check for end of attribute value
181 // looks for space and component separators
182 if (pos == length || chars[pos] == '+' || chars[pos] == ','
183 || chars[pos] == ';') {
184 end = pos;
185 break;
186 }
187
188 if (chars[pos] == ' ') {
189 end = pos;
190 pos++;
191 // skip trailing space chars before comma or semicolon.
192 // (compatibility with RFC 1779)
193 for (; pos < length && chars[pos] == ' '; pos++) {
194 }
195 break;
196 } else if (chars[pos] >= 'A' && chars[pos] <= 'F') {
197 chars[pos] += 32; //to low case
198 }
199
200 pos++;
201 }
202
203 // verify length of hex string
204 // encoded byte array must be not less then 4 and must be even number
205 int hexLen = end - beg; // skip first '#' char
206 if (hexLen < 5 || (hexLen & 1) == 0) {
207 throw new IOException(ERROR_PARSE_ERROR);
208 }
209
210 // get byte encoding from string representation
211 byte[] encoded = new byte[hexLen / 2];
212 for (int i = 0, p = beg + 1; i < encoded.length; p += 2, i++) {
213 encoded[i] = (byte) getByte(p);
214 }
215
216 return new String(chars, beg, hexLen);
217 }
218
219 // gets string attribute value: *( stringchar / pair )
220 private String escapedAV() throws IOException {
221
222 beg = pos;
223 end = pos;
224 while (true) {
225
226 if (pos >= length) {
227 // the end of DN has been found
228 return new String(chars, beg, end - beg);
229 }
230
231 switch (chars[pos]) {
232 case '+':
233 case ',':
234 case ';':
235 // separator char has beed found
236 return new String(chars, beg, end - beg);
237 case '\\':
238 // escaped char
239 chars[end++] = getEscaped();
240 pos++;
241 break;
242 case ' ':
243 // need to figure out whether space defines
244 // the end of attribute value or not
245 cur = end;
246
247 pos++;
248 chars[end++] = ' ';
249
250 for (; pos < length && chars[pos] == ' '; pos++) {
251 chars[end++] = ' ';
252 }
253 if (pos == length || chars[pos] == ',' || chars[pos] == '+'
254 || chars[pos] == ';') {
255 // separator char or the end of DN has beed found
256 return new String(chars, beg, cur - beg);
257 }
258 break;
259 default:
260 chars[end++] = chars[pos];
261 pos++;
262 }
263 }
264 }
265
266 // returns escaped char
267 private char getEscaped() throws IOException {
268
269 pos++;
270 if (pos == length) {
271 throw new IOException(ERROR_PARSE_ERROR);
272 }
273
274 switch (chars[pos]) {
275 case '"':
276 case '\\':
277 case ',':
278 case '=':
279 case '+':
280 case '<':
281 case '>':
282 case '#':
283 case ';':
284 case ' ':
285 case '*':
286 case '%':
287 case '_':
288 //FIXME: escaping is allowed only for leading or trailing space char
289 return chars[pos];
290 default:
291 // RFC doesn't explicitly say that escaped hex pair is
292 // interpreted as UTF-8 char. It only contains an example of such DN.
293 return getUTF8();
294 }
295 }
296
297 // decodes UTF-8 char
298 // see http://www.unicode.org for UTF-8 bit distribution table
299 private char getUTF8() throws IOException {
300
301 int res = getByte(pos);
302 pos++; //FIXME tmp
303
304 if (res < 128) { // one byte: 0-7F
305 return (char) res;
306 } else if (res >= 192 && res <= 247) {
307
308 int count;
309 if (res <= 223) { // two bytes: C0-DF
310 count = 1;
311 res = res & 0x1F;
312 } else if (res <= 239) { // three bytes: E0-EF
313 count = 2;
314 res = res & 0x0F;
315 } else { // four bytes: F0-F7
316 count = 3;
317 res = res & 0x07;
318 }
319
320 int b;
321 for (int i = 0; i < count; i++) {
322 pos++;
323 if (pos == length || chars[pos] != '\\') {
324 return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
325 }
326 pos++;
327
328 b = getByte(pos);
329 pos++; //FIXME tmp
330 if ((b & 0xC0) != 0x80) {
331 return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
332 }
333
334 res = (res << 6) + (b & 0x3F);
335 }
336 return (char) res;
337 } else {
338 return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
339 }
340 }
341
342 // Returns byte representation of a char pair
343 // The char pair is composed of DN char in
344 // specified 'position' and the next char
345 // According to BNF syntax:
346 // hexchar = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
347 // / "a" / "b" / "c" / "d" / "e" / "f"
348 private int getByte(int position) throws IOException {
349
350 if ((position + 1) >= length) {
351 // to avoid ArrayIndexOutOfBoundsException
352 throw new IOException(ERROR_PARSE_ERROR);
353 }
354
355 int b1, b2;
356
357 b1 = chars[position];
358 if (b1 >= '0' && b1 <= '9') {
359 b1 = b1 - '0';
360 } else if (b1 >= 'a' && b1 <= 'f') {
361 b1 = b1 - 87; // 87 = 'a' - 10
362 } else if (b1 >= 'A' && b1 <= 'F') {
363 b1 = b1 - 55; // 55 = 'A' - 10
364 } else {
365 throw new IOException(ERROR_PARSE_ERROR);
366 }
367
368 b2 = chars[position + 1];
369 if (b2 >= '0' && b2 <= '9') {
370 b2 = b2 - '0';
371 } else if (b2 >= 'a' && b2 <= 'f') {
372 b2 = b2 - 87; // 87 = 'a' - 10
373 } else if (b2 >= 'A' && b2 <= 'F') {
374 b2 = b2 - 55; // 55 = 'A' - 10
375 } else {
376 throw new IOException(ERROR_PARSE_ERROR);
377 }
378
379 return (b1 << 4) + b2;
380 }
381
382 /**
383 * Parses the DN and returns the attribute value for an attribute type.
384 *
385 * @param attributeType attribute type to look for (e.g. "ca")
386 * @return value of the attribute that first found, or null if none found
387 */
388 public String find(String attributeType) {
389 try {
390 // Initialize internal state.
391 pos = 0;
392 beg = 0;
393 end = 0;
394 cur = 0;
395 chars = dn.toCharArray();
396
397 String attType = nextAT();
398 if (attType == null) {
399 return null;
400 }
401 while (true) {
402 String attValue = "";
403
404 if (pos == length) {
405 return null;
406 }
407
408 switch (chars[pos]) {
409 case '"':
410 attValue = quotedAV();
411 break;
412 case '#':
413 attValue = hexAV();
414 break;
415 case '+':
416 case ',':
417 case ';': // compatibility with RFC 1779: semicolon can separate RDNs
418 //empty attribute value
419 break;
420 default:
421 attValue = escapedAV();
422 }
423
424 if (attributeType.equalsIgnoreCase(attType)) {
425 return attValue;
426 }
427
428 if (pos >= length) {
429 return null;
430 }
431
432 if (chars[pos] == ',' || chars[pos] == ';') {
433 } else if (chars[pos] != '+') {
434 throw new IOException(ERROR_PARSE_ERROR);
435 }
436
437 pos++;
438 attType = nextAT();
439 if (attType == null) {
440 throw new IOException(ERROR_PARSE_ERROR);
441 }
442 }
443 } catch (IOException e) {
444 // Parse error shouldn't happen, because we only handle DNs that
445 // X500Principal.getName() returns, which shouldn't be malformed.
446 Log.e(TAG, "Failed to parse DN: " + dn);
447 return null;
448 }
449 }
450}