Files
OwnCord/Server/auth/helpers.go
T
jevb 6eba999233 feat: add Let's Encrypt ACME support, fix security issues, improve server UX
Server:
- Add Let's Encrypt (ACME) TLS mode with autocert, HTTP-01 challenges on :80,
  and automatic certificate renewal (tls.mode: "acme" in config.yaml)
- Add ASCII art startup banner with server info and endpoint URLs
- Fix CSP blocking admin panel inline styles/scripts (per-route override)
- Suppress TLS handshake error noise in console output
- Fix TOCTOU race in invite consumption (atomic UPDATE with row-count check)
- Fix sendMsg mutex race condition (hold lock for entire send)
- Fix permission override formula (deny-first, allow-wins)
- Fix voice join parsing channelID before permission check
- Add session expiry check at WebSocket auth and periodic revalidation
- Add message length limit (4000 chars) and emoji length validation (32 bytes)
- Add file size enforcement in storage after io.Copy
- Add checksum URL validation in updater
- Add backup path traversal protection (BackupToSafe)
- Add self-modification guard in admin handlePatchUser
- Fix admin ownerOnlyMiddleware to use context user instead of re-auth
- Remove redundant startup log lines (banner shows same info)
- Add periodic expired session cleanup (15-min ticker)
- Add permissions package with bitfield constants and EffectivePerms
- Add rate limiter cleanup goroutine to prevent unbounded growth
- Add auth helpers (IsEffectivelyBanned, IsSessionExpired)
- Add WebSocket origin validation

Client:
- Add TOFU certificate trust service
- Add receive loop error handling
- Fix redundant else-if in OnChatMessage
2026-03-15 07:07:59 +01:00

68 lines
2.2 KiB
Go

package auth
import (
"net/http"
"strings"
"time"
"github.com/owncord/server/db"
)
// ExtractBearerToken parses the "Authorization: Bearer <token>" header from r
// and returns the token and true. Returns "", false if the header is absent,
// uses a scheme other than "bearer" (case-insensitive), or has an empty token.
func ExtractBearerToken(r *http.Request) (string, bool) {
header := r.Header.Get("Authorization")
if header == "" {
return "", false
}
parts := strings.SplitN(header, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") || parts[1] == "" {
return "", false
}
return parts[1], true
}
// IsEffectivelyBanned reports whether u is currently banned, accounting for
// temporary ban expiry. A user is effectively banned when:
// - u.Banned is true, AND
// - u.BanExpires is nil (permanent ban), OR the expiry is in the future.
//
// If u is nil the function returns false without panicking.
// If BanExpires holds an unparseable string the ban is treated as active
// (fail-safe: keep user blocked rather than silently unblocking them).
func IsEffectivelyBanned(u *db.User) bool {
if u == nil || !u.Banned {
return false
}
// Permanent ban — no expiry set.
if u.BanExpires == nil {
return true
}
// Temporary ban — parse the expiry and compare to now.
for _, layout := range []string{"2006-01-02 15:04:05", "2006-01-02T15:04:05Z"} {
t, err := time.Parse(layout, *u.BanExpires)
if err == nil {
// Ban is still active if expiry is in the future.
return time.Now().UTC().Before(t.UTC())
}
}
// Unparseable expiry — fail-safe: treat as still banned.
return true
}
// IsSessionExpired reports whether the expiresAt timestamp string represents a
// time in the past. It accepts both the SQLite space-separated format
// ("2006-01-02 15:04:05") and the ISO-8601 UTC format ("2006-01-02T15:04:05Z").
// Any string that cannot be parsed is treated as expired for safety.
func IsSessionExpired(expiresAt string) bool {
for _, layout := range []string{"2006-01-02 15:04:05", "2006-01-02T15:04:05Z"} {
t, err := time.Parse(layout, expiresAt)
if err == nil {
return time.Now().UTC().After(t.UTC())
}
}
// Unparseable expiry — treat as expired for safety.
return true
}