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  }