DeploymentManaged   F
last analyzed

Complexity

Total Complexity 81

Size/Duplication

Total Lines 628
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 81
eloc 274
c 4
b 0
f 0
dl 0
loc 628
rs 2

17 Methods

Rating   Name   Duplication   Size   Complexity  
F setRADIUSconfig() 0 35 15
A activate() 0 5 1
A deactivate() 0 5 1
A checkRADIUSHostandConfigDaemon() 0 16 5
A getOperatorName() 0 7 2
A updateFreshness() 0 4 1
A getFreshness() 0 7 2
A remove() 0 5 1
B sendMailtoAdmin() 0 31 8
B initialise() 0 34 6
A checkURL() 0 24 5
A sendToRADIUS() 0 30 4
B __construct() 0 67 9
A retrieveStatistics() 0 16 2
A getAllRealms() 0 16 5
A testRADIUSHost() 0 15 4
B findGoodServerLocation() 0 42 10

How to fix   Complexity   

Complex Class

Complex classes like DeploymentManaged often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DeploymentManaged, and based on these observations, apply Extract Interface, too.

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
/**
24
 * This file contains the AbstractProfile class. It contains common methods for
25
 * both RADIUS/EAP profiles and SilverBullet profiles
26
 *
27
 * @author Stefan Winter <[email protected]>
28
 * @author Tomasz Wolniewicz <[email protected]>
29
 *
30
 * @package Developer
31
 *
32
 */
33
34
namespace core;
35
36
use \Exception;
0 ignored issues
show
Bug introduced by
The type \Exception was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
37
38
/**
39
 * This class represents an EAP Profile.
40
 * Profiles can inherit attributes from their IdP, if the IdP has some. Otherwise,
41
 * one can set attribute in the Profile directly. If there is a conflict between
42
 * IdP-wide and Profile-wide attributes, the more specific ones (i.e. Profile) win.
43
 * 
44
 * @author Stefan Winter <[email protected]>
45
 * @author Tomasz Wolniewicz <[email protected]>
46
 *
47
 * @license see LICENSE file in root directory
48
 *
49
 * @package Developer
50
 */
51
class DeploymentManaged extends AbstractDeployment
52
{
53
54
    /**
55
     * This is the limit for dual-stack hosts. Single stack uses half of the FDs
56
     * in FreeRADIUS and take twice as many. initialise() takes this into
57
     * account.
58
     */
59
    const MAX_CLIENTS_PER_SERVER = 200;
60
    const PRODUCTNAME = "Managed SP";
61
62
    /**
63
     * the primary RADIUS server port for this SP instance
64
     * 
65
     * @var integer
66
     */
67
    public $port1;
68
69
    /**
70
     * the backup RADIUS server port for this SP instance
71
     * 
72
     * @var integer
73
     */
74
    public $port2;
75
76
    /**
77
     * the shared secret for this SP instance
78
     * 
79
     * @var string
80
     */
81
    public $secret;
82
83
    /**
84
     * the IPv4 address of the primary RADIUS server for this SP instance 
85
     * (can be NULL)
86
     * 
87
     * @var string
88
     */
89
    public $host1_v4;
90
91
    /**
92
     * the IPv6 address of the primary RADIUS server for this SP instance 
93
     * (can be NULL)
94
     * 
95
     * @var string
96
     */
97
    public $host1_v6;
98
99
    /**
100
     * the IPv4 address of the backup RADIUS server for this SP instance 
101
     * (can be NULL)
102
     * 
103
     * @var string
104
     */
105
    public $host2_v4;
106
107
    /**
108
     * the IPv6 address of the backup RADIUS server for this SP instance 
109
     * (can be NULL)
110
     * 
111
     * @var string
112
     */
113
    public $host2_v6;
114
115
    /**
116
     * the primary RADIUS server instance for this SP instance
117
     * 
118
     * @var string
119
     */
120
    public $radius_instance_1;
121
122
    /**
123
     * the backup RADIUS server instance for this SP instance
124
     * 
125
     * @var string
126
     */
127
    public $radius_instance_2;
128
129
    /**
130
     * the primary RADIUS server hostname - for sending configuration requests
131
     * 
132
     * @var string
133
     */
134
    public $radius_hostname_1;
135
136
    /**
137
     * the backup RADIUS server hostname - for sending configuration requests
138
     * 
139
     * @var string
140
     */
141
    public $radius_hostname_2;
142
143
    /**
144
     * the primary RADIUS server status - last configuration request result
145
     * 
146
     * @var string
147
     */
148
    public $radius_status_1;
149
150
    /**
151
     * the backup RADIUS server status - last configuration request result
152
     * 
153
     * @var string
154
     */
155
    public $radius_status_2;
156
    
157
    /**
158
     * the consortium this deployment is attached to
159
     * 
160
     * @var string
161
     */
162
    public $consortium;
163
164
    /**
165
     * the T&C of this service
166
     */
167
    public $termsAndConditions;
168
    
169
    /**
170
     * Class constructor for existing deployments (use 
171
     * IdP::newDeployment() to actually create one). Retrieves all 
172
     * attributes from the DB and stores them in the priv_ arrays.
173
     * 
174
     * @param IdP        $idpObject       optionally, the institution to which this Profile belongs. Saves the construction of the IdP instance. If omitted, an extra query and instantiation is executed to find out.
175
     * @param string|int $deploymentIdRaw identifier of the deployment in the DB
176
     * @param string     $consortium      identifier of the consortium to attach to (only relevant when initialising a new deployment for the first time
177
     * @throws Exception
178
     */
179
    public function __construct($idpObject, $deploymentIdRaw, $consortium = 'eduroam')
180
    {
181
        parent::__construct($idpObject, $deploymentIdRaw); // we now have access to our INST database handle and logging
182
        $this->entityOptionTable = "deployment_option";
183
        $this->entityIdColumn = "deployment_id";
184
        $this->type = AbstractDeployment::DEPLOYMENTTYPE_MANAGED;
185
        if (!is_numeric($deploymentIdRaw)) {
186
            throw new Exception("Managed SP instances have to have a numeric identifier");
187
        }
188
        $propertyQuery = "SELECT consortium, status,port_instance_1,port_instance_2,secret,radius_instance_1,radius_instance_2,radius_status_1,radius_status_2,consortium FROM deployment WHERE deployment_id = ?";
189
        $queryExec = $this->databaseHandle->exec($propertyQuery, "i", $deploymentIdRaw);
190
        if (mysqli_num_rows(/** @scrutinizer ignore-type */ $queryExec) == 0) {
191
            throw new Exception("Attempt to construct an unknown DeploymentManaged!");
192
        }
193
        $this->identifier = $deploymentIdRaw;
194
        while ($iterator = mysqli_fetch_object(/** @scrutinizer ignore-type */ $queryExec)) {
195
            if ($iterator->secret == NULL && $iterator->radius_instance_1 == NULL) {
196
                // we are instantiated for the first time, or all previous init attempts failed - so initialise us
197
                // first time: note the consortium, permanently
198
                if ($iterator->consortium === NULL) {
199
                    $this->consortium = $consortium;
200
                    $consortiumQuery = "UPDATE deployment SET consortium = '$this->consortium' WHERE deployment_id = ?";
201
                    $this->databaseHandle->exec($consortiumQuery, "i", $deploymentIdRaw);
202
                }
203
                $details = $this->initialise();
204
                $this->port1 = $details["port_instance_1"];
205
                $this->port2 = $details["port_instance_2"];
206
                $this->secret = $details["secret"];
207
                $this->radius_instance_1 = $details["radius_instance_1"];
208
                $this->radius_instance_2 = $details["radius_instance_2"];
209
                $this->radius_status_1 = 1;
210
                $this->radius_status_2 = 1;
211
                $this->status = AbstractDeployment::INACTIVE;
212
            } else {
213
                $this->port1 = $iterator->port_instance_1;
214
                $this->port2 = $iterator->port_instance_2;
215
                $this->secret = $iterator->secret;
216
                $this->radius_instance_1 = $iterator->radius_instance_1;
217
                $this->radius_instance_2 = $iterator->radius_instance_2;
218
                $this->radius_status_1 = $iterator->radius_status_1;
219
                $this->radius_status_2 = $iterator->radius_status_2;
220
                $this->status = $iterator->status;
221
                $this->consortium = $iterator->consortium;
222
            }
223
        }
224
        $server1 = $this->radius_instance_1;
225
        $server1details = $this->databaseHandle->exec("SELECT mgmt_hostname, radius_ip4, radius_ip6 FROM managed_sp_servers WHERE server_id = ?", "s", $server1);
226
        while ($iterator2 = mysqli_fetch_object(/** @scrutinizer ignore-type */ $server1details)) {
227
            $this->host1_v4 = $iterator2->radius_ip4;
228
            $this->host1_v6 = $iterator2->radius_ip6;
229
            $this->radius_hostname_1 = $iterator2->mgmt_hostname;
230
        }
231
        $server2 = $this->radius_instance_2;
232
        $server2details = $this->databaseHandle->exec("SELECT mgmt_hostname, radius_ip4, radius_ip6 FROM managed_sp_servers WHERE server_id = ?", "s", $server2);
233
        while ($iterator3 = mysqli_fetch_object(/** @scrutinizer ignore-type */ $server2details)) {
234
            $this->host2_v4 = $iterator3->radius_ip4;
235
            $this->host2_v6 = $iterator3->radius_ip6;
236
            $this->radius_hostname_2 = $iterator3->mgmt_hostname;
237
        }
238
        $thisLevelAttributes = $this->retrieveOptionsFromDatabase("SELECT DISTINCT option_name, option_lang, option_value, row_id 
239
                                            FROM $this->entityOptionTable
240
                                            WHERE $this->entityIdColumn = ?  
241
                                            ORDER BY option_name", "Profile");
242
        $tempAttribMergedIdP = $this->levelPrecedenceAttributeJoin($thisLevelAttributes, $this->idpAttributes, "IdP");
243
        $this->attributes = $this->levelPrecedenceAttributeJoin($tempAttribMergedIdP, $this->fedAttributes, "FED");
244
        
245
        $this->termsAndConditions = "<h2>Product Definition</h2>
246
<p>eduroam Managed SP enables eligible institutions to outsource the technical setup of the roaming uplink functions in an eduroam SP to the eduroam Operations Team. eduroam SP administrators use the service instead of a local RADIUS infrastructure. A unique selling point of this service is that the typical limitation of being required to have a static IP address is waived - eduroam SP Wi-Fi infrastructure can be managed by the system even on IP connectivity with changing IP addresses (e.g. DSL, mobile networks).</p>
247
<p>The service includes:</p>
248
<ul><li>web-based user management interface where eduroam SP deployment details can be created and deleted;</li>
249
    <li>web-based institution management interface where institutions are enabled or disabled to use the service;</li>
250
    <li>technical infrastructure ('RADIUS') which accepts RADIUS/UDP requests independently of client IP addresses, processes them according to eduroam Service Definition best practices, and forwards them to eduroam IdPs via the established eduroam roaming infrastructure.</li>
251
</ul>
252
<p>The aspects of eduroam SP operation beyond the RADIUS uplink remain in the responsibility of the eduroam SP administrator, and are subject to the eduroam Service Definition as usual. This includes (but is not limited to) local logging of IP leases to MAC addresses in the Enterprise Wi-Fi session, having sufficient Wi-Fi coverage, and making sure the IP uplink works within expected parameters.</p>
253
<h2>Terms of Use</h2>
254
<p>In addition to the provisions in your participation agreement with your NRO and the eduroam Service Definition, you undertake to keep confidential the RADIUS server uplink details you receive here (which consist of the tuple IP address, port and shared secret).";
255
    }
256
257
    /**
258
     * finds a suitable server which is geographically close to the admin
259
     * 
260
     * @param array  $adminLocation      the current geographic position of the admin
261
     * @param string $federation         the federation this deployment belongs to
262
     * @param array  $blacklistedServers list of server to IGNORE
263
     * @return string the server ID
264
     * @throws Exception
265
     */
266
    private function findGoodServerLocation($adminLocation, $federation, $blacklistedServers)
267
    {
268
        // find a server near him (list of all servers with capacity, ordered by distance)
269
        // first, if there is a pool of servers specifically for this federation, prefer it
270
        // only check the consortium pool group we want to attach to
271
        $cons = $this->consortium;
272
        $servers = $this->databaseHandle->exec("SELECT server_id, radius_ip4, radius_ip6, location_lon, location_lat FROM managed_sp_servers WHERE pool = ? AND consortium = ?", "ss", $federation, $cons);
273
274
        $serverCandidates = [];
275
        while ($iterator = mysqli_fetch_object(/** @scrutinizer ignore-type */ $servers)) {
276
            $maxSupportedClients = DeploymentManaged::MAX_CLIENTS_PER_SERVER;
277
            if ($iterator->radius_ip4 == NULL || $iterator->radius_ip6 == NULL) {
278
                // half the amount of IP stacks means half the amount of FDs in use, so we can take twice as many
279
                $maxSupportedClients = $maxSupportedClients * 2;
280
            }
281
            $serverId = $iterator->server_id;
282
            $clientCount1 = $this->databaseHandle->exec("SELECT port_instance_1 AS tenants1 FROM deployment WHERE radius_instance_1 = ?", "s", $serverId);
283
            $clientCount2 = $this->databaseHandle->exec("SELECT port_instance_2 AS tenants2 FROM deployment WHERE radius_instance_2 = ?", "s", $serverId);
284
285
            $clients = $clientCount1->num_rows + $clientCount2->num_rows;
286
            if (in_array($iterator->server_id, $blacklistedServers)) {
287
                continue;
288
            }
289
            if ($clients < $maxSupportedClients) {
290
                $serverCandidates[IdPlist::geoDistance($adminLocation, ['lat' => $iterator->location_lat, 'lon' => $iterator->location_lon])] = $iterator->server_id;
291
            }
292
            if ($clients > $maxSupportedClients * 0.9) {
293
                $this->loggerInstance->debug(1, "A RADIUS server for Managed SP (" . $iterator->server_id . ") is serving at more than 90% capacity!");
294
            }
295
        }
296
        if (count($serverCandidates) == 0 && $federation != "DEFAULT") {
297
            // we look in the default pool instead
298
            // recursivity! Isn't that cool!
299
            return $this->findGoodServerLocation($adminLocation, "DEFAULT", $blacklistedServers);
300
        }
301
        if (count($serverCandidates) == 0) {
302
            throw new Exception("No available server found for new SP in $federation!");
303
        }
304
        // put the nearest server on top of the list
305
        ksort($serverCandidates);
306
        $this->loggerInstance->debug(1, $serverCandidates);
307
        return array_shift($serverCandidates);
308
    }
309
310
    /**
311
     * retrieves usage statistics for the deployment
312
     * 
313
     * @param int $backlog how many seconds back in time should we look
314
     * @param int $limit   max number of entries to retrieve (if supplied, backlog will be ignored)
315
     * @return array of arrays: activity_time, realm, mac, result
316
     * @throws Exception
317
     */
318
    public function retrieveStatistics(int $backlog, int $limit = 0)
319
    {
320
        // find a server near him (list of all servers with capacity, ordered by distance)
321
        // first, if there is a pool of servers specifically for this federation, prefer it
322
        // only check the consortium pool group we want to attach to
323
        // TODO: if we also collect stats from OpenRoaming hosts, differentiate the logs!
324
        $opName = $this->getOperatorName();
325
        if ($limit !== 0) {
326
            $conditional1 = "";
327
            $conditional2 = "DESC LIMIT $limit";
328
        } else {
329
            $conditional1 = "AND activity_time > DATE_SUB(NOW(), INTERVAL $backlog SECOND)";
330
            $conditional2 = "";
331
        }
332
        $stats = $this->databaseHandle->exec("SELECT activity_time, realm, mac, cui, result, ap_id FROM activity WHERE operatorname = ? $conditional1 ORDER BY activity_time $conditional2", "s", $opName );
333
        return mysqli_fetch_all($stats, \MYSQLI_ASSOC);
334
    }
335
        
336
    /**
337
     * initialises a new SP
338
     * 
339
     * @return array details of the SP as generated during initialisation
340
     * @throws Exception
341
     */
342
    private function initialise()
343
    {
344
        // find out where the admin is located approximately
345
        $ourLocation = ['lon' => 0, 'lat' => 0];
346
        $geoip = DeviceLocation::locateDevice();
347
        if ($geoip['status'] == 'ok') {
348
            $ourLocation = ['lon' => $geoip['geo']['lon'], 'lat' => $geoip['geo']['lat']];
349
        }
350
        $inst = new IdP($this->institution);
351
        $ourserver = $this->findGoodServerLocation($ourLocation, $inst->federation, []);
352
        // now, find an unused port in the preferred server
353
        $foundFreePort1 = 0;
354
        while ($foundFreePort1 == 0) {
355
            $portCandidate = random_int(1200, 65535);
356
            $check = $this->databaseHandle->exec("SELECT port_instance_1 FROM deployment WHERE radius_instance_1 = ? AND port_instance_1 = ?", "si", $ourserver, $portCandidate);
357
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $check) == 0) {
358
                $foundFreePort1 = $portCandidate;
359
            }
360
        }
361
        $ourSecondServer = $this->findGoodServerLocation($ourLocation, $inst->federation, [$ourserver]);
362
        $foundFreePort2 = 0;
363
        while ($foundFreePort2 == 0) {
364
            $portCandidate = random_int(1200, 65535);
365
            $check = $this->databaseHandle->exec("SELECT port_instance_2 FROM deployment WHERE radius_instance_2 = ? AND port_instance_2 = ?", "si", $ourSecondServer, $portCandidate);
366
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $check) == 0) {
367
                $foundFreePort2 = $portCandidate;
368
            }
369
        }
370
        // and make up a shared secret that is halfways readable
371
        $futureSecret = $this->randomString(16, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
372
        $cons = $this->consortium;
373
        $id = $this->identifier;
374
        $this->databaseHandle->exec("UPDATE deployment SET radius_instance_1 = ?, radius_instance_2 = ?, port_instance_1 = ?, port_instance_2 = ?, secret = ?, consortium = ? WHERE deployment_id = ?", "ssiissi", $ourserver, $ourSecondServer, $foundFreePort1, $foundFreePort2, $futureSecret, $cons, $id);
375
        return ["port_instance_1" => $foundFreePort1, "port_instance_2" => $foundFreePort2, "secret" => $futureSecret, "radius_instance_1" => $ourserver, "radius_instance_2" => $ourSecondServer];
376
    }
377
378
    /**
379
     * update the last_changed timestamp for this deployment
380
     * 
381
     * @return void
382
     */
383
    public function updateFreshness()
384
    {
385
        $id = $this->identifier;
386
        $this->databaseHandle->exec("UPDATE deployment SET last_change = CURRENT_TIMESTAMP WHERE deployment_id = ?", "i", $id);
387
    }
388
389
    /**
390
     * gets the last-modified timestamp (useful for caching "dirty" check)
391
     * 
392
     * @return string the date in string form, as returned by SQL
393
     */
394
    public function getFreshness()
395
    {
396
        $id = $this->identifier;
397
        $execLastChange = $this->databaseHandle->exec("SELECT last_change FROM deployment WHERE deployment_id = ?", "i", $id);
398
        // SELECT always returns a resource, never a boolean
399
        if ($freshnessQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $execLastChange)) {
400
            return $freshnessQuery->last_change;
401
        }
402
    }
403
404
    /**
405
     * Deletes the deployment from database
406
     * 
407
     * @return void
408
     */
409
    public function remove()
410
    {
411
        $id = $this->identifier;
412
        $this->databaseHandle->exec("DELETE FROM deployment_option WHERE deployment_id = ?", "i", $id);
413
        $this->databaseHandle->exec("DELETE FROM deployment WHERE deployment_id = ?", "i", $id);
414
    }
415
416
    /**
417
     * marks the deployment as deactivated 
418
     * 
419
     * @return void
420
     */
421
    public function deactivate()
422
    {
423
        $id = $this->identifier;
424
        $inactive = DeploymentManaged::INACTIVE;
425
        $this->databaseHandle->exec("UPDATE deployment SET status = ? WHERE deployment_id = ?", "ii", $inactive, $id);
426
    }
427
    
428
    /**
429
     * marks the deployment as active.
430
     * 
431
     * @return void
432
     */
433
    public function activate()
434
    {
435
        $id = $this->identifier;
436
        $active = DeploymentManaged::ACTIVE;
437
        $this->databaseHandle->exec("UPDATE deployment SET status = ? WHERE deployment_id = ?", "ii", $active, $id);
438
    }
439
440
    /**
441
     * determines the Operator-Name attribute content to use in the RADIUS config
442
     * 
443
     * @return string
444
     */
445
    public function getOperatorName()
446
    {
447
        $customAttrib = $this->getAttributes("managedsp:operatorname");
448
        if (count($customAttrib) == 0) {
449
            return "1sp." . $this->identifier . "-" . $this->institution . \config\ConfAssistant::SILVERBULLET['realm_suffix'];
450
        }
451
        return $customAttrib[0]["value"];
452
    }
453
454
    /**
455
     * send request to RADIUS configuration daemom
456
     *
457
     * @param  integer $idx  server index 1 (primary) or 2 (backup)
458
     * @param  string  $post string to POST 
459
     * @return string  OK or FAILURE
460
     */
461
    private function sendToRADIUS(int $idx, $post)
462
    {
463
464
        $hostname = "radius_hostname_$idx";
465
        $ch = curl_init("http://" . $this->$hostname);
466
        if ($ch === FALSE) {
467
            $res = 'FAILURE';
468
        } else {
469
            curl_setopt($ch, CURLOPT_POST, 1);
470
            curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
471
            $this->loggerInstance->debug(1, "Posting to http://" . $this->$hostname . ": $post\n");
472
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
473
            curl_setopt($ch, CURLOPT_HEADER, 0);
474
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
475
            $exec = curl_exec($ch);
476
            if (!is_string($exec)) {
477
                $this->loggerInstance->debug(1, "curl_exec failure");
478
                $res = 'FAILURE';
479
            } else {
480
                $res = $exec;
481
            }
482
            curl_close($ch);
483
            $this->loggerInstance->debug(1, "Response from FR configurator: $res\n");
484
            $this->loggerInstance->debug(1, $this);
485
        }
486
        $this->loggerInstance->debug(1, "Database update");
487
        $id = $this->identifier;
488
        $resValue = ($res == 'OK' ? \core\AbstractDeployment::RADIUS_OK : \core\AbstractDeployment::RADIUS_FAILURE);
489
        $this->databaseHandle->exec("UPDATE deployment SET radius_status_$idx = ? WHERE deployment_id = ?", "ii", $resValue, $id);
490
        return $res;
491
    }
492
493
    /**
494
     * prepare and send email message to support mail
495
     *
496
     * @param  int    $remove   the flag indicating remove request
497
     * @param  array  $response setRADIUSconfig result
498
     * @param  string $status   the flag indicating status (FAILURE or OK)
499
     * @return void
500
     * 
501
     */
502
    private function sendMailtoAdmin($remove, $response, $status)
503
    {
504
        $txt = '';
505
        if ($status == 'OK') {
506
            $txt = $remove ? _('Profile deactivation succeeded') : _('Profile activation/modification succeeded');
507
        } else {
508
            $txt = $remove ? _('Profile deactivation failed') : _('Profile activation/modification failed');
509
        }
510
        $txt = $txt . ' ';
511
        if (array_count_values($response)[$status] == 2) {
512
            $txt = $txt . _('on both RADIUS servers: primary and backup') . '.';
513
        } else {
514
            if ($response['res[1]'] == $status) {
515
                $txt = $txt . _('on primary RADIUS server') . '.';
516
            } else {
517
                $txt = $txt . _('on backup RADIUS server') . '.';
518
            }
519
        }
520
        $mail = \core\common\OutsideComm::mailHandle();
521
        $email = $this->getAttributes("support:email")[0]['value'];
522
        $mail->FromName = \config\Master::APPEARANCE['productname'] . " Notification System";
523
        $mail->addAddress($email);
524
        if ($status == 'OK') {
525
            $mail->Subject = _('RADIUS profile update problem fixed');
526
        } else {
527
            $mail->Subject = _('RADIUS profile update problem');
528
        }
529
        $mail->Body = $txt;
530
        $sent = $mail->send();
531
        if ($sent === FALSE) {
532
            $this->loggerInstance->debug(1, 'Mailing on RADIUS problem failed');
533
        }
534
    }
535
536
    /**
537
     * check if URL responds with 200
538
     *
539
     * @param integer $idx server index 1 (primary) or 2 (backup)
540
     * @return integer or NULL
541
     */
542
    private function checkURL($idx)
543
    {
544
        $ch = curl_init();
545
        if ($ch === FALSE) {
546
            return NULL;
547
        }
548
        if ($idx == 1) {
549
            $host = $this->radius_hostname_1;
550
        } elseif ($idx == 2) {
551
            $host = $this->radius_hostname_2;
552
        } else {
553
            return NULL;
554
        }
555
        $timeout = 10;
556
        curl_setopt($ch, CURLOPT_URL, 'http://' . $host);
557
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
558
        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
559
        curl_exec($ch);
560
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
561
        curl_close($ch);
562
        if ($http_code == 200) {
563
            return 1;
564
        }
565
        return 0;
566
    }
567
568
    /**
569
     * check whether the configured RADIUS hosts actually exist
570
     * 
571
     * @param integer $idx server index 1 (primary) or 2 (backup)
572
     * @return integer or NULL
573
     */
574
    private function testRADIUSHost($idx)
575
    {
576
        if ($idx == 1) {
577
            $host = $this->radius_hostname_1;
578
        } elseif ($idx == 2) {
579
            $host = $this->radius_hostname_2;
580
        } else {
581
            return NULL;
582
        }
583
        $statusServer = new diag\RFC5997Tests($host, \config\Diagnostics::RADIUSSPTEST['port'], \config\Diagnostics::RADIUSSPTEST['secret']);
584
        $this->loggerInstance->debug(1, $statusServer);
585
        if ($statusServer->statusServerCheck() === diag\AbstractTest::RETVAL_OK) {
586
            return 1;
587
        }
588
        return 0;
589
    }
590
591
    /**
592
     * get institution realms
593
     * 
594
     * @return array of strings
595
     */
596
    private function getAllRealms()
597
    {
598
        $idp = new IdP($this->institution);
599
        $allProfiles = $idp->listProfiles(TRUE);
600
        $allRealms = [];
601
        if (($this->getAttributes("managedsp:realmforvlan") ?? NULL)) {
602
            $allRealms = array_values(array_unique(array_column($this->getAttributes("managedsp:realmforvlan"), "value")));
603
        }
604
        foreach ($allProfiles as $profile) {
605
            if ($realm = ($profile->getAttributes("internal:realm")[0]['value'] ?? NULL)) {
606
                if (!in_array($realm, $allRealms)) {
607
                    $allRealms[] = $realm;
608
                }
609
            }
610
        }
611
        return $allRealms;
612
    }
613
614
    /**
615
     * check if RADIUS configuration daemon is listening for requests
616
     *
617
     * @return array index res[1] indicate primary RADIUS status, index res[2] backup RADIUS status
618
     */
619
    public function checkRADIUSHostandConfigDaemon()
620
    {
621
        $res = array();
622
        if ($this->radius_status_1 == \core\AbstractDeployment::RADIUS_FAILURE) {
623
            $res[1] = $this->checkURL(1);
624
            if ($res[1]) {
625
                $res[1] = $this->testRADIUSHost(1);
626
            }
627
        }
628
        if ($this->radius_status_2 == \core\AbstractDeployment::RADIUS_FAILURE) {
629
            $res[2] = $this->checkURL(2);
630
            if ($res[2]) {
631
                $res[2] = $this->testRADIUSHost(2);
632
            }
633
        }
634
        return $res;
635
    }
636
637
    /**
638
     * prepare request to add/modify RADIUS settings for given deployment
639
     *
640
     * @param int $onlyone the flag indicating on which server to conduct modifications
641
     * @param int $notify  the flag indicating that an email notification should be sent
642
     * @return array index res[1] indicate primary RADIUS status, index res[2] backup RADIUS status
643
     */
644
    public function setRADIUSconfig($onlyone = 0, $notify = 0)
645
    {
646
        $remove = ($this->status == \core\AbstractDeployment::INACTIVE) ? 0 : 1;
647
        $toPost = ($onlyone ? array($onlyone => '') : array(1 => '', 2 => ''));
648
        $toPostTemplate = 'instid=' . $this->institution . '&deploymentid=' . $this->identifier . '&secret=' . $this->secret . '&country=' . $this->getAttributes("internal:country")[0]['value'] . '&';
649
        if ($remove) {
650
            $toPostTemplate = $toPostTemplate . 'remove=1&';
651
        } else {
652
            $toPostTemplate = $toPostTemplate . 'operatorname=' . $this->getOperatorName() . '&'; 
653
            if ($this->getAttributes("managedsp:vlan")[0]['value'] ?? NULL) {
654
                $allRealms = $this->getAllRealms();
655
                if (!empty($allRealms)) {
656
                    $toPostTemplate = $toPostTemplate . 'vlan=' . $this->getAttributes("managedsp:vlan")[0]['value'] . '&';
657
                    $toPostTemplate = $toPostTemplate . 'realmforvlan[]=' . implode('&realmforvlan[]=', $allRealms) . '&';
658
                }
659
            }
660
        }
661
        foreach (array_keys($toPost) as $key) {
662
            $elem = 'port' . $key;
663
            $toPost[$key] = $toPostTemplate . 'port=' . $this->$elem;
664
        }
665
        $response = array();
666
        foreach ($toPost as $key => $value) {
667
            $this->loggerInstance->debug(1, 'toPost ' . $toPost[$key] . "\n");
668
            $response['res[' . $key . ']'] = $this->sendToRADIUS($key, $toPost[$key]);
669
        }
670
        if ($onlyone) {
671
            $response['res[' . ($onlyone == 1) ? 2 : 1 . ']'] = \core\AbstractDeployment::RADIUS_OK;
672
        }
673
        foreach (array('OK', 'FAILURE') as $status) {
674
            if ( ( ($status == 'OK' && $notify) || ($status == 'FAILURE') ) && ( in_array($status, $response) ) ) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($status == 'OK' && $not...ray($status, $response), Probably Intended Meaning: $status == 'OK' && $noti...ay($status, $response))
Loading history...
675
                $this->sendMailtoAdmin($remove, $response, $status);
676
            }
677
        }
678
        return $response;
679
    }
680
}
681