source: code/trunk/partage.go@ 59

Last change on this file since 59 was 59, checked in by dev, 3 years ago

Add workaround for when removing the socket file is not possible

File size: 8.0 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 maxsize int64
51 expiry int64
52}
53
54var verbose bool
55
56func 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
88func 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 meta := metadata{
96 Filename: filepath.Base(filename),
97 Size: size,
98 Expiry: time.Now().Unix() + expiry,
99 }
100
101 if verbose {
102 log.Printf("Saving metadata for %s in %s", meta.Filename, conf.metapath+"/"+meta.Filename+".json")
103 }
104
105 f, err := os.Create(conf.metapath + "/" + meta.Filename + ".json")
106 if err != nil {
107 return err
108 }
109 defer f.Close()
110
111 j, err := json.Marshal(meta)
112 if err != nil {
113 return err
114 }
115
116 _, err = f.Write(j)
117
118 return err
119}
120
121func servetemplate(w http.ResponseWriter, f string, d templatedata) {
122 t, err := template.ParseFiles(conf.tmplpath + "/" + f)
123 if err != nil {
124 http.Error(w, "Internal error", http.StatusInternalServerError)
125 return
126 }
127
128 if verbose {
129 log.Printf("Serving template %s", t.Name())
130 }
131
132 err = t.Execute(w, d)
133 if err != nil {
134 fmt.Println(err)
135 }
136}
137
138func uploaderPut(w http.ResponseWriter, r *http.Request) {
139 /* limit upload size */
140 if r.ContentLength > conf.maxsize {
141 http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
142 }
143
144 tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(r.URL.Path))
145 f, err := os.Create(tmp.Name())
146 if err != nil {
147 fmt.Println(err)
148 return
149 }
150 defer f.Close()
151
152 if verbose {
153 log.Printf("Writing %d bytes to %s", r.ContentLength, tmp.Name())
154 }
155
156 if err = writefile(f, r.Body, r.ContentLength); err != nil {
157 http.Error(w, "Internal error", http.StatusInternalServerError)
158 defer os.Remove(tmp.Name())
159 return
160 }
161 writemeta(tmp.Name(), conf.expiry)
162
163 resp := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
164 w.Write([]byte(resp + "\r\n"))
165}
166
167func uploaderPost(w http.ResponseWriter, r *http.Request) {
168 /* read 32Mb at a time */
169 r.ParseMultipartForm(32 << 20)
170
171 links := []string{}
172 for _, h := range r.MultipartForm.File["file"] {
173 if h.Size > conf.maxsize {
174 http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
175 return
176 }
177
178 post, err := h.Open()
179 if err != nil {
180 http.Error(w, "Internal error", http.StatusInternalServerError)
181 return
182 }
183 defer post.Close()
184
185 tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(h.Filename))
186 f, err := os.Create(tmp.Name())
187 if err != nil {
188 http.Error(w, "Internal error", http.StatusInternalServerError)
189 return
190 }
191 defer f.Close()
192
193 if err = writefile(f, post, h.Size); err != nil {
194 http.Error(w, "Internal error", http.StatusInternalServerError)
195 defer os.Remove(tmp.Name())
196 return
197 }
198
199 writemeta(tmp.Name(), conf.expiry)
200
201 link := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
202 links = append(links, link)
203 }
204
205 switch r.PostFormValue("output") {
206 case "html":
207 data := templatedata{
208 Maxsize: humanize.IBytes(uint64(conf.maxsize)),
209 Links: links,
210 }
211 servetemplate(w, "/index.html", data)
212 case "json":
213 data, _ := json.Marshal(links)
214 w.Write(data)
215 default:
216 for _, link := range links {
217 w.Write([]byte(link + "\r\n"))
218 }
219 }
220}
221
222func uploaderGet(w http.ResponseWriter, r *http.Request) {
223 // r.URL.Path is sanitized regarding "." and ".."
224 filename := r.URL.Path
225 if r.URL.Path == "/" || r.URL.Path == "/index.html" {
226 data := templatedata{Maxsize: humanize.IBytes(uint64(conf.maxsize))}
227 servetemplate(w, "/index.html", data)
228 return
229 }
230
231 if verbose {
232 log.Printf("Serving file %s", conf.rootdir+filename)
233 }
234
235 http.ServeFile(w, r, conf.rootdir+filename)
236}
237
238func uploader(w http.ResponseWriter, r *http.Request) {
239 if verbose {
240 log.Printf("%s: <%s> %s %s %s", r.Host, r.RemoteAddr, r.Method, r.RequestURI, r.Proto)
241 }
242
243 switch r.Method {
244 case "POST":
245 uploaderPost(w, r)
246 case "PUT":
247 uploaderPut(w, r)
248 case "GET":
249 uploaderGet(w, r)
250 }
251}
252
253func parseconfig(file string) error {
254 cfg, err := ini.Load(file)
255 if err != nil {
256 return err
257 }
258
259 conf.listen = cfg.Section("").Key("listen").String()
260 conf.user = cfg.Section("").Key("user").String()
261 conf.group = cfg.Section("").Key("group").String()
262 conf.baseuri = cfg.Section("").Key("baseuri").String()
263 conf.filepath = cfg.Section("").Key("filepath").String()
264 conf.metapath = cfg.Section("").Key("metapath").String()
265 conf.filectx = cfg.Section("").Key("filectx").String()
266 conf.rootdir = cfg.Section("").Key("rootdir").String()
267 conf.chroot = cfg.Section("").Key("chroot").String()
268 conf.tmplpath = cfg.Section("").Key("tmplpath").String()
269 conf.maxsize, _ = cfg.Section("").Key("maxsize").Int64()
270 conf.expiry, _ = cfg.Section("").Key("expiry").Int64()
271
272 return nil
273}
274
275func usergroupids(username string, groupname string) (int, int, error) {
276 u, err := user.Lookup(username)
277 if err != nil {
278 return -1, -1, err
279 }
280
281 uid, _ := strconv.Atoi(u.Uid)
282 gid, _ := strconv.Atoi(u.Gid)
283
284 if conf.group != "" {
285 g, err := user.LookupGroup(groupname)
286 if err != nil {
287 return uid, -1, err
288 }
289 gid, _ = strconv.Atoi(g.Gid)
290 }
291
292 return uid, gid, nil
293}
294
295func main() {
296 var err error
297 var configfile string
298 var listener net.Listener
299
300 /* default values */
301 conf.listen = "0.0.0.0:8080"
302 conf.baseuri = "http://127.0.0.1:8080"
303 conf.rootdir = "static"
304 conf.tmplpath = "templates"
305 conf.filepath = "files"
306 conf.metapath = "meta"
307 conf.filectx = "/f/"
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 /* Remove any stale socket */
331 os.Remove(conf.listen)
332 if listener, err = net.Listen("unix", conf.listen); err != nil {
333 log.Fatal(err)
334 }
335 defer listener.Close()
336
337 /*
338 * Ensure unix socket is removed on exit.
339 * Note: this might not work when dropping privileges…
340 */
341 defer os.Remove(conf.listen)
342 sigs := make(chan os.Signal, 1)
343 signal.Notify(sigs, os.Interrupt, os.Kill, syscall.SIGTERM)
344 go func() {
345 _ = <-sigs
346 listener.Close()
347 if err = os.Remove(conf.listen); err != nil {
348 log.Fatal(err)
349 }
350 os.Exit(0)
351 }()
352 } else {
353 if listener, err = net.Listen("tcp", conf.listen); err != nil {
354 log.Fatal(err)
355 }
356 defer listener.Close()
357 }
358
359 if conf.user != "" {
360 if verbose {
361 log.Printf("Dropping privileges to %s", conf.user)
362 }
363 uid, gid, err := usergroupids(conf.user, conf.group)
364 if err != nil {
365 log.Fatal(err)
366 }
367
368 if listener.Addr().Network() == "unix" {
369 os.Chown(conf.listen, uid, gid)
370 }
371
372 syscall.Setuid(uid)
373 syscall.Setgid(gid)
374 }
375
376 http.HandleFunc("/", uploader)
377 http.Handle(conf.filectx, http.StripPrefix(conf.filectx, http.FileServer(http.Dir(conf.filepath))))
378
379 if verbose {
380 log.Printf("Listening on %s", conf.listen)
381 }
382
383 if listener.Addr().Network() == "unix" {
384 err = fcgi.Serve(listener, nil)
385 log.Fatal(err) /* NOTREACHED */
386 }
387
388 err = http.Serve(listener, nil)
389 log.Fatal(err) /* NOTREACHED */
390}
Note: See TracBrowser for help on using the repository browser.