Passed
Push — master ( 154e45...5720cd )
by Maja
13:53
created

DeploymentManaged::getRADIUSLogs()   F

Complexity

Conditions 17
Paths 1792

Size

Total Lines 68
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 52
c 0
b 0
f 0
dl 0
loc 68
rs 1.0499
cc 17
nop 2
nc 1792

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
/**
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
 * @author Maja Górecka-Wolniewicz <[email protected]>
30
 *
31
 * @package Developer
32
 *
33
 */
34
35
namespace core;
36
37
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...
38
39
/**
40
 * This class represents an EAP Profile.
41
 * Profiles can inherit attributes from their IdP, if the IdP has some. Otherwise,
42
 * one can set attribute in the Profile directly. If there is a conflict between
43
 * IdP-wide and Profile-wide attributes, the more specific ones (i.e. Profile) win.
44
 * 
45
 * @author Stefan Winter <[email protected]>
46
 * @author Tomasz Wolniewicz <[email protected]>
47
 *
48
 * @license see LICENSE file in root directory
49
 *
50
 * @package Developer
51
 */
52
class DeploymentManaged extends AbstractDeployment
53
{
54
55
    /**
56
     * This is the limit for dual-stack hosts. Single stack uses half of the FDs
57
     * in FreeRADIUS and take twice as many. initialise() takes this into
58
     * account.
59
     */
60
    const MAX_CLIENTS_PER_SERVER = 200;
61
    const PRODUCTNAME = "Managed SP";
62
63
    /**
64
     * the primary RADIUS server port for this SP instance
65
     * 
66
     * @var integer
67
     */
68
    public $port1;
69
70
    /**
71
     * the backup RADIUS server port for this SP instance
72
     * 
73
     * @var integer
74
     */
75
    public $port2;
76
77
    /**
78
     * the shared secret for this SP instance
79
     * 
80
     * @var string
81
     */
82
    public $secret;
83
84
    /**
85
     * the IPv4 address of the primary RADIUS server for this SP instance 
86
     * (can be NULL)
87
     * 
88
     * @var string
89
     */
90
    public $host1_v4;
91
92
    /**
93
     * the IPv6 address of the primary RADIUS server for this SP instance 
94
     * (can be NULL)
95
     * 
96
     * @var string
97
     */
98
    public $host1_v6;
99
100
    /**
101
     * the IPv4 address of the backup RADIUS server for this SP instance 
102
     * (can be NULL)
103
     * 
104
     * @var string
105
     */
106
    public $host2_v4;
107
108
    /**
109
     * the IPv6 address of the backup RADIUS server for this SP instance 
110
     * (can be NULL)
111
     * 
112
     * @var string
113
     */
114
    public $host2_v6;
115
116
    /**
117
     * the primary RADIUS server instance for this SP instance
118
     * 
119
     * @var string
120
     */
121
    public $radius_instance_1;
122
123
    /**
124
     * the backup RADIUS server instance for this SP instance
125
     * 
126
     * @var string
127
     */
128
    public $radius_instance_2;
129
130
    /**
131
     * the primary RADIUS server hostname - for sending configuration requests
132
     * 
133
     * @var string
134
     */
135
    public $radius_hostname_1;
136
137
    /**
138
     * the backup RADIUS server hostname - for sending configuration requests
139
     * 
140
     * @var string
141
     */
142
    public $radius_hostname_2;
143
144
    /**
145
     * the primary RADIUS server status - last configuration request result
146
     * 
147
     * @var string
148
     */
149
    public $radius_status_1;
150
151
    /**
152
     * the backup RADIUS server status - last configuration request result
153
     * 
154
     * @var string
155
     */
156
    public $radius_status_2;
157
    
158
    /**
159
     * the client private key
160
     * 
161
     * @var string
162
     */
163
    public $radsec_priv;
164
    
165
    /**
166
     * the client certificate
167
     * 
168
     * @var string
169
     */
170
    public $radsec_cert; 
171
    
172
    /**
173
     * the client certificate srial number 
174
     * 
175
     * @var string
176
     */
177
    public $radsec_cert_serial_no;
178
    
179
    /**
180
     * the TLS-PSK key
181
     * 
182
     * @var string
183
     */
184
    public $pskkey;
185
    
186
    /**
187
     * the consortium this deployment is attached to
188
     * 
189
     * @var string
190
     */
191
    public $consortium;
192
193
    /**
194
     * the T&C of this service
195
     */
196
    public $termsAndConditions;
197
    
198
    /**
199
     * Class constructor for existing deployments (use 
200
     * IdP::newDeployment() to actually create one). Retrieves all 
201
     * attributes from the DB and stores them in the priv_ arrays.
202
     * 
203
     * @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.
204
     * @param string|int $deploymentIdRaw identifier of the deployment in the DB
205
     * @param string     $consortium      identifier of the consortium to attach to (only relevant when initialising a new deployment for the first time
206
     * @throws Exception
207
     */
208
    public function __construct($idpObject, $deploymentIdRaw, $consortium = 'eduroam')
209
    {
210
        parent::__construct($idpObject, $deploymentIdRaw); // we now have access to our INST database handle and logging
211
        $this->entityOptionTable = "deployment_option";
212
        $this->entityIdColumn = "deployment_id";
213
        $this->type = AbstractDeployment::DEPLOYMENTTYPE_MANAGED;
214
        if (!is_numeric($deploymentIdRaw)) {
215
            throw new Exception("Managed SP instances have to have a numeric identifier");
216
        }
217
        $propertyQuery = "SELECT consortium,status,port_instance_1,port_instance_2,secret,radius_instance_1,radius_instance_2,radius_status_1,radius_status_2,radsec_priv,radsec_cert,pskkey FROM deployment WHERE deployment_id = ?";
218
        $queryExec = $this->databaseHandle->exec($propertyQuery, "i", $deploymentIdRaw);
219
        if (mysqli_num_rows(/** @scrutinizer ignore-type */ $queryExec) == 0) {
220
            throw new Exception("Attempt to construct an unknown DeploymentManaged!");
221
        }
222
        
223
        $this->identifier = $deploymentIdRaw;
224
        while ($iterator = mysqli_fetch_object(/** @scrutinizer ignore-type */ $queryExec)) {
225
            if ($iterator->secret == NULL && $iterator->radius_instance_1 == NULL) {
226
                // we are instantiated for the first time, or all previous init attempts failed - so initialise us
227
                // first time: note the consortium, permanently
228
                if ($iterator->consortium === NULL) {
229
                    $this->consortium = $consortium;
230
                    $consortiumQuery = "UPDATE deployment SET consortium = '$this->consortium' WHERE deployment_id = ?";
231
                    $this->databaseHandle->exec($consortiumQuery, "i", $deploymentIdRaw);
232
                }
233
                $details = $this->initialise();
234
                $this->port1 = $details["port_instance_1"];
235
                $this->port2 = $details["port_instance_2"];
236
                $this->secret = $details["secret"];
237
                $this->radius_instance_1 = $details["radius_instance_1"];
238
                $this->radius_instance_2 = $details["radius_instance_2"];
239
                $this->radius_status_1 = 1;
240
                $this->radius_status_2 = 1;
241
                $this->status = AbstractDeployment::INACTIVE;
242
            } else {
243
                $this->port1 = $iterator->port_instance_1;
244
                $this->port2 = $iterator->port_instance_2;
245
                $this->secret = $iterator->secret;
246
                $this->radius_instance_1 = $iterator->radius_instance_1;
247
                $this->radius_instance_2 = $iterator->radius_instance_2;
248
                $this->radius_status_1 = $iterator->radius_status_1;
249
                $this->radius_status_2 = $iterator->radius_status_2;
250
                $this->status = $iterator->status;
251
                $this->consortium = $iterator->consortium;
252
                $this->radsec_cert = $iterator->radsec_cert;
253
                $this->radsec_priv = $iterator->radsec_priv;
254
                $this->pskkey = $iterator->pskkey;
255
            }
256
        }
257
        $server1 = $this->radius_instance_1;
258
        $server1details = $this->databaseHandle->exec("SELECT mgmt_hostname, radius_ip4, radius_ip6 FROM managed_sp_servers WHERE server_id = ?", "s", $server1);
259
        while ($iterator2 = mysqli_fetch_object(/** @scrutinizer ignore-type */ $server1details)) {
260
            $this->host1_v4 = $iterator2->radius_ip4;
261
            $this->host1_v6 = $iterator2->radius_ip6;
262
            $this->radius_hostname_1 = $iterator2->mgmt_hostname;
263
        }
264
        $server2 = $this->radius_instance_2;
265
        $server2details = $this->databaseHandle->exec("SELECT mgmt_hostname, radius_ip4, radius_ip6 FROM managed_sp_servers WHERE server_id = ?", "s", $server2);
266
        while ($iterator3 = mysqli_fetch_object(/** @scrutinizer ignore-type */ $server2details)) {
267
            $this->host2_v4 = $iterator3->radius_ip4;
268
            $this->host2_v6 = $iterator3->radius_ip6;
269
            $this->radius_hostname_2 = $iterator3->mgmt_hostname;
270
        }
271
        $thisLevelAttributes = $this->retrieveOptionsFromDatabase("SELECT DISTINCT option_name, option_lang, option_value, row_id 
272
                                            FROM $this->entityOptionTable
273
                                            WHERE $this->entityIdColumn = ?  
274
                                            ORDER BY option_name", "Profile");
275
        $tempAttribMergedIdP = $this->levelPrecedenceAttributeJoin($thisLevelAttributes, $this->idpAttributes, "IdP");
276
        $this->attributes = $this->levelPrecedenceAttributeJoin($tempAttribMergedIdP, $this->fedAttributes, "FED");
277
        
278
        $this->termsAndConditions = "<h2>Product Definition</h2>
279
<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>
280
<p>The service includes:</p>
281
<ul><li>web-based user management interface where eduroam SP deployment details can be created and deleted;</li>
282
    <li>web-based institution management interface where institutions are enabled or disabled to use the service;</li>
283
    <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>
284
</ul>
285
<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>
286
<h2>Terms of Use</h2>
287
<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).";
288
    }
289
290
    /**
291
     * finds a suitable server which is geographically close to the admin
292
     * 
293
     * @param array  $adminLocation      the current geographic position of the admin
294
     * @param string $federation         the federation this deployment belongs to
295
     * @param array  $blacklistedServers list of server to IGNORE
296
     * @return string the server ID
297
     * @throws Exception
298
     */
299
    private function findGoodServerLocation($adminLocation, $federation, $blacklistedServers)
300
    {
301
        // find a server near him (list of all servers with capacity, ordered by distance)
302
        // first, if there is a pool of servers specifically for this federation, prefer it
303
        // only check the consortium pool group we want to attach to
304
        $cons = $this->consortium;
305
        $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);
306
        $serverCandidates = [];
307
        while ($iterator = mysqli_fetch_object(/** @scrutinizer ignore-type */ $servers)) {
308
            $maxSupportedClients = DeploymentManaged::MAX_CLIENTS_PER_SERVER;
309
            if ($iterator->radius_ip4 == NULL || $iterator->radius_ip6 == NULL) {
310
                // half the amount of IP stacks means half the amount of FDs in use, so we can take twice as many
311
                $maxSupportedClients = $maxSupportedClients * 2;
312
            }
313
            $serverId = $iterator->server_id;
314
            $clientCount1 = $this->databaseHandle->exec("SELECT port_instance_1 AS tenants1 FROM deployment WHERE radius_instance_1 = ?", "s", $serverId);
315
            $clientCount2 = $this->databaseHandle->exec("SELECT port_instance_2 AS tenants2 FROM deployment WHERE radius_instance_2 = ?", "s", $serverId);
316
317
            $clients = $clientCount1->num_rows + $clientCount2->num_rows;
318
            if (in_array($iterator->server_id, $blacklistedServers)) {
319
                continue;
320
            }
321
            if ($clients < $maxSupportedClients) {
322
                $serverCandidates[IdPlist::geoDistance($adminLocation, ['lat' => $iterator->location_lat, 'lon' => $iterator->location_lon])] = $iterator->server_id;
323
            }
324
            if ($clients > $maxSupportedClients * 0.9) {
325
                $this->loggerInstance->debug(1, "A RADIUS server for Managed SP (" . $iterator->server_id . ") is serving at more than 90% capacity!");
326
            }
327
        }
328
        if (count($serverCandidates) == 0 && $federation != "DEFAULT") {
329
            // we look in the default pool instead
330
            // recursivity! Isn't that cool!
331
            return $this->findGoodServerLocation($adminLocation, "DEFAULT", $blacklistedServers);
332
        }
333
        if (count($serverCandidates) == 0) {
334
            throw new Exception("No available server found for new SP in $federation!");
335
        }
336
        // put the nearest server on top of the list
337
        ksort($serverCandidates);
338
        $this->loggerInstance->debug(1, $serverCandidates);
339
        return array_shift($serverCandidates);
340
    }
341
342
    /**
343
     * create unique serialNumber for a TLS client certificate
344
     * 
345
     * @throws Exception
346
     */
347
    private function setTLSSerialNumber($max=PHP_INT_MAX) {
348
        $nonDupSerialFound = FALSE;
349
        do {
350
            $serial = random_int(1000000000, $max);
351
            $dupeQuery = $this->databaseHandle->exec("SELECT radsec_cert_serial_number FROM deployment WHERE radsec_cert_serial_number = ?", "i", $serial);
352
            // SELECT -> resource, not boolean
353
            if (mysqli_num_rows(/** @scrutinizer ignore-type */$dupeQuery) == 0) {
354
                $nonDupSerialFound = TRUE;
355
            }
356
        } while (!$nonDupSerialFound);
357
        $this->radsec_cert_serial_no = $serial;
358
    }
359
    
360
    /**
361
     * create TLS credentials for client
362
     * 
363
     * @throws Exception
364
     */
365
    private function createTLScredentials()
366
    {
367
        $clientName = 'SP' . $this->identifier . '-' . $this->institution;
368
        $dn = array(
369
                    "organizationName" => "eduroam",
370
                    "organizationalUnitName" => "eduroam Managed SP",
371
                    "commonName" => $clientName
372
        );
373
        // Generate a new private (and public) key pair
374
        $privkey = openssl_pkey_new(array(
375
                                          "private_key_bits" => 4096,
376
                                          "private_key_type" => OPENSSL_KEYTYPE_RSA));
377
        // export private key to $clientprivateKey (as string)
378
        openssl_pkey_export($privkey, $this->radsec_priv);
379
        // Generate a certificate signing request
380
        $csr = openssl_csr_new($dn, $privkey,
0 ignored issues
show
Bug introduced by
It seems like $privkey can also be of type resource; however, parameter $private_key of openssl_csr_new() does only seem to accept OpenSSLAsymmetricKey, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

380
        $csr = openssl_csr_new($dn, /** @scrutinizer ignore-type */ $privkey,
Loading history...
381
                               array('digest_alg' => 'sha256', 'config' => ROOT . "/config/ManagedSPCerts/openssl.cnf"));
382
        // get CA certificate and private key
383
        $caprivkey = array(file_get_contents(ROOT . "/config/ManagedSPCerts/eduroamSP-CA.key"),
384
                            \config\Master::MANAGEDSP['capass']);
0 ignored issues
show
Bug introduced by
The constant config\Master::MANAGEDSP was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
385
        $cacert = file_get_contents(ROOT .  "/config/ManagedSPCerts/eduroamSP-CA.pem");
386
        $this->setTLSSerialNumber();
387
        $clientcert = openssl_csr_sign($csr, $cacert, $caprivkey, \config\Master::MANAGEDSP['daystoexpiry'],
388
                          array('digest_alg'=>'sha512', 'config' => ROOT . "/config/ManagedSPCerts/openssl.cnf"), $this->radsec_cert_serial_no);
0 ignored issues
show
Bug introduced by
$this->radsec_cert_serial_no of type string is incompatible with the type integer expected by parameter $serial of openssl_csr_sign(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

388
                          array('digest_alg'=>'sha512', 'config' => ROOT . "/config/ManagedSPCerts/openssl.cnf"), /** @scrutinizer ignore-type */ $this->radsec_cert_serial_no);
Loading history...
389
        openssl_x509_export($clientcert, $this->radsec_cert);
390
    } 
391
    /**
392
     * retrieves usage statistics for the deployment
393
     * 
394
     * @param int $backlog how many seconds back in time should we look
395
     * @param int $limit   max number of entries to retrieve (if supplied, backlog will be ignored)
396
     * @return array of arrays: activity_time, realm, mac, result
397
     * @throws Exception
398
     */
399
    public function retrieveStatistics(int $backlog, int $limit = 0)
400
    {
401
        // find a server near him (list of all servers with capacity, ordered by distance)
402
        // first, if there is a pool of servers specifically for this federation, prefer it
403
        // only check the consortium pool group we want to attach to
404
        // TODO: if we also collect stats from OpenRoaming hosts, differentiate the logs!
405
        $opName = $this->getOperatorName();
0 ignored issues
show
Unused Code introduced by
The assignment to $opName is dead and can be removed.
Loading history...
406
        if ($limit !== 0) {
407
            $conditional1 = "";
408
            $conditional2 = "DESC LIMIT $limit";
409
        } else {
410
            $conditional1 = "AND activity_time > DATE_SUB(NOW(), INTERVAL $backlog SECOND )";
411
            $conditional2 = "DESC";
412
        }
413
        $client = 'SP' . $this->identifier . '-' . $this->institution;
414
        $stats = $this->databaseHandle->exec("SELECT activity_time, realm, mac, cui, result, ap_id, prot, outer_user FROM activity WHERE owner = ? $conditional1 ORDER BY activity_time $conditional2", "s", $client );
415
       
416
        return mysqli_fetch_all($stats, \MYSQLI_ASSOC);
417
    }
418
        
419
    /**
420
     * initialises a new SP
421
     * 
422
     * @return array details of the SP as generated during initialisation
423
     * @throws Exception
424
     */
425
    private function initialise()
426
    {
427
        // find out where the admin is located approximately
428
        $ourLocation = ['lon' => 0, 'lat' => 0];
429
        $geoip = DeviceLocation::locateDevice();
430
        if ($geoip['status'] == 'ok') {
431
            $ourLocation = ['lon' => $geoip['geo']['lon'], 'lat' => $geoip['geo']['lat']];
432
        }
433
        $inst = new IdP($this->institution);
434
        $ourserver = $this->findGoodServerLocation($ourLocation, $inst->federation, []);
435
        // now, find an unused port in the preferred server
436
        $foundFreePort1 = 0;
437
        while ($foundFreePort1 == 0) {
438
            $portCandidate = random_int(1200, 65535);
439
            $check = $this->databaseHandle->exec("SELECT port_instance_1 FROM deployment WHERE radius_instance_1 = ? AND port_instance_1 = ?", "si", $ourserver, $portCandidate);
440
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $check) == 0) {
441
                $foundFreePort1 = $portCandidate;
442
            }
443
        }
444
        $ourSecondServer = $this->findGoodServerLocation($ourLocation, $inst->federation, [$ourserver]);
445
        $foundFreePort2 = 0;
446
        while ($foundFreePort2 == 0) {
447
            $portCandidate = random_int(1200, 65535);
448
            $check = $this->databaseHandle->exec("SELECT port_instance_2 FROM deployment WHERE radius_instance_2 = ? AND port_instance_2 = ?", "si", $ourSecondServer, $portCandidate);
449
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $check) == 0) {
450
                $foundFreePort2 = $portCandidate;
451
            }
452
        }
453
        // and make up a shared secret that is halfways readable
454
        $futureSecret = trim(chunk_split(bin2hex(openssl_random_pseudo_bytes(14)), 4, '-'), '-');
455
        $this->createTLScredentials();
456
        $futurePSKkey = bin2hex(openssl_random_pseudo_bytes(32));
457
        $cons = $this->consortium;
458
        $id = $this->identifier;
459
        
460
        $this->databaseHandle->exec("UPDATE deployment SET radius_instance_1 = ?, radius_instance_2 = ?, port_instance_1 = ?, port_instance_2 = ?, secret = ?, radsec_priv = ?, radsec_cert = ?, radsec_cert_serial_number = ?, pskkey = ?, consortium = ? WHERE deployment_id = ?", "ssiisssissi", $ourserver, $ourSecondServer, $foundFreePort1, $foundFreePort2, $futureSecret, $this->radsec_priv, $this->radsec_cert, $this->radsec_cert_serial_no, $futurePSKkey, $cons, $id);
461
        return ["port_instance_1" => $foundFreePort1, "port_instance_2" => $foundFreePort2, "secret" => $futureSecret, "radius_instance_1" => $ourserver, "radius_instance_2" => $ourSecondServer, "pskkey" => $futurePSKkey];
462
    }
463
464
    /**
465
     * update the last_changed timestamp for this deployment
466
     * 
467
     * @return void
468
     */
469
    public function updateFreshness()
470
    {
471
        $id = $this->identifier;
472
        $this->databaseHandle->exec("UPDATE deployment SET last_change = CURRENT_TIMESTAMP WHERE deployment_id = ?", "i", $id);
473
    }
474
475
    /**
476
     * gets the last-modified timestamp (useful for caching "dirty" check)
477
     * 
478
     * @return string the date in string form, as returned by SQL
479
     */
480
    public function getFreshness()
481
    {
482
        $id = $this->identifier;
483
        $execLastChange = $this->databaseHandle->exec("SELECT last_change FROM deployment WHERE deployment_id = ?", "i", $id);
484
        // SELECT always returns a resource, never a boolean
485
        if ($freshnessQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $execLastChange)) {
486
            return $freshnessQuery->last_change;
487
        }
488
    }
489
490
    /**
491
     * Deletes the deployment from database
492
     * 
493
     * @return void
494
     */
495
    public function remove()
496
    {
497
        $id = $this->identifier;
498
        $this->databaseHandle->exec("DELETE FROM deployment_option WHERE deployment_id = ?", "i", $id);
499
        $this->databaseHandle->exec("DELETE FROM deployment WHERE deployment_id = ?", "i", $id);
500
    }
501
    
502
    /**
503
     * Renews the deployment TLS credentials
504
     * 
505
     * @return void
506
     */
507
    public function renewtls()
508
    {
509
       $id = $this->identifier;
510
       $futureTlsClient = $this->createTLScredentials();
0 ignored issues
show
Unused Code introduced by
The assignment to $futureTlsClient is dead and can be removed.
Loading history...
Bug introduced by
Are you sure the assignment to $futureTlsClient is correct as $this->createTLScredentials() targeting core\DeploymentManaged::createTLScredentials() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
511
       $this->databaseHandle->exec("UPDATE deployment SET radsec_priv = ?, radsec_cert = ?, radsec_cert_serial_number = ? WHERE deployment_id = ?", "ssii", $this->radsec_priv, $this->radsec_cert, $this->radsec_cert_serial_no, $id);           
512
    }
513
    
514
    /**
515
     * Create new deployment TLS credentials based on uploaded CSR
516
     * 
517
     * @return void
518
     */
519
    public function tlsfromcsr($csr)
520
    {
521
       $id = $this->identifier;
522
       $dn = array();
523
       $dn['rdnSequence'] = array();
524
       $dn['rdnSequence'][0] = array();
525
       $dn['rdnSequence'][0][] = array('type' => 'id-at-organizationName', 'value' => array());
526
       $dn['rdnSequence'][0][0]['value']['utf8String'] = 'eduroam';
527
       $dn['rdnSequence'][1] = array();
528
       $dn['rdnSequence'][1][] = array('type' => 'id-at-organizationalUnitName', 'value' => array());
529
       $dn['rdnSequence'][1][0]['value']['utf8String'] = 'eduroam Managed SP';
530
       $dn['rdnSequence'][2] = array();
531
       $dn['rdnSequence'][2][] = array('type' => 'id-at-commonName', 'value' => array());
532
       $dn['rdnSequence'][2][0]['value']['utf8String'] = 'SP' . $this->identifier . "-" . $this->institution;
533
       $csr->setDN($dn);
534
       $pemcakey = file_get_contents(ROOT . "/config/ManagedSPCerts/eduroamSP-CA.key");
535
       $cakey = \phpseclib3\Crypt\PublicKeyLoader::loadPrivateKey($pemcakey, \config\Master::MANAGEDSP['capass'] );
0 ignored issues
show
Bug introduced by
The constant config\Master::MANAGEDSP was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
536
       $pemca = file_get_contents(ROOT .  "/config/ManagedSPCerts/eduroamSP-CA.pem");
537
       $ca = new \phpseclib3\File\X509();
538
       $ca->loadX509($pemca);
539
       $ca->setPrivateKey($cakey);
540
       // Sign the updated request, producing the certificate.
541
       $x509 = new \phpseclib3\File\X509();
542
       $csr->setExtension('id-ce-keyUsage', ['digitalSignature', 'nonRepudiation', 'keyEncipherment']);
543
       $csr->setExtension('id-ce-extKeyUsage', ['id-kp-clientAuth']);
544
       $csr->setExtension('id-ce-basicConstraints', ['cA' => false], false);
545
       $x509->setEndDate('+' . \config\Master::MANAGEDSP['daystoexpiry'] . ' days');
546
       $this->setTLSSerialNumber(999999999999999999);
547
       $x509->setSerialNumber($this->radsec_cert_serial_no, 10);
548
       $cert = $x509->loadX509($x509->saveX509($x509->sign($ca, $csr)));
0 ignored issues
show
Bug introduced by
It seems like $x509->sign($ca, $csr) can also be of type false; however, parameter $cert of phpseclib3\File\X509::saveX509() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

548
       $cert = $x509->loadX509($x509->saveX509(/** @scrutinizer ignore-type */ $x509->sign($ca, $csr)));
Loading history...
549
       $this->radsec_cert = $x509->saveX509($cert);
550
       $this->radsec_priv = NULL;
551
       //$futureTlsClient = $this->createTLScredentials();
552
       $this->databaseHandle->exec("UPDATE deployment SET radsec_priv = NULL, radsec_cert = ?, radsec_cert_serial_number = ? WHERE deployment_id = ?", "sii", $this->radsec_cert, $this->radsec_cert_serial_no, $id);           
553
    }
554
    /**
555
     * marks the deployment as deactivated 
556
     * 
557
     * @return void
558
     */
559
    public function deactivate()
560
    {
561
        $id = $this->identifier;
562
        $inactive = DeploymentManaged::INACTIVE;
563
        $this->databaseHandle->exec("UPDATE deployment SET status = ? WHERE deployment_id = ?", "ii", $inactive, $id);
564
    }
565
    
566
    /**
567
     * marks the deployment as active.
568
     * 
569
     * @return void
570
     */
571
    public function activate()
572
    {
573
        $id = $this->identifier;
574
        $active = DeploymentManaged::ACTIVE;
575
        $this->databaseHandle->exec("UPDATE deployment SET status = ? WHERE deployment_id = ?", "ii", $active, $id);
576
    }
577
578
    /**
579
     * determines the Operator-Name attribute content to use in the RADIUS config
580
     * 
581
     * @return string
582
     */
583
    public function getOperatorName()
584
    {
585
        $customAttrib = $this->getAttributes("managedsp:operatorname");
586
        if (count($customAttrib) == 0) {
587
            return "1sp." . $this->identifier . "-" . $this->institution . \config\ConfAssistant::SILVERBULLET['realm_suffix'];
588
        }
589
        return $customAttrib[0]["value"];
590
    }
591
592
    /**
593
     * send request to RADIUS configuration daemom
594
     *
595
     * @param  integer $idx  server index 1 (primary) or 2 (backup)
596
     * @param  string  $post string to POST 
597
     * @return string  OK or FAILURE
598
     */
599
    private function sendToRADIUS(int $idx, $post)
600
    {
601
        $hostname = "radius_hostname_$idx";
602
        $ch = curl_init("http://" . $this->$hostname . ':' . \config\Master::MANAGEDSP['radiusconfigport']);
0 ignored issues
show
Bug introduced by
The constant config\Master::MANAGEDSP was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
603
        if ($ch === FALSE) {
604
            $res = 'FAILURE';
605
        } else {
606
            curl_setopt($ch, CURLOPT_USERAGENT, "CAT-ManagedSP");
607
            curl_setopt($ch, CURLOPT_POST, 1);
608
            curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
609
            $this->loggerInstance->debug(1, "Posting to http://" . $this->$hostname . ':' . \config\Master::MANAGEDSP['radiusconfigport'] . "/$post\n");
610
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
611
            curl_setopt($ch, CURLOPT_HEADER, 0);
612
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
613
            $exec = curl_exec($ch);
614
            if (!is_string($exec)) {
615
                $this->loggerInstance->debug(1, "curl_exec failure");
616
                $res = 'FAILURE';
617
            } else {
618
                $res = $exec;
619
            }
620
            curl_close($ch);
621
            $this->loggerInstance->debug(1, "Response from FR configurator: $res\n");
622
            $this->loggerInstance->debug(1, $this);
623
        }
624
        $this->loggerInstance->debug(1, "Database update");
625
        $id = $this->identifier;
626
        if ($res == 'OK' || $res == 'FAILURE') {
627
            $resValue = ($res == 'OK' ? \core\AbstractDeployment::RADIUS_OK : \core\AbstractDeployment::RADIUS_FAILURE);
628
            $this->databaseHandle->exec("UPDATE deployment SET radius_status_$idx = ? WHERE deployment_id = ?", "ii", $resValue, $id);
629
        }
630
        return $res;
631
    }
632
633
    /**
634
     * prepare and send email message to support mail
635
     *
636
     * @param  int    $remove   the flag indicating remove request
637
     * @param  array  $response setRADIUSconfig result
638
     * @param  string $status   the flag indicating status (FAILURE or OK)
639
     * @return void
640
     * 
641
     */
642
    private function sendMailtoAdmin($remove, $response, $status)
643
    {
644
        $txt = '';
645
        if ($status == 'OK') {
646
            $txt = $remove ? _('Profile deactivation succeeded') : _('Profile activation/modification succeeded');
647
        } else {
648
            $txt = $remove ? _('Profile deactivation failed') : _('Profile activation/modification failed');
649
        }
650
        $txt = $txt . ' ';
651
        if (array_count_values($response)[$status] == 2) {
652
            $txt = $txt . _('on both RADIUS servers: primary and backup') . '.';
653
        } else {
654
            if ($response['res[1]'] == $status) {
655
                $txt = $txt . _('on primary RADIUS server') . '.';
656
            } else {
657
                $txt = $txt . _('on backup RADIUS server') . '.';
658
            }
659
        }
660
        $mail = \core\common\OutsideComm::mailHandle();
661
        $email = $this->getAttributes("support:email")[0]['value'];
662
        $mail->FromName = \config\Master::APPEARANCE['productname'] . " Notification System";
663
        $mail->addAddress($email);
664
        if ($status == 'OK') {
665
            $mail->Subject = _('RADIUS profile update problem fixed');
666
        } else {
667
            $mail->Subject = _('RADIUS profile update problem');
668
        }
669
        $mail->Body = $txt;
670
        $sent = $mail->send();
671
        if ($sent === FALSE) {
672
            $this->loggerInstance->debug(1, 'Mailing on RADIUS problem failed');
673
        }
674
    }
675
676
    /**
677
     * check if URL responds with 200
678
     *
679
     * @param integer $idx server index 1 (primary) or 2 (backup)
680
     * @return integer or NULL
681
     */
682
    private function checkURL($idx)
683
    {
684
        $ch = curl_init();
685
        if ($ch === FALSE) {
686
            return NULL;
687
        }
688
        if ($idx == 1) {
689
            $host = $this->radius_hostname_1;
690
        } elseif ($idx == 2) {
691
            $host = $this->radius_hostname_2;
692
        } else {
693
            return NULL;
694
        }
695
        $timeout = 10;
696
        curl_setopt($ch, CURLOPT_URL, 'http://' . $host);
697
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
698
        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
699
        curl_exec($ch);
700
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
701
        curl_close($ch);
702
        if ($http_code == 200) {
703
            return 1;
704
        }
705
        return 0;
706
    }
707
708
    /**
709
     * check whether the configured RADIUS hosts actually exist
710
     * 
711
     * @param integer $idx server index 1 (primary) or 2 (backup)
712
     * @return integer or NULL
713
     */
714
    private function testRADIUSHost($idx)
715
    {
716
        if ($idx == 1) {
717
            $host = $this->radius_hostname_1;
718
        } elseif ($idx == 2) {
719
            $host = $this->radius_hostname_2;
720
        } else {
721
            return NULL;
722
        }
723
        $statusServer = new diag\RFC5997Tests($host, \config\Diagnostics::RADIUSSPTEST['port'], \config\Diagnostics::RADIUSSPTEST['secret']);
724
        $this->loggerInstance->debug(1, $statusServer);
725
        if ($statusServer->statusServerCheck() === diag\AbstractTest::RETVAL_OK) {
726
            return 1;
727
        }
728
        return 0;
729
    }
730
731
    /**
732
     * get institution realms
733
     * 
734
     * @return array of strings
735
     */
736
    private function getAllRealms()
737
    {
738
        $idp = new IdP($this->institution);
739
        $allProfiles = $idp->listProfiles(TRUE);
740
        $allRealms = [];
741
        if (($this->getAttributes("managedsp:realmforvlan") ?? NULL)) {
742
            $allRealms = array_values(array_unique(array_column($this->getAttributes("managedsp:realmforvlan"), "value")));
743
        }
744
        foreach ($allProfiles as $profile) {
745
            if ($realm = ($profile->getAttributes("internal:realm")[0]['value'] ?? NULL)) {
746
                if (!in_array($realm, $allRealms)) {
747
                    $allRealms[] = $realm;
748
                }
749
            }
750
        }
751
        return $allRealms;
752
    }
753
754
    /**
755
     * check if RADIUS configuration daemon is listening for requests
756
     *
757
     * @return array index res[1] indicate primary RADIUS status, index res[2] backup RADIUS status
758
     */
759
    public function checkRADIUSHostandConfigDaemon()
760
    {
761
        $res = array();
762
        if ($this->radius_status_1 == \core\AbstractDeployment::RADIUS_FAILURE) {
763
            $res[1] = $this->checkURL(1);
764
            if ($res[1]) {
765
                $res[1] = $this->testRADIUSHost(1);
766
            }
767
        }
768
        if ($this->radius_status_2 == \core\AbstractDeployment::RADIUS_FAILURE) {
769
            $res[2] = $this->checkURL(2);
770
            if ($res[2]) {
771
                $res[2] = $this->testRADIUSHost(2);
772
            }
773
        }
774
        return $res;
775
    }
776
777
    /**
778
     * prepare request to add/modify RADIUS settings for given deployment
779
     *
780
     * @param int $onlyone the flag indicating on which server to conduct modifications
781
     * @param int $notify  the flag indicating that an email notification should be sent
782
     * @return array index res[1] indicate primary RADIUS status, index res[2] backup RADIUS status
783
     */
784
    public function setRADIUSconfig($onlyone = 0, $notify = 0, $torevoke = "")
785
    {
786
        $toPost = ($onlyone ? array($onlyone => '') : array(1 => '', 2 => ''));
787
        if ($torevoke != '') {
788
            $toPostTemplate = 'instid=' . $this->institution . '&deploymentid=' . $this->identifier .
789
                    "&torevoke=$torevoke";
790
            foreach (array_keys($toPost) as $key) {
791
                $toPost[$key] = $toPostTemplate;
792
            }
793
        } else {
794
            $remove = ($this->status == \core\AbstractDeployment::INACTIVE) ? 0 : 1;
795
            $toPostTemplate = 'instid=' . $this->institution . '&deploymentid=' . $this->identifier . 
796
                '&secret=' . $this->secret .
797
                '&country=' . $this->getAttributes("internal:country")[0]['value'] .
798
                '&pskkey=' . $this->pskkey . '&';
799
            if ($remove) {
800
                $toPostTemplate = $toPostTemplate . 'remove=1&';
801
            } else {
802
                $toPostTemplate = $toPostTemplate . 'operatorname=' . $this->getOperatorName() . '&'; 
803
                if ($this->getAttributes("managedsp:vlan")[0]['value'] ?? NULL) {
804
                    $allRealms = $this->getAllRealms();
805
                    if (!empty($allRealms)) {
806
                        $toPostTemplate = $toPostTemplate . 'vlan=' . $this->getAttributes("managedsp:vlan")[0]['value'] . '&';
807
                        $toPostTemplate = $toPostTemplate . 'realmforvlan[]=' . implode('&realmforvlan[]=', $allRealms) . '&';
808
                    }
809
                }
810
                if ($this->getAttributes("managedsp:guest_vlan")[0]['value'] ?? NULL) {
811
                    $toPostTemplate = $toPostTemplate . 'guest_vlan=' . $this->getAttributes("managedsp:guest_vlan")[0]['value'] . '&';
812
                }
813
            }
814
            foreach (array_keys($toPost) as $key) {
815
                $elem = 'port' . $key;
816
                $toPost[$key] = $toPostTemplate . 'port=' . $this->$elem;
817
            }
818
        }
819
        $response = array();
820
        foreach ($toPost as $key => $value) {
821
            $this->loggerInstance->debug(1, 'toPost ' . $toPost[$key] . "\n");
822
            // temporarly one server $response['res[' . $key . ']'] = $this->sendToRADIUS($key, $toPost[$key]);
823
            //if ($key == 2) {
824
            //    $response['res[2]'] = 'OK'; 
825
            //} else {
826
                $response['res[' . $key . ']'] = $this->sendToRADIUS($key, $toPost[$key]);
827
            //}
828
        }
829
        if ($onlyone) {
830
            $response['res[' . ($onlyone == 1) ? 2 : 1 . ']'] = \core\AbstractDeployment::RADIUS_OK;
831
        }
832
        foreach (array('OK', 'FAILURE') as $status) {
833
            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...
834
                $this->sendMailtoAdmin($remove, $response, $status);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $remove does not seem to be defined for all execution paths leading up to this point.
Loading history...
835
            }
836
        }
837
        return $response;
838
    }
839
    /**
840
     * prepare request to get RADIUS logs for given deployment
841
     * @param int $onlyone the flag indicating on which server to conduct modifications
842
     *
843
     * 
844
     * @return array index res[1] indicate primary RADIUS status, index res[2] backup RADIUS status
845
     */
846
    public function getRADIUSLogs($onlyone = 0, $logs = 0)
847
    {
848
        $toPost = ($onlyone ? array($onlyone => '') : array(1 => '', 2 => ''));
849
        if ($logs) {
850
            $toPostTemplate = 'logid=DEBUG-' . $this->identifier . '-' .$this->institution . "&backlog=$logs";
851
            foreach (array_keys($toPost) as $key) {
852
                $toPost[$key] = $toPostTemplate;
853
            }
854
        }
855
        $response = array();
856
        $tempdir = \core\common\Entity::createTemporaryDirectory("test");
857
        $zipdir = $tempdir['dir'];
858
        foreach ($toPost as $key => $value) {
859
            $this->loggerInstance->debug(1, 'toPost ' . $toPost[$key] . "\n");
860
            $response['res[' . $key . ']'] = $this->sendToRADIUS($key, $toPost[$key]);
861
            $paths = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $paths is dead and can be removed.
Loading history...
862
            if (substr($response['res[' . $key . ']'], 0, 8) == 'ZIPDATA:') {
863
                $data = substr($response['res[' . $key . ']'], 8);
864
                if (!file_exists("$zipdir/$key")) {
865
                    mkdir("$zipdir/$key", 0755, true );
866
                }
867
                $fileHandle = fopen("$zipdir/$key/detail.zip", "wb");
868
                fwrite($fileHandle, $data);
869
                fclose($fileHandle);
870
            }
871
        }
872
        $zipt = new \ZipArchive;
873
        $zipt->open("$zipdir/detail-" . $this->identifier . '-' .$this->institution . '.zip', \ZipArchive::CREATE);
874
        $cnt = 0;
875
        foreach ($toPost as $key => $value) {
876
            if (file_exists("$zipdir/$key/detail.zip")) {
877
                $zipf = new \ZipArchive;
878
                $zipf->open("$zipdir/$key/detail.zip");
879
                if ($zipf->numFiles > 0) {
880
                    $zipf->extractTo("$zipdir/$key/");
881
                }
882
                $zipf->close();
883
                unlink("$zipdir/$key/detail.zip");
884
                $files = scandir("$zipdir/$key/");
885
                foreach($files as $file) {
886
                  if ($file == '.' || $file == '..') continue;
887
                  $data = file_get_contents("$zipdir/$key/$file");
888
                  $zipt->addFromString("radius-$key/$file", $data);
889
                  $cnt += 1;
890
                  unlink("$zipdir/$key/$file");
891
                } 
892
                if (file_exists("$zipdir/$key")) {
893
                    rmdir("$zipdir/$key");
894
                }
895
            }
896
        }
897
        if ($cnt == 0) {
898
            $zipt->addEmptyDir('.');
899
        }
900
        $zipt->close();
901
        if (file_exists("$zipdir/detail-" . $this->identifier . '-' .$this->institution . '.zip')) {
902
            $data = file_get_contents("$zipdir/detail-" . $this->identifier . '-' .$this->institution . '.zip');
903
            unlink("$zipdir/detail-" . $this->identifier . '-' .$this->institution . '.zip'); 
904
            rmdir($zipdir);
905
        }     
906
        if ($data !== FALSE) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $data does not seem to be defined for all execution paths leading up to this point.
Loading history...
907
            header('Content-Type: application/zip');
908
            header("Content-Disposition: attachment; filename=\"detail-".$this->identifier . '-' .$this->institution.".zip\"");
909
            header("Content-Transfer-Encoding: binary");
910
            echo $data;
911
        } 
912
        
913
        return;
914
    }
915
}
916