A LDAPv3 server framework for custom integrations or full-blown LDAP servers, with no external dependencies. Focus on the logic of your integration and forget the low-level details.
go get github.com/merlinz01/ldapserver
I needed to integrate an LDAP-compatible web app with my website database's stored login information. I didn't want to try to remotely manipulate an off-the-shelf LDAP server from my website's code, so a custom LDAP server was what I was looking for. The existing LDAP server frameworks I found threw obscure errors when I tested them. Being suspicious of the security implications of such errors, I decided to write a new framework, specifically focused on enabling the building of custom integrations.
This package provides an interface similar to that of net/http.
See test/main.go for a working demo implementation.
Create an object implementing the Handler interface.
The recommended way to do this is to define a struct that inherits
the BaseHandler type, which provides default handling
for all methods, and also handles StartTLS extended requests.
type MyHandler struct {
ldapserver.BaseHandler
}
handler := &MyHandler{}Create a LDAPServer object using NewLDAPServer().
server := ldapserver.NewLDAPServer(handler)Provide a key pair to the server using SetupTLS().
err := server.SetupTLS("cert.pem", "key.pem")
if err != nil {
log.Println("Error setting up TLS:", err)
return
}Or you can set/modify the server's TLSConfig field
for more specific configuration.
The server's TLSConfig must not be nil if you want
to support StartTLS or initial TLS.
Use ListenAndServe() for a ldap:// server,
or ListenAndServeTLS() for a ldaps:// server.
server.ListenAndServe(":389")server.ListenAndServeTLS(":636")If you need to shut down the server gracefully,
call its Shutdown() method.
server.Shutdown()To enable more functionality, define your own methods on the handler.
func (h *MyHandler) Bind(conn *ldapserver.Conn, msg *ldapserver.Message, req *ldapserver.BindRequest) {
// Put your authentication logic here
result := &ldapserver.BindResponse{}
result.ResultCode = ldapserver.LDAPResultSuccess
conn.SendResult(result)
}The BaseHandler struct handles the StartTLS extended operation.
If you want to handle other extended operations,
define your own Extended() method.
Use a switch statement to determine which extended operation
to use. For requests you do not handle, simply pass the function
arguments on to the BaseHandler's method, which handles
StartTLS and unsupported requests.
func (h *MyHandler) Extended(conn *ldapserver.Conn, msg *ldapserver.Message, req *ldapserver.ExtendedRequest) {
switch req.Name {
case ldapserver.OIDPasswordModify:
log.Println("Modify password")
// Put your password modify code here
default:
h.BaseHandler.Extended(conn, msg, req)
}
}To return a result, create a Result struct with the desired ResultCode.
For error results, you should also set the DiagnosticMessage field
so that the user knows what sort of error occurred.
Pass the result along with the appropriate response type code
to the connection's SendResult method.
The Bind and Extended requests have their own response types,
BindResponse and ExtendedResponse, that include a Result struct.
These, as well as the SearchResultEntry and SearchResultReference structs,
can also be passed to Conn.SendResult().
The library defines the result codes in RFC4511 with a Result prefix,
e.g. ResultNoSuchObject.
These constants have an AsResult() method that returns a pointer to a Result struct with the given DiagnosticMessage field
that can be passed directly to Conn.SendResult().
To support cancellation of an operation,
the following method is recommended.
See test/main.go for an example.
Add a map and an accompanying mutex to your handler's struct.
type MyHandler struct {
...
abandonment map[ldapserver.MessageID]bool // Don't forget to initialize the map!
abandonmentMutex sync.Mutex
}At the beginning of an cancelable method,
put a flag in the abandonment map to indicate that
the operation can be canceled.
h.abandonment[msg.MessageID] = false // i.e. not cancelled but may be
// Remove the flag when done
defer func() {
t.abandonmentLock.Lock()
delete(t.abandonment, msg.MessageID)
t.abandonmentLock.Unlock()
}()Wherever in the method you want to be able to cancel (e.g. at the beginning/end of a loop), put in the following logic:
...
if t.abandonment[msg.MessageID] {
log.Println("Abandoning operation")
return
}
...Then define your Abandon method like this:
func (t *TestHandler) Abandon(conn *ldapserver.Conn, msg *ldapserver.Message, messageID ldapserver.MessageID) {
t.abandonmentLock.Lock()
// Set the flag only if the messageID is in the map
if _, exists := t.abandonment[messageID]; exists {
t.abandonment[messageID] = true
}
t.abandonmentLock.Unlock()
}The Conn object passed to each request method
has an Authentication field with type any,
for storing implementation-defined authentication info.
See test/main.go for an example.
- TLS support
- Strict protocol validation
- OID validation
- DN parsing support
- Full concurrency ability
- Comprehensive message parsing tests
- Filter stringification
- Abandon request
- Add request (concurrent)
- Bind request
- Compare request (concurrent)
- Delete request (concurrent)
- Extended requests
- Modify request (concurrent)
- ModifyDN request (concurrent)
- Search request (concurrent)
- StartTLS request
- Unbind request
- Intermediate response
- Unsolicited notifications
- Notice of disconnection
- Full conformance to the relevant specifications, e.g. RFC 4511.
- Support for all builtin operations and common extended operations
- Comprehensive encoding/decoding tests
- Strict client data validity checking
- Ease of use
Contributions and bug reports are welcome!