From e6a4271a751730b1ce0e853a1bff2ea62ca4d885 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 31 May 2024 14:08:05 +0200 Subject: [PATCH] Add an API compatibly with addy.io to generate aliases --- addy.go | 164 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ ldap.go | 31 +++++++++++ login.go | 2 +- main.go | 1 + 4 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 addy.go diff --git a/addy.go b/addy.go new file mode 100644 index 0000000..fcb76d4 --- /dev/null +++ b/addy.go @@ -0,0 +1,164 @@ +package main + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/base32" + "encoding/json" + "flag" + "fmt" + "log" + "math/rand" + "net/http" + "os" + "strings" +) + +var apiSecret string + +type addyForm struct { + Alias string `json:"alias"` + Description string `json:"description"` + Domain string `json:"domain"` +} + +type addyResponse struct { + Data addyResponseData `json:"data"` +} +type addyResponseData struct { + Email string `json:"email"` +} + +func init() { + if val, ok := os.LookupEnv("ADDY_API_SECRET"); ok { + apiSecret = val + } + + flag.StringVar(&apiSecret, "addy-api-secret", apiSecret, "Secret used for the HMAC protecting addy.io-like API") +} + +func AddyAPISignature(username string) []byte { + mac := hmac.New(sha256.New224, []byte(apiSecret)) + mac.Write([]byte(username)) + return mac.Sum(nil) +} + +func AddyAPIToken(username string) string { + chain := []byte(username + ":") + chain = append(chain, AddyAPISignature(username)...) + return base32.StdEncoding.EncodeToString(chain) +} + +func checkAddyApiAuthorization(authorization []byte) *string { + fields := bytes.SplitN(authorization, []byte(":"), 2) + if len(fields) != 2 { + return nil + } + + username := string(fields[0]) + + expectedSign := AddyAPISignature(username) + if !hmac.Equal(expectedSign, fields[1]) { + return nil + } + + return &username +} + +func addyAliasAPI(w http.ResponseWriter, r *http.Request) { + // Check authorization header + fields := strings.Fields(r.Header.Get("Authorization")) + if len(fields) != 2 || fields[0] != "Bearer" { + http.Error(w, "Authorization header should be a valid Bearer token", http.StatusUnauthorized) + return + } + + // Decode header + authorization, err := base32.StdEncoding.DecodeString(fields[1]) + if err != nil { + log.Println("Invalid Authorization header: %s", err.Error()) + http.Error(w, "Authorization header should be a valid Bearer token", http.StatusUnauthorized) + return + } + + user := checkAddyApiAuthorization(authorization) + if user == nil { + http.Error(w, "Not authorized", http.StatusUnauthorized) + return + } + + // Decode body + var body addyForm + err = json.NewDecoder(r.Body).Decode(&body) + if err != nil { + http.Error(w, "Invalid body", http.StatusBadRequest) + return + } + + conn, err := myLDAP.Connect() + if err != nil || conn == nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + err = conn.ServiceBind() + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + dn, err := conn.SearchDN(*user, true) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if len(body.Alias) == 0 { + body.Alias = generateRandomString(10) + } + + email := body.Alias + "@" + body.Domain + nb, err := conn.SearchMailAlias(email) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if nb != 0 { + http.Error(w, fmt.Sprintf("The alias %q is already used.", email), http.StatusBadRequest) + return + } + + err = conn.AddMailAlias(dn, email) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + log.Printf("New alias created for %s: %s", dn, email) + + res := addyResponse{ + Data: addyResponseData{ + Email: email, + }, + } + err = json.NewEncoder(w).Encode(res) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func generateRandomString(length int) string { + charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + result := make([]byte, length) + for i := range result { + result[i] = charset[rand.Intn(len(charset))] + } + return string(result) +} diff --git a/ldap.go b/ldap.go index 5a3e1a6..773d1a9 100644 --- a/ldap.go +++ b/ldap.go @@ -135,3 +135,34 @@ func (l LDAPConn) ChangePassword(dn string, rawpassword string) error { return l.connection.Modify(modify) } + +func (l LDAPConn) AddMailAlias(dn string, alias string) error { + modify := ldap.NewModifyRequest(dn, nil) + modify.Add("mailAlias", []string{alias}) + + return l.connection.Modify(modify) +} + +func (l LDAPConn) SearchMailAlias(address string) (int, error) { + searchRequest := ldap.NewSearchRequest( + l.BaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + fmt.Sprintf("(&(objectClass=*)(mailAlias=%s))", address), + []string{"dn"}, + nil, + ) + + sr, err := l.connection.Search(searchRequest) + if err != nil { + return -1, err + } + + return len(sr.Entries), nil +} + +func (l LDAPConn) DelMailAlias(dn string, alias string) error { + modify := ldap.NewModifyRequest(dn, nil) + modify.Delete("mailAlias", []string{alias}) + + return l.connection.Modify(modify) +} diff --git a/login.go b/login.go index af91fb1..a702e07 100644 --- a/login.go +++ b/login.go @@ -60,7 +60,7 @@ func tryLogin(w http.ResponseWriter, r *http.Request) { } } } - displayTmpl(w, "message.html", map[string]interface{}{"details": template.HTML(`Login ok

Here are the information we have about you:` + cnt + "")}) + 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: " + AddyAPIToken(r.PostFormValue("login")) + "

")}) } } diff --git a/main.go b/main.go index 4049f75..696b94e 100644 --- a/main.go +++ b/main.go @@ -149,6 +149,7 @@ func main() { // Register handlers http.HandleFunc(fmt.Sprintf("%s/", *baseURL), changePassword) + http.HandleFunc(fmt.Sprintf("POST %s/api/v1/aliases", *baseURL), addyAliasAPI) http.HandleFunc(fmt.Sprintf("%s/auth", *baseURL), httpBasicAuth) http.HandleFunc(fmt.Sprintf("%s/login", *baseURL), tryLogin) http.HandleFunc(fmt.Sprintf("%s/change", *baseURL), changePassword)