package ldap import ( "context" "crypto/tls" "errors" "log/slog" "net" "time" "tug/internal/platform/ratelimit" "tug/internal/service/refusalcap" "tug/internal/storage" fk "tug/internal/transport/ldap/internal/glauthldap" ) const ( readTimeout = 2 * time.Minute writeTimeout = 30 * time.Second maxRequestBytes = 64 << 10 ) var errNoStore = errors.New("ldap: no store") type Config struct { BaseDN string PrimaryGroup string TLSCert string TLSKey string Observe func(result string) Refusals *refusalcap.Cap } type Server struct { fork *fk.Server dir *directory tls *tls.Config logger *slog.Logger backoff *backoff peers *peerLimit observe func(string) cursors *cursors binds *bindWindow refusal *refusalcap.Cap ctx context.Context cancel context.CancelFunc } type refuser struct{} func (refuser) Add(string, fk.AddRequest, net.Conn) (fk.LDAPResultCode, error) { return fk.LDAPResultUnwillingToPerform, nil } func (refuser) Modify(string, fk.ModifyRequest, net.Conn) (fk.LDAPResultCode, error) { return fk.LDAPResultUnwillingToPerform, nil } func (refuser) Delete(string, string, net.Conn) (fk.LDAPResultCode, error) { return fk.LDAPResultUnwillingToPerform, nil } func (refuser) ModifyDN(string, fk.ModifyDNRequest, net.Conn) (fk.LDAPResultCode, error) { return fk.LDAPResultUnwillingToPerform, nil } func (refuser) Compare(string, fk.CompareRequest, net.Conn) (fk.LDAPResultCode, error) { return fk.LDAPResultUnwillingToPerform, nil } func (refuser) Extended(string, fk.ExtendedRequest, net.Conn) (fk.LDAPResultCode, error) { return fk.LDAPResultProtocolError, nil } func New(store *storage.Store, cfg Config, logger *slog.Logger) (*Server, error) { if store == nil { return nil, errNoStore } dir, err := newDirectory(store, cfg.BaseDN, cfg.PrimaryGroup) if err != nil { return nil, err } var tlsConfig *tls.Config if cfg.TLSCert != "" || cfg.TLSKey != "" { pair, err := loadKeyPair(cfg.TLSCert, cfg.TLSKey) if err != nil { return nil, err } tlsConfig = &tls.Config{Certificates: []tls.Certificate{pair}, MinVersion: tls.VersionTLS12} } ctx, cancel := context.WithCancel(context.Background()) s := &Server{ dir: dir, tls: tlsConfig, logger: logger, backoff: newBackoff(time.Now, backoffEntries), peers: newPeerLimit(time.Now, ratelimit.Rate{Burst: peerBurst, Every: peerEvery}, peerEntries), observe: observeOrDiscard(cfg.Observe), cursors: &cursors{byConn: map[net.Conn][]*cursor{}}, binds: newBindWindow(time.Now, bindWindowEntries), refusal: refusalsOrOwn(cfg.Refusals, store, logger), ctx: ctx, cancel: cancel, } f := fk.NewServer() f.ReadTimeout = readTimeout f.WriteTimeout = writeTimeout f.MaxRequestBytes = maxRequestBytes f.BindFunc("", handler{s}) f.StructuredSearchFunc("", handler{s}) f.AddFunc("", refuser{}) f.ModifyFunc("", refuser{}) f.DeleteFunc("", refuser{}) f.ModifyDNFunc("", refuser{}) f.CompareFunc("", refuser{}) f.ExtendedFunc("", refuser{}) f.CloseFunc("", handler{s}) s.fork = f return s, nil } type handler struct{ s *Server } func (h handler) Bind(bindDN, bindSimplePw string, conn net.Conn) (fk.LDAPResultCode, error) { code, result := h.s.bindFrom(peerAddr(conn), bindDN, bindSimplePw) h.s.observe(result) return code, nil } func (h handler) SearchStructured(boundDN string, req fk.StructuredSearchRequest, conn net.Conn) (fk.ServerSearchResult, error) { return h.s.searchStructured(boundDN, req, conn) } func (h handler) Close(boundDN string, conn net.Conn) error { h.s.cursors.drop(conn) return conn.Close() } func (s *Server) Serve(ln net.Listener) error { if s.tls != nil { ln = tls.NewListener(ln, s.tls) } return s.fork.Serve(ln) } func (s *Server) Close() { s.cancel() s.fork.Close() } func (s *Server) Ready(ctx context.Context) error { _, err := s.dir.store.GroupByName(ctx, s.dir.primaryGroup) if err != nil && !errors.Is(err, storage.ErrNotFound) { return err } return nil } func observeOrDiscard(observe func(string)) func(string) { if observe == nil { return func(string) {} } return observe }