From 244d12d3914321c92dbab0171ed1749a47db3741 Mon Sep 17 00:00:00 2001 From: Johan de Klerk <johan@shiplogic.com> Date: Wed, 26 Apr 2023 10:14:01 +0200 Subject: [PATCH] Get Absa CIB statement transactions --- bank_transactions/absa_bank_transactions.go | 268 ++++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 bank_transactions/absa_bank_transactions.go diff --git a/bank_transactions/absa_bank_transactions.go b/bank_transactions/absa_bank_transactions.go new file mode 100644 index 0000000..8152342 --- /dev/null +++ b/bank_transactions/absa_bank_transactions.go @@ -0,0 +1,268 @@ +package bank_transactions + +import ( + "crypto/tls" + "encoding/base64" + "github.com/go-resty/resty/v2" + "github.com/google/uuid" + "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors" + "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/secrets_manager" + "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/string_utils" + "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/struct_utils" + "io" + "strings" + "time" +) + +type AbsaLoginInfo struct { + CertExpiry string + CertSerialNumber string + PartialNo string + DeviceFingerprint string + AbsaSecretName string + IsDebug bool +} + +const ( + sleepTime = time.Second * 2 +) + +func GetAbsaTransactions(loginInfo AbsaLoginInfo) ([]byte, error) { + + client, err := setupAbsaClient(loginInfo.AbsaSecretName, loginInfo.IsDebug) + if err != nil { + return nil, err + } + + defer logoff(client) + + // Get Initial session ID + err = login(client, loginInfo) + if err != nil { + return nil, err + } + + // Navigate to statement download + err = startStatementDownloadProcess(client) + if err != nil { + return nil, err + } + + // Request statement + dateString := time.Now().Format("20060102") + _, err = statementDownloadProcessCall(client, map[string]string{ + "page": "enquiry-statement", + "groupselect": "", + "linkallterm": "false", + "buttonClicked": "proceed", + "SelType": "D", + "FAInd": "false", + "systemDate": dateString, + "account_options": "all", + "fromaccountgroup": "blank", + "fromaccount": "blank", + "fromaccountdisplay1": "blank", + "showall_option": "showall", + "format_options": "file", + "file_format_options": "csv", + "statement_options": "last", + "days": "7", + "startdate": dateString, + "enddate": dateString, + "startnum": "", + "endnum": "", + "ErrorText": "", + }) + if err != nil { + return nil, err + } + + // Wait for statement to process + for { + responseBytes, err := statementDownloadProcessCall(client, map[string]string{ + "page": "enquiry-statement-status", + "buttonClicked": "proceed", + "ErrorText": "", + }) + if err != nil { + return nil, err + } + + if strings.Contains(string(responseBytes), "The statement file is available for download") { + break + } + time.Sleep(sleepTime) + } + + // Download file + statementBytes, err := statementDownloadProcessCall(client, map[string]string{ + "page": "enquiry-statement-file", + "buttonClicked": "saveas", + "filename": "25591016.csv", + "ErrorText": "", + }) + if err != nil { + return nil, err + } + return statementBytes, nil +} + +func setupAbsaClient(secretName string, isDebug bool) (*resty.Client, error) { + // Get cert data + credentials, err := getAbsaSecret(secretName, isDebug) + if err != nil { + return nil, err + } + certDataString := credentials["cert"] + certData, _ := base64.StdEncoding.DecodeString(certDataString) + + privateKeyDataString := credentials["private_key"] + privateKeyData, _ := base64.StdEncoding.DecodeString(privateKeyDataString) + + cert, err := tls.X509KeyPair(certData, privateKeyData) + if err != nil { + return nil, err + } + + client := resty.New(). + SetCertificates(cert) + return client, nil +} + +func login(client *resty.Client, loginInfo AbsaLoginInfo) error { + + // Get password + credentials, err := getAbsaSecret(loginInfo.AbsaSecretName, loginInfo.IsDebug) + if err != nil { + return err + } + + username := credentials["username"] + password := credentials["password"] + + // Get Initial session ID + response, err := client.R(). + SetDoNotParseResponse(true). + Get("https://bionline.absa.co.za/businessintegrator/login_new.jsp") + if err != nil { + return err + } + time.Sleep(sleepTime) + + // Login + universalTracker := uuid.New().String() + "-" + uuid.New().String() + response, err = client.R(). + SetDoNotParseResponse(true). + SetFormData(map[string]string{ + "lastpage": "login", + "buttonClicked": "proceed", + "hasCert": "Y", + "certExpired": "false", + "certExpiry": loginInfo.CertExpiry, + "displayIgnoreButton": "true", + "partialNo": loginInfo.PartialNo, + "universalTracker": universalTracker, + "deviceFingerprint": loginInfo.DeviceFingerprint, + "certSerialNo": loginInfo.CertSerialNumber, + "UserID": username, + "password_one": password, + }). + Post("https://bionline.absa.co.za/businessintegrator/SignOn") + if err != nil { + return err + } + + responseBytes, err := io.ReadAll(response.RawResponse.Body) + if strings.Contains(string(responseBytes), "User already signed on") { + // Log out + response, err = client.R(). + SetDoNotParseResponse(true). + SetFormData(map[string]string{ + "buttonClicked": "proceed", + "lastpage": "signedon_to_signoff", + "ErrorText": "", + }). + Post("https://bionline.absa.co.za/businessintegrator/SignOn") + if err != nil { + return err + } + + // Login again + err := login(client, loginInfo) + if err != nil { + return err + } + } + + return nil +} + +func startStatementDownloadProcess(client *resty.Client) error { + // Navigate to statement download + response, err := client.R(). + SetDoNotParseResponse(true). + SetQueryParam("dojo.preventCache", string_utils.Int64ToString(time.Now().UnixMilli())). + SetHeader("Referer", "https://bionline.absa.co.za/businessintegrator/enquiries/menu.jsp"). + SetHeader("Content-Type", "application/x-www-form-urlencoded"). + Get("https://bionline.absa.co.za/businessintegrator/EnquireBankStatement") + if err != nil { + return err + } + + responseBytes, _ := io.ReadAll(response.RawResponse.Body) + + // Ensure we are on the "Statement Enquiry" screen + if !strings.Contains(string(responseBytes), "Statement Enquiry") { + return errors.New("Could not load 'Statement Enquiry' screen") + } + time.Sleep(sleepTime) + return nil +} + +func statementDownloadProcessCall(client *resty.Client, formData map[string]string) ([]byte, error) { + response, err := client.R(). + SetDoNotParseResponse(true). + SetFormData(formData). + Post("https://bionline.absa.co.za/businessintegrator/EnquireBankStatement") + if err != nil { + return nil, err + } + + responseBytes, err := io.ReadAll(response.RawResponse.Body) + time.Sleep(sleepTime) + return responseBytes, nil +} + +func logoff(client *resty.Client) error { + // Logoff + _, err := client.R(). + SetDoNotParseResponse(true). + Get("https://bionline.absa.co.za/businessintegrator/Logoff") + if err != nil { + return err + } + return nil +} + +func getAbsaSecret(secretName string, isDebug bool) (map[string]string, error) { + // This secret is manually created on Secrets Manager + + /* + Extract the key and cert from the p12 certificate + ``` + openssl pkcs12 -in Absa.p12 -nocerts -out userkey.pem -nodes + openssl pkcs12 -in Absa.p12 -clcerts -nokeys -out usercert.pem + ``` + + Base64 encode the key and cert and save it in secrets manager + */ + + secretJson, _ := secrets_manager.GetSecret(secretName, isDebug) + var credentials map[string]string + err := struct_utils.UnmarshalJSON([]byte(secretJson), &credentials) + if err != nil { + return nil, err + } + + return credentials, nil +} -- GitLab