mirror of
https://github.com/lightninglabs/lightning-terminal.git
synced 2026-08-13 12:33:36 +02:00
413 lines
10 KiB
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
|
|
}
|