first commit
This commit is contained in:
604
apps/ldap-auth/main.go
Normal file
604
apps/ldap-auth/main.go
Normal file
@@ -0,0 +1,604 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
type ldapConfig struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
UseLdaps bool `json:"useLdaps"`
|
||||
SkipTlsVerify bool `json:"skipTlsVerify"`
|
||||
Domain string `json:"domain"`
|
||||
BaseDn string `json:"baseDn"`
|
||||
BindDn string `json:"bindDn"`
|
||||
BindPassword string `json:"bindPassword"`
|
||||
UserFilter string `json:"userFilter"`
|
||||
UsernameAttr string `json:"usernameAttr"`
|
||||
EmailAttr string `json:"emailAttr"`
|
||||
DisplayNameAttr string `json:"displayNameAttr"`
|
||||
}
|
||||
|
||||
type ldapTarget struct {
|
||||
host string
|
||||
port int
|
||||
useLdaps bool
|
||||
}
|
||||
|
||||
type authenticateRequest struct {
|
||||
Config ldapConfig `json:"config"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type authenticateResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Dn string `json:"dn,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func env(key, fallback string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, payload any) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(payload)
|
||||
}
|
||||
|
||||
func resolvePort(port int, useLdaps bool) int {
|
||||
if useLdaps {
|
||||
if port == 640 || port == 389 || port == 0 {
|
||||
return 636
|
||||
}
|
||||
return port
|
||||
}
|
||||
if port == 0 {
|
||||
return 389
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
func parseTargets(cfg ldapConfig) ([]ldapTarget, error) {
|
||||
raw := strings.TrimSpace(cfg.Host)
|
||||
if raw == "" {
|
||||
return nil, errors.New("не указан LDAP host")
|
||||
}
|
||||
|
||||
parts := strings.Split(raw, ",")
|
||||
targets := make([]ldapTarget, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
useLdaps := cfg.UseLdaps
|
||||
host := part
|
||||
port := cfg.Port
|
||||
|
||||
if strings.Contains(part, "://") {
|
||||
if strings.HasPrefix(strings.ToLower(part), "ldaps://") {
|
||||
useLdaps = true
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(part), "ldap://") {
|
||||
useLdaps = false
|
||||
}
|
||||
withoutScheme := part
|
||||
if idx := strings.Index(withoutScheme, "://"); idx >= 0 {
|
||||
withoutScheme = withoutScheme[idx+3:]
|
||||
}
|
||||
if slash := strings.Index(withoutScheme, "/"); slash >= 0 {
|
||||
withoutScheme = withoutScheme[:slash]
|
||||
}
|
||||
if colon := strings.LastIndex(withoutScheme, ":"); colon > 0 && colon < len(withoutScheme)-1 {
|
||||
host = withoutScheme[:colon]
|
||||
var parsedPort int
|
||||
_, scanErr := fmt.Sscanf(withoutScheme[colon+1:], "%d", &parsedPort)
|
||||
if scanErr == nil && parsedPort > 0 {
|
||||
port = parsedPort
|
||||
}
|
||||
} else {
|
||||
host = withoutScheme
|
||||
}
|
||||
}
|
||||
|
||||
host = strings.TrimSpace(host)
|
||||
if host == "" {
|
||||
continue
|
||||
}
|
||||
targets = append(targets, ldapTarget{
|
||||
host: host,
|
||||
port: resolvePort(port, useLdaps),
|
||||
useLdaps: useLdaps,
|
||||
})
|
||||
}
|
||||
|
||||
if len(targets) == 0 {
|
||||
return nil, errors.New("не указан LDAP host")
|
||||
}
|
||||
return targets, nil
|
||||
}
|
||||
|
||||
func connectTarget(target ldapTarget, skipTlsVerify bool) (*ldap.Conn, error) {
|
||||
address := fmt.Sprintf("%s:%d", target.host, target.port)
|
||||
if target.useLdaps {
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: target.host,
|
||||
InsecureSkipVerify: skipTlsVerify,
|
||||
}
|
||||
return ldap.DialURL(fmt.Sprintf("ldaps://%s", address), ldap.DialWithTLSConfig(tlsConfig))
|
||||
}
|
||||
return ldap.DialURL(fmt.Sprintf("ldap://%s", address))
|
||||
}
|
||||
|
||||
func connect(cfg ldapConfig) (*ldap.Conn, error) {
|
||||
targets, err := parseTargets(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, target := range targets {
|
||||
conn, dialErr := connectTarget(target, cfg.SkipTlsVerify)
|
||||
if dialErr == nil {
|
||||
return conn, nil
|
||||
}
|
||||
lastErr = fmt.Errorf("%s:%d: %w", target.host, target.port, dialErr)
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
if strings.Contains(lastErr.Error(), "connection refused") {
|
||||
return nil, fmt.Errorf("не удалось подключиться к LDAP (connection refused). Проверьте host, порт (636 для LDAPS) и доступность контейнера ldap-auth до AD: %w", lastErr)
|
||||
}
|
||||
return nil, fmt.Errorf("не удалось подключиться к LDAP: %w", lastErr)
|
||||
}
|
||||
return nil, errors.New("не удалось подключиться к LDAP")
|
||||
}
|
||||
|
||||
func resolveBindDN(cfg ldapConfig) string {
|
||||
bindDn := strings.TrimSpace(cfg.BindDn)
|
||||
if bindDn == "" {
|
||||
return bindDn
|
||||
}
|
||||
if strings.Contains(bindDn, "=") || strings.Contains(bindDn, `\`) || strings.Contains(bindDn, "@") {
|
||||
return bindDn
|
||||
}
|
||||
domain := strings.TrimSpace(cfg.Domain)
|
||||
if domain != "" {
|
||||
return bindDn + "@" + domain
|
||||
}
|
||||
return bindDn
|
||||
}
|
||||
|
||||
func normalizeUserFilter(filter string) string {
|
||||
filter = strings.ReplaceAll(filter, "{{username}}", "{username}")
|
||||
filter = strings.ReplaceAll(filter, "{user}", "{username}")
|
||||
filter = strings.TrimSpace(filter)
|
||||
for strings.HasPrefix(filter, "((") && strings.HasSuffix(filter, "))") && len(filter) > 4 {
|
||||
filter = filter[1 : len(filter)-1]
|
||||
}
|
||||
return filter
|
||||
}
|
||||
|
||||
func fetchDefaultNamingContext(conn *ldap.Conn) string {
|
||||
request := ldap.NewSearchRequest(
|
||||
"",
|
||||
ldap.ScopeBaseObject,
|
||||
ldap.NeverDerefAliases,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
"(objectClass=*)",
|
||||
[]string{"defaultNamingContext"},
|
||||
nil,
|
||||
)
|
||||
result, err := conn.Search(request)
|
||||
if err != nil || len(result.Entries) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(result.Entries[0].GetAttributeValue("defaultNamingContext"))
|
||||
}
|
||||
|
||||
func isNoSuchObject(err error) bool {
|
||||
ldapErr, ok := err.(*ldap.Error)
|
||||
return ok && ldapErr.ResultCode == 32
|
||||
}
|
||||
|
||||
func swapRdnPrefix(baseDn, fromPrefix, toPrefix string) string {
|
||||
upper := strings.ToUpper(baseDn)
|
||||
fromUpper := strings.ToUpper(fromPrefix)
|
||||
if !strings.HasPrefix(upper, fromUpper) {
|
||||
return ""
|
||||
}
|
||||
rest := baseDn[len(fromPrefix):]
|
||||
comma := strings.Index(rest, ",")
|
||||
if comma < 0 {
|
||||
return ""
|
||||
}
|
||||
name := strings.TrimSpace(rest[:comma])
|
||||
suffix := strings.TrimSpace(rest[comma+1:])
|
||||
if name == "" || suffix == "" {
|
||||
return ""
|
||||
}
|
||||
return toPrefix + name + "," + suffix
|
||||
}
|
||||
|
||||
func buildSearchBaseCandidates(configured string, defaultNamingContext string) []string {
|
||||
configured = strings.TrimSpace(configured)
|
||||
seen := map[string]struct{}{}
|
||||
out := make([]string, 0, 4)
|
||||
|
||||
add := func(dn string) {
|
||||
dn = strings.TrimSpace(dn)
|
||||
if dn == "" {
|
||||
return
|
||||
}
|
||||
key := strings.ToLower(dn)
|
||||
if _, exists := seen[key]; exists {
|
||||
return
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, dn)
|
||||
}
|
||||
|
||||
add(configured)
|
||||
add(swapRdnPrefix(configured, "CN=", "OU="))
|
||||
add(swapRdnPrefix(configured, "OU=", "CN="))
|
||||
add(defaultNamingContext)
|
||||
return out
|
||||
}
|
||||
|
||||
func searchUser(conn *ldap.Conn, baseDn, filter string, attrs []string) (*ldap.SearchResult, error) {
|
||||
request := ldap.NewSearchRequest(
|
||||
baseDn,
|
||||
ldap.ScopeWholeSubtree,
|
||||
ldap.NeverDerefAliases,
|
||||
1,
|
||||
15,
|
||||
false,
|
||||
filter,
|
||||
attrs,
|
||||
nil,
|
||||
)
|
||||
return conn.Search(request)
|
||||
}
|
||||
|
||||
func findMatchingContainers(conn *ldap.Conn, domainRoot, nameHint string) []string {
|
||||
nameHint = strings.TrimSpace(nameHint)
|
||||
if domainRoot == "" || nameHint == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
escaped := ldap.EscapeFilter(nameHint)
|
||||
filter := fmt.Sprintf("(|(ou=%s)(cn=%s)(name=%s))", escaped, escaped, escaped)
|
||||
result, err := conn.Search(ldap.NewSearchRequest(
|
||||
domainRoot,
|
||||
ldap.ScopeWholeSubtree,
|
||||
ldap.NeverDerefAliases,
|
||||
5,
|
||||
10,
|
||||
false,
|
||||
filter,
|
||||
[]string{"dn"},
|
||||
nil,
|
||||
))
|
||||
if err != nil || len(result.Entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
dns := make([]string, 0, len(result.Entries))
|
||||
for _, entry := range result.Entries {
|
||||
dns = append(dns, entry.DN)
|
||||
}
|
||||
return dns
|
||||
}
|
||||
|
||||
func extractNameHint(baseDn string) string {
|
||||
parts := strings.Split(baseDn, ",")
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
first := strings.TrimSpace(parts[0])
|
||||
if eq := strings.Index(first, "="); eq > 0 {
|
||||
return strings.TrimSpace(first[eq+1:])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func formatSearchError(baseDn string, defaultNamingContext string, alternatives []string, err error) error {
|
||||
if isNoSuchObject(err) {
|
||||
hint := fmt.Sprintf("Base DN «%s» не существует в Active Directory", baseDn)
|
||||
if ouVariant := swapRdnPrefix(baseDn, "CN=", "OU="); ouVariant != "" && !strings.EqualFold(ouVariant, baseDn) {
|
||||
hint += fmt.Sprintf(". Попробуйте OU вместо CN: %s", ouVariant)
|
||||
}
|
||||
if defaultNamingContext != "" {
|
||||
hint += fmt.Sprintf(". Корень домена: %s", defaultNamingContext)
|
||||
}
|
||||
if len(alternatives) > 0 {
|
||||
hint += fmt.Sprintf(". Найдены похожие контейнеры: %s", strings.Join(alternatives, "; "))
|
||||
} else {
|
||||
hint += ". Укажите OU или контейнер, где находятся пользователи"
|
||||
}
|
||||
return errors.New(hint)
|
||||
}
|
||||
return fmt.Errorf("ошибка поиска пользователя в «%s»: %w", baseDn, err)
|
||||
}
|
||||
|
||||
type discoverResponse struct {
|
||||
Success bool `json:"success"`
|
||||
DomainRoot string `json:"domainRoot,omitempty"`
|
||||
ConfiguredBaseDn string `json:"configuredBaseDn,omitempty"`
|
||||
SuggestedBaseDn string `json:"suggestedBaseDn,omitempty"`
|
||||
TopLevelContainers []string `json:"topLevelContainers,omitempty"`
|
||||
MatchingContainers []string `json:"matchingContainers,omitempty"`
|
||||
SearchBaseCandidates []string `json:"searchBaseCandidates,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func discover(cfg ldapConfig) (discoverResponse, error) {
|
||||
if strings.TrimSpace(cfg.BaseDn) == "" {
|
||||
return discoverResponse{}, errors.New("не указан Base DN")
|
||||
}
|
||||
bindDn := resolveBindDN(cfg)
|
||||
if bindDn == "" {
|
||||
return discoverResponse{}, errors.New("не указан Bind DN")
|
||||
}
|
||||
|
||||
conn, err := connect(cfg)
|
||||
if err != nil {
|
||||
return discoverResponse{}, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if err := conn.Bind(bindDn, cfg.BindPassword); err != nil {
|
||||
return discoverResponse{}, fmt.Errorf("ошибка bind сервисной учётной записи (%s): %w", bindDn, err)
|
||||
}
|
||||
|
||||
domainRoot := fetchDefaultNamingContext(conn)
|
||||
nameHint := extractNameHint(cfg.BaseDn)
|
||||
candidates := buildSearchBaseCandidates(cfg.BaseDn, domainRoot)
|
||||
matching := findMatchingContainers(conn, domainRoot, nameHint)
|
||||
|
||||
topLevel := make([]string, 0, 16)
|
||||
if domainRoot != "" {
|
||||
result, searchErr := conn.Search(ldap.NewSearchRequest(
|
||||
domainRoot,
|
||||
ldap.ScopeSingleLevel,
|
||||
ldap.NeverDerefAliases,
|
||||
0,
|
||||
10,
|
||||
false,
|
||||
"(|(objectClass=organizationalUnit)(objectClass=container)(objectClass=builtinDomain))",
|
||||
[]string{"dn", "name", "ou"},
|
||||
nil,
|
||||
))
|
||||
if searchErr == nil {
|
||||
for _, entry := range result.Entries {
|
||||
topLevel = append(topLevel, entry.DN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suggested := cfg.BaseDn
|
||||
if len(matching) > 0 {
|
||||
suggested = matching[0]
|
||||
} else if ouVariant := swapRdnPrefix(cfg.BaseDn, "CN=", "OU="); ouVariant != "" {
|
||||
suggested = ouVariant
|
||||
} else if domainRoot != "" {
|
||||
suggested = domainRoot
|
||||
}
|
||||
|
||||
return discoverResponse{
|
||||
Success: true,
|
||||
DomainRoot: domainRoot,
|
||||
ConfiguredBaseDn: cfg.BaseDn,
|
||||
SuggestedBaseDn: suggested,
|
||||
TopLevelContainers: topLevel,
|
||||
MatchingContainers: matching,
|
||||
SearchBaseCandidates: candidates,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func firstAttr(entry *ldap.Entry, names ...string) string {
|
||||
for _, name := range names {
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
values := entry.GetAttributeValues(name)
|
||||
if len(values) > 0 && strings.TrimSpace(values[0]) != "" {
|
||||
return strings.TrimSpace(values[0])
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func authenticate(cfg ldapConfig, username, password string) (authenticateResponse, error) {
|
||||
username = strings.TrimSpace(username)
|
||||
password = strings.TrimSpace(password)
|
||||
if username == "" || password == "" {
|
||||
return authenticateResponse{}, errors.New("укажите логин и пароль")
|
||||
}
|
||||
if strings.TrimSpace(cfg.BaseDn) == "" {
|
||||
return authenticateResponse{}, errors.New("не указан Base DN")
|
||||
}
|
||||
bindDn := resolveBindDN(cfg)
|
||||
if bindDn == "" {
|
||||
return authenticateResponse{}, errors.New("не указан Bind DN")
|
||||
}
|
||||
|
||||
filterTemplate := normalizeUserFilter(cfg.UserFilter)
|
||||
if filterTemplate == "" {
|
||||
filterTemplate = "(|(sAMAccountName={username})(userPrincipalName={username})(mail={username})(uid={username}))"
|
||||
}
|
||||
filter := strings.ReplaceAll(filterTemplate, "{username}", ldap.EscapeFilter(username))
|
||||
|
||||
conn, err := connect(cfg)
|
||||
if err != nil {
|
||||
return authenticateResponse{}, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if err := conn.Bind(bindDn, cfg.BindPassword); err != nil {
|
||||
return authenticateResponse{}, fmt.Errorf("ошибка bind сервисной учётной записи (%s): %w", bindDn, err)
|
||||
}
|
||||
|
||||
defaultNamingContext := fetchDefaultNamingContext(conn)
|
||||
searchAttrs := []string{"dn", cfg.UsernameAttr, cfg.EmailAttr, cfg.DisplayNameAttr, "cn", "mail", "uid", "sAMAccountName", "userPrincipalName"}
|
||||
searchBases := buildSearchBaseCandidates(cfg.BaseDn, defaultNamingContext)
|
||||
|
||||
var entry *ldap.Entry
|
||||
var lastSearchErr error
|
||||
var lastBaseDn string
|
||||
for _, baseDn := range searchBases {
|
||||
result, searchErr := searchUser(conn, baseDn, filter, searchAttrs)
|
||||
if searchErr != nil {
|
||||
lastSearchErr = searchErr
|
||||
lastBaseDn = baseDn
|
||||
if isNoSuchObject(searchErr) {
|
||||
continue
|
||||
}
|
||||
return authenticateResponse{}, formatSearchError(baseDn, defaultNamingContext, nil, searchErr)
|
||||
}
|
||||
if len(result.Entries) > 0 {
|
||||
entry = result.Entries[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
alternatives := findMatchingContainers(conn, defaultNamingContext, extractNameHint(cfg.BaseDn))
|
||||
if lastSearchErr != nil {
|
||||
return authenticateResponse{}, formatSearchError(lastBaseDn, defaultNamingContext, alternatives, lastSearchErr)
|
||||
}
|
||||
hint := fmt.Sprintf("пользователь «%s» не найден в LDAP", username)
|
||||
if len(alternatives) > 0 {
|
||||
hint += fmt.Sprintf(". Проверьте Base DN — возможные контейнеры: %s", strings.Join(alternatives, "; "))
|
||||
} else {
|
||||
hint += fmt.Sprintf(". Проверьте Base DN (%s) и фильтр поиска", cfg.BaseDn)
|
||||
}
|
||||
return authenticateResponse{}, errors.New(hint)
|
||||
}
|
||||
userDn := entry.DN
|
||||
|
||||
if err := conn.Bind(userDn, password); err != nil {
|
||||
return authenticateResponse{}, errors.New("неверный логин или пароль LDAP")
|
||||
}
|
||||
|
||||
usernameAttr := cfg.UsernameAttr
|
||||
if usernameAttr == "" {
|
||||
usernameAttr = "sAMAccountName"
|
||||
}
|
||||
emailAttr := cfg.EmailAttr
|
||||
if emailAttr == "" {
|
||||
emailAttr = "mail"
|
||||
}
|
||||
displayNameAttr := cfg.DisplayNameAttr
|
||||
if displayNameAttr == "" {
|
||||
displayNameAttr = "displayName"
|
||||
}
|
||||
|
||||
resolvedUsername := firstAttr(entry, usernameAttr, "sAMAccountName", "uid", "userPrincipalName")
|
||||
if resolvedUsername == "" {
|
||||
resolvedUsername = username
|
||||
}
|
||||
email := firstAttr(entry, emailAttr, "mail", "userPrincipalName")
|
||||
displayName := firstAttr(entry, displayNameAttr, "cn", "displayName")
|
||||
if displayName == "" {
|
||||
displayName = resolvedUsername
|
||||
}
|
||||
|
||||
return authenticateResponse{
|
||||
Success: true,
|
||||
Dn: userDn,
|
||||
Username: resolvedUsername,
|
||||
Email: email,
|
||||
DisplayName: displayName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
})
|
||||
|
||||
mux.HandleFunc("/discover", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, discoverResponse{Success: false, Error: "Метод не поддерживается"})
|
||||
return
|
||||
}
|
||||
|
||||
var req authenticateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, discoverResponse{Success: false, Error: "Некорректное тело запроса"})
|
||||
return
|
||||
}
|
||||
|
||||
result, err := discover(req.Config)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, discoverResponse{Success: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
})
|
||||
|
||||
mux.HandleFunc("/authenticate", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, authenticateResponse{Success: false, Error: "Метод не поддерживается"})
|
||||
return
|
||||
}
|
||||
|
||||
var req authenticateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, authenticateResponse{Success: false, Error: "Некорректное тело запроса"})
|
||||
return
|
||||
}
|
||||
|
||||
result, err := authenticate(req.Config, req.Username, req.Password)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusUnauthorized, authenticateResponse{Success: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
})
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":" + env("PORT", "8086"),
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("LDAP Auth запущен на %s", server.Addr)
|
||||
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatalf("Ошибка HTTP сервера: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-stop
|
||||
log.Println("Остановка LDAP Auth...")
|
||||
}
|
||||
Reference in New Issue
Block a user