From 1082b3136759152d472e82dbf35d82b943b90491 Mon Sep 17 00:00:00 2001 From: Michael Kriese Date: Thu, 21 Nov 2024 22:49:12 +0100 Subject: [PATCH 01/25] fix: partial secure cache --- act/artifactcache/handler.go | 123 +++++++++++++++++++++++------- act/artifactcache/handler_test.go | 6 +- act/artifactcache/mac.go | 41 ++++++++++ act/artifactcache/model.go | 1 + cmd/input.go | 1 + cmd/root.go | 2 +- 6 files changed, 143 insertions(+), 31 deletions(-) create mode 100644 act/artifactcache/mac.go diff --git a/act/artifactcache/handler.go b/act/artifactcache/handler.go index 252564f4..65d70e14 100644 --- a/act/artifactcache/handler.go +++ b/act/artifactcache/handler.go @@ -34,6 +34,7 @@ type Handler struct { listener net.Listener server *http.Server logger logrus.FieldLogger + secret string gcing atomic.Bool gcAt time.Time @@ -41,8 +42,10 @@ type Handler struct { outboundIP string } -func StartHandler(dir, outboundIP string, port uint16, logger logrus.FieldLogger) (*Handler, error) { - h := &Handler{} +func StartHandler(dir, outboundIP string, port uint16, secret string, logger logrus.FieldLogger) (*Handler, error) { + h := &Handler{ + secret: secret, + } if logger == nil { discard := logrus.New() @@ -80,12 +83,12 @@ func StartHandler(dir, outboundIP string, port uint16, logger logrus.FieldLogger } router := httprouter.New() - router.GET(urlBase+"/cache", h.middleware(h.find)) - router.POST(urlBase+"/caches", h.middleware(h.reserve)) - router.PATCH(urlBase+"/caches/:id", h.middleware(h.upload)) - router.POST(urlBase+"/caches/:id", h.middleware(h.commit)) - router.GET(urlBase+"/artifacts/:id", h.middleware(h.get)) - router.POST(urlBase+"/clean", h.middleware(h.clean)) + router.GET(cachePrefixPath+urlBase+"/cache", h.middleware(h.find)) + router.POST(cachePrefixPath+urlBase+"/caches", h.middleware(h.reserve)) + router.PATCH(cachePrefixPath+urlBase+"/caches/:id", h.middleware(h.upload)) + router.POST(cachePrefixPath+urlBase+"/caches/:id", h.middleware(h.commit)) + router.GET(cachePrefixPath+urlBase+"/artifacts/:id", h.middleware(h.get)) + router.POST(cachePrefixPath+urlBase+"/clean", h.middleware(h.clean)) h.router = router @@ -155,7 +158,13 @@ func (h *Handler) openDB() (*bolthold.Store, error) { } // GET /_apis/artifactcache/cache -func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { +func (h *Handler) find(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + repo, err := h.validateMac(params) + if err != nil { + h.responseJSON(w, r, 500, err) + return + } + keys := strings.Split(r.URL.Query().Get("keys"), ",") // cache keys are case insensitive for i, key := range keys { @@ -170,7 +179,7 @@ func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Para } defer db.Close() - cache, err := findCache(db, keys, version) + cache, err := findCache(db, repo, keys, version) if err != nil { h.responseJSON(w, r, 500, err) return @@ -196,7 +205,13 @@ func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Para } // POST /_apis/artifactcache/caches -func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { +func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + repo, err := h.validateMac(params) + if err != nil { + h.responseJSON(w, r, 500, err) + return + } + api := &Request{} if err := json.NewDecoder(r.Body).Decode(api); err != nil { h.responseJSON(w, r, 400, err) @@ -216,6 +231,7 @@ func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.P now := time.Now().Unix() cache.CreatedAt = now cache.UsedAt = now + cache.Repo = repo if err := insertCache(db, cache); err != nil { h.responseJSON(w, r, 500, err) return @@ -227,6 +243,12 @@ func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.P // PATCH /_apis/artifactcache/caches/:id func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + repo, err := h.validateMac(params) + if err != nil { + h.responseJSON(w, r, 500, err) + return + } + id, err := strconv.ParseUint(params.ByName("id"), 10, 64) if err != nil { h.responseJSON(w, r, 400, err) @@ -249,11 +271,17 @@ func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprout return } + // Should not happen + if cache.Repo != repo { + h.responseJSON(w, r, 500, ErrValidation) + return + } + if cache.Complete { h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key)) return } - db.Close() + defer db.Close() start, _, err := parseContentRange(r.Header.Get("Content-Range")) if err != nil { h.responseJSON(w, r, 400, err) @@ -262,12 +290,18 @@ func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprout if err := h.storage.Write(cache.ID, start, r.Body); err != nil { h.responseJSON(w, r, 500, err) } - h.useCache(id) + h.useCache(db, cache) h.responseJSON(w, r, 200) } // POST /_apis/artifactcache/caches/:id func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + repo, err := h.validateMac(params) + if err != nil { + h.responseJSON(w, r, 500, err) + return + } + id, err := strconv.ParseUint(params.ByName("id"), 10, 64) if err != nil { h.responseJSON(w, r, 400, err) @@ -290,6 +324,12 @@ func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprout return } + // Should not happen + if cache.Repo != repo { + h.responseJSON(w, r, 500, ErrValidation) + return + } + if cache.Complete { h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key)) return @@ -323,17 +363,51 @@ func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprout // GET /_apis/artifactcache/artifacts/:id func (h *Handler) get(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + repo, err := h.validateMac(params) + if err != nil { + h.responseJSON(w, r, 500, err) + return + } + id, err := strconv.ParseUint(params.ByName("id"), 10, 64) if err != nil { h.responseJSON(w, r, 400, err) return } - h.useCache(id) + + cache := &Cache{} + db, err := h.openDB() + if err != nil { + h.responseJSON(w, r, 500, err) + return + } + defer db.Close() + if err := db.Get(id, cache); err != nil { + if errors.Is(err, bolthold.ErrNotFound) { + h.responseJSON(w, r, 400, fmt.Errorf("cache %d: not reserved", id)) + return + } + h.responseJSON(w, r, 500, err) + return + } + + // Should not happen + if cache.Repo != repo { + h.responseJSON(w, r, 500, ErrValidation) + return + } + + h.useCache(db, cache) h.storage.Serve(w, r, id) } // POST /_apis/artifactcache/clean -func (h *Handler) clean(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { +func (h *Handler) clean(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + _, err := h.validateMac(params) + if err != nil { + h.responseJSON(w, r, 500, err) + return + } // TODO: don't support force deleting cache entries // see: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries @@ -349,12 +423,13 @@ func (h *Handler) middleware(handler httprouter.Handle) httprouter.Handle { } // if not found, return (nil, nil) instead of an error. -func findCache(db *bolthold.Store, keys []string, version string) (*Cache, error) { +func findCache(db *bolthold.Store, repo string, keys []string, version string) (*Cache, error) { cache := &Cache{} for _, prefix := range keys { // if a key in the list matches exactly, don't return partial matches if err := db.FindOne(cache, - bolthold.Where("Key").Eq(prefix). + bolthold.Where("Repo").Eq(repo). + And("Key").Eq(prefix). And("Version").Eq(version). And("Complete").Eq(true). SortBy("CreatedAt").Reverse()); err == nil || !errors.Is(err, bolthold.ErrNotFound) { @@ -369,7 +444,8 @@ func findCache(db *bolthold.Store, keys []string, version string) (*Cache, error continue } if err := db.FindOne(cache, - bolthold.Where("Key").RegExp(re). + bolthold.Where("Repo").Eq(repo). + And("Key").RegExp(re). And("Version").Eq(version). And("Complete").Eq(true). SortBy("CreatedAt").Reverse()); err != nil { @@ -394,16 +470,7 @@ func insertCache(db *bolthold.Store, cache *Cache) error { return nil } -func (h *Handler) useCache(id uint64) { - db, err := h.openDB() - if err != nil { - return - } - defer db.Close() - cache := &Cache{} - if err := db.Get(id, cache); err != nil { - return - } +func (h *Handler) useCache(db *bolthold.Store, cache *Cache) { cache.UsedAt = time.Now().Unix() _ = db.Update(cache.ID, cache) } diff --git a/act/artifactcache/handler_test.go b/act/artifactcache/handler_test.go index 252c4209..7391c73c 100644 --- a/act/artifactcache/handler_test.go +++ b/act/artifactcache/handler_test.go @@ -20,9 +20,11 @@ import ( func TestHandler(t *testing.T) { dir := filepath.Join(t.TempDir(), "artifactcache") - handler, err := StartHandler(dir, "", 0, nil) + handler, err := StartHandler(dir, "", 0, "secret", nil) require.NoError(t, err) + t.Skip("TODO: handle secret") + base := fmt.Sprintf("%s%s", handler.ExternalURL(), urlBase) defer func() { @@ -589,7 +591,7 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte func TestHandler_gcCache(t *testing.T) { dir := filepath.Join(t.TempDir(), "artifactcache") - handler, err := StartHandler(dir, "", 0, nil) + handler, err := StartHandler(dir, "", 0, "", nil) require.NoError(t, err) defer func() { diff --git a/act/artifactcache/mac.go b/act/artifactcache/mac.go new file mode 100644 index 00000000..3e6ac246 --- /dev/null +++ b/act/artifactcache/mac.go @@ -0,0 +1,41 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package artifactcache + +import ( + "crypto/hmac" + "crypto/sha256" + "errors" + "hash" + + "github.com/julienschmidt/httprouter" +) + +var ( + ErrValidation = errors.New("repo validation error") + cachePrefixPath = "repo:/run:/time:/mac:/" +) + +func (h *Handler) validateMac(params httprouter.Params) (string, error) { + repo := params.ByName("repo") + run := params.ByName("run") + time := params.ByName("time") + messageMAC := params.ByName("mac") + + mac := computeMac(h.secret, repo, run, time) + expectedMAC := mac.Sum(nil) + + if hmac.Equal([]byte(messageMAC), expectedMAC) { + return repo, nil + } + return repo, ErrValidation +} + +func computeMac(key, repo, run, time string) hash.Hash { + mac := hmac.New(sha256.New, []byte(key)) + mac.Write([]byte(repo)) + mac.Write([]byte(run)) + mac.Write([]byte(time)) + return mac +} diff --git a/act/artifactcache/model.go b/act/artifactcache/model.go index 57812b31..1c0f855d 100644 --- a/act/artifactcache/model.go +++ b/act/artifactcache/model.go @@ -25,6 +25,7 @@ func (c *Request) ToCache() *Cache { type Cache struct { ID uint64 `json:"id" boltholdKey:"ID"` + Repo string `json:"repo" boltholdIndex:"Repo"` Key string `json:"key" boltholdIndex:"Key"` Version string `json:"version" boltholdIndex:"Version"` Size int64 `json:"cacheSize"` diff --git a/cmd/input.go b/cmd/input.go index 36af6d86..e5aff64e 100644 --- a/cmd/input.go +++ b/cmd/input.go @@ -59,6 +59,7 @@ type Input struct { logPrefixJobID bool networkName string useNewActionCache bool + secret string } func (i *Input) resolve(path string) string { diff --git a/cmd/root.go b/cmd/root.go index 349e6ac4..c5d4ce0a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -632,7 +632,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str var cacheHandler *artifactcache.Handler if !input.noCacheServer && envs[cacheURLKey] == "" { var err error - cacheHandler, err = artifactcache.StartHandler(input.cacheServerPath, input.cacheServerAddr, input.cacheServerPort, common.Logger(ctx)) + cacheHandler, err = artifactcache.StartHandler(input.cacheServerPath, input.cacheServerAddr, input.cacheServerPort, input.secret, common.Logger(ctx)) if err != nil { return err } From 21ca8102fa331ad6284df43800be3e94ebef7bf0 Mon Sep 17 00:00:00 2001 From: Michael Kriese Date: Fri, 22 Nov 2024 01:36:40 +0100 Subject: [PATCH 02/25] fix: validate timestamp --- act/artifactcache/mac.go | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/act/artifactcache/mac.go b/act/artifactcache/mac.go index 3e6ac246..862ef9db 100644 --- a/act/artifactcache/mac.go +++ b/act/artifactcache/mac.go @@ -8,34 +8,56 @@ import ( "crypto/sha256" "errors" "hash" + "strconv" + "time" "github.com/julienschmidt/httprouter" ) var ( - ErrValidation = errors.New("repo validation error") - cachePrefixPath = "repo:/run:/time:/mac:/" + ErrValidation = errors.New("validation error") + cachePrefixPath = "repo:/run:/ts:/mac:/" ) func (h *Handler) validateMac(params httprouter.Params) (string, error) { + ts := params.ByName("ts") + repo := params.ByName("repo") run := params.ByName("run") - time := params.ByName("time") messageMAC := params.ByName("mac") - mac := computeMac(h.secret, repo, run, time) - expectedMAC := mac.Sum(nil) + // TODO: allow configurable max age + if !validateAge(ts) { + return "", ErrValidation + } + expectedMAC := computeMac(h.secret, repo, run, ts).Sum(nil) if hmac.Equal([]byte(messageMAC), expectedMAC) { return repo, nil } return repo, ErrValidation } -func computeMac(key, repo, run, time string) hash.Hash { +func validateAge(ts string) bool { + tsInt, err := strconv.ParseInt(ts, 10, 64) + if err != nil { + return false + } + if tsInt > time.Now().Unix() { + return false + } + return true +} + +func computeMac(key, repo, run, ts string) hash.Hash { mac := hmac.New(sha256.New, []byte(key)) mac.Write([]byte(repo)) mac.Write([]byte(run)) - mac.Write([]byte(time)) + mac.Write([]byte(ts)) return mac } + +func ComputeMac(key, repo, run, ts string) string { + mac := computeMac(key, repo, run, ts) + return string(mac.Sum(nil)) +} From 7458ddfaf4176802907654e7b3ec88700b35434e Mon Sep 17 00:00:00 2001 From: Michael Kriese Date: Fri, 22 Nov 2024 01:59:03 +0100 Subject: [PATCH 03/25] fix: join org and repo --- act/artifactcache/mac.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/act/artifactcache/mac.go b/act/artifactcache/mac.go index 862ef9db..968bc6f0 100644 --- a/act/artifactcache/mac.go +++ b/act/artifactcache/mac.go @@ -16,13 +16,13 @@ import ( var ( ErrValidation = errors.New("validation error") - cachePrefixPath = "repo:/run:/ts:/mac:/" + cachePrefixPath = "org:/repo:/run:/ts:/mac:/" ) func (h *Handler) validateMac(params httprouter.Params) (string, error) { ts := params.ByName("ts") - repo := params.ByName("repo") + repo := params.ByName("org") + "/" + params.ByName("repo") run := params.ByName("run") messageMAC := params.ByName("mac") From 8bc36fb69a857b551369a26445bb615616e70aaf Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Thu, 24 Oct 2024 15:04:04 +0200 Subject: [PATCH 04/25] create cache proxy based on cache server --- act/cacheproxy/handler.go | 427 ++++++++++++++++++++++++++++++++++++++ act/cacheproxy/model.go | 44 ++++ 2 files changed, 471 insertions(+) create mode 100644 act/cacheproxy/handler.go create mode 100644 act/cacheproxy/model.go diff --git a/act/cacheproxy/handler.go b/act/cacheproxy/handler.go new file mode 100644 index 00000000..89234c49 --- /dev/null +++ b/act/cacheproxy/handler.go @@ -0,0 +1,427 @@ +package cacheproxy + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "regexp" + "strconv" + "strings" + "time" + + "github.com/julienschmidt/httprouter" + "github.com/sirupsen/logrus" + "github.com/timshannon/bolthold" + + "github.com/nektos/act/pkg/common" +) + +const ( + urlBase = "/_apis/artifactcache" +) + +type Handler struct { + router *httprouter.Router + listener net.Listener + server *http.Server + logger logrus.FieldLogger + + outboundIP string + + cacheServerHost string + cacheServerPort uint16 + + repositoryName string +} + +func StartHandler(repoName string, targetHost string, targetPort uint16, outboundIP string, port uint16, logger logrus.FieldLogger) (*Handler, error) { + h := &Handler{} + + if logger == nil { + discard := logrus.New() + discard.Out = io.Discard + logger = discard + } + logger = logger.WithField("module", "artifactcache") + h.logger = logger + + h.repositoryName = repoName + + if outboundIP != "" { + h.outboundIP = outboundIP + } else if ip := common.GetOutboundIP(); ip == nil { + return nil, fmt.Errorf("unable to determine outbound IP address") + } else { + h.outboundIP = ip.String() + } + + router := httprouter.New() + router.GET(urlBase+"/cache", h.middleware(h.find)) + router.POST(urlBase+"/caches", h.middleware(h.reserve)) + router.PATCH(urlBase+"/caches/:id", h.middleware(h.upload)) + router.POST(urlBase+"/caches/:id", h.middleware(h.commit)) + router.GET(urlBase+"/artifacts/:id", h.middleware(h.get)) + router.POST(urlBase+"/clean", h.middleware(h.clean)) + + h.router = router + + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) // listen on all interfaces + if err != nil { + return nil, err + } + server := &http.Server{ + ReadHeaderTimeout: 2 * time.Second, + Handler: router, + } + go func() { + if err := server.Serve(listener); err != nil && errors.Is(err, net.ErrClosed) { + logger.Errorf("http serve: %v", err) + } + }() + h.listener = listener + h.server = server + + return h, nil +} + +func (h *Handler) ExternalURL() string { + // TODO: make the external url configurable if necessary + return fmt.Sprintf("http://%s:%d", + h.outboundIP, + h.listener.Addr().(*net.TCPAddr).Port) +} + +func (h *Handler) Close() error { + if h == nil { + return nil + } + var retErr error + if h.server != nil { + err := h.server.Close() + if err != nil { + retErr = err + } + h.server = nil + } + if h.listener != nil { + err := h.listener.Close() + if errors.Is(err, net.ErrClosed) { + err = nil + } + if err != nil { + retErr = err + } + h.listener = nil + } + return retErr +} + +// GET /_apis/artifactcache/cache +func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + keys := strings.Split(r.URL.Query().Get("keys"), ",") + // cache keys are case insensitive + for i, key := range keys { + keys[i] = strings.ToLower(key) + } + version := r.URL.Query().Get("version") + + db, err := h.openDB() + if err != nil { + h.responseJSON(w, r, 500, err) + return + } + defer db.Close() + + cache, err := h.findCache(db, keys, version) + if err != nil { + h.responseJSON(w, r, 500, err) + return + } + if cache == nil { + h.responseJSON(w, r, 204) + return + } + + if ok, err := h.storage.Exist(cache.ID); err != nil { + h.responseJSON(w, r, 500, err) + return + } else if !ok { + _ = db.Delete(cache.ID, cache) + h.responseJSON(w, r, 204) + return + } + h.responseJSON(w, r, 200, map[string]any{ + "result": "hit", + "archiveLocation": fmt.Sprintf("%s%s/artifacts/%d", h.ExternalURL(), urlBase, cache.ID), + "cacheKey": cache.Key, + }) +} + +// POST /_apis/artifactcache/caches +func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + api := &Request{} + if err := json.NewDecoder(r.Body).Decode(api); err != nil { + h.responseJSON(w, r, 400, err) + return + } + // cache keys are case insensitive + api.Key = strings.ToLower(api.Key) + + cache := api.ToCache() + cache.FillKeyVersionHash() + db, err := h.openDB() + if err != nil { + h.responseJSON(w, r, 500, err) + return + } + defer db.Close() + if err := db.FindOne(cache, bolthold.Where("KeyVersionHash").Eq(cache.KeyVersionHash)); err != nil { + if !errors.Is(err, bolthold.ErrNotFound) { + h.responseJSON(w, r, 500, err) + return + } + } else { + h.responseJSON(w, r, 400, fmt.Errorf("already exist")) + return + } + + now := time.Now().Unix() + cache.CreatedAt = now + cache.UsedAt = now + if err := db.Insert(bolthold.NextSequence(), cache); err != nil { + h.responseJSON(w, r, 500, err) + return + } + // write back id to db + if err := db.Update(cache.ID, cache); err != nil { + h.responseJSON(w, r, 500, err) + return + } + h.responseJSON(w, r, 200, map[string]any{ + "cacheId": cache.ID, + }) +} + +// PATCH /_apis/artifactcache/caches/:id +func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + id, err := strconv.ParseInt(params.ByName("id"), 10, 64) + if err != nil { + h.responseJSON(w, r, 400, err) + return + } + + cache := &Cache{} + db, err := h.openDB() + if err != nil { + h.responseJSON(w, r, 500, err) + return + } + defer db.Close() + if err := db.Get(id, cache); err != nil { + if errors.Is(err, bolthold.ErrNotFound) { + h.responseJSON(w, r, 400, fmt.Errorf("cache %d: not reserved", id)) + return + } + h.responseJSON(w, r, 500, err) + return + } + + if cache.Complete { + h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key)) + return + } + db.Close() + start, _, err := parseContentRange(r.Header.Get("Content-Range")) + if err != nil { + h.responseJSON(w, r, 400, err) + return + } + if err := h.storage.Write(cache.ID, start, r.Body); err != nil { + h.responseJSON(w, r, 500, err) + } + h.useCache(id) + h.responseJSON(w, r, 200) +} + +// POST /_apis/artifactcache/caches/:id +func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + id, err := strconv.ParseInt(params.ByName("id"), 10, 64) + if err != nil { + h.responseJSON(w, r, 400, err) + return + } + + cache := &Cache{} + db, err := h.openDB() + if err != nil { + h.responseJSON(w, r, 500, err) + return + } + defer db.Close() + if err := db.Get(id, cache); err != nil { + if errors.Is(err, bolthold.ErrNotFound) { + h.responseJSON(w, r, 400, fmt.Errorf("cache %d: not reserved", id)) + return + } + h.responseJSON(w, r, 500, err) + return + } + + if cache.Complete { + h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key)) + return + } + + db.Close() + + size, err := h.storage.Commit(cache.ID, cache.Size) + if err != nil { + h.responseJSON(w, r, 500, err) + return + } + // write real size back to cache, it may be different from the current value when the request doesn't specify it. + cache.Size = size + + db, err = h.openDB() + if err != nil { + h.responseJSON(w, r, 500, err) + return + } + defer db.Close() + + cache.Complete = true + if err := db.Update(cache.ID, cache); err != nil { + h.responseJSON(w, r, 500, err) + return + } + + h.responseJSON(w, r, 200) +} + +// GET /_apis/artifactcache/artifacts/:id +func (h *Handler) get(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + id, err := strconv.ParseInt(params.ByName("id"), 10, 64) + if err != nil { + h.responseJSON(w, r, 400, err) + return + } + h.useCache(id) + h.storage.Serve(w, r, uint64(id)) +} + +// POST /_apis/artifactcache/clean +func (h *Handler) clean(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + // TODO: don't support force deleting cache entries + // see: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries + + h.responseJSON(w, r, 200) +} + +func (h *Handler) middleware(handler httprouter.Handle) httprouter.Handle { + return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + h.logger.Debugf("%s %s", r.Method, r.RequestURI) + handler(w, r, params) + go h.gcCache() + } +} + +// if not found, return (nil, nil) instead of an error. +func (h *Handler) findCache(db *bolthold.Store, keys []string, version string) (*Cache, error) { + if len(keys) == 0 { + return nil, nil + } + key := keys[0] // the first key is for exact match. + + cache := &Cache{ + Key: key, + Version: version, + } + cache.FillKeyVersionHash() + + if err := db.FindOne(cache, bolthold.Where("KeyVersionHash").Eq(cache.KeyVersionHash)); err != nil { + if !errors.Is(err, bolthold.ErrNotFound) { + return nil, err + } + } else if cache.Complete { + return cache, nil + } + stop := fmt.Errorf("stop") + + for _, prefix := range keys[1:] { + found := false + prefixPattern := fmt.Sprintf("^%s", regexp.QuoteMeta(prefix)) + re, err := regexp.Compile(prefixPattern) + if err != nil { + continue + } + if err := db.ForEach(bolthold.Where("Key").RegExp(re).And("Version").Eq(version).SortBy("CreatedAt").Reverse(), func(v *Cache) error { + if !strings.HasPrefix(v.Key, prefix) { + return stop + } + if v.Complete { + cache = v + found = true + return stop + } + return nil + }); err != nil { + if !errors.Is(err, stop) { + return nil, err + } + } + if found { + return cache, nil + } + } + return nil, nil +} + +func (h *Handler) useCache(id int64) { + db, err := h.openDB() + if err != nil { + return + } + defer db.Close() + cache := &Cache{} + if err := db.Get(id, cache); err != nil { + return + } + cache.UsedAt = time.Now().Unix() + _ = db.Update(cache.ID, cache) +} + +func (h *Handler) responseJSON(w http.ResponseWriter, r *http.Request, code int, v ...any) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + var data []byte + if len(v) == 0 || v[0] == nil { + data, _ = json.Marshal(struct{}{}) + } else if err, ok := v[0].(error); ok { + h.logger.Errorf("%v %v: %v", r.Method, r.RequestURI, err) + data, _ = json.Marshal(map[string]any{ + "error": err.Error(), + }) + } else { + data, _ = json.Marshal(v[0]) + } + w.WriteHeader(code) + _, _ = w.Write(data) +} + +func parseContentRange(s string) (int64, int64, error) { + // support the format like "bytes 11-22/*" only + s, _, _ = strings.Cut(strings.TrimPrefix(s, "bytes "), "/") + s1, s2, _ := strings.Cut(s, "-") + + start, err := strconv.ParseInt(s1, 10, 64) + if err != nil { + return 0, 0, fmt.Errorf("parse %q: %w", s, err) + } + stop, err := strconv.ParseInt(s2, 10, 64) + if err != nil { + return 0, 0, fmt.Errorf("parse %q: %w", s, err) + } + return start, stop, nil +} diff --git a/act/cacheproxy/model.go b/act/cacheproxy/model.go new file mode 100644 index 00000000..de2fd9ea --- /dev/null +++ b/act/cacheproxy/model.go @@ -0,0 +1,44 @@ +package cacheproxy + +import ( + "crypto/sha256" + "fmt" +) + +type Request struct { + Key string `json:"key" ` + Version string `json:"version"` + Size int64 `json:"cacheSize"` +} + +func (c *Request) ToCache() *Cache { + if c == nil { + return nil + } + ret := &Cache{ + Key: c.Key, + Version: c.Version, + Size: c.Size, + } + if c.Size == 0 { + // So the request comes from old versions of actions, like `actions/cache@v2`. + // It doesn't send cache size. Set it to -1 to indicate that. + ret.Size = -1 + } + return ret +} + +type Cache struct { + ID uint64 `json:"id" boltholdKey:"ID"` + Key string `json:"key" boltholdIndex:"Key"` + Version string `json:"version" boltholdIndex:"Version"` + KeyVersionHash string `json:"keyVersionHash" boltholdUnique:"KeyVersionHash"` + Size int64 `json:"cacheSize"` + Complete bool `json:"complete"` + UsedAt int64 `json:"usedAt" boltholdIndex:"UsedAt"` + CreatedAt int64 `json:"createdAt" boltholdIndex:"CreatedAt"` +} + +func (c *Cache) FillKeyVersionHash() { + c.KeyVersionHash = fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%s:%s", c.Key, c.Version)))) +} From 11006f4ef3614c83432b77c8345f845b6802be9d Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Thu, 24 Oct 2024 16:24:28 +0200 Subject: [PATCH 05/25] implement hmac generation --- act/cacheproxy/handler.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/act/cacheproxy/handler.go b/act/cacheproxy/handler.go index 89234c49..69f36fff 100644 --- a/act/cacheproxy/handler.go +++ b/act/cacheproxy/handler.go @@ -1,6 +1,9 @@ package cacheproxy import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -35,9 +38,10 @@ type Handler struct { cacheServerPort uint16 repositoryName string + cacheKey string } -func StartHandler(repoName string, targetHost string, targetPort uint16, outboundIP string, port uint16, logger logrus.FieldLogger) (*Handler, error) { +func StartHandler(repoName string, targetHost string, targetPort uint16, outboundIP string, port uint16, cacheKey string, logger logrus.FieldLogger) (*Handler, error) { h := &Handler{} if logger == nil { @@ -49,6 +53,7 @@ func StartHandler(repoName string, targetHost string, targetPort uint16, outboun h.logger = logger h.repositoryName = repoName + h.cacheKey = h.cacheKey if outboundIP != "" { h.outboundIP = outboundIP @@ -119,6 +124,13 @@ func (h *Handler) Close() error { return retErr } +func (h *Handler) calculateMAC() string { + mac := hmac.New(sha256.New, []byte(h.cacheKey)) + mac.Write([]byte(h.repositoryName)) + macBytes := mac.Sum(nil) + return hex.EncodeToString(macBytes) +} + // GET /_apis/artifactcache/cache func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { keys := strings.Split(r.URL.Query().Get("keys"), ",") From 4b5ffd768f48aec38af5c1da68df73641c2bc9d8 Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Fri, 25 Oct 2024 17:15:43 +0200 Subject: [PATCH 06/25] implement proxy server --- act/cacheproxy/handler.go | 376 +++++--------------------------------- act/cacheproxy/model.go | 44 ----- 2 files changed, 49 insertions(+), 371 deletions(-) delete mode 100644 act/cacheproxy/model.go diff --git a/act/cacheproxy/handler.go b/act/cacheproxy/handler.go index 69f36fff..40bab63a 100644 --- a/act/cacheproxy/handler.go +++ b/act/cacheproxy/handler.go @@ -4,20 +4,17 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/hex" - "encoding/json" "errors" "fmt" "io" "net" "net/http" - "regexp" - "strconv" - "strings" + "net/http/httputil" + "net/url" "time" "github.com/julienschmidt/httprouter" "github.com/sirupsen/logrus" - "github.com/timshannon/bolthold" "github.com/nektos/act/pkg/common" ) @@ -35,13 +32,12 @@ type Handler struct { outboundIP string cacheServerHost string - cacheServerPort uint16 - repositoryName string - cacheKey string + repositoryName string + repositorySecret string } -func StartHandler(repoName string, targetHost string, targetPort uint16, outboundIP string, port uint16, cacheKey string, logger logrus.FieldLogger) (*Handler, error) { +func StartHandler(repoName string, targetHost string, outboundIP string, port uint16, cacheSecret string, logger logrus.FieldLogger) (*Handler, error) { h := &Handler{} if logger == nil { @@ -53,7 +49,11 @@ func StartHandler(repoName string, targetHost string, targetPort uint16, outboun h.logger = logger h.repositoryName = repoName - h.cacheKey = h.cacheKey + repoSecret, err := calculateMAC(repoName, cacheSecret) + if err != nil { + return nil, fmt.Errorf("unable to decode cacheSecret") + } + h.repositorySecret = repoSecret if outboundIP != "" { h.outboundIP = outboundIP @@ -63,13 +63,17 @@ func StartHandler(repoName string, targetHost string, targetPort uint16, outboun h.outboundIP = ip.String() } + h.cacheServerHost = targetHost + + proxy, err := h.newReverseProxy(targetHost) + if err != nil { + return nil, fmt.Errorf("unable to set up proxy to target host") + } + router := httprouter.New() - router.GET(urlBase+"/cache", h.middleware(h.find)) - router.POST(urlBase+"/caches", h.middleware(h.reserve)) - router.PATCH(urlBase+"/caches/:id", h.middleware(h.upload)) - router.POST(urlBase+"/caches/:id", h.middleware(h.commit)) - router.GET(urlBase+"/artifacts/:id", h.middleware(h.get)) - router.POST(urlBase+"/clean", h.middleware(h.clean)) + router.HandlerFunc("GET", urlBase, proxyRequestHandler(proxy)) + router.HandlerFunc("POST", urlBase, proxyRequestHandler(proxy)) + router.HandlerFunc("PATCH", urlBase, proxyRequestHandler(proxy)) h.router = router @@ -92,6 +96,27 @@ func StartHandler(repoName string, targetHost string, targetPort uint16, outboun return h, nil } +func proxyRequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + proxy.ServeHTTP(w, r) + } +} + +func (h *Handler) newReverseProxy(targetHost string) (*httputil.ReverseProxy, error) { + url, err := url.Parse(targetHost) + if err != nil { + return nil, err + } + + proxy := httputil.NewSingleHostReverseProxy(url) + proxy.Rewrite = func(r *httputil.ProxyRequest) { h.injectAuth(r) } + return proxy, nil +} + +func (h *Handler) injectAuth(r *httputil.ProxyRequest) { + r.Out.SetBasicAuth(h.repositoryName, h.repositorySecret) +} + func (h *Handler) ExternalURL() string { // TODO: make the external url configurable if necessary return fmt.Sprintf("http://%s:%d", @@ -124,316 +149,13 @@ func (h *Handler) Close() error { return retErr } -func (h *Handler) calculateMAC() string { - mac := hmac.New(sha256.New, []byte(h.cacheKey)) - mac.Write([]byte(h.repositoryName)) +func calculateMAC(repoName string, cacheSecret string) (string, error) { + sec, err := hex.DecodeString(cacheSecret) + if err != nil { + return "", err + } + mac := hmac.New(sha256.New, sec) + mac.Write([]byte(repoName)) macBytes := mac.Sum(nil) - return hex.EncodeToString(macBytes) -} - -// GET /_apis/artifactcache/cache -func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - keys := strings.Split(r.URL.Query().Get("keys"), ",") - // cache keys are case insensitive - for i, key := range keys { - keys[i] = strings.ToLower(key) - } - version := r.URL.Query().Get("version") - - db, err := h.openDB() - if err != nil { - h.responseJSON(w, r, 500, err) - return - } - defer db.Close() - - cache, err := h.findCache(db, keys, version) - if err != nil { - h.responseJSON(w, r, 500, err) - return - } - if cache == nil { - h.responseJSON(w, r, 204) - return - } - - if ok, err := h.storage.Exist(cache.ID); err != nil { - h.responseJSON(w, r, 500, err) - return - } else if !ok { - _ = db.Delete(cache.ID, cache) - h.responseJSON(w, r, 204) - return - } - h.responseJSON(w, r, 200, map[string]any{ - "result": "hit", - "archiveLocation": fmt.Sprintf("%s%s/artifacts/%d", h.ExternalURL(), urlBase, cache.ID), - "cacheKey": cache.Key, - }) -} - -// POST /_apis/artifactcache/caches -func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - api := &Request{} - if err := json.NewDecoder(r.Body).Decode(api); err != nil { - h.responseJSON(w, r, 400, err) - return - } - // cache keys are case insensitive - api.Key = strings.ToLower(api.Key) - - cache := api.ToCache() - cache.FillKeyVersionHash() - db, err := h.openDB() - if err != nil { - h.responseJSON(w, r, 500, err) - return - } - defer db.Close() - if err := db.FindOne(cache, bolthold.Where("KeyVersionHash").Eq(cache.KeyVersionHash)); err != nil { - if !errors.Is(err, bolthold.ErrNotFound) { - h.responseJSON(w, r, 500, err) - return - } - } else { - h.responseJSON(w, r, 400, fmt.Errorf("already exist")) - return - } - - now := time.Now().Unix() - cache.CreatedAt = now - cache.UsedAt = now - if err := db.Insert(bolthold.NextSequence(), cache); err != nil { - h.responseJSON(w, r, 500, err) - return - } - // write back id to db - if err := db.Update(cache.ID, cache); err != nil { - h.responseJSON(w, r, 500, err) - return - } - h.responseJSON(w, r, 200, map[string]any{ - "cacheId": cache.ID, - }) -} - -// PATCH /_apis/artifactcache/caches/:id -func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprouter.Params) { - id, err := strconv.ParseInt(params.ByName("id"), 10, 64) - if err != nil { - h.responseJSON(w, r, 400, err) - return - } - - cache := &Cache{} - db, err := h.openDB() - if err != nil { - h.responseJSON(w, r, 500, err) - return - } - defer db.Close() - if err := db.Get(id, cache); err != nil { - if errors.Is(err, bolthold.ErrNotFound) { - h.responseJSON(w, r, 400, fmt.Errorf("cache %d: not reserved", id)) - return - } - h.responseJSON(w, r, 500, err) - return - } - - if cache.Complete { - h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key)) - return - } - db.Close() - start, _, err := parseContentRange(r.Header.Get("Content-Range")) - if err != nil { - h.responseJSON(w, r, 400, err) - return - } - if err := h.storage.Write(cache.ID, start, r.Body); err != nil { - h.responseJSON(w, r, 500, err) - } - h.useCache(id) - h.responseJSON(w, r, 200) -} - -// POST /_apis/artifactcache/caches/:id -func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprouter.Params) { - id, err := strconv.ParseInt(params.ByName("id"), 10, 64) - if err != nil { - h.responseJSON(w, r, 400, err) - return - } - - cache := &Cache{} - db, err := h.openDB() - if err != nil { - h.responseJSON(w, r, 500, err) - return - } - defer db.Close() - if err := db.Get(id, cache); err != nil { - if errors.Is(err, bolthold.ErrNotFound) { - h.responseJSON(w, r, 400, fmt.Errorf("cache %d: not reserved", id)) - return - } - h.responseJSON(w, r, 500, err) - return - } - - if cache.Complete { - h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key)) - return - } - - db.Close() - - size, err := h.storage.Commit(cache.ID, cache.Size) - if err != nil { - h.responseJSON(w, r, 500, err) - return - } - // write real size back to cache, it may be different from the current value when the request doesn't specify it. - cache.Size = size - - db, err = h.openDB() - if err != nil { - h.responseJSON(w, r, 500, err) - return - } - defer db.Close() - - cache.Complete = true - if err := db.Update(cache.ID, cache); err != nil { - h.responseJSON(w, r, 500, err) - return - } - - h.responseJSON(w, r, 200) -} - -// GET /_apis/artifactcache/artifacts/:id -func (h *Handler) get(w http.ResponseWriter, r *http.Request, params httprouter.Params) { - id, err := strconv.ParseInt(params.ByName("id"), 10, 64) - if err != nil { - h.responseJSON(w, r, 400, err) - return - } - h.useCache(id) - h.storage.Serve(w, r, uint64(id)) -} - -// POST /_apis/artifactcache/clean -func (h *Handler) clean(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - // TODO: don't support force deleting cache entries - // see: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries - - h.responseJSON(w, r, 200) -} - -func (h *Handler) middleware(handler httprouter.Handle) httprouter.Handle { - return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) { - h.logger.Debugf("%s %s", r.Method, r.RequestURI) - handler(w, r, params) - go h.gcCache() - } -} - -// if not found, return (nil, nil) instead of an error. -func (h *Handler) findCache(db *bolthold.Store, keys []string, version string) (*Cache, error) { - if len(keys) == 0 { - return nil, nil - } - key := keys[0] // the first key is for exact match. - - cache := &Cache{ - Key: key, - Version: version, - } - cache.FillKeyVersionHash() - - if err := db.FindOne(cache, bolthold.Where("KeyVersionHash").Eq(cache.KeyVersionHash)); err != nil { - if !errors.Is(err, bolthold.ErrNotFound) { - return nil, err - } - } else if cache.Complete { - return cache, nil - } - stop := fmt.Errorf("stop") - - for _, prefix := range keys[1:] { - found := false - prefixPattern := fmt.Sprintf("^%s", regexp.QuoteMeta(prefix)) - re, err := regexp.Compile(prefixPattern) - if err != nil { - continue - } - if err := db.ForEach(bolthold.Where("Key").RegExp(re).And("Version").Eq(version).SortBy("CreatedAt").Reverse(), func(v *Cache) error { - if !strings.HasPrefix(v.Key, prefix) { - return stop - } - if v.Complete { - cache = v - found = true - return stop - } - return nil - }); err != nil { - if !errors.Is(err, stop) { - return nil, err - } - } - if found { - return cache, nil - } - } - return nil, nil -} - -func (h *Handler) useCache(id int64) { - db, err := h.openDB() - if err != nil { - return - } - defer db.Close() - cache := &Cache{} - if err := db.Get(id, cache); err != nil { - return - } - cache.UsedAt = time.Now().Unix() - _ = db.Update(cache.ID, cache) -} - -func (h *Handler) responseJSON(w http.ResponseWriter, r *http.Request, code int, v ...any) { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - var data []byte - if len(v) == 0 || v[0] == nil { - data, _ = json.Marshal(struct{}{}) - } else if err, ok := v[0].(error); ok { - h.logger.Errorf("%v %v: %v", r.Method, r.RequestURI, err) - data, _ = json.Marshal(map[string]any{ - "error": err.Error(), - }) - } else { - data, _ = json.Marshal(v[0]) - } - w.WriteHeader(code) - _, _ = w.Write(data) -} - -func parseContentRange(s string) (int64, int64, error) { - // support the format like "bytes 11-22/*" only - s, _, _ = strings.Cut(strings.TrimPrefix(s, "bytes "), "/") - s1, s2, _ := strings.Cut(s, "-") - - start, err := strconv.ParseInt(s1, 10, 64) - if err != nil { - return 0, 0, fmt.Errorf("parse %q: %w", s, err) - } - stop, err := strconv.ParseInt(s2, 10, 64) - if err != nil { - return 0, 0, fmt.Errorf("parse %q: %w", s, err) - } - return start, stop, nil + return hex.EncodeToString(macBytes), nil } diff --git a/act/cacheproxy/model.go b/act/cacheproxy/model.go deleted file mode 100644 index de2fd9ea..00000000 --- a/act/cacheproxy/model.go +++ /dev/null @@ -1,44 +0,0 @@ -package cacheproxy - -import ( - "crypto/sha256" - "fmt" -) - -type Request struct { - Key string `json:"key" ` - Version string `json:"version"` - Size int64 `json:"cacheSize"` -} - -func (c *Request) ToCache() *Cache { - if c == nil { - return nil - } - ret := &Cache{ - Key: c.Key, - Version: c.Version, - Size: c.Size, - } - if c.Size == 0 { - // So the request comes from old versions of actions, like `actions/cache@v2`. - // It doesn't send cache size. Set it to -1 to indicate that. - ret.Size = -1 - } - return ret -} - -type Cache struct { - ID uint64 `json:"id" boltholdKey:"ID"` - Key string `json:"key" boltholdIndex:"Key"` - Version string `json:"version" boltholdIndex:"Version"` - KeyVersionHash string `json:"keyVersionHash" boltholdUnique:"KeyVersionHash"` - Size int64 `json:"cacheSize"` - Complete bool `json:"complete"` - UsedAt int64 `json:"usedAt" boltholdIndex:"UsedAt"` - CreatedAt int64 `json:"createdAt" boltholdIndex:"CreatedAt"` -} - -func (c *Cache) FillKeyVersionHash() { - c.KeyVersionHash = fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%s:%s", c.Key, c.Version)))) -} From 975364553b6bf595f378911ccfd0b8424b48c5f6 Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Sat, 9 Nov 2024 16:56:20 +0100 Subject: [PATCH 07/25] fix cache proxy to work properly --- act/cacheproxy/handler.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/act/cacheproxy/handler.go b/act/cacheproxy/handler.go index 40bab63a..3f8c0cc2 100644 --- a/act/cacheproxy/handler.go +++ b/act/cacheproxy/handler.go @@ -71,9 +71,12 @@ func StartHandler(repoName string, targetHost string, outboundIP string, port ui } router := httprouter.New() - router.HandlerFunc("GET", urlBase, proxyRequestHandler(proxy)) - router.HandlerFunc("POST", urlBase, proxyRequestHandler(proxy)) - router.HandlerFunc("PATCH", urlBase, proxyRequestHandler(proxy)) + router.HandlerFunc("GET", urlBase+"/cache", proxyRequestHandler(proxy)) + router.HandlerFunc("POST", urlBase+"/caches", proxyRequestHandler(proxy)) + router.HandlerFunc("PATCH", urlBase+"/caches/:id", proxyRequestHandler(proxy)) + router.HandlerFunc("POST", urlBase+"/caches/:id", proxyRequestHandler(proxy)) + router.HandlerFunc("GET", urlBase+"/artifacts/:id", proxyRequestHandler(proxy)) + router.HandlerFunc("POST", urlBase+"/clean", proxyRequestHandler(proxy)) h.router = router @@ -108,8 +111,13 @@ func (h *Handler) newReverseProxy(targetHost string) (*httputil.ReverseProxy, er return nil, err } - proxy := httputil.NewSingleHostReverseProxy(url) - proxy.Rewrite = func(r *httputil.ProxyRequest) { h.injectAuth(r) } + proxy := &httputil.ReverseProxy{ + Rewrite: func(r *httputil.ProxyRequest) { + r.SetURL(url) + r.Out.Host = r.In.Host // if desired + h.injectAuth(r) + }, + } return proxy, nil } From d92f9305dc7df5ff6621958a9504ccfd600516a3 Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Fri, 22 Nov 2024 23:36:05 +0100 Subject: [PATCH 08/25] wip: begin implementation of new design in proxy --- act/artifactcache/mac.go | 2 +- act/cacheproxy/handler.go | 65 +++++++++++++++++++++++++++------------ 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/act/artifactcache/mac.go b/act/artifactcache/mac.go index 968bc6f0..88ed7e04 100644 --- a/act/artifactcache/mac.go +++ b/act/artifactcache/mac.go @@ -16,7 +16,7 @@ import ( var ( ErrValidation = errors.New("validation error") - cachePrefixPath = "org:/repo:/run:/ts:/mac:/" + cachePrefixPath = "/:org/:repo/:run/:ts/:mac" ) func (h *Handler) validateMac(params httprouter.Params) (string, error) { diff --git a/act/cacheproxy/handler.go b/act/cacheproxy/handler.go index 3f8c0cc2..61a98b31 100644 --- a/act/cacheproxy/handler.go +++ b/act/cacheproxy/handler.go @@ -2,6 +2,7 @@ package cacheproxy import ( "crypto/hmac" + "crypto/rand" "crypto/sha256" "encoding/hex" "errors" @@ -33,11 +34,20 @@ type Handler struct { cacheServerHost string - repositoryName string - repositorySecret string + cacheSecret string + + workflows map[string]WorkflowData } -func StartHandler(repoName string, targetHost string, outboundIP string, port uint16, cacheSecret string, logger logrus.FieldLogger) (*Handler, error) { +type WorkflowData struct { + repositoryOwner string + repositoryName string + runNumber string + timestamp string + repositoryMAC string +} + +func StartHandler(targetHost string, outboundIP string, port uint16, cacheSecret string, logger logrus.FieldLogger) (*Handler, error) { h := &Handler{} if logger == nil { @@ -48,12 +58,7 @@ func StartHandler(repoName string, targetHost string, outboundIP string, port ui logger = logger.WithField("module", "artifactcache") h.logger = logger - h.repositoryName = repoName - repoSecret, err := calculateMAC(repoName, cacheSecret) - if err != nil { - return nil, fmt.Errorf("unable to decode cacheSecret") - } - h.repositorySecret = repoSecret + h.cacheSecret = cacheSecret if outboundIP != "" { h.outboundIP = outboundIP @@ -122,7 +127,7 @@ func (h *Handler) newReverseProxy(targetHost string) (*httputil.ReverseProxy, er } func (h *Handler) injectAuth(r *httputil.ProxyRequest) { - r.Out.SetBasicAuth(h.repositoryName, h.repositorySecret) + // TODO: re-implement this one } func (h *Handler) ExternalURL() string { @@ -132,6 +137,31 @@ func (h *Handler) ExternalURL() string { h.listener.Addr().(*net.TCPAddr).Port) } +// Informs the proxy of a workflow that can make cache requests. +// The WorkflowData contains the information about the repository. +// The function returns the 32-bit random key which the workflow will use to identify itself. +func (h *Handler) AddWorkflow(data WorkflowData) (string, error) { + keyBytes := make([]byte, 4) + _, err := rand.Read(keyBytes) + if err != nil { + return "", errors.New("Could not generate the workflow key") + } + key := hex.EncodeToString(keyBytes) + + h.workflows[key] = data + + return key, nil +} + +func (h *Handler) RemoveWorkflow(workflowKey string) error { + _, exists := h.workflows[workflowKey] + if !exists { + return errors.New("The workflow key was not known to the proxy") + } + delete(h.workflows, workflowKey) + return nil +} + func (h *Handler) Close() error { if h == nil { return nil @@ -157,13 +187,10 @@ func (h *Handler) Close() error { return retErr } -func calculateMAC(repoName string, cacheSecret string) (string, error) { - sec, err := hex.DecodeString(cacheSecret) - if err != nil { - return "", err - } - mac := hmac.New(sha256.New, sec) - mac.Write([]byte(repoName)) - macBytes := mac.Sum(nil) - return hex.EncodeToString(macBytes), nil +func computeMac(key, repo, run, ts string) string { + mac := hmac.New(sha256.New, []byte(key)) + mac.Write([]byte(repo)) + mac.Write([]byte(run)) + mac.Write([]byte(ts)) + return string(mac.Sum(nil)) } From f81731e2d93e7e5b3efbfe582371cbe8d1e4e746 Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Sun, 24 Nov 2024 16:17:14 +0100 Subject: [PATCH 09/25] add CreateWorkflowData --- act/cacheproxy/handler.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/act/cacheproxy/handler.go b/act/cacheproxy/handler.go index 61a98b31..841caec2 100644 --- a/act/cacheproxy/handler.go +++ b/act/cacheproxy/handler.go @@ -47,6 +47,18 @@ type WorkflowData struct { repositoryMAC string } +func (h *Handler) CreateWorkflowData(owner string, name string, runnumber string, timestamp string) WorkflowData { + repo := owner + "/" + name + mac := computeMac(h.cacheSecret, repo, runnumber, timestamp) + return WorkflowData{ + repositoryOwner: owner, + repositoryName: name, + runNumber: runnumber, + timestamp: timestamp, + repositoryMAC: mac, + } +} + func StartHandler(targetHost string, outboundIP string, port uint16, cacheSecret string, logger logrus.FieldLogger) (*Handler, error) { h := &Handler{} @@ -59,6 +71,7 @@ func StartHandler(targetHost string, outboundIP string, port uint16, cacheSecret h.logger = logger h.cacheSecret = cacheSecret + h.workflows = make(map[string]WorkflowData) if outboundIP != "" { h.outboundIP = outboundIP @@ -187,8 +200,8 @@ func (h *Handler) Close() error { return retErr } -func computeMac(key, repo, run, ts string) string { - mac := hmac.New(sha256.New, []byte(key)) +func computeMac(secret, repo, run, ts string) string { + mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(repo)) mac.Write([]byte(run)) mac.Write([]byte(ts)) From ce51735d7aeb53b93f3f617fbe4e4798eda01f7a Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Sun, 24 Nov 2024 18:38:27 +0100 Subject: [PATCH 10/25] update the reverse proxy to read workflow id and inject new auth info --- act/cacheproxy/handler.go | 52 ++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/act/cacheproxy/handler.go b/act/cacheproxy/handler.go index 841caec2..1776b6f2 100644 --- a/act/cacheproxy/handler.go +++ b/act/cacheproxy/handler.go @@ -12,6 +12,7 @@ import ( "net/http" "net/http/httputil" "net/url" + "regexp" "time" "github.com/julienschmidt/httprouter" @@ -40,22 +41,19 @@ type Handler struct { } type WorkflowData struct { - repositoryOwner string - repositoryName string - runNumber string - timestamp string - repositoryMAC string + repositoryFullName string + runNumber string + timestamp string + repositoryMAC string } -func (h *Handler) CreateWorkflowData(owner string, name string, runnumber string, timestamp string) WorkflowData { - repo := owner + "/" + name - mac := computeMac(h.cacheSecret, repo, runnumber, timestamp) +func (h *Handler) CreateWorkflowData(fullName string, runNumber string, timestamp string) WorkflowData { + mac := computeMac(h.cacheSecret, fullName, runNumber, timestamp) return WorkflowData{ - repositoryOwner: owner, - repositoryName: name, - runNumber: runnumber, - timestamp: timestamp, - repositoryMAC: mac, + repositoryFullName: fullName, + runNumber: runNumber, + timestamp: timestamp, + repositoryMAC: mac, } } @@ -89,12 +87,12 @@ func StartHandler(targetHost string, outboundIP string, port uint16, cacheSecret } router := httprouter.New() - router.HandlerFunc("GET", urlBase+"/cache", proxyRequestHandler(proxy)) - router.HandlerFunc("POST", urlBase+"/caches", proxyRequestHandler(proxy)) - router.HandlerFunc("PATCH", urlBase+"/caches/:id", proxyRequestHandler(proxy)) - router.HandlerFunc("POST", urlBase+"/caches/:id", proxyRequestHandler(proxy)) - router.HandlerFunc("GET", urlBase+"/artifacts/:id", proxyRequestHandler(proxy)) - router.HandlerFunc("POST", urlBase+"/clean", proxyRequestHandler(proxy)) + router.HandlerFunc("GET", "/:workflowId"+urlBase+"/cache", proxyRequestHandler(proxy)) + router.HandlerFunc("POST", "/:workflowId"+urlBase+"/caches", proxyRequestHandler(proxy)) + router.HandlerFunc("PATCH", "/:workflowId"+urlBase+"/caches/:id", proxyRequestHandler(proxy)) + router.HandlerFunc("POST", "/:workflowId"+urlBase+"/caches/:id", proxyRequestHandler(proxy)) + router.HandlerFunc("GET", "/:workflowId"+urlBase+"/artifacts/:id", proxyRequestHandler(proxy)) + router.HandlerFunc("POST", "/:workflowId"+urlBase+"/clean", proxyRequestHandler(proxy)) h.router = router @@ -133,16 +131,20 @@ func (h *Handler) newReverseProxy(targetHost string) (*httputil.ReverseProxy, er Rewrite: func(r *httputil.ProxyRequest) { r.SetURL(url) r.Out.Host = r.In.Host // if desired - h.injectAuth(r) + re := regexp.MustCompile(`/(\w+)/_apis/artifactcache`) + matches := re.FindStringSubmatch(r.In.URL.Path) + id := matches[1] + data := h.workflows[id] + + r.Out.Header.Add("Forgejo-Cache-Repo", data.repositoryFullName) + r.Out.Header.Add("Forgejo-Cache-RunNumber", data.runNumber) + r.Out.Header.Add("Forgejo-Cache-Timestamp", data.timestamp) + r.Out.Header.Add("Forgejo-Cache-MAC", data.repositoryMAC) }, } return proxy, nil } -func (h *Handler) injectAuth(r *httputil.ProxyRequest) { - // TODO: re-implement this one -} - func (h *Handler) ExternalURL() string { // TODO: make the external url configurable if necessary return fmt.Sprintf("http://%s:%d", @@ -205,5 +207,5 @@ func computeMac(secret, repo, run, ts string) string { mac.Write([]byte(repo)) mac.Write([]byte(run)) mac.Write([]byte(ts)) - return string(mac.Sum(nil)) + return hex.EncodeToString(mac.Sum(nil)) } From d736bb3435397940beef95f3618d457c88a2c597 Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Sun, 24 Nov 2024 18:46:24 +0100 Subject: [PATCH 11/25] rename workflowid to runid --- act/cacheproxy/handler.go | 44 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/act/cacheproxy/handler.go b/act/cacheproxy/handler.go index 1776b6f2..49ef88ba 100644 --- a/act/cacheproxy/handler.go +++ b/act/cacheproxy/handler.go @@ -37,19 +37,19 @@ type Handler struct { cacheSecret string - workflows map[string]WorkflowData + runs map[string]RunData } -type WorkflowData struct { +type RunData struct { repositoryFullName string runNumber string timestamp string repositoryMAC string } -func (h *Handler) CreateWorkflowData(fullName string, runNumber string, timestamp string) WorkflowData { +func (h *Handler) CreateRunData(fullName string, runNumber string, timestamp string) RunData { mac := computeMac(h.cacheSecret, fullName, runNumber, timestamp) - return WorkflowData{ + return RunData{ repositoryFullName: fullName, runNumber: runNumber, timestamp: timestamp, @@ -69,7 +69,7 @@ func StartHandler(targetHost string, outboundIP string, port uint16, cacheSecret h.logger = logger h.cacheSecret = cacheSecret - h.workflows = make(map[string]WorkflowData) + h.runs = make(map[string]RunData) if outboundIP != "" { h.outboundIP = outboundIP @@ -87,12 +87,12 @@ func StartHandler(targetHost string, outboundIP string, port uint16, cacheSecret } router := httprouter.New() - router.HandlerFunc("GET", "/:workflowId"+urlBase+"/cache", proxyRequestHandler(proxy)) - router.HandlerFunc("POST", "/:workflowId"+urlBase+"/caches", proxyRequestHandler(proxy)) - router.HandlerFunc("PATCH", "/:workflowId"+urlBase+"/caches/:id", proxyRequestHandler(proxy)) - router.HandlerFunc("POST", "/:workflowId"+urlBase+"/caches/:id", proxyRequestHandler(proxy)) - router.HandlerFunc("GET", "/:workflowId"+urlBase+"/artifacts/:id", proxyRequestHandler(proxy)) - router.HandlerFunc("POST", "/:workflowId"+urlBase+"/clean", proxyRequestHandler(proxy)) + router.HandlerFunc("GET", "/:runId"+urlBase+"/cache", proxyRequestHandler(proxy)) + router.HandlerFunc("POST", "/:runId"+urlBase+"/caches", proxyRequestHandler(proxy)) + router.HandlerFunc("PATCH", "/:runId"+urlBase+"/caches/:id", proxyRequestHandler(proxy)) + router.HandlerFunc("POST", "/:runId"+urlBase+"/caches/:id", proxyRequestHandler(proxy)) + router.HandlerFunc("GET", "/:runId"+urlBase+"/artifacts/:id", proxyRequestHandler(proxy)) + router.HandlerFunc("POST", "/:runId"+urlBase+"/clean", proxyRequestHandler(proxy)) h.router = router @@ -134,7 +134,7 @@ func (h *Handler) newReverseProxy(targetHost string) (*httputil.ReverseProxy, er re := regexp.MustCompile(`/(\w+)/_apis/artifactcache`) matches := re.FindStringSubmatch(r.In.URL.Path) id := matches[1] - data := h.workflows[id] + data := h.runs[id] r.Out.Header.Add("Forgejo-Cache-Repo", data.repositoryFullName) r.Out.Header.Add("Forgejo-Cache-RunNumber", data.runNumber) @@ -152,28 +152,28 @@ func (h *Handler) ExternalURL() string { h.listener.Addr().(*net.TCPAddr).Port) } -// Informs the proxy of a workflow that can make cache requests. -// The WorkflowData contains the information about the repository. -// The function returns the 32-bit random key which the workflow will use to identify itself. -func (h *Handler) AddWorkflow(data WorkflowData) (string, error) { +// Informs the proxy of a workflow run that can make cache requests. +// The RunData contains the information about the repository. +// The function returns the 32-bit random key which the run will use to identify itself. +func (h *Handler) AddRun(data RunData) (string, error) { keyBytes := make([]byte, 4) _, err := rand.Read(keyBytes) if err != nil { - return "", errors.New("Could not generate the workflow key") + return "", errors.New("Could not generate the run id") } key := hex.EncodeToString(keyBytes) - h.workflows[key] = data + h.runs[key] = data return key, nil } -func (h *Handler) RemoveWorkflow(workflowKey string) error { - _, exists := h.workflows[workflowKey] +func (h *Handler) RemoveRun(runID string) error { + _, exists := h.runs[runID] if !exists { - return errors.New("The workflow key was not known to the proxy") + return errors.New("The run id was not known to the proxy") } - delete(h.workflows, workflowKey) + delete(h.runs, runID) return nil } From 41032137ea9919d8c51cd7a13432e36fcd081abb Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Sun, 24 Nov 2024 21:56:08 +0100 Subject: [PATCH 12/25] add copyright notice --- act/cacheproxy/handler.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/act/cacheproxy/handler.go b/act/cacheproxy/handler.go index 49ef88ba..da0b838c 100644 --- a/act/cacheproxy/handler.go +++ b/act/cacheproxy/handler.go @@ -1,3 +1,6 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package cacheproxy import ( From a6a1df7556ffc9e3d7e9f513c4e35d0bdfbd0cb8 Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Sat, 7 Dec 2024 15:22:27 +0100 Subject: [PATCH 13/25] use safe sync.Map --- act/cacheproxy/handler.go | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/act/cacheproxy/handler.go b/act/cacheproxy/handler.go index da0b838c..e8064cae 100644 --- a/act/cacheproxy/handler.go +++ b/act/cacheproxy/handler.go @@ -16,6 +16,7 @@ import ( "net/http/httputil" "net/url" "regexp" + "sync" "time" "github.com/julienschmidt/httprouter" @@ -40,7 +41,7 @@ type Handler struct { cacheSecret string - runs map[string]RunData + runs sync.Map } type RunData struct { @@ -72,7 +73,7 @@ func StartHandler(targetHost string, outboundIP string, port uint16, cacheSecret h.logger = logger h.cacheSecret = cacheSecret - h.runs = make(map[string]RunData) + // h.runs = make(map[string]RunData) if outboundIP != "" { h.outboundIP = outboundIP @@ -137,12 +138,19 @@ func (h *Handler) newReverseProxy(targetHost string) (*httputil.ReverseProxy, er re := regexp.MustCompile(`/(\w+)/_apis/artifactcache`) matches := re.FindStringSubmatch(r.In.URL.Path) id := matches[1] - data := h.runs[id] + data, ok := h.runs.Load(id) + var runData = data.(RunData) + if !ok { + // The ID doesn't exist. + // ! this should probably be handled more gracefully but i can't figure out how + // ! it really shouldn't happen anyway so it's fine for now + return + } - r.Out.Header.Add("Forgejo-Cache-Repo", data.repositoryFullName) - r.Out.Header.Add("Forgejo-Cache-RunNumber", data.runNumber) - r.Out.Header.Add("Forgejo-Cache-Timestamp", data.timestamp) - r.Out.Header.Add("Forgejo-Cache-MAC", data.repositoryMAC) + r.Out.Header.Add("Forgejo-Cache-Repo", runData.repositoryFullName) + r.Out.Header.Add("Forgejo-Cache-RunNumber", runData.runNumber) + r.Out.Header.Add("Forgejo-Cache-Timestamp", runData.timestamp) + r.Out.Header.Add("Forgejo-Cache-MAC", runData.repositoryMAC) }, } return proxy, nil @@ -166,17 +174,16 @@ func (h *Handler) AddRun(data RunData) (string, error) { } key := hex.EncodeToString(keyBytes) - h.runs[key] = data + h.runs.Store(key, data) return key, nil } func (h *Handler) RemoveRun(runID string) error { - _, exists := h.runs[runID] - if !exists { + _, existed := h.runs.LoadAndDelete(runID) + if !existed { return errors.New("The run id was not known to the proxy") } - delete(h.runs, runID) return nil } From 95e754c06b9ea9f02efcde6ef029569b775da586 Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Sat, 7 Dec 2024 17:48:07 +0100 Subject: [PATCH 14/25] integrate the new cache proxy with the server viceice set up --- act/artifactcache/handler.go | 40 +++++++++++++++++++++++++----------- act/artifactcache/mac.go | 36 +++++++++++--------------------- act/cacheproxy/handler.go | 34 +++++++++++++++--------------- 3 files changed, 58 insertions(+), 52 deletions(-) diff --git a/act/artifactcache/handler.go b/act/artifactcache/handler.go index 65d70e14..c02c450c 100644 --- a/act/artifactcache/handler.go +++ b/act/artifactcache/handler.go @@ -20,6 +20,7 @@ import ( "github.com/timshannon/bolthold" "go.etcd.io/bbolt" + "github.com/nektos/act/pkg/cacheproxy" "github.com/nektos/act/pkg/common" ) @@ -83,12 +84,12 @@ func StartHandler(dir, outboundIP string, port uint16, secret string, logger log } router := httprouter.New() - router.GET(cachePrefixPath+urlBase+"/cache", h.middleware(h.find)) - router.POST(cachePrefixPath+urlBase+"/caches", h.middleware(h.reserve)) - router.PATCH(cachePrefixPath+urlBase+"/caches/:id", h.middleware(h.upload)) - router.POST(cachePrefixPath+urlBase+"/caches/:id", h.middleware(h.commit)) - router.GET(cachePrefixPath+urlBase+"/artifacts/:id", h.middleware(h.get)) - router.POST(cachePrefixPath+urlBase+"/clean", h.middleware(h.clean)) + router.GET(urlBase+"/cache", h.middleware(h.find)) + router.POST(urlBase+"/caches", h.middleware(h.reserve)) + router.PATCH(urlBase+"/caches/:id", h.middleware(h.upload)) + router.POST(urlBase+"/caches/:id", h.middleware(h.commit)) + router.GET(urlBase+"/artifacts/:id", h.middleware(h.get)) + router.POST(urlBase+"/clean", h.middleware(h.clean)) h.router = router @@ -159,7 +160,8 @@ func (h *Handler) openDB() (*bolthold.Store, error) { // GET /_apis/artifactcache/cache func (h *Handler) find(w http.ResponseWriter, r *http.Request, params httprouter.Params) { - repo, err := h.validateMac(params) + rundata := runDataFromHeaders(r) + repo, err := h.validateMac(rundata) if err != nil { h.responseJSON(w, r, 500, err) return @@ -206,7 +208,8 @@ func (h *Handler) find(w http.ResponseWriter, r *http.Request, params httprouter // POST /_apis/artifactcache/caches func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, params httprouter.Params) { - repo, err := h.validateMac(params) + rundata := runDataFromHeaders(r) + repo, err := h.validateMac(rundata) if err != nil { h.responseJSON(w, r, 500, err) return @@ -243,7 +246,8 @@ func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, params httprou // PATCH /_apis/artifactcache/caches/:id func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprouter.Params) { - repo, err := h.validateMac(params) + rundata := runDataFromHeaders(r) + repo, err := h.validateMac(rundata) if err != nil { h.responseJSON(w, r, 500, err) return @@ -296,7 +300,8 @@ func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprout // POST /_apis/artifactcache/caches/:id func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprouter.Params) { - repo, err := h.validateMac(params) + rundata := runDataFromHeaders(r) + repo, err := h.validateMac(rundata) if err != nil { h.responseJSON(w, r, 500, err) return @@ -363,7 +368,8 @@ func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprout // GET /_apis/artifactcache/artifacts/:id func (h *Handler) get(w http.ResponseWriter, r *http.Request, params httprouter.Params) { - repo, err := h.validateMac(params) + rundata := runDataFromHeaders(r) + repo, err := h.validateMac(rundata) if err != nil { h.responseJSON(w, r, 500, err) return @@ -403,7 +409,8 @@ func (h *Handler) get(w http.ResponseWriter, r *http.Request, params httprouter. // POST /_apis/artifactcache/clean func (h *Handler) clean(w http.ResponseWriter, r *http.Request, params httprouter.Params) { - _, err := h.validateMac(params) + rundata := runDataFromHeaders(r) + _, err := h.validateMac(rundata) if err != nil { h.responseJSON(w, r, 500, err) return @@ -621,3 +628,12 @@ func parseContentRange(s string) (uint64, uint64, error) { } return start, stop, nil } + +func runDataFromHeaders(r *http.Request) cacheproxy.RunData { + return cacheproxy.RunData{ + RepositoryFullName: r.Header.Get("Forgejo-Cache-Repo"), + RunNumber: r.Header.Get("Forgejo-Cache-RunNumber"), + Timestamp: r.Header.Get("Forgejo-Cache-Timestamp"), + RepositoryMAC: r.Header.Get("Forgejo-Cache-MAC"), + } +} diff --git a/act/artifactcache/mac.go b/act/artifactcache/mac.go index 88ed7e04..0645fa81 100644 --- a/act/artifactcache/mac.go +++ b/act/artifactcache/mac.go @@ -6,36 +6,29 @@ package artifactcache import ( "crypto/hmac" "crypto/sha256" + "encoding/hex" "errors" - "hash" "strconv" "time" - "github.com/julienschmidt/httprouter" + "github.com/nektos/act/pkg/cacheproxy" ) var ( - ErrValidation = errors.New("validation error") - cachePrefixPath = "/:org/:repo/:run/:ts/:mac" + ErrValidation = errors.New("validation error") ) -func (h *Handler) validateMac(params httprouter.Params) (string, error) { - ts := params.ByName("ts") - - repo := params.ByName("org") + "/" + params.ByName("repo") - run := params.ByName("run") - messageMAC := params.ByName("mac") - +func (h *Handler) validateMac(rundata cacheproxy.RunData) (string, error) { // TODO: allow configurable max age - if !validateAge(ts) { + if !validateAge(rundata.Timestamp) { return "", ErrValidation } - expectedMAC := computeMac(h.secret, repo, run, ts).Sum(nil) - if hmac.Equal([]byte(messageMAC), expectedMAC) { - return repo, nil + expectedMAC := computeMac(h.secret, rundata.RepositoryFullName, rundata.RunNumber, rundata.Timestamp) + if expectedMAC == rundata.RepositoryMAC { + return rundata.RepositoryFullName, nil } - return repo, ErrValidation + return rundata.RepositoryFullName, ErrValidation } func validateAge(ts string) bool { @@ -49,15 +42,10 @@ func validateAge(ts string) bool { return true } -func computeMac(key, repo, run, ts string) hash.Hash { - mac := hmac.New(sha256.New, []byte(key)) +func computeMac(secret, repo, run, ts string) string { + mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(repo)) mac.Write([]byte(run)) mac.Write([]byte(ts)) - return mac -} - -func ComputeMac(key, repo, run, ts string) string { - mac := computeMac(key, repo, run, ts) - return string(mac.Sum(nil)) + return hex.EncodeToString(mac.Sum(nil)) } diff --git a/act/cacheproxy/handler.go b/act/cacheproxy/handler.go index e8064cae..efb67f4e 100644 --- a/act/cacheproxy/handler.go +++ b/act/cacheproxy/handler.go @@ -45,19 +45,19 @@ type Handler struct { } type RunData struct { - repositoryFullName string - runNumber string - timestamp string - repositoryMAC string + RepositoryFullName string + RunNumber string + Timestamp string + RepositoryMAC string } func (h *Handler) CreateRunData(fullName string, runNumber string, timestamp string) RunData { mac := computeMac(h.cacheSecret, fullName, runNumber, timestamp) return RunData{ - repositoryFullName: fullName, - runNumber: runNumber, - timestamp: timestamp, - repositoryMAC: mac, + RepositoryFullName: fullName, + RunNumber: runNumber, + Timestamp: timestamp, + RepositoryMAC: mac, } } @@ -126,16 +126,14 @@ func proxyRequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, } func (h *Handler) newReverseProxy(targetHost string) (*httputil.ReverseProxy, error) { - url, err := url.Parse(targetHost) + targetURL, err := url.Parse(targetHost) if err != nil { return nil, err } proxy := &httputil.ReverseProxy{ Rewrite: func(r *httputil.ProxyRequest) { - r.SetURL(url) - r.Out.Host = r.In.Host // if desired - re := regexp.MustCompile(`/(\w+)/_apis/artifactcache`) + re := regexp.MustCompile(`/(\w+)(/_apis/artifactcache/.+)`) matches := re.FindStringSubmatch(r.In.URL.Path) id := matches[1] data, ok := h.runs.Load(id) @@ -146,11 +144,15 @@ func (h *Handler) newReverseProxy(targetHost string) (*httputil.ReverseProxy, er // ! it really shouldn't happen anyway so it's fine for now return } + uri := matches[2] - r.Out.Header.Add("Forgejo-Cache-Repo", runData.repositoryFullName) - r.Out.Header.Add("Forgejo-Cache-RunNumber", runData.runNumber) - r.Out.Header.Add("Forgejo-Cache-Timestamp", runData.timestamp) - r.Out.Header.Add("Forgejo-Cache-MAC", runData.repositoryMAC) + r.SetURL(targetURL) + r.Out.URL.Path = uri + + r.Out.Header.Add("Forgejo-Cache-Repo", runData.RepositoryFullName) + r.Out.Header.Add("Forgejo-Cache-RunNumber", runData.RunNumber) + r.Out.Header.Add("Forgejo-Cache-Timestamp", runData.Timestamp) + r.Out.Header.Add("Forgejo-Cache-MAC", runData.RepositoryMAC) }, } return proxy, nil From e3adb49c50098dcf049d00cc1528e8ac16d57c0a Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Mon, 13 Jan 2025 16:50:45 +0100 Subject: [PATCH 15/25] functional save and restore through proxy --- act/artifactcache/handler.go | 3 ++- act/cacheproxy/handler.go | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/act/artifactcache/handler.go b/act/artifactcache/handler.go index c02c450c..013c642b 100644 --- a/act/artifactcache/handler.go +++ b/act/artifactcache/handler.go @@ -199,9 +199,10 @@ func (h *Handler) find(w http.ResponseWriter, r *http.Request, params httprouter h.responseJSON(w, r, 204) return } + archiveLocation := fmt.Sprintf("%s/%s%s/artifacts/%d", r.Header.Get("Forgejo-Cache-Host"), r.Header.Get("Forgejo-Cache-RunId"), urlBase, cache.ID) h.responseJSON(w, r, 200, map[string]any{ "result": "hit", - "archiveLocation": fmt.Sprintf("%s%s/artifacts/%d", h.ExternalURL(), urlBase, cache.ID), + "archiveLocation": archiveLocation, "cacheKey": cache.Key, }) } diff --git a/act/cacheproxy/handler.go b/act/cacheproxy/handler.go index efb67f4e..42d58d6b 100644 --- a/act/cacheproxy/handler.go +++ b/act/cacheproxy/handler.go @@ -151,8 +151,10 @@ func (h *Handler) newReverseProxy(targetHost string) (*httputil.ReverseProxy, er r.Out.Header.Add("Forgejo-Cache-Repo", runData.RepositoryFullName) r.Out.Header.Add("Forgejo-Cache-RunNumber", runData.RunNumber) + r.Out.Header.Add("Forgejo-Cache-RunId", id) r.Out.Header.Add("Forgejo-Cache-Timestamp", runData.Timestamp) r.Out.Header.Add("Forgejo-Cache-MAC", runData.RepositoryMAC) + r.Out.Header.Add("Forgejo-Cache-Host", h.ExternalURL()) }, } return proxy, nil From 11062e4d6a061b9dc0b025a01aa5f5c108b93831 Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Mon, 13 Jan 2025 16:59:07 +0100 Subject: [PATCH 16/25] return 403 instead of 500 when not authorized correctly --- act/artifactcache/handler.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/act/artifactcache/handler.go b/act/artifactcache/handler.go index 013c642b..8c603a8e 100644 --- a/act/artifactcache/handler.go +++ b/act/artifactcache/handler.go @@ -163,7 +163,7 @@ func (h *Handler) find(w http.ResponseWriter, r *http.Request, params httprouter rundata := runDataFromHeaders(r) repo, err := h.validateMac(rundata) if err != nil { - h.responseJSON(w, r, 500, err) + h.responseJSON(w, r, 403, err) return } @@ -212,7 +212,7 @@ func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, params httprou rundata := runDataFromHeaders(r) repo, err := h.validateMac(rundata) if err != nil { - h.responseJSON(w, r, 500, err) + h.responseJSON(w, r, 403, err) return } @@ -250,7 +250,7 @@ func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprout rundata := runDataFromHeaders(r) repo, err := h.validateMac(rundata) if err != nil { - h.responseJSON(w, r, 500, err) + h.responseJSON(w, r, 403, err) return } @@ -304,7 +304,7 @@ func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprout rundata := runDataFromHeaders(r) repo, err := h.validateMac(rundata) if err != nil { - h.responseJSON(w, r, 500, err) + h.responseJSON(w, r, 403, err) return } @@ -372,7 +372,7 @@ func (h *Handler) get(w http.ResponseWriter, r *http.Request, params httprouter. rundata := runDataFromHeaders(r) repo, err := h.validateMac(rundata) if err != nil { - h.responseJSON(w, r, 500, err) + h.responseJSON(w, r, 403, err) return } @@ -413,7 +413,7 @@ func (h *Handler) clean(w http.ResponseWriter, r *http.Request, params httproute rundata := runDataFromHeaders(r) _, err := h.validateMac(rundata) if err != nil { - h.responseJSON(w, r, 500, err) + h.responseJSON(w, r, 403, err) return } // TODO: don't support force deleting cache entries From 7a21d643333f05638c4ed03c4e73f7247707d8dd Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Tue, 14 Jan 2025 13:40:23 +0100 Subject: [PATCH 17/25] review: discard params in clean --- act/artifactcache/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/act/artifactcache/handler.go b/act/artifactcache/handler.go index 8c603a8e..0f8afd93 100644 --- a/act/artifactcache/handler.go +++ b/act/artifactcache/handler.go @@ -409,7 +409,7 @@ func (h *Handler) get(w http.ResponseWriter, r *http.Request, params httprouter. } // POST /_apis/artifactcache/clean -func (h *Handler) clean(w http.ResponseWriter, r *http.Request, params httprouter.Params) { +func (h *Handler) clean(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { rundata := runDataFromHeaders(r) _, err := h.validateMac(rundata) if err != nil { From ef43d7c615c9e0c7c3f5920da00e56fdf81b9a90 Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Sun, 26 Jan 2025 11:50:03 +0100 Subject: [PATCH 18/25] review: fix various issues brought up by Gusted --- act/artifactcache/mac.go | 2 +- act/cacheproxy/handler.go | 31 +++++++++++++++++++------------ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/act/artifactcache/mac.go b/act/artifactcache/mac.go index 0645fa81..d41a76cf 100644 --- a/act/artifactcache/mac.go +++ b/act/artifactcache/mac.go @@ -25,7 +25,7 @@ func (h *Handler) validateMac(rundata cacheproxy.RunData) (string, error) { } expectedMAC := computeMac(h.secret, rundata.RepositoryFullName, rundata.RunNumber, rundata.Timestamp) - if expectedMAC == rundata.RepositoryMAC { + if hmac.Equal([]byte(expectedMAC), []byte(rundata.RepositoryMAC)) { return rundata.RepositoryFullName, nil } return rundata.RepositoryFullName, ErrValidation diff --git a/act/cacheproxy/handler.go b/act/cacheproxy/handler.go index 42d58d6b..b79fa47f 100644 --- a/act/cacheproxy/handler.go +++ b/act/cacheproxy/handler.go @@ -16,6 +16,7 @@ import ( "net/http/httputil" "net/url" "regexp" + "strconv" "sync" "time" @@ -29,6 +30,10 @@ const ( urlBase = "/_apis/artifactcache" ) +var ( + urlRegex = regexp.MustCompile(`/(\w+)(/_apis/artifactcache/.+)`) +) + type Handler struct { router *httprouter.Router listener net.Listener @@ -133,8 +138,7 @@ func (h *Handler) newReverseProxy(targetHost string) (*httputil.ReverseProxy, er proxy := &httputil.ReverseProxy{ Rewrite: func(r *httputil.ProxyRequest) { - re := regexp.MustCompile(`/(\w+)(/_apis/artifactcache/.+)`) - matches := re.FindStringSubmatch(r.In.URL.Path) + matches := urlRegex.FindStringSubmatch(r.In.URL.Path) id := matches[1] data, ok := h.runs.Load(id) var runData = data.(RunData) @@ -149,12 +153,12 @@ func (h *Handler) newReverseProxy(targetHost string) (*httputil.ReverseProxy, er r.SetURL(targetURL) r.Out.URL.Path = uri - r.Out.Header.Add("Forgejo-Cache-Repo", runData.RepositoryFullName) - r.Out.Header.Add("Forgejo-Cache-RunNumber", runData.RunNumber) - r.Out.Header.Add("Forgejo-Cache-RunId", id) - r.Out.Header.Add("Forgejo-Cache-Timestamp", runData.Timestamp) - r.Out.Header.Add("Forgejo-Cache-MAC", runData.RepositoryMAC) - r.Out.Header.Add("Forgejo-Cache-Host", h.ExternalURL()) + r.Out.Header.Set("Forgejo-Cache-Repo", runData.RepositoryFullName) + r.Out.Header.Set("Forgejo-Cache-RunNumber", runData.RunNumber) + r.Out.Header.Set("Forgejo-Cache-RunId", id) + r.Out.Header.Set("Forgejo-Cache-Timestamp", runData.Timestamp) + r.Out.Header.Set("Forgejo-Cache-MAC", runData.RepositoryMAC) + r.Out.Header.Set("Forgejo-Cache-Host", h.ExternalURL()) }, } return proxy, nil @@ -162,9 +166,7 @@ func (h *Handler) newReverseProxy(targetHost string) (*httputil.ReverseProxy, er func (h *Handler) ExternalURL() string { // TODO: make the external url configurable if necessary - return fmt.Sprintf("http://%s:%d", - h.outboundIP, - h.listener.Addr().(*net.TCPAddr).Port) + return net.JoinHostPort(h.outboundIP, strconv.Itoa(h.listener.Addr().(*net.TCPAddr).Port)) } // Informs the proxy of a workflow run that can make cache requests. @@ -178,7 +180,10 @@ func (h *Handler) AddRun(data RunData) (string, error) { } key := hex.EncodeToString(keyBytes) - h.runs.Store(key, data) + _, loaded := h.runs.LoadOrStore(key, data) + if loaded { + return "", errors.New("Run id already exists") + } return key, nil } @@ -219,7 +224,9 @@ func (h *Handler) Close() error { func computeMac(secret, repo, run, ts string) string { mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(repo)) + mac.Write([]byte(">")) mac.Write([]byte(run)) + mac.Write([]byte(">")) mac.Write([]byte(ts)) return hex.EncodeToString(mac.Sum(nil)) } From aa0c46539c2386336fc1550b4c6981b647426dc0 Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Sun, 26 Jan 2025 11:56:04 +0100 Subject: [PATCH 19/25] review: add retries to generating runid in case of collision --- act/cacheproxy/handler.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/act/cacheproxy/handler.go b/act/cacheproxy/handler.go index b79fa47f..293b2bc7 100644 --- a/act/cacheproxy/handler.go +++ b/act/cacheproxy/handler.go @@ -173,19 +173,21 @@ func (h *Handler) ExternalURL() string { // The RunData contains the information about the repository. // The function returns the 32-bit random key which the run will use to identify itself. func (h *Handler) AddRun(data RunData) (string, error) { - keyBytes := make([]byte, 4) - _, err := rand.Read(keyBytes) - if err != nil { - return "", errors.New("Could not generate the run id") - } - key := hex.EncodeToString(keyBytes) + for retries := 0; retries < 3; retries++ { + keyBytes := make([]byte, 4) + _, err := rand.Read(keyBytes) + if err != nil { + return "", errors.New("Could not generate the run id") + } + key := hex.EncodeToString(keyBytes) - _, loaded := h.runs.LoadOrStore(key, data) - if loaded { - return "", errors.New("Run id already exists") + _, loaded := h.runs.LoadOrStore(key, data) + if !loaded { + // The key was unique and added successfully + return key, nil + } } - - return key, nil + return "", errors.New("Repeated collisions in generating run id") } func (h *Handler) RemoveRun(runID string) error { From 57a2a56e328048d46cb363a51414459941122650 Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Sun, 26 Jan 2025 12:44:34 +0100 Subject: [PATCH 20/25] add unit tests for mac calculation --- act/artifactcache/mac_test.go | 80 +++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 act/artifactcache/mac_test.go diff --git a/act/artifactcache/mac_test.go b/act/artifactcache/mac_test.go new file mode 100644 index 00000000..10d6c5a8 --- /dev/null +++ b/act/artifactcache/mac_test.go @@ -0,0 +1,80 @@ +package artifactcache + +import ( + "strconv" + "testing" + "time" + + "github.com/nektos/act/pkg/cacheproxy" + "github.com/stretchr/testify/require" +) + +func TestMac(t *testing.T) { + handler := &Handler{ + secret: "secret for testing", + } + + t.Run("validate correct mac", func(t *testing.T) { + name := "org/reponame" + run := "1" + ts := strconv.FormatInt(time.Now().Unix(), 10) + + mac := computeMac(handler.secret, name, run, ts) + rundata := cacheproxy.RunData{ + RepositoryFullName: name, + RunNumber: run, + Timestamp: ts, + RepositoryMAC: mac, + } + + repoName, err := handler.validateMac(rundata) + require.NoError(t, err) + require.Equal(t, name, repoName) + }) + + t.Run("validate incorrect timestamp", func(t *testing.T) { + name := "org/reponame" + run := "1" + ts := "9223372036854775807" // This should last us for a while... + + mac := computeMac(handler.secret, name, run, ts) + rundata := cacheproxy.RunData{ + RepositoryFullName: name, + RunNumber: run, + Timestamp: ts, + RepositoryMAC: mac, + } + + _, err := handler.validateMac(rundata) + require.Error(t, err) + }) + + t.Run("validate incorrect mac", func(t *testing.T) { + name := "org/reponame" + run := "1" + ts := strconv.FormatInt(time.Now().Unix(), 10) + + rundata := cacheproxy.RunData{ + RepositoryFullName: name, + RunNumber: run, + Timestamp: ts, + RepositoryMAC: "this is not the right mac :D", + } + + repoName, err := handler.validateMac(rundata) + require.Error(t, err) + require.Equal(t, name, repoName) + }) + + t.Run("compute correct mac", func(t *testing.T) { + secret := "this is my cool secret string :3" + name := "org/reponame" + run := "42" + ts := "1337" + + mac := computeMac(secret, name, run, ts) + expectedMac := "09b0e9111660359d319c2d55c0664b5a6c5915c2f705b08af61aa63e7542f511" // * Precomputed, anytime the computeMac function changes this needs to be recalculated + + require.Equal(t, mac, expectedMac) + }) +} From 43f1298653e77d480090c4dcbf54b9f6c3b1b5f7 Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Sun, 26 Jan 2025 15:56:55 +0100 Subject: [PATCH 21/25] fix: external url and mac function matching --- act/artifactcache/mac.go | 2 ++ act/cacheproxy/handler.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/act/artifactcache/mac.go b/act/artifactcache/mac.go index d41a76cf..1b129b77 100644 --- a/act/artifactcache/mac.go +++ b/act/artifactcache/mac.go @@ -45,7 +45,9 @@ func validateAge(ts string) bool { func computeMac(secret, repo, run, ts string) string { mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(repo)) + mac.Write([]byte(">")) mac.Write([]byte(run)) + mac.Write([]byte(">")) mac.Write([]byte(ts)) return hex.EncodeToString(mac.Sum(nil)) } diff --git a/act/cacheproxy/handler.go b/act/cacheproxy/handler.go index 293b2bc7..13997ef2 100644 --- a/act/cacheproxy/handler.go +++ b/act/cacheproxy/handler.go @@ -166,7 +166,7 @@ func (h *Handler) newReverseProxy(targetHost string) (*httputil.ReverseProxy, er func (h *Handler) ExternalURL() string { // TODO: make the external url configurable if necessary - return net.JoinHostPort(h.outboundIP, strconv.Itoa(h.listener.Addr().(*net.TCPAddr).Port)) + return fmt.Sprintf("http://%s", net.JoinHostPort(h.outboundIP, strconv.Itoa(h.listener.Addr().(*net.TCPAddr).Port))) } // Informs the proxy of a workflow run that can make cache requests. From e54faaf56d3a2ff4cf2040377b7e16e396b19e85 Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Thu, 20 Mar 2025 11:20:13 +0100 Subject: [PATCH 22/25] fix tests for cache proxy --- act/artifactcache/handler_test.go | 98 ++++++++++++++++++------------- act/artifactcache/mac_test.go | 2 +- 2 files changed, 59 insertions(+), 41 deletions(-) diff --git a/act/artifactcache/handler_test.go b/act/artifactcache/handler_test.go index 7391c73c..6059cd2d 100644 --- a/act/artifactcache/handler_test.go +++ b/act/artifactcache/handler_test.go @@ -18,13 +18,31 @@ import ( "go.etcd.io/bbolt" ) +const cache_repo = "testuser/repo" +const cache_runnum = "1" +const cache_timestamp = "0" +const cache_mac = "c13854dd1ac599d1d61680cd93c26b77ba0ee10f374a3408bcaea82f38ca1865" + +type AuthHeaderTransport struct { + T http.RoundTripper +} + +func (t *AuthHeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req.Header.Set("Forgejo-Cache-Repo", cache_repo) + req.Header.Set("Forgejo-Cache-RunNumber", cache_runnum) + req.Header.Set("Forgejo-Cache-Timestamp", cache_timestamp) + req.Header.Set("Forgejo-Cache-MAC", cache_mac) + return t.T.RoundTrip(req) +} + +var httpClientTransport = AuthHeaderTransport{http.DefaultTransport} +var httpClient = http.Client{Transport: &httpClientTransport} + func TestHandler(t *testing.T) { dir := filepath.Join(t.TempDir(), "artifactcache") handler, err := StartHandler(dir, "", 0, "secret", nil) require.NoError(t, err) - t.Skip("TODO: handle secret") - base := fmt.Sprintf("%s%s", handler.ExternalURL(), urlBase) defer func() { @@ -43,7 +61,7 @@ func TestHandler(t *testing.T) { require.NoError(t, handler.Close()) assert.Nil(t, handler.server) assert.Nil(t, handler.listener) - _, err := http.Post(fmt.Sprintf("%s/caches/%d", base, 1), "", nil) + _, err := httpClient.Post(fmt.Sprintf("%s/caches/%d", base, 1), "", nil) assert.Error(t, err) }) }() @@ -51,7 +69,7 @@ func TestHandler(t *testing.T) { t.Run("get not exist", func(t *testing.T) { key := strings.ToLower(t.Name()) version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20" - resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version)) + resp, err := httpClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version)) require.NoError(t, err) require.Equal(t, 204, resp.StatusCode) }) @@ -66,7 +84,7 @@ func TestHandler(t *testing.T) { }) t.Run("clean", func(t *testing.T) { - resp, err := http.Post(fmt.Sprintf("%s/clean", base), "", nil) + resp, err := httpClient.Post(fmt.Sprintf("%s/clean", base), "", nil) require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) }) @@ -74,7 +92,7 @@ func TestHandler(t *testing.T) { t.Run("reserve with bad request", func(t *testing.T) { body := []byte(`invalid json`) require.NoError(t, err) - resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body)) + resp, err := httpClient.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body)) require.NoError(t, err) assert.Equal(t, 400, resp.StatusCode) }) @@ -92,7 +110,7 @@ func TestHandler(t *testing.T) { Size: 100, }) require.NoError(t, err) - resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body)) + resp, err := httpClient.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body)) require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) @@ -106,7 +124,7 @@ func TestHandler(t *testing.T) { Size: 100, }) require.NoError(t, err) - resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body)) + resp, err := httpClient.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body)) require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) @@ -123,7 +141,7 @@ func TestHandler(t *testing.T) { require.NoError(t, err) req.Header.Set("Content-Type", "application/octet-stream") req.Header.Set("Content-Range", "bytes 0-99/*") - resp, err := http.DefaultClient.Do(req) + resp, err := httpClient.Do(req) require.NoError(t, err) assert.Equal(t, 400, resp.StatusCode) }) @@ -134,7 +152,7 @@ func TestHandler(t *testing.T) { require.NoError(t, err) req.Header.Set("Content-Type", "application/octet-stream") req.Header.Set("Content-Range", "bytes 0-99/*") - resp, err := http.DefaultClient.Do(req) + resp, err := httpClient.Do(req) require.NoError(t, err) assert.Equal(t, 400, resp.StatusCode) }) @@ -153,7 +171,7 @@ func TestHandler(t *testing.T) { Size: 100, }) require.NoError(t, err) - resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body)) + resp, err := httpClient.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body)) require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) @@ -169,12 +187,12 @@ func TestHandler(t *testing.T) { require.NoError(t, err) req.Header.Set("Content-Type", "application/octet-stream") req.Header.Set("Content-Range", "bytes 0-99/*") - resp, err := http.DefaultClient.Do(req) + resp, err := httpClient.Do(req) require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) } { - resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) + resp, err := httpClient.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) } @@ -184,7 +202,7 @@ func TestHandler(t *testing.T) { require.NoError(t, err) req.Header.Set("Content-Type", "application/octet-stream") req.Header.Set("Content-Range", "bytes 0-99/*") - resp, err := http.DefaultClient.Do(req) + resp, err := httpClient.Do(req) require.NoError(t, err) assert.Equal(t, 400, resp.StatusCode) } @@ -204,7 +222,7 @@ func TestHandler(t *testing.T) { Size: 100, }) require.NoError(t, err) - resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body)) + resp, err := httpClient.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body)) require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) @@ -220,7 +238,7 @@ func TestHandler(t *testing.T) { require.NoError(t, err) req.Header.Set("Content-Type", "application/octet-stream") req.Header.Set("Content-Range", "bytes xx-99/*") - resp, err := http.DefaultClient.Do(req) + resp, err := httpClient.Do(req) require.NoError(t, err) assert.Equal(t, 400, resp.StatusCode) } @@ -228,7 +246,7 @@ func TestHandler(t *testing.T) { t.Run("commit with bad id", func(t *testing.T) { { - resp, err := http.Post(fmt.Sprintf("%s/caches/invalid_id", base), "", nil) + resp, err := httpClient.Post(fmt.Sprintf("%s/caches/invalid_id", base), "", nil) require.NoError(t, err) assert.Equal(t, 400, resp.StatusCode) } @@ -236,7 +254,7 @@ func TestHandler(t *testing.T) { t.Run("commit with not exist id", func(t *testing.T) { { - resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, 100), "", nil) + resp, err := httpClient.Post(fmt.Sprintf("%s/caches/%d", base, 100), "", nil) require.NoError(t, err) assert.Equal(t, 400, resp.StatusCode) } @@ -256,7 +274,7 @@ func TestHandler(t *testing.T) { Size: 100, }) require.NoError(t, err) - resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body)) + resp, err := httpClient.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body)) require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) @@ -272,17 +290,17 @@ func TestHandler(t *testing.T) { require.NoError(t, err) req.Header.Set("Content-Type", "application/octet-stream") req.Header.Set("Content-Range", "bytes 0-99/*") - resp, err := http.DefaultClient.Do(req) + resp, err := httpClient.Do(req) require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) } { - resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) + resp, err := httpClient.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) } { - resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) + resp, err := httpClient.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) require.NoError(t, err) assert.Equal(t, 400, resp.StatusCode) } @@ -302,7 +320,7 @@ func TestHandler(t *testing.T) { Size: 100, }) require.NoError(t, err) - resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body)) + resp, err := httpClient.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body)) require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) @@ -318,31 +336,31 @@ func TestHandler(t *testing.T) { require.NoError(t, err) req.Header.Set("Content-Type", "application/octet-stream") req.Header.Set("Content-Range", "bytes 0-59/*") - resp, err := http.DefaultClient.Do(req) + resp, err := httpClient.Do(req) require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) } { - resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) + resp, err := httpClient.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) require.NoError(t, err) assert.Equal(t, 500, resp.StatusCode) } }) t.Run("get with bad id", func(t *testing.T) { - resp, err := http.Get(fmt.Sprintf("%s/artifacts/invalid_id", base)) + resp, err := httpClient.Get(fmt.Sprintf("%s/artifacts/invalid_id", base)) require.NoError(t, err) require.Equal(t, 400, resp.StatusCode) }) t.Run("get with not exist id", func(t *testing.T) { - resp, err := http.Get(fmt.Sprintf("%s/artifacts/%d", base, 100)) + resp, err := httpClient.Get(fmt.Sprintf("%s/artifacts/%d", base, 100)) require.NoError(t, err) require.Equal(t, 404, resp.StatusCode) }) t.Run("get with not exist id", func(t *testing.T) { - resp, err := http.Get(fmt.Sprintf("%s/artifacts/%d", base, 100)) + resp, err := httpClient.Get(fmt.Sprintf("%s/artifacts/%d", base, 100)) require.NoError(t, err) require.Equal(t, 404, resp.StatusCode) }) @@ -373,7 +391,7 @@ func TestHandler(t *testing.T) { key + "_a", }, ",") - resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version)) + resp, err := httpClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version)) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode) @@ -393,7 +411,7 @@ func TestHandler(t *testing.T) { assert.Equal(t, "hit", got.Result) assert.Equal(t, keys[except], got.CacheKey) - contentResp, err := http.Get(got.ArchiveLocation) + contentResp, err := httpClient.Get(got.ArchiveLocation) require.NoError(t, err) require.Equal(t, 200, contentResp.StatusCode) content, err := io.ReadAll(contentResp.Body) @@ -411,7 +429,7 @@ func TestHandler(t *testing.T) { { reqKey := key + "_aBc" - resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKey, version)) + resp, err := httpClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKey, version)) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode) got := struct { @@ -450,7 +468,7 @@ func TestHandler(t *testing.T) { key + "_a_b", }, ",") - resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version)) + resp, err := httpClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version)) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode) @@ -468,7 +486,7 @@ func TestHandler(t *testing.T) { require.NoError(t, json.NewDecoder(resp.Body).Decode(&got)) assert.Equal(t, keys[expect], got.CacheKey) - contentResp, err := http.Get(got.ArchiveLocation) + contentResp, err := httpClient.Get(got.ArchiveLocation) require.NoError(t, err) require.Equal(t, 200, contentResp.StatusCode) content, err := io.ReadAll(contentResp.Body) @@ -502,7 +520,7 @@ func TestHandler(t *testing.T) { key + "_a_b", }, ",") - resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version)) + resp, err := httpClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKeys, version)) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode) @@ -521,7 +539,7 @@ func TestHandler(t *testing.T) { require.NoError(t, json.NewDecoder(resp.Body).Decode(&got)) assert.Equal(t, keys[expect], got.CacheKey) - contentResp, err := http.Get(got.ArchiveLocation) + contentResp, err := httpClient.Get(got.ArchiveLocation) require.NoError(t, err) require.Equal(t, 200, contentResp.StatusCode) content, err := io.ReadAll(contentResp.Body) @@ -539,7 +557,7 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte Size: int64(len(content)), }) require.NoError(t, err) - resp, err := http.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body)) + resp, err := httpClient.Post(fmt.Sprintf("%s/caches", base), "application/json", bytes.NewReader(body)) require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) @@ -555,18 +573,18 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte require.NoError(t, err) req.Header.Set("Content-Type", "application/octet-stream") req.Header.Set("Content-Range", "bytes 0-99/*") - resp, err := http.DefaultClient.Do(req) + resp, err := httpClient.Do(req) require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) } { - resp, err := http.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) + resp, err := httpClient.Post(fmt.Sprintf("%s/caches/%d", base, id), "", nil) require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) } var archiveLocation string { - resp, err := http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version)) + resp, err := httpClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version)) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode) got := struct { @@ -580,7 +598,7 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte archiveLocation = got.ArchiveLocation } { - resp, err := http.Get(archiveLocation) //nolint:gosec + resp, err := httpClient.Get(archiveLocation) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode) got, err := io.ReadAll(resp.Body) diff --git a/act/artifactcache/mac_test.go b/act/artifactcache/mac_test.go index 10d6c5a8..2087ccc2 100644 --- a/act/artifactcache/mac_test.go +++ b/act/artifactcache/mac_test.go @@ -73,7 +73,7 @@ func TestMac(t *testing.T) { ts := "1337" mac := computeMac(secret, name, run, ts) - expectedMac := "09b0e9111660359d319c2d55c0664b5a6c5915c2f705b08af61aa63e7542f511" // * Precomputed, anytime the computeMac function changes this needs to be recalculated + expectedMac := "f666f06f917acb7186e152195b2a8c8d36d068ce683454be0878806e08e04f2b" // * Precomputed, anytime the computeMac function changes this needs to be recalculated require.Equal(t, mac, expectedMac) }) From 62310a5a092dad5ef752147bae027deffbdbaf71 Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Thu, 20 Mar 2025 14:20:33 +0100 Subject: [PATCH 23/25] set external url header --- act/artifactcache/handler_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/act/artifactcache/handler_test.go b/act/artifactcache/handler_test.go index 6059cd2d..cf0e11df 100644 --- a/act/artifactcache/handler_test.go +++ b/act/artifactcache/handler_test.go @@ -23,6 +23,8 @@ const cache_runnum = "1" const cache_timestamp = "0" const cache_mac = "c13854dd1ac599d1d61680cd93c26b77ba0ee10f374a3408bcaea82f38ca1865" +var handlerExternalUrl string + type AuthHeaderTransport struct { T http.RoundTripper } @@ -32,6 +34,7 @@ func (t *AuthHeaderTransport) RoundTrip(req *http.Request) (*http.Response, erro req.Header.Set("Forgejo-Cache-RunNumber", cache_runnum) req.Header.Set("Forgejo-Cache-Timestamp", cache_timestamp) req.Header.Set("Forgejo-Cache-MAC", cache_mac) + req.Header.Set("Forgejo-Cache-Host", handlerExternalUrl) return t.T.RoundTrip(req) } @@ -43,6 +46,7 @@ func TestHandler(t *testing.T) { handler, err := StartHandler(dir, "", 0, "secret", nil) require.NoError(t, err) + handlerExternalUrl = handler.ExternalURL() base := fmt.Sprintf("%s%s", handler.ExternalURL(), urlBase) defer func() { From 915008189228afa9cf8e712b0c7170df0ed2aeed Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Thu, 20 Mar 2025 14:35:26 +0100 Subject: [PATCH 24/25] return 404 when not found --- act/artifactcache/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/act/artifactcache/handler.go b/act/artifactcache/handler.go index 0f8afd93..2da6febd 100644 --- a/act/artifactcache/handler.go +++ b/act/artifactcache/handler.go @@ -391,7 +391,7 @@ func (h *Handler) get(w http.ResponseWriter, r *http.Request, params httprouter. defer db.Close() if err := db.Get(id, cache); err != nil { if errors.Is(err, bolthold.ErrNotFound) { - h.responseJSON(w, r, 400, fmt.Errorf("cache %d: not reserved", id)) + h.responseJSON(w, r, 404, fmt.Errorf("cache %d: not reserved", id)) return } h.responseJSON(w, r, 500, err) From ef94958cd5e6a74ba18e46837f53b5fe0bb179b2 Mon Sep 17 00:00:00 2001 From: Kwonunn Date: Fri, 21 Mar 2025 13:57:25 +0100 Subject: [PATCH 25/25] review: Gusted review --- act/artifactcache/handler_test.go | 27 +++++++++++++++++++++++++++ act/artifactcache/mac.go | 2 +- act/artifactcache/mac_test.go | 2 +- act/cacheproxy/handler.go | 15 ++++----------- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/act/artifactcache/handler_test.go b/act/artifactcache/handler_test.go index cf0e11df..ebfdad77 100644 --- a/act/artifactcache/handler_test.go +++ b/act/artifactcache/handler_test.go @@ -369,6 +369,33 @@ func TestHandler(t *testing.T) { require.Equal(t, 404, resp.StatusCode) }) + t.Run("get with bad MAC", func(t *testing.T) { + key := strings.ToLower(t.Name()) + version := "c19da02a2bd7e77277f1ac29ab45c09b7d46b4ee758284e26bb3045ad11d9d20" + content := make([]byte, 100) + _, err := rand.Read(content) + require.NoError(t, err) + + uploadCacheNormally(t, base, key, version, content) + + // Perform the request with the custom `httpClient` which will send correct MAC data + resp, err := httpClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version)) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) + + // Perform the same request with incorrect MAC data + req, err := http.NewRequest("GET", fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version), nil) + require.NoError(t, err) + req.Header.Set("Forgejo-Cache-Repo", cache_repo) + req.Header.Set("Forgejo-Cache-RunNumber", cache_runnum) + req.Header.Set("Forgejo-Cache-Timestamp", cache_timestamp) + req.Header.Set("Forgejo-Cache-MAC", "33f0e850ba0bdfd2f3e66ff79c1f8004b8226114e3b2e65c229222bb59df0f9d") // ! This is not the correct MAC + req.Header.Set("Forgejo-Cache-Host", handlerExternalUrl) + resp, err = http.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version)) + require.NoError(t, err) + require.Equal(t, 403, resp.StatusCode) + }) + t.Run("get with multiple keys", func(t *testing.T) { version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20" key := strings.ToLower(t.Name()) diff --git a/act/artifactcache/mac.go b/act/artifactcache/mac.go index 1b129b77..16f46c26 100644 --- a/act/artifactcache/mac.go +++ b/act/artifactcache/mac.go @@ -28,7 +28,7 @@ func (h *Handler) validateMac(rundata cacheproxy.RunData) (string, error) { if hmac.Equal([]byte(expectedMAC), []byte(rundata.RepositoryMAC)) { return rundata.RepositoryFullName, nil } - return rundata.RepositoryFullName, ErrValidation + return "", ErrValidation } func validateAge(ts string) bool { diff --git a/act/artifactcache/mac_test.go b/act/artifactcache/mac_test.go index 2087ccc2..b59280c7 100644 --- a/act/artifactcache/mac_test.go +++ b/act/artifactcache/mac_test.go @@ -63,7 +63,7 @@ func TestMac(t *testing.T) { repoName, err := handler.validateMac(rundata) require.Error(t, err) - require.Equal(t, name, repoName) + require.Equal(t, "", repoName) }) t.Run("compute correct mac", func(t *testing.T) { diff --git a/act/cacheproxy/handler.go b/act/cacheproxy/handler.go index 13997ef2..bd047926 100644 --- a/act/cacheproxy/handler.go +++ b/act/cacheproxy/handler.go @@ -78,7 +78,6 @@ func StartHandler(targetHost string, outboundIP string, port uint16, cacheSecret h.logger = logger h.cacheSecret = cacheSecret - // h.runs = make(map[string]RunData) if outboundIP != "" { h.outboundIP = outboundIP @@ -125,9 +124,7 @@ func StartHandler(targetHost string, outboundIP string, port uint16, cacheSecret } func proxyRequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - proxy.ServeHTTP(w, r) - } + return proxy.ServeHTTP } func (h *Handler) newReverseProxy(targetHost string) (*httputil.ReverseProxy, error) { @@ -141,13 +138,12 @@ func (h *Handler) newReverseProxy(targetHost string) (*httputil.ReverseProxy, er matches := urlRegex.FindStringSubmatch(r.In.URL.Path) id := matches[1] data, ok := h.runs.Load(id) - var runData = data.(RunData) if !ok { // The ID doesn't exist. - // ! this should probably be handled more gracefully but i can't figure out how - // ! it really shouldn't happen anyway so it's fine for now + h.logger.Warn(fmt.Sprintf("Tried starting a cache proxy with id %s, which does not exist.", id)) return } + var runData = data.(RunData) uri := matches[2] r.SetURL(targetURL) @@ -212,10 +208,7 @@ func (h *Handler) Close() error { } if h.listener != nil { err := h.listener.Close() - if errors.Is(err, net.ErrClosed) { - err = nil - } - if err != nil { + if !errors.Is(err, net.ErrClosed) { retErr = err } h.listener = nil