Files
warmbly/internal/api/handler/public_object.go
T

67 lines
2.0 KiB
Go

package handler
import (
"errors"
"io"
"mime"
"net/http"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"github.com/warmbly/warmbly/internal/infrastructure/storage"
"github.com/warmbly/warmbly/internal/models"
)
// ServePublicObject streams a publicly-readable blob (avatar, org logo, form
// asset, email-body image) from the active storage backend. It exists for the
// filesystem backend, which has no authority to serve objects itself; the S3
// backend returns object-storage URLs from PutPublic and never routes through
// here. Only the fixed public key prefixes are served so this can't be used to
// read arbitrary stored objects.
func (h *Handler) ServePublicObject(c *gin.Context) {
key := strings.TrimPrefix(c.Param("key"), "/")
if key == "" || !isPublicKey(key) {
c.Status(http.StatusNotFound)
return
}
if h.Storage == nil {
c.Status(http.StatusServiceUnavailable)
return
}
body, err := h.Storage.Get(c.Request.Context(), key)
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
c.Status(http.StatusNotFound)
return
}
c.Status(http.StatusInternalServerError)
return
}
defer body.Close()
if ct := mime.TypeByExtension(filepath.Ext(key)); ct != "" {
c.Header("Content-Type", ct)
}
// These are user uploads served from our own origin, so the browser must
// not be free to decide they are something executable.
c.Header("X-Content-Type-Options", "nosniff")
// Keys are content-addressed (they carry an epoch suffix), so they're safe
// to cache immutably.
c.Header("Cache-Control", "public, max-age=31536000, immutable")
c.Status(http.StatusOK)
_, _ = io.Copy(c.Writer, body)
}
// isPublicKey guards the /public route to the key prefixes PutPublic writes, so
// it can't be turned into a reader for arbitrary blob keys.
func isPublicKey(key string) bool {
if strings.Contains(key, "..") {
return false
}
return strings.HasPrefix(key, "avatars/") || strings.HasPrefix(key, "oauth-app-logos/") ||
strings.HasPrefix(key, "form-assets/") || strings.HasPrefix(key, models.EmailImageKeyPrefix)
}