From 12c7473e36f79e8da23d026d3f5601601a82fd77 Mon Sep 17 00:00:00 2001
From: Roi Feng <37480123+Rayzggz@users.noreply.github.com>
Date: Wed, 19 Feb 2025 22:24:45 -0500
Subject: [PATCH] feat: CAPTCHA session ID
---
config/error_page/CAPTCHA.html | 18 ++++++-
.../{CAPTCHA.yml => CAPTCHA.example.yml} | 3 +-
config/{torii.yml => torii.example.yml} | 6 +--
internal/check/Captcha.go | 49 +++++++++++++++++--
internal/dataType/type.go | 7 +--
internal/server/checker.go | 3 +-
internal/server/torii.go | 10 +++-
7 files changed, 82 insertions(+), 14 deletions(-)
rename config/rules/{CAPTCHA.yml => CAPTCHA.example.yml} (53%)
rename config/{torii.yml => torii.example.yml} (63%)
diff --git a/config/error_page/CAPTCHA.html b/config/error_page/CAPTCHA.html
index 17d7519..fa90b8b 100644
--- a/config/error_page/CAPTCHA.html
+++ b/config/error_page/CAPTCHA.html
@@ -65,6 +65,10 @@
alert("Bad CAPTCHA, please refresh the page and try again.\n"
+ "您未能通过人机验证,请刷新页面后重试。");
break;
+ case "timeout":
+ alert("Verification timeout, please refresh the page and try again.\n"
+ + "验证超时,请刷新页面后重试。");
+ break;
default:
alert("Unexpected error occurred, please refresh the page and try again.\n"
+ "发生了意料之外的错误,请刷新页面后重试。");
@@ -72,12 +76,24 @@
}
}
}
+
+ function checkCaptchaRender() {
+ const captchaDiv = document.querySelector(".h-captcha");
+
+ if (captchaDiv && captchaDiv.children.length > 0) {
+ } else {
+ document.getElementById("verifyBox").innerHTML = "Loading CAPTCHA failed, please check your internet connection and try again.
"
+ + "加载人机验证失败,请检查尝试更换网络环境后重试。";
+ }
+ }
+ setTimeout(checkCaptchaRender, 5000); // 5秒后检查
Checking that you are not a robot
-
+
请完成人机验证
+
diff --git a/config/rules/CAPTCHA.yml b/config/rules/CAPTCHA.example.yml
similarity index 53%
rename from config/rules/CAPTCHA.yml
rename to config/rules/CAPTCHA.example.yml
index df022a7..96736f0 100644
--- a/config/rules/CAPTCHA.yml
+++ b/config/rules/CAPTCHA.example.yml
@@ -1,3 +1,4 @@
secret_key: "0378b0f84c4310279918d71a5647ba5d"
-captcha_validate_time: 60
+captcha_validate_time: 600
+captcha_challenge_timeout: 120
hcaptcha_secret: ""
\ No newline at end of file
diff --git a/config/torii.yml b/config/torii.example.yml
similarity index 63%
rename from config/torii.yml
rename to config/torii.example.yml
index cc41179..e03094f 100644
--- a/config/torii.yml
+++ b/config/torii.example.yml
@@ -1,8 +1,8 @@
port: "25555"
web_path: "/torii"
-rule_path: "/www/dev/server_torii/config/rules"
-error_page: "/www/dev/server_torii/config/error_page"
-log_path: "/www/dev/server_torii/log/access.log"
+rule_path: "/www/server_torii/config/rules"
+error_page: "/www/server_torii/config/error_page"
+log_path: "/www/server_torii/log/access.log"
node_name: "Server Torii"
connecting_host_headers:
- "Torii-Real-Host"
diff --git a/internal/check/Captcha.go b/internal/check/Captcha.go
index ec73e4d..ff8cf10 100644
--- a/internal/check/Captcha.go
+++ b/internal/check/Captcha.go
@@ -31,7 +31,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
}
@@ -51,6 +51,11 @@ func CheckCaptcha(r *http.Request, reqData dataType.UserRequest, ruleSet *config
return
}
+ if !verifySessionCookie(reqData, *ruleSet) {
+ decision.SetResponse(action.Done, []byte("200"), []byte("timeout"))
+ return
+ }
+
data := url.Values{}
data.Set("secret", ruleSet.CAPTCHARule.HCaptchaSecret)
data.Set("response", hCaptchaResponse)
@@ -84,7 +89,7 @@ func CheckCaptcha(r *http.Request, reqData dataType.UserRequest, ruleSet *config
}
if !hCaptchaResp.Success {
- decision.SetResponse(action.Done, []byte("200"), []byte("bad4"))
+ decision.SetResponse(action.Done, []byte("200"), []byte("bad"))
return
}
@@ -93,10 +98,46 @@ func CheckCaptcha(r *http.Request, reqData dataType.UserRequest, ruleSet *config
}
+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-ID", timeNow, reqData.Host, reqData.UserAgent)))
+ return []byte(fmt.Sprintf("%s:%s", fmt.Sprintf("%d", time.Now().Unix()), fmt.Sprintf("%x", mac.Sum(nil))))
+}
+
+func verifySessionCookie(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.CaptchaChallengeTimeout {
+ return false
+ }
+
+ mac := hmac.New(sha512.New, []byte(ruleSet.CAPTCHARule.SecretKey))
+ mac.Write([]byte(fmt.Sprintf("%d%s%sCAPTCHA-SESSION-ID", parsedTimestamp, reqData.Host, reqData.UserAgent)))
+ computedHash := fmt.Sprintf("%x", mac.Sum(nil))
+
+ return hmac.Equal([]byte(computedHash), []byte(expectedHash))
+}
+
func GenClearance(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%s", timeNow, reqData.Host, reqData.UserAgent)))
+ mac.Write([]byte(fmt.Sprintf("%d%s%sCAPTCHA-CLEARANCE", timeNow, reqData.Host, reqData.UserAgent)))
return []byte(fmt.Sprintf("%s:%s", fmt.Sprintf("%d", time.Now().Unix()), fmt.Sprintf("%x", mac.Sum(nil))))
}
@@ -123,7 +164,7 @@ func verifyClearanceCookie(reqData dataType.UserRequest, ruleSet config.RuleSet)
}
mac := hmac.New(sha512.New, []byte(ruleSet.CAPTCHARule.SecretKey))
- mac.Write([]byte(fmt.Sprintf("%d%s%s", parsedTimestamp, reqData.Host, reqData.UserAgent)))
+ mac.Write([]byte(fmt.Sprintf("%d%s%sCAPTCHA-CLEARANCE", parsedTimestamp, reqData.Host, 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..3052c41 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"`
+ CaptchaChallengeTimeout int64 `yaml:"captcha_challenge_timeout"`
+ HCaptchaSecret string `yaml:"hcaptcha_secret"`
}
diff --git a/internal/server/checker.go b/internal/server/checker.go
index 9d048f3..8e5df8a 100644
--- a/internal/server/checker.go
+++ b/internal/server/checker.go
@@ -70,8 +70,9 @@ func CheckMain(w http.ResponseWriter, userRequestData dataType.UserRequest, rule
http.Error(w, "500 - Internal Server Error", http.StatusInternalServerError)
return
}
- w.WriteHeader(http.StatusServiceUnavailable)
+ w.Header().Set("Set-Cookie", "__torii_session_id="+string(decision.ResponseData)+"; 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 {
log.Printf("Error template: %v", err)
http.Error(w, "500 - Internal Server Error", http.StatusInternalServerError)
diff --git a/internal/server/torii.go b/internal/server/torii.go
index c45a74f..c2696db 100644
--- a/internal/server/torii.go
+++ b/internal/server/torii.go
@@ -29,13 +29,21 @@ func CheckTorii(w http.ResponseWriter, r *http.Request, reqData dataType.UserReq
}
return
} else if bytes.Compare(decision.ResponseData, []byte("good")) == 0 {
- w.Header().Set("Set-Cookie", "__torii_clearance="+string(check.GenClearance(reqData, *ruleSet))+"; Path=/; HttpOnly")
+ w.Header().Set("Set-Cookie", "__torii_clearance="+string(check.GenClearance(reqData, *ruleSet))+"; Path=/; Max-Age=86400; Priority=High; HttpOnly;")
w.WriteHeader(http.StatusOK)
_, err := w.Write(decision.ResponseData)
if err != nil {
log.Printf("Error writing response: %v", err)
return
}
+ } else if bytes.Compare(decision.ResponseData, []byte("timeout")) == 0 {
+ w.WriteHeader(http.StatusOK)
+ _, err := w.Write([]byte("timeout"))
+ if err != nil {
+ log.Printf("Error writing response: %v", err)
+ return
+ }
+ return
} else {
//should not be here
w.WriteHeader(http.StatusInternalServerError)