github.com/decred/dcrlnd@v0.7.6/cmd/dcrlncli/profile.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/x509"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"path"
    11  	"strings"
    12  
    13  	"github.com/decred/dcrlnd/lncfg"
    14  	"github.com/decred/dcrlnd/lnrpc"
    15  	"github.com/decred/dcrlnd/walletunlocker"
    16  	"github.com/urfave/cli"
    17  	"gopkg.in/macaroon.v2"
    18  )
    19  
    20  var (
    21  	errNoProfileFile = errors.New("no profile file found")
    22  )
    23  
    24  // profileEntry is a struct that represents all settings for one specific
    25  // profile.
    26  type profileEntry struct {
    27  	Name        string       `json:"name"`
    28  	RPCServer   string       `json:"rpcserver"`
    29  	LndDir      string       `json:"lnddir"`
    30  	Chain       string       `json:"chain"`
    31  	Network     string       `json:"network"`
    32  	NoMacaroons bool         `json:"no-macaroons,omitempty"`
    33  	TLSCert     string       `json:"tlscert"`
    34  	Macaroons   *macaroonJar `json:"macaroons"`
    35  }
    36  
    37  // cert returns the profile's TLS certificate as a x509 certificate pool.
    38  func (e *profileEntry) cert() (*x509.CertPool, error) {
    39  	if e.TLSCert == "" {
    40  		return nil, nil
    41  	}
    42  
    43  	cp := x509.NewCertPool()
    44  	if !cp.AppendCertsFromPEM([]byte(e.TLSCert)) {
    45  		return nil, fmt.Errorf("credentials: failed to append " +
    46  			"certificate")
    47  	}
    48  	return cp, nil
    49  }
    50  
    51  // getGlobalOptions returns the global connection options. If a profile file
    52  // exists, these global options might be read from a predefined profile. If no
    53  // profile exists, the global options from the command line are returned as an
    54  // ephemeral profile entry.
    55  func getGlobalOptions(ctx *cli.Context, skipMacaroons bool) (*profileEntry,
    56  	error) {
    57  
    58  	var profileName string
    59  
    60  	// Try to load the default profile file and depending on its existence
    61  	// what profile to use.
    62  	f, err := loadProfileFile(defaultProfileFile)
    63  	switch {
    64  	// The legacy case where no profile file exists and the user also didn't
    65  	// request to use one. We only consider the global options here.
    66  	case err == errNoProfileFile && !ctx.GlobalIsSet("profile"):
    67  		return profileFromContext(ctx, false, skipMacaroons)
    68  
    69  	// The file doesn't exist but the user specified an explicit profile.
    70  	case err == errNoProfileFile && ctx.GlobalIsSet("profile"):
    71  		return nil, fmt.Errorf("profile file %s does not exist",
    72  			defaultProfileFile)
    73  
    74  	// There is a file but we couldn't read/parse it.
    75  	case err != nil:
    76  		return nil, fmt.Errorf("could not read profile file %s: "+
    77  			"%v", defaultProfileFile, err)
    78  
    79  	// The user explicitly disabled the use of profiles for this command by
    80  	// setting the flag to an empty string. We fall back to the default/old
    81  	// behavior.
    82  	case ctx.GlobalIsSet("profile") && ctx.GlobalString("profile") == "":
    83  		return profileFromContext(ctx, false, skipMacaroons)
    84  
    85  	// There is a file, but no default profile is specified. The user also
    86  	// didn't specify a profile to use so we fall back to the default/old
    87  	// behavior.
    88  	case !ctx.GlobalIsSet("profile") && len(f.Default) == 0:
    89  		return profileFromContext(ctx, false, skipMacaroons)
    90  
    91  	// The user didn't specify a profile but there is a default one defined.
    92  	case !ctx.GlobalIsSet("profile") && len(f.Default) > 0:
    93  		profileName = f.Default
    94  
    95  	// The user specified a specific profile to use.
    96  	case ctx.GlobalIsSet("profile"):
    97  		profileName = ctx.GlobalString("profile")
    98  	}
    99  
   100  	// If we got to here, we do have a profile file and know the name of the
   101  	// profile to use. Now we just need to make sure it does exist.
   102  	for _, prof := range f.Profiles {
   103  		if prof.Name == profileName {
   104  			return prof, nil
   105  		}
   106  	}
   107  
   108  	return nil, fmt.Errorf("profile '%s' not found in file %s", profileName,
   109  		defaultProfileFile)
   110  }
   111  
   112  // profileFromContext creates an ephemeral profile entry from the global options
   113  // set in the CLI context.
   114  func profileFromContext(ctx *cli.Context, store, skipMacaroons bool) (
   115  	*profileEntry, error) {
   116  
   117  	// Parse the paths of the cert and macaroon. This will validate the
   118  	// chain and network value as well.
   119  	tlsCertPath, macPath, err := extractPathArgs(ctx)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	// Load the certificate file now, if specified. We store it as plain PEM
   125  	// directly.
   126  	var tlsCert []byte
   127  	if tlsCertPath != "" {
   128  		var err error
   129  		tlsCert, err = ioutil.ReadFile(tlsCertPath)
   130  		if err != nil {
   131  			return nil, fmt.Errorf("could not load TLS cert file: %v", err)
   132  		}
   133  	}
   134  
   135  	entry := &profileEntry{
   136  		RPCServer:   ctx.GlobalString("rpcserver"),
   137  		LndDir:      lncfg.CleanAndExpandPath(ctx.GlobalString("lnddir")),
   138  		Chain:       ctx.GlobalString("chain"),
   139  		Network:     ctx.GlobalString("network"),
   140  		NoMacaroons: ctx.GlobalBool("no-macaroons"),
   141  		TLSCert:     string(tlsCert),
   142  	}
   143  
   144  	// If we aren't using macaroons in general (flag --no-macaroons) or
   145  	// don't need macaroons for this command (wallet unlocker), we can now
   146  	// return already.
   147  	if skipMacaroons || ctx.GlobalBool("no-macaroons") {
   148  		return entry, nil
   149  	}
   150  
   151  	// Now load and possibly encrypt the macaroon file.
   152  	macBytes, err := ioutil.ReadFile(macPath)
   153  	if err != nil {
   154  		return nil, fmt.Errorf("unable to read macaroon path (check "+
   155  			"the network setting!): %v", err)
   156  	}
   157  	mac := &macaroon.Macaroon{}
   158  	if err = mac.UnmarshalBinary(macBytes); err != nil {
   159  		return nil, fmt.Errorf("unable to decode macaroon: %v", err)
   160  	}
   161  
   162  	var pw []byte
   163  	if store {
   164  		// Read a password from the terminal. If it's empty, we won't
   165  		// encrypt the macaroon and store it plaintext.
   166  		pw, err = capturePassword(
   167  			"Enter password to encrypt macaroon with or leave "+
   168  				"blank to store in plaintext: ", true,
   169  			walletunlocker.ValidatePassword,
   170  		)
   171  		if err != nil {
   172  			return nil, fmt.Errorf("unable to get encryption "+
   173  				"password: %v", err)
   174  		}
   175  	}
   176  	macEntry := &macaroonEntry{}
   177  	if err = macEntry.storeMacaroon(mac, pw); err != nil {
   178  		return nil, fmt.Errorf("unable to store macaroon: %v", err)
   179  	}
   180  
   181  	// We determine the name of the macaroon from the file itself but cut
   182  	// off the ".macaroon" at the end.
   183  	macEntry.Name = path.Base(macPath)
   184  	if path.Ext(macEntry.Name) == "macaroon" {
   185  		macEntry.Name = strings.TrimSuffix(macEntry.Name, ".macaroon")
   186  	}
   187  
   188  	// Now that we have the macaroon jar as well, let's return the entry
   189  	// with all the values populated.
   190  	entry.Macaroons = &macaroonJar{
   191  		Default: macEntry.Name,
   192  		Timeout: ctx.GlobalInt64("macaroontimeout"),
   193  		IP:      ctx.GlobalString("macaroonip"),
   194  		Jar:     []*macaroonEntry{macEntry},
   195  	}
   196  
   197  	return entry, nil
   198  }
   199  
   200  // loadProfileFile tries to load the file specified and JSON deserialize it into
   201  // the profile file struct.
   202  func loadProfileFile(file string) (*profileFile, error) {
   203  	if !lnrpc.FileExists(file) {
   204  		return nil, errNoProfileFile
   205  	}
   206  
   207  	content, err := ioutil.ReadFile(file)
   208  	if err != nil {
   209  		return nil, fmt.Errorf("could not load profile file %s: %v",
   210  			file, err)
   211  	}
   212  	f := &profileFile{}
   213  	err = f.unmarshalJSON(content)
   214  	if err != nil {
   215  		return nil, fmt.Errorf("could not unmarshal profile file %s: "+
   216  			"%v", file, err)
   217  	}
   218  	return f, nil
   219  }
   220  
   221  // saveProfileFile stores the given profile file struct in the specified file,
   222  // overwriting it if it already existed.
   223  func saveProfileFile(file string, f *profileFile) error {
   224  	content, err := f.marshalJSON()
   225  	if err != nil {
   226  		return fmt.Errorf("could not marshal profile: %v", err)
   227  	}
   228  	return ioutil.WriteFile(file, content, 0644)
   229  }
   230  
   231  // profileFile is a struct that represents the whole content of a profile file.
   232  type profileFile struct {
   233  	Default  string          `json:"default,omitempty"`
   234  	Profiles []*profileEntry `json:"profiles"`
   235  }
   236  
   237  // unmarshalJSON tries to parse the given JSON and unmarshal it into the
   238  // receiving instance.
   239  func (f *profileFile) unmarshalJSON(content []byte) error {
   240  	return json.Unmarshal(content, f)
   241  }
   242  
   243  // marshalJSON serializes the receiving instance to formatted/indented JSON.
   244  func (f *profileFile) marshalJSON() ([]byte, error) {
   245  	b, err := json.Marshal(f)
   246  	if err != nil {
   247  		return nil, fmt.Errorf("error JSON marshalling profile: %v",
   248  			err)
   249  	}
   250  
   251  	var out bytes.Buffer
   252  	err = json.Indent(&out, b, "", "  ")
   253  	if err != nil {
   254  		return nil, fmt.Errorf("error indenting profile JSON: %v", err)
   255  	}
   256  	out.WriteString("\n")
   257  	return out.Bytes(), nil
   258  }