lnd/cmd/lncli/profile.go
Oliver Gugger b28bf29c5f
lncli: don't read macaroons when not using them
When we either don't use macaroons (because the global --no-macaroons
flag is set) or don't need them (for the wallet unlocker commands), we
don't try to read the file at all to avoid running into an error if the
file doesn't exist (which it doesn't in those two cases).
2020-09-25 15:54:10 +02:00

260 lines
7.9 KiB
Go

package main
import (
"bytes"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"path"
"strings"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/walletunlocker"
"github.com/urfave/cli"
"gopkg.in/macaroon.v2"
)
var (
errNoProfileFile = errors.New("no profile file found")
)
// profileEntry is a struct that represents all settings for one specific
// profile.
type profileEntry struct {
Name string `json:"name"`
RPCServer string `json:"rpcserver"`
LndDir string `json:"lnddir"`
Chain string `json:"chain"`
Network string `json:"network"`
NoMacaroons bool `json:"no-macaroons,omitempty"`
TLSCert string `json:"tlscert"`
Macaroons *macaroonJar `json:"macaroons"`
}
// cert returns the profile's TLS certificate as a x509 certificate pool.
func (e *profileEntry) cert() (*x509.CertPool, error) {
if e.TLSCert == "" {
return nil, nil
}
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM([]byte(e.TLSCert)) {
return nil, fmt.Errorf("credentials: failed to append " +
"certificate")
}
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, skipMacaroons bool) (*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, skipMacaroons)
// 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, skipMacaroons)
// 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, skipMacaroons)
// 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
// set in the CLI context.
func profileFromContext(ctx *cli.Context, store, skipMacaroons bool) (
*profileEntry, error) {
// Parse the paths of the cert and macaroon. This will validate the
// chain and network value as well.
tlsCertPath, macPath, err := extractPathArgs(ctx)
if err != nil {
return nil, err
}
// Load the certificate file now, if specified. We store it as plain PEM
// directly.
var tlsCert []byte
if lnrpc.FileExists(tlsCertPath) {
var err error
tlsCert, err = ioutil.ReadFile(tlsCertPath)
if err != nil {
return nil, fmt.Errorf("could not load TLS cert file "+
"%s: %v", tlsCertPath, err)
}
}
entry := &profileEntry{
RPCServer: ctx.GlobalString("rpcserver"),
LndDir: lncfg.CleanAndExpandPath(ctx.GlobalString("lnddir")),
Chain: ctx.GlobalString("chain"),
Network: ctx.GlobalString("network"),
NoMacaroons: ctx.GlobalBool("no-macaroons"),
TLSCert: string(tlsCert),
}
// If we aren't using macaroons in general (flag --no-macaroons) or
// don't need macaroons for this command (wallet unlocker), we can now
// return already.
if skipMacaroons || ctx.GlobalBool("no-macaroons") {
return entry, nil
}
// Now load and possibly encrypt the macaroon file.
macBytes, err := ioutil.ReadFile(macPath)
if err != nil {
return nil, fmt.Errorf("unable to read macaroon path (check "+
"the network setting!): %v", err)
}
mac := &macaroon.Macaroon{}
if err = mac.UnmarshalBinary(macBytes); err != nil {
return nil, fmt.Errorf("unable to decode macaroon: %v", err)
}
var pw []byte
if store {
// Read a password from the terminal. If it's empty, we won't
// encrypt the macaroon and store it plaintext.
pw, err = capturePassword(
"Enter password to encrypt macaroon with or leave "+
"blank to store in plaintext: ", true,
walletunlocker.ValidatePassword,
)
if err != nil {
return nil, fmt.Errorf("unable to get encryption "+
"password: %v", err)
}
}
macEntry := &macaroonEntry{}
if err = macEntry.storeMacaroon(mac, pw); err != nil {
return nil, fmt.Errorf("unable to store macaroon: %v", err)
}
// We determine the name of the macaroon from the file itself but cut
// off the ".macaroon" at the end.
macEntry.Name = path.Base(macPath)
if path.Ext(macEntry.Name) == "macaroon" {
macEntry.Name = strings.TrimSuffix(macEntry.Name, ".macaroon")
}
// Now that we have the macaroon jar as well, let's return the entry
// with all the values populated.
entry.Macaroons = &macaroonJar{
Default: macEntry.Name,
Timeout: ctx.GlobalInt64("macaroontimeout"),
IP: ctx.GlobalString("macaroonip"),
Jar: []*macaroonEntry{macEntry},
}
return entry, nil
}
// loadProfileFile tries to load the file specified and JSON deserialize it into
// the profile file struct.
func loadProfileFile(file string) (*profileFile, error) {
if !lnrpc.FileExists(file) {
return nil, errNoProfileFile
}
content, err := ioutil.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("could not load profile file %s: %v",
file, err)
}
f := &profileFile{}
err = f.unmarshalJSON(content)
if err != nil {
return nil, fmt.Errorf("could not unmarshal profile file %s: "+
"%v", file, err)
}
return f, nil
}
// saveProfileFile stores the given profile file struct in the specified file,
// overwriting it if it already existed.
func saveProfileFile(file string, f *profileFile) error {
content, err := f.marshalJSON()
if err != nil {
return fmt.Errorf("could not marshal profile: %v", err)
}
return ioutil.WriteFile(file, content, 0644)
}
// profileFile is a struct that represents the whole content of a profile file.
type profileFile struct {
Default string `json:"default,omitempty"`
Profiles []*profileEntry `json:"profiles"`
}
// unmarshalJSON tries to parse the given JSON and unmarshal it into the
// receiving instance.
func (f *profileFile) unmarshalJSON(content []byte) error {
return json.Unmarshal(content, f)
}
// marshalJSON serializes the receiving instance to formatted/indented JSON.
func (f *profileFile) marshalJSON() ([]byte, error) {
b, err := json.Marshal(f)
if err != nil {
return nil, fmt.Errorf("error JSON marshalling profile: %v",
err)
}
var out bytes.Buffer
err = json.Indent(&out, b, "", " ")
if err != nil {
return nil, fmt.Errorf("error indenting profile JSON: %v", err)
}
out.WriteString("\n")
return out.Bytes(), nil
}