diff options
| -rw-r--r-- | CONTRIBUTORS.md | 15 | ||||
| -rw-r--r-- | README.md | 59 | ||||
| -rw-r--r-- | messages.go | 3 | ||||
| -rw-r--r-- | server.go | 31 | ||||
| -rw-r--r-- | server_test.go | 135 |
5 files changed, 216 insertions, 27 deletions
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..0af02ed --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,15 @@ +# Client library + +- [mmitton](https://github.com/mmitton) +- [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) +- [nmcclain](https://github.com/nmcclain) + +# Server library + +- [nmcclain](https://github.com/nmcclain) +- [mark-rushakoff](https://github.com/mark-rushakoff) +- [metala](https://github.com/metala) @@ -4,12 +4,12 @@ This library provides basic LDAP v3 functionality for the GO programming languag 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). -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. - +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. ## LDAP client notes: ### A simple LDAP bind operation: + ```go l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) // be sure to add error checking! @@ -23,6 +23,7 @@ if err==nil { ``` ### A simple LDAP search operation: + ```go search := &SearchRequest{ BaseDN: "dc=example,dc=com", @@ -33,25 +34,28 @@ searchResults, err := l.Search(search) ``` ### Implemented: -* Connecting, binding to LDAP server -* Searching for entries with filtering and paging controls -* Compiling string filters to LDAP filters -* Modify Requests / Responses + +- Connecting, binding to LDAP server +- Searching for entries with filtering and paging controls +- Compiling string filters to LDAP filters +- Modify Requests / Responses ### Not implemented: -* Add, Delete, Modify DN, Compare operations -* Most tests / benchmarks + +- Add, Delete, Modify DN, Compare operations +- Most tests / benchmarks ### LDAP client examples: -* examples/search.go: **Basic client bind and search** -* examples/searchSSL.go: **Client bind and search over SSL** -* examples/searchTLS.go: **Client bind and search over TLS** -* examples/modify.go: **Client modify operation** -*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)* +- examples/search.go: **Basic client bind and search** +- examples/searchSSL.go: **Client bind and search over SSL** +- examples/searchTLS.go: **Client bind and search over TLS** +- examples/modify.go: **Client modify operation** ## LDAP server notes: -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: + +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: + ```go type Binder interface { Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) @@ -65,6 +69,7 @@ type Closer interface { ``` ### A basic bind-only LDAP server + ```go func main() { s := ldap.NewServer() @@ -84,22 +89,28 @@ func (h ldapHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldap.LDAP } ``` -* 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. +- 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. +- Server.TLSConfig: If you set this variable, you will enable TLS connection upgrades. +- Server.EnforceTLS: This setting enforces and requires a TLS upgrade before any LDAP operation. ### LDAP server examples: -* examples/server.go: **Basic LDAP authentication (bind and search only)** -* examples/proxy.go: **Simple LDAP proxy server.** -* server_test.go: **The _test.go files have examples of all server functions.** + +- examples/server.go: **Basic LDAP authentication (bind and search only)** +- examples/proxy.go: **Simple LDAP proxy server.** +- server_test.go: **The \_test.go files have examples of all server functions.** ### Known limitations: -* 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. +- 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. ### Not implemented: + From the server perspective, all of [RFC4510](http://tools.ietf.org/html/rfc4510) is implemented **except**: -* 4.5.1.3. SearchRequest.derefAliases -* 4.5.1.5. SearchRequest.timeLimit -* 4.5.1.6. SearchRequest.typesOnly -* 4.14. StartTLS Operation -*Server library by: [nmcclain](https://github.com/nmcclain)* +- 4.5.1.3. SearchRequest.derefAliases +- 4.5.1.5. SearchRequest.timeLimit +- 4.5.1.6. SearchRequest.typesOnly + +## Contributors + +See: [CONTRIBUTORS.md](CONTRIBUTORS.md) diff --git a/messages.go b/messages.go new file mode 100644 index 0000000..a0d1450 --- /dev/null +++ b/messages.go @@ -0,0 +1,3 @@ +package ldap + +const errorEnforceTLSRequiresTLSConfig = "EnforceTLS requires TLSConfig set" @@ -2,6 +2,7 @@ package ldap import ( "crypto/tls" + "errors" "io" "log" "net" @@ -60,7 +61,8 @@ type Server struct { Stats *Stats // If set, server will accept StartTLS. - TLSConfig *tls.Config + TLSConfig *tls.Config + EnforceTLS bool closing chan struct{} } @@ -187,6 +189,10 @@ func (server *Server) ListenAndServe(listenString string) error { } func (server *Server) Serve(ln net.Listener) error { + if server.TLSConfig == nil && server.EnforceTLS { + return errors.New(errorEnforceTLSRequiresTLSConfig) + } + newConn := make(chan net.Conn) go func() { for { @@ -262,6 +268,19 @@ handler: } } + // Enforce TLS + switch conn.(type) { + case *tls.Conn: + default: + if server.EnforceTLS && req.Tag != ApplicationExtendedRequest { + responsePacket := encodeLDAPResponse(messageID, ApplicationExtendedResponse, LDAPResultProtocolError, "Upgrade to TLS is required") + if err = sendPacket(conn, responsePacket); err != nil { + log.Printf("sendPacket error %s", err.Error()) + } + break handler + } + } + //log.Printf("DEBUG: handling operation: %s [%d]", ApplicationMap[req.Tag], req.Tag) //ber.PrintPacket(packet) // DEBUG @@ -318,8 +337,12 @@ handler: } var ldapResultCode LDAPResultCode if tlsConn == nil { - // Wasn't an upgrade. Pass through. - ldapResultCode = HandleExtendedRequest(req, boundDN, server.ExtendedFns, conn) + // Wasn't an upgrade. + if server.EnforceTLS { + ldapResultCode = LDAPResultProtocolError + } else { + ldapResultCode = HandleExtendedRequest(req, boundDN, server.ExtendedFns, conn) + } } else { ldapResultCode = LDAPResultSuccess } @@ -330,6 +353,8 @@ handler: } if tlsConn != nil { conn = tlsConn + } else if server.EnforceTLS { + break handler } case ApplicationAbandonRequest: HandleAbandonRequest(req, boundDN, server.AbandonFns, conn) diff --git a/server_test.go b/server_test.go index 9b9d579..9cde9d7 100644 --- a/server_test.go +++ b/server_test.go @@ -230,6 +230,141 @@ which is very heavy-handed for a test like this. } } +func TestEnforcedTLSWithoutTLSConfig(t *testing.T) { + s := NewServer() + defer s.Close() + s.EnforceTLS = true + s.Bind = BindAnonOK + s.Search = SearchSimple + + ln, _ := mustListen() + done := make(chan error) + go func() { + if err := s.Serve(ln); err != nil { + done <- err + } + }() + + select { + case err := <-done: + msg := err.Error() + if msg != errorEnforceTLSRequiresTLSConfig { + t.Errorf("Unexpected server error: %s", msg) + } + case <-time.After(timeout): + t.Error("server did not return an error") + } +} +func TestEnforcedTLS(t *testing.T) { + if runtime.GOOS == "darwin" { + defer func() { + if t.Failed() { + t.Logf(`NOTE: this test won't pass with the built-in Mac ldap utilities. +Work around this by using brew install openldap, and running the test as PATH=/usr/local/opt/openldap/bin:$PATH go test. + +This test uses environment variables that are respected by OpenLDAP, but the Mac utilities don't let you override +security settings through environment variables; they expect certificates to be added to the system keychain, +which is very heavy-handed for a test like this. +`) + } + }() + } + cert := newSelfSignedCert() + defer cert.cleanup() + + s := NewServer() + defer s.Close() + s.EnforceTLS = true + s.Bind = BindAnonOK + s.Search = SearchSimple + s.TLSConfig = cert.ServerTLSConfig() + + ln, addr := mustListen() + go func() { + if err := s.Serve(ln); err != nil { + t.Errorf("s.Serve failed: %s", err.Error()) + } + }() + + done := make(chan struct{}) + go func() { + cmd := exec.Command("env", + "LDAPTLS_CACERT="+cert.CACertPath, + "ldapsearch", "-H", "ldap://"+addr, "-ZZ", "-d", "-1", "-x", "-b", "o=testers,c=test") + out, err := cmd.CombinedOutput() + if err != nil { + t.Error(err) + } + + if !strings.Contains(string(out), "# numEntries: 3") || !strings.Contains(string(out), "result: 0 Success") { + t.Errorf("search did not succeed:\n%s", out) + } + + close(done) + }() + + select { + case <-done: + case <-time.After(timeout): + t.Error("ldapsearch command timed out") + } +} + +func TestEnforcedTLSFail(t *testing.T) { + if runtime.GOOS == "darwin" { + defer func() { + if t.Failed() { + t.Logf(`NOTE: this test won't pass with the built-in Mac ldap utilities. +Work around this by using brew install openldap, and running the test as PATH=/usr/local/opt/openldap/bin:$PATH go test. + +This test uses environment variables that are respected by OpenLDAP, but the Mac utilities don't let you override +security settings through environment variables; they expect certificates to be added to the system keychain, +which is very heavy-handed for a test like this. +`) + } + }() + } + cert := newSelfSignedCert() + defer cert.cleanup() + + s := NewServer() + defer s.Close() + s.EnforceTLS = true + s.Bind = BindAnonOK + s.Search = SearchSimple + s.TLSConfig = cert.ServerTLSConfig() + + ln, addr := mustListen() + go func() { + if err := s.Serve(ln); err != nil { + t.Errorf("s.Serve failed: %s", err.Error()) + } + }() + + done := make(chan struct{}) + go func() { + cmd := exec.Command("env", + "LDAPTLS_CACERT="+cert.CACertPath, + "ldapsearch", "-H", "ldap://"+addr, "-d", "-1", "-x", "-b", "o=testers,c=test") + out, err := cmd.CombinedOutput() + if err == nil { + t.Error("search should have failed") + } + + if strings.Contains(string(out), "result: 0 Success") { + t.Errorf("search did succeed:\n%s", out) + } + + close(done) + }() + + select { + case <-done: + case <-time.After(timeout): + t.Error("ldapsearch command timed out") + } +} + ///////////////////////// func TestBindAnonOK(t *testing.T) { done := make(chan bool) |
