@@ -29,10 +29,20 @@ import (
2929 "github.com/lightningnetwork/lnd"
3030 "github.com/lightningnetwork/lnd/build"
3131 "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"
3241 "github.com/lightningnetwork/lnd/lntest/wait"
3342 "github.com/lightningnetwork/lnd/signal"
3443 "google.golang.org/grpc"
3544 "google.golang.org/grpc/codes"
45+ "google.golang.org/grpc/credentials"
3646 "google.golang.org/grpc/status"
3747 "gopkg.in/macaroon-bakery.v2/bakery"
3848)
@@ -43,6 +53,11 @@ const (
4353 defaultStartupTimeout = 5 * time .Second
4454)
4555
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+
4661var (
4762 // maxMsgRecvSize is the largest message our REST proxy will receive. We
4863 // set this to 200MiB atm.
6075 // appFilesDir is the sub directory of the above build directory which
6176 // we pass to the HTTP server.
6277 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+ }
63103)
64104
65105// LightningTerminal is the main grand unified binary instance. Its task is to
@@ -83,6 +123,9 @@ type LightningTerminal struct {
83123
84124 rpcProxy * rpcProxy
85125 httpServer * http.Server
126+
127+ restHandler http.Handler
128+ restCancel func ()
86129}
87130
88131// New creates a new instance of the lightning-terminal daemon.
@@ -170,6 +213,14 @@ func (g *LightningTerminal) Run() error {
170213 _ = g .RegisterGrpcSubserver (g .rpcProxy .grpcServer )
171214 }
172215
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+
173224 // Wait for lnd to be started up so we know we have a TLS cert.
174225 select {
175226 // 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 {
500551 g .lndClient .Close ()
501552 }
502553
554+ if g .restCancel != nil {
555+ g .restCancel ()
556+ }
557+
503558 if g .rpcProxy != nil {
504559 if err := g .rpcProxy .Stop (); err != nil {
505560 log .Errorf ("Error stopping lnd proxy: %v" , err )
@@ -536,17 +591,17 @@ func (g *LightningTerminal) shutdown() error {
536591// between the embedded HTTP server and the RPC proxy. An incoming request will
537592// go through the following chain of components:
538593//
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+ // +---+----------------------+ +----------------+
550605// |
551606// v native gRPC call with basic auth
552607// +---+----------------------+
@@ -597,6 +652,17 @@ func (g *LightningTerminal) startMainWebServer() error {
597652 return
598653 }
599654
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+
600666 // If we got here, it's a static file the browser wants, or
601667 // something we don't know in which case the static file server
602668 // will answer with a 404.
@@ -682,6 +748,137 @@ func (g *LightningTerminal) startMainWebServer() error {
682748 return nil
683749}
684750
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+
685882// showStartupInfo shows useful information to the user to easily access the
686883// web UI that was just started.
687884func (g * LightningTerminal ) showStartupInfo () error {
@@ -747,13 +944,11 @@ func (g *LightningTerminal) showStartupInfo() error {
747944 }
748945
749946 // If there's an additional HTTP listener, list it as well.
947+ listenAddr := g .cfg .HTTPSListen
750948 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 )
757952 }
758953
759954 str := "" +
@@ -764,10 +959,10 @@ func (g *LightningTerminal) showStartupInfo() error {
764959 " Node status %s \n " +
765960 " Alias %s \n " +
766961 " Version %s \n " +
767- " Web interface %s \n " +
962+ " Web interface %s (open %s in your browser) \n " +
768963 "----------------------------------------------------------\n "
769964 fmt .Printf (str , info .mode , info .status , info .alias , info .version ,
770- info .webURI )
965+ listenAddr , info .webURI )
771966
772967 return nil
773968}
@@ -790,3 +985,18 @@ func (i *ClientRouteWrapper) Open(name string) (http.File, error) {
790985
791986 return i .assets .Open ("/index.html" )
792987}
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