package main import ( "fmt" "log" "net/http" "net/url" "strings" "github.com/go-ldap/ldap/v3" ) type profileField struct { Name string Label string Value string } type profileAlias struct { Value string URLSafe string ElemID string Token string } var ldapLabels = map[string]string{ "cn": "Full name", "uid": "Username", "givenName": "First name", "sn": "Last name", "displayName": "Display name", "telephoneNumber": "Phone", "mobile": "Mobile", "employeeNumber": "Employee ID", "o": "Organization", "ou": "Department", "title": "Title", "description": "Description", "labeledURI": "Website", } // ldapSkip lists attributes that should never be shown to the user. var ldapSkip = map[string]bool{ "userPassword": true, "krbPrincipalKey": true, "objectClass": true, "entryUUID": true, "entryDN": true, "structuralObjectClass": true, "hasSubordinates": true, "krbExtraData": true, } // isGeneratedAlias returns true for auto-generated alias local parts: // exactly 10 characters and containing at least one digit or uppercase letter, // which distinguishes them from plain words like "postmaster" or "abonnement". func isGeneratedAlias(local string) bool { if len(local) != 10 { return false } for _, c := range local { if c >= '0' && c <= '9' || c >= 'A' && c <= 'Z' { return true } } return false } func ldapLabel(name string) string { if l, ok := ldapLabels[name]; ok { return l } return name } func login(login string, password string) ([]*ldap.EntryAttribute, error) { conn, err := myLDAP.Connect() if err != nil || conn == nil { return nil, err } if err = conn.ServiceBind(); err != nil { return nil, err } var dn string dn, err = conn.SearchDN(login, true) if err != nil { dn, err = conn.SearchDN(login, false) if err != nil { // User not found: perform a dummy bind to prevent username enumeration via timing. conn.Bind("cn=dummy,"+myLDAP.BaseDN, password) return nil, err } } if err := conn.Bind(dn, password); err != nil { return nil, err } if entries, err := conn.GetEntry(dn); err != nil { return nil, err } else { return entries, nil } } func tryLogin(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { displayTmpl(w, "login.html", map[string]any{}) return } if !authLimiter.Allow(remoteIP(r)) { displayTmplError(w, http.StatusTooManyRequests, "login.html", map[string]any{"error": "Too many login attempts. Please try again later."}) return } if !validateAltcha(r) { displayTmplError(w, http.StatusForbidden, "login.html", map[string]any{"error": "Invalid or missing altcha response. Please try again."}) return } loginName := r.PostFormValue("login") entries, err := login(loginName, r.PostFormValue("password")) if err != nil { log.Println(err) displayTmplError(w, http.StatusInternalServerError, "login.html", map[string]any{"error": err.Error()}) return } apiToken := AddyAPIToken(loginName) var fields []profileField var emails []string var aliases []profileAlias aliasIdx := 0 for _, e := range entries { if ldapSkip[e.Name] { continue } for _, v := range e.Values { switch { case e.Name == "mail": emails = append(emails, v) case e.Name == "mailAlias" && isGeneratedAlias(strings.SplitN(v, "@", 2)[0]): elemID := fmt.Sprintf("alias-%d", aliasIdx) aliasIdx++ aliases = append(aliases, profileAlias{ Value: v, URLSafe: url.PathEscape(v), ElemID: elemID, Token: apiToken, }) case e.Name == "mailAlias": emails = append(emails, v) default: fields = append(fields, profileField{ Name: e.Name, Label: ldapLabel(e.Name), Value: v, }) } } } displayTmpl(w, "profile.html", map[string]any{ "login": loginName, "fields": fields, "emails": emails, "aliases": aliases, "api_token": apiToken, "card_wide": true, }) } func httpBasicAuth(w http.ResponseWriter, r *http.Request) { if !authLimiter.Allow(remoteIP(r)) { w.Header().Set("WWW-Authenticate", `Basic realm="nemunai.re restricted"`) w.WriteHeader(http.StatusTooManyRequests) w.Write([]byte("Too many requests")) return } if user, pass, ok := r.BasicAuth(); ok { if entries, err := login(user, pass); err != nil { w.Header().Set("WWW-Authenticate", `Basic realm="nemunai.re restricted"`) w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(err.Error())) return } else { w.Header().Set("X-Remote-User", user) w.WriteHeader(http.StatusOK) for _, e := range entries { for _, v := range e.Values { if e.Name != "userPassword" { fmt.Fprintf(w, "%s: %s", e.Name, v) } } } return } } else if dockerRegistrySecret != "" && r.Header.Get("X-Special-Auth") == dockerRegistrySecret { method := r.Header.Get("X-Original-Method") uri := r.Header.Get("X-Original-URI") if (method == "GET" || method == "HEAD") && uri != "" && uri != "/" && uri != "/v2/" && !strings.HasPrefix(uri, "/v2/_") { log.Printf("docker-registry: Permit anonymous login for URL %s", uri) w.Header().Set("X-Remote-User", "anonymous") w.WriteHeader(http.StatusOK) return } } w.Header().Set("WWW-Authenticate", `Basic realm="nemunai.re restricted"`) w.WriteHeader(http.StatusUnauthorized) w.Write([]byte("Please login")) }