From 4f77d05222845693131429b1fc6cff5884558248 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Sun, 11 Oct 2020 09:15:28 +0200 Subject: [PATCH] remove spplus module (#47539) --- eopayment/__init__.py | 17 ++- eopayment/spplus.py | 276 ------------------------------------------ tests/test_spplus.py | 47 ------- 3 files changed, 7 insertions(+), 333 deletions(-) delete mode 100644 eopayment/spplus.py delete mode 100644 tests/test_spplus.py diff --git a/eopayment/__init__.py b/eopayment/__init__.py index 6e27094..43eefb8 100644 --- a/eopayment/__init__.py +++ b/eopayment/__init__.py @@ -28,7 +28,7 @@ from .common import ( # noqa: F401 ResponseError, PaymentException, ) -__all__ = ['Payment', 'URL', 'HTML', 'FORM', 'SIPS', 'SYSTEMPAY', 'SPPLUS', +__all__ = ['Payment', 'URL', 'HTML', 'FORM', 'SIPS', 'SYSTEMPAY', 'TIPI', 'DUMMY', 'get_backend', 'RECEIVED', 'ACCEPTED', 'PAID', 'DENIED', 'CANCELED', 'CANCELLED', 'ERROR', 'WAITING', 'get_backends', 'PAYFIP_WS'] @@ -39,7 +39,6 @@ if six.PY3: SIPS = 'sips' SIPS2 = 'sips2' SYSTEMPAY = 'systempayv2' -SPPLUS = 'spplus' TIPI = 'tipi' DUMMY = 'dummy' OGONE = 'ogone' @@ -57,7 +56,7 @@ def get_backend(kind): module = importlib.import_module('.' + kind, package='eopayment') return module.Payment -__BACKENDS = [DUMMY, SIPS, SIPS2, SYSTEMPAY, SPPLUS, OGONE, PAYBOX, PAYZEN, +__BACKENDS = [DUMMY, SIPS, SIPS2, SYSTEMPAY, OGONE, PAYBOX, PAYZEN, TIPI, PAYFIP_WS, KEYWARE, MOLLIE] @@ -77,19 +76,17 @@ class Payment(object): Interface to credit card online payment servers of French banks. The only use case supported for now is a unique automatic payment. - >>> spplus_options = { \ - 'cle': '58 6d fc 9c 34 91 9b 86 3f fd 64 ' \ - '63 c9 13 4a 26 ba 29 74 1e c7 e9 80 79', \ - 'siret': '00000000000001-01', \ + >>> options = { + 'numcli': '12345', } - >>> p = Payment(kind=SPPLUS, options=spplus_options) + >>> p = Payment(kind=TIPI, options=options) >>> transaction_id, kind, data = p.request('10.00', email='bob@example.com') >>> print (transaction_id, kind, data) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE - ('...', 1, 'https://www.spplus.net/paiement/init.do?...') + ('...', 1, 'https://www.payfip.gov.fr/tpa/paiement.web?...') Supported backend of French banks are: - - sips, for BNP, Banque Populaire (before 2010), CCF, HSBC, Crédit + - SIPS 2.0, for BNP, Banque Populaire (before 2010), CCF, HSBC, Crédit Agricole, La Banque Postale, LCL, Société Générale and Crédit du Nord. - spplus for Caisse d'épargne diff --git a/eopayment/spplus.py b/eopayment/spplus.py deleted file mode 100644 index 1f527c7..0000000 --- a/eopayment/spplus.py +++ /dev/null @@ -1,276 +0,0 @@ -# -*- coding: utf-8 -*- -# eopayment - online payment library -# Copyright (C) 2011-2020 Entr'ouvert -# -# This program 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 of the License, or -# (at your option) any later version. -# -# This program 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from decimal import Decimal -import binascii -import hmac -import hashlib -from six.moves.urllib import parse as urlparse -from six.moves.urllib import parse as urllib -import string -import datetime as dt -import logging -import re -import warnings - -import Crypto.Cipher.DES -from .common import ( - PaymentCommon, URL, PaymentResponse, RECEIVED, ACCEPTED, - PAID, ERROR, ResponseError, force_byte -) - - -def N_(message): - return message - -__all__ = ['Payment'] - -KEY_DES_KEY = b'\x45\x1f\xba\x4f\x4c\x3f\xd4\x97' -IV = b'\x30\x78\x30\x62\x2c\x30\x78\x30' -REFERENCE = 'reference' -ETAT = 'etat' -SPCHECKOK = 'spcheckok' -LOGGER = logging.getLogger(__name__) -REFSFP = 'refsfp' - -# Pour un paiement comptant la chaine des états est: 1 -> 4 -> 10, seul l'état -# 10 garanti le paiement -SPPLUS_RESPONSE_CODES = { - '1': 'Autorisation de paiement acceptée', - '2': 'Autorisation de paiement refusée', - '4': 'Echéance du paiement acceptée et en attente de remise', - '5': 'Echéance du paiement refusée', - '6': 'Paiement par chèque accepté', - '8': 'Chèque encaissé', - '10': 'Paiement terminé', - '11': 'Echéance du paiement annulée par le commerçant', - '12': 'Abandon de l’internaute', - '15': 'Remboursement enregistré', - '16': 'Remboursement annulé', - '17': 'Remboursement accepté', - '20': 'Echéance du paiement avec un impayé', - '21': 'Echéance du paiement avec un impayé et en attente de validation des services SP PLUS', - '30': 'Echéance du paiement remisée', - '99': 'Paiement de test en production', -} - -VALID_STATE = ('1', '4', '10') -ACCEPTED_STATE = ('1', '4') -PAID_STATE = ('10',) -TEST_STATE = ('99',) - - -def decrypt_ntkey(ntkey): - key = binascii.unhexlify(force_byte(ntkey).replace(b' ', b'')) - return decrypt_key(key) - - -def decrypt_key(key): - CIPHER = Crypto.Cipher.DES.new(KEY_DES_KEY, Crypto.Cipher.DES.MODE_CBC, IV) - return CIPHER.decrypt(key) - - -def extract_values(query_string): - kvs = query_string.split('&') - result = [] - for kv in kvs: - k, v = kv.split('=', 1) - if k != 'hmac': - result.append(v) - return force_byte(''.join(result)) - - -def sign_ntkey_query(ntkey, query): - key = decrypt_ntkey(ntkey) - data_to_sign = extract_values(query) - return hmac.new(key[:20], data_to_sign, hashlib.sha1).hexdigest().upper() - -PAIEMENT_FIELDS = [ - 'siret', REFERENCE, 'langue', 'devise', 'montant', 'taxe', 'validite' -] - - -def sign_url_paiement(ntkey, query): - if '?' in query: - query = query[query.index('?') + 1:] - key = decrypt_ntkey(ntkey) - data = urlparse.parse_qs(query, True) - fields = [data.get(field, [''])[0] for field in PAIEMENT_FIELDS] - data_to_sign = ''.join(fields) - return hmac.new(key[:20], data_to_sign, hashlib.sha1).hexdigest().upper() - -ALPHANUM = string.ascii_letters + string.digits -SERVICE_URL = "https://www.spplus.net/paiement/init.do" - - -class Payment(PaymentCommon): - description = { - 'caption': "SPPlus payment service of French bank Caisse d'epargne", - 'parameters': [ - { - 'name': 'normal_return_url', - 'caption': N_('Normal return URL'), - 'default': '', - 'required': True, - }, - { - 'name': 'automatic_return_url', - 'caption': N_('Automatic return URL'), - 'required': False, - }, - { - 'name': 'cle', - 'caption': 'Secret key, a 40 digits hexadecimal number', - 'regexp': re.compile('^ *((?:[a-fA-F0-9] *){40}) *$') - }, - { - 'name': 'siret', - 'caption': ( - 'Siret of the entreprise augmented with the ' - 'site number, example: 00000000000001-01' - ), - 'regexp': re.compile(r'^ *(\d{14}-\d{2}) *$') - }, - { - 'name': 'langue', - 'caption': 'Language of the customers', - 'default': 'FR', - }, - { - 'name': 'taxe', - 'caption': 'Taxes', - 'default': '0.00' - }, - { - 'name': 'modalite', - 'caption': '1x, 2x, 3x, xx, nx (if multiple separated by "/")', - 'default': '1x', - }, - { - 'name': 'moyen', - 'caption': ( - 'AUR, AMX, CBS, CGA, ' - 'CHK, DIN, PRE (if multiple separate by "/")' - ), - 'default': 'CBS', - }, - ] - } - devise = '978' - - def request(self, amount, name=None, address=None, email=None, phone=None, - orderid=None, info1=None, info2=None, info3=None, - next_url=None, logger=LOGGER, **kwargs): - logger.debug('requesting spplus payment with montant %s email=%s' % (amount, email)) - reference = self.transaction_id(20, ALPHANUM, 'spplus', self.siret) - validite = dt.date.today()+dt.timedelta(days=1) - validite = validite.strftime('%d/%m/%Y') - fields = { - 'siret': self.siret, - 'devise': self.devise, - 'langue': self.langue, - 'taxe': self.taxe, - 'montant': str(Decimal(amount)), - REFERENCE: orderid or reference, - 'validite': validite, - 'version': '1', - 'modalite': self.modalite, - 'moyen': self.moyen, - } - if email: - fields['email'] = email - normal_return_url = self.normal_return_url - if next_url and not normal_return_url: - warnings.warn("passing next_url to request() is deprecated, " - "set normal_return_url in options", DeprecationWarning) - normal_return_url = next_url - if normal_return_url: - if ((not normal_return_url.startswith('http://') - and not normal_return_url.startswith('https://')) - or '?' in normal_return_url): - raise ValueError('normal_return_url must be an absolute URL without parameters') - fields['urlretour'] = normal_return_url - logger.debug('sending fields %s' % fields) - query = urllib.urlencode(fields) - url = '%s?%s&hmac=%s' % (SERVICE_URL, query, sign_url_paiement(self.cle, query)) - logger.debug('full url %s' % url) - return reference, URL, url - - def response(self, query_string, logger=LOGGER, **kwargs): - form = urlparse.parse_qs(query_string) - if not set(form) >= set([REFERENCE, ETAT, REFSFP]): - raise ResponseError('missing %s, %s or %s' % (REFERENCE, ETAT, REFSFP)) - for key, value in form.items(): - form[key] = value[0] - logger.debug('received query_string %s' % query_string) - logger.debug('parsed as %s' % form) - reference = form.get(REFERENCE) - bank_status = [] - signed = False - form[self.BANK_ID] = form.get(REFSFP) - etat = form.get('etat') - status = '%s: %s' % (etat, SPPLUS_RESPONSE_CODES.get(etat, 'Unknown code')) - logger.debug('status is %s', status) - bank_status.append(status) - if 'hmac' in form: - try: - signed_data, signature = query_string.rsplit('&', 1) - _, hmac = signature.split('=', 1) - logger.debug('got signature %s' % hmac) - computed_hmac = sign_ntkey_query(self.cle, signed_data) - logger.debug('computed signature %s' % computed_hmac) - signed = hmac == computed_hmac - if not signed: - bank_status.append('invalid signature') - except ValueError: - bank_status.append('invalid signature') - - test = False - if etat in PAID_STATE: - result = PAID - elif etat in ACCEPTED_STATE: - result = ACCEPTED - elif etat in VALID_STATE: - result = RECEIVED - elif etat in TEST_STATE: - result = RECEIVED # what else ? - test = True - else: - result = ERROR - - response = PaymentResponse( - result=result, - signed=signed, - bank_data=form, - order_id=reference, - transaction_id=form[self.BANK_ID], - bank_status=' - '.join(bank_status), - return_content=SPCHECKOK, - test=test) - return response - - -if __name__ == '__main__': - import sys - - ntkey = '58 6d fc 9c 34 91 9b 86 3f fd 64 63 c9 13 4a 26 ba 29 74 1e c7 e9 80 79' - if len(sys.argv) == 2: - print(sign_url_paiement(ntkey, sys.argv[1])) - print(sign_ntkey_query(ntkey, sys.argv[1])) - elif len(sys.argv) > 2: - print(sign_url_paiement(sys.argv[1], sys.argv[2])) - print(sign_ntkey_query(sys.argv[1], sys.argv[2])) diff --git a/tests/test_spplus.py b/tests/test_spplus.py deleted file mode 100644 index 3be0db5..0000000 --- a/tests/test_spplus.py +++ /dev/null @@ -1,47 +0,0 @@ -# eopayment - online payment library -# Copyright (C) 2011-2020 Entr'ouvert -# -# This program 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 of the License, or -# (at your option) any later version. -# -# This program 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 Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import eopayment.spplus as spplus -from eopayment import ResponseError, force_text - -import pytest - - -ntkey = b'58 6d fc 9c 34 91 9b 86 3f ' \ - b'fd 64 63 c9 13 4a 26 ba 29 74 1e c7 e9 80 79' - -tests = [ - ('x=coin', - 'c04f8266d6ae3ce37551cce996c751be4a95d10a'), - ('x=coin&y=toto', - 'ef008e02f8dbf5e70e83da416b0b3a345db203de'), - ('x=wdwd%20%3Fdfgfdgd&z=343&hmac=04233b78bb5aff332d920d4e89394f505ec58a2a', - '04233b78bb5aff332d920d4e89394f505ec58a2a') -] - - -def test_spplus(): - payment = spplus.Payment({'cle': ntkey, 'siret': '00000000000001-01'}) - - for query, result in tests: - assert spplus.sign_ntkey_query(ntkey, query).lower() == result - - with pytest.raises(ResponseError, match=r'missing reference, etat or refsfp'): - payment.response('foo=bar') - - # make sure key string and bytes representations are understood - spplus.decrypt_ntkey(force_text(ntkey)) - spplus.decrypt_ntkey(ntkey)