From 279fa2a64b1e88c77d3176843819701f1668d6df Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Sat, 29 Jun 2019 22:48:42 -0700 Subject: [PATCH 01/13] Initial version of the proxy. Missing: logging, an Authenticator implementing the LSAT HTTP and gRPC protocol. --- INSTALL.md | 9 +++ auth/interface.go | 15 +++++ auth/mock_authenticator.go | 28 +++++++++ cmd/kirin/main.go | 7 +++ config.go | 23 +++++++ go.mod | 15 +++++ go.sum | 96 +++++++++++++++++++++++++++++ kirin.go | 57 ++++++++++++++++++ proxy/proxy.go | 120 +++++++++++++++++++++++++++++++++++++ proxy/proxy_test.go | 106 ++++++++++++++++++++++++++++++++ proxy/service.go | 14 +++++ sample-conf.yaml | 7 +++ 12 files changed, 497 insertions(+) create mode 100644 INSTALL.md create mode 100644 auth/interface.go create mode 100644 auth/mock_authenticator.go create mode 100644 cmd/kirin/main.go create mode 100644 config.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 kirin.go create mode 100644 proxy/proxy.go create mode 100644 proxy/proxy_test.go create mode 100644 proxy/service.go create mode 100644 sample-conf.yaml 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/auth/interface.go b/auth/interface.go new file mode 100644 index 0000000..5954fd0 --- /dev/null +++ b/auth/interface.go @@ -0,0 +1,15 @@ +package auth + +import "net/http" + +// 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) +} 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/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..7686ba5 --- /dev/null +++ b/config.go @@ -0,0 +1,23 @@ +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.crt" +) + +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"` + + // Services is a list of JSON objects in string format, which specify + // each backend service to Kirin. + Services []*proxy.Service `long:"service" description:"JSON configurations for each Kirin backend service."` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2c302ac --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/lightninglabs/kirin + +go 1.13 + +require ( + github.com/btcsuite/btcd v0.0.0-20190629003639-c26ffa870fd8 // indirect + github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/pretty v0.1.0 // indirect + golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 // indirect + google.golang.org/genproto v0.0.0-20190905072037-92dd089d5514 + google.golang.org/grpc v1.23.0 + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gopkg.in/yaml.v2 v2.2.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7320236 --- /dev/null +++ b/go.sum @@ -0,0 +1,96 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +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/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/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/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/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +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/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/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/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/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +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/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.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/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-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/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-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-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-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 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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-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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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-20190905072037-92dd089d5514 h1:oFSK4421fpCKRrpzIpybyBVWyht05NegY9+L/3TLAZs= +google.golang.org/genproto v0.0.0-20190905072037-92dd089d5514/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +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/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/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +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.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-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..6f1f7a5 --- /dev/null +++ b/kirin.go @@ -0,0 +1,57 @@ +package kirin + +import ( + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + + "gopkg.in/yaml.v2" + + "github.com/lightninglabs/kirin/auth" + "github.com/lightninglabs/kirin/proxy" +) + +/** + */ + +// 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) + } +} + +func start() error { + configFile := filepath.Join(kirinDataDir, defaultConfigFilename) + var cfg config + b, err := ioutil.ReadFile(configFile) + if err != nil { + return err + } + + yaml.Unmarshal(b, &cfg) + if cfg.ListenAddr == "" { + return fmt.Errorf("missing listen address for server") + } + + authenticator := auth.NewMockAuthenticator() + servicesProxy, err := proxy.New(*authenticator, cfg.Services) + if err != nil { + return err + } + + // Start the reverse proxy. + server := &http.Server{ + Addr: cfg.ListenAddr, + Handler: http.HandlerFunc(servicesProxy.ServeHTTP), + } + + tlsKeyFile := filepath.Join(kirinDataDir, defaultTLSKeyFilename) + tlsCertFile := filepath.Join(kirinDataDir, defaultTLSCertFilename) + return server.ListenAndServeTLS(tlsCertFile, tlsKeyFile) +} diff --git a/proxy/proxy.go b/proxy/proxy.go new file mode 100644 index 0000000..ae804ed --- /dev/null +++ b/proxy/proxy.go @@ -0,0 +1,120 @@ +package proxy + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "net/http" + "net/http/httputil" + + "github.com/lightninglabs/kirin/auth" +) + +// 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 { + server *httputil.ReverseProxy + + authenticator auth.Authenticator +} + +// 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) (*Proxy, error) { + cp, err := certPool(services) + if err != nil { + return nil, err + } + + tlsConfig := &tls.Config{ + RootCAs: cp, + InsecureSkipVerify: true, + } + transport := &http.Transport{ + ForceAttemptHTTP2: true, + TLSClientConfig: tlsConfig, + } + nameToService := formatServices(services) + grpcProxy := &httputil.ReverseProxy{ + Director: director(auth, nameToService), + Transport: transport, + FlushInterval: -1, + } + + return &Proxy{ + grpcProxy, + auth, + }, nil +} + +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 +} + +func formatServices(servicesList []*Service) map[string]*Service { + services := make(map[string]*Service) + for _, service := range servicesList { + services[service.FQDN] = service + } + + return services +} + +func director(auth auth.Authenticator, services map[string]*Service) func(req *http.Request) { + return func(req *http.Request) { + target := services[req.Host] + if target != nil { + req.URL.Scheme = "http" + if target.TLSCertPath != "" { + req.URL.Scheme = "https" + } + + req.URL.Host = target.Address + } + } +} + +// ServeHTTP checks a client's headers for appropriate authorization and either +// returns a challenge or forwards their request to the target backend service. +func (g *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if !g.authenticator.Accept(&r.Header) { + challengeHeader, err := g.authenticator.FreshChallengeHeader(r) + if err != nil { + w.WriteHeader(500) + return + } + + for name, value := range challengeHeader { + w.Header().Set(name, value[0]) + for i := 1; i < len(value); i++ { + w.Header().Add(name, value[i]) + } + } + + w.WriteHeader(402) + return + } + + g.server.ServeHTTP(w, r) +} diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go new file mode 100644 index 0000000..cbc4e43 --- /dev/null +++ b/proxy/proxy_test.go @@ -0,0 +1,106 @@ +package proxy_test + +import ( + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/lightninglabs/kirin/auth" + "github.com/lightninglabs/kirin/proxy" +) + +const ( + testFQDN = "localhost:10019" + testTargetServiceAddress = "localhost:8082" + testHTTPResponseBody = "HTTP Hello" +) + +func TestProxy(t *testing.T) { + // Create a list of services to proxy between. + services := []*proxy.Service{ + &proxy.Service{ + Address: testTargetServiceAddress, + FQDN: testFQDN, + }, + } + + auth := auth.NewMockAuthenticator() + proxy, err := proxy.New(auth, services) + if err != nil { + t.Fatalf("failed to create new proxy: %v", err) + } + + // Start server that gives requests to the proxy. + server := &http.Server{ + Addr: testFQDN, + 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) + } + }() + + // Test making a request to the backend service without the + // Authorization header set. + client := &http.Client{} + url := fmt.Sprintf("http://%s", testFQDN) + 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..6b217d8 --- /dev/null +++ b/proxy/service.go @@ -0,0 +1,14 @@ +package proxy + +// 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:"lnd instance rpc address"` + + // FQDN is the FQDN of the service. + FQDN string `long:"fqdn" description:"FQDN of the service."` +} diff --git a/sample-conf.yaml b/sample-conf.yaml new file mode 100644 index 0000000..b709544 --- /dev/null +++ b/sample-conf.yaml @@ -0,0 +1,7 @@ +listenaddr: "localhost:8081" +services: + - fqdn: "service1.com" + address: "127.0.0.1:10009" + tlscertpath: "path-to-optional-tls-cert/tls.crt" + - fqdn: "service2.com:8083" + address: "123.456.789:8082" From 9552a70cb9805eff18fb02a30cab4f73a315b821 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 1 Nov 2019 16:32:49 +0100 Subject: [PATCH 02/13] proxy: match service with regular expressions --- config.go | 2 +- go.mod | 2 -- go.sum | 34 ---------------------------------- kirin.go | 5 ++++- proxy/proxy.go | 35 ++++++++++++++++++++++++----------- proxy/service.go | 9 +++++++-- sample-conf.yaml | 8 ++++++-- 7 files changed, 42 insertions(+), 53 deletions(-) diff --git a/config.go b/config.go index 7686ba5..88a72c1 100644 --- a/config.go +++ b/config.go @@ -9,7 +9,7 @@ var ( kirinDataDir = btcutil.AppDataDir("kirin", false) defaultConfigFilename = "kirin.yaml" defaultTLSKeyFilename = "tls.key" - defaultTLSCertFilename = "tls.crt" + defaultTLSCertFilename = "tls.cert" ) type config struct { diff --git a/go.mod b/go.mod index 2c302ac..218570f 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,6 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/pretty v0.1.0 // indirect golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 // indirect - google.golang.org/genproto v0.0.0-20190905072037-92dd089d5514 - google.golang.org/grpc v1.23.0 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v2 v2.2.2 ) diff --git a/go.sum b/go.sum index 7320236..163856f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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/btcsuite/btcd v0.0.0-20190629003639-c26ffa870fd8 h1:mOg8/RgDSHTQ1R0IR+LMDuW4TDShPv+JzYHuR4GLoNA= @@ -15,19 +13,13 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku 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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 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/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/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/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/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -47,22 +39,10 @@ golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/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-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-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-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 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -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-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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -70,18 +50,6 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -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-20190905072037-92dd089d5514 h1:oFSK4421fpCKRrpzIpybyBVWyht05NegY9+L/3TLAZs= -google.golang.org/genproto v0.0.0-20190905072037-92dd089d5514/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -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/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= @@ -92,5 +60,3 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD 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-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 index 6f1f7a5..bd7bcc3 100644 --- a/kirin.go +++ b/kirin.go @@ -34,7 +34,10 @@ func start() error { return err } - yaml.Unmarshal(b, &cfg) + err = yaml.Unmarshal(b, &cfg) + if err != nil { + return err + } if cfg.ListenAddr == "" { return fmt.Errorf("missing listen address for server") } diff --git a/proxy/proxy.go b/proxy/proxy.go index ae804ed..05ee73d 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "net/http" "net/http/httputil" + "regexp" "github.com/lightninglabs/kirin/auth" ) @@ -38,9 +39,8 @@ func New(auth auth.Authenticator, services []*Service) (*Proxy, error) { ForceAttemptHTTP2: true, TLSClientConfig: tlsConfig, } - nameToService := formatServices(services) grpcProxy := &httputil.ReverseProxy{ - Director: director(auth, nameToService), + Director: director(auth, services), Transport: transport, FlushInterval: -1, } @@ -72,19 +72,32 @@ func certPool(services []*Service) (*x509.CertPool, error) { return cp, nil } -func formatServices(servicesList []*Service) map[string]*Service { - services := make(map[string]*Service) - for _, service := range servicesList { - services[service.FQDN] = service - } +func matchService(req *http.Request, services []*Service) (*Service, bool) { + for _, service := range services { + hostRegexp := regexp.MustCompile(service.HostRegexp) + if !hostRegexp.MatchString(req.Host) { + continue + } - return services + if service.PathRegexp == "" { + return service, true + } + + urlRegexp := regexp.MustCompile(service.PathRegexp) + if !urlRegexp.MatchString(req.URL.Path) { + continue + } + + return service, true + + } + return nil, false } -func director(auth auth.Authenticator, services map[string]*Service) func(req *http.Request) { +func director(auth auth.Authenticator, services []*Service) func(req *http.Request) { return func(req *http.Request) { - target := services[req.Host] - if target != nil { + target, ok := matchService(req, services) + if ok { req.URL.Scheme = "http" if target.TLSCertPath != "" { req.URL.Scheme = "https" diff --git a/proxy/service.go b/proxy/service.go index 6b217d8..2090200 100644 --- a/proxy/service.go +++ b/proxy/service.go @@ -9,6 +9,11 @@ type Service struct { // Address is the service's IP address and port. Address string `long:"address" description:"lnd instance rpc address"` - // FQDN is the FQDN of the service. - FQDN string `long:"fqdn" description:"FQDN of the service."` + // 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"` } diff --git a/sample-conf.yaml b/sample-conf.yaml index b709544..fc80b1a 100644 --- a/sample-conf.yaml +++ b/sample-conf.yaml @@ -1,7 +1,11 @@ listenaddr: "localhost:8081" +# Use single quotes for regular expressions with special characters in them to +# avoid YAML parsing errors! services: - - fqdn: "service1.com" + - hostregexp: "service1.com" + pathregexp: '^/.*$' address: "127.0.0.1:10009" tlscertpath: "path-to-optional-tls-cert/tls.crt" - - fqdn: "service2.com:8083" + - hostregexp: "service2.com:8083" + pathregexp: '^/.*$' address: "123.456.789:8082" From 4d1fbbf4d66f494b11bc3fec96aca2b6d57c87bf Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 29 Oct 2019 09:24:03 +0100 Subject: [PATCH 03/13] proxy: add static file server --- proxy/proxy.go | 31 +++++++++++++++++++++++++++++-- static/index.html | 5 +++++ 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 static/index.html diff --git a/proxy/proxy.go b/proxy/proxy.go index 05ee73d..ee8cbe6 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -19,6 +19,8 @@ import ( type Proxy struct { server *httputil.ReverseProxy + staticServer http.Handler + authenticator auth.Authenticator } @@ -40,13 +42,20 @@ func New(auth auth.Authenticator, services []*Service) (*Proxy, error) { TLSClientConfig: tlsConfig, } grpcProxy := &httputil.ReverseProxy{ - Director: director(auth, services), - Transport: transport, + Director: director(auth, services), + Transport: transport, + ModifyResponse: func(res *http.Response) error { + addCorsHeaders(res.Header) + return nil + }, FlushInterval: -1, } + staticServer := http.FileServer(http.Dir("static")) + return &Proxy{ grpcProxy, + staticServer, auth, }, nil } @@ -108,9 +117,27 @@ func director(auth auth.Authenticator, services []*Service) func(req *http.Reque } } +func addCorsHeaders(header http.Header) { + header.Add("Access-Control-Allow-Origin", "*") + header.Add("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + header.Add("Access-Control-Allow-Headers", "Authorization, Grpc-Metadata-macaroon") +} + // ServeHTTP checks a client's headers for appropriate authorization and either // returns a challenge or forwards their request to the target backend service. func (g *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" && + (r.URL.Path == "/" || r.URL.Path == "/index.html") { + g.staticServer.ServeHTTP(w, r) + return + } + + if r.Method == "OPTIONS" { + addCorsHeaders(w.Header()) + w.WriteHeader(200) + return + } + if !g.authenticator.Accept(&r.Header) { challengeHeader, err := g.authenticator.FreshChallengeHeader(r) if err != nil { diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..6cd02ad --- /dev/null +++ b/static/index.html @@ -0,0 +1,5 @@ + + +

LSAT auth server

+ + \ No newline at end of file From 5a3b8b79d238627b257bc8de3c1404e84b9a5273 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 1 Nov 2019 16:31:22 +0100 Subject: [PATCH 04/13] proxy: implement basic proxy functionality --- auth/config.go | 50 ++++++++ auth/lnd_authenticator.go | 137 ++++++++++++++++++++++ config.go | 5 +- go.mod | 9 +- go.sum | 240 ++++++++++++++++++++++++++++++++++++-- kirin.go | 12 +- macaroons/service.go | 121 +++++++++++++++++++ proxy/proxy.go | 129 +++++++++++++++----- proxy/proxy_test.go | 24 ++-- proxy/service.go | 14 ++- sample-conf.yaml | 14 ++- 11 files changed, 694 insertions(+), 61 deletions(-) create mode 100644 auth/config.go create mode 100644 auth/lnd_authenticator.go create mode 100644 macaroons/service.go diff --git a/auth/config.go b/auth/config.go new file mode 100644 index 0000000..73feb01 --- /dev/null +++ b/auth/config.go @@ -0,0 +1,50 @@ +package auth + +import ( + "fmt" + "strconv" + "strings" +) + +type Config 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 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() uint8 { + 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 uint8(count) +} + +func (l Level) IsOff() bool { + lower := l.lower() + return lower == "off" || lower == "false" +} diff --git a/auth/lnd_authenticator.go b/auth/lnd_authenticator.go new file mode 100644 index 0000000..da0ddc5 --- /dev/null +++ b/auth/lnd_authenticator.go @@ -0,0 +1,137 @@ +package auth + +import ( + "context" + "encoding/base64" + "encoding/hex" + "fmt" + "net/http" + "regexp" + + "github.com/lightninglabs/kirin/macaroons" + "github.com/lightninglabs/loop/lndclient" + "github.com/lightningnetwork/lnd/lnrpc" + "gopkg.in/macaroon-bakery.v2/bakery" + "gopkg.in/macaroon-bakery.v2/bakery/checkers" +) + +var ( + authRegex = regexp.MustCompile("LSAT (.*?):([a-f0-9]{64})") + opWildcard = "*" +) + +type LndAuthenticator struct { + client lnrpc.LightningClient + macService *macaroons.Service +} + +// A compile time flag to ensure the LndAuthenticator satisfies the +// Authenticator interface. +var _ Authenticator = (*LndAuthenticator)(nil) + +// NewLndAuthenticator creates a new authenticator that is connected to an lnd +// backend and can create new invoices if required. +func NewLndAuthenticator(cfg *Config) (*LndAuthenticator, error) { + client, err := lndclient.NewBasicClient( + cfg.LndHost, cfg.TlsPath, cfg.MacDir, cfg.Network, + ) + if err != nil { + return nil, err + } + macService, err := macaroons.NewService() + if err != nil { + return nil, err + } + + return &LndAuthenticator{ + client: client, + 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 *LndAuthenticator) Accept(header *http.Header) bool { + authHeader := header.Get("Authorization") + if authHeader == "" { + return false + } + + if !authRegex.MatchString(authHeader) { + return false + } + + matches := authRegex.FindStringSubmatch(authHeader) + if len(matches) != 3 { + return false + } + + macBase64, preimageHex := matches[1], matches[2] + macBytes, err := base64.StdEncoding.DecodeString(macBase64) + if err != nil { + return false + } + + preimageBytes, err := hex.DecodeString(preimageHex) + if err != nil { + return false + } + + // TODO(guggero): check preimage against payment hash caveat in the + // macaroon. + if len(preimageBytes) != 32 { + return false + } + + err = l.macService.ValidateMacaroon(macBytes, []bakery.Op{}) + if err != nil { + 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 *LndAuthenticator) FreshChallengeHeader(r *http.Request) ( + http.Header, 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. + ctx := context.Background() + invoice := &lnrpc.Invoice{ + Memo: "LSAT", + Value: 1, + } + response, err := l.client.AddInvoice(ctx, invoice) + if err != nil { + fmt.Printf("error adding invoice: %v\n", err) + return nil, err + } + paymentHashHex := hex.EncodeToString(response.RHash) + + // 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, paymentHashHex), + }, + ) + if err != nil { + fmt.Printf("error creating macaroon: %v\n", err) + return nil, err + } + + str := "LSAT macaroon='%s' invoice='%s'" + str = fmt.Sprintf( + str, base64.StdEncoding.EncodeToString(mac), + response.GetPaymentRequest(), + ) + header := r.Header + header.Set("WWW-Authenticate", str) + return header, nil +} diff --git a/config.go b/config.go index 88a72c1..c7b139b 100644 --- a/config.go +++ b/config.go @@ -2,6 +2,7 @@ package kirin import ( "github.com/btcsuite/btcutil" + "github.com/lightninglabs/kirin/auth" "github.com/lightninglabs/kirin/proxy" ) @@ -17,7 +18,9 @@ type config struct { // to listen for requests. ListenAddr string `long:"listenaddr" description:"The interface we should listen on for client requests"` + Authenticator *auth.Config `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:"JSON configurations for each Kirin backend service."` + Services []*proxy.Service `long:"service" description:"Configurations for each Kirin backend service."` } diff --git a/go.mod b/go.mod index 218570f..c3bd613 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,10 @@ module github.com/lightninglabs/kirin go 1.13 require ( - github.com/btcsuite/btcd v0.0.0-20190629003639-c26ffa870fd8 // indirect github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/kr/pretty v0.1.0 // indirect - golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 // indirect - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + 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 index 163856f..72692a6 100644 --- a/go.sum +++ b/go.sum @@ -1,62 +1,288 @@ +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 h1:S9+cnZ7N4EvkkOBQ3lUy4p7+XjW4GS81R4QjwuT06Cw= +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/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +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 h1:PWCIRUyow3Od4TMukVHL5jmNhjUPKhw6OVVruYCCUQ0= +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-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +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-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +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-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +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 h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +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 index bd7bcc3..1b5453e 100644 --- a/kirin.go +++ b/kirin.go @@ -6,11 +6,10 @@ import ( "net/http" "os" "path/filepath" - - "gopkg.in/yaml.v2" - + "github.com/lightninglabs/kirin/auth" "github.com/lightninglabs/kirin/proxy" + "gopkg.in/yaml.v2" ) /** @@ -42,8 +41,11 @@ func start() error { return fmt.Errorf("missing listen address for server") } - authenticator := auth.NewMockAuthenticator() - servicesProxy, err := proxy.New(*authenticator, cfg.Services) + authenticator, err := auth.NewLndAuthenticator(cfg.Authenticator) + if err != nil { + return err + } + servicesProxy, err := proxy.New(authenticator, cfg.Services) if err != nil { return err } 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/proxy.go b/proxy/proxy.go index ee8cbe6..7110677 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -22,6 +22,10 @@ type Proxy struct { staticServer http.Handler authenticator auth.Authenticator + + services []*Service + + freebieCounter map[string]uint8 } // New returns a new Proxy instance that proxies between the services specified, @@ -41,25 +45,32 @@ func New(auth auth.Authenticator, services []*Service) (*Proxy, error) { ForceAttemptHTTP2: true, TLSClientConfig: tlsConfig, } + grpcProxy := &httputil.ReverseProxy{ - Director: director(auth, services), + Director: director(services), 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, } staticServer := http.FileServer(http.Dir("static")) return &Proxy{ - grpcProxy, - staticServer, - auth, + server: grpcProxy, + staticServer: staticServer, + authenticator: auth, + services: services, + freebieCounter: map[string]uint8{}, }, nil } +// 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 { @@ -73,14 +84,16 @@ func certPool(services []*Service) (*x509.CertPool, error) { } if !cp.AppendCertsFromPEM(b) { - return nil, fmt.Errorf("credentials: failed to append " + - "certificate") + 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) @@ -98,63 +111,119 @@ func matchService(req *http.Request, services []*Service) (*Service, bool) { } return service, true - } return nil, false } -func director(auth auth.Authenticator, services []*Service) func(req *http.Request) { +// director returns a closure that rewrites an incoming request to be forwarded +// to a backend service. +func director(services []*Service) func(req *http.Request) { return func(req *http.Request) { target, ok := matchService(req, services) if ok { - req.URL.Scheme = "http" - if target.TLSCertPath != "" { - req.URL.Scheme = "https" - } - + // 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") } } } +// 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) { header.Add("Access-Control-Allow-Origin", "*") header.Add("Access-Control-Allow-Methods", "GET, POST, OPTIONS") - header.Add("Access-Control-Allow-Headers", "Authorization, Grpc-Metadata-macaroon") + 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 { + 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 { + fmt.Printf("error writing response: %v", err) + } } // ServeHTTP checks a client's headers for appropriate authorization and either // returns a challenge or forwards their request to the target backend service. -func (g *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Serve static index HTML page. if r.Method == "GET" && (r.URL.Path == "/" || r.URL.Path == "/index.html") { - g.staticServer.ServeHTTP(w, r) + p.staticServer.ServeHTTP(w, r) return } + // For OPTIONS requests we only need to set the CORS headers, not serve + // any content; if r.Method == "OPTIONS" { addCorsHeaders(w.Header()) - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) return } - if !g.authenticator.Accept(&r.Header) { - challengeHeader, err := g.authenticator.FreshChallengeHeader(r) - if err != nil { - w.WriteHeader(500) + // Every request that makes it to here must be matched to a backend + // service. Otherwise it a wrong request and receives a 404 not found. + target, ok := matchService(r, p.services) + if !ok { + w.WriteHeader(http.StatusNotFound) + return + } + + // Determine auth level required to access service and dispatch request + // accordingly. + switch { + case target.Auth.IsOn(): + if !p.authenticator.Accept(&r.Header) { + p.handlePaymentRequired(w, r) return } - - for name, value := range challengeHeader { - w.Header().Set(name, value[0]) - for i := 1; i < len(value); i++ { - w.Header().Add(name, value[i]) + 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) { + counter, ok := p.freebieCounter[r.RemoteAddr] + if !ok { + counter = 0 } + if counter >= target.Auth.FreebieCount() { + p.handlePaymentRequired(w, r) + return + } + p.freebieCounter[r.RemoteAddr] = counter + 1 } - - w.WriteHeader(402) - return + case target.Auth.IsOff(): } - g.server.ServeHTTP(w, r) + // If we got here, it means everything is OK to pass the request to the + // service backend via the reverse proxy. + p.server.ServeHTTP(w, r) } diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index cbc4e43..038bbfa 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -6,25 +6,28 @@ import ( "net/http" "strings" "testing" + "time" "github.com/lightninglabs/kirin/auth" "github.com/lightninglabs/kirin/proxy" ) const ( - testFQDN = "localhost:10019" + 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{ - &proxy.Service{ - Address: testTargetServiceAddress, - FQDN: testFQDN, - }, - } + services := []*proxy.Service{{ + Address: testTargetServiceAddress, + HostRegexp: testHostRegexp, + PathRegexp: testPathRegexp, + Protocol: "http", + }} auth := auth.NewMockAuthenticator() proxy, err := proxy.New(auth, services) @@ -34,7 +37,7 @@ func TestProxy(t *testing.T) { // Start server that gives requests to the proxy. server := &http.Server{ - Addr: testFQDN, + Addr: testAddr, Handler: http.HandlerFunc(proxy.ServeHTTP), } @@ -51,10 +54,13 @@ func TestProxy(t *testing.T) { } }() + // 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", testFQDN) + url := fmt.Sprintf("http://%s/grpc/test", testAddr) resp, err := client.Get(url) if err != nil { t.Fatalf("errored making http request: %v", err) diff --git a/proxy/service.go b/proxy/service.go index 2090200..c631f12 100644 --- a/proxy/service.go +++ b/proxy/service.go @@ -1,5 +1,7 @@ package proxy +import "github.com/lightninglabs/kirin/auth" + // Service generically specifies configuration data for backend services to the // Kirin proxy. type Service struct { @@ -7,7 +9,17 @@ type Service struct { 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:"lnd instance rpc address"` + 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. diff --git a/sample-conf.yaml b/sample-conf.yaml index fc80b1a..a94254f 100644 --- a/sample-conf.yaml +++ b/sample-conf.yaml @@ -1,11 +1,19 @@ listenaddr: "localhost:8081" -# Use single quotes for regular expressions with special characters in them to -# avoid YAML parsing errors! services: - - hostregexp: "service1.com" + # 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.crt" + - hostregexp: "service2.com:8083" pathregexp: '^/.*$' address: "123.456.789:8082" + protocol: https From 7e0c1dd97e88084f151f685037c0214bc11ffc24 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 11 Oct 2019 13:33:37 +0200 Subject: [PATCH 05/13] multi: add persistent logger --- auth/log.go | 29 ++++++++++++++++ config.go | 8 +++++ go.mod | 1 + go.sum | 2 -- kirin.go | 86 ++++++++++++++++++++++++++++++++++++++---------- log.go | 38 +++++++++++++++++++++ proxy/log.go | 29 ++++++++++++++++ sample-conf.yaml | 2 ++ 8 files changed, 176 insertions(+), 19 deletions(-) create mode 100644 auth/log.go create mode 100644 log.go create mode 100644 proxy/log.go 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/config.go b/config.go index c7b139b..67643ca 100644 --- a/config.go +++ b/config.go @@ -11,6 +11,10 @@ var ( defaultConfigFilename = "kirin.yaml" defaultTLSKeyFilename = "tls.key" defaultTLSCertFilename = "tls.cert" + defaultLogLevel = "info" + defaultLogFilename = "kirin.log" + defaultMaxLogFiles = 3 + defaultMaxLogFileSize = 10 ) type config struct { @@ -23,4 +27,8 @@ type config struct { // 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/go.mod b/go.mod index c3bd613..22a9550 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ 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 diff --git a/go.sum b/go.sum index 72692a6..1de0168 100644 --- a/go.sum +++ b/go.sum @@ -28,7 +28,6 @@ github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9 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 h1:S9+cnZ7N4EvkkOBQ3lUy4p7+XjW4GS81R4QjwuT06Cw= 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= @@ -146,7 +145,6 @@ github.com/lightninglabs/neutrino v0.10.0 h1:yWVy2cOCCXbKFdpYCE9vD1fWRJDd9FtGXhU 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 h1:PWCIRUyow3Od4TMukVHL5jmNhjUPKhw6OVVruYCCUQ0= 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= diff --git a/kirin.go b/kirin.go index 1b5453e..f342dab 100644 --- a/kirin.go +++ b/kirin.go @@ -6,15 +6,13 @@ import ( "net/http" "os" "path/filepath" - + "github.com/lightninglabs/kirin/auth" "github.com/lightninglabs/kirin/proxy" + "github.com/lightningnetwork/lnd/build" "gopkg.in/yaml.v2" ) -/** - */ - // Main is the true entrypoint of Kirin. func Main() { // TODO: Prevent from running twice. @@ -25,22 +23,21 @@ func Main() { } } +// 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) - var cfg config - b, err := ioutil.ReadFile(configFile) + cfg, err := getConfig(configFile) if err != nil { - return err - } - - err = yaml.Unmarshal(b, &cfg) - if err != nil { - return err - } - if cfg.ListenAddr == "" { - return fmt.Errorf("missing listen address for server") + 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 auxiliary services the proxy needs to work. authenticator, err := auth.NewLndAuthenticator(cfg.Authenticator) if err != nil { return err @@ -50,13 +47,68 @@ func start() error { return err } - // Start the reverse proxy. + // Finally start the reverse proxy. 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) 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) +} + +// 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/proxy/log.go b/proxy/log.go new file mode 100644 index 0000000..930b064 --- /dev/null +++ b/proxy/log.go @@ -0,0 +1,29 @@ +package proxy + +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("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 +} diff --git a/sample-conf.yaml b/sample-conf.yaml index a94254f..3ef6784 100644 --- a/sample-conf.yaml +++ b/sample-conf.yaml @@ -1,4 +1,6 @@ listenaddr: "localhost:8081" +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 From 83395c0c98d191e671693701b66ed5a22f40f53d Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 11 Oct 2019 14:25:47 +0200 Subject: [PATCH 06/13] proxy+auth: add log statements --- auth/lnd_authenticator.go | 13 +++++++++++-- kirin.go | 5 +++-- proxy/proxy.go | 33 ++++++++++++++++++++++++++++++--- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/auth/lnd_authenticator.go b/auth/lnd_authenticator.go index da0ddc5..e85e436 100644 --- a/auth/lnd_authenticator.go +++ b/auth/lnd_authenticator.go @@ -55,38 +55,45 @@ func NewLndAuthenticator(cfg *Config) (*LndAuthenticator, error) { // NOTE: This is part of the Authenticator interface. func (l *LndAuthenticator) 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 @@ -108,7 +115,7 @@ func (l *LndAuthenticator) FreshChallengeHeader(r *http.Request) ( } response, err := l.client.AddInvoice(ctx, invoice) if err != nil { - fmt.Printf("error adding invoice: %v\n", err) + log.Errorf("Error adding invoice: %v", err) return nil, err } paymentHashHex := hex.EncodeToString(response.RHash) @@ -122,7 +129,7 @@ func (l *LndAuthenticator) FreshChallengeHeader(r *http.Request) ( }, ) if err != nil { - fmt.Printf("error creating macaroon: %v\n", err) + log.Errorf("Error creating macaroon: %v", err) return nil, err } @@ -133,5 +140,7 @@ func (l *LndAuthenticator) FreshChallengeHeader(r *http.Request) ( ) header := r.Header header.Set("WWW-Authenticate", str) + + log.Debugf("Created new challenge header: [%s]", str) return header, nil } diff --git a/kirin.go b/kirin.go index f342dab..0c91098 100644 --- a/kirin.go +++ b/kirin.go @@ -46,8 +46,6 @@ func start() error { if err != nil { return err } - - // Finally start the reverse proxy. server := &http.Server{ Addr: cfg.ListenAddr, Handler: http.HandlerFunc(servicesProxy.ServeHTTP), @@ -59,6 +57,9 @@ func start() 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) } diff --git a/proxy/proxy.go b/proxy/proxy.go index 7110677..afbc076 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -12,6 +12,8 @@ import ( "github.com/lightninglabs/kirin/auth" ) +const formatPattern = "%s - - \"%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 @@ -98,20 +100,33 @@ 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 } - urlRegexp := regexp.MustCompile(service.PathRegexp) - if !urlRegexp.MatchString(req.URL.Path) { + 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 } @@ -139,6 +154,8 @@ func director(services []*Service) func(req *http.Request) { // 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") @@ -155,6 +172,7 @@ func (p *Proxy) handlePaymentRequired(w http.ResponseWriter, r *http.Request) { header, err := p.authenticator.FreshChallengeHeader(r) if err != nil { + log.Errorf("Error creating new challenge header, response 500.") w.WriteHeader(http.StatusInternalServerError) return } @@ -168,16 +186,25 @@ func (p *Proxy) handlePaymentRequired(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusPaymentRequired) if _, err := w.Write([]byte("payment required")); err != nil { - fmt.Printf("error writing response: %v", err) + log.Errorf("Error writing response: %v", err) } } // 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) { + logRequest := func() { + log.Infof(formatPattern, r.RemoteAddr, r.Method, r.RequestURI, + r.Proto, r.Referer(), r.UserAgent()) + } + defer logRequest() + // Serve static index HTML page. if r.Method == "GET" && (r.URL.Path == "/" || r.URL.Path == "/index.html") { + + log.Debugf("Dispatching request %s to static file server.", + r.URL.Path) p.staticServer.ServeHTTP(w, r) return } From a44e9fbd222c3f1d8bfdf1bd5bc98bbb4b39f6db Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 11 Oct 2019 15:38:57 +0200 Subject: [PATCH 07/13] freebie+proxy: add memory based freebie DB implementation --- auth/config.go | 6 +- freebie/interface.go | 15 ++++ freebie/mem_store.go | 50 ++++++++++++ proxy/proxy.go | 183 ++++++++++++++++++++++++++----------------- proxy/service.go | 7 +- sample-conf.yaml | 2 +- 6 files changed, 186 insertions(+), 77 deletions(-) create mode 100644 freebie/interface.go create mode 100644 freebie/mem_store.go diff --git a/auth/config.go b/auth/config.go index 73feb01..66aeb5e 100644 --- a/auth/config.go +++ b/auth/config.go @@ -4,6 +4,8 @@ import ( "fmt" "strconv" "strings" + + "github.com/lightninglabs/kirin/freebie" ) type Config struct { @@ -32,7 +34,7 @@ func (l Level) IsFreebie() bool { return strings.HasPrefix(l.lower(), "freebie") } -func (l Level) FreebieCount() uint8 { +func (l Level) FreebieCount() freebie.Count { parts := strings.Split(l.lower(), " ") if len(parts) != 2 { panic(fmt.Errorf("invalid auth value: %s", l.lower())) @@ -41,7 +43,7 @@ func (l Level) FreebieCount() uint8 { if err != nil { panic(err) } - return uint8(count) + return freebie.Count(count) } func (l Level) IsOff() bool { 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/proxy/proxy.go b/proxy/proxy.go index afbc076..8043435 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -5,14 +5,18 @@ import ( "crypto/x509" "fmt" "io/ioutil" + "net" "net/http" "net/http/httputil" "regexp" "github.com/lightninglabs/kirin/auth" + "github.com/lightninglabs/kirin/freebie" ) -const formatPattern = "%s - - \"%s %s %s\" \"%s\" \"%s\"" +const ( + formatPattern = "%s - - \"%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 @@ -26,14 +30,17 @@ type Proxy struct { authenticator auth.Authenticator services []*Service - - freebieCounter map[string]uint8 } // 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) (*Proxy, error) { + err := prepareServices(services) + if err != nil { + return nil, err + } + cp, err := certPool(services) if err != nil { return nil, err @@ -64,14 +71,109 @@ func New(auth auth.Authenticator, services []*Service) (*Proxy, error) { staticServer := http.FileServer(http.Dir("static")) return &Proxy{ - server: grpcProxy, - staticServer: staticServer, - authenticator: auth, - services: services, - freebieCounter: map[string]uint8{}, + server: grpcProxy, + staticServer: staticServer, + authenticator: auth, + services: services, }, 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. + remoteHost, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + remoteHost = "0.0.0.0" + } + remoteIp := net.ParseIP(remoteHost) + if remoteIp == nil { + remoteIp = net.IPv4zero + } + logRequest := func() { + log.Infof(formatPattern, remoteIp.String(), r.Method, + r.RequestURI, r.Proto, r.Referer(), r.UserAgent()) + } + defer logRequest() + + // Serve static index HTML page. + if r.Method == "GET" && + (r.URL.Path == "/" || r.URL.Path == "/index.html") { + + log.Debugf("Dispatching request %s to static file server.", + r.URL.Path) + p.staticServer.ServeHTTP(w, r) + return + } + + // 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 + } + + // Every request that makes it to here must be matched to a backend + // service. Otherwise it a wrong request and receives a 404 not found. + target, ok := matchService(r, p.services) + if !ok { + w.WriteHeader(http.StatusNotFound) + return + } + + // Determine auth level required to access service and dispatch request + // accordingly. + switch { + case target.Auth.IsOn(): + if !p.authenticator.Accept(&r.Header) { + 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 { + log.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 { + log.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.server.ServeHTTP(w, r) +} + +// 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(), + ) + } + } + return nil +} + // certPool builds a pool of x509 certificates from the backend services. func certPool(services []*Service) (*x509.CertPool, error) { cp := x509.NewCertPool() @@ -189,68 +291,3 @@ func (p *Proxy) handlePaymentRequired(w http.ResponseWriter, r *http.Request) { log.Errorf("Error writing response: %v", err) } } - -// 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) { - logRequest := func() { - log.Infof(formatPattern, r.RemoteAddr, r.Method, r.RequestURI, - r.Proto, r.Referer(), r.UserAgent()) - } - defer logRequest() - - // Serve static index HTML page. - if r.Method == "GET" && - (r.URL.Path == "/" || r.URL.Path == "/index.html") { - - log.Debugf("Dispatching request %s to static file server.", - r.URL.Path) - p.staticServer.ServeHTTP(w, r) - return - } - - // 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 - } - - // Every request that makes it to here must be matched to a backend - // service. Otherwise it a wrong request and receives a 404 not found. - target, ok := matchService(r, p.services) - if !ok { - w.WriteHeader(http.StatusNotFound) - return - } - - // Determine auth level required to access service and dispatch request - // accordingly. - switch { - case target.Auth.IsOn(): - if !p.authenticator.Accept(&r.Header) { - 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) { - counter, ok := p.freebieCounter[r.RemoteAddr] - if !ok { - counter = 0 - } - if counter >= target.Auth.FreebieCount() { - p.handlePaymentRequired(w, r) - return - } - p.freebieCounter[r.RemoteAddr] = counter + 1 - } - 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.server.ServeHTTP(w, r) -} diff --git a/proxy/service.go b/proxy/service.go index c631f12..6e7ffc5 100644 --- a/proxy/service.go +++ b/proxy/service.go @@ -1,6 +1,9 @@ package proxy -import "github.com/lightninglabs/kirin/auth" +import ( + "github.com/lightninglabs/kirin/auth" + "github.com/lightninglabs/kirin/freebie" +) // Service generically specifies configuration data for backend services to the // Kirin proxy. @@ -28,4 +31,6 @@ type Service struct { // 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"` + + freebieDb freebie.DB } diff --git a/sample-conf.yaml b/sample-conf.yaml index 3ef6784..f4ad375 100644 --- a/sample-conf.yaml +++ b/sample-conf.yaml @@ -13,7 +13,7 @@ services: pathregexp: '^/.*$' address: "127.0.0.1:10009" protocol: https - tlscertpath: "path-to-optional-tls-cert/tls.crt" + tlscertpath: "path-to-optional-tls-cert/tls.cert" - hostregexp: "service2.com:8083" pathregexp: '^/.*$' From c8cbeb9ab19f85b58dfc2b5fb559442e5e62ebba Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 18 Oct 2019 15:10:13 +0200 Subject: [PATCH 08/13] config: add static file root --- config.go | 4 ++++ kirin.go | 4 +++- proxy/proxy.go | 6 ++++-- proxy/proxy_test.go | 2 +- sample-conf.yaml | 1 + 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/config.go b/config.go index 67643ca..a743233 100644 --- a/config.go +++ b/config.go @@ -22,6 +22,10 @@ type config struct { // 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 *auth.Config `long:"authenticator" description:"Configuration for the authenticator."` // Services is a list of JSON objects in string format, which specify diff --git a/kirin.go b/kirin.go index 0c91098..3761bf3 100644 --- a/kirin.go +++ b/kirin.go @@ -42,7 +42,9 @@ func start() error { if err != nil { return err } - servicesProxy, err := proxy.New(authenticator, cfg.Services) + servicesProxy, err := proxy.New( + authenticator, cfg.Services, cfg.StaticRoot, + ) if err != nil { return err } diff --git a/proxy/proxy.go b/proxy/proxy.go index 8043435..49b3804 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -35,7 +35,9 @@ type Proxy struct { // 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) (*Proxy, error) { +func New(auth auth.Authenticator, services []*Service, staticRoot string) ( + *Proxy, error) { + err := prepareServices(services) if err != nil { return nil, err @@ -68,7 +70,7 @@ func New(auth auth.Authenticator, services []*Service) (*Proxy, error) { FlushInterval: -1, } - staticServer := http.FileServer(http.Dir("static")) + staticServer := http.FileServer(http.Dir(staticRoot)) return &Proxy{ server: grpcProxy, diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index 038bbfa..411f4d0 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -30,7 +30,7 @@ func TestProxy(t *testing.T) { }} auth := auth.NewMockAuthenticator() - proxy, err := proxy.New(auth, services) + proxy, err := proxy.New(auth, services, "static") if err != nil { t.Fatalf("failed to create new proxy: %v", err) } diff --git a/sample-conf.yaml b/sample-conf.yaml index f4ad375..bcf9cd0 100644 --- a/sample-conf.yaml +++ b/sample-conf.yaml @@ -1,4 +1,5 @@ listenaddr: "localhost:8081" +staticroot: "./static" debuglevel: "debug" services: From 8cbb4fc4fb2e8c333f3e60f87d8cfa54e0b0fa92 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 29 Oct 2019 13:08:16 +0100 Subject: [PATCH 09/13] auth: create invoice request with closure, add challenger --- ...{lnd_authenticator.go => authenticator.go} | 52 +++++-------- auth/config.go | 11 --- auth/interface.go | 21 ++++-- challenger.go | 74 +++++++++++++++++++ config.go | 14 +++- kirin.go | 35 ++++++--- 6 files changed, 146 insertions(+), 61 deletions(-) rename auth/{lnd_authenticator.go => authenticator.go} (66%) create mode 100644 challenger.go diff --git a/auth/lnd_authenticator.go b/auth/authenticator.go similarity index 66% rename from auth/lnd_authenticator.go rename to auth/authenticator.go index e85e436..ec51780 100644 --- a/auth/lnd_authenticator.go +++ b/auth/authenticator.go @@ -1,7 +1,6 @@ package auth import ( - "context" "encoding/base64" "encoding/hex" "fmt" @@ -9,8 +8,6 @@ import ( "regexp" "github.com/lightninglabs/kirin/macaroons" - "github.com/lightninglabs/loop/lndclient" - "github.com/lightningnetwork/lnd/lnrpc" "gopkg.in/macaroon-bakery.v2/bakery" "gopkg.in/macaroon-bakery.v2/bakery/checkers" ) @@ -20,31 +17,27 @@ var ( opWildcard = "*" ) -type LndAuthenticator struct { - client lnrpc.LightningClient +// 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 LndAuthenticator satisfies the +// A compile time flag to ensure the LsatAuthenticator satisfies the // Authenticator interface. -var _ Authenticator = (*LndAuthenticator)(nil) +var _ Authenticator = (*LsatAuthenticator)(nil) -// NewLndAuthenticator creates a new authenticator that is connected to an lnd -// backend and can create new invoices if required. -func NewLndAuthenticator(cfg *Config) (*LndAuthenticator, error) { - client, err := lndclient.NewBasicClient( - cfg.LndHost, cfg.TlsPath, cfg.MacDir, cfg.Network, - ) - if err != nil { - return nil, err - } +// 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 &LndAuthenticator{ - client: client, + return &LsatAuthenticator{ + challenger: challenger, macService: macService, }, nil } @@ -53,7 +46,7 @@ func NewLndAuthenticator(cfg *Config) (*LndAuthenticator, error) { // to a given backend service. // // NOTE: This is part of the Authenticator interface. -func (l *LndAuthenticator) Accept(header *http.Header) bool { +func (l *LsatAuthenticator) Accept(header *http.Header) bool { authHeader := header.Get("Authorization") log.Debugf("Trying to authorize with header value [%s].", authHeader) if authHeader == "" { @@ -103,29 +96,23 @@ func (l *LndAuthenticator) Accept(header *http.Header) bool { // complete. // // NOTE: This is part of the Authenticator interface. -func (l *LndAuthenticator) FreshChallengeHeader(r *http.Request) ( +func (l *LsatAuthenticator) FreshChallengeHeader(r *http.Request) ( http.Header, 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. - ctx := context.Background() - invoice := &lnrpc.Invoice{ - Memo: "LSAT", - Value: 1, - } - response, err := l.client.AddInvoice(ctx, invoice) + paymentRequest, paymentHash, err := l.challenger.NewChallenge() if err != nil { - log.Errorf("Error adding invoice: %v", err) + log.Errorf("Error creating new challenge: %v", err) return nil, err } - paymentHashHex := hex.EncodeToString(response.RHash) // 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, paymentHashHex), + checkers.Condition( + macaroons.CondRHash, paymentHash.String(), + ), }, ) if err != nil { @@ -135,8 +122,7 @@ func (l *LndAuthenticator) FreshChallengeHeader(r *http.Request) ( str := "LSAT macaroon='%s' invoice='%s'" str = fmt.Sprintf( - str, base64.StdEncoding.EncodeToString(mac), - response.GetPaymentRequest(), + str, base64.StdEncoding.EncodeToString(mac), paymentRequest, ) header := r.Header header.Set("WWW-Authenticate", str) diff --git a/auth/config.go b/auth/config.go index 66aeb5e..a4eb713 100644 --- a/auth/config.go +++ b/auth/config.go @@ -8,17 +8,6 @@ import ( "github.com/lightninglabs/kirin/freebie" ) -type Config 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 Level string func (l Level) lower() string { diff --git a/auth/interface.go b/auth/interface.go index 5954fd0..00fa8ad 100644 --- a/auth/interface.go +++ b/auth/interface.go @@ -1,15 +1,26 @@ package auth -import "net/http" +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 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 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/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/config.go b/config.go index a743233..368b57f 100644 --- a/config.go +++ b/config.go @@ -2,7 +2,6 @@ package kirin import ( "github.com/btcsuite/btcutil" - "github.com/lightninglabs/kirin/auth" "github.com/lightninglabs/kirin/proxy" ) @@ -17,6 +16,17 @@ var ( 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. @@ -26,7 +36,7 @@ type config struct { // is located. StaticRoot string `long:"staticroot" description:"The folder where the static content is located."` - Authenticator *auth.Config `long:"authenticator" description:"Configuration for the authenticator."` + 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. diff --git a/kirin.go b/kirin.go index 3761bf3..47e396c 100644 --- a/kirin.go +++ b/kirin.go @@ -10,6 +10,7 @@ import ( "github.com/lightninglabs/kirin/auth" "github.com/lightninglabs/kirin/proxy" "github.com/lightningnetwork/lnd/build" + "github.com/lightningnetwork/lnd/lnrpc" "gopkg.in/yaml.v2" ) @@ -37,17 +38,14 @@ func start() error { return fmt.Errorf("unable to set up logging: %v", err) } - // Create the auxiliary services the proxy needs to work. - authenticator, err := auth.NewLndAuthenticator(cfg.Authenticator) - if err != nil { - return err - } - servicesProxy, err := proxy.New( - authenticator, cfg.Services, cfg.StaticRoot, - ) - if err != nil { - return 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), @@ -103,6 +101,23 @@ func setupLogging(cfg *config) error { 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() From 672766b3400ab2efd8c6aee59c9e34822ed88f5b Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 1 Nov 2019 16:58:10 +0100 Subject: [PATCH 10/13] proxy: allow update of backend services --- proxy/proxy.go | 133 +++++++++++++++++++++-------------------------- proxy/service.go | 14 +++++ 2 files changed, 74 insertions(+), 73 deletions(-) diff --git a/proxy/proxy.go b/proxy/proxy.go index 49b3804..926da59 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -11,7 +11,6 @@ import ( "regexp" "github.com/lightninglabs/kirin/auth" - "github.com/lightninglabs/kirin/freebie" ) const ( @@ -23,13 +22,10 @@ const ( // a challenge to the client or forwards the request to another server and // proxies the response back to the client. type Proxy struct { - server *httputil.ReverseProxy - - staticServer http.Handler - + proxyBackend *httputil.ReverseProxy + staticServer http.Handler authenticator auth.Authenticator - - services []*Service + services []*Service } // New returns a new Proxy instance that proxies between the services specified, @@ -38,46 +34,18 @@ type Proxy struct { func New(auth auth.Authenticator, services []*Service, staticRoot string) ( *Proxy, error) { - err := prepareServices(services) - if err != nil { - return nil, err - } - - cp, err := certPool(services) - if err != nil { - return nil, err - } - - tlsConfig := &tls.Config{ - RootCAs: cp, - InsecureSkipVerify: true, - } - transport := &http.Transport{ - ForceAttemptHTTP2: true, - TLSClientConfig: tlsConfig, - } - - grpcProxy := &httputil.ReverseProxy{ - Director: director(services), - 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, - } - staticServer := http.FileServer(http.Dir(staticRoot)) - - return &Proxy{ - server: grpcProxy, + proxy := &Proxy{ staticServer: staticServer, authenticator: auth, services: services, - }, nil + } + err := proxy.UpdateServices(services) + if err != nil { + return nil, err + } + + return proxy, nil } // ServeHTTP checks a client's headers for appropriate authorization and either @@ -159,23 +127,61 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { // If we got here, it means everything is OK to pass the request to the // service backend via the reverse proxy. - p.server.ServeHTTP(w, r) + p.proxyBackend.ServeHTTP(w, r) } -// 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(), - ) - } +// 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") + } +} + // certPool builds a pool of x509 certificates from the backend services. func certPool(services []*Service) (*x509.CertPool, error) { cp := x509.NewCertPool() @@ -234,25 +240,6 @@ func matchService(req *http.Request, services []*Service) (*Service, bool) { return nil, false } -// director returns a closure that rewrites an incoming request to be forwarded -// to a backend service. -func director(services []*Service) func(req *http.Request) { - return func(req *http.Request) { - target, ok := matchService(req, 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") - } - } -} - // 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 diff --git a/proxy/service.go b/proxy/service.go index 6e7ffc5..113a921 100644 --- a/proxy/service.go +++ b/proxy/service.go @@ -34,3 +34,17 @@ type Service struct { 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(), + ) + } + } + return nil +} From 38c17f4de8712872cb8124768efc7e6681e3b7a0 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 4 Nov 2019 13:15:33 +0100 Subject: [PATCH 11/13] proxy: dispatch all non-matched requests to static file server --- proxy/proxy.go | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/proxy/proxy.go b/proxy/proxy.go index 926da59..c70eb86 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -67,16 +67,6 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { } defer logRequest() - // Serve static index HTML page. - if r.Method == "GET" && - (r.URL.Path == "/" || r.URL.Path == "/index.html") { - - log.Debugf("Dispatching request %s to static file server.", - r.URL.Path) - p.staticServer.ServeHTTP(w, r) - return - } - // For OPTIONS requests we only need to set the CORS headers, not serve // any content; if r.Method == "OPTIONS" { @@ -85,11 +75,15 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - // Every request that makes it to here must be matched to a backend - // service. Otherwise it a wrong request and receives a 404 not found. + // 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 { - w.WriteHeader(http.StatusNotFound) + log.Debugf("Dispatching request %s to static file server.", + r.URL.Path) + p.staticServer.ServeHTTP(w, r) return } From e704ba4aa957236b95f080d1d1fd05fa6c2dcee8 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 4 Nov 2019 13:48:21 +0100 Subject: [PATCH 12/13] proxy: add prefix logger to log remote IP address --- proxy/log.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++ proxy/proxy.go | 32 ++++++++++++------------- 2 files changed, 80 insertions(+), 16 deletions(-) diff --git a/proxy/log.go b/proxy/log.go index 930b064..89bc0cb 100644 --- a/proxy/log.go +++ b/proxy/log.go @@ -1,6 +1,9 @@ package proxy import ( + "fmt" + "net" + "github.com/btcsuite/btclog" "github.com/lightningnetwork/lnd/build" ) @@ -27,3 +30,64 @@ func DisableLog() { 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 index c70eb86..0341169 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -5,7 +5,6 @@ import ( "crypto/x509" "fmt" "io/ioutil" - "net" "net/http" "net/http/httputil" "regexp" @@ -14,7 +13,12 @@ import ( ) const ( - formatPattern = "%s - - \"%s %s %s\" \"%s\" \"%s\"" + // 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, @@ -53,17 +57,10 @@ func New(auth auth.Authenticator, services []*Service, staticRoot string) ( 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. - remoteHost, _, err := net.SplitHostPort(r.RemoteAddr) - if err != nil { - remoteHost = "0.0.0.0" - } - remoteIp := net.ParseIP(remoteHost) - if remoteIp == nil { - remoteIp = net.IPv4zero - } + remoteIp, prefixLog := NewRemoteIPPrefixLog(log, r.RemoteAddr) logRequest := func() { - log.Infof(formatPattern, remoteIp.String(), r.Method, - r.RequestURI, r.Proto, r.Referer(), r.UserAgent()) + prefixLog.Infof(formatPattern, r.Method, r.RequestURI, r.Proto, + r.Referer(), r.UserAgent()) } defer logRequest() @@ -81,8 +78,8 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { // will return a 404 for us. target, ok := matchService(r, p.services) if !ok { - log.Debugf("Dispatching request %s to static file server.", - r.URL.Path) + prefixLog.Debugf("Dispatching request %s to static file "+ + "server.", r.URL.Path) p.staticServer.ServeHTTP(w, r) return } @@ -92,6 +89,7 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch { case target.Auth.IsOn(): if !p.authenticator.Accept(&r.Header) { + prefixLog.Infof("Authentication failed. Sending 402.") p.handlePaymentRequired(w, r) return } @@ -101,7 +99,8 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { if !p.authenticator.Accept(&r.Header) { ok, err := target.freebieDb.CanPass(r, remoteIp) if err != nil { - log.Errorf("Error querying freebie db: %v", err) + prefixLog.Errorf("Error querying freebie db: "+ + "%v", err) w.WriteHeader(http.StatusInternalServerError) return } @@ -111,7 +110,8 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { } _, err = target.freebieDb.TallyFreebie(r, remoteIp) if err != nil { - log.Errorf("Error updating freebie db: %v", err) + prefixLog.Errorf("Error updating freebie db: "+ + "%v", err) w.WriteHeader(http.StatusInternalServerError) return } From 564deb6545e8c6cc6a816d698f93db44bbf60d98 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 1 Nov 2019 16:59:54 +0100 Subject: [PATCH 13/13] proxy: add demo and README with the use cases --- README.md | 57 ++++++++++++++++++++++ proxy/proxy.go | 6 +++ proxy/service.go | 60 +++++++++++++++++++++++ static/index.html | 122 +++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 README.md 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/proxy/proxy.go b/proxy/proxy.go index 0341169..793413b 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -173,6 +173,12 @@ func (p *Proxy) director(req *http.Request) { // 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) + } } } diff --git a/proxy/service.go b/proxy/service.go index 113a921..e74218a 100644 --- a/proxy/service.go +++ b/proxy/service.go @@ -1,10 +1,22 @@ 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 { @@ -32,6 +44,17 @@ type Service struct { // 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 } @@ -45,6 +68,43 @@ func prepareServices(services []*Service) error { 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/static/index.html b/static/index.html index 6cd02ad..7350b32 100644 --- a/static/index.html +++ b/static/index.html @@ -1,5 +1,125 @@ + + LSAT proxy demo page + + -

LSAT auth server

+
+
+

LND node info

+

+        
+    
+
+

Bos Scores

+

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