first commit

main
修改密码漏洞修复完成 3 months ago
parent 9911a3c7fb
commit 54b12659ba

@ -0,0 +1,14 @@
FROM golang:1.15
WORKDIR /go/src/github.com/mccutchen/go-httpbin
# Manually implement the subset of `make deps` we need to build the image
RUN cd /tmp && go get -u github.com/kevinburke/go-bindata/...
COPY . .
RUN make build buildtests
FROM gcr.io/distroless/base
COPY --from=0 /go/src/github.com/mccutchen/go-httpbin/dist/go-httpbin* /bin/
EXPOSE 8080
CMD ["/bin/go-httpbin"]

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Will McCutchen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,166 @@
.PHONY: clean deploy deps gcloud-auth image imagepush lint run stagedeploy test testci testcover
# The version that will be used in docker tags (e.g. to push a
# go-httpbin:latest image use `make imagepush VERSION=latest)`
VERSION ?= $(shell git rev-parse --short HEAD)
# Override these values to deploy to a different Cloud Run project
GCLOUD_PROJECT ?= httpbingo
GCLOUD_ACCOUNT ?= mccutchen@gmail.com
GCLOUD_REGION ?= us-central1
# The version tag for the Cloud Run deployment (override this to adjust
# pre-production URLs)
GCLOUD_TAG ?= "v-$(VERSION)"
# Run gcloud in a container to avoid needing to install the SDK locally
GCLOUD_COMMAND ?= ./bin/gcloud
# We push docker images to both docker hub and gcr.io
DOCKER_TAG_DOCKERHUB ?= mccutchen/go-httpbin:$(VERSION)
DOCKER_TAG_GCLOUD ?= gcr.io/$(GCLOUD_PROJECT)/go-httpbin:$(VERSION)
# Built binaries will be placed here
DIST_PATH ?= dist
# Default flags used by the test, testci, testcover targets
COVERAGE_PATH ?= coverage.txt
COVERAGE_ARGS ?= -covermode=atomic -coverprofile=$(COVERAGE_PATH)
TEST_ARGS ?= -race
# Tool dependencies
TOOL_BIN_DIR ?= $(shell go env GOPATH)/bin
TOOL_GOBINDATA := $(TOOL_BIN_DIR)/go-bindata
TOOL_GOLINT := $(TOOL_BIN_DIR)/golint
TOOL_STATICCHECK := $(TOOL_BIN_DIR)/staticcheck
GO_SOURCES = $(wildcard **/*.go)
GENERATED_ASSETS_PATH := httpbin/assets/assets.go
# =============================================================================
# build
# =============================================================================
build: $(DIST_PATH)/go-httpbin
$(DIST_PATH)/go-httpbin: assets $(GO_SOURCES)
mkdir -p $(DIST_PATH)
CGO_ENABLED=0 go build -ldflags="-s -w" -o $(DIST_PATH)/go-httpbin ./cmd/go-httpbin
assets: $(GENERATED_ASSETS_PATH)
buildtests:
CGO_ENABLED=0 go test -ldflags="-s -w" -v -c -o $(DIST_PATH)/go-httpbin.test ./httpbin
clean:
rm -rf $(DIST_PATH) $(COVERAGE_PATH)
$(GENERATED_ASSETS_PATH): $(TOOL_GOBINDATA) static/*
$(TOOL_GOBINDATA) -o $(GENERATED_ASSETS_PATH) -pkg=assets -prefix=static -modtime=1601471052 static
# reformat generated code
gofmt -s -w $(GENERATED_ASSETS_PATH)
# dumb hack to make generate code lint correctly
sed -i.bak 's/Html/HTML/g' $(GENERATED_ASSETS_PATH)
sed -i.bak 's/Xml/XML/g' $(GENERATED_ASSETS_PATH)
rm $(GENERATED_ASSETS_PATH).bak
# =============================================================================
# test & lint
# =============================================================================
test:
go test $(TEST_ARGS) ./...
# Test command to run for continuous integration, which includes code coverage
# based on codecov.io's documentation:
# https://github.com/codecov/example-go/blob/b85638743b972bd0bd2af63421fe513c6f968930/README.md
testci: build
go test $(TEST_ARGS) $(COVERAGE_ARGS) ./...
git diff --exit-code
testcover: testci
go tool cover -html=$(COVERAGE_PATH)
lint: $(TOOL_GOLINT) $(TOOL_STATICCHECK)
test -z "$$(gofmt -d -s -e .)" || (echo "Error: gofmt failed"; gofmt -d -s -e . ; exit 1)
go vet ./...
$(TOOL_GOLINT) -set_exit_status ./...
$(TOOL_STATICCHECK) ./...
# =============================================================================
# run locally
# =============================================================================
run: build
$(DIST_PATH)/go-httpbin
watch: $(TOOL_REFLEX)
reflex -s -r '\.(go|html)$$' make run
# =============================================================================
# deploy to fly.io
# =============================================================================
deploy:
flyctl deploy --strategy=rolling
# =============================================================================
# deploy to google cloud run
# =============================================================================
deploy-cloud-run: gcloud-auth imagepush
$(GCLOUD_COMMAND) beta run deploy \
$(GCLOUD_PROJECT) \
--image=$(DOCKER_TAG_GCLOUD) \
--revision-suffix=$(VERSION) \
--tag=$(GCLOUD_TAG) \
--project=$(GCLOUD_PROJECT) \
--region=$(GCLOUD_REGION) \
--allow-unauthenticated \
--platform=managed
$(GCLOUD_COMMAND) run services update-traffic --to-latest
stagedeploy-cloud-run: gcloud-auth imagepush
$(GCLOUD_COMMAND) beta run deploy \
$(GCLOUD_PROJECT) \
--image=$(DOCKER_TAG_GCLOUD) \
--revision-suffix=$(VERSION) \
--tag=$(GCLOUD_TAG) \
--project=$(GCLOUD_PROJECT) \
--region=$(GCLOUD_REGION) \
--allow-unauthenticated \
--platform=managed \
--no-traffic
gcloud-auth:
@$(GCLOUD_COMMAND) auth list | grep '^\*' | grep -q $(GCLOUD_ACCOUNT) || $(GCLOUD_COMMAND) auth login $(GCLOUD_ACCOUNT)
@$(GCLOUD_COMMAND) auth print-access-token | docker login -u oauth2accesstoken --password-stdin https://gcr.io
# =============================================================================
# docker images
# =============================================================================
image:
docker build -t $(DOCKER_TAG_DOCKERHUB) .
imagepush: image
docker push $(DOCKER_TAG_DOCKERHUB)
# =============================================================================
# dependencies
#
# Deps are installed outside of working dir to avoid polluting go modules
# =============================================================================
$(TOOL_GOBINDATA):
cd /tmp && go get -u github.com/kevinburke/go-bindata/...
$(TOOL_GOLINT):
cd /tmp && go get -u golang.org/x/lint/golint
$(TOOL_REFLEX):
cd /tmp && go get -u github.com/cespare/reflex
$(TOOL_STATICCHECK):
cd /tmp && go get -u honnef.co/go/tools/cmd/staticcheck

@ -1,3 +1,139 @@
# go-httpbin-master
# go-httpbin
go-httpbin-master
A reasonably complete and well-tested golang port of [Kenneth Reitz][kr]'s
[httpbin][httpbin-org] service, with zero dependencies outside the go stdlib.
[![GoDoc](https://godoc.org/github.com/mccutchen/go-httpbin?status.svg)](https://godoc.org/github.com/mccutchen/go-httpbin)
[![Build Status](https://travis-ci.org/mccutchen/go-httpbin.svg?branch=master)](http://travis-ci.org/mccutchen/go-httpbin)
[![Coverage](https://codecov.io/gh/mccutchen/go-httpbin/branch/master/graph/badge.svg)](https://codecov.io/gh/mccutchen/go-httpbin)
## Usage
Run as a standalone binary, configured by command line flags or environment
variables:
```
$ go-httpbin -help
Usage of go-httpbin:
-host string
Host to listen on (default "0.0.0.0")
-port int
Port to listen on (default 8080)
-https-cert-file string
HTTPS certificate file
-https-key-file string
HTTPS private key file
-max-body-size int
Maximum size of request or response, in bytes (default 1048576)
-max-duration duration
Maximum duration a response may take (default 10s)
Examples:
# Run http server
$ go-httpbin -host 127.0.0.1 -port 8081
# Run https server
# Generate .crt and .key files
$ openssl genrsa -out server.key 2048
$ openssl ecparam -genkey -name secp384r1 -out server.key
$ openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
$ go-httpbin -host 127.0.0.1 -port 8081 -https-cert-file ./server.crt -https-key-file ./server.key
```
Docker images are published to [Docker Hub][docker-hub]:
```
# Run http server
$ docker run -P mccutchen/go-httpbin
# Run https server
$ docker run -e HTTPS_CERT_FILE='/tmp/server.crt' -e HTTPS_KEY_FILE='/tmp/server.key' -p 8080:8080 -v /tmp:/tmp mccutchen/go-httpbin
```
The `github.com/mccutchen/go-httpbin/httpbin` package can also be used as a
library for testing an applications interactions with an upstream HTTP service,
like so:
```go
package httpbin_test
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/mccutchen/go-httpbin/httpbin"
)
func TestSlowResponse(t *testing.T) {
svc := httpbin.New()
srv := httptest.NewServer(svc.Handler())
defer srv.Close()
client := http.Client{
Timeout: time.Duration(1 * time.Second),
}
_, err := client.Get(srv.URL + "/delay/10")
if err == nil {
t.Fatal("expected timeout error")
}
}
```
## Installation
```
go get github.com/mccutchen/go-httpbin/cmd/go-httpbin
```
## Motivation & prior art
I've been a longtime user of [Kenneith Reitz][kr]'s original
[httpbin.org][httpbin-org], and wanted to write a golang port for fun and to
see how far I could get using only the stdlib.
When I started this project, there were a handful of existing and incomplete
golang ports, with the most promising being [ahmetb/go-httpbin][ahmet]. This
project showed me how useful it might be to have an `httpbin` _library_
available for testing golang applications.
### Known differences from other httpbin versions
Compared to [the original][httpbin-org]:
- No `/brotli` endpoint (due to lack of support in Go's stdlib)
- The `?show_env=1` query param is ignored (i.e. no special handling of
runtime environment headers)
- Response values which may be encoded as either a string or a list of strings
will always be encoded as a list of strings (e.g. request headers, query
params, form values)
Compared to [ahmetb/go-httpbin][ahmet]:
- No dependencies on 3rd party packages
- More complete implementation of endpoints
## Development
```bash
# local development
make
make test
make testcover
make run
# building & pushing docker images
make image
make imagepush
```
[kr]: https://github.com/kennethreitz
[httpbin-org]: https://httpbin.org/
[httpbin-repo]: https://github.com/kennethreitz/httpbin
[ahmet]: https://github.com/ahmetb/go-httpbin
[docker-hub]: https://hub.docker.com/r/mccutchen/go-httpbin/

@ -0,0 +1,16 @@
---
runtime: go113
main: ./cmd/go_httpbin
handlers:
# Always redirect index requests to https
- url: /
script: auto
secure: always
redirect_http_response_code: 301
# Allow requests for any other resources via either http or https
- url: /.+
script: auto
secure: optional

@ -0,0 +1,17 @@
#!/bin/bash
#
# A wrapper that executes the gcloud CLI in a docker container, to avoid
# requiring a local installation.
#
# Adapted from this helpful blog post:
# https://blog.scottlowe.org/2018/09/13/running-gcloud-cli-in-a-docker-container/
GCLOUD_SDK_TAG="312.0.0"
exec docker run \
--rm -it \
--workdir /code \
-v $PWD:/code \
-v $HOME/.config/gcloud:/root/.config/gcloud \
google/cloud-sdk:$GCLOUD_SDK_TAG \
gcloud $*

@ -0,0 +1,38 @@
# What is going on here?
## TL;DR
* `cmd/maincmd` package exposes all of this app's command line functionality in a `Main()`
* `cmd/go-httpbin` and `cmd/go_httpbin` build identical binaries using the
`maincmd` package for backwards compatibility reasons explained below
## Why tho
Originally, this project exposed only one command:
cmd/go-httpbin/main.go
But the dash in that path was incompatible with Google App Engine's naming
restrictions, so in [moving httpbingo.org onto Google App Engine][pr17], that
path was (carelessly) renamed to
cmd/go_httpbin/main.go
_That_ change had a number of unintended consequences:
* It broke existing workflows built around `go get github.com/mccutchen/go-httpbin/cmd/go-httpbin`,
as suggested in the README
* It broke the Makefile, which was still looking for `cmd/go-httpbin`
* It broke the absolute aesthetic truth that CLI binaries should use dashes
instead of underscores for word separators
So, to restore the former behavior while maintaining support for deploying to
App Engine, the actual main functionality was extracted into the `cmd/maincmd`
package here and shared between the other two.
(This is pretty dumb, I know, but it seems to work.)
[pr17]: https://github.com/mccutchen/go-httpbin/pull/17

@ -0,0 +1,7 @@
package main
import "github.com/mccutchen/go-httpbin/cmd/maincmd"
func main() {
maincmd.Main()
}

Binary file not shown.

@ -0,0 +1,7 @@
package main
import "github.com/mccutchen/go-httpbin/cmd/maincmd"
func main() {
maincmd.Main()
}

@ -0,0 +1,153 @@
package maincmd
import (
"context"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/mccutchen/go-httpbin/httpbin"
)
const defaultHost = "0.0.0.0"
const defaultPort = 8080
var (
host string
port int
maxBodySize int64
maxDuration time.Duration
httpsCertFile string
httpsKeyFile string
)
// Main implements the go-httpbin CLI's main() function in a reusable way
func Main() {
flag.StringVar(&host, "host", defaultHost, "Host to listen on")
flag.IntVar(&port, "port", defaultPort, "Port to listen on")
flag.StringVar(&httpsCertFile, "https-cert-file", "", "HTTPS Server certificate file")
flag.StringVar(&httpsKeyFile, "https-key-file", "", "HTTPS Server private key file")
flag.Int64Var(&maxBodySize, "max-body-size", httpbin.DefaultMaxBodySize, "Maximum size of request or response, in bytes")
flag.DurationVar(&maxDuration, "max-duration", httpbin.DefaultMaxDuration, "Maximum duration a response may take")
flag.Parse()
// Command line flags take precedence over environment vars, so we only
// check for environment vars if we have default values for our command
// line flags.
var err error
if maxBodySize == httpbin.DefaultMaxBodySize && os.Getenv("MAX_BODY_SIZE") != "" {
maxBodySize, err = strconv.ParseInt(os.Getenv("MAX_BODY_SIZE"), 10, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: invalid value %#v for env var MAX_BODY_SIZE: %s\n\n", os.Getenv("MAX_BODY_SIZE"), err)
flag.Usage()
os.Exit(1)
}
}
if maxDuration == httpbin.DefaultMaxDuration && os.Getenv("MAX_DURATION") != "" {
maxDuration, err = time.ParseDuration(os.Getenv("MAX_DURATION"))
if err != nil {
fmt.Fprintf(os.Stderr, "Error: invalid value %#v for env var MAX_DURATION: %s\n\n", os.Getenv("MAX_DURATION"), err)
flag.Usage()
os.Exit(1)
}
}
if host == defaultHost && os.Getenv("HOST") != "" {
host = os.Getenv("HOST")
}
if port == defaultPort && os.Getenv("PORT") != "" {
port, err = strconv.Atoi(os.Getenv("PORT"))
if err != nil {
fmt.Fprintf(os.Stderr, "Error: invalid value %#v for env var PORT: %s\n\n", os.Getenv("PORT"), err)
flag.Usage()
os.Exit(1)
}
}
if httpsCertFile == "" && os.Getenv("HTTPS_CERT_FILE") != "" {
httpsCertFile = os.Getenv("HTTPS_CERT_FILE")
}
if httpsKeyFile == "" && os.Getenv("HTTPS_KEY_FILE") != "" {
httpsKeyFile = os.Getenv("HTTPS_KEY_FILE")
}
var serveTLS bool
if httpsCertFile != "" || httpsKeyFile != "" {
serveTLS = true
if httpsCertFile == "" || httpsKeyFile == "" {
fmt.Fprintf(os.Stderr, "Error: https cert and key must both be provided\n\n")
flag.Usage()
os.Exit(1)
}
}
logger := log.New(os.Stderr, "", 0)
// A hacky log helper function to ensure that shutdown messages are
// formatted the same as other messages. See StdLogObserver in
// httpbin/middleware.go for the format we're matching here.
serverLog := func(msg string, args ...interface{}) {
const (
logFmt = "time=%q msg=%q"
dateFmt = "2006-01-02T15:04:05.9999"
)
logger.Printf(logFmt, time.Now().Format(dateFmt), fmt.Sprintf(msg, args...))
}
h := httpbin.New(
httpbin.WithMaxBodySize(maxBodySize),
httpbin.WithMaxDuration(maxDuration),
httpbin.WithObserver(httpbin.StdLogObserver(logger)),
)
listenAddr := net.JoinHostPort(host, strconv.Itoa(port))
server := &http.Server{
Addr: listenAddr,
Handler: h.Handler(),
}
// shutdownCh triggers graceful shutdown on SIGINT or SIGTERM
shutdownCh := make(chan os.Signal, 1)
signal.Notify(shutdownCh, syscall.SIGINT, syscall.SIGTERM)
// exitCh will be closed when it is safe to exit, after graceful shutdown
exitCh := make(chan struct{})
go func() {
sig := <-shutdownCh
serverLog("shutdown started by signal: %s", sig)
shutdownTimeout := maxDuration + 1*time.Second
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
server.SetKeepAlivesEnabled(false)
if err := server.Shutdown(ctx); err != nil {
serverLog("shutdown error: %s", err)
}
close(exitCh)
}()
var listenErr error
if serveTLS {
serverLog("go-httpbin listening on https://%s", listenAddr)
listenErr = server.ListenAndServeTLS(httpsCertFile, httpsKeyFile)
} else {
serverLog("go-httpbin listening on http://%s", listenAddr)
listenErr = server.ListenAndServe()
}
if listenErr != nil && listenErr != http.ErrServerClosed {
logger.Fatalf("failed to listen: %s", listenErr)
}
<-exitCh
serverLog("shutdown finished")
}

@ -0,0 +1,21 @@
app = "httpbingo"
[[services]]
internal_port = 8080
protocol = "tcp"
[services.concurrency]
hard_limit = 25
soft_limit = 20
[[services.ports]]
handlers = ["http"]
port = "80"
[[services.ports]]
handlers = ["tls", "http"]
port = "443"
[[services.tcp_checks]]
interval = 10000
timeout = 2000

@ -0,0 +1,3 @@
module github.com/mccutchen/go-httpbin
go 1.12

File diff suppressed because one or more lines are too long

@ -0,0 +1,221 @@
// Package digest provides a limited implementation of HTTP Digest
// Authentication, as defined in RFC 2617.
//
// Only the "auth" QOP directive is handled at this time, and while support for
// the SHA-256 algorithm is implemented here it does not actually work in
// either Chrome or Firefox.
//
// For more info, see:
// https://tools.ietf.org/html/rfc2617
// https://en.wikipedia.org/wiki/Digest_access_authentication
package digest
import (
"crypto/md5"
"crypto/sha256"
"crypto/subtle"
"fmt"
"math/rand"
"net/http"
"strings"
"time"
)
// digestAlgorithm is an algorithm used to hash digest payloads
type digestAlgorithm int
// Digest algorithms supported by this package
const (
MD5 digestAlgorithm = iota
SHA256
)
func (a digestAlgorithm) String() string {
switch a {
case MD5:
return "MD5"
case SHA256:
return "SHA-256"
}
return "UNKNOWN"
}
// Check returns a bool indicating whether the request is correctly
// authenticated for the given username and password.
func Check(req *http.Request, username, password string) bool {
auth := parseAuthorizationHeader(req.Header.Get("Authorization"))
if auth == nil || auth.username != username {
return false
}
expectedResponse := response(auth, password, req.Method, req.RequestURI)
return compare(auth.response, expectedResponse)
}
// Challenge returns a WWW-Authenticate header value for the given realm and
// algorithm. If an invalid realm or an unsupported algorithm is given
func Challenge(realm string, algorithm digestAlgorithm) string {
entropy := make([]byte, 32)
rand.Read(entropy)
opaqueVal := entropy[:16]
nonceVal := fmt.Sprintf("%s:%x", time.Now(), entropy[16:31])
// we use MD5 to hash nonces regardless of hash used for authentication
opaque := hash(opaqueVal, MD5)
nonce := hash([]byte(nonceVal), MD5)
return fmt.Sprintf("Digest qop=auth, realm=%#v, algorithm=%s, nonce=%s, opaque=%s", sanitizeRealm(realm), algorithm, nonce, opaque)
}
// sanitizeRealm tries to ensure that a given realm does not include any
// characters that will trip up our extremely simplistic header parser.
func sanitizeRealm(realm string) string {
realm = strings.Replace(realm, `"`, "", -1)
realm = strings.Replace(realm, ",", "", -1)
return realm
}
// authorization is the result of parsing an Authorization header
type authorization struct {
algorithm digestAlgorithm
cnonce string
nc string
nonce string
opaque string
qop string
realm string
response string
uri string
username string
}
// parseAuthorizationHeader parses an Authorization header into an
// Authorization struct, given a an authorization header like:
//
// Authorization: Digest username="Mufasa",
// realm="testrealm@host.com",
// nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
// uri="/dir/index.html",
// qop=auth,
// nc=00000001,
// cnonce="0a4f113b",
// response="6629fae49393a05397450978507c4ef1",
// opaque="5ccc069c403ebaf9f0171e9517f40e41"
//
// If the given value does not contain a Digest authorization header, or is in
// some other way malformed, nil is returned.
//
// Example from Wikipedia: https://en.wikipedia.org/wiki/Digest_access_authentication#Example_with_explanation
func parseAuthorizationHeader(value string) *authorization {
if value == "" {
return nil
}
parts := strings.SplitN(value, " ", 2)
if parts[0] != "Digest" || len(parts) != 2 {
return nil
}
authInfo := parts[1]
auth := parseDictHeader(authInfo)
algo := MD5
if strings.ToLower(auth["algorithm"]) == "sha-256" {
algo = SHA256
}
return &authorization{
algorithm: algo,
cnonce: auth["cnonce"],
nc: auth["nc"],
nonce: auth["nonce"],
opaque: auth["opaque"],
qop: auth["qop"],
realm: auth["realm"],
response: auth["response"],
uri: auth["uri"],
username: auth["username"],
}
}
// parseDictHeader is a simplistic, buggy, and incomplete implementation of
// parsing key-value pairs from a header value into a map.
func parseDictHeader(value string) map[string]string {
pairs := strings.Split(value, ",")
res := make(map[string]string, len(pairs))
for _, pair := range pairs {
parts := strings.SplitN(strings.TrimSpace(pair), "=", 2)
key := strings.TrimSpace(parts[0])
if len(key) == 0 {
continue
}
val := ""
if len(parts) > 1 {
val = strings.TrimSpace(parts[1])
if strings.HasPrefix(val, `"`) && strings.HasSuffix(val, `"`) {
val = val[1 : len(val)-1]
}
}
res[key] = val
}
return res
}
// hash generates the hex digest of the given data using the given hashing
// algorithm, which must be one of MD5 or SHA256.
func hash(data []byte, algorithm digestAlgorithm) string {
switch algorithm {
case SHA256:
return fmt.Sprintf("%x", sha256.Sum256(data))
default:
return fmt.Sprintf("%x", md5.Sum(data))
}
}
// makeHA1 returns the HA1 hash, where
//
// HA1 = H(A1) = H(username:realm:password)
//
// and H is one of MD5 or SHA256.
func makeHA1(realm, username, password string, algorithm digestAlgorithm) string {
A1 := fmt.Sprintf("%s:%s:%s", username, realm, password)
return hash([]byte(A1), algorithm)
}
// makeHA2 returns the HA2 hash, where
//
// HA2 = H(A2) = H(method:digestURI)
//
// and H is one of MD5 or SHA256.
func makeHA2(auth *authorization, method, uri string) string {
A2 := fmt.Sprintf("%s:%s", method, uri)
return hash([]byte(A2), auth.algorithm)
}
// Response calculates the correct digest auth response. If the qop directive's
// value is "auth" or "auth-int" , then compute the response as
//
// RESPONSE = H(HA1:nonce:nonceCount:clientNonce:qop:HA2)
//
// and if the qop directive is unspecified, then compute the response as
//
// RESPONSE = H(HA1:nonce:HA2)
//
// where H is one of MD5 or SHA256.
func response(auth *authorization, password, method, uri string) string {
ha1 := makeHA1(auth.realm, auth.username, password, auth.algorithm)
ha2 := makeHA2(auth, method, uri)
var r string
if auth.qop == "auth" || auth.qop == "auth-int" {
r = fmt.Sprintf("%s:%s:%s:%s:%s:%s", ha1, auth.nonce, auth.nc, auth.cnonce, auth.qop, ha2)
} else {
r = fmt.Sprintf("%s:%s:%s", ha1, auth.nonce, ha2)
}
return hash([]byte(r), auth.algorithm)
}
// compare is a constant-time string comparison
func compare(x, y string) bool {
return subtle.ConstantTimeCompare([]byte(x), []byte(y)) == 1
}

@ -0,0 +1,267 @@
package digest
import (
"fmt"
"net/http"
"reflect"
"testing"
)
// Well-formed examples from Wikipedia:
// https://en.wikipedia.org/wiki/Digest_access_authentication#Example_with_explanation
const (
exampleUsername = "Mufasa"
examplePassword = "Circle Of Life"
exampleAuthorization string = `Digest username="Mufasa",
realm="testrealm@host.com",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
uri="/dir/index.html",
qop=auth,
nc=00000001,
cnonce="0a4f113b",
response="6629fae49393a05397450978507c4ef1",
opaque="5ccc069c403ebaf9f0171e9517f40e41"`
)
func assertStringEquals(t *testing.T, expected, got string) {
if expected != got {
t.Errorf("Expected %#v, got %#v", expected, got)
}
}
func buildRequest(method, uri, authHeader string) *http.Request {
req, _ := http.NewRequest(method, uri, nil)
req.RequestURI = uri
if authHeader != "" {
req.Header.Set("Authorization", authHeader)
}
return req
}
func TestCheck(t *testing.T) {
t.Run("missing authorization", func(t *testing.T) {
req := buildRequest("GET", "/dir/index.html", "")
if Check(req, exampleUsername, examplePassword) != false {
t.Error("Missing Authorization header should fail")
}
})
t.Run("wrong username", func(t *testing.T) {
req := buildRequest("GET", "/dir/index.html", exampleAuthorization)
if Check(req, "Simba", examplePassword) != false {
t.Error("Incorrect username should fail")
}
})
t.Run("wrong password", func(t *testing.T) {
req := buildRequest("GET", "/dir/index.html", exampleAuthorization)
if Check(req, examplePassword, "foobar") != false {
t.Error("Incorrect password should fail")
}
})
t.Run("ok", func(t *testing.T) {
req := buildRequest("GET", "/dir/index.html", exampleAuthorization)
if Check(req, exampleUsername, examplePassword) != true {
t.Error("Correct credentials should pass")
}
})
}
func TestChallenge(t *testing.T) {
var tests = []struct {
realm string
expectedRealm string
algorithm digestAlgorithm
expectedAlgorithm string
}{
{"realm", "realm", MD5, "MD5"},
{"realm", "realm", SHA256, "SHA-256"},
{"realm with spaces", "realm with spaces", SHA256, "SHA-256"},
{`realm "with" "quotes"`, "realm with quotes", MD5, "MD5"},
{`spaces, "quotes," and commas`, "spaces quotes and commas", MD5, "MD5"},
}
for _, test := range tests {
challenge := Challenge(test.realm, test.algorithm)
result := parseDictHeader(challenge)
assertStringEquals(t, test.expectedRealm, result["realm"])
assertStringEquals(t, test.expectedAlgorithm, result["algorithm"])
}
}
func TestResponse(t *testing.T) {
auth := parseAuthorizationHeader(exampleAuthorization)
expected := auth.response
got := response(auth, examplePassword, "GET", "/dir/index.html")
assertStringEquals(t, expected, got)
}
func TestHash(t *testing.T) {
var tests = []struct {
algorithm digestAlgorithm
data []byte
expected string
}{
{SHA256, []byte("hello, world!\n"), "4dca0fd5f424a31b03ab807cbae77eb32bf2d089eed1cee154b3afed458de0dc"},
{MD5, []byte("hello, world!\n"), "910c8bc73110b0cd1bc5d2bcae782511"},
// Any unhandled hash results in MD5 being used
{digestAlgorithm(10), []byte("hello, world!\n"), "910c8bc73110b0cd1bc5d2bcae782511"},
}
for _, test := range tests {
t.Run(fmt.Sprintf("hash/%v", test.algorithm), func(t *testing.T) {
result := hash(test.data, test.algorithm)
assertStringEquals(t, test.expected, result)
})
}
}
func TestCompare(t *testing.T) {
if compare("foo", "bar") != false {
t.Error("Expected foo != bar")
}
if compare("foo", "foo") != true {
t.Error("Expected foo == foo")
}
}
func TestParseDictHeader(t *testing.T) {
var tests = []struct {
input string
expected map[string]string
}{
{"foo=bar", map[string]string{"foo": "bar"}},
// keys without values get the empty string
{"foo", map[string]string{"foo": ""}},
{"foo=bar, baz", map[string]string{"foo": "bar", "baz": ""}},
// no spaces required
{"foo=bar,baz=quux", map[string]string{"foo": "bar", "baz": "quux"}},
// spaces are stripped
{"foo=bar, baz=quux", map[string]string{"foo": "bar", "baz": "quux"}},
{"foo= bar, baz=quux", map[string]string{"foo": "bar", "baz": "quux"}},
{"foo=bar, baz = quux", map[string]string{"foo": "bar", "baz": "quux"}},
{" foo =bar, baz=quux", map[string]string{"foo": "bar", "baz": "quux"}},
{"foo=bar,baz = quux ", map[string]string{"foo": "bar", "baz": "quux"}},
// quotes around values are stripped
{`foo="bar two three four", baz=quux`, map[string]string{"foo": "bar two three four", "baz": "quux"}},
{`foo=bar, baz=""`, map[string]string{"foo": "bar", "baz": ""}},
// quotes around keys are not stripped
{`"foo"="bar", "baz two"=quux`, map[string]string{`"foo"`: "bar", `"baz two"`: "quux"}},
// spaces within quotes around values are preserved
{`foo=bar, baz=" quux "`, map[string]string{"foo": "bar", "baz": " quux "}},
// commas values are NOT handled correctly
{`foo="one, two, three", baz=quux`, map[string]string{"foo": `"one`, "two": "", `three"`: "", "baz": "quux"}},
{",,,", make(map[string]string)},
// trailing comma is okay
{"foo=bar,", map[string]string{"foo": "bar"}},
{"foo=bar, ", map[string]string{"foo": "bar"}},
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
results := parseDictHeader(test.input)
if !reflect.DeepEqual(test.expected, results) {
t.Errorf("expected %#v, got %#v", test.expected, results)
}
})
}
}
func TestParseAuthorizationHeader(t *testing.T) {
var tests = []struct {
input string
expected *authorization
}{
{"", nil},
{"Digest", nil},
{"Basic QWxhZGRpbjpPcGVuU2VzYW1l", nil},
// case sensitive on Digest
{"digest username=u, realm=r, nonce=n", nil},
// incomplete headers are fine
{"Digest username=u, realm=r, nonce=n", &authorization{
algorithm: MD5,
username: "u",
realm: "r",
nonce: "n",
}},
// algorithm can be either MD5 or SHA-256, with MD5 as default
{"Digest username=u", &authorization{
algorithm: MD5,
username: "u",
}},
{"Digest algorithm=MD5, username=u", &authorization{
algorithm: MD5,
username: "u",
}},
{"Digest algorithm=md5, username=u", &authorization{
algorithm: MD5,
username: "u",
}},
{"Digest algorithm=SHA-256, username=u", &authorization{
algorithm: SHA256,
username: "u",
}},
{"Digest algorithm=foo, username=u", &authorization{
algorithm: MD5,
username: "u",
}},
{"Digest algorithm=SHA-512, username=u", &authorization{
algorithm: MD5,
username: "u",
}},
// algorithm not case sensitive
{"Digest algorithm=sha-256, username=u", &authorization{
algorithm: SHA256,
username: "u",
}},
// but dash is required in SHA-256 is not recognized
{"Digest algorithm=SHA256, username=u", &authorization{
algorithm: MD5,
username: "u",
}},
// session variants not recognized
{"Digest algorithm=SHA-256-sess, username=u", &authorization{
algorithm: MD5,
username: "u",
}},
{"Digest algorithm=MD5-sess, username=u", &authorization{
algorithm: MD5,
username: "u",
}},
{exampleAuthorization, &authorization{
algorithm: MD5,
cnonce: "0a4f113b",
nc: "00000001",
nonce: "dcd98b7102dd2f0e8b11d0f600bfb0c093",
opaque: "5ccc069c403ebaf9f0171e9517f40e41",
qop: "auth",
realm: "testrealm@host.com",
response: "6629fae49393a05397450978507c4ef1",
uri: "/dir/index.html",
username: exampleUsername,
}},
}
for i, test := range tests {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
got := parseAuthorizationHeader(test.input)
if !reflect.DeepEqual(test.expected, got) {
t.Errorf("expected %#v, got %#v", test.expected, got)
}
})
}
}

@ -0,0 +1,983 @@
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)
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,306 @@
package httpbin
import (
"bytes"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"math/rand"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
)
// Base64MaxLen - Maximum input length for Base64 functions
const Base64MaxLen = 2000
// requestHeaders takes in incoming request and returns an http.Header map
// suitable for inclusion in our response data structures.
//
// This is necessary to ensure that the incoming Host header is included,
// because golang only exposes that header on the http.Request struct itself.
func getRequestHeaders(r *http.Request) http.Header {
h := r.Header
h.Set("Host", r.Host)
return h
}
func getOrigin(r *http.Request) string {
origin := r.Header.Get("X-Forwarded-For")
if origin == "" {
origin = r.RemoteAddr
}
return origin
}
func getURL(r *http.Request) *url.URL {
scheme := r.Header.Get("X-Forwarded-Proto")
if scheme == "" {
scheme = r.Header.Get("X-Forwarded-Protocol")
}
if scheme == "" && r.Header.Get("X-Forwarded-Ssl") == "on" {
scheme = "https"
}
if scheme == "" {
scheme = "http"
}
host := r.URL.Host
if host == "" {
host = r.Host
}
return &url.URL{
Scheme: scheme,
Opaque: r.URL.Opaque,
User: r.URL.User,
Host: host,
Path: r.URL.Path,
RawPath: r.URL.RawPath,
ForceQuery: r.URL.ForceQuery,
RawQuery: r.URL.RawQuery,
Fragment: r.URL.Fragment,
}
}
func writeResponse(w http.ResponseWriter, status int, contentType string, body []byte) {
w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body)))
w.WriteHeader(status)
w.Write(body)
}
func writeJSON(w http.ResponseWriter, body []byte, status int) {
writeResponse(w, status, jsonContentType, body)
}
func writeHTML(w http.ResponseWriter, body []byte, status int) {
writeResponse(w, status, htmlContentType, body)
}
// parseBody handles parsing a request body into our standard API response,
// taking care to only consume the request body once based on the Content-Type
// of the request. The given bodyResponse will be modified.
//
// Note: this function expects callers to limit the the maximum size of the
// request body. See, e.g., the limitRequestSize middleware.
func parseBody(w http.ResponseWriter, r *http.Request, resp *bodyResponse) error {
if r.Body == nil {
return nil
}
// Always set resp.Data to the incoming request body, in case we don't know
// how to handle the content type
body, err := ioutil.ReadAll(r.Body)
if err != nil {
r.Body.Close()
return err
}
resp.Data = string(body)
// After reading the body to populate resp.Data, we need to re-wrap it in
// an io.Reader for further processing below
r.Body.Close()
r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
ct := r.Header.Get("Content-Type")
switch {
case strings.HasPrefix(ct, "application/x-www-form-urlencoded"):
if err := r.ParseForm(); err != nil {
return err
}
resp.Form = r.PostForm
case strings.HasPrefix(ct, "multipart/form-data"):
// The memory limit here only restricts how many parts will be kept in
// memory before overflowing to disk:
// https://golang.org/pkg/net/http/#Request.ParseMultipartForm
if err := r.ParseMultipartForm(1024); err != nil {
return err
}
resp.Form = r.PostForm
case strings.HasPrefix(ct, "application/json"):
err := json.NewDecoder(r.Body).Decode(&resp.JSON)
if err != nil && err != io.EOF {
return err
}
}
return nil
}
// parseDuration takes a user's input as a string and attempts to convert it
// into a time.Duration. If not given as a go-style duration string, the input
// is assumed to be seconds as a float.
func parseDuration(input string) (time.Duration, error) {
d, err := time.ParseDuration(input)
if err != nil {
n, err := strconv.ParseFloat(input, 64)
if err != nil {
return 0, err
}
d = time.Duration(n*1000) * time.Millisecond
}
return d, nil
}
// parseBoundedDuration parses a time.Duration from user input and ensures that
// it is within a given maximum and minimum time
func parseBoundedDuration(input string, min, max time.Duration) (time.Duration, error) {
d, err := parseDuration(input)
if err != nil {
return 0, err
}
if d > max {
err = fmt.Errorf("duration %s longer than %s", d, max)
} else if d < min {
err = fmt.Errorf("duration %s shorter than %s", d, min)
}
return d, err
}
// syntheticByteStream implements the ReadSeeker interface to allow reading
// arbitrary subsets of bytes up to a maximum size given a function for
// generating the byte at a given offset.
type syntheticByteStream struct {
mu sync.Mutex
size int64
offset int64
factory func(int64) byte
}
// newSyntheticByteStream returns a new stream of bytes of a specific size,
// given a factory function for generating the byte at a given offset.
func newSyntheticByteStream(size int64, factory func(int64) byte) io.ReadSeeker {
return &syntheticByteStream{
size: size,
factory: factory,
}
}
// Read implements the Reader interface for syntheticByteStream
func (s *syntheticByteStream) Read(p []byte) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
start := s.offset
end := start + int64(len(p))
var err error
if end >= s.size {
err = io.EOF
end = s.size
}
for idx := start; idx < end; idx++ {
p[idx-start] = s.factory(idx)
}
s.offset = end
return int(end - start), err
}
// Seek implements the Seeker interface for syntheticByteStream
func (s *syntheticByteStream) Seek(offset int64, whence int) (int64, error) {
s.mu.Lock()
defer s.mu.Unlock()
switch whence {
case io.SeekStart:
s.offset = offset
case io.SeekCurrent:
s.offset += offset
case io.SeekEnd:
s.offset = s.size - offset
default:
return 0, errors.New("Seek: invalid whence")
}
if s.offset < 0 {
return 0, errors.New("Seek: invalid offset")
}
return s.offset, nil
}
func sha1hash(input string) string {
h := sha1.New()
return fmt.Sprintf("%x", h.Sum([]byte(input)))
}
func uuidv4() string {
buff := make([]byte, 16)
_, err := rand.Read(buff[:])
if err != nil {
panic(err)
}
buff[6] = (buff[6] & 0x0f) | 0x40 // Version 4
buff[8] = (buff[8] & 0x3f) | 0x80 // Variant 10
return fmt.Sprintf("%x-%x-%x-%x-%x", buff[0:4], buff[4:6], buff[6:8], buff[8:10], buff[10:])
}
// base64Helper - describes the base64 operation (encode|decode) and input data
type base64Helper struct {
operation string
data string
}
// newbase64Helper - create a new base64Helper struct
// Supports the following URL paths
// - /base64/input_str
// - /base64/encode/input_str
// - /base64/decode/input_str
func newBase64Helper(path string) (*base64Helper, error) {
parts := strings.Split(path, "/")
if len(parts) != 3 && len(parts) != 4 {
return nil, errors.New("invalid URL")
}
var b base64Helper
// Validation for - /base64/input_str
if len(parts) == 3 {
b.operation = "decode"
b.data = parts[2]
} else {
// Validation for
// - /base64/encode/input_str
// - /base64/encode/input_str
b.operation = parts[2]
if b.operation != "encode" && b.operation != "decode" {
return nil, fmt.Errorf("invalid operation: %s", b.operation)
}
b.data = parts[3]
}
if len(b.data) == 0 {
return nil, errors.New("no input data")
}
if len(b.data) >= Base64MaxLen {
return nil, fmt.Errorf("input length - %d, Cannot handle input >= %d", len(b.data), Base64MaxLen)
}
return &b, nil
}
// Encode - encode data as base64
func (b *base64Helper) Encode() ([]byte, error) {
buff := make([]byte, base64.StdEncoding.EncodedLen(len(b.data)))
base64.StdEncoding.Encode(buff, []byte(b.data))
return buff, nil
}
// Decode - decode data from base64
func (b *base64Helper) Decode() ([]byte, error) {
buff := make([]byte, base64.StdEncoding.DecodedLen(len(b.data)))
_, err := base64.StdEncoding.Decode(buff, []byte(b.data))
return buff, err
}

@ -0,0 +1,156 @@
package httpbin
import (
"fmt"
"io"
"reflect"
"testing"
"time"
)
func assertNil(t *testing.T, v interface{}) {
if v != nil {
t.Errorf("expected nil, got %#v", v)
}
}
func assertIntEqual(t *testing.T, a, b int) {
if a != b {
t.Errorf("expected %v == %v", a, b)
}
}
func assertBytesEqual(t *testing.T, a, b []byte) {
if !reflect.DeepEqual(a, b) {
t.Errorf("expected %v == %v", a, b)
}
}
func assertError(t *testing.T, got, expected error) {
if got != expected {
t.Errorf("expected error %v, got %v", expected, got)
}
}
func TestParseDuration(t *testing.T) {
var okTests = []struct {
input string
expected time.Duration
}{
// go-style durations
{"1s", time.Second},
{"500ms", 500 * time.Millisecond},
{"1.5h", 90 * time.Minute},
{"-10m", -10 * time.Minute},
// or floating point seconds
{"1", time.Second},
{"0.25", 250 * time.Millisecond},
{"-25", -25 * time.Second},
{"-2.5", -2500 * time.Millisecond},
}
for _, test := range okTests {
t.Run(fmt.Sprintf("ok/%s", test.input), func(t *testing.T) {
result, err := parseDuration(test.input)
if err != nil {
t.Fatalf("unexpected error parsing duration %v: %s", test.input, err)
}
if result != test.expected {
t.Fatalf("expected %s, got %s", test.expected, result)
}
})
}
var badTests = []struct {
input string
}{
{"foo"},
{"100foo"},
{"1/1"},
{"1.5.foo"},
{"0xFF"},
}
for _, test := range badTests {
t.Run(fmt.Sprintf("bad/%s", test.input), func(t *testing.T) {
_, err := parseDuration(test.input)
if err == nil {
t.Fatalf("expected error parsing %v", test.input)
}
})
}
}
func TestSyntheticByteStream(t *testing.T) {
factory := func(offset int64) byte {
return byte(offset)
}
t.Run("read", func(t *testing.T) {
s := newSyntheticByteStream(10, factory)
// read first half
p := make([]byte, 5)
count, err := s.Read(p)
assertNil(t, err)
assertIntEqual(t, count, 5)
assertBytesEqual(t, p, []byte{0, 1, 2, 3, 4})
// read second half
p = make([]byte, 5)
count, err = s.Read(p)
assertError(t, err, io.EOF)
assertIntEqual(t, count, 5)
assertBytesEqual(t, p, []byte{5, 6, 7, 8, 9})
// can't read any more
p = make([]byte, 5)
count, err = s.Read(p)
assertError(t, err, io.EOF)
assertIntEqual(t, count, 0)
assertBytesEqual(t, p, []byte{0, 0, 0, 0, 0})
})
t.Run("read into too-large buffer", func(t *testing.T) {
s := newSyntheticByteStream(5, factory)
p := make([]byte, 10)
count, err := s.Read(p)
assertError(t, err, io.EOF)
assertIntEqual(t, count, 5)
assertBytesEqual(t, p, []byte{0, 1, 2, 3, 4, 0, 0, 0, 0, 0})
})
t.Run("seek", func(t *testing.T) {
s := newSyntheticByteStream(100, factory)
p := make([]byte, 5)
s.Seek(10, io.SeekStart)
count, err := s.Read(p)
assertNil(t, err)
assertIntEqual(t, count, 5)
assertBytesEqual(t, p, []byte{10, 11, 12, 13, 14})
s.Seek(10, io.SeekCurrent)
count, err = s.Read(p)
assertNil(t, err)
assertIntEqual(t, count, 5)
assertBytesEqual(t, p, []byte{25, 26, 27, 28, 29})
s.Seek(10, io.SeekEnd)
count, err = s.Read(p)
assertNil(t, err)
assertIntEqual(t, count, 5)
assertBytesEqual(t, p, []byte{90, 91, 92, 93, 94})
// invalid whence
_, err = s.Seek(10, 666)
if err.Error() != "Seek: invalid whence" {
t.Errorf("Expected \"Seek: invalid whence\", got %#v", err.Error())
}
// invalid offset
_, err = s.Seek(-10, io.SeekStart)
if err.Error() != "Seek: invalid offset" {
t.Errorf("Expected \"Seek: invalid offset\", got %#v", err.Error())
}
})
}

@ -0,0 +1,258 @@
package httpbin
import (
"net/http"
"net/url"
"time"
)
// Default configuration values
const (
DefaultMaxBodySize int64 = 1024 * 1024
DefaultMaxDuration = 10 * time.Second
)
const jsonContentType = "application/json; encoding=utf-8"
const htmlContentType = "text/html; charset=utf-8"
type headersResponse struct {
Headers http.Header `json:"headers"`
}
type ipResponse struct {
Origin string `json:"origin"`
}
type userAgentResponse struct {
UserAgent string `json:"user-agent"`
}
type getResponse struct {
Args url.Values `json:"args"`
Headers http.Header `json:"headers"`
Origin string `json:"origin"`
URL string `json:"url"`
}
// A generic response for any incoming request that might contain a body
type bodyResponse struct {
Args url.Values `json:"args"`
Headers http.Header `json:"headers"`
Origin string `json:"origin"`
URL string `json:"url"`
Data string `json:"data"`
Files map[string][]string `json:"files"`
Form map[string][]string `json:"form"`
JSON interface{} `json:"json"`
}
type cookiesResponse map[string]string
type authResponse struct {
Authorized bool `json:"authorized"`
User string `json:"user"`
}
type gzipResponse struct {
Headers http.Header `json:"headers"`
Origin string `json:"origin"`
Gzipped bool `json:"gzipped"`
}
type deflateResponse struct {
Headers http.Header `json:"headers"`
Origin string `json:"origin"`
Deflated bool `json:"deflated"`
}
// An actual stream response body will be made up of one or more of these
// structs, encoded as JSON and separated by newlines
type streamResponse struct {
ID int `json:"id"`
Args url.Values `json:"args"`
Headers http.Header `json:"headers"`
Origin string `json:"origin"`
URL string `json:"url"`
}
type uuidResponse struct {
UUID string `json:"uuid"`
}
type bearerResponse struct {
Authenticated bool `json:"authenticated"`
Token string `json:"token"`
}
// HTTPBin contains the business logic
type HTTPBin struct {
// Max size of an incoming request generated response body, in bytes
MaxBodySize int64
// Max duration of a request, for those requests that allow user control
// over timing (e.g. /delay)
MaxDuration time.Duration
// Observer called with the result of each handled request
Observer Observer
// Default parameter values
DefaultParams DefaultParams
}
// DefaultParams defines default parameter values
type DefaultParams struct {
DripDuration time.Duration
DripDelay time.Duration
DripNumBytes int64
}
// DefaultDefaultParams defines the DefaultParams that are used by default. In
// general, these should match the original httpbin.org's defaults.
var DefaultDefaultParams = DefaultParams{
DripDuration: 2 * time.Second,
DripDelay: 2 * time.Second,
DripNumBytes: 10,
}
// Handler returns an http.Handler that exposes all HTTPBin endpoints
func (h *HTTPBin) Handler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/", methods(h.Index, "GET"))
mux.HandleFunc("/forms/post", methods(h.FormsPost, "GET"))
mux.HandleFunc("/encoding/utf8", methods(h.UTF8, "GET"))
mux.HandleFunc("/get", methods(h.Get, "GET"))
mux.HandleFunc("/post", methods(h.RequestWithBody, "POST"))
mux.HandleFunc("/put", methods(h.RequestWithBody, "PUT"))
mux.HandleFunc("/patch", methods(h.RequestWithBody, "PATCH"))
mux.HandleFunc("/delete", methods(h.RequestWithBody, "DELETE"))
mux.HandleFunc("/ip", h.IP)
mux.HandleFunc("/user-agent", h.UserAgent)
mux.HandleFunc("/headers", h.Headers)
mux.HandleFunc("/response-headers", h.ResponseHeaders)
mux.HandleFunc("/status/", h.Status)
mux.HandleFunc("/redirect/", h.Redirect)
mux.HandleFunc("/relative-redirect/", h.RelativeRedirect)
mux.HandleFunc("/absolute-redirect/", h.AbsoluteRedirect)
mux.HandleFunc("/redirect-to", h.RedirectTo)
mux.HandleFunc("/cookies", h.Cookies)
mux.HandleFunc("/cookies/set", h.SetCookies)
mux.HandleFunc("/cookies/delete", h.DeleteCookies)
mux.HandleFunc("/basic-auth/", h.BasicAuth)
mux.HandleFunc("/hidden-basic-auth/", h.HiddenBasicAuth)
mux.HandleFunc("/digest-auth/", h.DigestAuth)
mux.HandleFunc("/bearer", h.Bearer)
mux.HandleFunc("/deflate", h.Deflate)
mux.HandleFunc("/gzip", h.Gzip)
mux.HandleFunc("/stream/", h.Stream)
mux.HandleFunc("/delay/", h.Delay)
mux.HandleFunc("/drip", h.Drip)
mux.HandleFunc("/range/", h.Range)
mux.HandleFunc("/bytes/", h.Bytes)
mux.HandleFunc("/stream-bytes/", h.StreamBytes)
mux.HandleFunc("/html", h.HTML)
mux.HandleFunc("/robots.txt", h.Robots)
mux.HandleFunc("/deny", h.Deny)
mux.HandleFunc("/cache", h.Cache)
mux.HandleFunc("/cache/", h.CacheControl)
mux.HandleFunc("/etag/", h.ETag)
mux.HandleFunc("/links/", h.Links)
mux.HandleFunc("/image", h.ImageAccept)
mux.HandleFunc("/image/", h.Image)
mux.HandleFunc("/xml", h.XML)
mux.HandleFunc("/json", h.JSON)
mux.HandleFunc("/uuid", h.UUID)
mux.HandleFunc("/base64/", h.Base64)
// existing httpbin endpoints that we do not support
mux.HandleFunc("/brotli", notImplementedHandler)
// Make sure our ServeMux doesn't "helpfully" redirect these invalid
// endpoints by adding a trailing slash. See the ServeMux docs for more
// info: https://golang.org/pkg/net/http/#ServeMux
mux.HandleFunc("/absolute-redirect", http.NotFound)
mux.HandleFunc("/basic-auth", http.NotFound)
mux.HandleFunc("/delay", http.NotFound)
mux.HandleFunc("/digest-auth", http.NotFound)
mux.HandleFunc("/hidden-basic-auth", http.NotFound)
mux.HandleFunc("/redirect", http.NotFound)
mux.HandleFunc("/relative-redirect", http.NotFound)
mux.HandleFunc("/status", http.NotFound)
mux.HandleFunc("/stream", http.NotFound)
mux.HandleFunc("/bytes", http.NotFound)
mux.HandleFunc("/stream-bytes", http.NotFound)
mux.HandleFunc("/links", http.NotFound)
// Apply global middleware
var handler http.Handler
handler = mux
handler = limitRequestSize(h.MaxBodySize, handler)
handler = preflight(handler)
handler = autohead(handler)
if h.Observer != nil {
handler = observe(h.Observer, handler)
}
return handler
}
// New creates a new HTTPBin instance
func New(opts ...OptionFunc) *HTTPBin {
h := &HTTPBin{
MaxBodySize: DefaultMaxBodySize,
MaxDuration: DefaultMaxDuration,
DefaultParams: DefaultDefaultParams,
}
for _, opt := range opts {
opt(h)
}
return h
}
// OptionFunc uses the "functional options" pattern to customize an HTTPBin
// instance
type OptionFunc func(*HTTPBin)
// WithDefaultParams sets the default params handlers will use
func WithDefaultParams(defaultParams DefaultParams) OptionFunc {
return func(h *HTTPBin) {
h.DefaultParams = defaultParams
}
}
// WithMaxBodySize sets the maximum amount of memory
func WithMaxBodySize(m int64) OptionFunc {
return func(h *HTTPBin) {
h.MaxBodySize = m
}
}
// WithMaxDuration sets the maximum amount of time httpbin may take to respond
func WithMaxDuration(d time.Duration) OptionFunc {
return func(h *HTTPBin) {
h.MaxDuration = d
}
}
// WithObserver sets the request observer callback
func WithObserver(o Observer) OptionFunc {
return func(h *HTTPBin) {
h.Observer = o
}
}

@ -0,0 +1,66 @@
package httpbin
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestNew(t *testing.T) {
h := New()
if h.MaxBodySize != DefaultMaxBodySize {
t.Fatalf("expected default MaxBodySize == %d, got %#v", DefaultMaxBodySize, h.MaxBodySize)
}
if h.MaxDuration != DefaultMaxDuration {
t.Fatalf("expected default MaxDuration == %s, got %#v", DefaultMaxDuration, h.MaxDuration)
}
if h.Observer != nil {
t.Fatalf("expected default Observer == nil, got %#v", h.Observer)
}
}
func TestNewOptions(t *testing.T) {
maxDuration := 1 * time.Second
maxBodySize := int64(1024)
observer := func(_ Result) {}
h := New(
WithMaxBodySize(maxBodySize),
WithMaxDuration(maxDuration),
WithObserver(observer),
)
if h.MaxBodySize != maxBodySize {
t.Fatalf("expected MaxBodySize == %d, got %#v", maxBodySize, h.MaxBodySize)
}
if h.MaxDuration != maxDuration {
t.Fatalf("expected MaxDuration == %s, got %#v", maxDuration, h.MaxDuration)
}
if h.Observer == nil {
t.Fatalf("expected non-nil Observer")
}
}
func TestNewObserver(t *testing.T) {
expectedStatus := http.StatusTeapot
observed := false
observer := func(r Result) {
observed = true
if r.Status != expectedStatus {
t.Fatalf("expected result status = %d, got %d", expectedStatus, r.Status)
}
}
h := New(WithObserver(observer))
r, _ := http.NewRequest("GET", fmt.Sprintf("/status/%d", expectedStatus), nil)
w := httptest.NewRecorder()
h.Handler().ServeHTTP(w, r)
if observed == false {
t.Fatalf("observer never called")
}
}

@ -0,0 +1,166 @@
package httpbin
import (
"fmt"
"log"
"net/http"
"time"
)
func preflight(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin == "" {
origin = "*"
}
respHeader := w.Header()
respHeader.Set("Access-Control-Allow-Origin", origin)
respHeader.Set("Access-Control-Allow-Credentials", "true")
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, HEAD, PUT, DELETE, PATCH, OPTIONS")
w.Header().Set("Access-Control-Max-Age", "3600")
if r.Header.Get("Access-Control-Request-Headers") != "" {
w.Header().Set("Access-Control-Allow-Headers", r.Header.Get("Access-Control-Request-Headers"))
}
w.WriteHeader(200)
return
}
h.ServeHTTP(w, r)
})
}
func methods(h http.HandlerFunc, methods ...string) http.HandlerFunc {
methodMap := make(map[string]struct{}, len(methods))
for _, m := range methods {
methodMap[m] = struct{}{}
// GET implies support for HEAD
if m == "GET" {
methodMap["HEAD"] = struct{}{}
}
}
return func(w http.ResponseWriter, r *http.Request) {
if _, ok := methodMap[r.Method]; !ok {
http.Error(w, fmt.Sprintf("method %s not allowed", r.Method), http.StatusMethodNotAllowed)
return
}
h.ServeHTTP(w, r)
}
}
func limitRequestSize(maxSize int64, h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Body != nil {
r.Body = http.MaxBytesReader(w, r.Body, maxSize)
}
h.ServeHTTP(w, r)
})
}
// headResponseWriter implements http.ResponseWriter in order to discard the
// body of the response
type headResponseWriter struct {
*metaResponseWriter
}
func (hw *headResponseWriter) Write(b []byte) (int, error) {
return 0, nil
}
// autohead automatically discards the body of responses to HEAD requests
func autohead(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "HEAD" {
w = &headResponseWriter{&metaResponseWriter{w: w}}
}
h.ServeHTTP(w, r)
})
}
// metaResponseWriter implements http.ResponseWriter and http.Flusher in order
// to record a response's status code and body size for logging purposes.
type metaResponseWriter struct {
w http.ResponseWriter
status int
size int64
}
func (mw *metaResponseWriter) Write(b []byte) (int, error) {
size, err := mw.w.Write(b)
mw.size += int64(size)
return size, err
}
func (mw *metaResponseWriter) WriteHeader(s int) {
mw.w.WriteHeader(s)
mw.status = s
}
func (mw *metaResponseWriter) Flush() {
f := mw.w.(http.Flusher)
f.Flush()
}
func (mw *metaResponseWriter) Header() http.Header {
return mw.w.Header()
}
func (mw *metaResponseWriter) Status() int {
if mw.status == 0 {
return http.StatusOK
}
return mw.status
}
func (mw *metaResponseWriter) Size() int64 {
return mw.size
}
func observe(o Observer, h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mw := &metaResponseWriter{w: w}
t := time.Now()
h.ServeHTTP(mw, r)
o(Result{
Status: mw.Status(),
Method: r.Method,
URI: r.URL.RequestURI(),
Size: mw.Size(),
Duration: time.Since(t),
})
})
}
// Result is the result of handling a request, used for instrumentation
type Result struct {
Status int
Method string
URI string
Size int64
Duration time.Duration
}
// Observer is a function that will be called with the details of a handled
// request, which can be used for logging, instrumentation, etc
type Observer func(result Result)
// StdLogObserver creates an Observer that will log each request in structured
// format using the given stdlib logger
func StdLogObserver(l *log.Logger) Observer {
const (
logFmt = "time=%q status=%d method=%q uri=%q size_bytes=%d duration_ms=%0.02f"
dateFmt = "2006-01-02T15:04:05.9999"
)
return func(result Result) {
l.Printf(
logFmt,
time.Now().Format(dateFmt),
result.Status,
result.Method,
result.URI,
result.Size,
result.Duration.Seconds()*1e3, // https://github.com/golang/go/issues/5491#issuecomment-66079585
)
}
}

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<!-- Example form from HTML5 spec http://www.w3.org/TR/html5/forms.html#writing-a-form's-user-interface -->
<form method="post" action="/post">
<p><label>Customer name: <input name="custname"></label></p>
<p><label>Telephone: <input type=tel name="custtel"></label></p>
<p><label>E-mail address: <input type=email name="custemail"></label></p>
<fieldset>
<legend> Pizza Size </legend>
<p><label> <input type=radio name=size value="small"> Small </label></p>
<p><label> <input type=radio name=size value="medium"> Medium </label></p>
<p><label> <input type=radio name=size value="large"> Large </label></p>
</fieldset>
<fieldset>
<legend> Pizza Toppings </legend>
<p><label> <input type=checkbox name="topping" value="bacon"> Bacon </label></p>
<p><label> <input type=checkbox name="topping" value="cheese"> Extra Cheese </label></p>
<p><label> <input type=checkbox name="topping" value="onion"> Onion </label></p>
<p><label> <input type=checkbox name="topping" value="mushroom"> Mushroom </label></p>
</fieldset>
<p><label>Preferred delivery time: <input type=time min="11:00" max="21:00" step="900" name="delivery"></label></p>
<p><label>Delivery instructions: <textarea name="comments"></textarea></label></p>
<p><button>Submit order</button></p>
</form>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

@ -0,0 +1,259 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%" viewBox="0 0 100 100">
<title>SVG Logo</title>
<a xlink:href="http://www.w3.org/Graphics/SVG/" target="_parent"
xlink:title="W3C SVG Working Group home page">
<rect
id="background"
fill="#FF9900"
width="100"
height="100"
rx="4"
ry="4"/>
<rect
id="top-left"
fill="#FFB13B"
width="50"
height="50"
rx="4"
ry="4"/>
<rect
id="bottom-right"
x="50"
y="50"
fill="#DE8500"
width="50"
height="50"
rx="4"
ry="4"/>
<g id="circles" fill="#FF9900">
<circle
id="n"
cx="50"
cy="18.4"
r="18.4"/>
<circle
id="ne"
cx="72.4"
cy="27.6"
r="18.4"/>
<circle
id="e"
cx="81.6"
cy="50"
r="18.4"/>
<circle
id="se"
cx="72.4"
cy="72.4"
r="18.4"/>
<circle
id="s"
cx="50"
cy="81.6"
r="18.4"/>
<circle
id="sw"
cx="27.6"
cy="72.4"
r="18.4"/>
<circle
id="w"
cx="18.4"
cy="50"
r="18.4"/>
<circle
id="nw"
cx="27.6"
cy="27.6"
r="18.4"/>
</g>
<g id="stars">
<path
id="black-star"
d="M 63.086, 18.385
c 0.000, -7.227 -5.859,-13.086 -13.100,-13.086
c -7.235, 0.000 -13.096, 5.859 -13.096, 13.086
c -5.100, -5.110 -13.395, -5.110 -18.497, 0.000
c -5.119, 5.120 -5.119, 13.408 0.000, 18.524
c -7.234, 0.000 -13.103, 5.859 -13.103, 13.085
c 0.000, 7.230 5.870, 13.098 13.103, 13.098
c -5.119, 5.110 -5.119, 13.395 0.000, 18.515
c 5.102, 5.104 13.397, 5.104 18.497, 0.000
c 0.000, 7.228 5.860, 13.083 13.096, 13.083
c 7.240, 0.000 13.100, -5.855 13.100,-13.083
c 5.118, 5.104 13.416, 5.104 18.513, 0.000
c 5.101, -5.120 5.101,-13.410 0.000,-18.515
c 7.216, 0.000 13.081, -5.869 13.081,-13.098
c 0.000, -7.227 -5.865,-13.085 -13.081,-13.085
c 5.101, -5.119 5.101,-13.406 0.000,-18.524
C 76.502, 13.275 68.206, 13.275 63.086, 18.385 z"/>
<path
id="white-star"
fill="#FFFFFF"
d="M 55.003, 23.405
v 14.488
L 65.260, 27.640
c 0.000, -1.812 0.691,-3.618 2.066, -5.005
c 2.780, -2.771 7.275,-2.771 10.024, 0.000
c 2.771, 2.766 2.771, 7.255 0.000, 10.027
c -1.377, 1.375 -3.195, 2.072 -5.015, 2.072
L 62.101, 44.982
H 76.590
c 1.290, -1.280 3.054,-2.076 5.011, -2.076
c 3.900, 0.000 7.078, 3.179 7.078, 7.087
c 0.000, 3.906 -3.178, 7.088 -7.078, 7.088
c -1.957, 0.000 -3.721,-0.798 -5.011, -2.072
H 62.100
l 10.229, 10.244
c 1.824, 0.000 3.642, 0.694 5.015, 2.086
c 2.774, 2.759 2.774, 7.250 0.000, 10.010
c -2.750, 2.774 -7.239, 2.774 -10.025, 0.000
c -1.372, -1.372 -2.064,-3.192 -2.064, -5.003
L 55.000, 62.094
v 14.499
c 1.271, 1.276 2.084, 3.054 2.084, 5.013
c 0.000, 3.906 -3.177, 7.077 -7.098, 7.077
c -3.919, 0.000 -7.094,-3.167 -7.094, -7.077
c 0.000, -1.959 0.811,-3.732 2.081, -5.013
V 62.094
L 34.738, 72.346
c 0.000, 1.812 -0.705, 3.627 -2.084, 5.003
c -2.769, 2.772 -7.251, 2.772 -10.024, 0.000
c -2.775, -2.764 -2.775,-7.253 0.000,-10.012
c 1.377, -1.390 3.214,-2.086 5.012, -2.086
l 10.257,-10.242
H 23.414
c -1.289, 1.276 -3.072, 2.072 -5.015, 2.072
c -3.917, 0.000 -7.096,-3.180 -7.096, -7.088
s 3.177, -7.087 7.096,-7.087
c 1.940, 0.000 3.725, 0.796 5.015, 2.076
h 14.488
L 27.646, 34.736
c -1.797, 0.000 -3.632,-0.697 -5.012, -2.071
c -2.775, -2.772 -2.775,-7.260 0.000,-10.027
c 2.773, -2.771 7.256,-2.771 10.027, 0.000
c 1.375, 1.386 2.083, 3.195 2.083, 5.005
l 10.235, 10.252
V 23.407
c -1.270, -1.287 -2.082,-3.053 -2.082, -5.023
c 0.000, -3.908 3.175,-7.079 7.096, -7.079
c 3.919, 0.000 7.097, 3.168 7.097, 7.079
C 57.088, 20.356 56.274,22.119 55.003, 23.405 z"/>
</g>
<g id="svg-textbox">
<path
id="text-backdrop"
fill="black"
d="M 5.30,50.00
H 94.68
V 90.00
Q 94.68,95.00 89.68,95.00
H 10.30
Q 5.30,95.00 5.30,90.00 Z"/>
<path
id="shine"
fill="#3F3F3F"
d="M 14.657,54.211
h 71.394
c 2.908, 0.000 5.312, 2.385 5.312, 5.315
v 17.910
c -27.584,-3.403 -54.926,-8.125 -82.011,-7.683
V 59.526
C 9.353,56.596 11.743,54.211 14.657,54.211
L 14.657,54.211 z"/>
<g id="svg-text">
<title>SVG</title>
<path
id="S"
fill="#FFFFFF"
stroke="#000000"
stroke-width="0.5035"
d="M 18.312,72.927
c -2.103,-2.107 -3.407, -5.028 -3.407, -8.253
c 0.000,-6.445 5.223,-11.672 11.666,-11.672
c 6.446, 0.000 11.667, 5.225 11.667, 11.672
h -6.832
c 0.000,-2.674 -2.168, -4.837 -4.835, -4.837
c -2.663, 0.000 -4.838, 2.163 -4.838, 4.837
c 0.000, 1.338 0.549, 2.536 1.415, 3.420
l 0.000, 0.000
c 0.883, 0.874 2.101, 1.405 3.423, 1.405
v 0.012
c 3.232, 0.000 6.145, 1.309 8.243, 3.416
l 0.000, 0.000
c 2.118, 2.111 3.424, 5.034 3.424, 8.248
c 0.000, 6.454 -5.221, 11.680 -11.667, 11.680
c -6.442, 0.000 -11.666, -5.222 -11.666,-11.680
h 6.828
c 0.000, 2.679 2.175, 4.835 4.838, 4.835
c 2.667, 0.000 4.835, -2.156 4.835, -4.835
c 0.000,-1.329 -0.545, -2.527 -1.429, -3.407
l 0.000, 0.000
c -0.864,-0.880 -2.082, -1.418 -3.406, -1.418
l 0.000, 0.000
C 23.341,76.350 20.429, 75.036 18.312, 72.927
L 18.312,72.927
L 18.312,72.927 z"/>
<polygon
id="V"
fill="#FFFFFF"
stroke="#000000"
stroke-width="0.5035"
points="61.588,53.005
53.344,92.854
46.494,92.854
38.236,53.005
45.082,53.005
49.920,76.342
54.755,53.005"/>
<path
id="G"
fill="#FFFFFF"
stroke="#000000"
stroke-width="0.5035"
d="M 73.255,69.513
h 11.683
v 11.664
l 0.000, 0.000
c 0.000, 6.452 -5.226,11.678 -11.669, 11.678
c -6.441, 0.000 -11.666,-5.226 -11.666,-11.678
l 0.000, 0.000
V 64.676
h -0.017
C 61.586,58.229 66.827,53.000 73.253, 53.000
c 6.459, 0.000 11.683, 5.225 11.683, 11.676
h -6.849
c 0.000,-2.674 -2.152,-4.837 -4.834, -4.837
c -2.647, 0.000 -4.820, 2.163 -4.820, 4.837
v 16.501
l 0.000, 0.000
c 0.000, 2.675 2.173, 4.837 4.820, 4.837
c 2.682, 0.000 4.834,-2.162 4.834, -4.827
v -0.012
v -4.827
h -4.834
L 73.255,69.513
L 73.255,69.513 z"/>
</g>
</g>
</a>
</svg>

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

@ -0,0 +1,199 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='content-type' value='text/html;charset=utf8'>
<meta name='generator' value='Ronn/v0.7.3 (http://github.com/rtomayko/ronn/tree/0.7.3)'>
<title>go-httpbin(1): HTTP Client Testing Service</title>
<style type='text/css' media='all'>
/* style: man */
body#manpage {margin:0;background:#fff;}
.mp {max-width:100ex;padding:0 9ex 1ex 4ex}
.mp p,.mp pre,.mp ul,.mp ol,.mp dl {margin:0 0 20px 0}
.mp h2 {margin:10px 0 0 0}
.mp > p,.mp > pre,.mp > ul,.mp > ol,.mp > dl {margin-left:8ex}
.mp h3 {margin:0 0 0 4ex}
.mp dt {margin:0;clear:left}
.mp dt.flush {float:left;width:8ex}
.mp dd {margin:0 0 0 9ex}
.mp h1,.mp h2,.mp h3,.mp h4 {clear:left}
.mp pre {margin-bottom:20px}
.mp pre+h2,.mp pre+h3 {margin-top:22px}
.mp h2+pre,.mp h3+pre {margin-top:5px}
.mp img {display:block;margin:auto}
.mp h1.man-title {display:none}
.mp,.mp code,.mp pre,.mp tt,.mp kbd,.mp samp,.mp h3,.mp h4 {font-family:monospace;font-size:14px;line-height:1.42857142857143}
.mp h2 {font-size:16px;line-height:1.25}
.mp h1 {font-size:20px;line-height:2}
.mp,.mp code,.mp pre,.mp pre code,.mp tt,.mp kbd,.mp samp {color:#131211}
.mp h1,.mp h2,.mp h3,.mp h4 {color:#030201}
.mp u {text-decoration:underline}
.mp code,.mp strong,.mp b {font-weight:bold;color:#131211}
.mp em,.mp var {font-style:italic;color:#232221;text-decoration:none}
.mp a,.mp a:link,.mp a:hover,.mp a code,.mp a pre,.mp a tt,.mp a kbd,.mp a samp {color:#0000ff}
.mp b.man-ref {font-weight:normal;color:#434241}
.mp pre {padding:0 4ex}
.mp pre code {font-weight:normal;color:#434241}
.mp h2+pre,h3+pre {padding-left:0}
ol.man-decor,ol.man-decor li {margin:3px 0 10px 0;padding:0;float:left;width:33%;list-style-type:none;text-transform:uppercase;color:#999;letter-spacing:1px}
ol.man-decor {width:100%}
ol.man-decor li.tl {text-align:left}
ol.man-decor li.tc {text-align:center;letter-spacing:4px}
ol.man-decor li.tr {text-align:right;float:right}
</style>
<style type='text/css' media='all'>
/* style: 80c */
.mp {max-width:86ex}
ul {list-style: None; margin-left: 1em!important}
.man-navigation {left:101ex}
</style>
</head>
<body id='manpage'>
<div class='mp'>
<h1>go-httpbin(1)</h1>
<p>A golang port of the venerable <a href="https://httpbin.org/">httpbin.org</a> HTTP request &amp; response testing service.</p>
<h2 id="ENDPOINTS">ENDPOINTS</h2>
<ul>
<li><a href="/"><code>/</code></a> This page.</li>
<li><a href="/absolute-redirect/6"><code>/absolute-redirect/:n</code></a> 302 Absolute redirects <em>n</em> times.</li>
<li><a href="/base64/aHR0cGJpbmdvLm9yZw=="><code>/base64/:value</code></a> Decodes a Base64 encoded string.</li>
<li><a href="/base64/decode/aHR0cGJpbmdvLm9yZw=="><code>/base64/decode/:value</code></a> Explicit URL for decoding a Base64 encoded string.</li>
<li><a href="/base64/encode/httpbingo.org"><code>/base64/encode/:value</code></a> Encodes a string into Base64.</li>
<li><a href="/basic-auth/user/passwd"><code>/basic-auth/:user/:passwd</code></a> Challenges HTTPBasic Auth.</li>
<li><a href="/bearer"><code>/bearer</code></a> Checks Bearer token header - returns 401 if not set.</li>
<li><a href="/brotli"><code><del>/brotli</del></code></a> Returns brotli-encoded data.</del> <i>Not implemented!</i></li>
<li><a href="/bytes/1024"><code>/bytes/:n</code></a> Generates <em>n</em> random bytes of binary data, accepts optional <em>seed</em> integer parameter.</li>
<li><a href="/cache"><code>/cache</code></a> Returns 200 unless an If-Modified-Since or If-None-Match header is provided, when it returns a 304.</li>
<li><a href="/cache/60"><code>/cache/:n</code></a> Sets a Cache-Control header for <em>n</em> seconds.</li>
<li><a href="/cookies"><code>/cookies</code></a> Returns cookie data.</li>
<li><a href="/cookies/delete?k1=&amp;k2="><code>/cookies/delete?name</code></a> Deletes one or more simple cookies.</li>
<li><a href="/cookies/set?k1=v1&amp;k2=v2"><code>/cookies/set?name=value</code></a> Sets one or more simple cookies.</li>
<li><a href="/deflate"><code>/deflate</code></a> Returns deflate-encoded data.</li>
<li><a href="/delay/3"><code>/delay/:n</code></a> Delays responding for <em>min(n, 10)</em> seconds.</li>
<li><a href="/deny"><code>/deny</code></a> Denied by robots.txt file.</li>
<li><a href="/digest-auth/auth/user/passwd/MD5"><code>/digest-auth/:qop/:user/:passwd/:algorithm</code></a> Challenges HTTP Digest Auth.</li>
<li><a href="/digest-auth/auth/user/passwd/MD5"><code>/digest-auth/:qop/:user/:passwd</code></a> Challenges HTTP Digest Auth.</li>
<li><a href="/drip?code=200&amp;numbytes=5&amp;duration=5"><code>/drip?numbytes=n&amp;duration=s&amp;delay=s&amp;code=code</code></a> Drips data over a duration after an optional initial delay, then (optionally) returns with the given status code.</li>
<li><a href="/encoding/utf8"><code>/encoding/utf8</code></a> Returns page containing UTF-8 data.</li>
<li><a href="/etag/etag"><code>/etag/:etag</code></a> Assumes the resource has the given etag and responds to If-None-Match header with a 200 or 304 and If-Match with a 200 or 412 as appropriate.</li>
<li><a href="/forms/post"><code>/forms/post</code></a> HTML form that submits to <em>/post</em></li>
<li><a href="/get"><code>/get</code></a> Returns GET data.</li>
<li><a href="/gzip"><code>/gzip</code></a> Returns gzip-encoded data.</li>
<li><a href="/headers"><code>/headers</code></a> Returns header dict.</li>
<li><a href="/hidden-basic-auth/user/passwd"><code>/hidden-basic-auth/:user/:passwd</code></a> 404'd BasicAuth.</li>
<li><a href="/html"><code>/html</code></a> Renders an HTML Page.</li>
<li><a href="/image"><code>/image</code></a> Returns page containing an image based on sent Accept header.</li>
<li><a href="/image/jpeg"><code>/image/jpeg</code></a> Returns a JPEG image.</li>
<li><a href="/image/png"><code>/image/png</code></a> Returns a PNG image.</li>
<li><a href="/image/svg"><code>/image/svg</code></a> Returns a SVG image.</li>
<li><a href="/image/webp"><code>/image/webp</code></a> Returns a WEBP image.</li>
<li><a href="/ip"><code>/ip</code></a> Returns Origin IP.</li>
<li><a href="/json"><code>/json</code></a> Returns JSON.</li>
<li><a href="/links/10"><code>/links/:n</code></a> Returns page containing <em>n</em> HTML links.</li>
<li><a href="/range/1024"><code>/range/1024?duration=s&amp;chunk_size=code</code></a> Streams <em>n</em> bytes, and allows specifying a <em>Range</em> header to select a subset of the data. Accepts a <em>chunk_size</em> and request <em>duration</em> parameter.</li>
<li><a href="/redirect-to?status_code=307&amp;url=http%3A%2F%2Fexample.com%2F"><code>/redirect-to?url=foo&status_code=307</code></a> 307 Redirects to the <em>foo</em> URL.</li>
<li><a href="/redirect-to?url=http%3A%2F%2Fexample.com%2F"><code>/redirect-to?url=foo</code></a> 302 Redirects to the <em>foo</em> URL.</li>
<li><a href="/redirect/6"><code>/redirect/:n</code></a> 302 Redirects <em>n</em> times.</li>
<li><a href="/relative-redirect/6"><code>/relative-redirect/:n</code></a> 302 Relative redirects <em>n</em> times.</li>
<li><a href="/response-headers?Server=httpbin&amp;Content-Type=text%2Fplain%3B+charset%3DUTF-8"><code>/response-headers?key=val</code></a> Returns given response headers.</li>
<li><a href="/robots.txt"><code>/robots.txt</code></a> Returns some robots.txt rules.</li>
<li><a href="/status/418"><code>/status/:code</code></a> Returns given HTTP Status code.</li>
<li><a href="/stream-bytes/1024"><code>/stream-bytes/:n</code></a> Streams <em>n</em> random bytes of binary data, accepts optional <em>seed</em> and <em>chunk_size</em> integer parameters.</li>
<li><a href="/stream/20"><code>/stream/:n</code></a> Streams <em>min(n, 100)</em> lines.</li>
<li><a href="/user-agent"><code>/user-agent</code></a> Returns user-agent.</li>
<li><a href="/uuid"><code>/uuid</code></a> Generates a <a href="https://en.wikipedia.org/wiki/Universally_unique_identifier">UUIDv4</a> value.</li>
<li><a href="/xml"><code>/xml</code></a> Returns some XML</li>
</ul>
<h2 id="DESCRIPTION">DESCRIPTION</h2>
<p>Testing an HTTP Library can become difficult sometimes. <a href="http://requestb.in">RequestBin</a> is fantastic for testing POST requests, but doesn't let you control the response. This exists to cover all kinds of HTTP scenarios. Additional endpoints are being considered.</p>
<p>All endpoint responses are JSON-encoded.</p>
<h2 id="EXAMPLES">EXAMPLES</h2>
<h3 id="-curl-http-httpbin-org-ip">$ curl https://httpbingo.org/ip</h3>
<pre><code>{"origin": "24.127.96.129"}
</code></pre>
<h3 id="-curl-http-httpbin-org-user-agent">$ curl https://httpbingo.org/user-agent</h3>
<pre><code>{"user-agent": "curl/7.19.7 (universal-apple-darwin10.0) libcurl/7.19.7 OpenSSL/0.9.8l zlib/1.2.3"}
</code></pre>
<h3 id="-curl-http-httpbin-org-get">$ curl https://httpbingo.org/get</h3>
<pre><code>{
"args": {},
"headers": {
"Accept": "*/*",
"Connection": "close",
"Content-Length": "",
"Content-Type": "",
"Host": "httpbin.org",
"User-Agent": "curl/7.19.7 (universal-apple-darwin10.0) libcurl/7.19.7 OpenSSL/0.9.8l zlib/1.2.3"
},
"origin": "24.127.96.129",
"url": "https://httpbingo.org/get"
}
</code></pre>
<h3 id="-curl-I-http-httpbin-org-status-418">$ curl -I https://httpbingo.org/status/418</h3>
<pre><code>HTTP/1.1 418 I'M A TEAPOT
Server: nginx/0.7.67
Date: Mon, 13 Jun 2011 04:25:38 GMT
Connection: close
x-more-info: http://tools.ietf.org/html/rfc2324
Content-Length: 135
</code></pre>
<h3 id="-curl-https-httpbin-org-get-show_env-1">$ curl https://httpbingo.org/get?show_env=1</h3>
<pre><code>{
"headers": {
"Content-Length": "",
"Accept-Language": "en-US,en;q=0.8",
"Accept-Encoding": "gzip,deflate,sdch",
"X-Forwarded-Port": "443",
"X-Forwarded-For": "109.60.101.240",
"Host": "httpbin.org",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"User-Agent": "Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.83 Safari/535.11",
"X-Request-Start": "1350053933441",
"Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.3",
"Connection": "keep-alive",
"X-Forwarded-Proto": "https",
"Cookie": "_gauges_unique_day=1; _gauges_unique_month=1; _gauges_unique_year=1; _gauges_unique=1; _gauges_unique_hour=1",
"Content-Type": ""
},
"args": {
"show_env": "1"
},
"origin": "109.60.101.240",
"url": "https://httpbingo.org/get?show_env=1"
}
</code></pre>
<h2 id="AUTHOR">AUTHOR</h2>
<p>Ported to Go by <a href="https://github.com/mccutchen">Will McCutchen</a>.</p>
<p>From <a href="https://httpbin.org/">the original</a> <a href="http://kennethreitz.com/">Kenneth Reitz</a> project.</p>
<h2 id="SEE-ALSO">SEE ALSO</h2>
<p><a href="https://httpbin.org/">httpbin.org</a> &mdash; the original httpbin</p>
</div>
<a href="https://github.com/mccutchen/go-httpbin"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://camo.githubusercontent.com/38ef81f8aca64bb9a64448d0d70f1308ef5341ab/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f6461726b626c75655f3132313632312e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png"></a>
</body>
</html>

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<h1>Herman Melville - Moby-Dick</h1>
<div>
<p>
Availing himself of the mild, summer-cool weather that now reigned in these latitudes, and in preparation for the peculiarly active pursuits shortly to be anticipated, Perth, the begrimed, blistered old blacksmith, had not removed his portable forge to the hold again, after concluding his contributory work for Ahab's leg, but still retained it on deck, fast lashed to ringbolts by the foremast; being now almost incessantly invoked by the headsmen, and harpooneers, and bowsmen to do some little job for them; altering, or repairing, or new shaping their various weapons and boat furniture. Often he would be surrounded by an eager circle, all waiting to be served; holding boat-spades, pike-heads, harpoons, and lances, and jealously watching his every sooty movement, as he toiled. Nevertheless, this old man's was a patient hammer wielded by a patient arm. No murmur, no impatience, no petulance did come from him. Silent, slow, and solemn; bowing over still further his chronically broken back, he toiled away, as if toil were life itself, and the heavy beating of his hammer the heavy beating of his heart. And so it was.—Most miserable! A peculiar walk in this old man, a certain slight but painful appearing yawing in his gait, had at an early period of the voyage excited the curiosity of the mariners. And to the importunity of their persisted questionings he had finally given in; and so it came to pass that every one now knew the shameful story of his wretched fate. Belated, and not innocently, one bitter winter's midnight, on the road running between two country towns, the blacksmith half-stupidly felt the deadly numbness stealing over him, and sought refuge in a leaning, dilapidated barn. The issue was, the loss of the extremities of both feet. Out of this revelation, part by part, at last came out the four acts of the gladness, and the one long, and as yet uncatastrophied fifth act of the grief of his life's drama. He was an old man, who, at the age of nearly sixty, had postponedly encountered that thing in sorrow's technicals called ruin. He had been an artisan of famed excellence, and with plenty to do; owned a house and garden; embraced a youthful, daughter-like, loving wife, and three blithe, ruddy children; every Sunday went to a cheerful-looking church, planted in a grove. But one night, under cover of darkness, and further concealed in a most cunning disguisement, a desperate burglar slid into his happy home, and robbed them all of everything. And darker yet to tell, the blacksmith himself did ignorantly conduct this burglar into his family's heart. It was the Bottle Conjuror! Upon the opening of that fatal cork, forth flew the fiend, and shrivelled up his home. Now, for prudent, most wise, and economic reasons, the blacksmith's shop was in the basement of his dwelling, but with a separate entrance to it; so that always had the young and loving healthy wife listened with no unhappy nervousness, but with vigorous pleasure, to the stout ringing of her young-armed old husband's hammer; whose reverberations, muffled by passing through the floors and walls, came up to her, not unsweetly, in her nursery; and so, to stout Labor's iron lullaby, the blacksmith's infants were rocked to slumber. Oh, woe on woe! Oh, Death, why canst thou not sometimes be timely? Hadst thou taken this old blacksmith to thyself ere his full ruin came upon him, then had the young widow had a delicious grief, and her orphans a truly venerable, legendary sire to dream of in their after years; and all of them a care-killing competency.
</p>
</div>
</body>
</html>

@ -0,0 +1,21 @@
{
"slideshow": {
"author": "Yours Truly",
"date": "date of publication",
"slides": [
{
"title": "Wake up to WonderWidgets!",
"type": "all"
},
{
"items": [
"Why <em>WonderWidgets</em> are great",
"Who <em>buys</em> WonderWidgets"
],
"title": "Overview",
"type": "all"
}
],
"title": "Sample Slide Show"
}
}

@ -0,0 +1,24 @@
<?xml version='1.0' encoding='us-ascii'?>
<!-- A SAMPLE set of slides -->
<slideshow
title="Sample Slide Show"
date="Date of publication"
author="Yours Truly"
>
<!-- TITLE SLIDE -->
<slide type="all">
<title>Wake up to WonderWidgets!</title>
</slide>
<!-- OVERVIEW -->
<slide type="all">
<title>Overview</title>
<item>Why <em>WonderWidgets</em> are great</item>
<item/>
<item>Who <em>buys</em> WonderWidgets</item>
</slide>
</slideshow>

@ -0,0 +1,220 @@
<h1>Unicode Demo</h1>
<p>Taken from <a
href="http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-demo.txt">http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-demo.txt</a></p>
<pre>
UTF-8 encoded sample plain-text file
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
Markus Kuhn [ˈmaʳkʊs kuːn] <http://www.cl.cam.ac.uk/~mgk25/> — 2002-07-25
The ASCII compatible UTF-8 encoding used in this plain-text file
is defined in Unicode, ISO 10646-1, and RFC 2279.
Using Unicode/UTF-8, you can write in emails and source code things such as
Mathematics and sciences:
∮ E⋅da = Q, n → ∞, ∑ f(i) = ∏ g(i), ⎧⎡⎛┌─────┐⎞⎤⎫
⎪⎢⎜│a²+b³ ⎟⎥⎪
∀x∈: ⌈x⌉ = x⌋, α ∧ ¬β = ¬(¬α β), ⎪⎢⎜│───── ⎟⎥⎪
⎪⎢⎜⎷ c₈ ⎟⎥⎪
⊆ ℕ₀ ⊂ , ⎨⎢⎜ ⎟⎥⎬
⎪⎢⎜ ∞ ⎟⎥⎪
< a b c d (A B),
⎪⎢⎜ ⎳aⁱ-bⁱ⎟⎥⎪
2H₂ + O₂ ⇌ 2H₂O, R = 4.7 kΩ, ⌀ 200 mm ⎩⎣⎝i=1 ⎠⎦⎭
Linguistics and dictionaries:
ði ıntəˈnæʃənəl fəˈnɛtık əsoʊsiˈeıʃn
Y [ˈʏpsilɔn], Yen [jɛn], Yoga [ˈjoːgɑ]
APL:
((VV)=V)/V←,V ⌷←⍳→⍴∆∇⊃‾⍎⍕⌈
Nicer typography in plain text files:
╔══════════════════════════════════════════╗
║ ║
║ • single and “double” quotes ║
║ ║
║ • Curly apostrophes: “Weve been here” ║
║ ║
║ • Latin-1 apostrophe and accents: '´` ║
║ ║
║ • deutsche „Anführungszeichen“ ║
║ ║
║ • †, ‡, ‰, •, 34, —, 5/+5, ™, … ║
║ ║
║ • ASCII safety test: 1lI|, 0OD, 8B ║
║ ╭─────────╮ ║
║ • the euro symbol: │ 14.95 € │ ║
║ ╰─────────╯ ║
╚══════════════════════════════════════════╝
Combining characters:
STARGΛ̊TE SG-1, a = v̇ = r̈, a⃑ ⊥ b⃑
Greek (in Polytonic):
The Greek anthem:
Σὲ γνωρίζω ἀπὸ τὴν κόψη
τοῦ σπαθιοῦ τὴν τρομερή,
σὲ γνωρίζω ἀπὸ τὴν ὄψη
ποὺ μὲ βία μετράει τὴ γῆ.
᾿Απ᾿ τὰ κόκκαλα βγαλμένη
τῶν ῾Ελλήνων τὰ ἱερά
καὶ σὰν πρῶτα ἀνδρειωμένη
χαῖρε, ὦ χαῖρε, ᾿Ελευθεριά!
From a speech of Demosthenes in the 4th century BC:
Οὐχὶ ταὐτὰ παρίσταταί μοι γιγνώσκειν, ὦ ἄνδρες ᾿Αθηναῖοι,
ὅταν τ᾿ εἰς τὰ πράγματα ἀποβλέψω καὶ ὅταν πρὸς τοὺς
λόγους οὓς ἀκούω· τοὺς μὲν γὰρ λόγους περὶ τοῦ
τιμωρήσασθαι Φίλιππον ὁρῶ γιγνομένους, τὰ δὲ πράγματ᾿
εἰς τοῦτο προήκοντα, ὥσθ᾿ ὅπως μὴ πεισόμεθ᾿ αὐτοὶ
πρότερον κακῶς σκέψασθαι δέον. οὐδέν οὖν ἄλλο μοι δοκοῦσιν
οἱ τὰ τοιαῦτα λέγοντες ἢ τὴν ὑπόθεσιν, περὶ ἧς βουλεύεσθαι,
οὐχὶ τὴν οὖσαν παριστάντες ὑμῖν ἁμαρτάνειν. ἐγὼ δέ, ὅτι μέν
ποτ᾿ ἐξῆν τῇ πόλει καὶ τὰ αὑτῆς ἔχειν ἀσφαλῶς καὶ Φίλιππον
τιμωρήσασθαι, καὶ μάλ᾿ ἀκριβῶς οἶδα· ἐπ᾿ ἐμοῦ γάρ, οὐ πάλαι
γέγονεν ταῦτ᾿ ἀμφότερα· νῦν μέντοι πέπεισμαι τοῦθ᾿ ἱκανὸν
προλαβεῖν ἡμῖν εἶναι τὴν πρώτην, ὅπως τοὺς συμμάχους
σώσομεν. ἐὰν γὰρ τοῦτο βεβαίως ὑπάρξῃ, τότε καὶ περὶ τοῦ
τίνα τιμωρήσεταί τις καὶ ὃν τρόπον ἐξέσται σκοπεῖν· πρὶν δὲ
τὴν ἀρχὴν ὀρθῶς ὑποθέσθαι, μάταιον ἡγοῦμαι περὶ τῆς
τελευτῆς ὁντινοῦν ποιεῖσθαι λόγον.
Δημοσθένους, Γ´ ᾿Ολυνθιακὸς
Georgian:
From a Unicode conference invitation:
გთხოვთ ახლავე გაიაროთ რეგისტრაცია Unicode-ის მეათე საერთაშორისო
კონფერენციაზე დასასწრებად, რომელიც გაიმართება 10-12 მარტს,
ქ. მაინცში, გერმანიაში. კონფერენცია შეჰკრებს ერთად მსოფლიოს
ექსპერტებს ისეთ დარგებში როგორიცაა ინტერნეტი და Unicode-ი,
ინტერნაციონალიზაცია და ლოკალიზაცია, Unicode-ის გამოყენება
ოპერაციულ სისტემებსა, და გამოყენებით პროგრამებში, შრიფტებში,
ტექსტების დამუშავებასა და მრავალენოვან კომპიუტერულ სისტემებში.
Russian:
From a Unicode conference invitation:
Зарегистрируйтесь сейчас на Десятую Международную Конференцию по
Unicode, которая состоится 10-12 марта 1997 года в Майнце в Германии.
Конференция соберет широкий круг экспертов по вопросам глобального
Интернета и Unicode, локализации и интернационализации, воплощению и
применению Unicode в различных операционных системах и программных
приложениях, шрифтах, верстке и многоязычных компьютерных системах.
Thai (UCS Level 2):
Excerpt from a poetry on The Romance of The Three Kingdoms (a Chinese
classic 'San Gua'):
[----------------------------|------------------------]
๏ แผ่นดินฮั่นเสื่อมโทรมแสนสังเวช พระปกเกศกองบู๊กู้ขึ้นใหม่
สิบสองกษัตริย์ก่อนหน้าแลถัดไป สององค์ไซร้โง่เขลาเบาปัญญา
ทรงนับถือขันทีเป็นที่พึ่ง บ้านเมืองจึงวิปริตเป็นนักหนา
โฮจิ๋นเรียกทัพทั่วหัวเมืองมา หมายจะฆ่ามดชั่วตัวสำคัญ
เหมือนขับไสไล่เสือจากเคหา รับหมาป่าเข้ามาเลยอาสัญ
ฝ่ายอ้องอุ้นยุแยกให้แตกกัน ใช้สาวนั้นเป็นชนวนชื่นชวนใจ
พลันลิฉุยกุยกีกลับก่อเหตุ ช่างอาเพศจริงหนาฟ้าร้องไห้
ต้องรบราฆ่าฟันจนบรรลัย ฤๅหาใครค้ำชูกู้บรรลังก์ ฯ
(The above is a two-column text. If combining characters are handled
correctly, the lines of the second column should be aligned with the
| character above.)
Ethiopian:
Proverbs in the Amharic language:
ሰማይ አይታረስ ንጉሥ አይከሰስ።
ብላ ካለኝ እንደአባቴ በቆመጠኝ።
ጌጥ ያለቤቱ ቁምጥና ነው።
ደሀ በሕልሙ ቅቤ ባይጠጣ ንጣት በገደለው።
የአፍ ወለምታ በቅቤ አይታሽም።
አይጥ በበላ ዳዋ ተመታ።
ሲተረጉሙ ይደረግሙ።
ቀስ በቀስ፥ ዕንቁላል በእግሩ ይሄዳል።
ድር ቢያብር አንበሳ ያስር።
ሰው እንደቤቱ እንጅ እንደ ጉረቤቱ አይተዳደርም።
እግዜር የከፈተውን ጉሮሮ ሳይዘጋው አይድርም።
የጎረቤት ሌባ፥ ቢያዩት ይስቅ ባያዩት ያጠልቅ።
ሥራ ከመፍታት ልጄን ላፋታት።
ዓባይ ማደሪያ የለው፥ ግንድ ይዞ ይዞራል።
የእስላም አገሩ መካ የአሞራ አገሩ ዋርካ።
ተንጋሎ ቢተፉ ተመልሶ ባፉ።
ወዳጅህ ማር ቢሆን ጨርስህ አትላሰው።
እግርህን በፍራሽህ ልክ ዘርጋ።
Runes:
ᚻᛖ ᚳᚹᚫᚦ ᚦᚫᛏ ᚻᛖ ᛒᚢᛞᛖ ᚩᚾ ᚦᚫᛗ ᛚᚪᚾᛞᛖ ᚾᚩᚱᚦᚹᛖᚪᚱᛞᚢᛗ ᚹᛁᚦ ᚦᚪ ᚹᛖᛥᚫ
(Old English, which transcribed into Latin reads 'He cwaeth that he
bude thaem lande northweardum with tha Westsae.' and means 'He said
that he lived in the northern land near the Western Sea.')
Braille:
⡌⠁⠧⠑ ⠼⠁⠒ ⡍⠜⠇⠑⠹⠰⠎ ⡣⠕⠌
⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠙⠑⠁⠙⠒ ⠞⠕ ⠃⠑⠛⠔ ⠺⠊⠹⠲ ⡹⠻⠑ ⠊⠎ ⠝⠕ ⠙⠳⠃⠞
⠱⠁⠞⠑⠧⠻ ⠁⠃⠳⠞ ⠹⠁⠞⠲ ⡹⠑ ⠗⠑⠛⠊⠌⠻ ⠕⠋ ⠙⠊⠎ ⠃⠥⠗⠊⠁⠇ ⠺⠁⠎
⠎⠊⠛⠝⠫ ⠃⠹ ⠹⠑ ⠊⠇⠻⠛⠹⠍⠁⠝⠂ ⠹⠑ ⠊⠇⠻⠅⠂ ⠹⠑ ⠥⠝⠙⠻⠞⠁⠅⠻⠂
⠁⠝⠙ ⠹⠑ ⠡⠊⠑⠋ ⠍⠳⠗⠝⠻⠲ ⡎⠊⠗⠕⠕⠛⠑ ⠎⠊⠛⠝⠫ ⠊⠞⠲ ⡁⠝⠙
⡎⠊⠗⠕⠕⠛⠑⠰⠎ ⠝⠁⠍⠑ ⠺⠁⠎ ⠛⠕⠕⠙ ⠥⠏⠕⠝ ⠰⡡⠁⠝⠛⠑⠂ ⠋⠕⠗ ⠁⠝⠹⠹⠔⠛ ⠙⠑
⠡⠕⠎⠑ ⠞⠕ ⠏⠥⠞ ⠙⠊⠎ ⠙⠁⠝⠙ ⠞⠕⠲
⡕⠇⠙ ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠁⠎ ⠙⠑⠁⠙ ⠁⠎ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲
⡍⠔⠙⠖ ⡊ ⠙⠕⠝⠰⠞ ⠍⠑⠁⠝ ⠞⠕ ⠎⠁⠹ ⠹⠁⠞ ⡊ ⠅⠝⠪⠂ ⠕⠋ ⠍⠹
⠪⠝ ⠅⠝⠪⠇⠫⠛⠑⠂ ⠱⠁⠞ ⠹⠻⠑ ⠊⠎ ⠏⠜⠞⠊⠊⠥⠇⠜⠇⠹ ⠙⠑⠁⠙ ⠁⠃⠳⠞
⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ ⡊ ⠍⠊⠣⠞ ⠙⠁⠧⠑ ⠃⠑⠲ ⠔⠊⠇⠔⠫⠂ ⠍⠹⠎⠑⠇⠋⠂ ⠞⠕
⠗⠑⠛⠜⠙ ⠁ ⠊⠕⠋⠋⠔⠤⠝⠁⠊⠇ ⠁⠎ ⠹⠑ ⠙⠑⠁⠙⠑⠌ ⠏⠊⠑⠊⠑ ⠕⠋ ⠊⠗⠕⠝⠍⠕⠝⠛⠻⠹
⠔ ⠹⠑ ⠞⠗⠁⠙⠑⠲ ⡃⠥⠞ ⠹⠑ ⠺⠊⠎⠙⠕⠍ ⠕⠋ ⠳⠗ ⠁⠝⠊⠑⠌⠕⠗⠎
⠊⠎ ⠔ ⠹⠑ ⠎⠊⠍⠊⠇⠑⠆ ⠁⠝⠙ ⠍⠹ ⠥⠝⠙⠁⠇⠇⠪⠫ ⠙⠁⠝⠙⠎
⠩⠁⠇⠇ ⠝⠕⠞ ⠙⠊⠌⠥⠗⠃ ⠊⠞⠂ ⠕⠗ ⠹⠑ ⡊⠳⠝⠞⠗⠹⠰⠎ ⠙⠕⠝⠑ ⠋⠕⠗⠲ ⡹⠳
⠺⠊⠇⠇ ⠹⠻⠑⠋⠕⠗⠑ ⠏⠻⠍⠊⠞ ⠍⠑ ⠞⠕ ⠗⠑⠏⠑⠁⠞⠂ ⠑⠍⠏⠙⠁⠞⠊⠊⠁⠇⠇⠹⠂ ⠹⠁⠞
⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠁⠎ ⠙⠑⠁⠙ ⠁⠎ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲
(The first couple of paragraphs of "A Christmas Carol" by Dickens)
Compact font selection example text:
ABCDEFGHIJKLMNOPQRSTUVWXYZ /0123456789
abcdefghijklmnopqrstuvwxyz £©µÀÆÖÞßéöÿ
–—‘“”„†•…‰™œŠŸž€ ΑΒΓΔΩαβγδω АБВГДабвгд
∀∂∈ℝ∧∪≡∞ ↑↗↨↻⇣ ┐┼╔╘░►☺♀ fi<>⑀₂ἠḂӥẄɐː⍎אԱა
Greetings in various languages:
Hello world, Καλημέρα κόσμε, コンニチハ
Box drawing alignment tests: █
╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳
║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳
║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳
╠╡ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳
║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎
║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏
╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ ▗▄▖▛▀▜ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█
▝▀▘▙▄▟
</pre>
Loading…
Cancel
Save