commit 5899940ff3e6fce18358d385458149b36feac0bf
parent 0b3f97b6ae7fc927c0299d66def01b804ecede0d
Author: David Eisinger <[email protected]>
Date: Wed, 8 Oct 2025 15:20:05 -0400
Add --base-url option
Diffstat:
3 files changed, 38 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
@@ -231,6 +231,15 @@ curl -s http://localhost:8080/bookmarks/1/archive
- Archiver: `internal/archiver.go` (pure-Go HTML-to-text)
- Embedded web UI: `internal/web/` (served at `/`)
+### Reverse proxy / HTTPS
+
+- When running behind a TLS-terminating proxy (e.g., Cloudflare, nginx), the app
+ infers the public origin from standard headers and generates correct `https://`
+ links for the bookmarklet and feeds.
+- Recognized headers: `X-Forwarded-Proto`, `X-Forwarded-Host`, `X-Forwarded-Scheme`,
+ `X-Forwarded-Ssl`, `Forwarded`, and Cloudflare's `Cf-Visitor`.
+- To force a specific origin, set `--base-url` (e.g., `marky --base-url https://bookmarks.example.com`) or the env var `MARKY_BASE_URL`.
+
---
## Acknowledgments
diff --git a/cmd/marky.go b/cmd/marky.go
@@ -29,6 +29,7 @@ func main() {
addr := flag.String("addr", ":8080", "listen address (e.g. :8080, 127.0.0.1:8080)")
dbPath := flag.String("db", defaultDBPath, "path to SQLite DB file (created if missing)")
disableSignup := flag.Bool("disable-signup", false, "disable signup endpoint and hide from UI")
+ baseURL := flag.String("base-url", "", "public base URL (e.g. https://bookmarks.example.com)")
// Also accept explicit --help to exit 0 with usage.
for _, a := range os.Args[1:] {
@@ -51,7 +52,10 @@ func main() {
defer db.Close()
// Build the shared HTTP handler (API + UI)
- h, err := dbpkg.NewHandlerWithConfig(db, dbpkg.HandlerConfig{DisableSignup: *disableSignup})
+ h, err := dbpkg.NewHandlerWithConfig(db, dbpkg.HandlerConfig{
+ DisableSignup: *disableSignup,
+ BaseURL: *baseURL,
+ })
if err != nil {
log.Fatalf("build handler: %v", err)
}
diff --git a/internal/api.go b/internal/api.go
@@ -9,6 +9,7 @@ import (
"log"
"net/http"
"net/url"
+ "os"
"path"
"strconv"
"strings"
@@ -18,13 +19,19 @@ import (
// HandlerConfig controls optional features exposed by the HTTP handler.
type HandlerConfig struct {
DisableSignup bool
+ BaseURL string
}
// NewHandler constructs the HTTP API handler with default settings.
func NewHandler(db *DB) (http.Handler, error) { return NewHandlerWithConfig(db, HandlerConfig{}) }
// NewHandlerWithConfig constructs the HTTP API handler, including the embedded web UI.
+var baseURLOverride string
+
func NewHandlerWithConfig(db *DB, cfg HandlerConfig) (http.Handler, error) {
+ if strings.TrimSpace(cfg.BaseURL) != "" {
+ baseURLOverride = strings.TrimRight(strings.TrimSpace(cfg.BaseURL), "/")
+ }
mux := http.NewServeMux()
// Auth endpoints
@@ -839,6 +846,14 @@ func NewHandlerWithConfig(db *DB, cfg HandlerConfig) (http.Handler, error) {
// standard reverse-proxy headers when present. This supports deployments
// behind TLS-terminating proxies like Cloudflare or nginx.
func originFromRequest(r *http.Request) string {
+ // CLI-configured override takes highest precedence
+ if baseURLOverride != "" {
+ return baseURLOverride
+ }
+ // Explicit override via environment, e.g. MARKY_BASE_URL=https://example.com
+ if base := strings.TrimSpace(os.Getenv("MARKY_BASE_URL")); base != "" {
+ return strings.TrimRight(base, "/")
+ }
// Default scheme based on connection
scheme := "http"
if r.TLS != nil {
@@ -859,6 +874,15 @@ func originFromRequest(r *http.Request) string {
} else if strings.Contains(l, "http") {
scheme = "http"
}
+ } else if xs := r.Header.Get("X-Forwarded-Scheme"); xs != "" { // some proxies
+ first := strings.ToLower(strings.TrimSpace(strings.Split(xs, ",")[0]))
+ if first == "https" || first == "http" {
+ scheme = first
+ }
+ } else if xfssl := r.Header.Get("X-Forwarded-Ssl"); xfssl != "" { // nginx/common: on/off
+ if strings.EqualFold(strings.TrimSpace(strings.Split(xfssl, ",")[0]), "on") {
+ scheme = "https"
+ }
} 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