@@ -29,10 +29,20 @@ import (
29
29
"github.com/lightningnetwork/lnd"
30
30
"github.com/lightningnetwork/lnd/build"
31
31
"github.com/lightningnetwork/lnd/lnrpc"
32
+ "github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
33
+ "github.com/lightningnetwork/lnd/lnrpc/chainrpc"
34
+ "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
35
+ "github.com/lightningnetwork/lnd/lnrpc/routerrpc"
36
+ "github.com/lightningnetwork/lnd/lnrpc/signrpc"
37
+ "github.com/lightningnetwork/lnd/lnrpc/verrpc"
38
+ "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
39
+ "github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc"
40
+ "github.com/lightningnetwork/lnd/lnrpc/wtclientrpc"
32
41
"github.com/lightningnetwork/lnd/lntest/wait"
33
42
"github.com/lightningnetwork/lnd/signal"
34
43
"google.golang.org/grpc"
35
44
"google.golang.org/grpc/codes"
45
+ "google.golang.org/grpc/credentials"
36
46
"google.golang.org/grpc/status"
37
47
"gopkg.in/macaroon-bakery.v2/bakery"
38
48
)
@@ -43,6 +53,11 @@ const (
43
53
defaultStartupTimeout = 5 * time .Second
44
54
)
45
55
56
+ // restRegistration is a function type that represents a REST proxy
57
+ // registration.
58
+ type restRegistration func (context.Context , * restProxy.ServeMux , string ,
59
+ []grpc.DialOption ) error
60
+
46
61
var (
47
62
// maxMsgRecvSize is the largest message our REST proxy will receive. We
48
63
// set this to 200MiB atm.
60
75
// appFilesDir is the sub directory of the above build directory which
61
76
// we pass to the HTTP server.
62
77
appFilesDir = "app/build"
78
+
79
+ // patternRESTRequest is the regular expression that matches all REST
80
+ // URIs that are currently used by lnd, faraday, loop and pool.
81
+ patternRESTRequest = regexp .MustCompile (`^/v\d/.*` )
82
+
83
+ // lndRESTRegistrations is the list of all lnd REST handler registration
84
+ // functions we want to call when creating our REST proxy. We include
85
+ // all lnd subserver packages here, even though some might not be active
86
+ // in a remote lnd node. That will result in an "UNIMPLEMENTED" error
87
+ // instead of a 404 which should be an okay tradeoff vs. connecting
88
+ // first and querying all enabled subservers to dynamically populate
89
+ // this list.
90
+ lndRESTRegistrations = []restRegistration {
91
+ lnrpc .RegisterLightningHandlerFromEndpoint ,
92
+ lnrpc .RegisterWalletUnlockerHandlerFromEndpoint ,
93
+ autopilotrpc .RegisterAutopilotHandlerFromEndpoint ,
94
+ chainrpc .RegisterChainNotifierHandlerFromEndpoint ,
95
+ invoicesrpc .RegisterInvoicesHandlerFromEndpoint ,
96
+ routerrpc .RegisterRouterHandlerFromEndpoint ,
97
+ signrpc .RegisterSignerHandlerFromEndpoint ,
98
+ verrpc .RegisterVersionerHandlerFromEndpoint ,
99
+ walletrpc .RegisterWalletKitHandlerFromEndpoint ,
100
+ watchtowerrpc .RegisterWatchtowerHandlerFromEndpoint ,
101
+ wtclientrpc .RegisterWatchtowerClientHandlerFromEndpoint ,
102
+ }
63
103
)
64
104
65
105
// LightningTerminal is the main grand unified binary instance. Its task is to
@@ -83,6 +123,9 @@ type LightningTerminal struct {
83
123
84
124
rpcProxy * rpcProxy
85
125
httpServer * http.Server
126
+
127
+ restHandler http.Handler
128
+ restCancel func ()
86
129
}
87
130
88
131
// New creates a new instance of the lightning-terminal daemon.
@@ -170,6 +213,14 @@ func (g *LightningTerminal) Run() error {
170
213
_ = g .RegisterGrpcSubserver (g .rpcProxy .grpcServer )
171
214
}
172
215
216
+ // We'll also create a REST proxy that'll convert any REST calls to gRPC
217
+ // calls and forward them to the internal listener.
218
+ if g .cfg .EnableREST {
219
+ if err := g .createRESTProxy (); err != nil {
220
+ return fmt .Errorf ("error creating REST proxy: %v" , err )
221
+ }
222
+ }
223
+
173
224
// Wait for lnd to be started up so we know we have a TLS cert.
174
225
select {
175
226
// If lnd needs to be unlocked we get the signal that it's ready to do
@@ -500,6 +551,10 @@ func (g *LightningTerminal) shutdown() error {
500
551
g .lndClient .Close ()
501
552
}
502
553
554
+ if g .restCancel != nil {
555
+ g .restCancel ()
556
+ }
557
+
503
558
if g .rpcProxy != nil {
504
559
if err := g .rpcProxy .Stop (); err != nil {
505
560
log .Errorf ("Error stopping lnd proxy: %v" , err )
@@ -536,17 +591,17 @@ func (g *LightningTerminal) shutdown() error {
536
591
// between the embedded HTTP server and the RPC proxy. An incoming request will
537
592
// go through the following chain of components:
538
593
//
539
- // Request on port 8443
540
- // |
541
- // v
542
- // +---+----------------------+ other +----------------+
543
- // | Main web HTTP server +------->+ Embedded HTTP |
544
- // +---+----------------------+ +----------------+
545
- // |
546
- // v any RPC or REST call
547
- // +---+----------------------+
548
- // | grpc-web proxy |
549
- // +---+----------------------+
594
+ // Request on port 8443 <------------------------------------+
595
+ // | converted gRPC request |
596
+ // v |
597
+ // +---+----------------------+ other +----------------+ |
598
+ // | Main web HTTP server +------->+ Embedded HTTP | |
599
+ // +---+----------------------+____+ +----------------+ |
600
+ // | | |
601
+ // v any RPC or grpc-web call | any REST call |
602
+ // +---+----------------------+ |->+----------------+ |
603
+ // | grpc-web proxy | + grpc-gateway +-----------+
604
+ // +---+----------------------+ +----------------+
550
605
// |
551
606
// v native gRPC call with basic auth
552
607
// +---+----------------------+
@@ -597,6 +652,17 @@ func (g *LightningTerminal) startMainWebServer() error {
597
652
return
598
653
}
599
654
655
+ // REST requests aren't that easy to identify, we have to look
656
+ // at the URL itself. If this is a REST request, we give it
657
+ // directly to our REST handler which will then forward it to
658
+ // us again but converted to a gRPC request.
659
+ if g .cfg .EnableREST && isRESTRequest (req ) {
660
+ log .Infof ("Handling REST request: %s" , req .URL .Path )
661
+ g .restHandler .ServeHTTP (resp , req )
662
+
663
+ return
664
+ }
665
+
600
666
// If we got here, it's a static file the browser wants, or
601
667
// something we don't know in which case the static file server
602
668
// will answer with a 404.
@@ -682,6 +748,79 @@ func (g *LightningTerminal) startMainWebServer() error {
682
748
return nil
683
749
}
684
750
751
+ // createRESTProxy creates a grpc-gateway based REST proxy that takes any call
752
+ // identified as a REST call, converts it to a gRPC request and forwards it to
753
+ // our local main server for further triage/forwarding.
754
+ func (g * LightningTerminal ) createRESTProxy () error {
755
+ // The default JSON marshaler of the REST proxy only sets OrigName to
756
+ // true, which instructs it to use the same field names as specified in
757
+ // the proto file and not switch to camel case. What we also want is
758
+ // that the marshaler prints all values, even if they are falsey.
759
+ customMarshalerOption := restProxy .WithMarshalerOption (
760
+ restProxy .MIMEWildcard , & restProxy.JSONPb {
761
+ OrigName : true ,
762
+ EmitDefaults : true ,
763
+ },
764
+ )
765
+
766
+ // For our REST dial options, we increase the max message size that
767
+ // we'll decode to allow clients to hit endpoints which return more data
768
+ // such as the DescribeGraph call. We set this to 200MiB atm. Should be
769
+ // the same value as maxMsgRecvSize in lnd/cmd/lncli/main.go.
770
+ restDialOpts := []grpc.DialOption {
771
+ // We are forwarding the requests directly to the address of our
772
+ // own local listener. To not need to mess with the TLS
773
+ // certificate (which might be tricky if we're using Let's
774
+ // Encrypt), we just skip the certificate verification.
775
+ // Injecting a malicious hostname into the listener address will
776
+ // result in an error on startup so this should be quite safe.
777
+ grpc .WithTransportCredentials (credentials .NewTLS (
778
+ & tls.Config {InsecureSkipVerify : true },
779
+ )),
780
+ grpc .WithDefaultCallOptions (
781
+ grpc .MaxCallRecvMsgSize (1 * 1024 * 1024 * 200 ),
782
+ ),
783
+ }
784
+
785
+ // We use our own RPC listener as the destination for our REST proxy.
786
+ // If the listener is set to listen on all interfaces, we replace it
787
+ // with localhost, as we cannot dial it directly.
788
+ restProxyDest := toLocalAddress (g .cfg .HTTPSListen )
789
+
790
+ // Now start the REST proxy for our gRPC server above. We'll ensure
791
+ // we direct LND to connect to its loopback address rather than a
792
+ // wildcard to prevent certificate issues when accessing the proxy
793
+ // externally.
794
+ restMux := restProxy .NewServeMux (customMarshalerOption )
795
+ ctx , cancel := context .WithCancel (context .Background ())
796
+ g .restCancel = cancel
797
+ g .restHandler = restMux
798
+
799
+ // First register all lnd handlers. This will make it possible to speak
800
+ // REST over the main RPC listener port in both remote and integrated
801
+ // mode. In integrated mode the user can still use the --lnd.restlisten
802
+ // to spin up an extra REST listener that also offers the same
803
+ // functionality, but is no longer required. In remote mode REST will
804
+ // only be enabled on the main HTTP(S) listener.
805
+ for _ , registrationFn := range lndRESTRegistrations {
806
+ err := registrationFn (ctx , restMux , restProxyDest , restDialOpts )
807
+ if err != nil {
808
+ return fmt .Errorf ("error registering REST handler: %v" ,
809
+ err )
810
+ }
811
+ }
812
+
813
+ // Now register all handlers for faraday, loop and pool.
814
+ err := g .RegisterRestSubserver (
815
+ ctx , restMux , restProxyDest , restDialOpts ,
816
+ )
817
+ if err != nil {
818
+ return fmt .Errorf ("error registering REST handler: %v" , err )
819
+ }
820
+
821
+ return nil
822
+ }
823
+
685
824
// showStartupInfo shows useful information to the user to easily access the
686
825
// web UI that was just started.
687
826
func (g * LightningTerminal ) showStartupInfo () error {
@@ -795,3 +934,11 @@ func toLocalAddress(listenerAddress string) string {
795
934
addr := strings .ReplaceAll (listenerAddress , "0.0.0.0" , "localhost" )
796
935
return strings .ReplaceAll (addr , "[::]" , "localhost" )
797
936
}
937
+
938
+ // isRESTRequest determines if a request is a REST request by checking that the
939
+ // URI starts with /vX/ where X is a single digit number. This is currently true
940
+ // for all REST URIs of lnd, faraday, loop and pool as they all either start
941
+ // with /v1/ or /v2/.
942
+ func isRESTRequest (req * http.Request ) bool {
943
+ return patternRESTRequest .MatchString (req .URL .Path )
944
+ }
0 commit comments