From 80d7a356b3443e0a994e5d6abfa6082ba3d5e6e7 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Wed, 5 Jan 2022 20:01:15 -0500 Subject: [PATCH] caddyhttp: Redirect HTTP requests on the HTTPS port to https:// (#4313) * caddyhttp: Redirect HTTP requests on the HTTPS port to https:// * Apply suggestions from code review Co-authored-by: Matt Holt Co-authored-by: Matt Holt --- modules/caddyhttp/app.go | 5 + modules/caddyhttp/httpredirectlistener.go | 114 ++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 modules/caddyhttp/httpredirectlistener.go diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 64cc5401..67f9d1d6 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -343,6 +343,11 @@ func (app *App) Start() error { // enable TLS if there is a policy and if this is not the HTTP port useTLS := len(srv.TLSConnPolicies) > 0 && int(listenAddr.StartPort+portOffset) != app.httpPort() if useTLS { + // create HTTP redirect wrapper, which detects if + // the request had HTTP bytes on the HTTPS port, and + // triggers a redirect if so. + ln = &httpRedirectListener{Listener: ln} + // create TLS listener tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx) ln = tls.NewListener(ln, tlsCfg) diff --git a/modules/caddyhttp/httpredirectlistener.go b/modules/caddyhttp/httpredirectlistener.go new file mode 100644 index 00000000..38225a3d --- /dev/null +++ b/modules/caddyhttp/httpredirectlistener.go @@ -0,0 +1,114 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyhttp + +import ( + "bufio" + "fmt" + "net" + "net/http" + "sync" +) + +// httpRedirectListener is listener that checks the first few bytes +// of the request when the server is intended to accept HTTPS requests, +// to respond to an HTTP request with a redirect. +type httpRedirectListener struct { + net.Listener +} + +// Accept waits for and returns the next connection to the listener, +// wrapping it with a httpRedirectConn. +func (l *httpRedirectListener) Accept() (net.Conn, error) { + c, err := l.Listener.Accept() + if err != nil { + return nil, err + } + + return &httpRedirectConn{ + Conn: c, + r: bufio.NewReader(c), + }, nil +} + +type httpRedirectConn struct { + net.Conn + once sync.Once + r *bufio.Reader +} + +// Read tries to peek at the first few bytes of the request, and if we get +// an error reading the headers, and that error was due to the bytes looking +// like an HTTP request, then we perform a HTTP->HTTPS redirect on the same +// port as the original connection. +func (c *httpRedirectConn) Read(p []byte) (int, error) { + var errReturn error + c.once.Do(func() { + firstBytes, err := c.r.Peek(5) + if err != nil { + return + } + + // If the request doesn't look like HTTP, then it's probably + // TLS bytes and we don't need to do anything. + if !firstBytesLookLikeHTTP(firstBytes) { + return + } + + // Parse the HTTP request, so we can get the Host and URL to redirect to. + req, err := http.ReadRequest(c.r) + if err != nil { + return + } + + // Build the redirect response, using the same Host and URL, + // but replacing the scheme with https. + headers := make(http.Header) + headers.Add("Location", "https://"+req.Host+req.URL.String()) + resp := &http.Response{ + Proto: "HTTP/1.0", + Status: "308 Permanent Redirect", + StatusCode: 308, + ProtoMajor: 1, + ProtoMinor: 0, + Header: headers, + } + + err = resp.Write(c.Conn) + if err != nil { + errReturn = fmt.Errorf("couldn't write HTTP->HTTPS redirect") + return + } + + errReturn = fmt.Errorf("redirected HTTP request on HTTPS port") + c.Conn.Close() + }) + + if errReturn != nil { + return 0, errReturn + } + + return c.r.Read(p) +} + +// firstBytesLookLikeHTTP reports whether a TLS record header +// looks like it might've been a misdirected plaintext HTTP request. +func firstBytesLookLikeHTTP(hdr []byte) bool { + switch string(hdr[:5]) { + case "GET /", "HEAD ", "POST ", "PUT /", "OPTIO": + return true + } + return false +}