Mercurial > gemma
view controllers/pwreset.go @ 311:74559e12a59f
sql.Result.RowsAffected is a driver specific feature. Check
for it after handling errors.
author | Sascha L. Teichmann <sascha.teichmann@intevation.de> |
---|---|
date | Wed, 01 Aug 2018 17:29:52 +0200 |
parents | 4bee4ba6dc58 |
children | adceb47920fb |
line wrap: on
line source
package controllers import ( "bytes" "database/sql" "encoding/hex" "log" "math/big" "net/http" "os/exec" "strings" "sync" "text/template" "time" "gemma.intevation.de/gemma/auth" "gemma.intevation.de/gemma/config" "github.com/gorilla/mux" gomail "gopkg.in/gomail.v2" ) const ( userExistsSQL = `SELECT email_address FROM users.list_users WHERE username = $1 LIMIT 1` updatePasswordSQL = `UPDATE users.list_users SET pw = $1 WHERE username = $2` ) const ( passwordResetValid = time.Hour maxPasswordResets = 1000 maxPasswordRequestsPerUser = 5 ) var ( passwordResetRequestMailTmpl = template.Must( template.New("request").Parse(`You have requested a password change for your account {{ .User }} on {{ .HTTPS }}://{{ .Server }} Please follow this link to get to the page where you can change your password. {{ .HTTPS }}://{{ .Server }}/api/users/passwordreset/{{ .Hash }} The link is only valid for one hour. Best regards Your service team`)) passwordResetMailTmpl = template.Must( template.New("reset").Parse(`Your password for your account {{ .User }} on {{ .HTTPS }}://{{ .Server }} has been changed to {{ .Password }} Change it as soon as possible. Best regards Your service team`)) ) type timedUser struct { user string email string time time.Time } type resetRequests struct { sync.Mutex reqs map[string]*timedUser } var passwordResetRequests = func() *resetRequests { rr := &resetRequests{reqs: map[string]*timedUser{}} go func() { for { time.Sleep(2 * time.Minute) rr.removeOutdated() } }() return rr }() func (rr *resetRequests) len() int { rr.Lock() l := len(rr.reqs) rr.Unlock() return l } func (rr *resetRequests) userAllowed(user string) bool { rr.Lock() defer rr.Unlock() var count int for _, v := range rr.reqs { if v.user == user { if count++; count >= maxPasswordRequestsPerUser { return false } } } return true } func (rr *resetRequests) store(hash, user, email string) { now := time.Now() rr.Lock() rr.reqs[hash] = &timedUser{user, email, now} rr.Unlock() } func (rr *resetRequests) fetch(hash string) *timedUser { rr.Lock() defer rr.Unlock() tu := rr.reqs[hash] if tu == nil { return nil } if tu.time.Before(time.Now().Add(-passwordResetValid)) { delete(rr.reqs, hash) return nil } return tu } func (rr *resetRequests) delete(hash string) { rr.Lock() delete(rr.reqs, hash) rr.Unlock() } func (rr *resetRequests) removeOutdated() { good := time.Now().Add(-passwordResetValid) rr.Lock() defer rr.Unlock() for k, v := range rr.reqs { if v.time.Before(good) { delete(rr.reqs, k) } } } func requestMessageBody(https, user, hash, server string) string { var content = struct { User string HTTPS string Server string Hash string }{ User: user, HTTPS: https, Server: server, Hash: hash, } var buf bytes.Buffer if err := passwordResetRequestMailTmpl.Execute(&buf, &content); err != nil { log.Printf("error: %v\n", err) } return buf.String() } func changedMessageBody(https, user, password, server string) string { var content = struct { User string HTTPS string Server string Password string }{ User: user, HTTPS: https, Server: server, Password: password, } var buf bytes.Buffer if err := passwordResetMailTmpl.Execute(&buf, &content); err != nil { log.Printf("error: %v\n", err) } return buf.String() } func useHTTPS(req *http.Request) string { if strings.ToLower(req.URL.Scheme) == "https" { return "https" } return "http" } const ( hashLength = 32 passwordLength = 20 ) func generateHash() string { return hex.EncodeToString(auth.GenerateRandomKey(hashLength)) } const ( base62alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" special = ",.!;" ) var ( zero = big.NewInt(0) div62 = big.NewInt(62) ) func encodeToString(src []byte, max int) string { v := new(big.Int) v.SetBytes(src[1:]) m := new(big.Int) z := new(big.Int) out := make([]byte, 0, max) for { z.DivMod(v, div62, m) // reverse order but it doesnt matter. out = append(out, base62alphabet[m.Int64()]) if len(out) == max-1 || z.Cmp(zero) == 0 { break } v, z = z, v } out = append(out, special[int(src[0])%len(special)]) return string(out) } func generateNewPassword() string { // First try pwgen out, err := exec.Command("pwgen", "-y", "20", "1").Output() if err == nil { return strings.TrimSpace(string(out)) } // Use internal base62 encoder. return encodeToString(auth.GenerateRandomKey(20), 20) } func sendMail(email, subject, body string) error { cfg := &config.Config m := gomail.NewMessage() m.SetHeader("From", cfg.MailFrom) m.SetHeader("To", email) m.SetHeader("Subject", subject) m.SetBody("text/plain", body) d := gomail.Dialer{ Host: cfg.MailHost, Port: int(cfg.MailPort), Username: cfg.MailUser, Password: cfg.MailPassword, LocalName: cfg.MailHelo, SSL: cfg.MailPort == 465, } return d.DialAndSend(m) } func passwordResetRequest( input interface{}, req *http.Request, db *sql.DB, ) (jr JSONResult, err error) { // Limit total number of password requests. if passwordResetRequests.len() >= maxPasswordResets { err = JSONError{ Code: http.StatusServiceUnavailable, Message: "Too much password reset request", } return } user := input.(*PWResetUser) if user.User == "" { err = JSONError{http.StatusBadRequest, "Invalid user name"} return } cfg := &config.Config if db, err = auth.OpenDB(cfg.ServiceUser, cfg.ServicePassword); err != nil { return } defer db.Close() var email string err = db.QueryRow(userExistsSQL, user.User).Scan(&email) switch { case err == sql.ErrNoRows: err = JSONError{http.StatusNotFound, "User does not exist."} return case err != nil: return } // Limit requests per user if !passwordResetRequests.userAllowed(user.User) { err = JSONError{ Code: http.StatusServiceUnavailable, Message: "Too much password reset requests for user", } return } hash := generateHash() passwordResetRequests.store(hash, user.User, email) body := requestMessageBody(useHTTPS(req), user.User, hash, req.Host) if err = sendMail(email, "Password Reset Link", body); err != nil { return } jr.Result = &struct { SendTo string `json:"send-to"` }{email} return } func passwordReset( input interface{}, req *http.Request, db *sql.DB, ) (jr JSONResult, err error) { hash := mux.Vars(req)["hash"] if _, err = hex.DecodeString(hash); err != nil { err = JSONError{http.StatusBadRequest, "Invalid hash"} return } tu := passwordResetRequests.fetch(hash) if tu == nil { err = JSONError{http.StatusNotFound, "No such hash"} return } password := generateNewPassword() cfg := &config.Config if db, err = auth.OpenDB(cfg.ServiceUser, cfg.ServicePassword); err != nil { return } defer db.Close() var res sql.Result if res, err = db.Exec( updatePasswordSQL, password, tu.user, ); err != nil { return } passwordResetRequests.delete(hash) if n, err2 := res.RowsAffected(); err2 == nil && n == 0 { err = JSONError{ Code: http.StatusNotFound, Message: "User not found", } return } body := changedMessageBody(useHTTPS(req), tu.user, password, req.Host) if err = sendMail(tu.email, "Password Reset Done", body); err != nil { return } jr.Result = &struct { Message string `json:"message"` }{"User password changed"} return }