├── .gitignore ├── doc.go ├── .githooks └── pre-push ├── atomic_value.go ├── debug.go ├── atomic_value_go13.go ├── client.go ├── search_test.go ├── .travis.yml ├── README ├── LICENSE ├── Makefile ├── README.md ├── del.go ├── compare.go ├── error_test.go ├── add.go ├── control_test.go ├── bind.go ├── passwdmodify.go ├── modify.go ├── error.go ├── dn_test.go ├── ldap_test.go ├── filter_test.go ├── dn.go ├── example_test.go ├── conn_test.go ├── ldap.go ├── conn.go ├── control.go ├── search.go └── filter.go /.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package ldap provides basic LDAP v3 functionality. 3 | */ 4 | package ldap 5 | -------------------------------------------------------------------------------- /.githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # install from the root of the repo with: 4 | # ln -s ../../.githooks/pre-push .git/hooks/pre-push 5 | 6 | make vet fmt lint -------------------------------------------------------------------------------- /atomic_value.go: -------------------------------------------------------------------------------- 1 | // +build go1.4 2 | 3 | package ldap 4 | 5 | import ( 6 | "sync/atomic" 7 | ) 8 | 9 | // For compilers that support it, we just use the underlying sync/atomic.Value 10 | // type. 11 | type atomicValue struct { 12 | atomic.Value 13 | } 14 | -------------------------------------------------------------------------------- /debug.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "log" 5 | 6 | "gopkg.in/asn1-ber.v1" 7 | ) 8 | 9 | // debugging type 10 | // - has a Printf method to write the debug output 11 | type debugging bool 12 | 13 | // write debug output 14 | func (debug debugging) Printf(format string, args ...interface{}) { 15 | if debug { 16 | log.Printf(format, args...) 17 | } 18 | } 19 | 20 | func (debug debugging) PrintPacket(packet *ber.Packet) { 21 | if debug { 22 | ber.PrintPacket(packet) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /atomic_value_go13.go: -------------------------------------------------------------------------------- 1 | // +build !go1.4 2 | 3 | package ldap 4 | 5 | import ( 6 | "sync" 7 | ) 8 | 9 | // This is a helper type that emulates the use of the "sync/atomic.Value" 10 | // struct that's available in Go 1.4 and up. 11 | type atomicValue struct { 12 | value interface{} 13 | lock sync.RWMutex 14 | } 15 | 16 | func (av *atomicValue) Store(val interface{}) { 17 | av.lock.Lock() 18 | av.value = val 19 | av.lock.Unlock() 20 | } 21 | 22 | func (av *atomicValue) Load() interface{} { 23 | av.lock.RLock() 24 | ret := av.value 25 | av.lock.RUnlock() 26 | 27 | return ret 28 | } 29 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "crypto/tls" 5 | "time" 6 | ) 7 | 8 | // Client knows how to interact with an LDAP server 9 | type Client interface { 10 | Start() 11 | StartTLS(config *tls.Config) error 12 | Close() 13 | SetTimeout(time.Duration) 14 | 15 | Bind(username, password string) error 16 | SimpleBind(simpleBindRequest *SimpleBindRequest) (*SimpleBindResult, error) 17 | 18 | Add(addRequest *AddRequest) error 19 | Del(delRequest *DelRequest) error 20 | Modify(modifyRequest *ModifyRequest) error 21 | 22 | Compare(dn, attribute, value string) (bool, error) 23 | PasswordModify(passwordModifyRequest *PasswordModifyRequest) (*PasswordModifyResult, error) 24 | 25 | Search(searchRequest *SearchRequest) (*SearchResult, error) 26 | SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) 27 | } 28 | -------------------------------------------------------------------------------- /search_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | // TestNewEntry tests that repeated calls to NewEntry return the same value with the same input 9 | func TestNewEntry(t *testing.T) { 10 | dn := "testDN" 11 | attributes := map[string][]string{ 12 | "alpha": {"value"}, 13 | "beta": {"value"}, 14 | "gamma": {"value"}, 15 | "delta": {"value"}, 16 | "epsilon": {"value"}, 17 | } 18 | executedEntry := NewEntry(dn, attributes) 19 | 20 | iteration := 0 21 | for { 22 | if iteration == 100 { 23 | break 24 | } 25 | testEntry := NewEntry(dn, attributes) 26 | if !reflect.DeepEqual(executedEntry, testEntry) { 27 | t.Fatalf("subsequent calls to NewEntry did not yield the same result:\n\texpected:\n\t%s\n\tgot:\n\t%s\n", executedEntry, testEntry) 28 | } 29 | iteration = iteration + 1 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | env: 3 | global: 4 | - VET_VERSIONS="1.6 1.7 1.8 1.9 tip" 5 | - LINT_VERSIONS="1.6 1.7 1.8 1.9 tip" 6 | go: 7 | - 1.2 8 | - 1.3 9 | - 1.4 10 | - 1.5 11 | - 1.6 12 | - 1.7 13 | - 1.8 14 | - 1.9 15 | - tip 16 | matrix: 17 | fast_finish: true 18 | allow_failures: 19 | - go: tip 20 | go_import_path: gopkg.in/ldap.v2 21 | install: 22 | - go get gopkg.in/asn1-ber.v1 23 | - go get gopkg.in/ldap.v2 24 | - go get code.google.com/p/go.tools/cmd/cover || go get golang.org/x/tools/cmd/cover 25 | - go get github.com/golang/lint/golint || true 26 | - go build -v ./... 27 | script: 28 | - make test 29 | - make fmt 30 | - if [[ "$VET_VERSIONS" == *"$TRAVIS_GO_VERSION"* ]]; then make vet; fi 31 | - if [[ "$LINT_VERSIONS" == *"$TRAVIS_GO_VERSION"* ]]; then make lint; fi 32 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Basic LDAP v3 functionality for the GO programming language. 2 | 3 | Required Libraries: 4 | github.com/mmitton/asn1-ber 5 | 6 | Working: 7 | Connecting to LDAP server 8 | Binding to LDAP server 9 | Searching for entries 10 | Compiling string filters to LDAP filters 11 | Paging Search Results 12 | Mulitple internal goroutines to handle network traffic 13 | Makes library goroutine safe 14 | Can perform multiple search requests at the same time and return 15 | the results to the proper goroutine. All requests are blocking 16 | requests, so the goroutine does not need special handling 17 | 18 | Tests Implemented: 19 | Filter Compile / Decompile 20 | 21 | TODO: 22 | Modify Requests / Responses 23 | Add Requests / Responses 24 | Delete Requests / Responses 25 | Modify DN Requests / Responses 26 | Compare Requests / Responses 27 | Implement Tests / Benchmarks 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com) 4 | Portions copyright (c) 2015-2016 go-ldap Authors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: default install build test quicktest fmt vet lint 2 | 3 | GO_VERSION := $(shell go version | cut -d' ' -f3 | cut -d. -f2) 4 | 5 | # Only use the `-race` flag on newer versions of Go 6 | IS_OLD_GO := $(shell test $(GO_VERSION) -le 2 && echo true) 7 | ifeq ($(IS_OLD_GO),true) 8 | RACE_FLAG := 9 | else 10 | RACE_FLAG := -race -cpu 1,2,4 11 | endif 12 | 13 | default: fmt vet lint build quicktest 14 | 15 | install: 16 | go get -t -v ./... 17 | 18 | build: 19 | go build -v ./... 20 | 21 | test: 22 | go test -v $(RACE_FLAG) -cover ./... 23 | 24 | quicktest: 25 | go test ./... 26 | 27 | # Capture output and force failure when there is non-empty output 28 | fmt: 29 | @echo gofmt -l . 30 | @OUTPUT=`gofmt -l . 2>&1`; \ 31 | if [ "$$OUTPUT" ]; then \ 32 | echo "gofmt must be run on the following files:"; \ 33 | echo "$$OUTPUT"; \ 34 | exit 1; \ 35 | fi 36 | 37 | # Only run on go1.5+ 38 | vet: 39 | go tool vet -atomic -bool -copylocks -nilfunc -printf -shadow -rangeloops -unreachable -unsafeptr -unusedresult . 40 | 41 | # https://github.com/golang/lint 42 | # go get github.com/golang/lint/golint 43 | # Capture output and force failure when there is non-empty output 44 | # Only run on go1.5+ 45 | lint: 46 | @echo golint ./... 47 | @OUTPUT=`golint ./... 2>&1`; \ 48 | if [ "$$OUTPUT" ]; then \ 49 | echo "golint errors:"; \ 50 | echo "$$OUTPUT"; \ 51 | exit 1; \ 52 | fi 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/gopkg.in/ldap.v2?status.svg)](https://godoc.org/gopkg.in/ldap.v2) 2 | [![Build Status](https://travis-ci.org/go-ldap/ldap.svg)](https://travis-ci.org/go-ldap/ldap) 3 | 4 | # Basic LDAP v3 functionality for the GO programming language. 5 | 6 | ## Install 7 | 8 | For the latest version use: 9 | 10 | go get gopkg.in/ldap.v2 11 | 12 | Import the latest version with: 13 | 14 | import "gopkg.in/ldap.v2" 15 | 16 | ## Required Libraries: 17 | 18 | - gopkg.in/asn1-ber.v1 19 | 20 | ## Features: 21 | 22 | - Connecting to LDAP server (non-TLS, TLS, STARTTLS) 23 | - Binding to LDAP server 24 | - Searching for entries 25 | - Filter Compile / Decompile 26 | - Paging Search Results 27 | - Modify Requests / Responses 28 | - Add Requests / Responses 29 | - Delete Requests / Responses 30 | 31 | ## Examples: 32 | 33 | - search 34 | - modify 35 | 36 | ## Contributing: 37 | 38 | Bug reports and pull requests are welcome! 39 | 40 | Before submitting a pull request, please make sure tests and verification scripts pass: 41 | ``` 42 | make all 43 | ``` 44 | 45 | To set up a pre-push hook to run the tests and verify scripts before pushing: 46 | ``` 47 | ln -s ../../.githooks/pre-push .git/hooks/pre-push 48 | ``` 49 | 50 | --- 51 | The Go gopher was designed by Renee French. (http://reneefrench.blogspot.com/) 52 | The design is licensed under the Creative Commons 3.0 Attributions license. 53 | Read this article for more details: http://blog.golang.org/gopher 54 | -------------------------------------------------------------------------------- /del.go: -------------------------------------------------------------------------------- 1 | // 2 | // https://tools.ietf.org/html/rfc4511 3 | // 4 | // DelRequest ::= [APPLICATION 10] LDAPDN 5 | 6 | package ldap 7 | 8 | import ( 9 | "errors" 10 | "log" 11 | 12 | "gopkg.in/asn1-ber.v1" 13 | ) 14 | 15 | // DelRequest implements an LDAP deletion request 16 | type DelRequest struct { 17 | // DN is the name of the directory entry to delete 18 | DN string 19 | // Controls hold optional controls to send with the request 20 | Controls []Control 21 | } 22 | 23 | func (d DelRequest) encode() *ber.Packet { 24 | request := ber.Encode(ber.ClassApplication, ber.TypePrimitive, ApplicationDelRequest, d.DN, "Del Request") 25 | request.Data.Write([]byte(d.DN)) 26 | return request 27 | } 28 | 29 | // NewDelRequest creates a delete request for the given DN and controls 30 | func NewDelRequest(DN string, 31 | Controls []Control) *DelRequest { 32 | return &DelRequest{ 33 | DN: DN, 34 | Controls: Controls, 35 | } 36 | } 37 | 38 | // Del executes the given delete request 39 | func (l *Conn) Del(delRequest *DelRequest) error { 40 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 41 | packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) 42 | packet.AppendChild(delRequest.encode()) 43 | if delRequest.Controls != nil { 44 | packet.AppendChild(encodeControls(delRequest.Controls)) 45 | } 46 | 47 | l.Debug.PrintPacket(packet) 48 | 49 | msgCtx, err := l.sendMessage(packet) 50 | if err != nil { 51 | return err 52 | } 53 | defer l.finishMessage(msgCtx) 54 | 55 | l.Debug.Printf("%d: waiting for response", msgCtx.id) 56 | packetResponse, ok := <-msgCtx.responses 57 | if !ok { 58 | return NewError(ErrorNetwork, errors.New("ldap: response channel closed")) 59 | } 60 | packet, err = packetResponse.ReadPacket() 61 | l.Debug.Printf("%d: got response %p", msgCtx.id, packet) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | if l.Debug { 67 | if err := addLDAPDescriptions(packet); err != nil { 68 | return err 69 | } 70 | ber.PrintPacket(packet) 71 | } 72 | 73 | if packet.Children[1].Tag == ApplicationDelResponse { 74 | resultCode, resultDescription := getLDAPResultCode(packet) 75 | if resultCode != 0 { 76 | return NewError(resultCode, errors.New(resultDescription)) 77 | } 78 | } else { 79 | log.Printf("Unexpected Response: %d", packet.Children[1].Tag) 80 | } 81 | 82 | l.Debug.Printf("%d: returning", msgCtx.id) 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /compare.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | // 5 | // File contains Compare functionality 6 | // 7 | // https://tools.ietf.org/html/rfc4511 8 | // 9 | // CompareRequest ::= [APPLICATION 14] SEQUENCE { 10 | // entry LDAPDN, 11 | // ava AttributeValueAssertion } 12 | // 13 | // AttributeValueAssertion ::= SEQUENCE { 14 | // attributeDesc AttributeDescription, 15 | // assertionValue AssertionValue } 16 | // 17 | // AttributeDescription ::= LDAPString 18 | // -- Constrained to 19 | // -- [RFC4512] 20 | // 21 | // AttributeValue ::= OCTET STRING 22 | // 23 | 24 | package ldap 25 | 26 | import ( 27 | "errors" 28 | "fmt" 29 | 30 | "gopkg.in/asn1-ber.v1" 31 | ) 32 | 33 | // Compare checks to see if the attribute of the dn matches value. Returns true if it does otherwise 34 | // false with any error that occurs if any. 35 | func (l *Conn) Compare(dn, attribute, value string) (bool, error) { 36 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 37 | packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) 38 | 39 | request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationCompareRequest, nil, "Compare Request") 40 | request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, dn, "DN")) 41 | 42 | ava := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "AttributeValueAssertion") 43 | ava.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "AttributeDesc")) 44 | ava.AppendChild(ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagOctetString, value, "AssertionValue")) 45 | request.AppendChild(ava) 46 | packet.AppendChild(request) 47 | 48 | l.Debug.PrintPacket(packet) 49 | 50 | msgCtx, err := l.sendMessage(packet) 51 | if err != nil { 52 | return false, err 53 | } 54 | defer l.finishMessage(msgCtx) 55 | 56 | l.Debug.Printf("%d: waiting for response", msgCtx.id) 57 | packetResponse, ok := <-msgCtx.responses 58 | if !ok { 59 | return false, NewError(ErrorNetwork, errors.New("ldap: response channel closed")) 60 | } 61 | packet, err = packetResponse.ReadPacket() 62 | l.Debug.Printf("%d: got response %p", msgCtx.id, packet) 63 | if err != nil { 64 | return false, err 65 | } 66 | 67 | if l.Debug { 68 | if err := addLDAPDescriptions(packet); err != nil { 69 | return false, err 70 | } 71 | ber.PrintPacket(packet) 72 | } 73 | 74 | if packet.Children[1].Tag == ApplicationCompareResponse { 75 | resultCode, resultDescription := getLDAPResultCode(packet) 76 | if resultCode == LDAPResultCompareTrue { 77 | return true, nil 78 | } else if resultCode == LDAPResultCompareFalse { 79 | return false, nil 80 | } else { 81 | return false, NewError(resultCode, errors.New(resultDescription)) 82 | } 83 | } 84 | return false, fmt.Errorf("Unexpected Response: %d", packet.Children[1].Tag) 85 | } 86 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "gopkg.in/asn1-ber.v1" 11 | ) 12 | 13 | // TestNilPacket tests that nil packets don't cause a panic. 14 | func TestNilPacket(t *testing.T) { 15 | // Test for nil packet 16 | code, _ := getLDAPResultCode(nil) 17 | if code != ErrorUnexpectedResponse { 18 | t.Errorf("Should have an 'ErrorUnexpectedResponse' error in nil packets, got: %v", code) 19 | } 20 | 21 | // Test for nil result 22 | kids := []*ber.Packet{ 23 | {}, // Unused 24 | nil, // Can't be nil 25 | } 26 | pack := &ber.Packet{Children: kids} 27 | code, _ = getLDAPResultCode(pack) 28 | 29 | if code != ErrorUnexpectedResponse { 30 | t.Errorf("Should have an 'ErrorUnexpectedResponse' error in nil packets, got: %v", code) 31 | } 32 | } 33 | 34 | // TestConnReadErr tests that an unexpected error reading from underlying 35 | // connection bubbles up to the goroutine which makes a request. 36 | func TestConnReadErr(t *testing.T) { 37 | conn := &signalErrConn{ 38 | signals: make(chan error), 39 | } 40 | 41 | ldapConn := NewConn(conn, false) 42 | ldapConn.Start() 43 | 44 | // Make a dummy search request. 45 | searchReq := NewSearchRequest("dc=example,dc=com", ScopeWholeSubtree, DerefAlways, 0, 0, false, "(objectClass=*)", nil, nil) 46 | 47 | expectedError := errors.New("this is the error you are looking for") 48 | 49 | // Send the signal after a short amount of time. 50 | time.AfterFunc(10*time.Millisecond, func() { conn.signals <- expectedError }) 51 | 52 | // This should block until the underlying conn gets the error signal 53 | // which should bubble up through the reader() goroutine, close the 54 | // connection, and 55 | _, err := ldapConn.Search(searchReq) 56 | if err == nil || !strings.Contains(err.Error(), expectedError.Error()) { 57 | t.Errorf("not the expected error: %s", err) 58 | } 59 | } 60 | 61 | // signalErrConn is a helpful type used with TestConnReadErr. It implements the 62 | // net.Conn interface to be used as a connection for the test. Most methods are 63 | // no-ops but the Read() method blocks until it receives a signal which it 64 | // returns as an error. 65 | type signalErrConn struct { 66 | signals chan error 67 | } 68 | 69 | // Read blocks until an error is sent on the internal signals channel. That 70 | // error is returned. 71 | func (c *signalErrConn) Read(b []byte) (n int, err error) { 72 | return 0, <-c.signals 73 | } 74 | 75 | func (c *signalErrConn) Write(b []byte) (n int, err error) { 76 | return len(b), nil 77 | } 78 | 79 | func (c *signalErrConn) Close() error { 80 | close(c.signals) 81 | return nil 82 | } 83 | 84 | func (c *signalErrConn) LocalAddr() net.Addr { 85 | return (*net.TCPAddr)(nil) 86 | } 87 | 88 | func (c *signalErrConn) RemoteAddr() net.Addr { 89 | return (*net.TCPAddr)(nil) 90 | } 91 | 92 | func (c *signalErrConn) SetDeadline(t time.Time) error { 93 | return nil 94 | } 95 | 96 | func (c *signalErrConn) SetReadDeadline(t time.Time) error { 97 | return nil 98 | } 99 | 100 | func (c *signalErrConn) SetWriteDeadline(t time.Time) error { 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /add.go: -------------------------------------------------------------------------------- 1 | // 2 | // https://tools.ietf.org/html/rfc4511 3 | // 4 | // AddRequest ::= [APPLICATION 8] SEQUENCE { 5 | // entry LDAPDN, 6 | // attributes AttributeList } 7 | // 8 | // AttributeList ::= SEQUENCE OF attribute Attribute 9 | 10 | package ldap 11 | 12 | import ( 13 | "errors" 14 | "log" 15 | 16 | "gopkg.in/asn1-ber.v1" 17 | ) 18 | 19 | // Attribute represents an LDAP attribute 20 | type Attribute struct { 21 | // Type is the name of the LDAP attribute 22 | Type string 23 | // Vals are the LDAP attribute values 24 | Vals []string 25 | } 26 | 27 | func (a *Attribute) encode() *ber.Packet { 28 | seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attribute") 29 | seq.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, a.Type, "Type")) 30 | set := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "AttributeValue") 31 | for _, value := range a.Vals { 32 | set.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Vals")) 33 | } 34 | seq.AppendChild(set) 35 | return seq 36 | } 37 | 38 | // AddRequest represents an LDAP AddRequest operation 39 | type AddRequest struct { 40 | // DN identifies the entry being added 41 | DN string 42 | // Attributes list the attributes of the new entry 43 | Attributes []Attribute 44 | } 45 | 46 | func (a AddRequest) encode() *ber.Packet { 47 | request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationAddRequest, nil, "Add Request") 48 | request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, a.DN, "DN")) 49 | attributes := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes") 50 | for _, attribute := range a.Attributes { 51 | attributes.AppendChild(attribute.encode()) 52 | } 53 | request.AppendChild(attributes) 54 | return request 55 | } 56 | 57 | // Attribute adds an attribute with the given type and values 58 | func (a *AddRequest) Attribute(attrType string, attrVals []string) { 59 | a.Attributes = append(a.Attributes, Attribute{Type: attrType, Vals: attrVals}) 60 | } 61 | 62 | // NewAddRequest returns an AddRequest for the given DN, with no attributes 63 | func NewAddRequest(dn string) *AddRequest { 64 | return &AddRequest{ 65 | DN: dn, 66 | } 67 | 68 | } 69 | 70 | // Add performs the given AddRequest 71 | func (l *Conn) Add(addRequest *AddRequest) error { 72 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 73 | packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) 74 | packet.AppendChild(addRequest.encode()) 75 | 76 | l.Debug.PrintPacket(packet) 77 | 78 | msgCtx, err := l.sendMessage(packet) 79 | if err != nil { 80 | return err 81 | } 82 | defer l.finishMessage(msgCtx) 83 | 84 | l.Debug.Printf("%d: waiting for response", msgCtx.id) 85 | packetResponse, ok := <-msgCtx.responses 86 | if !ok { 87 | return NewError(ErrorNetwork, errors.New("ldap: response channel closed")) 88 | } 89 | packet, err = packetResponse.ReadPacket() 90 | l.Debug.Printf("%d: got response %p", msgCtx.id, packet) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | if l.Debug { 96 | if err := addLDAPDescriptions(packet); err != nil { 97 | return err 98 | } 99 | ber.PrintPacket(packet) 100 | } 101 | 102 | if packet.Children[1].Tag == ApplicationAddResponse { 103 | resultCode, resultDescription := getLDAPResultCode(packet) 104 | if resultCode != 0 { 105 | return NewError(resultCode, errors.New(resultDescription)) 106 | } 107 | } else { 108 | log.Printf("Unexpected Response: %d", packet.Children[1].Tag) 109 | } 110 | 111 | l.Debug.Printf("%d: returning", msgCtx.id) 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /control_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "reflect" 7 | "runtime" 8 | "testing" 9 | 10 | "gopkg.in/asn1-ber.v1" 11 | ) 12 | 13 | func TestControlPaging(t *testing.T) { 14 | runControlTest(t, NewControlPaging(0)) 15 | runControlTest(t, NewControlPaging(100)) 16 | } 17 | 18 | func TestControlManageDsaIT(t *testing.T) { 19 | runControlTest(t, NewControlManageDsaIT(true)) 20 | runControlTest(t, NewControlManageDsaIT(false)) 21 | } 22 | 23 | func TestControlString(t *testing.T) { 24 | runControlTest(t, NewControlString("x", true, "y")) 25 | runControlTest(t, NewControlString("x", true, "")) 26 | runControlTest(t, NewControlString("x", false, "y")) 27 | runControlTest(t, NewControlString("x", false, "")) 28 | } 29 | 30 | func runControlTest(t *testing.T, originalControl Control) { 31 | header := "" 32 | if callerpc, _, line, ok := runtime.Caller(1); ok { 33 | if caller := runtime.FuncForPC(callerpc); caller != nil { 34 | header = fmt.Sprintf("%s:%d: ", caller.Name(), line) 35 | } 36 | } 37 | 38 | encodedPacket := originalControl.Encode() 39 | encodedBytes := encodedPacket.Bytes() 40 | 41 | // Decode directly from the encoded packet (ensures Value is correct) 42 | fromPacket := DecodeControl(encodedPacket) 43 | if !bytes.Equal(encodedBytes, fromPacket.Encode().Bytes()) { 44 | t.Errorf("%sround-trip from encoded packet failed", header) 45 | } 46 | if reflect.TypeOf(originalControl) != reflect.TypeOf(fromPacket) { 47 | t.Errorf("%sgot different type decoding from encoded packet: %T vs %T", header, fromPacket, originalControl) 48 | } 49 | 50 | // Decode from the wire bytes (ensures ber-encoding is correct) 51 | fromBytes := DecodeControl(ber.DecodePacket(encodedBytes)) 52 | if !bytes.Equal(encodedBytes, fromBytes.Encode().Bytes()) { 53 | t.Errorf("%sround-trip from encoded bytes failed", header) 54 | } 55 | if reflect.TypeOf(originalControl) != reflect.TypeOf(fromPacket) { 56 | t.Errorf("%sgot different type decoding from encoded bytes: %T vs %T", header, fromBytes, originalControl) 57 | } 58 | } 59 | 60 | func TestDescribeControlManageDsaIT(t *testing.T) { 61 | runAddControlDescriptions(t, NewControlManageDsaIT(false), "Control Type (Manage DSA IT)") 62 | runAddControlDescriptions(t, NewControlManageDsaIT(true), "Control Type (Manage DSA IT)", "Criticality") 63 | } 64 | 65 | func TestDescribeControlPaging(t *testing.T) { 66 | runAddControlDescriptions(t, NewControlPaging(100), "Control Type (Paging)", "Control Value (Paging)") 67 | runAddControlDescriptions(t, NewControlPaging(0), "Control Type (Paging)", "Control Value (Paging)") 68 | } 69 | 70 | func TestDescribeControlString(t *testing.T) { 71 | runAddControlDescriptions(t, NewControlString("x", true, "y"), "Control Type ()", "Criticality", "Control Value") 72 | runAddControlDescriptions(t, NewControlString("x", true, ""), "Control Type ()", "Criticality", "Control Value") 73 | runAddControlDescriptions(t, NewControlString("x", false, "y"), "Control Type ()", "Control Value") 74 | runAddControlDescriptions(t, NewControlString("x", false, ""), "Control Type ()", "Control Value") 75 | } 76 | 77 | func runAddControlDescriptions(t *testing.T, originalControl Control, childDescriptions ...string) { 78 | header := "" 79 | if callerpc, _, line, ok := runtime.Caller(1); ok { 80 | if caller := runtime.FuncForPC(callerpc); caller != nil { 81 | header = fmt.Sprintf("%s:%d: ", caller.Name(), line) 82 | } 83 | } 84 | 85 | encodedControls := encodeControls([]Control{originalControl}) 86 | addControlDescriptions(encodedControls) 87 | encodedPacket := encodedControls.Children[0] 88 | if len(encodedPacket.Children) != len(childDescriptions) { 89 | t.Errorf("%sinvalid number of children: %d != %d", header, len(encodedPacket.Children), len(childDescriptions)) 90 | } 91 | for i, desc := range childDescriptions { 92 | if encodedPacket.Children[i].Description != desc { 93 | t.Errorf("%sdescription not as expected: %s != %s", header, encodedPacket.Children[i].Description, desc) 94 | } 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /bind.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "errors" 5 | 6 | "gopkg.in/asn1-ber.v1" 7 | ) 8 | 9 | // SimpleBindRequest represents a username/password bind operation 10 | type SimpleBindRequest struct { 11 | // Username is the name of the Directory object that the client wishes to bind as 12 | Username string 13 | // Password is the credentials to bind with 14 | Password string 15 | // Controls are optional controls to send with the bind request 16 | Controls []Control 17 | // AllowEmptyPassword sets whether the client allows binding with an empty password 18 | // (normally used for unauthenticated bind). 19 | AllowEmptyPassword bool 20 | } 21 | 22 | // SimpleBindResult contains the response from the server 23 | type SimpleBindResult struct { 24 | Controls []Control 25 | } 26 | 27 | // NewSimpleBindRequest returns a bind request 28 | func NewSimpleBindRequest(username string, password string, controls []Control) *SimpleBindRequest { 29 | return &SimpleBindRequest{ 30 | Username: username, 31 | Password: password, 32 | Controls: controls, 33 | AllowEmptyPassword: false, 34 | } 35 | } 36 | 37 | func (bindRequest *SimpleBindRequest) encode() *ber.Packet { 38 | request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") 39 | request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) 40 | request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, bindRequest.Username, "User Name")) 41 | request.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, bindRequest.Password, "Password")) 42 | 43 | request.AppendChild(encodeControls(bindRequest.Controls)) 44 | 45 | return request 46 | } 47 | 48 | // SimpleBind performs the simple bind operation defined in the given request 49 | func (l *Conn) SimpleBind(simpleBindRequest *SimpleBindRequest) (*SimpleBindResult, error) { 50 | if simpleBindRequest.Password == "" && !simpleBindRequest.AllowEmptyPassword { 51 | return nil, NewError(ErrorEmptyPassword, errors.New("ldap: empty password not allowed by the client")) 52 | } 53 | 54 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 55 | packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) 56 | encodedBindRequest := simpleBindRequest.encode() 57 | packet.AppendChild(encodedBindRequest) 58 | 59 | if l.Debug { 60 | ber.PrintPacket(packet) 61 | } 62 | 63 | msgCtx, err := l.sendMessage(packet) 64 | if err != nil { 65 | return nil, err 66 | } 67 | defer l.finishMessage(msgCtx) 68 | 69 | packetResponse, ok := <-msgCtx.responses 70 | if !ok { 71 | return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed")) 72 | } 73 | packet, err = packetResponse.ReadPacket() 74 | l.Debug.Printf("%d: got response %p", msgCtx.id, packet) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | if l.Debug { 80 | if err := addLDAPDescriptions(packet); err != nil { 81 | return nil, err 82 | } 83 | ber.PrintPacket(packet) 84 | } 85 | 86 | result := &SimpleBindResult{ 87 | Controls: make([]Control, 0), 88 | } 89 | 90 | if len(packet.Children) == 3 { 91 | for _, child := range packet.Children[2].Children { 92 | result.Controls = append(result.Controls, DecodeControl(child)) 93 | } 94 | } 95 | 96 | resultCode, resultDescription := getLDAPResultCode(packet) 97 | if resultCode != 0 { 98 | return result, NewError(resultCode, errors.New(resultDescription)) 99 | } 100 | 101 | return result, nil 102 | } 103 | 104 | // Bind performs a bind with the given username and password. 105 | // 106 | // It does not allow unauthenticated bind (i.e. empty password). Use the UnauthenticatedBind method 107 | // for that. 108 | func (l *Conn) Bind(username, password string) error { 109 | req := &SimpleBindRequest{ 110 | Username: username, 111 | Password: password, 112 | AllowEmptyPassword: false, 113 | } 114 | _, err := l.SimpleBind(req) 115 | return err 116 | } 117 | 118 | // UnauthenticatedBind performs an unauthenticated bind. 119 | // 120 | // A username may be provided for trace (e.g. logging) purpose only, but it is normally not 121 | // authenticated or otherwise validated by the LDAP server. 122 | // 123 | // See https://tools.ietf.org/html/rfc4513#section-5.1.2 . 124 | // See https://tools.ietf.org/html/rfc4513#section-6.3.1 . 125 | func (l *Conn) UnauthenticatedBind(username string) error { 126 | req := &SimpleBindRequest{ 127 | Username: username, 128 | Password: "", 129 | AllowEmptyPassword: true, 130 | } 131 | _, err := l.SimpleBind(req) 132 | return err 133 | } 134 | -------------------------------------------------------------------------------- /passwdmodify.go: -------------------------------------------------------------------------------- 1 | // This file contains the password modify extended operation as specified in rfc 3062 2 | // 3 | // https://tools.ietf.org/html/rfc3062 4 | // 5 | 6 | package ldap 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | 12 | "gopkg.in/asn1-ber.v1" 13 | ) 14 | 15 | const ( 16 | passwordModifyOID = "1.3.6.1.4.1.4203.1.11.1" 17 | ) 18 | 19 | // PasswordModifyRequest implements the Password Modify Extended Operation as defined in https://www.ietf.org/rfc/rfc3062.txt 20 | type PasswordModifyRequest struct { 21 | // UserIdentity is an optional string representation of the user associated with the request. 22 | // This string may or may not be an LDAPDN [RFC2253]. 23 | // If no UserIdentity field is present, the request acts up upon the password of the user currently associated with the LDAP session 24 | UserIdentity string 25 | // OldPassword, if present, contains the user's current password 26 | OldPassword string 27 | // NewPassword, if present, contains the desired password for this user 28 | NewPassword string 29 | } 30 | 31 | // PasswordModifyResult holds the server response to a PasswordModifyRequest 32 | type PasswordModifyResult struct { 33 | // GeneratedPassword holds a password generated by the server, if present 34 | GeneratedPassword string 35 | } 36 | 37 | func (r *PasswordModifyRequest) encode() (*ber.Packet, error) { 38 | request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Password Modify Extended Operation") 39 | request.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, passwordModifyOID, "Extended Request Name: Password Modify OID")) 40 | extendedRequestValue := ber.Encode(ber.ClassContext, ber.TypePrimitive, 1, nil, "Extended Request Value: Password Modify Request") 41 | passwordModifyRequestValue := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Password Modify Request") 42 | if r.UserIdentity != "" { 43 | passwordModifyRequestValue.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, r.UserIdentity, "User Identity")) 44 | } 45 | if r.OldPassword != "" { 46 | passwordModifyRequestValue.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 1, r.OldPassword, "Old Password")) 47 | } 48 | if r.NewPassword != "" { 49 | passwordModifyRequestValue.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 2, r.NewPassword, "New Password")) 50 | } 51 | 52 | extendedRequestValue.AppendChild(passwordModifyRequestValue) 53 | request.AppendChild(extendedRequestValue) 54 | 55 | return request, nil 56 | } 57 | 58 | // NewPasswordModifyRequest creates a new PasswordModifyRequest 59 | // 60 | // According to the RFC 3602: 61 | // userIdentity is a string representing the user associated with the request. 62 | // This string may or may not be an LDAPDN (RFC 2253). 63 | // If userIdentity is empty then the operation will act on the user associated 64 | // with the session. 65 | // 66 | // oldPassword is the current user's password, it can be empty or it can be 67 | // needed depending on the session user access rights (usually an administrator 68 | // can change a user's password without knowing the current one) and the 69 | // password policy (see pwdSafeModify password policy's attribute) 70 | // 71 | // newPassword is the desired user's password. If empty the server can return 72 | // an error or generate a new password that will be available in the 73 | // PasswordModifyResult.GeneratedPassword 74 | // 75 | func NewPasswordModifyRequest(userIdentity string, oldPassword string, newPassword string) *PasswordModifyRequest { 76 | return &PasswordModifyRequest{ 77 | UserIdentity: userIdentity, 78 | OldPassword: oldPassword, 79 | NewPassword: newPassword, 80 | } 81 | } 82 | 83 | // PasswordModify performs the modification request 84 | func (l *Conn) PasswordModify(passwordModifyRequest *PasswordModifyRequest) (*PasswordModifyResult, error) { 85 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 86 | packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) 87 | 88 | encodedPasswordModifyRequest, err := passwordModifyRequest.encode() 89 | if err != nil { 90 | return nil, err 91 | } 92 | packet.AppendChild(encodedPasswordModifyRequest) 93 | 94 | l.Debug.PrintPacket(packet) 95 | 96 | msgCtx, err := l.sendMessage(packet) 97 | if err != nil { 98 | return nil, err 99 | } 100 | defer l.finishMessage(msgCtx) 101 | 102 | result := &PasswordModifyResult{} 103 | 104 | l.Debug.Printf("%d: waiting for response", msgCtx.id) 105 | packetResponse, ok := <-msgCtx.responses 106 | if !ok { 107 | return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed")) 108 | } 109 | packet, err = packetResponse.ReadPacket() 110 | l.Debug.Printf("%d: got response %p", msgCtx.id, packet) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | if packet == nil { 116 | return nil, NewError(ErrorNetwork, errors.New("ldap: could not retrieve message")) 117 | } 118 | 119 | if l.Debug { 120 | if err := addLDAPDescriptions(packet); err != nil { 121 | return nil, err 122 | } 123 | ber.PrintPacket(packet) 124 | } 125 | 126 | if packet.Children[1].Tag == ApplicationExtendedResponse { 127 | resultCode, resultDescription := getLDAPResultCode(packet) 128 | if resultCode != 0 { 129 | return nil, NewError(resultCode, errors.New(resultDescription)) 130 | } 131 | } else { 132 | return nil, NewError(ErrorUnexpectedResponse, fmt.Errorf("Unexpected Response: %d", packet.Children[1].Tag)) 133 | } 134 | 135 | extendedResponse := packet.Children[1] 136 | for _, child := range extendedResponse.Children { 137 | if child.Tag == 11 { 138 | passwordModifyResponseValue := ber.DecodePacket(child.Data.Bytes()) 139 | if len(passwordModifyResponseValue.Children) == 1 { 140 | if passwordModifyResponseValue.Children[0].Tag == 0 { 141 | result.GeneratedPassword = ber.DecodeString(passwordModifyResponseValue.Children[0].Data.Bytes()) 142 | } 143 | } 144 | } 145 | } 146 | 147 | return result, nil 148 | } 149 | -------------------------------------------------------------------------------- /modify.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | // 5 | // File contains Modify functionality 6 | // 7 | // https://tools.ietf.org/html/rfc4511 8 | // 9 | // ModifyRequest ::= [APPLICATION 6] SEQUENCE { 10 | // object LDAPDN, 11 | // changes SEQUENCE OF change SEQUENCE { 12 | // operation ENUMERATED { 13 | // add (0), 14 | // delete (1), 15 | // replace (2), 16 | // ... }, 17 | // modification PartialAttribute } } 18 | // 19 | // PartialAttribute ::= SEQUENCE { 20 | // type AttributeDescription, 21 | // vals SET OF value AttributeValue } 22 | // 23 | // AttributeDescription ::= LDAPString 24 | // -- Constrained to 25 | // -- [RFC4512] 26 | // 27 | // AttributeValue ::= OCTET STRING 28 | // 29 | 30 | package ldap 31 | 32 | import ( 33 | "errors" 34 | "log" 35 | 36 | "gopkg.in/asn1-ber.v1" 37 | ) 38 | 39 | // Change operation choices 40 | const ( 41 | AddAttribute = 0 42 | DeleteAttribute = 1 43 | ReplaceAttribute = 2 44 | ) 45 | 46 | // PartialAttribute for a ModifyRequest as defined in https://tools.ietf.org/html/rfc4511 47 | type PartialAttribute struct { 48 | // Type is the type of the partial attribute 49 | Type string 50 | // Vals are the values of the partial attribute 51 | Vals []string 52 | } 53 | 54 | func (p *PartialAttribute) encode() *ber.Packet { 55 | seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "PartialAttribute") 56 | seq.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, p.Type, "Type")) 57 | set := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "AttributeValue") 58 | for _, value := range p.Vals { 59 | set.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Vals")) 60 | } 61 | seq.AppendChild(set) 62 | return seq 63 | } 64 | 65 | // ModifyRequest as defined in https://tools.ietf.org/html/rfc4511 66 | type ModifyRequest struct { 67 | // DN is the distinguishedName of the directory entry to modify 68 | DN string 69 | // AddAttributes contain the attributes to add 70 | AddAttributes []PartialAttribute 71 | // DeleteAttributes contain the attributes to delete 72 | DeleteAttributes []PartialAttribute 73 | // ReplaceAttributes contain the attributes to replace 74 | ReplaceAttributes []PartialAttribute 75 | } 76 | 77 | // Add inserts the given attribute to the list of attributes to add 78 | func (m *ModifyRequest) Add(attrType string, attrVals []string) { 79 | m.AddAttributes = append(m.AddAttributes, PartialAttribute{Type: attrType, Vals: attrVals}) 80 | } 81 | 82 | // Delete inserts the given attribute to the list of attributes to delete 83 | func (m *ModifyRequest) Delete(attrType string, attrVals []string) { 84 | m.DeleteAttributes = append(m.DeleteAttributes, PartialAttribute{Type: attrType, Vals: attrVals}) 85 | } 86 | 87 | // Replace inserts the given attribute to the list of attributes to replace 88 | func (m *ModifyRequest) Replace(attrType string, attrVals []string) { 89 | m.ReplaceAttributes = append(m.ReplaceAttributes, PartialAttribute{Type: attrType, Vals: attrVals}) 90 | } 91 | 92 | func (m ModifyRequest) encode() *ber.Packet { 93 | request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationModifyRequest, nil, "Modify Request") 94 | request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, m.DN, "DN")) 95 | changes := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Changes") 96 | for _, attribute := range m.AddAttributes { 97 | change := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Change") 98 | change.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(AddAttribute), "Operation")) 99 | change.AppendChild(attribute.encode()) 100 | changes.AppendChild(change) 101 | } 102 | for _, attribute := range m.DeleteAttributes { 103 | change := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Change") 104 | change.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(DeleteAttribute), "Operation")) 105 | change.AppendChild(attribute.encode()) 106 | changes.AppendChild(change) 107 | } 108 | for _, attribute := range m.ReplaceAttributes { 109 | change := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Change") 110 | change.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(ReplaceAttribute), "Operation")) 111 | change.AppendChild(attribute.encode()) 112 | changes.AppendChild(change) 113 | } 114 | request.AppendChild(changes) 115 | return request 116 | } 117 | 118 | // NewModifyRequest creates a modify request for the given DN 119 | func NewModifyRequest( 120 | dn string, 121 | ) *ModifyRequest { 122 | return &ModifyRequest{ 123 | DN: dn, 124 | } 125 | } 126 | 127 | // Modify performs the ModifyRequest 128 | func (l *Conn) Modify(modifyRequest *ModifyRequest) error { 129 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 130 | packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) 131 | packet.AppendChild(modifyRequest.encode()) 132 | 133 | l.Debug.PrintPacket(packet) 134 | 135 | msgCtx, err := l.sendMessage(packet) 136 | if err != nil { 137 | return err 138 | } 139 | defer l.finishMessage(msgCtx) 140 | 141 | l.Debug.Printf("%d: waiting for response", msgCtx.id) 142 | packetResponse, ok := <-msgCtx.responses 143 | if !ok { 144 | return NewError(ErrorNetwork, errors.New("ldap: response channel closed")) 145 | } 146 | packet, err = packetResponse.ReadPacket() 147 | l.Debug.Printf("%d: got response %p", msgCtx.id, packet) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | if l.Debug { 153 | if err := addLDAPDescriptions(packet); err != nil { 154 | return err 155 | } 156 | ber.PrintPacket(packet) 157 | } 158 | 159 | if packet.Children[1].Tag == ApplicationModifyResponse { 160 | resultCode, resultDescription := getLDAPResultCode(packet) 161 | if resultCode != 0 { 162 | return NewError(resultCode, errors.New(resultDescription)) 163 | } 164 | } else { 165 | log.Printf("Unexpected Response: %d", packet.Children[1].Tag) 166 | } 167 | 168 | l.Debug.Printf("%d: returning", msgCtx.id) 169 | return nil 170 | } 171 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gopkg.in/asn1-ber.v1" 7 | ) 8 | 9 | // LDAP Result Codes 10 | const ( 11 | LDAPResultSuccess = 0 12 | LDAPResultOperationsError = 1 13 | LDAPResultProtocolError = 2 14 | LDAPResultTimeLimitExceeded = 3 15 | LDAPResultSizeLimitExceeded = 4 16 | LDAPResultCompareFalse = 5 17 | LDAPResultCompareTrue = 6 18 | LDAPResultAuthMethodNotSupported = 7 19 | LDAPResultStrongAuthRequired = 8 20 | LDAPResultReferral = 10 21 | LDAPResultAdminLimitExceeded = 11 22 | LDAPResultUnavailableCriticalExtension = 12 23 | LDAPResultConfidentialityRequired = 13 24 | LDAPResultSaslBindInProgress = 14 25 | LDAPResultNoSuchAttribute = 16 26 | LDAPResultUndefinedAttributeType = 17 27 | LDAPResultInappropriateMatching = 18 28 | LDAPResultConstraintViolation = 19 29 | LDAPResultAttributeOrValueExists = 20 30 | LDAPResultInvalidAttributeSyntax = 21 31 | LDAPResultNoSuchObject = 32 32 | LDAPResultAliasProblem = 33 33 | LDAPResultInvalidDNSyntax = 34 34 | LDAPResultAliasDereferencingProblem = 36 35 | LDAPResultInappropriateAuthentication = 48 36 | LDAPResultInvalidCredentials = 49 37 | LDAPResultInsufficientAccessRights = 50 38 | LDAPResultBusy = 51 39 | LDAPResultUnavailable = 52 40 | LDAPResultUnwillingToPerform = 53 41 | LDAPResultLoopDetect = 54 42 | LDAPResultNamingViolation = 64 43 | LDAPResultObjectClassViolation = 65 44 | LDAPResultNotAllowedOnNonLeaf = 66 45 | LDAPResultNotAllowedOnRDN = 67 46 | LDAPResultEntryAlreadyExists = 68 47 | LDAPResultObjectClassModsProhibited = 69 48 | LDAPResultAffectsMultipleDSAs = 71 49 | LDAPResultOther = 80 50 | 51 | ErrorNetwork = 200 52 | ErrorFilterCompile = 201 53 | ErrorFilterDecompile = 202 54 | ErrorDebugging = 203 55 | ErrorUnexpectedMessage = 204 56 | ErrorUnexpectedResponse = 205 57 | ErrorEmptyPassword = 206 58 | ) 59 | 60 | // LDAPResultCodeMap contains string descriptions for LDAP error codes 61 | var LDAPResultCodeMap = map[uint8]string{ 62 | LDAPResultSuccess: "Success", 63 | LDAPResultOperationsError: "Operations Error", 64 | LDAPResultProtocolError: "Protocol Error", 65 | LDAPResultTimeLimitExceeded: "Time Limit Exceeded", 66 | LDAPResultSizeLimitExceeded: "Size Limit Exceeded", 67 | LDAPResultCompareFalse: "Compare False", 68 | LDAPResultCompareTrue: "Compare True", 69 | LDAPResultAuthMethodNotSupported: "Auth Method Not Supported", 70 | LDAPResultStrongAuthRequired: "Strong Auth Required", 71 | LDAPResultReferral: "Referral", 72 | LDAPResultAdminLimitExceeded: "Admin Limit Exceeded", 73 | LDAPResultUnavailableCriticalExtension: "Unavailable Critical Extension", 74 | LDAPResultConfidentialityRequired: "Confidentiality Required", 75 | LDAPResultSaslBindInProgress: "Sasl Bind In Progress", 76 | LDAPResultNoSuchAttribute: "No Such Attribute", 77 | LDAPResultUndefinedAttributeType: "Undefined Attribute Type", 78 | LDAPResultInappropriateMatching: "Inappropriate Matching", 79 | LDAPResultConstraintViolation: "Constraint Violation", 80 | LDAPResultAttributeOrValueExists: "Attribute Or Value Exists", 81 | LDAPResultInvalidAttributeSyntax: "Invalid Attribute Syntax", 82 | LDAPResultNoSuchObject: "No Such Object", 83 | LDAPResultAliasProblem: "Alias Problem", 84 | LDAPResultInvalidDNSyntax: "Invalid DN Syntax", 85 | LDAPResultAliasDereferencingProblem: "Alias Dereferencing Problem", 86 | LDAPResultInappropriateAuthentication: "Inappropriate Authentication", 87 | LDAPResultInvalidCredentials: "Invalid Credentials", 88 | LDAPResultInsufficientAccessRights: "Insufficient Access Rights", 89 | LDAPResultBusy: "Busy", 90 | LDAPResultUnavailable: "Unavailable", 91 | LDAPResultUnwillingToPerform: "Unwilling To Perform", 92 | LDAPResultLoopDetect: "Loop Detect", 93 | LDAPResultNamingViolation: "Naming Violation", 94 | LDAPResultObjectClassViolation: "Object Class Violation", 95 | LDAPResultNotAllowedOnNonLeaf: "Not Allowed On Non Leaf", 96 | LDAPResultNotAllowedOnRDN: "Not Allowed On RDN", 97 | LDAPResultEntryAlreadyExists: "Entry Already Exists", 98 | LDAPResultObjectClassModsProhibited: "Object Class Mods Prohibited", 99 | LDAPResultAffectsMultipleDSAs: "Affects Multiple DSAs", 100 | LDAPResultOther: "Other", 101 | 102 | ErrorNetwork: "Network Error", 103 | ErrorFilterCompile: "Filter Compile Error", 104 | ErrorFilterDecompile: "Filter Decompile Error", 105 | ErrorDebugging: "Debugging Error", 106 | ErrorUnexpectedMessage: "Unexpected Message", 107 | ErrorUnexpectedResponse: "Unexpected Response", 108 | ErrorEmptyPassword: "Empty password not allowed by the client", 109 | } 110 | 111 | func getLDAPResultCode(packet *ber.Packet) (code uint8, description string) { 112 | if packet == nil { 113 | return ErrorUnexpectedResponse, "Empty packet" 114 | } else if len(packet.Children) >= 2 { 115 | response := packet.Children[1] 116 | if response == nil { 117 | return ErrorUnexpectedResponse, "Empty response in packet" 118 | } 119 | if response.ClassType == ber.ClassApplication && response.TagType == ber.TypeConstructed && len(response.Children) >= 3 { 120 | // Children[1].Children[2] is the diagnosticMessage which is guaranteed to exist as seen here: https://tools.ietf.org/html/rfc4511#section-4.1.9 121 | return uint8(response.Children[0].Value.(int64)), response.Children[2].Value.(string) 122 | } 123 | } 124 | 125 | return ErrorNetwork, "Invalid packet format" 126 | } 127 | 128 | // Error holds LDAP error information 129 | type Error struct { 130 | // Err is the underlying error 131 | Err error 132 | // ResultCode is the LDAP error code 133 | ResultCode uint8 134 | } 135 | 136 | func (e *Error) Error() string { 137 | return fmt.Sprintf("LDAP Result Code %d %q: %s", e.ResultCode, LDAPResultCodeMap[e.ResultCode], e.Err.Error()) 138 | } 139 | 140 | // NewError creates an LDAP error with the given code and underlying error 141 | func NewError(resultCode uint8, err error) error { 142 | return &Error{ResultCode: resultCode, Err: err} 143 | } 144 | 145 | // IsErrorWithCode returns true if the given error is an LDAP error with the given result code 146 | func IsErrorWithCode(err error, desiredResultCode uint8) bool { 147 | if err == nil { 148 | return false 149 | } 150 | 151 | serverError, ok := err.(*Error) 152 | if !ok { 153 | return false 154 | } 155 | 156 | return serverError.ResultCode == desiredResultCode 157 | } 158 | -------------------------------------------------------------------------------- /dn_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestSuccessfulDNParsing(t *testing.T) { 9 | testcases := map[string]DN{ 10 | "": DN{[]*RelativeDN{}}, 11 | "cn=Jim\\2C \\22Hasse Hö\\22 Hansson!,dc=dummy,dc=com": DN{[]*RelativeDN{ 12 | &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"cn", "Jim, \"Hasse Hö\" Hansson!"}}}, 13 | &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"dc", "dummy"}}}, 14 | &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"dc", "com"}}}}}, 15 | "UID=jsmith,DC=example,DC=net": DN{[]*RelativeDN{ 16 | &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"UID", "jsmith"}}}, 17 | &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "example"}}}, 18 | &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "net"}}}}}, 19 | "OU=Sales+CN=J. Smith,DC=example,DC=net": DN{[]*RelativeDN{ 20 | &RelativeDN{[]*AttributeTypeAndValue{ 21 | &AttributeTypeAndValue{"OU", "Sales"}, 22 | &AttributeTypeAndValue{"CN", "J. Smith"}}}, 23 | &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "example"}}}, 24 | &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "net"}}}}}, 25 | "1.3.6.1.4.1.1466.0=#04024869": DN{[]*RelativeDN{ 26 | &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"1.3.6.1.4.1.1466.0", "Hi"}}}}}, 27 | "1.3.6.1.4.1.1466.0=#04024869,DC=net": DN{[]*RelativeDN{ 28 | &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"1.3.6.1.4.1.1466.0", "Hi"}}}, 29 | &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "net"}}}}}, 30 | "CN=Lu\\C4\\8Di\\C4\\87": DN{[]*RelativeDN{ 31 | &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"CN", "Lučić"}}}}}, 32 | " CN = Lu\\C4\\8Di\\C4\\87 ": DN{[]*RelativeDN{ 33 | &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"CN", "Lučić"}}}}}, 34 | ` A = 1 , B = 2 `: DN{[]*RelativeDN{ 35 | &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"A", "1"}}}, 36 | &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"B", "2"}}}}}, 37 | ` A = 1 + B = 2 `: DN{[]*RelativeDN{ 38 | &RelativeDN{[]*AttributeTypeAndValue{ 39 | &AttributeTypeAndValue{"A", "1"}, 40 | &AttributeTypeAndValue{"B", "2"}}}}}, 41 | ` \ \ A\ \ = \ \ 1\ \ , \ \ B\ \ = \ \ 2\ \ `: DN{[]*RelativeDN{ 42 | &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{" A ", " 1 "}}}, 43 | &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{" B ", " 2 "}}}}}, 44 | ` \ \ A\ \ = \ \ 1\ \ + \ \ B\ \ = \ \ 2\ \ `: DN{[]*RelativeDN{ 45 | &RelativeDN{[]*AttributeTypeAndValue{ 46 | &AttributeTypeAndValue{" A ", " 1 "}, 47 | &AttributeTypeAndValue{" B ", " 2 "}}}}}, 48 | } 49 | 50 | for test, answer := range testcases { 51 | dn, err := ParseDN(test) 52 | if err != nil { 53 | t.Errorf(err.Error()) 54 | continue 55 | } 56 | if !reflect.DeepEqual(dn, &answer) { 57 | t.Errorf("Parsed DN %s is not equal to the expected structure", test) 58 | t.Logf("Expected:") 59 | for _, rdn := range answer.RDNs { 60 | for _, attribs := range rdn.Attributes { 61 | t.Logf("#%v\n", attribs) 62 | } 63 | } 64 | t.Logf("Actual:") 65 | for _, rdn := range dn.RDNs { 66 | for _, attribs := range rdn.Attributes { 67 | t.Logf("#%v\n", attribs) 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | func TestErrorDNParsing(t *testing.T) { 75 | testcases := map[string]string{ 76 | "*": "DN ended with incomplete type, value pair", 77 | "cn=Jim\\0Test": "Failed to decode escaped character: encoding/hex: invalid byte: U+0054 'T'", 78 | "cn=Jim\\0": "Got corrupted escaped character", 79 | "DC=example,=net": "DN ended with incomplete type, value pair", 80 | "1=#0402486": "Failed to decode BER encoding: encoding/hex: odd length hex string", 81 | "test,DC=example,DC=com": "incomplete type, value pair", 82 | "=test,DC=example,DC=com": "incomplete type, value pair", 83 | } 84 | 85 | for test, answer := range testcases { 86 | _, err := ParseDN(test) 87 | if err == nil { 88 | t.Errorf("Expected %s to fail parsing but succeeded\n", test) 89 | } else if err.Error() != answer { 90 | t.Errorf("Unexpected error on %s:\n%s\nvs.\n%s\n", test, answer, err.Error()) 91 | } 92 | } 93 | } 94 | 95 | func TestDNEqual(t *testing.T) { 96 | testcases := []struct { 97 | A string 98 | B string 99 | Equal bool 100 | }{ 101 | // Exact match 102 | {"", "", true}, 103 | {"o=A", "o=A", true}, 104 | {"o=A", "o=B", false}, 105 | 106 | {"o=A,o=B", "o=A,o=B", true}, 107 | {"o=A,o=B", "o=A,o=C", false}, 108 | 109 | {"o=A+o=B", "o=A+o=B", true}, 110 | {"o=A+o=B", "o=A+o=C", false}, 111 | 112 | // Case mismatch in type is ignored 113 | {"o=A", "O=A", true}, 114 | {"o=A,o=B", "o=A,O=B", true}, 115 | {"o=A+o=B", "o=A+O=B", true}, 116 | 117 | // Case mismatch in value is significant 118 | {"o=a", "O=A", false}, 119 | {"o=a,o=B", "o=A,O=B", false}, 120 | {"o=a+o=B", "o=A+O=B", false}, 121 | 122 | // Multi-valued RDN order mismatch is ignored 123 | {"o=A+o=B", "O=B+o=A", true}, 124 | // Number of RDN attributes is significant 125 | {"o=A+o=B", "O=B+o=A+O=B", false}, 126 | 127 | // Missing values are significant 128 | {"o=A+o=B", "O=B+o=A+O=C", false}, // missing values matter 129 | {"o=A+o=B+o=C", "O=B+o=A", false}, // missing values matter 130 | 131 | // Whitespace tests 132 | // Matching 133 | { 134 | "cn=John Doe, ou=People, dc=sun.com", 135 | "cn=John Doe, ou=People, dc=sun.com", 136 | true, 137 | }, 138 | // Difference in leading/trailing chars is ignored 139 | { 140 | "cn=John Doe, ou=People, dc=sun.com", 141 | "cn=John Doe,ou=People,dc=sun.com", 142 | true, 143 | }, 144 | // Difference in values is significant 145 | { 146 | "cn=John Doe, ou=People, dc=sun.com", 147 | "cn=John Doe, ou=People, dc=sun.com", 148 | false, 149 | }, 150 | } 151 | 152 | for i, tc := range testcases { 153 | a, err := ParseDN(tc.A) 154 | if err != nil { 155 | t.Errorf("%d: %v", i, err) 156 | continue 157 | } 158 | b, err := ParseDN(tc.B) 159 | if err != nil { 160 | t.Errorf("%d: %v", i, err) 161 | continue 162 | } 163 | if expected, actual := tc.Equal, a.Equal(b); expected != actual { 164 | t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual) 165 | continue 166 | } 167 | if expected, actual := tc.Equal, b.Equal(a); expected != actual { 168 | t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual) 169 | continue 170 | } 171 | } 172 | } 173 | 174 | func TestDNAncestor(t *testing.T) { 175 | testcases := []struct { 176 | A string 177 | B string 178 | Ancestor bool 179 | }{ 180 | // Exact match returns false 181 | {"", "", false}, 182 | {"o=A", "o=A", false}, 183 | {"o=A,o=B", "o=A,o=B", false}, 184 | {"o=A+o=B", "o=A+o=B", false}, 185 | 186 | // Mismatch 187 | {"ou=C,ou=B,o=A", "ou=E,ou=D,ou=B,o=A", false}, 188 | 189 | // Descendant 190 | {"ou=C,ou=B,o=A", "ou=E,ou=C,ou=B,o=A", true}, 191 | } 192 | 193 | for i, tc := range testcases { 194 | a, err := ParseDN(tc.A) 195 | if err != nil { 196 | t.Errorf("%d: %v", i, err) 197 | continue 198 | } 199 | b, err := ParseDN(tc.B) 200 | if err != nil { 201 | t.Errorf("%d: %v", i, err) 202 | continue 203 | } 204 | if expected, actual := tc.Ancestor, a.AncestorOf(b); expected != actual { 205 | t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual) 206 | continue 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /ldap_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | var ldapServer = "ldap.itd.umich.edu" 10 | var ldapPort = uint16(389) 11 | var ldapTLSPort = uint16(636) 12 | var baseDN = "dc=umich,dc=edu" 13 | var filter = []string{ 14 | "(cn=cis-fac)", 15 | "(&(owner=*)(cn=cis-fac))", 16 | "(&(objectclass=rfc822mailgroup)(cn=*Computer*))", 17 | "(&(objectclass=rfc822mailgroup)(cn=*Mathematics*))"} 18 | var attributes = []string{ 19 | "cn", 20 | "description"} 21 | 22 | func TestDial(t *testing.T) { 23 | fmt.Printf("TestDial: starting...\n") 24 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) 25 | if err != nil { 26 | t.Errorf(err.Error()) 27 | return 28 | } 29 | defer l.Close() 30 | fmt.Printf("TestDial: finished...\n") 31 | } 32 | 33 | func TestDialTLS(t *testing.T) { 34 | fmt.Printf("TestDialTLS: starting...\n") 35 | l, err := DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapTLSPort), &tls.Config{InsecureSkipVerify: true}) 36 | if err != nil { 37 | t.Errorf(err.Error()) 38 | return 39 | } 40 | defer l.Close() 41 | fmt.Printf("TestDialTLS: finished...\n") 42 | } 43 | 44 | func TestStartTLS(t *testing.T) { 45 | fmt.Printf("TestStartTLS: starting...\n") 46 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) 47 | if err != nil { 48 | t.Errorf(err.Error()) 49 | return 50 | } 51 | err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) 52 | if err != nil { 53 | t.Errorf(err.Error()) 54 | return 55 | } 56 | fmt.Printf("TestStartTLS: finished...\n") 57 | } 58 | 59 | func TestSearch(t *testing.T) { 60 | fmt.Printf("TestSearch: starting...\n") 61 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) 62 | if err != nil { 63 | t.Errorf(err.Error()) 64 | return 65 | } 66 | defer l.Close() 67 | 68 | searchRequest := NewSearchRequest( 69 | baseDN, 70 | ScopeWholeSubtree, DerefAlways, 0, 0, false, 71 | filter[0], 72 | attributes, 73 | nil) 74 | 75 | sr, err := l.Search(searchRequest) 76 | if err != nil { 77 | t.Errorf(err.Error()) 78 | return 79 | } 80 | 81 | fmt.Printf("TestSearch: %s -> num of entries = %d\n", searchRequest.Filter, len(sr.Entries)) 82 | } 83 | 84 | func TestSearchStartTLS(t *testing.T) { 85 | fmt.Printf("TestSearchStartTLS: starting...\n") 86 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) 87 | if err != nil { 88 | t.Errorf(err.Error()) 89 | return 90 | } 91 | defer l.Close() 92 | 93 | searchRequest := NewSearchRequest( 94 | baseDN, 95 | ScopeWholeSubtree, DerefAlways, 0, 0, false, 96 | filter[0], 97 | attributes, 98 | nil) 99 | 100 | sr, err := l.Search(searchRequest) 101 | if err != nil { 102 | t.Errorf(err.Error()) 103 | return 104 | } 105 | 106 | fmt.Printf("TestSearchStartTLS: %s -> num of entries = %d\n", searchRequest.Filter, len(sr.Entries)) 107 | 108 | fmt.Printf("TestSearchStartTLS: upgrading with startTLS\n") 109 | err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) 110 | if err != nil { 111 | t.Errorf(err.Error()) 112 | return 113 | } 114 | 115 | sr, err = l.Search(searchRequest) 116 | if err != nil { 117 | t.Errorf(err.Error()) 118 | return 119 | } 120 | 121 | fmt.Printf("TestSearchStartTLS: %s -> num of entries = %d\n", searchRequest.Filter, len(sr.Entries)) 122 | } 123 | 124 | func TestSearchWithPaging(t *testing.T) { 125 | fmt.Printf("TestSearchWithPaging: starting...\n") 126 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) 127 | if err != nil { 128 | t.Errorf(err.Error()) 129 | return 130 | } 131 | defer l.Close() 132 | 133 | err = l.UnauthenticatedBind("") 134 | if err != nil { 135 | t.Errorf(err.Error()) 136 | return 137 | } 138 | 139 | searchRequest := NewSearchRequest( 140 | baseDN, 141 | ScopeWholeSubtree, DerefAlways, 0, 0, false, 142 | filter[2], 143 | attributes, 144 | nil) 145 | sr, err := l.SearchWithPaging(searchRequest, 5) 146 | if err != nil { 147 | t.Errorf(err.Error()) 148 | return 149 | } 150 | 151 | fmt.Printf("TestSearchWithPaging: %s -> num of entries = %d\n", searchRequest.Filter, len(sr.Entries)) 152 | 153 | searchRequest = NewSearchRequest( 154 | baseDN, 155 | ScopeWholeSubtree, DerefAlways, 0, 0, false, 156 | filter[2], 157 | attributes, 158 | []Control{NewControlPaging(5)}) 159 | sr, err = l.SearchWithPaging(searchRequest, 5) 160 | if err != nil { 161 | t.Errorf(err.Error()) 162 | return 163 | } 164 | 165 | fmt.Printf("TestSearchWithPaging: %s -> num of entries = %d\n", searchRequest.Filter, len(sr.Entries)) 166 | 167 | searchRequest = NewSearchRequest( 168 | baseDN, 169 | ScopeWholeSubtree, DerefAlways, 0, 0, false, 170 | filter[2], 171 | attributes, 172 | []Control{NewControlPaging(500)}) 173 | sr, err = l.SearchWithPaging(searchRequest, 5) 174 | if err == nil { 175 | t.Errorf("expected an error when paging size in control in search request doesn't match size given in call, got none") 176 | return 177 | } 178 | } 179 | 180 | func searchGoroutine(t *testing.T, l *Conn, results chan *SearchResult, i int) { 181 | searchRequest := NewSearchRequest( 182 | baseDN, 183 | ScopeWholeSubtree, DerefAlways, 0, 0, false, 184 | filter[i], 185 | attributes, 186 | nil) 187 | sr, err := l.Search(searchRequest) 188 | if err != nil { 189 | t.Errorf(err.Error()) 190 | results <- nil 191 | return 192 | } 193 | results <- sr 194 | } 195 | 196 | func testMultiGoroutineSearch(t *testing.T, TLS bool, startTLS bool) { 197 | fmt.Printf("TestMultiGoroutineSearch: starting...\n") 198 | var l *Conn 199 | var err error 200 | if TLS { 201 | l, err = DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapTLSPort), &tls.Config{InsecureSkipVerify: true}) 202 | if err != nil { 203 | t.Errorf(err.Error()) 204 | return 205 | } 206 | defer l.Close() 207 | } else { 208 | l, err = Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) 209 | if err != nil { 210 | t.Errorf(err.Error()) 211 | return 212 | } 213 | if startTLS { 214 | fmt.Printf("TestMultiGoroutineSearch: using StartTLS...\n") 215 | err := l.StartTLS(&tls.Config{InsecureSkipVerify: true}) 216 | if err != nil { 217 | t.Errorf(err.Error()) 218 | return 219 | } 220 | 221 | } 222 | } 223 | 224 | results := make([]chan *SearchResult, len(filter)) 225 | for i := range filter { 226 | results[i] = make(chan *SearchResult) 227 | go searchGoroutine(t, l, results[i], i) 228 | } 229 | for i := range filter { 230 | sr := <-results[i] 231 | if sr == nil { 232 | t.Errorf("Did not receive results from goroutine for %q", filter[i]) 233 | } else { 234 | fmt.Printf("TestMultiGoroutineSearch(%d): %s -> num of entries = %d\n", i, filter[i], len(sr.Entries)) 235 | } 236 | } 237 | } 238 | 239 | func TestMultiGoroutineSearch(t *testing.T) { 240 | testMultiGoroutineSearch(t, false, false) 241 | testMultiGoroutineSearch(t, true, true) 242 | testMultiGoroutineSearch(t, false, true) 243 | } 244 | 245 | func TestEscapeFilter(t *testing.T) { 246 | if got, want := EscapeFilter("a\x00b(c)d*e\\f"), `a\00b\28c\29d\2ae\5cf`; got != want { 247 | t.Errorf("Got %s, expected %s", want, got) 248 | } 249 | if got, want := EscapeFilter("Lučić"), `Lu\c4\8di\c4\87`; got != want { 250 | t.Errorf("Got %s, expected %s", want, got) 251 | } 252 | } 253 | 254 | func TestCompare(t *testing.T) { 255 | fmt.Printf("TestCompare: starting...\n") 256 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) 257 | if err != nil { 258 | t.Fatal(err.Error()) 259 | } 260 | defer l.Close() 261 | 262 | dn := "cn=math mich,ou=User Groups,ou=Groups,dc=umich,dc=edu" 263 | attribute := "cn" 264 | value := "math mich" 265 | 266 | sr, err := l.Compare(dn, attribute, value) 267 | if err != nil { 268 | t.Errorf(err.Error()) 269 | return 270 | } 271 | 272 | fmt.Printf("TestCompare: -> %v\n", sr) 273 | } 274 | -------------------------------------------------------------------------------- /filter_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "gopkg.in/asn1-ber.v1" 8 | ) 9 | 10 | type compileTest struct { 11 | filterStr string 12 | 13 | expectedFilter string 14 | expectedType int 15 | expectedErr string 16 | } 17 | 18 | var testFilters = []compileTest{ 19 | compileTest{ 20 | filterStr: "(&(sn=Miller)(givenName=Bob))", 21 | expectedFilter: "(&(sn=Miller)(givenName=Bob))", 22 | expectedType: FilterAnd, 23 | }, 24 | compileTest{ 25 | filterStr: "(|(sn=Miller)(givenName=Bob))", 26 | expectedFilter: "(|(sn=Miller)(givenName=Bob))", 27 | expectedType: FilterOr, 28 | }, 29 | compileTest{ 30 | filterStr: "(!(sn=Miller))", 31 | expectedFilter: "(!(sn=Miller))", 32 | expectedType: FilterNot, 33 | }, 34 | compileTest{ 35 | filterStr: "(sn=Miller)", 36 | expectedFilter: "(sn=Miller)", 37 | expectedType: FilterEqualityMatch, 38 | }, 39 | compileTest{ 40 | filterStr: "(sn=Mill*)", 41 | expectedFilter: "(sn=Mill*)", 42 | expectedType: FilterSubstrings, 43 | }, 44 | compileTest{ 45 | filterStr: "(sn=*Mill)", 46 | expectedFilter: "(sn=*Mill)", 47 | expectedType: FilterSubstrings, 48 | }, 49 | compileTest{ 50 | filterStr: "(sn=*Mill*)", 51 | expectedFilter: "(sn=*Mill*)", 52 | expectedType: FilterSubstrings, 53 | }, 54 | compileTest{ 55 | filterStr: "(sn=*i*le*)", 56 | expectedFilter: "(sn=*i*le*)", 57 | expectedType: FilterSubstrings, 58 | }, 59 | compileTest{ 60 | filterStr: "(sn=Mi*l*r)", 61 | expectedFilter: "(sn=Mi*l*r)", 62 | expectedType: FilterSubstrings, 63 | }, 64 | // substring filters escape properly 65 | compileTest{ 66 | filterStr: `(sn=Mi*함*r)`, 67 | expectedFilter: `(sn=Mi*\ed\95\a8*r)`, 68 | expectedType: FilterSubstrings, 69 | }, 70 | // already escaped substring filters don't get double-escaped 71 | compileTest{ 72 | filterStr: `(sn=Mi*\ed\95\a8*r)`, 73 | expectedFilter: `(sn=Mi*\ed\95\a8*r)`, 74 | expectedType: FilterSubstrings, 75 | }, 76 | compileTest{ 77 | filterStr: "(sn=Mi*le*)", 78 | expectedFilter: "(sn=Mi*le*)", 79 | expectedType: FilterSubstrings, 80 | }, 81 | compileTest{ 82 | filterStr: "(sn=*i*ler)", 83 | expectedFilter: "(sn=*i*ler)", 84 | expectedType: FilterSubstrings, 85 | }, 86 | compileTest{ 87 | filterStr: "(sn>=Miller)", 88 | expectedFilter: "(sn>=Miller)", 89 | expectedType: FilterGreaterOrEqual, 90 | }, 91 | compileTest{ 92 | filterStr: "(sn<=Miller)", 93 | expectedFilter: "(sn<=Miller)", 94 | expectedType: FilterLessOrEqual, 95 | }, 96 | compileTest{ 97 | filterStr: "(sn=*)", 98 | expectedFilter: "(sn=*)", 99 | expectedType: FilterPresent, 100 | }, 101 | compileTest{ 102 | filterStr: "(sn~=Miller)", 103 | expectedFilter: "(sn~=Miller)", 104 | expectedType: FilterApproxMatch, 105 | }, 106 | compileTest{ 107 | filterStr: `(objectGUID='\fc\fe\a3\ab\f9\90N\aaGm\d5I~\d12)`, 108 | expectedFilter: `(objectGUID='\fc\fe\a3\ab\f9\90N\aaGm\d5I~\d12)`, 109 | expectedType: FilterEqualityMatch, 110 | }, 111 | compileTest{ 112 | filterStr: `(objectGUID=абвгдеёжзийклмнопрстуфхцчшщъыьэюя)`, 113 | expectedFilter: `(objectGUID=\d0\b0\d0\b1\d0\b2\d0\b3\d0\b4\d0\b5\d1\91\d0\b6\d0\b7\d0\b8\d0\b9\d0\ba\d0\bb\d0\bc\d0\bd\d0\be\d0\bf\d1\80\d1\81\d1\82\d1\83\d1\84\d1\85\d1\86\d1\87\d1\88\d1\89\d1\8a\d1\8b\d1\8c\d1\8d\d1\8e\d1\8f)`, 114 | expectedType: FilterEqualityMatch, 115 | }, 116 | compileTest{ 117 | filterStr: `(objectGUID=함수목록)`, 118 | expectedFilter: `(objectGUID=\ed\95\a8\ec\88\98\eb\aa\a9\eb\a1\9d)`, 119 | expectedType: FilterEqualityMatch, 120 | }, 121 | compileTest{ 122 | filterStr: `(objectGUID=`, 123 | expectedFilter: ``, 124 | expectedType: 0, 125 | expectedErr: "unexpected end of filter", 126 | }, 127 | compileTest{ 128 | filterStr: `(objectGUID=함수목록`, 129 | expectedFilter: ``, 130 | expectedType: 0, 131 | expectedErr: "unexpected end of filter", 132 | }, 133 | compileTest{ 134 | filterStr: `((cn=)`, 135 | expectedFilter: ``, 136 | expectedType: 0, 137 | expectedErr: "unexpected end of filter", 138 | }, 139 | compileTest{ 140 | filterStr: `(&(objectclass=inetorgperson)(cn=中文))`, 141 | expectedFilter: `(&(objectclass=inetorgperson)(cn=\e4\b8\ad\e6\96\87))`, 142 | expectedType: 0, 143 | }, 144 | // attr extension 145 | compileTest{ 146 | filterStr: `(memberOf:=foo)`, 147 | expectedFilter: `(memberOf:=foo)`, 148 | expectedType: FilterExtensibleMatch, 149 | }, 150 | // attr+named matching rule extension 151 | compileTest{ 152 | filterStr: `(memberOf:test:=foo)`, 153 | expectedFilter: `(memberOf:test:=foo)`, 154 | expectedType: FilterExtensibleMatch, 155 | }, 156 | // attr+oid matching rule extension 157 | compileTest{ 158 | filterStr: `(cn:1.2.3.4.5:=Fred Flintstone)`, 159 | expectedFilter: `(cn:1.2.3.4.5:=Fred Flintstone)`, 160 | expectedType: FilterExtensibleMatch, 161 | }, 162 | // attr+dn+oid matching rule extension 163 | compileTest{ 164 | filterStr: `(sn:dn:2.4.6.8.10:=Barney Rubble)`, 165 | expectedFilter: `(sn:dn:2.4.6.8.10:=Barney Rubble)`, 166 | expectedType: FilterExtensibleMatch, 167 | }, 168 | // attr+dn extension 169 | compileTest{ 170 | filterStr: `(o:dn:=Ace Industry)`, 171 | expectedFilter: `(o:dn:=Ace Industry)`, 172 | expectedType: FilterExtensibleMatch, 173 | }, 174 | // dn extension 175 | compileTest{ 176 | filterStr: `(:dn:2.4.6.8.10:=Dino)`, 177 | expectedFilter: `(:dn:2.4.6.8.10:=Dino)`, 178 | expectedType: FilterExtensibleMatch, 179 | }, 180 | compileTest{ 181 | filterStr: `(memberOf:1.2.840.113556.1.4.1941:=CN=User1,OU=blah,DC=mydomain,DC=net)`, 182 | expectedFilter: `(memberOf:1.2.840.113556.1.4.1941:=CN=User1,OU=blah,DC=mydomain,DC=net)`, 183 | expectedType: FilterExtensibleMatch, 184 | }, 185 | 186 | // compileTest{ filterStr: "()", filterType: FilterExtensibleMatch }, 187 | } 188 | 189 | var testInvalidFilters = []string{ 190 | `(objectGUID=\zz)`, 191 | `(objectGUID=\a)`, 192 | } 193 | 194 | func TestFilter(t *testing.T) { 195 | // Test Compiler and Decompiler 196 | for _, i := range testFilters { 197 | filter, err := CompileFilter(i.filterStr) 198 | if err != nil { 199 | if i.expectedErr == "" || !strings.Contains(err.Error(), i.expectedErr) { 200 | t.Errorf("Problem compiling '%s' - '%v' (expected error to contain '%v')", i.filterStr, err, i.expectedErr) 201 | } 202 | } else if filter.Tag != ber.Tag(i.expectedType) { 203 | t.Errorf("%q Expected %q got %q", i.filterStr, FilterMap[uint64(i.expectedType)], FilterMap[uint64(filter.Tag)]) 204 | } else { 205 | o, err := DecompileFilter(filter) 206 | if err != nil { 207 | t.Errorf("Problem compiling %s - %s", i.filterStr, err.Error()) 208 | } else if i.expectedFilter != o { 209 | t.Errorf("%q expected, got %q", i.expectedFilter, o) 210 | } 211 | } 212 | } 213 | } 214 | 215 | func TestInvalidFilter(t *testing.T) { 216 | for _, filterStr := range testInvalidFilters { 217 | if _, err := CompileFilter(filterStr); err == nil { 218 | t.Errorf("Problem compiling %s - expected err", filterStr) 219 | } 220 | } 221 | } 222 | 223 | func BenchmarkFilterCompile(b *testing.B) { 224 | b.StopTimer() 225 | filters := make([]string, len(testFilters)) 226 | 227 | // Test Compiler and Decompiler 228 | for idx, i := range testFilters { 229 | filters[idx] = i.filterStr 230 | } 231 | 232 | maxIdx := len(filters) 233 | b.StartTimer() 234 | for i := 0; i < b.N; i++ { 235 | CompileFilter(filters[i%maxIdx]) 236 | } 237 | } 238 | 239 | func BenchmarkFilterDecompile(b *testing.B) { 240 | b.StopTimer() 241 | filters := make([]*ber.Packet, len(testFilters)) 242 | 243 | // Test Compiler and Decompiler 244 | for idx, i := range testFilters { 245 | filters[idx], _ = CompileFilter(i.filterStr) 246 | } 247 | 248 | maxIdx := len(filters) 249 | b.StartTimer() 250 | for i := 0; i < b.N; i++ { 251 | DecompileFilter(filters[i%maxIdx]) 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /dn.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | // 5 | // File contains DN parsing functionality 6 | // 7 | // https://tools.ietf.org/html/rfc4514 8 | // 9 | // distinguishedName = [ relativeDistinguishedName 10 | // *( COMMA relativeDistinguishedName ) ] 11 | // relativeDistinguishedName = attributeTypeAndValue 12 | // *( PLUS attributeTypeAndValue ) 13 | // attributeTypeAndValue = attributeType EQUALS attributeValue 14 | // attributeType = descr / numericoid 15 | // attributeValue = string / hexstring 16 | // 17 | // ; The following characters are to be escaped when they appear 18 | // ; in the value to be encoded: ESC, one of , leading 19 | // ; SHARP or SPACE, trailing SPACE, and NULL. 20 | // string = [ ( leadchar / pair ) [ *( stringchar / pair ) 21 | // ( trailchar / pair ) ] ] 22 | // 23 | // leadchar = LUTF1 / UTFMB 24 | // LUTF1 = %x01-1F / %x21 / %x24-2A / %x2D-3A / 25 | // %x3D / %x3F-5B / %x5D-7F 26 | // 27 | // trailchar = TUTF1 / UTFMB 28 | // TUTF1 = %x01-1F / %x21 / %x23-2A / %x2D-3A / 29 | // %x3D / %x3F-5B / %x5D-7F 30 | // 31 | // stringchar = SUTF1 / UTFMB 32 | // SUTF1 = %x01-21 / %x23-2A / %x2D-3A / 33 | // %x3D / %x3F-5B / %x5D-7F 34 | // 35 | // pair = ESC ( ESC / special / hexpair ) 36 | // special = escaped / SPACE / SHARP / EQUALS 37 | // escaped = DQUOTE / PLUS / COMMA / SEMI / LANGLE / RANGLE 38 | // hexstring = SHARP 1*hexpair 39 | // hexpair = HEX HEX 40 | // 41 | // where the productions , , , , 42 | // , , , , , , , , 43 | // , , and are defined in [RFC4512]. 44 | // 45 | 46 | package ldap 47 | 48 | import ( 49 | "bytes" 50 | enchex "encoding/hex" 51 | "errors" 52 | "fmt" 53 | "strings" 54 | 55 | "gopkg.in/asn1-ber.v1" 56 | ) 57 | 58 | // AttributeTypeAndValue represents an attributeTypeAndValue from https://tools.ietf.org/html/rfc4514 59 | type AttributeTypeAndValue struct { 60 | // Type is the attribute type 61 | Type string 62 | // Value is the attribute value 63 | Value string 64 | } 65 | 66 | // RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514 67 | type RelativeDN struct { 68 | Attributes []*AttributeTypeAndValue 69 | } 70 | 71 | // DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514 72 | type DN struct { 73 | RDNs []*RelativeDN 74 | } 75 | 76 | // ParseDN returns a distinguishedName or an error 77 | func ParseDN(str string) (*DN, error) { 78 | dn := new(DN) 79 | dn.RDNs = make([]*RelativeDN, 0) 80 | rdn := new(RelativeDN) 81 | rdn.Attributes = make([]*AttributeTypeAndValue, 0) 82 | buffer := bytes.Buffer{} 83 | attribute := new(AttributeTypeAndValue) 84 | escaping := false 85 | 86 | unescapedTrailingSpaces := 0 87 | stringFromBuffer := func() string { 88 | s := buffer.String() 89 | s = s[0 : len(s)-unescapedTrailingSpaces] 90 | buffer.Reset() 91 | unescapedTrailingSpaces = 0 92 | return s 93 | } 94 | 95 | for i := 0; i < len(str); i++ { 96 | char := str[i] 97 | if escaping { 98 | unescapedTrailingSpaces = 0 99 | escaping = false 100 | switch char { 101 | case ' ', '"', '#', '+', ',', ';', '<', '=', '>', '\\': 102 | buffer.WriteByte(char) 103 | continue 104 | } 105 | // Not a special character, assume hex encoded octet 106 | if len(str) == i+1 { 107 | return nil, errors.New("Got corrupted escaped character") 108 | } 109 | 110 | dst := []byte{0} 111 | n, err := enchex.Decode([]byte(dst), []byte(str[i:i+2])) 112 | if err != nil { 113 | return nil, fmt.Errorf("Failed to decode escaped character: %s", err) 114 | } else if n != 1 { 115 | return nil, fmt.Errorf("Expected 1 byte when un-escaping, got %d", n) 116 | } 117 | buffer.WriteByte(dst[0]) 118 | i++ 119 | } else if char == '\\' { 120 | unescapedTrailingSpaces = 0 121 | escaping = true 122 | } else if char == '=' { 123 | attribute.Type = stringFromBuffer() 124 | // Special case: If the first character in the value is # the 125 | // following data is BER encoded so we can just fast forward 126 | // and decode. 127 | if len(str) > i+1 && str[i+1] == '#' { 128 | i += 2 129 | index := strings.IndexAny(str[i:], ",+") 130 | data := str 131 | if index > 0 { 132 | data = str[i : i+index] 133 | } else { 134 | data = str[i:] 135 | } 136 | rawBER, err := enchex.DecodeString(data) 137 | if err != nil { 138 | return nil, fmt.Errorf("Failed to decode BER encoding: %s", err) 139 | } 140 | packet := ber.DecodePacket(rawBER) 141 | buffer.WriteString(packet.Data.String()) 142 | i += len(data) - 1 143 | } 144 | } else if char == ',' || char == '+' { 145 | // We're done with this RDN or value, push it 146 | if len(attribute.Type) == 0 { 147 | return nil, errors.New("incomplete type, value pair") 148 | } 149 | attribute.Value = stringFromBuffer() 150 | rdn.Attributes = append(rdn.Attributes, attribute) 151 | attribute = new(AttributeTypeAndValue) 152 | if char == ',' { 153 | dn.RDNs = append(dn.RDNs, rdn) 154 | rdn = new(RelativeDN) 155 | rdn.Attributes = make([]*AttributeTypeAndValue, 0) 156 | } 157 | } else if char == ' ' && buffer.Len() == 0 { 158 | // ignore unescaped leading spaces 159 | continue 160 | } else { 161 | if char == ' ' { 162 | // Track unescaped spaces in case they are trailing and we need to remove them 163 | unescapedTrailingSpaces++ 164 | } else { 165 | // Reset if we see a non-space char 166 | unescapedTrailingSpaces = 0 167 | } 168 | buffer.WriteByte(char) 169 | } 170 | } 171 | if buffer.Len() > 0 { 172 | if len(attribute.Type) == 0 { 173 | return nil, errors.New("DN ended with incomplete type, value pair") 174 | } 175 | attribute.Value = stringFromBuffer() 176 | rdn.Attributes = append(rdn.Attributes, attribute) 177 | dn.RDNs = append(dn.RDNs, rdn) 178 | } 179 | return dn, nil 180 | } 181 | 182 | // Equal returns true if the DNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). 183 | // Returns true if they have the same number of relative distinguished names 184 | // and corresponding relative distinguished names (by position) are the same. 185 | func (d *DN) Equal(other *DN) bool { 186 | if len(d.RDNs) != len(other.RDNs) { 187 | return false 188 | } 189 | for i := range d.RDNs { 190 | if !d.RDNs[i].Equal(other.RDNs[i]) { 191 | return false 192 | } 193 | } 194 | return true 195 | } 196 | 197 | // AncestorOf returns true if the other DN consists of at least one RDN followed by all the RDNs of the current DN. 198 | // "ou=widgets,o=acme.com" is an ancestor of "ou=sprockets,ou=widgets,o=acme.com" 199 | // "ou=widgets,o=acme.com" is not an ancestor of "ou=sprockets,ou=widgets,o=foo.com" 200 | // "ou=widgets,o=acme.com" is not an ancestor of "ou=widgets,o=acme.com" 201 | func (d *DN) AncestorOf(other *DN) bool { 202 | if len(d.RDNs) >= len(other.RDNs) { 203 | return false 204 | } 205 | // Take the last `len(d.RDNs)` RDNs from the other DN to compare against 206 | otherRDNs := other.RDNs[len(other.RDNs)-len(d.RDNs):] 207 | for i := range d.RDNs { 208 | if !d.RDNs[i].Equal(otherRDNs[i]) { 209 | return false 210 | } 211 | } 212 | return true 213 | } 214 | 215 | // Equal returns true if the RelativeDNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). 216 | // Relative distinguished names are the same if and only if they have the same number of AttributeTypeAndValues 217 | // and each attribute of the first RDN is the same as the attribute of the second RDN with the same attribute type. 218 | // The order of attributes is not significant. 219 | // Case of attribute types is not significant. 220 | func (r *RelativeDN) Equal(other *RelativeDN) bool { 221 | if len(r.Attributes) != len(other.Attributes) { 222 | return false 223 | } 224 | return r.hasAllAttributes(other.Attributes) && other.hasAllAttributes(r.Attributes) 225 | } 226 | 227 | func (r *RelativeDN) hasAllAttributes(attrs []*AttributeTypeAndValue) bool { 228 | for _, attr := range attrs { 229 | found := false 230 | for _, myattr := range r.Attributes { 231 | if myattr.Equal(attr) { 232 | found = true 233 | break 234 | } 235 | } 236 | if !found { 237 | return false 238 | } 239 | } 240 | return true 241 | } 242 | 243 | // Equal returns true if the AttributeTypeAndValue is equivalent to the specified AttributeTypeAndValue 244 | // Case of the attribute type is not significant 245 | func (a *AttributeTypeAndValue) Equal(other *AttributeTypeAndValue) bool { 246 | return strings.EqualFold(a.Type, other.Type) && a.Value == other.Value 247 | } 248 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "log" 7 | ) 8 | 9 | // ExampleConn_Bind demonstrates how to bind a connection to an ldap user 10 | // allowing access to restricted attributes that user has access to 11 | func ExampleConn_Bind() { 12 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389)) 13 | if err != nil { 14 | log.Fatal(err) 15 | } 16 | defer l.Close() 17 | 18 | err = l.Bind("cn=read-only-admin,dc=example,dc=com", "password") 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | } 23 | 24 | // ExampleConn_Search demonstrates how to use the search interface 25 | func ExampleConn_Search() { 26 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389)) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | defer l.Close() 31 | 32 | searchRequest := NewSearchRequest( 33 | "dc=example,dc=com", // The base dn to search 34 | ScopeWholeSubtree, NeverDerefAliases, 0, 0, false, 35 | "(&(objectClass=organizationalPerson))", // The filter to apply 36 | []string{"dn", "cn"}, // A list attributes to retrieve 37 | nil, 38 | ) 39 | 40 | sr, err := l.Search(searchRequest) 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | 45 | for _, entry := range sr.Entries { 46 | fmt.Printf("%s: %v\n", entry.DN, entry.GetAttributeValue("cn")) 47 | } 48 | } 49 | 50 | // ExampleStartTLS demonstrates how to start a TLS connection 51 | func ExampleConn_StartTLS() { 52 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389)) 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | defer l.Close() 57 | 58 | // Reconnect with TLS 59 | err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) 60 | if err != nil { 61 | log.Fatal(err) 62 | } 63 | 64 | // Operations via l are now encrypted 65 | } 66 | 67 | // ExampleConn_Compare demonstrates how to compare an attribute with a value 68 | func ExampleConn_Compare() { 69 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389)) 70 | if err != nil { 71 | log.Fatal(err) 72 | } 73 | defer l.Close() 74 | 75 | matched, err := l.Compare("cn=user,dc=example,dc=com", "uid", "someuserid") 76 | if err != nil { 77 | log.Fatal(err) 78 | } 79 | 80 | fmt.Println(matched) 81 | } 82 | 83 | func ExampleConn_PasswordModify_admin() { 84 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389)) 85 | if err != nil { 86 | log.Fatal(err) 87 | } 88 | defer l.Close() 89 | 90 | err = l.Bind("cn=admin,dc=example,dc=com", "password") 91 | if err != nil { 92 | log.Fatal(err) 93 | } 94 | 95 | passwordModifyRequest := NewPasswordModifyRequest("cn=user,dc=example,dc=com", "", "NewPassword") 96 | _, err = l.PasswordModify(passwordModifyRequest) 97 | 98 | if err != nil { 99 | log.Fatalf("Password could not be changed: %s", err.Error()) 100 | } 101 | } 102 | 103 | func ExampleConn_PasswordModify_generatedPassword() { 104 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389)) 105 | if err != nil { 106 | log.Fatal(err) 107 | } 108 | defer l.Close() 109 | 110 | err = l.Bind("cn=user,dc=example,dc=com", "password") 111 | if err != nil { 112 | log.Fatal(err) 113 | } 114 | 115 | passwordModifyRequest := NewPasswordModifyRequest("", "OldPassword", "") 116 | passwordModifyResponse, err := l.PasswordModify(passwordModifyRequest) 117 | if err != nil { 118 | log.Fatalf("Password could not be changed: %s", err.Error()) 119 | } 120 | 121 | generatedPassword := passwordModifyResponse.GeneratedPassword 122 | log.Printf("Generated password: %s\n", generatedPassword) 123 | } 124 | 125 | func ExampleConn_PasswordModify_setNewPassword() { 126 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389)) 127 | if err != nil { 128 | log.Fatal(err) 129 | } 130 | defer l.Close() 131 | 132 | err = l.Bind("cn=user,dc=example,dc=com", "password") 133 | if err != nil { 134 | log.Fatal(err) 135 | } 136 | 137 | passwordModifyRequest := NewPasswordModifyRequest("", "OldPassword", "NewPassword") 138 | _, err = l.PasswordModify(passwordModifyRequest) 139 | 140 | if err != nil { 141 | log.Fatalf("Password could not be changed: %s", err.Error()) 142 | } 143 | } 144 | 145 | func ExampleConn_Modify() { 146 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389)) 147 | if err != nil { 148 | log.Fatal(err) 149 | } 150 | defer l.Close() 151 | 152 | // Add a description, and replace the mail attributes 153 | modify := NewModifyRequest("cn=user,dc=example,dc=com") 154 | modify.Add("description", []string{"An example user"}) 155 | modify.Replace("mail", []string{"user@example.org"}) 156 | 157 | err = l.Modify(modify) 158 | if err != nil { 159 | log.Fatal(err) 160 | } 161 | } 162 | 163 | // Example User Authentication shows how a typical application can verify a login attempt 164 | func Example_userAuthentication() { 165 | // The username and password we want to check 166 | username := "someuser" 167 | password := "userpassword" 168 | 169 | bindusername := "readonly" 170 | bindpassword := "password" 171 | 172 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389)) 173 | if err != nil { 174 | log.Fatal(err) 175 | } 176 | defer l.Close() 177 | 178 | // Reconnect with TLS 179 | err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) 180 | if err != nil { 181 | log.Fatal(err) 182 | } 183 | 184 | // First bind with a read only user 185 | err = l.Bind(bindusername, bindpassword) 186 | if err != nil { 187 | log.Fatal(err) 188 | } 189 | 190 | // Search for the given username 191 | searchRequest := NewSearchRequest( 192 | "dc=example,dc=com", 193 | ScopeWholeSubtree, NeverDerefAliases, 0, 0, false, 194 | fmt.Sprintf("(&(objectClass=organizationalPerson)(uid=%s))", username), 195 | []string{"dn"}, 196 | nil, 197 | ) 198 | 199 | sr, err := l.Search(searchRequest) 200 | if err != nil { 201 | log.Fatal(err) 202 | } 203 | 204 | if len(sr.Entries) != 1 { 205 | log.Fatal("User does not exist or too many entries returned") 206 | } 207 | 208 | userdn := sr.Entries[0].DN 209 | 210 | // Bind as the user to verify their password 211 | err = l.Bind(userdn, password) 212 | if err != nil { 213 | log.Fatal(err) 214 | } 215 | 216 | // Rebind as the read only user for any further queries 217 | err = l.Bind(bindusername, bindpassword) 218 | if err != nil { 219 | log.Fatal(err) 220 | } 221 | } 222 | 223 | func Example_beherappolicy() { 224 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389)) 225 | if err != nil { 226 | log.Fatal(err) 227 | } 228 | defer l.Close() 229 | 230 | controls := []Control{} 231 | controls = append(controls, NewControlBeheraPasswordPolicy()) 232 | bindRequest := NewSimpleBindRequest("cn=admin,dc=example,dc=com", "password", controls) 233 | 234 | r, err := l.SimpleBind(bindRequest) 235 | ppolicyControl := FindControl(r.Controls, ControlTypeBeheraPasswordPolicy) 236 | 237 | var ppolicy *ControlBeheraPasswordPolicy 238 | if ppolicyControl != nil { 239 | ppolicy = ppolicyControl.(*ControlBeheraPasswordPolicy) 240 | } else { 241 | log.Printf("ppolicyControl response not available.\n") 242 | } 243 | if err != nil { 244 | errStr := "ERROR: Cannot bind: " + err.Error() 245 | if ppolicy != nil && ppolicy.Error >= 0 { 246 | errStr += ":" + ppolicy.ErrorString 247 | } 248 | log.Print(errStr) 249 | } else { 250 | logStr := "Login Ok" 251 | if ppolicy != nil { 252 | if ppolicy.Expire >= 0 { 253 | logStr += fmt.Sprintf(". Password expires in %d seconds\n", ppolicy.Expire) 254 | } else if ppolicy.Grace >= 0 { 255 | logStr += fmt.Sprintf(". Password expired, %d grace logins remain\n", ppolicy.Grace) 256 | } 257 | } 258 | log.Print(logStr) 259 | } 260 | } 261 | 262 | func Example_vchuppolicy() { 263 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389)) 264 | if err != nil { 265 | log.Fatal(err) 266 | } 267 | defer l.Close() 268 | l.Debug = true 269 | 270 | bindRequest := NewSimpleBindRequest("cn=admin,dc=example,dc=com", "password", nil) 271 | 272 | r, err := l.SimpleBind(bindRequest) 273 | 274 | passwordMustChangeControl := FindControl(r.Controls, ControlTypeVChuPasswordMustChange) 275 | var passwordMustChange *ControlVChuPasswordMustChange 276 | if passwordMustChangeControl != nil { 277 | passwordMustChange = passwordMustChangeControl.(*ControlVChuPasswordMustChange) 278 | } 279 | 280 | if passwordMustChange != nil && passwordMustChange.MustChange { 281 | log.Printf("Password Must be changed.\n") 282 | } 283 | 284 | passwordWarningControl := FindControl(r.Controls, ControlTypeVChuPasswordWarning) 285 | 286 | var passwordWarning *ControlVChuPasswordWarning 287 | if passwordWarningControl != nil { 288 | passwordWarning = passwordWarningControl.(*ControlVChuPasswordWarning) 289 | } else { 290 | log.Printf("ppolicyControl response not available.\n") 291 | } 292 | if err != nil { 293 | log.Print("ERROR: Cannot bind: " + err.Error()) 294 | } else { 295 | logStr := "Login Ok" 296 | if passwordWarning != nil { 297 | if passwordWarning.Expire >= 0 { 298 | logStr += fmt.Sprintf(". Password expires in %d seconds\n", passwordWarning.Expire) 299 | } 300 | } 301 | log.Print(logStr) 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /conn_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "net" 8 | "net/http" 9 | "net/http/httptest" 10 | "runtime" 11 | "sync" 12 | "testing" 13 | "time" 14 | 15 | "gopkg.in/asn1-ber.v1" 16 | ) 17 | 18 | func TestUnresponsiveConnection(t *testing.T) { 19 | // The do-nothing server that accepts requests and does nothing 20 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 21 | })) 22 | defer ts.Close() 23 | c, err := net.Dial(ts.Listener.Addr().Network(), ts.Listener.Addr().String()) 24 | if err != nil { 25 | t.Fatalf("error connecting to localhost tcp: %v", err) 26 | } 27 | 28 | // Create an Ldap connection 29 | conn := NewConn(c, false) 30 | conn.SetTimeout(time.Millisecond) 31 | conn.Start() 32 | defer conn.Close() 33 | 34 | // Mock a packet 35 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 36 | packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, conn.nextMessageID(), "MessageID")) 37 | bindRequest := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") 38 | bindRequest.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) 39 | packet.AppendChild(bindRequest) 40 | 41 | // Send packet and test response 42 | msgCtx, err := conn.sendMessage(packet) 43 | if err != nil { 44 | t.Fatalf("error sending message: %v", err) 45 | } 46 | defer conn.finishMessage(msgCtx) 47 | 48 | packetResponse, ok := <-msgCtx.responses 49 | if !ok { 50 | t.Fatalf("no PacketResponse in response channel") 51 | } 52 | packet, err = packetResponse.ReadPacket() 53 | if err == nil { 54 | t.Fatalf("expected timeout error") 55 | } 56 | if err.Error() != "ldap: connection timed out" { 57 | t.Fatalf("unexpected error: %v", err) 58 | } 59 | } 60 | 61 | // TestFinishMessage tests that we do not enter deadlock when a goroutine makes 62 | // a request but does not handle all responses from the server. 63 | func TestFinishMessage(t *testing.T) { 64 | ptc := newPacketTranslatorConn() 65 | defer ptc.Close() 66 | 67 | conn := NewConn(ptc, false) 68 | conn.Start() 69 | 70 | // Test sending 5 different requests in series. Ensure that we can 71 | // get a response packet from the underlying connection and also 72 | // ensure that we can gracefully ignore unhandled responses. 73 | for i := 0; i < 5; i++ { 74 | t.Logf("serial request %d", i) 75 | // Create a message and make sure we can receive responses. 76 | msgCtx := testSendRequest(t, ptc, conn) 77 | testReceiveResponse(t, ptc, msgCtx) 78 | 79 | // Send a few unhandled responses and finish the message. 80 | testSendUnhandledResponsesAndFinish(t, ptc, conn, msgCtx, 5) 81 | t.Logf("serial request %d done", i) 82 | } 83 | 84 | // Test sending 5 different requests in parallel. 85 | var wg sync.WaitGroup 86 | for i := 0; i < 5; i++ { 87 | wg.Add(1) 88 | go func(i int) { 89 | defer wg.Done() 90 | t.Logf("parallel request %d", i) 91 | // Create a message and make sure we can receive responses. 92 | msgCtx := testSendRequest(t, ptc, conn) 93 | testReceiveResponse(t, ptc, msgCtx) 94 | 95 | // Send a few unhandled responses and finish the message. 96 | testSendUnhandledResponsesAndFinish(t, ptc, conn, msgCtx, 5) 97 | t.Logf("parallel request %d done", i) 98 | }(i) 99 | } 100 | wg.Wait() 101 | 102 | // We cannot run Close() in a defer because t.FailNow() will run it and 103 | // it will block if the processMessage Loop is in a deadlock. 104 | conn.Close() 105 | } 106 | 107 | func testSendRequest(t *testing.T, ptc *packetTranslatorConn, conn *Conn) (msgCtx *messageContext) { 108 | var msgID int64 109 | runWithTimeout(t, time.Second, func() { 110 | msgID = conn.nextMessageID() 111 | }) 112 | 113 | requestPacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 114 | requestPacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, msgID, "MessageID")) 115 | 116 | var err error 117 | 118 | runWithTimeout(t, time.Second, func() { 119 | msgCtx, err = conn.sendMessage(requestPacket) 120 | if err != nil { 121 | t.Fatalf("unable to send request message: %s", err) 122 | } 123 | }) 124 | 125 | // We should now be able to get this request packet out from the other 126 | // side. 127 | runWithTimeout(t, time.Second, func() { 128 | if _, err = ptc.ReceiveRequest(); err != nil { 129 | t.Fatalf("unable to receive request packet: %s", err) 130 | } 131 | }) 132 | 133 | return msgCtx 134 | } 135 | 136 | func testReceiveResponse(t *testing.T, ptc *packetTranslatorConn, msgCtx *messageContext) { 137 | // Send a mock response packet. 138 | responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response") 139 | responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, msgCtx.id, "MessageID")) 140 | 141 | runWithTimeout(t, time.Second, func() { 142 | if err := ptc.SendResponse(responsePacket); err != nil { 143 | t.Fatalf("unable to send response packet: %s", err) 144 | } 145 | }) 146 | 147 | // We should be able to receive the packet from the connection. 148 | runWithTimeout(t, time.Second, func() { 149 | if _, ok := <-msgCtx.responses; !ok { 150 | t.Fatal("response channel closed") 151 | } 152 | }) 153 | } 154 | 155 | func testSendUnhandledResponsesAndFinish(t *testing.T, ptc *packetTranslatorConn, conn *Conn, msgCtx *messageContext, numResponses int) { 156 | // Send a mock response packet. 157 | responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response") 158 | responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, msgCtx.id, "MessageID")) 159 | 160 | // Send extra responses but do not attempt to receive them on the 161 | // client side. 162 | for i := 0; i < numResponses; i++ { 163 | runWithTimeout(t, time.Second, func() { 164 | if err := ptc.SendResponse(responsePacket); err != nil { 165 | t.Fatalf("unable to send response packet: %s", err) 166 | } 167 | }) 168 | } 169 | 170 | // Finally, attempt to finish this message. 171 | runWithTimeout(t, time.Second, func() { 172 | conn.finishMessage(msgCtx) 173 | }) 174 | } 175 | 176 | func runWithTimeout(t *testing.T, timeout time.Duration, f func()) { 177 | done := make(chan struct{}) 178 | go func() { 179 | f() 180 | close(done) 181 | }() 182 | 183 | select { 184 | case <-done: // Success! 185 | case <-time.After(timeout): 186 | _, file, line, _ := runtime.Caller(1) 187 | t.Fatalf("%s:%d timed out", file, line) 188 | } 189 | } 190 | 191 | // packetTranslatorConn is a helpful type which can be used with various tests 192 | // in this package. It implements the net.Conn interface to be used as an 193 | // underlying connection for a *ldap.Conn. Most methods are no-ops but the 194 | // Read() and Write() methods are able to translate ber-encoded packets for 195 | // testing LDAP requests and responses. 196 | // 197 | // Test cases can simulate an LDAP server sending a response by calling the 198 | // SendResponse() method with a ber-encoded LDAP response packet. Test cases 199 | // can simulate an LDAP server receiving a request from a client by calling the 200 | // ReceiveRequest() method which returns a ber-encoded LDAP request packet. 201 | type packetTranslatorConn struct { 202 | lock sync.Mutex 203 | isClosed bool 204 | 205 | responseCond sync.Cond 206 | requestCond sync.Cond 207 | 208 | responseBuf bytes.Buffer 209 | requestBuf bytes.Buffer 210 | } 211 | 212 | var errPacketTranslatorConnClosed = errors.New("connection closed") 213 | 214 | func newPacketTranslatorConn() *packetTranslatorConn { 215 | conn := &packetTranslatorConn{} 216 | conn.responseCond = sync.Cond{L: &conn.lock} 217 | conn.requestCond = sync.Cond{L: &conn.lock} 218 | 219 | return conn 220 | } 221 | 222 | // Read is called by the reader() loop to receive response packets. It will 223 | // block until there are more packet bytes available or this connection is 224 | // closed. 225 | func (c *packetTranslatorConn) Read(b []byte) (n int, err error) { 226 | c.lock.Lock() 227 | defer c.lock.Unlock() 228 | 229 | for !c.isClosed { 230 | // Attempt to read data from the response buffer. If it fails 231 | // with an EOF, wait and try again. 232 | n, err = c.responseBuf.Read(b) 233 | if err != io.EOF { 234 | return n, err 235 | } 236 | 237 | c.responseCond.Wait() 238 | } 239 | 240 | return 0, errPacketTranslatorConnClosed 241 | } 242 | 243 | // SendResponse writes the given response packet to the response buffer for 244 | // this connection, signalling any goroutine waiting to read a response. 245 | func (c *packetTranslatorConn) SendResponse(packet *ber.Packet) error { 246 | c.lock.Lock() 247 | defer c.lock.Unlock() 248 | 249 | if c.isClosed { 250 | return errPacketTranslatorConnClosed 251 | } 252 | 253 | // Signal any goroutine waiting to read a response. 254 | defer c.responseCond.Broadcast() 255 | 256 | // Writes to the buffer should always succeed. 257 | c.responseBuf.Write(packet.Bytes()) 258 | 259 | return nil 260 | } 261 | 262 | // Write is called by the processMessages() loop to send request packets. 263 | func (c *packetTranslatorConn) Write(b []byte) (n int, err error) { 264 | c.lock.Lock() 265 | defer c.lock.Unlock() 266 | 267 | if c.isClosed { 268 | return 0, errPacketTranslatorConnClosed 269 | } 270 | 271 | // Signal any goroutine waiting to read a request. 272 | defer c.requestCond.Broadcast() 273 | 274 | // Writes to the buffer should always succeed. 275 | return c.requestBuf.Write(b) 276 | } 277 | 278 | // ReceiveRequest attempts to read a request packet from this connection. It 279 | // will block until it is able to read a full request packet or until this 280 | // connection is closed. 281 | func (c *packetTranslatorConn) ReceiveRequest() (*ber.Packet, error) { 282 | c.lock.Lock() 283 | defer c.lock.Unlock() 284 | 285 | for !c.isClosed { 286 | // Attempt to parse a request packet from the request buffer. 287 | // If it fails with an unexpected EOF, wait and try again. 288 | requestReader := bytes.NewReader(c.requestBuf.Bytes()) 289 | packet, err := ber.ReadPacket(requestReader) 290 | switch err { 291 | case io.EOF, io.ErrUnexpectedEOF: 292 | c.requestCond.Wait() 293 | case nil: 294 | // Advance the request buffer by the number of bytes 295 | // read to decode the request packet. 296 | c.requestBuf.Next(c.requestBuf.Len() - requestReader.Len()) 297 | return packet, nil 298 | default: 299 | return nil, err 300 | } 301 | } 302 | 303 | return nil, errPacketTranslatorConnClosed 304 | } 305 | 306 | // Close closes this connection causing Read() and Write() calls to fail. 307 | func (c *packetTranslatorConn) Close() error { 308 | c.lock.Lock() 309 | defer c.lock.Unlock() 310 | 311 | c.isClosed = true 312 | c.responseCond.Broadcast() 313 | c.requestCond.Broadcast() 314 | 315 | return nil 316 | } 317 | 318 | func (c *packetTranslatorConn) LocalAddr() net.Addr { 319 | return (*net.TCPAddr)(nil) 320 | } 321 | 322 | func (c *packetTranslatorConn) RemoteAddr() net.Addr { 323 | return (*net.TCPAddr)(nil) 324 | } 325 | 326 | func (c *packetTranslatorConn) SetDeadline(t time.Time) error { 327 | return nil 328 | } 329 | 330 | func (c *packetTranslatorConn) SetReadDeadline(t time.Time) error { 331 | return nil 332 | } 333 | 334 | func (c *packetTranslatorConn) SetWriteDeadline(t time.Time) error { 335 | return nil 336 | } 337 | -------------------------------------------------------------------------------- /ldap.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "os" 7 | 8 | "gopkg.in/asn1-ber.v1" 9 | ) 10 | 11 | // LDAP Application Codes 12 | const ( 13 | ApplicationBindRequest = 0 14 | ApplicationBindResponse = 1 15 | ApplicationUnbindRequest = 2 16 | ApplicationSearchRequest = 3 17 | ApplicationSearchResultEntry = 4 18 | ApplicationSearchResultDone = 5 19 | ApplicationModifyRequest = 6 20 | ApplicationModifyResponse = 7 21 | ApplicationAddRequest = 8 22 | ApplicationAddResponse = 9 23 | ApplicationDelRequest = 10 24 | ApplicationDelResponse = 11 25 | ApplicationModifyDNRequest = 12 26 | ApplicationModifyDNResponse = 13 27 | ApplicationCompareRequest = 14 28 | ApplicationCompareResponse = 15 29 | ApplicationAbandonRequest = 16 30 | ApplicationSearchResultReference = 19 31 | ApplicationExtendedRequest = 23 32 | ApplicationExtendedResponse = 24 33 | ) 34 | 35 | // ApplicationMap contains human readable descriptions of LDAP Application Codes 36 | var ApplicationMap = map[uint8]string{ 37 | ApplicationBindRequest: "Bind Request", 38 | ApplicationBindResponse: "Bind Response", 39 | ApplicationUnbindRequest: "Unbind Request", 40 | ApplicationSearchRequest: "Search Request", 41 | ApplicationSearchResultEntry: "Search Result Entry", 42 | ApplicationSearchResultDone: "Search Result Done", 43 | ApplicationModifyRequest: "Modify Request", 44 | ApplicationModifyResponse: "Modify Response", 45 | ApplicationAddRequest: "Add Request", 46 | ApplicationAddResponse: "Add Response", 47 | ApplicationDelRequest: "Del Request", 48 | ApplicationDelResponse: "Del Response", 49 | ApplicationModifyDNRequest: "Modify DN Request", 50 | ApplicationModifyDNResponse: "Modify DN Response", 51 | ApplicationCompareRequest: "Compare Request", 52 | ApplicationCompareResponse: "Compare Response", 53 | ApplicationAbandonRequest: "Abandon Request", 54 | ApplicationSearchResultReference: "Search Result Reference", 55 | ApplicationExtendedRequest: "Extended Request", 56 | ApplicationExtendedResponse: "Extended Response", 57 | } 58 | 59 | // Ldap Behera Password Policy Draft 10 (https://tools.ietf.org/html/draft-behera-ldap-password-policy-10) 60 | const ( 61 | BeheraPasswordExpired = 0 62 | BeheraAccountLocked = 1 63 | BeheraChangeAfterReset = 2 64 | BeheraPasswordModNotAllowed = 3 65 | BeheraMustSupplyOldPassword = 4 66 | BeheraInsufficientPasswordQuality = 5 67 | BeheraPasswordTooShort = 6 68 | BeheraPasswordTooYoung = 7 69 | BeheraPasswordInHistory = 8 70 | ) 71 | 72 | // BeheraPasswordPolicyErrorMap contains human readable descriptions of Behera Password Policy error codes 73 | var BeheraPasswordPolicyErrorMap = map[int8]string{ 74 | BeheraPasswordExpired: "Password expired", 75 | BeheraAccountLocked: "Account locked", 76 | BeheraChangeAfterReset: "Password must be changed", 77 | BeheraPasswordModNotAllowed: "Policy prevents password modification", 78 | BeheraMustSupplyOldPassword: "Policy requires old password in order to change password", 79 | BeheraInsufficientPasswordQuality: "Password fails quality checks", 80 | BeheraPasswordTooShort: "Password is too short for policy", 81 | BeheraPasswordTooYoung: "Password has been changed too recently", 82 | BeheraPasswordInHistory: "New password is in list of old passwords", 83 | } 84 | 85 | // Adds descriptions to an LDAP Response packet for debugging 86 | func addLDAPDescriptions(packet *ber.Packet) (err error) { 87 | defer func() { 88 | if r := recover(); r != nil { 89 | err = NewError(ErrorDebugging, errors.New("ldap: cannot process packet to add descriptions")) 90 | } 91 | }() 92 | packet.Description = "LDAP Response" 93 | packet.Children[0].Description = "Message ID" 94 | 95 | application := uint8(packet.Children[1].Tag) 96 | packet.Children[1].Description = ApplicationMap[application] 97 | 98 | switch application { 99 | case ApplicationBindRequest: 100 | addRequestDescriptions(packet) 101 | case ApplicationBindResponse: 102 | addDefaultLDAPResponseDescriptions(packet) 103 | case ApplicationUnbindRequest: 104 | addRequestDescriptions(packet) 105 | case ApplicationSearchRequest: 106 | addRequestDescriptions(packet) 107 | case ApplicationSearchResultEntry: 108 | packet.Children[1].Children[0].Description = "Object Name" 109 | packet.Children[1].Children[1].Description = "Attributes" 110 | for _, child := range packet.Children[1].Children[1].Children { 111 | child.Description = "Attribute" 112 | child.Children[0].Description = "Attribute Name" 113 | child.Children[1].Description = "Attribute Values" 114 | for _, grandchild := range child.Children[1].Children { 115 | grandchild.Description = "Attribute Value" 116 | } 117 | } 118 | if len(packet.Children) == 3 { 119 | addControlDescriptions(packet.Children[2]) 120 | } 121 | case ApplicationSearchResultDone: 122 | addDefaultLDAPResponseDescriptions(packet) 123 | case ApplicationModifyRequest: 124 | addRequestDescriptions(packet) 125 | case ApplicationModifyResponse: 126 | case ApplicationAddRequest: 127 | addRequestDescriptions(packet) 128 | case ApplicationAddResponse: 129 | case ApplicationDelRequest: 130 | addRequestDescriptions(packet) 131 | case ApplicationDelResponse: 132 | case ApplicationModifyDNRequest: 133 | addRequestDescriptions(packet) 134 | case ApplicationModifyDNResponse: 135 | case ApplicationCompareRequest: 136 | addRequestDescriptions(packet) 137 | case ApplicationCompareResponse: 138 | case ApplicationAbandonRequest: 139 | addRequestDescriptions(packet) 140 | case ApplicationSearchResultReference: 141 | case ApplicationExtendedRequest: 142 | addRequestDescriptions(packet) 143 | case ApplicationExtendedResponse: 144 | } 145 | 146 | return nil 147 | } 148 | 149 | func addControlDescriptions(packet *ber.Packet) { 150 | packet.Description = "Controls" 151 | for _, child := range packet.Children { 152 | var value *ber.Packet 153 | controlType := "" 154 | child.Description = "Control" 155 | switch len(child.Children) { 156 | case 0: 157 | // at least one child is required for control type 158 | continue 159 | 160 | case 1: 161 | // just type, no criticality or value 162 | controlType = child.Children[0].Value.(string) 163 | child.Children[0].Description = "Control Type (" + ControlTypeMap[controlType] + ")" 164 | 165 | case 2: 166 | controlType = child.Children[0].Value.(string) 167 | child.Children[0].Description = "Control Type (" + ControlTypeMap[controlType] + ")" 168 | // Children[1] could be criticality or value (both are optional) 169 | // duck-type on whether this is a boolean 170 | if _, ok := child.Children[1].Value.(bool); ok { 171 | child.Children[1].Description = "Criticality" 172 | } else { 173 | child.Children[1].Description = "Control Value" 174 | value = child.Children[1] 175 | } 176 | 177 | case 3: 178 | // criticality and value present 179 | controlType = child.Children[0].Value.(string) 180 | child.Children[0].Description = "Control Type (" + ControlTypeMap[controlType] + ")" 181 | child.Children[1].Description = "Criticality" 182 | child.Children[2].Description = "Control Value" 183 | value = child.Children[2] 184 | 185 | default: 186 | // more than 3 children is invalid 187 | continue 188 | } 189 | if value == nil { 190 | continue 191 | } 192 | switch controlType { 193 | case ControlTypePaging: 194 | value.Description += " (Paging)" 195 | if value.Value != nil { 196 | valueChildren := ber.DecodePacket(value.Data.Bytes()) 197 | value.Data.Truncate(0) 198 | value.Value = nil 199 | valueChildren.Children[1].Value = valueChildren.Children[1].Data.Bytes() 200 | value.AppendChild(valueChildren) 201 | } 202 | value.Children[0].Description = "Real Search Control Value" 203 | value.Children[0].Children[0].Description = "Paging Size" 204 | value.Children[0].Children[1].Description = "Cookie" 205 | 206 | case ControlTypeBeheraPasswordPolicy: 207 | value.Description += " (Password Policy - Behera Draft)" 208 | if value.Value != nil { 209 | valueChildren := ber.DecodePacket(value.Data.Bytes()) 210 | value.Data.Truncate(0) 211 | value.Value = nil 212 | value.AppendChild(valueChildren) 213 | } 214 | sequence := value.Children[0] 215 | for _, child := range sequence.Children { 216 | if child.Tag == 0 { 217 | //Warning 218 | warningPacket := child.Children[0] 219 | packet := ber.DecodePacket(warningPacket.Data.Bytes()) 220 | val, ok := packet.Value.(int64) 221 | if ok { 222 | if warningPacket.Tag == 0 { 223 | //timeBeforeExpiration 224 | value.Description += " (TimeBeforeExpiration)" 225 | warningPacket.Value = val 226 | } else if warningPacket.Tag == 1 { 227 | //graceAuthNsRemaining 228 | value.Description += " (GraceAuthNsRemaining)" 229 | warningPacket.Value = val 230 | } 231 | } 232 | } else if child.Tag == 1 { 233 | // Error 234 | packet := ber.DecodePacket(child.Data.Bytes()) 235 | val, ok := packet.Value.(int8) 236 | if !ok { 237 | val = -1 238 | } 239 | child.Description = "Error" 240 | child.Value = val 241 | } 242 | } 243 | } 244 | } 245 | } 246 | 247 | func addRequestDescriptions(packet *ber.Packet) { 248 | packet.Description = "LDAP Request" 249 | packet.Children[0].Description = "Message ID" 250 | packet.Children[1].Description = ApplicationMap[uint8(packet.Children[1].Tag)] 251 | if len(packet.Children) == 3 { 252 | addControlDescriptions(packet.Children[2]) 253 | } 254 | } 255 | 256 | func addDefaultLDAPResponseDescriptions(packet *ber.Packet) { 257 | resultCode, _ := getLDAPResultCode(packet) 258 | packet.Children[1].Children[0].Description = "Result Code (" + LDAPResultCodeMap[resultCode] + ")" 259 | packet.Children[1].Children[1].Description = "Matched DN" 260 | packet.Children[1].Children[2].Description = "Error Message" 261 | if len(packet.Children[1].Children) > 3 { 262 | packet.Children[1].Children[3].Description = "Referral" 263 | } 264 | if len(packet.Children) == 3 { 265 | addControlDescriptions(packet.Children[2]) 266 | } 267 | } 268 | 269 | // DebugBinaryFile reads and prints packets from the given filename 270 | func DebugBinaryFile(fileName string) error { 271 | file, err := ioutil.ReadFile(fileName) 272 | if err != nil { 273 | return NewError(ErrorDebugging, err) 274 | } 275 | ber.PrintBytes(os.Stdout, file, "") 276 | packet := ber.DecodePacket(file) 277 | addLDAPDescriptions(packet) 278 | ber.PrintPacket(packet) 279 | 280 | return nil 281 | } 282 | 283 | var hex = "0123456789abcdef" 284 | 285 | func mustEscape(c byte) bool { 286 | return c > 0x7f || c == '(' || c == ')' || c == '\\' || c == '*' || c == 0 287 | } 288 | 289 | // EscapeFilter escapes from the provided LDAP filter string the special 290 | // characters in the set `()*\` and those out of the range 0 < c < 0x80, 291 | // as defined in RFC4515. 292 | func EscapeFilter(filter string) string { 293 | escape := 0 294 | for i := 0; i < len(filter); i++ { 295 | if mustEscape(filter[i]) { 296 | escape++ 297 | } 298 | } 299 | if escape == 0 { 300 | return filter 301 | } 302 | buf := make([]byte, len(filter)+escape*2) 303 | for i, j := 0, 0; i < len(filter); i++ { 304 | c := filter[i] 305 | if mustEscape(c) { 306 | buf[j+0] = '\\' 307 | buf[j+1] = hex[c>>4] 308 | buf[j+2] = hex[c&0xf] 309 | j += 3 310 | } else { 311 | buf[j] = c 312 | j++ 313 | } 314 | } 315 | return string(buf) 316 | } 317 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "net" 9 | "sync" 10 | "sync/atomic" 11 | "time" 12 | 13 | "gopkg.in/asn1-ber.v1" 14 | ) 15 | 16 | const ( 17 | // MessageQuit causes the processMessages loop to exit 18 | MessageQuit = 0 19 | // MessageRequest sends a request to the server 20 | MessageRequest = 1 21 | // MessageResponse receives a response from the server 22 | MessageResponse = 2 23 | // MessageFinish indicates the client considers a particular message ID to be finished 24 | MessageFinish = 3 25 | // MessageTimeout indicates the client-specified timeout for a particular message ID has been reached 26 | MessageTimeout = 4 27 | ) 28 | 29 | // PacketResponse contains the packet or error encountered reading a response 30 | type PacketResponse struct { 31 | // Packet is the packet read from the server 32 | Packet *ber.Packet 33 | // Error is an error encountered while reading 34 | Error error 35 | } 36 | 37 | // ReadPacket returns the packet or an error 38 | func (pr *PacketResponse) ReadPacket() (*ber.Packet, error) { 39 | if (pr == nil) || (pr.Packet == nil && pr.Error == nil) { 40 | return nil, NewError(ErrorNetwork, errors.New("ldap: could not retrieve response")) 41 | } 42 | return pr.Packet, pr.Error 43 | } 44 | 45 | type messageContext struct { 46 | id int64 47 | // close(done) should only be called from finishMessage() 48 | done chan struct{} 49 | // close(responses) should only be called from processMessages(), and only sent to from sendResponse() 50 | responses chan *PacketResponse 51 | } 52 | 53 | // sendResponse should only be called within the processMessages() loop which 54 | // is also responsible for closing the responses channel. 55 | func (msgCtx *messageContext) sendResponse(packet *PacketResponse) { 56 | select { 57 | case msgCtx.responses <- packet: 58 | // Successfully sent packet to message handler. 59 | case <-msgCtx.done: 60 | // The request handler is done and will not receive more 61 | // packets. 62 | } 63 | } 64 | 65 | type messagePacket struct { 66 | Op int 67 | MessageID int64 68 | Packet *ber.Packet 69 | Context *messageContext 70 | } 71 | 72 | type sendMessageFlags uint 73 | 74 | const ( 75 | startTLS sendMessageFlags = 1 << iota 76 | ) 77 | 78 | // Conn represents an LDAP Connection 79 | type Conn struct { 80 | conn net.Conn 81 | isTLS bool 82 | closing uint32 83 | closeErr atomicValue 84 | isStartingTLS bool 85 | Debug debugging 86 | chanConfirm chan struct{} 87 | messageContexts map[int64]*messageContext 88 | chanMessage chan *messagePacket 89 | chanMessageID chan int64 90 | wgClose sync.WaitGroup 91 | outstandingRequests uint 92 | messageMutex sync.Mutex 93 | requestTimeout int64 94 | } 95 | 96 | var _ Client = &Conn{} 97 | 98 | // DefaultTimeout is a package-level variable that sets the timeout value 99 | // used for the Dial and DialTLS methods. 100 | // 101 | // WARNING: since this is a package-level variable, setting this value from 102 | // multiple places will probably result in undesired behaviour. 103 | var DefaultTimeout = 60 * time.Second 104 | 105 | // Dial connects to the given address on the given network using net.Dial 106 | // and then returns a new Conn for the connection. 107 | func Dial(network, addr string) (*Conn, error) { 108 | c, err := net.DialTimeout(network, addr, DefaultTimeout) 109 | if err != nil { 110 | return nil, NewError(ErrorNetwork, err) 111 | } 112 | conn := NewConn(c, false) 113 | conn.Start() 114 | return conn, nil 115 | } 116 | 117 | // DialTLS connects to the given address on the given network using tls.Dial 118 | // and then returns a new Conn for the connection. 119 | func DialTLS(network, addr string, config *tls.Config) (*Conn, error) { 120 | dc, err := net.DialTimeout(network, addr, DefaultTimeout) 121 | if err != nil { 122 | return nil, NewError(ErrorNetwork, err) 123 | } 124 | c := tls.Client(dc, config) 125 | err = c.Handshake() 126 | if err != nil { 127 | // Handshake error, close the established connection before we return an error 128 | dc.Close() 129 | return nil, NewError(ErrorNetwork, err) 130 | } 131 | conn := NewConn(c, true) 132 | conn.Start() 133 | return conn, nil 134 | } 135 | 136 | // NewConn returns a new Conn using conn for network I/O. 137 | func NewConn(conn net.Conn, isTLS bool) *Conn { 138 | return &Conn{ 139 | conn: conn, 140 | chanConfirm: make(chan struct{}), 141 | chanMessageID: make(chan int64), 142 | chanMessage: make(chan *messagePacket, 10), 143 | messageContexts: map[int64]*messageContext{}, 144 | requestTimeout: 0, 145 | isTLS: isTLS, 146 | } 147 | } 148 | 149 | // Start initializes goroutines to read responses and process messages 150 | func (l *Conn) Start() { 151 | go l.reader() 152 | go l.processMessages() 153 | l.wgClose.Add(1) 154 | } 155 | 156 | // isClosing returns whether or not we're currently closing. 157 | func (l *Conn) isClosing() bool { 158 | return atomic.LoadUint32(&l.closing) == 1 159 | } 160 | 161 | // setClosing sets the closing value to true 162 | func (l *Conn) setClosing() bool { 163 | return atomic.CompareAndSwapUint32(&l.closing, 0, 1) 164 | } 165 | 166 | // Close closes the connection. 167 | func (l *Conn) Close() { 168 | l.messageMutex.Lock() 169 | defer l.messageMutex.Unlock() 170 | 171 | if l.setClosing() { 172 | l.Debug.Printf("Sending quit message and waiting for confirmation") 173 | l.chanMessage <- &messagePacket{Op: MessageQuit} 174 | <-l.chanConfirm 175 | close(l.chanMessage) 176 | 177 | l.Debug.Printf("Closing network connection") 178 | if err := l.conn.Close(); err != nil { 179 | log.Println(err) 180 | } 181 | 182 | l.wgClose.Done() 183 | } 184 | l.wgClose.Wait() 185 | } 186 | 187 | // SetTimeout sets the time after a request is sent that a MessageTimeout triggers 188 | func (l *Conn) SetTimeout(timeout time.Duration) { 189 | if timeout > 0 { 190 | atomic.StoreInt64(&l.requestTimeout, int64(timeout)) 191 | } 192 | } 193 | 194 | // Returns the next available messageID 195 | func (l *Conn) nextMessageID() int64 { 196 | if messageID, ok := <-l.chanMessageID; ok { 197 | return messageID 198 | } 199 | return 0 200 | } 201 | 202 | // StartTLS sends the command to start a TLS session and then creates a new TLS Client 203 | func (l *Conn) StartTLS(config *tls.Config) error { 204 | if l.isTLS { 205 | return NewError(ErrorNetwork, errors.New("ldap: already encrypted")) 206 | } 207 | 208 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 209 | packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) 210 | request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Start TLS") 211 | request.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, "1.3.6.1.4.1.1466.20037", "TLS Extended Command")) 212 | packet.AppendChild(request) 213 | l.Debug.PrintPacket(packet) 214 | 215 | msgCtx, err := l.sendMessageWithFlags(packet, startTLS) 216 | if err != nil { 217 | return err 218 | } 219 | defer l.finishMessage(msgCtx) 220 | 221 | l.Debug.Printf("%d: waiting for response", msgCtx.id) 222 | 223 | packetResponse, ok := <-msgCtx.responses 224 | if !ok { 225 | return NewError(ErrorNetwork, errors.New("ldap: response channel closed")) 226 | } 227 | packet, err = packetResponse.ReadPacket() 228 | l.Debug.Printf("%d: got response %p", msgCtx.id, packet) 229 | if err != nil { 230 | return err 231 | } 232 | 233 | if l.Debug { 234 | if err := addLDAPDescriptions(packet); err != nil { 235 | l.Close() 236 | return err 237 | } 238 | ber.PrintPacket(packet) 239 | } 240 | 241 | if resultCode, message := getLDAPResultCode(packet); resultCode == LDAPResultSuccess { 242 | conn := tls.Client(l.conn, config) 243 | 244 | if err := conn.Handshake(); err != nil { 245 | l.Close() 246 | return NewError(ErrorNetwork, fmt.Errorf("TLS handshake failed (%v)", err)) 247 | } 248 | 249 | l.isTLS = true 250 | l.conn = conn 251 | } else { 252 | return NewError(resultCode, fmt.Errorf("ldap: cannot StartTLS (%s)", message)) 253 | } 254 | go l.reader() 255 | 256 | return nil 257 | } 258 | 259 | func (l *Conn) sendMessage(packet *ber.Packet) (*messageContext, error) { 260 | return l.sendMessageWithFlags(packet, 0) 261 | } 262 | 263 | func (l *Conn) sendMessageWithFlags(packet *ber.Packet, flags sendMessageFlags) (*messageContext, error) { 264 | if l.isClosing() { 265 | return nil, NewError(ErrorNetwork, errors.New("ldap: connection closed")) 266 | } 267 | l.messageMutex.Lock() 268 | l.Debug.Printf("flags&startTLS = %d", flags&startTLS) 269 | if l.isStartingTLS { 270 | l.messageMutex.Unlock() 271 | return nil, NewError(ErrorNetwork, errors.New("ldap: connection is in startls phase")) 272 | } 273 | if flags&startTLS != 0 { 274 | if l.outstandingRequests != 0 { 275 | l.messageMutex.Unlock() 276 | return nil, NewError(ErrorNetwork, errors.New("ldap: cannot StartTLS with outstanding requests")) 277 | } 278 | l.isStartingTLS = true 279 | } 280 | l.outstandingRequests++ 281 | 282 | l.messageMutex.Unlock() 283 | 284 | responses := make(chan *PacketResponse) 285 | messageID := packet.Children[0].Value.(int64) 286 | message := &messagePacket{ 287 | Op: MessageRequest, 288 | MessageID: messageID, 289 | Packet: packet, 290 | Context: &messageContext{ 291 | id: messageID, 292 | done: make(chan struct{}), 293 | responses: responses, 294 | }, 295 | } 296 | l.sendProcessMessage(message) 297 | return message.Context, nil 298 | } 299 | 300 | func (l *Conn) finishMessage(msgCtx *messageContext) { 301 | close(msgCtx.done) 302 | 303 | if l.isClosing() { 304 | return 305 | } 306 | 307 | l.messageMutex.Lock() 308 | l.outstandingRequests-- 309 | if l.isStartingTLS { 310 | l.isStartingTLS = false 311 | } 312 | l.messageMutex.Unlock() 313 | 314 | message := &messagePacket{ 315 | Op: MessageFinish, 316 | MessageID: msgCtx.id, 317 | } 318 | l.sendProcessMessage(message) 319 | } 320 | 321 | func (l *Conn) sendProcessMessage(message *messagePacket) bool { 322 | l.messageMutex.Lock() 323 | defer l.messageMutex.Unlock() 324 | if l.isClosing() { 325 | return false 326 | } 327 | l.chanMessage <- message 328 | return true 329 | } 330 | 331 | func (l *Conn) processMessages() { 332 | defer func() { 333 | if err := recover(); err != nil { 334 | log.Printf("ldap: recovered panic in processMessages: %v", err) 335 | } 336 | for messageID, msgCtx := range l.messageContexts { 337 | // If we are closing due to an error, inform anyone who 338 | // is waiting about the error. 339 | if l.isClosing() && l.closeErr.Load() != nil { 340 | msgCtx.sendResponse(&PacketResponse{Error: l.closeErr.Load().(error)}) 341 | } 342 | l.Debug.Printf("Closing channel for MessageID %d", messageID) 343 | close(msgCtx.responses) 344 | delete(l.messageContexts, messageID) 345 | } 346 | close(l.chanMessageID) 347 | close(l.chanConfirm) 348 | }() 349 | 350 | var messageID int64 = 1 351 | for { 352 | select { 353 | case l.chanMessageID <- messageID: 354 | messageID++ 355 | case message := <-l.chanMessage: 356 | switch message.Op { 357 | case MessageQuit: 358 | l.Debug.Printf("Shutting down - quit message received") 359 | return 360 | case MessageRequest: 361 | // Add to message list and write to network 362 | l.Debug.Printf("Sending message %d", message.MessageID) 363 | 364 | buf := message.Packet.Bytes() 365 | _, err := l.conn.Write(buf) 366 | if err != nil { 367 | l.Debug.Printf("Error Sending Message: %s", err.Error()) 368 | message.Context.sendResponse(&PacketResponse{Error: fmt.Errorf("unable to send request: %s", err)}) 369 | close(message.Context.responses) 370 | break 371 | } 372 | 373 | // Only add to messageContexts if we were able to 374 | // successfully write the message. 375 | l.messageContexts[message.MessageID] = message.Context 376 | 377 | // Add timeout if defined 378 | requestTimeout := time.Duration(atomic.LoadInt64(&l.requestTimeout)) 379 | if requestTimeout > 0 { 380 | go func() { 381 | defer func() { 382 | if err := recover(); err != nil { 383 | log.Printf("ldap: recovered panic in RequestTimeout: %v", err) 384 | } 385 | }() 386 | time.Sleep(requestTimeout) 387 | timeoutMessage := &messagePacket{ 388 | Op: MessageTimeout, 389 | MessageID: message.MessageID, 390 | } 391 | l.sendProcessMessage(timeoutMessage) 392 | }() 393 | } 394 | case MessageResponse: 395 | l.Debug.Printf("Receiving message %d", message.MessageID) 396 | if msgCtx, ok := l.messageContexts[message.MessageID]; ok { 397 | msgCtx.sendResponse(&PacketResponse{message.Packet, nil}) 398 | } else { 399 | log.Printf("Received unexpected message %d, %v", message.MessageID, l.isClosing()) 400 | ber.PrintPacket(message.Packet) 401 | } 402 | case MessageTimeout: 403 | // Handle the timeout by closing the channel 404 | // All reads will return immediately 405 | if msgCtx, ok := l.messageContexts[message.MessageID]; ok { 406 | l.Debug.Printf("Receiving message timeout for %d", message.MessageID) 407 | msgCtx.sendResponse(&PacketResponse{message.Packet, errors.New("ldap: connection timed out")}) 408 | delete(l.messageContexts, message.MessageID) 409 | close(msgCtx.responses) 410 | } 411 | case MessageFinish: 412 | l.Debug.Printf("Finished message %d", message.MessageID) 413 | if msgCtx, ok := l.messageContexts[message.MessageID]; ok { 414 | delete(l.messageContexts, message.MessageID) 415 | close(msgCtx.responses) 416 | } 417 | } 418 | } 419 | } 420 | } 421 | 422 | func (l *Conn) reader() { 423 | cleanstop := false 424 | defer func() { 425 | if err := recover(); err != nil { 426 | log.Printf("ldap: recovered panic in reader: %v", err) 427 | } 428 | if !cleanstop { 429 | l.Close() 430 | } 431 | }() 432 | 433 | for { 434 | if cleanstop { 435 | l.Debug.Printf("reader clean stopping (without closing the connection)") 436 | return 437 | } 438 | packet, err := ber.ReadPacket(l.conn) 439 | if err != nil { 440 | // A read error is expected here if we are closing the connection... 441 | if !l.isClosing() { 442 | l.closeErr.Store(fmt.Errorf("unable to read LDAP response packet: %s", err)) 443 | l.Debug.Printf("reader error: %s", err.Error()) 444 | } 445 | return 446 | } 447 | addLDAPDescriptions(packet) 448 | if len(packet.Children) == 0 { 449 | l.Debug.Printf("Received bad ldap packet") 450 | continue 451 | } 452 | l.messageMutex.Lock() 453 | if l.isStartingTLS { 454 | cleanstop = true 455 | } 456 | l.messageMutex.Unlock() 457 | message := &messagePacket{ 458 | Op: MessageResponse, 459 | MessageID: packet.Children[0].Value.(int64), 460 | Packet: packet, 461 | } 462 | if !l.sendProcessMessage(message) { 463 | return 464 | } 465 | } 466 | } 467 | -------------------------------------------------------------------------------- /control.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "gopkg.in/asn1-ber.v1" 8 | ) 9 | 10 | const ( 11 | // ControlTypePaging - https://www.ietf.org/rfc/rfc2696.txt 12 | ControlTypePaging = "1.2.840.113556.1.4.319" 13 | // ControlTypeBeheraPasswordPolicy - https://tools.ietf.org/html/draft-behera-ldap-password-policy-10 14 | ControlTypeBeheraPasswordPolicy = "1.3.6.1.4.1.42.2.27.8.5.1" 15 | // ControlTypeVChuPasswordMustChange - https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00 16 | ControlTypeVChuPasswordMustChange = "2.16.840.1.113730.3.4.4" 17 | // ControlTypeVChuPasswordWarning - https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00 18 | ControlTypeVChuPasswordWarning = "2.16.840.1.113730.3.4.5" 19 | // ControlTypeManageDsaIT - https://tools.ietf.org/html/rfc3296 20 | ControlTypeManageDsaIT = "2.16.840.1.113730.3.4.2" 21 | ) 22 | 23 | // ControlTypeMap maps controls to text descriptions 24 | var ControlTypeMap = map[string]string{ 25 | ControlTypePaging: "Paging", 26 | ControlTypeBeheraPasswordPolicy: "Password Policy - Behera Draft", 27 | ControlTypeManageDsaIT: "Manage DSA IT", 28 | } 29 | 30 | // Control defines an interface controls provide to encode and describe themselves 31 | type Control interface { 32 | // GetControlType returns the OID 33 | GetControlType() string 34 | // Encode returns the ber packet representation 35 | Encode() *ber.Packet 36 | // String returns a human-readable description 37 | String() string 38 | } 39 | 40 | // ControlString implements the Control interface for simple controls 41 | type ControlString struct { 42 | ControlType string 43 | Criticality bool 44 | ControlValue string 45 | } 46 | 47 | // GetControlType returns the OID 48 | func (c *ControlString) GetControlType() string { 49 | return c.ControlType 50 | } 51 | 52 | // Encode returns the ber packet representation 53 | func (c *ControlString) Encode() *ber.Packet { 54 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") 55 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, c.ControlType, "Control Type ("+ControlTypeMap[c.ControlType]+")")) 56 | if c.Criticality { 57 | packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) 58 | } 59 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, string(c.ControlValue), "Control Value")) 60 | return packet 61 | } 62 | 63 | // String returns a human-readable description 64 | func (c *ControlString) String() string { 65 | return fmt.Sprintf("Control Type: %s (%q) Criticality: %t Control Value: %s", ControlTypeMap[c.ControlType], c.ControlType, c.Criticality, c.ControlValue) 66 | } 67 | 68 | // ControlPaging implements the paging control described in https://www.ietf.org/rfc/rfc2696.txt 69 | type ControlPaging struct { 70 | // PagingSize indicates the page size 71 | PagingSize uint32 72 | // Cookie is an opaque value returned by the server to track a paging cursor 73 | Cookie []byte 74 | } 75 | 76 | // GetControlType returns the OID 77 | func (c *ControlPaging) GetControlType() string { 78 | return ControlTypePaging 79 | } 80 | 81 | // Encode returns the ber packet representation 82 | func (c *ControlPaging) Encode() *ber.Packet { 83 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") 84 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypePaging, "Control Type ("+ControlTypeMap[ControlTypePaging]+")")) 85 | 86 | p2 := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (Paging)") 87 | seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Search Control Value") 88 | seq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(c.PagingSize), "Paging Size")) 89 | cookie := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Cookie") 90 | cookie.Value = c.Cookie 91 | cookie.Data.Write(c.Cookie) 92 | seq.AppendChild(cookie) 93 | p2.AppendChild(seq) 94 | 95 | packet.AppendChild(p2) 96 | return packet 97 | } 98 | 99 | // String returns a human-readable description 100 | func (c *ControlPaging) String() string { 101 | return fmt.Sprintf( 102 | "Control Type: %s (%q) Criticality: %t PagingSize: %d Cookie: %q", 103 | ControlTypeMap[ControlTypePaging], 104 | ControlTypePaging, 105 | false, 106 | c.PagingSize, 107 | c.Cookie) 108 | } 109 | 110 | // SetCookie stores the given cookie in the paging control 111 | func (c *ControlPaging) SetCookie(cookie []byte) { 112 | c.Cookie = cookie 113 | } 114 | 115 | // ControlBeheraPasswordPolicy implements the control described in https://tools.ietf.org/html/draft-behera-ldap-password-policy-10 116 | type ControlBeheraPasswordPolicy struct { 117 | // Expire contains the number of seconds before a password will expire 118 | Expire int64 119 | // Grace indicates the remaining number of times a user will be allowed to authenticate with an expired password 120 | Grace int64 121 | // Error indicates the error code 122 | Error int8 123 | // ErrorString is a human readable error 124 | ErrorString string 125 | } 126 | 127 | // GetControlType returns the OID 128 | func (c *ControlBeheraPasswordPolicy) GetControlType() string { 129 | return ControlTypeBeheraPasswordPolicy 130 | } 131 | 132 | // Encode returns the ber packet representation 133 | func (c *ControlBeheraPasswordPolicy) Encode() *ber.Packet { 134 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") 135 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeBeheraPasswordPolicy, "Control Type ("+ControlTypeMap[ControlTypeBeheraPasswordPolicy]+")")) 136 | 137 | return packet 138 | } 139 | 140 | // String returns a human-readable description 141 | func (c *ControlBeheraPasswordPolicy) String() string { 142 | return fmt.Sprintf( 143 | "Control Type: %s (%q) Criticality: %t Expire: %d Grace: %d Error: %d, ErrorString: %s", 144 | ControlTypeMap[ControlTypeBeheraPasswordPolicy], 145 | ControlTypeBeheraPasswordPolicy, 146 | false, 147 | c.Expire, 148 | c.Grace, 149 | c.Error, 150 | c.ErrorString) 151 | } 152 | 153 | // ControlVChuPasswordMustChange implements the control described in https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00 154 | type ControlVChuPasswordMustChange struct { 155 | // MustChange indicates if the password is required to be changed 156 | MustChange bool 157 | } 158 | 159 | // GetControlType returns the OID 160 | func (c *ControlVChuPasswordMustChange) GetControlType() string { 161 | return ControlTypeVChuPasswordMustChange 162 | } 163 | 164 | // Encode returns the ber packet representation 165 | func (c *ControlVChuPasswordMustChange) Encode() *ber.Packet { 166 | return nil 167 | } 168 | 169 | // String returns a human-readable description 170 | func (c *ControlVChuPasswordMustChange) String() string { 171 | return fmt.Sprintf( 172 | "Control Type: %s (%q) Criticality: %t MustChange: %v", 173 | ControlTypeMap[ControlTypeVChuPasswordMustChange], 174 | ControlTypeVChuPasswordMustChange, 175 | false, 176 | c.MustChange) 177 | } 178 | 179 | // ControlVChuPasswordWarning implements the control described in https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00 180 | type ControlVChuPasswordWarning struct { 181 | // Expire indicates the time in seconds until the password expires 182 | Expire int64 183 | } 184 | 185 | // GetControlType returns the OID 186 | func (c *ControlVChuPasswordWarning) GetControlType() string { 187 | return ControlTypeVChuPasswordWarning 188 | } 189 | 190 | // Encode returns the ber packet representation 191 | func (c *ControlVChuPasswordWarning) Encode() *ber.Packet { 192 | return nil 193 | } 194 | 195 | // String returns a human-readable description 196 | func (c *ControlVChuPasswordWarning) String() string { 197 | return fmt.Sprintf( 198 | "Control Type: %s (%q) Criticality: %t Expire: %b", 199 | ControlTypeMap[ControlTypeVChuPasswordWarning], 200 | ControlTypeVChuPasswordWarning, 201 | false, 202 | c.Expire) 203 | } 204 | 205 | // ControlManageDsaIT implements the control described in https://tools.ietf.org/html/rfc3296 206 | type ControlManageDsaIT struct { 207 | // Criticality indicates if this control is required 208 | Criticality bool 209 | } 210 | 211 | // GetControlType returns the OID 212 | func (c *ControlManageDsaIT) GetControlType() string { 213 | return ControlTypeManageDsaIT 214 | } 215 | 216 | // Encode returns the ber packet representation 217 | func (c *ControlManageDsaIT) Encode() *ber.Packet { 218 | //FIXME 219 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") 220 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeManageDsaIT, "Control Type ("+ControlTypeMap[ControlTypeManageDsaIT]+")")) 221 | if c.Criticality { 222 | packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) 223 | } 224 | return packet 225 | } 226 | 227 | // String returns a human-readable description 228 | func (c *ControlManageDsaIT) String() string { 229 | return fmt.Sprintf( 230 | "Control Type: %s (%q) Criticality: %t", 231 | ControlTypeMap[ControlTypeManageDsaIT], 232 | ControlTypeManageDsaIT, 233 | c.Criticality) 234 | } 235 | 236 | // NewControlManageDsaIT returns a ControlManageDsaIT control 237 | func NewControlManageDsaIT(Criticality bool) *ControlManageDsaIT { 238 | return &ControlManageDsaIT{Criticality: Criticality} 239 | } 240 | 241 | // FindControl returns the first control of the given type in the list, or nil 242 | func FindControl(controls []Control, controlType string) Control { 243 | for _, c := range controls { 244 | if c.GetControlType() == controlType { 245 | return c 246 | } 247 | } 248 | return nil 249 | } 250 | 251 | // DecodeControl returns a control read from the given packet, or nil if no recognized control can be made 252 | func DecodeControl(packet *ber.Packet) Control { 253 | var ( 254 | ControlType = "" 255 | Criticality = false 256 | value *ber.Packet 257 | ) 258 | 259 | switch len(packet.Children) { 260 | case 0: 261 | // at least one child is required for control type 262 | return nil 263 | 264 | case 1: 265 | // just type, no criticality or value 266 | packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")" 267 | ControlType = packet.Children[0].Value.(string) 268 | 269 | case 2: 270 | packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")" 271 | ControlType = packet.Children[0].Value.(string) 272 | 273 | // Children[1] could be criticality or value (both are optional) 274 | // duck-type on whether this is a boolean 275 | if _, ok := packet.Children[1].Value.(bool); ok { 276 | packet.Children[1].Description = "Criticality" 277 | Criticality = packet.Children[1].Value.(bool) 278 | } else { 279 | packet.Children[1].Description = "Control Value" 280 | value = packet.Children[1] 281 | } 282 | 283 | case 3: 284 | packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")" 285 | ControlType = packet.Children[0].Value.(string) 286 | 287 | packet.Children[1].Description = "Criticality" 288 | Criticality = packet.Children[1].Value.(bool) 289 | 290 | packet.Children[2].Description = "Control Value" 291 | value = packet.Children[2] 292 | 293 | default: 294 | // more than 3 children is invalid 295 | return nil 296 | } 297 | 298 | switch ControlType { 299 | case ControlTypeManageDsaIT: 300 | return NewControlManageDsaIT(Criticality) 301 | case ControlTypePaging: 302 | value.Description += " (Paging)" 303 | c := new(ControlPaging) 304 | if value.Value != nil { 305 | valueChildren := ber.DecodePacket(value.Data.Bytes()) 306 | value.Data.Truncate(0) 307 | value.Value = nil 308 | value.AppendChild(valueChildren) 309 | } 310 | value = value.Children[0] 311 | value.Description = "Search Control Value" 312 | value.Children[0].Description = "Paging Size" 313 | value.Children[1].Description = "Cookie" 314 | c.PagingSize = uint32(value.Children[0].Value.(int64)) 315 | c.Cookie = value.Children[1].Data.Bytes() 316 | value.Children[1].Value = c.Cookie 317 | return c 318 | case ControlTypeBeheraPasswordPolicy: 319 | value.Description += " (Password Policy - Behera)" 320 | c := NewControlBeheraPasswordPolicy() 321 | if value.Value != nil { 322 | valueChildren := ber.DecodePacket(value.Data.Bytes()) 323 | value.Data.Truncate(0) 324 | value.Value = nil 325 | value.AppendChild(valueChildren) 326 | } 327 | 328 | sequence := value.Children[0] 329 | 330 | for _, child := range sequence.Children { 331 | if child.Tag == 0 { 332 | //Warning 333 | warningPacket := child.Children[0] 334 | packet := ber.DecodePacket(warningPacket.Data.Bytes()) 335 | val, ok := packet.Value.(int64) 336 | if ok { 337 | if warningPacket.Tag == 0 { 338 | //timeBeforeExpiration 339 | c.Expire = val 340 | warningPacket.Value = c.Expire 341 | } else if warningPacket.Tag == 1 { 342 | //graceAuthNsRemaining 343 | c.Grace = val 344 | warningPacket.Value = c.Grace 345 | } 346 | } 347 | } else if child.Tag == 1 { 348 | // Error 349 | packet := ber.DecodePacket(child.Data.Bytes()) 350 | val, ok := packet.Value.(int8) 351 | if !ok { 352 | // what to do? 353 | val = -1 354 | } 355 | c.Error = val 356 | child.Value = c.Error 357 | c.ErrorString = BeheraPasswordPolicyErrorMap[c.Error] 358 | } 359 | } 360 | return c 361 | case ControlTypeVChuPasswordMustChange: 362 | c := &ControlVChuPasswordMustChange{MustChange: true} 363 | return c 364 | case ControlTypeVChuPasswordWarning: 365 | c := &ControlVChuPasswordWarning{Expire: -1} 366 | expireStr := ber.DecodeString(value.Data.Bytes()) 367 | 368 | expire, err := strconv.ParseInt(expireStr, 10, 64) 369 | if err != nil { 370 | return nil 371 | } 372 | c.Expire = expire 373 | value.Value = c.Expire 374 | 375 | return c 376 | default: 377 | c := new(ControlString) 378 | c.ControlType = ControlType 379 | c.Criticality = Criticality 380 | if value != nil { 381 | c.ControlValue = value.Value.(string) 382 | } 383 | return c 384 | } 385 | } 386 | 387 | // NewControlString returns a generic control 388 | func NewControlString(controlType string, criticality bool, controlValue string) *ControlString { 389 | return &ControlString{ 390 | ControlType: controlType, 391 | Criticality: criticality, 392 | ControlValue: controlValue, 393 | } 394 | } 395 | 396 | // NewControlPaging returns a paging control 397 | func NewControlPaging(pagingSize uint32) *ControlPaging { 398 | return &ControlPaging{PagingSize: pagingSize} 399 | } 400 | 401 | // NewControlBeheraPasswordPolicy returns a ControlBeheraPasswordPolicy 402 | func NewControlBeheraPasswordPolicy() *ControlBeheraPasswordPolicy { 403 | return &ControlBeheraPasswordPolicy{ 404 | Expire: -1, 405 | Grace: -1, 406 | Error: -1, 407 | } 408 | } 409 | 410 | func encodeControls(controls []Control) *ber.Packet { 411 | packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0, nil, "Controls") 412 | for _, control := range controls { 413 | packet.AppendChild(control.Encode()) 414 | } 415 | return packet 416 | } 417 | -------------------------------------------------------------------------------- /search.go: -------------------------------------------------------------------------------- 1 | // File contains Search functionality 2 | // 3 | // https://tools.ietf.org/html/rfc4511 4 | // 5 | // SearchRequest ::= [APPLICATION 3] SEQUENCE { 6 | // baseObject LDAPDN, 7 | // scope ENUMERATED { 8 | // baseObject (0), 9 | // singleLevel (1), 10 | // wholeSubtree (2), 11 | // ... }, 12 | // derefAliases ENUMERATED { 13 | // neverDerefAliases (0), 14 | // derefInSearching (1), 15 | // derefFindingBaseObj (2), 16 | // derefAlways (3) }, 17 | // sizeLimit INTEGER (0 .. maxInt), 18 | // timeLimit INTEGER (0 .. maxInt), 19 | // typesOnly BOOLEAN, 20 | // filter Filter, 21 | // attributes AttributeSelection } 22 | // 23 | // AttributeSelection ::= SEQUENCE OF selector LDAPString 24 | // -- The LDAPString is constrained to 25 | // -- in Section 4.5.1.8 26 | // 27 | // Filter ::= CHOICE { 28 | // and [0] SET SIZE (1..MAX) OF filter Filter, 29 | // or [1] SET SIZE (1..MAX) OF filter Filter, 30 | // not [2] Filter, 31 | // equalityMatch [3] AttributeValueAssertion, 32 | // substrings [4] SubstringFilter, 33 | // greaterOrEqual [5] AttributeValueAssertion, 34 | // lessOrEqual [6] AttributeValueAssertion, 35 | // present [7] AttributeDescription, 36 | // approxMatch [8] AttributeValueAssertion, 37 | // extensibleMatch [9] MatchingRuleAssertion, 38 | // ... } 39 | // 40 | // SubstringFilter ::= SEQUENCE { 41 | // type AttributeDescription, 42 | // substrings SEQUENCE SIZE (1..MAX) OF substring CHOICE { 43 | // initial [0] AssertionValue, -- can occur at most once 44 | // any [1] AssertionValue, 45 | // final [2] AssertionValue } -- can occur at most once 46 | // } 47 | // 48 | // MatchingRuleAssertion ::= SEQUENCE { 49 | // matchingRule [1] MatchingRuleId OPTIONAL, 50 | // type [2] AttributeDescription OPTIONAL, 51 | // matchValue [3] AssertionValue, 52 | // dnAttributes [4] BOOLEAN DEFAULT FALSE } 53 | // 54 | // 55 | 56 | package ldap 57 | 58 | import ( 59 | "errors" 60 | "fmt" 61 | "sort" 62 | "strings" 63 | 64 | "gopkg.in/asn1-ber.v1" 65 | ) 66 | 67 | // scope choices 68 | const ( 69 | ScopeBaseObject = 0 70 | ScopeSingleLevel = 1 71 | ScopeWholeSubtree = 2 72 | ) 73 | 74 | // ScopeMap contains human readable descriptions of scope choices 75 | var ScopeMap = map[int]string{ 76 | ScopeBaseObject: "Base Object", 77 | ScopeSingleLevel: "Single Level", 78 | ScopeWholeSubtree: "Whole Subtree", 79 | } 80 | 81 | // derefAliases 82 | const ( 83 | NeverDerefAliases = 0 84 | DerefInSearching = 1 85 | DerefFindingBaseObj = 2 86 | DerefAlways = 3 87 | ) 88 | 89 | // DerefMap contains human readable descriptions of derefAliases choices 90 | var DerefMap = map[int]string{ 91 | NeverDerefAliases: "NeverDerefAliases", 92 | DerefInSearching: "DerefInSearching", 93 | DerefFindingBaseObj: "DerefFindingBaseObj", 94 | DerefAlways: "DerefAlways", 95 | } 96 | 97 | // NewEntry returns an Entry object with the specified distinguished name and attribute key-value pairs. 98 | // The map of attributes is accessed in alphabetical order of the keys in order to ensure that, for the 99 | // same input map of attributes, the output entry will contain the same order of attributes 100 | func NewEntry(dn string, attributes map[string][]string) *Entry { 101 | var attributeNames []string 102 | for attributeName := range attributes { 103 | attributeNames = append(attributeNames, attributeName) 104 | } 105 | sort.Strings(attributeNames) 106 | 107 | var encodedAttributes []*EntryAttribute 108 | for _, attributeName := range attributeNames { 109 | encodedAttributes = append(encodedAttributes, NewEntryAttribute(attributeName, attributes[attributeName])) 110 | } 111 | return &Entry{ 112 | DN: dn, 113 | Attributes: encodedAttributes, 114 | } 115 | } 116 | 117 | // Entry represents a single search result entry 118 | type Entry struct { 119 | // DN is the distinguished name of the entry 120 | DN string 121 | // Attributes are the returned attributes for the entry 122 | Attributes []*EntryAttribute 123 | } 124 | 125 | // GetAttributeValues returns the values for the named attribute, or an empty list 126 | func (e *Entry) GetAttributeValues(attribute string) []string { 127 | for _, attr := range e.Attributes { 128 | if attr.Name == attribute { 129 | return attr.Values 130 | } 131 | } 132 | return []string{} 133 | } 134 | 135 | // GetRawAttributeValues returns the byte values for the named attribute, or an empty list 136 | func (e *Entry) GetRawAttributeValues(attribute string) [][]byte { 137 | for _, attr := range e.Attributes { 138 | if attr.Name == attribute { 139 | return attr.ByteValues 140 | } 141 | } 142 | return [][]byte{} 143 | } 144 | 145 | // GetAttributeValue returns the first value for the named attribute, or "" 146 | func (e *Entry) GetAttributeValue(attribute string) string { 147 | values := e.GetAttributeValues(attribute) 148 | if len(values) == 0 { 149 | return "" 150 | } 151 | return values[0] 152 | } 153 | 154 | // GetRawAttributeValue returns the first value for the named attribute, or an empty slice 155 | func (e *Entry) GetRawAttributeValue(attribute string) []byte { 156 | values := e.GetRawAttributeValues(attribute) 157 | if len(values) == 0 { 158 | return []byte{} 159 | } 160 | return values[0] 161 | } 162 | 163 | // Print outputs a human-readable description 164 | func (e *Entry) Print() { 165 | fmt.Printf("DN: %s\n", e.DN) 166 | for _, attr := range e.Attributes { 167 | attr.Print() 168 | } 169 | } 170 | 171 | // PrettyPrint outputs a human-readable description indenting 172 | func (e *Entry) PrettyPrint(indent int) { 173 | fmt.Printf("%sDN: %s\n", strings.Repeat(" ", indent), e.DN) 174 | for _, attr := range e.Attributes { 175 | attr.PrettyPrint(indent + 2) 176 | } 177 | } 178 | 179 | // NewEntryAttribute returns a new EntryAttribute with the desired key-value pair 180 | func NewEntryAttribute(name string, values []string) *EntryAttribute { 181 | var bytes [][]byte 182 | for _, value := range values { 183 | bytes = append(bytes, []byte(value)) 184 | } 185 | return &EntryAttribute{ 186 | Name: name, 187 | Values: values, 188 | ByteValues: bytes, 189 | } 190 | } 191 | 192 | // EntryAttribute holds a single attribute 193 | type EntryAttribute struct { 194 | // Name is the name of the attribute 195 | Name string 196 | // Values contain the string values of the attribute 197 | Values []string 198 | // ByteValues contain the raw values of the attribute 199 | ByteValues [][]byte 200 | } 201 | 202 | // Print outputs a human-readable description 203 | func (e *EntryAttribute) Print() { 204 | fmt.Printf("%s: %s\n", e.Name, e.Values) 205 | } 206 | 207 | // PrettyPrint outputs a human-readable description with indenting 208 | func (e *EntryAttribute) PrettyPrint(indent int) { 209 | fmt.Printf("%s%s: %s\n", strings.Repeat(" ", indent), e.Name, e.Values) 210 | } 211 | 212 | // SearchResult holds the server's response to a search request 213 | type SearchResult struct { 214 | // Entries are the returned entries 215 | Entries []*Entry 216 | // Referrals are the returned referrals 217 | Referrals []string 218 | // Controls are the returned controls 219 | Controls []Control 220 | } 221 | 222 | // Print outputs a human-readable description 223 | func (s *SearchResult) Print() { 224 | for _, entry := range s.Entries { 225 | entry.Print() 226 | } 227 | } 228 | 229 | // PrettyPrint outputs a human-readable description with indenting 230 | func (s *SearchResult) PrettyPrint(indent int) { 231 | for _, entry := range s.Entries { 232 | entry.PrettyPrint(indent) 233 | } 234 | } 235 | 236 | // SearchRequest represents a search request to send to the server 237 | type SearchRequest struct { 238 | BaseDN string 239 | Scope int 240 | DerefAliases int 241 | SizeLimit int 242 | TimeLimit int 243 | TypesOnly bool 244 | Filter string 245 | Attributes []string 246 | Controls []Control 247 | } 248 | 249 | func (s *SearchRequest) encode() (*ber.Packet, error) { 250 | request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationSearchRequest, nil, "Search Request") 251 | request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, s.BaseDN, "Base DN")) 252 | request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(s.Scope), "Scope")) 253 | request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(s.DerefAliases), "Deref Aliases")) 254 | request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, uint64(s.SizeLimit), "Size Limit")) 255 | request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, uint64(s.TimeLimit), "Time Limit")) 256 | request.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, s.TypesOnly, "Types Only")) 257 | // compile and encode filter 258 | filterPacket, err := CompileFilter(s.Filter) 259 | if err != nil { 260 | return nil, err 261 | } 262 | request.AppendChild(filterPacket) 263 | // encode attributes 264 | attributesPacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes") 265 | for _, attribute := range s.Attributes { 266 | attributesPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute")) 267 | } 268 | request.AppendChild(attributesPacket) 269 | return request, nil 270 | } 271 | 272 | // NewSearchRequest creates a new search request 273 | func NewSearchRequest( 274 | BaseDN string, 275 | Scope, DerefAliases, SizeLimit, TimeLimit int, 276 | TypesOnly bool, 277 | Filter string, 278 | Attributes []string, 279 | Controls []Control, 280 | ) *SearchRequest { 281 | return &SearchRequest{ 282 | BaseDN: BaseDN, 283 | Scope: Scope, 284 | DerefAliases: DerefAliases, 285 | SizeLimit: SizeLimit, 286 | TimeLimit: TimeLimit, 287 | TypesOnly: TypesOnly, 288 | Filter: Filter, 289 | Attributes: Attributes, 290 | Controls: Controls, 291 | } 292 | } 293 | 294 | // SearchWithPaging accepts a search request and desired page size in order to execute LDAP queries to fulfill the 295 | // search request. All paged LDAP query responses will be buffered and the final result will be returned atomically. 296 | // The following four cases are possible given the arguments: 297 | // - given SearchRequest missing a control of type ControlTypePaging: we will add one with the desired paging size 298 | // - given SearchRequest contains a control of type ControlTypePaging that isn't actually a ControlPaging: fail without issuing any queries 299 | // - given SearchRequest contains a control of type ControlTypePaging with pagingSize equal to the size requested: no change to the search request 300 | // - given SearchRequest contains a control of type ControlTypePaging with pagingSize not equal to the size requested: fail without issuing any queries 301 | // A requested pagingSize of 0 is interpreted as no limit by LDAP servers. 302 | func (l *Conn) SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) { 303 | var pagingControl *ControlPaging 304 | 305 | control := FindControl(searchRequest.Controls, ControlTypePaging) 306 | if control == nil { 307 | pagingControl = NewControlPaging(pagingSize) 308 | searchRequest.Controls = append(searchRequest.Controls, pagingControl) 309 | } else { 310 | castControl, ok := control.(*ControlPaging) 311 | if !ok { 312 | return nil, fmt.Errorf("Expected paging control to be of type *ControlPaging, got %v", control) 313 | } 314 | if castControl.PagingSize != pagingSize { 315 | return nil, fmt.Errorf("Paging size given in search request (%d) conflicts with size given in search call (%d)", castControl.PagingSize, pagingSize) 316 | } 317 | pagingControl = castControl 318 | } 319 | 320 | searchResult := new(SearchResult) 321 | for { 322 | result, err := l.Search(searchRequest) 323 | l.Debug.Printf("Looking for Paging Control...") 324 | if err != nil { 325 | return searchResult, err 326 | } 327 | if result == nil { 328 | return searchResult, NewError(ErrorNetwork, errors.New("ldap: packet not received")) 329 | } 330 | 331 | for _, entry := range result.Entries { 332 | searchResult.Entries = append(searchResult.Entries, entry) 333 | } 334 | for _, referral := range result.Referrals { 335 | searchResult.Referrals = append(searchResult.Referrals, referral) 336 | } 337 | for _, control := range result.Controls { 338 | searchResult.Controls = append(searchResult.Controls, control) 339 | } 340 | 341 | l.Debug.Printf("Looking for Paging Control...") 342 | pagingResult := FindControl(result.Controls, ControlTypePaging) 343 | if pagingResult == nil { 344 | pagingControl = nil 345 | l.Debug.Printf("Could not find paging control. Breaking...") 346 | break 347 | } 348 | 349 | cookie := pagingResult.(*ControlPaging).Cookie 350 | if len(cookie) == 0 { 351 | pagingControl = nil 352 | l.Debug.Printf("Could not find cookie. Breaking...") 353 | break 354 | } 355 | pagingControl.SetCookie(cookie) 356 | } 357 | 358 | if pagingControl != nil { 359 | l.Debug.Printf("Abandoning Paging...") 360 | pagingControl.PagingSize = 0 361 | l.Search(searchRequest) 362 | } 363 | 364 | return searchResult, nil 365 | } 366 | 367 | // Search performs the given search request 368 | func (l *Conn) Search(searchRequest *SearchRequest) (*SearchResult, error) { 369 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 370 | packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) 371 | // encode search request 372 | encodedSearchRequest, err := searchRequest.encode() 373 | if err != nil { 374 | return nil, err 375 | } 376 | packet.AppendChild(encodedSearchRequest) 377 | // encode search controls 378 | if searchRequest.Controls != nil { 379 | packet.AppendChild(encodeControls(searchRequest.Controls)) 380 | } 381 | 382 | l.Debug.PrintPacket(packet) 383 | 384 | msgCtx, err := l.sendMessage(packet) 385 | if err != nil { 386 | return nil, err 387 | } 388 | defer l.finishMessage(msgCtx) 389 | 390 | result := &SearchResult{ 391 | Entries: make([]*Entry, 0), 392 | Referrals: make([]string, 0), 393 | Controls: make([]Control, 0)} 394 | 395 | foundSearchResultDone := false 396 | for !foundSearchResultDone { 397 | l.Debug.Printf("%d: waiting for response", msgCtx.id) 398 | packetResponse, ok := <-msgCtx.responses 399 | if !ok { 400 | return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed")) 401 | } 402 | packet, err = packetResponse.ReadPacket() 403 | l.Debug.Printf("%d: got response %p", msgCtx.id, packet) 404 | if err != nil { 405 | return nil, err 406 | } 407 | 408 | if l.Debug { 409 | if err := addLDAPDescriptions(packet); err != nil { 410 | return nil, err 411 | } 412 | ber.PrintPacket(packet) 413 | } 414 | 415 | switch packet.Children[1].Tag { 416 | case 4: 417 | entry := new(Entry) 418 | entry.DN = packet.Children[1].Children[0].Value.(string) 419 | for _, child := range packet.Children[1].Children[1].Children { 420 | attr := new(EntryAttribute) 421 | attr.Name = child.Children[0].Value.(string) 422 | for _, value := range child.Children[1].Children { 423 | attr.Values = append(attr.Values, value.Value.(string)) 424 | attr.ByteValues = append(attr.ByteValues, value.ByteValue) 425 | } 426 | entry.Attributes = append(entry.Attributes, attr) 427 | } 428 | result.Entries = append(result.Entries, entry) 429 | case 5: 430 | resultCode, resultDescription := getLDAPResultCode(packet) 431 | if resultCode != 0 { 432 | return result, NewError(resultCode, errors.New(resultDescription)) 433 | } 434 | if len(packet.Children) == 3 { 435 | for _, child := range packet.Children[2].Children { 436 | result.Controls = append(result.Controls, DecodeControl(child)) 437 | } 438 | } 439 | foundSearchResultDone = true 440 | case 19: 441 | result.Referrals = append(result.Referrals, packet.Children[1].Children[0].Value.(string)) 442 | } 443 | } 444 | l.Debug.Printf("%d: returning", msgCtx.id) 445 | return result, nil 446 | } 447 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "bytes" 5 | hexpac "encoding/hex" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | "unicode/utf8" 10 | 11 | "gopkg.in/asn1-ber.v1" 12 | ) 13 | 14 | // Filter choices 15 | const ( 16 | FilterAnd = 0 17 | FilterOr = 1 18 | FilterNot = 2 19 | FilterEqualityMatch = 3 20 | FilterSubstrings = 4 21 | FilterGreaterOrEqual = 5 22 | FilterLessOrEqual = 6 23 | FilterPresent = 7 24 | FilterApproxMatch = 8 25 | FilterExtensibleMatch = 9 26 | ) 27 | 28 | // FilterMap contains human readable descriptions of Filter choices 29 | var FilterMap = map[uint64]string{ 30 | FilterAnd: "And", 31 | FilterOr: "Or", 32 | FilterNot: "Not", 33 | FilterEqualityMatch: "Equality Match", 34 | FilterSubstrings: "Substrings", 35 | FilterGreaterOrEqual: "Greater Or Equal", 36 | FilterLessOrEqual: "Less Or Equal", 37 | FilterPresent: "Present", 38 | FilterApproxMatch: "Approx Match", 39 | FilterExtensibleMatch: "Extensible Match", 40 | } 41 | 42 | // SubstringFilter options 43 | const ( 44 | FilterSubstringsInitial = 0 45 | FilterSubstringsAny = 1 46 | FilterSubstringsFinal = 2 47 | ) 48 | 49 | // FilterSubstringsMap contains human readable descriptions of SubstringFilter choices 50 | var FilterSubstringsMap = map[uint64]string{ 51 | FilterSubstringsInitial: "Substrings Initial", 52 | FilterSubstringsAny: "Substrings Any", 53 | FilterSubstringsFinal: "Substrings Final", 54 | } 55 | 56 | // MatchingRuleAssertion choices 57 | const ( 58 | MatchingRuleAssertionMatchingRule = 1 59 | MatchingRuleAssertionType = 2 60 | MatchingRuleAssertionMatchValue = 3 61 | MatchingRuleAssertionDNAttributes = 4 62 | ) 63 | 64 | // MatchingRuleAssertionMap contains human readable descriptions of MatchingRuleAssertion choices 65 | var MatchingRuleAssertionMap = map[uint64]string{ 66 | MatchingRuleAssertionMatchingRule: "Matching Rule Assertion Matching Rule", 67 | MatchingRuleAssertionType: "Matching Rule Assertion Type", 68 | MatchingRuleAssertionMatchValue: "Matching Rule Assertion Match Value", 69 | MatchingRuleAssertionDNAttributes: "Matching Rule Assertion DN Attributes", 70 | } 71 | 72 | // CompileFilter converts a string representation of a filter into a BER-encoded packet 73 | func CompileFilter(filter string) (*ber.Packet, error) { 74 | if len(filter) == 0 || filter[0] != '(' { 75 | return nil, NewError(ErrorFilterCompile, errors.New("ldap: filter does not start with an '('")) 76 | } 77 | packet, pos, err := compileFilter(filter, 1) 78 | if err != nil { 79 | return nil, err 80 | } 81 | switch { 82 | case pos > len(filter): 83 | return nil, NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter")) 84 | case pos < len(filter): 85 | return nil, NewError(ErrorFilterCompile, errors.New("ldap: finished compiling filter with extra at end: "+fmt.Sprint(filter[pos:]))) 86 | } 87 | return packet, nil 88 | } 89 | 90 | // DecompileFilter converts a packet representation of a filter into a string representation 91 | func DecompileFilter(packet *ber.Packet) (ret string, err error) { 92 | defer func() { 93 | if r := recover(); r != nil { 94 | err = NewError(ErrorFilterDecompile, errors.New("ldap: error decompiling filter")) 95 | } 96 | }() 97 | ret = "(" 98 | err = nil 99 | childStr := "" 100 | 101 | switch packet.Tag { 102 | case FilterAnd: 103 | ret += "&" 104 | for _, child := range packet.Children { 105 | childStr, err = DecompileFilter(child) 106 | if err != nil { 107 | return 108 | } 109 | ret += childStr 110 | } 111 | case FilterOr: 112 | ret += "|" 113 | for _, child := range packet.Children { 114 | childStr, err = DecompileFilter(child) 115 | if err != nil { 116 | return 117 | } 118 | ret += childStr 119 | } 120 | case FilterNot: 121 | ret += "!" 122 | childStr, err = DecompileFilter(packet.Children[0]) 123 | if err != nil { 124 | return 125 | } 126 | ret += childStr 127 | 128 | case FilterSubstrings: 129 | ret += ber.DecodeString(packet.Children[0].Data.Bytes()) 130 | ret += "=" 131 | for i, child := range packet.Children[1].Children { 132 | if i == 0 && child.Tag != FilterSubstringsInitial { 133 | ret += "*" 134 | } 135 | ret += EscapeFilter(ber.DecodeString(child.Data.Bytes())) 136 | if child.Tag != FilterSubstringsFinal { 137 | ret += "*" 138 | } 139 | } 140 | case FilterEqualityMatch: 141 | ret += ber.DecodeString(packet.Children[0].Data.Bytes()) 142 | ret += "=" 143 | ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes())) 144 | case FilterGreaterOrEqual: 145 | ret += ber.DecodeString(packet.Children[0].Data.Bytes()) 146 | ret += ">=" 147 | ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes())) 148 | case FilterLessOrEqual: 149 | ret += ber.DecodeString(packet.Children[0].Data.Bytes()) 150 | ret += "<=" 151 | ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes())) 152 | case FilterPresent: 153 | ret += ber.DecodeString(packet.Data.Bytes()) 154 | ret += "=*" 155 | case FilterApproxMatch: 156 | ret += ber.DecodeString(packet.Children[0].Data.Bytes()) 157 | ret += "~=" 158 | ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes())) 159 | case FilterExtensibleMatch: 160 | attr := "" 161 | dnAttributes := false 162 | matchingRule := "" 163 | value := "" 164 | 165 | for _, child := range packet.Children { 166 | switch child.Tag { 167 | case MatchingRuleAssertionMatchingRule: 168 | matchingRule = ber.DecodeString(child.Data.Bytes()) 169 | case MatchingRuleAssertionType: 170 | attr = ber.DecodeString(child.Data.Bytes()) 171 | case MatchingRuleAssertionMatchValue: 172 | value = ber.DecodeString(child.Data.Bytes()) 173 | case MatchingRuleAssertionDNAttributes: 174 | dnAttributes = child.Value.(bool) 175 | } 176 | } 177 | 178 | if len(attr) > 0 { 179 | ret += attr 180 | } 181 | if dnAttributes { 182 | ret += ":dn" 183 | } 184 | if len(matchingRule) > 0 { 185 | ret += ":" 186 | ret += matchingRule 187 | } 188 | ret += ":=" 189 | ret += EscapeFilter(value) 190 | } 191 | 192 | ret += ")" 193 | return 194 | } 195 | 196 | func compileFilterSet(filter string, pos int, parent *ber.Packet) (int, error) { 197 | for pos < len(filter) && filter[pos] == '(' { 198 | child, newPos, err := compileFilter(filter, pos+1) 199 | if err != nil { 200 | return pos, err 201 | } 202 | pos = newPos 203 | parent.AppendChild(child) 204 | } 205 | if pos == len(filter) { 206 | return pos, NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter")) 207 | } 208 | 209 | return pos + 1, nil 210 | } 211 | 212 | func compileFilter(filter string, pos int) (*ber.Packet, int, error) { 213 | var ( 214 | packet *ber.Packet 215 | err error 216 | ) 217 | 218 | defer func() { 219 | if r := recover(); r != nil { 220 | err = NewError(ErrorFilterCompile, errors.New("ldap: error compiling filter")) 221 | } 222 | }() 223 | newPos := pos 224 | 225 | currentRune, currentWidth := utf8.DecodeRuneInString(filter[newPos:]) 226 | 227 | switch currentRune { 228 | case utf8.RuneError: 229 | return nil, 0, NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", newPos)) 230 | case '(': 231 | packet, newPos, err = compileFilter(filter, pos+currentWidth) 232 | newPos++ 233 | return packet, newPos, err 234 | case '&': 235 | packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterAnd, nil, FilterMap[FilterAnd]) 236 | newPos, err = compileFilterSet(filter, pos+currentWidth, packet) 237 | return packet, newPos, err 238 | case '|': 239 | packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterOr, nil, FilterMap[FilterOr]) 240 | newPos, err = compileFilterSet(filter, pos+currentWidth, packet) 241 | return packet, newPos, err 242 | case '!': 243 | packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterNot, nil, FilterMap[FilterNot]) 244 | var child *ber.Packet 245 | child, newPos, err = compileFilter(filter, pos+currentWidth) 246 | packet.AppendChild(child) 247 | return packet, newPos, err 248 | default: 249 | const ( 250 | stateReadingAttr = 0 251 | stateReadingExtensibleMatchingRule = 1 252 | stateReadingCondition = 2 253 | ) 254 | 255 | state := stateReadingAttr 256 | 257 | attribute := "" 258 | extensibleDNAttributes := false 259 | extensibleMatchingRule := "" 260 | condition := "" 261 | 262 | for newPos < len(filter) { 263 | remainingFilter := filter[newPos:] 264 | currentRune, currentWidth = utf8.DecodeRuneInString(remainingFilter) 265 | if currentRune == ')' { 266 | break 267 | } 268 | if currentRune == utf8.RuneError { 269 | return packet, newPos, NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", newPos)) 270 | } 271 | 272 | switch state { 273 | case stateReadingAttr: 274 | switch { 275 | // Extensible rule, with only DN-matching 276 | case currentRune == ':' && strings.HasPrefix(remainingFilter, ":dn:="): 277 | packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) 278 | extensibleDNAttributes = true 279 | state = stateReadingCondition 280 | newPos += 5 281 | 282 | // Extensible rule, with DN-matching and a matching OID 283 | case currentRune == ':' && strings.HasPrefix(remainingFilter, ":dn:"): 284 | packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) 285 | extensibleDNAttributes = true 286 | state = stateReadingExtensibleMatchingRule 287 | newPos += 4 288 | 289 | // Extensible rule, with attr only 290 | case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="): 291 | packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) 292 | state = stateReadingCondition 293 | newPos += 2 294 | 295 | // Extensible rule, with no DN attribute matching 296 | case currentRune == ':': 297 | packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) 298 | state = stateReadingExtensibleMatchingRule 299 | newPos++ 300 | 301 | // Equality condition 302 | case currentRune == '=': 303 | packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterEqualityMatch, nil, FilterMap[FilterEqualityMatch]) 304 | state = stateReadingCondition 305 | newPos++ 306 | 307 | // Greater-than or equal 308 | case currentRune == '>' && strings.HasPrefix(remainingFilter, ">="): 309 | packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterGreaterOrEqual, nil, FilterMap[FilterGreaterOrEqual]) 310 | state = stateReadingCondition 311 | newPos += 2 312 | 313 | // Less-than or equal 314 | case currentRune == '<' && strings.HasPrefix(remainingFilter, "<="): 315 | packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterLessOrEqual, nil, FilterMap[FilterLessOrEqual]) 316 | state = stateReadingCondition 317 | newPos += 2 318 | 319 | // Approx 320 | case currentRune == '~' && strings.HasPrefix(remainingFilter, "~="): 321 | packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterApproxMatch, nil, FilterMap[FilterApproxMatch]) 322 | state = stateReadingCondition 323 | newPos += 2 324 | 325 | // Still reading the attribute name 326 | default: 327 | attribute += fmt.Sprintf("%c", currentRune) 328 | newPos += currentWidth 329 | } 330 | 331 | case stateReadingExtensibleMatchingRule: 332 | switch { 333 | 334 | // Matching rule OID is done 335 | case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="): 336 | state = stateReadingCondition 337 | newPos += 2 338 | 339 | // Still reading the matching rule oid 340 | default: 341 | extensibleMatchingRule += fmt.Sprintf("%c", currentRune) 342 | newPos += currentWidth 343 | } 344 | 345 | case stateReadingCondition: 346 | // append to the condition 347 | condition += fmt.Sprintf("%c", currentRune) 348 | newPos += currentWidth 349 | } 350 | } 351 | 352 | if newPos == len(filter) { 353 | err = NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter")) 354 | return packet, newPos, err 355 | } 356 | if packet == nil { 357 | err = NewError(ErrorFilterCompile, errors.New("ldap: error parsing filter")) 358 | return packet, newPos, err 359 | } 360 | 361 | switch { 362 | case packet.Tag == FilterExtensibleMatch: 363 | // MatchingRuleAssertion ::= SEQUENCE { 364 | // matchingRule [1] MatchingRuleID OPTIONAL, 365 | // type [2] AttributeDescription OPTIONAL, 366 | // matchValue [3] AssertionValue, 367 | // dnAttributes [4] BOOLEAN DEFAULT FALSE 368 | // } 369 | 370 | // Include the matching rule oid, if specified 371 | if len(extensibleMatchingRule) > 0 { 372 | packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionMatchingRule, extensibleMatchingRule, MatchingRuleAssertionMap[MatchingRuleAssertionMatchingRule])) 373 | } 374 | 375 | // Include the attribute, if specified 376 | if len(attribute) > 0 { 377 | packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionType, attribute, MatchingRuleAssertionMap[MatchingRuleAssertionType])) 378 | } 379 | 380 | // Add the value (only required child) 381 | encodedString, encodeErr := escapedStringToEncodedBytes(condition) 382 | if encodeErr != nil { 383 | return packet, newPos, encodeErr 384 | } 385 | packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionMatchValue, encodedString, MatchingRuleAssertionMap[MatchingRuleAssertionMatchValue])) 386 | 387 | // Defaults to false, so only include in the sequence if true 388 | if extensibleDNAttributes { 389 | packet.AppendChild(ber.NewBoolean(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionDNAttributes, extensibleDNAttributes, MatchingRuleAssertionMap[MatchingRuleAssertionDNAttributes])) 390 | } 391 | 392 | case packet.Tag == FilterEqualityMatch && condition == "*": 393 | packet = ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterPresent, attribute, FilterMap[FilterPresent]) 394 | case packet.Tag == FilterEqualityMatch && strings.Contains(condition, "*"): 395 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute")) 396 | packet.Tag = FilterSubstrings 397 | packet.Description = FilterMap[uint64(packet.Tag)] 398 | seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Substrings") 399 | parts := strings.Split(condition, "*") 400 | for i, part := range parts { 401 | if part == "" { 402 | continue 403 | } 404 | var tag ber.Tag 405 | switch i { 406 | case 0: 407 | tag = FilterSubstringsInitial 408 | case len(parts) - 1: 409 | tag = FilterSubstringsFinal 410 | default: 411 | tag = FilterSubstringsAny 412 | } 413 | encodedString, encodeErr := escapedStringToEncodedBytes(part) 414 | if encodeErr != nil { 415 | return packet, newPos, encodeErr 416 | } 417 | seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, tag, encodedString, FilterSubstringsMap[uint64(tag)])) 418 | } 419 | packet.AppendChild(seq) 420 | default: 421 | encodedString, encodeErr := escapedStringToEncodedBytes(condition) 422 | if encodeErr != nil { 423 | return packet, newPos, encodeErr 424 | } 425 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute")) 426 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, encodedString, "Condition")) 427 | } 428 | 429 | newPos += currentWidth 430 | return packet, newPos, err 431 | } 432 | } 433 | 434 | // Convert from "ABC\xx\xx\xx" form to literal bytes for transport 435 | func escapedStringToEncodedBytes(escapedString string) (string, error) { 436 | var buffer bytes.Buffer 437 | i := 0 438 | for i < len(escapedString) { 439 | currentRune, currentWidth := utf8.DecodeRuneInString(escapedString[i:]) 440 | if currentRune == utf8.RuneError { 441 | return "", NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", i)) 442 | } 443 | 444 | // Check for escaped hex characters and convert them to their literal value for transport. 445 | if currentRune == '\\' { 446 | // http://tools.ietf.org/search/rfc4515 447 | // \ (%x5C) is not a valid character unless it is followed by two HEX characters due to not 448 | // being a member of UTF1SUBSET. 449 | if i+2 > len(escapedString) { 450 | return "", NewError(ErrorFilterCompile, errors.New("ldap: missing characters for escape in filter")) 451 | } 452 | escByte, decodeErr := hexpac.DecodeString(escapedString[i+1 : i+3]) 453 | if decodeErr != nil { 454 | return "", NewError(ErrorFilterCompile, errors.New("ldap: invalid characters for escape in filter")) 455 | } 456 | buffer.WriteByte(escByte[0]) 457 | i += 2 // +1 from end of loop, so 3 total for \xx. 458 | } else { 459 | buffer.WriteRune(currentRune) 460 | } 461 | 462 | i += currentWidth 463 | } 464 | return buffer.String(), nil 465 | } 466 | --------------------------------------------------------------------------------