Skip to content

Commit

Permalink
Finish clean-up of initial old code implementation to current structu…
Browse files Browse the repository at this point in the history
…re. This still does not represent any finished product. However, it does represent what was being worked on before this effort was previously dropped.

There's a lot to be redesigned here and tested. But this provides a cleaner starting point.
  • Loading branch information
ChadSikorra committed Jun 11, 2023
1 parent 1ed04c7 commit 0ee0cd8
Show file tree
Hide file tree
Showing 8 changed files with 393 additions and 258 deletions.
8 changes: 6 additions & 2 deletions src/FreeDSx/Ldap/LdapClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use FreeDSx\Ldap\Operation\Request\ExtendedRequest;
use FreeDSx\Ldap\Operation\Request\RequestInterface;
use FreeDSx\Ldap\Operation\Request\SearchRequest;
use FreeDSx\Ldap\Operation\Request\SyncRequest;
use FreeDSx\Ldap\Operation\Response\CompareResponse;
use FreeDSx\Ldap\Operation\Response\ExtendedResponse;
use FreeDSx\Ldap\Operation\Response\SearchResponse;
Expand Down Expand Up @@ -326,9 +327,12 @@ public function dirSync(
/**
* A helper for performing a ReplSync / directory synchronization as described in RFC4533.
*/
public function syncRepl(): SyncRepl
public function syncRepl(SyncRequest $syncRequest): SyncRepl
{
return new SyncRepl($this);
return new SyncRepl(
$this,
$syncRequest,
);
}

/**
Expand Down
32 changes: 32 additions & 0 deletions src/FreeDSx/Ldap/Operation/Request/SyncRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace FreeDSx\Ldap\Operation\Request;

use FreeDSx\Ldap\Entry\Attribute;
use FreeDSx\Ldap\Search\Filter\FilterInterface;
use FreeDSx\Ldap\Search\SyncHandlerInterface;

class SyncRequest extends SearchRequest
{
private SyncHandlerInterface $syncHandler;

public function __construct(
SyncHandlerInterface $syncHandler,
FilterInterface $filter,
string|Attribute ...$attributes
) {
$this->syncHandler = $syncHandler;

parent::__construct(
$filter,
...$attributes
);
}

public function getSyncHandler(): SyncHandlerInterface
{
return $this->syncHandler;
}
}
17 changes: 17 additions & 0 deletions src/FreeDSx/Ldap/Operations.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@
use FreeDSx\Ldap\Operation\Request\SaslBindRequest;
use FreeDSx\Ldap\Operation\Request\SearchRequest;
use FreeDSx\Ldap\Operation\Request\SimpleBindRequest;
use FreeDSx\Ldap\Operation\Request\SyncRequest;
use FreeDSx\Ldap\Operation\Request\UnbindRequest;
use FreeDSx\Ldap\Protocol\ProtocolElementInterface;
use FreeDSx\Ldap\Search\Filter\FilterInterface;
use FreeDSx\Ldap\Search\Filters;
use FreeDSx\Ldap\Search\SyncHandlerInterface;
use Stringable;

/**
Expand Down Expand Up @@ -229,6 +231,21 @@ public static function search(
);
}

/**
* Sync with LDAP (via method RFC-4533 / SyncRepl) with a given filter, scope, etc to retrieve a set of entries.
*/
public static function sync(
SyncHandlerInterface $syncHandler,
?FilterInterface $filter = null,
Attribute|string ...$attributes,
): SearchRequest {
return new SyncRequest(
$syncHandler,
$filter ?? Filters::present('objectClass'),
...$attributes
);
}

/**
* Search for a specific base DN object to read. This sets a 'present' filter for the 'objectClass' attribute to help
* simplify it.
Expand Down
306 changes: 306 additions & 0 deletions src/FreeDSx/Ldap/Protocol/ClientProtocolHandler/ClientSyncHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
<?php

declare(strict_types=1);

/**
* This file is part of the FreeDSx LDAP package.
*
* (c) Chad Sikorra <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FreeDSx\Ldap\Protocol\ClientProtocolHandler;

use FreeDSx\Ldap\ClientOptions;
use FreeDSx\Ldap\Control\Control;
use FreeDSx\Ldap\Control\Sync\SyncDoneControl;
use FreeDSx\Ldap\Control\Sync\SyncRequestControl;
use FreeDSx\Ldap\Control\Sync\SyncStateControl;
use FreeDSx\Ldap\Exception\ProtocolException;
use FreeDSx\Ldap\Exception\RuntimeException;
use FreeDSx\Ldap\Operation\LdapResult;
use FreeDSx\Ldap\Operation\Request\SearchRequest;
use FreeDSx\Ldap\Operation\Request\SyncRequest;
use FreeDSx\Ldap\Operation\Response\SearchResultDone;
use FreeDSx\Ldap\Operation\Response\SearchResultEntry;
use FreeDSx\Ldap\Operation\Response\SearchResultReference;
use FreeDSx\Ldap\Operation\Response\SyncInfo\SyncIdSet;
use FreeDSx\Ldap\Operation\Response\SyncInfo\SyncNewCookie;
use FreeDSx\Ldap\Operation\Response\SyncInfo\SyncRefreshDelete;
use FreeDSx\Ldap\Operation\Response\SyncInfo\SyncRefreshPresent;
use FreeDSx\Ldap\Operation\Response\SyncResponse;
use FreeDSx\Ldap\Operation\Response\SyncResult;
use FreeDSx\Ldap\Protocol\LdapMessageRequest;
use FreeDSx\Ldap\Protocol\LdapMessageResponse;
use FreeDSx\Ldap\Protocol\Queue\ClientQueue;
use FreeDSx\Ldap\Search\SyncHandlerInterface;

class ClientSyncHandler extends ClientBasicHandler
{
private ?SyncHandlerInterface $syncHandler;

private LdapMessageResponse $lastResponse;

/**
* {@inheritDoc}
*/
public function handleRequest(ClientProtocolContext $context): ?LdapMessageResponse
{
/** @var SearchRequest $request */
$request = $context->getRequest();
if ($request->getBaseDn() === null) {
$request->setBaseDn($context->getOptions()->getBaseDn() ?? null);
}

return parent::handleRequest($context);
}

/**
* {@inheritDoc}
*/
public function handleResponse(
LdapMessageRequest $messageTo,
LdapMessageResponse $messageFrom,
ClientQueue $queue,
ClientOptions $options,
): ?LdapMessageResponse {
/** @var SyncRequest $searchRequest */
$searchRequest = $messageTo->getRequest();
$this->syncHandler = $searchRequest->getSyncHandler();

return $this->handle(
$messageTo,
$queue,
);
}

public function handle(
LdapMessageRequest $messageTo,
ClientQueue $queue,
): ?LdapMessageResponse {
$initial = [];
$updates = [];
$present = [];
$deleted = [];
$cookie = '';

$control = $messageTo->controls()->get(Control::OID_SYNC_REQUEST);
if (!$control instanceof SyncRequestControl) {
throw new RuntimeException('Expected a SyncRequestControl, but there is none.');
}

$syncDone = null;
if ($this->isInitialPollRequest($control)) {
$initial = $this->initialContent(
$messageTo,
$queue
);
$syncDone = $this->getSyncDoneControl($this->lastResponse);
if ($syncDone === null) {
throw new ProtocolException('Expected a SyncDoneControl, but none was received.');
}
$cookie = $syncDone->getCookie();
} elseif ($this->isContentUpdatePoll($control)) {
$updates = $this->persistStage(
$queue,
$messageTo,
$control,
);
$syncDone = $this->getSyncDoneControl($this->lastResponse);

if ($syncDone === null) {
throw new ProtocolException('Expected a SyncDoneControl, but none was received.');
}

$cookie = $syncDone->getCookie();
if (!$cookie) {
$cookie = $control->getCookie();
}
}

if ($this->isContentUpdatePoll($control) && $syncDone?->getRefreshDeletes()) {
$deleted = $this->phaseDeleted(
$queue,
$messageTo
);
}

return new LdapMessageResponse(
$this->lastResponse->getMessageId(),
new SyncResponse(
$this->getLdapResult($this->lastResponse),
(string) $cookie,
$present,
$deleted,
$initial,
$updates,
),
...$this->lastResponse->controls()->toArray()
);
}

private function initialContent(
LdapMessageRequest $messageTo,
ClientQueue $queue
): array {
$results = [];
$isDone = false;

foreach ($queue->getMessages($messageTo->getMessageId()) as $message) {
$response = $message->getResponse();
$syncResult = null;

if ($response instanceof SearchResultEntry) {
$syncResult = new SyncResult(
$response->getEntry(),
$this->getSyncStateControl($message)
);
} elseif ($response instanceof SearchResultReference) {
$syncResult = new SyncResult(
$response->getReferrals(),
$this->getSyncStateControl($message)
);
} elseif ($response instanceof SearchResultDone) {
$isDone = true;
} else {
throw new ProtocolException('Unexpected message encountered during initial content sync.');
}

if ($syncResult && $this->syncHandler) {
$this->syncHandler->initialPoll($syncResult);
} elseif ($syncResult) {
$results[] = $syncResult;
}
if ($isDone) {
$this->lastResponse = $message;
break;
}
}

return $results;
}

private function phaseDeleted(
ClientQueue $queue,
LdapMessageRequest $messageTo
): array {
$results = [];

foreach ($queue->getMessages($messageTo->getMessageId()) as $message) {
$response = $message->getResponse();

if ($response instanceof SearchResultEntry) {
$results[] = new SyncResult(
$response->getEntry(),
$this->getSyncStateControl($message),
);
} elseif ($response instanceof SearchResultReference) {
$results[] = new SyncResult(
$response->getReferrals(),
$this->getSyncStateControl($message),
);
} elseif ($response instanceof SearchResultDone) {
$this->lastResponse = $message;
break;
}
}

return $results;
}

private function persistStage(
ClientQueue $queue,
LdapMessageRequest $messageTo,
SyncRequestControl $syncRequest,
): array {
$isDone = false;
$syncInfoDelete = null;
$syncInfoPresent = null;
$syncNewCookie = null;
$message = null;

$syncResults = [];
do {
foreach ($queue->getMessages($messageTo->getMessageId()) as $message) {
$this->lastResponse = $message;
$response = $message->getResponse();

if ($response instanceof SyncRefreshDelete) {
$syncInfoDelete = $response;
$syncRequest->setCookie($syncInfoDelete->getCookie());
$queue->sendMessage($messageTo);
} elseif ($response instanceof SyncRefreshPresent) {
$syncInfoPresent = $response;
$syncRequest->setCookie($syncInfoPresent->getCookie());
$queue->sendMessage($messageTo);
} elseif ($response instanceof SyncNewCookie) {
$syncNewCookie = $response;
$syncRequest->setCookie($syncNewCookie->getCookie());
} elseif ($response instanceof SyncIdSet) {
} else {
if ($response instanceof SearchResultEntry) {
$syncResults[] = new SyncResult(
$response->getEntry(),
$this->getSyncStateControl($message)
);
} elseif ($response instanceof SearchResultReference) {
$syncResults[] = new SyncResult(
$response->getReferrals(),
$this->getSyncStateControl($message)
);
} elseif ($response instanceof SearchResultDone) {
$isDone = true;
break;
}
}
}
} while (!$isDone);

return $syncResults;
}

private function isInitialPollRequest(SyncRequestControl $control): bool
{
return empty($control->getCookie())
&& $control->getMode() === SyncRequestControl::MODE_REFRESH_ONLY;
}

private function isContentUpdatePoll(SyncRequestControl $control): bool
{
return !empty($control->getCookie());
}

private function getSyncStateControl(LdapMessageResponse $response): ?SyncStateControl
{
$syncState = $response->controls()->get(Control::OID_SYNC_STATE);

return $syncState instanceof SyncStateControl
? $syncState
: null;
}

private function getSyncDoneControl(LdapMessageResponse $response): ?SyncDoneControl
{
$syncDone = $response->controls()->get(Control::OID_SYNC_DONE);

return $syncDone instanceof SyncDoneControl
? $syncDone
: null;
}

private function getLdapResult(LdapMessageResponse $response): LdapResult
{
$result = $response->getResponse();

if (!$result instanceof LdapResult) {
throw new RuntimeException(sprintf(
'Expected an LdapResult, but received "%s".',
$result::class
));
}

return $result;
}
}
Loading

0 comments on commit 0ee0cd8

Please sign in to comment.