enforce remote quarantine for fresh immutable artifacts
- refuse uncached immutable paths published under quarantine_days ago with a JSON 403 - take publish time from Last-Modified, else a Redis first-seen key - let goproxy virtual probes skip a quarantining member - default quarantine_days to 3 on create and keep it on update - document quarantine in caching.md
This commit is contained in:
@@ -2,6 +2,7 @@ package v1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
@@ -121,7 +123,7 @@ func (h *ProxyHandler) serveFetch(w http.ResponseWriter, r *http.Request, remote
|
||||
if err != nil {
|
||||
var proxyErr *proxy.ProxyError
|
||||
if errors.As(err, &proxyErr) {
|
||||
http.Error(w, proxyErr.Message, proxyErr.Status)
|
||||
writeProxyError(w, proxyErr)
|
||||
return
|
||||
}
|
||||
slog.Error("proxy fetch failed", "remote", remote.Name, "path", path, "error", err)
|
||||
@@ -149,6 +151,21 @@ func (h *ProxyHandler) serveFetch(w http.ResponseWriter, r *http.Request, remote
|
||||
io.Copy(w, body)
|
||||
}
|
||||
|
||||
// writeProxyError answers a quarantine refusal as JSON and anything else as text.
|
||||
func writeProxyError(w http.ResponseWriter, pe *proxy.ProxyError) {
|
||||
if pe.Published.IsZero() {
|
||||
http.Error(w, pe.Message, pe.Status)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(pe.Status)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": pe.Message,
|
||||
"published": pe.Published.Format(time.RFC3339),
|
||||
"available": pe.Available.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
func rewritesMutable(prov provider.Provider, path string) bool {
|
||||
_, sr := prov.(provider.ServeRewriter)
|
||||
_, rr := prov.(provider.ResponseRewriter)
|
||||
@@ -176,7 +193,7 @@ func (h *ProxyHandler) handleProxyHead(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
var proxyErr *proxy.ProxyError
|
||||
if errors.As(err, &proxyErr) {
|
||||
http.Error(w, proxyErr.Message, proxyErr.Status)
|
||||
writeProxyError(w, proxyErr)
|
||||
return
|
||||
}
|
||||
slog.Error("proxy head failed", "remote", remote.Name, "path", path, "error", err)
|
||||
|
||||
@@ -71,8 +71,11 @@ func (h *RemotesHandler) get(w http.ResponseWriter, r *http.Request) {
|
||||
// that omits mutable_ttl gets it, an explicit 0 still means never expire.
|
||||
const defaultMutableTTL = 3600
|
||||
|
||||
// defaultQuarantineDays matches the remotes.quarantine_days column default.
|
||||
const defaultQuarantineDays = 3
|
||||
|
||||
func (h *RemotesHandler) create(w http.ResponseWriter, r *http.Request) {
|
||||
remote := models.Remote{MutableTTL: defaultMutableTTL}
|
||||
remote := models.Remote{MutableTTL: defaultMutableTTL, QuarantineDays: defaultQuarantineDays}
|
||||
if err := json.NewDecoder(r.Body).Decode(&remote); err != nil {
|
||||
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||
return
|
||||
@@ -126,6 +129,7 @@ func (h *RemotesHandler) update(w http.ResponseWriter, r *http.Request) {
|
||||
var remote models.Remote
|
||||
if oldKnown {
|
||||
remote.MutableTTL = existing.MutableTTL
|
||||
remote.QuarantineDays = existing.QuarantineDays
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&remote); err != nil {
|
||||
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||
|
||||
@@ -55,3 +55,39 @@ func TestMutableTTLOmittedVsZero(t *testing.T) {
|
||||
t.Errorf("explicit 0 stored as %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuarantineDaysDefault(t *testing.T) {
|
||||
if testDSN == "" {
|
||||
t.Skip("Docker unavailable")
|
||||
}
|
||||
db, err := database.New(testDSN)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
h := NewRemotesHandler(db, nil, nil).Routes()
|
||||
days := func() int {
|
||||
t.Helper()
|
||||
r, err := db.GetRemote(context.Background(), "q-days")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return r.QuarantineDays
|
||||
}
|
||||
const body = `"package_type":"npm","base_url":"https://example.com","quarantine_enabled":true`
|
||||
for i, s := range []struct {
|
||||
method, path, json string
|
||||
want int
|
||||
}{
|
||||
{"POST", "/", `{"name":"q-days",` + body + `}`, 3},
|
||||
{"PUT", "/q-days", `{` + body + `,"quarantine_days":14}`, 14},
|
||||
{"PUT", "/q-days", `{` + body + `}`, 14},
|
||||
} {
|
||||
if c := do(t, h, s.method, s.path, s.json); c/100 != 2 {
|
||||
t.Fatalf("step %d = %d", i, c)
|
||||
}
|
||||
if got := days(); got != s.want {
|
||||
t.Errorf("step %d: quarantine_days = %d, want %d", i, got, s.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user