Skip to content

Commit 75ff85f

Browse files
authored
Add cooldown support for Conda (#68)
* Add cooldown support for Conda Filter entries from Conda repodata.json based on the timestamp field (milliseconds since epoch). Filters both packages and packages.conda sections. When cooldown is disabled, repodata requests are proxied directly without parsing. * Update README table to mark Conda cooldown support
1 parent 70fe686 commit 75ff85f

4 files changed

Lines changed: 371 additions & 4 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Currently works with npm, PyPI, pub.dev, Composer, and Cargo, which all include
3737
| NuGet | .NET | Yes | ✓ |
3838
| Composer | PHP | Yes | ✓ |
3939
| Conan | C/C++ | | ✓ |
40-
| Conda | Python/R | | ✓ |
40+
| Conda | Python/R | Yes | ✓ |
4141
| CRAN | R | | ✓ |
4242
| Container | Docker/OCI | | ✓ |
4343
| Debian | Debian/Ubuntu | | ✓ |

docs/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ Durations support days (`7d`), hours (`48h`), and minutes (`30m`). Set to `0` to
209209

210210
Resolution order: package override, then ecosystem override, then global default. This lets you set a conservative default while exempting trusted packages.
211211

212-
Currently supported for npm, PyPI, pub.dev, Composer, Cargo, and NuGet. These ecosystems include publish timestamps in their metadata.
212+
Currently supported for npm, PyPI, pub.dev, Composer, Cargo, NuGet, and Conda. These ecosystems include publish timestamps in their metadata.
213213

214214
## Docker
215215

internal/handler/conda.go

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package handler
22

33
import (
4+
"encoding/json"
5+
"io"
46
"net/http"
57
"strings"
8+
"time"
9+
10+
"github.com/git-pkgs/purl"
611
)
712

813
const (
@@ -31,9 +36,9 @@ func (h *CondaHandler) Routes() http.Handler {
3136
mux := http.NewServeMux()
3237

3338
// Channel index (repodata)
34-
mux.HandleFunc("GET /{channel}/{arch}/repodata.json", h.proxyUpstream)
39+
mux.HandleFunc("GET /{channel}/{arch}/repodata.json", h.handleRepodata)
3540
mux.HandleFunc("GET /{channel}/{arch}/repodata.json.bz2", h.proxyUpstream)
36-
mux.HandleFunc("GET /{channel}/{arch}/current_repodata.json", h.proxyUpstream)
41+
mux.HandleFunc("GET /{channel}/{arch}/current_repodata.json", h.handleRepodata)
3742

3843
// Package downloads (cache these)
3944
mux.HandleFunc("GET /{channel}/{arch}/{filename}", h.handleDownload)
@@ -119,6 +124,114 @@ func (h *CondaHandler) parseFilename(filename string) (name, version string) {
119124
return name, version
120125
}
121126

127+
// handleRepodata proxies repodata.json, applying cooldown filtering when enabled.
128+
func (h *CondaHandler) handleRepodata(w http.ResponseWriter, r *http.Request) {
129+
if h.proxy.Cooldown == nil || !h.proxy.Cooldown.Enabled() {
130+
h.proxyUpstream(w, r)
131+
return
132+
}
133+
134+
upstreamURL := h.upstreamURL + r.URL.Path
135+
136+
h.proxy.Logger.Debug("fetching repodata for cooldown filtering", "url", upstreamURL)
137+
138+
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, upstreamURL, nil)
139+
if err != nil {
140+
http.Error(w, "failed to create request", http.StatusInternalServerError)
141+
return
142+
}
143+
req.Header.Set("Accept-Encoding", "gzip")
144+
145+
resp, err := h.proxy.HTTPClient.Do(req)
146+
if err != nil {
147+
h.proxy.Logger.Error("upstream request failed", "error", err)
148+
http.Error(w, "upstream request failed", http.StatusBadGateway)
149+
return
150+
}
151+
defer func() { _ = resp.Body.Close() }()
152+
153+
if resp.StatusCode != http.StatusOK {
154+
for k, vv := range resp.Header {
155+
for _, v := range vv {
156+
w.Header().Add(k, v)
157+
}
158+
}
159+
w.WriteHeader(resp.StatusCode)
160+
_, _ = io.Copy(w, resp.Body)
161+
return
162+
}
163+
164+
body, err := ReadMetadata(resp.Body)
165+
if err != nil {
166+
http.Error(w, "failed to read response", http.StatusInternalServerError)
167+
return
168+
}
169+
170+
filtered, err := h.applyCooldownFiltering(body)
171+
if err != nil {
172+
h.proxy.Logger.Warn("failed to filter repodata, proxying original", "error", err)
173+
w.Header().Set("Content-Type", "application/json")
174+
_, _ = w.Write(body)
175+
return
176+
}
177+
178+
w.Header().Set("Content-Type", "application/json")
179+
_, _ = w.Write(filtered)
180+
}
181+
182+
// condaTimestampDivisor converts Conda's millisecond timestamps to seconds.
183+
const condaTimestampDivisor = 1000
184+
185+
// applyCooldownFiltering removes entries from repodata.json that were
186+
// published too recently based on their timestamp field.
187+
func (h *CondaHandler) applyCooldownFiltering(body []byte) ([]byte, error) {
188+
if h.proxy.Cooldown == nil || !h.proxy.Cooldown.Enabled() {
189+
return body, nil
190+
}
191+
192+
var repodata map[string]any
193+
if err := json.Unmarshal(body, &repodata); err != nil {
194+
return nil, err
195+
}
196+
197+
for _, key := range []string{"packages", "packages.conda"} {
198+
packages, ok := repodata[key].(map[string]any)
199+
if !ok {
200+
continue
201+
}
202+
203+
for filename, entry := range packages {
204+
entryMap, ok := entry.(map[string]any)
205+
if !ok {
206+
continue
207+
}
208+
209+
ts, ok := entryMap["timestamp"].(float64)
210+
if !ok || ts == 0 {
211+
continue
212+
}
213+
214+
publishedAt := time.Unix(int64(ts)/condaTimestampDivisor, 0)
215+
216+
name, _ := entryMap["name"].(string)
217+
if name == "" {
218+
continue
219+
}
220+
221+
packagePURL := purl.MakePURLString("conda", name, "")
222+
223+
if !h.proxy.Cooldown.IsAllowed("conda", packagePURL, publishedAt) {
224+
version, _ := entryMap["version"].(string)
225+
h.proxy.Logger.Info("cooldown: filtering conda package",
226+
"name", name, "version", version, "filename", filename)
227+
delete(packages, filename)
228+
}
229+
}
230+
}
231+
232+
return json.Marshal(repodata)
233+
}
234+
122235
// proxyUpstream forwards a request to Anaconda without caching.
123236
func (h *CondaHandler) proxyUpstream(w http.ResponseWriter, r *http.Request) {
124237
h.proxy.ProxyUpstream(w, r, h.upstreamURL+r.URL.Path, []string{"Accept-Encoding"})

0 commit comments

Comments
 (0)