fix: 电竞手册用 ctn(acf_ccn) 鉴权替代 csrfToken
peacematch 与 peace 的兑换接口鉴权机制完全不同: - peace: 表单发 csrfToken 字段,multipart 格式 - peacematch: 表单发 ctn 字段(值=acf_ccn),urlencoded 格式, cvl_csrf_token 只放 Cookie,不需要 batchExchangeLimit/preExchange 改动: - Activity 加 UseCTN/CTNURL 字段 - GenerateCSRF 对 UseCTN 活动额外 FetchCTN 获取 acf_ccn,组合返回 - 新增 postForm(urlencoded) 方法用于 peacematch 兑换 - Exchange 按 UseCTN 分流:peacematch 发 ctn 字段,peace 发 csrfToken - BatchExchangeLimit/PreExchange 对 UseCTN 活动跳过(返回默认值) - 参考 /Users/yml/codes/douyu_login_py 中 Python 实现
This commit is contained in:
@@ -32,6 +32,14 @@ type Activity struct {
|
||||
BindInfoURL string
|
||||
ScoreURL string
|
||||
ScoreMethod string // "POST" 或 "GET"
|
||||
|
||||
// UseCTN 为 true 时(电竞手册),兑换接口用 ctn(acf_ccn) 鉴权而非 csrfToken:
|
||||
// - 表单发 ctn 字段(值=acf_ccn),不发 csrfToken
|
||||
// - cvl_csrf_token 只放 Cookie 头
|
||||
// - 跳过 batchExchangeLimit / preExchange(Python 实现直接调 exchange)
|
||||
// - Content-Type 用 application/x-www-form-urlencoded
|
||||
UseCTN bool
|
||||
CTNURL string // 获取 acf_ccn 的接口(UseCTN=true 时必填)
|
||||
}
|
||||
|
||||
const douyuBase = constants.BaseURL
|
||||
@@ -54,6 +62,7 @@ var peace = &Activity{
|
||||
BindInfoURL: douyuBase + "/japi/carnivalApi/v2/tencent/bindInfo?actAlias=20260120QYOOB",
|
||||
ScoreURL: douyuBase + "/japi/oms/web/peace/credit/balance",
|
||||
ScoreMethod: "POST",
|
||||
UseCTN: false,
|
||||
}
|
||||
|
||||
// peacematch 电竞手册(新活动,ActAlias=20260226ZMGDG)。
|
||||
@@ -77,6 +86,8 @@ var peacematch = &Activity{
|
||||
BindInfoURL: douyuBase + "/wgapi/ordnc/live/memberthird/tencent/bindInfo?actAlias=20260226ZMGDG",
|
||||
ScoreURL: douyuBase + "/wgapi/ordnc/activity/peacematch/score/balance",
|
||||
ScoreMethod: "GET",
|
||||
UseCTN: true,
|
||||
CTNURL: douyuBase + "/curl/csrfNlApi/getCsrfCookie",
|
||||
}
|
||||
|
||||
var registry = map[string]*Activity{
|
||||
|
||||
+127
-14
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -303,6 +304,14 @@ func (c *DouyuClient) GenerateCSRF(cookie string) (string, error) {
|
||||
if ck.Name == "cvl_csrf_token" && ck.Value != "" {
|
||||
c.Trace.RecordResponse("生成CSRF", req, resp, resp.StatusCode, time.Since(start), reqBody, body, "", api.Error, api.Msg)
|
||||
logNetworkSuccess("生成CSRF", req, time.Since(start), 0, "")
|
||||
// 电竞手册还需获取 acf_ccn 作为 ctn,组合返回 csrf\x00ctn
|
||||
if c.Activity.UseCTN {
|
||||
ctn, err := c.FetchCTN(utils.MergeCookie(cookie, "cvl_csrf_token", ck.Value))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取CTN失败: %w", err)
|
||||
}
|
||||
return ck.Value + "\x00" + ctn, nil
|
||||
}
|
||||
return ck.Value, nil
|
||||
}
|
||||
}
|
||||
@@ -312,6 +321,34 @@ func (c *DouyuClient) GenerateCSRF(cookie string) (string, error) {
|
||||
return "", errors.New("CSRF 响应没有 cvl_csrf_token")
|
||||
}
|
||||
|
||||
// FetchCTN 调用 csrfNlApi/getCsrfCookie 获取 acf_ccn(电竞手册用 ctn 鉴权)。
|
||||
func (c *DouyuClient) FetchCTN(cookie string) (string, error) {
|
||||
req, err := http.NewRequest("GET", c.Activity.CTNURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
c.setBrowserHeaders(req, cookie)
|
||||
|
||||
start := time.Now()
|
||||
reqBody := c.Trace.RequestBody(req)
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
c.Trace.Record("获取CTN", req, 0, time.Since(start), reqBody, nil, err.Error(), 0, "")
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
io.ReadAll(resp.Body) // 消费 body
|
||||
|
||||
for _, ck := range resp.Cookies() {
|
||||
if ck.Name == "acf_ccn" && ck.Value != "" {
|
||||
c.Trace.RecordResponse("获取CTN", req, resp, resp.StatusCode, time.Since(start), reqBody, nil, "", 0, "")
|
||||
return ck.Value, nil
|
||||
}
|
||||
}
|
||||
c.Trace.RecordResponse("获取CTN", req, resp, resp.StatusCode, time.Since(start), reqBody, nil, "响应没有 acf_ccn", 0, "")
|
||||
return "", errors.New("CTN 响应没有 acf_ccn")
|
||||
}
|
||||
|
||||
func csrfFromResponse(resp *http.Response) string {
|
||||
if resp == nil {
|
||||
return ""
|
||||
@@ -324,8 +361,11 @@ func csrfFromResponse(resp *http.Response) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// BatchExchangeLimit 请求兑换限制接口。
|
||||
// BatchExchangeLimit 请求兑换限制接口。电竞手册不需要此步骤,直接跳过返回默认限制。
|
||||
func (c *DouyuClient) BatchExchangeLimit(cookie, commodityID string, csrf *string) (int, error) {
|
||||
if c.Activity.UseCTN {
|
||||
return 1, nil
|
||||
}
|
||||
currentCSRF := ""
|
||||
if csrf != nil {
|
||||
currentCSRF = *csrf
|
||||
@@ -354,8 +394,11 @@ func (c *DouyuClient) BatchExchangeLimit(cookie, commodityID string, csrf *strin
|
||||
return limit, nil
|
||||
}
|
||||
|
||||
// PreExchange 按抓包流程执行预兑换检查。
|
||||
// PreExchange 按抓包流程执行预兑换检查。电竞手册不需要此步骤,直接跳过。
|
||||
func (c *DouyuClient) PreExchange(cookie, commodityID string, csrf *string) error {
|
||||
if c.Activity.UseCTN {
|
||||
return nil
|
||||
}
|
||||
currentCSRF := ""
|
||||
if csrf != nil {
|
||||
currentCSRF = *csrf
|
||||
@@ -378,10 +421,6 @@ func (c *DouyuClient) PreExchange(cookie, commodityID string, csrf *string) erro
|
||||
|
||||
// Exchange 正式兑换一个商品。
|
||||
func (c *DouyuClient) Exchange(cookie, commodityID string, csrf *string) (string, error) {
|
||||
currentCSRF := ""
|
||||
if csrf != nil {
|
||||
currentCSRF = *csrf
|
||||
}
|
||||
var result struct {
|
||||
types.APIResponse
|
||||
Data struct {
|
||||
@@ -390,14 +429,47 @@ func (c *DouyuClient) Exchange(cookie, commodityID string, csrf *string) (string
|
||||
CommodityTyp int `json:"commodityType"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := c.postMultipart(c.Activity.ExchangeURL, cookie, csrf, "正式兑换", map[string]string{
|
||||
"manualID": c.Activity.ManualID,
|
||||
"commodityID": commodityID,
|
||||
"num": "1",
|
||||
"rid": c.Activity.RoomID,
|
||||
"csrfToken": currentCSRF,
|
||||
}, &result); err != nil {
|
||||
return "", err
|
||||
if c.Activity.UseCTN {
|
||||
// 电竞手册:用 ctn(acf_ccn) 鉴权,urlencoded 表单
|
||||
ctn := ""
|
||||
if csrf != nil {
|
||||
parts := strings.SplitN(*csrf, "\x00", 2)
|
||||
if len(parts) > 1 {
|
||||
ctn = parts[1]
|
||||
}
|
||||
}
|
||||
if ctn == "" {
|
||||
// 缓存中没有 ctn,现场获取
|
||||
ctnVal, err := c.FetchCTN(cookie)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取CTN失败: %w", err)
|
||||
}
|
||||
ctn = ctnVal
|
||||
}
|
||||
if err := c.postForm(c.Activity.ExchangeURL, cookie, csrf, "正式兑换", map[string]string{
|
||||
"manualID": c.Activity.ManualID,
|
||||
"commodityID": commodityID,
|
||||
"num": "1",
|
||||
"rid": c.Activity.RoomID,
|
||||
"ctn": ctn,
|
||||
}, &result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
// 精英手册:用 csrfToken 鉴权,multipart 表单
|
||||
currentCSRF := ""
|
||||
if csrf != nil {
|
||||
currentCSRF = *csrf
|
||||
}
|
||||
if err := c.postMultipart(c.Activity.ExchangeURL, cookie, csrf, "正式兑换", map[string]string{
|
||||
"manualID": c.Activity.ManualID,
|
||||
"commodityID": commodityID,
|
||||
"num": "1",
|
||||
"rid": c.Activity.RoomID,
|
||||
"csrfToken": currentCSRF,
|
||||
}, &result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if result.Error != 0 {
|
||||
if strings.TrimSpace(result.Msg) == "" {
|
||||
@@ -504,6 +576,47 @@ func (c *DouyuClient) postMultipart(url, cookie string, csrf *string, label stri
|
||||
return err
|
||||
}
|
||||
|
||||
// postForm 发送 application/x-www-form-urlencoded POST 请求。
|
||||
// 用于电竞手册等使用 ctn 鉴权的活动。
|
||||
// csrf 参数为组合值 "cvl_csrf_token\x00acf_ccn",两者都会合并到 Cookie 头。
|
||||
func (c *DouyuClient) postForm(targetURL, cookie string, csrf *string, label string, fields map[string]string, out any) error {
|
||||
vals := url.Values{}
|
||||
for k, v := range fields {
|
||||
vals.Set(k, v)
|
||||
}
|
||||
req, err := http.NewRequest("POST", targetURL, strings.NewReader(vals.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 拆分组合 token,合并到 Cookie
|
||||
csrfVal := ""
|
||||
ctnVal := ""
|
||||
if csrf != nil {
|
||||
parts := strings.SplitN(*csrf, "\x00", 2)
|
||||
csrfVal = parts[0]
|
||||
if len(parts) > 1 {
|
||||
ctnVal = parts[1]
|
||||
}
|
||||
}
|
||||
mergedCookie := utils.MergeCookie(cookie, "cvl_csrf_token", csrfVal)
|
||||
if ctnVal != "" {
|
||||
mergedCookie = utils.MergeCookie(mergedCookie, "acf_ccn", ctnVal)
|
||||
}
|
||||
c.setBrowserHeaders(req, mergedCookie)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||
resp, err := c.doJSON(req, out, label)
|
||||
if resp != nil && csrf != nil {
|
||||
if nextCSRF := csrfFromResponse(resp); nextCSRF != "" {
|
||||
if ctnVal != "" {
|
||||
*csrf = nextCSRF + "\x00" + ctnVal
|
||||
} else {
|
||||
*csrf = nextCSRF
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *DouyuClient) doJSON(req *http.Request, out any, label string) (*http.Response, error) {
|
||||
start := time.Now()
|
||||
reqBody := c.Trace.RequestBody(req)
|
||||
|
||||
Reference in New Issue
Block a user