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