Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

feat: simplify login flow #274

Merged
merged 4 commits into from
Mar 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 43 additions & 89 deletions internal/cmd/login.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
package cmd

import (
"bufio"
"context"
"fmt"
"net"
"net/http"
"net/url"
"os"
"strings"

"github.com/pkg/browser"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"

"cdr.dev/coder-cli/coder-sdk"
"cdr.dev/coder-cli/internal/config"
"cdr.dev/coder-cli/internal/loginsrv"
"cdr.dev/coder-cli/internal/version"
"cdr.dev/coder-cli/internal/x/xcobra"
"cdr.dev/coder-cli/pkg/clog"
Expand All @@ -42,25 +40,55 @@ func loginCmd() *cobra.Command {
// From this point, the commandline is correct.
// Don't return errors as it would print the usage.

if err := login(cmd, u, config.URL, config.Session); err != nil {
if err := login(cmd.Context(), u); err != nil {
return xerrors.Errorf("login error: %w", err)
}
return nil
},
}
}

// newLocalListener creates up a local tcp server using port 0 (i.e. any available port).
// If ipv4 is disabled, try ipv6.
// It will be used by the http server waiting for the auth callback.
func newLocalListener() (net.Listener, error) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
if l, err = net.Listen("tcp6", "[::1]:0"); err != nil {
return nil, xerrors.Errorf("listen on a port: %w", err)
}
// storeConfig writes the env URL and session token to the local config directory.
// The config lib will handle the local config path lookup and creation.
func storeConfig(envURL *url.URL, sessionToken string, urlCfg, sessionCfg config.File) error {
if err := urlCfg.Write(envURL.String()); err != nil {
return xerrors.Errorf("store env url: %w", err)
}
if err := sessionCfg.Write(sessionToken); err != nil {
return xerrors.Errorf("store session token: %w", err)
}
return l, nil
return nil
}

func login(ctx context.Context, envURL *url.URL) error {
authURL := *envURL
authURL.Path = envURL.Path + "/internal-auth"
q := authURL.Query()
q.Add("show_token", "true")
authURL.RawQuery = q.Encode()

if err := browser.OpenURL(authURL.String()); err != nil || true {
fmt.Printf("Open the following in your browser:\n\n\t%s\n\n", authURL.String())
} else {
fmt.Printf("Your browser has been opened to visit:\n\n\t%s\n\n", authURL.String())
}

token := readLine("Paste token here: ")
if err := pingAPI(ctx, envURL, token); err != nil {
return xerrors.Errorf("ping API with credentials: %w", err)
}
if err := storeConfig(envURL, token, config.URL, config.Session); err != nil {
return xerrors.Errorf("store auth: %w", err)
}
clog.LogSuccess("logged in")
return nil
}

func readLine(prompt string) string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we can break this out from login.go, but that can be done later

reader := bufio.NewReader(os.Stdin)
fmt.Print(prompt)
text, _ := reader.ReadString('\n')
return strings.TrimSuffix(text, "\n")
}

// pingAPI creates a client from the given url/token and try to exec an api call.
Expand All @@ -85,77 +113,3 @@ func pingAPI(ctx context.Context, envURL *url.URL, token string) error {
}
return nil
}

// storeConfig writes the env URL and session token to the local config directory.
// The config lib will handle the local config path lookup and creation.
func storeConfig(envURL *url.URL, sessionToken string, urlCfg, sessionCfg config.File) error {
if err := urlCfg.Write(envURL.String()); err != nil {
return xerrors.Errorf("store env url: %w", err)
}
if err := sessionCfg.Write(sessionToken); err != nil {
return xerrors.Errorf("store session token: %w", err)
}
return nil
}

func login(cmd *cobra.Command, envURL *url.URL, urlCfg, sessionCfg config.File) error {
ctx := cmd.Context()

// Start by creating the listener so we can prompt the user with the URL.
listener, err := newLocalListener()
if err != nil {
return xerrors.Errorf("create local listener: %w", err)
}
defer func() { _ = listener.Close() }() // Best effort.

// Forge the auth URL with the callback set to the local server.
authURL := *envURL
authURL.Path = envURL.Path + "/internal-auth"
authURL.RawQuery = "local_service=http://" + listener.Addr().String()

// Try to open the browser on the local computer.
if err := browser.OpenURL(authURL.String()); err != nil {
// Discard the error as it is an expected one in non-X environments like over ssh.
// Tell the user to visit the URL instead.
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Visit the following URL in your browser:\n\n\t%s\n\n", &authURL) // Can't fail.
}

// Create our channel, it is going to be the central synchronization of the command.
tokenChan := make(chan string)

// Create the http server outside the errgroup goroutine scope so we can stop it later.
srv := &http.Server{Handler: &loginsrv.Server{TokenChan: tokenChan}}
defer func() { _ = srv.Close() }() // Best effort. Direct close as we are dealing with a one-off request.

// Start both the readline and http server in parallel. As they are both long-running routines,
// to know when to continue, we don't wait on the errgroup, but on the tokenChan.
group, ctx := errgroup.WithContext(ctx)
group.Go(func() error { return srv.Serve(listener) })
group.Go(func() error { return loginsrv.ReadLine(ctx, cmd.InOrStdin(), cmd.ErrOrStderr(), tokenChan) })

// Only close then tokenChan when the errgroup is done. Best effort basis.
// Will not return the http route is used with a regular terminal.
// Useful for non interactive session, manual input, tests or custom stdin.
go func() { defer close(tokenChan); _ = group.Wait() }()

var token string
select {
case <-ctx.Done():
return ctx.Err()
case token = <-tokenChan:
}

// Perform an API call to verify that the token is valid.
if err := pingAPI(ctx, envURL, token); err != nil {
return xerrors.Errorf("ping API: %w", err)
}

// Success. Store the config only at this point so we don't override the local one in case of failure.
if err := storeConfig(envURL, token, urlCfg, sessionCfg); err != nil {
return xerrors.Errorf("store config: %w", err)
}

clog.LogSuccess("logged in")

return nil
}
60 changes: 0 additions & 60 deletions internal/loginsrv/input.go

This file was deleted.

126 changes: 0 additions & 126 deletions internal/loginsrv/input_test.go

This file was deleted.

Loading