mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-30 10:12:45 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			368 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			368 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package httpserver
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"io"
 | |
| 	"io/ioutil"
 | |
| 	"net"
 | |
| 	"net/http"
 | |
| 	"net/http/httputil"
 | |
| 	"net/url"
 | |
| 	"os"
 | |
| 	"path"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/mholt/caddy"
 | |
| )
 | |
| 
 | |
| // requestReplacer is a strings.Replacer which is used to
 | |
| // encode literal \r and \n characters and keep everything
 | |
| // on one line
 | |
| var requestReplacer = strings.NewReplacer(
 | |
| 	"\r", "\\r",
 | |
| 	"\n", "\\n",
 | |
| )
 | |
| 
 | |
| var now = time.Now
 | |
| 
 | |
| // Replacer is a type which can replace placeholder
 | |
| // substrings in a string with actual values from a
 | |
| // http.Request and ResponseRecorder. Always use
 | |
| // NewReplacer to get one of these. Any placeholders
 | |
| // made with Set() should overwrite existing values if
 | |
| // the key is already used.
 | |
| type Replacer interface {
 | |
| 	Replace(string) string
 | |
| 	Set(key, value string)
 | |
| }
 | |
| 
 | |
| // replacer implements Replacer. customReplacements
 | |
| // is used to store custom replacements created with
 | |
| // Set() until the time of replacement, at which point
 | |
| // they will be used to overwrite other replacements
 | |
| // if there is a name conflict.
 | |
| type replacer struct {
 | |
| 	customReplacements map[string]string
 | |
| 	emptyValue         string
 | |
| 	responseRecorder   *ResponseRecorder
 | |
| 	request            *http.Request
 | |
| 	requestBody        *limitWriter
 | |
| }
 | |
| 
 | |
| type limitWriter struct {
 | |
| 	w      bytes.Buffer
 | |
| 	remain int
 | |
| }
 | |
| 
 | |
| func newLimitWriter(max int) *limitWriter {
 | |
| 	return &limitWriter{
 | |
| 		w:      bytes.Buffer{},
 | |
| 		remain: max,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (lw *limitWriter) Write(p []byte) (int, error) {
 | |
| 	// skip if we are full
 | |
| 	if lw.remain <= 0 {
 | |
| 		return len(p), nil
 | |
| 	}
 | |
| 	if n := len(p); n > lw.remain {
 | |
| 		p = p[:lw.remain]
 | |
| 	}
 | |
| 	n, err := lw.w.Write(p)
 | |
| 	lw.remain -= n
 | |
| 	return n, err
 | |
| }
 | |
| 
 | |
| func (lw *limitWriter) String() string {
 | |
| 	return lw.w.String()
 | |
| }
 | |
| 
 | |
| // NewReplacer makes a new replacer based on r and rr which
 | |
| // are used for request and response placeholders, respectively.
 | |
| // Request placeholders are created immediately, whereas
 | |
| // response placeholders are not created until Replace()
 | |
| // is invoked. rr may be nil if it is not available.
 | |
| // emptyValue should be the string that is used in place
 | |
| // of empty string (can still be empty string).
 | |
| func NewReplacer(r *http.Request, rr *ResponseRecorder, emptyValue string) Replacer {
 | |
| 	rb := newLimitWriter(MaxLogBodySize)
 | |
| 	if r.Body != nil {
 | |
| 		r.Body = struct {
 | |
| 			io.Reader
 | |
| 			io.Closer
 | |
| 		}{io.TeeReader(r.Body, rb), io.Closer(r.Body)}
 | |
| 	}
 | |
| 	return &replacer{
 | |
| 		request:            r,
 | |
| 		requestBody:        rb,
 | |
| 		responseRecorder:   rr,
 | |
| 		customReplacements: make(map[string]string),
 | |
| 		emptyValue:         emptyValue,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func canLogRequest(r *http.Request) bool {
 | |
| 	if r.Method == "POST" || r.Method == "PUT" {
 | |
| 		for _, cType := range r.Header[headerContentType] {
 | |
| 			// the cType could have charset and other info
 | |
| 			if strings.Contains(cType, contentTypeJSON) || strings.Contains(cType, contentTypeXML) {
 | |
| 				return true
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| // Replace performs a replacement of values on s and returns
 | |
| // the string with the replaced values.
 | |
| func (r *replacer) Replace(s string) string {
 | |
| 	// Do not attempt replacements if no placeholder is found.
 | |
| 	if !strings.ContainsAny(s, "{}") {
 | |
| 		return s
 | |
| 	}
 | |
| 
 | |
| 	result := ""
 | |
| 	for {
 | |
| 		idxStart := strings.Index(s, "{")
 | |
| 		if idxStart == -1 {
 | |
| 			// no placeholder anymore
 | |
| 			break
 | |
| 		}
 | |
| 		idxEnd := strings.Index(s[idxStart:], "}")
 | |
| 		if idxEnd == -1 {
 | |
| 			// unpaired placeholder
 | |
| 			break
 | |
| 		}
 | |
| 		idxEnd += idxStart
 | |
| 
 | |
| 		// get a replacement
 | |
| 		placeholder := s[idxStart : idxEnd+1]
 | |
| 		replacement := r.getSubstitution(placeholder)
 | |
| 
 | |
| 		// append prefix + replacement
 | |
| 		result += s[:idxStart] + replacement
 | |
| 
 | |
| 		// strip out scanned parts
 | |
| 		s = s[idxEnd+1:]
 | |
| 	}
 | |
| 
 | |
| 	// append unscanned parts
 | |
| 	return result + s
 | |
| }
 | |
| 
 | |
| func roundDuration(d time.Duration) time.Duration {
 | |
| 	if d >= time.Millisecond {
 | |
| 		return round(d, time.Millisecond)
 | |
| 	} else if d >= time.Microsecond {
 | |
| 		return round(d, time.Microsecond)
 | |
| 	}
 | |
| 
 | |
| 	return d
 | |
| }
 | |
| 
 | |
| // round rounds d to the nearest r
 | |
| func round(d, r time.Duration) time.Duration {
 | |
| 	if r <= 0 {
 | |
| 		return d
 | |
| 	}
 | |
| 	neg := d < 0
 | |
| 	if neg {
 | |
| 		d = -d
 | |
| 	}
 | |
| 	if m := d % r; m+m < r {
 | |
| 		d = d - m
 | |
| 	} else {
 | |
| 		d = d + r - m
 | |
| 	}
 | |
| 	if neg {
 | |
| 		return -d
 | |
| 	}
 | |
| 	return d
 | |
| }
 | |
| 
 | |
| // getSubstitution retrieves value from corresponding key
 | |
| func (r *replacer) getSubstitution(key string) string {
 | |
| 	// search custom replacements first
 | |
| 	if value, ok := r.customReplacements[key]; ok {
 | |
| 		return value
 | |
| 	}
 | |
| 
 | |
| 	// search request headers then
 | |
| 	if key[1] == '>' {
 | |
| 		want := key[2 : len(key)-1]
 | |
| 		for key, values := range r.request.Header {
 | |
| 			// Header placeholders (case-insensitive)
 | |
| 			if strings.EqualFold(key, want) {
 | |
| 				return strings.Join(values, ",")
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	// next check for cookies
 | |
| 	if key[1] == '~' {
 | |
| 		name := key[2 : len(key)-1]
 | |
| 		if cookie, err := r.request.Cookie(name); err == nil {
 | |
| 			return cookie.Value
 | |
| 		}
 | |
| 	}
 | |
| 	// next check for query argument
 | |
| 	if key[1] == '?' {
 | |
| 		query := r.request.URL.Query()
 | |
| 		name := key[2 : len(key)-1]
 | |
| 		return query.Get(name)
 | |
| 	}
 | |
| 
 | |
| 	// search default replacements in the end
 | |
| 	switch key {
 | |
| 	case "{method}":
 | |
| 		return r.request.Method
 | |
| 	case "{scheme}":
 | |
| 		if r.request.TLS != nil {
 | |
| 			return "https"
 | |
| 		}
 | |
| 		return "http"
 | |
| 	case "{hostname}":
 | |
| 		name, err := os.Hostname()
 | |
| 		if err != nil {
 | |
| 			return r.emptyValue
 | |
| 		}
 | |
| 		return name
 | |
| 	case "{host}":
 | |
| 		return r.request.Host
 | |
| 	case "{hostonly}":
 | |
| 		host, _, err := net.SplitHostPort(r.request.Host)
 | |
| 		if err != nil {
 | |
| 			return r.request.Host
 | |
| 		}
 | |
| 		return host
 | |
| 	case "{path}":
 | |
| 		u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
 | |
| 		return u.Path
 | |
| 	case "{path_escaped}":
 | |
| 		u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
 | |
| 		return url.QueryEscape(u.Path)
 | |
| 	case "{request_id}":
 | |
| 		reqid, _ := r.request.Context().Value(RequestIDCtxKey).(string)
 | |
| 		return reqid
 | |
| 	case "{rewrite_path}":
 | |
| 		return r.request.URL.Path
 | |
| 	case "{rewrite_path_escaped}":
 | |
| 		return url.QueryEscape(r.request.URL.Path)
 | |
| 	case "{query}":
 | |
| 		u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
 | |
| 		return u.RawQuery
 | |
| 	case "{query_escaped}":
 | |
| 		u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
 | |
| 		return url.QueryEscape(u.RawQuery)
 | |
| 	case "{fragment}":
 | |
| 		u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
 | |
| 		return u.Fragment
 | |
| 	case "{proto}":
 | |
| 		return r.request.Proto
 | |
| 	case "{remote}":
 | |
| 		host, _, err := net.SplitHostPort(r.request.RemoteAddr)
 | |
| 		if err != nil {
 | |
| 			return r.request.RemoteAddr
 | |
| 		}
 | |
| 		return host
 | |
| 	case "{port}":
 | |
| 		_, port, err := net.SplitHostPort(r.request.RemoteAddr)
 | |
| 		if err != nil {
 | |
| 			return r.emptyValue
 | |
| 		}
 | |
| 		return port
 | |
| 	case "{uri}":
 | |
| 		u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
 | |
| 		return u.RequestURI()
 | |
| 	case "{uri_escaped}":
 | |
| 		u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
 | |
| 		return url.QueryEscape(u.RequestURI())
 | |
| 	case "{rewrite_uri}":
 | |
| 		return r.request.URL.RequestURI()
 | |
| 	case "{rewrite_uri_escaped}":
 | |
| 		return url.QueryEscape(r.request.URL.RequestURI())
 | |
| 	case "{when}":
 | |
| 		return now().Format(timeFormat)
 | |
| 	case "{when_iso}":
 | |
| 		return now().UTC().Format(timeFormatISOUTC)
 | |
| 	case "{when_unix}":
 | |
| 		return strconv.FormatInt(now().Unix(), 10)
 | |
| 	case "{file}":
 | |
| 		_, file := path.Split(r.request.URL.Path)
 | |
| 		return file
 | |
| 	case "{dir}":
 | |
| 		dir, _ := path.Split(r.request.URL.Path)
 | |
| 		return dir
 | |
| 	case "{request}":
 | |
| 		dump, err := httputil.DumpRequest(r.request, false)
 | |
| 		if err != nil {
 | |
| 			return r.emptyValue
 | |
| 		}
 | |
| 		return requestReplacer.Replace(string(dump))
 | |
| 	case "{request_body}":
 | |
| 		if !canLogRequest(r.request) {
 | |
| 			return r.emptyValue
 | |
| 		}
 | |
| 		_, err := ioutil.ReadAll(r.request.Body)
 | |
| 		if err != nil {
 | |
| 			if err == ErrMaxBytesExceeded {
 | |
| 				return r.emptyValue
 | |
| 			}
 | |
| 		}
 | |
| 		return requestReplacer.Replace(r.requestBody.String())
 | |
| 	case "{mitm}":
 | |
| 		if val, ok := r.request.Context().Value(caddy.CtxKey("mitm")).(bool); ok {
 | |
| 			if val {
 | |
| 				return "likely"
 | |
| 			}
 | |
| 			return "unlikely"
 | |
| 		}
 | |
| 		return "unknown"
 | |
| 	case "{status}":
 | |
| 		if r.responseRecorder == nil {
 | |
| 			return r.emptyValue
 | |
| 		}
 | |
| 		return strconv.Itoa(r.responseRecorder.status)
 | |
| 	case "{size}":
 | |
| 		if r.responseRecorder == nil {
 | |
| 			return r.emptyValue
 | |
| 		}
 | |
| 		return strconv.Itoa(r.responseRecorder.size)
 | |
| 	case "{latency}":
 | |
| 		if r.responseRecorder == nil {
 | |
| 			return r.emptyValue
 | |
| 		}
 | |
| 		return roundDuration(time.Since(r.responseRecorder.start)).String()
 | |
| 	case "{latency_ms}":
 | |
| 		if r.responseRecorder == nil {
 | |
| 			return r.emptyValue
 | |
| 		}
 | |
| 		elapsedDuration := time.Since(r.responseRecorder.start)
 | |
| 		return strconv.FormatInt(convertToMilliseconds(elapsedDuration), 10)
 | |
| 	}
 | |
| 
 | |
| 	return r.emptyValue
 | |
| }
 | |
| 
 | |
| //convertToMilliseconds returns the number of milliseconds in the given duration
 | |
| func convertToMilliseconds(d time.Duration) int64 {
 | |
| 	return d.Nanoseconds() / 1e6
 | |
| }
 | |
| 
 | |
| // Set sets key to value in the r.customReplacements map.
 | |
| func (r *replacer) Set(key, value string) {
 | |
| 	r.customReplacements["{"+key+"}"] = value
 | |
| }
 | |
| 
 | |
| const (
 | |
| 	timeFormat        = "02/Jan/2006:15:04:05 -0700"
 | |
| 	timeFormatISOUTC  = "2006-01-02T15:04:05Z" // ISO 8601 with timezone to be assumed as UTC
 | |
| 	headerContentType = "Content-Type"
 | |
| 	contentTypeJSON   = "application/json"
 | |
| 	contentTypeXML    = "application/xml"
 | |
| 	// MaxLogBodySize limits the size of logged request's body
 | |
| 	MaxLogBodySize = 100 * 1024
 | |
| )
 |