package aperture import ( "context" "crypto/tls" "database/sql" "errors" "fmt" "io" "net/http" _ "net/http/pprof" // Blank import to set up profiling HTTP handlers. "os" "path/filepath" "regexp" "strings" "sync" "time" "github.com/goccy/go-yaml" grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" gateway "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" flags "github.com/jessevdk/go-flags" "github.com/lightninglabs/aperture/aperturedb" "github.com/lightninglabs/aperture/auth" "github.com/lightninglabs/aperture/challenger" "github.com/lightninglabs/aperture/lnc" "github.com/lightninglabs/aperture/mint" "github.com/lightninglabs/aperture/proxy" "github.com/lightninglabs/lightning-node-connect/hashmailrpc" "github.com/lightninglabs/lndclient" "github.com/lightningnetwork/lnd" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/cert" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/signal" "github.com/lightningnetwork/lnd/tor" clientv3 "go.etcd.io/etcd/client/v3" "golang.org/x/crypto/acme/autocert" "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" "google.golang.org/grpc/keepalive" "google.golang.org/protobuf/encoding/protojson" ) const ( // topLevelKey is the top level key for an etcd cluster where we'll // store all L402 proxy related data. topLevelKey = "lsat/proxy" // etcdKeyDelimeter is the delimeter we'll use for all etcd keys to // represent a path-like structure. etcdKeyDelimeter = "/" // selfSignedCertOrganization is the static string that we encode in the // organization field of a certificate if we create it ourselves. selfSignedCertOrganization = "aperture autogenerated cert" // selfSignedCertValidity is the certificate validity duration we are // using for aperture certificates. This is higher than lnd's default // 14 months and is set to a maximum just below what some operating // systems set as a sane maximum certificate duration. See // https://support.apple.com/en-us/HT210176 for more information. selfSignedCertValidity = time.Hour * 24 * 820 // selfSignedCertExpiryMargin is how much time before the certificate's // expiry date we already refresh it with a new one. We set this to half // the certificate validity length to make the chances bigger for it to // be refreshed on a routine server restart. selfSignedCertExpiryMargin = selfSignedCertValidity / 2 // hashMailGRPCPrefix is the prefix a gRPC request URI has when it is // meant for the hashmailrpc server to be handled. hashMailGRPCPrefix = "/hashmailrpc.HashMail/" // hashMailRESTPrefix is the prefix a REST request URI has when it is // meant for the hashmailrpc server to be handled. hashMailRESTPrefix = "/v1/lightning-node-connect/hashmail" // invoiceMacaroonName is the name of the invoice macaroon belonging // to the target lnd node. invoiceMacaroonName = "invoice.macaroon" // defaultMailboxAddress is the default address of the mailbox server // that will be used if none is specified. defaultMailboxAddress = "mailbox.terminal.lightning.today:443" ) var ( // http2TLSCipherSuites is the list of cipher suites we allow the server // to use. This list removes a CBC cipher from the list used in lnd's // cert package because the underlying HTTP/2 library treats it as a bad // cipher, according to https://tools.ietf.org/html/rfc7540#appendix-A // (also see golang.org/x/net/http2/ciphers.go). http2TLSCipherSuites = []uint16{ tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, } // clientStreamingURIs is the list of REST URIs that are // client-streaming and shouldn't be closed after a single message. clientStreamingURIs = []*regexp.Regexp{ regexp.MustCompile("^/v1/lightning-node-connect/hashmail/send$"), } ) // Main is the true entrypoint of Aperture. func Main() { // TODO: Prevent from running twice. err := run() // Unwrap our error and check whether help was requested from our flag // library. If the error is not wrapped, Unwrap returns nil. It is // still safe to check the type of this nil error. flagErr, isFlagErr := errors.Unwrap(err).(*flags.Error) isHelpErr := isFlagErr && flagErr.Type == flags.ErrHelp // If we got a nil error, or help was requested, just exit. if err == nil || isHelpErr { os.Exit(0) } // Print any other non-help related errors. _, _ = fmt.Fprintln(os.Stderr, err) os.Exit(1) } // run sets up the proxy server and runs it. This function blocks until a // shutdown signal is received. func run() error { // Before starting everything, make sure we can intercept any interrupt // signals so we can block on waiting for them later. interceptor, err := signal.Intercept() if err != nil { return err } // Next, parse configuration file and set up logging. cfg, err := getConfig() if err != nil { return fmt.Errorf("unable to parse config file: %w", err) } err = setupLogging(cfg, interceptor) if err != nil { return fmt.Errorf("unable to set up logging: %v", err) } errChan := make(chan error) a := NewAperture(cfg) if err := a.Start(errChan, interceptor.ShutdownChannel()); err != nil { return fmt.Errorf("unable to start aperture: %v", err) } select { case <-interceptor.ShutdownChannel(): log.Infof("Received interrupt signal, shutting down aperture.") case err := <-errChan: log.Errorf("Error while running aperture: %v", err) } return a.Stop() } // Aperture is the main type of the aperture service. It holds all components // that are required for the authenticating reverse proxy to do its job. type Aperture struct { cfg *Config etcdClient *clientv3.Client db *sql.DB challenger challenger.Challenger httpsServer *http.Server torHTTPServer *http.Server proxy *proxy.Proxy proxyCleanup func() wg sync.WaitGroup quit chan struct{} } // NewAperture creates a new instance of the Aperture service. func NewAperture(cfg *Config) *Aperture { return &Aperture{ cfg: cfg, quit: make(chan struct{}), } } // Start sets up the proxy server and starts it. func (a *Aperture) Start(errChan chan error, shutdown <-chan struct{}) error { // Start the prometheus exporter. err := StartPrometheusExporter(a.cfg.Prometheus, shutdown) if err != nil { return fmt.Errorf("unable to start the prometheus "+ "exporter: %v", err) } // Enable http profiling and validate profile port number if requested. if a.cfg.Profile != 0 { if a.cfg.Profile < 1024 { return fmt.Errorf("the profile port must be between " + "1024 and 65535") } go func() { http.Handle("/", http.RedirectHandler( "/debug/pprof", http.StatusSeeOther, )) listenAddr := fmt.Sprintf("localhost:%d", a.cfg.Profile) log.Infof("Starting profile server at %s", listenAddr) fmt.Println(http.ListenAndServe(listenAddr, nil)) }() } var ( secretStore mint.SecretStore onionStore tor.OnionStore lncStore lnc.Store ) // Connect to the chosen database backend. switch a.cfg.DatabaseBackend { case "etcd": // Initialize our etcd client. a.etcdClient, err = clientv3.New(clientv3.Config{ Endpoints: []string{a.cfg.Etcd.Host}, DialTimeout: 5 * time.Second, Username: a.cfg.Etcd.User, Password: a.cfg.Etcd.Password, }) if err != nil { return fmt.Errorf("unable to connect to etcd: %v", err) } secretStore = newSecretStore(a.etcdClient) onionStore = newOnionStore(a.etcdClient) case "postgres": db, err := aperturedb.NewPostgresStore(a.cfg.Postgres) if err != nil { return fmt.Errorf("unable to connect to postgres: %v", err) } a.db = db.DB dbSecretTxer := aperturedb.NewTransactionExecutor(db, func(tx *sql.Tx) aperturedb.SecretsDB { return db.WithTx(tx) }, ) secretStore = aperturedb.NewSecretsStore(dbSecretTxer) dbOnionTxer := aperturedb.NewTransactionExecutor(db, func(tx *sql.Tx) aperturedb.OnionDB { return db.WithTx(tx) }, ) onionStore = aperturedb.NewOnionStore(dbOnionTxer) dbLNCTxer := aperturedb.NewTransactionExecutor(db, func(tx *sql.Tx) aperturedb.LNCSessionsDB { return db.WithTx(tx) }, ) lncStore = aperturedb.NewLNCSessionsStore(dbLNCTxer) case "sqlite": db, err := aperturedb.NewSqliteStore(a.cfg.Sqlite) if err != nil { return fmt.Errorf("unable to connect to sqlite: %v", err) } a.db = db.DB dbSecretTxer := aperturedb.NewTransactionExecutor(db, func(tx *sql.Tx) aperturedb.SecretsDB { return db.WithTx(tx) }, ) secretStore = aperturedb.NewSecretsStore(dbSecretTxer) dbOnionTxer := aperturedb.NewTransactionExecutor(db, func(tx *sql.Tx) aperturedb.OnionDB { return db.WithTx(tx) }, ) onionStore = aperturedb.NewOnionStore(dbOnionTxer) dbLNCTxer := aperturedb.NewTransactionExecutor(db, func(tx *sql.Tx) aperturedb.LNCSessionsDB { return db.WithTx(tx) }, ) lncStore = aperturedb.NewLNCSessionsStore(dbLNCTxer) default: return fmt.Errorf("unknown database backend: %s", a.cfg.DatabaseBackend) } log.Infof("Using %v as database backend", a.cfg.DatabaseBackend) if !a.cfg.Authenticator.Disable { authCfg := a.cfg.Authenticator genInvoiceReq := func(price int64) (*lnrpc.Invoice, error) { return &lnrpc.Invoice{ Memo: "L402", Value: price, }, nil } switch { case authCfg.Passphrase != "": log.Infof("Using lnc's authenticator config") if a.cfg.DatabaseBackend == "etcd" { return fmt.Errorf("etcd is not supported as " + "a database backend for lnc " + "connections") } session, err := lnc.NewSession( authCfg.Passphrase, authCfg.MailboxAddress, authCfg.DevServer, ) if err != nil { return fmt.Errorf("unable to create lnc "+ "session: %w", err) } a.challenger, err = challenger.NewLNCChallenger( session, lncStore, a.cfg.InvoiceBatchSize, genInvoiceReq, errChan, a.cfg.StrictVerify, ) if err != nil { return fmt.Errorf("unable to start lnc "+ "challenger: %w", err) } case authCfg.LndHost != "": log.Infof("Using lnd's authenticator config") authCfg := a.cfg.Authenticator client, err := lndclient.NewBasicClient( authCfg.LndHost, authCfg.TLSPath, authCfg.MacDir, authCfg.Network, lndclient.MacFilename( invoiceMacaroonName, ), ) if err != nil { return err } a.challenger, err = challenger.NewLndChallenger( client, a.cfg.InvoiceBatchSize, genInvoiceReq, context.Background, errChan, a.cfg.StrictVerify, ) if err != nil { return err } } } // Create the proxy and connect it to lnd. a.proxy, a.proxyCleanup, err = createProxy( a.cfg, a.challenger, secretStore, ) if err != nil { return err } handler := http.HandlerFunc(a.proxy.ServeHTTP) a.httpsServer = &http.Server{ Addr: a.cfg.ListenAddr, Handler: handler, IdleTimeout: a.cfg.IdleTimeout, ReadTimeout: a.cfg.ReadTimeout, WriteTimeout: a.cfg.WriteTimeout, } log.Infof("Creating server with idle_timeout=%v, read_timeout=%v "+ "and write_timeout=%v", a.cfg.IdleTimeout, a.cfg.ReadTimeout, a.cfg.WriteTimeout) // Create TLS configuration by either creating new self-signed certs or // trying to obtain one through Let's Encrypt. var serveFn func() error if a.cfg.Insecure { // Normally, HTTP/2 only works with TLS. But there is a special // version called HTTP/2 Cleartext (h2c) that some clients // support and that gRPC uses when the grpc.WithInsecure() // option is used. The default HTTP handler doesn't support it // though so we need to add a special h2c handler here. serveFn = a.httpsServer.ListenAndServe a.httpsServer.Handler = h2c.NewHandler(handler, &http2.Server{}) } else { a.httpsServer.TLSConfig, err = getTLSConfig( a.cfg.ServerName, a.cfg.BaseDir, a.cfg.AutoCert, ) if err != nil { return err } serveFn = func() error { // The httpsServer.TLSConfig contains certificates at // this point so we don't need to pass in certificate // and key file names. return a.httpsServer.ListenAndServeTLS("", "") } } // Finally run the server. log.Infof("Starting the server, listening on %s.", a.cfg.ListenAddr) a.wg.Add(1) go func() { defer a.wg.Done() select { case errChan <- serveFn(): case <-a.quit: } }() // If we need to listen over Tor as well, we'll set up the onion // services now. We're not able to use TLS for onion services since they // can't be verified, so we'll spin up an additional HTTP/2 server // _without_ TLS that is not exposed to the outside world. This server // will only be reached through the onion services, which already // provide encryption, so running this additional HTTP server should be // relatively safe. if a.cfg.Tor.V3 { torController, err := initTorListener(a.cfg, onionStore) if err != nil { return err } defer func() { _ = torController.Stop() }() a.torHTTPServer = &http.Server{ Addr: fmt.Sprintf("localhost:%d", a.cfg.Tor.ListenPort), Handler: h2c.NewHandler(handler, &http2.Server{}), } a.wg.Add(1) go func() { defer a.wg.Done() select { case errChan <- a.torHTTPServer.ListenAndServe(): case <-a.quit: } }() } return nil } // UpdateServices instructs the proxy to re-initialize its internal // configuration of backend services. This can be used to add or remove backends // at run time or enable/disable authentication on the fly. func (a *Aperture) UpdateServices(services []*proxy.Service) error { return a.proxy.UpdateServices(services) } // Stop gracefully shuts down the Aperture service. func (a *Aperture) Stop() error { var returnErr error if a.challenger != nil { a.challenger.Stop() } // Stop everything that was started alongside the proxy, for example the // gRPC and REST servers. if a.proxyCleanup != nil { a.proxyCleanup() } if a.etcdClient != nil { if err := a.etcdClient.Close(); err != nil { log.Errorf("Error terminating etcd client: %v", err) returnErr = err } } if a.db != nil { if err := a.db.Close(); err != nil { log.Errorf("Error closing database: %v", err) returnErr = err } } // Shut down our client and server connections now. This should cause // the first goroutine to quit. cleanup(a.httpsServer, a.proxy) // If we started a tor server as well, shut it down now too to cause the // second goroutine to quit. if a.torHTTPServer != nil { if err := a.torHTTPServer.Close(); err != nil { log.Errorf("Error stopping tor server: %v", err) returnErr = err } } // Now we wait for the goroutines to exit before we return. The defers // will take care of the rest of our started resources. close(a.quit) a.wg.Wait() return returnErr } // fileExists reports whether the named file or directory exists. // This function is taken from https://github.com/btcsuite/btcd func fileExists(name string) bool { if _, err := os.Stat(name); err != nil { if os.IsNotExist(err) { return false } } return true } // getConfig loads and parses the configuration file then checks it for valid // content. func getConfig() (*Config, error) { // Pre-parse command line flags to determine whether we've been pointed // to a custom config file. cfg := NewConfig() if _, err := flags.Parse(cfg); err != nil { return nil, err } // If a custom config file is provided, we require that it exists. var mustExist bool // Start with our default path for config file. configFile := filepath.Join(apertureDataDir, defaultConfigFilename) // If a base directory is set, we'll look in there for a config file. // We don't require it to exist because we could just want to place // all of our files here, and specify all config inline. if cfg.BaseDir != "" { configFile = filepath.Join(cfg.BaseDir, defaultConfigFilename) } // If a specific config file is set, we'll look here for a config file, // even if a base directory for our files was set. In this case, the // config file must exist, since we're specifically being pointed it. if cfg.ConfigFile != "" { configFile = lnd.CleanAndExpandPath(cfg.ConfigFile) mustExist = true } // Read our config file, either from the custom path provided or our // default location. b, err := os.ReadFile(configFile) switch { // If the file was found, unmarshal it. case err == nil: err = yaml.Unmarshal(b, cfg) if err != nil { return nil, err } // If the error is unrelated to the existence of the file, we must // always return it. case !os.IsNotExist(err): return nil, err // If we require that the config file exists and we got an error // related to file existence, we must fail. case mustExist && os.IsNotExist(err): return nil, fmt.Errorf("config file: %v must exist: %w", configFile, err) } // Finally, parse the remaining command line options again to ensure // they take precedence. if _, err := flags.Parse(cfg); err != nil { return nil, err } // Clean and expand our base dir, cert and macaroon paths. cfg.BaseDir = lnd.CleanAndExpandPath(cfg.BaseDir) cfg.Authenticator.TLSPath = lnd.CleanAndExpandPath( cfg.Authenticator.TLSPath, ) cfg.Authenticator.MacDir = lnd.CleanAndExpandPath( cfg.Authenticator.MacDir, ) // Set default mailbox address if none is set. if cfg.Authenticator.MailboxAddress == "" { cfg.Authenticator.MailboxAddress = defaultMailboxAddress } // Then check the configuration that we got from the config file, all // required values need to be set at this point. if err := cfg.validate(); err != nil { return nil, err } return cfg, nil } // setupLogging parses the debug level and initializes the log file rotator. func setupLogging(cfg *Config, interceptor signal.Interceptor) error { if cfg.DebugLevel == "" { cfg.DebugLevel = defaultLogLevel } // Now initialize the logger and set the log level. sugLogMgr := build.NewSubLoggerManager( build.NewDefaultLogHandlers(cfg.Logging, logWriter)..., ) SetupLoggers(sugLogMgr, interceptor) // Use our default data dir unless a base dir is set. logFile := filepath.Join(apertureDataDir, defaultLogFilename) if cfg.BaseDir != "" { logFile = filepath.Join(cfg.BaseDir, defaultLogFilename) } err := logWriter.InitLogRotator( cfg.Logging.File, logFile, ) if err != nil { return err } return build.ParseAndSetDebugLevels(cfg.DebugLevel, sugLogMgr) } // getTLSConfig returns a TLS configuration for either a self-signed certificate // or one obtained through Let's Encrypt. func getTLSConfig(serverName, baseDir string, autoCert bool) ( *tls.Config, error) { // Use our default data dir unless a base dir is set. apertureDir := apertureDataDir if baseDir != "" { apertureDir = baseDir } // If requested, use the autocert library that will create a new // certificate through Let's Encrypt as soon as the first client HTTP // request on the server using the TLS config comes in. Unfortunately // you cannot tell the library to create a certificate on startup for a // specific host. if autoCert { serverName := serverName if serverName == "" { return nil, fmt.Errorf("servername option is " + "required for secure operation") } certDir := filepath.Join(apertureDir, "autocert") log.Infof("Configuring autocert for server %v with cache dir "+ "%v", serverName, certDir) manager := autocert.Manager{ Cache: autocert.DirCache(certDir), Prompt: autocert.AcceptTOS, HostPolicy: autocert.HostWhitelist(serverName), } go func() { err := http.ListenAndServe( ":http", manager.HTTPHandler(nil), ) if err != nil { log.Errorf("autocert http: %v", err) } }() return &tls.Config{ GetCertificate: manager.GetCertificate, CipherSuites: http2TLSCipherSuites, MinVersion: tls.VersionTLS10, }, nil } // If we're not using autocert, we want to create self-signed TLS certs // and save them at the specified location (if they don't already // exist). tlsKeyFile := filepath.Join(apertureDir, defaultTLSKeyFilename) tlsCertFile := filepath.Join(apertureDir, defaultTLSCertFilename) // Go 1.25 tightened x509 SAN validation and now rejects empty dNSName // entries (`x509: SAN dNSName is malformed`). When users rely on the // default config (no server name), we still want to generate a usable // self-signed cert, so we only append non-empty hostnames. var tlsExtraDomains []string if serverName != "" { tlsExtraDomains = append(tlsExtraDomains, serverName) } if !fileExists(tlsCertFile) && !fileExists(tlsKeyFile) { log.Infof("Generating TLS certificates...") certBytes, keyBytes, err := cert.GenCertPair( selfSignedCertOrganization, nil, tlsExtraDomains, false, selfSignedCertValidity, ) if err != nil { return nil, err } // Now that we have the certificate and key, we'll store them // to the file system. err = cert.WriteCertPair( tlsCertFile, tlsKeyFile, certBytes, keyBytes, ) if err != nil { return nil, err } log.Infof("Done generating TLS certificates") } // Load the certs now so we can inspect it and return a complete TLS // config later. certData, parsedCert, err := cert.LoadCert(tlsCertFile, tlsKeyFile) if err != nil { return nil, err } // The margin is negative, so adding it to the expiry date should give // us a date in about the middle of it's validity period. expiryWithMargin := parsedCert.NotAfter.Add( -1 * selfSignedCertExpiryMargin, ) // We only want to renew a certificate that we created ourselves. If // we are using a certificate that was passed to us (perhaps created by // an externally running Let's Encrypt process) we aren't going to try // to replace it. isSelfSigned := len(parsedCert.Subject.Organization) > 0 && parsedCert.Subject.Organization[0] == selfSignedCertOrganization // If the certificate expired or it was outdated, delete it and the TLS // key and generate a new pair. if isSelfSigned && time.Now().After(expiryWithMargin) { log.Info("TLS certificate will expire soon, generating a " + "new one") err := os.Remove(tlsCertFile) if err != nil { return nil, err } err = os.Remove(tlsKeyFile) if err != nil { return nil, err } log.Infof("Renewing TLS certificates...") certBytes, keyBytes, err := cert.GenCertPair( selfSignedCertOrganization, nil, tlsExtraDomains, false, selfSignedCertValidity, ) if err != nil { return nil, err } err = cert.WriteCertPair( tlsCertFile, tlsKeyFile, certBytes, keyBytes, ) if err != nil { return nil, err } log.Infof("Done renewing TLS certificates") // Reload the certificate data. certData, _, err = cert.LoadCert(tlsCertFile, tlsKeyFile) if err != nil { return nil, err } } return &tls.Config{ Certificates: []tls.Certificate{certData}, CipherSuites: http2TLSCipherSuites, MinVersion: tls.VersionTLS10, }, nil } // initTorListener initiates a Tor controller instance with the Tor server // specified in the config. Onion services will be created over which the proxy // can be reached at. func initTorListener(cfg *Config, store tor.OnionStore) (*tor.Controller, error) { // Establish a controller connection with the backing Tor server and // proceed to create the requested onion services. onionCfg := tor.AddOnionConfig{ VirtualPort: int(cfg.Tor.VirtualPort), TargetPorts: []int{int(cfg.Tor.ListenPort)}, Store: store, } torController := tor.NewController(cfg.Tor.Control, "", "") if err := torController.Start(); err != nil { return nil, err } if cfg.Tor.V3 { onionCfg.Type = tor.V3 addr, err := torController.AddOnion(onionCfg) if err != nil { return nil, err } log.Infof("Listening over Tor on %v", addr) } return torController, nil } // createProxy creates the proxy with all the services it needs. func createProxy(cfg *Config, challenger challenger.Challenger, store mint.SecretStore) (*proxy.Proxy, func(), error) { minter := mint.New(&mint.Config{ Challenger: challenger, Secrets: store, ServiceLimiter: newStaticServiceLimiter(cfg.Services), Now: time.Now, }) authenticator := auth.NewL402Authenticator(minter, challenger) // By default the static file server only returns 404 answers for // security reasons. Serving files from the staticRoot directory has to // be enabled intentionally. staticServer := http.NotFoundHandler() if cfg.ServeStatic { if len(strings.TrimSpace(cfg.StaticRoot)) == 0 { return nil, nil, fmt.Errorf("staticroot cannot be " + "empty, must contain path to directory that " + "contains index.html") } staticServer = http.FileServer(http.Dir(cfg.StaticRoot)) } var ( localServices []proxy.LocalService proxyCleanup = func() {} ) if cfg.HashMail.Enabled { hashMailServices, cleanup, err := createHashMailServer(cfg) if err != nil { return nil, nil, err } localServices = append(localServices, hashMailServices...) proxyCleanup = cleanup } // The static file server must be last since it will match all calls // that make it to it. localServices = append(localServices, proxy.NewLocalService( staticServer, func(r *http.Request) bool { return true }, )) prxy, err := proxy.New( authenticator, cfg.Services, cfg.Blocklist, localServices..., ) return prxy, proxyCleanup, err } // createHashMailServer creates the gRPC server for the hash mail message // gateway and an additional REST and WebSocket capable proxy for that gRPC // server. func createHashMailServer(cfg *Config) ([]proxy.LocalService, func(), error) { var localServices []proxy.LocalService serverOpts := []grpc.ServerOption{ grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{ MinTime: time.Minute, }), } // Before we register both servers, we'll also ensure that the collector // will export latency metrics for the histogram. if cfg.Prometheus != nil && cfg.Prometheus.Enabled { grpc_prometheus.EnableHandlingTimeHistogram() serverOpts = append( serverOpts, grpc.ChainUnaryInterceptor( grpc_prometheus.UnaryServerInterceptor, ), grpc.ChainStreamInterceptor( grpc_prometheus.StreamServerInterceptor, ), ) } // Create a gRPC server for the hashmail server. hashMailServer := newHashMailServer(hashMailServerConfig{ msgRate: cfg.HashMail.MessageRate, msgBurstAllowance: cfg.HashMail.MessageBurstAllowance, staleTimeout: cfg.HashMail.StaleTimeout, }) hashMailGRPC := grpc.NewServer(serverOpts...) hashmailrpc.RegisterHashMailServer(hashMailGRPC, hashMailServer) localServices = append(localServices, proxy.NewLocalService( hashMailGRPC, func(r *http.Request) bool { return strings.HasPrefix(r.URL.Path, hashMailGRPCPrefix) }), ) // Export the gRPC information for the public gRPC server. if cfg.Prometheus != nil && cfg.Prometheus.Enabled { grpc_prometheus.Register(hashMailGRPC) } // And a REST proxy for it as well. // 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 := gateway.WithMarshalerOption( gateway.MIMEWildcard, &gateway.JSONPb{ MarshalOptions: protojson.MarshalOptions{ UseProtoNames: true, EmitUnpopulated: true, }, }, ) // We'll also create and start an accompanying proxy to serve clients // through REST. ctxc, cancel := context.WithCancel(context.Background()) proxyCleanup := func() { hashMailServer.Stop() cancel() } // The REST proxy connects to our main listen address. If we're serving // TLS, we don't care about the certificate being valid, as we issue it // ourselves. If we are serving without TLS (for example when behind a // load balancer), we need to connect to ourselves without using TLS as // well. restProxyTLSOpt := grpc.WithTransportCredentials(credentials.NewTLS( &tls.Config{InsecureSkipVerify: true}, )) if cfg.Insecure { restProxyTLSOpt = grpc.WithTransportCredentials( insecure.NewCredentials(), ) } mux := gateway.NewServeMux(customMarshalerOption) err := hashmailrpc.RegisterHashMailHandlerFromEndpoint( ctxc, mux, cfg.ListenAddr, []grpc.DialOption{ restProxyTLSOpt, }, ) if err != nil { proxyCleanup() return nil, nil, err } // Wrap the default grpc-gateway handler with the WebSocket handler. restHandler := lnrpc.NewWebSocketProxy( mux, log, 0, 0, clientStreamingURIs, ) // Create our proxy chain now. A request will pass // through the following chain: // req ---> CORS handler --> WS proxy ---> REST proxy --> gRPC endpoint corsHandler := allowCORS(restHandler, []string{"*"}) localServices = append(localServices, proxy.NewLocalService( corsHandler, func(r *http.Request) bool { return strings.HasPrefix(r.URL.Path, hashMailRESTPrefix) }, )) return localServices, proxyCleanup, nil } // cleanup closes the given server and shuts down the log rotator. func cleanup(server io.Closer, proxy io.Closer) { if err := proxy.Close(); err != nil { log.Errorf("Error terminating proxy: %v", err) } err := server.Close() if err != nil { log.Errorf("Error closing server: %v", err) } log.Info("Shutdown complete") err = logWriter.Close() if err != nil { log.Errorf("Could not close log rotator: %v", err) } } // 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) }) }