mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 09:40:12 +00:00
234 lines
7.7 KiB
Go
234 lines
7.7 KiB
Go
package fixture
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestFixture_NATEchoHalfClose(t *testing.T) {
|
|
// Given
|
|
backend, err := StartNATHalfCloseEchoBackend()
|
|
requireNoFixtureError(t, err)
|
|
t.Cleanup(func() { requireNoFixtureError(t, backend.Close()) })
|
|
connection, err := net.DialTimeout("tcp", backend.Address(), time.Second)
|
|
requireNoFixtureError(t, err)
|
|
tcpConnection := connection.(*net.TCPConn)
|
|
defer tcpConnection.Close()
|
|
requireNoFixtureError(t, tcpConnection.SetDeadline(time.Now().Add(2*time.Second)))
|
|
requestBody := "half-closed"
|
|
request := fmt.Sprintf("POST /echo?case=half-close HTTP/1.1\r\nHost: nat.invalid\r\nX-AgentCompat-Echo: fixture\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", len(requestBody), requestBody)
|
|
|
|
// When
|
|
_, err = io.WriteString(tcpConnection, request)
|
|
requireNoFixtureError(t, err)
|
|
requireNoFixtureError(t, tcpConnection.CloseWrite())
|
|
response, err := http.ReadResponse(bufio.NewReader(tcpConnection), nil)
|
|
requireNoFixtureError(t, err)
|
|
responseBody, err := io.ReadAll(response.Body)
|
|
requireNoFixtureError(t, err)
|
|
requireNoFixtureError(t, response.Body.Close())
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
record, err := backend.WaitRequest(ctx)
|
|
requireNoFixtureError(t, err)
|
|
|
|
// Then
|
|
const expected = "method=POST\npath=/echo?case=half-close\nhost=nat.invalid\nx-agentcompat-echo=fixture\nbody=half-closed\n"
|
|
if response.StatusCode != http.StatusOK || string(responseBody) != expected {
|
|
t.Fatalf("NAT echo response = %d %q", response.StatusCode, responseBody)
|
|
}
|
|
if !record.RequestHalfClosed {
|
|
t.Fatal("backend responded before observing request half-close")
|
|
}
|
|
if record.Method != "POST" || record.Path != "/echo?case=half-close" || record.Host != "nat.invalid" || record.HeaderValue != "fixture" || string(record.Body) != requestBody {
|
|
t.Fatalf("NAT request record = %+v", record)
|
|
}
|
|
}
|
|
|
|
func TestFixture_NATEcho(t *testing.T) {
|
|
// Given
|
|
backend, err := StartNATEchoBackend()
|
|
requireNoFixtureError(t, err)
|
|
t.Cleanup(func() { requireNoFixtureError(t, backend.Close()) })
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodPut, "http://"+backend.Address()+"/echo?case=ordinary", strings.NewReader("ordinary"))
|
|
requireNoFixtureError(t, err)
|
|
request.Host = "nat.invalid"
|
|
request.Header.Set("X-AgentCompat-Echo", "fixture")
|
|
|
|
// When
|
|
response, err := http.DefaultClient.Do(request)
|
|
requireNoFixtureError(t, err)
|
|
responseBody, err := io.ReadAll(response.Body)
|
|
requireNoFixtureError(t, err)
|
|
requireNoFixtureError(t, response.Body.Close())
|
|
record, err := backend.WaitRequest(ctx)
|
|
requireNoFixtureError(t, err)
|
|
|
|
// Then
|
|
const expected = "method=PUT\npath=/echo?case=ordinary\nhost=nat.invalid\nx-agentcompat-echo=fixture\nbody=ordinary\n"
|
|
if response.StatusCode != http.StatusOK || string(responseBody) != expected {
|
|
t.Fatalf("NAT echo response = %d %q", response.StatusCode, responseBody)
|
|
}
|
|
if record.RequestHalfClosed || record.ResponseHalfClosed || record.Method != http.MethodPut || record.Host != "nat.invalid" {
|
|
t.Fatalf("NAT request record = %+v", record)
|
|
}
|
|
}
|
|
|
|
func TestFixture_NATResponseHalfCloseEcho(t *testing.T) {
|
|
// Given
|
|
backend, err := StartNATResponseHalfCloseEchoBackend()
|
|
requireNoFixtureError(t, err)
|
|
t.Cleanup(func() { requireNoFixtureError(t, backend.Close()) })
|
|
connection, err := net.DialTimeout("tcp", backend.Address(), time.Second)
|
|
requireNoFixtureError(t, err)
|
|
defer connection.Close()
|
|
requireNoFixtureError(t, connection.SetDeadline(time.Now().Add(2*time.Second)))
|
|
|
|
// When
|
|
_, err = io.WriteString(connection, "GET /response-half-close HTTP/1.1\r\nHost: nat.invalid\r\nContent-Length: 0\r\n\r\n")
|
|
requireNoFixtureError(t, err)
|
|
response, err := http.ReadResponse(bufio.NewReader(connection), nil)
|
|
requireNoFixtureError(t, err)
|
|
_, err = io.ReadAll(response.Body)
|
|
requireNoFixtureError(t, err)
|
|
requireNoFixtureError(t, response.Body.Close())
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
record, err := backend.WaitRequest(ctx)
|
|
requireNoFixtureError(t, err)
|
|
|
|
// Then
|
|
if !record.ResponseHalfClosed || record.RequestHalfClosed {
|
|
t.Fatalf("NAT response half-close record = %+v", record)
|
|
}
|
|
}
|
|
|
|
func TestFixture_NATEchoCloseInterruptsIncompleteRequest(t *testing.T) {
|
|
// Given
|
|
backend, err := StartNATEchoBackend()
|
|
requireNoFixtureError(t, err)
|
|
connection, err := net.DialTimeout("tcp", backend.Address(), time.Second)
|
|
requireNoFixtureError(t, err)
|
|
defer connection.Close()
|
|
_, err = io.WriteString(connection, "GET /incomplete HTTP/1.1\r\nHost: nat.invalid\r\n")
|
|
requireNoFixtureError(t, err)
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
requireNoFixtureError(t, backend.WaitConnection(ctx))
|
|
|
|
// When
|
|
closed := make(chan error, 1)
|
|
go func() { closed <- backend.Close() }()
|
|
|
|
// Then
|
|
select {
|
|
case err := <-closed:
|
|
requireNoFixtureError(t, err)
|
|
case <-ctx.Done():
|
|
t.Fatal("NAT echo close did not interrupt incomplete request")
|
|
}
|
|
}
|
|
|
|
func TestFixture_NATEchoCloseTerminatesSockets(t *testing.T) {
|
|
// Given
|
|
backend, err := StartNATHalfCloseEchoBackend()
|
|
requireNoFixtureError(t, err)
|
|
address := backend.Address()
|
|
connection, err := net.DialTimeout("tcp", address, time.Second)
|
|
requireNoFixtureError(t, err)
|
|
requireNoFixtureError(t, connection.SetDeadline(time.Now().Add(time.Second)))
|
|
|
|
// When
|
|
requireNoFixtureError(t, backend.Close())
|
|
|
|
// Then
|
|
buffer := make([]byte, 1)
|
|
if _, err := connection.Read(buffer); err == nil {
|
|
t.Fatal("active NAT socket remained readable after backend close")
|
|
}
|
|
requireNoFixtureError(t, connection.Close())
|
|
if connection, err := net.DialTimeout("tcp", address, 100*time.Millisecond); err == nil {
|
|
_ = connection.Close()
|
|
t.Fatal("NAT listener accepted a connection after backend close")
|
|
}
|
|
}
|
|
|
|
func TestFixture_NATEchoCloseIsConcurrentAndIdempotent(t *testing.T) {
|
|
// Given
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
requireNoFixtureError(t, err)
|
|
closeFailure := errors.New("injected listener close failure")
|
|
backend := newNATEchoBackend(closeErrorListener{Listener: listener, err: closeFailure}, false, false)
|
|
connection, err := net.DialTimeout("tcp", backend.Address(), time.Second)
|
|
requireNoFixtureError(t, err)
|
|
defer connection.Close()
|
|
_, err = io.WriteString(connection, "GET /incomplete HTTP/1.1\r\nHost: nat.invalid\r\n")
|
|
requireNoFixtureError(t, err)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
requireNoFixtureError(t, backend.WaitConnection(ctx))
|
|
|
|
// When
|
|
const callers = 16
|
|
start := make(chan struct{})
|
|
closeErrors := make(chan error, callers)
|
|
var waitGroup sync.WaitGroup
|
|
waitGroup.Add(callers)
|
|
for range callers {
|
|
go func() {
|
|
defer waitGroup.Done()
|
|
<-start
|
|
closeErrors <- backend.Close()
|
|
}()
|
|
}
|
|
close(start)
|
|
waitGroup.Wait()
|
|
close(closeErrors)
|
|
|
|
// Then
|
|
for closeErr := range closeErrors {
|
|
if !errors.Is(closeErr, closeFailure) {
|
|
t.Fatalf("concurrent close error = %v, want %v", closeErr, closeFailure)
|
|
}
|
|
}
|
|
if closeErr := backend.Close(); !errors.Is(closeErr, closeFailure) {
|
|
t.Fatalf("repeated close error = %v, want %v", closeErr, closeFailure)
|
|
}
|
|
}
|
|
|
|
type closeErrorListener struct {
|
|
net.Listener
|
|
err error
|
|
}
|
|
|
|
func (listener closeErrorListener) Close() error {
|
|
_ = listener.Listener.Close()
|
|
return listener.err
|
|
}
|
|
|
|
func (listener closeErrorListener) Accept() (net.Conn, error) {
|
|
connection, err := listener.Listener.Accept()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return alreadyClosedErrorConn{Conn: connection}, nil
|
|
}
|
|
|
|
type alreadyClosedErrorConn struct{ net.Conn }
|
|
|
|
func (connection alreadyClosedErrorConn) Close() error {
|
|
_ = connection.Conn.Close()
|
|
return net.ErrClosed
|
|
}
|