Route requests based on the Accept header in Flask - python

I would like to route to a different Flask views based on the Accept HTTP header, for example:
#api.route('/test', accept='text/html')
def test_html():
return "<html><body>Test</body></html>"
#api.route('/test', accept='text/json')
def test_json():
return jsonify(test="Test")
I haven't found relevant option in Werkzeug Rule constructor, which is used by Flask. Is it a missing feature or is it possible to achieve the same effect differently, for example by intercepting and modifying URL path before routing?
I don't want to merge the views into one because it would complicate code significantly, there are many of them and they reside in different blueprints.
I am aware that similar question has been asked, but nobody answered it using Flask. It's possible to do it in different web frameworks, for example in Pyramid using predicates - sample code can be found in this answer.

I know this is an old question but I ended up here looking for something similar so I hope it helps someone else.
flask_accept has the functionality to handle different Accept types through different routes.
from flask import Flask, jsonify
from flask_accept import accept
app = Flask(__name__)
#app.route('/')
#accept('text/html')
def hello_world():
return 'Hello World!'
#hello_world.support('application/json')
def hello_world_json():
return jsonify(result="Hello World!")
if __name__ == '__main__':
app.run()
if you just want to reject requests depending on whether they are a specific data type you could also use Flask-Negotiate
from flask import Flask
from flask_negotiate import consumes, produces
app = Flask(__name__)
#app.route('/consumes_json_only')
#consumes('application/json')
def consumes_json_only():
return 'consumes json only'
When one tries to access the endpoint without a valid Accept header:
$ curl localhost:5000 -I
HTTP 415 (Unsupported Media Type)

I wrote a decorator which does that (copying here for posterity). It's just a rough idea that could be improved further (e.g. returning 406 Not Acceptable response instead of using the default handler when there are no handlers that match given MIME type). More explanations are in the comments.
import functools
from flask import Flask, request, jsonify
app = Flask(__name__)
def accept(func_or_mimetype=None):
"""Decorator which allows to use multiple MIME type handlers for a single
endpoint.
"""
# Default MIME type.
mimetype = 'text/html'
class Accept(object):
def __init__(self, func):
self.default_mimetype = mimetype
self.accept_handlers = {mimetype: func}
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
default = self.default_mimetype
mimetypes = request.accept_mimetypes
best = mimetypes.best_match(self.accept_handlers.keys(), default)
# In case of Accept: */*, choose default handler.
if best != default and mimetypes[best] == mimetypes[default]:
best = default
return self.accept_handlers[best](*args, **kwargs)
def accept(self, mimetype):
"""Register a MIME type handler."""
def decorator(func):
self.accept_handlers[mimetype] = func
return func
return decorator
# If decorator is called without argument list, return Accept instance.
if callable(func_or_mimetype):
return Accept(func_or_mimetype)
# Otherwise set new MIME type (if provided) and let Accept act as a
# decorator.
if func_or_mimetype is not None:
mimetype = func_or_mimetype
return Accept
#app.route('/')
#accept # Or: #accept('text/html')
def index():
return '<strong>foobar</strong>'
#index.accept('application/json')
def index_json():
return jsonify(foobar=True)
#index.accept('text/plain')
def index_text():
return 'foobar\n', 200, {'Content-Type': 'text/plain'}

You can return different response types based on the Accept header using request. Example.
if request.accept_mimetypes['application/json']:
return jsonify(<object>), '200 OK'

Related

IP Whitelist Function - Flask - Python3.x

I have created a web app which will be publicly facing. There are some admin tools that the IT department will use to administer certain things in databases for example.
I have all my routes and models for the database, I just want to get some insight as to whether my function would be a suitable method of whitelisting IP addresses to routes and if I have missed something.
def allowed_ip(request):
if not request:
now = time.strftime("%b-%d-%Y_%H:%M:%S", time.gmtime(time.time()))
app.logger.info('No request was sent -=- {}'.format(now))
return False
if request and request.headers['X-Real-IP']:
if request.headers['X-Real-IP'] not in config.Config.ALLOWED_IPS:
now = time.strftime("%b-%d-%Y_%H:%M:%S", time.gmtime(time.time()))
app.logger.info('Request received from non-whitelist client {} -=- {}'.format(request.headers['X-Real-IP'],
now))
return False
else:
now = time.strftime("%b-%d-%Y_%H:%M:%S", time.gmtime(time.time()))
app.logger.info('Request received from whitelisted client {} -=- {}'.format(request.headers['X-Real-IP'],
now))
return True
else:
now = time.strftime("%b-%d-%Y_%H:%M:%S", time.gmtime(time.time()))
app.logger.info('Request received from but no IP sent -=- {}'.format(now))
return False
The function checks if it received a request, (I know that seems pointless but I was receiving some weird errors without this line), if it has received a request, it checks the X-Real-IP header to see if it in our whitelist.
Is there anything I'm missing that could be manipulated here?
I appreciate this may be a broad or off-topic question but I'm also open to other methods of doing this. Perhaps it would be better for me to manage whitelisting at Nginx's level?
My answer adapted into my code:
from functools import wraps
def whitelisted(f):
#wraps(f)
def decorated_function(*args, **kwargs):
if request.headers['X-Real-IP'] not in app.config.get('ALLOWED_IPS'):
return redirect(url_for('login', next=request.url))
return f(*args, **kwargs)
return decorated_function
Now this is possible:
#app.route('/map')
#whitelisted
#login_required
def show_all():
I will do something like this:
# helpers.py
from flask import request, current_app
def check_ip():
def decorator(f):
def wrapped_function(*args, **kwargs):
ip = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
if ip:
if ip in current_app.config.get('ALLOWED_IPS'):
return f(*args, **kwargs)
return 'Nice try! <3' # Do a real thing like real http code for forbiden, etc ... use response
return update_wrapper(wrapped_function, f)
return decorator
# routes.py
index = Blueprint('index ', __name__)
#index.route('/')
#check_ip()
def hello_world():
return render_template('home.html')
But just using the IP is not secure, if you want something better you should use flask_login or something like this in my opinion.

How do I dynamically decorate a Flask rule?

I have a application with a large number of automatically defined rules. I need every rule to check for a Bearer token before proceeding.
I have found app.url_map.iter_rules() which does let me see all the defined rules. I want a way to redefine every rule to use its current function, but wrap it with the authenticate_admin decorator defined below.
from flask import Flask
from functools import wraps
app = Flask(__name__)
# Check that the request has the correct `bearer_token`.
def authenticate_admin(func):
#wraps(func)
def wrapped(*args, **kwargs):
bearer_token = vault.get('secret/oauth')['bearer_token']
expected = ("Bearer " + bearer_token)
if expected != request.headers.get('Authorization'):
return jsonify({'error': "Authorization token incorrect"}), 401
return func(*args, **kwargs)
return wrapped
# .... Define a bunch of routes (Elided) ....
for rule in app.url_map.iter_rules():
# NEXT LINE IS PSEUDOCODE; IT IS WHAT I WANT TO ACHIEVE
rule.fx = authenticate_admin(rule.fx)
What I want is that after the above for loop executes, all rules will now require you to supply a Bearer token.
In the end, I just ended up adding following davidism's advice and made authenticate a function that is run by all endpoints. I added a whitelist of endpoints that do NOT require authentication.
from flask import request
import vault
app = Flask(__name__)
# .... Define a bunch of routes (Elided) ....
WHITELIST_POST = ['/post1', '/post2', '/post3']
WHITELIST_GET = ['/', '/get1', '/get2']
def authenticate():
if request.method == "GET" and request.url_rule.rule in WHITELIST_GET:
return
if request.method == "POST" and request.url_rule.rule in WHITELIST_POST:
return
bearer_token = vault.get('secret/oauth')['bearer_token']
expected = ("Bearer " + bearer_token)
if expected != request.headers.get('Authorization'):
abort(401)
app.before_request(authenticate)
On a side note, my boss liked this even more. It means that any endpoint we create on the server by default requires a bearer token. In the past we have forgotten to require a bearer token for some endpoints. Now it is opt out instead of opt in.

Override Falcon's default error handler when no route matches

When Falcon(-Framework) could not find a route for a specific request, 404 is returned. How can I override this default handler? I want to extend the handler with a custom response.
The default handler when no resource matches is the path_not_found responder:
But as you can see in the _get_responder method of falcon API, it can't be override without some monkey patching.
As far as I can see, there are two different ways to use a custom handler:
Subclass the API class, and overwrite the _get_responder method so it calls your custom handler
Use a default route that matches any route if none of the application ones are matched. You probably prefer to use a sink instead of a route, so you capture any HTTP method (GET, POST...) with the same function.
I would recommend the second option, as it looks much neater.
Your code would look like:
import falcon
class HomeResource:
def on_get(self, req, resp):
resp.body = 'Hello world'
def handle_404(req, resp):
resp.status = falcon.HTTP_404
resp.body = 'Not found'
application = falcon.API()
application.add_route('/', HomeResource())
# any other route should be placed before the handle_404 one
application.add_sink(handle_404, '')
There is a better solution here.
def custom_response_handler(req, resp, ex, params):
resp.status = falcon.HTTP_404
resp.text = "custom text response"
app = falcon.App()
app.add_error_handler(HTTPRouteNotFound, custom_response_handler)

Flask equivalent of Sinatra's "passing"

In my Flask application, I want to expose a URI like this:
http://<base_uri>/some_string
and I wish to handle requests to it differently depending on whether some_string is an integer or not.
With Sinatra I can achieve that via "passing" as shown below:
get '/:some_string' do
if is_integer(:some_string)
'Your URI contains an integer'
else
pass # This will pass the request on the the method below which can handle it
end
get '/*' do
'Your URI contains some string'
end
Here the call pass in the first route lets the second route process the request if :some_string is not an integer.
I couldn't find any equivalent functionality in Flask. Can someone please suggest a solution in Flask?
Type conversion in url routes can do this for you:
from flask import Flask
import unittest
app = Flask(__name__)
app.debug = True
#app.route('/<int:thing>')
def num(thing):
return 'INT'
#app.route('/<thing>')
def string(thing):
return 'STR'
class TestDispatch(unittest.TestCase):
def setUp(self):
self.client = app.test_client()
def test_int(self):
resp = self.client.get('/10')
self.assertEqual("INT", resp.data)
def test_str(self):
resp = self.client.get('/hello')
self.assertEqual("STR", resp.data)
if __name__ == '__main__':
unittest.main()

Flask - custom decorator breaks the routing

I have the following Flask routes and a custom helper:
from spots import app, db
from flask import Response
import simplejson as json
def json_response(action_func):
def create_json_response(*args, **kwargs):
ret = action_func(*args, **kwargs)
code = 200
if len(ret) == 2:
code = ret[0]
resp = ret[1]
else:
resp = ret[0]
return Response(
response=json.dumps(resp, indent=4),
status=code,
content_type='application/json'
)
return create_json_response
#app.route('/test')
#json_response
def test():
return 400, dict(result="Test success")
#app.route('/')
#json_response
def home():
return 200, dict(result="Home success")
I would expect a GET request to /test to return something like {"result":"Test success"} but that is not the case. Instead, any request seems to match the last route, i.e. home. Why?
I wonder if this is caused by some lack of insulation between the different calls to json_response?
Thanks in advance.
As Видул Петров said the solution is to use functools.wraps:
import functools
def json_response(action_func):
#functools.wraps(action_func)
def create_json_response(*args, **kwargs):
...
return create_json_response
The reason is that Flask’s routing system maps URLs to "endpoints", and then endpoints to view functions. The endpoint defaults to the __name__ attribute of the view function. In this case the decorated function was passed to app.route so the endpoint was create_json_response for both rules and the last view defined for that endpoint was used in both cases.
functools.wraps takes the __name__ (and other attributes) from the original function and fixes this. It is always a good idea to use it in decorated wrappers.

Categories