1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace X501\DN; |
4
|
|
|
|
5
|
|
|
use ASN1\Element; |
6
|
|
|
use ASN1\Exception\DecodeException; |
7
|
|
|
|
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* Distinguished Name parsing conforming to RFC 2253 and RFC 1779. |
11
|
|
|
* |
12
|
|
|
* @link https://tools.ietf.org/html/rfc1779 |
13
|
|
|
* @link https://tools.ietf.org/html/rfc2253 |
14
|
|
|
*/ |
15
|
|
|
class DNParser |
16
|
|
|
{ |
17
|
|
|
/** |
18
|
|
|
* DN string. |
19
|
|
|
* |
20
|
|
|
* @var string |
21
|
|
|
*/ |
22
|
|
|
private $_dn; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* DN string length. |
26
|
|
|
* |
27
|
|
|
* @var int |
28
|
|
|
*/ |
29
|
|
|
private $_len; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* RFC 2253 special characters. |
33
|
|
|
* |
34
|
|
|
* @var string |
35
|
|
|
*/ |
36
|
|
|
const SPECIAL_CHARS = ",=+<>#;"; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* Parse distinguished name string to name-components. |
40
|
|
|
* |
41
|
|
|
* @param string $dn |
42
|
|
|
* @return array |
43
|
|
|
*/ |
44
|
69 |
|
public static function parseString($dn) { |
45
|
69 |
|
$parser = new self($dn); |
46
|
69 |
|
return $parser->parse(); |
47
|
|
|
} |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* Escape a AttributeValue string conforming to RFC 2253. |
51
|
|
|
* |
52
|
|
|
* @link https://tools.ietf.org/html/rfc2253#section-2.4 |
53
|
|
|
* @param string $str |
54
|
|
|
* @return string |
55
|
|
|
*/ |
56
|
30 |
|
public static function escapeString($str) { |
57
|
|
|
// one of the characters ",", "+", """, "\", "<", ">" or ";" |
58
|
30 |
|
$str = preg_replace('/([,\+"\\\<\>;])/u', '\\\\$1', $str); |
59
|
|
|
// a space character occurring at the end of the string |
60
|
30 |
|
$str = preg_replace('/( )$/u', '\\\\$1', $str); |
61
|
|
|
// a space or "#" character occurring at the beginning of the string |
62
|
30 |
|
$str = preg_replace('/^([ #])/u', '\\\\$1', $str); |
63
|
|
|
// implementation specific special characters |
64
|
30 |
|
$str = preg_replace_callback('/([\pC])/u', |
65
|
|
|
function ($m) { |
66
|
2 |
|
$octets = str_split(bin2hex($m[1]), 2); |
67
|
2 |
|
return implode("", |
68
|
2 |
|
array_map( |
69
|
2 |
|
function ($octet) { |
70
|
2 |
|
return '\\' . strtoupper($octet); |
71
|
2 |
|
}, $octets)); |
72
|
30 |
|
}, $str); |
73
|
30 |
|
return $str; |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* Constructor |
78
|
|
|
* |
79
|
|
|
* @param string $dn Distinguised name |
80
|
|
|
*/ |
81
|
69 |
|
protected function __construct($dn) { |
82
|
69 |
|
$this->_dn = $dn; |
83
|
69 |
|
$this->_len = strlen($dn); |
84
|
69 |
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Parse DN to name-components. |
88
|
|
|
* |
89
|
|
|
* @throws \RuntimeException |
90
|
|
|
* @return array |
91
|
|
|
*/ |
92
|
69 |
|
protected function parse() { |
93
|
69 |
|
$offset = 0; |
94
|
69 |
|
$name = $this->_parseName($offset); |
95
|
61 |
|
if ($offset < $this->_len) { |
96
|
1 |
|
$remains = substr($this->_dn, $offset); |
97
|
1 |
|
throw new \UnexpectedValueException( |
98
|
|
|
"Parser finished before the end of string" . |
99
|
1 |
|
", remaining: '$remains'."); |
100
|
|
|
} |
101
|
60 |
|
return $name; |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Parse 'name'. |
106
|
|
|
* |
107
|
|
|
* name-component *("," name-component) |
108
|
|
|
* |
109
|
|
|
* @param int $offset |
110
|
|
|
* @return array Array of name-components |
111
|
|
|
*/ |
112
|
69 |
|
private function _parseName(&$offset) { |
113
|
69 |
|
$idx = $offset; |
114
|
69 |
|
$names = array(); |
115
|
69 |
|
while ($idx < $this->_len) { |
116
|
69 |
|
$names[] = $this->_parseNameComponent($idx); |
117
|
61 |
|
if ($idx >= $this->_len) { |
118
|
60 |
|
break; |
119
|
|
|
} |
120
|
20 |
|
$this->_skipWs($idx); |
121
|
20 |
|
if ("," != $this->_dn[$idx] && ";" != $this->_dn[$idx]) { |
122
|
1 |
|
break; |
123
|
|
|
} |
124
|
19 |
|
$idx++; |
125
|
19 |
|
$this->_skipWs($idx); |
126
|
19 |
|
} |
127
|
61 |
|
$offset = $idx; |
128
|
61 |
|
return array_reverse($names); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* Parse 'name-component'. |
133
|
|
|
* |
134
|
|
|
* attributeTypeAndValue *("+" attributeTypeAndValue) |
135
|
|
|
* |
136
|
|
|
* @param int $offset |
137
|
|
|
* @return array Array of [type, value] tuples |
138
|
|
|
*/ |
139
|
69 |
|
private function _parseNameComponent(&$offset) { |
140
|
69 |
|
$idx = $offset; |
141
|
69 |
|
$tvpairs = array(); |
142
|
69 |
|
while ($idx < $this->_len) { |
143
|
69 |
|
$tvpairs[] = $this->_parseAttrTypeAndValue($idx); |
144
|
61 |
|
$this->_skipWs($idx); |
145
|
61 |
|
if ($idx >= $this->_len || "+" != $this->_dn[$idx]) { |
146
|
61 |
|
break; |
147
|
|
|
} |
148
|
9 |
|
++$idx; |
149
|
9 |
|
$this->_skipWs($idx); |
150
|
9 |
|
} |
151
|
61 |
|
$offset = $idx; |
152
|
61 |
|
return $tvpairs; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Parse 'attributeTypeAndValue'. |
157
|
|
|
* |
158
|
|
|
* attributeType "=" attributeValue |
159
|
|
|
* |
160
|
|
|
* @param int $offset |
161
|
|
|
* @throws \UnexpectedValueException |
162
|
|
|
* @return array A tuple of [type, value]. Value may be either a string or |
163
|
|
|
* an Element, if it's encoded as hexstring. |
164
|
|
|
*/ |
165
|
69 |
|
private function _parseAttrTypeAndValue(&$offset) { |
166
|
69 |
|
$idx = $offset; |
167
|
69 |
|
$type = $this->_parseAttrType($idx); |
168
|
68 |
|
$this->_skipWs($idx); |
169
|
68 |
|
if ($idx >= $this->_len || "=" != $this->_dn[$idx++]) { |
170
|
1 |
|
throw new \UnexpectedValueException("Invalid type and value pair."); |
171
|
|
|
} |
172
|
67 |
|
$this->_skipWs($idx); |
173
|
|
|
// hexstring |
174
|
67 |
|
if ($idx < $this->_len && "#" == $this->_dn[$idx]) { |
175
|
8 |
|
++$idx; |
176
|
8 |
|
$data = $this->_parseAttrHexValue($idx); |
177
|
|
|
try { |
178
|
7 |
|
$value = Element::fromDER($data); |
179
|
7 |
|
} catch (DecodeException $e) { |
180
|
1 |
|
throw new \UnexpectedValueException( |
181
|
1 |
|
"Invalid DER encoding from hexstring.", 0, $e); |
182
|
|
|
} |
183
|
6 |
|
} else { |
184
|
59 |
|
$value = $this->_parseAttrStringValue($idx); |
185
|
|
|
} |
186
|
61 |
|
$offset = $idx; |
187
|
61 |
|
return array($type, $value); |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* Parse 'attributeType'. |
192
|
|
|
* |
193
|
|
|
* (ALPHA 1*keychar) / oid |
194
|
|
|
* |
195
|
|
|
* @param int $offset |
196
|
|
|
* @throws \UnexpectedValueException |
197
|
|
|
* @return string |
198
|
|
|
*/ |
199
|
69 |
View Code Duplication |
private function _parseAttrType(&$offset) { |
|
|
|
|
200
|
69 |
|
$idx = $offset; |
201
|
|
|
// dotted OID |
202
|
69 |
|
$type = $this->_regexMatch('/^(?:oid\.)?([0-9]+(?:\.[0-9]+)*)/i', $idx); |
203
|
69 |
|
if (null === $type) { |
204
|
|
|
// name |
205
|
62 |
|
$type = $this->_regexMatch('/^[a-z][a-z0-9\-]*/i', $idx); |
206
|
62 |
|
if (null === $type) { |
207
|
1 |
|
throw new \UnexpectedValueException("Invalid attribute type."); |
208
|
|
|
} |
209
|
61 |
|
} |
210
|
68 |
|
$offset = $idx; |
211
|
68 |
|
return $type; |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** |
215
|
|
|
* Parse 'attributeValue' of string type. |
216
|
|
|
* |
217
|
|
|
* @param int $offset |
218
|
|
|
* @throws \UnexpectedValueException |
219
|
|
|
* @return string |
220
|
|
|
*/ |
221
|
59 |
|
private function _parseAttrStringValue(&$offset) { |
222
|
59 |
|
$idx = $offset; |
223
|
59 |
|
if ($idx >= $this->_len) { |
224
|
2 |
|
return ""; |
225
|
|
|
} |
226
|
57 |
|
if ('"' == $this->_dn[$idx]) { // quoted string |
227
|
4 |
|
$val = $this->_parseQuotedAttrString($idx); |
228
|
4 |
|
} else { // string |
229
|
53 |
|
$val = $this->_parseAttrString($idx); |
230
|
|
|
} |
231
|
53 |
|
$offset = $idx; |
232
|
53 |
|
return $val; |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
/** |
236
|
|
|
* Parse plain 'attributeValue' string. |
237
|
|
|
* |
238
|
|
|
* @param int $offset |
239
|
|
|
* @throws \UnexpectedValueException |
240
|
|
|
* @return string |
241
|
|
|
*/ |
242
|
54 |
|
private function _parseAttrString(&$offset) { |
243
|
54 |
|
$idx = $offset; |
244
|
53 |
|
$val = ""; |
245
|
53 |
|
$wsidx = null; |
246
|
53 |
|
while ($idx < $this->_len) { |
247
|
53 |
|
$c = $this->_dn[$idx]; |
248
|
|
|
// pair (escape sequence) |
249
|
53 |
|
if ("\\" == $c) { |
250
|
10 |
|
++$idx; |
251
|
10 |
|
$val .= $this->_parsePairAfterSlash($idx); |
252
|
7 |
|
$wsidx = null; |
253
|
7 |
|
continue; |
254
|
48 |
|
} else if ('"' == $c) { |
255
|
1 |
|
throw new \UnexpectedValueException("Unexpected quotation."); |
256
|
48 |
|
} else if (false !== strpos(self::SPECIAL_CHARS, $c)) { |
257
|
26 |
|
break; |
258
|
|
|
} |
259
|
|
|
// keep track of the first consecutive whitespace |
260
|
48 |
|
if (' ' == $c) { |
261
|
8 |
|
if (null === $wsidx) { |
262
|
8 |
|
$wsidx = $idx; |
263
|
8 |
|
} |
264
|
8 |
|
} else { |
265
|
48 |
|
$wsidx = null; |
266
|
|
|
} |
267
|
|
|
// stringchar |
268
|
48 |
|
$val .= $c; |
269
|
48 |
|
++$idx; |
270
|
48 |
|
} |
271
|
|
|
// if there was non-escaped whitespace in the end of the value |
272
|
49 |
|
if (null !== $wsidx) { |
273
|
5 |
|
$val = substr($val, 0, -($idx - $wsidx)); |
274
|
5 |
|
} |
275
|
49 |
|
$offset = $idx; |
276
|
49 |
|
return $val; |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
/** |
280
|
|
|
* Parse quoted 'attributeValue' string. |
281
|
|
|
* |
282
|
|
|
* @param int $offset Offset to starting quote |
283
|
|
|
* @throws \UnexpectedValueException |
284
|
|
|
* @return string |
285
|
|
|
*/ |
286
|
4 |
|
private function _parseQuotedAttrString(&$offset) { |
287
|
4 |
|
$idx = $offset + 1; |
288
|
4 |
|
$val = ""; |
289
|
4 |
|
while ($idx < $this->_len) { |
290
|
4 |
|
$c = $this->_dn[$idx]; |
291
|
4 |
|
if ("\\" == $c) { // pair |
292
|
1 |
|
++$idx; |
293
|
1 |
|
$val .= $this->_parsePairAfterSlash($idx); |
294
|
1 |
|
continue; |
295
|
4 |
|
} else if ('"' == $c) { |
296
|
4 |
|
++$idx; |
297
|
4 |
|
break; |
298
|
|
|
} |
299
|
4 |
|
$val .= $c; |
300
|
4 |
|
++$idx; |
301
|
4 |
|
} |
302
|
4 |
|
$offset = $idx; |
303
|
4 |
|
return $val; |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
/** |
307
|
|
|
* Parse 'attributeValue' of binary type. |
308
|
|
|
* |
309
|
|
|
* @param int $offset |
310
|
|
|
* @throws \UnexpectedValueException |
311
|
|
|
* @return string |
312
|
|
|
*/ |
313
|
8 |
View Code Duplication |
private function _parseAttrHexValue(&$offset) { |
|
|
|
|
314
|
8 |
|
$idx = $offset; |
315
|
8 |
|
$hexstr = $this->_regexMatch('/^(?:[0-9a-f]{2})+/i', $idx); |
316
|
8 |
|
if (null === $hexstr) { |
317
|
1 |
|
throw new \UnexpectedValueException("Invalid hexstring."); |
318
|
|
|
} |
319
|
7 |
|
$data = hex2bin($hexstr); |
320
|
7 |
|
$offset = $idx; |
321
|
7 |
|
return $data; |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
/** |
325
|
|
|
* Parse 'pair' after leading slash. |
326
|
|
|
* |
327
|
|
|
* @param int $offset |
328
|
|
|
* @throws \UnexpectedValueException |
329
|
|
|
* @return string |
330
|
|
|
*/ |
331
|
11 |
|
private function _parsePairAfterSlash(&$offset) { |
332
|
11 |
|
$idx = $offset; |
333
|
11 |
|
if ($idx >= $this->_len) { |
334
|
1 |
|
throw new \UnexpectedValueException( |
335
|
1 |
|
"Unexpected end of escape sequence."); |
336
|
|
|
} |
337
|
10 |
|
$c = $this->_dn[$idx++]; |
338
|
|
|
// special | \ | " | SPACE |
339
|
10 |
|
if (false !== strpos(self::SPECIAL_CHARS . '\\" ', $c)) { |
340
|
7 |
|
$val = $c; |
341
|
7 |
|
} else { // hexpair |
342
|
3 |
|
if ($idx >= $this->_len) { |
343
|
1 |
|
throw new \UnexpectedValueException("Unexpected end of hexpair."); |
344
|
|
|
} |
345
|
2 |
|
$val = @hex2bin($c . $this->_dn[$idx++]); |
346
|
2 |
|
if (false === $val) { |
347
|
1 |
|
throw new \UnexpectedValueException("Invalid hexpair."); |
348
|
|
|
} |
349
|
|
|
} |
350
|
8 |
|
$offset = $idx; |
351
|
8 |
|
return $val; |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
/** |
355
|
|
|
* Match DN to pattern and extract the last capture group. |
356
|
|
|
* |
357
|
|
|
* Updates offset to fully matched pattern. |
358
|
|
|
* |
359
|
|
|
* @param string $pattern |
360
|
|
|
* @param int $offset |
361
|
|
|
* @return string|null Null if pattern doesn't match |
362
|
|
|
*/ |
363
|
69 |
|
private function _regexMatch($pattern, &$offset) { |
364
|
69 |
|
$idx = $offset; |
365
|
69 |
|
if (!preg_match($pattern, substr($this->_dn, $idx), $match)) { |
366
|
62 |
|
return null; |
367
|
|
|
} |
368
|
68 |
|
$idx += strlen($match[0]); |
369
|
68 |
|
$offset = $idx; |
370
|
68 |
|
return end($match); |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
/** |
374
|
|
|
* Skip consecutive spaces. |
375
|
|
|
* |
376
|
|
|
* @param int $offset |
377
|
|
|
*/ |
378
|
68 |
|
private function _skipWs(&$offset) { |
379
|
68 |
|
$idx = $offset; |
380
|
68 |
|
while ($idx < $this->_len) { |
381
|
67 |
|
if (" " != $this->_dn[$idx]) { |
382
|
67 |
|
break; |
383
|
|
|
} |
384
|
5 |
|
++$idx; |
385
|
5 |
|
} |
386
|
68 |
|
$offset = $idx; |
387
|
68 |
|
} |
388
|
|
|
} |
389
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.