diff --git a/cmd/gocq/login.go b/cmd/gocq/login.go index a2c35ed05..5b153bfed 100644 --- a/cmd/gocq/login.go +++ b/cmd/gocq/login.go @@ -3,6 +3,7 @@ package gocq import ( "bufio" "bytes" + "fmt" "image" "image/color" "image/png" @@ -10,6 +11,8 @@ import ( "strings" "time" + "github.com/Mrs4s/go-cqhttp/internal/download" + "github.com/LagrangeDev/LagrangeGo/utils" "github.com/LagrangeDev/LagrangeGo/client/packets/wtlogin/qrcodestate" @@ -59,6 +62,14 @@ var device *auth.DeviceInfo // ErrSMSRequestError SMS请求出错 var ErrSMSRequestError = errors.New("sms request error") +func commonLogin() error { + res, err := cli.PasswordLogin() + if err != nil { + return err + } + return loginResponseProcessor(res) +} + func printQRCode(imgData []byte) { // (".", "^", " ", "@") : ("▄", "▀", " ", "█") const ( @@ -104,6 +115,7 @@ func printQRCode(imgData []byte) { _, _ = colorable.NewColorableStdout().Write(buf) } +//nolint:unused func printQRCodeCommon(imgData []byte) { const ( black = "\033[48;5;0m \033[0m" @@ -166,13 +178,142 @@ func qrcodeLogin() error { case qrcodestate.WaitingForConfirm: log.Infof("扫码成功, 请在手机端确认登录.") case qrcodestate.Confirmed: - err := cli.QRCodeLogin(1) + res, err := cli.QRCodeLogin() if err != nil { return err } - return cli.Register() + return loginResponseProcessor(res) case qrcodestate.WaitingForScan: // ignore } } } + +func loginResponseProcessor(res *client.LoginResponse) error { + var err error + for { + if err != nil { + return err + } + if res.Success { + return nil + } + //var text string + //nolint:exhaustive + switch res.Error { + case client.SliderNeededError: + log.Warnf("登录需要滑条验证码, 请验证后重试.") + ticket, randStr := getTicket(res.VerifyURL) + if ticket == "" { + log.Infof("按 Enter 继续....") + readLine() + os.Exit(0) + } + res, err = cli.SubmitCaptcha(strings.Split(strings.Split(res.VerifyURL, "sid=")[1], "&")[0], ticket, randStr) + continue + //case client.NeedCaptcha: + // log.Warnf("登录需要验证码.") + // _ = os.WriteFile("captcha.jpg", res.CaptchaImage, 0o644) + // log.Warnf("请输入验证码 (captcha.jpg): (Enter 提交)") + // text = readLine() + // global.DelFile("captcha.jpg") + // res, err = cli.SubmitCaptcha(text, res.CaptchaSign) + // continue + // TODO 短信验证码? + //case client.SMSNeededError: + // log.Warnf("账号已开启设备锁, 按 Enter 向手机 %v 发送短信验证码.", res.SMSPhone) + // readLine() + // if !cli.RequestSMS() { + // log.Warnf("发送验证码失败,可能是请求过于频繁.") + // return errors.WithStack(ErrSMSRequestError) + // } + // log.Warn("请输入短信验证码: (Enter 提交)") + // text = readLine() + // res, err = cli.SubmitSMS(text) + // continue + // TODO 设备锁? + //case client.SMSOrVerifyNeededError: + // log.Warnf("账号已开启设备锁,请选择验证方式:") + // log.Warnf("1. 向手机 %v 发送短信验证码", res.SMSPhone) + // log.Warnf("2. 使用手机QQ扫码验证.") + // log.Warn("请输入(1 - 2):") + // text = readIfTTY("2") + // if strings.Contains(text, "1") { + // if !cli.RequestSMS() { + // log.Warnf("发送验证码失败,可能是请求过于频繁.") + // return errors.WithStack(ErrSMSRequestError) + // } + // log.Warn("请输入短信验证码: (Enter 提交)") + // text = readLine() + // res, err = cli.SubmitSMS(text) + // continue + // } + // fallthrough + case client.UnsafeDeviceError: + log.Warnf("账号已开启设备锁,请前往 -> %v <- 验证后重启Bot.", res.VerifyURL) + log.Infof("按 Enter 或等待 5s 后继续....") + readLineTimeout(time.Second * 5) + os.Exit(0) + case client.OtherLoginError, client.UnknownLoginError, client.TooManySMSRequestError: + fallthrough + default: + msg := res.ErrorMessage + log.Warnf("登录失败: %v Code: %v", msg, res.Code) + switch res.Code { + case 235: + log.Warnf("设备信息被封禁, 请删除 device.json 后重试.") + case 237: + log.Warnf("登录过于频繁, 请在手机QQ登录并根据提示完成认证后等一段时间重试") + case 45: + log.Warnf("你的账号被限制登录, 请配置 SignServer 后重试") + } + log.Infof("按 Enter 继续....") + readLine() + os.Exit(0) + } + } +} + +func getTicket(u string) (string, string) { + log.Warnf("请选择提交滑块ticket方式:") + log.Warnf("1. 自动提交") + log.Warnf("2. 手动抓取提交") + log.Warn("请输入(1 - 2):") + text := readLine() + id := utils.NewUUID() + auto := !strings.Contains(text, "2") + // TODO 自动获取验证码 + if auto { + u = strings.ReplaceAll(u, "https://ti.qq.com/safe/tools/captcha/sms-verify-login?", fmt.Sprintf("https://captcha.go-cqhttp.org/captcha?id=%v&", id)) + } + log.Warnf("请前往该地址验证 -> %v ", u) + if !auto { + log.Warn("请输入ticket: (Enter 提交)") + ticket := readLine() + log.Warn("请输入rand_str: (Enter 提交)") + randStr := readLine() + return ticket, randStr + } + + for count := 120; count > 0; count-- { + ticket, randStr := fetchCaptcha(id) + if ticket != "" && randStr != "" { + return ticket, randStr + } + time.Sleep(time.Second) + } + log.Warnf("验证超时") + return "", "" +} + +func fetchCaptcha(id string) (string, string) { + g, err := download.Request{URL: "https://captcha.go-cqhttp.org/captcha/ticket?id=" + id}.JSON() + if err != nil { + log.Debugf("获取 Ticket 时出现错误: %v", err) + return "", "" + } + if g.Get("ticket").Exists() && g.Get("randstr").Exists() { + return g.Get("ticket").String(), g.Get("randstr").String() + } + return "", "" +} diff --git a/cmd/gocq/main.go b/cmd/gocq/main.go index 0cb0afff3..c81b63ec1 100644 --- a/cmd/gocq/main.go +++ b/cmd/gocq/main.go @@ -3,6 +3,7 @@ package gocq import ( "crypto/aes" + "crypto/md5" "crypto/sha1" "encoding/hex" "fmt" @@ -17,9 +18,11 @@ import ( "github.com/LagrangeDev/LagrangeGo/client/auth" "github.com/LagrangeDev/LagrangeGo/client" + para "github.com/fumiama/go-hide-param" rotatelogs "github.com/lestrrat-go/file-rotatelogs" log "github.com/sirupsen/logrus" "golang.org/x/crypto/pbkdf2" + "golang.org/x/term" "github.com/Mrs4s/go-cqhttp/coolq" "github.com/Mrs4s/go-cqhttp/db" @@ -103,6 +106,7 @@ func PrepareData() { // LoginInteract 登录交互, 可能需要键盘输入, 必须在 InitBase, PrepareData 之后执行 func LoginInteract() { + var byteKey []byte arg := os.Args if len(arg) > 1 { for i := range arg { @@ -113,12 +117,18 @@ func LoginInteract() { } else { selfupdate.SelfUpdate("") } + case "key": + p := i + 1 + if len(arg) > p { + byteKey = []byte(arg[p]) + para.Hide(p) + } } } } - if !global.FileExists("session.token") { - log.Info("不存在会话缓存,使用二维码登录.") + if (base.Account.Uin == 0 || (base.Account.Password == "" && !base.Account.Encrypt)) && !global.PathExists("session.token") { + log.Warn("账号密码未配置, 将使用二维码登录.") if !base.FastStart { log.Warn("将在 5秒 后继续.") time.Sleep(time.Second * 5) @@ -143,6 +153,56 @@ func LoginInteract() { } } + if base.Account.Encrypt { + if !global.PathExists("password.encrypt") { + if base.Account.Password == "" { + log.Error("无法进行加密,请在配置文件中的添加密码后重新启动.") + } else { + log.Infof("密码加密已启用, 请输入Key对密码进行加密: (Enter 提交)") + byteKey, _ = term.ReadPassword(int(os.Stdin.Fd())) + base.PasswordHash = md5.Sum([]byte(base.Account.Password)) + _ = os.WriteFile("password.encrypt", []byte(PasswordHashEncrypt(base.PasswordHash[:], byteKey)), 0o644) + log.Info("密码已加密,为了您的账号安全,请删除配置文件中的密码后重新启动.") + } + readLine() + os.Exit(0) + } + if base.Account.Password != "" { + log.Error("密码已加密,为了您的账号安全,请删除配置文件中的密码后重新启动.") + readLine() + os.Exit(0) + } + if len(byteKey) == 0 { + log.Infof("密码加密已启用, 请输入Key对密码进行解密以继续: (Enter 提交)") + cancel := make(chan struct{}, 1) + state, _ := term.GetState(int(os.Stdin.Fd())) + go func() { + select { + case <-cancel: + return + case <-time.After(time.Second * 45): + log.Infof("解密key输入超时") + time.Sleep(3 * time.Second) + _ = term.Restore(int(os.Stdin.Fd()), state) + os.Exit(0) + } + }() + byteKey, _ = term.ReadPassword(int(os.Stdin.Fd())) + cancel <- struct{}{} + } else { + log.Infof("密码加密已启用, 使用运行时传递的参数进行解密,按 Ctrl+C 取消.") + } + + encrypt, _ := os.ReadFile("password.encrypt") + ph, err := PasswordHashDecrypt(string(encrypt), byteKey) + if err != nil { + log.Fatalf("加密存储的密码损坏,请尝试重新配置密码") + } + copy(base.PasswordHash[:], ph) + } else if len(base.Account.Password) > 0 { + base.PasswordHash = md5.Sum([]byte(base.Account.Password)) + } + if !base.FastStart { log.Info("Bot将在5秒后登录并开始信息处理, 按 Ctrl+C 取消.") time.Sleep(time.Second * 5) @@ -151,8 +211,9 @@ func LoginInteract() { app := auth.AppList["linux"]["3.2.10-25765"] log.Infof("使用协议: %s %s", app.OS, app.CurrentVersion) cli = newClient(app) + cli.UseVersion(app) cli.UseDevice(device) - isQRCodeLogin := true + isQRCodeLogin := (base.Account.Uin == 0 || len(base.Account.Password) == 0) && !base.Account.Encrypt isTokenLogin := false saveToken := func() { @@ -163,7 +224,20 @@ func LoginInteract() { token, _ := os.ReadFile("session.token") sig, err := auth.UnmarshalSigInfo(token, true) if err == nil { - if err = cli.FastLogin(&sig); err != nil { + if base.Account.Uin != 0 && int64(sig.Uin) != base.Account.Uin { + log.Warnf("警告: 配置文件内的QQ号 (%v) 与缓存内的QQ号 (%v) 不相同", base.Account.Uin, int64(sig.Uin)) + log.Warnf("1. 使用会话缓存继续.") + log.Warnf("2. 删除会话缓存并重启.") + log.Warnf("请选择:") + text := readIfTTY("1") + if text == "2" { + _ = os.Remove("session.token") + log.Infof("缓存已删除.") + os.Exit(0) + } + } + cli.UseSig(sig) + if err = cli.FastLogin(); err != nil { _ = os.Remove("session.token") log.Warnf("恢复会话失败: %v , 尝试使用正常流程登录.", err) time.Sleep(time.Second) @@ -176,9 +250,19 @@ func LoginInteract() { } } } + if base.Account.Uin != 0 && base.PasswordHash != [16]byte{} { + cli.Uin = uint32(base.Account.Uin) + cli.PasswordMD5 = base.PasswordHash + } if !isTokenLogin { - if err := qrcodeLogin(); err != nil { - log.Fatalf("登录时发生致命错误: %v", err) + if !isQRCodeLogin { + if err := commonLogin(); err != nil { + log.Fatalf("登录时发生致命错误: %v", err) + } + } else { + if err := qrcodeLogin(); err != nil { + log.Fatalf("登录时发生致命错误: %v", err) + } } } var times uint = 1 // 重试次数 @@ -212,7 +296,7 @@ func LoginInteract() { break } log.Warnf("尝试重连...") - err := cli.FastLogin(nil) + err := cli.FastLogin() if err == nil { saveToken() return @@ -242,7 +326,7 @@ func LoginInteract() { global.Check(cli.RefreshAllGroupsInfo(), true) GroupListLen := len(cli.GetCachedAllGroupsInfo()) log.Infof("共加载 %v 个群.", GroupListLen) - // TODO 设置在线状态 不支持? + // TODO 设置在线状态 暂不支持? // if uint(base.Account.Status) >= uint(len(allowStatus)) { // base.Account.Status = 0 //} @@ -297,16 +381,18 @@ func PasswordHashDecrypt(encryptedPasswordHash string, key []byte) ([]byte, erro return result, nil } -func newClient(appInfo *auth.AppInfo) *client.QQClient { - signUrls := make([]string, len(base.SignServers)) - for i, s := range base.SignServers { +func newClient(app *auth.AppInfo) *client.QQClient { + signUrls := make([]string, 0, len(base.SignServers)) + for _, s := range base.SignServers { u, err := url.Parse(s.URL) if err != nil || u.Hostname() == "" { continue } - signUrls[i] = u.String() + signUrls = append(signUrls, u.String()) } - c := client.NewClient(0, appInfo, signUrls...) + c := client.NewClientEmpty() + c.UseVersion(app) + c.AddSignServer(signUrls...) // TODO 服务器更新通知 // c.OnServerUpdated(func(bot *client.QQClient, e *client.ServerUpdatedEvent) bool { // if !base.UseSSOAddress { @@ -320,8 +406,7 @@ func newClient(appInfo *auth.AppInfo) *client.QQClient { log.Infof("检测到 address.txt 文件. 将覆盖目标IP.") addr := global.ReadAddrFile("address.txt") if len(addr) > 0 { - // TODO 使用自定义服务器 - // c.SetCustomServer(addr) + c.SetCustomServer(addr) } log.Infof("读取到 %v 个自定义地址.", len(addr)) } diff --git a/go.mod b/go.mod index f1384284d..b2b481430 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,12 @@ go 1.20 require ( github.com/FloatTech/sqlite v1.6.3 - github.com/LagrangeDev/LagrangeGo v0.1.1 + github.com/LagrangeDev/LagrangeGo v0.1.2-0.20241112102104-63ccad50ea67 github.com/Microsoft/go-winio v0.6.2-0.20230724192519-b29bbd58a65a github.com/RomiChan/syncx v0.0.0-20240418144900-b7402ffdebc7 github.com/RomiChan/websocket v1.4.3-0.20220227141055-9b2c6168c9c5 github.com/fumiama/go-base16384 v1.7.0 + github.com/fumiama/go-hide-param v0.2.0 github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible github.com/mattn/go-colorable v0.1.13 github.com/pkg/errors v0.9.1 @@ -22,6 +23,7 @@ require ( golang.org/x/crypto v0.29.0 golang.org/x/image v0.18.0 golang.org/x/sys v0.27.0 + golang.org/x/term v0.26.0 golang.org/x/time v0.3.0 gopkg.ilharper.com/x/isatty v1.1.1 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 8d800096c..ae33fd1d3 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/FloatTech/sqlite v1.6.3 h1:MQkqBNlkPuCoKQQgoNLuTL/2Ci3tBTFAnVYBdD0Wy4 github.com/FloatTech/sqlite v1.6.3/go.mod h1:zFbHzRfB+CJ+VidfjuVbrcin3DAz283F7hF1hIeHzpY= github.com/FloatTech/ttl v0.0.0-20230307105452-d6f7b2b647d1 h1:g4pTnDJUW4VbJ9NvoRfUvdjDrHz/6QhfN/LoIIpICbo= github.com/FloatTech/ttl v0.0.0-20230307105452-d6f7b2b647d1/go.mod h1:fHZFWGquNXuHttu9dUYoKuNbm3dzLETnIOnm1muSfDs= -github.com/LagrangeDev/LagrangeGo v0.1.1 h1:YqnFd1DaWZ9Y4CILS0PSDu3R2yt9myOp7C/xoDwrnGU= -github.com/LagrangeDev/LagrangeGo v0.1.1/go.mod h1:dDLKGYgka2pEfPP49fUlbdnazxAvAlSTDRjd8kAX/Xg= +github.com/LagrangeDev/LagrangeGo v0.1.2-0.20241112102104-63ccad50ea67 h1:rbD3p2W6IaQxaZIo1X5fWxeRStHb+VeJBhBITUg2bhE= +github.com/LagrangeDev/LagrangeGo v0.1.2-0.20241112102104-63ccad50ea67/go.mod h1:m7ydyvA8DKOg5c6iTFDfNtfIK9uhqXVJKRXl4mlGkTA= github.com/Microsoft/go-winio v0.6.2-0.20230724192519-b29bbd58a65a h1:aU1703IHxupjzipvhu16qYKLMR03e+8WuNR+JMsKfGU= github.com/Microsoft/go-winio v0.6.2-0.20230724192519-b29bbd58a65a/go.mod h1:OZqLNXdYJHmx7aqq/T6wAdFEdoGm5nmIfC4kU7M8P8o= github.com/RomiChan/protobuf v0.1.1-0.20230204044148-2ed269a2e54d h1:/Xuj3fIiMY2ls1TwvPKmaqQrtJsPY+c9s+0lOScVHd8= @@ -19,6 +19,8 @@ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fumiama/go-base16384 v1.7.0 h1:6fep7XPQWxRlh4Hu+KsdH+6+YdUp+w6CwRXtMWSsXCA= github.com/fumiama/go-base16384 v1.7.0/go.mod h1:OEn+947GV5gsbTAnyuUW/SrfxJYUdYupSIQXOuGOcXM= +github.com/fumiama/go-hide-param v0.2.0 h1:1IuDOYJBDZVH2/wvF4gzhO8a/3zWXpfOJDYyaLiRSVQ= +github.com/fumiama/go-hide-param v0.2.0/go.mod h1:vJkQlJIEI56nIyp7tCQu1/2QOyKtZpudsnJkGk9U1aY= github.com/fumiama/gofastTEA v0.1.0 h1:eW8yCq2BgqijIhgWcdjNuTs9RIOst8M81BKYvqHg0/g= github.com/fumiama/gofastTEA v0.1.0/go.mod h1:RIdbYZyB4MbH6ZBlPymRaXn3cD6SedlCu5W/HHfMPBk= github.com/fumiama/imgsz v0.0.4 h1:Lsasu2hdSSFS+vnD+nvR1UkiRMK7hcpyYCC0FzgSMFI= @@ -141,6 +143,8 @@ golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/modules/config/config.go b/modules/config/config.go index 4dc229ef8..11305deb6 100644 --- a/modules/config/config.go +++ b/modules/config/config.go @@ -28,6 +28,10 @@ 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"` diff --git a/modules/config/default_config.yml b/modules/config/default_config.yml index 499114de3..b94cb5000 100644 --- a/modules/config/default_config.yml +++ b/modules/config/default_config.yml @@ -1,6 +1,10 @@ # go-cqhttp 默认配置文件 account: # 账号相关 + uin: 1233456 # QQ账号 + password: '' # 密码为空时使用扫码登录 + encrypt: false # 是否开启密码加密 + status: 0 # 在线状态 请参考 https://docs.go-cqhttp.org/guide/config.html#在线状态 relogin: # 重连设置 delay: 3 # 首次重连延迟, 单位秒 interval: 3 # 重连间隔 @@ -14,16 +18,14 @@ account: # 账号相关 # 数据包的签名服务器列表,第一个作为主签名服务器,后续作为备用 # 与android签名不兼容 - # 如果遇到 登录 45 错误, 或者发送信息风控的话需要填入一个或多个服务器 - # 不建议设置过多,设置主备各一个即可,超过 5 个只会取前五个 # 示例: - # sign-servers: + # sign-servers: # - url: 'http://127.0.0.1:8080' # 本地签名服务器 # - url: 'https://signserver.example.com' # 线上签名服务器 # ... - # + # # 服务器不提供自建 - sign-servers: + sign-servers: - url: 'https://sign.ciallo.site/api/sign' # 主签名服务器地址, 必填 - url: 'https://sign.lagrangecore.org/api/sign/25765' # 备用