mirror of
https://github.com/netauth/ldap.git
synced 2026-11-03 11:43:44 -06:00
An LDAP client only accepts the result that pairs with the request it
sent, and silently ignores anything else. Unrouted operations were
answered with a generic result, which encodes with the wrong tag, so a
search with an unexpected base DN (for example the empty base that
ldapsearch uses when -b is omitted) left the client waiting forever
instead of reporting an error.
Every failure path now terminates the request with the matching result
type, panics included, so a single wrapper covers all operations rather
than one responder per operation. NetAuth calls carry a 30s deadline
because the gRPC client dials lazily and applies none of its own, and
refused operations and failed authentication are logged, which is what
an operator needs when a client appears to hang.
💘 Generated with Crush
Assisted-by: Crush:qwen38_flash_next_mtp
93 lines
2.6 KiB
Go
93 lines
2.6 KiB
Go
package ldap
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/vjeantet/goldap/message"
|
|
)
|
|
|
|
// TestUnroutedSearchTerminatesOverTheWire is the end-to-end
|
|
// regression test for the hang reported in the field: a search whose
|
|
// base DN matches no route (for example the empty base an LDAP client
|
|
// uses when no -b flag is given) must still be answered with a
|
|
// searchResDone, otherwise the client waits for one forever.
|
|
func TestUnroutedSearchTerminatesOverTheWire(t *testing.T) {
|
|
s := New(WithLogger(testLogger()), WithNetAuth(&fakeClient{}))
|
|
if err := s.SetDomain("example.com"); err != nil {
|
|
t.Fatalf("SetDomain: %s", err)
|
|
}
|
|
addr := freeAddr(t)
|
|
if err := s.Serve(addr); err != nil {
|
|
t.Fatalf("Serve: %s", err)
|
|
}
|
|
t.Cleanup(func() { s.Listener.Close() })
|
|
|
|
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
|
|
if err != nil {
|
|
t.Fatalf("dial: %s", err)
|
|
}
|
|
t.Cleanup(func() { conn.Close() })
|
|
|
|
// A search with an empty base DN and subtree scope, exactly what
|
|
// an LDAP client sends when no -b flag is given. It matches no
|
|
// route and must be answered, not silently dropped.
|
|
conn.SetWriteDeadline(time.Now().Add(2 * time.Second))
|
|
if _, err := conn.Write(berSearchRequest("")); err != nil {
|
|
t.Fatalf("sending search: %s", err)
|
|
}
|
|
|
|
// Read responses until a searchResDone arrives or the deadline
|
|
// passes; a client would block here forever if none is sent.
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
conn.SetReadDeadline(deadline)
|
|
raw, err := readLDAPPacket(conn)
|
|
if err != nil {
|
|
t.Fatalf("reading response: %s", err)
|
|
}
|
|
m, err := message.ReadLDAPMessage(message.NewBytes(0, raw))
|
|
if err != nil {
|
|
t.Fatalf("decoding response: %s", err)
|
|
}
|
|
if _, ok := m.ProtocolOp().(message.SearchResultDone); ok {
|
|
return
|
|
}
|
|
}
|
|
t.Error("no searchResDone received; an unrouted search hangs the client")
|
|
}
|
|
|
|
// readLDAPPacket reads one BER TLV (the top-level LDAPMessage
|
|
// SEQUENCE) and returns its full encoding.
|
|
func readLDAPPacket(r io.Reader) ([]byte, error) {
|
|
head := make([]byte, 2)
|
|
if _, err := io.ReadFull(r, head); err != nil {
|
|
return nil, err
|
|
}
|
|
packet := head
|
|
length := int(head[1])
|
|
if head[1]&0x80 != 0 {
|
|
nb := int(head[1] & 0x7f)
|
|
if nb == 0 || nb > 4 {
|
|
return nil, errors.New("unsupported length encoding")
|
|
}
|
|
ext := make([]byte, nb)
|
|
if _, err := io.ReadFull(r, ext); err != nil {
|
|
return nil, err
|
|
}
|
|
length = 0
|
|
for _, b := range ext {
|
|
length = length<<8 | int(b)
|
|
}
|
|
packet = append(packet, ext...)
|
|
}
|
|
body := make([]byte, length)
|
|
if _, err := io.ReadFull(r, body); err != nil {
|
|
return nil, err
|
|
}
|
|
return append(packet, body...), nil
|
|
}
|