diff --git a/cmd/gocq/login.go b/cmd/gocq/login.go index 8087cbeeb..4114c696f 100644 --- a/cmd/gocq/login.go +++ b/cmd/gocq/login.go @@ -3,18 +3,11 @@ 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" @@ -22,11 +15,9 @@ import ( "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" ) @@ -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 -} +} \ No newline at end of file diff --git a/cmd/gocq/main.go b/cmd/gocq/main.go index 4e71431e8..d0cf1738d 100644 --- a/cmd/gocq/main.go +++ b/cmd/gocq/main.go @@ -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 { diff --git a/cmd/gocq/qsign.go b/cmd/gocq/qsign.go new file mode 100644 index 000000000..082217e86 --- /dev/null +++ b/cmd/gocq/qsign.go @@ -0,0 +1,409 @@ +package gocq + +import ( + "bytes" + "encoding/hex" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "github.com/tidwall/gjson" + + "github.com/Mrs4s/MiraiGo/utils" + + "github.com/Mrs4s/go-cqhttp/global" + "github.com/Mrs4s/go-cqhttp/internal/base" + "github.com/Mrs4s/go-cqhttp/internal/download" + "github.com/Mrs4s/go-cqhttp/modules/config" +) + +type currentSignServer struct { + server *config.SignServer + ok bool + lock sync.RWMutex +} + +func (c *currentSignServer) getServer() *config.SignServer { + c.lock.RLock() + defer c.lock.RUnlock() + return c.server +} + +func (c *currentSignServer) isOK() bool { + c.lock.RLock() + defer c.lock.RUnlock() + return c.ok +} + +func (c *currentSignServer) setServer(server *config.SignServer, status bool) { + c.lock.Lock() + defer c.lock.Unlock() + if server != nil { + c.server = server + } + c.ok = status +} + +// 当前签名服务器 +var curSignServer *currentSignServer +var errorCount = uintptr(0) +var checkLock sync.Mutex + +func initSignServersConfig() { + if len(base.SignServers) == 0 { + log.Warn("no configured sign-server") + return + } + if len(base.SignServers) > 5 { + base.SignServers = base.SignServers[:5] + log.Warn("签名服务器数量配置过多,可能导致不可用时检查时间过久,已取前 5 个") + } + curSignServer = ¤tSignServer{ + server: &base.SignServers[0], + ok: true, + } +} + +// getAvaliableSignServer 获取可用的签名服务器,没有则返回空和相应错误 +func getAvaliableSignServer() (config.SignServer, error) { + if curSignServer.isOK() { + return *curSignServer.getServer(), nil + } + maxCount := base.Account.MaxCheckCount + if maxCount == 0 && atomic.LoadUintptr(&errorCount) >= 3 { + log.Warn("已连续 3 次获取不到可用签名服务器,将固定使用主签名服务器") + curSignServer.setServer(&base.SignServers[0], true) + return *curSignServer.getServer(), nil + } + if maxCount > 0 && int(atomic.LoadUintptr(&errorCount)) >= maxCount { + log.Fatalf("获取可用签名服务器失败次数超过 %v 次, 正在离线", maxCount) + } + if checkLock.TryLock() { + defer checkLock.Unlock() + cs := curSignServer.getServer() + if len(cs.URL) > 1 { + log.Warnf("当前签名服务器 %v 不可用,正在查找可用服务器", cs.URL) + } + return syncCheckServer(base.SignServers) + } + return config.SignServer{}, errors.New("checking sign-servers") +} + +func isServerAvaliable(signServer string) bool { + resp, err := download.Request{ + Method: http.MethodGet, + URL: signServer, + }.WithTimeout(3 * time.Second).Bytes() + if err == nil && gjson.GetBytes(resp, "code").Int() == 0 { + return true + } + log.Warnf("签名服务器 %v 可能不可用,请求出现错误:%v", signServer, err) + return false +} + +// syncCheckServer 按同步顺序检查所有签名服务器直到找到可用的 +func syncCheckServer(servers []config.SignServer) (config.SignServer, error) { + for i, server := range servers { + log.Infof("检查签名服务器:%v (%v/%v)", server.URL, i+1, len(servers)) + if len(server.URL) < 4 { + continue + } + if isServerAvaliable(server.URL) { + atomic.StoreUintptr(&errorCount, 0) + curSignServer.setServer(&server, true) + log.Infof("使用签名服务器 url=%v, key=%v, auth=%v", server.URL, server.Key, server.Authorization) + if base.Account.AutoRegister { + // 若配置了自动注册实例则在切换后注册实例,否则不需要注册,签名时由qsign自动注册 + signRegister(base.Account.Uin, device.AndroidId, device.Guid, device.QImei36, server.Key) + } + return server, nil + } + } + return config.SignServer{}, errors.New("no avaliable sign-server") +} + +/* +请求签名服务器 + + url: api + params 组合的字符串,无须包含签名服务器地址 + return: signServer, response, error +*/ +func requestSignServer(method string, url string, headers map[string]string, body io.Reader) (string, []byte, error) { + signServer, e := getAvaliableSignServer() + if e != nil && len(signServer.URL) <= 1 { // 没有可用的 + log.Warnf("获取可用签名服务器出错:%v, 将使用主签名服务器进行签名", e) + atomic.AddUintptr(&errorCount, 1) + signServer = base.SignServers[0] // 没有获取到时使用第一个 + } + if !strings.HasPrefix(url, signServer.URL) { + url = strings.TrimSuffix(signServer.URL, "/") + "/" + strings.TrimPrefix(url, "/") + } + if headers == nil { + headers = map[string]string{} + } + auth := signServer.Authorization + if auth != "-" && auth != "" { + headers["Authorization"] = auth + } + req := download.Request{ + Method: method, + Header: headers, + URL: url, + Body: body, + }.WithTimeout(time.Duration(base.SignServerTimeout) * time.Second) + resp, err := req.Bytes() + if err != nil { + curSignServer.setServer(nil, false) // 标记为不可用 + } + return signServer.URL, resp, err +} + +func energy(uin uint64, id string, _ string, salt []byte) ([]byte, error) { + url := "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)) + if base.IsBelow110 { + url = "custom_energy" + fmt.Sprintf("?data=%v&salt=%v", id, hex.EncodeToString(salt)) + } + signServer, response, err := requestSignServer(http.MethodGet, url, nil, nil) + 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 +// 提交回调 buffer +func signSubmit(uin string, cmd string, callbackID int64, buffer []byte, t string) { + 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, buffStr[:tail], endl) + + signServer, _, err := requestSignServer( + http.MethodGet, + "submit"+fmt.Sprintf("?uin=%v&cmd=%v&callback_id=%v&buffer=%v", + uin, cmd, callbackID, buffStr), + nil, nil, + ) + if err != nil { + log.Warnf("提交 callback 时出现错误: %v. server: %v", err, signServer) + } +} + +// signCallback +// 刷新 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 || len(ret) == 0 { + log.Warnf("Callback error: %v, Or response data is empty", err) + continue // 发送 SsoPacket 出错或返回数据为空时跳过 + } + 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) { + headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded"} + _, response, err := requestSignServer( + http.MethodPost, + "sign", + headers, + 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)))), + ) + 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, resp, err := requestSignServer( + http.MethodGet, + "register"+fmt.Sprintf("?uin=%v&android_id=%v&guid=%v&qimei36=%v&key=%s", + uin, utils.B2S(androidID), hex.EncodeToString(guid), qimei36, key), + nil, nil, + ) + 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 { + log.Info("正在刷新 token") + _, resp, err := requestSignServer( + http.MethodGet, + "request_token?uin="+uin, + nil, nil, + ) + 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) +var lastToken = "" + +func sign(seq uint64, uin string, cmd string, qua string, buff []byte) (sign []byte, extra []byte, token []byte, err error) { + i := 0 + for { + cs := curSignServer.getServer() + sign, extra, token, err = signRequset(seq, uin, cmd, qua, buff) + if err != nil { + log.Warnf("获取sso sign时出现错误: %v. server: %v", err, cs.URL) + } + if i > 0 { + break + } + i++ + if (!base.IsBelow110) && base.Account.AutoRegister && err == nil && len(sign) == 0 { + if registerLock.TryLock() { // 避免并发时多处同时销毁并重新注册 + log.Debugf("请求签名:cmd=%v, qua=%v, buff=%v", seq, cmd, hex.EncodeToString(buff)) + log.Debugf("返回结果:sign=%v, extra=%v, token=%v", + hex.EncodeToString(sign), hex.EncodeToString(extra), hex.EncodeToString(token)) + log.Warn("获取签名为空,实例可能丢失,正在尝试重新注册") + defer registerLock.Unlock() + err := signServerDestroy(uin) + if err != nil { + log.Warnln(err) // 实例真的丢失时则必出错,或许应该不 return , 以重新获取本次签名 + // return nil, nil, nil, err + } + signRegister(base.Account.Uin, device.AndroidId, device.Guid, device.QImei36, cs.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, cs.URL) + } else { + log.Info("刷新 token 成功") + } + } + continue + } + break + } + if tokenString := hex.EncodeToString(token); lastToken != tokenString { + log.Infof("token 已更新:%v -> %v", lastToken, tokenString) + lastToken = tokenString + } + rule := base.Account.RuleChangeSignServer + if (len(sign) == 0 && rule >= 1) || (len(token) == 0 && rule >= 2) { + curSignServer.setServer(nil, false) + } + return sign, extra, token, err +} + +func signServerDestroy(uin string) error { + 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) + } + signServer, resp, err := requestSignServer( + http.MethodGet, + "destroy"+fmt.Sprintf("?uin=%v&key=%v", uin, curSignServer.getServer().Key), + nil, nil, + ) + if err != nil || gjson.GetBytes(resp, "code").Int() != 0 { + return errors.Wrapf(err, "destroy 实例出现错误, server: %v", signServer) + } + return nil +} + +func signVersion() (signServer string, version string, err error) { + signServer, resp, err := requestSignServer(http.MethodGet, "", nil, nil) + if err != nil { + return signServer, "", err + } + if gjson.GetBytes(resp, "code").Int() == 0 { + return signServer, gjson.GetBytes(resp, "data.version").String(), nil + } + return signServer, "", 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 { + cs, master := curSignServer.getServer(), base.SignServers[0] + if cs.URL != master.URL && isServerAvaliable(master.URL) { + curSignServer.setServer(&master, true) + log.Infof("主签名服务器可用,已切换至主签名服务器 %v", cs.URL) + } + err := signRefreshToken(qqstr) + if err != nil { + log.Warnf("刷新 token 出现错误: %v. server: %v", err, cs.URL) + } + } +} diff --git a/coolq/api.go b/coolq/api.go index 5307919fd..7c9e47d64 100644 --- a/coolq/api.go +++ b/coolq/api.go @@ -1388,33 +1388,42 @@ func (bot *CQBot) CQGetGroupHonorInfo(groupID int64, t string) global.MSG { } } msg["talkative_list"] = convertMem(honor.TalkativeList) + } else { + log.Infof("获取群龙王出错:%v", err) } } if t == "performer" || t == "all" { if honor, err := bot.Client.GetGroupHonorInfo(groupID, client.Performer); err == nil { msg["performer_list"] = convertMem(honor.ActorList) + } else { + log.Infof("获取群聊之火出错:%v", err) } } if t == "legend" || t == "all" { if honor, err := bot.Client.GetGroupHonorInfo(groupID, client.Legend); err == nil { msg["legend_list"] = convertMem(honor.LegendList) + } else { + log.Infof("获取群聊炽焰出错:%v", err) } } if t == "strong_newbie" || t == "all" { if honor, err := bot.Client.GetGroupHonorInfo(groupID, client.StrongNewbie); err == nil { msg["strong_newbie_list"] = convertMem(honor.StrongNewbieList) + } else { + log.Infof("获取冒尖小春笋出错:%v", err) } } if t == "emotion" || t == "all" { if honor, err := bot.Client.GetGroupHonorInfo(groupID, client.Emotion); err == nil { msg["emotion_list"] = convertMem(honor.EmotionList) + } else { + log.Infof("获取快乐之源出错:%v", err) } } - return OK(msg) } diff --git a/go.mod b/go.mod index e2760cf56..95c58d59f 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.20 require ( github.com/FloatTech/sqlite v1.6.3 github.com/Microsoft/go-winio v0.6.2-0.20230724192519-b29bbd58a65a - github.com/Mrs4s/MiraiGo v0.0.0-20230801023408-b4cd7e8f2149 + github.com/Mrs4s/MiraiGo v0.0.0-20230823050531-a8213e127b2b github.com/RomiChan/syncx v0.0.0-20221202055724-5f842c53020e github.com/RomiChan/websocket v1.4.3-0.20220227141055-9b2c6168c9c5 github.com/fumiama/go-base16384 v1.7.0 diff --git a/go.sum b/go.sum index 0f9f21907..3d4873550 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/Microsoft/go-winio v0.6.2-0.20230724192519-b29bbd58a65a h1:aU1703IHxu github.com/Microsoft/go-winio v0.6.2-0.20230724192519-b29bbd58a65a/go.mod h1:OZqLNXdYJHmx7aqq/T6wAdFEdoGm5nmIfC4kU7M8P8o= github.com/Mrs4s/MiraiGo v0.0.0-20230801023408-b4cd7e8f2149 h1:q9w4m+ps0gTyUHLObX6avawN1Rfn0GQwbmEKCZ6WrBo= github.com/Mrs4s/MiraiGo v0.0.0-20230801023408-b4cd7e8f2149/go.mod h1:mU3fBFU+7eO0kaGes7YRKtzIDtwIU84nSSwTV7NK2b0= +github.com/Mrs4s/MiraiGo v0.0.0-20230823050531-a8213e127b2b h1:0GG6kDFgzie0HNdlkrgPwyX4WqUjckTP1xTM4cYaC2g= +github.com/Mrs4s/MiraiGo v0.0.0-20230823050531-a8213e127b2b/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= @@ -173,6 +175,7 @@ golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8= golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.ilharper.com/x/isatty v1.1.1 h1:RAg32Pxq/nIK4AVtdm9RBqxsxZZX1uRKRSS21E5SHMk= gopkg.ilharper.com/x/isatty v1.1.1/go.mod h1:ofpv77Td5qQO6R1dmDd3oNt8TZdRo+l5gYAMxopRyS0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/base/flag.go b/internal/base/flag.go index 8e0c37ac6..1bb5d4bf2 100644 --- a/internal/base/flag.go +++ b/internal/base/flag.go @@ -23,27 +23,25 @@ var ( // config file flags var ( - Debug bool // 是否开启 debug 模式 - RemoveReplyAt bool // 是否删除reply后的at - ExtraReplyData bool // 是否上报额外reply信息 - IgnoreInvalidCQCode bool // 是否忽略无效CQ码 - SplitURL bool // 是否分割URL - ForceFragmented bool // 是否启用强制分片 - SkipMimeScan bool // 是否跳过Mime扫描 - ConvertWebpImage bool // 是否转换Webp图片 - ReportSelfMessage bool // 是否上报自身消息 - UseSSOAddress bool // 是否使用服务器下发的新地址进行重连 - LogForceNew bool // 是否在每次启动时强制创建全新的文件储存日志 - LogColorful bool // 是否启用日志颜色 - FastStart bool // 是否为快速启动 - AllowTempSession bool // 是否允许发送临时会话信息 - UpdateProtocol bool // 是否更新协议 - SignServer string // 使用特定的服务器进行签名 - SignServerBearer string // 认证签名服务器的 Bearer Token - Key string // 签名服务器密钥 - IsBelow110 bool // 签名服务器版本是否低于1.1.0及以下 - HTTPTimeout int // download 超时时间 - SignServerTimeout int // 签名服务器超时时间 + Debug bool // 是否开启 debug 模式 + RemoveReplyAt bool // 是否删除reply后的at + ExtraReplyData bool // 是否上报额外reply信息 + IgnoreInvalidCQCode bool // 是否忽略无效CQ码 + SplitURL bool // 是否分割URL + ForceFragmented bool // 是否启用强制分片 + SkipMimeScan bool // 是否跳过Mime扫描 + ConvertWebpImage bool // 是否转换Webp图片 + ReportSelfMessage bool // 是否上报自身消息 + UseSSOAddress bool // 是否使用服务器下发的新地址进行重连 + LogForceNew bool // 是否在每次启动时强制创建全新的文件储存日志 + LogColorful bool // 是否启用日志颜色 + FastStart bool // 是否为快速启动 + AllowTempSession bool // 是否允许发送临时会话信息 + UpdateProtocol bool // 是否更新协议 + SignServers []config.SignServer // 使用特定的服务器进行签名 + IsBelow110 bool // 签名服务器版本是否低于1.1.0及以下 + HTTPTimeout int // download 超时时间 + SignServerTimeout int // 签名服务器超时时间 PostFormat string // 上报格式 string or array Proxy string // 存储 proxy_rewrite,用于设置代理 @@ -92,12 +90,10 @@ func Init() { ReportSelfMessage = conf.Message.ReportSelfMessage UseSSOAddress = conf.Account.UseSSOAddress AllowTempSession = conf.Account.AllowTempSession - SignServer = conf.Account.SignServer - SignServerBearer = conf.Account.SignServerBearer - Key = conf.Account.Key + SignServers = conf.Account.SignServers IsBelow110 = conf.Account.IsBelow110 HTTPTimeout = conf.Message.HTTPTimeout - SignServerTimeout = conf.Message.SignServerTimeout + SignServerTimeout = conf.Account.SignServerTimeout } { // others Proxy = conf.Message.ProxyRewrite diff --git a/internal/download/download.go b/internal/download/download.go index 84f7148af..c130f96e1 100644 --- a/internal/download/download.go +++ b/internal/download/download.go @@ -15,13 +15,15 @@ import ( "sync" "time" + "github.com/RomiChan/syncx" "github.com/pkg/errors" "github.com/tidwall/gjson" "github.com/Mrs4s/go-cqhttp/internal/base" ) -var client = newcli(time.Second * 15) +var client = newClient(time.Second * 15) +var clients syncx.Map[time.Duration, *http.Client] var clienth2 = &http.Client{ Transport: &http.Transport{ @@ -37,7 +39,7 @@ var clienth2 = &http.Client{ Timeout: time.Second * 15, } -func newcli(t time.Duration) *http.Client { +func newClient(t time.Duration) *http.Client { return &http.Client{ Transport: &http.Transport{ Proxy: func(request *http.Request) (*url.URL, error) { @@ -62,7 +64,13 @@ const UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 // WithTimeout get a download instance with timeout t func (r Request) WithTimeout(t time.Duration) *Request { - r.custcli = newcli(t) + if c, ok := clients.Load(t); ok { + r.custcli = c + } else { + c := newClient(t) + clients.Store(t, c) + r.custcli = c + } return &r } diff --git a/modules/config/config.go b/modules/config/config.go index 4bfbc18c7..2224ec1d6 100644 --- a/modules/config/config.go +++ b/modules/config/config.go @@ -28,20 +28,28 @@ type Reconnect struct { // Account 账号配置 type Account struct { - Uin int64 `yaml:"uin"` - Password string `yaml:"password"` - Encrypt bool `yaml:"encrypt"` - Status int `yaml:"status"` - ReLogin *Reconnect `yaml:"relogin"` - UseSSOAddress bool `yaml:"use-sso-address"` - AllowTempSession bool `yaml:"allow-temp-session"` - SignServer string `yaml:"sign-server"` - SignServerBearer string `yaml:"sign-server-bearer"` - 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"` + Uin int64 `yaml:"uin"` + Password string `yaml:"password"` + Encrypt bool `yaml:"encrypt"` + Status int `yaml:"status"` + ReLogin *Reconnect `yaml:"relogin"` + UseSSOAddress bool `yaml:"use-sso-address"` + AllowTempSession bool `yaml:"allow-temp-session"` + SignServers []SignServer `yaml:"sign-servers"` + RuleChangeSignServer int `yaml:"rule-change-sign-server"` + MaxCheckCount int `yaml:"max-check-count"` + SignServerTimeout int `yaml:"sign-server-timeout"` + IsBelow110 bool `yaml:"is-below-110"` + AutoRegister bool `yaml:"auto-register"` + AutoRefreshToken bool `yaml:"auto-refresh-token"` + RefreshInterval int64 `yaml:"refresh-interval"` +} + +// SignServer 签名服务器 +type SignServer struct { + URL string `yaml:"url"` + Key string `yaml:"key"` + Authorization string `yaml:"authorization"` } // Config 总配置文件 @@ -64,7 +72,6 @@ type Config struct { SkipMimeScan bool `yaml:"skip-mime-scan"` ConvertWebpImage bool `yaml:"convert-webp-image"` HTTPTimeout int `yaml:"http-timeout"` - SignServerTimeout int `yaml:"sign-server-timeout"` } `yaml:"message"` Output struct { diff --git a/modules/config/default_config.yml b/modules/config/default_config.yml index 5acb845f4..66c25e6f6 100644 --- a/modules/config/default_config.yml +++ b/modules/config/default_config.yml @@ -16,26 +16,49 @@ account: # 账号相关 # 是否允许发送临时会话消息 allow-temp-session: false - # 数据包的签名服务器 + # 数据包的签名服务器列表,第一个作为主签名服务器,后续作为备用 # 兼容 https://github.com/fuqiuluo/unidbg-fetch-qsign - # 如果遇到 登录 45 错误, 或者发送信息风控的话需要填入一个服务器 + # 如果遇到 登录 45 错误, 或者发送信息风控的话需要填入一个或多个服务器 + # 不建议设置过多,设置主备各一个即可,超过 5 个只会取前五个 # 示例: - # sign-server: 'http://127.0.0.1:8080' # 本地签名服务器 - # sign-server: 'https://signserver.example.com' # 线上签名服务器 + # sign-servers: + # - url: 'http://127.0.0.1:8080' # 本地签名服务器 + # key: "114514" # 相应 key + # authorization: "-" # authorization 内容, 依服务端设置 + # - url: 'https://signserver.example.com' # 线上签名服务器 + # key: "114514" + # authorization: "-" + # ... + # # 服务器可使用docker在本地搭建或者使用他人开放的服务 - sign-server: '-' - # 签名服务器认证 Bearer Token - # 使用开放的服务可能需要提供此 Token 进行认证 - sign-server-bearer: '-' + sign-servers: + - url: '-' # 主签名服务器地址, 必填 + key: '114514' # 签名服务器所需要的apikey, 如果签名服务器的版本在1.1.0及以下则此项无效 + authorization: '-' # authorization 内容, 依服务端设置,如 'Bearer xxxx' + - url: '-' # 备用 + key: '114514' + authorization: '-' + + # 判断签名服务不可用(需要切换)的额外规则 + # 0: 不设置 (此时仅在请求无法返回结果时判定为不可用) + # 1: 在获取到的 sign 为空 (若选此建议关闭 auto-register,一般为实例未注册但是请求签名的情况) + # 2: 在获取到的 sign 或 token 为空(若选此建议关闭 auto-refresh-token ) + rule-change-sign-server: 1 + + # 连续寻找可用签名服务器最大尝试次数 + # 为 0 时会在连续 3 次没有找到可用签名服务器后保持使用主签名服务器,不再尝试进行切换备用 + # 否则会在达到指定次数后 **退出** 主程序 + max-check-count: 0 + # 签名服务请求超时时间(s) + sign-server-timeout: 60 # 如果签名服务器的版本在1.1.0及以下, 请将下面的参数改成true + # 建议使用 1.1.6 以上版本,低版本普遍半个月冻结一次 is-below-110: false - # 签名服务器所需要的apikey, 如果签名服务器的版本在1.1.0及以下则此项无效 - # 本地部署的默认为114514 - key: '114514' # 在实例可能丢失(获取到的签名为空)时是否尝试重新注册 # 为 true 时,在签名服务不可用时可能每次发消息都会尝试重新注册并签名。 # 为 false 时,将不会自动注册实例,在签名服务器重启或实例被销毁后需要重启 go-cqhttp 以获取实例 # 否则后续消息将不会正常签名。关闭此项后可以考虑开启签名服务器端 auto_register 避免需要重启 + # 由于实现问题,当前建议关闭此项,推荐开启签名服务器的自动注册实例 auto-register: false # 是否在 token 过期后立即自动刷新签名 token(在需要签名时才会检测到,主要防止 token 意外丢失) # 独立于定时刷新 @@ -75,8 +98,6 @@ message: convert-webp-image: false # download 超时时间(s) http-timeout: 15 - # 签名服务超时时间(s) - sign-server-timeout: 60 output: # 日志等级 trace,debug,info,warn,error