1
1
package main
2
2
3
3
import (
4
- "context"
5
- "encoding/base64"
6
- "encoding/hex"
7
4
"fmt"
5
+ "io/ioutil"
8
6
"os"
9
7
"path/filepath"
10
8
"strings"
11
- "syscall"
12
9
13
10
terminal "github.com/lightninglabs/lightning-terminal"
14
11
"github.com/lightninglabs/lightning-terminal/litrpc"
@@ -17,17 +14,17 @@ import (
17
14
"github.com/lightninglabs/protobuf-hex-display/proto"
18
15
"github.com/lightningnetwork/lnd"
19
16
"github.com/lightningnetwork/lnd/lncfg"
17
+ "github.com/lightningnetwork/lnd/macaroons"
20
18
"github.com/urfave/cli"
21
- "golang.org/x/term"
22
19
"google.golang.org/grpc"
23
20
"google.golang.org/grpc/credentials"
24
- "google.golang.org/grpc/metadata "
21
+ "gopkg.in/macaroon.v2 "
25
22
)
26
23
27
24
const (
28
- // uiPasswordEnvName is the name of the environment variable under which
29
- // we look for the UI password for litcli .
30
- uiPasswordEnvName = "UI_PASSWORD"
25
+ // defaultMacaroonTimeout is the default macaroon timeout in seconds
26
+ // that we set when sending it over the line .
27
+ defaultMacaroonTimeout int64 = 60
31
28
)
32
29
33
30
var (
@@ -61,12 +58,10 @@ var (
61
58
Usage : "path to lnd's TLS certificate" ,
62
59
Value : lnd .DefaultConfig ().TLSCertPath ,
63
60
}
64
- uiPasswordFlag = cli.StringFlag {
65
- Name : "uipassword" ,
66
- Usage : "the UI password for authenticating against LiT; if " +
67
- "not specified will read from environment variable " +
68
- uiPasswordEnvName + " or prompt on terminal if both " +
69
- "values are empty" ,
61
+ macaroonPathFlag = cli.StringFlag {
62
+ Name : "macaroonpath" ,
63
+ Usage : "path to lit's macaroon file" ,
64
+ Value : terminal .DefaultMacaroonPath ,
70
65
}
71
66
)
72
67
@@ -87,7 +82,7 @@ func main() {
87
82
lndMode ,
88
83
tlsCertFlag ,
89
84
lndTlsCertFlag ,
90
- uiPasswordFlag ,
85
+ macaroonPathFlag ,
91
86
}
92
87
app .Commands = append (app .Commands , sessionCommands ... )
93
88
@@ -104,11 +99,11 @@ func fatal(err error) {
104
99
105
100
func getClient (ctx * cli.Context ) (litrpc.SessionsClient , func (), error ) {
106
101
rpcServer := ctx .GlobalString ("rpcserver" )
107
- tlsCertPath , err := extractPathArgs (ctx )
102
+ tlsCertPath , macPath , err := extractPathArgs (ctx )
108
103
if err != nil {
109
104
return nil , nil , err
110
105
}
111
- conn , err := getClientConn (rpcServer , tlsCertPath )
106
+ conn , err := getClientConn (rpcServer , tlsCertPath , macPath )
112
107
if err != nil {
113
108
return nil , nil , err
114
109
}
@@ -118,9 +113,18 @@ func getClient(ctx *cli.Context) (litrpc.SessionsClient, func(), error) {
118
113
return sessionsClient , cleanup , nil
119
114
}
120
115
121
- func getClientConn (address , tlsCertPath string ) (* grpc.ClientConn , error ) {
116
+ func getClientConn (address , tlsCertPath , macaroonPath string ) (* grpc.ClientConn ,
117
+ error ) {
118
+
119
+ // We always need to send a macaroon.
120
+ macOption , err := readMacaroon (macaroonPath )
121
+ if err != nil {
122
+ return nil , err
123
+ }
124
+
122
125
opts := []grpc.DialOption {
123
126
grpc .WithDefaultCallOptions (maxMsgRecvSize ),
127
+ macOption ,
124
128
}
125
129
126
130
// TLS cannot be disabled, we'll always have a cert file to read.
@@ -140,15 +144,33 @@ func getClientConn(address, tlsCertPath string) (*grpc.ClientConn, error) {
140
144
return conn , nil
141
145
}
142
146
143
- // extractPathArgs parses the TLS certificate from the command.
144
- func extractPathArgs (ctx * cli.Context ) (string , error ) {
147
+ // extractPathArgs parses the TLS certificate and macaroon paths from the
148
+ // command.
149
+ func extractPathArgs (ctx * cli.Context ) (string , string , error ) {
145
150
// We'll start off by parsing the network. This is needed to determine
146
151
// the correct path to the TLS certificate and macaroon when not
147
152
// specified.
148
153
networkStr := strings .ToLower (ctx .GlobalString ("network" ))
149
154
_ , err := lndclient .Network (networkStr ).ChainParams ()
150
155
if err != nil {
151
- return "" , err
156
+ return "" , "" , err
157
+ }
158
+
159
+ // Get the base dir so that we can reconstruct the default tls and
160
+ // macaroon paths if needed.
161
+ baseDir := lncfg .CleanAndExpandPath (ctx .GlobalString (baseDirFlag .Name ))
162
+
163
+ macaroonPath := lncfg .CleanAndExpandPath (ctx .GlobalString (
164
+ macaroonPathFlag .Name ,
165
+ ))
166
+
167
+ // If the macaroon path flag has not been set to a custom value,
168
+ // then reconstruct it with the possibly new base dir and network
169
+ // values.
170
+ if macaroonPath == terminal .DefaultMacaroonPath {
171
+ macaroonPath = filepath .Join (
172
+ baseDir , networkStr , terminal .DefaultMacaroonFilename ,
173
+ )
152
174
}
153
175
154
176
// Get the LND mode. If Lit is in integrated LND mode, then LND's tls
@@ -159,7 +181,7 @@ func extractPathArgs(ctx *cli.Context) (string, error) {
159
181
lndTlsCertFlag .Name ,
160
182
))
161
183
162
- return tlsCertPath , nil
184
+ return tlsCertPath , macaroonPath , nil
163
185
}
164
186
165
187
// Lit is in remote LND mode. So we need Lit's tls cert.
@@ -169,89 +191,78 @@ func extractPathArgs(ctx *cli.Context) (string, error) {
169
191
170
192
// If a custom TLS path was set, use it as is.
171
193
if tlsCertPath != terminal .DefaultTLSCertPath {
172
- return tlsCertPath , nil
194
+ return tlsCertPath , macaroonPath , nil
173
195
}
174
196
175
197
// If a custom base directory was set, we'll also check if custom paths
176
198
// for the TLS cert file was set as well. If not, we'll override the
177
199
// paths so they can be found within the custom base directory set.
178
200
// This allows us to set a custom base directory, along with custom
179
201
// paths to the TLS cert file.
180
- baseDir := lncfg .CleanAndExpandPath (ctx .GlobalString (baseDirFlag .Name ))
181
202
if baseDir != terminal .DefaultLitDir {
182
203
tlsCertPath = filepath .Join (
183
204
baseDir , terminal .DefaultTLSCertFilename ,
184
205
)
185
206
}
186
207
187
- return tlsCertPath , nil
208
+ return tlsCertPath , macaroonPath , nil
188
209
}
189
210
190
- func printRespJSON (resp proto.Message ) { // nolint
191
- jsonMarshaler := & jsonpb.Marshaler {
192
- EmitDefaults : true ,
193
- OrigName : true ,
194
- Indent : "\t " , // Matches indentation of printJSON.
211
+ // readMacaroon tries to read the macaroon file at the specified path and create
212
+ // gRPC dial options from it.
213
+ func readMacaroon (macPath string ) (grpc.DialOption , error ) {
214
+ // Load the specified macaroon file.
215
+ macBytes , err := ioutil .ReadFile (macPath )
216
+ if err != nil {
217
+ return nil , fmt .Errorf ("unable to read macaroon path : %v" , err )
195
218
}
196
219
197
- jsonStr , err := jsonMarshaler .MarshalToString (resp )
198
- if err != nil {
199
- fmt .Println ("unable to decode response: " , err )
200
- return
220
+ mac := & macaroon.Macaroon {}
221
+ if err = mac .UnmarshalBinary (macBytes ); err != nil {
222
+ return nil , fmt .Errorf ("unable to decode macaroon: %v" , err )
201
223
}
202
224
203
- fmt .Println (jsonStr )
204
- }
225
+ macConstraints := []macaroons.Constraint {
226
+ // We add a time-based constraint to prevent replay of the
227
+ // macaroon. It's good for 60 seconds by default to make up for
228
+ // any discrepancy between client and server clocks, but leaking
229
+ // the macaroon before it becomes invalid makes it possible for
230
+ // an attacker to reuse the macaroon. In addition, the validity
231
+ // time of the macaroon is extended by the time the server clock
232
+ // is behind the client clock, or shortened by the time the
233
+ // server clock is ahead of the client clock (or invalid
234
+ // altogether if, in the latter case, this time is more than 60
235
+ // seconds).
236
+ macaroons .TimeoutConstraint (defaultMacaroonTimeout ),
237
+ }
205
238
206
- func getAuthContext ( cliCtx * cli. Context ) context. Context {
207
- uiPassword , err := getUIPassword ( cliCtx )
239
+ // Apply constraints to the macaroon.
240
+ constrainedMac , err := macaroons . AddConstraints ( mac , macConstraints ... )
208
241
if err != nil {
209
- fatal ( err )
242
+ return nil , err
210
243
}
211
244
212
- basicAuth := base64 .StdEncoding .EncodeToString (
213
- []byte (fmt .Sprintf ("%s:%s" , uiPassword , uiPassword )),
214
- )
215
-
216
- ctxb := context .Background ()
217
- md := metadata.MD {}
218
-
219
- md .Set ("macaroon" , hex .EncodeToString (terminal .EmptyMacaroonBytes ))
220
- md .Set ("authorization" , fmt .Sprintf ("Basic %s" , basicAuth ))
221
-
222
- return metadata .NewOutgoingContext (ctxb , md )
223
- }
224
-
225
- func getUIPassword (ctx * cli.Context ) (string , error ) {
226
- // The command line flag has precedence.
227
- uiPassword := strings .TrimSpace (ctx .GlobalString (uiPasswordFlag .Name ))
228
-
229
- // To automate things with litcli, we also offer reading the password
230
- // from environment variables if the flag wasn't specified.
231
- if uiPassword == "" {
232
- uiPassword = strings .TrimSpace (os .Getenv (uiPasswordEnvName ))
245
+ // Now we append the macaroon credentials to the dial options.
246
+ cred , err := macaroons .NewMacaroonCredential (constrainedMac )
247
+ if err != nil {
248
+ return nil , fmt .Errorf ("error creating macaroon credential: %v" ,
249
+ err )
233
250
}
251
+ return grpc .WithPerRPCCredentials (cred ), nil
252
+ }
234
253
235
- if uiPassword == "" {
236
- // If there's no value in the environment, we'll now prompt the
237
- // user to enter their password on the terminal.
238
- fmt .Printf ("Input your LiT UI password: " )
239
-
240
- // The variable syscall.Stdin is of a different type in the
241
- // Windows API that's why we need the explicit cast. And of
242
- // course the linter doesn't like it either.
243
- pw , err := term .ReadPassword (int (syscall .Stdin )) // nolint:unconvert
244
- fmt .Println ()
245
-
246
- if err != nil {
247
- return "" , err
248
- }
249
- uiPassword = strings .TrimSpace (string (pw ))
254
+ func printRespJSON (resp proto.Message ) { // nolint
255
+ jsonMarshaler := & jsonpb.Marshaler {
256
+ EmitDefaults : true ,
257
+ OrigName : true ,
258
+ Indent : "\t " , // Matches indentation of printJSON.
250
259
}
251
260
252
- if uiPassword == "" {
253
- return "" , fmt .Errorf ("no UI password provided" )
261
+ jsonStr , err := jsonMarshaler .MarshalToString (resp )
262
+ if err != nil {
263
+ fmt .Println ("unable to decode response: " , err )
264
+ return
254
265
}
255
266
256
- return uiPassword , nil
267
+ fmt . Println ( jsonStr )
257
268
}
0 commit comments