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 <dgentry@tailscale.com>
This commit is contained in:
Denton Gentry 2021-11-20 21:08:34 -08:00 committed by Denton Gentry
parent d3d503d997
commit ffb16cdffb

View File

@ -6,6 +6,8 @@
import ( import (
"context" "context"
"encoding/base64"
"encoding/json"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
@ -69,6 +71,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
upf := newFlagSet("up") upf := newFlagSet("up")
upf.BoolVar(&upArgs.qr, "qr", false, "show QR code for login URLs") 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.forceReauth, "force-reauth", false, "force reauthentication")
upf.BoolVar(&upArgs.reset, "reset", false, "reset unspecified settings to their default values") 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" authKeyOrFile string // "secret" or "file:/path/to/secret"
hostname string hostname string
opUser string opUser string
json bool
} }
func (a upArgsT) getAuthKey() (string, error) { func (a upArgsT) getAuthKey() (string, error) {
@ -141,6 +145,33 @@ func (a upArgsT) getAuthKey() (string, error) {
var upArgs upArgsT 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{}) { func warnf(format string, args ...interface{}) {
printf("Warning: "+format+"\n", args...) printf("Warning: "+format+"\n", args...)
} }
@ -498,10 +529,16 @@ func runUp(ctx context.Context, args []string) error {
startLoginInteractive() startLoginInteractive()
case ipn.NeedsMachineAuth: case ipn.NeedsMachineAuth:
printed = true printed = true
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()) fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
}
case ipn.Running: case ipn.Running:
// Done full authentication process // 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. // Only need to print an update if we printed the "please click" message earlier.
fmt.Fprintf(Stderr, "Success.\n") fmt.Fprintf(Stderr, "Success.\n")
} }
@ -514,6 +551,24 @@ func runUp(ctx context.Context, args []string) error {
} }
if url := n.BrowseToURL; url != nil && printAuthURL(*url) { if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
printed = true printed = true
if upArgs.json {
js := &upOutputJSON{AuthURL: *url, BackendState: st.BackendState}
q, err := qrcode.New(*url, qrcode.Medium)
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) fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
if upArgs.qr { if upArgs.qr {
q, err := qrcode.New(*url, qrcode.Medium) q, err := qrcode.New(*url, qrcode.Medium)
@ -522,7 +577,7 @@ func runUp(ctx context.Context, args []string) error {
} else { } else {
fmt.Fprintf(Stderr, "%s\n", q.ToString(false)) 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 ( var (
prefsOfFlag = map[string][]string{} // "exit-node" => ExitNodeIP, ExitNodeID prefsOfFlag = map[string][]string{} // "exit-node" => ExitNodeIP, ExitNodeID
) )
@ -651,7 +716,7 @@ func addPrefFlagMapping(flagName string, prefNames ...string) {
// correspond to an ipn.Pref. // correspond to an ipn.Pref.
func preflessFlag(flagName string) bool { func preflessFlag(flagName string) bool {
switch flagName { switch flagName {
case "authkey", "force-reauth", "reset", "qr": case "authkey", "force-reauth", "reset", "qr", "json":
return true return true
} }
return false return false