diff --git a/addy.go b/addy.go
index 7a01e9a..c2878e1 100644
--- a/addy.go
+++ b/addy.go
@@ -12,7 +12,6 @@ import (
"log"
"net/http"
"os"
- "slices"
"strings"
)
@@ -135,7 +134,14 @@ func addyAliasAPI(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Alias creation is not configured", http.StatusServiceUnavailable)
return
}
- if !slices.Contains(allowedAliasDomains, body.Domain) {
+ domainAllowed := false
+ for _, d := range allowedAliasDomains {
+ if body.Domain == d {
+ domainAllowed = true
+ break
+ }
+ }
+ if !domainAllowed {
http.Error(w, "Domain not allowed", http.StatusBadRequest)
return
}
diff --git a/change.go b/change.go
index d915aba..0a9e7e6 100644
--- a/change.go
+++ b/change.go
@@ -33,7 +33,7 @@ func checkPasswdConstraint(password string) error {
func changePassword(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" && !changeLimiter.Allow(remoteIP(r)) {
csrfToken, _ := setCSRFToken(w)
- displayTmplError(w, http.StatusTooManyRequests, "change.html", map[string]any{"error": "Too many requests. Please try again later.", "csrf_token": csrfToken})
+ displayTmplError(w, http.StatusTooManyRequests, "change.html", map[string]interface{}{"error": "Too many requests. Please try again later.", "csrf_token": csrfToken})
return
}
@@ -43,25 +43,25 @@ func changePassword(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
- displayTmpl(w, "change.html", map[string]any{"csrf_token": csrfToken})
+ displayTmpl(w, "change.html", map[string]interface{}{"csrf_token": csrfToken})
return
}
if !validateCSRF(r) {
csrfToken, _ := setCSRFToken(w)
- displayTmplError(w, http.StatusForbidden, "change.html", map[string]any{"error": "Invalid or missing CSRF token. Please try again.", "csrf_token": csrfToken})
+ displayTmplError(w, http.StatusForbidden, "change.html", map[string]interface{}{"error": "Invalid or missing CSRF token. Please try again.", "csrf_token": csrfToken})
return
}
if !validateAltcha(r) {
csrfToken, _ := setCSRFToken(w)
- displayTmplError(w, http.StatusForbidden, "change.html", map[string]any{"error": "Invalid or missing altcha response. Please try again.", "csrf_token": csrfToken})
+ displayTmplError(w, http.StatusForbidden, "change.html", map[string]interface{}{"error": "Invalid or missing altcha response. Please try again.", "csrf_token": csrfToken})
return
}
renderError := func(status int, msg string) {
csrfToken, _ := setCSRFToken(w)
- displayTmplError(w, status, "change.html", map[string]any{"error": msg, "csrf_token": csrfToken})
+ displayTmplError(w, status, "change.html", map[string]interface{}{"error": msg, "csrf_token": csrfToken})
}
// Check the two new passwords are identical
diff --git a/login.go b/login.go
index 63af618..44ffe66 100644
--- a/login.go
+++ b/login.go
@@ -2,6 +2,8 @@ package main
import (
"fmt"
+ "html"
+ "html/template"
"log"
"net/http"
"net/url"
@@ -10,69 +12,6 @@ import (
"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 {
@@ -105,71 +44,47 @@ func login(login string, password string) ([]*ldap.EntryAttribute, error) {
func tryLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
- displayTmpl(w, "login.html", map[string]any{})
+ displayTmpl(w, "login.html", map[string]interface{}{})
return
}
if !authLimiter.Allow(remoteIP(r)) {
- displayTmplError(w, http.StatusTooManyRequests, "login.html", map[string]any{"error": "Too many login attempts. Please try again later."})
+ displayTmplError(w, http.StatusTooManyRequests, "login.html", map[string]interface{}{"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."})
+ displayTmplError(w, http.StatusForbidden, "login.html", map[string]interface{}{"error": "Invalid or missing altcha response. Please try again."})
return
}
- loginName := r.PostFormValue("login")
- entries, err := login(loginName, r.PostFormValue("password"))
- if err != nil {
+ if entries, err := login(r.PostFormValue("login"), r.PostFormValue("password")); err != nil {
log.Println(err)
- displayTmplError(w, http.StatusInternalServerError, "login.html", map[string]any{"error": err.Error()})
- return
- }
+ displayTmplError(w, http.StatusInternalServerError, "login.html", map[string]interface{}{"error": err.Error()})
+ } else {
+ apiToken := AddyAPIToken(r.PostFormValue("login"))
- 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,
- })
+ cnt := "
"
+ for _, e := range entries {
+ for i, v := range e.Values {
+ safeName := html.EscapeString(e.Name)
+ safeVal := html.EscapeString(v)
+ elemID := fmt.Sprintf("mailAlias-%d", i)
+ if e.Name == "userPassword" || e.Name == "krbPrincipalKey" {
+ cnt += "- " + safeName + ": [...]
"
+ } else if e.Name == "mailAlias" && len(strings.SplitN(v, "@", 2)[0]) == 10 {
+ safeURL := url.PathEscape(v)
+ safeToken := html.EscapeString(apiToken)
+ safeElemID := html.EscapeString(elemID)
+ cnt += `- ` + safeName + `: ` + safeVal +
+ `
`
+ } else {
+ cnt += "- " + safeName + ": " + safeVal + "
"
+ }
}
}
+ displayTmpl(w, "message.html", map[string]interface{}{"details": template.HTML(`Login ok
Here are the information we have about you:` + cnt + "
To use our Addy.io compatible API, use the following token: " + html.EscapeString(apiToken) + "
")})
}
-
- 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) {
@@ -192,7 +107,7 @@ func httpBasicAuth(w http.ResponseWriter, r *http.Request) {
for _, e := range entries {
for _, v := range e.Values {
if e.Name != "userPassword" {
- fmt.Fprintf(w, "%s: %s", e.Name, v)
+ w.Write([]byte(fmt.Sprintf("%s: %s", e.Name, v)))
}
}
}
diff --git a/lost.go b/lost.go
index 7afe093..1e4c9d3 100644
--- a/lost.go
+++ b/lost.go
@@ -88,7 +88,7 @@ func lostPasswordToken(conn *LDAPConn, login string) (string, string, error) {
func lostPassword(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" && !lostLimiter.Allow(remoteIP(r)) {
- displayTmplError(w, http.StatusTooManyRequests, "lost.html", map[string]any{"error": "Too many requests. Please try again later."})
+ displayTmplError(w, http.StatusTooManyRequests, "lost.html", map[string]interface{}{"error": "Too many requests. Please try again later."})
return
}
@@ -98,17 +98,17 @@ func lostPassword(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
- displayTmpl(w, "lost.html", map[string]any{"csrf_token": csrfToken})
+ displayTmpl(w, "lost.html", map[string]interface{}{"csrf_token": csrfToken})
return
}
if !validateCSRF(r) {
- displayTmplError(w, http.StatusForbidden, "lost.html", map[string]any{"error": "Invalid or missing CSRF token. Please try again."})
+ displayTmplError(w, http.StatusForbidden, "lost.html", map[string]interface{}{"error": "Invalid or missing CSRF token. Please try again."})
return
}
if !validateAltcha(r) {
- displayTmplError(w, http.StatusForbidden, "lost.html", map[string]any{"error": "Invalid or missing altcha response. Please try again."})
+ displayTmplError(w, http.StatusForbidden, "lost.html", map[string]interface{}{"error": "Invalid or missing altcha response. Please try again."})
return
}
@@ -116,7 +116,7 @@ func lostPassword(w http.ResponseWriter, r *http.Request) {
conn, err := myLDAP.Connect()
if err != nil || conn == nil {
log.Println(err)
- displayTmplError(w, http.StatusInternalServerError, "lost.html", map[string]any{"error": "Unable to process your request. Please try again later."})
+ displayTmplError(w, http.StatusInternalServerError, "lost.html", map[string]interface{}{"error": "Unable to process your request. Please try again later."})
return
}
@@ -167,7 +167,7 @@ func lostPassword(w http.ResponseWriter, r *http.Request) {
s, err = d.Dial()
if err != nil {
log.Println("Unable to connect to email server: " + err.Error())
- displayTmplError(w, http.StatusInternalServerError, "lost.html", map[string]any{"error": "Unable to send password recovery email. Please try again later."})
+ displayTmplError(w, http.StatusInternalServerError, "lost.html", map[string]interface{}{"error": "Unable to send password recovery email. Please try again later."})
return
}
} else {
@@ -202,7 +202,7 @@ func lostPassword(w http.ResponseWriter, r *http.Request) {
if err := gomail.Send(s, m); err != nil {
log.Println("Unable to send email: " + err.Error())
- displayTmplError(w, http.StatusInternalServerError, "lost.html", map[string]any{"error": "Unable to send password recovery email. Please try again later."})
+ displayTmplError(w, http.StatusInternalServerError, "lost.html", map[string]interface{}{"error": "Unable to send password recovery email. Please try again later."})
return
}
diff --git a/main.go b/main.go
index 32a1124..36ab61d 100644
--- a/main.go
+++ b/main.go
@@ -5,7 +5,7 @@ import (
"encoding/json"
"flag"
"fmt"
- "io"
+ "io/ioutil"
"log"
"net/http"
"net/url"
@@ -19,8 +19,6 @@ import (
var myPublicURL = "https://ldap.nemunai.re"
var devMode bool
-var brandName = "chldapasswd"
-var brandLogo = ""
// dockerRegistrySecret is required for X-Special-Auth anonymous access.
// If empty, the feature is disabled.
@@ -84,20 +82,10 @@ func main() {
var configfile = flag.String("config", "", "path to the configuration file")
var publicURL = flag.String("public-url", myPublicURL, "Public base URL used in password reset emails")
var dev = flag.Bool("dev", false, "Development mode: disables HSTS and cookie Secure flag for local HTTP testing")
- var bname = flag.String("brand-name", "chldapasswd", "Brand name displayed in the UI")
- var blogo = flag.String("brand-logo", "", "URL of brand logo displayed in the UI (added to CSP img-src)")
flag.Parse()
myPublicURL = *publicURL
devMode = *dev
- brandName = *bname
- brandLogo = *blogo
- if val, ok := os.LookupEnv("BRAND_NAME"); ok {
- brandName = val
- }
- if val, ok := os.LookupEnv("BRAND_LOGO"); ok {
- brandLogo = val
- }
if devMode {
log.Println("WARNING: running in development mode — security features relaxed, do not use in production")
}
@@ -116,7 +104,7 @@ func main() {
if configfile != nil && *configfile != "" {
if fd, err := os.Open(*configfile); err != nil {
log.Fatal(err)
- } else if cnt, err := io.ReadAll(fd); err != nil {
+ } else if cnt, err := ioutil.ReadAll(fd); err != nil {
log.Fatal(err)
} else if err := json.Unmarshal(cnt, &myLDAP); err != nil {
log.Fatal(err)
@@ -149,7 +137,7 @@ func main() {
if val, ok := os.LookupEnv("LDAP_SERVICE_PASSWORD_FILE"); ok {
if fd, err := os.Open(val); err != nil {
log.Fatal(err)
- } else if cnt, err := io.ReadAll(fd); err != nil {
+ } else if cnt, err := ioutil.ReadAll(fd); err != nil {
log.Fatal(err)
} else {
myLDAP.ServicePassword = string(cnt)
@@ -232,7 +220,6 @@ func main() {
// Register handlers
http.HandleFunc(fmt.Sprintf("GET %s/altcha.min.js", *baseURL), serveAltchaJS)
- http.HandleFunc(fmt.Sprintf("GET %s/style.css", *baseURL), serveStyleCSS)
http.HandleFunc(fmt.Sprintf("GET %s/altcha-challenge", *baseURL), serveAltchaChallenge)
http.HandleFunc(fmt.Sprintf("%s/{$}", *baseURL), changePassword)
http.HandleFunc(fmt.Sprintf("POST %s/api/v1/aliases", *baseURL), addyAliasAPI)
@@ -252,8 +239,7 @@ func main() {
go func() {
log.Fatal(srv.ListenAndServe())
}()
- log.Printf("Using LDAP server at %s:%d (baseDN: %s)", myLDAP.Host, myLDAP.Port, myLDAP.BaseDN)
- log.Printf("Ready, listening on %s", *bind)
+ log.Println(fmt.Sprintf("Ready, listening on %s", *bind))
// Wait shutdown signal
<-interrupt
diff --git a/reset.go b/reset.go
index a65c27c..a37f99d 100644
--- a/reset.go
+++ b/reset.go
@@ -16,7 +16,7 @@ func resetPassword(w http.ResponseWriter, r *http.Request) {
return
}
- base := map[string]any{
+ base := map[string]interface{}{
"login": r.URL.Query().Get("l"),
"token": r.URL.Query().Get("t"),
}
diff --git a/static.go b/static.go
index 6c5f316..4946083 100644
--- a/static.go
+++ b/static.go
@@ -5,8 +5,6 @@ import (
"html/template"
"log"
"net/http"
- "net/url"
- "strings"
)
func securityHeaders(next http.Handler) http.Handler {
@@ -14,16 +12,7 @@ func securityHeaders(next http.Handler) http.Handler {
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
-
- imgSrc := "'self' data:"
- if strings.HasPrefix(brandLogo, "http://") || strings.HasPrefix(brandLogo, "https://") {
- if u, err := url.Parse(brandLogo); err == nil {
- imgSrc += " " + u.Scheme + "://" + u.Host
- }
- }
- csp := "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline'; style-src 'self' 'sha256-W6z8OR2iqpPyNGe72eRXH58H75H3UVJDuwHoKA6pX98='; img-src " + imgSrc + "; worker-src blob:"
- w.Header().Set("Content-Security-Policy", csp)
-
+ w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline' https://stackpath.bootstrapcdn.com; style-src https://stackpath.bootstrapcdn.com; img-src 'self'; font-src https://stackpath.bootstrapcdn.com")
if !devMode {
w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
}
@@ -34,26 +23,7 @@ func securityHeaders(next http.Handler) http.Handler {
//go:embed all:static
var assets embed.FS
-func serveStyleCSS(w http.ResponseWriter, r *http.Request) {
- data, err := assets.ReadFile("static/style.css")
- if err != nil {
- http.NotFound(w, r)
- return
- }
- w.Header().Set("Content-Type", "text/css; charset=utf-8")
- w.Header().Set("Cache-Control", "public, max-age=3600")
- w.Write(data)
-}
-
-func displayTmpl(w http.ResponseWriter, page string, vars map[string]any) {
- if vars == nil {
- vars = map[string]any{}
- }
- vars["brand_name"] = brandName
- if brandLogo != "" {
- vars["brand_logo"] = brandLogo
- }
-
+func displayTmpl(w http.ResponseWriter, page string, vars map[string]interface{}) {
data, err := assets.ReadFile("static/" + page)
if err != nil {
log.Fatalf("Unable to find %q: %s", page, err.Error())
@@ -75,7 +45,7 @@ func displayTmpl(w http.ResponseWriter, page string, vars map[string]any) {
tpl.ExecuteTemplate(w, "page", vars)
}
-func displayTmplError(w http.ResponseWriter, statusCode int, page string, vars map[string]any) {
+func displayTmplError(w http.ResponseWriter, statusCode int, page string, vars map[string]interface{}) {
w.WriteHeader(statusCode)
displayTmpl(w, page, vars)
}
@@ -88,5 +58,5 @@ func displayMsg(w http.ResponseWriter, msg string, statusCode int) {
label = "message"
}
- displayTmpl(w, "message.html", map[string]any{label: msg})
+ displayTmpl(w, "message.html", map[string]interface{}{label: msg})
}
diff --git a/static/change.html b/static/change.html
index 5b6fcaf..424b171 100644
--- a/static/change.html
+++ b/static/change.html
@@ -1,46 +1,49 @@
-{{template "header" .}}
- Change your password
- Fill the following fields!
+{{template "header"}}
+ Change your password Fill the following fields!