├── .gitignore ├── CHANGELOG.md ├── README.md ├── composer.json ├── examples ├── ldap-client.php └── ldap-server.php ├── phpunit.xml.dist ├── src ├── Ber.php ├── Client.php ├── Ldap.php ├── LdapConnection.php ├── Parser.php ├── Request.php ├── Request │ ├── Add.php │ ├── Bind.php │ ├── Compare.php │ ├── Delete.php │ ├── ModDn.php │ ├── Modify.php │ ├── Search.php │ ├── StartTls.php │ └── Unbind.php ├── Response.php ├── Result.php └── Server.php └── tests ├── BerTest.php ├── ClientTest.php ├── RequestTest.php └── ResponseTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.?.? 4 | 5 | * BC break: Result() end event does not emit data anymore 6 | 7 | ## 0.1.0 (2017-07-25) 8 | 9 | * First tagged release 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # php-ldap-react 2 | Asynchronous LDAP client built on top of ReactPHP 3 | 4 | ## Quickstart example 5 | 6 | ```php 7 | $loop = React\EventLoop\Factory::create(); 8 | $client = new Fneufneu\React\Ldap\Client($loop, 'ldap://myhost.com'); 9 | $client->bind('user', 'password')->then(function ($client) { 10 | $res = $client->search([ 11 | 'base' => 'cn=foo, o=example', 12 | 'filter' => 'mail=*', 13 | ]); 14 | $res->on('data', function ($data) { 15 | echo json_encode($data) . PHP_EOL; 16 | }); 17 | $client->unbind(); 18 | }); 19 | $loop->run(); 20 | ``` 21 | ## Client usage 22 | 23 | The `Client` class is the main class in this package that let you connect to 24 | a LDAP Server. 25 | 26 | ```php 27 | $client = new Client($loop, 'ldap://host', $options); 28 | ``` 29 | 30 | The constructor needs 31 | - an [`EventLoop`](https://github.com/reactphp/event-loop) 32 | - an URI to the LDAP host (`ldap://myhost.com:389`, `ldaptls://yourhost.fr`, `ldaps://mycomp.com`) 33 | - an optional array of options 34 | 35 | ### Supported options 36 | 37 | | option | description | 38 | | ------ | ----------- | 39 | | connector | a custom React\Socket\ConnectorInterface | 40 | | timeout | timeout in sec for default Connector, connect() and bind() request | 41 | 42 | ### Events 43 | 44 | The client emit usual event: end, close and error: 45 | 46 | ```php 47 | $client->on('end', function () { 48 | echo "client's connection ended" . PHP_EOL; 49 | }); 50 | 51 | $client->on('close', function () { 52 | echo "client's connection closed" . PHP_EOL; 53 | }); 54 | 55 | $client->on('error', function (Exception $e) { 56 | echo 'error: '.$e->getMessage() . PHP_EOL; 57 | }); 58 | ``` 59 | 60 | ### bind() 61 | 62 | bind call connect() and return a promise. 63 | 64 | ```php 65 | $client->bind('toto', 'password')->done(function ($client) { 66 | echo 'successfuly binded' . PHP_EOL; 67 | }, function (Exception $e) { 68 | echo 'bind failed with error: ' . $e->getMessage() . PHP_EOL; 69 | }); 70 | ``` 71 | 72 | ### unbind() 73 | 74 | Send an unbind() request to the server. 75 | 76 | The server will usually disconnect the client just after. 77 | 78 | ### search() 79 | 80 | Performs a ldap_search and return a Result object see [Result usage](#result-usage) 81 | 82 | The `search(array): Result` method takes an array of options. 83 | 84 | | option | type | default value | description | 85 | | ------ | ---- | ------- | ------ | 86 | | base | string | no default | *mandatory* The base DN | 87 | | filter | string | (objectclass=*) | The search filter | 88 | | attributes | array | [] | An array of the required attributes | 89 | | scope | enum | Ldap::wholeSubtree | Ldap::wholeSubtree, Ldap::singleLevel, Ldap::baseObject | 90 | | pagesize | int | 0 | enable automatic paging | 91 | | sizelimit | int | 0 | Enables you to limit the count of entries fetched. Setting this to 0 means no limit | 92 | | timelimit | int | 0 | Sets the number of seconds how long is spend on the search. Setting this to 0 means no limit | 93 | | ~~typesonly~~ | bool | false | ~~set to true if only attribute types are wanted~~ (not supported) | 94 | | derefaliases | enum | Ldap::never | Specifies how aliases should be handled during the search (`Ldap::never`, `ldap::searching`, `Ldap::finding`, `Ldap:always`) | 95 | | resultfilter | ? | ? | ? | 96 | 97 | #### Paging 98 | 99 | In order to retrieve results beyond the usual 1000 limits, you can set pagesize to an int > 0 to page results. 100 | 101 | When enabled, the Client use an internal mechanisms to automate the process and perform as many search() as necessary. 102 | 103 | ### add() 104 | 105 | :heavy_exclamation_mark: **not tested** 106 | 107 | ```php 108 | add(string $dn, array entry): Result 109 | ``` 110 | 111 | ### modify() 112 | 113 | :heavy_exclamation_mark: **not tested** 114 | 115 | ```php 116 | modify(string $dn, array changes): Result 117 | ``` 118 | 119 | example: 120 | 121 | ```php 122 | $result = $client->modify('cn=test', [ 123 | ['add' => ['mail' => ['john@doe.com']], 124 | ['delete' => ['email' => ['john@doe.com']], 125 | ['replace' => ['sn' => ['John']], 126 | ], 127 | ]); 128 | ``` 129 | 130 | ### delete() 131 | 132 | ```php 133 | delete(string $dn): Result 134 | ``` 135 | 136 | ### modDN() 137 | 138 | :heavy_exclamation_mark: **not tested** 139 | 140 | ```php 141 | modDN(string $dn, string $newDn, bool $deleteOldDn, string $newSuperior): Result 142 | ``` 143 | 144 | ### compare() 145 | 146 | :heavy_exclamation_mark: **not tested** 147 | 148 | ```php 149 | compare(string $dn, string $attribute, string $value): Result 150 | ``` 151 | 152 | ## Result usage 153 | 154 | `Result` emit usual event: data, end and error: 155 | 156 | ```php 157 | $result->on('data', function ($data) { 158 | // one search entry 159 | }); 160 | 161 | $result->on('end', function ($data) { 162 | // all search entries or an empty array if none 163 | }); 164 | 165 | $result->on('error', function (Exception $e) { 166 | echo 'error: '.$e->getMessage() . PHP_EOL; 167 | }); 168 | ``` 169 | 170 | ## Server usage 171 | 172 | See [examples](examples). 173 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fneufneu/ldap-react", 3 | "description": "LDAP client for ReactPHP", 4 | "keywords": ["ldap"], 5 | "license": "MIT", 6 | "autoload": { 7 | "psr-4": {"Fneufneu\\React\\Ldap\\": "src"} 8 | }, 9 | "require": { 10 | "php": ">=5.4.0", 11 | "react/socket": "^1.0 || ^0.8", 12 | "phpseclib/phpseclib": "^2.0" 13 | }, 14 | "require-dev": { 15 | "phpunit/phpunit": "^6.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/ldap-client.php: -------------------------------------------------------------------------------- 1 | addTimer(7, function () use ($loop) { 8 | $loop->stop(); 9 | }); 10 | 11 | $client = new Fneufneu\React\Ldap\Client($loop, 'ldap://blabla.com'); 12 | $client->on('data', function ($data) { 13 | echo "data received: ".var_export($data, true).PHP_EOL; 14 | }); 15 | $client->on('error', function ($e) use ($loop) { 16 | echo "cmd failed: ".$e->getMessage().PHP_EOL; 17 | $loop->stop(); 18 | }); 19 | $client->on('end', function () use ($loop) { 20 | echo "client end".PHP_EOL; 21 | $loop->stop(); 22 | }); 23 | $client->bind('blabla', 'blabla75')->then(function ($client) { 24 | echo "binded\n"; 25 | $results = $client->search([ 26 | 'base' => "cn=blabla", 27 | 'filter' => "(&(uid=*)(mail=y*))", 28 | 'attributes' => ['uid', 'cn', 'mail'], 29 | ]); 30 | $results2 = $client->search([ 31 | 'base' => "cn=blabla", 32 | 'filter' => "(&(uid=*)(mail=m*))", 33 | 'attributes' => ['uid', 'cn', 'mail'], 34 | ]); 35 | $results3 = $client->search([ 36 | 'base' => "cn=blabla", 37 | 'filter' => "(&(uid=*)(mail=a*))", 38 | 'attributes' => ['uid', 'cn', 'mail'], 39 | ]); 40 | 41 | $print_data = function ($data) { 42 | echo json_encode($data) . PHP_EOL; 43 | }; 44 | $print_end = function () { 45 | printf('end'.PHP_EOL); 46 | }; 47 | $print_close = function () { 48 | printf('close'.PHP_EOL); 49 | }; 50 | $print_error = function ($e) { 51 | echo 'error: '.$e->getMessage().PHP_EOL; 52 | }; 53 | $results->on('data', $print_data) 54 | ->on('end', $print_end) 55 | ->on('close', $print_close) 56 | ->on('error', $print_error); 57 | $results2->on('data', $print_data) 58 | ->on('end', $print_end) 59 | ->on('close', $print_close) 60 | ->on('error', $print_error); 61 | $results3->on('data', $print_data) 62 | ->on('end', $print_end) 63 | ->on('close', $print_close) 64 | ->on('error', $print_error); 65 | $client->unbind(); 66 | }, function ($e) use ($loop) { 67 | echo "bind failed: ".$e->getMessage().PHP_EOL; 68 | $loop->stop(); 69 | }); 70 | 71 | $loop->run(); 72 | -------------------------------------------------------------------------------- /examples/ldap-server.php: -------------------------------------------------------------------------------- 1 | on('bind', function ($infos) use ($client) { 15 | echo "new bindRequest: " . json_encode($infos) . PHP_EOL; 16 | $resp = new Response($infos['messageID'], Ldap::bindResponse); 17 | $client->write($resp); 18 | }); 19 | $client->on('unbind', function ($infos) use ($client) { 20 | $client->end(); 21 | }); 22 | $client->on('search', function ($infos) use ($client) { 23 | echo "new search: " . json_encode($infos) . PHP_EOL; 24 | $client->write(new Response($infos['messageID'], Ldap::searchResDone, 0, '', '')); 25 | }); 26 | $client->on('add', function ($infos) use ($client) { 27 | $client->write(new Response($infos['messageID'], Ldap::addResponse, 1, '', 'not implemented')); 28 | }); 29 | }); 30 | 31 | $socket = new React\Socket\Server('0.0.0.0:33389', $loop); 32 | $server->listen($socket); 33 | 34 | $loop->run(); 35 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./tests/ 7 | 8 | 9 | 10 | 11 | 12 | ./src/ 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Ber.php: -------------------------------------------------------------------------------- 1 | ' ' && $x <= '~') $xtra .= $x; 89 | else $xtra .= "."; 90 | if ($c == 15) { 91 | print " " . $xtra; 92 | $xtra = ""; 93 | } 94 | $i++; 95 | } 96 | if ($c != 15) print " " . str_repeat(' ', 15 - $c) . $xtra; 97 | print "\n"; 98 | } 99 | } 100 | 101 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | 10, 20 | ); 21 | private $loop; 22 | private $connnector; 23 | private $connected; 24 | private $stream; 25 | private $uri; 26 | private $buffer; 27 | private $deferred; 28 | private $requests; 29 | private $asyncRequests; 30 | private $parser; 31 | private $messageID = 1; 32 | private $expectedAnswer; 33 | 34 | function __construct(LoopInterface $loop, string $uri, $options = array()) 35 | { 36 | $this->options = $options + $this->options + array( 37 | 'uri' => $uri, 38 | 'connector' => null, 39 | 'dn' => null, 40 | 'password' => null 41 | ); 42 | 43 | if (isset($options['connector']) && $options['connector'] instanceof ConnectorInterface) { 44 | $this->connector = $options['connector']; 45 | } else { 46 | $this->connector = new Connector($loop, array('timeout' => $this->options['timeout'])); 47 | } 48 | 49 | $this->parser = new Parser(); 50 | $this->buffer = ''; 51 | $this->connected = false; 52 | $this->uri = $uri; 53 | $this->loop = $loop; 54 | $this->asyncRequests = new \SplPriorityQueue(); 55 | } 56 | 57 | private function sendldapmessage($pdu) 58 | { 59 | return $this->stream->write($pdu); 60 | } 61 | 62 | public function handleData($data) 63 | { 64 | if ($this->buffer != '') { 65 | $data = $this->buffer . $data; 66 | $this->buffer = ''; 67 | } 68 | $message = $this->parser->decode($data); 69 | if (!$message) { 70 | // incomplet data 71 | $this->buffer .= $data; 72 | return; 73 | } 74 | 75 | //printf('received %s request id (%d)' . PHP_EOL, $message['protocolOp'], $message['messageID']); 76 | $this->handleMessage($message); 77 | 78 | if (strlen($data) > 0) 79 | $this->handleData($data); 80 | } 81 | 82 | public function handleMessage($message) 83 | { 84 | $result = $this->requests[$message['messageID']]; 85 | 86 | if ($message['protocolOp'] == 'bindResponse') { 87 | if (0 != $message['resultCode']) { 88 | $this->deferred->reject(new \RuntimeException($message['resultCode'] . ' ' . $message['diagnosticMessage'])); 89 | } else { 90 | $this->deferred->resolve($this); 91 | } 92 | unset($this->requests[$message['messageID']]); 93 | } elseif (0 != $message['resultCode']) { 94 | if ($result) 95 | $result->error(new \RuntimeException($message['diagnosticMessage'])); 96 | else 97 | $this->emit('error', array(new \RuntimeException($message['diagnosticMessage']))); 98 | } elseif ($message['protocolOp'] == 'extendedResp') { 99 | $streamEncryption = new \React\Socket\StreamEncryption($this->loop, false); 100 | $streamEncryption->enable($this->stream)->then(function () { 101 | $this->startTlsDeferred->resolve(); 102 | }); 103 | unset($this->requests[$message['messageID']]); 104 | } elseif ($message['protocolOp'] == 'searchResEntry') { 105 | $message = $this->searchResEntry($message); 106 | $result->data($message); 107 | } else { 108 | if ($message['protocolOp'] == 'searchResDone' 109 | and array_key_exists('1.2.840.113556.1.4.319', $message['controls'])) { 110 | $cookie = $message['controls']['1.2.840.113556.1.4.319']; 111 | if ("" != $cookie) { 112 | $options = $this->savedSearchOptions[$message['messageID']]; 113 | $options['cookie'] = $cookie; 114 | $request = new Request\Search($this->messageID++, $options); 115 | $this->savedSearchOptions[$request->messageId] = $options; 116 | $this->asyncRequests->insert($request, 0); 117 | $this->requests[$request->messageId] = &$this->requests[$message['messageID']]; 118 | $this->pollRequests(); 119 | } else { 120 | $result->end(); 121 | unset($this->requests[$message['messageID']], $this->savedSearchOptions[$message['messageID']]); 122 | } 123 | } elseif ($result) { 124 | $result->end(); 125 | unset($this->requests[$message['messageID']]); 126 | } 127 | } 128 | if ($message['protocolOp'] == $this->expectedAnswer) { 129 | $this->expectedAnswer = ''; 130 | $this->pollRequests(); 131 | } 132 | } 133 | 134 | private function searchResEntry($response) 135 | { 136 | $res = $response['attributes']; 137 | foreach ($res as $k => $v) { 138 | if (is_array($v) and 1 == count($v)) 139 | $res[$k] = array_shift($v); 140 | } 141 | $res['dn'] = $response['objectName']; 142 | return $res; 143 | } 144 | 145 | private function connect() 146 | { 147 | $url = $this->options['uri']; 148 | 149 | if (! preg_match('/^(?:(ldap(?:|s|tls))(?::\/\/))?(.+?):?(\d+)?$/', $url, $d)) { 150 | throw new \InvalidArgumentException('invalid uri: '.$url); 151 | } 152 | 153 | $defaultport = array(null => '389', 'ldap' => '389', 'ldaps' => '636', 'ldaptls' => '389'); 154 | list($dummy, $protocol, $address, $port) = $d; 155 | if (!$port) 156 | $port = $defaultport[$protocol]; 157 | 158 | $starttls = $protocol === 'ldaptls'; 159 | $transport = $protocol === 'ldaps' ? 'tls://' : 'tcp://'; 160 | 161 | $promise = $this->connector->connect("$transport$address:$port") 162 | ->then(function (\React\Socket\ConnectionInterface $stream) { 163 | $this->stream = $stream; 164 | $stream->on('data', [$this, 'handleData'] 165 | )->on('end', function () { 166 | $this->connected = false; 167 | $this->emit('end'); 168 | })->on('error', function (\Exception $e) { 169 | $this->emit('error', [$e]); 170 | })->on('close', function () { 171 | $this->connected = false; 172 | $this->emit('close'); 173 | }); 174 | $this->connected = true; 175 | $this->pollRequests(); 176 | }, function (\Exception $error) { 177 | $this->deferred->reject($error); 178 | }); 179 | 180 | if ($starttls) 181 | $promise = $this->startTLS($promise); 182 | 183 | return $promise; 184 | } 185 | 186 | public function bind($bind_rdn = NULL, $bind_password = NULL) 187 | { 188 | $this->deferred = new Deferred(); 189 | 190 | $request = new Request\Bind($this->messageID++, $bind_rdn, $bind_password); 191 | 192 | if ($this->connected) { 193 | $this->queueRequest($request, 0); 194 | } else { 195 | $this->connect()->done(function () use ($bind_rdn, $bind_password, $request) { 196 | $this->queueRequest($request, 0); 197 | }); 198 | } 199 | 200 | return Timer\timeout($this->deferred->promise(), $this->options['timeout'], $this->loop); 201 | } 202 | 203 | public function unbind() 204 | { 205 | $request = new Request\Unbind($this->messageID++); 206 | 207 | return $this->queueRequest($request, PHP_INT_MIN); 208 | } 209 | 210 | protected function startTLS($promise) 211 | { 212 | $this->startTlsDeferred = new Deferred(); 213 | 214 | $promise->done(function () { 215 | $starttls = new Request\StartTls($this->messageID++); 216 | $this->queueRequest($starttls); 217 | }); 218 | 219 | return $this->startTlsDeferred->promise(); 220 | } 221 | 222 | /** 223 | * options: base, filter 224 | */ 225 | public function search($options) 226 | { 227 | $request = new Request\Search($this->messageID++, $options); 228 | if ($options['pagesize']) 229 | $this->savedSearchOptions[$request->messageId] = $options; 230 | 231 | return $this->queueRequest($request); 232 | } 233 | 234 | private function queueRequest($request, $priority = null) 235 | { 236 | if (is_null($priority)) 237 | $priority = 0 - $request->messageId; 238 | $this->asyncRequests->insert($request, $priority); 239 | $result = new Result(); 240 | $this->requests[$request->messageId] = $result; 241 | $this->pollRequests(); 242 | 243 | return $result; 244 | } 245 | 246 | private function pollRequests() 247 | { 248 | if ($this->asyncRequests->isEmpty()) 249 | return; 250 | if (!$this->connected) 251 | return; 252 | if ('' != $this->expectedAnswer) 253 | return; 254 | 255 | $request = $this->asyncRequests->extract(); 256 | //printf('sending request (%d) and expect %s' . PHP_EOL, $request->messageId, $request->expectedAnswer); 257 | $this->expectedAnswer = $request->expectedAnswer; 258 | $this->sendldapmessage($request->toString()); 259 | } 260 | 261 | public function modify($dn, $changes) 262 | { 263 | $request = new Request\Modify($this->messageID++, $dn, $changes); 264 | 265 | return $this->queueRequest($request); 266 | } 267 | 268 | public function add($entry, $attributes) 269 | { 270 | $request = new Request\Add($this->messageID++, $entry, $attributes); 271 | 272 | return $this->queueRequest($request); 273 | } 274 | 275 | public function delete($dn) 276 | { 277 | $request = new Request\Delete($this->messageID++, $dn); 278 | 279 | return $this->queueRequest($request); 280 | } 281 | 282 | public function modDN($entry, $newrdn, $deleteoldrnd = true, $newsuperior = '') 283 | { 284 | $request = new Request\ModDn($this->messageID++, $newrdn, $deleteoldrnd, $newsuperior); 285 | 286 | return $this->queueRequest($request); 287 | } 288 | 289 | public function compare($entry, $attributeDesc, $assertionValue) 290 | { 291 | $request = new Request\Compare($this->messageID++, $entry, $attributeDesc, $assertionValue); 292 | 293 | return $this->queueRequest($request); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/Ldap.php: -------------------------------------------------------------------------------- 1 | 'searchResRef', 83 | 23 => 'extendedReq', 84 | 24 => 'extendedResp', 85 | ); 86 | 87 | protected $protocolOp2int; 88 | 89 | protected $fd; 90 | protected $cookie = ''; 91 | 92 | function __construct() { 93 | $this->protocolOp2int = array_flip($this->int2protocolOp); 94 | } 95 | 96 | static function filter($filter) 97 | { 98 | # extensibleMatch not supported ... 99 | if (!preg_match("/^\(.*\)$/", $filter)) $filter = '(' . $filter . ')'; 100 | $elements = preg_split("/(\(|\)|~=|>=|<=|=\*\)|\*|=|&|\||!|\\\\[a-z0-9]{2})/i", $filter, -1, PREG_SPLIT_DELIM_CAPTURE + PREG_SPLIT_NO_EMPTY); 101 | $i = 0; 102 | $res = self::dofilter($elements, $i); 103 | if ($i - sizeof($elements) != 1) 104 | throw new \InvalidArgumentException("Unmatched ) or ( in filter: $filter"); 105 | return self::filter2ber($res); 106 | } 107 | 108 | static function dofilter(&$elements, &$i) 109 | { 110 | $res = array(); 111 | while ($element = $elements[$i++]) { 112 | $unescapedelement = $element; 113 | if (preg_match("/^\\\\([0-9a-z]{2})$/i", $element, $d)) { 114 | $unescapedelement = chr(hexdec($d[1])); 115 | } 116 | if ($element == '(') $res['filters'][] = self::dofilter($elements, $i); 117 | elseif ($element == ')') break; 118 | elseif (in_array($element, array('&', '|', '!'))) $res['op'] = $element; 119 | elseif (in_array($element, array('=', '~=', '>=', '<=', '=*)'))) { 120 | $res['filtertype'] = $element; 121 | if ($element == '=*)') break; 122 | } elseif ($element == '*') { 123 | $res['filtertype'] = $element; 124 | unset($res['final']); 125 | unset($res['assertionValue']); 126 | } elseif ($res['filtertype'] == '*') $res['final'] .= $res['any'][] .= $unescapedelement; 127 | elseif ($res['filtertype']) $res['initial'] = $res['assertionValue'] .= $unescapedelement; 128 | else $res['attributeDesc'] .= $unescapedelement; 129 | } 130 | if ($res['final']) array_pop($res['any']); 131 | return $res; 132 | } 133 | 134 | static function filter2ber($filter) 135 | { 136 | #print_r($filter); 137 | $ops = array('&' => "\xa0", '|' => "\xa1", '!' => "\xa2", '*' => "\xa4", '=' => "\xa3", '~=' => "\xa8", '>=' => '\xa5', '<=' => "\xa6",); 138 | foreach ($filter['filters'] as $f) { 139 | if ($f['op']) $res .= self::filter2ber($f); 140 | else { 141 | if ('=*)' == $f['filtertype']) { 142 | $res .= Ber::choice(7) . Ber::len($f['attributeDesc']) . $f['attributeDesc']; 143 | } elseif ('*' == $f['filtertype']) { 144 | $payload = $f['initial'] ? "\x80" . Ber::len($f['initial']) . $f['initial'] : ''; 145 | foreach ((array)$f['any'] as $any) $payload .= "\x81" . Ber::len($any) . $any; 146 | $payload .= $f['final'] ? "\x82" . Ber::len($f['final']) . $f['final'] : ''; 147 | $payload = Ber::octetstring($f['attributeDesc']) . Ber::sequence($payload); 148 | $res .= "\xa4" . Ber::len($payload) . $payload; 149 | } else { 150 | $payload = Ber::octetstring($f['attributeDesc']) . Ber::octetstring($f['assertionValue']); 151 | $res .= $ops[$f['filtertype']] . Ber::len($payload) . $payload; 152 | } 153 | } 154 | } 155 | if ($op = $filter['op']) return $ops[$op] . Ber::len($res) . $res; 156 | return $res; 157 | } 158 | static function control($controlType, $criticality = false, $controlValue = '') 159 | { 160 | return Ber::octetstring($controlType) . ($criticality ? Ber::boolean($criticality) : '') . Ber::octetstring($controlValue); 161 | } 162 | 163 | protected function attributes(array $attributes) 164 | { 165 | foreach ($attributes as $attribute) { 166 | $pdu .= Ber::octetstring($attribute); 167 | } 168 | return Ber::sequence($pdu); 169 | } 170 | 171 | protected function ldapMessage($messageId, $protocolOp, $pdu, $controls = '') 172 | { 173 | return Ber::sequence(Ber::integer($messageId) 174 | . Ber::application($protocolOp, $pdu) 175 | . ($controls ? "\xA0" . Ber::len($controls) . $controls : '')); 176 | } 177 | 178 | static function matchedValuesControl($filter = '', $criticality = false) 179 | { 180 | return Ber::sequence(self::control('1.2.826.0.1.3344810.2.3', $criticality, Ber::sequence($filter))); 181 | } 182 | 183 | static function pagedResultsControl($size, $cookie = '', $criticality = false) 184 | { 185 | return Ber::sequence(self::control('1.2.840.113556.1.4.319', $criticality, Ber::sequence(Ber::integer($size) . Ber::octetstring($cookie)))); 186 | } 187 | 188 | public function pretty($message) { 189 | $message = $message[0]['value']; 190 | $tag = $message[1]['tag']; 191 | $op = $message[1]['value']; 192 | if ($tag == 10) { # delRequest has "inline" value - fake a real structure 193 | $op = array(array('value' => $op)); 194 | } 195 | 196 | $structs = array( 197 | self::bindRequest => array('version', 'name', 'authentication'), 198 | self::bindResponse => array('resultCode', 'matchedDN', 'diagnosticMessage'), 199 | self::unbindRequest => array(), 200 | self::searchRequest => array('baseObject', 'scope', 'derefAliases', 'sizeLimit', 'timeLimit', 'typesOnly', 'filter' => 'Filter', 'attributes' => 'AttributeSelection'), 201 | self::searchResEntry => array('objectName', 'attributes' => 'PartialAttributeList'), 202 | self::searchResDone => array('resultCode', 'matchedDN', 'diagnosticMessage'), 203 | self::modifyRequest => array('object', 'changes' => 'Changes'), 204 | self::modifyResponse => array('resultCode', 'matchedDN', 'diagnosticMessage'), 205 | self::addRequest => array('entry', 'attributes' => 'PartialAttributeList'), 206 | self::addResponse => array('resultCode', 'matchedDN', 'diagnosticMessage'), 207 | self::delRequest => array('LDAPDN'), 208 | self::delResponse => array('resultCode', 'matchedDN', 'diagnosticMessage'), 209 | self::modDNRequest => array('entry', 'newrdn', 'deleteoldrdn', 'newSuperior'), 210 | self::modDNResponse => array('resultCode', 'matchedDN', 'diagnosticMessage'), 211 | self::compareRequest => array('entry', 'ava' => 'AttributeValueAssertion'), 212 | self::compareResponse => array('resultCode', 'matchedDN', 'diagnosticMessage'), 213 | self::abandonRequest => array('MessageID'), 214 | self::extendedReq => array('requestName', 'requestValue'), 215 | self::extendedResp => array('resultCode', 'matchedDN', 'diagnosticMessage', 'responseName', 'responseValue'), 216 | ); 217 | 218 | $i = 0; 219 | foreach ($structs[$tag] as $j => $name) { 220 | if (is_int($j)) $pp[$name] = $op[$i++]['value']; 221 | else $pp[$j] = $this->{$name.'_'}($op[$i++]); 222 | } 223 | $pp['controls'] = $this->Controls_($message[2]); 224 | $pp['messageID'] = $message[0]['value']; 225 | $pp['protocolOp'] = $this->int2protocolOp[$tag]; 226 | return $pp; 227 | } 228 | 229 | private function Filter_($op) { 230 | $choices = array('and', 'or', 'not', 'equalityMatch', 'substrings', 'greaterOrEqual', 'lessOrEqual', 'present', 'approxMatch', 'extensibleMatch'); 231 | $structs = array('and' => 'Filter', 'or' => 'Filter', 'not' => 'Filter', 'equalityMatch' => 'AttributeValueAssertionx', 'substrings' => 'SubstringFilter', 'greaterOrEqual' => 'AttributeValueAssertionx', 232 | 'lessOrEqual' => 'AttributeValueAssertionx', 'present' => 'AttributeDescription', 'ApproxMatch' => 'MatchingRuleAssertion'); 233 | $key = $choices[$op['tag']]; 234 | $function = $structs[$key]; 235 | if ($key == 'and' || $key == 'or') { 236 | foreach($op['value'] as $filter) { 237 | $res[$key][] = $this->{$function.'_'}($filter); 238 | } 239 | return $res; 240 | } 241 | return array($key => $this->{$function.'_'}($op['value'])); 242 | } 243 | 244 | private function SubstringFilter_($op) { 245 | $res['type'] = $op[0]['value']; 246 | foreach($op[1]['value'] as $string) { 247 | $tag = $string['tag']; 248 | if ($tag == 0) $res['initial'] = $string['value']; 249 | elseif ($tag = 1) $res['any'][] = $string['value']; 250 | else $res['final'] = $string['value']; 251 | } 252 | return $res; 253 | } 254 | 255 | private function AttributeValueAssertion_($op) { 256 | return array("attributeDesc" => $op['value'][0]['value'], 'assertionValue' => $op['value'][1]['value']); 257 | } 258 | 259 | private function AttributeValueAssertionx_($op) { 260 | return array("attributeDesc" => $op[0]['value'], 'assertionValue' => $op[1]['value']); 261 | } 262 | 263 | private function AttributeSelection_($op) { 264 | $res = array(); 265 | foreach($op['value'] as $attribute) { 266 | $res[] = $attribute['value']; 267 | } 268 | return $res; 269 | } 270 | 271 | private function PartialAttributeList_($op) { 272 | foreach($op['value'] as $attribute) { 273 | $attributeDesc = $attribute['value'][0]['value']; 274 | foreach ($attribute['value'][1]['value'] as $value) { 275 | $res[$attributeDesc][] = $value['value']; 276 | } 277 | } 278 | return $res; 279 | } 280 | 281 | private function Changes_($op) { 282 | $ops = array('add', 'delete', 'replace'); 283 | foreach($op['value'] as $operation) { 284 | $adddelrep = $ops[$operation['value'][0]['value']]; 285 | foreach($operation['value'][1] as $attrs) { 286 | $attributeDesc = $attrs[0]['value']; 287 | foreach((array)$attrs[1]['value'] as $value) { 288 | $res[$adddelrep][$attributeDesc][] = $value['value']; 289 | } 290 | } 291 | } 292 | return $res; 293 | } 294 | 295 | private function Controls_($controls) { 296 | $res = array(); 297 | 298 | if (!is_array($controls['value'])) 299 | return $res; 300 | 301 | foreach($controls['value'] as $control) { 302 | $ctrl['controlType'] = $control['value'][0]['value']; 303 | $ctrl['controlValue'] = $control['value'][1]['value']; 304 | $res[] = $ctrl; 305 | } 306 | return $res; 307 | } 308 | } 309 | 310 | -------------------------------------------------------------------------------- /src/LdapConnection.php: -------------------------------------------------------------------------------- 1 | conn = $conn; 19 | $this->parser = new Parser(); 20 | $conn->on('data', [$this, 'handleData']); 21 | Util::forwardEvents($conn, $this, ['error', 'end', 'close']); 22 | } 23 | 24 | public function handleData($data) 25 | { 26 | if ($this->buffer != '') { 27 | $data = $this->buffer . $data; 28 | $this->buffer = ''; 29 | } 30 | $message = $this->parser->decode($data); 31 | if (!$message) { 32 | // incomplet data 33 | $this->buffer .= $data; 34 | return; 35 | } 36 | //echo "LdapConnection handleData: ".json_encode($message).PHP_EOL; 37 | 38 | $op = $message['protocolOp']; 39 | $p = strpos($op, 'Request'); 40 | if ($p !== false) 41 | $op = substr($op, 0, $p); 42 | 43 | $events = $this->listeners($op); 44 | if (empty($events)) { 45 | $op = array_search("${op}Request", $this->parser->int2protocolOp); 46 | if ($op == 3) 47 | ++$op; 48 | $this->write(new Response($message['messageID'], 1 + $op, 1, '', 'not implemented')); 49 | } else { 50 | $this->emit($op, [$message]); 51 | } 52 | 53 | if (strlen($data) > 0) 54 | $this->handleData($data); 55 | } 56 | 57 | public function write($data) 58 | { 59 | return $this->conn->write($this->encode($data)); 60 | } 61 | 62 | public function end($data = '') 63 | { 64 | return $this->conn->end($this->encode($data)); 65 | } 66 | 67 | private function encode($data) 68 | { 69 | // TODO BER encoder ? 70 | return $data; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Parser.php: -------------------------------------------------------------------------------- 1 | 'searchResRef', 29 | 23 => 'extendedReq', 30 | 24 => 'extendedResp', 31 | ); 32 | 33 | const bindRequest = 0; 34 | const bindResponse = 1; 35 | const unbindRequest = 2; 36 | const searchRequest = 3; 37 | const searchResEntry = 4; 38 | const searchResDone = 5; 39 | const searchResRef = 19; 40 | const modifyRequest = 6; 41 | const modifyResponse = 7; 42 | const addRequest = 8; 43 | const addResponse = 9; 44 | const delRequest = 10; 45 | const delResponse = 11; 46 | const modDNRequest = 12; 47 | const modDNResponse = 13; 48 | const compareRequest = 14; 49 | const compareResponse = 15; 50 | const abandonRequest = 16; 51 | const extendedReq = 23; 52 | const extendedResp = 24; 53 | 54 | private $asn1; 55 | 56 | public function __construct() 57 | { 58 | $this->asn1 = new ASN1(); 59 | } 60 | 61 | public function decode(&$data) 62 | { 63 | $decoded = $this->asn1->decodeBER($data); 64 | 65 | if ($decoded[0] === false) 66 | return false; 67 | 68 | $data = substr($data, $decoded[0]['length']); 69 | 70 | return $this->pretty($decoded); 71 | } 72 | 73 | /** 74 | * value => content 75 | * tag => constant 76 | */ 77 | public function pretty($message) 78 | { 79 | $message = $message[0]['content']; 80 | $tag = $message[1]['constant']; 81 | $op = $message[1]['content']; 82 | if ($tag == 10) { # delRequest has "inline" value - fake a real structure 83 | $op = array(array('content' => $op)); 84 | } 85 | 86 | $structs = array( 87 | self::bindRequest => array('version', 'name', 'authentication'), 88 | self::bindResponse => array('resultCode', 'matchedDN', 'diagnosticMessage'), 89 | self::unbindRequest => array(), 90 | self::searchRequest => array('baseObject', 'scope', 'derefAliases', 'sizeLimit', 'timeLimit', 'typesOnly', 'filter' => 'Filter', 'attributes' => 'AttributeSelection'), 91 | self::searchResEntry => array('objectName', 'attributes' => 'PartialAttributeList'), 92 | self::searchResDone => array('resultCode', 'matchedDN', 'diagnosticMessage'), 93 | self::modifyRequest => array('object', 'changes' => 'Changes'), 94 | self::modifyResponse => array('resultCode', 'matchedDN', 'diagnosticMessage'), 95 | self::addRequest => array('entry', 'attributes' => 'PartialAttributeList'), 96 | self::addResponse => array('resultCode', 'matchedDN', 'diagnosticMessage'), 97 | self::delRequest => array('LDAPDN'), 98 | self::delResponse => array('resultCode', 'matchedDN', 'diagnosticMessage'), 99 | self::modDNRequest => array('entry', 'newrdn', 'deleteoldrdn', 'newSuperior'), 100 | self::modDNResponse => array('resultCode', 'matchedDN', 'diagnosticMessage'), 101 | self::compareRequest => array('entry', 'ava' => 'AttributeValueAssertion'), 102 | self::compareResponse => array('resultCode', 'matchedDN', 'diagnosticMessage'), 103 | self::abandonRequest => array('MessageID'), 104 | self::extendedReq => array('requestName', 'requestValue'), 105 | self::extendedResp => array('resultCode', 'matchedDN', 'diagnosticMessage', 'responseName', 'responseValue'), 106 | ); 107 | 108 | $i = 0; 109 | if (is_array($structs[$tag])) 110 | foreach ($structs[$tag] as $j => $name) { 111 | if (is_int($j)) 112 | $pp[$name] = $op[$i++]['content']; 113 | else 114 | $pp[$j] = $this->{$name.'_'}($op[$i++]); 115 | } 116 | // TODO adapter Controls_ 117 | if (isset($message[2])) { 118 | $pp['controls'] = $this->Controls_($message[2]); 119 | } else { 120 | $pp['controls'] = []; 121 | } 122 | 123 | $pp['messageID'] = $message[0]['content']; 124 | $pp['protocolOp'] = $this->int2protocolOp[$tag]; 125 | 126 | foreach ($pp as $k => $v) 127 | if (is_object($v) 128 | and is_a($v, 'phpseclib\Math\BigInteger')) 129 | $pp[$k] = $v->toString(); 130 | 131 | return $pp; 132 | } 133 | 134 | private function Filter_($op) { 135 | $choices = array('and', 'or', 'not', 'equalityMatch', 'substrings', 'greaterOrEqual', 'lessOrEqual', 'present', 'approxMatch', 'extensibleMatch'); 136 | $structs = array('and' => 'Filter', 'or' => 'Filter', 'not' => 'Filter', 'equalityMatch' => 'AttributeValueAssertionx', 'substrings' => 'SubstringFilter', 'greaterOrEqual' => 'AttributeValueAssertionx', 137 | 'lessOrEqual' => 'AttributeValueAssertionx', 'present' => 'AttributeDescription', 'ApproxMatch' => 'MatchingRuleAssertion'); 138 | $key = $choices[$op['constant']]; 139 | $function = $structs[$key]; 140 | if ($key == 'and' || $key == 'or') { 141 | foreach($op['content'] as $filter) { 142 | $res[$key][] = $this->{$function.'_'}($filter); 143 | } 144 | return $res; 145 | } 146 | return array($key => $this->{$function.'_'}($op['content'])); 147 | } 148 | 149 | private function SubstringFilter_($op) { 150 | $res['type'] = $op[0]['content']; 151 | foreach($op[1]['content'] as $string) { 152 | $tag = $string['constant']; 153 | if ($tag == 0) $res['initial'] = $string['content']; 154 | elseif ($tag = 1) $res['any'][] = $string['content']; 155 | else $res['final'] = $string['content']; 156 | } 157 | return $res; 158 | } 159 | 160 | private function AttributeValueAssertion_($op) { 161 | return array("attributeDesc" => $op['content'][0]['content'], 'assertionValue' => $op['content'][1]['content']); 162 | } 163 | 164 | private function AttributeValueAssertionx_($op) { 165 | return array("attributeDesc" => $op[0]['content'], 'assertionValue' => $op[1]['content']); 166 | } 167 | 168 | private function AttributeSelection_($op) { 169 | $res = array(); 170 | foreach($op['content'] as $attribute) { 171 | $res[] = $attribute['content']; 172 | } 173 | return $res; 174 | } 175 | 176 | private function AttributeDescription_($op) { 177 | // TODO if not a string ? 178 | return $op; 179 | } 180 | 181 | private function PartialAttributeList_($op) { 182 | foreach($op['content'] as $attribute) { 183 | $attributeDesc = $attribute['content'][0]['content']; 184 | if (!is_array($attribute['content'][1]['content'])) { 185 | var_dump($attribute); 186 | continue; 187 | } 188 | foreach ($attribute['content'][1]['content'] as $value) { 189 | $res[$attributeDesc][] = $value['content']; 190 | } 191 | } 192 | return $res; 193 | } 194 | 195 | private function Changes_($op) { 196 | $ops = array('add', 'delete', 'replace'); 197 | foreach($op['content'] as $operation) { 198 | $adddelrep = $ops[$operation['content'][0]['content']]; 199 | foreach($operation['content'][1] as $attrs) { 200 | $attributeDesc = $attrs[0]['content']; 201 | foreach((array)$attrs[1]['content'] as $content) { 202 | $res[$adddelrep][$attributeDesc][] = $content['content']; 203 | } 204 | } 205 | } 206 | return $res; 207 | } 208 | 209 | private function Controls_($controls) { 210 | $res = array(); 211 | 212 | if (!is_array($controls['content'])) 213 | return $res; 214 | 215 | foreach($controls['content'] as $control) { 216 | $controlType = $control['content'][0]['content']; 217 | $criticality = $control['content'][1]['content']; 218 | $controlValue = $control['content'][2]['content']; 219 | if ('1.2.840.113556.1.4.319' == $controlType) { 220 | $controlValue = $this->asn1->decodeBER($controlValue); 221 | $controlValue = $controlValue[0]['content'][1]['content']; 222 | } 223 | $res[$controlType] = $controlValue; 224 | } 225 | 226 | return $res; 227 | } 228 | } 229 | 230 | -------------------------------------------------------------------------------- /src/Request.php: -------------------------------------------------------------------------------- 1 | 'bindResponse', 14 | self::searchRequest => 'searchResDone', 15 | self::modifyRequest => 'modifyResponse', 16 | self::addRequest => 'addRequest', 17 | self::delRequest => 'delResponse', 18 | self::modDNRequest => 'modDNResponse', 19 | self::compareRequest => 'compareResponse', 20 | self::extendedReq => 'extendedResp', 21 | ); 22 | 23 | public function __construct($messageId, $protocolOp, $pdu, $controls = '') 24 | { 25 | 26 | if (!array_key_exists($protocolOp, $this->answers)) 27 | throw new \InvalidArgumentException('invalid protocolOp'); 28 | 29 | $this->messageId = $messageId; 30 | $this->expectedAnswer = $this->answers[$protocolOp]; 31 | $this->message = $this->ldapMessage($messageId, $protocolOp, $pdu, $controls); 32 | } 33 | 34 | public function toString() 35 | { 36 | return $this->message; 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/Request/Add.php: -------------------------------------------------------------------------------- 1 | $attributeValues) { 13 | $pdu = ''; 14 | if (!is_array($attributeValues)) $attributeValues = array($attributeValues); 15 | foreach ($attributeValues as $attributeValue) { 16 | $pdu .= Ber::octetstring($attributeValue); 17 | } 18 | $pdux .= Ber::sequence(Ber::octetstring($attributeDesc) . Ber::set($pdu)); 19 | } 20 | $protocolOp = self::addRequest; 21 | $pdu = Ber::octetstring($entry) . Ber::sequence($pdux); 22 | 23 | parent::__construct($messageId, $protocolOp, $pdu); 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/Request/Bind.php: -------------------------------------------------------------------------------- 1 | messageId = $messageId; 13 | $this->expectedAnswer = 'delResponse'; 14 | $this->message = Ber::sequence(Ber::integer($this->messageID++) 15 | . Ber::application(self::delRequest, $dn, false)); 16 | } 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/Request/ModDn.php: -------------------------------------------------------------------------------- 1 | 0, 'delete' => 1, 'replace' => 2); 15 | $pdu = ''; 16 | foreach ($changes as $operation) { 17 | foreach ($operation as $type => $modification) { 18 | $pdu = ""; 19 | foreach ($modification as $attributeDesc => $attributeValues) { 20 | foreach ($attributeValues as $attributeValue) { 21 | $pdu .= Ber::octetstring($attributeValue); 22 | } 23 | $pdu = Ber::sequence(Ber::octetstring($attributeDesc) . Ber::set($pdu)); 24 | } 25 | $pdux .= Ber::sequence(Ber::enumeration($ops[$type]) . $pdu); 26 | } 27 | } 28 | $pdu = Ber::octetstring($dn) . Ber::sequence($pdux); 29 | 30 | parent::__construct($messageId, $protocolOp, $pdu); 31 | } 32 | 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/Request/Search.php: -------------------------------------------------------------------------------- 1 | '', 12 | 'filter' => '(objectclass=*)', 13 | 'pagesize' => 0, 14 | 'resultfilter' => '', 15 | 'scope' => self::wholeSubtree, 16 | 'sizelimit' => 0, 17 | 'timelimit' => 0, 18 | 'typesonly' => false, 19 | 'derefaliases' => self::never, 20 | 'attributes' => [], 21 | ); 22 | 23 | public function __construct($messageId, array $options) 24 | { 25 | $options += $this->options; 26 | 27 | $protocolOp = self::searchRequest; 28 | $pdu = Ber::octetstring($options['base']) 29 | . Ber::enumeration($options['scope']) 30 | . Ber::enumeration($options['derefaliases']) 31 | . Ber::integer($options['sizelimit']) 32 | . Ber::integer($options['timelimit']) 33 | . Ber::boolean($options['typesonly']) 34 | . self::filter($options['filter']) 35 | . self::attributes($options['attributes']); 36 | $controls = ''; 37 | if ($pagesize = $options['pagesize']) { 38 | $controls .= self::pagedResultsControl($pagesize, $options['cookie'], true); 39 | } 40 | if ($resultfilter = $options['resultfilter']) { 41 | $controls .= self::matchedValuesControl(self::filter($resultfilter)); 42 | } 43 | 44 | parent::__construct($messageId, $protocolOp, $pdu, $controls); 45 | } 46 | 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/Request/StartTls.php: -------------------------------------------------------------------------------- 1 | messageId = $messageId; 14 | $this->message = Ber::sequence( 15 | Ber::integer($messageId) 16 | . Ber::application(self::unbindRequest, '', false)); 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | 'success', 14 | 1 => 'operationsError', 15 | 2 => 'protocolError', 16 | 3 => 'timeLimitExceeded', 17 | 4 => 'sizeLimitExceeded', 18 | 5 => 'compareFalse', 19 | 6 => 'compareTrue', 20 | 7 => 'authMethodNotSupported', 21 | 8 => 'strongerAuthRequired', 22 | 10 => 'referral', 23 | 11 => 'adminLimitExceeded', 24 | 12 => 'unavailableCriticalExtension', 25 | 13 => 'confidentialityRequired', 26 | 14 => 'saslBindInProgress', 27 | 16 => 'noSuchAttribute', 28 | 17 => 'undefinedAttributeType', 29 | 18 => 'inappropriateMatching', 30 | 18 => 'constraintViolation', 31 | 20 => 'attributeOrValueExists', 32 | 21 => 'invalidAttributeSyntax', 33 | 32 => 'noSuchObject', 34 | 33 => 'aliasProblem', 35 | 34 => 'invalidDNSyntax', 36 | 36 => 'aliasDereferencingProblem', 37 | 48 => 'inappropriateAuthentication', 38 | 49 => 'invalidCredentials', 39 | 50 => 'insufficientAccessRights', 40 | 51 => 'busy', 41 | 52 => 'unavailable', 42 | 53 => 'unwillingToPerform', 43 | 54 => 'loopDetect', 44 | 64 => 'namingViolation', 45 | 65 => 'objectClassViolation', 46 | 66 => 'notAllowedOnNonLeaf', 47 | 67 => 'notAllowedOnRDN', 48 | 68 => 'entryAlreadyExists', 49 | 69 => 'objectClassModsProhibited', 50 | 71 => 'affectsMultipleDSAs', 51 | 80 => 'other', 52 | ]; 53 | 54 | public function __construct($messageId, $protocolOp, $resultCode = 0, $matchedDN = '', $diagnosticMessage = '') 55 | { 56 | if (!array_key_exists($resultCode, $this->resultCodes)) 57 | return new \InvalidArgumentException('invalid resultCode'); 58 | 59 | if (!array_key_exists($protocolOp, $this->int2protocolOp)) 60 | return new \InvalidArgumentException('invalid protocolOp'); 61 | 62 | $this->messageId = $messageId; 63 | $pdu = Ber::enumeration($resultCode) . Ber::octetstring($matchedDN) . Ber::octetstring($diagnosticMessage); 64 | $this->message = $this->ldapMessage($messageId, $protocolOp, $pdu, $controls); 65 | } 66 | 67 | public function __toString() 68 | { 69 | return $this->message; 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /src/Result.php: -------------------------------------------------------------------------------- 1 | readable) 20 | return; 21 | 22 | $this->emit('error', [$e]); 23 | $this->close(); 24 | } 25 | 26 | public function data($data) { 27 | if (!$this->readable) 28 | return; 29 | 30 | $this->emit('data', [$data]); 31 | } 32 | 33 | public function end() { 34 | if (!$this->readable) 35 | return; 36 | 37 | $this->emit('end'); 38 | $this->close(); 39 | } 40 | 41 | public function close() { 42 | if (!$this->readable) 43 | return; 44 | 45 | $this->readable = false; 46 | 47 | $this->emit('close'); 48 | $this->removeAllListeners(); 49 | } 50 | 51 | public function isReadable() { 52 | return $this->readable; 53 | } 54 | 55 | public function pause() { 56 | // NYI 57 | } 58 | 59 | public function resume() { 60 | // NYI 61 | } 62 | 63 | public function pipe(WritableStreamInterface $dest, array $options = array()) { 64 | Util::pipe($this, $dest, $options); 65 | 66 | return $dest; 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/Server.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 17 | } 18 | 19 | public function listen(\React\Socket\ServerInterface $socket) 20 | { 21 | $socket->on('connection', [$this, 'handleConnection']); 22 | } 23 | 24 | public function handleConnection(\React\Socket\ConnectionInterface $conn) 25 | { 26 | $callback = $this->callback; 27 | $callback(new LdapConnection($conn)); 28 | } 29 | 30 | /* 31 | public function __call($operation, $args) { 32 | echo "call with "; 33 | var_dump($operation, $args); 34 | $message = $args[0]; 35 | $msgid = $message['messageID']; 36 | $pdu = self::sequence(self::integer($msgid) . self::application($this->protocolOp2int[$operation]+1 , self::ldapResult())); # . ($controls ? "\xA0" . self::len($controls) . $controls : '')); 37 | #self::dump($pdu); 38 | fwrite($this->fd, $pdu); 39 | } 40 | 41 | static function ldapResult($resultCode = 0, $matchedDN = '', $diagnosticMessage = '') { 42 | return self::enumeration($resultCode) . self::octetstring($matchedDN) . self::octetstring($diagnosticMessage); 43 | } 44 | 45 | static function array2PartialAttributeList($entry) { 46 | $pdux = ''; 47 | foreach ($entry as $attributeDesc => $attributeValues) { 48 | $pdu = ''; 49 | foreach($attributeValues as $attributeValue) { 50 | $pdu .= self::octetstring($attributeValue); 51 | } 52 | $pdux .= self::sequence(self::octetstring($attributeDesc) . self::set($pdu)); 53 | } 54 | return self::sequence($pdux); 55 | } 56 | 57 | protected function searchRequest($message) 58 | { 59 | $msgid = $message[0]['value'][0]['value']; 60 | $entry = array('cn' => array('Mads Freek Petersen'), 'x' => array('værdi1', 'værdi2')); 61 | $PartialAttributeList = self::array2PartialAttributeList($entry); 62 | $pdu = self::sequence(self::integer($msgid) . self::application($this->protocolOp2int['searchResEntry'] , self::octetstring('ou=dn') . $PartialAttributeList)); 63 | fwrite($this->fd, $pdu); 64 | $pdu = self::sequence(self::integer($msgid) . self::application($this->protocolOp2int['searchResDone'] , self::ldapResult())); 65 | fwrite($this->fd, $pdu); 66 | } 67 | 68 | protected function extendedReq($Req) 69 | { #STREAM_CRYPTO_METHOD_TLS_SERVER 70 | 71 | } 72 | 73 | protected function bindRequest($Request) {}; 74 | protected function bindResponse($Response) {}; 75 | protected function unbindRequest($Request) {}; 76 | protected function searchRequest($Request) {}; 77 | protected function searchResEntry($ResEntry) {}; 78 | protected function searchResDone($ResDone) {}; 79 | protected function searchResRef($ResRef) {}; 80 | protected function modifyRequest($Request) {}; 81 | protected function modifyResponse($Response) {}; 82 | protected function addRequest($Request) {}; 83 | protected function addResponse($Response) {}; 84 | protected function delRequest($Request) {}; 85 | protected function delResponse($Response) {}; 86 | protected function modDNRequest($Request) {}; 87 | protected function modDNResponse($Response) {}; 88 | protected function compareRequest($Request) {}; 89 | protected function compareResponse($Response) {}; 90 | protected function abandonRequest($Request) {}; 91 | protected function extendedReq($Req) {}; 92 | protected function extendedResp($Resp) {}; 93 | */ 94 | 95 | } 96 | 97 | -------------------------------------------------------------------------------- /tests/BerTest.php: -------------------------------------------------------------------------------- 1 | assertInternalType('string', Ber::int(42)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/ClientTest.php: -------------------------------------------------------------------------------- 1 | bindRequest = base64_decode('MCACAQFgGwIBAwQMam9obkBkb2UudGxkgAhwYXNzd29yZA=='); 18 | } 19 | 20 | public function testHandleDataOneMessage() 21 | { 22 | $loop = $this->getMockBuilder(LoopInterface::class)->getMock(); 23 | 24 | $client = $this->getMockBuilder(Client::class) 25 | ->setConstructorArgs([$loop, '']) 26 | ->setMethods(['handleMessage']) 27 | ->getMock(); 28 | 29 | $client->expects($this->once()) 30 | ->method('handleMessage') 31 | ->with($this->arrayHasKey('protocolOp')); 32 | 33 | $client->handleData($this->bindRequest); 34 | } 35 | 36 | public function testHandleDataPartialMessages() 37 | { 38 | $loop = $this->getMockBuilder(LoopInterface::class)->getMock(); 39 | 40 | $client = $this->getMockBuilder(Client::class) 41 | ->setConstructorArgs([$loop, '']) 42 | ->setMethods(['handleMessage']) 43 | ->getMock(); 44 | 45 | $client->expects($this->exactly(2)) 46 | ->method('handleMessage') 47 | ->with($this->arrayHasKey('protocolOp')); 48 | 49 | $client->handleData($this->bindRequest 50 | . $this->bindRequest 51 | . substr($this->bindRequest, 0, 5)); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /tests/RequestTest.php: -------------------------------------------------------------------------------- 1 | asn1 = new ASN1(); 17 | } 18 | 19 | /** 20 | * @dataProvider generateAllRequest 21 | */ 22 | public function testValidateGeneratedBer(Request $request) 23 | { 24 | $decoded = $this->asn1->decodeBER($request->toString()); 25 | 26 | $this->assertInternalType('array', $decoded); 27 | $this->assertEquals(strlen($request->toString()), $decoded[0]['length']); 28 | } 29 | 30 | public static function generateAllRequest() 31 | { 32 | return [ 33 | [new Request\Add(1, 'cn=test', ['cn' => 'test'])], 34 | [new Request\Bind(2, 'john@doe.tld', 'password')], 35 | [new Request\Compare(3, 'cn=test', 'cn', 'test')], 36 | [new Request\Delete(4, 'cn=test')], 37 | [new Request\ModDn(5, 'cn=test', 'cn=test, ou=people')], 38 | [new Request\Modify(6, 'cn=test', [])], 39 | [new Request\Search(7, ['basedn' => ''])], 40 | [new Request\StartTls(8)], 41 | [new Request\Unbind(9)] 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/ResponseTest.php: -------------------------------------------------------------------------------- 1 | asn1 = new ASN1(); 18 | } 19 | 20 | /** 21 | * @dataProvider generateAllResponse 22 | */ 23 | public function testValidateGeneratedBer(Response $resp) 24 | { 25 | $decoded = $this->asn1->decodeBER((string) $resp); 26 | 27 | $this->assertInternalType('array', $decoded); 28 | $this->assertEquals(strlen((string) $resp), $decoded[0]['length']); 29 | } 30 | 31 | public static function generateAllResponse() 32 | { 33 | return [ 34 | [new Response(1, Ldap::bindResponse)], 35 | [new Response(2, Ldap::searchResDone, 0, '', '')], 36 | [new Response(3, Ldap::addResponse, 1, '', 'not implemented')], 37 | ]; 38 | } 39 | } 40 | --------------------------------------------------------------------------------