diff --git a/go.mod b/go.mod index 9802cd4..f071874 100644 --- a/go.mod +++ b/go.mod @@ -2,13 +2,17 @@ module github.com/foomo/contentfulproxy go 1.17 -require github.com/stretchr/testify v1.7.0 +require ( + github.com/stretchr/testify v1.7.0 + go.uber.org/zap v1.19.1 +) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect - go.uber.org/zap v1.19.1 // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + github.com/foomo/keel v0.2.9 ) diff --git a/main.go b/main.go index 36c4183..4164eea 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( ) func main() { - flagAddr := flag.String("addr", ":80", "address to listen to") + flagAddr := flag.String("addr", ":8888", "address to listen to") flag.Parse() l, err := zap.NewProduction() if err != nil { @@ -23,7 +23,15 @@ func main() { if len(args) != 1 { l.Error("unexpected number of args - must be exactly one for backendURL") } - p := proxy.NewProxy(context.Background(), l, args[0]) + p := proxy.NewProxy( + context.Background(), + l, + args[0], + []proxy.WebHookURL{ + "https://www.bestbytes.com", + "https://www.spiegel.de", + }, + ) l.Info("starting proxy for", zap.String("backendURL", args[0]), zap.String("addr", *flagAddr)) l.Error("http listen failed", zap.Error(http.ListenAndServe(*flagAddr, p))) } diff --git a/proxy/cache.go b/proxy/cache.go index 1e114cf..659e8fa 100644 --- a/proxy/cache.go +++ b/proxy/cache.go @@ -1,6 +1,7 @@ package proxy import ( + "go.uber.org/zap" "io/ioutil" "net/http" "sort" @@ -10,6 +11,8 @@ import ( type cacheID string +type requestFlush string + type cachedResponse struct { header http.Header response []byte @@ -19,6 +22,8 @@ type cacheMap map[cacheID]*cachedResponse type cache struct { sync.RWMutex cacheMap cacheMap + webHooks []WebHookURL + l *zap.Logger } func (c *cache) set(id cacheID, response *http.Response) (*cachedResponse, error) { @@ -26,7 +31,10 @@ func (c *cache) set(id cacheID, response *http.Response) (*cachedResponse, error if err != nil { return nil, err } - response.Body.Close() + err = response.Body.Close() + if err != nil { + return nil, err + } c.Lock() defer c.Unlock() cr := &cachedResponse{ @@ -44,6 +52,22 @@ func (c *cache) get(id cacheID) (*cachedResponse, bool) { return response, ok } +func (c *cache) flush() { + c.RLock() + defer c.RUnlock() + c.cacheMap = cacheMap{} +} + +func (c *cache) callWebHooks() { + for _, url := range c.webHooks { + c.l.Info("call webhook", zap.String("url", string(url))) + _, err := http.Get(string(url)) + if err != nil { + c.l.Error("could not call webhook", zap.String("url", string(url))) + } + } +} + func getCacheIDForRequest(r *http.Request) cacheID { id := r.URL.RequestURI() keys := make([]string, len(r.Header)) diff --git a/proxy/jobs.go b/proxy/jobs.go index 76bcdc1..ea82538 100644 --- a/proxy/jobs.go +++ b/proxy/jobs.go @@ -17,7 +17,19 @@ type jobRunner func(job requestJob, id cacheID) func getJobRunner(c *cache, backendURL string, chanJobDone chan requestJobDone) jobRunner { return func(job requestJob, id cacheID) { - resp, err := http.Get(backendURL + job.request.URL.RequestURI()) + req, err := http.NewRequest("GET", backendURL+job.request.URL.RequestURI(), nil) + if err != nil { + chanJobDone <- requestJobDone{ + id: id, + err: err, + } + return + } + for k, v := range job.request.Header { + req.Header.Set(k, string(v[0])) + } + client := http.Client{} + resp, err := client.Do(req) if err != nil { chanJobDone <- requestJobDone{ id: id, diff --git a/proxy/proxy.go b/proxy/proxy.go index 0df4052..6c2d6eb 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -7,33 +7,51 @@ import ( "go.uber.org/zap" ) +type WebHookURL string + type Proxy struct { + l *zap.Logger cache *cache backendURL string chanRequestJob chan requestJob - l *zap.Logger + chanFlushJob chan requestFlush } -func NewProxy(ctx context.Context, l *zap.Logger, backendURL string) *Proxy { +func NewProxy(ctx context.Context, l *zap.Logger, backendURL string, webHooks []WebHookURL) *Proxy { chanRequest := make(chan requestJob) + chanFlush := make(chan requestFlush) c := &cache{ cacheMap: cacheMap{}, + webHooks: webHooks, + l: l, } - go getLoop(ctx, l, backendURL, c, chanRequest) + go getLoop(ctx, l, backendURL, c, chanRequest, chanFlush) return &Proxy{ l: l, cache: c, backendURL: backendURL, chanRequestJob: chanRequest, + chanFlushJob: chanFlush, } } -func getLoop(ctx context.Context, l *zap.Logger, backendURL string, c *cache, chanRequestJob chan requestJob) { +func getLoop( + ctx context.Context, + l *zap.Logger, + backendURL string, + c *cache, + chanRequestJob chan requestJob, + chanFlush chan requestFlush, +) { pendingRequests := map[cacheID][]chan requestJobDone{} chanJobDone := make(chan requestJobDone) jobRunner := getJobRunner(c, backendURL, chanJobDone) for { select { + case command := <-chanFlush: + c.flush() + l.Info("cache flush command coming in", zap.String("flushCommand", string(command))) + c.callWebHooks() case nextJob := <-chanRequestJob: id := getCacheIDForRequest(nextJob.request) pendingRequests[id] = append(pendingRequests[id], nextJob.chanDone) @@ -55,8 +73,16 @@ func getLoop(ctx context.Context, l *zap.Logger, backendURL string, c *cache, ch } func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/flush": + command := requestFlush("doit") + p.chanFlushJob <- command + return + } + switch r.Method { case http.MethodGet: + p.l.Info("serve get request", zap.String("url", r.RequestURI)) cacheID := getCacheIDForRequest(r) cachedResponse, ok := p.cache.get(cacheID) if !ok { @@ -71,13 +97,19 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } cachedResponse = jobDone.cachedResponse + p.l.Info("serve response after cache creation", zap.String("url", r.RequestURI)) + } else { + p.l.Info("serve response from cache", zap.String("url", r.RequestURI)) } for key, values := range cachedResponse.header { for _, value := range values { w.Header().Set(key, value) } } - w.Write(cachedResponse.response) + _, err := w.Write(cachedResponse.response) + if err != nil { + p.l.Info("writing cached response failed", zap.String("url", r.RequestURI)) + } default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) }