Completed
Branch v2 (ec0e33)
by Sam
02:09
created

Validator::ipAddress()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
/*
4
 * This file is part of Badcow DNS Library.
5
 *
6
 * (c) Samuel Williams <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Badcow\DNS;
13
14
use Badcow\DNS\Rdata\NS;
15
use Badcow\DNS\Rdata\SOA;
16
17
class Validator
18
{
19
    const ZONE_OKAY = 0;
20
21
    const ZONE_NO_SOA = 1;
22
23
    const ZONE_TOO_MANY_SOA = 2;
24
25
    const ZONE_NO_NS = 4;
26
27
    const ZONE_NO_CLASS = 8;
28
29
    const ZONE_TOO_MANY_CLASSES = 16;
30
31
    /**
32
     * Validate the string as a valid hostname in accordance with RFC 952 {@link https://tools.ietf.org/html/rfc952}
33
     * and RFC 1123 {@link https://tools.ietf.org/html/rfc1123}.
34
     *
35
     * @param string $name
36
     *
37
     * @return bool
38
     */
39
    public static function hostName(string $name): bool
40
    {
41
        return self::fullyQualifiedDomainName(rtrim($name, '.').'.');
42
    }
43
44
    /**
45
     * Validate the string is a Fully Qualified Domain Name.
46
     *
47
     * @param string $name
48
     *
49
     * @return bool
50
     */
51
    public static function fullyQualifiedDomainName(string $name): bool
52
    {
53
        //Is there a trailing dot?
54
        if ('.' !== substr($name, -1, 1)) {
55
            return false;
56
        }
57
58
        if (strlen($name) > 255) {
59
            return false;
60
        }
61
62
        $labels = explode('.', rtrim($name, '.'));
63
64
        //Are there more than 127 levels?
65
        if (count($labels) > 127) {
66
            return false;
67
        }
68
69
        $isValid = true;
70
        foreach ($labels as $label) {
71
            //Does the label start with a hyphen?
72
            $isValid &= ('-' !== substr($label, 0, 1));
73
74
            //Does the label end with a hyphen?
75
            $isValid &= ('-' !== substr($label, -1, 1));
76
77
            //Does the label contain anything other than alphanumeric characters or hyphens?
78
            $isValid &= (1 === preg_match('/^[a-zA-Z0-9\-]+$/i', $label));
79
80
            //Is the length of the label between 1 and 63 characters?
81
            $isValid &= strlen($label) > 0 && strlen($label) < 64;
82
        }
83
84
        return $isValid;
85
    }
86
87
    /**
88
     * Validate the name for a Resource Record. This is distinct from validating a hostname in that this function
89
     * will permit '@' and wildcards as well as underscores used in SRV records.
90
     *
91
     * @param string $name
92
     *
93
     * @return bool
94
     */
95
    public static function resourceRecordName(string $name): bool
96
    {
97
        if ('@' === $name) {
98
            return true;
99
        }
100
101
        if ('*.' === $name) {
102
            return false;
103
        }
104
105
        if (strlen($name) > 255) {
106
            return false;
107
        }
108
109
        $labels = explode('.', rtrim($name, '.'));
110
111
        //Are there more than 127 levels?
112
        if (count($labels) > 127) {
113
            return false;
114
        }
115
116
        $isValid = true;
117
        foreach ($labels as $i => $label) {
118
            //Is the first label a wildcard?
119
            if (0 === $i && '*' === $label) {
120
                $isValid &= true;
121
                continue;
122
            }
123
124
            //Does the label start with a hyphen?
125
            $isValid &= ('-' !== substr($label, 0, 1));
126
127
            //Does the label end with a hyphen?
128
            $isValid &= ('-' !== substr($label, -1, 1));
129
130
            //Does the label contain anything other than alphanumeric characters, underscores or hyphens?
131
            $isValid &= (1 === preg_match('/^[a-zA-Z0-9\-_]+$/i', $label));
132
133
            //Is the length of the label between 1 and 63 characters?
134
            $isValid &= strlen($label) > 0 && strlen($label) < 64;
135
        }
136
137
        return $isValid;
138
    }
139
140
    /**
141
     * Validates an IPv4 Address.
142
     *
143
     * @static
144
     *
145
     * @param string $address
146
     *
147
     * @return bool
148
     */
149
    public static function ipv4(string $address): bool
150
    {
151
        return (bool) filter_var($address, FILTER_VALIDATE_IP, [
152
            'flags' => FILTER_FLAG_IPV4,
153
        ]);
154
    }
155
156
    /**
157
     * Validates an IPv6 Address.
158
     *
159
     * @static
160
     *
161
     * @param string $address
162
     *
163
     * @return bool
164
     */
165
    public static function ipv6(string $address): bool
166
    {
167
        return (bool) filter_var($address, FILTER_VALIDATE_IP, [
168
            'flags' => FILTER_FLAG_IPV6,
169
        ]);
170
    }
171
172
    /**
173
     * Validates an IPv4 or IPv6 address.
174
     *
175
     * @static
176
     *
177
     * @param $address
178
     *
179
     * @return bool
180
     */
181
    public static function ipAddress(string $address): bool
182
    {
183
        return (bool) filter_var($address, FILTER_VALIDATE_IP);
184
    }
185
186
    /**
187
     * Validates that the zone meets
188
     * RFC-1035 especially that:
189
     *   1) 5.2.1 All RRs in the file should be of the same class.
190
     *   2) 5.2.2 Exactly one SOA RR should be present at the top of the zone.
191
     *   3) There is at least one NS record.
192
     *
193
     * Return values are:
194
     *   - ZONE_NO_SOA
195
     *   - ZONE_TOO_MANY_SOA
196
     *   - ZONE_NO_NS
197
     *   - ZONE_NO_CLASS
198
     *   - ZONE_TOO_MANY_CLASSES
199
     *   - ZONE_OKAY
200
     *
201
     * You SHOULD compare these return values to the defined constants of this
202
     * class rather than against integers directly.
203
     *
204
     * @param Zone $zone
205
     *
206
     * @return int
207
     */
208
    public static function zone(Zone $zone): int
209
    {
210
        $n_soa = self::countResourceRecords($zone, SOA::TYPE);
211
        $n_ns = self::countResourceRecords($zone, NS::TYPE);
212
        $classes = [];
213
214
        foreach ($zone as $rr) {
215
            if (null !== $rr->getClass()) {
216
                $classes[$rr->getClass()] = null;
217
            }
218
        }
219
220
        $n_class = count($classes);
221
        $errors = 0;
222
223
        if ($n_soa < 1) {
224
            $errors += self::ZONE_NO_SOA;
225
        }
226
227
        if ($n_soa > 1) {
228
            $errors += self::ZONE_TOO_MANY_SOA;
229
        }
230
231
        if ($n_ns < 1) {
232
            $errors += self::ZONE_NO_NS;
233
        }
234
235
        if ($n_class < 1) {
236
            $errors += self::ZONE_NO_CLASS;
237
        }
238
239
        if ($n_class > 1) {
240
            $errors += self::ZONE_TOO_MANY_CLASSES;
241
        }
242
243
        return $errors;
244
    }
245
246
    /**
247
     * Counts the number of Resource Records of a particular type ($type) in a Zone.
248
     *
249
     * @param Zone   $zone
250
     * @param string $type The ResourceRecord type to be counted. If NULL, then the method will return
251
     *                     the number of records without RData.
252
     *
253
     * @return int the number of records to be counted
254
     */
255
    public static function countResourceRecords(Zone $zone, ?string $type = null): int
256
    {
257
        $n = 0;
258
        foreach ($zone as $rr) {
259
            $n += (int) ($type === $rr->getType());
260
        }
261
262
        return $n;
263
    }
264
265
    /**
266
     * Validates a reverse IPv4 address. Ensures that all octets are in the range [0-255].
267
     *
268
     * @param string $address
269
     *
270
     * @return bool
271
     */
272
    public static function reverseIpv4(string $address): bool
273
    {
274
        $pattern = '/^((?:[0-9]+\.){1,4})in\-addr\.arpa\.$/i';
275
276
        if (1 !== preg_match($pattern, $address, $matches)) {
277
            return false;
278
        }
279
280
        $octets = explode('.', $matches[1]);
281
        array_pop($octets); //Remove the last decimal from the array
282
283
        foreach ($octets as $octet) {
284
            if ((int) $octet > 255) {
285
                return false;
286
            }
287
        }
288
289
        return true;
290
    }
291
292
    /**
293
     * Validates a reverse IPv6 address.
294
     *
295
     * @param string $address
296
     *
297
     * @return bool
298
     */
299
    public static function reverseIpv6(string $address): bool
300
    {
301
        $pattern = '/^(?:[0-9a-f]\.){1,32}ip6\.arpa\.$/i';
302
303
        return 1 === preg_match($pattern, $address);
304
    }
305
}
306