Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions openapi/Swarm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,7 @@ paths:
$ref: "SwarmCommon.yaml#/components/schemas/SwarmReference"
required: true
description: Swarm address of content
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmFeedLegacyResolve"
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmCache"
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyStrategyParameter"
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyFallbackModeParameter"
Expand Down Expand Up @@ -503,6 +504,7 @@ paths:
type: string
required: true
description: Path to the file in the collection.
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmFeedLegacyResolve"
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyStrategyParameter"
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyFallbackModeParameter"
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmChunkRetrievalTimeoutParameter"
Expand Down Expand Up @@ -1036,6 +1038,7 @@ paths:
$ref: "SwarmCommon.yaml#/components/schemas/FeedType"
required: false
description: "Feed indexing scheme (default: sequence)"
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmFeedLegacyResolve"
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmOnlyRootChunkParameter"
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmCache"
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyStrategyParameter"
Expand Down
8 changes: 8 additions & 0 deletions openapi/SwarmCommon.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1280,6 +1280,14 @@ components:
required: false
description: "ACT history Unix timestamp"

SwarmFeedLegacyResolve:
in: query
name: swarm-feed-legacy-resolve
schema:
type: boolean
required: false
description: "Resolves feed payloads in legacy structure (timestamp, content address)."

responses:
"200":
description: OK.
Expand Down
36 changes: 32 additions & 4 deletions pkg/api/bzz.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,15 @@ func (s *Service) bzzDownloadHandler(w http.ResponseWriter, r *http.Request) {
paths.Path = strings.TrimRight(paths.Path, "/") + "/" // NOTE: leave one slash if there was some.
}

s.serveReference(logger, address, paths.Path, w, r, false)
queries := struct {
FeedLegacyResolve bool `map:"swarm-feed-legacy-resolve"`
}{}
if response := s.mapStructure(r.URL.Query(), &queries); response != nil {
response("invalid query params", logger, w)
return
}

s.serveReference(logger, address, paths.Path, w, r, false, queries.FeedLegacyResolve)
}

func (s *Service) bzzHeadHandler(w http.ResponseWriter, r *http.Request) {
Expand All @@ -352,6 +360,14 @@ func (s *Service) bzzHeadHandler(w http.ResponseWriter, r *http.Request) {
return
}

queries := struct {
FeedLegacyResolve bool `map:"swarm-feed-legacy-resolve"`
}{}
if response := s.mapStructure(r.URL.Query(), &queries); response != nil {
response("invalid query params", logger, w)
return
}

address := paths.Address
if v := getAddressFromContext(r.Context()); !v.IsZero() {
address = v
Expand All @@ -361,10 +377,10 @@ func (s *Service) bzzHeadHandler(w http.ResponseWriter, r *http.Request) {
paths.Path = strings.TrimRight(paths.Path, "/") + "/" // NOTE: leave one slash if there was some.
}

s.serveReference(logger, address, paths.Path, w, r, true)
s.serveReference(logger, address, paths.Path, w, r, true, queries.FeedLegacyResolve)
}

func (s *Service) serveReference(logger log.Logger, address swarm.Address, pathVar string, w http.ResponseWriter, r *http.Request, headerOnly bool) {
func (s *Service) serveReference(logger log.Logger, address swarm.Address, pathVar string, w http.ResponseWriter, r *http.Request, headerOnly bool, feedLegacyResolve bool) {
loggerV1 := logger.V(1).Build()

headers := struct {
Expand Down Expand Up @@ -433,8 +449,20 @@ FETCH:
jsonhttp.NotFound(w, "no update found")
return
}
wc, err := feeds.GetWrappedChunk(ctx, s.storer.Download(cache), ch)
wc, err := feeds.GetWrappedChunk(ctx, s.storer.Download(cache), ch, feedLegacyResolve)
if err != nil {
if errors.Is(err, feeds.ErrNotLegacyPayload) {
logger.Debug("bzz: download: feed is not a legacy payload")
logger.Error(err, "bzz download: feed is not a legacy payload")
jsonhttp.BadRequest(w, "bzz download: feed is not a legacy payload")
return
}
if errors.As(err, &feeds.WrappedChunkNotFoundError{}) {
logger.Debug("bzz download: feed pointing to the wrapped chunk not found", "error", err)
logger.Error(err, "bzz download: feed pointing to the wrapped chunk not found")
jsonhttp.NotFound(w, "bzz download: feed pointing to the wrapped chunk not found")
return
}
logger.Debug("bzz download: mapStructure feed update failed", "error", err)
logger.Error(nil, "bzz download: mapStructure feed update failed")
jsonhttp.InternalServerError(w, "mapStructure feed update")
Expand Down
106 changes: 75 additions & 31 deletions pkg/api/bzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"mime"
"mime/multipart"
"net/http"
"net/url"
"strconv"
"strings"
"testing"
Expand All @@ -26,6 +27,7 @@ import (
"github.com/ethersphere/bee/v2/pkg/manifest"
mockbatchstore "github.com/ethersphere/bee/v2/pkg/postage/batchstore/mock"
mockpost "github.com/ethersphere/bee/v2/pkg/postage/mock"
testingsoc "github.com/ethersphere/bee/v2/pkg/soc/testing"
"github.com/ethersphere/bee/v2/pkg/storage/inmemchunkstore"
mockstorer "github.com/ethersphere/bee/v2/pkg/storer/mock"
"github.com/ethersphere/bee/v2/pkg/swarm"
Expand Down Expand Up @@ -759,11 +761,24 @@ func TestFeedIndirection(t *testing.T) {
updateData = []byte("<h1>Swarm Feeds Hello World!</h1>")
logger = log.Noop
storer = mockstorer.New()
ctx = context.Background()
client, _, _, _ = newTestServer(t, testServerOptions{
Storer: storer,
Logger: logger,
Post: mockpost.New(mockpost.WithAcceptAll()),
})
bzzDownloadResource = func(addr, path string, legacyFeed bool) string {
values := url.Values{}
if legacyFeed {
values.Set("swarm-feed-legacy-resolve", strconv.FormatBool(legacyFeed))
}

baseURL := "/bzz/" + addr + "/" + path
if len(values) > 0 {
return baseURL + "?" + values.Encode()
}
return baseURL
}
)
// tar all the test case files
tarReader := tarFiles(t, []f{
Expand Down Expand Up @@ -794,29 +809,6 @@ func TestFeedIndirection(t *testing.T) {
t.Fatalf("expected file reference, did not got any")
}

// now use the "content" to mock the feed lookup
// also, use the mocked mantaray chunks that unmarshal
// into a real manifest with the mocked feed values when
// called from the bzz endpoint. then call the bzz endpoint with
// the pregenerated feed root manifest hash

feedUpdate := toChunk(t, 121212, resp.Reference.Bytes())

var (
look = newMockLookup(-1, 0, feedUpdate, nil, &id{}, nil)
factory = newMockFactory(look)
bzzDownloadResource = func(addr, path string) string { return "/bzz/" + addr + "/" + path }
ctx = context.Background()
)
client, _, _, _ = newTestServer(t, testServerOptions{
Storer: storer,
Logger: logger,
Feeds: factory,
})
err := storer.Cache().Put(ctx, feedUpdate)
if err != nil {
t.Fatal(err)
}
m, err := manifest.NewDefaultManifest(
loadsave.New(storer.ChunkStore(), storer.Cache(), pipelineFactory(storer.Cache(), false, 0), redundancy.DefaultLevel),
false,
Expand All @@ -838,14 +830,66 @@ func TestFeedIndirection(t *testing.T) {
t.Fatal(err)
}

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"),
)
// now use the "content" root chunk to mock the feed lookup
// also, use the mocked mantaray chunks that unmarshal
// into a real manifest with the mocked feed values when
// called from the bzz endpoint. then call the bzz endpoint with
// the pregenerated feed root manifest hash

t.Run("legacy feed", func(t *testing.T) {
feedUpdate := toChunk(t, 121212, resp.Reference.Bytes())

var (
look = newMockLookup(-1, 0, feedUpdate, nil, &id{}, nil)
factory = newMockFactory(look)
)
client, _, _, _ = newTestServer(t, testServerOptions{
Storer: storer,
Logger: logger,
Feeds: factory,
})

jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), "", true), 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"),
)

jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), "", false), http.StatusNotFound)
})

t.Run("wrapped feed", func(t *testing.T) {
// get root chunk of data and wrap it in a feed
rootCh, err := storer.ChunkStore().Get(ctx, resp.Reference)
if err != nil {
t.Fatal(err)
}
socRootCh := testingsoc.GenerateMockSOC(t, rootCh.Data()[swarm.SpanSize:]).Chunk()

var (
look = newMockLookup(-1, 0, socRootCh, nil, &id{}, nil)
factory = newMockFactory(look)
)
client, _, _, _ = newTestServer(t, testServerOptions{
Storer: storer,
Logger: logger,
Feeds: factory,
})

jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), "", false), 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"),
)

jsonhttptest.Request(t, client, http.MethodGet, bzzDownloadResource(manifRef.String(), "", true), http.StatusBadRequest)
})
}

func Test_bzzDownloadHandler_invalidInputs(t *testing.T) {
Expand Down
7 changes: 4 additions & 3 deletions pkg/api/feed.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ func (s *Service) feedGetHandler(w http.ResponseWriter, r *http.Request) {
}

queries := struct {
At int64 `map:"at"`
After uint64 `map:"after"`
At int64 `map:"at"`
After uint64 `map:"after"`
FeedLegacyResolve bool `map:"swarm-feed-legacy-resolve"`
}{}
if response := s.mapStructure(r.URL.Query(), &queries); response != nil {
response("invalid query params", logger, w)
Expand Down Expand Up @@ -102,7 +103,7 @@ func (s *Service) feedGetHandler(w http.ResponseWriter, r *http.Request) {
return
}

wc, err := feeds.GetWrappedChunk(r.Context(), s.storer.Download(false), ch)
wc, err := feeds.GetWrappedChunk(r.Context(), s.storer.Download(false), ch, queries.FeedLegacyResolve)
if err != nil {
logger.Error(nil, "wrapped chunk cannot be retrieved")
jsonhttp.NotFound(w, "wrapped chunk cannot be retrieved")
Expand Down
47 changes: 37 additions & 10 deletions pkg/api/feed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"io"
"math/big"
"net/http"
"net/url"
"strconv"
"testing"

"github.com/ethersphere/bee/v2/pkg/api"
Expand Down Expand Up @@ -42,11 +44,20 @@ func TestFeed_Get(t *testing.T) {
t.Parallel()

var (
feedResource = func(owner, topic, at string) string {
feedResource = func(owner, topic, at string, legacyFeed bool) string {
values := url.Values{}
if at != "" {
return fmt.Sprintf("/feeds/%s/%s?at=%s", owner, topic, at)
values.Set("at", at)
}
return fmt.Sprintf("/feeds/%s/%s", owner, topic)
if legacyFeed {
values.Set("swarm-feed-legacy-resolve", strconv.FormatBool(legacyFeed))
}

baseURL := fmt.Sprintf("/feeds/%s/%s", owner, topic)
if len(values) > 0 {
return baseURL + "?" + values.Encode()
}
return baseURL
}
mockStorer = mockstorer.New()
)
Expand Down Expand Up @@ -75,7 +86,7 @@ func TestFeed_Get(t *testing.T) {
})
)

jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "12"), http.StatusOK,
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "12", true), http.StatusOK,
jsonhttptest.WithExpectedResponse(mockWrappedCh.Data()[swarm.SpanSize:]),
jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedIndexHeader, hex.EncodeToString(idBytes)),
jsonhttptest.WithExpectedResponseHeader(api.AccessControlExposeHeaders, api.SwarmFeedIndexHeader),
Expand All @@ -86,7 +97,7 @@ func TestFeed_Get(t *testing.T) {
)
})

t.Run("latest", func(t *testing.T) {
t.Run("latest with legacy payload", func(t *testing.T) {
t.Parallel()

var (
Expand All @@ -102,7 +113,7 @@ func TestFeed_Get(t *testing.T) {
})
)

jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", ""), http.StatusOK,
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "", true), http.StatusOK,
jsonhttptest.WithExpectedResponse(mockWrappedCh.Data()[swarm.SpanSize:]),
jsonhttptest.WithExpectedContentLength(len(mockWrappedCh.Data()[swarm.SpanSize:])),
jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedIndexHeader, hex.EncodeToString(idBytes)),
Expand Down Expand Up @@ -131,7 +142,7 @@ func TestFeed_Get(t *testing.T) {
})
)

jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", ""), http.StatusOK,
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "", false), http.StatusOK,
jsonhttptest.WithExpectedResponse(testData),
jsonhttptest.WithExpectedContentLength(len(testData)),
jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedIndexHeader, hex.EncodeToString(idBytes)),
Expand Down Expand Up @@ -161,7 +172,23 @@ func TestFeed_Get(t *testing.T) {
})
)

jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", ""), http.StatusNotFound)
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "", true), http.StatusNotFound)
})

t.Run("query parameter legacy feed resolve", func(t *testing.T) {
t.Parallel()

var (
look = newMockLookup(1, 0, nil, errors.New("dummy"), &id{}, &id{})
factory = newMockFactory(look)
client, _, _, _ = newTestServer(t, testServerOptions{
Storer: mockStorer,
Feeds: factory,
})
)

// Test with the legacyFeed parameter set to true which should add the query parameter
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "", false), http.StatusNotFound)
})

t.Run("bigger payload than one chunk", func(t *testing.T) {
Expand Down Expand Up @@ -193,7 +220,7 @@ func TestFeed_Get(t *testing.T) {
)

t.Run("retrieve chunk tree", func(t *testing.T) {
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", ""), http.StatusOK,
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "", false), http.StatusOK,
jsonhttptest.WithExpectedResponse(testData),
jsonhttptest.WithExpectedContentLength(testDataLen),
jsonhttptest.WithExpectedResponseHeader(api.SwarmFeedIndexHeader, hex.EncodeToString(idBytes)),
Expand All @@ -206,7 +233,7 @@ func TestFeed_Get(t *testing.T) {
})

t.Run("retrieve only wrapped chunk", func(t *testing.T) {
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", ""), http.StatusOK,
jsonhttptest.Request(t, client, http.MethodGet, feedResource(ownerString, "aabbcc", "", false), http.StatusOK,
jsonhttptest.WithRequestHeader(api.SwarmOnlyRootChunk, "true"),
jsonhttptest.WithExpectedResponse(testRootCh.Data()),
jsonhttptest.WithExpectedContentLength(len(testRootCh.Data())),
Expand Down
10 changes: 9 additions & 1 deletion pkg/api/subdomain.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,13 @@ func (s *Service) subdomainHandler(w http.ResponseWriter, r *http.Request) {
paths.Path = strings.TrimRight(paths.Path, "/") + "/" // NOTE: leave one slash if there was some.
}

s.serveReference(logger, paths.Subdomain, paths.Path, w, r, false)
queries := struct {
FeedLegacyResolve bool `map:"swarm-feed-legacy-resolve"`
}{}
if response := s.mapStructure(r.URL.Query(), &queries); response != nil {
response("invalid query params", logger, w)
return
}

s.serveReference(logger, paths.Subdomain, paths.Path, w, r, false, queries.FeedLegacyResolve)
}
Loading
Loading