From ffb16cdffbe769ebb6b92b21219b5d52490aece8 Mon Sep 17 00:00:00 2001 From: Denton Gentry Date: Sat, 20 Nov 2021 21:08:34 -0800 Subject: [PATCH] cmd/tailscale: add --json for up Print JSON to stdout containing everything needed for authentication. { "AuthURL": "https://login.tailscale.com/a/0123456789", "QR": "data:image/png;base64,iV...QmCC", "BackendState": "NeedsLogin" } { "BackendState": "Running" } Signed-off-by: Denton Gentry --- cmd/tailscale/cli/up.go | 83 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 9 deletions(-) diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go index cc9f1cbce..8a6a6da9a 100644 --- a/cmd/tailscale/cli/up.go +++ b/cmd/tailscale/cli/up.go @@ -6,6 +6,8 @@ package cli import ( "context" + "encoding/base64" + "encoding/json" "errors" "flag" "fmt" @@ -69,6 +71,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet { upf := newFlagSet("up") upf.BoolVar(&upArgs.qr, "qr", false, "show QR code for login URLs") + upf.BoolVar(&upArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)") upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication") upf.BoolVar(&upArgs.reset, "reset", false, "reset unspecified settings to their default values") @@ -124,6 +127,7 @@ type upArgsT struct { authKeyOrFile string // "secret" or "file:/path/to/secret" hostname string opUser string + json bool } func (a upArgsT) getAuthKey() (string, error) { @@ -141,6 +145,33 @@ func (a upArgsT) getAuthKey() (string, error) { var upArgs upArgsT +// Fields output when `tailscale up --json` is used. Two JSON blocks will be output. +// +// When "tailscale up" is run it first outputs a block with AuthURL and QR populated, +// providing the link for where to authenticate this client. BackendState would be +// valid but boring, as it will almost certainly be "NeedsLogin". Error would be +// populated if something goes badly wrong. +// +// When the client is authenticated by having someone visit the AuthURL, a second +// JSON block will be output. The AuthURL and QR fields will not be present, the +// BackendState and Error fields will give the result of the authentication. +// Ex: +// { +// "AuthURL": "https://login.tailscale.com/a/0123456789abcdef", +// "QR": "data:image/png;base64,0123...cdef" +// "BackendState": "NeedsLogin" +// } +// { +// "BackendState": "Running" +// } +// +type upOutputJSON struct { + AuthURL string `json:",omitempty"` // Authentication URL of the form https://login.tailscale.com/a/0123456789 + QR string `json:",omitempty"` // a DataURL (base64) PNG of a QR code AuthURL + BackendState string `json:",omitempty"` // name of state like Running or NeedsMachineAuth + Error string `json:",omitempty"` // description of an error +} + func warnf(format string, args ...interface{}) { printf("Warning: "+format+"\n", args...) } @@ -498,10 +529,16 @@ func runUp(ctx context.Context, args []string) error { startLoginInteractive() case ipn.NeedsMachineAuth: printed = true - fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL()) + if env.upArgs.json { + printUpDoneJSON(ipn.NeedsMachineAuth, "") + } else { + fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL()) + } case ipn.Running: // Done full authentication process - if printed { + if env.upArgs.json { + printUpDoneJSON(ipn.Running, "") + } else if printed { // Only need to print an update if we printed the "please click" message earlier. fmt.Fprintf(Stderr, "Success.\n") } @@ -514,15 +551,33 @@ func runUp(ctx context.Context, args []string) error { } if url := n.BrowseToURL; url != nil && printAuthURL(*url) { printed = true - fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url) - if upArgs.qr { + if upArgs.json { + js := &upOutputJSON{AuthURL: *url, BackendState: st.BackendState} + q, err := qrcode.New(*url, qrcode.Medium) - if err != nil { - log.Printf("QR code error: %v", err) - } else { - fmt.Fprintf(Stderr, "%s\n", q.ToString(false)) + if err == nil { + png, err := q.PNG(128) + if err == nil { + js.QR = "data:image/png;base64," + base64.StdEncoding.EncodeToString(png) + } } + data, err := json.MarshalIndent(js, "", "\t") + if err != nil { + log.Printf("upOutputJSON marshalling error: %v", err) + } else { + fmt.Println(string(data)) + } + } else { + fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url) + if upArgs.qr { + q, err := qrcode.New(*url, qrcode.Medium) + if err != nil { + log.Printf("QR code error: %v", err) + } else { + fmt.Fprintf(Stderr, "%s\n", q.ToString(false)) + } + } } } }) @@ -609,6 +664,16 @@ func runUp(ctx context.Context, args []string) error { } } +func printUpDoneJSON(state ipn.State, errorString string) { + js := &upOutputJSON{BackendState: state.String(), Error: errorString} + data, err := json.MarshalIndent(js, "", " ") + if err != nil { + log.Printf("printUpDoneJSON marshalling error: %v", err) + } else { + fmt.Println(string(data)) + } +} + var ( prefsOfFlag = map[string][]string{} // "exit-node" => ExitNodeIP, ExitNodeID ) @@ -651,7 +716,7 @@ func addPrefFlagMapping(flagName string, prefNames ...string) { // correspond to an ipn.Pref. func preflessFlag(flagName string) bool { switch flagName { - case "authkey", "force-reauth", "reset", "qr": + case "authkey", "force-reauth", "reset", "qr", "json": return true } return false