<p>laforge has uploaded this change for <strong>review</strong>.</p><p><a href="https://gerrit.osmocom.org/c/pysim/+/24342">View Change</a></p><pre style="font-family: monospace,monospace; white-space: pre-wrap;">contrib: Add sim-rest-{server,client}.py<br><br>sim-rest-server.py can be used to provide a RESTful API to allow remote<br>clients to perform the authentication command against a SIM card in a<br>PC/SC reader.<br><br>sim-rest-client.py is an example client against sim-rest-server.py<br>which can be used to test the functionality of sim-rest-server.py.<br><br>Change-Id: I738ca3109ab038d4f5595cc1dab6a49087df5886<br>---<br>A contrib/sim-rest-client.py<br>A contrib/sim-rest-server.py<br>2 files changed, 246 insertions(+), 0 deletions(-)<br><br></pre><pre style="font-family: monospace,monospace; white-space: pre-wrap;">git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/42/24342/1</pre><pre style="font-family: monospace,monospace; white-space: pre-wrap;"><span>diff --git a/contrib/sim-rest-client.py b/contrib/sim-rest-client.py</span><br><span>new file mode 100755</span><br><span>index 0000000..68f87b0</span><br><span>--- /dev/null</span><br><span>+++ b/contrib/sim-rest-client.py</span><br><span>@@ -0,0 +1,157 @@</span><br><span style="color: hsl(120, 100%, 40%);">+#!/usr/bin/env python3</span><br><span style="color: hsl(120, 100%, 40%);">+#</span><br><span style="color: hsl(120, 100%, 40%);">+# sim-rest-client.py: client program to test the sim-rest-server.py</span><br><span style="color: hsl(120, 100%, 40%);">+#</span><br><span style="color: hsl(120, 100%, 40%);">+# this will generate authentication tuples just like a HLR / HSS</span><br><span style="color: hsl(120, 100%, 40%);">+# and will then send the related challenge to the REST interface</span><br><span style="color: hsl(120, 100%, 40%);">+# of sim-rest-server.py</span><br><span style="color: hsl(120, 100%, 40%);">+#</span><br><span style="color: hsl(120, 100%, 40%);">+# sim-rest-server.py will then contact the SIM card to perform the</span><br><span style="color: hsl(120, 100%, 40%);">+# authentication (just like a 3GPP RAN), and return the results via</span><br><span style="color: hsl(120, 100%, 40%);">+# the REST to sim-rest-client.py.</span><br><span style="color: hsl(120, 100%, 40%);">+#</span><br><span style="color: hsl(120, 100%, 40%);">+# (C) 2021 by Harald Welte <laforge@osmocom.org></span><br><span style="color: hsl(120, 100%, 40%);">+#</span><br><span style="color: hsl(120, 100%, 40%);">+# This program is free software: you can redistribute it and/or modify</span><br><span style="color: hsl(120, 100%, 40%);">+# it under the terms of the GNU General Public License as published by</span><br><span style="color: hsl(120, 100%, 40%);">+# the Free Software Foundation, either version 2 of the License, or</span><br><span style="color: hsl(120, 100%, 40%);">+# (at your option) any later version.</span><br><span style="color: hsl(120, 100%, 40%);">+#</span><br><span style="color: hsl(120, 100%, 40%);">+# This program is distributed in the hope that it will be useful,</span><br><span style="color: hsl(120, 100%, 40%);">+# but WITHOUT ANY WARRANTY; without even the implied warranty of</span><br><span style="color: hsl(120, 100%, 40%);">+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the</span><br><span style="color: hsl(120, 100%, 40%);">+# GNU General Public License for more details.</span><br><span style="color: hsl(120, 100%, 40%);">+#</span><br><span style="color: hsl(120, 100%, 40%);">+# You should have received a copy of the GNU General Public License</span><br><span style="color: hsl(120, 100%, 40%);">+# along with this program.  If not, see <http://www.gnu.org/licenses/>.</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+import sys</span><br><span style="color: hsl(120, 100%, 40%);">+import argparse</span><br><span style="color: hsl(120, 100%, 40%);">+import requests</span><br><span style="color: hsl(120, 100%, 40%);">+import struct</span><br><span style="color: hsl(120, 100%, 40%);">+import secrets</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+from CryptoMobile.Milenage import Milenage</span><br><span style="color: hsl(120, 100%, 40%);">+from CryptoMobile.utils import xor_buf</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+def unpack48(x:bytes) -> int:</span><br><span style="color: hsl(120, 100%, 40%);">+    """Decode a big-endian 48bit number from binary to integer."""</span><br><span style="color: hsl(120, 100%, 40%);">+    x2, x3 = struct.unpack('>IH', x)</span><br><span style="color: hsl(120, 100%, 40%);">+    return (x2 << 16) | x3</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+def pack48(x:int) -> bytes:</span><br><span style="color: hsl(120, 100%, 40%);">+    """Encode a big-endian 48bit number from integer to binary."""</span><br><span style="color: hsl(120, 100%, 40%);">+    x3 = x & 0xffff</span><br><span style="color: hsl(120, 100%, 40%);">+    x2 = x >> 16</span><br><span style="color: hsl(120, 100%, 40%);">+    return struct.pack('>IH', x2, x3)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+def milenage_generate(opc:bytes, amf:bytes, k:bytes, sqn:bytes, rand:bytes) -> dict():</span><br><span style="color: hsl(120, 100%, 40%);">+    """Generate an MILENAGE Authentication Tuple."""</span><br><span style="color: hsl(120, 100%, 40%);">+    m = Milenage(None)</span><br><span style="color: hsl(120, 100%, 40%);">+    m.set_opc(opc)</span><br><span style="color: hsl(120, 100%, 40%);">+    mac_a = m.f1(k, rand, sqn, amf)</span><br><span style="color: hsl(120, 100%, 40%);">+    res, ck, ik, ak = m.f2345(k, rand)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    # AUTN = (SQN ^ AK) || AMF || MAC</span><br><span style="color: hsl(120, 100%, 40%);">+    sqn_ak = xor_buf(sqn, ak)</span><br><span style="color: hsl(120, 100%, 40%);">+    autn = b''.join([sqn_ak, amf, mac_a])</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    return {'res': res, 'ck': ck, 'ik': ik, 'autn': autn}</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+def milenage_auts(opc:bytes, k:bytes, rand:bytes, auts:bytes):</span><br><span style="color: hsl(120, 100%, 40%);">+    """Validate AUTS. If successful, returns SQN_MS"""</span><br><span style="color: hsl(120, 100%, 40%);">+    amf = b'\x00\x00' # TS 33.102 Section 6.3.3</span><br><span style="color: hsl(120, 100%, 40%);">+    m = Milenage(None)</span><br><span style="color: hsl(120, 100%, 40%);">+    m.set_opc(opc)</span><br><span style="color: hsl(120, 100%, 40%);">+    ak = m.f5star(k, rand)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    sqn_ak = auts[:6]</span><br><span style="color: hsl(120, 100%, 40%);">+    sqn = xor_buf(sqn_ak, ak[:6])</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    mac_s = m.f1star(k, rand, sqn, amf)</span><br><span style="color: hsl(120, 100%, 40%);">+    if mac_s == auts[6:14]:</span><br><span style="color: hsl(120, 100%, 40%);">+        return sqn</span><br><span style="color: hsl(120, 100%, 40%);">+    else:</span><br><span style="color: hsl(120, 100%, 40%);">+        return False</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+def build_url(suffix:str) -> str:</span><br><span style="color: hsl(120, 100%, 40%);">+    """Build an URL from global server_host, server_port, BASE_PATH and suffix."""</span><br><span style="color: hsl(120, 100%, 40%);">+    BASE_PATH= "/sim-auth-api/v1"</span><br><span style="color: hsl(120, 100%, 40%);">+    return "http://%s:%u%s%s" % (server_host, server_port, BASE_PATH, suffix)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+def rest_post(suffix:str, js:dict = None):</span><br><span style="color: hsl(120, 100%, 40%);">+    """Perform a RESTful POST."""</span><br><span style="color: hsl(120, 100%, 40%);">+    url = build_url(suffix)</span><br><span style="color: hsl(120, 100%, 40%);">+    if verbose:</span><br><span style="color: hsl(120, 100%, 40%);">+        print("POST %s (%s)" % (url, str(js)))</span><br><span style="color: hsl(120, 100%, 40%);">+    resp = requests.post(url, json=js)</span><br><span style="color: hsl(120, 100%, 40%);">+    if verbose:</span><br><span style="color: hsl(120, 100%, 40%);">+        print("-> %s" % (resp))</span><br><span style="color: hsl(120, 100%, 40%);">+    if not resp.ok:</span><br><span style="color: hsl(120, 100%, 40%);">+        print("POST failed")</span><br><span style="color: hsl(120, 100%, 40%);">+    return resp</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+def main(argv):</span><br><span style="color: hsl(120, 100%, 40%);">+    global server_port, server_host, verbose</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    parser = argparse.ArgumentParser()</span><br><span style="color: hsl(120, 100%, 40%);">+    parser.add_argument("-H", "--host", help="Host to connect to", default="localhost")</span><br><span style="color: hsl(120, 100%, 40%);">+    parser.add_argument("-p", "--port", help="TCP port to connect to", default=8000)</span><br><span style="color: hsl(120, 100%, 40%);">+    parser.add_argument("-v", "--verbose", help="increase output verbosity", action='count', default=0)</span><br><span style="color: hsl(120, 100%, 40%);">+    parser.add_argument("-n", "--slot-nr", help="SIM slot number", type=int, default=0)</span><br><span style="color: hsl(120, 100%, 40%);">+    parser.add_argument("-c", "--count", help="Auth count", type=int, default=10)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    parser.add_argument("-k", "--key", help="Secret key K (hex)", type=str, required=True)</span><br><span style="color: hsl(120, 100%, 40%);">+    parser.add_argument("-o", "--opc", help="Secret OPc (hex)", type=str, required=True)</span><br><span style="color: hsl(120, 100%, 40%);">+    parser.add_argument("-a", "--amf", help="AMF Field (hex)", type=str, default="0000")</span><br><span style="color: hsl(120, 100%, 40%);">+    parser.add_argument("-s", "--sqn", help="SQN Field (hex)", type=str, default="000000000000")</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    args = parser.parse_args()</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    server_host = args.host</span><br><span style="color: hsl(120, 100%, 40%);">+    server_port = args.port</span><br><span style="color: hsl(120, 100%, 40%);">+    verbose = args.verbose</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    #opc = bytes.fromhex('767A662ACF4587EB0C450C6A95540A04')</span><br><span style="color: hsl(120, 100%, 40%);">+    #k = bytes.fromhex('876B2D8D403EE96755BEF3E0A1857EBE')</span><br><span style="color: hsl(120, 100%, 40%);">+    opc = bytes.fromhex(args.opc)</span><br><span style="color: hsl(120, 100%, 40%);">+    k = bytes.fromhex(args.key)</span><br><span style="color: hsl(120, 100%, 40%);">+    amf = bytes.fromhex(args.amf)</span><br><span style="color: hsl(120, 100%, 40%);">+    sqn = bytes.fromhex(args.sqn)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    for i in range(args.count):</span><br><span style="color: hsl(120, 100%, 40%);">+        rand = secrets.token_bytes(16)</span><br><span style="color: hsl(120, 100%, 40%);">+        t = milenage_generate(opc=opc, amf=amf, k=k, sqn=sqn, rand=rand)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+        req_json = {'rand': rand.hex(), 'autn': t['autn'].hex()}</span><br><span style="color: hsl(120, 100%, 40%);">+        print("-> %s" % req_json)</span><br><span style="color: hsl(120, 100%, 40%);">+        resp = rest_post('/slot/%u' % args.slot_nr, req_json)</span><br><span style="color: hsl(120, 100%, 40%);">+        resp_json = resp.json()</span><br><span style="color: hsl(120, 100%, 40%);">+        print("<- %s" % resp_json)</span><br><span style="color: hsl(120, 100%, 40%);">+        if 'synchronisation_failure' in resp_json:</span><br><span style="color: hsl(120, 100%, 40%);">+            auts = bytes.fromhex(resp_json['synchronisation_failure']['auts'])</span><br><span style="color: hsl(120, 100%, 40%);">+            sqn_ms = milenage_auts(opc, k, rand, auts)</span><br><span style="color: hsl(120, 100%, 40%);">+            if sqn_ms != False:</span><br><span style="color: hsl(120, 100%, 40%);">+                print("SQN_MS = %s" % sqn_ms.hex())</span><br><span style="color: hsl(120, 100%, 40%);">+                sqn_ms_int = unpack48(sqn_ms)</span><br><span style="color: hsl(120, 100%, 40%);">+                # we assume an IND bit-length of 5 here</span><br><span style="color: hsl(120, 100%, 40%);">+                sqn = pack48(sqn_ms_int + (1 << 5))</span><br><span style="color: hsl(120, 100%, 40%);">+            else:</span><br><span style="color: hsl(120, 100%, 40%);">+                raise RuntimeError("AUTS auth failure during re-sync?!?")</span><br><span style="color: hsl(120, 100%, 40%);">+        elif 'successful_3g_authentication' in resp_json:</span><br><span style="color: hsl(120, 100%, 40%);">+            auth_res = resp_json['successful_3g_authentication']</span><br><span style="color: hsl(120, 100%, 40%);">+            assert bytes.fromhex(auth_res['res']) == t['res']</span><br><span style="color: hsl(120, 100%, 40%);">+            assert bytes.fromhex(auth_res['ck']) == t['ck']</span><br><span style="color: hsl(120, 100%, 40%);">+            assert bytes.fromhex(auth_res['ik']) == t['ik']</span><br><span style="color: hsl(120, 100%, 40%);">+            # we assume an IND bit-length of 5 here</span><br><span style="color: hsl(120, 100%, 40%);">+            sqn = pack48(unpack48(sqn) + (1 << 5))</span><br><span style="color: hsl(120, 100%, 40%);">+        else:</span><br><span style="color: hsl(120, 100%, 40%);">+            raise RuntimeError("Auth failure")</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+if __name__ == "__main__":</span><br><span style="color: hsl(120, 100%, 40%);">+    main(sys.argv)</span><br><span>diff --git a/contrib/sim-rest-server.py b/contrib/sim-rest-server.py</span><br><span>new file mode 100755</span><br><span>index 0000000..a727d2f</span><br><span>--- /dev/null</span><br><span>+++ b/contrib/sim-rest-server.py</span><br><span>@@ -0,0 +1,89 @@</span><br><span style="color: hsl(120, 100%, 40%);">+#!/usr/bin/env python3</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+# RESTful HTTP service for performing authentication against USIM cards</span><br><span style="color: hsl(120, 100%, 40%);">+#</span><br><span style="color: hsl(120, 100%, 40%);">+# (C) 2021 by Harald Welte <laforge@osmocom.org></span><br><span style="color: hsl(120, 100%, 40%);">+#</span><br><span style="color: hsl(120, 100%, 40%);">+# This program is free software: you can redistribute it and/or modify</span><br><span style="color: hsl(120, 100%, 40%);">+# it under the terms of the GNU General Public License as published by</span><br><span style="color: hsl(120, 100%, 40%);">+# the Free Software Foundation, either version 2 of the License, or</span><br><span style="color: hsl(120, 100%, 40%);">+# (at your option) any later version.</span><br><span style="color: hsl(120, 100%, 40%);">+#</span><br><span style="color: hsl(120, 100%, 40%);">+# This program is distributed in the hope that it will be useful,</span><br><span style="color: hsl(120, 100%, 40%);">+# but WITHOUT ANY WARRANTY; without even the implied warranty of</span><br><span style="color: hsl(120, 100%, 40%);">+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the</span><br><span style="color: hsl(120, 100%, 40%);">+# GNU General Public License for more details.</span><br><span style="color: hsl(120, 100%, 40%);">+#</span><br><span style="color: hsl(120, 100%, 40%);">+# You should have received a copy of the GNU General Public License</span><br><span style="color: hsl(120, 100%, 40%);">+# along with this program.  If not, see <http://www.gnu.org/licenses/>.</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+import json</span><br><span style="color: hsl(120, 100%, 40%);">+import sys</span><br><span style="color: hsl(120, 100%, 40%);">+import argparse</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+from klein import run, route</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+from pySim.transport import ApduTracer</span><br><span style="color: hsl(120, 100%, 40%);">+from pySim.transport.pcsc import PcscSimLink</span><br><span style="color: hsl(120, 100%, 40%);">+from pySim.commands import SimCardCommands</span><br><span style="color: hsl(120, 100%, 40%);">+from pySim.cards import UsimCard</span><br><span style="color: hsl(120, 100%, 40%);">+from pySim.exceptions import *</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+class ApduPrintTracer(ApduTracer):</span><br><span style="color: hsl(120, 100%, 40%);">+    def trace_response(self, cmd, sw, resp):</span><br><span style="color: hsl(120, 100%, 40%);">+        #print("CMD: %s -> RSP: %s %s" % (cmd, sw, resp))</span><br><span style="color: hsl(120, 100%, 40%);">+        pass</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+@route('/sim-auth-api/v1/slot/<int:slot>')</span><br><span style="color: hsl(120, 100%, 40%);">+def auth(request, slot):</span><br><span style="color: hsl(120, 100%, 40%);">+    """REST API endpoint for performing authentication against a USIM.</span><br><span style="color: hsl(120, 100%, 40%);">+       Expects a JSON body containing RAND and AUTN.</span><br><span style="color: hsl(120, 100%, 40%);">+       Returns a JSON body containing RES, CK, IK and Kc."""</span><br><span style="color: hsl(120, 100%, 40%);">+    try:</span><br><span style="color: hsl(120, 100%, 40%);">+        # there are two hex-string JSON parameters in the body: rand and autn</span><br><span style="color: hsl(120, 100%, 40%);">+        content = json.loads(request.content.read())</span><br><span style="color: hsl(120, 100%, 40%);">+        rand = content['rand']</span><br><span style="color: hsl(120, 100%, 40%);">+        autn = content['autn']</span><br><span style="color: hsl(120, 100%, 40%);">+    except:</span><br><span style="color: hsl(120, 100%, 40%);">+        request.setResponseCode(400)</span><br><span style="color: hsl(120, 100%, 40%);">+        return "Malformed Request"</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    try:</span><br><span style="color: hsl(120, 100%, 40%);">+        tp = PcscSimLink(slot, apdu_tracer=ApduPrintTracer())</span><br><span style="color: hsl(120, 100%, 40%);">+        tp.connect()</span><br><span style="color: hsl(120, 100%, 40%);">+    except ReaderError:</span><br><span style="color: hsl(120, 100%, 40%);">+        request.setResponseCode(404)</span><br><span style="color: hsl(120, 100%, 40%);">+        return "Specified SIM Slot doesn't exist"</span><br><span style="color: hsl(120, 100%, 40%);">+    except ProtocolError:</span><br><span style="color: hsl(120, 100%, 40%);">+        request.setResponseCode(500)</span><br><span style="color: hsl(120, 100%, 40%);">+        return "Error"</span><br><span style="color: hsl(120, 100%, 40%);">+    except NoCardError:</span><br><span style="color: hsl(120, 100%, 40%);">+        request.setResponseCode(410)</span><br><span style="color: hsl(120, 100%, 40%);">+        return "No SIM card inserted in slot"</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    scc = SimCardCommands(tp)</span><br><span style="color: hsl(120, 100%, 40%);">+    card = UsimCard(scc)</span><br><span style="color: hsl(120, 100%, 40%);">+    try:</span><br><span style="color: hsl(120, 100%, 40%);">+        card.read_aids()</span><br><span style="color: hsl(120, 100%, 40%);">+        card.select_adf_by_aid(adf='usim')</span><br><span style="color: hsl(120, 100%, 40%);">+        res, sw = scc.authenticate(rand, autn)</span><br><span style="color: hsl(120, 100%, 40%);">+    except SwMatchError as e:</span><br><span style="color: hsl(120, 100%, 40%);">+        request.setResponseCode(500)</span><br><span style="color: hsl(120, 100%, 40%);">+        return "Communication Error %s" % e</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    tp.disconnect()</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    return json.dumps(res, indent=4)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+def main(argv):</span><br><span style="color: hsl(120, 100%, 40%);">+    parser = argparse.ArgumentParser()</span><br><span style="color: hsl(120, 100%, 40%);">+    parser.add_argument("-H", "--host", help="Host/IP to bind HTTP to", default="localhost")</span><br><span style="color: hsl(120, 100%, 40%);">+    parser.add_argument("-p", "--port", help="TCP port to bind HTTP to", default=8000)</span><br><span style="color: hsl(120, 100%, 40%);">+    #parser.add_argument("-v", "--verbose", help="increase output verbosity", action='count', default=0)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    args = parser.parse_args()</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    run(args.host, args.port)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+if __name__ == "__main__":</span><br><span style="color: hsl(120, 100%, 40%);">+    main(sys.argv)</span><br><span></span><br></pre><p>To view, visit <a href="https://gerrit.osmocom.org/c/pysim/+/24342">change 24342</a>. To unsubscribe, or for help writing mail filters, visit <a href="https://gerrit.osmocom.org/settings">settings</a>.</p><div itemscope itemtype="http://schema.org/EmailMessage"><div itemscope itemprop="action" itemtype="http://schema.org/ViewAction"><link itemprop="url" href="https://gerrit.osmocom.org/c/pysim/+/24342"/><meta itemprop="name" content="View Change"/></div></div>

<div style="display:none"> Gerrit-Project: pysim </div>
<div style="display:none"> Gerrit-Branch: master </div>
<div style="display:none"> Gerrit-Change-Id: I738ca3109ab038d4f5595cc1dab6a49087df5886 </div>
<div style="display:none"> Gerrit-Change-Number: 24342 </div>
<div style="display:none"> Gerrit-PatchSet: 1 </div>
<div style="display:none"> Gerrit-Owner: laforge <laforge@osmocom.org> </div>
<div style="display:none"> Gerrit-MessageType: newchange </div>