diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..ea82c0a --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,9 @@ +## Requirements + +* go 1.13 + +## Up and Running +1. Clone the repository. +2. See `sample-conf.yaml` to see how to configure your target backend services (and, optionally, change the port that Kirin runs on). +3. `cd cmd/kirin && go build` +4. `./kirin` diff --git a/README.md b/README.md new file mode 100644 index 0000000..d8b4a99 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# Lightning Service Authentication Token (LSAT) proxy + +Kirin is a HTTP reverse proxy that supports proxying requests for gRPC (HTTP/2) +and REST (HTTP/1 and HTTP/2) backends. + +## Installation + +See [INSTALL.md](install.md). + +## Demo + +There is a demo installation available at +[test-staging.swap.lightning.today:8081](https://test-staging.swap.lightning.today:8081). + +### Use Case 1: Web GUI + +If you visit the demo installation in the browser, you see a simple web GUI. +There you can request the current BOS scores for testnet. Notice that you can +only request the scores three times per IP addres. After the free requests have +been used up, you receive an LSAT token/macaroon and are challenged to pay an +invoice to authorize it. + +You have two options to pay for the invoice: + +1. If you have Joule installed in your browser and connected to a testnet node, + you can click the "Pay invoice with Joule" button to pay the invoice. After + successful payment the page should automatically refresh. +1. In case you want to pay the invoice manually, copy the payment request to + your wallet of choice that has the feature to reveal the preimage after a + successful payment. Copy the payment preimage in hex format, then click the + button "Paste preimage of manual payment" and paste it in the dialog box. + +### Use Case 2: cURL + +First, let's request the BOS scores until we hit the freebie limit: + +`curl -k -v https://test-staging.swap.lightning.today:8081/availability/v1/btc.json` + +At some point, we will get an answer 402 with an authorization header: + +``` +www-authenticate: LSAT macaroon='...' invoice='lntb10n1...' +``` + +We will need both these values, the `macaroon` and the `invoice` so copy them +to a text file somewhere (without the single quotes!). +Let's pay the invoice now, choose any LN wallet that displays the preimage after +a successful payment. Copy the hex encoded preimage to the text file too once +you get it from the wallet. + +Finally, you can issue the authenticated request with the following command: + +``` +curl -k -v \ +--header "Authorization: LSAT :" \ +https://test-staging.swap.lightning.today:8081/availability/v1/btc.json +``` diff --git a/auth/authenticator.go b/auth/authenticator.go new file mode 100644 index 0000000..ec51780 --- /dev/null +++ b/auth/authenticator.go @@ -0,0 +1,132 @@ +package auth + +import ( + "encoding/base64" + "encoding/hex" + "fmt" + "net/http" + "regexp" + + "github.com/lightninglabs/kirin/macaroons" + "gopkg.in/macaroon-bakery.v2/bakery" + "gopkg.in/macaroon-bakery.v2/bakery/checkers" +) + +var ( + authRegex = regexp.MustCompile("LSAT (.*?):([a-f0-9]{64})") + opWildcard = "*" +) + +// LsatAuthenticator is an authenticator that uses the LSAT protocol to +// authenticate requests. +type LsatAuthenticator struct { + challenger Challenger + macService *macaroons.Service +} + +// A compile time flag to ensure the LsatAuthenticator satisfies the +// Authenticator interface. +var _ Authenticator = (*LsatAuthenticator)(nil) + +// NewLsatAuthenticator creates a new authenticator that authenticates requests +// based on LSAT tokens. +func NewLsatAuthenticator(challenger Challenger) (*LsatAuthenticator, error) { + macService, err := macaroons.NewService() + if err != nil { + return nil, err + } + + return &LsatAuthenticator{ + challenger: challenger, + macService: macService, + }, nil +} + +// Accept returns whether or not the header successfully authenticates the user +// to a given backend service. +// +// NOTE: This is part of the Authenticator interface. +func (l *LsatAuthenticator) Accept(header *http.Header) bool { + authHeader := header.Get("Authorization") + log.Debugf("Trying to authorize with header value [%s].", authHeader) + if authHeader == "" { + return false + } + + if !authRegex.MatchString(authHeader) { + log.Debugf("Deny: Auth header in invalid format.") + return false + } + + matches := authRegex.FindStringSubmatch(authHeader) + if len(matches) != 3 { + log.Debugf("Deny: Auth header in invalid format.") + return false + } + + macBase64, preimageHex := matches[1], matches[2] + macBytes, err := base64.StdEncoding.DecodeString(macBase64) + if err != nil { + log.Debugf("Deny: Base64 decode of macaroon failed: %v", err) + return false + } + + preimageBytes, err := hex.DecodeString(preimageHex) + if err != nil { + log.Debugf("Deny: Hex decode of preimage failed: %v", err) + return false + } + + // TODO(guggero): check preimage against payment hash caveat in the + // macaroon. + if len(preimageBytes) != 32 { + log.Debugf("Deny: Decoded preimage has invalid length.") + return false + } + + err = l.macService.ValidateMacaroon(macBytes, []bakery.Op{}) + if err != nil { + log.Debugf("Deny: Macaroon validation failed: %v", err) + return false + } + return true +} + +// FreshChallengeHeader returns a header containing a challenge for the user to +// complete. +// +// NOTE: This is part of the Authenticator interface. +func (l *LsatAuthenticator) FreshChallengeHeader(r *http.Request) ( + http.Header, error) { + + paymentRequest, paymentHash, err := l.challenger.NewChallenge() + if err != nil { + log.Errorf("Error creating new challenge: %v", err) + return nil, err + } + + // Create a new macaroon and add the payment hash as a caveat. + // The bakery requires at least one operation so we add an "allow all" + // permission set for now. + mac, err := l.macService.NewMacaroon( + []bakery.Op{{Entity: opWildcard, Action: opWildcard}}, []string{ + checkers.Condition( + macaroons.CondRHash, paymentHash.String(), + ), + }, + ) + if err != nil { + log.Errorf("Error creating macaroon: %v", err) + return nil, err + } + + str := "LSAT macaroon='%s' invoice='%s'" + str = fmt.Sprintf( + str, base64.StdEncoding.EncodeToString(mac), paymentRequest, + ) + header := r.Header + header.Set("WWW-Authenticate", str) + + log.Debugf("Created new challenge header: [%s]", str) + return header, nil +} diff --git a/auth/config.go b/auth/config.go new file mode 100644 index 0000000..a4eb713 --- /dev/null +++ b/auth/config.go @@ -0,0 +1,41 @@ +package auth + +import ( + "fmt" + "strconv" + "strings" + + "github.com/lightninglabs/kirin/freebie" +) + +type Level string + +func (l Level) lower() string { + return strings.ToLower(string(l)) +} + +func (l Level) IsOn() bool { + lower := l.lower() + return lower == "" || lower == "on" || lower == "true" +} + +func (l Level) IsFreebie() bool { + return strings.HasPrefix(l.lower(), "freebie") +} + +func (l Level) FreebieCount() freebie.Count { + parts := strings.Split(l.lower(), " ") + if len(parts) != 2 { + panic(fmt.Errorf("invalid auth value: %s", l.lower())) + } + count, err := strconv.Atoi(parts[1]) + if err != nil { + panic(err) + } + return freebie.Count(count) +} + +func (l Level) IsOff() bool { + lower := l.lower() + return lower == "off" || lower == "false" +} diff --git a/auth/interface.go b/auth/interface.go new file mode 100644 index 0000000..00fa8ad --- /dev/null +++ b/auth/interface.go @@ -0,0 +1,26 @@ +package auth + +import ( + "net/http" + + "github.com/lightningnetwork/lnd/lntypes" +) + +// Authenticator is the generic interface for validating client headers and +// returning new challenge headers. +type Authenticator interface { + // Accept returns whether or not the header successfully authenticates + // the user to a given backend service. + Accept(*http.Header) bool + + // FreshChallengeHeader returns a header containing a challenge for the + // user to complete. + FreshChallengeHeader(r *http.Request) (http.Header, error) +} + +// Challenger is an interface for generating new payment challenges. +type Challenger interface { + // NewChallenge creates a new LSAT payment challenge, returning a + // payment request (invoice) and the corresponding payment hash. + NewChallenge() (string, lntypes.Hash, error) +} diff --git a/auth/log.go b/auth/log.go new file mode 100644 index 0000000..d1fe86c --- /dev/null +++ b/auth/log.go @@ -0,0 +1,29 @@ +package auth + +import ( + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" +) + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger("AUTH", nil)) +} + +// DisableLog disables all library log output. Logging output is disabled +// by default until UseLogger is called. +func DisableLog() { + UseLogger(btclog.Disabled) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/auth/mock_authenticator.go b/auth/mock_authenticator.go new file mode 100644 index 0000000..d0b25b6 --- /dev/null +++ b/auth/mock_authenticator.go @@ -0,0 +1,28 @@ +package auth + +import "net/http" + +// MockAuthenticator is a mock implementation of the authenticator. +type MockAuthenticator struct{} + +// NewMockAuthenticator returns a new MockAuthenticator instance. +func NewMockAuthenticator() *MockAuthenticator { + return &MockAuthenticator{} +} + +// Accept returns whether or not the header successfully authenticates the user +// to a given backend service. +func (a MockAuthenticator) Accept(header *http.Header) bool { + if header.Get("Authorization") != "" { + return true + } + return false +} + +// FreshChallengeHeader returns a header containing a challenge for the user to +// complete. +func (a MockAuthenticator) FreshChallengeHeader(r *http.Request) (http.Header, error) { + header := r.Header + header.Set("WWW-Authenticate", "LSAT macaroon='AGIAJEemVQUTEyNCR0exk7ek90Cg==' invoice='lnbc1500n1pw5kjhmpp5fu6xhthlt2vucmzkx6c7wtlh2r625r30cyjsfqhu8rsx4xpz5lwqdpa2fjkzep6yptksct5yp5hxgrrv96hx6twvusycn3qv9jx7ur5d9hkugr5dusx6cqzpgxqr23s79ruapxc4j5uskt4htly2salw4drq979d7rcela9wz02elhypmdzmzlnxuknpgfyfm86pntt8vvkvffma5qc9n50h4mvqhngadqy3ngqjcym5a'") + return header, nil +} diff --git a/challenger.go b/challenger.go new file mode 100644 index 0000000..4c79a62 --- /dev/null +++ b/challenger.go @@ -0,0 +1,74 @@ +package kirin + +import ( + "context" + "fmt" + + "github.com/lightninglabs/kirin/auth" + "github.com/lightninglabs/loop/lndclient" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lntypes" +) + +// InvoiceRequestGenerator is a function type that returns a new request for the +// lnrpc.AddInvoice call. +type InvoiceRequestGenerator func() (*lnrpc.Invoice, error) + +// LndChallenger is a challenger that uses an lnd backend to create new LSAT +// payment challenges. +type LndChallenger struct { + client lnrpc.LightningClient + genInvoiceReq InvoiceRequestGenerator +} + +// A compile time flag to ensure the LndChallenger satisfies the +// Challenger interface. +var _ auth.Challenger = (*LndChallenger)(nil) + +// NewLndChallenger creates a new challenger that uses the given connection +// details to connect to an lnd backend to create payment challenges. +func NewLndChallenger(cfg *authConfig, genInvoiceReq InvoiceRequestGenerator) ( + auth.Challenger, error) { + + if genInvoiceReq == nil { + return nil, fmt.Errorf("genInvoiceReq cannot be nil") + } + + client, err := lndclient.NewBasicClient( + cfg.LndHost, cfg.TlsPath, cfg.MacDir, cfg.Network, + ) + if err != nil { + return nil, err + } + return &LndChallenger{ + client: client, + genInvoiceReq: genInvoiceReq, + }, nil +} + +// NewChallenge creates a new LSAT payment challenge, returning a payment +// request (invoice) and the corresponding payment hash. +// +// NOTE: This is part of the Challenger interface. +func (l *LndChallenger) NewChallenge() (string, lntypes.Hash, error) { + // Obtain a new invoice from lnd first. We need to know the payment hash + // so we can add it as a caveat to the macaroon. + invoice, err := l.genInvoiceReq() + if err != nil { + log.Errorf("Error generating invoice request: %v", err) + return "", lntypes.ZeroHash, err + } + ctx := context.Background() + response, err := l.client.AddInvoice(ctx, invoice) + if err != nil { + log.Errorf("Error adding invoice: %v", err) + return "", lntypes.ZeroHash, err + } + paymentHash, err := lntypes.MakeHash(response.RHash) + if err != nil { + log.Errorf("Error parsing payment hash: %v", err) + return "", lntypes.ZeroHash, err + } + + return response.PaymentRequest, paymentHash, nil +} diff --git a/cmd/kirin/main.go b/cmd/kirin/main.go new file mode 100644 index 0000000..ac85736 --- /dev/null +++ b/cmd/kirin/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/lightninglabs/kirin" + +func main() { + kirin.Main() +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..368b57f --- /dev/null +++ b/config.go @@ -0,0 +1,48 @@ +package kirin + +import ( + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/kirin/proxy" +) + +var ( + kirinDataDir = btcutil.AppDataDir("kirin", false) + defaultConfigFilename = "kirin.yaml" + defaultTLSKeyFilename = "tls.key" + defaultTLSCertFilename = "tls.cert" + defaultLogLevel = "info" + defaultLogFilename = "kirin.log" + defaultMaxLogFiles = 3 + defaultMaxLogFileSize = 10 +) + +type authConfig struct { + // LndHost is the hostname of the LND instance to connect to. + LndHost string `long:"lndhost" description:"Hostname of the LND instance to connect to"` + + TlsPath string `long:"tlspath"` + + MacDir string `long:"macdir"` + + Network string `long:"network"` +} + +type config struct { + // ListenAddr is the listening address that we should use to allow Kirin + // to listen for requests. + ListenAddr string `long:"listenaddr" description:"The interface we should listen on for client requests"` + + // StaticRoot is the folder where the static content served by the proxy + // is located. + StaticRoot string `long:"staticroot" description:"The folder where the static content is located."` + + Authenticator *authConfig `long:"authenticator" description:"Configuration for the authenticator."` + + // Services is a list of JSON objects in string format, which specify + // each backend service to Kirin. + Services []*proxy.Service `long:"service" description:"Configurations for each Kirin backend service."` + + // DebugLevel is a string defining the log level for the service either + // for all subsystems the same or individual level by subsystem. + DebugLevel string `long:"debuglevel" description:"Debug level for the Kirin application and its subsystems."` +} diff --git a/freebie/interface.go b/freebie/interface.go new file mode 100644 index 0000000..cf18274 --- /dev/null +++ b/freebie/interface.go @@ -0,0 +1,15 @@ +package freebie + +import ( + "net" + "net/http" +) + +// DB is the main interface of the package freebie. It represents a store that +// keeps track of how many free requests a certain IP address can make to a +// certain resource. +type DB interface { + CanPass(*http.Request, net.IP) (bool, error) + + TallyFreebie(*http.Request, net.IP) (bool, error) +} diff --git a/freebie/mem_store.go b/freebie/mem_store.go new file mode 100644 index 0000000..cf80f74 --- /dev/null +++ b/freebie/mem_store.go @@ -0,0 +1,50 @@ +package freebie + +import ( + "net" + "net/http" +) + +var ( + defaultIpMask = net.IPv4Mask(0xff, 0xff, 0xff, 0x00) +) + +type Count uint16 + +type memStore struct { + numFreebies Count + freebieCounter map[string]Count +} + +func (m *memStore) getKey(ip net.IP) string { + return ip.Mask(defaultIpMask).String() +} + +func (m *memStore) currentCount(ip net.IP) Count { + counter, ok := m.freebieCounter[m.getKey(ip)] + if !ok { + return 0 + } + return counter +} + +func (m *memStore) CanPass(r *http.Request, ip net.IP) (bool, error) { + return m.currentCount(ip) < m.numFreebies, nil +} + +func (m *memStore) TallyFreebie(r *http.Request, ip net.IP) (bool, error) { + counter := m.currentCount(ip) + 1 + m.freebieCounter[m.getKey(ip)] = counter + return true, nil +} + +// NewMemIpMaskStore creates a new in-memory freebie store that masks the last +// byte of an IP address to keep track of free requests. The last byte of the +// address is discarded for the mapping to reduce risk of abuse by users that +// have a whole range of IPs at their disposal. +func NewMemIpMaskStore(numFreebies Count) DB { + return &memStore{ + numFreebies: numFreebies, + freebieCounter: make(map[string]Count), + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..22a9550 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/lightninglabs/kirin + +go 1.13 + +require ( + github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f + github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d + github.com/lightninglabs/loop v0.2.3-alpha + github.com/lightningnetwork/lnd v0.8.0-beta-rc3.0.20191029004703-c069bdd4c7c1 + gopkg.in/macaroon-bakery.v2 v2.1.0 + gopkg.in/macaroon.v2 v2.1.0 + gopkg.in/yaml.v2 v2.2.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1de0168 --- /dev/null +++ b/go.sum @@ -0,0 +1,286 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e h1:F2x1bq7RaNCIuqYpswggh1+c1JmwdnkHNC9wy1KDip0= +git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e/go.mod h1:BWqTsj8PgcPriQJGl7el20J/7TuT1d/hSyFDXMEpoEo= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e h1:n+DcnTNkQnHlwpsrHoQtkrJIO7CBx029fw6oR4vIob4= +github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ= +github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82 h1:MG93+PZYs9PyEsj/n5/haQu2gK0h4tUtSy9ejtMwWa0= +github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82/go.mod h1:GbuBk21JqF+driLX3XtJYNZjGa45YDoa9IqCTzNSfEc= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2 h1:2be4ykKKov3M1yISM2E8gnGXZ/N2SsPawfnGiXxaYEU= +github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/btcsuite/btcd v0.0.0-20190629003639-c26ffa870fd8 h1:mOg8/RgDSHTQ1R0IR+LMDuW4TDShPv+JzYHuR4GLoNA= +github.com/btcsuite/btcd v0.0.0-20190629003639-c26ffa870fd8/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= +github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3 h1:A/EVblehb75cUgXA5njHPn0kLAsykn6mJGz7rnmW5W0= +github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= +github.com/btcsuite/btcd v0.20.0-beta h1:DnZGUjFbRkpytojHWwy6nfUSA7vFrzWXDLpFNzt74ZA= +github.com/btcsuite/btcd v0.20.0-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcwallet v0.0.0-20190911065739-d5cdeb4b91b0/go.mod h1:ntLqUbZ12G8FmPX1nJj7W83WiAFOLRGiuarH4zDYdlI= +github.com/btcsuite/btcwallet v0.10.0 h1:fFZncfYJ7VByePTGttzJc3qfCyDzU95ucZYk0M912lU= +github.com/btcsuite/btcwallet v0.10.0/go.mod h1:4TqBEuceheGNdeLNrelliLHJzmXauMM2vtWfuy1pFiM= +github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0 h1:KGHMW5sd7yDdDMkCZ/JpP0KltolFsQcB973brBnfj4c= +github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0/go.mod h1:VufDts7bd/zs3GV13f/lXc/0lXrPnvxD/NvmpG/FEKU= +github.com/btcsuite/btcwallet/wallet/txrules v1.0.0 h1:2VsfS0sBedcM5KmDzRMT3+b6xobqWveZGvjb+jFez5w= +github.com/btcsuite/btcwallet/wallet/txrules v1.0.0/go.mod h1:UwQE78yCerZ313EXZwEiu3jNAtfXj2n2+c8RWiE/WNA= +github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0 h1:6DxkcoMnCPY4E9cUDPB5tbuuf40SmmMkSQkoE8vCT+s= +github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0/go.mod h1:pauEU8UuMFiThe5PB3EO+gO5kx87Me5NvdQDsTuq6cs= +github.com/btcsuite/btcwallet/walletdb v1.0.0 h1:mheT7vCWK5EP6rZzhxsQ7ms9+yX4VE8bwiJctECBeNw= +github.com/btcsuite/btcwallet/walletdb v1.0.0/go.mod h1:bZTy9RyYZh9fLnSua+/CD48TJtYJSHjjYcSaszuxCCk= +github.com/btcsuite/btcwallet/walletdb v1.1.0 h1:JHAL7wZ8pX4SULabeAv/wPO9sseRWMGzE80lfVmRw6Y= +github.com/btcsuite/btcwallet/walletdb v1.1.0/go.mod h1:bZTy9RyYZh9fLnSua+/CD48TJtYJSHjjYcSaszuxCCk= +github.com/btcsuite/btcwallet/wtxmgr v1.0.0 h1:aIHgViEmZmZfe0tQQqF1xyd2qBqFWxX5vZXkkbjtbeA= +github.com/btcsuite/btcwallet/wtxmgr v1.0.0/go.mod h1:vc4gBprll6BP0UJ+AIGDaySoc7MdAmZf8kelfNb8CFY= +github.com/btcsuite/fastsha256 v0.0.0-20160815193821-637e65642941 h1:kij1x2aL7VE6gtx8KMIt8PGPgI5GV9LgtHFG5KaEMPY= +github.com/btcsuite/fastsha256 v0.0.0-20160815193821-637e65642941/go.mod h1:QcFA8DZHtuIAdYKCq/BzELOaznRsCvwf4zTPmaYwaig= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8/go.mod h1:tYvUd8KLhm/oXvUeSEs2VlLghFjQt9+ZaF9ghH0JNjc= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0 h1:Tvd0BfvqX9o823q1j2UZ/epQo09eJh6dTcRp79ilIN4= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJGQE= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY= +github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= +github.com/frankban/quicktest v1.2.2 h1:xfmOhhoH5fGPgbEAlhLpJH9p0z/0Qizio9osmvn9IUY= +github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v0.0.0-20170724004829-f2862b476edc/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/grpc-ecosystem/grpc-gateway v1.10.0 h1:yqx/nTDLC6pVrQ8fTaCeeeMJNbmt7HglUpysQATYXV4= +github.com/grpc-ecosystem/grpc-gateway v1.10.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jackpal/gateway v1.0.5 h1:qzXWUJfuMdlLMtt0a3Dgt+xkWQiA5itDEITVJtuSwMc= +github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA= +github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad h1:heFfj7z0pGsNCekUlsFhO2jstxO4b5iQ665LjwM5mDc= +github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c h1:3UvYABOQRhJAApj9MdCN+Ydv841ETSoy6xLzdmmr/9A= +github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= +github.com/juju/errors v0.0.0-20190806202954-0232dcc7464d h1:hJXjZMxj0SWlMoQkzeZDLi2cmeiWKa7y1B8Rg+qaoEc= +github.com/juju/errors v0.0.0-20190806202954-0232dcc7464d/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= +github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 h1:UUHMLvzt/31azWTN/ifGWef4WUqvXk0iRqdhdy/2uzI= +github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= +github.com/juju/retry v0.0.0-20180821225755-9058e192b216 h1:/eQL7EJQKFHByJe3DeE8Z36yqManj9UY5zppDoQi4FU= +github.com/juju/retry v0.0.0-20180821225755-9058e192b216/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= +github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2 h1:Pp8RxiF4rSoXP9SED26WCfNB28/dwTDpPXS8XMJR8rc= +github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= +github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d h1:irPlN9z5VCe6BTsqVsxheCZH99OFSmqSVyTigW4mEoY= +github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= +github.com/juju/version v0.0.0-20180108022336-b64dbd566305 h1:lQxPJ1URr2fjsKnJRt/BxiIxjLt9IKGvS+0injMHbag= +github.com/juju/version v0.0.0-20180108022336-b64dbd566305/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec h1:n1NeQ3SgUHyISrjFFoO5dR748Is8dBL9qpaTNfphQrs= +github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lightninglabs/gozmq v0.0.0-20190710231225-cea2a031735d h1:tt8hwvxl6fksSfchjBGaWu+pnWJQfG1OWiCM20qOSAE= +github.com/lightninglabs/gozmq v0.0.0-20190710231225-cea2a031735d/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= +github.com/lightninglabs/loop v0.2.3-alpha h1:bAujEe1V/pv3VounArjXibTSVJ6myXSl3PUwQFOs3To= +github.com/lightninglabs/loop v0.2.3-alpha/go.mod h1:n/8uTYPcWrU12xAQmUvjvfxKTFWSRNuYr5dTuAxImi0= +github.com/lightninglabs/neutrino v0.0.0-20190906012717-f087198de655 h1:/EpOX/6QvD5CdoAfMt1yvZeUPjJ8sCiHv6CRNG2lEuY= +github.com/lightninglabs/neutrino v0.0.0-20190906012717-f087198de655/go.mod h1:awTrhbCWjWNH4yVwZ4IE7nZbvpQ27e7OyD+jao7wRxA= +github.com/lightninglabs/neutrino v0.10.0 h1:yWVy2cOCCXbKFdpYCE9vD1fWRJDd9FtGXhUws4l9RkU= +github.com/lightninglabs/neutrino v0.10.0/go.mod h1:C3KhCMk1Mcx3j8v0qRVWM1Ow6rIJSvSPnUAq00ZNAfk= +github.com/lightningnetwork/lightning-onion v0.0.0-20190909101754-850081b08b6a h1:GoWPN4i4jTKRxhVNh9a2vvBBO1Y2seiJB+SopUYoKyo= +github.com/lightningnetwork/lightning-onion v0.0.0-20190909101754-850081b08b6a/go.mod h1:rigfi6Af/KqsF7Za0hOgcyq2PNH4AN70AaMRxcJkff4= +github.com/lightningnetwork/lnd v0.7.1-beta-rc2.0.20190914085956-35027e52fc22/go.mod h1:VaY0b5o38keUN3Ga6GVb/Mgta4B/CcCXwNvPAvhbv/A= +github.com/lightningnetwork/lnd v0.8.0-beta-rc3.0.20191029004703-c069bdd4c7c1 h1:HZqM9i0znXr+FZAO1Km7bpnlUFt+/qbfFDkfOEDT6Gc= +github.com/lightningnetwork/lnd v0.8.0-beta-rc3.0.20191029004703-c069bdd4c7c1/go.mod h1:nq06y2BDv7vwWeMmwgB7P3pT7/Uj7sGf5FzHISVD6t4= +github.com/lightningnetwork/lnd/queue v1.0.1 h1:jzJKcTy3Nj5lQrooJ3aaw9Lau3I0IwvQR5sqtjdv2R0= +github.com/lightningnetwork/lnd/queue v1.0.1/go.mod h1:vaQwexir73flPW43Mrm7JOgJHmcEFBWWSl9HlyASoms= +github.com/lightningnetwork/lnd/ticker v1.0.0 h1:S1b60TEGoTtCe2A0yeB+ecoj/kkS4qpwh6l+AkQEZwU= +github.com/lightningnetwork/lnd/ticker v1.0.0/go.mod h1:iaLXJiVgI1sPANIF2qYYUJXjoksPNvGNYowB8aRbpX0= +github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw= +github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796/go.mod h1:3p7ZTf9V1sNPI5H8P3NkTFF4LuwMdPl2DodF60qAKqY= +github.com/ltcsuite/ltcutil v0.0.0-20181217130922-17f3b04680b6/go.mod h1:8Vg/LTOO0KYa/vlHWJ6XZAevPQThGH5sufO0Hrou/lA= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8 h1:PRMAcldsl4mXKJeRNB/KVNz6TlbS6hk2Rs42PqgU3Ws= +github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 h1:tcJ6OjwOMvExLlzrAVZute09ocAGa7KqOON60++Gz4E= +github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY= +github.com/urfave/cli v1.18.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472 h1:Gv7RPwsi3eZ2Fgewe3CBsuOebPwO27PoXzRpJPsvSSM= +golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd h1:DBH9mDw0zluJT/R+nGuV3jWFWLFaHyYZWD4tOT+cjn0= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v1 v1.0.1 h1:oQFRXzZ7CkBGdm1XZm/EbQYaYNNEElNBOd09M6cqNso= +gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/macaroon-bakery.v2 v2.0.1/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETfr3S9MbIA= +gopkg.in/macaroon-bakery.v2 v2.1.0 h1:9Jw/+9XHBSutkaeVpWhDx38IcSNLJwWUICkOK98DHls= +gopkg.in/macaroon-bakery.v2 v2.1.0/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETfr3S9MbIA= +gopkg.in/macaroon.v2 v2.0.0/go.mod h1:+I6LnTMkm/uV5ew/0nsulNjL16SK4+C8yDmRUzHR17I= +gopkg.in/macaroon.v2 v2.1.0 h1:HZcsjBCzq9t0eBPMKqTN/uSN6JOm78ZJ2INbqcBQOUI= +gopkg.in/macaroon.v2 v2.1.0/go.mod h1:OUb+TQP/OP0WOerC2Jp/3CwhIKyIa9kQjuc7H24e6/o= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/kirin.go b/kirin.go new file mode 100644 index 0000000..47e396c --- /dev/null +++ b/kirin.go @@ -0,0 +1,132 @@ +package kirin + +import ( + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + + "github.com/lightninglabs/kirin/auth" + "github.com/lightninglabs/kirin/proxy" + "github.com/lightningnetwork/lnd/build" + "github.com/lightningnetwork/lnd/lnrpc" + "gopkg.in/yaml.v2" +) + +// Main is the true entrypoint of Kirin. +func Main() { + // TODO: Prevent from running twice. + err := start() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +// start sets up the proxy server and runs it. This function blocks until a +// shutdown signal is received. +func start() error { + // First, parse configuration file and set up logging. + configFile := filepath.Join(kirinDataDir, defaultConfigFilename) + cfg, err := getConfig(configFile) + if err != nil { + return fmt.Errorf("unable to parse config file: %v", err) + } + err = setupLogging(cfg) + if err != nil { + return fmt.Errorf("unable to set up logging: %v", err) + } + + // Create the proxy and connect it to lnd. + genInvoiceReq := func() (*lnrpc.Invoice, error) { + return &lnrpc.Invoice{ + Memo: "LSAT", + Value: 1, + }, nil + } + servicesProxy, err := createProxy(cfg, genInvoiceReq) + server := &http.Server{ + Addr: cfg.ListenAddr, + Handler: http.HandlerFunc(servicesProxy.ServeHTTP), + } + tlsKeyFile := filepath.Join(kirinDataDir, defaultTLSKeyFilename) + tlsCertFile := filepath.Join(kirinDataDir, defaultTLSCertFilename) + + // The ListenAndServeTLS below will block until shut down or an error + // occurs. So we can just defer a cleanup function here that will close + // everything on shutdown. + defer cleanup(server) + + // Finally start the server. + log.Infof("Starting the server, listening on %s.", cfg.ListenAddr) + return server.ListenAndServeTLS(tlsCertFile, tlsKeyFile) +} + +// getConfig loads and parses the configuration file then checks it for valid +// content. +func getConfig(configFile string) (*config, error) { + cfg := &config{} + b, err := ioutil.ReadFile(configFile) + if err != nil { + return nil, err + } + err = yaml.Unmarshal(b, cfg) + if err != nil { + return nil, err + } + + // Then check the configuration that we got from the config file, all + // required values need to be set at this point. + if cfg.ListenAddr == "" { + return nil, fmt.Errorf("missing listen address for server") + } + return cfg, nil +} + +// setupLogging parses the debug level and initializes the log file rotator. +func setupLogging(cfg *config) error { + if cfg.DebugLevel == "" { + cfg.DebugLevel = defaultLogLevel + } + + // Now initialize the logger and set the log level. + logFile := filepath.Join(kirinDataDir, defaultLogFilename) + err := logWriter.InitLogRotator( + logFile, defaultMaxLogFileSize, defaultMaxLogFiles, + ) + if err != nil { + return err + } + return build.ParseAndSetDebugLevels(cfg.DebugLevel, logWriter) +} + +// createProxy creates the proxy with all the services it needs. +func createProxy(cfg *config, genInvoiceReq InvoiceRequestGenerator) ( + *proxy.Proxy, error) { + + challenger, err := NewLndChallenger( + cfg.Authenticator, genInvoiceReq, + ) + if err != nil { + return nil, err + } + authenticator, err := auth.NewLsatAuthenticator(challenger) + if err != nil { + return nil, err + } + return proxy.New(authenticator, cfg.Services, cfg.StaticRoot) +} + +// cleanup closes the given server and shuts down the log rotator. +func cleanup(server *http.Server) { + 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) + } +} diff --git a/log.go b/log.go new file mode 100644 index 0000000..cf1cca8 --- /dev/null +++ b/log.go @@ -0,0 +1,38 @@ +package kirin + +import ( + "github.com/btcsuite/btclog" + "github.com/lightninglabs/kirin/auth" + "github.com/lightninglabs/kirin/proxy" + "github.com/lightningnetwork/lnd/build" +) + +var ( + logWriter = build.NewRotatingLogWriter() + + log = build.NewSubLogger("MAIN", logWriter.GenSubLogger) +) + +func init() { + setSubLogger("MAIN", log, nil) + addSubLogger("AUTH", auth.UseLogger) + addSubLogger("PRXY", proxy.UseLogger) +} + +// addSubLogger is a helper method to conveniently create and register the +// logger of a sub system. +func addSubLogger(subsystem string, useLogger func(btclog.Logger)) { + logger := build.NewSubLogger(subsystem, logWriter.GenSubLogger) + setSubLogger(subsystem, logger, useLogger) +} + +// setSubLogger is a helper method to conveniently register the logger of a sub +// system. +func setSubLogger(subsystem string, logger btclog.Logger, + useLogger func(btclog.Logger)) { + + logWriter.RegisterSubLogger(subsystem, logger) + if useLogger != nil { + useLogger(logger) + } +} diff --git a/macaroons/service.go b/macaroons/service.go new file mode 100644 index 0000000..b253b4f --- /dev/null +++ b/macaroons/service.go @@ -0,0 +1,121 @@ +package macaroons + +import ( + "context" + "encoding/hex" + + "github.com/lightningnetwork/lnd/macaroons" + "gopkg.in/macaroon-bakery.v2/bakery" + "gopkg.in/macaroon-bakery.v2/bakery/checkers" + "gopkg.in/macaroon.v2" +) + +const ( + CondRHash = "r-hash" +) + +var ( + rootKey = "aabbccddeeff00112233445566778899" + rootKeyId = []byte("0") +) + +type rootKeyStore struct{} + +func (r *rootKeyStore) Get(_ context.Context, id []byte) ([]byte, error) { + return hex.DecodeString(rootKey) +} + +func (r *rootKeyStore) RootKey(_ context.Context) (rootKey, id []byte, + err error) { + + key, err := r.Get(nil, rootKeyId) + if err != nil { + return nil, nil, err + } + return key, rootKeyId, nil +} + +type Service struct { + bakery.Bakery +} + +func (s *Service) NewMacaroon(operations []bakery.Op, caveats []string) ( + []byte, error) { + + ctx := context.Background() + mac, err := s.Oven.NewMacaroon( + ctx, bakery.LatestVersion, nil, operations..., + ) + if err != nil { + return nil, err + } + + // Add all first party caveats before serializing the macaroon. + for _, caveat := range caveats { + err := mac.M().AddFirstPartyCaveat([]byte(caveat)) + if err != nil { + return nil, err + } + } + macBytes, err := mac.M().MarshalBinary() + if err != nil { + return nil, err + } + return macBytes, nil +} + +func (s *Service) ValidateMacaroon(macBytes []byte, + requiredPermissions []bakery.Op) error { + + mac := &macaroon.Macaroon{} + err := mac.UnmarshalBinary(macBytes) + if err != nil { + return err + } + + // Check the method being called against the permitted operation and + // the expiration time and IP address and return the result. + authChecker := s.Checker.Auth(macaroon.Slice{mac}) + _, err = authChecker.Allow(context.Background(), requiredPermissions...) + return err +} + +func NewService(checks ...macaroons.Checker) (*Service, error) { + macaroonParams := bakery.BakeryParams{ + Location: "kirin", + RootKeyStore: &rootKeyStore{}, + Locator: nil, + Key: nil, + } + + svc := bakery.New(macaroonParams) + + // Register all custom caveat checkers with the bakery's checker. + checker := svc.Checker.FirstPartyCaveatChecker.(*checkers.Checker) + for _, check := range checks { + cond, fun := check() + if !isRegistered(checker, cond) { + checker.Register(cond, "std", fun) + } + } + + return &Service{*svc}, nil +} + +// isRegistered checks to see if the required checker has already been +// registered in order to avoid a panic caused by double registration. +func isRegistered(c *checkers.Checker, name string) bool { + if c == nil { + return false + } + + for _, info := range c.Info() { + if info.Name == name && + info.Prefix == "" && + info.Namespace == "std" { + return true + } + } + + return false +} diff --git a/proxy/log.go b/proxy/log.go new file mode 100644 index 0000000..89bc0cb --- /dev/null +++ b/proxy/log.go @@ -0,0 +1,93 @@ +package proxy + +import ( + "fmt" + "net" + + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" +) + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger("PRXY", nil)) +} + +// DisableLog disables all library log output. Logging output is disabled +// by default until UseLogger is called. +func DisableLog() { + UseLogger(btclog.Disabled) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} + +// PrefixLog logs with a given static string prefix. +type PrefixLog struct { + logger btclog.Logger + prefix string +} + +// NewRemoteIPPrefixLog returns a new prefix logger that logs the remote IP +// address. +func NewRemoteIPPrefixLog(logger btclog.Logger, remoteAddr string) (net.IP, + *PrefixLog) { + + remoteHost, _, err := net.SplitHostPort(remoteAddr) + if err != nil { + remoteHost = "0.0.0.0" + } + remoteIp := net.ParseIP(remoteHost) + if remoteIp == nil { + remoteIp = net.IPv4zero + } + return remoteIp, &PrefixLog{ + logger: logger, + prefix: remoteIp.String(), + } +} + +// Debugf formats message according to format specifier and writes to +// log with LevelDebug. +func (s *PrefixLog) Debugf(format string, params ...interface{}) { + s.logger.Debugf( + fmt.Sprintf("%s %s", s.prefix, format), + params..., + ) +} + +// Infof formats message according to format specifier and writes to +// log with LevelInfo. +func (s *PrefixLog) Infof(format string, params ...interface{}) { + s.logger.Infof( + fmt.Sprintf("%s %s", s.prefix, format), + params..., + ) +} + +// Warnf formats message according to format specifier and writes to +// to log with LevelError. +func (s *PrefixLog) Warnf(format string, params ...interface{}) { + s.logger.Warnf( + fmt.Sprintf("%s %s", s.prefix, format), + params..., + ) +} + +// Errorf formats message according to format specifier and writes to +// to log with LevelError. +func (s *PrefixLog) Errorf(format string, params ...interface{}) { + s.logger.Errorf( + fmt.Sprintf("%s %s", s.prefix, format), + params..., + ) +} diff --git a/proxy/proxy.go b/proxy/proxy.go new file mode 100644 index 0000000..793413b --- /dev/null +++ b/proxy/proxy.go @@ -0,0 +1,282 @@ +package proxy + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "net/http" + "net/http/httputil" + "regexp" + + "github.com/lightninglabs/kirin/auth" +) + +const ( + // formatPattern is the pattern in which the request log will be + // printed. This is loosely oriented on the apache log format. + // An example entry would look like this: + // 2019-11-09 04:07:55.072 [INF] PRXY: 66.249.69.89 - - + // "GET /availability/v1/btc.json HTTP/1.1" "" "Mozilla/5.0 ..." + formatPattern = "- - \"%s %s %s\" \"%s\" \"%s\"" +) + +// Proxy is a HTTP, HTTP/2 and gRPC handler that takes an incoming request, +// uses its authenticator to validate the request's headers, and either returns +// a challenge to the client or forwards the request to another server and +// proxies the response back to the client. +type Proxy struct { + proxyBackend *httputil.ReverseProxy + staticServer http.Handler + authenticator auth.Authenticator + services []*Service +} + +// New returns a new Proxy instance that proxies between the services specified, +// using the auth to validate each request's headers and get new challenge +// headers if necessary. +func New(auth auth.Authenticator, services []*Service, staticRoot string) ( + *Proxy, error) { + + staticServer := http.FileServer(http.Dir(staticRoot)) + proxy := &Proxy{ + staticServer: staticServer, + authenticator: auth, + services: services, + } + err := proxy.UpdateServices(services) + if err != nil { + return nil, err + } + + return proxy, nil +} + +// ServeHTTP checks a client's headers for appropriate authorization and either +// returns a challenge or forwards their request to the target backend service. +func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Parse and log the remote IP address. We also need the parsed IP + // address for the freebie count. + remoteIp, prefixLog := NewRemoteIPPrefixLog(log, r.RemoteAddr) + logRequest := func() { + prefixLog.Infof(formatPattern, r.Method, r.RequestURI, r.Proto, + r.Referer(), r.UserAgent()) + } + defer logRequest() + + // For OPTIONS requests we only need to set the CORS headers, not serve + // any content; + if r.Method == "OPTIONS" { + addCorsHeaders(w.Header()) + w.WriteHeader(http.StatusOK) + return + } + + // Requests that can't be matched to a service backend will be + // dispatched to the static file server. If the file exists in the + // static file folder it will be served, otherwise the static server + // will return a 404 for us. + target, ok := matchService(r, p.services) + if !ok { + prefixLog.Debugf("Dispatching request %s to static file "+ + "server.", r.URL.Path) + p.staticServer.ServeHTTP(w, r) + return + } + + // Determine auth level required to access service and dispatch request + // accordingly. + switch { + case target.Auth.IsOn(): + if !p.authenticator.Accept(&r.Header) { + prefixLog.Infof("Authentication failed. Sending 402.") + p.handlePaymentRequired(w, r) + return + } + case target.Auth.IsFreebie(): + // We only need to respect the freebie counter if the user + // is not authenticated at all. + if !p.authenticator.Accept(&r.Header) { + ok, err := target.freebieDb.CanPass(r, remoteIp) + if err != nil { + prefixLog.Errorf("Error querying freebie db: "+ + "%v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + if !ok { + p.handlePaymentRequired(w, r) + return + } + _, err = target.freebieDb.TallyFreebie(r, remoteIp) + if err != nil { + prefixLog.Errorf("Error updating freebie db: "+ + "%v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + } + case target.Auth.IsOff(): + } + + // If we got here, it means everything is OK to pass the request to the + // service backend via the reverse proxy. + p.proxyBackend.ServeHTTP(w, r) +} + +// UpdateServices re-configures the proxy to use a new set of backend services. +func (p *Proxy) UpdateServices(services []*Service) error { + err := prepareServices(services) + if err != nil { + return err + } + + certPool, err := certPool(services) + if err != nil { + return err + } + transport := &http.Transport{ + ForceAttemptHTTP2: true, + TLSClientConfig: &tls.Config{ + RootCAs: certPool, + InsecureSkipVerify: true, + }, + } + + p.proxyBackend = &httputil.ReverseProxy{ + Director: p.director, + Transport: transport, + ModifyResponse: func(res *http.Response) error { + addCorsHeaders(res.Header) + return nil + }, + + // A negative value means to flush immediately after each write + // to the client. + FlushInterval: -1, + } + + return nil +} + +// director is a method that rewrites an incoming request to be forwarded to a +// backend service. +func (p *Proxy) director(req *http.Request) { + target, ok := matchService(req, p.services) + if ok { + // Rewrite address and protocol in the request so the + // real service is called instead. + req.Host = target.Address + req.URL.Host = target.Address + req.URL.Scheme = target.Protocol + + // Don't forward the authorization header since the + // services won't know what it is. + req.Header.Del("Authorization") + + // Now overwrite header fields of the client request + // with the fields from the configuration file. + for name, value := range target.Headers { + req.Header.Add(name, value) + } + } +} + +// certPool builds a pool of x509 certificates from the backend services. +func certPool(services []*Service) (*x509.CertPool, error) { + cp := x509.NewCertPool() + for _, service := range services { + if service.TLSCertPath == "" { + continue + } + + b, err := ioutil.ReadFile(service.TLSCertPath) + if err != nil { + return nil, err + } + + if !cp.AppendCertsFromPEM(b) { + return nil, fmt.Errorf("credentials: failed to " + + "append certificate") + } + } + + return cp, nil +} + +// matchService tries to match a backend service to an HTTP request by regular +// expression matching the host and path. +func matchService(req *http.Request, services []*Service) (*Service, bool) { + for _, service := range services { + hostRegexp := regexp.MustCompile(service.HostRegexp) + if !hostRegexp.MatchString(req.Host) { + log.Tracef("Req host [%s] doesn't match [%s].", + req.Host, hostRegexp) + continue + } + + if service.PathRegexp == "" { + log.Debugf("Host [%s] matched pattern [%s] and path "+ + "expression is empty. Using service [%s].", + req.Host, hostRegexp, service.Address) + return service, true + } + + pathRegexp := regexp.MustCompile(service.PathRegexp) + if !pathRegexp.MatchString(req.URL.Path) { + log.Tracef("Req path [%s] doesn't match [%s].", + req.URL.Path, pathRegexp) + continue + } + + log.Debugf("Host [%s] matched pattern [%s] and path [%s] "+ + "matched [%s]. Using service [%s].", + req.Host, hostRegexp, req.URL.Path, pathRegexp, + service.Address) + return service, true + } + log.Errorf("No backend service matched request [%s%s].", req.Host, + req.URL.Path) + return nil, false +} + +// addCorsHeaders adds HTTP header fields that are required for Cross Origin +// Resource Sharing. These header fields are needed to signal to the browser +// that it's ok to allow requests to sub domains, even if the JS was served from +// the top level domain. +func addCorsHeaders(header http.Header) { + log.Debugf("Adding CORS headers to response.") + + header.Add("Access-Control-Allow-Origin", "*") + header.Add("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + header.Add("Access-Control-Expose-Headers", "WWW-Authenticate") + header.Add( + "Access-Control-Allow-Headers", + "Authorization, Grpc-Metadata-macaroon, WWW-Authenticate", + ) +} + +// handlePaymentRequired returns fresh challenge header fields and status code +// to the client signaling that a payment is required to fulfil the request. +func (p *Proxy) handlePaymentRequired(w http.ResponseWriter, r *http.Request) { + addCorsHeaders(r.Header) + + header, err := p.authenticator.FreshChallengeHeader(r) + if err != nil { + log.Errorf("Error creating new challenge header, response 500.") + w.WriteHeader(http.StatusInternalServerError) + return + } + + for name, value := range header { + w.Header().Set(name, value[0]) + for i := 1; i < len(value); i++ { + w.Header().Add(name, value[i]) + } + } + + w.WriteHeader(http.StatusPaymentRequired) + if _, err := w.Write([]byte("payment required")); err != nil { + log.Errorf("Error writing response: %v", err) + } +} diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go new file mode 100644 index 0000000..411f4d0 --- /dev/null +++ b/proxy/proxy_test.go @@ -0,0 +1,112 @@ +package proxy_test + +import ( + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + "time" + + "github.com/lightninglabs/kirin/auth" + "github.com/lightninglabs/kirin/proxy" +) + +const ( + testAddr = "localhost:10019" + testHostRegexp = "^localhost:.*$" + testPathRegexp = "^/grpc/.*$" + testTargetServiceAddress = "localhost:8082" + testHTTPResponseBody = "HTTP Hello" +) + +func TestProxy(t *testing.T) { + // Create a list of services to proxy between. + services := []*proxy.Service{{ + Address: testTargetServiceAddress, + HostRegexp: testHostRegexp, + PathRegexp: testPathRegexp, + Protocol: "http", + }} + + auth := auth.NewMockAuthenticator() + proxy, err := proxy.New(auth, services, "static") + if err != nil { + t.Fatalf("failed to create new proxy: %v", err) + } + + // Start server that gives requests to the proxy. + server := &http.Server{ + Addr: testAddr, + Handler: http.HandlerFunc(proxy.ServeHTTP), + } + + go func() { + if err := server.ListenAndServe(); err != nil { + t.Fatalf("failed to serve to proxy: %v", err) + } + }() + + // Start the target backend service. + go func() { + if err := startHTTPHello(); err != nil { + t.Fatalf("failed to start backend service: %v", err) + } + }() + + // Wait for servers to start. + time.Sleep(100 * time.Millisecond) + + // Test making a request to the backend service without the + // Authorization header set. + client := &http.Client{} + url := fmt.Sprintf("http://%s/grpc/test", testAddr) + resp, err := client.Get(url) + if err != nil { + t.Fatalf("errored making http request: %v", err) + } + + if resp.Status != "402 Payment Required" { + t.Fatalf("expected 402 status code, got: %v", resp.Status) + } + + authHeader := resp.Header.Get("Www-Authenticate") + if !strings.Contains(authHeader, "LSAT") { + t.Fatalf("expected partial LSAT in response header, got: %v", + authHeader) + } + + // Make sure that if the Auth header is set, the client's request is + // proxied to the backend service. + req, err := http.NewRequest("GET", url, nil) + req.Header.Add("Authorization", "foobar") + + resp, err = client.Do(req) + if err != nil { + t.Fatalf("errored making http request: %v", err) + } + + if resp.Status != "200 OK" { + t.Fatalf("expected 200 OK status code, got: %v", resp.Status) + } + + // Ensure that we got the response body we expect. + defer resp.Body.Close() + bodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("failed to read response body: %v", err) + } + + if string(bodyBytes) != testHTTPResponseBody { + t.Fatalf("expected response body %v, got %v", + testHTTPResponseBody, string(bodyBytes)) + } +} + +func startHTTPHello() error { + sayHello := func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(testHTTPResponseBody)) + } + http.HandleFunc("/", sayHello) + return http.ListenAndServe(testTargetServiceAddress, nil) +} diff --git a/proxy/service.go b/proxy/service.go new file mode 100644 index 0000000..e74218a --- /dev/null +++ b/proxy/service.go @@ -0,0 +1,110 @@ +package proxy + +import ( + "encoding/base64" + "encoding/hex" + "fmt" + "io/ioutil" + "strings" + + "github.com/lightninglabs/kirin/auth" + "github.com/lightninglabs/kirin/freebie" +) + +var ( + filePrefix = "!file" + filePrefixHex = filePrefix + "+hex" + filePrefixBase64 = filePrefix + "+base64" +) + +// Service generically specifies configuration data for backend services to the +// Kirin proxy. +type Service struct { + // TLSCertPath is the optional path to the service's TLS certificate. + TLSCertPath string `long:"tlscertpath" description:"Path to the service's TLS certificate"` + + // Address is the service's IP address and port. + Address string `long:"address" description:"service instance rpc address"` + + // Protocol is the protocol that should be used to connect to the + // service. Currently supported is http and https. + Protocol string `long:"protocol" description:"service instance protocol"` + + // Auth is the authentication level required for this service to be + // accessed. Valid values are "on" for full authentication, "freebie X" + // for X free requests per IP address before authentication is required + // or "off" for no authentication. + Auth auth.Level `long:"auth" description:"required authentication"` + + // HostRegexp is a regular expression that is tested against the 'Host' + // HTTP header field to find out if this service should be used. + HostRegexp string `long:"hostregexp" description:"Regular expression to match the host against"` + + // PathRegexp is a regular expression that is tested against the path + // of the URL of a request to find out if this service should be used. + PathRegexp string `long:"pathregexp" description:"Regular expression to match the path of the URL against"` + + // Headers is a map of strings that defines header name and values that + // should always be passed to the backend service, overwriting any + // headers with the same name that might have been set by the client + // request. + // If the value of a header field starts with the prefix "!file+hex:", + // the rest of the value is treated as a path to a file and the content + // of that file is sent to the backend with each call (hex encoded). + // If the value starts with the prefix "!file+base64:", the content of + // the file is sent encoded as base64. + Headers map[string]string `long:"headers" description:"Header fields to always pass to the service"` + + freebieDb freebie.DB +} + +// prepareServices prepares the backend service configurations to be used by the +// proxy. +func prepareServices(services []*Service) error { + for _, service := range services { + // Each freebie enabled service gets its own store. + if service.Auth.IsFreebie() { + service.freebieDb = freebie.NewMemIpMaskStore( + service.Auth.FreebieCount(), + ) + } + + // Replace placeholders/directives in the header fields with the + // actual desired values. + for key, value := range service.Headers { + if !strings.HasPrefix(value, filePrefix) { + continue + } + + parts := strings.Split(value, ":") + if len(parts) != 2 { + return fmt.Errorf("invalid header config, " + + "must be '!file+hex:path'") + } + prefix, fileName := parts[0], parts[1] + bytes, err := ioutil.ReadFile(fileName) + if err != nil { + return err + } + + // There are two supported formats to encode the file + // content in: hex and base64. + switch { + case prefix == filePrefixHex: + newValue := hex.EncodeToString(bytes) + service.Headers[key] = newValue + + case prefix == filePrefixBase64: + newValue := base64.StdEncoding.EncodeToString( + bytes, + ) + service.Headers[key] = newValue + + default: + return fmt.Errorf("unsupported file prefix "+ + "format %s", value) + } + } + } + return nil +} diff --git a/sample-conf.yaml b/sample-conf.yaml new file mode 100644 index 0000000..bcf9cd0 --- /dev/null +++ b/sample-conf.yaml @@ -0,0 +1,22 @@ +listenaddr: "localhost:8081" +staticroot: "./static" +debuglevel: "debug" + +services: + # List of services that should be reachable behind the proxy. + # Requests will be matched to the services in order, picking the first + # that satisfies hostregexp and (if set) pathregexp. + # So order is important! + # + # Use single quotes for regular expressions with special characters in them to + # avoid YAML parsing errors! + - hostregexp: '^service1.com$' + pathregexp: '^/.*$' + address: "127.0.0.1:10009" + protocol: https + tlscertpath: "path-to-optional-tls-cert/tls.cert" + + - hostregexp: "service2.com:8083" + pathregexp: '^/.*$' + address: "123.456.789:8082" + protocol: https diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..7350b32 --- /dev/null +++ b/static/index.html @@ -0,0 +1,125 @@ + + + LSAT proxy demo page + + + +
+
+

LND node info

+

+        
+    
+
+

Bos Scores

+

+        
+        
+        
+    
+
+ + + + + \ No newline at end of file