commit 0b3f97b6ae7fc927c0299d66def01b804ecede0d
parent c78b6d1aaf2568df0ba8be543597323cbd743dc0
Author: David Eisinger <[email protected]>
Date: Wed, 8 Oct 2025 15:04:08 -0400
Handle CloudFlare SSL properly
Diffstat:
| M | internal/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}}