404 lines
11 KiB
Go
404 lines
11 KiB
Go
// Copyright 2013 The Gorilla Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package handlers
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// MethodHandler is an http.Handler that dispatches to a handler whose key in the
|
|
// MethodHandler's map matches the name of the HTTP request's method, eg: GET
|
|
//
|
|
// If the request's method is OPTIONS and OPTIONS is not a key in the map then
|
|
// the handler responds with a status of 200 and sets the Allow header to a
|
|
// comma-separated list of available methods.
|
|
//
|
|
// If the request's method doesn't match any of its keys the handler responds
|
|
// with a status of HTTP 405 "Method Not Allowed" and sets the Allow header to a
|
|
// comma-separated list of available methods.
|
|
type MethodHandler map[string]http.Handler
|
|
|
|
func (h MethodHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
if handler, ok := h[req.Method]; ok {
|
|
handler.ServeHTTP(w, req)
|
|
} else {
|
|
allow := []string{}
|
|
for k := range h {
|
|
allow = append(allow, k)
|
|
}
|
|
sort.Strings(allow)
|
|
w.Header().Set("Allow", strings.Join(allow, ", "))
|
|
if req.Method == "OPTIONS" {
|
|
w.WriteHeader(http.StatusOK)
|
|
} else {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
}
|
|
|
|
// loggingHandler is the http.Handler implementation for LoggingHandlerTo and its
|
|
// friends
|
|
type loggingHandler struct {
|
|
writer io.Writer
|
|
handler http.Handler
|
|
}
|
|
|
|
// combinedLoggingHandler is the http.Handler implementation for LoggingHandlerTo
|
|
// and its friends
|
|
type combinedLoggingHandler struct {
|
|
writer io.Writer
|
|
handler http.Handler
|
|
}
|
|
|
|
func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
t := time.Now()
|
|
logger := makeLogger(w)
|
|
url := *req.URL
|
|
h.handler.ServeHTTP(logger, req)
|
|
writeLog(h.writer, req, url, t, logger.Status(), logger.Size())
|
|
}
|
|
|
|
func (h combinedLoggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
t := time.Now()
|
|
logger := makeLogger(w)
|
|
url := *req.URL
|
|
h.handler.ServeHTTP(logger, req)
|
|
writeCombinedLog(h.writer, req, url, t, logger.Status(), logger.Size())
|
|
}
|
|
|
|
func makeLogger(w http.ResponseWriter) loggingResponseWriter {
|
|
var logger loggingResponseWriter = &responseLogger{w: w}
|
|
if _, ok := w.(http.Hijacker); ok {
|
|
logger = &hijackLogger{responseLogger{w: w}}
|
|
}
|
|
h, ok1 := logger.(http.Hijacker)
|
|
c, ok2 := w.(http.CloseNotifier)
|
|
if ok1 && ok2 {
|
|
return hijackCloseNotifier{logger, h, c}
|
|
}
|
|
if ok2 {
|
|
return &closeNotifyWriter{logger, c}
|
|
}
|
|
return logger
|
|
}
|
|
|
|
type loggingResponseWriter interface {
|
|
http.ResponseWriter
|
|
http.Flusher
|
|
Status() int
|
|
Size() int
|
|
}
|
|
|
|
// responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP
|
|
// status code and body size
|
|
type responseLogger struct {
|
|
w http.ResponseWriter
|
|
status int
|
|
size int
|
|
}
|
|
|
|
func (l *responseLogger) Header() http.Header {
|
|
return l.w.Header()
|
|
}
|
|
|
|
func (l *responseLogger) Write(b []byte) (int, error) {
|
|
if l.status == 0 {
|
|
// The status will be StatusOK if WriteHeader has not been called yet
|
|
l.status = http.StatusOK
|
|
}
|
|
size, err := l.w.Write(b)
|
|
l.size += size
|
|
return size, err
|
|
}
|
|
|
|
func (l *responseLogger) WriteHeader(s int) {
|
|
l.w.WriteHeader(s)
|
|
l.status = s
|
|
}
|
|
|
|
func (l *responseLogger) Status() int {
|
|
return l.status
|
|
}
|
|
|
|
func (l *responseLogger) Size() int {
|
|
return l.size
|
|
}
|
|
|
|
func (l *responseLogger) Flush() {
|
|
f, ok := l.w.(http.Flusher)
|
|
if ok {
|
|
f.Flush()
|
|
}
|
|
}
|
|
|
|
type hijackLogger struct {
|
|
responseLogger
|
|
}
|
|
|
|
func (l *hijackLogger) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
|
h := l.responseLogger.w.(http.Hijacker)
|
|
conn, rw, err := h.Hijack()
|
|
if err == nil && l.responseLogger.status == 0 {
|
|
// The status will be StatusSwitchingProtocols if there was no error and
|
|
// WriteHeader has not been called yet
|
|
l.responseLogger.status = http.StatusSwitchingProtocols
|
|
}
|
|
return conn, rw, err
|
|
}
|
|
|
|
type closeNotifyWriter struct {
|
|
loggingResponseWriter
|
|
http.CloseNotifier
|
|
}
|
|
|
|
type hijackCloseNotifier struct {
|
|
loggingResponseWriter
|
|
http.Hijacker
|
|
http.CloseNotifier
|
|
}
|
|
|
|
const lowerhex = "0123456789abcdef"
|
|
|
|
func appendQuoted(buf []byte, s string) []byte {
|
|
var runeTmp [utf8.UTFMax]byte
|
|
for width := 0; len(s) > 0; s = s[width:] {
|
|
r := rune(s[0])
|
|
width = 1
|
|
if r >= utf8.RuneSelf {
|
|
r, width = utf8.DecodeRuneInString(s)
|
|
}
|
|
if width == 1 && r == utf8.RuneError {
|
|
buf = append(buf, `\x`...)
|
|
buf = append(buf, lowerhex[s[0]>>4])
|
|
buf = append(buf, lowerhex[s[0]&0xF])
|
|
continue
|
|
}
|
|
if r == rune('"') || r == '\\' { // always backslashed
|
|
buf = append(buf, '\\')
|
|
buf = append(buf, byte(r))
|
|
continue
|
|
}
|
|
if strconv.IsPrint(r) {
|
|
n := utf8.EncodeRune(runeTmp[:], r)
|
|
buf = append(buf, runeTmp[:n]...)
|
|
continue
|
|
}
|
|
switch r {
|
|
case '\a':
|
|
buf = append(buf, `\a`...)
|
|
case '\b':
|
|
buf = append(buf, `\b`...)
|
|
case '\f':
|
|
buf = append(buf, `\f`...)
|
|
case '\n':
|
|
buf = append(buf, `\n`...)
|
|
case '\r':
|
|
buf = append(buf, `\r`...)
|
|
case '\t':
|
|
buf = append(buf, `\t`...)
|
|
case '\v':
|
|
buf = append(buf, `\v`...)
|
|
default:
|
|
switch {
|
|
case r < ' ':
|
|
buf = append(buf, `\x`...)
|
|
buf = append(buf, lowerhex[s[0]>>4])
|
|
buf = append(buf, lowerhex[s[0]&0xF])
|
|
case r > utf8.MaxRune:
|
|
r = 0xFFFD
|
|
fallthrough
|
|
case r < 0x10000:
|
|
buf = append(buf, `\u`...)
|
|
for s := 12; s >= 0; s -= 4 {
|
|
buf = append(buf, lowerhex[r>>uint(s)&0xF])
|
|
}
|
|
default:
|
|
buf = append(buf, `\U`...)
|
|
for s := 28; s >= 0; s -= 4 {
|
|
buf = append(buf, lowerhex[r>>uint(s)&0xF])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return buf
|
|
|
|
}
|
|
|
|
// buildCommonLogLine builds a log entry for req in Apache Common Log Format.
|
|
// ts is the timestamp with which the entry should be logged.
|
|
// status and size are used to provide the response HTTP status and size.
|
|
func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int, size int) []byte {
|
|
username := "-"
|
|
if url.User != nil {
|
|
if name := url.User.Username(); name != "" {
|
|
username = name
|
|
}
|
|
}
|
|
|
|
host, _, err := net.SplitHostPort(req.RemoteAddr)
|
|
|
|
if err != nil {
|
|
host = req.RemoteAddr
|
|
}
|
|
|
|
uri := req.RequestURI
|
|
|
|
// Requests using the CONNECT method over HTTP/2.0 must use
|
|
// the authority field (aka r.Host) to identify the target.
|
|
// Refer: https://httpwg.github.io/specs/rfc7540.html#CONNECT
|
|
if req.ProtoMajor == 2 && req.Method == "CONNECT" {
|
|
uri = req.Host
|
|
}
|
|
if uri == "" {
|
|
uri = url.RequestURI()
|
|
}
|
|
|
|
buf := make([]byte, 0, 3*(len(host)+len(username)+len(req.Method)+len(uri)+len(req.Proto)+50)/2)
|
|
buf = append(buf, host...)
|
|
buf = append(buf, " - "...)
|
|
buf = append(buf, username...)
|
|
buf = append(buf, " ["...)
|
|
buf = append(buf, ts.Format("02/Jan/2006:15:04:05 -0700")...)
|
|
buf = append(buf, `] "`...)
|
|
buf = append(buf, req.Method...)
|
|
buf = append(buf, " "...)
|
|
buf = appendQuoted(buf, uri)
|
|
buf = append(buf, " "...)
|
|
buf = append(buf, req.Proto...)
|
|
buf = append(buf, `" `...)
|
|
buf = append(buf, strconv.Itoa(status)...)
|
|
buf = append(buf, " "...)
|
|
buf = append(buf, strconv.Itoa(size)...)
|
|
return buf
|
|
}
|
|
|
|
// writeLog writes a log entry for req to w in Apache Common Log Format.
|
|
// ts is the timestamp with which the entry should be logged.
|
|
// status and size are used to provide the response HTTP status and size.
|
|
func writeLog(w io.Writer, req *http.Request, url url.URL, ts time.Time, status, size int) {
|
|
buf := buildCommonLogLine(req, url, ts, status, size)
|
|
buf = append(buf, '\n')
|
|
w.Write(buf)
|
|
}
|
|
|
|
// writeCombinedLog writes a log entry for req to w in Apache Combined Log Format.
|
|
// ts is the timestamp with which the entry should be logged.
|
|
// status and size are used to provide the response HTTP status and size.
|
|
func writeCombinedLog(w io.Writer, req *http.Request, url url.URL, ts time.Time, status, size int) {
|
|
buf := buildCommonLogLine(req, url, ts, status, size)
|
|
buf = append(buf, ` "`...)
|
|
buf = appendQuoted(buf, req.Referer())
|
|
buf = append(buf, `" "`...)
|
|
buf = appendQuoted(buf, req.UserAgent())
|
|
buf = append(buf, '"', '\n')
|
|
w.Write(buf)
|
|
}
|
|
|
|
// CombinedLoggingHandler return a http.Handler that wraps h and logs requests to out in
|
|
// Apache Combined Log Format.
|
|
//
|
|
// See http://httpd.apache.org/docs/2.2/logs.html#combined for a description of this format.
|
|
//
|
|
// LoggingHandler always sets the ident field of the log to -
|
|
func CombinedLoggingHandler(out io.Writer, h http.Handler) http.Handler {
|
|
return combinedLoggingHandler{out, h}
|
|
}
|
|
|
|
// LoggingHandler return a http.Handler that wraps h and logs requests to out in
|
|
// Apache Common Log Format (CLF).
|
|
//
|
|
// See http://httpd.apache.org/docs/2.2/logs.html#common for a description of this format.
|
|
//
|
|
// LoggingHandler always sets the ident field of the log to -
|
|
//
|
|
// Example:
|
|
//
|
|
// r := mux.NewRouter()
|
|
// r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
// w.Write([]byte("This is a catch-all route"))
|
|
// })
|
|
// loggedRouter := handlers.LoggingHandler(os.Stdout, r)
|
|
// http.ListenAndServe(":1123", loggedRouter)
|
|
//
|
|
func LoggingHandler(out io.Writer, h http.Handler) http.Handler {
|
|
return loggingHandler{out, h}
|
|
}
|
|
|
|
// isContentType validates the Content-Type header matches the supplied
|
|
// contentType. That is, its type and subtype match.
|
|
func isContentType(h http.Header, contentType string) bool {
|
|
ct := h.Get("Content-Type")
|
|
if i := strings.IndexRune(ct, ';'); i != -1 {
|
|
ct = ct[0:i]
|
|
}
|
|
return ct == contentType
|
|
}
|
|
|
|
// ContentTypeHandler wraps and returns a http.Handler, validating the request
|
|
// content type is compatible with the contentTypes list. It writes a HTTP 415
|
|
// error if that fails.
|
|
//
|
|
// Only PUT, POST, and PATCH requests are considered.
|
|
func ContentTypeHandler(h http.Handler, contentTypes ...string) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if !(r.Method == "PUT" || r.Method == "POST" || r.Method == "PATCH") {
|
|
h.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
for _, ct := range contentTypes {
|
|
if isContentType(r.Header, ct) {
|
|
h.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
}
|
|
http.Error(w, fmt.Sprintf("Unsupported content type %q; expected one of %q", r.Header.Get("Content-Type"), contentTypes), http.StatusUnsupportedMediaType)
|
|
})
|
|
}
|
|
|
|
const (
|
|
// HTTPMethodOverrideHeader is a commonly used
|
|
// http header to override a request method.
|
|
HTTPMethodOverrideHeader = "X-HTTP-Method-Override"
|
|
// HTTPMethodOverrideFormKey is a commonly used
|
|
// HTML form key to override a request method.
|
|
HTTPMethodOverrideFormKey = "_method"
|
|
)
|
|
|
|
// HTTPMethodOverrideHandler wraps and returns a http.Handler which checks for
|
|
// the X-HTTP-Method-Override header or the _method form key, and overrides (if
|
|
// valid) request.Method with its value.
|
|
//
|
|
// This is especially useful for HTTP clients that don't support many http verbs.
|
|
// It isn't secure to override e.g a GET to a POST, so only POST requests are
|
|
// considered. Likewise, the override method can only be a "write" method: PUT,
|
|
// PATCH or DELETE.
|
|
//
|
|
// Form method takes precedence over header method.
|
|
func HTTPMethodOverrideHandler(h http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == "POST" {
|
|
om := r.FormValue(HTTPMethodOverrideFormKey)
|
|
if om == "" {
|
|
om = r.Header.Get(HTTPMethodOverrideHeader)
|
|
}
|
|
if om == "PUT" || om == "PATCH" || om == "DELETE" {
|
|
r.Method = om
|
|
}
|
|
}
|
|
h.ServeHTTP(w, r)
|
|
})
|
|
}
|