e25b19f028
If the client accepts gzip, just serve the file as a gzip-encoded plain text. If it doesn't, gunzip it first.
297 lines
7.4 KiB
Go
297 lines
7.4 KiB
Go
/*
|
|
Copyright 2017 Vector Creations Ltd
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"crypto/subtle"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var maxPayloadSize = 1024 * 1024 * 55 // 55 MB
|
|
|
|
type LogEntry struct {
|
|
ID string `json:"id"`
|
|
Lines string `json:"lines"`
|
|
}
|
|
|
|
type Payload struct {
|
|
Text string `json:"text"`
|
|
Version string `json:"version"`
|
|
UserAgent string `json:"user_agent"`
|
|
Logs []LogEntry `json:"logs"`
|
|
}
|
|
|
|
func respond(code int, w http.ResponseWriter) {
|
|
w.WriteHeader(code)
|
|
w.Write([]byte("{}"))
|
|
}
|
|
|
|
func gzipAndSave(data []byte, dirname, fpath string) error {
|
|
_ = os.MkdirAll(filepath.Join("bugs", dirname), os.ModePerm)
|
|
fpath = filepath.Join("bugs", dirname, fpath)
|
|
|
|
if _, err := os.Stat(fpath); err == nil {
|
|
return fmt.Errorf("file already exists") // the user can just retry
|
|
}
|
|
var b bytes.Buffer
|
|
gz := gzip.NewWriter(&b)
|
|
if _, err := gz.Write(data); err != nil {
|
|
return err
|
|
}
|
|
if err := gz.Flush(); err != nil {
|
|
return err
|
|
}
|
|
if err := gz.Close(); err != nil {
|
|
return err
|
|
}
|
|
if err := ioutil.WriteFile(fpath, b.Bytes(), 0644); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func basicAuth(handler http.Handler, username, password, realm string) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
user, pass, ok := r.BasicAuth() // pull creds from the request
|
|
|
|
// check user and pass securely
|
|
if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 {
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`)
|
|
w.WriteHeader(401)
|
|
w.Write([]byte("Unauthorised.\n"))
|
|
return
|
|
}
|
|
|
|
handler.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func main() {
|
|
http.HandleFunc("/api/submit", func(w http.ResponseWriter, req *http.Request) {
|
|
if req.Method != "POST" && req.Method != "OPTIONS" {
|
|
respond(405, w)
|
|
return
|
|
}
|
|
// Set CORS
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
|
|
if req.Method == "OPTIONS" {
|
|
respond(200, w)
|
|
return
|
|
}
|
|
if length, err := strconv.Atoi(req.Header.Get("Content-Length")); err != nil || length > maxPayloadSize {
|
|
respond(413, w)
|
|
return
|
|
}
|
|
var p Payload
|
|
if err := json.NewDecoder(req.Body).Decode(&p); err != nil {
|
|
respond(400, w)
|
|
return
|
|
}
|
|
// Dump bug report to disk as form:
|
|
// "bugreport-20170115-112233.log.gz" => user text, version, user agent, # logs
|
|
// "bugreport-20170115-112233-0.log.gz" => most recent log
|
|
// "bugreport-20170115-112233-1.log.gz" => ...
|
|
// "bugreport-20170115-112233-N.log.gz" => oldest log
|
|
t := time.Now().UTC()
|
|
prefix := t.Format("2006-01-02/150405")
|
|
summary := fmt.Sprintf(
|
|
"%s\n\nNumber of logs: %d\nVersion: %s\nUser-Agent: %s\n", p.Text, len(p.Logs), p.Version, p.UserAgent,
|
|
)
|
|
if err := gzipAndSave([]byte(summary), prefix, "details.log.gz"); err != nil {
|
|
respond(500, w)
|
|
return
|
|
}
|
|
for i, log := range p.Logs {
|
|
if err := gzipAndSave([]byte(log.Lines), prefix, fmt.Sprintf("logs-%d.log.gz", i)); err != nil {
|
|
respond(500, w)
|
|
return // TODO: Rollback?
|
|
}
|
|
}
|
|
respond(200, w)
|
|
})
|
|
|
|
// Make sure bugs directory exists
|
|
_ = os.Mkdir("bugs", os.ModePerm)
|
|
|
|
// serve files under "bugs"
|
|
ls := &logServer{"bugs"}
|
|
fs := http.StripPrefix("/api/listing/", ls)
|
|
|
|
// set auth if env vars exist
|
|
usr := os.Getenv("BUGS_USER")
|
|
pass := os.Getenv("BUGS_PASS")
|
|
if usr == "" || pass == "" {
|
|
fmt.Println("BUGS_USER and BUGS_PASS env vars not found. No authentication is running for /api/listing")
|
|
} else {
|
|
fs = basicAuth(fs, usr, pass, "Riot bug reports")
|
|
}
|
|
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 == '\\' }
|