You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
984 lines
25 KiB
Go
984 lines
25 KiB
Go
|
3 months ago
|
package httpbin
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"compress/gzip"
|
||
|
|
"compress/zlib"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"math/rand"
|
||
|
|
"net/http"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/mccutchen/go-httpbin/httpbin/assets"
|
||
|
|
"github.com/mccutchen/go-httpbin/httpbin/digest"
|
||
|
|
)
|
||
|
|
|
||
|
|
var acceptedMediaTypes = []string{
|
||
|
|
"image/webp",
|
||
|
|
"image/svg+xml",
|
||
|
|
"image/jpeg",
|
||
|
|
"image/png",
|
||
|
|
"image/",
|
||
|
|
}
|
||
|
|
|
||
|
|
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 != "/" {
|
||
|
|
http.Error(w, "Not Found", 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, assets.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, assets.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, assets.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: getRequestHeaders(r),
|
||
|
|
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: getRequestHeaders(r),
|
||
|
|
Origin: getOrigin(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
|
||
|
|
}
|
||
|
|
|
||
|
|
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: getRequestHeaders(r),
|
||
|
|
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: getRequestHeaders(r),
|
||
|
|
Origin: getOrigin(r),
|
||
|
|
Deflated: true,
|
||
|
|
}
|
||
|
|
body, _ := json.Marshal(resp)
|
||
|
|
|
||
|
|
buf := &bytes.Buffer{}
|
||
|
|
w2 := zlib.NewWriter(buf)
|
||
|
|
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: getRequestHeaders(r),
|
||
|
|
})
|
||
|
|
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,
|
||
|
|
})
|
||
|
|
|
||
|
|
http300body := []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>`)
|
||
|
|
|
||
|
|
http308body := []byte(`<!doctype html>
|
||
|
|
<head>
|
||
|
|
<title>Permanent Redirect</title>
|
||
|
|
</head>
|
||
|
|
<body>Permanently redirected to <a href="/image/jpeg">/image/jpeg</a>
|
||
|
|
</body>
|
||
|
|
</html>`)
|
||
|
|
|
||
|
|
specialCases := map[int]*statusCase{
|
||
|
|
300: {
|
||
|
|
body: http300body,
|
||
|
|
headers: map[string]string{
|
||
|
|
"Location": "/image/jpeg",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
301: redirectHeaders,
|
||
|
|
302: redirectHeaders,
|
||
|
|
303: redirectHeaders,
|
||
|
|
305: redirectHeaders,
|
||
|
|
307: redirectHeaders,
|
||
|
|
308: {
|
||
|
|
body: http308body,
|
||
|
|
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: notAcceptableBody,
|
||
|
|
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",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
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(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)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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()
|
||
|
|
|
||
|
|
url := q.Get("url")
|
||
|
|
if url == "" {
|
||
|
|
http.Error(w, "Missing URL", http.StatusBadRequest)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
var err error
|
||
|
|
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", url)
|
||
|
|
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
|
||
|
|
}
|
||
|
|
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)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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: 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.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):
|
||
|
|
}
|
||
|
|
|
||
|
|
for i := int64(0); i < numBytes; i++ {
|
||
|
|
w.Write([]byte("*"))
|
||
|
|
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, assets.MustAsset("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 response 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))
|
||
|
|
|
||
|
|
// TODO: This mostly duplicates the work of Get() above, should this be
|
||
|
|
// pulled into a little helper?
|
||
|
|
resp := &getResponse{
|
||
|
|
Args: r.URL.Query(),
|
||
|
|
Headers: getRequestHeaders(r),
|
||
|
|
Origin: getOrigin(r),
|
||
|
|
URL: getURL(r).String(),
|
||
|
|
}
|
||
|
|
body, _ := json.Marshal(resp)
|
||
|
|
|
||
|
|
// 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(body))
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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 < 1 {
|
||
|
|
numBytes = 1
|
||
|
|
} else 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)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var seed int64
|
||
|
|
rawSeed := r.URL.Query().Get("seed")
|
||
|
|
if rawSeed != "" {
|
||
|
|
seed, err = strconv.ParseInt(rawSeed, 10, 64)
|
||
|
|
if err != nil {
|
||
|
|
http.Error(w, "invalid seed", http.StatusBadRequest)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
seed = time.Now().Unix()
|
||
|
|
}
|
||
|
|
|
||
|
|
src := rand.NewSource(seed)
|
||
|
|
rng := rand.New(src)
|
||
|
|
|
||
|
|
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 := assets.Asset("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", assets.MustAsset("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
|
||
|
|
}
|
||
|
|
|
||
|
|
resp, _ := json.Marshal(&authResponse{
|
||
|
|
Authorized: true,
|
||
|
|
User: user,
|
||
|
|
})
|
||
|
|
writeJSON(w, resp, http.StatusOK)
|
||
|
|
}
|
||
|
|
|
||
|
|
// UUID - responds with a generated UUID
|
||
|
|
func (h *HTTPBin) UUID(w http.ResponseWriter, r *http.Request) {
|
||
|
|
resp, _ := json.Marshal(&uuidResponse{
|
||
|
|
UUID: uuidv4(),
|
||
|
|
})
|
||
|
|
writeJSON(w, resp, http.StatusOK)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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/html", result)
|
||
|
|
}
|
||
|
|
|
||
|
|
// JSON - returns a sample json
|
||
|
|
func (h *HTTPBin) JSON(w http.ResponseWriter, r *http.Request) {
|
||
|
|
writeJSON(w, assets.MustAsset("sample.json"), http.StatusOK)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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
|
||
|
|
}
|
||
|
|
body, _ := json.Marshal(&bearerResponse{
|
||
|
|
Authenticated: true,
|
||
|
|
Token: tokenFields[1],
|
||
|
|
})
|
||
|
|
writeJSON(w, body, http.StatusOK)
|
||
|
|
}
|