mirror of
https://github.com/dutchcoders/transfer.sh.git
synced 2024-12-05 02:10:18 +01:00
cb6e5cb0c7
* use dep for vendoring * lets encrypt * moved web to transfer.sh-web repo * single command install * added first tests
231 lines
6.2 KiB
Go
231 lines
6.2 KiB
Go
package handlers
|
|
|
|
// Inspired by node's Connect library implementation of the logging middleware
|
|
// https://github.com/senchalabs/connect
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/PuerkitoBio/ghost"
|
|
)
|
|
|
|
const (
|
|
// Predefined logging formats that can be passed as format string.
|
|
Ldefault = "_default_"
|
|
Lshort = "_short_"
|
|
Ltiny = "_tiny_"
|
|
)
|
|
|
|
var (
|
|
// Token parser for request and response headers
|
|
rxHeaders = regexp.MustCompile(`^(req|res)\[([^\]]+)\]$`)
|
|
|
|
// Lookup table for predefined formats
|
|
predefFormats = map[string]struct {
|
|
fmt string
|
|
toks []string
|
|
}{
|
|
Ldefault: {
|
|
`%s - - [%s] "%s %s HTTP/%s" %d %s "%s" "%s"`,
|
|
[]string{"remote-addr", "date", "method", "url", "http-version", "status", "res[Content-Length]", "referrer", "user-agent"},
|
|
},
|
|
Lshort: {
|
|
`%s - %s %s HTTP/%s %d %s - %.3f s`,
|
|
[]string{"remote-addr", "method", "url", "http-version", "status", "res[Content-Length]", "response-time"},
|
|
},
|
|
Ltiny: {
|
|
`%s %s %d %s - %.3f s`,
|
|
[]string{"method", "url", "status", "res[Content-Length]", "response-time"},
|
|
},
|
|
}
|
|
)
|
|
|
|
// Augmented ResponseWriter implementation that captures the status code for the logger.
|
|
type statusResponseWriter struct {
|
|
http.ResponseWriter
|
|
code int
|
|
oriURL string
|
|
}
|
|
|
|
// Intercept the WriteHeader call to save the status code.
|
|
func (this *statusResponseWriter) WriteHeader(code int) {
|
|
this.code = code
|
|
this.ResponseWriter.WriteHeader(code)
|
|
}
|
|
|
|
// Intercept the Write call to save the default status code.
|
|
func (this *statusResponseWriter) Write(data []byte) (int, error) {
|
|
if this.code == 0 {
|
|
this.code = http.StatusOK
|
|
}
|
|
return this.ResponseWriter.Write(data)
|
|
}
|
|
|
|
// Implement the WrapWriter interface.
|
|
func (this *statusResponseWriter) WrappedWriter() http.ResponseWriter {
|
|
return this.ResponseWriter
|
|
}
|
|
|
|
// LogHandler options
|
|
type LogOptions struct {
|
|
LogFn func(string, ...interface{}) // Defaults to ghost.LogFn if nil
|
|
Format string
|
|
Tokens []string
|
|
CustomTokens map[string]func(http.ResponseWriter, *http.Request) string
|
|
Immediate bool
|
|
DateFormat string
|
|
}
|
|
|
|
// Create a new LogOptions struct. The DateFormat defaults to time.RFC3339.
|
|
func NewLogOptions(l func(string, ...interface{}), ft string, tok ...string) *LogOptions {
|
|
return &LogOptions{
|
|
LogFn: l,
|
|
Format: ft,
|
|
Tokens: tok,
|
|
CustomTokens: make(map[string]func(http.ResponseWriter, *http.Request) string),
|
|
DateFormat: time.RFC3339,
|
|
}
|
|
}
|
|
|
|
// LogHandlerFunc is the same as LogHandler, it is just a convenience
|
|
// signature that accepts a func(http.ResponseWriter, *http.Request) instead of
|
|
// a http.Handler interface. It saves the boilerplate http.HandlerFunc() cast.
|
|
func LogHandlerFunc(h http.HandlerFunc, opts *LogOptions) http.HandlerFunc {
|
|
return LogHandler(h, opts)
|
|
}
|
|
|
|
// Create a log handler for every request it receives.
|
|
func LogHandler(h http.Handler, opts *LogOptions) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := getStatusWriter(w); ok {
|
|
// Self-awareness, logging handler already set up
|
|
h.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
// Save the response start time
|
|
st := time.Now()
|
|
// Call the wrapped handler, with the augmented ResponseWriter to handle the status code
|
|
stw := &statusResponseWriter{w, 0, ""}
|
|
|
|
// Log immediately if requested, otherwise on exit
|
|
if opts.Immediate {
|
|
logRequest(stw, r, st, opts)
|
|
} else {
|
|
// Store original URL, may get modified by handlers (i.e. StripPrefix)
|
|
stw.oriURL = r.URL.String()
|
|
defer logRequest(stw, r, st, opts)
|
|
}
|
|
h.ServeHTTP(stw, r)
|
|
}
|
|
}
|
|
|
|
func getIpAddress(r *http.Request) string {
|
|
hdr := r.Header
|
|
hdrRealIp := hdr.Get("X-Real-Ip")
|
|
hdrForwardedFor := hdr.Get("X-Forwarded-For")
|
|
if hdrRealIp == "" && hdrForwardedFor == "" {
|
|
return r.RemoteAddr
|
|
}
|
|
if hdrForwardedFor != "" {
|
|
// X-Forwarded-For is potentially a list of addresses separated with ","
|
|
part := strings.Split(hdrForwardedFor, ",")[0]
|
|
return strings.TrimSpace(part) + ":0"
|
|
}
|
|
return hdrRealIp
|
|
}
|
|
|
|
// Check if the specified token is a predefined one, and if so return its current value.
|
|
func getPredefinedTokenValue(t string, w *statusResponseWriter, r *http.Request,
|
|
st time.Time, opts *LogOptions) (interface{}, bool) {
|
|
|
|
switch t {
|
|
case "http-version":
|
|
return fmt.Sprintf("%d.%d", r.ProtoMajor, r.ProtoMinor), true
|
|
case "response-time":
|
|
return time.Now().Sub(st).Seconds(), true
|
|
case "remote-addr":
|
|
return getIpAddress(r), true
|
|
case "date":
|
|
return time.Now().Format(opts.DateFormat), true
|
|
case "method":
|
|
return r.Method, true
|
|
case "url":
|
|
if w.oriURL != "" {
|
|
return w.oriURL, true
|
|
}
|
|
return r.URL.String(), true
|
|
case "referrer", "referer":
|
|
return r.Referer(), true
|
|
case "user-agent":
|
|
return r.UserAgent(), true
|
|
case "status":
|
|
return w.code, true
|
|
}
|
|
|
|
// Handle special cases for header
|
|
mtch := rxHeaders.FindStringSubmatch(t)
|
|
if len(mtch) > 2 {
|
|
if mtch[1] == "req" {
|
|
return r.Header.Get(mtch[2]), true
|
|
} else {
|
|
// This only works for headers explicitly set via the Header() map of
|
|
// the writer, not those added by the http package under the covers.
|
|
return w.Header().Get(mtch[2]), true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// Do the actual logging.
|
|
func logRequest(w *statusResponseWriter, r *http.Request, st time.Time, opts *LogOptions) {
|
|
var (
|
|
fn func(string, ...interface{})
|
|
ok bool
|
|
format string
|
|
toks []string
|
|
)
|
|
|
|
// If no specific log function, use the default one from the ghost package
|
|
if opts.LogFn == nil {
|
|
fn = ghost.LogFn
|
|
} else {
|
|
fn = opts.LogFn
|
|
}
|
|
|
|
// If this is a predefined format, use it instead
|
|
if v, ok := predefFormats[opts.Format]; ok {
|
|
format = v.fmt
|
|
toks = v.toks
|
|
} else {
|
|
format = opts.Format
|
|
toks = opts.Tokens
|
|
}
|
|
args := make([]interface{}, len(toks))
|
|
for i, t := range toks {
|
|
if args[i], ok = getPredefinedTokenValue(t, w, r, st, opts); !ok {
|
|
if f, ok := opts.CustomTokens[t]; ok && f != nil {
|
|
args[i] = f(w, r)
|
|
} else {
|
|
args[i] = "?"
|
|
}
|
|
}
|
|
}
|
|
fn(format, args...)
|
|
}
|
|
|
|
// Helper function to retrieve the status writer.
|
|
func getStatusWriter(w http.ResponseWriter) (*statusResponseWriter, bool) {
|
|
st, ok := GetResponseWriter(w, func(tst http.ResponseWriter) bool {
|
|
_, ok := tst.(*statusResponseWriter)
|
|
return ok
|
|
})
|
|
if ok {
|
|
return st.(*statusResponseWriter), true
|
|
}
|
|
return nil, false
|
|
}
|