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"))))) }