Heavy security and file splitting
CI / Go Tests (push) Successful in 15s
CI / Build (push) Successful in 9s
Format / gofmt (push) Successful in 7s
Release Artifacts / Validate release tag (push) Successful in 2s
Release Artifacts / Build and release executables (push) Successful in 15s
Release Artifacts / Build and release Docker image (push) Successful in 28s

This commit is contained in:
2026-06-01 20:15:28 -05:00
parent bdbe1a9416
commit ad4355df17
27 changed files with 1540 additions and 1166 deletions
+89 -14
View File
@@ -69,16 +69,36 @@ func (s *Server) Routes() http.Handler {
mux.Handle("GET /assets/", http.FileServerFS(webassets.StaticFS()))
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServerFS(webassets.StaticFS())))
// Special-case well-known paths to return small plain-text responses instead of full SPA 404 shell.
// This addresses low-priority audit item for robots.txt etc.
mux.Handle("GET /robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("User-agent: *\nDisallow: /\n"))
}))
mux.Handle("GET /sitemap.xml", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
}))
// Serve the real favicon (added to eliminate 404 noise). Placed in static root via public/ in Vite build.
mux.Handle("GET /favicon.svg", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.ServeFileFS(w, r, webassets.StaticFS(), "favicon.svg")
}))
mux.Handle("GET /{path...}", http.HandlerFunc(s.notFoundPage))
return AccessLogMiddleware(
accessLogged := AccessLogMiddleware(
s.accessLogger,
s.cfg.Security.TrustProxyHeaders,
s.cfg.Security.TrustedProxyPrefixes,
)(mux)
return SecurityHeadersMiddleware(s.cfg.Security.HSTSEnabled, accessLogged)
}
func (s *Server) serveSPA(w http.ResponseWriter, r *http.Request) {
s.serveSPAWithStatus(w, http.StatusOK)
s.serveSPAWithStatus(w, http.StatusOK, nil)
}
func (s *Server) getUIConfig(w http.ResponseWriter, r *http.Request) {
@@ -91,19 +111,22 @@ func (s *Server) getUIConfig(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) notFoundPage(w http.ResponseWriter, _ *http.Request) {
s.serveSPAWithStatus(w, http.StatusNotFound)
s.serveSPAWithStatus(w, http.StatusNotFound, nil)
}
func (s *Server) expiredScratchPage(w http.ResponseWriter) {
s.serveSPAWithStatus(w, http.StatusGone)
s.serveSPAWithStatus(w, http.StatusGone, nil)
}
func (s *Server) serveSPAWithStatus(w http.ResponseWriter, status int) {
func (s *Server) serveSPAWithStatus(w http.ResponseWriter, status int, modifier func([]byte) []byte) {
b, err := fs.ReadFile(webassets.StaticFS(), "index.html")
if err != nil {
http.Error(w, "failed to render page", http.StatusInternalServerError)
return
}
if modifier != nil {
b = modifier(b)
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(status)
_, _ = w.Write(b)
@@ -117,17 +140,36 @@ func (s *Server) scratchPage(w http.ResponseWriter, r *http.Request) {
}
meta, err := s.store.Get(id)
if err != nil {
s.expiredScratchPage(w)
return
}
if s.isExpired(meta) {
_ = s.store.Delete(id)
s.expiredScratchPage(w)
if err != nil || s.isExpired(meta) {
if err == nil {
_ = s.store.Delete(id)
}
// For /s/{id} 410/expired (including never-existed), inject id/filename based title+meta even on 410 shell.
// This provides good SEO/social for share links and bots (even on 410), per audit rec.
// We use the id (and name if we had meta before delete) for title.
goneTitle := id + " • Scratchbox"
if err == nil && meta.OriginalName != "" {
goneTitle = meta.OriginalName + " • Scratchbox"
}
s.serveSPAWithStatus(w, http.StatusGone, func(b []byte) []byte {
return injectMeta(b, goneTitle, goneTitle, "This scratch is no longer available.")
})
return
}
s.serveSPA(w, r)
// Valid scratch: serve shell with per-scratch title and og meta injected server-side.
// (Client-side <svelte:head> also updates for SPA, but server inj benefits crawlers/JS-disabled.)
title := id + " • Scratchbox"
if meta.OriginalName != "" {
title = meta.OriginalName + " • Scratchbox"
}
desc := "Temporary scratch file shared on Scratchbox."
if meta.OriginalName != "" {
desc = meta.OriginalName + " — temporary file share on Scratchbox."
}
s.serveSPAWithStatus(w, http.StatusOK, func(b []byte) []byte {
return injectMeta(b, title, title, desc)
})
}
func (s *Server) createScratch(w http.ResponseWriter, r *http.Request) {
@@ -180,7 +222,7 @@ func (s *Server) createScratch(w http.ResponseWriter, r *http.Request) {
}
}
meta, err := s.store.CreateWithOriginalName(r.Context(), reader, contentType, originalName, s.cfg.Limits.DefaultTTLDuration)
meta, err := s.store.CreateWithOriginalName(reader, contentType, originalName, s.cfg.Limits.DefaultTTLDuration)
if err != nil {
if strings.Contains(err.Error(), "request body too large") {
http.Error(w, s.maxUploadSizeError(), http.StatusRequestEntityTooLarge)
@@ -332,3 +374,36 @@ func (s *Server) isUploadAllowedForRequest(r *http.Request) bool {
return false
}
// injectMeta performs lightweight server-side injection of <title> and social meta tags
// into the static index.html shell bytes. Uses simple string replace (safe for this tiny known shell,
// no new deps). Generic pages keep the static metas from the HTML template; /s/{id} get dynamic.
func injectMeta(html []byte, pageTitle, ogTitle, ogDescription string) []byte {
if pageTitle == "" {
pageTitle = "Scratchbox"
}
if ogTitle == "" {
ogTitle = pageTitle
}
if ogDescription == "" {
ogDescription = "Minimal temporary file sharing. Upload once, share the link, auto-expires."
}
s := string(html)
// Override <title>
s = strings.Replace(s, "<title>Scratchbox</title>", "<title>"+htmlEscape(pageTitle)+"</title>", 1)
// Override the static og/twitter metas added to the shells (first occurrence only; safe).
s = strings.Replace(s, `<meta property="og:title" content="Scratchbox" />`, `<meta property="og:title" content="`+htmlEscape(ogTitle)+`" />`, 1)
s = strings.Replace(s, `<meta name="twitter:title" content="Scratchbox" />`, `<meta name="twitter:title" content="`+htmlEscape(ogTitle)+`" />`, 1)
s = strings.Replace(s, `<meta name="description" content="Minimal temporary file sharing. Upload once, share the link, auto-expires." />`, `<meta name="description" content="`+htmlEscape(ogDescription)+`" />`, 1)
s = strings.Replace(s, `<meta property="og:description" content="Minimal temporary file sharing. Upload once, share the link, auto-expires." />`, `<meta property="og:description" content="`+htmlEscape(ogDescription)+`" />`, 1)
s = strings.Replace(s, `<meta name="twitter:description" content="Minimal temporary file sharing. Upload once, share the link, auto-expires." />`, `<meta name="twitter:description" content="`+htmlEscape(ogDescription)+`" />`, 1)
return []byte(s)
}
func htmlEscape(s string) string {
s = strings.ReplaceAll(s, "&", "&amp;")
s = strings.ReplaceAll(s, "<", "&lt;")
s = strings.ReplaceAll(s, ">", "&gt;")
s = strings.ReplaceAll(s, `"`, "&quot;")
return s
}