Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: meta=eof for IPIP-431; ask for and expect (but not require) from http fetches #378

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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 cmd/lassie/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,9 @@ func defaultFetchRun(
blockCount,
humanize.IBytes(stats.Size),
)
if stats.CarProperties != nil {
fmt.Fprintf(msgWriter, "\tChecksum: %x\n", stats.CarProperties.ChecksumMultihash)
}

return nil
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ require (
github.com/ipfs/go-unixfsnode v1.7.1
github.com/ipld/go-car/v2 v2.10.1
github.com/ipld/go-codec-dagpb v1.6.0
github.com/ipld/go-ipld-prime v0.20.1-0.20230329011551-5056175565b0
github.com/ipld/go-ipld-prime v0.21.1-0.20230811030745-6e31cea491de
github.com/ipni/go-libipni v0.0.8-0.20230425184153-86a1fcb7f7ff
github.com/libp2p/go-libp2p v0.27.8
github.com/libp2p/go-libp2p-routing-helpers v0.7.0
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ=
github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
Expand Down Expand Up @@ -336,8 +336,8 @@ github.com/ipld/go-car/v2 v2.10.1 h1:MRDqkONNW9WRhB79u+Z3U5b+NoN7lYA5B8n8qI3+BoI
github.com/ipld/go-car/v2 v2.10.1/go.mod h1:sQEkXVM3csejlb1kCCb+vQ/pWBKX9QtvsrysMQjOgOg=
github.com/ipld/go-codec-dagpb v1.6.0 h1:9nYazfyu9B1p3NAgfVdpRco3Fs2nFC72DqVsMj6rOcc=
github.com/ipld/go-codec-dagpb v1.6.0/go.mod h1:ANzFhfP2uMJxRBr8CE+WQWs5UsNa0pYtmKZ+agnUw9s=
github.com/ipld/go-ipld-prime v0.20.1-0.20230329011551-5056175565b0 h1:iJTl9tx5DEsnKpppX5PmfdoQ3ITuBmkh3yyEpHWY2SI=
github.com/ipld/go-ipld-prime v0.20.1-0.20230329011551-5056175565b0/go.mod h1:wmOtdy70ajP48iZITH8uLsGJVMqA4EJM61/bSfYYGhs=
github.com/ipld/go-ipld-prime v0.21.1-0.20230811030745-6e31cea491de h1:N6Wfk6dvcBjF4AJJDSmti6CkgHWZPDZ0fuqSQL+kKnU=
github.com/ipld/go-ipld-prime v0.21.1-0.20230811030745-6e31cea491de/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd h1:gMlw/MhNr2Wtp5RwGdsW23cs+yCuj9k2ON7i9MiJlRo=
github.com/ipni/go-libipni v0.0.8-0.20230425184153-86a1fcb7f7ff h1:xbKrIvnpQkbF8iHPk/HGcegsypCDpcXWHhzBCLyCWf8=
github.com/ipni/go-libipni v0.0.8-0.20230425184153-86a1fcb7f7ff/go.mod h1:paYP9U4N3/vOzGCuN9kU972vtvw9JUcQjOKyiCFGwRk=
Expand Down Expand Up @@ -616,7 +616,7 @@ github.com/urfave/cli/v2 v2.24.4 h1:0gyJJEBYtCV87zI/x2nZCPyDxD51K6xM8SkwjHFCNEU=
github.com/urfave/cli/v2 v2.24.4/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/warpfork/go-testmark v0.11.0 h1:J6LnV8KpceDvo7spaNU4+DauH2n1x+6RaO2rJrmpQ9U=
github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsXyEHU0s=
github.com/warpfork/go-wish v0.0.0-20180510122957-5ad1f5abf436/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
github.com/warpfork/go-wish v0.0.0-20190328234359-8b3e70f8e830/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
Expand Down
19 changes: 19 additions & 0 deletions pkg/httputil/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package httputil

import "fmt"

const (
MimeTypeCar = "application/vnd.ipld.car" // The only accepted MIME type
MimeTypeCarVersion = "1" // We only accept version 1 of the MIME type
ResponseAcceptRangesHeader = "none" // We currently don't accept range requests
ResponseCacheControlHeader = "public, max-age=29030400, immutable" // Magic cache control values
FilenameExtCar = ".car" // The only valid filename extension
FormatParameterCar = "car" // The only valid format parameter value
DefaultIncludeDupes = true // The default value for an unspecified "dups" parameter. See https://github.com/ipfs/specs/pull/412.
)

var (
ResponseChunkDelimeter = []byte("0\r\n") // An http/1.1 chunk delimeter, used for specifying an early end to the response
ResponseContentTypeHeader = fmt.Sprintf("%s; version=%s; order=dfs; dups=y", MimeTypeCar, MimeTypeCarVersion)
RequestAcceptHeader = fmt.Sprintf("%s; version=%s; order=dfs; dups=y; meta=eof", MimeTypeCar, MimeTypeCarVersion)
)
70 changes: 70 additions & 0 deletions pkg/httputil/metadata/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package metadata

import (
"fmt"
"io"

"github.com/filecoin-project/lassie/pkg/types"
"github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime/codec/dagjson"
bindnoderegistry "github.com/ipld/go-ipld-prime/node/bindnode/registry"
mh "github.com/multiformats/go-multihash"

_ "embed"
)

//go:embed metadata.ipldsch
var schema []byte

var BindnodeRegistry = bindnoderegistry.NewRegistry()

type CarMetadata struct {
Metadata *Metadata
}

func (cm CarMetadata) Serialize(w io.Writer) error {
// TODO: do the same checks we do on Deserialize()
return BindnodeRegistry.TypeToWriter(&cm, w, dagjson.Encode)
}

func (cm *CarMetadata) Deserialize(r io.Reader) error {
cmIface, err := BindnodeRegistry.TypeFromReader(r, &CarMetadata{}, dagjson.Decode)
if err != nil {
return fmt.Errorf("invalid CarMetadata: %w", err)
}
cmm := cmIface.(*CarMetadata) // safe to assume type
if cmm.Metadata.Properties == nil && cmm.Metadata.Error == nil {
return fmt.Errorf("invalid CarMetadata: must contain either properties or error fields")
}
if (cmm.Metadata.Properties == nil) == (cmm.Metadata.Error == nil) {
return fmt.Errorf("invalid CarMetadata: must contain either properties or error fields, not both")
}
if cmm.Metadata.Properties != nil {
if _, err := mh.Decode(cmm.Metadata.Properties.ChecksumMultihash); err != nil {
return fmt.Errorf("invalid CarMetadata: checksum multihash: %w", err)
}
}
// TODO: parse and check EntityBytes format
*cm = *cmm
return nil
}

type Metadata struct {
Request Request
Properties *types.CarProperties
Error *string
}

type Request struct {
Root cid.Cid
Path *string
Scope types.DagScope
Duplicates bool
EntityBytes *string
}

func init() {
if err := BindnodeRegistry.RegisterType((*CarMetadata)(nil), string(schema), "CarMetadata"); err != nil {
panic(err.Error())
}
}
31 changes: 31 additions & 0 deletions pkg/httputil/metadata/metadata.ipldsch
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
type CarMetadata union {
| Metadata "car-metadata/v1"
} representation keyed

type Metadata struct {
request Request
# must contain either a properties or an error
properties optional CarProperties
error optional String
}

type Request struct {
root &Any
path optional String
scope DagScope
duplicates Bool (rename "dups")
entityBytes optional String (rename "entity-bytes") # Must be a valid entity-bytes param: "from:to"
}

type DagScope enum {
| all
| entity
| block
}

type CarProperties struct {
carBytes Int (rename "car_bytes")
dataBytes Int (rename "data_bytes")
blockCount Int (rename "block_count")
checksumMultihash optional Bytes (rename "checksum") # Must be a valid multihash
}
118 changes: 118 additions & 0 deletions pkg/httputil/metadata/metadata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package metadata_test

import (
"bytes"
"testing"

"github.com/filecoin-project/lassie/pkg/httputil/metadata"
"github.com/filecoin-project/lassie/pkg/types"
"github.com/ipfs/go-cid"
"github.com/stretchr/testify/require"
)

var testCid = cid.MustParse("bafybeic56z3yccnla3cutmvqsn5zy3g24muupcsjtoyp3pu5pm5amurjx4")

func TestCarMetadataRoundtrip(t *testing.T) {
path := "/birb.mp4"
orig := metadata.CarMetadata{
Metadata: &metadata.Metadata{
Request: metadata.Request{
Root: testCid,
Path: &path,
Scope: types.DagScopeAll,
Duplicates: true,
},
Properties: &types.CarProperties{
CarBytes: 202020,
DataBytes: 101010,
BlockCount: 303,
ChecksumMultihash: testCid.Hash(),
},
},
}
var buf bytes.Buffer
require.NoError(t, orig.Serialize(&buf))

t.Log("metadata dag-json:", buf.String())

var roundtrip metadata.CarMetadata
require.NoError(t, roundtrip.Deserialize(&buf))
require.Equal(t, orig, roundtrip)
require.NotNil(t, roundtrip.Metadata)
require.Equal(t, testCid, roundtrip.Metadata.Request.Root)
require.NotNil(t, roundtrip.Metadata.Request.Path)
require.Equal(t, "/birb.mp4", *roundtrip.Metadata.Request.Path)
require.Equal(t, types.DagScopeAll, roundtrip.Metadata.Request.Scope)
require.True(t, roundtrip.Metadata.Request.Duplicates)
require.NotNil(t, roundtrip.Metadata.Properties)
require.Nil(t, roundtrip.Metadata.Error)
require.Equal(t, int64(202020), roundtrip.Metadata.Properties.CarBytes)
require.Equal(t, int64(101010), roundtrip.Metadata.Properties.DataBytes)
require.Equal(t, int64(303), roundtrip.Metadata.Properties.BlockCount)
require.Equal(t, []byte(testCid.Hash()), roundtrip.Metadata.Properties.ChecksumMultihash)
}

func TestCarMetadataErrorRoundtrip(t *testing.T) {
path := "/birb.mp4"
msg := "something bad happened"
orig := metadata.CarMetadata{
Metadata: &metadata.Metadata{
Request: metadata.Request{
Root: testCid,
Path: &path,
Scope: types.DagScopeAll,
Duplicates: true,
},
Error: &msg,
},
}
var buf bytes.Buffer
require.NoError(t, orig.Serialize(&buf))

t.Log("metadata dag-json:", buf.String())

var roundtrip metadata.CarMetadata
require.NoError(t, roundtrip.Deserialize(&buf))
require.Equal(t, orig, roundtrip)
require.NotNil(t, roundtrip.Metadata)
require.Equal(t, testCid, roundtrip.Metadata.Request.Root)
require.NotNil(t, roundtrip.Metadata.Request.Path)
require.Equal(t, "/birb.mp4", *roundtrip.Metadata.Request.Path)
require.Equal(t, types.DagScopeAll, roundtrip.Metadata.Request.Scope)
require.True(t, roundtrip.Metadata.Request.Duplicates)
require.Nil(t, roundtrip.Metadata.Properties)
require.NotNil(t, roundtrip.Metadata.Error)
require.Equal(t, "something bad happened", *roundtrip.Metadata.Error)
}

func TestBadMetadata(t *testing.T) {
testCases := []struct {
name string
byts string
err string
}{
{"empty", `{}`, `union structure constraints for CarMetadata caused rejection: a union must have exactly one entry`},
{"bad key", `{"not metadata":true}`, `union structure constraints for CarMetadata caused rejection: no member named "not metadata"`},
{
"bad multihash",
`{"car-metadata/v1":{"properties":{"block_count":303,"car_bytes":202020,"checksum":{"/":{"bytes":"bm90IGEgbXVsdGloYXNo"}},"data_bytes":101010},"request":{"dups":true,"path":"/birb.mp4","root":{"/":"bafybeic56z3yccnla3cutmvqsn5zy3g24muupcsjtoyp3pu5pm5amurjx4"},"scope":"all"}}}`,
`invalid CarMetadata: checksum multihash:`,
},
{
"no properties or error",
`{"car-metadata/v1":{"request":{"dups":true,"path":"/birb.mp4","root":{"/":"bafybeic56z3yccnla3cutmvqsn5zy3g24muupcsjtoyp3pu5pm5amurjx4"},"scope":"all"}}}`,
`invalid CarMetadata: must contain either properties or error fields`,
},
{
"both properties and error",
`{"car-metadata/v1":{"error":"something bad happened","properties":{"block_count":303,"car_bytes":202020,"checksum":{"/":{"bytes":"EiBd9neBCasGxUmysJN7nGza4ylHikmbsP2+nXs6BlIpvw"}},"data_bytes":101010},"request":{"dups":true,"path":"/birb.mp4","root":{"/":"bafybeic56z3yccnla3cutmvqsn5zy3g24muupcsjtoyp3pu5pm5amurjx4"},"scope":"all"}}}`,
`invalid CarMetadata: must contain either properties or error fields, not both`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var roundtrip metadata.CarMetadata
require.ErrorContains(t, roundtrip.Deserialize(bytes.NewBuffer([]byte(tc.byts))), tc.err)
})
}
}
27 changes: 18 additions & 9 deletions pkg/server/http/util.go → pkg/httputil/server.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package httpserver
package httputil

import (
"errors"
Expand Down Expand Up @@ -74,33 +74,34 @@ func ParseFilename(req *http.Request) (string, error) {
//
// Lassie only allows the "car" format query parameter
// https://specs.ipfs.tech/http-gateways/path-gateway/#format-request-query-parameter
func CheckFormat(req *http.Request) (bool, error) {
func CheckFormat(req *http.Request) (bool, bool, error) {
hasAccept := req.Header.Get("Accept") != ""
// check if Accept header includes application/vnd.ipld.car
validAccept, includeDupes := ParseAccept(req.Header.Get("Accept"))
validAccept, includeDupes, includeMeta := ParseAccept(req.Header.Get("Accept"))
if hasAccept && !validAccept {
return false, fmt.Errorf("no acceptable content type")
return false, false, fmt.Errorf("no acceptable content type")
}

// check if format is "car"
hasFormat := req.URL.Query().Has("format")
if hasFormat && req.URL.Query().Get("format") != FormatParameterCar {
return false, fmt.Errorf("requested non-supported format %s", req.URL.Query().Get("format"))
return false, false, fmt.Errorf("requested non-supported format %s", req.URL.Query().Get("format"))
}

// if neither are provided return
// one of them has to be given with a CAR type since we only return CAR data
if !validAccept && !hasFormat {
return false, fmt.Errorf("neither a valid accept header or format parameter were provided")
return false, false, fmt.Errorf("neither a valid accept header or format parameter were provided")
}

return includeDupes, nil
return includeDupes, includeMeta, nil
}

// ParseAccept validates that the request Accept header is of the type CAR and
// returns whether or not duplicate blocks are allowed in the response via
// IPIP-412: https://github.com/ipfs/specs/pull/412.
func ParseAccept(acceptHeader string) (validAccept bool, includeDupes bool) {
// IPIP-412: https://github.com/ipfs/specs/pull/412, and whether or not
// metadata is requested via IPIP-431: https://github.com/ipfs/specs/pull/431.
func ParseAccept(acceptHeader string) (validAccept bool, includeDupes bool, includeMeta bool) {
acceptTypes := strings.Split(acceptHeader, ",")
validAccept = false
includeDupes = DefaultIncludeDupes
Expand Down Expand Up @@ -140,6 +141,14 @@ func ParseAccept(acceptHeader string) (validAccept bool, includeDupes bool) {
// we only do dfs, which also satisfies unk, future extensions are not yet supported
validAccept = false
}
case "meta":
switch value {
case "eof":
includeMeta = true
default:
// we only support eof, future extensions are not yet supported
validAccept = false
}
default:
// ignore others
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/internal/itest/http_fetch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,7 @@ func TestHttpFetch(t *testing.T) {
req.Equal(fmt.Sprintf(`attachment; filename="%s.car"`, srcData[i].Root.String()), resp.Header.Get("Content-Disposition"))
req.Equal("none", resp.Header.Get("Accept-Ranges"))
req.Equal("public, max-age=29030400, immutable", resp.Header.Get("Cache-Control"))
req.Equal("application/vnd.ipld.car; version=1", resp.Header.Get("Content-Type"))
req.Equal("application/vnd.ipld.car; version=1; order=dfs; dups=y", resp.Header.Get("Content-Type"))
req.Equal("nosniff", resp.Header.Get("X-Content-Type-Options"))
etagStart := fmt.Sprintf(`"%s.car.`, srcData[i].Root.String())
etagGot := resp.Header.Get("ETag")
Expand Down
Loading