1
|
|
|
<?php |
2
|
|
|
/* |
3
|
|
|
* This file is part of PHP DNS Server. |
4
|
|
|
* |
5
|
|
|
* (c) Yif Swery <[email protected]> |
6
|
|
|
* |
7
|
|
|
* For the full copyright and license information, please view the LICENSE |
8
|
|
|
* file that was distributed with this source code. |
9
|
|
|
*/ |
10
|
|
|
|
11
|
|
|
namespace yswery\DNS; |
12
|
|
|
|
13
|
|
|
class Encoder |
14
|
|
|
{ |
15
|
|
|
/** |
16
|
|
|
* @param Message $message |
17
|
|
|
* |
18
|
|
|
* @return string |
19
|
|
|
* |
20
|
|
|
* @throws UnsupportedTypeException |
21
|
|
|
*/ |
22
|
|
|
public static function encodeMessage(Message $message): string |
23
|
|
|
{ |
24
|
|
|
return |
25
|
|
|
self::encodeHeader($message->getHeader()). |
26
|
|
|
self::encodeResourceRecords($message->getQuestions()). |
27
|
|
|
self::encodeResourceRecords($message->getAnswers()). |
28
|
|
|
self::encodeResourceRecords($message->getAuthoritatives()). |
29
|
|
|
self::encodeResourceRecords($message->getAdditionals()); |
30
|
|
|
} |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Encode a domain name as a sequence of labels. |
34
|
|
|
* |
35
|
|
|
* @param $domain |
36
|
|
|
* |
37
|
|
|
* @return string |
38
|
|
|
*/ |
39
|
|
|
public static function encodeDomainName($domain): string |
40
|
|
|
{ |
41
|
|
|
if ('.' === $domain) { |
42
|
|
|
return chr(0); |
43
|
|
|
} |
44
|
|
|
|
45
|
|
|
$domain = rtrim($domain, '.').'.'; |
46
|
|
|
$res = ''; |
47
|
|
|
|
48
|
|
|
foreach (explode('.', $domain) as $label) { |
49
|
|
|
$res .= chr(strlen($label)).$label; |
50
|
|
|
} |
51
|
|
|
|
52
|
|
|
return $res; |
53
|
|
|
} |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* @param int $type |
57
|
|
|
* @param string|array $rdata |
58
|
|
|
* |
59
|
|
|
* @return string |
60
|
|
|
* |
61
|
|
|
* @throws UnsupportedTypeException|\InvalidArgumentException |
62
|
|
|
*/ |
63
|
|
|
public static function encodeRdata(int $type, $rdata): string |
64
|
|
|
{ |
65
|
|
|
switch ($type) { |
66
|
|
|
case RecordTypeEnum::TYPE_A: |
67
|
|
|
case RecordTypeEnum::TYPE_AAAA: |
68
|
|
|
if (!filter_var($rdata, FILTER_VALIDATE_IP)) { |
69
|
|
|
throw new \InvalidArgumentException(sprintf('The IP address "%s" is invalid.', $rdata)); |
|
|
|
|
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
return inet_pton($rdata); |
|
|
|
|
73
|
|
|
case RecordTypeEnum::TYPE_NS: |
74
|
|
|
case RecordTypeEnum::TYPE_CNAME: |
75
|
|
|
case RecordTypeEnum::TYPE_PTR: |
76
|
|
|
return self::encodeDomainName($rdata); |
77
|
|
|
case RecordTypeEnum::TYPE_SOA: |
78
|
|
|
return self::encodeSOA($rdata); |
|
|
|
|
79
|
|
|
case RecordTypeEnum::TYPE_MX: |
80
|
|
|
return pack('n', (int) $rdata['preference']).self::encodeDomainName($rdata['exchange']); |
81
|
|
|
case RecordTypeEnum::TYPE_TXT: |
82
|
|
|
$rdata = substr($rdata, 0, 255); |
|
|
|
|
83
|
|
|
|
84
|
|
|
return chr(strlen($rdata)).$rdata; |
85
|
|
|
case RecordTypeEnum::TYPE_SRV: |
86
|
|
|
return pack('nnn', (int) $rdata['priority'], (int) $rdata['weight'], (int) $rdata['port']). |
87
|
|
|
self::encodeDomainName($rdata['target']); |
88
|
|
|
case RecordTypeEnum::TYPE_AXFR: |
89
|
|
|
case RecordTypeEnum::TYPE_ANY: |
90
|
|
|
return ''; |
91
|
|
|
default: |
92
|
|
|
throw new UnsupportedTypeException( |
93
|
|
|
sprintf('Record type "%s" is not a supported type.', RecordTypeEnum::getName($type)) |
94
|
|
|
); |
95
|
|
|
} |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* @param ResourceRecord[] $resourceRecords |
100
|
|
|
* |
101
|
|
|
* @return string |
102
|
|
|
* |
103
|
|
|
* @throws UnsupportedTypeException |
104
|
|
|
*/ |
105
|
|
|
public static function encodeResourceRecords(array $resourceRecords): string |
106
|
|
|
{ |
107
|
|
|
$res = ''; |
108
|
|
|
|
109
|
|
|
foreach ($resourceRecords as $rr) { |
110
|
|
|
$res .= self::encodeDomainName($rr->getName()); |
111
|
|
|
if ($rr->isQuestion()) { |
112
|
|
|
$res .= pack('nn', $rr->getType(), $rr->getClass()); |
113
|
|
|
continue; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
$data = self::encodeRdata($rr->getType(), $rr->getRdata()); |
117
|
|
|
$res .= pack('nnNn', $rr->getType(), $rr->getClass(), $rr->getTtl(), strlen($data)); |
118
|
|
|
$res .= $data; |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
return $res; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* @param Header $header |
126
|
|
|
* |
127
|
|
|
* @return string |
128
|
|
|
*/ |
129
|
|
|
public static function encodeHeader(Header $header): string |
130
|
|
|
{ |
131
|
|
|
return pack( |
132
|
|
|
'nnnnnn', |
133
|
|
|
$header->getId(), |
134
|
|
|
self::encodeFlags($header), |
135
|
|
|
$header->getQuestionCount(), |
136
|
|
|
$header->getAnswerCount(), |
137
|
|
|
$header->getNameServerCount(), |
138
|
|
|
$header->getAdditionalRecordsCount() |
139
|
|
|
); |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* Encode the bit field of the Header between "ID" and "QDCOUNT". |
144
|
|
|
* |
145
|
|
|
* @param Header $header |
146
|
|
|
* |
147
|
|
|
* @return int |
148
|
|
|
*/ |
149
|
|
|
private static function encodeFlags(Header $header): int |
150
|
|
|
{ |
151
|
|
|
return 0x0 | |
152
|
|
|
($header->isResponse() & 0x1) << 15 | |
153
|
|
|
($header->getOpcode() & 0xf) << 11 | |
154
|
|
|
($header->isAuthoritative() & 0x1) << 10 | |
155
|
|
|
($header->isTruncated() & 0x1) << 9 | |
156
|
|
|
($header->isRecursionDesired() & 0x1) << 8 | |
157
|
|
|
($header->isRecursionAvailable() & 0x1) << 7 | |
158
|
|
|
($header->getZ() & 0x7) << 4 | |
159
|
|
|
($header->getRcode() & 0xf); |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* @param array $soa |
164
|
|
|
* |
165
|
|
|
* @return string |
166
|
|
|
*/ |
167
|
|
|
private static function encodeSOA(array $soa): string |
168
|
|
|
{ |
169
|
|
|
return |
170
|
|
|
self::encodeDomainName($soa['mname']). |
171
|
|
|
self::encodeDomainName($soa['rname']). |
172
|
|
|
pack( |
173
|
|
|
'NNNNN', |
174
|
|
|
$soa['serial'], |
175
|
|
|
$soa['refresh'], |
176
|
|
|
$soa['retry'], |
177
|
|
|
$soa['expire'], |
178
|
|
|
$soa['minimum'] |
179
|
|
|
); |
180
|
|
|
} |
181
|
|
|
} |
182
|
|
|
|