From 87ae55c33ee0c49a67b9c293375606c1848c1717 Mon Sep 17 00:00:00 2001 From: Tetsuya Morimoto Date: Wed, 19 Jul 2023 14:37:12 +0900 Subject: [PATCH 1/5] feat: add syncrepl (rfc-4533) consumer feature #422 --- client.go | 1 + control.go | 4 + control_syncrepl.go | 427 ++++++++++++++++++++++++++++++++++++++++ examples_test.go | 37 ++++ go.mod | 1 + go.sum | 2 + ldap.go | 2 + response_syncrepl.go | 156 +++++++++++++++ search.go | 17 +- v3/client.go | 1 + v3/control.go | 4 + v3/control_syncrepl.go | 427 ++++++++++++++++++++++++++++++++++++++++ v3/examples_test.go | 37 ++++ v3/go.mod | 1 + v3/go.sum | 2 + v3/ldap.go | 2 + v3/response_syncrepl.go | 156 +++++++++++++++ v3/search.go | 17 +- 18 files changed, 1292 insertions(+), 2 deletions(-) create mode 100644 control_syncrepl.go create mode 100644 response_syncrepl.go create mode 100644 v3/control_syncrepl.go create mode 100644 v3/response_syncrepl.go 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..ee3c8a69 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) + } + controls := r.Controls() + if len(controls) != 0 { + fmt.Printf("%s", controls) + } + } + 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..ee3c8a69 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) + } + controls := r.Controls() + if len(controls) != 0 { + fmt.Printf("%s", controls) + } + } + 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 { From 513ab5a7bdf2915aba018a0edbc93a0b03f28483 Mon Sep 17 00:00:00 2001 From: Tetsuya Morimoto Date: Sat, 22 Jul 2023 22:55:04 +0900 Subject: [PATCH 2/5] refactor: change Syncrepl interface to be able to pass reloadHint #447 --- client.go | 2 +- control_syncrepl.go | 6 ++++-- examples_test.go | 2 +- search.go | 4 ++-- v3/client.go | 2 +- v3/control_syncrepl.go | 6 ++++-- v3/examples_test.go | 2 +- v3/search.go | 4 ++-- 8 files changed, 16 insertions(+), 12 deletions(-) diff --git a/client.go b/client.go index b7193345..c1e93d04 100644 --- a/client.go +++ b/client.go @@ -36,5 +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 + Syncrepl(ctx context.Context, searchRequest *SearchRequest, bufferSize int, mode ControlSyncRequestMode, cookie []byte, reloadHint bool) Response } diff --git a/control_syncrepl.go b/control_syncrepl.go index 46866f7f..e785905f 100644 --- a/control_syncrepl.go +++ b/control_syncrepl.go @@ -70,12 +70,14 @@ type ControlSyncRequest struct { ReloadHint bool } -func NewControlSyncRequest(mode ControlSyncRequestMode, cookie []byte) *ControlSyncRequest { +func NewControlSyncRequest( + mode ControlSyncRequestMode, cookie []byte, reloadHint bool, +) *ControlSyncRequest { return &ControlSyncRequest{ Criticality: true, Mode: mode, Cookie: cookie, - ReloadHint: false, + ReloadHint: reloadHint, } } diff --git a/examples_test.go b/examples_test.go index ee3c8a69..86b23a30 100644 --- a/examples_test.go +++ b/examples_test.go @@ -101,7 +101,7 @@ func ExampleConn_Syncrepl() { mode := SyncRequestModeRefreshAndPersist var cookie []byte = nil - r := l.Syncrepl(ctx, searchRequest, 64, mode, cookie) + r := l.Syncrepl(ctx, searchRequest, 64, mode, cookie, false) for r.Next() { entry := r.Entry() if entry != nil { diff --git a/search.go b/search.go index 83fc2959..e462e438 100644 --- a/search.go +++ b/search.go @@ -599,9 +599,9 @@ func (l *Conn) SearchAsync( // 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, + mode ControlSyncRequestMode, cookie []byte, reloadHint bool, ) Response { - control := NewControlSyncRequest(mode, cookie) + control := NewControlSyncRequest(mode, cookie, reloadHint) searchRequest.Controls = append(searchRequest.Controls, control) r := newSyncReplResponse(l, bufferSize) r.start(ctx, searchRequest) diff --git a/v3/client.go b/v3/client.go index b7193345..c1e93d04 100644 --- a/v3/client.go +++ b/v3/client.go @@ -36,5 +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 + Syncrepl(ctx context.Context, searchRequest *SearchRequest, bufferSize int, mode ControlSyncRequestMode, cookie []byte, reloadHint bool) Response } diff --git a/v3/control_syncrepl.go b/v3/control_syncrepl.go index 46866f7f..e785905f 100644 --- a/v3/control_syncrepl.go +++ b/v3/control_syncrepl.go @@ -70,12 +70,14 @@ type ControlSyncRequest struct { ReloadHint bool } -func NewControlSyncRequest(mode ControlSyncRequestMode, cookie []byte) *ControlSyncRequest { +func NewControlSyncRequest( + mode ControlSyncRequestMode, cookie []byte, reloadHint bool, +) *ControlSyncRequest { return &ControlSyncRequest{ Criticality: true, Mode: mode, Cookie: cookie, - ReloadHint: false, + ReloadHint: reloadHint, } } diff --git a/v3/examples_test.go b/v3/examples_test.go index ee3c8a69..86b23a30 100644 --- a/v3/examples_test.go +++ b/v3/examples_test.go @@ -101,7 +101,7 @@ func ExampleConn_Syncrepl() { mode := SyncRequestModeRefreshAndPersist var cookie []byte = nil - r := l.Syncrepl(ctx, searchRequest, 64, mode, cookie) + r := l.Syncrepl(ctx, searchRequest, 64, mode, cookie, false) for r.Next() { entry := r.Entry() if entry != nil { diff --git a/v3/search.go b/v3/search.go index 4ad92998..39d37d3b 100644 --- a/v3/search.go +++ b/v3/search.go @@ -601,9 +601,9 @@ func (l *Conn) SearchAsync( // 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, + mode ControlSyncRequestMode, cookie []byte, reloadHint bool, ) Response { - control := NewControlSyncRequest(mode, cookie) + control := NewControlSyncRequest(mode, cookie, reloadHint) searchRequest.Controls = append(searchRequest.Controls, control) r := newSyncReplResponse(l, bufferSize) r.start(ctx, searchRequest) From 3c7b473eb7439a7e0e956e90dc89fa00399158f5 Mon Sep 17 00:00:00 2001 From: Tetsuya Morimoto Date: Sun, 23 Jul 2023 02:13:38 +0900 Subject: [PATCH 3/5] refactor: unify syncrepl (Syncrepl) instead of syncRepl (SyncRepl) #447 --- control_syncrepl.go | 2 +- response_syncrepl.go | 26 +++++++++++++------------- search.go | 2 +- v3/control_syncrepl.go | 2 +- v3/response_syncrepl.go | 26 +++++++++++++------------- v3/search.go | 2 +- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/control_syncrepl.go b/control_syncrepl.go index e785905f..e94cd08c 100644 --- a/control_syncrepl.go +++ b/control_syncrepl.go @@ -18,7 +18,7 @@ const ( ControlTypeSyncInfo = "1.3.6.1.4.1.4203.1.9.1.4" ) -func DecodeSyncReplControl(packet *ber.Packet) (Control, error) { +func DecodeSyncreplControl(packet *ber.Packet) (Control, error) { var ( controlType string value *ber.Packet diff --git a/response_syncrepl.go b/response_syncrepl.go index 4cbd0ffb..9804a0ad 100644 --- a/response_syncrepl.go +++ b/response_syncrepl.go @@ -8,41 +8,41 @@ import ( ber "github.com/go-asn1-ber/asn1-ber" ) -type syncReplResponse struct { +type syncreplResponse struct { sr *searchResponse } // Entry returns an entry from the given search request -func (r *syncReplResponse) Entry() *Entry { +func (r *syncreplResponse) Entry() *Entry { return r.sr.entry } // Referral returns a referral from the given search request -func (r *syncReplResponse) Referral() string { +func (r *syncreplResponse) Referral() string { return r.sr.referral } // Controls returns controls from the given search request -func (r *syncReplResponse) Controls() []Control { +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 { +func (r *syncreplResponse) Err() error { return r.sr.err } // Next returns whether next data exist or not -func (r *syncReplResponse) Next() bool { +func (r *syncreplResponse) Next() bool { return r.sr.Next() } -func (r *syncReplResponse) start(ctx context.Context, searchRequest *SearchRequest) { +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) + r.sr.conn.err = fmt.Errorf("ldap: recovered panic in syncreplResponse: %v", err) } }() @@ -100,7 +100,7 @@ func (r *syncReplResponse) start(ctx context.Context, searchRequest *SearchReque continue } controlPacket := packet.Children[2].Children[0] - decoded, err := DecodeSyncReplControl(controlPacket) + decoded, err := DecodeSyncreplControl(controlPacket) if err != nil { werr := fmt.Errorf("failed to decode search result entry: %w", err) result.Error = werr @@ -119,7 +119,7 @@ func (r *syncReplResponse) start(ctx context.Context, searchRequest *SearchReque return } controlPacket := packet.Children[2].Children[0] - decoded, err := DecodeSyncReplControl(controlPacket) + decoded, err := DecodeSyncreplControl(controlPacket) if err != nil { werr := fmt.Errorf("failed to decode search result done: %w", err) r.sr.ch <- &SearchSingleResult{Error: werr} @@ -131,7 +131,7 @@ func (r *syncReplResponse) start(ctx context.Context, searchRequest *SearchReque return case ApplicationIntermediateResponse: - decoded, err := DecodeSyncReplControl(packet.Children[1]) + 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} @@ -149,8 +149,8 @@ func (r *syncReplResponse) start(ctx context.Context, searchRequest *SearchReque }() } -func newSyncReplResponse(conn *Conn, bufferSize int) *syncReplResponse { - return &syncReplResponse{ +func newSyncreplResponse(conn *Conn, bufferSize int) *syncreplResponse { + return &syncreplResponse{ sr: newSearchResponse(conn, bufferSize), } } diff --git a/search.go b/search.go index e462e438..248a4ad0 100644 --- a/search.go +++ b/search.go @@ -603,7 +603,7 @@ func (l *Conn) Syncrepl( ) Response { control := NewControlSyncRequest(mode, cookie, reloadHint) searchRequest.Controls = append(searchRequest.Controls, control) - r := newSyncReplResponse(l, bufferSize) + r := newSyncreplResponse(l, bufferSize) r.start(ctx, searchRequest) return r } diff --git a/v3/control_syncrepl.go b/v3/control_syncrepl.go index e785905f..e94cd08c 100644 --- a/v3/control_syncrepl.go +++ b/v3/control_syncrepl.go @@ -18,7 +18,7 @@ const ( ControlTypeSyncInfo = "1.3.6.1.4.1.4203.1.9.1.4" ) -func DecodeSyncReplControl(packet *ber.Packet) (Control, error) { +func DecodeSyncreplControl(packet *ber.Packet) (Control, error) { var ( controlType string value *ber.Packet diff --git a/v3/response_syncrepl.go b/v3/response_syncrepl.go index 4cbd0ffb..9804a0ad 100644 --- a/v3/response_syncrepl.go +++ b/v3/response_syncrepl.go @@ -8,41 +8,41 @@ import ( ber "github.com/go-asn1-ber/asn1-ber" ) -type syncReplResponse struct { +type syncreplResponse struct { sr *searchResponse } // Entry returns an entry from the given search request -func (r *syncReplResponse) Entry() *Entry { +func (r *syncreplResponse) Entry() *Entry { return r.sr.entry } // Referral returns a referral from the given search request -func (r *syncReplResponse) Referral() string { +func (r *syncreplResponse) Referral() string { return r.sr.referral } // Controls returns controls from the given search request -func (r *syncReplResponse) Controls() []Control { +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 { +func (r *syncreplResponse) Err() error { return r.sr.err } // Next returns whether next data exist or not -func (r *syncReplResponse) Next() bool { +func (r *syncreplResponse) Next() bool { return r.sr.Next() } -func (r *syncReplResponse) start(ctx context.Context, searchRequest *SearchRequest) { +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) + r.sr.conn.err = fmt.Errorf("ldap: recovered panic in syncreplResponse: %v", err) } }() @@ -100,7 +100,7 @@ func (r *syncReplResponse) start(ctx context.Context, searchRequest *SearchReque continue } controlPacket := packet.Children[2].Children[0] - decoded, err := DecodeSyncReplControl(controlPacket) + decoded, err := DecodeSyncreplControl(controlPacket) if err != nil { werr := fmt.Errorf("failed to decode search result entry: %w", err) result.Error = werr @@ -119,7 +119,7 @@ func (r *syncReplResponse) start(ctx context.Context, searchRequest *SearchReque return } controlPacket := packet.Children[2].Children[0] - decoded, err := DecodeSyncReplControl(controlPacket) + decoded, err := DecodeSyncreplControl(controlPacket) if err != nil { werr := fmt.Errorf("failed to decode search result done: %w", err) r.sr.ch <- &SearchSingleResult{Error: werr} @@ -131,7 +131,7 @@ func (r *syncReplResponse) start(ctx context.Context, searchRequest *SearchReque return case ApplicationIntermediateResponse: - decoded, err := DecodeSyncReplControl(packet.Children[1]) + 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} @@ -149,8 +149,8 @@ func (r *syncReplResponse) start(ctx context.Context, searchRequest *SearchReque }() } -func newSyncReplResponse(conn *Conn, bufferSize int) *syncReplResponse { - return &syncReplResponse{ +func newSyncreplResponse(conn *Conn, bufferSize int) *syncreplResponse { + return &syncreplResponse{ sr: newSearchResponse(conn, bufferSize), } } diff --git a/v3/search.go b/v3/search.go index 39d37d3b..f6dd1d58 100644 --- a/v3/search.go +++ b/v3/search.go @@ -605,7 +605,7 @@ func (l *Conn) Syncrepl( ) Response { control := NewControlSyncRequest(mode, cookie, reloadHint) searchRequest.Controls = append(searchRequest.Controls, control) - r := newSyncReplResponse(l, bufferSize) + r := newSyncreplResponse(l, bufferSize) r.start(ctx, searchRequest) return r } From a9912a016d003910da96e954be1bb6664ee9a9ac Mon Sep 17 00:00:00 2001 From: Tetsuya Morimoto Date: Sat, 5 Aug 2023 12:55:05 +0900 Subject: [PATCH 4/5] refactor: integrate xxx_syncrepl.go into control.go/response.go for maintainability #447 --- control.go | 413 +++++++++++++++++++++++++++++++++++++- control_syncrepl.go | 429 ---------------------------------------- response.go | 29 ++- response_syncrepl.go | 156 --------------- search.go | 2 +- v3/control.go | 413 +++++++++++++++++++++++++++++++++++++- v3/control_syncrepl.go | 429 ---------------------------------------- v3/response.go | 29 ++- v3/response_syncrepl.go | 156 --------------- v3/search.go | 2 +- 10 files changed, 882 insertions(+), 1176 deletions(-) delete mode 100644 control_syncrepl.go delete mode 100644 response_syncrepl.go delete mode 100644 v3/control_syncrepl.go delete mode 100644 v3/response_syncrepl.go diff --git a/control.go b/control.go index 16b6cca1..fa74da49 100644 --- a/control.go +++ b/control.go @@ -5,6 +5,7 @@ import ( "strconv" ber "github.com/go-asn1-ber/asn1-ber" + "github.com/google/uuid" ) const ( @@ -31,6 +32,15 @@ const ( ControlTypeMicrosoftServerLinkTTL = "1.2.840.113556.1.4.2309" // ControlTypeDirSync - Active Directory DirSync - https://msdn.microsoft.com/en-us/library/aa366978(v=vs.85).aspx ControlTypeDirSync = "1.2.840.113556.1.4.841" + + // 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" ) // Flags for DirSync control @@ -387,7 +397,13 @@ func DecodeControl(packet *ber.Packet) (Control, error) { case 2: packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")" - ControlType = packet.Children[0].Value.(string) + if packet.Children[0].Value != nil { + ControlType = packet.Children[0].Value.(string) + } else if packet.Children[0].Data != nil { + ControlType = packet.Children[0].Data.String() + } else { + return nil, fmt.Errorf("not found where to get the control type") + } // Children[1] could be criticality or value (both are optional) // duck-type on whether this is a boolean @@ -530,6 +546,27 @@ func DecodeControl(packet *ber.Packet) (Control, error) { c.Cookie = value.Children[2].Data.Bytes() value.Children[2].Value = c.Cookie return c, nil + case ControlTypeSyncState: + value.Description += " (Sync State)" + valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) + if err != nil { + return nil, fmt.Errorf("failed to decode data bytes: %s", err) + } + return NewControlSyncState(valueChildren) + case ControlTypeSyncDone: + value.Description += " (Sync Done)" + valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) + if err != nil { + return nil, fmt.Errorf("failed to decode data bytes: %s", err) + } + return NewControlSyncDone(valueChildren) + case ControlTypeSyncInfo: + value.Description += " (Sync Info)" + valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) + if err != nil { + return nil, fmt.Errorf("failed to decode data bytes: %s", err) + } + return NewControlSyncInfo(valueChildren) default: c := new(ControlString) c.ControlType = ControlType @@ -656,3 +693,377 @@ func (c *ControlDirSync) Encode() *ber.Packet { func (c *ControlDirSync) SetCookie(cookie []byte) { c.Cookie = cookie } + +// 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, reloadHint bool, +) *ControlSyncRequest { + return &ControlSyncRequest{ + Criticality: true, + Mode: mode, + Cookie: cookie, + ReloadHint: reloadHint, + } +} + +// 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/control_syncrepl.go b/control_syncrepl.go deleted file mode 100644 index e94cd08c..00000000 --- a/control_syncrepl.go +++ /dev/null @@ -1,429 +0,0 @@ -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, reloadHint bool, -) *ControlSyncRequest { - return &ControlSyncRequest{ - Criticality: true, - Mode: mode, - Cookie: cookie, - ReloadHint: reloadHint, - } -} - -// 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/response.go b/response.go index 1cb6c37e..8f23b940 100644 --- a/response.go +++ b/response.go @@ -127,12 +127,25 @@ func (r *searchResponse) start(ctx context.Context, searchRequest *SearchRequest switch packet.Children[1].Tag { case ApplicationSearchResultEntry: - r.ch <- &SearchSingleResult{ + 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.ch <- result + continue + } + decoded, err := DecodeControl(packet.Children[2].Children[0]) + if err != nil { + werr := fmt.Errorf("failed to decode search result entry: %w", err) + result.Error = werr + r.ch <- result + return + } + result.Controls = append(result.Controls, decoded) + r.ch <- result case ApplicationSearchResultDone: if err := GetLDAPError(packet); err != nil { @@ -157,6 +170,20 @@ func (r *searchResponse) start(ctx context.Context, searchRequest *SearchRequest case ApplicationSearchResultReference: ref := packet.Children[1].Children[0].Value.(string) r.ch <- &SearchSingleResult{Referral: ref} + + case ApplicationIntermediateResponse: + decoded, err := DecodeControl(packet.Children[1]) + if err != nil { + werr := fmt.Errorf("failed to decode intermediate response: %w", err) + r.ch <- &SearchSingleResult{Error: werr} + return + } + result := &SearchSingleResult{} + result.Controls = append(result.Controls, decoded) + r.ch <- result + + default: + r.conn.Debug.Printf("got application code: %d", packet.Children[1].Tag) } } } diff --git a/response_syncrepl.go b/response_syncrepl.go deleted file mode 100644 index 9804a0ad..00000000 --- a/response_syncrepl.go +++ /dev/null @@ -1,156 +0,0 @@ -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 248a4ad0..0d353b94 100644 --- a/search.go +++ b/search.go @@ -603,7 +603,7 @@ func (l *Conn) Syncrepl( ) Response { control := NewControlSyncRequest(mode, cookie, reloadHint) searchRequest.Controls = append(searchRequest.Controls, control) - r := newSyncreplResponse(l, bufferSize) + r := newSearchResponse(l, bufferSize) r.start(ctx, searchRequest) return r } diff --git a/v3/control.go b/v3/control.go index ba4163d9..6710feb5 100644 --- a/v3/control.go +++ b/v3/control.go @@ -5,6 +5,7 @@ import ( "strconv" ber "github.com/go-asn1-ber/asn1-ber" + "github.com/google/uuid" ) const ( @@ -36,6 +37,15 @@ const ( ControlTypeMicrosoftServerLinkTTL = "1.2.840.113556.1.4.2309" // ControlTypeDirSync - Active Directory DirSync - https://msdn.microsoft.com/en-us/library/aa366978(v=vs.85).aspx ControlTypeDirSync = "1.2.840.113556.1.4.841" + + // 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" ) // Flags for DirSync control @@ -394,7 +404,13 @@ func DecodeControl(packet *ber.Packet) (Control, error) { case 2: packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")" - ControlType = packet.Children[0].Value.(string) + if packet.Children[0].Value != nil { + ControlType = packet.Children[0].Value.(string) + } else if packet.Children[0].Data != nil { + ControlType = packet.Children[0].Data.String() + } else { + return nil, fmt.Errorf("not found where to get the control type") + } // Children[1] could be criticality or value (both are optional) // duck-type on whether this is a boolean @@ -541,6 +557,27 @@ func DecodeControl(packet *ber.Packet) (Control, error) { c.Cookie = value.Children[2].Data.Bytes() value.Children[2].Value = c.Cookie return c, nil + case ControlTypeSyncState: + value.Description += " (Sync State)" + valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) + if err != nil { + return nil, fmt.Errorf("failed to decode data bytes: %s", err) + } + return NewControlSyncState(valueChildren) + case ControlTypeSyncDone: + value.Description += " (Sync Done)" + valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) + if err != nil { + return nil, fmt.Errorf("failed to decode data bytes: %s", err) + } + return NewControlSyncDone(valueChildren) + case ControlTypeSyncInfo: + value.Description += " (Sync Info)" + valueChildren, err := ber.DecodePacketErr(value.Data.Bytes()) + if err != nil { + return nil, fmt.Errorf("failed to decode data bytes: %s", err) + } + return NewControlSyncInfo(valueChildren) default: c := new(ControlString) c.ControlType = ControlType @@ -854,3 +891,377 @@ func (c *ControlServerSideSortingResult) String() string { c.Result, ) } + +// 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, reloadHint bool, +) *ControlSyncRequest { + return &ControlSyncRequest{ + Criticality: true, + Mode: mode, + Cookie: cookie, + ReloadHint: reloadHint, + } +} + +// 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/control_syncrepl.go b/v3/control_syncrepl.go deleted file mode 100644 index e94cd08c..00000000 --- a/v3/control_syncrepl.go +++ /dev/null @@ -1,429 +0,0 @@ -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, reloadHint bool, -) *ControlSyncRequest { - return &ControlSyncRequest{ - Criticality: true, - Mode: mode, - Cookie: cookie, - ReloadHint: reloadHint, - } -} - -// 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/response.go b/v3/response.go index 1cb6c37e..8f23b940 100644 --- a/v3/response.go +++ b/v3/response.go @@ -127,12 +127,25 @@ func (r *searchResponse) start(ctx context.Context, searchRequest *SearchRequest switch packet.Children[1].Tag { case ApplicationSearchResultEntry: - r.ch <- &SearchSingleResult{ + 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.ch <- result + continue + } + decoded, err := DecodeControl(packet.Children[2].Children[0]) + if err != nil { + werr := fmt.Errorf("failed to decode search result entry: %w", err) + result.Error = werr + r.ch <- result + return + } + result.Controls = append(result.Controls, decoded) + r.ch <- result case ApplicationSearchResultDone: if err := GetLDAPError(packet); err != nil { @@ -157,6 +170,20 @@ func (r *searchResponse) start(ctx context.Context, searchRequest *SearchRequest case ApplicationSearchResultReference: ref := packet.Children[1].Children[0].Value.(string) r.ch <- &SearchSingleResult{Referral: ref} + + case ApplicationIntermediateResponse: + decoded, err := DecodeControl(packet.Children[1]) + if err != nil { + werr := fmt.Errorf("failed to decode intermediate response: %w", err) + r.ch <- &SearchSingleResult{Error: werr} + return + } + result := &SearchSingleResult{} + result.Controls = append(result.Controls, decoded) + r.ch <- result + + default: + r.conn.Debug.Printf("got application code: %d", packet.Children[1].Tag) } } } diff --git a/v3/response_syncrepl.go b/v3/response_syncrepl.go deleted file mode 100644 index 9804a0ad..00000000 --- a/v3/response_syncrepl.go +++ /dev/null @@ -1,156 +0,0 @@ -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 f6dd1d58..e6bd78f9 100644 --- a/v3/search.go +++ b/v3/search.go @@ -605,7 +605,7 @@ func (l *Conn) Syncrepl( ) Response { control := NewControlSyncRequest(mode, cookie, reloadHint) searchRequest.Controls = append(searchRequest.Controls, control) - r := newSyncreplResponse(l, bufferSize) + r := newSearchResponse(l, bufferSize) r.start(ctx, searchRequest) return r } From 81d8b258b04843a66f8c87d9dc1ba5bdd298e430 Mon Sep 17 00:00:00 2001 From: Tetsuya Morimoto Date: Sat, 5 Aug 2023 21:58:53 +0900 Subject: [PATCH 5/5] refactor: return an error if an unknown tag is received during the search process --- response.go | 4 +++- v3/response.go | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/response.go b/response.go index 8f23b940..1abe02a3 100644 --- a/response.go +++ b/response.go @@ -183,7 +183,9 @@ func (r *searchResponse) start(ctx context.Context, searchRequest *SearchRequest r.ch <- result default: - r.conn.Debug.Printf("got application code: %d", packet.Children[1].Tag) + err := fmt.Errorf("unknown tag: %d", packet.Children[1].Tag) + r.ch <- &SearchSingleResult{Error: err} + return } } } diff --git a/v3/response.go b/v3/response.go index 8f23b940..1abe02a3 100644 --- a/v3/response.go +++ b/v3/response.go @@ -183,7 +183,9 @@ func (r *searchResponse) start(ctx context.Context, searchRequest *SearchRequest r.ch <- result default: - r.conn.Debug.Printf("got application code: %d", packet.Children[1].Tag) + err := fmt.Errorf("unknown tag: %d", packet.Children[1].Tag) + r.ch <- &SearchSingleResult{Error: err} + return } } }