package htsesh import ( "crypto/rand" "crypto/sha512" "encoding/base32" "encoding/base64" "net/http" "sync/atomic" "time" ) type session struct { V []byte T time.Time } func (s *session) Hash() string { h := sha512.New() h.Write(s.V) h.Write([]byte(s.T.String())) return base64.URLEncoding.EncodeToString(h.Sum(nil)) } func (s *session) Validate(cookie *http.Cookie) bool { if s == nil { return false } if time.Now().After(s.T) || s.T.After(time.Now().Add(time.Hour)) { s.V = nil s.T = time.Time{} sessionID.Store(nil) return false } if cookie == nil || cookie.Value != s.Hash() { return false } return true } func sessionOK(r *http.Request) bool { cookie, err := r.Cookie("session_id") if err != nil { return false } return sessionID.Load().Validate(cookie) } func Authenticate(next http.HandlerFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if sessionOK(r) { if r.URL.Query().Has("logout") { ji := base32.StdEncoding.EncodeToString(sessionID.Load().V[:5]) if r.URL.Query().Get("id") == ji { sessionID.Store(nil) http.SetCookie(w, &http.Cookie{ Name: "session_id", Value: "", HttpOnly: true, Secure: true, Expires: time.Now(), SameSite: http.SameSiteStrictMode, }) http.Redirect(w, r, r.URL.Path, http.StatusSeeOther) } else { w.Header().Set("Content-Type", "text/html") w.WriteHeader(http.StatusUnauthorized) w.Write([]byte("
Logout")) } return } next.ServeHTTP(w, r) } else if loginHandler(w, r) { http.Redirect(w, r, r.URL.Path, http.StatusSeeOther) } else { w.Header().Set("Content-Type", "text/html") w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(FORM)) } }) } func loginHandler(w http.ResponseWriter, r *http.Request) bool { if r.Method != http.MethodPost || r.ParseForm() != nil { return false } username := r.FormValue("username") password := r.FormValue("password") otp := r.FormValue("otp") if username == "" || password == "" || otp == "" { return false } if !verify(username, password, otp) { return false } s := session{ V: make([]byte, 32), T: time.Now().Add(time.Hour), } n, err := rand.Read(s.V) if err != nil || 32 != len(s.V) || n != 32 { return false } sessionID.Store(&s) http.SetCookie(w, &http.Cookie{ Name: "session_id", Value: s.Hash(), HttpOnly: true, Secure: true, Expires: s.T, SameSite: http.SameSiteStrictMode, }) return true } var ( verify = func(username, password, otp string) bool { return false } sessionID atomic.Pointer[session] ) func SetVerifyFunc(f func(username, password, otp string) bool) { verify = f } var FORM = ``