1package main23import (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"1516 "github.com/gabriel-vasile/mimetype"17 "github.com/gorilla/schema"18 "golang.org/x/crypto/bcrypt"19)2021var render = NewRender()2223func DeleteNoteAPI(isrich bool) http.HandlerFunc {24 return func(w http.ResponseWriter, r *http.Request) {25 var db = DatabaseFromCtx(r.Context())26 var key string27 parts := strings.Split(strings.TrimSuffix(r.URL.EscapedPath(), "/"), "/")2829 if len(parts) != 2 {30 ErrResp(w, "Unknown note key", http.StatusBadRequest)31 return32 } else {33 key = parts[1]34 }3536 note, err := db.LoadNote(r.Context(), key)37 if err != nil {38 ErrResp(w, "Unknown note key", http.StatusBadRequest)39 return40 }4142 if len(note.AuthHashedPassword) == 0 {43 w.WriteHeader(http.StatusUnauthorized)44 fmt.Fprintf(w, "Can not delete an unprotected note\n")45 return46 }4748 authUser, authPasswd, _ := r.BasicAuth()4950 if authUser != note.AuthUser {51 ErrResp(w, "Unauthorized", http.StatusUnauthorized)52 return53 }5455 if err := bcrypt.CompareHashAndPassword([]byte(note.AuthHashedPassword), []byte(authPasswd)); err != nil {56 ErrResp(w, "Unauthorized", http.StatusUnauthorized)57 return58 }5960 if err := db.DeleteNote(r.Context(), key); err != nil {61 ErrResp(w, err.Error(), http.StatusInternalServerError)62 return63 }6465 w.WriteHeader(http.StatusOK)66 fmt.Fprintf(w, "%s deleted \n", key)67 }68}6970func NewOrUpdateNoteAPI(isrich bool) http.HandlerFunc {71 var decoder = schema.NewDecoder()7273 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 }7980 var (81 ctx = r.Context()82 db = DatabaseFromCtx(ctx)83 buf bytes.Buffer8485 note Note86 err error87 )8889 _, 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 return97 }9899 r.Body = io.NopCloser(bytes.NewReader(buf.Bytes()))100101 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 return109 }110111 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 return116 }117118 }119120 // auth information121 var (122 passwdstr string123 ok bool124 )125 note.AuthUser, passwdstr, ok = r.BasicAuth()126 if ok {127 hashed, _ := bcrypt.GenerateFromPassword([]byte(passwdstr), bcrypt.DefaultCost)128 note.AuthHashedPassword = string(hashed)129 }130131 // read times132 if form.Read != nil {133 note.MaxRead = int(*form.Read)134 } else {135 note.MaxRead = -1136 }137138 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 }143144 // note key145 var key string146 parts := strings.Split(strings.TrimSuffix(r.URL.EscapedPath(), "/"), "/")147148 if len(parts) > 0 && parts[len(parts)-1] != "" {149 key = parts[len(parts)-1]150151 if orinote, err := db.LoadNote(ctx, key); err == nil {152 // if the note already exists, auth and inherit153 if len(orinote.AuthHashedPassword) == 0 {154 ErrResp(w, "Can not update a unprotected note", http.StatusForbidden)155 return156 }157 if orinote.AuthUser != note.AuthUser {158 ErrResp(w, "Authentication failed", http.StatusUnauthorized)159 return160 }161 if err := bcrypt.CompareHashAndPassword([]byte(orinote.AuthHashedPassword), []byte(passwdstr)); err != nil {162 ErrResp(w, "Authentication Failed", http.StatusUnauthorized)163 return164 }165166 note.Readed = orinote.Readed167 if form.Read == nil {168 note.MaxRead = orinote.MaxRead169 }170 if form.ExpireDays == nil {171 note.ExpireAt = orinote.ExpireAt172 }173 // always update mime_type, filname, payload174 }175 } else {176 key = NextKey(ctx, db)177 }178179 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.Buffer185 if _, err := io.Copy(&b, fsrc); err != nil {186 ErrResp(w, "Receive source failed", http.StatusBadRequest)187 return188 }189190 note.Filename = fheader.Filename191 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())200201 note.Payload = buf.Bytes()202 }203204 if err := db.SaveNote(ctx, key, ¬e); err != nil {205 ErrResp(w, err.Error(), http.StatusBadRequest)206 return207 }208209 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 }216217 }218}219220func GetNoteAPI(isrich bool) http.HandlerFunc {221 return func(w http.ResponseWriter, r *http.Request) {222 var (223 ctx = r.Context()224 db = DatabaseFromCtx(ctx)225226 key string227 )228229 parts := strings.Split(strings.TrimSuffix(r.URL.EscapedPath(), "/"), "/")230 if len(parts) > 0 {231 key = parts[len(parts)-1]232 }233234 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 return242 }243244 note.Readed += 1245 if !note.IsVisable() {246 ErrResp(w, ErrNotExist.Error(), http.StatusBadRequest)247 return248 }249 if err := db.IncRead(ctx, key); err != nil {250 ErrResp(w, err.Error(), http.StatusBadRequest)251 return252 }253254 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 }261262 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)269270 w.Write(note.Payload)271 }272 }273}274275func ErrResp(w http.ResponseWriter, msg string, status int) {276 w.WriteHeader(status)277 w.Write([]byte(msg + "\n"))278}279280func 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 = *r287 r2.URL = new(url.URL)288 *r2.URL = *r.URL289 r2.URL.Path = fmt.Sprintf("/%s", serveFlags.MOTD)290 r2.URL.RawPath = r2.URL.Path291 GetNoteAPI(false).ServeHTTP(w, r2)292 }293 }294}