mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 17:50:12 +00:00
226 lines
6.6 KiB
Go
226 lines
6.6 KiB
Go
package fixture
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"sync"
|
|
|
|
"github.com/nezhahq/nezha/pkg/agentcompatcontract"
|
|
)
|
|
|
|
type NATEchoRecord struct {
|
|
Method string
|
|
Path string
|
|
Host string
|
|
HeaderValue string
|
|
Body []byte
|
|
RequestHalfClosed bool
|
|
ResponseHalfClosed bool
|
|
SensitiveHeadersPresent bool
|
|
}
|
|
|
|
type natEchoResult struct {
|
|
record NATEchoRecord
|
|
err error
|
|
}
|
|
|
|
type NATEchoBackend struct {
|
|
listener net.Listener
|
|
results chan natEchoResult
|
|
done chan struct{}
|
|
connectionsReady chan struct{}
|
|
requireRequestHalfClose bool
|
|
halfCloseResponse bool
|
|
closeOnce sync.Once
|
|
closeErr error
|
|
waitGroup sync.WaitGroup
|
|
mutex sync.Mutex
|
|
connections map[net.Conn]struct{}
|
|
closing bool
|
|
}
|
|
|
|
func StartNATEchoBackend() (*NATEchoBackend, error) {
|
|
return startNATEchoBackend(false, false)
|
|
}
|
|
|
|
func StartNATHalfCloseEchoBackend() (*NATEchoBackend, error) {
|
|
return startNATEchoBackend(true, false)
|
|
}
|
|
|
|
func StartNATResponseHalfCloseEchoBackend() (*NATEchoBackend, error) {
|
|
return startNATEchoBackend(false, true)
|
|
}
|
|
|
|
func startNATEchoBackend(requireRequestHalfClose, halfCloseResponse bool) (*NATEchoBackend, error) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("listen for NAT echo: %w", err)
|
|
}
|
|
return newNATEchoBackend(listener, requireRequestHalfClose, halfCloseResponse), nil
|
|
}
|
|
|
|
func newNATEchoBackend(listener net.Listener, requireRequestHalfClose, halfCloseResponse bool) *NATEchoBackend {
|
|
backend := &NATEchoBackend{
|
|
listener: listener,
|
|
results: make(chan natEchoResult, 16),
|
|
done: make(chan struct{}),
|
|
connectionsReady: make(chan struct{}, 16),
|
|
requireRequestHalfClose: requireRequestHalfClose,
|
|
halfCloseResponse: halfCloseResponse,
|
|
connections: make(map[net.Conn]struct{}),
|
|
}
|
|
backend.waitGroup.Add(1)
|
|
go backend.accept()
|
|
return backend
|
|
}
|
|
|
|
func (backend *NATEchoBackend) Address() string {
|
|
return backend.listener.Addr().String()
|
|
}
|
|
|
|
func (backend *NATEchoBackend) WaitRequest(ctx context.Context) (NATEchoRecord, error) {
|
|
select {
|
|
case result := <-backend.results:
|
|
return result.record, result.err
|
|
case <-ctx.Done():
|
|
return NATEchoRecord{}, ctx.Err()
|
|
case <-backend.done:
|
|
return NATEchoRecord{}, errors.New("NAT echo backend closed")
|
|
}
|
|
}
|
|
|
|
func (backend *NATEchoBackend) WaitConnection(ctx context.Context) error {
|
|
select {
|
|
case <-backend.connectionsReady:
|
|
return nil
|
|
case <-ctx.Done():
|
|
return fmt.Errorf("wait for NAT echo connection: %w", ctx.Err())
|
|
case <-backend.done:
|
|
return errors.New("NAT echo backend closed")
|
|
}
|
|
}
|
|
|
|
func (backend *NATEchoBackend) Close() error {
|
|
backend.closeOnce.Do(func() {
|
|
close(backend.done)
|
|
backend.closeErr = normalizeNATEchoCloseError(backend.listener.Close())
|
|
backend.mutex.Lock()
|
|
backend.closing = true
|
|
connections := make([]net.Conn, 0, len(backend.connections))
|
|
for connection := range backend.connections {
|
|
connections = append(connections, connection)
|
|
}
|
|
backend.mutex.Unlock()
|
|
for _, connection := range connections {
|
|
backend.closeErr = errors.Join(backend.closeErr, normalizeNATEchoCloseError(connection.Close()))
|
|
}
|
|
backend.waitGroup.Wait()
|
|
})
|
|
// sync.Once publishes the first shutdown result after cleanup completes, so
|
|
// concurrent and repeated callers observe the same outcome.
|
|
return backend.closeErr
|
|
}
|
|
|
|
func normalizeNATEchoCloseError(err error) error {
|
|
if errors.Is(err, net.ErrClosed) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (backend *NATEchoBackend) accept() {
|
|
defer backend.waitGroup.Done()
|
|
for {
|
|
connection, err := backend.listener.Accept()
|
|
if err != nil {
|
|
if errors.Is(err, net.ErrClosed) {
|
|
return
|
|
}
|
|
backend.publish(natEchoResult{err: fmt.Errorf("accept NAT echo connection: %w", err)})
|
|
return
|
|
}
|
|
backend.mutex.Lock()
|
|
if backend.closing {
|
|
backend.mutex.Unlock()
|
|
_ = connection.Close()
|
|
continue
|
|
}
|
|
backend.connections[connection] = struct{}{}
|
|
backend.waitGroup.Add(1)
|
|
backend.mutex.Unlock()
|
|
select {
|
|
case backend.connectionsReady <- struct{}{}:
|
|
default:
|
|
}
|
|
go backend.handle(connection)
|
|
}
|
|
}
|
|
|
|
func (backend *NATEchoBackend) handle(connection net.Conn) {
|
|
defer backend.waitGroup.Done()
|
|
defer func() {
|
|
backend.mutex.Lock()
|
|
delete(backend.connections, connection)
|
|
backend.mutex.Unlock()
|
|
_ = connection.Close()
|
|
}()
|
|
reader := bufio.NewReader(connection)
|
|
request, err := http.ReadRequest(reader)
|
|
if err != nil {
|
|
backend.publish(natEchoResult{err: fmt.Errorf("read NAT echo request: %w", err)})
|
|
return
|
|
}
|
|
body, err := io.ReadAll(request.Body)
|
|
if closeErr := request.Body.Close(); err == nil {
|
|
err = closeErr
|
|
}
|
|
if err != nil {
|
|
backend.publish(natEchoResult{err: fmt.Errorf("read NAT echo body: %w", err)})
|
|
return
|
|
}
|
|
halfClosed := false
|
|
if backend.requireRequestHalfClose {
|
|
_, halfCloseErr := reader.ReadByte()
|
|
halfClosed = errors.Is(halfCloseErr, io.EOF)
|
|
if halfCloseErr != nil && !halfClosed {
|
|
backend.publish(natEchoResult{err: fmt.Errorf("observe NAT request half-close: %w", halfCloseErr)})
|
|
return
|
|
}
|
|
}
|
|
record := NATEchoRecord{
|
|
Method: request.Method,
|
|
Path: request.URL.RequestURI(),
|
|
Host: request.Host,
|
|
HeaderValue: request.Header.Get("X-AgentCompat-Echo"),
|
|
Body: append([]byte(nil), body...),
|
|
RequestHalfClosed: halfClosed,
|
|
SensitiveHeadersPresent: request.Header.Get(agentcompatcontract.IOStreamCapabilityHeader) != "" || request.Header.Get("Authorization") != "",
|
|
}
|
|
responseBody := fmt.Sprintf("method=%s\npath=%s\nhost=%s\nx-agentcompat-echo=%s\nbody=%s\n", record.Method, record.Path, record.Host, record.HeaderValue, record.Body)
|
|
response := fmt.Sprintf("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", len(responseBody), responseBody)
|
|
if _, err := io.WriteString(connection, response); err != nil {
|
|
backend.publish(natEchoResult{err: fmt.Errorf("write NAT echo response: %w", err)})
|
|
return
|
|
}
|
|
if tcpConnection, ok := connection.(*net.TCPConn); ok && backend.halfCloseResponse {
|
|
if err := tcpConnection.CloseWrite(); err != nil {
|
|
backend.publish(natEchoResult{err: fmt.Errorf("half-close NAT echo response: %w", err)})
|
|
return
|
|
}
|
|
record.ResponseHalfClosed = true
|
|
}
|
|
backend.publish(natEchoResult{record: record})
|
|
}
|
|
|
|
func (backend *NATEchoBackend) publish(result natEchoResult) {
|
|
select {
|
|
case backend.results <- result:
|
|
case <-backend.done:
|
|
}
|
|
}
|