URL shortener and file dump for hashru.link https://hashru.link
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

422 lines
11 KiB

  1. package rushlink
  2. import (
  3. "crypto/subtle"
  4. "fmt"
  5. "log"
  6. "mime/multipart"
  7. "net/http"
  8. "net/url"
  9. "os"
  10. "strings"
  11. "time"
  12. "gitea.hashru.nl/dsprenkels/rushlink/internal/db"
  13. "github.com/google/uuid"
  14. "github.com/gorilla/mux"
  15. "github.com/pkg/errors"
  16. bolt "go.etcd.io/bbolt"
  17. )
  18. const (
  19. // formParseMaxMemory value is based on the default value that is used in
  20. // Request.ParseMultipartForm.
  21. formParseMaxMemory = 32 << 20 // 32 MB
  22. // highOnlineEntropy is the desired entropy of an "unguessable" paste URL
  23. // (in bits). It should be chosen such that it should be hard for an
  24. // attacker to find *any* key that should not be found.
  25. // It is desired that the probability to guess a good key is small.
  26. //
  27. // [ amount of pastes ]
  28. // Pr[ good key ] = ---------------------------
  29. // [ amount of possible keys ]
  30. //
  31. // So with a conservative [ amount of pastes ] = 2^32 (= 4 billion), and
  32. // an [ amount of possible keys ] = 2^80 then the probability of a correct
  33. // guess is 2^-48.
  34. highOnlineEntropy = 80
  35. )
  36. type viewPaste uint
  37. const (
  38. _ viewPaste = 1 << iota
  39. viewNoRedirect
  40. viewShowMeta
  41. )
  42. const cookieDeleteToken = "owner_token"
  43. type canDelete uint
  44. const (
  45. canDeleteUndef canDelete = iota
  46. canDeleteYes
  47. canDeleteNo
  48. )
  49. func (cd *canDelete) Bool() bool {
  50. return *cd == canDeleteYes
  51. }
  52. func (cd *canDelete) String() string {
  53. switch *cd {
  54. case canDeleteUndef:
  55. return "undefined"
  56. case canDeleteYes:
  57. return "correct"
  58. case canDeleteNo:
  59. return "invalid"
  60. default:
  61. panic("unreachable")
  62. }
  63. }
  64. func (rl *rushlink) staticGetHandler(w http.ResponseWriter, r *http.Request) {
  65. rl.renderStatic(w, r, mux.Vars(r)["path"])
  66. }
  67. func (rl *rushlink) indexGetHandler(w http.ResponseWriter, r *http.Request) {
  68. rl.render(w, r, http.StatusOK, "index", map[string]interface{}{})
  69. }
  70. func (rl *rushlink) viewPasteHandler(w http.ResponseWriter, r *http.Request) {
  71. rl.viewPasteHandlerFlags(w, r, 0)
  72. }
  73. func (rl *rushlink) viewPasteHandlerNoRedirect(w http.ResponseWriter, r *http.Request) {
  74. rl.viewPasteHandlerFlags(w, r, viewNoRedirect)
  75. }
  76. func (rl *rushlink) viewPasteHandlerMeta(w http.ResponseWriter, r *http.Request) {
  77. rl.viewPasteHandlerFlags(w, r, viewShowMeta)
  78. }
  79. func (rl *rushlink) viewPasteHandlerFlags(w http.ResponseWriter, r *http.Request, flags viewPaste) {
  80. vars := mux.Vars(r)
  81. key := vars["key"]
  82. var p *db.Paste
  83. var fu *db.FileUpload
  84. err := rl.db.Bolt.View(func(tx *bolt.Tx) error {
  85. var err error
  86. p, err = db.GetPaste(tx, key)
  87. if err != nil {
  88. return err
  89. }
  90. if p != nil && p.Type == db.PasteTypeFileUpload {
  91. var id uuid.UUID
  92. copy(id[:], p.Content)
  93. fu, err = db.GetFileUpload(tx, id)
  94. if err != nil {
  95. return err
  96. }
  97. }
  98. return nil
  99. })
  100. if err != nil {
  101. status := db.ErrHTTPStatusCode(err)
  102. if status == http.StatusInternalServerError {
  103. panic(err)
  104. }
  105. rl.renderError(w, r, status, err.Error())
  106. return
  107. }
  108. rl.viewPasteHandlerInner(w, r, flags, p, fu)
  109. }
  110. func (rl *rushlink) viewPasteHandlerInner(w http.ResponseWriter, r *http.Request, flags viewPaste, p *db.Paste, fu *db.FileUpload) {
  111. if flags&viewShowMeta != 0 {
  112. rl.viewPasteHandlerInnerMeta(w, r, p, fu)
  113. return
  114. }
  115. switch p.State {
  116. case db.PasteStatePresent:
  117. switch p.Type {
  118. case db.PasteTypeFileUpload:
  119. if fu == nil {
  120. panic(fmt.Sprintf("file for id %v does not exist in database\n", string(p.Content)))
  121. }
  122. rl.viewFileUploadHandler(w, r, fu)
  123. return
  124. case db.PasteTypeRedirect:
  125. if flags&viewNoRedirect != 0 {
  126. w.Write([]byte(p.RedirectURL().String()))
  127. return
  128. }
  129. http.Redirect(w, r, p.RedirectURL().String(), http.StatusTemporaryRedirect)
  130. return
  131. default:
  132. panic("paste type unsupported")
  133. }
  134. case db.PasteStateDeleted:
  135. rl.renderError(w, r, http.StatusGone, "paste has been deleted\n")
  136. return
  137. default:
  138. panic(errors.Errorf("invalid paste.State (%v) for key '%v'", p.State, p.Key))
  139. }
  140. }
  141. func (rl *rushlink) viewFileUploadHandler(w http.ResponseWriter, r *http.Request, fu *db.FileUpload) {
  142. filePath := fu.Path(rl.fs)
  143. file, err := os.Open(filePath)
  144. if err != nil {
  145. if os.IsNotExist(err) {
  146. log.Printf("error: '%v' should exist according to the database, but it doesn't", filePath)
  147. rl.renderError(w, r, http.StatusNotFound, "file not found")
  148. return
  149. }
  150. // unexpected error
  151. panic(err)
  152. }
  153. var modtime time.Time
  154. info, err := file.Stat()
  155. if err != nil {
  156. log.Printf("error: %v", errors.Wrapf(err, "could not stat file '%v'", filePath))
  157. } else {
  158. modtime = info.ModTime()
  159. }
  160. // Provide the real filename to the client (to be used in Ctrl+S etc.)
  161. quotedName := strings.ReplaceAll(fu.FileName, "\"", "\\\"")
  162. w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", quotedName))
  163. // We use http.ServeContent (instead of http.ServeFile) because we cannot
  164. // use http.ServeFile together with the assertion that the file exists,
  165. // without introducing a TOCTOU flaw.
  166. http.ServeContent(w, r, fu.FileName, modtime, file)
  167. }
  168. func (rl *rushlink) viewPasteHandlerInnerMeta(w http.ResponseWriter, r *http.Request, p *db.Paste, fu *db.FileUpload) {
  169. var cd canDelete
  170. deleteToken := getDeleteTokenFromRequest(r)
  171. if deleteToken != "" {
  172. if subtle.ConstantTimeCompare([]byte(deleteToken), []byte(p.DeleteToken)) == 1 {
  173. cd = canDeleteYes
  174. } else {
  175. cd = canDeleteNo
  176. }
  177. }
  178. var fileExt string
  179. if fu != nil {
  180. fileExt = fu.Ext()
  181. }
  182. data := map[string]interface{}{
  183. "Paste": p,
  184. "FileExt": fileExt,
  185. "CanDeleteString": cd.String(),
  186. "CanDeleteBool": cd.Bool(),
  187. }
  188. var status int
  189. if p.State == db.PasteStateDeleted {
  190. status = http.StatusGone
  191. } else {
  192. status = http.StatusOK
  193. }
  194. rl.render(w, r, status, "pasteMeta", data)
  195. return
  196. }
  197. func (rl *rushlink) viewActionSuccess(w http.ResponseWriter, r *http.Request, p *db.Paste, fu *db.FileUpload) {
  198. var fileExt string
  199. if fu != nil {
  200. fileExt = fu.Ext()
  201. }
  202. // Redirect to the new paste.
  203. pasteURL := url.URL{
  204. Path: fmt.Sprintf("/%s%s/meta", p.Key, fileExt),
  205. RawQuery: fmt.Sprintf("deleteToken=%s", url.QueryEscape(p.DeleteToken)),
  206. }
  207. http.Redirect(w, r, pasteURL.String(), http.StatusFound)
  208. // But still render the page for CURL-like clients.
  209. cd := canDeleteYes
  210. data := map[string]interface{}{
  211. "Paste": p,
  212. "FileExt": fileExt,
  213. "CanDeleteString": cd.String(),
  214. "CanDeleteBool": cd.Bool(),
  215. }
  216. rl.render(w, r, 0, "pasteMeta", data)
  217. return
  218. }
  219. func (rl *rushlink) newPasteHandler(w http.ResponseWriter, r *http.Request) {
  220. if err := r.ParseMultipartForm(formParseMaxMemory); err != nil {
  221. msg := fmt.Sprintf("could not parse form: %v\n", err)
  222. rl.renderError(w, r, http.StatusBadRequest, msg)
  223. return
  224. }
  225. fileHeaders, fileHeadersPrs := r.MultipartForm.File["file"]
  226. shortens, shortensPrs := r.MultipartForm.Value["shorten"]
  227. if !shortensPrs && !fileHeadersPrs {
  228. rl.renderError(w, r, http.StatusBadRequest, "no 'file' and no 'shorten' fields given in form\n")
  229. return
  230. }
  231. if shortensPrs && fileHeadersPrs {
  232. rl.renderError(w, r, http.StatusBadRequest, "both 'file' and 'shorten' fields provided in form\n")
  233. return
  234. }
  235. if shortensPrs {
  236. rl.newRedirectPasteHandler(w, r, shortens[0])
  237. return
  238. }
  239. if fileHeadersPrs {
  240. fileHeader := fileHeaders[0]
  241. file, err := fileHeader.Open()
  242. if err != nil {
  243. rl.renderInternalServerError(w, r, err)
  244. return
  245. }
  246. rl.newFileUploadPasteHandler(w, r, file, *fileHeader)
  247. return
  248. }
  249. }
  250. func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Request, file multipart.File, header multipart.FileHeader) {
  251. var fu *db.FileUpload
  252. var paste *db.Paste
  253. if err := rl.db.Bolt.Update(func(tx *bolt.Tx) error {
  254. var err error
  255. fu, err = db.NewFileUpload(rl.fs, file, header.Filename)
  256. if err != nil {
  257. panic(errors.Wrap(err, "creating fileUpload"))
  258. }
  259. if err := fu.Save(tx); err != nil {
  260. panic(errors.Wrap(err, "saving fileUpload in db"))
  261. }
  262. paste, err = shortenFileUploadID(tx, fu.ID)
  263. return err
  264. }); err != nil {
  265. panic(err)
  266. }
  267. rl.viewActionSuccess(w, r, paste, fu)
  268. }
  269. func (rl *rushlink) newRedirectPasteHandler(w http.ResponseWriter, r *http.Request, rawurl string) {
  270. userURL, err := url.Parse(rawurl)
  271. if err != nil {
  272. msg := fmt.Sprintf("invalid url (%v): %v", err, rawurl)
  273. rl.renderError(w, r, http.StatusBadRequest, msg)
  274. return
  275. }
  276. if userURL.Scheme == "" {
  277. rl.renderError(w, r, http.StatusBadRequest, "invalid url (unspecified scheme)\n")
  278. return
  279. }
  280. if userURL.Host == "" {
  281. rl.renderError(w, r, http.StatusBadRequest, "invalid url (unspecified host)\n")
  282. return
  283. }
  284. var paste *db.Paste
  285. if err := rl.db.Bolt.Update(func(tx *bolt.Tx) error {
  286. var err error
  287. paste, err = shortenURL(tx, userURL)
  288. return err
  289. }); err != nil {
  290. panic(err)
  291. }
  292. rl.viewActionSuccess(w, r, paste, nil)
  293. }
  294. // Delete a URL from the database
  295. func (rl *rushlink) deletePasteHandler(w http.ResponseWriter, r *http.Request) {
  296. vars := mux.Vars(r)
  297. key := vars["key"]
  298. deleteToken := getDeleteTokenFromRequest(r)
  299. if deleteToken == "" {
  300. rl.renderError(w, r, http.StatusBadRequest, "no delete token provided\n")
  301. return
  302. }
  303. var errorCode int
  304. var paste *db.Paste
  305. if err := rl.db.Bolt.Update(func(tx *bolt.Tx) error {
  306. var err error
  307. paste, err = db.GetPaste(tx, key)
  308. if err != nil {
  309. errorCode = http.StatusNotFound
  310. return err
  311. }
  312. if paste.State == db.PasteStateDeleted {
  313. errorCode = http.StatusGone
  314. return errors.New("already deleted")
  315. }
  316. if subtle.ConstantTimeCompare([]byte(deleteToken), []byte(paste.DeleteToken)) == 0 {
  317. errorCode = http.StatusForbidden
  318. return errors.New("invalid delete token")
  319. }
  320. if err := paste.Delete(tx, rl.fs); err != nil {
  321. errorCode = http.StatusInternalServerError
  322. return err
  323. }
  324. return nil
  325. }); err != nil {
  326. log.Printf("error: %v\n", err)
  327. rl.renderError(w, r, errorCode, fmt.Sprintf("error: %v\n", err))
  328. return
  329. }
  330. rl.viewActionSuccess(w, r, paste, nil)
  331. }
  332. // Add a new fileUpload redirect to the database
  333. //
  334. // Returns the new paste key if the fileUpload was successfully added to the
  335. // database
  336. func shortenFileUploadID(tx *bolt.Tx, id uuid.UUID) (*db.Paste, error) {
  337. return shorten(tx, db.PasteTypeFileUpload, id[:])
  338. }
  339. // Add a new URL to the database
  340. //
  341. // Returns the new paste key if the url was successfully shortened
  342. func shortenURL(tx *bolt.Tx, userURL *url.URL) (*db.Paste, error) {
  343. return shorten(tx, db.PasteTypeRedirect, []byte(userURL.String()))
  344. }
  345. // Add a paste (of any kind) to the database with arbitrary content.
  346. func shorten(tx *bolt.Tx, ty db.PasteType, content []byte) (*db.Paste, error) {
  347. // Generate the paste key
  348. var keyEntropy int
  349. if ty == db.PasteTypeFileUpload || ty == db.PasteTypePaste {
  350. keyEntropy = highOnlineEntropy
  351. }
  352. pasteKey, err := db.GeneratePasteKey(tx, keyEntropy)
  353. if err != nil {
  354. return nil, errors.Wrap(err, "generating paste key")
  355. }
  356. // Also generate a deleteToken
  357. deleteToken, err := db.GenerateDeleteToken()
  358. if err != nil {
  359. return nil, errors.Wrap(err, "generating delete token")
  360. }
  361. // Store the new key
  362. p := db.Paste{
  363. Type: ty,
  364. State: db.PasteStatePresent,
  365. Content: content,
  366. Key: pasteKey,
  367. DeleteToken: deleteToken,
  368. TimeCreated: time.Now().UTC(),
  369. }
  370. if err := p.Save(tx); err != nil {
  371. return nil, err
  372. }
  373. return &p, nil
  374. }
  375. func getDeleteTokenFromRequest(r *http.Request) string {
  376. return r.URL.Query().Get("deleteToken")
  377. }