1
0
mirror of https://github.com/Mrs4s/go-cqhttp.git synced 2025-05-04 19:17:37 +08:00
go-cqhttp/server/apiAdmin.go
2020-11-12 23:07:34 +08:00

566 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package server
import (
"bufio"
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/Mrs4s/MiraiGo/client"
"github.com/Mrs4s/go-cqhttp/coolq"
"github.com/Mrs4s/go-cqhttp/global"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"github.com/yinghau76/go-ascii-art"
"image"
"io/ioutil"
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"time"
)
var WebInput = make(chan string, 1) //长度1用于阻塞
var Console = make(chan os.Signal, 1)
var JsonConfig *global.JsonConfig
type webServer struct {
engine *gin.Engine
bot *coolq.CQBot
Cli *client.QQClient
Conf *global.JsonConfig //old config
Console *bufio.Reader
}
var WebServer = &webServer{}
// admin 子站的 路由映射
var HttpuriAdmin = map[string]func(s *webServer, c *gin.Context){
"do_restart": AdminDoRestart, //热重启
"get_web_write": AdminWebWrite, //获取是否验证码输入
"do_web_write": AdminDoWebWrite, //web上进行输入操作
"do_restart_docker": AdminDoRestartDocker, //直接停止依赖supervisord/docker重新拉起
"do_config_base": AdminDoConfigBase, //修改config.json中的基础部分
"do_config_http": AdminDoConfigHttp, //修改config.json的http部分
"do_config_ws": AdminDoConfigWs, //修改config.json的正向ws部分
"do_config_reverse": AdminDoConfigReverse, //修改config.json 中的反向ws部分
"do_config_json": AdminDoConfigJson, //直接修改 config.json配置
"get_config_json": AdminGetConfigJson, //拉取 当前的config.json配置
}
func Failed(code int, msg string) coolq.MSG {
return coolq.MSG{"data": nil, "retcode": code, "status": "failed", "msg": msg}
}
func (s *webServer) Run(addr string, cli *client.QQClient) *coolq.CQBot {
s.Cli = cli
s.Conf = GetConf()
JsonConfig = s.Conf
gin.SetMode(gin.ReleaseMode)
s.engine = gin.New()
s.engine.Use(AuthMiddleWare())
//通用路由
s.engine.Any("/admin/:action", s.admin)
go func() {
//开启端口监听
if s.Conf.WebUi.Enabled {
log.Infof("Admin API 服务器已启动: %v", addr)
err := s.engine.Run(addr)
if err != nil {
log.Error(err)
log.Infof("请检查端口是否被占用.")
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, os.Kill)
<-c
os.Exit(1)
}
} else {
//关闭端口监听
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, os.Kill)
<-c
os.Exit(1)
}
}()
s.Dologin()
s.UpServer()
b := s.bot //外部引入 bot对象用于操作bot
return b
}
func (s *webServer) Dologin() {
s.Console = bufio.NewReader(os.Stdin)
readLine := func() (str string) {
str, _ = s.Console.ReadString('\n')
return
}
conf := GetConf()
cli := s.Cli
cli.AllowSlider = true
rsp, err := cli.Login()
for {
global.Check(err)
var text string
if !rsp.Success {
switch rsp.Error {
case client.SliderNeededError:
if client.SystemDeviceInfo.Protocol == client.AndroidPhone {
log.Warnf("警告: Android Phone 强制要求暂不支持的滑条验证码, 请开启设备锁或切换到Watch协议验证通过后再使用.")
log.Infof("按 Enter 继续....")
readLine()
os.Exit(0)
}
cli.AllowSlider = false
cli.Disconnect()
rsp, err = cli.Login()
continue
case client.NeedCaptcha:
_ = ioutil.WriteFile("captcha.jpg", rsp.CaptchaImage, 0644)
img, _, _ := image.Decode(bytes.NewReader(rsp.CaptchaImage))
fmt.Println(asciiart.New("image", img).Art)
if conf.WebUi.WebInput {
log.Warnf("请输入验证码 (captcha.jpg) (http://%s:%d/admin/do_web_write 输入)", conf.WebUi.Host, conf.WebUi.WebUiPort)
text = <-WebInput
} else {
log.Warn("请输入验证码 (captcha.jpg) (Enter 提交)")
text = readLine()
}
rsp, err = cli.SubmitCaptcha(strings.ReplaceAll(text, "\n", ""), rsp.CaptchaSign)
global.DelFile("captcha.jpg")
continue
case client.SMSNeededError:
log.Warnf("账号已开启设备锁, 按下 Enter 向手机 %v 发送短信验证码.", rsp.SMSPhone)
readLine()
if !cli.RequestSMS() {
log.Warnf("发送验证码失败,可能是请求过于频繁.")
time.Sleep(time.Second * 5)
os.Exit(0)
}
log.Warn("请输入短信验证码: (Enter 提交)")
text = readLine()
rsp, err = cli.SubmitSMS(strings.ReplaceAll(strings.ReplaceAll(text, "\n", ""), "\r", ""))
continue
case client.SMSOrVerifyNeededError:
log.Warnf("账号已开启设备锁,请选择验证方式:")
log.Warnf("1. 向手机 %v 发送短信验证码", rsp.SMSPhone)
log.Warnf("2. 使用手机QQ扫码验证.")
log.Warn("请输入(1 - 2): ")
text = readLine()
if strings.Contains(text, "1") {
if !cli.RequestSMS() {
log.Warnf("发送验证码失败,可能是请求过于频繁.")
time.Sleep(time.Second * 5)
os.Exit(0)
}
log.Warn("请输入短信验证码: (Enter 提交)")
text = readLine()
rsp, err = cli.SubmitSMS(strings.ReplaceAll(strings.ReplaceAll(text, "\n", ""), "\r", ""))
continue
}
log.Warnf("请前往 -> %v <- 验证并重启Bot.", rsp.VerifyUrl)
log.Infof("按 Enter 继续....")
readLine()
os.Exit(0)
return
case client.UnsafeDeviceError:
log.Warnf("账号已开启设备锁,请前往 -> %v <- 验证并重启Bot.", rsp.VerifyUrl)
if conf.WebUi.WebInput {
log.Infof(" (http://%s:%d/admin/do_web_write 确认后继续)....", conf.WebUi.Host, conf.WebUi.WebUiPort)
text = <-WebInput
} else {
log.Infof("按 Enter 继续....")
readLine()
}
log.Info(text)
os.Exit(0)
return
case client.OtherLoginError, client.UnknownLoginError:
msg := rsp.ErrorMessage
if strings.Contains(msg, "版本") {
msg = "密码错误或账号被冻结"
}
log.Warnf("登录失败: %v", msg)
log.Infof("按 Enter 继续....")
readLine()
os.Exit(0)
return
}
}
break
}
log.Infof("登录成功 欢迎使用: %v", cli.Nickname)
time.Sleep(time.Second)
log.Info("开始加载好友列表...")
global.Check(cli.ReloadFriendList())
log.Infof("共加载 %v 个好友.", len(cli.FriendList))
log.Infof("开始加载群列表...")
global.Check(cli.ReloadGroupList())
log.Infof("共加载 %v 个群.", len(cli.GroupList))
s.bot = coolq.NewQQBot(cli, conf)
if conf.PostMessageFormat != "string" && conf.PostMessageFormat != "array" {
log.Warnf("post_message_format 配置错误, 将自动使用 string")
coolq.SetMessageFormat("string")
} else {
coolq.SetMessageFormat(conf.PostMessageFormat)
}
if conf.RateLimit.Enabled {
global.InitLimiter(conf.RateLimit.Frequency, conf.RateLimit.BucketSize)
}
log.Info("正在加载事件过滤器.")
global.BootFilter()
global.InitCodec()
coolq.IgnoreInvalidCQCode = conf.IgnoreInvalidCQCode
coolq.ForceFragmented = conf.ForceFragmented
log.Info("资源初始化完成, 开始处理信息.")
log.Info("アトリは、高性能ですから!")
cli.OnDisconnected(func(bot *client.QQClient, e *client.ClientDisconnectedEvent) {
if conf.ReLogin.Enabled {
var times uint = 1
for {
if cli.Online {
log.Warn("Bot已登录")
return
}
if times > conf.ReLogin.MaxReloginTimes && conf.ReLogin.MaxReloginTimes != 0 {
break
}
log.Warnf("Bot已离线 (%v),将在 %v 秒后尝试重连. 重连次数:%v",
e.Message, conf.ReLogin.ReLoginDelay, times)
times++
time.Sleep(time.Second * time.Duration(conf.ReLogin.ReLoginDelay))
rsp, err := cli.Login()
if err != nil {
log.Errorf("重连失败: %v", err)
continue
}
if !rsp.Success {
switch rsp.Error {
case client.NeedCaptcha:
log.Fatalf("重连失败: 需要验证码. (验证码处理正在开发中)")
case client.UnsafeDeviceError:
log.Fatalf("重连失败: 设备锁")
default:
log.Errorf("重连失败: %v", rsp.ErrorMessage)
cli.Disconnect()
continue
}
}
log.Info("重连成功")
return
}
log.Fatal("重连失败: 重连次数达到设置的上限值")
}
s.bot.Release()
log.Fatalf("Bot已离线%v", e.Message)
})
}
func (s *webServer) admin(c *gin.Context) {
action := c.Param("action")
log.Debugf("WebServer接收到cgi调用: %v", action)
if f, ok := HttpuriAdmin[action]; ok {
f(s, c)
} else {
c.JSON(200, coolq.Failed(404))
}
}
// 获取当前配置文件信息
func GetConf() *global.JsonConfig {
if JsonConfig != nil {
return JsonConfig
}
conf := global.Load("config.json")
return conf
}
// admin 控制器 登录验证
func AuthMiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
conf := GetConf()
//处理跨域问题
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token")
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, PATCH, DELETE")
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
c.Header("Access-Control-Allow-Credentials", "true")
// 放行所有OPTIONS方法因为有的模板是要请求两次的
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
// 处理请求
if c.Request.Method != "GET" && c.Request.Method != "POST" {
log.Warnf("已拒绝客户端 %v 的请求: 方法错误", c.Request.RemoteAddr)
c.Status(404)
c.Abort()
}
if c.Request.Method == "POST" && strings.Contains(c.Request.Header.Get("Content-Type"), "application/json") {
d, err := c.GetRawData()
if err != nil {
log.Warnf("获取请求 %v 的Body时出现错误: %v", c.Request.RequestURI, err)
c.Status(400)
c.Abort()
}
if !gjson.ValidBytes(d) {
log.Warnf("已拒绝客户端 %v 的请求: 非法Json", c.Request.RemoteAddr)
c.Status(400)
c.Abort()
}
c.Set("json_body", gjson.ParseBytes(d))
}
authToken := conf.AccessToken
if auth := c.Request.Header.Get("Authorization"); auth != "" {
if strings.SplitN(auth, " ", 2)[1] != authToken {
c.AbortWithStatus(401)
return
}
} else if c.Query("access_token") != authToken {
c.AbortWithStatus(401)
return
} else {
c.Next()
}
}
}
func (s *webServer) DoReLogin() { // TODO: 协议层的 ReLogin
JsonConfig = nil
conf := GetConf()
OldConf := s.Conf
cli := client.NewClient(conf.Uin, conf.Password)
log.Info("开始尝试登录并同步消息...")
log.Infof("使用协议: %v", func() string {
switch client.SystemDeviceInfo.Protocol {
case client.AndroidPad:
return "Android Pad"
case client.AndroidPhone:
return "Android Phone"
case client.AndroidWatch:
return "Android Watch"
}
return "未知"
}())
cli.OnLog(func(c *client.QQClient, e *client.LogEvent) {
switch e.Type {
case "INFO":
log.Info("Protocol -> " + e.Message)
case "ERROR":
log.Error("Protocol -> " + e.Message)
case "DEBUG":
log.Debug("Protocol -> " + e.Message)
}
})
cli.OnServerUpdated(func(bot *client.QQClient, e *client.ServerUpdatedEvent) bool {
if !conf.UseSSOAddress {
log.Infof("收到服务器地址更新通知, 根据配置文件已忽略.")
return false
}
log.Infof("收到服务器地址更新通知, 将在下一次重连时应用. ")
return true
})
s.Cli = cli
s.Dologin()
//关闭之前的 server
if OldConf.HttpConfig != nil && OldConf.HttpConfig.Enabled {
HttpServer.ShutDown()
}
//if OldConf.WSConfig != nil && OldConf.WSConfig.Enabled {
// server.WsShutdown()
//}
//s.UpServer()
s.ReloadServer()
s.Conf = conf
}
func (s *webServer) UpServer() {
conf := GetConf()
if conf.HttpConfig != nil && conf.HttpConfig.Enabled {
go HttpServer.Run(fmt.Sprintf("%s:%d", conf.HttpConfig.Host, conf.HttpConfig.Port), conf.AccessToken, s.bot)
for k, v := range conf.HttpConfig.PostUrls {
NewHttpClient().Run(k, v, conf.HttpConfig.Timeout, s.bot)
}
}
if conf.WSConfig != nil && conf.WSConfig.Enabled {
go WebsocketServer.Run(fmt.Sprintf("%s:%d", conf.WSConfig.Host, conf.WSConfig.Port), conf.AccessToken, s.bot)
}
for _, rc := range conf.ReverseServers {
go NewWebsocketClient(rc, conf.AccessToken, s.bot).Run()
}
}
// 暂不支持ws服务的重启
func (s *webServer) ReloadServer() {
conf := GetConf()
if conf.HttpConfig != nil && conf.HttpConfig.Enabled {
go HttpServer.Run(fmt.Sprintf("%s:%d", conf.HttpConfig.Host, conf.HttpConfig.Port), conf.AccessToken, s.bot)
for k, v := range conf.HttpConfig.PostUrls {
NewHttpClient().Run(k, v, conf.HttpConfig.Timeout, s.bot)
}
}
for _, rc := range conf.ReverseServers {
go NewWebsocketClient(rc, conf.AccessToken, s.bot).Run()
}
}
// 热重启
func AdminDoRestart(s *webServer, c *gin.Context) {
s.bot.Release()
s.bot = nil
s.Cli = nil
s.DoReLogin()
c.JSON(200, coolq.OK(coolq.MSG{}))
return
}
// 冷重启
func AdminDoRestartDocker(s *webServer, c *gin.Context) {
Console <- os.Kill
c.JSON(200, coolq.OK(coolq.MSG{}))
return
}
// web输入 html 页面
func AdminWebWrite(s *webServer, c *gin.Context) {
pic := global.ReadAllText("captcha.jpg")
var picbase64 string
var ispic = false
if pic != "" {
input := []byte(pic)
// base64编码
picbase64 = base64.StdEncoding.EncodeToString(input)
ispic = true
}
c.JSON(200, coolq.OK(coolq.MSG{
"ispic": ispic, //为空则为 设备锁 或者没有需要输入
"picbase64": picbase64, //web上显示图片
}))
}
// web输入 处理
func AdminDoWebWrite(s *webServer, c *gin.Context) {
input := c.PostForm("input")
WebInput <- input
c.JSON(200, coolq.OK(coolq.MSG{}))
}
// 普通配置修改
func AdminDoConfigBase(s *webServer, c *gin.Context) {
conf := GetConf()
conf.Uin, _ = strconv.ParseInt(c.PostForm("uin"), 10, 64)
conf.Password = c.PostForm("password")
if c.PostForm("enable_db") == "true" {
conf.EnableDB = true
} else {
conf.EnableDB = false
}
conf.AccessToken = c.PostForm("access_token")
if err := conf.Save("config.json"); err != nil {
log.Fatalf("保存 config.json 时出现错误: %v", err)
c.JSON(200, Failed(502, "保存 config.json 时出现错误:"+fmt.Sprintf("%v", err)))
} else {
JsonConfig = nil
c.JSON(200, coolq.OK(coolq.MSG{}))
}
}
// http配置修改
func AdminDoConfigHttp(s *webServer, c *gin.Context) {
conf := GetConf()
p, _ := strconv.ParseUint(c.PostForm("port"), 10, 16)
conf.HttpConfig.Port = uint16(p)
conf.HttpConfig.Host = c.PostForm("host")
if c.PostForm("enable") == "true" {
conf.HttpConfig.Enabled = true
} else {
conf.HttpConfig.Enabled = false
}
t, _ := strconv.ParseInt(c.PostForm("timeout"), 10, 32)
conf.HttpConfig.Timeout = int32(t)
if c.PostForm("post_url") != "" {
conf.HttpConfig.PostUrls[c.PostForm("post_url")] = c.PostForm("post_secret")
}
if err := conf.Save("config.json"); err != nil {
log.Fatalf("保存 config.json 时出现错误: %v", err)
c.JSON(200, Failed(502, "保存 config.json 时出现错误:"+fmt.Sprintf("%v", err)))
} else {
JsonConfig = nil
c.JSON(200, coolq.OK(coolq.MSG{}))
}
}
// ws配置修改
func AdminDoConfigWs(s *webServer, c *gin.Context) {
conf := GetConf()
p, _ := strconv.ParseUint(c.PostForm("port"), 10, 16)
conf.WSConfig.Port = uint16(p)
conf.WSConfig.Host = c.PostForm("host")
if c.PostForm("enable") == "true" {
conf.WSConfig.Enabled = true
} else {
conf.WSConfig.Enabled = false
}
if err := conf.Save("config.json"); err != nil {
log.Fatalf("保存 config.json 时出现错误: %v", err)
c.JSON(200, Failed(502, "保存 config.json 时出现错误:"+fmt.Sprintf("%v", err)))
} else {
JsonConfig = nil
c.JSON(200, coolq.OK(coolq.MSG{}))
}
}
// 反向ws配置修改
func AdminDoConfigReverse(s *webServer, c *gin.Context) {
conf := GetConf()
conf.ReverseServers[0].ReverseApiUrl = c.PostForm("reverse_api_url")
conf.ReverseServers[0].ReverseUrl = c.PostForm("reverse_url")
conf.ReverseServers[0].ReverseEventUrl = c.PostForm("reverse_event_url")
t, _ := strconv.ParseUint(c.PostForm("reverse_reconnect_interval"), 10, 16)
conf.ReverseServers[0].ReverseReconnectInterval = uint16(t)
if c.PostForm("enable") == "true" {
conf.ReverseServers[0].Enabled = true
} else {
conf.ReverseServers[0].Enabled = false
}
if err := conf.Save("config.json"); err != nil {
log.Fatalf("保存 config.json 时出现错误: %v", err)
c.JSON(200, Failed(502, "保存 config.json 时出现错误:"+fmt.Sprintf("%v", err)))
} else {
JsonConfig = nil
c.JSON(200, coolq.OK(coolq.MSG{}))
}
}
// config.json配置修改
func AdminDoConfigJson(s *webServer, c *gin.Context) {
conf := GetConf()
Json := c.PostForm("json")
err := json.Unmarshal([]byte(Json), &conf)
if err != nil {
log.Warnf("尝试加载配置文件 %v 时出现错误: %v", "config.json", err)
c.JSON(200, Failed(502, "保存 config.json 时出现错误:"+fmt.Sprintf("%v", err)))
return
}
if err := conf.Save("config.json"); err != nil {
log.Fatalf("保存 config.json 时出现错误: %v", err)
c.JSON(200, Failed(502, "保存 config.json 时出现错误:"+fmt.Sprintf("%v", err)))
} else {
JsonConfig = nil
c.JSON(200, coolq.OK(coolq.MSG{}))
}
}
// 拉取config.json配置
func AdminGetConfigJson(s *webServer, c *gin.Context) {
conf := GetConf()
c.JSON(200, coolq.OK(coolq.MSG{"config": conf}))
}