Skip to content

Commit b1c7fc9

Browse files
committed
config+proxy: add basic authentication
1 parent b12ed4c commit b1c7fc9

File tree

2 files changed

+95
-10
lines changed

2 files changed

+95
-10
lines changed

config.go

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package shushtar
33
import (
44
"crypto/tls"
55
"fmt"
6+
"io/ioutil"
67
"net"
78
"os"
89
"path/filepath"
@@ -18,14 +19,23 @@ import (
1819
"github.com/mwitkow/go-conntrack/connhelpers"
1920
)
2021

22+
const (
23+
defaultHTTPSListen = "127.0.0.1:8443"
24+
25+
uiPasswordMinLength = 8
26+
)
27+
2128
// Config is the main configuration struct of shushtar. It contains all config
2229
// items of its enveloping subservers, each prefixed with their daemon's short
2330
// name.
2431
type Config struct {
25-
HTTPSListen string `long:"httpslisten" description:"host:port to listen for incoming HTTP/2 connections on"`
26-
Lnd *lnd.Config `group:"lnd" namespace:"lnd"`
27-
Faraday *faraday.Config `group:"faraday" namespace:"faraday"`
28-
Loop *loopd.Config `group:"loop" namespace:"loop"`
32+
HTTPSListen string `long:"httpslisten" description:"host:port to listen for incoming HTTP/2 connections on"`
33+
UIPassword string `long:"uipassword" description:"the password that must be entered when using the loop UI. use a strong password to protect your node from unauthorized access through the web UI"`
34+
UIPasswordFile string `long:"uipassword_file" description:"same as uipassword but instead of passing in the value directly, read the password from the specified file"`
35+
UIPasswordEnv string `long:"uipassword_env" description:"same as uipassword but instead of passing in the value directly, read the password from the specified environment variable"`
36+
Lnd *lnd.Config `group:"lnd" namespace:"lnd"`
37+
Faraday *faraday.Config `group:"faraday" namespace:"faraday"`
38+
Loop *loopd.Config `group:"loop" namespace:"loop"`
2939
}
3040

3141
// loadLndConfig loads and sanitizes the lnd main configuration and hooks up all
@@ -114,6 +124,45 @@ func getNetwork(cfg *lncfg.Chain) (string, error) {
114124
}
115125
}
116126

127+
// readUIPassword reads the password for the UI either from the command line
128+
// flag, a file specified or an environment variable.
129+
func readUIPassword(config *Config) error {
130+
// A password is passed in as a command line flag (or config file
131+
// variable) directly.
132+
if len(strings.TrimSpace(config.UIPassword)) > 0 {
133+
config.UIPassword = strings.TrimSpace(config.UIPassword)
134+
return nil
135+
}
136+
137+
// A file that contains the password is specified.
138+
if len(strings.TrimSpace(config.UIPasswordFile)) > 0 {
139+
content, err := ioutil.ReadFile(strings.TrimSpace(
140+
config.UIPasswordFile,
141+
))
142+
if err != nil {
143+
return fmt.Errorf("could not read file %s: %v",
144+
config.UIPasswordFile, err)
145+
}
146+
config.UIPassword = strings.TrimSpace(string(content))
147+
return nil
148+
}
149+
150+
// The name of an environment variable was specified.
151+
if len(strings.TrimSpace(config.UIPasswordEnv)) > 0 {
152+
content := os.Getenv(strings.TrimSpace(config.UIPasswordEnv))
153+
if len(content) == 0 {
154+
return fmt.Errorf("environment variable %s is empty",
155+
config.UIPasswordEnv)
156+
}
157+
config.UIPassword = strings.TrimSpace(content)
158+
return nil
159+
}
160+
161+
return fmt.Errorf("mandatory password for UI not configured. specify " +
162+
"either a password directly or a file or environment " +
163+
"variable that contains the password")
164+
}
165+
117166
func buildTLSConfigForHttp2(config *lnd.Config) (*tls.Config, error) {
118167
tlsCert, _, err := cert.LoadCert(config.TLSCertPath, config.TLSKeyPath)
119168
if err != nil {

shushtar.go

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package shushtar
33
import (
44
"context"
55
"crypto/tls"
6+
"encoding/base64"
67
"errors"
78
"fmt"
89
"io/ioutil"
@@ -30,9 +31,11 @@ import (
3031
"github.com/rakyll/statik/fs"
3132
"google.golang.org/grpc"
3233
"google.golang.org/grpc/backoff"
34+
"google.golang.org/grpc/codes"
3335
"google.golang.org/grpc/credentials"
3436
"google.golang.org/grpc/grpclog"
3537
"google.golang.org/grpc/metadata"
38+
"google.golang.org/grpc/status"
3639
"gopkg.in/macaroon.v2"
3740

3841
// Import generated go package that contains all static files for the
@@ -41,7 +44,6 @@ import (
4144
)
4245

4346
const (
44-
defaultHTTPSListen = "127.0.0.1:8443"
4547
defaultServerTimeout = 10 * time.Second
4648
defaultStartupTimeout = 5 * time.Second
4749
)
@@ -51,6 +53,10 @@ var (
5153
// set this to 200MiB atm.
5254
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200)
5355

56+
authError = status.Error(
57+
codes.Unauthenticated, "authentication required",
58+
)
59+
5460
lndDefaultConfig = lnd.DefaultConfig()
5561
faradayDefaultConfig = faraday.DefaultConfig()
5662
loopDefaultConfig = loopd.DefaultConfig()
@@ -102,6 +108,15 @@ func (g *Shushtar) Run() error {
102108
return err
103109
}
104110

111+
err = readUIPassword(g.cfg)
112+
if err != nil {
113+
return fmt.Errorf("could not read UI password: %v", err)
114+
}
115+
if len(g.cfg.UIPassword) < uiPasswordMinLength {
116+
return fmt.Errorf("please set a strong password for the UI, "+
117+
"at least %d characters long", uiPasswordMinLength)
118+
}
119+
105120
// Load the configuration, and parse any command line options. This
106121
// function will also set up logging properly.
107122
g.cfg.Lnd, err = loadLndConfig(g.cfg)
@@ -416,7 +431,7 @@ func (g *Shushtar) startGrpcWebProxy() error {
416431
// admin macaroon and converts the browser's gRPC web calls into native
417432
// gRPC.
418433
lndGrpcServer, grpcServer, err := buildGrpcWebProxyServer(
419-
g.lndAddr, g.cfg.Lnd,
434+
g.lndAddr, g.cfg.UIPassword, g.cfg.Lnd,
420435
)
421436
if err != nil {
422437
return fmt.Errorf("could not create gRPC web proxy: %v", err)
@@ -480,7 +495,7 @@ func (g *Shushtar) startGrpcWebProxy() error {
480495
// buildGrpcWebProxyServer creates a gRPC server that will serve gRPC web to the
481496
// browser and translate all incoming gRPC web calls into native gRPC that are
482497
// then forwarded to lnd's RPC interface.
483-
func buildGrpcWebProxyServer(lndAddr string,
498+
func buildGrpcWebProxyServer(lndAddr, uiPassword string,
484499
config *lnd.Config) (*grpcweb.WrappedGrpcServer, *grpc.Server, error) {
485500

486501
// Apply gRPC-wide changes.
@@ -489,14 +504,21 @@ func buildGrpcWebProxyServer(lndAddr string,
489504
config.LogWriter, GrpcLogSubsystem,
490505
))
491506

507+
// The gRPC web calls are protected by HTTP basic auth which is defined
508+
// by base64(username:password). Because we only have a password, we
509+
// just use base64(password:password).
510+
basicAuth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(
511+
"%s:%s", uiPassword, uiPassword,
512+
)))
513+
492514
// Setup the connection to lnd. GRPC web has a few kinks that need to be
493515
// addressed with a custom director that just takes care of a few HTTP
494516
// header fields.
495517
backendConn, err := dialLnd(lndAddr, config)
496518
if err != nil {
497519
return nil, nil, fmt.Errorf("could not dial lnd: %v", err)
498520
}
499-
director := newDirector(backendConn)
521+
director := newDirector(backendConn, basicAuth)
500522

501523
// Set up the final gRPC server that will serve gRPC web to the browser
502524
// and translate all incoming gRPC web calls into native gRPC that are
@@ -515,16 +537,30 @@ func buildGrpcWebProxyServer(lndAddr string,
515537

516538
// newDirector returns a new director function that fixes some common known
517539
// issues when using gRPC web from the browser.
518-
func newDirector(backendConn *grpc.ClientConn) proxy.StreamDirector {
540+
func newDirector(backendConn *grpc.ClientConn,
541+
basicAuth string) proxy.StreamDirector {
542+
519543
return func(ctx context.Context, fullMethodName string) (context.Context,
520544
*grpc.ClientConn, error) {
521545

522546
md, _ := metadata.FromIncomingContext(ctx)
523-
mdCopy := md.Copy()
547+
548+
authHeaders := md.Get("authorization")
549+
if len(authHeaders) == 0 {
550+
return nil, nil, authError
551+
}
552+
authHeaderParts := strings.Split(authHeaders[0], " ")
553+
if len(authHeaderParts) != 2 {
554+
return nil, nil, authError
555+
}
556+
if authHeaderParts[1] != basicAuth {
557+
return nil, nil, authError
558+
}
524559

525560
// If this header is present in the request from the web client,
526561
// the actual connection to the backend will not be established.
527562
// https://github.com/improbable-eng/grpc-web/issues/568
563+
mdCopy := md.Copy()
528564
delete(mdCopy, "connection")
529565

530566
outCtx := metadata.NewOutgoingContext(ctx, mdCopy)

0 commit comments

Comments
 (0)