Skip to content

Commit 0159e0a

Browse files
committed
multi: add flag to enable REST calls on main listener(s)
Fixes #213 by allowing users to enable REST calls to be made directly to the main HTTP(S) listener(s). This approach is chosen over spinning up an additional listener (or multiple, if non-TLS is also needed) just for REST because it should make everyone's lives easier if only one port needs to be used. There also shouldn't be any security tradeoff since a macaroon is still required and all communication happens over TLS anyway.
1 parent 48d7b9b commit 0159e0a

File tree

3 files changed

+160
-14
lines changed

3 files changed

+160
-14
lines changed

config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ var (
120120
type Config struct {
121121
HTTPSListen string `long:"httpslisten" description:"The host:port to listen for incoming HTTP/2 connections on for the web UI only."`
122122
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."`
123124
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."`
124125
UIPasswordFile string `long:"uipassword_file" description:"Same as uipassword but instead of passing in the value directly, read the password from the specified file."`
125126
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."`

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: 158 additions & 11 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,79 @@ 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+
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+
685824
// showStartupInfo shows useful information to the user to easily access the
686825
// web UI that was just started.
687826
func (g *LightningTerminal) showStartupInfo() error {
@@ -795,3 +934,11 @@ func toLocalAddress(listenerAddress string) string {
795934
addr := strings.ReplaceAll(listenerAddress, "0.0.0.0", "localhost")
796935
return strings.ReplaceAll(addr, "[::]", "localhost")
797936
}
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

Comments
 (0)