1
1
package cmd
2
2
3
3
import (
4
+ "bufio"
4
5
"context"
5
6
"fmt"
6
- "net"
7
- "net/http"
8
7
"net/url"
8
+ "os"
9
9
"strings"
10
10
11
11
"github.com/pkg/browser"
12
12
"github.com/spf13/cobra"
13
- "golang.org/x/sync/errgroup"
14
13
"golang.org/x/xerrors"
15
14
16
15
"cdr.dev/coder-cli/coder-sdk"
17
16
"cdr.dev/coder-cli/internal/config"
18
- "cdr.dev/coder-cli/internal/loginsrv"
19
17
"cdr.dev/coder-cli/internal/version"
20
18
"cdr.dev/coder-cli/internal/x/xcobra"
21
19
"cdr.dev/coder-cli/pkg/clog"
@@ -42,25 +40,55 @@ func loginCmd() *cobra.Command {
42
40
// From this point, the commandline is correct.
43
41
// Don't return errors as it would print the usage.
44
42
45
- if err := login (cmd , u , config . URL , config . Session ); err != nil {
43
+ if err := login (cmd . Context (), u ); err != nil {
46
44
return xerrors .Errorf ("login error: %w" , err )
47
45
}
48
46
return nil
49
47
},
50
48
}
51
49
}
52
50
53
- // newLocalListener creates up a local tcp server using port 0 (i.e. any available port).
54
- // If ipv4 is disabled, try ipv6.
55
- // It will be used by the http server waiting for the auth callback.
56
- func newLocalListener () (net.Listener , error ) {
57
- l , err := net .Listen ("tcp" , "127.0.0.1:0" )
58
- if err != nil {
59
- if l , err = net .Listen ("tcp6" , "[::1]:0" ); err != nil {
60
- return nil , xerrors .Errorf ("listen on a port: %w" , err )
61
- }
51
+ // storeConfig writes the env URL and session token to the local config directory.
52
+ // The config lib will handle the local config path lookup and creation.
53
+ func storeConfig (envURL * url.URL , sessionToken string , urlCfg , sessionCfg config.File ) error {
54
+ if err := urlCfg .Write (envURL .String ()); err != nil {
55
+ return xerrors .Errorf ("store env url: %w" , err )
56
+ }
57
+ if err := sessionCfg .Write (sessionToken ); err != nil {
58
+ return xerrors .Errorf ("store session token: %w" , err )
62
59
}
63
- return l , nil
60
+ return nil
61
+ }
62
+
63
+ func login (ctx context.Context , envURL * url.URL ) error {
64
+ authURL := * envURL
65
+ authURL .Path = envURL .Path + "/internal-auth"
66
+ q := authURL .Query ()
67
+ q .Add ("show_token" , "true" )
68
+ authURL .RawQuery = q .Encode ()
69
+
70
+ if err := browser .OpenURL (authURL .String ()); err != nil || true {
71
+ fmt .Printf ("Open the following in your browser:\n \n \t %s\n \n " , authURL .String ())
72
+ } else {
73
+ fmt .Printf ("Your browser has been opened to visit:\n \n \t %s\n \n " , authURL .String ())
74
+ }
75
+
76
+ token := readLine ("Paste token here: " )
77
+ if err := pingAPI (ctx , envURL , token ); err != nil {
78
+ return xerrors .Errorf ("ping API with credentials: %w" , err )
79
+ }
80
+ if err := storeConfig (envURL , token , config .URL , config .Session ); err != nil {
81
+ return xerrors .Errorf ("store auth: %w" , err )
82
+ }
83
+ clog .LogSuccess ("logged in" )
84
+ return nil
85
+ }
86
+
87
+ func readLine (prompt string ) string {
88
+ reader := bufio .NewReader (os .Stdin )
89
+ fmt .Print (prompt )
90
+ text , _ := reader .ReadString ('\n' )
91
+ return strings .TrimSuffix (text , "\n " )
64
92
}
65
93
66
94
// pingAPI creates a client from the given url/token and try to exec an api call.
@@ -85,77 +113,3 @@ func pingAPI(ctx context.Context, envURL *url.URL, token string) error {
85
113
}
86
114
return nil
87
115
}
88
-
89
- // storeConfig writes the env URL and session token to the local config directory.
90
- // The config lib will handle the local config path lookup and creation.
91
- func storeConfig (envURL * url.URL , sessionToken string , urlCfg , sessionCfg config.File ) error {
92
- if err := urlCfg .Write (envURL .String ()); err != nil {
93
- return xerrors .Errorf ("store env url: %w" , err )
94
- }
95
- if err := sessionCfg .Write (sessionToken ); err != nil {
96
- return xerrors .Errorf ("store session token: %w" , err )
97
- }
98
- return nil
99
- }
100
-
101
- func login (cmd * cobra.Command , envURL * url.URL , urlCfg , sessionCfg config.File ) error {
102
- ctx := cmd .Context ()
103
-
104
- // Start by creating the listener so we can prompt the user with the URL.
105
- listener , err := newLocalListener ()
106
- if err != nil {
107
- return xerrors .Errorf ("create local listener: %w" , err )
108
- }
109
- defer func () { _ = listener .Close () }() // Best effort.
110
-
111
- // Forge the auth URL with the callback set to the local server.
112
- authURL := * envURL
113
- authURL .Path = envURL .Path + "/internal-auth"
114
- authURL .RawQuery = "local_service=http://" + listener .Addr ().String ()
115
-
116
- // Try to open the browser on the local computer.
117
- if err := browser .OpenURL (authURL .String ()); err != nil {
118
- // Discard the error as it is an expected one in non-X environments like over ssh.
119
- // Tell the user to visit the URL instead.
120
- _ , _ = fmt .Fprintf (cmd .ErrOrStderr (), "Visit the following URL in your browser:\n \n \t %s\n \n " , & authURL ) // Can't fail.
121
- }
122
-
123
- // Create our channel, it is going to be the central synchronization of the command.
124
- tokenChan := make (chan string )
125
-
126
- // Create the http server outside the errgroup goroutine scope so we can stop it later.
127
- srv := & http.Server {Handler : & loginsrv.Server {TokenChan : tokenChan }}
128
- defer func () { _ = srv .Close () }() // Best effort. Direct close as we are dealing with a one-off request.
129
-
130
- // Start both the readline and http server in parallel. As they are both long-running routines,
131
- // to know when to continue, we don't wait on the errgroup, but on the tokenChan.
132
- group , ctx := errgroup .WithContext (ctx )
133
- group .Go (func () error { return srv .Serve (listener ) })
134
- group .Go (func () error { return loginsrv .ReadLine (ctx , cmd .InOrStdin (), cmd .ErrOrStderr (), tokenChan ) })
135
-
136
- // Only close then tokenChan when the errgroup is done. Best effort basis.
137
- // Will not return the http route is used with a regular terminal.
138
- // Useful for non interactive session, manual input, tests or custom stdin.
139
- go func () { defer close (tokenChan ); _ = group .Wait () }()
140
-
141
- var token string
142
- select {
143
- case <- ctx .Done ():
144
- return ctx .Err ()
145
- case token = <- tokenChan :
146
- }
147
-
148
- // Perform an API call to verify that the token is valid.
149
- if err := pingAPI (ctx , envURL , token ); err != nil {
150
- return xerrors .Errorf ("ping API: %w" , err )
151
- }
152
-
153
- // Success. Store the config only at this point so we don't override the local one in case of failure.
154
- if err := storeConfig (envURL , token , urlCfg , sessionCfg ); err != nil {
155
- return xerrors .Errorf ("store config: %w" , err )
156
- }
157
-
158
- clog .LogSuccess ("logged in" )
159
-
160
- return nil
161
- }
0 commit comments