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(builder-spec): Correctly reject SSZ Accept/Content-Types headers. #678

Merged
merged 9 commits into from
Feb 19, 2025
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
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.4.3-0.20240124194555-d44db06f45fa
github.com/attestantio/go-eth2-client v0.21.1
github.com/bradfitz/gomemcache v0.0.0-20230124162541-5f7a7d875746
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ github.com/alicebob/miniredis/v2 v2.32.1 h1:Bz7CciDnYSaa0mX5xODh6GUITRSx+cVhjNoO
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.4.3-0.20240124194555-d44db06f45fa h1:Kj6d1tXAA+EAi7fK8z8NakBEpY4WYzZMuCmLZjwBpTM=
github.com/attestantio/go-builder-client v0.4.3-0.20240124194555-d44db06f45fa/go.mod h1:e02i/WO4fjs3/u9oIZEjiC8CK1Qyxy4cpiMMGKx4VqQ=
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 @@ import (
"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 @@ -930,6 +931,46 @@ func (api *RelayAPI) handleStatus(w http.ResponseWriter, req *http.Request) {
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 @@ -1205,6 +1246,12 @@ func (api *RelayAPI) handleGetHeader(w http.ResponseWriter, req *http.Request) {
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 @@ -1312,6 +1359,25 @@ func (api *RelayAPI) handleGetPayload(w http.ResponseWriter, req *http.Request)
)
}()

// 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 "application/json":
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 @@ -1379,3 +1379,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 @@ -23,6 +23,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