Skip to content

Commit 9b84dbc

Browse files
ellemoutonjamaljsr
authored andcommitted
cmd/litcli: add litcli for session functions
1 parent 13429b8 commit 9b84dbc

File tree

9 files changed

+523
-31
lines changed

9 files changed

+523
-31
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
.env*.local
33

44
litd-debug
5+
litcli-debug
56
/lightning-terminal-*
67

78
# MacOS junk

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ EXPOSE 8443 10009 9735
6464

6565
# Copy the binaries and entrypoint from the builder image.
6666
COPY --from=golangbuilder /go/bin/litd /bin/
67+
COPY --from=golangbuilder /go/bin/litcli /bin/
6768
COPY --from=golangbuilder /go/bin/lncli /bin/
6869
COPY --from=golangbuilder /go/bin/frcli /bin/
6970
COPY --from=golangbuilder /go/bin/loop /bin/

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,12 @@ install: app-build go-install
108108
go-build:
109109
@$(call print, "Building lightning-terminal.")
110110
$(GOBUILD) -tags="$(LND_RELEASE_TAGS)" -ldflags "$(LDFLAGS)" -o litd-debug $(PKG)/cmd/litd
111+
$(GOBUILD) -tags="$(LND_RELEASE_TAGS)" -ldflags "$(LDFLAGS)" -o litcli-debug $(PKG)/cmd/litcli
111112

112113
go-install:
113114
@$(call print, "Installing lightning-terminal.")
114115
$(GOINSTALL) -tags="$(LND_RELEASE_TAGS)" -ldflags "$(LDFLAGS)" $(PKG)/cmd/litd
116+
$(GOINSTALL) -tags="$(LND_RELEASE_TAGS)" -ldflags "$(LDFLAGS)" $(PKG)/cmd/litcli
115117

116118
go-install-cli:
117119
@$(call print, "Installing all CLI binaries.")

cmd/litcli/main.go

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
"syscall"
11+
12+
terminal "github.com/lightninglabs/lightning-terminal"
13+
"github.com/lightninglabs/lightning-terminal/litrpc"
14+
"github.com/lightninglabs/lndclient"
15+
"github.com/lightninglabs/protobuf-hex-display/jsonpb"
16+
"github.com/lightninglabs/protobuf-hex-display/proto"
17+
"github.com/lightningnetwork/lnd"
18+
"github.com/lightningnetwork/lnd/lncfg"
19+
"github.com/urfave/cli"
20+
"golang.org/x/term"
21+
"google.golang.org/grpc"
22+
"google.golang.org/grpc/credentials"
23+
"google.golang.org/grpc/metadata"
24+
)
25+
26+
const (
27+
// uiPasswordEnvName is the name of the environment variable under which
28+
// we look for the UI password for litcli.
29+
uiPasswordEnvName = "UI_PASSWORD"
30+
)
31+
32+
var (
33+
// maxMsgRecvSize is the largest message our client will receive. We
34+
// set this to 200MiB atm.
35+
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200)
36+
37+
baseDirFlag = cli.StringFlag{
38+
Name: "basedir",
39+
Value: terminal.DefaultLitDir,
40+
Usage: "path to lit's base directory",
41+
}
42+
networkFlag = cli.StringFlag{
43+
Name: "network, n",
44+
Usage: "the network litd is running on e.g. mainnet, " +
45+
"testnet, etc.",
46+
Value: terminal.DefaultNetwork,
47+
}
48+
tlsCertFlag = cli.StringFlag{
49+
Name: "tlscertpath",
50+
Usage: "path to lit's TLS certificate",
51+
Value: terminal.DefaultTLSCertPath,
52+
}
53+
lndMode = cli.StringFlag{
54+
Name: "lndmode",
55+
Usage: "the mode that lnd is running in: remote or integrated",
56+
Value: terminal.ModeIntegrated,
57+
}
58+
lndTlsCertFlag = cli.StringFlag{
59+
Name: "lndtlscertpath",
60+
Usage: "path to lnd's TLS certificate",
61+
Value: lnd.DefaultConfig().TLSCertPath,
62+
}
63+
uiPasswordFlag = cli.StringFlag{
64+
Name: "uipassword",
65+
Usage: "the UI password for authenticating against LiT; if " +
66+
"not specified will read from environment variable " +
67+
uiPasswordEnvName + " or prompt on terminal if both " +
68+
"values are empty",
69+
}
70+
)
71+
72+
func main() {
73+
app := cli.NewApp()
74+
75+
app.Name = "litcli"
76+
app.Usage = "control plane for your Lightning Terminal (lit) daemon"
77+
app.Flags = []cli.Flag{
78+
cli.StringFlag{
79+
Name: "rpcserver",
80+
Value: "localhost:8443",
81+
Usage: "lit daemon address host:port",
82+
},
83+
networkFlag,
84+
baseDirFlag,
85+
lndMode,
86+
tlsCertFlag,
87+
lndTlsCertFlag,
88+
uiPasswordFlag,
89+
}
90+
app.Commands = append(app.Commands, sessionCommands...)
91+
92+
err := app.Run(os.Args)
93+
if err != nil {
94+
fatal(err)
95+
}
96+
}
97+
98+
func fatal(err error) {
99+
fmt.Fprintf(os.Stderr, "[litcli] %v\n", err)
100+
os.Exit(1)
101+
}
102+
103+
func getClient(ctx *cli.Context) (litrpc.SessionsClient, func(), error) {
104+
rpcServer := ctx.GlobalString("rpcserver")
105+
tlsCertPath, err := extractPathArgs(ctx)
106+
if err != nil {
107+
return nil, nil, err
108+
}
109+
conn, err := getClientConn(rpcServer, tlsCertPath)
110+
if err != nil {
111+
return nil, nil, err
112+
}
113+
cleanup := func() { _ = conn.Close() }
114+
115+
sessionsClient := litrpc.NewSessionsClient(conn)
116+
return sessionsClient, cleanup, nil
117+
}
118+
119+
func getClientConn(address, tlsCertPath string) (*grpc.ClientConn, error) {
120+
opts := []grpc.DialOption{
121+
grpc.WithDefaultCallOptions(maxMsgRecvSize),
122+
}
123+
124+
// TLS cannot be disabled, we'll always have a cert file to read.
125+
creds, err := credentials.NewClientTLSFromFile(tlsCertPath, "")
126+
if err != nil {
127+
fatal(err)
128+
}
129+
130+
opts = append(opts, grpc.WithTransportCredentials(creds))
131+
132+
conn, err := grpc.Dial(address, opts...)
133+
if err != nil {
134+
return nil, fmt.Errorf("unable to connect to RPC server: %v",
135+
err)
136+
}
137+
138+
return conn, nil
139+
}
140+
141+
// extractPathArgs parses the TLS certificate from the command.
142+
func extractPathArgs(ctx *cli.Context) (string, error) {
143+
// We'll start off by parsing the network. This is needed to determine
144+
// the correct path to the TLS certificate and macaroon when not
145+
// specified.
146+
networkStr := strings.ToLower(ctx.GlobalString("network"))
147+
_, err := lndclient.Network(networkStr).ChainParams()
148+
if err != nil {
149+
return "", err
150+
}
151+
152+
// We'll now fetch the basedir so we can make a decision on how to
153+
// properly read the cert. This will either be the default,
154+
// or will have been overwritten by the end user.
155+
baseDir := lncfg.CleanAndExpandPath(ctx.GlobalString(baseDirFlag.Name))
156+
lndmode := strings.ToLower(ctx.GlobalString(lndMode.Name))
157+
158+
if lndmode == terminal.ModeIntegrated {
159+
tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString(
160+
lndTlsCertFlag.Name,
161+
))
162+
163+
return tlsCertPath, nil
164+
}
165+
166+
tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString(
167+
tlsCertFlag.Name,
168+
))
169+
170+
// If a custom base directory was set, we'll also check if custom paths
171+
// for the TLS cert file was set as well. If not, we'll override the
172+
// paths so they can be found within the custom base directory set.
173+
// This allows us to set a custom base directory, along with custom
174+
// paths to the TLS cert file.
175+
if baseDir != terminal.DefaultLitDir || networkStr != terminal.DefaultNetwork {
176+
tlsCertPath = filepath.Join(
177+
baseDir, networkStr, terminal.DefaultTLSCertFilename,
178+
)
179+
}
180+
181+
return tlsCertPath, nil
182+
}
183+
184+
func printRespJSON(resp proto.Message) { // nolint
185+
jsonMarshaler := &jsonpb.Marshaler{
186+
EmitDefaults: true,
187+
OrigName: true,
188+
Indent: "\t", // Matches indentation of printJSON.
189+
}
190+
191+
jsonStr, err := jsonMarshaler.MarshalToString(resp)
192+
if err != nil {
193+
fmt.Println("unable to decode response: ", err)
194+
return
195+
}
196+
197+
fmt.Println(jsonStr)
198+
}
199+
200+
func getAuthContext(cliCtx *cli.Context) context.Context {
201+
uiPassword, err := getUIPassword(cliCtx)
202+
if err != nil {
203+
fatal(err)
204+
}
205+
206+
basicAuth := base64.StdEncoding.EncodeToString(
207+
[]byte(fmt.Sprintf("%s:%s", uiPassword, uiPassword)),
208+
)
209+
210+
ctxb := context.Background()
211+
md := metadata.MD{}
212+
213+
md.Set("macaroon", "no-macaroons-for-litcli")
214+
md.Set("authorization", fmt.Sprintf("Basic %s", basicAuth))
215+
216+
return metadata.NewOutgoingContext(ctxb, md)
217+
}
218+
219+
func getUIPassword(ctx *cli.Context) (string, error) {
220+
// The command line flag has precedence.
221+
uiPassword := strings.TrimSpace(ctx.GlobalString(uiPasswordFlag.Name))
222+
223+
// To automate things with litcli, we also offer reading the password
224+
// from environment variables if the flag wasn't specified.
225+
if uiPassword == "" {
226+
uiPassword = strings.TrimSpace(os.Getenv(uiPasswordEnvName))
227+
}
228+
229+
if uiPassword == "" {
230+
// If there's no value in the environment, we'll now prompt the
231+
// user to enter their password on the terminal.
232+
fmt.Printf("Input your LiT UI password: ")
233+
234+
// The variable syscall.Stdin is of a different type in the
235+
// Windows API that's why we need the explicit cast. And of
236+
// course the linter doesn't like it either.
237+
pw, err := term.ReadPassword(int(syscall.Stdin)) // nolint:unconvert
238+
fmt.Println()
239+
240+
if err != nil {
241+
return "", err
242+
}
243+
uiPassword = strings.TrimSpace(string(pw))
244+
}
245+
246+
if uiPassword == "" {
247+
return "", fmt.Errorf("no UI password provided")
248+
}
249+
250+
return uiPassword, nil
251+
}

0 commit comments

Comments
 (0)