# -*- coding: utf-8 -*-
"""
httpbin.core
~~~~~~~~~~~~
This module provides the core HttpBin experience.
"""
import base64
import json
import os
import random
import time
import uuid
import argparse
from flask import (
Flask,
Response,
request,
render_template,
redirect,
jsonify as flask_jsonify,
make_response,
url_for,
abort,
)
from six.moves import range as xrange
from werkzeug.datastructures import WWWAuthenticate, MultiDict
from werkzeug.http import http_date
from werkzeug.wrappers import BaseResponse
from werkzeug.http import parse_authorization_header
from flasgger import Swagger, NO_SANITIZER
from . import filters
from .helpers import (
get_headers,
status_code,
get_dict,
get_request_range,
check_basic_auth,
check_digest_auth,
secure_cookie,
H,
ROBOT_TXT,
ANGRY_ASCII,
parse_multi_value_header,
next_stale_after_value,
digest_challenge_response,
)
from .utils import weighted_choice
from .structures import CaseInsensitiveDict
with open(
os.path.join(os.path.realpath(os.path.dirname(__file__)), "VERSION")
) as version_file:
version = version_file.read().strip()
ENV_COOKIES = (
"_gauges_unique",
"_gauges_unique_year",
"_gauges_unique_month",
"_gauges_unique_day",
"_gauges_unique_hour",
"__utmz",
"__utma",
"__utmb",
)
def jsonify(*args, **kwargs):
response = flask_jsonify(*args, **kwargs)
if not response.data.endswith(b"\n"):
response.data += b"\n"
return response
# Prevent WSGI from correcting the casing of the Location header
BaseResponse.autocorrect_location_header = False
# Find the correct template folder when running from a different location
tmpl_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates")
app = Flask(__name__, template_folder=tmpl_dir)
app.debug = bool(os.environ.get("DEBUG"))
app.config["JSONIFY_PRETTYPRINT_REGULAR"] = True
app.add_template_global("HTTPBIN_TRACKING" in os.environ, name="tracking_enabled")
app.config["SWAGGER"] = {"title": "httpbin.org", "uiversion": 3}
template = {
"swagger": "2.0",
"info": {
"title": "httpbin.org",
"description": (
"A simple HTTP Request & Response Service."
"
Run locally: $ docker run -p 80:80 kennethreitz/httpbin"
),
"contact": {
"responsibleOrganization": "Kenneth Reitz",
"responsibleDeveloper": "Kenneth Reitz",
"email": "me@kennethreitz.org",
"url": "https://kennethreitz.org",
},
# "termsOfService": "http://me.com/terms",
"version": version,
},
"host": "httpbin.org", # overrides localhost:5000
"basePath": "/", # base bash for blueprint registration
"schemes": ["https"],
"protocol": "https",
"tags": [
{
"name": "HTTP Methods",
"description": "Testing different HTTP verbs",
# 'externalDocs': {'description': 'Learn more', 'url': 'https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html'}
},
{"name": "Auth", "description": "Auth methods"},
{
"name": "Status codes",
"description": "Generates responses with given status code",
},
{"name": "Request inspection", "description": "Inspect the request data"},
{
"name": "Response inspection",
"description": "Inspect the response data like caching and headers",
},
{
"name": "Response formats",
"description": "Returns responses in different data formats",
},
{"name": "Dynamic data", "description": "Generates random and dynamic data"},
{"name": "Cookies", "description": "Creates, reads and deletes Cookies"},
{"name": "Images", "description": "Returns different image formats"},
{"name": "Redirects", "description": "Returns different redirect responses"},
{
"name": "Anything",
"description": "Returns anything that is passed to request",
},
],
}
swagger_config = {
"headers": [],
"specs": [
{
"endpoint": "spec",
"route": "/spec.json",
"rule_filter": lambda rule: True, # all in
"model_filter": lambda tag: True, # all in
}
],
"static_url_path": "/flasgger_static",
# "static_folder": "static", # must be set by user
"swagger_ui": True,
"specs_route": "/",
}
swagger = Swagger(app, sanitizer=NO_SANITIZER, template=template, config=swagger_config)
# Set up Bugsnag exception tracking, if desired. To use Bugsnag, install the
# Bugsnag Python client with the command "pip install bugsnag", and set the
# environment variable BUGSNAG_API_KEY. You can also optionally set
# BUGSNAG_RELEASE_STAGE.
if os.environ.get("BUGSNAG_API_KEY") is not None:
try:
import bugsnag
import bugsnag.flask
release_stage = os.environ.get("BUGSNAG_RELEASE_STAGE") or "production"
bugsnag.configure(
api_key=os.environ.get("BUGSNAG_API_KEY"),
project_root=os.path.dirname(os.path.abspath(__file__)),
use_ssl=True,
release_stage=release_stage,
ignore_classes=["werkzeug.exceptions.NotFound"],
)
bugsnag.flask.handle_exceptions(app)
except:
app.logger.warning("Unable to initialize Bugsnag exception handling.")
# -----------
# Middlewares
# -----------
"""
https://github.com/kennethreitz/httpbin/issues/340
Adds a middleware to provide chunked request encoding support running under
gunicorn only.
Werkzeug required environ 'wsgi.input_terminated' to be set otherwise it
empties the input request stream.
- gunicorn seems to support input_terminated but does not add the environ,
so we add it here.
- flask will hang and does not seem to properly terminate the request, so
we explicitly deny chunked requests.
"""
@app.before_request
def before_request():
if request.environ.get("HTTP_TRANSFER_ENCODING", "").lower() == "chunked":
server = request.environ.get("SERVER_SOFTWARE", "")
if server.lower().startswith("gunicorn/"):
if "wsgi.input_terminated" in request.environ:
app.logger.debug(
"environ wsgi.input_terminated already set, keeping: %s"
% request.environ["wsgi.input_terminated"]
)
else:
request.environ["wsgi.input_terminated"] = 1
else:
abort(501, "Chunked requests are not supported for server %s" % server)
@app.after_request
def set_cors_headers(response):
response.headers["Access-Control-Allow-Origin"] = request.headers.get("Origin", "*")
response.headers["Access-Control-Allow-Credentials"] = "true"
if request.method == "OPTIONS":
# Both of these headers are only used for the "preflight request"
# http://www.w3.org/TR/cors/#access-control-allow-methods-response-header
response.headers[
"Access-Control-Allow-Methods"
] = "GET, POST, PUT, DELETE, PATCH, OPTIONS"
response.headers["Access-Control-Max-Age"] = "3600" # 1 hour cache
if request.headers.get("Access-Control-Request-Headers") is not None:
response.headers["Access-Control-Allow-Headers"] = request.headers[
"Access-Control-Request-Headers"
]
return response
# ------
# Routes
# ------
@app.route("/legacy")
def view_landing_page():
"""Generates Landing Page in legacy layout."""
return render_template("index.html")
@app.route("/html")
def view_html_page():
"""Returns a simple HTML document.
---
tags:
- Response formats
produces:
- text/html
responses:
200:
description: An HTML page.
"""
return render_template("moby.html")
@app.route("/robots.txt")
def view_robots_page():
"""Returns some robots.txt rules.
---
tags:
- Response formats
produces:
- text/plain
responses:
200:
description: Robots file
"""
response = make_response()
response.data = ROBOT_TXT
response.content_type = "text/plain"
return response
@app.route("/deny")
def view_deny_page():
"""Returns page denied by robots.txt rules.
---
tags:
- Response formats
produces:
- text/plain
responses:
200:
description: Denied message
"""
response = make_response()
response.data = ANGRY_ASCII
response.content_type = "text/plain"
return response
# return "YOU SHOULDN'T BE HERE"
@app.route("/ip")
def view_origin():
"""Returns the requester's IP Address.
---
tags:
- Request inspection
produces:
- application/json
responses:
200:
description: The Requester's IP Address.
"""
return jsonify(origin=request.headers.get("X-Forwarded-For", request.remote_addr))
@app.route("/uuid")
def view_uuid():
"""Return a UUID4.
---
tags:
- Dynamic data
produces:
- application/json
responses:
200:
description: A UUID4.
"""
return jsonify(uuid=str(uuid.uuid4()))
@app.route("/headers")
def view_headers():
"""Return the incoming request's HTTP headers.
---
tags:
- Request inspection
produces:
- application/json
responses:
200:
description: The request's headers.
"""
return jsonify(get_dict('headers'))
@app.route("/user-agent")
def view_user_agent():
"""Return the incoming requests's User-Agent header.
---
tags:
- Request inspection
produces:
- application/json
responses:
200:
description: The request's User-Agent header.
"""
headers = get_headers()
return jsonify({"user-agent": headers["user-agent"]})
@app.route("/get", methods=("GET",))
def view_get():
"""The request's query parameters.
---
tags:
- HTTP Methods
produces:
- application/json
responses:
200:
description: The request's query parameters.
"""
return jsonify(get_dict("url", "args", "headers", "origin"))
@app.route("/anything", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "TRACE"])
@app.route(
"/anything/",
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "TRACE"],
)
def view_anything(anything=None):
"""Returns anything passed in request data.
---
tags:
- Anything
produces:
- application/json
responses:
200:
description: Anything passed in request
"""
return jsonify(
get_dict(
"url",
"args",
"headers",
"origin",
"method",
"form",
"data",
"files",
"json",
)
)
@app.route("/post", methods=("POST",))
def view_post():
"""The request's POST parameters.
---
tags:
- HTTP Methods
produces:
- application/json
responses:
200:
description: The request's POST parameters.
"""
return jsonify(
get_dict("url", "args", "form", "data", "origin", "headers", "files", "json")
)
@app.route("/put", methods=("PUT",))
def view_put():
"""The request's PUT parameters.
---
tags:
- HTTP Methods
produces:
- application/json
responses:
200:
description: The request's PUT parameters.
"""
return jsonify(
get_dict("url", "args", "form", "data", "origin", "headers", "files", "json")
)
@app.route("/patch", methods=("PATCH",))
def view_patch():
"""The request's PATCH parameters.
---
tags:
- HTTP Methods
produces:
- application/json
responses:
200:
description: The request's PATCH parameters.
"""
return jsonify(
get_dict("url", "args", "form", "data", "origin", "headers", "files", "json")
)
@app.route("/delete", methods=("DELETE",))
def view_delete():
"""The request's DELETE parameters.
---
tags:
- HTTP Methods
produces:
- application/json
responses:
200:
description: The request's DELETE parameters.
"""
return jsonify(
get_dict("url", "args", "form", "data", "origin", "headers", "files", "json")
)
@app.route("/gzip")
@filters.gzip
def view_gzip_encoded_content():
"""Returns GZip-encoded data.
---
tags:
- Response formats
produces:
- application/json
responses:
200:
description: GZip-encoded data.
"""
return jsonify(get_dict("origin", "headers", method=request.method, gzipped=True))
@app.route("/deflate")
@filters.deflate
def view_deflate_encoded_content():
"""Returns Deflate-encoded data.
---
tags:
- Response formats
produces:
- application/json
responses:
200:
description: Defalte-encoded data.
"""
return jsonify(get_dict("origin", "headers", method=request.method, deflated=True))
@app.route("/brotli")
@filters.brotli
def view_brotli_encoded_content():
"""Returns Brotli-encoded data.
---
tags:
- Response formats
produces:
- application/json
responses:
200:
description: Brotli-encoded data.
"""
return jsonify(get_dict("origin", "headers", method=request.method, brotli=True))
@app.route("/redirect/")
def redirect_n_times(n):
"""302 Redirects n times.
---
tags:
- Redirects
parameters:
- in: path
name: n
type: int
produces:
- text/html
responses:
302:
description: A redirection.
"""
assert n > 0
absolute = request.args.get("absolute", "false").lower() == "true"
if n == 1:
return redirect(url_for("view_get", _external=absolute))
if absolute:
return _redirect("absolute", n, True)
else:
return _redirect("relative", n, False)
def _redirect(kind, n, external):
return redirect(
url_for("{0}_redirect_n_times".format(kind), n=n - 1, _external=external)
)
@app.route("/redirect-to", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "TRACE"])
def redirect_to():
"""302/3XX Redirects to the given URL.
---
tags:
- Redirects
produces:
- text/html
get:
parameters:
- in: query
name: url
type: string
required: true
- in: query
name: status_code
type: int
post:
consumes:
- application/x-www-form-urlencoded
parameters:
- in: formData
name: url
type: string
required: true
- in: formData
name: status_code
type: int
required: false
patch:
consumes:
- application/x-www-form-urlencoded
parameters:
- in: formData
name: url
type: string
required: true
- in: formData
name: status_code
type: int
required: false
put:
consumes:
- application/x-www-form-urlencoded
parameters:
- in: formData
name: url
type: string
required: true
- in: formData
name: status_code
type: int
required: false
responses:
302:
description: A redirection.
"""
args_dict = request.args.items()
args = CaseInsensitiveDict(args_dict)
# We need to build the response manually and convert to UTF-8 to prevent
# werkzeug from "fixing" the URL. This endpoint should set the Location
# header to the exact string supplied.
response = app.make_response("")
response.status_code = 302
if "status_code" in args:
status_code = int(args["status_code"])
if status_code >= 300 and status_code < 400:
response.status_code = status_code
response.headers["Location"] = args["url"].encode("utf-8")
return response
@app.route("/relative-redirect/")
def relative_redirect_n_times(n):
"""Relatively 302 Redirects n times.
---
tags:
- Redirects
parameters:
- in: path
name: n
type: int
produces:
- text/html
responses:
302:
description: A redirection.
"""
assert n > 0
response = app.make_response("")
response.status_code = 302
if n == 1:
response.headers["Location"] = url_for("view_get")
return response
response.headers["Location"] = url_for("relative_redirect_n_times", n=n - 1)
return response
@app.route("/absolute-redirect/")
def absolute_redirect_n_times(n):
"""Absolutely 302 Redirects n times.
---
tags:
- Redirects
parameters:
- in: path
name: n
type: int
produces:
- text/html
responses:
302:
description: A redirection.
"""
assert n > 0
if n == 1:
return redirect(url_for("view_get", _external=True))
return _redirect("absolute", n, True)
@app.route("/stream/")
def stream_n_messages(n):
"""Stream n JSON responses
---
tags:
- Dynamic data
parameters:
- in: path
name: n
type: int
produces:
- application/json
responses:
200:
description: Streamed JSON responses.
"""
response = get_dict("url", "args", "headers", "origin")
n = min(n, 100)
def generate_stream():
for i in range(n):
response["id"] = i
yield json.dumps(response) + "\n"
return Response(generate_stream(), headers={"Content-Type": "application/json"})
@app.route(
"/status/", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "TRACE"]
)
def view_status_code(codes):
"""Return status code or random status code if more than one are given
---
tags:
- Status codes
parameters:
- in: path
name: codes
produces:
- text/plain
responses:
100:
description: Informational responses
200:
description: Success
300:
description: Redirection
400:
description: Client Errors
500:
description: Server Errors
"""
if "," not in codes:
try:
code = int(codes)
except ValueError:
return Response("Invalid status code", status=400)
return status_code(code)
choices = []
for choice in codes.split(","):
if ":" not in choice:
code = choice
weight = 1
else:
code, weight = choice.split(":")
try:
choices.append((int(code), float(weight)))
except ValueError:
return Response("Invalid status code", status=400)
code = weighted_choice(choices)
return status_code(code)
@app.route("/response-headers", methods=["GET", "POST"])
def response_headers():
"""Returns a set of response headers from the query string.
---
tags:
- Response inspection
parameters:
- in: query
name: freeform
explode: true
allowEmptyValue: true
schema:
type: object
additionalProperties:
type: string
style: form
produces:
- application/json
responses:
200:
description: Response headers
"""
# Pending swaggerUI update
# https://github.com/swagger-api/swagger-ui/issues/3850
headers = MultiDict(request.args.items(multi=True))
response = jsonify(list(headers.lists()))
while True:
original_data = response.data
d = {}
for key in response.headers.keys():
value = response.headers.get_all(key)
if len(value) == 1:
value = value[0]
d[key] = value
response = jsonify(d)
for key, value in headers.items(multi=True):
response.headers.add(key, value)
response_has_changed = response.data != original_data
if not response_has_changed:
break
return response
@app.route("/cookies")
def view_cookies(hide_env=True):
"""Returns cookie data.
---
tags:
- Cookies
produces:
- application/json
responses:
200:
description: Set cookies.
"""
cookies = dict(request.cookies.items())
if hide_env and ("show_env" not in request.args):
for key in ENV_COOKIES:
try:
del cookies[key]
except KeyError:
pass
return jsonify(cookies=cookies)
@app.route("/forms/post")
def view_forms_post():
"""Simple HTML form."""
return render_template("forms-post.html")
@app.route("/cookies/set//")
def set_cookie(name, value):
"""Sets a cookie and redirects to cookie list.
---
tags:
- Cookies
parameters:
- in: path
name: name
type: string
- in: path
name: value
type: string
produces:
- text/plain
responses:
200:
description: Set cookies and redirects to cookie list.
"""
r = app.make_response(redirect(url_for("view_cookies")))
r.set_cookie(key=name, value=value, secure=secure_cookie())
return r
@app.route("/cookies/set")
def set_cookies():
"""Sets cookie(s) as provided by the query string and redirects to cookie list.
---
tags:
- Cookies
parameters:
- in: query
name: freeform
explode: true
allowEmptyValue: true
schema:
type: object
additionalProperties:
type: string
style: form
produces:
- text/plain
responses:
200:
description: Redirect to cookie list
"""
cookies = dict(request.args.items())
r = app.make_response(redirect(url_for("view_cookies")))
for key, value in cookies.items():
r.set_cookie(key=key, value=value, secure=secure_cookie())
return r
@app.route("/cookies/delete")
def delete_cookies():
"""Deletes cookie(s) as provided by the query string and redirects to cookie list.
---
tags:
- Cookies
parameters:
- in: query
name: freeform
explode: true
allowEmptyValue: true
schema:
type: object
additionalProperties:
type: string
style: form
produces:
- text/plain
responses:
200:
description: Redirect to cookie list
"""
cookies = dict(request.args.items())
r = app.make_response(redirect(url_for("view_cookies")))
for key, value in cookies.items():
r.delete_cookie(key=key)
return r
@app.route("/basic-auth//")
def basic_auth(user="user", passwd="passwd"):
"""Prompts the user for authorization using HTTP Basic Auth.
---
tags:
- Auth
parameters:
- in: path
name: user
type: string
- in: path
name: passwd
type: string
produces:
- application/json
responses:
200:
description: Sucessful authentication.
401:
description: Unsuccessful authentication.
"""
if not check_basic_auth(user, passwd):
return status_code(401)
return jsonify(authenticated=True, user=user)
@app.route("/hidden-basic-auth//")
def hidden_basic_auth(user="user", passwd="passwd"):
"""Prompts the user for authorization using HTTP Basic Auth.
---
tags:
- Auth
parameters:
- in: path
name: user
type: string
- in: path
name: passwd
type: string
produces:
- application/json
responses:
200:
description: Sucessful authentication.
404:
description: Unsuccessful authentication.
"""
if not check_basic_auth(user, passwd):
return status_code(404)
return jsonify(authenticated=True, user=user)
@app.route("/bearer")
def bearer_auth():
"""Prompts the user for authorization using bearer authentication.
---
tags:
- Auth
parameters:
- in: header
name: Authorization
schema:
type: string
produces:
- application/json
responses:
200:
description: Sucessful authentication.
401:
description: Unsuccessful authentication.
"""
authorization = request.headers.get("Authorization")
if not (authorization and authorization.startswith("Bearer ")):
response = app.make_response("")
response.headers["WWW-Authenticate"] = "Bearer"
response.status_code = 401
return response
slice_start = len("Bearer ")
token = authorization[slice_start:]
return jsonify(authenticated=True, token=token)
@app.route("/digest-auth///")
def digest_auth_md5(qop=None, user="user", passwd="passwd"):
"""Prompts the user for authorization using Digest Auth.
---
tags:
- Auth
parameters:
- in: path
name: qop
type: string
description: auth or auth-int
- in: path
name: user
type: string
- in: path
name: passwd
type: string
produces:
- application/json
responses:
200:
description: Sucessful authentication.
401:
description: Unsuccessful authentication.
"""
return digest_auth(qop, user, passwd, "MD5", "never")
@app.route("/digest-auth////")
def digest_auth_nostale(qop=None, user="user", passwd="passwd", algorithm="MD5"):
"""Prompts the user for authorization using Digest Auth + Algorithm.
---
tags:
- Auth
parameters:
- in: path
name: qop
type: string
description: auth or auth-int
- in: path
name: user
type: string
- in: path
name: passwd
type: string
- in: path
name: algorithm
type: string
description: MD5, SHA-256, SHA-512
default: MD5
produces:
- application/json
responses:
200:
description: Sucessful authentication.
401:
description: Unsuccessful authentication.
"""
return digest_auth(qop, user, passwd, algorithm, "never")
@app.route("/digest-auth/////")
def digest_auth(
qop=None, user="user", passwd="passwd", algorithm="MD5", stale_after="never"
):
"""Prompts the user for authorization using Digest Auth + Algorithm.
allow settings the stale_after argument.
---
tags:
- Auth
parameters:
- in: path
name: qop
type: string
description: auth or auth-int
- in: path
name: user
type: string
- in: path
name: passwd
type: string
- in: path
name: algorithm
type: string
description: MD5, SHA-256, SHA-512
default: MD5
- in: path
name: stale_after
type: string
default: never
produces:
- application/json
responses:
200:
description: Sucessful authentication.
401:
description: Unsuccessful authentication.
"""
require_cookie_handling = request.args.get("require-cookie", "").lower() in (
"1",
"t",
"true",
)
if algorithm not in ("MD5", "SHA-256", "SHA-512"):
algorithm = "MD5"
if qop not in ("auth", "auth-int"):
qop = None
authorization = request.headers.get("Authorization")
credentials = None
if authorization:
credentials = parse_authorization_header(authorization)
if (
not authorization
or not credentials
or credentials.type.lower() != "digest"
or (require_cookie_handling and "Cookie" not in request.headers)
):
response = digest_challenge_response(app, qop, algorithm)
response.set_cookie("stale_after", value=stale_after)
response.set_cookie("fake", value="fake_value")
return response
if require_cookie_handling and request.cookies.get("fake") != "fake_value":
response = jsonify({"errors": ["missing cookie set on challenge"]})
response.set_cookie("fake", value="fake_value")
response.status_code = 403
return response
current_nonce = credentials.get("nonce")
stale_after_value = None
if "stale_after" in request.cookies:
stale_after_value = request.cookies.get("stale_after")
if (
"last_nonce" in request.cookies
and current_nonce == request.cookies.get("last_nonce")
or stale_after_value == "0"
):
response = digest_challenge_response(app, qop, algorithm, True)
response.set_cookie("stale_after", value=stale_after)
response.set_cookie("last_nonce", value=current_nonce)
response.set_cookie("fake", value="fake_value")
return response
if not check_digest_auth(user, passwd):
response = digest_challenge_response(app, qop, algorithm, False)
response.set_cookie("stale_after", value=stale_after)
response.set_cookie("last_nonce", value=current_nonce)
response.set_cookie("fake", value="fake_value")
return response
response = jsonify(authenticated=True, user=user)
response.set_cookie("fake", value="fake_value")
if stale_after_value:
response.set_cookie(
"stale_after", value=next_stale_after_value(stale_after_value)
)
return response
@app.route("/delay/", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "TRACE"])
def delay_response(delay):
"""Returns a delayed response (max of 10 seconds).
---
tags:
- Dynamic data
parameters:
- in: path
name: delay
type: int
produces:
- application/json
responses:
200:
description: A delayed response.
"""
delay = min(float(delay), 10)
time.sleep(delay)
return jsonify(
get_dict("url", "args", "form", "data", "origin", "headers", "files")
)
@app.route("/drip")
def drip():
"""Drips data over a duration after an optional initial delay.
---
tags:
- Dynamic data
parameters:
- in: query
name: duration
type: number
description: The amount of time (in seconds) over which to drip each byte
default: 2
required: false
- in: query
name: numbytes
type: integer
description: The number of bytes to respond with
default: 10
required: false
- in: query
name: code
type: integer
description: The response code that will be returned
default: 200
required: false
- in: query
name: delay
type: number
description: The amount of time (in seconds) to delay before responding
default: 2
required: false
produces:
- application/octet-stream
responses:
200:
description: A dripped response.
"""
args = CaseInsensitiveDict(request.args.items())
duration = float(args.get("duration", 2))
numbytes = min(int(args.get("numbytes", 10)), (10 * 1024 * 1024)) # set 10MB limit
code = int(args.get("code", 200))
if numbytes <= 0:
response = Response("number of bytes must be positive", status=400)
return response
delay = float(args.get("delay", 0))
if delay > 0:
time.sleep(delay)
pause = duration / numbytes
def generate_bytes():
for i in xrange(numbytes):
yield b"*"
time.sleep(pause)
response = Response(
generate_bytes(),
headers={
"Content-Type": "application/octet-stream",
"Content-Length": str(numbytes),
},
)
response.status_code = code
return response
@app.route("/base64/")
def decode_base64(value):
"""Decodes base64url-encoded string.
---
tags:
- Dynamic data
parameters:
- in: path
name: value
type: string
default: SFRUUEJJTiBpcyBhd2Vzb21l
produces:
- text/html
responses:
200:
description: Decoded base64 content.
"""
encoded = value.encode("utf-8") # base64 expects binary string as input
try:
return base64.urlsafe_b64decode(encoded).decode("utf-8")
except:
return "Incorrect Base64 data try: SFRUUEJJTiBpcyBhd2Vzb21l"
@app.route("/cache", methods=("GET",))
def cache():
"""Returns a 304 if an If-Modified-Since header or If-None-Match is present. Returns the same as a GET otherwise.
---
tags:
- Response inspection
parameters:
- in: header
name: If-Modified-Since
- in: header
name: If-None-Match
produces:
- application/json
responses:
200:
description: Cached response
304:
description: Modified
"""
is_conditional = request.headers.get("If-Modified-Since") or request.headers.get(
"If-None-Match"
)
if is_conditional is None:
response = view_get()
response.headers["Last-Modified"] = http_date()
response.headers["ETag"] = uuid.uuid4().hex
return response
else:
return status_code(304)
@app.route("/etag/", methods=("GET",))
def etag(etag):
"""Assumes the resource has the given etag and responds to If-None-Match and If-Match headers appropriately.
---
tags:
- Response inspection
parameters:
- in: header
name: If-None-Match
- in: header
name: If-Match
produces:
- application/json
responses:
200:
description: Normal response
412:
description: match
"""
if_none_match = parse_multi_value_header(request.headers.get("If-None-Match"))
if_match = parse_multi_value_header(request.headers.get("If-Match"))
if if_none_match:
if etag in if_none_match or "*" in if_none_match:
response = status_code(304)
response.headers["ETag"] = etag
return response
elif if_match:
if etag not in if_match and "*" not in if_match:
return status_code(412)
# Special cases don't apply, return normal response
response = view_get()
response.headers["ETag"] = etag
return response
@app.route("/cache/")
def cache_control(value):
"""Sets a Cache-Control header for n seconds.
---
tags:
- Response inspection
parameters:
- in: path
name: value
type: integer
produces:
- application/json
responses:
200:
description: Cache control set
"""
response = view_get()
response.headers["Cache-Control"] = "public, max-age={0}".format(value)
return response
@app.route("/encoding/utf8")
def encoding():
"""Returns a UTF-8 encoded body.
---
tags:
- Response formats
produces:
- text/html
responses:
200:
description: Encoded UTF-8 content.
"""
return render_template("UTF-8-demo.txt")
@app.route("/bytes/")
def random_bytes(n):
"""Returns n random bytes generated with given seed
---
tags:
- Dynamic data
parameters:
- in: path
name: n
type: int
produces:
- application/octet-stream
responses:
200:
description: Bytes.
"""
n = min(n, 100 * 1024) # set 100KB limit
params = CaseInsensitiveDict(request.args.items())
if "seed" in params:
random.seed(int(params["seed"]))
response = make_response()
# Note: can't just use os.urandom here because it ignores the seed
response.data = bytearray(random.randint(0, 255) for i in range(n))
response.content_type = "application/octet-stream"
return response
@app.route("/stream-bytes/")
def stream_random_bytes(n):
"""Streams n random bytes generated with given seed, at given chunk size per packet.
---
tags:
- Dynamic data
parameters:
- in: path
name: n
type: int
produces:
- application/octet-stream
responses:
200:
description: Bytes.
"""
n = min(n, 100 * 1024) # set 100KB limit
params = CaseInsensitiveDict(request.args.items())
if "seed" in params:
random.seed(int(params["seed"]))
if "chunk_size" in params:
chunk_size = max(1, int(params["chunk_size"]))
else:
chunk_size = 10 * 1024
def generate_bytes():
chunks = bytearray()
for i in xrange(n):
chunks.append(random.randint(0, 255))
if len(chunks) == chunk_size:
yield (bytes(chunks))
chunks = bytearray()
if chunks:
yield (bytes(chunks))
headers = {"Content-Type": "application/octet-stream"}
return Response(generate_bytes(), headers=headers)
@app.route("/range/")
def range_request(numbytes):
"""Streams n random bytes generated with given seed, at given chunk size per packet.
---
tags:
- Dynamic data
parameters:
- in: path
name: numbytes
type: int
produces:
- application/octet-stream
responses:
200:
description: Bytes.
"""
if numbytes <= 0 or numbytes > (100 * 1024):
response = Response(
headers={"ETag": "range%d" % numbytes, "Accept-Ranges": "bytes"}
)
response.status_code = 404
response.data = "number of bytes must be in the range (0, 102400]"
return response
params = CaseInsensitiveDict(request.args.items())
if "chunk_size" in params:
chunk_size = max(1, int(params["chunk_size"]))
else:
chunk_size = 10 * 1024
duration = float(params.get("duration", 0))
pause_per_byte = duration / numbytes
request_headers = get_headers()
first_byte_pos, last_byte_pos = get_request_range(request_headers, numbytes)
range_length = (last_byte_pos + 1) - first_byte_pos
if (
first_byte_pos > last_byte_pos
or first_byte_pos not in xrange(0, numbytes)
or last_byte_pos not in xrange(0, numbytes)
):
response = Response(
headers={
"ETag": "range%d" % numbytes,
"Accept-Ranges": "bytes",
"Content-Range": "bytes */%d" % numbytes,
"Content-Length": "0",
}
)
response.status_code = 416
return response
def generate_bytes():
chunks = bytearray()
for i in xrange(first_byte_pos, last_byte_pos + 1):
# We don't want the resource to change across requests, so we need
# to use a predictable data generation function
chunks.append(ord("a") + (i % 26))
if len(chunks) == chunk_size:
yield (bytes(chunks))
time.sleep(pause_per_byte * chunk_size)
chunks = bytearray()
if chunks:
time.sleep(pause_per_byte * len(chunks))
yield (bytes(chunks))
content_range = "bytes %d-%d/%d" % (first_byte_pos, last_byte_pos, numbytes)
response_headers = {
"Content-Type": "application/octet-stream",
"ETag": "range%d" % numbytes,
"Accept-Ranges": "bytes",
"Content-Length": str(range_length),
"Content-Range": content_range,
}
response = Response(generate_bytes(), headers=response_headers)
if (first_byte_pos == 0) and (last_byte_pos == (numbytes - 1)):
response.status_code = 200
else:
response.status_code = 206
return response
@app.route("/links//")
def link_page(n, offset):
"""Generate a page containing n links to other pages which do the same.
---
tags:
- Dynamic data
parameters:
- in: path
name: n
type: int
- in: path
name: offset
type: int
produces:
- text/html
responses:
200:
description: HTML links.
"""
n = min(max(1, n), 200) # limit to between 1 and 200 links
link = "{1} "
html = ["Links"]
for i in xrange(n):
if i == offset:
html.append("{0} ".format(i))
else:
html.append(link.format(url_for("link_page", n=n, offset=i), i))
html.append("")
return "".join(html)
@app.route("/links/")
def links(n):
"""Redirect to first links page."""
return redirect(url_for("link_page", n=n, offset=0))
@app.route("/image")
def image():
"""Returns a simple image of the type suggest by the Accept header.
---
tags:
- Images
produces:
- image/webp
- image/svg+xml
- image/jpeg
- image/png
- image/*
responses:
200:
description: An image.
"""
headers = get_headers()
if "accept" not in headers:
return image_png() # Default media type to png
accept = headers["accept"].lower()
if "image/webp" in accept:
return image_webp()
elif "image/svg+xml" in accept:
return image_svg()
elif "image/jpeg" in accept:
return image_jpeg()
elif "image/png" in accept or "image/*" in accept:
return image_png()
else:
return status_code(406) # Unsupported media type
@app.route("/image/png")
def image_png():
"""Returns a simple PNG image.
---
tags:
- Images
produces:
- image/png
responses:
200:
description: A PNG image.
"""
data = resource("images/pig_icon.png")
return Response(data, headers={"Content-Type": "image/png"})
@app.route("/image/jpeg")
def image_jpeg():
"""Returns a simple JPEG image.
---
tags:
- Images
produces:
- image/jpeg
responses:
200:
description: A JPEG image.
"""
data = resource("images/jackal.jpg")
return Response(data, headers={"Content-Type": "image/jpeg"})
@app.route("/image/webp")
def image_webp():
"""Returns a simple WEBP image.
---
tags:
- Images
produces:
- image/webp
responses:
200:
description: A WEBP image.
"""
data = resource("images/wolf_1.webp")
return Response(data, headers={"Content-Type": "image/webp"})
@app.route("/image/svg")
def image_svg():
"""Returns a simple SVG image.
---
tags:
- Images
produces:
- image/svg+xml
responses:
200:
description: An SVG image.
"""
data = resource("images/svg_logo.svg")
return Response(data, headers={"Content-Type": "image/svg+xml"})
def resource(filename):
path = os.path.join(tmpl_dir, filename)
with open(path, "rb") as f:
return f.read()
@app.route("/xml")
def xml():
"""Returns a simple XML document.
---
tags:
- Response formats
produces:
- application/xml
responses:
200:
description: An XML document.
"""
response = make_response(render_template("sample.xml"))
response.headers["Content-Type"] = "application/xml"
return response
@app.route("/json")
def a_json_endpoint():
"""Returns a simple JSON document.
---
tags:
- Response formats
produces:
- application/json
responses:
200:
description: An JSON document.
"""
return flask_jsonify(
slideshow={
"title": "Sample Slide Show",
"date": "date of publication",
"author": "Yours Truly",
"slides": [
{"type": "all", "title": "Wake up to WonderWidgets!"},
{
"type": "all",
"title": "Overview",
"items": [
"Why WonderWidgets are great",
"Who buys WonderWidgets",
],
},
],
}
)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--port", type=int, default=5000)
parser.add_argument("--host", default="127.0.0.1")
args = parser.parse_args()
app.run(port=args.port, host=args.host)