source: code/trunk/partage.go@ 48

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

Rename "bind" parameter to "listen"

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 listen 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 switch r.PostFormValue("output") {
207 case "html":
208 data := templatedata{Links: links}
209 servetemplate(w, "/upload.html", data)
210 case "json":
211 data, _ := json.Marshal(links)
212 w.Write(data)
213 default:
214 for _, link := range links {
215 w.Write([]byte(link + "\r\n"))
216 }
217 }
218}
219
220func uploaderGet(w http.ResponseWriter, r *http.Request) {
221 // r.URL.Path is sanitized regarding "." and ".."
222 filename := r.URL.Path
223 if r.URL.Path == "/" || r.URL.Path == "/index.html" {
224 data := templatedata{Maxsize: humanize.IBytes(uint64(conf.maxsize))}
225 servetemplate(w, "/index.html", data)
226 return
227 }
228
229 if verbose {
230 log.Printf("Serving file %s", conf.rootdir+filename)
231 }
232
233 http.ServeFile(w, r, conf.rootdir+filename)
234}
235
236func uploader(w http.ResponseWriter, r *http.Request) {
237 if verbose {
238 log.Printf("%s: <%s> %s %s %s", r.Host, r.RemoteAddr, r.Method, r.RequestURI, r.Proto)
239 }
240
241 switch r.Method {
242 case "POST":
243 uploaderPost(w, r)
244 case "PUT":
245 uploaderPut(w, r)
246 case "GET":
247 uploaderGet(w, r)
248 }
249}
250
251func parseconfig(file string) error {
252 cfg, err := ini.Load(file)
253 if err != nil {
254 return err
255 }
256
257 conf.listen = cfg.Section("").Key("listen").String()
258 conf.user = cfg.Section("").Key("user").String()
259 conf.group = cfg.Section("").Key("group").String()
260 conf.baseuri = cfg.Section("").Key("baseuri").String()
261 conf.filepath = cfg.Section("").Key("filepath").String()
262 conf.metapath = cfg.Section("").Key("metapath").String()
263 conf.filectx = cfg.Section("").Key("filectx").String()
264 conf.metactx = cfg.Section("").Key("metactx").String()
265 conf.rootdir = cfg.Section("").Key("rootdir").String()
266 conf.chroot = cfg.Section("").Key("chroot").String()
267 conf.tmplpath = cfg.Section("").Key("tmplpath").String()
268 conf.maxsize, _ = cfg.Section("").Key("maxsize").Int64()
269 conf.expiry, _ = cfg.Section("").Key("expiry").Int64()
270
271 return nil
272}
273
274func usergroupids(username string, groupname string) (int, int, error) {
275 u, err := user.Lookup(username)
276 if err != nil {
277 return -1, -1, err
278 }
279
280 uid, _ := strconv.Atoi(u.Uid)
281 gid, _ := strconv.Atoi(u.Gid)
282
283 if conf.group != "" {
284 g, err := user.LookupGroup(groupname)
285 if err != nil {
286 return uid, -1, err
287 }
288 gid, _ = strconv.Atoi(g.Gid)
289 }
290
291 return uid, gid, nil
292}
293
294func main() {
295 var err error
296 var configfile string
297 var listener net.Listener
298
299 /* default values */
300 conf.listen = "0.0.0.0:8080"
301 conf.baseuri = "http://127.0.0.1:8080"
302 conf.rootdir = "static"
303 conf.tmplpath = "templates"
304 conf.filepath = "files"
305 conf.metapath = "meta"
306 conf.filectx = "/f/"
307 conf.metactx = "/m/"
308 conf.maxsize = 34359738368
309 conf.expiry = 86400
310
311 flag.StringVar(&configfile, "f", "", "Configuration file")
312 flag.BoolVar(&verbose, "v", false, "Verbose logging")
313 flag.Parse()
314
315 if configfile != "" {
316 if verbose {
317 log.Printf("Reading configuration %s", configfile)
318 }
319 parseconfig(configfile)
320 }
321
322 if conf.chroot != "" {
323 if verbose {
324 log.Printf("Changing root to %s", conf.chroot)
325 }
326 syscall.Chroot(conf.chroot)
327 }
328
329 if conf.listen[0] == '/' {
330 listener, err = net.Listen("unix", conf.listen)
331 if err != nil {
332 log.Fatal(err)
333 }
334
335 /* Ensure unix socket is removed on exit */
336 defer os.Remove(conf.listen)
337 sigs := make(chan os.Signal, 1)
338 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
339 go func() {
340 _ = <-sigs
341 os.Remove(conf.listen)
342 os.Exit(0)
343 }()
344 } else {
345 listener, err = net.Listen("tcp", conf.listen)
346 if err != nil {
347 log.Fatal(err)
348 }
349 }
350
351 if conf.user != "" {
352 if verbose {
353 log.Printf("Dropping privileges to %s", conf.user)
354 }
355 uid, gid, err := usergroupids(conf.user, conf.group)
356 if err != nil {
357 log.Fatal(err)
358 }
359
360 if listener.Addr().Network() == "unix" {
361 os.Chown(conf.listen, uid, gid)
362 }
363
364 syscall.Setuid(uid)
365 syscall.Setgid(gid)
366 }
367
368 http.HandleFunc("/", uploader)
369 http.Handle(conf.filectx, http.StripPrefix(conf.filectx, http.FileServer(http.Dir(conf.filepath))))
370
371 if verbose {
372 log.Printf("Listening on %s", conf.listen)
373 }
374
375 if listener.Addr().Network() == "unix" {
376 err = fcgi.Serve(listener, nil)
377 log.Fatal(err) /* NOTREACHED */
378 }
379
380 err = http.Serve(listener, nil)
381 log.Fatal(err) /* NOTREACHED */
382}
Note: See TracBrowser for help on using the repository browser.