2020-07-21 22:06:15 +02:00
|
|
|
package terminal
|
2020-05-26 16:30:26 +02:00
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/tls"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net"
|
|
|
|
|
"net/http"
|
2020-06-22 12:41:48 -04:00
|
|
|
"os"
|
2021-01-18 15:38:42 +01:00
|
|
|
"path"
|
2020-05-26 16:30:26 +02:00
|
|
|
"path/filepath"
|
2020-07-01 01:26:16 -04:00
|
|
|
"regexp"
|
2020-10-08 09:34:49 +02:00
|
|
|
"strings"
|
2020-05-26 16:30:26 +02:00
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
restProxy "github.com/grpc-ecosystem/grpc-gateway/runtime"
|
|
|
|
|
"github.com/jessevdk/go-flags"
|
|
|
|
|
"github.com/lightninglabs/faraday/frdrpc"
|
2020-07-08 16:00:56 -07:00
|
|
|
"github.com/lightninglabs/lndclient"
|
2020-10-13 19:04:34 +02:00
|
|
|
"github.com/lightninglabs/loop"
|
2020-05-26 16:30:26 +02:00
|
|
|
"github.com/lightninglabs/loop/loopd"
|
|
|
|
|
"github.com/lightninglabs/loop/looprpc"
|
2020-07-21 10:47:53 +02:00
|
|
|
"github.com/lightninglabs/pool"
|
|
|
|
|
"github.com/lightninglabs/pool/poolrpc"
|
2020-05-26 16:30:26 +02:00
|
|
|
"github.com/lightningnetwork/lnd"
|
2020-10-08 09:34:49 +02:00
|
|
|
"github.com/lightningnetwork/lnd/build"
|
2020-05-26 16:30:26 +02:00
|
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
|
|
|
"github.com/lightningnetwork/lnd/lntest/wait"
|
|
|
|
|
"github.com/lightningnetwork/lnd/signal"
|
|
|
|
|
"github.com/rakyll/statik/fs"
|
|
|
|
|
"google.golang.org/grpc"
|
2020-10-08 09:34:49 +02:00
|
|
|
"google.golang.org/grpc/codes"
|
|
|
|
|
"google.golang.org/grpc/status"
|
2020-10-08 09:34:42 +02:00
|
|
|
"gopkg.in/macaroon-bakery.v2/bakery"
|
|
|
|
|
|
2020-05-26 16:30:26 +02:00
|
|
|
// Import generated go package that contains all static files for the
|
|
|
|
|
// UI in a compressed format.
|
2020-07-21 22:06:15 +02:00
|
|
|
_ "github.com/lightninglabs/lightning-terminal/statik"
|
2020-05-26 16:30:26 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
defaultServerTimeout = 10 * time.Second
|
2020-05-29 16:03:22 +02:00
|
|
|
defaultConnectTimeout = 5 * time.Second
|
2020-05-26 16:30:26 +02:00
|
|
|
defaultStartupTimeout = 5 * time.Second
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
// maxMsgRecvSize is the largest message our REST proxy will receive. We
|
|
|
|
|
// set this to 200MiB atm.
|
|
|
|
|
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200)
|
|
|
|
|
)
|
|
|
|
|
|
2020-07-21 22:06:15 +02:00
|
|
|
// 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 {
|
2020-10-08 09:34:43 +02:00
|
|
|
cfg *Config
|
2020-05-26 16:30:26 +02:00
|
|
|
|
|
|
|
|
wg sync.WaitGroup
|
|
|
|
|
lndErrChan chan error
|
|
|
|
|
|
2021-02-26 15:41:14 +01:00
|
|
|
lndClient *lndclient.GrpcLndServices
|
2020-05-26 16:30:26 +02:00
|
|
|
|
|
|
|
|
faradayServer *frdrpc.RPCServer
|
|
|
|
|
faradayStarted bool
|
|
|
|
|
|
|
|
|
|
loopServer *loopd.Daemon
|
|
|
|
|
loopStarted bool
|
|
|
|
|
|
2020-07-21 10:47:53 +02:00
|
|
|
poolServer *pool.Server
|
|
|
|
|
poolStarted bool
|
|
|
|
|
|
2020-10-08 09:34:41 +02:00
|
|
|
rpcProxy *rpcProxy
|
|
|
|
|
httpServer *http.Server
|
2020-05-26 16:30:26 +02:00
|
|
|
}
|
|
|
|
|
|
2020-07-21 22:06:15 +02:00
|
|
|
// New creates a new instance of the lightning-terminal daemon.
|
|
|
|
|
func New() *LightningTerminal {
|
|
|
|
|
return &LightningTerminal{
|
2020-05-26 16:30:26 +02:00
|
|
|
lndErrChan: make(chan error, 1),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Run starts everything and then blocks until either the application is shut
|
|
|
|
|
// down or a critical error happens.
|
2020-07-21 22:06:15 +02:00
|
|
|
func (g *LightningTerminal) Run() error {
|
2020-10-08 09:34:43 +02:00
|
|
|
cfg, err := loadAndValidateConfig()
|
2020-05-26 16:30:26 +02:00
|
|
|
if err != nil {
|
2020-10-08 09:34:43 +02:00
|
|
|
return fmt.Errorf("could not load config: %w", err)
|
2020-05-26 16:30:26 +02:00
|
|
|
}
|
2020-10-08 09:34:43 +02:00
|
|
|
g.cfg = cfg
|
2020-05-26 16:30:26 +02:00
|
|
|
|
|
|
|
|
// Create the instances of our subservers now so we can hook them up to
|
|
|
|
|
// lnd once it's fully started.
|
2020-10-08 09:34:43 +02:00
|
|
|
g.faradayServer = frdrpc.NewRPCServer(g.cfg.faradayRpcConfig)
|
2020-05-26 16:30:26 +02:00
|
|
|
g.loopServer = loopd.New(g.cfg.Loop, nil)
|
2020-07-21 10:47:53 +02:00
|
|
|
g.poolServer = pool.NewServer(g.cfg.Pool)
|
2020-10-08 09:34:42 +02:00
|
|
|
g.rpcProxy = newRpcProxy(g.cfg, g, getAllPermissions())
|
2020-05-26 16:30:26 +02:00
|
|
|
|
2021-01-27 15:48:30 +01:00
|
|
|
// Overwrite the loop and pool daemon's user agent name so it sends
|
|
|
|
|
// "litd" instead of "loopd" and "poold" respectively.
|
2020-10-13 19:04:34 +02:00
|
|
|
loop.AgentName = "litd"
|
2021-01-27 15:48:30 +01:00
|
|
|
pool.SetAgentName("litd")
|
2020-10-13 19:04:34 +02:00
|
|
|
|
2020-05-26 16:30:26 +02:00
|
|
|
// Hook interceptor for os signals.
|
2020-10-08 09:34:39 +02:00
|
|
|
err = signal.Intercept()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("could not intercept signals: %v", err)
|
|
|
|
|
}
|
2020-05-26 16:30:26 +02:00
|
|
|
|
|
|
|
|
// Call the "real" main in a nested manner so the defers will properly
|
|
|
|
|
// be executed in the case of a graceful shutdown.
|
2020-10-08 09:34:43 +02:00
|
|
|
readyChan := make(chan struct{})
|
|
|
|
|
unlockChan := make(chan struct{})
|
|
|
|
|
if g.cfg.LndMode == ModeIntegrated {
|
|
|
|
|
g.wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer g.wg.Done()
|
|
|
|
|
|
|
|
|
|
extSubCfg := &lnd.RPCSubserverConfig{
|
|
|
|
|
Permissions: getSubserverPermissions(),
|
|
|
|
|
Registrar: g,
|
|
|
|
|
MacaroonValidator: g,
|
|
|
|
|
}
|
|
|
|
|
lisCfg := lnd.ListenerCfg{
|
|
|
|
|
RPCListener: &lnd.ListenerWithSignal{
|
|
|
|
|
Listener: &onDemandListener{
|
|
|
|
|
addr: g.cfg.Lnd.RPCListeners[0],
|
|
|
|
|
},
|
|
|
|
|
Ready: readyChan,
|
|
|
|
|
ExternalRPCSubserverCfg: extSubCfg,
|
|
|
|
|
ExternalRestRegistrar: g,
|
|
|
|
|
},
|
|
|
|
|
WalletUnlocker: &lnd.ListenerWithSignal{
|
|
|
|
|
Listener: &onDemandListener{
|
|
|
|
|
addr: g.cfg.Lnd.RPCListeners[0],
|
|
|
|
|
},
|
|
|
|
|
Ready: unlockChan,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := lnd.Main(
|
|
|
|
|
g.cfg.Lnd, lisCfg, signal.ShutdownChannel(),
|
|
|
|
|
)
|
|
|
|
|
if e, ok := err.(*flags.Error); err != nil &&
|
|
|
|
|
(!ok || e.Type != flags.ErrHelp) {
|
|
|
|
|
|
|
|
|
|
log.Errorf("Error running main lnd: %v", err)
|
|
|
|
|
g.lndErrChan <- err
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
close(g.lndErrChan)
|
|
|
|
|
}()
|
|
|
|
|
} else {
|
|
|
|
|
close(unlockChan)
|
|
|
|
|
close(readyChan)
|
|
|
|
|
|
|
|
|
|
_ = g.RegisterGrpcSubserver(g.rpcProxy.grpcServer)
|
|
|
|
|
}
|
2020-05-26 16:30:26 +02:00
|
|
|
|
2020-10-08 09:34:43 +02:00
|
|
|
// 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:
|
2020-05-26 16:30:26 +02:00
|
|
|
|
2020-10-08 09:34:43 +02:00
|
|
|
// If lnd is running with --noseedbackup and doesn't need unlocking, we
|
|
|
|
|
// get the ready signal immediately.
|
|
|
|
|
case <-readyChan:
|
2020-05-26 16:30:26 +02:00
|
|
|
|
2020-10-08 09:34:43 +02:00
|
|
|
case err := <-g.lndErrChan:
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
|
|
case <-signal.ShutdownChannel():
|
|
|
|
|
return errors.New("shutting down")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We now know that starting lnd was successful. If we now run into an
|
|
|
|
|
// error, we must shut down lnd correctly.
|
2020-05-26 16:30:26 +02:00
|
|
|
defer func() {
|
|
|
|
|
err := g.shutdown()
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Errorf("Error shutting down: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
2020-10-08 09:34:43 +02:00
|
|
|
// Now start the RPC proxy that will handle all incoming gRPC, grpc-web
|
|
|
|
|
// and REST requests. We also start the main web server that dispatches
|
|
|
|
|
// requests either to the static UI file server or the RPC proxy. This
|
2021-02-16 13:48:24 +01:00
|
|
|
// makes it possible to unlock lnd through the UI.
|
2020-10-08 09:34:43 +02:00
|
|
|
if err := g.rpcProxy.Start(); err != nil {
|
|
|
|
|
return fmt.Errorf("error starting lnd gRPC proxy server: %v",
|
|
|
|
|
err)
|
|
|
|
|
}
|
|
|
|
|
if err := g.startMainWebServer(); err != nil {
|
|
|
|
|
return fmt.Errorf("error starting UI HTTP server: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-08 09:34:49 +02:00
|
|
|
// 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(); err != nil {
|
|
|
|
|
return fmt.Errorf("error displaying startup info: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-26 16:30:26 +02:00
|
|
|
// Wait for lnd to be unlocked, then start all clients.
|
|
|
|
|
select {
|
2020-10-08 09:34:43 +02:00
|
|
|
case <-readyChan:
|
|
|
|
|
|
|
|
|
|
case err := <-g.lndErrChan:
|
|
|
|
|
return err
|
2020-05-26 16:30:26 +02:00
|
|
|
|
|
|
|
|
case <-signal.ShutdownChannel():
|
|
|
|
|
return errors.New("shutting down")
|
|
|
|
|
}
|
2020-10-08 09:34:43 +02:00
|
|
|
|
2020-10-08 09:34:38 +02:00
|
|
|
err = g.startSubservers()
|
2020-05-26 16:30:26 +02:00
|
|
|
if err != nil {
|
|
|
|
|
log.Errorf("Could not start subservers: %v", err)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now block until we receive an error or the main shutdown signal.
|
|
|
|
|
select {
|
|
|
|
|
case err := <-g.loopServer.ErrChan:
|
|
|
|
|
// Loop will shut itself down if an error happens. We don't need
|
|
|
|
|
// to try to stop it again.
|
|
|
|
|
g.loopStarted = false
|
|
|
|
|
log.Errorf("Received critical error from loop, shutting down: "+
|
|
|
|
|
"%v", err)
|
|
|
|
|
|
|
|
|
|
case err := <-g.lndErrChan:
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Errorf("Received critical error from lnd, "+
|
|
|
|
|
"shutting down: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case <-signal.ShutdownChannel():
|
|
|
|
|
log.Infof("Shutdown signal received")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// startSubservers creates an internal connection to lnd and then starts all
|
|
|
|
|
// embedded daemons as external subservers that hook into the same gRPC and REST
|
|
|
|
|
// servers that lnd started.
|
2020-10-08 09:34:38 +02:00
|
|
|
func (g *LightningTerminal) startSubservers() error {
|
2020-05-26 16:30:26 +02:00
|
|
|
var basicClient lnrpc.LightningClient
|
2021-02-16 13:39:31 +01:00
|
|
|
host, network, tlsPath, macPath := g.cfg.lndConnectParams()
|
2020-10-08 09:34:38 +02:00
|
|
|
|
2020-05-26 16:30:26 +02:00
|
|
|
// The main RPC listener of lnd might need some time to start, it could
|
2020-09-01 09:31:12 +02:00
|
|
|
// 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. We'll check
|
|
|
|
|
// if lnd is also ready to be used in the next step.
|
2021-02-16 13:39:31 +01:00
|
|
|
err := wait.NoError(func() error {
|
2020-05-26 16:30:26 +02:00
|
|
|
// 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.
|
|
|
|
|
var err error
|
|
|
|
|
basicClient, err = lndclient.NewBasicClient(
|
2021-01-18 15:38:42 +01:00
|
|
|
host, tlsPath, path.Dir(macPath), string(network),
|
|
|
|
|
lndclient.MacFilename(path.Base(macPath)),
|
2020-05-26 16:30:26 +02:00
|
|
|
)
|
|
|
|
|
return err
|
|
|
|
|
}, defaultStartupTimeout)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-01 09:31:12 +02:00
|
|
|
// Now we know that the connection itself is ready. But we also 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. But we
|
|
|
|
|
// still want to react to shutdown requests, so we need to listen for
|
|
|
|
|
// those.
|
|
|
|
|
ctxc, cancel := context.WithCancel(context.Background())
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
// Make sure the context is canceled if the user requests shutdown.
|
|
|
|
|
go func() {
|
2020-05-26 16:30:26 +02:00
|
|
|
select {
|
2020-09-01 09:31:12 +02:00
|
|
|
// Client requests shutdown, cancel the wait.
|
|
|
|
|
case <-signal.ShutdownChannel():
|
|
|
|
|
cancel()
|
2020-05-26 16:30:26 +02:00
|
|
|
|
2020-09-01 09:31:12 +02:00
|
|
|
// The check was completed and the above defer canceled the
|
|
|
|
|
// context. We can just exit the goroutine, nothing more to do.
|
|
|
|
|
case <-ctxc.Done():
|
2020-05-26 16:30:26 +02:00
|
|
|
}
|
2020-09-01 09:31:12 +02:00
|
|
|
}()
|
|
|
|
|
g.lndClient, err = lndclient.NewLndServices(
|
|
|
|
|
&lndclient.LndServicesConfig{
|
2020-10-08 09:34:38 +02:00
|
|
|
LndAddress: host,
|
|
|
|
|
Network: network,
|
|
|
|
|
TLSPath: tlsPath,
|
2021-01-18 15:38:42 +01:00
|
|
|
CustomMacaroonPath: macPath,
|
2020-09-01 09:31:12 +02:00
|
|
|
BlockUntilChainSynced: true,
|
2021-01-18 15:38:40 +01:00
|
|
|
BlockUntilUnlocked: true,
|
2021-01-18 15:38:39 +01:00
|
|
|
CallerCtx: ctxc,
|
2020-09-01 09:31:12 +02:00
|
|
|
},
|
|
|
|
|
)
|
2020-05-26 16:30:26 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-16 13:39:39 +01:00
|
|
|
// Both connection types are ready now, let's start our subservers if
|
|
|
|
|
// they should be started locally as an integrated service.
|
|
|
|
|
if !g.cfg.faradayRemote {
|
|
|
|
|
err = g.faradayServer.StartAsSubserver(g.lndClient.LndServices)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
g.faradayStarted = true
|
2020-05-26 16:30:26 +02:00
|
|
|
}
|
|
|
|
|
|
2021-02-16 13:39:39 +01:00
|
|
|
if !g.cfg.loopRemote {
|
|
|
|
|
err = g.loopServer.StartAsSubserver(g.lndClient)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
g.loopStarted = true
|
2020-05-26 16:30:26 +02:00
|
|
|
}
|
|
|
|
|
|
2021-02-16 13:39:39 +01:00
|
|
|
if !g.cfg.poolRemote {
|
|
|
|
|
err = g.poolServer.StartAsSubserver(basicClient, g.lndClient)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
g.poolStarted = true
|
2020-07-21 10:47:53 +02:00
|
|
|
}
|
|
|
|
|
|
2020-05-26 16:30:26 +02:00
|
|
|
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.
|
2021-02-26 15:41:14 +01:00
|
|
|
func (g *LightningTerminal) RegisterGrpcSubserver(server *grpc.Server) error {
|
2021-02-16 13:39:39 +01:00
|
|
|
// 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 !g.cfg.faradayRemote {
|
2021-02-26 15:41:14 +01:00
|
|
|
frdrpc.RegisterFaradayServerServer(server, g.faradayServer)
|
2021-02-16 13:39:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !g.cfg.loopRemote {
|
2021-02-26 15:41:14 +01:00
|
|
|
looprpc.RegisterSwapClientServer(server, g.loopServer)
|
2021-02-16 13:39:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !g.cfg.poolRemote {
|
2021-02-26 15:41:14 +01:00
|
|
|
poolrpc.RegisterTraderServer(server, g.poolServer)
|
2021-02-16 13:39:39 +01:00
|
|
|
}
|
|
|
|
|
|
2020-05-26 16:30:26 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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.
|
2020-07-21 22:06:15 +02:00
|
|
|
func (g *LightningTerminal) RegisterRestSubserver(ctx context.Context,
|
2020-05-26 16:30:26 +02:00
|
|
|
mux *restProxy.ServeMux, endpoint string,
|
|
|
|
|
dialOpts []grpc.DialOption) error {
|
|
|
|
|
|
|
|
|
|
err := frdrpc.RegisterFaradayServerHandlerFromEndpoint(
|
|
|
|
|
ctx, mux, endpoint, dialOpts,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-21 10:47:53 +02:00
|
|
|
err = looprpc.RegisterSwapClientHandlerFromEndpoint(
|
|
|
|
|
ctx, mux, endpoint, dialOpts,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return poolrpc.RegisterTraderHandlerFromEndpoint(
|
2020-05-26 16:30:26 +02:00
|
|
|
ctx, mux, endpoint, dialOpts,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-08 09:34:42 +02:00
|
|
|
// 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.
|
|
|
|
|
func (g *LightningTerminal) ValidateMacaroon(ctx context.Context,
|
|
|
|
|
requiredPermissions []bakery.Op, fullMethod string) error {
|
|
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
switch {
|
2021-02-16 13:39:40 +01:00
|
|
|
case isFaradayURI(fullMethod):
|
2021-02-16 13:39:39 +01:00
|
|
|
// In remote mode we just pass through the request, the remote
|
|
|
|
|
// daemon will check the macaroon.
|
2021-02-16 13:39:40 +01:00
|
|
|
if g.cfg.faradayRemote {
|
2021-02-16 13:39:39 +01:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-16 13:39:40 +01:00
|
|
|
if !g.faradayStarted {
|
|
|
|
|
return fmt.Errorf("faraday is not yet ready for " +
|
2020-10-08 09:34:43 +02:00
|
|
|
"requests, lnd possibly still starting or " +
|
|
|
|
|
"syncing")
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-16 13:39:40 +01:00
|
|
|
return g.faradayServer.ValidateMacaroon(
|
2020-10-08 09:34:42 +02:00
|
|
|
ctx, requiredPermissions, fullMethod,
|
|
|
|
|
)
|
|
|
|
|
|
2021-02-16 13:39:40 +01:00
|
|
|
case isLoopURI(fullMethod):
|
2021-02-16 13:39:39 +01:00
|
|
|
// In remote mode we just pass through the request, the remote
|
|
|
|
|
// daemon will check the macaroon.
|
2021-02-16 13:39:40 +01:00
|
|
|
if g.cfg.loopRemote {
|
2021-02-16 13:39:39 +01:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-16 13:39:40 +01:00
|
|
|
if !g.loopStarted {
|
|
|
|
|
return fmt.Errorf("loop is not yet ready for " +
|
2020-10-08 09:34:43 +02:00
|
|
|
"requests, lnd possibly still starting or " +
|
|
|
|
|
"syncing")
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-16 13:39:40 +01:00
|
|
|
return g.loopServer.ValidateMacaroon(
|
2020-10-08 09:34:42 +02:00
|
|
|
ctx, requiredPermissions, fullMethod,
|
|
|
|
|
)
|
2020-07-21 10:47:53 +02:00
|
|
|
|
|
|
|
|
case isPoolURI(fullMethod):
|
2021-02-16 13:39:39 +01:00
|
|
|
// In remote mode we just pass through the request, the remote
|
|
|
|
|
// daemon will check the macaroon.
|
|
|
|
|
if g.cfg.poolRemote {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !g.poolStarted {
|
|
|
|
|
return fmt.Errorf("pool is not yet ready for " +
|
|
|
|
|
"requests, lnd possibly still starting or " +
|
|
|
|
|
"syncing")
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-21 10:47:53 +02:00
|
|
|
return g.poolServer.ValidateMacaroon(
|
|
|
|
|
ctx, requiredPermissions, fullMethod,
|
|
|
|
|
)
|
2020-10-08 09:34:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-26 16:30:26 +02:00
|
|
|
// shutdown stops all subservers that were started and attached to lnd.
|
2020-07-21 22:06:15 +02:00
|
|
|
func (g *LightningTerminal) shutdown() error {
|
2020-05-26 16:30:26 +02:00
|
|
|
var returnErr error
|
|
|
|
|
|
|
|
|
|
if g.faradayStarted {
|
2021-02-16 13:39:33 +01:00
|
|
|
if err := g.faradayServer.Stop(); err != nil {
|
2020-05-26 16:30:26 +02:00
|
|
|
log.Errorf("Error stopping faraday: %v", err)
|
|
|
|
|
returnErr = err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if g.loopStarted {
|
|
|
|
|
g.loopServer.Stop()
|
2021-02-16 13:39:33 +01:00
|
|
|
if err := <-g.loopServer.ErrChan; err != nil {
|
2020-05-26 16:30:26 +02:00
|
|
|
log.Errorf("Error stopping loop: %v", err)
|
|
|
|
|
returnErr = err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-16 13:39:33 +01:00
|
|
|
if g.poolStarted {
|
|
|
|
|
if err := g.poolServer.Stop(); err != nil {
|
|
|
|
|
log.Errorf("Error stopping pool: %v", err)
|
|
|
|
|
returnErr = err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-26 16:30:26 +02:00
|
|
|
if g.lndClient != nil {
|
|
|
|
|
g.lndClient.Close()
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-08 09:34:41 +02:00
|
|
|
if g.rpcProxy != nil {
|
|
|
|
|
if err := g.rpcProxy.Stop(); err != nil {
|
|
|
|
|
log.Errorf("Error stopping lnd proxy: %v", err)
|
|
|
|
|
returnErr = err
|
|
|
|
|
}
|
2020-10-08 09:34:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if g.httpServer != nil {
|
2020-10-08 09:34:41 +02:00
|
|
|
if err := g.httpServer.Close(); err != nil {
|
2020-10-08 09:34:43 +02:00
|
|
|
log.Errorf("Error stopping UI server: %v", err)
|
2020-05-26 16:30:26 +02:00
|
|
|
returnErr = err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// In case the error wasn't thrown by lnd, make sure we stop it too.
|
|
|
|
|
signal.RequestShutdown()
|
|
|
|
|
|
|
|
|
|
g.wg.Wait()
|
|
|
|
|
|
2020-10-08 09:34:43 +02:00
|
|
|
// The lnd error channel is only used if we are actually running lnd in
|
|
|
|
|
// the same process.
|
|
|
|
|
if g.cfg.LndMode == ModeIntegrated {
|
|
|
|
|
err := <-g.lndErrChan
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Errorf("Error stopping lnd: %v", err)
|
|
|
|
|
returnErr = err
|
|
|
|
|
}
|
2020-05-26 16:30:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return returnErr
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-08 09:34:41 +02:00
|
|
|
// startMainWebServer creates the main web HTTP server that delegates requests
|
|
|
|
|
// between the Statik HTTP server and the RPC proxy. An incoming request will
|
|
|
|
|
// go through the following chain of components:
|
|
|
|
|
//
|
|
|
|
|
// Request on port 8443
|
|
|
|
|
// |
|
|
|
|
|
// v
|
|
|
|
|
// +---+----------------------+ other +----------------+
|
|
|
|
|
// | Main web HTTP server +------->+ Statik HTTP |
|
|
|
|
|
// +---+----------------------+ +----------------+
|
|
|
|
|
// |
|
|
|
|
|
// v any RPC or REST call
|
|
|
|
|
// +---+----------------------+
|
|
|
|
|
// | grpc-web proxy |
|
|
|
|
|
// +---+----------------------+
|
|
|
|
|
// |
|
|
|
|
|
// v native gRPC call with basic auth
|
|
|
|
|
// +---+----------------------+
|
|
|
|
|
// | interceptors |
|
|
|
|
|
// +---+----------------------+
|
|
|
|
|
// |
|
|
|
|
|
// v native gRPC call with macaroon
|
2021-02-26 15:41:14 +01:00
|
|
|
// +---+----------------------+
|
|
|
|
|
// | 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 |
|
|
|
|
|
// +---------------------+
|
2020-10-08 09:34:41 +02:00
|
|
|
//
|
|
|
|
|
func (g *LightningTerminal) startMainWebServer() error {
|
2020-05-26 16:30:26 +02:00
|
|
|
// Initialize the in-memory file server from the content compiled by
|
|
|
|
|
// the statik library.
|
|
|
|
|
statikFS, err := fs.New()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("could not load statik file system: %v", err)
|
|
|
|
|
}
|
2020-06-22 12:41:48 -04:00
|
|
|
staticFileServer := http.FileServer(&ClientRouteWrapper{statikFS})
|
2020-05-26 16:30:26 +02:00
|
|
|
|
|
|
|
|
// 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) {
|
2020-10-08 09:34:41 +02:00
|
|
|
// 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) {
|
2020-05-26 16:30:26 +02:00
|
|
|
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)
|
2020-10-08 09:34:41 +02:00
|
|
|
|
|
|
|
|
// 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)/.*`)
|
2020-07-01 01:26:16 -04:00
|
|
|
if re.MatchString(req.URL.Path) {
|
|
|
|
|
resp.Header().Set("Cache-Control", "max-age=31536000")
|
|
|
|
|
}
|
2020-10-08 09:34:41 +02:00
|
|
|
|
|
|
|
|
// Transfer static files using gzip to save up to 70% of
|
|
|
|
|
// bandwidth.
|
2020-07-01 01:26:16 -04:00
|
|
|
gzipHandler := makeGzipHandler(staticFileServer.ServeHTTP)
|
|
|
|
|
gzipHandler(resp, req)
|
2020-05-26 16:30:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create and start our HTTPS server now that will handle both gRPC web
|
|
|
|
|
// and static file requests.
|
|
|
|
|
g.httpServer = &http.Server{
|
2020-11-16 21:09:25 +01:00
|
|
|
// 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),
|
2020-05-26 16:30:26 +02:00
|
|
|
}
|
|
|
|
|
httpListener, err := net.Listen("tcp", g.cfg.HTTPSListen)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("unable to listen on %v: %v",
|
|
|
|
|
g.cfg.HTTPSListen, err)
|
|
|
|
|
}
|
2020-05-29 16:03:22 +02:00
|
|
|
tlsConfig, err := buildTLSConfigForHttp2(g.cfg)
|
2020-05-26 16:30:26 +02:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
2020-12-17 18:19:29 +01:00
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-26 16:30:26 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-08 09:34:49 +02:00
|
|
|
// showStartupInfo shows useful information to the user to easily access the
|
|
|
|
|
// web UI that was just started.
|
|
|
|
|
func (g *LightningTerminal) showStartupInfo() 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(
|
2020-12-17 18:19:29 +01:00
|
|
|
g.cfg.HTTPSListen, "0.0.0.0", "localhost",
|
|
|
|
|
), "[::]", "localhost",
|
2020-10-08 09:34:49 +02:00
|
|
|
)),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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.
|
2021-02-16 13:39:31 +01:00
|
|
|
host, network, tlsPath, macPath := g.cfg.lndConnectParams()
|
2020-10-08 09:34:49 +02:00
|
|
|
basicClient, err := lndclient.NewBasicClient(
|
2021-01-18 15:38:42 +01:00
|
|
|
host, tlsPath, path.Dir(macPath), string(network),
|
|
|
|
|
lndclient.MacFilename(path.Base(macPath)),
|
2020-10-08 09:34:49 +02:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error querying remote node: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
res, err := basicClient.GetInfo(ctx, &lnrpc.GetInfoRequest{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
s, ok := status.FromError(err)
|
|
|
|
|
if !ok || s.Code() != codes.Unimplemented {
|
|
|
|
|
// Some other error that we didn't expect at
|
|
|
|
|
// this moment.
|
|
|
|
|
return fmt.Errorf("error querying remote "+
|
|
|
|
|
"node : %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Node is locked.
|
|
|
|
|
info.status = "locked"
|
|
|
|
|
info.alias = "???? (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"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-17 18:19:29 +01:00
|
|
|
// If there's an additional HTTP listener, list it as well.
|
|
|
|
|
if g.cfg.HTTPListen != "" {
|
|
|
|
|
host := strings.ReplaceAll(
|
|
|
|
|
strings.ReplaceAll(
|
|
|
|
|
g.cfg.HTTPListen, "0.0.0.0", "localhost",
|
|
|
|
|
), "[::]", "localhost",
|
|
|
|
|
)
|
|
|
|
|
info.webURI = fmt.Sprintf("%s, http://%s", info.webURI, host)
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-08 09:34:49 +02:00
|
|
|
str := "" +
|
|
|
|
|
"----------------------------------------------------------\n" +
|
|
|
|
|
" Lightning Terminal (LiT) by Lightning Labs \n" +
|
|
|
|
|
" \n" +
|
|
|
|
|
" Operating mode %s \n" +
|
|
|
|
|
" Node status %s \n" +
|
|
|
|
|
" Alias %s \n" +
|
|
|
|
|
" Version %s \n" +
|
|
|
|
|
" Web interface %s \n" +
|
|
|
|
|
"----------------------------------------------------------\n"
|
|
|
|
|
fmt.Printf(str, info.mode, info.status, info.alias, info.version,
|
|
|
|
|
info.webURI)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-22 12:41:48 -04:00
|
|
|
// 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) {
|
|
|
|
|
ret, err := i.assets.Open(name)
|
|
|
|
|
if !os.IsNotExist(err) || filepath.Ext(name) != "" {
|
|
|
|
|
return ret, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return i.assets.Open("/index.html")
|
|
|
|
|
}
|