diff --git a/pkg/api/api.go b/pkg/api/api.go index b226a5cff24..abb8312a6cc 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -98,13 +98,14 @@ const ( GasLimitHeader = "Gas-Limit" ETagHeader = "ETag" - AuthorizationHeader = "Authorization" - AcceptEncodingHeader = "Accept-Encoding" - ContentTypeHeader = "Content-Type" - ContentDispositionHeader = "Content-Disposition" - ContentLengthHeader = "Content-Length" - RangeHeader = "Range" - OriginHeader = "Origin" + AuthorizationHeader = "Authorization" + AcceptEncodingHeader = "Accept-Encoding" + ContentTypeHeader = "Content-Type" + ContentDispositionHeader = "Content-Disposition" + ContentLengthHeader = "Content-Length" + RangeHeader = "Range" + OriginHeader = "Origin" + AccessControlExposeHeaders = "Access-Control-Expose-Headers" ) const ( diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index 63f366615f3..986bc663d3e 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -12,7 +12,6 @@ import ( "encoding/hex" "encoding/json" "errors" - "io" "math/big" "net" "net/http" @@ -294,23 +293,6 @@ func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket. return httpClient, conn, ts.Listener.Addr().String(), chanStore } -func request(t *testing.T, client *http.Client, method, resource string, body io.Reader, responseCode int) *http.Response { - t.Helper() - - req, err := http.NewRequestWithContext(context.TODO(), method, resource, body) - if err != nil { - t.Fatal(err) - } - resp, err := client.Do(req) - if err != nil { - t.Fatal(err) - } - if resp.StatusCode != responseCode { - t.Fatalf("got response status %s, want %v %s", resp.Status, responseCode, http.StatusText(responseCode)) - } - return resp -} - func pipelineFactory(s storage.Putter, encrypt bool, rLevel redundancy.Level) func() pipeline.Interface { return func() pipeline.Interface { return builder.NewPipelineBuilder(context.Background(), s, encrypt, rLevel) diff --git a/pkg/api/bytes.go b/pkg/api/bytes.go index d0d2886683b..45692f5c4d3 100644 --- a/pkg/api/bytes.go +++ b/pkg/api/bytes.go @@ -155,7 +155,7 @@ func (s *Service) bytesUploadHandler(w http.ResponseWriter, r *http.Request) { span.LogFields(olog.Bool("success", true)) - w.Header().Set("Access-Control-Expose-Headers", SwarmTagHeader) + w.Header().Set(AccessControlExposeHeaders, SwarmTagHeader) jsonhttp.Created(w, bytesPostResponse{ Reference: encryptedReference, }) @@ -210,7 +210,7 @@ func (s *Service) bytesHeadHandler(w http.ResponseWriter, r *http.Request) { return } - w.Header().Add("Access-Control-Expose-Headers", "Accept-Ranges, Content-Encoding") + w.Header().Add(AccessControlExposeHeaders, "Accept-Ranges, Content-Encoding") w.Header().Add(ContentTypeHeader, "application/octet-stream") var span int64 diff --git a/pkg/api/bytes_test.go b/pkg/api/bytes_test.go index f03fa8b973e..40280e44992 100644 --- a/pkg/api/bytes_test.go +++ b/pkg/api/bytes_test.go @@ -113,6 +113,8 @@ func TestBytes(t *testing.T) { jsonhttptest.Request(t, client, http.MethodGet, resource+"/"+expHash, http.StatusOK, jsonhttptest.WithExpectedContentLength(len(content)), jsonhttptest.WithExpectedResponse(content), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.ContentDispositionHeader), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "application/octet-stream"), ) }) diff --git a/pkg/api/bzz.go b/pkg/api/bzz.go index 14a236c183f..9acf227c6d4 100644 --- a/pkg/api/bzz.go +++ b/pkg/api/bzz.go @@ -304,7 +304,7 @@ func (s *Service) fileUploadHandler( span.SetTag("tagID", tagID) } w.Header().Set(ETagHeader, fmt.Sprintf("%q", reference.String())) - w.Header().Set("Access-Control-Expose-Headers", SwarmTagHeader) + w.Header().Set(AccessControlExposeHeaders, SwarmTagHeader) jsonhttp.Created(w, bzzUploadResponse{ Reference: reference, @@ -412,7 +412,7 @@ FETCH: // go on normally. if !feedDereferenced { if l, err := s.manifestFeed(ctx, m); err == nil { - //we have a feed manifest here + // we have a feed manifest here ch, cur, _, err := l.At(ctx, time.Now().Unix(), 0) if err != nil { logger.Debug("bzz download: feed lookup failed", "error", err) @@ -451,7 +451,7 @@ FETCH: // we should implement an append functionality for this specific header, // since different parts of handlers might be overriding others' values // resulting in inconsistent headers in the response. - w.Header().Set("Access-Control-Expose-Headers", SwarmFeedIndexHeader) + w.Header().Set(AccessControlExposeHeaders, SwarmFeedIndexHeader) goto FETCH } } @@ -551,8 +551,7 @@ func (s *Service) serveManifestEntry( mtdt := manifestEntry.Metadata() if fname, ok := mtdt[manifest.EntryMetadataFilenameKey]; ok { fname = filepath.Base(fname) // only keep the file name - additionalHeaders[ContentDispositionHeader] = - []string{fmt.Sprintf("inline; filename=\"%s\"", fname)} + additionalHeaders[ContentDispositionHeader] = []string{fmt.Sprintf("inline; filename=\"%s\"", escapeQuotes(fname))} } if mimeType, ok := mtdt[manifest.EntryMetadataContentTypeKey]; ok { additionalHeaders[ContentTypeHeader] = []string{mimeType} @@ -616,13 +615,15 @@ func (s *Service) downloadHandler(logger log.Logger, w http.ResponseWriter, r *h // include additional headers for name, values := range additionalHeaders { - w.Header().Set(name, strings.Join(values, "; ")) + for _, value := range values { + w.Header().Add(name, value) + } } if etag { w.Header().Set(ETagHeader, fmt.Sprintf("%q", reference)) } w.Header().Set(ContentLengthHeader, strconv.FormatInt(l, 10)) - w.Header().Set("Access-Control-Expose-Headers", ContentDispositionHeader) + w.Header().Add(AccessControlExposeHeaders, ContentDispositionHeader) if headersOnly { w.WriteHeader(http.StatusOK) diff --git a/pkg/api/bzz_test.go b/pkg/api/bzz_test.go index 246ed106778..55936d52568 100644 --- a/pkg/api/bzz_test.go +++ b/pkg/api/bzz_test.go @@ -841,6 +841,10 @@ func TestFeedIndirection(t *testing.T) { jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), ""), http.StatusOK, jsonhttptest.WithExpectedResponse(updateData), jsonhttptest.WithExpectedContentLength(len(updateData)), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexHeader), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.ContentDispositionHeader), + jsonhttptest.WithExpectedResponseHeader(api.ContentDispositionHeader, `inline; filename="index.html"`), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), ) } @@ -1090,3 +1094,53 @@ func TestDirectUploadBzz(t *testing.T) { }), ) } + +func TestBzzDownloadHeaders(t *testing.T) { + t.Parallel() + var ( + data = []byte("

Swarm Hello World!

") + logger = log.Noop + storer = mockstorer.New() + testServer, _, _, _ = newTestServer(t, testServerOptions{ + Storer: storer, + Logger: logger, + Post: mockpost.New(mockpost.WithAcceptAll()), + }) + ) + // tar all the test case files + tarReader := tarFiles(t, []f{ + { + data: data, + name: "\"index.html\"", + dir: "", + filePath: "./index.html", + }, + }) + + var resp api.BzzUploadResponse + + options := []jsonhttptest.Option{ + jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"), + jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), + jsonhttptest.WithRequestBody(tarReader), + jsonhttptest.WithRequestHeader(api.ContentTypeHeader, api.ContentTypeTar), + jsonhttptest.WithRequestHeader(api.SwarmCollectionHeader, "True"), + jsonhttptest.WithUnmarshalJSONResponse(&resp), + jsonhttptest.WithRequestHeader(api.SwarmIndexDocumentHeader, "index.html"), + } + + // verify directory tar upload response + jsonhttptest.Request(t, testServer, http.MethodPost, "/bzz", http.StatusCreated, options...) + + if resp.Reference.String() == "" { + t.Fatalf("expected file reference, did not got any") + } + + jsonhttptest.Request(t, testServer, http.MethodGet, "/bzz/"+resp.Reference.String(), http.StatusOK, + jsonhttptest.WithExpectedResponse(data), + jsonhttptest.WithExpectedContentLength(len(data)), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.ContentDispositionHeader), + jsonhttptest.WithExpectedResponseHeader(api.ContentDispositionHeader, `inline; filename="index.html"`), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "text/html; charset=utf-8"), + ) +} diff --git a/pkg/api/chunk.go b/pkg/api/chunk.go index efce3349501..1cbed96fef1 100644 --- a/pkg/api/chunk.go +++ b/pkg/api/chunk.go @@ -216,7 +216,7 @@ func (s *Service) chunkUploadHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set(SwarmTagHeader, fmt.Sprint(tag)) } - w.Header().Set("Access-Control-Expose-Headers", SwarmTagHeader) + w.Header().Set(AccessControlExposeHeaders, SwarmTagHeader) jsonhttp.Created(w, chunkAddressResponse{Reference: reference}) } diff --git a/pkg/api/dirs.go b/pkg/api/dirs.go index 23be6929acc..72c5a37ee87 100644 --- a/pkg/api/dirs.go +++ b/pkg/api/dirs.go @@ -134,7 +134,7 @@ func (s *Service) dirUploadHandler( w.Header().Set(SwarmTagHeader, fmt.Sprint(tag)) span.LogFields(olog.Bool("success", true)) } - w.Header().Set("Access-Control-Expose-Headers", SwarmTagHeader) + w.Header().Set(AccessControlExposeHeaders, SwarmTagHeader) jsonhttp.Created(w, bzzUploadResponse{ Reference: encryptedReference, }) @@ -153,7 +153,6 @@ func storeDir( errorFilename string, rLevel redundancy.Level, ) (swarm.Address, error) { - logger := tracing.NewLoggerWithTraceID(ctx, log) loggerV1 := logger.V(1).Build() diff --git a/pkg/api/feed.go b/pkg/api/feed.go index e2ab2f0affa..9f1ab16bfcc 100644 --- a/pkg/api/feed.go +++ b/pkg/api/feed.go @@ -11,7 +11,6 @@ import ( "io" "net/http" "strconv" - "strings" "time" "github.com/ethereum/go-ethereum/common" @@ -134,18 +133,20 @@ func (s *Service) feedGetHandler(w http.ResponseWriter, r *http.Request) { sig := socCh.Signature() additionalHeaders := http.Header{ - ContentTypeHeader: {"application/octet-stream"}, - SwarmFeedIndexHeader: {hex.EncodeToString(curBytes)}, - SwarmFeedIndexNextHeader: {hex.EncodeToString(nextBytes)}, - SwarmSocSignatureHeader: {hex.EncodeToString(sig)}, - "Access-Control-Expose-Headers": {SwarmFeedIndexHeader, SwarmFeedIndexNextHeader, SwarmSocSignatureHeader}, + ContentTypeHeader: {"application/octet-stream"}, + SwarmFeedIndexHeader: {hex.EncodeToString(curBytes)}, + SwarmFeedIndexNextHeader: {hex.EncodeToString(nextBytes)}, + SwarmSocSignatureHeader: {hex.EncodeToString(sig)}, + AccessControlExposeHeaders: {SwarmFeedIndexHeader, SwarmFeedIndexNextHeader, SwarmSocSignatureHeader}, } if headers.OnlyRootChunk { w.Header().Set(ContentLengthHeader, strconv.Itoa(len(wc.Data()))) // include additional headers for name, values := range additionalHeaders { - w.Header().Set(name, strings.Join(values, ", ")) + for _, value := range values { + w.Header().Add(name, value) + } } _, _ = io.Copy(w, bytes.NewReader(wc.Data())) return diff --git a/pkg/api/feed_test.go b/pkg/api/feed_test.go index 843756d7237..0e2fa807ad4 100644 --- a/pkg/api/feed_test.go +++ b/pkg/api/feed_test.go @@ -77,6 +77,11 @@ func TestFeed_Get(t *testing.T) { jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "12"), http.StatusOK, jsonhttptest.WithExpectedResponse(mockWrappedCh.Data()[swarm.SpanSize:]), jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedIndexHeader, hex.EncodeToString(idBytes)), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexHeader), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexNextHeader), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmSocSignatureHeader), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.ContentDispositionHeader), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "application/octet-stream"), ) }) @@ -100,6 +105,11 @@ func TestFeed_Get(t *testing.T) { jsonhttptest.WithExpectedResponse(mockWrappedCh.Data()[swarm.SpanSize:]), jsonhttptest.WithExpectedContentLength(len(mockWrappedCh.Data()[swarm.SpanSize:])), jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedIndexHeader, hex.EncodeToString(idBytes)), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexHeader), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexNextHeader), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmSocSignatureHeader), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.ContentDispositionHeader), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "application/octet-stream"), ) }) @@ -124,6 +134,11 @@ func TestFeed_Get(t *testing.T) { jsonhttptest.WithExpectedResponse(testData), jsonhttptest.WithExpectedContentLength(len(testData)), jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedIndexHeader, hex.EncodeToString(idBytes)), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexHeader), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexNextHeader), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmSocSignatureHeader), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.ContentDispositionHeader), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "application/octet-stream"), ) }) @@ -181,6 +196,11 @@ func TestFeed_Get(t *testing.T) { jsonhttptest.WithExpectedResponse(testData), jsonhttptest.WithExpectedContentLength(testDataLen), jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedIndexHeader, hex.EncodeToString(idBytes)), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexHeader), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexNextHeader), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmSocSignatureHeader), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.ContentDispositionHeader), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "application/octet-stream"), ) }) @@ -190,6 +210,10 @@ func TestFeed_Get(t *testing.T) { jsonhttptest.WithExpectedResponse(testRootCh.Data()), jsonhttptest.WithExpectedContentLength(len(testRootCh.Data())), jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedIndexHeader, hex.EncodeToString(idBytes)), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexHeader), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexNextHeader), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmSocSignatureHeader), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "application/octet-stream"), ) }) }) @@ -267,7 +291,6 @@ func TestFeed_Post(t *testing.T) { ) }) }) - } // TestDirectUploadFeed tests that the direct upload endpoint give correct error message in dev mode diff --git a/pkg/api/soc.go b/pkg/api/soc.go index 9f0f838bfc9..54e9e4cc4d8 100644 --- a/pkg/api/soc.go +++ b/pkg/api/soc.go @@ -11,7 +11,6 @@ import ( "io" "net/http" "strconv" - "strings" "github.com/ethersphere/bee/v2/pkg/accesscontrol" "github.com/ethersphere/bee/v2/pkg/cac" @@ -257,16 +256,18 @@ func (s *Service) socGetHandler(w http.ResponseWriter, r *http.Request) { wc := socCh.WrappedChunk() additionalHeaders := http.Header{ - ContentTypeHeader: {"application/octet-stream"}, - SwarmSocSignatureHeader: {hex.EncodeToString(sig)}, - "Access-Control-Expose-Headers": {SwarmSocSignatureHeader}, + ContentTypeHeader: {"application/octet-stream"}, + SwarmSocSignatureHeader: {hex.EncodeToString(sig)}, + AccessControlExposeHeaders: {SwarmSocSignatureHeader}, } if headers.OnlyRootChunk { w.Header().Set(ContentLengthHeader, strconv.Itoa(len(wc.Data()))) // include additional headers for name, values := range additionalHeaders { - w.Header().Set(name, strings.Join(values, ", ")) + for _, value := range values { + w.Header().Add(name, value) + } } _, _ = io.Copy(w, bytes.NewReader(wc.Data())) return diff --git a/pkg/api/soc_test.go b/pkg/api/soc_test.go index 6c0d6fa0449..fb34eb82297 100644 --- a/pkg/api/soc_test.go +++ b/pkg/api/soc_test.go @@ -9,7 +9,6 @@ import ( "context" "encoding/hex" "fmt" - "io" "net/http" "testing" "time" @@ -99,28 +98,22 @@ func TestSOC(t *testing.T) { // try to fetch the same chunk t.Run("chunks fetch", func(t *testing.T) { rsrc := fmt.Sprintf("/chunks/%s", s.Address().String()) - resp := request(t, client, http.MethodGet, rsrc, nil, http.StatusOK) - data, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(s.Chunk().Data(), data) { - t.Fatal("data retrieved doesn't match uploaded content") - } + jsonhttptest.Request(t, client, http.MethodGet, rsrc, http.StatusOK, + jsonhttptest.WithExpectedResponse(s.Chunk().Data()), + jsonhttptest.WithExpectedContentLength(len(s.Chunk().Data())), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "binary/octet-stream"), + ) }) t.Run("soc fetch", func(t *testing.T) { rsrc := fmt.Sprintf("/soc/%s/%s", hex.EncodeToString(s.Owner), hex.EncodeToString(s.ID)) - resp := request(t, client, http.MethodGet, rsrc, nil, http.StatusOK) - data, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(s.WrappedChunk.Data()[swarm.SpanSize:], data) { - t.Fatal("data retrieved doesn't match uploaded content") - } + jsonhttptest.Request(t, client, http.MethodGet, rsrc, http.StatusOK, + jsonhttptest.WithExpectedResponse(s.WrappedChunk.Data()[swarm.SpanSize:]), + jsonhttptest.WithExpectedContentLength(len(s.WrappedChunk.Data()[swarm.SpanSize:])), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmSocSignatureHeader), + jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.ContentDispositionHeader), + jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "application/octet-stream"), + ) }) }) diff --git a/pkg/api/util.go b/pkg/api/util.go index a1ad148f6d5..6fdb575873a 100644 --- a/pkg/api/util.go +++ b/pkg/api/util.go @@ -347,3 +347,9 @@ func flattenValue(val reflect.Value) reflect.Value { } return val } + +var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") + +func escapeQuotes(s string) string { + return quoteEscaper.Replace(s) +}