diff --git a/config_example/error_page/CAPTCHA.html b/config_example/error_page/CAPTCHA.html index 6ae636c..6e276af 100644 --- a/config_example/error_page/CAPTCHA.html +++ b/config_example/error_page/CAPTCHA.html @@ -66,6 +66,11 @@ + "您未能通过人机验证,请刷新页面后重试。"); window.location.reload(); break; + case "badSession": + alert("Session invalid, please refresh the page and try again.\n" + + "会话无效,请刷新页面后重试。"); + window.location.reload(); + break; default: alert("Unexpected error occurred, please refresh the page and try again.\n" + "发生了意料之外的错误,请刷新页面后重试。"); diff --git a/config_example/rules/CAPTCHA.yml b/config_example/rules/CAPTCHA.yml index 14c411d..d3bbb2c 100644 --- a/config_example/rules/CAPTCHA.yml +++ b/config_example/rules/CAPTCHA.yml @@ -1,3 +1,4 @@ secret_key: "0378b0f84c4310279918d71a5647ba5d" captcha_validate_time: 600 +captcha_challenge_session_timeout: 120 hcaptcha_secret: "" \ No newline at end of file diff --git a/internal/check/Captcha.go b/internal/check/Captcha.go index d56db0f..97178b4 100644 --- a/internal/check/Captcha.go +++ b/internal/check/Captcha.go @@ -32,7 +32,7 @@ func Captcha(reqData dataType.UserRequest, ruleSet *config.RuleSet, decision *ac } if !verifyClearanceCookie(reqData, *ruleSet) { - decision.SetCode(action.Done, []byte("CAPTCHA")) + decision.SetResponse(action.Done, []byte("CAPTCHA"), genSessionID(reqData, *ruleSet)) return } @@ -52,6 +52,11 @@ func CheckCaptcha(r *http.Request, reqData dataType.UserRequest, ruleSet *config return } + if !verifySessionIDCookie(reqData, *ruleSet) { + decision.SetResponse(action.Done, []byte("200"), []byte("badSession")) + return + } + data := url.Values{} data.Set("secret", ruleSet.CAPTCHARule.HCaptchaSecret) data.Set("response", hCaptchaResponse) @@ -130,3 +135,40 @@ func verifyClearanceCookie(reqData dataType.UserRequest, ruleSet config.RuleSet) return hmac.Equal([]byte(computedHash), []byte(expectedHash)) } + +func genSessionID(reqData dataType.UserRequest, ruleSet config.RuleSet) []byte { + timeNow := time.Now().Unix() + mac := hmac.New(sha512.New, []byte(ruleSet.CAPTCHARule.SecretKey)) + mac.Write([]byte(fmt.Sprintf("%d%s%sCAPTCHA-SESSION", timeNow, reqData.Host, utils.GetClearanceUserAgent(reqData.UserAgent)))) + return []byte(fmt.Sprintf("%s:%s", fmt.Sprintf("%d", time.Now().Unix()), fmt.Sprintf("%x", mac.Sum(nil)))) +} + +func verifySessionIDCookie(reqData dataType.UserRequest, ruleSet config.RuleSet) bool { + if reqData.ToriiSessionID == "" { + return false + } + parts := strings.Split(reqData.ToriiSessionID, ":") + if len(parts) != 2 { + return false + } + timestamp := parts[0] + expectedHash := parts[1] + + timeNow := time.Now().Unix() + parsedTimestamp, err := strconv.ParseInt(timestamp, 10, 64) + if err != nil { + log.Printf("Error parsing timestamp: %v", err) + return false + } + + if timeNow-parsedTimestamp > ruleSet.CAPTCHARule.CaptchaChallengeSessionTimeout { + return false + } + + mac := hmac.New(sha512.New, []byte(ruleSet.CAPTCHARule.SecretKey)) + mac.Write([]byte(fmt.Sprintf("%d%s%sCAPTCHA-SESSION", parsedTimestamp, reqData.Host, utils.GetClearanceUserAgent(reqData.UserAgent)))) + computedHash := fmt.Sprintf("%x", mac.Sum(nil)) + + return hmac.Equal([]byte(computedHash), []byte(expectedHash)) + +} diff --git a/internal/dataType/type.go b/internal/dataType/type.go index db83d66..426764e 100644 --- a/internal/dataType/type.go +++ b/internal/dataType/type.go @@ -11,7 +11,8 @@ type UserRequest struct { } type CaptchaRule struct { - SecretKey string `yaml:"secret_key"` - CaptchaValidateTime int64 `yaml:"captcha_validate_time"` - HCaptchaSecret string `yaml:"hcaptcha_secret"` + SecretKey string `yaml:"secret_key"` + CaptchaValidateTime int64 `yaml:"captcha_validate_time"` + CaptchaChallengeSessionTimeout int64 `yaml:"captcha_challenge_session_timeout"` + HCaptchaSecret string `yaml:"hcaptcha_secret"` } diff --git a/internal/server/checker.go b/internal/server/checker.go index 962094e..1356362 100644 --- a/internal/server/checker.go +++ b/internal/server/checker.go @@ -70,6 +70,7 @@ func CheckMain(w http.ResponseWriter, userRequestData dataType.UserRequest, rule http.Error(w, "500 - Internal Server Error", http.StatusInternalServerError) return } + w.Header().Set("Set-Cookie", "__torii_session_id="+string(decision.ResponseData)+"; Path=/; Path=/; Max-Age=86400; Priority=High; HttpOnly;") w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusServiceUnavailable) if err = tpl.Execute(w, nil); err != nil { diff --git a/internal/server/torii.go b/internal/server/torii.go index 5218d50..e314732 100644 --- a/internal/server/torii.go +++ b/internal/server/torii.go @@ -28,6 +28,14 @@ func CheckTorii(w http.ResponseWriter, r *http.Request, reqData dataType.UserReq return } return + } else if bytes.Compare(decision.ResponseData, []byte("badSession")) == 0 { + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte("badSession")) + if err != nil { + log.Printf("Error writing response: %v", err) + return + } + return } else if bytes.Compare(decision.ResponseData, []byte("good")) == 0 { w.Header().Set("Set-Cookie", "__torii_clearance="+string(check.GenClearance(reqData, *ruleSet))+"; Path=/; Max-Age=86400; Priority=High; HttpOnly;") w.WriteHeader(http.StatusOK)