Skip to content

Commit a75f67b

Browse files
akrem-chabchoubv1rtl
authored andcommitted
fix: add validation for redundancy level in bytes and bzz handlers (ethersphere#5311)
1 parent 124c549 commit a75f67b

8 files changed

Lines changed: 268 additions & 38 deletions

File tree

openapi/Swarm.yaml

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -163,37 +163,12 @@ paths:
163163
tags:
164164
- Bytes
165165
parameters:
166-
- in: header
167-
schema:
168-
$ref: "SwarmCommon.yaml#/components/parameters/SwarmPostageBatchId"
169-
name: swarm-postage-batch-id
170-
required: true
171-
- in: header
172-
schema:
173-
$ref: "SwarmCommon.yaml#/components/parameters/SwarmTagParameter"
174-
name: swarm-tag
175-
required: false
176-
- in: header
177-
schema:
178-
$ref: "SwarmCommon.yaml#/components/parameters/SwarmPinParameter"
179-
name: swarm-pin
180-
required: false
181-
- in: header
182-
schema:
183-
$ref: "SwarmCommon.yaml#/components/parameters/SwarmDeferredUpload"
184-
name: swarm-deferred-upload
185-
required: false
186-
- in: header
187-
schema:
188-
$ref: "SwarmCommon.yaml#/components/parameters/SwarmEncryptParameter"
189-
name: swarm-encrypt
190-
required: false
191-
- in: header
192-
schema:
193-
$ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyLevelParameter"
194-
name: swarm-redundancy-level
195-
required: false
196-
166+
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmPostageBatchId"
167+
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmTagParameter"
168+
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmPinParameter"
169+
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmDeferredUpload"
170+
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmEncryptParameter"
171+
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyLevelParameter"
197172
requestBody:
198173
content:
199174
application/octet-stream:

pkg/api/api.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,9 @@ type Service struct {
215215

216216
whitelistedWithdrawalAddress []common.Address
217217

218-
preMapHooks map[string]func(v string) (string, error)
219-
validate *validator.Validate
218+
preMapHooks map[string]func(v string) (string, error)
219+
customValidationMessages map[string]func(err validator.FieldError) error
220+
validate *validator.Validate
220221

221222
redistributionAgent *storageincentives.Agent
222223

@@ -322,6 +323,7 @@ func New(
322323
}
323324
return name
324325
})
326+
s.setupValidation()
325327
s.stamperStore = stamperStore
326328

327329
for _, v := range whitelistedWithdrawalAddress {
@@ -710,11 +712,17 @@ func (s *Service) mapStructure(input, output any) func(string, log.Logger, http.
710712
case []byte:
711713
val = string(v)
712714
}
715+
var cause error
716+
if msgFn, ok := s.customValidationMessages[err.Tag()]; ok {
717+
cause = msgFn(err)
718+
} else {
719+
cause = fmt.Errorf("want %s:%s", err.Tag(), err.Param())
720+
}
713721
vErrs = multierror.Append(vErrs,
714722
&validationError{
715723
Entry: strings.ToLower(err.Field()),
716724
Value: val,
717-
Cause: fmt.Errorf("want %s:%s", err.Tag(), err.Param()),
725+
Cause: cause,
718726
})
719727
}
720728
return response(vErrs.ErrorOrNil())

pkg/api/bytes.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func (s *Service) bytesUploadHandler(w http.ResponseWriter, r *http.Request) {
4040
Pin bool `map:"Swarm-Pin"`
4141
Deferred *bool `map:"Swarm-Deferred-Upload"`
4242
Encrypt bool `map:"Swarm-Encrypt"`
43-
RLevel redundancy.Level `map:"Swarm-Redundancy-Level"`
43+
RLevel redundancy.Level `map:"Swarm-Redundancy-Level" validate:"rLevel"`
4444
Act bool `map:"Swarm-Act"`
4545
HistoryAddress swarm.Address `map:"Swarm-Act-History-Address"`
4646
}{}

pkg/api/bytes_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import (
88
"bytes"
99
"context"
1010
"errors"
11+
"fmt"
1112
"net/http"
1213
"strconv"
1314
"testing"
1415

1516
"github.com/ethersphere/bee/v2/pkg/api"
17+
"github.com/ethersphere/bee/v2/pkg/file/redundancy"
1618
"github.com/ethersphere/bee/v2/pkg/jsonhttp"
1719
"github.com/ethersphere/bee/v2/pkg/jsonhttp/jsonhttptest"
1820
"github.com/ethersphere/bee/v2/pkg/log"
@@ -408,3 +410,68 @@ func TestBytesDirectUpload(t *testing.T) {
408410
}),
409411
)
410412
}
413+
414+
func TestBytesRedundancyLevel(t *testing.T) {
415+
t.Parallel()
416+
417+
client, _, _, _ := newTestServer(t, testServerOptions{
418+
Storer: mockstorer.New(),
419+
Post: mockpost.New(mockpost.WithAcceptAll()),
420+
})
421+
422+
const maxValidLevel = redundancy.PARANOID
423+
424+
tests := []struct {
425+
name string
426+
level int
427+
want *jsonhttp.StatusResponse
428+
}{
429+
{"minimum level (NONE) is valid", int(redundancy.NONE), nil},
430+
{"maximum valid level (PARANOID) is valid", int(maxValidLevel), nil},
431+
{
432+
"level below minimum is invalid", int(-1),
433+
&jsonhttp.StatusResponse{
434+
Code: http.StatusBadRequest,
435+
Message: "invalid header params",
436+
Reasons: []jsonhttp.Reason{
437+
{
438+
Field: "Swarm-Redundancy-Level",
439+
Error: "invalid syntax",
440+
},
441+
},
442+
},
443+
},
444+
{
445+
"level above maximum is invalid", int(maxValidLevel + 1),
446+
&jsonhttp.StatusResponse{
447+
Code: http.StatusBadRequest,
448+
Message: "invalid header params",
449+
Reasons: []jsonhttp.Reason{
450+
{
451+
Field: "swarm-redundancy-level",
452+
Error: fmt.Sprintf("want redundancy level to be between %d and %d", int(redundancy.NONE), int(redundancy.PARANOID)),
453+
},
454+
},
455+
},
456+
},
457+
}
458+
459+
for _, tt := range tests {
460+
t.Run(tt.name, func(t *testing.T) {
461+
opts := []jsonhttptest.Option{
462+
jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"),
463+
jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr),
464+
jsonhttptest.WithRequestHeader(api.SwarmRedundancyLevelHeader, strconv.Itoa(tt.level)),
465+
jsonhttptest.WithRequestBody(bytes.NewReader([]byte("test"))),
466+
}
467+
var statusCode int
468+
if tt.want == nil {
469+
statusCode = http.StatusCreated
470+
} else {
471+
statusCode = tt.want.Code
472+
opts = append(opts, jsonhttptest.WithExpectedJSONResponse(*tt.want))
473+
}
474+
jsonhttptest.Request(t, client, http.MethodPost, "/bytes", statusCode, opts...)
475+
})
476+
}
477+
}

pkg/api/bzz.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func (s *Service) bzzUploadHandler(w http.ResponseWriter, r *http.Request) {
7272
Deferred *bool `map:"Swarm-Deferred-Upload"`
7373
Encrypt bool `map:"Swarm-Encrypt"`
7474
IsDir bool `map:"Swarm-Collection"`
75-
RLevel redundancy.Level `map:"Swarm-Redundancy-Level"`
75+
RLevel redundancy.Level `map:"Swarm-Redundancy-Level" validate:"rLevel"`
7676
Act bool `map:"Swarm-Act"`
7777
HistoryAddress swarm.Address `map:"Swarm-Act-History-Address"`
7878
}{}
@@ -480,7 +480,7 @@ func (s *Service) serveReference(logger log.Logger, address swarm.Address, pathV
480480
Cache *bool `map:"Swarm-Cache"`
481481
Strategy *getter.Strategy `map:"Swarm-Redundancy-Strategy"`
482482
FallbackMode *bool `map:"Swarm-Redundancy-Fallback-Mode"`
483-
RLevel *redundancy.Level `map:"Swarm-Redundancy-Level"`
483+
RLevel *redundancy.Level `map:"Swarm-Redundancy-Level" validate:"omitempty,rLevel"`
484484
ChunkRetrievalTimeout *string `map:"Swarm-Chunk-Retrieval-Timeout"`
485485
}{}
486486

@@ -693,7 +693,7 @@ func (s *Service) serveManifestEntry(
693693
func (s *Service) downloadHandler(logger log.Logger, w http.ResponseWriter, r *http.Request, reference swarm.Address, additionalHeaders http.Header, etag, headersOnly bool, rootCh swarm.Chunk) {
694694
headers := struct {
695695
Strategy *getter.Strategy `map:"Swarm-Redundancy-Strategy"`
696-
RLevel *redundancy.Level `map:"Swarm-Redundancy-Level"`
696+
RLevel *redundancy.Level `map:"Swarm-Redundancy-Level" validate:"omitempty,rLevel"`
697697
FallbackMode *bool `map:"Swarm-Redundancy-Fallback-Mode"`
698698
ChunkRetrievalTimeout *string `map:"Swarm-Chunk-Retrieval-Timeout"`
699699
LookaheadBufferSize *int `map:"Swarm-Lookahead-Buffer-Size"`

pkg/api/bzz_test.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,3 +1178,144 @@ func TestBzzDownloadHeaders(t *testing.T) {
11781178
jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "text/html; charset=utf-8"),
11791179
)
11801180
}
1181+
1182+
func TestBzzUploadRedundancyLevel(t *testing.T) {
1183+
t.Parallel()
1184+
1185+
client, _, _, _ := newTestServer(t, testServerOptions{
1186+
Storer: mockstorer.New(),
1187+
Post: mockpost.New(mockpost.WithAcceptAll()),
1188+
})
1189+
1190+
const maxValidLevel = redundancy.PARANOID
1191+
1192+
tests := []struct {
1193+
name string
1194+
level int
1195+
want *jsonhttp.StatusResponse
1196+
}{
1197+
{"minimum level (NONE) is valid", int(redundancy.NONE), nil},
1198+
{"maximum valid level (PARANOID) is valid", int(maxValidLevel), nil},
1199+
{
1200+
"level below minimum is invalid", int(-1),
1201+
&jsonhttp.StatusResponse{
1202+
Code: http.StatusBadRequest,
1203+
Message: "invalid header params",
1204+
Reasons: []jsonhttp.Reason{
1205+
{
1206+
Field: "Swarm-Redundancy-Level",
1207+
Error: "invalid syntax",
1208+
},
1209+
},
1210+
},
1211+
},
1212+
{
1213+
"level above maximum is invalid", int(maxValidLevel + 1),
1214+
&jsonhttp.StatusResponse{
1215+
Code: http.StatusBadRequest,
1216+
Message: "invalid header params",
1217+
Reasons: []jsonhttp.Reason{
1218+
{
1219+
Field: "swarm-redundancy-level",
1220+
Error: fmt.Sprintf("want redundancy level to be between %d and %d", int(redundancy.NONE), int(redundancy.PARANOID)),
1221+
},
1222+
},
1223+
},
1224+
},
1225+
}
1226+
1227+
for _, tt := range tests {
1228+
t.Run(tt.name, func(t *testing.T) {
1229+
opts := []jsonhttptest.Option{
1230+
jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/plain"),
1231+
jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"),
1232+
jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr),
1233+
jsonhttptest.WithRequestHeader(api.SwarmRedundancyLevelHeader, strconv.Itoa(tt.level)),
1234+
jsonhttptest.WithRequestBody(bytes.NewReader([]byte("test"))),
1235+
}
1236+
var statusCode int
1237+
if tt.want == nil {
1238+
statusCode = http.StatusCreated
1239+
} else {
1240+
statusCode = tt.want.Code
1241+
opts = append(opts, jsonhttptest.WithExpectedJSONResponse(*tt.want))
1242+
}
1243+
jsonhttptest.Request(t, client, http.MethodPost, "/bzz", statusCode, opts...)
1244+
})
1245+
}
1246+
}
1247+
1248+
func TestBzzDownloadRedundancyLevel(t *testing.T) {
1249+
t.Parallel()
1250+
1251+
client, _, _, _ := newTestServer(t, testServerOptions{
1252+
Storer: mockstorer.New(),
1253+
Post: mockpost.New(mockpost.WithAcceptAll()),
1254+
})
1255+
1256+
testData := []byte("test download redundancy level")
1257+
var resp api.BzzUploadResponse
1258+
jsonhttptest.Request(t, client, http.MethodPost, "/bzz", http.StatusCreated,
1259+
jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/plain"),
1260+
jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"),
1261+
jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr),
1262+
jsonhttptest.WithRequestBody(bytes.NewReader(testData)),
1263+
jsonhttptest.WithUnmarshalJSONResponse(&resp),
1264+
)
1265+
1266+
const maxValidLevel = redundancy.PARANOID
1267+
1268+
tests := []struct {
1269+
name string
1270+
level int
1271+
want *jsonhttp.StatusResponse
1272+
}{
1273+
{"minimum level (NONE) is valid", int(redundancy.NONE), nil},
1274+
{"maximum valid level (PARANOID) is valid", int(maxValidLevel), nil},
1275+
{
1276+
"level below minimum is invalid", int(-1),
1277+
&jsonhttp.StatusResponse{
1278+
Code: http.StatusBadRequest,
1279+
Message: "invalid header params",
1280+
Reasons: []jsonhttp.Reason{
1281+
{
1282+
Field: "Swarm-Redundancy-Level",
1283+
Error: "invalid syntax",
1284+
},
1285+
},
1286+
},
1287+
},
1288+
{
1289+
"level above maximum is invalid", int(maxValidLevel + 1),
1290+
&jsonhttp.StatusResponse{
1291+
Code: http.StatusBadRequest,
1292+
Message: "invalid header params",
1293+
Reasons: []jsonhttp.Reason{
1294+
{
1295+
Field: "swarm-redundancy-level",
1296+
Error: fmt.Sprintf("want redundancy level to be between %d and %d", int(redundancy.NONE), int(redundancy.PARANOID)),
1297+
},
1298+
},
1299+
},
1300+
},
1301+
}
1302+
1303+
for _, tt := range tests {
1304+
t.Run(tt.name, func(t *testing.T) {
1305+
opts := []jsonhttptest.Option{
1306+
jsonhttptest.WithRequestHeader(api.SwarmRedundancyLevelHeader, strconv.Itoa(tt.level)),
1307+
}
1308+
var statusCode int
1309+
if tt.want == nil {
1310+
statusCode = http.StatusOK
1311+
opts = append(opts,
1312+
jsonhttptest.WithExpectedResponse(testData),
1313+
)
1314+
} else {
1315+
statusCode = tt.want.Code
1316+
opts = append(opts, jsonhttptest.WithExpectedJSONResponse(*tt.want))
1317+
}
1318+
jsonhttptest.Request(t, client, http.MethodGet, "/bzz/"+resp.Reference.String(), statusCode, opts...)
1319+
})
1320+
}
1321+
}

pkg/api/validation.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2026 The Swarm Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package api
6+
7+
import (
8+
"fmt"
9+
10+
"github.com/ethersphere/bee/v2/pkg/file/redundancy"
11+
"github.com/go-playground/validator/v10"
12+
)
13+
14+
const (
15+
RedundancyLevelTag = "rLevel"
16+
)
17+
18+
// setupValidation configures custom validation rules and their custom error messages.
19+
func (s *Service) setupValidation() {
20+
err := s.validate.RegisterValidation(RedundancyLevelTag, func(fl validator.FieldLevel) bool {
21+
level := redundancy.Level(fl.Field().Uint())
22+
return level.Validate()
23+
})
24+
if err != nil {
25+
s.logger.Error(err, "failed to register validation")
26+
panic(err)
27+
}
28+
29+
s.customValidationMessages = map[string]func(err validator.FieldError) error{
30+
RedundancyLevelTag: func(err validator.FieldError) error {
31+
return fmt.Errorf("want redundancy level to be between %d and %d", int(redundancy.NONE), int(redundancy.PARANOID))
32+
},
33+
}
34+
}

pkg/file/redundancy/level.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ const (
2929
PARANOID
3030
)
3131

32+
// Validate validates the redundancy level
33+
func (l Level) Validate() bool {
34+
return l >= NONE && l <= PARANOID
35+
}
36+
3237
// GetParities returns number of parities based on appendix F table 5
3338
func (l Level) GetParities(shards int) int {
3439
et, err := l.getErasureTable()

0 commit comments

Comments
 (0)