From f466ca7a72bde7b20b96b60477a8602990fffbd3 Mon Sep 17 00:00:00 2001 From: Lumine <66518048+1umine@users.noreply.github.com> Date: Tue, 1 Aug 2023 10:33:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8F=90=E4=BE=9B=201.1.6=20=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E4=BB=A5=E4=B8=8A=20qsign=20=E7=9A=84=E5=AF=B9?= =?UTF-8?q?=E6=8E=A5=E6=94=AF=E6=8C=81=20(#2307)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 增加签名服务超时设置 * 获取签名和err为空时尝试重新注册实例 * 可配置自动刷新token以及自动注册 * fix lint * wrap callback * add config: refresh-interval * support qsign's `auto-register` * fix: add registerLock to avoid repeat registraion. * update: Enable disabling auto token refresh * fix: use string android_id (not hexadecimal * update default_config.yml * fix: compatible with older qsign (bellow 1.1.0 * fix: refresh token * update dependency * update go.sum * fix: fix warnings on old version sign server --------- Co-authored-by: 源文雨 <41315874+fumiama@users.noreply.github.com> --- cmd/gocq/login.go | 172 +++++++++++++++++++++++++++++- cmd/gocq/main.go | 15 ++- go.mod | 2 +- go.sum | 4 +- modules/config/config.go | 3 + modules/config/default_config.yml | 13 ++- 6 files changed, 202 insertions(+), 7 deletions(-) diff --git a/cmd/gocq/login.go b/cmd/gocq/login.go index 51d9626..5828b03 100644 --- a/cmd/gocq/login.go +++ b/cmd/gocq/login.go @@ -10,7 +10,9 @@ import ( "net/http" "net/url" "os" + "strconv" "strings" + "sync" "time" "github.com/Mrs4s/MiraiGo/client" @@ -296,7 +298,40 @@ func energy(uin uint64, id string, _ string, salt []byte) ([]byte, error) { return data, nil } -func sign(seq uint64, uin string, cmd string, qua string, buff []byte) (sign []byte, extra []byte, token []byte, err error) { +// t: 提交的操作类型 +func submit(uin string, cmd string, callbackID int64, buffer []byte, t string) { + signServer := base.SignServer + if !strings.HasSuffix(signServer, "/") { + signServer += "/" + } + buffStr := hex.EncodeToString(buffer) + log.Infof("submit %v: uin=%v, cmd=%v, callbackID=%v, buffer-end=%v", t, uin, cmd, callbackID, + buffStr[len(buffStr)-10:]) + _, err := download.Request{ + Method: http.MethodGet, + URL: signServer + "submit" + fmt.Sprintf("?uin=%v&cmd=%v&callback_id=%v&buffer=%v", + uin, cmd, callbackID, buffStr), + }.Bytes() + if err != nil { + log.Warnf("提交 callback 时出现错误: %v server: %v", err, signServer) + } +} + +// request token和签名的回调 +func callback(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) + } + submit(uin, cmd, callbackID, ret, t) + } +} + +func _sign(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 += "/" @@ -309,15 +344,19 @@ func sign(seq uint64, uin string, cmd string, qua string, buff []byte) (sign []b uin, qua, cmd, seq, hex.EncodeToString(buff), utils.B2S(device.AndroidId), hex.EncodeToString(device.Guid)))), }.Bytes() if err != nil { - log.Warnf("获取sso sign时出现错误: %v server: %v", err, signServer) 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 callback(uin, gjson.GetBytes(response, "data.requestCallback").Array(), "sign") + } return sign, extra, token, nil } +var registerLock sync.Mutex + func register(uin int64, androidID, guid []byte, qimei36, key string) { if base.IsBelow110 { log.Warn("签名服务器版本低于1.1.0, 跳过实例注册") @@ -344,6 +383,135 @@ func register(uin int64, androidID, guid []byte, qimei36, key string) { log.Infof("注册QQ实例 %v 成功: %v", uin, msg) } +func refreshToken(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" + fmt.Sprintf("?uin=%v", uin), + }.Bytes() + if err != nil { + return err + } + msg := gjson.GetBytes(resp, "msg") + if gjson.GetBytes(resp, "code").Int() != 0 { + return errors.New(msg) + } + go callback(uin, gjson.GetBytes(resp, "data").Array(), "request token") + return true +} + +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 = _sign(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 := destroySignServer(uin) + if err != nil { + log.Warnf(err) + return nil, nil, nil, err + } + register(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 := refreshToken(uin); err != nil { + log.Warnf("刷新 token 出现错误: %v server: %v", err, base.SignServer) + } else { + log.Info("刷新 token 成功") + } + } + continue + } + break + } + return sign, extra, token, err +} + +func destroySignServer(uin string) error { + signServer := base.SignServer + if !strings.HasSuffix(signServer, "/") { + signServer += "/" + } + signVersion, err := getSignServerVersion() + 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), + }.Bytes() + if err != nil { + return errors.Wrapf(err, "destroy 实例出现错误, server: %v", signServer) + } + msg := gjson.GetBytes(resp, "msg") + if gjson.GetBytes(resp, "code").Int() != 0 { + return errors.Wrapf(err, "destroy 实例出现错误, server: %v", signServer) + } + return nil +} + +func getSignServerVersion() (version string, err error) { + signServer := base.SignServer + resp, err := download.Request{ + Method: http.MethodGet, + URL: signServer, + }.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 startRefreshTokenTask(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) + defer t.Stop() + for range t.C { + err := refreshToken(strconv.FormatInt(base.Account.Uin, 10)) + if err != nil { + log.Warnf("刷新 token 出现错误: %v server: %v", err, base.SignServer) + } + } +} + func waitSignServer() bool { t := time.NewTicker(time.Second*5) defer t.Stop() diff --git a/cmd/gocq/main.go b/cmd/gocq/main.go index 606fae8..281ae25 100644 --- a/cmd/gocq/main.go +++ b/cmd/gocq/main.go @@ -166,13 +166,25 @@ func LoginInteract() { if base.SignServer != "-" && base.SignServer != "" { log.Infof("使用服务器 %s 进行数据包签名", base.SignServer) + download.SetTimeout(time.Duration(base.HTTPTimeout) * time.Second) // 设置签名超时时间 // 等待签名服务器直到连接成功 if !waitSignServer() { log.Fatalf("连接签名服务器失败") } register(base.Account.Uin, device.AndroidId, device.Guid, device.QImei36, base.Key) + go startRefreshTokenTask(base.Account.RefreshInterval) // 定时刷新 token wrapper.DandelionEnergy = energy wrapper.FekitGetSign = sign + if !base.IsBelow110 { + if !base.Account.AutoRegister { + log.Warn("自动注册实例已关闭,若未配置 sign-server 端自动注册实例则实例丢失时需要重启 go-cqhttp 以正常签名") + } + if !base.Account.AutoRefreshToken { + log.Warn("自动刷新 token 已关闭,token 过期后获取签名时将不会立即尝试刷新获取新 token") + } + } else { + log.Warn("签名服务器版本 <= 1.1.0 ,无法使用刷新 token 等操作,建议使用 1.1.6 版本及以上签名服务器") + } } else { log.Warnf("警告: 未配置签名服务器, 这可能会导致登录 45 错误码或发送消息被风控") } @@ -294,6 +306,7 @@ func LoginInteract() { } if !base.FastStart { log.Infof("正在检查协议更新...") + download.SetTimeout(time.Second * 5) // 防止协议更新堵塞过久 currentVersionName := device.Protocol.Version().SortVersionName remoteVersion, err := getRemoteLatestProtocolVersion(int(device.Protocol.Version().Protocol)) if err == nil { @@ -378,7 +391,7 @@ func LoginInteract() { }) saveToken() cli.AllowSlider = true - download.SetTimeout(time.Duration(base.HTTPTimeout) * time.Second) // 在登录完成后设置, 防止在堵塞协议更新 + download.SetTimeout(time.Duration(base.HTTPTimeout) * time.Second) // 登陆完成进行最终超时设置 log.Infof("登录成功 欢迎使用: %v", cli.Nickname) log.Info("开始加载好友列表...") global.Check(cli.ReloadFriendList(), true) diff --git a/go.mod b/go.mod index ffe42a1..8ea6e36 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.20 require ( github.com/FloatTech/sqlite v1.5.7 github.com/Microsoft/go-winio v0.6.0 - github.com/Mrs4s/MiraiGo v0.0.0-20230627090859-19e3d172596e + github.com/Mrs4s/MiraiGo v0.0.0-20230730133947-d344e0f318ab github.com/RomiChan/syncx v0.0.0-20221202055724-5f842c53020e github.com/RomiChan/websocket v1.4.3-0.20220123145318-307a86b127bc github.com/fumiama/go-base16384 v1.6.1 diff --git a/go.sum b/go.sum index d251a11..61e86ae 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/FloatTech/ttl v0.0.0-20220715042055-15612be72f5b h1:tvciXWq2nuvTbFeJG github.com/FloatTech/ttl v0.0.0-20220715042055-15612be72f5b/go.mod h1:fHZFWGquNXuHttu9dUYoKuNbm3dzLETnIOnm1muSfDs= github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= -github.com/Mrs4s/MiraiGo v0.0.0-20230627090859-19e3d172596e h1:99itMjI//+KaFF0+0QCBg/uHhGMJ99jG2lP6z/UnOsU= -github.com/Mrs4s/MiraiGo v0.0.0-20230627090859-19e3d172596e/go.mod h1:mU3fBFU+7eO0kaGes7YRKtzIDtwIU84nSSwTV7NK2b0= +github.com/Mrs4s/MiraiGo v0.0.0-20230730133947-d344e0f318ab h1:SLciJTlC5YiG3qqvGJf4sHJDHDXUdH+v4rjqVhE5SIQ= +github.com/Mrs4s/MiraiGo v0.0.0-20230730133947-d344e0f318ab/go.mod h1:mU3fBFU+7eO0kaGes7YRKtzIDtwIU84nSSwTV7NK2b0= github.com/RomiChan/protobuf v0.1.1-0.20230204044148-2ed269a2e54d h1:/Xuj3fIiMY2ls1TwvPKmaqQrtJsPY+c9s+0lOScVHd8= github.com/RomiChan/protobuf v0.1.1-0.20230204044148-2ed269a2e54d/go.mod h1:2Ie+hdBFQpQFDHfeklgxoFmQRCE7O+KwFpISeXq7OwA= github.com/RomiChan/syncx v0.0.0-20221202055724-5f842c53020e h1:wR3MXQ3VbUlPKOOUwLOYgh/QaJThBTYtsl673O3lqSA= diff --git a/modules/config/config.go b/modules/config/config.go index cf8143f..f424054 100644 --- a/modules/config/config.go +++ b/modules/config/config.go @@ -38,6 +38,9 @@ type Account struct { SignServer string `yaml:"sign-server"` Key string `yaml:"key"` IsBelow110 bool `yaml:"is-below-110"` + AutoRegister bool `yaml:"auto-register"` + AutoRefreshToken bool `yaml:"auto-refresh-token"` + RefreshInterval int64 `yaml:"refresh-interval"` } // Config 总配置文件 diff --git a/modules/config/default_config.yml b/modules/config/default_config.yml index 0d62c53..df2244b 100644 --- a/modules/config/default_config.yml +++ b/modules/config/default_config.yml @@ -29,6 +29,17 @@ account: # 账号相关 # 签名服务器所需要的apikey, 如果签名服务器的版本在1.1.0及以下则此项无效 # 本地部署的默认为114514 key: '114514' + # 在实例可能丢失(获取到的签名为空)时是否尝试重新注册 + # 为 true 时,在签名服务不可用时可能每次发消息都会尝试重新注册并签名。 + # 为 false 时,将不会自动注册实例,在签名服务器重启或实例被销毁后需要重启 go-cqhttp 以获取实例 + # 否则后续消息将不会正常签名。关闭此项后可以考虑开启签名服务器端 auto_register 避免需要重启 + auto-register: false + # 是否在 token 过期后立即自动刷新签名 token(在需要签名时才会检测到,主要防止 token 意外丢失) + # 独立于定时刷新 + auto-refresh-token: false + # 定时刷新 token 间隔时间,单位为分钟, 建议 30~40 分钟, 不可超过 60 分钟 + # 目前丢失token也不会有太大影响,可设置为 0 以关闭,推荐开启 + refresh-interval: 40 heartbeat: # 心跳频率, 单位秒 @@ -59,7 +70,7 @@ message: skip-mime-scan: false # 是否自动转换 WebP 图片 convert-webp-image: false - # http超时时间 + # http超时时间,同时作为签名服务超时时间 http-timeout: 0 output: