mirror of https://github.com/dexidp/dex.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
378 lines
10 KiB
378 lines
10 KiB
// 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 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 := 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 - |
|
func LoggingHandler(out io.Writer, h http.Handler) http.Handler { |
|
return loggingHandler{out, h} |
|
} |
|
|
|
// isContentType validates the Content-Type header |
|
// is 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 acompatible 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) |
|
}) |
|
}
|
|
|