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
1 change: 1 addition & 0 deletions docs/Server/Monitoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ off rather than returned empty.
| `connectionsRejected` | Connections turned away at the connection limit. |
| `connectionsWriteTimeouts`, `connectionsIdleTimeouts` | Connections closed by the write or idle timeout. |
| `connectionsRequestSizeExceeded` | Connections dropped because a request exceeded `setMaxRequestSize`. |
| `connectionsProtocolErrors` | Connections dropped by a malformed/undecodable request PDU (Notice of Disconnection). |
| `connectionsMax` | The configured connection limit (`0` is unlimited). |
| `operationsCompleted`, `operationsFailed` | Total operations and the failed subset. |
| `operationsByType` | Per-type counts, e.g. `search=1402, bind=210, add=8`. |
Expand Down
1 change: 1 addition & 0 deletions src/FreeDSx/Ldap/Protocol/ServerProtocolHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ public function handle(): ?ConnectionObservation
# Per RFC 4511 §4.1.1, a PDU that cannot be processed (malformed) warrants a disconnect with a protocol
# error. The NoticeOfDisconnectSent event records the specific reason.
$this->sendNoticeOfDisconnect('The message could not be processed.');
$closeReason = ConnectionObservation::ProtocolError;
} catch (Throwable $e) {
if ($this->queue->isConnected()) {
$this->sendNoticeOfDisconnect(cause: $e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ private function attributes(MetricsSnapshot $snapshot): array
'connectionsWriteTimeouts' => [(string) $connections->writeTimeouts],
'connectionsIdleTimeouts' => [(string) $connections->idleTimeouts],
'connectionsRequestSizeExceeded' => [(string) $connections->requestSizeExceeded],
'connectionsProtocolErrors' => [(string) $connections->protocolErrors],
'connectionsMax' => [(string) $this->options->getMaxConnections()],
'operationsCompleted' => [(string) $operations->total()],
'operationsFailed' => [(string) $operations->totalErrors()],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
namespace FreeDSx\Ldap\Protocol\ServerProtocolHandler;

use FreeDSx\Ldap\Control\PagingControl;
use FreeDSx\Ldap\Control\Sorting\SortingResponseControl;
use FreeDSx\Ldap\Entry\Entries;
use FreeDSx\Ldap\Entry\Entry;
use FreeDSx\Ldap\Exception\OperationException;
Expand Down Expand Up @@ -119,9 +118,12 @@ public function handleRequest(
$controls[] = new PagingControl(0, '');
}

$sortControl = $this->sortingControl($message);
if ($sortControl !== null) {
$controls[] = new SortingResponseControl(0);
$sortResponse = $this->sortingResponseControl(
$this->sortingControl($message),
$this->schema,
);
if ($sortResponse !== null) {
$controls[] = $sortResponse;
}

$pagingRequest->markProcessed();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

namespace FreeDSx\Ldap\Protocol\ServerProtocolHandler;

use FreeDSx\Ldap\Control\Sorting\SortingResponseControl;
use FreeDSx\Ldap\Entry\Entry;
use FreeDSx\Ldap\Operation\Request\AbandonRequest;
use FreeDSx\Ldap\Operation\Request\CancelRequest;
Expand Down Expand Up @@ -88,9 +87,12 @@ public function handleRequest(
$state,
);

$sortControl = $this->sortingControl($message);
$responseControls = $sortControl !== null
? [new SortingResponseControl(0)]
$sortResponse = $this->sortingResponseControl(
$this->sortingControl($message),
$this->schema,
);
$responseControls = $sortResponse !== null
? [$sortResponse]
: [];

$this->sendEntriesToClient(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use FreeDSx\Ldap\Control\ControlBag;
use FreeDSx\Ldap\Control\PagingControl;
use FreeDSx\Ldap\Control\Sorting\SortingControl;
use FreeDSx\Ldap\Control\Sorting\SortingResponseControl;
use FreeDSx\Ldap\Entry\Dn;
use FreeDSx\Ldap\Exception\OperationException;
use FreeDSx\Ldap\Exception\RuntimeException;
Expand All @@ -30,6 +31,7 @@
use FreeDSx\Ldap\Protocol\LdapMessageRequest;
use FreeDSx\Ldap\Protocol\LdapMessageResponse;
use FreeDSx\Ldap\Protocol\Queue\ServerQueue;
use FreeDSx\Ldap\Schema\Schema;
use Generator;

trait ServerSearchTrait
Expand Down Expand Up @@ -191,4 +193,41 @@ private function sortingControl(LdapMessageRequest $message): ?SortingControl
? $control
: null;
}

/**
* The RFC 2891 sort response control, with the result code per §1.2 (first unsortable key wins).
*/
private function sortingResponseControl(
?SortingControl $sortControl,
Schema $schema,
): ?SortingResponseControl {
if ($sortControl === null) {
return null;
}

foreach ($sortControl->getSortKeys() as $sortKey) {
$attribute = $sortKey->getAttribute();
$attributeType = $schema->getAttributeType($attribute);

if ($attributeType === null) {
return new SortingResponseControl(
ResultCode::NO_SUCH_ATTRIBUTE,
$attribute,
);
}

$orderingRule = $sortKey->getOrderingRule();
$unknownRule = $orderingRule !== null
&& $schema->getMatchingRule($orderingRule) === null;

if ($unknownRule || ($orderingRule === null && $attributeType->orderingOid === null)) {
return new SortingResponseControl(
ResultCode::INAPPROPRIATE_MATCHING,
$attribute,
);
}
}

return new SortingResponseControl(ResultCode::SUCCESS);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@ enum ConnectionObservation: string
case IdleTimeout = 'idle_timeout';

case RequestSizeExceeded = 'request_size_exceeded';

case ProtocolError = 'protocol_error';
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ final class InMemoryMetricsRecorder implements MetricsRecorderInterface, Metrics
*/
private int $requestSizeExceeded = 0;

/**
* @var int<0, max>
*/
private int $protocolErrors = 0;

/**
* @var array<string, int<0, max>>
*/
Expand Down Expand Up @@ -177,6 +182,7 @@ public function connectionObserved(ConnectionObservation $observation): void
ConnectionObservation::WriteTimeout => $this->writeTimeouts++,
ConnectionObservation::IdleTimeout => $this->idleTimeouts++,
ConnectionObservation::RequestSizeExceeded => $this->requestSizeExceeded++,
ConnectionObservation::ProtocolError => $this->protocolErrors++,
};
}

Expand Down Expand Up @@ -206,6 +212,7 @@ public function snapshot(): MetricsSnapshot
$this->writeTimeouts,
$this->idleTimeouts,
$this->requestSizeExceeded,
$this->protocolErrors,
),
$this->operationMetrics(),
$this->operationsInProgress,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function __construct(
public int $writeTimeouts = 0,
public int $idleTimeouts = 0,
public int $requestSizeExceeded = 0,
public int $protocolErrors = 0,
) {}

/**
Expand All @@ -41,6 +42,7 @@ public function toArray(): array
'write_timeouts' => $this->writeTimeouts,
'idle_timeouts' => $this->idleTimeouts,
'request_size_exceeded' => $this->requestSizeExceeded,
'protocol_errors' => $this->protocolErrors,
];
}

Expand All @@ -56,6 +58,7 @@ public static function fromArray(array $data): self
writeTimeouts: SnapshotValue::toInt($data['write_timeouts'] ?? null),
idleTimeouts: SnapshotValue::toInt($data['idle_timeouts'] ?? null),
requestSizeExceeded: SnapshotValue::toInt($data['request_size_exceeded'] ?? null),
protocolErrors: SnapshotValue::toInt($data['protocol_errors'] ?? null),
);
}
}
4 changes: 4 additions & 0 deletions src/FreeDSx/Ldap/Server/ServerRunner/PcntlServerRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ class PcntlServerRunner implements ServerRunnerInterface

private const EXIT_CODE_REQUEST_SIZE_EXCEEDED = 12;

private const EXIT_CODE_PROTOCOL_ERROR = 13;

private SocketServer $server;

/**
Expand Down Expand Up @@ -214,6 +216,7 @@ private function childExitCodeFor(?ConnectionObservation $closeReason): int
ConnectionObservation::WriteTimeout => self::EXIT_CODE_WRITE_TIMEOUT,
ConnectionObservation::IdleTimeout => self::EXIT_CODE_IDLE_TIMEOUT,
ConnectionObservation::RequestSizeExceeded => self::EXIT_CODE_REQUEST_SIZE_EXCEEDED,
ConnectionObservation::ProtocolError => self::EXIT_CODE_PROTOCOL_ERROR,
default => 0,
};
}
Expand All @@ -233,6 +236,7 @@ private function recordChildCloseReason(
self::EXIT_CODE_WRITE_TIMEOUT => ConnectionObservation::WriteTimeout,
self::EXIT_CODE_IDLE_TIMEOUT => ConnectionObservation::IdleTimeout,
self::EXIT_CODE_REQUEST_SIZE_EXCEEDED => ConnectionObservation::RequestSizeExceeded,
self::EXIT_CODE_PROTOCOL_ERROR => ConnectionObservation::ProtocolError,
default => null,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,19 @@ public function test_it_reports_connections_closed_by_an_oversized_request(): vo
);
}

public function test_it_reports_connections_closed_by_a_protocol_error(): void
{
$this->metrics->connectionObserved(ConnectionObservation::ProtocolError);
$this->metrics->connectionObserved(ConnectionObservation::ProtocolError);

$entry = $this->handleAndCaptureEntry();

self::assertSame(
['2'],
$entry->get('connectionsProtocolErrors')?->getValues(),
);
}

public function test_it_reports_traffic_totals(): void
{
$this->metrics->trafficObserved(new TrafficObservation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use FreeDSx\Ldap\Protocol\Queue\ServerQueue;
use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerPagingHandler;
use FreeDSx\Ldap\Schema\Schema;
use FreeDSx\Ldap\Schema\StandardSchemaProvider;
use FreeDSx\Ldap\Search\Filters;
use FreeDSx\Ldap\Server\AccessControl\AccessControlInterface;
use FreeDSx\Ldap\Server\Backend\LdapBackendInterface;
Expand Down Expand Up @@ -77,7 +78,7 @@ protected function setUp(): void
$this->mockFilterEvaluator = $this->createMock(FilterEvaluatorInterface::class);
$this->mockAccessControl = $this->createMock(AccessControlInterface::class);
$this->requestHistory = new RequestHistory();
$this->schema = new Schema();
$this->schema = StandardSchemaProvider::buildCore();
$this->sentMessages = [];

$this->mockFilterEvaluator
Expand Down Expand Up @@ -623,6 +624,39 @@ public function test_sort_control_appends_sorting_response_control_to_done_messa
);
}

public function test_sort_by_unknown_attribute_reports_no_such_attribute(): void
{
$message = new LdapMessageRequest(
2,
$this->makeSearchRequest(),
new PagingControl(10, ''),
new SortingControl(SortKey::ascending('bogusAttr')),
);

$this->mockBackend
->method('search')
->willReturn(new EntryStream($this->makeGenerator()));

$this->subject->handleRequest(
$message,
$this->mockToken,
);

$sortControl = $this->doneMessage()->controls()->get(Control::OID_SORTING_RESPONSE);
self::assertInstanceOf(
SortingResponseControl::class,
$sortControl,
);
self::assertSame(
ResultCode::NO_SUCH_ATTRIBUTE,
$sortControl->getResult(),
);
self::assertSame(
'bogusAttr',
$sortControl->getAttribute(),
);
}

public function test_no_sort_control_does_not_append_sorting_response_control(): void
{
$message = $this->makeSearchMessage(size: 10);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use FreeDSx\Ldap\Protocol\Queue\ServerQueue;
use FreeDSx\Ldap\Protocol\ServerProtocolHandler\ServerSearchHandler;
use FreeDSx\Ldap\Schema\Schema;
use FreeDSx\Ldap\Schema\StandardSchemaProvider;
use FreeDSx\Ldap\Search\Filters;
use FreeDSx\Ldap\Server\AccessControl\AccessControlInterface;
use FreeDSx\Ldap\Server\Backend\LdapBackendInterface;
Expand Down Expand Up @@ -73,7 +74,7 @@ protected function setUp(): void
$this->mockBackend = $this->createMock(LdapBackendInterface::class);
$this->mockFilterEvaluator = $this->createMock(FilterEvaluatorInterface::class);
$this->mockAccessControl = $this->createMock(AccessControlInterface::class);
$this->schema = new Schema();
$this->schema = StandardSchemaProvider::buildCore();
$this->sentMessages = [];

$this->mockAccessControl
Expand Down Expand Up @@ -646,6 +647,70 @@ public function test_sort_control_appends_sorting_response_control_to_done_messa
);
}

public function test_sort_by_unknown_attribute_reports_no_such_attribute(): void
{
$search = new LdapMessageRequest(
2,
(new SearchRequest(Filters::present('cn')))->base('dc=foo,dc=bar'),
new SortingControl(SortKey::ascending('bogusAttr')),
);

$this->mockBackend
->method('search')
->willReturn(new EntryStream($this->makeGenerator()));

$this->subject->handleRequest(
$search,
$this->mockToken,
);

$done = end($this->sentMessages);
self::assertInstanceOf(LdapMessageResponse::class, $done);
$sortControl = $done->controls()->get(Control::OID_SORTING_RESPONSE);
self::assertInstanceOf(
SortingResponseControl::class,
$sortControl,
);
self::assertSame(
ResultCode::NO_SUCH_ATTRIBUTE,
$sortControl->getResult(),
);
self::assertSame(
'bogusAttr',
$sortControl->getAttribute(),
);
}

public function test_sort_by_attribute_without_ordering_rule_reports_inappropriate_matching(): void
{
$search = new LdapMessageRequest(
2,
(new SearchRequest(Filters::present('cn')))->base('dc=foo,dc=bar'),
new SortingControl(SortKey::ascending('userPassword')),
);

$this->mockBackend
->method('search')
->willReturn(new EntryStream($this->makeGenerator()));

$this->subject->handleRequest(
$search,
$this->mockToken,
);

$done = end($this->sentMessages);
self::assertInstanceOf(LdapMessageResponse::class, $done);
$sortControl = $done->controls()->get(Control::OID_SORTING_RESPONSE);
self::assertInstanceOf(
SortingResponseControl::class,
$sortControl,
);
self::assertSame(
ResultCode::INAPPROPRIATE_MATCHING,
$sortControl->getResult(),
);
}

public function test_no_sort_control_does_not_append_sorting_response_control(): void
{
$search = new LdapMessageRequest(
Expand Down
Loading
Loading