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

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"path"
     8  	"strings"
     9  
    10  	"github.com/decred/dcrd/dcrutil/v4"
    11  	"github.com/decred/dcrlnd/lncfg"
    12  	"github.com/urfave/cli"
    13  	"gopkg.in/macaroon.v2"
    14  )
    15  
    16  var (
    17  	// defaultDcrlncliDir is the default directory to store the profile file
    18  	// in. This defaults to:
    19  	//   C:\Users\<username>\AppData\Local\Dcrlncli\ on Windows
    20  	//   ~/.dcrlncli/ on Linux
    21  	//   ~/Library/Application Support/Dcrlncli/ on MacOS
    22  	defaultDcrlncliDir = dcrutil.AppDataDir("dcrlncli", false)
    23  
    24  	// defaultProfileFile is the full, absolute path of the profile file.
    25  	defaultProfileFile = path.Join(defaultDcrlncliDir, "profiles.json")
    26  )
    27  
    28  var profileSubCommand = cli.Command{
    29  	Name:     "profile",
    30  	Category: "Profiles",
    31  	Usage:    "Create and manage lncli profiles.",
    32  	Description: `
    33  	Profiles for lncli are an easy and comfortable way to manage multiple
    34  	nodes from the command line by storing node specific parameters like RPC
    35  	host, network, TLS certificate path or macaroons in a named profile.
    36  
    37  	To use a predefined profile, just use the '--profile=myprofile' (or
    38  	short version '-p=myprofile') with any lncli command.
    39  
    40  	A default profile can also be defined, lncli will then always use the
    41  	connection/node parameters from that profile instead of the default
    42  	values.
    43  
    44  	WARNING: Setting a default profile changes the default behavior of
    45  	lncli! To disable the use of the default profile for a single command,
    46  	set '--profile= '.
    47  
    48  	The profiles are stored in a file called profiles.json in the user's
    49  	home directory, for example:
    50  		C:\Users\<username>\AppData\Local\Lncli\profiles.json on Windows
    51  		~/.lncli/profiles.json on Linux
    52  		~/Library/Application Support/Lncli/profiles.json on MacOS
    53  	`,
    54  	Subcommands: []cli.Command{
    55  		profileListCommand,
    56  		profileAddCommand,
    57  		profileRemoveCommand,
    58  		profileSetDefaultCommand,
    59  		profileUnsetDefaultCommand,
    60  		profileAddMacaroonCommand,
    61  	},
    62  }
    63  
    64  var profileListCommand = cli.Command{
    65  	Name:   "list",
    66  	Usage:  "Lists all lncli profiles",
    67  	Action: profileList,
    68  }
    69  
    70  func profileList(_ *cli.Context) error {
    71  	f, err := loadProfileFile(defaultProfileFile)
    72  	if err != nil {
    73  		return err
    74  	}
    75  
    76  	printJSON(f)
    77  	return nil
    78  }
    79  
    80  var profileAddCommand = cli.Command{
    81  	Name:      "add",
    82  	Usage:     "Add a new profile.",
    83  	ArgsUsage: "name",
    84  	Description: `
    85  	Add a new named profile to the main profiles.json. All global options
    86  	(see 'lncli --help') passed into this command are stored in that named
    87  	profile.
    88  	`,
    89  	Flags: []cli.Flag{
    90  		cli.StringFlag{
    91  			Name:  "name",
    92  			Usage: "the name of the new profile",
    93  		},
    94  		cli.BoolFlag{
    95  			Name:  "default",
    96  			Usage: "set the new profile to be the default profile",
    97  		},
    98  	},
    99  	Action: profileAdd,
   100  }
   101  
   102  func profileAdd(ctx *cli.Context) error {
   103  	if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
   104  		return cli.ShowCommandHelp(ctx, "add")
   105  	}
   106  
   107  	// Load the default profile file or create a new one if it doesn't exist
   108  	// yet.
   109  	f, err := loadProfileFile(defaultProfileFile)
   110  	switch {
   111  	case err == errNoProfileFile:
   112  		f = &profileFile{}
   113  		_ = os.MkdirAll(path.Dir(defaultProfileFile), 0700)
   114  
   115  	case err != nil:
   116  		return err
   117  	}
   118  
   119  	// Create a profile struct from all the global options.
   120  	profile, err := profileFromContext(ctx, true, false)
   121  	if err != nil {
   122  		return fmt.Errorf("could not load global options: %v", err)
   123  	}
   124  
   125  	// Finally, all that's left is to get the profile name from either
   126  	// positional argument or flag.
   127  	args := ctx.Args()
   128  	switch {
   129  	case ctx.IsSet("name"):
   130  		profile.Name = ctx.String("name")
   131  	case args.Present():
   132  		profile.Name = args.First()
   133  	default:
   134  		return fmt.Errorf("name argument missing")
   135  	}
   136  
   137  	// Is there already a profile with that name?
   138  	for _, p := range f.Profiles {
   139  		if p.Name == profile.Name {
   140  			return fmt.Errorf("a profile with the name %s already "+
   141  				"exists", profile.Name)
   142  		}
   143  	}
   144  
   145  	// Do we need to update the default entry to be this one?
   146  	if ctx.Bool("default") {
   147  		f.Default = profile.Name
   148  	}
   149  
   150  	// All done, store the updated profile file.
   151  	f.Profiles = append(f.Profiles, profile)
   152  	if err = saveProfileFile(defaultProfileFile, f); err != nil {
   153  		return fmt.Errorf("error writing profile file %s: %v",
   154  			defaultProfileFile, err)
   155  	}
   156  
   157  	fmt.Printf("Profile %s added to file %s.\n", profile.Name,
   158  		defaultProfileFile)
   159  	return nil
   160  }
   161  
   162  var profileRemoveCommand = cli.Command{
   163  	Name:        "remove",
   164  	Usage:       "Remove a profile",
   165  	ArgsUsage:   "name",
   166  	Description: `Remove the specified profile from the profile file.`,
   167  	Flags: []cli.Flag{
   168  		cli.StringFlag{
   169  			Name:  "name",
   170  			Usage: "the name of the profile to delete",
   171  		},
   172  	},
   173  	Action: profileRemove,
   174  }
   175  
   176  func profileRemove(ctx *cli.Context) error {
   177  	if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
   178  		return cli.ShowCommandHelp(ctx, "remove")
   179  	}
   180  
   181  	// Load the default profile file.
   182  	f, err := loadProfileFile(defaultProfileFile)
   183  	if err != nil {
   184  		return fmt.Errorf("could not load profile file: %v", err)
   185  	}
   186  
   187  	// Get the profile name from either positional argument or flag.
   188  	var (
   189  		args  = ctx.Args()
   190  		name  string
   191  		found = false
   192  	)
   193  	switch {
   194  	case ctx.IsSet("name"):
   195  		name = ctx.String("name")
   196  	case args.Present():
   197  		name = args.First()
   198  	default:
   199  		return fmt.Errorf("name argument missing")
   200  	}
   201  
   202  	// Create a copy of all profiles but don't include the one to delete.
   203  	newProfiles := make([]*profileEntry, 0, len(f.Profiles)-1)
   204  	for _, p := range f.Profiles {
   205  		// Skip the one we want to delete.
   206  		if p.Name == name {
   207  			found = true
   208  
   209  			if p.Name == f.Default {
   210  				fmt.Println("Warning: removing default profile.")
   211  			}
   212  			continue
   213  		}
   214  
   215  		// Keep all others.
   216  		newProfiles = append(newProfiles, p)
   217  	}
   218  
   219  	// If what we were looking for didn't exist in the first place, there's
   220  	// no need for updating the file.
   221  	if !found {
   222  		return fmt.Errorf("profile with name %s not found in file",
   223  			name)
   224  	}
   225  
   226  	// Great, everything updated, now let's save the file.
   227  	f.Profiles = newProfiles
   228  	return saveProfileFile(defaultProfileFile, f)
   229  }
   230  
   231  var profileSetDefaultCommand = cli.Command{
   232  	Name:      "setdefault",
   233  	Usage:     "Set the default profile.",
   234  	ArgsUsage: "name",
   235  	Description: `
   236  	Set a specified profile to be used as the default profile.
   237  
   238  	WARNING: Setting a default profile changes the default behavior of
   239  	lncli! To disable the use of the default profile for a single command,
   240  	set '--profile= '.
   241  	`,
   242  	Flags: []cli.Flag{
   243  		cli.StringFlag{
   244  			Name:  "name",
   245  			Usage: "the name of the profile to set as default",
   246  		},
   247  	},
   248  	Action: profileSetDefault,
   249  }
   250  
   251  func profileSetDefault(ctx *cli.Context) error {
   252  	if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
   253  		return cli.ShowCommandHelp(ctx, "setdefault")
   254  	}
   255  
   256  	// Load the default profile file.
   257  	f, err := loadProfileFile(defaultProfileFile)
   258  	if err != nil {
   259  		return fmt.Errorf("could not load profile file: %v", err)
   260  	}
   261  
   262  	// Get the profile name from either positional argument or flag.
   263  	var (
   264  		args  = ctx.Args()
   265  		name  string
   266  		found = false
   267  	)
   268  	switch {
   269  	case ctx.IsSet("name"):
   270  		name = ctx.String("name")
   271  	case args.Present():
   272  		name = args.First()
   273  	default:
   274  		return fmt.Errorf("name argument missing")
   275  	}
   276  
   277  	// Make sure the new default profile actually exists.
   278  	for _, p := range f.Profiles {
   279  		if p.Name == name {
   280  			found = true
   281  			f.Default = p.Name
   282  
   283  			break
   284  		}
   285  	}
   286  
   287  	// If the default profile doesn't exist, there's no need for updating
   288  	// the file.
   289  	if !found {
   290  		return fmt.Errorf("profile with name %s not found in file",
   291  			name)
   292  	}
   293  
   294  	// Great, everything updated, now let's save the file.
   295  	return saveProfileFile(defaultProfileFile, f)
   296  }
   297  
   298  var profileUnsetDefaultCommand = cli.Command{
   299  	Name:  "unsetdefault",
   300  	Usage: "Unsets the default profile.",
   301  	Description: `
   302  	Disables the use of a default profile and restores lncli to its original
   303  	behavior.
   304  	`,
   305  	Action: profileUnsetDefault,
   306  }
   307  
   308  func profileUnsetDefault(_ *cli.Context) error {
   309  	// Load the default profile file.
   310  	f, err := loadProfileFile(defaultProfileFile)
   311  	if err != nil {
   312  		return fmt.Errorf("could not load profile file: %v", err)
   313  	}
   314  
   315  	// Save the file with the flag disabled.
   316  	f.Default = ""
   317  	return saveProfileFile(defaultProfileFile, f)
   318  }
   319  
   320  var profileAddMacaroonCommand = cli.Command{
   321  	Name:      "addmacaroon",
   322  	Usage:     "Add a macaroon to a profile's macaroon jar.",
   323  	ArgsUsage: "macaroon-name",
   324  	Description: `
   325  	Add an additional macaroon specified by the global option --macaroonpath
   326  	to an existing profile's macaroon jar.
   327  
   328  	If no profile is selected, the macaroon is added to the default profile
   329  	(if one exists). To add a macaroon to a specific profile, use the global
   330  	--profile=myprofile option.
   331  
   332  	If multiple macaroons exist in a profile's macaroon jar, the one to use
   333  	can be specified with the global option --macfromjar=xyz.
   334  	`,
   335  	Flags: []cli.Flag{
   336  		cli.StringFlag{
   337  			Name:  "name",
   338  			Usage: "the name of the macaroon",
   339  		},
   340  		cli.BoolFlag{
   341  			Name: "default",
   342  			Usage: "set the new macaroon to be the default " +
   343  				"macaroon in the jar",
   344  		},
   345  	},
   346  	Action: profileAddMacaroon,
   347  }
   348  
   349  func profileAddMacaroon(ctx *cli.Context) error {
   350  	if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
   351  		return cli.ShowCommandHelp(ctx, "addmacaroon")
   352  	}
   353  
   354  	// Load the default profile file or create a new one if it doesn't exist
   355  	// yet.
   356  	f, err := loadProfileFile(defaultProfileFile)
   357  	if err != nil {
   358  		return fmt.Errorf("could not load profile file: %v", err)
   359  	}
   360  
   361  	// Finally, all that's left is to get the profile name from either
   362  	// positional argument or flag.
   363  	var (
   364  		args        = ctx.Args()
   365  		profileName string
   366  		macName     string
   367  	)
   368  	switch {
   369  	case ctx.IsSet("name"):
   370  		macName = ctx.String("name")
   371  	case args.Present():
   372  		macName = args.First()
   373  	default:
   374  		return fmt.Errorf("name argument missing")
   375  	}
   376  
   377  	// Make sure the user actually set a macaroon path to use.
   378  	if !ctx.GlobalIsSet("macaroonpath") {
   379  		return fmt.Errorf("macaroonpath global option missing")
   380  	}
   381  
   382  	// Find out which profile we should add the macaroon. The global flag
   383  	// takes precedence over the default profile.
   384  	if f.Default != "" {
   385  		profileName = f.Default
   386  	}
   387  	if ctx.GlobalIsSet("profile") {
   388  		profileName = ctx.GlobalString("profile")
   389  	}
   390  	if len(strings.TrimSpace(profileName)) == 0 {
   391  		return fmt.Errorf("no profile specified and no default " +
   392  			"profile exists")
   393  	}
   394  
   395  	// Is there a profile with that name?
   396  	var selectedProfile *profileEntry
   397  	for _, p := range f.Profiles {
   398  		if p.Name == profileName {
   399  			selectedProfile = p
   400  			break
   401  		}
   402  	}
   403  	if selectedProfile == nil {
   404  		return fmt.Errorf("profile with name %s not found", profileName)
   405  	}
   406  
   407  	// Does a macaroon with that name already exist?
   408  	for _, m := range selectedProfile.Macaroons.Jar {
   409  		if m.Name == macName {
   410  			return fmt.Errorf("a macaroon with the name %s "+
   411  				"already exists", macName)
   412  		}
   413  	}
   414  
   415  	// Do we need to update the default entry to be this one?
   416  	if ctx.Bool("default") {
   417  		selectedProfile.Macaroons.Default = macName
   418  	}
   419  
   420  	// Now load and possibly encrypt the macaroon file.
   421  	macPath := lncfg.CleanAndExpandPath(ctx.GlobalString("macaroonpath"))
   422  	macBytes, err := ioutil.ReadFile(macPath)
   423  	if err != nil {
   424  		return fmt.Errorf("unable to read macaroon path: %v", err)
   425  	}
   426  	mac := &macaroon.Macaroon{}
   427  	if err = mac.UnmarshalBinary(macBytes); err != nil {
   428  		return fmt.Errorf("unable to decode macaroon: %v", err)
   429  	}
   430  	macEntry := &macaroonEntry{
   431  		Name: macName,
   432  	}
   433  	if err = macEntry.storeMacaroon(mac, nil); err != nil {
   434  		return fmt.Errorf("unable to store macaroon: %v", err)
   435  	}
   436  
   437  	// All done, store the updated profile file.
   438  	selectedProfile.Macaroons.Jar = append(
   439  		selectedProfile.Macaroons.Jar, macEntry,
   440  	)
   441  	if err = saveProfileFile(defaultProfileFile, f); err != nil {
   442  		return fmt.Errorf("error writing profile file %s: %v",
   443  			defaultProfileFile, err)
   444  	}
   445  
   446  	fmt.Printf("Macaroon %s added to profile %s in file %s.\n", macName,
   447  		selectedProfile.Name, defaultProfileFile)
   448  	return nil
   449  }