source: code/trunk/partage.go@ 62

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

Merge branch 'master' of git.z3bra.org:partage

File size: 8.2 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 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
125func 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
142func 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
171func 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
230func 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
246func 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
261func 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
283func 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
303func 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}
Note: See TracBrowser for help on using the repository browser.