Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.22.0
require (
github.com/NYTimes/gziphandler v1.1.1
github.com/alicebob/miniredis/v2 v2.32.1
github.com/aohorodnyk/mimeheader v0.0.6
github.com/attestantio/go-builder-client v0.5.1-0.20250120215322-c65b220a98eb
github.com/attestantio/go-eth2-client v0.22.1-0.20250106164842-07b6ce39bb43
github.com/bradfitz/gomemcache v0.0.0-20230124162541-5f7a7d875746
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@ github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZp
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.32.1 h1:Bz7CciDnYSaa0mX5xODh6GUITRSx+cVhjNoOR4JssBo=
github.com/alicebob/miniredis/v2 v2.32.1/go.mod h1:AqkLNAfUm0K07J28hnAyyQKf/x0YkCY/g5DCtuL01Mw=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/aohorodnyk/mimeheader v0.0.6 h1:WCV4NQjtbqnd2N3FT5MEPesan/lfvaLYmt5v4xSaX/M=
github.com/aohorodnyk/mimeheader v0.0.6/go.mod h1:/Gd3t3vszyZYwjNJo2qDxoftZjjVzMdkQZxkiINp3vM=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/attestantio/go-builder-client v0.5.1-0.20250120215322-c65b220a98eb h1:J4Dpih8da/kACBsY104/mKzDv42a3O5TXTElY5ZkN80=
github.com/attestantio/go-builder-client v0.5.1-0.20250120215322-c65b220a98eb/go.mod h1:X31JAUL4q6cY/OGClpBQcwFN7FBixt6Wjrqy7RrlhEc=
github.com/attestantio/go-eth2-client v0.22.1-0.20250106164842-07b6ce39bb43 h1:lORlCOleRXvVt3H7fan64UaYAK4FJDHdy19uYfe7FKQ=
github.com/attestantio/go-eth2-client v0.22.1-0.20250106164842-07b6ce39bb43/go.mod h1:vy5jU/uDZ2+RcVzq5BfnG+bQ3/6uu9DGwCrGsPtjJ1A=
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
Expand Down
66 changes: 66 additions & 0 deletions services/api/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"time"

"github.com/NYTimes/gziphandler"
"github.com/aohorodnyk/mimeheader"
builderApi "github.com/attestantio/go-builder-client/api"
builderApiV1 "github.com/attestantio/go-builder-client/api/v1"
"github.com/attestantio/go-eth2-client/spec"
Expand Down Expand Up @@ -434,9 +435,9 @@
log.Infof("forkSchedule: version=%s / epoch=%d", fork.CurrentVersion, fork.Epoch)
switch fork.CurrentVersion {
case api.opts.EthNetDetails.CapellaForkVersionHex:
api.capellaEpoch = int64(fork.Epoch)

Check failure on line 438 in services/api/service.go

View workflow job for this annotation

GitHub Actions / Lint

G115: integer overflow conversion uint64 -> int64 (gosec)
case api.opts.EthNetDetails.DenebForkVersionHex:
api.denebEpoch = int64(fork.Epoch)

Check failure on line 440 in services/api/service.go

View workflow job for this annotation

GitHub Actions / Lint

G115: integer overflow conversion uint64 -> int64 (gosec)
case api.opts.EthNetDetails.ElectraForkVersionHex:
api.electraEpoch = int64(fork.Epoch)
}
Expand Down Expand Up @@ -944,6 +945,46 @@
w.WriteHeader(http.StatusOK)
}

const (
ApplicationJSON = "application/json"
ApplicationOctetStream = "application/octet-stream"
)

// RequestAcceptsJSON returns true if the Accept header is empty (defaults to JSON)
// or application/json can be negotiated.
func RequestAcceptsJSON(req *http.Request) bool {
ah := req.Header.Get("Accept")
if ah == "" {
return true
}
mh := mimeheader.ParseAcceptHeader(ah)
_, _, matched := mh.Negotiate(
[]string{ApplicationJSON},
ApplicationJSON,
)
return matched
}

// NegotiateRequestResponseType returns whether the request accepts
// JSON (application/json) or SSZ (application/octet-stream) responses.
// If accepted is false, no mime type could be negotiated and the server
// should respond with http.StatusNotAcceptable.
func NegotiateRequestResponseType(req *http.Request) (mimeType string, err error) {
ah := req.Header.Get("Accept")
if ah == "" {
return ApplicationJSON, nil
}
mh := mimeheader.ParseAcceptHeader(ah)
_, mimeType, matched := mh.Negotiate(
[]string{ApplicationJSON, ApplicationOctetStream},
ApplicationJSON,
)
if !matched {
return "", ErrNotAcceptable
}
return mimeType, nil
}

// ---------------
// PROPOSER APIS
// ---------------
Expand Down Expand Up @@ -1219,6 +1260,12 @@
return
}

// TODO: Use NegotiateRequestResponseType, for now we only accept JSON
if !RequestAcceptsJSON(req) {
api.RespondError(w, http.StatusNotAcceptable, "only Accept: application/json is currently supported")
return
}

log.Debug("getHeader request received")
defer func() {
metrics.GetHeaderLatencyHistogram.Record(
Expand Down Expand Up @@ -1296,7 +1343,7 @@
}
}

func (api *RelayAPI) handleGetPayload(w http.ResponseWriter, req *http.Request) {

Check failure on line 1346 in services/api/service.go

View workflow job for this annotation

GitHub Actions / Lint

cognitive complexity 88 of func `(*RelayAPI).handleGetPayload` is high (> 85) (gocognit)
api.getPayloadCallsInFlight.Add(1)
defer api.getPayloadCallsInFlight.Done()

Expand Down Expand Up @@ -1328,6 +1375,25 @@
)
}()

// TODO: Use NegotiateRequestResponseType, for now we only accept JSON
if !RequestAcceptsJSON(req) {
api.RespondError(w, http.StatusNotAcceptable, "only Accept: application/json is currently supported")
return
}

// If the Content-Type header is included, for now only allow JSON.
// TODO: support Content-Type: application/octet-stream and allow SSZ
// request bodies.
if ct := req.Header.Get("Content-Type"); ct != "" {
switch ct {
case ApplicationJSON:
break
default:
api.RespondError(w, http.StatusUnsupportedMediaType, "only Content-Type: application/json is currently supported")
return
}
}

// Read the body first, so we can decode it later
body, err := io.ReadAll(req.Body)
if err != nil {
Expand Down
58 changes: 58 additions & 0 deletions services/api/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1384,3 +1384,61 @@ func gzipBytes(t *testing.T, b []byte) []byte {
require.NoError(t, zw.Close())
return buf.Bytes()
}

func TestRequestAcceptsJSON(t *testing.T) {
for _, tc := range []struct {
Header string
Expected bool
}{
{Header: "", Expected: true},
{Header: "application/json", Expected: true},
{Header: "application/octet-stream", Expected: false},
{Header: "application/octet-stream;q=1.0,application/json;q=0.9", Expected: true},
{Header: "application/octet-stream;q=1.0,application/something-else;q=0.9", Expected: false},
{Header: "application/octet-stream;q=1.0,application/*;q=0.9", Expected: true},
{Header: "application/octet-stream;q=1.0,*/*;q=0.9", Expected: true},
{Header: "application/*;q=0.9", Expected: true},
{Header: "application/*", Expected: true},
} {
t.Run(tc.Header, func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/eth/v1/builder/header/1/0x00/0xaa", nil)
require.NoError(t, err)
req.Header.Set("Accept", tc.Header)
actual := RequestAcceptsJSON(req)
require.Equal(t, tc.Expected, actual)
})
}
}

func TestNegotiateRequestResponseType(t *testing.T) {
for _, tc := range []struct {
Header string
Expected string
Error error
}{
{Header: "", Expected: ApplicationJSON},
{Header: "application/json", Expected: ApplicationJSON},
{Header: "application/octet-stream", Expected: ApplicationOctetStream},
{Header: "application/octet-stream;q=1.0,application/json;q=0.9", Expected: ApplicationOctetStream},
{Header: "application/octet-stream;q=1.0,application/something-else;q=0.9", Expected: ApplicationOctetStream},
{Header: "application/octet-stream;q=1.0,application/*;q=0.9", Expected: ApplicationOctetStream},
{Header: "application/octet-stream;q=1.0,*/*;q=0.9", Expected: ApplicationOctetStream},
{Header: "application/octet-stream;q=0.9,*/*;q=1.0", Expected: ApplicationJSON},
{Header: "application/*;q=0.9", Expected: ApplicationJSON, Error: nil},
{Header: "application/*", Expected: ApplicationJSON, Error: nil},
{Header: "text/html", Error: ErrNotAcceptable},
} {
t.Run(tc.Header, func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/eth/v1/builder/header/1/0x00/0xaa", nil)
require.NoError(t, err)
req.Header.Set("Accept", tc.Header)
negotiated, err := NegotiateRequestResponseType(req)
if tc.Error != nil {
require.Equal(t, tc.Error, err)
} else {
require.NoError(t, err)
require.Equal(t, tc.Expected, negotiated)
}
})
}
}
1 change: 1 addition & 0 deletions services/api/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var (
ErrPayloadMismatch = errors.New("beacon-block and payload version mismatch")
ErrHeaderHTRMismatch = errors.New("beacon-block and payload header mismatch")
ErrBlobMismatch = errors.New("beacon-block and payload blob contents mismatch")
ErrNotAcceptable = errors.New("not acceptable")
)

func SanityCheckBuilderBlockSubmission(payload *common.VersionedSubmitBlockRequest) error {
Expand Down
Loading