Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
],
"require": {
"php": ">=8.2",
"freedsx/asn1": "dev-main#b57a38f",
"freedsx/asn1": "dev-main#d6cee05",
"freedsx/socket": "dev-main#df083d9",
"freedsx/sasl": "dev-main#ebf53ad",
"psr/log": "^3"
Expand Down
2 changes: 1 addition & 1 deletion src/FreeDSx/Ldap/Operation/Response/SearchResultEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
*/
class SearchResultEntry implements ResponseInterface
{
protected const TAG_NUMBER = 4;
public const TAG_NUMBER = 4;

public function __construct(private readonly Entry $entry) {}

Expand Down
66 changes: 65 additions & 1 deletion src/FreeDSx/Ldap/Protocol/LdapQueue.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@

namespace FreeDSx\Ldap\Protocol;

use FreeDSx\Asn1\Asn1;
use FreeDSx\Asn1\Encoder\EncoderInterface;
use FreeDSx\Asn1\Encoder\EncoderOptions;
use FreeDSx\Asn1\Exception\EncoderException;
use FreeDSx\Asn1\Exception\PduLengthException;
use FreeDSx\Asn1\Helper\AttributeEntryEncoder;
use FreeDSx\Asn1\Type\SequenceOfType;
use FreeDSx\Ldap\Exception\ProtocolException;
use FreeDSx\Ldap\Exception\RequestSizeExceededException;
use FreeDSx\Ldap\Exception\UnsolicitedNotificationException;
use FreeDSx\Ldap\Operation\Response\ExtendedResponse;
use FreeDSx\Ldap\Operation\Response\SearchResultEntry;
use FreeDSx\Ldap\Protocol\Queue\MessageWrapperInterface;
use FreeDSx\Socket\Exception\ConnectionException;
use FreeDSx\Socket\Queue\Asn1MessageQueue;
Expand All @@ -45,6 +49,8 @@ class LdapQueue extends Asn1MessageQueue

private ?MessageWrapperInterface $messageWrapper = null;

private readonly AttributeEntryEncoder $entryEncoder;

public function __construct(
Socket $socket,
?EncoderInterface $encoder = null,
Expand All @@ -54,6 +60,7 @@ public function __construct(
$socket,
$encoder ?? new LdapEncoder(new EncoderOptions(maxLength: $maxReceiveSize)),
);
$this->entryEncoder = new AttributeEntryEncoder();
}

/**
Expand Down Expand Up @@ -165,7 +172,7 @@ protected function sendLdapMessage(iterable $messages): static
$buffer = '';

foreach ($messages as $message) {
$encoded = $this->encoder->encode($message->toAsn1());
$encoded = $this->encodeMessage($message);
$this->onMessageEncoded($encoded);
$buffer .= $this->messageWrapper !== null ? $this->messageWrapper->wrap($encoded) : $encoded;
$bufferLen = strlen($buffer);
Expand Down Expand Up @@ -250,4 +257,61 @@ protected function getAndValidateMessage(?int $id): LdapMessage

return $message;
}

/**
* Encodes a message to BER, taking a dedicated fast path for search result entries.
*
* @throws EncoderException
*/
private function encodeMessage(LdapMessage $message): string
{
$response = $message instanceof LdapMessageResponse
? $message->getResponse()
: null;

if (!($response instanceof SearchResultEntry)) {
return $this->encoder->encode($message->toAsn1());
}

$envelope = Asn1::sequence(
Asn1::integer($message->getMessageId()),
Asn1::raw($this->encodeSearchResultEntry($response)),
);

$controls = $message->controls()->toArray();
if ($controls !== []) {
/** @var SequenceOfType $context */
$context = Asn1::context(
tagNumber: 0,
type: Asn1::sequenceOf(),
);
foreach ($controls as $control) {
$context->addChild($control->toAsn1());
}
$envelope->addChild($context);
}

return $this->encoder->encode($envelope);
}

/**
* @throws EncoderException
*/
private function encodeSearchResultEntry(SearchResultEntry $response): string
{
$entry = $response->getEntry();
$attributes = [];
foreach ($entry->getAttributes() as $attribute) {
$attributes[] = [
$attribute->getDescription(),
$attribute->getValues(),
];
}

return $this->entryEncoder->encode(
SearchResultEntry::TAG_NUMBER,
$entry->getDn()->toString(),
$attributes,
);
}
}
81 changes: 81 additions & 0 deletions tests/unit/Protocol/Queue/ServerQueueTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@
use FreeDSx\Asn1\Encoder\EncoderInterface;
use FreeDSx\Asn1\Type\IncompleteType;
use FreeDSx\Ldap\Control\Control;
use FreeDSx\Ldap\Control\Sync\SyncStateControl;
use FreeDSx\Ldap\Entry\Attribute;
use FreeDSx\Ldap\Entry\Entry;
use FreeDSx\Ldap\Operation\Request\AbandonRequest;
use FreeDSx\Ldap\Operation\Request\CancelRequest;
use FreeDSx\Ldap\Operation\Request\DeleteRequest;
use FreeDSx\Ldap\Operation\Response\DeleteResponse;
use FreeDSx\Ldap\Operation\Response\SearchResultEntry;
use FreeDSx\Ldap\Protocol\LdapEncoder;
use FreeDSx\Ldap\Protocol\LdapMessageRequest;
use FreeDSx\Ldap\Protocol\LdapMessageResponse;
Expand Down Expand Up @@ -418,6 +422,83 @@ public function test_it_runs_interceptors_in_order(): void
);
}

public function test_it_encodes_a_search_result_entry_identically_to_the_generic_path(): void
{
$message = new LdapMessageResponse(
2,
new SearchResultEntry(new Entry(
'cn=foo,dc=example,dc=com',
new Attribute('cn', 'foo'),
new Attribute('objectClass', 'top', 'person'),
new Attribute('sn', 'Foo'),
new Attribute('userCertificate;binary', "\x00\x01\x02"),
new Attribute('emptyAttribute'),
)),
);

self::assertSame(
(new LdapEncoder())->encode($message->toAsn1()),
$this->captureSentBytes($message),
);
}

public function test_it_encodes_a_search_result_entry_with_controls_identically(): void
{
$message = new LdapMessageResponse(
3,
new SearchResultEntry(new Entry(
'cn=bar,dc=example,dc=com',
new Attribute('cn', 'bar'),
)),
new Control('1.2.3.4'),
);

self::assertSame(
(new LdapEncoder())->encode($message->toAsn1()),
$this->captureSentBytes($message),
);
}

public function test_it_encodes_a_sync_entry_with_a_state_control_identically(): void
{
$message = new LdapMessageResponse(
4,
new SearchResultEntry(new Entry(
'cn=synced,dc=example,dc=com',
new Attribute('cn', 'synced'),
new Attribute('objectClass', 'top', 'person'),
)),
new SyncStateControl(
SyncStateControl::STATE_ADD,
str_repeat("\x11", 16),
),
);

self::assertSame(
(new LdapEncoder())->encode($message->toAsn1()),
$this->captureSentBytes($message),
);
}

/**
* Sends the message through a real-encoder queue and returns the bytes written to the socket.
*/
private function captureSentBytes(LdapMessageResponse $message): string
{
$written = '';
$socket = $this->createMock(Socket::class);
$socket->method('write')->willReturnCallback(function (string $data) use (&$written, $socket): Socket {
$written .= $data;

return $socket;
});

# A null encoder builds a real LdapEncoder, so the real search-entry fast path runs.
(new ServerQueue($socket))->sendMessage($message);

return $written;
}

private function makeQueueWithEncodedRequest(LdapMessageRequest $message): ServerQueue
{
$encoder = new LdapEncoder();
Expand Down
Loading