## # This file is part of TALER # (C) 2014, 2015, 2016, 2020 Taler Systems SA # # TALER is free software; you can redistribute it and/or modify it # under the terms of the GNU Affero General Public License as # published by the Free Software Foundation; either version 3, 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 General Public License for more # details. # # You should have received a copy of the GNU General Public # License along with TALER; see the file COPYING. If not, see # # # @author Marcello Stanisci # @author Florian Dold from functools import wraps import math import json import logging import hashlib import random import re import time import base64 import uuid from urllib.parse import urlparse, parse_qsl import django.contrib.auth import django.contrib.auth.views import django.contrib.auth.forms from django.db import transaction from django import forms from django.conf import settings from django.contrib.auth.decorators import login_required from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST, require_GET from django.utils.translation import gettext from django.views.decorators.http import require_http_methods from django.urls import reverse, get_script_prefix from django.contrib.auth.models import User from django.db.models import Q from django.http import JsonResponse, HttpResponse from django.shortcuts import render, redirect from django.core.exceptions import ObjectDoesNotExist from datetime import datetime from .models import BankAccount, BankTransaction, TalerWithdrawOperation from taler.util.amount import Amount, SignedAmount from taler.util.taler_error_codes import ErrorCode from http import HTTPStatus import qrcode import qrcode.image.svg import lxml from .schemas import ( HistoryParams, URLParamValidationError, AddIncomingData, JSONFieldException, InvalidSession, WithdrawHeadless, WithdrawHeadlessUri, ) LOGGER = logging.getLogger(__name__) ## # Constant value for the biggest number the bank handles. # This value is just equal to the biggest number that JavaScript # can handle (because of the wallet). UINT64_MAX = (2 ** 64) - 1 ## # Decorator function that authenticates requests by fetching # the credentials over the HTTP requests headers. # # @param view_func function that will be called after the # authentication, and that will usually serve the requested # endpoint. # @return FIXME. def login_via_headers(view_func): def _decorator(request, *args, **kwargs): user_account = basic_auth(request) if not user_account: raise LoginFailed("authentication failed") return view_func(request, user_account, *args, **kwargs) return wraps(view_func)(_decorator) def allow_origin_star(view_func): def _decorator(request, *args, **kwargs): response = view_func(request, *args, **kwargs) response["Access-Control-Allow-Origin"] = "*" return response return wraps(view_func)(_decorator) class PaytoParse: def __init__(self, payto_uri): parsed_payto = urlparse(payto_uri) if parsed_payto.scheme != "payto": raise Exception("Bad Payto URI: '%s'" % payto_uri) path_as_list = parsed_payto.path.split("/") if len(path_as_list) == 0: raise Exception("No account/user name found: '%s'" % payto_uri) self.account = path_as_list[-1] params = dict(parse_qsl(parsed_payto.query)) self.subject = params.get("subject") self.amount = Amount.parse(params.get("amount")) ## # Exception raised upon failing login. # class LoginFailed(Exception): def __init__(self, msg): super(LoginFailed, self).__init__(msg) self.hint = "Wrong password given" self.http_status_code = HTTPStatus.UNAUTHORIZED self.taler_error_code = ErrorCode.BANK_LOGIN_FAILED class InvalidInputData(Exception): def __init__(self, msg): super(InvalidInputData, self).__init__(msg) self.hint = msg # should mention the picked username self.http_status_code = HTTPStatus.BAD_REQUEST self.taler_error_code = ErrorCode.BANK_SOFT_EXCEPTION class UsernameUnavailable(Exception): def __init__(self, msg): super(UsernameUnavailable, self).__init__(msg) self.hint = msg # should mention the picked username self.http_status_code = HTTPStatus.NOT_ACCEPTABLE self.taler_error_code = ErrorCode.BANK_SOFT_EXCEPTION ## # Exception raised when the public history from # a ordinary user account is tried to be accessed. class PrivateAccountException(Exception): def __init__(self, msg): super(PrivateAccountException, self).__init__(msg) self.hint = "Cannot show history from private persons accounts" self.http_status_code = HTTPStatus.FORBIDDEN ## # Exception raised when some financial operation goes # beyond the limit threshold. class DebitLimitException(Exception): def __init__(self, msg): super(DebitLimitException, self).__init__(msg) self.hint = "Payment aborted for insufficient credit" self.http_status_code = HTTPStatus.FORBIDDEN self.taler_error_code = ErrorCode.BANK_UNALLOWED_DEBIT ## # Exception raised when some financial operation is # attempted and both parties are the same account number. # class SameAccountException(Exception): def __init__(self, msg): super(SameAccountException, self).__init__(msg) self.hint = "Cannot send payment to oneself." self.http_status_code = HTTPStatus.BAD_REQUEST self.taler_error_code = ErrorCode.BANK_SAME_ACCOUNT class UnhandledException(Exception): def __init__(self, msg="Unhandled exception happened!"): super(UnhandledException, self).__init__(msg) self.hint = msg self.http_status_code = HTTPStatus.INTERNAL_SERVER_ERROR self.taler_error_code = ErrorCode.BANK_UNMANAGED_EXCEPTION ## # The authentication for users to log in the bank. # class TalerAuthenticationForm(django.contrib.auth.forms.AuthenticationForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["username"].widget.attrs["autofocus"] = True self.fields["username"].widget.attrs["placeholder"] = "Username" self.fields["password"].widget.attrs["placeholder"] = "Password" ## # Return a empty response. Used in "/favicon.ico" requests. # def ignore(request): del request return HttpResponse() ## # Decode body, when it is expected to be UTF-8. # # @param request the HTTP request being served. # @return the body as string. def decode_body(request): return request.body.decode("utf-8") ## # Get a flag from the session and clear it. # # @param request the HTTP request being served. # @param name name of the session value that should be retrieved. # @return the value, if found; otherwise False. def get_session_flag(request, name): if name in request.session: ret = request.session[name] del request.session[name] return ret return False ## # A session "hint" is a tuple indicating whether the # message is for a failure or a success, and containing # the message itself. # # @param request the HTTP request being served. # @param name hint name # @return the hint (a "null" one if none was found) def get_session_hint(request): ret = True, "" if "hint" in request.session: ret = request.session["hint"] del request.session["hint"] return ret def set_session_hint(request, success, hint): if "hint" in request.session: LOGGER.warning("Overriding a non consumed hint") del request.session["hint"] request.session["hint"] = success, hint ## # Build the list containing all the predefined accounts; the # list contains, for example, the exchange, the bank itself, and # all the public accounts (like GNUnet / Tor / FSF / ..) def predefined_accounts_list(): account = 2 ret = [] for i in settings.TALER_PREDEFINED_ACCOUNTS[1:]: ret.append((account, "%s (#%d)" % (i, account))) account += 1 return ret ## # Thanks to [1], this class provides a dropdown menu that # can be used within a