package native_test import ( "bufio" "context" "errors" "fmt" "io" "net" "net/http" "strings" "sync/atomic" "testing" "time" omni "git.yuntuo.hk/k2safe/go-omni" "git.yuntuo.hk/k2safe/go-omni/transport/native" netrouter "git.yuntuo.hk/k2safe/go-omni/transport/nethttp" ) func TestServerKeepAliveAndApplicationBridge(t *testing.T) { application := omni.NewWithRouter(netrouter.New()) application.GET("/hello", func(ctx *omni.Context) error { return ctx.OK(map[string]string{"message": "hello"}) }) server, listener, done := startServer(t, application) defer stopServer(t, server, listener, done) conn, err := net.Dial("tcp", listener.Addr().String()) if err != nil { t.Fatal(err) } defer conn.Close() reader := bufio.NewReader(conn) for index, closeAfter := range []bool{false, true} { connectionHeader := "keep-alive" if closeAfter { connectionHeader = "close" } fmt.Fprintf(conn, "GET /hello HTTP/1.1\r\nHost: test\r\nConnection: %s\r\n\r\n", connectionHeader) request := &http.Request{Method: http.MethodGet} response, err := http.ReadResponse(reader, request) if err != nil { t.Fatalf("response %d: %v", index, err) } body, err := io.ReadAll(response.Body) response.Body.Close() if err != nil || response.StatusCode != http.StatusOK || !strings.Contains(string(body), "\"message\":\"hello\"") { t.Fatalf("response %d: status=%d body=%s error=%v", index, response.StatusCode, body, err) } } } func TestServerRejectsUnsupportedBodyAndLimits(t *testing.T) { application := omni.NewWithRouter(netrouter.New()) application.POST("/body", func(ctx *omni.Context) error { return ctx.OK("ok") }) server := native.New(application) server.MaxBodyBytes = 4 listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } done := make(chan error, 1) go func() { done <- server.Serve(context.Background(), listener) }() waitForServer(t, listener.Addr().String()) defer stopServer(t, server, listener, done) for _, requestText := range []string{ "POST /body HTTP/1.1\r\nHost: test\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n0\r\n\r\n", "POST /body HTTP/1.1\r\nHost: test\r\nContent-Length: 5\r\n\r\ntest!", } { conn, err := net.Dial("tcp", listener.Addr().String()) if err != nil { t.Fatal(err) } _, _ = io.WriteString(conn, requestText) response, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: http.MethodPost}) if err != nil { conn.Close() t.Fatal(err) } response.Body.Close() if response.StatusCode != http.StatusNotImplemented && response.StatusCode != http.StatusRequestEntityTooLarge { t.Fatalf("unexpected protocol status %d", response.StatusCode) } conn.Close() } } func TestServerSlowBodyTimesOutBeforeHandler(t *testing.T) { var handlerCalls atomic.Int32 application := omni.NewWithRouter(netrouter.New()) application.POST("/body", func(ctx *omni.Context) error { handlerCalls.Add(1) return ctx.OK("ok") }) server := native.New(application) server.ReadTimeout = 30 * time.Millisecond server.IdleTimeout = time.Second listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } done := make(chan error, 1) go func() { done <- server.Serve(context.Background(), listener) }() waitForServer(t, listener.Addr().String()) defer stopServer(t, server, listener, done) conn, err := net.Dial("tcp", listener.Addr().String()) if err != nil { t.Fatal(err) } defer conn.Close() conn.SetReadDeadline(time.Now().Add(time.Second)) _, _ = io.WriteString(conn, "POST /body HTTP/1.1\r\nHost: test\r\nContent-Length: 4\r\n\r\na") buffer := make([]byte, 1) if _, err := conn.Read(buffer); err == nil { t.Fatal("expected slow request body to time out and close") } if handlerCalls.Load() != 0 { t.Fatal("handler ran before the request body was complete") } } func TestServerPipelinedRequestsAndLargeBody(t *testing.T) { application := omni.NewWithRouter(netrouter.New()) application.POST("/body", func(ctx *omni.Context) error { body, err := io.ReadAll(ctx.Request().Body) if err != nil { return err } return ctx.OK(map[string]int{"length": len(body)}) }) application.GET("/health", func(ctx *omni.Context) error { return ctx.OK("ok") }) server, listener, done := startServer(t, application) defer stopServer(t, server, listener, done) body := strings.Repeat("x", 64<<10) conn, err := net.Dial("tcp", listener.Addr().String()) if err != nil { t.Fatal(err) } defer conn.Close() reader := bufio.NewReader(conn) request := fmt.Sprintf("POST /body HTTP/1.1\r\nHost: test\r\nConnection: keep-alive\r\nContent-Length: %d\r\n\r\n%sGET /health HTTP/1.1\r\nHost: test\r\nConnection: close\r\n\r\n", len(body), body) if _, err := io.WriteString(conn, request); err != nil { t.Fatal(err) } first, err := http.ReadResponse(reader, &http.Request{Method: http.MethodPost}) if err != nil { t.Fatal(err) } firstBody, err := io.ReadAll(first.Body) first.Body.Close() if err != nil || first.StatusCode != http.StatusOK || !strings.Contains(string(firstBody), `"length":65536`) { t.Fatalf("unexpected large body response: status=%d body=%s error=%v", first.StatusCode, firstBody, err) } second, err := http.ReadResponse(reader, &http.Request{Method: http.MethodGet}) if err != nil { t.Fatal(err) } secondBody, err := io.ReadAll(second.Body) second.Body.Close() if err != nil || second.StatusCode != http.StatusOK || !strings.Contains(string(secondBody), `"ok"`) { t.Fatalf("unexpected pipelined response: status=%d body=%s error=%v", second.StatusCode, secondBody, err) } } func TestServerProtocolBoundaries(t *testing.T) { application := omni.NewWithRouter(netrouter.New()) application.GET("/health", func(ctx *omni.Context) error { return ctx.OK("ok") }) server, listener, done := startServer(t, application) defer stopServer(t, server, listener, done) tests := []struct { name string request string status int }{ { name: "missing host", request: "GET /health HTTP/1.1\r\n\r\n", status: http.StatusBadRequest, }, { name: "duplicate host", request: "GET /health HTTP/1.1\r\nHost: one\r\nHost: two\r\n\r\n", status: http.StatusBadRequest, }, { name: "unsupported version", request: "GET /health HTTP/2.0\r\nHost: test\r\n\r\n", status: http.StatusHTTPVersionNotSupported, }, { name: "malformed header", request: "GET /health HTTP/1.1\r\nHost: test\r\nBroken\r\n\r\n", status: http.StatusBadRequest, }, { name: "folded header", request: "GET /health HTTP/1.1\r\nHost: test\r\n X-Bad: value\r\n\r\n", status: http.StatusBadRequest, }, { name: "control header value", request: "GET /health HTTP/1.1\r\nHost: test\r\nX-Bad: value\x01\r\n\r\n", status: http.StatusBadRequest, }, { name: "chunked", request: "POST /health HTTP/1.1\r\nHost: test\r\nTransfer-Encoding: chunked\r\n\r\n", status: http.StatusNotImplemented, }, { name: "expectation", request: "GET /health HTTP/1.1\r\nHost: test\r\nExpect: 100-continue\r\n\r\n", status: http.StatusExpectationFailed, }, { name: "conflicting content length", request: "POST /health HTTP/1.1\r\nHost: test\r\nContent-Length: 1\r\nContent-Length: 2\r\n\r\na", status: http.StatusBadRequest, }, { name: "invalid content length", request: "POST /health HTTP/1.1\r\nHost: test\r\nContent-Length: nope\r\n\r\n", status: http.StatusBadRequest, }, { name: "malformed target", request: "GET health HTTP/1.1\r\nHost: test\r\n\r\n", status: http.StatusBadRequest, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { response := rawResponse(t, listener.Addr().String(), test.request) defer response.Body.Close() if response.StatusCode != test.status { t.Fatalf("expected status %d, got %d", test.status, response.StatusCode) } if !response.Close { t.Fatal("protocol error should close the connection") } }) } } func TestServerHTTP10KeepAliveAndRequestMetadata(t *testing.T) { application := omni.NewWithRouter(netrouter.New()) application.GET("/metadata", func(ctx *omni.Context) error { return ctx.OK(map[string]string{ "method": ctx.Request().Method, "host": ctx.Request().Host, }) }) server, listener, done := startServer(t, application) defer stopServer(t, server, listener, done) conn, err := net.Dial("tcp", listener.Addr().String()) if err != nil { t.Fatal(err) } defer conn.Close() reader := bufio.NewReader(conn) _, _ = io.WriteString(conn, "GET /metadata HTTP/1.0\r\nHost: legacy\r\nConnection: keep-alive\r\n\r\n") response, err := http.ReadResponse(reader, &http.Request{Method: http.MethodGet}) if err != nil { t.Fatal(err) } body, err := io.ReadAll(response.Body) response.Body.Close() if err != nil || response.StatusCode != http.StatusOK || response.Close || !strings.Contains(string(body), `"host":"legacy"`) { t.Fatalf("unexpected HTTP/1.0 response: close=%v status=%d body=%s error=%v", response.Close, response.StatusCode, body, err) } _, _ = io.WriteString(conn, "GET /metadata HTTP/1.0\r\nHost: legacy\r\n\r\n") response, err = http.ReadResponse(reader, &http.Request{Method: http.MethodGet}) if err != nil { t.Fatal(err) } response.Body.Close() if !response.Close { t.Fatal("HTTP/1.0 response without keep-alive should close") } } func TestServerSuppressesBodiesForHeadAndNoContent(t *testing.T) { application := omni.NewWithRouter(netrouter.New()) application.Handle(http.MethodHead, "/head", func(ctx *omni.Context) error { return ctx.OK("hidden") }) application.GET("/empty", func(ctx *omni.Context) error { ctx.Raw().WriteHeader(http.StatusNoContent) return nil }) server, listener, done := startServer(t, application) defer stopServer(t, server, listener, done) headResponse := rawResponseMethod(t, listener.Addr().String(), http.MethodHead, "HEAD /head HTTP/1.1\r\nHost: test\r\n\r\n") headBody, err := io.ReadAll(headResponse.Body) headResponse.Body.Close() if err != nil || len(headBody) != 0 || headResponse.StatusCode != http.StatusOK || headResponse.ContentLength <= 0 { t.Fatalf("unexpected HEAD response: status=%d contentLength=%d body=%q error=%v", headResponse.StatusCode, headResponse.ContentLength, headBody, err) } emptyResponse := rawResponse(t, listener.Addr().String(), "GET /empty HTTP/1.1\r\nHost: test\r\n\r\n") emptyBody, err := io.ReadAll(emptyResponse.Body) emptyResponse.Body.Close() if err != nil || len(emptyBody) != 0 || emptyResponse.StatusCode != http.StatusNoContent || emptyResponse.ContentLength != 0 { t.Fatalf("unexpected 204 response: status=%d contentLength=%d body=%q error=%v", emptyResponse.StatusCode, emptyResponse.ContentLength, emptyBody, err) } } func rawResponse(t *testing.T, address, request string) *http.Response { return rawResponseMethod(t, address, http.MethodGet, request) } func rawResponseMethod(t *testing.T, address, method, request string) *http.Response { t.Helper() conn, err := net.Dial("tcp", address) if err != nil { t.Fatal(err) } _, _ = io.WriteString(conn, request) response, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: method}) if err != nil { conn.Close() t.Fatal(err) } response.Body = &closeBody{ReadCloser: response.Body, conn: conn} return response } type closeBody struct { io.ReadCloser conn net.Conn } func (b *closeBody) Close() error { err := b.ReadCloser.Close() return errors.Join(err, b.conn.Close()) } func TestServerShutdownCancelsActiveHandler(t *testing.T) { started := make(chan struct{}) application := omni.NewWithRouter(netrouter.New()) application.GET("/wait", func(ctx *omni.Context) error { close(started) <-ctx.Context().Done() return ctx.Context().Err() }) server, listener, done := startServer(t, application) conn, err := net.Dial("tcp", listener.Addr().String()) if err != nil { t.Fatal(err) } _, _ = io.WriteString(conn, "GET /wait HTTP/1.1\r\nHost: test\r\nConnection: close\r\n\r\n") select { case <-started: case <-time.After(2 * time.Second): t.Fatal("handler did not start") } shutdownContext, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() if err := server.Shutdown(shutdownContext); err != nil { t.Fatal(err) } response, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: http.MethodGet}) if err != nil { t.Fatal(err) } response.Body.Close() if response.StatusCode != http.StatusInternalServerError { t.Fatalf("expected canceled handler to become 500, got %d", response.StatusCode) } conn.Close() select { case err := <-done: if err != nil && !errors.Is(err, net.ErrClosed) { t.Fatal(err) } case <-time.After(2 * time.Second): t.Fatal("server did not stop") } } func TestServerConcurrentRequestLimit(t *testing.T) { started := make(chan struct{}) release := make(chan struct{}) application := omni.NewWithRouter(netrouter.New()) application.GET("/work", func(ctx *omni.Context) error { close(started) <-release return ctx.OK("done") }) server := native.New(application) server.MaxConcurrentRequests = 1 listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } done := make(chan error, 1) go func() { done <- server.Serve(context.Background(), listener) }() waitForServer(t, listener.Addr().String()) defer func() { stopServer(t, server, listener, done) }() first, err := net.Dial("tcp", listener.Addr().String()) if err != nil { t.Fatal(err) } defer first.Close() _, _ = io.WriteString(first, "GET /work HTTP/1.1\r\nHost: test\r\nConnection: close\r\n\r\n") select { case <-started: case <-time.After(2 * time.Second): t.Fatal("first handler did not start") } second, err := net.Dial("tcp", listener.Addr().String()) if err != nil { t.Fatal(err) } defer second.Close() _, _ = io.WriteString(second, "GET /work HTTP/1.1\r\nHost: test\r\nConnection: close\r\n\r\n") response, err := http.ReadResponse(bufio.NewReader(second), &http.Request{Method: http.MethodGet}) if err != nil { t.Fatal(err) } response.Body.Close() if response.StatusCode != http.StatusServiceUnavailable { t.Fatalf("expected 503 at concurrency limit, got %d", response.StatusCode) } close(release) response, err = http.ReadResponse(bufio.NewReader(first), &http.Request{Method: http.MethodGet}) if err != nil { t.Fatal(err) } response.Body.Close() if response.StatusCode != http.StatusOK { t.Fatalf("expected first request to complete, got %d", response.StatusCode) } } func TestServerConnectionLimitReleasesAfterRequest(t *testing.T) { started := make(chan struct{}) release := make(chan struct{}) application := omni.NewWithRouter(netrouter.New()) application.GET("/hold", func(ctx *omni.Context) error { close(started) <-release return ctx.OK("done") }) application.GET("/health", func(ctx *omni.Context) error { return ctx.OK("ok") }) server := native.New(application) server.MaxConnections = 1 listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } done := make(chan error, 1) go func() { done <- server.Serve(context.Background(), listener) }() waitForServer(t, listener.Addr().String()) defer stopServer(t, server, listener, done) first, err := net.Dial("tcp", listener.Addr().String()) if err != nil { t.Fatal(err) } defer first.Close() _, _ = io.WriteString(first, "GET /hold HTTP/1.1\r\nHost: test\r\nConnection: close\r\n\r\n") select { case <-started: case <-time.After(time.Second): t.Fatal("first request did not start") } second, err := net.Dial("tcp", listener.Addr().String()) if err != nil { t.Fatal(err) } second.SetReadDeadline(time.Now().Add(time.Second)) _, _ = io.WriteString(second, "GET /health HTTP/1.1\r\nHost: test\r\nConnection: close\r\n\r\n") if _, err := second.Read(make([]byte, 1)); err == nil { second.Close() t.Fatal("connection over MaxConnections was not rejected") } second.Close() close(release) firstResponse, err := http.ReadResponse(bufio.NewReader(first), &http.Request{Method: http.MethodGet}) if err != nil { t.Fatal(err) } firstResponse.Body.Close() third, err := net.Dial("tcp", listener.Addr().String()) if err != nil { t.Fatal(err) } defer third.Close() _, _ = io.WriteString(third, "GET /health HTTP/1.1\r\nHost: test\r\nConnection: close\r\n\r\n") thirdResponse, err := http.ReadResponse(bufio.NewReader(third), &http.Request{Method: http.MethodGet}) if err != nil { t.Fatal(err) } thirdResponse.Body.Close() if thirdResponse.StatusCode != http.StatusOK { t.Fatalf("connection slot was not released, got status %d", thirdResponse.StatusCode) } } func TestServerReadHeaderTimeout(t *testing.T) { application := omni.NewWithRouter(netrouter.New()) application.GET("/health", func(ctx *omni.Context) error { return ctx.OK("ok") }) server := native.New(application) server.ReadHeaderTimeout = 25 * time.Millisecond server.IdleTimeout = 0 listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } done := make(chan error, 1) go func() { done <- server.Serve(context.Background(), listener) }() waitForServer(t, listener.Addr().String()) defer stopServer(t, server, listener, done) conn, err := net.Dial("tcp", listener.Addr().String()) if err != nil { t.Fatal(err) } defer conn.Close() conn.SetReadDeadline(time.Now().Add(time.Second)) _, _ = io.WriteString(conn, "GET /health HTTP/1.1\r\nHost: test") buffer := make([]byte, 1) if _, err := conn.Read(buffer); err == nil { t.Fatal("expected incomplete headers to time out and close") } } func TestAppRunWithNativeRunner(t *testing.T) { application := omni.NewWithRouter(netrouter.New()) application.GET("/health", func(ctx *omni.Context) error { return ctx.OK("ok") }) address := freeAddress(t) ctx, cancel := context.WithCancel(context.Background()) defer cancel() done := make(chan error, 1) go func() { done <- application.RunWith(ctx, native.New(nil), address) }() waitForServer(t, address) response, err := http.Get("http://" + address + "/health") if err != nil { t.Fatal(err) } response.Body.Close() if response.StatusCode != http.StatusOK { t.Fatalf("unexpected status %d", response.StatusCode) } cancel() select { case err := <-done: if err != nil { t.Fatal(err) } case <-time.After(3 * time.Second): t.Fatal("RunWith did not stop") } } func startServer(t *testing.T, handler http.Handler) (*native.Server, net.Listener, <-chan error) { t.Helper() listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } server := native.New(handler) done := make(chan error, 1) go func() { done <- server.Serve(context.Background(), listener) }() waitForServer(t, listener.Addr().String()) return server, listener, done } func stopServer(t *testing.T, server *native.Server, listener net.Listener, done <-chan error) { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() _ = server.Shutdown(ctx) select { case <-done: case <-time.After(2 * time.Second): t.Fatal("server did not stop") } _ = listener.Close() } func freeAddress(t *testing.T) string { t.Helper() listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } address := listener.Addr().String() listener.Close() return address } func waitForServer(t *testing.T, address string) { t.Helper() deadline := time.Now().Add(2 * time.Second) for { conn, err := net.DialTimeout("tcp", address, 50*time.Millisecond) if err == nil { conn.Close() return } if time.Now().After(deadline) { t.Fatalf("server did not start: %v", err) } time.Sleep(10 * time.Millisecond) } }