package ldap import ( "reflect" "testing" "github.com/hashicorp/go-hclog" "github.com/vjeantet/goldap/message" ldap "github.com/vjeantet/ldapserver" ) // recordingWriter captures everything written to it so tests can // assert on the exact sequence of responses a handler produces. type recordingWriter struct { responses []message.ProtocolOp } func (w *recordingWriter) Write(po message.ProtocolOp) { w.responses = append(w.responses, po) } func testLogger() hclog.Logger { return hclog.NewNullLogger() } func TestGuardRecoversPanic(t *testing.T) { s := &server{l: testLogger()} h := s.guard(func(w ldap.ResponseWriter, m *ldap.Message) { panic("boom") }) w := &recordingWriter{} h(w, mustDecode(t, berSearchRequest("ou=entities,dc=netauth,dc=example,dc=com"))) if len(w.responses) != 1 { t.Fatalf("got %d responses, want 1", len(w.responses)) } if _, ok := w.responses[0].(message.SearchResultDone); !ok { t.Errorf("response is %T, want message.SearchResultDone", w.responses[0]) } if code := resultCode(t, w.responses[0]); code != ldap.LDAPResultOperationsError { t.Errorf("result code is %d, want %d", code, ldap.LDAPResultOperationsError) } } func TestGuardSilentAfterPartialWrite(t *testing.T) { s := &server{l: testLogger()} h := s.guard(func(w ldap.ResponseWriter, m *ldap.Message) { w.Write(ldap.NewSearchResultEntry("cn=test")) panic("boom") }) w := &recordingWriter{} h(w, mustDecode(t, berSearchRequest("ou=entities,dc=netauth,dc=example,dc=com"))) if len(w.responses) != 1 { t.Errorf("got %d responses, want 1", len(w.responses)) } if _, ok := w.responses[0].(message.SearchResultEntry); !ok { t.Errorf("response is %T, want the partial entry only", w.responses[0]) } } func TestGuardPassesThrough(t *testing.T) { s := &server{l: testLogger()} h := s.guard(func(w ldap.ResponseWriter, m *ldap.Message) { w.Write(ldap.NewSearchResultDoneResponse(ldap.LDAPResultSuccess)) }) w := &recordingWriter{} h(w, &ldap.Message{}) if len(w.responses) != 1 { t.Fatalf("got %d responses, want 1", len(w.responses)) } if _, ok := w.responses[0].(message.SearchResultDone); !ok { t.Errorf("response is %T, want message.SearchResultDone", w.responses[0]) } } // TestResultForRequestPairing is the regression test for the client // hang: an LDAP client only accepts the result type that pairs with // the request it sent and silently discards anything else, so a // mismatched type leaves it waiting for a done message forever. func TestResultForRequestPairing(t *testing.T) { tests := []struct { name string op message.ProtocolOp want string }{ {"search", message.SearchRequest{}, "SearchResultDone"}, {"bind", message.BindRequest{}, "BindResponse"}, {"extended", message.ExtendedRequest{}, "ExtendedResponse"}, {"compare", message.CompareRequest{}, "CompareResponse"}, {"add", message.AddRequest{}, "AddResponse"}, {"delete", message.DelRequest("cn=x,dc=example,dc=com"), "DelResponse"}, {"modify", message.ModifyRequest{}, "ModifyResponse"}, {"modifyDN", message.ModifyDNRequest{}, "ModifyDNResponse"}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { res, ok := resultForRequest(tc.op, ldap.LDAPResultUnwillingToPerform, "nope") if !ok { t.Fatal("no result produced for a request that needs one") } if got := reflect.TypeOf(res).Name(); got != tc.want { t.Errorf("result type is %s, want %s", got, tc.want) } }) } // Abandon is never answered, so it must produce nothing rather // than an unsolicited result. if res, ok := resultForRequest(message.AbandonRequest(1), ldap.LDAPResultSuccess, ""); ok { t.Errorf("abandon produced %T, want no response", res) } } // TestSearchResultDoneWireTag pins the search reply to the // searchResDone application tag (5). A reply carrying any other tag, // including the bare LDAPResult universal SEQUENCE, is ignored by // clients and hangs them. func TestSearchResultDoneWireTag(t *testing.T) { res, ok := resultForRequest(message.SearchRequest{}, ldap.LDAPResultUnwillingToPerform, "nope") if !ok { t.Fatal("no result produced for a search request") } m := message.NewLDAPMessageWithProtocolOp(res) m.SetMessageID(2) b, err := m.Write() if err != nil { t.Fatalf("encoding response: %s", err) } raw := b.Bytes() // Descend from the LDAPMessage SEQUENCE into the protocolOp. off := 0 if _, off, err = message.ParseTagAndLength(raw, off); err != nil { t.Fatalf("message tag: %s", err) } var id message.TagAndLength if id, off, err = message.ParseTagAndLength(raw, off); err != nil { t.Fatalf("messageID tag: %s", err) } off += id.Length op, _, err := message.ParseTagAndLength(raw, off) if err != nil { t.Fatalf("protocolOp tag: %s", err) } if op.Class != 1 || op.Tag != 5 { t.Errorf("protocolOp is class %d tag %d, want APPLICATION (1) tag 5", op.Class, op.Tag) } }