diff --git a/client.go b/client.go index 5799f39b..b7193345 100644 --- a/client.go +++ b/client.go @@ -36,4 +36,5 @@ type Client interface { SearchAsync(ctx context.Context, searchRequest *SearchRequest, bufferSize int) Response SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) DirSync(searchRequest *SearchRequest, flags, maxAttrCount int64, cookie []byte) (*SearchResult, error) + Syncrepl(ctx context.Context, searchRequest *SearchRequest, bufferSize int, mode ControlSyncRequestMode, cookie []byte) Response } diff --git a/control.go b/control.go index 8bbc026f..16b6cca1 100644 --- a/control.go +++ b/control.go @@ -51,6 +51,10 @@ var ControlTypeMap = map[string]string{ ControlTypeMicrosoftShowDeleted: "Show Deleted Objects - Microsoft", ControlTypeMicrosoftServerLinkTTL: "Return TTL-DNs for link values with associated expiry times - Microsoft", ControlTypeDirSync: "DirSync", + ControlTypeSyncRequest: "Sync Request", + ControlTypeSyncState: "Sync State", + ControlTypeSyncDone: "Sync Done", + ControlTypeSyncInfo: "Sync Info", } // Control defines an interface controls provide to encode and describe themselves diff --git a/control_syncrepl.go b/control_syncrepl.go new file mode 100644 index 00000000..46866f7f --- /dev/null +++ b/control_syncrepl.go @@ -0,0 +1,427 @@ +package ldap + +import ( + "fmt" + + ber "github.com/go-asn1-ber/asn1-ber" + "github.com/google/uuid" +) + +const ( + // ControlTypeSyncRequest - https://www.ietf.org/rfc/rfc4533.txt + ControlTypeSyncRequest = "1.3.6.1.4.1.4203.1.9.1.1" + // ControlTypeSyncState - https://www.ietf.org/rfc/rfc4533.txt + ControlTypeSyncState = "1.3.6.1.4.1.4203.1.9.1.2" + // ControlTypeSyncDone - https://www.ietf.org/rfc/rfc4533.txt + ControlTypeSyncDone = "1.3.6.1.4.1.4203.1.9.1.3" + // ControlTypeSyncInfo - https://www.ietf.org/rfc/rfc4533.txt + ControlTypeSyncInfo = "1.3.6.1.4.1.4203.1.9.1.4" +) + +func DecodeSyncReplControl(packet *ber.Packet) (Control, error) { + var ( + controlType string + value *ber.Packet + err error + ) + switch len(packet.Children) { + case 0: + return nil, nil + case 1: + return nil, nil + case 2: + controlType = packet.Children[0].Data.String() + value, err = ber.DecodePacketErr(packet.Children[1].Data.Bytes()) + if err != nil { + return nil, fmt.Errorf("failed to decode data bytes: %w", err) + } + default: + return nil, fmt.Errorf("unsupported handling children: %d", len(packet.Children)) + } + + switch controlType { + case ControlTypeSyncState: + value.Description += " (Sync State)" + return NewControlSyncState(value) + case ControlTypeSyncDone: + value.Description += " (Sync Done)" + return NewControlSyncDone(value) + case ControlTypeSyncInfo: + value.Description += " (Sync Info)" + return NewControlSyncInfo(value) + default: + return nil, fmt.Errorf("unsupported control type: %s", controlType) + } +} + +// Mode for ControlTypeSyncRequest +type ControlSyncRequestMode int64 + +const ( + SyncRequestModeRefreshOnly ControlSyncRequestMode = 1 + SyncRequestModeRefreshAndPersist ControlSyncRequestMode = 3 +) + +// ControlSyncRequest implements the Sync Request Control described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncRequest struct { + Criticality bool + Mode ControlSyncRequestMode + Cookie []byte + ReloadHint bool +} + +func NewControlSyncRequest(mode ControlSyncRequestMode, cookie []byte) *ControlSyncRequest { + return &ControlSyncRequest{ + Criticality: true, + Mode: mode, + Cookie: cookie, + ReloadHint: false, + } +} + +// GetControlType returns the OID +func (c *ControlSyncRequest) GetControlType() string { + return ControlTypeSyncRequest +} + +// Encode encodes the control +func (c *ControlSyncRequest) Encode() *ber.Packet { + _mode := int64(c.Mode) + mode := ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, _mode, "Mode") + cookie := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Cookie") + cookie.Value = c.Cookie + cookie.Data.Write(c.Cookie) + reloadHint := ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.ReloadHint, "Reload Hint") + + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeSyncRequest, "Control Type ("+ControlTypeMap[ControlTypeSyncRequest]+")")) + packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) + + val := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (Sync Request)") + seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Sync Request Value") + seq.AppendChild(mode) + seq.AppendChild(cookie) + seq.AppendChild(reloadHint) + val.AppendChild(seq) + + packet.AppendChild(val) + return packet +} + +// String returns a human-readable description +func (c *ControlSyncRequest) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %t Mode: %d Cookie: %s ReloadHint: %t", + ControlTypeMap[ControlTypeSyncRequest], + ControlTypeSyncRequest, + c.Criticality, + c.Mode, + string(c.Cookie), + c.ReloadHint, + ) +} + +// State for ControlSyncState +type ControlSyncStateState int64 + +const ( + SyncStatePresent ControlSyncStateState = 0 + SyncStateAdd ControlSyncStateState = 1 + SyncStateModify ControlSyncStateState = 2 + SyncStateDelete ControlSyncStateState = 3 +) + +// ControlSyncState implements the Sync State Control described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncState struct { + Criticality bool + State ControlSyncStateState + EntryUUID uuid.UUID + Cookie []byte +} + +func NewControlSyncState(pkt *ber.Packet) (*ControlSyncState, error) { + var ( + state ControlSyncStateState + entryUUID uuid.UUID + cookie []byte + err error + ) + switch len(pkt.Children) { + case 0, 1: + return nil, fmt.Errorf("at least two children are required: %d", len(pkt.Children)) + case 2: + state = ControlSyncStateState(pkt.Children[0].Value.(int64)) + entryUUID, err = uuid.FromBytes(pkt.Children[1].ByteValue) + if err != nil { + return nil, fmt.Errorf("failed to decode uuid: %w", err) + } + case 3: + state = ControlSyncStateState(pkt.Children[0].Value.(int64)) + entryUUID, err = uuid.FromBytes(pkt.Children[1].ByteValue) + if err != nil { + return nil, fmt.Errorf("failed to decode uuid: %w", err) + } + cookie = pkt.Children[2].ByteValue + } + return &ControlSyncState{ + Criticality: false, + State: state, + EntryUUID: entryUUID, + Cookie: cookie, + }, nil +} + +// GetControlType returns the OID +func (c *ControlSyncState) GetControlType() string { + return ControlTypeSyncState +} + +// Encode encodes the control +func (c *ControlSyncState) Encode() *ber.Packet { + return nil +} + +// String returns a human-readable description +func (c *ControlSyncState) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %t State: %d EntryUUID: %s Cookie: %s", + ControlTypeMap[ControlTypeSyncState], + ControlTypeSyncState, + c.Criticality, + c.State, + c.EntryUUID.String(), + string(c.Cookie), + ) +} + +// ControlSyncDone implements the Sync Done Control described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncDone struct { + Criticality bool + Cookie []byte + RefreshDeletes bool +} + +func NewControlSyncDone(pkt *ber.Packet) (*ControlSyncDone, error) { + var ( + cookie []byte + refreshDeletes bool + ) + switch len(pkt.Children) { + case 0: + // have nothing to do + case 1: + cookie = pkt.Children[0].ByteValue + case 2: + cookie = pkt.Children[0].ByteValue + refreshDeletes = pkt.Children[1].Value.(bool) + } + return &ControlSyncDone{ + Criticality: false, + Cookie: cookie, + RefreshDeletes: refreshDeletes, + }, nil +} + +// GetControlType returns the OID +func (c *ControlSyncDone) GetControlType() string { + return ControlTypeSyncDone +} + +// Encode encodes the control +func (c *ControlSyncDone) Encode() *ber.Packet { + return nil +} + +// String returns a human-readable description +func (c *ControlSyncDone) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %t Cookie: %s RefreshDeletes: %t", + ControlTypeMap[ControlTypeSyncDone], + ControlTypeSyncDone, + c.Criticality, + string(c.Cookie), + c.RefreshDeletes, + ) +} + +// Tag For ControlSyncInfo +type ControlSyncInfoValue uint64 + +const ( + SyncInfoNewcookie ControlSyncInfoValue = 0 + SyncInfoRefreshDelete ControlSyncInfoValue = 1 + SyncInfoRefreshPresent ControlSyncInfoValue = 2 + SyncInfoSyncIdSet ControlSyncInfoValue = 3 +) + +// ControlSyncInfoNewCookie implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncInfoNewCookie struct { + Cookie []byte +} + +// String returns a human-readable description +func (c *ControlSyncInfoNewCookie) String() string { + return fmt.Sprintf( + "NewCookie[Cookie: %s]", + string(c.Cookie), + ) +} + +// ControlSyncInfoRefreshDelete implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncInfoRefreshDelete struct { + Cookie []byte + RefreshDone bool +} + +// String returns a human-readable description +func (c *ControlSyncInfoRefreshDelete) String() string { + return fmt.Sprintf( + "RefreshDelete[Cookie: %s RefreshDone: %t]", + string(c.Cookie), + c.RefreshDone, + ) +} + +// ControlSyncInfoRefreshPresent implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncInfoRefreshPresent struct { + Cookie []byte + RefreshDone bool +} + +// String returns a human-readable description +func (c *ControlSyncInfoRefreshPresent) String() string { + return fmt.Sprintf( + "RefreshPresent[Cookie: %s RefreshDone: %t]", + string(c.Cookie), + c.RefreshDone, + ) +} + +// ControlSyncInfoSyncIdSet implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncInfoSyncIdSet struct { + Cookie []byte + RefreshDeletes bool + SyncUUIDs []uuid.UUID +} + +// String returns a human-readable description +func (c *ControlSyncInfoSyncIdSet) String() string { + return fmt.Sprintf( + "SyncIdSet[Cookie: %s RefreshDeletes: %t SyncUUIDs: %v]", + string(c.Cookie), + c.RefreshDeletes, + c.SyncUUIDs, + ) +} + +// ControlSyncInfo implements the Sync Info Control described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncInfo struct { + Criticality bool + Value ControlSyncInfoValue + NewCookie *ControlSyncInfoNewCookie + RefreshDelete *ControlSyncInfoRefreshDelete + RefreshPresent *ControlSyncInfoRefreshPresent + SyncIdSet *ControlSyncInfoSyncIdSet +} + +func NewControlSyncInfo(pkt *ber.Packet) (*ControlSyncInfo, error) { + var ( + cookie []byte + refreshDone = true + refreshDeletes bool + syncUUIDs []uuid.UUID + ) + c := &ControlSyncInfo{Criticality: false} + switch ControlSyncInfoValue(pkt.Identifier.Tag) { + case SyncInfoNewcookie: + c.Value = SyncInfoNewcookie + c.NewCookie = &ControlSyncInfoNewCookie{ + Cookie: pkt.ByteValue, + } + case SyncInfoRefreshDelete: + c.Value = SyncInfoRefreshDelete + switch len(pkt.Children) { + case 0: + // have nothing to do + case 1: + cookie = pkt.Children[0].ByteValue + case 2: + cookie = pkt.Children[0].ByteValue + refreshDone = pkt.Children[1].Value.(bool) + } + c.RefreshDelete = &ControlSyncInfoRefreshDelete{ + Cookie: cookie, + RefreshDone: refreshDone, + } + case SyncInfoRefreshPresent: + c.Value = SyncInfoRefreshPresent + switch len(pkt.Children) { + case 0: + // have nothing to do + case 1: + cookie = pkt.Children[0].ByteValue + case 2: + cookie = pkt.Children[0].ByteValue + refreshDone = pkt.Children[1].Value.(bool) + } + c.RefreshPresent = &ControlSyncInfoRefreshPresent{ + Cookie: cookie, + RefreshDone: refreshDone, + } + case SyncInfoSyncIdSet: + c.Value = SyncInfoSyncIdSet + switch len(pkt.Children) { + case 0: + // have nothing to do + case 1: + cookie = pkt.Children[0].ByteValue + case 2: + cookie = pkt.Children[0].ByteValue + refreshDeletes = pkt.Children[1].Value.(bool) + case 3: + cookie = pkt.Children[0].ByteValue + refreshDeletes = pkt.Children[1].Value.(bool) + syncUUIDs = make([]uuid.UUID, 0, len(pkt.Children[2].Children)) + for _, child := range pkt.Children[2].Children { + u, err := uuid.FromBytes(child.ByteValue) + if err != nil { + return nil, fmt.Errorf("failed to decode uuid: %w", err) + } + syncUUIDs = append(syncUUIDs, u) + } + } + c.SyncIdSet = &ControlSyncInfoSyncIdSet{ + Cookie: cookie, + RefreshDeletes: refreshDeletes, + SyncUUIDs: syncUUIDs, + } + default: + return nil, fmt.Errorf("unknown sync info value: %d", pkt.Identifier.Tag) + } + return c, nil +} + +// GetControlType returns the OID +func (c *ControlSyncInfo) GetControlType() string { + return ControlTypeSyncInfo +} + +// Encode encodes the control +func (c *ControlSyncInfo) Encode() *ber.Packet { + return nil +} + +// String returns a human-readable description +func (c *ControlSyncInfo) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %t Value: %d %s %s %s %s", + ControlTypeMap[ControlTypeSyncInfo], + ControlTypeSyncInfo, + c.Criticality, + c.Value, + c.NewCookie, + c.RefreshDelete, + c.RefreshPresent, + c.SyncIdSet, + ) +} diff --git a/examples_test.go b/examples_test.go index 61f16197..8b4e17b5 100644 --- a/examples_test.go +++ b/examples_test.go @@ -80,6 +80,43 @@ func ExampleConn_SearchAsync() { } } +// This example demonstrates how to do syncrepl (persistent search) +func ExampleConn_Syncrepl() { + l, err := DialURL(fmt.Sprintf("%s:%d", "ldap.example.com", 389)) + if err != nil { + log.Fatal(err) + } + defer l.Close() + + searchRequest := NewSearchRequest( + "dc=example,dc=com", // The base dn to search + ScopeWholeSubtree, NeverDerefAliases, 0, 0, false, + "(&(objectClass=organizationalPerson))", // The filter to apply + []string{"dn", "cn"}, // A list attributes to retrieve + nil, + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + mode := SyncRequestModeRefreshAndPersist + var cookie []byte = nil + r := l.Syncrepl(ctx, searchRequest, 64, mode, cookie) + for r.Next() { + entry := r.Entry() + if entry != nil { + fmt.Printf("%s has DN %s\n", entry.GetAttributeValue("cn"), entry.DN) + } + controlls := r.Controls() + if len(controlls) != 0 { + fmt.Printf("%s", controlls) + } + } + if err := r.Err(); err != nil { + log.Fatal(err) + } +} + // This example demonstrates how to start a TLS connection func ExampleConn_StartTLS() { l, err := DialURL("ldap://ldap.example.com:389") diff --git a/go.mod b/go.mod index df4841f6..b201fd8b 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 github.com/go-asn1-ber/asn1-ber v1.5.4 + github.com/google/uuid v1.3.0 github.com/stretchr/testify v1.8.0 golang.org/x/crypto v0.7.0 // indirect ) diff --git a/go.sum b/go.sum index bb57aeae..9b27dce7 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/ldap.go b/ldap.go index e2c758fb..90837a77 100644 --- a/ldap.go +++ b/ldap.go @@ -32,6 +32,7 @@ const ( ApplicationSearchResultReference = 19 ApplicationExtendedRequest = 23 ApplicationExtendedResponse = 24 + ApplicationIntermediateResponse = 25 ) // ApplicationMap contains human readable descriptions of LDAP Application Codes @@ -56,6 +57,7 @@ var ApplicationMap = map[uint8]string{ ApplicationSearchResultReference: "Search Result Reference", ApplicationExtendedRequest: "Extended Request", ApplicationExtendedResponse: "Extended Response", + ApplicationIntermediateResponse: "Intermediate Response", } // Ldap Behera Password Policy Draft 10 (https://tools.ietf.org/html/draft-behera-ldap-password-policy-10) diff --git a/response_syncrepl.go b/response_syncrepl.go new file mode 100644 index 00000000..4cbd0ffb --- /dev/null +++ b/response_syncrepl.go @@ -0,0 +1,156 @@ +package ldap + +import ( + "context" + "errors" + "fmt" + + ber "github.com/go-asn1-ber/asn1-ber" +) + +type syncReplResponse struct { + sr *searchResponse +} + +// Entry returns an entry from the given search request +func (r *syncReplResponse) Entry() *Entry { + return r.sr.entry +} + +// Referral returns a referral from the given search request +func (r *syncReplResponse) Referral() string { + return r.sr.referral +} + +// Controls returns controls from the given search request +func (r *syncReplResponse) Controls() []Control { + return r.sr.controls +} + +// Err returns an error when the given search request was failed +func (r *syncReplResponse) Err() error { + return r.sr.err +} + +// Next returns whether next data exist or not +func (r *syncReplResponse) Next() bool { + return r.sr.Next() +} + +func (r *syncReplResponse) start(ctx context.Context, searchRequest *SearchRequest) { + go func() { + defer func() { + close(r.sr.ch) + if err := recover(); err != nil { + r.sr.conn.err = fmt.Errorf("ldap: recovered panic in syncReplResponse: %v", err) + } + }() + + if r.sr.conn.IsClosing() { + return + } + + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, r.sr.conn.nextMessageID(), "MessageID")) + // encode search request + err := searchRequest.appendTo(packet) + if err != nil { + r.sr.ch <- &SearchSingleResult{Error: err} + return + } + r.sr.conn.Debug.PrintPacket(packet) + + msgCtx, err := r.sr.conn.sendMessage(packet) + if err != nil { + r.sr.ch <- &SearchSingleResult{Error: err} + return + } + defer r.sr.conn.finishMessage(msgCtx) + + for { + select { + case <-ctx.Done(): + r.sr.conn.Debug.Printf("%d: %s", msgCtx.id, ctx.Err().Error()) + return + default: + r.sr.conn.Debug.Printf("%d: waiting for response", msgCtx.id) + packetResponse, ok := <-msgCtx.responses + if !ok { + err := NewError(ErrorNetwork, errors.New("ldap: response channel closed")) + r.sr.ch <- &SearchSingleResult{Error: err} + return + } + packet, err = packetResponse.ReadPacket() + r.sr.conn.Debug.Printf("%d: got response %p", msgCtx.id, packet) + if err != nil { + r.sr.ch <- &SearchSingleResult{Error: err} + return + } + + switch packet.Children[1].Tag { + case ApplicationSearchResultEntry: + result := &SearchSingleResult{ + Entry: &Entry{ + DN: packet.Children[1].Children[0].Value.(string), + Attributes: unpackAttributes(packet.Children[1].Children[1].Children), + }, + } + if len(packet.Children) != 3 { + r.sr.ch <- result + continue + } + controlPacket := packet.Children[2].Children[0] + decoded, err := DecodeSyncReplControl(controlPacket) + if err != nil { + werr := fmt.Errorf("failed to decode search result entry: %w", err) + result.Error = werr + r.sr.ch <- result + return + } + result.Controls = append(result.Controls, decoded) + r.sr.ch <- result + + case ApplicationSearchResultDone: + if err := GetLDAPError(packet); err != nil { + r.sr.ch <- &SearchSingleResult{Error: err} + return + } + if len(packet.Children) != 3 { + return + } + controlPacket := packet.Children[2].Children[0] + decoded, err := DecodeSyncReplControl(controlPacket) + if err != nil { + werr := fmt.Errorf("failed to decode search result done: %w", err) + r.sr.ch <- &SearchSingleResult{Error: werr} + return + } + result := &SearchSingleResult{} + result.Controls = append(result.Controls, decoded) + r.sr.ch <- result + return + + case ApplicationIntermediateResponse: + decoded, err := DecodeSyncReplControl(packet.Children[1]) + if err != nil { + werr := fmt.Errorf("failed to decode intermediate response: %w", err) + r.sr.ch <- &SearchSingleResult{Error: werr} + return + } + result := &SearchSingleResult{} + result.Controls = append(result.Controls, decoded) + r.sr.ch <- result + + default: + r.sr.conn.Debug.Printf("got application code: %d", packet.Children[1].Tag) + } + } + } + }() +} + +func newSyncReplResponse(conn *Conn, bufferSize int) *syncReplResponse { + return &syncReplResponse{ + sr: newSearchResponse(conn, bufferSize), + } +} diff --git a/search.go b/search.go index 3d8d9e70..83fc2959 100644 --- a/search.go +++ b/search.go @@ -585,7 +585,7 @@ func (l *Conn) Search(searchRequest *SearchRequest) (*SearchResult, error) { // SearchAsync performs a search request and returns all search results asynchronously. // This means you get all results until an error happens (or the search successfully finished), // e.g. for size / time limited requests all are recieved until the limit is reached. -// To stop the search, call cancel function returned context. +// To stop the search, call cancel function of the context. func (l *Conn) SearchAsync( ctx context.Context, searchRequest *SearchRequest, bufferSize int) Response { r := newSearchResponse(l, bufferSize) @@ -593,6 +593,21 @@ func (l *Conn) SearchAsync( return r } +// Syncrepl is a short name for LDAP Sync Replication engine that works on the +// consumer-side. This can perform a persistent search and returns an entry +// when the entry is updated on the server side. +// To stop the search, call cancel function of the context. +func (l *Conn) Syncrepl( + ctx context.Context, searchRequest *SearchRequest, bufferSize int, + mode ControlSyncRequestMode, cookie []byte, +) Response { + control := NewControlSyncRequest(mode, cookie) + searchRequest.Controls = append(searchRequest.Controls, control) + r := newSyncReplResponse(l, bufferSize) + r.start(ctx, searchRequest) + return r +} + // unpackAttributes will extract all given LDAP attributes and it's values // from the ber.Packet func unpackAttributes(children []*ber.Packet) []*EntryAttribute { diff --git a/v3/client.go b/v3/client.go index 5799f39b..b7193345 100644 --- a/v3/client.go +++ b/v3/client.go @@ -36,4 +36,5 @@ type Client interface { SearchAsync(ctx context.Context, searchRequest *SearchRequest, bufferSize int) Response SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) DirSync(searchRequest *SearchRequest, flags, maxAttrCount int64, cookie []byte) (*SearchResult, error) + Syncrepl(ctx context.Context, searchRequest *SearchRequest, bufferSize int, mode ControlSyncRequestMode, cookie []byte) Response } diff --git a/v3/control.go b/v3/control.go index 0be697e2..ba4163d9 100644 --- a/v3/control.go +++ b/v3/control.go @@ -58,6 +58,10 @@ var ControlTypeMap = map[string]string{ ControlTypeServerSideSorting: "Server Side Sorting Request - LDAP Control Extension for Server Side Sorting of Search Results (RFC2891)", ControlTypeServerSideSortingResult: "Server Side Sorting Results - LDAP Control Extension for Server Side Sorting of Search Results (RFC2891)", ControlTypeDirSync: "DirSync", + ControlTypeSyncRequest: "Sync Request", + ControlTypeSyncState: "Sync State", + ControlTypeSyncDone: "Sync Done", + ControlTypeSyncInfo: "Sync Info", } // Control defines an interface controls provide to encode and describe themselves diff --git a/v3/control_syncrepl.go b/v3/control_syncrepl.go new file mode 100644 index 00000000..46866f7f --- /dev/null +++ b/v3/control_syncrepl.go @@ -0,0 +1,427 @@ +package ldap + +import ( + "fmt" + + ber "github.com/go-asn1-ber/asn1-ber" + "github.com/google/uuid" +) + +const ( + // ControlTypeSyncRequest - https://www.ietf.org/rfc/rfc4533.txt + ControlTypeSyncRequest = "1.3.6.1.4.1.4203.1.9.1.1" + // ControlTypeSyncState - https://www.ietf.org/rfc/rfc4533.txt + ControlTypeSyncState = "1.3.6.1.4.1.4203.1.9.1.2" + // ControlTypeSyncDone - https://www.ietf.org/rfc/rfc4533.txt + ControlTypeSyncDone = "1.3.6.1.4.1.4203.1.9.1.3" + // ControlTypeSyncInfo - https://www.ietf.org/rfc/rfc4533.txt + ControlTypeSyncInfo = "1.3.6.1.4.1.4203.1.9.1.4" +) + +func DecodeSyncReplControl(packet *ber.Packet) (Control, error) { + var ( + controlType string + value *ber.Packet + err error + ) + switch len(packet.Children) { + case 0: + return nil, nil + case 1: + return nil, nil + case 2: + controlType = packet.Children[0].Data.String() + value, err = ber.DecodePacketErr(packet.Children[1].Data.Bytes()) + if err != nil { + return nil, fmt.Errorf("failed to decode data bytes: %w", err) + } + default: + return nil, fmt.Errorf("unsupported handling children: %d", len(packet.Children)) + } + + switch controlType { + case ControlTypeSyncState: + value.Description += " (Sync State)" + return NewControlSyncState(value) + case ControlTypeSyncDone: + value.Description += " (Sync Done)" + return NewControlSyncDone(value) + case ControlTypeSyncInfo: + value.Description += " (Sync Info)" + return NewControlSyncInfo(value) + default: + return nil, fmt.Errorf("unsupported control type: %s", controlType) + } +} + +// Mode for ControlTypeSyncRequest +type ControlSyncRequestMode int64 + +const ( + SyncRequestModeRefreshOnly ControlSyncRequestMode = 1 + SyncRequestModeRefreshAndPersist ControlSyncRequestMode = 3 +) + +// ControlSyncRequest implements the Sync Request Control described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncRequest struct { + Criticality bool + Mode ControlSyncRequestMode + Cookie []byte + ReloadHint bool +} + +func NewControlSyncRequest(mode ControlSyncRequestMode, cookie []byte) *ControlSyncRequest { + return &ControlSyncRequest{ + Criticality: true, + Mode: mode, + Cookie: cookie, + ReloadHint: false, + } +} + +// GetControlType returns the OID +func (c *ControlSyncRequest) GetControlType() string { + return ControlTypeSyncRequest +} + +// Encode encodes the control +func (c *ControlSyncRequest) Encode() *ber.Packet { + _mode := int64(c.Mode) + mode := ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, _mode, "Mode") + cookie := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Cookie") + cookie.Value = c.Cookie + cookie.Data.Write(c.Cookie) + reloadHint := ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.ReloadHint, "Reload Hint") + + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeSyncRequest, "Control Type ("+ControlTypeMap[ControlTypeSyncRequest]+")")) + packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) + + val := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (Sync Request)") + seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Sync Request Value") + seq.AppendChild(mode) + seq.AppendChild(cookie) + seq.AppendChild(reloadHint) + val.AppendChild(seq) + + packet.AppendChild(val) + return packet +} + +// String returns a human-readable description +func (c *ControlSyncRequest) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %t Mode: %d Cookie: %s ReloadHint: %t", + ControlTypeMap[ControlTypeSyncRequest], + ControlTypeSyncRequest, + c.Criticality, + c.Mode, + string(c.Cookie), + c.ReloadHint, + ) +} + +// State for ControlSyncState +type ControlSyncStateState int64 + +const ( + SyncStatePresent ControlSyncStateState = 0 + SyncStateAdd ControlSyncStateState = 1 + SyncStateModify ControlSyncStateState = 2 + SyncStateDelete ControlSyncStateState = 3 +) + +// ControlSyncState implements the Sync State Control described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncState struct { + Criticality bool + State ControlSyncStateState + EntryUUID uuid.UUID + Cookie []byte +} + +func NewControlSyncState(pkt *ber.Packet) (*ControlSyncState, error) { + var ( + state ControlSyncStateState + entryUUID uuid.UUID + cookie []byte + err error + ) + switch len(pkt.Children) { + case 0, 1: + return nil, fmt.Errorf("at least two children are required: %d", len(pkt.Children)) + case 2: + state = ControlSyncStateState(pkt.Children[0].Value.(int64)) + entryUUID, err = uuid.FromBytes(pkt.Children[1].ByteValue) + if err != nil { + return nil, fmt.Errorf("failed to decode uuid: %w", err) + } + case 3: + state = ControlSyncStateState(pkt.Children[0].Value.(int64)) + entryUUID, err = uuid.FromBytes(pkt.Children[1].ByteValue) + if err != nil { + return nil, fmt.Errorf("failed to decode uuid: %w", err) + } + cookie = pkt.Children[2].ByteValue + } + return &ControlSyncState{ + Criticality: false, + State: state, + EntryUUID: entryUUID, + Cookie: cookie, + }, nil +} + +// GetControlType returns the OID +func (c *ControlSyncState) GetControlType() string { + return ControlTypeSyncState +} + +// Encode encodes the control +func (c *ControlSyncState) Encode() *ber.Packet { + return nil +} + +// String returns a human-readable description +func (c *ControlSyncState) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %t State: %d EntryUUID: %s Cookie: %s", + ControlTypeMap[ControlTypeSyncState], + ControlTypeSyncState, + c.Criticality, + c.State, + c.EntryUUID.String(), + string(c.Cookie), + ) +} + +// ControlSyncDone implements the Sync Done Control described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncDone struct { + Criticality bool + Cookie []byte + RefreshDeletes bool +} + +func NewControlSyncDone(pkt *ber.Packet) (*ControlSyncDone, error) { + var ( + cookie []byte + refreshDeletes bool + ) + switch len(pkt.Children) { + case 0: + // have nothing to do + case 1: + cookie = pkt.Children[0].ByteValue + case 2: + cookie = pkt.Children[0].ByteValue + refreshDeletes = pkt.Children[1].Value.(bool) + } + return &ControlSyncDone{ + Criticality: false, + Cookie: cookie, + RefreshDeletes: refreshDeletes, + }, nil +} + +// GetControlType returns the OID +func (c *ControlSyncDone) GetControlType() string { + return ControlTypeSyncDone +} + +// Encode encodes the control +func (c *ControlSyncDone) Encode() *ber.Packet { + return nil +} + +// String returns a human-readable description +func (c *ControlSyncDone) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %t Cookie: %s RefreshDeletes: %t", + ControlTypeMap[ControlTypeSyncDone], + ControlTypeSyncDone, + c.Criticality, + string(c.Cookie), + c.RefreshDeletes, + ) +} + +// Tag For ControlSyncInfo +type ControlSyncInfoValue uint64 + +const ( + SyncInfoNewcookie ControlSyncInfoValue = 0 + SyncInfoRefreshDelete ControlSyncInfoValue = 1 + SyncInfoRefreshPresent ControlSyncInfoValue = 2 + SyncInfoSyncIdSet ControlSyncInfoValue = 3 +) + +// ControlSyncInfoNewCookie implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncInfoNewCookie struct { + Cookie []byte +} + +// String returns a human-readable description +func (c *ControlSyncInfoNewCookie) String() string { + return fmt.Sprintf( + "NewCookie[Cookie: %s]", + string(c.Cookie), + ) +} + +// ControlSyncInfoRefreshDelete implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncInfoRefreshDelete struct { + Cookie []byte + RefreshDone bool +} + +// String returns a human-readable description +func (c *ControlSyncInfoRefreshDelete) String() string { + return fmt.Sprintf( + "RefreshDelete[Cookie: %s RefreshDone: %t]", + string(c.Cookie), + c.RefreshDone, + ) +} + +// ControlSyncInfoRefreshPresent implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncInfoRefreshPresent struct { + Cookie []byte + RefreshDone bool +} + +// String returns a human-readable description +func (c *ControlSyncInfoRefreshPresent) String() string { + return fmt.Sprintf( + "RefreshPresent[Cookie: %s RefreshDone: %t]", + string(c.Cookie), + c.RefreshDone, + ) +} + +// ControlSyncInfoSyncIdSet implements a part of syncInfoValue described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncInfoSyncIdSet struct { + Cookie []byte + RefreshDeletes bool + SyncUUIDs []uuid.UUID +} + +// String returns a human-readable description +func (c *ControlSyncInfoSyncIdSet) String() string { + return fmt.Sprintf( + "SyncIdSet[Cookie: %s RefreshDeletes: %t SyncUUIDs: %v]", + string(c.Cookie), + c.RefreshDeletes, + c.SyncUUIDs, + ) +} + +// ControlSyncInfo implements the Sync Info Control described in https://www.ietf.org/rfc/rfc4533.txt +type ControlSyncInfo struct { + Criticality bool + Value ControlSyncInfoValue + NewCookie *ControlSyncInfoNewCookie + RefreshDelete *ControlSyncInfoRefreshDelete + RefreshPresent *ControlSyncInfoRefreshPresent + SyncIdSet *ControlSyncInfoSyncIdSet +} + +func NewControlSyncInfo(pkt *ber.Packet) (*ControlSyncInfo, error) { + var ( + cookie []byte + refreshDone = true + refreshDeletes bool + syncUUIDs []uuid.UUID + ) + c := &ControlSyncInfo{Criticality: false} + switch ControlSyncInfoValue(pkt.Identifier.Tag) { + case SyncInfoNewcookie: + c.Value = SyncInfoNewcookie + c.NewCookie = &ControlSyncInfoNewCookie{ + Cookie: pkt.ByteValue, + } + case SyncInfoRefreshDelete: + c.Value = SyncInfoRefreshDelete + switch len(pkt.Children) { + case 0: + // have nothing to do + case 1: + cookie = pkt.Children[0].ByteValue + case 2: + cookie = pkt.Children[0].ByteValue + refreshDone = pkt.Children[1].Value.(bool) + } + c.RefreshDelete = &ControlSyncInfoRefreshDelete{ + Cookie: cookie, + RefreshDone: refreshDone, + } + case SyncInfoRefreshPresent: + c.Value = SyncInfoRefreshPresent + switch len(pkt.Children) { + case 0: + // have nothing to do + case 1: + cookie = pkt.Children[0].ByteValue + case 2: + cookie = pkt.Children[0].ByteValue + refreshDone = pkt.Children[1].Value.(bool) + } + c.RefreshPresent = &ControlSyncInfoRefreshPresent{ + Cookie: cookie, + RefreshDone: refreshDone, + } + case SyncInfoSyncIdSet: + c.Value = SyncInfoSyncIdSet + switch len(pkt.Children) { + case 0: + // have nothing to do + case 1: + cookie = pkt.Children[0].ByteValue + case 2: + cookie = pkt.Children[0].ByteValue + refreshDeletes = pkt.Children[1].Value.(bool) + case 3: + cookie = pkt.Children[0].ByteValue + refreshDeletes = pkt.Children[1].Value.(bool) + syncUUIDs = make([]uuid.UUID, 0, len(pkt.Children[2].Children)) + for _, child := range pkt.Children[2].Children { + u, err := uuid.FromBytes(child.ByteValue) + if err != nil { + return nil, fmt.Errorf("failed to decode uuid: %w", err) + } + syncUUIDs = append(syncUUIDs, u) + } + } + c.SyncIdSet = &ControlSyncInfoSyncIdSet{ + Cookie: cookie, + RefreshDeletes: refreshDeletes, + SyncUUIDs: syncUUIDs, + } + default: + return nil, fmt.Errorf("unknown sync info value: %d", pkt.Identifier.Tag) + } + return c, nil +} + +// GetControlType returns the OID +func (c *ControlSyncInfo) GetControlType() string { + return ControlTypeSyncInfo +} + +// Encode encodes the control +func (c *ControlSyncInfo) Encode() *ber.Packet { + return nil +} + +// String returns a human-readable description +func (c *ControlSyncInfo) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %t Value: %d %s %s %s %s", + ControlTypeMap[ControlTypeSyncInfo], + ControlTypeSyncInfo, + c.Criticality, + c.Value, + c.NewCookie, + c.RefreshDelete, + c.RefreshPresent, + c.SyncIdSet, + ) +} diff --git a/v3/examples_test.go b/v3/examples_test.go index 61f16197..8b4e17b5 100644 --- a/v3/examples_test.go +++ b/v3/examples_test.go @@ -80,6 +80,43 @@ func ExampleConn_SearchAsync() { } } +// This example demonstrates how to do syncrepl (persistent search) +func ExampleConn_Syncrepl() { + l, err := DialURL(fmt.Sprintf("%s:%d", "ldap.example.com", 389)) + if err != nil { + log.Fatal(err) + } + defer l.Close() + + searchRequest := NewSearchRequest( + "dc=example,dc=com", // The base dn to search + ScopeWholeSubtree, NeverDerefAliases, 0, 0, false, + "(&(objectClass=organizationalPerson))", // The filter to apply + []string{"dn", "cn"}, // A list attributes to retrieve + nil, + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + mode := SyncRequestModeRefreshAndPersist + var cookie []byte = nil + r := l.Syncrepl(ctx, searchRequest, 64, mode, cookie) + for r.Next() { + entry := r.Entry() + if entry != nil { + fmt.Printf("%s has DN %s\n", entry.GetAttributeValue("cn"), entry.DN) + } + controlls := r.Controls() + if len(controlls) != 0 { + fmt.Printf("%s", controlls) + } + } + if err := r.Err(); err != nil { + log.Fatal(err) + } +} + // This example demonstrates how to start a TLS connection func ExampleConn_StartTLS() { l, err := DialURL("ldap://ldap.example.com:389") diff --git a/v3/go.mod b/v3/go.mod index 5043df65..b1810ae8 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -6,6 +6,7 @@ require ( github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 github.com/go-asn1-ber/asn1-ber v1.5.4 + github.com/google/uuid v1.3.0 github.com/stretchr/testify v1.8.0 golang.org/x/crypto v0.7.0 // indirect ) diff --git a/v3/go.sum b/v3/go.sum index bb57aeae..9b27dce7 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -7,6 +7,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/v3/ldap.go b/v3/ldap.go index e2c758fb..90837a77 100644 --- a/v3/ldap.go +++ b/v3/ldap.go @@ -32,6 +32,7 @@ const ( ApplicationSearchResultReference = 19 ApplicationExtendedRequest = 23 ApplicationExtendedResponse = 24 + ApplicationIntermediateResponse = 25 ) // ApplicationMap contains human readable descriptions of LDAP Application Codes @@ -56,6 +57,7 @@ var ApplicationMap = map[uint8]string{ ApplicationSearchResultReference: "Search Result Reference", ApplicationExtendedRequest: "Extended Request", ApplicationExtendedResponse: "Extended Response", + ApplicationIntermediateResponse: "Intermediate Response", } // Ldap Behera Password Policy Draft 10 (https://tools.ietf.org/html/draft-behera-ldap-password-policy-10) diff --git a/v3/response_syncrepl.go b/v3/response_syncrepl.go new file mode 100644 index 00000000..4cbd0ffb --- /dev/null +++ b/v3/response_syncrepl.go @@ -0,0 +1,156 @@ +package ldap + +import ( + "context" + "errors" + "fmt" + + ber "github.com/go-asn1-ber/asn1-ber" +) + +type syncReplResponse struct { + sr *searchResponse +} + +// Entry returns an entry from the given search request +func (r *syncReplResponse) Entry() *Entry { + return r.sr.entry +} + +// Referral returns a referral from the given search request +func (r *syncReplResponse) Referral() string { + return r.sr.referral +} + +// Controls returns controls from the given search request +func (r *syncReplResponse) Controls() []Control { + return r.sr.controls +} + +// Err returns an error when the given search request was failed +func (r *syncReplResponse) Err() error { + return r.sr.err +} + +// Next returns whether next data exist or not +func (r *syncReplResponse) Next() bool { + return r.sr.Next() +} + +func (r *syncReplResponse) start(ctx context.Context, searchRequest *SearchRequest) { + go func() { + defer func() { + close(r.sr.ch) + if err := recover(); err != nil { + r.sr.conn.err = fmt.Errorf("ldap: recovered panic in syncReplResponse: %v", err) + } + }() + + if r.sr.conn.IsClosing() { + return + } + + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, r.sr.conn.nextMessageID(), "MessageID")) + // encode search request + err := searchRequest.appendTo(packet) + if err != nil { + r.sr.ch <- &SearchSingleResult{Error: err} + return + } + r.sr.conn.Debug.PrintPacket(packet) + + msgCtx, err := r.sr.conn.sendMessage(packet) + if err != nil { + r.sr.ch <- &SearchSingleResult{Error: err} + return + } + defer r.sr.conn.finishMessage(msgCtx) + + for { + select { + case <-ctx.Done(): + r.sr.conn.Debug.Printf("%d: %s", msgCtx.id, ctx.Err().Error()) + return + default: + r.sr.conn.Debug.Printf("%d: waiting for response", msgCtx.id) + packetResponse, ok := <-msgCtx.responses + if !ok { + err := NewError(ErrorNetwork, errors.New("ldap: response channel closed")) + r.sr.ch <- &SearchSingleResult{Error: err} + return + } + packet, err = packetResponse.ReadPacket() + r.sr.conn.Debug.Printf("%d: got response %p", msgCtx.id, packet) + if err != nil { + r.sr.ch <- &SearchSingleResult{Error: err} + return + } + + switch packet.Children[1].Tag { + case ApplicationSearchResultEntry: + result := &SearchSingleResult{ + Entry: &Entry{ + DN: packet.Children[1].Children[0].Value.(string), + Attributes: unpackAttributes(packet.Children[1].Children[1].Children), + }, + } + if len(packet.Children) != 3 { + r.sr.ch <- result + continue + } + controlPacket := packet.Children[2].Children[0] + decoded, err := DecodeSyncReplControl(controlPacket) + if err != nil { + werr := fmt.Errorf("failed to decode search result entry: %w", err) + result.Error = werr + r.sr.ch <- result + return + } + result.Controls = append(result.Controls, decoded) + r.sr.ch <- result + + case ApplicationSearchResultDone: + if err := GetLDAPError(packet); err != nil { + r.sr.ch <- &SearchSingleResult{Error: err} + return + } + if len(packet.Children) != 3 { + return + } + controlPacket := packet.Children[2].Children[0] + decoded, err := DecodeSyncReplControl(controlPacket) + if err != nil { + werr := fmt.Errorf("failed to decode search result done: %w", err) + r.sr.ch <- &SearchSingleResult{Error: werr} + return + } + result := &SearchSingleResult{} + result.Controls = append(result.Controls, decoded) + r.sr.ch <- result + return + + case ApplicationIntermediateResponse: + decoded, err := DecodeSyncReplControl(packet.Children[1]) + if err != nil { + werr := fmt.Errorf("failed to decode intermediate response: %w", err) + r.sr.ch <- &SearchSingleResult{Error: werr} + return + } + result := &SearchSingleResult{} + result.Controls = append(result.Controls, decoded) + r.sr.ch <- result + + default: + r.sr.conn.Debug.Printf("got application code: %d", packet.Children[1].Tag) + } + } + } + }() +} + +func newSyncReplResponse(conn *Conn, bufferSize int) *syncReplResponse { + return &syncReplResponse{ + sr: newSearchResponse(conn, bufferSize), + } +} diff --git a/v3/search.go b/v3/search.go index afac768c..4ad92998 100644 --- a/v3/search.go +++ b/v3/search.go @@ -587,7 +587,7 @@ func (l *Conn) Search(searchRequest *SearchRequest) (*SearchResult, error) { // SearchAsync performs a search request and returns all search results asynchronously. // This means you get all results until an error happens (or the search successfully finished), // e.g. for size / time limited requests all are recieved until the limit is reached. -// To stop the search, call cancel function returned context. +// To stop the search, call cancel function of the context. func (l *Conn) SearchAsync( ctx context.Context, searchRequest *SearchRequest, bufferSize int) Response { r := newSearchResponse(l, bufferSize) @@ -595,6 +595,21 @@ func (l *Conn) SearchAsync( return r } +// Syncrepl is a short name for LDAP Sync Replication engine that works on the +// consumer-side. This can perform a persistent search and returns an entry +// when the entry is updated on the server side. +// To stop the search, call cancel function of the context. +func (l *Conn) Syncrepl( + ctx context.Context, searchRequest *SearchRequest, bufferSize int, + mode ControlSyncRequestMode, cookie []byte, +) Response { + control := NewControlSyncRequest(mode, cookie) + searchRequest.Controls = append(searchRequest.Controls, control) + r := newSyncReplResponse(l, bufferSize) + r.start(ctx, searchRequest) + return r +} + // unpackAttributes will extract all given LDAP attributes and it's values // from the ber.Packet func unpackAttributes(children []*ber.Packet) []*EntryAttribute {