https://maths.tmos.es/add/5/7
HTTP/1.1 200 OK
Content-Type: text/plain
12
https://maths.tmos.es/
{
"operation": "multiply",
"arguments": [ 12, 6.1 ]
}
HTTP/1.1 200 OK
Content-Type: application/json
{
"result": 73.2
}
https://maths.tmos.es/any/random/path/like/this
{
"operation": "subtract",
"arguments": [ 10, 38 ]
}
HTTP/1.1 200 OK
Content-Type: application/json
{
"result": -28
}
https://maths.tmos.es/foo/9/8
HTTP/1.1 400 BAD REQUEST
Content-Type: text/plain
operation was not valid
https://maths.tmos.es/dinner
{
"operation": "multiply",
"arguments": [ "chicken", "pie" ]
}
HTTP/1.1 400 BAD REQUEST
Content-Type: text/plain
an argument was not a number
https://maths.tmos.es/divide/831/0
HTTP/1.1 405 METHOD NOT ALLOWED
Content-Type: text/plain
cannot divide by 0
This code is hosted on Google Cloud, in an App Engine instance.
It was initially hosted on PythonAnywhere which made this assignment surprisingly simple, so I decided to do a bit extra.
Written in Python using Flask. This web server take traffic for (almost) any subdomain of tmos.es with almost any path.
At the maths.tmos.es subomain I have the HTTP Assignemnt set up, and I have also built a URL shortener at tmos.es
Any GET requests sent to a subdomain which I don't have in use is forwarded to tmos.es with the same path.
The URL Shortener uses a Firestore database and sends 302 response forwarding to the set url. If not, then you have the option to give this path a destination.
To set a Short URL destination, my server accepts POST requests to the path (e.g. https://tmos.es/news) they want to set with a destination.
When visiting a short URL, http or https are accepted. When setting a destination, https must be used. When visiting any page (apart from a redirect), you are redirected to the https version.
For tmos.es and these main subdomains (preview.tmos.es & maths.tmos.es), I have Google-managed, auto-renewing SSL Security. However to redirect any random subdomain over https I have set up a manually created Wildcard certificate (*.tmos.es) from Lets Encrypt. The limitation of this is that second level subdomains will not work over https (as defined in RFC 2818)... but who is trying to visit https://random.sub.subdomain.tmos.es/news anyway when the whole point is a url shortener. Anyway, http://any.random.long.subdomain.like.this.tmos.es/news will work.
import logging from flask import Flask, Response, request, render_template, json, redirect, send_from_directory from decimal import Decimal from werkzeug.exceptions import BadRequest from google.cloud import firestore app = Flask(__name__, subdomain_matching=True) app.config['SERVER_NAME'] = "tmos.es" db = firestore.Client() ##################### # URL SHORTENER # ##################### # home page @app.route('/') def home(): # redirect to https for home page if not request.is_secure and request.method == 'GET': url = request.url.replace('http://', 'https://', 1) code = 301 return redirect(url, code=code) return render_template('shorter-home.html') # url redirector @app.route('/<path:path>') def apex(path): destination = getDestinationURL(path) if destination: return redirect(destination) else: if not request.is_secure and request.method == 'GET': url = request.url.replace('http://', 'https://', 1) code = 301 return redirect(url, code=code) return render_template('shorter.html', data={'path': path}) # url redirect preview @app.route('/<path:path>', subdomain="preview") def preview(path): # redirect to https for preview if not request.is_secure and request.method == 'GET': url = request.url.replace('http://', 'https://', 1) code = 301 return redirect(url, code=code) destination = getDestinationURL(path) if destination: return f'redirect to: {destination}' return f'there is no redirect for that path: tmos.es/{path}' # create a short URL @app.route('/<path:path>', methods=["POST"]) def shorter(path): # deal with post to create short url if not request.is_secure: return Response( 'you need to post to https', status=400, mimetype='text/plain') if not getDestinationURL(path): reqJson = request.get_json() destination = reqJson['destination'] setDestinationURL(path, destination) resp = Response( f'destination set to: {destination}', status=200, mimetype='text/plain') resp.headers['Access-Control-Allow-Origin'] = '*' return resp else: return Response( 'that path already has a destination set', status=400, mimetype='text/plain') @app.route('/tailwind.css') def sendShorterCSS(): return send_from_directory('css', 'tailwind.css') # get destination url def getDestinationURL(path): path = path.lower().replace('/','-') docRef = db.collection(u'redirects').document(path) doc = docRef.get().to_dict() if doc and doc['destination']: return doc['destination'] else: return None # set destination url def setDestinationURL(path, destination): path = path.lower().replace('/','-') data = {'destination': destination} docRef = db.collection(u'redirects').document(path) doc = docRef.set(data) ########################## # REDIRECT ANY SUBDOMAIN # ########################## @app.route('/', defaults={'path': ''}, subdomain="<subdomain>") @app.route('/<path:path>', subdomain="<subdomain>") def any(path, subdomain): print(f'hello any: {subdomain}.tmos.es/{path}') return redirect(f'https://tmos.es/{path}') ##################### # MATHS # ##################### HTTP_METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH'] @app.route('/', defaults={'path': ''}, methods=HTTP_METHODS, subdomain="maths") @app.route('/<path:path>', methods=HTTP_METHODS, subdomain="maths") def math(path): # enforce https for maths subdomain if not request.is_secure and request.method == 'GET': url = request.url.replace('http://', 'https://', 1) code = 301 return redirect(url, code=code) try: if request.method == 'POST': return handlePost() elif request.method == 'GET' or request.method == 'HEAD': return handleGet(path) else: # if not GET or POST, return method not allowed return Response( 'method not allowed', status=405, mimetype='text/plain') except BadRequest: return Response( "bad request", status=400, mimetype='text/plain') except Exception as e: print(f'An exception of type {type(e).__name__} occurred. Arguments:\n{e.args}') print(e) logging.exception('An error occurred during a request.') return Response( "something went wrong on my end, this has been logged", status=500, mimetype='text/plain') @app.route('/tailwind.css', subdomain="maths") def sendCSS(): return send_from_directory('css', 'tailwind.css') def handlePost(): # get and validate json data reqJson = request.get_json() if not reqJson: return Response( "no json was sent in the request", status=400, mimetype='text/plain') # check for valid operation operations = ['add','subtract','multiply','divide'] if ('operation' not in reqJson.keys() or reqJson['operation'] not in operations): return Response( "valid operation was not in the request json", status=400, mimetype='text/plain') # check for valid argurments if ('arguments' not in reqJson.keys() or not isinstance(reqJson['arguments'], list) or len(reqJson['arguments']) != 2): return Response( "2 arguments were not in the request json", status=400, mimetype='text/plain') for i in [0,1]: if (not (isinstance(reqJson['arguments'][i], float) or isinstance(reqJson['arguments'][i], int))): return Response( "an argument was not a number", status=400, mimetype='text/plain') # check for divide by 0 if reqJson['operation'] == 'divide' and reqJson['arguments'][1] == 0: return Response( 'cannot divide by 0', status=422, mimetype='text/plain') # calculate and return result result = calculate( reqJson['operation'], reqJson['arguments'][0], reqJson['arguments'][1]) return Response( json.dumps({'result': result}), status=200, mimetype='application/json') def handleGet(path): # if no path given, show tutorial if path == '': # I am ignoring q-factor weighting as that seems a bit overkill if ('text/html' in request.headers["Accept"] or '*/*' in request.headers["Accept"] or 'text/*' in request.headers["Accept"]): return render_template('tutorial.html') else: return Response( "invalid request, revisit this url in a browser for help", status=406, mimetype='text/plain') # get and validate path parts # check for valid operation operations = ['add','subtract','multiply','divide'] parts = path.split('/') if len(parts) != 3: return Response( "expected 3 parts in the path", status=400, mimetype='text/plain') operation = parts[0] if operation not in operations: return Response( "operation was not valid", status=400, mimetype='text/plain') # check for valid arguments arguments=[] for pathPart in [1,2]: try: arguments.append(float(parts[pathPart])) except ValueError: return Response( f'item at position {pathPart} was not a number', status=400, mimetype='text/plain') # check for divide by 0 if operation == 'divide' and arguments[1] == 0: return Response('cannot divide by 0', status=422, mimetype='text/plain') # calculate and return result result = calculate(operation, arguments[0], arguments[1]) return Response(str(result), status=200, mimetype='text/plain') def calculate(operation, first, second): first = Decimal(first) second = Decimal(second) result = 0 if operation == 'add': result = first + second elif operation == 'subtract': result = first - second elif operation == 'multiply': result = first * second elif operation == 'divide': result = first / second result = float(result) if result.is_integer(): result = int(result) return result