marky

Lightweight bookmarking + plaintext archive
Log | Files | Refs | README | LICENSE

commit 0b3f97b6ae7fc927c0299d66def01b804ecede0d
parent c78b6d1aaf2568df0ba8be543597323cbd743dc0
Author: David Eisinger <[email protected]>
Date:   Wed,  8 Oct 2025 15:04:08 -0400

Handle CloudFlare SSL properly

Diffstat:
Minternal/api.go | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
1 file changed, 64 insertions(+), 10 deletions(-)

diff --git a/internal/api.go b/internal/api.go @@ -108,11 +108,7 @@ func NewHandlerWithConfig(db *DB, cfg HandlerConfig) (http.Handler, error) { } } // Resolve origin to use in links opened by the bookmarklet - scheme := "http" - if r.TLS != nil { - scheme = "https" - } - origin := scheme + "://" + r.Host + origin := originFromRequest(r) // Fetch API token (if present) for interpolation var token string var present bool @@ -839,6 +835,68 @@ func NewHandlerWithConfig(db *DB, cfg HandlerConfig) (http.Handler, error) { return mux, nil } +// originFromRequest determines the request origin (scheme://host), honoring +// standard reverse-proxy headers when present. This supports deployments +// behind TLS-terminating proxies like Cloudflare or nginx. +func originFromRequest(r *http.Request) string { + // Default scheme based on connection + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + + // Prefer standard proxy headers when present + if xfp := r.Header.Get("X-Forwarded-Proto"); xfp != "" { + // May be a comma-separated list; take the first value + first := strings.ToLower(strings.TrimSpace(strings.Split(xfp, ",")[0])) + if first == "https" || first == "http" { + scheme = first + } + } else if cfv := r.Header.Get("Cf-Visitor"); cfv != "" { // Cloudflare + l := strings.ToLower(cfv) + if strings.Contains(l, "\"https\"") || strings.Contains(l, "https") { // {"scheme":"https"} + scheme = "https" + } else if strings.Contains(l, "http") { + scheme = "http" + } + } else if fwd := r.Header.Get("Forwarded"); fwd != "" { + // Parse Forwarded header (RFC 7239), look for proto= + // Example: Forwarded: for=1.2.3.4; proto=https; host=example.com + // The header may contain multiple entries separated by commas; use the first + first := strings.Split(fwd, ",")[0] + for _, part := range strings.Split(first, ";") { + kv := strings.SplitN(strings.TrimSpace(part), "=", 2) + if len(kv) == 2 && strings.ToLower(strings.TrimSpace(kv[0])) == "proto" { + v := strings.Trim(kv[1], "\"')") + v = strings.ToLower(strings.TrimSpace(v)) + if v == "https" || v == "http" { + scheme = v + } + break + } + } + } + + host := r.Host + if xfh := r.Header.Get("X-Forwarded-Host"); xfh != "" { + host = strings.TrimSpace(strings.Split(xfh, ",")[0]) + } else if fwd := r.Header.Get("Forwarded"); fwd != "" { + // Parse host= from Forwarded header + first := strings.Split(fwd, ",")[0] + for _, part := range strings.Split(first, ";") { + kv := strings.SplitN(strings.TrimSpace(part), "=", 2) + if len(kv) == 2 && strings.ToLower(strings.TrimSpace(kv[0])) == "host" { + v := strings.Trim(kv[1], "\"')") + if v != "" { + host = v + } + break + } + } + } + return scheme + "://" + host +} + // userIDFromRequest resolves the user ID using either a signed session cookie // or HTTP Basic auth where the username is the account username and the // password is the API token. @@ -952,11 +1010,7 @@ func writeRSS(w http.ResponseWriter, r *http.Request, items []bookmarkResponse, } chanLink := "" if r != nil && r.Host != "" { - scheme := "http" - if r.TLS != nil { - scheme = "https" - } - chanLink = fmt.Sprintf("%s://%s/bookmarks", scheme, r.Host) + chanLink = strings.TrimRight(originFromRequest(r), "/") + "/bookmarks" } chanDesc := chanTitle out := rssFeed{Version: "2.0", Channel: rssChannel{Title: chanTitle, Link: chanLink, Description: chanDesc}}