1344 lines
36 KiB
Go
1344 lines
36 KiB
Go
// 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"
|
|
}
|
|
_ = writeSimple(state.conn, status, message, true, s.WriteTimeout)
|
|
}
|
|
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
|
|
if body, ok := request.Body.(*requestBody); ok && !body.done && body.err == nil && 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
|
|
} 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 {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
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
|
|
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 }
|
|
|
|
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
|
|
}
|