@@ -11,10 +11,13 @@ import (
11
11
"fmt"
12
12
"net/http"
13
13
"os"
14
+ "regexp"
14
15
"runtime/debug"
16
+ "strings"
15
17
"syscall/js"
16
18
17
19
"github.com/btcsuite/btcd/btcec/v2"
20
+ "github.com/golang/protobuf/proto"
18
21
"github.com/jessevdk/go-flags"
19
22
"github.com/lightninglabs/faraday/frdrpc"
20
23
"github.com/lightninglabs/lightning-node-connect/mailbox"
@@ -34,27 +37,36 @@ import (
34
37
"github.com/lightningnetwork/lnd/lnrpc/wtclientrpc"
35
38
"github.com/lightningnetwork/lnd/signal"
36
39
"google.golang.org/grpc"
40
+ "gopkg.in/macaroon-bakery.v2/bakery"
41
+ "gopkg.in/macaroon-bakery.v2/bakery/checkers"
42
+ "gopkg.in/macaroon.v2"
37
43
)
38
44
39
45
type stubPackageRegistration func (map [string ]func (context.Context ,
40
46
* grpc.ClientConn , string , func (string , error )))
41
47
42
- var registrations = []stubPackageRegistration {
43
- lnrpc .RegisterLightningJSONCallbacks ,
44
- lnrpc .RegisterStateJSONCallbacks ,
45
- autopilotrpc .RegisterAutopilotJSONCallbacks ,
46
- chainrpc .RegisterChainNotifierJSONCallbacks ,
47
- invoicesrpc .RegisterInvoicesJSONCallbacks ,
48
- routerrpc .RegisterRouterJSONCallbacks ,
49
- signrpc .RegisterSignerJSONCallbacks ,
50
- verrpc .RegisterVersionerJSONCallbacks ,
51
- walletrpc .RegisterWalletKitJSONCallbacks ,
52
- watchtowerrpc .RegisterWatchtowerJSONCallbacks ,
53
- wtclientrpc .RegisterWatchtowerClientJSONCallbacks ,
54
- looprpc .RegisterSwapClientJSONCallbacks ,
55
- poolrpc .RegisterTraderJSONCallbacks ,
56
- frdrpc .RegisterFaradayServerJSONCallbacks ,
57
- }
48
+ var (
49
+ registrations = []stubPackageRegistration {
50
+ lnrpc .RegisterLightningJSONCallbacks ,
51
+ lnrpc .RegisterStateJSONCallbacks ,
52
+ autopilotrpc .RegisterAutopilotJSONCallbacks ,
53
+ chainrpc .RegisterChainNotifierJSONCallbacks ,
54
+ invoicesrpc .RegisterInvoicesJSONCallbacks ,
55
+ routerrpc .RegisterRouterJSONCallbacks ,
56
+ signrpc .RegisterSignerJSONCallbacks ,
57
+ verrpc .RegisterVersionerJSONCallbacks ,
58
+ walletrpc .RegisterWalletKitJSONCallbacks ,
59
+ watchtowerrpc .RegisterWatchtowerJSONCallbacks ,
60
+ wtclientrpc .RegisterWatchtowerClientJSONCallbacks ,
61
+ looprpc .RegisterSwapClientJSONCallbacks ,
62
+ poolrpc .RegisterTraderJSONCallbacks ,
63
+ frdrpc .RegisterFaradayServerJSONCallbacks ,
64
+ }
65
+
66
+ perms = getAllMethodPermissions ()
67
+
68
+ jsonCBRegex = regexp .MustCompile ("(\\ w+)\\ .(\\ w+)\\ .(\\ w+)" )
69
+ )
58
70
59
71
func main () {
60
72
defer func () {
@@ -104,6 +116,9 @@ func main() {
104
116
callbacks .Set ("wasmClientDisconnect" , js .FuncOf (wc .Disconnect ))
105
117
callbacks .Set ("wasmClientInvokeRPC" , js .FuncOf (wc .InvokeRPC ))
106
118
callbacks .Set ("wasmClientStatus" , js .FuncOf (wc .Status ))
119
+ callbacks .Set ("wasmClientGetExpiry" , js .FuncOf (wc .GetExpiry ))
120
+ callbacks .Set ("wasmClientHasPerms" , js .FuncOf (wc .HasPermissions ))
121
+ callbacks .Set ("wasmClientIsReadOnly" , js .FuncOf (wc .IsReadOnly ))
107
122
js .Global ().Set (cfg .NameSpace , callbacks )
108
123
109
124
for _ , registration := range registrations {
@@ -127,6 +142,8 @@ type wasmClient struct {
127
142
128
143
statusChecker func () mailbox.ConnStatus
129
144
145
+ mac * macaroon.Macaroon
146
+
130
147
registry map [string ]func (context.Context , * grpc.ClientConn ,
131
148
string , func (string , error ))
132
149
}
@@ -202,10 +219,28 @@ func (w *wasmClient) ConnectServer(_ js.Value, args []js.Value) interface{} {
202
219
),
203
220
)
204
221
}, func (data []byte ) error {
222
+ parts := strings .Split (string (data ), ": " )
223
+ if len (parts ) != 2 || parts [0 ] != "Macaroon" {
224
+ return fmt .Errorf ("authdata does " +
225
+ "not contain a macaroon" )
226
+ }
227
+
228
+ macBytes , err := hex .DecodeString (parts [1 ])
229
+ if err != nil {
230
+ return err
231
+ }
232
+
233
+ mac := & macaroon.Macaroon {}
234
+ err = mac .UnmarshalBinary (macBytes )
235
+ if err != nil {
236
+ return fmt .Errorf ("unable to decode " +
237
+ "macaroon: %v" , err )
238
+ }
239
+
240
+ w .mac = mac
241
+
205
242
return callJsCallback (
206
- w .cfg .OnAuthData , hex .EncodeToString (
207
- data ,
208
- ),
243
+ w .cfg .OnAuthData , string (data ),
209
244
)
210
245
},
211
246
)
@@ -281,6 +316,134 @@ func (w *wasmClient) InvokeRPC(_ js.Value, args []js.Value) interface{} {
281
316
282
317
}
283
318
319
+ func (w * wasmClient ) GetExpiry (_ js.Value , _ []js.Value ) interface {} {
320
+ if w .mac == nil {
321
+ log .Errorf ("macaroon not obtained yet. GetExpiry should " +
322
+ "only be called once the connection is complete" )
323
+ return nil
324
+ }
325
+
326
+ expiry , found := checkers .ExpiryTime (nil , w .mac .Caveats ())
327
+ if ! found {
328
+ return nil
329
+ }
330
+
331
+ return js .ValueOf (expiry .Unix ())
332
+ }
333
+
334
+ func (w * wasmClient ) IsReadOnly (_ js.Value , _ []js.Value ) interface {} {
335
+ if w .mac == nil {
336
+ log .Errorf ("macaroon not obtained yet. IsReadOnly should " +
337
+ "only be called once the connection is complete" )
338
+ return js .ValueOf (false )
339
+ }
340
+
341
+ macOps , err := extractMacaroonOps (w .mac )
342
+ if err != nil {
343
+ log .Errorf ("could not extract macaroon ops: %v" , err )
344
+ return js .ValueOf (false )
345
+ }
346
+
347
+ // Check that the macaroon contains each of the required permissions
348
+ // for the given URI.
349
+ return js .ValueOf (isReadOnly (macOps ))
350
+ }
351
+
352
+ func (w * wasmClient ) HasPermissions (_ js.Value , args []js.Value ) interface {} {
353
+ if len (args ) != 1 {
354
+ return js .ValueOf (false )
355
+ }
356
+
357
+ if w .mac == nil {
358
+ log .Errorf ("macaroon not obtained yet. HasPermissions should " +
359
+ "only be called once the connection is complete" )
360
+ return js .ValueOf (false )
361
+ }
362
+
363
+ // Convert JSON callback to grpc URI. JSON callbacks are of the form:
364
+ // `lnrpc.Lightning.WalletBalance` and the corresponding grpc URI is of
365
+ // the form: `/lnrpc.Lightning/WalletBalance`. So to convert the one to
366
+ // the other, we first convert all the `.` into `/`. Then we replace the
367
+ // first `/` back to a `.` and then we prepend the result with a `/`.
368
+ uri := jsonCBRegex .ReplaceAllString (args [0 ].String (), "/$1.$2/$3" )
369
+
370
+ ops , ok := perms [uri ]
371
+ if ! ok {
372
+ log .Errorf ("uri %s not found in known permissions list" , uri )
373
+ return js .ValueOf (false )
374
+ }
375
+
376
+ macOps , err := extractMacaroonOps (w .mac )
377
+ if err != nil {
378
+ log .Errorf ("could not extract macaroon ops: %v" , err )
379
+ return js .ValueOf (false )
380
+ }
381
+
382
+ // Check that the macaroon contains each of the required permissions
383
+ // for the given URI.
384
+ return js .ValueOf (hasPermissions (macOps , ops ))
385
+ }
386
+
387
+ // extractMacaroonOps is a helper function that extracts operations from the
388
+ // ID of a macaroon.
389
+ func extractMacaroonOps (mac * macaroon.Macaroon ) ([]* lnrpc.Op , error ) {
390
+ rawID := mac .Id ()
391
+ if rawID [0 ] != byte (bakery .LatestVersion ) {
392
+ return nil , fmt .Errorf ("invalid macaroon version: %x" , rawID )
393
+ }
394
+
395
+ decodedID := & lnrpc.MacaroonId {}
396
+ idProto := rawID [1 :]
397
+ err := proto .Unmarshal (idProto , decodedID )
398
+ if err != nil {
399
+ return nil , fmt .Errorf ("unable to decode macaroon: %v" , err )
400
+ }
401
+
402
+ return decodedID .Ops , nil
403
+ }
404
+
405
+ // isReadOnly returns true if the given operations only contain "read" actions.
406
+ func isReadOnly (ops []* lnrpc.Op ) bool {
407
+ for _ , op := range ops {
408
+ for _ , action := range op .Actions {
409
+ if action != "read" {
410
+ return false
411
+ }
412
+ }
413
+ }
414
+
415
+ return true
416
+ }
417
+
418
+ // hasPermissions returns true if all the operations in requiredOps can also be
419
+ // found in macOps.
420
+ func hasPermissions (macOps []* lnrpc.Op , requiredOps []bakery.Op ) bool {
421
+ // Create a lookup map of the macaroon operations.
422
+ macOpsMap := make (map [string ]map [string ]bool )
423
+ for _ , op := range macOps {
424
+ macOpsMap [op .Entity ] = make (map [string ]bool )
425
+
426
+ for _ , action := range op .Actions {
427
+ macOpsMap [op.Entity ][action ] = true
428
+ }
429
+ }
430
+
431
+ // For each of the required operations, we ensure that the macaroon also
432
+ // contains the operation.
433
+ for _ , op := range requiredOps {
434
+ macEntity , ok := macOpsMap [op .Entity ]
435
+ if ! ok {
436
+ return false
437
+ }
438
+
439
+ if ! macEntity [op .Action ] {
440
+ return false
441
+ }
442
+ }
443
+
444
+ return true
445
+ }
446
+
284
447
// validateArgs checks that the correct keys and callback functions have been
285
448
// provided.
286
449
func validateArgs (cfg * config , localPrivKey , remotePubKey string ) error {
0 commit comments