checkRADIUSHostandConfigDaemon()   A
last analyzed

Complexity

Conditions 5
Paths 9

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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

395
        $csr = openssl_csr_new($dn, /** @scrutinizer ignore-type */ $privkey,
Loading history...
396
                               array('digest_alg' => 'sha256', 'config' => ROOT . "/config/ManagedSPCerts/openssl.cnf"));
397
        // get CA certificate and private key
398
        $caprivkey = array(file_get_contents(ROOT . "/config/ManagedSPCerts/eduroamSP-CA.key"),
399
                            \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...
400
        $cacert = file_get_contents(ROOT .  "/config/ManagedSPCerts/eduroamSP-CA.pem");
401
        $this->setTLSSerialNumber();
402
        $clientcert = openssl_csr_sign($csr, $cacert, $caprivkey, \config\Master::MANAGEDSP['daystoexpiry'],
403
                          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

403
                          array('digest_alg'=>'sha512', 'config' => ROOT . "/config/ManagedSPCerts/openssl.cnf"), /** @scrutinizer ignore-type */ $this->radsec_cert_serial_no);
Loading history...
404
        openssl_x509_export($clientcert, $this->radsec_cert);
405
    } 
406
    /**
407
     * retrieves usage statistics for the deployment
408
     * 
409
     * @param int $backlog how many seconds back in time should we look
410
     * @param int $limit   max number of entries to retrieve (if supplied, backlog will be ignored)
411
     * @return array of arrays: activity_time, realm, mac, result
412
     * @throws Exception
413
     */
414
    public function retrieveStatistics(int $backlog, int $limit = 0)
415
    {
416
        // find a server near him (list of all servers with capacity, ordered by distance)
417
        // first, if there is a pool of servers specifically for this federation, prefer it
418
        // only check the consortium pool group we want to attach to
419
        // TODO: if we also collect stats from OpenRoaming hosts, differentiate the logs!
420
        $opName = $this->getOperatorName();
0 ignored issues
show
Unused Code introduced by
The assignment to $opName is dead and can be removed.
Loading history...
421
        if ($limit !== 0) {
422
            $conditional1 = "";
423
            $conditional2 = "DESC LIMIT $limit";
424
        } else {
425
            $conditional1 = "AND activity_time > DATE_SUB(NOW(), INTERVAL $backlog SECOND )";
426
            $conditional2 = "DESC";
427
        }
428
        $client = 'SP' . $this->identifier . '-' . $this->institution;
429
        $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 );
430
       
431
        return mysqli_fetch_all($stats, \MYSQLI_ASSOC);
432
    }
433
        
434
    /**
435
     * initialises a new SP
436
     * 
437
     * @return array details of the SP as generated during initialisation
438
     * @throws Exception
439
     */
440
    private function initialise()
441
    {
442
        // find out where the admin is located approximately
443
        $ourLocation = ['lon' => 0, 'lat' => 0];
444
        $geoip = DeviceLocation::locateDevice();
445
        if ($geoip['status'] == 'ok') {
446
            $ourLocation = ['lon' => $geoip['geo']['lon'], 'lat' => $geoip['geo']['lat']];
447
        }
448
        $inst = new IdP($this->institution);
449
        $ourserver = $this->findGoodServerLocation($ourLocation, $inst->federation, []);
450
        // now, find an unused port in the preferred server
451
        $foundFreePort1 = 0;
452
        while ($foundFreePort1 == 0) {
453
            $portCandidate = random_int(1200, 65535);
454
            $check = $this->databaseHandle->exec("SELECT port_instance_1 FROM deployment WHERE radius_instance_1 = ? AND port_instance_1 = ?", "si", $ourserver, $portCandidate);
455
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $check) == 0) {
456
                $foundFreePort1 = $portCandidate;
457
            }
458
        }
459
        $ourSecondServer = $this->findGoodServerLocation($ourLocation, $inst->federation, [$ourserver]);
460
        $foundFreePort2 = 0;
461
        while ($foundFreePort2 == 0) {
462
            $portCandidate = random_int(1200, 65535);
463
            $check = $this->databaseHandle->exec("SELECT port_instance_2 FROM deployment WHERE radius_instance_2 = ? AND port_instance_2 = ?", "si", $ourSecondServer, $portCandidate);
464
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $check) == 0) {
465
                $foundFreePort2 = $portCandidate;
466
            }
467
        }
468
        // and make up a shared secret that is halfways readable
469
        $futureSecret = trim(chunk_split(bin2hex(openssl_random_pseudo_bytes(14)), 4, '-'), '-');
470
        $this->createTLScredentials();
471
        $futurePSKkey = bin2hex(openssl_random_pseudo_bytes(32));
472
        $cons = $this->consortium;
473
        $id = $this->identifier;
474
        
475
        $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);
476
        return ["port_instance_1" => $foundFreePort1, "port_instance_2" => $foundFreePort2, "secret" => $futureSecret, "radius_instance_1" => $ourserver, "radius_instance_2" => $ourSecondServer, "pskkey" => $futurePSKkey];
477
    }
478
479
    /**
480
     * update the last_changed timestamp for this deployment
481
     * 
482
     * @return void
483
     */
484
    public function updateFreshness()
485
    {
486
        $id = $this->identifier;
487
        $this->databaseHandle->exec("UPDATE deployment SET last_change = CURRENT_TIMESTAMP WHERE deployment_id = ?", "i", $id);
488
    }
489
490
    /**
491
     * gets the last-modified timestamp (useful for caching "dirty" check)
492
     * 
493
     * @return string the date in string form, as returned by SQL
494
     */
495
    public function getFreshness()
496
    {
497
        $id = $this->identifier;
498
        $execLastChange = $this->databaseHandle->exec("SELECT last_change FROM deployment WHERE deployment_id = ?", "i", $id);
499
        // SELECT always returns a resource, never a boolean
500
        if ($freshnessQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $execLastChange)) {
501
            return $freshnessQuery->last_change;
502
        }
503
    }
504
505
    /**
506
     * Deletes the deployment from database
507
     * 
508
     * @return void
509
     */
510
    public function remove()
511
    {
512
        $id = $this->identifier;
513
        $this->databaseHandle->exec("DELETE FROM deployment_option WHERE deployment_id = ?", "i", $id);
514
        $this->databaseHandle->exec("DELETE FROM deployment WHERE deployment_id = ?", "i", $id);
515
    }
516
    
517
    /**
518
     * Renews the deployment TLS credentials
519
     * 
520
     * @return void
521
     */
522
    public function renewtls()
523
    {
524
       $id = $this->identifier;
525
       $futureTlsClient = $this->createTLScredentials();
0 ignored issues
show
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...
Unused Code introduced by
The assignment to $futureTlsClient is dead and can be removed.
Loading history...
526
       $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);           
527
    }
528
    
529
    /**
530
     * Create new deployment TLS credentials based on uploaded CSR
531
     * 
532
     * @return void
533
     */
534
    public function tlsfromcsr($csr)
535
    {
536
       $id = $this->identifier;
537
       $dn = array();
538
       $dn['rdnSequence'] = array();
539
       $dn['rdnSequence'][0] = array();
540
       $dn['rdnSequence'][0][] = array('type' => 'id-at-organizationName', 'value' => array());
541
       $dn['rdnSequence'][0][0]['value']['utf8String'] = 'eduroam';
542
       $dn['rdnSequence'][1] = array();
543
       $dn['rdnSequence'][1][] = array('type' => 'id-at-organizationalUnitName', 'value' => array());
544
       $dn['rdnSequence'][1][0]['value']['utf8String'] = 'eduroam Managed SP';
545
       $dn['rdnSequence'][2] = array();
546
       $dn['rdnSequence'][2][] = array('type' => 'id-at-commonName', 'value' => array());
547
       $dn['rdnSequence'][2][0]['value']['utf8String'] = 'SP' . $this->identifier . "-" . $this->institution;
548
       $csr->setDN($dn);
549
       $pemcakey = file_get_contents(ROOT . "/config/ManagedSPCerts/eduroamSP-CA.key");
550
       $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...
551
       $pemca = file_get_contents(ROOT .  "/config/ManagedSPCerts/eduroamSP-CA.pem");
552
       $ca = new \phpseclib3\File\X509();
553
       $ca->loadX509($pemca);
554
       $ca->setPrivateKey($cakey);
555
       // Sign the updated request, producing the certificate.
556
       $x509 = new \phpseclib3\File\X509();
557
       $csr->setExtension('id-ce-keyUsage', ['digitalSignature', 'nonRepudiation', 'keyEncipherment']);
558
       $csr->setExtension('id-ce-extKeyUsage', ['id-kp-clientAuth']);
559
       $csr->setExtension('id-ce-basicConstraints', ['cA' => false], false);
560
       $x509->setEndDate('+' . \config\Master::MANAGEDSP['daystoexpiry'] . ' days');
561
       $this->setTLSSerialNumber(999999999999999999);
562
       $x509->setSerialNumber($this->radsec_cert_serial_no, 10);
563
       $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

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