diff options
Diffstat (limited to 'internal/handlers')
-rw-r--r-- | internal/handlers/auth.go | 162 | ||||
-rw-r--r-- | internal/handlers/main.go | 64 | ||||
-rw-r--r-- | internal/handlers/middleware.go | 75 |
3 files changed, 301 insertions, 0 deletions
diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go new file mode 100644 index 0000000..d0ccdff --- /dev/null +++ b/internal/handlers/auth.go @@ -0,0 +1,162 @@ +package handlers + +import ( + "crypto/hmac" + "crypto/sha256" + "demoon/internal/models" + "demoon/pkg/utils" + "encoding/base64" + "encoding/json" + "html/template" + "net/http" + "strings" + "time" + + "golang.org/x/crypto/bcrypt" +) + +func abortWithError(w http.ResponseWriter, msg string) { + tmpl := template.Must(template.ParseGlob("components/*.html")) + tmpl.ExecuteTemplate(w, "error", msg) +} + +func (h *Handlers) HandleSignup(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + username := r.PostFormValue("username") + if username == "" { + msg := "username not provided" + h.log.Error(msg) + abortWithError(w, msg) + return + } + password := r.PostFormValue("password") + if password == "" { + msg := "password not provided" + h.log.Error(msg) + abortWithError(w, msg) + return + } + // make sure username does not exists + cleanName := utils.RemoveSpacesFromStr(username) + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 8) + // create user in db + now := time.Now() + nextMidnight := time.Date(now.Year(), now.Month(), now.Day(), + 0, 0, 0, 0, time.UTC).Add(time.Hour * 24) + newUser := &models.UserScore{ + Username: cleanName, Password: string(hashedPassword), + BurnTime: nextMidnight, CreatedAt: now, + } + // login user + cookie, err := h.makeCookie(cleanName, r.RemoteAddr) + if err != nil { + h.log.Error("failed to login", "error", err) + abortWithError(w, err.Error()) + return + } + http.SetCookie(w, cookie) + // http.Redirect(w, r, "/", 302) + tmpl, err := template.ParseGlob("components/*.html") + if err != nil { + abortWithError(w, err.Error()) + return + } + tmpl.ExecuteTemplate(w, "main", newUser) +} + +func (h *Handlers) HandleLogin(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + username := r.PostFormValue("username") + if username == "" { + msg := "username not provided" + h.log.Error(msg) + abortWithError(w, msg) + return + } + password := r.PostFormValue("password") + if password == "" { + msg := "password not provided" + h.log.Error(msg) + abortWithError(w, msg) + return + } + cleanName := utils.RemoveSpacesFromStr(username) + tmpl, err := template.ParseGlob("components/*.html") + if err != nil { + abortWithError(w, err.Error()) + return + } + cookie, err := h.makeCookie(cleanName, r.RemoteAddr) + if err != nil { + h.log.Error("failed to login", "error", err) + abortWithError(w, err.Error()) + return + } + http.SetCookie(w, cookie) + tmpl.ExecuteTemplate(w, "main", nil) +} + +func (h *Handlers) makeCookie(username string, remote string) (*http.Cookie, error) { + // secret + // Create a new random session token + // sessionToken := xid.New().String() + sessionToken := "token" + expiresAt := time.Now().Add(time.Duration(h.cfg.SessionLifetime) * time.Second) + // Set the token in the session map, along with the session information + session := &models.Session{ + Username: username, + Expiry: expiresAt, + } + cookieName := "session_token" + // hmac to protect cookies + hm := hmac.New(sha256.New, []byte(h.cfg.CookieSecret)) + hm.Write([]byte(cookieName)) + hm.Write([]byte(sessionToken)) + signature := hm.Sum(nil) + // b64 enc to avoid non-ascii + cookieValue := base64.URLEncoding.EncodeToString([]byte( + string(signature) + sessionToken)) + cookie := &http.Cookie{ + Name: cookieName, + Value: cookieValue, + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteNoneMode, + Domain: h.cfg.ServerConfig.Host, + } + h.log.Info("check remote addr for cookie set", + "remote", remote, "session", session) + if strings.Contains(remote, "192.168.0") { + // no idea what is going on + cookie.Domain = "192.168.0.101" + } + // set ctx? + // set user in session + if err := h.cacheSetSession(sessionToken, session); err != nil { + return nil, err + } + return cookie, nil +} + +func (h *Handlers) cacheGetSession(key string) (*models.Session, error) { + userSessionB, err := h.mc.Get(key) + if err != nil { + return nil, err + } + var us *models.Session + if err := json.Unmarshal(userSessionB, &us); err != nil { + return nil, err + } + return us, nil +} + +func (h *Handlers) cacheSetSession(key string, session *models.Session) error { + sesb, err := json.Marshal(session) + if err != nil { + return err + } + h.mc.Set(key, sesb) + // expire in 10 min + h.mc.Expire(key, 10*60) + return nil +} diff --git a/internal/handlers/main.go b/internal/handlers/main.go new file mode 100644 index 0000000..960b26d --- /dev/null +++ b/internal/handlers/main.go @@ -0,0 +1,64 @@ +package handlers + +import ( + "demoon/config" + "demoon/internal/database/repos" + "demoon/internal/models" + "demoon/pkg/cache" + "html/template" + "log/slog" + "net/http" + "os" +) + +var defUS = models.UserScore{} + +// Handlers structure +type Handlers struct { + cfg config.Config + log *slog.Logger + repo repos.FullRepo + mc cache.Cache +} + +// NewHandlers constructor +func NewHandlers( + cfg config.Config, l *slog.Logger, repo repos.FullRepo, +) *Handlers { + if l == nil { + l = slog.New(slog.NewJSONHandler(os.Stdout, nil)) + } + h := &Handlers{ + cfg: cfg, + log: l, + repo: repo, + mc: cache.MemCache, + } + return h +} + +func (h *Handlers) Ping(w http.ResponseWriter, r *http.Request) { + h.log.Info("got ping request") + w.Write([]byte("pong")) +} + +func (h *Handlers) MainPage(w http.ResponseWriter, r *http.Request) { + tmpl, err := template.ParseGlob("components/*.html") + if err != nil { + abortWithError(w, err.Error()) + return + } + // get recommendations + usernameRaw := r.Context().Value("username") + h.log.Info("got mainpage request", "username", usernameRaw) + if usernameRaw == nil { + tmpl.ExecuteTemplate(w, "main", defUS) + return + } + username := usernameRaw.(string) + if username == "" { + tmpl.ExecuteTemplate(w, "main", defUS) + return + } + tmpl.ExecuteTemplate(w, "main", nil) +} diff --git a/internal/handlers/middleware.go b/internal/handlers/middleware.go new file mode 100644 index 0000000..8b871a2 --- /dev/null +++ b/internal/handlers/middleware.go @@ -0,0 +1,75 @@ +package handlers + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "errors" + "net/http" +) + +func (h *Handlers) GetSession(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cookieName := "session_token" + sessionCookie, err := r.Cookie(cookieName) + if err != nil { + msg := "auth failed; failed to get session token from cookies" + h.log.Debug(msg, "error", err) + next.ServeHTTP(w, r) + return + } + cookieValueB, err := base64.URLEncoding. + DecodeString(sessionCookie.Value) + if err != nil { + msg := "auth failed; failed to decode b64 cookie" + h.log.Debug(msg, "error", err) + next.ServeHTTP(w, r) + return + } + cookieValue := string(cookieValueB) + if len(cookieValue) < sha256.Size { + h.log.Warn("small cookie", "size", len(cookieValue)) + next.ServeHTTP(w, r) + return + } + // Split apart the signature and original cookie value. + signature := cookieValue[:sha256.Size] + sessionToken := cookieValue[sha256.Size:] + //verify signature + mac := hmac.New(sha256.New, []byte(h.cfg.CookieSecret)) + mac.Write([]byte(cookieName)) + mac.Write([]byte(sessionToken)) + expectedSignature := mac.Sum(nil) + if !hmac.Equal([]byte(signature), expectedSignature) { + h.log.Debug("cookie with an invalid sign") + next.ServeHTTP(w, r) + return + } + userSession, err := h.cacheGetSession(sessionToken) + if err != nil { + msg := "auth failed; session does not exists" + err = errors.New(msg) + h.log.Debug(msg, "error", err) + next.ServeHTTP(w, r) + return + } + if userSession.IsExpired() { + h.mc.RemoveKey(sessionToken) + msg := "session is expired" + h.log.Debug(msg, "error", err, "token", sessionToken) + next.ServeHTTP(w, r) + return + } + ctx := context.WithValue(r.Context(), + "username", userSession.Username) + if err := h.cacheSetSession(sessionToken, + userSession); err != nil { + msg := "failed to marshal user session" + h.log.Warn(msg, "error", err) + next.ServeHTTP(w, r) + return + } + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} |