mirror of
https://github.com/lightninglabs/aperture.git
synced 2026-01-08 20:04:23 +01:00
Merge pull request #9 from guggero/proxy-demo
initial proxy with freebie DB and lnd backend
This commit is contained in:
9
INSTALL.md
Normal file
9
INSTALL.md
Normal file
@@ -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`
|
||||
57
README.md
Normal file
57
README.md
Normal file
@@ -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 <macaroon>:<preimage>" \
|
||||
https://test-staging.swap.lightning.today:8081/availability/v1/btc.json
|
||||
```
|
||||
132
auth/authenticator.go
Normal file
132
auth/authenticator.go
Normal file
@@ -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
|
||||
}
|
||||
41
auth/config.go
Normal file
41
auth/config.go
Normal file
@@ -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"
|
||||
}
|
||||
26
auth/interface.go
Normal file
26
auth/interface.go
Normal file
@@ -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)
|
||||
}
|
||||
29
auth/log.go
Normal file
29
auth/log.go
Normal file
@@ -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
|
||||
}
|
||||
28
auth/mock_authenticator.go
Normal file
28
auth/mock_authenticator.go
Normal file
@@ -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
|
||||
}
|
||||
74
challenger.go
Normal file
74
challenger.go
Normal file
@@ -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
|
||||
}
|
||||
7
cmd/kirin/main.go
Normal file
7
cmd/kirin/main.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "github.com/lightninglabs/kirin"
|
||||
|
||||
func main() {
|
||||
kirin.Main()
|
||||
}
|
||||
48
config.go
Normal file
48
config.go
Normal file
@@ -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."`
|
||||
}
|
||||
15
freebie/interface.go
Normal file
15
freebie/interface.go
Normal file
@@ -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)
|
||||
}
|
||||
50
freebie/mem_store.go
Normal file
50
freebie/mem_store.go
Normal file
@@ -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),
|
||||
}
|
||||
}
|
||||
13
go.mod
Normal file
13
go.mod
Normal file
@@ -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
|
||||
)
|
||||
286
go.sum
Normal file
286
go.sum
Normal file
@@ -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=
|
||||
132
kirin.go
Normal file
132
kirin.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
38
log.go
Normal file
38
log.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
121
macaroons/service.go
Normal file
121
macaroons/service.go
Normal file
@@ -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
|
||||
}
|
||||
93
proxy/log.go
Normal file
93
proxy/log.go
Normal file
@@ -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...,
|
||||
)
|
||||
}
|
||||
282
proxy/proxy.go
Normal file
282
proxy/proxy.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
112
proxy/proxy_test.go
Normal file
112
proxy/proxy_test.go
Normal file
@@ -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)
|
||||
}
|
||||
110
proxy/service.go
Normal file
110
proxy/service.go
Normal file
@@ -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
|
||||
}
|
||||
22
sample-conf.yaml
Normal file
22
sample-conf.yaml
Normal file
@@ -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
|
||||
125
static/index.html
Normal file
125
static/index.html
Normal file
@@ -0,0 +1,125 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>LSAT proxy demo page</title>
|
||||
<style>
|
||||
.row:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.col {
|
||||
width: 45%;
|
||||
float: left;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.max-height-scroll {
|
||||
max-height: 500px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap; /* Since CSS 2.1 */
|
||||
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
||||
white-space: -o-pre-wrap; /* Opera 7 */
|
||||
word-wrap: break-word; /* Internet Explorer 5.5+ */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>LND node info</h1>
|
||||
<pre id="getinfo-lnd"></pre>
|
||||
<button id="reload-lnd">Reload</button>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h1>Bos Scores</h1>
|
||||
<pre id="bos-scores" class="max-height-scroll"></pre>
|
||||
<button id="reload-bos">Reload</button>
|
||||
<button id="pay">Pay invoice with Joule</button>
|
||||
<button id="add-preimage">Paste preimage of manual payment</button>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
|
||||
<script src="https://unpkg.com/webln@0.2.1/dist/webln.min.js"
|
||||
integrity="sha384-Enk2tnv6U0yPoFk7RasscZ5oQIG2fzVYaG4ledkAf7MdEXP9fMazV74tAgEwvxm7"
|
||||
crossorigin="anonymous"></script>
|
||||
<script>
|
||||
|
||||
let authorization = "";
|
||||
let lastMacaroon = null;
|
||||
let lastInvoice = null;
|
||||
|
||||
function parseInvoice(invoice) {
|
||||
const rex = /LSAT macaroon='(.*?)' invoice='(.*?)'/i;
|
||||
parts = invoice.match(rex);
|
||||
lastMacaroon = parts[1];
|
||||
lastInvoice = parts[2];
|
||||
}
|
||||
|
||||
function loadJSON(url, elem) {
|
||||
elem.text("");
|
||||
$.ajax(url, {
|
||||
cache: false,
|
||||
dataType: 'json',
|
||||
crossDomain: true,
|
||||
headers: {
|
||||
'Authorization': authorization,
|
||||
},
|
||||
success: (data, status) => {
|
||||
elem.text(JSON.stringify(data, null, 2));
|
||||
},
|
||||
statusCode: {
|
||||
402: (xhr, status, err) => {
|
||||
var invoice = xhr.getResponseHeader('www-authenticate');
|
||||
parseInvoice(invoice);
|
||||
elem.text("payment required: " + invoice);
|
||||
}
|
||||
},
|
||||
error: (xhr, status, err) => {
|
||||
console.log("error: " + err + ", headers: " + xhr.getAllResponseHeaders())
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function payInvoice() {
|
||||
if (window.WebLN) {
|
||||
WebLN.requestProvider()
|
||||
.then((provider) => {
|
||||
provider.sendPayment(lastInvoice)
|
||||
.then((response) => {
|
||||
authorization = "LSAT " + lastMacaroon + ":" + response.preimage;
|
||||
$('#reload-bos').click();
|
||||
$('#reload-lnd').click();
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
alert(err);
|
||||
});
|
||||
} else {
|
||||
alert("Joule not installed or failed to load WebLN library.");
|
||||
}
|
||||
}
|
||||
|
||||
function addPreimage() {
|
||||
let preimage = prompt("Enter hex encoded preimage");
|
||||
authorization = "LSAT " + lastMacaroon + ":" + preimage;
|
||||
$('#reload-bos').click();
|
||||
$('#reload-lnd').click();
|
||||
}
|
||||
|
||||
$(document).ready(() => {
|
||||
const host = document.location.host;
|
||||
|
||||
$('#reload-lnd').on('click', () => loadJSON('//alice.' + host + '/v1/getinfo', $('#getinfo-lnd'))).click();
|
||||
$('#reload-bos').on('click', () => loadJSON('//' + host + '/availability/v1/btc.json', $('#bos-scores'))).click();
|
||||
$('#pay').on('click', () => payInvoice());
|
||||
$('#add-preimage').on('click', () => addPreimage());
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user