Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions conf/artalk.example.simple.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ captcha:
geetest:
captcha_id: ""
captcha_key: ""
capjs:
key_id: ""
secret_key: ""
api_endpoint: ""
img_upload:
enabled: true
path: ./data/artalk-img/
Expand Down
8 changes: 6 additions & 2 deletions conf/artalk.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ captcha:
enabled: true
# Captcha is required always
always: false
# Captcha type ["image", "turnstile", "recaptcha", "hcaptcha", "geetest"]
# Captcha type ["image", "turnstile", "recaptcha", "hcaptcha", "geetest", "capjs"]
captcha_type: image
# Action limit
# (the number of actions required to activate captcha)
Expand All @@ -175,7 +175,11 @@ captcha:
geetest:
captcha_id: ""
captcha_key: ""

# Cap (https://capjs.js.org/)
capjs:
key_id: ""
secret_key: ""
api_endpoint: ""
# Upload
img_upload:
# Enable image upload
Expand Down
8 changes: 6 additions & 2 deletions conf/artalk.example.zh-CN.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ captcha:
enabled: true
# 总是需要验证码
always: false
# 验证类型 ["image", "turnstile", "recaptcha", "hcaptcha", "geetest"]
# 验证类型 ["image", "turnstile", "recaptcha", "hcaptcha", "geetest", "capjs"]
captcha_type: image
# 激活验证码所需操作次数
action_limit: 3
Expand All @@ -176,7 +176,11 @@ captcha:
geetest:
captcha_id: ""
captcha_key: ""

# Cap (https://capjs.js.org/)
capjs:
key_id: ""
secret_key: ""
api_endpoint: ""
# IP 属地
ip_region:
# 启用 IP 属地展示
Expand Down
5 changes: 4 additions & 1 deletion docs/docs/en/guide/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,10 @@ ATK_TRUSTED_DOMAINS_0="https://a.com"
| **ATK_CAPTCHA_ACTION_LIMIT** | `3` | Action limit (the number of actions required to activate captcha) | captcha.action_limit (Captcha > Action limit) |
| **ATK_CAPTCHA_ACTION_RESET** | `60` | Reset Timeout (timeout to reset action counter. unit: s, set to -1 to disable) | captcha.action_reset (Captcha > Reset Timeout) |
| **ATK_CAPTCHA_ALWAYS** | `false` | Captcha is required always | captcha.always (Captcha > Captcha is required always) |
| **ATK_CAPTCHA_CAPTCHA_TYPE** | `"image"` | Captcha type (可选:`["image", "turnstile", "recaptcha", "hcaptcha", "geetest"]`) | captcha.captcha_type (Captcha > Captcha type) |
| **ATK_CAPTCHA_CAPJS_API_ENDPOINT** | `""` | ApiEndpoint | captcha.capjs.api_endpoint (Captcha > Cap > ApiEndpoint) |
| **ATK_CAPTCHA_CAPJS_KEY_ID** | `""` | KeyId | captcha.capjs.key_id (Captcha > Cap > KeyId) |
| **ATK_CAPTCHA_CAPJS_SECRET_KEY** | `""` | SecretKey | captcha.capjs.secret_key (Captcha > Cap > SecretKey) |
| **ATK_CAPTCHA_CAPTCHA_TYPE** | `"image"` | Captcha type (可选:`["image", "turnstile", "recaptcha", "hcaptcha", "geetest", "capjs"]`) | captcha.captcha_type (Captcha > Captcha type) |
| **ATK_CAPTCHA_ENABLED** | `true` | Enable captcha | captcha.enabled (Captcha > Enable captcha) |
| **ATK_CAPTCHA_GEETEST_CAPTCHA_ID** | `""` | CaptchaId | captcha.geetest.captcha_id (Captcha > Geetest > CaptchaId) |
| **ATK_CAPTCHA_GEETEST_CAPTCHA_KEY** | `""` | CaptchaKey | captcha.geetest.captcha_key (Captcha > Geetest > CaptchaKey) |
Expand Down
5 changes: 4 additions & 1 deletion docs/docs/zh/guide/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,10 @@ ATK_TRUSTED_DOMAINS_0="https://a.com"
| **ATK_CAPTCHA_ACTION_LIMIT** | `3` | 激活验证码所需操作次数 | captcha.action_limit (验证码 > 激活验证码所需操作次数) |
| **ATK_CAPTCHA_ACTION_RESET** | `60` | 重置操作计数器超时 (单位:s, 设为 -1 不重置) | captcha.action_reset (验证码 > 重置操作计数器超时) |
| **ATK_CAPTCHA_ALWAYS** | `false` | 总是需要验证码 | captcha.always (验证码 > 总是需要验证码) |
| **ATK_CAPTCHA_CAPTCHA_TYPE** | `"image"` | 验证类型 (可选:`["image", "turnstile", "recaptcha", "hcaptcha", "geetest"]`) | captcha.captcha_type (验证码 > 验证类型) |
| **ATK_CAPTCHA_CAPJS_API_ENDPOINT** | `""` | ApiEndpoint | captcha.capjs.api_endpoint (验证码 > Cap > ApiEndpoint) |
| **ATK_CAPTCHA_CAPJS_KEY_ID** | `""` | KeyId | captcha.capjs.key_id (验证码 > Cap > KeyId) |
| **ATK_CAPTCHA_CAPJS_SECRET_KEY** | `""` | SecretKey | captcha.capjs.secret_key (验证码 > Cap > SecretKey) |
| **ATK_CAPTCHA_CAPTCHA_TYPE** | `"image"` | 验证类型 (可选:`["image", "turnstile", "recaptcha", "hcaptcha", "geetest", "capjs"]`) | captcha.captcha_type (验证码 > 验证类型) |
| **ATK_CAPTCHA_ENABLED** | `true` | 启用验证码 | captcha.enabled (验证码 > 启用验证码) |
| **ATK_CAPTCHA_GEETEST_CAPTCHA_ID** | `""` | CaptchaId | captcha.geetest.captcha_id (验证码 > Geetest 极验 > CaptchaId) |
| **ATK_CAPTCHA_GEETEST_CAPTCHA_KEY** | `""` | CaptchaKey | captcha.geetest.captcha_key (验证码 > Geetest 极验 > CaptchaKey) |
Expand Down
2 changes: 2 additions & 0 deletions internal/captcha/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
return NewHCaptchaChecker(&conf.HCaptcha, &conf.User)
case config.TypeGeetest:
return NewGeetestChecker(&conf.Geetest, &conf.User)
case config.TypeCapJS:
return NewCapJSChecker(&conf.CapJS, &conf.User)

Check warning on line 45 in internal/captcha/checker.go

View check run for this annotation

Codecov / codecov/patch

internal/captcha/checker.go#L44-L45

Added lines #L44 - L45 were not covered by tests
default:
panic("Unknown captcha type")
}
Expand Down
69 changes: 69 additions & 0 deletions internal/captcha/checker_cap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package captcha

import (
"fmt"
"io"
"net/http"
"net/url"
"time"

"github.com/artalkjs/artalk/v2/internal/config"
"github.com/tidwall/gjson"
)

var _ Checker = (*CapJSCaptchaChecker)(nil)

type CapJSCaptchaChecker struct {
User *User
KeyID string
SecretKey string
APIEndpoint string
}

func NewCapJSChecker(conf *config.CapJSConf, user *User) *CapJSCaptchaChecker {
return &CapJSCaptchaChecker{
User: user,
KeyID: conf.KeyID,
SecretKey: conf.SecretKey,
APIEndpoint: conf.APIEndpoint,
}

Check warning on line 29 in internal/captcha/checker_cap.go

View check run for this annotation

Codecov / codecov/patch

internal/captcha/checker_cap.go#L23-L29

Added lines #L23 - L29 were not covered by tests
}

func (c *CapJSCaptchaChecker) Check(value string) (bool, error) {
// 构建 POST 请求的参数
values := make(url.Values)
values.Add("secret", c.SecretKey)
values.Add("response", value)

// 发送 POST 请求
url := c.APIEndpoint + "/" + c.KeyID + "/siteverify"
cli := http.Client{Timeout: time.Second * 10} // 10s 超时
resp, err := cli.PostForm(url, values)
if err != nil || resp.StatusCode != 200 {
return false, err
}
defer resp.Body.Close()

// 解析响应内容
respBuf, _ := io.ReadAll(resp.Body)
success := gjson.GetBytes(respBuf, "success")
if success.Exists() && success.Bool() {
// 验证成功
return true, nil
} else {
// 验证失败
return false, fmt.Errorf("err reason: %s", gjson.GetBytes(respBuf, "error-codes").String())
}

Check warning on line 56 in internal/captcha/checker_cap.go

View check run for this annotation

Codecov / codecov/patch

internal/captcha/checker_cap.go#L32-L56

Added lines #L32 - L56 were not covered by tests
}

func (c *CapJSCaptchaChecker) Type() CaptchaType {
return IFrame

Check warning on line 60 in internal/captcha/checker_cap.go

View check run for this annotation

Codecov / codecov/patch

internal/captcha/checker_cap.go#L59-L60

Added lines #L59 - L60 were not covered by tests
}

func (c *CapJSCaptchaChecker) Get() ([]byte, error) {
return RenderIFrame("capjs.html", Map{
"api_endpoint": c.APIEndpoint,
"key_id": c.KeyID,
"secret_key": c.SecretKey,
})

Check warning on line 68 in internal/captcha/checker_cap.go

View check run for this annotation

Codecov / codecov/patch

internal/captcha/checker_cap.go#L63-L68

Added lines #L63 - L68 were not covered by tests
}
56 changes: 56 additions & 0 deletions internal/captcha/pages/capjs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cap</title>
<script src="{{.api_endpoint}}/assets/widget.js"></script>
<style>
body,
html {
margin: 0;
padding: 0;
}
body {
display: flex;
justify-content: center;
height: 100vh;
align-items: center;
}
</style>
</head>
<body>
<cap-widget
data-cap-api-endpoint="{{.api_endpoint}}/{{.key_id}}/api/"
></cap-widget>
<script>
const capWidget = document.querySelector('cap-widget');
capWidget.addEventListener('solve', function (event) {
console.log('验证成功', event.detail.token);

fetch('./verify', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ value: event.detail.token }),
})
.then((res) => {
if (!res.ok)
res.json().then((json) => {
alert('验证失败:' + res.status + ' ' + json.msg || '')
})
})
.catch((err) => {
console.error(err)
alert('后端 API 请求失败:' + err.message || '')
})
});
// capWidget.addEventListener('error', function (event) {
// alert('验证失败');
// });
</script>
</body>
</html>
4 changes: 2 additions & 2 deletions internal/config/cache.go

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ type CaptchaConf struct {
ReCaptcha ReCaptchaConf `koanf:"recaptcha" json:"recaptcha"`
HCaptcha HCaptchaConf `koanf:"hcaptcha" json:"hcaptcha"`
Geetest GeetestConf `koanf:"geetest" json:"geetest"`
CapJS CapJSConf `koanf:"capjs" json:"capjs"`
}

type CaptchaType string
Expand All @@ -172,6 +173,7 @@ const (
TypeReCaptcha CaptchaType = "recaptcha"
TypeHCaptcha CaptchaType = "hcaptcha"
TypeGeetest CaptchaType = "geetest"
TypeCapJS CaptchaType = "capjs"
)

type TurnstileConf struct {
Expand All @@ -195,6 +197,12 @@ type GeetestConf struct {
CaptchaKey string `koanf:"captcha_key" json:"captcha_key"`
}

type CapJSConf struct {
KeyID string `koanf:"key_id" json:"key_id"`
SecretKey string `koanf:"secret_key" json:"secret_key"`
APIEndpoint string `koanf:"api_endpoint" json:"api_endpoint"`
}

type EmailConf struct {
Enabled bool `koanf:"enabled" json:"enabled"` // 总开关
SendType EmailSenderType `koanf:"send_type" json:"send_type"` // 发送方式
Expand Down