Test Failed
Push — master ( 949d22...5a1e09 )
by Stefan
05:33
created

DeploymentManaged::findGoodServerLocation()   B

Complexity

Conditions 8
Paths 22

Size

Total Lines 33
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 22
dl 0
loc 33
rs 8.4444
c 0
b 0
f 0
cc 8
nc 22
nop 2
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 int
65
     */
66
    public $port1;
67
68
    /**
69
     * the backup RADIUS server port for this SP instance
70
     * 
71
     * @var int
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
     * Class constructor for existing deployments (use 
130
     * IdP::newDeployment() to actually create one). Retrieves all 
131
     * attributes from the DB and stores them in the priv_ arrays.
132
     * 
133
     * @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.
134
     * @param string|int $deploymentIdRaw identifier of the deployment in the DB
135
     */
136
    public function __construct($idpObject, $deploymentIdRaw) {
137
        parent::__construct($idpObject, $deploymentIdRaw); // we now have access to our INST database handle and logging
138
        $this->entityOptionTable = "deployment_option";
139
        $this->entityIdColumn = "deployment_id";
140
        $this->type = AbstractDeployment::DEPLOYMENTTYPE_MANAGED;
141
        if (!is_numeric($deploymentIdRaw)) {
142
            throw new Exception("Managed SP instances have to have a numeric identifier");
143
        }
144
        $propertyQuery = "SELECT status,port_instance_1,port_instance_2,secret,radius_instance_1,radius_instance_2 FROM deployment WHERE deployment_id = ?";
145
        $queryExec = $this->databaseHandle->exec($propertyQuery, "i", $deploymentIdRaw);
146
        while ($iterator = mysqli_fetch_object(/** @scrutinizer ignore-type */ $queryExec)) {
147
            if ($iterator->secret == NULL && $iterator->radius_instance_1 == NULL) {
148
                // we are instantiated for the first time, initialise us
149
                $details = $this->initialise();
150
                $this->port1 = $details["port_instance_1"];
151
                $this->port2 = $details["port_instance_2"];
152
                $this->secret = $details["secret"];
153
                $this->radius_instance_1 = $details["radius_instance_1"];
154
                $this->radius_instance_2 = $details["radius_instance_2"];
155
                $this->status = AbstractDeployment::INACTIVE;
156
            } else {
157
                $this->port1 = $iterator->port_instance_1;
158
                $this->port2 = $iterator->port_instance_2;
159
                $this->secret = $iterator->secret;
160
                $this->radius_instance_1 = $iterator->radius_instance_1;
161
                $this->radius_instance_2 = $iterator->radius_instance_2;
162
                $this->status = $iterator->status;
163
            }
164
        }
165
        $server1details = $this->databaseHandle->exec("SELECT radius_ip4, radius_ip6 FROM managed_sp_servers WHERE server_id = '$this->radius_instance_1'");
166
        while ($iterator2 = mysqli_fetch_object(/** @scrutinizer ignore-type */ $server1details)) {
167
            $this->host1_v4 = $iterator2->radius_ip4;
168
            $this->host1_v6 = $iterator2->radius_ip6;
169
        }
170
        $server2details = $this->databaseHandle->exec("SELECT radius_ip4, radius_ip6 FROM managed_sp_servers WHERE server_id = '$this->radius_instance_2'");
171
        while ($iterator3 = mysqli_fetch_object(/** @scrutinizer ignore-type */ $server2details)) {
172
            $this->host2_v4 = $iterator3->radius_ip4;
173
            $this->host2_v6 = $iterator3->radius_ip6;
174
        }
175
        $this->attributes = $this->retrieveOptionsFromDatabase("SELECT DISTINCT option_name, option_lang, option_value, row 
176
                                            FROM $this->entityOptionTable
177
                                            WHERE $this->entityIdColumn = ?  
178
                                            ORDER BY option_name", "Profile");
179
    }
180
181
    /**
182
     * finds a suitable server which is geographically close to the admin
183
     * 
184
     * @param array $adminLocation      the current geographic position of the admin
185
     * @param array $blacklistedServers list of server to IGNORE
186
     * @return string the server ID
187
     * @throws Exception
188
     */
189
    private function findGoodServerLocation($adminLocation, $blacklistedServers) {
190
        // find a server near him (list of all servers with capacity, ordered by distance)
191
        $servers = $this->databaseHandle->exec("SELECT server_id, radius_ip4, radius_ip6, location_lon, location_lat FROM managed_sp_servers");
192
        $serverCandidates = [];
193
        while ($iterator = mysqli_fetch_object(/** @scrutinizer ignore-type */ $servers)) {
194
            $maxSupportedClients = DeploymentManaged::MAX_CLIENTS_PER_SERVER;
195
            if ($iterator->radius_ip4 == NULL || $iterator->radius_ip6 == NULL) {
196
                // half the amount of IP stacks means half the amount of FDs in use, so we can take twice as many
197
                $maxSupportedClients = $maxSupportedClients * 2;
198
            }
199
            $clientCount1 = $this->databaseHandle->exec("SELECT count(port_instance_1) AS tenants1 FROM deployment WHERE radius_instance_1 = '$iterator->server_id'");
200
            $clientCount2 = $this->databaseHandle->exec("SELECT count(port_instance_2) AS tenants2 FROM deployment WHERE radius_instance_2 = '$iterator->server_id'");
201
            $row1 = mysqli_fetch_row($clientCount1);
0 ignored issues
show
Bug introduced by
It seems like $clientCount1 can also be of type true; however, parameter $result of mysqli_fetch_row() does only seem to accept mysqli_result, 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

201
            $row1 = mysqli_fetch_row(/** @scrutinizer ignore-type */ $clientCount1);
Loading history...
202
            $row2 = mysqli_fetch_row($clientCount2);
203
204
            $clients = $row1[0] + $row2[0];
205
            if (in_array($iterator->server_id, $blacklistedServers)) {
206
                continue;
207
            }
208
            if ($clients < $maxSupportedClients) {
209
                $serverCandidates[IdPlist::geoDistance($adminLocation, ['lat' => $iterator->location_lat, 'lon' => $iterator->location_lon])] = $iterator->server_id;
210
            }
211
            if ($clients > $maxSupportedClients * 0.9) {
212
                $this->loggerInstance->debug(1, "A RADIUS server for Managed SP (" . $iterator->server_id . ") is serving at more than 90% capacity!");
213
            }
214
        }
215
        if (count($serverCandidates) == 0) {
216
            throw new Exception("No available server found for new SP!");
217
        }
218
        // put the nearest server on top of the list
219
        ksort($serverCandidates);
220
        $this->loggerInstance->debug(1, $serverCandidates);
221
        return array_shift($serverCandidates);
222
    }
223
224
    /**
225
     * initialises a new SP
226
     * 
227
     * @return array details of the SP as generated during initialisation
228
     * @throws Exception
229
     */
230
    private function initialise() {
231
        // find out where the admin is located approximately
232
        $ourLocation = ['lon' => 0, 'lat' => 0];
233
        $geoip = DeviceLocation::locateDevice();
234
        if ($geoip['status'] == 'ok') {
235
            $ourLocation = ['lon' => $geoip['geo']['lon'], 'lat' => $geoip['geo']['lat']];
236
        }
237
        $ourserver = $this->findGoodServerLocation($ourLocation, []);
238
        // now, find an unused port in the preferred server
239
        $foundFreePort1 = 0;
240
        while ($foundFreePort1 == 0) {
241
            $portCandidate = random_int(1025, 65535);
242
            $check = $this->databaseHandle->exec("SELECT port_instance_1 FROM deployment WHERE radius_instance_1 = '" . $ourserver . "' AND port_instance_1 = $portCandidate");
243
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $check) == 0) {
244
                $foundFreePort1 = $portCandidate;
245
            }
246
        }
247
        $ourSecondServer = $this->findGoodServerLocation($ourLocation, [$ourserver]);
248
        $foundFreePort2 = 0;
249
        while ($foundFreePort2 == 0) {
250
            $portCandidate = random_int(1025, 65535);
251
            $check = $this->databaseHandle->exec("SELECT port_instance_2 FROM deployment WHERE radius_instance_2 = '" . $ourSecondServer . "' AND port_instance_2 = $portCandidate");
252
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $check) == 0) {
253
                $foundFreePort2 = $portCandidate;
254
            }
255
        }
256
        // and make up a shared secret that is halfways readable
257
        $futureSecret = $this->randomString(16, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
258
        $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");
259
        return ["port_instance_1" => $foundFreePort1, "port_instance_2" => $foundFreePort2, "secret" => $futureSecret, "radius_instance_1" => $ourserver, "radius_instance_2" => $ourserver];
260
    }
261
262
    /**
263
     * update the last_changed timestamp for this deployment
264
     * 
265
     * @return void
266
     */
267
    public function updateFreshness() {
268
        $this->databaseHandle->exec("UPDATE deployment SET last_change = CURRENT_TIMESTAMP WHERE deployment_id = $this->identifier");
269
    }
270
271
    /**
272
     * gets the last-modified timestamp (useful for caching "dirty" check)
273
     * 
274
     * @return string the date in string form, as returned by SQL
275
     */
276
    public function getFreshness() {
277
        $execLastChange = $this->databaseHandle->exec("SELECT last_change FROM deployment WHERE deployment_id = $this->identifier");
278
        // SELECT always returns a resource, never a boolean
279
        if ($freshnessQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $execLastChange)) {
280
            return $freshnessQuery->last_change;
281
        }
282
    }
283
284
    /**
285
     * Deletes the deployment from database
286
     * 
287
     * @return void
288
     */
289
    public function destroy() {
290
        $this->databaseHandle->exec("DELETE FROM deployment_option WHERE deployment_id = $this->identifier");
291
        $this->databaseHandle->exec("DELETE FROM deployment WHERE deployment_id = $this->identifier");
292
    }
293
294
    /**
295
     * deactivates the deployment.
296
     * TODO: needs to call the RADIUS server reconfiguration routines...
297
     * 
298
     * @return void
299
     */
300
    public function deactivate() {
301
        $this->databaseHandle->exec("UPDATE deployment SET status = " . DeploymentManaged::INACTIVE . " WHERE deployment_id = $this->identifier");
302
    }
303
304
    /**
305
     * activates the deployment.
306
     * TODO: needs to call the RADIUS server reconfiguration routines...
307
     * 
308
     * @return void
309
     */
310
    public function activate() {
311
        $this->databaseHandle->exec("UPDATE deployment SET status = " . DeploymentManaged::ACTIVE . " WHERE deployment_id = $this->identifier");
312
    }
313
314
}
315