From 7f8d1e44eeba4156c0a258b6a1af47cb0a7e2d80 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 21 Jul 2021 00:27:43 +0800 Subject: strings in LDAP are case-insensitive (#8) Thank you @wxiaoguang ! * strings in LDAP are case-insensitive * optmize routeFunc (faster, case-insensitive) * small optimiztion to routeFunc * request the directory server to return operational attributes by adding + (the plus sign) in your ldapsearch command. * request the directory server to return operational attributes by adding + (the plus sign) in your ldapsearch command. * request the directory server to return operational attributes by adding + (the plus sign) in your ldapsearch command. * remove operational attributes --- .gitignore | 1 + filter.go | 11 ++++++----- server.go | 16 +++++++++++----- server_search.go | 47 ++++++++++++++++++++++++++++++++++------------- server_search_test.go | 36 ++++++++++++++++++++++++++++++++++++ server_test.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 137 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 87275bf..1cc73fd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ examples/modify examples/search examples/searchSSL examples/searchTLS +.idea diff --git a/filter.go b/filter.go index 9f4c949..05c4bb2 100644 --- a/filter.go +++ b/filter.go @@ -315,22 +315,23 @@ func ServerApplyFilter(f *ber.Packet, entry *Entry) (bool, LDAPResultCode) { return false, LDAPResultOperationsError } attribute := f.Children[0].Value.(string) - bytes := f.Children[1].Children[0].Data.Bytes() - value := string(bytes[:]) + valueBytes := f.Children[1].Children[0].Data.Bytes() + valueLower := strings.ToLower(string(valueBytes[:])) for _, a := range entry.Attributes { if strings.ToLower(a.Name) == strings.ToLower(attribute) { for _, v := range a.Values { + vLower := strings.ToLower(v) switch f.Children[1].Children[0].Tag { case FilterSubstringsInitial: - if strings.HasPrefix(v, value) { + if strings.HasPrefix(vLower, valueLower) { return true, LDAPResultSuccess } case FilterSubstringsAny: - if strings.Contains(v, value) { + if strings.Contains(vLower, valueLower) { return true, LDAPResultSuccess } case FilterSubstringsFinal: - if strings.HasSuffix(v, value) { + if strings.HasSuffix(vLower, valueLower) { return true, LDAPResultSuccess } } diff --git a/server.go b/server.go index 3576475..24b1e77 100644 --- a/server.go +++ b/server.go @@ -376,14 +376,20 @@ func sendPacket(conn net.Conn, packet *ber.Packet) error { // func routeFunc(dn string, funcNames []string) string { bestPick := "" + bestPickWeight := 0 + dnMatch := "," + strings.ToLower(dn) + var weight int for _, fn := range funcNames { - if strings.HasSuffix(dn, fn) { - l := len(strings.Split(bestPick, ",")) - if bestPick == "" { - l = 0 + if strings.HasSuffix(dnMatch, "," + fn) { + // empty string as 0, no-comma string 1 , etc + if fn == "" { + weight = 0 + } else { + weight = strings.Count(fn, ",") + 1 } - if len(strings.Split(fn, ",")) > l { + if weight > bestPickWeight { bestPick = fn + bestPickWeight = weight } } } diff --git a/server_search.go b/server_search.go index b4f7a5f..12a6caf 100644 --- a/server_search.go +++ b/server_search.go @@ -46,6 +46,7 @@ func HandleSearchRequest(req *ber.Packet, controls *[]Control, messageID uint64, } i := 0 + searchReqBaseDNLower := strings.ToLower(searchReq.BaseDN) for _, entry := range searchResp.Entries { if server.EnforceLDAP { // filter @@ -61,25 +62,24 @@ func HandleSearchRequest(req *ber.Packet, controls *[]Control, messageID uint64, switch searchReq.Scope { case ScopeWholeSubtree: // The scope is constrained to the entry named by baseObject and to all its subordinates. case ScopeBaseObject: // The scope is constrained to the entry named by baseObject. - if entry.DN != searchReq.BaseDN { + if strings.ToLower(entry.DN) != searchReqBaseDNLower { continue } case ScopeSingleLevel: // The scope is constrained to the immediate subordinates of the entry named by baseObject. - parts := strings.Split(entry.DN, ",") - if len(parts) < 2 && entry.DN != searchReq.BaseDN { + entryDNLower := strings.ToLower(entry.DN) + parts := strings.Split(entryDNLower, ",") + if len(parts) < 2 && entryDNLower != searchReqBaseDNLower { continue } - if dn := strings.Join(parts[1:], ","); dn != searchReq.BaseDN { + if dnSuffix := strings.Join(parts[1:], ","); dnSuffix != searchReqBaseDNLower { continue } } - // attributes - if len(searchReq.Attributes) > 1 || (len(searchReq.Attributes) == 1 && len(searchReq.Attributes[0]) > 0) { - entry, err = filterAttributes(entry, searchReq.Attributes) - if err != nil { - return NewError(LDAPResultOperationsError, err) - } + // filter attributes + entry, err = filterAttributes(entry, searchReq.Attributes) + if err != nil { + return NewError(LDAPResultOperationsError, err) } // size limit @@ -160,9 +160,30 @@ func filterAttributes(entry *Entry, attributes []string) (*Entry, error) { // only return requested attributes newAttributes := []*EntryAttribute{} - for _, attr := range entry.Attributes { - for _, requested := range attributes { - if requested == "*" || strings.ToLower(attr.Name) == strings.ToLower(requested) { + if len(attributes) > 1 || (len(attributes) == 1 && len(attributes[0]) > 0) { + for _, attr := range entry.Attributes { + attrNameLower := strings.ToLower(attr.Name) + for _, requested := range attributes { + requestedLower := strings.ToLower(requested) + // You can request the directory server to return operational attributes by adding + (the plus sign) in your ldapsearch command. + // "+supportedControl" is treated as an operational attribute + if strings.HasPrefix(attrNameLower, "+") { + if requestedLower == "+" || attrNameLower == "+"+requestedLower { + newAttributes = append(newAttributes, &EntryAttribute{attr.Name[1:], attr.Values}) + break + } + } else { + if requested == "*" || attrNameLower == requestedLower { + newAttributes = append(newAttributes, attr) + break + } + } + } + } + } else { + // remove operational attributes + for _, attr := range entry.Attributes { + if !strings.HasPrefix(attr.Name, "+") { newAttributes = append(newAttributes, attr) } } diff --git a/server_search_test.go b/server_search_test.go index 5a083b0..09e2b14 100644 --- a/server_search_test.go +++ b/server_search_test.go @@ -451,6 +451,42 @@ func TestSearchScope(t *testing.T) { quit <- true } + +///////////////////////// +func TestSearchScopeCaseInsensitive(t *testing.T) { + quit := make(chan bool) + done := make(chan bool) + go func() { + s := NewServer() + s.EnforceLDAP = true + s.QuitChannel(quit) + s.SearchFunc("", searchCaseInsensitive{}) + s.BindFunc("", bindCaseInsensitive{}) + if err := s.ListenAndServe(listenString); err != nil { + t.Errorf("s.ListenAndServe failed: %s", err.Error()) + } + }() + + go func() { + cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", + "-b", "cn=Case,o=testers,c=test", "-D", "cn=CAse,o=testers,c=test", "-w", "iLike2test", "-s", "base", "cn=CASe") + out, _ := cmd.CombinedOutput() + if !strings.Contains(string(out), "dn: cn=CASE,o=testers,c=test") { + t.Errorf("ldapsearch 'base' scope failed - didn't find expected DN: %v", string(out)) + } + + done <- true + }() + + select { + case <-done: + case <-time.After(timeout): + t.Errorf("ldapsearch command timed out") + } + quit <- true +} + + func TestSearchControls(t *testing.T) { quit := make(chan bool) done := make(chan bool) diff --git a/server_test.go b/server_test.go index 88c47bf..233f7ea 100644 --- a/server_test.go +++ b/server_test.go @@ -329,6 +329,17 @@ func (b bindPanic) Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultC return LDAPResultInvalidCredentials, nil } +type bindCaseInsensitive struct { +} + +func (b bindCaseInsensitive) Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) { + if strings.ToLower(bindDN) == "cn=case,o=testers,c=test" && bindSimplePw == "iLike2test" { + return LDAPResultSuccess, nil + } + return LDAPResultInvalidCredentials, nil +} + + type searchSimple struct { } @@ -408,3 +419,41 @@ func (s searchControls) Search(boundDN string, searchReq SearchRequest, conn net } return ServerSearchResult{entries, []string{}, []Control{}, LDAPResultSuccess}, nil } + + +type searchCaseInsensitive struct { +} + +func (s searchCaseInsensitive) Search(boundDN string, searchReq SearchRequest, conn net.Conn) (ServerSearchResult, error) { + entries := []*Entry{ + &Entry{"cn=CASE,o=testers,c=test", []*EntryAttribute{ + &EntryAttribute{"cn", []string{"CaSe"}}, + &EntryAttribute{"o", []string{"ate"}}, + &EntryAttribute{"uidNumber", []string{"5005"}}, + &EntryAttribute{"accountstatus", []string{"active"}}, + &EntryAttribute{"uid", []string{"trent"}}, + &EntryAttribute{"description", []string{"trent via sa"}}, + &EntryAttribute{"objectclass", []string{"posixaccount"}}, + }}, + } + return ServerSearchResult{entries, []string{}, []Control{}, LDAPResultSuccess}, nil +} + + +func TestRouteFunc(t *testing.T) { + if routeFunc("", []string{"a", "xyz", "tt"}) != "" { + t.Error("routeFunc failed") + } + if routeFunc("a=b", []string{"a=b", "x=y,a=b", "tt"}) != "a=b" { + t.Error("routeFunc failed") + } + if routeFunc("x=y,a=b", []string{"a=b", "x=y,a=b", "tt"}) != "x=y,a=b" { + t.Error("routeFunc failed") + } + if routeFunc("x=y,a=b", []string{"x=y,a=b", "a=b", "tt"}) != "x=y,a=b" { + t.Error("routeFunc failed") + } + if routeFunc("nosuch", []string{"x=y,a=b", "a=b", "tt"}) != "" { + t.Error("routeFunc failed") + } +} -- cgit v1.2.3