diff --git a/.gitignore b/.gitignore index 509be2f..f06bc58 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ run.sh mcbot *.sqlite + +tmp +tg-mc.db \ No newline at end of file diff --git a/conf/env.go b/conf/env.go index 57c90d5..81c8925 100644 --- a/conf/env.go +++ b/conf/env.go @@ -1,19 +1,32 @@ package conf import ( + "log" + "github.com/ilyakaznacheev/cleanenv" "github.com/joho/godotenv" ) type botSettings struct { - HTTPProxy string `env:"HTTP_PROXY"` - BotToken string `env:"BOT_TOKEN"` - MCServer string `env:"MC_SERVER"` - MCBotName string `env:"MC_BOT_NAME"` - GroupID int64 `env:"GROUP_ID"` - DBPath string `env:"DB_PATH"` - BotAPI string `env:"TG_BOT_API"` - AdminID []int64 `env:"ADMIN_ID"` + HTTPProxy string `env:"HTTP_PROXY"` + BotToken string `env:"BOT_TOKEN"` + MCServer string `env:"MC_SERVER"` + MCBotName string `env:"MC_BOT_NAME"` + GroupID int64 `env:"GROUP_ID"` + DBPath string `env:"DB_PATH" env-default:"tg-mc.db"` + BotAPI string `env:"TG_BOT_API"` + AdminID []int64 `env:"ADMIN_ID"` + GatewaySettings GatewaySettings `env-prefix:"GATEWAY_"` + EnableGateway bool `env:"ENABLE_GATEWAY" env-default:"true"` + EnableBridge bool `env:"ENABLE_BRIDGE" env-default:"true"` + EnableBot bool `env:"ENABLE_BOT" env-default:"true"` +} + +type GatewaySettings struct { + ServerHost string `json:"server_host" env:"MC_SERVER_HOST" env-default:"127.0.0.1"` + ServerPort int `json:"server_port" env:"MC_SERVER_PORT" env-default:"25566"` + ProxyHost string `json:"proxy_host" env:"PROXY_HOST" env-default:"127.0.0.1"` + ProxyPort int `json:"proxy_port" env:"PROXY_PORT" env-default:"25565"` } var ( @@ -23,6 +36,7 @@ var ( func init() { godotenv.Load() cleanenv.ReadEnv(&botSettingsInstance) + log.Printf("Bot settings: %+v", botSettingsInstance) } func GetBotSettings() *botSettings { diff --git a/go.mod b/go.mod index 21c8b8e..6954d62 100644 --- a/go.mod +++ b/go.mod @@ -35,4 +35,4 @@ require ( olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect ) -// replace github.com/Tnze/go-mc => /Users/vaala/Workdir/Code/go-mc +// replace github.com/Tnze/go-mc => /home/coder/go-mc diff --git a/services/gateway/auth.go b/services/gateway/auth.go new file mode 100644 index 0000000..947ae7a --- /dev/null +++ b/services/gateway/auth.go @@ -0,0 +1,86 @@ +package gateway + +import ( + "fmt" + "sync" + "tg-mc/conf" + "tg-mc/defs" + "tg-mc/models" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +type Auth interface { + IsAuthed(u models.User) bool + RequestAuth(u models.User, req *LoginRequest) + Reject(u models.User) + SetAuth(u models.User) +} + +type Authcator struct { + UserMap *sync.Map +} + +var authcator *Authcator + +func GetAuthcator() Auth { + if authcator == nil { + authcator = &Authcator{ + UserMap: &sync.Map{}, + } + } + return authcator +} + +func (a *Authcator) getUserLoginReq(u models.User) *LoginRequest { + chAny, ok := a.UserMap.Load(u.MCName) + if !ok { + return nil + } + + loginReq, ok := chAny.(*LoginRequest) + if !ok { + return nil + } + + return loginReq +} + +func (a *Authcator) IsAuthed(u models.User) bool { + loginReq := a.getUserLoginReq(u) + if loginReq == nil { + return false + } + + return <-loginReq.Resolve +} + +func (a *Authcator) RequestAuth(u models.User, req *LoginRequest) { + a.UserMap.Store(u.MCName, req) + m := tgbotapi.NewMessage(u.TGID, fmt.Sprintf("MC用户:%v 尝试登录,请选择操作", u.MCName)) + + m.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup( + tgbotapi.NewInlineKeyboardRow( + tgbotapi.NewInlineKeyboardButtonData("批准", defs.NewApproveCommand(u.MCName).ToJSON()), + tgbotapi.NewInlineKeyboardButtonData("拒绝", defs.NewRejectCommand(u.MCName).ToJSON())), + ) + conf.Bot.Send(m) +} + +func (a *Authcator) Reject(u models.User) { + loginReq := a.getUserLoginReq(u) + if loginReq == nil { + return + } + + loginReq.Resolve <- false +} + +func (a *Authcator) SetAuth(u models.User) { + loginReq := a.getUserLoginReq(u) + if loginReq == nil { + return + } + + loginReq.Resolve <- true +} diff --git a/services/gateway/gw.go b/services/gateway/gw.go new file mode 100644 index 0000000..87c543f --- /dev/null +++ b/services/gateway/gw.go @@ -0,0 +1,214 @@ +package gateway + +import ( + "errors" + "fmt" + "strconv" + "tg-mc/conf" + "tg-mc/models" + "time" + + "github.com/Tnze/go-mc/net" + pk "github.com/Tnze/go-mc/net/packet" + "github.com/Tnze/go-mc/offline" + "github.com/google/uuid" + "github.com/sirupsen/logrus" +) + +type LoginRequest struct { + ChatID int64 + Resolve chan bool +} + +func requestVerify(mcid string) (bool, error) { + + user, err := models.GetUserByMCName(mcid) + if err != nil { + return false, errors.New("mcid not found in settings") + } + + // Send a message to the user to accept or reject using your bot logic + logrus.Infof("Requesting verification for MCID: %s with ChatID: %d\n", mcid, user.TGID) + + // The channel to await a response from the verification process + loginRequest := &LoginRequest{ + ChatID: user.TGID, + Resolve: make(chan bool), + } + + GetAuthcator().RequestAuth(user, loginRequest) + + select { + case result := <-loginRequest.Resolve: + return result, nil + case <-time.After(10 * time.Second): // Replace with your actual timeout handling logic + return false, nil + } +} + +// This function should be called when a new client connection is accepted +func HandleClientConnection(clientConn net.Conn) { + settings := conf.GetBotSettings().GatewaySettings + + targetConns, err := net.DialMC(settings.ServerHost + ":" + strconv.Itoa(settings.ServerPort)) + + targetConn := *targetConns + + // targetConn, err := gonet.Dial("tcp", settings.ServerHost+":"+strconv.Itoa(settings.ServerPort)) + if err != nil { + logrus.Errorf("Failed to connect to target: %s", err) + return + } + protocol, intention, err := handshake(clientConn, targetConn) + if err != nil { + logrus.Errorf("Handshake error: %v", err) + return + } + + logrus.Infof("Protocol: %v, Intention: %v", protocol, intention) + + switch intention { + default: // unknown error + logrus.Errorf("Unknown handshake intention: %v", intention) + case 1: // for status + handleProxyConnection(clientConn, targetConn) + case 2: // for login + handlePlaying(clientConn, targetConn) + } +} + +type PlayerInfo struct { + Name string + UUID uuid.UUID + OPLevel int +} + +func handlePlaying(conn, target net.Conn) { + // login, get player info + info, err := acceptLogin(conn, target) + if err != nil { + logrus.Errorf("user [%s] Login failed", info.Name) + return + } + + logrus.Infof("Login successful: %s", info.Name) + + // Write LoginSuccess packet + + handleProxyConnection(conn, target) +} + +// acceptLogin check player's account +func acceptLogin(conn, target net.Conn) (info PlayerInfo, err error) { + // login start + var p pk.Packet + err = conn.ReadPacket(&p) + if err != nil { + return + } + + err = p.Scan((*pk.String)(&info.Name)) // decode username as pk.String + if err != nil { + return + } + + if ok, err := requestVerify(info.Name); !ok || err != nil { + err = errors.New("verify failed") + conn.Close() + target.Close() + return info, err + } + + if err := target.WritePacket(p); err != nil { + return info, err + } + + // auth + const OnlineMode = false + if OnlineMode { + info.UUID = offline.NameToUUID(info.Name) + } else { + // offline-mode UUID + info.UUID = offline.NameToUUID(info.Name) + } + + return +} + +func handshake(conn, target net.Conn) (protocol, intention int32, err error) { + var ( + p pk.Packet + Protocol, Intention pk.VarInt + ServerAddress pk.String // ignored + ServerPort pk.UnsignedShort // ignored + ) + // receive handshake packet + if err = conn.ReadPacket(&p); err != nil { + return + } + err = p.Scan(&Protocol, &ServerAddress, &ServerPort, &Intention) + if err != nil { + return + } + err = target.WritePacket(p) + + return int32(Protocol), int32(Intention), err +} + +func handleProxyConnection(clientConn net.Conn, targetConn net.Conn) { + defer clientConn.Close() + + go func() { + for { + var p pk.Packet + if err := clientConn.ReadPacket(&p); err != nil { + logrus.Errorf("ReadPacket error: %v", err) + break + } + if err := targetConn.WritePacket(p); err != nil { + logrus.Errorf("WritePacket error: %v", err) + break + } + } + }() + + func() { + for { + var p pk.Packet + if err := targetConn.ReadPacket(&p); err != nil { + logrus.Errorf("ReadPacket error: %v", err) + break + } + if err := clientConn.WritePacket(p); err != nil { + logrus.Errorf("WritePacket error: %v", err) + break + } + } + }() +} + +func startProxyServer(proxyHost string, proxyPort int) { + + settings := conf.GetBotSettings().GatewaySettings + + l, err := net.ListenMC(fmt.Sprintf(":%d", settings.ProxyPort)) + if err != nil { + logrus.Fatalf("Error starting proxy server: %s", err) + } + defer l.Close() + logrus.Infof("Proxy server started on %s:%d", proxyHost, proxyPort) + + for { + clientConn, err := l.Accept() + if err != nil { + logrus.Errorf("Error accepting connection: %s", err) + continue + } + go HandleClientConnection(clientConn) + } +} + +func StartGateway() { + settings := conf.GetBotSettings().GatewaySettings + startProxyServer(settings.ProxyHost, settings.ProxyPort) +} diff --git a/services/handler.go b/services/handler.go index 30ee690..1a4ca76 100644 --- a/services/handler.go +++ b/services/handler.go @@ -1,18 +1,28 @@ package services import ( + "tg-mc/conf" + "tg-mc/services/gateway" "tg-mc/services/mc" "tg-mc/services/tgbot" - "tg-mc/services/utils" ) func Run() { - go func() { - for { - if err := mc.Run(); err != nil { - utils.SendMsg("致命错误:" + err.Error()) - } + settings := conf.GetBotSettings() + + if settings.EnableGateway { + go gateway.StartGateway() + } + + if settings.EnableBridge { + go mc.StartBridgeClient() + } + + if settings.EnableBot { + if settings.EnableBridge { + tgbot.Run(mc.SendMsg, mc.SendCommand) + } else { + tgbot.Run(mc.SendMsg, func(s string) error { return nil }) } - }() - tgbot.Run(mc.SendMsg, mc.SendCommand) + } } diff --git a/services/mc/auth.go b/services/mc/auth.go deleted file mode 100644 index 471e76f..0000000 --- a/services/mc/auth.go +++ /dev/null @@ -1,53 +0,0 @@ -package mc - -// import ( -// "sync" -// "tg-mc/models" -// "time" -// ) - -// type Auth interface { -// IsAuthed(u models.User, expireMode bool) bool -// Auth(u models.User) -// Reject(u models.User) -// } - -// type Authcator struct { -// UserMap *sync.Map -// } - -// var authcator *Authcator - -// func GetAuthcator() Auth { -// if authcator == nil { -// authcator = &Authcator{ -// UserMap: &sync.Map{}, -// } -// } -// return authcator -// } - -// func (a *Authcator) IsAuthed(u models.User, expireMode bool) bool { -// return true -// // if u.MCName != "VaalaCat" { -// // return true -// // } -// if approveTime, ok := a.UserMap.Load(u.MCName); ok { -// if !expireMode { -// return true -// } else if time.Since(approveTime.(time.Time)) < 30*time.Second { -// return true -// } else { -// return false -// } -// } -// return false -// } - -// func (a *Authcator) Auth(u models.User) { -// a.UserMap.Store(u.MCName, time.Now()) -// } - -// func (a *Authcator) Reject(u models.User) { -// a.UserMap.Delete(u.MCName) -// } diff --git a/services/mc/mc.go b/services/mc/mc.go index aa4c955..a38e3b3 100644 --- a/services/mc/mc.go +++ b/services/mc/mc.go @@ -5,6 +5,7 @@ import ( "log" "strings" "tg-mc/conf" + "tg-mc/services/utils" "time" "github.com/Tnze/go-mc/bot" @@ -17,6 +18,15 @@ import ( "github.com/sirupsen/logrus" ) +func StartBridgeClient() { + for { + if err := Run(); err != nil { + utils.SendMsg("致命错误:" + err.Error()) + } + time.Sleep(time.Second * 5) + } +} + func Run() error { conf.Client = bot.NewClient() conf.Client.Auth.Name = conf.GetBotSettings().MCBotName @@ -44,7 +54,8 @@ func Run() error { ) if err != nil { - log.Fatal(err) + log.Printf("joinserver error: %v", err) + return err } log.Println("Login success") diff --git a/services/tgbot/approve.go b/services/tgbot/approve.go index ae585fd..46f8561 100644 --- a/services/tgbot/approve.go +++ b/services/tgbot/approve.go @@ -1,5 +1,33 @@ package tgbot +import ( + "fmt" + "tg-mc/conf" + "tg-mc/defs" + "tg-mc/models" + "tg-mc/services/gateway" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/sirupsen/logrus" +) + +func ApproveHandler(update tgbotapi.Update, cmd defs.Command) { + u, err := models.GetUserByTGID(update.CallbackQuery.From.ID) + if err != nil { + return + } + gateway.GetAuthcator().SetAuth(u) + + callback := tgbotapi.NewCallback(update.CallbackQuery.ID, "已授权") + if _, err := conf.Bot.Request(callback); err != nil { + logrus.Panic(err) + } + conf.Bot.Send(tgbotapi.NewDeleteMessage(update.CallbackQuery.Message.Chat.ID, + update.CallbackQuery.Message.MessageID)) + conf.Bot.Send(tgbotapi.NewMessage(update.CallbackQuery.Message.Chat.ID, + fmt.Sprintf("已授权☑️: %s 登录MC", u.MCName))) +} + // func ApproveHandler(update tgbotapi.Update, cmd defs.Command) { // u, err := models.GetUserByTGID(update.CallbackQuery.From.ID) // if err != nil { diff --git a/services/tgbot/bot.go b/services/tgbot/bot.go index b3c32cd..0030456 100644 --- a/services/tgbot/bot.go +++ b/services/tgbot/bot.go @@ -23,8 +23,8 @@ var funcHandlers = map[string]func(*tgbotapi.Message, interface{}){ } var callBackHandlers = map[string]func(tgbotapi.Update, defs.Command){ - // defs.CMD_APPROVE: ApproveHandler, - // defs.CMD_REJECT: RejectHandler, + defs.CMD_APPROVE: ApproveHandler, + defs.CMD_REJECT: RejectHandler, } func init() { diff --git a/services/tgbot/reject.go b/services/tgbot/reject.go index 0d92b1f..2813cfa 100644 --- a/services/tgbot/reject.go +++ b/services/tgbot/reject.go @@ -5,6 +5,7 @@ import ( "tg-mc/conf" "tg-mc/defs" "tg-mc/models" + "tg-mc/services/gateway" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/sirupsen/logrus" @@ -15,6 +16,8 @@ func RejectHandler(update tgbotapi.Update, cmd defs.Command) { if err != nil { return } + gateway.GetAuthcator().Reject(u) + callback := tgbotapi.NewCallback(update.CallbackQuery.ID, "已拒绝") if _, err := conf.Bot.Request(callback); err != nil { logrus.Panic(err) diff --git a/utils/database/db.go b/utils/database/db.go index bea388a..039fd11 100644 --- a/utils/database/db.go +++ b/utils/database/db.go @@ -2,7 +2,6 @@ package database import ( "github.com/joho/godotenv" - "github.com/sirupsen/logrus" "gorm.io/gorm" ) @@ -15,10 +14,10 @@ func GetDB() *gorm.DB { return GetSqlite() } -func CloseDB(db *gorm.DB) { - tdb, err := db.DB() - if err != nil { - logrus.WithError(err).Errorf("Close DB error") - } - tdb.Close() -} +// func CloseDB(db *gorm.DB) { +// tdb, err := db.DB() +// if err != nil { +// logrus.WithError(err).Errorf("Close DB error") +// } +// tdb.Close() +// } diff --git a/utils/database/sqlite.go b/utils/database/sqlite.go index af50dfb..f049f02 100644 --- a/utils/database/sqlite.go +++ b/utils/database/sqlite.go @@ -9,23 +9,25 @@ import ( "gorm.io/gorm" ) +var ( + db *gorm.DB +) + func initSqlite() { var err error godotenv.Load() dbPath := conf.GetBotSettings().DBPath - db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) + db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) if err != nil { logrus.Panic(err, "Initializing DB Error") } - CloseDB(db) + logrus.Info("Initialized DB at ", dbPath) } func GetSqlite() *gorm.DB { - dbPath := conf.GetBotSettings().DBPath - db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) - if err != nil { - return nil + if db == nil { + initSqlite() } return db }