##
# 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