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 }