lncli: use profiles
This commit is contained in:
parent
a2721a15a8
commit
070cfb804f
@ -6,12 +6,10 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
macaroon "gopkg.in/macaroon.v2"
|
|
||||||
|
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
"github.com/lightningnetwork/lnd/build"
|
"github.com/lightningnetwork/lnd/build"
|
||||||
@ -20,6 +18,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/macaroons"
|
"github.com/lightningnetwork/lnd/macaroons"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
)
|
)
|
||||||
@ -68,18 +67,20 @@ func getClient(ctx *cli.Context) (lnrpc.LightningClient, func()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
|
func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
|
||||||
// First, we'll parse the args from the command.
|
// First, we'll get the selected stored profile or an ephemeral one
|
||||||
tlsCertPath, macPath, err := extractPathArgs(ctx)
|
// created from the global options in the CLI context.
|
||||||
|
profile, err := getGlobalOptions(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatal(err)
|
fatal(fmt.Errorf("could not load global options: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the specified TLS certificate and build transport credentials
|
// Load the specified TLS certificate and build transport credentials
|
||||||
// with it.
|
// with it.
|
||||||
creds, err := credentials.NewClientTLSFromFile(tlsCertPath, "")
|
certPool, err := profile.cert()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatal(err)
|
fatal(fmt.Errorf("could not create cert pool: %v", err))
|
||||||
}
|
}
|
||||||
|
creds := credentials.NewClientTLSFromCert(certPool, "")
|
||||||
|
|
||||||
// Create a dial options array.
|
// Create a dial options array.
|
||||||
opts := []grpc.DialOption{
|
opts := []grpc.DialOption{
|
||||||
@ -88,17 +89,31 @@ func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
|
|||||||
|
|
||||||
// Only process macaroon credentials if --no-macaroons isn't set and
|
// Only process macaroon credentials if --no-macaroons isn't set and
|
||||||
// if we're not skipping macaroon processing.
|
// if we're not skipping macaroon processing.
|
||||||
if !ctx.GlobalBool("no-macaroons") && !skipMacaroons {
|
if !profile.NoMacaroons && !skipMacaroons {
|
||||||
// Load the specified macaroon file.
|
// Find out which macaroon to load.
|
||||||
macBytes, err := ioutil.ReadFile(macPath)
|
macName := profile.Macaroons.Default
|
||||||
if err != nil {
|
if ctx.GlobalIsSet("macfromjar") {
|
||||||
fatal(fmt.Errorf("unable to read macaroon path (check "+
|
macName = ctx.GlobalString("macfromjar")
|
||||||
"the network setting!): %v", err))
|
}
|
||||||
|
var macEntry *macaroonEntry
|
||||||
|
for _, entry := range profile.Macaroons.Jar {
|
||||||
|
if entry.Name == macName {
|
||||||
|
macEntry = entry
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if macEntry == nil {
|
||||||
|
fatal(fmt.Errorf("macaroon with name '%s' not found "+
|
||||||
|
"in profile", macName))
|
||||||
}
|
}
|
||||||
|
|
||||||
mac := &macaroon.Macaroon{}
|
// Get and possibly decrypt the specified macaroon.
|
||||||
if err = mac.UnmarshalBinary(macBytes); err != nil {
|
//
|
||||||
fatal(fmt.Errorf("unable to decode macaroon: %v", err))
|
// TODO(guggero): Make it possible to cache the password so we
|
||||||
|
// don't need to ask for it every time.
|
||||||
|
mac, err := macEntry.loadMacaroon(readPassword)
|
||||||
|
if err != nil {
|
||||||
|
fatal(fmt.Errorf("could not load macaroon: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
macConstraints := []macaroons.Constraint{
|
macConstraints := []macaroons.Constraint{
|
||||||
@ -113,16 +128,18 @@ func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
|
|||||||
// altogether if, in the latter case, this time is more than 60
|
// altogether if, in the latter case, this time is more than 60
|
||||||
// seconds).
|
// seconds).
|
||||||
// TODO(aakselrod): add better anti-replay protection.
|
// TODO(aakselrod): add better anti-replay protection.
|
||||||
macaroons.TimeoutConstraint(ctx.GlobalInt64("macaroontimeout")),
|
macaroons.TimeoutConstraint(profile.Macaroons.Timeout),
|
||||||
|
|
||||||
// Lock macaroon down to a specific IP address.
|
// Lock macaroon down to a specific IP address.
|
||||||
macaroons.IPLockConstraint(ctx.GlobalString("macaroonip")),
|
macaroons.IPLockConstraint(profile.Macaroons.IP),
|
||||||
|
|
||||||
// ... Add more constraints if needed.
|
// ... Add more constraints if needed.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply constraints to the macaroon.
|
// Apply constraints to the macaroon.
|
||||||
constrainedMac, err := macaroons.AddConstraints(mac, macConstraints...)
|
constrainedMac, err := macaroons.AddConstraints(
|
||||||
|
mac, macConstraints...,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatal(err)
|
fatal(err)
|
||||||
}
|
}
|
||||||
@ -138,7 +155,7 @@ func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
|
|||||||
opts = append(opts, grpc.WithContextDialer(genericDialer))
|
opts = append(opts, grpc.WithContextDialer(genericDialer))
|
||||||
opts = append(opts, grpc.WithDefaultCallOptions(maxMsgRecvSize))
|
opts = append(opts, grpc.WithDefaultCallOptions(maxMsgRecvSize))
|
||||||
|
|
||||||
conn, err := grpc.Dial(ctx.GlobalString("rpcserver"), opts...)
|
conn, err := grpc.Dial(profile.RPCServer, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatal(fmt.Errorf("unable to connect to RPC server: %v", err))
|
fatal(fmt.Errorf("unable to connect to RPC server: %v", err))
|
||||||
}
|
}
|
||||||
@ -210,45 +227,60 @@ func main() {
|
|||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "rpcserver",
|
Name: "rpcserver",
|
||||||
Value: defaultRPCHostPort,
|
Value: defaultRPCHostPort,
|
||||||
Usage: "host:port of ln daemon",
|
Usage: "The host:port of LN daemon.",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "lnddir",
|
Name: "lnddir",
|
||||||
Value: defaultLndDir,
|
Value: defaultLndDir,
|
||||||
Usage: "path to lnd's base directory",
|
Usage: "The path to lnd's base directory.",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "tlscertpath",
|
Name: "tlscertpath",
|
||||||
Value: defaultTLSCertPath,
|
Value: defaultTLSCertPath,
|
||||||
Usage: "path to TLS certificate",
|
Usage: "The path to lnd's TLS certificate.",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "chain, c",
|
Name: "chain, c",
|
||||||
Usage: "the chain lnd is running on e.g. bitcoin",
|
Usage: "The chain lnd is running on, e.g. bitcoin.",
|
||||||
Value: "bitcoin",
|
Value: "bitcoin",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "network, n",
|
Name: "network, n",
|
||||||
Usage: "the network lnd is running on e.g. mainnet, " +
|
Usage: "The network lnd is running on, e.g. mainnet, " +
|
||||||
"testnet, etc.",
|
"testnet, etc.",
|
||||||
Value: "mainnet",
|
Value: "mainnet",
|
||||||
},
|
},
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "no-macaroons",
|
Name: "no-macaroons",
|
||||||
Usage: "disable macaroon authentication",
|
Usage: "Disable macaroon authentication.",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "macaroonpath",
|
Name: "macaroonpath",
|
||||||
Usage: "path to macaroon file",
|
Usage: "The path to macaroon file.",
|
||||||
},
|
},
|
||||||
cli.Int64Flag{
|
cli.Int64Flag{
|
||||||
Name: "macaroontimeout",
|
Name: "macaroontimeout",
|
||||||
Value: 60,
|
Value: 60,
|
||||||
Usage: "anti-replay macaroon validity time in seconds",
|
Usage: "Anti-replay macaroon validity time in seconds.",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "macaroonip",
|
Name: "macaroonip",
|
||||||
Usage: "if set, lock macaroon to specific IP address",
|
Usage: "If set, lock macaroon to specific IP address.",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "profile, p",
|
||||||
|
Usage: "Instead of reading settings from command " +
|
||||||
|
"line parameters or using the default " +
|
||||||
|
"profile, use a specific profile. If " +
|
||||||
|
"a default profile is set, this flag can be " +
|
||||||
|
"set to an empty string to disable reading " +
|
||||||
|
"values from the profiles file.",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "macfromjar",
|
||||||
|
Usage: "Use this macaroon from the profile's " +
|
||||||
|
"macaroon jar instead of the default one. " +
|
||||||
|
"Can only be used if profiles are defined.",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
app.Commands = []cli.Command{
|
app.Commands = []cli.Command{
|
||||||
@ -321,3 +353,16 @@ func main() {
|
|||||||
fatal(err)
|
fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readPassword reads a password from the terminal. This requires there to be an
|
||||||
|
// actual TTY so passing in a password from stdin won't work.
|
||||||
|
func readPassword(text string) ([]byte, error) {
|
||||||
|
fmt.Print(text)
|
||||||
|
|
||||||
|
// The variable syscall.Stdin is of a different type in the Windows API
|
||||||
|
// that's why we need the explicit cast. And of course the linter
|
||||||
|
// doesn't like it either.
|
||||||
|
pw, err := terminal.ReadPassword(int(syscall.Stdin)) // nolint:unconvert
|
||||||
|
fmt.Println()
|
||||||
|
return pw, err
|
||||||
|
}
|
||||||
|
@ -44,6 +44,65 @@ func (e *profileEntry) cert() (*x509.CertPool, error) {
|
|||||||
return cp, nil
|
return cp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getGlobalOptions returns the global connection options. If a profile file
|
||||||
|
// exists, these global options might be read from a predefined profile. If no
|
||||||
|
// profile exists, the global options from the command line are returned as an
|
||||||
|
// ephemeral profile entry.
|
||||||
|
func getGlobalOptions(ctx *cli.Context) (*profileEntry, error) {
|
||||||
|
var profileName string
|
||||||
|
|
||||||
|
// Try to load the default profile file and depending on its existence
|
||||||
|
// what profile to use.
|
||||||
|
f, err := loadProfileFile(defaultProfileFile)
|
||||||
|
switch {
|
||||||
|
// The legacy case where no profile file exists and the user also didn't
|
||||||
|
// request to use one. We only consider the global options here.
|
||||||
|
case err == errNoProfileFile && !ctx.GlobalIsSet("profile"):
|
||||||
|
return profileFromContext(ctx, false)
|
||||||
|
|
||||||
|
// The file doesn't exist but the user specified an explicit profile.
|
||||||
|
case err == errNoProfileFile && ctx.GlobalIsSet("profile"):
|
||||||
|
return nil, fmt.Errorf("profile file %s does not exist",
|
||||||
|
defaultProfileFile)
|
||||||
|
|
||||||
|
// There is a file but we couldn't read/parse it.
|
||||||
|
case err != nil:
|
||||||
|
return nil, fmt.Errorf("could not read profile file %s: "+
|
||||||
|
"%v", defaultProfileFile, err)
|
||||||
|
|
||||||
|
// The user explicitly disabled the use of profiles for this command by
|
||||||
|
// setting the flag to an empty string. We fall back to the default/old
|
||||||
|
// behavior.
|
||||||
|
case ctx.GlobalIsSet("profile") && ctx.GlobalString("profile") == "":
|
||||||
|
return profileFromContext(ctx, false)
|
||||||
|
|
||||||
|
// There is a file, but no default profile is specified. The user also
|
||||||
|
// didn't specify a profile to use so we fall back to the default/old
|
||||||
|
// behavior.
|
||||||
|
case !ctx.GlobalIsSet("profile") && len(f.Default) == 0:
|
||||||
|
return profileFromContext(ctx, false)
|
||||||
|
|
||||||
|
// The user didn't specify a profile but there is a default one defined.
|
||||||
|
case !ctx.GlobalIsSet("profile") && len(f.Default) > 0:
|
||||||
|
profileName = f.Default
|
||||||
|
|
||||||
|
// The user specified a specific profile to use.
|
||||||
|
case ctx.GlobalIsSet("profile"):
|
||||||
|
profileName = ctx.GlobalString("profile")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got to here, we do have a profile file and know the name of the
|
||||||
|
// profile to use. Now we just need to make sure it does exist.
|
||||||
|
for _, prof := range f.Profiles {
|
||||||
|
if prof.Name == profileName {
|
||||||
|
return prof, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("profile '%s' not found in file %s", profileName,
|
||||||
|
defaultProfileFile)
|
||||||
|
}
|
||||||
|
|
||||||
// profileFromContext creates an ephemeral profile entry from the global options
|
// profileFromContext creates an ephemeral profile entry from the global options
|
||||||
// set in the CLI context.
|
// set in the CLI context.
|
||||||
func profileFromContext(ctx *cli.Context, store bool) (*profileEntry, error) {
|
func profileFromContext(ctx *cli.Context, store bool) (*profileEntry, error) {
|
||||||
|
Loading…
Reference in New Issue
Block a user