source: code/trunk/partage.go@ 44

Last change on this file since 44 was 44, checked in by dev, 4 years ago

Change form file attribute to "file"

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