GoBlog/captcha.go

113 lines
3.0 KiB
Go

package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"io"
"net/http"
"time"
"github.com/dchest/captcha"
"github.com/dgrijalva/jwt-go"
)
func captchaMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 1. Check JWT
claims := &captchaClaims{}
if captchaCookie, err := r.Cookie("captcha"); err == nil {
if tkn, err := jwt.ParseWithClaims(captchaCookie.Value, claims, func(t *jwt.Token) (interface{}, error) {
return jwtKey(), nil
}); err == nil && tkn.Valid && claims.TokenType == "captcha" {
next.ServeHTTP(w, r)
return
}
}
// 2. Show Captcha
w.WriteHeader(http.StatusUnauthorized)
h, _ := json.Marshal(r.Header.Clone())
b, _ := io.ReadAll(io.LimitReader(r.Body, 2000000)) // Only allow 20 Megabyte
_ = r.Body.Close()
if len(b) == 0 {
// Maybe it's a form
_ = r.ParseForm()
b = []byte(r.PostForm.Encode())
}
render(w, templateCaptcha, &renderData{
Data: map[string]string{
"captchamethod": r.Method,
"captchaheaders": base64.StdEncoding.EncodeToString(h),
"captchabody": base64.StdEncoding.EncodeToString(b),
"captchaid": captcha.New(),
},
})
})
}
func checkIsCaptcha(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if !checkCaptcha(rw, r) {
next.ServeHTTP(rw, r)
}
})
}
func checkCaptcha(w http.ResponseWriter, r *http.Request) bool {
if r.Method == http.MethodPost &&
r.Header.Get(contentType) == contentTypeWWWForm &&
r.FormValue("captchaaction") == "captcha" {
// Do original request
captchabody, _ := base64.StdEncoding.DecodeString(r.FormValue("captchabody"))
req, _ := http.NewRequest(r.FormValue("captchamethod"), r.RequestURI, bytes.NewReader(captchabody))
// Copy original headers
captchaheaders, _ := base64.StdEncoding.DecodeString(r.FormValue("captchaheaders"))
var headers http.Header
_ = json.Unmarshal(captchaheaders, &headers)
for k, v := range headers {
req.Header[k] = v
}
// Check captcha
if captcha.VerifyString(r.FormValue("captchaid"), r.FormValue("digits")) {
// Create cookie
captchaCookie, err := createCaptchaCookie()
if err != nil {
serveError(w, r, err.Error(), http.StatusInternalServerError)
return true
}
// Add cookie to original request
req.AddCookie(captchaCookie)
// Send cookie
http.SetCookie(w, captchaCookie)
}
// Serve original request
d.ServeHTTP(w, req)
return true
}
return false
}
type captchaClaims struct {
*jwt.StandardClaims
TokenType string
}
func createCaptchaCookie() (*http.Cookie, error) {
expiration := time.Now().Add(24 * time.Hour)
tokenString, err := jwt.NewWithClaims(jwt.SigningMethodHS256, &captchaClaims{
&jwt.StandardClaims{ExpiresAt: expiration.Unix()},
"captcha",
}).SignedString(jwtKey())
if err != nil {
return nil, err
}
return &http.Cookie{
Name: "captcha",
Value: tokenString,
Expires: expiration,
Secure: httpsConfigured(),
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}, nil
}