chore: remove unreachable middleware package
This commit is contained in:
@@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user