mirror of
https://github.com/lightninglabs/lightning-terminal.git
synced 2026-08-13 12:33:36 +02:00
This introduces a new firewall.request-logger.disable config option to completely disable request logging. When disabled, the request logger interceptor is not instantiated, avoiding all logging overhead including database writes and request processing. This is implemented as a separate disable flag rather than adding a new log level because the goal is to bypass the logging system entirely for performance reasons, not just filter events. A log level would still process and filter each request through the interceptor. The change also adds validation to ensure autopilot remains enabled only when request logging is active, since autopilot relies on action logs for rule enforcement and auditing.
2150 lines
65 KiB
Go
2150 lines
65 KiB
Go
package terminal
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"math/rand"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
restProxy "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
|
"github.com/jessevdk/go-flags"
|
|
"github.com/lightninglabs/lightning-terminal/accounts"
|
|
"github.com/lightninglabs/lightning-terminal/autopilotserver"
|
|
"github.com/lightninglabs/lightning-terminal/firewall"
|
|
"github.com/lightninglabs/lightning-terminal/firewalldb"
|
|
"github.com/lightninglabs/lightning-terminal/litrpc"
|
|
litmac "github.com/lightninglabs/lightning-terminal/macaroons"
|
|
"github.com/lightninglabs/lightning-terminal/perms"
|
|
"github.com/lightninglabs/lightning-terminal/queue"
|
|
mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware"
|
|
"github.com/lightninglabs/lightning-terminal/rules"
|
|
"github.com/lightninglabs/lightning-terminal/session"
|
|
"github.com/lightninglabs/lightning-terminal/status"
|
|
"github.com/lightninglabs/lightning-terminal/subservers"
|
|
"github.com/lightninglabs/lndclient"
|
|
taprootassets "github.com/lightninglabs/taproot-assets"
|
|
"github.com/lightningnetwork/lnd"
|
|
"github.com/lightningnetwork/lnd/build"
|
|
"github.com/lightningnetwork/lnd/chainreg"
|
|
"github.com/lightningnetwork/lnd/clock"
|
|
"github.com/lightningnetwork/lnd/fn/v2"
|
|
"github.com/lightningnetwork/lnd/funding"
|
|
"github.com/lightningnetwork/lnd/htlcswitch"
|
|
"github.com/lightningnetwork/lnd/kvdb"
|
|
"github.com/lightningnetwork/lnd/lncfg"
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/verrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/wtclientrpc"
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
|
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
|
"github.com/lightningnetwork/lnd/lnwallet/chancloser"
|
|
"github.com/lightningnetwork/lnd/macaroons"
|
|
"github.com/lightningnetwork/lnd/msgmux"
|
|
"github.com/lightningnetwork/lnd/rpcperms"
|
|
"github.com/lightningnetwork/lnd/signal"
|
|
"github.com/lightningnetwork/lnd/sweep"
|
|
grpcProxy "github.com/mwitkow/grpc-proxy/proxy"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials"
|
|
"google.golang.org/grpc/test/bufconn"
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
"gopkg.in/macaroon-bakery.v2/bakery"
|
|
"gopkg.in/macaroon.v2"
|
|
)
|
|
|
|
const (
|
|
MainnetServer = "autopilot.lightning.finance:12010"
|
|
TestnetServer = "test.autopilot.lightning.finance:12010"
|
|
|
|
// lndWalletReadyStatus is a custom status that will be used with the
|
|
// LND subserver. If the subserver is in this state then it will allow
|
|
// certain wallet calls through while denying other calls that require
|
|
// LND to be fully started.
|
|
lndWalletReadyStatus = "Wallet Ready"
|
|
|
|
defaultServerTimeout = 10 * time.Second
|
|
defaultConnectTimeout = 15 * time.Second
|
|
defaultRPCTimeout = 3 * time.Minute
|
|
minimumRPCTimeout = 30 * time.Second
|
|
defaultStartupTimeout = 5 * time.Second
|
|
)
|
|
|
|
// restRegistration is a function type that represents a REST proxy
|
|
// registration.
|
|
type restRegistration func(context.Context, *restProxy.ServeMux, string,
|
|
[]grpc.DialOption) error
|
|
|
|
var (
|
|
// maxMsgRecvSize is the largest message our REST proxy will receive. We
|
|
// set this to 200MiB atm.
|
|
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200)
|
|
|
|
// macDatabaseOpenTimeout is how long we wait for acquiring the lock on
|
|
// the macaroon database before we give up with an error.
|
|
macDatabaseOpenTimeout = time.Second * 5
|
|
|
|
// appFilesDir is the sub directory of the above build directory which
|
|
// we pass to the HTTP server.
|
|
appFilesDir = "app/build"
|
|
|
|
// appFilesPrefix is the path prefix the static assets of the UI are
|
|
// exposed under. This variable can be overwritten during build time if
|
|
// a different deployment path should be used.
|
|
appFilesPrefix = ""
|
|
|
|
// patternRESTRequest is the regular expression that matches all REST
|
|
// URIs that are currently used by lnd, faraday, loop and pool.
|
|
patternRESTRequest = regexp.MustCompile(`^/v\d/.*`)
|
|
|
|
// lndRESTRegistrations is the list of all lnd REST handler registration
|
|
// functions we want to call when creating our REST proxy. We include
|
|
// all lnd subserver packages here, even though some might not be active
|
|
// in a remote lnd node. That will result in an "UNIMPLEMENTED" error
|
|
// instead of a 404 which should be an okay tradeoff vs. connecting
|
|
// first and querying all enabled subservers to dynamically populate
|
|
// this list.
|
|
lndRESTRegistrations = []restRegistration{
|
|
lnrpc.RegisterLightningHandlerFromEndpoint,
|
|
lnrpc.RegisterWalletUnlockerHandlerFromEndpoint,
|
|
lnrpc.RegisterStateHandlerFromEndpoint,
|
|
autopilotrpc.RegisterAutopilotHandlerFromEndpoint,
|
|
chainrpc.RegisterChainNotifierHandlerFromEndpoint,
|
|
invoicesrpc.RegisterInvoicesHandlerFromEndpoint,
|
|
routerrpc.RegisterRouterHandlerFromEndpoint,
|
|
signrpc.RegisterSignerHandlerFromEndpoint,
|
|
verrpc.RegisterVersionerHandlerFromEndpoint,
|
|
walletrpc.RegisterWalletKitHandlerFromEndpoint,
|
|
watchtowerrpc.RegisterWatchtowerHandlerFromEndpoint,
|
|
wtclientrpc.RegisterWatchtowerClientHandlerFromEndpoint,
|
|
}
|
|
|
|
// minimalCompatibleVersion is the minimal lnd version that is required
|
|
// to run LiT in remote mode.
|
|
minimalCompatibleVersion = &verrpc.Version{
|
|
AppMajor: 0,
|
|
AppMinor: 19,
|
|
AppPatch: 0,
|
|
BuildTags: []string{
|
|
"signrpc", "walletrpc", "chainrpc", "invoicesrpc",
|
|
},
|
|
}
|
|
|
|
// walletUnlockerServiceMethods defines methods of the wallet unlocker
|
|
// service that we don't require macaroons to access. We also allow
|
|
// these methods to be called even if lnd is not yet fully marked as
|
|
// started up (because it cannot start if it's still locked or no wallet
|
|
// exists).
|
|
walletUnlockerServiceMethods = map[string]struct{}{
|
|
"/lnrpc.WalletUnlocker/GenSeed": {},
|
|
"/lnrpc.WalletUnlocker/InitWallet": {},
|
|
"/lnrpc.WalletUnlocker/UnlockWallet": {},
|
|
"/lnrpc.WalletUnlocker/ChangePassword": {},
|
|
}
|
|
|
|
// stateServiceMethods defines status methods that we don't require
|
|
// macaroons to access.
|
|
stateServiceMethods = map[string]struct{}{
|
|
// The State service must be available at all times, even
|
|
// before we can check macaroons, so we whitelist it.
|
|
"/lnrpc.State/SubscribeState": {},
|
|
"/lnrpc.State/GetState": {},
|
|
}
|
|
)
|
|
|
|
// LightningTerminal is the main grand unified binary instance. Its task is to
|
|
// start an lnd node then start and register external subservers to it.
|
|
type LightningTerminal struct {
|
|
cfg *Config
|
|
|
|
defaultImplCfg *lnd.ImplementationCfg
|
|
|
|
permsMgr *perms.Manager
|
|
|
|
// lndInterceptorChain is a reference to lnd's interceptor chain that
|
|
// guards all incoming calls. This is only set in integrated mode!
|
|
lndInterceptorChain *rpcperms.InterceptorChain
|
|
|
|
wg sync.WaitGroup
|
|
errQueue *queue.ConcurrentQueue[error]
|
|
|
|
lndConnID string
|
|
lndConn *grpc.ClientConn
|
|
lndClient *lndclient.GrpcLndServices
|
|
|
|
// basicClient may be accessed by other sub-systems but this access
|
|
// should be provided via the basicLNDClient method.
|
|
basicClient lnrpc.LightningClient
|
|
basicClientSet atomic.Bool
|
|
|
|
subServerMgr *subservers.Manager
|
|
statusMgr *status.Manager
|
|
|
|
autopilotClient autopilotserver.Autopilot
|
|
|
|
ruleMgrs rules.ManagerSet
|
|
|
|
rpcProxy *rpcProxy
|
|
httpServer *http.Server
|
|
|
|
sessionRpcServer *sessionRpcServer
|
|
sessionRpcServerStarted bool
|
|
|
|
macaroonService *lndclient.MacaroonService
|
|
macaroonServiceStarted bool
|
|
macaroonDB kvdb.Backend
|
|
|
|
middleware *mid.Manager
|
|
middlewareStarted bool
|
|
|
|
accountService *accounts.InterceptorService
|
|
accountServiceStarted bool
|
|
|
|
accountRpcServer *accounts.RPCServer
|
|
|
|
stores *stores
|
|
|
|
restHandler http.Handler
|
|
restCancel func()
|
|
}
|
|
|
|
// New creates a new instance of the lightning-terminal daemon.
|
|
func New() *LightningTerminal {
|
|
return &LightningTerminal{
|
|
statusMgr: status.NewStatusManager(),
|
|
}
|
|
}
|
|
|
|
// stores holds a collection of the DB stores that are used by LiT.
|
|
type stores struct {
|
|
accounts accounts.Store
|
|
sessions session.Store
|
|
|
|
firewall *firewalldb.DB
|
|
|
|
// closeFns holds various callbacks that can be used to close any open
|
|
// stores in the stores struct.
|
|
closeFns map[string]func() error
|
|
}
|
|
|
|
// close closes all open stores in the stores struct. It returns an error if
|
|
// any of the stores could not be closed.
|
|
func (s *stores) close() error {
|
|
var returnErr error
|
|
for storeName, closeFn := range s.closeFns {
|
|
err := closeFn()
|
|
if err != nil {
|
|
log.Errorf("error closing %s store: %v",
|
|
storeName, err)
|
|
returnErr = err
|
|
}
|
|
}
|
|
|
|
return returnErr
|
|
}
|
|
|
|
// Run starts everything and then blocks until either the application is shut
|
|
// down or a critical error happens.
|
|
func (g *LightningTerminal) Run(ctx context.Context) error {
|
|
// Hook interceptor for os signals.
|
|
shutdownInterceptor, err := signal.Intercept()
|
|
if err != nil {
|
|
return fmt.Errorf("could not intercept signals: %v", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
// Make sure the context is canceled if the user requests shutdown and
|
|
// that the shutdown signal is requested if the context is canceled.
|
|
go func() {
|
|
select {
|
|
// Client requests shutdown, cancel the wait.
|
|
case <-shutdownInterceptor.ShutdownChannel():
|
|
cancel()
|
|
|
|
// The check was completed and the above defer canceled the
|
|
// context. We can just exit the goroutine, nothing more to do.
|
|
case <-ctx.Done():
|
|
shutdownInterceptor.RequestShutdown()
|
|
}
|
|
}()
|
|
|
|
cfg, err := loadAndValidateConfig(shutdownInterceptor)
|
|
if err != nil {
|
|
return fmt.Errorf("could not load config: %w", err)
|
|
}
|
|
g.cfg = cfg
|
|
g.defaultImplCfg = g.cfg.Lnd.ImplementationConfig(shutdownInterceptor)
|
|
|
|
// Show version at startup.
|
|
log.Infof("LiT version: %s", RichVersion())
|
|
|
|
// This concurrent error queue can be used by every component that can
|
|
// raise runtime errors. Using a queue will prevent us from blocking on
|
|
// sending errors to it, as long as the queue is running.
|
|
g.errQueue = queue.NewConcurrentQueue[error](queue.DefaultQueueSize)
|
|
g.errQueue.Start()
|
|
defer g.errQueue.Stop()
|
|
|
|
// Construct a new Manager.
|
|
g.permsMgr, err = perms.NewManager(false)
|
|
if err != nil {
|
|
return fmt.Errorf("could not create permissions manager: %w",
|
|
err)
|
|
}
|
|
|
|
// The litcli status command will call the "/lnrpc.State/GetState" RPC.
|
|
// As the status command is available to the user before the macaroons
|
|
// have been loaded/created, and before the lnd clients have been
|
|
// set up, we need to override the isReady check for this specific
|
|
// URI as soon as LND can accept the call, i.e. when the lnd sub-server
|
|
// is in the "Wallet Ready" state. The same goes for the streaming
|
|
// variant of the status RPC and any calls to the wallet unlocker
|
|
// service.
|
|
lndOverride := func(uri, manualStatus string) (bool, bool) {
|
|
_, isWalletUnlockerService := walletUnlockerServiceMethods[uri]
|
|
_, isStatusService := stateServiceMethods[uri]
|
|
|
|
// If this is a call to the wallet unlocker or status subserver,
|
|
// we return true for ready if we've set up everything for lnd,
|
|
// and it is just waiting to be unlocked.
|
|
if isWalletUnlockerService || isStatusService {
|
|
return manualStatus == lndWalletReadyStatus, true
|
|
}
|
|
|
|
return false, false
|
|
}
|
|
|
|
// Register LND, LiT and Accounts with the status manager.
|
|
err = g.statusMgr.RegisterAndEnableSubServer(
|
|
subservers.LND, status.WithIsReadyOverride(lndOverride),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = g.statusMgr.RegisterAndEnableSubServer(subservers.LIT)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = g.statusMgr.RegisterSubServer(subservers.ACCOUNTS)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Also enable the accounts subserver if it's not disabled.
|
|
if !g.cfg.Accounts.Disable {
|
|
g.statusMgr.SetEnabled(subservers.ACCOUNTS)
|
|
}
|
|
|
|
// Create the instances of our subservers now so we can hook them up to
|
|
// lnd once it's fully started.
|
|
g.subServerMgr = subservers.NewManager(g.permsMgr, g.statusMgr)
|
|
|
|
// Register our sub-servers. This must be done before the REST proxy is
|
|
// set up so that the correct REST handlers are registered.
|
|
err = g.initSubServers()
|
|
if err != nil {
|
|
return fmt.Errorf("could not initialise sub-servers: %w", err)
|
|
}
|
|
|
|
// Construct the rpcProxy. It must be initialised before the main web
|
|
// server is started.
|
|
g.rpcProxy = newRpcProxy(
|
|
g.cfg, g, g.validateSuperMacaroon, g.permsMgr, g.subServerMgr,
|
|
g.statusMgr, g.basicLNDClient,
|
|
)
|
|
|
|
// Register any gRPC services that should be served using LiT's
|
|
// gRPC server regardless of the LND mode being used.
|
|
litrpc.RegisterProxyServer(g.rpcProxy.grpcServer, g.rpcProxy)
|
|
litrpc.RegisterStatusServer(g.rpcProxy.grpcServer, g.statusMgr)
|
|
|
|
// Start the main web server that dispatches requests either to the
|
|
// static UI file server or the RPC proxy. This makes it possible to
|
|
// unlock lnd through the UI.
|
|
if err := g.startMainWebServer(); err != nil {
|
|
return fmt.Errorf("error starting main proxy HTTP server: %v",
|
|
err)
|
|
}
|
|
|
|
// We'll also create a REST proxy that'll convert any REST calls to gRPC
|
|
// calls and forward them to the internal listener.
|
|
if g.cfg.EnableREST {
|
|
if err := g.createRESTProxy(ctx); err != nil {
|
|
return fmt.Errorf("error creating REST proxy: %v", err)
|
|
}
|
|
}
|
|
|
|
// Attempt to start Lit and all of its sub-servers. If an error is
|
|
// returned, it means that either one of Lit's internal sub-servers
|
|
// could not start or LND could not start or be connected to.
|
|
startErr := g.start(ctx)
|
|
if startErr != nil {
|
|
g.statusMgr.SetErrored(
|
|
subservers.LIT, "could not start Lit: %v", startErr,
|
|
)
|
|
}
|
|
|
|
// Now block until we receive an error or the main shutdown
|
|
// signal.
|
|
<-shutdownInterceptor.ShutdownChannel()
|
|
log.Infof("Shutdown signal received")
|
|
|
|
err = g.shutdownSubServers()
|
|
if err != nil {
|
|
log.Errorf("Error shutting down: %v", err)
|
|
}
|
|
|
|
g.wg.Wait()
|
|
|
|
return startErr
|
|
}
|
|
|
|
// start attempts to start all the various components of Litd. Only Litd and
|
|
// LND errors are considered fatal and will result in an error being returned.
|
|
// If any of the sub-servers managed by the subServerMgr error while starting
|
|
// up, these are considered non-fatal and will not result in an error being
|
|
// returned.
|
|
func (g *LightningTerminal) start(ctx context.Context) error {
|
|
var err error
|
|
|
|
accountServiceErrCallback := func(err error) {
|
|
g.statusMgr.SetErrored(subservers.ACCOUNTS, err.Error())
|
|
|
|
log.Errorf("Error thrown in the accounts service, keeping "+
|
|
"litd running: %v", err,
|
|
)
|
|
}
|
|
|
|
networkDir := filepath.Join(g.cfg.LitDir, g.cfg.Network)
|
|
err = makeDirectories(networkDir)
|
|
if err != nil {
|
|
return fmt.Errorf("could not create network directory: %v", err)
|
|
}
|
|
|
|
// We create a reference to the `accountRpcServer` here before starting
|
|
// it and prior to setting up the LND connection. This is because when
|
|
// the LND connection is set up for an integrated LND instance, LND will
|
|
// call litd's `RegisterGrpcSubserver` function during the setup of the
|
|
// connection.
|
|
// That function calls `registerSubDaemonGrpcServers` which requires
|
|
// that the `accountRpcServer` pointer exist, to not nil pointer panic
|
|
// when requests get passed to the server.
|
|
g.accountRpcServer = accounts.NewRPCServer()
|
|
|
|
g.ruleMgrs = rules.NewRuleManagerSet()
|
|
|
|
if !g.cfg.Autopilot.Disable {
|
|
if g.cfg.Autopilot.Address == "" &&
|
|
len(g.cfg.Autopilot.DialOpts) == 0 {
|
|
|
|
switch g.cfg.Network {
|
|
case "mainnet":
|
|
g.cfg.Autopilot.Address = MainnetServer
|
|
case "testnet":
|
|
g.cfg.Autopilot.Address = TestnetServer
|
|
default:
|
|
return errors.New("no autopilot server " +
|
|
"address specified")
|
|
}
|
|
}
|
|
|
|
g.cfg.Autopilot.LitVersion = autopilotserver.Version{
|
|
Major: uint32(appMajor),
|
|
Minor: uint32(appMinor),
|
|
Patch: uint32(appPatch),
|
|
}
|
|
|
|
g.autopilotClient, err = autopilotserver.NewClient(
|
|
g.cfg.Autopilot,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Similar to the init of the `accountRpcServer` reference above, we
|
|
// create a reference to the `sessionRpcServer` here before setting up
|
|
// the LND connection. See the comment above for the `accountRpcServer`
|
|
// to understand why this is necessary.
|
|
g.sessionRpcServer = newSessionRPCServer()
|
|
|
|
// Call the "real" main in a nested manner so the defers will properly
|
|
// be executed in the case of a graceful shutdown.
|
|
var (
|
|
bufRpcListener = bufconn.Listen(100)
|
|
readyChan = make(chan struct{})
|
|
bufReadyChan = make(chan struct{})
|
|
unlockChan = make(chan struct{})
|
|
lndQuit = make(chan struct{})
|
|
macChan = make(chan []byte, 1)
|
|
)
|
|
if g.cfg.LndMode == ModeIntegrated {
|
|
lisCfg := lnd.ListenerCfg{
|
|
RPCListeners: []*lnd.ListenerWithSignal{{
|
|
Listener: &onDemandListener{
|
|
addr: g.cfg.Lnd.RPCListeners[0],
|
|
},
|
|
Ready: readyChan,
|
|
}, {
|
|
Listener: bufRpcListener,
|
|
Ready: bufReadyChan,
|
|
MacChan: macChan,
|
|
}},
|
|
}
|
|
|
|
var auxComponents lnd.AuxComponents
|
|
switch g.cfg.TaprootAssetsMode {
|
|
case ModeRemote, ModeDisable:
|
|
log.Warnf("Taproot Assets daemon is either disabled " +
|
|
"or running in remote mode. Taproot Asset " +
|
|
"channel functionality will NOT be " +
|
|
"available. To enable, set Taproot Assets " +
|
|
"mode to 'integrated' in the config file.")
|
|
|
|
case ModeIntegrated:
|
|
components, err := g.buildAuxComponents(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("could not build aux "+
|
|
"components: %w", err)
|
|
}
|
|
|
|
auxComponents = *components
|
|
}
|
|
|
|
// nolint:ll
|
|
implCfg := &lnd.ImplementationCfg{
|
|
GrpcRegistrar: g,
|
|
RestRegistrar: g,
|
|
ExternalValidator: g,
|
|
DatabaseBuilder: g.defaultImplCfg.DatabaseBuilder,
|
|
WalletConfigBuilder: g,
|
|
ChainControlBuilder: g.defaultImplCfg.ChainControlBuilder,
|
|
AuxComponents: auxComponents,
|
|
}
|
|
|
|
g.wg.Add(1)
|
|
go func() {
|
|
defer g.wg.Done()
|
|
|
|
// Note that LND will call litd's RegisterGrpcSubserver
|
|
// function during the execution of this, as `g` is
|
|
// referenced in the passed `implCfg`
|
|
err := lnd.Main(g.cfg.Lnd, lisCfg, implCfg, interceptor)
|
|
if e, ok := err.(*flags.Error); err != nil &&
|
|
(!ok || e.Type != flags.ErrHelp) {
|
|
|
|
errStr := fmt.Sprintf("Error running main "+
|
|
"lnd: %v", err)
|
|
log.Errorf(errStr)
|
|
|
|
g.statusMgr.SetErrored(subservers.LND, errStr)
|
|
g.errQueue.ChanIn() <- err
|
|
|
|
return
|
|
}
|
|
|
|
close(lndQuit)
|
|
}()
|
|
} else {
|
|
close(unlockChan)
|
|
close(readyChan)
|
|
close(bufReadyChan)
|
|
|
|
_ = g.RegisterGrpcSubserver(g.rpcProxy.grpcServer)
|
|
}
|
|
|
|
// Wait for lnd to be started up so we know we have a TLS cert.
|
|
select {
|
|
// If lnd needs to be unlocked we get the signal that it's ready to do
|
|
// so. We then go ahead and start the UI so we can unlock it there as
|
|
// well.
|
|
case <-unlockChan:
|
|
|
|
// If lnd is running with --noseedbackup and doesn't need unlocking, we
|
|
// get the ready signal immediately.
|
|
case <-readyChan:
|
|
|
|
case err := <-g.errQueue.ChanOut():
|
|
g.statusMgr.SetErrored(
|
|
subservers.LND, "error from errQueue channel",
|
|
)
|
|
|
|
return fmt.Errorf("could not start LND: %v", err)
|
|
|
|
case <-lndQuit:
|
|
g.statusMgr.SetErrored(
|
|
subservers.LND, "lndQuit channel closed",
|
|
)
|
|
|
|
return fmt.Errorf("LND has stopped")
|
|
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
|
|
// Connect to LND.
|
|
g.lndConn, err = connectLND(g.cfg, bufRpcListener)
|
|
if err != nil {
|
|
g.statusMgr.SetErrored(
|
|
subservers.LND, "could not connect to LND: %v", err,
|
|
)
|
|
|
|
return fmt.Errorf("could not connect to LND")
|
|
}
|
|
|
|
// In order to be able to create unique middleware request identifiers,
|
|
// we set a new unique connection ID. This should be refreshed every
|
|
// time we (re)connect to LND.
|
|
// TODO: This assumes that litd needs to be restarted when the
|
|
// connection to LND is interrupted, leading to a unique connection ID.
|
|
// When automatic reconnection is implemented, we need to make sure that
|
|
// the connection ID is refreshed when the connection is re-established.
|
|
g.lndConnID = randId(rules.LndConnIdLen)
|
|
|
|
// Initialise any connections to sub-servers that we are running in
|
|
// remote mode.
|
|
g.subServerMgr.ConnectRemoteSubServers()
|
|
|
|
// bakeSuperMac is a closure that can be used to bake a new super
|
|
// macaroon that contains all active permissions.
|
|
bakeSuperMac := func(ctx context.Context, rootKeyIDSuffix uint32,
|
|
readOnly bool) (string, error) {
|
|
|
|
var suffixBytes [4]byte
|
|
binary.BigEndian.PutUint32(suffixBytes[:], rootKeyIDSuffix)
|
|
|
|
rootKeyID := litmac.NewSuperMacaroonRootKeyID(suffixBytes)
|
|
|
|
return litmac.BakeSuperMacaroon(
|
|
ctx, g.basicClient, rootKeyID,
|
|
g.permsMgr.ActivePermissions(readOnly), nil,
|
|
)
|
|
}
|
|
|
|
// Now start the RPC proxy that will handle all incoming gRPC, grpc-web
|
|
// and REST requests.
|
|
if err := g.rpcProxy.Start(g.lndConn, bakeSuperMac); err != nil {
|
|
return fmt.Errorf("error starting lnd gRPC proxy server: %v",
|
|
err)
|
|
}
|
|
|
|
// We now set a custom status for the LND sub-server to indicate that
|
|
// the wallet is ready.
|
|
// This is done _before_ we have set up the lnd clients so that the
|
|
// litcli status command won't error before the lnd sub-server has
|
|
// been marked as running.
|
|
g.statusMgr.SetCustomStatus(subservers.LND, lndWalletReadyStatus)
|
|
|
|
// Now that we have started the main UI web server, show some useful
|
|
// information to the user so they can access the web UI easily.
|
|
if err := g.showStartupInfo(ctx); err != nil {
|
|
return fmt.Errorf("error displaying startup info: %v", err)
|
|
}
|
|
|
|
// waitForSignal is a helper closure that can be used to wait on the
|
|
// given channel for a signal while also being responsive to an error
|
|
// from the error Queue, LND quiting or the interceptor receiving a
|
|
// shutdown signal.
|
|
waitForSignal := func(c chan struct{}) error {
|
|
select {
|
|
case <-c:
|
|
return nil
|
|
|
|
case err := <-g.errQueue.ChanOut():
|
|
return err
|
|
|
|
case <-lndQuit:
|
|
g.statusMgr.SetErrored(
|
|
subservers.LND, "lndQuit channel closed",
|
|
)
|
|
|
|
return fmt.Errorf("LND has stopped")
|
|
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
// Wait for lnd to be unlocked, then start all clients.
|
|
if err = waitForSignal(readyChan); err != nil {
|
|
return err
|
|
}
|
|
|
|
// If we're in integrated mode, we'll need to wait for lnd to send the
|
|
// macaroon after unlock before going any further.
|
|
if g.cfg.LndMode == ModeIntegrated {
|
|
if err = waitForSignal(bufReadyChan); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create a new macReady channel that will serve to signal that
|
|
// the LND macaroon is ready. Spin off a goroutine that will
|
|
// close this channel when the macaroon has been received.
|
|
macReady := make(chan struct{})
|
|
go func() {
|
|
g.cfg.lndAdminMacaroon = <-macChan
|
|
close(macReady)
|
|
}()
|
|
|
|
if err = waitForSignal(macReady); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Since we are now connected to LND, we can now set up a basic LND
|
|
// client. Note this doesn't require LND to be synced, but can still be
|
|
// used to fetch info from LND such as its macaroons. Therefore, it's ok
|
|
// set it up prior to setting up the stores and starting the other RPC
|
|
// servers, as the setup will be fast.
|
|
err = g.setupBasicLNDClient(ctx, lndQuit)
|
|
if err != nil {
|
|
g.statusMgr.SetErrored(
|
|
subservers.LND,
|
|
"could not to set up a basic LND client: %v", err,
|
|
)
|
|
|
|
return fmt.Errorf("could not start LND")
|
|
}
|
|
|
|
g.stores, err = NewStores(g.cfg, clock.NewDefaultClock())
|
|
if err != nil {
|
|
return fmt.Errorf("could not create stores: %v", err)
|
|
}
|
|
|
|
if err := g.stores.firewall.Start(ctx); err != nil {
|
|
return fmt.Errorf("could not start firewall DB: %v", err)
|
|
}
|
|
|
|
g.accountService, err = accounts.NewService(
|
|
g.stores.accounts, accountServiceErrCallback,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating account service: %v", err)
|
|
}
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("could not create new session rpc "+
|
|
"server: %v", err)
|
|
}
|
|
|
|
// Set up a full LND client. With this, we now have all LND clients
|
|
// needed for LiT to be fully started.
|
|
err = g.setupFullLNDClient(ctx, lndQuit)
|
|
if err != nil {
|
|
g.statusMgr.SetErrored(
|
|
subservers.LND,
|
|
"could not to set up a full LND client: %v", err,
|
|
)
|
|
|
|
return fmt.Errorf("could not start LND")
|
|
}
|
|
|
|
// Mark that lnd is now completely running after connecting the
|
|
// lnd clients.
|
|
g.statusMgr.SetRunning(subservers.LND)
|
|
|
|
// Both connection types are ready now, let's start our sub-servers if
|
|
// they should be started locally as an integrated service.
|
|
createDefaultMacaroons := !g.cfg.statelessInitMode
|
|
g.subServerMgr.StartIntegratedServers(
|
|
g.basicClient, g.lndClient, createDefaultMacaroons,
|
|
)
|
|
|
|
err = g.startInternalSubServers(ctx, !g.cfg.statelessInitMode)
|
|
if err != nil {
|
|
return fmt.Errorf("could not start litd sub-servers: %v", err)
|
|
}
|
|
|
|
// We can now set the status of LiT as running.
|
|
g.statusMgr.SetRunning(subservers.LIT)
|
|
|
|
// Now block until we receive an error or the main shutdown signal.
|
|
select {
|
|
case err := <-g.errQueue.ChanOut():
|
|
if err != nil {
|
|
return fmt.Errorf("received critical error from "+
|
|
"subsystem, shutting down: %v", err)
|
|
}
|
|
|
|
case <-lndQuit:
|
|
g.statusMgr.SetErrored(
|
|
subservers.LND, "lndQuit channel closed",
|
|
)
|
|
|
|
return fmt.Errorf("LND is not running")
|
|
|
|
case <-ctx.Done():
|
|
log.Infof("Shutdown signal received")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// basicLNDClient provides access to LiT's basicClient if it has been set.
|
|
func (g *LightningTerminal) basicLNDClient() (lnrpc.LightningClient, error) {
|
|
if !g.basicClientSet.Load() {
|
|
return nil, fmt.Errorf("basic LND client has not yet been set")
|
|
}
|
|
|
|
return g.basicClient, nil
|
|
}
|
|
|
|
// checkRunning checks if we should continue running for the duration of the
|
|
// defaultStartupTimeout, or else returns an error indicating why a shut-down is
|
|
// needed.
|
|
func (g *LightningTerminal) checkRunning(ctx context.Context,
|
|
lndQuit chan struct{}) error {
|
|
|
|
select {
|
|
case err := <-g.errQueue.ChanOut():
|
|
return fmt.Errorf("error from subsystem: %v", err)
|
|
|
|
case <-lndQuit:
|
|
return fmt.Errorf("LND has stopped")
|
|
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
|
|
case <-time.After(g.cfg.LndConnectInterval):
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// setupBasicLNDClient sets up a basic LND client that can be used to connect to
|
|
// LND without requiring LND to be fully synced. Since this client is only a
|
|
// basic client, not all of LNDs functionality is available through it.
|
|
func (g *LightningTerminal) setupBasicLNDClient(ctx context.Context,
|
|
lndQuit chan struct{}) error {
|
|
|
|
var (
|
|
err error
|
|
clientOptions []lndclient.BasicClientOption
|
|
)
|
|
|
|
host, network, tlsPath, macPath, macData := g.cfg.lndConnectParams()
|
|
clientOptions = append(clientOptions, lndclient.MacaroonData(
|
|
hex.EncodeToString(macData),
|
|
))
|
|
clientOptions = append(
|
|
clientOptions, lndclient.MacFilename(filepath.Base(macPath)),
|
|
)
|
|
|
|
// If we're in integrated mode, we can retrieve the macaroon string
|
|
// from lnd directly, rather than grabbing it from disk.
|
|
if g.cfg.LndMode == ModeIntegrated {
|
|
clientOptions = append(clientOptions, lndclient.Insecure())
|
|
}
|
|
|
|
// 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
|
|
// because that doesn't do anything else than just connect.
|
|
log.Infof("Connecting basic lnd client")
|
|
|
|
for {
|
|
// Create an lnd client now that we have the full configuration.
|
|
// We'll need a basic client and a full client because not all
|
|
// subservers have the same requirements.
|
|
g.basicClient, err = lndclient.NewBasicClient(
|
|
host, tlsPath, filepath.Dir(macPath),
|
|
string(network), clientOptions...,
|
|
)
|
|
if err == nil {
|
|
log.Infof("Basic lnd client connected")
|
|
|
|
break
|
|
}
|
|
|
|
g.statusMgr.SetErrored(
|
|
subservers.LIT,
|
|
"Error when setting up basic LND Client: %v", err,
|
|
)
|
|
|
|
err = g.checkRunning(ctx, lndQuit)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Infof("Retrying to connect basic lnd client")
|
|
}
|
|
g.basicClientSet.Store(true)
|
|
|
|
// If we're in integrated and stateless init mode, we won't create
|
|
// macaroon files in any of the subserver daemons.
|
|
if g.cfg.LndMode == ModeIntegrated && g.lndInterceptorChain != nil &&
|
|
g.lndInterceptorChain.MacaroonService() != nil {
|
|
|
|
// If the wallet was initialized in stateless mode, we don't
|
|
// want any macaroons lying around on the filesystem. In that
|
|
// case only the UI will be able to access any of the integrated
|
|
// daemons. In all other cases we want default macaroons so we
|
|
// can use the CLI tools to interact with loop/pool/faraday.
|
|
macService := g.lndInterceptorChain.MacaroonService()
|
|
g.cfg.statelessInitMode = macService.StatelessInit
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setupFullLNDClient connects a up a full LND client to LND. Note that the
|
|
// setup of this client will block until LND is fully synced and unlocked.
|
|
func (g *LightningTerminal) setupFullLNDClient(ctx context.Context,
|
|
lndQuit chan struct{}) error {
|
|
|
|
var (
|
|
err error
|
|
insecure bool
|
|
)
|
|
|
|
host, network, tlsPath, macPath, macData := g.cfg.lndConnectParams()
|
|
|
|
if g.cfg.LndMode == ModeIntegrated {
|
|
// Ssince we will not require tls when communicating with lnd
|
|
// via a bufconn in integrated mode, we set the insecure flag
|
|
// to true.
|
|
insecure = true
|
|
}
|
|
|
|
// When setting up a full LND client, we we need to wait for two things:
|
|
// The chain notifier to be ready and the lnd wallet being fully synced
|
|
// to its chain backend. The chain notifier will always be ready first
|
|
// so if we instruct the lndclient to wait for the wallet sync, we
|
|
// should be fully ready to start all our subservers. This will just
|
|
// block until lnd signals readiness.
|
|
log.Infof("Connecting full lnd client")
|
|
for {
|
|
g.lndClient, err = lndclient.NewLndServices(
|
|
&lndclient.LndServicesConfig{
|
|
LndAddress: host,
|
|
Network: network,
|
|
TLSPath: tlsPath,
|
|
Insecure: insecure,
|
|
CustomMacaroonPath: macPath,
|
|
CustomMacaroonHex: hex.EncodeToString(
|
|
macData,
|
|
),
|
|
CallerCtx: ctx,
|
|
CheckVersion: minimalCompatibleVersion,
|
|
RPCTimeout: g.cfg.LndRPCTimeout,
|
|
ChainSyncPollInterval: g.cfg.LndConnectInterval,
|
|
|
|
BlockUntilChainSynced: true,
|
|
BlockUntilUnlocked: true,
|
|
BlockUntilChainNotifier: true,
|
|
},
|
|
)
|
|
if err == nil {
|
|
log.Infof("Full lnd client connected")
|
|
|
|
break
|
|
}
|
|
|
|
g.statusMgr.SetErrored(
|
|
subservers.LIT,
|
|
"Error when creating LND Services client: %v",
|
|
err,
|
|
)
|
|
|
|
err = g.checkRunning(ctx, lndQuit)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Infof("Retrying to create LND Services client")
|
|
}
|
|
|
|
// Pass LND's build tags to the permission manager so that it can
|
|
// filter the available permissions accordingly.
|
|
g.permsMgr.OnLNDBuildTags(g.lndClient.Version.BuildTags)
|
|
|
|
// In the integrated mode, we received an admin macaroon once lnd was
|
|
// ready. We can now bake a "super macaroon" that contains all
|
|
// permissions of all daemons that we can use for any internal calls.
|
|
if g.cfg.LndMode == ModeIntegrated {
|
|
// Create a super macaroon that can be used to control lnd,
|
|
// faraday, loop, and pool, all at the same time.
|
|
log.Infof("Baking internal super macaroon")
|
|
superMacaroon, err := litmac.BakeSuperMacaroon(
|
|
ctx, g.basicClient, litmac.NewSuperMacaroonRootKeyID(
|
|
[4]byte{},
|
|
),
|
|
g.permsMgr.ActivePermissions(false), nil,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
g.rpcProxy.superMacaroon = superMacaroon
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// startInternalSubServers starts all Litd specific sub-servers.
|
|
func (g *LightningTerminal) startInternalSubServers(ctx context.Context,
|
|
createDefaultMacaroons bool) error {
|
|
|
|
log.Infof("Starting LiT macaroon service")
|
|
|
|
// Set up the macaroon service.
|
|
rks, db, err := lndclient.NewBoltMacaroonStore(
|
|
filepath.Join(g.cfg.LitDir, g.cfg.Network),
|
|
lncfg.MacaroonDBName, macDatabaseOpenTimeout,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
g.macaroonDB = db
|
|
g.macaroonService, err = lndclient.NewMacaroonService(
|
|
&lndclient.MacaroonServiceConfig{
|
|
RootKeyStore: rks,
|
|
MacaroonLocation: "litd",
|
|
StatelessInit: !createDefaultMacaroons,
|
|
RequiredPerms: perms.RequiredPermissions,
|
|
LndClient: &g.lndClient.LndServices,
|
|
EphemeralKey: lndclient.SharedKeyNUMS,
|
|
KeyLocator: lndclient.SharedKeyLocator,
|
|
MacaroonPath: g.cfg.MacaroonPath,
|
|
},
|
|
)
|
|
if err != nil {
|
|
log.Errorf("Could not create a new macaroon service: %v", err)
|
|
return err
|
|
}
|
|
|
|
if err := g.macaroonService.Start(); err != nil {
|
|
return fmt.Errorf("could not start macaroon service: %v", err)
|
|
}
|
|
g.macaroonServiceStarted = true
|
|
|
|
superMacBaker := func(ctx context.Context, rootKeyID uint64,
|
|
perms []bakery.Op, caveats []macaroon.Caveat) (string, error) {
|
|
|
|
return litmac.BakeSuperMacaroon(
|
|
ctx, g.basicClient, rootKeyID, perms, caveats,
|
|
)
|
|
}
|
|
|
|
log.Infof("Starting LiT accounts server")
|
|
|
|
g.accountRpcServer.Start(g.accountService, superMacBaker)
|
|
|
|
if !g.cfg.Autopilot.Disable {
|
|
withLndVersion := func(cfg *autopilotserver.Config) {
|
|
cfg.LndVersion = autopilotserver.Version{
|
|
Major: g.lndClient.Version.AppMajor,
|
|
Minor: g.lndClient.Version.AppMinor,
|
|
Patch: g.lndClient.Version.AppPatch,
|
|
}
|
|
}
|
|
|
|
err = g.autopilotClient.Start(ctx, withLndVersion)
|
|
if err != nil {
|
|
return fmt.Errorf("could not start the autopilot "+
|
|
"client: %v", err)
|
|
}
|
|
}
|
|
|
|
log.Infof("Starting LiT session server")
|
|
|
|
sessionCfg := &sessionRpcServerConfig{
|
|
db: g.stores.sessions,
|
|
basicAuth: g.rpcProxy.basicAuth,
|
|
grpcOptions: []grpc.ServerOption{
|
|
// nolint:staticcheck,
|
|
grpc.CustomCodec(grpcProxy.Codec()),
|
|
grpc.ChainStreamInterceptor(
|
|
g.rpcProxy.StreamServerInterceptor,
|
|
),
|
|
grpc.ChainUnaryInterceptor(
|
|
g.rpcProxy.UnaryServerInterceptor,
|
|
),
|
|
grpc.UnknownServiceHandler(
|
|
grpcProxy.TransparentHandler(
|
|
// Don't allow calls to litrpc.
|
|
g.rpcProxy.makeDirector(false),
|
|
),
|
|
),
|
|
},
|
|
registerGrpcServers: func(server *grpc.Server) {
|
|
g.registerSubDaemonGrpcServers(server, true)
|
|
},
|
|
superMacBaker: superMacBaker,
|
|
firstConnectionDeadline: g.cfg.FirstLNCConnDeadline,
|
|
permMgr: g.permsMgr,
|
|
actionsDB: g.stores.firewall,
|
|
autopilot: g.autopilotClient,
|
|
ruleMgrs: g.ruleMgrs,
|
|
privMap: g.stores.firewall,
|
|
}
|
|
|
|
if err = g.sessionRpcServer.start(ctx, sessionCfg); err != nil {
|
|
return err
|
|
}
|
|
g.sessionRpcServerStarted = true
|
|
|
|
// The rest of the function only applies if the rpc middleware
|
|
// interceptor has been enabled.
|
|
if g.cfg.RPCMiddleware.Disabled {
|
|
log.Infof("Internal sub server startup complete")
|
|
|
|
return nil
|
|
}
|
|
|
|
// Even if the accounts service fails on the Start function, or the
|
|
// accounts service is disabled, we still want to call Stop function as
|
|
// this closes the contexts and the db store which were opened with the
|
|
// accounts.NewService function call in the LightningTerminal start
|
|
// function above.
|
|
closeAccountService := func() {
|
|
if err := g.accountService.Stop(); err != nil {
|
|
// We only log the error if we fail to stop the service,
|
|
// as it's not critical that this succeeds in order to
|
|
// keep litd running
|
|
log.Errorf("Error stopping account service: %v", err)
|
|
}
|
|
}
|
|
|
|
log.Infof("Starting LiT account service")
|
|
if !g.cfg.Accounts.Disable {
|
|
err = g.accountService.Start(
|
|
ctx, g.lndClient.Client, g.lndClient.Router,
|
|
g.lndClient.ChainParams,
|
|
)
|
|
if err != nil {
|
|
log.Errorf("error starting account service: %v, "+
|
|
"disabling account service", err)
|
|
|
|
g.statusMgr.SetErrored(subservers.ACCOUNTS, err.Error())
|
|
|
|
closeAccountService()
|
|
} else {
|
|
g.statusMgr.SetRunning(subservers.ACCOUNTS)
|
|
|
|
g.accountServiceStarted = true
|
|
}
|
|
} else {
|
|
closeAccountService()
|
|
}
|
|
|
|
privacyMapper := firewall.NewPrivacyMapper(
|
|
g.stores.firewall, firewall.CryptoRandIntn,
|
|
g.stores.sessions,
|
|
)
|
|
|
|
mw := []mid.RequestInterceptor{
|
|
privacyMapper,
|
|
g.accountService,
|
|
}
|
|
|
|
var (
|
|
requestLogger *firewall.RequestLogger
|
|
markActionErrored = func(context.Context, uint64,
|
|
string) error {
|
|
|
|
return nil
|
|
}
|
|
)
|
|
|
|
if !g.cfg.Firewall.RequestLogger.Disable {
|
|
requestLogger, err = firewall.NewRequestLogger(
|
|
g.cfg.Firewall.RequestLogger, g.stores.firewall,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating new request "+
|
|
"logger: %w", err)
|
|
}
|
|
|
|
markActionErrored = func(ctx context.Context, reqID uint64,
|
|
reason string) error {
|
|
|
|
return requestLogger.MarkAction(
|
|
ctx, reqID, firewalldb.ActionStateError, reason,
|
|
)
|
|
}
|
|
|
|
mw = append(mw, requestLogger)
|
|
}
|
|
|
|
if !g.cfg.Autopilot.Disable {
|
|
ruleEnforcer := firewall.NewRuleEnforcer(
|
|
g.stores.firewall, g.stores.firewall,
|
|
g.stores.sessions,
|
|
g.autopilotClient.ListFeaturePerms,
|
|
g.permsMgr, g.lndClient.NodePubkey,
|
|
g.lndClient.Router,
|
|
g.lndClient.Client, g.lndConnID, g.ruleMgrs,
|
|
markActionErrored, g.stores.firewall,
|
|
)
|
|
|
|
mw = append(mw, ruleEnforcer)
|
|
}
|
|
|
|
// Start the middleware manager.
|
|
log.Infof("Starting LiT middleware manager")
|
|
g.middleware = mid.NewManager(
|
|
g.cfg.RPCMiddleware.InterceptTimeout,
|
|
g.lndClient.Client, g.errQueue.ChanIn(), mw...,
|
|
)
|
|
|
|
if err = g.middleware.Start(ctx); err != nil {
|
|
return err
|
|
}
|
|
g.middlewareStarted = true
|
|
|
|
log.Infof("Internal sub server startup complete")
|
|
|
|
return nil
|
|
}
|
|
|
|
// RegisterGrpcSubserver is a callback on the lnd.SubserverConfig struct that is
|
|
// called once lnd has initialized its main gRPC server instance. It gives the
|
|
// daemons (or external subservers) the possibility to register themselves to
|
|
// the same server instance.
|
|
//
|
|
// NOTE: This is part of the lnd.GrpcRegistrar interface.
|
|
func (g *LightningTerminal) RegisterGrpcSubserver(server *grpc.Server) error {
|
|
if err := g.defaultImplCfg.RegisterGrpcSubserver(server); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Register all other daemon RPC servers that are running in-process.
|
|
// The LiT session server should be enabled on the main interface.
|
|
g.registerSubDaemonGrpcServers(server, false)
|
|
|
|
return nil
|
|
}
|
|
|
|
// registerSubDaemonGrpcServers registers the sub daemon (Faraday, Loop, Pool
|
|
// and LiT session) servers to a given gRPC server, given they are running in
|
|
// the local process. Some of LiT's own sub-servers should be registered with
|
|
// LNC sessions and some should not - the forLNCSession boolean can be used to
|
|
// control this.
|
|
func (g *LightningTerminal) registerSubDaemonGrpcServers(server *grpc.Server,
|
|
forLNCSession bool) {
|
|
|
|
g.subServerMgr.RegisterRPCServices(server)
|
|
|
|
if forLNCSession {
|
|
litrpc.RegisterStatusServer(server, g.statusMgr)
|
|
litrpc.RegisterProxyServer(server, g.rpcProxy)
|
|
} else {
|
|
litrpc.RegisterSessionsServer(server, g.sessionRpcServer)
|
|
|
|
if !g.cfg.Accounts.Disable {
|
|
litrpc.RegisterAccountsServer(
|
|
server, g.accountRpcServer,
|
|
)
|
|
}
|
|
}
|
|
|
|
litrpc.RegisterFirewallServer(server, g.sessionRpcServer)
|
|
|
|
if !g.cfg.Autopilot.Disable {
|
|
litrpc.RegisterAutopilotServer(server, g.sessionRpcServer)
|
|
}
|
|
}
|
|
|
|
// RegisterRestSubserver is a callback on the lnd.SubserverConfig struct that is
|
|
// called once lnd has initialized its main REST server instance. It gives the
|
|
// daemons (or external subservers) the possibility to register themselves to
|
|
// the same server instance.
|
|
//
|
|
// NOTE: This is part of the lnd.RestRegistrar interface.
|
|
func (g *LightningTerminal) RegisterRestSubserver(ctx context.Context,
|
|
mux *restProxy.ServeMux, endpoint string,
|
|
dialOpts []grpc.DialOption) error {
|
|
|
|
err := g.defaultImplCfg.RegisterRestSubserver(
|
|
ctx, mux, endpoint, dialOpts,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !g.cfg.Accounts.Disable {
|
|
err = litrpc.RegisterAccountsHandlerFromEndpoint(
|
|
ctx, mux, endpoint, dialOpts,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
err = litrpc.RegisterFirewallHandlerFromEndpoint(
|
|
ctx, mux, endpoint, dialOpts,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = litrpc.RegisterAutopilotHandlerFromEndpoint(
|
|
ctx, mux, endpoint, dialOpts,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = litrpc.RegisterSessionsHandlerFromEndpoint(
|
|
ctx, mux, endpoint, dialOpts,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = litrpc.RegisterProxyHandlerFromEndpoint(
|
|
ctx, mux, endpoint, dialOpts,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = litrpc.RegisterStatusHandlerFromEndpoint(
|
|
ctx, mux, endpoint, dialOpts,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return g.subServerMgr.RegisterRestServices(ctx, mux, endpoint, dialOpts)
|
|
}
|
|
|
|
// ValidateMacaroon extracts the macaroon from the context's gRPC metadata,
|
|
// checks its signature, makes sure all specified permissions for the called
|
|
// method are contained within and finally ensures all caveat conditions are
|
|
// met. A non-nil error is returned if any of the checks fail.
|
|
//
|
|
// NOTE: This is part of the lnd.ExternalValidator interface.
|
|
func (g *LightningTerminal) ValidateMacaroon(ctx context.Context,
|
|
requiredPermissions []bakery.Op, fullMethod string) error {
|
|
|
|
// If the URL being queried has been whitelisted, then no macaroon
|
|
// validation is required for the query.
|
|
if g.permsMgr.IsWhiteListedURL(fullMethod) {
|
|
return nil
|
|
}
|
|
|
|
macHex, err := macaroons.RawMacaroonFromContext(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If we're using a super macaroon, we just make sure it is valid and
|
|
// contains all the permissions needed. If we get to this point, we're
|
|
// either in integrated lnd mode where this is the only macaroon
|
|
// validation function, and we're done after the check. Or we're in
|
|
// remote lnd mode but the request is for an in-process daemon which we
|
|
// can validate here. Any request for a remote sub-daemon goes through
|
|
// the proxy and its director and any super macaroon will be converted
|
|
// to a daemon specific macaroon before directing the call to the remote
|
|
// daemon. Those calls don't land here.
|
|
if litmac.IsSuperMacaroon(macHex) {
|
|
macBytes, err := hex.DecodeString(macHex)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return g.validateSuperMacaroon(
|
|
ctx, macBytes, requiredPermissions, fullMethod,
|
|
)
|
|
}
|
|
|
|
// Validate all macaroons for services that are running in the local
|
|
// process. Calls that we proxy to a remote host don't need to be
|
|
// checked as they'll have their own interceptor.
|
|
handledBySubserver, err := g.subServerMgr.ValidateMacaroon(
|
|
ctx, requiredPermissions, fullMethod,
|
|
)
|
|
if handledBySubserver {
|
|
return err
|
|
}
|
|
|
|
if g.permsMgr.IsSubServerURI(subservers.LIT, fullMethod) {
|
|
if !g.macaroonServiceStarted {
|
|
return fmt.Errorf("the macaroon service has not " +
|
|
"started yet")
|
|
}
|
|
|
|
if err := g.macaroonService.ValidateMacaroon(
|
|
ctx, requiredPermissions, fullMethod,
|
|
); err != nil {
|
|
return &proxyErr{
|
|
proxyContext: "lit",
|
|
wrapped: fmt.Errorf("invalid macaroon: %w",
|
|
err),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Because lnd will spin up its own gRPC server with macaroon
|
|
// interceptors if it is running in this process, it will check its
|
|
// macaroons there. If lnd is running remotely, that process will check
|
|
// the macaroons. So we don't need to worry about anything other than
|
|
// the subservers that are running in the local process.
|
|
return nil
|
|
}
|
|
|
|
// Permissions returns all permissions for which the external validator of the
|
|
// terminal is responsible.
|
|
//
|
|
// NOTE: This is part of the lnd.ExternalValidator interface.
|
|
func (g *LightningTerminal) Permissions() map[string][]bakery.Op {
|
|
return g.permsMgr.GetLitPerms()
|
|
}
|
|
|
|
// BuildWalletConfig is responsible for creating or unlocking and then
|
|
// fully initializing a wallet.
|
|
//
|
|
// NOTE: This is only implemented in order for us to intercept the setup call
|
|
// and store a reference to the interceptor chain.
|
|
//
|
|
// NOTE: This is part of the lnd.WalletConfigBuilder interface.
|
|
func (g *LightningTerminal) BuildWalletConfig(ctx context.Context,
|
|
dbs *lnd.DatabaseInstances, auxComponents *lnd.AuxComponents,
|
|
interceptorChain *rpcperms.InterceptorChain,
|
|
grpcListeners []*lnd.ListenerWithSignal) (*chainreg.PartialChainControl,
|
|
*btcwallet.Config, func(), error) {
|
|
|
|
g.lndInterceptorChain = interceptorChain
|
|
|
|
return g.defaultImplCfg.WalletConfigBuilder.BuildWalletConfig(
|
|
ctx, dbs, auxComponents, interceptorChain, grpcListeners,
|
|
)
|
|
}
|
|
|
|
// buildAuxComponent builds the auxiliary components required by lnd when
|
|
// running in integrated mode with tapd being the service that provides the
|
|
// aux component implementations.
|
|
func (g *LightningTerminal) buildAuxComponents(
|
|
ctx context.Context) (*lnd.AuxComponents, error) {
|
|
|
|
errNotAvailable := fmt.Errorf("tapd is not available, both lnd and " +
|
|
"tapd must be started in integrated mode for Taproot " +
|
|
"Assets Channels to be available")
|
|
|
|
tapdWrapper, available := g.subServerMgr.GetServer(subservers.TAP)
|
|
if !available {
|
|
return nil, errNotAvailable
|
|
}
|
|
|
|
if tapdWrapper.Remote() {
|
|
return nil, errNotAvailable
|
|
}
|
|
|
|
tapdOpt := tapdWrapper.Impl()
|
|
tapdAny, err := tapdOpt.UnwrapOrErr(errors.New("tapd not available"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tapd, ok := tapdAny.(*taprootassets.Server)
|
|
if !ok {
|
|
return nil, fmt.Errorf("tapd is not of the expected type")
|
|
}
|
|
|
|
router := msgmux.NewMultiMsgRouter()
|
|
router.Start(ctx)
|
|
err = router.RegisterEndpoint(tapd)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error registering tapd endpoint: %w",
|
|
err)
|
|
}
|
|
|
|
return &lnd.AuxComponents{
|
|
AuxLeafStore: fn.Some[lnwallet.AuxLeafStore](tapd),
|
|
MsgRouter: fn.Some[msgmux.Router](router),
|
|
AuxFundingController: fn.Some[funding.AuxFundingController](
|
|
tapd,
|
|
),
|
|
AuxSigner: fn.Some[lnwallet.AuxSigner](tapd),
|
|
TrafficShaper: fn.Some[htlcswitch.AuxTrafficShaper](tapd),
|
|
AuxDataParser: fn.Some[lnd.AuxDataParser](tapd),
|
|
AuxChanCloser: fn.Some[chancloser.AuxChanCloser](tapd),
|
|
AuxSweeper: fn.Some[sweep.AuxSweeper](tapd),
|
|
AuxContractResolver: fn.Some[lnwallet.AuxContractResolver](
|
|
tapd,
|
|
),
|
|
AuxChannelNegotiator: fn.Some[lnwallet.AuxChannelNegotiator](
|
|
tapd,
|
|
),
|
|
}, nil
|
|
}
|
|
|
|
// shutdownSubServers stops all subservers that were started and attached to
|
|
// lnd.
|
|
func (g *LightningTerminal) shutdownSubServers() error {
|
|
var returnErr error
|
|
|
|
err := g.subServerMgr.Stop()
|
|
if err != nil {
|
|
returnErr = err
|
|
}
|
|
|
|
if g.autopilotClient != nil {
|
|
g.autopilotClient.Stop()
|
|
}
|
|
|
|
if g.sessionRpcServerStarted {
|
|
if err := g.sessionRpcServer.stop(); err != nil {
|
|
log.Errorf("Error closing session DB: %v", err)
|
|
returnErr = err
|
|
}
|
|
}
|
|
|
|
if g.macaroonServiceStarted {
|
|
if err := g.macaroonService.Stop(); err != nil {
|
|
log.Errorf("Error stopping macaroon service: %v", err)
|
|
returnErr = err
|
|
}
|
|
}
|
|
|
|
if g.macaroonDB != nil {
|
|
g.macaroonDB.Close()
|
|
}
|
|
|
|
if g.accountServiceStarted {
|
|
if err := g.accountService.Stop(); err != nil {
|
|
log.Errorf("Error stopping account service: %v", err)
|
|
returnErr = err
|
|
}
|
|
}
|
|
|
|
if g.middlewareStarted {
|
|
g.middleware.Stop()
|
|
}
|
|
|
|
if g.ruleMgrs != nil {
|
|
if err := g.ruleMgrs.Stop(); err != nil {
|
|
log.Errorf("Error stopping rule manager set: %v", err)
|
|
returnErr = err
|
|
}
|
|
}
|
|
|
|
if g.stores != nil {
|
|
if g.stores.firewall != nil {
|
|
if err := g.stores.firewall.Stop(); err != nil {
|
|
log.Errorf("Error stopping firewall DB: %v",
|
|
err)
|
|
|
|
returnErr = err
|
|
}
|
|
}
|
|
|
|
err = g.stores.close()
|
|
if err != nil {
|
|
log.Errorf("Error closing stores: %v", err)
|
|
returnErr = err
|
|
}
|
|
}
|
|
|
|
if g.lndClient != nil {
|
|
g.lndClient.Close()
|
|
}
|
|
|
|
if g.restCancel != nil {
|
|
g.restCancel()
|
|
}
|
|
|
|
if g.lndConn != nil {
|
|
if err := g.lndConn.Close(); err != nil {
|
|
log.Errorf("Error closing lnd connection: %v", err)
|
|
returnErr = err
|
|
}
|
|
}
|
|
|
|
if g.rpcProxy != nil {
|
|
if err := g.rpcProxy.Stop(); err != nil {
|
|
log.Errorf("Error stopping rpc proxy: %v", err)
|
|
returnErr = err
|
|
}
|
|
}
|
|
|
|
if g.httpServer != nil {
|
|
if err := g.httpServer.Close(); err != nil {
|
|
log.Errorf("Error stopping UI server: %v", err)
|
|
returnErr = err
|
|
}
|
|
}
|
|
|
|
// Do we have any last errors to display? We use an anonymous function,
|
|
// so we can use return instead of breaking to a label in the default
|
|
// case.
|
|
func() {
|
|
for {
|
|
select {
|
|
case err := <-g.errQueue.ChanOut():
|
|
if err != nil {
|
|
log.Errorf("Error while stopping "+
|
|
"litd: %v", err)
|
|
returnErr = err
|
|
}
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
return returnErr
|
|
}
|
|
|
|
// startMainWebServer creates the main web HTTP server that delegates requests
|
|
// between the embedded HTTP server and the RPC proxy. An incoming request will
|
|
// go through the following chain of components:
|
|
//
|
|
// Request on port 8443 <------------------------------------+
|
|
// | converted gRPC request |
|
|
// v |
|
|
// +---+----------------------+ other +----------------+ |
|
|
// | Main web HTTP server +------->+ Embedded HTTP | |
|
|
// +---+----------------------+____+ +----------------+ |
|
|
// | | |
|
|
// v any RPC or grpc-web call | any REST call |
|
|
// +---+----------------------+ |->+----------------+ |
|
|
// | grpc-web proxy | + grpc-gateway +-----------+
|
|
// +---+----------------------+ +----------------+
|
|
// |
|
|
// v native gRPC call with basic auth
|
|
// +---+----------------------+
|
|
// | interceptors |
|
|
// +---+----------------------+
|
|
// |
|
|
// v native gRPC call with macaroon
|
|
// +---+----------------------+
|
|
// | gRPC server |
|
|
// +---+----------------------+
|
|
// |
|
|
// v unknown authenticated call, gRPC server is just a wrapper
|
|
// +---+----------------------+
|
|
// | director |
|
|
// +---+----------------------+
|
|
// |
|
|
// v authenticated call
|
|
// +---+----------------------+ call to lnd or integrated daemon
|
|
// | lnd (remote or local) +---------------+
|
|
// | faraday remote | |
|
|
// | loop remote | +----------v----------+
|
|
// | pool remote | | lnd local subserver |
|
|
// +--------------------------+ | - faraday |
|
|
// | - loop |
|
|
// | - pool |
|
|
// +---------------------+
|
|
func (g *LightningTerminal) startMainWebServer() error {
|
|
// Initialize the in-memory file server from the content compiled by
|
|
// the go:embed directive. Since everything's relative to the root dir,
|
|
// we need to create an FS of the sub directory app/build.
|
|
buildDir, err := fs.Sub(appBuildFS, appFilesDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
staticFileServer := http.FileServer(&ClientRouteWrapper{
|
|
assets: http.FS(buildDir),
|
|
})
|
|
|
|
// 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) {
|
|
// 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
|
|
}
|
|
|
|
// REST requests aren't that easy to identify, we have to look
|
|
// at the URL itself. If this is a REST request, we give it
|
|
// directly to our REST handler which will then forward it to
|
|
// us again but converted to a gRPC request.
|
|
if g.cfg.EnableREST && isRESTRequest(req) {
|
|
log.Infof("Handling REST request: %s", req.URL.Path)
|
|
g.restHandler.ServeHTTP(resp, req)
|
|
|
|
return
|
|
}
|
|
|
|
// If the UI is disabled, then we return a 401 here to prevent
|
|
// serving any of the static files.
|
|
if g.cfg.DisableUI {
|
|
resp.WriteHeader(http.StatusUnauthorized)
|
|
|
|
return
|
|
}
|
|
|
|
// If we got here, it's a static file the browser wants, or
|
|
// 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)/.*`)
|
|
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.
|
|
gzipHandler := makeGzipHandler(staticFileServer.ServeHTTP)
|
|
gzipHandler(resp, req)
|
|
}
|
|
|
|
// Create and start our HTTPS server now that will handle both gRPC web
|
|
// and static file requests.
|
|
g.httpServer = &http.Server{
|
|
// To make sure that long-running calls and indefinitely opened
|
|
// streaming connections aren't terminated by the internal
|
|
// proxy, we need to disable all timeouts except the one for
|
|
// reading the HTTP headers. That timeout shouldn't be removed
|
|
// as we would otherwise be prone to the slowloris attack where
|
|
// an attacker takes too long to send the headers and uses up
|
|
// connections that way. Once the headers are read, we either
|
|
// know it's a static resource and can deliver that very cheaply
|
|
// or check the authentication for other calls.
|
|
WriteTimeout: 0,
|
|
IdleTimeout: 0,
|
|
ReadTimeout: 0,
|
|
ReadHeaderTimeout: defaultServerTimeout,
|
|
Handler: http.HandlerFunc(httpHandler),
|
|
}
|
|
httpListener, err := net.Listen("tcp", g.cfg.HTTPSListen)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to listen on %v: %v",
|
|
g.cfg.HTTPSListen, err)
|
|
}
|
|
tlsConfig, err := buildTLSConfigForHttp2(g.cfg)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create TLS config: %v", err)
|
|
}
|
|
tlsListener := tls.NewListener(httpListener, tlsConfig)
|
|
|
|
g.wg.Add(1)
|
|
go func() {
|
|
defer g.wg.Done()
|
|
|
|
log.Infof("Listening for http_tls on: %v", tlsListener.Addr())
|
|
err := g.httpServer.Serve(tlsListener)
|
|
if err != nil && err != http.ErrServerClosed {
|
|
log.Errorf("http_tls server error: %v", err)
|
|
}
|
|
}()
|
|
|
|
// We only enable an additional HTTP only listener if the user
|
|
// explicitly sets a value.
|
|
if g.cfg.HTTPListen != "" {
|
|
insecureListener, err := net.Listen("tcp", g.cfg.HTTPListen)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to listen on %v: %v",
|
|
g.cfg.HTTPListen, err)
|
|
}
|
|
|
|
g.wg.Add(1)
|
|
go func() {
|
|
defer g.wg.Done()
|
|
|
|
log.Infof("Listening for http on: %v",
|
|
insecureListener.Addr())
|
|
err := g.httpServer.Serve(insecureListener)
|
|
if err != nil && err != http.ErrServerClosed {
|
|
log.Errorf("http server error: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// createRESTProxy creates a grpc-gateway based REST proxy that takes any call
|
|
// identified as a REST call, converts it to a gRPC request and forwards it to
|
|
// our local main server for further triage/forwarding.
|
|
func (g *LightningTerminal) createRESTProxy(ctx context.Context) error {
|
|
// The default JSON marshaler of the REST proxy only sets OrigName to
|
|
// true, which instructs it to use the same field names as specified in
|
|
// the proto file and not switch to camel case. What we also want is
|
|
// that the marshaler prints all values, even if they are falsey.
|
|
customMarshalerOption := restProxy.WithMarshalerOption(
|
|
restProxy.MIMEWildcard, &restProxy.JSONPb{
|
|
MarshalOptions: protojson.MarshalOptions{
|
|
UseProtoNames: true,
|
|
EmitUnpopulated: true,
|
|
},
|
|
},
|
|
)
|
|
|
|
// For our REST dial options, we increase the max message size that
|
|
// we'll decode to allow clients to hit endpoints which return more data
|
|
// such as the DescribeGraph call. We set this to 200MiB atm. Should be
|
|
// the same value as maxMsgRecvSize in lnd/cmd/lncli/main.go.
|
|
restDialOpts := []grpc.DialOption{
|
|
// We are forwarding the requests directly to the address of our
|
|
// own local listener. To not need to mess with the TLS
|
|
// certificate (which might be tricky if we're using Let's
|
|
// Encrypt), we just skip the certificate verification.
|
|
// Injecting a malicious hostname into the listener address will
|
|
// result in an error on startup so this should be quite safe.
|
|
grpc.WithTransportCredentials(credentials.NewTLS(
|
|
&tls.Config{InsecureSkipVerify: true},
|
|
)),
|
|
grpc.WithDefaultCallOptions(
|
|
grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200),
|
|
),
|
|
}
|
|
|
|
// We use our own RPC listener as the destination for our REST proxy.
|
|
// If the listener is set to listen on all interfaces, we replace it
|
|
// with localhost, as we cannot dial it directly.
|
|
restProxyDest := toLocalAddress(g.cfg.HTTPSListen)
|
|
|
|
// Now start the REST proxy for our gRPC server above. We'll ensure
|
|
// we direct LND to connect to its loopback address rather than a
|
|
// wildcard to prevent certificate issues when accessing the proxy
|
|
// externally.
|
|
restMux := restProxy.NewServeMux(customMarshalerOption)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
g.restCancel = cancel
|
|
|
|
// Enable WebSocket and CORS support as well. A request will pass
|
|
// through the following chain:
|
|
// req ---> CORS handler --> WS proxy ---> REST proxy --> gRPC endpoint
|
|
// where gRPC endpoint is our main HTTP(S) listener again.
|
|
restHandler := lnrpc.NewWebSocketProxy(
|
|
restMux, log, g.cfg.Lnd.WSPingInterval, g.cfg.Lnd.WSPongWait,
|
|
lnrpc.LndClientStreamingURIs,
|
|
)
|
|
g.restHandler = allowCORS(restHandler, g.cfg.RestCORS)
|
|
|
|
// First register all lnd handlers. This will make it possible to speak
|
|
// REST over the main RPC listener port in both remote and integrated
|
|
// mode. In integrated mode the user can still use the --lnd.restlisten
|
|
// to spin up an extra REST listener that also offers the same
|
|
// functionality, but is no longer required. In remote mode REST will
|
|
// only be enabled on the main HTTP(S) listener.
|
|
for _, registrationFn := range lndRESTRegistrations {
|
|
err := registrationFn(ctx, restMux, restProxyDest, restDialOpts)
|
|
if err != nil {
|
|
return fmt.Errorf("error registering REST handler: %v",
|
|
err)
|
|
}
|
|
}
|
|
|
|
// Now register all handlers for faraday, loop and pool.
|
|
err := g.RegisterRestSubserver(
|
|
ctx, restMux, restProxyDest, restDialOpts,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("error registering REST handler: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateSuperMacaroon makes sure the given macaroon is a valid super macaroon
|
|
// that was issued by lnd and contains all the required permissions, even if
|
|
// the actual RPC method isn't a lnd request.
|
|
func (g *LightningTerminal) validateSuperMacaroon(ctx context.Context,
|
|
superMacaroon []byte, requiredPermissions []bakery.Op,
|
|
fullMethod string) error {
|
|
|
|
// If we haven't connected to lnd yet, we can't check the super
|
|
// macaroon. The user will need to wait a bit.
|
|
if g.lndClient == nil {
|
|
return fmt.Errorf("cannot validate macaroon, not yet " +
|
|
"connected to lnd, please wait")
|
|
}
|
|
|
|
// Convert permissions to the form that lndClient will accept.
|
|
permissions := make(
|
|
[]lndclient.MacaroonPermission, len(requiredPermissions),
|
|
)
|
|
for idx, perm := range requiredPermissions {
|
|
permissions[idx] = lndclient.MacaroonPermission{
|
|
Entity: perm.Entity,
|
|
Action: perm.Action,
|
|
}
|
|
}
|
|
|
|
res, err := g.lndClient.Client.CheckMacaroonPermissions(
|
|
ctx, superMacaroon, permissions, fullMethod,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("lnd macaroon validation failed: %v",
|
|
err)
|
|
}
|
|
if !res {
|
|
return fmt.Errorf("macaroon is not valid")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// initSubServers registers the faraday and loop sub-servers with the
|
|
// subServerMgr.
|
|
func (g *LightningTerminal) initSubServers() error {
|
|
err := g.subServerMgr.AddServer(
|
|
subservers.NewFaradaySubServer(
|
|
g.cfg.Faraday, g.cfg.faradayRpcConfig,
|
|
g.cfg.Remote.Faraday, g.cfg.faradayRemote,
|
|
), g.cfg.FaradayMode != ModeDisable,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("could not register Faraday subserver: %w",
|
|
err)
|
|
}
|
|
|
|
err = g.subServerMgr.AddServer(
|
|
subservers.NewLoopSubServer(
|
|
g.cfg.Loop, g.cfg.Remote.Loop, g.cfg.loopRemote,
|
|
), g.cfg.LoopMode != ModeDisable,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("could not register Loop subserver: %w", err)
|
|
}
|
|
|
|
err = g.subServerMgr.AddServer(
|
|
subservers.NewPoolSubServer(
|
|
g.cfg.Pool, g.cfg.Remote.Pool, g.cfg.poolRemote,
|
|
), g.cfg.PoolMode != ModeDisable,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("could not register Pool subserver: %w", err)
|
|
}
|
|
|
|
err = g.subServerMgr.AddServer(
|
|
subservers.NewTaprootAssetsSubServer(
|
|
g.cfg.Network, g.cfg.TaprootAssets,
|
|
g.cfg.Remote.TaprootAssets,
|
|
g.cfg.tapRemote, g.cfg.lndRemote,
|
|
), g.cfg.TaprootAssetsMode != ModeDisable,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("could not register Taproot Assets "+
|
|
"subserver: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// allowCORS wraps the given http.Handler with a function that adds the
|
|
// Access-Control-Allow-Origin header to the response.
|
|
func allowCORS(handler http.Handler, origins []string) http.Handler {
|
|
allowHeaders := "Access-Control-Allow-Headers"
|
|
allowMethods := "Access-Control-Allow-Methods"
|
|
allowOrigin := "Access-Control-Allow-Origin"
|
|
|
|
// If the user didn't supply any origins that means CORS is disabled
|
|
// and we should return the original handler.
|
|
if len(origins) == 0 {
|
|
return handler
|
|
}
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
origin := r.Header.Get("Origin")
|
|
|
|
// Skip everything if the browser doesn't send the Origin field.
|
|
if origin == "" {
|
|
handler.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
// Set the static header fields first.
|
|
w.Header().Set(
|
|
allowHeaders,
|
|
"Content-Type, Accept, Grpc-Metadata-Macaroon",
|
|
)
|
|
w.Header().Set(allowMethods, "GET, POST, DELETE")
|
|
|
|
// Either we allow all origins or the incoming request matches
|
|
// a specific origin in our list of allowed origins.
|
|
for _, allowedOrigin := range origins {
|
|
if allowedOrigin == "*" || origin == allowedOrigin {
|
|
// Only set allowed origin to requested origin.
|
|
w.Header().Set(allowOrigin, origin)
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
// For a pre-flight request we only need to send the headers
|
|
// back. No need to call the rest of the chain.
|
|
if r.Method == "OPTIONS" {
|
|
return
|
|
}
|
|
|
|
// Everything's prepared now, we can pass the request along the
|
|
// chain of handlers.
|
|
handler.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// showStartupInfo shows useful information to the user to easily access the
|
|
// web UI that was just started.
|
|
func (g *LightningTerminal) showStartupInfo(ctx context.Context) error {
|
|
info := struct {
|
|
mode string
|
|
status string
|
|
alias string
|
|
version string
|
|
webURI string
|
|
}{
|
|
mode: g.cfg.LndMode,
|
|
status: "locked",
|
|
alias: g.cfg.Lnd.Alias,
|
|
version: build.Version(),
|
|
webURI: fmt.Sprintf("https://%s", strings.ReplaceAll(
|
|
strings.ReplaceAll(
|
|
g.cfg.HTTPSListen, "0.0.0.0", "localhost",
|
|
), "[::]", "localhost",
|
|
)),
|
|
}
|
|
|
|
// In remote mode we try to query the info.
|
|
if g.cfg.LndMode == ModeRemote {
|
|
// We try to query GetInfo on the remote node to find out the
|
|
// alias. But the wallet might be locked.
|
|
host, network, tlsPath, macPath, _ := g.cfg.lndConnectParams()
|
|
basicClient, err := lndclient.NewBasicClient(
|
|
host, tlsPath, filepath.Dir(macPath), string(network),
|
|
lndclient.MacFilename(filepath.Base(macPath)),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("error querying remote node: %v", err)
|
|
}
|
|
|
|
res, err := basicClient.GetInfo(ctx, &lnrpc.GetInfoRequest{})
|
|
if err != nil {
|
|
if !lndclient.IsUnlockError(err) {
|
|
return fmt.Errorf("error querying remote "+
|
|
"node : %v", err)
|
|
}
|
|
|
|
// Node is locked.
|
|
info.status = "locked"
|
|
info.alias = "???? (node is locked)"
|
|
info.version = "???? (node is locked)"
|
|
} else {
|
|
info.status = "online"
|
|
info.alias = res.Alias
|
|
info.version = res.Version
|
|
}
|
|
}
|
|
|
|
// In integrated mode, we can derive the state from our configuration.
|
|
if g.cfg.LndMode == ModeIntegrated {
|
|
// If the integrated node is running with no seed backup, the
|
|
// wallet cannot be locked and the node is online right away.
|
|
if g.cfg.Lnd.NoSeedBackup {
|
|
info.status = "online"
|
|
}
|
|
}
|
|
|
|
// If there's an additional HTTP listener, list it as well.
|
|
listenAddr := g.cfg.HTTPSListen
|
|
if g.cfg.HTTPListen != "" {
|
|
host := toLocalAddress(g.cfg.HTTPListen)
|
|
info.webURI = fmt.Sprintf("%s or http://%s", info.webURI, host)
|
|
listenAddr = fmt.Sprintf("%s, %s", listenAddr, g.cfg.HTTPListen)
|
|
}
|
|
|
|
webInterfaceString := fmt.Sprintf(
|
|
"%s (open %s in your browser)", listenAddr, info.webURI,
|
|
)
|
|
if g.cfg.DisableUI {
|
|
webInterfaceString = "disabled"
|
|
}
|
|
|
|
// nolint:ll
|
|
str := "" +
|
|
"----------------------------------------------------------\n" +
|
|
" Lightning Terminal (LiT) by Lightning Labs \n" +
|
|
" \n" +
|
|
" LND Operating mode %s \n" +
|
|
" LND Node status %s \n" +
|
|
" LND Alias %s \n" +
|
|
" LND Version %s \n" +
|
|
" LiT Version %s \n" +
|
|
" Web interface %s \n" +
|
|
"----------------------------------------------------------\n"
|
|
fmt.Printf(str, info.mode, info.status, info.alias, info.version,
|
|
RichVersion(), webInterfaceString)
|
|
|
|
return 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
|
|
type ClientRouteWrapper struct {
|
|
assets http.FileSystem
|
|
}
|
|
|
|
// Open intercepts requests to open files. If the file does not exist and there
|
|
// is no file extension, then assume this is a client side route and return the
|
|
// contents of index.html
|
|
func (i *ClientRouteWrapper) Open(name string) (http.File, error) {
|
|
localName := name
|
|
|
|
// The file prefix can be overwritten during build time.
|
|
if appFilesPrefix != "" {
|
|
localName = strings.Replace(name, appFilesPrefix, "/", 1)
|
|
}
|
|
localName = strings.ReplaceAll(localName, "//", "/")
|
|
ret, err := i.assets.Open(localName)
|
|
if !os.IsNotExist(err) || filepath.Ext(localName) != "" {
|
|
return ret, err
|
|
}
|
|
|
|
return i.assets.Open("/index.html")
|
|
}
|
|
|
|
// toLocalAddress converts an address that is meant as a wildcard listening
|
|
// address ("0.0.0.0" or "[::]") into an address that can be dialed (localhost).
|
|
func toLocalAddress(listenerAddress string) string {
|
|
addr := strings.ReplaceAll(listenerAddress, "0.0.0.0", "localhost")
|
|
return strings.ReplaceAll(addr, "[::]", "localhost")
|
|
}
|
|
|
|
// isRESTRequest determines if a request is a REST request by checking that the
|
|
// URI starts with /vX/ where X is a single digit number. This is currently true
|
|
// for all REST URIs of lnd, faraday, loop and pool as they all either start
|
|
// with /v1/ or /v2/.
|
|
func isRESTRequest(req *http.Request) bool {
|
|
return patternRESTRequest.MatchString(req.URL.Path)
|
|
}
|
|
|
|
// randId generates a random character string of length n.
|
|
func randId(n int) string {
|
|
var letters = []rune(
|
|
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
)
|
|
|
|
b := make([]rune, n)
|
|
for i := range b {
|
|
b[i] = letters[rand.Intn(len(letters))] //nolint: gosec
|
|
}
|
|
|
|
return string(b)
|
|
}
|