Server::handleDelete()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 8
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 12
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\cdc;
6
7
use SimpleSAML\Assert\Assert;
8
use SimpleSAML\Configuration;
9
use SimpleSAML\Error;
10
use SimpleSAML\Logger;
11
use SimpleSAML\SAML2\Exception\ProtocolViolationException;
12
use SimpleSAML\Utils;
13
14
use function array_search;
15
use function base64_decode;
16
use function base64_encode;
17
use function explode;
18
use function implode;
19
use function intval;
20
use function is_string;
21
use function json_decode;
22
use function json_encode;
23
use function sha1;
24
use function strlen;
25
use function strval;
26
use function time;
27
use function var_export;
28
29
/**
30
 * CDC server class.
31
 *
32
 * @package SimpleSAMLphp
33
 */
34
35
class Server
36
{
37
    /**
38
     * The domain.
39
     *
40
     * @var string
41
     */
42
    private string $domain;
43
44
    /**
45
     * The URL to the server.
46
     *
47
     * @var string
48
     */
49
    private string $server;
50
51
    /**
52
     * Our shared key.
53
     *
54
     * @var string
55
     */
56
    private string $key;
57
58
59
    /**
60
     * The lifetime of our cookie, in seconds.
61
     *
62
     * If this is 0, the cookie will expire when the browser is closed.
63
     *
64
     * @var int
65
     */
66
    private int $cookieLifetime;
67
68
69
    /**
70
     * Initialize a CDC server.
71
     *
72
     * @param string $domain  The domain we are a server for.
73
     * @throws \SimpleSAML\Error\Exception
74
     */
75
    public function __construct(string $domain)
76
    {
77
        $cdcConfig = Configuration::getConfig('module_cdc.php');
78
        $config = $cdcConfig->getOptionalConfigItem($domain, null);
79
80
        if ($config === null) {
81
            throw new Error\Exception('Unknown CDC domain: ' . var_export($domain, true));
82
        }
83
84
        $this->domain = $domain;
85
        $this->server = $config->getString('server');
86
        $this->key = $config->getString('key');
87
        $this->cookieLifetime = $config->getOptionalInteger('cookie.lifetime', 0);
88
89
        if ($this->key === 'ExampleSharedKey') {
90
            throw new Error\Exception(
91
                'Key for CDC domain ' . var_export($domain, true) . ' not changed from default.',
92
            );
93
        }
94
    }
95
96
97
    /**
98
     * Send a request to this CDC server.
99
     *
100
     * @param array<mixed> $request  The CDC request.
101
     */
102
    public function sendRequest(array $request): void
103
    {
104
        Assert::keyExists($request, 'return');
105
        Assert::keyExists($request, 'op');
106
107
        $request['domain'] = $this->domain;
108
        $this->send($this->server, 'CDCRequest', $request);
109
    }
110
111
112
    /**
113
     * Parse and validate response received from a CDC server.
114
     *
115
     * @return array<mixed>|null  The response, or NULL if no response is received.
116
     * @throws \SimpleSAML\Error\Exception
117
     */
118
    public function getResponse(): ?array
119
    {
120
        $response = self::get('CDCResponse');
121
        if ($response === null) {
122
            return null;
123
        }
124
125
        if ($response['domain'] !== $this->domain) {
126
            throw new Error\Exception('Response received from wrong domain.');
127
        }
128
129
        $this->validate('CDCResponse');
130
131
        return $response;
132
    }
133
134
135
    /**
136
     * Parse and process a CDC request.
137
     * @throws \SimpleSAML\Error\BadRequest
138
     */
139
    public static function processRequest(): void
140
    {
141
        $request = self::get('CDCRequest');
142
        if ($request === null) {
143
            throw new Error\BadRequest('Missing "CDCRequest" parameter.');
144
        }
145
146
        $domain = $request['domain'];
147
        $server = new Server($domain);
148
149
        $server->validate('CDCRequest');
150
        $server->handleRequest($request);
151
    }
152
153
154
    /**
155
     * Handle a parsed CDC requst.
156
     *
157
     * @param array<mixed> $request
158
     * @throws \SimpleSAML\Error\Exception
159
     */
160
    private function handleRequest(array $request): void
161
    {
162
        if (!isset($request['op'])) {
163
            throw new Error\BadRequest('Missing "op" in CDC request.');
164
        }
165
        $op = strval($request['op']);
166
167
        Logger::info('Received CDC request with "op": ' . var_export($op, true));
168
169
        if (!isset($request['return'])) {
170
            throw new Error\BadRequest('Missing "return" in CDC request.');
171
        }
172
        $ret = strval($request['return']);
173
174
        switch ($op) {
175
            case 'append':
176
                $response = $this->handleAppend($request);
177
                break;
178
            case 'delete':
179
                $response = $this->handleDelete($request);
180
                break;
181
            case 'read':
182
                $response = $this->handleRead($request);
183
                break;
184
            default:
185
                $response = 'unknown-op';
186
        }
187
188
        if (is_string($response)) {
189
            $response = [
190
                'status' => $response,
191
            ];
192
        }
193
194
        $response['op'] = $op;
195
        if (isset($request['id'])) {
196
            $response['id'] = strval($request['id']);
197
        }
198
        $response['domain'] = $this->domain;
199
200
        $this->send($ret, 'CDCResponse', $response);
201
    }
202
203
204
    /**
205
     * Handle an append request.
206
     *
207
     * @param array<mixed> $request  The request.
208
     * @throws \SimpleSAML\Error\BadRequest
209
     * @return string The response.
210
     */
211
    private function handleAppend(array $request): string
212
    {
213
        if (!isset($request['entityID'])) {
214
            throw new Error\BadRequest('Missing entityID in append request.');
215
        }
216
        $entityID = (string) $request['entityID'];
217
218
        $list = $this->getCDC();
219
220
        $prevIndex = array_search($entityID, $list, true);
221
        if ($prevIndex !== false) {
222
            unset($list[$prevIndex]);
223
        }
224
        $list[] = $entityID;
225
226
        $this->setCDC($list);
227
228
        return 'ok';
229
    }
230
231
232
    /**
233
     * Handle a delete request.
234
     *
235
     * @param array<mixed> $request  The request.
236
     * @return string The response.
237
     */
238
    private function handleDelete(array $request): string
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

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

238
    private function handleDelete(/** @scrutinizer ignore-unused */ array $request): string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
239
    {
240
        $params = [
241
            'path' => '/',
242
            'domain' => '.' . $this->domain,
243
            'secure' => true,
244
            'httponly' => false,
245
        ];
246
247
        $httpUtils = new Utils\HTTP();
248
        $httpUtils->setCookie('_saml_idp', null, $params, false);
249
        return 'ok';
250
    }
251
252
253
    /**
254
     * Handle a read request.
255
     *
256
     * @param array<mixed> $request  The request.
257
     * @return array<mixed>  The response.
258
     */
259
    private function handleRead(array $request): array
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

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

259
    private function handleRead(/** @scrutinizer ignore-unused */ array $request): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
260
    {
261
        $list = $this->getCDC();
262
263
        return [
264
            'status' => 'ok',
265
            'cdc' => $list,
266
        ];
267
    }
268
269
270
    /**
271
     * Helper function for parsing and validating a CDC message.
272
     *
273
     * @param string $parameter  The name of the query parameter.
274
     * @throws \SimpleSAML\Error\BadRequest
275
     * @return array<mixed>|null  The response, or NULL if no response is received.
276
     */
277
    private static function get(string $parameter): ?array
278
    {
279
        if (!isset($_REQUEST[$parameter])) {
280
            return null;
281
        }
282
        $message = (string) $_REQUEST[$parameter];
283
        Assert::validBase64($message, ProtocolViolationException::class);
284
285
        $message = @base64_decode($message, true);
286
        if ($message === false) {
287
            throw new Error\BadRequest('Error base64-decoding CDC message.');
288
        }
289
290
        $message = @json_decode($message, true);
291
        if ($message === false) {
292
            throw new Error\BadRequest('Error json-decoding CDC message.');
293
        }
294
295
        if (!isset($message['timestamp'])) {
296
            throw new Error\BadRequest('Missing timestamp in CDC message.');
297
        }
298
        $timestamp = intval($message['timestamp']);
299
300
        if ($timestamp + 60 < time()) {
301
            throw new Error\BadRequest('CDC signature has expired.');
302
        }
303
        if ($timestamp - 60 > time()) {
304
            throw new Error\BadRequest('CDC signature from the future.');
305
        }
306
307
        if (!isset($message['domain'])) {
308
            throw new Error\BadRequest('Missing domain in CDC message.');
309
        }
310
311
        return $message;
312
    }
313
314
315
    /**
316
     * Helper function for validating the signature on a CDC message.
317
     *
318
     * Will throw an exception if the message is invalid.
319
     *
320
     * @param string $parameter  The name of the query parameter.
321
     * @throws \SimpleSAML\Error\BadRequest
322
     */
323
    private function validate(string $parameter): void
324
    {
325
        Assert::keyExists($_REQUEST, $parameter);
326
327
        $message = (string) $_REQUEST[$parameter];
328
329
        if (!isset($_REQUEST['Signature'])) {
330
            throw new Error\BadRequest('Missing Signature on CDC message.');
331
        }
332
        $signature = (string) $_REQUEST['Signature'];
333
334
        $cSignature = $this->calcSignature($message);
335
        if ($signature !== $cSignature) {
336
            throw new Error\BadRequest('Invalid signature on CDC message.');
337
        }
338
    }
339
340
341
    /**
342
     * Helper function for sending CDC messages.
343
     *
344
     * @param string $to  The URL the message should be delivered to.
345
     * @param string $parameter  The query parameter the message should be sent in.
346
     * @param array<mixed> $message  The CDC message.
347
     */
348
    private function send(string $to, string $parameter, array $message): void
349
    {
350
        $message['timestamp'] = time();
351
        $message = json_encode($message);
352
        $message = base64_encode($message);
353
354
        $signature = $this->calcSignature($message);
355
356
        $params = [
357
            $parameter => $message,
358
            'Signature' => $signature,
359
        ];
360
361
        $httpUtils = new Utils\HTTP();
362
        $url = $httpUtils->addURLParameters($to, $params);
363
        if (strlen($url) < 2048) {
364
            $httpUtils->redirectTrustedURL($url);
365
        } else {
366
            $httpUtils->submitPOSTData($to, $params);
367
        }
368
    }
369
370
371
    /**
372
     * Calculate the signature on the given message.
373
     *
374
     * @param string $rawMessage  The base64-encoded message.
375
     * @return string  The signature.
376
     */
377
    private function calcSignature(string $rawMessage): string
378
    {
379
        return sha1($this->key . $rawMessage . $this->key);
380
    }
381
382
383
    /**
384
     * Get the IdP entities saved in the common domain cookie.
385
     *
386
     * @return string[]  List of IdP entities.
387
     */
388
    private function getCDC(): array
389
    {
390
        if (!isset($_COOKIE['_saml_idp'])) {
391
            return [];
392
        }
393
394
        $ret = strval($_COOKIE['_saml_idp']);
395
396
        $ret = explode(' ', $ret);
397
        foreach ($ret as &$idp) {
398
            Assert::validBase64($idp, ProtocolViolationException::class);
399
            $idp = base64_decode($idp, true);
400
            if ($idp === false) {
401
                // Not properly base64 encoded
402
                Logger::warning('CDC - Invalid base64-encoding of CDC entry.');
403
                return [];
404
            }
405
            Assert::validURI($idp, ProtocolViolationException::class);
406
        }
407
408
        return $ret;
409
    }
410
411
412
    /**
413
     * Build a CDC cookie string.
414
     *
415
     * @param string[] $list  The list of IdPs.
416
     * @return string  The CDC cookie value.
417
     */
418
    private function setCDC(array $list): string
419
    {
420
        foreach ($list as &$value) {
421
            $value = base64_encode($value);
422
        }
423
424
        $cookie = implode(' ', $list);
425
426
        while (strlen($cookie) > 4000) {
427
            // The cookie is too long. Remove the oldest elements until it is short enough
428
            $tmp = explode(' ', $cookie, 2);
429
            if (count($tmp) === 1) {
430
                /*
431
                 * We are left with a single entityID whose base64
432
                 * representation is too long to fit in a cookie.
433
                 */
434
                break;
435
            }
436
            $cookie = $tmp[1];
437
        }
438
439
        $params = [
440
            'lifetime' => $this->cookieLifetime,
441
            'path' => '/',
442
            'domain' => '.' . $this->domain,
443
            'secure' => true,
444
            'httponly' => false,
445
        ];
446
447
        $httpUtils = new Utils\HTTP();
448
        $httpUtils->setCookie('_saml_idp', $cookie, $params, false);
449
450
        return '_saml_idp';
451
    }
452
}
453