├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── internal │ └── ldapcmd │ │ └── ldapcmd.go ├── ldapsearch │ └── main.go └── ldapwhoami │ └── main.go ├── go.mod ├── go.sum └── ldap ├── add.go ├── backend.go ├── ber.go ├── ber_test.go ├── bind.go ├── client.go ├── client_test.go ├── delete.go ├── extended.go ├── filter.go ├── filter_test.go ├── ldap.go ├── modify.go ├── modifydn.go ├── search.go └── server.go /.gitignore: -------------------------------------------------------------------------------- 1 | cmd/ldapsearch/ldapsearch 2 | cmd/ldapwhoami/ldapwhoami 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Samuel Stauffer 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of the author nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | LDAP Client and Server Package For Go 2 | ===================================== 3 | 4 | Documentation: https://godoc.org/github.com/samuel/go-ldap/ldap 5 | 6 | Lightweight Directory Access Protocol (LDAP): RFCs 7 | -------------------------------------------------- 8 | 9 | - [rfc4510](https://tools.ietf.org/html/rfc4510) - Technical Specification Road Map 10 | - [rfc4511](https://tools.ietf.org/html/rfc4511) - The Protocol 11 | - [rfc4512](https://tools.ietf.org/html/rfc4512) - Directory Information Models 12 | - [rfc4513](https://tools.ietf.org/html/rfc4513) - Authentication Methods and Security Mechanisms 13 | - [rfc4514](https://tools.ietf.org/html/rfc4514) - String Representation of Distinguished Names 14 | - [rfc4519](https://tools.ietf.org/html/rfc4519) - Schema for User Applications 15 | 16 | License 17 | ------- 18 | 19 | 3-clause BSD. See LICENSE file. 20 | -------------------------------------------------------------------------------- /cmd/internal/ldapcmd/ldapcmd.go: -------------------------------------------------------------------------------- 1 | package ldapcmd 2 | 3 | import ( 4 | "crypto/tls" 5 | "flag" 6 | "fmt" 7 | "net/url" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/howeyc/gopass" 12 | "github.com/samuel/go-ldap/ldap" 13 | ) 14 | 15 | var ( 16 | flagBindDN = flag.String("D", "", "bind DN") 17 | flagBindPass = flag.String("w", "", "bind password (for simple authentication)") 18 | flagHost = flag.String("h", "127.0.0.1", "LDAP server") 19 | flagInsecure = flag.Bool("insecure", false, "Don't validate server certificate") 20 | flagPort = flag.Int("p", 389, "port on LDAP server") 21 | flagPromptPass = flag.Bool("W", false, "prompt for bind password") 22 | flagSimpleAuth = flag.Bool("x", false, "Simple authentication") 23 | flagStartTLS = flag.Bool("Z", false, "Start TLS request (-ZZ to require successful response)") // TODO: implement ZZ 24 | flagURI = flag.String("H", "", "LDAP Uniform Resource Identifier(s)") 25 | ) 26 | 27 | // Connect connects to the LDAP server. flag.Parse must 28 | // have been called first. 29 | func Connect() (*ldap.Client, error) { 30 | addr := *flagHost 31 | enableTLS := false 32 | if *flagURI != "" { 33 | u, err := url.Parse(*flagURI) 34 | if err != nil { 35 | return nil, fmt.Errorf("failed to parse URI %s: %s", *flagURI, err.Error()) 36 | } 37 | if u.Scheme == "ldaps" { 38 | enableTLS = true 39 | if *flagPort == 389 { 40 | *flagPort = 636 41 | } 42 | } else if u.Scheme != "ldap" { 43 | return nil, fmt.Errorf("URI scheme must be ldap or ldaps: %s", *flagURI) 44 | } 45 | addr = u.Host 46 | } 47 | if strings.IndexByte(addr, ':') < 0 { 48 | addr += ":" + strconv.Itoa(*flagPort) 49 | } 50 | var err error 51 | var cli *ldap.Client 52 | if enableTLS { 53 | conf := &tls.Config{ 54 | InsecureSkipVerify: *flagInsecure, 55 | } 56 | cli, err = ldap.DialTLS("tcp", addr, conf) 57 | } else { 58 | cli, err = ldap.Dial("tcp", addr) 59 | } 60 | if err != nil { 61 | return nil, fmt.Errorf("failed to connect to server: %w", err) 62 | } 63 | 64 | if !enableTLS && *flagStartTLS { 65 | err := cli.StartTLS(&tls.Config{ 66 | InsecureSkipVerify: *flagInsecure, 67 | }) 68 | if err != nil { 69 | return nil, fmt.Errorf("failed to StartTLS: %w", err) 70 | } 71 | } 72 | 73 | if *flagSimpleAuth { 74 | var pass []byte 75 | if *flagPromptPass { 76 | fmt.Printf("Enter LDAP Password: ") 77 | pass, err = gopass.GetPasswd() 78 | if err != nil { 79 | return nil, fmt.Errorf("getpasswd failed: %w", err) 80 | } 81 | } else { 82 | pass = []byte(*flagBindPass) 83 | } 84 | if err := cli.Bind(*flagBindDN, pass); err != nil { 85 | return nil, fmt.Errorf("bind failed: %w", err) 86 | } 87 | } 88 | 89 | return cli, nil 90 | } 91 | -------------------------------------------------------------------------------- /cmd/ldapsearch/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strings" 9 | 10 | "github.com/samuel/go-ldap/cmd/internal/ldapcmd" 11 | "github.com/samuel/go-ldap/ldap" 12 | ) 13 | 14 | var ( 15 | flagBaseDN = flag.String("b", "", "base dn for search") 16 | flagScope = flag.String("s", "sub", "one of base, one, sub or children (search scope)") 17 | ) 18 | 19 | var scopes = map[string]ldap.Scope{ 20 | "base": ldap.ScopeBaseObject, 21 | "one": ldap.ScopeSingleLevel, 22 | "sub": ldap.ScopeWholeSubtree, 23 | "children": ldap.ScopeChildren, 24 | } 25 | 26 | func main() { 27 | log.SetFlags(0) 28 | flag.Parse() 29 | 30 | req := &ldap.SearchRequest{ 31 | BaseDN: *flagBaseDN, 32 | } 33 | 34 | // Parse args either as "filter attribute,attribute,..." or either by itself. A filter 35 | // string always start with a '(' 36 | n := 0 37 | if flag.NArg() > n { 38 | s := flag.Arg(n) 39 | if len(s) > 0 && s[0] == '(' { 40 | n++ 41 | f, err := ldap.ParseFilter(s) 42 | if err != nil { 43 | log.Fatalf("Failed to parse filter '%s': %s", s, err.Error()) 44 | } 45 | req.Filter = f 46 | } 47 | } 48 | if flag.NArg() > n { 49 | attr := strings.Split(flag.Arg(n), ",") 50 | req.Attributes = make(map[string]bool) 51 | for _, a := range attr { 52 | req.Attributes[a] = true 53 | } 54 | } 55 | 56 | var ok bool 57 | req.Scope, ok = scopes[*flagScope] 58 | if !ok { 59 | log.Fatalf("Unknown scope %s", *flagScope) 60 | } 61 | 62 | cli, err := ldapcmd.Connect() 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | 67 | res, err := cli.Search(req) 68 | if err != nil { 69 | log.Fatalf("Search failed: %s", err.Error()) 70 | } 71 | for i, r := range res { 72 | if i != 0 { 73 | fmt.Println() 74 | } 75 | _ = r.ToLDIF(os.Stdout) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /cmd/ldapwhoami/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/samuel/go-ldap/cmd/internal/ldapcmd" 9 | ) 10 | 11 | func main() { 12 | log.SetFlags(0) 13 | flag.Parse() 14 | 15 | cli, err := ldapcmd.Connect() 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | 20 | id, err := cli.WhoAmI() 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | fmt.Println(id) 25 | } 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/samuel/go-ldap 2 | 3 | go 1.22 4 | 5 | require github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef 6 | 7 | require ( 8 | golang.org/x/crypto v0.26.0 // indirect 9 | golang.org/x/sys v0.24.0 // indirect 10 | golang.org/x/term v0.23.0 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM= 2 | github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= 3 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= 4 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 5 | golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= 6 | golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 7 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 8 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 9 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 10 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 11 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= 12 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 13 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 14 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 15 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= 16 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 17 | golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= 18 | golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= 19 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 20 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 21 | -------------------------------------------------------------------------------- /ldap/add.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import "io" 4 | 5 | type AddRequest struct { 6 | DN string 7 | Attributes map[string][][]byte 8 | } 9 | 10 | type AddResponse struct { 11 | BaseResponse 12 | } 13 | 14 | func parseAddRequest(pkt *Packet) (*AddRequest, error) { 15 | if len(pkt.Items) != 2 { 16 | return nil, &ProtocolError{Reason: "add request requires 2 items"} 17 | } 18 | var ok bool 19 | req := &AddRequest{} 20 | req.DN, ok = pkt.Items[0].Str() 21 | if !ok { 22 | return nil, &ProtocolError{Reason: "invalid dn"} 23 | } 24 | req.Attributes = make(map[string][][]byte) 25 | for _, at := range pkt.Items[1].Items { 26 | if len(at.Items) != 2 { 27 | return nil, &ProtocolError{Reason: "invalid attribute"} 28 | } 29 | attrName, ok := at.Items[0].Str() 30 | if !ok { 31 | return nil, &ProtocolError{Reason: "invalid attribute"} 32 | } 33 | var vals [][]byte 34 | for _, v := range at.Items[1].Items { 35 | vb, ok := v.Bytes() 36 | if !ok { 37 | return nil, &ProtocolError{Reason: "invalid attribute value"} 38 | } 39 | vals = append(vals, vb) 40 | } 41 | req.Attributes[attrName] = vals 42 | } 43 | return req, nil 44 | } 45 | 46 | func (r *AddResponse) WritePackets(w io.Writer, msgID int) error { 47 | res := NewResponsePacket(msgID) 48 | pkt := res.AddItem(r.BaseResponse.NewPacket()) 49 | pkt.Tag = ApplicationAddResponse 50 | return res.Write(w) 51 | } 52 | -------------------------------------------------------------------------------- /ldap/backend.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | ) 8 | 9 | // State is passed created by and passed back to a server backend to provide 10 | // state for a client connection. 11 | type State interface{} 12 | 13 | // Backend is implemented by an LDAP database to provide the backing store. 14 | type Backend interface { 15 | Add(ctx context.Context, state State, req *AddRequest) (*AddResponse, error) 16 | Bind(ctx context.Context, state State, req *BindRequest) (*BindResponse, error) 17 | Connect(remoteAddr net.Addr) (State, error) 18 | Delete(ctx context.Context, state State, req *DeleteRequest) (*DeleteResponse, error) 19 | Disconnect(state State) 20 | ExtendedRequest(ctx context.Context, state State, req *ExtendedRequest) (*ExtendedResponse, error) 21 | Modify(ctx context.Context, state State, req *ModifyRequest) (*ModifyResponse, error) 22 | ModifyDN(ctx context.Context, state State, req *ModifyDNRequest) (*ModifyDNResponse, error) 23 | PasswordModify(ctx context.Context, state State, req *PasswordModifyRequest) ([]byte, error) 24 | Search(ctx context.Context, state State, req *SearchRequest) (*SearchResponse, error) 25 | Whoami(ctx context.Context, state State) (string, error) 26 | } 27 | 28 | type debugBackend struct{} 29 | 30 | // DebugBackend is an implementation of a server backend that prints out requests. 31 | var DebugBackend Backend = debugBackend{} 32 | 33 | func (debugBackend) Add(ctx context.Context, state State, req *AddRequest) (*AddResponse, error) { 34 | fmt.Printf("ADD %+v\n", req) 35 | return &AddResponse{}, nil 36 | } 37 | 38 | func (debugBackend) Bind(ctx context.Context, state State, req *BindRequest) (*BindResponse, error) { 39 | fmt.Printf("BIND %+v\n", req) 40 | return &BindResponse{ 41 | BaseResponse: BaseResponse{ 42 | Code: ResultSuccess, 43 | MatchedDN: "", 44 | Message: "", 45 | }, 46 | }, nil 47 | } 48 | 49 | func (debugBackend) Connect(remoteAddr net.Addr) (State, error) { 50 | return nil, nil 51 | } 52 | 53 | func (debugBackend) Disconnect(state State) { 54 | } 55 | 56 | func (debugBackend) Delete(ctx context.Context, state State, req *DeleteRequest) (*DeleteResponse, error) { 57 | fmt.Printf("DELETE %+v\n", req) 58 | return &DeleteResponse{}, nil 59 | } 60 | 61 | func (debugBackend) ExtendedRequest(ctx context.Context, state State, req *ExtendedRequest) (*ExtendedResponse, error) { 62 | fmt.Printf("EXTENDED %+v\n", req) 63 | return nil, &ProtocolError{Reason: "unsupported extended request"} 64 | } 65 | 66 | func (debugBackend) Modify(ctx context.Context, state State, req *ModifyRequest) (*ModifyResponse, error) { 67 | fmt.Printf("MODIFY dn=%s\n", req.DN) 68 | for _, m := range req.Mods { 69 | fmt.Printf("\t%s %s\n", m.Type, m.Name) 70 | for _, v := range m.Values { 71 | fmt.Printf("\t\t%s\n", string(v)) 72 | } 73 | } 74 | return &ModifyResponse{}, nil 75 | } 76 | 77 | func (debugBackend) ModifyDN(ctx context.Context, state State, req *ModifyDNRequest) (*ModifyDNResponse, error) { 78 | fmt.Printf("MODIFYDN %+v\n", req) 79 | return &ModifyDNResponse{}, nil 80 | } 81 | 82 | func (debugBackend) PasswordModify(ctx context.Context, state State, req *PasswordModifyRequest) ([]byte, error) { 83 | fmt.Printf("PASSWORD MODIFY %+v\n", req) 84 | return []byte("genpass"), nil 85 | } 86 | 87 | func (debugBackend) Search(ctx context.Context, state State, req *SearchRequest) (*SearchResponse, error) { 88 | fmt.Printf("SEARCH %+v\n", req) 89 | return &SearchResponse{ 90 | BaseResponse: BaseResponse{ 91 | Code: ResultSuccess, //LDAPResultNoSuchObject, 92 | MatchedDN: "", 93 | Message: "", 94 | }, 95 | Results: []*SearchResult{ 96 | { 97 | DN: "cn=admin,dc=example,dc=com", 98 | Attributes: map[string][][]byte{ 99 | "objectClass": {[]byte("person")}, 100 | "cn": {[]byte("admin")}, 101 | "uid": {[]byte("123")}, 102 | }, 103 | }, 104 | }, 105 | }, nil 106 | } 107 | 108 | func (debugBackend) Whoami(ctx context.Context, state State) (string, error) { 109 | fmt.Println("WHOAMI") 110 | return "cn=someone,o=somewhere", nil 111 | } 112 | -------------------------------------------------------------------------------- /ldap/ber.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | // TODO: handle negative integers properly 4 | 5 | import ( 6 | "bytes" 7 | "encoding/hex" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "log" 12 | "strconv" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | const maxPacketSize = 32 << 20 // 32 MB 18 | 19 | type InvalidBEREncodingError string 20 | 21 | func (e InvalidBEREncodingError) Error() string { 22 | return string(e) 23 | } 24 | 25 | type Class byte 26 | 27 | const ( 28 | ClassUniversal Class = 0 29 | ClassApplication Class = 1 30 | ClassContext Class = 2 31 | ClassPrivate Class = 3 32 | ) 33 | 34 | var ClassNames = map[Class]string{ 35 | ClassUniversal: "Universal", 36 | ClassApplication: "Application", 37 | ClassContext: "Context", 38 | ClassPrivate: "Private", 39 | } 40 | 41 | func (c Class) String() string { 42 | return ClassNames[c] 43 | } 44 | 45 | const ( 46 | TagEOC = 0x00 47 | TagBoolean = 0x01 48 | TagInteger = 0x02 49 | TagBitString = 0x03 50 | TagOctetString = 0x04 51 | TagNULL = 0x05 52 | TagObjectIdentifier = 0x06 53 | TagObjectDescriptor = 0x07 54 | TagExternal = 0x08 55 | TagRealFloat = 0x09 56 | TagEnumerated = 0x0a 57 | TagEmbeddedPDV = 0x0b 58 | TagUTF8String = 0x0c 59 | TagRelativeOID = 0x0d 60 | TagSequence = 0x10 61 | TagSet = 0x11 62 | TagNumericString = 0x12 63 | TagPrintableString = 0x13 64 | TagT61String = 0x14 65 | TagVideotexString = 0x15 66 | TagIA5String = 0x16 67 | TagUTCTime = 0x17 68 | TagGeneralizedTime = 0x18 69 | TagGraphicString = 0x19 70 | TagVisibleString = 0x1a 71 | TagGeneralString = 0x1b 72 | TagUniversalString = 0x1c 73 | TagCharacterString = 0x1d 74 | TagBMPString = 0x1e 75 | ) 76 | 77 | var TagNames = map[int]string{ 78 | TagEOC: "EOC (End-of-Content)", 79 | TagBoolean: "Boolean", 80 | TagInteger: "Integer", 81 | TagBitString: "Bit String", 82 | TagOctetString: "Octet String", 83 | TagNULL: "NULL", 84 | TagObjectIdentifier: "Object Identifier", 85 | TagObjectDescriptor: "Object Descriptor", 86 | TagExternal: "External", 87 | TagRealFloat: "Real (float)", 88 | TagEnumerated: "Enumerated", 89 | TagEmbeddedPDV: "Embedded PDV", 90 | TagUTF8String: "UTF8 String", 91 | TagRelativeOID: "Relative-OID", 92 | TagSequence: "Sequence and Sequence of", 93 | TagSet: "Set and Set OF", 94 | TagNumericString: "Numeric String", 95 | TagPrintableString: "Printable String", 96 | TagT61String: "T61 String", 97 | TagVideotexString: "Videotex String", 98 | TagIA5String: "IA5 String", 99 | TagUTCTime: "UTC Time", 100 | TagGeneralizedTime: "Generalized Time", 101 | TagGraphicString: "Graphic String", 102 | TagVisibleString: "Visible String", 103 | TagGeneralString: "General String", 104 | TagUniversalString: "Universal String", 105 | TagCharacterString: "Character String", 106 | TagBMPString: "BMP String", 107 | } 108 | 109 | type Packet struct { 110 | Class Class 111 | Primitive bool // true=primitive, false=constructed 112 | Tag int 113 | Value interface{} 114 | Items []*Packet 115 | } 116 | 117 | func NewPacket(class Class, primitive bool, tag int, value interface{}) *Packet { 118 | return &Packet{ 119 | Class: class, 120 | Primitive: primitive, 121 | Tag: tag, 122 | Value: value, 123 | } 124 | } 125 | 126 | func ReadPacket(rd io.Reader) (*Packet, int, error) { 127 | buf := make([]byte, 16) 128 | if n, err := io.ReadFull(rd, buf[:2]); err != nil { 129 | return nil, n, err 130 | } 131 | if cn, ok := rd.(interface { 132 | SetReadDeadline(t time.Time) error 133 | }); ok { 134 | // Give 5 seconds to read entire body after first bytes. 135 | if err := cn.SetReadDeadline(time.Now().Add(time.Second * 5)); err != nil { 136 | log.Printf("Failed to set read deadline: %s", err) 137 | } 138 | defer func() { 139 | if err := cn.SetReadDeadline(time.Time{}); err != nil { 140 | log.Printf("Failed to clear read deadline: %s", err) 141 | } 142 | }() 143 | } 144 | hdr := 2 145 | dataLen := int(buf[1]) 146 | if dataLen&0x80 != 0 { 147 | nl := dataLen & 0x7f 148 | if nl == 0 { 149 | return nil, 2, InvalidBEREncodingError("ldap: indefinite form for length not supported") 150 | } else if nl > 8 { 151 | return nil, 2, InvalidBEREncodingError("ldap: number of size bytes failed sanity check") 152 | } 153 | if n, err := io.ReadFull(rd, buf[2:2+nl]); err != nil { 154 | return nil, hdr + n, err 155 | } 156 | hdr += nl 157 | dataLen = 0 158 | for i := 2; i < 2+nl; i++ { 159 | dataLen = (dataLen << 8) | int(buf[i]) 160 | } 161 | if dataLen > maxPacketSize { 162 | return nil, 2 + nl, InvalidBEREncodingError("ldap: packet larger than max allowed size") 163 | } 164 | } 165 | 166 | total := dataLen + hdr 167 | if total > len(buf) { 168 | buf2 := make([]byte, total) 169 | copy(buf2, buf[:hdr]) 170 | buf = buf2 171 | } else { 172 | buf = buf[:total] 173 | } 174 | if n, err := io.ReadFull(rd, buf[hdr:total]); err != nil { 175 | return nil, hdr + n, err 176 | } 177 | return ParsePacket(buf) 178 | } 179 | 180 | func ParsePacket(buf []byte) (*Packet, int, error) { 181 | if len(buf) < 2 { 182 | return nil, 0, InvalidBEREncodingError("ldap: short packet") 183 | } 184 | 185 | hdr := 2 186 | dataLen := int(buf[1]) 187 | if dataLen&0x80 != 0 { 188 | n := dataLen & 0x7f 189 | if n == 0 { 190 | return nil, hdr, InvalidBEREncodingError("ldap: indefinite form for length not supported") 191 | } else if n > 8 { 192 | return nil, hdr, InvalidBEREncodingError("ldap: number of size bytes failed sanity check") 193 | } 194 | if len(buf) < 2+n { 195 | return nil, hdr, InvalidBEREncodingError("ldap: short packet") 196 | } 197 | hdr += n 198 | dataLen = 0 199 | for i := 2; i < 2+n; i++ { 200 | dataLen = (dataLen << 8) | int(buf[i]) 201 | } 202 | if dataLen > maxPacketSize { 203 | return nil, hdr, InvalidBEREncodingError("ldap: packet larger than max allowed size") 204 | } 205 | } 206 | 207 | if dataLen > len(buf)-hdr { 208 | return nil, hdr, InvalidBEREncodingError("ldap: short packet") 209 | } 210 | data := buf[hdr : hdr+dataLen] 211 | 212 | pkt := &Packet{ 213 | Class: Class(buf[0] >> 6), 214 | Primitive: buf[0]&0x20 == 0, 215 | Tag: int(buf[0] & 0x1f), 216 | } 217 | 218 | if pkt.Primitive { 219 | if pkt.Class == ClassUniversal { 220 | var err error 221 | pkt.Value, err = parseValue(pkt.Tag, data) 222 | if err != nil { 223 | return nil, hdr + dataLen, err 224 | } 225 | } else { 226 | pkt.Value = data 227 | } 228 | } else { 229 | for len(data) > 0 { 230 | item, n, err := ParsePacket(data) 231 | if err != nil { 232 | return nil, hdr + dataLen - len(data) + n, err 233 | } 234 | pkt.Items = append(pkt.Items, item) 235 | data = data[n:] 236 | } 237 | } 238 | 239 | return pkt, hdr + dataLen, nil 240 | } 241 | 242 | func (p *Packet) AddItem(it *Packet) *Packet { 243 | p.Items = append(p.Items, it) 244 | return it 245 | } 246 | 247 | func (p *Packet) Bool() (bool, bool) { 248 | v, ok := p.Value.(bool) 249 | return v, ok 250 | } 251 | 252 | func (p *Packet) Bytes() ([]byte, bool) { 253 | v, ok := p.Value.([]byte) 254 | return v, ok 255 | } 256 | 257 | func (p *Packet) Int() (int, bool) { 258 | v, ok := p.Value.(int) 259 | return v, ok 260 | } 261 | 262 | func (p *Packet) Uint() (uint, bool) { 263 | v, ok := p.Value.(int) 264 | return uint(v), ok 265 | } 266 | 267 | func (p *Packet) Str() (string, bool) { 268 | if s, ok := p.Value.(string); ok { 269 | return s, true 270 | } 271 | if s, ok := p.Value.([]byte); ok { 272 | return string(s), true 273 | } 274 | return "", false 275 | } 276 | 277 | // TODO: handle negatives properly. 278 | func intSize(v int64) int { 279 | n := 0 280 | for x := uint64(v); x != 0; x >>= 8 { 281 | n++ 282 | } 283 | if n == 0 { 284 | return 1 285 | } 286 | return n 287 | } 288 | 289 | // Size returns data size, total size with headers, and an error for unknown types. 290 | func (p *Packet) Size() (int, int, error) { 291 | var size int 292 | if p.Primitive { 293 | if p.Value == nil { 294 | return 0, 0, errors.New("ldap: nil value in Packet.Size") 295 | } 296 | switch v := p.Value.(type) { 297 | case []byte: 298 | size = len(v) 299 | case string: 300 | size = len(v) 301 | case int: 302 | size = intSize(int64(v)) 303 | case bool: 304 | size = 1 305 | default: 306 | return 0, 0, fmt.Errorf("ldap: unknown type in Packet.Size: %T", p.Value) 307 | } 308 | } else { 309 | for _, it := range p.Items { 310 | _, n, err := it.Size() 311 | if err != nil { 312 | return 0, 0, err 313 | } 314 | size += n 315 | } 316 | } 317 | if size < 128 { 318 | return size, size + 2, nil 319 | } 320 | n := 0 321 | for x := size; x != 0; x >>= 8 { 322 | n++ 323 | } 324 | return size, size + 2 + n, nil 325 | } 326 | 327 | func (p *Packet) Encode() ([]byte, error) { 328 | b := &bytes.Buffer{} 329 | if err := p.Write(b); err != nil { 330 | return nil, err 331 | } 332 | return b.Bytes(), nil 333 | } 334 | 335 | func (p *Packet) Write(w io.Writer) error { 336 | return p.write(w, make([]byte, 16)) 337 | } 338 | 339 | func (p *Packet) write(w io.Writer, b []byte) error { 340 | sz, total, err := p.Size() 341 | if err != nil { 342 | return err 343 | } 344 | if total > maxPacketSize { 345 | return fmt.Errorf("ldap: packet larger than max size (%d > %d)", total, maxPacketSize) 346 | } 347 | pri := byte(0x20) 348 | if p.Primitive { 349 | pri = 0 350 | } 351 | hdr := 2 352 | b[0] = byte(p.Class)<<6 | pri | byte(p.Tag)&0x1f 353 | if sz < 128 { 354 | b[1] = byte(sz) 355 | } else { 356 | n := 0 357 | for x := sz; x > 0; x >>= 8 { 358 | n++ 359 | } 360 | hdr += n 361 | b[1] = 0x80 | byte(n) 362 | s := uint((n - 1) * 8) 363 | for i := range n { 364 | b[i+2] = byte(sz >> s & 0xff) 365 | s -= 8 366 | } 367 | } 368 | if _, err := w.Write(b[:hdr]); err != nil { 369 | return err 370 | } 371 | 372 | if p.Primitive { 373 | if p.Value == nil { 374 | return errors.New("ldap: nil value in Packet.write") 375 | } 376 | switch v := p.Value.(type) { 377 | case []byte: 378 | if _, err := w.Write(v); err != nil { 379 | return err 380 | } 381 | case string: 382 | if _, err := io.WriteString(w, v); err != nil { 383 | return err 384 | } 385 | case int: 386 | n := 0 387 | if v == 0 { 388 | n = 1 389 | b[0] = 0 390 | } else { 391 | for x := v; x > 0; x >>= 8 { 392 | n++ 393 | } 394 | s := uint((n - 1) * 8) 395 | for i := range n { 396 | b[i] = byte(v >> s & 0xff) 397 | s -= 8 398 | } 399 | } 400 | if _, err := w.Write(b[:n]); err != nil { 401 | return err 402 | } 403 | case bool: 404 | b[0] = 0 405 | if v { 406 | b[0] = 0xff 407 | } 408 | if _, err := w.Write(b[:1]); err != nil { 409 | return err 410 | } 411 | default: 412 | return errors.New("ldap: unknown type in Packet.write") 413 | } 414 | } else { 415 | if p.Value != nil { 416 | return errors.New("ldap: non-primitive type has a value") 417 | } 418 | for _, it := range p.Items { 419 | if err := it.write(w, b); err != nil { 420 | return err 421 | } 422 | } 423 | } 424 | 425 | return nil 426 | } 427 | 428 | func (p *Packet) Format(w io.Writer) error { 429 | return p.format(w, "") 430 | } 431 | 432 | func (p *Packet) format(w io.Writer, indent string) error { 433 | pri := "Primitive" 434 | if !p.Primitive { 435 | pri = "Constructed" 436 | } 437 | if _, err := fmt.Fprintf(w, "%sClass:%s %s", indent, p.Class.String(), pri); err != nil { 438 | return err 439 | } 440 | var tag string 441 | if p.Class == ClassUniversal { 442 | tag = TagNames[p.Tag] 443 | } 444 | if tag == "" { 445 | tag = strconv.Itoa(p.Tag) 446 | } 447 | if _, err := fmt.Fprintf(w, " Tag:%s", tag); err != nil { 448 | return err 449 | } 450 | 451 | if p.Primitive { 452 | if b, ok := p.Value.([]byte); ok { 453 | if _, err := fmt.Fprintf(w, " Len:%d\n", len(b)); err != nil { 454 | return err 455 | } 456 | for _, s := range strings.Split(hex.Dump(b), "\n") { 457 | if s != "" { 458 | if _, err := fmt.Fprintf(w, "%s %s\n", indent, s); err != nil { 459 | return err 460 | } 461 | } 462 | } 463 | } else if _, err := fmt.Fprintf(w, " Value:%+v\n", p.Value); err != nil { 464 | return err 465 | } 466 | } else { 467 | if _, err := w.Write([]byte("\n")); err != nil { 468 | return err 469 | } 470 | for _, it := range p.Items { 471 | if err := it.format(w, indent+" "); err != nil { 472 | return err 473 | } 474 | } 475 | } 476 | 477 | return nil 478 | } 479 | 480 | func parseValue(tag int, data []byte) (interface{}, error) { 481 | switch tag { 482 | default: 483 | return data, nil 484 | case TagBoolean: 485 | if len(data) != 1 { 486 | return nil, InvalidBEREncodingError("ldap: bool other than 1") 487 | } 488 | return data[0] != 0, nil 489 | case TagInteger, TagEnumerated: 490 | // TODO: handle negatives properly 491 | i := 0 492 | for _, b := range data { 493 | i = (i << 8) | int(b) 494 | } 495 | return i, nil 496 | case TagPrintableString: 497 | // Treat this as ASCII rather than UTF-8 498 | runes := make([]rune, len(data)) 499 | for i, c := range data { 500 | runes[i] = rune(c) 501 | } 502 | return string(runes), nil 503 | case TagUTF8String: // TODO: also TagOctetString? 504 | return string(data), nil 505 | } 506 | } 507 | -------------------------------------------------------------------------------- /ldap/ber_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestIntSize(t *testing.T) { 10 | t.Parallel() 11 | tests := []struct { 12 | Int int64 13 | Size int 14 | }{ 15 | {0, 1}, 16 | {1, 1}, 17 | {0xff, 1}, 18 | {0xffff, 2}, 19 | {-1, 8}, 20 | } 21 | 22 | for _, is := range tests { 23 | if n := intSize(is.Int); n != is.Size { 24 | t.Errorf("intSize(%d) = %d. Want %d", is.Int, n, is.Size) 25 | } 26 | } 27 | } 28 | 29 | func TestEncodeDecode(t *testing.T) { 30 | t.Parallel() 31 | var tests []*Packet 32 | 33 | pkt := NewPacket(ClassUniversal, false, TagSequence, nil) 34 | pkt.AddItem(NewPacket(ClassUniversal, true, TagInteger, 0x1234)) 35 | tests = append(tests, pkt) 36 | 37 | b := make([]byte, 1024) 38 | for i := 0; i < len(b); i++ { 39 | b[i] = byte(i) 40 | } 41 | pkt = NewPacket(ClassUniversal, false, TagSequence, nil) 42 | pkt.AddItem(NewPacket(ClassUniversal, true, TagOctetString, b)) 43 | pkt.AddItem(NewPacket(ClassUniversal, true, TagUTF8String, "Testing")) 44 | tests = append(tests, pkt) 45 | 46 | for _, pkt := range tests { 47 | b := &bytes.Buffer{} 48 | if err := pkt.Write(b); err != nil { 49 | t.Fatal(err) 50 | } 51 | pkt2, _, err := ReadPacket(b) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | if !reflect.DeepEqual(pkt, pkt2) { 56 | t.Errorf("Decode(Encode(%+v)) != %+v", pkt, pkt2) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ldap/bind.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import "io" 4 | 5 | type BindRequest struct { 6 | DN string 7 | Password []byte 8 | // TODO: SASL 9 | } 10 | 11 | type BindResponse struct { 12 | BaseResponse 13 | } 14 | 15 | func parseBindRequest(pkt *Packet) (*BindRequest, error) { 16 | if len(pkt.Items) != 3 { 17 | return nil, &ProtocolError{Reason: "bind request should have 3 values"} 18 | } 19 | ver, ok := pkt.Items[0].Int() 20 | if !ok || ver != protocolVersion { 21 | return nil, &ProtocolError{Reason: "unsupported or invalid version"} 22 | } 23 | req := &BindRequest{} 24 | if req.DN, ok = pkt.Items[1].Str(); !ok { 25 | return nil, &ProtocolError{Reason: "can't parse dn for bind request"} 26 | } 27 | if req.Password, ok = pkt.Items[2].Bytes(); !ok { 28 | return nil, &ProtocolError{Reason: "can't parse simple password for bind request"} 29 | } 30 | // TODO: SASL 31 | return req, nil 32 | } 33 | 34 | func parseBindResponse(pkt *Packet) (*BindResponse, error) { 35 | res := &BindResponse{} 36 | if err := parseBaseResponse(pkt, &res.BaseResponse); err != nil { 37 | return nil, err 38 | } 39 | return res, nil 40 | } 41 | 42 | func (r *BindResponse) WritePackets(w io.Writer, msgID int) error { 43 | res := NewResponsePacket(msgID) 44 | pkt := res.AddItem(r.BaseResponse.NewPacket()) 45 | pkt.Tag = ApplicationBindResponse 46 | return res.Write(w) 47 | } 48 | 49 | func (r *BindRequest) WritePackets(w io.Writer, msgID int) error { 50 | pkt := NewPacket(ClassApplication, false, ApplicationBindRequest, nil) 51 | pkt.AddItem(NewPacket(ClassUniversal, true, TagInteger, protocolVersion)) 52 | pkt.AddItem(NewPacket(ClassUniversal, true, TagOctetString, r.DN)) 53 | pkt.AddItem(NewPacket(ClassContext, true, 0, r.Password)) 54 | 55 | req := NewRequestPacket(msgID) 56 | req.AddItem(pkt) 57 | return req.Write(w) 58 | } 59 | -------------------------------------------------------------------------------- /ldap/client.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | // TODO: streaming search response 4 | 5 | import ( 6 | "bufio" 7 | "crypto/tls" 8 | "errors" 9 | "io" 10 | "log" 11 | "net" 12 | "sync" 13 | "sync/atomic" 14 | ) 15 | 16 | // ErrAlreadyTLS is returned when trying to start a TLS connection when the connection is already using TLS. 17 | var ErrAlreadyTLS = errors.New("ldap: connection already using TLS") 18 | 19 | func NewRequestPacket(msgID int) *Packet { 20 | pkt := NewPacket(ClassUniversal, false, TagSequence, nil) 21 | pkt.AddItem(NewPacket(ClassUniversal, true, TagInteger, msgID)) 22 | return pkt 23 | } 24 | 25 | type Request interface { 26 | WritePackets(w io.Writer, msgID int) error 27 | } 28 | 29 | type packetError struct { 30 | msgID int 31 | pkt *Packet 32 | err error 33 | } 34 | 35 | type cliReq struct { 36 | i int 37 | r Request 38 | c chan packetError 39 | } 40 | 41 | type Client struct { 42 | msgID uint32 43 | cn net.Conn 44 | wr *bufio.Writer 45 | isTLS bool 46 | mu sync.Mutex 47 | rq chan cliReq 48 | rmap map[int]chan packetError 49 | waitNextRecvCh chan chan struct{} 50 | waitNextSendCh chan chan struct{} 51 | } 52 | 53 | // NewClient returns a new initialized client using the provided existing connection. 54 | // The provided connection should be considered owned by the Client and not used after 55 | // this call. 56 | func NewClient(cn net.Conn, isTLS bool) *Client { 57 | c := &Client{ 58 | cn: cn, 59 | wr: bufio.NewWriter(cn), 60 | msgID: 1, 61 | rq: make(chan cliReq), 62 | rmap: make(map[int]chan packetError), 63 | isTLS: isTLS, 64 | waitNextRecvCh: make(chan chan struct{}, 1), 65 | waitNextSendCh: make(chan chan struct{}, 1), 66 | } 67 | c.start() 68 | return c 69 | } 70 | 71 | // Dial connects to a server that is not using TLS. 72 | func Dial(network, address string) (*Client, error) { 73 | cn, err := net.Dial(network, address) 74 | if err != nil { 75 | return nil, err 76 | } 77 | return NewClient(cn, false), nil 78 | } 79 | 80 | // DialTLS connects to a server that is using TLS. 81 | func DialTLS(network, address string, config *tls.Config) (*Client, error) { 82 | cn, err := tls.Dial(network, address, config) 83 | if err != nil { 84 | return nil, err 85 | } 86 | return NewClient(cn, true), nil 87 | } 88 | 89 | func (c *Client) start() { 90 | // Recv loop 91 | go func() { 92 | defer func() { 93 | c.cn.Close() 94 | }() 95 | var e error 96 | for { 97 | pkt, _, err := ReadPacket(c.cn) 98 | if err != nil { 99 | e = err 100 | break 101 | } 102 | if pkt.Class != ClassUniversal || pkt.Primitive || pkt.Tag != TagSequence || len(pkt.Items) < 2 { 103 | e = &ProtocolError{Reason: "invalid response packet"} 104 | break 105 | } 106 | msgID, ok := pkt.Items[0].Int() 107 | if !ok { 108 | e = &ProtocolError{Reason: "failed to parse msgID from response"} 109 | break 110 | } 111 | c.mu.Lock() 112 | ch := c.rmap[msgID] 113 | c.mu.Unlock() 114 | 115 | if ch == nil { 116 | log.Printf("Response for unknown message ID %d", msgID) 117 | } else { 118 | ch <- packetError{msgID: msgID, pkt: pkt.Items[1]} 119 | } 120 | 121 | select { 122 | case ch := <-c.waitNextRecvCh: 123 | <-ch 124 | default: 125 | } 126 | } 127 | if e != nil { 128 | log.Printf("ldap: error on receive: %s", e) 129 | } 130 | }() 131 | // Send loop 132 | go func() { 133 | defer func() { 134 | c.cn.Close() 135 | }() 136 | for { 137 | rq, ok := <-c.rq 138 | if !ok { 139 | break 140 | } 141 | if err := rq.r.WritePackets(c.wr, rq.i); err != nil { 142 | rq.c <- packetError{err: err} 143 | break 144 | } 145 | if err := c.wr.Flush(); err != nil { 146 | rq.c <- packetError{err: err} 147 | break 148 | } 149 | 150 | c.mu.Lock() 151 | c.rmap[rq.i] = rq.c 152 | c.mu.Unlock() 153 | 154 | select { 155 | case ch := <-c.waitNextSendCh: 156 | <-ch 157 | default: 158 | } 159 | } 160 | }() 161 | } 162 | 163 | func (c *Client) newID() int { 164 | return int(atomic.AddUint32(&c.msgID, 1)) 165 | } 166 | 167 | func (c *Client) request(req Request) (*Packet, error) { 168 | id := c.newID() 169 | ch := make(chan packetError, 1) 170 | c.rq <- cliReq{ 171 | i: id, 172 | r: req, 173 | c: ch, 174 | } 175 | r := <-ch 176 | c.finishMessage(id) 177 | return r.pkt, r.err 178 | } 179 | 180 | // Close closes the underlying connection to the server. 181 | func (c *Client) Close() error { 182 | return c.cn.Close() 183 | } 184 | 185 | func (c *Client) finishMessage(msgID int) { 186 | c.mu.Lock() 187 | delete(c.rmap, msgID) 188 | c.mu.Unlock() 189 | } 190 | 191 | // StartTLS requests a TLS connection from the server. It must not be 192 | // called concurrently with other requests. 193 | func (c *Client) StartTLS(config *tls.Config) error { 194 | if c.isTLS { 195 | return ErrAlreadyTLS 196 | } 197 | // Tell send and recv loop to stop after the next packet 198 | chS := make(chan struct{}) 199 | c.waitNextSendCh <- chS 200 | chR := make(chan struct{}) 201 | c.waitNextRecvCh <- chR 202 | defer func() { 203 | chS <- struct{}{} 204 | chR <- struct{}{} 205 | }() 206 | pkt, err := c.request(&ExtendedRequest{ 207 | Name: OIDStartTLS, 208 | }) 209 | if err != nil { 210 | return err 211 | } 212 | res, err := parseExtendedResponse(pkt) 213 | if err != nil { 214 | return err 215 | } 216 | if err := res.BaseResponse.Err(); err != nil { 217 | return err 218 | } 219 | tlsCn := tls.Client(c.cn, config) 220 | if err := tlsCn.Handshake(); err != nil { 221 | return err 222 | } 223 | c.cn = tlsCn 224 | c.wr.Reset(c.cn) 225 | return nil 226 | } 227 | 228 | // Bind authenticates using the provided dn and password. 229 | func (c *Client) Bind(dn string, pass []byte) error { 230 | pkt, err := c.request(&BindRequest{ 231 | DN: dn, 232 | Password: pass, 233 | }) 234 | if err != nil { 235 | return err 236 | } 237 | res, err := parseBindResponse(pkt) 238 | if err != nil { 239 | return err 240 | } 241 | return res.BaseResponse.Err() 242 | } 243 | 244 | // Delete a node. 245 | func (c *Client) Delete(dn string) error { 246 | pkt, err := c.request(&DeleteRequest{ 247 | DN: dn, 248 | }) 249 | if err != nil { 250 | return err 251 | } 252 | res, err := parseDeleteResponse(pkt) 253 | if err != nil { 254 | return err 255 | } 256 | return res.BaseResponse.Err() 257 | } 258 | 259 | // Search performs a search query against the LDAP database. 260 | func (c *Client) Search(req *SearchRequest) ([]*SearchResult, error) { 261 | id := c.newID() 262 | ch := make(chan packetError, 1) 263 | c.rq <- cliReq{ 264 | i: id, 265 | r: req, 266 | c: ch, 267 | } 268 | defer c.finishMessage(id) 269 | 270 | var results []*SearchResult 271 | for { 272 | r := <-ch 273 | if r.err != nil { 274 | return results, r.err 275 | } 276 | 277 | switch r.pkt.Tag { 278 | case ApplicationSearchResultEntry: 279 | res, err := parseSearchResultResponse(r.pkt) 280 | if err != nil { 281 | return results, err 282 | } 283 | results = append(results, res) 284 | case ApplicationSearchResultReference: 285 | // TODO 286 | case ApplicationSearchResultDone: 287 | var res BaseResponse 288 | if err := parseBaseResponse(r.pkt, &res); err != nil { 289 | return results, err 290 | } 291 | return results, res.Err() 292 | default: 293 | return results, &ProtocolError{Reason: "unexpected tag for search response"} 294 | } 295 | } 296 | } 297 | 298 | // Modify operation allows a client to request that a modification 299 | // of an entry be performed on its behalf by a server. 300 | func (c *Client) Modify(dn string, mods []*Mod) error { 301 | pkt, err := c.request(&ModifyRequest{ 302 | DN: dn, 303 | Mods: mods, 304 | }) 305 | if err != nil { 306 | return err 307 | } 308 | var res ModifyResponse 309 | if err := parseBaseResponse(pkt, &res.BaseResponse); err != nil { 310 | return err 311 | } 312 | return res.BaseResponse.Err() 313 | } 314 | 315 | // WhoAmI returns the authzId for the authenticated user on the connection. 316 | // https://tools.ietf.org/html/rfc4532 317 | func (c *Client) WhoAmI() (string, error) { 318 | pkt, err := c.request(&ExtendedRequest{ 319 | Name: OIDWhoAmI, 320 | }) 321 | if err != nil { 322 | return "", err 323 | } 324 | res, err := parseExtendedResponse(pkt) 325 | if err != nil { 326 | return "", err 327 | } 328 | if err := res.BaseResponse.Err(); err != nil { 329 | return "", err 330 | } 331 | if len(res.Value) == 0 { 332 | return "anonymous", nil 333 | } 334 | return string(res.Value), nil 335 | } 336 | -------------------------------------------------------------------------------- /ldap/client_test.go: -------------------------------------------------------------------------------- 1 | package ldap_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/samuel/go-ldap/ldap" 7 | ) 8 | 9 | func TestClientBind(t *testing.T) { 10 | t.Parallel() 11 | c, err := ldap.Dial("tcp", "127.0.0.1:1389") 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | if err := c.Bind("cn=test", nil); err != nil { 16 | t.Fatal(err) 17 | } 18 | if err := c.Bind("cn=test", []byte("verysecure")); err != nil { 19 | t.Fatal(err) 20 | } 21 | } 22 | 23 | func TestClientDelete(t *testing.T) { 24 | t.Parallel() 25 | c, err := ldap.Dial("tcp", "127.0.0.1:1389") 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | if err := c.Delete("cn=test"); err != nil { 30 | t.Fatal(err) 31 | } 32 | } 33 | 34 | func TestClientSearch(t *testing.T) { 35 | t.Parallel() 36 | c, err := ldap.Dial("tcp", "127.0.0.1:1389") 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | req := &ldap.SearchRequest{ 41 | Scope: ldap.ScopeWholeSubtree, 42 | } 43 | if res, err := c.Search(req); err != nil { 44 | t.Fatal(err) 45 | } else { 46 | for _, r := range res { 47 | t.Logf("%+v\n", r) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ldap/delete.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import "io" 4 | 5 | type DeleteRequest struct { 6 | DN string 7 | } 8 | 9 | type DeleteResponse struct { 10 | BaseResponse 11 | } 12 | 13 | func parseDeleteRequest(pkt *Packet) (*DeleteRequest, error) { 14 | dn, ok := pkt.Str() 15 | if !ok { 16 | return nil, &ProtocolError{Reason: "invalid dn"} 17 | } 18 | return &DeleteRequest{DN: dn}, nil 19 | } 20 | 21 | func parseDeleteResponse(pkt *Packet) (*DeleteResponse, error) { 22 | res := &DeleteResponse{} 23 | if err := parseBaseResponse(pkt, &res.BaseResponse); err != nil { 24 | return nil, err 25 | } 26 | return res, nil 27 | } 28 | 29 | func (r *DeleteResponse) WritePackets(w io.Writer, msgID int) error { 30 | res := NewResponsePacket(msgID) 31 | pkt := res.AddItem(r.BaseResponse.NewPacket()) 32 | pkt.Tag = ApplicationDelResponse 33 | return res.Write(w) 34 | } 35 | 36 | func (r *DeleteRequest) WritePackets(w io.Writer, msgID int) error { 37 | req := NewRequestPacket(msgID) 38 | req.AddItem(NewPacket(ClassApplication, true, ApplicationDelRequest, r.DN)) 39 | return req.Write(w) 40 | } 41 | -------------------------------------------------------------------------------- /ldap/extended.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import "io" 4 | 5 | type ExtendedRequest struct { 6 | Name string 7 | Value []byte 8 | } 9 | 10 | type ExtendedResponse struct { 11 | BaseResponse 12 | Name string 13 | Value []byte 14 | } 15 | 16 | func (r *ExtendedResponse) WritePackets(w io.Writer, msgID int) error { 17 | res := NewResponsePacket(msgID) 18 | pkt := res.AddItem(r.BaseResponse.NewPacket()) 19 | pkt.Tag = ApplicationExtendedResponse 20 | if r.Name != "" { 21 | pkt.AddItem(NewPacket(ClassContext, true, 10, r.Name)) 22 | } 23 | if r.Value != nil { 24 | pkt.AddItem(NewPacket(ClassContext, true, 11, r.Value)) 25 | } 26 | return res.Write(w) 27 | } 28 | 29 | func (r *ExtendedRequest) WritePackets(w io.Writer, msgID int) error { 30 | pkt := NewPacket(ClassApplication, false, ApplicationExtendedRequest, nil) 31 | if r.Name != "" { 32 | pkt.AddItem(NewPacket(ClassContext, true, 0, r.Name)) 33 | } 34 | if r.Value != nil { 35 | pkt.AddItem(NewPacket(ClassContext, true, 1, r.Value)) 36 | } 37 | req := NewRequestPacket(msgID) 38 | req.AddItem(pkt) 39 | return req.Write(w) 40 | } 41 | 42 | func parseExtendedResponse(pkt *Packet) (*ExtendedResponse, error) { 43 | res := &ExtendedResponse{} 44 | if err := parseBaseResponse(pkt, &res.BaseResponse); err != nil { 45 | return nil, err 46 | } 47 | var ok bool 48 | for _, it := range pkt.Items[3:] { 49 | switch it.Tag { 50 | case 10: 51 | res.Name, ok = it.Str() 52 | if !ok { 53 | return nil, &ProtocolError{Reason: "invalid extended response oid"} 54 | } 55 | case 11: 56 | res.Value, ok = it.Bytes() 57 | if !ok { 58 | return nil, &ProtocolError{Reason: "invalid extended response value"} 59 | } 60 | default: 61 | return nil, &ProtocolError{Reason: "unsupported extended response tag"} 62 | } 63 | } 64 | return res, nil 65 | } 66 | 67 | func parseExtendedRequest(pkt *Packet) (*ExtendedRequest, error) { 68 | var ok bool 69 | req := &ExtendedRequest{} 70 | if len(pkt.Items) > 2 { 71 | return nil, &ProtocolError{Reason: "too many tags for extended request"} 72 | } 73 | for _, it := range pkt.Items { 74 | switch it.Tag { 75 | case 0: 76 | req.Name, ok = it.Str() 77 | if !ok { 78 | return nil, &ProtocolError{Reason: "invalid extended request oid"} 79 | } 80 | case 1: 81 | req.Value, ok = it.Bytes() 82 | if !ok { 83 | return nil, &ProtocolError{Reason: "invalid extended request value"} 84 | } 85 | default: 86 | return nil, &ProtocolError{Reason: "unsupported extended request tag"} 87 | } 88 | } 89 | return req, nil 90 | } 91 | 92 | type PasswordModifyRequest struct { 93 | UserIdentity string 94 | OldPassword []byte 95 | NewPassword []byte 96 | } 97 | 98 | type PasswordModifyResponse struct { 99 | GenPassword []byte // [0] OCTET STRING OPTIONAL 100 | } 101 | 102 | func parsePasswordModifyRequest(pkt *Packet) (*PasswordModifyRequest, error) { 103 | var ok bool 104 | req := &PasswordModifyRequest{} 105 | for _, it := range pkt.Items { 106 | switch it.Tag { 107 | case 0: 108 | req.UserIdentity, ok = it.Str() 109 | if !ok { 110 | return nil, &ProtocolError{Reason: "invalid user identity tag"} 111 | } 112 | case 1: 113 | req.OldPassword, ok = it.Bytes() 114 | if !ok { 115 | return nil, &ProtocolError{Reason: "invalid old password tag"} 116 | } 117 | case 2: 118 | req.NewPassword, ok = it.Bytes() 119 | if !ok { 120 | return nil, &ProtocolError{Reason: "invalid new password tag"} 121 | } 122 | default: 123 | return nil, &ProtocolError{Reason: "unknown tag"} 124 | } 125 | } 126 | return req, nil 127 | } 128 | -------------------------------------------------------------------------------- /ldap/filter.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | // TODO: better validation especially of attribute names 4 | 5 | import ( 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | "unicode/utf8" 10 | ) 11 | 12 | const ( 13 | filterTagAND = 0 14 | filterTagOR = 1 15 | filterTagNOT = 2 16 | filterTagEqualityMatch = 3 17 | filterTagSubstrings = 4 18 | filterTagGreaterOrEqual = 5 19 | filterTagLessOrEqual = 6 20 | filterTagPresent = 7 21 | filterTagApproxMatch = 8 22 | filterTagExtensibleMatch = 9 23 | ) 24 | 25 | type ErrFilterSyntaxError struct { 26 | Pos int 27 | Msg string 28 | } 29 | 30 | func (e *ErrFilterSyntaxError) Error() string { 31 | return fmt.Sprintf("ldap: filter syntax error at position %d: %s", e.Pos, e.Msg) 32 | } 33 | 34 | type Filter interface { 35 | String() string 36 | Encode() (*Packet, error) 37 | } 38 | 39 | type AND struct { 40 | Filters []Filter 41 | } 42 | 43 | func (a *AND) String() string { 44 | s := make([]string, len(a.Filters)) 45 | for i, f := range a.Filters { 46 | s[i] = f.String() 47 | } 48 | return fmt.Sprintf("(&%s)", strings.Join(s, "")) 49 | } 50 | 51 | func (a *AND) Encode() (*Packet, error) { 52 | pkt := NewPacket(ClassContext, false, filterTagAND, nil) 53 | for _, f := range a.Filters { 54 | p, err := f.Encode() 55 | if err != nil { 56 | return nil, err 57 | } 58 | pkt.AddItem(p) 59 | } 60 | return pkt, nil 61 | } 62 | 63 | type OR struct { 64 | Filters []Filter 65 | } 66 | 67 | func (o *OR) Encode() (*Packet, error) { 68 | pkt := NewPacket(ClassContext, false, filterTagOR, nil) 69 | for _, f := range o.Filters { 70 | p, err := f.Encode() 71 | if err != nil { 72 | return nil, err 73 | } 74 | pkt.AddItem(p) 75 | } 76 | return pkt, nil 77 | } 78 | 79 | func (o *OR) String() string { 80 | s := make([]string, len(o.Filters)) 81 | for i, f := range o.Filters { 82 | s[i] = f.String() 83 | } 84 | return fmt.Sprintf("(|%s)", strings.Join(s, "")) 85 | } 86 | 87 | type NOT struct { 88 | Filter 89 | } 90 | 91 | func (n *NOT) Encode() (*Packet, error) { 92 | pkt := NewPacket(ClassContext, false, filterTagNOT, nil) 93 | p, err := n.Filter.Encode() 94 | if err != nil { 95 | return nil, err 96 | } 97 | pkt.AddItem(p) 98 | return pkt, nil 99 | } 100 | 101 | func (n *NOT) String() string { 102 | return fmt.Sprintf("(!%s)", n.Filter.String()) 103 | } 104 | 105 | type AttributeValueAssertion struct { 106 | Attribute string 107 | Value []byte 108 | } 109 | 110 | type EqualityMatch AttributeValueAssertion 111 | 112 | func (f *EqualityMatch) Encode() (*Packet, error) { 113 | pkt := NewPacket(ClassContext, false, filterTagEqualityMatch, nil) 114 | pkt.AddItem(NewPacket(ClassUniversal, true, TagOctetString, f.Attribute)) 115 | pkt.AddItem(NewPacket(ClassUniversal, true, TagOctetString, f.Value)) 116 | return pkt, nil 117 | } 118 | 119 | func (f *EqualityMatch) String() string { 120 | return fmt.Sprintf("(%s=%s)", filterEscape(f.Attribute), filterEscape(string(f.Value))) 121 | } 122 | 123 | type GreaterOrEqual AttributeValueAssertion 124 | 125 | func (f *GreaterOrEqual) Encode() (*Packet, error) { 126 | pkt := NewPacket(ClassContext, false, filterTagGreaterOrEqual, nil) 127 | pkt.AddItem(NewPacket(ClassUniversal, true, TagOctetString, f.Attribute)) 128 | pkt.AddItem(NewPacket(ClassUniversal, true, TagOctetString, f.Value)) 129 | return pkt, nil 130 | } 131 | 132 | func (f *GreaterOrEqual) String() string { 133 | return fmt.Sprintf("(%s>=%s)", filterEscape(f.Attribute), filterEscape(string(f.Value))) 134 | } 135 | 136 | type LessOrEqual AttributeValueAssertion 137 | 138 | func (f *LessOrEqual) Encode() (*Packet, error) { 139 | pkt := NewPacket(ClassContext, false, filterTagLessOrEqual, nil) 140 | pkt.AddItem(NewPacket(ClassUniversal, true, TagOctetString, f.Attribute)) 141 | pkt.AddItem(NewPacket(ClassUniversal, true, TagOctetString, f.Value)) 142 | return pkt, nil 143 | } 144 | 145 | func (f *LessOrEqual) String() string { 146 | return fmt.Sprintf("(%s<=%s)", filterEscape(f.Attribute), filterEscape(string(f.Value))) 147 | } 148 | 149 | type ApproxMatch AttributeValueAssertion 150 | 151 | func (f *ApproxMatch) Encode() (*Packet, error) { 152 | pkt := NewPacket(ClassContext, false, filterTagApproxMatch, nil) 153 | pkt.AddItem(NewPacket(ClassUniversal, true, TagOctetString, f.Attribute)) 154 | pkt.AddItem(NewPacket(ClassUniversal, true, TagOctetString, f.Value)) 155 | return pkt, nil 156 | } 157 | 158 | func (f *ApproxMatch) String() string { 159 | return fmt.Sprintf("(%s~=%s)", filterEscape(f.Attribute), filterEscape(string(f.Value))) 160 | } 161 | 162 | type Present struct { 163 | Attribute string 164 | } 165 | 166 | func (f *Present) Encode() (*Packet, error) { 167 | return NewPacket(ClassContext, true, filterTagPresent, f.Attribute), nil 168 | } 169 | 170 | func (f *Present) String() string { 171 | return fmt.Sprintf("(%s=*)", filterEscape(f.Attribute)) 172 | } 173 | 174 | type Substrings struct { 175 | Attribute string 176 | Initial string 177 | Final string 178 | Any []string 179 | } 180 | 181 | func (s *Substrings) Encode() (*Packet, error) { 182 | pkt := NewPacket(ClassContext, false, filterTagSubstrings, nil) 183 | pkt.AddItem(NewPacket(ClassUniversal, true, TagOctetString, s.Attribute)) 184 | p := pkt.AddItem(NewPacket(ClassUniversal, false, TagSequence, nil)) 185 | if s.Initial != "" { 186 | p.AddItem(NewPacket(ClassContext, true, 0, s.Initial)) 187 | } 188 | for _, a := range s.Any { 189 | if a != "" { 190 | p.AddItem(NewPacket(ClassContext, true, 1, a)) 191 | } 192 | } 193 | if s.Final != "" { 194 | p.AddItem(NewPacket(ClassContext, true, 2, s.Final)) 195 | } 196 | return pkt, nil 197 | } 198 | 199 | func (s *Substrings) String() string { 200 | n := len(s.Any) + 2 201 | parts := make([]string, n) 202 | parts[0] = filterEscape(s.Initial) 203 | parts[len(parts)-1] = filterEscape(s.Final) 204 | for i, s := range s.Any { 205 | parts[i+1] = filterEscape(s) 206 | } 207 | return fmt.Sprintf("(%s=%s)", filterEscape(s.Attribute), strings.Join(parts, "*")) 208 | } 209 | 210 | type tokenizer struct { 211 | s string 212 | pos int // byte position 213 | cpos int // character position 214 | } 215 | 216 | func (t *tokenizer) next() rune { 217 | if t.pos == len(t.s) { 218 | return 0 219 | } 220 | r, size := utf8.DecodeRuneInString(t.s[t.pos:]) 221 | t.pos += size 222 | t.cpos++ 223 | return r 224 | } 225 | 226 | var escapes = map[rune][]rune{ 227 | '(': []rune(`\28`), 228 | ')': []rune(`\29`), 229 | '&': []rune(`\26`), 230 | '|': []rune(`\3c`), 231 | '=': []rune(`\3d`), 232 | '>': []rune(`\3e`), 233 | '<': []rune(`\3c`), 234 | '~': []rune(`\7e`), 235 | '*': []rune(`\2a`), 236 | '/': []rune(`\2f`), 237 | '\\': []rune(`\5c`), 238 | } 239 | 240 | func filterEscape(s string) string { 241 | out := make([]rune, 0, len(s)) 242 | for _, r := range s { 243 | if e := escapes[r]; e != nil { 244 | out = append(out, e...) 245 | } else { 246 | out = append(out, r) 247 | } 248 | } 249 | return string(out) 250 | } 251 | 252 | func ParseFilter(filter string) (Filter, error) { 253 | if len(filter) == 0 { 254 | return nil, &ErrFilterSyntaxError{Pos: 0, Msg: "empty filter"} 255 | } 256 | tok := &tokenizer{s: filter} 257 | return parseFilter(tok, false) 258 | } 259 | 260 | func parseFilter(tok *tokenizer, checkClose bool) (Filter, error) { 261 | r := tok.next() 262 | if checkClose && r == ')' { 263 | tok.pos-- 264 | return nil, nil 265 | } else if r != '(' { 266 | return nil, &ErrFilterSyntaxError{Pos: tok.cpos - 1, Msg: "expected ("} 267 | } 268 | var filter Filter 269 | r = tok.next() 270 | switch r { 271 | case 0, utf8.RuneError: 272 | return nil, &ErrFilterSyntaxError{Pos: tok.cpos, Msg: "unxpected end of filter"} 273 | case '&', '|': 274 | var filters []Filter 275 | for { 276 | f, err := parseFilter(tok, true) 277 | if err != nil { 278 | return nil, err 279 | } 280 | if f == nil { 281 | break 282 | } 283 | filters = append(filters, f) 284 | } 285 | switch r { 286 | case '&': 287 | filter = &AND{Filters: filters} 288 | case '|': 289 | filter = &OR{Filters: filters} 290 | } 291 | case '!': 292 | f, err := parseFilter(tok, false) 293 | if err != nil { 294 | return nil, err 295 | } 296 | filter = &NOT{Filter: f} 297 | default: 298 | name := []rune{r} 299 | var op string 300 | for op == "" { 301 | r = tok.next() 302 | switch r { 303 | case 0, utf8.RuneError: 304 | return nil, &ErrFilterSyntaxError{Pos: tok.cpos, Msg: "unxpected end of filter"} 305 | case '=': 306 | op = "=" 307 | case '>', '<', '~': 308 | op = string(r) + "=" 309 | if r2 := tok.next(); r2 != '=' { 310 | return nil, &ErrFilterSyntaxError{Pos: tok.cpos - 1, Msg: "expected = after " + string(r)} 311 | } 312 | case '\\': 313 | // hex code 314 | r1 := tok.next() 315 | r2 := tok.next() 316 | if r1 == 0 || r2 == 0 || r1 == utf8.RuneError || r2 == utf8.RuneError { 317 | return nil, &ErrFilterSyntaxError{Pos: tok.cpos, Msg: "unxpected end of filter"} 318 | } 319 | h := string(r1) + string(r2) 320 | n, err := strconv.ParseInt(h, 16, 8) 321 | if err != nil { 322 | return nil, &ErrFilterSyntaxError{Pos: tok.cpos - 2, Msg: "unable to parse hex code: " + err.Error()} 323 | } 324 | name = append(name, rune(n)) 325 | default: 326 | name = append(name, r) 327 | } 328 | } 329 | var value []rune 330 | hasStar := false 331 | valueLoop: 332 | for { 333 | r = tok.next() 334 | if r == '*' { 335 | hasStar = true 336 | } 337 | switch r { 338 | case 0, utf8.RuneError: 339 | return nil, &ErrFilterSyntaxError{Pos: tok.cpos, Msg: "unxpected end of filter"} 340 | case ')': 341 | tok.pos-- 342 | break valueLoop 343 | case '\\': 344 | // hex code 345 | r1 := tok.next() 346 | r2 := tok.next() 347 | if r1 == 0 || r2 == 0 || r1 == utf8.RuneError || r2 == utf8.RuneError { 348 | return nil, &ErrFilterSyntaxError{Pos: tok.cpos, Msg: "unxpected end of filter"} 349 | } 350 | h := string(r1) + string(r2) 351 | n, err := strconv.ParseInt(h, 16, 8) 352 | if err != nil { 353 | return nil, &ErrFilterSyntaxError{Pos: tok.cpos - 2, Msg: "unable to parse hex code: " + err.Error()} 354 | } 355 | value = append(value, rune(n)) 356 | default: 357 | value = append(value, r) 358 | } 359 | } 360 | nameS := string(name) 361 | valueS := string(value) 362 | switch { 363 | case valueS == "*": 364 | if op != "=" { 365 | return nil, &ErrFilterSyntaxError{Pos: tok.cpos, Msg: "* value for non = op"} 366 | } 367 | filter = &Present{Attribute: nameS} 368 | case hasStar: 369 | if op != "=" { 370 | return nil, &ErrFilterSyntaxError{Pos: tok.cpos, Msg: "non equality substring match not allowed"} 371 | } 372 | // substring match 373 | parts := strings.Split(valueS, "*") 374 | filter = &Substrings{ 375 | Attribute: nameS, 376 | Initial: parts[0], 377 | Final: parts[len(parts)-1], 378 | Any: parts[1 : len(parts)-1], 379 | } 380 | default: 381 | switch op { 382 | case "=": 383 | filter = &EqualityMatch{Attribute: nameS, Value: []byte(valueS)} 384 | case ">=": 385 | filter = &GreaterOrEqual{Attribute: nameS, Value: []byte(valueS)} 386 | case "<=": 387 | filter = &LessOrEqual{Attribute: nameS, Value: []byte(valueS)} 388 | case "~=": 389 | filter = &ApproxMatch{Attribute: nameS, Value: []byte(valueS)} 390 | } 391 | } 392 | } 393 | if r := tok.next(); r != ')' { 394 | return nil, &ErrFilterSyntaxError{Pos: tok.cpos - 1, Msg: "expected )"} 395 | } 396 | return filter, nil 397 | } 398 | 399 | func parseSearchFilter(pkt *Packet) (Filter, error) { 400 | switch pkt.Tag { 401 | case filterTagAND: 402 | fAnd := &AND{} 403 | for _, c := range pkt.Items { 404 | f, err := parseSearchFilter(c) 405 | if err != nil { 406 | return nil, err 407 | } 408 | fAnd.Filters = append(fAnd.Filters, f) 409 | } 410 | return fAnd, nil 411 | case filterTagOR: 412 | fOr := &OR{} 413 | for _, c := range pkt.Items { 414 | f, err := parseSearchFilter(c) 415 | if err != nil { 416 | return nil, err 417 | } 418 | fOr.Filters = append(fOr.Filters, f) 419 | } 420 | return fOr, nil 421 | case filterTagNOT: 422 | f, err := parseSearchFilter(pkt.Items[0]) 423 | if err != nil { 424 | return nil, err 425 | } 426 | return &NOT{ 427 | Filter: f, 428 | }, nil 429 | case filterTagEqualityMatch: 430 | var ok bool 431 | f := &EqualityMatch{} 432 | if f.Attribute, ok = pkt.Items[0].Str(); !ok { 433 | return nil, &ProtocolError{Reason: "failed to parse equalityMatch.attribute in filter"} 434 | } 435 | if f.Value, ok = pkt.Items[1].Bytes(); !ok { 436 | return nil, &ProtocolError{Reason: "failed to parse equalityMatch.value in filter"} 437 | } 438 | return f, nil 439 | case filterTagSubstrings: 440 | var ok bool 441 | q := &Substrings{} 442 | if q.Attribute, ok = pkt.Items[0].Str(); !ok { 443 | return nil, &ProtocolError{Reason: "failed to parse substrings.attribute in filter"} 444 | } 445 | for i, c := range pkt.Items[1].Items { 446 | switch c.Tag { 447 | case 0: // initial 448 | if i != 0 { 449 | return nil, &ProtocolError{Reason: "search filter substrings has final as non-first child"} 450 | } 451 | var ok bool 452 | if q.Initial, ok = c.Str(); !ok { 453 | return nil, &ProtocolError{Reason: "failed to parse initial in search filter"} 454 | } 455 | case 1: // Any 456 | s, ok := c.Str() 457 | if !ok { 458 | return nil, &ProtocolError{Reason: "failed to parse any in search filter"} 459 | } 460 | q.Any = append(q.Any, s) 461 | case 2: // Final 462 | if i != len(pkt.Items[1].Items)-1 { 463 | return nil, &ProtocolError{Reason: "search filter substrings has final as non-last child"} 464 | } 465 | var ok bool 466 | if q.Final, ok = c.Str(); !ok { 467 | return nil, &ProtocolError{Reason: "failed to parse final in search filter"} 468 | } 469 | default: 470 | return nil, &ProtocolError{Reason: fmt.Sprintf("unknown filter substring type %d", c.Tag)} 471 | } 472 | } 473 | return q, nil 474 | case filterTagGreaterOrEqual: 475 | var ok bool 476 | f := &GreaterOrEqual{} 477 | if f.Attribute, ok = pkt.Items[0].Str(); !ok { 478 | return nil, &ProtocolError{Reason: "failed to parse greaterOrEqual.attribute in filter"} 479 | } 480 | if f.Value, ok = pkt.Items[1].Bytes(); !ok { 481 | return nil, &ProtocolError{Reason: "failed to parse greaterOrEqual.value in filter"} 482 | } 483 | return f, nil 484 | case filterTagLessOrEqual: 485 | var ok bool 486 | f := &LessOrEqual{} 487 | if f.Attribute, ok = pkt.Items[0].Str(); !ok { 488 | return nil, &ProtocolError{Reason: "failed to parse lessOrEqual.attribute in filter"} 489 | } 490 | if f.Value, ok = pkt.Items[1].Bytes(); !ok { 491 | return nil, &ProtocolError{Reason: "failed to parse lessOrEqual.value in filter"} 492 | } 493 | return f, nil 494 | case filterTagPresent: 495 | attr, ok := pkt.Str() 496 | if !ok { 497 | return nil, &ProtocolError{Reason: "failed to parse present in search filter"} 498 | } 499 | return &Present{ 500 | Attribute: attr, 501 | }, nil 502 | case filterTagApproxMatch: 503 | var ok bool 504 | f := &ApproxMatch{} 505 | if f.Attribute, ok = pkt.Items[0].Str(); !ok { 506 | return nil, &ProtocolError{Reason: "failed to parse approxMatch.attribute in filter"} 507 | } 508 | if f.Value, ok = pkt.Items[1].Bytes(); !ok { 509 | return nil, &ProtocolError{Reason: "failed to parse approxMatch.value in filter"} 510 | } 511 | return f, nil 512 | case filterTagExtensibleMatch: 513 | // TODO 514 | } 515 | return nil, &ProtocolError{Reason: fmt.Sprintf("unknown filter tag %d", pkt.Tag)} 516 | } 517 | -------------------------------------------------------------------------------- /ldap/filter_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import "testing" 4 | 5 | func TestParseFilter(t *testing.T) { 6 | t.Parallel() 7 | cases := []string{ 8 | "(present=*)", 9 | "(less<=123)", 10 | "(greater>=123)", 11 | "(approx~=abc)", 12 | "(!(not=123))", 13 | "(&(abc=123)(easy<=hard))", 14 | "(|(abc=123)(easy<=hard))", 15 | "(escaped=\\28\\29)", 16 | "(substr=prefix*mid1*mid2*suffix)", 17 | "(prefix=prefix*)", 18 | "(suffix=*suffix)", 19 | "(middle=*middle*)", 20 | } 21 | for _, c := range cases { 22 | if f, err := ParseFilter(c); err != nil { 23 | t.Errorf("Failed to parse '%s': %s", c, err.Error()) 24 | } else if f.String() != c { 25 | t.Errorf("Parse filter '%s' != '%s'", c, f.String()) 26 | } 27 | } 28 | } 29 | 30 | func TestFilterEncoding(t *testing.T) { 31 | t.Parallel() 32 | cases := []Filter{ 33 | &Present{ 34 | Attribute: "attr", 35 | }, 36 | &GreaterOrEqual{ 37 | Attribute: "foo", 38 | Value: []byte("bar"), 39 | }, 40 | &LessOrEqual{ 41 | Attribute: "foo", 42 | Value: []byte("bar"), 43 | }, 44 | &ApproxMatch{ 45 | Attribute: "foo", 46 | Value: []byte{1, 2, 3}, 47 | }, 48 | &NOT{Filter: &EqualityMatch{ 49 | Attribute: "abc", 50 | Value: []byte("123"), 51 | }}, 52 | &AND{ 53 | Filters: []Filter{&EqualityMatch{ 54 | Attribute: "abc", 55 | Value: []byte("123"), 56 | }}, 57 | }, 58 | &OR{ 59 | Filters: []Filter{&EqualityMatch{ 60 | Attribute: "or", 61 | Value: []byte("123"), 62 | }}, 63 | }, 64 | &Substrings{ 65 | Attribute: "attr", 66 | Initial: "init", 67 | Final: "final", 68 | Any: []string{"one", "two"}, 69 | }, 70 | } 71 | for _, c := range cases { 72 | pkt, err := c.Encode() 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | f, err := parseSearchFilter(pkt) 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | if c.String() != f.String() { 81 | t.Errorf("'%s' != '%s'", f.String(), c.String()) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /ldap/ldap.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | // http://www.iana.org/assignments/ldap-parameters/ldap-parameters.xml 9 | 10 | const protocolVersion = 3 11 | 12 | // Controls 13 | const ( 14 | OIDContentSynchControl = "1.3.6.1.4.1.4203.1.9.1.1" // https://tools.ietf.org/html/rfc4533 15 | OIDProxiedAuthControl = "2.16.840.1.113730.3.4.18" // https://tools.ietf.org/html/rfc4370 16 | OIDNamedSubordinateReferenceControl = "2.16.840.1.113730.3.4.2" // https://tools.ietf.org/html/rfc3296 17 | ) 18 | 19 | // Extensions 20 | const ( 21 | OIDCancel = "1.3.6.1.1.8" // https://tools.ietf.org/html/rfc3909 22 | OIDStartTLS = "1.3.6.1.4.1.1466.20037" // http://www.iana.org/go/rfc4511 - http://www.iana.org/go/rfc4513 23 | OIDPasswordModify = "1.3.6.1.4.1.4203.1.11.1" // http://www.iana.org/go/rfc3062 24 | OIDWhoAmI = "1.3.6.1.4.1.4203.1.11.3" // http://www.iana.org/go/rfc4532 25 | ) 26 | 27 | // Features 28 | const ( 29 | OIDModifyIncrement = "1.3.6.1.1.14" // http://www.iana.org/go/rfc4525 30 | OIDAllOperationalAttributes = "1.3.6.1.4.1.4203.1.5.1" // https://www.rfc-editor.org/rfc/rfc3673.txt 31 | OIDAttributesByObjectClass = "1.3.6.1.4.1.4203.1.5.2" // https://tools.ietf.org/html/rfc4529 32 | OIDTrueFalseFilters = "1.3.6.1.4.1.4203.1.5.3" // https://tools.ietf.org/html/rfc4526 33 | OIDLanguageTagOptions = "1.3.6.1.4.1.4203.1.5.4" // https://tools.ietf.org/html/rfc3866 34 | OIDLanguageRangeOptions = "1.3.6.1.4.1.4203.1.5.5" // http://tools.ietf.org/html/rfc3866 35 | ) 36 | 37 | var RootDSE = map[string][]string{ 38 | "supportedLDAPVersion": { 39 | "3", 40 | }, 41 | "supportedFeatures": { 42 | OIDModifyIncrement, 43 | OIDAllOperationalAttributes, 44 | }, 45 | "supportedExtension": { 46 | OIDWhoAmI, 47 | OIDPasswordModify, 48 | }, 49 | "supportedSASLMechanisms": {}, 50 | } 51 | 52 | const ( 53 | ApplicationBindRequest = 0 54 | ApplicationBindResponse = 1 55 | ApplicationUnbindRequest = 2 56 | ApplicationSearchRequest = 3 57 | ApplicationSearchResultEntry = 4 58 | ApplicationSearchResultDone = 5 59 | ApplicationModifyRequest = 6 60 | ApplicationModifyResponse = 7 61 | ApplicationAddRequest = 8 62 | ApplicationAddResponse = 9 63 | ApplicationDelRequest = 10 64 | ApplicationDelResponse = 11 65 | ApplicationModifyDNRequest = 12 66 | ApplicationModifyDNResponse = 13 67 | ApplicationCompareRequest = 14 68 | ApplicationCompareResponse = 15 69 | ApplicationAbandonRequest = 16 70 | ApplicationSearchResultReference = 19 71 | ApplicationExtendedRequest = 23 72 | ApplicationExtendedResponse = 24 73 | ) 74 | 75 | var ApplicationMap = map[uint8]string{ 76 | ApplicationBindRequest: "Bind Request", 77 | ApplicationBindResponse: "Bind Response", 78 | ApplicationUnbindRequest: "Unbind Request", 79 | ApplicationSearchRequest: "Search Request", 80 | ApplicationSearchResultEntry: "Search Result Entry", 81 | ApplicationSearchResultDone: "Search Result Done", 82 | ApplicationModifyRequest: "Modify Request", 83 | ApplicationModifyResponse: "Modify Response", 84 | ApplicationAddRequest: "Add Request", 85 | ApplicationAddResponse: "Add Response", 86 | ApplicationDelRequest: "Del Request", 87 | ApplicationDelResponse: "Del Response", 88 | ApplicationModifyDNRequest: "Modify DN Request", 89 | ApplicationModifyDNResponse: "Modify DN Response", 90 | ApplicationCompareRequest: "Compare Request", 91 | ApplicationCompareResponse: "Compare Response", 92 | ApplicationAbandonRequest: "Abandon Request", 93 | ApplicationSearchResultReference: "Search Result Reference", 94 | ApplicationExtendedRequest: "Extended Request", 95 | ApplicationExtendedResponse: "Extended Response", 96 | } 97 | 98 | type ResultCode byte 99 | 100 | const ( 101 | ResultSuccess ResultCode = 0 102 | ResultOperationsError ResultCode = 1 103 | ResultProtocolError ResultCode = 2 104 | ResultTimeLimitExceeded ResultCode = 3 105 | ResultSizeLimitExceeded ResultCode = 4 106 | ResultCompareFalse ResultCode = 5 107 | ResultCompareTrue ResultCode = 6 108 | ResultAuthMethodNotSupported ResultCode = 7 109 | ResultStrongAuthRequired ResultCode = 8 110 | ResultReferral ResultCode = 10 111 | ResultAdminLimitExceeded ResultCode = 11 112 | ResultUnavailableCriticalExtension ResultCode = 12 113 | ResultConfidentialityRequired ResultCode = 13 114 | ResultSaslBindInProgress ResultCode = 14 115 | ResultNoSuchAttribute ResultCode = 16 116 | ResultUndefinedAttributeType ResultCode = 17 117 | ResultInappropriateMatching ResultCode = 18 118 | ResultConstraintViolation ResultCode = 19 119 | ResultAttributeOrValueExists ResultCode = 20 120 | ResultInvalidAttributeSyntax ResultCode = 21 121 | ResultNoSuchObject ResultCode = 32 122 | ResultAliasProblem ResultCode = 33 123 | ResultInvalidDNSyntax ResultCode = 34 124 | ResultAliasDereferencingProblem ResultCode = 36 125 | ResultInappropriateAuthentication ResultCode = 48 126 | ResultInvalidCredentials ResultCode = 49 127 | ResultInsufficientAccessRights ResultCode = 50 128 | ResultBusy ResultCode = 51 129 | ResultUnavailable ResultCode = 52 130 | ResultUnwillingToPerform ResultCode = 53 131 | ResultLoopDetect ResultCode = 54 132 | ResultNamingViolation ResultCode = 64 133 | ResultObjectClassViolation ResultCode = 65 134 | ResultNotAllowedOnNonLeaf ResultCode = 66 135 | ResultNotAllowedOnRDN ResultCode = 67 136 | ResultEntryAlreadyExists ResultCode = 68 137 | ResultObjectClassModsProhibited ResultCode = 69 138 | ResultAffectsMultipleDSAs ResultCode = 71 139 | ResultOther ResultCode = 80 140 | ) 141 | 142 | var ResultCodeMap = map[ResultCode]string{ 143 | ResultSuccess: "Success", 144 | ResultOperationsError: "Operations Error", 145 | ResultProtocolError: "Protocol Error", 146 | ResultTimeLimitExceeded: "Time Limit Exceeded", 147 | ResultSizeLimitExceeded: "Size Limit Exceeded", 148 | ResultCompareFalse: "Compare False", 149 | ResultCompareTrue: "Compare True", 150 | ResultAuthMethodNotSupported: "Auth Method Not Supported", 151 | ResultStrongAuthRequired: "Strong Auth Required", 152 | ResultReferral: "Referral", 153 | ResultAdminLimitExceeded: "Admin Limit Exceeded", 154 | ResultUnavailableCriticalExtension: "Unavailable Critical Extension", 155 | ResultConfidentialityRequired: "Confidentiality Required", 156 | ResultSaslBindInProgress: "Sasl Bind In Progress", 157 | ResultNoSuchAttribute: "No Such Attribute", 158 | ResultUndefinedAttributeType: "Undefined Attribute Type", 159 | ResultInappropriateMatching: "Inappropriate Matching", 160 | ResultConstraintViolation: "Constraint Violation", 161 | ResultAttributeOrValueExists: "Attribute Or Value Exists", 162 | ResultInvalidAttributeSyntax: "Invalid Attribute Syntax", 163 | ResultNoSuchObject: "No Such Object", 164 | ResultAliasProblem: "Alias Problem", 165 | ResultInvalidDNSyntax: "Invalid DN Syntax", 166 | ResultAliasDereferencingProblem: "Alias Dereferencing Problem", 167 | ResultInappropriateAuthentication: "Inappropriate Authentication", 168 | ResultInvalidCredentials: "Invalid Credentials", 169 | ResultInsufficientAccessRights: "Insufficient Access Rights", 170 | ResultBusy: "Busy", 171 | ResultUnavailable: "Unavailable", 172 | ResultUnwillingToPerform: "Unwilling To Perform", 173 | ResultLoopDetect: "Loop Detect", 174 | ResultNamingViolation: "Naming Violation", 175 | ResultObjectClassViolation: "Object Class Violation", 176 | ResultNotAllowedOnNonLeaf: "Not Allowed On Non Leaf", 177 | ResultNotAllowedOnRDN: "Not Allowed On RDN", 178 | ResultEntryAlreadyExists: "Entry Already Exists", 179 | ResultObjectClassModsProhibited: "Object Class Mods Prohibited", 180 | ResultAffectsMultipleDSAs: "Affects Multiple DSAs", 181 | ResultOther: "Other", 182 | } 183 | 184 | func (c ResultCode) String() string { 185 | s := ResultCodeMap[c] 186 | if s == "" { 187 | s = strconv.Itoa(int(c)) 188 | } 189 | return s 190 | } 191 | 192 | type UnsupportedRequestTagError struct { 193 | Tag int 194 | } 195 | 196 | func (e *UnsupportedRequestTagError) Error() string { 197 | return fmt.Sprintf("ldap: unsupported request tag %d", e.Tag) 198 | } 199 | 200 | type ProtocolError struct { 201 | Reason string 202 | } 203 | 204 | func (e *ProtocolError) Error() string { 205 | return "ldap: protocol error: " + e.Reason 206 | } 207 | -------------------------------------------------------------------------------- /ldap/modify.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | type ModType int 9 | 10 | const ( 11 | Add ModType = 0 12 | Delete ModType = 1 13 | Replace ModType = 2 14 | Increment ModType = 3 15 | ) 16 | 17 | func (mt ModType) String() string { 18 | switch mt { 19 | case Add: 20 | return "Add" 21 | case Delete: 22 | return "Delete" 23 | case Replace: 24 | return "Replace" 25 | case Increment: 26 | return "Increment" 27 | } 28 | return fmt.Sprintf("ModType(%d)", int(mt)) 29 | } 30 | 31 | type Mod struct { 32 | Type ModType 33 | Name string 34 | Values [][]byte 35 | } 36 | 37 | type ModifyRequest struct { 38 | DN string 39 | Mods []*Mod 40 | } 41 | 42 | type ModifyResponse struct { 43 | BaseResponse 44 | } 45 | 46 | func parseModifyRequest(pkt *Packet) (*ModifyRequest, error) { 47 | if len(pkt.Items) != 2 { 48 | return nil, &ProtocolError{Reason: "modify request requires exactly 2 items"} 49 | } 50 | dn, ok := pkt.Items[0].Str() 51 | if !ok { 52 | return nil, &ProtocolError{Reason: "invalid dn"} 53 | } 54 | req := &ModifyRequest{DN: dn} 55 | for _, it := range pkt.Items[1].Items { 56 | if len(it.Items) != 2 || len(it.Items[1].Items) != 2 { 57 | return nil, &ProtocolError{Reason: "mod operation requires 2 items"} 58 | } 59 | mod := &Mod{} 60 | typ, ok := it.Items[0].Int() 61 | if !ok { 62 | return nil, &ProtocolError{Reason: "invalid mod op"} 63 | } 64 | mod.Type = ModType(typ) 65 | mod.Name, ok = it.Items[1].Items[0].Str() 66 | if !ok { 67 | return nil, &ProtocolError{Reason: "invalid attribute name"} 68 | } 69 | mod.Values = make([][]byte, len(it.Items[1].Items[1].Items)) 70 | for i, c := range it.Items[1].Items[1].Items { 71 | val, ok := c.Bytes() 72 | if !ok { 73 | return nil, &ProtocolError{Reason: "invalid attribute value"} 74 | } 75 | mod.Values[i] = val 76 | } 77 | req.Mods = append(req.Mods, mod) 78 | } 79 | return req, nil 80 | } 81 | 82 | func (r *ModifyRequest) WritePackets(w io.Writer, msgID int) error { 83 | req := NewRequestPacket(msgID) 84 | pkt := req.AddItem(NewPacket(ClassApplication, false, ApplicationModifyRequest, nil)) 85 | pkt.AddItem(NewPacket(ClassUniversal, true, TagOctetString, r.DN)) 86 | pkt = pkt.AddItem(NewPacket(ClassUniversal, false, TagSequence, nil)) 87 | for _, m := range r.Mods { 88 | p := pkt.AddItem(NewPacket(ClassUniversal, false, TagSequence, nil)) 89 | p.AddItem(NewPacket(ClassUniversal, true, TagEnumerated, int(m.Type))) 90 | p = p.AddItem(NewPacket(ClassUniversal, false, TagSequence, nil)) 91 | p.AddItem(NewPacket(ClassUniversal, true, TagOctetString, m.Name)) 92 | p = p.AddItem(NewPacket(ClassUniversal, false, TagSet, nil)) 93 | for _, v := range m.Values { 94 | p.AddItem(NewPacket(ClassUniversal, true, TagOctetString, v)) 95 | } 96 | } 97 | return req.Write(w) 98 | } 99 | 100 | func (r *ModifyResponse) WritePackets(w io.Writer, msgID int) error { 101 | res := NewResponsePacket(msgID) 102 | pkt := res.AddItem(r.BaseResponse.NewPacket()) 103 | pkt.Tag = ApplicationModifyResponse 104 | return res.Write(w) 105 | } 106 | -------------------------------------------------------------------------------- /ldap/modifydn.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import "io" 4 | 5 | type ModifyDNRequest struct { 6 | DN string 7 | NewRDN string 8 | DeleteOldRDN bool 9 | NewSuperior string 10 | } 11 | 12 | type ModifyDNResponse struct { 13 | BaseResponse 14 | } 15 | 16 | func parseModifyDNRequest(pkt *Packet) (*ModifyDNRequest, error) { 17 | if len(pkt.Items) < 3 || len(pkt.Items) > 4 { 18 | return nil, &ProtocolError{Reason: "wrong number of items"} 19 | } 20 | var ok bool 21 | req := &ModifyDNRequest{} 22 | req.DN, ok = pkt.Items[0].Str() 23 | if !ok { 24 | return nil, &ProtocolError{Reason: "invalid dn"} 25 | } 26 | req.NewRDN, ok = pkt.Items[1].Str() 27 | if !ok { 28 | return nil, &ProtocolError{Reason: "invalid newrdn"} 29 | } 30 | req.DeleteOldRDN, ok = pkt.Items[2].Bool() 31 | if !ok { 32 | return nil, &ProtocolError{Reason: "invalid deleteoldrdn"} 33 | } 34 | if len(pkt.Items) == 4 { 35 | req.NewSuperior, ok = pkt.Items[3].Str() 36 | if !ok { 37 | return nil, &ProtocolError{Reason: "invalid newSuperior"} 38 | } 39 | } 40 | return req, nil 41 | } 42 | 43 | func (r *ModifyDNResponse) WritePackets(w io.Writer, msgID int) error { 44 | res := NewResponsePacket(msgID) 45 | pkt := res.AddItem(r.BaseResponse.NewPacket()) 46 | pkt.Tag = ApplicationModifyDNResponse 47 | return res.Write(w) 48 | } 49 | -------------------------------------------------------------------------------- /ldap/search.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | "unicode/utf8" 9 | ) 10 | 11 | type Scope int 12 | 13 | const ( 14 | ScopeBaseObject Scope = 0 15 | ScopeSingleLevel Scope = 1 16 | ScopeWholeSubtree Scope = 2 17 | ScopeChildren Scope = 3 // used by ldapsearch/ldaptools (-s children) but not part of the standard 18 | ) 19 | 20 | var ScopeMap = map[Scope]string{ 21 | ScopeBaseObject: "Base Object", 22 | ScopeSingleLevel: "Single Level", 23 | ScopeWholeSubtree: "Whole Subtree", 24 | ScopeChildren: "Children", 25 | } 26 | 27 | func (sc Scope) String() string { 28 | if s := ScopeMap[sc]; s != "" { 29 | return s 30 | } 31 | return strconv.Itoa(int(sc)) 32 | } 33 | 34 | type DerefAliases int 35 | 36 | const ( 37 | NeverDerefAliases DerefAliases = 0 38 | DerefInSearching DerefAliases = 1 39 | DerefFindingBaseObj DerefAliases = 2 40 | DerefAlways DerefAliases = 3 41 | ) 42 | 43 | var DerefMap = map[DerefAliases]string{ 44 | NeverDerefAliases: "NeverDerefAliases", 45 | DerefInSearching: "DerefInSearching", 46 | DerefFindingBaseObj: "DerefFindingBaseObj", 47 | DerefAlways: "DerefAlways", 48 | } 49 | 50 | func (d DerefAliases) String() string { 51 | if s := DerefMap[d]; s != "" { 52 | return s 53 | } 54 | return strconv.Itoa(int(d)) 55 | } 56 | 57 | type ExtensibleMatch struct { 58 | MatchingRule string // optional 59 | Attribute string 60 | Value string 61 | DNAttributes bool 62 | } 63 | 64 | type SearchRequest struct { 65 | BaseDN string 66 | Scope Scope 67 | DerefAliases DerefAliases 68 | SizeLimit int 69 | TimeLimit int 70 | TypesOnly bool 71 | Filter Filter 72 | Attributes map[string]bool 73 | } 74 | 75 | type SearchResult struct { 76 | DN string 77 | Attributes map[string][][]byte 78 | } 79 | 80 | func IsPrintable(v []byte) bool { 81 | for i := 0; i < len(v); { 82 | r, s := utf8.DecodeRune(v[i:]) 83 | if r == utf8.RuneError || r < 32 { 84 | return false 85 | } 86 | i += s 87 | } 88 | return true 89 | } 90 | 91 | func (r *SearchResult) ToLDIF(w io.Writer) error { 92 | if _, err := fmt.Fprintf(w, "dn: %s\n", r.DN); err != nil { 93 | return err 94 | } 95 | for name, values := range r.Attributes { 96 | for _, v := range values { 97 | if IsPrintable(v) { 98 | if _, err := fmt.Fprintf(w, "%s: %s\n", name, string(v)); err != nil { 99 | return err 100 | } 101 | } else { 102 | if _, err := fmt.Fprintf(w, "%s:: %s\n", name, base64.StdEncoding.EncodeToString(v)); err != nil { 103 | return err 104 | } 105 | } 106 | } 107 | } 108 | return nil 109 | } 110 | 111 | type SearchResponse struct { 112 | BaseResponse 113 | Results []*SearchResult 114 | } 115 | 116 | func (r *SearchResponse) WritePackets(w io.Writer, msgID int) error { 117 | top := NewResponsePacket(msgID) 118 | for _, res := range r.Results { 119 | top.Items = top.Items[:1] 120 | pkt := top.AddItem(NewPacket(ClassApplication, false, ApplicationSearchResultEntry, nil)) 121 | pkt.AddItem(NewPacket(ClassUniversal, true, TagOctetString, res.DN)) 122 | attrPkt := pkt.AddItem(NewPacket(ClassUniversal, false, TagSequence, nil)) 123 | for name, vals := range res.Attributes { 124 | p := attrPkt.AddItem(NewPacket(ClassUniversal, false, TagSequence, nil)) 125 | p.AddItem(NewPacket(ClassUniversal, true, TagOctetString, name)) 126 | valsPkt := p.AddItem(NewPacket(ClassUniversal, false, TagSet, nil)) 127 | for _, v := range vals { 128 | valsPkt.AddItem(NewPacket(ClassUniversal, true, TagOctetString, v)) 129 | } 130 | } 131 | if err := top.Write(w); err != nil { 132 | return err 133 | } 134 | } 135 | top.Items = top.Items[:1] 136 | pkt := top.AddItem(r.BaseResponse.NewPacket()) 137 | pkt.Tag = ApplicationSearchResultDone 138 | if len(r.Results) == 0 && r.BaseResponse.Code == ResultSuccess { 139 | r.BaseResponse.Code = ResultNoSuchObject 140 | } 141 | return top.Write(w) 142 | } 143 | 144 | func (r *SearchRequest) WritePackets(w io.Writer, msgID int) error { 145 | pkt := NewPacket(ClassApplication, false, ApplicationSearchRequest, nil) 146 | pkt.AddItem(NewPacket(ClassUniversal, true, TagOctetString, r.BaseDN)) 147 | pkt.AddItem(NewPacket(ClassUniversal, true, TagEnumerated, int(r.Scope))) 148 | pkt.AddItem(NewPacket(ClassUniversal, true, TagEnumerated, int(r.DerefAliases))) 149 | pkt.AddItem(NewPacket(ClassUniversal, true, TagInteger, r.SizeLimit)) 150 | pkt.AddItem(NewPacket(ClassUniversal, true, TagInteger, r.TimeLimit)) 151 | pkt.AddItem(NewPacket(ClassUniversal, true, TagBoolean, r.TypesOnly)) 152 | if r.Filter == nil { 153 | r.Filter = &Present{Attribute: "objectClass"} 154 | } 155 | p, err := r.Filter.Encode() 156 | if err != nil { 157 | return err 158 | } 159 | pkt.AddItem(p) 160 | p = pkt.AddItem(NewPacket(ClassUniversal, false, TagSequence, nil)) 161 | for a := range r.Attributes { 162 | p.AddItem(NewPacket(ClassUniversal, true, TagOctetString, a)) 163 | } 164 | 165 | req := NewRequestPacket(msgID) 166 | req.AddItem(pkt) 167 | return req.Write(w) 168 | } 169 | 170 | func parseSearchRequest(pkt *Packet) (*SearchRequest, error) { 171 | if len(pkt.Items) != 8 { 172 | return nil, &ProtocolError{Reason: "search request should have 8 items"} 173 | } 174 | var ok bool 175 | req := &SearchRequest{} 176 | if req.BaseDN, ok = pkt.Items[0].Str(); !ok { 177 | return nil, &ProtocolError{Reason: "can't parse baseObject for search request"} 178 | } 179 | scope, ok := pkt.Items[1].Int() 180 | if !ok { 181 | return nil, &ProtocolError{Reason: "can't parse scope for search request"} 182 | } 183 | req.Scope = Scope(scope) 184 | deref, ok := pkt.Items[2].Int() 185 | if !ok { 186 | return nil, &ProtocolError{Reason: "can't parse derefAliases for search request"} 187 | } 188 | req.DerefAliases = DerefAliases(deref) 189 | if req.SizeLimit, ok = pkt.Items[3].Int(); !ok { 190 | return nil, &ProtocolError{Reason: "can't parse sizeLimit for search request"} 191 | } 192 | if req.TimeLimit, ok = pkt.Items[4].Int(); !ok { 193 | return nil, &ProtocolError{Reason: "can't parse sizeLimit for search request"} 194 | } 195 | if req.TypesOnly, ok = pkt.Items[5].Bool(); !ok { 196 | return nil, &ProtocolError{Reason: "can't parse typesOnly for search request"} 197 | } 198 | var err error 199 | req.Filter, err = parseSearchFilter(pkt.Items[6]) 200 | if err != nil { 201 | return nil, err 202 | } 203 | req.Attributes = make(map[string]bool) 204 | for _, it := range pkt.Items[7].Items { 205 | s, ok := it.Str() 206 | if !ok { 207 | return nil, &ProtocolError{Reason: "can't parse attribute from list for search request"} 208 | } 209 | req.Attributes[s] = true // TODO: should we lower case these? [strings.ToLower(s)] = true 210 | } 211 | return req, nil 212 | } 213 | 214 | func parseSearchResultResponse(pkt *Packet) (*SearchResult, error) { 215 | if len(pkt.Items) != 2 { 216 | return nil, &ProtocolError{Reason: "search result response should have 2 items"} 217 | } 218 | var ok bool 219 | res := &SearchResult{} 220 | res.DN, ok = pkt.Items[0].Str() 221 | if !ok { 222 | return nil, &ProtocolError{Reason: "failed to parse dn for search result response"} 223 | } 224 | res.Attributes = make(map[string][][]byte) 225 | for _, p := range pkt.Items[1].Items { 226 | if len(p.Items) != 2 { 227 | return nil, &ProtocolError{Reason: "search result response attribute should have 2 items"} 228 | } 229 | name, ok := p.Items[0].Str() 230 | if !ok { 231 | return nil, &ProtocolError{Reason: "failed to parse attribute name in search result response"} 232 | } 233 | for _, p2 := range p.Items[1].Items { 234 | value, ok := p2.Bytes() 235 | if !ok { 236 | return nil, &ProtocolError{Reason: "failed to parse attribute value in search result response"} 237 | } 238 | res.Attributes[name] = append(res.Attributes[name], value) 239 | } 240 | } 241 | return res, nil 242 | } 243 | -------------------------------------------------------------------------------- /ldap/server.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "crypto/tls" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "log" 11 | "net" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | func NewResponsePacket(msgID int) *Packet { 17 | pkt := NewPacket(ClassUniversal, false, TagSequence, nil) 18 | pkt.AddItem(NewPacket(ClassUniversal, true, TagInteger, msgID)) 19 | return pkt 20 | } 21 | 22 | type Response interface { 23 | WritePackets(w io.Writer, msgID int) error 24 | } 25 | 26 | type BaseResponse struct { 27 | MessageType int 28 | Code ResultCode 29 | MatchedDN string 30 | Message string 31 | // TODO Referral 32 | } 33 | 34 | func (r *BaseResponse) Error() string { 35 | return fmt.Sprintf("ldap: %s: %s", r.Code.String(), r.Message) 36 | } 37 | 38 | func (r *BaseResponse) Err() error { 39 | if r.Code == 0 { 40 | return nil 41 | } 42 | return r 43 | } 44 | 45 | func (r *BaseResponse) WritePackets(w io.Writer, msgID int) error { 46 | pkt := NewResponsePacket(msgID) 47 | pkt.AddItem(r.NewPacket()) 48 | return pkt.Write(w) 49 | } 50 | 51 | func (r *BaseResponse) NewPacket() *Packet { 52 | pkt := NewPacket(ClassApplication, false, r.MessageType, nil) 53 | pkt.AddItem(NewPacket(ClassUniversal, true, TagEnumerated, int(r.Code))) 54 | pkt.AddItem(NewPacket(ClassUniversal, true, TagOctetString, r.MatchedDN)) 55 | pkt.AddItem(NewPacket(ClassUniversal, true, TagOctetString, r.Message)) 56 | return pkt 57 | } 58 | 59 | func parseBaseResponse(pkt *Packet, res *BaseResponse) error { 60 | if len(pkt.Items) < 3 { 61 | return &ProtocolError{Reason: "base response should have at least 3 values"} 62 | } 63 | code, ok := pkt.Items[0].Int() 64 | if !ok { 65 | return &ProtocolError{Reason: "invalid code in response"} 66 | } 67 | res.Code = ResultCode(code) 68 | res.MatchedDN, ok = pkt.Items[1].Str() 69 | if !ok { 70 | return &ProtocolError{Reason: "invalid matchedDN in response"} 71 | } 72 | res.Message, ok = pkt.Items[2].Str() 73 | if !ok { 74 | return &ProtocolError{Reason: "invalid message in response"} 75 | } 76 | return nil 77 | } 78 | 79 | type Server struct { 80 | Backend Backend 81 | RootDSE map[string][]string 82 | 83 | tlsConfig *tls.Config 84 | // processingTimeout is how long to allow for the execution of a request. 85 | processingTimeout time.Duration 86 | // responseTimeout is how long to allow for the response to be written to the client. 87 | responseTimeout time.Duration 88 | } 89 | 90 | type srvClient struct { 91 | cn net.Conn 92 | wr *bufio.Writer 93 | srv *Server 94 | state State 95 | remoteAddr net.Addr 96 | } 97 | 98 | func NewServer(be Backend, tlsConfig *tls.Config) (*Server, error) { 99 | // Copy the default RootDSE 100 | sf := make(map[string][]string, len(RootDSE)) 101 | for name, vals := range RootDSE { 102 | sf[name] = append([]string(nil), vals...) 103 | } 104 | if tlsConfig != nil { 105 | sf["supportedExtension"] = append(sf["supportedExtension"], OIDStartTLS) 106 | } 107 | return &Server{ 108 | Backend: be, 109 | RootDSE: sf, 110 | tlsConfig: tlsConfig, 111 | processingTimeout: time.Second * 10, 112 | responseTimeout: time.Second * 5, 113 | }, nil 114 | } 115 | 116 | func (srv *Server) ServeTLS(network, addr string, tlsConfig *tls.Config) error { 117 | if tlsConfig == nil { 118 | tlsConfig = srv.tlsConfig 119 | } 120 | if tlsConfig == nil { 121 | return errors.New("ldap: no TLS config") 122 | } 123 | ln, err := tls.Listen(network, addr, tlsConfig) 124 | if err != nil { 125 | return err 126 | } 127 | return srv.serve(ln) 128 | } 129 | 130 | func (srv *Server) Serve(network, addr string) error { 131 | ln, err := net.Listen(network, addr) 132 | if err != nil { 133 | return err 134 | } 135 | return srv.serve(ln) 136 | } 137 | 138 | func (srv *Server) serve(ln net.Listener) error { 139 | for { 140 | cn, err := ln.Accept() 141 | if err != nil { 142 | log.Printf("Accept failed: %+v", err) 143 | continue 144 | } 145 | 146 | go (&srvClient{ 147 | cn: cn, 148 | wr: bufio.NewWriter(cn), 149 | srv: srv, 150 | remoteAddr: cn.RemoteAddr(), 151 | }).serve() 152 | } 153 | } 154 | 155 | func (cli *srvClient) serve() { 156 | state, err := cli.srv.Backend.Connect(cli.remoteAddr) 157 | if err != nil { 158 | if err := cli.cn.Close(); err != nil { 159 | log.Printf("[%s] Failed to close client connection: %s", cli.remoteAddr, err) 160 | } 161 | return 162 | } 163 | cli.state = state 164 | 165 | ctx := context.Background() 166 | ctx, cancel := context.WithCancel(ctx) 167 | defer cancel() 168 | 169 | defer func() { 170 | if err := cli.cn.Close(); err != nil { 171 | log.Printf("[%s] Failed to close client connection: %s", cli.remoteAddr, err) 172 | } 173 | if cli.state != nil { 174 | cli.srv.Backend.Disconnect(state) 175 | } 176 | }() 177 | 178 | for { 179 | pkt, _, err := ReadPacket(cli.cn) 180 | if err != nil { 181 | if !errors.Is(err, io.EOF) { 182 | log.Printf("[%s] ReadPacket failed: %s", cli.remoteAddr, err) 183 | } 184 | return 185 | } 186 | if pkt.Class != ClassUniversal || pkt.Primitive || pkt.Tag != TagSequence || len(pkt.Items) < 2 { 187 | log.Printf("[%s] Unknown classtype, tagtype, tag, or too few items", cli.remoteAddr) 188 | return 189 | } 190 | 191 | // pkt.Format(os.Stdout) 192 | 193 | msgID, ok := pkt.Items[0].Int() 194 | if !ok { 195 | log.Printf("Failed to read MessageID") 196 | return 197 | } 198 | 199 | // TODO: parse rest of packet: control 200 | // https://ldapwiki.com/wiki/SupportedControl 201 | // 1.2.840.113556.1.4.319 202 | // https://ldapwiki.com/wiki/Simple%20Paged%20Results%20Control 203 | // https://oidref.com/1.2.840.113556.1.4.319 204 | 205 | if err := cli.processRequest(ctx, msgID, pkt.Items[1]); err != nil { 206 | end := true 207 | if !errors.Is(err, io.EOF) { 208 | log.Printf("[%s] Processing of request failed: %s", cli.remoteAddr, err) 209 | res := &BaseResponse{ 210 | MessageType: pkt.Items[1].Tag + 1, 211 | Code: ResultOther, 212 | Message: "ERROR", 213 | } 214 | if e, ok := errorAsType[*ProtocolError](err); ok { 215 | res.Code = ResultProtocolError 216 | res.Message = e.Reason 217 | end = false 218 | } else if e, ok := errorAsType[*UnsupportedRequestTagError](err); ok { 219 | res.Code = ResultUnwillingToPerform 220 | res.Message = fmt.Sprintf("unsupported request tag %d", e.Tag) 221 | end = false 222 | } 223 | if err := cli.cn.SetWriteDeadline(time.Now().Add(cli.srv.responseTimeout)); err != nil { 224 | log.Printf("[%s] Failed to set write deadline: %s", cli.remoteAddr, err) 225 | end = true 226 | } else if err := res.WritePackets(cli.wr, msgID); err != nil { 227 | log.Printf("[%s] Failed to write error response: %s", cli.remoteAddr, err) 228 | end = true 229 | } else if err := cli.wr.Flush(); err != nil { 230 | log.Printf("[%s] Failed to flush: %s", cli.remoteAddr, err) 231 | end = true 232 | } else if err := cli.cn.SetWriteDeadline(time.Time{}); err != nil { 233 | log.Printf("[%s] Failed to clear write deadline: %s", cli.remoteAddr, err) 234 | end = true 235 | } 236 | } 237 | if end { 238 | return 239 | } 240 | } 241 | } 242 | } 243 | 244 | // return an error when the client connection should be closed 245 | func (cli *srvClient) processRequest(ctx context.Context, msgID int, pkt *Packet) error { 246 | ctx, cancel := context.WithTimeout(ctx, cli.srv.processingTimeout) 247 | defer cancel() 248 | 249 | // TODO: use context for deadlines and cancellations 250 | var res Response 251 | switch pkt.Tag { 252 | default: 253 | // _ = pkt.Format(os.Stdout) 254 | return &UnsupportedRequestTagError{Tag: pkt.Tag} 255 | case ApplicationUnbindRequest: 256 | return io.EOF 257 | case ApplicationBindRequest: 258 | // TODO: SASL 259 | req, err := parseBindRequest(pkt) 260 | if err != nil { 261 | return err 262 | } 263 | res, err = cli.srv.Backend.Bind(ctx, cli.state, req) 264 | if err != nil { 265 | return err 266 | } 267 | case ApplicationSearchRequest: 268 | req, err := parseSearchRequest(pkt) 269 | if err != nil { 270 | return err 271 | } 272 | if req.BaseDN == "" && req.Scope == ScopeBaseObject { // TODO check filter 273 | res, err = cli.rootDSE(req) 274 | } else { 275 | res, err = cli.srv.Backend.Search(ctx, cli.state, req) 276 | } 277 | if err != nil { 278 | return err 279 | } 280 | case ApplicationAddRequest: 281 | req, err := parseAddRequest(pkt) 282 | if err != nil { 283 | return err 284 | } 285 | res, err = cli.srv.Backend.Add(ctx, cli.state, req) 286 | if err != nil { 287 | return err 288 | } 289 | case ApplicationDelRequest: 290 | req, err := parseDeleteRequest(pkt) 291 | if err != nil { 292 | return err 293 | } 294 | res, err = cli.srv.Backend.Delete(ctx, cli.state, req) 295 | if err != nil { 296 | return err 297 | } 298 | case ApplicationModifyRequest: 299 | req, err := parseModifyRequest(pkt) 300 | if err != nil { 301 | return err 302 | } 303 | res, err = cli.srv.Backend.Modify(ctx, cli.state, req) 304 | if err != nil { 305 | return err 306 | } 307 | case ApplicationModifyDNRequest: 308 | req, err := parseModifyDNRequest(pkt) 309 | if err != nil { 310 | return err 311 | } 312 | res, err = cli.srv.Backend.ModifyDN(ctx, cli.state, req) 313 | if err != nil { 314 | return err 315 | } 316 | case ApplicationExtendedRequest: 317 | req, err := parseExtendedRequest(pkt) 318 | if err != nil { 319 | return err 320 | } 321 | 322 | switch req.Name { 323 | default: 324 | res, err = cli.srv.Backend.ExtendedRequest(ctx, cli.state, req) 325 | if err != nil { 326 | return err 327 | } 328 | case OIDStartTLS: 329 | if cli.srv.tlsConfig == nil { 330 | res = &ExtendedResponse{ 331 | BaseResponse: BaseResponse{ 332 | Code: ResultUnavailable, 333 | Message: "TLS not configured", 334 | }, 335 | Name: OIDStartTLS, 336 | } 337 | } else { 338 | res = &ExtendedResponse{ 339 | Name: OIDStartTLS, 340 | } 341 | if err := res.WritePackets(cli.wr, msgID); err != nil { 342 | return err 343 | } 344 | if err := cli.wr.Flush(); err != nil { 345 | return err 346 | } 347 | cli.cn = tls.Server(cli.cn, cli.srv.tlsConfig) 348 | cli.wr.Reset(cli.cn) 349 | return nil 350 | } 351 | case OIDPasswordModify: 352 | var r *PasswordModifyRequest 353 | if len(req.Value) != 0 { 354 | p, _, err := ParsePacket(req.Value) 355 | if err != nil { 356 | return err 357 | } 358 | r, err = parsePasswordModifyRequest(p) 359 | if err != nil { 360 | return err 361 | } 362 | } else { 363 | r = &PasswordModifyRequest{} 364 | } 365 | gen, err := cli.srv.Backend.PasswordModify(ctx, cli.state, r) 366 | if err != nil { 367 | return err 368 | } 369 | p := NewPacket(ClassUniversal, false, TagSequence, nil) 370 | if gen != nil { 371 | p.AddItem(NewPacket(ClassContext, true, 0, gen)) 372 | } 373 | b, err := p.Encode() 374 | if err != nil { 375 | return err 376 | } 377 | res = &ExtendedResponse{ 378 | Value: b, 379 | } 380 | case OIDWhoAmI: 381 | v, err := cli.srv.Backend.Whoami(ctx, cli.state) 382 | if err != nil { 383 | return err 384 | } 385 | res = &ExtendedResponse{ 386 | Value: []byte(v), 387 | } 388 | } 389 | } 390 | if err := cli.cn.SetWriteDeadline(time.Now().Add(cli.srv.responseTimeout)); err != nil { 391 | return fmt.Errorf("failed to set deadline for write: %w", err) 392 | } 393 | defer func() { 394 | if err := cli.cn.SetWriteDeadline(time.Time{}); err != nil { 395 | log.Printf("failed to clear deadline for write: %s", err) 396 | } 397 | }() 398 | if res != nil { 399 | if err := res.WritePackets(cli.wr, msgID); err != nil { 400 | return err 401 | } 402 | } 403 | return cli.wr.Flush() 404 | } 405 | 406 | func (cli *srvClient) rootDSE(req *SearchRequest) (*SearchResponse, error) { 407 | r := &SearchResult{DN: "", Attributes: make(map[string][][]byte)} 408 | res := &SearchResponse{Results: []*SearchResult{r}} 409 | if len(req.Attributes) == 0 { 410 | r.Attributes["objectClass"] = [][]byte{[]byte("top")} 411 | return res, nil 412 | } 413 | for name, vals := range cli.srv.RootDSE { 414 | if req.Attributes["+"] || req.Attributes[strings.ToLower(name)] { 415 | r.Attributes[name] = make([][]byte, len(vals)) 416 | for i, v := range vals { 417 | r.Attributes[name][i] = []byte(v) 418 | } 419 | } 420 | } 421 | return res, nil 422 | } 423 | --------------------------------------------------------------------------------