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"