├── .gitignore ├── LICENSE ├── README.md ├── godap.go ├── godap_test.go ├── simplesearch.go └── tls.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Brad Peabody 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | godap 2 | ===== 3 | 4 | A minimalistic LDAP server in Go 5 | 6 | What is this thing? 7 | ------------------- 8 | 9 | This is the beginnings of LDAP serving functionality written in Go. It is not intended 10 | to be or become a full LDAP server, it's just minimal bind and simplistic search functionality. 11 | It aims to be enough to implement authentication-related LDAP services for use with Apache 12 | httpd or anything else that supports LDAP auth. 13 | 14 | Theoretically more could be built out to approach the functionality of 15 | a full LDAP server. I don't have time for that stuff. 16 | 17 | Why was this made? 18 | ------------------ 19 | 20 | Because the road to hell is paved with good intentions. 21 | 22 | The short version of the story goes like this: 23 | I hate LDAP. I used to love it. But I loved it for all the wrong reasons. 24 | LDAP is supported as an authentication solution by many different pieces of 25 | software. Aside from its de jure standard status, its wide deployment 26 | cements it as a de facto standard as well. 27 | 28 | However, just because it is a standard doesn't mean it is a great idea. 29 | 30 | I'll admit that given its age LDAP has had a good run. I'm sure its 31 | authors carefully considered how to construct the protocol and chose 32 | ASN.1 and its encoding with all of wellest of well meaning intentions. 33 | 34 | The trouble is that with today's Internet, LDAP is just a pain in the ass. 35 | You can't call it from your browser. It's not human readable or easy 36 | to debug. Tooling is often arcane and confusing. It's way more complicated 37 | than what is needed for most simple authentication-only uses. (Yes, I know 38 | there are many other uses than authentication - but it's often too complicated 39 | for those too.) 40 | 41 | Likely owing to the complexity of the protocol, there seems to be virtually 42 | no easy to use library to implement the server side of the LDAP protocol 43 | that isn't tied in with some complete directory server system; and certainly 44 | not in a language as easy to "make it work" as Go. 45 | 46 | So this means that if you are a web developer and you have a database table 47 | with usernames and (hopefully properly salted and hashed) passwords in it, and you 48 | have a third party application that supports LDAP authentication, you 49 | can't easily use your own user data source and just make it "speak LDAP" 50 | to this other application. 51 | 52 | Well, this project provides the basic guts to make that work. 53 | Have a look at the test file for an example of what to do. 54 | 55 | In a way, with this project I embrace LDAP. In the sense that a wrestler 56 | embraces the neck of his opponent to form a headlock, and the unruly 57 | brute is muscled into submission. 58 | 59 | Dependencies 60 | ------------ 61 | 62 | ASN.1+BER encoding and decoding is done with this: https://github.com/go-asn1-ber/asn1-ber 63 | 64 | It works, it's cool. 65 | 66 | Disclaimers 67 | ----------- 68 | 69 | 1. This thing is still fairly rough. Haven't had time to polish it. But I 70 | wanted to get it up on Github in case anyone else was interested. I've 71 | been searching for something like this off and on for a while, finally 72 | got around to writing it. 73 | 74 | 2. It's not impossible that in some places I'm violating pieces of the 75 | LDAP spec. My goal with this project was to get it so LDAP can be 76 | useful as an authentication mechanism on top of other 77 | non-directory data sources. It does that, I'm happy. 78 | Pull requests welcome to fix things that aren't perfect. License 79 | is MIT so feel free to do whatever you want with the code. 80 | -------------------------------------------------------------------------------- /godap.go: -------------------------------------------------------------------------------- 1 | package godap 2 | 3 | // our minimalistic LDAP server that translates LDAP bind requests 4 | // into a simple callback function 5 | 6 | import ( 7 | "fmt" 8 | "github.com/go-asn1-ber/asn1-ber" 9 | // "io/ioutil" 10 | "log" 11 | "net" 12 | "time" 13 | ) 14 | 15 | // lame, but simple - set to true when you want log output 16 | var LDAPDebug = false 17 | 18 | func ldapdebug(format string, v ...interface{}) { 19 | if LDAPDebug { 20 | log.Printf(format, v...) 21 | } 22 | } 23 | 24 | // Handles socket interaction and a chain of handlers 25 | type LDAPServer struct { 26 | Listener net.Listener 27 | Handlers []LDAPRequestHandler 28 | } 29 | 30 | // listens and runs a plain (non-TLS) LDAP server on the address:port specified 31 | func (s *LDAPServer) ListenAndServe(addr string) error { 32 | ln, err := net.Listen("tcp", addr) 33 | if err != nil { 34 | return err 35 | } 36 | s.Listener = ln 37 | return s.Serve() 38 | } 39 | 40 | // something that the handlers can use to keep track of stuff across multiple requests in the same connection/session 41 | type LDAPSession struct { 42 | Attributes map[string]interface{} 43 | } 44 | 45 | // serves an ldap server on the listener specified in the LDAPServer struct 46 | func (s *LDAPServer) Serve() error { 47 | for { 48 | conn, err := s.Listener.Accept() 49 | if err != nil { 50 | return err 51 | } 52 | go func(conn net.Conn) { 53 | 54 | // catch and report panics - we don't want it to crash the server 55 | defer func() { 56 | if r := recover(); r != nil { 57 | log.Printf("Caught panic while serving request (conn=%v): %v", conn, r) 58 | } 59 | }() 60 | 61 | defer conn.Close() 62 | 63 | // keep the connection around for a minute 64 | conn.SetDeadline(time.Now().Add(time.Minute)) 65 | 66 | ssn := 67 | &LDAPSession{ 68 | Attributes: make(map[string]interface{}), 69 | } 70 | 71 | for { 72 | 73 | // read a request 74 | p, err := ber.ReadPacket(conn) 75 | if err != nil { 76 | ldapdebug("Error while reading packet: %v", err) 77 | return 78 | } 79 | 80 | handled := false 81 | for _, h := range s.Handlers { 82 | retplist := h.ServeLDAP(ssn, p) 83 | if len(retplist) > 0 { 84 | 85 | ldapdebug("Got LDAP response(s) from handler, writing it/them back to client") 86 | 87 | for _, retp := range retplist { 88 | 89 | b := retp.Bytes() 90 | 91 | _, err = conn.Write(b) 92 | if err != nil { 93 | ldapdebug("Error while writing response packet: %v", err) 94 | return 95 | } 96 | } 97 | 98 | handled = true 99 | break 100 | } 101 | } 102 | 103 | if !handled { 104 | 105 | if IsUnbindRequest(p) { 106 | ldapdebug("Got unbind request, closing connection") 107 | return 108 | } 109 | 110 | ldapdebug("Unhandled packet, closing connection: %v", p) 111 | if LDAPDebug { 112 | ber.PrintPacket(p) 113 | // ioutil.WriteFile("tmpdump.dat", p.Bytes(), 0644) 114 | } 115 | // TODO: in theory, we should be sending a "Notice of Disconnection" 116 | // here, in practice I don't know that it matters 117 | return 118 | } 119 | 120 | // loop back around and wait for another 121 | 122 | } 123 | 124 | }(conn) 125 | } 126 | return nil 127 | } 128 | 129 | // processes a request, or not 130 | type LDAPRequestHandler interface { 131 | // read a packet and return one or more packets as a response 132 | // or nil/empty to indicate we don't want to handle this packet 133 | ServeLDAP(*LDAPSession, *ber.Packet) []*ber.Packet 134 | } 135 | 136 | type LDAPResultCodeHandler struct { 137 | ReplyTypeId int64 // the overall type of the response, e.g. 1 is BindResponse - it must be a response that is just a result code 138 | ResultCode int64 // the result code, i.e. 0 is success, 49 is invalid credentials, etc. 139 | } 140 | 141 | func (h *LDAPResultCodeHandler) ServeLDAP(ssn *LDAPSession, p *ber.Packet) []*ber.Packet { 142 | 143 | // extract message ID... 144 | messageId, err := ExtractMessageId(p) 145 | if err != nil { 146 | ldapdebug("Unable to extract message id: %v", err) 147 | return nil 148 | } 149 | 150 | replypacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response") 151 | replypacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageId, "MessageId")) 152 | bindResult := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ber.Tag(h.ReplyTypeId), nil, "Response") 153 | bindResult.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, h.ResultCode, "Result Code")) 154 | // per the spec these are "matchedDN" and "diagnosticMessage", but we don't need them for this 155 | bindResult.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "Unused")) 156 | bindResult.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "Unused")) 157 | replypacket.AppendChild(bindResult) 158 | 159 | return []*ber.Packet{replypacket} 160 | 161 | } 162 | 163 | // function that checks simple auth credentials (username/password style) 164 | type LDAPBindFunc func(binddn string, bindpw []byte) bool 165 | 166 | // responds to bind requests 167 | type LDAPBindFuncHandler struct { 168 | LDAPBindFunc LDAPBindFunc 169 | } 170 | 171 | func (h *LDAPBindFuncHandler) ServeLDAP(ssn *LDAPSession, p *ber.Packet) []*ber.Packet { 172 | 173 | reth := &LDAPResultCodeHandler{ReplyTypeId: 1, ResultCode: 49} 174 | 175 | // this is really just part of the message validation - we don't need the message id here 176 | _, err := ExtractMessageId(p) 177 | if err != nil { 178 | ldapdebug("Unable to extract message id: %v", err) 179 | return nil 180 | } 181 | 182 | // check for bind request contents 183 | err = CheckPacket(p.Children[1], ber.ClassApplication, ber.TypeConstructed, 0x0) 184 | if err != nil { 185 | //ldapdebug("Not a bind request: %v", err) 186 | return nil 187 | } 188 | 189 | // this should be ldap version 190 | err = CheckPacket(p.Children[1].Children[0], ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger) 191 | if err != nil { 192 | ldapdebug("Error trying to read the ldap version: %v", err) 193 | return nil 194 | } 195 | 196 | // check ldap version number 197 | ldapv := ForceInt64(p.Children[1].Children[0].Value) 198 | if ldapv < 2 { 199 | ldapdebug("LDAP version too small - should be >= 2 but was: %v", ldapv) 200 | return nil 201 | } 202 | 203 | // make sure we have at least our version number, bind dn and bind password 204 | if len(p.Children[1].Children) < 3 { 205 | ldapdebug("At least 3 elements required in bind request, found %v", len(p.Children[1].Children)) 206 | return nil 207 | } 208 | 209 | // should be the bind DN (the "username") - note that this will fail if it's SASL auth 210 | err = CheckPacket(p.Children[1].Children[1], ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString) 211 | if err != nil { 212 | ldapdebug("Error verifying packet: %v", err) 213 | return nil 214 | } 215 | myBindDn := string(p.Children[1].Children[1].ByteValue) 216 | // fmt.Printf("myBindDn: %v\n", myBindDn) 217 | 218 | err = CheckPacket(p.Children[1].Children[2], ber.ClassContext, ber.TypePrimitive, 0x0) 219 | if err != nil { 220 | ldapdebug("Error verifying packet: %v", err) 221 | return nil 222 | } 223 | 224 | myBindPw := p.Children[1].Children[2].Data.Bytes() 225 | // fmt.Printf("myBindPw: %v\n", myBindPw) 226 | 227 | // call back to the auth handler 228 | if h.LDAPBindFunc(myBindDn, myBindPw) { 229 | // it worked, result code should be zero 230 | reth.ResultCode = 0 231 | } 232 | 233 | return reth.ServeLDAP(ssn, p) 234 | 235 | } 236 | 237 | // check if this is an unbind 238 | func IsUnbindRequest(p *ber.Packet) bool { 239 | // message validation 240 | _, err := ExtractMessageId(p) 241 | if err != nil { 242 | ldapdebug("Unable to extract message id: %v", err) 243 | return false 244 | } 245 | err = CheckPacket(p.Children[1], ber.ClassApplication, ber.TypePrimitive, 0x2) 246 | if err != nil { 247 | return false 248 | } 249 | return true 250 | } 251 | 252 | func ExtractMessageId(p *ber.Packet) (int64, error) { 253 | 254 | // check overall packet header 255 | err := CheckPacket(p, ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence) 256 | if err != nil { 257 | return -1, err 258 | } 259 | 260 | // check type of message id 261 | err = CheckPacket(p.Children[0], ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger) 262 | if err != nil { 263 | return -1, err 264 | } 265 | 266 | // message id 267 | messageId := ForceInt64(p.Children[0].Value) 268 | 269 | return messageId, nil 270 | } 271 | 272 | func CheckPacket(p *ber.Packet, cl ber.Class, ty ber.Type, ta ber.Tag) error { 273 | if p.ClassType != cl { 274 | return fmt.Errorf("Incorrect class, expected %v but got %v", cl, p.ClassType) 275 | } 276 | if p.TagType != ty { 277 | return fmt.Errorf("Incorrect type, expected %v but got %v", cl, p.TagType) 278 | } 279 | if p.Tag != ta { 280 | return fmt.Errorf("Incorrect tag, expected %v but got %v", cl, p.Tag) 281 | } 282 | return nil 283 | } 284 | 285 | func ForceInt64(v interface{}) int64 { 286 | switch v := v.(type) { 287 | case int64: 288 | return v 289 | case uint64: 290 | return int64(v) 291 | case int32: 292 | return int64(v) 293 | case uint32: 294 | return int64(v) 295 | case int: 296 | return int64(v) 297 | case byte: 298 | return int64(v) 299 | default: 300 | panic(fmt.Sprintf("ForceInt64() doesn't understand values of type: %t", v)) 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /godap_test.go: -------------------------------------------------------------------------------- 1 | package godap 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | // test a very simple LDAP server with hard coded bind and search results 11 | func TestLdapServer1(t *testing.T) { 12 | 13 | hs := make([]LDAPRequestHandler, 0) 14 | 15 | // use a LDAPBindFuncHandler to provide a callback function to respond 16 | // to bind requests 17 | hs = append(hs, &LDAPBindFuncHandler{LDAPBindFunc: func(binddn string, bindpw []byte) bool { 18 | if strings.HasPrefix(binddn, "cn=Joe Dimaggio,") && string(bindpw) == "marylinisthebomb" { 19 | return true 20 | } 21 | return false 22 | }}) 23 | 24 | // use a LDAPSimpleSearchFuncHandler to reply to search queries 25 | hs = append(hs, &LDAPSimpleSearchFuncHandler{LDAPSimpleSearchFunc: func(req *LDAPSimpleSearchRequest) []*LDAPSimpleSearchResultEntry { 26 | 27 | ret := make([]*LDAPSimpleSearchResultEntry, 0, 1) 28 | 29 | // here we produce a single search result that matches whatever 30 | // they are searching for 31 | if req.FilterAttr == "uid" { 32 | ret = append(ret, &LDAPSimpleSearchResultEntry{ 33 | DN: "cn=" + req.FilterValue + "," + req.BaseDN, 34 | Attrs: map[string]interface{}{ 35 | "sn": req.FilterValue, 36 | "cn": req.FilterValue, 37 | "uid": req.FilterValue, 38 | "homeDirectory": "/home/" + req.FilterValue, 39 | "objectClass": []string{ 40 | "top", 41 | "posixAccount", 42 | "inetOrgPerson", 43 | }, 44 | }, 45 | }) 46 | } 47 | 48 | return ret 49 | 50 | }}) 51 | 52 | s := &LDAPServer{ 53 | Handlers: hs, 54 | } 55 | 56 | go s.ListenAndServe("127.0.0.1:10000") 57 | 58 | // yeah, you gotta have ldapsearch (from openldap) installed; but if you're 59 | // serious about hurting yourself with ldap, you've already done this 60 | b, err := exec.Command("/usr/bin/ldapsearch", 61 | `-H`, 62 | `ldap://127.0.0.1:10000/`, 63 | `-Dcn=Joe Dimaggio,dc=example,dc=net`, 64 | `-wmarylinisthebomb`, 65 | `-v`, 66 | `-bou=people,dc=example,dc=net`, 67 | `(uid=jfk)`, 68 | ).CombinedOutput() 69 | fmt.Printf("RESULT1: %s\n", string(b)) 70 | if err != nil { 71 | t.Fatalf("Error executing: %v", err) 72 | } 73 | 74 | bstr := string(b) 75 | 76 | if !strings.Contains(bstr, "dn: cn=jfk,ou=people,dc=example,dc=net") { 77 | t.Fatalf("Didn't find expected result string") 78 | } 79 | if !strings.Contains(bstr, "numEntries: 1") { 80 | t.Fatalf("Should have found exactly one result") 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /simplesearch.go: -------------------------------------------------------------------------------- 1 | package godap 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-asn1-ber/asn1-ber" 7 | ) 8 | 9 | // a simplified ldap search request 10 | type LDAPSimpleSearchRequest struct { 11 | Packet *ber.Packet 12 | BaseDN string // DN under which to start searching 13 | Scope int64 // baseObject(0), singleLevel(1), wholeSubtree(2) 14 | DerefAliases int64 // neverDerefAliases(0),derefInSearching(1),derefFindingBaseObj(2),derefAlways(3) 15 | SizeLimit int64 // max number of results to return 16 | TimeLimit int64 // max time in seconds to spend processing 17 | TypesOnly bool // if true client is expecting only type info 18 | FilterAttr string // filter attribute name (assumed to be an equality match with just this one attribute) 19 | FilterValue string // filter attribute value 20 | } 21 | 22 | var ErrNotASearchRequest = fmt.Errorf("not a search request") 23 | var ErrSearchRequestTooComplex = fmt.Errorf("this search request is too complex to be parsed as a 'simple search'") 24 | 25 | func ParseLDAPSimpleSearchRequestPacket(p *ber.Packet) (*LDAPSimpleSearchRequest, error) { 26 | 27 | ret := &LDAPSimpleSearchRequest{} 28 | 29 | if len(p.Children) < 2 { 30 | return nil, ErrNotASearchRequest 31 | } 32 | 33 | err := CheckPacket(p.Children[1], ber.ClassApplication, ber.TypeConstructed, 0x3) 34 | if err != nil { 35 | return nil, ErrNotASearchRequest 36 | } 37 | 38 | rps := p.Children[1].Children 39 | 40 | ret.BaseDN = string(rps[0].ByteValue) 41 | ret.Scope = ForceInt64(rps[1].Value) 42 | ret.DerefAliases = ForceInt64(rps[2].Value) 43 | ret.SizeLimit = ForceInt64(rps[3].Value) 44 | ret.TimeLimit = ForceInt64(rps[4].Value) 45 | ret.TypesOnly = rps[5].Value.(bool) 46 | 47 | // Check to see if it looks like a simple search criteria 48 | err = CheckPacket(rps[6], ber.ClassContext, ber.TypeConstructed, 0x3) 49 | if err == nil { 50 | // It is, return the attribute and value 51 | ret.FilterAttr = string(rps[6].Children[0].ByteValue) 52 | ret.FilterValue = string(rps[6].Children[1].ByteValue) 53 | } else { 54 | // This is likely some sort of complex search criteria. 55 | // Try to generate a searchFingerPrint based on the values 56 | // You will have to understand this fingerprint in your code 57 | var getContextValue func(p *ber.Packet) string 58 | getContextValue = func(p *ber.Packet) string { 59 | ret := "" 60 | if p.Value != nil { 61 | ret = fmt.Sprint(p.Value) 62 | } 63 | for _, child := range p.Children { 64 | childVal := getContextValue(child) 65 | if childVal != "" { 66 | if ret != "" { 67 | ret += "," 68 | } 69 | ret += childVal 70 | } 71 | } 72 | return ret 73 | } 74 | 75 | ret.FilterAttr = "searchFingerprint" 76 | ret.FilterValue = getContextValue(rps[6]) 77 | for index := 7; index < len(rps); index++ { 78 | value := getContextValue(rps[index]) 79 | if value != "" { 80 | if ret.FilterValue != "" { 81 | ret.FilterValue += "," 82 | } 83 | ret.FilterValue += value 84 | } 85 | } 86 | 87 | } 88 | 89 | return ret, nil 90 | 91 | } 92 | 93 | // a simplified ldap search response 94 | type LDAPSimpleSearchResultEntry struct { 95 | DN string // DN of this search result 96 | Attrs map[string]interface{} // map of attributes 97 | } 98 | 99 | func (e *LDAPSimpleSearchResultEntry) MakePacket(msgid int64) *ber.Packet { 100 | 101 | messageId := msgid 102 | 103 | replypacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response") 104 | replypacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageId, "MessageId")) 105 | searchResult := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ber.Tag(4), nil, "Response") 106 | searchResult.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, e.DN, "DN")) 107 | attrs := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attrs") 108 | 109 | for k, v := range e.Attrs { 110 | 111 | attr := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attr") 112 | attr.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, k, "Key")) 113 | attrvals := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "Values") 114 | 115 | switch v := v.(type) { 116 | case string: 117 | attrvals.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, v, "String Value")) 118 | case []string: 119 | for _, v1 := range v { 120 | attrvals.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, v1, "String Value (from slice)")) 121 | } 122 | default: 123 | ldapdebug("skipping value for key '%s' because I can't process type '%t'", k, v) 124 | continue 125 | } 126 | 127 | attr.AppendChild(attrvals) 128 | attrs.AppendChild(attr) 129 | 130 | } 131 | searchResult.AppendChild(attrs) 132 | 133 | replypacket.AppendChild(searchResult) 134 | 135 | return replypacket 136 | 137 | } 138 | 139 | func MakeLDAPSearchResultDonePacket(msgid int64) *ber.Packet { 140 | 141 | messageId := msgid 142 | 143 | replypacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response") 144 | replypacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageId, "MessageId")) 145 | searchResult := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ber.Tag(5), nil, "Response") 146 | searchResult.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, 0, "Result Code")) 147 | // per the spec these are "matchedDN" and "diagnosticMessage", but we don't need them for this 148 | searchResult.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "Unused")) 149 | searchResult.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "Unused")) 150 | replypacket.AppendChild(searchResult) 151 | 152 | return replypacket 153 | 154 | } 155 | 156 | func MakeLDAPSearchResultNoSuchObjectPacket(msgid int64) *ber.Packet { 157 | 158 | messageId := msgid 159 | 160 | replypacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response") 161 | replypacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageId, "MessageId")) 162 | searchResult := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ber.Tag(5), nil, "Response") 163 | // 32 is "noSuchObject" 164 | searchResult.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, 32, "Result Code")) 165 | // per the spec these are "matchedDN" and "diagnosticMessage", but we don't need them for this 166 | searchResult.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "Unused")) 167 | searchResult.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "Unused")) 168 | replypacket.AppendChild(searchResult) 169 | 170 | return replypacket 171 | 172 | } 173 | 174 | // a callback function to produce search results; should return nil to mean 175 | // we chose not to attempt to search (i.e. this request is not for us); 176 | // or return empty slice to mean 0 results (or slice with data for results) 177 | type LDAPSimpleSearchFunc func(*LDAPSimpleSearchRequest) []*LDAPSimpleSearchResultEntry 178 | 179 | type LDAPSimpleSearchFuncHandler struct { 180 | LDAPSimpleSearchFunc LDAPSimpleSearchFunc 181 | } 182 | 183 | func (h *LDAPSimpleSearchFuncHandler) ServeLDAP(ssn *LDAPSession, p *ber.Packet) []*ber.Packet { 184 | 185 | req, err := ParseLDAPSimpleSearchRequestPacket(p) 186 | if err == ErrNotASearchRequest { 187 | return nil 188 | } else if err == ErrSearchRequestTooComplex { 189 | ldapdebug("Search request too complex, skipping") 190 | return nil 191 | } else if err != nil { 192 | ldapdebug("Error while trying to parse search request: %v", err) 193 | return nil 194 | } 195 | 196 | res := h.LDAPSimpleSearchFunc(req) 197 | 198 | // the function is telling us it is opting not to process this search request 199 | if res == nil { 200 | return nil 201 | } 202 | 203 | msgid, err := ExtractMessageId(p) 204 | if err != nil { 205 | ldapdebug("Failed to extract message id") 206 | return nil 207 | } 208 | 209 | // no results 210 | if len(res) < 1 { 211 | return []*ber.Packet{MakeLDAPSearchResultNoSuchObjectPacket(msgid)} 212 | } 213 | 214 | // format each result 215 | ret := make([]*ber.Packet, 0) 216 | for _, resitem := range res { 217 | resultPacket := resitem.MakePacket(msgid) 218 | // fmt.Printf("--------------------\n") 219 | // ber.PrintPacket(resultPacket) 220 | ret = append(ret, resultPacket) 221 | } 222 | 223 | // end with a done packet 224 | ret = append(ret, MakeLDAPSearchResultDonePacket(msgid)) 225 | 226 | return ret 227 | 228 | } 229 | -------------------------------------------------------------------------------- /tls.go: -------------------------------------------------------------------------------- 1 | package godap 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "time" 7 | ) 8 | 9 | func LDAPListenTLS(listenAddr, certFile, keyFile string) (net.Listener, error) { 10 | config := &tls.Config{} 11 | // if config.NextProtos == nil { 12 | // config.NextProtos = []string{"http/1.1"} 13 | // } 14 | 15 | var err error 16 | config.Certificates = make([]tls.Certificate, 1) 17 | config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | ln, err := net.Listen("tcp", listenAddr) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | tlsListener := tls.NewListener(ldapTcpKeepAliveListener{ln.(*net.TCPListener)}, config) 28 | return tlsListener, nil 29 | } 30 | 31 | // ldapTcpKeepAliveListener sets TCP keep-alive timeouts on accepted 32 | // connections. 33 | type ldapTcpKeepAliveListener struct { 34 | *net.TCPListener 35 | } 36 | 37 | func (ln ldapTcpKeepAliveListener) Accept() (c net.Conn, err error) { 38 | tc, err := ln.AcceptTCP() 39 | if err != nil { 40 | return 41 | } 42 | tc.SetKeepAlive(true) 43 | tc.SetKeepAlivePeriod(3 * time.Minute) 44 | return tc, nil 45 | } 46 | --------------------------------------------------------------------------------