Skip to content
Snippets Groups Projects
Select Git revision
  • main default protected
  • drip-server-timing
  • compress-middleware
  • v2.11.0
  • v2.10.0
  • v2.9.2
  • v2.9.1
  • v2.9.0
  • v2.8.0
  • v2.7.0
  • v2.6.0
  • v2.5.6
  • v2.5.5
  • v2.5.4
  • v2.5.3
  • v2.5.2
  • v2.5.1
  • v2.5.0
  • v2.4.2
  • v2.4.1
  • v2.4.0
  • v2.3.0
  • v2.2.2
23 results

handlers.go

Blame
  • handlers.go 13.69 KiB
    package httpbin
    
    import (
    	"bytes"
    	"compress/flate"
    	"compress/gzip"
    	"encoding/json"
    	"fmt"
    	"net/http"
    	"strconv"
    	"strings"
    	"time"
    )
    
    var acceptedMediaTypes = []string{
    	"image/webp",
    	"image/svg+xml",
    	"image/jpeg",
    	"image/png",
    	"image/",
    }
    
    // Index renders an HTML index page
    func (h *HTTPBin) Index(w http.ResponseWriter, r *http.Request) {
    	if r.URL.Path != "/" {
    		http.Error(w, "Not Found", http.StatusNotFound)
    		return
    	}
    	writeHTML(w, MustAsset("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, MustAsset("forms-post.html"), http.StatusOK)
    }
    
    // UTF8 renders an HTML encoding stress test
    func (h *HTTPBin) UTF8(w http.ResponseWriter, r *http.Request) {
    	writeHTML(w, MustAsset("utf8.html"), http.StatusOK)
    }
    
    // Get handles HTTP GET requests
    func (h *HTTPBin) Get(w http.ResponseWriter, r *http.Request) {
    	resp := &getResponse{
    		Args:    r.URL.Query(),
    		Headers: r.Header,
    		Origin:  getOrigin(r),
    		URL:     getURL(r).String(),
    	}
    	body, _ := json.Marshal(resp)
    	writeJSON(w, body, http.StatusOK)
    }
    
    // RequestWithBody handles POST, PUT, and PATCH requests
    func (h *HTTPBin) RequestWithBody(w http.ResponseWriter, r *http.Request) {
    	resp := &bodyResponse{
    		Args:    r.URL.Query(),
    		Headers: r.Header,
    		Origin:  getOrigin(r),
    		URL:     getURL(r).String(),
    	}
    
    	err := parseBody(w, r, resp, h.options.MaxMemory)
    	if err != nil {
    		http.Error(w, fmt.Sprintf("error parsing request body: %s", err), http.StatusBadRequest)
    		return
    	}
    
    	body, _ := json.Marshal(resp)
    	writeJSON(w, body, http.StatusOK)
    }
    
    // Gzip returns a gzipped response
    func (h *HTTPBin) Gzip(w http.ResponseWriter, r *http.Request) {
    	resp := &gzipResponse{
    		Headers: r.Header,
    		Origin:  getOrigin(r),
    		Gzipped: true,
    	}
    	body, _ := json.Marshal(resp)
    
    	buf := &bytes.Buffer{}
    	gzw := gzip.NewWriter(buf)
    	gzw.Write(body)
    	gzw.Close()
    
    	gzBody := buf.Bytes()
    
    	w.Header().Set("Content-Encoding", "gzip")
    	writeJSON(w, gzBody, http.StatusOK)
    }
    
    // Deflate returns a gzipped response
    func (h *HTTPBin) Deflate(w http.ResponseWriter, r *http.Request) {
    	resp := &deflateResponse{
    		Headers:  r.Header,
    		Origin:   getOrigin(r),
    		Deflated: true,
    	}
    	body, _ := json.Marshal(resp)
    
    	buf := &bytes.Buffer{}
    	w2, _ := flate.NewWriter(buf, flate.DefaultCompression)
    	w2.Write(body)
    	w2.Close()
    
    	compressedBody := buf.Bytes()
    
    	w.Header().Set("Content-Encoding", "deflate")
    	writeJSON(w, compressedBody, http.StatusOK)
    }
    
    // IP echoes the IP address of the incoming request
    func (h *HTTPBin) IP(w http.ResponseWriter, r *http.Request) {
    	body, _ := json.Marshal(&ipResponse{
    		Origin: getOrigin(r),
    	})
    	writeJSON(w, body, http.StatusOK)
    }
    
    // UserAgent echoes the incoming User-Agent header
    func (h *HTTPBin) UserAgent(w http.ResponseWriter, r *http.Request) {
    	body, _ := json.Marshal(&userAgentResponse{
    		UserAgent: r.Header.Get("User-Agent"),
    	})
    	writeJSON(w, body, http.StatusOK)
    }
    
    // Headers echoes the incoming request headers
    func (h *HTTPBin) Headers(w http.ResponseWriter, r *http.Request) {
    	body, _ := json.Marshal(&headersResponse{
    		Headers: r.Header,
    	})
    	writeJSON(w, body, http.StatusOK)
    }
    
    // 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
    	}
    
    	type statusCase struct {
    		headers map[string]string
    		body    []byte
    	}
    
    	redirectHeaders := &statusCase{
    		headers: map[string]string{
    			"Location": "/redirect/1",
    		},
    	}
    	notAcceptableBody, _ := json.Marshal(map[string]interface{}{
    		"message": "Client did not request a supported media type",
    		"accept":  acceptedMediaTypes,
    	})
    
    	specialCases := map[int]*statusCase{
    		301: redirectHeaders,
    		302: redirectHeaders,
    		303: redirectHeaders,
    		305: redirectHeaders,
    		307: redirectHeaders,
    		401: &statusCase{
    			headers: map[string]string{
    				"WWW-Authenticate": `Basic realm="Fake Realm"`,
    			},
    		},
    		402: &statusCase{
    			body: []byte("Fuck you, pay me!"),
    			headers: map[string]string{
    				"X-More-Info": "http://vimeo.com/22053820",
    			},
    		},
    		406: &statusCase{
    			body: notAcceptableBody,
    			headers: map[string]string{
    				"Content-Type": jsonContentType,
    			},
    		},
    		407: &statusCase{
    			headers: map[string]string{
    				"Proxy-Authenticate": `Basic realm="Fake Realm"`,
    			},
    		},
    		418: &statusCase{
    			body: []byte("I'm a teapot!"),
    			headers: map[string]string{
    				"X-More-Info": "http://tools.ietf.org/html/rfc2324",
    			},
    		},
    	}
    
    	if specialCase, ok := specialCases[code]; ok {
    		if specialCase.headers != nil {
    			for key, val := range specialCase.headers {
    				w.Header().Set(key, val)
    			}
    		}
    		w.WriteHeader(code)
    		if specialCase.body != nil {
    			w.Write(specialCase.body)
    		}
    	} else {
    		w.WriteHeader(code)
    	}
    }
    
    // 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(http.CanonicalHeaderKey(k), v)
    		}
    	}
    	body, _ := json.Marshal(args)
    	if contentType := w.Header().Get("Content-Type"); contentType == "" {
    		w.Header().Set("Content-Type", jsonContentType)
    	}
    	w.Write(body)
    }
    
    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)
    }
    
    // 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
    	}
    	body, _ := json.Marshal(resp)
    	writeJSON(w, body, http.StatusOK)
    }
    
    // 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"`)
    	}
    
    	body, _ := json.Marshal(&authResponse{
    		Authorized: authorized,
    		User:       givenUser,
    	})
    	writeJSON(w, body, status)
    }
    
    // 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
    	}
    
    	body, _ := json.Marshal(&authResponse{
    		Authorized: authorized,
    		User:       givenUser,
    	})
    	writeJSON(w, body, http.StatusOK)
    }
    
    // DigestAuth is not yet implemented, and returns 501 Not Implemented. It
    // appears that stdlib support for working with digest authentication is
    // lacking, and I'm not yet ready to implement it myself.
    func (h *HTTPBin) DigestAuth(w http.ResponseWriter, r *http.Request) {
    	parts := strings.Split(r.URL.Path, "/")
    	if len(parts) != 5 {
    		http.Error(w, "Not Found", http.StatusNotFound)
    		return
    	}
    	http.Error(w, "Not Implemented", http.StatusNotImplemented)
    }
    
    // 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: r.Header,
    		Origin:  getOrigin(r),
    		URL:     getURL(r).String(),
    	}
    
    	f := w.(http.Flusher)
    	for i := 0; i < n; i++ {
    		resp.ID = i
    		line, _ := json.Marshal(resp)
    		w.Write(line)
    		w.Write([]byte("\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.options.MaxResponseTime)
    	if err != nil {
    		http.Error(w, "Invalid duration", http.StatusBadRequest)
    		return
    	}
    
    	<-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()
    
    	duration := time.Duration(0)
    	delay := time.Duration(0)
    	numbytes := int64(10)
    	code := http.StatusOK
    
    	var err error
    
    	userDuration := q.Get("duration")
    	if userDuration != "" {
    		duration, err = parseBoundedDuration(userDuration, 0, h.options.MaxResponseTime)
    		if err != nil {
    			http.Error(w, "Invalid duration", http.StatusBadRequest)
    			return
    		}
    	}
    
    	userDelay := q.Get("delay")
    	if userDelay != "" {
    		delay, err = parseBoundedDuration(userDelay, 0, h.options.MaxResponseTime)
    		if err != nil {
    			http.Error(w, "Invalid delay", http.StatusBadRequest)
    			return
    		}
    	}
    
    	userNumBytes := q.Get("numbytes")
    	if userNumBytes != "" {
    		numbytes, err = strconv.ParseInt(userNumBytes, 10, 64)
    		if err != nil || numbytes <= 0 || numbytes > h.options.MaxResponseSize {
    			http.Error(w, "Invalid numbytes", http.StatusBadRequest)
    			return
    		}
    	}
    
    	userCode := q.Get("code")
    	if 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.options.MaxResponseTime {
    		http.Error(w, "Too much time", http.StatusBadRequest)
    		return
    	}
    
    	pause := duration / time.Duration(numbytes)
    
    	<-time.After(delay)
    
    	w.WriteHeader(code)
    	w.Header().Set("Content-Type", "application/octet-stream")
    
    	f := w.(http.Flusher)
    	for i := int64(0); i < numbytes; i++ {
    		w.Write([]byte("*"))
    		f.Flush()
    		<-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.options.MaxMemory {
    		http.Error(w, "Invalid number of bytes", http.StatusBadRequest)
    		return
    	}
    
    	content := &syntheticReadSeeker{
    		numBytes:    numBytes,
    		byteFactory: func(offset int64) byte { return byte(97 + (offset % 26)) },
    	}
    	var modtime time.Time
    	http.ServeContent(w, r, "", modtime, content)
    }