refactor: reorganize project structure following go standards
This commit is contained in:
44
pkg/middleware/csrf.go
Normal file
44
pkg/middleware/csrf.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
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 == "" {
|
||||
// If neither is present, and it's a POST/PUT/DELETE, reject it (strict policy)
|
||||
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
|
||||
}
|
||||
|
||||
host := r.Host
|
||||
// If origin doesn't match host (accounting for potential schema prefixes)
|
||||
expectedHTTP := "http://" + host
|
||||
expectedHTTPS := "https://" + host
|
||||
|
||||
if origin != expectedHTTP && origin != expectedHTTPS {
|
||||
http.Error(w, "Cross-Site Request Forgery (CSRF) origin mismatch", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
76
pkg/middleware/logging.go
Normal file
76
pkg/middleware/logging.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type statusRecorder struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
wroteHeader bool
|
||||
}
|
||||
|
||||
func newStatusRecorder(w http.ResponseWriter) *statusRecorder {
|
||||
return &statusRecorder{
|
||||
ResponseWriter: w,
|
||||
statusCode: http.StatusOK,
|
||||
}
|
||||
}
|
||||
|
||||
func (rw *statusRecorder) WriteHeader(code int) {
|
||||
if rw.wroteHeader {
|
||||
return
|
||||
}
|
||||
rw.statusCode = code
|
||||
rw.wroteHeader = true
|
||||
rw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func (rw *statusRecorder) Write(b []byte) (int, error) {
|
||||
if !rw.wroteHeader {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
return rw.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
func (rw *statusRecorder) Flush() {
|
||||
if flusher, ok := rw.ResponseWriter.(http.Flusher); ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (rw *statusRecorder) Unwrap() http.ResponseWriter {
|
||||
return rw.ResponseWriter
|
||||
}
|
||||
|
||||
func RequestLogger(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
recorder := newStatusRecorder(w)
|
||||
|
||||
next.ServeHTTP(recorder, r)
|
||||
|
||||
log.Printf("%s %s %d %s", r.Method, r.URL.Path, recorder.statusCode, time.Since(start))
|
||||
})
|
||||
}
|
||||
93
pkg/middleware/ratelimit.go
Normal file
93
pkg/middleware/ratelimit.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type visitor struct {
|
||||
attempts int
|
||||
lastSeen time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
visitors = make(map[string]*visitor)
|
||||
mu sync.Mutex
|
||||
quit = make(chan struct{})
|
||||
)
|
||||
|
||||
func init() {
|
||||
go cleanupVisitors()
|
||||
}
|
||||
|
||||
func StopCleanup() {
|
||||
close(quit)
|
||||
}
|
||||
|
||||
func cleanupVisitors() {
|
||||
for {
|
||||
select {
|
||||
case <-quit:
|
||||
return
|
||||
case <-time.After(time.Minute):
|
||||
mu.Lock()
|
||||
for ip, v := range visitors {
|
||||
if time.Since(v.lastSeen) > 3*time.Minute {
|
||||
delete(visitors, ip)
|
||||
}
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getIP(r *http.Request) string {
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
ips := strings.Split(xff, ",")
|
||||
return strings.TrimSpace(ips[0])
|
||||
}
|
||||
if realIP := r.Header.Get("X-Real-IP"); realIP != "" {
|
||||
return realIP
|
||||
}
|
||||
ip := r.RemoteAddr
|
||||
if colonIdx := strings.LastIndex(ip, ":"); colonIdx != -1 {
|
||||
ip = ip[:colonIdx]
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
func RateLimitAuth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ip := getIP(r)
|
||||
|
||||
mu.Lock()
|
||||
v, exists := visitors[ip]
|
||||
if !exists {
|
||||
visitors[ip] = &visitor{1, time.Now()}
|
||||
} else {
|
||||
// Reset attempts if it's been more than a minute
|
||||
if time.Since(v.lastSeen) > time.Minute {
|
||||
v.attempts = 0
|
||||
}
|
||||
v.attempts++
|
||||
v.lastSeen = time.Now()
|
||||
}
|
||||
|
||||
// If more than 5 attempts within a minute, block
|
||||
if exists && v.attempts > 5 {
|
||||
mu.Unlock()
|
||||
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
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user