Source code for bbc_app

# -*- coding: utf-8 -*-
"""
Copyright (c) 2017 beyond-blockchain.org.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import gevent
from gevent import monkey
monkey.patch_all()
from gevent import socket
import json
import traceback
import queue
import binascii
import os

import sys
sys.path.append("../../")

from bbc1.common import bbclib, message_key_types
from bbc1.common.bbclib import NodeInfo, ServiceMessageType as MsgType, StorageType
from bbc1.common.message_key_types import KeyType, PayloadType
from bbc1.common.bbc_error import *
from bbc1.common import logger

DEFAULT_CORE_PORT = 9000
MAPPING_FILE = ".bbc_id_mappings"


[docs]def store_id_mappings(name, asset_group_id, transaction_id=None, asset_ids=None): if transaction_id is None and asset_ids is None: return mapping = dict() asset_group_id_str = binascii.b2a_hex(asset_group_id).decode() if os.path.exists(MAPPING_FILE): with open(MAPPING_FILE, "r") as f: mapping = json.load(f) mapping.setdefault(asset_group_id_str, dict()).setdefault(name, dict()) if transaction_id is not None: mapping[asset_group_id_str][name]['transaction_id'] = binascii.b2a_hex(transaction_id).decode() if asset_ids is not None: if isinstance(asset_ids, list): entry = [] for ast in asset_ids: entry.append(binascii.b2a_hex(ast)) mapping[asset_group_id_str][name]['asset_id'] = entry else: mapping[asset_group_id_str][name]['asset_id'] = binascii.b2a_hex(asset_ids).decode() with open(MAPPING_FILE, "w") as f: json.dump(mapping, f, indent=4)
[docs]def remove_id_mappings(name, asset_group_id): mapping = dict() asset_group_id_str = binascii.b2a_hex(asset_group_id).decode() if os.path.exists(MAPPING_FILE): with open(MAPPING_FILE, "r") as f: mapping = json.load(f) if asset_group_id_str in mapping: mapping[asset_group_id_str].pop(name, None) if len(mapping[asset_group_id_str].keys()) == 0: del mapping[asset_group_id_str] with open(MAPPING_FILE, "w") as f: json.dump(mapping, f, indent=4)
[docs]def get_id_from_mappings(name, asset_group_id): if not os.path.exists(MAPPING_FILE): return None asset_group_id_str = binascii.b2a_hex(asset_group_id).decode() with open(MAPPING_FILE, "r") as f: mapping = json.load(f) if mapping is None: return None if asset_group_id_str in mapping and name in mapping[asset_group_id_str]: result = dict() if 'transaction_id' in mapping[asset_group_id_str][name]: result['transaction_id'] = binascii.a2b_hex(mapping[asset_group_id_str][name]['transaction_id']) if 'asset_id' in mapping[asset_group_id_str][name]: if isinstance(mapping[asset_group_id_str][name]['asset_id'], list): entry = [] for ast in mapping[asset_group_id_str][name]['asset_id']: entry.append(binascii.a2b_hex(ast)) result['asset_id'] = entry else: result['asset_id'] = binascii.a2b_hex(mapping[asset_group_id_str][name]['asset_id']) return result return None
[docs]class BBcAppClient: def __init__(self, host='127.0.0.1', port=DEFAULT_CORE_PORT, logname="-", loglevel="none"): self.logger = logger.get_logger(key="bbc_app", level=loglevel, logname=logname) self.connection = socket.create_connection((host, port)) self.callback = Callback(log=self.logger) self.asset_groups = set() self.user_id = None self.query_id = (0).to_bytes(2, 'little') self.start_receiver_loop()
[docs] def set_callback(self, callback_obj): """ Set callback object that implements message processing functions :param callback_obj: :return: """ self.callback = callback_obj self.callback.set_logger(self.logger)
[docs] def set_user_id(self, identifier): """ Set user_id of the object :param identifier: :return: """ self.user_id = identifier
[docs] def set_asset_group_id(self, asset_group_id): """ Set asset_group_id (before register_to_core) :param asset_group_id: :return: """ self.asset_groups.add(asset_group_id)
[docs] def make_message_structure(self, asset_group_id, cmd): """ (internal use) make a base message structure for sending to the core node :param asset_group_id: :param cmd: :return: """ self.query_id = ((int.from_bytes(self.query_id, 'little') + 1) // 65536).to_bytes(2, 'little') return { KeyType.command: cmd, KeyType.asset_group_id: asset_group_id, KeyType.source_user_id: self.user_id, KeyType.query_id: self.query_id, KeyType.status: ESUCCESS, }
[docs] def send_msg(self, dat): """ (internal use) send the message to the core node :param dat: :return: """ if KeyType.asset_group_id not in dat or KeyType.source_user_id not in dat: self.logger.warn("Message must include asset_group_id and source_id") return False try: msg = message_key_types.make_message(PayloadType.Type_msgpack, dat) self.connection.sendall(msg) except Exception as e: self.logger.error(e) return False return True
[docs] def domain_setup(self, domain_id, module_name=None): """ Set up domain with the specified network module (maybe used by a system administrator) :param domain_id: :param module_name: :param storage_type: StorageType value :param storage_path: :return: """ dat = self.make_message_structure(None, MsgType.REQUEST_SETUP_DOMAIN) dat[KeyType.domain_id] = domain_id if module_name is not None: dat[KeyType.network_module] = module_name return self.send_msg(dat)
[docs] def get_domain_peerlist(self, domain_id): """ Get peer list of the domain from the core node (maybe used by a system administrator) :param domain_id: :return: """ dat = self.make_message_structure(None, MsgType.REQUEST_GET_PEERLIST) dat[KeyType.domain_id] = domain_id return self.send_msg(dat)
[docs] def set_domain_static_node(self, domain_id, node_id, ipv4, ipv6, port): """ Set static node to the core node (maybe used by a system administrator) :param domain_id: :param node_id: :param ipv4: :param ipv6: :param port: :return: """ dat = self.make_message_structure(None, MsgType.REQUEST_SET_STATIC_NODE) dat[KeyType.domain_id] = domain_id dat[KeyType.peer_info] = [node_id, ipv4, ipv6, port] return self.send_msg(dat)
[docs] def send_domain_ping(self, domain_id, ipv4, ipv6, port): """ Send domain ping to notify the existence of the node (maybe used by a system administrator) :param domain_id: :param ipv4: :param ipv6: :param port: :return: """ dat = self.make_message_structure(None, MsgType.DOMAIN_PING) dat[KeyType.domain_id] = domain_id dat[KeyType.ipv4_address] = ipv4 dat[KeyType.ipv6_address] = ipv6 dat[KeyType.port_number] = port return self.send_msg(dat)
[docs] def register_asset_group(self, domain_id, asset_group_id, storage_type=StorageType.FILESYSTEM, storage_path=None, advertise_in_domain0=False): """ Register an asset_group in the core node (maybe used by a system administrator) :param domain_id: :param asset_group_id: :param storage_type: :param storage_path: :param advertise_in_domain0: :return: """ dat = self.make_message_structure(asset_group_id, MsgType.REQUEST_SETUP_ASSET_GROUP) dat[KeyType.domain_id] = domain_id dat[KeyType.storage_type] = storage_type dat[KeyType.advertise_in_domain0] = advertise_in_domain0 if storage_path is not None: dat[KeyType.storage_path] = storage_path return self.send_msg(dat)
[docs] def get_bbc_config(self): """ Get config file of bbc_core (maybe used by a system administrator) :return: """ dat = self.make_message_structure(None, MsgType.REQUEST_GET_CONFIG) return self.send_msg(dat)
[docs] def get_domain_list(self): """ Get domain_id list in bbc_core (maybe used by a system administrator) :return: """ dat = self.make_message_structure(None, MsgType.REQUEST_GET_DOMAINLIST) return self.send_msg(dat)
[docs] def manipulate_ledger_subsystem(self, enable=False): """ start/stop ledger_subsystem on the bbc_core (maybe used by a system administrator) :param enable: True->start, False->stop :return: """ dat = self.make_message_structure(None, MsgType.REQUEST_MANIP_LEDGER_SUBSYS) dat[KeyType.ledger_subsys_manip] = enable return self.send_msg(dat)
[docs] def register_to_core(self): """ Register the client (user_id) to the core node. After that, the client can communicate with the core node :return: """ for asset_group_id in self.asset_groups: dat = self.make_message_structure(asset_group_id, MsgType.REGISTER) self.send_msg(dat) return True
[docs] def unregister_from_core(self): """ Unregister and disconnect from the core node :return: """ dat = self.make_message_structure(None, MsgType.UNREGISTER) return self.send_msg(dat)
[docs] def get_cross_refs(self, asset_group_id, number): """ Get cross_refs :param asset_group_id: :param number: :return: """ dat = self.make_message_structure(asset_group_id, MsgType.REQUEST_CROSS_REF) dat[KeyType.count] = number return self.send_msg(dat)
[docs] def gather_signatures(self, asset_group_id, tx_obj, reference_obj=None, destinations=None, asset_files=None): """ Request to gather signatures from the specified user_ids :param asset_group_id: :param tx_obj: :param reference_obj: BBcReference object :param destinations: list of destination user_ids :param asset_files: dictionary of {asset_id: file_content} :return: """ if reference_obj is None and destinations is None: return False dat = self.make_message_structure(asset_group_id, MsgType.REQUEST_GATHER_SIGNATURE) dat[KeyType.transaction_data] = tx_obj.serialize() if reference_obj is not None: dat[KeyType.destination_user_ids] = reference_obj.get_destinations() referred_transactions = dict() referred_transactions.update(reference_obj.get_referred_transaction()) if len(referred_transactions) > 0: dat[KeyType.transactions] = referred_transactions elif destinations is not None: dat[KeyType.destination_user_ids] = destinations if isinstance(asset_files, dict): dat[KeyType.all_asset_files] = asset_files return self.send_msg(dat)
[docs] def sendback_signature(self, asset_group_id, dst, ref_index, sig): """ Send back the signed transaction to the source :param asset_group_id: :param dst: :param ref_index: Which reference in transaction the signature is for :param sig: :return: """ dat = self.make_message_structure(asset_group_id, MsgType.RESPONSE_SIGNATURE) dat[KeyType.destination_user_id] = dst dat[KeyType.ref_index] = ref_index dat[KeyType.signature] = sig.serialize() return self.send_msg(dat)
[docs] def sendback_denial_of_sign(self, asset_group_id, dst, reason_text): """ Send back the denial of sign the transaction :param asset_group_id: :param dst: :param reason_text: :return: """ dat = self.make_message_structure(asset_group_id, MsgType.RESPONSE_SIGNATURE) dat[KeyType.destination_user_id] = dst dat[KeyType.status] = EOTHER dat[KeyType.reason] = reason_text return self.send_msg(dat)
[docs] def insert_transaction(self, asset_group_id, tx_obj): """ Request to insert a legitimate transaction :param asset_group_id: :param tx_obj: Transaction object (not deserialized one) :return: """ if tx_obj.transaction_id is None: tx_obj.digest() dat = self.make_message_structure(asset_group_id, MsgType.REQUEST_INSERT) dat[KeyType.transaction_data] = tx_obj.serialize() ast = dict() for evt in tx_obj.events: if evt.asset is None: continue asset_digest, content = evt.asset.get_asset_file() if content is not None: ast[evt.asset.asset_id] = content dat[KeyType.all_asset_files] = ast return self.send_msg(dat)
[docs] def search_asset(self, asset_group_id, asset_id): """ Search request for the specified asset. This would return transaction_data (and asset_file file content) :param asset_group_id: :param asset_id: :return: """ dat = self.make_message_structure(asset_group_id, MsgType.REQUEST_SEARCH_ASSET) dat[KeyType.asset_id] = asset_id return self.send_msg(dat)
[docs] def search_transaction(self, asset_group_id, transaction_id): """ Search request for transaction_data :param asset_group_id: :param transaction_id: :return: """ dat = self.make_message_structure(asset_group_id, MsgType.REQUEST_SEARCH_TRANSACTION) dat[KeyType.transaction_id] = transaction_id return self.send_msg(dat)
[docs] def register_in_ledger_subsystem(self, asset_group_id, transaction_id): """ Register transaction_id in the ledger_subsystem :param asset_group_id: :param transaction_id: :return: """ dat = self.make_message_structure(asset_group_id, MsgType.REQUEST_REGISTER_HASH_IN_SUBSYS) dat[KeyType.transaction_id] = transaction_id return self.send_msg(dat)
[docs] def verify_in_ledger_subsystem(self, asset_group_id, transaction_id): """ Verify transaction_id in the ledger_subsystem :param asset_group_id: :param transaction_id: :return: """ dat = self.make_message_structure(asset_group_id, MsgType.REQUEST_VERIFY_HASH_IN_SUBSYS) dat[KeyType.transaction_id] = transaction_id return self.send_msg(dat)
[docs] def send_message(self, msg, asset_group_id, dst_user_id): """ Send peer-to-peer message to the specified user_id :param msg: :param asset_group_id: :param dst_user_id: :return: """ dat = self.make_message_structure(asset_group_id, MsgType.MESSAGE) dat[KeyType.destination_user_id] = dst_user_id dat[KeyType.message] = msg return self.send_msg(dat)
[docs] def start_receiver_loop(self): jobs = [gevent.spawn(self.receiver_loop)]
#gevent.joinall(jobs)
[docs] def receiver_loop(self): msg_parser = message_key_types.Message() try: while True: buf = self.connection.recv(8192) if len(buf) == 0: break msg_parser.recv(buf) while True: msg = msg_parser.parse() if msg is None: break self.callback.dispatch(msg, msg_parser.payload_type) except Exception as e: self.logger.info("TCP disconnect: %s" % e) print(traceback.format_exc()) self.connection.close()
[docs]class Callback: """ Set of callback functions for processing received message """ def __init__(self, log=None): self.logger = log self.queue = queue.Queue()
[docs] def set_logger(self, log): self.logger = log
[docs] def dispatch(self, dat, payload_type): #self.logger.debug("Received: %s" % dat) if KeyType.command not in dat: self.logger.warn("No command exists") return if dat[KeyType.command] == MsgType.RESPONSE_SEARCH_TRANSACTION: self.proc_resp_search_transaction(dat) elif dat[KeyType.command] == MsgType.RESPONSE_SEARCH_ASSET: self.proc_resp_search_asset(dat) elif dat[KeyType.command] == MsgType.RESPONSE_GATHER_SIGNATURE: self.proc_resp_gather_signature(dat) elif dat[KeyType.command] == MsgType.REQUEST_SIGNATURE: self.proc_cmd_sign_request(dat) elif dat[KeyType.command] == MsgType.RESPONSE_SIGNATURE: self.proc_resp_sign_request(dat) elif dat[KeyType.command] == MsgType.RESPONSE_INSERT: self.proc_resp_insert(dat) elif dat[KeyType.command] == MsgType.RESPONSE_CROSS_REF: self.proc_resp_cross_ref(dat) elif dat[KeyType.command] == MsgType.MESSAGE: self.proc_user_message(dat) elif dat[KeyType.command] == MsgType.RESPONSE_REGISTER_HASH_IN_SUBSYS: self.proc_resp_register_hash(dat) elif dat[KeyType.command] == MsgType.RESPONSE_VERIFY_HASH_IN_SUBSYS: self.proc_resp_verify_hash(dat) elif dat[KeyType.command] == MsgType.RESPONSE_SETUP_ASSET_GROUP: self.proc_resp_asset_group_setup(dat) elif dat[KeyType.command] == MsgType.RESPONSE_SETUP_DOMAIN: self.proc_resp_domain_setup(dat) elif dat[KeyType.command] == MsgType.RESPONSE_GET_PEERLIST: self.proc_resp_get_peerlist(dat) elif dat[KeyType.command] == MsgType.RESPONSE_GET_DOMAINLIST: self.proc_resp_get_domainlist(dat) elif dat[KeyType.command] == MsgType.RESPONSE_SET_STATIC_NODE: self.proc_resp_set_peer(dat) elif dat[KeyType.command] == MsgType.RESPONSE_GET_CONFIG: self.proc_resp_get_config(dat) elif dat[KeyType.command] == MsgType.RESPONSE_MANIP_LEDGER_SUBSYS: self.proc_resp_ledger_subsystem(dat) else: self.logger.warn("No method to process for command=%d" % dat[KeyType.command])
[docs] def synchronize(self, timeout=None): """ Wait for receiving message :param timeout: timeout second for waiting :return: """ try: return self.queue.get(timeout=timeout) except: return None
[docs] def proc_resp_cross_ref(self, dat): cross_refs = [] for cross_ref in dat[KeyType.cross_refs]: cross = bbclib.BBcCrossRef(cross_ref[0], cross_ref[1]) cross_refs.append(cross) self.queue.put(cross_refs)
[docs] def proc_cmd_sign_request(self, dat): self.queue.put(dat)
[docs] def proc_resp_sign_request(self, dat): self.queue.put(dat)
[docs] def proc_resp_gather_signature(self, dat): if KeyType.status not in dat or dat[KeyType.status] < ESUCCESS: self.queue.put(dat) return sig = bbclib.recover_signature_object(dat[KeyType.signature]) self.queue.put({KeyType.status: ESUCCESS, KeyType.result: (dat[KeyType.ref_index], dat[KeyType.source_user_id], sig)})
[docs] def proc_resp_insert(self, dat): self.queue.put(dat)
[docs] def proc_resp_search_asset(self, dat): self.queue.put(dat)
[docs] def proc_resp_search_transaction(self, dat): if KeyType.transaction_data in dat: tx_obj = bbclib.recover_transaction_object_from_rawdata(dat[KeyType.transaction_data]) tx_obj.digest() digest = tx_obj.digest() for i in range(len(tx_obj.signatures)): result = tx_obj.signatures[i].verify(digest) if not result: dat = {KeyType.status: EBADTXSIGNATURE, KeyType.reason: "Verify failure", KeyType.transaction_data: tx_obj} break self.queue.put(dat)
[docs] def proc_user_message(self, dat): self.queue.put(dat)
[docs] def proc_resp_register_hash(self, dat): self.queue.put(dat)
[docs] def proc_resp_verify_hash(self, dat): self.queue.put(dat)
[docs] def proc_resp_asset_group_setup(self, dat): self.queue.put(dat)
[docs] def proc_resp_domain_setup(self, dat): self.queue.put(dat)
[docs] def proc_resp_get_peerlist(self, dat): """ Return node info :param dat: :return: list of node info (the first one is that of the connecting core) """ if KeyType.peer_list not in dat: self.queue.put(None) return peerlist = dat[KeyType.peer_list] results = [] count = int.from_bytes(peerlist[:4], 'little') for i in range(count): base = 4 + i*(32+4+16+2) node_id = peerlist[base:base+32] ipv4 = peerlist[base+32:base+36] ipv6 = peerlist[base+36:base+52] port = peerlist[base+52:base+54] info = NodeInfo() info.recover_nodeinfo(node_id, ipv4, ipv6, port) results.append([info.node_id, info.ipv4, info.ipv6, info.port]) self.queue.put(results)
[docs] def proc_resp_get_domainlist(self, dat): """ Return domain_ids :param dat: :return: list of domain_id """ if KeyType.domain_list not in dat: self.queue.put(None) return domainlist = dat[KeyType.domain_list] results = [] count = int.from_bytes(domainlist[:2], 'big') for i in range(count): base = 2 + 32*i domain_id = domainlist[base:base+32] results.append(domain_id) self.queue.put(results)
[docs] def proc_resp_set_peer(self, dat): self.queue.put(dat)
[docs] def proc_resp_get_config(self, dat): self.queue.put(dat)
[docs] def proc_resp_ledger_subsystem(self, dat): self.queue.put(dat)