Skip to content
Snippets Groups Projects
Select Git revision
  • 4791190b7aae085e2f56c928aa2c4c6247f3483b
  • master default protected
  • 1.31
  • 4.38.7
  • 4.38.6
  • 4.38.5
  • 4.38.4
  • 4.38.3
  • 4.38.2
  • 4.38.1
  • 4.38.0
  • 4.37.2
  • 4.37.1
  • 4.37.0
  • 4.36.0
  • 4.35.0
  • 4.34.1
  • 4.34.0
  • 4.33.1
  • 4.33.0
  • 4.32.2
  • 4.32.1
  • 4.32.0
23 results

Monster_Data.Transformer.html

Blame
  • handlers.go 27.78 KiB
    package httpbin
    
    import (
    	"bytes"
    	"compress/gzip"
    	"compress/zlib"
    	"encoding/json"
    	"fmt"
    	"net/http"
    	"net/http/httputil"
    	"net/url"
    	"sort"
    	"strconv"
    	"strings"
    	"time"
    
    	"github.com/mccutchen/go-httpbin/v2/httpbin/digest"
    )
    
    func notImplementedHandler(w http.ResponseWriter, r *http.Request) {
    	http.Error(w, "Not implemented", http.StatusNotImplemented)
    }
    
    // Index renders an HTML index page
    func (h *HTTPBin) Index(w http.ResponseWriter, r *http.Request) {
    	if r.URL.Path != "/" {
    		msg := fmt.Sprintf("Not Found (go-httpbin does not handle the path %s)", r.URL.Path)
    		http.Error(w, msg, http.StatusNotFound)
    		return
    	}
    	w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' camo.githubusercontent.com")
    	writeHTML(w, mustStaticAsset("index.html"), http.StatusOK)
    }
    
    // FormsPost renders an HTML form that submits a request to the /post endpoint
    func (h *HTTPBin) FormsPost(w http.ResponseWriter, r *http.Request) {
    	writeHTML(w, mustStaticAsset("forms-post.html"), http.StatusOK)
    }
    
    // UTF8 renders an HTML encoding stress test
    func (h *HTTPBin) UTF8(w http.ResponseWriter, r *http.Request) {
    	writeHTML(w, mustStaticAsset("utf8.html"), http.StatusOK)
    }
    
    // Get handles HTTP GET requests
    func (h *HTTPBin) Get(w http.ResponseWriter, r *http.Request) {
    	writeJSON(http.StatusOK, w, &noBodyResponse{
    		Args:    r.URL.Query(),
    		Headers: getRequestHeaders(r),
    		Method:  r.Method,
    		Origin:  getClientIP(r),
    		URL:     getURL(r).String(),
    	})
    }
    
    // Anything returns anything that is passed to request.
    func (h *HTTPBin) Anything(w http.ResponseWriter, r *http.Request) {
    	// Short-circuit for HEAD requests, which should be handled like regular
    	// GET requests (where the autohead middleware will take care of discarding
    	// the body)
    	if r.Method == http.MethodHead {
    		h.Get(w, r)
    		return
    	}
    	// All other requests will be handled the same.  For compatibility with
    	// httpbin, the /anything endpoint even allows GET requests to have bodies.
    	h.RequestWithBody(w, r)
    }
    
    // RequestWithBody handles POST, PUT, and PATCH requests
    func (h *HTTPBin) RequestWithBody(w http.ResponseWriter, r *http.Request) {
    	resp := &bodyResponse{
    		Args:    r.URL.Query(),
    		Headers: getRequestHeaders(r),
    		Method:  r.Method,
    		Origin:  getClientIP(r),
    		URL:     getURL(r).String(),
    	}
    
    	err := parseBody(w, r, resp)
    	if err != nil {
    		http.Error(w, fmt.Sprintf("error parsing request body: %s", err), http.StatusBadRequest)
    		return
    	}
    
    	writeJSON(http.StatusOK, w, resp)
    }
    
    // Gzip returns a gzipped response
    func (h *HTTPBin) Gzip(w http.ResponseWriter, r *http.Request) {
    	var (
    		buf bytes.Buffer
    		gzw = gzip.NewWriter(&buf)
    	)
    	mustMarshalJSON(gzw, &noBodyResponse{
    		Args:    r.URL.Query(),
    		Headers: getRequestHeaders(r),
    		Method:  r.Method,
    		Origin:  getClientIP(r),
    		Gzipped: true,
    	})
    	gzw.Close()
    
    	body := buf.Bytes()
    	w.Header().Set("Content-Encoding", "gzip")
    	w.Header().Set("Content-Type", jsonContentType)
    	w.Header().Set("Content-Length", strconv.Itoa(len(body)))
    	w.WriteHeader(http.StatusOK)
    	w.Write(body)
    }
    
    // Deflate returns a gzipped response
    func (h *HTTPBin) Deflate(w http.ResponseWriter, r *http.Request) {
    	var (
    		buf bytes.Buffer
    		zw  = zlib.NewWriter(&buf)
    	)
    	mustMarshalJSON(zw, &noBodyResponse{
    		Args:     r.URL.Query(),
    		Headers:  getRequestHeaders(r),
    		Method:   r.Method,
    		Origin:   getClientIP(r),
    		Deflated: true,
    	})
    	zw.Close()
    
    	body := buf.Bytes()
    	w.Header().Set("Content-Encoding", "deflate")
    	w.Header().Set("Content-Type", jsonContentType)
    	w.Header().Set("Content-Length", strconv.Itoa(len(body)))
    	w.WriteHeader(http.StatusOK)
    	w.Write(body)
    }
    
    // IP echoes the IP address of the incoming request
    func (h *HTTPBin) IP(w http.ResponseWriter, r *http.Request) {
    	writeJSON(http.StatusOK, w, &ipResponse{
    		Origin: getClientIP(r),
    	})
    }
    
    // UserAgent echoes the incoming User-Agent header
    func (h *HTTPBin) UserAgent(w http.ResponseWriter, r *http.Request) {
    	writeJSON(http.StatusOK, w, &userAgentResponse{
    		UserAgent: r.Header.Get("User-Agent"),
    	})
    }
    
    // Headers echoes the incoming request headers
    func (h *HTTPBin) Headers(w http.ResponseWriter, r *http.Request) {
    	writeJSON(http.StatusOK, w, &headersResponse{
    		Headers: getRequestHeaders(r),
    	})
    }
    
    type statusCase struct {
    	headers map[string]string
    	body    []byte
    }
    
    var (
    	statusRedirectHeaders = &statusCase{
    		headers: map[string]string{
    			"Location": "/redirect/1",
    		},
    	}
    	statusNotAcceptableBody = []byte(`{
      "message": "Client did not request a supported media type",
      "accept": [
        "image/webp",
        "image/svg+xml",
        "image/jpeg",
        "image/png",
        "image/"
      ]
    }
    `)
    	statusHTTP300body = []byte(`<!doctype html>
    <head>
    <title>Multiple Choices</title>
    </head>
    <body>
    <ul>
    <li><a href="/image/jpeg">/image/jpeg</a></li>
    <li><a href="/image/png">/image/png</a></li>
    <li><a href="/image/svg">/image/svg</a></li>
    </body>
    </html>`)
    
    	statusHTTP308Body = []byte(`<!doctype html>
    <head>
    <title>Permanent Redirect</title>
    </head>
    <body>Permanently redirected to <a href="/image/jpeg">/image/jpeg</a>
    </body>
    </html>`)
    
    	statusSpecialCases = map[int]*statusCase{
    		300: {
    			body: statusHTTP300body,
    			headers: map[string]string{
    				"Location": "/image/jpeg",
    			},
    		},
    		301: statusRedirectHeaders,
    		302: statusRedirectHeaders,
    		303: statusRedirectHeaders,
    		305: statusRedirectHeaders,
    		307: statusRedirectHeaders,
    		308: {
    			body: statusHTTP308Body,
    			headers: map[string]string{
    				"Location": "/image/jpeg",
    			},
    		},
    		401: {
    			headers: map[string]string{
    				"WWW-Authenticate": `Basic realm="Fake Realm"`,
    			},
    		},
    		402: {
    			body: []byte("Fuck you, pay me!"),
    			headers: map[string]string{
    				"X-More-Info": "http://vimeo.com/22053820",
    			},
    		},
    		406: {
    			body: statusNotAcceptableBody,
    			headers: map[string]string{
    				"Content-Type": jsonContentType,
    			},
    		},
    		407: {
    			headers: map[string]string{
    				"Proxy-Authenticate": `Basic realm="Fake Realm"`,
    			},
    		},
    		418: {
    			body: []byte("I'm a teapot!"),
    			headers: map[string]string{
    				"X-More-Info": "http://tools.ietf.org/html/rfc2324",
    			},
    		},
    	}
    )
    
    // Status responds with the specified status code. TODO: support random choice
    // from multiple, optionally weighted status codes.
    func (h *HTTPBin) Status(w http.ResponseWriter, r *http.Request) {
    	parts := strings.Split(r.URL.Path, "/")
    	if len(parts) != 3 {
    		http.Error(w, "Not found", http.StatusNotFound)
    		return
    	}
    	code, err := strconv.Atoi(parts[2])
    	if err != nil {
    		http.Error(w, "Invalid status", http.StatusBadRequest)
    		return
    	}
    
    	if specialCase, ok := statusSpecialCases[code]; ok {
    		for key, val := range specialCase.headers {
    			w.Header().Set(key, val)
    		}
    		w.WriteHeader(code)
    		if specialCase.body != nil {
    			w.Write(specialCase.body)
    		}
    		return
    	}
    
    	w.WriteHeader(code)
    }
    
    // Unstable - returns 500, sometimes
    func (h *HTTPBin) Unstable(w http.ResponseWriter, r *http.Request) {
    	var err error
    
    	// rng/seed
    	rng, err := parseSeed(r.URL.Query().Get("seed"))
    	if err != nil {
    		http.Error(w, "invalid seed", http.StatusBadRequest)
    		return
    	}
    
    	// failure_rate
    	var failureRate float64
    	rawFailureRate := r.URL.Query().Get("failure_rate")
    	if rawFailureRate != "" {
    		failureRate, err = strconv.ParseFloat(rawFailureRate, 64)
    		if err != nil || failureRate < 0 || failureRate > 1 {
    			http.Error(w, "invalid failure_rate", http.StatusBadRequest)
    			return
    		}
    	} else {
    		failureRate = 0.5
    	}
    
    	var status int
    	if rng.Float64() < failureRate {
    		status = http.StatusInternalServerError
    	} else {
    		status = http.StatusOK
    	}
    	w.WriteHeader(status)
    }
    
    // ResponseHeaders responds with a map of header values
    func (h *HTTPBin) ResponseHeaders(w http.ResponseWriter, r *http.Request) {
    	args := r.URL.Query()
    	for k, vs := range args {
    		for _, v := range vs {
    			w.Header().Add(k, v)
    		}
    	}
    	if contentType := w.Header().Get("Content-Type"); contentType == "" {
    		w.Header().Set("Content-Type", jsonContentType)
    	}
    	mustMarshalJSON(w, args)
    }
    
    func redirectLocation(r *http.Request, relative bool, n int) string {
    	var location string
    	var path string
    
    	if n < 1 {
    		path = "/get"
    	} else if relative {
    		path = fmt.Sprintf("/relative-redirect/%d", n)
    	} else {
    		path = fmt.Sprintf("/absolute-redirect/%d", n)
    	}
    
    	if relative {
    		location = path
    	} else {
    		u := getURL(r)
    		u.Path = path
    		u.RawQuery = ""
    		location = u.String()
    	}
    
    	return location
    }
    
    func doRedirect(w http.ResponseWriter, r *http.Request, relative bool) {
    	parts := strings.Split(r.URL.Path, "/")
    	if len(parts) != 3 {
    		http.Error(w, "Not found", http.StatusNotFound)
    		return
    	}
    	n, err := strconv.Atoi(parts[2])
    	if err != nil || n < 1 {
    		http.Error(w, "Invalid redirect", http.StatusBadRequest)
    		return
    	}
    
    	w.Header().Set("Location", redirectLocation(r, relative, n-1))
    	w.WriteHeader(http.StatusFound)
    }
    
    // Redirect responds with 302 redirect a given number of times. Defaults to a
    // relative redirect, but an ?absolute=true query param will trigger an
    // absolute redirect.
    func (h *HTTPBin) Redirect(w http.ResponseWriter, r *http.Request) {
    	params := r.URL.Query()
    	relative := strings.ToLower(params.Get("absolute")) != "true"
    	doRedirect(w, r, relative)
    }
    
    // RelativeRedirect responds with an HTTP 302 redirect a given number of times
    func (h *HTTPBin) RelativeRedirect(w http.ResponseWriter, r *http.Request) {
    	doRedirect(w, r, true)
    }
    
    // AbsoluteRedirect responds with an HTTP 302 redirect a given number of times
    func (h *HTTPBin) AbsoluteRedirect(w http.ResponseWriter, r *http.Request) {
    	doRedirect(w, r, false)
    }
    
    // RedirectTo responds with a redirect to a specific URL with an optional
    // status code, which defaults to 302
    func (h *HTTPBin) RedirectTo(w http.ResponseWriter, r *http.Request) {
    	q := r.URL.Query()
    
    	inputURL := q.Get("url")
    	if inputURL == "" {
    		http.Error(w, "Missing URL", http.StatusBadRequest)
    		return
    	}
    
    	u, err := url.Parse(inputURL)
    	if err != nil {
    		http.Error(w, "Invalid URL", http.StatusBadRequest)
    		return
    	}
    
    	if u.IsAbs() && len(h.AllowedRedirectDomains) > 0 {
    		if _, ok := h.AllowedRedirectDomains[u.Hostname()]; !ok {
    			domainListItems := make([]string, 0, len(h.AllowedRedirectDomains))
    			for domain := range h.AllowedRedirectDomains {
    				domainListItems = append(domainListItems, fmt.Sprintf("- %s", domain))
    			}
    			sort.Strings(domainListItems)
    			formattedDomains := strings.Join(domainListItems, "\n")
    			msg := fmt.Sprintf(`Forbidden redirect URL. Please be careful with this link.
    
    Allowed redirect destinations:
    %s`, formattedDomains)
    			http.Error(w, msg, http.StatusForbidden)
    			return
    		}
    	}
    
    	statusCode := http.StatusFound
    	rawStatusCode := q.Get("status_code")
    	if rawStatusCode != "" {
    		statusCode, err = strconv.Atoi(q.Get("status_code"))
    		if err != nil || statusCode < 300 || statusCode > 399 {
    			http.Error(w, "Invalid status code", http.StatusBadRequest)
    			return
    		}
    	}
    
    	w.Header().Set("Location", u.String())
    	w.WriteHeader(statusCode)
    }
    
    // Cookies responds with the cookies in the incoming request
    func (h *HTTPBin) Cookies(w http.ResponseWriter, r *http.Request) {
    	resp := cookiesResponse{}
    	for _, c := range r.Cookies() {
    		resp[c.Name] = c.Value
    	}
    	writeJSON(http.StatusOK, w, resp)
    }
    
    // SetCookies sets cookies as specified in query params and redirects to
    // Cookies endpoint
    func (h *HTTPBin) SetCookies(w http.ResponseWriter, r *http.Request) {
    	params := r.URL.Query()
    	for k := range params {
    		http.SetCookie(w, &http.Cookie{
    			Name:     k,
    			Value:    params.Get(k),
    			HttpOnly: true,
    		})
    	}
    	w.Header().Set("Location", "/cookies")
    	w.WriteHeader(http.StatusFound)
    }
    
    // DeleteCookies deletes cookies specified in query params and redirects to
    // Cookies endpoint
    func (h *HTTPBin) DeleteCookies(w http.ResponseWriter, r *http.Request) {
    	params := r.URL.Query()
    	for k := range params {
    		http.SetCookie(w, &http.Cookie{
    			Name:     k,
    			Value:    params.Get(k),
    			HttpOnly: true,
    			MaxAge:   -1,
    			Expires:  time.Now().Add(-1 * 24 * 365 * time.Hour),
    		})
    	}
    	w.Header().Set("Location", "/cookies")
    	w.WriteHeader(http.StatusFound)
    }
    
    // BasicAuth requires basic authentication
    func (h *HTTPBin) BasicAuth(w http.ResponseWriter, r *http.Request) {
    	parts := strings.Split(r.URL.Path, "/")
    	if len(parts) != 4 {
    		http.Error(w, "Not Found", http.StatusNotFound)
    		return
    	}
    	expectedUser := parts[2]
    	expectedPass := parts[3]
    
    	givenUser, givenPass, _ := r.BasicAuth()
    
    	status := http.StatusOK
    	authorized := givenUser == expectedUser && givenPass == expectedPass
    	if !authorized {
    		status = http.StatusUnauthorized
    		w.Header().Set("WWW-Authenticate", `Basic realm="Fake Realm"`)
    	}
    
    	writeJSON(status, w, authResponse{
    		Authorized: authorized,
    		User:       givenUser,
    	})
    }
    
    // HiddenBasicAuth requires HTTP Basic authentication but returns a status of
    // 404 if the request is unauthorized
    func (h *HTTPBin) HiddenBasicAuth(w http.ResponseWriter, r *http.Request) {
    	parts := strings.Split(r.URL.Path, "/")
    	if len(parts) != 4 {
    		http.Error(w, "Not Found", http.StatusNotFound)
    		return
    	}
    	expectedUser := parts[2]
    	expectedPass := parts[3]
    
    	givenUser, givenPass, _ := r.BasicAuth()
    
    	authorized := givenUser == expectedUser && givenPass == expectedPass
    	if !authorized {
    		http.Error(w, "Not Found", http.StatusNotFound)
    		return
    	}
    
    	writeJSON(http.StatusOK, w, authResponse{
    		Authorized: authorized,
    		User:       givenUser,
    	})
    }
    
    // Stream responds with max(n, 100) lines of JSON-encoded request data.
    func (h *HTTPBin) Stream(w http.ResponseWriter, r *http.Request) {
    	parts := strings.Split(r.URL.Path, "/")
    	if len(parts) != 3 {
    		http.Error(w, "Not found", http.StatusNotFound)
    		return
    	}
    	n, err := strconv.Atoi(parts[2])
    	if err != nil {
    		http.Error(w, "Invalid integer", http.StatusBadRequest)
    		return
    	}
    
    	if n > 100 {
    		n = 100
    	} else if n < 1 {
    		n = 1
    	}
    
    	resp := &streamResponse{
    		Args:    r.URL.Query(),
    		Headers: getRequestHeaders(r),
    		Origin:  getClientIP(r),
    		URL:     getURL(r).String(),
    	}
    
    	f := w.(http.Flusher)
    	for i := 0; i < n; i++ {
    		resp.ID = i
    		// Call json.Marshal directly to avoid pretty printing
    		line, _ := json.Marshal(resp)
    		w.Write(append(line, '\n'))
    		f.Flush()
    	}
    }
    
    // Delay waits for a given amount of time before responding, where the time may
    // be specified as a golang-style duration or seconds in floating point.
    func (h *HTTPBin) Delay(w http.ResponseWriter, r *http.Request) {
    	parts := strings.Split(r.URL.Path, "/")
    	if len(parts) != 3 {
    		http.Error(w, "Not found", http.StatusNotFound)
    		return
    	}
    
    	delay, err := parseBoundedDuration(parts[2], 0, h.MaxDuration)
    	if err != nil {
    		http.Error(w, "Invalid duration", http.StatusBadRequest)
    		return
    	}
    
    	select {
    	case <-r.Context().Done():
    		w.WriteHeader(499) // "Client Closed Request" https://httpstatuses.com/499
    		return
    	case <-time.After(delay):
    	}
    	h.RequestWithBody(w, r)
    }
    
    // Drip returns data over a duration after an optional initial delay, then
    // (optionally) returns with the given status code.
    func (h *HTTPBin) Drip(w http.ResponseWriter, r *http.Request) {
    	q := r.URL.Query()
    
    	var (
    		duration = h.DefaultParams.DripDuration
    		delay    = h.DefaultParams.DripDelay
    		numBytes = h.DefaultParams.DripNumBytes
    		code     = http.StatusOK
    
    		err error
    	)
    
    	if userDuration := q.Get("duration"); userDuration != "" {
    		duration, err = parseBoundedDuration(userDuration, 0, h.MaxDuration)
    		if err != nil {
    			http.Error(w, "Invalid duration", http.StatusBadRequest)
    			return
    		}
    	}
    
    	if userDelay := q.Get("delay"); userDelay != "" {
    		delay, err = parseBoundedDuration(userDelay, 0, h.MaxDuration)
    		if err != nil {
    			http.Error(w, "Invalid delay", http.StatusBadRequest)
    			return
    		}
    	}
    
    	if userNumBytes := q.Get("numbytes"); userNumBytes != "" {
    		numBytes, err = strconv.ParseInt(userNumBytes, 10, 64)
    		if err != nil || numBytes <= 0 || numBytes > h.MaxBodySize {
    			http.Error(w, "Invalid numbytes", http.StatusBadRequest)
    			return
    		}
    	}
    
    	if userCode := q.Get("code"); userCode != "" {
    		code, err = strconv.Atoi(userCode)
    		if err != nil || code < 100 || code >= 600 {
    			http.Error(w, "Invalid code", http.StatusBadRequest)
    			return
    		}
    	}
    
    	if duration+delay > h.MaxDuration {
    		http.Error(w, "Too much time", http.StatusBadRequest)
    		return
    	}
    
    	pause := duration / time.Duration(numBytes)
    	flusher := w.(http.Flusher)
    
    	w.Header().Set("Content-Type", "application/octet-stream")
    	w.Header().Set("Content-Length", fmt.Sprintf("%d", numBytes))
    	w.WriteHeader(code)
    	flusher.Flush()
    
    	select {
    	case <-r.Context().Done():
    		return
    	case <-time.After(delay):
    	}
    
    	b := []byte{'*'}
    	for i := int64(0); i < numBytes; i++ {
    		w.Write(b)
    		flusher.Flush()
    
    		select {
    		case <-r.Context().Done():
    			return
    		case <-time.After(pause):
    		}
    	}
    }
    
    // Range returns up to N bytes, with support for HTTP Range requests.
    //
    // This departs from httpbin by not supporting the chunk_size or duration
    // parameters.
    func (h *HTTPBin) Range(w http.ResponseWriter, r *http.Request) {
    	parts := strings.Split(r.URL.Path, "/")
    	if len(parts) != 3 {
    		http.Error(w, "Not found", http.StatusNotFound)
    		return
    	}
    
    	numBytes, err := strconv.ParseInt(parts[2], 10, 64)
    	if err != nil {
    		http.Error(w, err.Error(), http.StatusBadRequest)
    		return
    	}
    
    	w.Header().Add("ETag", fmt.Sprintf("range%d", numBytes))
    	w.Header().Add("Accept-Ranges", "bytes")
    
    	if numBytes <= 0 || numBytes > h.MaxBodySize {
    		http.Error(w, "Invalid number of bytes", http.StatusBadRequest)
    		return
    	}
    
    	content := newSyntheticByteStream(numBytes, func(offset int64) byte {
    		return byte(97 + (offset % 26))
    	})
    	var modtime time.Time
    	http.ServeContent(w, r, "", modtime, content)
    }
    
    // HTML renders a basic HTML page
    func (h *HTTPBin) HTML(w http.ResponseWriter, r *http.Request) {
    	writeHTML(w, mustStaticAsset("moby.html"), http.StatusOK)
    }
    
    // Robots renders a basic robots.txt file
    func (h *HTTPBin) Robots(w http.ResponseWriter, r *http.Request) {
    	robotsTxt := []byte(`User-agent: *
    Disallow: /deny
    `)
    	writeResponse(w, http.StatusOK, "text/plain", robotsTxt)
    }
    
    // Deny renders a basic page that robots should never access
    func (h *HTTPBin) Deny(w http.ResponseWriter, r *http.Request) {
    	writeResponse(w, http.StatusOK, "text/plain", []byte(`YOU SHOULDN'T BE HERE`))
    }
    
    // Cache returns a 304 if an If-Modified-Since or an If-None-Match header is
    // present, otherwise returns the same response as Get.
    func (h *HTTPBin) Cache(w http.ResponseWriter, r *http.Request) {
    	if r.Header.Get("If-Modified-Since") != "" || r.Header.Get("If-None-Match") != "" {
    		w.WriteHeader(http.StatusNotModified)
    		return
    	}
    
    	lastModified := time.Now().Format(time.RFC1123)
    	w.Header().Add("Last-Modified", lastModified)
    	w.Header().Add("ETag", sha1hash(lastModified))
    	h.Get(w, r)
    }
    
    // CacheControl sets a Cache-Control header for N seconds for /cache/N requests
    func (h *HTTPBin) CacheControl(w http.ResponseWriter, r *http.Request) {
    	parts := strings.Split(r.URL.Path, "/")
    	if len(parts) != 3 {
    		http.Error(w, "Not found", http.StatusNotFound)
    		return
    	}
    
    	seconds, err := strconv.ParseInt(parts[2], 10, 64)
    	if err != nil {
    		http.Error(w, err.Error(), http.StatusBadRequest)
    		return
    	}
    
    	w.Header().Add("Cache-Control", fmt.Sprintf("public, max-age=%d", seconds))
    	h.Get(w, r)
    }
    
    // ETag assumes the resource has the given etag and responds to If-None-Match
    // and If-Match headers appropriately.
    func (h *HTTPBin) ETag(w http.ResponseWriter, r *http.Request) {
    	parts := strings.Split(r.URL.Path, "/")
    	if len(parts) != 3 {
    		http.Error(w, "Not found", http.StatusNotFound)
    		return
    	}
    
    	etag := parts[2]
    	w.Header().Set("ETag", fmt.Sprintf(`"%s"`, etag))
    
    	var buf bytes.Buffer
    	mustMarshalJSON(&buf, noBodyResponse{
    		Args:    r.URL.Query(),
    		Headers: getRequestHeaders(r),
    		Method:  r.Method,
    		Origin:  getClientIP(r),
    		URL:     getURL(r).String(),
    	})
    
    	// Let http.ServeContent deal with If-None-Match and If-Match headers:
    	// https://golang.org/pkg/net/http/#ServeContent
    	http.ServeContent(w, r, "response.json", time.Now(), bytes.NewReader(buf.Bytes()))
    }
    
    // Bytes returns N random bytes generated with an optional seed
    func (h *HTTPBin) Bytes(w http.ResponseWriter, r *http.Request) {
    	handleBytes(w, r, false)
    }
    
    // StreamBytes streams N random bytes generated with an optional seed in chunks
    // of a given size.
    func (h *HTTPBin) StreamBytes(w http.ResponseWriter, r *http.Request) {
    	handleBytes(w, r, true)
    }
    
    // handleBytes consolidates the logic for validating input params of the Bytes
    // and StreamBytes endpoints and knows how to write the response in chunks if
    // streaming is true.
    func handleBytes(w http.ResponseWriter, r *http.Request, streaming bool) {
    	parts := strings.Split(r.URL.Path, "/")
    	if len(parts) != 3 {
    		http.Error(w, "Not found", http.StatusNotFound)
    		return
    	}
    
    	numBytes, err := strconv.Atoi(parts[2])
    	if err != nil {
    		http.Error(w, err.Error(), http.StatusBadRequest)
    		return
    	}
    
    	if numBytes < 0 {
    		http.Error(w, "Bad Request", http.StatusBadRequest)
    		return
    	}
    
    	// Special case 0 bytes and exit early, since streaming & chunk size do not
    	// matter here.
    	if numBytes == 0 {
    		w.Header().Set("Content-Length", "0")
    		w.WriteHeader(http.StatusOK)
    		return
    	}
    
    	if numBytes > 100*1024 {
    		numBytes = 100 * 1024
    	}
    
    	var chunkSize int
    	var write func([]byte)
    
    	if streaming {
    		if r.URL.Query().Get("chunk_size") != "" {
    			chunkSize, err = strconv.Atoi(r.URL.Query().Get("chunk_size"))
    			if err != nil {
    				http.Error(w, err.Error(), http.StatusBadRequest)
    				return
    			}
    		} else {
    			chunkSize = 10 * 1024
    		}
    
    		write = func() func(chunk []byte) {
    			f := w.(http.Flusher)
    			return func(chunk []byte) {
    				w.Write(chunk)
    				f.Flush()
    			}
    		}()
    	} else {
    		chunkSize = numBytes
    		write = func(chunk []byte) {
    			w.Header().Set("Content-Length", strconv.Itoa(len(chunk)))
    			w.Write(chunk)
    		}
    	}
    
    	// rng/seed
    	rng, err := parseSeed(r.URL.Query().Get("seed"))
    	if err != nil {
    		http.Error(w, "invalid seed", http.StatusBadRequest)
    		return
    	}
    
    	w.Header().Set("Content-Type", "application/octet-stream")
    	w.WriteHeader(http.StatusOK)
    
    	var chunk []byte
    	for i := 0; i < numBytes; i++ {
    		chunk = append(chunk, byte(rng.Intn(256)))
    		if len(chunk) == chunkSize {
    			write(chunk)
    			chunk = nil
    		}
    	}
    	if len(chunk) > 0 {
    		write(chunk)
    	}
    }
    
    // Links redirects to the first page in a series of N links
    func (h *HTTPBin) Links(w http.ResponseWriter, r *http.Request) {
    	parts := strings.Split(r.URL.Path, "/")
    	if len(parts) != 3 && len(parts) != 4 {
    		http.Error(w, "Not found", http.StatusNotFound)
    		return
    	}
    
    	n, err := strconv.Atoi(parts[2])
    	if err != nil || n < 0 || n > 256 {
    		http.Error(w, "Invalid link count", http.StatusBadRequest)
    		return
    	}
    
    	// Are we handling /links/<n>/<offset>? If so, render an HTML page
    	if len(parts) == 4 {
    		offset, err := strconv.Atoi(parts[3])
    		if err != nil {
    			http.Error(w, "Invalid offset", http.StatusBadRequest)
    		}
    		doLinksPage(w, r, n, offset)
    		return
    	}
    
    	// Otherwise, redirect from /links/<n> to /links/<n>/0
    	r.URL.Path = r.URL.Path + "/0"
    	w.Header().Set("Location", r.URL.String())
    	w.WriteHeader(http.StatusFound)
    }
    
    // doLinksPage renders a page with a series of N links
    func doLinksPage(w http.ResponseWriter, r *http.Request, n int, offset int) {
    	w.Header().Add("Content-Type", htmlContentType)
    	w.WriteHeader(http.StatusOK)
    
    	w.Write([]byte("<html><head><title>Links</title></head><body>"))
    	for i := 0; i < n; i++ {
    		if i == offset {
    			fmt.Fprintf(w, "%d ", i)
    		} else {
    			fmt.Fprintf(w, `<a href="/links/%d/%d">%d</a> `, n, i, i)
    		}
    	}
    	w.Write([]byte("</body></html>"))
    }
    
    // ImageAccept responds with an appropriate image based on the Accept header
    func (h *HTTPBin) ImageAccept(w http.ResponseWriter, r *http.Request) {
    	accept := r.Header.Get("Accept")
    	if accept == "" || strings.Contains(accept, "image/png") || strings.Contains(accept, "image/*") {
    		doImage(w, "png")
    	} else if strings.Contains(accept, "image/webp") {
    		doImage(w, "webp")
    	} else if strings.Contains(accept, "image/svg+xml") {
    		doImage(w, "svg")
    	} else if strings.Contains(accept, "image/jpeg") {
    		doImage(w, "jpeg")
    	} else {
    		http.Error(w, "Unsupported media type", http.StatusUnsupportedMediaType)
    	}
    }
    
    // Image responds with an image of a specific kind, from /image/<kind>
    func (h *HTTPBin) Image(w http.ResponseWriter, r *http.Request) {
    	parts := strings.Split(r.URL.Path, "/")
    	if len(parts) != 3 {
    		http.Error(w, "Not found", http.StatusNotFound)
    		return
    	}
    	doImage(w, parts[2])
    }
    
    // doImage responds with a specific kind of image, if there is an image asset
    // of the given kind.
    func doImage(w http.ResponseWriter, kind string) {
    	img, err := staticAsset("image." + kind)
    	if err != nil {
    		http.Error(w, "Not Found", http.StatusNotFound)
    	}
    	contentType := "image/" + kind
    	if kind == "svg" {
    		contentType = "image/svg+xml"
    	}
    	writeResponse(w, http.StatusOK, contentType, img)
    }
    
    // XML responds with an XML document
    func (h *HTTPBin) XML(w http.ResponseWriter, r *http.Request) {
    	writeResponse(w, http.StatusOK, "application/xml", mustStaticAsset("sample.xml"))
    }
    
    // DigestAuth handles a simple implementation of HTTP Digest Authentication,
    // which supports the "auth" QOP and the MD5 and SHA-256 crypto algorithms.
    //
    // /digest-auth/<qop>/<user>/<passwd>
    // /digest-auth/<qop>/<user>/<passwd>/<algorithm>
    func (h *HTTPBin) DigestAuth(w http.ResponseWriter, r *http.Request) {
    	parts := strings.Split(r.URL.Path, "/")
    	count := len(parts)
    
    	if count != 5 && count != 6 {
    		http.Error(w, "Not Found", http.StatusNotFound)
    		return
    	}
    
    	qop := strings.ToLower(parts[2])
    	user := parts[3]
    	password := parts[4]
    
    	algoName := "MD5"
    	if count == 6 {
    		algoName = strings.ToUpper(parts[5])
    	}
    
    	if qop != "auth" {
    		http.Error(w, "Invalid QOP directive", http.StatusBadRequest)
    		return
    	}
    	if algoName != "MD5" && algoName != "SHA-256" {
    		http.Error(w, "Invalid algorithm", http.StatusBadRequest)
    		return
    	}
    
    	algorithm := digest.MD5
    	if algoName == "SHA-256" {
    		algorithm = digest.SHA256
    	}
    
    	if !digest.Check(r, user, password) {
    		w.Header().Set("WWW-Authenticate", digest.Challenge("go-httpbin", algorithm))
    		w.WriteHeader(http.StatusUnauthorized)
    		return
    	}
    
    	writeJSON(http.StatusOK, w, authResponse{
    		Authorized: true,
    		User:       user,
    	})
    }
    
    // UUID - responds with a generated UUID
    func (h *HTTPBin) UUID(w http.ResponseWriter, r *http.Request) {
    	writeJSON(http.StatusOK, w, uuidResponse{
    		UUID: uuidv4(),
    	})
    }
    
    // Base64 - encodes/decodes input data
    func (h *HTTPBin) Base64(w http.ResponseWriter, r *http.Request) {
    	b, err := newBase64Helper(r.URL.Path)
    	if err != nil {
    		http.Error(w, fmt.Sprintf("%s", err), http.StatusBadRequest)
    		return
    	}
    
    	var result []byte
    	var base64Error error
    
    	if b.operation == "decode" {
    		result, base64Error = b.Decode()
    	} else {
    		result, base64Error = b.Encode()
    	}
    
    	if base64Error != nil {
    		http.Error(w, fmt.Sprintf("%s failed: %s", b.operation, base64Error), http.StatusBadRequest)
    		return
    	}
    	writeResponse(w, http.StatusOK, "text/plain", result)
    }
    
    // DumpRequest - returns the given request in its HTTP/1.x wire representation.
    // The returned representation is an approximation only;
    // some details of the initial request are lost while parsing it into
    // an http.Request. In particular, the order and case of header field
    // names are lost.
    func (h *HTTPBin) DumpRequest(w http.ResponseWriter, r *http.Request) {
    	dump, err := httputil.DumpRequest(r, true)
    	if err != nil {
    		http.Error(w, err.Error(), http.StatusInternalServerError)
    		return
    	}
    	w.Write(dump)
    }
    
    // JSON - returns a sample json
    func (h *HTTPBin) JSON(w http.ResponseWriter, r *http.Request) {
    	w.Header().Set("Content-Type", jsonContentType)
    	w.WriteHeader(http.StatusOK)
    	w.Write(mustStaticAsset("sample.json"))
    }
    
    // Bearer - Prompts the user for authorization using bearer authentication.
    func (h *HTTPBin) Bearer(w http.ResponseWriter, r *http.Request) {
    	reqToken := r.Header.Get("Authorization")
    	tokenFields := strings.Fields(reqToken)
    	if len(tokenFields) != 2 || tokenFields[0] != "Bearer" {
    		w.Header().Set("WWW-Authenticate", "Bearer")
    		w.WriteHeader(http.StatusUnauthorized)
    		return
    	}
    	writeJSON(http.StatusOK, w, bearerResponse{
    		Authenticated: true,
    		Token:         tokenFields[1],
    	})
    }
    
    // Hostname - returns the hostname.
    func (h *HTTPBin) Hostname(w http.ResponseWriter, r *http.Request) {
    	writeJSON(http.StatusOK, w, hostnameResponse{
    		Hostname: h.hostname,
    	})
    }