package main import ( crand "crypto/rand" "database/sql" "encoding/hex" "net/http" "regexp" "strconv" "strings" "time" "golang.org/x/crypto/bcrypt" ) type Session struct { UID int64 CreatedAt int64 Expires int64 Description string Value string } var ( reNonWord = regexp.MustCompile(`[^._\w\s-]`) reSpace = regexp.MustCompile(`\s+`) ) // getSession liest den session-Cookie und liefert die zugehörige, noch gültige Session. func getSession(r *http.Request) (*Session, bool) { c, err := r.Cookie("session") if err != nil || c.Value == "" { return nil, false } s := Session{Value: c.Value} err = db.QueryRow( `SELECT uid, created_at, expires, description FROM session WHERE value = ?`, c.Value, ).Scan(&s.UID, &s.CreatedAt, &s.Expires, &s.Description) if err != nil { return nil, false } if s.Expires < time.Now().Unix() { db.Exec(`DELETE FROM session WHERE value = ?`, c.Value) return nil, false } return &s, true } func handleLogin(w http.ResponseWriter, r *http.Request) { if _, ok := getSession(r); ok { writeJSON(w, http.StatusOK, map[string]string{"status": "already_logged_in"}) return } username := sanitizeUsername(r.FormValue("user")) if len(username) < 3 { writeError(w, http.StatusBadRequest, "Der Nutzername ist zu kurz.") return } var uid int64 var hash []byte err := db.QueryRow(`SELECT uid, password FROM user WHERE username = ?`, username).Scan(&uid, &hash) if err == sql.ErrNoRows { writeError(w, http.StatusUnauthorized, "Nutzername oder Passwort ist falsch.") return } else if err != nil { writeError(w, http.StatusInternalServerError, "Datenbankfehler") return } if bcrypt.CompareHashAndPassword(hash, []byte(r.FormValue("pass"))) != nil { writeError(w, http.StatusUnauthorized, "Nutzername oder Passwort ist falsch.") return } now := time.Now().Unix() timeoutSec, _ := strconv.ParseInt(r.FormValue("timeout"), 10, 64) expires := now + timeoutSec token, err := newToken() if err != nil { writeError(w, http.StatusInternalServerError, "Token konnte nicht erzeugt werden") return } db.Exec(`DELETE FROM session WHERE expires < ?`, now) if _, err := db.Exec( `INSERT INTO session (uid, created_at, expires, description, value) VALUES (?, ?, ?, ?, ?)`, uid, now, expires, "", token, ); err != nil { writeError(w, http.StatusInternalServerError, "Session konnte nicht erstellt werden") return } db.Exec(`UPDATE user SET last_login = ? WHERE uid = ?`, now, uid) http.SetCookie(w, &http.Cookie{ Name: "session", Value: token, Expires: time.Unix(expires, 0), Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, }) writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } func handleLogout(w http.ResponseWriter, r *http.Request) { if s, ok := getSession(r); ok { db.Exec(`DELETE FROM session WHERE value = ?`, s.Value) } http.SetCookie(w, &http.Cookie{ Name: "session", Value: "", Path: "/", Expires: time.Unix(0, 0), MaxAge: -1, }) writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } func handleNewUser(w http.ResponseWriter, r *http.Request) { var errs []string username := sanitizeUsername(r.FormValue("user")) if len(username) < 3 { errs = append(errs, "Der Nutzername ist zu kurz.") } if idFromUsername(username) != 0 { errs = append(errs, "Der Nutzername wird bereits verwendet.") } pass1 := r.FormValue("pass1") pass2 := r.FormValue("pass2") if pass1 != pass2 { errs = append(errs, "Passwort und Passwortbestätigung sind ungleich.") } if len(pass1) < 10 { errs = append(errs, "Das Passwort ist kürzer als 10 Zeichen.") } if len(errs) > 0 { writeJSON(w, http.StatusBadRequest, map[string]any{"errors": errs}) return } if err := createUser(username, pass1); err != nil { writeError(w, http.StatusInternalServerError, "Account konnte nicht erstellt werden") return } writeJSON(w, http.StatusCreated, map[string]string{"username": username}) } func handleSessionInfo(w http.ResponseWriter, r *http.Request) { s, ok := getSession(r) if !ok { writeError(w, http.StatusUnauthorized, "Invalid session") return } writeJSON(w, http.StatusOK, map[string]any{ "uid": s.UID, "created_at": s.CreatedAt, "expires": s.Expires, "description": s.Description, }) } func handleHeaderbar(w http.ResponseWriter, r *http.Request) { _, ok := getSession(r) writeJSON(w, http.StatusOK, map[string]bool{"loggedin": ok}) } func sanitizeUsername(u string) string { u = strings.TrimSpace(u) if len(u) > 32 { u = u[:32] } u = reNonWord.ReplaceAllString(u, "") u = reSpace.ReplaceAllString(u, "_") return u } func newToken() (string, error) { b := make([]byte, 32) if _, err := crand.Read(b); err != nil { return "", err } return hex.EncodeToString(b), nil }