lightning-terminal/cmd/litcli/main.go
Viktor Tigerström e30ac3d0a8
multi: update GetInfoResponse response
Add the `commit_hash` field to the GetInfoResponse. The `commit_hash`
field will contain the most recent commit_hash that the build was based
on. If the build had uncommitted changes, this field will contain the
most recent commit hash, suffixed by "-dirty".

The semantics of the `version` field is also updated to always contain
the most recent semantic version of the litd node, following the
semantic versioning 2.0.0 spec (http://semver.org/).
2025-05-13 12:27:19 +02:00

358 lines
9.9 KiB
Go

package main
import (
"context"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strings"
terminal "github.com/lightninglabs/lightning-terminal"
"github.com/lightninglabs/lightning-terminal/litrpc"
"github.com/lightninglabs/lndclient"
"github.com/lightningnetwork/lnd/cmd/commands"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/lightningnetwork/lnd/signal"
"github.com/urfave/cli"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/protobuf/proto"
"gopkg.in/macaroon.v2"
)
const (
// defaultMacaroonTimeout is the default macaroon timeout in seconds
// that we set when sending it over the line.
defaultMacaroonTimeout int64 = 60
// Environment variables names that can be used to set the global flags.
envVarRPCServer = "LITCLI_RPCSERVER"
envVarNetwork = "LITCLI_NETWORK"
envVarBaseDir = "LITCLI_BASEDIR"
envVarTLSCert = "LITCLI_TLSCERTPATH"
envVarMacaroonPath = "LITCLI_MACAROONPATH"
envVarLNDDir = "LITCLI_LNDDIR"
envVarMacaroonTimeout = "LITCLI_MACAROONTIMEOUT"
)
var (
// maxMsgRecvSize is the largest message our client will receive. We
// set this to 200MiB atm.
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200)
baseDirFlag = cli.StringFlag{
Name: "basedir",
Value: terminal.DefaultLitDir,
Usage: "Path to LiT's base directory",
EnvVar: envVarBaseDir,
}
networkFlag = cli.StringFlag{
Name: "network, n",
Usage: "The network litd is running on e.g. mainnet, " +
"testnet, etc.",
Value: terminal.DefaultNetwork,
EnvVar: envVarNetwork,
}
tlsCertFlag = cli.StringFlag{
Name: "tlscertpath",
Usage: "Path to lit's TLS certificate",
Value: terminal.DefaultTLSCertPath,
EnvVar: envVarTLSCert,
}
macaroonPathFlag = cli.StringFlag{
Name: "macaroonpath",
Usage: "Path to lit's macaroon file",
Value: terminal.DefaultMacaroonPath,
EnvVar: envVarMacaroonPath,
}
)
func main() {
app := cli.NewApp()
app.Version = terminal.RichVersion()
app.Name = "litcli"
app.Usage = "control plane for your Lightning Terminal (lit) daemon"
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "rpcserver",
Value: "localhost:8443",
Usage: "LiT daemon address host:port",
EnvVar: envVarRPCServer,
},
networkFlag,
baseDirFlag,
tlsCertFlag,
macaroonPathFlag,
// The following two flags are only required for the 'litcli ln'
// sub commands, because they call into lnd's commands package
// that requires them. They only need to be _defined_, but
// they're not actually actively used.
cli.StringFlag{
Name: "lnddir",
Usage: "Path to lnd's base directory",
Hidden: true,
Value: commands.DefaultLndDir,
EnvVar: envVarLNDDir,
},
cli.Int64Flag{
Name: "macaroontimeout",
Value: 60,
Hidden: true,
Usage: "Anti-replay macaroon validity time in " +
"seconds.",
EnvVar: envVarMacaroonTimeout,
},
}
app.Commands = append(app.Commands, sessionCommands...)
app.Commands = append(app.Commands, accountsCommands...)
app.Commands = append(app.Commands, listActionsCommand)
app.Commands = append(app.Commands, privacyMapCommands)
app.Commands = append(app.Commands, autopilotCommands)
app.Commands = append(app.Commands, litCommands...)
app.Commands = append(app.Commands, helperCommands)
app.Commands = append(app.Commands, statusCommands...)
app.Commands = append(app.Commands, lnCommands...)
err := app.Run(os.Args)
if err != nil {
fatal(err)
}
}
func fatal(err error) {
fmt.Fprintf(os.Stderr, "[litcli] %v\n", err)
os.Exit(1)
}
func connectClient(ctx *cli.Context, noMac bool) (grpc.ClientConnInterface,
func(), error) {
rpcServer := ctx.GlobalString("rpcserver")
tlsCertPath, macPath, err := extractPathArgs(ctx)
if err != nil {
return nil, nil, err
}
conn, err := getClientConn(rpcServer, tlsCertPath, macPath, noMac, nil)
if err != nil {
return nil, nil, err
}
cleanup := func() { _ = conn.Close() }
return conn, cleanup, nil
}
func connectClientWithMac(ctx *cli.Context,
mac []byte) (grpc.ClientConnInterface, func(), error) {
rpcServer := ctx.GlobalString("rpcserver")
tlsCertPath, macPath, err := extractPathArgs(ctx)
if err != nil {
return nil, nil, err
}
conn, err := getClientConn(rpcServer, tlsCertPath, macPath, false, mac)
if err != nil {
return nil, nil, err
}
cleanup := func() { _ = conn.Close() }
return conn, cleanup, nil
}
func getClientConn(address, tlsCertPath, macaroonPath string, noMac bool,
customMac []byte) (*grpc.ClientConn, error) {
opts := []grpc.DialOption{
grpc.WithDefaultCallOptions(maxMsgRecvSize),
}
switch {
case len(customMac) > 0:
macOption, err := macFromBytes(customMac)
if err != nil {
return nil, err
}
opts = append(opts, macOption)
case noMac:
// Don't do anything.
default:
macOption, err := readMacaroon(macaroonPath)
if err != nil {
return nil, err
}
opts = append(opts, macOption)
}
// TLS cannot be disabled, we'll always have a cert file to read.
creds, err := credentials.NewClientTLSFromFile(tlsCertPath, "")
if err != nil {
fatal(err)
}
opts = append(opts, grpc.WithTransportCredentials(creds))
conn, err := grpc.Dial(address, opts...)
if err != nil {
return nil, fmt.Errorf("unable to connect to RPC server: %v",
err)
}
return conn, nil
}
// extractPathArgs parses the TLS certificate and macaroon paths from the
// command.
func extractPathArgs(ctx *cli.Context) (string, string, error) {
// We'll start off by parsing the network. This is needed to determine
// the correct path to the TLS certificate and macaroon when not
// specified.
networkStr := strings.ToLower(ctx.GlobalString("network"))
_, err := lndclient.Network(networkStr).ChainParams()
if err != nil {
return "", "", err
}
// Get the base dir so that we can reconstruct the default tls and
// macaroon paths if needed.
baseDir := lncfg.CleanAndExpandPath(ctx.GlobalString(baseDirFlag.Name))
macaroonPath := lncfg.CleanAndExpandPath(ctx.GlobalString(
macaroonPathFlag.Name,
))
// If the macaroon path flag has not been set to a custom value,
// then reconstruct it with the possibly new base dir and network
// values.
if macaroonPath == terminal.DefaultMacaroonPath {
macaroonPath = filepath.Join(
baseDir, networkStr, terminal.DefaultMacaroonFilename,
)
}
tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString(
tlsCertFlag.Name,
))
// If a custom TLS path was set, use it as is.
if tlsCertPath != terminal.DefaultTLSCertPath {
return tlsCertPath, macaroonPath, nil
}
// If a custom base directory was set, we'll also check if custom paths
// for the TLS cert file was set as well. If not, we'll override the
// paths so they can be found within the custom base directory set.
// This allows us to set a custom base directory, along with custom
// paths to the TLS cert file.
if baseDir != terminal.DefaultLitDir {
tlsCertPath = filepath.Join(
baseDir, terminal.DefaultTLSCertFilename,
)
}
return tlsCertPath, macaroonPath, nil
}
// readMacaroon tries to read the macaroon file at the specified path and create
// gRPC dial options from it.
func readMacaroon(macPath string) (grpc.DialOption, error) {
// Load the specified macaroon file.
macBytes, err := os.ReadFile(macPath)
if err != nil {
return nil, fmt.Errorf("unable to read macaroon path : %v", err)
}
return macFromBytes(macBytes)
}
// macFromBytes returns a macaroon from the given byte slice.
func macFromBytes(macBytes []byte) (grpc.DialOption, error) {
mac := &macaroon.Macaroon{}
if err := mac.UnmarshalBinary(macBytes); err != nil {
return nil, fmt.Errorf("unable to decode macaroon: %v", err)
}
macConstraints := []macaroons.Constraint{
// We add a time-based constraint to prevent replay of the
// macaroon. It's good for 60 seconds by default to make up for
// any discrepancy between client and server clocks, but leaking
// the macaroon before it becomes invalid makes it possible for
// an attacker to reuse the macaroon. In addition, the validity
// time of the macaroon is extended by the time the server clock
// is behind the client clock, or shortened by the time the
// server clock is ahead of the client clock (or invalid
// altogether if, in the latter case, this time is more than 60
// seconds).
macaroons.TimeoutConstraint(defaultMacaroonTimeout),
}
// Apply constraints to the macaroon.
constrainedMac, err := macaroons.AddConstraints(mac, macConstraints...)
if err != nil {
return nil, err
}
// Now we append the macaroon credentials to the dial options.
cred, err := macaroons.NewMacaroonCredential(constrainedMac)
if err != nil {
return nil, fmt.Errorf("error creating macaroon credential: %v",
err)
}
return grpc.WithPerRPCCredentials(cred), nil
}
func printRespJSON(resp proto.Message) { // nolint
jsonBytes, err := lnrpc.ProtoJSONMarshalOpts.Marshal(resp)
if err != nil {
fmt.Println("unable to decode response: ", err)
return
}
fmt.Println(string(jsonBytes))
}
func connectSuperMacClient(ctx context.Context, cli *cli.Context) (
grpc.ClientConnInterface, func(), error) {
litdConn, cleanup, err := connectClient(cli, false)
if err != nil {
return nil, nil, fmt.Errorf("error connecting client: %w", err)
}
defer cleanup()
litClient := litrpc.NewProxyClient(litdConn)
macResp, err := litClient.BakeSuperMacaroon(
ctx, &litrpc.BakeSuperMacaroonRequest{},
)
if err != nil {
return nil, nil, fmt.Errorf("error baking macaroon: %w", err)
}
macBytes, err := hex.DecodeString(macResp.Macaroon)
if err != nil {
return nil, nil, fmt.Errorf("error decoding macaroon: %w", err)
}
return connectClientWithMac(cli, macBytes)
}
func getContext() context.Context {
shutdownInterceptor, err := signal.Intercept()
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
ctxc, cancel := context.WithCancel(context.Background())
go func() {
<-shutdownInterceptor.ShutdownChannel()
cancel()
}()
return ctxc
}