From cf6541ba29780cae2103ca7b254f45cb9e123449 Mon Sep 17 00:00:00 2001 From: khuzema786 Date: Fri, 25 Aug 2023 00:43:53 +0530 Subject: [PATCH] ONDC Registry Subscribe --- utilities/on_subscibe-service/Readme.md | 41 +++ .../on_subscibe-service/requirements.txt | 24 ++ utilities/on_subscibe-service/server.py | 254 ++++++++++++++++++ 3 files changed, 319 insertions(+) create mode 100644 utilities/on_subscibe-service/Readme.md create mode 100644 utilities/on_subscibe-service/requirements.txt create mode 100644 utilities/on_subscibe-service/server.py diff --git a/utilities/on_subscibe-service/Readme.md b/utilities/on_subscibe-service/Readme.md new file mode 100644 index 0000000..a2f27c9 --- /dev/null +++ b/utilities/on_subscibe-service/Readme.md @@ -0,0 +1,41 @@ +# Subscribing to ONDC Registry + +1. Add following environment variables like below. +``` +- name: BAP_BASE_URL + value: "" +- name: BPP_BASE_URL + value: "" +- name: APP_PORT + value: "5000" +- name: STATIC_FILE_PORT + value: "9000" +- name: REGISTRY_URL + value: https://prod.registry.ondc.org/subscribe +``` +2. In **server.py** code and the below request_body please replace the `...` with the required details reffering from [ONDC Onboarding Guide](https://github.com/ONDC-Official/developer-docs/blob/main/registry/Onboarding%20of%20Participants.md) to get the server up and running properly. +3. To subscribe the BAP and BPP to ONDC registry after whitelisting of subscriber_id. You need to call your server with the below request body. +``` +curl --location 'https:///subscribe' \ +--header 'Content-Type: application/json' \ +--data '{ + "/bap/beckn/v1/4b17bd06-ae7e-48e9-85bf-282fb310209c | 60": { + "signingPublicKey": "...", + "signingPrivateKey": "...", + "ondcPublicKey": "MCowBQYDK2VuAyEAvVEyZY91O2yV8w8/CAwVDAnqIZDJJUPdLUUKwLo3K0M=", + "encPublicKey": "...", + "encPrivateKey": "...", + "type": "BAP", + "city": "std:080" + }, + "/dobpp/beckn/7f7896dd-787e-4a0b-8675-e9e6fe93bb8f | 50": { + "signingPublicKey": "HUVYp98+DBp/LIbs7LoeSec3NwQcojLZhsa/tQdqbP4=", + "signingPrivateKey": "...", + "ondcPublicKey": "MCowBQYDK2VuAyEAvVEyZY91O2yV8w8/CAwVDAnqIZDJJUPdLUUKwLo3K0M=", + "encPublicKey": "...", + "encPrivateKey": "...", + "type": "BPP", + "city": "std:080" + } +}' +``` \ No newline at end of file diff --git a/utilities/on_subscibe-service/requirements.txt b/utilities/on_subscibe-service/requirements.txt new file mode 100644 index 0000000..507720c --- /dev/null +++ b/utilities/on_subscibe-service/requirements.txt @@ -0,0 +1,24 @@ +autopep8==2.0.2 +blinker==1.6.2 +certifi==2023.5.7 +cffi==1.15.1 +charset-normalizer==3.1.0 +click==8.1.3 +cryptography==39.0.1 +fire==0.5.0 +Flask==2.3.2 +idna==3.4 +itsdangerous==2.1.2 +Jinja2==3.1.2 +MarkupSafe==2.1.3 +prometheus-client==0.17.0 +pycodestyle==2.10.0 +pycparser==2.21 +pycryptodomex==3.17 +PyNaCl==1.5.0 +pytz==2023.3 +requests==2.31.0 +six==1.16.0 +termcolor==2.3.0 +urllib3==2.0.3 +Werkzeug==2.3.6 diff --git a/utilities/on_subscibe-service/server.py b/utilities/on_subscibe-service/server.py new file mode 100644 index 0000000..feec8e2 --- /dev/null +++ b/utilities/on_subscibe-service/server.py @@ -0,0 +1,254 @@ +import json +import time +import os +import base64 +import datetime +import json +import nacl.encoding +import nacl.hash +from nacl.signing import SigningKey +from cryptography.hazmat.primitives import serialization +from Cryptodome.Cipher import AES +from Cryptodome.Util.Padding import unpad +from flask import Flask, request +import requests +import uuid +import multiprocessing +import threading +import pytz + +bapBaseUrl = os.getenv("BAP_BASE_URL") +bppBaseUrl = os.getenv("BPP_BASE_URL") +registry_url = os.getenv("REGISTRY_URL") +app_port = os.getenv("APP_PORT") +static_file_port = os.getenv("STATIC_FILE_PORT") +subscribers = None +subscribers_uniquekey_store = {} + +bapSubscribeBody = { + "context": { + "operation": { + "ops_no": 1 + }, + }, + "message": { + "request_id": "", + "timestamp": "", + "entity": { + "gst": { + "legal_entity_name": "...", + "business_address": "...", + "city_code": [ + "std:080" + ], + "gst_no": "..." + }, + "pan": { + "name_as_per_pan": "...", + "pan_no": "...", + "date_of_incorporation": "..." + }, + "name_of_authorised_signatory": "...", + "email_id": "email@domain.in", + "mobile_no": ..., + "country": "IND", + "subscriber_id": "", + "unique_key_id": "", + "callback_url": "/", + "key_pair": { + "signing_public_key": "", + "encryption_public_key": "", + "valid_from": "", + "valid_until": "2030-06-19T11:57:54.101Z" + } + }, + "network_participant": [ + { + "subscriber_url": "/", + "domain": "ONDC:TRV10", + "type": "buyerApp", + "msn": False, + "city_code": [] + } + ] + } +} + +bppSubscribeBody = { + "context": { + "operation": { + "ops_no": 2 + } + }, + "message": { + "request_id": "", + "timestamp": "", + "entity": { + "gst": { + "legal_entity_name": "...", + "business_address": "...", + "city_code": [ + "std:080" + ], + "gst_no": "..." + }, + "pan": { + "name_as_per_pan": "...", + "pan_no": "...", + "date_of_incorporation": "..." + }, + "name_of_authorised_signatory": "...", + "email_id": "email@domain.in", + "mobile_no": ..., + "country": "IND", + "subscriber_id": "", + "unique_key_id": "", + "callback_url": "/", + "key_pair": { + "signing_public_key": "", + "encryption_public_key": "", + "valid_from": "", + "valid_until": "2030-06-19T11:57:54.101Z" + } + }, + "network_participant": [ + { + "subscriber_url": "/", + "domain": "ONDC:TRV10", + "type": "sellerApp", + "msn": False, + "city_code": [] + } + ] + } +} + +def sign(signing_key, signing_private_key): + signing_private_key64 = base64.b64decode(signing_private_key) + signer = SigningKey(signing_private_key64, encoder=nacl.encoding.RawEncoder) + signed = signer.sign(bytes(signing_key, encoding='utf8')) + signature = base64.b64encode(signed.signature).decode() + return signature + +def decrypt(enc_public_key, enc_private_key, cipherstring): + private_key = serialization.load_der_private_key( + base64.b64decode(enc_private_key), + password=None + ) + public_key = serialization.load_der_public_key( + base64.b64decode(enc_public_key) + ) + shared_key = private_key.exchange(public_key) + cipher = AES.new(shared_key, AES.MODE_ECB) + ciphertxt = base64.b64decode(cipherstring) + return unpad(cipher.decrypt(ciphertxt), AES.block_size).decode('utf-8') + +def createHtml(request_id, subscriber, subscriber_id): + signature = sign(request_id, subscriber['signingPrivateKey']) + htmlFile = f''' + + + + + + ONDC Site Verification Page + + + ''' + if subscriber['type'] == "BAP": + if not os.path.exists(f'ondc-verification/{subscriber_id[slice(len(bapBaseUrl) + 1, len(subscriber_id))]}'): + os.makedirs(f'ondc-verification/{subscriber_id[slice(len(bapBaseUrl) + 1, len(subscriber_id))]}') + with open(f"ondc-verification/{subscriber_id[slice(len(bapBaseUrl) + 1, len(subscriber_id))]}/ondc-site-verification.html", "w+") as file: + file.write(htmlFile) + elif subscriber['type'] == "BPP": + if not os.path.exists(f'ondc-verification/{subscriber_id[slice(len(bppBaseUrl) + 1, len(subscriber_id))]}'): + os.makedirs(f'ondc-verification/{subscriber_id[slice(len(bppBaseUrl) + 1, len(subscriber_id))]}') + with open(f"ondc-verification/{subscriber_id[slice(len(bppBaseUrl) + 1, len(subscriber_id))]}/ondc-site-verification.html", "w+") as file: + file.write(htmlFile) + +app = Flask(__name__) + +@app.route('/subscribe/on_subscribe', methods=['POST']) +def onsubscribe(): + data = request.get_json() + print(f"/subscribe/on_subscribe called :: Request -> {data}") + subscriber_id = data['subscriber_id'] + unique_key_id = subscribers_uniquekey_store[subscriber_id] + subscriber = subscribers[f"{subscriber_id} | {unique_key_id}"] + return { + "answer" : decrypt(subscriber['ondcPublicKey'], subscriber['encPrivateKey'], data['challenge']) + } + +def serve_file(): + os.system(f'python -m http.server {static_file_port} --directory ondc-verification') + +def subscribe_helper(): + if subscribers != None: + global subscribers_uniquekey_store + for subscriber_uk_id, subscriber in subscribers.items(): + [subscriber_id, unique_key_id] = subscriber_uk_id.split(' | ') + subscribers_uniquekey_store[subscriber_id] = unique_key_id + request_id = str(uuid.uuid4()) + subscribers[subscriber_uk_id]['requestId'] = request_id + createHtml(request_id, subscriber, subscriber_id) + process = multiprocessing.Process(target=serve_file) + process.start() + time.sleep(5) + for subscriber_uk_id, subscriber in subscribers.items(): + [subscriber_id, unique_key_id] = subscriber_uk_id.split(' | ') + request_id = subscriber['requestId'] + current_datetime = datetime.datetime.now().astimezone(pytz.timezone('Asia/Kolkata')) + current_datetime_iso8601 = current_datetime.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" + + if subscriber['type'] == 'BAP': + bapSubscribeBody['message']['request_id'] = request_id + bapSubscribeBody['message']['timestamp'] = current_datetime_iso8601 + bapSubscribeBody['message']['entity']['subscriber_id'] = subscriber_id + bapSubscribeBody['message']['entity']['unique_key_id'] = unique_key_id + bapSubscribeBody['message']['entity']['key_pair']['signing_public_key'] = subscriber['signingPublicKey'] + bapSubscribeBody['message']['entity']['key_pair']['encryption_public_key'] = subscriber['encPublicKey'] + bapSubscribeBody['message']['entity']['key_pair']['valid_from'] = current_datetime_iso8601 + bapSubscribeBody['message']['network_participant'][0]['city_code'] = [subscriber['city']] + + print(json.dumps(bapSubscribeBody)) + + response = requests.post(registry_url, json=bapSubscribeBody) + if response.status_code == 200: + print(f"/subscribe for {subscriber_uk_id} request successful :: {response.json()}") + else: + print(f"/subscribe for {subscriber_uk_id} request failed :: {response.json()}") + elif subscriber['type'] == 'BPP': + bppSubscribeBody['message']['request_id'] = request_id + bppSubscribeBody['message']['timestamp'] = current_datetime_iso8601 + bppSubscribeBody['message']['entity']['subscriber_id'] = subscriber_id + bppSubscribeBody['message']['entity']['unique_key_id'] = unique_key_id + bppSubscribeBody['message']['entity']['key_pair']['signing_public_key'] = subscriber['signingPublicKey'] + bppSubscribeBody['message']['entity']['key_pair']['encryption_public_key'] = subscriber['encPublicKey'] + bppSubscribeBody['message']['entity']['key_pair']['valid_from'] = current_datetime_iso8601 + bppSubscribeBody['message']['network_participant'][0]['city_code'] = [subscriber['city']] + + print(json.dumps(bppSubscribeBody)) + + response = requests.post(registry_url, json=bppSubscribeBody) + if response.status_code == 200: + print(f"/subscribe for {subscriber_uk_id} request successful :: {response.json()}") + else: + print(f"/subscribe for {subscriber_uk_id} request failed :: {response.json()}") + time.sleep(300) + process.terminate() + process.join() + +@app.route('/subscribe', methods=['POST']) +def subscribe(): + global subscribers + subscribers = request.get_json() + print(f"/subscribe called :: Request -> {subscribers}") + thread1 = threading.Thread(target=subscribe_helper) + thread1.start() + return { "success" : "ACK" } + +def start_flask_app(): + app.run(port=app_port, host="0.0.0.0") + +if __name__ == '__main__': + start_flask_app(); \ No newline at end of file