linx-server/upload.go
mutantmonkey b0d2f2a142 support .tar.gz-style extensions
Some extensions actually consist of multiple parts, like .tar.gz, so we
should handle this properly instead of merging part of the extension
with the bare name. Right now only tar is allowed, but others can be
added easily.

Fixes #74.
2016-02-12 21:27:39 -08:00

371 lines
9.1 KiB
Go

package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"bitbucket.org/taruti/mimemagic"
"github.com/dchest/uniuri"
"github.com/zenazn/goji/web"
)
var fileBlacklist = map[string]bool{
"favicon.ico": true,
"index.htm": true,
"index.html": true,
"index.php": true,
"robots.txt": true,
"crossdomain.xml": true,
}
// Describes metadata directly from the user request
type UploadRequest struct {
src io.Reader
filename string
expiry time.Duration // Seconds until expiry, 0 = never
randomBarename bool
deletionKey string // Empty string if not defined
}
// Metadata associated with a file as it would actually be stored
type Upload struct {
Filename string // Final filename on disk
Metadata Metadata
}
func uploadPostHandler(c web.C, w http.ResponseWriter, r *http.Request) {
if !strictReferrerCheck(r, Config.siteURL, []string{"Linx-Delete-Key", "Linx-Expiry", "Linx-Randomize", "X-Requested-With"}) {
badRequestHandler(c, w, r)
return
}
upReq := UploadRequest{}
uploadHeaderProcess(r, &upReq)
contentType := r.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "multipart/form-data") {
file, headers, err := r.FormFile("file")
if err != nil {
oopsHandler(c, w, r, RespHTML, "Could not upload file.")
return
}
defer file.Close()
r.ParseForm()
if r.Form.Get("randomize") == "true" {
upReq.randomBarename = true
}
upReq.expiry = parseExpiry(r.Form.Get("expires"))
upReq.src = file
upReq.filename = headers.Filename
} else {
if r.FormValue("content") == "" {
oopsHandler(c, w, r, RespHTML, "Empty file")
return
}
extension := r.FormValue("extension")
if extension == "" {
extension = "txt"
}
upReq.src = strings.NewReader(r.FormValue("content"))
upReq.expiry = parseExpiry(r.FormValue("expires"))
upReq.filename = r.FormValue("filename") + "." + extension
}
upload, err := processUpload(upReq)
if strings.EqualFold("application/json", r.Header.Get("Accept")) {
if err != nil {
oopsHandler(c, w, r, RespJSON, "Could not upload file: "+err.Error())
return
}
js := generateJSONresponse(upload)
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.Write(js)
} else {
if err != nil {
oopsHandler(c, w, r, RespHTML, "Could not upload file: "+err.Error())
return
}
http.Redirect(w, r, Config.sitePath+upload.Filename, 303)
}
}
func uploadPutHandler(c web.C, w http.ResponseWriter, r *http.Request) {
upReq := UploadRequest{}
uploadHeaderProcess(r, &upReq)
defer r.Body.Close()
upReq.filename = c.URLParams["name"]
upReq.src = r.Body
upload, err := processUpload(upReq)
if strings.EqualFold("application/json", r.Header.Get("Accept")) {
if err != nil {
oopsHandler(c, w, r, RespJSON, "Could not upload file: "+err.Error())
return
}
js := generateJSONresponse(upload)
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.Write(js)
} else {
if err != nil {
oopsHandler(c, w, r, RespPLAIN, "Could not upload file: "+err.Error())
return
}
fmt.Fprintf(w, Config.siteURL+upload.Filename)
}
}
func uploadRemote(c web.C, w http.ResponseWriter, r *http.Request) {
if Config.remoteAuthFile != "" {
result, err := checkAuth(remoteAuthKeys, r.FormValue("key"))
if err != nil || !result {
unauthorizedHandler(c, w, r)
return
}
}
if r.FormValue("url") == "" {
http.Redirect(w, r, Config.sitePath, 303)
return
}
upReq := UploadRequest{}
grabUrl, _ := url.Parse(r.FormValue("url"))
resp, err := http.Get(grabUrl.String())
if err != nil {
oopsHandler(c, w, r, RespAUTO, "Could not retrieve URL")
return
}
upReq.filename = filepath.Base(grabUrl.Path)
upReq.src = resp.Body
upReq.deletionKey = r.FormValue("deletekey")
upReq.randomBarename = r.FormValue("randomize") == "yes"
upReq.expiry = parseExpiry(r.FormValue("expiry"))
upload, err := processUpload(upReq)
if strings.EqualFold("application/json", r.Header.Get("Accept")) {
if err != nil {
oopsHandler(c, w, r, RespJSON, "Could not upload file: "+err.Error())
return
}
js := generateJSONresponse(upload)
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.Write(js)
} else {
if err != nil {
oopsHandler(c, w, r, RespHTML, "Could not upload file: "+err.Error())
return
}
http.Redirect(w, r, Config.sitePath+upload.Filename, 303)
}
}
func uploadHeaderProcess(r *http.Request, upReq *UploadRequest) {
if r.Header.Get("Linx-Randomize") == "yes" {
upReq.randomBarename = true
}
upReq.deletionKey = r.Header.Get("Linx-Delete-Key")
// Get seconds until expiry. Non-integer responses never expire.
expStr := r.Header.Get("Linx-Expiry")
upReq.expiry = parseExpiry(expStr)
}
func processUpload(upReq UploadRequest) (upload Upload, err error) {
// Determine the appropriate filename, then write to disk
barename, extension := barePlusExt(upReq.filename)
if upReq.randomBarename || len(barename) == 0 {
barename = generateBarename()
}
var header []byte
if len(extension) == 0 {
// Pull the first 512 bytes off for use in MIME detection
header = make([]byte, 512)
n, _ := upReq.src.Read(header)
if n == 0 {
return upload, errors.New("Empty file")
}
header = header[:n]
// Determine the type of file from header
mimetype := mimemagic.Match("", header)
// If the mime type is in our map, use that
// otherwise just use "ext"
if val, exists := mimeToExtension[mimetype]; exists {
extension = val
} else {
extension = "ext"
}
}
upload.Filename = strings.Join([]string{barename, extension}, ".")
_, err = os.Stat(path.Join(Config.filesDir, upload.Filename))
fileexists := err == nil
// Check if the delete key matches, in which case overwrite
if fileexists {
metad, merr := metadataRead(upload.Filename)
if merr == nil {
if upReq.deletionKey == metad.DeleteKey {
fileexists = false
}
}
}
for fileexists {
counter, err := strconv.Atoi(string(barename[len(barename)-1]))
if err != nil {
barename = barename + "1"
} else {
barename = barename[:len(barename)-1] + strconv.Itoa(counter+1)
}
upload.Filename = strings.Join([]string{barename, extension}, ".")
_, err = os.Stat(path.Join(Config.filesDir, upload.Filename))
fileexists = err == nil
}
if fileBlacklist[strings.ToLower(upload.Filename)] {
return upload, errors.New("Prohibited filename")
}
dst, err := os.Create(path.Join(Config.filesDir, upload.Filename))
if err != nil {
return
}
defer dst.Close()
// Get the rest of the metadata needed for storage
var expiry time.Time
if upReq.expiry == 0 {
expiry = neverExpire
} else {
expiry = time.Now().Add(upReq.expiry)
}
bytes, err := io.Copy(dst, io.MultiReader(bytes.NewReader(header), upReq.src))
if bytes == 0 {
os.Remove(path.Join(Config.filesDir, upload.Filename))
return upload, errors.New("Empty file")
} else if err != nil {
os.Remove(path.Join(Config.filesDir, upload.Filename))
return
} else if bytes > Config.maxSize {
os.Remove(path.Join(Config.filesDir, upload.Filename))
return upload, errors.New("File too large")
}
upload.Metadata, err = generateMetadata(upload.Filename, expiry, upReq.deletionKey)
if err != nil {
os.Remove(path.Join(Config.filesDir, upload.Filename))
os.Remove(path.Join(Config.metaDir, upload.Filename))
return
}
err = metadataWrite(upload.Filename, &upload.Metadata)
if err != nil {
os.Remove(path.Join(Config.filesDir, upload.Filename))
os.Remove(path.Join(Config.metaDir, upload.Filename))
return
}
return
}
func generateBarename() string {
return uniuri.NewLenChars(8, []byte("abcdefghijklmnopqrstuvwxyz0123456789"))
}
func generateJSONresponse(upload Upload) []byte {
js, _ := json.Marshal(map[string]string{
"url": Config.siteURL + upload.Filename,
"filename": upload.Filename,
"delete_key": upload.Metadata.DeleteKey,
"expiry": strconv.FormatInt(upload.Metadata.Expiry.Unix(), 10),
"size": strconv.FormatInt(upload.Metadata.Size, 10),
"mimetype": upload.Metadata.Mimetype,
"sha256sum": upload.Metadata.Sha256sum,
})
return js
}
var bareRe = regexp.MustCompile(`[^A-Za-z0-9\-]`)
var extRe = regexp.MustCompile(`[^A-Za-z0-9\-\.]`)
var compressedExts = map[string]bool{
".bz2": true,
".gz": true,
".xz": true,
}
var archiveExts = map[string]bool{
".tar": true,
}
func barePlusExt(filename string) (barename, extension string) {
filename = strings.TrimSpace(filename)
filename = strings.ToLower(filename)
extension = path.Ext(filename)
barename = filename[:len(filename)-len(extension)]
if compressedExts[extension] {
ext2 := path.Ext(barename)
if archiveExts[ext2] {
barename = barename[:len(barename)-len(ext2)]
extension = ext2 + extension
}
}
extension = extRe.ReplaceAllString(extension, "")
barename = bareRe.ReplaceAllString(barename, "")
extension = strings.Trim(extension, "-.")
barename = strings.Trim(barename, "-")
return
}
func parseExpiry(expStr string) time.Duration {
if expStr == "" {
return 0
} else {
expiry, err := strconv.ParseInt(expStr, 10, 64)
if err != nil {
return 0
} else {
return time.Duration(expiry) * time.Second
}
}
}