github.com/decred/dcrlnd@v0.7.6/cmd/dcrlncli/main.go (about) 1 // Copyright (c) 2013-2017 The btcsuite developers 2 // Copyright (c) 2015-2019 The Decred developers 3 // Copyright (C) 2015-2017 The Lightning Network Developers 4 5 package main 6 7 import ( 8 "crypto/tls" 9 "fmt" 10 "os" 11 "path/filepath" 12 "runtime" 13 "strings" 14 "syscall" 15 16 "github.com/decred/dcrd/dcrutil/v4" 17 "github.com/decred/dcrlnd/build" 18 "github.com/decred/dcrlnd/lncfg" 19 "github.com/decred/dcrlnd/lnrpc" 20 "github.com/decred/dcrlnd/macaroons" 21 "github.com/urfave/cli" 22 23 "golang.org/x/term" 24 "google.golang.org/grpc" 25 "google.golang.org/grpc/credentials" 26 ) 27 28 const ( 29 defaultDataDir = "data" 30 defaultChainSubDir = "chain" 31 defaultTLSCertFilename = "tls.cert" 32 defaultMacaroonFilename = "admin.macaroon" 33 defaultRPCPort = "10009" 34 defaultRPCHostPort = "localhost:" + defaultRPCPort 35 ) 36 37 var ( 38 defaultLndDir = dcrutil.AppDataDir("dcrlnd", false) 39 defaultTLSCertPath = filepath.Join(defaultLndDir, defaultTLSCertFilename) 40 41 // maxMsgRecvSize is the largest message our client will receive. We 42 // set this to 200MiB atm. 43 maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200) 44 ) 45 46 func fatal(err error) { 47 fmt.Fprintf(os.Stderr, "[dcrlncli] %v\n", err) 48 os.Exit(1) 49 } 50 51 func getWalletUnlockerClient(ctx *cli.Context) (lnrpc.WalletUnlockerClient, func()) { 52 conn := getClientConn(ctx, true) 53 54 cleanUp := func() { 55 conn.Close() 56 } 57 58 return lnrpc.NewWalletUnlockerClient(conn), cleanUp 59 } 60 61 func getStateServiceClient(ctx *cli.Context) (lnrpc.StateClient, func()) { 62 conn := getClientConn(ctx, true) 63 64 cleanUp := func() { 65 conn.Close() 66 } 67 68 return lnrpc.NewStateClient(conn), cleanUp 69 } 70 71 func getClient(ctx *cli.Context) (lnrpc.LightningClient, func()) { 72 conn := getClientConn(ctx, false) 73 74 cleanUp := func() { 75 conn.Close() 76 } 77 78 return lnrpc.NewLightningClient(conn), cleanUp 79 } 80 81 func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn { 82 // First, we'll get the selected stored profile or an ephemeral one 83 // created from the global options in the CLI context. 84 profile, err := getGlobalOptions(ctx, skipMacaroons) 85 if err != nil { 86 fatal(fmt.Errorf("could not load global options: %v", err)) 87 } 88 89 // Load the specified TLS certificate. 90 certPool, err := profile.cert() 91 if err != nil { 92 fatal(fmt.Errorf("could not create cert pool: %v", err)) 93 } 94 95 // Build transport credentials from the certificate pool. If there is no 96 // certificate pool, we expect the server to use a non-self-signed 97 // certificate such as a certificate obtained from Let's Encrypt. 98 var creds credentials.TransportCredentials 99 if certPool != nil { 100 creds = credentials.NewClientTLSFromCert(certPool, "") 101 } else { 102 // Fallback to the system pool. Using an empty tls config is an 103 // alternative to x509.SystemCertPool(). That call is not 104 // supported on Windows. 105 creds = credentials.NewTLS(&tls.Config{}) 106 } 107 108 // Create a dial options array. 109 opts := []grpc.DialOption{ 110 grpc.WithTransportCredentials(creds), 111 } 112 113 // Only process macaroon credentials if --no-macaroons isn't set and 114 // if we're not skipping macaroon processing. 115 if !profile.NoMacaroons && !skipMacaroons { 116 // Find out which macaroon to load. 117 macName := profile.Macaroons.Default 118 if ctx.GlobalIsSet("macfromjar") { 119 macName = ctx.GlobalString("macfromjar") 120 } 121 var macEntry *macaroonEntry 122 for _, entry := range profile.Macaroons.Jar { 123 if entry.Name == macName { 124 macEntry = entry 125 break 126 } 127 } 128 if macEntry == nil { 129 fatal(fmt.Errorf("macaroon with name '%s' not found "+ 130 "in profile", macName)) 131 } 132 133 // Get and possibly decrypt the specified macaroon. 134 // 135 // TODO(guggero): Make it possible to cache the password so we 136 // don't need to ask for it every time. 137 mac, err := macEntry.loadMacaroon(readPassword) 138 if err != nil { 139 fatal(fmt.Errorf("could not load macaroon: %v", err)) 140 } 141 142 macConstraints := []macaroons.Constraint{ 143 // We add a time-based constraint to prevent replay of the 144 // macaroon. It's good for 60 seconds by default to make up for 145 // any discrepancy between client and server clocks, but leaking 146 // the macaroon before it becomes invalid makes it possible for 147 // an attacker to reuse the macaroon. In addition, the validity 148 // time of the macaroon is extended by the time the server clock 149 // is behind the client clock, or shortened by the time the 150 // server clock is ahead of the client clock (or invalid 151 // altogether if, in the latter case, this time is more than 60 152 // seconds). 153 // TODO(aakselrod): add better anti-replay protection. 154 macaroons.TimeoutConstraint(profile.Macaroons.Timeout), 155 156 // Lock macaroon down to a specific IP address. 157 macaroons.IPLockConstraint(profile.Macaroons.IP), 158 159 // ... Add more constraints if needed. 160 } 161 162 // Apply constraints to the macaroon. 163 constrainedMac, err := macaroons.AddConstraints( 164 mac, macConstraints..., 165 ) 166 if err != nil { 167 fatal(err) 168 } 169 170 // Now we append the macaroon credentials to the dial options. 171 cred, err := macaroons.NewMacaroonCredential(constrainedMac) 172 if err != nil { 173 fatal(fmt.Errorf("error cloning mac: %v", err)) 174 } 175 opts = append(opts, grpc.WithPerRPCCredentials(cred)) 176 } 177 178 // We need to use a custom dialer so we can also connect to unix sockets 179 // and not just TCP addresses. 180 genericDialer := lncfg.ClientAddressDialer(defaultRPCPort) 181 opts = append(opts, grpc.WithContextDialer(genericDialer)) 182 opts = append(opts, grpc.WithDefaultCallOptions(maxMsgRecvSize)) 183 184 conn, err := grpc.Dial(profile.RPCServer, opts...) 185 if err != nil { 186 fatal(fmt.Errorf("unable to connect to RPC server: %v", err)) 187 } 188 189 return conn 190 } 191 192 // extractPathArgs parses the TLS certificate and macaroon paths from the 193 // command. 194 func extractPathArgs(ctx *cli.Context) (string, string, error) { 195 // We'll start off by parsing the active chain and network. These are 196 // needed to determine the correct path to the macaroon when not 197 // specified. 198 chain := strings.ToLower(ctx.GlobalString("chain")) 199 switch chain { 200 case "decred": 201 default: 202 return "", "", fmt.Errorf("unknown chain: %v", chain) 203 } 204 205 // We default to mainnet if no other options are specified. 206 network := "mainnet" 207 208 networks := []string{"testnet", "regtest", "simnet"} 209 numNets := 0 210 211 for _, n := range networks { 212 if ctx.GlobalBool(n) { 213 network = n 214 numNets++ 215 } 216 } 217 218 if numNets > 1 { 219 str := "extractPathArgs: The testnet, regtest, and simnet params" + 220 "can't be used together -- choose one of the three" 221 err := fmt.Errorf(str) 222 223 return "", "", err 224 } 225 226 // We'll now fetch the lnddir so we can make a decision on how to 227 // properly read the macaroons (if needed) and also the cert. This will 228 // either be the default, or will have been overwritten by the end 229 // user. 230 lndDir := lncfg.CleanAndExpandPath(ctx.GlobalString("lnddir")) 231 232 // If the macaroon path as been manually provided, then we'll only 233 // target the specified file. 234 var macPath string 235 if ctx.GlobalString("macaroonpath") != "" { 236 macPath = lncfg.CleanAndExpandPath(ctx.GlobalString("macaroonpath")) 237 } else { 238 // Otherwise, we'll go into the path: 239 // lnddir/data/chain/<chain>/<network> in order to fetch the 240 // macaroon that we need. 241 macPath = filepath.Join( 242 lndDir, defaultDataDir, defaultChainSubDir, chain, 243 network, defaultMacaroonFilename, 244 ) 245 } 246 247 tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString("tlscertpath")) 248 249 // If a custom dcrlnd directory was set, we'll also check if custom 250 // paths for the TLS cert and macaroon file were set as well. If not, 251 // we'll override their paths so they can be found within the custom 252 // dcrlnd directory set. This allows us to set a custom lnd directory, 253 // along with custom paths to the TLS cert and macaroon file. 254 if lndDir != defaultLndDir { 255 tlsCertPath = filepath.Join(lndDir, defaultTLSCertFilename) 256 } 257 258 return tlsCertPath, macPath, nil 259 } 260 261 // checkNotBothSet accepts two flag names, a and b, and checks that only flag a 262 // or flag b can be set, but not both. It returns the name of the flag or an 263 // error. 264 // 265 //nolint:unused 266 func checkNotBothSet(ctx *cli.Context, a, b string) (string, error) { 267 if ctx.IsSet(a) && ctx.IsSet(b) { 268 return "", fmt.Errorf( 269 "either %s or %s should be set, but not both", a, b, 270 ) 271 272 } 273 274 if ctx.IsSet(a) { 275 return a, nil 276 277 } 278 279 return b, nil 280 } 281 282 func main() { 283 // Use the standart decred -V (capital V) flag for version. 284 cli.VersionFlag = cli.BoolFlag{ 285 Name: "version, V", 286 Usage: "print the version", 287 } 288 289 // Build the standard version string. 290 versionStr := fmt.Sprintf("%s (Go version %s %s/%s)", 291 build.Version(), runtime.Version(), runtime.GOOS, runtime.GOARCH) 292 293 app := cli.NewApp() 294 app.Name = "dcrlncli" 295 app.Version = versionStr 296 app.Usage = "control plane for your Decred Lightning Network Daemon (dcrlnd)" 297 app.Flags = []cli.Flag{ 298 cli.StringFlag{ 299 Name: "rpcserver", 300 Value: defaultRPCHostPort, 301 Usage: "The host:port of Decred LN daemon", 302 }, 303 cli.StringFlag{ 304 Name: "lnddir", 305 Value: defaultLndDir, 306 Usage: "The path to dcrlnd's base directory", 307 }, 308 cli.StringFlag{ 309 Name: "tlscertpath", 310 Value: defaultTLSCertPath, 311 Usage: "The path to lnd's TLS certificate.", 312 }, 313 cli.StringFlag{ 314 Name: "chain, c", 315 Usage: "The chain lnd is running on e.g. decred", 316 Value: "decred", 317 }, 318 cli.BoolFlag{ 319 Name: "testnet", 320 Usage: "Use the test network", 321 }, 322 cli.BoolFlag{ 323 Name: "simnet", 324 Usage: "Use the simulation network", 325 }, 326 cli.BoolFlag{ 327 Name: "regtest", 328 Usage: "Use the regression test network", 329 }, 330 cli.BoolFlag{ 331 Name: "no-macaroons", 332 Usage: "Disable macaroon authentication.", 333 }, 334 cli.StringFlag{ 335 Name: "macaroonpath", 336 Usage: "The path to macaroon file.", 337 }, 338 cli.Int64Flag{ 339 Name: "macaroontimeout", 340 Value: 60, 341 Usage: "Anti-replay macaroon validity time in seconds.", 342 }, 343 cli.StringFlag{ 344 Name: "macaroonip", 345 Usage: "If set, lock macaroon to specific IP address.", 346 }, 347 cli.StringFlag{ 348 Name: "profile, p", 349 Usage: "Instead of reading settings from command " + 350 "line parameters or using the default " + 351 "profile, use a specific profile. If " + 352 "a default profile is set, this flag can be " + 353 "set to an empty string to disable reading " + 354 "values from the profiles file.", 355 }, 356 cli.StringFlag{ 357 Name: "macfromjar", 358 Usage: "Use this macaroon from the profile's " + 359 "macaroon jar instead of the default one. " + 360 "Can only be used if profiles are defined.", 361 }, 362 } 363 app.Commands = []cli.Command{ 364 createCommand, 365 createWatchOnlyCommand, 366 unlockCommand, 367 changePasswordCommand, 368 newAddressCommand, 369 estimateFeeCommand, 370 sendManyCommand, 371 sendCoinsCommand, 372 listUnspentCommand, 373 connectCommand, 374 disconnectCommand, 375 openChannelCommand, 376 batchOpenChannelCommand, 377 closeChannelCommand, 378 closeAllChannelsCommand, 379 abandonChannelCommand, 380 listPeersCommand, 381 walletBalanceCommand, 382 channelBalanceCommand, 383 getInfoCommand, 384 getRecoveryInfoCommand, 385 pendingChannelsCommand, 386 sendPaymentCommand, 387 payInvoiceCommand, 388 sendToRouteCommand, 389 addInvoiceCommand, 390 lookupInvoiceCommand, 391 listInvoicesCommand, 392 listChannelsCommand, 393 closedChannelsCommand, 394 listPaymentsCommand, 395 describeGraphCommand, 396 getNodeMetricsCommand, 397 getChanInfoCommand, 398 getNodeInfoCommand, 399 queryRoutesCommand, 400 getNetworkInfoCommand, 401 debugLevelCommand, 402 calcPayStatsCommand, 403 decodePayReqCommand, 404 listChainTxnsCommand, 405 stopCommand, 406 signMessageCommand, 407 verifyMessageCommand, 408 feeReportCommand, 409 updateChannelPolicyCommand, 410 forwardingHistoryCommand, 411 exportChanBackupCommand, 412 verifyChanBackupCommand, 413 restoreChanBackupCommand, 414 bakeMacaroonCommand, 415 listMacaroonIDsCommand, 416 deleteMacaroonIDCommand, 417 listPermissionsCommand, 418 printMacaroonCommand, 419 trackPaymentCommand, 420 versionCommand, 421 profileSubCommand, 422 getStateCommand, 423 deletePaymentsCommand, 424 sendCustomCommand, 425 subscribeCustomCommand, 426 } 427 428 // Add any extra commands determined by build flags. 429 app.Commands = append(app.Commands, autopilotCommands()...) 430 app.Commands = append(app.Commands, invoicesCommands()...) 431 app.Commands = append(app.Commands, routerCommands()...) 432 app.Commands = append(app.Commands, walletCommands()...) 433 app.Commands = append(app.Commands, watchtowerCommands()...) 434 app.Commands = append(app.Commands, wtclientCommands()...) 435 436 if err := app.Run(os.Args); err != nil { 437 fatal(err) 438 } 439 } 440 441 // readPassword reads a password from the terminal. This requires there to be an 442 // actual TTY so passing in a password from stdin won't work. 443 func readPassword(text string) ([]byte, error) { 444 fmt.Print(text) 445 446 // The variable syscall.Stdin is of a different type in the Windows API 447 // that's why we need the explicit cast. And of course the linter 448 // doesn't like it either. 449 pw, err := term.ReadPassword(int(syscall.Stdin)) // nolint:unconvert 450 fmt.Println() 451 return pw, err 452 }