From 793f52a85d8f1b5660c0618b64a1945a425c76be Mon Sep 17 00:00:00 2001 From: Savely Krendelhoff Date: Sat, 23 Aug 2025 13:37:43 +0700 Subject: [PATCH] [PHASE-7] Implement integration tests --- test/integration/slowloris_test.go | 198 +++++++++++++++++++++++++++++ test/integration/timeout_test.go | 192 ++++++++++++++++++++++++++++ 2 files changed, 390 insertions(+) create mode 100644 test/integration/slowloris_test.go create mode 100644 test/integration/timeout_test.go diff --git a/test/integration/slowloris_test.go b/test/integration/slowloris_test.go new file mode 100644 index 0000000..7f756fa --- /dev/null +++ b/test/integration/slowloris_test.go @@ -0,0 +1,198 @@ +package integration + +import ( + "net" + "testing" + "time" + + "hash-of-wisdom/internal/protocol" + "hash-of-wisdom/internal/server" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSlowlorisProtection_SlowReader(t *testing.T) { + // Setup server with very short read timeout for testing + config := server.DefaultConfig() + config.Address = ":0" + config.Timeouts.Read = 100 * time.Millisecond + config.Timeouts.Write = 5 * time.Second + config.Timeouts.Connection = 15 * time.Second + + srv := setupTestServerWithConfig(t, config) + defer srv.Stop() + + // Connect to server + conn, err := net.Dial("tcp", srv.Address()) + require.NoError(t, err) + defer conn.Close() + + // Send partial message header very slowly (slowloris attack) + _, err = conn.Write([]byte{0x01}) // Challenge request type + require.NoError(t, err) + + // Wait longer than read timeout before sending length + time.Sleep(200 * time.Millisecond) + + // Try to send more data - connection should be timed out + _, err = conn.Write([]byte{0x00, 0x00, 0x00, 0x00}) // Payload length + + // Verify connection is closed by trying to read + buffer := make([]byte, 1024) + conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond)) + _, err = conn.Read(buffer) + assert.Error(t, err, "Connection should be closed due to slow reading") +} + +func TestSlowlorisProtection_SlowWriter(t *testing.T) { + // Setup server with very short write timeout for testing + config := server.DefaultConfig() + config.Address = ":0" + config.Timeouts.Read = 5 * time.Second + config.Timeouts.Write = 100 * time.Millisecond + config.Timeouts.Connection = 15 * time.Second + + srv := setupTestServerWithConfig(t, config) + defer srv.Stop() + + // Connect to server but don't read responses (simulate slow writer client) + conn, err := net.Dial("tcp", srv.Address()) + require.NoError(t, err) + defer conn.Close() + + // Send a complete challenge request + challengeReq := &protocol.ChallengeRequest{} + err = challengeReq.Encode(conn) + require.NoError(t, err) + + // Don't read the response to simulate slow writer + // Server should timeout when trying to write response + time.Sleep(200 * time.Millisecond) + + // Try to send another request - connection should be closed + err = challengeReq.Encode(conn) + assert.Error(t, err, "Connection should be closed due to slow writing") +} + +func TestSlowlorisProtection_ConnectionTimeout(t *testing.T) { + // Setup server with very short connection timeout + config := server.DefaultConfig() + config.Address = ":0" + config.Timeouts.Read = 5 * time.Second + config.Timeouts.Write = 5 * time.Second + config.Timeouts.Connection = 100 * time.Millisecond + + srv := setupTestServerWithConfig(t, config) + defer srv.Stop() + + // Connect to server + conn, err := net.Dial("tcp", srv.Address()) + require.NoError(t, err) + defer conn.Close() + + // Wait longer than connection timeout without sending any data + time.Sleep(200 * time.Millisecond) + + // Try to read from connection - should get EOF or connection reset + buffer := make([]byte, 1024) + conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) + _, err = conn.Read(buffer) + assert.Error(t, err, "Connection should be closed due to connection timeout") +} + +func TestSlowlorisProtection_MultipleSlowConnections(t *testing.T) { + // Setup server with short timeouts + config := server.DefaultConfig() + config.Address = ":0" + config.Timeouts.Read = 50 * time.Millisecond + config.Timeouts.Write = 50 * time.Millisecond + config.Timeouts.Connection = 200 * time.Millisecond + + srv := setupTestServerWithConfig(t, config) + defer srv.Stop() + + // Create multiple slow connections (simulating slowloris attack) + var conns []net.Conn + for i := 0; i < 3; i++ { + conn, err := net.Dial("tcp", srv.Address()) + require.NoError(t, err) + conns = append(conns, conn) + + // Send partial data to trigger slow reader behavior + _, err = conn.Write([]byte{0x01}) // Just message type + require.NoError(t, err) + } + + // Clean up connections + defer func() { + for _, conn := range conns { + conn.Close() + } + }() + + // Wait for read timeouts to kick in + time.Sleep(100 * time.Millisecond) + + // Verify slow connections are closed by trying to read from them + for i, conn := range conns { + buffer := make([]byte, 1024) + conn.SetReadDeadline(time.Now().Add(50 * time.Millisecond)) + _, err := conn.Read(buffer) + assert.Error(t, err, "Slow connection %d should be closed", i) + } +} + +func TestSlowlorisProtection_NormalOperationWithinTimeouts(t *testing.T) { + // Setup server with reasonable timeouts + config := server.DefaultConfig() + config.Address = ":0" + + srv := setupTestServerWithConfig(t, config) + defer srv.Stop() + + // Connect and complete normal flow quickly + conn, err := net.Dial("tcp", srv.Address()) + require.NoError(t, err) + defer conn.Close() + + // Request challenge + challengeReq := &protocol.ChallengeRequest{} + err = challengeReq.Encode(conn) + require.NoError(t, err) + + // Should receive challenge response without timeout + decoder := protocol.NewMessageDecoder() + msg, err := decoder.Decode(conn) + require.NoError(t, err) + assert.Equal(t, protocol.ChallengeResponseType, msg.Type) + assert.Greater(t, msg.PayloadLength, uint32(0), "Challenge payload should not be empty") +} + +func TestSlowlorisProtection_PartialHeaderAttack(t *testing.T) { + // Setup server with short read timeout + config := server.DefaultConfig() + config.Address = ":0" + config.Timeouts.Read = 100 * time.Millisecond + + srv := setupTestServerWithConfig(t, config) + defer srv.Stop() + + // Connect to server + conn, err := net.Dial("tcp", srv.Address()) + require.NoError(t, err) + defer conn.Close() + + // Send only message type byte, then stall + _, err = conn.Write([]byte{0x01}) + require.NoError(t, err) + + // Wait for read timeout + time.Sleep(200 * time.Millisecond) + + // Try to read from connection - should be closed + buffer := make([]byte, 1024) + conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) + _, err = conn.Read(buffer) + assert.Error(t, err, "Connection should be closed due to partial header") +} diff --git a/test/integration/timeout_test.go b/test/integration/timeout_test.go new file mode 100644 index 0000000..18d3570 --- /dev/null +++ b/test/integration/timeout_test.go @@ -0,0 +1,192 @@ +package integration + +import ( + "context" + "net" + "testing" + "time" + + "hash-of-wisdom/internal/lib/sl" + "hash-of-wisdom/internal/pow/challenge" + "hash-of-wisdom/internal/protocol" + "hash-of-wisdom/internal/quotes" + "hash-of-wisdom/internal/server" + "hash-of-wisdom/internal/service" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTCPServer_TimeoutProtection_SlowReader(t *testing.T) { + // Setup server with very short read timeout for testing + config := server.DefaultConfig() + config.Address = ":0" + config.Timeouts.Read = 500 * time.Millisecond + config.Timeouts.Write = 5 * time.Second + config.Timeouts.Connection = 15 * time.Second + srv := setupTestServerWithConfig(t, config) + defer srv.Stop() + + // Connect to server + conn, err := net.Dial("tcp", srv.Address()) + require.NoError(t, err) + defer conn.Close() + + // Send partial message header (just type byte) + _, err = conn.Write([]byte{0x01}) // Challenge request type + require.NoError(t, err) + + // Wait longer than read timeout before sending length + time.Sleep(700 * time.Millisecond) + + // Try to send more data - connection should be timed out + _, err = conn.Write([]byte{0x00, 0x00, 0x00, 0x00}) // Payload length + + // Verify connection is closed by reading + buffer := make([]byte, 1024) + conn.SetReadDeadline(time.Now().Add(1 * time.Second)) + _, err = conn.Read(buffer) + assert.Error(t, err, "Connection should be closed due to slow reading") +} + +func TestTCPServer_TimeoutProtection_ConnectionTimeout(t *testing.T) { + // Setup server with very short connection timeout + config := server.DefaultConfig() + config.Address = ":0" + config.Timeouts.Read = 5 * time.Second + config.Timeouts.Write = 5 * time.Second + config.Timeouts.Connection = 1 * time.Second + srv := setupTestServerWithConfig(t, config) + defer srv.Stop() + + // Connect to server + conn, err := net.Dial("tcp", srv.Address()) + require.NoError(t, err) + defer conn.Close() + + // Wait longer than connection timeout + time.Sleep(1500 * time.Millisecond) + + // Try to read from connection - should get EOF or connection reset + buffer := make([]byte, 1024) + conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond)) + _, err = conn.Read(buffer) + assert.Error(t, err, "Connection should be closed due to timeout") +} + +func TestTCPServer_NormalOperation_WithinTimeouts(t *testing.T) { + srv := setupTestServer(t) + defer srv.Stop() + + // Connect and complete normal flow quickly + conn, err := net.Dial("tcp", srv.Address()) + require.NoError(t, err) + defer conn.Close() + + // Request challenge using new protocol API + challengeReq := &protocol.ChallengeRequest{} + err = challengeReq.Encode(conn) + require.NoError(t, err) + + // Should receive challenge response without timeout + decoder := protocol.NewMessageDecoder() + msg, err := decoder.Decode(conn) + require.NoError(t, err) + assert.Equal(t, protocol.ChallengeResponseType, msg.Type) + assert.Greater(t, msg.PayloadLength, uint32(0), "Challenge payload should not be empty") +} + +func TestTCPServer_MultipleConnections_IndependentTimeouts(t *testing.T) { + config := server.DefaultConfig() + config.Address = ":0" + config.Timeouts.Read = 1 * time.Second + config.Timeouts.Write = 5 * time.Second + config.Timeouts.Connection = 3 * time.Second + srv := setupTestServerWithConfig(t, config) + defer srv.Stop() + + // Start two connections + conn1, err := net.Dial("tcp", srv.Address()) + require.NoError(t, err) + defer conn1.Close() + + conn2, err := net.Dial("tcp", srv.Address()) + require.NoError(t, err) + defer conn2.Close() + + // Conn1: Send complete request quickly + go func() { + req := &protocol.ChallengeRequest{} + req.Encode(conn1) + }() + + // Conn2: Send partial request and stall + conn2.Write([]byte{0x01}) // Just message type + + // Wait for read timeout + time.Sleep(1500 * time.Millisecond) + + // Conn1 should still work, Conn2 should be closed + buffer := make([]byte, 1024) + + // Conn1 should receive response + conn1.SetReadDeadline(time.Now().Add(1 * time.Second)) + n, err := conn1.Read(buffer) + assert.NoError(t, err) + assert.Greater(t, n, 0, "Conn1 should receive response") + + // Conn2 should be closed + conn2.SetReadDeadline(time.Now().Add(500 * time.Millisecond)) + _, err = conn2.Read(buffer) + assert.Error(t, err, "Conn2 should be closed due to timeout") +} + +// Helper function to create test server with default config +func setupTestServer(t *testing.T) *server.TCPServer { + config := server.DefaultConfig() + config.Address = ":0" + return setupTestServerWithConfig(t, config) +} + +// Helper function to create test server with custom config +func setupTestServerWithConfig(t *testing.T, serverConfig *server.Config) *server.TCPServer { + // Create test components + challengeConfig := challenge.TestConfig() + generator := challenge.NewGenerator(challengeConfig) + verifier := challenge.NewVerifier(challengeConfig) + + // Create a simple test quote service + quoteService := &testQuoteService{} + + // Wire up service + genAdapter := service.NewGeneratorAdapter(generator) + wisdomService := service.NewWisdomService(genAdapter, verifier, quoteService) + + // Create server with custom config using functional options + logger := sl.NewMockLogger() + srv := server.NewTCPServer(wisdomService, + server.WithConfig(serverConfig), + server.WithLogger(logger)) + + // Start server + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + err := srv.Start(ctx) + require.NoError(t, err) + + // Give server time to start + time.Sleep(100 * time.Millisecond) + + return srv +} + +// testQuoteService provides test quotes +type testQuoteService struct{} + +func (s *testQuoteService) GetRandomQuote(ctx context.Context) (*quotes.Quote, error) { + return "es.Quote{ + Text: "Test quote for integration testing", + Author: "Test Author", + }, nil +}