diff --git a/internal/router/static_router.go b/internal/router/static_router.go index e16002cfc..290148b62 100644 --- a/internal/router/static_router.go +++ b/internal/router/static_router.go @@ -68,7 +68,8 @@ func (a *StaticRouter) RegisterStaticRouter(r *gin.RouterGroup) { } func attachmentFileLocalPath(uploadPath, requestPath, originalFilename string) (string, bool) { - realFilename := strings.TrimSuffix(requestPath, "/"+originalFilename) + filepath.Ext(originalFilename) + // The uploader saves attachments with a lowercased extension, so match it here. + realFilename := strings.TrimSuffix(requestPath, "/"+originalFilename) + strings.ToLower(filepath.Ext(originalFilename)) attachmentRoot := filepath.Join(uploadPath, constant.FilesPostSubPath) fileLocalPath := filepath.Join(attachmentRoot, realFilename) relPath, err := filepath.Rel(attachmentRoot, fileLocalPath) diff --git a/internal/router/static_router_test.go b/internal/router/static_router_test.go index b123c95db..21a83e0ad 100644 --- a/internal/router/static_router_test.go +++ b/internal/router/static_router_test.go @@ -20,10 +20,15 @@ package router import ( + "net/http" + "net/http/httptest" + "os" "path/filepath" "testing" "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/service/service_config" + "github.com/gin-gonic/gin" ) func TestAttachmentFileLocalPathRejectsTraversal(t *testing.T) { @@ -44,3 +49,43 @@ func TestAttachmentFileLocalPathRejectsTraversal(t *testing.T) { } } } + +// The uploader stores attachments under a lowercased extension (Report.PDF is +// saved as hash.pdf), so the download route must look the file up the same way. +func TestAttachmentFileLocalPathLowercasesExtension(t *testing.T) { + uploadPath := t.TempDir() + + filePath, ok := attachmentFileLocalPath(uploadPath, "/hash/Report.PDF", "Report.PDF") + if !ok { + t.Fatal("valid attachment path was rejected") + } + want := filepath.Join(uploadPath, constant.FilesPostSubPath, "hash.pdf") + if filePath != want { + t.Fatalf("attachment path = %q, want %q", filePath, want) + } +} + +func TestAttachmentDownloadWithUppercaseExtension(t *testing.T) { + gin.SetMode(gin.TestMode) + uploadPath := t.TempDir() + attachmentDir := filepath.Join(uploadPath, constant.FilesPostSubPath) + if err := os.MkdirAll(attachmentDir, 0o755); err != nil { + t.Fatal(err) + } + // This is the name UploadPostAttachment gives an upload called Report.PDF. + if err := os.WriteFile(filepath.Join(attachmentDir, "hash.pdf"), []byte("%PDF-1.4"), 0o644); err != nil { + t.Fatal(err) + } + + r := gin.New() + NewStaticRouter(&service_config.ServiceConfig{UploadPath: uploadPath}).RegisterStaticRouter(&r.RouterGroup) + + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/uploads/files/post/hash/Report.PDF", nil)) + if w.Code != http.StatusOK { + t.Fatalf("status = %d (location %q), want %d", w.Code, w.Header().Get("Location"), http.StatusOK) + } + if got := w.Body.String(); got != "%PDF-1.4" { + t.Fatalf("body = %q, want the stored attachment", got) + } +}