diff --git a/ldapserver/client.go b/ldapserver/client.go new file mode 100644 index 0000000..501af2a --- /dev/null +++ b/ldapserver/client.go @@ -0,0 +1,257 @@ +package ldapserver + +import ( + "bufio" + "net" + "sync" + "time" + + ldap "github.com/vjeantet/goldap/message" +) + +type UserState interface{} + +type client struct { + Numero int + srv *Server + rwc net.Conn + br *bufio.Reader + bw *bufio.Writer + chanOut chan *ldap.LDAPMessage + wg sync.WaitGroup + closing chan bool + requestList map[int]*Message + mutex sync.Mutex + writeDone chan bool + rawData []byte + userState UserState +} + +func (c *client) GetConn() net.Conn { + return c.rwc +} + +func (c *client) GetRaw() []byte { + return c.rawData +} + +func (c *client) SetConn(conn net.Conn) { + c.rwc = conn + c.br = bufio.NewReader(c.rwc) + c.bw = bufio.NewWriter(c.rwc) +} + +func (c *client) GetMessageByID(messageID int) (*Message, bool) { + if requestToAbandon, ok := c.requestList[messageID]; ok { + return requestToAbandon, true + } + return nil, false +} + +func (c *client) Addr() net.Addr { + return c.rwc.RemoteAddr() +} + +func (c *client) ReadPacket() (*messagePacket, error) { + mP, err := readMessagePacket(c.br) + c.rawData = make([]byte, len(mP.bytes)) + copy(c.rawData, mP.bytes) + return mP, err +} + +func (c *client) serve() { + defer c.close() + + c.closing = make(chan bool) + if onc := c.srv.OnNewConnection; onc != nil { + if err := onc(c.rwc); err != nil { + Logger.Printf("Erreur OnNewConnection: %s", err) + return + } + } + + // Create the ldap response queue to be writted to client (buffered to 20) + // buffered to 20 means that If client is slow to handler responses, Server + // Handlers will stop to send more respones + c.chanOut = make(chan *ldap.LDAPMessage) + c.writeDone = make(chan bool) + // for each message in c.chanOut send it to client + go func() { + for msg := range c.chanOut { + c.writeMessage(msg) + } + close(c.writeDone) + }() + + // Listen for server signal to shutdown + go func() { + for { + select { + case <-c.srv.chDone: // server signals shutdown process + c.wg.Add(1) + r := NewExtendedResponse(LDAPResultUnwillingToPerform) + r.SetDiagnosticMessage("server is about to stop") + r.SetResponseName(NoticeOfDisconnection) + + m := ldap.NewLDAPMessageWithProtocolOp(r) + + c.chanOut <- m + c.wg.Done() + c.rwc.SetReadDeadline(time.Now().Add(time.Millisecond)) + return + case <-c.closing: + return + } + } + }() + + c.requestList = make(map[int]*Message) + + for { + + if c.srv.ReadTimeout != 0 { + c.rwc.SetReadDeadline(time.Now().Add(c.srv.ReadTimeout)) + } + if c.srv.WriteTimeout != 0 { + c.rwc.SetWriteDeadline(time.Now().Add(c.srv.WriteTimeout)) + } + + //Read client input as a ASN1/BER binary message + messagePacket, err := c.ReadPacket() + if err != nil { + if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() { + Logger.Printf("Sorry client %d, i can not wait anymore (reading timeout) ! %s", c.Numero, err) + } else { + Logger.Printf("Error readMessagePacket: %s", err) + } + return + } + + //Convert ASN1 binaryMessage to a ldap Message + message, err := messagePacket.readMessage() + + if err != nil { + Logger.Printf("Error reading Message : %s\n\t%x", err.Error(), messagePacket.bytes) + continue + } + Logger.Printf("<<< %d - %s - hex=%x", c.Numero, message.ProtocolOpName(), messagePacket) + + // TODO: Use a implementation to limit runnuning request by client + // solution 1 : when the buffered output channel is full, send a busy + // solution 2 : when 10 client requests (goroutines) are running, send a busy message + // And when the limit is reached THEN send a BusyLdapMessage + + // When message is an UnbindRequest, stop serving + if _, ok := message.ProtocolOp().(ldap.UnbindRequest); ok { + return + } + + // If client requests a startTls, do not handle it in a + // goroutine, connection has to remain free until TLS is OK + // @see RFC https://tools.ietf.org/html/rfc4511#section-4.14.1 + if req, ok := message.ProtocolOp().(ldap.ExtendedRequest); ok { + if req.RequestName() == NoticeOfStartTLS { + c.wg.Add(1) + c.ProcessRequestMessage(&message) + continue + } + } + + // TODO: go/non go routine choice should be done in the ProcessRequestMessage + // not in the client.serve func + c.wg.Add(1) + go c.ProcessRequestMessage(&message) + } + +} + +// close closes client, +// * stop reading from client +// * signals to all currently running request processor to stop +// * wait for all request processor to end +// * close client connection +// * signal to server that client shutdown is ok +func (c *client) close() { + Logger.Printf("client %d close()", c.Numero) + close(c.closing) + + // stop reading from client + c.rwc.SetReadDeadline(time.Now().Add(time.Millisecond)) + Logger.Printf("client %d close() - stop reading from client", c.Numero) + + // signals to all currently running request processor to stop + c.mutex.Lock() + for messageID, request := range c.requestList { + Logger.Printf("Client %d close() - sent abandon signal to request[messageID = %d]", c.Numero, messageID) + go request.Abandon() + } + c.mutex.Unlock() + Logger.Printf("client %d close() - Abandon signal sent to processors", c.Numero) + + c.wg.Wait() // wait for all current running request processor to end + close(c.chanOut) // No more message will be sent to client, close chanOUT + Logger.Printf("client [%d] request processors ended", c.Numero) + + <-c.writeDone // Wait for the last message sent to be written + c.rwc.Close() // close client connection + Logger.Printf("client [%d] connection closed", c.Numero) + + c.srv.wg.Done() // signal to server that client shutdown is ok +} + +func (c *client) writeMessage(m *ldap.LDAPMessage) { + data, _ := m.Write() + Logger.Printf(">>> %d - %s - hex=%x", c.Numero, m.ProtocolOpName(), data.Bytes()) + c.bw.Write(data.Bytes()) + c.bw.Flush() +} + +// ResponseWriter interface is used by an LDAP handler to +// construct an LDAP response. +type ResponseWriter interface { + // Write writes the LDAPResponse to the connection as part of an LDAP reply. + Write(po ldap.ProtocolOp) +} + +type responseWriterImpl struct { + chanOut chan *ldap.LDAPMessage + messageID int +} + +func (w responseWriterImpl) Write(po ldap.ProtocolOp) { + m := ldap.NewLDAPMessageWithProtocolOp(po) + m.SetMessageID(w.messageID) + w.chanOut <- m +} + +func (c *client) ProcessRequestMessage(message *ldap.LDAPMessage) { + defer c.wg.Done() + + var m Message + m = Message{ + LDAPMessage: message, + Done: make(chan bool, 2), + Client: c, + } + + c.registerRequest(&m) + defer c.unregisterRequest(&m) + + var w responseWriterImpl + w.chanOut = c.chanOut + w.messageID = m.MessageID().Int() + + c.srv.Handler.ServeLDAP(c.userState, w, &m) +} + +func (c *client) registerRequest(m *Message) { + c.mutex.Lock() + c.requestList[m.MessageID().Int()] = m + c.mutex.Unlock() +} + +func (c *client) unregisterRequest(m *Message) { + c.mutex.Lock() + delete(c.requestList, m.MessageID().Int()) + c.mutex.Unlock() +} diff --git a/ldapserver/constants.go b/ldapserver/constants.go new file mode 100644 index 0000000..75c632c --- /dev/null +++ b/ldapserver/constants.go @@ -0,0 +1,96 @@ +package ldapserver + +import ldap "github.com/vjeantet/goldap/message" + +// LDAP Application Codes +const ( + ApplicationBindRequest = 0 + ApplicationBindResponse = 1 + ApplicationUnbindRequest = 2 + ApplicationSearchRequest = 3 + ApplicationSearchResultEntry = 4 + ApplicationSearchResultDone = 5 + ApplicationModifyRequest = 6 + ApplicationModifyResponse = 7 + ApplicationAddRequest = 8 + ApplicationAddResponse = 9 + ApplicationDelRequest = 10 + ApplicationDelResponse = 11 + ApplicationModifyDNRequest = 12 + ApplicationModifyDNResponse = 13 + ApplicationCompareRequest = 14 + ApplicationCompareResponse = 15 + ApplicationAbandonRequest = 16 + ApplicationSearchResultReference = 19 + ApplicationExtendedRequest = 23 + ApplicationExtendedResponse = 24 +) + +// LDAP Result Codes +const ( + LDAPResultSuccess = 0 + LDAPResultOperationsError = 1 + LDAPResultProtocolError = 2 + LDAPResultTimeLimitExceeded = 3 + LDAPResultSizeLimitExceeded = 4 + LDAPResultCompareFalse = 5 + LDAPResultCompareTrue = 6 + LDAPResultAuthMethodNotSupported = 7 + LDAPResultStrongAuthRequired = 8 + LDAPResultReferral = 10 + LDAPResultAdminLimitExceeded = 11 + LDAPResultUnavailableCriticalExtension = 12 + LDAPResultConfidentialityRequired = 13 + LDAPResultSaslBindInProgress = 14 + LDAPResultNoSuchAttribute = 16 + LDAPResultUndefinedAttributeType = 17 + LDAPResultInappropriateMatching = 18 + LDAPResultConstraintViolation = 19 + LDAPResultAttributeOrValueExists = 20 + LDAPResultInvalidAttributeSyntax = 21 + LDAPResultNoSuchObject = 32 + LDAPResultAliasProblem = 33 + LDAPResultInvalidDNSyntax = 34 + LDAPResultAliasDereferencingProblem = 36 + LDAPResultInappropriateAuthentication = 48 + LDAPResultInvalidCredentials = 49 + LDAPResultInsufficientAccessRights = 50 + LDAPResultBusy = 51 + LDAPResultUnavailable = 52 + LDAPResultUnwillingToPerform = 53 + LDAPResultLoopDetect = 54 + LDAPResultNamingViolation = 64 + LDAPResultObjectClassViolation = 65 + LDAPResultNotAllowedOnNonLeaf = 66 + LDAPResultNotAllowedOnRDN = 67 + LDAPResultEntryAlreadyExists = 68 + LDAPResultObjectClassModsProhibited = 69 + LDAPResultAffectsMultipleDSAs = 71 + LDAPResultOther = 80 + + ErrorNetwork = 200 + ErrorFilterCompile = 201 + ErrorFilterDecompile = 202 + ErrorDebugging = 203 +) + +// Modify Request Operation code +const ( + ModifyRequestChangeOperationAdd = 0 + ModifyRequestChangeOperationDelete = 1 + ModifyRequestChangeOperationReplace = 2 +) + +const SearchRequestScopeBaseObject = 0 +const SearchRequestSingleLevel = 1 +const SearchRequestHomeSubtree = 2 + +// Extended operation responseName and requestName +const ( + NoticeOfDisconnection ldap.LDAPOID = "1.3.6.1.4.1.1466.2003" + NoticeOfCancel ldap.LDAPOID = "1.3.6.1.1.8" + NoticeOfStartTLS ldap.LDAPOID = "1.3.6.1.4.1.1466.20037" + NoticeOfWhoAmI ldap.LDAPOID = "1.3.6.1.4.1.4203.1.11.3" + NoticeOfGetConnectionID ldap.LDAPOID = "1.3.6.1.4.1.26027.1.6.2" + NoticeOfPasswordModify ldap.LDAPOID = "1.3.6.1.4.1.4203.1.11.1" +) diff --git a/ldapserver/logger.go b/ldapserver/logger.go new file mode 100644 index 0000000..fdac2f4 --- /dev/null +++ b/ldapserver/logger.go @@ -0,0 +1,33 @@ +package ldapserver + +import ( + "io/ioutil" + "log" + "os" +) + +var Logger logger + +// Logger represents log.Logger functions from the standard library +type logger interface { + Fatal(v ...interface{}) + Fatalf(format string, v ...interface{}) + Fatalln(v ...interface{}) + + Panic(v ...interface{}) + Panicf(format string, v ...interface{}) + Panicln(v ...interface{}) + + Print(v ...interface{}) + Printf(format string, v ...interface{}) + Println(v ...interface{}) +} + +func init() { + Logger = log.New(os.Stdout, "", log.LstdFlags) +} + +var ( + // DiscardingLogger can be used to disable logging output + DiscardingLogger = log.New(ioutil.Discard, "", 0) +) diff --git a/ldapserver/message.go b/ldapserver/message.go new file mode 100644 index 0000000..1761a4a --- /dev/null +++ b/ldapserver/message.go @@ -0,0 +1,55 @@ +package ldapserver + +import ( + "fmt" + + ldap "github.com/vjeantet/goldap/message" +) + +type Message struct { + *ldap.LDAPMessage + Client *client + Done chan bool +} + +func (m *Message) String() string { + return fmt.Sprintf("MessageId=%d, %s", m.MessageID(), m.ProtocolOpName()) +} + +// Abandon close the Done channel, to notify handler's user function to stop any +// running process +func (m *Message) Abandon() { + m.Done <- true +} + +func (m *Message) GetAbandonRequest() ldap.AbandonRequest { + return m.ProtocolOp().(ldap.AbandonRequest) +} + +func (m *Message) GetSearchRequest() ldap.SearchRequest { + return m.ProtocolOp().(ldap.SearchRequest) +} + +func (m *Message) GetBindRequest() ldap.BindRequest { + return m.ProtocolOp().(ldap.BindRequest) +} + +func (m *Message) GetAddRequest() ldap.AddRequest { + return m.ProtocolOp().(ldap.AddRequest) +} + +func (m *Message) GetDeleteRequest() ldap.DelRequest { + return m.ProtocolOp().(ldap.DelRequest) +} + +func (m *Message) GetModifyRequest() ldap.ModifyRequest { + return m.ProtocolOp().(ldap.ModifyRequest) +} + +func (m *Message) GetCompareRequest() ldap.CompareRequest { + return m.ProtocolOp().(ldap.CompareRequest) +} + +func (m *Message) GetExtendedRequest() ldap.ExtendedRequest { + return m.ProtocolOp().(ldap.ExtendedRequest) +} diff --git a/ldapserver/packet.go b/ldapserver/packet.go new file mode 100644 index 0000000..24d01ed --- /dev/null +++ b/ldapserver/packet.go @@ -0,0 +1,148 @@ +package ldapserver + +import ( + "bufio" + "errors" + "fmt" + + ldap "github.com/vjeantet/goldap/message" +) + +type messagePacket struct { + bytes []byte +} + +func readMessagePacket(br *bufio.Reader) (*messagePacket, error) { + var err error + var bytes *[]byte + bytes, err = readLdapMessageBytes(br) + + if err == nil { + messagePacket := &messagePacket{bytes: *bytes} + return messagePacket, err + } + return &messagePacket{}, err + +} + +func (msg *messagePacket) readMessage() (m ldap.LDAPMessage, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("invalid packet received hex=%x, %#v", msg.bytes, r) + } + }() + + return decodeMessage(msg.bytes) +} + +func decodeMessage(bytes []byte) (ret ldap.LDAPMessage, err error) { + defer func() { + if e := recover(); e != nil { + err = errors.New(fmt.Sprintf("%s", e)) + } + }() + zero := 0 + ret, err = ldap.ReadLDAPMessage(ldap.NewBytes(zero, bytes)) + return +} + +// BELLOW SHOULD BE IN ROOX PACKAGE + +func readLdapMessageBytes(br *bufio.Reader) (ret *[]byte, err error) { + var bytes []byte + var tagAndLength ldap.TagAndLength + tagAndLength, err = readTagAndLength(br, &bytes) + if err != nil { + return + } + readBytes(br, &bytes, tagAndLength.Length) + return &bytes, err +} + +// readTagAndLength parses an ASN.1 tag and length pair from a live connection +// into a byte slice. It returns the parsed data and the new offset. SET and +// SET OF (tag 17) are mapped to SEQUENCE and SEQUENCE OF (tag 16) since we +// don't distinguish between ordered and unordered objects in this code. +func readTagAndLength(conn *bufio.Reader, bytes *[]byte) (ret ldap.TagAndLength, err error) { + // offset = initOffset + //b := bytes[offset] + //offset++ + var b byte + b, err = readBytes(conn, bytes, 1) + if err != nil { + return + } + ret.Class = int(b >> 6) + ret.IsCompound = b&0x20 == 0x20 + ret.Tag = int(b & 0x1f) + + // // If the bottom five bits are set, then the tag number is actually base 128 + // // encoded afterwards + // if ret.tag == 0x1f { + // ret.tag, err = parseBase128Int(conn, bytes) + // if err != nil { + // return + // } + // } + // We are expecting the LDAP sequence tag 0x30 as first byte + if b != 0x30 { + panic(fmt.Sprintf("Expecting 0x30 as first byte, but got %#x instead", b)) + } + + b, err = readBytes(conn, bytes, 1) + if err != nil { + return + } + if b&0x80 == 0 { + // The length is encoded in the bottom 7 bits. + ret.Length = int(b & 0x7f) + } else { + // Bottom 7 bits give the number of length bytes to follow. + numBytes := int(b & 0x7f) + if numBytes == 0 { + err = ldap.SyntaxError{"indefinite length found (not DER)"} + return + } + ret.Length = 0 + for i := 0; i < numBytes; i++ { + + b, err = readBytes(conn, bytes, 1) + if err != nil { + return + } + if ret.Length >= 1<<23 { + // We can't shift ret.length up without + // overflowing. + err = ldap.StructuralError{"length too large"} + return + } + ret.Length <<= 8 + ret.Length |= int(b) + // Compat some lib which use go-ldap or someone else, + // they encode int may have leading zeros when it's greater then 127 + // if ret.Length == 0 { + // // DER requires that lengths be minimal. + // err = ldap.StructuralError{"superfluous leading zeros in length"} + // return + // } + } + } + + return +} + +// Read "length" bytes from the connection +// Append the read bytes to "bytes" +// Return the last read byte +func readBytes(conn *bufio.Reader, bytes *[]byte, length int) (b byte, err error) { + newbytes := make([]byte, length) + n, err := conn.Read(newbytes) + if n != length { + fmt.Errorf("%d bytes read instead of %d", n, length) + } else if err != nil { + return + } + *bytes = append(*bytes, newbytes...) + b = (*bytes)[len(*bytes)-1] + return +} diff --git a/ldapserver/responsemessage.go b/ldapserver/responsemessage.go new file mode 100644 index 0000000..aaf2ede --- /dev/null +++ b/ldapserver/responsemessage.go @@ -0,0 +1,57 @@ +package ldapserver + +import ldap "github.com/vjeantet/goldap/message" + +func NewBindResponse(resultCode int) ldap.BindResponse { + r := ldap.BindResponse{} + r.SetResultCode(resultCode) + return r +} + +func NewResponse(resultCode int) ldap.LDAPResult { + r := ldap.LDAPResult{} + r.SetResultCode(resultCode) + return r +} + +func NewExtendedResponse(resultCode int) ldap.ExtendedResponse { + r := ldap.ExtendedResponse{} + r.SetResultCode(resultCode) + return r +} + +func NewCompareResponse(resultCode int) ldap.CompareResponse { + r := ldap.CompareResponse{} + r.SetResultCode(resultCode) + return r +} + +func NewModifyResponse(resultCode int) ldap.ModifyResponse { + r := ldap.ModifyResponse{} + r.SetResultCode(resultCode) + return r +} + +func NewDeleteResponse(resultCode int) ldap.DelResponse { + r := ldap.DelResponse{} + r.SetResultCode(resultCode) + return r +} + +func NewAddResponse(resultCode int) ldap.AddResponse { + r := ldap.AddResponse{} + r.SetResultCode(resultCode) + return r +} + +func NewSearchResultDoneResponse(resultCode int) ldap.SearchResultDone { + r := ldap.SearchResultDone{} + r.SetResultCode(resultCode) + return r +} + +func NewSearchResultEntry(objectname string) ldap.SearchResultEntry { + r := ldap.SearchResultEntry{} + r.SetObjectName(objectname) + return r +} diff --git a/ldapserver/route.go b/ldapserver/route.go new file mode 100644 index 0000000..d5bd00a --- /dev/null +++ b/ldapserver/route.go @@ -0,0 +1,255 @@ +package ldapserver + +import ( + "strings" + + ldap "github.com/vjeantet/goldap/message" +) + +// Constant to LDAP Request protocol Type names +const ( + SEARCH = "SearchRequest" + BIND = "BindRequest" + COMPARE = "CompareRequest" + ADD = "AddRequest" + MODIFY = "ModifyRequest" + DELETE = "DelRequest" + EXTENDED = "ExtendedRequest" + ABANDON = "AbandonRequest" +) + +// HandlerFunc type is an adapter to allow the use of +// ordinary functions as LDAP handlers. If f is a function +// with the appropriate signature, HandlerFunc(f) is a +// Handler object that calls f. +type HandlerFunc func(UserState, ResponseWriter, *Message) + +// RouteMux manages all routes +type RouteMux struct { + routes []*route + notFoundRoute *route +} + +type route struct { + label string + operation string + handler HandlerFunc + exoName string + sBasedn string + uBasedn bool + sFilter string + uFilter bool + sScope int + uScope bool + sAuthChoice string + uAuthChoice bool +} + +// Match return true when the *Message matches the route +// conditions +func (r *route) Match(m *Message) bool { + if m.ProtocolOpName() != r.operation { + return false + } + + switch v := m.ProtocolOp().(type) { + case ldap.BindRequest: + if r.uAuthChoice == true { + if strings.ToLower(v.AuthenticationChoice()) != r.sAuthChoice { + return false + } + } + return true + + case ldap.ExtendedRequest: + if string(v.RequestName()) != r.exoName { + return false + } + return true + + case ldap.SearchRequest: + if r.uBasedn == true { + if strings.ToLower(string(v.BaseObject())) != r.sBasedn { + return false + } + } + + if r.uFilter == true { + if strings.ToLower(v.FilterString()) != r.sFilter { + return false + } + } + + if r.uScope == true { + if int(v.Scope()) != r.sScope { + return false + } + } + return true + } + return true +} + +func (r *route) Label(label string) *route { + r.label = label + return r +} + +func (r *route) BaseDn(dn string) *route { + r.sBasedn = strings.ToLower(dn) + r.uBasedn = true + return r +} + +func (r *route) AuthenticationChoice(choice string) *route { + r.sAuthChoice = strings.ToLower(choice) + r.uAuthChoice = true + return r +} + +func (r *route) Filter(pattern string) *route { + r.sFilter = strings.ToLower(pattern) + r.uFilter = true + return r +} + +func (r *route) Scope(scope int) *route { + r.sScope = scope + r.uScope = true + return r +} + +func (r *route) RequestName(name ldap.LDAPOID) *route { + r.exoName = string(name) + return r +} + +// NewRouteMux returns a new *RouteMux +// RouteMux implements ldapserver.Handler +func NewRouteMux() *RouteMux { + return &RouteMux{} +} + +// Handler interface used to serve a LDAP Request message +type Handler interface { + ServeLDAP(s UserState, w ResponseWriter, r *Message) +} + +// ServeLDAP dispatches the request to the handler whose +// pattern most closely matches the request request Message. +func (h *RouteMux) ServeLDAP(s UserState, w ResponseWriter, r *Message) { + + //find a matching Route + for _, route := range h.routes { + + //if the route don't match, skip it + if route.Match(r) == false { + continue + } + + if route.label != "" { + Logger.Printf("") + Logger.Printf(" ROUTE MATCH ; %s", route.label) + Logger.Printf("") + // Logger.Printf(" ROUTE MATCH ; %s", runtime.FuncForPC(reflect.ValueOf(route.handler).Pointer()).Name()) + } + + route.handler(s, w, r) + return + } + + // Catch a AbandonRequest not handled by user + switch v := r.ProtocolOp().(type) { + case ldap.AbandonRequest: + // retreive the request to abandon, and send a abort signal to it + if requestToAbandon, ok := r.Client.GetMessageByID(int(v)); ok { + requestToAbandon.Abandon() + } + } + + if h.notFoundRoute != nil { + h.notFoundRoute.handler(s, w, r) + } else { + res := NewResponse(LDAPResultUnwillingToPerform) + res.SetDiagnosticMessage("Operation not implemented by server") + w.Write(res) + } +} + +// Adds a new Route to the Handler +func (h *RouteMux) addRoute(r *route) { + //and finally append to the list of Routes + //create the Route + h.routes = append(h.routes, r) +} + +func (h *RouteMux) NotFound(handler HandlerFunc) *route { + route := &route{} + route.handler = handler + h.notFoundRoute = route + return route +} + +func (h *RouteMux) Bind(handler HandlerFunc) *route { + route := &route{} + route.operation = BIND + route.handler = handler + h.addRoute(route) + return route +} + +func (h *RouteMux) Search(handler HandlerFunc) *route { + route := &route{} + route.operation = SEARCH + route.handler = handler + h.addRoute(route) + return route +} + +func (h *RouteMux) Add(handler HandlerFunc) *route { + route := &route{} + route.operation = ADD + route.handler = handler + h.addRoute(route) + return route +} + +func (h *RouteMux) Delete(handler HandlerFunc) *route { + route := &route{} + route.operation = DELETE + route.handler = handler + h.addRoute(route) + return route +} + +func (h *RouteMux) Modify(handler HandlerFunc) *route { + route := &route{} + route.operation = MODIFY + route.handler = handler + h.addRoute(route) + return route +} + +func (h *RouteMux) Compare(handler HandlerFunc) *route { + route := &route{} + route.operation = COMPARE + route.handler = handler + h.addRoute(route) + return route +} + +func (h *RouteMux) Extended(handler HandlerFunc) *route { + route := &route{} + route.operation = EXTENDED + route.handler = handler + h.addRoute(route) + return route +} + +func (h *RouteMux) Abandon(handler HandlerFunc) *route { + route := &route{} + route.operation = ABANDON + route.handler = handler + h.addRoute(route) + return route +} diff --git a/ldapserver/server.go b/ldapserver/server.go new file mode 100644 index 0000000..3640d0a --- /dev/null +++ b/ldapserver/server.go @@ -0,0 +1,145 @@ +package ldapserver + +import ( + "bufio" + "net" + "sync" + "time" +) + +// Server is an LDAP server. +type Server struct { + Listener net.Listener + ReadTimeout time.Duration // optional read timeout + WriteTimeout time.Duration // optional write timeout + wg sync.WaitGroup // group of goroutines (1 by client) + chDone chan bool // Channel Done, value => shutdown + + // OnNewConnection, if non-nil, is called on new connections. + // If it returns non-nil, the connection is closed. + OnNewConnection func(c net.Conn) error + + // Handler handles ldap message received from client + // it SHOULD "implement" RequestHandler interface + Handler Handler + NewUserState func() UserState +} + +//NewServer return a LDAP Server +func NewServer() *Server { + return &Server{ + chDone: make(chan bool), + } +} + +// Handle registers the handler for the server. +// If a handler already exists for pattern, Handle panics +func (s *Server) Handle(h Handler) { + if s.Handler != nil { + panic("LDAP: multiple Handler registrations") + } + s.Handler = h +} + +// ListenAndServe listens on the TCP network address s.Addr and then +// calls Serve to handle requests on incoming connections. If +// s.Addr is blank, ":389" is used. +func (s *Server) ListenAndServe(addr string, options ...func(*Server)) error { + + if addr == "" { + addr = ":389" + } + + var e error + s.Listener, e = net.Listen("tcp", addr) + if e != nil { + return e + } + Logger.Printf("Listening on %s\n", addr) + + for _, option := range options { + option(s) + } + + return s.serve() +} + +// Handle requests messages on the ln listener +func (s *Server) serve() error { + defer s.Listener.Close() + + if s.Handler == nil { + Logger.Panicln("No LDAP Request Handler defined") + } + + i := 0 + + for { + select { + case <-s.chDone: + Logger.Print("Stopping server") + s.Listener.Close() + return nil + default: + } + + rw, err := s.Listener.Accept() + + if s.ReadTimeout != 0 { + rw.SetReadDeadline(time.Now().Add(s.ReadTimeout)) + } + if s.WriteTimeout != 0 { + rw.SetWriteDeadline(time.Now().Add(s.WriteTimeout)) + } + if nil != err { + if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() { + continue + } + Logger.Println(err) + } + + cli, err := s.newClient(rw) + + if err != nil { + continue + } + + i = i + 1 + cli.Numero = i + Logger.Printf("Connection client [%d] from %s accepted", cli.Numero, cli.rwc.RemoteAddr().String()) + s.wg.Add(1) + go cli.serve() + } + + return nil +} + +// Return a new session with the connection +// client has a writer and reader buffer +func (s *Server) newClient(rwc net.Conn) (c *client, err error) { + c = &client{ + srv: s, + rwc: rwc, + br: bufio.NewReader(rwc), + bw: bufio.NewWriter(rwc), + userState: s.NewUserState(), + } + return c, nil +} + +// Termination of the LDAP session is initiated by the server sending a +// Notice of Disconnection. In this case, each +// protocol peer gracefully terminates the LDAP session by ceasing +// exchanges at the LDAP message layer, tearing down any SASL layer, +// tearing down any TLS layer, and closing the transport connection. +// A protocol peer may determine that the continuation of any +// communication would be pernicious, and in this case, it may abruptly +// terminate the session by ceasing communication and closing the +// transport connection. +// In either case, when the LDAP session is terminated. +func (s *Server) Stop() { + close(s.chDone) + Logger.Print("gracefully closing client connections...") + s.wg.Wait() + Logger.Print("all clients connection closed") +} diff --git a/main.go b/main.go index baf0e09..1fc5d3b 100644 --- a/main.go +++ b/main.go @@ -1,18 +1,18 @@ package main import ( + "encoding/base64" + "encoding/json" "fmt" "log" + "math/rand" "os" "os/signal" - "syscall" "strings" - "encoding/json" - "encoding/base64" - "math/rand" + "syscall" + ldap "./ldapserver" consul "github.com/hashicorp/consul/api" - ldap "github.com/vjeantet/ldapserver" message "github.com/vjeantet/goldap/message" ) @@ -28,7 +28,7 @@ func dnToConsul(dn string) string { } type DNComponent struct { - Type string + Type string Value string } @@ -43,7 +43,7 @@ func parseDN(dn string) ([]DNComponent, error) { return nil, fmt.Errorf("Wrong DN component: %s (expected type=value)", rdn) } ret = append(ret, DNComponent{ - Type: splits[0], + Type: splits[0], Value: splits[1], }) } @@ -56,7 +56,11 @@ type Config struct { type Server struct { config Config - kv *consul.KV + kv *consul.KV +} + +type State struct { + bindDn string } type Attributes map[string]interface{} @@ -73,7 +77,7 @@ func main() { kv := client.KV() // TODO read config from somewhere - config := Config { + config := Config{ Suffix: "dc=gobottin,dc=eu", } @@ -85,6 +89,9 @@ func main() { //Create a new LDAP Server ldapserver := ldap.NewServer() + ldapserver.NewUserState = func() ldap.UserState { + return &State{} + } routes := ldap.NewRouteMux() routes.Bind(gobottin.handleBind) @@ -104,7 +111,7 @@ func main() { } func (server *Server) init() error { - pair, _, err := server.kv.Get(dnToConsul(server.config.Suffix) + "/attribute=objectClass", nil) + pair, _, err := server.kv.Get(dnToConsul(server.config.Suffix)+"/attribute=objectClass", nil) if err != nil { return err } @@ -114,7 +121,7 @@ func (server *Server) init() error { } base_attributes := Attributes{ - "objectClass": []string{"top", "dcObject", "organization"}, + "objectClass": []string{"top", "dcObject", "organization"}, "structuralObjectClass": "Organization", } suffix_dn, err := parseDN(server.config.Suffix) @@ -135,12 +142,12 @@ func (server *Server) init() error { admin_dn := "cn=admin," + server.config.Suffix admin_attributes := Attributes{ - "objectClass": []string{"simpleSecurityObject", "organizationalRole"}, - "description": "LDAP administrator", - "cn": "admin", - "userpassword": admin_pass_hash, + "objectClass": []string{"simpleSecurityObject", "organizationalRole"}, + "description": "LDAP administrator", + "cn": "admin", + "userpassword": admin_pass_hash, "structuralObjectClass": "organizationalRole", - "permissions": []string{"read", "write"}, + "permissions": []string{"read", "write"}, } err = server.addElements(admin_dn, admin_attributes) @@ -173,10 +180,11 @@ func (server *Server) addElements(dn string, attrs Attributes) error { return nil } -func (server *Server) handleBind(w ldap.ResponseWriter, m *ldap.Message) { +func (server *Server) handleBind(s ldap.UserState, w ldap.ResponseWriter, m *ldap.Message) { + state := s.(*State) r := m.GetBindRequest() - result_code, err := server.handleBindInternal(w, r) + result_code, err := server.handleBindInternal(state, w, r) res := ldap.NewBindResponse(result_code) if err != nil { @@ -186,9 +194,9 @@ func (server *Server) handleBind(w ldap.ResponseWriter, m *ldap.Message) { w.Write(res) } -func (server *Server) handleBindInternal(w ldap.ResponseWriter, r message.BindRequest) (int, error) { +func (server *Server) handleBindInternal(state *State, w ldap.ResponseWriter, r message.BindRequest) (int, error) { - pair, _, err := server.kv.Get(dnToConsul(string(r.Name())) + "/attribute=userpassword", nil) + pair, _, err := server.kv.Get(dnToConsul(string(r.Name()))+"/attribute=userpassword", nil) if err != nil { return ldap.LDAPResultOperationsError, err } @@ -205,6 +213,7 @@ func (server *Server) handleBindInternal(w ldap.ResponseWriter, r message.BindRe valid := SSHAMatches(hash, []byte(r.AuthenticationSimple())) if valid { + state.bindDn = string(r.Name()) return ldap.LDAPResultSuccess, nil } else { return ldap.LDAPResultInvalidCredentials, nil diff --git a/ssha.go b/ssha.go index 203b994..f1c5a8b 100644 --- a/ssha.go +++ b/ssha.go @@ -1,11 +1,11 @@ package main import ( - "fmt" "bytes" - "math/rand" - "encoding/base64" "crypto/sha1" + "encoding/base64" + "fmt" + "math/rand" ) // Encode encodes the []byte of raw password