Skip to content

Commit a89aea6

Browse files
feat(builder-spec): Correctly reject SSZ Accept/Content-Types headers. (#678)
* feat(builder-spec): Correctly reject SSZ Accept/Content-Types headers. * fixup: clearer Content-Type comment * fixup: GHA lint differs from local one. * Update services/api/service.go Co-authored-by: Justin Traglia <[email protected]> * Fix ErrNotAcceptable indentation * Run go mod tidy * Add content type check for registerValidators --------- Co-authored-by: Justin Traglia <[email protected]> Co-authored-by: Justin Traglia <[email protected]>
1 parent 3a0417e commit a89aea6

File tree

6 files changed

+142
-0
lines changed

6 files changed

+142
-0
lines changed

.golangci.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ linters:
2929
- interfacebloat
3030
- exhaustruct
3131
- tenv
32+
- gocognit
3233

3334
#
3435
# Disabled because of generics:

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.24.0
55
require (
66
github.com/NYTimes/gziphandler v1.1.1
77
github.com/alicebob/miniredis/v2 v2.32.1
8+
github.com/aohorodnyk/mimeheader v0.0.6
89
github.com/attestantio/go-builder-client v0.5.1-0.20250120215322-c65b220a98eb
910
github.com/attestantio/go-eth2-client v0.22.1-0.20250106164842-07b6ce39bb43
1011
github.com/bradfitz/gomemcache v0.0.0-20230124162541-5f7a7d875746

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZp
99
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
1010
github.com/alicebob/miniredis/v2 v2.32.1 h1:Bz7CciDnYSaa0mX5xODh6GUITRSx+cVhjNoOR4JssBo=
1111
github.com/alicebob/miniredis/v2 v2.32.1/go.mod h1:AqkLNAfUm0K07J28hnAyyQKf/x0YkCY/g5DCtuL01Mw=
12+
github.com/aohorodnyk/mimeheader v0.0.6 h1:WCV4NQjtbqnd2N3FT5MEPesan/lfvaLYmt5v4xSaX/M=
13+
github.com/aohorodnyk/mimeheader v0.0.6/go.mod h1:/Gd3t3vszyZYwjNJo2qDxoftZjjVzMdkQZxkiINp3vM=
1214
github.com/attestantio/go-builder-client v0.5.1-0.20250120215322-c65b220a98eb h1:J4Dpih8da/kACBsY104/mKzDv42a3O5TXTElY5ZkN80=
1315
github.com/attestantio/go-builder-client v0.5.1-0.20250120215322-c65b220a98eb/go.mod h1:X31JAUL4q6cY/OGClpBQcwFN7FBixt6Wjrqy7RrlhEc=
1416
github.com/attestantio/go-eth2-client v0.22.1-0.20250106164842-07b6ce39bb43 h1:lORlCOleRXvVt3H7fan64UaYAK4FJDHdy19uYfe7FKQ=

services/api/service.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"time"
2222

2323
"github.com/NYTimes/gziphandler"
24+
"github.com/aohorodnyk/mimeheader"
2425
builderApi "github.com/attestantio/go-builder-client/api"
2526
builderApiV1 "github.com/attestantio/go-builder-client/api/v1"
2627
"github.com/attestantio/go-eth2-client/spec"
@@ -944,6 +945,46 @@ func (api *RelayAPI) handleStatus(w http.ResponseWriter, req *http.Request) {
944945
w.WriteHeader(http.StatusOK)
945946
}
946947

948+
const (
949+
ApplicationJSON = "application/json"
950+
ApplicationOctetStream = "application/octet-stream"
951+
)
952+
953+
// RequestAcceptsJSON returns true if the Accept header is empty (defaults to JSON)
954+
// or application/json can be negotiated.
955+
func RequestAcceptsJSON(req *http.Request) bool {
956+
ah := req.Header.Get("Accept")
957+
if ah == "" {
958+
return true
959+
}
960+
mh := mimeheader.ParseAcceptHeader(ah)
961+
_, _, matched := mh.Negotiate(
962+
[]string{ApplicationJSON},
963+
ApplicationJSON,
964+
)
965+
return matched
966+
}
967+
968+
// NegotiateRequestResponseType returns whether the request accepts
969+
// JSON (application/json) or SSZ (application/octet-stream) responses.
970+
// If accepted is false, no mime type could be negotiated and the server
971+
// should respond with http.StatusNotAcceptable.
972+
func NegotiateRequestResponseType(req *http.Request) (mimeType string, err error) {
973+
ah := req.Header.Get("Accept")
974+
if ah == "" {
975+
return ApplicationJSON, nil
976+
}
977+
mh := mimeheader.ParseAcceptHeader(ah)
978+
_, mimeType, matched := mh.Negotiate(
979+
[]string{ApplicationJSON, ApplicationOctetStream},
980+
ApplicationJSON,
981+
)
982+
if !matched {
983+
return "", ErrNotAcceptable
984+
}
985+
return mimeType, nil
986+
}
987+
947988
// ---------------
948989
// PROPOSER APIS
949990
// ---------------
@@ -963,6 +1004,19 @@ func (api *RelayAPI) handleRegisterValidator(w http.ResponseWriter, req *http.Re
9631004
"contentLength": req.ContentLength,
9641005
})
9651006

1007+
// If the Content-Type header is included, for now only allow JSON.
1008+
// TODO: support Content-Type: application/octet-stream and allow SSZ
1009+
// request bodies.
1010+
if ct := req.Header.Get("Content-Type"); ct != "" {
1011+
switch ct {
1012+
case ApplicationJSON:
1013+
break
1014+
default:
1015+
api.RespondError(w, http.StatusUnsupportedMediaType, "only Content-Type: application/json is currently supported")
1016+
return
1017+
}
1018+
}
1019+
9661020
start := time.Now().UTC()
9671021
registrationTimestampUpperBound := start.Unix() + 10 // 10 seconds from now
9681022

@@ -1219,6 +1273,12 @@ func (api *RelayAPI) handleGetHeader(w http.ResponseWriter, req *http.Request) {
12191273
return
12201274
}
12211275

1276+
// TODO: Use NegotiateRequestResponseType, for now we only accept JSON
1277+
if !RequestAcceptsJSON(req) {
1278+
api.RespondError(w, http.StatusNotAcceptable, "only Accept: application/json is currently supported")
1279+
return
1280+
}
1281+
12221282
log.Debug("getHeader request received")
12231283
defer func() {
12241284
metrics.GetHeaderLatencyHistogram.Record(
@@ -1328,6 +1388,25 @@ func (api *RelayAPI) handleGetPayload(w http.ResponseWriter, req *http.Request)
13281388
)
13291389
}()
13301390

1391+
// TODO: Use NegotiateRequestResponseType, for now we only accept JSON
1392+
if !RequestAcceptsJSON(req) {
1393+
api.RespondError(w, http.StatusNotAcceptable, "only Accept: application/json is currently supported")
1394+
return
1395+
}
1396+
1397+
// If the Content-Type header is included, for now only allow JSON.
1398+
// TODO: support Content-Type: application/octet-stream and allow SSZ
1399+
// request bodies.
1400+
if ct := req.Header.Get("Content-Type"); ct != "" {
1401+
switch ct {
1402+
case ApplicationJSON:
1403+
break
1404+
default:
1405+
api.RespondError(w, http.StatusUnsupportedMediaType, "only Content-Type: application/json is currently supported")
1406+
return
1407+
}
1408+
}
1409+
13311410
// Read the body first, so we can decode it later
13321411
body, err := io.ReadAll(req.Body)
13331412
if err != nil {

services/api/service_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,3 +1383,61 @@ func gzipBytes(t *testing.T, b []byte) []byte {
13831383
require.NoError(t, zw.Close())
13841384
return buf.Bytes()
13851385
}
1386+
1387+
func TestRequestAcceptsJSON(t *testing.T) {
1388+
for _, tc := range []struct {
1389+
Header string
1390+
Expected bool
1391+
}{
1392+
{Header: "", Expected: true},
1393+
{Header: "application/json", Expected: true},
1394+
{Header: "application/octet-stream", Expected: false},
1395+
{Header: "application/octet-stream;q=1.0,application/json;q=0.9", Expected: true},
1396+
{Header: "application/octet-stream;q=1.0,application/something-else;q=0.9", Expected: false},
1397+
{Header: "application/octet-stream;q=1.0,application/*;q=0.9", Expected: true},
1398+
{Header: "application/octet-stream;q=1.0,*/*;q=0.9", Expected: true},
1399+
{Header: "application/*;q=0.9", Expected: true},
1400+
{Header: "application/*", Expected: true},
1401+
} {
1402+
t.Run(tc.Header, func(t *testing.T) {
1403+
req, err := http.NewRequest(http.MethodGet, "/eth/v1/builder/header/1/0x00/0xaa", nil)
1404+
require.NoError(t, err)
1405+
req.Header.Set("Accept", tc.Header)
1406+
actual := RequestAcceptsJSON(req)
1407+
require.Equal(t, tc.Expected, actual)
1408+
})
1409+
}
1410+
}
1411+
1412+
func TestNegotiateRequestResponseType(t *testing.T) {
1413+
for _, tc := range []struct {
1414+
Header string
1415+
Expected string
1416+
Error error
1417+
}{
1418+
{Header: "", Expected: ApplicationJSON},
1419+
{Header: "application/json", Expected: ApplicationJSON},
1420+
{Header: "application/octet-stream", Expected: ApplicationOctetStream},
1421+
{Header: "application/octet-stream;q=1.0,application/json;q=0.9", Expected: ApplicationOctetStream},
1422+
{Header: "application/octet-stream;q=1.0,application/something-else;q=0.9", Expected: ApplicationOctetStream},
1423+
{Header: "application/octet-stream;q=1.0,application/*;q=0.9", Expected: ApplicationOctetStream},
1424+
{Header: "application/octet-stream;q=1.0,*/*;q=0.9", Expected: ApplicationOctetStream},
1425+
{Header: "application/octet-stream;q=0.9,*/*;q=1.0", Expected: ApplicationJSON},
1426+
{Header: "application/*;q=0.9", Expected: ApplicationJSON, Error: nil},
1427+
{Header: "application/*", Expected: ApplicationJSON, Error: nil},
1428+
{Header: "text/html", Error: ErrNotAcceptable},
1429+
} {
1430+
t.Run(tc.Header, func(t *testing.T) {
1431+
req, err := http.NewRequest(http.MethodGet, "/eth/v1/builder/header/1/0x00/0xaa", nil)
1432+
require.NoError(t, err)
1433+
req.Header.Set("Accept", tc.Header)
1434+
negotiated, err := NegotiateRequestResponseType(req)
1435+
if tc.Error != nil {
1436+
require.Equal(t, tc.Error, err)
1437+
} else {
1438+
require.NoError(t, err)
1439+
require.Equal(t, tc.Expected, negotiated)
1440+
}
1441+
})
1442+
}
1443+
}

services/api/utils.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ var (
2525
ErrPayloadMismatch = errors.New("beacon-block and payload version mismatch")
2626
ErrHeaderHTRMismatch = errors.New("beacon-block and payload header mismatch")
2727
ErrBlobMismatch = errors.New("beacon-block and payload blob contents mismatch")
28+
ErrNotAcceptable = errors.New("not acceptable")
2829
)
2930

3031
func SanityCheckBuilderBlockSubmission(payload *common.VersionedSubmitBlockRequest) error {

0 commit comments

Comments
 (0)