diff --git a/cmd/bedrocktool/main.go b/cmd/bedrocktool/main.go index ab6d9a7..eb95821 100644 --- a/cmd/bedrocktool/main.go +++ b/cmd/bedrocktool/main.go @@ -90,8 +90,12 @@ func main() { log.Infof(locale.Loc("bedrocktool_version", locale.Strmap{"Version": updater.Version})) } + err := utils.Auth.Startup() + if err != nil { + logrus.Fatal(err) + } + ctx, cancel := context.WithCancelCause(context.Background()) - utils.Auth.InitCtx(ctx) recovery.ErrorHandler = func(err error) { if isDebug { @@ -107,7 +111,7 @@ func main() { os.Exit(1) } - flag.StringVar(&utils.RealmsEnv, "realms-env", "", "realms env") + //flag.StringVar(&utils.RealmsEnv, "realms-env", "", "realms env") flag.BoolVar(&utils.Options.Debug, "debug", false, locale.Loc("debug_mode", nil)) flag.BoolVar(&utils.Options.ExtraDebug, "extra-debug", false, locale.Loc("extra_debug", nil)) flag.StringVar(&utils.Options.PathCustomUserData, "userdata", "", locale.Loc("custom_user_data", nil)) @@ -133,7 +137,7 @@ func main() { log.Error("Failed to init UI!") return } - err := ui.Start(ctx, cancel) + err = ui.Start(ctx, cancel) cancel(err) if err != nil { log.Error(err) @@ -149,7 +153,7 @@ func (*TransCMD) Synopsis() string { return "" } func (c *TransCMD) SetFlags(f *flag.FlagSet) { f.BoolVar(&c.auth, "auth", false, locale.Loc("should_login_xbox", nil)) } -func (c *TransCMD) Execute(_ context.Context) error { +func (c *TransCMD) Execute(ctx context.Context) error { const ( BlackFg = "\033[30m" Bold = "\033[1m" @@ -159,10 +163,10 @@ func (c *TransCMD) Execute(_ context.Context) error { Reset = "\033[0m" ) if c.auth { - _, err := utils.Auth.GetTokenSource() - if err != nil { - logrus.Error(err) - return nil + if utils.Auth.LoggedIn() { + logrus.Info("Already Logged in") + } else { + utils.Auth.Login(ctx, nil) } } fmt.Println(BlackFg + Bold + Blue + " Trans " + Pink + " Rights " + White + " Are " + Pink + " Human " + Blue + " Rights " + Reset) diff --git a/cmd/generate-color-lookup/main.go b/cmd/generate-color-lookup/main.go index 3d0f684..92bcae0 100644 --- a/cmd/generate-color-lookup/main.go +++ b/cmd/generate-color-lookup/main.go @@ -23,7 +23,7 @@ func main() { log.Fatal(err) } - var packs []utils.Pack + var packs []resource.Pack for _, fi := range packNames { p := folder + "/" + fi.Name() pack, err := utils.PackFromBase(resource.MustReadPath(p)) diff --git a/gophertunnel b/gophertunnel index 103a68f..f831996 160000 --- a/gophertunnel +++ b/gophertunnel @@ -1 +1 @@ -Subproject commit 103a68f81f2d147154b324f570b35a871c037d69 +Subproject commit f831996530e9dc42829176a9d6d03dc97c3232b1 diff --git a/subcommands/realms-list.go b/subcommands/realms-list.go index f1ef432..5ec99ef 100644 --- a/subcommands/realms-list.go +++ b/subcommands/realms-list.go @@ -16,7 +16,17 @@ func (*RealmListCMD) Name() string { return "list-realms" } func (*RealmListCMD) Synopsis() string { return locale.Loc("list_realms_synopsis", nil) } func (c *RealmListCMD) SetFlags(f *flag.FlagSet) {} func (c *RealmListCMD) Execute(ctx context.Context) error { - realms, err := utils.GetRealmsAPI().Realms(ctx) + if !utils.Auth.LoggedIn() { + err := utils.Auth.Login(ctx, nil) + if err != nil { + return err + } + } + realmsClient, err := utils.Auth.Realms() + if err != nil { + return err + } + realms, err := realmsClient.Realms(ctx) if err != nil { return err } diff --git a/ui/cli/cli.go b/ui/cli/cli.go index cba0880..7a6f7e5 100644 --- a/ui/cli/cli.go +++ b/ui/cli/cli.go @@ -5,6 +5,7 @@ import ( "flag" "github.com/bedrock-tool/bedrocktool/ui/messages" + "github.com/bedrock-tool/bedrocktool/utils" "github.com/bedrock-tool/bedrocktool/utils/updater" "github.com/google/subcommands" ) @@ -28,5 +29,13 @@ func (c *CLI) Start(ctx context.Context, cancel context.CancelCauseFunc) error { } func (c *CLI) HandleMessage(msg *messages.Message) *messages.Message { + switch data := msg.Data.(type) { + case messages.RequestLogin: + if data.Wait { + utils.Auth.Login(context.Background(), nil) + } else { + go utils.Auth.Login(context.Background(), nil) + } + } return nil } diff --git a/ui/gui/pages/msauth.go b/ui/gui/pages/msauth.go deleted file mode 100644 index d42df75..0000000 --- a/ui/gui/pages/msauth.go +++ /dev/null @@ -1,30 +0,0 @@ -package pages - -import ( - "github.com/bedrock-tool/bedrocktool/ui/gui/popups" - "github.com/bedrock-tool/bedrocktool/ui/messages" -) - -type msAuth struct { - router *Router - p popups.Popup -} - -func (m *msAuth) Success() { - m.router.RemovePopup(m.p.ID()) - m.p = nil -} - -func (m *msAuth) PollError(err error) error { - m.p.HandleMessage(&messages.Message{ - Source: "msAuth", - Data: messages.Error(err), - }) - m.router.Invalidate() - return err -} - -func (m *msAuth) AuthCode(uri, code string) { - m.p = popups.NewGuiAuth(uri, code) - m.router.PushPopup(m.p) -} diff --git a/ui/gui/pages/router.go b/ui/gui/pages/router.go index ad23f83..0c69470 100644 --- a/ui/gui/pages/router.go +++ b/ui/gui/pages/router.go @@ -3,6 +3,7 @@ package pages import ( "context" "errors" + "image/color" "log" "reflect" "sync" @@ -32,7 +33,6 @@ type Router struct { cmdCtx context.Context cmdCtxCancel context.CancelFunc Wg sync.WaitGroup - msAuth *msAuth Invalidate func() LogWidget func(layout.Context, *material.Theme) layout.Dimensions @@ -45,6 +45,8 @@ type Router struct { ModalLayer *component.ModalLayer NonModalDrawer bool + loggingIn bool + loginButton widget.Clickable updateButton widget.Clickable updateAvailable bool @@ -61,7 +63,6 @@ func NewRouter(uii ui.UI) *Router { r := &Router{ ui: uii, pages: make(map[string]func() Page), - msAuth: &msAuth{}, ModalLayer: modal, ModalNavDrawer: component.ModalNavFrom(&nav, modal), AppBar: component.NewAppBar(modal), @@ -70,8 +71,6 @@ func NewRouter(uii ui.UI) *Router { Duration: time.Millisecond * 250, }, } - r.msAuth.router = r - utils.Auth.MSHandler = r.msAuth return r } @@ -183,8 +182,35 @@ func (r *Router) Layout(gtx layout.Context, th *material.Theme) layout.Dimension return layout.Dimensions{Size: gtx.Constraints.Max} } +func (r *Router) layoutLoginButton(gtx layout.Context, fg, bg color.NRGBA) layout.Dimensions { + if r.loginButton.Clicked(gtx) { + if !utils.Auth.LoggedIn() { + if !r.loggingIn { + messages.Router.Handle(&messages.Message{ + Source: "ui", + Target: "ui", + Data: messages.RequestLogin{}, + }) + } + } else { + utils.Auth.Logout() + } + } + + var text = "Login" + if utils.Auth.LoggedIn() { + text = "Logout" + } + button := material.Button(r.th, &r.loginButton, text) + button.Background.R -= 20 + button.Background.G -= 20 + button.Background.B -= 32 + return button.Layout(gtx) +} + func (r *Router) setActions() { var extra []component.AppBarAction + extra = append(extra, component.AppBarAction{Layout: r.layoutLoginButton}) extra = append(extra, AppBarSwitch(&r.logToggle, "Logs", &r.th)) if r.updateAvailable { @@ -230,6 +256,34 @@ func (r *Router) HandleMessage(msg *messages.Message) *messages.Message { case "popup": r.RemovePopup(data.ID) } + + case messages.RequestLogin: + if r.loggingIn { + logrus.Info("RequestLogin, while already logging in") + break + } + r.loggingIn = true + ctx, cancel := context.WithCancel(r.Ctx) + loginPopup := popups.NewGuiAuth(r.Invalidate, cancel, func(err error) {}) + r.PushPopup(loginPopup) + c := make(chan struct{}) + go func() { + err := utils.Auth.Login(ctx, loginPopup) + if err != nil { + if !errors.Is(err, context.Canceled) { + r.PushPopup(popups.NewErrorPopup(err, nil, false)) + } + } + r.loggingIn = false + r.Invalidate() + close(c) + }() + if data.Wait { + <-c + } + + case messages.Error: + r.PushPopup(popups.NewErrorPopup(data, nil, false)) } for _, p := range r.popups { diff --git a/ui/gui/popups/authpopup.go b/ui/gui/popups/authpopup.go index 1feb981..ef9c7f6 100644 --- a/ui/gui/popups/authpopup.go +++ b/ui/gui/popups/authpopup.go @@ -9,19 +9,36 @@ import ( ) type guiAuth struct { + invalidate func() + cancel func() + onError func(error) uri string click widget.Clickable code string codeSelect widget.Selectable - err error close widget.Clickable } -func NewGuiAuth(uri, code string) Popup { - return &guiAuth{ - uri: uri, - code: code, +func (g *guiAuth) AuthCode(uri string, code string) { + g.uri = uri + g.code = code + g.invalidate() +} + +func (g *guiAuth) Finished(err error) { + if err != nil { + g.onError(err) } + messages.Router.Handle(&messages.Message{ + Source: "ui", + Target: "ui", + Data: messages.Close{Type: "popup", ID: g.ID()}, + }) + g.cancel() +} + +func NewGuiAuth(invalidate func(), cancel func(), onError func(error)) *guiAuth { + return &guiAuth{invalidate: invalidate, cancel: cancel, onError: onError} } func (guiAuth) ID() string { @@ -34,12 +51,12 @@ func (g *guiAuth) Layout(gtx layout.Context, th *material.Theme) layout.Dimensio } if g.close.Clicked(gtx) { - utils.Auth.Cancel() messages.Router.Handle(&messages.Message{ Source: "ui", Target: "ui", Data: messages.Close{Type: "popup", ID: g.ID()}, }) + g.cancel() } return LayoutPopupBackground(gtx, th, "guiAuth", func(gtx C) D { @@ -48,6 +65,9 @@ func (g *guiAuth) Layout(gtx layout.Context, th *material.Theme) layout.Dimensio }.Layout(gtx, layout.Flexed(1, func(gtx C) D { return layout.Center.Layout(gtx, func(gtx C) D { + if g.code == "" { + return material.Body1(th, "Loading").Layout(gtx) + } return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx, layout.Rigid(func(gtx C) D { return layout.Flex{ @@ -87,9 +107,5 @@ func (g *guiAuth) Layout(gtx layout.Context, th *material.Theme) layout.Dimensio } func (p *guiAuth) HandleMessage(msg *messages.Message) *messages.Message { - switch m := msg.Data.(type) { - case messages.Error: - p.err = m - } return nil } diff --git a/ui/gui/popups/errorpopup.go b/ui/gui/popups/errorpopup.go index 1ce451c..04af850 100644 --- a/ui/gui/popups/errorpopup.go +++ b/ui/gui/popups/errorpopup.go @@ -31,7 +31,9 @@ func (errorPopup) ID() string { func (e *errorPopup) Layout(gtx C, th *material.Theme) D { if e.close.Clicked(gtx) { - e.onClose() + if e.onClose != nil { + e.onClose() + } messages.Router.Handle(&messages.Message{ Source: e.ID(), Target: "ui", diff --git a/ui/gui/popups/realmsinput.go b/ui/gui/popups/realmsinput.go index 41cbab5..9dce0cc 100644 --- a/ui/gui/popups/realmsinput.go +++ b/ui/gui/popups/realmsinput.go @@ -12,7 +12,6 @@ import ( "github.com/bedrock-tool/bedrocktool/ui/messages" "github.com/bedrock-tool/bedrocktool/utils" "github.com/sandertv/gophertunnel/minecraft/realms" - "github.com/sirupsen/logrus" ) type RealmsList struct { @@ -44,18 +43,22 @@ func (*RealmsList) ID() string { var _ Popup = &RealmsList{} -func (r *RealmsList) Load() { - var err error - r.realms, err = utils.GetRealmsAPI().Realms(context.Background()) +func (r *RealmsList) Load() error { + realmsClient, err := utils.Auth.Realms() + if err != nil { + return err + } + r.realms, err = realmsClient.Realms(context.Background()) + if err != nil { + return err + } clear(r.buttons) for _, realm := range r.realms { r.buttons[realm.ID] = &widget.Clickable{} } r.loading = false r.loaded = true - if err != nil { - logrus.Error(err) - } + return nil } func (r *RealmsList) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions { @@ -79,6 +82,35 @@ func (r *RealmsList) Layout(gtx layout.Context, th *material.Theme) layout.Dimen }) } + if !r.loaded && !r.loading { + r.loading = true + go func() { + if !utils.Auth.LoggedIn() { + messages.Router.Handle(&messages.Message{ + Source: r.ID(), + Target: "ui", + Data: messages.RequestLogin{Wait: true}, + }) + } + err := r.Load() + if err != nil { + messages.Router.Handle(&messages.Message{ + Source: r.ID(), + Target: "ui", + Data: messages.Error(err), + }) + messages.Router.Handle(&messages.Message{ + Source: r.ID(), + Target: "ui", + Data: messages.Close{ + Type: "popup", + ID: r.ID(), + }, + }) + } + }() + } + return LayoutPopupBackground(gtx, th, "Realms", func(gtx C) D { return layout.Flex{ Axis: layout.Vertical, @@ -91,11 +123,6 @@ func (r *RealmsList) Layout(gtx layout.Context, th *material.Theme) layout.Dimen }) } - if !r.loaded && !r.loading { - r.loading = true - go r.Load() - } - r.l.Lock() defer r.l.Unlock() if len(r.realms) == 0 { diff --git a/ui/messages/messages.go b/ui/messages/messages.go index 523e070..bc0629e 100644 --- a/ui/messages/messages.go +++ b/ui/messages/messages.go @@ -143,6 +143,10 @@ type HaveFinishScreen struct{} type Error error +type RequestLogin struct { + Wait bool +} + type ServerInput struct { Request bool // if this is a request for input IsReplay bool diff --git a/utils/auth.go b/utils/auth.go index 8f4c8f5..1d09c44 100644 --- a/utils/auth.go +++ b/utils/auth.go @@ -3,7 +3,7 @@ package utils import ( "context" "encoding/json" - "fmt" + "errors" "os" "github.com/sandertv/gophertunnel/minecraft/auth" @@ -15,33 +15,77 @@ import ( const TokenFile = "token.json" type authsrv struct { - src oauth2.TokenSource - baseCtx context.Context - ctx context.Context - cancel context.CancelFunc - MSHandler auth.MSAuthHandler - log *logrus.Entry + liveToken *oauth2.Token + tokenSource oauth2.TokenSource + realms *realms.Client + log *logrus.Entry } var Auth authsrv = authsrv{ log: logrus.WithField("part", "Auth"), } -func (a *authsrv) InitCtx(ctx context.Context) { - a.baseCtx = ctx - a.ctx, a.cancel = context.WithCancel(a.baseCtx) +// reads token from storage if there is one +func (a *authsrv) Startup() (err error) { + a.liveToken, err = a.readToken() + if errors.Is(err, os.ErrNotExist) { + return nil + } + a.tokenSource = auth.RefreshTokenSource(a.liveToken) + a.realms = realms.NewClient(a.tokenSource) + _, err = a.TokenSource() + if err != nil { + return err + } + return nil } -func (a *authsrv) Cancel() { - a.cancel() - a.ctx, a.cancel = context.WithCancel(a.baseCtx) +// if the user is currently logged in or not +func (a *authsrv) LoggedIn() bool { + return a.tokenSource != nil } -func (a *authsrv) HaveToken() bool { - _, err := os.Stat(TokenFile) - return err == nil +// performs microsoft login using the handler passed +func (a *authsrv) Login(ctx context.Context, handler auth.MSAuthHandler) (err error) { + a.liveToken, err = auth.RequestLiveTokenWriter(ctx, handler) + if err != nil { + return err + } + err = a.writeToken(a.liveToken) + if err != nil { + return err + } + a.tokenSource = auth.RefreshTokenSource(a.liveToken) + a.realms = realms.NewClient(a.tokenSource) + return nil +} + +func (a *authsrv) Logout() { + a.liveToken = nil + a.tokenSource = nil + a.realms = nil + os.Remove(TokenFile) } +func (a *authsrv) refreshLiveToken() (err error) { + if a.liveToken.Valid() { + return nil + } + a.log.Info("Refreshing Microsoft Token") + a.liveToken, err = a.tokenSource.Token() + if err != nil { + return err + } + err = a.writeToken(a.liveToken) + if err != nil { + return err + } + a.tokenSource = auth.RefreshTokenSource(a.liveToken) + a.realms = realms.NewClient(a.tokenSource) + return nil +} + +// writes the livetoken to storage func (a *authsrv) writeToken(token *oauth2.Token) error { f, err := os.Create(TokenFile) if err != nil { @@ -52,6 +96,7 @@ func (a *authsrv) writeToken(token *oauth2.Token) error { return e.Encode(token) } +// reads the live token from storage, returns os.ErrNotExist if no token is stored func (a *authsrv) readToken() (*oauth2.Token, error) { var token oauth2.Token f, err := os.Open(TokenFile) @@ -67,60 +112,22 @@ func (a *authsrv) readToken() (*oauth2.Token, error) { return &token, nil } -func (a *authsrv) GetTokenSource() (src oauth2.TokenSource, err error) { - if a.src != nil { - return a.src, nil - } - var token *oauth2.Token - if a.HaveToken() { - // read the existing token - token, err = a.readToken() - if err != nil { - return nil, err - } - } else { - // request a new token - token, err = auth.RequestLiveTokenWriter(a.ctx, a.MSHandler) - if err != nil { - return nil, err - } - err := a.writeToken(token) - if err != nil { - return nil, err - } - } - a.src = auth.RefreshTokenSource(token) +var ErrNotLoggedIn = errors.New("not logged in") - // if the old token isnt valid save the new one - if !token.Valid() { - a.log.Debug("Refreshing token") - token, err = a.src.Token() - if err != nil { - return nil, err - } - err = a.writeToken(token) - if err != nil { - return nil, err - } +func (a *authsrv) TokenSource() (src oauth2.TokenSource, err error) { + if a.tokenSource == nil { + return nil, ErrNotLoggedIn } - - return a.src, nil + err = a.refreshLiveToken() + if err != nil { + return nil, err + } + return a.tokenSource, nil } -var RealmsEnv string - -var realmsAPI *realms.Client - -func GetRealmsAPI() *realms.Client { - if realmsAPI == nil { - if RealmsEnv != "" { - realms.RealmsAPIBase = fmt.Sprintf("https://pocket-%s.realms.minecraft.net/", RealmsEnv) - } - src, err := Auth.GetTokenSource() - if err != nil { - logrus.WithField("part", "Realms-api").Fatal(err) - } - realmsAPI = realms.NewClient(src) +func (a *authsrv) Realms() (*realms.Client, error) { + if a.realms != nil { + return a.realms, nil } - return realmsAPI + return nil, ErrNotLoggedIn } diff --git a/utils/proxy/context.go b/utils/proxy/context.go index a9a12fc..1a21cce 100644 --- a/utils/proxy/context.go +++ b/utils/proxy/context.go @@ -319,7 +319,16 @@ func (p *Context) doSession(ctx context.Context, cancel context.CancelCauseFunc) if !isReplay { // ask for login before listening - p.tokenSource, err = utils.Auth.GetTokenSource() + if !utils.Auth.LoggedIn() { + messages.Router.Handle(&messages.Message{ + Source: "proxy", + Target: "ui", + Data: messages.RequestLogin{ + Wait: true, + }, + }) + } + p.tokenSource, err = utils.Auth.TokenSource() if err != nil { return err } diff --git a/utils/realms.go b/utils/realms.go index d2a7991..101945a 100644 --- a/utils/realms.go +++ b/utils/realms.go @@ -7,7 +7,11 @@ import ( ) func getRealm(ctx context.Context, realmName, id string) (name string, address string, err error) { - realms, err := GetRealmsAPI().Realms(ctx) + realmsClient, err := Auth.Realms() + if err != nil { + return "", "", err + } + realms, err := realmsClient.Realms(ctx) if err != nil { return "", "", err }