partage

File upload system
git clone git://git.z3bra.org/partage.git
Log | Files | Refs | README | LICENSE

partage.go (8391B)


      1 package main
      2 
      3 import (
      4 	"encoding/json"
      5 	"flag"
      6 	"fmt"
      7 	"html/template"
      8 	"io"
      9 	"io/ioutil"
     10 	"log"
     11 	"net"
     12 	"net/http"
     13 	"net/http/fcgi"
     14 	"os"
     15 	"os/signal"
     16 	"os/user"
     17 	"path"
     18 	"path/filepath"
     19 	"strconv"
     20 	"syscall"
     21 	"time"
     22 
     23 	"github.com/dustin/go-humanize"
     24 	"gopkg.in/ini.v1"
     25 )
     26 
     27 type templatedata struct {
     28 	Links   []string
     29 	Size    string
     30 	Maxsize string
     31 }
     32 
     33 type metadata struct {
     34 	Filename string
     35 	Size     int64
     36 	Expiry   int64
     37 }
     38 
     39 var conf struct {
     40 	user     string
     41 	group    string
     42 	chroot   string
     43 	listen     string
     44 	baseuri  string
     45 	rootdir  string
     46 	tmplpath string
     47 	filepath string
     48 	metapath string
     49 	filectx  string
     50 	maxsize  int64
     51 	expiry   int64
     52 }
     53 
     54 var verbose bool
     55 
     56 func writefile(f *os.File, s io.ReadCloser, contentlength int64) error {
     57 	buffer := make([]byte, 4096)
     58 	eof := false
     59 	sz := int64(0)
     60 
     61 	defer f.Sync()
     62 
     63 	for !eof {
     64 		n, err := s.Read(buffer)
     65 		if err != nil && err != io.EOF {
     66 			return err
     67 		} else if err == io.EOF {
     68 			eof = true
     69 		}
     70 
     71 		/* ensure we don't write more than expected */
     72 		r := int64(n)
     73 		if sz+r > contentlength {
     74 			r = contentlength - sz
     75 			eof = true
     76 		}
     77 
     78 		_, err = f.Write(buffer[:r])
     79 		if err != nil {
     80 			return err
     81 		}
     82 		sz += r
     83 	}
     84 
     85 	return nil
     86 }
     87 
     88 func writemeta(filename string, expiry int64) error {
     89 
     90 	f, _ := os.Open(filename)
     91 	stat, _ := f.Stat()
     92 	size := stat.Size()
     93 	f.Close()
     94 
     95 	if expiry < 0 {
     96 		expiry = conf.expiry
     97 	}
     98 
     99 	meta := metadata{
    100 		Filename: filepath.Base(filename),
    101 		Size:     size,
    102 		Expiry:   time.Now().Unix() + expiry,
    103 	}
    104 
    105 	if verbose {
    106 		log.Printf("Saving metadata for %s in %s", meta.Filename, conf.metapath+"/"+meta.Filename+".json")
    107 	}
    108 
    109 	f, err := os.Create(conf.metapath + "/" + meta.Filename + ".json")
    110 	if err != nil {
    111 		return err
    112 	}
    113 	defer f.Close()
    114 
    115 	j, err := json.Marshal(meta)
    116 	if err != nil {
    117 		return err
    118 	}
    119 
    120 	_, err = f.Write(j)
    121 
    122 	return err
    123 }
    124 
    125 func servetemplate(w http.ResponseWriter, f string, d templatedata) {
    126 	t, err := template.ParseFiles(conf.tmplpath + "/" + f)
    127 	if err != nil {
    128 		http.Error(w, "Internal error", http.StatusInternalServerError)
    129 		return
    130 	}
    131 
    132 	if verbose {
    133 		log.Printf("Serving template %s", t.Name())
    134 	}
    135 
    136 	err = t.Execute(w, d)
    137 	if err != nil {
    138 		fmt.Println(err)
    139 	}
    140 }
    141 
    142 func uploaderPut(w http.ResponseWriter, r *http.Request) {
    143 	/* limit upload size */
    144 	if r.ContentLength > conf.maxsize {
    145 		http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
    146 	}
    147 
    148 	tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(r.URL.Path))
    149 	f, err := os.Create(tmp.Name())
    150 	if err != nil {
    151 		fmt.Println(err)
    152 		return
    153 	}
    154 	defer f.Close()
    155 
    156 	if verbose {
    157 		log.Printf("Writing %d bytes to %s", r.ContentLength, tmp.Name())
    158 	}
    159 
    160 	if err = writefile(f, r.Body, r.ContentLength); err != nil {
    161 		http.Error(w, "Internal error", http.StatusInternalServerError)
    162 		defer os.Remove(tmp.Name())
    163 		return
    164 	}
    165 	writemeta(tmp.Name(), conf.expiry)
    166 
    167 	resp := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
    168 	w.Write([]byte(resp + "\r\n"))
    169 }
    170 
    171 func uploaderPost(w http.ResponseWriter, r *http.Request) {
    172 	/* read 32Mb at a time */
    173 	r.ParseMultipartForm(32 << 20)
    174 
    175 	links := []string{}
    176 	for _, h := range r.MultipartForm.File["file"] {
    177 		if h.Size > conf.maxsize {
    178 			http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
    179 			return
    180 		}
    181 
    182 		post, err := h.Open()
    183 		if err != nil {
    184 			http.Error(w, "Internal error", http.StatusInternalServerError)
    185 			return
    186 		}
    187 		defer post.Close()
    188 
    189 		tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(h.Filename))
    190 		f, err := os.Create(tmp.Name())
    191 		if err != nil {
    192 			http.Error(w, "Internal error", http.StatusInternalServerError)
    193 			return
    194 		}
    195 		defer f.Close()
    196 
    197 		if err = writefile(f, post, h.Size); err != nil {
    198 			http.Error(w, "Internal error", http.StatusInternalServerError)
    199 			defer os.Remove(tmp.Name())
    200 			return
    201 		}
    202 
    203 		expiry, err := strconv.Atoi(r.PostFormValue("expiry"))
    204 		if err != nil || expiry < 0 {
    205 			expiry = int(conf.expiry)
    206 		}
    207 		writemeta(tmp.Name(), int64(expiry))
    208 
    209 		link := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
    210 		links = append(links, link)
    211 	}
    212 
    213 	switch r.PostFormValue("output") {
    214 	case "html":
    215 		data := templatedata{
    216 			Maxsize: humanize.IBytes(uint64(conf.maxsize)),
    217 			Links: links,
    218 		}
    219 		servetemplate(w, "/index.html", data)
    220 	case "json":
    221 		data, _ := json.Marshal(links)
    222 		w.Write(data)
    223 	default:
    224 		for _, link := range links {
    225 			w.Write([]byte(link + "\r\n"))
    226 		}
    227 	}
    228 }
    229 
    230 func uploaderGet(w http.ResponseWriter, r *http.Request) {
    231 	// r.URL.Path is sanitized regarding "." and ".."
    232 	filename := r.URL.Path
    233 	if r.URL.Path == "/" || r.URL.Path == "/index.html" {
    234 		data := templatedata{Maxsize: humanize.IBytes(uint64(conf.maxsize))}
    235 		servetemplate(w, "/index.html", data)
    236 		return
    237 	}
    238 
    239 	if verbose {
    240 		log.Printf("Serving file %s", conf.rootdir+filename)
    241 	}
    242 
    243 	http.ServeFile(w, r, conf.rootdir+filename)
    244 }
    245 
    246 func uploader(w http.ResponseWriter, r *http.Request) {
    247 	if verbose {
    248 		log.Printf("%s: <%s> %s %s %s", r.Host, r.RemoteAddr, r.Method, r.RequestURI, r.Proto)
    249 	}
    250 
    251 	switch r.Method {
    252 	case "POST":
    253 		uploaderPost(w, r)
    254 	case "PUT":
    255 		uploaderPut(w, r)
    256 	case "GET":
    257 		uploaderGet(w, r)
    258 	}
    259 }
    260 
    261 func parseconfig(file string) error {
    262 	cfg, err := ini.Load(file)
    263 	if err != nil {
    264 		return err
    265 	}
    266 
    267 	conf.listen = cfg.Section("").Key("listen").String()
    268 	conf.user = cfg.Section("").Key("user").String()
    269 	conf.group = cfg.Section("").Key("group").String()
    270 	conf.baseuri = cfg.Section("").Key("baseuri").String()
    271 	conf.filepath = cfg.Section("").Key("filepath").String()
    272 	conf.metapath = cfg.Section("").Key("metapath").String()
    273 	conf.filectx = cfg.Section("").Key("filectx").String()
    274 	conf.rootdir = cfg.Section("").Key("rootdir").String()
    275 	conf.chroot = cfg.Section("").Key("chroot").String()
    276 	conf.tmplpath = cfg.Section("").Key("tmplpath").String()
    277 	conf.maxsize, _ = cfg.Section("").Key("maxsize").Int64()
    278 	conf.expiry, _ = cfg.Section("").Key("expiry").Int64()
    279 
    280 	return nil
    281 }
    282 
    283 func usergroupids(username string, groupname string) (int, int, error) {
    284 	u, err := user.Lookup(username)
    285 	if err != nil {
    286 		return -1, -1, err
    287 	}
    288 
    289 	uid, _ := strconv.Atoi(u.Uid)
    290 	gid, _ := strconv.Atoi(u.Gid)
    291 
    292 	if conf.group != "" {
    293 		g, err := user.LookupGroup(groupname)
    294 		if err != nil {
    295 			return uid, -1, err
    296 		}
    297 		gid, _ = strconv.Atoi(g.Gid)
    298 	}
    299 
    300 	return uid, gid, nil
    301 }
    302 
    303 func main() {
    304 	var err error
    305 	var configfile string
    306 	var listener net.Listener
    307 
    308 	/* default values */
    309 	conf.listen = "0.0.0.0:8080"
    310 	conf.baseuri = "http://127.0.0.1:8080"
    311 	conf.rootdir = "static"
    312 	conf.tmplpath = "templates"
    313 	conf.filepath = "files"
    314 	conf.metapath = "meta"
    315 	conf.filectx = "/f/"
    316 	conf.maxsize = 34359738368
    317 	conf.expiry = 86400
    318 
    319 	flag.StringVar(&configfile, "f", "", "Configuration file")
    320 	flag.BoolVar(&verbose, "v", false, "Verbose logging")
    321 	flag.Parse()
    322 
    323 	if configfile != "" {
    324 		if verbose {
    325 			log.Printf("Reading configuration %s", configfile)
    326 		}
    327 		parseconfig(configfile)
    328 	}
    329 
    330 	if conf.chroot != "" {
    331 		if verbose {
    332 			log.Printf("Changing root to %s", conf.chroot)
    333 		}
    334 		syscall.Chroot(conf.chroot)
    335 	}
    336 
    337 	if conf.listen[0] == '/' {
    338 		/* Remove any stale socket */
    339 		os.Remove(conf.listen)
    340 		if listener, err = net.Listen("unix", conf.listen); err != nil {
    341 			log.Fatal(err)
    342 		}
    343 		defer listener.Close()
    344 
    345 		/*
    346 		 * Ensure unix socket is removed on exit.
    347 		 * Note: this might not work when dropping privileges…
    348 		 */
    349 		defer os.Remove(conf.listen)
    350 		sigs := make(chan os.Signal, 1)
    351 		signal.Notify(sigs, os.Interrupt, os.Kill, syscall.SIGTERM)
    352 		go func() {
    353 			_ = <-sigs
    354 			listener.Close()
    355 			if err = os.Remove(conf.listen); err != nil {
    356 				log.Fatal(err)
    357 			}
    358 			os.Exit(0)
    359 		}()
    360 	} else {
    361 		if listener, err = net.Listen("tcp", conf.listen); err != nil {
    362 			log.Fatal(err)
    363 		}
    364 		defer listener.Close()
    365 	}
    366 
    367 	if conf.user != "" {
    368 		if verbose {
    369 			log.Printf("Dropping privileges to %s", conf.user)
    370 		}
    371 		uid, gid, err := usergroupids(conf.user, conf.group)
    372 		if err != nil {
    373 			log.Fatal(err)
    374 		}
    375 
    376 		if listener.Addr().Network() == "unix" {
    377 			os.Chown(conf.listen, uid, gid)
    378 		}
    379 
    380 		syscall.Setuid(uid)
    381 		syscall.Setgid(gid)
    382 	}
    383 
    384 	http.HandleFunc("/", uploader)
    385 	http.Handle(conf.filectx, http.StripPrefix(conf.filectx, http.FileServer(http.Dir(conf.filepath))))
    386 
    387 	if verbose {
    388 		log.Printf("Listening on %s", conf.listen)
    389 	}
    390 
    391 	if listener.Addr().Network() == "unix" {
    392 		err = fcgi.Serve(listener, nil)
    393 		log.Fatal(err) /* NOTREACHED */
    394 	}
    395 
    396 	err = http.Serve(listener, nil)
    397 	log.Fatal(err) /* NOTREACHED */
    398 }