router: implement ETags/If-None-Match based on build time

Essentially caching based on when the program was built.
This commit is contained in:
Harvey Tindall
2025-05-27 16:29:58 +01:00
parent 0fd3981d9b
commit 688e941d64
4 changed files with 33 additions and 10 deletions

3
go.mod
View File

@@ -30,7 +30,6 @@ require (
github.com/fsnotify/fsnotify v1.8.0 github.com/fsnotify/fsnotify v1.8.0
github.com/getlantern/systray v1.2.2 github.com/getlantern/systray v1.2.2
github.com/gin-contrib/pprof v1.5.0 github.com/gin-contrib/pprof v1.5.0
github.com/gin-contrib/static v1.1.2
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang-jwt/jwt v3.2.2+incompatible
@@ -43,6 +42,7 @@ require (
github.com/hrfee/jfa-go/logger v0.0.0-20241105225412-da4470bc4fbc github.com/hrfee/jfa-go/logger v0.0.0-20241105225412-da4470bc4fbc
github.com/hrfee/jfa-go/logmessages v0.0.0-20241105225412-da4470bc4fbc github.com/hrfee/jfa-go/logmessages v0.0.0-20241105225412-da4470bc4fbc
github.com/hrfee/jfa-go/ombi v0.0.0-20241105225412-da4470bc4fbc github.com/hrfee/jfa-go/ombi v0.0.0-20241105225412-da4470bc4fbc
github.com/hrfee/mediabrowser v0.3.28
github.com/itchyny/timefmt-go v0.1.6 github.com/itchyny/timefmt-go v0.1.6
github.com/lithammer/shortuuid/v3 v3.0.7 github.com/lithammer/shortuuid/v3 v3.0.7
github.com/mailgun/mailgun-go/v4 v4.18.1 github.com/mailgun/mailgun-go/v4 v4.18.1
@@ -97,7 +97,6 @@ require (
github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect
github.com/hrfee/mediabrowser v0.3.28 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/compress v1.17.11 // indirect

4
go.sum
View File

@@ -98,8 +98,6 @@ github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NB
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-contrib/static v1.1.2 h1:c3kT4bFkUJn2aoRU3s6XnMjJT8J6nNWJkR0NglqmlZ4=
github.com/gin-contrib/static v1.1.2/go.mod h1:Fw90ozjHCmZBWbgrsqrDvO28YbhKEKzKp8GixhR4yLw=
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
@@ -205,8 +203,6 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hrfee/mediabrowser v0.3.27 h1:8bxPamBFLD1Xqy6pf6M3Oc5GUQ0iU/flO0S64G1AsIM=
github.com/hrfee/mediabrowser v0.3.27/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/hrfee/mediabrowser v0.3.28 h1:KkSgODXxUnZLrkmjSWpma8mXwEVxlOtI51uS2QP/e+c= github.com/hrfee/mediabrowser v0.3.28 h1:KkSgODXxUnZLrkmjSWpma8mXwEVxlOtI51uS2QP/e+c=
github.com/hrfee/mediabrowser v0.3.28/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U= github.com/hrfee/mediabrowser v0.3.28/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=

View File

@@ -9,7 +9,6 @@ import (
"github.com/fatih/color" "github.com/fatih/color"
"github.com/gin-contrib/pprof" "github.com/gin-contrib/pprof"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
lm "github.com/hrfee/jfa-go/logmessages" lm "github.com/hrfee/jfa-go/logmessages"
swaggerFiles "github.com/swaggo/files" swaggerFiles "github.com/swaggo/files"
@@ -94,7 +93,7 @@ func (app *appContext) loadRouter(address string, debug bool) *gin.Engine {
router.Use(gin.Recovery()) router.Use(gin.Recovery())
app.loadHTML(router) app.loadHTML(router)
router.Use(static.Serve("/", app.webFS)) router.Use(serveTaggedStatic("/", app.webFS))
router.NoRoute(app.NoRouteHandler) router.NoRoute(app.NoRouteHandler)
if *PPROF { if *PPROF {
app.debug.Println(lm.RegisterPprof) app.debug.Println(lm.RegisterPprof)
@@ -117,7 +116,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
for _, p := range routePrefixes { for _, p := range routePrefixes {
router.GET(p+"/lang/:page", app.GetLanguages) router.GET(p+"/lang/:page", app.GetLanguages)
router.Use(static.Serve(p+"/", app.webFS)) router.Use(serveTaggedStatic(p+"/", app.webFS))
router.GET(p+PAGES.Admin, app.AdminPage) router.GET(p+PAGES.Admin, app.AdminPage)
if app.config.Section("password_resets").Key("link_reset").MustBool(false) { if app.config.Section("password_resets").Key("link_reset").MustBool(false) {
@@ -136,7 +135,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
router.GET(p+"/token/login", app.getTokenLogin) router.GET(p+"/token/login", app.getTokenLogin)
router.GET(p+"/token/refresh", app.getTokenRefresh) router.GET(p+"/token/refresh", app.getTokenRefresh)
router.POST(p+"/user/invite", app.NewUserFromInvite) router.POST(p+"/user/invite", app.NewUserFromInvite)
router.Use(static.Serve(p+PAGES.Form+"/", app.webFS)) router.Use(serveTaggedStatic(p+PAGES.Form+"/", app.webFS))
router.GET(p+PAGES.Form+"/:invCode", app.InviteProxy) router.GET(p+PAGES.Form+"/:invCode", app.InviteProxy)
if app.config.Section("captcha").Key("enabled").MustBool(false) { if app.config.Section("captcha").Key("enabled").MustBool(false) {
router.GET(p+"/captcha/gen/:invCode", app.GenCaptcha) router.GET(p+"/captcha/gen/:invCode", app.GenCaptcha)

View File

@@ -4,6 +4,8 @@ import (
"io/fs" "io/fs"
"net/http" "net/http"
"strings" "strings"
"github.com/gin-gonic/gin"
) )
// Since the gin-static middleware uses a version of http.Filesystem with an extra Exists() func, we extend it here. // Since the gin-static middleware uses a version of http.Filesystem with an extra Exists() func, we extend it here.
@@ -30,3 +32,30 @@ func (f httpFS) Exists(prefix string, filepath string) bool {
} }
return false return false
} }
var (
etag = buildTimeUnix
)
// Use unix build time as the ETag for a request, allowing caching of static files.
// Copied from gin-contrib/static:
// https://github.com/gin-gonic/contrib/blob/2b1292699c15c6bc6ee8f0e801a4d0b4e807f366/static/static.go
func serveTaggedStatic(urlPrefix string, fs httpFS) gin.HandlerFunc {
fileserver := http.FileServer(fs)
if urlPrefix != "" {
fileserver = http.StripPrefix(urlPrefix, fileserver)
}
return func(gc *gin.Context) {
if fs.Exists(urlPrefix, gc.Request.URL.Path) {
gc.Header("Cache-Control", "no-cache")
gc.Header("ETag", buildTimeUnix)
ifNoneMatchTag := gc.Request.Header.Get("If-None-Match")
if ifNoneMatchTag != "" && ifNoneMatchTag == etag && (gc.Request.Method == http.MethodGet || gc.Request.Method == http.MethodHead) {
gc.AbortWithStatus(http.StatusNotModified)
} else {
fileserver.ServeHTTP(gc.Writer, gc.Request)
gc.Abort()
}
}
}
}