From 9ee19b46daea42bad13f68388b0fa8ada1c9805c Mon Sep 17 00:00:00 2001 From: Patricio Whittingslow Date: Wed, 29 Jul 2026 11:47:37 -0300 Subject: [PATCH] httphi.Handle rejects unsupported protocols --- http/httphi/exchange_test.go | 52 ++++++++++++++++++++++++++++++++++++ http/httphi/mux.go | 10 ++++--- http/httphi/router.go | 3 ++- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/http/httphi/exchange_test.go b/http/httphi/exchange_test.go index ae7afb3..fa28eef 100644 --- a/http/httphi/exchange_test.go +++ b/http/httphi/exchange_test.go @@ -262,6 +262,58 @@ func TestHandleMalformedRequest(t *testing.T) { } } +// A request-line naming a version this package does not speak must be answered +// 505 without reaching a handler, RFC 9112 2.6. The third token is not +// validated by the parser, so anything that is not HTTP/1.0 or HTTP/1.1 lands +// here: bogus versions, non-HTTP tokens, and the tail of a request-target that +// contained a space and got split across the URI/protocol boundary. +func TestHandleUnsupportedProtocol(t *testing.T) { + for _, test := range []struct { + name string + request string + }{ + {name: "future major", request: "GET / HTTP/6.9\r\nHost: h\r\n\r\n"}, + {name: "future major keepalive", request: "GET / HTTP/6.9\r\nHost: h\r\nConnection: keep-alive\r\n\r\n"}, + {name: "http2", request: "GET / HTTP/2.0\r\nHost: h\r\n\r\n"}, + {name: "long minor", request: "GET / HTTP/1.10\r\nHost: h\r\n\r\n"}, + {name: "lowercase", request: "GET / http/1.1\r\nHost: h\r\n\r\n"}, + {name: "not http", request: "GET / BANANA\r\nHost: h\r\n\r\n"}, + {name: "space in target", request: "GET /a b HTTP/1.1\r\nHost: h\r\n\r\n"}, + } { + t.Run(test.name, func(t *testing.T) { + var handled bool + var sm MuxSlice + sm.Handle("/", func(ex *Exchange) { handled = true }) + sm.Handle("/a", func(ex *Exchange) { handled = true }) + conn := newConn(test.request) + conn.Hangup() + exch := newExchange(t, conn, ExchangeConfig{RawBuf: make([]byte, 2*1024), RequestBufferLim: 1024}) + if err := Handle(exch, &sm, nopBackoff); err == nil { + t.Error("want error on unsupported protocol, got nil") + } + if handled { + t.Error("handler must not run on unsupported protocol") + } + const want = "HTTP/1.1 505 HTTP Version Not Supported\r\n\r\n" + if got := conn.ViewWritten(); got != want { + t.Errorf("want %q, got %q", want, got) + } + }) + } +} + +// HTTP/1.0 predates HTTP/1.1 but is still served: only the connection is not +// kept alive. It must not be swept up by the 505 gate. +func TestHandleHTTP10Served(t *testing.T) { + var sm MuxSlice + sm.Handle("/", func(ex *Exchange) { ex.WriteHeader(200) }) + conn := serve(t, "GET / HTTP/1.0\r\nHost: h\r\n\r\n", &sm) + const want = "HTTP/1.1 200 OK\r\n\r\n" + if got := conn.ViewWritten(); got != want { + t.Errorf("want %q, got %q", want, got) + } +} + // No registered handler must yield 404, not an empty response. func TestHandleNoHandler(t *testing.T) { var sm MuxSlice diff --git a/http/httphi/mux.go b/http/httphi/mux.go index e6c8950..1d405e4 100644 --- a/http/httphi/mux.go +++ b/http/httphi/mux.go @@ -52,11 +52,15 @@ func Handle(exch *Exchange, mux Mux, backoff lneto.BackoffStrategy) error { exch.respRemains = reqhdr.BufferReceived() - parsed exch.respHeaderOff = uint16(parsed) exch.respHeaderLen = 0 - if len(reqhdr.Protocol()) == 0 { - // Request line with no HTTP version is a HTTP/0.9 simple-request, which - // httpraw tolerates. It is not a valid HTTP/1.1 request-line, RFC 9112 3. + proto := b2s(reqhdr.Protocol()) + if len(proto) == 0 { + // HTTP/0.9 not tolerated RFC 9112 3. exch.WriteHeader(int(StatusBadRequest)) return errNoRequestProto + } else if proto != "HTTP/1.1" && proto != "HTTP/1.0" { + // RFC 9112 2.6. + exch.WriteHeader(int(StatusHTTPVersionNotSupported)) + return errBadRequestProto } // Mux on the request path: the query string is the handler's business. path := reqhdr.RequestPath() diff --git a/http/httphi/router.go b/http/httphi/router.go index f46d36a..bcb5335 100644 --- a/http/httphi/router.go +++ b/http/httphi/router.go @@ -20,7 +20,8 @@ import ( const reconfigureWait = 10 * time.Millisecond var ( - errNoRequestProto = errors.New("httphi: request line with no HTTP version") + errNoRequestProto = errors.New("httphi: request line with no HTTP version") + errBadRequestProto = errors.New("httphi: unsupported HTTP version in request line") errBusyExchanges = errors.New("httphi: exchanges still serving, cannot reuse their buffers") errRouterTornDown = errors.New("httphi: router torn down, configure it before serving")