├── .gitignore ├── go.mod ├── tests ├── add2.ldif ├── add.ldif ├── modify2.ldif ├── modify.ldif ├── cert_DONOTUSE.pem └── key_DONOTUSE.pem ├── go.sum ├── debug.go ├── examples ├── searchTLS.go ├── searchSSL.go ├── search.go ├── enterprise.ldif ├── slapd.conf ├── server.go ├── modify.go └── proxy.go ├── LICENSE ├── server_bind.go ├── bind.go ├── ldap_test.go ├── filter_test.go ├── README.md ├── control.go ├── modify.go ├── server_modify_test.go ├── server_modify.go ├── server_search.go ├── conn.go ├── search.go ├── ldap.go ├── filter.go ├── server_test.go ├── server.go └── server_search_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | examples/modify 2 | examples/search 3 | examples/searchSSL 4 | examples/searchTLS 5 | .idea 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nmcclain/ldap 2 | 3 | go 1.14 4 | 5 | require github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 6 | -------------------------------------------------------------------------------- /tests/add2.ldif: -------------------------------------------------------------------------------- 1 | dn: cn=Big Bob,dc=example,dc=com 2 | objectClass: person 3 | cn: Big Bob 4 | sn: Bob 5 | mail: bob@example.com 6 | uid: bob 7 | -------------------------------------------------------------------------------- /tests/add.ldif: -------------------------------------------------------------------------------- 1 | dn: cn=Barbara Jensen,dc=example,dc=com 2 | objectClass: person 3 | cn: Barbara Jensen 4 | sn: Jensen 5 | mail: bjensen@example.com 6 | uid: bjensen 7 | -------------------------------------------------------------------------------- /tests/modify2.ldif: -------------------------------------------------------------------------------- 1 | dn: cn=testo,dc=example,dc=com 2 | changetype: modify 3 | replace: mail 4 | mail: modid@example.com 5 | - 6 | delete: manager 7 | - 8 | add: title 9 | title: Other Poobah 10 | - 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 h1:D9EvfGQvlkKaDr2CRKN++7HbSXbefUNDrPq60T+g24s= 2 | github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484/go.mod h1:O1EljZ+oHprtxDDPHiMWVo/5dBT6PlvWX5PSwj80aBA= 3 | -------------------------------------------------------------------------------- /tests/modify.ldif: -------------------------------------------------------------------------------- 1 | dn: cn=testy,dc=example,dc=com 2 | changetype: modify 3 | replace: mail 4 | mail: modme@example.com 5 | - 6 | delete: manager 7 | - 8 | add: title 9 | title: Grand Poobah 10 | - 11 | delete: description 12 | - 13 | delete: details 14 | - 15 | replace: fullname 16 | fullname: Test Testerson 17 | -------------------------------------------------------------------------------- /debug.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/nmcclain/asn1-ber" 7 | ) 8 | 9 | // debbuging type 10 | // - has a Printf method to write the debug output 11 | type debugging bool 12 | 13 | // write debug output 14 | func (debug debugging) Printf(format string, args ...interface{}) { 15 | if debug { 16 | log.Printf(format, args...) 17 | } 18 | } 19 | 20 | func (debug debugging) PrintPacket(packet *ber.Packet) { 21 | if debug { 22 | ber.PrintPacket(packet) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/cert_DONOTUSE.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC9jCCAeCgAwIBAgIRAOG6xrSjAWQvJl9xFTIte/owCwYJKoZIhvcNAQELMBIx 3 | EDAOBgNVBAoTB0FjbWUgQ28wHhcNMTQwODIwMTY1MjQ4WhcNMTUwODIwMTY1MjQ4 4 | WjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 5 | CgKCAQEA30gcjawL7RXQ5B5IfAcCPsJkG3GBbfhkbBRI22VxktBoNvqh2TWyECG3 6 | WsB/N1WMmATnLxamBZ5mfouNbd120gbO1M06Ti57NP1YTmMp8AU18Dm4OjZ6IeQf 7 | ip1xYSSSb6UyucFN6zIt+5PY2o4DoGb6fSNKb1ybgu91LmC1O/TDlyYUWn2TtF73 8 | FOUwSt+A6t3/Jhjhlp4n5Oobw1rrAgf7DPhWFg0Thj1yknPzWALY2LPREOMWob0D 9 | EgR5C3WS2eYPyHkeMZWoSY6BiWTIU+hFqQUkdOvrWhflFoiZIsOl6iXmQpo2EQlg 10 | j3Oy2zyZk1ndAfHlFoAgPIIbnBc+2QIDAQABo0swSTAOBgNVHQ8BAf8EBAMCAKAw 11 | EwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAUBgNVHREEDTALggls 12 | b2NhbGhvc3QwCwYJKoZIhvcNAQELA4IBAQBB9xNt3rDrBA9tCLCjdlnIQuUu9Uf0 13 | tHsSH6keBkhEoAylzHjmkNlerhTaLkRgB0D8qjE5+1APz42TuRpHRunYHSTNN0aF 14 | N6zKlpXS0g+J/ViCh/Zw7xQI4mpSFqYzTgn4T733FqwLrmtKsj0IOOkDYSZc7qfh 15 | qwXp/SB1J0Kp8G8S3G73dCZZYuW8y/eYMEoSkjNwNLAXzEAmFkGd8f1xhWTvnOxz 16 | ZBbOOjggdRLxr7cMZ8GaVWFgEG93y3AYMhFxZYRwWTcWJvSTNP3xC/CWqxXkiKdO 17 | 2BROqmTw8zdqjXCIbgX4B5G5njMq9fk0gc4SiTAQkCOF6Xo0wQUvBAbN 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /examples/searchTLS.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | // Copyright 2014 The Go Authors. All rights reserved. 4 | // Use of this source code is governed by a BSD-style 5 | // license that can be found in the LICENSE file. 6 | 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "log" 12 | 13 | "github.com/nmcclain/ldap" 14 | ) 15 | 16 | var ( 17 | LdapServer string = "localhost" 18 | LdapPort uint16 = 389 19 | BaseDN string = "dc=enterprise,dc=org" 20 | Filter string = "(cn=kirkj)" 21 | Attributes []string = []string{"mail"} 22 | ) 23 | 24 | func main() { 25 | l, err := ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", LdapServer, LdapPort), nil) 26 | if err != nil { 27 | log.Fatalf("ERROR: %s\n", err.Error()) 28 | } 29 | defer l.Close() 30 | // l.Debug = true 31 | 32 | search := ldap.NewSearchRequest( 33 | BaseDN, 34 | ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, 35 | Filter, 36 | Attributes, 37 | nil) 38 | 39 | sr, err := l.Search(search) 40 | if err != nil { 41 | log.Fatalf("ERROR: %s\n", err.Error()) 42 | return 43 | } 44 | 45 | log.Printf("Search: %s -> num of entries = %d\n", search.Filter, len(sr.Entries)) 46 | sr.PrettyPrint(0) 47 | } 48 | -------------------------------------------------------------------------------- /examples/searchSSL.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | // Copyright 2014 The Go Authors. All rights reserved. 4 | // Use of this source code is governed by a BSD-style 5 | // license that can be found in the LICENSE file. 6 | 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "log" 12 | 13 | "github.com/nmcclain/ldap" 14 | ) 15 | 16 | var ( 17 | LdapServer string = "localhost" 18 | LdapPort uint16 = 636 19 | BaseDN string = "dc=enterprise,dc=org" 20 | Filter string = "(cn=kirkj)" 21 | Attributes []string = []string{"mail"} 22 | ) 23 | 24 | func main() { 25 | l, err := ldap.DialSSL("tcp", fmt.Sprintf("%s:%d", LdapServer, LdapPort), nil) 26 | if err != nil { 27 | log.Fatalf("ERROR: %s\n", err.String()) 28 | } 29 | defer l.Close() 30 | // l.Debug = true 31 | 32 | search := ldap.NewSearchRequest( 33 | BaseDN, 34 | ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, 35 | Filter, 36 | Attributes, 37 | nil) 38 | 39 | sr, err := l.Search(search) 40 | if err != nil { 41 | log.Fatalf("ERROR: %s\n", err.String()) 42 | return 43 | } 44 | 45 | log.Printf("Search: %s -> num of entries = %d\n", search.Filter, len(sr.Entries)) 46 | sr.PrettyPrint(0) 47 | } 48 | -------------------------------------------------------------------------------- /examples/search.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | // Copyright 2014 The Go Authors. All rights reserved. 4 | // Use of this source code is governed by a BSD-style 5 | // license that can be found in the LICENSE file. 6 | 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "log" 12 | 13 | "github.com/nmcclain/ldap" 14 | ) 15 | 16 | var ( 17 | ldapServer string = "adserver" 18 | ldapPort uint16 = 3268 19 | baseDN string = "dc=*,dc=*" 20 | filter string = "(&(objectClass=user)(sAMAccountName=*)(memberOf=CN=*,OU=*,DC=*,DC=*))" 21 | Attributes []string = []string{"memberof"} 22 | user string = "*" 23 | passwd string = "*" 24 | ) 25 | 26 | func main() { 27 | l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) 28 | if err != nil { 29 | log.Fatalf("ERROR: %s\n", err.Error()) 30 | } 31 | defer l.Close() 32 | // l.Debug = true 33 | 34 | err = l.Bind(user, passwd) 35 | if err != nil { 36 | log.Printf("ERROR: Cannot bind: %s\n", err.Error()) 37 | return 38 | } 39 | search := ldap.NewSearchRequest( 40 | baseDN, 41 | ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, 42 | filter, 43 | Attributes, 44 | nil) 45 | 46 | sr, err := l.Search(search) 47 | if err != nil { 48 | log.Fatalf("ERROR: %s\n", err.Error()) 49 | return 50 | } 51 | 52 | log.Printf("Search: %s -> num of entries = %d\n", search.Filter, len(sr.Entries)) 53 | sr.PrettyPrint(0) 54 | } 55 | -------------------------------------------------------------------------------- /examples/enterprise.ldif: -------------------------------------------------------------------------------- 1 | dn: dc=enterprise,dc=org 2 | objectClass: dcObject 3 | objectClass: organization 4 | o: acme 5 | 6 | dn: cn=admin,dc=enterprise,dc=org 7 | objectClass: person 8 | cn: admin 9 | sn: admin 10 | description: "LDAP Admin" 11 | 12 | dn: ou=crew,dc=enterprise,dc=org 13 | ou: crew 14 | objectClass: organizationalUnit 15 | 16 | 17 | dn: cn=kirkj,ou=crew,dc=enterprise,dc=org 18 | cn: kirkj 19 | sn: Kirk 20 | gn: James Tiberius 21 | mail: james.kirk@enterprise.org 22 | objectClass: inetOrgPerson 23 | 24 | dn: cn=spock,ou=crew,dc=enterprise,dc=org 25 | cn: spock 26 | sn: Spock 27 | mail: spock@enterprise.org 28 | objectClass: inetOrgPerson 29 | 30 | dn: cn=mccoyl,ou=crew,dc=enterprise,dc=org 31 | cn: mccoyl 32 | sn: McCoy 33 | gn: Leonard 34 | mail: leonard.mccoy@enterprise.org 35 | objectClass: inetOrgPerson 36 | 37 | dn: cn=scottm,ou=crew,dc=enterprise,dc=org 38 | cn: scottm 39 | sn: Scott 40 | gn: Montgomery 41 | mail: Montgomery.scott@enterprise.org 42 | objectClass: inetOrgPerson 43 | 44 | dn: cn=uhuran,ou=crew,dc=enterprise,dc=org 45 | cn: uhuran 46 | sn: Uhura 47 | gn: Nyota 48 | mail: nyota.uhura@enterprise.org 49 | objectClass: inetOrgPerson 50 | 51 | dn: cn=suluh,ou=crew,dc=enterprise,dc=org 52 | cn: suluh 53 | sn: Sulu 54 | gn: Hikaru 55 | mail: hikaru.sulu@enterprise.org 56 | objectClass: inetOrgPerson 57 | 58 | dn: cn=chekovp,ou=crew,dc=enterprise,dc=org 59 | cn: chekovp 60 | sn: Chekov 61 | gn: pavel 62 | mail: pavel.chekov@enterprise.org 63 | objectClass: inetOrgPerson 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | 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 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /tests/key_DONOTUSE.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEoQIBAAKCAQEA30gcjawL7RXQ5B5IfAcCPsJkG3GBbfhkbBRI22VxktBoNvqh 3 | 2TWyECG3WsB/N1WMmATnLxamBZ5mfouNbd120gbO1M06Ti57NP1YTmMp8AU18Dm4 4 | OjZ6IeQfip1xYSSSb6UyucFN6zIt+5PY2o4DoGb6fSNKb1ybgu91LmC1O/TDlyYU 5 | Wn2TtF73FOUwSt+A6t3/Jhjhlp4n5Oobw1rrAgf7DPhWFg0Thj1yknPzWALY2LPR 6 | EOMWob0DEgR5C3WS2eYPyHkeMZWoSY6BiWTIU+hFqQUkdOvrWhflFoiZIsOl6iXm 7 | Qpo2EQlgj3Oy2zyZk1ndAfHlFoAgPIIbnBc+2QIDAQABAoIBAGXECi+QEMd4QAMY 8 | wlS1JRLRqqrPavxiT/Lqs+I7NC6EClu0k/vZ+1Ra6aTVQ6ZGuZO3+F5/5h99eJ2I 9 | oWdHnxZOwApBl6d2i/U02wCvNbgNx+27gPoXRkcYIEAfTkPGVW/JTXtYXVkrP8YA 10 | NsA2JfT/un86jHyBKufcl/4RWcj/BddduEWZRAV7UH4iUVzr6kTHJHyIkrJrME4w 11 | 0Njrd/+tvjX/AsBfj+4IqZo6yMSVQuGhnJYEWt+eU3+5OlLsWe8oy9PJN2hVPxvG 12 | bWaMx0/I88gSf9DQRnpIL2Kr715c4NUqy+82DP+Tg6h/lQ1oba68redtZoctDrmq 13 | IXox6PkCgYEA+fElqxWqdDTOw+yhD4CeZ6yxipI/iUQu2t7atRmTgi2WAgaT5aAP 14 | 1lWhTmWPW7IzjYzFe/CK7OAe0P7JgmvBI2SMlSg/NWOx+pnTmOVO3buz46+B3VI9 15 | IFhMqAkVfnXukT0YgLsdvRTZb7irYeelVImZ0VVvn0+HzqZ1JUj8kwMCgYEA5LGK 16 | jZ0NmBQT/yxsp1GWlmIJnTizlxEGoc7ftL4SNGcshMKbSK6aYnUvjnnRdFwjQUu4 17 | mAN0LlDn7SalEL7PUnotUMNA278o4Zj88VcWQqjukkThDpOFVcreXuGkqIfxYyn6 18 | jJxLouF6L0zspEhFiEWjNYOVDzZw7Fh0tOCxkfMCf1L8voUPrIjo/74N02xSSEYk 19 | EM7xwCbTfLsvQ27eDxwqBqSlinWzr4564BQnpHHNuVBGbUu5kmcUAydhcYbcQESA 20 | Hi1oL5SKhY2vhZI+kPEOYaw3mebiZ2lV6B3i5kAW6B9RKdGUT0t4oLl3l2/qefqX 21 | tXrL40QCJBV5L2wxz6sCgYEAoDRXUTkSCtUV5Q3j15pqGVL4VTEhbdQ5hyR6xgzY 22 | h+k24JHLYjEeaZaaB/8CYbch413+JE9XFhMLRbBqtb5VUfvQvuDpEIdrRg58MzzE 23 | lVHuPn0OA74IC7+f42vCg2UoDkWcBOCAg8vcYkJLDBKs0veli5lv1EZY+NhGeWdm 24 | PU0CgYAhEHZnVC8DuKAUxuIXEpDil3F7iGYs1rAnGd3GkKofiait+9YlsCxFx/4F 25 | 95VQjHm6Fdc+vwGUa2Z986wKmocWzVP3TbznMdbvr0/8LCOhQKDrtCkFWHtGsP/d 26 | PnCkwIdaTEen0E52PkMK8GNq6wjitINzRp5hpV23WFtGQxmmlA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /examples/slapd.conf: -------------------------------------------------------------------------------- 1 | # 2 | # See slapd.conf(5) for details on configuration options. 3 | # This file should NOT be world readable. 4 | # 5 | include /private/etc/openldap/schema/core.schema 6 | include /private/etc/openldap/schema/cosine.schema 7 | include /private/etc/openldap/schema/inetorgperson.schema 8 | 9 | # Define global ACLs to disable default read access. 10 | 11 | # Do not enable referrals until AFTER you have a working directory 12 | # service AND an understanding of referrals. 13 | #referral ldap://root.openldap.org 14 | 15 | pidfile /private/var/db/openldap/run/slapd.pid 16 | argsfile /private/var/db/openldap/run/slapd.args 17 | 18 | # Load dynamic backend modules: 19 | # modulepath /usr/libexec/openldap 20 | # moduleload back_bdb.la 21 | # moduleload back_hdb.la 22 | # moduleload back_ldap.la 23 | 24 | # Sample security restrictions 25 | # Require integrity protection (prevent hijacking) 26 | # Require 112-bit (3DES or better) encryption for updates 27 | # Require 63-bit encryption for simple bind 28 | # security ssf=1 update_ssf=112 simple_bind=64 29 | 30 | # Sample access control policy: 31 | # Root DSE: allow anyone to read it 32 | # Subschema (sub)entry DSE: allow anyone to read it 33 | # Other DSEs: 34 | # Allow self write access 35 | # Allow authenticated users read access 36 | # Allow anonymous users to authenticate 37 | # Directives needed to implement policy: 38 | # access to dn.base="" by * read 39 | # access to dn.base="cn=Subschema" by * read 40 | # access to * 41 | # by self write 42 | # by users read 43 | # by anonymous auth 44 | # 45 | # if no access controls are present, the default policy 46 | # allows anyone and everyone to read anything but restricts 47 | # updates to rootdn. (e.g., "access to * by * read") 48 | # 49 | # rootdn can always read and write EVERYTHING! 50 | 51 | ####################################################################### 52 | # BDB database definitions 53 | ####################################################################### 54 | 55 | database bdb 56 | suffix "dc=enterprise,dc=org" 57 | rootdn "cn=admin,dc=enterprise,dc=org" 58 | # Cleartext passwords, especially for the rootdn, should 59 | # be avoid. See slappasswd(8) and slapd.conf(5) for details. 60 | # Use of strong authentication encouraged. 61 | rootpw {SSHA}laO00HsgszhK1O0Z5qR0/i/US69Osfeu 62 | # The database directory MUST exist prior to running slapd AND 63 | # should only be accessible by the slapd and slap tools. 64 | # Mode 700 recommended. 65 | directory /private/var/db/openldap/openldap-data 66 | # Indices to maintain 67 | index objectClass eq 68 | -------------------------------------------------------------------------------- /examples/server.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | "net" 8 | 9 | "github.com/nmcclain/ldap" 10 | ) 11 | 12 | ///////////// 13 | // Sample searches you can try against this simple LDAP server: 14 | // 15 | // ldapsearch -H ldap://localhost:3389 -x -b 'dn=test,dn=com' 16 | // ldapsearch -H ldap://localhost:3389 -x -b 'dn=test,dn=com' 'cn=ned' 17 | // ldapsearch -H ldap://localhost:3389 -x -b 'dn=test,dn=com' 'uidnumber=5000' 18 | ///////////// 19 | 20 | ///////////// Run a simple LDAP server 21 | func main() { 22 | s := ldap.NewServer() 23 | 24 | // register Bind and Search function handlers 25 | handler := ldapHandler{} 26 | s.BindFunc("", handler) 27 | s.SearchFunc("", handler) 28 | 29 | // start the server 30 | listen := "localhost:3389" 31 | log.Printf("Starting example LDAP server on %s", listen) 32 | if err := s.ListenAndServe(listen); err != nil { 33 | log.Fatalf("LDAP Server Failed: %s", err.Error()) 34 | } 35 | } 36 | 37 | type ldapHandler struct { 38 | } 39 | 40 | ///////////// Allow anonymous binds only 41 | func (h ldapHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldap.LDAPResultCode, error) { 42 | if bindDN == "" && bindSimplePw == "" { 43 | return ldap.LDAPResultSuccess, nil 44 | } 45 | return ldap.LDAPResultInvalidCredentials, nil 46 | } 47 | 48 | ///////////// Return some hardcoded search results - we'll respond to any baseDN for testing 49 | func (h ldapHandler) Search(boundDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) { 50 | entries := []*ldap.Entry{ 51 | &ldap.Entry{"cn=ned," + searchReq.BaseDN, []*ldap.EntryAttribute{ 52 | &ldap.EntryAttribute{"cn", []string{"ned"}}, 53 | &ldap.EntryAttribute{"uidNumber", []string{"5000"}}, 54 | &ldap.EntryAttribute{"accountStatus", []string{"active"}}, 55 | &ldap.EntryAttribute{"uid", []string{"ned"}}, 56 | &ldap.EntryAttribute{"description", []string{"ned"}}, 57 | &ldap.EntryAttribute{"objectClass", []string{"posixAccount"}}, 58 | }}, 59 | &ldap.Entry{"cn=trent," + searchReq.BaseDN, []*ldap.EntryAttribute{ 60 | &ldap.EntryAttribute{"cn", []string{"trent"}}, 61 | &ldap.EntryAttribute{"uidNumber", []string{"5005"}}, 62 | &ldap.EntryAttribute{"accountStatus", []string{"active"}}, 63 | &ldap.EntryAttribute{"uid", []string{"trent"}}, 64 | &ldap.EntryAttribute{"description", []string{"trent"}}, 65 | &ldap.EntryAttribute{"objectClass", []string{"posixAccount"}}, 66 | }}, 67 | } 68 | return ldap.ServerSearchResult{entries, []string{}, []ldap.Control{}, ldap.LDAPResultSuccess}, nil 69 | } 70 | -------------------------------------------------------------------------------- /examples/modify.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | // Copyright 2014 The Go Authors. All rights reserved. 4 | // Use of this source code is governed by a BSD-style 5 | // license that can be found in the LICENSE file. 6 | 7 | package main 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | "log" 13 | 14 | "github.com/nmcclain/ldap" 15 | ) 16 | 17 | var ( 18 | LdapServer string = "localhost" 19 | LdapPort uint16 = 389 20 | BaseDN string = "dc=enterprise,dc=org" 21 | BindDN string = "cn=admin,dc=enterprise,dc=org" 22 | BindPW string = "enterprise" 23 | Filter string = "(cn=kirkj)" 24 | ) 25 | 26 | func search(l *ldap.Conn, filter string, attributes []string) (*ldap.Entry, error) { 27 | search := ldap.NewSearchRequest( 28 | BaseDN, 29 | ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, 30 | filter, 31 | attributes, 32 | nil) 33 | 34 | sr, err := l.Search(search) 35 | if err != nil { 36 | log.Fatalf("ERROR: %s\n", err) 37 | return nil, err 38 | } 39 | 40 | log.Printf("Search: %s -> num of entries = %d\n", search.Filter, len(sr.Entries)) 41 | if len(sr.Entries) == 0 { 42 | return nil, ldap.NewError(ldap.ErrorDebugging, errors.New(fmt.Sprintf("no entries found for: %s", filter))) 43 | } 44 | return sr.Entries[0], nil 45 | } 46 | 47 | func main() { 48 | l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", LdapServer, LdapPort)) 49 | if err != nil { 50 | log.Fatalf("ERROR: %s\n", err.Error()) 51 | } 52 | defer l.Close() 53 | // l.Debug = true 54 | 55 | l.Bind(BindDN, BindPW) 56 | 57 | log.Printf("The Search for Kirk ... %s\n", Filter) 58 | entry, err := search(l, Filter, []string{}) 59 | if err != nil { 60 | log.Fatal("could not get entry") 61 | } 62 | entry.PrettyPrint(0) 63 | 64 | log.Printf("modify the mail address and add a description ... \n") 65 | modify := ldap.NewModifyRequest(entry.DN) 66 | modify.Add("description", []string{"Captain of the USS Enterprise"}) 67 | modify.Replace("mail", []string{"captain@enterprise.org"}) 68 | if err := l.Modify(modify); err != nil { 69 | log.Fatalf("ERROR: %s\n", err.Error()) 70 | } 71 | 72 | entry, err = search(l, Filter, []string{}) 73 | if err != nil { 74 | log.Fatal("could not get entry") 75 | } 76 | entry.PrettyPrint(0) 77 | 78 | log.Printf("reset the entry ... \n") 79 | modify = ldap.NewModifyRequest(entry.DN) 80 | modify.Delete("description", []string{}) 81 | modify.Replace("mail", []string{"james.kirk@enterprise.org"}) 82 | if err := l.Modify(modify); err != nil { 83 | log.Fatalf("ERROR: %s\n", err.Error()) 84 | } 85 | 86 | entry, err = search(l, Filter, []string{}) 87 | if err != nil { 88 | log.Fatal("could not get entry") 89 | } 90 | entry.PrettyPrint(0) 91 | } 92 | -------------------------------------------------------------------------------- /server_bind.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "github.com/nmcclain/asn1-ber" 5 | "log" 6 | "net" 7 | ) 8 | 9 | func HandleBindRequest(req *ber.Packet, fns map[string]Binder, conn net.Conn) (resultCode LDAPResultCode) { 10 | defer func() { 11 | if r := recover(); r != nil { 12 | resultCode = LDAPResultOperationsError 13 | } 14 | }() 15 | 16 | // we only support ldapv3 17 | ldapVersion, ok := req.Children[0].Value.(uint64) 18 | if !ok { 19 | return LDAPResultProtocolError 20 | } 21 | if ldapVersion != 3 { 22 | log.Printf("Unsupported LDAP version: %d", ldapVersion) 23 | return LDAPResultInappropriateAuthentication 24 | } 25 | 26 | // auth types 27 | bindDN, ok := req.Children[1].Value.(string) 28 | if !ok { 29 | return LDAPResultProtocolError 30 | } 31 | bindAuth := req.Children[2] 32 | switch bindAuth.Tag { 33 | default: 34 | log.Print("Unknown LDAP authentication method") 35 | return LDAPResultInappropriateAuthentication 36 | case LDAPBindAuthSimple: 37 | if len(req.Children) == 3 { 38 | fnNames := []string{} 39 | for k := range fns { 40 | fnNames = append(fnNames, k) 41 | } 42 | fn := routeFunc(bindDN, fnNames) 43 | resultCode, err := fns[fn].Bind(bindDN, bindAuth.Data.String(), conn) 44 | if err != nil { 45 | log.Printf("BindFn Error %s", err.Error()) 46 | return LDAPResultOperationsError 47 | } 48 | return resultCode 49 | } else { 50 | log.Print("Simple bind request has wrong # children. len(req.Children) != 3") 51 | return LDAPResultInappropriateAuthentication 52 | } 53 | case LDAPBindAuthSASL: 54 | log.Print("SASL authentication is not supported") 55 | return LDAPResultInappropriateAuthentication 56 | } 57 | return LDAPResultOperationsError 58 | } 59 | 60 | func encodeBindResponse(messageID uint64, ldapResultCode LDAPResultCode) *ber.Packet { 61 | responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response") 62 | responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "Message ID")) 63 | 64 | bindReponse := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindResponse, nil, "Bind Response") 65 | bindReponse.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(ldapResultCode), "resultCode: ")) 66 | bindReponse.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "matchedDN: ")) 67 | bindReponse.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "errorMessage: ")) 68 | 69 | responsePacket.AppendChild(bindReponse) 70 | 71 | // ber.PrintPacket(responsePacket) 72 | return responsePacket 73 | } 74 | -------------------------------------------------------------------------------- /examples/proxy.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "crypto/sha256" 7 | "fmt" 8 | "log" 9 | "net" 10 | "sync" 11 | 12 | "github.com/nmcclain/ldap" 13 | ) 14 | 15 | type ldapHandler struct { 16 | sessions map[string]session 17 | lock sync.Mutex 18 | ldapServer string 19 | ldapPort int 20 | } 21 | 22 | ///////////// Run a simple LDAP proxy 23 | func main() { 24 | s := ldap.NewServer() 25 | 26 | handler := ldapHandler{ 27 | sessions: make(map[string]session), 28 | ldapServer: "localhost", 29 | ldapPort: 3389, 30 | } 31 | s.BindFunc("", handler) 32 | s.SearchFunc("", handler) 33 | s.CloseFunc("", handler) 34 | 35 | // start the server 36 | if err := s.ListenAndServe("localhost:3388"); err != nil { 37 | log.Fatal("LDAP Server Failed: %s", err.Error()) 38 | } 39 | } 40 | 41 | ///////////// 42 | type session struct { 43 | id string 44 | c net.Conn 45 | ldap *ldap.Conn 46 | } 47 | 48 | func (h ldapHandler) getSession(conn net.Conn) (session, error) { 49 | id := connID(conn) 50 | h.lock.Lock() 51 | s, ok := h.sessions[id] // use server connection if it exists 52 | h.lock.Unlock() 53 | if !ok { // open a new server connection if not 54 | l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", h.ldapServer, h.ldapPort)) 55 | if err != nil { 56 | return session{}, err 57 | } 58 | s = session{id: id, c: conn, ldap: l} 59 | h.lock.Lock() 60 | h.sessions[s.id] = s 61 | h.lock.Unlock() 62 | } 63 | return s, nil 64 | } 65 | 66 | ///////////// 67 | func (h ldapHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldap.LDAPResultCode, error) { 68 | s, err := h.getSession(conn) 69 | if err != nil { 70 | return ldap.LDAPResultOperationsError, err 71 | } 72 | if err := s.ldap.Bind(bindDN, bindSimplePw); err != nil { 73 | return ldap.LDAPResultOperationsError, err 74 | } 75 | return ldap.LDAPResultSuccess, nil 76 | } 77 | 78 | ///////////// 79 | func (h ldapHandler) Search(boundDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) { 80 | s, err := h.getSession(conn) 81 | if err != nil { 82 | return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, nil 83 | } 84 | search := ldap.NewSearchRequest( 85 | searchReq.BaseDN, 86 | ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, 87 | searchReq.Filter, 88 | searchReq.Attributes, 89 | nil) 90 | sr, err := s.ldap.Search(search) 91 | if err != nil { 92 | return ldap.ServerSearchResult{}, err 93 | } 94 | //log.Printf("P: Search OK: %s -> num of entries = %d\n", search.Filter, len(sr.Entries)) 95 | return ldap.ServerSearchResult{sr.Entries, []string{}, []ldap.Control{}, ldap.LDAPResultSuccess}, nil 96 | } 97 | func (h ldapHandler) Close(boundDN string, conn net.Conn) error { 98 | conn.Close() // close connection to the server when then client is closed 99 | h.lock.Lock() 100 | defer h.lock.Unlock() 101 | delete(h.sessions, connID(conn)) 102 | return nil 103 | } 104 | func connID(conn net.Conn) string { 105 | h := sha256.New() 106 | h.Write([]byte(conn.LocalAddr().String() + conn.RemoteAddr().String())) 107 | sha := fmt.Sprintf("% x", h.Sum(nil)) 108 | return string(sha) 109 | } 110 | -------------------------------------------------------------------------------- /bind.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ldap 6 | 7 | import ( 8 | "errors" 9 | 10 | "github.com/nmcclain/asn1-ber" 11 | ) 12 | 13 | func (l *Conn) Bind(username, password string) error { 14 | messageID := l.nextMessageID() 15 | 16 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 17 | packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "MessageID")) 18 | bindRequest := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") 19 | bindRequest.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) 20 | bindRequest.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, username, "User Name")) 21 | bindRequest.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, password, "Password")) 22 | packet.AppendChild(bindRequest) 23 | 24 | if l.Debug { 25 | ber.PrintPacket(packet) 26 | } 27 | 28 | channel, err := l.sendMessage(packet) 29 | if err != nil { 30 | return err 31 | } 32 | if channel == nil { 33 | return NewError(ErrorNetwork, errors.New("ldap: could not send message")) 34 | } 35 | defer l.finishMessage(messageID) 36 | 37 | packet = <-channel 38 | if packet == nil { 39 | return NewError(ErrorNetwork, errors.New("ldap: could not retrieve response")) 40 | } 41 | 42 | if l.Debug { 43 | if err := addLDAPDescriptions(packet); err != nil { 44 | return err 45 | } 46 | ber.PrintPacket(packet) 47 | } 48 | 49 | resultCode, resultDescription := getLDAPResultCode(packet) 50 | if resultCode != 0 { 51 | return NewError(resultCode, errors.New(resultDescription)) 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func (l *Conn) Unbind() error { 58 | defer l.Close() 59 | 60 | messageID := l.nextMessageID() 61 | 62 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 63 | packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "MessageID")) 64 | unbindRequest := ber.Encode(ber.ClassApplication, ber.TypePrimitive, ApplicationUnbindRequest, nil, "Unbind Request") 65 | packet.AppendChild(unbindRequest) 66 | 67 | if l.Debug { 68 | ber.PrintPacket(packet) 69 | } 70 | 71 | channel, err := l.sendMessage(packet) 72 | if err != nil { 73 | return err 74 | } 75 | if channel == nil { 76 | return NewError(ErrorNetwork, errors.New("ldap: could not send message")) 77 | } 78 | defer l.finishMessage(messageID) 79 | 80 | packet = <-channel 81 | if packet == nil { 82 | return NewError(ErrorNetwork, errors.New("ldap: could not retrieve response")) 83 | } 84 | 85 | if l.Debug { 86 | if err := addLDAPDescriptions(packet); err != nil { 87 | return err 88 | } 89 | ber.PrintPacket(packet) 90 | } 91 | 92 | resultCode, resultDescription := getLDAPResultCode(packet) 93 | if resultCode != 0 { 94 | return NewError(resultCode, errors.New(resultDescription)) 95 | } 96 | 97 | return nil 98 | } 99 | 100 | -------------------------------------------------------------------------------- /ldap_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | var ldapServer = "ldap.itd.umich.edu" 9 | var ldapPort = uint16(389) 10 | var baseDN = "dc=umich,dc=edu" 11 | var filter = []string{ 12 | "(cn=cis-fac)", 13 | "(&(objectclass=rfc822mailgroup)(cn=*Computer*))", 14 | "(&(objectclass=rfc822mailgroup)(cn=*Mathematics*))"} 15 | var attributes = []string{ 16 | "cn", 17 | "description"} 18 | 19 | func TestConnect(t *testing.T) { 20 | fmt.Printf("TestConnect: starting...\n") 21 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) 22 | if err != nil { 23 | t.Errorf(err.Error()) 24 | return 25 | } 26 | defer l.Close() 27 | fmt.Printf("TestConnect: finished...\n") 28 | } 29 | 30 | func TestSearch(t *testing.T) { 31 | fmt.Printf("TestSearch: starting...\n") 32 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) 33 | if err != nil { 34 | t.Errorf(err.Error()) 35 | return 36 | } 37 | defer l.Close() 38 | 39 | searchRequest := NewSearchRequest( 40 | baseDN, 41 | ScopeWholeSubtree, DerefAlways, 0, 0, false, 42 | filter[0], 43 | attributes, 44 | nil) 45 | 46 | sr, err := l.Search(searchRequest) 47 | if err != nil { 48 | t.Errorf(err.Error()) 49 | return 50 | } 51 | 52 | fmt.Printf("TestSearch: %s -> num of entries = %d\n", searchRequest.Filter, len(sr.Entries)) 53 | } 54 | 55 | func TestSearchWithPaging(t *testing.T) { 56 | fmt.Printf("TestSearchWithPaging: starting...\n") 57 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) 58 | if err != nil { 59 | t.Errorf(err.Error()) 60 | return 61 | } 62 | defer l.Close() 63 | 64 | err = l.Bind("", "") 65 | if err != nil { 66 | t.Errorf(err.Error()) 67 | return 68 | } 69 | 70 | searchRequest := NewSearchRequest( 71 | baseDN, 72 | ScopeWholeSubtree, DerefAlways, 0, 0, false, 73 | filter[1], 74 | attributes, 75 | nil) 76 | sr, err := l.SearchWithPaging(searchRequest, 5) 77 | if err != nil { 78 | t.Errorf(err.Error()) 79 | return 80 | } 81 | 82 | fmt.Printf("TestSearchWithPaging: %s -> num of entries = %d\n", searchRequest.Filter, len(sr.Entries)) 83 | } 84 | 85 | func testMultiGoroutineSearch(t *testing.T, l *Conn, results chan *SearchResult, i int) { 86 | searchRequest := NewSearchRequest( 87 | baseDN, 88 | ScopeWholeSubtree, DerefAlways, 0, 0, false, 89 | filter[i], 90 | attributes, 91 | nil) 92 | sr, err := l.Search(searchRequest) 93 | if err != nil { 94 | t.Errorf(err.Error()) 95 | results <- nil 96 | return 97 | } 98 | results <- sr 99 | } 100 | 101 | func TestMultiGoroutineSearch(t *testing.T) { 102 | fmt.Printf("TestMultiGoroutineSearch: starting...\n") 103 | l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) 104 | if err != nil { 105 | t.Errorf(err.Error()) 106 | return 107 | } 108 | defer l.Close() 109 | 110 | results := make([]chan *SearchResult, len(filter)) 111 | for i := range filter { 112 | results[i] = make(chan *SearchResult) 113 | go testMultiGoroutineSearch(t, l, results[i], i) 114 | } 115 | for i := range filter { 116 | sr := <-results[i] 117 | if sr == nil { 118 | t.Errorf("Did not receive results from goroutine for %q", filter[i]) 119 | } else { 120 | fmt.Printf("TestMultiGoroutineSearch(%d): %s -> num of entries = %d\n", i, filter[i], len(sr.Entries)) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /filter_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | ber "github.com/nmcclain/asn1-ber" 8 | ) 9 | 10 | type compileTest struct { 11 | filterStr string 12 | filterType uint8 13 | } 14 | 15 | var testFilters = []compileTest{ 16 | {filterStr: "(&(sn=Müller)(givenName=Bob))", filterType: FilterAnd}, 17 | {filterStr: "(|(sn=Möller)(givenName=Bob))", filterType: FilterOr}, 18 | {filterStr: "(!(sn=Møller))", filterType: FilterNot}, 19 | {filterStr: "(sn=Müller)", filterType: FilterEqualityMatch}, 20 | {filterStr: "(sn=Möll*)", filterType: FilterSubstrings}, 21 | {filterStr: "(sn=*Møll)", filterType: FilterSubstrings}, 22 | {filterStr: "(sn=*Müll*)", filterType: FilterSubstrings}, 23 | {filterStr: "(sn>=Möller)", filterType: FilterGreaterOrEqual}, 24 | {filterStr: "(sn<=Møller)", filterType: FilterLessOrEqual}, 25 | {filterStr: "(sn=*)", filterType: FilterPresent}, 26 | {filterStr: "(sn~=Müller)", filterType: FilterApproxMatch}, 27 | // { filterStr: "()", filterType: FilterExtensibleMatch }, 28 | } 29 | 30 | func TestFilter(t *testing.T) { 31 | // Test Compiler and Decompiler 32 | for _, i := range testFilters { 33 | filter, err := CompileFilter(i.filterStr) 34 | if err != nil { 35 | t.Errorf("Problem compiling %s - %s", i.filterStr, err.Error()) 36 | } else if filter.Tag != uint8(i.filterType) { 37 | t.Errorf("%q Expected %q got %q", i.filterStr, FilterMap[i.filterType], FilterMap[filter.Tag]) 38 | } else { 39 | o, err := DecompileFilter(filter) 40 | if err != nil { 41 | t.Errorf("Problem compiling %s - %s", i.filterStr, err.Error()) 42 | } else if i.filterStr != o { 43 | t.Errorf("%q expected, got %q", i.filterStr, o) 44 | } 45 | } 46 | } 47 | } 48 | 49 | type binTestFilter struct { 50 | bin []byte 51 | str string 52 | } 53 | 54 | var binTestFilters = []binTestFilter{ 55 | {bin: []byte{0x87, 0x06, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72}, str: "(member=*)"}, 56 | } 57 | 58 | func TestFiltersDecode(t *testing.T) { 59 | for i, test := range binTestFilters { 60 | p := ber.DecodePacket(test.bin) 61 | if filter, err := DecompileFilter(p); err != nil { 62 | t.Errorf("binTestFilters[%d], DecompileFilter returned : %s", i, err) 63 | } else if filter != test.str { 64 | t.Errorf("binTestFilters[%d], %q expected, got %q", i, test.str, filter) 65 | } 66 | } 67 | } 68 | 69 | func TestFiltersEncode(t *testing.T) { 70 | for i, test := range binTestFilters { 71 | p, err := CompileFilter(test.str) 72 | if err != nil { 73 | t.Errorf("binTestFilters[%d], CompileFilter returned : %s", i, err) 74 | continue 75 | } 76 | b := p.Bytes() 77 | if !reflect.DeepEqual(b, test.bin) { 78 | t.Errorf("binTestFilters[%d], %q expected for CompileFilter(%q), got %q", i, test.bin, test.str, b) 79 | } 80 | } 81 | } 82 | 83 | func BenchmarkFilterCompile(b *testing.B) { 84 | b.StopTimer() 85 | filters := make([]string, len(testFilters)) 86 | 87 | // Test Compiler and Decompiler 88 | for idx, i := range testFilters { 89 | filters[idx] = i.filterStr 90 | } 91 | 92 | maxIdx := len(filters) 93 | b.StartTimer() 94 | for i := 0; i < b.N; i++ { 95 | CompileFilter(filters[i%maxIdx]) 96 | } 97 | } 98 | 99 | func BenchmarkFilterDecompile(b *testing.B) { 100 | b.StopTimer() 101 | filters := make([]*ber.Packet, len(testFilters)) 102 | 103 | // Test Compiler and Decompiler 104 | for idx, i := range testFilters { 105 | filters[idx], _ = CompileFilter(i.filterStr) 106 | } 107 | 108 | maxIdx := len(filters) 109 | b.StartTimer() 110 | for i := 0; i < b.N; i++ { 111 | DecompileFilter(filters[i%maxIdx]) 112 | } 113 | } 114 | 115 | func TestGetFilterObjectClass(t *testing.T) { 116 | c, err := GetFilterObjectClass("(objectClass=*)") 117 | if err != nil { 118 | t.Errorf("GetFilterObjectClass failed") 119 | } 120 | if c != "" { 121 | t.Errorf("GetFilterObjectClass failed") 122 | } 123 | c, err = GetFilterObjectClass("(objectClass=posixAccount)") 124 | if err != nil { 125 | t.Errorf("GetFilterObjectClass failed") 126 | } 127 | if c != "posixaccount" { 128 | t.Errorf("GetFilterObjectClass failed") 129 | } 130 | c, err = GetFilterObjectClass("(&(cn=awesome)(objectClass=posixGroup))") 131 | if err != nil { 132 | t.Errorf("GetFilterObjectClass failed") 133 | } 134 | if c != "posixgroup" { 135 | t.Errorf("GetFilterObjectClass failed") 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LDAP for Golang 2 | 3 | This library provides basic LDAP v3 functionality for the GO programming language. 4 | 5 | The **client** portion is limited, but sufficient to perform LDAP authentication and directory lookups (binds and searches) against any modern LDAP server (tested with OpenLDAP and AD). 6 | 7 | The **server** portion implements Bind and Search from [RFC4510](http://tools.ietf.org/html/rfc4510), has good testing coverage, and is compatible with any LDAPv3 client. It provides the building blocks for a custom LDAP server, but you must implement the backend datastore of your choice. 8 | 9 | 10 | ## LDAP client notes: 11 | 12 | ### A simple LDAP bind operation: 13 | ```go 14 | l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) 15 | // be sure to add error checking! 16 | defer l.Close() 17 | err = l.Bind(user, passwd) 18 | if err==nil { 19 | // authenticated 20 | } else { 21 | // invalid authentication 22 | } 23 | ``` 24 | 25 | ### A simple LDAP search operation: 26 | ```go 27 | search := &SearchRequest{ 28 | BaseDN: "dc=example,dc=com", 29 | Filter: "(objectclass=*)", 30 | } 31 | searchResults, err := l.Search(search) 32 | // be sure to add error checking! 33 | ``` 34 | 35 | ### Implemented: 36 | * Connecting, binding to LDAP server 37 | * Searching for entries with filtering and paging controls 38 | * Compiling string filters to LDAP filters 39 | * Modify Requests / Responses 40 | 41 | ### Not implemented: 42 | * Add, Delete, Modify DN, Compare operations 43 | * Most tests / benchmarks 44 | 45 | ### LDAP client examples: 46 | * examples/search.go: **Basic client bind and search** 47 | * examples/searchSSL.go: **Client bind and search over SSL** 48 | * examples/searchTLS.go: **Client bind and search over TLS** 49 | * examples/modify.go: **Client modify operation** 50 | 51 | *Client library by: [mmitton](https://github.com/mmitton), with contributions from: [uavila](https://github.com/uavila), [vanackere](https://github.com/vanackere), [juju2013](https://github.com/juju2013), [johnweldon](https://github.com/johnweldon), [marcsauter](https://github.com/marcsauter), and [nmcclain](https://github.com/nmcclain)* 52 | 53 | ## LDAP server notes: 54 | The server library is modeled after net/http - you designate handlers for the LDAP operations you want to support (Bind/Search/etc.), then start the server with ListenAndServe(). You can specify different handlers for different baseDNs - they must implement the interfaces of the operations you want to support: 55 | ```go 56 | type Binder interface { 57 | Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) 58 | } 59 | type Searcher interface { 60 | Search(boundDN string, searchReq SearchRequest, conn net.Conn) (ServerSearchResult, error) 61 | } 62 | type Closer interface { 63 | Close(conn net.Conn) error 64 | } 65 | ``` 66 | 67 | ### A basic bind-only LDAP server 68 | ```go 69 | func main() { 70 | s := ldap.NewServer() 71 | handler := ldapHandler{} 72 | s.BindFunc("", handler) 73 | if err := s.ListenAndServe("localhost:389"); err != nil { 74 | log.Fatal("LDAP Server Failed: %s", err.Error()) 75 | } 76 | } 77 | type ldapHandler struct { 78 | } 79 | func (h ldapHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldap.LDAPResultCode, error) { 80 | if bindDN == "" && bindSimplePw == "" { 81 | return ldap.LDAPResultSuccess, nil 82 | } 83 | return ldap.LDAPResultInvalidCredentials, nil 84 | } 85 | ``` 86 | 87 | * Server.EnforceLDAP: Normally, the LDAP server will return whatever results your handler provides. Set the **Server.EnforceLDAP** flag to **true** and the server will apply the LDAP **search filter**, **attributes limits**, **size/time limits**, **search scope**, and **base DN matching** to your handler's dataset. This makes it a lot simpler to write a custom LDAP server without worrying about LDAP internals. 88 | 89 | ### LDAP server examples: 90 | * examples/server.go: **Basic LDAP authentication (bind and search only)** 91 | * examples/proxy.go: **Simple LDAP proxy server.** 92 | * server_test.go: **The _test.go files have examples of all server functions.** 93 | 94 | ### Known limitations: 95 | 96 | * Golang's TLS implementation does not support SSLv2. Some old OSs require SSLv2, and are not able to connect to an LDAP server created with this library's ListenAndServeTLS() function. If you *must* support legacy (read: *insecure*) SSLv2 clients, run your LDAP server behind HAProxy. 97 | 98 | ### Not implemented: 99 | From the server perspective, all of [RFC4510](http://tools.ietf.org/html/rfc4510) is implemented **except**: 100 | * 4.5.1.3. SearchRequest.derefAliases 101 | * 4.5.1.5. SearchRequest.timeLimit 102 | * 4.5.1.6. SearchRequest.typesOnly 103 | * 4.14. StartTLS Operation 104 | 105 | *Server library by: [nmcclain](https://github.com/nmcclain)* 106 | -------------------------------------------------------------------------------- /control.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ldap 6 | 7 | import ( 8 | "strings" 9 | "fmt" 10 | "github.com/nmcclain/asn1-ber" 11 | ) 12 | 13 | const ( 14 | ControlTypePaging = "1.2.840.113556.1.4.319" 15 | ) 16 | 17 | var ControlTypeMap = map[string]string{ 18 | ControlTypePaging: "Paging", 19 | } 20 | 21 | type Control interface { 22 | GetControlType() string 23 | Encode() *ber.Packet 24 | String() string 25 | } 26 | 27 | type ControlString struct { 28 | ControlType string 29 | Criticality bool 30 | ControlValue string 31 | } 32 | 33 | func (c *ControlString) GetControlType() string { 34 | return c.ControlType 35 | } 36 | 37 | func (c *ControlString) Encode() *ber.Packet { 38 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") 39 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, c.ControlType, "Control Type ("+ControlTypeMap[c.ControlType]+")")) 40 | if c.Criticality { 41 | packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) 42 | } 43 | if strings.TrimSpace(c.ControlValue) != "" { 44 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, c.ControlValue, "Control Value")) 45 | } 46 | return packet 47 | } 48 | 49 | func (c *ControlString) String() string { 50 | return fmt.Sprintf("Control Type: %s (%q) Criticality: %t Control Value: %s", ControlTypeMap[c.ControlType], c.ControlType, c.Criticality, c.ControlValue) 51 | } 52 | 53 | type ControlPaging struct { 54 | PagingSize uint32 55 | Cookie []byte 56 | } 57 | 58 | func (c *ControlPaging) GetControlType() string { 59 | return ControlTypePaging 60 | } 61 | 62 | func (c *ControlPaging) Encode() *ber.Packet { 63 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") 64 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypePaging, "Control Type ("+ControlTypeMap[ControlTypePaging]+")")) 65 | 66 | p2 := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (Paging)") 67 | seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Search Control Value") 68 | seq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, uint64(c.PagingSize), "Paging Size")) 69 | cookie := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Cookie") 70 | cookie.Value = c.Cookie 71 | cookie.Data.Write(c.Cookie) 72 | seq.AppendChild(cookie) 73 | p2.AppendChild(seq) 74 | 75 | packet.AppendChild(p2) 76 | return packet 77 | } 78 | 79 | func (c *ControlPaging) String() string { 80 | return fmt.Sprintf( 81 | "Control Type: %s (%q) Criticality: %t PagingSize: %d Cookie: %q", 82 | ControlTypeMap[ControlTypePaging], 83 | ControlTypePaging, 84 | false, 85 | c.PagingSize, 86 | c.Cookie) 87 | } 88 | 89 | func (c *ControlPaging) SetCookie(cookie []byte) { 90 | c.Cookie = cookie 91 | } 92 | 93 | func FindControl(controls []Control, controlType string) Control { 94 | for _, c := range controls { 95 | if c.GetControlType() == controlType { 96 | return c 97 | } 98 | } 99 | return nil 100 | } 101 | 102 | func DecodeControl(packet *ber.Packet) Control { 103 | ControlType := packet.Children[0].Value.(string) 104 | packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")" 105 | c := new(ControlString) 106 | c.ControlType = ControlType 107 | c.Criticality = false 108 | 109 | if len(packet.Children) > 1 { 110 | value := packet.Children[1] 111 | if len(packet.Children) == 3 { 112 | value = packet.Children[2] 113 | packet.Children[1].Description = "Criticality" 114 | c.Criticality = packet.Children[1].Value.(bool) 115 | } 116 | 117 | value.Description = "Control Value" 118 | switch ControlType { 119 | case ControlTypePaging: 120 | value.Description += " (Paging)" 121 | c := new(ControlPaging) 122 | if value.Value != nil { 123 | valueChildren := ber.DecodePacket(value.Data.Bytes()) 124 | value.Data.Truncate(0) 125 | value.Value = nil 126 | value.AppendChild(valueChildren) 127 | } 128 | value = value.Children[0] 129 | value.Description = "Search Control Value" 130 | value.Children[0].Description = "Paging Size" 131 | value.Children[1].Description = "Cookie" 132 | c.PagingSize = uint32(value.Children[0].Value.(uint64)) 133 | c.Cookie = value.Children[1].Data.Bytes() 134 | value.Children[1].Value = c.Cookie 135 | return c 136 | } 137 | c.ControlValue = value.Value.(string) 138 | } 139 | return c 140 | } 141 | 142 | func NewControlString(controlType string, criticality bool, controlValue string) *ControlString { 143 | return &ControlString{ 144 | ControlType: controlType, 145 | Criticality: criticality, 146 | ControlValue: controlValue, 147 | } 148 | } 149 | 150 | func NewControlPaging(pagingSize uint32) *ControlPaging { 151 | return &ControlPaging{PagingSize: pagingSize} 152 | } 153 | 154 | func encodeControls(controls []Control) *ber.Packet { 155 | packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0, nil, "Controls") 156 | for _, control := range controls { 157 | packet.AppendChild(control.Encode()) 158 | } 159 | return packet 160 | } 161 | -------------------------------------------------------------------------------- /modify.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | // 5 | // File contains Modify functionality 6 | // 7 | // https://tools.ietf.org/html/rfc4511 8 | // 9 | // ModifyRequest ::= [APPLICATION 6] SEQUENCE { 10 | // object LDAPDN, 11 | // changes SEQUENCE OF change SEQUENCE { 12 | // operation ENUMERATED { 13 | // add (0), 14 | // delete (1), 15 | // replace (2), 16 | // ... }, 17 | // modification PartialAttribute } } 18 | // 19 | // PartialAttribute ::= SEQUENCE { 20 | // type AttributeDescription, 21 | // vals SET OF value AttributeValue } 22 | // 23 | // AttributeDescription ::= LDAPString 24 | // -- Constrained to 25 | // -- [RFC4512] 26 | // 27 | // AttributeValue ::= OCTET STRING 28 | // 29 | 30 | package ldap 31 | 32 | import ( 33 | "errors" 34 | "log" 35 | 36 | "github.com/nmcclain/asn1-ber" 37 | ) 38 | 39 | const ( 40 | AddAttribute = 0 41 | DeleteAttribute = 1 42 | ReplaceAttribute = 2 43 | ) 44 | 45 | var LDAPModifyAttributeMap = map[uint64]string{ 46 | AddAttribute: "Add", 47 | DeleteAttribute: "Delete", 48 | ReplaceAttribute: "Replace", 49 | } 50 | 51 | type PartialAttribute struct { 52 | AttrType string 53 | AttrVals []string 54 | } 55 | 56 | func (p *PartialAttribute) encode() *ber.Packet { 57 | seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "PartialAttribute") 58 | seq.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, p.AttrType, "Type")) 59 | set := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "AttributeValue") 60 | for _, value := range p.AttrVals { 61 | set.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Vals")) 62 | } 63 | seq.AppendChild(set) 64 | return seq 65 | } 66 | 67 | type ModifyRequest struct { 68 | Dn string 69 | AddAttributes []PartialAttribute 70 | DeleteAttributes []PartialAttribute 71 | ReplaceAttributes []PartialAttribute 72 | } 73 | 74 | func (m *ModifyRequest) Add(attrType string, attrVals []string) { 75 | m.AddAttributes = append(m.AddAttributes, PartialAttribute{AttrType: attrType, AttrVals: attrVals}) 76 | } 77 | 78 | func (m *ModifyRequest) Delete(attrType string, attrVals []string) { 79 | m.DeleteAttributes = append(m.DeleteAttributes, PartialAttribute{AttrType: attrType, AttrVals: attrVals}) 80 | } 81 | 82 | func (m *ModifyRequest) Replace(attrType string, attrVals []string) { 83 | m.ReplaceAttributes = append(m.ReplaceAttributes, PartialAttribute{AttrType: attrType, AttrVals: attrVals}) 84 | } 85 | 86 | func (m ModifyRequest) encode() *ber.Packet { 87 | request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationModifyRequest, nil, "Modify Request") 88 | request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, m.Dn, "DN")) 89 | changes := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Changes") 90 | for _, attribute := range m.AddAttributes { 91 | change := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Change") 92 | change.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(AddAttribute), "Operation")) 93 | change.AppendChild(attribute.encode()) 94 | changes.AppendChild(change) 95 | } 96 | for _, attribute := range m.DeleteAttributes { 97 | change := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Change") 98 | change.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(DeleteAttribute), "Operation")) 99 | change.AppendChild(attribute.encode()) 100 | changes.AppendChild(change) 101 | } 102 | for _, attribute := range m.ReplaceAttributes { 103 | change := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Change") 104 | change.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(ReplaceAttribute), "Operation")) 105 | change.AppendChild(attribute.encode()) 106 | changes.AppendChild(change) 107 | } 108 | request.AppendChild(changes) 109 | return request 110 | } 111 | 112 | func NewModifyRequest( 113 | dn string, 114 | ) *ModifyRequest { 115 | return &ModifyRequest{ 116 | Dn: dn, 117 | } 118 | } 119 | 120 | func (l *Conn) Modify(modifyRequest *ModifyRequest) error { 121 | messageID := l.nextMessageID() 122 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 123 | packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "MessageID")) 124 | packet.AppendChild(modifyRequest.encode()) 125 | 126 | l.Debug.PrintPacket(packet) 127 | 128 | channel, err := l.sendMessage(packet) 129 | if err != nil { 130 | return err 131 | } 132 | if channel == nil { 133 | return NewError(ErrorNetwork, errors.New("ldap: could not send message")) 134 | } 135 | defer l.finishMessage(messageID) 136 | 137 | l.Debug.Printf("%d: waiting for response", messageID) 138 | packet = <-channel 139 | l.Debug.Printf("%d: got response %p", messageID, packet) 140 | if packet == nil { 141 | return NewError(ErrorNetwork, errors.New("ldap: could not retrieve message")) 142 | } 143 | 144 | if l.Debug { 145 | if err := addLDAPDescriptions(packet); err != nil { 146 | return err 147 | } 148 | ber.PrintPacket(packet) 149 | } 150 | 151 | if packet.Children[1].Tag == ApplicationModifyResponse { 152 | resultCode, resultDescription := getLDAPResultCode(packet) 153 | if resultCode != 0 { 154 | return NewError(resultCode, errors.New(resultDescription)) 155 | } 156 | } else { 157 | log.Printf("Unexpected Response: %d", packet.Children[1].Tag) 158 | } 159 | 160 | l.Debug.Printf("%d: returning", messageID) 161 | return nil 162 | } 163 | -------------------------------------------------------------------------------- /server_modify_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "net" 5 | "os/exec" 6 | "strings" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | // 12 | func TestAdd(t *testing.T) { 13 | quit := make(chan bool) 14 | done := make(chan bool) 15 | go func() { 16 | s := NewServer() 17 | s.QuitChannel(quit) 18 | s.BindFunc("", modifyTestHandler{}) 19 | s.AddFunc("", modifyTestHandler{}) 20 | if err := s.ListenAndServe(listenString); err != nil { 21 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 22 | } 23 | }() 24 | go func() { 25 | cmd := exec.Command("ldapadd", "-v", "-H", ldapURL, "-x", "-f", "tests/add.ldif") 26 | out, _ := cmd.CombinedOutput() 27 | if !strings.Contains(string(out), "modify complete") { 28 | t.Errorf("ldapadd failed: %v", string(out)) 29 | } 30 | cmd = exec.Command("ldapadd", "-v", "-H", ldapURL, "-x", "-f", "tests/add2.ldif") 31 | out, _ = cmd.CombinedOutput() 32 | if !strings.Contains(string(out), "ldap_add: Insufficient access") { 33 | t.Errorf("ldapadd should have failed: %v", string(out)) 34 | } 35 | if strings.Contains(string(out), "modify complete") { 36 | t.Errorf("ldapadd should have failed: %v", string(out)) 37 | } 38 | done <- true 39 | }() 40 | select { 41 | case <-done: 42 | case <-time.After(timeout): 43 | t.Errorf("ldapadd command timed out") 44 | } 45 | quit <- true 46 | } 47 | 48 | // 49 | func TestDelete(t *testing.T) { 50 | quit := make(chan bool) 51 | done := make(chan bool) 52 | go func() { 53 | s := NewServer() 54 | s.QuitChannel(quit) 55 | s.BindFunc("", modifyTestHandler{}) 56 | s.DeleteFunc("", modifyTestHandler{}) 57 | if err := s.ListenAndServe(listenString); err != nil { 58 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 59 | } 60 | }() 61 | go func() { 62 | cmd := exec.Command("ldapdelete", "-v", "-H", ldapURL, "-x", "cn=Delete Me,dc=example,dc=com") 63 | out, _ := cmd.CombinedOutput() 64 | if !strings.Contains(string(out), "Delete Result: Success (0)") || !strings.Contains(string(out), "Additional info: Success") { 65 | t.Errorf("ldapdelete failed: %v", string(out)) 66 | } 67 | cmd = exec.Command("ldapdelete", "-v", "-H", ldapURL, "-x", "cn=Bob,dc=example,dc=com") 68 | out, _ = cmd.CombinedOutput() 69 | if strings.Contains(string(out), "Success") || !strings.Contains(string(out), "ldap_delete: Insufficient access") { 70 | t.Errorf("ldapdelete should have failed: %v", string(out)) 71 | } 72 | done <- true 73 | }() 74 | select { 75 | case <-done: 76 | case <-time.After(timeout): 77 | t.Errorf("ldapdelete command timed out") 78 | } 79 | quit <- true 80 | } 81 | 82 | func TestModify(t *testing.T) { 83 | quit := make(chan bool) 84 | done := make(chan bool) 85 | go func() { 86 | s := NewServer() 87 | s.QuitChannel(quit) 88 | s.BindFunc("", modifyTestHandler{}) 89 | s.ModifyFunc("", modifyTestHandler{}) 90 | if err := s.ListenAndServe(listenString); err != nil { 91 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 92 | } 93 | }() 94 | go func() { 95 | cmd := exec.Command("ldapmodify", "-v", "-H", ldapURL, "-x", "-f", "tests/modify.ldif") 96 | out, _ := cmd.CombinedOutput() 97 | if !strings.Contains(string(out), "modify complete") { 98 | t.Errorf("ldapmodify failed: %v", string(out)) 99 | } 100 | cmd = exec.Command("ldapmodify", "-v", "-H", ldapURL, "-x", "-f", "tests/modify2.ldif") 101 | out, _ = cmd.CombinedOutput() 102 | if !strings.Contains(string(out), "ldap_modify: Insufficient access") || strings.Contains(string(out), "modify complete") { 103 | t.Errorf("ldapmodify should have failed: %v", string(out)) 104 | } 105 | done <- true 106 | }() 107 | select { 108 | case <-done: 109 | case <-time.After(timeout): 110 | t.Errorf("ldapadd command timed out") 111 | } 112 | quit <- true 113 | } 114 | 115 | /* 116 | func TestModifyDN(t *testing.T) { 117 | quit := make(chan bool) 118 | done := make(chan bool) 119 | go func() { 120 | s := NewServer() 121 | s.QuitChannel(quit) 122 | s.BindFunc("", modifyTestHandler{}) 123 | s.AddFunc("", modifyTestHandler{}) 124 | if err := s.ListenAndServe(listenString); err != nil { 125 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 126 | } 127 | }() 128 | go func() { 129 | cmd := exec.Command("ldapadd", "-v", "-H", ldapURL, "-x", "-f", "tests/add.ldif") 130 | //ldapmodrdn -H ldap://localhost:3389 -x "uid=babs,dc=example,dc=com" "uid=babsy,dc=example,dc=com" 131 | out, _ := cmd.CombinedOutput() 132 | if !strings.Contains(string(out), "modify complete") { 133 | t.Errorf("ldapadd failed: %v", string(out)) 134 | } 135 | cmd = exec.Command("ldapadd", "-v", "-H", ldapURL, "-x", "-f", "tests/add2.ldif") 136 | out, _ = cmd.CombinedOutput() 137 | if !strings.Contains(string(out), "ldap_add: Insufficient access") { 138 | t.Errorf("ldapadd should have failed: %v", string(out)) 139 | } 140 | if strings.Contains(string(out), "modify complete") { 141 | t.Errorf("ldapadd should have failed: %v", string(out)) 142 | } 143 | done <- true 144 | }() 145 | select { 146 | case <-done: 147 | case <-time.After(timeout): 148 | t.Errorf("ldapadd command timed out") 149 | } 150 | quit <- true 151 | } 152 | */ 153 | 154 | // 155 | type modifyTestHandler struct { 156 | } 157 | 158 | func (h modifyTestHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) { 159 | if bindDN == "" && bindSimplePw == "" { 160 | return LDAPResultSuccess, nil 161 | } 162 | return LDAPResultInvalidCredentials, nil 163 | } 164 | func (h modifyTestHandler) Add(boundDN string, req AddRequest, conn net.Conn) (LDAPResultCode, error) { 165 | // only succeed on expected contents of add.ldif: 166 | if len(req.attributes) == 5 && req.dn == "cn=Barbara Jensen,dc=example,dc=com" && 167 | req.attributes[2].attrType == "sn" && len(req.attributes[2].attrVals) == 1 && 168 | req.attributes[2].attrVals[0] == "Jensen" { 169 | return LDAPResultSuccess, nil 170 | } 171 | return LDAPResultInsufficientAccessRights, nil 172 | } 173 | func (h modifyTestHandler) Delete(boundDN, deleteDN string, conn net.Conn) (LDAPResultCode, error) { 174 | // only succeed on expected deleteDN 175 | if deleteDN == "cn=Delete Me,dc=example,dc=com" { 176 | return LDAPResultSuccess, nil 177 | } 178 | return LDAPResultInsufficientAccessRights, nil 179 | } 180 | func (h modifyTestHandler) Modify(boundDN string, req ModifyRequest, conn net.Conn) (LDAPResultCode, error) { 181 | // only succeed on expected contents of modify.ldif: 182 | if req.Dn == "cn=testy,dc=example,dc=com" && len(req.AddAttributes) == 1 && 183 | len(req.DeleteAttributes) == 3 && len(req.ReplaceAttributes) == 2 && 184 | req.DeleteAttributes[2].AttrType == "details" && len(req.DeleteAttributes[2].AttrVals) == 0 { 185 | return LDAPResultSuccess, nil 186 | } 187 | return LDAPResultInsufficientAccessRights, nil 188 | } 189 | func (h modifyTestHandler) ModifyDN(boundDN string, req ModifyDNRequest, conn net.Conn) (LDAPResultCode, error) { 190 | return LDAPResultInsufficientAccessRights, nil 191 | } 192 | -------------------------------------------------------------------------------- /server_modify.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "log" 5 | "net" 6 | 7 | "github.com/nmcclain/asn1-ber" 8 | ) 9 | 10 | func HandleAddRequest(req *ber.Packet, boundDN string, fns map[string]Adder, conn net.Conn) (resultCode LDAPResultCode) { 11 | if len(req.Children) != 2 { 12 | return LDAPResultProtocolError 13 | } 14 | var ok bool 15 | addReq := AddRequest{} 16 | addReq.dn, ok = req.Children[0].Value.(string) 17 | if !ok { 18 | return LDAPResultProtocolError 19 | } 20 | addReq.attributes = []Attribute{} 21 | for _, attr := range req.Children[1].Children { 22 | if len(attr.Children) != 2 { 23 | return LDAPResultProtocolError 24 | } 25 | 26 | a := Attribute{} 27 | a.attrType, ok = attr.Children[0].Value.(string) 28 | if !ok { 29 | return LDAPResultProtocolError 30 | } 31 | a.attrVals = []string{} 32 | for _, val := range attr.Children[1].Children { 33 | v, ok := val.Value.(string) 34 | if !ok { 35 | return LDAPResultProtocolError 36 | } 37 | a.attrVals = append(a.attrVals, v) 38 | } 39 | addReq.attributes = append(addReq.attributes, a) 40 | } 41 | fnNames := []string{} 42 | for k := range fns { 43 | fnNames = append(fnNames, k) 44 | } 45 | fn := routeFunc(boundDN, fnNames) 46 | resultCode, err := fns[fn].Add(boundDN, addReq, conn) 47 | if err != nil { 48 | log.Printf("AddFn Error %s", err.Error()) 49 | return LDAPResultOperationsError 50 | } 51 | return resultCode 52 | } 53 | 54 | func HandleDeleteRequest(req *ber.Packet, boundDN string, fns map[string]Deleter, conn net.Conn) (resultCode LDAPResultCode) { 55 | deleteDN := ber.DecodeString(req.Data.Bytes()) 56 | fnNames := []string{} 57 | for k := range fns { 58 | fnNames = append(fnNames, k) 59 | } 60 | fn := routeFunc(boundDN, fnNames) 61 | resultCode, err := fns[fn].Delete(boundDN, deleteDN, conn) 62 | if err != nil { 63 | log.Printf("DeleteFn Error %s", err.Error()) 64 | return LDAPResultOperationsError 65 | } 66 | return resultCode 67 | } 68 | 69 | func HandleModifyRequest(req *ber.Packet, boundDN string, fns map[string]Modifier, conn net.Conn) (resultCode LDAPResultCode) { 70 | if len(req.Children) != 2 { 71 | return LDAPResultProtocolError 72 | } 73 | var ok bool 74 | modReq := ModifyRequest{} 75 | modReq.Dn, ok = req.Children[0].Value.(string) 76 | if !ok { 77 | return LDAPResultProtocolError 78 | } 79 | for _, change := range req.Children[1].Children { 80 | if len(change.Children) != 2 { 81 | return LDAPResultProtocolError 82 | } 83 | attr := PartialAttribute{} 84 | attrs := change.Children[1].Children 85 | if len(attrs) != 2 { 86 | return LDAPResultProtocolError 87 | } 88 | attr.AttrType, ok = attrs[0].Value.(string) 89 | if !ok { 90 | return LDAPResultProtocolError 91 | } 92 | for _, val := range attrs[1].Children { 93 | v, ok := val.Value.(string) 94 | if !ok { 95 | return LDAPResultProtocolError 96 | } 97 | attr.AttrVals = append(attr.AttrVals, v) 98 | } 99 | op, ok := change.Children[0].Value.(uint64) 100 | if !ok { 101 | return LDAPResultProtocolError 102 | } 103 | switch op { 104 | default: 105 | log.Printf("Unrecognized Modify attribute %d", op) 106 | return LDAPResultProtocolError 107 | case AddAttribute: 108 | modReq.Add(attr.AttrType, attr.AttrVals) 109 | case DeleteAttribute: 110 | modReq.Delete(attr.AttrType, attr.AttrVals) 111 | case ReplaceAttribute: 112 | modReq.Replace(attr.AttrType, attr.AttrVals) 113 | } 114 | } 115 | fnNames := []string{} 116 | for k := range fns { 117 | fnNames = append(fnNames, k) 118 | } 119 | fn := routeFunc(boundDN, fnNames) 120 | resultCode, err := fns[fn].Modify(boundDN, modReq, conn) 121 | if err != nil { 122 | log.Printf("ModifyFn Error %s", err.Error()) 123 | return LDAPResultOperationsError 124 | } 125 | return resultCode 126 | } 127 | 128 | func HandleCompareRequest(req *ber.Packet, boundDN string, fns map[string]Comparer, conn net.Conn) (resultCode LDAPResultCode) { 129 | if len(req.Children) != 2 { 130 | return LDAPResultProtocolError 131 | } 132 | var ok bool 133 | compReq := CompareRequest{} 134 | compReq.dn, ok = req.Children[0].Value.(string) 135 | if !ok { 136 | return LDAPResultProtocolError 137 | } 138 | ava := req.Children[1] 139 | if len(ava.Children) != 2 { 140 | return LDAPResultProtocolError 141 | } 142 | attr, ok := ava.Children[0].Value.(string) 143 | if !ok { 144 | return LDAPResultProtocolError 145 | } 146 | val, ok := ava.Children[1].Value.(string) 147 | if !ok { 148 | return LDAPResultProtocolError 149 | } 150 | compReq.ava = []AttributeValueAssertion{AttributeValueAssertion{attr, val}} 151 | fnNames := []string{} 152 | for k := range fns { 153 | fnNames = append(fnNames, k) 154 | } 155 | fn := routeFunc(boundDN, fnNames) 156 | resultCode, err := fns[fn].Compare(boundDN, compReq, conn) 157 | if err != nil { 158 | log.Printf("CompareFn Error %s", err.Error()) 159 | return LDAPResultOperationsError 160 | } 161 | return resultCode 162 | } 163 | 164 | func HandleExtendedRequest(req *ber.Packet, boundDN string, fns map[string]Extender, conn net.Conn) (resultCode LDAPResultCode) { 165 | if len(req.Children) != 1 && len(req.Children) != 2 { 166 | return LDAPResultProtocolError 167 | } 168 | name := ber.DecodeString(req.Children[0].Data.Bytes()) 169 | var val string 170 | if len(req.Children) == 2 { 171 | val = ber.DecodeString(req.Children[1].Data.Bytes()) 172 | } 173 | extReq := ExtendedRequest{name, val} 174 | fnNames := []string{} 175 | for k := range fns { 176 | fnNames = append(fnNames, k) 177 | } 178 | fn := routeFunc(boundDN, fnNames) 179 | resultCode, err := fns[fn].Extended(boundDN, extReq, conn) 180 | if err != nil { 181 | log.Printf("ExtendedFn Error %s", err.Error()) 182 | return LDAPResultOperationsError 183 | } 184 | return resultCode 185 | } 186 | 187 | func HandleAbandonRequest(req *ber.Packet, boundDN string, fns map[string]Abandoner, conn net.Conn) error { 188 | fnNames := []string{} 189 | for k := range fns { 190 | fnNames = append(fnNames, k) 191 | } 192 | fn := routeFunc(boundDN, fnNames) 193 | err := fns[fn].Abandon(boundDN, conn) 194 | return err 195 | } 196 | 197 | func HandleModifyDNRequest(req *ber.Packet, boundDN string, fns map[string]ModifyDNr, conn net.Conn) (resultCode LDAPResultCode) { 198 | if len(req.Children) != 3 && len(req.Children) != 4 { 199 | return LDAPResultProtocolError 200 | } 201 | var ok bool 202 | mdnReq := ModifyDNRequest{} 203 | mdnReq.dn, ok = req.Children[0].Value.(string) 204 | if !ok { 205 | return LDAPResultProtocolError 206 | } 207 | mdnReq.newrdn, ok = req.Children[1].Value.(string) 208 | if !ok { 209 | return LDAPResultProtocolError 210 | } 211 | mdnReq.deleteoldrdn, ok = req.Children[2].Value.(bool) 212 | if !ok { 213 | return LDAPResultProtocolError 214 | } 215 | if len(req.Children) == 4 { 216 | mdnReq.newSuperior, ok = req.Children[3].Value.(string) 217 | if !ok { 218 | return LDAPResultProtocolError 219 | } 220 | } 221 | fnNames := []string{} 222 | for k := range fns { 223 | fnNames = append(fnNames, k) 224 | } 225 | fn := routeFunc(boundDN, fnNames) 226 | resultCode, err := fns[fn].ModifyDN(boundDN, mdnReq, conn) 227 | if err != nil { 228 | log.Printf("ModifyDN Error %s", err.Error()) 229 | return LDAPResultOperationsError 230 | } 231 | return resultCode 232 | } 233 | -------------------------------------------------------------------------------- /server_search.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "strings" 8 | 9 | ber "github.com/nmcclain/asn1-ber" 10 | ) 11 | 12 | func HandleSearchRequest(req *ber.Packet, controls *[]Control, messageID uint64, boundDN string, server *Server, conn net.Conn) (resultErr error) { 13 | defer func() { 14 | if r := recover(); r != nil { 15 | resultErr = NewError(LDAPResultOperationsError, fmt.Errorf("Search function panic: %s", r)) 16 | } 17 | }() 18 | 19 | searchReq, err := parseSearchRequest(boundDN, req, controls) 20 | if err != nil { 21 | return NewError(LDAPResultOperationsError, err) 22 | } 23 | 24 | filterPacket, err := CompileFilter(searchReq.Filter) 25 | if err != nil { 26 | return NewError(LDAPResultOperationsError, err) 27 | } 28 | 29 | fnNames := []string{} 30 | for k := range server.SearchFns { 31 | fnNames = append(fnNames, k) 32 | } 33 | fn := routeFunc(searchReq.BaseDN, fnNames) 34 | searchResp, err := server.SearchFns[fn].Search(boundDN, searchReq, conn) 35 | if err != nil { 36 | return NewError(searchResp.ResultCode, err) 37 | } 38 | 39 | if server.EnforceLDAP { 40 | if searchReq.DerefAliases != NeverDerefAliases { // [-a {never|always|search|find} 41 | // TODO: Server DerefAliases not supported: RFC4511 4.5.1.3 42 | } 43 | if searchReq.TimeLimit > 0 { 44 | // TODO: Server TimeLimit not implemented 45 | } 46 | } 47 | 48 | i := 0 49 | searchReqBaseDNLower := strings.ToLower(searchReq.BaseDN) 50 | for _, entry := range searchResp.Entries { 51 | if server.EnforceLDAP { 52 | // filter 53 | keep, resultCode := ServerApplyFilter(filterPacket, entry) 54 | if resultCode != LDAPResultSuccess { 55 | return NewError(resultCode, errors.New("ServerApplyFilter error")) 56 | } 57 | if !keep { 58 | continue 59 | } 60 | 61 | // constrained search scope 62 | switch searchReq.Scope { 63 | case ScopeWholeSubtree: // The scope is constrained to the entry named by baseObject and to all its subordinates. 64 | case ScopeBaseObject: // The scope is constrained to the entry named by baseObject. 65 | if strings.ToLower(entry.DN) != searchReqBaseDNLower { 66 | continue 67 | } 68 | case ScopeSingleLevel: // The scope is constrained to the immediate subordinates of the entry named by baseObject. 69 | entryDNLower := strings.ToLower(entry.DN) 70 | parts := strings.Split(entryDNLower, ",") 71 | if len(parts) < 2 && entryDNLower != searchReqBaseDNLower { 72 | continue 73 | } 74 | if dnSuffix := strings.Join(parts[1:], ","); dnSuffix != searchReqBaseDNLower { 75 | continue 76 | } 77 | } 78 | 79 | // filter attributes 80 | entry, err = filterAttributes(entry, searchReq.Attributes) 81 | if err != nil { 82 | return NewError(LDAPResultOperationsError, err) 83 | } 84 | 85 | // size limit 86 | if searchReq.SizeLimit > 0 && i >= searchReq.SizeLimit { 87 | break 88 | } 89 | i++ 90 | } 91 | 92 | // respond 93 | responsePacket := encodeSearchResponse(messageID, searchReq, entry) 94 | if err = sendPacket(conn, responsePacket); err != nil { 95 | return NewError(LDAPResultOperationsError, err) 96 | } 97 | } 98 | return nil 99 | } 100 | 101 | ///////////////////////// 102 | func parseSearchRequest(boundDN string, req *ber.Packet, controls *[]Control) (SearchRequest, error) { 103 | if len(req.Children) != 8 { 104 | return SearchRequest{}, NewError(LDAPResultOperationsError, errors.New("Bad search request")) 105 | } 106 | 107 | // Parse the request 108 | baseObject, ok := req.Children[0].Value.(string) 109 | if !ok { 110 | return SearchRequest{}, NewError(LDAPResultProtocolError, errors.New("Bad search request")) 111 | } 112 | s, ok := req.Children[1].Value.(uint64) 113 | if !ok { 114 | return SearchRequest{}, NewError(LDAPResultProtocolError, errors.New("Bad search request")) 115 | } 116 | scope := int(s) 117 | d, ok := req.Children[2].Value.(uint64) 118 | if !ok { 119 | return SearchRequest{}, NewError(LDAPResultProtocolError, errors.New("Bad search request")) 120 | } 121 | derefAliases := int(d) 122 | s, ok = req.Children[3].Value.(uint64) 123 | if !ok { 124 | return SearchRequest{}, NewError(LDAPResultProtocolError, errors.New("Bad search request")) 125 | } 126 | sizeLimit := int(s) 127 | t, ok := req.Children[4].Value.(uint64) 128 | if !ok { 129 | return SearchRequest{}, NewError(LDAPResultProtocolError, errors.New("Bad search request")) 130 | } 131 | timeLimit := int(t) 132 | typesOnly := false 133 | if req.Children[5].Value != nil { 134 | typesOnly, ok = req.Children[5].Value.(bool) 135 | if !ok { 136 | return SearchRequest{}, NewError(LDAPResultProtocolError, errors.New("Bad search request")) 137 | } 138 | } 139 | filter, err := DecompileFilter(req.Children[6]) 140 | if err != nil { 141 | return SearchRequest{}, err 142 | } 143 | attributes := []string{} 144 | for _, attr := range req.Children[7].Children { 145 | a, ok := attr.Value.(string) 146 | if !ok { 147 | return SearchRequest{}, NewError(LDAPResultProtocolError, errors.New("Bad search request")) 148 | } 149 | attributes = append(attributes, a) 150 | } 151 | searchReq := SearchRequest{baseObject, scope, 152 | derefAliases, sizeLimit, timeLimit, 153 | typesOnly, filter, attributes, *controls} 154 | 155 | return searchReq, nil 156 | } 157 | 158 | ///////////////////////// 159 | func filterAttributes(entry *Entry, attributes []string) (*Entry, error) { 160 | // only return requested attributes 161 | newAttributes := []*EntryAttribute{} 162 | 163 | if len(attributes) > 1 || (len(attributes) == 1 && len(attributes[0]) > 0) { 164 | for _, attr := range entry.Attributes { 165 | attrNameLower := strings.ToLower(attr.Name) 166 | for _, requested := range attributes { 167 | requestedLower := strings.ToLower(requested) 168 | // You can request the directory server to return operational attributes by adding + (the plus sign) in your ldapsearch command. 169 | // "+supportedControl" is treated as an operational attribute 170 | if strings.HasPrefix(attrNameLower, "+") { 171 | if requestedLower == "+" || attrNameLower == "+"+requestedLower { 172 | newAttributes = append(newAttributes, &EntryAttribute{attr.Name[1:], attr.Values}) 173 | break 174 | } 175 | } else { 176 | if requested == "*" || attrNameLower == requestedLower { 177 | newAttributes = append(newAttributes, attr) 178 | break 179 | } 180 | } 181 | } 182 | } 183 | } else { 184 | // remove operational attributes 185 | for _, attr := range entry.Attributes { 186 | if !strings.HasPrefix(attr.Name, "+") { 187 | newAttributes = append(newAttributes, attr) 188 | } 189 | } 190 | } 191 | entry.Attributes = newAttributes 192 | 193 | return entry, nil 194 | } 195 | 196 | ///////////////////////// 197 | func encodeSearchResponse(messageID uint64, req SearchRequest, res *Entry) *ber.Packet { 198 | responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response") 199 | responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "Message ID")) 200 | 201 | searchEntry := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationSearchResultEntry, nil, "Search Result Entry") 202 | searchEntry.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, res.DN, "Object Name")) 203 | 204 | attrs := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes:") 205 | for _, attribute := range res.Attributes { 206 | attrs.AppendChild(encodeSearchAttribute(attribute.Name, attribute.Values)) 207 | } 208 | 209 | searchEntry.AppendChild(attrs) 210 | responsePacket.AppendChild(searchEntry) 211 | 212 | return responsePacket 213 | } 214 | 215 | func encodeSearchAttribute(name string, values []string) *ber.Packet { 216 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attribute") 217 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, name, "Attribute Name")) 218 | 219 | valuesPacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "Attribute Values") 220 | for _, value := range values { 221 | valuesPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Attribute Value")) 222 | } 223 | 224 | packet.AppendChild(valuesPacket) 225 | 226 | return packet 227 | } 228 | 229 | func encodeSearchDone(messageID uint64, ldapResultCode LDAPResultCode) *ber.Packet { 230 | responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response") 231 | responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "Message ID")) 232 | donePacket := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationSearchResultDone, nil, "Search result done") 233 | donePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(ldapResultCode), "resultCode: ")) 234 | donePacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "matchedDN: ")) 235 | donePacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "errorMessage: ")) 236 | responsePacket.AppendChild(donePacket) 237 | 238 | return responsePacket 239 | } 240 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ldap 6 | 7 | import ( 8 | "crypto/tls" 9 | "errors" 10 | "log" 11 | "net" 12 | "sync" 13 | "time" 14 | 15 | "github.com/nmcclain/asn1-ber" 16 | ) 17 | 18 | const ( 19 | MessageQuit = 0 20 | MessageRequest = 1 21 | MessageResponse = 2 22 | MessageFinish = 3 23 | ) 24 | 25 | type messagePacket struct { 26 | Op int 27 | MessageID uint64 28 | Packet *ber.Packet 29 | Channel chan *ber.Packet 30 | } 31 | 32 | // Conn represents an LDAP Connection 33 | type Conn struct { 34 | conn net.Conn 35 | isTLS bool 36 | Debug debugging 37 | chanConfirm chan bool 38 | chanResults map[uint64]chan *ber.Packet 39 | chanMessage chan *messagePacket 40 | chanMessageID chan uint64 41 | wgSender sync.WaitGroup 42 | chanDone chan struct{} 43 | once sync.Once 44 | } 45 | 46 | // Dial connects to the given address on the given network using net.Dial 47 | // and then returns a new Conn for the connection. 48 | func Dial(network, addr string) (*Conn, error) { 49 | c, err := net.Dial(network, addr) 50 | if err != nil { 51 | return nil, NewError(ErrorNetwork, err) 52 | } 53 | conn := NewConn(c) 54 | conn.start() 55 | return conn, nil 56 | } 57 | 58 | // DialTimeout connects to the given address on the given network using net.DialTimeout 59 | // and then returns a new Conn for the connection. Acts like Dial but takes a timeout. 60 | func DialTimeout(network, addr string, timeout time.Duration) (*Conn, error) { 61 | c, err := net.DialTimeout(network, addr, timeout) 62 | if err != nil { 63 | return nil, NewError(ErrorNetwork, err) 64 | } 65 | conn := NewConn(c) 66 | conn.start() 67 | return conn, nil 68 | } 69 | 70 | // DialTLS connects to the given address on the given network using tls.Dial 71 | // and then returns a new Conn for the connection. 72 | func DialTLS(network, addr string, config *tls.Config) (*Conn, error) { 73 | c, err := tls.Dial(network, addr, config) 74 | if err != nil { 75 | return nil, NewError(ErrorNetwork, err) 76 | } 77 | conn := NewConn(c) 78 | conn.isTLS = true 79 | conn.start() 80 | return conn, nil 81 | } 82 | 83 | // DialTLSDialer connects to the given address on the given network using tls.DialWithDialer 84 | // and then returns a new Conn for the connection. 85 | func DialTLSDialer(network, addr string, config *tls.Config, dialer *net.Dialer) (*Conn, error) { 86 | c, err := tls.DialWithDialer(dialer, network, addr, config) 87 | if err != nil { 88 | return nil, NewError(ErrorNetwork, err) 89 | } 90 | conn := NewConn(c) 91 | conn.isTLS = true 92 | conn.start() 93 | return conn, nil 94 | } 95 | 96 | // NewConn returns a new Conn using conn for network I/O. 97 | func NewConn(conn net.Conn) *Conn { 98 | return &Conn{ 99 | conn: conn, 100 | chanConfirm: make(chan bool), 101 | chanMessageID: make(chan uint64), 102 | chanMessage: make(chan *messagePacket, 10), 103 | chanResults: map[uint64]chan *ber.Packet{}, 104 | chanDone: make(chan struct{}), 105 | } 106 | } 107 | 108 | func (l *Conn) start() { 109 | go l.reader() 110 | go l.processMessages() 111 | } 112 | 113 | // Close closes the connection. 114 | func (l *Conn) Close() { 115 | l.once.Do(func() { 116 | close(l.chanDone) 117 | l.wgSender.Wait() 118 | 119 | l.Debug.Printf("Sending quit message and waiting for confirmation") 120 | l.chanMessage <- &messagePacket{Op: MessageQuit} 121 | <-l.chanConfirm 122 | close(l.chanMessage) 123 | 124 | l.Debug.Printf("Closing network connection") 125 | if err := l.conn.Close(); err != nil { 126 | log.Print(err) 127 | } 128 | }) 129 | <-l.chanDone 130 | } 131 | 132 | // Returns the next available messageID 133 | func (l *Conn) nextMessageID() uint64 { 134 | if l.chanMessageID != nil { 135 | if messageID, ok := <-l.chanMessageID; ok { 136 | return messageID 137 | } 138 | } 139 | return 0 140 | } 141 | 142 | // StartTLS sends the command to start a TLS session and then creates a new TLS Client 143 | func (l *Conn) StartTLS(config *tls.Config) error { 144 | messageID := l.nextMessageID() 145 | 146 | if l.isTLS { 147 | return NewError(ErrorNetwork, errors.New("ldap: already encrypted")) 148 | } 149 | 150 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 151 | packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "MessageID")) 152 | request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Start TLS") 153 | request.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, "1.3.6.1.4.1.1466.20037", "TLS Extended Command")) 154 | packet.AppendChild(request) 155 | l.Debug.PrintPacket(packet) 156 | 157 | _, err := l.conn.Write(packet.Bytes()) 158 | if err != nil { 159 | return NewError(ErrorNetwork, err) 160 | } 161 | 162 | packet, err = ber.ReadPacket(l.conn) 163 | if err != nil { 164 | return NewError(ErrorNetwork, err) 165 | } 166 | 167 | if l.Debug { 168 | if err := addLDAPDescriptions(packet); err != nil { 169 | return err 170 | } 171 | ber.PrintPacket(packet) 172 | } 173 | 174 | if packet.Children[1].Children[0].Value.(uint64) == 0 { 175 | conn := tls.Client(l.conn, config) 176 | l.isTLS = true 177 | l.conn = conn 178 | } 179 | 180 | return nil 181 | } 182 | 183 | func (l *Conn) closing() bool { 184 | select { 185 | case <-l.chanDone: 186 | return true 187 | default: 188 | return false 189 | } 190 | } 191 | 192 | func (l *Conn) sendMessage(packet *ber.Packet) (chan *ber.Packet, error) { 193 | if l.closing() { 194 | return nil, NewError(ErrorNetwork, errors.New("ldap: connection closed")) 195 | } 196 | out := make(chan *ber.Packet) 197 | message := &messagePacket{ 198 | Op: MessageRequest, 199 | MessageID: packet.Children[0].Value.(uint64), 200 | Packet: packet, 201 | Channel: out, 202 | } 203 | l.sendProcessMessage(message) 204 | return out, nil 205 | } 206 | 207 | func (l *Conn) finishMessage(messageID uint64) { 208 | if l.closing() { 209 | return 210 | } 211 | message := &messagePacket{ 212 | Op: MessageFinish, 213 | MessageID: messageID, 214 | } 215 | l.sendProcessMessage(message) 216 | } 217 | 218 | func (l *Conn) sendProcessMessage(message *messagePacket) bool { 219 | l.wgSender.Add(1) 220 | defer l.wgSender.Done() 221 | 222 | if l.closing() { 223 | return false 224 | } 225 | l.chanMessage <- message 226 | return true 227 | } 228 | 229 | func (l *Conn) processMessages() { 230 | defer func() { 231 | for messageID, channel := range l.chanResults { 232 | l.Debug.Printf("Closing channel for MessageID %d", messageID) 233 | close(channel) 234 | delete(l.chanResults, messageID) 235 | } 236 | close(l.chanMessageID) 237 | l.chanConfirm <- true 238 | close(l.chanConfirm) 239 | }() 240 | 241 | var messageID uint64 = 1 242 | for { 243 | select { 244 | case l.chanMessageID <- messageID: 245 | messageID++ 246 | case messagePacket, ok := <-l.chanMessage: 247 | if !ok { 248 | l.Debug.Printf("Shutting down - message channel is closed") 249 | return 250 | } 251 | switch messagePacket.Op { 252 | case MessageQuit: 253 | l.Debug.Printf("Shutting down - quit message received") 254 | return 255 | case MessageRequest: 256 | // Add to message list and write to network 257 | l.Debug.Printf("Sending message %d", messagePacket.MessageID) 258 | l.chanResults[messagePacket.MessageID] = messagePacket.Channel 259 | // go routine 260 | buf := messagePacket.Packet.Bytes() 261 | 262 | _, err := l.conn.Write(buf) 263 | if err != nil { 264 | l.Debug.Printf("Error Sending Message: %s", err.Error()) 265 | break 266 | } 267 | case MessageResponse: 268 | l.Debug.Printf("Receiving message %d", messagePacket.MessageID) 269 | if chanResult, ok := l.chanResults[messagePacket.MessageID]; ok { 270 | chanResult <- messagePacket.Packet 271 | } else { 272 | log.Printf("Received unexpected message %d", messagePacket.MessageID) 273 | ber.PrintPacket(messagePacket.Packet) 274 | } 275 | case MessageFinish: 276 | // Remove from message list 277 | l.Debug.Printf("Finished message %d", messagePacket.MessageID) 278 | close(l.chanResults[messagePacket.MessageID]) 279 | delete(l.chanResults, messagePacket.MessageID) 280 | } 281 | } 282 | } 283 | } 284 | 285 | func (l *Conn) reader() { 286 | defer func() { 287 | l.Close() 288 | }() 289 | 290 | for { 291 | packet, err := ber.ReadPacket(l.conn) 292 | if err != nil { 293 | l.Debug.Printf("reader: %s", err.Error()) 294 | return 295 | } 296 | addLDAPDescriptions(packet) 297 | message := &messagePacket{ 298 | Op: MessageResponse, 299 | MessageID: packet.Children[0].Value.(uint64), 300 | Packet: packet, 301 | } 302 | if !l.sendProcessMessage(message) { 303 | return 304 | } 305 | 306 | } 307 | } 308 | 309 | // Use Abandon operation to perform connection keepalives 310 | func (l *Conn) Ping() error { 311 | 312 | messageID := l.nextMessageID() 313 | 314 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 315 | packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "MessageID")) 316 | abandonRequest := ber.Encode(ber.ClassApplication, ber.TypePrimitive, ApplicationAbandonRequest, nil, "Abandon Request") 317 | packet.AppendChild(abandonRequest) 318 | 319 | if l.Debug { 320 | ber.PrintPacket(packet) 321 | } 322 | 323 | channel, err := l.sendMessage(packet) 324 | if err != nil { 325 | return err 326 | } 327 | if channel == nil { 328 | return NewError(ErrorNetwork, errors.New("ldap: could not send message")) 329 | } 330 | defer l.finishMessage(messageID) 331 | 332 | if l.Debug { 333 | if err := addLDAPDescriptions(packet); err != nil { 334 | return err 335 | } 336 | ber.PrintPacket(packet) 337 | } 338 | 339 | return nil 340 | } 341 | -------------------------------------------------------------------------------- /search.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | // 5 | // File contains Search functionality 6 | // 7 | // https://tools.ietf.org/html/rfc4511 8 | // 9 | // SearchRequest ::= [APPLICATION 3] SEQUENCE { 10 | // baseObject LDAPDN, 11 | // scope ENUMERATED { 12 | // baseObject (0), 13 | // singleLevel (1), 14 | // wholeSubtree (2), 15 | // ... }, 16 | // derefAliases ENUMERATED { 17 | // neverDerefAliases (0), 18 | // derefInSearching (1), 19 | // derefFindingBaseObj (2), 20 | // derefAlways (3) }, 21 | // sizeLimit INTEGER (0 .. maxInt), 22 | // timeLimit INTEGER (0 .. maxInt), 23 | // typesOnly BOOLEAN, 24 | // filter Filter, 25 | // attributes AttributeSelection } 26 | // 27 | // AttributeSelection ::= SEQUENCE OF selector LDAPString 28 | // -- The LDAPString is constrained to 29 | // -- in Section 4.5.1.8 30 | // 31 | // Filter ::= CHOICE { 32 | // and [0] SET SIZE (1..MAX) OF filter Filter, 33 | // or [1] SET SIZE (1..MAX) OF filter Filter, 34 | // not [2] Filter, 35 | // equalityMatch [3] AttributeValueAssertion, 36 | // substrings [4] SubstringFilter, 37 | // greaterOrEqual [5] AttributeValueAssertion, 38 | // lessOrEqual [6] AttributeValueAssertion, 39 | // present [7] AttributeDescription, 40 | // approxMatch [8] AttributeValueAssertion, 41 | // extensibleMatch [9] MatchingRuleAssertion, 42 | // ... } 43 | // 44 | // SubstringFilter ::= SEQUENCE { 45 | // type AttributeDescription, 46 | // substrings SEQUENCE SIZE (1..MAX) OF substring CHOICE { 47 | // initial [0] AssertionValue, -- can occur at most once 48 | // any [1] AssertionValue, 49 | // final [2] AssertionValue } -- can occur at most once 50 | // } 51 | // 52 | // MatchingRuleAssertion ::= SEQUENCE { 53 | // matchingRule [1] MatchingRuleId OPTIONAL, 54 | // type [2] AttributeDescription OPTIONAL, 55 | // matchValue [3] AssertionValue, 56 | // dnAttributes [4] BOOLEAN DEFAULT FALSE } 57 | // 58 | // 59 | 60 | package ldap 61 | 62 | import ( 63 | "errors" 64 | "fmt" 65 | "strings" 66 | 67 | "github.com/nmcclain/asn1-ber" 68 | ) 69 | 70 | const ( 71 | ScopeBaseObject = 0 72 | ScopeSingleLevel = 1 73 | ScopeWholeSubtree = 2 74 | ) 75 | 76 | var ScopeMap = map[int]string{ 77 | ScopeBaseObject: "Base Object", 78 | ScopeSingleLevel: "Single Level", 79 | ScopeWholeSubtree: "Whole Subtree", 80 | } 81 | 82 | const ( 83 | NeverDerefAliases = 0 84 | DerefInSearching = 1 85 | DerefFindingBaseObj = 2 86 | DerefAlways = 3 87 | ) 88 | 89 | var DerefMap = map[int]string{ 90 | NeverDerefAliases: "NeverDerefAliases", 91 | DerefInSearching: "DerefInSearching", 92 | DerefFindingBaseObj: "DerefFindingBaseObj", 93 | DerefAlways: "DerefAlways", 94 | } 95 | 96 | type Entry struct { 97 | DN string 98 | Attributes []*EntryAttribute 99 | } 100 | 101 | func (e *Entry) GetAttributeValues(attribute string) []string { 102 | for _, attr := range e.Attributes { 103 | if attr.Name == attribute { 104 | return attr.Values 105 | } 106 | } 107 | return []string{} 108 | } 109 | 110 | func (e *Entry) GetAttributeValue(attribute string) string { 111 | values := e.GetAttributeValues(attribute) 112 | if len(values) == 0 { 113 | return "" 114 | } 115 | return values[0] 116 | } 117 | 118 | func (e *Entry) Print() { 119 | fmt.Printf("DN: %s\n", e.DN) 120 | for _, attr := range e.Attributes { 121 | attr.Print() 122 | } 123 | } 124 | 125 | func (e *Entry) PrettyPrint(indent int) { 126 | fmt.Printf("%sDN: %s\n", strings.Repeat(" ", indent), e.DN) 127 | for _, attr := range e.Attributes { 128 | attr.PrettyPrint(indent + 2) 129 | } 130 | } 131 | 132 | type EntryAttribute struct { 133 | Name string 134 | Values []string 135 | } 136 | 137 | func (e *EntryAttribute) Print() { 138 | fmt.Printf("%s: %s\n", e.Name, e.Values) 139 | } 140 | 141 | func (e *EntryAttribute) PrettyPrint(indent int) { 142 | fmt.Printf("%s%s: %s\n", strings.Repeat(" ", indent), e.Name, e.Values) 143 | } 144 | 145 | type SearchResult struct { 146 | Entries []*Entry 147 | Referrals []string 148 | Controls []Control 149 | } 150 | 151 | func (s *SearchResult) Print() { 152 | for _, entry := range s.Entries { 153 | entry.Print() 154 | } 155 | } 156 | 157 | func (s *SearchResult) PrettyPrint(indent int) { 158 | for _, entry := range s.Entries { 159 | entry.PrettyPrint(indent) 160 | } 161 | } 162 | 163 | type SearchRequest struct { 164 | BaseDN string 165 | Scope int 166 | DerefAliases int 167 | SizeLimit int 168 | TimeLimit int 169 | TypesOnly bool 170 | Filter string 171 | Attributes []string 172 | Controls []Control 173 | } 174 | 175 | func (s *SearchRequest) encode() (*ber.Packet, error) { 176 | request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationSearchRequest, nil, "Search Request") 177 | request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, s.BaseDN, "Base DN")) 178 | request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(s.Scope), "Scope")) 179 | request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(s.DerefAliases), "Deref Aliases")) 180 | request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, uint64(s.SizeLimit), "Size Limit")) 181 | request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, uint64(s.TimeLimit), "Time Limit")) 182 | request.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, s.TypesOnly, "Types Only")) 183 | // compile and encode filter 184 | filterPacket, err := CompileFilter(s.Filter) 185 | if err != nil { 186 | return nil, err 187 | } 188 | request.AppendChild(filterPacket) 189 | // encode attributes 190 | attributesPacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes") 191 | for _, attribute := range s.Attributes { 192 | attributesPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute")) 193 | } 194 | request.AppendChild(attributesPacket) 195 | return request, nil 196 | } 197 | 198 | func NewSearchRequest( 199 | BaseDN string, 200 | Scope, DerefAliases, SizeLimit, TimeLimit int, 201 | TypesOnly bool, 202 | Filter string, 203 | Attributes []string, 204 | Controls []Control, 205 | ) *SearchRequest { 206 | return &SearchRequest{ 207 | BaseDN: BaseDN, 208 | Scope: Scope, 209 | DerefAliases: DerefAliases, 210 | SizeLimit: SizeLimit, 211 | TimeLimit: TimeLimit, 212 | TypesOnly: TypesOnly, 213 | Filter: Filter, 214 | Attributes: Attributes, 215 | Controls: Controls, 216 | } 217 | } 218 | 219 | func (l *Conn) SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) { 220 | if searchRequest.Controls == nil { 221 | searchRequest.Controls = make([]Control, 0) 222 | } 223 | 224 | pagingControl := NewControlPaging(pagingSize) 225 | searchRequest.Controls = append(searchRequest.Controls, pagingControl) 226 | searchResult := new(SearchResult) 227 | for { 228 | result, err := l.Search(searchRequest) 229 | l.Debug.Printf("Looking for Paging Control...") 230 | if err != nil { 231 | return searchResult, err 232 | } 233 | if result == nil { 234 | return searchResult, NewError(ErrorNetwork, errors.New("ldap: packet not received")) 235 | } 236 | 237 | for _, entry := range result.Entries { 238 | searchResult.Entries = append(searchResult.Entries, entry) 239 | } 240 | for _, referral := range result.Referrals { 241 | searchResult.Referrals = append(searchResult.Referrals, referral) 242 | } 243 | for _, control := range result.Controls { 244 | searchResult.Controls = append(searchResult.Controls, control) 245 | } 246 | 247 | l.Debug.Printf("Looking for Paging Control...") 248 | pagingResult := FindControl(result.Controls, ControlTypePaging) 249 | if pagingResult == nil { 250 | pagingControl = nil 251 | l.Debug.Printf("Could not find paging control. Breaking...") 252 | break 253 | } 254 | 255 | cookie := pagingResult.(*ControlPaging).Cookie 256 | if len(cookie) == 0 { 257 | pagingControl = nil 258 | l.Debug.Printf("Could not find cookie. Breaking...") 259 | break 260 | } 261 | pagingControl.SetCookie(cookie) 262 | } 263 | 264 | if pagingControl != nil { 265 | l.Debug.Printf("Abandoning Paging...") 266 | pagingControl.PagingSize = 0 267 | l.Search(searchRequest) 268 | } 269 | 270 | return searchResult, nil 271 | } 272 | 273 | func (l *Conn) Search(searchRequest *SearchRequest) (*SearchResult, error) { 274 | messageID := l.nextMessageID() 275 | packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") 276 | packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "MessageID")) 277 | // encode search request 278 | encodedSearchRequest, err := searchRequest.encode() 279 | if err != nil { 280 | return nil, err 281 | } 282 | packet.AppendChild(encodedSearchRequest) 283 | // encode search controls 284 | if searchRequest.Controls != nil { 285 | packet.AppendChild(encodeControls(searchRequest.Controls)) 286 | } 287 | 288 | l.Debug.PrintPacket(packet) 289 | 290 | channel, err := l.sendMessage(packet) 291 | if err != nil { 292 | return nil, err 293 | } 294 | if channel == nil { 295 | return nil, NewError(ErrorNetwork, errors.New("ldap: could not send message")) 296 | } 297 | defer l.finishMessage(messageID) 298 | 299 | result := &SearchResult{ 300 | Entries: make([]*Entry, 0), 301 | Referrals: make([]string, 0), 302 | Controls: make([]Control, 0)} 303 | 304 | foundSearchResultDone := false 305 | for !foundSearchResultDone { 306 | l.Debug.Printf("%d: waiting for response", messageID) 307 | packet = <-channel 308 | l.Debug.Printf("%d: got response %p", messageID, packet) 309 | if packet == nil { 310 | return nil, NewError(ErrorNetwork, errors.New("ldap: could not retrieve message")) 311 | } 312 | 313 | if l.Debug { 314 | if err := addLDAPDescriptions(packet); err != nil { 315 | return nil, err 316 | } 317 | ber.PrintPacket(packet) 318 | } 319 | 320 | switch packet.Children[1].Tag { 321 | case 4: 322 | entry := new(Entry) 323 | entry.DN = packet.Children[1].Children[0].Value.(string) 324 | for _, child := range packet.Children[1].Children[1].Children { 325 | attr := new(EntryAttribute) 326 | attr.Name = child.Children[0].Value.(string) 327 | for _, value := range child.Children[1].Children { 328 | attr.Values = append(attr.Values, value.Value.(string)) 329 | } 330 | entry.Attributes = append(entry.Attributes, attr) 331 | } 332 | result.Entries = append(result.Entries, entry) 333 | case 5: 334 | resultCode, resultDescription := getLDAPResultCode(packet) 335 | if resultCode != 0 { 336 | return result, NewError(resultCode, errors.New(resultDescription)) 337 | } 338 | if len(packet.Children) == 3 { 339 | for _, child := range packet.Children[2].Children { 340 | result.Controls = append(result.Controls, DecodeControl(child)) 341 | } 342 | } 343 | foundSearchResultDone = true 344 | case 19: 345 | result.Referrals = append(result.Referrals, packet.Children[1].Children[0].Value.(string)) 346 | } 347 | } 348 | l.Debug.Printf("%d: returning", messageID) 349 | return result, nil 350 | } 351 | -------------------------------------------------------------------------------- /ldap.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ldap 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "io/ioutil" 11 | 12 | "github.com/nmcclain/asn1-ber" 13 | ) 14 | 15 | // LDAP Application Codes 16 | const ( 17 | ApplicationBindRequest = 0 18 | ApplicationBindResponse = 1 19 | ApplicationUnbindRequest = 2 20 | ApplicationSearchRequest = 3 21 | ApplicationSearchResultEntry = 4 22 | ApplicationSearchResultDone = 5 23 | ApplicationModifyRequest = 6 24 | ApplicationModifyResponse = 7 25 | ApplicationAddRequest = 8 26 | ApplicationAddResponse = 9 27 | ApplicationDelRequest = 10 28 | ApplicationDelResponse = 11 29 | ApplicationModifyDNRequest = 12 30 | ApplicationModifyDNResponse = 13 31 | ApplicationCompareRequest = 14 32 | ApplicationCompareResponse = 15 33 | ApplicationAbandonRequest = 16 34 | ApplicationSearchResultReference = 19 35 | ApplicationExtendedRequest = 23 36 | ApplicationExtendedResponse = 24 37 | ) 38 | 39 | var ApplicationMap = map[uint8]string{ 40 | ApplicationBindRequest: "Bind Request", 41 | ApplicationBindResponse: "Bind Response", 42 | ApplicationUnbindRequest: "Unbind Request", 43 | ApplicationSearchRequest: "Search Request", 44 | ApplicationSearchResultEntry: "Search Result Entry", 45 | ApplicationSearchResultDone: "Search Result Done", 46 | ApplicationModifyRequest: "Modify Request", 47 | ApplicationModifyResponse: "Modify Response", 48 | ApplicationAddRequest: "Add Request", 49 | ApplicationAddResponse: "Add Response", 50 | ApplicationDelRequest: "Del Request", 51 | ApplicationDelResponse: "Del Response", 52 | ApplicationModifyDNRequest: "Modify DN Request", 53 | ApplicationModifyDNResponse: "Modify DN Response", 54 | ApplicationCompareRequest: "Compare Request", 55 | ApplicationCompareResponse: "Compare Response", 56 | ApplicationAbandonRequest: "Abandon Request", 57 | ApplicationSearchResultReference: "Search Result Reference", 58 | ApplicationExtendedRequest: "Extended Request", 59 | ApplicationExtendedResponse: "Extended Response", 60 | } 61 | 62 | // LDAP Result Codes 63 | const ( 64 | LDAPResultSuccess = 0 65 | LDAPResultOperationsError = 1 66 | LDAPResultProtocolError = 2 67 | LDAPResultTimeLimitExceeded = 3 68 | LDAPResultSizeLimitExceeded = 4 69 | LDAPResultCompareFalse = 5 70 | LDAPResultCompareTrue = 6 71 | LDAPResultAuthMethodNotSupported = 7 72 | LDAPResultStrongAuthRequired = 8 73 | LDAPResultReferral = 10 74 | LDAPResultAdminLimitExceeded = 11 75 | LDAPResultUnavailableCriticalExtension = 12 76 | LDAPResultConfidentialityRequired = 13 77 | LDAPResultSaslBindInProgress = 14 78 | LDAPResultNoSuchAttribute = 16 79 | LDAPResultUndefinedAttributeType = 17 80 | LDAPResultInappropriateMatching = 18 81 | LDAPResultConstraintViolation = 19 82 | LDAPResultAttributeOrValueExists = 20 83 | LDAPResultInvalidAttributeSyntax = 21 84 | LDAPResultNoSuchObject = 32 85 | LDAPResultAliasProblem = 33 86 | LDAPResultInvalidDNSyntax = 34 87 | LDAPResultAliasDereferencingProblem = 36 88 | LDAPResultInappropriateAuthentication = 48 89 | LDAPResultInvalidCredentials = 49 90 | LDAPResultInsufficientAccessRights = 50 91 | LDAPResultBusy = 51 92 | LDAPResultUnavailable = 52 93 | LDAPResultUnwillingToPerform = 53 94 | LDAPResultLoopDetect = 54 95 | LDAPResultNamingViolation = 64 96 | LDAPResultObjectClassViolation = 65 97 | LDAPResultNotAllowedOnNonLeaf = 66 98 | LDAPResultNotAllowedOnRDN = 67 99 | LDAPResultEntryAlreadyExists = 68 100 | LDAPResultObjectClassModsProhibited = 69 101 | LDAPResultAffectsMultipleDSAs = 71 102 | LDAPResultOther = 80 103 | 104 | ErrorNetwork = 200 105 | ErrorFilterCompile = 201 106 | ErrorFilterDecompile = 202 107 | ErrorDebugging = 203 108 | ) 109 | 110 | var LDAPResultCodeMap = map[LDAPResultCode]string{ 111 | LDAPResultSuccess: "Success", 112 | LDAPResultOperationsError: "Operations Error", 113 | LDAPResultProtocolError: "Protocol Error", 114 | LDAPResultTimeLimitExceeded: "Time Limit Exceeded", 115 | LDAPResultSizeLimitExceeded: "Size Limit Exceeded", 116 | LDAPResultCompareFalse: "Compare False", 117 | LDAPResultCompareTrue: "Compare True", 118 | LDAPResultAuthMethodNotSupported: "Auth Method Not Supported", 119 | LDAPResultStrongAuthRequired: "Strong Auth Required", 120 | LDAPResultReferral: "Referral", 121 | LDAPResultAdminLimitExceeded: "Admin Limit Exceeded", 122 | LDAPResultUnavailableCriticalExtension: "Unavailable Critical Extension", 123 | LDAPResultConfidentialityRequired: "Confidentiality Required", 124 | LDAPResultSaslBindInProgress: "Sasl Bind In Progress", 125 | LDAPResultNoSuchAttribute: "No Such Attribute", 126 | LDAPResultUndefinedAttributeType: "Undefined Attribute Type", 127 | LDAPResultInappropriateMatching: "Inappropriate Matching", 128 | LDAPResultConstraintViolation: "Constraint Violation", 129 | LDAPResultAttributeOrValueExists: "Attribute Or Value Exists", 130 | LDAPResultInvalidAttributeSyntax: "Invalid Attribute Syntax", 131 | LDAPResultNoSuchObject: "No Such Object", 132 | LDAPResultAliasProblem: "Alias Problem", 133 | LDAPResultInvalidDNSyntax: "Invalid DN Syntax", 134 | LDAPResultAliasDereferencingProblem: "Alias Dereferencing Problem", 135 | LDAPResultInappropriateAuthentication: "Inappropriate Authentication", 136 | LDAPResultInvalidCredentials: "Invalid Credentials", 137 | LDAPResultInsufficientAccessRights: "Insufficient Access Rights", 138 | LDAPResultBusy: "Busy", 139 | LDAPResultUnavailable: "Unavailable", 140 | LDAPResultUnwillingToPerform: "Unwilling To Perform", 141 | LDAPResultLoopDetect: "Loop Detect", 142 | LDAPResultNamingViolation: "Naming Violation", 143 | LDAPResultObjectClassViolation: "Object Class Violation", 144 | LDAPResultNotAllowedOnNonLeaf: "Not Allowed On Non Leaf", 145 | LDAPResultNotAllowedOnRDN: "Not Allowed On RDN", 146 | LDAPResultEntryAlreadyExists: "Entry Already Exists", 147 | LDAPResultObjectClassModsProhibited: "Object Class Mods Prohibited", 148 | LDAPResultAffectsMultipleDSAs: "Affects Multiple DSAs", 149 | LDAPResultOther: "Other", 150 | } 151 | 152 | // Other LDAP constants 153 | const ( 154 | LDAPBindAuthSimple = 0 155 | LDAPBindAuthSASL = 3 156 | ) 157 | 158 | type LDAPResultCode uint8 159 | 160 | type Attribute struct { 161 | attrType string 162 | attrVals []string 163 | } 164 | type AddRequest struct { 165 | dn string 166 | attributes []Attribute 167 | } 168 | type DeleteRequest struct { 169 | dn string 170 | } 171 | type ModifyDNRequest struct { 172 | dn string 173 | newrdn string 174 | deleteoldrdn bool 175 | newSuperior string 176 | } 177 | type AttributeValueAssertion struct { 178 | attributeDesc string 179 | assertionValue string 180 | } 181 | type CompareRequest struct { 182 | dn string 183 | ava []AttributeValueAssertion 184 | } 185 | type ExtendedRequest struct { 186 | requestName string 187 | requestValue string 188 | } 189 | 190 | // Adds descriptions to an LDAP Response packet for debugging 191 | func addLDAPDescriptions(packet *ber.Packet) (err error) { 192 | defer func() { 193 | if r := recover(); r != nil { 194 | err = NewError(ErrorDebugging, errors.New("ldap: cannot process packet to add descriptions")) 195 | } 196 | }() 197 | packet.Description = "LDAP Response" 198 | packet.Children[0].Description = "Message ID" 199 | 200 | application := packet.Children[1].Tag 201 | packet.Children[1].Description = ApplicationMap[application] 202 | 203 | switch application { 204 | case ApplicationBindRequest: 205 | addRequestDescriptions(packet) 206 | case ApplicationBindResponse: 207 | addDefaultLDAPResponseDescriptions(packet) 208 | case ApplicationUnbindRequest: 209 | addRequestDescriptions(packet) 210 | case ApplicationSearchRequest: 211 | addRequestDescriptions(packet) 212 | case ApplicationSearchResultEntry: 213 | packet.Children[1].Children[0].Description = "Object Name" 214 | packet.Children[1].Children[1].Description = "Attributes" 215 | for _, child := range packet.Children[1].Children[1].Children { 216 | child.Description = "Attribute" 217 | child.Children[0].Description = "Attribute Name" 218 | child.Children[1].Description = "Attribute Values" 219 | for _, grandchild := range child.Children[1].Children { 220 | grandchild.Description = "Attribute Value" 221 | } 222 | } 223 | if len(packet.Children) == 3 { 224 | addControlDescriptions(packet.Children[2]) 225 | } 226 | case ApplicationSearchResultDone: 227 | addDefaultLDAPResponseDescriptions(packet) 228 | case ApplicationModifyRequest: 229 | addRequestDescriptions(packet) 230 | case ApplicationModifyResponse: 231 | case ApplicationAddRequest: 232 | addRequestDescriptions(packet) 233 | case ApplicationAddResponse: 234 | case ApplicationDelRequest: 235 | addRequestDescriptions(packet) 236 | case ApplicationDelResponse: 237 | case ApplicationModifyDNRequest: 238 | addRequestDescriptions(packet) 239 | case ApplicationModifyDNResponse: 240 | case ApplicationCompareRequest: 241 | addRequestDescriptions(packet) 242 | case ApplicationCompareResponse: 243 | case ApplicationAbandonRequest: 244 | addRequestDescriptions(packet) 245 | case ApplicationSearchResultReference: 246 | case ApplicationExtendedRequest: 247 | addRequestDescriptions(packet) 248 | case ApplicationExtendedResponse: 249 | } 250 | 251 | return nil 252 | } 253 | 254 | func addControlDescriptions(packet *ber.Packet) { 255 | packet.Description = "Controls" 256 | for _, child := range packet.Children { 257 | child.Description = "Control" 258 | child.Children[0].Description = "Control Type (" + ControlTypeMap[child.Children[0].Value.(string)] + ")" 259 | value := child.Children[1] 260 | if len(child.Children) == 3 { 261 | child.Children[1].Description = "Criticality" 262 | value = child.Children[2] 263 | } 264 | value.Description = "Control Value" 265 | 266 | switch child.Children[0].Value.(string) { 267 | case ControlTypePaging: 268 | value.Description += " (Paging)" 269 | if value.Value != nil { 270 | valueChildren := ber.DecodePacket(value.Data.Bytes()) 271 | value.Data.Truncate(0) 272 | value.Value = nil 273 | valueChildren.Children[1].Value = valueChildren.Children[1].Data.Bytes() 274 | value.AppendChild(valueChildren) 275 | } 276 | value.Children[0].Description = "Real Search Control Value" 277 | value.Children[0].Children[0].Description = "Paging Size" 278 | value.Children[0].Children[1].Description = "Cookie" 279 | } 280 | } 281 | } 282 | 283 | func addRequestDescriptions(packet *ber.Packet) { 284 | packet.Description = "LDAP Request" 285 | packet.Children[0].Description = "Message ID" 286 | packet.Children[1].Description = ApplicationMap[packet.Children[1].Tag] 287 | if len(packet.Children) == 3 { 288 | addControlDescriptions(packet.Children[2]) 289 | } 290 | } 291 | 292 | func addDefaultLDAPResponseDescriptions(packet *ber.Packet) { 293 | resultCode := packet.Children[1].Children[0].Value.(uint64) 294 | packet.Children[1].Children[0].Description = "Result Code (" + LDAPResultCodeMap[LDAPResultCode(resultCode)] + ")" 295 | packet.Children[1].Children[1].Description = "Matched DN" 296 | packet.Children[1].Children[2].Description = "Error Message" 297 | if len(packet.Children[1].Children) > 3 { 298 | packet.Children[1].Children[3].Description = "Referral" 299 | } 300 | if len(packet.Children) == 3 { 301 | addControlDescriptions(packet.Children[2]) 302 | } 303 | } 304 | 305 | func DebugBinaryFile(fileName string) error { 306 | file, err := ioutil.ReadFile(fileName) 307 | if err != nil { 308 | return NewError(ErrorDebugging, err) 309 | } 310 | ber.PrintBytes(file, "") 311 | packet := ber.DecodePacket(file) 312 | addLDAPDescriptions(packet) 313 | ber.PrintPacket(packet) 314 | 315 | return nil 316 | } 317 | 318 | type Error struct { 319 | Err error 320 | ResultCode LDAPResultCode 321 | } 322 | 323 | func (e *Error) Error() string { 324 | return fmt.Sprintf("LDAP Result Code %d %q: %s", e.ResultCode, LDAPResultCodeMap[e.ResultCode], e.Err.Error()) 325 | } 326 | 327 | func NewError(resultCode LDAPResultCode, err error) error { 328 | return &Error{ResultCode: resultCode, Err: err} 329 | } 330 | 331 | func getLDAPResultCode(packet *ber.Packet) (code LDAPResultCode, description string) { 332 | if len(packet.Children) >= 2 { 333 | response := packet.Children[1] 334 | if response.ClassType == ber.ClassApplication && response.TagType == ber.TypeConstructed && len(response.Children) == 3 { 335 | return LDAPResultCode(response.Children[0].Value.(uint64)), response.Children[2].Value.(string) 336 | } 337 | } 338 | 339 | return ErrorNetwork, "Invalid packet format" 340 | } 341 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ldap 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "strings" 11 | "unicode/utf8" 12 | 13 | ber "github.com/nmcclain/asn1-ber" 14 | ) 15 | 16 | const ( 17 | FilterAnd = 0 18 | FilterOr = 1 19 | FilterNot = 2 20 | FilterEqualityMatch = 3 21 | FilterSubstrings = 4 22 | FilterGreaterOrEqual = 5 23 | FilterLessOrEqual = 6 24 | FilterPresent = 7 25 | FilterApproxMatch = 8 26 | FilterExtensibleMatch = 9 27 | ) 28 | 29 | var FilterMap = map[uint8]string{ 30 | FilterAnd: "And", 31 | FilterOr: "Or", 32 | FilterNot: "Not", 33 | FilterEqualityMatch: "Equality Match", 34 | FilterSubstrings: "Substrings", 35 | FilterGreaterOrEqual: "Greater Or Equal", 36 | FilterLessOrEqual: "Less Or Equal", 37 | FilterPresent: "Present", 38 | FilterApproxMatch: "Approx Match", 39 | FilterExtensibleMatch: "Extensible Match", 40 | } 41 | 42 | const ( 43 | FilterSubstringsInitial = 0 44 | FilterSubstringsAny = 1 45 | FilterSubstringsFinal = 2 46 | ) 47 | 48 | func CompileFilter(filter string) (*ber.Packet, error) { 49 | if len(filter) == 0 || filter[0] != '(' { 50 | return nil, NewError(ErrorFilterCompile, errors.New("ldap: filter does not start with an '('")) 51 | } 52 | packet, pos, err := compileFilter(filter, 1) 53 | if err != nil { 54 | return nil, err 55 | } 56 | if pos != len(filter) { 57 | return nil, NewError(ErrorFilterCompile, errors.New("ldap: finished compiling filter with extra at end: "+fmt.Sprint(filter[pos:]))) 58 | } 59 | return packet, nil 60 | } 61 | 62 | func DecompileFilter(packet *ber.Packet) (ret string, err error) { 63 | defer func() { 64 | if r := recover(); r != nil { 65 | err = NewError(ErrorFilterDecompile, errors.New("ldap: error decompiling filter")) 66 | } 67 | }() 68 | ret = "(" 69 | err = nil 70 | childStr := "" 71 | 72 | switch packet.Tag { 73 | case FilterAnd: 74 | ret += "&" 75 | for _, child := range packet.Children { 76 | childStr, err = DecompileFilter(child) 77 | if err != nil { 78 | return 79 | } 80 | ret += childStr 81 | } 82 | case FilterOr: 83 | ret += "|" 84 | for _, child := range packet.Children { 85 | childStr, err = DecompileFilter(child) 86 | if err != nil { 87 | return 88 | } 89 | ret += childStr 90 | } 91 | case FilterNot: 92 | ret += "!" 93 | childStr, err = DecompileFilter(packet.Children[0]) 94 | if err != nil { 95 | return 96 | } 97 | ret += childStr 98 | 99 | case FilterSubstrings: 100 | ret += ber.DecodeString(packet.Children[0].Data.Bytes()) 101 | ret += "=" 102 | switch packet.Children[1].Children[0].Tag { 103 | case FilterSubstringsInitial: 104 | ret += ber.DecodeString(packet.Children[1].Children[0].Data.Bytes()) + "*" 105 | case FilterSubstringsAny: 106 | ret += "*" + ber.DecodeString(packet.Children[1].Children[0].Data.Bytes()) + "*" 107 | case FilterSubstringsFinal: 108 | ret += "*" + ber.DecodeString(packet.Children[1].Children[0].Data.Bytes()) 109 | } 110 | case FilterEqualityMatch: 111 | ret += ber.DecodeString(packet.Children[0].Data.Bytes()) 112 | ret += "=" 113 | ret += ber.DecodeString(packet.Children[1].Data.Bytes()) 114 | case FilterGreaterOrEqual: 115 | ret += ber.DecodeString(packet.Children[0].Data.Bytes()) 116 | ret += ">=" 117 | ret += ber.DecodeString(packet.Children[1].Data.Bytes()) 118 | case FilterLessOrEqual: 119 | ret += ber.DecodeString(packet.Children[0].Data.Bytes()) 120 | ret += "<=" 121 | ret += ber.DecodeString(packet.Children[1].Data.Bytes()) 122 | case FilterPresent: 123 | ret += ber.DecodeString(packet.Data.Bytes()) 124 | ret += "=*" 125 | case FilterApproxMatch: 126 | ret += ber.DecodeString(packet.Children[0].Data.Bytes()) 127 | ret += "~=" 128 | ret += ber.DecodeString(packet.Children[1].Data.Bytes()) 129 | } 130 | 131 | ret += ")" 132 | return 133 | } 134 | 135 | func compileFilterSet(filter string, pos int, parent *ber.Packet) (int, error) { 136 | for pos < len(filter) && filter[pos] == '(' { 137 | child, newPos, err := compileFilter(filter, pos+1) 138 | if err != nil { 139 | return pos, err 140 | } 141 | pos = newPos 142 | parent.AppendChild(child) 143 | } 144 | if pos == len(filter) { 145 | return pos, NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter")) 146 | } 147 | 148 | return pos + 1, nil 149 | } 150 | 151 | func compileFilter(filter string, pos int) (*ber.Packet, int, error) { 152 | var packet *ber.Packet 153 | var err error 154 | 155 | defer func() { 156 | if r := recover(); r != nil { 157 | err = NewError(ErrorFilterCompile, errors.New("ldap: error compiling filter")) 158 | } 159 | }() 160 | 161 | newPos := pos 162 | switch filter[pos] { 163 | case '(': 164 | packet, newPos, err = compileFilter(filter, pos+1) 165 | newPos++ 166 | return packet, newPos, err 167 | case '&': 168 | packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterAnd, nil, FilterMap[FilterAnd]) 169 | newPos, err = compileFilterSet(filter, pos+1, packet) 170 | return packet, newPos, err 171 | case '|': 172 | packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterOr, nil, FilterMap[FilterOr]) 173 | newPos, err = compileFilterSet(filter, pos+1, packet) 174 | return packet, newPos, err 175 | case '!': 176 | packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterNot, nil, FilterMap[FilterNot]) 177 | var child *ber.Packet 178 | child, newPos, err = compileFilter(filter, pos+1) 179 | packet.AppendChild(child) 180 | return packet, newPos, err 181 | default: 182 | attribute := "" 183 | condition := "" 184 | 185 | for w := 0; newPos < len(filter) && filter[newPos] != ')'; newPos += w { 186 | rune, width := utf8.DecodeRuneInString(filter[newPos:]) 187 | w = width 188 | switch { 189 | case packet != nil: 190 | condition += fmt.Sprintf("%c", rune) 191 | case filter[newPos] == '=': 192 | packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterEqualityMatch, nil, FilterMap[FilterEqualityMatch]) 193 | case filter[newPos] == '>' && filter[newPos+1] == '=': 194 | packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterGreaterOrEqual, nil, FilterMap[FilterGreaterOrEqual]) 195 | newPos++ 196 | case filter[newPos] == '<' && filter[newPos+1] == '=': 197 | packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterLessOrEqual, nil, FilterMap[FilterLessOrEqual]) 198 | newPos++ 199 | case filter[newPos] == '~' && filter[newPos+1] == '=': 200 | packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterApproxMatch, nil, FilterMap[FilterLessOrEqual]) 201 | newPos++ 202 | case packet == nil: 203 | attribute += fmt.Sprintf("%c", filter[newPos]) 204 | } 205 | } 206 | if newPos == len(filter) { 207 | err = NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter")) 208 | return packet, newPos, err 209 | } 210 | if packet == nil { 211 | err = NewError(ErrorFilterCompile, errors.New("ldap: error parsing filter")) 212 | return packet, newPos, err 213 | } 214 | // Handle FilterEqualityMatch as a separate case (is primitive, not constructed like the other filters) 215 | if packet.Tag == FilterEqualityMatch && condition == "*" { 216 | packet.TagType = ber.TypePrimitive 217 | packet.Tag = FilterPresent 218 | packet.Description = FilterMap[packet.Tag] 219 | packet.Data.WriteString(attribute) 220 | return packet, newPos + 1, nil 221 | } 222 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute")) 223 | switch { 224 | case packet.Tag == FilterEqualityMatch && condition[0] == '*' && condition[len(condition)-1] == '*': 225 | // Any 226 | packet.Tag = FilterSubstrings 227 | packet.Description = FilterMap[packet.Tag] 228 | seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Substrings") 229 | seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterSubstringsAny, condition[1:len(condition)-1], "Any Substring")) 230 | packet.AppendChild(seq) 231 | case packet.Tag == FilterEqualityMatch && condition[0] == '*': 232 | // Final 233 | packet.Tag = FilterSubstrings 234 | packet.Description = FilterMap[packet.Tag] 235 | seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Substrings") 236 | seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterSubstringsFinal, condition[1:], "Final Substring")) 237 | packet.AppendChild(seq) 238 | case packet.Tag == FilterEqualityMatch && condition[len(condition)-1] == '*': 239 | // Initial 240 | packet.Tag = FilterSubstrings 241 | packet.Description = FilterMap[packet.Tag] 242 | seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Substrings") 243 | seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterSubstringsInitial, condition[:len(condition)-1], "Initial Substring")) 244 | packet.AppendChild(seq) 245 | default: 246 | packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, condition, "Condition")) 247 | } 248 | newPos++ 249 | return packet, newPos, err 250 | } 251 | } 252 | 253 | func ServerApplyFilter(f *ber.Packet, entry *Entry) (bool, LDAPResultCode) { 254 | switch FilterMap[f.Tag] { 255 | default: 256 | //log.Fatalf("Unknown LDAP filter code: %d", f.Tag) 257 | return false, LDAPResultOperationsError 258 | case "Equality Match": 259 | if len(f.Children) != 2 { 260 | return false, LDAPResultOperationsError 261 | } 262 | attribute := f.Children[0].Value.(string) 263 | value := f.Children[1].Value.(string) 264 | for _, a := range entry.Attributes { 265 | if strings.ToLower(a.Name) == strings.ToLower(attribute) { 266 | for _, v := range a.Values { 267 | if strings.ToLower(v) == strings.ToLower(value) { 268 | return true, LDAPResultSuccess 269 | } 270 | } 271 | } 272 | } 273 | case "Present": 274 | for _, a := range entry.Attributes { 275 | if strings.ToLower(a.Name) == strings.ToLower(f.Data.String()) { 276 | return true, LDAPResultSuccess 277 | } 278 | } 279 | case "And": 280 | for _, child := range f.Children { 281 | ok, exitCode := ServerApplyFilter(child, entry) 282 | if exitCode != LDAPResultSuccess { 283 | return false, exitCode 284 | } 285 | if !ok { 286 | return false, LDAPResultSuccess 287 | } 288 | } 289 | return true, LDAPResultSuccess 290 | case "Or": 291 | anyOk := false 292 | for _, child := range f.Children { 293 | ok, exitCode := ServerApplyFilter(child, entry) 294 | if exitCode != LDAPResultSuccess { 295 | return false, exitCode 296 | } else if ok { 297 | anyOk = true 298 | } 299 | } 300 | if anyOk { 301 | return true, LDAPResultSuccess 302 | } 303 | case "Not": 304 | if len(f.Children) != 1 { 305 | return false, LDAPResultOperationsError 306 | } 307 | ok, exitCode := ServerApplyFilter(f.Children[0], entry) 308 | if exitCode != LDAPResultSuccess { 309 | return false, exitCode 310 | } else if !ok { 311 | return true, LDAPResultSuccess 312 | } 313 | case "Substrings": 314 | if len(f.Children) != 2 { 315 | return false, LDAPResultOperationsError 316 | } 317 | attribute := f.Children[0].Value.(string) 318 | valueBytes := f.Children[1].Children[0].Data.Bytes() 319 | valueLower := strings.ToLower(string(valueBytes[:])) 320 | for _, a := range entry.Attributes { 321 | if strings.ToLower(a.Name) == strings.ToLower(attribute) { 322 | for _, v := range a.Values { 323 | vLower := strings.ToLower(v) 324 | switch f.Children[1].Children[0].Tag { 325 | case FilterSubstringsInitial: 326 | if strings.HasPrefix(vLower, valueLower) { 327 | return true, LDAPResultSuccess 328 | } 329 | case FilterSubstringsAny: 330 | if strings.Contains(vLower, valueLower) { 331 | return true, LDAPResultSuccess 332 | } 333 | case FilterSubstringsFinal: 334 | if strings.HasSuffix(vLower, valueLower) { 335 | return true, LDAPResultSuccess 336 | } 337 | } 338 | } 339 | } 340 | } 341 | case "FilterGreaterOrEqual": // TODO 342 | return false, LDAPResultOperationsError 343 | case "FilterLessOrEqual": // TODO 344 | return false, LDAPResultOperationsError 345 | case "FilterApproxMatch": // TODO 346 | return false, LDAPResultOperationsError 347 | case "FilterExtensibleMatch": // TODO 348 | return false, LDAPResultOperationsError 349 | } 350 | 351 | return false, LDAPResultSuccess 352 | } 353 | 354 | func GetFilterObjectClass(filter string) (string, error) { 355 | f, err := CompileFilter(filter) 356 | if err != nil { 357 | return "", err 358 | } 359 | return parseFilterObjectClass(f) 360 | } 361 | func parseFilterObjectClass(f *ber.Packet) (string, error) { 362 | objectClass := "" 363 | switch FilterMap[f.Tag] { 364 | case "Equality Match": 365 | if len(f.Children) != 2 { 366 | return "", errors.New("Equality match must have only two children") 367 | } 368 | attribute := strings.ToLower(f.Children[0].Value.(string)) 369 | value := f.Children[1].Value.(string) 370 | if attribute == "objectclass" { 371 | objectClass = strings.ToLower(value) 372 | } 373 | case "And": 374 | for _, child := range f.Children { 375 | subType, err := parseFilterObjectClass(child) 376 | if err != nil { 377 | return "", err 378 | } 379 | if len(subType) > 0 { 380 | objectClass = subType 381 | } 382 | } 383 | case "Or": 384 | for _, child := range f.Children { 385 | subType, err := parseFilterObjectClass(child) 386 | if err != nil { 387 | return "", err 388 | } 389 | if len(subType) > 0 { 390 | objectClass = subType 391 | } 392 | } 393 | case "Not": 394 | if len(f.Children) != 1 { 395 | return "", errors.New("Not filter must have only one child") 396 | } 397 | subType, err := parseFilterObjectClass(f.Children[0]) 398 | if err != nil { 399 | return "", err 400 | } 401 | if len(subType) > 0 { 402 | objectClass = subType 403 | } 404 | 405 | } 406 | return strings.ToLower(objectClass), nil 407 | } 408 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "net" 7 | "os/exec" 8 | "strings" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | var listenString = "localhost:3389" 14 | var ldapURL = "ldap://" + listenString 15 | var timeout = 400 * time.Millisecond 16 | var serverBaseDN = "o=testers,c=test" 17 | 18 | ///////////////////////// 19 | func TestBindAnonOK(t *testing.T) { 20 | quit := make(chan bool) 21 | done := make(chan bool) 22 | go func() { 23 | s := NewServer() 24 | s.QuitChannel(quit) 25 | s.BindFunc("", bindAnonOK{}) 26 | if err := s.ListenAndServe(listenString); err != nil { 27 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 28 | } 29 | }() 30 | 31 | go func() { 32 | cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", "-b", "o=testers,c=test") 33 | out, _ := cmd.CombinedOutput() 34 | if !strings.Contains(string(out), "result: 0 Success") { 35 | t.Errorf("ldapsearch failed: %v", string(out)) 36 | } 37 | done <- true 38 | }() 39 | 40 | select { 41 | case <-done: 42 | case <-time.After(timeout): 43 | t.Errorf("ldapsearch command timed out") 44 | } 45 | quit <- true 46 | } 47 | 48 | ///////////////////////// 49 | func TestBindAnonFail(t *testing.T) { 50 | quit := make(chan bool) 51 | done := make(chan bool) 52 | go func() { 53 | s := NewServer() 54 | s.QuitChannel(quit) 55 | if err := s.ListenAndServe(listenString); err != nil { 56 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 57 | } 58 | }() 59 | 60 | time.Sleep(timeout) 61 | go func() { 62 | cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", "-b", "o=testers,c=test") 63 | out, _ := cmd.CombinedOutput() 64 | if !strings.Contains(string(out), "ldap_bind: Invalid credentials (49)") { 65 | t.Errorf("ldapsearch failed: %v", string(out)) 66 | } 67 | done <- true 68 | }() 69 | 70 | select { 71 | case <-done: 72 | case <-time.After(timeout): 73 | t.Errorf("ldapsearch command timed out") 74 | } 75 | time.Sleep(timeout) 76 | quit <- true 77 | } 78 | 79 | ///////////////////////// 80 | func TestBindSimpleOK(t *testing.T) { 81 | quit := make(chan bool) 82 | done := make(chan bool) 83 | go func() { 84 | s := NewServer() 85 | s.QuitChannel(quit) 86 | s.SearchFunc("", searchSimple{}) 87 | s.BindFunc("", bindSimple{}) 88 | if err := s.ListenAndServe(listenString); err != nil { 89 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 90 | } 91 | }() 92 | 93 | serverBaseDN := "o=testers,c=test" 94 | 95 | go func() { 96 | cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", 97 | "-b", serverBaseDN, "-D", "cn=testy,"+serverBaseDN, "-w", "iLike2test") 98 | out, _ := cmd.CombinedOutput() 99 | if !strings.Contains(string(out), "result: 0 Success") { 100 | t.Errorf("ldapsearch failed: %v", string(out)) 101 | } 102 | done <- true 103 | }() 104 | 105 | select { 106 | case <-done: 107 | case <-time.After(timeout): 108 | t.Errorf("ldapsearch command timed out") 109 | } 110 | quit <- true 111 | } 112 | 113 | ///////////////////////// 114 | func TestBindSimpleFailBadPw(t *testing.T) { 115 | quit := make(chan bool) 116 | done := make(chan bool) 117 | go func() { 118 | s := NewServer() 119 | s.QuitChannel(quit) 120 | s.BindFunc("", bindSimple{}) 121 | if err := s.ListenAndServe(listenString); err != nil { 122 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 123 | } 124 | }() 125 | 126 | serverBaseDN := "o=testers,c=test" 127 | 128 | go func() { 129 | cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", 130 | "-b", serverBaseDN, "-D", "cn=testy,"+serverBaseDN, "-w", "BADPassword") 131 | out, _ := cmd.CombinedOutput() 132 | if !strings.Contains(string(out), "ldap_bind: Invalid credentials (49)") { 133 | t.Errorf("ldapsearch succeeded - should have failed: %v", string(out)) 134 | } 135 | done <- true 136 | }() 137 | 138 | select { 139 | case <-done: 140 | case <-time.After(timeout): 141 | t.Errorf("ldapsearch command timed out") 142 | } 143 | quit <- true 144 | } 145 | 146 | ///////////////////////// 147 | func TestBindSimpleFailBadDn(t *testing.T) { 148 | quit := make(chan bool) 149 | done := make(chan bool) 150 | go func() { 151 | s := NewServer() 152 | s.QuitChannel(quit) 153 | s.BindFunc("", bindSimple{}) 154 | if err := s.ListenAndServe(listenString); err != nil { 155 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 156 | } 157 | }() 158 | 159 | serverBaseDN := "o=testers,c=test" 160 | 161 | go func() { 162 | cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", 163 | "-b", serverBaseDN, "-D", "cn=testoy,"+serverBaseDN, "-w", "iLike2test") 164 | out, _ := cmd.CombinedOutput() 165 | if string(out) != "ldap_bind: Invalid credentials (49)\n" { 166 | t.Errorf("ldapsearch succeeded - should have failed: %v", string(out)) 167 | } 168 | done <- true 169 | }() 170 | 171 | select { 172 | case <-done: 173 | case <-time.After(timeout): 174 | t.Errorf("ldapsearch command timed out") 175 | } 176 | quit <- true 177 | } 178 | 179 | ///////////////////////// 180 | func TestBindSSL(t *testing.T) { 181 | ldapURLSSL := "ldaps://" + listenString 182 | longerTimeout := 300 * time.Millisecond 183 | quit := make(chan bool) 184 | done := make(chan bool) 185 | go func() { 186 | s := NewServer() 187 | s.QuitChannel(quit) 188 | s.BindFunc("", bindAnonOK{}) 189 | if err := s.ListenAndServeTLS(listenString, "tests/cert_DONOTUSE.pem", "tests/key_DONOTUSE.pem"); err != nil { 190 | t.Errorf("s.ListenAndServeTLS failed: %s", err.Error()) 191 | } 192 | }() 193 | 194 | go func() { 195 | time.Sleep(longerTimeout * 2) 196 | cmd := exec.Command("ldapsearch", "-H", ldapURLSSL, "-x", "-b", "o=testers,c=test") 197 | out, _ := cmd.CombinedOutput() 198 | if !strings.Contains(string(out), "result: 0 Success") { 199 | t.Errorf("ldapsearch failed: %v", string(out)) 200 | } 201 | done <- true 202 | }() 203 | 204 | select { 205 | case <-done: 206 | case <-time.After(longerTimeout * 2): 207 | t.Errorf("ldapsearch command timed out") 208 | } 209 | quit <- true 210 | } 211 | 212 | ///////////////////////// 213 | func TestBindPanic(t *testing.T) { 214 | quit := make(chan bool) 215 | done := make(chan bool) 216 | go func() { 217 | s := NewServer() 218 | s.QuitChannel(quit) 219 | s.BindFunc("", bindPanic{}) 220 | if err := s.ListenAndServe(listenString); err != nil { 221 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 222 | } 223 | }() 224 | 225 | go func() { 226 | cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", "-b", "o=testers,c=test") 227 | out, _ := cmd.CombinedOutput() 228 | if !strings.Contains(string(out), "ldap_bind: Operations error") { 229 | t.Errorf("ldapsearch should have returned operations error due to panic: %v", string(out)) 230 | } 231 | done <- true 232 | }() 233 | 234 | select { 235 | case <-done: 236 | case <-time.After(timeout): 237 | t.Errorf("ldapsearch command timed out") 238 | } 239 | quit <- true 240 | } 241 | 242 | ///////////////////////// 243 | type testStatsWriter struct { 244 | buffer *bytes.Buffer 245 | } 246 | 247 | func (tsw testStatsWriter) Write(buf []byte) (int, error) { 248 | tsw.buffer.Write(buf) 249 | return len(buf), nil 250 | } 251 | 252 | func TestSearchStats(t *testing.T) { 253 | w := testStatsWriter{&bytes.Buffer{}} 254 | log.SetOutput(w) 255 | 256 | quit := make(chan bool) 257 | done := make(chan bool) 258 | s := NewServer() 259 | 260 | go func() { 261 | s.QuitChannel(quit) 262 | s.SearchFunc("", searchSimple{}) 263 | s.BindFunc("", bindAnonOK{}) 264 | s.SetStats(true) 265 | if err := s.ListenAndServe(listenString); err != nil { 266 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 267 | } 268 | }() 269 | 270 | go func() { 271 | cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", "-b", "o=testers,c=test") 272 | out, _ := cmd.CombinedOutput() 273 | if !strings.Contains(string(out), "result: 0 Success") { 274 | t.Errorf("ldapsearch failed: %v", string(out)) 275 | } 276 | done <- true 277 | }() 278 | 279 | select { 280 | case <-done: 281 | case <-time.After(timeout): 282 | t.Errorf("ldapsearch command timed out") 283 | } 284 | 285 | stats := s.GetStats() 286 | log.Println(stats) 287 | if stats.Conns != 1 || stats.Binds != 1 { 288 | t.Errorf("Stats data missing or incorrect: %v", w.buffer.String()) 289 | } 290 | quit <- true 291 | } 292 | 293 | ///////////////////////// 294 | type bindAnonOK struct { 295 | } 296 | 297 | func (b bindAnonOK) Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) { 298 | if bindDN == "" && bindSimplePw == "" { 299 | return LDAPResultSuccess, nil 300 | } 301 | return LDAPResultInvalidCredentials, nil 302 | } 303 | 304 | type bindSimple struct { 305 | } 306 | 307 | func (b bindSimple) Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) { 308 | if bindDN == "cn=testy,o=testers,c=test" && bindSimplePw == "iLike2test" { 309 | return LDAPResultSuccess, nil 310 | } 311 | return LDAPResultInvalidCredentials, nil 312 | } 313 | 314 | type bindSimple2 struct { 315 | } 316 | 317 | func (b bindSimple2) Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) { 318 | if bindDN == "cn=testy,o=testers,c=testz" && bindSimplePw == "ZLike2test" { 319 | return LDAPResultSuccess, nil 320 | } 321 | return LDAPResultInvalidCredentials, nil 322 | } 323 | 324 | type bindPanic struct { 325 | } 326 | 327 | func (b bindPanic) Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) { 328 | panic("test panic at the disco") 329 | return LDAPResultInvalidCredentials, nil 330 | } 331 | 332 | type bindCaseInsensitive struct { 333 | } 334 | 335 | func (b bindCaseInsensitive) Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) { 336 | if strings.ToLower(bindDN) == "cn=case,o=testers,c=test" && bindSimplePw == "iLike2test" { 337 | return LDAPResultSuccess, nil 338 | } 339 | return LDAPResultInvalidCredentials, nil 340 | } 341 | 342 | 343 | type searchSimple struct { 344 | } 345 | 346 | func (s searchSimple) Search(boundDN string, searchReq SearchRequest, conn net.Conn) (ServerSearchResult, error) { 347 | entries := []*Entry{ 348 | &Entry{"cn=ned,o=testers,c=test", []*EntryAttribute{ 349 | &EntryAttribute{"cn", []string{"ned"}}, 350 | &EntryAttribute{"o", []string{"ate"}}, 351 | &EntryAttribute{"uidNumber", []string{"5000"}}, 352 | &EntryAttribute{"accountstatus", []string{"active"}}, 353 | &EntryAttribute{"uid", []string{"ned"}}, 354 | &EntryAttribute{"description", []string{"ned via sa"}}, 355 | &EntryAttribute{"objectclass", []string{"posixaccount"}}, 356 | }}, 357 | &Entry{"cn=trent,o=testers,c=test", []*EntryAttribute{ 358 | &EntryAttribute{"cn", []string{"trent"}}, 359 | &EntryAttribute{"o", []string{"ate"}}, 360 | &EntryAttribute{"uidNumber", []string{"5005"}}, 361 | &EntryAttribute{"accountstatus", []string{"active"}}, 362 | &EntryAttribute{"uid", []string{"trent"}}, 363 | &EntryAttribute{"description", []string{"trent via sa"}}, 364 | &EntryAttribute{"objectclass", []string{"posixaccount"}}, 365 | }}, 366 | &Entry{"cn=randy,o=testers,c=test", []*EntryAttribute{ 367 | &EntryAttribute{"cn", []string{"randy"}}, 368 | &EntryAttribute{"o", []string{"ate"}}, 369 | &EntryAttribute{"uidNumber", []string{"5555"}}, 370 | &EntryAttribute{"accountstatus", []string{"active"}}, 371 | &EntryAttribute{"uid", []string{"randy"}}, 372 | &EntryAttribute{"objectclass", []string{"posixaccount"}}, 373 | }}, 374 | } 375 | return ServerSearchResult{entries, []string{}, []Control{}, LDAPResultSuccess}, nil 376 | } 377 | 378 | type searchSimple2 struct { 379 | } 380 | 381 | func (s searchSimple2) Search(boundDN string, searchReq SearchRequest, conn net.Conn) (ServerSearchResult, error) { 382 | entries := []*Entry{ 383 | &Entry{"cn=hamburger,o=testers,c=testz", []*EntryAttribute{ 384 | &EntryAttribute{"cn", []string{"hamburger"}}, 385 | &EntryAttribute{"o", []string{"testers"}}, 386 | &EntryAttribute{"uidNumber", []string{"5000"}}, 387 | &EntryAttribute{"accountstatus", []string{"active"}}, 388 | &EntryAttribute{"uid", []string{"hamburger"}}, 389 | &EntryAttribute{"objectclass", []string{"posixaccount"}}, 390 | }}, 391 | } 392 | return ServerSearchResult{entries, []string{}, []Control{}, LDAPResultSuccess}, nil 393 | } 394 | 395 | type searchPanic struct { 396 | } 397 | 398 | func (s searchPanic) Search(boundDN string, searchReq SearchRequest, conn net.Conn) (ServerSearchResult, error) { 399 | entries := []*Entry{} 400 | panic("this is a test panic") 401 | return ServerSearchResult{entries, []string{}, []Control{}, LDAPResultSuccess}, nil 402 | } 403 | 404 | type searchControls struct { 405 | } 406 | 407 | func (s searchControls) Search(boundDN string, searchReq SearchRequest, conn net.Conn) (ServerSearchResult, error) { 408 | entries := []*Entry{} 409 | if len(searchReq.Controls) == 1 && searchReq.Controls[0].GetControlType() == "1.2.3.4.5" { 410 | newEntry := &Entry{"cn=hamburger,o=testers,c=testz", []*EntryAttribute{ 411 | &EntryAttribute{"cn", []string{"hamburger"}}, 412 | &EntryAttribute{"o", []string{"testers"}}, 413 | &EntryAttribute{"uidNumber", []string{"5000"}}, 414 | &EntryAttribute{"accountstatus", []string{"active"}}, 415 | &EntryAttribute{"uid", []string{"hamburger"}}, 416 | &EntryAttribute{"objectclass", []string{"posixaccount"}}, 417 | }} 418 | entries = append(entries, newEntry) 419 | } 420 | return ServerSearchResult{entries, []string{}, []Control{}, LDAPResultSuccess}, nil 421 | } 422 | 423 | 424 | type searchCaseInsensitive struct { 425 | } 426 | 427 | func (s searchCaseInsensitive) Search(boundDN string, searchReq SearchRequest, conn net.Conn) (ServerSearchResult, error) { 428 | entries := []*Entry{ 429 | &Entry{"cn=CASE,o=testers,c=test", []*EntryAttribute{ 430 | &EntryAttribute{"cn", []string{"CaSe"}}, 431 | &EntryAttribute{"o", []string{"ate"}}, 432 | &EntryAttribute{"uidNumber", []string{"5005"}}, 433 | &EntryAttribute{"accountstatus", []string{"active"}}, 434 | &EntryAttribute{"uid", []string{"trent"}}, 435 | &EntryAttribute{"description", []string{"trent via sa"}}, 436 | &EntryAttribute{"objectclass", []string{"posixaccount"}}, 437 | }}, 438 | } 439 | return ServerSearchResult{entries, []string{}, []Control{}, LDAPResultSuccess}, nil 440 | } 441 | 442 | 443 | func TestRouteFunc(t *testing.T) { 444 | if routeFunc("", []string{"a", "xyz", "tt"}) != "" { 445 | t.Error("routeFunc failed") 446 | } 447 | if routeFunc("a=b", []string{"a=b", "x=y,a=b", "tt"}) != "a=b" { 448 | t.Error("routeFunc failed") 449 | } 450 | if routeFunc("x=y,a=b", []string{"a=b", "x=y,a=b", "tt"}) != "x=y,a=b" { 451 | t.Error("routeFunc failed") 452 | } 453 | if routeFunc("x=y,a=b", []string{"x=y,a=b", "a=b", "tt"}) != "x=y,a=b" { 454 | t.Error("routeFunc failed") 455 | } 456 | if routeFunc("nosuch", []string{"x=y,a=b", "a=b", "tt"}) != "" { 457 | t.Error("routeFunc failed") 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "crypto/tls" 5 | "io" 6 | "log" 7 | "net" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/nmcclain/asn1-ber" 12 | ) 13 | 14 | type Binder interface { 15 | Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) 16 | } 17 | type Searcher interface { 18 | Search(boundDN string, req SearchRequest, conn net.Conn) (ServerSearchResult, error) 19 | } 20 | type Adder interface { 21 | Add(boundDN string, req AddRequest, conn net.Conn) (LDAPResultCode, error) 22 | } 23 | type Modifier interface { 24 | Modify(boundDN string, req ModifyRequest, conn net.Conn) (LDAPResultCode, error) 25 | } 26 | type Deleter interface { 27 | Delete(boundDN, deleteDN string, conn net.Conn) (LDAPResultCode, error) 28 | } 29 | type ModifyDNr interface { 30 | ModifyDN(boundDN string, req ModifyDNRequest, conn net.Conn) (LDAPResultCode, error) 31 | } 32 | type Comparer interface { 33 | Compare(boundDN string, req CompareRequest, conn net.Conn) (LDAPResultCode, error) 34 | } 35 | type Abandoner interface { 36 | Abandon(boundDN string, conn net.Conn) error 37 | } 38 | type Extender interface { 39 | Extended(boundDN string, req ExtendedRequest, conn net.Conn) (LDAPResultCode, error) 40 | } 41 | type Unbinder interface { 42 | Unbind(boundDN string, conn net.Conn) (LDAPResultCode, error) 43 | } 44 | type Closer interface { 45 | Close(boundDN string, conn net.Conn) error 46 | } 47 | 48 | // 49 | type Server struct { 50 | BindFns map[string]Binder 51 | SearchFns map[string]Searcher 52 | AddFns map[string]Adder 53 | ModifyFns map[string]Modifier 54 | DeleteFns map[string]Deleter 55 | ModifyDNFns map[string]ModifyDNr 56 | CompareFns map[string]Comparer 57 | AbandonFns map[string]Abandoner 58 | ExtendedFns map[string]Extender 59 | UnbindFns map[string]Unbinder 60 | CloseFns map[string]Closer 61 | Quit chan bool 62 | EnforceLDAP bool 63 | Stats *Stats 64 | } 65 | 66 | type Stats struct { 67 | Conns int 68 | Binds int 69 | Unbinds int 70 | Searches int 71 | statsMutex sync.Mutex 72 | } 73 | 74 | type ServerSearchResult struct { 75 | Entries []*Entry 76 | Referrals []string 77 | Controls []Control 78 | ResultCode LDAPResultCode 79 | } 80 | 81 | // 82 | func NewServer() *Server { 83 | s := new(Server) 84 | s.Quit = make(chan bool) 85 | 86 | d := defaultHandler{} 87 | s.BindFns = make(map[string]Binder) 88 | s.SearchFns = make(map[string]Searcher) 89 | s.AddFns = make(map[string]Adder) 90 | s.ModifyFns = make(map[string]Modifier) 91 | s.DeleteFns = make(map[string]Deleter) 92 | s.ModifyDNFns = make(map[string]ModifyDNr) 93 | s.CompareFns = make(map[string]Comparer) 94 | s.AbandonFns = make(map[string]Abandoner) 95 | s.ExtendedFns = make(map[string]Extender) 96 | s.UnbindFns = make(map[string]Unbinder) 97 | s.CloseFns = make(map[string]Closer) 98 | s.BindFunc("", d) 99 | s.SearchFunc("", d) 100 | s.AddFunc("", d) 101 | s.ModifyFunc("", d) 102 | s.DeleteFunc("", d) 103 | s.ModifyDNFunc("", d) 104 | s.CompareFunc("", d) 105 | s.AbandonFunc("", d) 106 | s.ExtendedFunc("", d) 107 | s.UnbindFunc("", d) 108 | s.CloseFunc("", d) 109 | s.Stats = nil 110 | return s 111 | } 112 | func (server *Server) BindFunc(baseDN string, f Binder) { 113 | server.BindFns[baseDN] = f 114 | } 115 | func (server *Server) SearchFunc(baseDN string, f Searcher) { 116 | server.SearchFns[baseDN] = f 117 | } 118 | func (server *Server) AddFunc(baseDN string, f Adder) { 119 | server.AddFns[baseDN] = f 120 | } 121 | func (server *Server) ModifyFunc(baseDN string, f Modifier) { 122 | server.ModifyFns[baseDN] = f 123 | } 124 | func (server *Server) DeleteFunc(baseDN string, f Deleter) { 125 | server.DeleteFns[baseDN] = f 126 | } 127 | func (server *Server) ModifyDNFunc(baseDN string, f ModifyDNr) { 128 | server.ModifyDNFns[baseDN] = f 129 | } 130 | func (server *Server) CompareFunc(baseDN string, f Comparer) { 131 | server.CompareFns[baseDN] = f 132 | } 133 | func (server *Server) AbandonFunc(baseDN string, f Abandoner) { 134 | server.AbandonFns[baseDN] = f 135 | } 136 | func (server *Server) ExtendedFunc(baseDN string, f Extender) { 137 | server.ExtendedFns[baseDN] = f 138 | } 139 | func (server *Server) UnbindFunc(baseDN string, f Unbinder) { 140 | server.UnbindFns[baseDN] = f 141 | } 142 | func (server *Server) CloseFunc(baseDN string, f Closer) { 143 | server.CloseFns[baseDN] = f 144 | } 145 | func (server *Server) QuitChannel(quit chan bool) { 146 | server.Quit = quit 147 | } 148 | 149 | func (server *Server) ListenAndServeTLS(listenString string, certFile string, keyFile string) error { 150 | cert, err := tls.LoadX509KeyPair(certFile, keyFile) 151 | if err != nil { 152 | return err 153 | } 154 | tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}} 155 | tlsConfig.ServerName = "localhost" 156 | ln, err := tls.Listen("tcp", listenString, &tlsConfig) 157 | if err != nil { 158 | return err 159 | } 160 | err = server.Serve(ln) 161 | if err != nil { 162 | return err 163 | } 164 | return nil 165 | } 166 | 167 | func (server *Server) SetStats(enable bool) { 168 | if enable { 169 | server.Stats = &Stats{} 170 | } else { 171 | server.Stats = nil 172 | } 173 | } 174 | 175 | func (server *Server) GetStats() Stats { 176 | defer func() { 177 | server.Stats.statsMutex.Unlock() 178 | }() 179 | server.Stats.statsMutex.Lock() 180 | return *server.Stats 181 | } 182 | 183 | func (server *Server) ListenAndServe(listenString string) error { 184 | ln, err := net.Listen("tcp", listenString) 185 | if err != nil { 186 | return err 187 | } 188 | err = server.Serve(ln) 189 | if err != nil { 190 | return err 191 | } 192 | return nil 193 | } 194 | 195 | func (server *Server) Serve(ln net.Listener) error { 196 | newConn := make(chan net.Conn) 197 | go func() { 198 | for { 199 | conn, err := ln.Accept() 200 | if err != nil { 201 | if !strings.HasSuffix(err.Error(), "use of closed network connection") { 202 | log.Printf("Error accepting network connection: %s", err.Error()) 203 | } 204 | break 205 | } 206 | newConn <- conn 207 | } 208 | }() 209 | 210 | listener: 211 | for { 212 | select { 213 | case c := <-newConn: 214 | server.Stats.countConns(1) 215 | go server.handleConnection(c) 216 | case <-server.Quit: 217 | ln.Close() 218 | break listener 219 | } 220 | } 221 | return nil 222 | } 223 | 224 | // 225 | func (server *Server) handleConnection(conn net.Conn) { 226 | boundDN := "" // "" == anonymous 227 | 228 | handler: 229 | for { 230 | // read incoming LDAP packet 231 | packet, err := ber.ReadPacket(conn) 232 | if err == io.EOF { // Client closed connection 233 | break 234 | } else if err != nil { 235 | log.Printf("handleConnection ber.ReadPacket ERROR: %s", err.Error()) 236 | break 237 | } 238 | 239 | // sanity check this packet 240 | if len(packet.Children) < 2 { 241 | log.Print("len(packet.Children) < 2") 242 | break 243 | } 244 | // check the message ID and ClassType 245 | messageID, ok := packet.Children[0].Value.(uint64) 246 | if !ok { 247 | log.Print("malformed messageID") 248 | break 249 | } 250 | req := packet.Children[1] 251 | if req.ClassType != ber.ClassApplication { 252 | log.Print("req.ClassType != ber.ClassApplication") 253 | break 254 | } 255 | // handle controls if present 256 | controls := []Control{} 257 | if len(packet.Children) > 2 { 258 | for _, child := range packet.Children[2].Children { 259 | controls = append(controls, DecodeControl(child)) 260 | } 261 | } 262 | 263 | //log.Printf("DEBUG: handling operation: %s [%d]", ApplicationMap[req.Tag], req.Tag) 264 | //ber.PrintPacket(packet) // DEBUG 265 | 266 | // dispatch the LDAP operation 267 | switch req.Tag { // ldap op code 268 | default: 269 | responsePacket := encodeLDAPResponse(messageID, ApplicationAddResponse, LDAPResultOperationsError, "Unsupported operation: add") 270 | if err = sendPacket(conn, responsePacket); err != nil { 271 | log.Printf("sendPacket error %s", err.Error()) 272 | } 273 | log.Printf("Unhandled operation: %s [%d]", ApplicationMap[req.Tag], req.Tag) 274 | break handler 275 | 276 | case ApplicationBindRequest: 277 | server.Stats.countBinds(1) 278 | ldapResultCode := HandleBindRequest(req, server.BindFns, conn) 279 | if ldapResultCode == LDAPResultSuccess { 280 | boundDN, ok = req.Children[1].Value.(string) 281 | if !ok { 282 | log.Printf("Malformed Bind DN") 283 | break handler 284 | } 285 | } 286 | responsePacket := encodeBindResponse(messageID, ldapResultCode) 287 | if err = sendPacket(conn, responsePacket); err != nil { 288 | log.Printf("sendPacket error %s", err.Error()) 289 | break handler 290 | } 291 | case ApplicationSearchRequest: 292 | server.Stats.countSearches(1) 293 | if err := HandleSearchRequest(req, &controls, messageID, boundDN, server, conn); err != nil { 294 | log.Printf("handleSearchRequest error %s", err.Error()) // TODO: make this more testable/better err handling - stop using log, stop using breaks? 295 | e := err.(*Error) 296 | if err = sendPacket(conn, encodeSearchDone(messageID, e.ResultCode)); err != nil { 297 | log.Printf("sendPacket error %s", err.Error()) 298 | break handler 299 | } 300 | break handler 301 | } else { 302 | if err = sendPacket(conn, encodeSearchDone(messageID, LDAPResultSuccess)); err != nil { 303 | log.Printf("sendPacket error %s", err.Error()) 304 | break handler 305 | } 306 | } 307 | case ApplicationUnbindRequest: 308 | server.Stats.countUnbinds(1) 309 | break handler // simply disconnect 310 | case ApplicationExtendedRequest: 311 | ldapResultCode := HandleExtendedRequest(req, boundDN, server.ExtendedFns, conn) 312 | responsePacket := encodeLDAPResponse(messageID, ApplicationExtendedResponse, ldapResultCode, LDAPResultCodeMap[ldapResultCode]) 313 | if err = sendPacket(conn, responsePacket); err != nil { 314 | log.Printf("sendPacket error %s", err.Error()) 315 | break handler 316 | } 317 | case ApplicationAbandonRequest: 318 | HandleAbandonRequest(req, boundDN, server.AbandonFns, conn) 319 | break handler 320 | 321 | case ApplicationAddRequest: 322 | ldapResultCode := HandleAddRequest(req, boundDN, server.AddFns, conn) 323 | responsePacket := encodeLDAPResponse(messageID, ApplicationAddResponse, ldapResultCode, LDAPResultCodeMap[ldapResultCode]) 324 | if err = sendPacket(conn, responsePacket); err != nil { 325 | log.Printf("sendPacket error %s", err.Error()) 326 | break handler 327 | } 328 | case ApplicationModifyRequest: 329 | ldapResultCode := HandleModifyRequest(req, boundDN, server.ModifyFns, conn) 330 | responsePacket := encodeLDAPResponse(messageID, ApplicationModifyResponse, ldapResultCode, LDAPResultCodeMap[ldapResultCode]) 331 | if err = sendPacket(conn, responsePacket); err != nil { 332 | log.Printf("sendPacket error %s", err.Error()) 333 | break handler 334 | } 335 | case ApplicationDelRequest: 336 | ldapResultCode := HandleDeleteRequest(req, boundDN, server.DeleteFns, conn) 337 | responsePacket := encodeLDAPResponse(messageID, ApplicationDelResponse, ldapResultCode, LDAPResultCodeMap[ldapResultCode]) 338 | if err = sendPacket(conn, responsePacket); err != nil { 339 | log.Printf("sendPacket error %s", err.Error()) 340 | break handler 341 | } 342 | case ApplicationModifyDNRequest: 343 | ldapResultCode := HandleModifyDNRequest(req, boundDN, server.ModifyDNFns, conn) 344 | responsePacket := encodeLDAPResponse(messageID, ApplicationModifyDNResponse, ldapResultCode, LDAPResultCodeMap[ldapResultCode]) 345 | if err = sendPacket(conn, responsePacket); err != nil { 346 | log.Printf("sendPacket error %s", err.Error()) 347 | break handler 348 | } 349 | case ApplicationCompareRequest: 350 | ldapResultCode := HandleCompareRequest(req, boundDN, server.CompareFns, conn) 351 | responsePacket := encodeLDAPResponse(messageID, ApplicationCompareResponse, ldapResultCode, LDAPResultCodeMap[ldapResultCode]) 352 | if err = sendPacket(conn, responsePacket); err != nil { 353 | log.Printf("sendPacket error %s", err.Error()) 354 | break handler 355 | } 356 | } 357 | } 358 | 359 | for _, c := range server.CloseFns { 360 | c.Close(boundDN, conn) 361 | } 362 | 363 | conn.Close() 364 | } 365 | 366 | // 367 | func sendPacket(conn net.Conn, packet *ber.Packet) error { 368 | _, err := conn.Write(packet.Bytes()) 369 | if err != nil { 370 | log.Printf("Error Sending Message: %s", err.Error()) 371 | return err 372 | } 373 | return nil 374 | } 375 | 376 | // 377 | func routeFunc(dn string, funcNames []string) string { 378 | bestPick := "" 379 | bestPickWeight := 0 380 | dnMatch := "," + strings.ToLower(dn) 381 | var weight int 382 | for _, fn := range funcNames { 383 | if strings.HasSuffix(dnMatch, "," + fn) { 384 | // empty string as 0, no-comma string 1 , etc 385 | if fn == "" { 386 | weight = 0 387 | } else { 388 | weight = strings.Count(fn, ",") + 1 389 | } 390 | if weight > bestPickWeight { 391 | bestPick = fn 392 | bestPickWeight = weight 393 | } 394 | } 395 | } 396 | return bestPick 397 | } 398 | 399 | // 400 | func encodeLDAPResponse(messageID uint64, responseType uint8, ldapResultCode LDAPResultCode, message string) *ber.Packet { 401 | responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response") 402 | responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "Message ID")) 403 | reponse := ber.Encode(ber.ClassApplication, ber.TypeConstructed, responseType, nil, ApplicationMap[responseType]) 404 | reponse.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(ldapResultCode), "resultCode: ")) 405 | reponse.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "matchedDN: ")) 406 | reponse.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, message, "errorMessage: ")) 407 | responsePacket.AppendChild(reponse) 408 | return responsePacket 409 | } 410 | 411 | // 412 | type defaultHandler struct { 413 | } 414 | 415 | func (h defaultHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) { 416 | return LDAPResultInvalidCredentials, nil 417 | } 418 | func (h defaultHandler) Search(boundDN string, req SearchRequest, conn net.Conn) (ServerSearchResult, error) { 419 | return ServerSearchResult{make([]*Entry, 0), []string{}, []Control{}, LDAPResultSuccess}, nil 420 | } 421 | func (h defaultHandler) Add(boundDN string, req AddRequest, conn net.Conn) (LDAPResultCode, error) { 422 | return LDAPResultInsufficientAccessRights, nil 423 | } 424 | func (h defaultHandler) Modify(boundDN string, req ModifyRequest, conn net.Conn) (LDAPResultCode, error) { 425 | return LDAPResultInsufficientAccessRights, nil 426 | } 427 | func (h defaultHandler) Delete(boundDN, deleteDN string, conn net.Conn) (LDAPResultCode, error) { 428 | return LDAPResultInsufficientAccessRights, nil 429 | } 430 | func (h defaultHandler) ModifyDN(boundDN string, req ModifyDNRequest, conn net.Conn) (LDAPResultCode, error) { 431 | return LDAPResultInsufficientAccessRights, nil 432 | } 433 | func (h defaultHandler) Compare(boundDN string, req CompareRequest, conn net.Conn) (LDAPResultCode, error) { 434 | return LDAPResultInsufficientAccessRights, nil 435 | } 436 | func (h defaultHandler) Abandon(boundDN string, conn net.Conn) error { 437 | return nil 438 | } 439 | func (h defaultHandler) Extended(boundDN string, req ExtendedRequest, conn net.Conn) (LDAPResultCode, error) { 440 | return LDAPResultProtocolError, nil 441 | } 442 | func (h defaultHandler) Unbind(boundDN string, conn net.Conn) (LDAPResultCode, error) { 443 | return LDAPResultSuccess, nil 444 | } 445 | func (h defaultHandler) Close(boundDN string, conn net.Conn) error { 446 | conn.Close() 447 | return nil 448 | } 449 | 450 | // 451 | func (stats *Stats) countConns(delta int) { 452 | if stats != nil { 453 | stats.statsMutex.Lock() 454 | stats.Conns += delta 455 | stats.statsMutex.Unlock() 456 | } 457 | } 458 | func (stats *Stats) countBinds(delta int) { 459 | if stats != nil { 460 | stats.statsMutex.Lock() 461 | stats.Binds += delta 462 | stats.statsMutex.Unlock() 463 | } 464 | } 465 | func (stats *Stats) countUnbinds(delta int) { 466 | if stats != nil { 467 | stats.statsMutex.Lock() 468 | stats.Unbinds += delta 469 | stats.statsMutex.Unlock() 470 | } 471 | } 472 | func (stats *Stats) countSearches(delta int) { 473 | if stats != nil { 474 | stats.statsMutex.Lock() 475 | stats.Searches += delta 476 | stats.statsMutex.Unlock() 477 | } 478 | } 479 | 480 | // 481 | -------------------------------------------------------------------------------- /server_search_test.go: -------------------------------------------------------------------------------- 1 | package ldap 2 | 3 | import ( 4 | "os/exec" 5 | "strings" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | // 11 | func TestSearchSimpleOK(t *testing.T) { 12 | quit := make(chan bool) 13 | done := make(chan bool) 14 | go func() { 15 | s := NewServer() 16 | s.QuitChannel(quit) 17 | s.SearchFunc("", searchSimple{}) 18 | s.BindFunc("", bindSimple{}) 19 | if err := s.ListenAndServe(listenString); err != nil { 20 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 21 | } 22 | }() 23 | 24 | serverBaseDN := "o=testers,c=test" 25 | 26 | go func() { 27 | cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", 28 | "-b", serverBaseDN, "-D", "cn=testy,"+serverBaseDN, "-w", "iLike2test") 29 | out, _ := cmd.CombinedOutput() 30 | if !strings.Contains(string(out), "dn: cn=ned,o=testers,c=test") { 31 | t.Errorf("ldapsearch failed: %v", string(out)) 32 | } 33 | if !strings.Contains(string(out), "uidNumber: 5000") { 34 | t.Errorf("ldapsearch failed: %v", string(out)) 35 | } 36 | if !strings.Contains(string(out), "result: 0 Success") { 37 | t.Errorf("ldapsearch failed: %v", string(out)) 38 | } 39 | if !strings.Contains(string(out), "numResponses: 4") { 40 | t.Errorf("ldapsearch failed: %v", string(out)) 41 | } 42 | done <- true 43 | }() 44 | 45 | select { 46 | case <-done: 47 | case <-time.After(timeout): 48 | t.Errorf("ldapsearch command timed out") 49 | } 50 | quit <- true 51 | } 52 | 53 | func TestSearchSizelimit(t *testing.T) { 54 | quit := make(chan bool) 55 | done := make(chan bool) 56 | go func() { 57 | s := NewServer() 58 | s.EnforceLDAP = true 59 | s.QuitChannel(quit) 60 | s.SearchFunc("", searchSimple{}) 61 | s.BindFunc("", bindSimple{}) 62 | if err := s.ListenAndServe(listenString); err != nil { 63 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 64 | } 65 | }() 66 | 67 | go func() { 68 | cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", 69 | "-b", serverBaseDN, "-D", "cn=testy,"+serverBaseDN, "-w", "iLike2test") // no limit for this test 70 | out, _ := cmd.CombinedOutput() 71 | if !strings.Contains(string(out), "result: 0 Success") { 72 | t.Errorf("ldapsearch failed: %v", string(out)) 73 | } 74 | if !strings.Contains(string(out), "numEntries: 3") { 75 | t.Errorf("ldapsearch sizelimit unlimited failed - not enough entries: %v", string(out)) 76 | } 77 | 78 | cmd = exec.Command("ldapsearch", "-H", ldapURL, "-x", 79 | "-b", serverBaseDN, "-D", "cn=testy,"+serverBaseDN, "-w", "iLike2test", "-z", "9") // effectively no limit for this test 80 | out, _ = cmd.CombinedOutput() 81 | if !strings.Contains(string(out), "result: 0 Success") { 82 | t.Errorf("ldapsearch failed: %v", string(out)) 83 | } 84 | if !strings.Contains(string(out), "numEntries: 3") { 85 | t.Errorf("ldapsearch sizelimit 9 failed - not enough entries: %v", string(out)) 86 | } 87 | 88 | cmd = exec.Command("ldapsearch", "-H", ldapURL, "-x", 89 | "-b", serverBaseDN, "-D", "cn=testy,"+serverBaseDN, "-w", "iLike2test", "-z", "2") 90 | out, _ = cmd.CombinedOutput() 91 | if !strings.Contains(string(out), "result: 0 Success") { 92 | t.Errorf("ldapsearch failed: %v", string(out)) 93 | } 94 | if !strings.Contains(string(out), "numEntries: 2") { 95 | t.Errorf("ldapsearch sizelimit 2 failed - too many entries: %v", string(out)) 96 | } 97 | 98 | cmd = exec.Command("ldapsearch", "-H", ldapURL, "-x", 99 | "-b", serverBaseDN, "-D", "cn=testy,"+serverBaseDN, "-w", "iLike2test", "-z", "1") 100 | out, _ = cmd.CombinedOutput() 101 | if !strings.Contains(string(out), "result: 0 Success") { 102 | t.Errorf("ldapsearch failed: %v", string(out)) 103 | } 104 | if !strings.Contains(string(out), "numEntries: 1") { 105 | t.Errorf("ldapsearch sizelimit 1 failed - too many entries: %v", string(out)) 106 | } 107 | 108 | cmd = exec.Command("ldapsearch", "-H", ldapURL, "-x", 109 | "-b", serverBaseDN, "-D", "cn=testy,"+serverBaseDN, "-w", "iLike2test", "-z", "0") 110 | out, _ = cmd.CombinedOutput() 111 | if !strings.Contains(string(out), "result: 0 Success") { 112 | t.Errorf("ldapsearch failed: %v", string(out)) 113 | } 114 | if !strings.Contains(string(out), "numEntries: 3") { 115 | t.Errorf("ldapsearch sizelimit 0 failed - wrong number of entries: %v", string(out)) 116 | } 117 | 118 | cmd = exec.Command("ldapsearch", "-H", ldapURL, "-x", 119 | "-b", serverBaseDN, "-D", "cn=testy,"+serverBaseDN, "-w", "iLike2test", "-z", "1", "(uid=trent)") 120 | out, _ = cmd.CombinedOutput() 121 | if !strings.Contains(string(out), "result: 0 Success") { 122 | t.Errorf("ldapsearch failed: %v", string(out)) 123 | } 124 | if !strings.Contains(string(out), "numEntries: 1") { 125 | t.Errorf("ldapsearch sizelimit 1 with filter failed - wrong number of entries: %v", string(out)) 126 | } 127 | 128 | cmd = exec.Command("ldapsearch", "-H", ldapURL, "-x", 129 | "-b", serverBaseDN, "-D", "cn=testy,"+serverBaseDN, "-w", "iLike2test", "-z", "0", "(uid=trent)") 130 | out, _ = cmd.CombinedOutput() 131 | if !strings.Contains(string(out), "result: 0 Success") { 132 | t.Errorf("ldapsearch failed: %v", string(out)) 133 | } 134 | if !strings.Contains(string(out), "numEntries: 1") { 135 | t.Errorf("ldapsearch sizelimit 0 with filter failed - wrong number of entries: %v", string(out)) 136 | } 137 | done <- true 138 | }() 139 | 140 | select { 141 | case <-done: 142 | case <-time.After(timeout): 143 | t.Errorf("ldapsearch command timed out") 144 | } 145 | quit <- true 146 | } 147 | 148 | ///////////////////////// 149 | func TestBindSearchMulti(t *testing.T) { 150 | quit := make(chan bool) 151 | done := make(chan bool) 152 | go func() { 153 | s := NewServer() 154 | s.QuitChannel(quit) 155 | s.BindFunc("", bindSimple{}) 156 | s.BindFunc("c=testz", bindSimple2{}) 157 | s.SearchFunc("", searchSimple{}) 158 | s.SearchFunc("c=testz", searchSimple2{}) 159 | if err := s.ListenAndServe(listenString); err != nil { 160 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 161 | } 162 | }() 163 | 164 | go func() { 165 | cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", "-b", "o=testers,c=test", 166 | "-D", "cn=testy,o=testers,c=test", "-w", "iLike2test", "cn=ned") 167 | out, _ := cmd.CombinedOutput() 168 | if !strings.Contains(string(out), "result: 0 Success") { 169 | t.Errorf("error routing default bind/search functions: %v", string(out)) 170 | } 171 | if !strings.Contains(string(out), "dn: cn=ned,o=testers,c=test") { 172 | t.Errorf("search default routing failed: %v", string(out)) 173 | } 174 | cmd = exec.Command("ldapsearch", "-H", ldapURL, "-x", "-b", "o=testers,c=testz", 175 | "-D", "cn=testy,o=testers,c=testz", "-w", "ZLike2test", "cn=hamburger") 176 | out, _ = cmd.CombinedOutput() 177 | if !strings.Contains(string(out), "result: 0 Success") { 178 | t.Errorf("error routing custom bind/search functions: %v", string(out)) 179 | } 180 | if !strings.Contains(string(out), "dn: cn=hamburger,o=testers,c=testz") { 181 | t.Errorf("search custom routing failed: %v", string(out)) 182 | } 183 | done <- true 184 | }() 185 | 186 | select { 187 | case <-done: 188 | case <-time.After(timeout): 189 | t.Errorf("ldapsearch command timed out") 190 | } 191 | 192 | quit <- true 193 | } 194 | 195 | ///////////////////////// 196 | func TestSearchPanic(t *testing.T) { 197 | quit := make(chan bool) 198 | done := make(chan bool) 199 | go func() { 200 | s := NewServer() 201 | s.QuitChannel(quit) 202 | s.SearchFunc("", searchPanic{}) 203 | s.BindFunc("", bindAnonOK{}) 204 | if err := s.ListenAndServe(listenString); err != nil { 205 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 206 | } 207 | }() 208 | 209 | go func() { 210 | cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", "-b", "o=testers,c=test") 211 | out, _ := cmd.CombinedOutput() 212 | if !strings.Contains(string(out), "result: 1 Operations error") { 213 | t.Errorf("ldapsearch should have returned operations error due to panic: %v", string(out)) 214 | } 215 | done <- true 216 | }() 217 | 218 | select { 219 | case <-done: 220 | case <-time.After(timeout): 221 | t.Errorf("ldapsearch command timed out") 222 | } 223 | quit <- true 224 | } 225 | 226 | ///////////////////////// 227 | type compileSearchFilterTest struct { 228 | name string 229 | filterStr string 230 | numResponses string 231 | } 232 | 233 | var searchFilterTestFilters = []compileSearchFilterTest{ 234 | compileSearchFilterTest{name: "equalityOk", filterStr: "(uid=ned)", numResponses: "2"}, 235 | compileSearchFilterTest{name: "equalityNo", filterStr: "(uid=foo)", numResponses: "1"}, 236 | compileSearchFilterTest{name: "equalityOk", filterStr: "(objectclass=posixaccount)", numResponses: "4"}, 237 | compileSearchFilterTest{name: "presentEmptyOk", filterStr: "", numResponses: "4"}, 238 | compileSearchFilterTest{name: "presentOk", filterStr: "(objectclass=*)", numResponses: "4"}, 239 | compileSearchFilterTest{name: "presentOk", filterStr: "(description=*)", numResponses: "3"}, 240 | compileSearchFilterTest{name: "presentNo", filterStr: "(foo=*)", numResponses: "1"}, 241 | compileSearchFilterTest{name: "andOk", filterStr: "(&(uid=ned)(objectclass=posixaccount))", numResponses: "2"}, 242 | compileSearchFilterTest{name: "andNo", filterStr: "(&(uid=ned)(objectclass=posixgroup))", numResponses: "1"}, 243 | compileSearchFilterTest{name: "andNo", filterStr: "(&(uid=ned)(uid=trent))", numResponses: "1"}, 244 | compileSearchFilterTest{name: "orOk", filterStr: "(|(uid=ned)(uid=trent))", numResponses: "3"}, 245 | compileSearchFilterTest{name: "orOk", filterStr: "(|(uid=ned)(objectclass=posixaccount))", numResponses: "4"}, 246 | compileSearchFilterTest{name: "orNo", filterStr: "(|(uid=foo)(objectclass=foo))", numResponses: "1"}, 247 | compileSearchFilterTest{name: "andOrOk", filterStr: "(&(|(uid=ned)(uid=trent))(objectclass=posixaccount))", numResponses: "3"}, 248 | compileSearchFilterTest{name: "notOk", filterStr: "(!(uid=ned))", numResponses: "3"}, 249 | compileSearchFilterTest{name: "notOk", filterStr: "(!(uid=foo))", numResponses: "4"}, 250 | compileSearchFilterTest{name: "notAndOrOk", filterStr: "(&(|(uid=ned)(uid=trent))(!(objectclass=posixgroup)))", numResponses: "3"}, 251 | /* 252 | compileSearchFilterTest{filterStr: "(sn=Mill*)", filterType: FilterSubstrings}, 253 | compileSearchFilterTest{filterStr: "(sn=*Mill)", filterType: FilterSubstrings}, 254 | compileSearchFilterTest{filterStr: "(sn=*Mill*)", filterType: FilterSubstrings}, 255 | compileSearchFilterTest{filterStr: "(sn>=Miller)", filterType: FilterGreaterOrEqual}, 256 | compileSearchFilterTest{filterStr: "(sn<=Miller)", filterType: FilterLessOrEqual}, 257 | compileSearchFilterTest{filterStr: "(sn~=Miller)", filterType: FilterApproxMatch}, 258 | */ 259 | } 260 | 261 | ///////////////////////// 262 | func TestSearchFiltering(t *testing.T) { 263 | quit := make(chan bool) 264 | done := make(chan bool) 265 | go func() { 266 | s := NewServer() 267 | s.EnforceLDAP = true 268 | s.QuitChannel(quit) 269 | s.SearchFunc("", searchSimple{}) 270 | s.BindFunc("", bindSimple{}) 271 | if err := s.ListenAndServe(listenString); err != nil { 272 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 273 | } 274 | }() 275 | 276 | for _, i := range searchFilterTestFilters { 277 | t.Log(i.name) 278 | 279 | go func() { 280 | cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", 281 | "-b", serverBaseDN, "-D", "cn=testy,"+serverBaseDN, "-w", "iLike2test", i.filterStr) 282 | out, _ := cmd.CombinedOutput() 283 | if !strings.Contains(string(out), "numResponses: "+i.numResponses) { 284 | t.Errorf("ldapsearch failed - expected numResponses==%s: %v", i.numResponses, string(out)) 285 | } 286 | done <- true 287 | }() 288 | 289 | select { 290 | case <-done: 291 | case <-time.After(timeout): 292 | t.Errorf("ldapsearch command timed out") 293 | } 294 | } 295 | quit <- true 296 | } 297 | 298 | ///////////////////////// 299 | func TestSearchAttributes(t *testing.T) { 300 | quit := make(chan bool) 301 | done := make(chan bool) 302 | go func() { 303 | s := NewServer() 304 | s.EnforceLDAP = true 305 | s.QuitChannel(quit) 306 | s.SearchFunc("", searchSimple{}) 307 | s.BindFunc("", bindSimple{}) 308 | if err := s.ListenAndServe(listenString); err != nil { 309 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 310 | } 311 | }() 312 | 313 | go func() { 314 | filterString := "" 315 | cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", 316 | "-b", serverBaseDN, "-D", "cn=testy,"+serverBaseDN, "-w", "iLike2test", filterString, "cn") 317 | out, _ := cmd.CombinedOutput() 318 | 319 | if !strings.Contains(string(out), "dn: cn=ned,o=testers,c=test") { 320 | t.Errorf("ldapsearch failed - missing requested DN attribute: %v", string(out)) 321 | } 322 | if !strings.Contains(string(out), "cn: ned") { 323 | t.Errorf("ldapsearch failed - missing requested CN attribute: %v", string(out)) 324 | } 325 | if strings.Contains(string(out), "uidNumber") { 326 | t.Errorf("ldapsearch failed - uidNumber attr should not be displayed: %v", string(out)) 327 | } 328 | if strings.Contains(string(out), "accountstatus") { 329 | t.Errorf("ldapsearch failed - accountstatus attr should not be displayed: %v", string(out)) 330 | } 331 | done <- true 332 | }() 333 | 334 | select { 335 | case <-done: 336 | case <-time.After(timeout): 337 | t.Errorf("ldapsearch command timed out") 338 | } 339 | quit <- true 340 | } 341 | 342 | func TestSearchAllUserAttributes(t *testing.T) { 343 | quit := make(chan bool) 344 | done := make(chan bool) 345 | go func() { 346 | s := NewServer() 347 | s.EnforceLDAP = true 348 | s.QuitChannel(quit) 349 | s.SearchFunc("", searchSimple{}) 350 | s.BindFunc("", bindSimple{}) 351 | if err := s.ListenAndServe(listenString); err != nil { 352 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 353 | } 354 | }() 355 | 356 | go func() { 357 | filterString := "" 358 | cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", 359 | "-b", serverBaseDN, "-D", "cn=testy,"+serverBaseDN, "-w", "iLike2test", filterString, "*") 360 | out, _ := cmd.CombinedOutput() 361 | 362 | if !strings.Contains(string(out), "dn: cn=ned,o=testers,c=test") { 363 | t.Errorf("ldapsearch failed - missing requested DN attribute: %v", string(out)) 364 | } 365 | if !strings.Contains(string(out), "cn: ned") { 366 | t.Errorf("ldapsearch failed - missing requested CN attribute: %v", string(out)) 367 | } 368 | if !strings.Contains(string(out), "uidNumber") { 369 | t.Errorf("ldapsearch failed - missing requested uidNumber attribute: %v", string(out)) 370 | } 371 | if !strings.Contains(string(out), "accountstatus") { 372 | t.Errorf("ldapsearch failed - missing requested accountstatus attribute: %v", string(out)) 373 | } 374 | if !strings.Contains(string(out), "o: ate") { 375 | t.Errorf("ldapsearch failed - missing requested o attribute: %v", string(out)) 376 | } 377 | if !strings.Contains(string(out), "description") { 378 | t.Errorf("ldapsearch failed - missing requested description attribute: %v", string(out)) 379 | } 380 | if !strings.Contains(string(out), "objectclass") { 381 | t.Errorf("ldapsearch failed - missing requested objectclass attribute: %v", string(out)) 382 | } 383 | done <- true 384 | }() 385 | 386 | select { 387 | case <-done: 388 | case <-time.After(timeout): 389 | t.Errorf("ldapsearch command timed out") 390 | } 391 | quit <- true 392 | } 393 | 394 | ///////////////////////// 395 | func TestSearchScope(t *testing.T) { 396 | quit := make(chan bool) 397 | done := make(chan bool) 398 | go func() { 399 | s := NewServer() 400 | s.EnforceLDAP = true 401 | s.QuitChannel(quit) 402 | s.SearchFunc("", searchSimple{}) 403 | s.BindFunc("", bindSimple{}) 404 | if err := s.ListenAndServe(listenString); err != nil { 405 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 406 | } 407 | }() 408 | 409 | go func() { 410 | cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", 411 | "-b", "c=test", "-D", "cn=testy,o=testers,c=test", "-w", "iLike2test", "-s", "sub", "cn=trent") 412 | out, _ := cmd.CombinedOutput() 413 | if !strings.Contains(string(out), "dn: cn=trent,o=testers,c=test") { 414 | t.Errorf("ldapsearch 'sub' scope failed - didn't find expected DN: %v", string(out)) 415 | } 416 | 417 | cmd = exec.Command("ldapsearch", "-H", ldapURL, "-x", 418 | "-b", "o=testers,c=test", "-D", "cn=testy,o=testers,c=test", "-w", "iLike2test", "-s", "one", "cn=trent") 419 | out, _ = cmd.CombinedOutput() 420 | if !strings.Contains(string(out), "dn: cn=trent,o=testers,c=test") { 421 | t.Errorf("ldapsearch 'one' scope failed - didn't find expected DN: %v", string(out)) 422 | } 423 | cmd = exec.Command("ldapsearch", "-H", ldapURL, "-x", 424 | "-b", "c=test", "-D", "cn=testy,o=testers,c=test", "-w", "iLike2test", "-s", "one", "cn=trent") 425 | out, _ = cmd.CombinedOutput() 426 | if strings.Contains(string(out), "dn: cn=trent,o=testers,c=test") { 427 | t.Errorf("ldapsearch 'one' scope failed - found unexpected DN: %v", string(out)) 428 | } 429 | 430 | cmd = exec.Command("ldapsearch", "-H", ldapURL, "-x", 431 | "-b", "cn=trent,o=testers,c=test", "-D", "cn=testy,o=testers,c=test", "-w", "iLike2test", "-s", "base", "cn=trent") 432 | out, _ = cmd.CombinedOutput() 433 | if !strings.Contains(string(out), "dn: cn=trent,o=testers,c=test") { 434 | t.Errorf("ldapsearch 'base' scope failed - didn't find expected DN: %v", string(out)) 435 | } 436 | cmd = exec.Command("ldapsearch", "-H", ldapURL, "-x", 437 | "-b", "o=testers,c=test", "-D", "cn=testy,o=testers,c=test", "-w", "iLike2test", "-s", "base", "cn=trent") 438 | out, _ = cmd.CombinedOutput() 439 | if strings.Contains(string(out), "dn: cn=trent,o=testers,c=test") { 440 | t.Errorf("ldapsearch 'base' scope failed - found unexpected DN: %v", string(out)) 441 | } 442 | 443 | done <- true 444 | }() 445 | 446 | select { 447 | case <-done: 448 | case <-time.After(timeout): 449 | t.Errorf("ldapsearch command timed out") 450 | } 451 | quit <- true 452 | } 453 | 454 | 455 | ///////////////////////// 456 | func TestSearchScopeCaseInsensitive(t *testing.T) { 457 | quit := make(chan bool) 458 | done := make(chan bool) 459 | go func() { 460 | s := NewServer() 461 | s.EnforceLDAP = true 462 | s.QuitChannel(quit) 463 | s.SearchFunc("", searchCaseInsensitive{}) 464 | s.BindFunc("", bindCaseInsensitive{}) 465 | if err := s.ListenAndServe(listenString); err != nil { 466 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 467 | } 468 | }() 469 | 470 | go func() { 471 | cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", 472 | "-b", "cn=Case,o=testers,c=test", "-D", "cn=CAse,o=testers,c=test", "-w", "iLike2test", "-s", "base", "cn=CASe") 473 | out, _ := cmd.CombinedOutput() 474 | if !strings.Contains(string(out), "dn: cn=CASE,o=testers,c=test") { 475 | t.Errorf("ldapsearch 'base' scope failed - didn't find expected DN: %v", string(out)) 476 | } 477 | 478 | done <- true 479 | }() 480 | 481 | select { 482 | case <-done: 483 | case <-time.After(timeout): 484 | t.Errorf("ldapsearch command timed out") 485 | } 486 | quit <- true 487 | } 488 | 489 | 490 | func TestSearchControls(t *testing.T) { 491 | quit := make(chan bool) 492 | done := make(chan bool) 493 | go func() { 494 | s := NewServer() 495 | s.QuitChannel(quit) 496 | s.SearchFunc("", searchControls{}) 497 | s.BindFunc("", bindSimple{}) 498 | if err := s.ListenAndServe(listenString); err != nil { 499 | t.Errorf("s.ListenAndServe failed: %s", err.Error()) 500 | } 501 | }() 502 | 503 | serverBaseDN := "o=testers,c=test" 504 | 505 | go func() { 506 | cmd := exec.Command("ldapsearch", "-H", ldapURL, "-x", 507 | "-b", serverBaseDN, "-D", "cn=testy,"+serverBaseDN, "-w", "iLike2test", "-e", "1.2.3.4.5") 508 | out, _ := cmd.CombinedOutput() 509 | if !strings.Contains(string(out), "dn: cn=hamburger,o=testers,c=testz") { 510 | t.Errorf("ldapsearch with control failed: %v", string(out)) 511 | } 512 | if !strings.Contains(string(out), "result: 0 Success") { 513 | t.Errorf("ldapsearch with control failed: %v", string(out)) 514 | } 515 | if !strings.Contains(string(out), "numResponses: 2") { 516 | t.Errorf("ldapsearch with control failed: %v", string(out)) 517 | } 518 | 519 | cmd = exec.Command("ldapsearch", "-H", ldapURL, "-x", 520 | "-b", serverBaseDN, "-D", "cn=testy,"+serverBaseDN, "-w", "iLike2test") 521 | out, _ = cmd.CombinedOutput() 522 | if strings.Contains(string(out), "dn: cn=hamburger,o=testers,c=testz") { 523 | t.Errorf("ldapsearch without control failed: %v", string(out)) 524 | } 525 | if !strings.Contains(string(out), "result: 0 Success") { 526 | t.Errorf("ldapsearch without control failed: %v", string(out)) 527 | } 528 | if !strings.Contains(string(out), "numResponses: 1") { 529 | t.Errorf("ldapsearch without control failed: %v", string(out)) 530 | } 531 | 532 | done <- true 533 | }() 534 | 535 | select { 536 | case <-done: 537 | case <-time.After(timeout): 538 | t.Errorf("ldapsearch command timed out") 539 | } 540 | quit <- true 541 | } 542 | --------------------------------------------------------------------------------