forked from lightninglabs/aperture
-
Notifications
You must be signed in to change notification settings - Fork 0
/
aperture.go
888 lines (766 loc) · 25.9 KB
/
aperture.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
package aperture
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
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/auth"
"github.com/lightninglabs/aperture/mint"
"github.com/lightninglabs/aperture/proxy"
"github.com/lightninglabs/lightning-node-connect/hashmailrpc"
"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/keepalive"
"google.golang.org/protobuf/encoding/protojson"
"gopkg.in/yaml.v2"
// Blank import to set up profiling HTTP handlers.
_ "net/http/pprof"
)
const (
// topLevelKey is the top level key for an etcd cluster where we'll
// store all LSAT 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"
)
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); 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
challenger *LndChallenger
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) error {
// Start the prometheus exporter.
err := StartPrometheusExporter(a.cfg.Prometheus)
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.ProfilePort != 0 {
if a.cfg.ProfilePort < 1024 || a.cfg.ProfilePort > 65535 {
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.ProfilePort,
)
log.Infof("Starting profile server at %s", listenAddr)
fmt.Println(http.ListenAndServe(listenAddr, nil))
}()
}
// 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)
}
// Create our challenger that uses our backing lnd node to create
// invoices and check their settlement status.
genInvoiceReq := func(price int64) (*lnrpc.Invoice, error) {
return &lnrpc.Invoice{
Memo: "LSAT",
Value: price,
}, nil
}
if !a.cfg.Authenticator.Disable {
a.challenger, err = NewLndChallenger(
a.cfg.Authenticator, genInvoiceReq, errChan,
)
if err != nil {
return err
}
err = a.challenger.Start()
if err != nil {
return err
}
}
// Create the proxy and connect it to lnd.
a.proxy, a.proxyCleanup, err = createProxy(
a.cfg, a.challenger, a.etcdClient,
)
if err != nil {
return err
}
handler := http.HandlerFunc(a.proxy.ServeHTTP)
a.httpsServer = &http.Server{
Addr: a.cfg.ListenAddr,
Handler: handler,
IdleTimeout: 0,
ReadTimeout: 0,
WriteTimeout: 0,
}
// 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, a.etcdClient)
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()
}
// Shut down our client and server connections now. This should cause
// the first goroutine to quit.
cleanup(a.etcdClient, 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 {
returnErr = a.torHTTPServer.Close()
}
// 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,
)
// 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.
SetupLoggers(logWriter, 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(
logFile, defaultMaxLogFileSize, defaultMaxLogFiles,
)
if err != nil {
return err
}
return build.ParseAndSetDebugLevels(cfg.DebugLevel, logWriter)
}
// 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)
tlsExtraDomains := []string{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, nil, 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, etcd *clientv3.Client) (*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: newOnionStore(etcd),
}
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 *LndChallenger,
etcdClient *clientv3.Client) (*proxy.Proxy, func(), error) {
minter := mint.New(&mint.Config{
Challenger: challenger,
Secrets: newSecretStore(etcdClient),
ServiceLimiter: newStaticServiceLimiter(cfg.Services),
Now: time.Now,
})
authenticator := auth.NewLsatAuthenticator(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, 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.WithInsecure()
}
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(etcdClient io.Closer, server io.Closer, proxy io.Closer) {
if err := proxy.Close(); err != nil {
log.Errorf("Error terminating proxy: %v", err)
}
if err := etcdClient.Close(); err != nil {
log.Errorf("Error terminating etcd client: %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)
})
}