diff --git a/cmd/bedrocktool/main.go b/cmd/bedrocktool/main.go index e6d3ceb..30cb759 100644 --- a/cmd/bedrocktool/main.go +++ b/cmd/bedrocktool/main.go @@ -5,6 +5,7 @@ import ( "context" "flag" "fmt" + "io" "os" "os/signal" "runtime/debug" @@ -31,9 +32,31 @@ type CLI struct { func (c *CLI) Init() bool { utils.SetCurrentUI(c) + utils.Auth.LoginWithMicrosoftCallback = func(r io.Reader) { + io.Copy(os.Stdout, r) + } return true } +/* +var m = &worlds.Map{} + +func (c *CLI) Message(data interface{}) messages.MessageResponse { + + switch me := data.(type) { + case messages.CanShowImages: + return messages.MessageResponse{Ok: true} + case messages.UpdateMap: + m.Update(&me) + } + + return messages.MessageResponse{ + Ok: false, + Data: nil, + } +} +*/ + func (c *CLI) Start(ctx context.Context, cancel context.CancelFunc) error { flag.Parse() subcommands.Execute(ctx) @@ -162,7 +185,7 @@ func (c *TransCMD) Execute(_ context.Context, ui utils.UI) error { Reset = "\033[0m" ) if c.auth { - utils.GetTokenSource() + utils.Auth.GetTokenSource() } fmt.Println(BlackFg + Bold + Blue + " Trans " + Pink + " Rights " + White + " Are " + Pink + " Human " + Blue + " Rights " + Reset) return nil diff --git a/ui/gui.go b/ui/gui.go index 2935086..0fdcd3d 100644 --- a/ui/gui.go +++ b/ui/gui.go @@ -1,10 +1,12 @@ -//go:build gui +//go:builda gui package ui import ( + "bufio" "context" "image/color" + "io" "gioui.org/app" "gioui.org/font/gofont" @@ -27,11 +29,14 @@ import ( type GUI struct { utils.BaseUI - router pages.Router - cancel context.CancelFunc + router pages.Router + cancel context.CancelFunc + authPopup bool + authPopupText string } func (g *GUI) Init() bool { + utils.Auth.LoginWithMicrosoftCallback = g.LoginWithMicrosoftCallback return true } @@ -98,7 +103,20 @@ func (g *GUI) run(w *app.Window) error { return e.Err case system.FrameEvent: gtx := layout.NewContext(&ops, e) - g.router.Layout(gtx, g.router.Theme) + layout.Stack{ + Alignment: layout.Center, + }.Layout(gtx, + layout.Expanded(func(gtx layout.Context) layout.Dimensions { + return g.router.Layout(gtx, g.router.Theme) + }), + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + if g.authPopup { + return g.AuthPopup(gtx) + } + return layout.Dimensions{} + }), + ) + e.Frame(gtx.Ops) } case <-g.router.Ctx.Done(): @@ -129,6 +147,29 @@ func (g *GUI) Message(data interface{}) messages.MessageResponse { return r } +func (g *GUI) AuthPopup(gtx layout.Context) layout.Dimensions { + gtx.Constraints.Max = gtx.Constraints.Max.Div(2) + return layout.Center.Layout(gtx, material.Body1(g.router.Theme, g.authPopupText).Layout) +} + +func (g *GUI) LoginWithMicrosoftCallback(r io.Reader) { + g.authPopup = true + b := bufio.NewReader(r) + for { + line, _, err := b.ReadLine() + if err != nil { + panic(err) + } + println(string(line)) + g.authPopupText += string(line) + "\n" + g.router.Invalidate() + if string(line) == "Authentication successful." { + break + } + } + g.authPopup = false +} + func init() { utils.MakeGui = func() utils.UI { return &GUI{} diff --git a/ui/gui/settings/address-input.go b/ui/gui/settings/address-input.go new file mode 100644 index 0000000..1497776 --- /dev/null +++ b/ui/gui/settings/address-input.go @@ -0,0 +1,128 @@ +package settings + +import ( + "context" + "fmt" + "image" + "image/color" + "sync" + + "gioui.org/layout" + "gioui.org/unit" + "gioui.org/widget" + "gioui.org/widget/material" + "github.com/bedrock-tool/bedrocktool/utils" + "github.com/sandertv/gophertunnel/minecraft/realms" + "github.com/sirupsen/logrus" +) + +type addressInput struct { + Editor widget.Editor + showRealmsList widget.Bool + l sync.Mutex + realmsList widget.List + realms []realms.Realm + realmsButtons map[int]*widget.Clickable + loading bool +} + +var AddressInput = &addressInput{ + Editor: widget.Editor{ + SingleLine: true, + }, + realmsList: widget.List{ + List: layout.List{ + Axis: layout.Vertical, + }, + }, +} + +func (a *addressInput) Value() string { + return a.Editor.Text() +} + +func (a *addressInput) getRealms() { + var err error + a.loading = true + a.realms, err = utils.GetRealmsAPI().Realms(context.Background()) + a.realmsButtons = make(map[int]*widget.Clickable) + for _, r := range a.realms { + a.realmsButtons[r.ID] = &widget.Clickable{} + } + a.loading = false + if err != nil { + logrus.Error(err) + } +} + +func MulAlpha(c color.NRGBA, alpha uint8) color.NRGBA { + c.A = uint8(uint32(c.A) * uint32(alpha) / 0xFF) + return c +} + +func (a *addressInput) Layout(th *material.Theme) layout.Widget { + for k, c := range a.realmsButtons { + if c.Clicked() { + for _, r := range a.realms { + if r.ID == k { + a.Editor.SetText(fmt.Sprintf("realm:%s:%d", r.Name, r.ID)) + } + } + } + } + + return func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + e := material.Editor(th, &a.Editor, "server Address") + return layout.UniformInset(5).Layout(gtx, e.Layout) + }), + layout.Rigid(layout.Spacer{Width: unit.Dp(10)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(material.Label(th, th.TextSize, "list realms").Layout), + layout.Rigid(material.Switch(th, &a.showRealmsList, "realms").Layout), + ) + }), + ) + }), + layout.Flexed(0.5, func(gtx layout.Context) layout.Dimensions { + if a.loading { + return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + gtx.Constraints.Max = image.Pt(20, 20) + return material.Loader(th).Layout(gtx) + }) + } + + if a.showRealmsList.Value { + if a.showRealmsList.Changed() { + go a.getRealms() + } + a.l.Lock() + defer a.l.Unlock() + if len(a.realms) == 0 { + return material.Label(th, th.TextSize, "you have no realms").Layout(gtx) + } + return material.List(th, &a.realmsList).Layout(gtx, len(a.realms), func(gtx layout.Context, index int) layout.Dimensions { + entry := a.realms[index] + return material.ButtonLayoutStyle{ + Background: MulAlpha(th.Palette.Bg, 0x60), + Button: a.realmsButtons[entry.ID], + CornerRadius: 3, + }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(15).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Rigid(material.Label(th, th.TextSize, entry.Name).Layout), + ) + }) + }) + }) + } + return layout.Dimensions{} + }), + ) + + } +} diff --git a/ui/gui/settings/packs.go b/ui/gui/settings/packs.go index e5b76e3..0cac1a1 100644 --- a/ui/gui/settings/packs.go +++ b/ui/gui/settings/packs.go @@ -2,7 +2,6 @@ package settings import ( "gioui.org/layout" - "gioui.org/widget" "gioui.org/widget/material" "github.com/bedrock-tool/bedrocktool/subcommands" "github.com/bedrock-tool/bedrocktool/utils" @@ -11,21 +10,21 @@ import ( type packsSettings struct { packs *subcommands.ResourcePackCMD - serverAddress widget.Editor + serverAddress *addressInput } func (s *packsSettings) Init() { s.packs = utils.ValidCMDs["packs"].(*subcommands.ResourcePackCMD) - s.serverAddress.SingleLine = true + s.serverAddress = AddressInput } func (s *packsSettings) Apply() { - s.packs.ServerAddress = s.serverAddress.Text() + s.packs.ServerAddress = s.serverAddress.Value() } func (s *packsSettings) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(material.Editor(th, &s.serverAddress, "Server Address").Layout), + layout.Rigid(s.serverAddress.Layout(th)), ) } diff --git a/ui/gui/settings/skins.go b/ui/gui/settings/skins.go index cbf98b8..fa01e06 100644 --- a/ui/gui/settings/skins.go +++ b/ui/gui/settings/skins.go @@ -14,12 +14,12 @@ type skinsSettings struct { Filter widget.Editor Proxy widget.Bool - serverAddress widget.Editor + serverAddress *addressInput } func (s *skinsSettings) Init() { s.skins = utils.ValidCMDs["skins"].(*skins.SkinCMD) - s.serverAddress.SingleLine = true + s.serverAddress = AddressInput s.Filter.SingleLine = true s.Proxy.Value = true } @@ -27,7 +27,7 @@ func (s *skinsSettings) Init() { func (s *skinsSettings) Apply() { s.skins.Filter = s.Filter.Text() s.skins.NoProxy = !s.Proxy.Value - s.skins.ServerAddress = s.serverAddress.Text() + s.skins.ServerAddress = s.serverAddress.Value() } func (s *skinsSettings) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions { @@ -35,7 +35,7 @@ func (s *skinsSettings) Layout(gtx layout.Context, th *material.Theme) layout.Di layout.Rigid(material.CheckBox(th, &s.Proxy, "Enable Proxy").Layout), layout.Rigid(material.Editor(th, &s.Filter, "Player name filter").Layout), layout.Rigid(layout.Spacer{Height: unit.Dp(15)}.Layout), - layout.Rigid(material.Editor(th, &s.serverAddress, "server Address").Layout), + layout.Rigid(s.serverAddress.Layout(th)), ) } diff --git a/ui/gui/settings/worlds.go b/ui/gui/settings/worlds.go index 8bdf727..2326fee 100644 --- a/ui/gui/settings/worlds.go +++ b/ui/gui/settings/worlds.go @@ -15,12 +15,12 @@ type worldSettings struct { voidGen widget.Bool saveImage widget.Bool PacketCapture widget.Bool - serverAddress widget.Editor + serverAddress *addressInput } func (s *worldSettings) Init() { s.worlds = utils.ValidCMDs["worlds"].(*world.WorldCMD) - s.serverAddress.SingleLine = true + s.serverAddress = AddressInput s.voidGen.Value = true s.PacketCapture.Value = false } @@ -29,7 +29,7 @@ func (s *worldSettings) Apply() { s.worlds.Packs = s.withPacks.Value s.worlds.EnableVoid = s.voidGen.Value s.worlds.SaveImage = s.saveImage.Value - s.worlds.ServerAddress = s.serverAddress.Text() + s.worlds.ServerAddress = s.serverAddress.Value() s.worlds.SaveEntities = true s.worlds.SaveInventories = true utils.Options.Capture = s.PacketCapture.Value @@ -41,7 +41,7 @@ func (s *worldSettings) Layout(gtx layout.Context, th *material.Theme) layout.Di layout.Rigid(material.CheckBox(th, &s.voidGen, "void Generator").Layout), layout.Rigid(material.CheckBox(th, &s.saveImage, "save image").Layout), layout.Rigid(material.CheckBox(th, &s.PacketCapture, "packet capture").Layout), - layout.Rigid(material.Editor(th, &s.serverAddress, "server Address").Layout), + layout.Rigid(s.serverAddress.Layout(th)), ) } diff --git a/utils/auth.go b/utils/auth.go index dec9c20..54ce519 100644 --- a/utils/auth.go +++ b/utils/auth.go @@ -3,35 +3,102 @@ package utils import ( "encoding/json" "fmt" + "io" "os" - "github.com/bedrock-tool/bedrocktool/locale" "github.com/sandertv/gophertunnel/minecraft/auth" "github.com/sandertv/gophertunnel/minecraft/realms" - "github.com/sirupsen/logrus" "golang.org/x/oauth2" ) const TokenFile = "token.json" -var gTokenSrc oauth2.TokenSource +type authsrv struct { + t *oauth2.Token + src oauth2.TokenSource -func GetTokenSource() oauth2.TokenSource { - if gTokenSrc != nil { - return gTokenSrc - } - token := getToken() - gTokenSrc = auth.RefreshTokenSource(&token) - newToken, err := gTokenSrc.Token() + LoginWithMicrosoftCallback func(io.Reader) +} + +var Auth authsrv + +func (a *authsrv) HaveToken() bool { + _, err := os.Stat(TokenFile) + return err == nil +} + +func (a *authsrv) Refresh() error { + a.src = auth.RefreshTokenSource(a.t) + return nil +} + +func (a *authsrv) writeToken() error { + f, err := os.Create(TokenFile) if err != nil { - panic(err) - } - if !token.Valid() { - logrus.Info(locale.Loc("refreshed_token", nil)) - writeToken(newToken) + return err } + defer f.Close() + e := json.NewEncoder(f) + return e.Encode(a.t) +} - return gTokenSrc +func (a *authsrv) readToken() error { + var token oauth2.Token + f, err := os.Open(TokenFile) + if err != nil { + return err + } + defer f.Close() + e := json.NewDecoder(f) + err = e.Decode(&token) + if err != nil { + return err + } + a.t = &token + return nil +} + +func (a *authsrv) GetTokenSource() (src oauth2.TokenSource, err error) { + if a.src != nil { + return a.src, nil + } + if !a.HaveToken() { + // request a new token + r, w := io.Pipe() + go a.LoginWithMicrosoftCallback(r) + a.t, err = auth.RequestLiveTokenWriter(w) + if err != nil { + return nil, err + } + err := a.writeToken() + if err != nil { + return nil, err + } + } else { + // read the existing token + err := a.readToken() + if err != nil { + return nil, err + } + } + // refresh the token if necessary + err = a.Refresh() + if err != nil { + return nil, err + } + // if the old token isnt valid save the new one + if !a.t.Valid() { + newToken, err := a.src.Token() + if err != nil { + return nil, err + } + a.t = newToken + err = a.writeToken() + if err != nil { + return nil, err + } + } + return a.src, nil } var RealmsEnv string @@ -43,37 +110,7 @@ func GetRealmsAPI() *realms.Client { if RealmsEnv != "" { realms.RealmsAPIBase = fmt.Sprintf("https://pocket-%s.realms.minecraft.net/", RealmsEnv) } - gRealmsAPI = realms.NewClient(GetTokenSource()) + gRealmsAPI = realms.NewClient(Auth.src) } return gRealmsAPI } - -func writeToken(token *oauth2.Token) { - buf, err := json.Marshal(token) - if err != nil { - panic(err) - } - os.WriteFile(TokenFile, buf, 0o755) -} - -func getToken() oauth2.Token { - var token oauth2.Token - if _, err := os.Stat(TokenFile); err == nil { - f, err := os.Open(TokenFile) - if err != nil { - panic(err) - } - defer f.Close() - if err := json.NewDecoder(f).Decode(&token); err != nil { - panic(err) - } - } else { - _token, err := auth.RequestLiveToken() - if err != nil { - panic(err) - } - writeToken(_token) - token = *_token - } - return token -} diff --git a/utils/proxy.go b/utils/proxy.go index df1ba94..0e5fdd0 100644 --- a/utils/proxy.go +++ b/utils/proxy.go @@ -23,6 +23,7 @@ import ( "github.com/sandertv/gophertunnel/minecraft/protocol/packet" "github.com/sandertv/gophertunnel/minecraft/resource" "github.com/sirupsen/logrus" + "golang.org/x/oauth2" ) var DisconnectReason = "Connection lost" @@ -310,13 +311,11 @@ func (p *ProxyContext) IsClient(addr net.Addr) bool { var NewDebugLogger func(bool) *ProxyHandler var NewPacketCapturer func() *ProxyHandler -func (p *ProxyContext) connectClient(ctx context.Context, serverAddress string, cdpp **login.ClientData) (err error) { - GetTokenSource() // ask for login before listening - +func (p *ProxyContext) connectClient(ctx context.Context, serverAddress string, cdpp **login.ClientData, tokenSource oauth2.TokenSource) (err error) { var packs []*resource.Pack if Options.Preload { logrus.Info(locale.Loc("preloading_packs", nil)) - serverConn, err := connectServer(ctx, serverAddress, nil, true, func(header packet.Header, payload []byte, src, dst net.Addr) {}) + serverConn, err := connectServer(ctx, serverAddress, nil, true, nil, tokenSource) if err != nil { return fmt.Errorf(locale.Loc("failed_to_connect", locale.Strmap{"Address": serverAddress, "Err": err})) } @@ -389,10 +388,19 @@ func (p *ProxyContext) Run(ctx context.Context, serverAddress, name string) (err isReplay = true } + var tokenSource oauth2.TokenSource + if !isReplay { + // ask for login before listening + tokenSource, err = Auth.GetTokenSource() + if err != nil { + return err + } + } + var cdp *login.ClientData = nil if p.WithClient && !isReplay { CurrentUI.Message(messages.SetUIState(messages.UIStateConnect)) - err = p.connectClient(ctx, serverAddress, &cdp) + err = p.connectClient(ctx, serverAddress, &cdp, tokenSource) if err != nil { return err } @@ -436,7 +444,7 @@ func (p *ProxyContext) Run(ctx context.Context, serverAddress, name string) (err return err } } else { - p.Server, err = connectServer(ctx, serverAddress, cdp, p.AlwaysGetPacks, packetFunc) + p.Server, err = connectServer(ctx, serverAddress, cdp, p.AlwaysGetPacks, packetFunc, tokenSource) } if err != nil { for _, handler := range p.handlers { diff --git a/utils/utils.go b/utils/utils.go index b347a80..bf3b2a4 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -22,6 +22,7 @@ import ( "github.com/google/uuid" "github.com/sandertv/gophertunnel/minecraft" "github.com/sirupsen/logrus" + "golang.org/x/oauth2" //"github.com/sandertv/gophertunnel/minecraft/gatherings" @@ -57,7 +58,7 @@ func CleanupName(name string) string { // connections -func connectServer(ctx context.Context, address string, ClientData *login.ClientData, wantPacks bool, packetFunc PacketFunc) (serverConn *minecraft.Conn, err error) { +func connectServer(ctx context.Context, address string, ClientData *login.ClientData, wantPacks bool, packetFunc PacketFunc, tokenSource oauth2.TokenSource) (serverConn *minecraft.Conn, err error) { cd := login.ClientData{} if ClientData != nil { cd = *ClientData @@ -65,7 +66,7 @@ func connectServer(ctx context.Context, address string, ClientData *login.Client logrus.Info(locale.Loc("connecting", locale.Strmap{"Address": address})) serverConn, err = minecraft.Dialer{ - TokenSource: GetTokenSource(), + TokenSource: tokenSource, ClientData: cd, PacketFunc: packetFunc, DownloadResourcePack: func(id uuid.UUID, version string, current int, total int) bool { @@ -188,6 +189,6 @@ func ShowFile(path string) { return } if runtime.GOOS == "linux" { - + println(path) } }