package ldap import ( "bytes" "log" "net" "os/exec" "strings" "testing" "time" ) var listenString = "localhost:3389" var ldapURL = "ldap://" + listenString var timeout = 400 * time.Millisecond var serverBaseDN = "o=testers,c=test" ///////////////////////// func TestBindAnonOK(t *testing.T) { quit := make(chan bool) done := make(chan bool) go func() { s := NewServer() s.QuitChannel(quit) s.BindFunc("", bindAnonOK{}) 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", "o=testers,c=test") out, _ := cmd.CombinedOutput() if !strings.Contains(string(out), "result: 0 Success") { t.Errorf("ldapsearch failed: %v", string(out)) } done <- true }() select { case <-done: case <-time.After(timeout): t.Errorf("ldapsearch command timed out") } quit <- true } ///////////////////////// func TestBindAnonFail(t *testing.T) { quit := make(chan bool) done := make(chan bool) go func() { s := NewServer() s.QuitChannel(quit) if err := s.ListenAndServe(listenString); err != nil { t.Errorf("s.ListenAndServe failed: %s", err.Error()) } }() time.Sleep(timeout) go func() { cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", "-b", "o=testers,c=test") out, _ := cmd.CombinedOutput() if !strings.Contains(string(out), "ldap_bind: Invalid credentials (49)") { t.Errorf("ldapsearch failed: %v", string(out)) } done <- true }() select { case <-done: case <-time.After(timeout): t.Errorf("ldapsearch command timed out") } time.Sleep(timeout) quit <- true } ///////////////////////// func TestBindSimpleOK(t *testing.T) { quit := make(chan bool) done := make(chan bool) go func() { s := NewServer() s.QuitChannel(quit) s.SearchFunc("", searchSimple{}) s.BindFunc("", bindSimple{}) if err := s.ListenAndServe(listenString); err != nil { t.Errorf("s.ListenAndServe failed: %s", err.Error()) } }() serverBaseDN := "o=testers,c=test" go func() { cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", "-b", serverBaseDN, "-D", "cn=testy,"+serverBaseDN, "-w", "iLike2test") out, _ := cmd.CombinedOutput() if !strings.Contains(string(out), "result: 0 Success") { t.Errorf("ldapsearch failed: %v", string(out)) } done <- true }() select { case <-done: case <-time.After(timeout): t.Errorf("ldapsearch command timed out") } quit <- true } ///////////////////////// func TestBindSimpleFailBadPw(t *testing.T) { quit := make(chan bool) done := make(chan bool) go func() { s := NewServer() s.QuitChannel(quit) s.BindFunc("", bindSimple{}) if err := s.ListenAndServe(listenString); err != nil { t.Errorf("s.ListenAndServe failed: %s", err.Error()) } }() serverBaseDN := "o=testers,c=test" go func() { cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", "-b", serverBaseDN, "-D", "cn=testy,"+serverBaseDN, "-w", "BADPassword") out, _ := cmd.CombinedOutput() if !strings.Contains(string(out), "ldap_bind: Invalid credentials (49)") { t.Errorf("ldapsearch succeeded - should have failed: %v", string(out)) } done <- true }() select { case <-done: case <-time.After(timeout): t.Errorf("ldapsearch command timed out") } quit <- true } ///////////////////////// func TestBindSimpleFailBadDn(t *testing.T) { quit := make(chan bool) done := make(chan bool) go func() { s := NewServer() s.QuitChannel(quit) s.BindFunc("", bindSimple{}) if err := s.ListenAndServe(listenString); err != nil { t.Errorf("s.ListenAndServe failed: %s", err.Error()) } }() serverBaseDN := "o=testers,c=test" go func() { cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", "-b", serverBaseDN, "-D", "cn=testoy,"+serverBaseDN, "-w", "iLike2test") out, _ := cmd.CombinedOutput() if string(out) != "ldap_bind: Invalid credentials (49)\n" { t.Errorf("ldapsearch succeeded - should have failed: %v", string(out)) } done <- true }() select { case <-done: case <-time.After(timeout): t.Errorf("ldapsearch command timed out") } quit <- true } ///////////////////////// func TestBindSSL(t *testing.T) { ldapURLSSL := "ldaps://" + listenString longerTimeout := 300 * time.Millisecond quit := make(chan bool) done := make(chan bool) go func() { s := NewServer() s.QuitChannel(quit) s.BindFunc("", bindAnonOK{}) if err := s.ListenAndServeTLS(listenString, "tests/cert_DONOTUSE.pem", "tests/key_DONOTUSE.pem"); err != nil { t.Errorf("s.ListenAndServeTLS failed: %s", err.Error()) } }() go func() { time.Sleep(longerTimeout * 2) cmd := exec.Command("ldapsearch", "-H", ldapURLSSL, "-x", "-b", "o=testers,c=test") out, _ := cmd.CombinedOutput() if !strings.Contains(string(out), "result: 0 Success") { t.Errorf("ldapsearch failed: %v", string(out)) } done <- true }() select { case <-done: case <-time.After(longerTimeout * 2): t.Errorf("ldapsearch command timed out") } quit <- true } ///////////////////////// func TestBindPanic(t *testing.T) { quit := make(chan bool) done := make(chan bool) go func() { s := NewServer() s.QuitChannel(quit) s.BindFunc("", bindPanic{}) 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", "o=testers,c=test") out, _ := cmd.CombinedOutput() if !strings.Contains(string(out), "ldap_bind: Operations error") { t.Errorf("ldapsearch should have returned operations error due to panic: %v", string(out)) } done <- true }() select { case <-done: case <-time.After(timeout): t.Errorf("ldapsearch command timed out") } quit <- true } ///////////////////////// type testStatsWriter struct { buffer *bytes.Buffer } func (tsw testStatsWriter) Write(buf []byte) (int, error) { tsw.buffer.Write(buf) return len(buf), nil } func TestSearchStats(t *testing.T) { w := testStatsWriter{&bytes.Buffer{}} log.SetOutput(w) quit := make(chan bool) done := make(chan bool) s := NewServer() go func() { s.QuitChannel(quit) s.SearchFunc("", searchSimple{}) s.BindFunc("", bindAnonOK{}) s.SetStats(true) 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", "o=testers,c=test") out, _ := cmd.CombinedOutput() if !strings.Contains(string(out), "result: 0 Success") { t.Errorf("ldapsearch failed: %v", string(out)) } done <- true }() select { case <-done: case <-time.After(timeout): t.Errorf("ldapsearch command timed out") } stats := s.GetStats() log.Println(stats) if stats.Conns != 1 || stats.Binds != 1 { t.Errorf("Stats data missing or incorrect: %v", w.buffer.String()) } quit <- true } ///////////////////////// type bindAnonOK struct { } func (b bindAnonOK) Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) { if bindDN == "" && bindSimplePw == "" { return LDAPResultSuccess, nil } return LDAPResultInvalidCredentials, nil } type bindSimple struct { } func (b bindSimple) Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) { if bindDN == "cn=testy,o=testers,c=test" && bindSimplePw == "iLike2test" { return LDAPResultSuccess, nil } return LDAPResultInvalidCredentials, nil } type bindSimple2 struct { } func (b bindSimple2) Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) { if bindDN == "cn=testy,o=testers,c=testz" && bindSimplePw == "ZLike2test" { return LDAPResultSuccess, nil } return LDAPResultInvalidCredentials, nil } type bindPanic struct { } func (b bindPanic) Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) { panic("test panic at the disco") 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 { } func (s searchSimple) Search(boundDN string, searchReq SearchRequest, conn net.Conn) (ServerSearchResult, error) { entries := []*Entry{ &Entry{"cn=ned,o=testers,c=test", []*EntryAttribute{ &EntryAttribute{"cn", []string{"ned"}}, &EntryAttribute{"o", []string{"ate"}}, &EntryAttribute{"uidNumber", []string{"5000"}}, &EntryAttribute{"accountstatus", []string{"active"}}, &EntryAttribute{"uid", []string{"ned"}}, &EntryAttribute{"description", []string{"ned via sa"}}, &EntryAttribute{"objectclass", []string{"posixaccount"}}, }}, &Entry{"cn=trent,o=testers,c=test", []*EntryAttribute{ &EntryAttribute{"cn", []string{"trent"}}, &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"}}, }}, &Entry{"cn=randy,o=testers,c=test", []*EntryAttribute{ &EntryAttribute{"cn", []string{"randy"}}, &EntryAttribute{"o", []string{"ate"}}, &EntryAttribute{"uidNumber", []string{"5555"}}, &EntryAttribute{"accountstatus", []string{"active"}}, &EntryAttribute{"uid", []string{"randy"}}, &EntryAttribute{"objectclass", []string{"posixaccount"}}, }}, } return ServerSearchResult{entries, []string{}, []Control{}, LDAPResultSuccess}, nil } type searchSimple2 struct { } func (s searchSimple2) Search(boundDN string, searchReq SearchRequest, conn net.Conn) (ServerSearchResult, error) { entries := []*Entry{ &Entry{"cn=hamburger,o=testers,c=testz", []*EntryAttribute{ &EntryAttribute{"cn", []string{"hamburger"}}, &EntryAttribute{"o", []string{"testers"}}, &EntryAttribute{"uidNumber", []string{"5000"}}, &EntryAttribute{"accountstatus", []string{"active"}}, &EntryAttribute{"uid", []string{"hamburger"}}, &EntryAttribute{"objectclass", []string{"posixaccount"}}, }}, } return ServerSearchResult{entries, []string{}, []Control{}, LDAPResultSuccess}, nil } type searchPanic struct { } func (s searchPanic) Search(boundDN string, searchReq SearchRequest, conn net.Conn) (ServerSearchResult, error) { entries := []*Entry{} panic("this is a test panic") return ServerSearchResult{entries, []string{}, []Control{}, LDAPResultSuccess}, nil } type searchControls struct { } func (s searchControls) Search(boundDN string, searchReq SearchRequest, conn net.Conn) (ServerSearchResult, error) { entries := []*Entry{} if len(searchReq.Controls) == 1 && searchReq.Controls[0].GetControlType() == "1.2.3.4.5" { newEntry := &Entry{"cn=hamburger,o=testers,c=testz", []*EntryAttribute{ &EntryAttribute{"cn", []string{"hamburger"}}, &EntryAttribute{"o", []string{"testers"}}, &EntryAttribute{"uidNumber", []string{"5000"}}, &EntryAttribute{"accountstatus", []string{"active"}}, &EntryAttribute{"uid", []string{"hamburger"}}, &EntryAttribute{"objectclass", []string{"posixaccount"}}, }} entries = append(entries, newEntry) } 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") } }