Skip to content

Commit 5afe984

Browse files
authored
Merge pull request #216 from lightninglabs/rest-support
Add full REST support
2 parents 8724a48 + d591d5a commit 5afe984

File tree

3 files changed

+237
-27
lines changed

3 files changed

+237
-27
lines changed

config.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,13 @@ var (
118118
// all config items of its enveloping subservers, each prefixed with their
119119
// daemon's short name.
120120
type Config struct {
121-
HTTPSListen string `long:"httpslisten" description:"The host:port to listen for incoming HTTP/2 connections on for the web UI only."`
122-
HTTPListen string `long:"insecure-httplisten" description:"The host:port to listen on with TLS disabled. This is dangerous to enable as credentials will be submitted without encryption. Should only be used in combination with Tor hidden services or other external encryption."`
123-
UIPassword string `long:"uipassword" description:"The password that must be entered when using the loop UI. use a strong password to protect your node from unauthorized access through the web UI."`
124-
UIPasswordFile string `long:"uipassword_file" description:"Same as uipassword but instead of passing in the value directly, read the password from the specified file."`
125-
UIPasswordEnv string `long:"uipassword_env" description:"Same as uipassword but instead of passing in the value directly, read the password from the specified environment variable."`
121+
HTTPSListen string `long:"httpslisten" description:"The host:port to listen for incoming HTTP/2 connections on for the web UI only."`
122+
HTTPListen string `long:"insecure-httplisten" description:"The host:port to listen on with TLS disabled. This is dangerous to enable as credentials will be submitted without encryption. Should only be used in combination with Tor hidden services or other external encryption."`
123+
EnableREST bool `long:"enablerest" description:"Also allow REST requests to be made to the main HTTP(s) port(s) configured above."`
124+
RestCORS []string `long:"restcors" description:"Add an ip:port/hostname to allow cross origin access from. To allow all origins, set as \"*\"."`
125+
UIPassword string `long:"uipassword" description:"The password that must be entered when using the loop UI. use a strong password to protect your node from unauthorized access through the web UI."`
126+
UIPasswordFile string `long:"uipassword_file" description:"Same as uipassword but instead of passing in the value directly, read the password from the specified file."`
127+
UIPasswordEnv string `long:"uipassword_env" description:"Same as uipassword but instead of passing in the value directly, read the password from the specified environment variable."`
126128

127129
LetsEncrypt bool `long:"letsencrypt" description:"Use Let's Encrypt to create a TLS certificate for the UI instead of using lnd's TLS certificate. Port 80 must be free to listen on and must be reachable from the internet for this to work."`
128130
LetsEncryptHost string `long:"letsencrypthost" description:"The host name to create a Let's Encrypt certificate for."`

rpc_proxy.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator,
8787
// it is handled there. If not, the director will forward the call to either a
8888
// local or remote lnd instance.
8989
//
90-
// any RPC or REST call
90+
// any RPC or grpc-web call
9191
// |
9292
// V
9393
// +---+----------------------+
@@ -246,8 +246,6 @@ func (p *rpcProxy) isHandling(resp http.ResponseWriter,
246246
return true
247247
}
248248

249-
// TODO(guggero): Handle REST calls as well.
250-
251249
return false
252250
}
253251

terminal.go

Lines changed: 229 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
4661
var (
4762
// maxMsgRecvSize is the largest message our REST proxy will receive. We
4863
// set this to 200MiB atm.
@@ -60,6 +75,31 @@ var (
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.
687884
func (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

Comments
 (0)