lnd.xprv/cmd/lncli/profile.go
2021-06-22 01:32:54 +02:00

259 lines
7.8 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 tlsCertPath != "" {
var err error
tlsCert, err = ioutil.ReadFile(tlsCertPath)
if err != nil {
return nil, fmt.Errorf("could not load TLS cert file: %v", 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
}