httpbin-0.4.0/0000755000076500000240000000000012612231410013412 5ustar kevinstaff00000000000000httpbin-0.4.0/AUTHORS0000644000076500000240000000111312612231203014456 0ustar kevinstaff00000000000000HttpBin is written and maintained by Kenneth Reitz and various contributors: Development Lead ```````````````` - Kenneth Reitz <_@kennethreitz.com> Patches and Suggestions ``````````````````````` - Zbigniew Siciarz - Andrey Petrov - Lispython - Kyle Conroy - Flavio Percoco - Radomir Stevanovic (http://github.com/randomir) - Steven Honson - Bob Carroll @rcarz - Cory Benfield (Lukasa) - Matt Robenolt (https://github.com/mattrobenolt) - Dave Challis (https://github.com/davechallis) - Florian Bruhin (https://github.com/The-Compiler) httpbin-0.4.0/httpbin/0000755000076500000240000000000012612231410015062 5ustar kevinstaff00000000000000httpbin-0.4.0/httpbin/__init__.py0000644000076500000240000000005512612231203017173 0ustar kevinstaff00000000000000# -*- coding: utf-8 -*- from .core import * httpbin-0.4.0/httpbin/core.py0000644000076500000240000005011112612231203016362 0ustar kevinstaff00000000000000# -*- coding: utf-8 -*- """ httpbin.core ~~~~~~~~~~~~ This module provides the core HttpBin experience. """ import base64 import json import os import random import time import uuid from flask import Flask, Response, request, render_template, redirect, jsonify as flask_jsonify, make_response, url_for from werkzeug.datastructures import WWWAuthenticate, MultiDict from werkzeug.http import http_date from werkzeug.wrappers import BaseResponse from six.moves import range as xrange 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 from .utils import weighted_choice from .structures import CaseInsensitiveDict 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) # 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 # ----------- @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('/') def view_landing_page(): """Generates Landing Page.""" tracking_enabled = 'HTTPBIN_TRACKING' in os.environ return render_template('index.html', tracking_enabled=tracking_enabled) @app.route('/html') def view_html_page(): """Simple Html Page""" return render_template('moby.html') @app.route('/robots.txt') def view_robots_page(): """Simple Html Page""" response = make_response() response.data = ROBOT_TXT response.content_type = "text/plain" return response @app.route('/deny') def view_deny_page(): """Simple Html Page""" 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 Origin IP.""" return jsonify(origin=request.headers.get('X-Forwarded-For', request.remote_addr)) @app.route('/headers') def view_headers(): """Returns HTTP HEADERS.""" return jsonify(get_dict('headers')) @app.route('/user-agent') def view_user_agent(): """Returns User-Agent.""" headers = get_headers() return jsonify({'user-agent': headers['user-agent']}) @app.route('/get', methods=('GET',)) def view_get(): """Returns GET Data.""" return jsonify(get_dict('url', 'args', 'headers', 'origin')) @app.route('/post', methods=('POST',)) def view_post(): """Returns POST Data.""" return jsonify(get_dict( 'url', 'args', 'form', 'data', 'origin', 'headers', 'files', 'json')) @app.route('/put', methods=('PUT',)) def view_put(): """Returns PUT Data.""" return jsonify(get_dict( 'url', 'args', 'form', 'data', 'origin', 'headers', 'files', 'json')) @app.route('/patch', methods=('PATCH',)) def view_patch(): """Returns PATCH Data.""" return jsonify(get_dict( 'url', 'args', 'form', 'data', 'origin', 'headers', 'files', 'json')) @app.route('/delete', methods=('DELETE',)) def view_delete(): """Returns DELETE Data.""" 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.""" 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.""" return jsonify(get_dict( 'origin', 'headers', method=request.method, deflated=True)) @app.route('/redirect/') def redirect_n_times(n): """302 Redirects n times.""" 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') def redirect_to(): """302 Redirects to the given URL.""" args = CaseInsensitiveDict(request.args.items()) # 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 response.headers['Location'] = args['url'].encode('utf-8') return response @app.route('/relative-redirect/') def relative_redirect_n_times(n): """302 Redirects n times.""" 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): """302 Redirects n times.""" 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 messages""" 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""" if not ',' in codes: code = int(codes) return status_code(code) choices = [] for choice in codes.split(','): if not ':' in choice: code = choice weight = 1 else: code, weight = choice.split(':') choices.append((int(code), float(weight))) code = weighted_choice(choices) return status_code(code) @app.route('/response-headers') def response_headers(): """Returns a set of response headers from the query string """ headers = MultiDict(request.args.items(multi=True)) response = jsonify(headers.lists()) while True: content_len_shown = response.headers['Content-Length'] 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) if response.headers['Content-Length'] == content_len_shown: break return response @app.route('/cookies') def view_cookies(hide_env=True): """Returns cookie data.""" 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.""" 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.""" 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.""" 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.""" 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.""" if not check_basic_auth(user, passwd): return status_code(404) return jsonify(authenticated=True, user=user) @app.route('/digest-auth///') def digest_auth(qop=None, user='user', passwd='passwd'): """Prompts the user for authorization using HTTP Digest auth""" if qop not in ('auth', 'auth-int'): qop = None if 'Authorization' not in request.headers or \ not check_digest_auth(user, passwd) or \ not 'Cookie' in request.headers: response = app.make_response('') response.status_code = 401 # RFC2616 Section4.2: HTTP headers are ASCII. That means # request.remote_addr was originally ASCII, so I should be able to # encode it back to ascii. Also, RFC2617 says about nonces: "The # contents of the nonce are implementation dependent" nonce = H(b''.join([ getattr(request,'remote_addr',u'').encode('ascii'), b':', str(time.time()).encode('ascii'), b':', os.urandom(10) ])) opaque = H(os.urandom(10)) auth = WWWAuthenticate("digest") auth.set_digest('me@kennethreitz.com', nonce, opaque=opaque, qop=('auth', 'auth-int') if qop is None else (qop, )) response.headers['WWW-Authenticate'] = auth.to_header() response.headers['Set-Cookie'] = 'fake=fake_value' return response return jsonify(authenticated=True, user=user) @app.route('/delay/') def delay_response(delay): """Returns a delayed response""" delay = min(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.""" args = CaseInsensitiveDict(request.args.items()) duration = float(args.get('duration', 2)) numbytes = int(args.get('numbytes', 10)) code = int(args.get('code', 200)) pause = duration / numbytes delay = float(args.get('delay', 0)) if delay > 0: time.sleep(delay) def generate_bytes(): for i in xrange(numbytes): yield u"*".encode('utf-8') 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""" encoded = value.encode('utf-8') # base64 expects binary string as input return base64.urlsafe_b64decode(encoded).decode('utf-8') @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.""" 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('/cache/') def cache_control(value): """Sets a Cache-Control header.""" response = view_get() response.headers['Cache-Control'] = 'public, max-age={0}'.format(value) return response @app.route('/encoding/utf8') def encoding(): return render_template('UTF-8-demo.txt') @app.route('/bytes/') def random_bytes(n): """Returns n random bytes generated with given seed.""" 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.""" 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.""" 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, 10240]' 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) 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 }) 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-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.""" 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.""" 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(): data = resource('images/pig_icon.png') return Response(data, headers={'Content-Type': 'image/png'}) @app.route('/image/jpeg') def image_jpeg(): data = resource('images/jackal.jpg') return Response(data, headers={'Content-Type': 'image/jpeg'}) @app.route('/image/webp') def image_webp(): data = resource('images/wolf_1.webp') return Response(data, headers={'Content-Type': 'image/webp'}) @app.route('/image/svg') def image_svg(): 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) return open(path, 'rb').read() @app.route("/xml") def xml(): response = make_response(render_template("sample.xml")) response.headers["Content-Type"] = "application/xml" return response if __name__ == '__main__': app.run() httpbin-0.4.0/httpbin/filters.py0000644000076500000240000000337512612231203017114 0ustar kevinstaff00000000000000# -*- coding: utf-8 -*- """ httpbin.filters ~~~~~~~~~~~~~~~ This module provides response filter decorators. """ import gzip as gzip2 import zlib from six import BytesIO from decimal import Decimal from time import time as now from decorator import decorator from flask import Flask, Response app = Flask(__name__) @decorator def x_runtime(f, *args, **kwargs): """X-Runtime Flask Response Decorator.""" _t0 = now() r = f(*args, **kwargs) _t1 = now() r.headers['X-Runtime'] = '{0}s'.format(Decimal(str(_t1 - _t0))) return r @decorator def gzip(f, *args, **kwargs): """GZip Flask Response Decorator.""" data = f(*args, **kwargs) if isinstance(data, Response): content = data.data else: content = data gzip_buffer = BytesIO() gzip_file = gzip2.GzipFile( mode='wb', compresslevel=4, fileobj=gzip_buffer ) gzip_file.write(content) gzip_file.close() gzip_data = gzip_buffer.getvalue() if isinstance(data, Response): data.data = gzip_data data.headers['Content-Encoding'] = 'gzip' data.headers['Content-Length'] = str(len(data.data)) return data return gzip_data @decorator def deflate(f, *args, **kwargs): """Deflate Flask Response Decorator.""" data = f(*args, **kwargs) if isinstance(data, Response): content = data.data else: content = data deflater = zlib.compressobj() deflated_data = deflater.compress(content) deflated_data += deflater.flush() if isinstance(data, Response): data.data = deflated_data data.headers['Content-Encoding'] = 'deflate' data.headers['Content-Length'] = str(len(data.data)) return data return deflated_data httpbin-0.4.0/httpbin/helpers.py0000644000076500000240000002646612612231203017114 0ustar kevinstaff00000000000000# -*- coding: utf-8 -*- """ httpbin.helpers ~~~~~~~~~~~~~~~ This module provides helper functions for httpbin. """ import json import base64 from hashlib import md5 from werkzeug.http import parse_authorization_header from flask import request, make_response from six.moves.urllib.parse import urlparse, urlunparse from .structures import CaseInsensitiveDict ASCII_ART = """ -=[ teapot ]=- _...._ .' _ _ `. | ."` ^ `". _, \_;`"---"`|// | ;/ \_ _/ `\"\"\"` """ REDIRECT_LOCATION = '/redirect/1' ENV_HEADERS = ( 'X-Varnish', 'X-Request-Start', 'X-Heroku-Queue-Depth', 'X-Real-Ip', 'X-Forwarded-Proto', 'X-Forwarded-Protocol', 'X-Forwarded-Ssl', 'X-Heroku-Queue-Wait-Time', 'X-Forwarded-For', 'X-Heroku-Dynos-In-Use', 'X-Forwarded-For', 'X-Forwarded-Protocol', 'X-Forwarded-Port', 'Runscope-Service' ) ROBOT_TXT = """User-agent: * Disallow: /deny """ ACCEPTED_MEDIA_TYPES = [ 'image/webp', 'image/svg+xml', 'image/jpeg', 'image/png', 'image/*' ] ANGRY_ASCII =""" .-''''''-. .' _ _ '. / O O \\ : : | | : __ : \ .-"` `"-. / '. .' '-......-' YOU SHOULDN'T BE HERE """ def json_safe(string, content_type='application/octet-stream'): """Returns JSON-safe version of `string`. If `string` is a Unicode string or a valid UTF-8, it is returned unmodified, as it can safely be encoded to JSON string. If `string` contains raw/binary data, it is Base64-encoded, formatted and returned according to "data" URL scheme (RFC2397). Since JSON is not suitable for binary data, some additional encoding was necessary; "data" URL scheme was chosen for its simplicity. """ try: string = string.decode('utf-8') _encoded = json.dumps(string) return string except (ValueError, TypeError): return b''.join([ b'data:', content_type.encode('utf-8'), b';base64,', base64.b64encode(string) ]).decode('utf-8') def get_files(): """Returns files dict from request context.""" files = dict() for k, v in request.files.items(): content_type = request.files[k].content_type or 'application/octet-stream' val = json_safe(v.read(), content_type) if files.get(k): if not isinstance(files[k], list): files[k] = [files[k]] files[k].append(val) else: files[k] = val return files def get_headers(hide_env=True): """Returns headers dict from request context.""" headers = dict(request.headers.items()) if hide_env and ('show_env' not in request.args): for key in ENV_HEADERS: try: del headers[key] except KeyError: pass return CaseInsensitiveDict(headers.items()) def semiflatten(multi): """Convert a MutiDict into a regular dict. If there are more than one value for a key, the result will have a list of values for the key. Otherwise it will have the plain value.""" if multi: result = multi.to_dict(flat=False) for k, v in result.items(): if len(v) == 1: result[k] = v[0] return result else: return multi def get_url(request): """ Since we might be hosted behind a proxy, we need to check the X-Forwarded-Proto, X-Forwarded-Protocol, or X-Forwarded-SSL headers to find out what protocol was used to access us. """ protocol = request.headers.get('X-Forwarded-Proto') or request.headers.get('X-Forwarded-Protocol') if protocol is None and request.headers.get('X-Forwarded-Ssl') == 'on': protocol = 'https' if protocol is None: return request.url url = list(urlparse(request.url)) url[0] = protocol return urlunparse(url) def get_dict(*keys, **extras): """Returns request dict of given keys.""" _keys = ('url', 'args', 'form', 'data', 'origin', 'headers', 'files', 'json') assert all(map(_keys.__contains__, keys)) data = request.data form = request.form form = semiflatten(request.form) try: _json = json.loads(data.decode('utf-8')) except (ValueError, TypeError): _json = None d = dict( url=get_url(request), args=semiflatten(request.args), form=form, data=json_safe(data), origin=request.headers.get('X-Forwarded-For', request.remote_addr), headers=get_headers(), files=get_files(), json=_json ) out_d = dict() for key in keys: out_d[key] = d.get(key) out_d.update(extras) return out_d def status_code(code): """Returns response object of given status code.""" redirect = dict(headers=dict(location=REDIRECT_LOCATION)) code_map = { 301: redirect, 302: redirect, 303: redirect, 304: dict(data=''), 305: redirect, 307: redirect, 401: dict(headers={'WWW-Authenticate': 'Basic realm="Fake Realm"'}), 402: dict( data='Fuck you, pay me!', headers={ 'x-more-info': 'http://vimeo.com/22053820' } ), 406: dict(data=json.dumps({ 'message': 'Client did not request a supported media type.', 'accept': ACCEPTED_MEDIA_TYPES }), headers={ 'Content-Type': 'application/json' }), 407: dict(headers={'Proxy-Authenticate': 'Basic realm="Fake Realm"'}), 418: dict( # I'm a teapot! data=ASCII_ART, headers={ 'x-more-info': 'http://tools.ietf.org/html/rfc2324' } ), } r = make_response() r.status_code = code if code in code_map: m = code_map[code] if 'data' in m: r.data = m['data'] if 'headers' in m: r.headers = m['headers'] return r def check_basic_auth(user, passwd): """Checks user authentication using HTTP Basic Auth.""" auth = request.authorization return auth and auth.username == user and auth.password == passwd # Digest auth helpers # qop is a quality of protection def H(data): return md5(data).hexdigest() def HA1(realm, username, password): """Create HA1 hash by realm, username, password HA1 = md5(A1) = MD5(username:realm:password) """ if not realm: realm = u'' return H(b":".join([username.encode('utf-8'), realm.encode('utf-8'), password.encode('utf-8')])) def HA2(credentails, request): """Create HA2 md5 hash If the qop directive's value is "auth" or is unspecified, then HA2: HA2 = md5(A2) = MD5(method:digestURI) If the qop directive's value is "auth-int" , then HA2 is HA2 = md5(A2) = MD5(method:digestURI:MD5(entityBody)) """ if credentails.get("qop") == "auth" or credentails.get('qop') is None: return H(b":".join([request['method'].encode('utf-8'), request['uri'].encode('utf-8')])) elif credentails.get("qop") == "auth-int": for k in 'method', 'uri', 'body': if k not in request: raise ValueError("%s required" % k) return H("%s:%s:%s" % (request['method'], request['uri'], H(request['body']))) raise ValueError def response(credentails, password, request): """Compile digest auth response If the qop directive's value is "auth" or "auth-int" , then compute the response as follows: RESPONSE = MD5(HA1:nonce:nonceCount:clienNonce:qop:HA2) Else if the qop directive is unspecified, then compute the response as follows: RESPONSE = MD5(HA1:nonce:HA2) Arguments: - `credentails`: credentails dict - `password`: request user password - `request`: request dict """ response = None HA1_value = HA1( credentails.get('realm'), credentails.get('username'), password ) HA2_value = HA2(credentails, request) if credentails.get('qop') is None: response = H(b":".join([ HA1_value.encode('utf-8'), credentails.get('nonce', '').encode('utf-8'), HA2_value.encode('utf-8') ])) elif credentails.get('qop') == 'auth' or credentails.get('qop') == 'auth-int': for k in 'nonce', 'nc', 'cnonce', 'qop': if k not in credentails: raise ValueError("%s required for response H" % k) response = H(b":".join([HA1_value.encode('utf-8'), credentails.get('nonce').encode('utf-8'), credentails.get('nc').encode('utf-8'), credentails.get('cnonce').encode('utf-8'), credentails.get('qop').encode('utf-8'), HA2_value.encode('utf-8')])) else: raise ValueError("qop value are wrong") return response def check_digest_auth(user, passwd): """Check user authentication using HTTP Digest auth""" if request.headers.get('Authorization'): credentails = parse_authorization_header(request.headers.get('Authorization')) if not credentails: return response_hash = response(credentails, passwd, dict(uri=request.script_root + request.path, body=request.data, method=request.method)) if credentails.get('response') == response_hash: return True return False def secure_cookie(): """Return true if cookie should have secure attribute""" return request.environ['wsgi.url_scheme'] == 'https' def __parse_request_range(range_header_text): """ Return a tuple describing the byte range requested in a GET request If the range is open ended on the left or right side, then a value of None will be set. RFC7233: http://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7233.html#header.range Examples: Range : bytes=1024- Range : bytes=10-20 Range : bytes=-999 """ left = None right = None if not range_header_text: return left, right range_header_text = range_header_text.strip() if not range_header_text.startswith('bytes'): return left, right components = range_header_text.split("=") if len(components) != 2: return left, right components = components[1].split("-") try: right = int(components[1]) except: pass try: left = int(components[0]) except: pass return left, right def get_request_range(request_headers, upper_bound): first_byte_pos, last_byte_pos = __parse_request_range(request_headers['range']) if first_byte_pos is None and last_byte_pos is None: # Request full range first_byte_pos = 0 last_byte_pos = upper_bound - 1 elif first_byte_pos is None: # Request the last X bytes first_byte_pos = max(0, upper_bound - last_byte_pos) last_byte_pos = upper_bound - 1 elif last_byte_pos is None: # Request the last X bytes last_byte_pos = upper_bound - 1 return first_byte_pos, last_byte_pos httpbin-0.4.0/httpbin/structures.py0000644000076500000240000000125512612231203017662 0ustar kevinstaff00000000000000# -*- coding: utf-8 -*- """ httpbin.structures ~~~~~~~~~~~~~~~~~~~ Data structures that power httpbin. """ class CaseInsensitiveDict(dict): """Case-insensitive Dictionary for headers. For example, ``headers['content-encoding']`` will return the value of a ``'Content-Encoding'`` response header. """ def _lower_keys(self): return [str.lower(k) for k in self.keys()] def __contains__(self, key): return key.lower() in self._lower_keys() def __getitem__(self, key): # We allow fall-through here, so values default to None if key in self: return list(self.items())[self._lower_keys().index(key.lower())][1] httpbin-0.4.0/httpbin/templates/0000755000076500000240000000000012612231410017060 5ustar kevinstaff00000000000000httpbin-0.4.0/httpbin/templates/httpbin.1.html0000644000076500000240000002300512612231203021555 0ustar kevinstaff00000000000000

httpbin(1): HTTP Request & Response Service

Freely hosted in HTTP, HTTPS & EU flavors by Runscope

ENDPOINTS

DESCRIPTION

Testing an HTTP Library can become difficult sometimes. RequestBin 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.

All endpoint responses are JSON-encoded.

EXAMPLES

$ curl http://httpbin.org/ip

{"origin": "24.127.96.129"}

$ curl http://httpbin.org/user-agent

{"user-agent": "curl/7.19.7 (universal-apple-darwin10.0) libcurl/7.19.7 OpenSSL/0.9.8l zlib/1.2.3"}

$ curl http://httpbin.org/get

{
   "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": "http://httpbin.org/get"
}

$ curl -I http://httpbin.org/status/418

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

$ curl https://httpbin.org/get?show_env=1

{
  "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": "http://httpbin.org/get?show_env=1"
}

Installing and running from PyPI

You can install httpbin as a library from PyPI and run it as a WSGI app. For example, using Gunicorn:

$ pip install httpbin
$ gunicorn httpbin:app

Changelog

  • 0.2.0: Added an XML endpoint. Also fixes several bugs with unicode, CORS headers, digest auth, and more.
  • 0.1.2: Fix a couple Python3 bugs with the random byte endpoints, fix a bug when uploading files without a Content-Type header set.
  • 0.1.1: Added templates as data in setup.py
  • 0.1.0: Added python3 support and (re)publish on PyPI

AUTHOR

A Runscope Community Project.

Originally created by Kenneth Reitz.

SEE ALSO

Hurl.it - Make HTTP requests.

RequestBin - Inspect HTTP requests.

http://python-requests.org

httpbin-0.4.0/httpbin/templates/images/0000755000076500000240000000000012612231410020325 5ustar kevinstaff00000000000000httpbin-0.4.0/httpbin/templates/images/jackal.jpg0000644000076500000240000010540412612231203022260 0ustar kevinstaff00000000000000JFIF5Edited by Paul Sherman for WPClipart, Public DomainC     C    M!1#A"$Q 34a2DqBCST%cdr5Rbs&Ut5 !1AQaq"2#3C$ ?bxs}g\QDK]uUMRKͯʹ#Q"|ՙ+)+3,`3|)h>Wn$k~ $~¿ R%htK4,*m,&j>(w}=oI׻k~DaJ?qS O>T%Nwo rq8 8AApŒfIa8Q[D[$UW^ZJss&5\v<;7~J. \+P|7iDYh ReUkDq+=&L;.i<%F#q[qꪋhOsisrv'3mFLl"R-Z1TE^fWTY;ӾU*xz! H@b-mDe2Ɇuǟ+ c߫~o5؂_օ<3$x\Kx6S*&"/|}|l8w!|T8Uf`ȓé^׺AUEZjVq'2.D]y+>x\&""jH*s+z&1\g-8ĪJxiNh}U<%}pLa=؝WHL~y?M -ơIbB`1ls˟ "cL*juO]G}GkbuO\hdU"34aQ\_vj,`nU[ʍSIN.NoޠFb j_Sv*8 GxS$75rlcR|9³u6c%XTt)mF Sw=F찘]0Ҫ{3ۉ|ObNvK|vl{@jӢi3R%?2.㊃Q'P63 BWw;|]ӻl*qQqRrLxl9=& nˑFGvG^FfذۥE)F[*sTO:*c3NjUJ#q{l4$|y{hBY`^]ULhq??'z*rpJ Y]i荲q4Q>auMvxc1f?sQ^3@|JntDF-[|frJ&S 5`Wĸ܎rc~yi(NT>+<-8K+E "1ܡDO>D|Ջ~Z%¥^'qͮ2nM[ӏd_v,}D/wZuEC[٣yQYjykZx]kVojTQwsxCߜnaiV%+GVrU|_Uϕύ1)K]ws_Zu*wĘ*o[J *]=/5qAE*T'^-oy+'rvt4}qAV<{cmV9 Tʘ%mpĨ2_UƞhvƋBߪ"߶A9T%EB>UE®SnfΚxbZNaBۣ")rm° G']_`k2KR`3;.a@a'Q i_>=_)kkYQHB>}+ N@_3gM4Q?X1Lc;1Bț . ȕ:&|h @Y{HUWqi؈F~KHp&(hէ/\Ol:[*z0=$#MM>sJoϟ>3Us[aJf☸fbt] QpLc}\sQ6݉M-4:VWELj덴2@:S L}|Tc`ǶʅyhH$КD|-? ο`*70}nl $hiq1q<TXe6weGLU*3\tb16Ha^-(jݴ.U[䅪jG.Ueʮtaźck휼MXzMשtǴ}*W k6hbކ+QU=v57S3h ݢo7l0P#ӢTsLjaI_n ' # R&W_SEbIUBo 9˚%(vSAUtOe&ţTŽk+EMnvnS Fzi Kv/r&ݻhe"=u#;̱}=ڿ՘7ғrtwaX4)uDE,맺Olqm{~hʘ㠡HxAapꊩ26= c22HbkX%XBURWED>qU?SjohZ@De$,S1[Tc6ZwD]Y-2F_Հ_Lg_O:/bUĸ+M{™%$7tW ˨"h=U>_F$&.A۲An ߘ4?O姝Wٖ8)bر]竰8 z 0gvgsD_ބe,*t*aW(O_?6K:Q& -%.dbaz1cMqIE zELZ$\'YYyO}̮ >Ǖ:@O$qU];3u25`9[6ZKiGWZ}SVQrϷxH*h$@Qv@xCj`aa|L*&tkܕR[ / q3֕`Yոy ԓ )R&WWΪTفKfYճhfFӔeMx _aW0L#vm&鸬2$^ߗUPwEETg&=l`\M߲ed}[BOT Y{*?ڢyMlWnT!S8PtOW?snÏUj FzSWU]> NK-鈦C}a*B#jtOwp12Ƙ8[=tcCOߝO>NqU,s7~2=ʣڡfi_2c6: mWyc~ug]rkձDj:Ҭi:GbTP 3)AnX*I8&q2O]6 UԈVdͽGꃔEELbEMu|+1ZpZ5wz^jc-.)<~~f(SktQ^7ګbރR#o $2W26.Wc *1uUwrLîGG9l*4*T*.S&4ˌܔu!M42Bdutgݫ}OTmpoz TWڍg/ /#}kEnPZsx<.-* $Sm /՝l4żEUzrHU?T]8T.3DWk&KA&>0'q|~MꀒaSm;p1GtQEƙdƨ !7$,ްFaTvH2xWTBa3v_|M"Liwǚc?JȾ0-µ ٤󌡹8u,9"*ze>Wan]Ɍ(ita:ceqg0_vTTfo [X$"z 0&' uS8( )]pJ&=C[N=g":j77U<]p>:1WT5=iQ?MЪ3w44߰ Ƶ-vZ0M/~0Sܢ*y}?Df}wi((-h\62VUUS4OoFk9))i7S1#D0AիFB9{|l[> B"wz\>A *ʠz*U.gŹs4 AAy;TG^&;+*lD*S:y 9K0dD k9_"*1vdoMEIUF-/0גtF}gx?Lm=Dd%- InݥdtwyڤP2k4xۭ"XM"]^WTM}m L4;{͌JsՐP2c:hw7iWqV}Mƾ(T.U3)+_T n*|@!T5<Sszը$yΑ >jVݠ5-N5Jn]*+yGbX,&6r#\bom^{ћrl!nV)&d2u/c̛pgFLS+mKlSvfe]fL.'{*"sF;E0<;PeS =¸ vTqWB "9BV _L}abjuyq4Q\HYC9kI~"&T|"5/k)i1f*"쉚&& `GiT\jE.PP\ Sh k 7CF)!ԅ!r>_]@\q-9vnZbضiGfk22._+쨻Z&D+@y+PmHNq谋X= |uv PhRĘw.[fJTJWꖆ_aT}fGC5wLwjB5;BC{n; Dp$}L7v@@Cqmg)\c) ڱ/3qۈxތ7k89T\dZ3-n֯ۇ0# dCʌ.> }a'8x"xc O 'lwYD**?ZADsʪc>lGѢ[޸3wܙta̫ڛIh/XCcXDcϏ8j`~KI R**ܹa.K6Ӳ$xv*C^0| j?׫4&S³݅"M0#델ͤδ)?OB=|*clSH|y- l0{vdqVԄ[ 01@%1ETU~:__f*ap;\/s\".Pwq6aWʴ+t[6{g[aIMO%_lD]YC) deZn6 CU>z#ã2>^ϗh_̬h%Tmݺf 艟B*azԣAu*TdU_ʪt7as g_p\{ӃEbBz &=܎ ι mlFSž(f$nP\{Rߴ ԹIѰX#Q|ToX1@gzj H(qh"G"ϓ!iA{r:<ދhT(i6IWw#'PI'-+y*%A'H~Ga[8]sS$sDlƦY50H[">Jb B[)aED5Q;ёݑ]q Alo ~FѭiO]zÐTwG(@7XާxLnUE3ITzj> _a֪JA}qO_("d) DNg[}D|m4Jrpv +XGԊLP`OD>o n'u_Q]Sl]kƭ)T)UhDu\U2*=w;Rzb-~cp "L I,՚UF2L*]o&(8%+}J`#W%A)J֮|,m'?FTI`ŽĆh~$8ҬC',&ImPb觯Щ^ ^9}I~4T J, FI)Źpu}n:nΦhuc9H榚Eo,G>zxl$nn8(Cuո!7ea2mGGQ(:)az X_c:&<,nRlV/nN]>cz8$bV[r+ 1MPnaqM7nHTi:Js#jȦ(ѕI]r?mx1CFNDPAaDZ4d @U h_6 !piff]̍U*u}S§쳘LBih1}]34II1XZ?T[2~w,W)W RUHWGJ#S^0cUTq=qW77=ݼV#VyBnݮ(Τ&QGD%5¯ÌKKHuwnޖ nKhi}_H_}}0t]4z~ON*邿mwF+ gp'owTcc;^YDu}>j,h>+*K,F8)R -JsB I]B}ܴf 9 xki T1cW/paPR7n7@j8gm_|V𥮘m5" [q jsw4<ժm5̗}r˳\4՘0>ړn7/oM}뤓LET:tׯ ZgTrL=.*4i/tPWG 6LYzPF_~'RWyu #tdF.)^hRkL&F<],5vҗ\̱ũtthYR/Qe AяǰuAܬ)nU{INlhZl۵@!b8vc+0u Qc;=Dtȱ7s̖+AۢV[(QYOe'}1=Yh6 j,9-װɰ8+<ʫ_pD2yn|VXr~di!C~*ea9"+W>#Z݌+[;C:+ s>"L|لBQf"@٠mgD)WMkb6*k4F7˥Inǁ.l{QӯyaAVTR]hc(^y^񢯔Aj7d|y&45sDlbHchNX&o>07}#T >, ?H[aXtyC,Bʊc+\Ҫ5_@a;ᨼVt7rλf=&GNjq"(SB+^W.a;nR!I+ qFʮkk \*ꞏuȾ~kk޿7ϳewxBΘ:?KCaGJks[|*l0.ugTaFK.q8gk LRH˪.Ճz C@ut򱮙&cg,y:J AA&z" 0Q?0:.`5:9hwrT<%²>QG_"g!crLP~s{$sdb M% \UD_D<.S\66'/{$fqhc9WٱUvRTzO}bhVDJr7m+Otb9O4*Bd$ha_cOf @expgu⭎ɮPlm]}G*%;v9Վ"gDARE܄`;owbNf t};.QFJUVdZ.V fo{v4_UOԎ}%%5U{k_ay(骦 Z hPsL?Vs"~Z8"Cq댜rCk:%Zת9yds[v 6+ koGv^=v޺ja*Qyo)T^v'DXA'Wb\^ANlٕry1.+aj1z?m;IUScuڮBBr*T%1G2tgtEz ݃(n!erAa1t $}+QFŵlӤ軰m;y79w0-t{]'h^c^=6u2G+NC[7|13t# YJ+3i@Մ5v菸sսXJ0{9y,%  cڑ E98*7pI#ƓBۇũVh j,HW*qGKEhOJqjҤw{\jꗾvH#qYJih^4ֺQRN;Uuծj9}k@ " 2Ip[)7(~T+?\RH^y ڎUü .[b >e-zP#hGlJ?AUQiK fPss{Nsӣ"._p/?st"1$GR:jMJM46/5i21́c =q#nҠqgHåt4VE)DNE\ m)|I}{7pcH YQM@(u7MN޵DNkQwE }YU {'8nKVJY Dry&/}4lÇ8$<{oSq3,,Mj![A:r`EcUۿi{ud7,hIفX:Duc:]?{]t˫ؑ$ﻄgv*7ƥqJ^R=;݄=#\oؑGVȃ/KS)pY3V59q+2U6J&73* :b[ni[p:6: NkJ)f~%=$^ZuV)̐7w@ZTU]$HFRIQ̶ cȥsBxmVsJ<0b_19V,y$GDܑ'"Ȝ]tEtҟ}:ܫTM^JK˳7~ʩtbNihLUQk3&oA2 WnCKV9=D#xaҫZ:V=5]"xhGfLµ7_DuȥՍiAt[S\Jh VDEםv蚳J.z~*鈍>^ $aŦ\iRks ʴgKTlץu6/Ɯ\r4-'hIH1YQ\l"j=~ ǧ';ݟ{#Sć6SoG)7⣵ ! xu+~ /G!˳%|{.u.(ihsU^)#>3Goaqwd _7z"4ts@RZaߩuUr;uEꈞ6nXu@dpS;wr%mV-ab%!4]S\]jŠHߟu#,ɚj,(tz!Yuw8tm9TGMڋK:wy%/-4D^ꢋݵQssñBN&.x/oC7 nCI:DI F -nC4*e4;1x&Uz4AS2/PdIo>)HۄVOҾٖ#HU!Az""&(?!&,o9BԘ~,㎳:R^I-ڔq(V3̷DFj{ot׌P;^hY(hIVi&Gm5+\RQZggФ7}O{u)/FCH4)R[E5T5T FFQU1#n=C!gٳ۶¿ꐍfz nHTg(7i毦fkv¤;k ,w\=Ku.!i~- J1GDT_RDqA{}uڕ.k([nE gɢ+4]pbvC0]uվߩ91.5]r46#=BQ yF\d4aDlu"Ҝ#fX17ݡX%YL [rYUS&VuDO.2[sӟ̭4N pnPK!9J+VjVEQSg*ts| 7[Xq.ȶ_G*UR>t^T*SW!SKF?n~d"n.9 q*R-N:ý*֮^/1,KJdkokÉ%wy2-b!H~zKa :[NeR^Ĉ*gHcFcs 4>v B=0e'i+J0֋K8)mI&"[K3@̢$ңTVUk^e0*qEK[nꤦv>^8vv jۥmi=ltO+f7W6sm6K[/+m |tP)Q]K^<"**'=8Gv~[3D^Vp4/'hw|{aUO(>vLS!eP);~-[6սr&md*% *=58<?lw_MWacHiyZ2ΒܵōtlpnY1 peG*q~@vV4o9eEݟ26]7jL4|bGN~Ҧ%*Hx7|U2޷CFEV 8tI_,#O?ˑL~^rV~k*qu[fdNƪԗ jԕI>*bofmmJ%98{qxʆxcvAGS068洌VH7$"4$3ܨ* MSQeU9lpdѩ kߞ8Џ.̸y:t%Ն2cFRc5\~7w9N~J߲l78ns2SDWU]9ӍB KdH!G Q6]D b>$ n..i I0CpG \jntZӥ+u0LH ( MC}s|y>aO3 7-lxފ}syۯ10T]s,$Ua%P㎄dqO=U`&O3އMv*练`'CxЌ}p;$ gݹ6]w+pĆxw%.--bY՗2FF!\3Ou(6.Z~"w낤Z㞎mXB}i| *P)*2DڐR{KZM2ِ< teTE3bvfz uFTl8G[iqKiJ-P'"Gk&>r;ڗ -\}z֩z:/ >>ԭP]r,bD#Cb۔uǜfQ;v#aaEE맔d_9zf]^gKz+mV], g"*ņT|#OMv+T2|}Luq]/LA7Af]_*UU (+L\ʮS HH{OZC8K>HxA4"peD:7Z bkQpJe+iLBb䓣oS7Nբpl |,d‰;$ DӍ>)4@}!ܳe"vE1~Tp8#V0wgEsnx@S:L=DfHcԗ ڮ]I"rR޽5pۆ„{Smr?AꩁЌaͅ w*P+QU!'3p*$m-Po>U:5U Q48:L;u/ŒV&YMBKa\v^6z:B4w+KtL];FEmݲs].`g8à̎>Vrqqj%AsH^mWufn/8C$.2+0C㾄&}\j5S.#}g=S3VlU'Pԃo@r9C+S^l#-vYp'~e\Zlm 4+S0-?n{?鰣*9]*h(?)Gqo1wK&j4(B箺04,/"c?m 7&^iA,xngܾ)Ymm)Ly^pJE~Jl!CTPiPRmi}aOڎmܪ6!pE2jV_p W_DJ7 $NL l .Z qh]Q2PNSM_[qOo+ݗ9ߴa3ě2ijED5u]UԹpV֘J)E5"yTJZ2 rXs,~b15 ERMC͡u LuWylSq=;,zԈV܀Ur?d5/O2~6ҩ2$NISX4HF|u;ȉ"'igSwy!֗ .Zɴ3-9A:wsp~_S.vNw5?Renqqemf~4rQn TW0VÕ]LR峄w*t\BMNTïeuZgB/7Q~JQvɣM[5].χMob67[;M UʢaSֺ5W`6!hfᳯ(Ժ1h\2(vMkR診*J6b '41/ v[ܾؐ%F!L,8~.5JzVE¯joDF_}.wds7E7PD@[U]29ʴ[;oSˠ 0$߆}mH5PNE6٪Ξf3F[낁Shr^n{?}dݤC)#J!T2@׎tuHƺh<+aߪ ~~ wUCHۗ\ H}/`VGUttۡ\k\,m~B˄t mσPD)JDD_>QHȮFg)[tqAP6(ԼB>4\"iPJ< aI /[G0 B_G\|vqw]ԲmlM1K&݊0u됽 3=b84Ž~i]½"Q=]oᆰ2^>_)j,\rf̤$e1TՠfT2!81_LQfHH,WŶwQYy/F&5\:YqQNC.'nѪ:8ݒI޽)~z1'T{^1X4]-?mS[ܛf-O$sr$%S v{cf6'[0!{傐qo UaK@[__g|N$L#GT=!/7[=ZcHdQfW{En 2zK8]߲0QHHhSf)]9+JRVgiۑԆ?]WwHIV`@\IoĠ/ƣG?0mn|-79IJ펑!Ai+trZ_e^EFQNC}pҭ{L[)ɸ(rd hl7hgyOU(k҅VRm6yyfq;\cCL}ۖk}Mc*}4=zO_Q]C.Hi <}5rM$UH -J%2Bs:zzvXH .W|!}{D۳H]Gp? <>8Zv6ë:(T'P A=wx)xvQB$A')IѮ\\8V =A_yU*4ێ%TWCNǵ³: >Rdzn qް/c\df~FTWJXPdpuUqb^P&QynsNb%X`' "DzJn1uMrTgD7+u;ȹI1\~L^tF8#!!X g?}~ҟ|w|3CK0SpUꞯ.} oL?.5 Y6.N-H VчJ^ڑ֗ߧC RstUVm\RB`^U VUE|zWyQTA((]WiR0jC-7Qp@O.-cI9Q׸v"7~ RιWɯ}ΑS p"++RJ㒊ciR,5*&#*cyӑF=j:tIc r(O\Djp3|NnG#aw05]!r۲0ۿ+|t![QB,adyϹEZѭU|lk;H2 )UW ݍuf&)]يhu A #O?Tǡ%VL/ҭ[rd`:OAcWM4ۋW4ZMܻq!j䤶 0Z$zѷzޭF{9Cawn{}wOw Z[&5 nhTEZܪR[vjKr a&]I];&dHqd GDNŇ3 S_q>IK&/-KH*R8J &r0?D0D4]CB $5֣0@nDrGv>]/gAe]!FU7(Sƈ}:ƤxQWWL էĆ /Q<=iG58;ǑSkRcԪUZk"{9l/;ݦ%f/ N4]9-]V|1 VDϣ02۾]7u@^qv瓞EFQ)ԟ"t4c%}rB0& PwA-hgtWLjƞx\mƀ"vS7\cB<+ħE;2hL1nbj$w%e;+R1qA4QbH]i_H]jĨMۈOS`%M}6û7a{[qUJnP#7Cy`5U"0܍GEEGhϸkiς[P4Yة&E} \sΪ='٢Z):;g 0*[fnЮHad̩jep>U?}t-VlCp_׸[ ǼK>>Jw{g%"YIHqFWATjuzof5ӠWڅe[;~UCo.i+F&_Ԍz!U+es$sSTK̭p,M1k[eţC/G?S ~{c7ɺK!cZ](}E1R<.6k %(瞻\+3.V8 y梑}qT֔ MLg?Kg}.emkf$# тBijTJM*"ep&i jc2.MFFV>HG0}kΊVs?rE専<Օ^2;JvӜRb(@ݧ*e4]5ӒE@3$MGf!oe9>W<"c1nS֭۲$!=M|0Eڧ*bDM|8MƒvJh>0y*䯷Ud &WUی!r4d?fk v>1LEw$UL.jmܮ V'.fܳwq;p P}/b@d&uTκ1ڏڰDfR& P~pRF43S`nmwi6vHMzHSPZ51$>"/p|g]5+-ݛmZI7- \@ò&)_Z+E{.*~mZd4LUxkn"ݿ1'/%E;~(w$hCXiGYOP]tP#hFwUalAwas#&&D{іVh.| jBGɼA1~#m{ 6-=:$ ˿cG QcDz.1pʁ-k:ƕfiƒapEO#%:] 7~brj(##U0GA"~ʌ&Wrw01>ᐦ@p"E45KZdD!Z9t\kN~\ \qgze(u*gH3h jr6 i[.h*/ S)tury|Mk*d[^F^}&ґ\Ev) nGOAv7hNcDZܸ!#[*C= QUcZapsjg6pNXIweiaEg W;w6$^LZ^Vf%u3PU~fǼs /()tQL'L޺ݕ煾d40LDH0)@3?\vSp;{~>vԞڔCܐs6n.~?EGvPaYT7_*/CTTEp jU)ڳ1;\\%DT %ӷ^AGqV :y7~npU.1T;|&$ h*Ȩ9M}4>P*[ǖW u@2ti/IZ9)f㇍c S|E_TE01a>>I 8ܧ@!# BdGn?_?T} }F}4T7M+t>FoKMR鄴B|3]49]_rTEL&yiEiv`k*JXFhfAiYpVEL!KVUS9Duz,CS%|;)LLdɯ0J{|I:&אF}ʙVyIWBrR5Qg: k!ӐrruKRH*Wdm8y0EJpH.&'q-2lIGFISC>A‰_:_eڏx*l^%x=OMUDV;I}QWM=p*w5] {wPVPbRFޟ1l*a" +$BI\DKлy{ lv_hJO)t5aKu\E="M%/ۍ1\ %cD_n1wCʱx?7qq`ۛ2I!ik?/zs&lN [&74jw_u(OV7s#v3!ִC΍E;CԿܶ"ӄ\mLdO%UWŦ狉{ˉݱq_wE v@TRݝMP23J)Ny]}Yk3s)'|AxJ#tuw\hQ#9}@:IM8$W#䐁?^wR "NHJY;_;nzZ ܏rW%uݷʦH IR}n/J Aɒщk=ڎ@l^,~[8|v=fɈ+lEϻoetޱ,6Aqyz: ﰄaSL\d eۨ|pj/Y?g7iUg-^ mn<z!G鬖\Qapo/=h;i^Љj^,hz׼(iBa8Z*T^ ^܁Y(FLo^p90-o| o rU--IfdqFA}q=uO5m>GjUFŝYWeӶr)aaQҤry\g_}^P퉾pE9oWnQ=Ut^a}{JS_%i|U ͹r qN$`ԵXpD E.!,vԔЕ+*l9By]2L\*4?~K[yچ8aH0bAHHUzHl沚{>7pYp ,Ԑ*rG4Vt!Hۀ;]TM ;hY˓tzK-k !u# ^\j?nsCwŁ>p)sIGM]4HI܀FJIIM?RR68)n;{.!A<@ӀP>LϷ-;6ɷz7k>.)ukdYQIL[!|ͨߪ"jz聓=]YFz~aqԟ]QP~m?QH\ ܕzIPT$1"#5y CQSf_8DPy5@znnҲ^᷃Or08>BSJpJ|Ԧ8F1]P/ÄF,ɑF̈|'Uӫ |1=q9Mەv9*`)*5U#dJ)_YW<:_GQb6W O$&r8պIU3j9'ےrq!J3!@v>CO5!nH=+}[%Eo nHG[q גOӏn5փ󬂘w|LEgTfmĕ,|s?lğrL./MCdᏈEMn+hqO߻Tq++!1dq37⩚:-~7-Jw1uݵ5F␙aFt;DnfWf .5#! w=1v>zm~m`՚e\d^̀yƉ4v}&)Bd~rɢ9&5ϓ#e5чeΎ*}Ȏ&w_q7DΖ [0ig*QBq"mjQVyeC0lCUxҌrD 3$*ҋQdRs HDª8ё;EnpKSt̳5SWn$\U>XJd|&(k0w]| bwhZWo$F)@.2I:VS*uFvKPvN]ݱCԞeSNџ>t)5 uo,FZ{&ܘ}4^ޤ!ۆ(7vIj8ڧR!bYBENh~{|iku]%L&p>ૉXr[VanY& af$ =^z VP*&cn#_==cQs> 8o TO~RZύ "e娗Ju)!IW};BW(tca}?(Ͽ)s;^ݷ%煮S$ %L}g=a;bYXUΈֵH2@R AphXa؃D1uƊܮOetݏa?akݢfHAyc5̭֚p}'XmG3zlj`5n@4ͽ]彼P~!فHAˎ2zIS=0f@srcbj?jꗜJdRkץ%`2;|P;os&KT秝Ie>ceuA Ka+_ʯVC2RN(C)\$}C"e :!UWQ`{VQ,(6Ƈ#};}sѕNB][ i⃄G|q[= ߧ5`M[^hu+ª5J'CWiMهk/TZ@ܭ^NpTqDT{jI>w˫)J18yq1U!y"c'd |!aȊXE0ƈ#TB<(p&}Jq]}%n\(s#2yH g >5!#TT q;S)jp&"J*-Y;S3z#5Ma\^ᮗrEH \5eT9%-z;uV=v?Vrwo+0FW*nkU/} ޶-vW8[ -!$!,##Fq$5 wآ|۲MqM5YY'*0"̃F^ET,! x\`Pq1gREf|Q >9B@|M{&s`wެ\#F҆,$tQqAyiNL|!koN{)P~$Zw%poeS ҠXӈMt+sMhkZ墼EŨ54kiSp:oaws?uo2݆pbhU@tX(P g.Ye)蘫CbOv˜A7nyg7x{Gfe-*+'lA$wtoa!>JvG.l7ݛԔʎ!#vÏ( cK$Ow`m[O2]ɏsn6}N.M1|T^x.al]^~ TxPz>UQvMIt$#`熍0@$$yCC,,# vHVʿ Կ0H[2Ʉg >.+mAߐc.NQa8ZwXl3%q, q֯ RDe߻C V ^wEaW,1uN:ɪ"lo;ǂgꘝ$+XAZ+fRdU~eV0oG\/o1}mA+Nܘ9Sdd$; aU TA$ HjaN,9 nUER7 DCQ1 9GĂzl+S>sZV=TX>Kwn& N>泫tZ&B$QGM;vG-u 4)t"hϳ^8m&etGB+Pa%"?5[h; C|kWIt^=A3Ꮍoihۺ:6NI|XFF`V*6U |jl3Y8n"23YجTIeZ7\7b CT/:!9#?#"8q? b3elxfNS 1]@UTɌQ0]I?aU>c.U2'~# DƒQǟe_ceqΉqDF9=ejaj?8z|#anW4!!2SNJËX$3ʚ߫};+_Y!16̉`錎*#Q'oRC轺&{w mS a4Pi[,Ei܎HUGۗuyaitT#=ǜu\zD7cf ܝ2G2Jŝ= #>o=~wM.n-v[y oSgoIot sJYʎ+T??tk H|*Uuly և<[D5 Ն'6a*ȝ9Ex&冴HVGFu}SUHvV=цB1 ߐ=dXuOY9xr;{g[ bI u1xil]G|tc!U6ýpbi5HlĮ0Zt@ṆVPr0r,h}atP$>Uh{ݱ9%3 0*&Y}* /D^:*D.%TOڳ{5K+qlV }'/"u|Ͼ]pm jR* F'k:n9aaTZGybgQ2;c4~C5d\҅SAҽc3D|Vk-ˍ2_jU^ Ǣ\v(Ԭ8e-C :Dz :_±˱쫁&BZ.ܲZpGʑREGc lMoP5VTn{p3:Rbi8!#o߰= '}vwU07YM4.h>MF=¨7 PE, nzMk'rŰqsn)Rv̼h+оISZ?JY*j]<<*؈2ߌ+8~o#ohMo{z5GܐӔFwD|_FWg6@ݗڣ7*D40CqC; ȕ !HF:m r,q\xy!lIaQX%?0+Fj._GJW kPD`̇0.uB :Z_|wB<;#L(7xw*Zr+!DD0w9=__?!9Pt0śQ-[ФC.' 1qMFz~ʉ"kNJ"I$F;!HN( hO"nn]H !k%#\jVD>}F]}ꆋe5Ƿv<:T?5Я8 \SwfK74FKA1Pg_!>_pC˼_Mwϴ$x[qL'\|HV$*ÕT̨J8ζgMTXڲ0% G[Jh Lps5I4d SUj0vј0.IV9\;j aLS #U,qO0fnz.YUnsS&}%h1 zSϮvk⣢-2cuK;1r_s:ʌۣh2!pl|Jޒ oFE\[􍞺0%KZXNU8EvvƯ^03 uȖL㢼!';WkFqdPQWoHēA~ЎAc.=hni (Y98YUV) Tqq MQ*nè]WcHN4+ХV7Yl=id_~Z> #8:I{ɬ 'Hz64^ƌ7|3u5l 8mąi/}h\YP5Ȣw8#G!Э|<Dߊ,Cr׳s7S$ O<>GeMMdcj L-˩+td hI?؞VNpC9dA~BBJjx`LJ5EENnA?qdnP]zY1b(dGe$: H`F%rzF[&Tl3WEwy04ԩ#CNRB[ꝸྚ#=H;MdRo/ "HXVu }|'/_X_ʿ2DZ.589,4鎊c8^tcϯE<-K` #:GHs8c+/6#zj~jxa䫬"a_!zjo]D\zejNWQ"5NT\I,jE :YD`:eg]Ci:]$ RF\HCV}ʾeSN몿w֜mrBX6(fI$j =~c|i bԙ#wS+PB۩=VWnG_t?xb5r nfFuQɻ02$Tz$}j&BQ.br6-_na$z!: 2}_C\q“,m0%n"+`y@HzU}u G\va ]]@Ppḛ?+"sQ8!][oo'sNtVeGTW# OK81L!x/ @~ި37~[nźndyk1upqdoPRÕ6efwSy^9{+2&QߵK4G@21E E SrM)"""a6or$)c8B2CQO-=zST0+Dªx_Ӳŭ;6=)i ozi[n)^)hrʟLc @] %/owoe]ZsZę @] fu!6GE5UUhl3Ln'8 `>!/Z(hn2}\4S؈Nkϊ vE&7QSU/?UrUU]\}jQɘMfic{TTT13D_}?}6햝˃4 f═g[ԽЦ9(R^9uEtarN+$RdP}kxekVkMJr.\;t q1}% [h`f4Д'ZDJQ6ʒ֝<\q[CNo +K\Ҵ+Jӕ>諰}GW7M4JFzQ/C ՞f Zw$◉A2UqʸW|-8a>t.Z{V(e8hfhzhLDj-SO S퍠5@a7Ē-+LRO>\^+>$k9>*{Zji3QQU\*]v8!4茂O]%Ust'ST|JUnԮg.xᣍKMԷJa(EJ&hWlf&qgLU}יV蜊ytMQsvx&|LUɎw=/ִ+]hۨUM){m12x:@cCL]d*p3ZWZg\.vlg/?Y0sG_`1wOWENUBrcUn9* 6.\X<ϽMRܡ_R[jʥ? ieN8&C, ){0`ǎKJJ$ UUUfjjjbbbjjTTT`0`0r&bL&"000쁉 @ҏ?~|fwd;;;77 UUUѩ&&&2vvvAAAwmmmŤFyy3gO,߭*))9;;;vDzw 2i411IMM0 ft2r;N.wu74|k/@ yyy-_H蘘WUd:`r8mYCXcƌ?~Xx h(+jD&($DBi>~##ܾ}!m^hbJ?ս)+{mzQQ'+gΜzj///bbbbbbJJJDΰcdd5|::8N悪ªw DH$w޽?3!! IG566:DOOGMM11ۯ\}L2F=|Y\Wrw8d"t6jr>~6177K{{/_VTT*|mMM%ETVdgGgffnAWW>77Zxfюt:Df6;H/*z`d Q˧N0s2`fmOL(** 2%rzij,p81c͘1rpKoLJf'VV~&o7H%6;;:++9?_bgnnc>9(rڴ~BKRJ=}--taOTbjcX ]XU0 *+ *+>x>nfoowkkű^vv 9sN$Z}-nnܘ1nd)Zx95uEHW7 *+/$'`USSHNuu5=/;.\(jS[GGVqqzQo1 KK{<}ܸȸ/QaM~~JJ_~PWV^1ѣ}7nԶr~8vl'-jj.>!󖺸L7nu>rVyP tݍRkϮ\ ,S}(CC =nQuċO8?>@H\9s-pcq8KW-suUP_{Jtzaaӕ d2/:y2Gd|G,0RePںͰ$.ÆnfyW+ >}%5N555@(F2r>|pݳ1uݻ0Cm-㭬IJjk$÷(C̃]G";sKK% t:sE550qbځFC(1?ithlէ! NNNd2޼k;|ϯZ%H޸dz;;}BBZ[!d2 YʎH/yfoS>̰9x154 G8::4Ur4Z-3) GDRc|^Xjkˡ5w\tpqj7}рz2P"D2g32`/k`FOOx^XxŋO +V5 (OO}ut_dRH$ .\X|9K6lWҥWUT2{sQ lmmBz=.A"޽MXMMZgm'/^**%Oҏ~}D| Vl =4x '%ű8lmWjYָ rM ,H%h9CCCtoLUW;u¢000iŊ Q6X &Ϸp1[,@^^oggGYc-JD N3.]Czzʭ K,A'{ :~8@!wuwZ(..++sttB ?nlll9bDCX(VN!+Wz^d===qk0o.7t6{ٷzbbbjoLCBBFx}}}Sc7eeK~[^nojj*YrD x@bb"A'd磨 JJl Ct.P/KJL6줤$tF@Vtt4: *Lz|( x@ x"S_QSWF::ȍ66d Dڠ56ff99`oo?Pt)7';33%$c@\\@EE^amb()ijjڼy!ۼysSSFXV;S ͖,fg0/=-ŋȊ6HPHUnn~e ޽e?rsCqxRBL;i-[$K޾eI?=&uuۼyϞEDDݻ711o*D@@ܝ;IsD@BnD_Ka! UUU;::V{jOׯ]v#0eEZaܓ'.^ۡC0+kk/ 8'oo7455;::`Q} --b<@hkؚF=th4#qG7a|S:$w3L E3qeFtg:MHYt60ʲwww@_1 0 p8@}-AH33SUzZi}}VqqǏ`ojj?bW6rMNd~!$ǃ2`>x cg(okk`2LUWUW0ןdi!G"lV"MJjk ('gVS# <@7@1dBc#|iEð[iee*xnd:}cHS4=ں:"Iύ˅&AMF*QF@NPtt`[oka Eֆ| {O=x0B_մi?N*ɏP9L&, p--e<) u "'NH% PPmm':XYjil܂^zիvf+޾޼_fQBrX,PK g2]AqH:aGqvIɪ[i7zĈ5/8qwttwt6+26o"""> '1u d2_Y=/]ϞG@ŘLQ32wFLYhĬThϏ?ނFRݻ*mի" u :Lĺ=7{U>8qK"˛_f`tv~}_HD"˝{D, /zP( PFSD}Ӯ/paꪪΝ ^R5ΝSWUcغ .*>@䨫 j4@& x<@"H@$ N pEk`VJdtGG鎎)C`mXX0p6",I5KMWM >eTUy|P"R,S6VVp8 4Tzf*dY:: H%vd 1 +%ƀQkX5 -54 hhk>TQMFZON߿:*>50PGSSTnŢtuu(y@[AYYD> 0|8HoR߿U?"Kr|PPV/X^fy<gii 48 ?%*ᠭ •s( *f(GaVVVdJ 03C cA() XZBH$s)}$T-b!̸"())uuu|[1J!CZ[dPS55PDg%55@30u{iE>eY,t"@pppHMM}'QD"hh"P%s6?nn,ғ/%[ZB.\ӷ-_yAHgXW6tP"VUTVʑB,(m 0QӁO^JJX;v$6…#p$<۶Mx ðҪ*jNy|||-k16T*tp8oLnI[QB*jk;8j˗(Q331c@B|g]d.}dq"V%#:+ LX\Fq4}5HY5sOsO^FV;V\驦sW'>sؘ8XX(8\nbn.xyyD *%2s&T s#m7CCU 9ҚqH񈈪:RN!d?Eߢ;90'?Ւ>Ñ'JE)% d3zP?P>PH7n$<­[%`ˍ򚚅[vx$e4>uM&% u[\߉rekcie%W\6*(YT*ܼ9Yr {T:\q PY[ [}|BVKDBAOG^ҥ8 رuٲ/*r],`ǎ6 ÝXTi1YY(nÆ C,5Y|L&}kªf|Ogg'|>?>5 h@[ܪUR[[ÇZ"2\]Uϟ{ӓg(0;:vGENH{89Mu`0(-}Ug·e˻@y8zhPP襾da6a„l5 顚5UTq>P)S(Cuttu eEgՓi@ttuYlPD % QgVPAοned`0(Z,-88,٭Pz4Xf[[[Lœgkka[m򖹺_)f?RR4'$D>9k?|_@zѢE%>fB B<i'Rib⚼+I#2,hnnvpp(**pq;vYw&B-RT\PY՘,H-_&سiwuweff-d@AAĉ[[[5_7rŹsh|} DiMK˞ hH]KqBȄ;Z[I$ңGZD$$$x{{|3*5Asv!9yoTkݗL2N<@Γ::_ꊗw [r_ =uVp=Ap`;B n^9iɒ}L浴ȧOˑ;6zy f] ~8~J.e`ҥ( ϯZ%qM,ֱȔFIs*ujNS(55jFu5C]pno%h,Ao'*j͛0NWWt8e֬AZ | {ݕԛ쒪B&Ϟ0a˴cnt tzzzrR555(|GeC'jkKjk?֖541l}}s}}s*jpkkEM1 {Ɓ[0 .Kd@nn'}ر76oJn}ٝKSZZZ.RC@TVVLq۷+jQ۠'$tzllu A!SRRf]sse wDLJ(++/GG[KӣlPw `t4 `0&k׵khG"Ξ=+y)yyy>>>eeeB&o'OAGW/ӻu떄1(n͚5wAϞ| <~ nnnaaapU ,ϟo۶--- }4;`BF"&+kի++Gkk됐.2A1d!DGGܹ}ϟ?v0Iǰ*h,ZHQч$ x<ޅ +A%-UUO[[?{66 _̸/2hkkڵkdzL>qÇ"yVd1c}Əחlݗ/ssBlذaΝr BBKKK\\\LLLbb"K72p8È}7r03*UA7WZ_Ϩ~WQU1 W+ȖGɱU x+olP[?VWW}?N) YB`FdgYaaa*7 oL(ZZZ Š )f54w˜SlIENDB`httpbin-0.4.0/httpbin/templates/images/svg_logo.svg0000644000076500000240000002143012612231203022665 0ustar kevinstaff00000000000000 SVG Logo SVG httpbin-0.4.0/httpbin/templates/images/wolf_1.webp0000644000076500000240000002451012612231203022375 0ustar kevinstaff00000000000000RIFF@)WEBPVP8X nVP8 X%*o>1CC`$ gPO;A8T`?Rz{@v &o^_/xg>f~m~(3'ƥ/us߁'g?쟷q~/?;Mݯ_Cg}}fWq'L?1K{tدW$zin1xx'w&^B}B~ft׃fu^B!RV|'ĝl= j(R/ ubPL8;"zw5"&:Dwf5:Hm_=SNCvL3~(qT- ^euTp]p`c~37X8 ~IYj*dw>'N\bR ߵ$s ̡/KUbJWmAC/ Nzl9sh5:γcI54țCpg[tR+ m pn@8x.";, 5D;`!7"!_pƧ1'|m, I Ob8O4''&$(Ja7ӅB*JcE]#:c3UHş:0F< =]{:~1p| 4I7W!ȡ g|Z4Wmqesu]vgVTܓP9~SrvK R]Q.Qy{VhZ(}9SN!F6!8ZVцZPP6&k^ q{}ַ]4If_10\bND:!SJi= Bƛd3uΛC,*&] -uUFߗ%sL ʭ9J?'3j#KwGi@P]ըHop|Z!<?]hщzD{ͼ9W{,$CfjE߹OGoXƁ|uyC_Ƅ2f;1*iD,MQN{tzǝm ݂~Ӓ(v!7dfpJ>s .FY_!y+$27w8 <16*ʹל4cA`&T@gi)&y\{iUzh-S [6 9w$a&EJDXlY:P!VC֑<^U׊ $Kr&ZK&[k^΍QqfK8zS+s9+?:_"rOb ~;XiύRQC(6xXHⱩCj G:8bj 5Ih`1p?H=7i-ھn:t6&=Q`( Gf'y_+)8l"'TYj~9Mv3>6b?P8>9x3kjɉ}ߥh洛QxcDT!@RZhKDTRUY#<& 9U_wԓ \+UݏY$ߏt]akj2 MV~d[7noˏ%im64n^JϮ}fE2ůF72L-Ȁ )i"2K 줪ŢQʝN~+ooj}\h6@ZM{9Q+jIY-9bg1! ", 7:=0Vߊ 4$A<QA/>2:{~Cp%qrtaC;bk.稠V*LMT T pKEbM:TUDO6u'R @vDRJUY!mi᷈%XY\sl+Vʺ󶞽 r6 "x/ h5ZAtbWxp_U~Oy鳪4=@C~s6=P2tXo-ygvF3,kOќ >5J  t Zma:qM|S攠;Wޏs;:\jsBdHb% [יִa*jyӒVG!.a3>|X;ܙt "Ar01:K3FQN>*¢H,qqٍ!f VG*voxYl0.׶5(19βM}TbM~؝oN\\,B!ԭc頮fE\:)Bб~rLH_R}Q)|u,NO¼ `Op6(3cܶ`Ӱ,[eԳGxW=0AAx[dM#CٶA"@VKFs>>n8KhoB:S{Rd*:(¶ެNHA@Fs+LXHt)2HῃxLk5x{Mq1*k 97vEZș=`_[.YFUkܗY@>W-T5ZVEB@k G@ KE o(6 `?_hQG?H?UÐ#8-fu&mA4%hn[\Iةz_hoWU6Kts{D1{OAԡbҷu+%l =Pg8~ɾk)UV F*Ę3ك˷5@ \"F5)Z6xeT>JDVMSzɾ>[FCIKzᘱTl;wUN${KO*BFr bޅ`@EvZ #YGoMIDE,8@2 kolc؛[q4L"Aۘ(Ez/ VlT6ȱUoop_Z5vFŹ#.ҤJ~**AK۫7C%JE}Z#IZXэhpگf vT+V!w0r SC/8wFNoa1HCi_XKQVrw=Ў"z0r)MҘ&eD}tԏyJD&?P߯S'j86l=Ó|6Fʟ dS<>:aNVrYОB߳TuX\uK|fn{pzQ/6:O 6(bDVQkete SƙZ el;7^%.@u`+>có 9DT)vw\eՠqsz9,<t.yK: 5<|5fq')ŕ,4lmkM78$dfMlS.@2G]`|=ڼ&m =41kF7e$WN CG ;( s.j% h{ {5ړȗ4p/r7:8$kNO[$rW88]$w <dFS ߨNUqS`˱c5!TreQŢQ~cv=1&s~ SQ[\ .%pJ;S `-_DVM Dctޞ`F#Yf<61:5lZYpŘ'3t f>35eqoT ݖv*F?Svwyf<XcﮘҒ?W^MKN4oK1>WP-A0@$>zjs2z! YJh3́WVw! .G:#V-oVfHPΑD#&(EY͆~n#,!]R*01!dmO8<&|qAfHxLZEIsD~D˯Q6IT;C.dI|*uQ~-NsDZ{Fɨ[Uns_7` qOZT)w V21FUht"HWAuQ_CniE{{풙W%Yo;(Rڧ??/CT<ٰ3P[)2ۮ `~õ#>SWyw1Z!.Uʆ>HFؽJh!{{/OG*b:"`̈́/)GY7]f+>5=*L "N!D5 =g?E2cJn ?'Wz?=%Wjںp U6?S?U`PpKf!p\ ##H@]uS  4ˋFCq0|D]'W8lk%"%xeLO͆5k>ж3;?&/ѢJ&[囶RCUs+ LZ5N#m!sȗ@ {s$͈Cغ¡#2O:/cQȯW1L#{sv,_zDld_+15o6â):ȭY?j ҙsQ,'ցU EkSQ UDd6UZD^K Xm8uY`ZYafo5Dw;q5Cdi| 1T`S?̻8R&wW~E^˗1\Ữkًkc:xDȥ vo"=8@ 8hB7TNjr cW.9'gup΃SY_!ä$oֳm~ D8a`XMP  False http://www.wpclipart.com/terms.html edited by Paul Sherman, WPClipart Licensed to the public under Creative Commons Public Domain. httpbin-0.4.0/httpbin/templates/index.html0000644000076500000240000000503112612231203021054 0ustar kevinstaff00000000000000 httpbin(1): HTTP Client Testing Service Fork me on GitHub {% include 'httpbin.1.html' %} {% if tracking_enabled %} {% include 'trackingscripts.html' %} {% endif %} httpbin-0.4.0/httpbin/templates/moby.html0000644000076500000240000000723612612231203020724 0ustar kevinstaff00000000000000

Herman Melville - Moby-Dick

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.

httpbin-0.4.0/httpbin/templates/sample.xml0000644000076500000240000000101312612231203021056 0ustar kevinstaff00000000000000 Wake up to WonderWidgets! Overview Why WonderWidgets are great Who buys WonderWidgets httpbin-0.4.0/httpbin/templates/trackingscripts.html0000644000076500000240000000163312612231203023163 0ustar kevinstaff00000000000000{# place tracking scripts (like Google Analytics) here #} httpbin-0.4.0/httpbin/utils.py0000644000076500000240000000111412612231203016571 0ustar kevinstaff00000000000000# -*- coding: utf-8 -*- """ httpbin.utils ~~~~~~~~~~~~~~~ Utility functions. """ import random import bisect def weighted_choice(choices): """Returns a value from choices chosen by weighted random selection choices should be a list of (value, weight) tuples. eg. weighted_choice([('val1', 5), ('val2', 0.3), ('val3', 1)]) """ values, weights = zip(*choices) total = 0 cum_weights = [] for w in weights: total += w cum_weights.append(total) x = random.uniform(0, total) i = bisect.bisect(cum_weights, x) return values[i] httpbin-0.4.0/LICENSE0000644000076500000240000000133312612231203014417 0ustar kevinstaff00000000000000Copyright (c) 2011 Kenneth Reitz. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.httpbin-0.4.0/MANIFEST.in0000644000076500000240000000013212612231203015144 0ustar kevinstaff00000000000000include README.rst LICENSE AUTHORS requirements.txt recursive-include httpbin/templates * httpbin-0.4.0/PKG-INFO0000644000076500000240000002674212612231410014522 0ustar kevinstaff00000000000000Metadata-Version: 1.1 Name: httpbin Version: 0.4.0 Summary: HTTP Request and Response Service Home-page: https://github.com/Runscope/httpbin Author: Runscope Author-email: httpbin@runscope.com License: MIT Description: httpbin(1): HTTP Request & Response Service =========================================== Freely hosted in `HTTP `__, `HTTPS `__ & `EU `__ flavors by `Runscope `__ |Deploy|_ .. |Deploy| image:: https://www.herokucdn.com/deploy/button.svg .. _Deploy: https://heroku.com/deploy?template=https://github.com/runscope/httpbin |Build Status| ENDPOINTS --------- ====================================== ================================================================================================================== Endpoint Description -------------------------------------- ------------------------------------------------------------------------------------------------------------------ `/`_ This page. `/ip`_ Returns Origin IP. `/user-agent`_ Returns user-agent. `/headers`_ Returns header dict. `/get`_ Returns GET data. `/post` Returns POST data. `/patch` Returns PATCH data. `/put` Returns PUT data. `/delete` Returns DELETE data `/gzip`_ Returns gzip-encoded data. `/deflate`_ Returns deflate-encoded data. `/status/:code`_ Returns given HTTP Status code. `/response-headers`_ Returns given response headers. `/redirect/:n`_ 302 Redirects *n* times. `/redirect-to?url=foo`_ 302 Redirects to the *foo* URL. `/relative-redirect/:n`_ 302 Relative redirects *n* times. `/cookies`_ Returns cookie data. `/cookies/set?name=value`_ Sets one or more simple cookies. `/cookies/delete?name`_ Deletes one or more simple cookies. `/basic-auth/:user/:passwd`_ Challenges HTTPBasic Auth. `/hidden-basic-auth/:user/:passwd`_ 404'd BasicAuth. `/digest-auth/:qop/:user/:passwd`_ Challenges HTTP Digest Auth. `/stream/:n`_ Streams *n* – 100 lines. `/delay/:n`_ Delays responding for *n* – 10 seconds. `/drip`_ Drips data over a duration after an optional initial delay, then (optionally) returns with the given status code. `/range/:n`_ Streams *n* bytes, and allows specifying a *Range* header to select a subset of the data. Accepts a *chunk\_size* and request *duration* parameter. `/html`_ Renders an HTML Page. `/robots.txt`_ Returns some robots.txt rules. `/deny`_ Denied by robots.txt file. `/cache`_ Returns 200 unless an If-Modified-Since or If-None-Match header is provided, when it returns a 304. `/cache/:n`_ Sets a Cache-Control header for *n* seconds. `/bytes/:n`_ Generates *n* random bytes of binary data, accepts optional *seed* integer parameter. `/stream-bytes/:n`_ Streams *n* random bytes of binary data, accepts optional *seed* and *chunk\_size* integer parameters. `/links/:n`_ Returns page containing *n* HTML links. `/forms/post`_ HTML form that submits to */post* `/xml`_ Returns some XML `/encoding/utf8`_ Returns page containing UTF-8 data. ====================================== ================================================================================================================== .. _/user-agent: http://httpbin.org/user-agent .. _/headers: http://httpbin.org/headers .. _/get: http://httpbin.org/get .. _/: http://httpbin.org/ .. _/ip: http://httpbin.org/ip .. _/gzip: http://httpbin.org/gzip .. _/deflate: http://httpbin.org/deflate .. _/status/:code: http://httpbin.org/status/418 .. _/response-headers: http://httpbin.org/response-headers?Content-Type=text/plain;%20charset=UTF-8&Server=httpbin .. _/redirect/:n: http://httpbin.org/redirect/6 .. _/redirect-to?url=foo: http://httpbin.org/redirect-to?url=http://example.com/ .. _/relative-redirect/:n: http://httpbin.org/relative-redirect/6 .. _/cookies: http://httpbin.org/cookies .. _/cookies/set?name=value: http://httpbin.org/cookies/set?k1=v1&k2=v2 .. _/cookies/delete?name: http://httpbin.org/cookies/delete?k1&k2 .. _/basic-auth/:user/:passwd: http://httpbin.org/basic-auth/user/passwd .. _/hidden-basic-auth/:user/:passwd: http://httpbin.org/hidden-basic-auth/user/passwd .. _/digest-auth/:qop/:user/:passwd: http://httpbin.org/digest-auth/auth/user/passwd .. _/stream/:n: http://httpbin.org/stream/20 .. _/delay/:n: http://httpbin.org/delay/3 .. _/drip: http://httpbin.org/drip?numbytes=5&duration=5&code=200 .. _/range/:n: http://httpbin.org/range/1024 .. _/html: http://httpbin.org/html .. _/robots.txt: http://httpbin.org/robots.txt .. _/deny: http://httpbin.org/deny .. _/cache: http://httpbin.org/cache .. _/cache/:n: http://httpbin.org/cache/60 .. _/bytes/:n: http://httpbin.org/bytes/1024 .. _/stream-bytes/:n: http://httpbin.org/stream-bytes/1024 .. _/links/:n: http://httpbin.org/links/10 .. _/forms/post: http://httpbin.org/forms/post .. _/xml: http://httpbin.org/xml .. _/encoding/utf8: http://httpbin.org/encoding/utf8 DESCRIPTION ----------- Testing an HTTP Library can become difficult sometimes. `RequestBin `__ 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. All endpoint responses are JSON-encoded. EXAMPLES -------- $ curl http://httpbin.org/ip ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: {"origin": "24.127.96.129"} $ curl http://httpbin.org/user-agent ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: {"user-agent": "curl/7.19.7 (universal-apple-darwin10.0) libcurl/7.19.7 OpenSSL/0.9.8l zlib/1.2.3"} $ curl http://httpbin.org/get ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: { "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": "http://httpbin.org/get" } $ curl -I http://httpbin.org/status/418 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: 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 $ curl https://httpbin.org/get?show\_env=1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: { "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": "http://httpbin.org/get?show_env=1" } Installing and running from PyPI -------------------------------- You can install httpbin as a library from PyPI and run it as a WSGI app. For example, using Gunicorn: .. code:: bash $ pip install httpbin $ gunicorn httpbin:app Or run it directly: .. code:: bash $ python -m httpbin.core Changelog --------- - 0.4.0: New /image/svg endpoint, add deploy to heroku button, add 406 response to /image, and don't always emit the transfer-encoding header for stream endpoint. - 0.3.0: A number of new features, including a /range endpoint, lots of bugfixes, and a /encoding/utf8 endpoint - 0.2.0: Added an XML endpoint. Also fixes several bugs with unicode, CORS headers, digest auth, and more. - 0.1.2: Fix a couple Python3 bugs with the random byte endpoints, fix a bug when uploading files without a Content-Type header set. - 0.1.1: Added templates as data in setup.py - 0.1.0: Added python3 support and (re)publish on PyPI AUTHOR ------ A `Runscope Community Project `__. Originally created by `Kenneth Reitz `__. SEE ALSO -------- - https://www.hurl.it - http://requestb.in - http://python-requests.org .. |Build Status| image:: https://travis-ci.org/Runscope/httpbin.svg :target: https://travis-ci.org/Runscope/httpbin Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.4 httpbin-0.4.0/README.rst0000644000076500000240000002222612612231325015112 0ustar kevinstaff00000000000000httpbin(1): HTTP Request & Response Service =========================================== Freely hosted in `HTTP `__, `HTTPS `__ & `EU `__ flavors by `Runscope `__ |Deploy|_ .. |Deploy| image:: https://www.herokucdn.com/deploy/button.svg .. _Deploy: https://heroku.com/deploy?template=https://github.com/runscope/httpbin |Build Status| ENDPOINTS --------- ====================================== ================================================================================================================== Endpoint Description -------------------------------------- ------------------------------------------------------------------------------------------------------------------ `/`_ This page. `/ip`_ Returns Origin IP. `/user-agent`_ Returns user-agent. `/headers`_ Returns header dict. `/get`_ Returns GET data. `/post` Returns POST data. `/patch` Returns PATCH data. `/put` Returns PUT data. `/delete` Returns DELETE data `/gzip`_ Returns gzip-encoded data. `/deflate`_ Returns deflate-encoded data. `/status/:code`_ Returns given HTTP Status code. `/response-headers`_ Returns given response headers. `/redirect/:n`_ 302 Redirects *n* times. `/redirect-to?url=foo`_ 302 Redirects to the *foo* URL. `/relative-redirect/:n`_ 302 Relative redirects *n* times. `/cookies`_ Returns cookie data. `/cookies/set?name=value`_ Sets one or more simple cookies. `/cookies/delete?name`_ Deletes one or more simple cookies. `/basic-auth/:user/:passwd`_ Challenges HTTPBasic Auth. `/hidden-basic-auth/:user/:passwd`_ 404'd BasicAuth. `/digest-auth/:qop/:user/:passwd`_ Challenges HTTP Digest Auth. `/stream/:n`_ Streams *n* – 100 lines. `/delay/:n`_ Delays responding for *n* – 10 seconds. `/drip`_ Drips data over a duration after an optional initial delay, then (optionally) returns with the given status code. `/range/:n`_ Streams *n* bytes, and allows specifying a *Range* header to select a subset of the data. Accepts a *chunk\_size* and request *duration* parameter. `/html`_ Renders an HTML Page. `/robots.txt`_ Returns some robots.txt rules. `/deny`_ Denied by robots.txt file. `/cache`_ Returns 200 unless an If-Modified-Since or If-None-Match header is provided, when it returns a 304. `/cache/:n`_ Sets a Cache-Control header for *n* seconds. `/bytes/:n`_ Generates *n* random bytes of binary data, accepts optional *seed* integer parameter. `/stream-bytes/:n`_ Streams *n* random bytes of binary data, accepts optional *seed* and *chunk\_size* integer parameters. `/links/:n`_ Returns page containing *n* HTML links. `/forms/post`_ HTML form that submits to */post* `/xml`_ Returns some XML `/encoding/utf8`_ Returns page containing UTF-8 data. ====================================== ================================================================================================================== .. _/user-agent: http://httpbin.org/user-agent .. _/headers: http://httpbin.org/headers .. _/get: http://httpbin.org/get .. _/: http://httpbin.org/ .. _/ip: http://httpbin.org/ip .. _/gzip: http://httpbin.org/gzip .. _/deflate: http://httpbin.org/deflate .. _/status/:code: http://httpbin.org/status/418 .. _/response-headers: http://httpbin.org/response-headers?Content-Type=text/plain;%20charset=UTF-8&Server=httpbin .. _/redirect/:n: http://httpbin.org/redirect/6 .. _/redirect-to?url=foo: http://httpbin.org/redirect-to?url=http://example.com/ .. _/relative-redirect/:n: http://httpbin.org/relative-redirect/6 .. _/cookies: http://httpbin.org/cookies .. _/cookies/set?name=value: http://httpbin.org/cookies/set?k1=v1&k2=v2 .. _/cookies/delete?name: http://httpbin.org/cookies/delete?k1&k2 .. _/basic-auth/:user/:passwd: http://httpbin.org/basic-auth/user/passwd .. _/hidden-basic-auth/:user/:passwd: http://httpbin.org/hidden-basic-auth/user/passwd .. _/digest-auth/:qop/:user/:passwd: http://httpbin.org/digest-auth/auth/user/passwd .. _/stream/:n: http://httpbin.org/stream/20 .. _/delay/:n: http://httpbin.org/delay/3 .. _/drip: http://httpbin.org/drip?numbytes=5&duration=5&code=200 .. _/range/:n: http://httpbin.org/range/1024 .. _/html: http://httpbin.org/html .. _/robots.txt: http://httpbin.org/robots.txt .. _/deny: http://httpbin.org/deny .. _/cache: http://httpbin.org/cache .. _/cache/:n: http://httpbin.org/cache/60 .. _/bytes/:n: http://httpbin.org/bytes/1024 .. _/stream-bytes/:n: http://httpbin.org/stream-bytes/1024 .. _/links/:n: http://httpbin.org/links/10 .. _/forms/post: http://httpbin.org/forms/post .. _/xml: http://httpbin.org/xml .. _/encoding/utf8: http://httpbin.org/encoding/utf8 DESCRIPTION ----------- Testing an HTTP Library can become difficult sometimes. `RequestBin `__ 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. All endpoint responses are JSON-encoded. EXAMPLES -------- $ curl http://httpbin.org/ip ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: {"origin": "24.127.96.129"} $ curl http://httpbin.org/user-agent ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: {"user-agent": "curl/7.19.7 (universal-apple-darwin10.0) libcurl/7.19.7 OpenSSL/0.9.8l zlib/1.2.3"} $ curl http://httpbin.org/get ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: { "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": "http://httpbin.org/get" } $ curl -I http://httpbin.org/status/418 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: 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 $ curl https://httpbin.org/get?show\_env=1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: { "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": "http://httpbin.org/get?show_env=1" } Installing and running from PyPI -------------------------------- You can install httpbin as a library from PyPI and run it as a WSGI app. For example, using Gunicorn: .. code:: bash $ pip install httpbin $ gunicorn httpbin:app Or run it directly: .. code:: bash $ python -m httpbin.core Changelog --------- - 0.4.0: New /image/svg endpoint, add deploy to heroku button, add 406 response to /image, and don't always emit the transfer-encoding header for stream endpoint. - 0.3.0: A number of new features, including a /range endpoint, lots of bugfixes, and a /encoding/utf8 endpoint - 0.2.0: Added an XML endpoint. Also fixes several bugs with unicode, CORS headers, digest auth, and more. - 0.1.2: Fix a couple Python3 bugs with the random byte endpoints, fix a bug when uploading files without a Content-Type header set. - 0.1.1: Added templates as data in setup.py - 0.1.0: Added python3 support and (re)publish on PyPI AUTHOR ------ A `Runscope Community Project `__. Originally created by `Kenneth Reitz `__. SEE ALSO -------- - https://www.hurl.it - http://requestb.in - http://python-requests.org .. |Build Status| image:: https://travis-ci.org/Runscope/httpbin.svg :target: https://travis-ci.org/Runscope/httpbin httpbin-0.4.0/requirements.txt0000644000076500000240000000023112612231203016672 0ustar kevinstaff00000000000000decorator==3.4.0 Flask==0.10.1 gevent==1.0.1 greenlet==0.4.2 gunicorn==19.2 itsdangerous==0.24 Jinja2==2.7.2 MarkupSafe==0.23 six==1.6.1 Werkzeug==0.9.4 httpbin-0.4.0/setup.cfg0000644000076500000240000000013012612231410015225 0ustar kevinstaff00000000000000[bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 httpbin-0.4.0/setup.py0000644000076500000240000000203712612231252015132 0ustar kevinstaff00000000000000from setuptools import setup, find_packages import codecs import os import re long_description = open( os.path.join(os.path.dirname(__file__), 'README.rst')).read() setup( name="httpbin", version="0.4.0", description="HTTP Request and Response Service", long_description=long_description, # The project URL. url='https://github.com/Runscope/httpbin', # Author details author='Runscope', author_email='httpbin@runscope.com', # Choose your license license='MIT', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Natural Language :: English', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.4', ], packages=find_packages(), include_package_data = True, # include files listed in MANIFEST.in install_requires=['Flask','MarkupSafe','decorator','itsdangerous','six'], )