Skip to content

Commit

Permalink
修复TCP缓冲区不足问题;重构 qsign 签名服务对接部分;支持配置多个签名服务器 (#2389)
Browse files Browse the repository at this point in the history
* fix: skip callback error

* update: update comment

* change the logic of callback and auto-register

* add token update prompt.

* fix log buffer string

* fix #2368

增加对 client 的利用,避免创建过多 clients

* refactor: wrap sign request

* feat: impl additional sign servers configuration

* fix error in using configurations.

* fix lint error

* 支持切换回主签名服务器

* feat: support different key and auth

* optimize: find avaliable sign-server

* fix: register instance after server is changed

* fix lint error

* update: add config 'sync-check-servers'

* update: first check master sign-server, or wait 3s

* add checking log & optimize wait for checking done

* fix wrong judge

* add config: rule for changing sign server

* optimize registration logic after changing server

* add some log

* fix #2390

* resolve requested changes in #2389

* update dependency

* fix lint error 'idx is unused'

* refactor: extract sync check and async check logic

* delete async check sign-server
  • Loading branch information
1umine committed Aug 27, 2023
1 parent d85d697 commit f8354ec
Show file tree
Hide file tree
Showing 10 changed files with 522 additions and 376 deletions.
305 changes: 1 addition & 304 deletions cmd/gocq/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,21 @@ package gocq
import (
"bufio"
"bytes"
"encoding/hex"
"fmt"
"image"
"image/png"
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"

"github.com/Mrs4s/MiraiGo/client"
"github.com/Mrs4s/MiraiGo/utils"
"github.com/mattn/go-colorable"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"gopkg.ilharper.com/x/isatty"

"github.com/Mrs4s/go-cqhttp/global"
"github.com/Mrs4s/go-cqhttp/internal/base"
"github.com/Mrs4s/go-cqhttp/internal/download"
)

Expand Down Expand Up @@ -268,298 +259,4 @@ func fetchCaptcha(id string) string {
return g.Get("ticket").String()
}
return ""
}

func energy(uin uint64, id string, _ string, salt []byte) ([]byte, error) {
signServer := base.SignServer
if !strings.HasSuffix(signServer, "/") {
signServer += "/"
}
headers := make(map[string]string)
signServerBearer := base.SignServerBearer
if signServerBearer != "-" && signServerBearer != "" {
headers["Authorization"] = "Bearer " + signServerBearer
}
req := download.Request{
Method: http.MethodGet,
Header: headers,
URL: signServer + "custom_energy" + fmt.Sprintf("?data=%v&salt=%v&uin=%v&android_id=%v&guid=%v",
id, hex.EncodeToString(salt), uin, utils.B2S(device.AndroidId), hex.EncodeToString(device.Guid)),
}.WithTimeout(time.Duration(base.SignServerTimeout) * time.Second)
if base.IsBelow110 {
req.URL = signServer + "custom_energy" + fmt.Sprintf("?data=%v&salt=%v", id, hex.EncodeToString(salt))
}
response, err := req.Bytes()
if err != nil {
log.Warnf("获取T544 sign时出现错误: %v. server: %v", err, signServer)
return nil, err
}
data, err := hex.DecodeString(gjson.GetBytes(response, "data").String())
if err != nil {
log.Warnf("获取T544 sign时出现错误: %v", err)
return nil, err
}
if len(data) == 0 {
log.Warnf("获取T544 sign时出现错误: %v.", "data is empty")
return nil, errors.New("data is empty")
}
return data, nil
}

// signSubmit 提交的操作类型
func signSubmit(uin string, cmd string, callbackID int64, buffer []byte, t string) {
signServer := base.SignServer
if !strings.HasSuffix(signServer, "/") {
signServer += "/"
}
buffStr := hex.EncodeToString(buffer)
tail := 64
endl := "..."
if len(buffStr) < tail {
tail = len(buffStr)
endl = "."
}
log.Infof("submit %v: uin=%v, cmd=%v, callbackID=%v, buffer=%v%s", t, uin, cmd, callbackID, buffer[:tail], endl)
_, err := download.Request{
Method: http.MethodGet,
URL: signServer + "submit" + fmt.Sprintf("?uin=%v&cmd=%v&callback_id=%v&buffer=%v",
uin, cmd, callbackID, buffStr),
}.WithTimeout(time.Duration(base.SignServerTimeout) * time.Second).Bytes()
if err != nil {
log.Warnf("提交 callback 时出现错误: %v. server: %v", err, signServer)
}
}

// signCallback request token 和签名的回调
func signCallback(uin string, results []gjson.Result, t string) {
for _, result := range results {
cmd := result.Get("cmd").String()
callbackID := result.Get("callbackId").Int()
body, _ := hex.DecodeString(result.Get("body").String())
ret, err := cli.SendSsoPacket(cmd, body)
if err != nil {
log.Warnf("callback error: %v", err)
}
signSubmit(uin, cmd, callbackID, ret, t)
}
}

func signRequset(seq uint64, uin string, cmd string, qua string, buff []byte) (sign []byte, extra []byte, token []byte, err error) {
signServer := base.SignServer
if !strings.HasSuffix(signServer, "/") {
signServer += "/"
}
headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded"}
signServerBearer := base.SignServerBearer
if signServerBearer != "-" && signServerBearer != "" {
headers["Authorization"] = "Bearer " + signServerBearer
}
response, err := download.Request{
Method: http.MethodPost,
URL: signServer + "sign",
Header: headers,
Body: bytes.NewReader([]byte(fmt.Sprintf("uin=%v&qua=%s&cmd=%s&seq=%v&buffer=%v&android_id=%v&guid=%v",
uin, qua, cmd, seq, hex.EncodeToString(buff), utils.B2S(device.AndroidId), hex.EncodeToString(device.Guid)))),
}.WithTimeout(time.Duration(base.SignServerTimeout) * time.Second).Bytes()
if err != nil {
return nil, nil, nil, err
}
sign, _ = hex.DecodeString(gjson.GetBytes(response, "data.sign").String())
extra, _ = hex.DecodeString(gjson.GetBytes(response, "data.extra").String())
token, _ = hex.DecodeString(gjson.GetBytes(response, "data.token").String())
if !base.IsBelow110 {
go signCallback(uin, gjson.GetBytes(response, "data.requestCallback").Array(), "sign")
}
return sign, extra, token, nil
}

var registerLock sync.Mutex

func signRegister(uin int64, androidID, guid []byte, qimei36, key string) {
if base.IsBelow110 {
log.Warn("签名服务器版本低于1.1.0, 跳过实例注册")
return
}
signServer := base.SignServer
if !strings.HasSuffix(signServer, "/") {
signServer += "/"
}
resp, err := download.Request{
Method: http.MethodGet,
URL: signServer + "register" + fmt.Sprintf("?uin=%v&android_id=%v&guid=%v&qimei36=%v&key=%s",
uin, utils.B2S(androidID), hex.EncodeToString(guid), qimei36, key),
}.WithTimeout(time.Duration(base.SignServerTimeout) * time.Second).Bytes()
if err != nil {
log.Warnf("注册QQ实例时出现错误: %v. server: %v", err, signServer)
return
}
msg := gjson.GetBytes(resp, "msg")
if gjson.GetBytes(resp, "code").Int() != 0 {
log.Warnf("注册QQ实例时出现错误: %v. server: %v", msg, signServer)
return
}
log.Infof("注册QQ实例 %v 成功: %v", uin, msg)
}

func signRefreshToken(uin string) error {
signServer := base.SignServer
if !strings.HasSuffix(signServer, "/") {
signServer += "/"
}
log.Info("正在刷新 token")
resp, err := download.Request{
Method: http.MethodGet,
URL: signServer + "request_token?uin=" + uin,
}.WithTimeout(time.Duration(base.SignServerTimeout) * time.Second).Bytes()
if err != nil {
return err
}
msg := gjson.GetBytes(resp, "msg")
code := gjson.GetBytes(resp, "code")
if code.Int() != 0 {
return errors.New("code=" + code.String() + ", msg: " + msg.String())
}
go signCallback(uin, gjson.GetBytes(resp, "data").Array(), "request token")
return nil
}

var missTokenCount = uint64(0)

func sign(seq uint64, uin string, cmd string, qua string, buff []byte) (sign []byte, extra []byte, token []byte, err error) {
i := 0
for {
sign, extra, token, err = signRequset(seq, uin, cmd, qua, buff)
if err != nil {
log.Warnf("获取sso sign时出现错误: %v. server: %v", err, base.SignServer)
}
if i > 0 {
break
}
i++
if (!base.IsBelow110) && base.Account.AutoRegister && err == nil && len(sign) == 0 {
if registerLock.TryLock() { // 避免并发时多处同时销毁并重新注册
log.Warn("获取签名为空,实例可能丢失,正在尝试重新注册")
defer registerLock.Unlock()
err := signServerDestroy(uin)
if err != nil {
log.Warnln(err)
return nil, nil, nil, err
}
signRegister(base.Account.Uin, device.AndroidId, device.Guid, device.QImei36, base.Key)
}
continue
}
if (!base.IsBelow110) && base.Account.AutoRefreshToken && len(token) == 0 {
log.Warnf("token 已过期, 总丢失 token 次数为 %v", atomic.AddUint64(&missTokenCount, 1))
if registerLock.TryLock() {
defer registerLock.Unlock()
if err := signRefreshToken(uin); err != nil {
log.Warnf("刷新 token 出现错误: %v. server: %v", err, base.SignServer)
} else {
log.Info("刷新 token 成功")
}
}
continue
}
break
}
return sign, extra, token, err
}

func signServerDestroy(uin string) error {
signServer := base.SignServer
if !strings.HasSuffix(signServer, "/") {
signServer += "/"
}
signVersion, err := signVersion()
if err != nil {
return errors.Wrapf(err, "获取签名服务版本出现错误, server: %v", signServer)
}
if global.VersionNameCompare("v"+signVersion, "v1.1.6") {
return errors.Errorf("当前签名服务器版本 %v 低于 1.1.6,无法使用 destroy 接口", signVersion)
}
resp, err := download.Request{
Method: http.MethodGet,
URL: signServer + "destroy" + fmt.Sprintf("?uin=%v&key=%v", uin, base.Key),
}.WithTimeout(time.Duration(base.SignServerTimeout) * time.Second).Bytes()
if err != nil || gjson.GetBytes(resp, "code").Int() != 0 {
return errors.Wrapf(err, "destroy 实例出现错误, server: %v", signServer)
}
return nil
}

func signVersion() (version string, err error) {
signServer := base.SignServer
resp, err := download.Request{
Method: http.MethodGet,
URL: signServer,
}.WithTimeout(time.Duration(base.SignServerTimeout) * time.Second).Bytes()
if err != nil {
return "", err
}
if gjson.GetBytes(resp, "code").Int() == 0 {
return gjson.GetBytes(resp, "data.version").String(), nil
}
return "", errors.New("empty version")
}

// 定时刷新 token, interval 为间隔时间(分钟)
func signStartRefreshToken(interval int64) {
if interval <= 0 {
log.Warn("定时刷新 token 已关闭")
return
}
log.Infof("每 %v 分钟将刷新一次签名 token", interval)
if interval < 10 {
log.Warnf("间隔时间 %v 分钟较短,推荐 30~40 分钟", interval)
}
if interval > 60 {
log.Warn("间隔时间不能超过 60 分钟,已自动设置为 60 分钟")
interval = 60
}
t := time.NewTicker(time.Duration(interval) * time.Minute)
qqstr := strconv.FormatInt(base.Account.Uin, 10)
defer t.Stop()
for range t.C {
err := signRefreshToken(qqstr)
if err != nil {
log.Warnf("刷新 token 出现错误: %v. server: %v", err, base.SignServer)
}
}
}

func signWaitServer() bool {
t := time.NewTicker(time.Second * 5)
defer t.Stop()
i := 0
for range t.C {
if i > 3 {
return false
}
i++
u, err := url.Parse(base.SignServer)
if err != nil {
log.Warnf("连接到签名服务器出现错误: %v", err)
continue
}
host := u.Hostname()
port := u.Port()
if port == "" {
switch u.Scheme {
case "https":
port = "443"
case "http":
port = "80"
}
}
hostPort := net.JoinHostPort(host, port)
r := utils.RunTCPPingLoop(hostPort, 4)
if r.PacketsLoss > 0 {
log.Warnf("连接到签名服务器出现错误: 丢包%d/%d 时延%dms", r.PacketsLoss, r.PacketsSent, r.AvgTimeMill)
continue
}
break
}
log.Infof("连接至签名服务器: %s", base.SignServer)
return true
}
}
24 changes: 10 additions & 14 deletions cmd/gocq/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,32 +163,28 @@ func LoginInteract() {
log.Fatalf("加载设备信息失败: %v", err)
}
}

if base.SignServer != "-" && base.SignServer != "" {
log.Infof("使用服务器 %s 进行数据包签名", base.SignServer)
if base.SignServerBearer != "-" && base.SignServerBearer != "" {
log.Infof("使用 Bearer %s 认证签名服务器 %s ", base.SignServerBearer, base.SignServer)
}
// 等待签名服务器直到连接成功
if !signWaitServer() {
log.Fatalf("连接签名服务器失败")
}
signRegister(base.Account.Uin, device.AndroidId, device.Guid, device.QImei36, base.Key)
initSignServersConfig()
signServer, err := getAvaliableSignServer() // 获取可用签名服务器
if err != nil {
log.Warn(err)
}
if len(signServer.URL) > 1 {
log.Infof("使用签名服务器:%v", signServer.URL)
go signStartRefreshToken(base.Account.RefreshInterval) // 定时刷新 token
wrapper.DandelionEnergy = energy
wrapper.FekitGetSign = sign
if !base.IsBelow110 {
if !base.Account.AutoRegister {
log.Warn("自动注册实例已关闭,若未配置 sign-server 端自动注册实例则实例丢失时需要重启 go-cqhttp 以正常签名")
log.Warn("自动注册实例已关闭,请配置 sign-server 端自动注册实例以保持正常签名")
}
if !base.Account.AutoRefreshToken {
log.Warn("自动刷新 token 已关闭,token 过期后获取签名时将不会立即尝试刷新获取新 token")
log.Info("自动刷新 token 已关闭,token 过期后获取签名时将不会立即尝试刷新获取新 token")
}
} else {
log.Warn("签名服务器版本 <= 1.1.0 ,无法使用刷新 token 等操作,建议使用 1.1.6 版本及以上签名服务器")
}
} else {
log.Warnf("警告: 未配置签名服务器, 这可能会导致登录 45 错误码或发送消息被风控")
log.Warnf("警告: 未配置签名服务器或签名服务器不可用, 这可能会导致登录 45 错误码或发送消息被风控")
}

if base.Account.Encrypt {
Expand Down
Loading

0 comments on commit f8354ec

Please sign in to comment.