@@ -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,137 @@ 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
+
798
+ // Enable WebSocket and CORS support as well. A request will pass
799
+ // through the following chain:
800
+ // req ---> CORS handler --> WS proxy ---> REST proxy --> gRPC endpoint
801
+ // where gRPC endpoint is our main HTTP(S) listener again.
802
+ restHandler := lnrpc .NewWebSocketProxy (restMux , log )
803
+ g .restHandler = allowCORS (restHandler , g .cfg .RestCORS )
804
+
805
+ // First register all lnd handlers. This will make it possible to speak
806
+ // REST over the main RPC listener port in both remote and integrated
807
+ // mode. In integrated mode the user can still use the --lnd.restlisten
808
+ // to spin up an extra REST listener that also offers the same
809
+ // functionality, but is no longer required. In remote mode REST will
810
+ // only be enabled on the main HTTP(S) listener.
811
+ for _ , registrationFn := range lndRESTRegistrations {
812
+ err := registrationFn (ctx , restMux , restProxyDest , restDialOpts )
813
+ if err != nil {
814
+ return fmt .Errorf ("error registering REST handler: %v" ,
815
+ err )
816
+ }
817
+ }
818
+
819
+ // Now register all handlers for faraday, loop and pool.
820
+ err := g .RegisterRestSubserver (
821
+ ctx , restMux , restProxyDest , restDialOpts ,
822
+ )
823
+ if err != nil {
824
+ return fmt .Errorf ("error registering REST handler: %v" , err )
825
+ }
826
+
827
+ return nil
828
+ }
829
+
830
+ // allowCORS wraps the given http.Handler with a function that adds the
831
+ // Access-Control-Allow-Origin header to the response.
832
+ func allowCORS (handler http.Handler , origins []string ) http.Handler {
833
+ allowHeaders := "Access-Control-Allow-Headers"
834
+ allowMethods := "Access-Control-Allow-Methods"
835
+ allowOrigin := "Access-Control-Allow-Origin"
836
+
837
+ // If the user didn't supply any origins that means CORS is disabled
838
+ // and we should return the original handler.
839
+ if len (origins ) == 0 {
840
+ return handler
841
+ }
842
+
843
+ return http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
844
+ origin := r .Header .Get ("Origin" )
845
+
846
+ // Skip everything if the browser doesn't send the Origin field.
847
+ if origin == "" {
848
+ handler .ServeHTTP (w , r )
849
+ return
850
+ }
851
+
852
+ // Set the static header fields first.
853
+ w .Header ().Set (
854
+ allowHeaders ,
855
+ "Content-Type, Accept, Grpc-Metadata-Macaroon" ,
856
+ )
857
+ w .Header ().Set (allowMethods , "GET, POST, DELETE" )
858
+
859
+ // Either we allow all origins or the incoming request matches
860
+ // a specific origin in our list of allowed origins.
861
+ for _ , allowedOrigin := range origins {
862
+ if allowedOrigin == "*" || origin == allowedOrigin {
863
+ // Only set allowed origin to requested origin.
864
+ w .Header ().Set (allowOrigin , origin )
865
+
866
+ break
867
+ }
868
+ }
869
+
870
+ // For a pre-flight request we only need to send the headers
871
+ // back. No need to call the rest of the chain.
872
+ if r .Method == "OPTIONS" {
873
+ return
874
+ }
875
+
876
+ // Everything's prepared now, we can pass the request along the
877
+ // chain of handlers.
878
+ handler .ServeHTTP (w , r )
879
+ })
880
+ }
881
+
685
882
// showStartupInfo shows useful information to the user to easily access the
686
883
// web UI that was just started.
687
884
func (g * LightningTerminal ) showStartupInfo () error {
@@ -747,13 +944,11 @@ func (g *LightningTerminal) showStartupInfo() error {
747
944
}
748
945
749
946
// If there's an additional HTTP listener, list it as well.
947
+ listenAddr := g .cfg .HTTPSListen
750
948
if g .cfg .HTTPListen != "" {
751
- host := strings .ReplaceAll (
752
- strings .ReplaceAll (
753
- g .cfg .HTTPListen , "0.0.0.0" , "localhost" ,
754
- ), "[::]" , "localhost" ,
755
- )
756
- info .webURI = fmt .Sprintf ("%s, http://%s" , info .webURI , host )
949
+ host := toLocalAddress (listenAddr )
950
+ info .webURI = fmt .Sprintf ("%s or http://%s" , info .webURI , host )
951
+ listenAddr = fmt .Sprintf ("%s, %s" , listenAddr , g .cfg .HTTPListen )
757
952
}
758
953
759
954
str := "" +
@@ -764,10 +959,10 @@ func (g *LightningTerminal) showStartupInfo() error {
764
959
" Node status %s \n " +
765
960
" Alias %s \n " +
766
961
" Version %s \n " +
767
- " Web interface %s \n " +
962
+ " Web interface %s (open %s in your browser) \n " +
768
963
"----------------------------------------------------------\n "
769
964
fmt .Printf (str , info .mode , info .status , info .alias , info .version ,
770
- info .webURI )
965
+ listenAddr , info .webURI )
771
966
772
967
return nil
773
968
}
@@ -790,3 +985,18 @@ func (i *ClientRouteWrapper) Open(name string) (http.File, error) {
790
985
791
986
return i .assets .Open ("/index.html" )
792
987
}
988
+
989
+ // toLocalAddress converts an address that is meant as a wildcard listening
990
+ // address ("0.0.0.0" or "[::]") into an address that can be dialed (localhost).
991
+ func toLocalAddress (listenerAddress string ) string {
992
+ addr := strings .ReplaceAll (listenerAddress , "0.0.0.0" , "localhost" )
993
+ return strings .ReplaceAll (addr , "[::]" , "localhost" )
994
+ }
995
+
996
+ // isRESTRequest determines if a request is a REST request by checking that the
997
+ // URI starts with /vX/ where X is a single digit number. This is currently true
998
+ // for all REST URIs of lnd, faraday, loop and pool as they all either start
999
+ // with /v1/ or /v2/.
1000
+ func isRESTRequest (req * http.Request ) bool {
1001
+ return patternRESTRequest .MatchString (req .URL .Path )
1002
+ }
0 commit comments