1<?php 2/** 3 * This file is part of the FreeDSx LDAP package. 4 * 5 * (c) Chad Sikorra <Chad.Sikorra@gmail.com> 6 * 7 * For the full copyright and license information, please view the LICENSE 8 * file that was distributed with this source code. 9 */ 10 11namespace FreeDSx\Ldap\Protocol\ClientProtocolHandler; 12 13use FreeDSx\Ldap\Exception\ConnectionException; 14use FreeDSx\Ldap\Exception\OperationException; 15use FreeDSx\Ldap\Exception\ReferralException; 16use FreeDSx\Ldap\Exception\RuntimeException; 17use FreeDSx\Ldap\Exception\SkipReferralException; 18use FreeDSx\Ldap\LdapClient; 19use FreeDSx\Ldap\LdapUrl; 20use FreeDSx\Ldap\Operation\LdapResult; 21use FreeDSx\Ldap\Operation\Request\BindRequest; 22use FreeDSx\Ldap\Operation\Request\DnRequestInterface; 23use FreeDSx\Ldap\Operation\Request\RequestInterface; 24use FreeDSx\Ldap\Operation\Request\SearchRequest; 25use FreeDSx\Ldap\Operation\ResultCode; 26use FreeDSx\Ldap\Protocol\LdapMessageRequest; 27use FreeDSx\Ldap\Protocol\LdapMessageResponse; 28use FreeDSx\Ldap\Protocol\Queue\ClientQueue; 29use FreeDSx\Ldap\Protocol\ReferralContext; 30use FreeDSx\Ldap\ReferralChaserInterface; 31use FreeDSx\Ldap\Search\Filters; 32 33/** 34 * Logic for handling referrals. 35 * 36 * @author Chad Sikorra <Chad.Sikorra@gmail.com> 37 */ 38class ClientReferralHandler implements ResponseHandlerInterface 39{ 40 /** 41 * @var array 42 */ 43 protected $options = []; 44 45 /** 46 * {@inheritDoc} 47 */ 48 public function handleResponse(LdapMessageRequest $messageTo, LdapMessageResponse $messageFrom, ClientQueue $queue, array $options): ?LdapMessageResponse 49 { 50 $this->options = $options; 51 $result = $messageFrom->getResponse(); 52 switch ($this->options['referral']) { 53 case 'throw': 54 $message = $result instanceof LdapResult ? $result->getDiagnosticMessage() : 'Referral response encountered.'; 55 $referrals = $result instanceof LdapResult ? $result->getReferrals() : []; 56 57 throw new ReferralException($message, ...$referrals); 58 break; 59 case 'follow': 60 return $this->followReferral($messageTo, $messageFrom); 61 break; 62 default: 63 throw new RuntimeException(sprintf( 64 'The referral option "%s" is invalid.', 65 $this->options['referral'] 66 )); 67 } 68 } 69 70 protected function followReferral(LdapMessageRequest $messageTo, LdapMessageResponse $messageFrom): ?LdapMessageResponse 71 { 72 $referralChaser = $this->options['referral_chaser']; 73 if (!($referralChaser === null || $referralChaser instanceof ReferralChaserInterface)) { 74 throw new RuntimeException(sprintf( 75 'The referral_chaser must implement "%s" or be null.', 76 ReferralChaserInterface::class 77 )); 78 } 79 if (!$messageFrom->getResponse() instanceof LdapResult || \count($messageFrom->getResponse()->getReferrals()) === 0) { 80 throw new OperationException( 81 'Encountered a referral request, but no referrals were supplied.', 82 ResultCode::REFERRAL 83 ); 84 } 85 86 # Initialize a referral context to track the referrals we have already visited as well as count. 87 if (!isset($this->options['_referral_context'])) { 88 $this->options['_referral_context'] = new ReferralContext(); 89 } 90 91 foreach ($messageFrom->getResponse()->getReferrals() as $referral) { 92 # We must skip referrals we have already visited to avoid a referral loop 93 if ($this->options['_referral_context']->hasReferral($referral)) { 94 continue; 95 } 96 97 $this->options['_referral_context']->addReferral($referral); 98 if ($this->options['_referral_context']->count() > $this->options['referral_limit']) { 99 throw new OperationException(sprintf( 100 'The referral limit of %s has been reached.', 101 $this->options['referral_limit'] 102 )); 103 } 104 105 $bind = null; 106 try { 107 # @todo Remove the bind parameter from the interface in a future release. 108 if ($referralChaser !== null) { 109 $bind = $referralChaser->chase($messageTo, $referral, null); 110 } 111 } catch (SkipReferralException $e) { 112 continue; 113 } 114 $options = $this->options; 115 $options['servers'] = $referral->getHost() !== null ? [$referral->getHost()] : []; 116 $options['port'] = $referral->getPort() ?? 389; 117 $options['use_ssl'] = $referral->getUseSsl(); 118 119 # Each referral could potentially modify different aspects of the request, depending on the URL. Clone it 120 # here, merge the options, then use that request to send to LDAP. This makes sure we don't accidentally mix 121 # options from different referrals. 122 $request = clone $messageTo->getRequest(); 123 $this->mergeReferralOptions($request, $referral); 124 125 try { 126 $client = $referralChaser !== null ? $referralChaser->client($options) : new LdapClient($options); 127 128 # If we have a referral on a bind request, then do not bind initially. 129 # 130 # It's not clear that this should even be allowed, though RFC 4511 makes no indication that referrals 131 # should not be followed on a bind request. The problem is that while we bind on a different server, 132 # this client continues on with a different bind state, which seems confusing / problematic. 133 if ($bind !== null && !$messageTo->getRequest() instanceof BindRequest) { 134 $client->send($bind); 135 } 136 137 $response = $client->send($messageTo->getRequest(), ...$messageTo->controls()); 138 139 return $response; 140 # Skip referrals that fail due to connection issues and not other issues 141 } catch (ConnectionException $e) { 142 continue; 143 # If the referral encountered other referrals but exhausted them, continue to the next one. 144 } catch (OperationException $e) { 145 if ($e->getCode() === ResultCode::REFERRAL) { 146 continue; 147 } 148 # Other operation errors should bubble up, so throw it 149 throw $e; 150 } catch (\Throwable $e) { 151 throw $e; 152 } 153 } 154 155 # If we have exhausted all referrals consider it an operation exception. 156 throw new OperationException(sprintf( 157 'All referral attempts have been exhausted. %s', 158 $messageFrom->getResponse()->getDiagnosticMessage() 159 ), ResultCode::REFERRAL); 160 } 161 162 /** 163 * @param RequestInterface $request 164 * @param LdapUrl $referral 165 */ 166 protected function mergeReferralOptions(RequestInterface $request, LdapUrl $referral): void 167 { 168 if ($referral->getDn() !== null && $request instanceof SearchRequest) { 169 $request->setBaseDn($referral->getDn()); 170 } elseif ($referral->getDn() !== null && $request instanceof DnRequestInterface) { 171 $request->setDn($referral->getDn()); 172 } 173 174 if ($referral->getScope() !== null && $request instanceof SearchRequest) { 175 if ($referral->getScope() === LdapUrl::SCOPE_SUB) { 176 $request->setScope(SearchRequest::SCOPE_WHOLE_SUBTREE); 177 } elseif ($referral->getScope() === LdapUrl::SCOPE_BASE) { 178 $request->setScope(SearchRequest::SCOPE_SINGLE_LEVEL); 179 } else { 180 $request->setScope(SearchRequest::SCOPE_BASE_OBJECT); 181 } 182 } 183 184 if ($referral->getFilter() !== null && $request instanceof SearchRequest) { 185 $request->setFilter(Filters::raw($referral->getFilter())); 186 } 187 } 188} 189