diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 00000000..79aaae35 --- /dev/null +++ b/.github/README.md @@ -0,0 +1,36 @@ +This is a test fork of the [Python-based EUDIW +issuer](https://github.com/eu-digital-identity-wallet/eudi-srv-web-issuing-eudiw-py/), +aiming to ease local test deployments. + +## Requirements + +This setup assumes two devices (one Android, one Linux). + +## Set up + +### This repo (issuer) + +1. Switch to branch "local-deploy-v2". + +2. Run `./setup-issuer.sh` to setup the issuer (e.g., set up virtual + environment, install dependencies, generate self-signed certificate + bound to the local host IP). + +4. Run `./run-issuer.sh` to spin up the issuer server. + +### Android wallet + +1. Clone [the Android app fork](https://github.com/gfour/eudi-app-android-wallet-ui) + and switch to branch "local-deploy". + +3. Run the issuer as above + +4. Build the Android app (`./gradlew assembleDevDebug` or through Android Studio) and deploy + it to the connected Android device (`adb install path/to/app.apk`). + +## Use + +1. Linux device: choose a credential type to issue (`https://:5000/credential_offer_choice`) + and continue to generate a QR code for a credential. + +2. Scan the QR code with the Android app. When prompted, use the Form Country (FC). diff --git a/.gitignore b/.gitignore index 1298c6c7..10227a88 100644 --- a/.gitignore +++ b/.gitignore @@ -203,3 +203,18 @@ app/tests/output.xml app/tests/output.xml app/tests/report.html log + +# IP file +.config.ip + +# Local keys, regenerated on server startup +app/static/jwks.json +app/private/cookie_jwks.json + +# vim swap files +*.sw* + + +*.pem + +.idea/ \ No newline at end of file diff --git a/app/app_config/config_countries.py b/app/app_config/config_countries.py index 0a669793..99685029 100644 --- a/app/app_config/config_countries.py +++ b/app/app_config/config_countries.py @@ -36,10 +36,10 @@ class ConfCountries: "EU": { "name": "nodeEU", "pid_url_oidc": cfgserv.service_url + "eidasnode/lightrequest?country=EU", - "pid_mdoc_privkey": "/etc/eudiw/pid-issuer/privKey/PID-DS-0001_EU.pem", + "pid_mdoc_privkey": "/etc/eudiw/pid-issuer/privKey/PID-DS-0002.cert.der", # "pid_mdoc_privkey": 'app\certs\PID-DS-0001_EU.pem', - "pid_mdoc_privkey_passwd": None, # None or bytes, - "pid_mdoc_cert": "/etc/eudiw/pid-issuer/cert/PID-DS-0001_EU_cert.der", + "pid_mdoc_privkey_passwd": b"pid-ds-0002", # None or bytes, + "pid_mdoc_cert": "/etc/eudiw/pid-issuer/cert/PID-DS-0002.pid-ds-0002.key.pem", "loa": "http://eidas.europa.eu/LoA/high", "supported_credentials": [ "eu.europa.ec.eudi.pid_mdoc", @@ -56,11 +56,14 @@ class ConfCountries: formCountry: { "name": "FormEU", "pid_url": cfgserv.service_url + "pid/form", - "pid_mdoc_privkey": "/etc/eudiw/pid-issuer/privKey/PID-DS-0001_UT.pem", + "pid_mdoc_privkey": "/etc/eudiw/pid-issuer/privKey/PID-DS-0002.pid-ds-0002.key.pem", + # "pid_mdoc_privkey": "/etc/eudiw/pid-issuer/privKey/PID-DS-0001_UT.pem", # "pid_mdoc_privkey": "/etc/eudiw/pid-issuer/privKey/hackathon-DS-0001_UT.pem", # "pid_mdoc_privkey": 'app\certs\PID-DS-0001_UT.pem', - "pid_mdoc_privkey_passwd": None, # None or bytes - "pid_mdoc_cert": "/etc/eudiw/pid-issuer/cert/PID-DS-0001_UT_cert.der", + # "pid_mdoc_privkey_passwd": None, # None or bytes + "pid_mdoc_privkey_passwd": b"pid-ds-0002", # None or bytes + "pid_mdoc_cert": "/etc/eudiw/pid-issuer/cert/PID-DS-0002.cert.der", + # "pid_mdoc_cert": "/etc/eudiw/pid-issuer/cert/PID-DS-0001_UT_cert.der", # "pid_mdoc_cert": "/etc/eudiw/pid-issuer/cert/hackathon-DS-0001_UT_cert.der", "un_distinguishing_sign": "FC", "supported_credentials": [ diff --git a/app/data_management.py b/app/data_management.py index 3222b700..d8c6e7ce 100644 --- a/app/data_management.py +++ b/app/data_management.py @@ -108,7 +108,7 @@ def clear_par(): request_data = json.dumps(request_data) request_headers = deferredRequests[req]["headers"] - response = requests.post(cfgservice.service_url+"credential", data=request_data, headers=request_headers) + response = requests.post(cfgservice.service_url+"credential", data=request_data, headers=request_headers, verify=False) response_data = response.json() if response.status_code == 200: diff --git a/app/formatter_func.py b/app/formatter_func.py index 5ee65187..ce042fdf 100644 --- a/app/formatter_func.py +++ b/app/formatter_func.py @@ -144,7 +144,7 @@ def mdocFormatter(data, doctype, country, device_publickey): 'X-Api-Key': revocation_api_key } - response = requests.get(cfgservice.revocation_service_url, headers=headers, data=payload) + response = requests.get(cfgservice.revocation_service_url, headers=headers, data=payload, verify=False) if response.status_code == 200: revocation_json = response.json() @@ -241,7 +241,7 @@ def sdjwtFormatter(PID, country): 'X-Api-Key': revocation_api_key } - response = requests.get(cfgservice.revocation_service_url, headers=headers, data=payload) + response = requests.get(cfgservice.revocation_service_url, headers=headers, data=payload, verify=False) if response.status_code == 200: revocation_json = response.json() diff --git a/app/metadata_config/credentials_supported/pid_jwt_vc_json.json b/app/metadata_config/credentials_supported/pid_jwt_vc_json.json index 3320a525..e6f7980a 100644 --- a/app/metadata_config/credentials_supported/pid_jwt_vc_json.json +++ b/app/metadata_config/credentials_supported/pid_jwt_vc_json.json @@ -1,6 +1,7 @@ { "eu.europa.ec.eudi.pid_jwt_vc_json": { "format": "vc+sd-jwt", + "doctype": "eu.europa.ec.eudi.pid.1", "scope": "eu.europa.ec.eudi.pid.1", "cryptographic_binding_methods_supported": [ "jwk", "cose_key" diff --git a/app/metadata_config/metadata_config.json b/app/metadata_config/metadata_config.json index 860ca8b7..ab9f315a 100644 --- a/app/metadata_config/metadata_config.json +++ b/app/metadata_config/metadata_config.json @@ -1,15 +1,15 @@ { - "credential_issuer": "https://issuer.eudiw.dev", - "credential_endpoint": "https://issuer.eudiw.dev/credential", - "batch_credential_endpoint": "https://issuer.eudiw.dev/batch_credential", - "notification_endpoint": "https://issuer.eudiw.dev/notification", - "deferred_credential_endpoint": "https://issuer.eudiw.dev/deferred_credential", + "credential_issuer": "https://192.168.2.4:5000", + "credential_endpoint": "https://192.168.2.4:5000/credential", + "batch_credential_endpoint": "https://192.168.2.4:5000/batch_credential", + "notification_endpoint": "https://192.168.2.4:5000/notification", + "deferred_credential_endpoint": "https://192.168.2.4:5000/deferred_credential", "display": [ { "name": "Digital Credentials Issuer", "locale": "en", "logo": { - "uri": "https://issuer.eudiw.dev/ic-logo.png", + "uri": "https://192.168.2.4:5000/ic-logo.png", "alt_text": "EU Digital Identity Wallet Logo" } } diff --git a/app/metadata_config/oauth-authorization-server.json b/app/metadata_config/oauth-authorization-server.json index 4113ee47..efaea26b 100644 --- a/app/metadata_config/oauth-authorization-server.json +++ b/app/metadata_config/oauth-authorization-server.json @@ -1,10 +1,10 @@ { "issuer": - "https://issuer.eudiw.dev", + "https://192.168.2.4:5000", "authorization_endpoint": - "https://issuer.eudiw.dev/authorizationV3", + "https://192.168.2.4:5000/authorizationV3", "token_endpoint": - "https://issuer.eudiw.dev/token", + "https://192.168.2.4:5000/token", "token_endpoint_auth_methods_supported": ["public"], "token_endpoint_auth_signing_alg_values_supported": @@ -12,11 +12,11 @@ "code_challenge_methods_supported": ["S256"], "userinfo_endpoint": - "https://issuer.eudiw.dev/userinfo", + "https://192.168.2.4:5000/userinfo", "jwks_uri": - "https://issuer.eudiw.dev/static/jwks.json", + "https://192.168.2.4:5000/static/jwks.json", "registration_endpoint": - "https://issuer.eudiw.dev/registration", + "https://192.168.2.4:5000/registration", "scopes_supported": [ "openid"], "response_types_supported": diff --git a/app/metadata_config/openid-configuration.json b/app/metadata_config/openid-configuration.json index 7ddc19eb..75e3d267 100644 --- a/app/metadata_config/openid-configuration.json +++ b/app/metadata_config/openid-configuration.json @@ -13,7 +13,7 @@ "urn:ietf:params:oauth:grant-type:jwt-bearer", "refresh_token" ], - "jwks_uri": "https://issuer.eudiw.dev/static/jwks.json", + "jwks_uri": "https://192.168.2.4:5000/static/jwks.json", "scopes_supported": [ "openid" ], @@ -78,13 +78,13 @@ "code_challenge_methods_supported": [ "S256" ], - "issuer": "https://issuer.eudiw.dev", - "registration_endpoint": "https://issuer.eudiw.dev/registration", - "introspection_endpoint": "https://issuer.eudiw.dev/introspection", - "authorization_endpoint": "https://issuer.eudiw.dev/authorizationV3", - "token_endpoint": "https://issuer.eudiw.dev/token", - "userinfo_endpoint": "https://issuer.eudiw.dev/userinfo", - "end_session_endpoint": "https://issuer.eudiw.dev/session", - "pushed_authorization_request_endpoint": "https://issuer.eudiw.dev/pushed_authorizationv2", - "credential_endpoint": "https://issuer.eudiw.dev/credential" + "issuer": "https://192.168.2.4:5000", + "registration_endpoint": "https://192.168.2.4:5000/registration", + "introspection_endpoint": "https://192.168.2.4:5000/introspection", + "authorization_endpoint": "https://192.168.2.4:5000/authorizationV3", + "token_endpoint": "https://192.168.2.4:5000/token", + "userinfo_endpoint": "https://192.168.2.4:5000/userinfo", + "end_session_endpoint": "https://192.168.2.4:5000/session", + "pushed_authorization_request_endpoint": "https://192.168.2.4:5000/pushed_authorizationv2", + "credential_endpoint": "https://192.168.2.4:5000/credential" } \ No newline at end of file diff --git a/app/private/cookie_jwks.json b/app/private/cookie_jwks.json deleted file mode 100644 index 8956ebcb..00000000 --- a/app/private/cookie_jwks.json +++ /dev/null @@ -1 +0,0 @@ -{"keys": [{"kty": "oct", "use": "enc", "kid": "enc", "k": "XDU4HzMHeWeByr3pjqg0zCgtjwBNfhLM"}, {"kty": "oct", "use": "sig", "kid": "sig", "k": "ajW9gh8sYZ_3AnMUzpAlvwqtqovDLgIA"}]} \ No newline at end of file diff --git a/app/redirect_func.py b/app/redirect_func.py index 54272967..ff5dc544 100644 --- a/app/redirect_func.py +++ b/app/redirect_func.py @@ -77,5 +77,5 @@ def json_post(url_path: str, json: dict): Return: Returns the answer to the HTTP POST """ return requests.post( - url_path, json=json, headers={"Content-Type": "application/json"} + url_path, json=json, headers={"Content-Type": "application/json"}, verify=False ) diff --git a/app/requirements.txt b/app/requirements.txt index b26043fa..436270d9 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -28,4 +28,6 @@ git+https://github.com/eu-digital-identity-wallet/openid4v.git #git+https://github.com/openwallet-foundation-labs/sd-jwt-python.git@9181a0a4514b7de2cc30a84d320b509e9fc5cb59 git+https://github.com/openwallet-foundation-labs/sd-jwt-python.git@v0.10.4-1 git+https://github.com/rohe/fedservice.git@a4bef2e3f230f4b07c6ef45e4c8be55778d0561e -git+https://github.com/rohe/idpy-sdjwt.git@de1715ef89c7f3db5daf3cdc53fec26d6ade03d9 \ No newline at end of file +git+https://github.com/rohe/idpy-sdjwt.git@de1715ef89c7f3db5daf3cdc53fec26d6ade03d9 +https://github.com/wbond/oscrypto/archive/d5f3437ed24257895ae1edd9e503cfb352e635a8.zip +pudb diff --git a/app/route_dynamic.py b/app/route_dynamic.py index 3c19a487..3e37db66 100644 --- a/app/route_dynamic.py +++ b/app/route_dynamic.py @@ -266,7 +266,7 @@ def dynamic_R1(country): country_data = cfgcountries.supported_countries[country]["oidc_auth"] metadata_url = country_data["base_url"] + "/.well-known/openid-configuration" - metadata_json = requests.get(metadata_url).json() + metadata_json = requests.get(metadata_url, verify=False).json() authorization_endpoint = metadata_json["authorization_endpoint"] @@ -422,7 +422,7 @@ def red(): metadata_url = cfgcountries.supported_countries[session["country"]]["oidc_auth"]["base_url"] + "/.well-known/openid-configuration" - metadata_json = requests.get(metadata_url).json() + metadata_json = requests.get(metadata_url, verify=False).json() token_endpoint = metadata_json["token_endpoint"] @@ -626,7 +626,7 @@ def dynamic_R2_data_collect(country, user_id): url = attribute_request["url"] + user_id # headers = attribute_request["header"] try: - r2 = requests.get(url) + r2 = requests.get(url, verify=False) json_response = r2.json() for attribute in json_response: @@ -653,7 +653,7 @@ def dynamic_R2_data_collect(country, user_id): ] + "/.well-known/openid-configuration" ) - metadata_json = requests.get(metadata_url).json() + metadata_json = requests.get(metadata_url, verify=False).json() userinfo_endpoint = metadata_json["userinfo_endpoint"] @@ -667,7 +667,7 @@ def dynamic_R2_data_collect(country, user_id): headers["Authorization"] = f"Bearer {user_id}" try: - r2 = requests.get(url, headers=headers) + r2 = requests.get(url, headers=headers, verify=False) json_response = json.loads(r2.text) data = json_response if ( diff --git a/app/route_formatter.py b/app/route_formatter.py index 20b22e11..b6315df2 100644 --- a/app/route_formatter.py +++ b/app/route_formatter.py @@ -170,8 +170,9 @@ def cborformatter(): } ) + from samples import inject_sample_data base64_mdoc = mdocFormatter( - request.json["data"], + inject_sample_data(request.json), request.json["doctype"], request.json["country"], request.json["device_publickey"], diff --git a/app/route_oidc.py b/app/route_oidc.py index 5d302c57..31863bdb 100644 --- a/app/route_oidc.py +++ b/app/route_oidc.py @@ -25,12 +25,8 @@ import base64 import hashlib import io -import random import re -import sys -import time import uuid -import threading import urllib.parse import segno @@ -45,7 +41,7 @@ render_template, url_for, ) -from flask.helpers import make_response, send_from_directory +from flask.helpers import make_response import os from flask_cors import CORS @@ -55,9 +51,7 @@ import sys import traceback from typing import Union -from urllib.parse import urlparse -from cryptojwt import as_unicode from idpyoidc.message.oidc import AccessTokenRequest import werkzeug @@ -234,6 +228,7 @@ def verify_user(): @oidc.route("/.well-known/") def well_known(service): + session_id = request.values.get("sessionId") if service == "openid-credential-issuer": info = { "response": oidc_metadata, @@ -357,14 +352,16 @@ def authorizationv2( url = url + "&authorization_details=" + authorization_details if code_challenge and code_challenge_method: - url = url + "&code_challenge=" - +code_challenge - +"&code_challenge_method=" - +code_challenge_method + url = ( + url + "&code_challenge=" + + code_challenge + + "&code_challenge_method=" + + code_challenge_method + ) payload = {} headers = {} - response = requests.request("GET", url, headers=headers, data=payload) + response = requests.request("GET", url, headers=headers, data=payload, verify=False) if response.status_code != 200: cfgservice.app_logger.error("Authorization endpoint invalid request") @@ -391,10 +388,7 @@ def authorizationv2( session["authorization_params"] = params - session_id = str(uuid.uuid4()) - session_ids.update( - {session_id: {"expires": datetime.now() + timedelta(minutes=60)}} - ) + session_id = request.values["session-id"] session["session_id"] = session_id cfgservice.app_logger.info( ", Session ID: " @@ -492,7 +486,7 @@ def authorizationV3(): payload = {} headers = {} - response = requests.request("GET", url, headers=headers, data=payload) + response = requests.request("GET", url, headers=headers, data=payload, verify=False) if response.status_code != 200: cfgservice.app_logger.error("Authorization endpoint invalid request") @@ -536,7 +530,7 @@ def pid_authorization_get(): "Content-Type": "application/json", } - response = requests.request("GET", url, headers=headers) + response = requests.request("GET", url, headers=headers, verify=False) if response.status_code != 200: error_msg = str(response.status_code) return jsonify({"error": error_msg}), 500 @@ -713,7 +707,7 @@ def token(): ) headers = {"Content-Type": "application/x-www-form-urlencoded"} - response = requests.request("POST", url, headers=headers, data=payload) + response = requests.request("POST", url, headers=headers, data=payload, verify=False) if response.status_code != 200: return make_response("invalid_request", 400) @@ -771,9 +765,7 @@ def par_endpoint(): @oidc.route("/pushed_authorizationv2", methods=["POST"]) def par_endpointv2(): - - session_id = str(uuid.uuid4()) - + session_id = request.values["sessionId"] cfgservice.app_logger.info( ", Session ID: " + session_id @@ -812,21 +804,15 @@ def par_endpointv2(): + str(json.loads(response.get_data())) ) - session_ids.update( - { - session_id: { - "expires": datetime.now() + timedelta(minutes=60), - "request_uri": json.loads(response.get_data())["request_uri"], - } - } - ) + session_ids[session_id].update({ + "request_uri": json.loads(response.get_data())["request_uri"], + }) return response @oidc.route("/credential", methods=["POST"]) def credential(): - headers = dict(request.headers) payload = json.loads(request.data) @@ -835,7 +821,7 @@ def credential(): access_token = headers["Authorization"][7:] session_id = getSessionId_accessToken(access_token) - + session_id = request.values["sessionId"] cfgservice.app_logger.info( ", Session ID: " + session_id @@ -891,6 +877,7 @@ def credential(): + "Credential response, Payload: " + str(_response) ) + session_ids[session_id]["status"] = "success" return _response @@ -1117,81 +1104,138 @@ def load_test(): return "load" """ +@oidc.route("/issueStatus", methods=["GET"]) +def issue_status(): + session_id = request.values["sessionId"] + + if session_id in session_ids: + return { + "status": "pending", + "reason": "ok", + "sessionId": session_id, + }, 200 + else: + return { + "status": "fail", + "reason": "no issuance session found", + "sessionId": session_id, + }, 500 + + + +def create_qr_code(credential_offer) -> tuple[str, str]: + json_string = json.dumps(credential_offer) + credential_offer_uri = oidc_metadata["credential_endpoint"] + uri = f"{credential_offer_uri}?credential_offer=" + urllib.parse.quote(json_string, safe=":/") + + qrcode = segno.make(uri) + out = io.BytesIO() + qrcode.save(out, kind="png", scale=3) + + qr_img_base64 = "data:image/png;base64," + base64.b64encode(out.getvalue()).decode("utf-8") + cfgservice.app_logger.info(uri) + return uri, qr_img_base64 + + @oidc.route("/credential_offer", methods=["GET", "POST"]) def credentialOffer(): + values = request.values - credentialsSupported = oidc_metadata["credential_configurations_supported"] - auth_choice = request.form.get("Authorization Code Grant") - form_keys = request.form.keys() - credential_offer_URI = request.form.get("credential_offer_URI") - - if "proceed" in form_keys: - form = list(form_keys) - form.remove("proceed") - form.remove("credential_offer_URI") - form.remove("Authorization Code Grant") - all_exist = all(credential in credentialsSupported for credential in form) - - if all_exist: - credentials_id = form - session["credentials_id"] = credentials_id - credentials_id_list = json.dumps(form) - if auth_choice == "pre_auth_code": - session["credential_offer_URI"] = credential_offer_URI - return redirect( - url_for("preauth.preauthRed", credentials_id=credentials_id_list) - ) + if "sessionId" in values and "credentialType" in values: - else: + session_id = values["sessionId"] + credential_type = values["credentialType"] - credential_offer = { - "credential_issuer": cfgservice.service_url[:-1], - "credential_configuration_ids": credentials_id, - "grants": {"authorization_code": {}}, - } + session_ids[session_id] = { + "expires": datetime.now() + timedelta(minutes=60), + } - # create URI - json_string = json.dumps(credential_offer) - uri = ( - f"{credential_offer_URI}credential_offer?credential_offer=" - + urllib.parse.quote(json_string, safe=":/") - ) + if credential_type == "pid": + credential_id = "eu.europa.ec.eudi.pid_mdoc" + elif credential_type == "pid_jwt": + credential_id = "eu.europa.ec.eudi.pid_jwt_vc_json" + else: + return "Credential type not found", 400 + + credential_offer = { + "credential_issuer": cfgservice.service_url[:-1], + "credential_configuration_ids": [credential_id,], + "grants": { + "authorization_code": {} + }, + "issuer_state": str(uuid.uuid4()), + } - # Generate QR code - # img = qrcode.make("uri") - # QRCode.print_ascii() - - qrcode = segno.make(uri) - out = io.BytesIO() - qrcode.save(out, kind="png", scale=3) - - """ qrcode.to_artistic( - background=cfgtest.qr_png, - target=out, - kind="png", - scale=4, - ) """ - # qrcode.terminal() - # qr_img_base64 = qrcode.png_data_uri(scale=4) - - qr_img_base64 = "data:image/png;base64," + base64.b64encode( - out.getvalue() - ).decode("utf-8") - - wallet_url = cfgservice.wallet_test_url + "credential_offer" - - return render_template( - "openid/credential_offer_qr_code.html", - wallet_dev=wallet_url - + "?credential_offer=" - + json.dumps(credential_offer), - url_data=uri, - qrcode=qr_img_base64, - ) + uri, qr_img_base64 = create_qr_code(credential_offer) + return { + "qr": qr_img_base64, + "sessionId": session_id, + }, 200 else: - return redirect(cfgservice.service_url + "credential_offer_choice") + credentialsSupported = oidc_metadata["credential_configurations_supported"] + auth_choice = request.form.get("Authorization Code Grant") + form_keys = request.form.keys() + credential_offer_URI = request.form.get("credential_offer_URI") + + if "proceed" in form_keys: + form = list(form_keys) + form.remove("proceed") + form.remove("credential_offer_URI") + form.remove("Authorization Code Grant") + all_exist = all(credential in credentialsSupported for credential in form) + + if all_exist: + credentials_id = form + session["credentials_id"] = credentials_id + credentials_id_list = json.dumps(form) + if auth_choice == "pre_auth_code": + session["credential_offer_URI"] = credential_offer_URI + return redirect( + url_for("preauth.preauthRed", credentials_id=credentials_id_list) + ) + else: + + credential_offer = { + "credential_issuer": cfgservice.service_url[:-1], + "credential_configuration_ids": credentials_id, + "grants": { + "authorization_code": {}}, + } + + # create URI + json_string = json.dumps(credential_offer) + + uri = ( + f"{credential_offer_URI}credential_offer?credential_offer=" + + urllib.parse.quote(json_string, safe=":/") + ) + + qrcode = segno.make(uri) + out = io.BytesIO() + qrcode.save(out, kind="png", scale=3) + + qr_img_base64 = "data:image/png;base64," + base64.b64encode( + out.getvalue() + ).decode("utf-8") + + wallet_url = cfgservice.wallet_test_url + "credential_offer" + + return render_template( + "openid/credential_offer_qr_code.html", + wallet_dev=wallet_url + + "?credential_offer=" + + json.dumps(credential_offer), + url_data=uri, + qrcode=qr_img_base64, + ) + return None + else: + return redirect(cfgservice.service_url + "credential_offer_choice") + + + """ @oidc.route("/testgetauth", methods=["GET"]) diff --git a/app/samples.py b/app/samples.py new file mode 100644 index 00000000..c22b69c3 --- /dev/null +++ b/app/samples.py @@ -0,0 +1,97 @@ +# header: \x59\x3b\xcf +ERIKA_MUSTERMAN_IMAGE = b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00H\x00H\x00\x00\xff\xdb\x00C\x00\t\x06\x07\x08\x07\x06\t\x08\x07\x08\n\n\t\x0b\r\x16\x0f\r\x0c\x0c\r\x1b\x14\x15\x10\x16 \x1d"" \x1d\x1f\x1f$(4,$&1\'\x1f\x1f-=-157:::#+?D?8C49:7\xff\xdb\x00C\x01\n\n\n\r\x0c\r\x1a\x0f\x0f\x1a7%\x1f%77777777777777777777777777777777777777777777777777\xff\xc2\x00\x11\x08\x01[\x01\x0e\x03\x01"\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x1b\x00\x00\x02\x03\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x04\x01\x02\x05\x06\x00\x07\xff\xc4\x00\x18\x01\x00\x03\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\xff\xda\x00\x0c\x03\x01\x00\x02\x10\x03\x10\x00\x00\x01\xd8\x0f\x97\x11\xfc\xbc\r\x9f/\xe41\xe5\xfc\x0cU<\xb9\xad\x0c|\xc8\x1b\xf3\x9d\xeaZW\xc9\xf25k\x95\xe6i_*E\xad\x19\xf6\x9au\xac\xb8\x1fJ\x0c\x8d\x19\xafh\xe4..\xb2\xf8Z\x15\x0e\xf9y\xb9;\xd9:\x00\xba\xe7\\^\x89\x84zc\xc3\x90Y\x08\xa5\xb1\xe5\xa7@\x1f\xac\xd4V\xe4LB\x9aTL\xc4\x84L@Z\xd1Q\x9a\xeb\xcaoyW\xa6\x99:\x86\x8a\x13l\x01\xad;\xe7\xe8i\x94h \xfdJ\xcb\xb0\xb8z=\xe4\xe6\x96\x0c\xb0\xf3\xfa\x89\xc6\xb0\x9bK\xd1\x1e)\x02\xaa\xbc\x88\x852\xf5Ja\xb49\xf7\xa6\xc1\x17\xa9\x15\x06I@\xf1hD\xf4L\x8e\x9ezY\xcc\xe7\x93\x136V\xa1\xfd\x0c\xbdMqYvVj"h\x9c\xa6\xc0\xb1\xd9F\x9b\xc1Z"\xd3TiM\x1bB\xac\xc5_5D\xe7l\xe24\x08o\xd5+\x98\xd7MI-\x93\x80>!\xa8\x7f\x15\xaf:\xbbqL\xa4c\xa0\xb7\xcb\xd0\x10\xf7r_\xac\xfc\xb9\xc1\xb6T\xa4\xdf\x1d\x85WI\x9e\xb1\x85\xb6Eh\xa9\xae1\xa0\x07R\x1a\xfa~\x1b\x9c\x80\xb7kU\x06\xbaR\xfd]0\x02^~\xe1\x9db4,\x99z\x8d\x0c\xb2@\xab\xa8\x1dJ\xcc\x1f1\x9a\xfa\x18\xfb$\x85fS\xd7\x1b\xbf\x05\xc3\xa5=\\n\x84Y:K0\xd2\xea\xebgN\x99\xc26\x82\xd5L\x86\xa8\xd2Z9]CY\xa1\xbb\x88\x804 \x86\xea\xd1\x1c\xb6\xceF\xab\xa5(\xe5D\x99\xacQ$\'\xf3\x9c\xead\xeb\x88Kls\xfd\x13C\xa5\x9bre[G-\xbd\xb5\x9e\xfb\xca\x93\x07\xb8[4\xa0\x8e\x8b2\xce`!\x97\xa1\x96\xf5w\xa6H\xab<]\xdc\xc7\x95\xd39\xac\xaa[\xae(\xe1\x97*\xc2\x92\xf5x\xea\x9adG\x01\x99|m\xfcW:\x96\xa9R\xc4\xdf\xc6\xd6\xa9m\x92\xcaJ\xe4\xea\xe0\xc6\xdd\x13\x14\xbdaU\xa75h[f\xec\x14|\xfbq\xcd4u\xb5\x1e\xdd\x06{\xd9\x13,\xb3\x97\xac\x8cu\x9cY\xde\xb3XL<\x94R\x8b\xd0\xdb\xf8zhn\x96^ML\xcd\x04\x9c\xb0\xea/\t3M\x9c\xef\x08\xd4#7\x1d\xec\xd8\xe8\xeb\t\x11|\xf8\xe1\x86c\xaeA\x897\x9c\xdf\n*z\xa6y\xcd\xec\xfa\x1f\xcdzQ\x9a\xcd\xea\xac\n_>\xa3A\xbcp\xd4j\xd3\x06\x95\x1b\x08\xb0\xe2\xa1\x1b#bG\x15y"o\xa9\x93\xaeH\xbc\xc0\x1c\xed\x0c\x88\x93\x92\xab\nN\xfdX\xe0\x0f,\xe2\x01\x99\xeb\xc9\xd3*\xfac\x86\xbb\xa1\xbc\xcf"Zt\xeaN\x9b9j\x01\x8cE\x0b3Q]1\xcaeh\xd3\x08\xb0e\xe6\xd8\xde\x99\xd8GZ\xb1\xa7J\x99\xc5\x00z\x0eg\xa4r\xc2\xad/Y\xea\xe4\xe9\xe2\xc5\x03?C9m\xd5\xe72\xa3\x89\x9fUo\'V\x00\x80\x95\xdbL\xa7m\xaf\x12\xde\x95\x966\x80T\x01\xb8\xa8\xc2\x8dT\xae!\xa00\x05\xb0\x8d5|m\xe4\xd3\x96s\xdd3G\xa9\xe5\xbarI`\x9c\x96\xb1\xb6\xf2\x014\xf4\x13\x8d\xf6\x168\xc8\xad\xebW\xad\xa9\xea:\x18\xa55N\xbc\x86\x816V\xd2\x92\x8b\xbc\'C\xb8\xae!,\xfeeL\xdcl\x8a\xc7\x82*\xa5|$\xf3\xb5r\xf4/%\xfaNk\xa1Y\xb0OQ\xc3\xe9\x1aQ\x99\x9f\xab\x91\x1d\x0f\xdd\'\x9a\xad\x82\xc1r\x06j\xd6vn\xc2\xebU\xf7Ra\xc1\xc1\xcf6\xe7L,\xe2M\xb9<\xe6\xedO\xb3\xdfTtvL\xaa\xd1z +\x16\xac\xcfo1\xcb\xe76\xf6\x1e\xe4\xcb\x82\xb8\x9c\x1c\xb2\x19\xa0\xe3lc-N\xd2\xb4aYM\xe9\xbf\x18\x17`\xe4\r*$W\xd55\x81\x8dQQ0\x19h\r\x89 $Lt\x99\x8dM\xb0 \r\x84\x81\x90\x9c\x17\xb3\xdd\xdb\x99\xed\xfew\xa0\xc9\xf8\x89\xb2\xe7Y6\x17\x97\\\r\xbcu\xa13\x9e\xe7\xa8\xe869~\xa2hp\xc7\x87\x9eo.[\xb5\xc7\xad=A!\x03f\xa9\xacF\x84\xe6y\xc6\x9cg\xc0:\x0b@\x1a\x84\x85^$x9\xf6Tg^]=|\xb6\xb2\xd2\xee\xe7:\xe3T-\x02i\x1c\xf3\xae\xaf5\x02F\xa9^\xd7\x89\xdc\'\xa7%-\x96\x94\x19\x86R\xeb\xbd\xe7xy\xbd&}^\\\xb3\x0eU\x93\x14\x8a\xb5\x17\x94\t-\x02\xd1k\xaa\x10\xc8\xab\x9cG\xf3\xb56\xe5t\xe1c-e\xc14\xe3qvR\x99\xc7\xc8\xd8\xc07\r\x0e\x1d3V\xf5\xad\xe7\xf4;b\xedr\xf5E-\xe6\xe7\xd01\x8dF\x97-x\xb8\x99_Z\xaf9\xac\xc8z\x93d\xed4\xf0\xeb\x8a\xfe\x1e\xbc\xfe\xd3\xcdf\xf1\xde,\xdb\x1d\xd9g?P\x9d\\\xadL\xf9\x9c\x0c\xad\x14\x9e\xb5\x03a\xa4\x9dM7\x1a\xbd\x0f?\xd3a\xb8\xac9U#%(\xa2\xcc\x01P\x86J\x0cQq\xb8\x8fZ\x81>\xad\x00\x82\xa9E\x8c\x9bJ\xf4rM\xebg=Q*~n\x90k \xcb\x8d\xcc\xfd\x0c\x92pVp\x0ba\xae\xd5\x1aM\x8a\x82\x96\xa7G\xc9t\xd9\xe8\xd2\x8d\x05\x00\xad\x80kzIX\x10\x11Q\x8cd\x1dgZ\xd7\xceb\xfea2\x0c\xe9\xa7\x8e\xb3\x0b\xf4q\xf9\x8fh\xa7\xbf\x07S\r\xf4D[<\xf5\xf24\xf2\x11\x986B\xb6\x15_\x96ce\xf4\x1c\xee\xb9\xb9\xd6q}~z;X\xbc\x94\x8bB~\x9b\r\xa0P\xa1Z\xabY\x1bJC1QCE\xd3\xf2N.\xd6(\x1aS~Vu\xc6\xc4k\xbe\x8b\xe9\xe5G>V\xd5C\x98\xfa\x99r\n\x8eRv0X\x15 b\xf5b\xd1|\xe7\xa5\xa6\x0b\xcf\xbb7\'\xb3\x8e\xfau\t\xc5\xe8\xa5\x03\xcb\xd8+@\x8e\xb4\xa9,\xd4\x81\x136\x08Q\x8cg\t\xea\xfb\xaa\xd7\x0c\xe1\x98q\xb3\x89JR\x89\xa3\x95\xa5y\xea\xad\xad\t"=h\x04)\xa9@\xa9\r:\xe1\x9d\x91\xd4\x04\xd3\xe6\x03\xfa\x86B\xaeCkG\xd3S.\xb1\x1ab\xad\xd0\xd5>N:\x8f5\xcc\x93\xa1\xab\\\xf2\xdd \xaax\xcd\xee\x9d\x9a\x84G\xa1q\xe4a\xf6IM\xf2\xd4\xea\xbc\xa7\x95\xd2\xd8b\xa3\xff\xc4\x00+\x10\x00\x02\x02\x01\x03\x04\x01\x04\x02\x03\x01\x01\x00\x00\x00\x00\x01\x02\x00\x03\x11\x04\x12!\x10"12\x13\x14#3A\x05 $4BC0\xff\xda\x00\x08\x01\x01\x00\x01\x05\x02\xba\xd7\x0f\xf3<\xf9\x9e|\xd6O\x99\xe7\xcdd\xf9\xac\x9f5\x93\xe6xu\x0f.\xd72G\xfeGPO\xd7j\'\xd7j\'\xd7j\'\xd6\xdf>\xba\xe9\xf5\xda\x89\xf5\xda\x89\xf5\xd7\xcf\xad\xd4O\xad\xd4O\xae\xd4D\xd7\xdc`\xba\xd6\x06\xfb\xe7\xd6^\nj\xec1u\x0eg\xcc\xf3\xe6y\xf3<\xf9\x9ei\xdd\x8c\xbf\xf2\x7fvi~\xa3\x11\x98\xb1\xc4\xf1\xd33\x9f\xe9\x98\x0c\xc8\x9cM\xb1,)\x16\xc5\xb2[LG\xdaV\xc8\xad\x9f\xe9\xa5\x97\xfeO\xec\xed5\x16\xe2{\x9cB\xd0.\'\x99\x8cC\xfds\x03M\xcb\x01\x83\r6\x91+\xb6={\xc0\x1bJ1R\xa7#\xa6\x96_\xf9?\xab6%\xb6m\x04\x97`\x9b\x03\xb6\xe2\x17l\xc1i\xea\x0f?\xfcD\xe5em\xb8\x15\x95\xbe&\xdd\xd3n%m\x83\xd3K/\xfc\x9f\xd0\x9cF8\x17\xb1cUb\xb5\xb5\xcd\x8c\x8b\x89\xecTf1\xdc`\x11\xbb\x7f\xa2\xc2\xbd|E\xf3[f2\xca\xdb3\xd80*kn\x9aY\x7f\xe4\xeaa2\xd7\x9aj\x8b\x9dM\x995\xa4iZn7\x1c\x06\xe2\x016\xec\x04\xe4\xc1\x08\x80J\xe3\x08 \xe2\x15\x95s\x10\xe6X\n\x14`\xe3 \xc26\x14l\xcd,\xbf\xf2\xf51\xbb\x9a\xca\xfeW\xd4\xed\xa2\xa5B\xc7\x1c\x05\xc9\x03b?\x11W2\xaa@\x97\xbe\xe7\x1c\xc2 \x10\x88\xbe\x0f\x0eD+1\x16/\x9f\x05pG5:\x10a\x1c\x0e\xd3\xa4\xe6_\xf9:\x13\x18\xcfD\xd1W\x85\xd5Xn\xb9\x13\x01\x96W^#w\xb3\x8d\xcdUy\x9a\x96\xd8\x9bp\x15x\xc4\t\x02\xe6\xca\x87\x16\x0e\x11a^1\x14EY\\N%\xe9\xb9+8 \x98p\xc3De\xff\x00\x92\x1e\x81w\x1b\x87\xcfv\xb3\xb6\xaa\xa9\xc9e\xc0U\xc9o\x0c6\xadi\xc2\x00\x8bgs\xb0\x9b0\xa4L`\x81\xcdk,^\xda\xfc\xb2\x8d\xf8\x98\x98\xe3\x9c72\xbeE\xeb\xb1\xeb~\xa5\xb6\xd6\x89.\xb3t\xd1W\xbd\xad\xed\x96w\xda\xbfm/;\x13N\xff\x00z\x8euZ\xbf\xf5\xcb\x03)?e\xc9)\xef@\xfcc\xd5\xc7j\xf1`\xe4\xfbV\xe3\x16h\xa5\xfc\xdbR\xe1|\x07\xe5\xf4\xa272\xe8{\x13~\xe6\xf2\xca6-\x99\xb6\xcde\x98\x98\xcc\xfe:\xbd\x89\xaa\xdf\xfd\xff\x00\xdd\xd6q\x90\x89\xa4\xef{I\x97X)S\xcb(\x94.\xda\xb5-\xba\xca\xce\xd1@\xfb\x97w\xde\xb2\xa3\x8bu\r\x9a\xadX\x9c\n\x8e\xd6\xa4\xed\xb1}\x91\xbb\xfc\xd7g\xe4\xaf\x9ae\xe2\x7f\x1f\x18wY\x1f\xc1\xee\x94z\x9fw\xe4\xddgu\x99\xb4\xd6\x02-\xba\x80\x8c\xf6=\xd6"\x19J\xcd\xdbR\xe6\xe4\xbeR\xa6\x03\xa9\xb3e\xb6ZM\rd\xf9s\x03\xca\xdf\x81\xee\xdd\xb6\xa73Q\xc3P{)\xee\xa2\xef\x1a.\x1b\xf6ymQ\xc2\x03\x85\xa3\x8a\xcf\x12\xeb6\x81L\xad\x15e\xd6\x8a\xd7\xe7\xab+m\x04\x00\xa4"\xcb96!\x816\x8c\xe240,e\x1f\x1bQY?OX\x8f\xa7\xcc\xee\xa5\x92\xcc\xcdG\x94\xe2k&\x8f\x93\xa5\x96\x0e\xdd\'\x9f\xfa\x9a\xd3\xdc9d\xf5iq\xdde\x87\x02\xed]\x9b\xab\xfe>\xedB\x9a\xd0\x9f\x88M;8Zl\xde3<\x97\x13\x12\xde&\xee^\xfd\xb1\xaf%[P\xe6\rK\xe6\xbdD;lV\xfbL\xe7~\x94s^\xaf\xd7E\xf9\xa9\xe2\xdbD\xd2\xf4\xce\x06\xb0\xfd\xfa?\xd8\xd3\x1d\xd5Z{\x0f\xb6\xa0\x16\x07O\xf1\x8d>\xb5n\xa6\xcd\x87K\xa6?K+R4z[~H\x0f\x029\x9b\xa5\x91\xb2\x0bq6\x8f\xa7\xb4\x9a\xa3\xa0\xfac\xa6?\rvl\x96a\xc6\x99\xb3E\x7f\x8fU\xf8\xb4\xc7\x17\x7f\xebo\x9d4~\x03\xcdH\xff\x00"\xae,\xd2\x7f\xaf\xa9m\xb5\x01\xb9\xb9\x83\x99v\x9a\xbbC\xe8\xb9\xfaP\xb1\xd5\x9a%[l\x13\xf4\xe6\x03\x1e9\xc2\xdb\xb9\xe7\xfc\xf2\x00\xee\x85m\xb1\x12\xa5\x96!\xaeR\xdd\xd4\x1d\xda}G5\xd4~\xeaynWN9\xb2^c\xf2\xeb4\xa3\x14j{\x95x\x99\x13\x89\xbb\x86\xc4c\x1c\xf3U|\x88e\x90ta\x18\x10@\x81DD\x10\xa4A\xcd\xa9\xb8\x14\xd8\xdaC\xdbg5\xa7\x95\x132\x91\x86\xb6j=\x8f\xa9\xf5\xd2\x13\xf4vr\xd3\x1d7\x18^\x1eb\xa0\xc8\x1d\x0f\x8b\x04\x1d\x02\xe6X\x82l\x80@\x07A\x04\xd4\xa6f\x95\xb1\x0f\xe3YW\xa3yOw\xf1o,}-\\&\x83\x9d+{u&\x19\x989"b<3\xc4\x10Kz~\xc0\xccU\x9ba\xe27\x84\xe1\x87\xa0\xf3W\xab\xfbS\xec\xd1\x97\x9f\xd6\xab\xf1\xe8\x0fe\xbf\x90u\xf1\x18\xc6lJH\x9f\xf2\x8cK\x19\x89\xb6\x1e"\x99t\xfd\xe6\'V\x84\xc6\xe2\xc4=\x83\xdbK\xccoj}\x87\xbb.\x0f\xfdj\xa6\x80\xcdG\x06bc\x86\x8c%\x82Q+\\\xc2\xab\x08\x86\x19\xe6bY\x0f\x90"\x0cA\x0c0\xf9\xbc\xe0\xd2r\x9f\xbd\x14\xff\x00\xd6\x8f\xc8\xcd\xf7,\xe5f\xab\xdbJ\xdfwT;i;\xab\x82\x11\x18B\xb9\x89^\xd9\x9e\xdb\xf4\xd7n\xd2[p-.\xb4\x88\x1a\xf2\xea\xdd\xb6r\xaa3\x02\xc0:\x18\xdd5g/\xa6=\x9f\xad\x04\xcf~\x97\x92G\xdd\xcfi\xf5\xd5~M/\xbb\xb7\xc9^\x93\xd2\x03<\xc7\x88\xbd10&W-\xcc\xc0\x07\x02c\x10\xc1\xc1\x06\t\x98\xcd3\xd2\xff\x00}4>\xda.\x15\x8c\xd1\xc7\xf6\'\xb9\xb8k\xfb\x9e\x93\xb6\xad3\xfd\x9d(+a\xe8;T\x13cg\x1d3\x0c\xcfC3\x04>L\x16`\xee\x85\xa1=?V\xfbS\x1b\xce\x9c\xff\x00\x8fcv\xe8|Y\xe0y\x85\x8el\x1bi\xa5\xfe\xedm\xb7TGG\xf5\xae\xc1]kmo\xd3\x06\x10c)\x13\x9f\xec\xea\x18T\xdc\xf5>-\xf7\xa6y\x81\xb6\xe9\xc9\xfbz\t\xa88\x9e\x03\x9d\xb4U\xf9\xf5M\xc6\xec\\\xf6}\xfc\xf4#\xb7lz\x167\xc8\xb3\xeaH\x9fP\xb0\xdccj\x08\x9f3O\x9d\x84\xfa\x93>\xa0\xcf\x96\xc2\x10\x93\x02\xee\xb3\xf7\xfb\x87\xd5\xfd\xaa\xf5_k\x0f\xda\xcf\xdb\xfe>[\xcb4\xd5\x9c-\x03\x0bu\x9b\x9e\xd1\xce\xf34\x96|\xbay\xfa\xe8\xeb\x98\xc3\x02\xea\x04\xd9\xce\xc8\xcb\xc7\xc7\x02\xc4I\x8e\x08\xc4N\x9e:[\xe9\xe4\xd7\xf8\xd3\x90y\x00}\xbd\x04q\xca\x8c\xbe\xac\xeemClC\x0bd4\xfe&\xd84\x86\xa4\x9f\rs\xe2\xae|I\x1a\x9a\xe7\xd3\xd3(\xa6\xaa\xc3\xa2\x99\xb1f\xd1\n\x88j\xae55\xc1Mp\xd3\\\xa2\xb4\x13\xff\xc4\x00#\x11\x00\x02\x02\x02\x02\x02\x03\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x02\x11\x101\x03!\x12 \x13"A2QB\xff\xda\x00\x08\x01\x03\x01\x01?\x01\xf4K\xda\xb1\xd1D\x97\xb2F\xf2\xfd,X\xa1\xafD\xac\xa1\xff\x00\x82C\x12\x19B\xca\xc3YBX\xa1!\xc4g\x89\xe2x\x8d\x15\xe9%\x88\xa3HZ(K\x14x\x94QEa\xe2DU\x89\x13\x1b\xec\x83\xb2N\x8a\xc6\x95\x9f\x85\xf7\xec\xc8!\x13\xd8\xf4q\xaf\x11?)YvntK\xfc#\xa2\x7f\xd7\xb3\xd0\x99\x11\xff\x00D#\xfaKB\xe8\x89\x05\xf6\xb2k\xa2\xa8\x92\xee\xcf/W\xa1\x0bG\xfd\tQ9(\x8b\x99\x8a^H_Sd\x99)#\xe5\x89\xd3\xd6\x1e\x1e\x88\xab\x16\x84\xbe\xc3G$SB\xe1\x91\x08\xb8\xb1\xe2D\x8adb\xf0\xf1\xf8q\xacq\xaf\xb3\xc5X\x8a\xc3\x1a\xb2\x8e\x84\x86\x87\x89\x1cx\xe3\xeaO\x0b\x0c]\re"\xb1,H\x8fO\x0b\xfa\xc2f\xc9vSE\xf4\'xK-\xf6=\x12\x17k\x0b\rf\x8d\x17\x8f"\xf1\xfb\x89\x10\xd0\x89:\x13\xb2\xca\xb6x\xe2E\x97\xe8\xb7\x89h\x8e\x8f\xc1\xb4\xd52\x0f\xba\xc2<\x8d\x8debZ!\x89h\x8f\xf81\x9axE\x08~\xbc\xb2!\xbcKG\x18\xf1G\xe6P\xd7\xac\xc5\x86q\xfa.\xe2G-\x8f/D\xf6F8\x91\xc4\xb1D\xfa8\xdd\xc7\xd1\x0c\xac2{"\xa8Z%\xa3\x8cB\x1c\x93\xe8\x8f\xd0\xe9\x94P\x90\xf3\'DWv\xca\xc4\x85\xc9G\xc9\xd9\xe69vG\x94\xf2\x175l|\x87\xca|\x87\xca>c\xc8S\xa3\xe4>A\xc8\xff\xc4\x00 \x11\x00\x02\x03\x00\x02\x03\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x10\x11 1\x12!0AQ\x03\xff\xda\x00\x08\x01\x02\x01\x01?\x01\xe0\xd8\x97=j\xa2\xf96u\xcf\x07Z\'\xc1\xb1\x7fE\xfd4Cb\xe2\xe91S\x1b;f\x9an\x08\xd3M4\xd1\xdc]I\x9b[zi\xa6\x9ao\x04I\x8d\x88\xfc\x1d\xbf\x82\x17d\xdf\x07\xee\x90\xedrC\xaf\xc3i-\x1d+HhB\xa8\x8e\xbf*0\xd3\xc0k-#\x19\xe3#\x7f\xa6`\x86D}\xd3\xa8\xb3\xc8x\xd5\xablk\xd0\x86D\x97t\xf8m\xe9\xa6\x8cL\xfd\x19\x1e\xc9S\xf8.Q\x1f6\x8c\x1a\xcan\xf3\xd5.\xc7\xdf\xcf-\xf42$\xbb\xa8\xad\x1d\xe1\x86\x1e\'\x8f\x07\xd0\xc8\x8f\xba^\x89\xaf[h\xd3M6\xd7d\xba\xa8\x8c\x8f\xd2\x08\x97T\xbb$*\xd2]\xfcb:D\xbb\xa5R\xef\xe0\x847Q%\xdd"$\xd7\xbf\x82\x10\xea$\xbb\xa4/D\x97\x91\x8ds\x8a\xa7Q\x1c\x0f\x03\xc4\xc1\xa3\t\x7f\x9a<\x0f\x03\x0f\x13\xc0\xc3\x07\x13\x04\x8f\xff\xc4\x00.\x10\x00\x02\x01\x02\x04\x05\x04\x02\x01\x05\x01\x00\x00\x00\x00\x00\x00\x01\x02\x10\x11\x12!1a\x03 AQq02@\x81"3\x13\x04Rb\x91\xa1r\xff\xda\x00\x08\x01\x01\x00\x06?\x02y\x9a\x9a\x9a\x9a\x9a\x9a\x9a\x9a\x9a\x9e\xe3)\x9f\xb1\x9f\xb0\xf7\x9e\xf3\xf6\x1f\xb0\xfd\x87\xbc\xfd\x87\xec?a\x9c\xcf\xc6g\xec3\x91\xee5=\xc6\xa6\xa6\xa3\xbb\x1f\xa3e\xf03?\x12\xcf\x99\x8f\xd0\xb5m\x12\xef\xd2\xd2\xc6\xb4\xd4\xb3\xa5\x9f+\x1f>\xf4\xbb6.\xcb\xd2\xfe\x95\xe2\xf2\xa6U\xb3\xe4c\xe6\xbb,b\x95.\xcd\x8d\x91\xb5m\xcbu[\xaan]\x16\xeao\xc9!\xf2\xdd\x8eOC\xf9%\xa1d\\\xb1\xb1\x81\x18P\x8b\xb2\xfc\xae%\xebtoLQ.\xb5,\xcd\xa9!\xf2\xd8\x8f\x06?f\x14]\x96T\xbb\x1c\xde\xb4\xc4Yh\xaa\xbc\x8e\x89\x97\xe4\xdc\xc5\xd1\x98_ZgV>K\x0f\x88\xfe\x89q\xa5\xd4\xcbB\xf5\xd9S\xc9\x85k[1\x12\xf01\r\x1e9\x1c\x1fR\xc6.\xa5\x8d\r\xc9\x0f\x96\x1c\x18,\x96\xa2\xe1\xc7\xa9s2\xfd\x0bue\x8f#e\xdd\x15,H\xc5\xdcG\xd1\xff\x00\xa5K\xd2\xe6$y\xa6fD\x87\xc8\xdaY\xf4%/\xfa/\x06e\x97S\n\xe8b,l\x8b*\xa4Ewtf\x16=\xa8\xbf\xc6Th\xb1\x85\x8a\xb6e\x9e\x84\x87]\xc5\x1e\xa2\x88\xa2\x85\x12\xff\x00\xe8\xb7V[\xb1r\xdd],9\x0c[\x0fb\xdb\x16&\xc9\r\x7fr\xb8\x85\xbdg[\x96$:]\xd1_\xa0\x89>\xc3]\xa9r\xdd\xc5}\x17"\xa3\x93\x1b\xee\xcf\xa2\xdd\x0e!#\x87#\xc9\x07D/\x14\xb9z1\xf2b\xa3\x97r\xfd\xcc%\x8b\xf6,\x8cR2-D\x8c(\xb0\xe5I\xd8e\xb7"\xfb\x0e\xb1>\xc9F\x88c\xa7\x93\x08\xd8\x85\x1e\xe7\x82\xe5\xbb\xd2\xc8\xfe(}\xd3\x11n\xe2\x12\xef"\xc7\xd8\x89\x08~F\xb6$\x89\xc6\x8a\x91\x97|\x86\x8d\xc67Y:#\x13\xe9L\xc9M\xe8\x8f&\x15\xefu\x8a,M\x91\xd9\x13\xda\x97\xd8~i(\xba5\xdct{\x17\x1e\xccN\x92\xa2\x1b\xa7\xd50\x96\x14Q\x1e\x1c~\xc7\xb0\xe5+\xe7L\xe9q$7\xb1\'\xde\x99v/\xb9\x9dT\x97A\x08b\x1f\x83tE\x93\xe4{\x88l\xcbY\x19\xba<\xf3c\xc5#+\x19R\xcb\x9d#2\xe9\x17\xa5\xd1\tQ\rlI\x0fbU\xb0\xa3\xde\x8c\x8d-\xc3G\xf2\xff\x00Q;GR\xfaD\xbf\r\xdc\xc4\x8e\xd2\xe7\xdc\xccXQ\x9b5\xbd2"\xcf\xa24~I\xa1\x8e\x99\x0bb\xe4\x88\x96\x89x\xe6\xc9p\'\xf8\xce\xd6\x17\x01\xacM35\xcc\xc9\xfax%\xaf\xa1jJ\xb7\xea\xcf\x02"\xc8\xb5\xcbx\xeaw?(\xb3\\\xcc\x9d55\xa6\x85\xd231s\xa2\xd4\x90\xcb\x1e\x07\'\xd6\x97\xa4_5\x9cn\x8b\xc2\xb9\x17fH\xcf\xd2F\xc3%O\x06\x15\xcb.\x1b\xf4u\xa7\x7fM\xec5O\xb2Tr\x1c\xdf*\x91~\xfc\xd9\xafV\xd4q\xa2b$XQF\x05\xf7\xcd\x85\xf4\xf8\xa8hL\xe1\x92\xa5\xcf<\xcd|X\xb22\xa4h\xea\xb9W\xc5\x8f\x92q\xfb-IQ\xf3%\xf1\x96\xcc\x92\xd8j\x93\xab\xad\xa8\xd0\xd0\xb9s\xf5\xe3\xdd\x8a_\xe2\\\xc8r]}\x0b\xfckv \xd9a\xae\xc3\x1f*C,_\xe2+\x8fvF\x08\xbfc\x11+\x0f\x91\xd7\x1c)g\xf0\xb1\xcd\xd8\xba\x90\xc9>\x88\x92}ID\x9a\x97J\xdb\x96\xd8O\xe4\xe0\xac\xfb\x1d\xbe\x05\x91\xfc\xbcgh\x9f\x8c\x86\xc6\xcb\xf7\xa2$\xd0\xcb\xa3r\xe5\xeb\x96\xa7b\xef\xf1\x91\x86Y\xae\xfe\xb5\xb8j\xe7\xf2\x7fR\xbf.\xc5\x92\xb5\x18\xc4\xab1\xe4iG\x91\xa1\xa7&\x87\xe5\x14\xcf\xd7\x13\xf5\xc4\xf6#\xdahh{Q\xedG\xb1\x1e\xd4{Q\xecG\xebG\xe1\x04\x8c\xd1\xa1\xa1\xa1\xedG\xb1\x1e\xc4{\x10\xed\x14\x7f\xff\xc4\x00&\x10\x00\x02\x02\x02\x02\x03\x01\x00\x03\x01\x01\x01\x01\x00\x00\x00\x00\x01\x11!1AQa\x10q\x81\x91 \xa1\xb10\xc1\xe1\xf1\xff\xda\x00\x08\x01\x01\x00\x01?!A/\x06\xff\x00\x9f\xbc\x9b\xd1)\xf9\x16\xa9.\x97\x84\\\xe1rNf\x1av\x1b0\xc2\x97!c\x99\xda\x1f(\x8fo\xc2\x82b\\\xdfB\xfb&,\xde*\xbf\xe4jm\xe7\xe7\xfc\x1b\xa1(J\xed&LM\xac\x8d.\xcc\xf6t!\xb6W\x95\x03\x9d\x12d\xb6\x08v8\xcb*\xaa\xfb\x1b\x9b!\x97+\xa6F\xc5\'\x9d\xff\x00\xc5pR\xc9\x02\x0e\xcb\x14\xb9t\xbcd\x1d\xa4H\x82K\x1az\xfe)\x88r\xa4M\xcb0\x95\x8f\xd1\x06\x06\xceDV}\x11$&2\xe64\xc5\t\xc0\x9a\x8d\x7f\xc5q!d\x9b\xb1\x1b\x11\x85CbAI\xfd\x07\xa2!L\xc6n\xd8\xc8\x90\xeb\xccJ\x94`\xc8\xd0\xef\x02\xd9\xb6B\x7f\xd5\x1b\x10sCtA`p~\x0c\xa2br\xbce\xfc\xae1=\x9bDe\xe3?H\x10\x9f\x90\xf20\xcb`\xa6\xe3\x0f\x12\x89\xee\xf0\x8642m\x89\x90{\x12`\xb4\x93\x86@\xff\x00\x02\xe0")\x04\x92\x13\xa1jT""|e\xe0u\xe6\x08R\xe0\x14\xd3\x10\x84\x04\xeaq\xb6\x89\xd4\xb0e\x02\x01\x04\xc0\xdb\x11t\xb2\xc6\xb8,\xb2\x10\xd8\xfe\xd8\xe71\xa3$E%\x86_\x81*\x86VP\xd2\x8b\x8e\x81\x8c\xea\xa9\x05-\xfdBA,{\x83E\xa4eV\x10\xa6_\xc4_\xf0\xa9\xe10\xe9\xe7\xe0\xdcg\x848\xe8,\x95\xbf\xacG=H\xed\x0c\r\xcb/$\xc3|=R\t&\x88\nBXt,\xc8X\xcc3\xf5\x119C\x81\x8bLF\x91\xaaO\xec\xc0\x9d\x82\xe9X2X\\\t\xe4\xcb\x0c\xf036\xb7\xf8=\x08\xa85\xcfF\x01\xe5#\x01>VZ\xa2\xa8DK\xfa<\xde\xb4Qi\r\xd6\xff\x00\xc0\x94\xe5(\x14\xa9\xc9\x969\xf4\x0eN|\x90\x04\x1f\x07\x064\x91\x03\x1f\xba\t\xc0\x86\xfb$\xaf\xa8\xd0V\x0c\x9bO)\x88\x8c\xa9\x930\x8fbj\xff\x00\x05\xd4B\x0eP\xf2\xde\x10\xa9N\xcc\xe8\xef\xfc\xb2\x89\x81\xe1\xf0\x91\x85&N@\xb6\xaa\x08\xd4@\xa8?L\x87\x967\x04\x93\x8a\x94\xbb\x18\x17\xc7d\xd1\x0c!\xb4pKO\x91\xca\x91[\x8a\xc8I\xd3\xc4\x9fEvk\xfa\x16e\xab(\xe9\xab0\xd3C%&=qY\x08\x84\xb6K\x19xrr\x1e\xa5\x13\xb0\x14\x12\x96\xde\xf91\xb7\xb1!\xb5\xb3b\x97cB"z\xb23|![\xaeL[\xea\x15j,^\xc8\x14\x88t j@\x87\xe8\xf64\xe0M\x01\xa9\xe8*\x8c\xf2IcS\xf4%W\x0c\x97dI\x96\x17\xe1F\x11\xc3\x83C\x10\xf7\xb5C\xccN\xe6048x/F\x9eY@(yR9\xc8U\x9c7b\xd0?\xa2\x12q\xb0\xc4\x03\xe0\x1a[\xd1\x0fnb\xb4\xf0\x87I\xd1\x0bn!!\xbcQ&\xd0O\x98\\\x17E\xbd\t\x89\xeb\t\xe926\xd2\x83g\xc8\x84\x97\x01\x901\xc1\x1f\xe9\nE\xc4\xa1\xd9\xecq\x0e6\x871\x18\xda\x1d\xa8?=\x8a\x01\xd9a\x16]h&QhN\x90\xbe\x91\x84)i\x08\xd1\xec$l\xc2\x15\x82\xe1\x19\x92-\x8f3X\x15Z\xcc\x13z\x98\x86\'7lN\t\xa0\xc8\\\r2;"\xa6E\x11i\n\xae\x8d\x1e\x87\x90\xf7\xa0\xd0\xa13o\x82\x02\x1a\x19\xc6<\xf8\x99\xd2&\x03\xbfC\x9b0\xc5[+\x10\xcf\x9b\x17\xa4\xfa1\xdf\x81\x9cp\xae`{n\xa4\x89\xadbb[D!\xc2C\xc5\xaa\x91\x97\xc1\x15\x1f"\xa4c\x03Y\xc2! `\x9d\x0b\xf50\xb0\xe4:\xdbC\xb7\xea^\xba\x18\xd9\xa3\x07\x80\xfc\x94H\xd2\x85\xe4\x8aa\x18\x9a2\x83\x92A]p\x8a\xc9%\xeeY,2\xeaBNle\x97\xcc\xb0\xb5\xad\x95_\xff\x00\x04E\xf4\xb3\xe4l\xd4!\xaf\x92\x17\t\x7f\x16H\x88w\x06\xc5\x86\xc2\xd3\xd4\x8b\x08[eTh\xa0\xe9\x18\x94\xe5\x92\x1d\xb9\x85\xbb\xd2H\xd2\xe8T\\1"\xfc\x94\xf6\x82\xd2\xa8&\x93\xc0\xbf\xa4\x9d\xc3\xefb\xba\\""y\xa2%\x8c\xc4\x8d\xb8\xfa%K\x02\x92C\x92E\xbeE\xeaZ%Vp\x1c\x9a\xb6>\x10\xe9\xed9of\xb5\xfa!?\xb6D&\x05np\x89e\xe4\x1d\xb8\xf0R3\xc9b}\x06!\xc8[\xc1-\xe4\x86\xe8\xe2\x04\xa2\x1e\xe4b\x12\xe7\xf8/u!3\xdc\x8b\xdfh\xaa\x12C\x92\xa1\xe3\xaa#G<\xc9Ua\xf3\xe8hS\xdb,%\xc0\xf8\x03\xfc\x10\x1fn0]\xb4\xa37\xc1\x06\x1f\xe8\xdaB\x9fd\x10\t\x00\xb2+A\xaa\xd8\xe3\x85\x93`p\'j\x90\xf4\xb2\x91\xd82lCef\xceE\x10\xcd8\x89\xf0\x13\'\x91\x0e\xc4\xbf\xc8$A\x9d\xcc"\x9b\x92E\xb0\xb0\xde`T\x07\xab\x07B\x11\xb2^Y\x89R\xc7lQ5@9\x10\x83\x06\n\xa2\xa6\x84<\xe0\x84[\x1b\xd1T\xe4\xc2$D\xdeE7/c\xba\x04\xa2\xc3m\xa7$\x08B&\xf2\x95\xb2\xa33\xfd\x0b\x08\xf2I$\xc1\xd2\xc3\xf0\xa4S\xe5\'\xab"\xf3\xd0\xf2\xe8J\x13l\x9d\x10\xa6l:i\x18F2t"j\xa4\xe8D\xf3\x02MAlm\xa9PnJ)\x12\'M\xa3\x04u\x0f\x0cD\xa9A\x94g\xecK"\x1129\xa1\xde\xc25\xac+\xe0De:HM"\x05m!\xad)O\xfc\r\x9c\x999q\x14\xb7)\xc9\n6\xack\'#\xc2\xaeD\xc3\x81\x03\xe6\xb2I\xccy\rl\'\x8e\x8a\xcb3\x84\x94\xfd\xb2\'\x0e\x94\xfe\xc5-\xbb\x1d .\xee\xfaN\xc4\x99\x0c\xb9\x85Au\x83\x12\xcfL/4\x04\xa6\xaf{F\x12^I\xd9q\x1a\xab/dq\x04Cs\xa3\x189D\xa1\xe6$\xaftM\x89E\xaeF\xc1\x03\x89\x0e\x86\xc0\xb6^SL\xbbB~/Y\xd8\xaa/\x9b\x19\x0b=\x8f\xb0}\xb1\xe6\xf5\x03$\xa0\xa6\xcb\xcb\x1a\xbd\x10\x9c!\x08\xfd\x89!t\xd7\xee\xb2w\x00\xa6\x18.\x18\x19\x9c\x8d\x85\x9d\xe7B\\\xa4\xfbz)8\x10\xd9\xb1\x18ajp"3\x85`y)\x97c\x17\xcc\x89zh\xa6e\xa5\x0b\x1f\xbb"\xedB\xb7\x01W\x852#\x8dI\x0c\xc0\xb7\xcd[olE#\xfdh\x91\x18\'\xa3dhIe!\x11\x81\x8e\x02-\x81\x18\x94-\x95\x1a\x94N\x9a \x0cC~\x17"\x86DS\xc1\x17A1\xb1\x8fC\xb3\xe1\x96J\xf0\x19\x98S+#d\x02\r]\x8c\xd4\x089\'\x94+P \x9e\xca\x03\xa1\x96\xde X\x9a\'\xd1!\x90\xf3\xb2\x12#\x12<\'\x9c\x0e\x071\x00Q44\x1d\x9c\x8bN2\x0bv3nQ5\xd8\xc1\xa7`\xb2.\xe4\xe3\xc6J\x89>\xd8\xcd\xad\xa1#\xc6(t#\x82\x11#\xb0\xf8D0\x11\xe4F\xd3!\x86\x94d#L\x9505\xe1$b\x8f\x82\xca\xe8\xa2-S\xf4\x0f(\xb8)\xf7\x18\xd2R\x931\x98pQ<\xb1\x11\x15\xaf\x83\xc8\xd3gb"P\xc2\x91B\t3\x05\xc7A\xd0\xe2&jH\xc2\x18B\x02\xa2\xcc\x8e!l\xe4\x8a\x06A\xdb1\x06\x8d\xa6sF\xa1\xec\xf4r$\x93\x05\x94\xbd\x90$x(\x8e\x1a-\xe2W\x16!\xa2<\x16p%8\x08\xba\xe7\x81\xd3\x81!\x0c\xa0\x84\xc2TS#\xf8P\x8b\x89\x1ee\xd8\x99\x1e\xe3\xcb\x07\x11\x8c4\\\x14\x95!&k#R\xcb8H\x13\xc3.\xf3\x04\x89\x91\x16\xc1P\x99\x17\xa1\x13\xb1;g\xa6 \xec\xa6\xfb!\xac4\xc7\xa6 E\x90\xbc\xd6\x85K\x96d\x93\xac\x84\xa8\xc1\x18,\xc8\xb2\xa7\x83\xee!\xea\xf2\x8c\x83\x97H\xb3E\xa7E\x99\x91\xa7,j\xae\x11&\xd1\xa1\x85v\x86\x9d\x86\'\xc0\x9cH&-\xd8\xc9\x163\x8b\x04\x91\xdcx\xd3\'\xc2" \xd42\x02\x8c\r\x03\tX\x1a?\x0f \xd0\xd3\x94>\x05\x98\xfe\x80\xda\n\x10Eb\n\x10KZ"*6a!b\xe2X\xd0\xe9\xcca\x8c#@\x98\xd8w#\xe4+\x9b2l\x90\xd2\xc9\x05o\x195#\x15+\x03\'\xc5\xa3\x0c\xfcG\xae\xa6\xc20\\A\xa4\xd5Q\x91J%\xb3A\xe8.\x82g)\x88\x9cy\xc0\xc7\xbf\x82\x87\x91[q\x116\x0c!"\xe08\xd08\xe6Q.\x04Bh9\x96<\xb2\t(\x88o\x91\xb3\xc2\x1d\x8dX\xa9\x8doE\x98o\xf4\x8a"a8\'\xf9\x19\xb1\x80\xb6M\xbd\x13\xc1t\xd2p\xb2\xfc4\x93i\x8f\x9a\xd9d\x99\x14H\xe8\x86\x1a.*\x04*\n\x9a\x02b\xb7C\x92\xa1SU&\xdc\xc9\x18\x96\xf3\xe2\xbc#\x17I\xa1h\x87a)\x08xv\xc7\xb9Yv<7M\r\x82lw%\xa41\xb6z1K\x96@p\x86\xd5\x16E\xa5\x81\xe9Tyv\xb0s\xaaP\xc4\xe8f\x06"\xe8b\x12\xe4HyfCu\x08d&\xd0\x8cvE\n$\xd2\x12Qx\x14\x96\x87\xc0\x90y\x1a\x1d\x8d\x8d\xd29\x1a[\xd1:p%b\xd9W\xeaL\xc5\xa6\x83\xb9G\x02#\\\x0c\xd5\x89e\r3\x04O\xdeC@\x9b\xf2I\x13JP\xab\x95\x0f\xa2\xe2F\x8e\x87\xb2cL\xb9o\nP\x85D=\x08\xd4\xb5\x8b\xb3#\xed\x992\xd3\xb0\xd5\xa8\xf0RO\x02RP\xb0P\xd8\xdc\xc56I\xa8\xb2K\x83\x04d\x90\xa3\x13\xe2lALY\x0f\x04\xc8\x96\x89\x91\xbf\x04N\xef\xc0A\xa2#\xe0E\x0b>2\xc8`o\x81\xbb\x92\x9a\xe9\x14\x10\xf2\x91\x82kLY\x83\x8f\n\x94O\\<\x80\xe9l\xc2\x90\xf33\xff\x00\x02\xdf\x8d\xa8\xc8\xd6V\x05T;\xa47P>B\x99\x19\xc0\xa6\xa9\x16\xa0\xe1FN\x8d\x0e\x88\xa3B\t\x89e\x96vj=\xba\x8fd\xb44\xf4\x87@\xe9\x8br\xdaE\x04\x87\x11\x9a\xcenb\xbd\x89\xfe\nb\x99\x16}\xa16\xf0\xf07b[\x12p@\xd0\x84 \xd7C\xf0e-\x8d\xc9\xc0\xe4\x12\xe0\xc8\x19\xf8T\xb99\xc9dj\x84\xb1e\x97\xb4L\xb5\x95\x9f\x13B\x18\xe7\x9b\xa2\xa0Zb\xd0\x91\xb4\x92\xaf\xa3\x80=\xf9^L\xa9.\x1eM\x8d\xb9\x1a\x99\x10hb\x08R\xc5=B+\xf1<\xf8\xff\x00\\Y\x16!\x0e\x9a\x88\x97p=<5\x82\x922M\xde\x05\xb9#>\n\x82\xc0\x91\xcf8 \x95\xc7,B\x1a\xd0\xee\x92p\x1f\x81\x1d\x88\xe4Ke\x99B\x95\x1dG\x81\xc6\x06e\x89\x9d\x91\xe8h\x19\xcc\x84%\xd6\xe8\x17\xd2\x0e\x86\xd8\xb8\xb4,\x11e\xc1E\x08\x81\xb7\x02\x91\xb4"P\xeb\xda\x1eLvL\xf6\xf9\x18\xa5\xd1@\xd3G\x19:*\xd8\xce\xb41*c\xa8}\x1dx\xee\xc2B^%\x8f0\xa4\x13%\x18\x942Dn\xe2s&^\x8eEOZ\x1f#HE>\xc8i\x0f\x9b\xe0nP\x96\xb5\xbb(!/\xb8\xd2,\x12\x85\x82\x99*$T$\xd0\xd2P\x83%\x0b3\x1f!x\xaa:E\xfcS\n\x88\xb6\xd4\x8fUa\x82\xa2\xb6n\n\x93\xd0\xa5\x17\xc1\x8b\xcf\xc0\xa9C\x875\x10\xad\x83\xf1\x08\xe7`\xadh\x9a\xf9\x15Y\x80\xe9\x1d\x88\x84udX\xde-*\x08H\xa49j\x8al\xc2\xa1\xaa\xcc4/\x8c\xaf\x02HKC\xdez\x14\xf41\x88!\r\xf85\x92\x81\x0b\xfe\n\x9c\x83\xecE\xf9\xd0\x91"\xbf\xbf\x88jZ\x82\xe4\xdf#m\x96D,\xb2\xd4\xcc+2\xa8c\x0c`9A\x98\xa1\x91K\xc1\x80\xb6Y\xcc\xccH\xdd\'\xb1\xde0\xe5C\xd0\xafL\x9dm\x8b,\xa1\xb8,l\xa1\xf3r=\xf2\xcd\x8fD\xf5\x15;aEShS\xe7B\xe5\xd0\xa9`.4\xe0\xc5\x1c\x88\x8c<\x08bYycd\x9b\x17\xa84\x86\xce\x012\x87NH+8\xa2\x0b9\x16\xacA\xec\x9ca\xba\x139\xfa\x18h-\tk\xa6FMAR\xad\xb1\xdfX\xff\x00R(<\x13M\x07\xdc\x93"k\x01*\xa2\xba\n\xa8\x0fM\x81\x12\xa2\x11]\x0f\x06I\x05\xb0\x93\xb4+_\xc8\xff\x00\xf0\x84\xe9\xf9\x8aa\x07\xa18\x11\x91$\x9d\x00\xd5\xe2f\xf2\x13;\xfcI\xcb~R\x10R\xc5\x02\xa8\xa1\xc0\x83\x80\xf3rL\xbb\xc0AH\x19O\x80\xa5C\xff\xda\x00\x0c\x03\x01\x00\x02\x00\x03\x00\x00\x00\x10\xb0M\n \x82\x08\xb3]\xe0"g\x902\xcd(i\x89zT\xd1\x10\xeaA\xce\x17\x07\x14M)\x14}\xddoz\xeb\xb2\xe3\x08T\xd5q\x07:~q\xb7\x8e)=\x05cbm\xb6Fw\xf0;,\xb7D@P\xf6\xf3\x1c\xa7n\x82\x806R\xc7\xdb\x13\xf6O\x92?\xa8@(\xf7\xb1\xf9\xcc\x94\x06A\xa9\xd9Ea\x19\x15\n#\xd3M\xbc1[\x02`5}\xa6\xd9\xa4u-\x83\xadK\x8a\xc0X\xd4U6\xa6\x9fQ\xeb[\xe2\xca\x15\xc90\x98\x01\xb0v5\x88\x1dc\x96c\xa7]\xbdh\xca;\xfd\xf2\x0e\xc9\x00\x03QA\xe4 t\xba\xd3\x9d\xd2!\x15\x0c\xb2\xd7\xdf\x10\xfc\x04\xb0\xc1\x1ed\xb8\xc1tZ\xd7\x80\xd8\xa8*\xe6\xd7\xc8\xd3I\x00\xed\xf8\xf8n\x02\xb5Q\xeb8\xa9mxrf9%\t=\x0f\xc4\x1f\x80\xd68\x86\xc3\xa4\x05t\xca\x07&\xbct\xe8\xa0\x08,\xe1\xfa\xbb\xdd\xaa\xb3\xfd3\x18\xb9\x8f\xef\x84\x86b\xf2\x98\xe5R\xd4^be\x16\xf9\x91\x83\x18\xec\xa2`/d\xcd\xbef\t\xfc\xb8\x05\x10\x166\xc7\xc9\xd7\x8b\xb8\xb9b\x7f\xf8\x83\x7f\xff\xc4\x00\x1f\x11\x01\x01\x01\x00\x03\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x01\x00\x11\x10!1AQa q\x81\xff\xda\x00\x08\x01\x03\x01\x01?\x10\xff\x00\x1fV{\xe8\xb22_\x91c \xcb\x11`\xb0\xf3\xfdo\xdd\x9f\xf8\xbd\xf2\xcf\xb2\xfc\xbb\xfbm\xa9\nG\x96l\xcc\xff\x00\xc6\x8b\xf0C\xe2\xf8\xddz\xb5\xee\xec\xde!\xd66c\xdc\x17\x8b-\xa4\xce\x06\xb6d\x18G\x17\xb6=ay\x8c\xf2LA\xc2q}\xe3\x03K\x06\x9b\x19a\xdc\x9b\xc4\x1c^\x06g!\xd5\xa3\x89\xe6\x16\xc3\xf0\xb7@#\x07s\x84\xf6\xc7\xa5=\x04\x92q\x93\xed\xe6\xc3\xbe(n\x9e\x9e\xb6+\xb0xF\xbb;\x83\xe4B\r\xd5\xe3\x9f\xc9\xe5\x9e\x0e\xc0\xe1\xb5G\xba\x9b\xa0\xfbh\x06uw\xee\xefV\xd3\xfb\x18A\x88\x98FI\x9e\x05\xf7\x8b\xd5d\'-\x9eE\xd1\xd9/\xd9\xc1\x89\x84_#Lm:O\xbb\xdd\xea\xf4\x8b\xe7\x8d\xba\xbcd\x91\x18\x8d\x12\xa5c\xd3y\xc8\x1cv\x1e\xe5\xb9\x17\xc8\xef\x87\xc8g\x82\xdb\x137\xa9\xee\x91\xec\xf2u\x1e\xdd\x1b\tz\x96\x1dwi\xe4p\xf4\xb3\xc5\xe9\x1e\xc9\xd4\xfb\x11\xfb\x86\xd8[\xbb\xb6\xd0\xd9}\x87^\x1e\xafH\xbb\x11\xc4\xe0\xf2:p\x1bj\xd5\xe5\xb0w\xc8\xf5{Gq\xf8\xb7\xad\xe1\x8d~\xa3\\\x97\xba\x85?\xe5\x0b\x9c^\x88\'#\xee*\x11\xe4\x12\xf9\x1e\x99\x85\xb8.\x12\x06)vB\x0b\xa7\xa8\x06\\\xbf\x94\xaf\x9f\xc1-\xe5\xf5\x0b\xf6\x8b\xdb\xa5T?|u5p\x97\x05\xee/s\x06\xe0-\xb1\x95\x0c_\x9b\xb9\xac/\xb7\x04j\xac\xed\x80\xf3\xc7QF\'a\x85\xf6e\xcb{\x9d\x0be\xeez\xeej\x01\xbdY\n*\x9e\xa1\xb6\x83\xd8\xc2\xd7I\xcd\xc2WW\x91\x0e\xa5\xc9W.VXU\x0b\x87\x98i\x12=\xcb*\x0c,\xfa\x11_\xbe\xbf\x87u5\xfcj\x7fQ\x1d7\n\xd6\xbbu)\x8a\xb5\xcb\x04k\xf9\xee\\UGd\r\xa7Fe&\xa6o<\x11\xd60\xea\x01\xf3*$\x08\r5\x14Q\x8f$\x11\xf4\x07\x12\x9a\x84\x83AE\xf5\x13\xda\x06\x9e#\x01i\xda\x0e}$\x8b\x0f\xb1\xd40\xcb\x84$\xab\x81\x04\xdb\xe35<\xc1c\xfc]K\xbe\' -\x107\x8foQP\xb4]\xc6\xeb\xd1\xe6S\xba\xe0\x12\xae\x96iB\xc2\xb0\xeb\xcc)#_\xb3]\x1c\x10mq\\\x93\x05r\xee\x1f\xd4\xbegty:\x87l\xca\x0fP*#\xcc\x97\x0bW&\xa5+i\xb3r\xdd\xc3\xa2\x10\xeb*]\xe2\xb5t\xc7\xa8J\xff\x00\x84l\xc5B\xb4\xd3\x02\xa7\xf4g\xee\xfe\x19r\xc8V\xf3\x01\x85\xad\x19\x97\xca\xb3\x92!a\xdc\xa9\xe8\xe8!!\xc7\x0e\xe0V\xf0k\xcc\xb4\xf2\x8cz\xda\x9b\xaf\x11\x90\x10E\x8c\xcc\xf8\x99[a\xb6\xa0\xdc[\xf86Og\x04\x15\xd3\x86\x18\xc1\x99f\xcef-ou\x00\x8d\r\xf6\xf3\x11\xae\x9b\x08\xc2Pb\xfb%)\x87\xec`\xfb\xbdJ\x8c\x87\x12\xf2G\xf8G\xf7\xff\x00\x0b\x98\xb3\xcc\xa9\x96\xa7_\xb5\xe6P\xe04E\xea\x03\xb4\x04\xd3\xc09\x88\xc3\x8e\xdf1X\n]\xf8"\xf0\x9eaK\xc9E\x7f\xb0\x91\x0b\x16_\xd4D6\xb8%\x03\xba\x96\xe0\x95\x13N}\xca\x9a\xe5\xca\x88\xe4\xacg\x0b\x18)\xc8\x91\xcc\xe4_\xa8\xc46 \xe0\x88\xbc\xdb\x9fr\x96\xcc\xae!\x10*\x9b\xf3+\x16\xecG\xe6\x88\x0f2\xe26\xccK\xc6\x99o\xe5\xab>O\xfd\x80/\xb1\x06F\x14\xe2ZLS$\xa3|&S\xfa\x94L\x13}\x0cj\xb7A\x84\xb8\xc5y\x89Y\xab\xc1\x0c\xf7\x0e\xf4\x8f\xa5\xc5\r\xcf\x11\xd4\xfdR\x86\xee[w2\xc6\xcf1D*m\x94BAg\x1d#\t\r1k\x031\x11\xd0E\xa4\nb\xa5\xb7\x00<\xc4C\x96\x83\xc4\xeaAk\xcc\x0b\xd8\xfe\x08\xa94\xc5\xcb]\xa7\x04\xf2\x16\xccq\xd6\x12\xd9mFX\xb8\xb0\x0c\xf9a\x14dT\xca\xd8\x13\x07\xa2n8#\x99\xd8\x93\xf28WW\x88\xac\x8cV \xc0\xb7u\x1bM\xac\xc2P\x11\xfes3\xec\x84\xf34\xe0)<\xca\xdc\xd5\xaa\xe5\xa8\xd9\xab!\xc3\x9f\xa9\x80\x0bj\xe2\xcd\xe64.b\xdd\tTV+\x9a( {p$\n\xc4\xdc\x0f,\x18\xe4\xd8\xf7\rZ\xfaGp\x88qe\x1b>\x8c0\xdb\xb8+\xb3\x91\x058\x86\xea[\x9eU\x12\xa7X4\xfcA\xa9\xc2\xcc/\xb2_P\xa8\xaf/A\x0c\xdd\xe1\rp\x8b\x8f\x0cI\x18T\x96\xe5T>\xb9\x9c\xf2\x80>\x8e8\x80\xf7\x91\xeep\x82\x94\xcd\x83\x9b/$\xe1hw\x7f\x92\xda\x14\xcf\xd3\x18\xa7"s\xc4\xc4@\xb5\xce\xba\x96\xad\x10\x83\x0f\x92WCl\xba\x81\xba\x88\x17\xcc\xcd\xc5-\xe3\x0f<\xc0\\\x04}\xc2\'\x1cG\x04\xab\x82\xe2\xd1\xd8\x00P\xff\x00gc\x16\xbe (\xdc+\xc5\xcb\xda\xb3j+0\x80mr\xdf\xb0\x8d\xb3\x04\xc9a\xb4}\xc2\xa9\xb0\x06\x1d\x15\x92\xc3\xf6\xa7\xca\xce/ \xfd@*\x00j\x8d\x99\x98\xf3\x94\x06V\xca\xc7\x98&,\xf6\xaa\x84)\x80eyJ\x0c\x10n]\xcc\t\x94\xa52\x0b"U\x0ea\x1f\x98B\xe9By\x84\x13&`\xd0\xee\x19\xb5@T_a\x0b\xc3\xc3.\n\xca\xc4ZiM\xe22\xb2\x16G\t!T_#\x18\x9d\xe2F\x0f\x03DD\xecc\xc1\x0cU1\xa0So\x96\x0b\xe4\xa8\x07\x04\xb20b\x18ia\n\xf6\x11\xca\xff\x00\xfc\xca\xe9\x85o\xc4E\r\x02\xbe\x08\xcc\xba\x07\xdc\xc1}\x8f\xc4A\x00\xe97\xe0\xab\xfb\x88\x92\x91}C(\xfb\x00{\x98H\xb7L\xcf\x05\xaa`\x128\xc45\r\x8eKV\x8c\xe2)8p\x97\xb8\xac."x\xe4ek\x02\xd1\x98\xeb\xdf\x15\x99\x80\xb6\ri\x8a\xcf\x88\xae\xf2\xd2\xfb\x82\xa0\xab\xf9\x10\r\xe5\x8c\xecX\x99jy`\xbb\xbc\xa9\x9d\x943P\xcdZ\xb9\xeef*r\x10\xa2\x18\xb9\xa2\x7f\xf1\x14\x95\x8bT\xa3\xca\xc8\xac3`\x1cB3\xccS\x91\x03\x1dOL\x15\x1d\x00d\xfe\xa3\xa1\xb4\xb3\xd1\xd4c\x14\xcbe\n\x1c\xed\xd0\x8dV\xeb]\xa2\x10\xf8 @(\xab\x9d\xd0\xa5\xda^\xef\xa9\xba\xaf\xe4C\xcd\xb2`\xea#\x0eO\xf7(\xf6\x0f\xaf\xe1"\xef6W\xfct\x83\x9c\x916\xb4\x03\xf5\x11d\x95D\xb89\xd2W\xdc,\x14Ao\x18\xcc5\x15g\x7f@\x12T\x85.#T\xbb\x0c\xac\x9bp\xc6\x8d\x98(\x96\x99\xe4\x94yx \xd7\xd3\xb2u\x00\xd6"\xe1\x80{\x8d\xb3Y@\xca\x0e\x1d\xcc\x0b~\xc2<\x19%\x03\x82\x1e \xdb\xb2E\x0e\xca\x99K\xed)R\xa46$mQ\xf98\xba\'E\x1b\x13\xe5\x99\x03Z\xfa\xb8\xa8\xf6\x10\x8c\x8b@\x11\x08\xf0J\xc7`\xdccvk\x16\xe2\xa5\x8f\x86T\xbb,\x00\xe1KLj\xd2w\x06\xfb<\x10b\n\xc5\xdfx\x8dVi\x98\x83\x85\x01\x8a\xdc\x01+\x078\x02V\x93m\xc0!x8\x8a\xd3\xa5\x00\xb2\xd9\r\x16\xa8L\x9a\xd9\x84p\xb5m\xbe \xd1S\x89-\xba\x8e\xe2\x8c\xc1\xeeA\xf0y\\\x13\xcdq5R\xe9\xb2\xb8\x93\xb9\x99\xcb\x01\x8b\xa5\x04\xb8\xd8\xfe@\x93<\xf9Y\xd7\x03\x85x\xc8\xcf\xcc\xb3\x971AAu\xcb\xb8\xab!\xc0\x84\xf7\r\x93)\x88+\xa7\xd3\tb\xd4\x9e"\xe0+\xf8\xc0\x85\xca\xe6\t\x1b\x19;\x98>\x06I\xf7\x19k\x96\n5t.\x00\n\xcbb?\xa6\x82\xaf\xde\xa2\xc4/\x0f\xb6\rLr&@\xb6\xa3\xc4\xdfT.ta\x98\xf4\x1b{\xa2T\xa6-\xc0\xf4\xb0n\xdec\xb1\xad]\xe4 \xc3\x83<\xdc\x1aA\x1c\x15\x0f!z\x96(\x96\xb5R\xd8\x10E\xf9\x99\x02\xd7\x17\r,\xda\xc8\x05\xb8j0\xd4\tU\xa2\xdco\xb7\x0b\x97\x9c\x13l\x0e\xd6\xad\x0cD\x15\xdb\t\x958#i{\xc3I\x11[\x14O\xf1\x1f]E\xbe\xe3\xb3\xd6.\x07\x1ar>\x19Z\x1d&\x17g&\xa5\x18\xbd\xb7\xe2\x05\xc3BY\xe8\x10\xdbm\x88\xca\xccc\xa2R\xdd\x80\xc7C\x028\x85\xc68\x0c\xae8h\xa2X\xda/2\xc3A@\xee:s]\xb5\xb8\xfd\xa8Qv\x0e\xe1v\\\xe9\x98\x02\xf1v\xd8u\t\xf3\x0b\xc3-Ks\xb7\xb8(|\x92\xa3J\xc1\x03.\xd9j\x18\x11\xcc\xf2\\\xab\xd6j/\x8d%\xc0\xf0\x00R\x92\xe0\x95\xfd\xca\xb9]\x9c\xc6\x02F\x0b\xc3W$Th\x168I`\xed\xf31(\x04\xa8\xcc\xb5\x82\x898\x10"x\x11YsWa\x1d.\x85\xab\x8ex\r\\\x14\xd4l\x10\x86\x87Ph0\x9f\xdcr\xb4\x15p-\x8d\x16\x19GT3\xd26uV\x89\xcc1\x8b\xab\x96*b\x92\xf1U\xb6\x9b\xe6\\J\x01\xd9\x98\x9f(7\x14\xdd\xc0m\xc0\r\xe0D{\xd7\xa1\x8a\xae\x84\xdb\x82,\xa6\xaa\x06\xe6\xee\x1e_\xa12\xca\xad\x1c\x91\x05\xe4[\x1e\xd8\xcb\x9f6\x87\xcc}\xb7\x83\xa0\xe6\x12\xa4\x16\n\xbc\xa3\xf0y#\xda\x91r\xd8r\xef\xc2:H<\xaa\x10B\x9a%\xe6\xe8\xb4\x85Q\xe8\xb7\x98\x80\x95i\xe5)\x8f8\xff\x00b\x01\xca\x99_\x04/\xab\x85\x85\xc1&\x7f\xbdQ\xee\xb4\x1b\xf7s5\x89\x15\xdb\x10\x06\n\xcc\xe0 \xea\x17\x83\x1e\xcd\xc2\xae\x1c\x98V1*\x9c\xd9\x94\xea\xa2\xbc\xb0\xd50P.\x88\x1c\xc4p\xc6\xa5\x14\xa1~\xe5,\xe8\x80R\xe6\xe1\xc2\x07j6\x96\xe4\x01@\x10\xab9\x85\x05\x8e\x1e\xa3`\xd4\xe5\xcc\xb8H)\x89\n!Uz\xb8\x19`3\x18iu\x1dJ*\x8d2\'A\x89a0\x1fd\xa0\xbd\xd7\x13z\xda\xcc\xde\xb3}\x13\x1a1\x88\x81\xe6\x8e\xb4&\xa1\x9a]\x0b\xf7*\x96\xaf\xcc*\xe4S\x99b\x7f\xc2\x10\xd08\xdc\xbf\x95_\x11-\x95\xf1((\x1c56"\x16\xd4\x8c\xc6\xd6c?\x0b\xe2\t1\x12\xf5\xd4\\\xf5\x98r\xcd\xdc\xe0.W\x85\xff\x00\x92\xb6Qx\x94\x01Gp\xad\xde\xf1-\x04\xfa\xca\x80\xbd\x0c\xadj\x86"\xe1\xa6\xb0\xc4\x01\xea\xe1\xaa@\x99\xf1\x1d\xe36\xcaX\xe6\x85\x8faPBVf\x10\xa8\xd8\xc21+F\x18\xc6ys9)G\xccp6]\xa7\x11\xcb9\x1f\x13\xc7\x06\xd2(\x8a\xc1\x00\xc9\x99b\x94\x82\xe8s\x18P.ZF\xf3\xc4-\xa9p\xe8\xc4\x00 h\xb9\x97\x89\xc4\'\x08\x80\x19a\\\\@\x8c\xf7\x15\xa1\x9fr\xec\n`2_W)*k\xcctV\xa1\xb4Tt\xc2\x85N\xc1\x0bL\xc8\x80\x1c^!E5x\x82;)\nm+r\xe2:\x02"\xa6V!r\x19\x06\xde\xa5c\xf3\x98\xce\xbd\x19\x8eFS^\xa5@\xf0\xcc\xadT\xc2\x9c\xca\xbbH\xb1)r\xbaw)\xb9\xa9C\xc2P\x1eb\xb33Q\x8bd\xbe R\xb2\xf6\xda\x80]\x92\xc1\xbdK\xe1\xa7\x98\x1d\x99X\nL\x04NTA\n\x9f\x12p\x8e;\x8cw\x98w-G\x95X\x19&\xf1\xa1\xfb)=\x85\xccW\x01\x88{!\xdf\xf9\x88\x9e\xc6\xa0\\\x8cC\x92\x84\xee.\xaaT\xcd\x16[\'q\xa6\xcc\xe4\x18\xdd\xeb\x0e\xdex\x95\xb6\xe1b\x8b\xfa\x8b5o\x04\xab\x14LK\xa7\x10Ux\x85ilb\x81\x16\xd0\xe43\r]\x80\xdcV+$=)\x80\xb9\xc4F\n\xcd\x96\xe2Sq2\xc4f\xcc\xc2\x9e\xcd@\x04*\x92\xe9\xc5/\x88D\xac\xd3\x01[\x02\xa5\xc3\xbb\x08\x86\xed\x85\xe7\xac\x17\x9bb0\x1f\x7f\x13\x9eA-\x93\x0ba\xb8\'H\xa3\xb8\xcbg2\xc1\x94\xce\xd4{\x89\x04\xf40\x94\x87K\x0cCZ5O0D\x18rU\x898:\x96q\xb8Af`FBP\xbb\x95\xbb`q%\xd7ie8&(in%mVd\xf2C\x10\x9fQ\xd8\xc8\xe6p\xe9\x99\xa6\x96\x14\xfb\x97\x17\x19\x89n\xa1X\x8d\x1aa\x15\xee\x051p1\xda\xd4\xb1\xcb\n\x95\x06J\xcc\xfdi5La\x8a\xd21P\xfd\x1a\xd4\xcc\xaa\xcdT\xad\r\xa2\x0c\x8c\xe4\x0cM\xbaC2\xf4\xb6nR\xd2\x10\xb0\x94\xe1\n1\xa5Q;q\n\x85\x8bI\xc0\x98\x89\xca@\x9f\xf1\x132\xe0$kze\xa8\xb6L\xce\xa2\xc0\x82[\xadK\x10[\xe0\x82U\xc5\x11\x10\xc9,\x067\x91\x97\xdf\x06\xa3\x9a\x18\x02\xc5(\x1cG\xd6\xa5L\n\xae\xf9\x86\xc5\xbb,\xc4\x0fQ\xc6\xde`\x178\x96\xad\xe2\x13lnhC@eV\xdc\xa4W{\x99\tu3\xaf\xc6W\x13\xc1\x03nY\x80\xcd\xc4\n\xb1\xcb\x04i\x12\x93\x82]\xeeX\x17\x9b\xc2j\xf3\x84\xd6/r\xed=Q\xea\x02=B|\x83-V\xc5s+\xca\x8a\x95At\xb1\xa9\x86(U\x11\x8b\xa8\xc3u\x93\x10\xd1M\xcc\xd6\xc9+3\x8c{\x8b\xa9l\xd1\xccrs\xb7\xb8\xedp#0\xbec\xdd\x83\xc4\xbeU\x9e8\x89\xd54\xc0\xa8,\xe6X\n|B\x85\xe6\xe2\xea\x92\x14n>\xa5q\xe4B<\xf0\xbd\x90ds\x16\xa45\xd9x\x98"\x17\x83s\x19\xe2\x1c\x01\x9b\x83\x85Ti\x8c\xc0\x83\x80#\x112\xb2@[uh*a\xdb\x8c Y\xec\x95\x8d\xd5.<\xc5\xc9I\xdcp\xaa\r\xce\xce \xce#P\x1c\x83\x01P\xf1\x1cY9\xb9\x92\x058Y\xae\x96\xde%\xc9#\xb7\x89i-\xfc\xc2\xaa82\xc3\x0f`""\xa5\xc03\xfa\x13\x15\xe4\xfa\x8d\x02s\xee\x03\xd8\x92\xdba\xa7\xcc\xdej7i1\x1e\xe7\')\x96!\xdcT\xf6\x90R\x00)\x85\x99\xdb\xa2L\xc2\xf4\x11\xd10\xaae\xbc\t\x9e=\x08\x18\xee\\\xf0\x11\x88m\xe3\xdcZy\x957\xbf.\xa0\\~\x80\x8d\x88|\xca8o\xa8\xd5\xb6z\xd1\x0c\xd0\xc7\t3\x98\x91I\x1b\x93\x99_\x01\xd4n\xac\xc5\x8aH\x0c\tO\x1c\xc1*\x13\xb6)\x10A\xa2+\xd4\xde\x00$\xa8\xa5\xbd@\xa5a.\x02d\x92\x90\xd1\xf1+\x18Ebq\xf7\x9e\xaa%\x9e\x83\x1a\x8eW\x1eec\xaa\x18\x848\xb5`n\xb0\xb4\xb30\xe51\xd1\xab\\\xb2\xc2\xae \xb1c\x13\x05\xac\x94KE\x11d\xaex0\xa3!\xa2)g5.(\xc4\r\x8b \xe3i\x95\x9b>\xd2\x8a\x1c\x1cL\xb5\x8e\x12\xd8\xd5\xca"IjLL\xb3\x04\x960\x04nb\x92o\x8f)\xbaq\x0c\xa4\x16\x19\xc6\xc9]\x12\xe0j\x83\xe5\x96\x05\xc0"\xfc\xc5\x1f\\\xc0\xe3\x03~\xee\x01sd\n\xef\xb6\t\x17t\xb8\xb2UF\x14\xc4U\x03n\xe5\xe6FP\xcb\n\x9d\xc4N\xce\xa1\x97f*<\x1c\xcd\xca\xa8n,\xa0\xb8\x01\xc4o\x03r\xb6\x15S\x01.\x1a0\xa3\x9a\x95v\xcaE)R\xc7l\x0e\xe5\x11\xb8!m0VC\xe47\x05l\x8d\xd7r\xce\xa9\xf1\xa5\x98/\x9b8\x84^\x82\xae\xbcEV\xc8O"$\xe3\xbc\xae3*\xc5\xc9\x98B\xaeGq\xc2m&)Z\xc3\x00\x06Vq\xa0/\x10\xa5\x96\x0b\x8a\xb6\x1ex\x9eP\x15\xe6Q\xbe\xf8E\x88Ts\x04\xb8\xdfPE\xd6\x16\x18\xad8\x97@B+\xcadxfm.Y\xba\xea \x0b\xcc\xa5Z\xcf\x11\r\x19\x95V\xa3:\x18\xeeX\x9f\xb0\x87\xd4b.tL~b\x9d\xcd\x9c\xdfN\x88F\xe0Y\x01n\x1bTE.\x8ej;\x93\x83\xecy\x88\x8c\xcb-X\xed\x13/\x81\x88\xdd\xc0OpQ\x07j\x8c\xc4E\x88\xd4\xfe\xa91/.!\x99D\xcd\x03Z\'/W\xc4\nj-c\xa8\xb4\xac\x9ee\xe5\x956\xb76N`YU\x98\x10\xb3\xcc\xa8\xb5\xb8\xd1\xe6h9\x8b\x96\xd1p\x0cs\x0e\xdaA\x1eU\xc5C\x1a\x8cv\xce\xaa\xf8=M%\xa5\x8fg\x00\xbe"3\x9d\xf8\x94#\x9a\xb8p8\x9e\xa1!\xd3j3\x91J~\xcb@\xb5\x8e\x07B\xc5d\xbb.V\x13\x7f\x89cm\x8b\xaa\x97\xf2\xdb=T\x18\xc8l\x88D)dJ\x8eD\x1d\xcb\x08\xa0\x9c\x93a\xb4\xa8h\xc4D%\xc6\xba\x8c\xb8\xd4pDU\x1b\x83"\xccB\xa3P\xca\xae`4\x04\xa9d=J\x05a\x86\\%\x8c88\x88m\x04\xee\xe0\xc6\xb7m\xc4HH\x10b\xf9\x97\xf2\x81l6\xbc\x90\x90K\xf8\x847(\nJ\x1a\x95\t\n\\\xdc\xc2\x87\xdc\xe2(\xb8\xe4e\x16B\x90\x9e\x91\xb20hna\xd0\xeb\x15\xfb\x8c\x13\xb4v$\xc2=u\x0ei\xf7\x0c<2\xc4\xb7P\x04pw\x0c\xb5q\x1a\xb0\xdc\x0ed:\x83\xe8_\x96\r(\xcf\x10e\xab\xed\xa9l C\x80G\xa0\\."\xb3sq\xe6!EX\x1a\x88\xb58\xa9\x8e\xad\xb7\x15\x05\xbbPK\xb6\xb5\x91\n\x05q\xc2T\x88\x96\x97\xab\xd4y\x15\x15,\xf5\t\x00\x88\xf2\x93\x11P\xc5}@<\x0e\xaa=\xad\xde\x10\xddF8\x86\xb5.\xf7\t\x95G\x0b\x9e\xe2Z\x93VET=\x7f\x04\x03E\xea+\x00\xfcF\x81\xa7\xd4\xb7\xfe1\xaa\xb7z\x88W\xf4F\xb6\xfaf\xb0\xfe#[\xfd\x13d\xfe \xf0\xb8p\xe8\xe7#\xee\x02R\xad\xa4\xaba\xae\xbf\x88\x1f\x93|G5\xb7\xd4\x11g\xd2\x18\x14\xf5P\xc1_H^J\x17D\xff\xd9' + +ERIKA_MUSTERMAN_PID = { + "birth_date": "1986-03-14", + "birth_place": "The Netherlands, Leiden", + "nationality": ["NL"], + "resident_address": "De Heyderweg 2, Leiden", + "resident_country": "NL", + "resident_state": "Zuid-Holland", + "resident_city": "Leiden", + "resident_postal_code": "2314 XZ", + "resident_street": "De Heyderweg", + "resident_house_number": "2", + "personal_administrative_number": "9876543210", + "portrait": ERIKA_MUSTERMAN_IMAGE, + "family_name_birth": "Mustermann", + "given_name_birth": "Erika", + "sex": 2, + "email_address": "erika@mustermann.nl", + "mobile_phone_number": "+31717993005", + "expiry_date": "2030-01-28", + "issuing_authority": "Fime", + "issuing_country": "NL", + "document_number": "0123456789", + "issuance_date": "2024-01-28", + "age_over_18": True, + "age_in_years": 38, + "age_birth_year": 1986, +} + +ERIKA_MUSTERMAN_MDL = ERIKA_MUSTERMAN_PID + +ERIKA_MUSTERMAN_PHOTOID1 = { + "family_name_unicode": "Mustermann", + "given_name_unicode": "Erika", + "birth_date": "1986-03-14", + "birthplace": "The Netherlands, Leiden", + "name_at_birth": "Erika Mustermann", + "nationality": ["NL"], + "resident_address": "De Heyderweg 2, Leiden", + "resident_country": "NL", + "resident_city": "Leiden", + "resident_postal_code": "2314 XZ", + "portrait": ERIKA_MUSTERMAN_IMAGE, + "sex": 2, + "expiry_date": "2029-01-08", + "issuing_authority_unicode": "Fime", + "issuing_country": "NL", + "document_number": "0123456789", + "issue_date": "2024-08-01", + "age_over_18": True, + "age_in_years": 38, + "age_birth_year": 1986, + "portrait_capture_date": "2022-11-14T00:00:00Z", +} + +ERIKA_MUSTERMAN_PHOTOID2 = { + "person_id": "1234567890", + "birth_country": "NL", + "birth_state": "Zuid-Holland", + "birth_city": "Leiden", + "administrative_number": "9876543210", + "resident_street": "De Heyderweg", + "resident_house_number": "2", + "travel_document_number": "C11T002JM", +} + +def inject_sample_data(json_data: dict) -> dict: + from copy import deepcopy + json = deepcopy(json_data) + data = json["data"] + if "eu.europa.ec.eudi.pid.1" in data: + profile = data["eu.europa.ec.eudi.pid.1"] + if profile["family_name"] == "Mustermann" and profile["given_name"] == "Erika": + print(f"Sample profile input found: {profile}") + profile.update(ERIKA_MUSTERMAN_PID) + print(f"Sample mutated: {profile}") + if "org.iso.18013.5.1" in data: + profile = data["org.iso.18013.5.1"] + if profile["family_name"] == "Mustermann" and profile["given_name"] == "Erika": + print(f"Sample profile input found: {profile}") + profile.update(ERIKA_MUSTERMAN_MDL) + print(f"Sample mutated: {profile}") + if "org.iso.23220.1" in data: + profile = data["org.iso.23220.1"] + if profile["family_name"] == "Mustermann" and profile["given_name"] == "Erika": + print(f"Sample profile input found: {profile}") + profile.update(ERIKA_MUSTERMAN_PHOTOID1) + print(f"Sample mutated: {profile}") + if "org.iso.23220.photoid.1" in data: + profile = data["org.iso.23220.photoid.1"] + if profile["family_name"] == "Mustermann" and profile["given_name"] == "Erika": + print(f"Sample profile input found: {profile}") + profile.update(ERIKA_MUSTERMAN_PHOTOID2) + print(f"Sample mutated: {profile}") + return data diff --git a/app/static/jwks.json b/app/static/jwks.json deleted file mode 100644 index 60bc0d10..00000000 --- a/app/static/jwks.json +++ /dev/null @@ -1 +0,0 @@ -{"keys": [{"kty": "RSA", "use": "sig", "kid": "andTMHFPNUxuZVh2MHV2MmwtcWZQdGpmSWhHTE9idGx0akJGbFlfaVZHOA", "e": "AQAB", "n": "2fv0MmYjq_bxG4Cc0PRapFjEmuaBd-Lw7xLgR-252ZUPbbSBjX94_KMfS-orQJ_B3BzbGhKBbH6xJZt5CIb1KRpUrQ7pr-A_eO68FxsgXqbp4wqoHscqLh7EQiMIORiaNlDsCHFXmRyRq9opctbABlgCItEIGgV5K7lXcA-_ZYB6iluHd8dsQFP2P7H1_fytqHZoVpnnlBBtVqaK_fPeX6O3dGmzP0Th5cp_Omnxnr-Sg-Zkbb8eCvQa5LGKv8wHheeLzScfY1E6ll2W3vsOtvNlvCtVYh9ZchDvRWpM8sdPTz8tj5xohKW_BLCqOli8Fbx-uLElpwiy2bpC8OFIQQ"}, {"kty": "EC", "use": "sig", "kid": "MmZHSC14RXp5TTB5d0tuQ19kQXVrSVVKRWJzUVI5eDBzQi1wVnAwU0h2bw", "crv": "P-256", "x": "Q42rGKKOWQXyd1a1UpuZ7HOwI6Qmh0So6XNha4FZ3p4", "y": "CNIlLsXMps61l1rEXi-WFxrD59870OuPny94czFASSo"}]} \ No newline at end of file diff --git a/resolve-ip.sh b/resolve-ip.sh new file mode 100755 index 00000000..6b63340d --- /dev/null +++ b/resolve-ip.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +hostname -I | awk '{print $1}' diff --git a/run-issuer.sh b/run-issuer.sh new file mode 100755 index 00000000..2c5303b2 --- /dev/null +++ b/run-issuer.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +HOST_IP=$(<.config.ip) + +source .venv/bin/activate +export REQUESTS_CA_BUNDLE=$(realpath cert.pem) +export SERVICE_URL="https://${HOST_IP}:5000/" +export EIDAS_NODE_URL="https://TODO1/" +export DYNAMIC_PRESENTATION_URL="https://TODO2/" + +flask --app app run --cert=cert.pem --key=key.pem --host="$HOST_IP" diff --git a/scripts/watchdog.py b/scripts/watchdog.py new file mode 100644 index 00000000..a0f8447e --- /dev/null +++ b/scripts/watchdog.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +import argparse +import requests +import time +import urllib3 +from subprocess import Popen + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +DEFAULT_HOST = "https://snf-74864.ok-kno.grnetcloud.net:5000" +DEFAULT_TIMEOUT = 5 + +proc = None + + +def restart(): + global proc + if proc is not None: + print("Terminating stuck server...") + try: + proc.terminate() + except Exception as e: + print(f"Error terminating server: {e}") + print("Restarting server...") + proc = Popen(["./run-issuer.sh"]) + print("Server restarted.") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Monitor server function") + parser.add_argument( + "--host", + type=str, + required=False, + default=DEFAULT_HOST, + help=f"URL to watch for changes, default: {DEFAULT_HOST}", + ) + parser.add_argument( + "--timeout", + type=int, + required=False, + default=DEFAULT_TIMEOUT, + help=f"The timeout to use, default: {DEFAULT_TIMEOUT}", + ) + args = parser.parse_args() + + url = f"{args.host}/.well-known/openid-credential-issuer" + print(f"Watching {url} for changes (every {DEFAULT_TIMEOUT}s)...") + + while True: + try: + response = requests.get(url, timeout=args.timeout, verify=False) + response.raise_for_status() + except requests.exceptions.Timeout: + print("Request timed out.") + restart() + except requests.exceptions.RequestException as e: + print(f"An error occurred: {e}") + restart() + time.sleep(DEFAULT_TIMEOUT) diff --git a/setup-cert.sh b/setup-cert.sh new file mode 100755 index 00000000..c79996d2 --- /dev/null +++ b/setup-cert.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +if [ "$1" == "-h" ]; then + echo "Set up the certificate of the issuer" + echo "Usage: setup-cert.sh [LOCAL_IP]" + exit +elif [ "$1" == "" ]; then + echo $(./resolve-ip.sh) > .config.ip +else + echo $1 > .config.ip +fi + + +LOCAL_ADDR=$(cat .config.ip) +echo "Using local address: ${LOCAL_ADDR}" + +echo Installing keys/certificates... +PRIVKEY_DIR=/etc/eudiw/pid-issuer/privKey/ +CERT_DIR=/etc/eudiw/pid-issuer/cert/ +sudo mkdir -p ${PRIVKEY_DIR} +sudo mkdir -p ${CERT_DIR} +sudo chmod +rx /etc/eudiw +sudo chmod +rx /etc/eudiw/pid-issuer +sudo chmod +rx ${PRIVKEY_DIR} +sudo chmod +rx ${CERT_DIR} + +echo Copying signing certificates... +sudo unzip -o api_docs/test_tokens/DS-token/PID-DS-0002.zip -d ${PRIVKEY_DIR} +sudo mv ${PRIVKEY_DIR}/PID-DS-0002.cert.der ${CERT_DIR}/ +sudo chmod +r ${PRIVKEY_DIR}* +if [ -f "${LOCAL_ADDR}.crt" ]; then + sudo cp ${LOCAL_ADDR}.crt ${CERT_DIR}/ +fi +if [ -f "${LOCAL_ADDR}.key" ]; then + sudo cp ${LOCAL_ADDR}.key ${PRIVKEY_DIR}/ +fi + +echo Copying IACA files... +gunzip -f -k api_docs/test_tokens/IACA-token/PIDIssuerCAUT01.pem.gz +sudo mkdir -p /etc/eudiw/pid-issuer/cert/ +sudo chmod +rx /etc/eudiw/pid-issuer/cert/ +sudo cp api_docs/test_tokens/IACA-token/PIDIssuerCAUT01.pem /etc/eudiw/pid-issuer/cert/ +if [ -f "root-ca-grnet.pem" ]; then + sudo cp root-ca-grnet.pem /etc/eudiw/pid-issuer/cert/ +fi + +function generate_config_file() +{ + host_ip=$1 + cat << EOF + [req]\n + default_bits = 2048\n + distinguished_name = req_distinguished_name\n + x509_extensions = v3_req\n + prompt = no\n + [req_distinguished_name]\n + countryName = XX\n + stateOrProvinceName = N/A\n + localityName = N/A\n + organizationName = GRNET\n + commonName = GRNET-EBSI-Issuer\n + [v3_req]\n + subjectAltName = @alt_names\n + [alt_names]\n + IP.1 = $host_ip\n +EOF +} + +config_file=$(generate_config_file $(<.config.ip)) + +openssl req -x509 -nodes -days 730 \ + -newkey rsa:2048 \ + -keyout key.pem \ + -out cert.pem \ + -config <(printf "$config_file") diff --git a/setup-issuer.sh b/setup-issuer.sh new file mode 100755 index 00000000..bce4a44c --- /dev/null +++ b/setup-issuer.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +if [ "$1" == "-h" ]; then + echo "Usage: setup-issuer.sh [IP]" + echo + echo "IP the local IP of the issuer, empty for autodetection" + exit +fi + +./setup-venv.sh +./setup-cert.sh $1 +cp app/app_config/__config_secrets.py app/app_config/config_secrets.py + +if [ "$1" == "" ]; then + IP=$(cat .config.ip) +else + IP=$1 +fi +git restore app/metadata_config/metadata_config.json app/metadata_config/oauth-authorization-server.json app/metadata_config/openid-configuration.json +git grep issuer.eudiw.dev | fgrep --color=none .json | cut -d ':' -f 1 | sort -u | xargs sed -i -e "s/https:\/\/issuer.eudiw.dev/https:\/\/${IP}:5000/g" diff --git a/setup-venv.sh b/setup-venv.sh new file mode 100755 index 00000000..bd392c0b --- /dev/null +++ b/setup-venv.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +echo Installing Python dependencies... +if [ ! -d ".venv" ]; then + python3 -m venv .venv +fi + +source .venv/bin/activate +pip install -r app/requirements.txt