1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
|
package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"image"
"io"
"net/http"
"os"
"path/filepath"
"regexp"
"time"
)
const maxAttachment = 5 << 20
var (
attachName = regexp.MustCompile(`^[0-9a-f]{64}\.(png|jpg|gif|webp)$`)
attachExt = map[string]string{"png": "png", "jpeg": "jpg", "gif": "gif", "webp": "webp"}
attachType = map[string]string{"png": "image/png", "jpg": "image/jpeg", "gif": "image/gif", "webp": "image/webp"}
)
func (s *Server) upload(w http.ResponseWriter, r *http.Request) {
u := s.current(r)
if u == nil {
http.Error(w, "log in to attach images", http.StatusUnauthorized)
return
}
if s.uploads.blocked(u.Name) {
http.Error(w, "too many uploads, try again later", http.StatusTooManyRequests)
return
}
data, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxAttachment))
if err != nil {
http.Error(w, "images are limited to 5 MB", http.StatusRequestEntityTooLarge)
return
}
cfg, format, err := image.DecodeConfig(bytes.NewReader(data))
ext, ok := attachExt[format]
if err != nil || !ok || cfg.Width*cfg.Height > 50_000_000 {
http.Error(w, "only png, jpeg, gif and webp images can be attached", http.StatusUnsupportedMediaType)
return
}
sum := sha256.Sum256(data)
name := hex.EncodeToString(sum[:]) + "." + ext
dir := filepath.Join(s.root, "attachments")
path := filepath.Join(dir, name)
if _, err := os.Stat(path); err != nil {
tmp, err := os.CreateTemp(dir, "upload")
if os.IsNotExist(err) {
os.MkdirAll(dir, 0o755)
tmp, err = os.CreateTemp(dir, "upload")
}
if err != nil {
s.fail(w, r, 500, err.Error())
return
}
_, err = tmp.Write(data)
if cerr := tmp.Close(); err == nil {
err = cerr
}
if err == nil {
err = os.Chmod(tmp.Name(), 0o644)
}
if err == nil {
err = os.Rename(tmp.Name(), path)
}
if err != nil {
os.Remove(tmp.Name())
s.fail(w, r, 500, err.Error())
return
}
}
s.uploads.fail(u.Name)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"url": "/attachments/" + name})
}
func (s *Server) attachment(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
if !attachName.MatchString(name) {
http.NotFound(w, r)
return
}
f, err := os.Open(filepath.Join(s.root, "attachments", name))
if err != nil {
http.NotFound(w, r)
return
}
defer f.Close()
w.Header().Set("Content-Type", attachType[filepath.Ext(name)[1:]])
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Content-Security-Policy", "default-src 'none'; sandbox")
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
http.ServeContent(w, r, "", time.Time{}, f)
}
func (s *Server) preview(w http.ResponseWriter, r *http.Request) {
if s.current(r) == nil {
http.Error(w, "log in to preview", http.StatusUnauthorized)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 256<<10)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
io.WriteString(w, string(s.renderMarkdown([]byte(r.FormValue("body")))))
}
|