From 26b45c98bf3bc319ff0a6fc59250b33524871538 Mon Sep 17 00:00:00 2001 From: sky22333 Date: Sat, 11 Jul 2026 20:56:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=89=8D=E7=AB=AF=E7=9A=84?= =?UTF-8?q?=E6=9F=90=E4=BA=9B=E5=B0=8Fbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/handlers/imagetar.go | 28 ++++++------ src/handlers/imagetar_test.go | 20 ++++++++ src/main_test.go | 7 ++- src/public/images.html | 23 ++++------ src/public/index.html | 3 +- src/public/search.html | 86 +++++++++++++++++++++-------------- 6 files changed, 103 insertions(+), 64 deletions(-) diff --git a/src/handlers/imagetar.go b/src/handlers/imagetar.go index a618a05..776d06f 100644 --- a/src/handlers/imagetar.go +++ b/src/handlers/imagetar.go @@ -709,22 +709,26 @@ func formatPlatformText(platform string) string { func InitImageTarRoutes(router *gin.Engine) { imageAPI := router.Group("/api/image") { - imageAPI.GET("/download/:image", handleDirectImageDownload) - imageAPI.GET("/info/:image", handleImageInfo) + imageAPI.GET("/download", handleDirectImageDownload) + imageAPI.GET("/info", handleImageInfo) imageAPI.GET("/batch", handleSimpleBatchDownload) imageAPI.POST("/batch", handleSimpleBatchDownload) } } +// resolveImageRef 从 query image 读取镜像引用,避免 path 段用 _ 代替 / 导致下划线歧义。 +func resolveImageRef(c *gin.Context) string { + return strings.TrimSpace(c.Query("image")) +} + // handleDirectImageDownload 处理单镜像下载 func handleDirectImageDownload(c *gin.Context) { - imageParam := c.Param("image") - if imageParam == "" { + imageRef := resolveImageRef(c) + if imageRef == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "缺少镜像参数"}) return } - imageRef := strings.ReplaceAll(imageParam, "_", "/") platform := c.Query("platform") tag := c.DefaultQuery("tag", "") useCompressed := c.DefaultQuery("compressed", "true") == "true" @@ -767,11 +771,10 @@ func handleDirectImageDownload(c *gin.Context) { return } - downloadURL := fmt.Sprintf("/api/image/download/%s?token=%s", imageParam, token) - if tag != "" { - downloadURL = downloadURL + "&tag=" + url.QueryEscape(tag) - } - c.JSON(http.StatusOK, gin.H{"download_url": downloadURL}) + q := url.Values{} + q.Set("image", imageRef) + q.Set("token", token) + c.JSON(http.StatusOK, gin.H{"download_url": "/api/image/download?" + q.Encode()}) return } @@ -931,13 +934,12 @@ func handleSimpleBatchDownload(c *gin.Context) { // handleImageInfo 处理镜像信息查询 func handleImageInfo(c *gin.Context) { - imageParam := c.Param("image") - if imageParam == "" { + imageRef := resolveImageRef(c) + if imageRef == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "缺少镜像参数"}) return } - imageRef := strings.ReplaceAll(imageParam, "_", "/") tag := c.DefaultQuery("tag", "latest") if !strings.Contains(imageRef, ":") && !strings.Contains(imageRef, "@") { diff --git a/src/handlers/imagetar_test.go b/src/handlers/imagetar_test.go index 8aa0e5e..c84342a 100644 --- a/src/handlers/imagetar_test.go +++ b/src/handlers/imagetar_test.go @@ -65,6 +65,26 @@ func TestGenerateContentFingerprintStable(t *testing.T) { } } +func TestResolveImageRef(t *testing.T) { + gin.SetMode(gin.TestMode) + + t.Run("query preserves underscores", func(t *testing.T) { + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + c.Request = httptest.NewRequest(http.MethodGet, "/api/image/download?image=user/my_app:v1", nil) + if got := resolveImageRef(c); got != "user/my_app:v1" { + t.Fatalf("got %q", got) + } + }) + + t.Run("missing image is empty", func(t *testing.T) { + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + c.Request = httptest.NewRequest(http.MethodGet, "/api/image/download", nil) + if got := resolveImageRef(c); got != "" { + t.Fatalf("got %q", got) + } + }) +} + func TestWriteDownloadErrorSkipsJSONAfterBodyStarted(t *testing.T) { gin.SetMode(gin.TestMode) diff --git a/src/main_test.go b/src/main_test.go index 0d3df0f..378a19c 100644 --- a/src/main_test.go +++ b/src/main_test.go @@ -82,7 +82,7 @@ enableFrontend = false func TestSingleImageDownloadPrepareReturnsURL(t *testing.T) { router := newTestRouter(t, "") - w := performRequest(router, http.MethodGet, "/api/image/download/nginx?mode=prepare", "") + w := performRequest(router, http.MethodGet, "/api/image/download?image=nginx&mode=prepare", "") if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body=%s", w.Code, w.Body.String()) } @@ -93,7 +93,10 @@ func TestSingleImageDownloadPrepareReturnsURL(t *testing.T) { if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { t.Fatal(err) } - if !strings.HasPrefix(got.DownloadURL, "/api/image/download/nginx?token=") { + if !strings.Contains(got.DownloadURL, "image=nginx") || !strings.Contains(got.DownloadURL, "token=") { + t.Fatalf("download_url = %q", got.DownloadURL) + } + if !strings.HasPrefix(got.DownloadURL, "/api/image/download?") { t.Fatalf("download_url = %q", got.DownloadURL) } } diff --git a/src/public/images.html b/src/public/images.html index a01e992..645f2a1 100644 --- a/src/public/images.html +++ b/src/public/images.html @@ -555,7 +555,6 @@ 🚀 GitHub加速 🐳 离线镜像下载 🔍 镜像搜索 - 📄 Hosts + ${escapeHtml(pullCommand)} +
- +
`; @@ -1345,30 +1359,36 @@ function renderTagsBatch(tags, fullRepoName, container, replaceContent = false) { const tagsHtml = tags.map(tag => { const vulnIndicators = Object.entries(tag.vulnerabilities || {}) - .map(([level, count]) => count > 0 ? `` : '') + .map(([level, count]) => { + if (!(count > 0)) return ''; + const safeLevel = String(level).toLowerCase().replace(/[^a-z0-9_-]/g, ''); + if (!safeLevel) return ''; + return ``; + }) .join(''); const images = tag.images || []; const architectures = images.map(img => { - const arch = `${img.os}/${img.architecture}${img.variant ? '/' + img.variant : ''}`; + const arch = `${img.os || ''}/${img.architecture || ''}${img.variant ? '/' + img.variant : ''}`; const size = formatUtils.formatSize(img.size); - return `
${arch}
`; + return `
${escapeHtml(arch)}
`; }).join(''); + const pullCommand = `docker pull ${fullRepoName}:${tag.name || ''}`; return `
- ${tag.name} + ${escapeHtml(tag.name)} ${vulnIndicators ? `
${vulnIndicators}
` : ''}
- 最后更新: ${formatUtils.formatTimeAgo(tag.last_updated)} - ${tag.last_pusher ? `由 ${tag.last_pusher} 推送` : ''} - ${tag.full_size ? `大小: ${formatUtils.formatSize(tag.full_size)}` : ''} + 最后更新: ${escapeHtml(formatUtils.formatTimeAgo(tag.last_updated))} + ${tag.last_pusher ? `由 ${escapeHtml(tag.last_pusher)} 推送` : ''} + ${tag.full_size ? `大小: ${escapeHtml(formatUtils.formatSize(tag.full_size))}` : ''}
- docker pull ${fullRepoName}:${tag.name} - + ${escapeHtml(pullCommand)} +
${architectures ? `
${architectures}
` : ''}