Skip to content

Commit

Permalink
Browserless Login (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
coffee-cup authored Oct 20, 2020
1 parent be6d2cc commit 1992033
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 16 deletions.
11 changes: 9 additions & 2 deletions cmd/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,17 @@ import (
)

func (h *Handler) Login(ctx context.Context, req *entity.CommandRequest) error {
user, err := h.ctrl.Login(ctx)
isBrowserless, err := req.Cmd.Flags().GetBool("browserless")
if err != nil {
return err
}
ui.StopSpinner(fmt.Sprintf("🎉 Logged in as %s (%s)", ui.Bold(user.Name), user.Email))

user, err := h.ctrl.Login(ctx, isBrowserless)
if err != nil {
return err
}

fmt.Printf(fmt.Sprintf("🎉 Logged in as %s (%s)", ui.Bold(user.Name), user.Email))

return nil
}
125 changes: 113 additions & 12 deletions controller/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"strconv"
"sync"
"time"

"github.com/pkg/browser"
configs "github.com/railwayapp/cli/configs"
Expand All @@ -32,6 +33,9 @@ type LoginResponse struct {
Error string `json:"error,omitempty"`
}

const maxAttempts = 2 * 60
const pollInterval = 1 * time.Second

func (c *Controller) GetUser(ctx context.Context) (*entity.User, error) {
userCfg, err := c.cfg.GetUserConfigs()
if err != nil {
Expand All @@ -43,21 +47,25 @@ func (c *Controller) GetUser(ctx context.Context) (*entity.User, error) {
return c.gtwy.GetUser(ctx)
}

func (c *Controller) Login(ctx context.Context) (*entity.User, error) {
func (c *Controller) browserBasedLogin(ctx context.Context) (*entity.User, error) {
var token string
var returnedCode string
port, err := c.randomizer.Port()

if err != nil {
return nil, err
}

code := c.randomizer.Code()

wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
ctx := context.Background()
srv := &http.Server{Addr: strconv.Itoa(port)}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", getAPIURL())

if r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
token = r.URL.Query().Get("token")
Expand All @@ -73,8 +81,10 @@ func (c *Controller) Login(ctx context.Context) (*entity.User, error) {
w.Write(byteRes)
return
}

res := LoginResponse{Status: loginSuccessResponse}
byteRes, err := json.Marshal(&res)

if err != nil {
fmt.Println(err)
}
Expand All @@ -87,28 +97,103 @@ func (c *Controller) Login(ctx context.Context) (*entity.User, error) {
w.WriteHeader(204)
return
}

wg.Done()

if err := srv.Shutdown(ctx); err != nil {
fmt.Println(err)
}
})

http.ListenAndServe(fmt.Sprintf("localhost:%d", port), nil)
}()
url := getLoginURL(port, code)

confirmBrowserOpen("Logging in...", url)
url := getBrowserBasedLoginURL(port, code)
err = confirmBrowserOpen("Logging in...", url)

if err != nil {
// Opening the browser failed. Try browserless login
return c.browserlessLogin(ctx)
}

wg.Wait()

if code != returnedCode {
return nil, errors.New("Login failed")
}

err = c.cfg.SetUserConfigs(&entity.UserConfig{
Token: token,
})
if err != nil {
return nil, err
}

user, err := c.gtwy.GetUser(ctx)
if err != nil {
return nil, err
}

return user, nil
}

func (c *Controller) pollForToken(ctx context.Context, code string) (string, error) {
var count = 0
for count < maxAttempts {
token, err := c.gtwy.ConsumeLoginSession(ctx, code)

if err != nil {
return "", errors.New("Login failed")
}

if token != "" {
return token, nil
}

count++
time.Sleep(pollInterval)
}

return "", errors.New("Login timeout")
}

func (c *Controller) browserlessLogin(ctx context.Context) (*entity.User, error) {
wordCode, err := c.gtwy.CreateLoginSession(ctx)
if err != nil {
return nil, err
}

url := getBrowserlessLoginURL(wordCode)

fmt.Printf("Your pairing code is: %s\n", wordCode)
fmt.Printf("To authenticate with Railway, please go to \n %s\n", url)

token, err := c.pollForToken(ctx, wordCode)
if err != nil {
return nil, err
}

err = c.cfg.SetUserConfigs(&entity.UserConfig{
Token: token,
})
if err != nil {
return nil, err
}
if code == returnedCode {
return c.gtwy.GetUser(ctx)

user, err := c.gtwy.GetUser(ctx)
if err != nil {
return nil, err
}

return user, nil
}

func (c *Controller) Login(ctx context.Context, isBrowserless bool) (*entity.User, error) {
if isBrowserless || isSSH() {
return c.browserlessLogin(ctx)
}
return nil, nil

return c.browserBasedLogin(ctx)
}

func (c *Controller) Logout(ctx context.Context) error {
Expand Down Expand Up @@ -138,19 +223,21 @@ func (c *Controller) IsLoggedIn(ctx context.Context) (bool, error) {
return isLoggedIn, nil
}

func confirmBrowserOpen(spinnerMsg string, url string) {
func confirmBrowserOpen(spinnerMsg string, url string) error {
fmt.Printf("Press Enter to open the browser (^C to quit)")
fmt.Fscanln(os.Stdin)
ui.StartSpinner(&ui.SpinnerCfg{
Message: spinnerMsg,
})

err := browser.OpenURL(url)

if err != nil {
ui.StopSpinner(fmt.Sprintf("Failed to open browser, please go to %s manually.", url))
ui.StartSpinner(&ui.SpinnerCfg{
Message: spinnerMsg,
})
ui.StopSpinner(fmt.Sprintf("Failed to open browser, attempting browserless login.", url))
return err
}

return nil
}

func getAPIURL() string {
Expand All @@ -160,8 +247,22 @@ func getAPIURL() string {
return baseRailwayURL
}

func getLoginURL(port int, code string) string {
func getBrowserBasedLoginURL(port int, code string) string {
buffer := b64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("port=%d&code=%s", port, code)))
url := fmt.Sprintf("%s/cli-login?d=%s", getAPIURL(), buffer)
return url
}

func getBrowserlessLoginURL(wordCode string) string {
buffer := b64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("wordCode=%s", wordCode)))
url := fmt.Sprintf("%s/cli-login?d=%s", getAPIURL(), buffer)
return url
}

func isSSH() bool {
if os.Getenv("SSH_TTY") != "" || os.Getenv("SSH_CONNECTION") != "" || os.Getenv("SSH_CLIENT") != "" {
return true
}

return false
}
33 changes: 33 additions & 0 deletions gateway/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,36 @@ func (g *Gateway) GetUser(ctx context.Context) (*entity.User, error) {
}
return resp.User, nil
}

func (g *Gateway) CreateLoginSession(ctx context.Context) (string, error) {
gqlReq := gql.NewRequest(`mutation { createLoginSession } `)

var resp struct {
Code string `json:"createLoginSession"`
}

if err := g.gqlClient.Run(ctx, gqlReq, &resp); err != nil {
return "", err
}

return resp.Code, nil
}

func (g *Gateway) ConsumeLoginSession(ctx context.Context, code string) (string, error) {
gqlReq := gql.NewRequest(`
mutation($code: String!) {
consumeLoginSession(code: $code)
}
`)
gqlReq.Var("code", code)

var resp struct {
Token string `json:"consumeLoginSession"`
}

if err := g.gqlClient.Run(ctx, gqlReq, &resp); err != nil {
return "", err
}

return resp.Token, nil
}
8 changes: 6 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,15 @@ func contextualize(fn entity.HandlerFunction) entity.CobraFunction {
func init() {
// Initializes all commands
handler := cmd.New()
rootCmd.AddCommand(&cobra.Command{

loginCmd := &cobra.Command{
Use: "login",
Short: "Login to Railway",
RunE: contextualize(handler.Login),
})
}
loginCmd.Flags().Bool("browserless", false, "--browserless")

rootCmd.AddCommand(loginCmd)
rootCmd.AddCommand(&cobra.Command{
Use: "logout",
Short: "Logout of Railway",
Expand Down

0 comments on commit 1992033

Please sign in to comment.