Passed
Push — master ( 9c11f9...2a3f12 )
by Tim
02:42
created

Ldap   F

Complexity

Total Complexity 104

Size/Duplication

Total Lines 833
Duplicated Lines 0 %

Importance

Changes 6
Bugs 1 Features 0
Metric Value
eloc 312
c 6
b 1
f 0
dl 0
loc 833
rs 2
wmc 104

14 Methods

Rating   Name   Duplication   Size   Complexity  
A authzidToDn() 0 14 3
B search() 0 80 11
A searchfordn() 0 34 5
C makeException() 0 51 14
C __construct() 0 75 12
A whoami() 0 13 3
F searchformultiple() 0 99 20
B getAttributes() 0 65 9
B bind() 0 53 9
A asc2hex32() 0 13 4
A encodeIfBinary() 0 10 3
A validate() 0 37 5
A escapeFilterValue() 0 25 4
A setOption() 0 15 2

How to fix   Complexity   

Complex Class

Complex classes like Ldap 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 Ldap, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * The LDAP class holds helper functions to access an LDAP database.
5
 *
6
 * @author Andreas Aakre Solberg, UNINETT AS. <[email protected]>
7
 * @author Anders Lund, UNINETT AS. <[email protected]>
8
 * @package SimpleSAMLphp
9
 */
10
11
namespace SimpleSAML\Module\ldap\Auth;
12
13
use SimpleSAML\Error;
14
use SimpleSAML\Logger;
15
use SimpleSAML\Utils;
16
use Webmozart\Assert\Assert;
17
18
/**
19
 * Constants defining possible errors
20
 */
21
define('ERR_INTERNAL', 1);
22
define('ERR_NO_USER', 2);
23
define('ERR_WRONG_PW', 3);
24
define('ERR_AS_DATA_INCONSIST', 4);
25
define('ERR_AS_INTERNAL', 5);
26
define('ERR_AS_ATTRIBUTE', 6);
27
28
// not defined in earlier PHP versions
29
if (!defined('LDAP_OPT_DIAGNOSTIC_MESSAGE')) {
30
    define('LDAP_OPT_DIAGNOSTIC_MESSAGE', 0x0032);
31
}
32
33
class Ldap
34
{
35
    /**
36
     * LDAP link identifier.
37
     *
38
     * @var resource
39
     */
40
    protected $ldap;
41
42
    /**
43
     * LDAP user: authz_id if SASL is in use, binding dn otherwise
44
     *
45
     * @var string|null
46
     */
47
    protected $authz_id = null;
48
49
    /**
50
     * Timeout value, in seconds.
51
     *
52
     * @var int
53
     */
54
    protected $timeout = 0;
55
56
    /**
57
     * Private constructor restricts instantiation to getInstance().
58
     *
59
     * @param string $hostname
60
     * @param bool $enable_tls
61
     * @param bool $debug
62
     * @param int $timeout
63
     * @param int $port
64
     * @param bool $referrals
65
     * @psalm-suppress NullArgument
66
     */
67
    public function __construct(
68
        string $hostname,
69
        bool $enable_tls = true,
70
        bool $debug = false,
71
        int $timeout = 0,
72
        int $port = 389,
73
        bool $referrals = true
74
    ) {
75
        // Debug
76
        Logger::debug('Library - LDAP __construct(): Setup LDAP with ' .
77
            'host=\'' . $hostname .
78
            '\', tls=' . var_export($enable_tls, true) .
79
            ', debug=' . var_export($debug, true) .
80
            ', timeout=' . var_export($timeout, true) .
81
            ', referrals=' . var_export($referrals, true));
82
83
        /*
84
         * Set debug level before calling connect. Note that this passes
85
         * NULL to ldap_set_option, which is an undocumented feature.
86
         *
87
         * OpenLDAP 2.x.x or Netscape Directory SDK x.x needed for this option.
88
         */
89
        if ($debug && !ldap_set_option(null, LDAP_OPT_DEBUG_LEVEL, 7)) {
90
            Logger::warning('Library - LDAP __construct(): Unable to set debug level (LDAP_OPT_DEBUG_LEVEL) to 7');
91
        }
92
93
        /*
94
         * Prepare a connection for to this LDAP server. Note that this function
95
         * doesn't actually connect to the server.
96
         */
97
        $resource = @ldap_connect($hostname, $port);
98
        if ($resource === false) {
99
            throw $this->makeException(
100
                'Library - LDAP __construct(): Unable to connect to \'' . $hostname . '\'',
101
                ERR_INTERNAL
102
            );
103
        }
104
        $this->ldap = $resource;
105
106
        // Enable LDAP protocol version 3
107
        if (!@ldap_set_option($this->ldap, LDAP_OPT_PROTOCOL_VERSION, 3)) {
108
            throw $this->makeException(
109
                'Library - LDAP __construct(): Failed to set LDAP Protocol version (LDAP_OPT_PROTOCOL_VERSION) to 3',
110
                ERR_INTERNAL
111
            );
112
        }
113
114
        // Set referral option
115
        if (!@ldap_set_option($this->ldap, LDAP_OPT_REFERRALS, $referrals)) {
116
            throw $this->makeException(
117
                'Library - LDAP __construct(): Failed to set LDAP Referrals (LDAP_OPT_REFERRALS) to ' . $referrals,
118
                ERR_INTERNAL
119
            );
120
        }
121
122
        // Set timeouts, if supported
123
        // (OpenLDAP 2.x.x or Netscape Directory SDK x.x needed)
124
        $this->timeout = $timeout;
125
        if ($timeout > 0) {
126
            if (!@ldap_set_option($this->ldap, LDAP_OPT_NETWORK_TIMEOUT, $timeout)) {
127
                Logger::warning(
128
                    'Library - LDAP __construct(): Unable to set timeouts (LDAP_OPT_NETWORK_TIMEOUT) to ' . $timeout
129
                );
130
            }
131
            if (!@ldap_set_option($this->ldap, LDAP_OPT_TIMELIMIT, $timeout)) {
132
                Logger::warning(
133
                    'Library - LDAP __construct(): Unable to set timeouts (LDAP_OPT_TIMELIMIT) to ' . $timeout
134
                );
135
            }
136
        }
137
138
        // Enable TLS, if needed
139
        if (stripos($hostname, "ldaps:") === false && $enable_tls) {
140
            if (!@ldap_start_tls($this->ldap)) {
141
                throw $this->makeException('Library - LDAP __construct():' . ' Unable to force TLS', ERR_INTERNAL);
142
            }
143
        }
144
    }
145
146
147
    /**
148
     * Convenience method to create an LDAPException as well as log the
149
     * description.
150
     *
151
     * @param string $description The exception's description
152
     * @param int|null $type The exception's type
153
     * @return \Exception
154
     */
155
    private function makeException(string $description, int $type = null): \Exception
156
    {
157
        $errNo = @ldap_errno($this->ldap);
158
159
        // Decide exception type and return
160
        if ($type !== null) {
161
            if ($errNo !== 0) {
162
                // Only log real LDAP errors; not success
163
                Logger::error($description . '; cause: \'' . ldap_error($this->ldap) . '\' (0x' . dechex($errNo) . ')');
164
            } else {
165
                Logger::error($description);
166
            }
167
168
            switch ($type) {
169
                case ERR_INTERNAL:// 1 - ExInternal
170
                    return new Error\Exception($description, $errNo);
171
                case ERR_NO_USER:// 2 - ExUserNotFound
172
                    return new Error\UserNotFound($description, $errNo);
173
                case ERR_WRONG_PW:// 3 - ExInvalidCredential
174
                    return new Error\InvalidCredential($description, $errNo);
175
                case ERR_AS_DATA_INCONSIST:// 4 - ExAsDataInconsist
176
                    return new Error\AuthSource('ldap', $description);
177
                case ERR_AS_INTERNAL:// 5 - ExAsInternal
178
                    return new Error\AuthSource('ldap', $description);
179
            }
180
        } else {
181
            if ($errNo !== 0) {
182
                $description .= '; cause: \'' . ldap_error($this->ldap) . '\' (0x' . dechex($errNo) . ')';
183
                if (
184
                    @ldap_get_option($this->ldap, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extendedError)
185
                    && !empty($extendedError)
186
                ) {
187
                    $description .= '; additional: \'' . $extendedError . '\'';
188
                }
189
            }
190
            switch ($errNo) {
191
                case 0x20://LDAP_NO_SUCH_OBJECT
192
                    Logger::warning($description);
193
                    return new Error\UserNotFound($description, $errNo);
194
                case 0x31://LDAP_INVALID_CREDENTIALS
195
                    Logger::info($description);
196
                    return new Error\InvalidCredential($description, $errNo);
197
                case -1://NO_SERVER_CONNECTION
198
                    Logger::error($description);
199
                    return new Error\AuthSource('ldap', $description);
200
                default:
201
                    Logger::error($description);
202
                    return new Error\AuthSource('ldap', $description);
203
            }
204
        }
205
        return new \Exception('Unknown LDAP error.');
206
    }
207
208
209
    /**
210
     * Search for DN from a single base.
211
     *
212
     * @param string $base
213
     * Indication of root of subtree to search
214
     * @param string|array $attribute
215
     * The attribute name(s) to search for.
216
     * @param string $value
217
     * The attribute value to search for.
218
     * Additional search filter
219
     * @param string|null $searchFilter
220
     * The scope of the search
221
     * @param string $scope
222
     * @return string
223
     * The DN of the resulting found element.
224
     * @throws Error\Exception if:
225
     * - Attribute parameter is wrong type
226
     * @throws Error\AuthSource if:
227
     * - Not able to connect to LDAP server
228
     * - False search result
229
     * - Count return false
230
     * - Searche found more than one result
231
     * - Failed to get first entry from result
232
     * - Failed to get DN for entry
233
     * @throws Error\UserNotFound if:
234
     * - Zero entries were found
235
     * @psalm-suppress TypeDoesNotContainType
236
     */
237
    private function search(
238
        string $base,
239
        $attribute,
240
        string $value,
241
        ?string $searchFilter = null,
242
        string $scope = "subtree"
243
    ): string {
244
        // Create the search filter
245
        /** @var array $attribute */
246
        $attribute = self::escapeFilterValue($attribute, false);
247
248
        /** @var string $value */
249
        $value = self::escapeFilterValue($value, true);
250
251
        $filter = '';
252
        foreach ($attribute as $attr) {
253
            $filter .= '(' . $attr . '=' . $value . ')';
254
        }
255
        $filter = '(|' . $filter . ')';
256
257
        // Append LDAP filters if defined
258
        if ($searchFilter !== null) {
259
            $filter = "(&" . $filter . "" . $searchFilter . ")";
260
        }
261
262
        // Search using generated filter
263
        Logger::debug(
264
            'Library - LDAP search(): Searching base (' . $scope . ') \'' . $base . '\' for \'' . $filter . '\''
265
        );
266
        if ($scope === 'base') {
267
            $result = @ldap_read($this->ldap, $base, $filter, [], 0, 0, $this->timeout, LDAP_DEREF_NEVER);
268
        } elseif ($scope === 'onelevel') {
269
            $result = @ldap_list($this->ldap, $base, $filter, [], 0, 0, $this->timeout, LDAP_DEREF_NEVER);
270
        } else {
271
            $result = @ldap_search($this->ldap, $base, $filter, [], 0, 0, $this->timeout, LDAP_DEREF_NEVER);
272
        }
273
274
        if ($result === false) {
275
            throw $this->makeException(
276
                'Library - LDAP search(): Failed search on base \'' . $base . '\' for \'' . $filter . '\''
277
            );
278
        }
279
280
        // Sanity checks on search results
281
        $count = @ldap_count_entries($this->ldap, $result);
282
        if ($count === false) {
283
            throw $this->makeException('Library - LDAP search(): Failed to get number of entries returned');
284
        } elseif ($count > 1) {
285
            // More than one entry is found. External error
286
            throw $this->makeException(
287
                'Library - LDAP search(): Found ' . $count . ' entries searching base \'' . $base .
288
                    '\' for \'' . $filter . '\'',
289
                ERR_AS_DATA_INCONSIST
290
            );
291
        } elseif ($count === 0) {
292
            // No entry is fond => wrong username is given (or not registered in the catalogue). User error
293
            throw $this->makeException(
294
                'Library - LDAP search(): Found no entries searching base \'' . $base .
295
                    '\' for \'' . $filter . '\'',
296
                ERR_NO_USER
297
            );
298
        }
299
300
301
        // Resolve the DN from the search result
302
        $entry = @ldap_first_entry($this->ldap, $result);
303
        if ($entry === false) {
304
            throw $this->makeException(
305
                'Library - LDAP search(): Unable to retrieve result after searching base \'' .
306
                    $base . '\' for \'' . $filter . '\''
307
            );
308
        }
309
        $dn = @ldap_get_dn($this->ldap, $entry);
310
        if ($dn === false) {
311
            throw $this->makeException(
312
                'Library - LDAP search(): Unable to get DN after searching base \'' . $base .
313
                    '\' for \'' . $filter . '\''
314
            );
315
        }
316
        return $dn;
317
    }
318
319
320
    /**
321
     * Search for a DN.
322
     *
323
     * @param string|array $base
324
     * The base, or bases, which to search from.
325
     * @param string|array $attribute
326
     * The attribute name(s) searched for.
327
     * @param string $value
328
     * The attribute value searched for.
329
     * @param bool $allowZeroHits
330
     * Determines if the method will throw an exception if no hits are found.
331
     * Defaults to FALSE.
332
     * @param string|null $searchFilter
333
     * Additional searchFilter to be added to the (attribute=value) filter
334
     * @param string $scope
335
     * The scope of the search
336
     * @return string|null
337
     * The DN of the matching element, if found. If no element was found and
338
     * $allowZeroHits is set to FALSE, an exception will be thrown; otherwise
339
     * NULL will be returned.
340
     * @throws Error\AuthSource if:
341
     * - LDAP search encounter some problems when searching cataloge
342
     * - Not able to connect to LDAP server
343
     * @throws Error\UserNotFound if:
344
     * - $allowZeroHits is FALSE and no result is found
345
     *
346
     */
347
    public function searchfordn(
348
        $base,
349
        $attribute,
350
        string $value,
351
        bool $allowZeroHits = false,
352
        ?string $searchFilter = null,
353
        string $scope = 'subtree'
354
    ): ?string {
355
        // Traverse all search bases, returning DN if found
356
        $bases = Utils\Arrays::arrayize($base);
357
        foreach ($bases as $current) {
358
            try {
359
                // Single base search
360
                $result = $this->search($current, $attribute, $value, $searchFilter, $scope);
361
362
                // We don't hawe to look any futher if user is found
363
                if (!empty($result)) {
364
                    return $result;
365
                }
366
                // If search failed, attempt the other base DNs
367
            } catch (Error\UserNotFound $e) {
368
                // Just continue searching
369
            }
370
        }
371
        // Decide what to do for zero entries
372
        Logger::debug('Library - LDAP searchfordn(): No entries found');
373
        if ($allowZeroHits) {
374
            // Zero hits allowed
375
            return null;
376
        } else {
377
            // Zero hits not allowed
378
            throw $this->makeException('Library - LDAP searchfordn(): LDAP search returned zero entries for' .
379
                ' filter \'(' . join(' | ', Utils\Arrays::arrayize($attribute)) .
380
                ' = ' . $value . ')\' on base(s) \'(' . join(' & ', $bases) . ')\'', 2);
381
        }
382
    }
383
384
385
    /**
386
     * This method was created specifically for the ldap:AttributeAddUsersGroups->searchActiveDirectory()
387
     * method, but could be used for other LDAP search needs. It will search LDAP and return all the entries.
388
     *
389
     * @throws \Exception
390
     * @param string|array $bases
391
     * @param string|array $filters Array of 'attribute' => 'values' to be combined into the filter,
392
     *     or a raw filter string
393
     * @param string|array $attributes Array of attributes requested from LDAP
394
     * @param bool $and If multiple filters defined, then either bind them with & or |
395
     * @param bool $escape Weather to escape the filter values or not
396
     * @param string $scope The scope of the search
397
     * @return array
398
     */
399
    public function searchformultiple(
400
        $bases,
401
        $filters,
402
        $attributes = [],
403
        bool $and = true,
404
        bool $escape = true,
405
        string $scope = 'subtree'
406
    ): array {
407
        // Escape the filter values, if requested
408
        if ($escape) {
409
            $filters = $this->escapeFilterValue($filters, false);
410
        }
411
412
        // Build search filter
413
        $filter = '';
414
        if (is_array($filters)) {
415
            foreach ($filters as $attribute => $value) {
416
                $filter .= "($attribute=$value)";
417
            }
418
            if (count($filters) > 1) {
419
                $filter = ($and ? '(&' : '(|') . $filter . ')';
420
            }
421
        } else {
422
            /** @psalm-suppress RedundantConditionGivenDocblockType */
423
            Assert::string($filters);
424
            $filter = $filters;
425
        }
426
427
        // Verify filter was created
428
        if ($filter == '' || $filter == '(=)') {
429
            throw $this->makeException('ldap:LdapConnection->search_manual : No search filters defined', ERR_INTERNAL);
430
        }
431
432
        // Verify at least one base was passed
433
        $bases = (array) $bases;
434
        if (empty($bases)) {
435
            throw $this->makeException('ldap:LdapConnection->search_manual : No base DNs were passed', ERR_INTERNAL);
436
        }
437
438
        $attributes = Utils\Arrays::arrayize($attributes);
439
440
        // Search each base until result is found
441
        $result = false;
442
        foreach ($bases as $base) {
443
            if ($scope === 'base') {
444
                $result = @ldap_read($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
445
            } elseif ($scope === 'onelevel') {
446
                $result = @ldap_list($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
447
            } else {
448
                $result = @ldap_search($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
449
            }
450
451
            if ($result !== false && @ldap_count_entries($this->ldap, $result) > 0) {
452
                break;
453
            }
454
        }
455
456
        // Verify that a result was found in one of the bases
457
        if ($result === false) {
458
            throw $this->makeException(
459
                'ldap:LdapConnection->search_manual : Failed to search LDAP using base(s) [' .
460
                implode('; ', $bases) . '] with filter [' . $filter . ']. LDAP error [' .
461
                ldap_error($this->ldap) . ']'
462
            );
463
        } elseif (@ldap_count_entries($this->ldap, $result) < 1) {
464
            throw $this->makeException(
465
                'ldap:LdapConnection->search_manual : No entries found in LDAP using base(s) [' .
466
                implode('; ', $bases) . '] with filter [' . $filter . ']',
467
                ERR_NO_USER
468
            );
469
        }
470
471
        // Get all results
472
        $results = ldap_get_entries($this->ldap, $result);
473
        if ($results === false) {
474
            throw $this->makeException(
475
                'ldap:LdapConnection->search_manual : Unable to retrieve entries from search results'
476
            );
477
        }
478
479
        // parse each entry and process its attributes
480
        for ($i = 0; $i < $results['count']; $i++) {
481
            $entry = $results[$i];
482
483
            // iterate over the attributes of the entry
484
            for ($j = 0; $j < $entry['count']; $j++) {
485
                $name = $entry[$j];
486
                $attribute = $entry[$name];
487
488
                // decide whether to base64 encode or not
489
                for ($k = 0; $k < $attribute['count']; $k++) {
490
                    $results[$i][$name][$k] = $this->encodeIfBinary($attribute[$k]);
491
                }
492
            }
493
        }
494
495
        // Remove the count and return
496
        unset($results['count']);
497
        return $results;
498
    }
499
500
501
    /**
502
     * Bind to LDAP with a specific DN and password. Simple wrapper around
503
     * ldap_bind() with some additional logging.
504
     *
505
     * @param string $dn
506
     * The DN used.
507
     * @param string $password
508
     * The password used.
509
     * @param array $sasl_args
510
     * Array of SASL options for SASL bind
511
     * @return bool
512
     * Returns TRUE if successful, FALSE if
513
     * LDAP_INVALID_CREDENTIALS, LDAP_X_PROXY_AUTHZ_FAILURE,
514
     * LDAP_INAPPROPRIATE_AUTH, LDAP_INSUFFICIENT_ACCESS
515
     * @throws Error\Exception on other errors
516
     */
517
    public function bind(string $dn, string $password, array $sasl_args = null): ?bool
518
    {
519
        if ($sasl_args != null) {
520
            if (!function_exists('ldap_sasl_bind')) {
521
                $ex_msg = 'Library - missing SASL support';
522
                throw $this->makeException($ex_msg);
523
            }
524
525
            // SASL Bind, with error handling
526
            $authz_id = $sasl_args['authz_id'];
527
            $error = @ldap_sasl_bind(
528
                $this->ldap,
529
                $dn,
530
                $password,
531
                $sasl_args['mech'],
532
                $sasl_args['realm'],
533
                $sasl_args['authc_id'],
534
                $sasl_args['authz_id'],
535
                $sasl_args['props']
536
            );
537
        } else {
538
            // Simple Bind, with error handling
539
            $authz_id = $dn;
540
            $error = @ldap_bind($this->ldap, $dn, $password);
541
        }
542
543
        if ($error === true) {
544
            // Good
545
            $this->authz_id = $authz_id;
546
            Logger::debug('Library - LDAP bind(): Bind successful with DN \'' . $dn . '\'');
547
            return true;
548
        }
549
550
        /* Handle errors
551
         * LDAP_INVALID_CREDENTIALS
552
         * LDAP_INSUFFICIENT_ACCESS */
553
        switch (ldap_errno($this->ldap)) {
554
            case 32: // LDAP_NO_SUCH_OBJECT
555
                // no break
556
            case 47: // LDAP_X_PROXY_AUTHZ_FAILURE
557
                // no break
558
            case 48: // LDAP_INAPPROPRIATE_AUTH
559
                // no break
560
            case 49: // LDAP_INVALID_CREDENTIALS
561
                // no break
562
            case 50: // LDAP_INSUFFICIENT_ACCESS
563
                return false;
564
            default:
565
                break;
566
        }
567
568
        // Bad
569
        throw $this->makeException('Library - LDAP bind(): Bind failed with DN \'' . $dn . '\'');
570
    }
571
572
573
    /**
574
     * Applies an LDAP option to the current connection.
575
     *
576
     * @throws \Exception
577
     * @param mixed $option
578
     * @param mixed $value
579
     * @return void
580
     */
581
    public function setOption($option, $value): void
582
    {
583
        // Attempt to set the LDAP option
584
        if (!@ldap_set_option($this->ldap, $option, $value)) {
585
            throw $this->makeException(
586
                'ldap:LdapConnection->setOption : Failed to set LDAP option [' .
587
                $option . '] with the value [' . $value . '] error: ' . ldap_error($this->ldap),
588
                ERR_INTERNAL
589
            );
590
        }
591
592
        // Log debug message
593
        Logger::debug(
594
            'ldap:LdapConnection->setOption : Set the LDAP option [' .
595
            $option . '] with the value [' . $value . ']'
596
        );
597
    }
598
599
600
    /**
601
     * Search a given DN for attributes, and return the resulting associative
602
     * array.
603
     *
604
     * @param string $dn
605
     * The DN of an element.
606
     * @param string|array $attributes
607
     * The names of the attribute(s) to retrieve. Defaults to NULL; that is,
608
     * all available attributes. Note that this is not very effective.
609
     * @param int $maxsize
610
     * The maximum size of any attribute's value(s). If exceeded, the attribute
611
     * will not be returned.
612
     * @return array
613
     * The array of attributes and their values.
614
     * @see http://no.php.net/manual/en/function.ldap-read.php
615
     */
616
    public function getAttributes(string $dn, $attributes = null, int $maxsize = null): array
617
    {
618
        // Preparations, including a pretty debug message...
619
        $description = 'all attributes';
620
        if (is_array($attributes)) {
621
            $description = '\'' . join(',', $attributes) . '\'';
622
        } else {
623
            // Get all attributes...
624
            // TODO: Verify that this originally was the intended behaviour. Could $attributes be a string?
625
            $attributes = [];
626
        }
627
        Logger::debug('Library - LDAP getAttributes(): Getting ' . $description . ' from DN \'' . $dn . '\'');
628
629
        // Attempt to get attributes
630
        // TODO: Should aliases be dereferenced?
631
        $result = @ldap_read($this->ldap, $dn, 'objectClass=*', $attributes, 0, 0, $this->timeout);
632
        if ($result === false) {
633
            throw $this->makeException(
634
                'Library - LDAP getAttributes(): Failed to get attributes from DN \'' . $dn . '\''
635
            );
636
        }
637
        $entry = @ldap_first_entry($this->ldap, $result);
638
        if ($entry === false) {
639
            throw $this->makeException(
640
                'Library - LDAP getAttributes(): Could not get first entry from DN \'' . $dn . '\''
641
            );
642
        }
643
        unset($attributes);
644
645
        $attributes = @ldap_get_attributes($this->ldap, $entry);
646
        if ($attributes === false) {
647
            throw $this->makeException(
648
                'Library - LDAP getAttributes(): Could not get attributes of first entry from DN \'' . $dn . '\''
649
            );
650
        }
651
652
        // Parsing each found attribute into our result set
653
        $result = []; // Recycling $result... Possibly bad practice.
654
        for ($i = 0; $i < $attributes['count']; $i++) {
655
            // Ignore attributes that exceed the maximum allowed size
656
            $name = $attributes[$i];
657
            $attribute = $attributes[$name];
658
659
            // Deciding whether to base64 encode
660
            $values = [];
661
            for ($j = 0; $j < $attribute['count']; $j++) {
662
                $value = $attribute[$j];
663
664
                if (!empty($maxsize) && strlen($value) > $maxsize) {
665
                    // Ignoring and warning
666
                    Logger::warning('Library - LDAP getAttributes(): Attribute \'' .
667
                        $name . '\' exceeded maximum allowed size by ' . (strlen($value) - $maxsize));
668
                    continue;
669
                }
670
671
                $values[] = $this->encodeIfBinary($value);
672
            }
673
674
            // Adding
675
            $result[$name] = $values;
676
        }
677
678
        // We're done
679
        Logger::debug('Library - LDAP getAttributes(): Found attributes \'(' . join(',', array_keys($result)) . ')\'');
680
        return $result;
681
    }
682
683
684
    /**
685
     * Enter description here...
686
     *
687
     * @param array $config
688
     * @param string $username
689
     * @param string|null $password
690
     * @return array|false
691
     */
692
    public function validate(array $config, string $username, string $password = null)
693
    {
694
        /**
695
         * Escape any characters with a special meaning in LDAP. The following
696
         * characters have a special meaning (according to RFC 2253):
697
         * ',', '+', '"', '\', '<', '>', ';', '*'
698
         * These characters are escaped by prefixing them with '\'.
699
         */
700
        $username = addcslashes($username, ',+"\\<>;*');
701
702
        if (isset($config['priv_user_dn'])) {
703
            $this->bind($config['priv_user_dn'], $config['priv_user_pw']);
704
        }
705
        if (isset($config['dnpattern'])) {
706
            $dn = str_replace('%username%', $username, $config['dnpattern']);
707
        } else {
708
            /** @var string $dn */
709
            $dn = $this->searchfordn($config['searchbase'], $config['searchattributes'], $username, false);
710
        }
711
712
        if ($password !== null) {
713
            // checking users credentials ... assuming below that she may read her own attributes ...
714
            // escape characters with a special meaning, also in the password
715
            $password = addcslashes($password, ',+"\\<>;*');
716
            if (!$this->bind($dn, $password)) {
717
                Logger::info(
718
                    'Library - LDAP validate(): Failed to authenticate \'' . $username . '\' using DN \'' . $dn . '\''
719
                );
720
                return false;
721
            }
722
        }
723
724
        /**
725
         * Retrieve attributes from LDAP
726
         */
727
        $attributes = $this->getAttributes($dn, $config['attributes']);
728
        return $attributes;
729
    }
730
731
732
    /**
733
     * Borrowed function from PEAR:LDAP.
734
     *
735
     * Escapes the given VALUES according to RFC 2254 so that they can be safely used in LDAP filters.
736
     *
737
     * Any control characters with an ACII code < 32 as well as the characters with special meaning in
738
     * LDAP filters "*", "(", ")", and "\" (the backslash) are converted into the representation of a
739
     * backslash followed by two hex digits representing the hexadecimal value of the character.
740
     *
741
     * @static
742
     * @param string|array $values Array of values to escape
743
     * @param bool $singleValue
744
     * @return string|array Array $values, but escaped
745
     */
746
    public static function escapeFilterValue($values = [], bool $singleValue = true)
747
    {
748
        // Parameter validation
749
        $values = Utils\Arrays::arrayize($values);
750
751
        foreach ($values as $key => $val) {
752
            if ($val === null) {
753
                $val = '\0'; // apply escaped "null" if string is empty
754
            } else {
755
                // Escaping of filter meta characters
756
                $val = str_replace('\\', '\5c', $val);
757
                $val = str_replace('*', '\2a', $val);
758
                $val = str_replace('(', '\28', $val);
759
                $val = str_replace(')', '\29', $val);
760
761
                // ASCII < 32 escaping
762
                $val = self::asc2hex32($val);
763
            }
764
765
            $values[$key] = $val;
766
        }
767
        if ($singleValue) {
768
            return $values[0];
769
        }
770
        return $values;
771
    }
772
773
774
    /**
775
     * Borrowed function from PEAR:LDAP.
776
     *
777
     * Converts all ASCII chars < 32 to "\HEX"
778
     *
779
     * @param string $string String to convert
780
     *
781
     * @static
782
     * @return string
783
     */
784
    public static function asc2hex32(string $string): string
785
    {
786
        for ($i = 0; $i < strlen($string); $i++) {
787
            $char = substr($string, $i, 1);
788
            if (ord($char) < 32) {
789
                $hex = dechex(ord($char));
790
                if (strlen($hex) == 1) {
791
                    $hex = '0' . $hex;
792
                }
793
                $string = str_replace($char, '\\' . $hex, $string);
794
            }
795
        }
796
        return $string;
797
    }
798
799
800
    /**
801
     * Convert SASL authz_id into a DN
802
     *
803
     * @param string $searchBase
804
     * @param array $searchAttributes
805
     * @param string $authz_id
806
     * @return string|null
807
     */
808
    private function authzidToDn(string $searchBase, array $searchAttributes, string $authz_id): ?string
809
    {
810
        if (preg_match("/^dn:/", $authz_id)) {
811
            return preg_replace("/^dn:/", "", $authz_id);
812
        }
813
814
        if (preg_match("/^u:/", $authz_id)) {
815
            return $this->searchfordn(
816
                $searchBase,
817
                $searchAttributes,
818
                preg_replace("/^u:/", "", $authz_id)
819
            );
820
        }
821
        return $authz_id;
822
    }
823
824
825
    /**
826
     * ldap_exop_whoami accessor, if available. Use requested authz_id
827
     * otherwise.
828
     *
829
     * @param string $searchBase
830
     * @param array $searchAttributes
831
     * @throws \Exception
832
     * @return string
833
     */
834
    public function whoami(string $searchBase, array $searchAttributes): string
835
    {
836
        $authz_id = ldap_exop_whoami($this->ldap);
1 ignored issue
show
Bug introduced by
The function ldap_exop_whoami was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

836
        $authz_id = /** @scrutinizer ignore-call */ ldap_exop_whoami($this->ldap);
Loading history...
837
        if ($authz_id === false) {
838
            throw $this->makeException('LDAP whoami exop failure');
839
        }
840
841
        $dn = $this->authzidToDn($searchBase, $searchAttributes, $authz_id);
842
        if (empty($dn)) {
843
            throw $this->makeException('Cannot figure userID');
844
        }
845
846
        return $dn;
847
    }
848
849
850
    /**
851
     * Base64 encode binary attributes, or pass original content back
852
     *
853
     * @param string $value  Possibly binary string
854
     * @return string  Value safe for use in SAML attributes (base64 encoded)
855
     */
856
    private function encodeIfBinary(string $value): string
857
    {
858
        // detect binary values
859
        if (
860
            mb_detect_encoding($value) === false
861
            || preg_match('~[^\x20-\x7E\t\r\n]~', $value) > 0
862
        ) {
863
            return base64_encode($value);
864
        }
865
        return $value;
866
    }
867
}
868