Compare commits

..

58 Commits

Author SHA1 Message Date
a97d52bc59 update user agent
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 47s
2025-01-10 09:31:04 +05:00
80eb74c65c add jq to keep alive
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 1m20s
2025-01-10 08:24:12 +05:00
470df69c56 auto build
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 1m50s
2025-01-10 08:19:47 +05:00
c2ab2ea996 add the cookie server script to docker.... 2025-01-10 08:18:24 +05:00
37dfe7e261 update docs 2025-01-10 08:10:59 +05:00
a72f22b9a4 build cookie server 2025-01-10 07:59:41 +05:00
0e60f139ef added support for cookie server 2025-01-10 07:52:09 +05:00
618fc9371d no more cookie hunting, totp generated automatically 2025-01-10 07:38:28 +05:00
9decf2ed2b no more cookie hunting 2025-01-10 07:37:41 +05:00
ee57150807 Add support for profile selection 2025-01-10 07:36:06 +05:00
1264d92a04 cookie api works, but need to select profile 2025-01-10 06:35:58 +05:00
03ee1dec32 print json, nicely ask mib ismath to give me real api 2025-01-10 05:48:44 +05:00
e0684cb11e get cookie automatically 2025-01-10 05:26:58 +05:00
ab64ce071e dont know how jq got removed 2024-12-20 16:56:03 +05:00
af0a122d2b ignore negative transacations 2024-12-20 16:51:33 +05:00
244f63e80d noooooooo how did this happen 2024-12-09 10:05:41 +05:00
95235e39e8 f 2024-12-09 09:13:02 +05:00
f96f787861 til that there is a tab for cookies 2024-12-09 09:12:10 +05:00
bfe7238877 docs update - better errors 2024-12-09 08:59:48 +05:00
61384f39bf update docs 2024-12-09 08:58:32 +05:00
5fa2bf780d stop giving false json 2024-12-09 08:45:34 +05:00
867b32e45e update docs for cleaner transaction output 2024-12-09 08:25:51 +05:00
c91a4c06fd clean up transaction output 2024-12-09 08:09:22 +05:00
155e56d7b6 added date time to keepalive 2024-12-09 00:37:19 +05:00
203319aba9 add detailed tx info to output 2024-12-09 00:19:00 +05:00
098e268001 custom sleep timer for keep alinve 2024-12-08 23:52:18 +05:00
3dd6680a9f docs update 2024-10-22 02:03:51 +05:00
bc897f90bd docs update 2024-10-22 01:53:50 +05:00
2b05b9100c docs update 2024-10-22 01:52:46 +05:00
1f20accdfb docs update 2024-10-22 01:45:20 +05:00
227b5e9e58 lmao.. varaible account number 2024-10-22 01:44:05 +05:00
f43ec48fb1 docs update 2024-10-22 01:42:07 +05:00
6759057875 docs update 2024-10-22 01:36:16 +05:00
b4190d3f38 Add getting started 2024-10-22 01:22:45 +05:00
0b3076db38 cleaner cookie 2024-10-22 00:42:17 +05:00
121dc00125 i will not understand 2024-10-22 00:37:58 +05:00
3e8dbb5931 reduce default time delta and allowed to be modified with variable 2024-10-22 00:37:19 +05:00
aa1d8e7dcf Update fetchname.sh to docker 2024-10-20 02:03:02 +05:00
46edfd8c94 add support for 0. decimal values 2024-10-20 01:57:27 +05:00
e44aba9931 Update Readme title 2024-10-20 01:41:57 +05:00
a5ea55d725 Update Readme with exmaple reponse 2024-10-20 01:39:32 +05:00
48bf3b864b Update Readme 2024-10-20 01:33:33 +05:00
16a226393f attempt to use Name first, if name fails, then use Account number 2024-10-20 01:29:33 +05:00
6e0fc175d9 implment fetching name from number 2024-10-20 01:10:48 +05:00
5c8c0fc99e add fetchname 2024-10-20 00:59:53 +05:00
51409fdc14 cleaner cookie 2024-10-20 00:59:28 +05:00
02c1ccc677 ignore __pycache__/ 2024-10-19 04:23:52 +05:00
73eee65323 use gunicorn for prod api 2024-10-19 04:23:11 +05:00
3dda493315 load more transactions 2024-10-19 03:47:44 +05:00
2511eacd2b increase delay to keep alive 2024-10-19 03:47:13 +05:00
ab400a5b65 add example request 2024-10-19 03:11:52 +05:00
35efd063c2 add deployment compose 2024-10-19 03:03:01 +05:00
6bae62ddb6 use shebang 2024-10-19 03:00:53 +05:00
b2c319d4ec fix docker log 2024-10-19 03:00:25 +05:00
f2594a2449 run on 0.0.0.0, added python shebang 2024-10-19 02:59:48 +05:00
77ac4dca59 that was a mistake, just stay quite and use whatever 2024-10-19 02:42:57 +05:00
f29417f9e9 source .env if .env exists, else use env (for docker) 2024-10-19 02:35:38 +05:00
922f07763a add debug mode var 2024-10-19 02:34:50 +05:00
17 changed files with 546 additions and 387 deletions

View File

@@ -1,8 +1,10 @@
FROM python:3.9.20-slim-bookworm FROM python:3.9.20-slim-bookworm
RUN pip install python-dotenv flask RUN pip install python-dotenv flask gunicorn
RUN apt-get update && apt-get install -y curl jq && apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
COPY api.py tx.sh . COPY api.py tx.sh fetchname.sh .
RUN chmod +x tx.sh api.py fetchname.sh
CMD python /app/api.py CMD gunicorn --workers 4 --bind 0.0.0.0:5000 api:app

View File

@@ -9,3 +9,8 @@ services:
context: ../../ context: ../../
dockerfile: .build/prod/api.Dockerfile dockerfile: .build/prod/api.Dockerfile
image: git.shihaam.dev/shihaam/mib-payment-verify/api image: git.shihaam.dev/shihaam/mib-payment-verify/api
cookieserver:
build:
context: ../../
dockerfile: .build/prod/cookieserver.Dockerfile
image: git.shihaam.dev/shihaam/mib-payment-verify/cookieserver

View File

@@ -0,0 +1,10 @@
FROM python:3.9.20-slim-bookworm
RUN pip install beautifulsoup4 pyotp Flask python-dotenv requests
RUN apt-get update && apt-get install -y curl jq && apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY getcookie.py .
CMD python getcookie.py

View File

@@ -1,10 +1,10 @@
FROM debian:bullseye-slim FROM debian:bullseye-slim
RUN apt-get update && apt-get install -y curl && apt-get clean && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y curl jq && apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
COPY keepalive.sh . COPY keepalive.sh .
RUN chmod +x keepalive.sh RUN chmod +x keepalive.sh
CMD /app/keepalive.sh CMD ./keepalive.sh

View File

@@ -2,7 +2,16 @@ USERNAME=
PASSWORD= PASSWORD=
TOTP_SEED= TOTP_SEED=
PROFILE_ID=
PROFILE_TYPE=
ACCOUNT_NUMBER=
#Cookie COOKIE_SERVER=http://cookieserver:5000
QL_0=
IBSID= APP_DEBUG=true
## This valu is for time difference between request json and actual time of the transacation (+ or -) in mins, 5 or 10 is probably a good value
TIME_DIFF_LIMIT=7
## Sleep is the delay between keepalive request for extending sesssion, somewhere between 30 and 150 should be ok
SLEEP=45

View File

@@ -0,0 +1,37 @@
name: Build and Push Docker Images
on:
push:
branches:
- main
- master
pull_request:
branches:
- main
- master
jobs:
docker:
name: Build and Push Docker Images
runs-on: builder
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Build Docker images
working-directory: .build/prod
run: docker compose build
- name: Login to Docker Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
registry: ${{ vars.DOCKER_REGISTRY_URL }}
username: ${{ vars.DOCKER_REGISTRY_USER }}
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
- name: Push Docker images
if: github.event_name != 'pull_request'
working-directory: .build/prod
run: docker compose push

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
env/* env/*
.env .env
__pycache__/

118
README.md Normal file
View File

@@ -0,0 +1,118 @@
### Maldives Islamic Bank Payment Verification API
- This api will allow you to match customers payment and your MIB account history, it will match with transaction time, account owner name and amount.
- Issue with using account number name is that, MIB to MIB transaction sometimes have MIBs made-up name for the account owner name, This is challenging, so work around that, I added option to send customers account number to fetch the made-up name and match with it.
- BML (Farava) transations works perfectly with just the name!
- It uses cookie from webssion to fetch everything, it also has a keepalive service so websession does not expire after 5mins.
## How to deploy/get started (with docker/podman)
1. Login to MIB web [https://faisanet.mib.com.mv/](https://faisanet.mib.com.mv/) and get the your profile id and type:
![select_profile.jpg](select_profile.jpg)
2. copy .env.example to .env and fill your MIB Username, Password and TOTP Seed, `PROFILE_ID` and `PROFILE_TYPE` needs to be filled with contents from step 1, Make sure to fill `ACCOUNT_NUMBER` with your account number.
3. Create `compose.yml` with the follwing contents:
```yaml
services:
keepalive:
env_file: .env
image: git.shihaam.dev/shihaam/mib-payment-verify/keepalive
cookieserver:
env_file: .env
image: git.shihaam.dev/shihaam/mib-payment-verify/cookieserver
api:
env_file: .env
image: git.shihaam.dev/shihaam/mib-payment-verify/api
ports:
- 5000:5000
```
4. Run `docker compose up -d` and that is all.
## Using the API with curl examples:
1. With both benefName and accountNo:
```bash
curl -X POST http://localhost:5000/verify-payment \
-H "Content-Type: application/json" \
-d '{
"benefName": "ABDLA.MAJUDHU AHMED",
"accountNo": "90103101178641000",
"absAmount": "100",
"time": "2024-10-16 16:08"
}'
```
In this request, it will first try to verify with benfName, if it fails, then it will try with accountNo.
2. With only benefName:
```bash
curl -X POST http://localhost:5000/verify-payment \
-H "Content-Type: application/json" \
-d '{
"benefName": "ABDLA.MAJUDHU AHMED",
"absAmount": "100",
"time": "2024-10-16 16:08"
}'
```
3. With only accountNo:
```bash
curl -X POST http://localhost:5000/verify-payment \
-H "Content-Type: application/json" \
-d '{
"accountNo": "90103101178641000",
"absAmount": "100",
"time": "2024-10-16 16:08"
}'
```
## Exmaple Succcess responses:
1. Both benefName and accountNo or only benefName provided
```json
{
"message": "Payment verified using beneficiary name",
"success": true,
"transaction": {
"ref": "MALBIPS20241208190231 HBTLGAKCNUS",
"sourceBank": "BML",
"trxDate": "2024-12-09 00:02:36"
}
}
```
2. Both benefName and accountNo or only accountNo provided, but failed with benefName
```json
{
"message": "Payment verified using account number",
"success": true,
"transaction": {
"ref": "1-77722758-34140519-1",
"sourceBank": "MIB",
"trxDate": "2024-12-05 12:57:32"
}
}
```
## Example Failed response:
1. If transaction is not found with the provided details.
```json
{
"message": "Transaction not found, contact support",
"success": false
}
```
2. If session times out
```json
{
"message": "Error from tx.sh: Session Expired",
"success": false
}
```
3. If 500 from server
```json
{
"message": "Internal server error"
"success": false,
}
```

149
api.py Normal file → Executable file
View File

@@ -1,34 +1,112 @@
#!/usr/bin/env python3
from decimal import Decimal
import os
from dotenv import load_dotenv
from flask import Flask, request, jsonify from flask import Flask, request, jsonify
import subprocess import subprocess
import json import json
from datetime import datetime, timedelta from datetime import datetime, timedelta
load_dotenv() # This will load environment variables from a .env file if it exists
TIME_DIFF_LIMIT = int(os.getenv('TIME_DIFF_LIMIT', 2)) # Default to 2mins if not set
app = Flask(__name__) app = Flask(__name__)
def fetch_account_name(account_no):
try:
result = subprocess.run(['./fetchname.sh', account_no], capture_output=True, text=True, check=True)
response = json.loads(result.stdout)
if response.get('success'):
return response.get('accountName')
except (subprocess.CalledProcessError, json.JSONDecodeError) as e:
app.logger.error(f"Error fetching account name: {str(e)}")
return None
def get_transaction_details(tx_data):
"""Extract reference and sourceBank based on transaction type"""
descr1 = tx_data.get('descr1', '')
if descr1 == "Favara Credit":
reference = tx_data.get('descr2', '')
# Extract sourceBank from descr3 (part before the first '-')
descr3 = tx_data.get('descr3', '')
sourceBank = descr3.split(' - ')[0] if ' - ' in descr3 else ''
elif descr1 == "IB Acc to Acc":
reference = tx_data.get('trxNumber', '')
sourceBank = "MIB"
else:
reference = tx_data.get('trxNumber', '')
sourceBank = ""
return {
"ref": reference,
"trxDate": tx_data.get('trxDate', ''),
"sourceBank": sourceBank
}
def verify_transaction(benef_name, abs_amount, request_time, tx_data_list):
for tx_data in tx_data_list:
required_keys = ['trxDate', 'benefName', 'absAmount']
if not all(key in tx_data for key in required_keys):
continue # Skip this transaction if it's missing required keys
try:
tx_time = datetime.strptime(tx_data['trxDate'], "%Y-%m-%d %H:%M:%S")
time_diff = abs(tx_time - request_time)
tx_benef_name = tx_data['benefName'].strip().lower()
if (tx_benef_name == benef_name.strip().lower() and
compare_amounts(tx_data['absAmount'], abs_amount) and
time_diff <= timedelta(minutes=TIME_DIFF_LIMIT)):
return True, tx_data
except ValueError as e:
app.logger.error(f"Error processing transaction: {str(e)}")
return False, None
def compare_amounts(amount1, amount2):
"""Compare two amount strings as Decimal objects."""
return Decimal(amount1) == Decimal(amount2)
@app.errorhandler(400)
def bad_request(error):
return jsonify({"success": False, "message": "Invalid request: Malformed JSON"}), 400
@app.errorhandler(500)
def internal_error(error):
return jsonify({"success": False, "message": "Internal server error"}), 500
@app.route('/verify-payment', methods=['POST']) @app.route('/verify-payment', methods=['POST'])
def verify_payment(): def verify_payment():
# Get data from request try:
data = request.json if not request.is_json:
benef_name = data.get('benefName', '').strip().lower() # Normalize input name return jsonify({"success": False, "message": "Request must be JSON"}), 400
data = request.get_json()
if data is None:
return jsonify({"success": False, "message": "Invalid JSON format"}), 400
benef_name = data.get('benefName', '').strip() if data.get('benefName') else ''
account_no = data.get('accountNo')
abs_amount = data.get('absAmount') abs_amount = data.get('absAmount')
time_str = data.get('time') time_str = data.get('time')
# Validate input if not all([abs_amount, time_str]):
if not all([benef_name, abs_amount, time_str]):
return jsonify({"success": False, "message": "Missing required parameters"}), 400 return jsonify({"success": False, "message": "Missing required parameters"}), 400
if not benef_name and not account_no:
return jsonify({"success": False, "message": "Either benefName or accountNo must be provided"}), 400
try: try:
# Parse the input time
request_time = datetime.strptime(time_str, "%Y-%m-%d %H:%M") request_time = datetime.strptime(time_str, "%Y-%m-%d %H:%M")
except ValueError: except ValueError:
return jsonify({"success": False, "message": "Invalid time format"}), 400 return jsonify({"success": False, "message": "Invalid time format"}), 400
# Execute tx.sh and get the output
try: try:
result = subprocess.run(['./tx.sh'], capture_output=True, text=True, check=True) result = subprocess.run(['./tx.sh'], capture_output=True, text=True, check=True)
tx_response = json.loads(result.stdout) tx_response = json.loads(result.stdout)
# Debug: Print the entire tx_response
app.logger.debug(f"tx_response: {json.dumps(tx_response, indent=2)}") app.logger.debug(f"tx_response: {json.dumps(tx_response, indent=2)}")
if not tx_response.get('success'): if not tx_response.get('success'):
@@ -39,35 +117,40 @@ def verify_payment():
return jsonify({"success": False, "message": "No transaction data found"}), 404 return jsonify({"success": False, "message": "No transaction data found"}), 404
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
return jsonify({"success": False, "message": f"Error executing tx.sh: {str(e)}", "stderr": e.stderr}), 500 return jsonify({"success": False, "message": f"Error executing tx.sh: {str(e)}"}), 500
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
return jsonify({"success": False, "message": f"Error parsing tx.sh output: {str(e)}", "output": result.stdout}), 500 return jsonify({"success": False, "message": f"Error parsing tx.sh output: {str(e)}"}), 500
# Check transactions # First, try to verify with benefName if provided
for tx_data in tx_data_list: if benef_name:
# Check if the required keys exist in tx_data verified, tx_data = verify_transaction(benef_name, abs_amount, request_time, tx_data_list)
required_keys = ['trxDate', 'benefName', 'absAmount'] if verified:
missing_keys = [key for key in required_keys if key not in tx_data] return jsonify({
if missing_keys: "success": True,
continue # Skip this transaction if it's missing required keys "message": "Payment verified using beneficiary name",
"transaction": get_transaction_details(tx_data)
})
try: # If benefName verification failed or wasn't provided, try with accountNo
tx_time = datetime.strptime(tx_data['trxDate'], "%Y-%m-%d %H:%M:%S") if account_no:
time_diff = abs(tx_time - request_time) fetched_name = fetch_account_name(account_no)
if fetched_name:
verified, tx_data = verify_transaction(fetched_name, abs_amount, request_time, tx_data_list)
if verified:
return jsonify({
"success": True,
"message": "Payment verified using account number",
"transaction": get_transaction_details(tx_data)
})
# Normalize transaction beneficiary name for comparison # If both verifications fail
tx_benef_name = tx_data['benefName'].strip().lower()
if (tx_benef_name == benef_name and
str(tx_data['absAmount']) == str(abs_amount) and
time_diff <= timedelta(minutes=30)):
return jsonify({"success": True, "message": "Payment verified"})
except ValueError as e:
app.logger.error(f"Error processing transaction: {str(e)}")
continue # Skip this transaction if there's an error processing it
# If we've checked all transactions and found no match
return jsonify({"success": False, "message": "Transaction not found, contact support"}) return jsonify({"success": False, "message": "Transaction not found, contact support"})
except Exception as e:
app.logger.error(f"Unexpected error: {str(e)}")
return jsonify({"success": False, "message": "Internal server error"}), 500
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True) debug_mode = os.getenv('APP_DEBUG', 'False').lower() in ('true', '1', 't')
port = int(os.getenv('PORT', 5000))
app.run(host='0.0.0.0', port=port, debug=debug_mode)

316
auth.js
View File

@@ -1,316 +0,0 @@
let displayError = function (msg) {
$("#error-display").text(msg).show('slow');
};
let hideError = function (msg) {
$("#error-display").hide('slow');
};
window.location.hash = '';
let hideProgress = function () {
$("#login-submit").removeClass('button-loader');
};
let showProgress = function () {
$("#login-submit").addClass('button-loader');
};
let pushDashboardToHistory = function () {
let defaultLandingPage = "https://faisanet.mib.com.mv/dashboard";
window.history.pushState({}, '', defaultLandingPage)
};
let getLandingPage = function (landingPageOption) {
let landingPage = "https://faisanet.mib.com.mv/profiles";
switch (landingPageOption) {
case "0":
{
//individual account login
landingPage = "https://faisanet.mib.com.mv/dashboard";
break;
}
case "1":
{
//profiles
landingPage = "https://faisanet.mib.com.mv/accounts";
break;
}
case "2":
{
//quick
landingPage = "https://faisanet.mib.com.mv/transfer/quick";
break;
}
case "3":
{
//local
landingPage = "https://faisanet.mib.com.mv/transfer/local";
break;
}
default:
{
//profiles
landingPage = "https://faisanet.mib.com.mv/profiles";
}
}
return landingPage;
}
let requestAuthType = function () {
showProgress();
hideError();
actionBlocker.blockActions();
let formData = $('#login-submit-form').serializeArray().reduce(function (obj, item) {
obj[item.name] = item.value;
return obj;
}, {});
delete formData['pgf02'];
$.ajax({
type: 'POST',
url: "https://faisanet.mib.com.mv/aAuth/getAuthType",
data: formData,
success: function (data, textStatus, request) {
actionBlocker.unblockActions();
//hideProgress();
let responseData = (JSON.parse(request.responseText));
let loginTypeParams = responseData['data'][0];
console.log(data, textStatus, request);
console.log(loginTypeParams);
if (loginTypeParams['loginType'] == 0) {
requestSimpleAuth();
} else {
requestXAuth(loginTypeParams);
}
},
error: function (request, textStatus, errorThrown) {
actionBlocker.unblockActions();
hideProgress();
let responseData = (JSON.parse(request.responseText));
displayError(responseData.reasonText);
}
});
};
let get256Hash = function (text) {
const shaObj = new jsSHA("SHA-256", "TEXT", {encoding: "UTF8"});
/* .update() can be chained */
//shaObj.update("This is").update(" a ");
shaObj.update(text);
const hash = shaObj.getHash("HEX");
return hash.toUpperCase();
};
function getSalt(length) {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
let counter = 0;
while (counter < length) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
counter += 1;
}
return result;
}
let getHashedLoginParams = function (userSalt) {
let formData = $('#login-submit-form').serializeArray().reduce(function (obj, item) {
obj[item.name] = item.value;
return obj;
}, {});
let clientSalt = getSalt(32);
let hashedpw = get256Hash (clientSalt + get256Hash(get256Hash(formData['pgf02']) + userSalt));
delete formData['pgf02'];
formData['pgf03']= hashedpw;
formData['clientSalt'] = clientSalt;
return formData;
};
let requestXAuth = function (loginTypeParams) {
showProgress();
hideError();
actionBlocker.blockActions();
let hashedLoginParams = getHashedLoginParams(loginTypeParams['userSalt']);
$.ajax({
type: 'POST',
url: "https://faisanet.mib.com.mv/aAuth/xAuth",
data: hashedLoginParams,
success: function (data, textStatus, request) {
actionBlocker.unblockActions();
//hideProgress();
let responseData = (JSON.parse(request.responseText));
console.log(data, textStatus, request);
console.log(responseData);
hideProgress();
if (responseData.otpVerified == 0) {
window.location = 'https://faisanet.mib.com.mv/auth2FA';
return;
}
let landingPageOption = responseData.landingPage;
let landingPage = getLandingPage(landingPageOption);
switch (responseData.reasonCode) {
case "101":
{
//individual account login
window.location = landingPage;
break;
}
case "102":
{
//profiles
window.location = "https://faisanet.mib.com.mv/profiles";
break;
}
default:
{
//profiles
window.location = "https://faisanet.mib.com.mv/profiles";
}
}
},
error: function (request, textStatus, errorThrown) {
actionBlocker.unblockActions();
hideProgress();
let responseData = (JSON.parse(request.responseText));
displayError(responseData.reasonText);
}
});
};
let requestSimpleAuth = function () {
showProgress();
hideError();
actionBlocker.blockActions();
let formData = $('#login-submit-form').serializeArray().reduce(function (obj, item) {
obj[item.name] = item.value;
return obj;
}, {});
$.ajax({
type: 'POST',
url: "https://faisanet.mib.com.mv/aAuth",
data: formData,
success: function (data, textStatus, request) {
actionBlocker.unblockActions();
//hideProgress();
let responseData = (JSON.parse(request.responseText));
console.log(data, textStatus, request);
console.log(responseData);
hideProgress();
if (responseData.otpVerified == 0) {
window.location = 'https://faisanet.mib.com.mv/auth2FA';
return;
}
let landingPageOption = responseData.landingPage;
let landingPage = getLandingPage(landingPageOption);
switch (responseData.reasonCode) {
case "101":
{
//individual account login
window.location = landingPage;
break;
}
case "102":
{
//profiles
window.location = "https://faisanet.mib.com.mv/profiles";
break;
}
default:
{
//profiles
window.location = "https://faisanet.mib.com.mv/profiles";
}
}
},
error: function (request, textStatus, errorThrown) {
actionBlocker.unblockActions();
hideProgress();
let responseData = (JSON.parse(request.responseText));
displayError(responseData.reasonText);
}
});
};
$("#login-submit").click(function (e) {
$('#error-info').hide('slow');
e.preventDefault();
requestAuthType();
//hashPassword();
//requestSimpleAuth();
});

12
compose.yml Normal file
View File

@@ -0,0 +1,12 @@
services:
keepalive:
env_file: .env
image: git.shihaam.dev/shihaam/mib-payment-verify/keepalive
cookieserver:
env_file: .env
image: git.shihaam.dev/shihaam/mib-payment-verify/cookieserver
api:
env_file: .env
image: git.shihaam.dev/shihaam/mib-payment-verify/api
ports:
- 5000:5000

13
fetchname.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
source .env 2> /dev/null
accountNo=$1
COOKIE=$(curl -s $COOKIE_SERVER/getcookie)
IBSID=$(echo $COOKIE | jq -r .IBSID)
QL_0=$(echo $COOKIE | jq -r .ql_0)
curl -s 'https://faisanet.mib.com.mv/ajaxBeneficiary/getAccountName' \
-H "cookie: ql_0=${QL_0}; IBSID=${IBSID}" \
--data-raw "accountNo=${accountNo}"

View File

@@ -1,6 +0,0 @@
import pyotp
import sys
seed = sys.argv[1]
totp = pyotp.TOTP(seed)
print(totp.now())

158
getcookie.py Normal file
View File

@@ -0,0 +1,158 @@
from flask import Flask, jsonify, request
import os
import pyotp
import time
from urllib.parse import urlencode
from bs4 import BeautifulSoup
import requests
from dotenv import load_dotenv
class MIBLogin:
def __init__(self):
self.base_url = "https://faisanet.mib.com.mv"
self.headers = {
"User-Agent": "Mozilla/5.0 (ismath-said-will) give-real-api/so-i-wont-have-to-do-this AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36",
"Accept": "*/*"
}
self.last_cookie = None
self.last_renewal_time = 0
self.create_new_session()
def create_new_session(self):
self.session = requests.Session()
def get_login_page(self):
response = self.session.get(f"{self.base_url}/auth", headers=self.headers)
soup = BeautifulSoup(response.text, 'html.parser')
rtag_input = soup.find('input', {'name': 'rTag'})
if not rtag_input:
raise ValueError("Failed to find rTag input field in login page.")
return rtag_input['value']
def get_auth_type(self, rtag, username, retain=1):
data = {'rTag': rtag, 'pgf01': username, 'retain': retain}
headers = {**self.headers, 'Content-Type': 'application/x-www-form-urlencoded'}
response = self.session.post(
f"{self.base_url}/aAuth/getAuthType",
headers=headers,
data=urlencode(data)
)
return response.json()
def login(self, username, password, retain=1):
rtag = self.get_login_page()
self.get_auth_type(rtag, username, retain)
data = {'rTag': rtag, 'pgf01': username, 'pgf02': password, 'retain': retain}
headers = {**self.headers, 'Content-Type': 'application/x-www-form-urlencoded'}
self.session.post(
f"{self.base_url}/aAuth",
headers=headers,
data=urlencode(data)
)
def auth_2fa(self, totp_seed):
self.session.get(f"{self.base_url}/auth2FA", headers=self.headers)
totp = pyotp.TOTP(totp_seed)
data = {'otpType': 3, 'otp': totp.now()}
headers = {**self.headers, 'Content-Type': 'application/x-www-form-urlencoded'}
self.session.post(
f"{self.base_url}/aAuth2FA/verifyOTP",
headers=headers,
data=urlencode(data)
)
cookies = {
cookie.name: cookie.value
for cookie in self.session.cookies
if cookie.name in ['IBSID', 'ql_0']
}
self.last_cookie = cookies
self.last_renewal_time = time.time()
return cookies
def get_profile_rtag(self):
url = f"{self.base_url}/profiles"
response = self.session.get(url, headers=self.headers)
if response.status_code == 200:
soup = BeautifulSoup(response.text, 'html.parser')
profile_card = soup.find('div', {'class': 'card profile-card smooth smooth-shadow'})
if profile_card:
return profile_card.get('data-rt')
else:
raise ValueError("Failed to fetch profile rTag.")
else:
raise ValueError(f"Failed to retrieve profiles page. Status code: {response.status_code}")
def switch_profile(self, profile_id, profile_type):
rtag = self.get_profile_rtag()
url = f"{self.base_url}/aProfileHandler/switchProfile"
data = {'rTag': rtag, 'profileId': profile_id, 'profileType': profile_type}
headers = {**self.headers, 'Content-Type': 'application/x-www-form-urlencoded'}
response = self.session.post(url, headers=headers, data=urlencode(data))
if response.status_code == 200:
result = response.json()
if result.get("success"):
print("Profile switched successfully.")
else:
raise ValueError("Failed to switch profile: " + result.get("reasonText", "Unknown error"))
else:
raise ValueError(f"Profile switch failed with status code: {response.status_code}")
def clear_cookie(self):
self.last_cookie = None
self.create_new_session()
# Initialize Flask app
app = Flask(__name__)
mib = MIBLogin()
load_dotenv()
def renew_cookie_internal():
try:
print(f"Starting cookie renewal process...")
mib.clear_cookie()
mib.login(os.getenv('USERNAME'), os.getenv('PASSWORD'))
cookies = mib.auth_2fa(os.getenv('TOTP_SEED'))
# Profile selection after successful login and 2FA
mib.switch_profile(os.getenv('PROFILE_ID'), os.getenv('PROFILE_TYPE'))
print(f"Cookie renewal and profile switch completed successfully")
return cookies
except Exception as e:
print(f"Error during cookie renewal: {str(e)}")
return {"error": str(e)}
@app.route('/getcookie', methods=['GET'])
def get_cookie():
client_ip = request.remote_addr
print(f"Providing cookie to: {client_ip}")
if mib.last_cookie:
return jsonify(mib.last_cookie)
else:
cookies = renew_cookie_internal()
if "error" in cookies:
return jsonify(cookies), 500
return jsonify(cookies)
@app.route('/newcookie', methods=['GET'])
def new_cookie():
client_ip = request.remote_addr
current_time = time.time()
# Check if less than 60 seconds since last renewal
if current_time - mib.last_renewal_time < 60:
print(f"Providing existing cookie to {client_ip} (last renewal was {int(current_time - mib.last_renewal_time)} seconds ago)")
return jsonify(mib.last_cookie)
print(f"Renewing cookie as requested by: {client_ip}")
cookies = renew_cookie_internal()
if "error" in cookies:
return jsonify(cookies), 500
return jsonify(cookies)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)

View File

@@ -1,12 +1,40 @@
#!/bin/bash #!/bin/bash
request_new_cookie(){
curl -s $COOKIE_SERVER/newcookie > /dev/null
}
while true; do while true; do
source .env source .env 2> /dev/null
curl 'https://faisanet.mib.com.mv/aProfile/keepAlive' \ # Get current date and time
CURRENT_TIME=$(date -u '+%Y-%m-%dT%H:%M:%S.%3NZ')
COOKIE=$(curl -s $COOKIE_SERVER/getcookie)
IBSID=$(echo $COOKIE | jq -r .IBSID)
QL_0=$(echo $COOKIE | jq -r .ql_0)
# Make the keep-alive request
KEEP_ALIVE=$(curl -s 'https://faisanet.mib.com.mv/aProfile/keepAlive' \
-X 'POST' \ -X 'POST' \
-H "cookie: prefTheme=1; ql_0=${QL_0}; IBSID=${IBSID}; dashboardStatsMode=1; _zwaf_ua=Brave; time-tracker=600" -H "cookie: ql_0=${QL_0}; IBSID=${IBSID}")
sleep 30 # Check for session expiration
SUCCESS=$(echo "$KEEP_ALIVE" | jq -r .success)
RESPONSE_CODE=$(echo "$KEEP_ALIVE" | jq -r .responseCode)
if [[ "$SUCCESS" == "false" && "$RESPONSE_CODE" == "3" ]]; then
echo "Session expired. Requesting new cookie..."
request_new_cookie
continue # Restart the loop
fi
# Remove the last closing brace from KEEP_ALIVE
KEEP_ALIVE_MODIFIED=${KEEP_ALIVE%?}
# Combine timestamp with the keep-alive response
echo "$KEEP_ALIVE_MODIFIED, \"timestamp\":\"$CURRENT_TIME\"}"
# Sleep for the specified duration
sleep $SLEEP
done done

BIN
select_profile.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

13
tx.sh
View File

@@ -1,8 +1,13 @@
#!/bin/bash #!/bin/bash
source .env source .env 2> /dev/null
COOKIE=$(curl -s $COOKIE_SERVER/getcookie)
IBSID=$(echo $COOKIE | jq -r .IBSID)
QL_0=$(echo $COOKIE | jq -r .ql_0)
curl 'https://faisanet.mib.com.mv/ajaxAccounts/trxHistory' \ curl 'https://faisanet.mib.com.mv/ajaxAccounts/trxHistory' \
-H "cookie: prefTheme=1; ql_0=${QL_0}; IBSID=${IBSID}; dashboardStatsMode=1; _zwaf_ua=Brave" \ -H "cookie: ql_0=${QL_0}; IBSID=${IBSID}" \
--data-raw 'accountNo=90101400028321000&trxNo=&trxType=0&sortTrx=date&sortDir=desc&fromDate=&toDate=&start=1&end=2&includeCount=1' -s \ --data-raw "accountNo=${ACCOUNT_NUMBER}&trxNo=&trxType=0&sortTrx=date&sortDir=desc&fromDate=&toDate=&start=1&end=20&includeCount=1" -s \
| sed 's/"benefName":"BML - /"benefName":"/g' | sed 's/"benefName":"BML - /"benefName":"/g' \
| jq '.data |= map(select(.baseAmount | tonumber > 0))'