Files
ark/server/internal/interface/grpc/service.go
Pietralberto Mazza 03670c9c4b Move btc wallet syncing in background (#352)
* Move btc wallet syncing in bg

* Fix

* Fix e2e test

* Use neutrino in regtest for e2e

* Revert

* Fix
2024-10-17 16:09:19 +02:00

383 lines
9.3 KiB
Go

package grpcservice
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"path/filepath"
"strings"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
appconfig "github.com/ark-network/ark/server/internal/app-config"
"github.com/ark-network/ark/server/internal/core/application"
interfaces "github.com/ark-network/ark/server/internal/interface"
"github.com/ark-network/ark/server/internal/interface/grpc/handlers"
"github.com/ark-network/ark/server/internal/interface/grpc/interceptors"
"github.com/ark-network/ark/server/pkg/kvdb"
"github.com/ark-network/ark/server/pkg/macaroons"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
log "github.com/sirupsen/logrus"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
grpchealth "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/protobuf/encoding/protojson"
)
const (
macaroonsLocation = "ark"
macaroonsDbFile = "macaroons.db"
macaroonsFolder = "macaroons"
tlsKeyFile = "key.pem"
tlsCertFile = "cert.pem"
tlsFolder = "tls"
)
type service struct {
config Config
appConfig *appconfig.Config
server *http.Server
grpcServer *grpc.Server
macaroonSvc *macaroons.Service
}
func NewService(
svcConfig Config, appConfig *appconfig.Config,
) (interfaces.Service, error) {
if err := svcConfig.Validate(); err != nil {
return nil, fmt.Errorf("invalid service config: %s", err)
}
if err := appConfig.Validate(); err != nil {
return nil, fmt.Errorf("invalid app config: %s", err)
}
var macaroonSvc *macaroons.Service
if !svcConfig.NoMacaroons {
macaroonDB, err := kvdb.Create(
kvdb.BoltBackendName,
filepath.Join(svcConfig.Datadir, macaroonsDbFile),
true,
kvdb.DefaultDBTimeout,
)
if err != nil {
return nil, err
}
keyStore, err := macaroons.NewRootKeyStorage(macaroonDB)
if err != nil {
return nil, err
}
svc, err := macaroons.NewService(
keyStore, macaroonsLocation, false, macaroons.IPLockChecker,
)
if err != nil {
return nil, err
}
macaroonSvc = svc
}
if !svcConfig.insecure() {
if err := generateOperatorTLSKeyCert(
svcConfig.tlsDatadir(), svcConfig.TLSExtraIPs, svcConfig.TLSExtraDomains,
); err != nil {
return nil, err
}
log.Debugf("generated TLS key pair at path: %s", svcConfig.tlsDatadir())
}
return &service{svcConfig, appConfig, nil, nil, macaroonSvc}, nil
}
func (s *service) Start() error {
withoutAppSvc := false
if err := s.start(withoutAppSvc); err != nil {
return err
}
if s.appConfig.UnlockerService() != nil {
return s.autoUnlock()
}
return nil
}
func (s *service) Stop() {
withAppSvc := true
s.stop(withAppSvc)
}
func (s *service) start(withAppSvc bool) error {
tlsConfig, err := s.config.tlsConfig()
if err != nil {
return err
}
if err := s.newServer(tlsConfig, withAppSvc); err != nil {
return err
}
if withAppSvc {
appSvc, _ := s.appConfig.AppService()
if err := appSvc.Start(); err != nil {
return fmt.Errorf("failed to start app service: %s", err)
}
log.Info("started app service")
}
if s.config.insecure() {
// nolint:all
go s.server.ListenAndServe()
} else {
// nolint:all
go s.server.ListenAndServeTLS("", "")
}
log.Infof("started listening at %s", s.config.address())
return nil
}
func (s *service) stop(withAppSvc bool) {
//nolint:all
s.server.Shutdown(context.Background())
log.Info("stopped grpc server")
if withAppSvc {
appSvc, _ := s.appConfig.AppService()
if appSvc != nil {
appSvc.Stop()
log.Info("stopped app service")
}
}
}
func (s *service) newServer(tlsConfig *tls.Config, withAppSvc bool) error {
grpcConfig := []grpc.ServerOption{
interceptors.UnaryInterceptor(s.macaroonSvc),
interceptors.StreamInterceptor(s.macaroonSvc),
}
creds := insecure.NewCredentials()
if !s.config.insecure() {
creds = credentials.NewTLS(tlsConfig)
}
grpcConfig = append(grpcConfig, grpc.Creds(creds))
// Server grpc.
grpcServer := grpc.NewServer(grpcConfig...)
var appSvc application.Service
if withAppSvc {
svc, err := s.appConfig.AppService()
if err != nil {
return err
}
appSvc = svc
appHandler := handlers.NewHandler(appSvc)
arkv1.RegisterArkServiceServer(grpcServer, appHandler)
}
adminHandler := handlers.NewAdminHandler(s.appConfig.AdminService(), appSvc)
arkv1.RegisterAdminServiceServer(grpcServer, adminHandler)
walletHandler := handlers.NewWalletHandler(s.appConfig.WalletService())
arkv1.RegisterWalletServiceServer(grpcServer, walletHandler)
walletInitHandler := handlers.NewWalletInitializerHandler(
s.appConfig.WalletService(), s.onInit, s.onUnlock, s.onReady,
)
arkv1.RegisterWalletInitializerServiceServer(grpcServer, walletInitHandler)
healthHandler := handlers.NewHealthHandler()
grpchealth.RegisterHealthServer(grpcServer, healthHandler)
// Creds for grpc gateway reverse proxy.
gatewayCreds := insecure.NewCredentials()
if !s.config.insecure() {
gatewayCreds = credentials.NewTLS(&tls.Config{
InsecureSkipVerify: true, // #nosec
})
}
gatewayOpts := grpc.WithTransportCredentials(gatewayCreds)
conn, err := grpc.NewClient(
s.config.gatewayAddress(), gatewayOpts,
)
if err != nil {
return err
}
customMatcher := func(key string) (string, bool) {
switch key {
case "X-Macaroon":
return "macaroon", true
default:
return key, false
}
}
// Reverse proxy grpc-gateway.
gwmux := runtime.NewServeMux(
runtime.WithIncomingHeaderMatcher(customMatcher),
runtime.WithHealthzEndpoint(grpchealth.NewHealthClient(conn)),
runtime.WithMarshalerOption("application/json+pretty", &runtime.JSONPb{
MarshalOptions: protojson.MarshalOptions{
Indent: " ",
Multiline: true,
},
UnmarshalOptions: protojson.UnmarshalOptions{
DiscardUnknown: true,
},
}),
)
ctx := context.Background()
if err := arkv1.RegisterAdminServiceHandler(
ctx, gwmux, conn,
); err != nil {
return err
}
if err := arkv1.RegisterWalletServiceHandler(
ctx, gwmux, conn,
); err != nil {
return err
}
if err := arkv1.RegisterWalletInitializerServiceHandler(
ctx, gwmux, conn,
); err != nil {
return err
}
if withAppSvc {
if err := arkv1.RegisterArkServiceHandler(
ctx, gwmux, conn,
); err != nil {
return err
}
}
grpcGateway := http.Handler(gwmux)
handler := router(grpcServer, grpcGateway)
mux := http.NewServeMux()
mux.Handle("/", handler)
httpServerHandler := http.Handler(mux)
if s.config.insecure() {
httpServerHandler = h2c.NewHandler(httpServerHandler, &http2.Server{})
}
s.server = &http.Server{
Addr: s.config.address(),
Handler: httpServerHandler,
TLSConfig: tlsConfig,
}
return nil
}
func (s *service) onUnlock(password string) {
if s.config.NoMacaroons {
return
}
pwd := []byte(password)
datadir := s.config.macaroonsDatadir()
if err := s.macaroonSvc.CreateUnlock(&pwd); err != nil {
if err != macaroons.ErrAlreadyUnlocked {
log.WithError(err).Warn("failed to unlock macaroon store")
}
}
done, err := genMacaroons(
context.Background(), s.macaroonSvc, datadir,
)
if err != nil {
log.WithError(err).Warn("failed to create macaroons")
}
if done {
log.Debugf("created and stored macaroons at path %s", datadir)
}
}
func (s *service) onInit(password string) {
if s.config.NoMacaroons {
return
}
pwd := []byte(password)
datadir := s.config.macaroonsDatadir()
if err := s.macaroonSvc.CreateUnlock(&pwd); err != nil {
log.WithError(err).Warn("failed to initialize macaroon store")
}
if _, err := genMacaroons(
context.Background(), s.macaroonSvc, datadir,
); err != nil {
log.WithError(err).Warn("failed to create macaroons")
}
log.Debugf("generated macaroons at path %s", datadir)
}
func (s *service) onReady() {
withoutAppSvc := false
s.stop(withoutAppSvc)
withAppSvc := true
if err := s.start(withAppSvc); err != nil {
panic(err)
}
}
func (s *service) autoUnlock() error {
ctx := context.Background()
wallet := s.appConfig.WalletService()
status, err := wallet.Status(ctx)
if err != nil {
return fmt.Errorf("failed to get wallet status: %s", err)
}
if !status.IsInitialized() {
log.Debug("wallet not initiialized, skipping auto unlock")
return nil
}
password, err := s.appConfig.UnlockerService().GetPassword(ctx)
if err != nil {
return fmt.Errorf("failed to get password: %s", err)
}
if err := wallet.Unlock(ctx, password); err != nil {
return fmt.Errorf("failed to auto unlock: %s", err)
}
go s.onUnlock(password)
log.Debug("service auto unlocked")
return nil
}
func router(
grpcServer *grpc.Server, grpcGateway http.Handler,
) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if isOptionRequest(r) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "*")
w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
return
}
if isHttpRequest(r) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "*")
w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
grpcGateway.ServeHTTP(w, r)
return
}
grpcServer.ServeHTTP(w, r)
})
}
func isOptionRequest(req *http.Request) bool {
return req.Method == http.MethodOptions
}
func isHttpRequest(req *http.Request) bool {
return req.Method == http.MethodGet ||
strings.Contains(req.Header.Get("Content-Type"), "application/json")
}