118 lines
2.8 KiB
Go
118 lines
2.8 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/xml"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
const maxImageBytes = 16 << 20
|
|
|
|
func (a *API) wsImage(w http.ResponseWriter, r *http.Request, root *os.Root) {
|
|
if r.Method != http.MethodGet {
|
|
fail(w, 405, "method not allowed")
|
|
return
|
|
}
|
|
p := r.URL.Query().Get("path")
|
|
if p == "" || path.IsAbs(p) || p == "." || p == ".." || strings.HasPrefix(p, "../") || path.Clean(p) != p || strings.ContainsRune(p, 0) {
|
|
fail(w, 400, "invalid path")
|
|
return
|
|
}
|
|
anchor, err := root.Open(".")
|
|
if err != nil {
|
|
fail(w, 500, "storage unavailable")
|
|
return
|
|
}
|
|
defer anchor.Close()
|
|
f, err := openDownload(anchor, p)
|
|
if err != nil {
|
|
fail(w, 404, "image unavailable")
|
|
return
|
|
}
|
|
defer f.Close()
|
|
st, err := f.Stat()
|
|
if err != nil || !st.Mode().IsRegular() {
|
|
fail(w, 415, "unsupported image")
|
|
return
|
|
}
|
|
if st.Size() > maxImageBytes {
|
|
fail(w, 413, "image too large")
|
|
return
|
|
}
|
|
b, err := io.ReadAll(io.LimitReader(f, maxImageBytes+1))
|
|
if err != nil {
|
|
fail(w, 500, "image read failed")
|
|
return
|
|
}
|
|
if len(b) > maxImageBytes {
|
|
fail(w, 413, "image too large")
|
|
return
|
|
}
|
|
after, err := f.Stat()
|
|
if err != nil || int64(len(b)) != st.Size() || st.Size() != after.Size() || !st.ModTime().Equal(after.ModTime()) || st.Sys().(*syscall.Stat_t).Ctim != after.Sys().(*syscall.Stat_t).Ctim {
|
|
fail(w, 409, "image changed; reload")
|
|
return
|
|
}
|
|
mimeType := workspaceImageType(p, b)
|
|
if mimeType == "" {
|
|
fail(w, 415, "unsupported image")
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", mimeType)
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
w.Header().Set("Content-Security-Policy", "sandbox; default-src 'none'; style-src 'unsafe-inline'")
|
|
w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": path.Base(p)}))
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
w.Header().Set("Content-Length", strconv.Itoa(len(b)))
|
|
w.Write(b)
|
|
}
|
|
|
|
func workspaceImageType(name string, b []byte) string {
|
|
switch mimeType := http.DetectContentType(b); mimeType {
|
|
case "image/png", "image/jpeg", "image/gif", "image/webp":
|
|
return mimeType
|
|
}
|
|
if !strings.EqualFold(path.Ext(name), ".svg") {
|
|
return ""
|
|
}
|
|
d := xml.NewDecoder(bytes.NewReader(bytes.TrimPrefix(b, []byte{0xef, 0xbb, 0xbf})))
|
|
depth, roots := 0, 0
|
|
for {
|
|
token, err := d.Token()
|
|
if err == io.EOF {
|
|
if roots == 1 && depth == 0 {
|
|
return "image/svg+xml"
|
|
}
|
|
return ""
|
|
}
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
switch t := token.(type) {
|
|
case xml.StartElement:
|
|
if depth == 0 {
|
|
roots++
|
|
if roots != 1 || t.Name.Local != "svg" || t.Name.Space != "http://www.w3.org/2000/svg" {
|
|
return ""
|
|
}
|
|
}
|
|
depth++
|
|
case xml.EndElement:
|
|
depth--
|
|
case xml.CharData:
|
|
if depth == 0 && len(bytes.TrimSpace(t)) != 0 {
|
|
return ""
|
|
}
|
|
case xml.Directive:
|
|
return ""
|
|
}
|
|
}
|
|
}
|