9280542b33
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>
61 lines
2.1 KiB
Go
61 lines
2.1 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"regexp"
|
|
"time"
|
|
)
|
|
|
|
// versionLine matches the userscript metadata block's @version directive.
|
|
var versionLine = regexp.MustCompile(`(?m)^// @version[ \t]+.*$`)
|
|
|
|
// stampVersion replaces the served @version with one derived from the file's
|
|
// mtime, discarding whatever the file body says.
|
|
//
|
|
// Violentmonkey only updates when the served version sorts higher than the
|
|
// installed one. Deriving it from the body means one accidental downgrade or
|
|
// typo freezes updates forever; an mtime-derived version is monotonic by
|
|
// construction, so any later write always outranks any earlier one.
|
|
//
|
|
// A file with no @version line is returned untouched: such a script never
|
|
// auto-updates anyway, and inventing a metadata block is not this handler's job.
|
|
func stampVersion(src []byte, mod time.Time) []byte {
|
|
return versionLine.ReplaceAll(src, []byte("// @version "+mod.UTC().Format("2006.01.02.1504")))
|
|
}
|
|
|
|
// userscriptHandler serves the userscript to Violentmonkey's updater.
|
|
//
|
|
// The token lives in the path because the update poll sends no Authorization
|
|
// header, and the file embeds API_TOKEN in plain text, so an open path would
|
|
// hand that token to anyone who guessed the URL. A mismatch answers 404 rather
|
|
// than 401: a prober learns nothing about whether the route exists.
|
|
//
|
|
// The file is read per request — that is what lets a bindmounted copy be edited
|
|
// on the host without a restart. It is ~50 KB and polled about once a day.
|
|
func userscriptHandler(token, path string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if subtle.ConstantTimeCompare([]byte(r.PathValue("token")), []byte(token)) != 1 {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
log.Printf("userscript: stat %s: %v", path, err)
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
src, err := os.ReadFile(path)
|
|
if err != nil {
|
|
log.Printf("userscript: read %s: %v", path, err)
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/javascript; charset=utf-8")
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
w.Write(stampVersion(src, info.ModTime()))
|
|
}
|
|
}
|