aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Auvolat <alex@adnab.me>2020-01-19 13:00:53 +0100
committerAlex Auvolat <alex@adnab.me>2020-01-19 13:00:53 +0100
commit67fa504e20095d9acd5537b46f604ce8baa4e44a (patch)
treedcbe95bfdcd71a2d0307ea7214017c3632f44f8f
parentbade33cf1529893a92a283f6dc86e73f8766049e (diff)
downloadbottin-67fa504e20095d9acd5537b46f604ce8baa4e44a.tar.gz
bottin-67fa504e20095d9acd5537b46f604ce8baa4e44a.zip
Add ldapserver source in here & add support for client state
-rw-r--r--ldapserver/client.go257
-rw-r--r--ldapserver/constants.go96
-rw-r--r--ldapserver/logger.go33
-rw-r--r--ldapserver/message.go55
-rw-r--r--ldapserver/packet.go148
-rw-r--r--ldapserver/responsemessage.go57
-rw-r--r--ldapserver/route.go255
-rw-r--r--ldapserver/server.go145
-rw-r--r--main.go49
-rw-r--r--ssha.go6
10 files changed, 1078 insertions, 23 deletions
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