From 31c378db5a5a8359f714b5d0e489fed064259ae9 Mon Sep 17 00:00:00 2001 From: diamondburned Date: Tue, 27 Oct 2020 14:35:22 -0700 Subject: [PATCH] changed Authenticator impl to the new cchat API --- discord.go | 5 +- go.mod | 2 +- go.sum | 2 + .../discord/authenticate/authenticator.go | 109 ++---------------- internal/discord/authenticate/discordlogin.go | 33 ++++-- internal/discord/authenticate/login.go | 65 +++++++++++ internal/discord/authenticate/token.go | 49 ++++++++ internal/discord/authenticate/totp.go | 77 +++++++++++++ 8 files changed, 228 insertions(+), 114 deletions(-) create mode 100644 internal/discord/authenticate/login.go create mode 100644 internal/discord/authenticate/token.go create mode 100644 internal/discord/authenticate/totp.go diff --git a/discord.go b/discord.go index 90b3e5c..3149d20 100644 --- a/discord.go +++ b/discord.go @@ -24,10 +24,7 @@ func (Service) Name() text.Rich { } func (Service) Authenticate() []cchat.Authenticator { - return []cchat.Authenticator{ - authenticate.New(), - authenticate.NewDiscordLogin(), - } + return authenticate.FirstStageAuthenticators() } func (Service) AsIconer() cchat.Iconer { diff --git a/go.mod b/go.mod index 543cb96..4581f68 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.14 require ( github.com/diamondburned/arikawa v1.3.6 - github.com/diamondburned/cchat v0.3.8 + github.com/diamondburned/cchat v0.3.11 github.com/diamondburned/ningen v0.2.1-0.20201023061015-ce64ffb0bb12 github.com/dustin/go-humanize v1.0.0 github.com/go-test/deep v1.0.7 diff --git a/go.sum b/go.sum index 4f3fb93..aa0011c 100644 --- a/go.sum +++ b/go.sum @@ -101,6 +101,8 @@ github.com/diamondburned/cchat v0.3.7 h1:0t3FkbzC/pBRAR3w0uYznJ+7dYqcR1M48a9wgz4 github.com/diamondburned/cchat v0.3.7/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI= github.com/diamondburned/cchat v0.3.8 h1:vgFe8giVfwsAO+WpTYsTDIXvRUN48osVPNu0pZNvPEk= github.com/diamondburned/cchat v0.3.8/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI= +github.com/diamondburned/cchat v0.3.11 h1:C1f9Tp7Kz3t+T1SlepL1RS7b/kACAKWAIZXAgJEpCHg= +github.com/diamondburned/cchat v0.3.11/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI= github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249 h1:yP7kJ+xCGpDz6XbcfACJcju4SH1XDPwlrvbofz3lP8I= github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249/go.mod h1:xW9hpBZsGi8KpAh10TyP+YQlYBo+Xc+2w4TR6N0951A= github.com/diamondburned/ningen v0.1.1-0.20200708085949-b64e350f3b8c h1:3h/kyk6HplYZF3zLi106itjYJWjbuMK/twijeGLEy2M= diff --git a/internal/discord/authenticate/authenticator.go b/internal/discord/authenticate/authenticator.go index 94da2cd..8477fc7 100644 --- a/internal/discord/authenticate/authenticator.go +++ b/internal/discord/authenticate/authenticator.go @@ -4,8 +4,6 @@ import ( "errors" "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-discord/internal/discord/session" - "github.com/diamondburned/cchat-discord/internal/discord/state" ) var ( @@ -13,105 +11,12 @@ var ( EnterPassword = errors.New("enter your password") ) -type Authenticator struct { - username string - password string -} - -func New() cchat.Authenticator { - return &Authenticator{} -} - -func (a *Authenticator) stage() int { - switch { - // Stage 1: Prompt for the token OR username. - case a.username == "" && a.password == "": - return 0 - - // Stage 2: Prompt for the password. - case a.password == "": - return 1 - - // Stage 3: Prompt for the TOTP token. - default: - return 2 +// FirstStageAuthenticators constructs a slice of newly made first stage +// authenticators. +func FirstStageAuthenticators() []cchat.Authenticator { + return []cchat.Authenticator{ + NewTokenAuthenticator(), + NewLoginAuthenticator(), + NewDiscordLogin(), } } - -func (a *Authenticator) AuthenticateForm() []cchat.AuthenticateEntry { - switch a.stage() { - case 0: - return []cchat.AuthenticateEntry{ - {Name: "Token", Secret: true}, - {Name: "Username", Description: "Fill either Token or Username only."}, - } - case 1: - return []cchat.AuthenticateEntry{ - {Name: "Password", Secret: true}, - } - case 2: - return []cchat.AuthenticateEntry{ - {Name: "Auth Code", Description: "6-digit code for Two-factor Authentication."}, - } - default: - return nil - } -} - -func (a *Authenticator) Authenticate(form []string) (cchat.Session, error) { - switch a.stage() { - case 0: - if len(form) != 2 { - return nil, ErrMalformed - } - - switch { - case form[0] != "": // Token - i, err := state.NewFromToken(form[0]) - if err != nil { - return nil, err - } - - return session.NewFromInstance(i) - - case form[1] != "": // Username - // Move to a new stage. - a.username = form[1] - return nil, EnterPassword - } - - case 1: - if len(form) != 1 { - return nil, ErrMalformed - } - - a.password = form[0] - - i, err := state.Login(a.username, a.password, "") - if err != nil { - // If the error is not ErrMFA, then we should reset password to - // empty. - if !errors.Is(err, session.ErrMFA) { - a.password = "" - } - - return nil, err - } - - return session.NewFromInstance(i) - - case 2: - if len(form) != 1 { - return nil, ErrMalformed - } - - i, err := state.Login(a.username, a.password, form[0]) - if err != nil { - return nil, err - } - - return session.NewFromInstance(i) - } - - return nil, ErrMalformed -} diff --git a/internal/discord/authenticate/discordlogin.go b/internal/discord/authenticate/discordlogin.go index 8692ac7..4e7c32a 100644 --- a/internal/discord/authenticate/discordlogin.go +++ b/internal/discord/authenticate/discordlogin.go @@ -8,29 +8,41 @@ import ( "github.com/diamondburned/cchat" "github.com/diamondburned/cchat-discord/internal/discord/session" "github.com/diamondburned/cchat-discord/internal/discord/state" + "github.com/diamondburned/cchat/text" "github.com/pkg/errors" "github.com/skratchdot/open-golang/open" ) var ErrDLNotFound = errors.New("DiscordLogin not found. Please install it from the GitHub page.") +// DiscordLoginAuth is a first stage authenticator that allows the user to +// authenticate using DiscordLogin. The Authenticate() function will exec up the +// application if possible. If not, it'll try and exec up a browser. type DiscordLoginAuth struct{} func NewDiscordLogin() cchat.Authenticator { return DiscordLoginAuth{} } +func (DiscordLoginAuth) Name() text.Rich { + return text.Plain("DiscordLogin") +} + +func (DiscordLoginAuth) Description() text.Rich { + return text.Plain("Log in using DiscordLogin, a WebKit application.") +} + // AuthenticateForm returns an empty slice. func (DiscordLoginAuth) AuthenticateForm() []cchat.AuthenticateEntry { return []cchat.AuthenticateEntry{} } -// Authenticate pops up discordlogin. -func (DiscordLoginAuth) Authenticate([]string) (cchat.Session, error) { +// Authenticate pops up DiscordLogin. +func (DiscordLoginAuth) Authenticate([]string) (cchat.Session, cchat.AuthenticateError) { path, err := lookPathExtras("discordlogin") if err != nil { openDiscordLoginPage() - return nil, ErrDLNotFound + return nil, cchat.WrapAuthenticateError(ErrDLNotFound) } cmd := &exec.Cmd{Path: path} @@ -40,19 +52,26 @@ func (DiscordLoginAuth) Authenticate([]string) (cchat.Session, error) { b, err := cmd.Output() if err != nil { - return nil, errors.Wrap(err, "DiscordLogin failed") + return nil, cchat.WrapAuthenticateError(errors.Wrap(err, "DiscordLogin failed")) } if len(b) == 0 { - return nil, errors.New("DiscordLogin returned nothing, check Console.") + return nil, cchat.WrapAuthenticateError( + errors.New("DiscordLogin returned nothing, check Console."), + ) } i, err := state.NewFromToken(string(b)) if err != nil { - return nil, err + return nil, cchat.WrapAuthenticateError(errors.Wrap(err, "failed to use token")) } - return session.NewFromInstance(i) + s, err := session.NewFromInstance(i) + if err != nil { + return nil, cchat.WrapAuthenticateError(errors.Wrap(err, "failed to make a session")) + } + + return s, nil } func openDiscordLoginPage() { diff --git a/internal/discord/authenticate/login.go b/internal/discord/authenticate/login.go new file mode 100644 index 0000000..eb886cd --- /dev/null +++ b/internal/discord/authenticate/login.go @@ -0,0 +1,65 @@ +package authenticate + +import ( + "github.com/diamondburned/arikawa/api" + "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat-discord/internal/discord/session" + "github.com/diamondburned/cchat-discord/internal/discord/state" + "github.com/diamondburned/cchat/text" + "github.com/pkg/errors" +) + +// LoginAuthenticator is a first stage authenticator that allows the user to +// authenticate using their email and password. +type LoginAuthenticator struct { + client *api.Client +} + +func NewLoginAuthenticator() cchat.Authenticator { + return &LoginAuthenticator{ + client: api.NewClient(""), + } +} + +func (a *LoginAuthenticator) Name() text.Rich { + return text.Plain("Email") +} + +func (a *LoginAuthenticator) Description() text.Rich { + return text.Plain("Log in using your email.") +} + +func (a *LoginAuthenticator) AuthenticateForm() []cchat.AuthenticateEntry { + return []cchat.AuthenticateEntry{ + {Name: "Email"}, + {Name: "Password", Secret: true}, + } +} + +func (a *LoginAuthenticator) Authenticate(form []string) (cchat.Session, cchat.AuthenticateError) { + if len(form) != 2 { + return nil, cchat.WrapAuthenticateError(ErrMalformed) + } + + // Try to login without TOTP + l, err := a.client.Login(form[0], form[1]) + if err != nil { + return nil, cchat.WrapAuthenticateError(errors.Wrap(err, "failed to login")) + } + + if l.MFA { + return nil, &ErrNeeds2FA{loginResp: l} + } + + i, err := state.NewFromToken(l.Token) + if err != nil { + return nil, cchat.WrapAuthenticateError(errors.Wrap(err, "failed to use token")) + } + + s, err := session.NewFromInstance(i) + if err != nil { + return nil, cchat.WrapAuthenticateError(errors.Wrap(err, "failed to make a session")) + } + + return s, nil +} diff --git a/internal/discord/authenticate/token.go b/internal/discord/authenticate/token.go new file mode 100644 index 0000000..89e9dc2 --- /dev/null +++ b/internal/discord/authenticate/token.go @@ -0,0 +1,49 @@ +package authenticate + +import ( + "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat-discord/internal/discord/session" + "github.com/diamondburned/cchat-discord/internal/discord/state" + "github.com/diamondburned/cchat/text" + "github.com/pkg/errors" +) + +// TokenAuthenticator is a first stage authenticator that allows the user to +// authenticate directly using a token. +type TokenAuthenticator struct{} + +func NewTokenAuthenticator() cchat.Authenticator { + return TokenAuthenticator{} +} + +func (TokenAuthenticator) Name() text.Rich { + return text.Plain("Token") +} + +func (TokenAuthenticator) Description() text.Rich { + return text.Plain("Log in using a token") +} + +func (TokenAuthenticator) AuthenticateForm() []cchat.AuthenticateEntry { + return []cchat.AuthenticateEntry{ + {Name: "Token", Secret: true}, + } +} + +func (TokenAuthenticator) Authenticate(form []string) (cchat.Session, cchat.AuthenticateError) { + if len(form) != 1 { + return nil, cchat.WrapAuthenticateError(ErrMalformed) + } + + i, err := state.NewFromToken(form[0]) + if err != nil { + return nil, cchat.WrapAuthenticateError(errors.Wrap(err, "failed to use token")) + } + + s, err := session.NewFromInstance(i) + if err != nil { + return nil, cchat.WrapAuthenticateError(errors.Wrap(err, "failed to make a session")) + } + + return s, nil +} diff --git a/internal/discord/authenticate/totp.go b/internal/discord/authenticate/totp.go new file mode 100644 index 0000000..f732966 --- /dev/null +++ b/internal/discord/authenticate/totp.go @@ -0,0 +1,77 @@ +package authenticate + +import ( + "github.com/diamondburned/arikawa/api" + "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat-discord/internal/discord/session" + "github.com/diamondburned/cchat-discord/internal/discord/state" + "github.com/diamondburned/cchat/text" + "github.com/pkg/errors" +) + +// ErrNeeds2FA is returned from Authenticator if the user login requires a 2FA +// token. +type ErrNeeds2FA struct { + loginResp *api.LoginResponse +} + +func (err ErrNeeds2FA) Error() string { + return "Two-Factor Authentication token required" +} + +func (err ErrNeeds2FA) NextStage() []cchat.Authenticator { + return []cchat.Authenticator{ + NewTOTPAuthenticator(err.loginResp.Ticket), + } +} + +// TOTPAuthenticator is a second stage authenticator that follows the normal +// Authenticator if the user has Two-Factor Authentication enabled. +type TOTPAuthenticator struct { + client *api.Client + ticket string +} + +func NewTOTPAuthenticator(ticket string) cchat.Authenticator { + return &TOTPAuthenticator{ + client: api.NewClient(""), + ticket: ticket, + } +} + +func (auth *TOTPAuthenticator) Name() text.Rich { + return text.Plain("2FA Prompt") +} + +func (auth *TOTPAuthenticator) Description() text.Rich { + return text.Plain("Enter your 2FA token.") +} + +func (auth *TOTPAuthenticator) AuthenticateForm() []cchat.AuthenticateEntry { + return []cchat.AuthenticateEntry{ + {Name: "Token", Description: "6-digit code"}, + } +} + +func (auth *TOTPAuthenticator) Authenticate(v []string) (cchat.Session, cchat.AuthenticateError) { + if len(v) != 1 { + return nil, cchat.WrapAuthenticateError(ErrMalformed) + } + + l, err := auth.client.TOTP(v[0], auth.ticket) + if err != nil { + return nil, cchat.WrapAuthenticateError(errors.Wrap(err, "failed to login with 2FA")) + } + + i, err := state.NewFromToken(l.Token) + if err != nil { + return nil, cchat.WrapAuthenticateError(errors.Wrap(err, "failed to use token")) + } + + s, err := session.NewFromInstance(i) + if err != nil { + return nil, cchat.WrapAuthenticateError(errors.Wrap(err, "failed to make a session")) + } + + return s, nil +}