diff --git a/handlers/favicon.go b/handlers/favicon.go new file mode 100644 index 0000000..4024ee1 --- /dev/null +++ b/handlers/favicon.go @@ -0,0 +1,19 @@ +package handlers + +import ( + "net/http" + "strings" +) + +func NewIgnoreFaviconRequests() func(h http.Handler) http.Handler { + return func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.RequestURI, "/favicon") { + http.NotFound(w, r) + return + } + + h.ServeHTTP(w, r) + }) + } +} diff --git a/handlers/ratelimiter.go b/handlers/ratelimiter.go new file mode 100644 index 0000000..4147985 --- /dev/null +++ b/handlers/ratelimiter.go @@ -0,0 +1,23 @@ +package handlers + +import ( + "net/http" + "time" + + "github.com/go-kit/kit/log" + "github.com/juju/ratelimit" +) + +func NewRateLimitHandler(b *ratelimit.Bucket, l log.Logger) func(h http.Handler) http.Handler { + return func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + d := b.Take(1) + if d > 0 { + l.Log("msg", "Rate limiting", "delay", d) + time.Sleep(d) + } + + h.ServeHTTP(w, r) + }) + } +} diff --git a/handlers/urlparameters.go b/handlers/urlparameters.go new file mode 100644 index 0000000..b1c712f --- /dev/null +++ b/handlers/urlparameters.go @@ -0,0 +1,43 @@ +package handlers + +import ( + "net/http" + "net/url" + + "github.com/go-kit/kit/log" +) + +func NewValidateURLParameter(l log.Logger, allowedHosts []string) func(h http.Handler) http.Handler { + var hosts = make(map[string]bool, len(allowedHosts)) + for _, host := range allowedHosts { + hosts[host] = true + } + + return func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + queryURL := r.URL.Query().Get("url") + if !isValidQueryURL(queryURL, hosts) { + l.Log("error", "domain not registered", "QS", r.URL.RawQuery, "URL", queryURL) + http.Error(w, "Unregisterd domain", http.StatusNotAcceptable) + return + } + + h.ServeHTTP(w, r) + }) + } +} + +func isValidQueryURL(i string, hosts map[string]bool) bool { + if i == "" || len(i) > 2048 { + return false + } + + qURL, err := url.Parse(i) + if err != nil { + return false + } + + _, exists := hosts[qURL.Host] + return exists + +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..50ef9f3 --- /dev/null +++ b/main.go @@ -0,0 +1,110 @@ +package main + +import ( + "net/http" + "net/http/httputil" + "net/url" + "os" + "time" + + "strings" + + "flag" + + "fmt" + + "github.com/Pimmr/proxima/handlers" + "github.com/go-kit/kit/log" + "github.com/juju/ratelimit" +) + +var ( + allowedHosts argumentList + imaginaryURL string + listenPort int64 + bucketRate float64 + bucketSize int64 + + Version = "dev" + logger = log.With( + log.NewLogfmtLogger(os.Stderr), + "ts", log.DefaultTimestampUTC, + "caller", log.DefaultCaller, + ) +) + +type argumentList []string + +func (l argumentList) String() string { + return strings.Join(l, ",") +} + +func (l *argumentList) Set(value string) error { + *l = append(*l, value) + return nil +} + +func init() { + flag.Var(&allowedHosts, "allow-host", "Repeatable flag for hosts to allow for the URL parameter (e.g. \"d2dktr6aauwgqs.cloudfront.net\")") + flag.StringVar(&imaginaryURL, "imaginary-url", "http://localhost:9000", "URL to imaginary (default: http://localhost:9000)") + flag.Int64Var(&listenPort, "listen-port", 8080, "Port to listen on") + flag.Float64Var(&bucketRate, "bucket-rate", 20, "Rate limiter bucket fill rate (req/s)") + flag.Int64Var(&bucketSize, "bucket-size", 500, "Rate limiter bucket size (burst capacity)") + +} + +func main() { + flag.Parse() + + logger.Log( + "msg", "Starting.", + "version", Version, + "allowed_hosts", allowedHosts.String(), + "imaginary_backend", imaginaryURL, + ) + + rURL, err := url.Parse(imaginaryURL) + if err != nil { + panic(err) + } + + rlBucket := ratelimit.NewBucketWithRate(bucketRate, bucketSize) + + proxy := httputil.NewSingleHostReverseProxy(rURL) + proxy.Transport = &http.Transport{ + DisableCompression: true, + DisableKeepAlives: false, + IdleConnTimeout: 5 * time.Minute, + MaxIdleConns: 10000, + MaxIdleConnsPerHost: 10000, + ResponseHeaderTimeout: 10 * time.Second, + } + + s := &http.Server{ + Addr: fmt.Sprintf(":%d", listenPort), + Handler: decorateHandler(proxy, rlBucket), + ReadHeaderTimeout: 2 * time.Second, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 60 * time.Second, + MaxHeaderBytes: 1 << 20, + } + + s.ListenAndServe() +} + +type httpHandler func(h http.Handler) http.Handler + +func decorateHandler(h http.Handler, b *ratelimit.Bucket) http.Handler { + decorators := []httpHandler{ + handlers.NewRateLimitHandler(b, logger), + handlers.NewIgnoreFaviconRequests(), + handlers.NewValidateURLParameter(logger, allowedHosts), + } + var handler http.Handler = h + for _, d := range decorators { + handler = d(handler) + } + + return handler +}