diff --git a/pkg/middleware/csrf.go b/pkg/middleware/csrf.go deleted file mode 100644 index faf73e1..0000000 --- a/pkg/middleware/csrf.go +++ /dev/null @@ -1,54 +0,0 @@ -package middleware - -import ( - "net/http" - "net/url" -) - -// VerifyOrigin validates that the request Origin/Referer matches the host -// skips validation for safe methods (GET, HEAD, OPTIONS) -func VerifyOrigin(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodGet || r.Method == http.MethodHead || r.Method == http.MethodOptions { - next.ServeHTTP(w, r) - return - } - - origin := r.Header.Get("Origin") - if origin == "" { - referer := r.Header.Get("Referer") - if referer == "" { - http.Error(w, "Missing Origin or Referer header", http.StatusForbidden) - return - } - - refURL, err := url.Parse(referer) - if err != nil { - http.Error(w, "Invalid Referer header", http.StatusForbidden) - return - } - origin = refURL.Scheme + "://" + refURL.Host - } - - originURL, err := url.Parse(origin) - if err != nil { - http.Error(w, "Invalid Origin header", http.StatusForbidden) - return - } - - host := r.Host - if forwardedHost := r.Header.Get("X-Forwarded-Host"); forwardedHost != "" { - host = forwardedHost // support reverse proxies - } - - expectedHTTP := "http://" + host - expectedHTTPS := "https://" + host - - if originURL.Scheme+"://"+originURL.Host != expectedHTTP && originURL.Scheme+"://"+originURL.Host != expectedHTTPS { - http.Error(w, "Cross-Site Request Forgery (CSRF) origin mismatch", http.StatusForbidden) - return - } - - next.ServeHTTP(w, r) - }) -} diff --git a/pkg/middleware/logging.go b/pkg/middleware/logging.go deleted file mode 100644 index 76774a3..0000000 --- a/pkg/middleware/logging.go +++ /dev/null @@ -1,100 +0,0 @@ -package middleware - -import ( - "bufio" - "fmt" - "log" - "net" - "net/http" - "strings" - "time" -) - -// statusRecorder wraps ResponseWriter to capture the status code -// defaults to 200 if WriteHeader is never called before Write -type statusRecorder struct { - http.ResponseWriter - statusCode int - wroteHeader bool -} - -func newStatusRecorder(w http.ResponseWriter) *statusRecorder { - return &statusRecorder{ - ResponseWriter: w, - statusCode: http.StatusOK, - } -} - -// WriteHeader records the status code and proxies to underlying writer -func (rw *statusRecorder) WriteHeader(code int) { - if rw.wroteHeader { - return - } - rw.statusCode = code - rw.wroteHeader = true - rw.ResponseWriter.WriteHeader(code) -} - -// Write ensures a status code is set before writing the body -func (rw *statusRecorder) Write(b []byte) (int, error) { - if !rw.wroteHeader { - rw.WriteHeader(http.StatusOK) - } - return rw.ResponseWriter.Write(b) -} - -// Flush proxies the Flusher interface if supported -func (rw *statusRecorder) Flush() { - if flusher, ok := rw.ResponseWriter.(http.Flusher); ok { - flusher.Flush() - } -} - -// Hijack proxies the Hijacker interface if supported -func (rw *statusRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { - hijacker, ok := rw.ResponseWriter.(http.Hijacker) - if !ok { - return nil, nil, fmt.Errorf("response writer does not support hijacking") - } - return hijacker.Hijack() -} - -// Push proxies the Pusher interface if supported -func (rw *statusRecorder) Push(target string, opts *http.PushOptions) error { - pusher, ok := rw.ResponseWriter.(http.Pusher) - if !ok { - return http.ErrNotSupported - } - return pusher.Push(target, opts) -} - -// Unwrap returns the underlying ResponseWriter for middleware chaining -func (rw *statusRecorder) Unwrap() http.ResponseWriter { - return rw.ResponseWriter -} - -// RequestLogger logs requests that result in 4xx/5xx responses -// skips static assets, streaming, and common bot paths -func RequestLogger(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - - if strings.HasPrefix(r.URL.Path, "/dist/") || - strings.HasPrefix(r.URL.Path, "/static/") || - strings.HasPrefix(r.URL.Path, "/watch/proxy/stream") || - strings.HasPrefix(r.URL.Path, "/watch/proxy/segment") || - r.URL.Path == "/favicon.ico" || - r.URL.Path == "/robots.txt" { - next.ServeHTTP(w, r) - return - } - - recorder := newStatusRecorder(w) - - next.ServeHTTP(recorder, r) - - if recorder.statusCode >= 400 { - log.Printf("%s %s %d %s", r.Method, r.URL.Path, recorder.statusCode, time.Since(start)) - } - }) -} diff --git a/pkg/middleware/ratelimit.go b/pkg/middleware/ratelimit.go deleted file mode 100644 index ce419d5..0000000 --- a/pkg/middleware/ratelimit.go +++ /dev/null @@ -1,101 +0,0 @@ -package middleware - -import ( - "fmt" - "net/http" - "strings" - "sync" - "time" -) - -// visitor tracks request attempts and last access time per IP -type visitor struct { - attempts int - lastSeen time.Time -} - -// Config holds rate limiter settings -type Config struct { - MaxAttempts int // max requests per window - Window time.Duration // sliding window duration -} - -// Limiter implements a simple in-memory IP-based rate limiter -type Limiter struct { - mu sync.Mutex - visitors map[string]*visitor - config Config -} - -func NewLimiter(cfg Config) *Limiter { - return &Limiter{ - visitors: make(map[string]*visitor), - config: cfg, - } -} - -// Cleanup removes stale visitor entries older than 3x the window -func (l *Limiter) Cleanup(now time.Time) { - l.mu.Lock() - defer l.mu.Unlock() - for ip, v := range l.visitors { - if now.Sub(v.lastSeen) > l.config.Window*3 { - delete(l.visitors, ip) - } - } -} - -// getIP extracts the client IP, checking X-Forwarded-For and X-Real-IP headers -func getIP(r *http.Request) string { - if xff := r.Header.Get("X-Forwarded-For"); xff != "" { - ips := strings.Split(xff, ",") - return strings.TrimSpace(ips[0]) // first proxy IP - } - if realIP := r.Header.Get("X-Real-IP"); realIP != "" { - return realIP - } - ip := r.RemoteAddr - if colonIdx := strings.LastIndex(ip, ":"); colonIdx != -1 { - ip = ip[:colonIdx] // strip port for IPv4-mapped IPv6 - } - return ip -} - -// AuthMiddleware redirects rate-limited form submissions back to the page -// returns 429 for non-path requests (e.g. API calls) -func (l *Limiter) AuthMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if !l.allow(getIP(r)) { - if strings.HasPrefix(r.URL.Path, "/") { - http.Redirect(w, r, fmt.Sprintf("%s?error=rate_limited", r.URL.Path), http.StatusFound) - return - } - http.Error(w, "Too many requests. Please try again later.", http.StatusTooManyRequests) - return - } - next.ServeHTTP(w, r) - }) -} - -// allow checks and updates the visitor's attempt count; returns true if allowed -// resets counter if window has expired, otherwise increments and checks limit -func (l *Limiter) allow(ip string) bool { - l.mu.Lock() - defer l.mu.Unlock() - - v, exists := l.visitors[ip] - if !exists { - l.visitors[ip] = &visitor{1, time.Now()} - return true - } - - if time.Since(v.lastSeen) > l.config.Window { - v.attempts = 1 // reset counter on window expiry - v.lastSeen = time.Now() - return true - } - - v.attempts++ - v.lastSeen = time.Now() - return v.attempts <= l.config.MaxAttempts -}