terminal: extract RPC proxy

To make the purpose and the flow of the RPC proxy more easy to
understand, it is extracted into its own struct and cleaned up.
This commit is contained in:
Oliver Gugger 2020-10-08 09:34:41 +02:00
parent b36f8aefed
commit df71037904
No known key found for this signature in database
GPG key ID: 8E4256593F177720
5 changed files with 511 additions and 184 deletions

View file

@ -28,6 +28,8 @@ const (
defaultHTTPSListen = "127.0.0.1:8443"
uiPasswordMinLength = 8
defaultLndMacaroon = "admin.macaroon"
)
var (

3
log.go
View file

@ -67,6 +67,9 @@ func SetupLoggers(root *build.RotatingLogWriter) {
lnd.AddSubLogger(root, "LNDC", lndclient.UseLogger)
lnd.AddSubLogger(root, "STORE", loopdb.UseLogger)
lnd.AddSubLogger(root, lsat.Subsystem, lsat.UseLogger)
// Setup the gRPC loggers too.
grpclog.SetLoggerV2(NewGrpcLogLogger(root, GrpcLogSubsystem))
}
// NewGrpcLogLogger creates a new grpclog compatible logger and attaches it as

403
rpc_proxy.go Normal file
View file

@ -0,0 +1,403 @@
package terminal
import (
"context"
"encoding/base64"
"encoding/hex"
"fmt"
"io/ioutil"
"net/http"
"path"
"strings"
"time"
"github.com/improbable-eng/grpc-web/go/grpcweb"
"github.com/lightningnetwork/lnd/macaroons"
grpcProxy "github.com/mwitkow/grpc-proxy/proxy"
"google.golang.org/grpc"
"google.golang.org/grpc/backoff"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"gopkg.in/macaroon-bakery.v2/bakery"
"gopkg.in/macaroon.v2"
)
const (
contentTypeGrpc = "application/grpc"
// HeaderMacaroon is the HTTP header field name that is used to send
// the macaroon.
HeaderMacaroon = "Macaroon"
)
// newRpcProxy creates a new RPC proxy that can take any native gRPC, grpc-web
// or REST request and delegate (and convert if necessary) it to the correct
// component.
func newRpcProxy(cfg *Config, validator macaroons.MacaroonValidator,
permissionMap map[string][]bakery.Op) *rpcProxy {
// The gRPC web calls are protected by HTTP basic auth which is defined
// by base64(username:password). Because we only have a password, we
// just use base64(password:password).
basicAuth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(
"%s:%s", cfg.UIPassword, cfg.UIPassword,
)))
// Set up the final gRPC server that will serve gRPC web to the browser
// and translate all incoming gRPC web calls into native gRPC that are
// then forwarded to lnd's RPC interface. GRPC web has a few kinks that
// need to be addressed with a custom director that just takes care of a
// few HTTP header fields.
p := &rpcProxy{
cfg: cfg,
basicAuth: basicAuth,
macValidator: validator,
}
p.grpcServer = grpc.NewServer(
// From the grpxProxy doc: This codec is *crucial* to the
// functioning of the proxy.
grpc.CustomCodec(grpcProxy.Codec()),
grpc.ChainStreamInterceptor(p.StreamServerInterceptor(
permissionMap,
)),
grpc.ChainUnaryInterceptor(p.UnaryServerInterceptor(
permissionMap,
)),
grpc.UnknownServiceHandler(
grpcProxy.TransparentHandler(p.director),
),
)
// Create the gRPC web proxy that wraps the just created grpcServer and
// converts the browser's gRPC web calls into native gRPC.
options := []grpcweb.Option{
grpcweb.WithWebsockets(true),
grpcweb.WithWebsocketPingInterval(2 * time.Minute),
grpcweb.WithCorsForRegisteredEndpointsOnly(false),
}
p.grpcWebProxy = grpcweb.WrapServer(p.grpcServer, options...)
return p
}
// rpcProxy is an RPC proxy server that can take any native gRPC, grpc-web or
// REST request and delegate it to the correct component. Any grpc-web request
// is first converted into a native gRPC request. The gRPC call is then handed
// to our local gRPC server that has all in-process RPC servers registered. If
// the call is meant for a component that is registered as running in-process,
// it is handled there. If not, the director will forward the call to either a
// local or remote lnd instance.
//
// any RPC or REST call
// |
// V
// +---+----------------------+
// | grpc-web proxy |
// +---+----------------------+
// |
// v native gRPC call with basic auth
// +---+----------------------+
// | interceptors |
// +---+----------------------+
// |
// v native gRPC call with macaroon
// +---+----------------------+ registered call
// | gRPC server +--------------+
// +---+----------------------+ |
// | |
// v non-registered call |
// +---+----------------------+ +---------v----------+
// | director | | local subserver |
// +---+----------------------+ | - loop |
// | | - faraday |
// v authenticated call | |
// +---+----------------------+ +--------------------+
// | lnd (remote or local) |
// +--------------------------+
//
type rpcProxy struct {
cfg *Config
basicAuth string
macValidator macaroons.MacaroonValidator
lndConn *grpc.ClientConn
grpcServer *grpc.Server
grpcWebProxy *grpcweb.WrappedGrpcServer
}
// Start creates initial connection to lnd.
func (p *rpcProxy) Start() error {
// Setup the connection to lnd.
host, _, tlsPath, _, err := p.cfg.lndConnectParams()
if err != nil {
return err
}
p.lndConn, err = dialLnd(host, tlsPath)
if err != nil {
return fmt.Errorf("could not dial lnd: %v", err)
}
return nil
}
// Stop shuts down the lnd connection.
func (p *rpcProxy) Stop() error {
p.grpcServer.Stop()
if p.lndConn != nil {
if err := p.lndConn.Close(); err != nil {
log.Errorf("Error closing lnd connection: %v", err)
return err
}
}
return nil
}
// isHandling checks if the specified request is something to be handled by lnd
// or any of the attached sub daemons. If true is returned, the call was handled
// by the RPC proxy and the caller MUST NOT handle it again. If false is
// returned, the request was not handled and the caller MUST handle it.
func (p *rpcProxy) isHandling(resp http.ResponseWriter,
req *http.Request) bool {
// gRPC web requests are easy to identify. Send them to the gRPC
// web proxy.
if p.grpcWebProxy.IsGrpcWebRequest(req) ||
p.grpcWebProxy.IsGrpcWebSocketRequest(req) {
log.Infof("Handling gRPC web request: %s", req.URL.Path)
p.grpcWebProxy.ServeHTTP(resp, req)
return true
}
// Normal gRPC requests are also easy to identify. These we can
// send directly to the lnd proxy's gRPC server.
if isGrpcRequest(req) {
log.Infof("Handling gRPC request: %s", req.URL.Path)
p.grpcServer.ServeHTTP(resp, req)
return true
}
// TODO(guggero): Handle REST calls as well.
return false
}
// director is a function that directs an incoming request to the correct
// backend, depending on what kind of authentication information is attached to
// the request.
func (p *rpcProxy) director(ctx context.Context,
_ string) (context.Context, *grpc.ClientConn, error) {
// If this header is present in the request from the web client,
// the actual connection to the backend will not be established.
// https://github.com/improbable-eng/grpc-web/issues/568
md, _ := metadata.FromIncomingContext(ctx)
mdCopy := md.Copy()
delete(mdCopy, "connection")
outCtx := metadata.NewOutgoingContext(ctx, mdCopy)
return outCtx, p.lndConn, nil
}
// UnaryServerInterceptor is a gRPC interceptor that checks whether the
// request is authorized by the included macaroons.
func (p *rpcProxy) UnaryServerInterceptor(
permissionMap map[string][]bakery.Op) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (interface{}, error) {
uriPermissions, ok := permissionMap[info.FullMethod]
if !ok {
return nil, fmt.Errorf("%s: unknown permissions "+
"required for method", info.FullMethod)
}
// For now, basic authentication is just a quick fix until we
// have proper macaroon support implemented in the UI. We allow
// gRPC web requests to have it and "convert" the auth into a
// proper macaroon now.
newCtx, err := p.basicAuthToMacaroon(ctx, info.FullMethod)
if err != nil {
return nil, fmt.Errorf("error upgrading basic auth: %v",
err)
}
// With the basic auth converted to a macaroon if necessary,
// let's now validate the macaroon.
err = p.macValidator.ValidateMacaroon(
newCtx, uriPermissions, info.FullMethod,
)
if err != nil {
return nil, err
}
return handler(ctx, req)
}
}
// StreamServerInterceptor is a GRPC interceptor that checks whether the
// request is authorized by the included macaroons.
func (p *rpcProxy) StreamServerInterceptor(
permissionMap map[string][]bakery.Op) grpc.StreamServerInterceptor {
return func(srv interface{}, ss grpc.ServerStream,
info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
uriPermissions, ok := permissionMap[info.FullMethod]
if !ok {
return fmt.Errorf("%s: unknown permissions required "+
"for method", info.FullMethod)
}
// For now, basic authentication is just a quick fix until we
// have proper macaroon support implemented in the UI. We allow
// gRPC web requests to have it and "convert" the auth into a
// proper macaroon now.
ctx, err := p.basicAuthToMacaroon(ss.Context(), info.FullMethod)
if err != nil {
return fmt.Errorf("error upgrading basic auth: %v", err)
}
// With the basic auth converted to a macaroon if necessary,
// let's now validate the macaroon.
err = p.macValidator.ValidateMacaroon(
ctx, uriPermissions, info.FullMethod,
)
if err != nil {
return err
}
return handler(srv, ss)
}
}
// basicAuthToMacaroon checks that the incoming request context has the expected
// and valid basic authentication header then attaches the correct macaroon to
// the context so it can be forwarded to the actual gRPC server.
func (p *rpcProxy) basicAuthToMacaroon(ctx context.Context,
requestURI string) (context.Context, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return ctx, nil
}
authHeaders := md.Get("authorization")
if len(authHeaders) == 0 {
// No basic auth provided, we don't add a macaroon and let the
// gRPC security interceptor reject the request.
return ctx, nil
}
// The user specified an authorization header so this is very likely a
// gRPC Web call from the UI. But we only attach the macaroon if the
// auth is correct. That way an attacker doesn't know that basic auth
// is even allowed as the error message will only be the macaroon error
// from the lnd backend.
authHeaderParts := strings.Split(authHeaders[0], " ")
if len(authHeaderParts) != 2 {
return ctx, nil
}
if authHeaderParts[1] != p.basicAuth {
return ctx, nil
}
var (
macPath string
err error
)
switch {
case isLndURI(requestURI):
_, _, _, macPath, err = p.cfg.lndConnectParams()
macPath = path.Join(macPath, defaultLndMacaroon)
case isLoopURI(requestURI):
macPath = p.cfg.Loop.MacaroonPath
case isFaradayURI(requestURI):
macPath = p.cfg.Faraday.MacaroonPath
default:
return ctx, fmt.Errorf("unknown gRPC web request: %v",
requestURI)
}
if err != nil {
return ctx, fmt.Errorf("error getting macaroon path: %v", err)
}
// Now that we know which macaroon to load, do it and attach it to the
// request context.
macBytes, err := readMacaroon(macPath)
if err != nil {
return ctx, fmt.Errorf("error reading macaroon: %v", err)
}
md.Set(HeaderMacaroon, hex.EncodeToString(macBytes))
return metadata.NewIncomingContext(ctx, md), nil
}
// dialLnd connects to lnd through the given address and uses the given TLS
// certificate to authenticate the connection.
func dialLnd(dialAddr, tlsCertPath string) (*grpc.ClientConn, error) {
var opts []grpc.DialOption
tlsConfig, err := credentials.NewClientTLSFromFile(tlsCertPath, "")
if err != nil {
return nil, fmt.Errorf("could not read lnd TLS cert %s: %v",
tlsCertPath, err)
}
opts = append(
opts,
// From the grpxProxy doc: This codec is *crucial* to the
// functioning of the proxy.
grpc.WithCodec(grpcProxy.Codec()), // nolint
grpc.WithTransportCredentials(tlsConfig),
grpc.WithDefaultCallOptions(maxMsgRecvSize),
grpc.WithConnectParams(grpc.ConnectParams{
Backoff: backoff.DefaultConfig,
MinConnectTimeout: defaultConnectTimeout,
}),
)
log.Infof("Dialing lnd gRPC server at %s", dialAddr)
cc, err := grpc.Dial(dialAddr, opts...)
if err != nil {
return nil, fmt.Errorf("failed dialing backend: %v", err)
}
return cc, nil
}
// readMacaroon tries to read the macaroon file at the specified path and create
// gRPC dial options from it.
func readMacaroon(macPath string) ([]byte, error) {
// Load the specified macaroon file.
macBytes, err := ioutil.ReadFile(macPath)
if err != nil {
return nil, fmt.Errorf("unable to read macaroon path : %v", err)
}
// Make sure it actually is a macaroon by parsing it.
mac := &macaroon.Macaroon{}
if err = mac.UnmarshalBinary(macBytes); err != nil {
return nil, fmt.Errorf("unable to decode macaroon: %v", err)
}
// It's a macaroon alright, let's return the binary data now.
return macBytes, nil
}
// isGrpcRequest determines if a request is a gRPC request by checking that the
// "content-type" is "application/grpc" and that the protocol is HTTP/2.
func isGrpcRequest(req *http.Request) bool {
contentType := req.Header.Get("content-type")
return req.ProtoMajor == 2 &&
strings.HasPrefix(contentType, contentTypeGrpc)
}

View file

@ -3,6 +3,7 @@ package terminal
import (
"github.com/lightninglabs/faraday/frdrpc"
"github.com/lightninglabs/loop/loopd"
"github.com/lightningnetwork/lnd"
"gopkg.in/macaroon-bakery.v2/bakery"
)
@ -20,3 +21,37 @@ func getSubserverPermissions() map[string][]bakery.Op {
}
return result
}
// getAllPermissions returns a merged map of lnd's and all subservers' macaroon
// permissions.
func getAllPermissions() map[string][]bakery.Op {
subserverPermissions := getSubserverPermissions()
lndPermissions := lnd.MainRPCServerPermissions()
mapSize := len(subserverPermissions) + len(lndPermissions)
result := make(map[string][]bakery.Op, mapSize)
for key, value := range lndPermissions {
result[key] = value
}
for key, value := range subserverPermissions {
result[key] = value
}
return result
}
// isLndURI returns true if the given URI belongs to an RPC of lnd.
func isLndURI(uri string) bool {
_, ok := lnd.MainRPCServerPermissions()[uri]
return ok
}
// isLoopURI returns true if the given URI belongs to an RPC of loopd.
func isLoopURI(uri string) bool {
_, ok := loopd.RequiredPermissions[uri]
return ok
}
// isFaradayURI returns true if the given URI belongs to an RPC of faraday.
func isFaradayURI(uri string) bool {
_, ok := frdrpc.RequiredPermissions[uri]
return ok
}

View file

@ -3,21 +3,17 @@ package terminal
import (
"context"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
restProxy "github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/improbable-eng/grpc-web/go/grpcweb"
"github.com/jessevdk/go-flags"
"github.com/lightninglabs/faraday"
"github.com/lightninglabs/faraday/chain"
@ -29,19 +25,9 @@ import (
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntest/wait"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/lightningnetwork/lnd/signal"
"github.com/mwitkow/grpc-proxy/proxy"
"github.com/rakyll/statik/fs"
"google.golang.org/grpc"
"google.golang.org/grpc/backoff"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"gopkg.in/macaroon.v2"
// Import generated go package that contains all static files for the
// UI in a compressed format.
_ "github.com/lightninglabs/lightning-terminal/statik"
@ -57,10 +43,6 @@ var (
// maxMsgRecvSize is the largest message our REST proxy will receive. We
// set this to 200MiB atm.
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200)
authError = status.Error(
codes.Unauthenticated, "authentication required",
)
)
// LightningTerminal is the main grand unified binary instance. Its task is to
@ -81,8 +63,8 @@ type LightningTerminal struct {
loopServer *loopd.Daemon
loopStarted bool
grpcWebProxy *grpc.Server
httpServer *http.Server
rpcProxy *rpcProxy
httpServer *http.Server
}
// New creates a new instance of the lightning-terminal daemon.
@ -154,6 +136,7 @@ func (g *LightningTerminal) Run() error {
g.cfg.frdrpcCfg = &frdrpc.Config{}
g.faradayServer = frdrpc.NewRPCServer(g.cfg.frdrpcCfg)
g.loopServer = loopd.New(g.cfg.Loop, nil)
g.rpcProxy = newRpcProxy(g.cfg, nil, getAllPermissions())
// Hook interceptor for os signals.
err = signal.Intercept()
@ -200,8 +183,7 @@ func (g *LightningTerminal) Run() error {
return err
}
err = g.startGrpcWebProxy()
if err != nil {
if err := g.startMainWebServer(); err != nil {
log.Errorf("Could not start gRPC web proxy server: %v", err)
return err
}
@ -265,6 +247,13 @@ func (g *LightningTerminal) startSubservers() error {
}
}
// Now start the RPC proxy that will handle all incoming gRPC, grpc-web
// and REST requests.
if err := g.rpcProxy.Start(); err != nil {
return fmt.Errorf("error starting lnd gRPC proxy server: %v",
err)
}
// The main RPC listener of lnd might need some time to start, it could
// be that we run into a connection refused a few times. We use the
// basic client connection to find out if the RPC server is started yet
@ -394,11 +383,13 @@ func (g *LightningTerminal) shutdown() error {
g.lndClient.Close()
}
if g.grpcWebProxy != nil {
g.grpcWebProxy.Stop()
err := g.httpServer.Close()
if err != nil {
log.Errorf("Error stopping loop: %v", err)
if g.rpcProxy != nil {
if err := g.rpcProxy.Stop(); err != nil {
log.Errorf("Error stopping lnd proxy: %v", err)
returnErr = err
}
if err := g.httpServer.Close(); err != nil {
log.Errorf("Error stopping lnd: %v", err)
returnErr = err
}
}
@ -417,10 +408,43 @@ func (g *LightningTerminal) shutdown() error {
return returnErr
}
// startGrpcWebProxy creates a proxy that speaks gRPC web on one side and native
// gRPC on the other side. This allows gRPC web requests from the browser to be
// forwarded to lnd's native gRPC interface.
func (g *LightningTerminal) startGrpcWebProxy() error {
// startMainWebServer creates the main web HTTP server that delegates requests
// between the Statik HTTP server and the RPC proxy. An incoming request will
// go through the following chain of components:
//
// Request on port 8443
// |
// v
// +---+----------------------+ other +----------------+
// | Main web HTTP server +------->+ Statik HTTP |
// +---+----------------------+ +----------------+
// |
// v any RPC or REST call
// +---+----------------------+
// | grpc-web proxy |
// +---+----------------------+
// |
// v native gRPC call with basic auth
// +---+----------------------+
// | interceptors |
// +---+----------------------+
// |
// v native gRPC call with macaroon
// +---+----------------------+ registered call
// | gRPC server +--------------+
// +---+----------------------+ |
// | |
// v non-registered call |
// +---+----------------------+ +---------v----------+
// | director | | local subserver |
// +---+----------------------+ | - loop |
// | | - faraday |
// v authenticated call | |
// +---+----------------------+ +--------------------+
// | lnd (remote or local) |
// +--------------------------+
//
func (g *LightningTerminal) startMainWebServer() error {
// Initialize the in-memory file server from the content compiled by
// the statik library.
statikFS, err := fs.New()
@ -429,29 +453,14 @@ func (g *LightningTerminal) startGrpcWebProxy() error {
}
staticFileServer := http.FileServer(&ClientRouteWrapper{statikFS})
// Create the gRPC web proxy that connects to lnd internally using the
// admin macaroon and converts the browser's gRPC web calls into native
// gRPC.
lndGrpcServer, grpcServer, err := buildGrpcWebProxyServer(
g.cfg.Lnd.RPCListeners[0].String(), g.cfg.UIPassword, g.cfg.Lnd,
)
if err != nil {
return fmt.Errorf("could not create gRPC web proxy: %v", err)
}
g.grpcWebProxy = grpcServer
// Both gRPC (web) and static file requests will come into through the
// main UI HTTP server. We use this simple switching handler to send the
// requests to the correct implementation.
httpHandler := func(resp http.ResponseWriter, req *http.Request) {
// gRPC requests are easy to identify. Send them to the gRPC web
// proxy.
if lndGrpcServer.IsGrpcWebRequest(req) ||
lndGrpcServer.IsGrpcWebSocketRequest(req) {
log.Infof("Handling gRPC request: %s", req.URL.Path)
lndGrpcServer.ServeHTTP(resp, req)
// If this is some kind of gRPC, gRPC Web or REST call that
// should go to lnd or one of the daemons, pass it to the proxy
// that handles all those calls.
if g.rpcProxy.isHandling(resp, req) {
return
}
@ -459,14 +468,18 @@ func (g *LightningTerminal) startGrpcWebProxy() error {
// something we don't know in which case the static file server
// will answer with a 404.
log.Infof("Handling static file request: %s", req.URL.Path)
// add 1-year cache header for static files. React uses content-based
// hashes in file names, so when any file is updated, the url will
// change causing the browser cached version to be invalidated
var re = regexp.MustCompile(`^\/(static|fonts|icons)\/.*`)
// Add 1-year cache header for static files. React uses content-
// based hashes in file names, so when any file is updated, the
// url will change causing the browser cached version to be
// invalidated.
var re = regexp.MustCompile(`^/(static|fonts|icons)/.*`)
if re.MatchString(req.URL.Path) {
resp.Header().Set("Cache-Control", "max-age=31536000")
}
// transfer static files using gzip to save up to 70% of bandwidth
// Transfer static files using gzip to save up to 70% of
// bandwidth.
gzipHandler := makeGzipHandler(staticFileServer.ServeHTTP)
gzipHandler(resp, req)
}
@ -503,135 +516,6 @@ func (g *LightningTerminal) startGrpcWebProxy() error {
return nil
}
// buildGrpcWebProxyServer creates a gRPC server that will serve gRPC web to the
// browser and translate all incoming gRPC web calls into native gRPC that are
// then forwarded to lnd's RPC interface.
func buildGrpcWebProxyServer(lndAddr, uiPassword string,
config *lnd.Config) (*grpcweb.WrappedGrpcServer, *grpc.Server, error) {
// Apply gRPC-wide changes.
grpc.EnableTracing = true
grpclog.SetLoggerV2(NewGrpcLogLogger(
config.LogWriter, GrpcLogSubsystem,
))
// The gRPC web calls are protected by HTTP basic auth which is defined
// by base64(username:password). Because we only have a password, we
// just use base64(password:password).
basicAuth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(
"%s:%s", uiPassword, uiPassword,
)))
// Setup the connection to lnd. GRPC web has a few kinks that need to be
// addressed with a custom director that just takes care of a few HTTP
// header fields.
backendConn, err := dialLnd(lndAddr, config)
if err != nil {
return nil, nil, fmt.Errorf("could not dial lnd: %v", err)
}
director := newDirector(backendConn, basicAuth)
// Set up the final gRPC server that will serve gRPC web to the browser
// and translate all incoming gRPC web calls into native gRPC that are
// then forwarded to lnd's RPC interface.
grpcServer := grpc.NewServer(
grpc.CustomCodec(proxy.Codec()),
grpc.UnknownServiceHandler(proxy.TransparentHandler(director)),
)
options := []grpcweb.Option{
grpcweb.WithWebsockets(true),
grpcweb.WithWebsocketPingInterval(2 * time.Minute),
grpcweb.WithCorsForRegisteredEndpointsOnly(false),
}
return grpcweb.WrapServer(grpcServer, options...), grpcServer, nil
}
// newDirector returns a new director function that fixes some common known
// issues when using gRPC web from the browser.
func newDirector(backendConn *grpc.ClientConn,
basicAuth string) proxy.StreamDirector {
return func(ctx context.Context, fullMethodName string) (context.Context,
*grpc.ClientConn, error) {
md, _ := metadata.FromIncomingContext(ctx)
authHeaders := md.Get("authorization")
if len(authHeaders) == 0 {
return nil, nil, authError
}
authHeaderParts := strings.Split(authHeaders[0], " ")
if len(authHeaderParts) != 2 {
return nil, nil, authError
}
if authHeaderParts[1] != basicAuth {
return nil, nil, authError
}
// If this header is present in the request from the web client,
// the actual connection to the backend will not be established.
// https://github.com/improbable-eng/grpc-web/issues/568
mdCopy := md.Copy()
delete(mdCopy, "connection")
outCtx := metadata.NewOutgoingContext(ctx, mdCopy)
return outCtx, backendConn, nil
}
}
// dialLnd connects to lnd through the given address and uses the admin macaroon
// to authenticate.
func dialLnd(lndAddr string, config *lnd.Config) (*grpc.ClientConn, error) {
dialAdminMac, err := readMacaroon(config.AdminMacPath)
if err != nil {
return nil, fmt.Errorf("could not read admin macaroon: %v", err)
}
tlsConfig, err := credentials.NewClientTLSFromFile(
config.TLSCertPath, "",
)
if err != nil {
return nil, fmt.Errorf("could not read lnd TLS cert: %v", err)
}
opt := []grpc.DialOption{
dialAdminMac,
grpc.WithCodec(proxy.Codec()), // nolint
grpc.WithTransportCredentials(tlsConfig),
grpc.WithDefaultCallOptions(maxMsgRecvSize),
grpc.WithConnectParams(grpc.ConnectParams{
Backoff: backoff.DefaultConfig,
MinConnectTimeout: defaultConnectTimeout,
}),
}
log.Infof("Dialing lnd gRPC server at %s", lndAddr)
cc, err := grpc.Dial(lndAddr, opt...)
if err != nil {
return nil, fmt.Errorf("failed dialing backend: %v", err)
}
return cc, nil
}
// readMacaroon tries to read the macaroon file at the specified path and create
// gRPC dial options from it.
func readMacaroon(macPath string) (grpc.DialOption, error) {
// Load the specified macaroon file.
macBytes, err := ioutil.ReadFile(macPath)
if err != nil {
return nil, fmt.Errorf("unable to read macaroon path : %v", err)
}
mac := &macaroon.Macaroon{}
if err = mac.UnmarshalBinary(macBytes); err != nil {
return nil, fmt.Errorf("unable to decode macaroon: %v", err)
}
// Now we append the macaroon credentials to the dial options.
cred := macaroons.NewMacaroonCredential(mac)
return grpc.WithPerRPCCredentials(cred), nil
}
// ClientRouteWrapper is a wrapper around a FileSystem which properly handles
// URL routes that are defined in the client app but unknown to the backend
// http server