60 lines
2.6 KiB
Go
60 lines
2.6 KiB
Go
// Package reqtls answers one question, in one place, for the whole
|
|
// application: did this request reach the service over TLS?
|
|
//
|
|
// It exists because that question used to be answered independently in
|
|
// several packages, by hand, and the answers disagreed. The session
|
|
// cookie's Secure attribute was decided at startup from the configured
|
|
// environment while the CSRF cookie's was decided per-request, so a
|
|
// deployment behind a TLS proxy in the default environment emitted one
|
|
// Secure cookie and one non-Secure cookie on the same response.
|
|
// Everything kept working, which is exactly why nobody noticed.
|
|
//
|
|
// Any code that needs a scheme or a Secure flag must call IsTLS rather
|
|
// than reading the request itself.
|
|
package reqtls
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// forwardedProtoHeader is the de-facto standard header by which a
|
|
// TLS-terminating reverse proxy reports the protocol the CLIENT used.
|
|
const forwardedProtoHeader = "X-Forwarded-Proto"
|
|
|
|
// IsTLS reports whether the client-facing connection uses TLS: either
|
|
// the request arrived over TLS directly, or a reverse proxy terminated
|
|
// TLS and said so in X-Forwarded-Proto.
|
|
//
|
|
// The header is only as trustworthy as whatever sits in front of the
|
|
// listener. A proxy that overwrites it -- which is what the deployment
|
|
// documentation requires -- makes it authoritative; a listener exposed
|
|
// directly to clients lets any client assert it. That is the same
|
|
// exposure every X-Forwarded-* consumer carries.
|
|
func IsTLS(r *http.Request) bool {
|
|
return r.TLS != nil || forwardedProto(r) == "https"
|
|
}
|
|
|
|
// forwardedProto reduces X-Forwarded-Proto to a bare, comparable
|
|
// protocol token, or "" when the header is absent or blank.
|
|
//
|
|
// Two shapes that real infrastructure emits do not survive an exact
|
|
// comparison against "https", and both name a TLS client connection:
|
|
//
|
|
// - "HTTPS", because the header value is a case-insensitive token and
|
|
// nothing obliges a proxy to emit it lowercased.
|
|
// - "https, http", because a proxy chained behind another proxy
|
|
// APPENDS its own hop instead of replacing the value. As with
|
|
// X-Forwarded-For, the leftmost element is the one nearest the
|
|
// client, so it is the element that describes the browser's
|
|
// connection -- the only hop a cookie's Secure attribute is about.
|
|
//
|
|
// Landing on the plaintext path for either of those spellings is not a
|
|
// cosmetic error: it stops gorilla/csrf enforcing the strict Referer
|
|
// check on a site that genuinely is HTTPS.
|
|
func forwardedProto(r *http.Request) string {
|
|
first, _, _ := strings.Cut(r.Header.Get(forwardedProtoHeader), ",")
|
|
|
|
return strings.ToLower(strings.TrimSpace(first))
|
|
}
|