Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings
This repository was archived by the owner on Aug 30, 2024. It is now read-only.
/coder-v1-cliPublic archive

Commit8b35b02

Browse files
committed
- Allow for manual input of token during login.
- Support path based reverse proxy.Signed-off-by: Guillaume J. Charmes <guillaume@coder.com>
1 parentc796bca commit8b35b02

File tree

9 files changed

+418
-67
lines changed

9 files changed

+418
-67
lines changed

‎.github/workflows/test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
-name:test
4444
uses:./ci/image
4545
with:
46-
args:go test ./internal/... ./cmd/...
46+
args:go test-v -cover -covermode=count./internal/... ./cmd/...
4747
gendocs:
4848
runs-on:ubuntu-latest
4949
steps:

‎docs/coder_login.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Authenticate this client for future operations
77
Authenticate this client for future operations
88

99
```
10-
coder login [Coder Enterprise URL eg.http://my.coder.domain/] [flags]
10+
coder login [Coder Enterprise URL eg.https://my.coder.domain/] [flags]
1111
```
1212

1313
###Options

‎go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ require (
1414
github.com/pkg/browserv0.0.0-20180916011732-0a3d74bf9ce4
1515
github.com/rjeczalik/notifyv0.9.2
1616
github.com/spf13/cobrav1.0.0
17-
github.com/stretchr/testifyv1.6.1// indirect
17+
github.com/stretchr/testifyv1.6.1
1818
go.coder.com/flogv0.0.0-20190906214207-47dd47ea0512
1919
golang.org/x/cryptov0.0.0-20200422194213-44a606286825
2020
golang.org/x/syncv0.0.0-20200317015054-43a5402ce75a

‎internal/cmd/login.go

Lines changed: 111 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,149 @@
11
package cmd
22

33
import (
4+
"context"
5+
"fmt"
46
"net"
57
"net/http"
68
"net/url"
9+
"os"
710
"strings"
8-
"sync"
911

12+
"cdr.dev/coder-cli/coder-sdk"
1013
"cdr.dev/coder-cli/internal/config"
1114
"cdr.dev/coder-cli/internal/loginsrv"
1215
"github.com/pkg/browser"
1316
"github.com/spf13/cobra"
17+
"golang.org/x/sync/errgroup"
1418
"golang.org/x/xerrors"
1519

1620
"go.coder.com/flog"
1721
)
1822

1923
funcmakeLoginCmd()*cobra.Command {
20-
cmd:=&cobra.Command{
21-
Use:"login [Coder Enterprise URL eg.http://my.coder.domain/]",
24+
return&cobra.Command{
25+
Use:"login [Coder Enterprise URL eg.https://my.coder.domain/]",
2226
Short:"Authenticate this client for future operations",
2327
Args:cobra.ExactArgs(1),
24-
RunE:login,
25-
}
26-
returncmd
27-
}
28+
RunE:func(cmd*cobra.Command,args []string)error {
29+
// Pull the URL from the args and do some sanity check.
30+
rawURL:=args[0]
31+
ifrawURL==""||!strings.HasPrefix(rawURL,"http") {
32+
returnxerrors.Errorf("invalid URL")
33+
}
34+
u,err:=url.Parse(rawURL)
35+
iferr!=nil {
36+
returnxerrors.Errorf("parse url: %w",err)
37+
}
38+
// Remove the trailing '/' if any.
39+
u.Path=strings.TrimSuffix(u.Path,"/")
40+
41+
// From this point, the commandline is correct.
42+
// Don't return errors as it would print the usage.
43+
44+
iferr:=login(cmd,u,config.URL,config.Session);err!=nil {
45+
flog.Error("Login error: %s.",err)
46+
os.Exit(1)
47+
}
2848

29-
funclogin(cmd*cobra.Command,args []string)error {
30-
rawURL:=args[0]
31-
ifrawURL==""||!strings.HasPrefix(rawURL,"http") {
32-
returnxerrors.Errorf("invalid URL")
49+
returnnil
50+
},
3351
}
52+
}
3453

35-
u,err:=url.Parse(rawURL)
54+
// newLocalListener creates up a local tcp server using port 0 (i.e. any available port).
55+
// If ipv4 is disabled, try ipv6.
56+
// It will be used by the http server waiting for the auth callback.
57+
funcnewLocalListener() (net.Listener,error) {
58+
l,err:=net.Listen("tcp","127.0.0.1:0")
3659
iferr!=nil {
37-
returnxerrors.Errorf("parse url: %v",err)
60+
ifl,err=net.Listen("tcp6","[::1]:0");err!=nil {
61+
returnnil,xerrors.Errorf("listen on a port: %w",err)
62+
}
3863
}
64+
returnl,nil
65+
}
3966

40-
listener,err:=net.Listen("tcp","127.0.0.1:0")
41-
iferr!=nil {
42-
returnxerrors.Errorf("create login server: %+v",err)
67+
// pingAPI creates a client from the given url/token and try to exec an api call.
68+
// Not using the SDK as we want to verify the url/token pair before storing the config files.
69+
funcpingAPI(ctx context.Context,envURL*url.URL,tokenstring)error {
70+
client:=&coder.Client{BaseURL:envURL,Token:token}
71+
if_,err:=client.Me(ctx);err!=nil {
72+
returnxerrors.Errorf("call api: %w",err)
4373
}
44-
deferlistener.Close()
74+
returnnil
75+
}
4576

46-
srv:=&loginsrv.Server{
47-
TokenCond:sync.NewCond(&sync.Mutex{}),
77+
// storeConfig writes the env URL and session token to the local config directory.
78+
// The config lib will handle the local config path lookup and creation.
79+
funcstoreConfig(envURL*url.URL,sessionTokenstring,urlCfg,sessionCfg config.File)error {
80+
iferr:=urlCfg.Write(envURL.String());err!=nil {
81+
returnxerrors.Errorf("store env url: %w",err)
4882
}
49-
gofunc() {
50-
_=http.Serve(
51-
listener,srv,
52-
)
53-
}()
54-
55-
err=config.URL.Write(
56-
(&url.URL{Scheme:u.Scheme,Host:u.Host}).String(),
57-
)
58-
iferr!=nil {
59-
returnxerrors.Errorf("write url: %v",err)
83+
iferr:=sessionCfg.Write(sessionToken);err!=nil {
84+
returnxerrors.Errorf("store session token: %w",err)
6085
}
86+
returnnil
87+
}
6188

62-
authURL:= url.URL{
63-
Scheme:u.Scheme,
64-
Host:u.Host,
65-
Path:"/internal-auth/",
66-
RawQuery:"local_service=http://"+listener.Addr().String(),
67-
}
89+
funclogin(cmd*cobra.Command,envURL*url.URL,urlCfg,sessionCfg config.File)error {
90+
ctx:=cmd.Context()
6891

69-
err=browser.OpenURL(authURL.String())
92+
// Start by creating the listener so we can prompt the user with the URL.
93+
listener,err:=newLocalListener()
7094
iferr!=nil {
95+
returnxerrors.Errorf("create local listener: %w",err)
96+
}
97+
deferfunc() {_=listener.Close() }()// Best effort.
98+
99+
// Forge the auth URL with the callback set to the local server.
100+
authURL:=*envURL
101+
authURL.Path=envURL.Path+"/internal-auth"
102+
authURL.RawQuery="local_service=http://"+listener.Addr().String()
103+
104+
// Try to open the browser on the local computer.
105+
iferr:=browser.OpenURL(authURL.String());err!=nil {
106+
// Discard the error as it is an expected one in non-X environments like over ssh.
71107
// Tell the user to visit the URL instead.
72-
flog.Info("visit %s to login",authURL.String())
108+
_,_=fmt.Fprintf(cmd.ErrOrStderr(),"Visit the following URL in your browser:\n\n\t%s\n\n",&authURL)// Can't fail.
73109
}
74-
srv.TokenCond.L.Lock()
75-
srv.TokenCond.Wait()
76-
err=config.Session.Write(srv.Token)
77-
srv.TokenCond.L.Unlock()
78-
iferr!=nil {
79-
returnxerrors.Errorf("set session: %v",err)
110+
111+
// Create our channel, it is going to be the central synchronization of the command.
112+
tokenChan:=make(chanstring)
113+
114+
// Create the http server outside the errgroup goroutine scope so we can stop it later.
115+
srv:=&http.Server{Handler:&loginsrv.Server{TokenChan:tokenChan}}
116+
deferfunc() {_=srv.Close() }()// Best effort. Direct close as we are dealing with a one-off request.
117+
118+
// Start both the readline and http server in parallel. As they are both long-running routines,
119+
// to know when to continue, we don't wait on the errgroup, but on the tokenChan.
120+
group,ctx:=errgroup.WithContext(ctx)
121+
group.Go(func()error {returnsrv.Serve(listener) })
122+
group.Go(func()error {returnloginsrv.ReadLine(ctx,cmd.InOrStdin(),cmd.ErrOrStderr(),tokenChan) })
123+
124+
// Only close then tokenChan when the errgroup is done. Best effort basis.
125+
// Will not return the http route is used with a regular terminal.
126+
// Useful for non interactive session, manual input, tests or custom stdin.
127+
gofunc() {deferclose(tokenChan);_=group.Wait() }()
128+
129+
vartokenstring
130+
select {
131+
case<-ctx.Done():
132+
returnctx.Err()
133+
casetoken=<-tokenChan:
134+
}
135+
136+
// Perform an API call to verify that the token is valid.
137+
iferr:=pingAPI(ctx,envURL,token);err!=nil {
138+
returnxerrors.Errorf("ping API: %w",err)
80139
}
81-
flog.Success("logged in")
140+
141+
// Success. Store the config only at this point so we don't override the local one in case of failure.
142+
iferr:=storeConfig(envURL,token,urlCfg,sessionCfg);err!=nil {
143+
returnxerrors.Errorf("store config: %w",err)
144+
}
145+
146+
flog.Success("Logged in.")
147+
82148
returnnil
83149
}

‎internal/config/file.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
package config
22

3-
// File provides convenience methods for interacting with *os.File
3+
// File provides convenience methods for interacting with *os.File.
44
typeFilestring
55

6-
// Delete deletes the file
6+
// Delete deletes the file.
77
func (fFile)Delete()error {
88
returnrm(string(f))
99
}
1010

11-
// Write writes the string to the file
11+
// Write writes the string to the file.
1212
func (fFile)Write(sstring)error {
1313
returnwrite(string(f),0600, []byte(s))
1414
}
1515

16-
// Read reads the file to a string
16+
// Read reads the file to a string.
1717
func (fFile)Read() (string,error) {
1818
byt,err:=read(string(f))
1919
returnstring(byt),err
2020
}
2121

22-
// Coder CLI configuration files
22+
// Coder CLI configuration files.
2323
var (
2424
SessionFile="session"
2525
URLFile="url"

‎internal/loginsrv/input.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package loginsrv
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"fmt"
7+
"io"
8+
"net/url"
9+
"strings"
10+
11+
"golang.org/x/xerrors"
12+
)
13+
14+
// ReadLine waits for the manual login input to send the session token.
15+
// NOTE: As we are dealing with a Read, cancelling the context will not unblock.
16+
// The caller is expected to close the reader.
17+
funcReadLine(ctx context.Context,r io.Reader,w io.Writer,tokenChanchan<-string)error {
18+
// Wrap the reader with bufio to simplify the readline.
19+
buf:=bufio.NewReader(r)
20+
21+
retry:
22+
_,_=fmt.Fprintf(w,"or enter token manually:\n")// Best effort. Can only fail on custom writers.
23+
line,err:=buf.ReadString('\n')
24+
iferr!=nil {
25+
// If we get an expected error, discard it and stop the routine.
26+
// NOTE: UnexpectedEOF is indeed an expected error as we can get it if we receive the token via the http server.
27+
iferr==io.EOF||err==io.ErrClosedPipe||err==io.ErrUnexpectedEOF {
28+
returnnil
29+
}
30+
// In the of error, we don't try again. Error out right away.
31+
returnxerrors.Errorf("read input: %w",err)
32+
}
33+
34+
// If we don't have any data, try again to read.
35+
line=strings.TrimSpace(line)
36+
ifline=="" {
37+
goto retry
38+
}
39+
40+
// Handle the case where we copy/paste the full URL instead of just the token.
41+
// Useful as most browser will auto-select the full URL.
42+
ifu,err:=url.Parse(line);err==nil {
43+
// Check the query string only in case of success, ignore the error otherwise
44+
// as we consider the input to be the token itself.
45+
iftoken:=u.Query().Get("session_token");token!="" {
46+
line=token
47+
}
48+
// If the session_token is missing, we also consider the input the be the token, don't error out.
49+
}
50+
51+
select {
52+
case<-ctx.Done():
53+
returnctx.Err()
54+
casetokenChan<-line:
55+
}
56+
57+
returnnil
58+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp