package main import ( "fmt" "log" mrand "math/rand" "net/http" "os" "path" "strings" "text/template" "time" "github.com/gorilla/mux" "github.com/tredoe/osutil/v2/userutil/crypt/sha512_crypt" "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 } 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) }) } func main() { f := forms.NewForm() f.ContainerClasses = "relative mx-auto gap-x-16 bg-slate-100 w-full lg-w-1/2 lg:px-8 lg:pt-16" f.Classes = " py-2 text-indigo-300 md:px-10 lg:col-start-2 lg:row-start-1 lg:mx-auto lg:w-full lg:max-w-lg lg:bg-transparent lg:px-0 " f.InputClasses = " w-full mr-12 pr-12 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" f.LabelClasses = "block text-sm font-medium text-gray-700" f.ErrorClasses = "text-red-800 font-bold" f.Autocomplete = "one-time-code" e := forms.NewElement() e.Label = "Email Address" e.Type = "text" e.Validator = "email" e.FailMessage = "Must be a valid email address" e.Name = "field1" f.Add(e) e = forms.NewElement() e.Label = "Old" e.Type = "password" e.Validator = "" e.FailMessage = "Passsword Incorrect" e.Name = "field2" f.Add(e) e = forms.NewElement() e.Label = "New" e.Type = "password" e.FailMessage = "Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter and one digit" e.Validator = "minlength=8;haslowercase;hasuppercase;hasdigit" e.Name = "field3" f.Add(e) e = forms.NewElement() e.Label = "Confirm " e.Type = "password" e.FailMessage = "Passwords do not match" e.Validator = "mustmatch=field3" e.Name = "field4" 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) f.Route = "/chpass/changepassword" 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 r := mux.NewRouter() r.Use(mwLog) r.HandleFunc("/style.css", http.FileServer(http.Dir("./")).ServeHTTP) r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { d.Body = f.Render(false) tmpl.Execute(w, d) }) r.HandleFunc("/changepassword", func(w http.ResponseWriter, r *http.Request) { 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 p := pop3.New(pop3.Opt{Host: "teamworkapps.com", Port: 995, TLSEnabled: true}) c, err := p.NewConn() if err != nil { log.Println(err) d.Body = "Internal error" tmpl.Execute(w, d) return } defer c.Quit() // Authenticate. if err := c.Auth(f.GetValue("field1"), f.GetValue("field2")); err != nil { f.MakeInvalid("theoldpassword", "Invalid password") d.Body = f.Render(true) tmpl.Execute(w, d) return } // Change the password if !modifyPasswordFile(f.GetValue("field1"), f.GetValue("field3")) { log.Println(err) d.Body = "Internal error" tmpl.Execute(w, d) return } d.Body = "Password changed. You will need to login again." tmpl.Execute(w, d) }) d.Time = time.Now().Unix() d.Body = f.Render(true) tmpl.Execute(os.Stdout, d) http.ListenAndServe("127.0.0.1:8910", r) } /* 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 { email = strings.ToLower(email) log.Printf("Changing password for %s", email) // read the password file // find the entry with the given email // replace the password with the new password // write the file back in, err := os.ReadFile("/root/docker/mail/config/postfix-accounts.cf") if err != nil { log.Println(err) return false } out, err := os.Create("/root/docker/mail/config/postfix-accounts.cf.tmp") if err != nil { log.Println(err) return false } lines := strings.Split(string(in), "\n") for _, line := range lines { tmp := strings.Split(line, "|") if len(tmp) != 2 { continue } if tmp[0] == email { line = createPasswordEntry(email, password) } out.WriteString(line) out.WriteString("\n") } out.Close() os.Rename("/root/docker/mail/config/postfix-accounts.cf", "/root/docker/mail/config/postfix-accounts.cf.old") os.Rename("/root/docker/mail/config/postfix-accounts.cf.tmp", "/root/docker/mail/config/postfix-accounts.cf") log.Println("Successfully changed password") return true } func createPasswordEntry(email, password string) string { salt := RandStringBytesMaskImprSrc(16) 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) } const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" const ( letterIdxBits = 6 // 6 bits to represent a letter index letterIdxMask = 1<= 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-- } return string(b) }