diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9414382 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +Dockerfile diff --git a/.gitignore b/.gitignore index ec156a9..ea1169a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist/ .tox *.egg-info *.swp +.vscode/ diff --git a/Dockerfile b/Dockerfile index 681bcd7..541f4cd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,10 @@ FROM ubuntu:18.04 +LABEL name="httpbin" +LABEL version="0.9.0" +LABEL description="A simple HTTP service." +LABEL org.kennethreitz.vendor="Kenneth Reitz" + RUN apt update -y && apt install python3-pip -y EXPOSE 80 diff --git a/MANIFEST.in b/MANIFEST.in index 9375654..894af4c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -include README.rst LICENSE AUTHORS requirements.txt test_httpbin.py +include httpbin/VERSION README.md LICENSE AUTHORS test_httpbin.py recursive-include httpbin/templates * recursive-include httpbin/static * diff --git a/httpbin/VERSION b/httpbin/VERSION new file mode 100644 index 0000000..ac39a10 --- /dev/null +++ b/httpbin/VERSION @@ -0,0 +1 @@ +0.9.0 diff --git a/httpbin/core.py b/httpbin/core.py index 0a00a82..8791b21 100644 --- a/httpbin/core.py +++ b/httpbin/core.py @@ -30,6 +30,9 @@ from .helpers import get_headers, status_code, get_dict, get_request_range, chec 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', @@ -55,6 +58,7 @@ 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') @@ -78,13 +82,12 @@ template = { "url": "https://kennethreitz.org", }, # "termsOfService": "http://me.com/terms", - "version": "0.9.0" + "version": version }, "host": "httpbin.org", # overrides localhost:5000 "basePath": "/", # base bash for blueprint registration "schemes": [ - "https", - "http" + "https" ], 'protocol': 'https', 'tags': [ @@ -510,17 +513,58 @@ def redirect_to(): - Redirects produces: - text/html - parameters: - - name: url - type: string - - name: status_code - type: int + 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 = CaseInsensitiveDict(request.args.items()) + argsDict = request.form.to_dict(flat=True) if request.method in ('POST', 'PATCH', 'PUT') else request.args.items() + args = CaseInsensitiveDict(argsDict) # 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 @@ -907,13 +951,14 @@ def bearer_auth(): 401: description: Unsuccessful authentication. """ - if 'Authorization' not in request.headers: + 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 - authorization = request.headers.get('Authorization') - token = authorization.lstrip('Bearer ') + slice_start = len('Bearer ') + token = authorization[slice_start:] return jsonify(authenticated=True, token=token) @@ -1072,7 +1117,7 @@ def digest_auth(qop=None, user='user', passwd='passwd', algorithm='MD5', stale_a return response -@app.route('/delay/') +@app.route('/delay/', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'TRACE']) def delay_response(delay): """"Returns a delayed response (max of 10 seconds). --- @@ -1102,6 +1147,31 @@ def drip(): --- 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: @@ -1124,7 +1194,7 @@ def drip(): pause = duration / numbytes def generate_bytes(): for i in xrange(numbytes): - yield u"*".encode('utf-8') + yield b"*" time.sleep(pause) response = Response(generate_bytes(), headers={ diff --git a/httpbin/templates/index.html b/httpbin/templates/index.html index 7fbae03..0348536 100644 --- a/httpbin/templates/index.html +++ b/httpbin/templates/index.html @@ -1,70 +1,251 @@ + httpbin(1): HTTP Client Testing Service - - - + + + -{% include 'httpbin.1.html' %} - -{% if tracking_enabled %} - {% include 'trackingscripts.html' %} -{% endif %} + {% include 'httpbin.1.html' %} {% if tracking_enabled %} {% include 'trackingscripts.html' %} {% endif %} + diff --git a/now.json b/now.json new file mode 100644 index 0000000..26fd17f --- /dev/null +++ b/now.json @@ -0,0 +1,10 @@ +{ + "name": "httpbin", + "regions": [ + "all" + ], + "alias": [ + "httpbin.org" + ], + "type": "docker" +} diff --git a/setup.py b/setup.py index 52a01e9..afbef58 100644 --- a/setup.py +++ b/setup.py @@ -2,9 +2,13 @@ from setuptools import setup, find_packages import os import io + +with open(os.path.join(os.path.realpath(os.path.dirname(__file__)), 'httpbin', 'VERSION')) as version_file: + version = version_file.read().strip() + setup( name="httpbin", - version="0.9.0", + version=version, description="HTTP Request and Response Service", long_description="A simple HTTP Request & Response Service, written in Python + Flask.", diff --git a/test_httpbin.py b/test_httpbin.py index 27a9042..9db3515 100755 --- a/test_httpbin.py +++ b/test_httpbin.py @@ -280,6 +280,37 @@ class HttpbinTestCase(unittest.TestCase): response = self.app.get('/brotli') self.assertEqual(response.status_code, 200) + def test_bearer_auth(self): + token = 'abcd1234' + response = self.app.get( + '/bearer', + headers={'Authorization': 'Bearer ' + token} + ) + self.assertEqual(response.status_code, 200) + assert json.loads(response.data.decode('utf-8'))['token'] == token + + def test_bearer_auth_with_wrong_authorization_type(self): + """Sending an non-Bearer Authorization header to /bearer should return a 401""" + auth_headers = ( + ('Authorization', 'Basic 1234abcd'), + ('Authorization', ''), + ('', '') + ) + for header in auth_headers: + response = self.app.get( + '/bearer', + headers={header[0]: header[1]} + ) + self.assertEqual(response.status_code, 401) + + def test_bearer_auth_with_missing_token(self): + """Sending an 'Authorization: Bearer' header with no token to /bearer should return a 401""" + response = self.app.get( + '/bearer', + headers={'Authorization': 'Bearer'} + ) + self.assertEqual(response.status_code, 401) + def test_digest_auth_with_wrong_password(self): auth_header = 'Digest username="user",realm="wrong",nonce="wrong",uri="/digest-auth/user/passwd/MD5",response="wrong",opaque="wrong"' response = self.app.get(