Building simple url-redirection service using Flask and Python
There are many technologies that can be used for building back-ends for web-sites, web-services and etc. In this post, I want to show how easily web-services can be created using Python and Flask. I don’t really like writing UI (or HTML) so I will avoid it by making the management of the urls database available using RESTful API.
From the Flask’s official website:
Flask is a microframework for Python based on Werkzeug, Jinja 2 and good intentions
In shorter words, Flask gives you the ability to get HTTP requests, redirect them to the appropriate handle that is defined for the specific path in the python application, do what ever you need to do, and generate a response that will be sent back as HTTP response to the requester (probably web-browser).
For the example, we’ll use PickleDb as our database (that’s what google gave me as a first result for “simple key value database for python”). From the official website:
pickleDB is a lightweight and simple key-value store. It is built upon Python’s simplejson module and was inspired by redis
So what do we need for our service ?
- Redirection management:
- Create new redirections – the key will be an alias for the redirection (or can be an auto-generated unique id) and the value will be the actual redirection address.
- Delete existing redirections.
- Make actual redirections – redirect the given key to the appropriate value (in case it exists).
- Security and Authentication – All the management system should be protected so only authorized users can manage redirections.
Let’s start with installing the pre-requirements:
1 2 | pip install flask pip install pickledb |
Now, let’s create a new Flask application and run it from the main entry to the script:
1 2 3 4 5 | from flask import Flask, request, abort app = Flask(__name__) if __name__ == "__main__": app.run() |
In order to “hide” the database implementation from the application, let’s create a class that will manage data persistence:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class RedirectionsStore(object): DATABASE_PATH = "redirections.db" def __init__(self): # True asks the database to dump every change directly to the underlying file self.db = pickledb.load(RedirectionsStore.DATABASE_PATH, True) def add_record(self, key, value): if self.db.get(key) != None: return False self.db.set(key, value) return True def get_record(self, key): return self.db.get(key) def delete_record(self, key): self.db.rem(key) def get_all_records(self): return self.db.getall() |
The management operations we want to allow are creating and deleting redirections. In order to do that let’s create an app route called “/mgmt” and implement POST and DELETE requests for it.
POST request will be used for adding new redirection and its implementation looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | def add_new_redirection_request(): # Try parsing base64 key and value try: key = base64.b64decode(request.json["key"]) value = base64.b64decode(request.json["value"]) except Exception as e: logger.error("Failed parsing data: %s" % e.message) abort(400) # Bad request logger.info("Adding redirection with key=%s, value=%s" % (key, value)) db = RedirectionsStore() added = db.add_record(key, value) if not(added): logger.error("Key already exists - discarding request (key=%s)" % key) abort(403) # Forbidden return ("", 201) # Created |
The post request should contain “key” and “value” arguments, base64 encoded and in JSON format. The function is simple, we’re trying to get the arguments, in case we fail we’ll respond with error code 400, in case the key already exists in the database we’ll respond with error code 403, otherwise we’ll store the key-value in the database and respond with error code 201 (you can read more about HTTP error codes here).
The redirection deletion method should be simple enough as well:
1 2 3 4 5 6 7 8 9 10 11 12 | def delete_redirection_request(): # Try parsing base64 key try: key = base64.b64decode(request.json["key"]) except Exception as e: logger.error("Failed parsing data: %s" % e.message) abort(400) # Bad request logger.info("Deleting redirection with key=%s" % key) db = RedirectionsStore() db.delete_record(key) return ("", 204) |
In order to route those two functions to “/mgmt” we’ll do the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @app.route("/mgmt", methods=[ "POST", "DELETE" ]) def api_mgmt(): # Make sure we receive arguments with json format if not(request.json): logger.warn("Got mgmt API request not in json format - discarding!") abort(415) # Unsupported media type if request.method == "POST": logger.debug("Handling mgmt POST request") return ServerImplementation.add_new_redirection_request() if request.method == "DELETE": logger.debug("Handling mgmt DELETE request") return ServerImplementation.delete_redirection_request() logger.warn("Got mgmt request that cannot be handled") abort(400) # Bad request |
We can print all the existing redirections by creating another route:
1 2 3 4 5 6 7 8 | @app.route("/redirections", methods=[ "GET" ]) def api_redirections(): logger.info("Got a request to list all redirections from database") db = RedirectionsStore() records = db.get_all_records() result = json.dumps(records) return (result, 200) |
My favorite REST client for chrome is Postman and I’ll be using it for testing the API:
Create new redirection named “blah” to http://google.com
Delete redirection named “blah”:
For the example, I’ll create some more redirections and list them using the “/redirections” API:
Now what left is implementing the url redirection itself:
1 2 3 4 5 6 7 8 9 10 11 12 | @app.route("/redirect/<path:key>") def redirect_request(key): logger.info("Got a redirection request with key=%s" % key) db = RedirectionsStore() result = db.get_record(key) if result == None: logger.error("Key %s has no redirection defined" % key) abort(400) # Bad request logger.debug("Redirecting to %s" % result) return redirect(result, 302) |
Browsing to http://localhost:5000/redirect/search brought me to google.com and browsing to http://localhost:5000/redirect/unknown gave me the following error page:
The last thing left is adding some basic authentication for the management API. There are multiple things that can be done with Flask, the simple one is defining user name and password and verifying it for each request:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | def secure_api(f): @wraps(f) def implementation(*args, **kwargs): auth = request.authorization if not(auth): logger.error("No authorization supplied, discarding request!") abort(401) # Unauthorized if (auth.username != "admin" or auth.password != "password"): logger.error("Bad user name or password (username=%s, password=%s)" % (auth.username, auth.password)) abort(401) # Unauthorized return f(*args, **kwargs) return implementation |
and what left is add “@secure_api” for every routed method we want to protect:
1 2 3 4 5 6 7 8 9 | @app.route("/mgmt", methods=[ "POST", "DELETE" ]) @secure_api def api_mgmt(): ... @app.route("/redirections", methods=[ "GET" ]) @secure_api def api_redirections(): ... |
Sending GET request without providing credentials (or supplying wrong user name or password) will give us the following error:
When re-sending the request with the right credentials will execute the request:
In conclusion, Flask is an easy to use framework for implementing RESTful APIs and it can be used for many things. This is a very simple example without good handling for performance or scale but I guess it demonstrate the power of this framework.
The full source code can be found here and as usual, feel free to use it.
– Alexander