2021-08-10 19:07:01 +02:00
|
|
|
package terminal
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
2022-01-07 13:50:27 +02:00
|
|
|
"sync"
|
2021-08-10 19:07:01 +02:00
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/btcsuite/btcd/btcec"
|
|
|
|
|
"github.com/lightninglabs/lightning-node-connect/mailbox"
|
|
|
|
|
"github.com/lightninglabs/lightning-terminal/litrpc"
|
|
|
|
|
"github.com/lightninglabs/lightning-terminal/session"
|
2022-03-30 14:14:47 +02:00
|
|
|
"google.golang.org/grpc"
|
2022-06-13 12:52:09 +02:00
|
|
|
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
|
|
|
|
|
"gopkg.in/macaroon.v2"
|
2021-08-10 19:07:01 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// sessionRpcServer is the gRPC server for the Session RPC interface.
|
|
|
|
|
type sessionRpcServer struct {
|
|
|
|
|
litrpc.UnimplementedSessionsServer
|
|
|
|
|
|
2022-03-30 14:14:47 +02:00
|
|
|
cfg *sessionRpcServerConfig
|
2021-08-10 19:07:01 +02:00
|
|
|
db *session.DB
|
|
|
|
|
sessionServer *session.Server
|
2022-01-07 13:50:27 +02:00
|
|
|
|
2022-01-17 17:25:08 +02:00
|
|
|
quit chan struct{}
|
|
|
|
|
wg sync.WaitGroup
|
2022-01-07 13:50:27 +02:00
|
|
|
stopOnce sync.Once
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-30 14:14:47 +02:00
|
|
|
// sessionRpcServerConfig holds the values used to configure the
|
|
|
|
|
// sessionRpcServer.
|
|
|
|
|
type sessionRpcServerConfig struct {
|
|
|
|
|
basicAuth string
|
|
|
|
|
dbDir string
|
|
|
|
|
grpcOptions []grpc.ServerOption
|
|
|
|
|
registerGrpcServers func(server *grpc.Server)
|
|
|
|
|
superMacBaker func(ctx context.Context, rootKeyID uint64,
|
|
|
|
|
recipe *session.MacaroonRecipe) (string, error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// newSessionRPCServer creates a new sessionRpcServer using the passed config.
|
|
|
|
|
func newSessionRPCServer(cfg *sessionRpcServerConfig) (*sessionRpcServer,
|
|
|
|
|
error) {
|
|
|
|
|
|
|
|
|
|
// Create an instance of the local Terminal Connect session store DB.
|
|
|
|
|
db, err := session.NewDB(cfg.dbDir, session.DBFilename)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("error creating session DB: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create the gRPC server that handles adding/removing sessions and the
|
|
|
|
|
// actual mailbox server that spins up the Terminal Connect server
|
|
|
|
|
// interface.
|
|
|
|
|
server := session.NewServer(
|
|
|
|
|
func(opts ...grpc.ServerOption) *grpc.Server {
|
|
|
|
|
allOpts := append(cfg.grpcOptions, opts...)
|
|
|
|
|
grpcServer := grpc.NewServer(allOpts...)
|
|
|
|
|
|
|
|
|
|
cfg.registerGrpcServers(grpcServer)
|
|
|
|
|
|
|
|
|
|
return grpcServer
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return &sessionRpcServer{
|
|
|
|
|
cfg: cfg,
|
|
|
|
|
db: db,
|
|
|
|
|
sessionServer: server,
|
|
|
|
|
quit: make(chan struct{}),
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// start all the components necessary for the sessionRpcServer to start serving
|
|
|
|
|
// requests. This includes starting the macaroon service and resuming all
|
|
|
|
|
// non-revoked sessions.
|
|
|
|
|
func (s *sessionRpcServer) start() error {
|
|
|
|
|
// Start up all previously created sessions.
|
|
|
|
|
sessions, err := s.db.ListSessions()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error listing sessions: %v", err)
|
|
|
|
|
}
|
|
|
|
|
for _, sess := range sessions {
|
|
|
|
|
if err := s.resumeSession(sess); err != nil {
|
|
|
|
|
return fmt.Errorf("error resuming sesion: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-07 13:50:27 +02:00
|
|
|
// stop cleans up any sessionRpcServer resources.
|
2022-03-30 14:14:47 +02:00
|
|
|
func (s *sessionRpcServer) stop() error {
|
|
|
|
|
var returnErr error
|
2022-01-07 13:50:27 +02:00
|
|
|
s.stopOnce.Do(func() {
|
2022-03-30 14:14:47 +02:00
|
|
|
if err := s.db.Close(); err != nil {
|
|
|
|
|
log.Errorf("Error closing session DB: %v", err)
|
|
|
|
|
returnErr = err
|
|
|
|
|
}
|
|
|
|
|
s.sessionServer.Stop()
|
|
|
|
|
|
2022-01-07 13:50:27 +02:00
|
|
|
close(s.quit)
|
2022-01-17 17:25:08 +02:00
|
|
|
s.wg.Wait()
|
2022-01-07 13:50:27 +02:00
|
|
|
})
|
2022-03-30 14:14:47 +02:00
|
|
|
|
|
|
|
|
return returnErr
|
2021-08-10 19:07:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AddSession adds and starts a new Terminal Connect session.
|
|
|
|
|
func (s *sessionRpcServer) AddSession(_ context.Context,
|
|
|
|
|
req *litrpc.AddSessionRequest) (*litrpc.AddSessionResponse, error) {
|
|
|
|
|
|
2022-02-02 13:28:31 +02:00
|
|
|
expiry := time.Unix(int64(req.ExpiryTimestampSeconds), 0)
|
|
|
|
|
if time.Now().After(expiry) {
|
|
|
|
|
return nil, fmt.Errorf("expiry must be in the future")
|
|
|
|
|
}
|
2021-08-10 19:07:01 +02:00
|
|
|
|
2022-02-02 13:28:31 +02:00
|
|
|
typ, err := unmarshalRPCType(req.SessionType)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2021-08-10 19:07:01 +02:00
|
|
|
}
|
|
|
|
|
|
2022-05-16 14:37:33 +02:00
|
|
|
if typ != session.TypeMacaroonAdmin &&
|
2022-02-02 13:39:34 +02:00
|
|
|
typ != session.TypeMacaroonReadonly {
|
|
|
|
|
|
2022-05-16 14:37:33 +02:00
|
|
|
return nil, fmt.Errorf("invalid session type, only admin " +
|
|
|
|
|
"and readonly macaroon types supported in LiT")
|
2021-08-10 19:07:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sess, err := session.NewSession(
|
|
|
|
|
req.Label, typ, expiry, req.MailboxServerAddr, req.DevServer,
|
2022-02-02 13:13:24 +02:00
|
|
|
nil, nil,
|
2021-08-10 19:07:01 +02:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("error creating new session: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := s.db.StoreSession(sess); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("error storing session: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := s.resumeSession(sess); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("error starting session: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rpcSession, err := marshalRPCSession(sess)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("error marshaling session: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &litrpc.AddSessionResponse{
|
|
|
|
|
Session: rpcSession,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// resumeSession tries to start an existing session if it is not expired, not
|
|
|
|
|
// revoked and a LiT session.
|
|
|
|
|
func (s *sessionRpcServer) resumeSession(sess *session.Session) error {
|
2022-01-17 17:25:08 +02:00
|
|
|
pubKey := sess.LocalPublicKey
|
|
|
|
|
pubKeyBytes := pubKey.SerializeCompressed()
|
|
|
|
|
|
2021-08-10 19:07:01 +02:00
|
|
|
// We only start non-revoked, non-expired LiT sessions. Everything else
|
|
|
|
|
// we just skip.
|
|
|
|
|
if sess.State != session.StateInUse &&
|
|
|
|
|
sess.State != session.StateCreated {
|
|
|
|
|
|
2022-02-02 13:28:31 +02:00
|
|
|
log.Debugf("Not resuming session %x with state %d", pubKeyBytes,
|
|
|
|
|
sess.State)
|
2021-08-10 19:07:01 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
2022-02-02 13:28:31 +02:00
|
|
|
|
|
|
|
|
// Don't resume an expired session.
|
2021-08-10 19:07:01 +02:00
|
|
|
if sess.Expiry.Before(time.Now()) {
|
|
|
|
|
log.Debugf("Not resuming session %x with expiry %s",
|
2022-01-17 17:25:08 +02:00
|
|
|
pubKeyBytes, sess.Expiry)
|
|
|
|
|
|
|
|
|
|
if err := s.db.RevokeSession(pubKey); err != nil {
|
|
|
|
|
return fmt.Errorf("error revoking session: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-10 19:07:01 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-16 14:37:33 +02:00
|
|
|
if sess.Type != session.TypeMacaroonAdmin &&
|
|
|
|
|
sess.Type != session.TypeMacaroonReadonly {
|
2022-02-02 13:28:31 +02:00
|
|
|
|
|
|
|
|
log.Debugf("Not resuming session %x with type %d", pubKeyBytes,
|
|
|
|
|
sess.Type)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-13 12:52:09 +02:00
|
|
|
var (
|
|
|
|
|
caveats []macaroon.Caveat
|
|
|
|
|
readOnly = sess.Type == session.TypeMacaroonReadonly
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Add the session expiry as a macaroon caveat.
|
|
|
|
|
macExpiry := checkers.TimeBeforeCaveat(sess.Expiry)
|
|
|
|
|
caveats = append(caveats, macaroon.Caveat{
|
|
|
|
|
Id: []byte(macExpiry.Condition),
|
|
|
|
|
})
|
|
|
|
|
|
2022-05-16 14:37:33 +02:00
|
|
|
mac, err := s.cfg.superMacBaker(
|
|
|
|
|
context.Background(), sess.MacaroonRootKey,
|
|
|
|
|
&session.MacaroonRecipe{
|
|
|
|
|
Permissions: GetAllPermissions(readOnly),
|
2022-06-13 12:52:09 +02:00
|
|
|
Caveats: caveats,
|
2022-05-16 14:37:33 +02:00
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Debugf("Not resuming session %x. Could not bake "+
|
|
|
|
|
"the necessary macaroon: %w", pubKeyBytes, err)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
authData := []byte(fmt.Sprintf("%s: %s", HeaderMacaroon, mac))
|
|
|
|
|
|
2022-04-29 14:15:35 +02:00
|
|
|
sessionClosedSub, err := s.sessionServer.StartSession(
|
|
|
|
|
sess, authData, s.db.StoreSession,
|
|
|
|
|
)
|
2022-01-17 17:25:08 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s.wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer s.wg.Done()
|
|
|
|
|
|
|
|
|
|
ticker := time.NewTimer(time.Until(sess.Expiry))
|
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case <-s.quit:
|
|
|
|
|
case <-sessionClosedSub:
|
|
|
|
|
case <-ticker.C:
|
|
|
|
|
log.Debugf("Stopping expired session %x with "+
|
|
|
|
|
"type %d", pubKeyBytes, sess.Type)
|
|
|
|
|
|
|
|
|
|
err = s.sessionServer.StopSession(pubKey)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Debugf("Error stopping session: "+
|
|
|
|
|
"%v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = s.db.RevokeSession(pubKey)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Debugf("error revoking session: "+
|
|
|
|
|
"%v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
2022-02-02 13:28:31 +02:00
|
|
|
return nil
|
2021-08-10 19:07:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ListSessions returns all sessions known to the session store.
|
|
|
|
|
func (s *sessionRpcServer) ListSessions(_ context.Context,
|
|
|
|
|
_ *litrpc.ListSessionsRequest) (*litrpc.ListSessionsResponse, error) {
|
|
|
|
|
|
|
|
|
|
sessions, err := s.db.ListSessions()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("error fetching sessions: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response := &litrpc.ListSessionsResponse{
|
|
|
|
|
Sessions: make([]*litrpc.Session, len(sessions)),
|
|
|
|
|
}
|
|
|
|
|
for idx, sess := range sessions {
|
|
|
|
|
response.Sessions[idx], err = marshalRPCSession(sess)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("error marshaling session: %v",
|
|
|
|
|
err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return response, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RevokeSession revokes a single session and also stops it if it is currently
|
|
|
|
|
// active.
|
|
|
|
|
func (s *sessionRpcServer) RevokeSession(_ context.Context,
|
|
|
|
|
req *litrpc.RevokeSessionRequest) (*litrpc.RevokeSessionResponse, error) {
|
|
|
|
|
|
|
|
|
|
pubKey, err := btcec.ParsePubKey(req.LocalPublicKey, btcec.S256())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("error parsing public key: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := s.db.RevokeSession(pubKey); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("error revoking session: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the session expired already it might not be running anymore. So we
|
|
|
|
|
// only log possible errors here.
|
|
|
|
|
if err := s.sessionServer.StopSession(pubKey); err != nil {
|
|
|
|
|
log.Debugf("Error stopping session: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &litrpc.RevokeSessionResponse{}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// marshalRPCSession converts a session into its RPC counterpart.
|
|
|
|
|
func marshalRPCSession(sess *session.Session) (*litrpc.Session, error) {
|
|
|
|
|
rpcState, err := marshalRPCState(sess.State)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rpcType, err := marshalRPCType(sess.Type)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var remotePubKey []byte
|
|
|
|
|
if sess.RemotePublicKey != nil {
|
|
|
|
|
remotePubKey = sess.RemotePublicKey.SerializeCompressed()
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-29 14:15:35 +02:00
|
|
|
mnemonic, err := mailbox.PassphraseEntropyToMnemonic(sess.PairingSecret)
|
2021-08-10 19:07:01 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &litrpc.Session{
|
|
|
|
|
Label: sess.Label,
|
|
|
|
|
SessionState: rpcState,
|
|
|
|
|
SessionType: rpcType,
|
|
|
|
|
ExpiryTimestampSeconds: uint64(sess.Expiry.Unix()),
|
|
|
|
|
MailboxServerAddr: sess.ServerAddr,
|
|
|
|
|
DevServer: sess.DevServer,
|
|
|
|
|
PairingSecret: sess.PairingSecret[:],
|
|
|
|
|
PairingSecretMnemonic: strings.Join(mnemonic[:], " "),
|
|
|
|
|
LocalPublicKey: sess.LocalPublicKey.SerializeCompressed(),
|
|
|
|
|
RemotePublicKey: remotePubKey,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// marshalRPCState converts a session state to its RPC counterpart.
|
|
|
|
|
func marshalRPCState(state session.State) (litrpc.SessionState, error) {
|
|
|
|
|
switch state {
|
|
|
|
|
case session.StateCreated:
|
|
|
|
|
return litrpc.SessionState_STATE_CREATED, nil
|
|
|
|
|
|
|
|
|
|
case session.StateInUse:
|
|
|
|
|
return litrpc.SessionState_STATE_IN_USE, nil
|
|
|
|
|
|
|
|
|
|
case session.StateRevoked:
|
|
|
|
|
return litrpc.SessionState_STATE_REVOKED, nil
|
|
|
|
|
|
|
|
|
|
case session.StateExpired:
|
|
|
|
|
return litrpc.SessionState_STATE_EXPIRED, nil
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return 0, fmt.Errorf("unknown state <%d>", state)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// marshalRPCType converts a session type to its RPC counterpart.
|
|
|
|
|
func marshalRPCType(typ session.Type) (litrpc.SessionType, error) {
|
|
|
|
|
switch typ {
|
|
|
|
|
case session.TypeMacaroonReadonly:
|
|
|
|
|
return litrpc.SessionType_TYPE_MACAROON_READONLY, nil
|
|
|
|
|
|
|
|
|
|
case session.TypeMacaroonAdmin:
|
|
|
|
|
return litrpc.SessionType_TYPE_MACAROON_ADMIN, nil
|
|
|
|
|
|
|
|
|
|
case session.TypeMacaroonCustom:
|
|
|
|
|
return litrpc.SessionType_TYPE_MACAROON_CUSTOM, nil
|
|
|
|
|
|
|
|
|
|
case session.TypeUIPassword:
|
|
|
|
|
return litrpc.SessionType_TYPE_UI_PASSWORD, nil
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return 0, fmt.Errorf("unknown type <%d>", typ)
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-02-02 13:28:31 +02:00
|
|
|
|
|
|
|
|
// unmarshalRPCType converts an RPC session type to its session counterpart.
|
|
|
|
|
func unmarshalRPCType(typ litrpc.SessionType) (session.Type, error) {
|
|
|
|
|
switch typ {
|
|
|
|
|
case litrpc.SessionType_TYPE_MACAROON_READONLY:
|
|
|
|
|
return session.TypeMacaroonReadonly, nil
|
|
|
|
|
|
|
|
|
|
case litrpc.SessionType_TYPE_MACAROON_ADMIN:
|
|
|
|
|
return session.TypeMacaroonAdmin, nil
|
|
|
|
|
|
|
|
|
|
case litrpc.SessionType_TYPE_MACAROON_CUSTOM:
|
|
|
|
|
return session.TypeMacaroonCustom, nil
|
|
|
|
|
|
|
|
|
|
case litrpc.SessionType_TYPE_UI_PASSWORD:
|
|
|
|
|
return session.TypeUIPassword, nil
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return 0, fmt.Errorf("unknown type <%d>", typ)
|
|
|
|
|
}
|
|
|
|
|
}
|