Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 98 additions & 83 deletions cmd/litcli/main.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
package main

import (
"context"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note, this means that in stateless mode, litcli calls wont work and all calls will need to go through tht UI.

Which UI?

Isn't it the case that litcli can still just use the macaroon to interact w/ litd? Stateless just means we don't write the macaroon to disk, but as long as litcli can find that macaroon somehow, then things should work.

Copy link
Member Author

@ellemouton ellemouton Jun 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which UI?

The UI served from the Litd binary.

but as long as litcli can find that macaroon somehow, then things should work.

yeah 👍 updated the commit message 👍

"encoding/base64"
"encoding/hex"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"syscall"

terminal "github.com/lightninglabs/lightning-terminal"
"github.com/lightninglabs/lightning-terminal/litrpc"
Expand All @@ -17,17 +14,17 @@ import (
"github.com/lightninglabs/protobuf-hex-display/proto"
"github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/urfave/cli"
"golang.org/x/term"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"gopkg.in/macaroon.v2"
)

const (
// uiPasswordEnvName is the name of the environment variable under which
// we look for the UI password for litcli.
uiPasswordEnvName = "UI_PASSWORD"
// defaultMacaroonTimeout is the default macaroon timeout in seconds
// that we set when sending it over the line.
defaultMacaroonTimeout int64 = 60
)

var (
Expand Down Expand Up @@ -61,12 +58,10 @@ var (
Usage: "path to lnd's TLS certificate",
Value: lnd.DefaultConfig().TLSCertPath,
}
uiPasswordFlag = cli.StringFlag{
Name: "uipassword",
Usage: "the UI password for authenticating against LiT; if " +
"not specified will read from environment variable " +
uiPasswordEnvName + " or prompt on terminal if both " +
"values are empty",
macaroonPathFlag = cli.StringFlag{
Name: "macaroonpath",
Usage: "path to lit's macaroon file",
Value: terminal.DefaultMacaroonPath,
}
)

Expand All @@ -87,7 +82,7 @@ func main() {
lndMode,
tlsCertFlag,
lndTlsCertFlag,
uiPasswordFlag,
macaroonPathFlag,
}
app.Commands = append(app.Commands, sessionCommands...)

Expand All @@ -104,11 +99,11 @@ func fatal(err error) {

func getClient(ctx *cli.Context) (litrpc.SessionsClient, func(), error) {
rpcServer := ctx.GlobalString("rpcserver")
tlsCertPath, err := extractPathArgs(ctx)
tlsCertPath, macPath, err := extractPathArgs(ctx)
if err != nil {
return nil, nil, err
}
conn, err := getClientConn(rpcServer, tlsCertPath)
conn, err := getClientConn(rpcServer, tlsCertPath, macPath)
if err != nil {
return nil, nil, err
}
Expand All @@ -118,9 +113,18 @@ func getClient(ctx *cli.Context) (litrpc.SessionsClient, func(), error) {
return sessionsClient, cleanup, nil
}

func getClientConn(address, tlsCertPath string) (*grpc.ClientConn, error) {
func getClientConn(address, tlsCertPath, macaroonPath string) (*grpc.ClientConn,
error) {

// We always need to send a macaroon.
macOption, err := readMacaroon(macaroonPath)
if err != nil {
return nil, err
}

opts := []grpc.DialOption{
grpc.WithDefaultCallOptions(maxMsgRecvSize),
macOption,
}

// TLS cannot be disabled, we'll always have a cert file to read.
Expand All @@ -140,114 +144,125 @@ func getClientConn(address, tlsCertPath string) (*grpc.ClientConn, error) {
return conn, nil
}

// extractPathArgs parses the TLS certificate from the command.
func extractPathArgs(ctx *cli.Context) (string, error) {
// 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
return "", "", err
}

// We'll now fetch the basedir so we can make a decision on how to
// properly read the cert. This will either be the default,
// or will have been overwritten by the end user.
// Get the base dir so that we can reconstruct the default tls and
// macaroon paths if needed.
baseDir := lncfg.CleanAndExpandPath(ctx.GlobalString(baseDirFlag.Name))
lndmode := strings.ToLower(ctx.GlobalString(lndMode.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,
)
}

// Get the LND mode. If Lit is in integrated LND mode, then LND's tls
// cert is used directly. Otherwise, Lit's own tls cert is used.
lndmode := strings.ToLower(ctx.GlobalString(lndMode.Name))
if lndmode == terminal.ModeIntegrated {
tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString(
lndTlsCertFlag.Name,
))

return tlsCertPath, nil
return tlsCertPath, macaroonPath, nil
}

// Lit is in remote LND mode. So we need Lit's tls cert.
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 || networkStr != terminal.DefaultNetwork {
if baseDir != terminal.DefaultLitDir {
tlsCertPath = filepath.Join(
baseDir, networkStr, terminal.DefaultTLSCertFilename,
baseDir, terminal.DefaultTLSCertFilename,
)
}

return tlsCertPath, nil
return tlsCertPath, macaroonPath, nil
}

func printRespJSON(resp proto.Message) { // nolint
jsonMarshaler := &jsonpb.Marshaler{
EmitDefaults: true,
OrigName: true,
Indent: "\t", // Matches indentation of printJSON.
// 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 := ioutil.ReadFile(macPath)
if err != nil {
return nil, fmt.Errorf("unable to read macaroon path : %v", err)
}

jsonStr, err := jsonMarshaler.MarshalToString(resp)
if err != nil {
fmt.Println("unable to decode response: ", err)
return
mac := &macaroon.Macaroon{}
if err = mac.UnmarshalBinary(macBytes); err != nil {
return nil, fmt.Errorf("unable to decode macaroon: %v", err)
}

fmt.Println(jsonStr)
}
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),
}

func getAuthContext(cliCtx *cli.Context) context.Context {
uiPassword, err := getUIPassword(cliCtx)
// Apply constraints to the macaroon.
constrainedMac, err := macaroons.AddConstraints(mac, macConstraints...)
if err != nil {
fatal(err)
return nil, err
}

basicAuth := base64.StdEncoding.EncodeToString(
[]byte(fmt.Sprintf("%s:%s", uiPassword, uiPassword)),
)

ctxb := context.Background()
md := metadata.MD{}

md.Set("macaroon", hex.EncodeToString(terminal.EmptyMacaroonBytes))
md.Set("authorization", fmt.Sprintf("Basic %s", basicAuth))

return metadata.NewOutgoingContext(ctxb, md)
}

func getUIPassword(ctx *cli.Context) (string, error) {
// The command line flag has precedence.
uiPassword := strings.TrimSpace(ctx.GlobalString(uiPasswordFlag.Name))

// To automate things with litcli, we also offer reading the password
// from environment variables if the flag wasn't specified.
if uiPassword == "" {
uiPassword = strings.TrimSpace(os.Getenv(uiPasswordEnvName))
// 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
}

if uiPassword == "" {
// If there's no value in the environment, we'll now prompt the
// user to enter their password on the terminal.
fmt.Printf("Input your LiT UI password: ")

// The variable syscall.Stdin is of a different type in the
// Windows API that's why we need the explicit cast. And of
// course the linter doesn't like it either.
pw, err := term.ReadPassword(int(syscall.Stdin)) // nolint:unconvert
fmt.Println()

if err != nil {
return "", err
}
uiPassword = strings.TrimSpace(string(pw))
func printRespJSON(resp proto.Message) { // nolint
jsonMarshaler := &jsonpb.Marshaler{
EmitDefaults: true,
OrigName: true,
Indent: "\t", // Matches indentation of printJSON.
}

if uiPassword == "" {
return "", fmt.Errorf("no UI password provided")
jsonStr, err := jsonMarshaler.MarshalToString(resp)
if err != nil {
fmt.Println("unable to decode response: ", err)
return
}

return uiPassword, nil
fmt.Println(jsonStr)
}
10 changes: 7 additions & 3 deletions cmd/litcli/sessions.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"encoding/hex"
"fmt"
"time"
Expand Down Expand Up @@ -88,8 +89,9 @@ func addSession(ctx *cli.Context) error {
sessionLength := time.Second * time.Duration(ctx.Uint64("expiry"))
sessionExpiry := time.Now().Add(sessionLength).Unix()

ctxb := context.Background()
resp, err := client.AddSession(
getAuthContext(ctx), &litrpc.AddSessionRequest{
ctxb, &litrpc.AddSessionRequest{
Label: label,
SessionType: sessType,
ExpiryTimestampSeconds: uint64(sessionExpiry),
Expand Down Expand Up @@ -196,8 +198,9 @@ func listSessions(filter sessionFilter) func(ctx *cli.Context) error {
}
defer cleanup()

ctxb := context.Background()
resp, err := client.ListSessions(
getAuthContext(ctx), &litrpc.ListSessionsRequest{},
ctxb, &litrpc.ListSessionsRequest{},
)
if err != nil {
return err
Expand Down Expand Up @@ -248,8 +251,9 @@ func revokeSession(ctx *cli.Context) error {
return err
}

ctxb := context.Background()
resp, err := client.RevokeSession(
getAuthContext(ctx), &litrpc.RevokeSessionRequest{
ctxb, &litrpc.RevokeSessionRequest{
LocalPublicKey: pubkey,
},
)
Expand Down
21 changes: 21 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ const (
// certificate. The value corresponds to 14 months
// (14 months * 30 days * 24 hours).
DefaultAutogenValidity = 14 * 30 * 24 * time.Hour

// DefaultMacaroonFilename is the default file name for the
// autogenerated lit macaroon.
DefaultMacaroonFilename = "lit.macaroon"
)

var (
Expand Down Expand Up @@ -119,6 +123,12 @@ var (
lndDefaultConfig.DataDir, defaultLndChainSubDir,
defaultLndChain, DefaultNetwork, defaultLndMacaroon,
)

// DefaultMacaroonPath is the default full path of the base lit
// macaroon.
DefaultMacaroonPath = filepath.Join(
DefaultLitDir, DefaultNetwork, DefaultMacaroonFilename,
)
)

// Config is the main configuration struct of lightning-terminal. It contains
Expand All @@ -141,6 +151,8 @@ type Config struct {
LitDir string `long:"lit-dir" description:"The main directory where LiT looks for its configuration file. If LiT is running in 'remote' lnd mode, this is also the directory where the TLS certificates and log files are stored by default."`
ConfigFile string `long:"configfile" description:"Path to LiT's configuration file."`

MacaroonPath string `long:"macaroonpath" description:"Path to write the macaroon for litd's RPC and REST services if it doesn't exist."`

// Network is the Bitcoin network we're running on. This will be parsed
// before the configuration is loaded and will set the correct flag on
// `lnd.bitcoin.mainnet|testnet|regtest` and also for the other daemons.
Expand Down Expand Up @@ -296,6 +308,7 @@ func defaultConfig() *Config {
LitDir: DefaultLitDir,
LetsEncryptListen: defaultLetsEncryptListen,
LetsEncryptDir: defaultLetsEncryptDir,
MacaroonPath: DefaultMacaroonPath,
ConfigFile: defaultConfigFile,
FaradayMode: defaultFaradayMode,
Faraday: &faradayDefaultConfig,
Expand Down Expand Up @@ -394,6 +407,14 @@ func loadAndValidateConfig(interceptor signal.Interceptor) (*Config, error) {
"UI, at least %d characters long", uiPasswordMinLength)
}

if cfg.Network != DefaultNetwork {
if cfg.MacaroonPath == DefaultMacaroonPath {
cfg.MacaroonPath = filepath.Join(
litDir, cfg.Network, DefaultMacaroonFilename,
)
}
}

// Initiate our listeners. For now, we only support listening on one
// port at a time because we can only pass in one pre-configured RPC
// listener into lnd.
Expand Down
Loading