feat(backend): serve userscript at token-protected path
Reads the file per request from USERSCRIPT_PATH and rewrites its @version
to an mtime-derived value, so Violentmonkey always sees a higher version
after an edit regardless of what the file body claims. Also guards against
ServeMux's own path-cleaning redirect turning an empty {token} segment into
a 307 instead of the required 404.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+30
-6
@@ -21,6 +21,9 @@ type Config struct {
|
||||
Port string
|
||||
// WebPassword gates the browser UI. Empty disables the web routes entirely.
|
||||
WebPassword string
|
||||
// UserscriptPath is the file served at /u/{token}/manga-bookmark.user.js.
|
||||
// Supplied by a bindmount so the script can be edited without a rebuild.
|
||||
UserscriptPath string
|
||||
// LatestPoll configures the background latest-chapter fetcher.
|
||||
LatestPoll LatestPoll
|
||||
}
|
||||
@@ -128,11 +131,12 @@ func loadLatestPoll() LatestPoll {
|
||||
|
||||
func loadConfig() Config {
|
||||
c := Config{
|
||||
Token: os.Getenv("API_TOKEN"),
|
||||
DBPath: envOr("DB_PATH", "/data/bookmarks.db"),
|
||||
Port: envOr("PORT", "8080"),
|
||||
WebPassword: os.Getenv("WEB_PASSWORD"),
|
||||
LatestPoll: loadLatestPoll(),
|
||||
Token: os.Getenv("API_TOKEN"),
|
||||
DBPath: envOr("DB_PATH", "/data/bookmarks.db"),
|
||||
Port: envOr("PORT", "8080"),
|
||||
WebPassword: os.Getenv("WEB_PASSWORD"),
|
||||
UserscriptPath: envOr("USERSCRIPT_PATH", "/userscript/manga-bookmark.user.js"),
|
||||
LatestPoll: loadLatestPoll(),
|
||||
}
|
||||
for _, o := range strings.Split(os.Getenv("ALLOWED_ORIGINS"), ",") {
|
||||
if o = strings.TrimSpace(o); o != "" {
|
||||
@@ -149,6 +153,11 @@ func newRouter(store *Store, cfg Config) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /healthz", healthz)
|
||||
|
||||
// Outside withAuth (the updater sends no Authorization header) and outside
|
||||
// the WEB_PASSWORD gate (the script must be installable either way). The
|
||||
// path segment carries the token instead.
|
||||
mux.HandleFunc("GET /u/{token}/manga-bookmark.user.js", userscriptHandler(cfg.Token, cfg.UserscriptPath))
|
||||
|
||||
h := &bookmarkHandler{store: store}
|
||||
protected := http.NewServeMux()
|
||||
protected.HandleFunc("GET /bookmarks", h.list)
|
||||
@@ -170,7 +179,22 @@ func newRouter(store *Store, cfg Config) http.Handler {
|
||||
web.register(mux)
|
||||
}
|
||||
|
||||
return withCORS(cfg.AllowedOrigins, mux)
|
||||
return withCORS(cfg.AllowedOrigins, guardEmptyUserscriptToken(mux))
|
||||
}
|
||||
|
||||
// guardEmptyUserscriptToken heads off ServeMux's own path-cleaning redirect:
|
||||
// an empty {token} segment makes the request path "/u//manga-bookmark.user.js",
|
||||
// and ServeMux 307s that to "/u/manga-bookmark.user.js" before pattern
|
||||
// matching ever runs. The endpoint's contract is 404 for any wrong token,
|
||||
// including this one, so catch it ahead of the mux.
|
||||
func guardEmptyUserscriptToken(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.URL.Path, "/u//") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
Reference in New Issue
Block a user