Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions easebuzz/api.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import frappe
import requests


@frappe.whitelist(allow_guest=True)
def webhook_handler(**kwargs):
try:
data = frappe.parse_json(kwargs)
controller = frappe.get_last_doc("Easebuzz Settings")
if data.get("udf3") == "webform":
controller.handle_response_web_form(data)
else:
controller.handle_response(data)
url = "https://fees.walnutedu.in/index.php/payment/easebuzz_webhook_callback"
r = requests.post(url, json = data)
return controller.handle_response_web_form(data)
controller.handle_response(data)
url = "https://fees.walnutedu.in/index.php/payment/easebuzz_webhook_callback"
requests.post(url, json=data, timeout=(5, 30))
return {"message": "ok"}
except Exception as e:
frappe.logger("easebuzz").exception(e)
frappe.local.response["http_status_code"] = 400
frappe.logger("easebuzz").exception(e)
return {"message": "invalid callback"}
37 changes: 30 additions & 7 deletions easebuzz/easebuzz/doctype/easebuzz_settings/easebuzz_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
# For license information, please see license.txt

import json

import frappe
from frappe import _
from frappe.model.document import Document
from easebuzz.easebuzz.utils import payment as easebuzz_payment
from easebuzz.easebuzz.utils.easebuzz_payment_gateway import Easebuzz
from frappe.utils import call_hook_method
from frappe.utils.data import cint, flt
Expand Down Expand Up @@ -169,19 +172,36 @@ def get_settings(self, data):

return settings

def validate_gateway_response(self, data):
salt = self.get_password(fieldname="salt", raise_exception=False)
response = easebuzz_payment.easebuzzResponse(data, salt)
if response.get("status") != 1:
frappe.throw(_("Invalid Easebuzz callback"))
return response.get("data", {})

def _reject_replay(self, doc, transaction_id):
existing_transaction_id = getattr(doc, "transaction_id", None)
if existing_transaction_id and existing_transaction_id != transaction_id:
frappe.throw(_("Conflicting transaction callback received"))
if existing_transaction_id == transaction_id:
return True
return False

def handle_response(self, data):
data = self.validate_gateway_response(data)
payment_request_doctype = data.get("udf1")
payment_request_docname = data.get("udf2")
status = data.get("status")
transaction_id = data.get("txnid")
if status == "success":
if frappe.db.exists(payment_request_doctype, payment_request_docname):
frappe.msgprint("Payment Request exists")
payment_request = frappe.get_doc(
payment_request_doctype,
payment_request_docname,
ignore_permissions=True,
)
if self._reject_replay(payment_request, transaction_id):
return {"message": "Duplicate callback ignored"}
frappe.db.set_value(
payment_request_doctype,
payment_request_docname,
Expand All @@ -190,22 +210,23 @@ def handle_response(self, data):
)
payment_request.on_payment_authorized(status="Completed")
return {"message": "Payment Successful"}
else:
frappe.msgprint("Payment Request does not exist, Invalid Request")
frappe.throw(_("Payment Request does not exist, Invalid Request"))

def handle_response_web_form(self, data):
data = self.validate_gateway_response(data)
doctype = data.get("udf1")
docname = data.get("udf2")
docname = docname.replace("@", "(").replace("#", ")")
status = data.get("status")
transaction_id = data.get("txnid")
if status == "success":
if frappe.db.exists(doctype, docname):
frappe.db.set_value(doctype, docname, "transaction_id", transaction_id)
doc = frappe.get_doc(doctype, docname, ignore_permissions=True)
if self._reject_replay(doc, transaction_id):
return {"message": "Duplicate callback ignored"}
frappe.db.set_value(doctype, docname, "transaction_id", transaction_id)
return doc.validate_payment(data)
else:
frappe.log_error(f"{doctype} {docname} does not exist")
frappe.throw(_("{0} {1} does not exist").format(doctype, docname))

def initiateRefund(self, data):
amounts = float(data.get("amount"))
Expand All @@ -223,8 +244,10 @@ def initiateRefund(self, data):
return response


@frappe.whitelist(allow_guest=True)
@frappe.whitelist()
def get_merchant_key():
if frappe.session.user == "Guest":
frappe.throw(_("Authentication required"), frappe.PermissionError)
controller = frappe.get_doc("Easebuzz Settings")
return controller.merchant_key

Expand Down
34 changes: 34 additions & 0 deletions easebuzz/easebuzz/utils/http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import time

import requests
from requests import RequestException

DEFAULT_CONNECT_TIMEOUT = 5
DEFAULT_READ_TIMEOUT = 30
DEFAULT_MAX_ATTEMPTS = 3
DEFAULT_BACKOFF_SECONDS = 1
DEFAULT_STATUS_RETRY_CODES = {408, 429, 500, 502, 503, 504}


def post_with_retries(url, data, timeout=None, max_attempts=DEFAULT_MAX_ATTEMPTS, backoff_seconds=DEFAULT_BACKOFF_SECONDS, retry_status_codes=None):
timeout = timeout or (DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT)
retry_status_codes = retry_status_codes or DEFAULT_STATUS_RETRY_CODES
last_error = None

for attempt in range(1, max_attempts + 1):
try:
response = requests.post(url, data, timeout=timeout)
if response.status_code not in retry_status_codes or attempt == max_attempts:
return response
last_error = requests.HTTPError(f"Retryable HTTP status: {response.status_code}", response=response)
except (requests.Timeout, requests.ConnectionError) as exc:
last_error = exc
if attempt == max_attempts:
raise
except RequestException:
raise

time.sleep(backoff_seconds * (2 ** (attempt - 1)))

if last_error:
raise last_error
5 changes: 3 additions & 2 deletions easebuzz/easebuzz/utils/payment.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from hashlib import sha512

import requests
import json
import re
import traceback

from .http import post_with_retries

# import webbrowser

'''
Expand Down Expand Up @@ -538,7 +539,7 @@ def _pay(params_array, salt_key, url):
params_array['hash'] = hash_key

# requests call for initiate pay link
request_result = requests.post(url + 'payment/initiateLink', params_array)
request_result = post_with_retries(url + 'payment/initiateLink', params_array)
result = json.loads(request_result.content)
# print(params_array)
if result['status'] == 1:
Expand Down
5 changes: 3 additions & 2 deletions easebuzz/easebuzz/utils/payout.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from hashlib import sha512

import requests
import json
import re
import traceback

from .http import post_with_retries

'''
*
* get_payout_details_by_date method payout amount and dispay the payout response.
Expand Down Expand Up @@ -342,7 +343,7 @@ def _payoutPayment(params_array, salt_key, url):
params_array['hash'] = hash_key

# requests call for retrive all payout
request_result = requests.post(url + 'payout/v1/retrieve', params_array)
request_result = post_with_retries(url + 'payout/v1/retrieve', params_array)
result = json.loads(request_result.content)

return result
Expand Down
5 changes: 3 additions & 2 deletions easebuzz/easebuzz/utils/refund.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from hashlib import sha512

import requests
import json
import re
import traceback

from .http import post_with_retries

'''
*
* initiate_refund method refund amount and call dispay the refund page.
Expand Down Expand Up @@ -387,7 +388,7 @@ def _refundPayment(params_array, salt_key, url):
params_array['hash'] = hash_key

# requests call for retrive transaction
request_result = requests.post(url + 'transaction/v1/refund', params_array)
request_result = post_with_retries(url + 'transaction/v1/refund', params_array)

return json.loads(request_result.content)

Expand Down
5 changes: 3 additions & 2 deletions easebuzz/easebuzz/utils/transaction.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from hashlib import sha512

import requests
import json
import re
import traceback

from .http import post_with_retries

'''
*
* get_transaction_details method use for transaction
Expand Down Expand Up @@ -376,7 +377,7 @@ def _getTransaction(params_array, salt_key, url):
params_array['hash'] = hash_key

# requests call for retrive transaction
request_result = requests.post(url + 'transaction/v1/retrieve', params_array)
request_result = post_with_retries(url + 'transaction/v1/retrieve', params_array)

return json.loads(request_result.content)

Expand Down
5 changes: 3 additions & 2 deletions easebuzz/easebuzz/utils/transaction_date.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from hashlib import sha512

import requests
import json
import re
import traceback

from .http import post_with_retries

'''
*
* get_transactions_by_date method use for transaction based on date
Expand Down Expand Up @@ -296,7 +297,7 @@ def _getDateTransaction(params_array, salt_key, url):
params_array['hash'] = hash_key

# requests call for retrive transaction based on date
request_result = requests.post(url + 'transaction/v1/retrieve/date', params_array)
request_result = post_with_retries(url + 'transaction/v1/retrieve/date', params_array)
temp_result = json.loads(request_result.content)

return temp_result
Expand Down
9 changes: 2 additions & 7 deletions easebuzz/www/easebuzz/success.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,9 @@

def get_context(context):
try:
if frappe.session.user == "Guest":
frappe.set_user("Administrator")
data = frappe.form_dict
handle_data(data)
frappe.set_user("Guest")
else:
handle_data(data)
handle_data(frappe.form_dict)
except Exception as e:
frappe.local.response["http_status_code"] = 400
frappe.logger("easebuzz").exception(e)


Expand Down
47 changes: 47 additions & 0 deletions tests_security_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from hashlib import sha512
from unittest.mock import patch

import requests

from easebuzz.easebuzz.utils import payment
from easebuzz.easebuzz.utils.http import post_with_retries


def build_callback_payload(salt="secret"):
data = {
"status": "success",
"txnid": "txn-1",
"amount": "100.00",
"productinfo": "Order",
"firstname": "Test",
"email": "test@example.com",
"phone": "9999999999",
"udf1": "Payment Request",
"udf2": "PR-0001",
"udf3": "",
"udf4": "",
"udf5": "",
"key": "merchant",
"surl": "https://example.com/success",
"furl": "https://example.com/failure",
}
seq = "udf10|udf9|udf8|udf7|udf6|udf5|udf4|udf3|udf2|udf1|email|firstname|productinfo|amount|txnid|key".split("|")
reverse = salt + "|" + data["status"]
for field in seq:
reverse += "|" + str(data.get(field, ""))
data["hash"] = sha512(reverse.encode("utf-8")).hexdigest().lower()
return data


payload = build_callback_payload()
assert payment.easebuzzResponse(payload, "secret")["status"] == 1

with patch("easebuzz.easebuzz.utils.http.requests.post") as mocked:
mocked.side_effect = [
requests.Timeout(),
type("Resp", (), {"status_code": 200, "content": b"{}"})(),
]
response = post_with_retries("https://example.com", {"a": 1}, max_attempts=2, backoff_seconds=0)
assert response.status_code == 200

print("security smoke checks passed")