@@ -9,16 +9,20 @@ import (
99 "context"
1010 "encoding/hex"
1111 "fmt"
12+ "net"
1213 "net/http"
1314 "net/url"
1415 "strings"
16+ "sync"
1517 "testing"
1618 "time"
1719
1820 "github.com/ethersphere/bee/v2/pkg/api"
1921 "github.com/ethersphere/bee/v2/pkg/cac"
2022 "github.com/ethersphere/bee/v2/pkg/crypto"
2123 "github.com/ethersphere/bee/v2/pkg/gsoc"
24+ "github.com/ethersphere/bee/v2/pkg/jsonhttp"
25+ "github.com/ethersphere/bee/v2/pkg/jsonhttp/jsonhttptest"
2226 "github.com/ethersphere/bee/v2/pkg/log"
2327 mockbatchstore "github.com/ethersphere/bee/v2/pkg/postage/batchstore/mock"
2428 "github.com/ethersphere/bee/v2/pkg/soc"
@@ -243,6 +247,207 @@ func TestGsocWebsocketSocFieldsDeduplication(t *testing.T) {
243247 }
244248}
245249
250+ // TestGsocWebsocketInvalidFieldsHeader verifies that an unknown field name in
251+ // the Swarm-Soc-Fields header is rejected with a 400 Bad Request before the
252+ // websocket upgrade is attempted.
253+ func TestGsocWebsocketInvalidFieldsHeader (t * testing.T ) {
254+ t .Parallel ()
255+
256+ var (
257+ id = make ([]byte , 32 )
258+ gsocSvc = gsoc .New (log .Noop )
259+ addrHex = hex .EncodeToString (id )
260+ batchStore = mockbatchstore .New ()
261+ storer = mockstorer .New ()
262+ )
263+ testutil .CleanupCloser (t , gsocSvc )
264+
265+ client , _ , _ , _ := newTestServer (t , testServerOptions {
266+ Gsoc : gsocSvc ,
267+ Storer : storer ,
268+ BatchStore : batchStore ,
269+ Logger : log .Noop ,
270+ })
271+
272+ jsonhttptest .Request (t , client , http .MethodGet , "/gsoc/subscribe/" + addrHex , http .StatusBadRequest ,
273+ jsonhttptest .WithRequestHeader (api .SwarmSocFieldsHeader , "bogusfield" ),
274+ jsonhttptest .WithExpectedJSONResponse (jsonhttp.StatusResponse {
275+ Message : "invalid soc fields header" ,
276+ Code : http .StatusBadRequest ,
277+ }),
278+ )
279+ }
280+
281+ // TestGsocWebsocketSlowConsumer verifies that when a subscriber cannot keep up
282+ // with incoming GSOC messages, the server closes the connection instead of
283+ // blocking indefinitely or racing on the underlying websocket connection.
284+ //
285+ // The connection is served over an in-memory net.Pipe, which is fully
286+ // synchronous (unbuffered): a write only completes once a matching read
287+ // consumes it. This makes the small dataC buffer overflow deterministically
288+ // as soon as the client stops reading, instead of depending on the size of
289+ // the OS's (possibly very large, auto-tuned) TCP socket buffers.
290+ func TestGsocWebsocketSlowConsumer (t * testing.T ) {
291+ t .Parallel ()
292+
293+ const messageCount = 10
294+
295+ var (
296+ id = make ([]byte , 32 )
297+ batchStore = mockbatchstore .New ()
298+ storer = mockstorer .New ()
299+ gsocSvc = gsoc .New (log .Noop )
300+ svc * api.Service
301+ )
302+ testutil .CleanupCloser (t , gsocSvc )
303+
304+ newTestServer (t , testServerOptions {
305+ Gsoc : gsocSvc ,
306+ Storer : storer ,
307+ BatchStore : batchStore ,
308+ Logger : log .Noop ,
309+ ServiceOut : & svc ,
310+ })
311+
312+ privKey , err := crypto .GenerateSecp256k1Key ()
313+ if err != nil {
314+ t .Fatal (err )
315+ }
316+ signer := crypto .NewDefaultSigner (privKey )
317+ owner , err := signer .EthereumAddress ()
318+ if err != nil {
319+ t .Fatal (err )
320+ }
321+ chunkAddr , _ := soc .CreateAddress (id , owner .Bytes ())
322+
323+ ln := newPipeListener ()
324+ srv := & http.Server {Handler : svc }
325+ testutil .CleanupCloser (t , srv )
326+ go func () { _ = srv .Serve (ln ) }()
327+
328+ clientConn , serverConn := net .Pipe ()
329+ ln .offer (serverConn )
330+
331+ u := url.URL {Scheme : "ws" , Host : "pipe" , Path : "/gsoc/subscribe/" + hex .EncodeToString (chunkAddr .Bytes ())}
332+ dialer := websocket.Dialer {
333+ NetDial : func (_ , _ string ) (net.Conn , error ) { return clientConn , nil },
334+ }
335+ cl , _ , err := dialer .Dial (u .String (), nil )
336+ if err != nil {
337+ t .Fatalf ("client handshake: %v" , err )
338+ }
339+ testutil .CleanupCloser (t , cl )
340+
341+ // never read from cl, so the dataC buffer (cap 2) fills up almost
342+ // immediately: the first message blocks the single writer goroutine
343+ // (nothing reads the pipe), and the next ones queue up and overflow.
344+ for i := range messageCount {
345+ payload := []byte {byte (i )}
346+ ch , _ := cac .New (payload )
347+ socCh := soc .New (id , ch )
348+ signedCh , _ := socCh .Sign (signer )
349+ socCh , _ = soc .FromChunk (signedCh )
350+ gsocSvc .Handle (socCh )
351+ }
352+
353+ if err := cl .SetReadDeadline (time .Now ().Add (longTimeout )); err != nil {
354+ t .Fatal (err )
355+ }
356+
357+ // Drain whatever messages had already been handed to the (synchronous)
358+ // pipe before the overflow was detected; the connection must eventually
359+ // be closed instead of the server delivering every message regardless of
360+ // how far behind the consumer falls.
361+ var readErr error
362+ for i := 0 ; i < messageCount && readErr == nil ; i ++ {
363+ _ , _ , readErr = cl .ReadMessage ()
364+ }
365+ if readErr == nil {
366+ t .Fatal ("expected connection to be closed for a slow consumer" )
367+ }
368+ }
369+
370+ // pipeListener is a net.Listener that hands out pre-established net.Conn
371+ // pairs, so an http.Server can be driven over an in-memory net.Pipe instead
372+ // of a real OS socket.
373+ type pipeListener struct {
374+ connCh chan net.Conn
375+ closed chan struct {}
376+ once sync.Once
377+ }
378+
379+ func newPipeListener () * pipeListener {
380+ return & pipeListener {
381+ connCh : make (chan net.Conn , 1 ),
382+ closed : make (chan struct {}),
383+ }
384+ }
385+
386+ func (l * pipeListener ) offer (conn net.Conn ) { l .connCh <- conn }
387+
388+ func (l * pipeListener ) Accept () (net.Conn , error ) {
389+ select {
390+ case c := <- l .connCh :
391+ return c , nil
392+ case <- l .closed :
393+ return nil , net .ErrClosed
394+ }
395+ }
396+
397+ func (l * pipeListener ) Close () error {
398+ l .once .Do (func () { close (l .closed ) })
399+ return nil
400+ }
401+
402+ func (l * pipeListener ) Addr () net.Addr { return pipeAddr {} }
403+
404+ type pipeAddr struct {}
405+
406+ func (pipeAddr ) Network () string { return "pipe" }
407+ func (pipeAddr ) String () string { return "pipe" }
408+
409+ // TestGsocWebsocketMessageOrdering verifies that sequential Handle calls for
410+ // the same GSOC address are delivered to the subscriber in the same order.
411+ func TestGsocWebsocketMessageOrdering (t * testing.T ) {
412+ t .Parallel ()
413+
414+ const messageCount = 10
415+
416+ var (
417+ id = make ([]byte , 32 )
418+ g , cl , signer , _ = newGsocTest (t , id , 0 )
419+ )
420+
421+ err := cl .SetReadDeadline (time .Now ().Add (longTimeout ))
422+ if err != nil {
423+ t .Fatal (err )
424+ }
425+ cl .SetReadLimit (swarm .ChunkSize )
426+
427+ payloads := make ([][]byte , messageCount )
428+ for i := range payloads {
429+ payloads [i ] = fmt .Appendf (nil , "message-%d" , i )
430+ }
431+
432+ for _ , payload := range payloads {
433+ ch , _ := cac .New (payload )
434+ socCh := soc .New (id , ch )
435+ signedCh , _ := socCh .Sign (signer )
436+ socCh , _ = soc .FromChunk (signedCh )
437+ g .Handle (socCh )
438+ }
439+
440+ for i , want := range payloads {
441+ _ , got , err := cl .ReadMessage ()
442+ if err != nil {
443+ t .Fatalf ("message %d: %v" , i , err )
444+ }
445+ if ! bytes .Equal (got , want ) {
446+ t .Fatalf ("message %d: got %q, want %q" , i , got , want )
447+ }
448+ }
449+ }
450+
246451// TestGsocWebsocketCacheWrappedChunk verifies that the Swarm-Cache-Wrapped-Chunk
247452// header causes the wrapped chunk to be stored in the cache so that it can be
248453// resolved through the bytes endpoint.
0 commit comments