Serve log files as plain text
If the client accepts gzip, just serve the file as a gzip-encoded plain text. If it doesn't, gunzip it first.
This commit is contained in:
parent
7e551a8fc1
commit
e25b19f028
1 changed files with 140 additions and 2 deletions
|
@ -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 == '\\' }
|
||||
|
|
Reference in a new issue