@@ -11,11 +11,13 @@ import (
11
11
"fmt"
12
12
"net/http"
13
13
"os"
14
+ "regexp"
14
15
"runtime/debug"
15
16
"strings"
16
17
"syscall/js"
17
18
18
19
"github.com/btcsuite/btcd/btcec/v2"
20
+ "github.com/golang/protobuf/proto"
19
21
"github.com/jessevdk/go-flags"
20
22
"github.com/lightninglabs/faraday/frdrpc"
21
23
"github.com/lightninglabs/lightning-node-connect/mailbox"
@@ -35,29 +37,36 @@ import (
35
37
"github.com/lightningnetwork/lnd/lnrpc/wtclientrpc"
36
38
"github.com/lightningnetwork/lnd/signal"
37
39
"google.golang.org/grpc"
40
+ "gopkg.in/macaroon-bakery.v2/bakery"
38
41
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
39
42
"gopkg.in/macaroon.v2"
40
43
)
41
44
42
45
type stubPackageRegistration func (map [string ]func (context.Context ,
43
46
* grpc.ClientConn , string , func (string , error )))
44
47
45
- var registrations = []stubPackageRegistration {
46
- lnrpc .RegisterLightningJSONCallbacks ,
47
- lnrpc .RegisterStateJSONCallbacks ,
48
- autopilotrpc .RegisterAutopilotJSONCallbacks ,
49
- chainrpc .RegisterChainNotifierJSONCallbacks ,
50
- invoicesrpc .RegisterInvoicesJSONCallbacks ,
51
- routerrpc .RegisterRouterJSONCallbacks ,
52
- signrpc .RegisterSignerJSONCallbacks ,
53
- verrpc .RegisterVersionerJSONCallbacks ,
54
- walletrpc .RegisterWalletKitJSONCallbacks ,
55
- watchtowerrpc .RegisterWatchtowerJSONCallbacks ,
56
- wtclientrpc .RegisterWatchtowerClientJSONCallbacks ,
57
- looprpc .RegisterSwapClientJSONCallbacks ,
58
- poolrpc .RegisterTraderJSONCallbacks ,
59
- frdrpc .RegisterFaradayServerJSONCallbacks ,
60
- }
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
+ )
61
70
62
71
func main () {
63
72
defer func () {
@@ -108,6 +117,8 @@ func main() {
108
117
callbacks .Set ("wasmClientInvokeRPC" , js .FuncOf (wc .InvokeRPC ))
109
118
callbacks .Set ("wasmClientStatus" , js .FuncOf (wc .Status ))
110
119
callbacks .Set ("wasmClientGetExpiry" , js .FuncOf (wc .GetExpiry ))
120
+ callbacks .Set ("wasmClientHasPerms" , js .FuncOf (wc .HasPermissions ))
121
+ callbacks .Set ("wasmClientIsReadOnly" , js .FuncOf (wc .IsReadOnly ))
111
122
js .Global ().Set (cfg .NameSpace , callbacks )
112
123
113
124
for _ , registration := range registrations {
@@ -319,33 +330,118 @@ func (w *wasmClient) GetExpiry(_ js.Value, _ []js.Value) interface{} {
319
330
320
331
return js .ValueOf (expiry .Unix ())
321
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 {} {
322
353
if len (args ) != 1 {
323
- return js .ValueOf ("invalid use of wasmClientExtractExpiry, " +
324
- "need 1 parameters: macaroon string" )
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 )
325
380
}
326
381
327
- parts := strings .Split (args [0 ].String (), ": " )
328
- if len (parts ) != 2 || parts [0 ] != "Macaroon" {
329
- return js .ValueOf ("macaroon missing from auth data" )
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 )
330
393
}
331
394
332
- macBytes , err := hex .DecodeString (parts [1 ])
395
+ decodedID := & lnrpc.MacaroonId {}
396
+ idProto := rawID [1 :]
397
+ err := proto .Unmarshal (idProto , decodedID )
333
398
if err != nil {
334
- return js . ValueOf ( err . Error () )
399
+ return nil , fmt . Errorf ( "unable to decode macaroon: %v" , err )
335
400
}
336
401
337
- mac := & macaroon.Macaroon {}
338
- if err := mac .UnmarshalBinary (macBytes ); err != nil {
339
- return js .ValueOf (fmt .Sprintf ("unable to decode macaroon: %v" ,
340
- err ))
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
+ }
341
413
}
342
414
343
- expiry , found := checkers .ExpiryTime (nil , mac .Caveats ())
344
- if ! found {
345
- return nil
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
+ }
346
429
}
347
430
348
- return js .ValueOf (expiry .Unix ())
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
349
445
}
350
446
351
447
// validateArgs checks that the correct keys and callback functions have been
0 commit comments