Skip to content

Commit 827140c

Browse files
authored
Merge pull request #2 from Rocket-Rescue-Node/jms/ssz-register-validator
Support ssz-formatted register_validator calls
2 parents a9e66a4 + d4d53de commit 827140c

File tree

11 files changed

+702
-37
lines changed

11 files changed

+702
-37
lines changed

.github/workflows/golangci-lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
steps:
2020
- uses: actions/setup-go@v3
2121
with:
22-
go-version: 1.19
22+
go-version: 1.21
2323
- uses: actions/checkout@v3
2424
- name: golangci-lint
2525
uses: golangci/golangci-lint-action@v3

.github/workflows/test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
- name: Set up Go
1313
uses: actions/setup-go@v4
1414
with:
15-
go-version: '1.19'
15+
go-version: '1.21'
1616

1717
- name: Test
1818
run: go test -v ./...

go.mod

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
module github.com/Rocket-Rescue-Node/guarded-beacon-proxy
22

3-
go 1.19
3+
go 1.21
44

55
require (
66
github.com/ethereum/go-ethereum v1.12.0
7+
github.com/ferranbt/fastssz v0.1.4
78
github.com/gorilla/mux v1.8.0
89
github.com/mwitkow/grpc-proxy v0.0.0-20230212185441-f345521cb9c9
910
github.com/prysmaticlabs/prysm/v4 v4.0.6
@@ -18,6 +19,7 @@ require (
1819
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
1920
github.com/cespare/xxhash/v2 v2.2.0 // indirect
2021
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
22+
github.com/emicklei/dot v1.6.2 // indirect
2123
github.com/golang/protobuf v1.5.3 // indirect
2224
github.com/google/go-cmp v0.5.9 // indirect
2325
github.com/grpc-ecosystem/grpc-gateway/v2 v2.0.1 // indirect
@@ -33,7 +35,7 @@ require (
3335
github.com/prometheus/procfs v0.9.0 // indirect
3436
github.com/prysmaticlabs/fastssz v0.0.0-20220628121656-93dfe28febab // indirect
3537
github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7 // indirect
36-
github.com/prysmaticlabs/gohashtree v0.0.3-alpha // indirect
38+
github.com/prysmaticlabs/gohashtree v0.0.4-beta // indirect
3739
github.com/thomaso-mirodin/intmath v0.0.0-20160323211736-5dc6d854e46e // indirect
3840
golang.org/x/crypto v0.7.0 // indirect
3941
golang.org/x/sys v0.7.0 // indirect

go.sum

Lines changed: 43 additions & 2 deletions
Large diffs are not rendered by default.

guarded-beacon-proxy.go

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,23 @@ package guardedbeaconproxy
33
import (
44
"context"
55
"fmt"
6+
"io"
67
"log"
78
"net"
89
"net/http"
910
"net/http/httputil"
1011
"net/url"
1112
"time"
1213

14+
"github.com/Rocket-Rescue-Node/guarded-beacon-proxy/jsontypes"
1315
"github.com/gorilla/mux"
1416
"github.com/mwitkow/grpc-proxy/proxy"
1517
"google.golang.org/grpc"
1618
)
1719

20+
type RegisterValidatorRequest = jsontypes.RegisterValidatorRequest
21+
type PrepareBeaconProposerRequest = jsontypes.PrepareBeaconProposerRequest
22+
1823
// PrepareBeaconProposerGuard is a function that validates whether or not a PrepareBeaconProposer call
1924
// should be proxied. The provided Context is whatever was returned by the authenticator.
2025
type PrepareBeaconProposerGuard func(PrepareBeaconProposerRequest, context.Context) (AuthenticationStatus, error)
@@ -53,6 +58,9 @@ type GuardedBeaconProxy struct {
5358
Addr string
5459
// Optional GRPC address to listen on
5560
GRPCAddr string
61+
// Maximum request body size in bytes
62+
// If 0, no limit is applied
63+
MaxRequestBodySize int64
5664
// Pass-through HTTP server settings
5765
ReadTimeout time.Duration
5866
ReadHeaderTimeout time.Duration
@@ -113,6 +121,43 @@ func (gbp *GuardedBeaconProxy) init() {
113121
gbp.server.ErrorLog = gbp.ErrorLog
114122
}
115123

124+
func (gbp *GuardedBeaconProxy) limitRequestBodyHandlerFunc(next httpGuard) httpGuard {
125+
return func(w http.ResponseWriter, r *http.Request) bool {
126+
if gbp.MaxRequestBodySize == 0 {
127+
return next(w, r)
128+
}
129+
130+
// Allow 1 extra byte. If it actually gets read, we will return an error.
131+
// This lets us detect if the request body is exactly the size of the limit.
132+
sizeLimit := gbp.MaxRequestBodySize + 1
133+
134+
if r.ContentLength >= sizeLimit {
135+
gbp.httpError(w, http.StatusRequestEntityTooLarge, fmt.Errorf("request body too large"))
136+
return false
137+
}
138+
139+
limited := &io.LimitedReader{
140+
R: r.Body,
141+
N: sizeLimit,
142+
}
143+
// According to the docs, http servers don't need to close the body ReadCloser, only Clients do.
144+
r.Body = io.NopCloser(limited)
145+
shouldProxy := next(w, r)
146+
if !shouldProxy {
147+
return false
148+
}
149+
if limited.N == 0 {
150+
// The next handler didn't return an error, but we exceeded the limit.
151+
// Do not proxy the request, and return StatusRequestEntityTooLarge.
152+
gbp.httpError(w, http.StatusRequestEntityTooLarge, fmt.Errorf("request body too large"))
153+
return false
154+
}
155+
// The next handler didn't return an error, and we didn't exceed the limit.
156+
// Proxy the request.
157+
return true
158+
}
159+
}
160+
116161
// Serve attaches the proxy to the provided listener(s)
117162
//
118163
// Serve blocks until Stop is called or an error is encountered.
@@ -124,12 +169,25 @@ func (gbp *GuardedBeaconProxy) Serve(httpListener net.Listener, grpcListener *ne
124169
router := mux.NewRouter()
125170

126171
if gbp.PrepareBeaconProposerGuard != nil {
127-
router.Path("/eth/v1/validator/prepare_beacon_proposer").HandlerFunc(gbp.prepareBeaconProposer)
172+
router.Path("/eth/v1/validator/prepare_beacon_proposer").HandlerFunc(
173+
func(w http.ResponseWriter, r *http.Request) {
174+
if gbp.limitRequestBodyHandlerFunc(gbp.prepareBeaconProposer)(w, r) {
175+
gbp.proxy.ServeHTTP(w, r)
176+
}
177+
},
178+
)
128179
}
129180

130181
if gbp.RegisterValidatorGuard != nil {
131-
router.Path("/eth/v1/validator/register_validator").HandlerFunc(gbp.registerValidator)
182+
router.Path("/eth/v1/validator/register_validator").HandlerFunc(
183+
func(w http.ResponseWriter, r *http.Request) {
184+
if gbp.limitRequestBodyHandlerFunc(gbp.registerValidator)(w, r) {
185+
gbp.proxy.ServeHTTP(w, r)
186+
}
187+
},
188+
)
132189
}
190+
133191
router.PathPrefix("/").Handler(gbp.proxy)
134192

135193
if gbp.HTTPAuthenticator != nil {

http.go

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import (
66
"encoding/json"
77
"fmt"
88
"io"
9+
"mime"
910
"net/http"
11+
12+
"github.com/Rocket-Rescue-Node/guarded-beacon-proxy/ssz"
1013
)
1114

1215
// HTTPAuthenticator is a function type which can authenticate HTTP requests.
@@ -22,6 +25,9 @@ import (
2225
// information.
2326
type HTTPAuthenticator func(*http.Request) (AuthenticationStatus, context.Context, error)
2427

28+
// If true is returned, the upstream will proxy the request.
29+
type httpGuard func(w http.ResponseWriter, r *http.Request) bool
30+
2531
func (gbp *GuardedBeaconProxy) authenticationMiddleware(next http.Handler) http.Handler {
2632
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2733
status, context, err := gbp.HTTPAuthenticator(r)
@@ -40,16 +46,13 @@ func (gbp *GuardedBeaconProxy) authenticationMiddleware(next http.Handler) http.
4046
}
4147

4248
func cloneRequestBody(r *http.Request) (io.ReadCloser, error) {
43-
// Read the body
44-
buf, err := io.ReadAll(r.Body)
45-
if err != nil {
46-
return nil, err
47-
}
49+
// Use an io.TeeReader to return a reader that re-writes the body to the original request body.
50+
buf := bytes.NewBuffer(nil)
51+
tee := io.TeeReader(r.Body, buf)
52+
out := io.NopCloser(tee)
53+
r.Body = io.NopCloser(buf)
4854

49-
original := io.NopCloser(bytes.NewBuffer(buf))
50-
clone := io.NopCloser(bytes.NewBuffer(buf))
51-
r.Body = original
52-
return clone, nil
55+
return out, nil
5356
}
5457

5558
func (gbp *GuardedBeaconProxy) httpError(w http.ResponseWriter, code int, err error) {
@@ -60,46 +63,64 @@ func (gbp *GuardedBeaconProxy) httpError(w http.ResponseWriter, code int, err er
6063
}
6164
}
6265

63-
func (gbp *GuardedBeaconProxy) prepareBeaconProposer(w http.ResponseWriter, r *http.Request) {
64-
buf, err := cloneRequestBody(r)
66+
func (gbp *GuardedBeaconProxy) prepareBeaconProposer(w http.ResponseWriter, r *http.Request) bool {
67+
reader, err := cloneRequestBody(r)
6568
if err != nil {
6669
gbp.httpError(w, http.StatusInternalServerError, nil)
67-
return
70+
return false
6871
}
6972

7073
var proposers PrepareBeaconProposerRequest
71-
if err := json.NewDecoder(buf).Decode(&proposers); err != nil {
74+
if err := json.NewDecoder(reader).Decode(&proposers); err != nil {
7275
gbp.httpError(w, http.StatusBadRequest, nil)
73-
return
76+
return false
7477
}
7578

7679
status, err := gbp.PrepareBeaconProposerGuard(proposers, r.Context())
7780
if status != Allowed {
7881
gbp.httpError(w, status.httpStatus(), err)
79-
return
82+
return false
8083
}
8184

82-
gbp.proxy.ServeHTTP(w, r)
85+
return true
8386
}
8487

85-
func (gbp *GuardedBeaconProxy) registerValidator(w http.ResponseWriter, r *http.Request) {
86-
buf, err := cloneRequestBody(r)
88+
func (gbp *GuardedBeaconProxy) registerValidator(w http.ResponseWriter, r *http.Request) bool {
89+
reader, err := cloneRequestBody(r)
8790
if err != nil {
8891
gbp.httpError(w, http.StatusInternalServerError, nil)
89-
return
92+
return false
93+
}
94+
95+
// Check the content-type header
96+
contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
97+
if err != nil {
98+
gbp.httpError(w, http.StatusUnsupportedMediaType, err)
99+
return false
90100
}
91101

92102
var validators RegisterValidatorRequest
93-
if err := json.NewDecoder(buf).Decode(&validators); err != nil {
94-
gbp.httpError(w, http.StatusBadRequest, nil)
95-
return
103+
switch contentType {
104+
case "application/json":
105+
if err := json.NewDecoder(reader).Decode(&validators); err != nil {
106+
gbp.httpError(w, http.StatusBadRequest, err)
107+
return false
108+
}
109+
case "application/octet-stream":
110+
if err, status := ssz.ToRegisterValidatorRequest(&validators, reader, gbp.MaxRequestBodySize); err != nil {
111+
gbp.httpError(w, status, err)
112+
return false
113+
}
114+
default:
115+
gbp.httpError(w, http.StatusUnsupportedMediaType, fmt.Errorf("unsupported content type: %s", contentType))
116+
return false
96117
}
97118

98119
status, err := gbp.RegisterValidatorGuard(validators, r.Context())
99120
if status != Allowed {
100121
gbp.httpError(w, status.httpStatus(), err)
101-
return
122+
return false
102123
}
103124

104-
gbp.proxy.ServeHTTP(w, r)
125+
return true
105126
}

0 commit comments

Comments
 (0)