sim-rest-server: Render error messages as JSON

Let's make sure even error messages are returned in JSON format.

While at it, also reduce some code duplication between the 'auth'
and 'info' route handlers by using the klein handle_errors decorator
instead of manual exception catching.

Change-Id: I1e0364e28ba7ce7451993f57c8228f9a7ade6b0e
Closes: OS#5607
diff --git a/contrib/sim-rest-server.py b/contrib/sim-rest-server.py
index a31aeed..62498b4 100755
--- a/contrib/sim-rest-server.py
+++ b/contrib/sim-rest-server.py
@@ -21,7 +21,7 @@
 import sys
 import argparse
 
-from klein import run, route
+from klein import Klein
 
 from pySim.transport import ApduTracer
 from pySim.transport.pcsc import PcscSimLink
@@ -50,78 +50,87 @@
 
     return tp, scc, card
 
+class ApiError:
+    def __init__(self, msg:str):
+        self.msg = msg
+
+    def __str__(self):
+        return json.dumps({'error': {'message':self.msg}})
+
+
 def set_headers(request):
     request.setHeader('Content-Type', 'application/json')
 
-@route('/sim-auth-api/v1/slot/<int:slot>')
-def auth(request, slot):
-    """REST API endpoint for performing authentication against a USIM.
-       Expects a JSON body containing RAND and AUTN.
-       Returns a JSON body containing RES, CK, IK and Kc."""
-    try:
-        # there are two hex-string JSON parameters in the body: rand and autn
-        content = json.loads(request.content.read())
-        rand = content['rand']
-        autn = content['autn']
-    except:
-        request.setResponseCode(400)
-        return "Malformed Request"
+class SimRestServer:
+    app = Klein()
 
-    try:
-        tp, scc, card = connect_to_card(slot)
-    except ReaderError:
-        request.setResponseCode(404)
-        return "Specified SIM Slot doesn't exist"
-    except ProtocolError:
-        request.setResponseCode(500)
-        return "Error"
-    except NoCardError:
+    @app.handle_errors(NoCardError)
+    def no_card_error(self, request, failure):
+        set_headers(request)
         request.setResponseCode(410)
-        return "No SIM card inserted in slot"
+        return str(ApiError("No SIM card inserted in slot"))
 
-    try:
+    @app.handle_errors(ReaderError)
+    def reader_error(self, request, failure):
+        set_headers(request)
+        request.setResponseCode(404)
+        return str(ApiError("Reader Error: Specified SIM Slot doesn't exist"))
+
+    @app.handle_errors(ProtocolError)
+    def protocol_error(self, request, failure):
+        set_headers(request)
+        request.setResponseCode(500)
+        return str(ApiError("Protocol Error"))
+
+    @app.handle_errors(SwMatchError)
+    def sw_match_error(self, request, failure):
+        set_headers(request)
+        request.setResponseCode(500)
+        return str(ApiError("Card Communication Error %s" % failure))
+
+
+    @app.route('/sim-auth-api/v1/slot/<int:slot>')
+    def auth(self, request, slot):
+        """REST API endpoint for performing authentication against a USIM.
+           Expects a JSON body containing RAND and AUTN.
+           Returns a JSON body containing RES, CK, IK and Kc."""
+        try:
+            # there are two hex-string JSON parameters in the body: rand and autn
+            content = json.loads(request.content.read())
+            rand = content['rand']
+            autn = content['autn']
+        except:
+            set_headers(request)
+            request.setResponseCode(400)
+            return str(ApiError("Malformed Request"))
+
+        tp, scc, card = connect_to_card(slot)
+
         card.select_adf_by_aid(adf='usim')
         res, sw = scc.authenticate(rand, autn)
-    except SwMatchError as e:
-        request.setResponseCode(500)
-        return "Communication Error %s" % e
 
-    tp.disconnect()
+        tp.disconnect()
 
-    set_headers(request)
-    return json.dumps(res, indent=4)
+        set_headers(request)
+        return json.dumps(res, indent=4)
 
-@route('/sim-info-api/v1/slot/<int:slot>')
-def info(request, slot):
-    """REST API endpoint for obtaining information about an USIM.
-    Expects empty body in request.
-    Returns a JSON body containing ICCID, IMSI."""
+    @app.route('/sim-info-api/v1/slot/<int:slot>')
+    def info(self, request, slot):
+        """REST API endpoint for obtaining information about an USIM.
+        Expects empty body in request.
+        Returns a JSON body containing ICCID, IMSI."""
 
-    try:
         tp, scc, card = connect_to_card(slot)
-    except ReaderError:
-        request.setResponseCode(404)
-        return "Specified SIM Slot doesn't exist"
-    except ProtocolError:
-        request.setResponseCode(500)
-        return "Error"
-    except NoCardError:
-        request.setResponseCode(410)
-        return "No SIM card inserted in slot"
 
-    try:
         card.select_adf_by_aid(adf='usim')
         iccid, sw = card.read_iccid()
         imsi, sw = card.read_imsi()
         res = {"imsi": imsi, "iccid": iccid }
-    except SwMatchError as e:
-        request.setResponseCode(500)
-        return "Communication Error %s" % e
 
-    tp.disconnect()
+        tp.disconnect()
 
-    set_headers(request)
-    return json.dumps(res, indent=4)
+        set_headers(request)
+        return json.dumps(res, indent=4)
 
 
 def main(argv):
@@ -132,7 +141,8 @@
 
     args = parser.parse_args()
 
-    run(args.host, args.port)
+    srr = SimRestServer()
+    srr.app.run(args.host, args.port)
 
 if __name__ == "__main__":
     main(sys.argv)