Add an API compatibly with addy.io to generate aliases
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
b197fcd9af
commit
e6a4271a75
164
addy.go
Normal file
164
addy.go
Normal file
@ -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)
|
||||
}
|
31
ldap.go
31
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)
|
||||
}
|
||||
|
2
login.go
2
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<br><br>Here are the information we have about you:` + cnt + "</ul>")})
|
||||
displayTmpl(w, "message.html", map[string]interface{}{"details": template.HTML(`Login ok<br><br>Here are the information we have about you:` + cnt + "</ul><p>To use our Addy.io compatible API, use the following token: <code>" + AddyAPIToken(r.PostFormValue("login")) + "</code></p>")})
|
||||
}
|
||||
}
|
||||
|
||||
|
1
main.go
1
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)
|
||||
|
Loading…
Reference in New Issue
Block a user