Passed
Push — master ( 20c378...20937d )
by Maja
13:45
created

DeploymentManaged   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 353
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 41
eloc 146
dl 0
loc 353
rs 9.1199
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
B setRADIUSconfig() 0 42 9
A activate() 0 2 1
A deactivate() 0 2 1
A updateFreshness() 0 2 1
A getOperatorName() 0 6 2
A getFreshness() 0 5 2
B initialise() 0 31 6
B __construct() 0 51 8
A destroy() 0 3 1
B findGoodServerLocation() 0 38 10

How to fix   Complexity   

Complex Class

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

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

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

1
<?php
2
3
/*
4
 * *****************************************************************************
5
 * Contributions to this work were made on behalf of the GÉANT project, a 
6
 * project that has received funding from the European Union’s Framework 
7
 * Programme 7 under Grant Agreements No. 238875 (GN3) and No. 605243 (GN3plus),
8
 * Horizon 2020 research and innovation programme under Grant Agreements No. 
9
 * 691567 (GN4-1) and No. 731122 (GN4-2).
10
 * On behalf of the aforementioned projects, GEANT Association is the sole owner
11
 * of the copyright in all material which was developed by a member of the GÉANT
12
 * project. GÉANT Vereniging (Association) is registered with the Chamber of 
13
 * Commerce in Amsterdam with registration number 40535155 and operates in the 
14
 * UK as a branch of GÉANT Vereniging.
15
 * 
16
 * Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands. 
17
 * UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK
18
 *
19
 * License: see the web/copyright.inc.php file in the file structure or
20
 *          <base_url>/copyright.php after deploying the software
21
 */
22
23
/**
24
 * This file contains the AbstractProfile class. It contains common methods for
25
 * both RADIUS/EAP profiles and SilverBullet profiles
26
 *
27
 * @author Stefan Winter <[email protected]>
28
 * @author Tomasz Wolniewicz <[email protected]>
29
 *
30
 * @package Developer
31
 *
32
 */
33
34
namespace core;
35
36
use \Exception;
37
38
/**
39
 * This class represents an EAP Profile.
40
 * Profiles can inherit attributes from their IdP, if the IdP has some. Otherwise,
41
 * one can set attribute in the Profile directly. If there is a conflict between
42
 * IdP-wide and Profile-wide attributes, the more specific ones (i.e. Profile) win.
43
 * 
44
 * @author Stefan Winter <[email protected]>
45
 * @author Tomasz Wolniewicz <[email protected]>
46
 *
47
 * @license see LICENSE file in root directory
48
 *
49
 * @package Developer
50
 */
51
class DeploymentManaged extends AbstractDeployment {
52
53
    /**
54
     * This is the limit for dual-stack hosts. Single stack uses half of the FDs
55
     * in FreeRADIUS and take twice as many. initialise() takes this into
56
     * account.
57
     */
58
    const MAX_CLIENTS_PER_SERVER = 200;
59
    const PRODUCTNAME = "Managed SP";
60
61
    /**
62
     * the primary RADIUS server port for this SP instance
63
     * 
64
     * @var integer
65
     */
66
    public $port1;
67
68
    /**
69
     * the backup RADIUS server port for this SP instance
70
     * 
71
     * @var integer
72
     */
73
    public $port2;
74
75
    /**
76
     * the shared secret for this SP instance
77
     * 
78
     * @var string
79
     */
80
    public $secret;
81
82
    /**
83
     * the IPv4 address of the primary RADIUS server for this SP instance 
84
     * (can be NULL)
85
     * 
86
     * @var string
87
     */
88
    public $host1_v4;
89
90
    /**
91
     * the IPv6 address of the primary RADIUS server for this SP instance 
92
     * (can be NULL)
93
     * 
94
     * @var string
95
     */
96
    public $host1_v6;
97
98
    /**
99
     * the IPv4 address of the backup RADIUS server for this SP instance 
100
     * (can be NULL)
101
     * 
102
     * @var string
103
     */
104
    public $host2_v4;
105
106
    /**
107
     * the IPv6 address of the backup RADIUS server for this SP instance 
108
     * (can be NULL)
109
     * 
110
     * @var string
111
     */
112
    public $host2_v6;
113
114
    /**
115
     * the primary RADIUS server instance for this SP instance
116
     * 
117
     * @var string
118
     */
119
    public $radius_instance_1;
120
121
    /**
122
     * the backup RADIUS server instance for this SP instance
123
     * 
124
     * @var string
125
     */
126
    public $radius_instance_2;
127
    
128
    /**
129
     * the primary RADIUS server hostname - for sending configuration requests
130
     * 
131
     * @var string
132
     */
133
    public $radius_hostname_1;
134
135
    /**
136
     * the backup RADIUS server hostname - for sending configuration requests
137
     * 
138
     * @var string
139
     */
140
    public $radius_hostname_2;
141
142
    /**
143
     * Class constructor for existing deployments (use 
144
     * IdP::newDeployment() to actually create one). Retrieves all 
145
     * attributes from the DB and stores them in the priv_ arrays.
146
     * 
147
     * @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.
148
     * @param string|int $deploymentIdRaw identifier of the deployment in the DB
149
     * @throws Exception
150
     */
151
    public function __construct($idpObject, $deploymentIdRaw) {
152
        parent::__construct($idpObject, $deploymentIdRaw); // we now have access to our INST database handle and logging
153
        $this->entityOptionTable = "deployment_option";
154
        $this->entityIdColumn = "deployment_id";
155
        $this->type = AbstractDeployment::DEPLOYMENTTYPE_MANAGED;
156
        if (!is_numeric($deploymentIdRaw)) {
157
            throw new Exception("Managed SP instances have to have a numeric identifier");
158
        }
159
        $propertyQuery = "SELECT status,port_instance_1,port_instance_2,secret,radius_instance_1,radius_instance_2 FROM deployment WHERE deployment_id = ?";
160
        $queryExec = $this->databaseHandle->exec($propertyQuery, "i", $deploymentIdRaw);
161
        if (mysqli_num_rows(/** @scrutinizer ignore-type */ $queryExec) == 0) {
162
            throw new Exception("Attempt to construct an unknown DeploymentManaged!");
163
        }
164
        $this->identifier = $deploymentIdRaw;
165
        while ($iterator = mysqli_fetch_object(/** @scrutinizer ignore-type */ $queryExec)) {
166
            if ($iterator->secret == NULL && $iterator->radius_instance_1 == NULL) {
167
                // we are instantiated for the first time, initialise us
168
                $details = $this->initialise();
169
                $this->port1 = $details["port_instance_1"];
170
                $this->port2 = $details["port_instance_2"];
171
                $this->secret = $details["secret"];
172
                $this->radius_instance_1 = $details["radius_instance_1"];
173
                $this->radius_instance_2 = $details["radius_instance_2"];
174
                $this->status = AbstractDeployment::INACTIVE;
175
            } else {
176
                $this->port1 = $iterator->port_instance_1;
177
                $this->port2 = $iterator->port_instance_2;
178
                $this->secret = $iterator->secret;
179
                $this->radius_instance_1 = $iterator->radius_instance_1;
180
                $this->radius_instance_2 = $iterator->radius_instance_2;
181
                $this->status = $iterator->status;
182
            }
183
        }
184
        $server1details = $this->databaseHandle->exec("SELECT mgmt_hostname, radius_ip4, radius_ip6 FROM managed_sp_servers WHERE server_id = '$this->radius_instance_1'");
185
        while ($iterator2 = mysqli_fetch_object(/** @scrutinizer ignore-type */ $server1details)) {
186
            $this->host1_v4 = $iterator2->radius_ip4;
187
            $this->host1_v6 = $iterator2->radius_ip6;
188
            $this->radius_hostname_1 = $iterator2->mgmt_hostname;
189
        }
190
        $server2details = $this->databaseHandle->exec("SELECT mgmt_hostname, radius_ip4, radius_ip6 FROM managed_sp_servers WHERE server_id = '$this->radius_instance_2'");
191
        while ($iterator3 = mysqli_fetch_object(/** @scrutinizer ignore-type */ $server2details)) {
192
            $this->host2_v4 = $iterator3->radius_ip4;
193
            $this->host2_v6 = $iterator3->radius_ip6;
194
            $this->radius_hostname_2 = $iterator3->mgmt_hostname;
195
        }
196
        $thisLevelAttributes = $this->retrieveOptionsFromDatabase("SELECT DISTINCT option_name, option_lang, option_value, row 
197
                                            FROM $this->entityOptionTable
198
                                            WHERE $this->entityIdColumn = ?  
199
                                            ORDER BY option_name", "Profile");
200
        $tempAttribMergedIdP = $this->levelPrecedenceAttributeJoin($thisLevelAttributes, $this->idpAttributes, "IdP");
201
        $this->attributes = $this->levelPrecedenceAttributeJoin($tempAttribMergedIdP, $this->fedAttributes, "FED");
202
    }
203
204
    /**
205
     * finds a suitable server which is geographically close to the admin
206
     * 
207
     * @param array  $adminLocation      the current geographic position of the admin
208
     * @param string $federation         the federation this deployment belongs to
209
     * @param array  $blacklistedServers list of server to IGNORE
210
     * @return string the server ID
211
     * @throws Exception
212
     */
213
    private function findGoodServerLocation($adminLocation, $federation, $blacklistedServers) {
214
        // find a server near him (list of all servers with capacity, ordered by distance)
215
        // first, if there is a pool of servers specifically for this federation, prefer it
216
        $servers = $this->databaseHandle->exec("SELECT server_id, radius_ip4, radius_ip6, location_lon, location_lat FROM managed_sp_servers WHERE pool = '$federation'");
217
        
218
        $serverCandidates = [];
219
        while ($iterator = mysqli_fetch_object(/** @scrutinizer ignore-type */ $servers)) {
220
            $maxSupportedClients = DeploymentManaged::MAX_CLIENTS_PER_SERVER;
221
            if ($iterator->radius_ip4 == NULL || $iterator->radius_ip6 == NULL) {
222
                // half the amount of IP stacks means half the amount of FDs in use, so we can take twice as many
223
                $maxSupportedClients = $maxSupportedClients * 2;
224
            }
225
            $clientCount1 = $this->databaseHandle->exec("SELECT port_instance_1 AS tenants1 FROM deployment WHERE radius_instance_1 = '$iterator->server_id'");
226
            $clientCount2 = $this->databaseHandle->exec("SELECT port_instance_2 AS tenants2 FROM deployment WHERE radius_instance_2 = '$iterator->server_id'");
227
228
            $clients = $clientCount1->num_rows + $clientCount2->num_rows;
229
            if (in_array($iterator->server_id, $blacklistedServers)) {
230
                continue;
231
            }
232
            if ($clients < $maxSupportedClients) {
233
                $serverCandidates[IdPlist::geoDistance($adminLocation, ['lat' => $iterator->location_lat, 'lon' => $iterator->location_lon])] = $iterator->server_id;
234
            }
235
            if ($clients > $maxSupportedClients * 0.9) {
236
                $this->loggerInstance->debug(1, "A RADIUS server for Managed SP (" . $iterator->server_id . ") is serving at more than 90% capacity!");
237
            }
238
        }
239
        if (count($serverCandidates) == 0 && $federation != "DEFAULT") {
240
            // we look in the default pool instead
241
            // recursivity! Isn't that cool!
242
            return $this->findGoodServerLocation($adminLocation, "DEFAULT", $blacklistedServers);
243
        }
244
        if (count($serverCandidates) == 0) {
245
            throw new Exception("No available server found for new SP! $federation ".print_r($serverCandidates, true));
246
        }
247
        // put the nearest server on top of the list
248
        ksort($serverCandidates);
249
        $this->loggerInstance->debug(1, $serverCandidates);
250
        return array_shift($serverCandidates);
251
    }
252
253
    /**
254
     * initialises a new SP
255
     * 
256
     * @return array details of the SP as generated during initialisation
257
     * @throws Exception
258
     */
259
    private function initialise() {
260
        // find out where the admin is located approximately
261
        $ourLocation = ['lon' => 0, 'lat' => 0];
262
        $geoip = DeviceLocation::locateDevice();
263
        if ($geoip['status'] == 'ok') {
264
            $ourLocation = ['lon' => $geoip['geo']['lon'], 'lat' => $geoip['geo']['lat']];
265
        }
266
        $inst = new IdP($this->institution);
267
        $ourserver = $this->findGoodServerLocation($ourLocation, $inst->federation , []);
268
        // now, find an unused port in the preferred server
269
        $foundFreePort1 = 0;
270
        while ($foundFreePort1 == 0) {
271
            $portCandidate = random_int(1200, 65535);
272
            $check = $this->databaseHandle->exec("SELECT port_instance_1 FROM deployment WHERE radius_instance_1 = '" . $ourserver . "' AND port_instance_1 = $portCandidate");
273
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $check) == 0) {
274
                $foundFreePort1 = $portCandidate;
275
            }
276
        }
277
        $ourSecondServer = $this->findGoodServerLocation($ourLocation, $inst->federation , [$ourserver]);
278
        $foundFreePort2 = 0;
279
        while ($foundFreePort2 == 0) {
280
            $portCandidate = random_int(1200, 65535);
281
            $check = $this->databaseHandle->exec("SELECT port_instance_2 FROM deployment WHERE radius_instance_2 = '" . $ourSecondServer . "' AND port_instance_2 = $portCandidate");
282
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $check) == 0) {
283
                $foundFreePort2 = $portCandidate;
284
            }
285
        }
286
        // and make up a shared secret that is halfways readable
287
        $futureSecret = $this->randomString(16, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
288
        $this->databaseHandle->exec("UPDATE deployment SET radius_instance_1 = '" . $ourserver . "', radius_instance_2 = '" . $ourSecondServer . "', port_instance_1 = $foundFreePort1, port_instance_2 = $foundFreePort2, secret = '$futureSecret' WHERE deployment_id = $this->identifier");
289
        return ["port_instance_1" => $foundFreePort1, "port_instance_2" => $foundFreePort2, "secret" => $futureSecret, "radius_instance_1" => $ourserver, "radius_instance_2" => $ourserver];
290
    }
291
292
    /**
293
     * update the last_changed timestamp for this deployment
294
     * 
295
     * @return void
296
     */
297
    public function updateFreshness() {
298
        $this->databaseHandle->exec("UPDATE deployment SET last_change = CURRENT_TIMESTAMP WHERE deployment_id = $this->identifier");
299
    }
300
301
    /**
302
     * gets the last-modified timestamp (useful for caching "dirty" check)
303
     * 
304
     * @return string the date in string form, as returned by SQL
305
     */
306
    public function getFreshness() {
307
        $execLastChange = $this->databaseHandle->exec("SELECT last_change FROM deployment WHERE deployment_id = $this->identifier");
308
        // SELECT always returns a resource, never a boolean
309
        if ($freshnessQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $execLastChange)) {
310
            return $freshnessQuery->last_change;
311
        }
312
    }
313
314
    /**
315
     * Deletes the deployment from database
316
     * 
317
     * @return void
318
     */
319
    public function destroy() {
320
        $this->databaseHandle->exec("DELETE FROM deployment_option WHERE deployment_id = $this->identifier");
321
        $this->databaseHandle->exec("DELETE FROM deployment WHERE deployment_id = $this->identifier");
322
    }
323
324
    /**
325
     * deactivates the deployment.
326
     * TODO: needs to call the RADIUS server reconfiguration routines...
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
327
     * 
328
     * @return void
329
     */
330
    public function deactivate() {
331
        $this->databaseHandle->exec("UPDATE deployment SET status = " . DeploymentManaged::INACTIVE . " WHERE deployment_id = $this->identifier");
332
    }
333
334
    /**
335
     * activates the deployment.
336
     * TODO: needs to call the RADIUS server reconfiguration routines...
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
337
     * 
338
     * @return void
339
     */
340
    public function activate() {
341
        $this->databaseHandle->exec("UPDATE deployment SET status = " . DeploymentManaged::ACTIVE . " WHERE deployment_id = $this->identifier");
342
    }
343
344
    /**
345
     * determines the Operator-Name attribute content to use in the RADIUS config
346
     * 
347
     * @return string
348
     */
349
    public function getOperatorName() {
350
        $customAttrib = $this->getAttributes("managedsp:operatorname");
351
        if (count($customAttrib) == 0) {
352
            return "1sp.".$this->identifier."-".$this->institution.\config\ConfAssistant::SILVERBULLET['realm_suffix'];
353
        }
354
        return $customAttrib[0]["value"];
355
    }
356
    
357
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $remove should have a doc-comment as per coding-style.
Loading history...
358
     * sends request to add/modify RADIUS settings for given deployment
359
     *
360
     * @return string
361
     */
362
    public function setRADIUSconfig($remove = 0) {
363
        $toPost1 = 'instid=' . $this->institution . '&deploymentid=' . $this->identifier . '&secret=' . $this->secret . '&country=' . $this->getAttributes("internal:country")[0]['value'] . '&';
364
        if ($remove) {
365
            $toPost1 = $toPost1 . 'remove=1&';
366
        } else {
367
            if ($this->getAttributes("managedsp:operatorname")[0]['value'] ?? NULL) {
368
                $toPost1 = $toPost1 . 'operatorname=' . $this->getAttributes("managedsp:operatorname")[0]['value'] . '&';
369
            }
370
            if ($this->getAttributes("managedsp:vlan")[0]['value'] ?? NULL) {
371
                $idp = new IdP($this->institution);
372
                $allProfiles = $idp->listProfiles(TRUE);
373
                $allRealms = [];
374
                if ($realmforvlan = ($this->getAttributes("managedsp:realmforvlan") ?? NULL)) {
0 ignored issues
show
Unused Code introduced by
The assignment to $realmforvlan is dead and can be removed.
Loading history...
375
                    $allRealms = array_values(array_unique(array_column($this->getAttributes("managedsp:realmforvlan"), "value")));
376
                }
377
                foreach ($allProfiles as $profile) {
378
                    if ($realm = ($profile->getAttributes("internal:realm")[0]['value'] ?? NULL)) {
379
                        if (!in_array($realm, $allRealms)) {
380
                            $allRealms[] = $realm;
381
                        }
382
                    }
383
                }
384
                if (!empty($allRealms)) {
385
                    $this->loggerInstance->debug(1, $allRealms);
386
                    $toPost1 = $toPost1 . 'vlan=' . $this->getAttributes("managedsp:vlan")[0]['value'] . '&';
387
                    $toPost1 = $toPost1 . 'realmforvlan[]=' . implode('&realmforvlan[]=', $allRealms) . '&';
388
                }
389
            }
390
        }
391
        $toPost2 = $toPost1;
392
        $toPost1 = $toPost1 . 'port=' . $this->port1;
393
        $toPost2 = $toPost2 . 'port=' . $this->port2;
0 ignored issues
show
Unused Code introduced by
The assignment to $toPost2 is dead and can be removed.
Loading history...
394
        $ch = curl_init( "http://" . $this->radius_hostname_1 );
395
        curl_setopt( $ch, CURLOPT_POST, 1);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_setopt() does only seem to accept resource, 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
        curl_setopt( /** @scrutinizer ignore-type */ $ch, CURLOPT_POST, 1);
Loading history...
396
        curl_setopt( $ch, CURLOPT_POSTFIELDS, $toPost1);
397
        $this->loggerInstance->debug(1, "Posting to http://" . $this->radius_hostname_1 . ": $toPost1\n");
398
        curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1);
399
        curl_setopt( $ch, CURLOPT_HEADER, 0);
400
        curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1);
401
        $response = curl_exec( $ch );
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_exec() does only seem to accept resource, 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

401
        $response = curl_exec( /** @scrutinizer ignore-type */ $ch );
Loading history...
402
        $this->loggerInstance->debug(1, "Response from FR configurator: $response\n");
403
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response also could return the type boolean which is incompatible with the documented return type string.
Loading history...
404
    }
405
}
406