package ldapserver import ( "bytes" "crypto/tls" "log" "net" "os/exec" "strings" "testing" "time" ) var timeout = 400 * time.Millisecond var serverBaseDN = "o=testers,c=test" ///////////////////////// func TestBindAnonOK(t *testing.T) { done := make(chan bool) s := NewServer() defer s.Close() ln, addr := mustListen() go func() { s.Bind = BindAnonOK if err := s.Serve(ln); err != nil { t.Errorf("s.Serve failed: %s", err.Error()) } }() go func() { cmd := exec.Command("ldapsearch", "-H", "ldap://"+addr, "-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") } } ///////////////////////// func TestBindAnonFail(t *testing.T) { done := make(chan bool) s := NewServer() defer s.Close() ln, addr := mustListen() go func() { if err := s.Serve(ln); err != nil { t.Errorf("s.Serve failed: %s", err.Error()) } }() time.Sleep(timeout) go func() { cmd := exec.Command("ldapsearch", "-H", "ldap://"+addr, "-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") } } ///////////////////////// func TestBindSimpleOK(t *testing.T) { done := make(chan bool) s := NewServer() defer s.Close() ln, addr := mustListen() go func() { s.Search = SearchSimple s.Bind = BindSimple if err := s.Serve(ln); err != nil { t.Errorf("s.Serve failed: %s", err.Error()) } }() serverBaseDN := "o=testers,c=test" go func() { cmd := exec.Command("ldapsearch", "-H", "ldap://"+addr, "-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") } } ///////////////////////// func TestBindSimpleFailBadPw(t *testing.T) { done := make(chan bool) s := NewServer() defer s.Close() ln, addr := mustListen() go func() { s.Bind = BindSimple if err := s.Serve(ln); err != nil { t.Errorf("s.Serve failed: %s", err.Error()) } }() serverBaseDN := "o=testers,c=test" go func() { cmd := exec.Command("ldapsearch", "-H", "ldap://"+addr, "-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") } } ///////////////////////// func TestBindSimpleFailBadDn(t *testing.T) { done := make(chan bool) s := NewServer() defer s.Close() ln, addr := mustListen() go func() { s.Bind = BindSimple if err := s.Serve(ln); err != nil { t.Errorf("s.Serve failed: %s", err.Error()) } }() serverBaseDN := "o=testers,c=test" go func() { cmd := exec.Command("ldapsearch", "-H", "ldap://"+addr, "-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") } } ///////////////////////// func TestBindSSL(t *testing.T) { t.Skip("unclear how to configure ldapsearch command to trust or skip verification of a custom SSL cert") longerTimeout := 300 * time.Millisecond done := make(chan bool) s := NewServer() defer s.Close() cert, err := tls.LoadX509KeyPair("tests/cert_DONOTUSE.pem", "tests/key_DONOTUSE.pem") if err != nil { t.Fatal(err) } tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}} tlsConfig.ServerName = "localhost" ln, err := tls.Listen("tcp", "localhost:0", &tlsConfig) if err != nil { t.Fatal(err) } ldapURLSSL := "ldaps://" + ln.Addr().String() go func() { s.Bind = BindAnonOK if err := s.Serve(ln); err != nil { t.Errorf("s.Serve failed: %s", err.Error()) } }() go func() { time.Sleep(longerTimeout) cmd := exec.Command("ldapsearch", "-H", ldapURLSSL, "-x", "-b", "o=testers,c=test") out, err := cmd.CombinedOutput() if err != nil { t.Error(err) return } 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") } } ///////////////////////// func TestBindPanic(t *testing.T) { done := make(chan bool) s := NewServer() defer s.Close() ln, addr := mustListen() go func() { s.Bind = BindPanic if err := s.Serve(ln); err != nil { t.Errorf("s.Serve failed: %s", err.Error()) } }() go func() { cmd := exec.Command("ldapsearch", "-H", "ldap://"+addr, "-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") } } ///////////////////////// 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) done := make(chan bool) s := NewServer() defer s.Close() ln, addr := mustListen() go func() { s.Search = SearchSimple s.Bind = BindAnonOK s.SetStats(true) if err := s.Serve(ln); err != nil { t.Errorf("s.Serve failed: %s", err.Error()) } }() go func() { cmd := exec.Command("ldapsearch", "-H", "ldap://"+addr, "-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()) } } func BindAnonOK(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) { if bindDN == "" && bindSimplePw == "" { return LDAPResultSuccess, nil } return LDAPResultInvalidCredentials, nil } func BindSimple(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) { if bindDN == "cn=testy,o=testers,c=test" && bindSimplePw == "iLike2test" { return LDAPResultSuccess, nil } return LDAPResultInvalidCredentials, nil } func BindPanic(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) { panic("test panic at the disco") } func SearchSimple(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 } func SearchPanic(boundDN string, searchReq SearchRequest, conn net.Conn) (ServerSearchResult, error) { panic("this is a test panic") } func SearchControls(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 } // mustListen returns a net.Listener listening on a random port. func mustListen() (ln net.Listener, actualAddr string) { ln, err := net.Listen("tcp", "localhost:0") if err != nil { panic(err) } return ln, ln.Addr().String() }