diff --git a/src/github.com/matrix-org/rageshake/main.go b/src/github.com/matrix-org/rageshake/main.go index 1fd7eec..467a24f 100644 --- a/src/github.com/matrix-org/rageshake/main.go +++ b/src/github.com/matrix-org/rageshake/main.go @@ -22,12 +22,15 @@ import ( "crypto/subtle" "encoding/json" "fmt" + "io" "io/ioutil" "log" "net/http" "os" + "path" "path/filepath" "strconv" + "strings" "time" ) @@ -140,8 +143,8 @@ func main() { _ = os.Mkdir("bugs", os.ModePerm) // serve files under "bugs" - fs := http.FileServer(http.Dir("bugs")) - fs = http.StripPrefix("/api/listing/", fs) + ls := &logServer{"bugs"} + fs := http.StripPrefix("/api/listing/", ls) // set auth if env vars exist usr := os.Getenv("BUGS_USER") @@ -154,5 +157,140 @@ func main() { http.Handle("/api/listing/", fs) port := os.Args[1] + log.Println("Listening on port", port) + log.Fatal(http.ListenAndServe(":"+port, nil)) } + +type logServer struct { + root string +} + +func (f *logServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + upath := r.URL.Path + + if !strings.HasPrefix(upath, "/") { + upath = "/" + upath + r.URL.Path = upath + } + + log.Println("Serving", upath) + + // eliminate ., .., //, etc + upath = path.Clean(upath) + + // reject some dodgy paths + if containsDotDot(upath) || strings.Contains(upath, "\x00") || (filepath.Separator != '/' && strings.IndexRune(upath, filepath.Separator) >= 0) { + http.Error(w, "invalid URL path", http.StatusBadRequest) + return + } + + // convert to abs path + upath, err := filepath.Abs(filepath.Join(f.root, filepath.FromSlash(upath))) + + if err != nil { + msg, code := toHTTPError(err) + http.Error(w, msg, code) + return + } + + serveFile(w, r, upath) +} + +func serveFile(w http.ResponseWriter, r *http.Request, path string) { + d, err := os.Stat(path) + if err != nil { + msg, code := toHTTPError(err) + http.Error(w, msg, code) + return + } + + // if it's a directory, or doesn't look like a gzip, serve as normal + if d.IsDir() || !strings.HasSuffix(path, ".gz") { + log.Println("Serving", path) + http.ServeFile(w, r, path) + return + } + + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + + acceptsGzip := false + splitRune := func(s rune) bool { return s == ' ' || s == '\t' || s == '\n' || s == ',' } + for _, hdr := range r.Header["Accept-Encoding"] { + for _, enc := range strings.FieldsFunc(hdr, splitRune) { + if enc == "gzip" { + acceptsGzip = true + break + } + } + } + + if acceptsGzip { + serveGzip(w, r, path, d.Size()) + } else { + serveUngzipped(w, r, path) + } +} + +// serveGzip serves a gzipped file with gzip content-encoding +func serveGzip(w http.ResponseWriter, r *http.Request, path string, size int64) { + f, err := os.Open(path) + if err != nil { + msg, code := toHTTPError(err) + http.Error(w, msg, code) + return + } + defer f.Close() + + w.Header().Set("Content-Encoding", "gzip") + w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) + + w.WriteHeader(http.StatusOK) + io.Copy(w, f) +} + +// serveUngzipped ungzips a gzipped file and serves it +func serveUngzipped(w http.ResponseWriter, r *http.Request, path string) { + f, err := os.Open(path) + if err != nil { + msg, code := toHTTPError(err) + http.Error(w, msg, code) + return + } + defer f.Close() + + gz, err := gzip.NewReader(f) + if err != nil { + msg, code := toHTTPError(err) + http.Error(w, msg, code) + return + } + defer gz.Close() + + w.WriteHeader(http.StatusOK) + io.Copy(w, gz) +} + +func toHTTPError(err error) (msg string, httpStatus int) { + if os.IsNotExist(err) { + return "404 page not found", http.StatusNotFound + } + if os.IsPermission(err) { + return "403 Forbidden", http.StatusForbidden + } + // Default: + return "500 Internal Server Error", http.StatusInternalServerError +} + +func containsDotDot(v string) bool { + if !strings.Contains(v, "..") { + return false + } + for _, ent := range strings.FieldsFunc(v, isSlashRune) { + if ent == ".." { + return true + } + } + return false +} +func isSlashRune(r rune) bool { return r == '/' || r == '\\' }