// 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)) }