mirror of
https://github.com/lightninglabs/lightning-terminal.git
synced 2026-08-13 12:33:36 +02:00
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/).
358 lines
9.9 KiB
Go
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
|
|
}
|