diff --git a/README.md b/README.md index 0fb1999..a05b18c 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Example: package main import ( + "context" "log" "os" "os/signal" @@ -46,17 +47,18 @@ func main() { log.Fatalf("unable to create router: %s", err.Error()) } r.Bind(bindHandler) + r.Search(searchHandler) s.Router(r) - go s.Run(":10389") // listen on port 10389 // stop server gracefully when ctrl-c, sigint or sigterm occurs - ch := make(chan os.Signal) - signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) - <-ch - close(ch) - - s.Stop() + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + select { + case <-ctx.Done(): + log.Printf("\nstopping directory") + s.Stop() + } } func bindHandler(w *gldap.ResponseWriter, r *gldap.Request) { @@ -78,6 +80,51 @@ func bindHandler(w *gldap.ResponseWriter, r *gldap.Request) { log.Println("bind success") return } + +func searchHandler(w *gldap.ResponseWriter, r *gldap.Request) { + resp := r.NewSearchDoneResponse() + defer func() { + w.Write(resp) + }() + m, err := r.GetSearchMessage() + if err != nil { + log.Printf("not a search message: %s", err) + return + } + log.Printf("search base dn: %s", m.BaseDN) + log.Printf("search scope: %d", m.Scope) + log.Printf("search filter: %s", m.Filter) + + if strings.Contains(m.Filter, "uid=alice") || m.BaseDN == "uid=alice,ou=people,cn=example,dc=org" { + entry := r.NewSearchResponseEntry( + "uid=alice,ou=people,cn=example,dc=org", + gldap.WithAttributes(map[string][]string{ + "objectclass": {"top", "person", "organizationalPerson", "inetOrgPerson"}, + "uid": {"alice"}, + "cn": {"alice eve smith"}, + "givenname": {"alice"}, + "sn": {"smith"}, + "ou": {"people"}, + "description": {"friend of Rivest, Shamir and Adleman"}, + "password": {"{SSHA}U3waGJVC7MgXYc0YQe7xv7sSePuTP8zN"}, + }), + ) + entry.AddAttribute("email", []string{"alice@example.org"}) + w.Write(entry) + resp.SetResultCode(gldap.ResultSuccess) + } + if m.BaseDN == "ou=people,cn=example,dc=org" { + entry := r.NewSearchResponseEntry( + "ou=people,cn=example,dc=org", + gldap.WithAttributes(map[string][]string{ + "objectclass": {"organizationalUnit"}, + "ou": {"people"}, + }), + ) + w.Write(entry) + resp.SetResultCode(gldap.ResultSuccess) + } + return } ```
@@ -113,3 +160,4 @@ service much easier. `testdirectory` is also a great working example of how you can use `gldap` to build a custom ldap server to meet your specific needs. + diff --git a/codes.go b/codes.go index 262ac7d..91a17aa 100644 --- a/codes.go +++ b/codes.go @@ -1,6 +1,6 @@ package gldap -// Result Codes +// ldap result codes const ( ResultSuccess = 0 ResultOperationsError = 1 @@ -77,7 +77,7 @@ const ( ResultSyncRefreshRequired = 4096 ) -// ResultCodeMap contains string descriptions for error codes +// ResultCodeMap contains string descriptions for ldap result codes var ResultCodeMap = map[uint16]string{ ResultSuccess: "Success", ResultOperationsError: "Operations Error", @@ -154,7 +154,7 @@ var ResultCodeMap = map[uint16]string{ ResultAuthorizationDenied: "Authorization Denied", } -// Application Codes +// ldap application codes const ( ApplicationBindRequest = 0 ApplicationBindResponse = 1 @@ -178,7 +178,7 @@ const ( ApplicationExtendedResponse = 24 ) -// ApplicationCodeMap contains human readable descriptions of Application Codes +// ApplicationCodeMap contains human readable descriptions of ldap application codes var ApplicationCodeMap = map[uint8]string{ ApplicationBindRequest: "Bind Request", ApplicationBindResponse: "Bind Response", diff --git a/control.go b/control.go new file mode 100644 index 0000000..c1dcc2e --- /dev/null +++ b/control.go @@ -0,0 +1,21 @@ +package gldap + +import ber "github.com/go-asn1-ber/asn1-ber" + +// Control defines a common interface for all ldap controls +type Control interface { + // GetControlType returns the OID + GetControlType() string + // Encode returns the ber packet representation + Encode() *ber.Packet + // String returns a human-readable description + String() string +} + +func encodeControls(controls []Control) *ber.Packet { + packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0, nil, "Controls") + for _, control := range controls { + packet.AppendChild(control.Encode()) + } + return packet +} diff --git a/examples/simple-bind/.gitignore b/examples/simple-bind/.gitignore deleted file mode 100644 index 7a8b512..0000000 --- a/examples/simple-bind/.gitignore +++ /dev/null @@ -1 +0,0 @@ -simple-bind diff --git a/examples/simple/.gitignore b/examples/simple/.gitignore new file mode 100644 index 0000000..ab23474 --- /dev/null +++ b/examples/simple/.gitignore @@ -0,0 +1 @@ +simple diff --git a/examples/simple-bind/main.go b/examples/simple/main.go similarity index 57% rename from examples/simple-bind/main.go rename to examples/simple/main.go index 3165484..b3dff86 100644 --- a/examples/simple-bind/main.go +++ b/examples/simple/main.go @@ -5,6 +5,7 @@ import ( "log" "os" "os/signal" + "strings" "github.com/hashicorp/go-hclog" "github.com/jimlambrt/gldap" @@ -29,7 +30,7 @@ func main() { log.Fatalf("unable to create router: %s", err.Error()) } r.Bind(bindHandler) - r.Search(searchHandler) + r.Search(searchHandler, gldap.WithLabel("All Searches")) s.Router(r) go s.Run(":10389") // listen on port 10389 @@ -64,7 +65,7 @@ func bindHandler(w *gldap.ResponseWriter, r *gldap.Request) { } func searchHandler(w *gldap.ResponseWriter, r *gldap.Request) { - resp := r.NewSearchDoneResponse() + resp := r.NewSearchDoneResponse(gldap.WithResponseCode(gldap.ResultNoSuchObject)) defer func() { w.Write(resp) }() @@ -77,5 +78,34 @@ func searchHandler(w *gldap.ResponseWriter, r *gldap.Request) { log.Printf("search scope: %d", m.Scope) log.Printf("search filter: %s", m.Filter) + if strings.Contains(m.Filter, "uid=alice") || m.BaseDN == "uid=alice,ou=people,cn=example,dc=org" { + entry := r.NewSearchResponseEntry( + "uid=alice,ou=people,cn=example,dc=org", + gldap.WithAttributes(map[string][]string{ + "objectclass": {"top", "person", "organizationalPerson", "inetOrgPerson"}, + "uid": {"alice"}, + "cn": {"alice eve smith"}, + "givenname": {"alice"}, + "sn": {"smith"}, + "ou": {"people"}, + "description": {"friend of Rivest, Shamir and Adleman"}, + "password": {"{SSHA}U3waGJVC7MgXYc0YQe7xv7sSePuTP8zN"}, + }), + ) + entry.AddAttribute("email", []string{"alice@example.org"}) + w.Write(entry) + resp.SetResultCode(gldap.ResultSuccess) + } + if m.BaseDN == "ou=people,cn=example,dc=org" { + entry := r.NewSearchResponseEntry( + "ou=people,cn=example,dc=org", + gldap.WithAttributes(map[string][]string{ + "objectclass": {"organizationalUnit"}, + "ou": {"people"}, + }), + ) + w.Write(entry) + resp.SetResultCode(gldap.ResultSuccess) + } return } diff --git a/message.go b/message.go index fa267a1..e3c4478 100644 --- a/message.go +++ b/message.go @@ -1,6 +1,8 @@ package gldap -import "fmt" +import ( + "fmt" +) // Scope represents the scope of a search (see: https://ldap.com/the-ldap-search-operation/) type Scope int64 @@ -43,6 +45,7 @@ const ( // Message defines a common interface for all messages type Message interface { + // GetID returns the message ID GetID() int64 } @@ -57,7 +60,7 @@ func (m baseMessage) GetID() int64 { return m.id } // SearchMessage is a search request message type SearchMessage struct { baseMessage - // BaseObject for the request + // BaseDN for the request BaseDN string // Scope of the request Scope Scope @@ -77,8 +80,6 @@ type SearchMessage struct { Controls []Control } -type Control string - // SimpleBindMesssage is a simple bind request message type SimpleBindMessage struct { baseMessage diff --git a/mux.go b/mux.go index 81f68a7..149341f 100644 --- a/mux.go +++ b/mux.go @@ -45,6 +45,7 @@ func (m *Mux) Bind(bindFn HandlerFunc, opt ...Option) error { } // Search will register a handler for search requests. +// Options supported: WithLabel, WithBaseDN, WithScope func (m *Mux) Search(searchFn HandlerFunc, opt ...Option) error { const op = "gldap.(Mux).Search" if searchFn == nil { @@ -68,6 +69,7 @@ func (m *Mux) Search(searchFn HandlerFunc, opt ...Option) error { } // ExtendedOperation will register a handler for extended operation requests. +// Options supported: WithLabel func (m *Mux) ExtendedOperation(operationFn HandlerFunc, exName ExtendedOperationName, opt ...Option) error { const op = "gldap.(Mux).Search" if operationFn == nil { @@ -101,7 +103,7 @@ func (m *Mux) DefaultRoute(noRouteFN HandlerFunc, opt ...Option) error { } m.mu.Lock() defer m.mu.Unlock() - m.routes = append(m.routes, r) + m.defaultRoute = r return nil } diff --git a/mux_test.go b/mux_test.go new file mode 100644 index 0000000..41e9821 --- /dev/null +++ b/mux_test.go @@ -0,0 +1,96 @@ +package gldap + +import ( + "bufio" + "bytes" + "fmt" + "strings" + "sync" + "testing" + "time" + + "github.com/go-ldap/ldap/v3" + "github.com/hashicorp/go-hclog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMux_serve(t *testing.T) { + t.Run("no-matching-handler", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + var buf bytes.Buffer + testLogger := hclog.New(&hclog.LoggerOptions{ + Name: "TestServer_Run-logger", + Level: hclog.Debug, + Output: &buf, + }) + s, err := NewServer(WithLogger(testLogger)) + require.NoError(err) + port := freePort(t) + go func() { + err = s.Run(fmt.Sprintf(":%d", port)) + assert.NoError(err) + }() + defer s.Stop() + time.Sleep(1 * time.Millisecond) + client, err := ldap.DialURL(fmt.Sprintf("ldap://localhost:%d", port)) + require.NoError(err) + defer client.Close() + err = client.UnauthenticatedBind("alice") + require.Error(err) + assert.Contains(strings.ToLower(err.Error()), "no matching handler found") + }) + t.Run("default-route", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + var buf bytes.Buffer + testLogger := hclog.New(&hclog.LoggerOptions{ + Name: "TestServer_Run-logger", + Level: hclog.Debug, + Output: &buf, + }) + s, err := NewServer(WithLogger(testLogger)) + require.NoError(err) + + mux, err := NewMux() + require.NoError(err) + mux.DefaultRoute(func(w *ResponseWriter, req *Request) { + resp := req.NewResponse(WithResponseCode(ResultUnwillingToPerform), WithDiagnosticMessage("default handler")) + _ = w.Write(resp) + }) + s.Router(mux) + + port := freePort(t) + go func() { + err = s.Run(fmt.Sprintf(":%d", port)) + assert.NoError(err) + }() + defer s.Stop() + time.Sleep(1 * time.Millisecond) + client, err := ldap.DialURL(fmt.Sprintf("ldap://localhost:%d", port)) + require.NoError(err) + defer client.Close() + err = client.UnauthenticatedBind("alice") + require.Error(err) + assert.Contains(strings.ToLower(err.Error()), "default handler") + }) + t.Run("bad-parameters", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + m := &Mux{} + assert.Panics( + func() { m.serve(nil, &Request{}) }, + "missing response writer", + ) + + var writerBuf bytes.Buffer + var logBuf bytes.Buffer + testLogger := hclog.New(&hclog.LoggerOptions{ + Name: "TestServer_Run-logger", + Level: hclog.Debug, + Output: &logBuf, + }) + w, err := newResponseWriter(bufio.NewWriter(&writerBuf), &sync.Mutex{}, testLogger, 1, 2) + require.NoError(err) + m.serve(w, nil) + assert.Contains(logBuf.String(), "missing request") + }) +} diff --git a/packet.go b/packet.go index e620ed4..e91cd60 100644 --- a/packet.go +++ b/packet.go @@ -48,7 +48,11 @@ func (p *packet) requestMessageID() (int64, error) { if err := msgIdPacket.assert(ber.ClassUniversal, ber.TypePrimitive, withTag(ber.TagInteger)); err != nil { return 0, fmt.Errorf("%s: missing/invalid packet: %w", op, err) } - return msgIdPacket.Value.(int64), nil + id, ok := msgIdPacket.Value.(int64) + if !ok { + return 0, fmt.Errorf("%s: expected int64 message ID and got %t: %w", op, msgIdPacket.Value, ErrInvalidParameter) + } + return id, nil } func (p *packet) requestPacket() (*packet, error) { @@ -306,6 +310,7 @@ func (p *packet) assert(cl ber.Class, ty ber.Type, opt ...Option) error { return nil } +// Log will pretty print log a packet func (p *packet) Log(out io.Writer, indent int, printBytes bool) { indent_str := "" diff --git a/request.go b/request.go index 9754a7d..e783a3e 100644 --- a/request.go +++ b/request.go @@ -5,9 +5,10 @@ import ( "fmt" ) +// ExtendedOperationName is an extended operation request/response name type ExtendedOperationName string -// Extended operation response/request name +// Extended operation response/request names const ( ExtendedOperationDisconnection ExtendedOperationName = "1.3.6.1.4.1.1466.2003" ExtendedOperationCancel ExtendedOperationName = "1.3.6.1.1.8" @@ -18,8 +19,12 @@ const ( ExtendedOperationUnknown ExtendedOperationName = "Unknown" ) +// Request represents an ldap request type Request struct { + // ID is the request number for a specific connection. Every connection has + // its own request counter which starts at 1. ID int + // conn is needed this for cancellation among other things. conn *conn message Message @@ -33,7 +38,7 @@ func newRequest(id int, c *conn, p *packet) (*Request, error) { return nil, fmt.Errorf("%s: missing connection: %w", op, ErrInvalidParameter) } if p == nil { - return nil, fmt.Errorf("%s: missing ber packet: %w", op, ErrInvalidParameter) + return nil, fmt.Errorf("%s: missing packet: %w", op, ErrInvalidParameter) } m, err := newMessage(p) @@ -51,6 +56,8 @@ func newRequest(id int, c *conn, p *packet) (*Request, error) { routeOp = extendedRouteOperation extendedName = v.Name default: + // this should be unreachable, since newMessage defaults to returning an + // *ExtendedOperationMessage return nil, fmt.Errorf("%s: %v is an unsupported route operation: %w", op, v, ErrInternal) } @@ -80,8 +87,9 @@ func (r *Request) StartTLS(tlsconfig *tls.Config) error { return nil } -// NewResponse creates a general response (not tied to any specific request) -// options supported: WithApplicationCode, WithDiagnosticMessage, WithMatchedDN +// NewResponse creates a general response (not tied to any specific request). +// Supported options: WithResponseCode, WithApplicationCode, +// WithDiagnosticMessage, WithMatchedDN func (r *Request) NewResponse(opt ...Option) *GeneralResponse { const op = "gldap.NewBindResponse" opts := getResponseOpts(opt...) @@ -103,7 +111,7 @@ func (r *Request) NewResponse(opt ...Option) *GeneralResponse { } // NewExtendedResponse creates a new extended response. -// Supports options: WithResponseCode +// Supported options: WithResponseCode func (r *Request) NewExtendedResponse(opt ...Option) *ExtendedResponse { const op = "gldap.NewExtendedResponse" opts := getResponseOpts(opt...) @@ -119,7 +127,7 @@ func (r *Request) NewExtendedResponse(opt ...Option) *ExtendedResponse { } // NewBindResponse creates a new bind response. -// Supports options: WithResponseCode +// Supported options: WithResponseCode func (r *Request) NewBindResponse(opt ...Option) *BindResponse { const op = "gldap.NewBindResponse" opts := getResponseOpts(opt...) @@ -149,7 +157,7 @@ func (r *Request) GetSimpleBindMessage() (*SimpleBindMessage, error) { // results found, then set the response code by adding the option // WithResponseCode(ResultNoSuchObject) // -// Supports options: WithResponseCode +// Supported options: WithResponseCode func (r *Request) NewSearchDoneResponse(opt ...Option) *SearchResponseDone { const op = "gldap.(Request).NewSearchDoneResponse" opts := getResponseOpts(opt...) @@ -175,10 +183,7 @@ func (r *Request) GetSearchMessage() (*SearchMessage, error) { return s, nil } -func intPtr(i int) *int { - return &i -} - +// NewSearchResponseEntry is a search response entry. // Supported options: WithAttributes func (r *Request) NewSearchResponseEntry(entryDN string, opt ...Option) *SearchResponseEntry { opts := getResponseOpts(opt...) @@ -196,3 +201,7 @@ func (r *Request) NewSearchResponseEntry(entryDN string, opt ...Option) *SearchR }, } } + +func intPtr(i int) *int { + return &i +} diff --git a/request_test.go b/request_test.go new file mode 100644 index 0000000..76e362c --- /dev/null +++ b/request_test.go @@ -0,0 +1,92 @@ +package gldap + +import ( + "testing" + + ber "github.com/go-asn1-ber/asn1-ber" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_newRequest(t *testing.T) { + tests := []struct { + name string + requestID int + conn *conn + packet *packet + wantErr bool + wantErrIs error + wantErrContains string + }{ + { + name: "missing-conn", + requestID: 1, + packet: &packet{ + Packet: &ber.Packet{}, + }, + wantErr: true, + wantErrIs: ErrInvalidParameter, + wantErrContains: "missing connection", + }, + { + name: "missing-packet", + requestID: 1, + conn: &conn{}, + wantErr: true, + wantErrIs: ErrInvalidParameter, + wantErrContains: "missing packet", + }, + { + name: "invalid-message", + requestID: 1, + conn: &conn{}, + packet: &packet{ + Packet: &ber.Packet{}, + }, + wantErr: true, + wantErrIs: ErrInvalidParameter, + wantErrContains: "unable to build message", + }, + { + name: "valid-simple-bind", + requestID: 1, + conn: &conn{}, + packet: testSimpleBindRequestPacket(t, + SimpleBindMessage{baseMessage: baseMessage{id: 1}, UserName: "alice", Password: "fido"}, + ), + }, + { + name: "valid-search", + requestID: 1, + conn: &conn{}, + packet: testSearchRequestPacket(t, + SearchMessage{baseMessage: baseMessage{id: 1}, Filter: "(uid=alice)"}, + ), + }, + { + name: "valid-extended", + requestID: 1, + conn: &conn{}, + packet: testStartTLSRequestPacket(t, 1), + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + req, err := newRequest(tc.requestID, tc.conn, tc.packet) + if tc.wantErr { + require.Error(err) + assert.Nil(req) + if tc.wantErrIs != nil { + assert.ErrorIs(err, tc.wantErrIs) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + require.NotNil(req) + }) + } +} diff --git a/server.go b/server.go index 014eef1..0939a4b 100644 --- a/server.go +++ b/server.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "fmt" "net" + "strings" "sync" "time" @@ -88,9 +89,12 @@ func (s *Server) Run(addr string, opt ...Option) error { default: // need a default to fall through to rest of loop... } - c, err := s.listener.Accept() if err != nil { + if strings.Contains(err.Error(), "use of closed network connection") { + s.logger.Debug("accept on closed conn") + return nil + } return fmt.Errorf("%s: error accepting conn: %w", op, err) } s.logger.Debug("new connection accepted", "op", op, "conn", connID) diff --git a/server_test.go b/server_test.go index 1a79c1d..3a6083b 100644 --- a/server_test.go +++ b/server_test.go @@ -120,6 +120,7 @@ func TestServer_shutdownCtx(t *testing.T) { }) fakeT := &testdirectory.Logger{Logger: l} td := testdirectory.Start(fakeT, testdirectory.WithDefaults(t, &testdirectory.Defaults{AllowAnonymousBind: true})) + time.Sleep(5 * time.Millisecond) go func() { client := td.Conn() defer client.Close() diff --git a/testing.go b/testing.go index 1d94f3d..80ef863 100644 --- a/testing.go +++ b/testing.go @@ -4,6 +4,8 @@ import ( "net" "testing" + ber "github.com/go-asn1-ber/asn1-ber" + "github.com/go-ldap/ldap/v3" "github.com/stretchr/testify/require" ) @@ -18,3 +20,69 @@ func freePort(t *testing.T) int { defer l.Close() return l.Addr().(*net.TCPAddr).Port } + +func testStartTLSRequestPacket(t *testing.T, messageID int) *packet { + t.Helper() + envelope := testRequestEnvelope(t, int(messageID)) + + request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Start TLS") + request.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, "1.3.6.1.4.1.1466.20037", "TLS Extended Command")) + envelope.AppendChild(request) + + return &packet{ + Packet: envelope, + } +} + +func testSearchRequestPacket(t *testing.T, s SearchMessage) *packet { + t.Helper() + require := require.New(t) + envelope := testRequestEnvelope(t, int(s.GetID())) + pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationSearchRequest, nil, "Search Request") + pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, s.BaseDN, "Base DN")) + pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, int64(s.Scope), "Scope")) + pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, int64(s.DerefAliases), "Deref Aliases")) + pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(s.SizeLimit), "Size Limit")) + pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(s.TimeLimit), "Time Limit")) + pkt.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, s.TypesOnly, "Types Only")) + + // compile and encode filter + filterPacket, err := ldap.CompileFilter(s.Filter) + require.NoError(err) + pkt.AppendChild(filterPacket) + + attributesPacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes") + for _, attribute := range s.Attributes { + attributesPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute")) + } + pkt.AppendChild(attributesPacket) + + envelope.AppendChild(pkt) + if len(s.Controls) > 0 { + envelope.AppendChild(encodeControls(s.Controls)) + } + + return &packet{ + Packet: envelope, + } +} + +func testSimpleBindRequestPacket(t *testing.T, m SimpleBindMessage) *packet { + t.Helper() + + envelope := testRequestEnvelope(t, int(m.GetID())) + pkt := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") + pkt.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(3), "Version")) + pkt.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, m.UserName, "User Name")) + pkt.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, string(m.Password), "Password")) + envelope.AppendChild(pkt) + return &packet{ + Packet: envelope, + } +} + +func testRequestEnvelope(t *testing.T, messageID int) *ber.Packet { + p := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + p.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(messageID), "MessageID")) + return p +}