lightning-terminal/subservers/manager.go

413 lines
10 KiB
Go

package subservers
import (
"context"
"fmt"
"io/ioutil"
"sync"
"time"
restProxy "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/lightninglabs/lightning-terminal/perms"
"github.com/lightninglabs/lightning-terminal/status"
"github.com/lightninglabs/lndclient"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc"
grpcProxy "github.com/mwitkow/grpc-proxy/proxy"
"google.golang.org/grpc"
"google.golang.org/grpc/backoff"
"google.golang.org/grpc/credentials"
"gopkg.in/macaroon-bakery.v2/bakery"
"gopkg.in/macaroon.v2"
)
var (
// maxMsgRecvSize is the largest message our REST proxy will receive. We
// set this to 200MiB atm.
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200)
// defaultConnectTimeout is the default timeout for connecting to the
// backend.
defaultConnectTimeout = 15 * time.Second
)
// Manager manages a set of subServer objects.
type Manager struct {
servers map[string]*subServerWrapper
permsMgr *perms.Manager
statusServer *status.Manager
mu sync.RWMutex
}
// NewManager constructs a new Manager.
func NewManager(permsMgr *perms.Manager,
statusServer *status.Manager) *Manager {
return &Manager{
servers: make(map[string]*subServerWrapper),
permsMgr: permsMgr,
statusServer: statusServer,
}
}
// AddServer adds a new subServer to the manager's set.
func (s *Manager) AddServer(ss SubServer, enable bool) error {
// Register all sub-servers with the status server.
err := s.statusServer.RegisterSubServer(ss.Name())
if err != nil {
return err
}
// If the sub-server has explicitly been disabled, then we don't add it
// to the set of servers tracked by the Manager.
if !enable {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.servers[ss.Name()]
if ok {
return fmt.Errorf("a subserver with name %s has already "+
"been registered with the subserver manager", ss.Name())
}
// Add the enabled server to the set of servers tracked by the Manager.
s.servers[ss.Name()] = &subServerWrapper{
SubServer: ss,
quit: make(chan struct{}),
}
// Register the sub-server's permissions with the permission manager.
s.permsMgr.RegisterSubServer(
ss.Name(), ss.Permissions(), ss.WhiteListedURLs(),
)
// Mark the sub-server as enabled with the status manager.
s.statusServer.SetEnabled(ss.Name())
return nil
}
// GetServer returns the sub-server with the given name if it exists.
func (s *Manager) GetServer(name string) (SubServer, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
ss, ok := s.servers[name]
if !ok {
return nil, false
}
return ss.SubServer, true
}
// StartIntegratedServers starts all the manager's sub-servers that should be
// started in integrated mode.
func (s *Manager) StartIntegratedServers(lndClient lnrpc.LightningClient,
lndGrpc *lndclient.GrpcLndServices, withMacaroonService bool) {
s.mu.Lock()
defer s.mu.Unlock()
for _, ss := range s.servers {
if ss.Remote() {
continue
}
err := ss.startIntegrated(
lndClient, lndGrpc, withMacaroonService,
func(err error) {
s.statusServer.SetErrored(
ss.Name(), err.Error(),
)
},
)
if err != nil {
s.statusServer.SetErrored(ss.Name(), err.Error())
continue
}
s.statusServer.SetRunning(ss.Name())
}
}
// ConnectRemoteSubServers creates connections to all the manager's sub-servers
// that are running remotely.
func (s *Manager) ConnectRemoteSubServers() {
s.mu.Lock()
defer s.mu.Unlock()
for _, ss := range s.servers {
if !ss.Remote() {
continue
}
err := ss.connectRemote()
if err != nil {
s.statusServer.SetErrored(ss.Name(), err.Error())
continue
}
s.statusServer.SetRunning(ss.Name())
}
}
// RegisterRPCServices registers all the manager's sub-servers with the given
// grpc registrar.
func (s *Manager) RegisterRPCServices(server grpc.ServiceRegistrar) {
s.mu.RLock()
defer s.mu.RUnlock()
for _, ss := range s.servers {
// In remote mode the "director" of the RPC proxy will act as
// a catch-all for any gRPC request that isn't known because we
// didn't register any server for it. The director will then
// forward the request to the remote service.
if ss.Remote() {
continue
}
ss.RegisterGrpcService(server)
}
}
// RegisterRestServices registers all the manager's sub-servers REST handlers
// with the given endpoint.
func (s *Manager) RegisterRestServices(ctx context.Context,
mux *restProxy.ServeMux, endpoint string,
dialOpts []grpc.DialOption) error {
s.mu.RLock()
defer s.mu.RUnlock()
for _, ss := range s.servers {
// In remote mode the "director" of the RPC proxy will act as
// a catch-all for any gRPC request that isn't known because we
// didn't register any server for it. The director will then
// forward the request to the remote service.
if ss.Remote() {
continue
}
err := ss.RegisterRestService(ctx, mux, endpoint, dialOpts)
if err != nil {
return err
}
}
return nil
}
// GetRemoteConn checks if any of the manager's sub-servers owns the given uri
// and if so, the remote connection to that sub-server is returned. The bool
// return value indicates if the uri is managed by one of the sub-servers
// running in remote mode.
func (s *Manager) GetRemoteConn(uri string) (bool, *grpc.ClientConn, error) {
s.mu.RLock()
defer s.mu.RUnlock()
for _, ss := range s.servers {
if !s.permsMgr.IsSubServerURI(ss.Name(), uri) {
continue
}
if !ss.Remote() {
return false, nil, nil
}
if ss.remoteConn == nil {
return true, nil, fmt.Errorf("not yet connected to "+
"remote sub-server(%s)", ss.Name())
}
return true, ss.remoteConn, nil
}
return false, nil, nil
}
// ValidateMacaroon checks if any of the manager's sub-servers owns the given
// uri and if so, if it is running in remote mode, then true is returned since
// the macaroon will be validated by the remote subserver itself when the
// request arrives. Otherwise, the integrated sub-server's validator validates
// the macaroon.
func (s *Manager) ValidateMacaroon(ctx context.Context,
requiredPermissions []bakery.Op, uri string) (bool, error) {
s.mu.RLock()
defer s.mu.RUnlock()
for _, ss := range s.servers {
if !s.permsMgr.IsSubServerURI(ss.Name(), uri) {
continue
}
// If the sub-server is running in remote mode, then we don't
// need to validate the macaroon here since the remote server
// will do it when the request arrives. But we have handled the
// request, as we were able to identify it.
if ss.Remote() {
return true, nil
}
// If the sub-server hasn't started yet, then we can't validate
// the macaroon. But we know that we can handle the request, as
// we were able to identify it.
if !ss.started() {
return true, fmt.Errorf("%s is not yet ready for "+
"requests, the subserver has not started or "+
"lnd still starting/syncing",
ss.Name())
}
// Validate the macaroon with the integrated sub-server's own
// validator.
err := ss.ValidateMacaroon(ctx, requiredPermissions, uri)
if err != nil {
return true, fmt.Errorf("invalid macaroon: %v", err)
}
// The macaroon is valid for this sub-server, we can return
// early.
return true, nil
}
// No sub-server owns the given uri, so we haven't handled this call.
return false, nil
}
// MacaroonPath checks if any of the manager's sub-servers owns the given uri
// and if so, the appropriate macaroon path is returned for that sub-server.
func (s *Manager) MacaroonPath(uri string) (bool, string) {
s.mu.RLock()
defer s.mu.RUnlock()
for _, ss := range s.servers {
if !s.permsMgr.IsSubServerURI(ss.Name(), uri) {
continue
}
if ss.Remote() {
return true, ss.RemoteConfig().MacaroonPath
}
return true, ss.MacPath()
}
return false, ""
}
// ReadRemoteMacaroon checks if any of the manager's sub-servers running in
// remote mode owns the given uri and if so, the appropriate macaroon path is
// returned for that sub-server.
func (s *Manager) ReadRemoteMacaroon(uri string) (bool, []byte, error) {
s.mu.RLock()
defer s.mu.RUnlock()
for _, ss := range s.servers {
if !s.permsMgr.IsSubServerURI(ss.Name(), uri) {
continue
}
if !ss.Remote() {
return false, nil, nil
}
macBytes, err := readMacaroon(lncfg.CleanAndExpandPath(
ss.RemoteConfig().MacaroonPath,
))
return true, macBytes, err
}
return false, nil, nil
}
// Handles returns true if one of its sub-servers owns the given URI along with
// the name of the service.
func (s *Manager) Handles(uri string) (bool, string) {
s.mu.RLock()
defer s.mu.RUnlock()
for _, ss := range s.servers {
if !s.permsMgr.IsSubServerURI(ss.Name(), uri) {
continue
}
return true, ss.Name()
}
return false, ""
}
// Stop stops all the manager's sub-servers
func (s *Manager) Stop() error {
var returnErr error
s.mu.RLock()
defer s.mu.RUnlock()
for _, ss := range s.servers {
if ss.Remote() {
continue
}
err := ss.stop()
if err != nil {
log.Errorf("Error stopping %s: %v", ss.Name(), err)
returnErr = err
}
s.statusServer.SetStopped(ss.Name())
}
return returnErr
}
func dialBackend(name, dialAddr, tlsCertPath string) (*grpc.ClientConn, error) {
tlsConfig, err := credentials.NewClientTLSFromFile(tlsCertPath, "")
if err != nil {
return nil, fmt.Errorf("could not read %s TLS cert %s: %v",
name, tlsCertPath, err)
}
opts := []grpc.DialOption{
// From the grpcProxy 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 %s gRPC server at %s", name, dialAddr)
cc, err := grpc.Dial(dialAddr, opts...)
if err != nil {
return nil, fmt.Errorf("failed dialing %s backend: %v", name,
err)
}
return cc, nil
}
// readMacaroon tries to read the macaroon file at the specified path.
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
}