## # This file is part of GNU TALER. # Copyright (C) 2014-2016 INRIA # # TALER is free software; you can redistribute it and/or modify it under the # terms of the GNU Lesser General Public License as published by the Free Software # Foundation; either version 2.1, or (at your option) any later version. # # TALER is distributed in the hope that it will be useful, but WITHOUT ANY # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License along with # GNU TALER; see the file COPYING. If not, see # # @author Florian Dold # @author Marcello Stanisci # @brief Implementation of a donations site. from urllib.parse import urljoin, parse_qsl import logging import os import uuid import base64 import random import requests import flask import traceback import urllib import qrcode import qrcode.image.svg import lxml.etree from taler.util.talerconfig import TalerConfig LOGGER = logging.getLogger(__name__) BASE_DIR = os.path.dirname(os.path.abspath(__file__)) app = flask.Flask(__name__, template_folder=BASE_DIR) app.debug = True app.secret_key = base64.b64encode(os.urandom(64)).decode('utf-8') TC = TalerConfig.from_env() BACKEND_BASE_URL = TC["frontends"]["backend"].value_string(required=True) CURRENCY = TC["taler"]["currency"].value_string(required=True) APIKEY = TC["frontends"]["backend_apikey"].value_string(required=True) app.config.from_object(__name__) ## # Extend the templating language with a function # that fetches values from the environment. # # @return the environment-reading function. @app.context_processor def utility_processor(): def env(name, default=None): return os.environ.get(name, default) return dict(env=env) ## # Return a error response to the client. # # @param abort_status_code status code to return along the response. # @param params _kw_ arguments to passed verbatim to the templating engine. def err_abort(abort_status_code, **params): t = flask.render_template("templates/error.html", **params) flask.abort(flask.make_response(t, abort_status_code)) ## # Issue a GET request to the backend. # # @param endpoint the backend endpoint where to issue the request. # @param params (dict type of) URL parameters to append to the request. # @return the JSON response from the backend, or a error response # if something unexpected happens. def backend_instanced_get(instance, endpoint, params): backend_url = urljoin(BACKEND_BASE_URL, f"instances/{instance}/") headers = {"Authorization": "ApiKey " + APIKEY} try: resp = requests.get( urljoin(backend_url, endpoint), params=params, headers=headers ) except requests.ConnectionError: err_abort(500, message="Could not establish connection to backend") try: response_json = resp.json() except ValueError: err_abort(500, message="Could not parse response from backend") if resp.status_code != 200: err_abort( 500, message="Backend returned error status", json=response_json, status_code=resp.status_code ) return response_json ## # POST a request to the backend, and return a error # response if any error occurs. # # @param endpoint the backend endpoint where to POST # this request. # @param json the POST's body. # @return the backend response (JSON format). def backend_instanced_post(instance, endpoint, json): backend_url = urljoin(BACKEND_BASE_URL, f"instances/{instance}/") try: resp = requests.post( urljoin(backend_url, endpoint), json=json, headers=headers ) except requests.ConnectionError: err_abort(500, message=f"Could not establish connection to backend (url={url})") try: response_json = resp.json() except ValueError: err_abort( 500, message="Could not parse response from backend", status_code=resp.status_code ) if resp.status_code != 200: err_abort( 500, message="Backend returned error status", json=response_json, status_code=resp.status_code ) return response_json ## # Inspect GET arguments in the look for a parameter. # # @param name the parameter name to lookup. # @return the parameter value, or a error page if not found. def expect_parameter(name): val = flask.request.args.get(name) if not val: return err_abort(400, message="parameter '{}' required".format(name)) return val ## # "Fallback" exception handler to capture all the unmanaged errors. # # @param e the Exception object, currently unused. # @return flask-native response object carrying the error message # (and execution stack!). @app.errorhandler(Exception) def internal_error(e): return flask.render_template( "templates/error.html", message="Internal error", stack=traceback.format_exc() ) ## # Serve the main index page. # # @return response object of the index page. @app.route("/") def index(): return flask.render_template( "templates/index.html", merchant_currency=CURRENCY ) ## # Serve the "/javascript" page. # # @return response object for the /javascript page. @app.route("/javascript") def javascript_licensing(): return flask.render_template("templates/javascript.html") ## # Serve the "/checkout" page. This page lets the # user pick the payment method they want to use, # and finally confirm the donation. # # @return response object for the /checkout page. @app.route("/checkout", methods=["GET"]) def checkout(): amount = expect_parameter("donation_amount") donation_receiver = expect_parameter("donation_receiver") donation_donor = expect_parameter("donation_donor") return flask.render_template( "templates/checkout.html", donation_amount=amount, donation_receiver=donation_receiver, donation_donor=donation_donor, merchant_currency=CURRENCY ) ## # Serve the page advising the user about the impossibility # of further processing the payment method they chose. # # @return response object about the mentioned impossibility. @app.route("/provider-not-supported") def provider_not_supported(): return flask.render_template("templates/provider-not-supported.html") ## # POST the donation request to the backend. In particular, # it uses the "POST /order" API. # # @return response object that will redirect the browser to # the fulfillment URL, where all the pay-logic will # happen. @app.route("/donate") def donate(): donation_receiver = expect_parameter("donation_receiver") donation_amount = expect_parameter("donation_amount") donation_donor = expect_parameter("donation_donor") payment_system = expect_parameter("payment_system") if payment_system != "taler": return flask.redirect(flask.url_for("provider_not_supported")) fulfillment_url = flask.url_for( "fulfillment", receiver=donation_receiver, _external=True ) order = dict( amount=donation_amount, extra=dict( donor=donation_donor, receiver=donation_receiver, amount=donation_amount ), fulfillment_url=fulfillment_url, summary="Donation to {}".format(donation_receiver), ) order_resp = backend_instanced_post(donation_receiver, "order", dict(order=order)) order_id = order_resp["order_id"] return flask.redirect( flask.url_for( "fulfillment", receiver=donation_receiver, order_id=order_id ) ) ## # This endpoint is used by the payment request page # to check if the payment has been completed via the QR code. @app.route("/check-status//") def check_status(instance, order_id): pay_params = dict(order_id=order_id) pay_status = backend_instanced_get(instance, "check-payment", pay_params) return flask.jsonify(paid=pay_status["paid"]) def get_qrcode_svg(data): factory = qrcode.image.svg.SvgImage img = qrcode.make(data, image_factory=factory) return lxml.etree.tostring(img.get_image()).decode("utf-8") ## # Serve the fulfillment page. # # @param receiver the donation receiver name, that should # correspond to a merchant instance. # @return after the wallet sent the payment, the final HTML "congrats" # page is returned; otherwise, the browser will be redirected # to a page that accepts the payment. @app.route("/donation/") def fulfillment(receiver): order_id = expect_parameter("order_id") pay_params = dict(order_id=order_id) pay_status = backend_instanced_get(receiver, "check-payment", pay_params) if pay_status.get("paid"): extra = pay_status["contract_terms"]["extra"] return flask.render_template( "templates/fulfillment.html", donation_receiver=extra["receiver"], donation_amount=extra["amount"], donation_donor=extra["donor"], order_id=order_id, currency=CURRENCY ) else: taler_pay_uri = pay_status["taler_pay_uri"] qrcode_svg = get_qrcode_svg(taler_pay_uri) check_status_url_enc = urllib.parse.quote( flask.url_for("check_status", instance=receiver, order_id=order_id) ) content = flask.render_template( "templates/request_payment.html", taler_pay_uri=taler_pay_uri, qrcode_svg=qrcode_svg, check_status_url_enc=check_status_url_enc ) headers = {"Taler": taler_pay_uri} resp = flask.Response(content, status=402, headers=headers) return resp # no pay_redirect but article not paid, this should never happen! err_abort(500, message="Internal error, invariant failed", json=pay_status)