From a44e9fbd222c3f1d8bfdf1bd5bc98bbb4b39f6db Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 11 Oct 2019 15:38:57 +0200 Subject: [PATCH] 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: '^/.*$'