1<?php 2 3/** 4 * This file is part of the FreeDSx LDAP package. 5 * 6 * (c) Chad Sikorra <Chad.Sikorra@gmail.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace FreeDSx\Ldap\Protocol\ServerProtocolHandler; 13 14use FreeDSx\Ldap\Control\Control; 15use FreeDSx\Ldap\Control\ControlBag; 16use FreeDSx\Ldap\Control\PagingControl; 17use FreeDSx\Ldap\Entry\Entries; 18use FreeDSx\Ldap\Exception\OperationException; 19use FreeDSx\Ldap\Exception\ProtocolException; 20use FreeDSx\Ldap\Operation\ResultCode; 21use FreeDSx\Ldap\Protocol\LdapMessageRequest; 22use FreeDSx\Ldap\Protocol\Queue\ServerQueue; 23use FreeDSx\Ldap\Server\Paging\PagingRequest; 24use FreeDSx\Ldap\Server\Paging\PagingRequestComparator; 25use FreeDSx\Ldap\Server\Paging\PagingResponse; 26use FreeDSx\Ldap\Server\RequestContext; 27use FreeDSx\Ldap\Server\RequestHandler\PagingHandlerInterface; 28use FreeDSx\Ldap\Server\RequestHandler\RequestHandlerInterface; 29use FreeDSx\Ldap\Server\RequestHistory; 30use FreeDSx\Ldap\Server\Token\TokenInterface; 31use Throwable; 32 33/** 34 * Handles paging search request logic. 35 * 36 * @author Chad Sikorra <Chad.Sikorra@gmail.com> 37 */ 38class ServerPagingHandler implements ServerProtocolHandlerInterface 39{ 40 use ServerSearchTrait; 41 42 /** 43 * @var PagingHandlerInterface 44 */ 45 private $pagingHandler; 46 47 /** 48 * @var RequestHistory 49 */ 50 private $requestHistory; 51 52 /** 53 * @var PagingRequestComparator 54 */ 55 private $requestComparator; 56 57 public function __construct( 58 PagingHandlerInterface $pagingHandler, 59 RequestHistory $requestHistory, 60 ?PagingRequestComparator $requestComparator = null 61 ) { 62 $this->pagingHandler = $pagingHandler; 63 $this->requestHistory = $requestHistory; 64 $this->requestComparator = $requestComparator ?? new PagingRequestComparator(); 65 } 66 67 /** 68 * @inheritDoc 69 * @throws ProtocolException 70 */ 71 public function handleRequest( 72 LdapMessageRequest $message, 73 TokenInterface $token, 74 RequestHandlerInterface $dispatcher, 75 ServerQueue $queue, 76 array $options 77 ): void { 78 $context = new RequestContext( 79 $message->controls(), 80 $token 81 ); 82 $pagingRequest = $this->findOrMakePagingRequest($message); 83 84 $response = $this->handlePaging( 85 $context, 86 $pagingRequest, 87 $message 88 ); 89 90 $pagingRequest->markProcessed(); 91 92 if ($response->isComplete()) { 93 $this->requestHistory->pagingRequest() 94 ->remove($pagingRequest); 95 $this->pagingHandler->remove( 96 $pagingRequest, 97 $context 98 ); 99 } 100 101 $this->sendEntriesToClient( 102 $response->getEntries(), 103 $message, 104 $queue, 105 new PagingControl( 106 $response->getRemaining(), 107 $response->isComplete() ? '' : $pagingRequest->getNextCookie() 108 ) 109 ); 110 } 111 112 /** 113 * @throws OperationException 114 */ 115 private function handlePaging( 116 RequestContext $context, 117 PagingRequest $pagingRequest, 118 LdapMessageRequest $message 119 ): PagingResponse { 120 if (!$pagingRequest->isPagingStart()) { 121 return $this->handleExistingCookie( 122 $pagingRequest, 123 $context, 124 $message 125 ); 126 } else { 127 return $this->handlePagingStart( 128 $pagingRequest, 129 $context 130 ); 131 } 132 } 133 134 /** 135 * @throws OperationException 136 */ 137 private function handleExistingCookie( 138 PagingRequest $pagingRequest, 139 RequestContext $context, 140 LdapMessageRequest $message 141 ): PagingResponse { 142 $newPagingRequest = $this->makePagingRequest($message); 143 144 if (!$this->requestComparator->compare($pagingRequest, $newPagingRequest)) { 145 throw new OperationException( 146 'The search request and controls must be identical between paging requests.', 147 ResultCode::OPERATIONS_ERROR 148 ); 149 } 150 151 $pagingRequest->updatePagingControl($this->getPagingControlFromMessage($message)); 152 153 if ($pagingRequest->isAbandonRequest()) { 154 $response = PagingResponse::makeFinal(new Entries()); 155 } else { 156 $response = $this->pagingHandler->page( 157 $pagingRequest, 158 $context 159 ); 160 $pagingRequest->updateNextCookie($this->generateCookie()); 161 } 162 163 return $response; 164 } 165 166 /** 167 * @todo It would be useful to prefix these by a unique client ID or something else somewhat identifiable. 168 * @return string 169 * @throws OperationException 170 */ 171 private function generateCookie(): string 172 { 173 try { 174 return random_bytes(16); 175 } catch (Throwable $e) { 176 throw new OperationException( 177 'Internal server error.', 178 ResultCode::OPERATIONS_ERROR 179 ); 180 } 181 } 182 183 /** 184 * @param LdapMessageRequest $message 185 * @return PagingRequest 186 * @throws OperationException 187 * @throws ProtocolException 188 */ 189 private function findOrMakePagingRequest(LdapMessageRequest $message): PagingRequest 190 { 191 $pagingControl = $this->getPagingControlFromMessage($message); 192 193 if ($pagingControl->getCookie() !== '') { 194 return $this->findPagingRequestOrThrow($pagingControl->getCookie()); 195 } 196 197 $pagingRequest = $this->makePagingRequest($message); 198 $this->requestHistory->pagingRequest()->add($pagingRequest); 199 200 return $pagingRequest; 201 } 202 203 /** 204 * @param LdapMessageRequest $message 205 * @return PagingRequest 206 * @throws OperationException 207 */ 208 private function makePagingRequest(LdapMessageRequest $message): PagingRequest 209 { 210 $request = $this->getSearchRequestFromMessage($message); 211 $pagingControl = $this->getPagingControlFromMessage($message); 212 213 $filteredControls = array_filter( 214 $message->controls()->toArray(), 215 function (Control $control) { 216 return $control->getTypeOid() !== Control::OID_PAGING; 217 } 218 ); 219 220 return new PagingRequest( 221 $pagingControl, 222 $request, 223 new ControlBag(...$filteredControls), 224 $this->generateCookie() 225 ); 226 } 227 228 /** 229 * @param string $cookie 230 * @return PagingRequest 231 * @throws OperationException 232 */ 233 private function findPagingRequestOrThrow(string $cookie): PagingRequest 234 { 235 try { 236 return $this->requestHistory 237 ->pagingRequest() 238 ->findByNextCookie($cookie); 239 } catch (ProtocolException $e) { 240 throw new OperationException( 241 $e->getMessage(), 242 ResultCode::OPERATIONS_ERROR 243 ); 244 } 245 } 246 247 /** 248 * @param PagingRequest $pagingRequest 249 * @param RequestContext $context 250 * @return PagingResponse 251 * @throws OperationException 252 */ 253 private function handlePagingStart( 254 PagingRequest $pagingRequest, 255 RequestContext $context 256 ): PagingResponse { 257 $response = $this->pagingHandler->page( 258 $pagingRequest, 259 $context 260 ); 261 $pagingRequest->updateNextCookie($this->generateCookie()); 262 263 return $response; 264 } 265} 266