kpaste

git clone git://git.lin.moe/go/kpaste.git

  1package main
  2
  3import (
  4	"bytes"
  5	"errors"
  6	"fmt"
  7	"io"
  8	"log/slog"
  9	"mime"
 10	"net/http"
 11	"net/url"
 12	"path"
 13	"strings"
 14	"time"
 15
 16	"github.com/gabriel-vasile/mimetype"
 17	"github.com/gorilla/schema"
 18	"golang.org/x/crypto/bcrypt"
 19)
 20
 21var render = NewRender()
 22
 23func DeleteNoteAPI(isrich bool) http.HandlerFunc {
 24	return func(w http.ResponseWriter, r *http.Request) {
 25		var db = DatabaseFromCtx(r.Context())
 26		var key string
 27		parts := strings.Split(strings.TrimSuffix(r.URL.EscapedPath(), "/"), "/")
 28
 29		if len(parts) != 2 {
 30			ErrResp(w, "Unknown note key", http.StatusBadRequest)
 31			return
 32		} else {
 33			key = parts[1]
 34		}
 35
 36		note, err := db.LoadNote(r.Context(), key)
 37		if err != nil {
 38			ErrResp(w, "Unknown note key", http.StatusBadRequest)
 39			return
 40		}
 41
 42		if len(note.AuthHashedPassword) == 0 {
 43			w.WriteHeader(http.StatusUnauthorized)
 44			fmt.Fprintf(w, "Can not delete an unprotected note\n")
 45			return
 46		}
 47
 48		authUser, authPasswd, _ := r.BasicAuth()
 49
 50		if authUser != note.AuthUser {
 51			ErrResp(w, "Unauthorized", http.StatusUnauthorized)
 52			return
 53		}
 54
 55		if err := bcrypt.CompareHashAndPassword([]byte(note.AuthHashedPassword), []byte(authPasswd)); err != nil {
 56			ErrResp(w, "Unauthorized", http.StatusUnauthorized)
 57			return
 58		}
 59
 60		if err := db.DeleteNote(r.Context(), key); err != nil {
 61			ErrResp(w, err.Error(), http.StatusInternalServerError)
 62			return
 63		}
 64
 65		w.WriteHeader(http.StatusOK)
 66		fmt.Fprintf(w, "%s deleted \n", key)
 67	}
 68}
 69
 70func NewOrUpdateNoteAPI(isrich bool) http.HandlerFunc {
 71	var decoder = schema.NewDecoder()
 72
 73	return func(w http.ResponseWriter, r *http.Request) {
 74		var form struct {
 75			Text       *string `schema:"text"`
 76			Read       *int    `schema:"read"`
 77			ExpireDays *uint   `schema:"expire"`
 78		}
 79
 80		var (
 81			ctx = r.Context()
 82			db  = DatabaseFromCtx(ctx)
 83			buf bytes.Buffer
 84
 85			note Note
 86			err  error
 87		)
 88
 89		_, err = io.Copy(&buf, http.MaxBytesReader(w, r.Body, int64(serveFlags.UploadLimit)*1024))
 90		if err != nil {
 91			if err, ok := err.(*http.MaxBytesError); ok {
 92				ErrResp(w, err.Error(), http.StatusRequestEntityTooLarge)
 93			} else {
 94				ErrResp(w, err.Error(), http.StatusBadRequest)
 95			}
 96			return
 97		}
 98
 99		r.Body = io.NopCloser(bytes.NewReader(buf.Bytes()))
100
101		slog.Debug(r.Header.Get("Content-Type"))
102		if strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/") {
103			err = r.ParseMultipartForm(int64(serveFlags.UploadLimit) * 1024)
104			if err != nil {
105				ErrResp(w,
106					fmt.Sprintf("Invalid multiplart form data: %v", err),
107					http.StatusBadRequest)
108				return
109			}
110
111			if err := decoder.Decode(&form, r.PostForm); err != nil {
112				ErrResp(w,
113					fmt.Sprintf("Invalid multiplart form data: %v", err),
114					http.StatusBadRequest)
115				return
116			}
117
118		}
119
120		// auth information
121		var (
122			passwdstr string
123			ok        bool
124		)
125		note.AuthUser, passwdstr, ok = r.BasicAuth()
126		if ok {
127			hashed, _ := bcrypt.GenerateFromPassword([]byte(passwdstr), bcrypt.DefaultCost)
128			note.AuthHashedPassword = string(hashed)
129		}
130
131		// read times
132		if form.Read != nil {
133			note.MaxRead = int(*form.Read)
134		} else {
135			note.MaxRead = -1
136		}
137
138		if form.ExpireDays != nil {
139			note.ExpireAt = time.Now().Add(time.Hour * 24 * time.Duration(*form.ExpireDays)).UTC()
140		} else {
141			note.ExpireAt = time.Now().Add(serveFlags.DefaultExpireDuration).UTC()
142		}
143
144		// note key
145		var key string
146		parts := strings.Split(strings.TrimSuffix(r.URL.EscapedPath(), "/"), "/")
147
148		if len(parts) > 0 && parts[len(parts)-1] != "" {
149			key = parts[len(parts)-1]
150
151			if orinote, err := db.LoadNote(ctx, key); err == nil {
152				// if the note already exists, auth and inherit
153				if len(orinote.AuthHashedPassword) == 0 {
154					ErrResp(w, "Can not update a unprotected note", http.StatusForbidden)
155					return
156				}
157				if orinote.AuthUser != note.AuthUser {
158					ErrResp(w, "Authentication failed", http.StatusUnauthorized)
159					return
160				}
161				if err := bcrypt.CompareHashAndPassword([]byte(orinote.AuthHashedPassword), []byte(passwdstr)); err != nil {
162					ErrResp(w, "Authentication Failed", http.StatusUnauthorized)
163					return
164				}
165
166				note.Readed = orinote.Readed
167				if form.Read == nil {
168					note.MaxRead = orinote.MaxRead
169				}
170				if form.ExpireDays == nil {
171					note.ExpireAt = orinote.ExpireAt
172				}
173				// always update mime_type, filname, payload
174			}
175		} else {
176			key = NextKey(ctx, db)
177		}
178
179		if form.Text != nil {
180			note.Payload = []byte(*form.Text)
181			note.MimeType = "text/plain"
182			note.Filename = fmt.Sprintf("%s.txt", key)
183		} else if fsrc, fheader, err := r.FormFile("f"); err == nil {
184			var b bytes.Buffer
185			if _, err := io.Copy(&b, fsrc); err != nil {
186				ErrResp(w, "Receive source failed", http.StatusBadRequest)
187				return
188			}
189
190			note.Filename = fheader.Filename
191			note.MimeType = mime.TypeByExtension(path.Ext(fheader.Filename))
192			if note.MimeType == "" {
193				note.MimeType = mimetype.Detect(b.Bytes()).String()
194			}
195			note.Payload = b.Bytes()
196		} else {
197			t := mimetype.Detect(buf.Bytes())
198			note.MimeType = t.String()
199			note.Filename = fmt.Sprintf("%s%s", key, t.Extension())
200
201			note.Payload = buf.Bytes()
202		}
203
204		if err := db.SaveNote(ctx, key, &note); err != nil {
205			ErrResp(w, err.Error(), http.StatusBadRequest)
206			return
207		}
208
209		if isrich && strings.HasPrefix(note.MimeType, "text/") {
210			render.Render(w, r, key, note)
211		} else {
212			w.WriteHeader(http.StatusOK)
213			w.Header().Set("Content-Disposition", fmt.Sprintf("filename=%q", note.Filename))
214			fmt.Fprintf(w, "%s://%s/%s\n", r.URL.Scheme, r.Host, key)
215		}
216
217	}
218}
219
220func GetNoteAPI(isrich bool) http.HandlerFunc {
221	return func(w http.ResponseWriter, r *http.Request) {
222		var (
223			ctx = r.Context()
224			db  = DatabaseFromCtx(ctx)
225
226			key string
227		)
228
229		parts := strings.Split(strings.TrimSuffix(r.URL.EscapedPath(), "/"), "/")
230		if len(parts) > 0 {
231			key = parts[len(parts)-1]
232		}
233
234		note, err := db.LoadNote(ctx, key)
235		if err != nil {
236			if errors.Is(err, ErrNotExist) {
237				ErrResp(w, err.Error(), http.StatusBadRequest)
238			} else {
239				ErrResp(w, err.Error(), http.StatusInternalServerError)
240			}
241			return
242		}
243
244		note.Readed += 1
245		if !note.IsVisable() {
246			ErrResp(w, ErrNotExist.Error(), http.StatusBadRequest)
247			return
248		}
249		if err := db.IncRead(ctx, key); err != nil {
250			ErrResp(w, err.Error(), http.StatusBadRequest)
251			return
252		}
253
254		w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'self'; sandbox allow-downloads allow-forms allow-popups")
255		w.Header().Set("X-Create-At", note.CreateAt.In(time.UTC).Format(time.RFC3339))
256		w.Header().Set("X-Expire-At", note.ExpireAt.In(time.UTC).Format(time.RFC3339))
257		w.Header().Set("X-Readed-Times", fmt.Sprint(note.Readed))
258		if note.MaxRead >= 0 {
259			w.Header().Set("X-Max-Read-Times", fmt.Sprint(note.MaxRead))
260		}
261
262		if isrich {
263			render.Render(w, r, key, *note)
264		} else {
265			if note.Filename != "-" {
266				w.Header().Set("Content-Disposition", fmt.Sprintf("filename=%q", note.Filename))
267			}
268			w.WriteHeader(http.StatusOK)
269
270			w.Write(note.Payload)
271		}
272	}
273}
274
275func ErrResp(w http.ResponseWriter, msg string, status int) {
276	w.WriteHeader(status)
277	w.Write([]byte(msg + "\n"))
278}
279
280func Index(isrich bool) http.HandlerFunc {
281	return func(w http.ResponseWriter, r *http.Request) {
282		if isrich {
283			render.Index(w, serveFlags.MOTD)
284		} else {
285			r2 := new(http.Request)
286			*r2 = *r
287			r2.URL = new(url.URL)
288			*r2.URL = *r.URL
289			r2.URL.Path = fmt.Sprintf("/%s", serveFlags.MOTD)
290			r2.URL.RawPath = r2.URL.Path
291			GetNoteAPI(false).ServeHTTP(w, r2)
292		}
293	}
294}