docker-mailserver-passwords/main.go

229 lines
6.5 KiB
Go
Raw Normal View History

2024-02-19 16:47:27 +00:00
package main
import (
2024-05-07 11:54:41 +00:00
"fmt"
2024-02-19 16:47:27 +00:00
"log"
2024-05-07 12:39:38 +00:00
mrand "math/rand"
2024-02-19 16:47:27 +00:00
"net/http"
"os"
"path"
2024-05-07 11:54:41 +00:00
"strings"
2024-02-19 16:47:27 +00:00
"text/template"
"time"
2024-02-19 17:08:07 +00:00
"github.com/gorilla/mux"
2024-05-07 13:00:01 +00:00
"github.com/tredoe/osutil/v2/userutil/crypt/sha512_crypt"
2024-02-19 17:08:07 +00:00
2024-02-19 16:47:27 +00:00
"git.teamworkapps.com/shortcut/forms"
"github.com/knadh/go-pop3"
)
// We use my forms package to create a password change form which on success executes a docker call to change the password
type tplData struct {
Time int64
Body string
}
2024-02-19 17:08:07 +00:00
func mwLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.RemoteAddr, r.URL.String())
next.ServeHTTP(w, r)
})
}
2024-02-19 16:47:27 +00:00
func main() {
2024-10-08 22:35:47 +00:00
forms.GlobalStyles.ContainerClasses = " bg-gray-200 justify-center py-6 rounded-lg shadow-lg"
2024-10-08 22:36:46 +00:00
forms.GlobalStyles.ItemClasses = "px-1 py-2"
2024-02-19 16:47:27 +00:00
forms.GlobalStyles.ErrorClasses = "text-red-600 text-right font-bold text-xl"
2024-10-08 22:36:46 +00:00
forms.GlobalStyles.LabelClasses = "text-sm font-medium leading-6 text-gray-900"
forms.GlobalStyles.InputClasses = "rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
2024-02-19 16:47:27 +00:00
f := forms.NewForm()
e := forms.NewElement()
e.Label = "Email Address"
e.Type = "text"
e.Validator = "email"
2024-02-20 16:52:37 +00:00
e.FailMessage = "Must be a valid email address"
2024-02-19 16:47:27 +00:00
e.Name = "email"
f.Add(e)
e = forms.NewElement()
e.Label = "Old Password"
e.Type = "password"
e.Validator = ""
2024-02-20 16:52:37 +00:00
e.FailMessage = "Passsword Incorrect"
2024-02-19 16:47:27 +00:00
e.Name = "oldpassword"
f.Add(e)
e = forms.NewElement()
e.Label = "New Password"
e.Type = "password"
2024-02-20 16:52:37 +00:00
e.FailMessage = "Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter and one digit"
2024-02-19 16:47:27 +00:00
e.Validator = "minlength=8;haslowercase;hasuppercase;hasdigit"
e.Name = "newpassword"
f.Add(e)
e = forms.NewElement()
e.Label = "Confirm Password"
e.Type = "password"
2024-02-20 16:52:37 +00:00
e.FailMessage = "Passwords do not match"
2024-02-19 16:47:27 +00:00
e.Validator = "mustmatch=newpassword"
e.Name = "confirmpassword"
f.Add(e)
e = forms.NewElement()
e.Label = "Change Password"
e.Type = "submit"
e.InputClasses = "flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
f.Add(e)
2024-02-19 17:15:51 +00:00
f.Route = "/chpass/changepassword"
2024-02-19 16:47:27 +00:00
out, err := f.ToJSON()
if err == nil {
os.WriteFile("form.json", out, 0644)
}
tmpFile := "template.html"
tmpl, err := template.New(path.Base(tmpFile)).ParseFiles(tmpFile)
if err != nil {
log.Fatal(err)
}
var d tplData
2024-02-19 17:08:07 +00:00
r := mux.NewRouter()
r.Use(mwLog)
r.HandleFunc("/style.css", http.FileServer(http.Dir("./")).ServeHTTP)
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
2024-02-19 17:12:31 +00:00
d.Body = f.Render(false)
2024-02-19 16:47:27 +00:00
tmpl.Execute(w, d)
})
2024-02-19 17:08:07 +00:00
r.HandleFunc("/changepassword", func(w http.ResponseWriter, r *http.Request) {
2024-02-19 16:47:27 +00:00
if f.Validate(r) != nil {
d.Body = f.Render(true)
tmpl.Execute(w, d)
return
}
// Test if the given username ans password can authenticate via pop3
// If so, change the password
2024-02-19 19:35:01 +00:00
p := pop3.New(pop3.Opt{Host: "teamworkapps.com", Port: 995, TLSEnabled: true})
2024-02-19 16:47:27 +00:00
c, err := p.NewConn()
if err != nil {
2024-02-19 17:18:08 +00:00
log.Println(err)
2024-02-20 16:52:37 +00:00
d.Body = "Internal error"
2024-02-19 16:47:27 +00:00
tmpl.Execute(w, d)
return
}
defer c.Quit()
// Authenticate.
if err := c.Auth(f.GetValue("email"), f.GetValue("oldpassword")); err != nil {
f.MakeInvalid("oldpassword", "Invalid password")
d.Body = f.Render(true)
tmpl.Execute(w, d)
return
}
2024-05-07 11:54:41 +00:00
// Change the password
if !modifyPasswordFile(f.GetValue("email"), f.GetValue("newpassword")) {
2024-02-20 16:52:37 +00:00
log.Println(err)
d.Body = "Internal error"
tmpl.Execute(w, d)
return
}
2024-02-20 17:16:36 +00:00
d.Body = "Password changed. You will need to login again."
2024-02-20 16:52:37 +00:00
tmpl.Execute(w, d)
2024-02-19 16:47:27 +00:00
})
d.Time = time.Now().Unix()
d.Body = f.Render(true)
tmpl.Execute(os.Stdout, d)
2024-02-19 17:14:26 +00:00
http.ListenAndServe("127.0.0.1:8910", r)
2024-02-19 16:47:27 +00:00
}
2024-05-07 11:54:41 +00:00
/*
example password entry:
recreation@pinnaclelake.com|{SHA512-CRYPT}$6$uTiy7Q5xn1CIN22w$3VAElns3TFfejtdTCTJMcz8k2UPPwAmlUIoXC4NgPJRnDcgo3CnI91EsB6irgwuecCrolxAx7i4mjvTxPaAlf0
*/
// createPasswordEntry: input email and plain text password, output a password entry with sha512-crypt.
func modifyPasswordFile(email, password string) bool {
2024-10-08 22:27:12 +00:00
email = strings.ToLower(email)
2024-05-07 16:42:20 +00:00
log.Printf("Changing password for %s", email)
2024-05-07 11:54:41 +00:00
// read the password file
// find the entry with the given email
// replace the password with the new password
// write the file back
2024-05-07 11:55:22 +00:00
in, err := os.ReadFile("/root/docker/mail/config/postfix-accounts.cf")
2024-05-07 11:54:41 +00:00
if err != nil {
log.Println(err)
return false
}
2024-05-07 11:55:22 +00:00
out, err := os.Create("/root/docker/mail/config/postfix-accounts.cf.tmp")
2024-05-07 11:54:41 +00:00
if err != nil {
log.Println(err)
return false
}
lines := strings.Split(string(in), "\n")
for _, line := range lines {
2024-10-08 22:27:12 +00:00
tmp := strings.Split(line, "|")
2024-05-07 16:42:20 +00:00
if len(tmp) != 2 {
continue
}
2024-10-08 22:27:12 +00:00
if tmp[0] == email {
2024-05-07 11:54:41 +00:00
line = createPasswordEntry(email, password)
}
out.WriteString(line)
out.WriteString("\n")
}
out.Close()
2024-05-07 12:11:54 +00:00
os.Rename("/root/docker/mail/config/postfix-accounts.cf", "/root/docker/mail/config/postfix-accounts.cf.old")
2024-05-07 11:55:22 +00:00
os.Rename("/root/docker/mail/config/postfix-accounts.cf.tmp", "/root/docker/mail/config/postfix-accounts.cf")
2024-05-07 16:42:20 +00:00
log.Println("Successfully changed password")
2024-05-07 11:54:41 +00:00
return true
}
func createPasswordEntry(email, password string) string {
2024-05-07 12:47:28 +00:00
salt := RandStringBytesMaskImprSrc(16)
2024-05-07 12:39:38 +00:00
2024-05-07 13:00:01 +00:00
c := sha512_crypt.New()
hash, err := c.Generate([]byte(password), []byte("$6$"+salt))
if err != nil {
log.Println(err)
return ""
}
return fmt.Sprintf("%s|{SHA512-CRYPT}%s", email, hash)
2024-05-07 11:54:41 +00:00
}
2024-05-07 12:11:11 +00:00
2024-05-07 12:39:38 +00:00
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
// http://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang
func RandStringBytesMaskImprSrc(n int) string {
var src = mrand.NewSource(time.Now().UnixNano())
b := make([]byte, n)
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
b[i] = letterBytes[idx]
i--
}
cache >>= letterIdxBits
remain--
2024-05-07 12:11:11 +00:00
}
2024-05-07 12:39:38 +00:00
return string(b)
2024-05-07 12:11:11 +00:00
}