// Package native provides GoOmni's first project-owned HTTP/1.1 server. // // It deliberately keeps the public handler boundary as net/http so existing // GoOmni business code remains unchanged while connection management and // protocol I/O move out of net/http.Server. The parser supports Content-Length // and chunked request bodies with configurable size limits. package native import ( "bufio" "bytes" "context" "crypto/tls" "errors" "fmt" "io" "net" "net/http" "net/url" "sort" "strconv" "strings" "sync" "sync/atomic" "time" ) const ( defaultReadHeaderTimeout = 5 * time.Second defaultReadTimeout = 30 * time.Second defaultWriteTimeout = 30 * time.Second defaultIdleTimeout = 60 * time.Second defaultMaxHeaderBytes = 1 << 20 defaultMaxBodyBytes = 8 << 20 defaultReadBufferSize = 4 << 10 maxPooledResponseBytes = 64 << 10 nativeALPNProtocol = "http/1.1" ) // Server is a small HTTP/1.1 server with project-owned connection lifecycle. // It supports keep-alive, graceful shutdown, bounded concurrency and standard // http.Handler values. Zero MaxConnections and MaxConcurrentRequests mean // unlimited; pooled protocol objects are reused automatically. // A Server owns one serving lifecycle; create a new Server to restart. type Server struct { ReadHeaderTimeout time.Duration ReadTimeout time.Duration WriteTimeout time.Duration IdleTimeout time.Duration ShutdownTimeout time.Duration ReadBufferSize int MaxHeaderBytes int MaxBodyBytes int64 MaxConnections int MaxConcurrentRequests int // TLSConfig enables TLS for Run and App.RunWith. ServeTLS can be used // when the listener and TLS configuration are assembled by the caller. TLSConfig *tls.Config mu sync.Mutex handler http.Handler listener net.Listener conns map[*connection]struct{} closing bool cancel context.CancelFunc wg sync.WaitGroup connSlots chan struct{} requestSlots chan struct{} accepted atomic.Uint64 rejectedConns atomic.Uint64 rejectedRequests atomic.Uint64 activeConns atomic.Int64 activeRequests atomic.Int64 readerPool sync.Pool writerPool sync.Pool connectionPool sync.Pool } // Stats is a point-in-time view of the native server lifecycle. Counters are // monotonic unless their name starts with Active. type Stats struct { AcceptedConnections uint64 `json:"acceptedConnections"` // Rejections count admission decisions, even if writing a 503 fails. RejectedConnections uint64 `json:"rejectedConnections"` RejectedRequests uint64 `json:"rejectedRequests"` ActiveConnections int64 `json:"activeConnections"` ActiveRequests int64 `json:"activeRequests"` } // Stats returns native connection and request counters without taking the // server lifecycle mutex, so it is safe to call from a metrics endpoint. func (s *Server) Stats() Stats { if s == nil { return Stats{} } return Stats{ AcceptedConnections: s.accepted.Load(), RejectedConnections: s.rejectedConns.Load(), RejectedRequests: s.rejectedRequests.Load(), ActiveConnections: s.activeConns.Load(), ActiveRequests: s.activeRequests.Load(), } } type connection struct { server *Server conn net.Conn active atomic.Bool closeWhenIdle atomic.Bool closed atomic.Bool counted bool slotHeld bool closeDone chan struct{} } // New creates a native server. The handler may also be supplied later through // App.RunWith, which is useful when the App owns the lifecycle. func New(handler http.Handler) *Server { server := &Server{ handler: handler, ReadHeaderTimeout: defaultReadHeaderTimeout, ReadTimeout: defaultReadTimeout, WriteTimeout: defaultWriteTimeout, IdleTimeout: defaultIdleTimeout, ReadBufferSize: defaultReadBufferSize, MaxHeaderBytes: defaultMaxHeaderBytes, MaxBodyBytes: defaultMaxBodyBytes, conns: make(map[*connection]struct{}), } server.readerPool.New = func() any { return bufio.NewReaderSize(nil, server.readBufferSize()) } server.writerPool.New = func() any { return newResponseWriter("") } server.connectionPool.New = func() any { return &connection{server: server} } return server } func (s *Server) readBufferSize() int { if s.ReadBufferSize > 0 { return s.ReadBufferSize } return defaultReadBufferSize } // Run implements app.Runner without importing the app package. func (s *Server) Run(ctx context.Context, handler http.Handler, address string) error { if handler != nil { s.handler = handler } if s.handler == nil { return errors.New("go-omni/native: nil handler") } if address == "" { address = ":8080" } listener, err := net.Listen("tcp", address) if err != nil { return err } if s.TLSConfig != nil { return s.ServeTLS(ctx, listener, s.TLSConfig) } return s.Serve(ctx, listener) } // RunTLS loads a certificate pair, listens on address and serves native HTTP // over TLS. The server still owns the listener and follows the same shutdown // and connection-limit rules as Run. func (s *Server) RunTLS(ctx context.Context, handler http.Handler, address, certFile, keyFile string) error { if handler != nil { s.handler = handler } if s.handler == nil { return errors.New("go-omni/native: nil handler") } if address == "" { address = ":8443" } certificate, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return err } listener, err := net.Listen("tcp", address) if err != nil { return err } return s.ServeTLS(ctx, listener, &tls.Config{ Certificates: []tls.Certificate{certificate}, MinVersion: tls.VersionTLS12, }) } // ServeTLS wraps listener with a TLS listener and serves native HTTP. The // caller owns the TLS configuration value; it must contain a certificate or // a certificate callback accepted by crypto/tls. func (s *Server) ServeTLS(ctx context.Context, listener net.Listener, config *tls.Config) error { if listener == nil { return errors.New("go-omni/native: nil listener") } if config == nil { return errors.New("go-omni/native: nil TLS config") } config, err := prepareTLSConfig(config) if err != nil { _ = listener.Close() return err } return s.Serve(ctx, tls.NewListener(listener, config)) } func prepareTLSConfig(config *tls.Config) (*tls.Config, error) { clone := config.Clone() if len(clone.NextProtos) == 0 { clone.NextProtos = []string{nativeALPNProtocol} } for _, protocol := range clone.NextProtos { if protocol != nativeALPNProtocol { return nil, fmt.Errorf("go-omni/native: unsupported TLS ALPN protocol %q; only %q is supported", protocol, nativeALPNProtocol) } } return clone, nil } // Serve accepts connections from listener until the context is canceled or an // unrecoverable accept error occurs. The listener is owned by the Server. func (s *Server) Serve(ctx context.Context, listener net.Listener) error { if listener == nil { return errors.New("go-omni/native: nil listener") } if s.handler == nil { return errors.New("go-omni/native: nil handler") } if ctx == nil { ctx = context.Background() } s.mu.Lock() if s.cancel != nil { s.mu.Unlock() return errors.New("go-omni/native: server has already been started") } s.listener = listener s.closing = false if s.MaxConnections > 0 { s.connSlots = make(chan struct{}, s.MaxConnections) } else { s.connSlots = nil } if s.MaxConcurrentRequests > 0 { s.requestSlots = make(chan struct{}, s.MaxConcurrentRequests) } else { s.requestSlots = nil } baseCtx, cancel := context.WithCancel(context.Background()) s.cancel = cancel s.mu.Unlock() defer cancel() stopWatcher, watcherDone := make(chan struct{}), make(chan struct{}) go func() { defer close(watcherDone) select { case <-ctx.Done(): // Wake Accept; the serving goroutine owns draining and its error. _ = listener.Close() case <-stopWatcher: } }() var serveErr error for { conn, err := listener.Accept() if err != nil { if ctx.Err() != nil || s.isClosing() { break } serveErr = err break } s.accepted.Add(1) if slots := s.connSlots; slots != nil { select { case slots <- struct{}{}: default: s.rejectedConns.Add(1) _ = conn.Close() continue } } state := s.connectionPool.Get().(*connection) state.server = s state.conn = conn state.active.Store(false) state.closeWhenIdle.Store(false) state.closed.Store(false) state.counted = true state.slotHeld = s.connSlots != nil state.closeDone = make(chan struct{}) s.activeConns.Add(1) s.mu.Lock() if s.closing { s.mu.Unlock() state.close() s.recycleConnection(state) continue } s.conns[state] = struct{}{} s.wg.Add(1) s.mu.Unlock() go s.serveConnection(baseCtx, state) } close(stopWatcher) <-watcherDone timeout := s.shutdownTimeout() shutdownCtx, cancel := context.WithTimeout(context.Background(), timeout) shutdownErr := s.Shutdown(shutdownCtx) cancel() return errors.Join(shutdownErr, serveErr) } // Shutdown stops accepting requests and waits for active handlers to finish. // On timeout, request contexts are canceled and active sockets are closed. func (s *Server) Shutdown(ctx context.Context) error { if ctx == nil { ctx = context.Background() } s.mu.Lock() s.closing = true cancelRequests := s.cancel listener := s.listener states := make([]*connection, 0, len(s.conns)) for state := range s.conns { state.closeWhenIdle.Store(true) states = append(states, state) } s.listener = nil s.mu.Unlock() if listener != nil { _ = listener.Close() } for _, state := range states { if !state.active.Load() { state.close() } } done := make(chan struct{}) go func() { s.wg.Wait() close(done) }() select { case <-done: return nil case <-ctx.Done(): if cancelRequests != nil { cancelRequests() } for _, state := range states { state.close() } // Handlers must cooperate with cancellation. Waiting here would make // an uncooperative handler defeat the caller's shutdown deadline. return ctx.Err() } } func (s *Server) shutdownTimeout() time.Duration { if s.ShutdownTimeout > 0 { return s.ShutdownTimeout } if value := s.WriteTimeout; value > 0 { return value } return defaultWriteTimeout } func (s *Server) isClosing() bool { s.mu.Lock() defer s.mu.Unlock() return s.closing } func (s *Server) serveConnection(baseCtx context.Context, state *connection) { defer s.wg.Done() defer func() { state.close() s.recycleConnection(state) }() reader := s.readerPool.Get().(*bufio.Reader) reader.Reset(state.conn) defer func() { reader.Reset(nil) s.readerPool.Put(reader) }() firstRequest := true for { readTimeout := s.ReadHeaderTimeout if readTimeout <= 0 { readTimeout = defaultReadHeaderTimeout } if !firstRequest && s.IdleTimeout > 0 { readTimeout = s.IdleTimeout } else if s.IdleTimeout > 0 && s.IdleTimeout < readTimeout { readTimeout = s.IdleTimeout } _ = state.conn.SetReadDeadline(time.Now().Add(readTimeout)) request, closeAfter, err := readRequest(reader, state.conn, s.MaxHeaderBytes, s.MaxBodyBytes, func() { if s.ReadTimeout > 0 { _ = state.conn.SetReadDeadline(time.Now().Add(s.ReadTimeout)) } else { _ = state.conn.SetReadDeadline(time.Time{}) } }) if err != nil { if !errors.Is(err, io.EOF) && !errors.Is(err, net.ErrClosed) && !isTimeout(err) { status := http.StatusBadRequest message := "bad request\n" var protocolErr *protocolError if errors.As(err, &protocolErr) { status = protocolErr.status message = protocolErr.message + "\n" } if writeSimple(state.conn, status, message, true, s.WriteTimeout) == nil && status == http.StatusRequestEntityTooLarge { closeRejectedBody(baseCtx, state.conn) } } return } _ = state.conn.SetReadDeadline(time.Time{}) firstRequest = false s.mu.Lock() if s.closing || state.closed.Load() { s.mu.Unlock() return } state.active.Store(true) s.mu.Unlock() requestContext, cancel := context.WithCancel(baseCtx) request = request.WithContext(requestContext) s.activeRequests.Add(1) writer := s.writerPool.Get().(*responseWriter) writer.reset(request.Method) writer.configure(request, state.conn, closeAfter, s.WriteTimeout, cancel) requestSlot := false if slots := s.requestSlots; slots != nil { select { case slots <- struct{}{}: requestSlot = true case <-baseCtx.Done(): cancel() writer.reset("") s.writerPool.Put(writer) s.finishRequest(state) return default: s.rejectedRequests.Add(1) writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(http.StatusServiceUnavailable) writer.body.WriteString("server busy\n") closing := s.isClosing() || state.closeWhenIdle.Load() _ = writer.writeTo(state.conn, closeAfter || closing, s.WriteTimeout) cancel() writer.reset("") s.writerPool.Put(writer) closing = s.finishRequest(state) || closing if closeAfter || closing { return } continue } } handler := s.handler var disconnectWatch *clientDisconnectWatch if body, ok := request.Body.(*requestBody); ok { // A body reader and the disconnect watcher must never read the // bufio.Reader concurrently. The body starts the watcher once it // reaches EOF; body read errors cancel the request directly. body.cancel = cancel body.onComplete = func() { disconnectWatch = newClientDisconnectWatch(reader, state.conn, cancel) } } else { disconnectWatch = newClientDisconnectWatch(reader, state.conn, cancel) } handler.ServeHTTP(writer, request) var bodyErr error rejectedBody := false if body, ok := request.Body.(*requestBody); ok && !body.done && body.err == nil && (writer.bodyTooLarge || writer.status == http.StatusRequestEntityTooLarge) { // An application can reject size from the headers alone when its // body limit is below the transport limit. Waiting to // drain a slow or absent body would withhold the rejection response. // Abandon it and close; unread bytes can never become a next request. body.closed = true closeAfter = true rejectedBody = true } else { bodyErr = request.Body.Close() } if disconnectWatch != nil { disconnectWatch.stop() } cancel() closing := s.isClosing() || state.closeWhenIdle.Load() || bodyErr != nil if bodyErr != nil { var protocolErr *protocolError if !errors.As(bodyErr, &protocolErr) { if requestSlot { <-s.requestSlots } writer.reset("") s.writerPool.Put(writer) s.finishRequest(state) return } // Request bodies are streamed into the handler, so a malformed // chunk or an over-limit body can be discovered while draining // after the handler returns. Replace the buffered response with // the same protocol error that eager parsing used to return. if !writer.streaming { writer.reset(request.Method) writer.configure(request, state.conn, true, s.WriteTimeout, cancel) writer.Header().Set("Content-Type", "text/plain; charset=utf-8") writer.WriteHeader(protocolErr.status) writer.body.WriteString(protocolErr.message + "\n") } } if err := writer.writeTo(state.conn, closeAfter || closing, s.WriteTimeout); err != nil { if requestSlot { <-s.requestSlots } writer.reset("") s.writerPool.Put(writer) s.finishRequest(state) return } if requestSlot { <-s.requestSlots } shouldClose := closeAfter || writer.closeAfter || closing writer.reset("") s.writerPool.Put(writer) shouldClose = s.finishRequest(state) || shouldClose if shouldClose { if rejectedBody { closeRejectedBody(baseCtx, state.conn) } return } } } // Half-close after flushing a rejection, then allow the peer to consume it // before closing a socket with unread upload data. As in net/http, immediate // full close can cause a TCP reset to hide the response on BSD systems. This // never waits for the body and cancellation bounds the additional grace. func closeRejectedBody(ctx context.Context, conn net.Conn) { if writer, ok := conn.(interface{ CloseWrite() error }); ok { _ = writer.CloseWrite() } timer := time.NewTimer(500 * time.Millisecond) defer timer.Stop() select { case <-timer.C: case <-ctx.Done(): } } // Publish the idle transition under the same lock that starts shutdown. Either // Shutdown observes an idle connection and closes it, or this request observes // shutdown and closes it instead of starting another Keep-Alive read. func (s *Server) finishRequest(state *connection) bool { s.mu.Lock() state.active.Store(false) closing := s.closing || state.closeWhenIdle.Load() s.mu.Unlock() s.activeRequests.Add(-1) return closing } func (c *connection) close() { if c.closed.CompareAndSwap(false, true) { defer close(c.closeDone) _ = c.conn.Close() c.server.mu.Lock() delete(c.server.conns, c) c.server.mu.Unlock() if c.counted { c.server.activeConns.Add(-1) c.counted = false } } } func (s *Server) recycleConnection(c *connection) { <-c.closeDone if c.slotHeld { if slots := s.connSlots; slots != nil { <-slots } c.slotHeld = false } s.connectionPool.Put(c) } type protocolError struct { status int message string } func (e *protocolError) Error() string { return e.message } func readRequest(reader *bufio.Reader, conn net.Conn, maxHeaderBytes int, maxBodyBytes int64, setBodyDeadline func()) (*http.Request, bool, error) { if maxHeaderBytes <= 0 { maxHeaderBytes = defaultMaxHeaderBytes } if maxBodyBytes <= 0 { maxBodyBytes = defaultMaxBodyBytes } used := 0 readLine := func() (string, error) { line, err := reader.ReadString('\n') used += len(line) if used > maxHeaderBytes { return "", &protocolError{status: http.StatusRequestHeaderFieldsTooLarge, message: "request headers too large"} } if err != nil { return "", err } if !strings.HasSuffix(line, "\n") { return "", &protocolError{status: http.StatusBadRequest, message: "malformed request line"} } return strings.TrimSuffix(strings.TrimSuffix(line, "\n"), "\r"), nil } line, err := readLine() if err != nil { return nil, false, err } parts := strings.Split(line, " ") if len(parts) != 3 || !validToken(parts[0]) { return nil, true, &protocolError{status: http.StatusBadRequest, message: "malformed request line"} } protoMajor, protoMinor, ok := parseProtocol(parts[2]) if !ok { return nil, true, &protocolError{status: http.StatusHTTPVersionNotSupported, message: "unsupported HTTP version"} } target := parts[1] var requestURL *url.URL if target == "*" { requestURL = &url.URL{Path: "*"} } else { requestURL, err = url.ParseRequestURI(target) if err != nil || requestURL.Path == "" || !strings.HasPrefix(requestURL.Path, "/") { return nil, true, &protocolError{status: http.StatusBadRequest, message: "malformed request target"} } } header := make(http.Header) for { line, err = readLine() if err != nil { return nil, true, err } if line == "" { break } if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") { return nil, true, &protocolError{status: http.StatusBadRequest, message: "folded headers are not supported"} } index := strings.IndexByte(line, ':') if index <= 0 { return nil, true, &protocolError{status: http.StatusBadRequest, message: "malformed header"} } name := strings.TrimSpace(line[:index]) value := strings.TrimSpace(line[index+1:]) if !validToken(name) || !validHeaderValue(value) { return nil, true, &protocolError{status: http.StatusBadRequest, message: "malformed header"} } header.Add(name, value) } if protoMajor == 1 && protoMinor == 1 && (len(header.Values("Host")) != 1 || header.Get("Host") == "") { return nil, true, &protocolError{status: http.StatusBadRequest, message: "missing host header"} } for _, value := range header.Values("Expect") { if strings.TrimSpace(value) != "" { return nil, true, &protocolError{status: http.StatusExpectationFailed, message: "unsupported expectation"} } } chunked := false for _, value := range header.Values("Transfer-Encoding") { for _, coding := range strings.Split(value, ",") { coding = strings.TrimSpace(coding) if coding == "" || strings.EqualFold(coding, "identity") { continue } if strings.EqualFold(coding, "chunked") && !chunked { chunked = true continue } return nil, true, &protocolError{status: http.StatusNotImplemented, message: "transfer encoding is not supported"} } } if chunked && len(header.Values("Content-Length")) > 0 { return nil, true, &protocolError{status: http.StatusBadRequest, message: "content length with chunked encoding"} } contentLength := int64(0) contentLengthSeen := false for _, value := range header.Values("Content-Length") { for _, part := range strings.Split(value, ",") { part = strings.TrimSpace(part) parsed, parseErr := strconv.ParseInt(part, 10, 64) if parseErr != nil || parsed < 0 || (contentLengthSeen && parsed != contentLength) { return nil, true, &protocolError{status: http.StatusBadRequest, message: "invalid content length"} } contentLength = parsed contentLengthSeen = true } } if !chunked && contentLength > maxBodyBytes { return nil, true, &protocolError{status: http.StatusRequestEntityTooLarge, message: "request body too large"} } var body io.ReadCloser = http.NoBody var trailer http.Header if chunked { trailer = make(http.Header) body = &requestBody{ reader: reader, maxBytes: maxBodyBytes, chunked: true, trailer: trailer, setDeadline: setBodyDeadline, readLine: readLine, } // net/http uses -1 to indicate that the decoded length was not known // from the request headers. contentLength = -1 } else { if contentLength > 0 { if setBodyDeadline != nil { setBodyDeadline() } body = &requestBody{reader: reader, remaining: contentLength, setDeadline: setBodyDeadline} } } request := &http.Request{ Method: parts[0], URL: requestURL, Proto: parts[2], ProtoMajor: protoMajor, ProtoMinor: protoMinor, Header: header, Body: body, ContentLength: contentLength, Host: header.Get("Host"), RequestURI: target, RemoteAddr: conn.RemoteAddr().String(), Trailer: trailer, } if tlsConn, ok := conn.(*tls.Conn); ok { state := tlsConn.ConnectionState() request.TLS = &state } if chunked { request.TransferEncoding = []string{"chunked"} } closeAfter := headerHasToken(header.Values("Connection"), "close") || (protoMajor == 1 && protoMinor == 0 && !headerHasToken(header.Values("Connection"), "keep-alive")) request.Close = closeAfter return request, closeAfter, nil } type requestBody struct { reader *bufio.Reader remaining int64 maxBytes int64 readBytes int64 chunked bool chunkRemain int64 needSuffix bool trailer http.Header readLine func() (string, error) setDeadline func() started bool cancel context.CancelFunc onComplete func() done bool closed bool err error } func (b *requestBody) Read(p []byte) (int, error) { if len(p) == 0 { return 0, nil } if b.err != nil { return 0, b.err } if b.done { return 0, io.EOF } if !b.chunked { return b.readContentLength(p) } return b.readChunked(p) } func (b *requestBody) readContentLength(p []byte) (int, error) { if b.remaining == 0 { b.complete() return 0, io.EOF } if !b.started { b.started = true if b.setDeadline != nil { b.setDeadline() } } if int64(len(p)) > b.remaining { p = p[:b.remaining] } n, err := b.reader.Read(p) b.remaining -= int64(n) b.readBytes += int64(n) if b.remaining == 0 { b.complete() if n > 0 { return n, nil } return 0, io.EOF } if err != nil { b.fail(err) } return n, err } func (b *requestBody) readChunked(p []byte) (int, error) { for { if b.chunkRemain == 0 { if b.needSuffix { var suffix [2]byte if _, err := io.ReadFull(b.reader, suffix[:]); err != nil { b.fail(err) return 0, err } if suffix[0] != '\r' || suffix[1] != '\n' { err := &protocolError{status: http.StatusBadRequest, message: "malformed chunk terminator"} b.fail(err) return 0, err } b.needSuffix = false } if err := b.beginChunk(); err != nil { b.fail(err) return 0, err } if b.done { return 0, io.EOF } } if int64(len(p)) > b.chunkRemain { p = p[:b.chunkRemain] } n, err := b.reader.Read(p) b.chunkRemain -= int64(n) b.readBytes += int64(n) if b.chunkRemain == 0 { b.needSuffix = true } if n > 0 { if err != nil && b.chunkRemain > 0 { b.fail(err) return n, err } return n, nil } if err != nil { b.fail(err) return 0, err } } } func (b *requestBody) beginChunk() error { if b.setDeadline != nil { b.setDeadline() } line, err := b.readLine() if err != nil { return err } chunkSizeText := line if extension := strings.IndexByte(chunkSizeText, ';'); extension >= 0 { chunkSizeText = chunkSizeText[:extension] } chunkSizeText = strings.TrimSpace(chunkSizeText) if chunkSizeText == "" || !validHexNumber(chunkSizeText) { return &protocolError{status: http.StatusBadRequest, message: "invalid chunk size"} } chunkSize, parseErr := strconv.ParseUint(chunkSizeText, 16, 63) if parseErr != nil { return &protocolError{status: http.StatusBadRequest, message: "invalid chunk size"} } if chunkSize == 0 { for { line, err = b.readLine() if err != nil { return err } if line == "" { b.complete() return nil } if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") { return &protocolError{status: http.StatusBadRequest, message: "folded trailers are not supported"} } index := strings.IndexByte(line, ':') if index <= 0 { return &protocolError{status: http.StatusBadRequest, message: "malformed trailer"} } name := strings.TrimSpace(line[:index]) value := strings.TrimSpace(line[index+1:]) if !validToken(name) || !validHeaderValue(value) { return &protocolError{status: http.StatusBadRequest, message: "malformed trailer"} } b.trailer.Add(name, value) } } if int64(chunkSize) > b.maxBytes-b.readBytes { return &protocolError{status: http.StatusRequestEntityTooLarge, message: "request body too large"} } b.chunkRemain = int64(chunkSize) return nil } func (b *requestBody) Close() error { if b.closed { return b.err } b.closed = true if !b.done && b.err == nil { _, _ = io.Copy(io.Discard, b) } return b.err } func (b *requestBody) complete() { if b.done { return } b.done = true if b.onComplete != nil { b.onComplete() } } func (b *requestBody) fail(err error) { if b.err == nil { b.err = err } b.done = true if b.cancel != nil { b.cancel() } } func validHexNumber(value string) bool { for index := 0; index < len(value); index++ { char := value[index] if !((char >= '0' && char <= '9') || (char >= 'a' && char <= 'f') || (char >= 'A' && char <= 'F')) { return false } } return true } func parseProtocol(value string) (int, int, bool) { if value != "HTTP/1.1" && value != "HTTP/1.0" { return 0, 0, false } return 1, int(value[len(value)-1] - '0'), true } func validToken(value string) bool { if value == "" { return false } for index := 0; index < len(value); index++ { char := value[index] if !isTokenByte(char) { return false } } return true } func isTokenByte(char byte) bool { if char >= '0' && char <= '9' || char >= 'A' && char <= 'Z' || char >= 'a' && char <= 'z' { return true } return strings.ContainsRune("!#$%&'*+-.^_`|~", rune(char)) } func validHeaderValue(value string) bool { for index := 0; index < len(value); index++ { char := value[index] if (char < 0x20 && char != '\t') || char == 0x7f { return false } } return true } func headerHasToken(values []string, token string) bool { for _, value := range values { for _, item := range strings.Split(value, ",") { if strings.EqualFold(strings.TrimSpace(item), token) { return true } } } return false } func isTimeout(err error) bool { var netErr net.Error return errors.As(err, &netErr) && netErr.Timeout() } // Most handlers complete before this delay. A delayed, blocking read avoids a // watcher goroutine for those requests and does not poll idle client sockets. const clientDisconnectWatchDelay = 5 * time.Millisecond type clientDisconnectWatch struct { conn net.Conn done chan struct{} stopped atomic.Bool timer *time.Timer } func newClientDisconnectWatch(reader *bufio.Reader, conn net.Conn, cancel context.CancelFunc) *clientDisconnectWatch { watch := &clientDisconnectWatch{conn: conn, done: make(chan struct{})} watch.timer = time.AfterFunc(clientDisconnectWatchDelay, func() { defer close(watch.done) if watch.stopped.Load() { return } // Only this goroutine may read until stop joins it. Peek preserves // pipelined bytes for the next request and bounds read-ahead to the // existing reader buffer. Once data is available, stop watching: // draining it to detect EOF would require an unbounded pipeline queue. _, err := reader.Peek(1) if err != nil && !(watch.stopped.Load() && isTimeout(err)) { cancel() } }) return watch } func (w *clientDisconnectWatch) stop() { if w.timer.Stop() { return } w.stopped.Store(true) // The watcher never changes deadlines: this interrupt cannot be // overwritten by a callback starting concurrently with stop. _ = w.conn.SetReadDeadline(time.Now()) <-w.done _ = w.conn.SetReadDeadline(time.Time{}) } func writeSimple(conn net.Conn, status int, body string, closeAfter bool, timeout time.Duration) error { writer := newResponseWriter("") writer.status = status writer.wroteHeader = true writer.body.WriteString(body) writer.header.Set("Content-Type", "text/plain; charset=utf-8") return writer.writeTo(conn, closeAfter, timeout) } type responseWriter struct { header http.Header bodyTooLarge bool status int wroteHeader bool body bytes.Buffer requestMethod string protoMajor int protoMinor int conn net.Conn closeAfter bool writeTimeout time.Duration streaming bool chunked bool streamingClose bool writeErr error cancel context.CancelFunc } func newResponseWriter(method string) *responseWriter { return &responseWriter{header: make(http.Header), requestMethod: method} } func (w *responseWriter) reset(method string) { for key := range w.header { delete(w.header, key) } w.status = 0 w.wroteHeader = false w.bodyTooLarge = false if w.body.Cap() > maxPooledResponseBytes { w.body = bytes.Buffer{} } else { w.body.Reset() } w.requestMethod = method w.protoMajor = 0 w.protoMinor = 0 w.conn = nil w.closeAfter = false w.writeTimeout = 0 w.streaming = false w.chunked = false w.streamingClose = false w.writeErr = nil w.cancel = nil } func (w *responseWriter) configure(request *http.Request, conn net.Conn, closeAfter bool, timeout time.Duration, cancel context.CancelFunc) { w.cancel = cancel w.protoMajor = request.ProtoMajor w.protoMinor = request.ProtoMinor w.conn = conn w.closeAfter = closeAfter w.writeTimeout = timeout } func (w *responseWriter) Header() http.Header { return w.header } // RequestBodyTooLarge preserves semantic body rejection independently from a // project's chosen HTTP response status. Called through the optional writer hook. func (w *responseWriter) RequestBodyTooLarge() { w.bodyTooLarge = true } func (w *responseWriter) WriteHeader(status int) { if w.wroteHeader { return } w.status = status w.wroteHeader = true } func (w *responseWriter) Write(data []byte) (int, error) { if w.writeErr != nil { return 0, w.writeErr } if !w.wroteHeader { w.WriteHeader(http.StatusOK) } if w.streaming { written, err := w.writeStream(data) if err != nil { w.writeErr = err } return written, err } return w.body.Write(data) } // Flush implements http.Flusher. Once a response is flushed, HTTP/1.1 uses // chunked framing unless the handler supplied Content-Length. HTTP/1.0 falls // back to close-delimited output because it has no chunked framing. func (w *responseWriter) Flush() { if w.writeErr != nil || w.streaming || w.requestMethod == http.MethodHead || w.status == http.StatusNoContent || w.status == http.StatusResetContent || w.status == http.StatusNotModified || (w.status >= 100 && w.status < 200) { return } _ = w.recordWriteError(w.beginStream()) } func (w *responseWriter) beginStream() error { if w.conn == nil { return errors.New("go-omni/native: response stream is not connected") } if !w.wroteHeader { w.status = http.StatusOK w.wroteHeader = true } if w.requestMethod == http.MethodHead || w.status == http.StatusNoContent || w.status == http.StatusResetContent || w.status == http.StatusNotModified || (w.status >= 100 && w.status < 200) { return nil } contentLength := w.header.Get("Content-Length") w.chunked = contentLength == "" && w.protoMajor == 1 && w.protoMinor == 1 if w.chunked { w.header.Set("Transfer-Encoding", "chunked") } else if contentLength == "" { w.streamingClose = true } closeAfter := w.closeAfter || w.streamingClose if closeAfter { w.header.Set("Connection", "close") } w.closeAfter = closeAfter if err := w.writeHeaders(w.conn); err != nil { return err } w.streaming = true buffered := w.body.Bytes() w.body.Reset() if len(buffered) == 0 { return nil } _, err := w.writeStream(buffered) return err } func (w *responseWriter) writeStream(data []byte) (int, error) { if len(data) == 0 { return 0, nil } if w.conn == nil { return 0, errors.New("go-omni/native: response stream is not connected") } if w.chunked { var prefix [32]byte framed := strconv.AppendInt(prefix[:0], int64(len(data)), 16) framed = append(framed, '\r', '\n') if err := w.writeBytes(framed); err != nil { return 0, err } if err := w.writeBytes(data); err != nil { return 0, err } if err := w.writeBytes([]byte("\r\n")); err != nil { return 0, err } return len(data), nil } if err := w.writeBytes(data); err != nil { return 0, err } return len(data), nil } func (w *responseWriter) writeBytes(data []byte) error { if w.writeTimeout > 0 { _ = w.conn.SetWriteDeadline(time.Now().Add(w.writeTimeout)) } return w.recordWriteError(writeAll(w.conn, data)) } func (w *responseWriter) recordWriteError(err error) error { if err != nil { w.writeErr = err if w.cancel != nil { w.cancel() } } return err } func (w *responseWriter) writeTo(conn net.Conn, closeAfter bool, timeout time.Duration) error { if w.writeErr != nil { _ = conn.SetWriteDeadline(time.Time{}) return w.writeErr } if w.streaming { if w.chunked { if err := w.writeBytes([]byte("0\r\n\r\n")); err != nil { w.writeErr = err } } _ = conn.SetWriteDeadline(time.Time{}) return w.writeErr } if timeout > 0 { _ = conn.SetWriteDeadline(time.Now().Add(timeout)) } if !w.wroteHeader { w.status = http.StatusOK w.wroteHeader = true } body := w.body.Bytes() noBody := w.requestMethod == http.MethodHead || w.status == http.StatusNoContent || w.status == http.StatusResetContent || w.status == http.StatusNotModified || (w.status >= 100 && w.status < 200) if noBody { body = nil } if w.header.Get("Content-Length") == "" && w.header.Get("Transfer-Encoding") == "" && w.status >= 200 && w.status != http.StatusNoContent && w.status != http.StatusNotModified && w.status != http.StatusResetContent { w.header.Set("Content-Length", strconv.Itoa(len(w.body.Bytes()))) } if closeAfter { w.header.Set("Connection", "close") } keys := make([]string, 0, len(w.header)) for key := range w.header { keys = append(keys, key) } sort.Strings(keys) var output bytes.Buffer output.WriteString("HTTP/1.1 ") output.WriteString(strconv.Itoa(w.status)) output.WriteByte(' ') output.WriteString(http.StatusText(w.status)) output.WriteString("\r\n") for _, key := range keys { if !validToken(key) { return errors.New("invalid response header name") } for _, value := range w.header.Values(key) { if !validHeaderValue(value) { return errors.New("invalid response header") } output.WriteString(key) output.WriteString(": ") output.WriteString(value) output.WriteString("\r\n") } } output.WriteString("\r\n") output.Write(body) err := writeAll(conn, output.Bytes()) _ = conn.SetWriteDeadline(time.Time{}) return err } func (w *responseWriter) writeHeaders(conn net.Conn) error { if w.writeTimeout > 0 { _ = conn.SetWriteDeadline(time.Now().Add(w.writeTimeout)) } keys := make([]string, 0, len(w.header)) for key := range w.header { keys = append(keys, key) } sort.Strings(keys) var output bytes.Buffer output.WriteString("HTTP/1.1 ") output.WriteString(strconv.Itoa(w.status)) output.WriteByte(' ') output.WriteString(http.StatusText(w.status)) output.WriteString("\r\n") for _, key := range keys { if !validToken(key) { return errors.New("invalid response header name") } for _, value := range w.header.Values(key) { if !validHeaderValue(value) { return errors.New("invalid response header") } output.WriteString(key) output.WriteString(": ") output.WriteString(value) output.WriteString("\r\n") } } output.WriteString("\r\n") return w.recordWriteError(writeAll(conn, output.Bytes())) } func writeAll(conn net.Conn, data []byte) error { for len(data) > 0 { written, err := conn.Write(data) if written > 0 { data = data[written:] } if err != nil { return err } if written == 0 { return io.ErrShortWrite } } return nil }