mirror of
https://code.forgejo.org/forgejo/runner.git
synced 2025-09-15 18:57:01 +00:00
Merge pull request 'Fix security issues with cache by proxying access' (#107) from Kwonunn/act:fix/cache-proxy into main
Reviewed-on: https://code.forgejo.org/forgejo/act/pulls/107
This commit is contained in:
commit
776ccb8b21
8 changed files with 560 additions and 63 deletions
|
@ -20,6 +20,7 @@ import (
|
||||||
"github.com/timshannon/bolthold"
|
"github.com/timshannon/bolthold"
|
||||||
"go.etcd.io/bbolt"
|
"go.etcd.io/bbolt"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/cacheproxy"
|
||||||
"github.com/nektos/act/pkg/common"
|
"github.com/nektos/act/pkg/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -34,6 +35,7 @@ type Handler struct {
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
server *http.Server
|
server *http.Server
|
||||||
logger logrus.FieldLogger
|
logger logrus.FieldLogger
|
||||||
|
secret string
|
||||||
|
|
||||||
gcing atomic.Bool
|
gcing atomic.Bool
|
||||||
gcAt time.Time
|
gcAt time.Time
|
||||||
|
@ -41,8 +43,10 @@ type Handler struct {
|
||||||
outboundIP string
|
outboundIP string
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartHandler(dir, outboundIP string, port uint16, logger logrus.FieldLogger) (*Handler, error) {
|
func StartHandler(dir, outboundIP string, port uint16, secret string, logger logrus.FieldLogger) (*Handler, error) {
|
||||||
h := &Handler{}
|
h := &Handler{
|
||||||
|
secret: secret,
|
||||||
|
}
|
||||||
|
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
discard := logrus.New()
|
discard := logrus.New()
|
||||||
|
@ -155,7 +159,14 @@ func (h *Handler) openDB() (*bolthold.Store, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /_apis/artifactcache/cache
|
// 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) {
|
||||||
|
rundata := runDataFromHeaders(r)
|
||||||
|
repo, err := h.validateMac(rundata)
|
||||||
|
if err != nil {
|
||||||
|
h.responseJSON(w, r, 403, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
keys := strings.Split(r.URL.Query().Get("keys"), ",")
|
keys := strings.Split(r.URL.Query().Get("keys"), ",")
|
||||||
// cache keys are case insensitive
|
// cache keys are case insensitive
|
||||||
for i, key := range keys {
|
for i, key := range keys {
|
||||||
|
@ -170,7 +181,7 @@ func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Para
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
cache, err := findCache(db, keys, version)
|
cache, err := findCache(db, repo, keys, version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.responseJSON(w, r, 500, err)
|
h.responseJSON(w, r, 500, err)
|
||||||
return
|
return
|
||||||
|
@ -188,15 +199,23 @@ func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Para
|
||||||
h.responseJSON(w, r, 204)
|
h.responseJSON(w, r, 204)
|
||||||
return
|
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{
|
h.responseJSON(w, r, 200, map[string]any{
|
||||||
"result": "hit",
|
"result": "hit",
|
||||||
"archiveLocation": fmt.Sprintf("%s%s/artifacts/%d", h.ExternalURL(), urlBase, cache.ID),
|
"archiveLocation": archiveLocation,
|
||||||
"cacheKey": cache.Key,
|
"cacheKey": cache.Key,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /_apis/artifactcache/caches
|
// 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) {
|
||||||
|
rundata := runDataFromHeaders(r)
|
||||||
|
repo, err := h.validateMac(rundata)
|
||||||
|
if err != nil {
|
||||||
|
h.responseJSON(w, r, 403, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
api := &Request{}
|
api := &Request{}
|
||||||
if err := json.NewDecoder(r.Body).Decode(api); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(api); err != nil {
|
||||||
h.responseJSON(w, r, 400, err)
|
h.responseJSON(w, r, 400, err)
|
||||||
|
@ -216,6 +235,7 @@ func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.P
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
cache.CreatedAt = now
|
cache.CreatedAt = now
|
||||||
cache.UsedAt = now
|
cache.UsedAt = now
|
||||||
|
cache.Repo = repo
|
||||||
if err := insertCache(db, cache); err != nil {
|
if err := insertCache(db, cache); err != nil {
|
||||||
h.responseJSON(w, r, 500, err)
|
h.responseJSON(w, r, 500, err)
|
||||||
return
|
return
|
||||||
|
@ -227,6 +247,13 @@ func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.P
|
||||||
|
|
||||||
// PATCH /_apis/artifactcache/caches/:id
|
// PATCH /_apis/artifactcache/caches/:id
|
||||||
func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||||
|
rundata := runDataFromHeaders(r)
|
||||||
|
repo, err := h.validateMac(rundata)
|
||||||
|
if err != nil {
|
||||||
|
h.responseJSON(w, r, 403, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
id, err := strconv.ParseUint(params.ByName("id"), 10, 64)
|
id, err := strconv.ParseUint(params.ByName("id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.responseJSON(w, r, 400, err)
|
h.responseJSON(w, r, 400, err)
|
||||||
|
@ -249,11 +276,17 @@ func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprout
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Should not happen
|
||||||
|
if cache.Repo != repo {
|
||||||
|
h.responseJSON(w, r, 500, ErrValidation)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if cache.Complete {
|
if cache.Complete {
|
||||||
h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
|
h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
db.Close()
|
defer db.Close()
|
||||||
start, _, err := parseContentRange(r.Header.Get("Content-Range"))
|
start, _, err := parseContentRange(r.Header.Get("Content-Range"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.responseJSON(w, r, 400, err)
|
h.responseJSON(w, r, 400, err)
|
||||||
|
@ -262,12 +295,19 @@ func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprout
|
||||||
if err := h.storage.Write(cache.ID, start, r.Body); err != nil {
|
if err := h.storage.Write(cache.ID, start, r.Body); err != nil {
|
||||||
h.responseJSON(w, r, 500, err)
|
h.responseJSON(w, r, 500, err)
|
||||||
}
|
}
|
||||||
h.useCache(id)
|
h.useCache(db, cache)
|
||||||
h.responseJSON(w, r, 200)
|
h.responseJSON(w, r, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /_apis/artifactcache/caches/:id
|
// POST /_apis/artifactcache/caches/:id
|
||||||
func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||||
|
rundata := runDataFromHeaders(r)
|
||||||
|
repo, err := h.validateMac(rundata)
|
||||||
|
if err != nil {
|
||||||
|
h.responseJSON(w, r, 403, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
id, err := strconv.ParseUint(params.ByName("id"), 10, 64)
|
id, err := strconv.ParseUint(params.ByName("id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.responseJSON(w, r, 400, err)
|
h.responseJSON(w, r, 400, err)
|
||||||
|
@ -290,6 +330,12 @@ func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprout
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Should not happen
|
||||||
|
if cache.Repo != repo {
|
||||||
|
h.responseJSON(w, r, 500, ErrValidation)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if cache.Complete {
|
if cache.Complete {
|
||||||
h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
|
h.responseJSON(w, r, 400, fmt.Errorf("cache %v %q: already complete", cache.ID, cache.Key))
|
||||||
return
|
return
|
||||||
|
@ -323,17 +369,53 @@ func (h *Handler) commit(w http.ResponseWriter, r *http.Request, params httprout
|
||||||
|
|
||||||
// GET /_apis/artifactcache/artifacts/:id
|
// GET /_apis/artifactcache/artifacts/:id
|
||||||
func (h *Handler) get(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
func (h *Handler) get(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||||
|
rundata := runDataFromHeaders(r)
|
||||||
|
repo, err := h.validateMac(rundata)
|
||||||
|
if err != nil {
|
||||||
|
h.responseJSON(w, r, 403, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
id, err := strconv.ParseUint(params.ByName("id"), 10, 64)
|
id, err := strconv.ParseUint(params.ByName("id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.responseJSON(w, r, 400, err)
|
h.responseJSON(w, r, 400, err)
|
||||||
return
|
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, 404, 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)
|
h.storage.Serve(w, r, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /_apis/artifactcache/clean
|
// 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, _ httprouter.Params) {
|
||||||
|
rundata := runDataFromHeaders(r)
|
||||||
|
_, err := h.validateMac(rundata)
|
||||||
|
if err != nil {
|
||||||
|
h.responseJSON(w, r, 403, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
// TODO: don't support force deleting cache entries
|
// 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
|
// see: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
|
||||||
|
|
||||||
|
@ -349,12 +431,13 @@ func (h *Handler) middleware(handler httprouter.Handle) httprouter.Handle {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if not found, return (nil, nil) instead of an error.
|
// 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{}
|
cache := &Cache{}
|
||||||
for _, prefix := range keys {
|
for _, prefix := range keys {
|
||||||
// if a key in the list matches exactly, don't return partial matches
|
// if a key in the list matches exactly, don't return partial matches
|
||||||
if err := db.FindOne(cache,
|
if err := db.FindOne(cache,
|
||||||
bolthold.Where("Key").Eq(prefix).
|
bolthold.Where("Repo").Eq(repo).
|
||||||
|
And("Key").Eq(prefix).
|
||||||
And("Version").Eq(version).
|
And("Version").Eq(version).
|
||||||
And("Complete").Eq(true).
|
And("Complete").Eq(true).
|
||||||
SortBy("CreatedAt").Reverse()); err == nil || !errors.Is(err, bolthold.ErrNotFound) {
|
SortBy("CreatedAt").Reverse()); err == nil || !errors.Is(err, bolthold.ErrNotFound) {
|
||||||
|
@ -369,7 +452,8 @@ func findCache(db *bolthold.Store, keys []string, version string) (*Cache, error
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := db.FindOne(cache,
|
if err := db.FindOne(cache,
|
||||||
bolthold.Where("Key").RegExp(re).
|
bolthold.Where("Repo").Eq(repo).
|
||||||
|
And("Key").RegExp(re).
|
||||||
And("Version").Eq(version).
|
And("Version").Eq(version).
|
||||||
And("Complete").Eq(true).
|
And("Complete").Eq(true).
|
||||||
SortBy("CreatedAt").Reverse()); err != nil {
|
SortBy("CreatedAt").Reverse()); err != nil {
|
||||||
|
@ -394,16 +478,7 @@ func insertCache(db *bolthold.Store, cache *Cache) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) useCache(id uint64) {
|
func (h *Handler) useCache(db *bolthold.Store, cache *Cache) {
|
||||||
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()
|
cache.UsedAt = time.Now().Unix()
|
||||||
_ = db.Update(cache.ID, cache)
|
_ = db.Update(cache.ID, cache)
|
||||||
}
|
}
|
||||||
|
@ -554,3 +629,12 @@ func parseContentRange(s string) (uint64, uint64, error) {
|
||||||
}
|
}
|
||||||
return start, stop, nil
|
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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -18,11 +18,35 @@ import (
|
||||||
"go.etcd.io/bbolt"
|
"go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const cache_repo = "testuser/repo"
|
||||||
|
const cache_runnum = "1"
|
||||||
|
const cache_timestamp = "0"
|
||||||
|
const cache_mac = "c13854dd1ac599d1d61680cd93c26b77ba0ee10f374a3408bcaea82f38ca1865"
|
||||||
|
|
||||||
|
var handlerExternalUrl string
|
||||||
|
|
||||||
|
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)
|
||||||
|
req.Header.Set("Forgejo-Cache-Host", handlerExternalUrl)
|
||||||
|
return t.T.RoundTrip(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
var httpClientTransport = AuthHeaderTransport{http.DefaultTransport}
|
||||||
|
var httpClient = http.Client{Transport: &httpClientTransport}
|
||||||
|
|
||||||
func TestHandler(t *testing.T) {
|
func TestHandler(t *testing.T) {
|
||||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
handler, err := StartHandler(dir, "", 0, nil)
|
handler, err := StartHandler(dir, "", 0, "secret", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
handlerExternalUrl = handler.ExternalURL()
|
||||||
base := fmt.Sprintf("%s%s", handler.ExternalURL(), urlBase)
|
base := fmt.Sprintf("%s%s", handler.ExternalURL(), urlBase)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -41,7 +65,7 @@ func TestHandler(t *testing.T) {
|
||||||
require.NoError(t, handler.Close())
|
require.NoError(t, handler.Close())
|
||||||
assert.Nil(t, handler.server)
|
assert.Nil(t, handler.server)
|
||||||
assert.Nil(t, handler.listener)
|
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)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
|
@ -49,7 +73,7 @@ func TestHandler(t *testing.T) {
|
||||||
t.Run("get not exist", func(t *testing.T) {
|
t.Run("get not exist", func(t *testing.T) {
|
||||||
key := strings.ToLower(t.Name())
|
key := strings.ToLower(t.Name())
|
||||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
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.NoError(t, err)
|
||||||
require.Equal(t, 204, resp.StatusCode)
|
require.Equal(t, 204, resp.StatusCode)
|
||||||
})
|
})
|
||||||
|
@ -64,7 +88,7 @@ func TestHandler(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("clean", func(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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
})
|
})
|
||||||
|
@ -72,7 +96,7 @@ func TestHandler(t *testing.T) {
|
||||||
t.Run("reserve with bad request", func(t *testing.T) {
|
t.Run("reserve with bad request", func(t *testing.T) {
|
||||||
body := []byte(`invalid json`)
|
body := []byte(`invalid json`)
|
||||||
require.NoError(t, err)
|
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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 400, resp.StatusCode)
|
assert.Equal(t, 400, resp.StatusCode)
|
||||||
})
|
})
|
||||||
|
@ -90,7 +114,7 @@ func TestHandler(t *testing.T) {
|
||||||
Size: 100,
|
Size: 100,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
|
@ -104,7 +128,7 @@ func TestHandler(t *testing.T) {
|
||||||
Size: 100,
|
Size: 100,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
|
@ -121,7 +145,7 @@ func TestHandler(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Set("Content-Type", "application/octet-stream")
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := httpClient.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 400, resp.StatusCode)
|
assert.Equal(t, 400, resp.StatusCode)
|
||||||
})
|
})
|
||||||
|
@ -132,7 +156,7 @@ func TestHandler(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Set("Content-Type", "application/octet-stream")
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := httpClient.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 400, resp.StatusCode)
|
assert.Equal(t, 400, resp.StatusCode)
|
||||||
})
|
})
|
||||||
|
@ -151,7 +175,7 @@ func TestHandler(t *testing.T) {
|
||||||
Size: 100,
|
Size: 100,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
|
@ -167,12 +191,12 @@ func TestHandler(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Set("Content-Type", "application/octet-stream")
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := httpClient.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
@ -182,7 +206,7 @@ func TestHandler(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Set("Content-Type", "application/octet-stream")
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := httpClient.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 400, resp.StatusCode)
|
assert.Equal(t, 400, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
@ -202,7 +226,7 @@ func TestHandler(t *testing.T) {
|
||||||
Size: 100,
|
Size: 100,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
|
@ -218,7 +242,7 @@ func TestHandler(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Set("Content-Type", "application/octet-stream")
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
req.Header.Set("Content-Range", "bytes xx-99/*")
|
req.Header.Set("Content-Range", "bytes xx-99/*")
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := httpClient.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 400, resp.StatusCode)
|
assert.Equal(t, 400, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
@ -226,7 +250,7 @@ func TestHandler(t *testing.T) {
|
||||||
|
|
||||||
t.Run("commit with bad id", func(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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 400, resp.StatusCode)
|
assert.Equal(t, 400, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
@ -234,7 +258,7 @@ func TestHandler(t *testing.T) {
|
||||||
|
|
||||||
t.Run("commit with not exist id", func(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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 400, resp.StatusCode)
|
assert.Equal(t, 400, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
@ -254,7 +278,7 @@ func TestHandler(t *testing.T) {
|
||||||
Size: 100,
|
Size: 100,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
|
@ -270,17 +294,17 @@ func TestHandler(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Set("Content-Type", "application/octet-stream")
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := httpClient.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 400, resp.StatusCode)
|
assert.Equal(t, 400, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
@ -300,7 +324,7 @@ func TestHandler(t *testing.T) {
|
||||||
Size: 100,
|
Size: 100,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
|
@ -316,35 +340,62 @@ func TestHandler(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Set("Content-Type", "application/octet-stream")
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
req.Header.Set("Content-Range", "bytes 0-59/*")
|
req.Header.Set("Content-Range", "bytes 0-59/*")
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := httpClient.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 500, resp.StatusCode)
|
assert.Equal(t, 500, resp.StatusCode)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("get with bad id", func(t *testing.T) {
|
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.NoError(t, err)
|
||||||
require.Equal(t, 400, resp.StatusCode)
|
require.Equal(t, 400, resp.StatusCode)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("get with not exist id", func(t *testing.T) {
|
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.NoError(t, err)
|
||||||
require.Equal(t, 404, resp.StatusCode)
|
require.Equal(t, 404, resp.StatusCode)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("get with not exist id", func(t *testing.T) {
|
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.NoError(t, err)
|
||||||
require.Equal(t, 404, resp.StatusCode)
|
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) {
|
t.Run("get with multiple keys", func(t *testing.T) {
|
||||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||||
key := strings.ToLower(t.Name())
|
key := strings.ToLower(t.Name())
|
||||||
|
@ -371,7 +422,7 @@ func TestHandler(t *testing.T) {
|
||||||
key + "_a",
|
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.NoError(t, err)
|
||||||
require.Equal(t, 200, resp.StatusCode)
|
require.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
|
@ -391,7 +442,7 @@ func TestHandler(t *testing.T) {
|
||||||
assert.Equal(t, "hit", got.Result)
|
assert.Equal(t, "hit", got.Result)
|
||||||
assert.Equal(t, keys[except], got.CacheKey)
|
assert.Equal(t, keys[except], got.CacheKey)
|
||||||
|
|
||||||
contentResp, err := http.Get(got.ArchiveLocation)
|
contentResp, err := httpClient.Get(got.ArchiveLocation)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 200, contentResp.StatusCode)
|
require.Equal(t, 200, contentResp.StatusCode)
|
||||||
content, err := io.ReadAll(contentResp.Body)
|
content, err := io.ReadAll(contentResp.Body)
|
||||||
|
@ -409,7 +460,7 @@ func TestHandler(t *testing.T) {
|
||||||
|
|
||||||
{
|
{
|
||||||
reqKey := key + "_aBc"
|
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.NoError(t, err)
|
||||||
require.Equal(t, 200, resp.StatusCode)
|
require.Equal(t, 200, resp.StatusCode)
|
||||||
got := struct {
|
got := struct {
|
||||||
|
@ -448,7 +499,7 @@ func TestHandler(t *testing.T) {
|
||||||
key + "_a_b",
|
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.NoError(t, err)
|
||||||
require.Equal(t, 200, resp.StatusCode)
|
require.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
|
@ -466,7 +517,7 @@ func TestHandler(t *testing.T) {
|
||||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||||
assert.Equal(t, keys[expect], got.CacheKey)
|
assert.Equal(t, keys[expect], got.CacheKey)
|
||||||
|
|
||||||
contentResp, err := http.Get(got.ArchiveLocation)
|
contentResp, err := httpClient.Get(got.ArchiveLocation)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 200, contentResp.StatusCode)
|
require.Equal(t, 200, contentResp.StatusCode)
|
||||||
content, err := io.ReadAll(contentResp.Body)
|
content, err := io.ReadAll(contentResp.Body)
|
||||||
|
@ -500,7 +551,7 @@ func TestHandler(t *testing.T) {
|
||||||
key + "_a_b",
|
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.NoError(t, err)
|
||||||
require.Equal(t, 200, resp.StatusCode)
|
require.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
|
@ -519,7 +570,7 @@ func TestHandler(t *testing.T) {
|
||||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||||
assert.Equal(t, keys[expect], got.CacheKey)
|
assert.Equal(t, keys[expect], got.CacheKey)
|
||||||
|
|
||||||
contentResp, err := http.Get(got.ArchiveLocation)
|
contentResp, err := httpClient.Get(got.ArchiveLocation)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 200, contentResp.StatusCode)
|
require.Equal(t, 200, contentResp.StatusCode)
|
||||||
content, err := io.ReadAll(contentResp.Body)
|
content, err := io.ReadAll(contentResp.Body)
|
||||||
|
@ -537,7 +588,7 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
|
||||||
Size: int64(len(content)),
|
Size: int64(len(content)),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
|
@ -553,18 +604,18 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
req.Header.Set("Content-Type", "application/octet-stream")
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
req.Header.Set("Content-Range", "bytes 0-99/*")
|
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := httpClient.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 200, resp.StatusCode)
|
assert.Equal(t, 200, resp.StatusCode)
|
||||||
}
|
}
|
||||||
var archiveLocation string
|
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.NoError(t, err)
|
||||||
require.Equal(t, 200, resp.StatusCode)
|
require.Equal(t, 200, resp.StatusCode)
|
||||||
got := struct {
|
got := struct {
|
||||||
|
@ -578,7 +629,7 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
|
||||||
archiveLocation = got.ArchiveLocation
|
archiveLocation = got.ArchiveLocation
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
resp, err := http.Get(archiveLocation) //nolint:gosec
|
resp, err := httpClient.Get(archiveLocation)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 200, resp.StatusCode)
|
require.Equal(t, 200, resp.StatusCode)
|
||||||
got, err := io.ReadAll(resp.Body)
|
got, err := io.ReadAll(resp.Body)
|
||||||
|
@ -589,7 +640,7 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
|
||||||
|
|
||||||
func TestHandler_gcCache(t *testing.T) {
|
func TestHandler_gcCache(t *testing.T) {
|
||||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
handler, err := StartHandler(dir, "", 0, nil)
|
handler, err := StartHandler(dir, "", 0, "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|
53
act/artifactcache/mac.go
Normal file
53
act/artifactcache/mac.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package artifactcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/cacheproxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrValidation = errors.New("validation error")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *Handler) validateMac(rundata cacheproxy.RunData) (string, error) {
|
||||||
|
// TODO: allow configurable max age
|
||||||
|
if !validateAge(rundata.Timestamp) {
|
||||||
|
return "", ErrValidation
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedMAC := computeMac(h.secret, rundata.RepositoryFullName, rundata.RunNumber, rundata.Timestamp)
|
||||||
|
if hmac.Equal([]byte(expectedMAC), []byte(rundata.RepositoryMAC)) {
|
||||||
|
return rundata.RepositoryFullName, nil
|
||||||
|
}
|
||||||
|
return "", ErrValidation
|
||||||
|
}
|
||||||
|
|
||||||
|
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(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))
|
||||||
|
}
|
80
act/artifactcache/mac_test.go
Normal file
80
act/artifactcache/mac_test.go
Normal file
|
@ -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, "", 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 := "f666f06f917acb7186e152195b2a8c8d36d068ce683454be0878806e08e04f2b" // * Precomputed, anytime the computeMac function changes this needs to be recalculated
|
||||||
|
|
||||||
|
require.Equal(t, mac, expectedMac)
|
||||||
|
})
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ func (c *Request) ToCache() *Cache {
|
||||||
|
|
||||||
type Cache struct {
|
type Cache struct {
|
||||||
ID uint64 `json:"id" boltholdKey:"ID"`
|
ID uint64 `json:"id" boltholdKey:"ID"`
|
||||||
|
Repo string `json:"repo" boltholdIndex:"Repo"`
|
||||||
Key string `json:"key" boltholdIndex:"Key"`
|
Key string `json:"key" boltholdIndex:"Key"`
|
||||||
Version string `json:"version" boltholdIndex:"Version"`
|
Version string `json:"version" boltholdIndex:"Version"`
|
||||||
Size int64 `json:"cacheSize"`
|
Size int64 `json:"cacheSize"`
|
||||||
|
|
227
act/cacheproxy/handler.go
Normal file
227
act/cacheproxy/handler.go
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cacheproxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/nektos/act/pkg/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
urlBase = "/_apis/artifactcache"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
urlRegex = regexp.MustCompile(`/(\w+)(/_apis/artifactcache/.+)`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
router *httprouter.Router
|
||||||
|
listener net.Listener
|
||||||
|
server *http.Server
|
||||||
|
logger logrus.FieldLogger
|
||||||
|
|
||||||
|
outboundIP string
|
||||||
|
|
||||||
|
cacheServerHost string
|
||||||
|
|
||||||
|
cacheSecret string
|
||||||
|
|
||||||
|
runs sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunData struct {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartHandler(targetHost string, outboundIP string, port uint16, cacheSecret string, 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.cacheSecret = cacheSecret
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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
|
||||||
|
|
||||||
|
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 proxyRequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) {
|
||||||
|
return proxy.ServeHTTP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) newReverseProxy(targetHost string) (*httputil.ReverseProxy, error) {
|
||||||
|
targetURL, err := url.Parse(targetHost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := &httputil.ReverseProxy{
|
||||||
|
Rewrite: func(r *httputil.ProxyRequest) {
|
||||||
|
matches := urlRegex.FindStringSubmatch(r.In.URL.Path)
|
||||||
|
id := matches[1]
|
||||||
|
data, ok := h.runs.Load(id)
|
||||||
|
if !ok {
|
||||||
|
// The ID doesn't exist.
|
||||||
|
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)
|
||||||
|
r.Out.URL.Path = uri
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ExternalURL() string {
|
||||||
|
// TODO: make the external url configurable if necessary
|
||||||
|
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.
|
||||||
|
// 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) {
|
||||||
|
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 {
|
||||||
|
// The key was unique and added successfully
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.New("Repeated collisions in generating run id")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) RemoveRun(runID string) error {
|
||||||
|
_, existed := h.runs.LoadAndDelete(runID)
|
||||||
|
if !existed {
|
||||||
|
return errors.New("The run id was not known to the proxy")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
retErr = err
|
||||||
|
}
|
||||||
|
h.listener = nil
|
||||||
|
}
|
||||||
|
return retErr
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
|
@ -59,6 +59,7 @@ type Input struct {
|
||||||
logPrefixJobID bool
|
logPrefixJobID bool
|
||||||
networkName string
|
networkName string
|
||||||
useNewActionCache bool
|
useNewActionCache bool
|
||||||
|
secret string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Input) resolve(path string) string {
|
func (i *Input) resolve(path string) string {
|
||||||
|
|
|
@ -632,7 +632,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
|
||||||
var cacheHandler *artifactcache.Handler
|
var cacheHandler *artifactcache.Handler
|
||||||
if !input.noCacheServer && envs[cacheURLKey] == "" {
|
if !input.noCacheServer && envs[cacheURLKey] == "" {
|
||||||
var err error
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue