diff --git a/README.md b/README.md index 9ea1134..4d5e31c 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Freely hosted in [HTTP](http://httpbin.org) & - [`/relative-redirect/:n`](http://httpbin.org/relative-redirect/6) 302 Relative redirects *n* times. - [`/cookies`](http://httpbin.org/cookies) Returns cookie data. - [`/cookies/set?name=value`](http://httpbin.org/cookies/set?k1=v1&k2=v2) Sets one or more simple cookies. +- [`/cookies/delete?name`](http://httpbin.org/cookies/delete?k1&k2) Deletes one or more simple cookies. - [`/basic-auth/:user/:passwd`](http://httpbin.org/basic-auth/user/passwd) Challenges HTTPBasic Auth. - [`/hidden-basic-auth/:user/:passwd`](http://httpbin.org/hidden-basic-auth/user/passwd) 404'd BasicAuth. - [`/digest-auth/:qop/:user/:passwd`](http://httpbin.org/digest-auth/auth/user/passwd) Challenges HTTP Digest Auth. @@ -82,6 +83,35 @@ All endpoint responses are JSON-encoded. 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", + "X-Heroku-Dynos-In-Use": "1", + "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", + "X-Heroku-Queue-Depth": "0", + "X-Heroku-Queue-Wait-Time": "11", + "Content-Type": "" + }, + "args": { + "show_env": "1" + }, + "origin": "109.60.101.240", + "url": "http://httpbin.org/get?show_env=1" + } + ## AUTHOR A [Kenneth Reitz](http://kennethreitz.com/pages/open-projects.html) diff --git a/httpbin/core.py b/httpbin/core.py index b503160..1f2861c 100644 --- a/httpbin/core.py +++ b/httpbin/core.py @@ -46,6 +46,20 @@ app = Flask(__name__) sentry = Sentry(app) +# ----------- +# Middlewares +# ----------- +@app.after_request +def set_cors_headers(response): + response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*') + + if request.method == 'OPTIONS': + response.headers['Access-Control-Allow-Credentials'] = 'true' + response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, PATCH, OPTIONS' + response.headers['Access-Control-Max-Age'] = '3600' # 1 hour cache + return response + + # ------ # Routes # ------ @@ -293,6 +307,18 @@ def set_cookies(): 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('/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.""" diff --git a/httpbin/templates/httpbin.1.html b/httpbin/templates/httpbin.1.html index 5b6eca4..2a9d1f5 100644 --- a/httpbin/templates/httpbin.1.html +++ b/httpbin/templates/httpbin.1.html @@ -24,6 +24,7 @@
  • /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.
  • diff --git a/test_httpbin.py b/test_httpbin.py index 8d7149c..4287270 100755 --- a/test_httpbin.py +++ b/test_httpbin.py @@ -1,9 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import base64 +import unittest import httpbin -import unittest -import base64 def _string_to_base64(string): @@ -41,6 +41,22 @@ class HttpbinTestCase(unittest.TestCase): response = self.app.post('/post', data={"file": f}) self.assertEquals(response.status_code, 200) + def test_set_cors_headers_after_request(self): + response = self.app.get('/get') + self.assertEquals(response.headers.get('Access-Control-Allow-Origin'), '*') + + def test_set_cors_headers_after_request_with_request_origin(self): + response = self.app.get('/get', headers={'Origin': 'origin'}) + self.assertEquals(response.headers.get('Access-Control-Allow-Origin'), 'origin') + + def test_set_cors_headers_with_options_verb(self): + response = self.app.open('/get', method='OPTIONS') + self.assertEquals(response.headers.get('Access-Control-Allow-Origin'), '*') + self.assertEquals(response.headers.get('Access-Control-Allow-Credentials'), 'true') + self.assertEquals(response.headers.get('Access-Control-Allow-Methods'), 'GET, POST, PUT, DELETE, PATCH, OPTIONS') + self.assertEquals(response.headers.get('Access-Control-Max-Age'), '3600') + self.assertNotIn('Access-Control-Allow-Headers', response.headers) # FIXME should we add any extra headers? + if __name__ == '__main__': unittest.main()