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
+}