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; 13 14use FreeDSx\Ldap\Exception\RuntimeException; 15use FreeDSx\Ldap\Server\RequestHandler\PagingHandlerInterface; 16use FreeDSx\Ldap\Server\RequestHandler\ProxyHandler; 17use FreeDSx\Ldap\Server\RequestHandler\ProxyPagingHandler; 18use FreeDSx\Ldap\Server\RequestHandler\RequestHandlerInterface; 19use FreeDSx\Ldap\Server\RequestHandler\RootDseHandlerInterface; 20use FreeDSx\Ldap\Server\ServerRunner\PcntlServerRunner; 21use FreeDSx\Ldap\Server\ServerRunner\ServerRunnerInterface; 22use FreeDSx\Socket\Exception\ConnectionException; 23use FreeDSx\Socket\SocketServer; 24use Psr\Log\LoggerInterface; 25 26/** 27 * The LDAP server. 28 * 29 * @author Chad Sikorra <Chad.Sikorra@gmail.com> 30 */ 31class LdapServer 32{ 33 use LoggerTrait; 34 35 /** 36 * @var array 37 */ 38 protected $options = [ 39 'ip' => '0.0.0.0', 40 'port' => 389, 41 'unix_socket' => '/var/run/ldap.socket', 42 'transport' => 'tcp', 43 'idle_timeout' => 600, 44 'require_authentication' => true, 45 'allow_anonymous' => false, 46 'request_handler' => null, 47 'rootdse_handler' => null, 48 'paging_handler' => null, 49 'logger' => null, 50 'use_ssl' => false, 51 'ssl_cert' => null, 52 'ssl_cert_passphrase' => null, 53 'dse_alt_server' => null, 54 'dse_naming_contexts' => 'dc=FreeDSx,dc=local', 55 'dse_vendor_name' => 'FreeDSx', 56 'dse_vendor_version' => null, 57 ]; 58 59 /** 60 * @var ServerRunnerInterface|null 61 */ 62 protected $runner; 63 64 /** 65 * @param array $options 66 * @param ServerRunnerInterface|null $serverRunner 67 * @throws RuntimeException 68 */ 69 public function __construct( 70 array $options = [], 71 ?ServerRunnerInterface $serverRunner = null 72 ) { 73 $this->options = array_merge( 74 $this->options, 75 $options 76 ); 77 $this->runner = $serverRunner; 78 } 79 80 /** 81 * Runs the LDAP server. Binds the socket on the request IP/port and sends it to the server runner. 82 * 83 * @throws ConnectionException 84 */ 85 public function run(): void 86 { 87 $isUnixSocket = $this->options['transport'] === 'unix'; 88 $resource = $isUnixSocket 89 ? $this->options['unix_socket'] 90 : $this->options['ip']; 91 92 if ($isUnixSocket) { 93 $this->removeExistingSocketIfNeeded($resource); 94 } 95 96 $socketServer = SocketServer::bind( 97 $resource, 98 $this->options['port'], 99 $this->options 100 ); 101 102 $this->runner()->run($socketServer); 103 } 104 105 /** 106 * Get the options currently set for the LDAP server. 107 * 108 * @return array<string, mixed> 109 */ 110 public function getOptions(): array 111 { 112 return $this->options; 113 } 114 115 /** 116 * Specify an instance of a request handler to use for incoming LDAP requests. 117 * 118 * @param RequestHandlerInterface $requestHandler 119 * @return $this 120 */ 121 public function useRequestHandler(RequestHandlerInterface $requestHandler): self 122 { 123 $this->options['request_handler'] = $requestHandler; 124 125 return $this; 126 } 127 128 /** 129 * Specify an instance of a RootDSE handler to use for RootDSE requests. 130 * 131 * @param RootDseHandlerInterface $rootDseHandler 132 * @return $this 133 */ 134 public function useRootDseHandler(RootDseHandlerInterface $rootDseHandler): self 135 { 136 $this->options['rootdse_handler'] = $rootDseHandler; 137 138 return $this; 139 } 140 141 /** 142 * Specify an instance of a paging handler to use for paged search requests. 143 * 144 * @param PagingHandlerInterface $pagingHandler 145 * @return $this 146 */ 147 public function usePagingHandler(PagingHandlerInterface $pagingHandler): self 148 { 149 $this->options['paging_handler'] = $pagingHandler; 150 151 return $this; 152 } 153 154 /** 155 * Specify a logger to be used by the server process. 156 */ 157 public function useLogger(LoggerInterface $logger): self 158 { 159 $this->options['logger'] = $logger; 160 161 return $this; 162 } 163 164 /** 165 * Convenience method for generating an LDAP server instance that will proxy client request's to an LDAP server. 166 * 167 * Note: This is only intended to work with the PCNTL server runner. 168 * 169 * @param string|string[] $servers The LDAP server(s) to proxy the request to. 170 * @param array<string, mixed> $clientOptions Any additional client options for the proxy connection. 171 * @param array<string, mixed> $serverOptions Any additional server options for the LDAP server. 172 * @return LdapServer 173 */ 174 public static function makeProxy( 175 $servers, 176 array $clientOptions = [], 177 array $serverOptions = [] 178 ): LdapServer { 179 $client = new LdapClient(array_merge([ 180 'servers' => $servers, 181 ], $clientOptions)); 182 183 $proxyRequestHandler = new ProxyHandler($client); 184 $server = new LdapServer($serverOptions); 185 $server->useRequestHandler($proxyRequestHandler); 186 $server->useRootDseHandler($proxyRequestHandler); 187 $server->usePagingHandler(new ProxyPagingHandler($client)); 188 189 return $server; 190 } 191 192 private function runner(): ServerRunnerInterface 193 { 194 if (!$this->runner) { 195 $this->runner = new PcntlServerRunner($this->options); 196 } 197 198 return $this->runner; 199 } 200 201 private function removeExistingSocketIfNeeded(string $socket): void 202 { 203 if (!file_exists($socket)) { 204 return; 205 } 206 207 if (!is_writeable($socket)) { 208 $this->logAndThrow(sprintf( 209 'The socket "%s" already exists and is not writeable. To run the LDAP server, you must remove the existing socket.', 210 $socket 211 )); 212 } 213 214 if (!unlink($socket)) { 215 $this->logAndThrow(sprintf( 216 'The existing socket "%s" could not be removed. To run the LDAP server, you must remove the existing socket.', 217 $socket 218 )); 219 } 220 } 221} 222