maddy-password-reset/main.go
2023-02-12 23:44:50 +03:00

230 lines
6.7 KiB
Go

package main
import (
cryptorand "crypto/rand"
"database/sql"
"github.com/akyoto/cache"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"html/template"
"io"
"log"
"math/big"
_ "modernc.org/sqlite"
"naebet-password-reset/templates"
"net/http"
"net/smtp"
"os/exec"
"strconv"
"strings"
"time"
)
const (
// MaddyPath is path to your Maddy credentials database
//
// FYI, Maddy's password database by default is "/var/lib/maddy/credentials.db"
MaddyPath = ""
// HostingURL is your domain name,
// for example: `http://localhost:1323/`
HostingURL = ""
// SMTPMailUsername is your full mail address,
// for example: `robot@local.host`
SMTPMailUsername = ""
// SMTPMailPassword is your mailbox password
SMTPMailPassword = ""
// SMTPMailHostname is your mail hostname,
// for example: `mx1.local.host`
SMTPMailHostname = ""
// MXServer is your mail `MX` record + `PORT`,
// for example: `mx1.local.host:587`
MXServer = ""
// EmailFrom is a EmailTemplate's "$FROM" section
EmailFrom = ""
// EmailSubject is a EmailTemplate's "$SUBJECT" section
EmailSubject = ""
// EmailMessage is a EmailTemplate's "$MESSAGE" section
//
// Remember to provide a password reset link to a user ($RESET_LINK)
EmailMessage = "Here's your reset link: $RESET_LINK\r\n"
// EmailTemplate is your reset mail message
EmailTemplate = "To: $TO\r\n" +
"From: $FROM\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n" +
"Subject: $SUBJECT\r\n" +
"\r\n" +
"$MESSAGE"
// CacheTime is the duration that your password reset link will last
CacheTime = 15 * time.Minute
// HTTPServerPort is an HTTP server port
HTTPServerPort = 1323
)
const (
// TokenAlphabet is created for random string creation, see randomString() function
TokenAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
)
func randomString(length int) string {
l := big.NewInt(int64(len(TokenAlphabet)))
res := new(strings.Builder)
for i := 0; i < length; i++ {
n, err := cryptorand.Int(cryptorand.Reader, l)
if err != nil {
panic(err)
}
res.WriteByte(TokenAlphabet[n.Int64()])
}
return res.String()
}
type Template struct {
templates *template.Template
}
func (t *Template) Render(w io.Writer, name string, data interface{}, _ echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
func main() {
log.Println("[EmailMessage const] Checking your template")
if !strings.Contains(EmailMessage, "$RESET_LINK") {
log.Fatalln("[EmailMessage const] Your message template does not contain $RESET_LINK, so user can't reset his password!")
}
log.Println("[EmailTemplate const] Checking your template")
if !strings.Contains(EmailTemplate, "$TO") {
log.Fatalln("[EmailTemplate const] Your template does not contain $TO, make sure to add it.")
}
if !strings.Contains(EmailTemplate, "$FROM") {
log.Fatalln("[EmailTemplate const] Your template does not contain $FROM, make sure to add it.")
}
if !strings.Contains(EmailTemplate, "$SUBJECT") {
log.Fatalln("[EmailTemplate const] Your template does not contain $SUBJECT, make sure to add it, so user can see a message preview.")
}
if !strings.Contains(EmailTemplate, "$MESSAGE") {
log.Fatalln("[EmailTemplate const] Your template does not contain $MESSAGE, make sure to add it.")
}
log.Println("[Sqlite] Loading Maddy's credentials database")
db, err := sql.Open("sqlite", MaddyPath)
if err != nil {
log.Fatalln(err)
}
// Set up authentication information.
auth := smtp.PlainAuth("", SMTPMailUsername, SMTPMailPassword, SMTPMailHostname)
log.Println("[Cache] Registering cache for password resets")
passwordResetCache := cache.New(CacheTime)
log.Println("[Echo] Initializing echo web server")
e := echo.New()
e.HideBanner = true
e.Use(middleware.LoggerWithConfig(
middleware.LoggerConfig{
Format: `${time_custom} [Echo] ${latency_human} ${method} ${uri} - Error = ${error} - ${remote_ip} "${user_agent}"` + "\n",
CustomTimeFormat: "2006/01/02 15:04:05",
}))
e.Use(middleware.Recover())
log.Println("[Echo] Registering Go templates")
t := template.Must(template.ParseFS(templates.Templates, "*.gohtml"))
e.Renderer = &Template{
t,
}
e.GET("/reset", func(c echo.Context) error {
return c.Render(http.StatusOK, "reset.gohtml", nil)
})
e.POST("/reset", func(c echo.Context) error {
mail := c.FormValue("email")
go func() {
// Check if there is already a password reset
_, exists := passwordResetCache.Get(mail)
if exists {
log.Printf("[Cache] Mail %q already exists in cache, ignoring\n", mail)
return
}
// Check if it's exists in Maddy db
// It will return an error is there is no user found
var password string
err = db.QueryRow("SELECT value FROM passwords WHERE key = ?", mail).Scan(&password)
if err != nil {
log.Println("[Sqlite] An error occurred while trying to get password from Maddy database:", err)
return
}
// Generating an unique key
random := randomString(10)
passwordResetCache.Set(random, mail, CacheTime)
// Connect to the server, authenticate, set the sender and recipient,
// and send the email all in one step.
to := []string{mail}
msg := strings.ReplaceAll(EmailTemplate, "$TO", mail)
msg = strings.ReplaceAll(msg, "$FROM", EmailFrom)
msg = strings.ReplaceAll(msg, "$SUBJECT", EmailSubject)
msg = strings.ReplaceAll(msg, "$MESSAGE", EmailMessage)
msg = strings.ReplaceAll(msg, "$RESET_LINK", HostingURL+"reset/"+random)
//msg := []byte(fmt.Sprintf(EmailMessage, mail, SMTPMailUsername, HostingURL+"reset/"+random))
err := smtp.SendMail(MXServer, auth, SMTPMailUsername, to, []byte(msg))
if err != nil {
log.Println("[SMTP] Failed to send mail - ", err)
return
}
}()
return c.Render(http.StatusOK, "reset.gohtml", map[string]any{
"Sent": true,
})
})
e.GET("/reset/:key", func(c echo.Context) error {
key := c.Param("key")
_, exists := passwordResetCache.Get(key)
if !exists {
return c.Redirect(http.StatusTemporaryRedirect, "/reset")
}
return c.Render(http.StatusOK, "reset.gohtml", map[string]any{
"UniqueLinkTriggered": true,
})
})
e.POST("/reset/:key", func(c echo.Context) error {
key := c.Param("key")
password := c.FormValue("password")
mail, exists := passwordResetCache.Get(key)
if exists {
passwordResetCache.Delete(key)
}
maddyExecCommand := exec.Command("maddy", "creds", "password", mail.(string), "-p", password)
err = maddyExecCommand.Run()
if err != nil {
log.Println("[maddyExecCommand] Failed to execute Maddy's password reset command - ", err)
return err
}
return c.String(http.StatusOK, "All good! Your password is now changed.")
})
log.Println("[echo] Starting Echo web server")
e.Logger.Fatal(e.Start(":" + strconv.Itoa(HTTPServerPort)))
}