Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
177 lines
4.9 KiB
Go
177 lines
4.9 KiB
Go
//go:build unit
|
|
|
|
package server
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func ingressTestConfig() *config.Config {
|
|
return &config.Config{
|
|
Server: config.ServerConfig{
|
|
Host: "127.0.0.1",
|
|
ReadHeaderTimeout: 1,
|
|
IdleTimeout: 5,
|
|
MaxHeaderBytes: 8 * 1024,
|
|
MaxRequestBodySize: 1024,
|
|
},
|
|
Gateway: config.GatewayConfig{MaxBodySize: 1024},
|
|
}
|
|
}
|
|
|
|
func TestProvideHTTPServerAppliesIngressLimits(t *testing.T) {
|
|
srv := ProvideHTTPServer(ingressTestConfig(), gin.New())
|
|
require.Equal(t, 8*1024, srv.MaxHeaderBytes)
|
|
require.Equal(t, time.Second, srv.ReadHeaderTimeout)
|
|
require.Equal(t, 5*time.Second, srv.IdleTimeout)
|
|
}
|
|
|
|
func TestProvideHTTPServerEnablesBoundedH2C(t *testing.T) {
|
|
cfg := ingressTestConfig()
|
|
cfg.Server.H2C = config.H2CConfig{
|
|
Enabled: true,
|
|
MaxConcurrentStreams: 25,
|
|
IdleTimeout: 30,
|
|
MaxReadFrameSize: 64 * 1024,
|
|
MaxUploadBufferPerConnection: 1024 * 1024,
|
|
MaxUploadBufferPerStream: 256 * 1024,
|
|
}
|
|
srv := ProvideHTTPServer(cfg, gin.New())
|
|
require.NotNil(t, srv.Protocols)
|
|
require.True(t, srv.Protocols.UnencryptedHTTP2())
|
|
require.True(t, srv.Protocols.HTTP1())
|
|
}
|
|
|
|
func TestConfigureTrustedProxies(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
tests := []struct {
|
|
name string
|
|
cfg config.ServerConfig
|
|
want string
|
|
}{
|
|
{
|
|
name: "configured proxy resolves forwarded client",
|
|
cfg: config.ServerConfig{
|
|
TrustedProxies: []string{"9.9.9.9/32"},
|
|
TrustedProxiesConfigured: true,
|
|
},
|
|
want: "1.2.3.4",
|
|
},
|
|
{
|
|
name: "explicit empty list ignores forwarded client",
|
|
cfg: config.ServerConfig{
|
|
TrustedProxiesConfigured: true,
|
|
},
|
|
want: "9.9.9.9",
|
|
},
|
|
{
|
|
name: "invalid proxy list fails closed",
|
|
cfg: config.ServerConfig{
|
|
TrustedProxies: []string{"not-a-cidr"},
|
|
TrustedProxiesConfigured: true,
|
|
},
|
|
want: "9.9.9.9",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
r := gin.New()
|
|
configureTrustedProxies(r, tc.cfg)
|
|
r.GET("/t", func(c *gin.Context) { c.String(http.StatusOK, c.ClientIP()) })
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/t", nil)
|
|
req.RemoteAddr = "9.9.9.9:12345"
|
|
req.Header.Set("X-Forwarded-For", "1.2.3.4")
|
|
r.ServeHTTP(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
require.Equal(t, tc.want, w.Body.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHTTPServerRejectsOversizedHTTP1Header(t *testing.T) {
|
|
r := gin.New()
|
|
r.GET("/", func(c *gin.Context) { c.Status(http.StatusOK) })
|
|
srv := ProvideHTTPServer(ingressTestConfig(), r)
|
|
addr, stop := serveIngressTestServer(t, srv)
|
|
defer stop()
|
|
|
|
conn, err := net.DialTimeout("tcp", addr, time.Second)
|
|
require.NoError(t, err)
|
|
defer func() { _ = conn.Close() }()
|
|
_ = conn.SetDeadline(time.Now().Add(3 * time.Second))
|
|
_, err = io.WriteString(conn, "GET / HTTP/1.1\r\nHost: test\r\nX-Fill: "+strings.Repeat("a", 32*1024)+"\r\n\r\n")
|
|
require.NoError(t, err)
|
|
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
|
|
require.NoError(t, err)
|
|
defer func() { _ = resp.Body.Close() }()
|
|
require.Equal(t, http.StatusRequestHeaderFieldsTooLarge, resp.StatusCode)
|
|
}
|
|
|
|
func TestHTTPServerClosesSlowIncompleteHeader(t *testing.T) {
|
|
r := gin.New()
|
|
r.GET("/", func(c *gin.Context) { c.Status(http.StatusOK) })
|
|
srv := ProvideHTTPServer(ingressTestConfig(), r)
|
|
addr, stop := serveIngressTestServer(t, srv)
|
|
defer stop()
|
|
|
|
conn, err := net.DialTimeout("tcp", addr, time.Second)
|
|
require.NoError(t, err)
|
|
defer func() { _ = conn.Close() }()
|
|
_, err = io.WriteString(conn, "GET / HTTP/1.1\r\nHost: test\r\nX-Slow:")
|
|
require.NoError(t, err)
|
|
time.Sleep(1200 * time.Millisecond)
|
|
_ = conn.SetReadDeadline(time.Now().Add(time.Second))
|
|
_, err = bufio.NewReader(conn).ReadByte()
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestHTTPServerGlobalBodyLimit(t *testing.T) {
|
|
r := gin.New()
|
|
r.POST("/", func(c *gin.Context) {
|
|
_, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
var maxErr *http.MaxBytesError
|
|
if errors.As(err, &maxErr) {
|
|
c.Status(http.StatusRequestEntityTooLarge)
|
|
return
|
|
}
|
|
}
|
|
c.Status(http.StatusOK)
|
|
})
|
|
srv := ProvideHTTPServer(ingressTestConfig(), r)
|
|
req, err := http.NewRequest(http.MethodPost, "/", strings.NewReader(strings.Repeat("x", 1025)))
|
|
require.NoError(t, err)
|
|
rec := httptest.NewRecorder()
|
|
srv.Handler.ServeHTTP(rec, req)
|
|
require.Equal(t, http.StatusRequestEntityTooLarge, rec.Code)
|
|
}
|
|
|
|
func serveIngressTestServer(t *testing.T, srv *http.Server) (string, func()) {
|
|
t.Helper()
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
go func() { _ = srv.Serve(ln) }()
|
|
return ln.Addr().String(), func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
_ = srv.Shutdown(ctx)
|
|
}
|
|
}
|