RFC7585Tests::relevantNAPTRhostnameResolution()   F
last analyzed

Complexity

Conditions 16
Paths 1058

Size

Total Lines 67
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 40
dl 0
loc 67
rs 1.4
c 2
b 0
f 0
cc 16
nc 1058
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * *****************************************************************************
5
 * Contributions to this work were made on behalf of the GÉANT project, a 
6
 * project that has received funding from the European Union’s Framework 
7
 * Programme 7 under Grant Agreements No. 238875 (GN3) and No. 605243 (GN3plus),
8
 * Horizon 2020 research and innovation programme under Grant Agreements No. 
9
 * 691567 (GN4-1) and No. 731122 (GN4-2).
10
 * On behalf of the aforementioned projects, GEANT Association is the sole owner
11
 * of the copyright in all material which was developed by a member of the GÉANT
12
 * project. GÉANT Vereniging (Association) is registered with the Chamber of 
13
 * Commerce in Amsterdam with registration number 40535155 and operates in the 
14
 * UK as a branch of GÉANT Vereniging.
15
 * 
16
 * Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands. 
17
 * UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK
18
 *
19
 * License: see the web/copyright.inc.php file in the file structure or
20
 *          <base_url>/copyright.php after deploying the software
21
 */
22
23
namespace core\diag;
24
25
/**
26
 * Test suite to verify that a given NAI realm has NAPTR records according to
27
 * consortium-agreed criteria
28
 * Can only be used if \config\Diagnostics::RADIUSTESTS is configured.
29
 *
30
 * @author Stefan Winter <[email protected]>
31
 * @author Tomasz Wolniewicz <[email protected]>
32
 * @author Maja Górecka-Wolniewicz <[email protected]>
33
 *
34
 * @license see LICENSE file in root directory
35
 *
36
 * @package Developer
37
 */
38
class RFC7585Tests extends AbstractTest
39
{
40
41
    /**
42
     * maintains state for the question: has the NAPTR existence check already been executed? Holds the number of NAPTR records found if so.
43
     * 
44
     * @var integer
45
     */
46
    private $NAPTR_executed;
47
48
    /**
49
     * maintains state for the question: has the NAPTR compliance check already been executed?
50
     * 
51
     * @var integer
52
     */
53
    private $NAPTR_compliance_executed;
54
55
    /**
56
     * maintains state for the question: has the NAPTR SRV check already been executed? Holds the number of SRV records if so.
57
     * 
58
     * @var integer
59
     */
60
    private $NAPTR_SRV_executed;
61
62
    /**
63
     * maintains state for the question: has the existrence of hostnames been checked already? Holds the number of IP:port pairs if so.
64
     * 
65
     * @var integer
66
     */
67
    private $NAPTR_hostname_executed;
68
69
    /**
70
     * holds the list of NAPTR records found. Exposed because we may want to
71
     * double-check the replacement against NRO expectations
72
     * 
73
     * @var array
74
     */
75
    public $NAPTR_records;
76
77
    /**
78
     * holds the list of SRV records found
79
     * 
80
     * @var array
81
     */
82
    private $NAPTR_SRV_records;
83
84
    /**
85
     * stores the various errors encountered during the checks
86
     * 
87
     * @var array
88
     */
89
    private $errorlist;
90
91
    /**
92
     * stores the IP address / port pairs (strings) which were ultimately found
93
     * as candidate RADIUS/TLS servers
94
     * 
95
     * @var array
96
     */
97
    public $NAPTR_hostname_records;
98
    // return codes specific to NAPTR existence checks
99
100
    /**
101
     * test hasn't been run yet
102
     */
103
    const RETVAL_NOTRUNYET = -1;
104
105
    /**
106
     * no NAPTRs for domain; this is not an error, simply means that realm is not doing dynamic discovery for any service
107
     */
108
    const RETVAL_NONAPTR = -104;
109
110
    /**
111
     * no eduroam NAPTR for domain; this is not an error, simply means that realm is not doing dynamic discovery for eduroam
112
     */
113
    const RETVAL_ONLYUNRELATEDNAPTR = -105;
114
115
    /**
116
     * This private variable contains the realm to be checked. Is filled in the
117
     * class constructor.
118
     * 
119
     * @var string
120
     */
121
    private $realm;
122
123
    /**
124
     * An instance of the Net_DNS2 resolver to do lookups with
125
     * 
126
     * @var \Net_DNS2_Resolver
127
     */
128
    private $resolver;
129
130
    /**
131
     * maintains state whether all DNS responses were DNSSEC-secured
132
     * 
133
     * @var bool
134
     */
135
    public $allResponsesSecure;
136
137
    /**
138
     * the discovery tag to use for the DNS lookups
139
     * 
140
     * @var string
141
     */
142
    private $discoveryTag;
143
    
144
    /**
145
     * Initialises the dynamic discovery test instance for a specific realm that is to be tested
146
     * 
147
     * @param string $realm the realm to be tested
148
     */
149
    public function __construct(string $realm, $discoverytag = \config\Diagnostics::RADIUSTESTS['TLS-discoverytag'])
150
    {
151
        parent::__construct();
152
        \core\common\Entity::intoThePotatoes();
153
        // return codes specific to NAPTR existence checks
154
        /**
155
         * no NAPTRs for domain; this is not an error, simply means that realm is not doing dynamic discovery for any service
156
         */
157
        $this->returnCodes[RFC7585Tests::RETVAL_NONAPTR]["message"] = _("This realm has no NAPTR records.");
158
        $this->returnCodes[RFC7585Tests::RETVAL_NONAPTR]["severity"] = \core\common\Entity::L_OK;
159
160
        /**
161
         * no eduroam NAPTR for domain; this is not an error, simply means that realm is not doing dynamic discovery for eduroam
162
         */
163
        $this->returnCodes[RFC7585Tests::RETVAL_ONLYUNRELATEDNAPTR]["message"] = _("NAPTR records were found, but all of them refer to unrelated services.");
164
        $this->returnCodes[RFC7585Tests::RETVAL_ONLYUNRELATEDNAPTR]["severity"] = \core\common\Entity::L_OK;
165
166
        $this->realm = $realm;
167
        $this->NAPTR_executed = RFC7585Tests::RETVAL_NOTRUNYET;
168
        $this->NAPTR_compliance_executed = RFC7585Tests::RETVAL_NOTRUNYET;
169
        $this->NAPTR_SRV_executed = RFC7585Tests::RETVAL_NOTRUNYET;
170
        $this->NAPTR_hostname_executed = RFC7585Tests::RETVAL_NOTRUNYET;
171
        $this->NAPTR_records = [];
172
        $this->NAPTR_SRV_records = [];
173
        $this->NAPTR_hostname_records = [];
174
        $this->errorlist = [];
175
        \core\common\Entity::outOfThePotatoes();
176
177
        $this->discoveryTag = $discoverytag;
178
        
179
        $this->resolver = new \Net_DNS2_Resolver(
180
                ["dnssec_ad_flag" => true]
181
        );
182
        $this->allResponsesSecure = true;
183
    }
184
185
    /**
186
     * Tests if this realm exists in DNS and has NAPTR records matching the
187
     * configured consortium NAPTR target.
188
     * 
189
     * possible RETVALs:
190
     * - RETVAL_NOTCONFIGURED; needs \config\Diagnostics::RADIUSTESTS['TLS-discoverytag']
191
     * - RETVAL_ONLYUNRELATEDNAPTR
192
     * - RETVAL_NONAPTR
193
     * 
194
     * @return int Either a RETVAL constant or a positive number (count of relevant NAPTR records)
195
     */
196
    public function relevantNAPTR()
197
    {
198
        if ($this->discoveryTag == "") {
199
            $this->NAPTR_executed = RADIUSTests::RETVAL_NOTCONFIGURED;
200
            return RADIUSTests::RETVAL_NOTCONFIGURED;
201
        }
202
        $NAPTRs = [];
203
        try {
204
            $response = $this->resolver->query($this->realm, 'NAPTR');
205
            $securedAnswer = $response->header->ad ?? 0;
206
            if ($securedAnswer == 0) {
207
                $this->allResponsesSecure = FALSE;
208
            }
209
            foreach ($response->answer as $oneAnswer) {
210
                $NAPTRs[] = [
211
                    'services' => $oneAnswer->services,
212
                    'flags' => $oneAnswer->flags,
213
                    'regexp' => $oneAnswer->regexp,
214
                    'replacement' => $oneAnswer->replacement,
215
                    'ad' => $securedAnswer,
216
                ];
217
            }
218
        } catch (\Net_DNS2_Exception $e) {
219
            // NXDOMAIN is an Exception, but we don't care - no result means no result
220
        }
221
        if (count($NAPTRs) == 0) {
222
            $this->NAPTR_executed = RFC7585Tests::RETVAL_NONAPTR;
223
            return RFC7585Tests::RETVAL_NONAPTR;
224
        }
225
        $NAPTRs_consortium = [];
226
        foreach ($NAPTRs as $naptr) {
227
            if ($naptr["services"] == $this->discoveryTag) {
228
                $NAPTRs_consortium[] = $naptr;
229
            }
230
        }
231
        if (count($NAPTRs_consortium) == 0) {
232
            $this->NAPTR_executed = RFC7585Tests::RETVAL_ONLYUNRELATEDNAPTR;
233
            return RFC7585Tests::RETVAL_ONLYUNRELATEDNAPTR;
234
        }
235
        $this->NAPTR_records = $NAPTRs_consortium;
236
        $this->NAPTR_executed = count($NAPTRs_consortium);
237
        return count($NAPTRs_consortium);
238
    }
239
240
    /**
241
     * Tests if all the discovered NAPTR entries conform to the consortium's requirements
242
     * 
243
     * possible RETVALs:
244
     * - RETVAL_NOTCONFIGURED; needs \config\Diagnostics::RADIUSTESTS['TLS-discoverytag']
245
     * - RETVAL_INVALID (at least one format error)
246
     * - RETVAL_OK (all fine)
247
248
     * @return int one of two RETVALs above
249
     */
250
    public function relevantNAPTRcompliance()
251
    {
252
// did we query DNS for the NAPTRs yet? If not, do so now.
253
        if ($this->NAPTR_executed == RFC7585Tests::RETVAL_NOTRUNYET) {
254
            $this->relevantNAPTR();
255
        }
256
// if the NAPTR checks aren't configured, tell the caller
257
        if ($this->NAPTR_executed === RADIUSTests::RETVAL_NOTCONFIGURED) {
258
            $this->NAPTR_compliance_executed = RADIUSTests::RETVAL_NOTCONFIGURED;
259
            return RADIUSTests::RETVAL_NOTCONFIGURED;
260
        }
261
// if there were no relevant NAPTR records, we are compliant :-)
262
        if (count($this->NAPTR_records) == 0) {
263
            $this->NAPTR_compliance_executed = RADIUSTests::RETVAL_OK;
264
            return RADIUSTests::RETVAL_OK;
265
        }
266
        $formatErrors = [];
267
// format of NAPTRs is consortium specific. eduroam and OpenRoaming below; 
268
// others need their own code
269
        if ($this->discoveryTag == "x-eduroam:radius.tls" || $this->discoveryTag == "aaa+auth:radius.tls.tcp") {
270
            foreach ($this->NAPTR_records as $edupointer) {
271
// must be "s" type for SRV
272
                if ($edupointer["flags"] != "s" && $edupointer["flags"] != "S") {
273
                    $formatErrors[] = ["TYPE" => "NAPTR-FLAG", "TARGET" => $edupointer['flag']];
274
                }
275
// no regex
276
                if (isset($edupointer["regex"]) && $edupointer["regex"] != "") {
277
                    $formatErrors[] = ["TYPE" => "NAPTR-REGEX", "TARGET" => $edupointer['regex']];
278
                }
279
            }
280
        }
281
        if (count($formatErrors) == 0) {
282
            $this->NAPTR_compliance_executed = RADIUSTests::RETVAL_OK;
283
            return RADIUSTests::RETVAL_OK;
284
        }
285
        $this->errorlist = array_merge($this->errorlist, $formatErrors);
286
        $this->NAPTR_compliance_executed = RADIUSTests::RETVAL_INVALID;
287
        return RADIUSTests::RETVAL_INVALID;
288
    }
289
290
    /**
291
     * Tests if NAPTR records can be resolved to SRVs. Will only run if NAPTR
292
     * checks completed without error.
293
     *
294
     * possible RETVALs:
295
     * - RETVAL_INVALID
296
     * - RETVAL_SKIPPED
297
     * 
298
     * @return int one of the RETVALs above or the number of SRV records which were resolved
299
     */
300
    public function relevantNAPTRsrvResolution()
301
    {
302
// see if preceding checks have been run, and run them if not
303
// compliance check will cascade NAPTR check on its own
304
        if ($this->NAPTR_compliance_executed == RFC7585Tests::RETVAL_NOTRUNYET) {
305
            $this->relevantNAPTRcompliance();
306
        }
307
// we only run the SRV checks if all records are compliant and more than one relevant NAPTR exists
308
        if ($this->NAPTR_executed <= 0 || $this->NAPTR_compliance_executed == RADIUSTests::RETVAL_INVALID) {
309
            $this->NAPTR_SRV_executed = RADIUSTests::RETVAL_SKIPPED;
310
            return RADIUSTests::RETVAL_SKIPPED;
311
        }
312
        $sRVerrors = [];
313
        $sRVtargets = [];
314
        $sRVcount = 0;
315
316
        foreach ($this->NAPTR_records as $edupointer) {
317
            try {
318
                $response = $this->resolver->query($edupointer["replacement"], 'SRV');
319
                $securedAnswer = $response->header->ad ?? 0;
320
                if ($securedAnswer == 0) {
321
                    $this->allResponsesSecure = FALSE;
322
                }
323
                foreach ($response->answer as $oneAnswer) {
324
                    $sRVtargets[] = [
325
                        'hostname' => $oneAnswer->target,
326
                        'port' => $oneAnswer->port,
327
                        'ad' => $securedAnswer,
328
                    ];
329
                }
330
            } catch (\Net_DNS2_Exception $e) {
331
                // NXDOMAIN is an Exception, but we don't care - no result means no result
332
            }
333
            if (count($sRVtargets) == $sRVcount) { // no new target added... defunct replacement
334
                $sRVerrors[] = ["TYPE" => "SRV_NOT_RESOLVING", "TARGET" => $edupointer['replacement']];
335
            }
336
            $sRVcount = count($sRVtargets);
337
        }
338
        $this->NAPTR_SRV_records = $sRVtargets;
339
        if (count($sRVerrors) > 0) {
340
            $this->NAPTR_SRV_executed = RADIUSTests::RETVAL_INVALID;
341
            $this->errorlist = array_merge($this->errorlist, $sRVerrors);
342
            return RADIUSTests::RETVAL_INVALID;
343
        }
344
        $this->NAPTR_SRV_executed = count($sRVtargets);
345
        return count($sRVtargets);
346
    }
347
348
    /**
349
     * Checks whether the previously discovered hostnames have actual IP addresses in DNS.
350
     * 
351
     * The actual list is stored in the class property NAPTR_hostname_records.
352
     * 
353
     * @return int count of IP / port pairs for all the hostnames
354
     */
355
    public function relevantNAPTRhostnameResolution()
356
    {
357
// make sure the previous tests have been run before we go on
358
// preceding tests will cascade automatically if needed
359
        if ($this->NAPTR_SRV_executed == RFC7585Tests::RETVAL_NOTRUNYET) {
360
            $this->relevantNAPTRsrvResolution();
361
        }
362
// if previous are SKIPPED, skip this one, too
363
        if ($this->NAPTR_SRV_executed == RADIUSTests::RETVAL_SKIPPED) {
364
            $this->NAPTR_hostname_executed = RADIUSTests::RETVAL_SKIPPED;
365
            return RADIUSTests::RETVAL_SKIPPED;
366
        }
367
// the SRV check may have returned INVALID, but could have found a
368
// a working subset of hosts anyway. We should continue checking all 
369
// discovered names.
370
371
        $ipAddrs = [];
372
        $resolutionErrors = [];
373
374
        $responsesUpToHereWereSecure = $this->allResponsesSecure;
375
        foreach ($this->NAPTR_SRV_records as $server) {
376
            foreach (["A", "AAAA"] as $family) {
377
                try {
378
                    $response = $this->resolver->query($server["hostname"], $family);
379
                    $securedAnswer = $response->header->ad ?? 0;
380
                    if ($securedAnswer == 0) {
381
                        $this->allResponsesSecure = FALSE;
382
                    }
383
                    foreach ($response->answer as $oneAnswer) {
384
                        if (!$oneAnswer->address) {
385
                            continue;
386
                        }
387
                        $ipAddrs[] = [
388
                            'hostname' => $server['hostname'],
389
                            'port' => $server['port'],
390
                            'family' => ($family == "A" ? "IPv4" : "IPv6"),
391
                            'IP' => $oneAnswer->address,
392
                            'securepath' => $securedAnswer && $responsesUpToHereWereSecure,
393
                        ];
394
                    }
395
                } catch (\Net_DNS2_Exception $e) {
396
                    // NXDOMAIN is an Exception, but we don't care - no result means no result
397
                }
398
            }
399
        }
400
        
401
        $this->NAPTR_hostname_records = $ipAddrs;
402
        $orphanedHostnames = [];
403
        foreach ($this->NAPTR_SRV_records as $srvHostnames) {
404
            $orphanedHostnames[$srvHostnames['hostname']] = "MISSING";
405
            foreach ($this->NAPTR_hostname_records as $resolvedHostnames) {
406
                if ($resolvedHostnames['hostname'] == $srvHostnames['hostname']) {
407
                    unset($orphanedHostnames[$resolvedHostnames['hostname']]);
408
                }
409
            }
410
        }
411
        foreach ($orphanedHostnames as $name => $nomatter) {
412
                $resolutionErrors[] = ["TYPE" => "HOST_NO_ADDRESS", "TARGET" => $name];
413
            }
414
                    
415
        if (count($resolutionErrors) > 0) {
416
            $this->errorlist = array_merge($this->errorlist, $resolutionErrors);
417
            $this->NAPTR_hostname_executed = RADIUSTests::RETVAL_INVALID;
418
            return RADIUSTests::RETVAL_INVALID;
419
        }
420
        $this->NAPTR_hostname_executed = count($this->NAPTR_hostname_records);
421
        return count($this->NAPTR_hostname_records);
422
    }
423
}
424