-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmiddleware.go
More file actions
108 lines (99 loc) · 3.42 KB
/
middleware.go
File metadata and controls
108 lines (99 loc) · 3.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// Package paywall provides Bitcoin payment protection for HTTP handlers
package paywall
import (
"net/http"
"time"
)
// Middleware wraps an http.Handler to enforce Bitcoin payment requirements
//
// Parameters:
// - next: The HTTP handler to protect with payment verification
//
// Returns:
// - http.Handler: A handler that checks payment status before allowing access
//
// Flow:
// 1. Checks for existing payment_id cookie
// 2. If cookie exists:
// - Verifies payment status and expiration
// - Allows access for confirmed, unexpired payments
// - Shows payment page for pending, unexpired payments
// 3. If no valid payment:
// - Creates new payment
// - Sets secure payment_id cookie
// - Shows payment page
//
// Error Handling:
// - Returns 500 Internal Server Error if payment creation fails
// - Invalid/expired payments result in new payment creation
//
// Security:
// - Uses secure, HTTP-only cookies with SameSite=Strict
// - Payment IDs are cryptographically random
// - Validates payment status and expiration
//
// Related types: Payment, PaymentStore, PaymentStatus
func (p *Paywall) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Determine cookie name and security based on connection type
cookieName := "payment_id"
isSecure := false
// Use __Host- prefix only for HTTPS connections
if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
cookieName = "__Host-payment_id"
isSecure = true
}
// First check for existing cookie (try both names for compatibility)
cookie, err := r.Cookie(cookieName)
if err != nil && cookieName == "payment_id" {
// Fallback: try __Host- version for backward compatibility with HTTPS-only cookies
// This allows HTTP sessions to access cookies from previous HTTPS sessions during migration
cookie, err = r.Cookie("__Host-payment_id")
}
if err == nil {
// Cookie exists, verify payment
// update expiration +15 minutes
cookie.Expires = time.Now().Add(1 * time.Hour)
http.SetCookie(w, cookie)
payment, err := p.Store.GetPayment(cookie.Value)
if err == nil && payment != nil {
if payment.Status == StatusConfirmed && time.Now().Before(payment.ExpiresAt) {
// Payment confirmed and not expired, allow access
next.ServeHTTP(w, r)
return
}
if payment.Status == StatusPending && time.Now().Before(payment.ExpiresAt) {
// Payment pending and not expired, show existing payment page
p.renderPaymentPage(w, payment)
return
}
}
}
// No valid payment found, create new one
payment, err := p.CreatePayment()
if err != nil {
http.Error(w, "Failed to create payment", http.StatusInternalServerError)
return
}
cookieExpiration := time.Now().Add(1 * time.Hour)
// Set cookie for new payment with appropriate security settings
http.SetCookie(w, &http.Cookie{
Name: cookieName,
Value: payment.ID,
Path: "/",
Secure: isSecure,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Domain: "",
Expires: cookieExpiration,
})
// Show payment page
p.renderPaymentPage(w, payment)
})
}
func (p *Paywall) MiddlewareFunc(next http.Handler) http.HandlerFunc {
return http.HandlerFunc(p.Middleware(next).(http.HandlerFunc))
}
func (p *Paywall) MiddlewareFuncFunc(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(p.Middleware(next).(http.HandlerFunc))
}