def configure(self, **config): self.created = datetime.fromtimestamp( config['created']) if 'created' in config else datetime.now() if 'trigger_type' in config and valid_trigger_type( config['trigger_type']): self.trigger_type = config['trigger_type'] if 'script' in config and valid_script(config['script']): self.script = config['script'] if 'data' in config and isinstance(config['data'], dict): self.data = config['data'] if 'multi' in config and config['multi'] in [True, False]: self.multi = config['multi'] if 'status' in config and valid_status(config['status']): self.status = config['status'] if 'reset' in config and config['reset'] is True: self.triggered = 0 self.status = 'Active' elif 'triggered' in config and valid_amount(config['triggered']): self.triggered = config['triggered'] if 'description' in config and valid_description( config['description']): self.description = config['description'] if 'creator_name' in config and valid_creator(config['creator_name']): self.creator_name = config['creator_name'] if 'creator_email' in config and valid_email(config['creator_email']): self.creator_email = config['creator_email'] if 'youtube' in config and valid_youtube_id(config['youtube']): self.youtube = config['youtube'] if 'visibility' in config and valid_visibility(config['visibility']): self.visibility = config['visibility'] if 'actions' in config and valid_actions(config['actions']): self.actions = config['actions'] configured_actions = get_actions() for action_id in self.actions: if action_id not in configured_actions: LOG.warning('Trigger %s contains unknown action: %s' % (self.id, action_id)) if 'self_destruct' in config and valid_timestamp( config['self_destruct']): self.self_destruct = config['self_destruct'] if 'destruct_actions' in config and config['destruct_actions'] in [ True, False ]: self.destruct_actions = config['destruct_actions']
def push_tx(tx): # Must do import here to avoid circular import from data.data import get_explorer_api LOG.warning( 'BTC.com api does not support broadcasting transactions, using Blockchain.info instead!' ) blockchain_info_api = get_explorer_api('blockchain.info') return blockchain_info_api.push_tx(tx)
def get_transactions(self, address): LOG.warning( 'DO NOT USE CHAIN.SO TO GET ADDRESS TRANSACTIONS!!!!!!!!!!!!!') url = '{api_url}/address/{network}/{address}'.format( api_url=self.url, network=self.network, address=address) try: LOG.info('GET %s' % url) r = requests.get(url) data = r.json() except Exception as ex: LOG.error( 'Unable to get transactions of address %s from Chain.so: %s' % (address, ex)) return { 'error': 'Unable to get transactions of address %s from Chain.so' % address } if 'data' not in data: LOG.error('Invalid response data from Chain.so: %s' % data) return {'error': 'Invalid response data from Chain.so: %s' % data} data = data['data'] txids = [transaction['txid'] for transaction in data['txs']] txs = [] # I know this is very ugly, but its the only way to get the necessary information from chain.so for txid in txids: transaction_data = self.get_transaction(txid=txid) if 'transaction' in transaction_data: txs.append(transaction_data['transaction']) sleep( 10 ) # try to avoid hitting the rate limits probably won't work, extremely slow!!!!! return {'transactions': txs}
def verify_signed_message(trigger_id, **data): if not all(key in data for key in ['address', 'message', 'signature']): return { 'error': 'Request data does not contain all required keys: address, message and signature' } triggers = get_triggers() if trigger_id not in triggers: return {'error': 'Unknown trigger id: %s' % trigger_id} trigger = get_trigger(trigger_id) if trigger.trigger_type != TriggerType.SIGNEDMESSAGE: return { 'error': 'Trigger %s is not a Signedmessage trigger' % trigger.trigger_type } if trigger.address is not None and trigger.address != data['address']: return { 'error': 'Trigger %s only listens to signed messages from address %s' % (trigger.id, trigger.address) } if verify_message(address=data['address'], message=data['message'], signature=data['signature']) is True: if trigger.status == 'Active': LOG.info('Trigger %s received a verified signed message' % trigger_id) trigger.process_message( address=data['address'], message=data['message'], signature=data['signature'], data=data['data'] if 'data' in data else None, ipfs_object=data['ipfs_object'] if 'ipfs_object' in data else None) return trigger.activate() else: LOG.warning('Trigger %s received a bad signed message' % trigger_id) LOG.warning('message: %s' % data['message']) LOG.warning('address: %s' % data['address']) LOG.warning('signature: %s' % data['signature']) return {'error': 'Signature is invalid!'}
def __init__(self): super(SpellbookRESTAPI, self).__init__() # Initialize variables self.host = get_host() self.port = get_port() # Log the requests to the REST API in a separate file by installing a custom LoggingPlugin self.install(self.log_to_logger) # Make sure that an api_keys.json file is present, the first time the server is started # a new random api key and secret pair will be generated if not os.path.isfile('json/private/api_keys.json'): LOG.info('Generating new API keys') initialize_api_keys_file() LOG.info('Starting Bitcoin Spellbook') try: get_hot_wallet() except Exception as ex: LOG.error('Unable to decrypt hot wallet: %s' % ex) sys.exit(1) LOG.info( 'To make the server run in the background: use Control-Z, then use command: bg %1' ) # Initialize the routes for the REST API self.route( '/', method='GET', callback=self.index ) # on linux this gets requested every minute or so, but not on windows self.route('/favicon.ico', method='GET', callback=self.get_favicon) # Route for ping, to test if server is online self.route('/spellbook/ping', method='GET', callback=self.ping) # Routes for managing blockexplorers self.route('/spellbook/explorers', method='GET', callback=self.get_explorers) self.route('/spellbook/explorers/<explorer_id:re:[a-zA-Z0-9_\-.]+>', method='POST', callback=self.save_explorer) self.route('/spellbook/explorers/<explorer_id:re:[a-zA-Z0-9_\-.]+>', method='GET', callback=self.get_explorer_config) self.route('/spellbook/explorers/<explorer_id:re:[a-zA-Z0-9_\-.]+>', method='DELETE', callback=self.delete_explorer) # Routes for retrieving data from the blockchain self.route('/spellbook/blocks/latest', method='GET', callback=self.get_latest_block) self.route('/spellbook/blocks/<height:int>', method='GET', callback=self.get_block_by_height) self.route('/spellbook/blocks/<block_hash:re:[a-f0-9]+>', method='GET', callback=self.get_block_by_hash) self.route('/spellbook/transactions/<txid:re:[a-f0-9]+>/prime_input', method='GET', callback=self.get_prime_input_address) self.route('/spellbook/transactions/<txid:re:[a-f0-9]+>', method='GET', callback=self.get_transaction) self.route( '/spellbook/addresses/<address:re:[a-zA-Z1-9]+>/transactions', method='GET', callback=self.get_transactions) self.route('/spellbook/addresses/<address:re:[a-zA-Z1-9]+>/balance', method='GET', callback=self.get_balance) self.route('/spellbook/addresses/<address:re:[a-zA-Z1-9]+>/utxos', method='GET', callback=self.get_utxos) # Routes for Simplified Inputs List (SIL) self.route('/spellbook/addresses/<address:re:[a-zA-Z1-9]+>/SIL', method='GET', callback=self.get_sil) # Routes for Profile self.route('/spellbook/addresses/<address:re:[a-zA-Z1-9]+>/profile', method='GET', callback=self.get_profile) # Routes for Simplified UTXO List (SUL) self.route('/spellbook/addresses/<address:re:[a-zA-Z1-9]+>/SUL', method='GET', callback=self.get_sul) # Routes for Linked Lists self.route('/spellbook/addresses/<address:re:[a-zA-Z1-9]+>/LAL', method='GET', callback=self.get_lal) self.route('/spellbook/addresses/<address:re:[a-zA-Z1-9]+>/LBL', method='GET', callback=self.get_lbl) self.route('/spellbook/addresses/<address:re:[a-zA-Z1-9]+>/LRL', method='GET', callback=self.get_lrl) self.route('/spellbook/addresses/<address:re:[a-zA-Z1-9]+>/LSL', method='GET', callback=self.get_lsl) # Routes for Random Address self.route('/spellbook/addresses/<address:re:[a-zA-Z1-9]+>/random/SIL', method='GET', callback=self.get_random_address_from_sil) self.route('/spellbook/addresses/<address:re:[a-zA-Z1-9]+>/random/LBL', method='GET', callback=self.get_random_address_from_lbl) self.route('/spellbook/addresses/<address:re:[a-zA-Z1-9]+>/random/LRL', method='GET', callback=self.get_random_address_from_lrl) self.route('/spellbook/addresses/<address:re:[a-zA-Z1-9]+>/random/LSL', method='GET', callback=self.get_random_address_from_lsl) # Routes for Triggers self.route('/spellbook/triggers', method='GET', callback=self.get_triggers) self.route('/spellbook/triggers/<trigger_id:re:[a-zA-Z0-9_\-.]+>', method='GET', callback=self.get_trigger) self.route('/spellbook/triggers/<trigger_id:re:[a-zA-Z0-9_\-.]+>', method='POST', callback=self.save_trigger) self.route('/spellbook/triggers/<trigger_id:re:[a-zA-Z0-9_\-.]+>', method='DELETE', callback=self.delete_trigger) self.route( '/spellbook/triggers/<trigger_id:re:[a-zA-Z0-9_\-.]+>/activate', method='GET', callback=self.activate_trigger) self.route( '/spellbook/triggers/<trigger_id:re:[a-zA-Z0-9_\-.]+>/message', method='POST', callback=self.verify_signed_message) self.route('/spellbook/triggers/<trigger_id:re:[a-zA-Z0-9_\-.]+>/get', method='GET', callback=self.http_get_request) self.route('/spellbook/triggers/<trigger_id:re:[a-zA-Z0-9_\-.]+>/post', method='POST', callback=self.http_post_request) self.route( '/spellbook/triggers/<trigger_id:re:[a-zA-Z0-9_\-.]+>/delete', method='DELETE', callback=self.http_delete_request) self.route( '/spellbook/triggers/<trigger_id:re:[a-zA-Z0-9_\-.]+>/check', method='GET', callback=self.check_trigger) self.route('/spellbook/check_triggers', method='GET', callback=self.check_all_triggers) # Additional routes for Rest API endpoints self.route('/api/<trigger_id:re:[a-zA-Z0-9_\-.]+>', method='GET', callback=self.http_get_request) self.route('/api/<trigger_id:re:[a-zA-Z0-9_\-.]+>', method='OPTIONS', callback=self.http_get_request) self.route('/api/<trigger_id:re:[a-zA-Z0-9_\-.]+>', method='POST', callback=self.http_post_request) self.route('/api/<trigger_id:re:[a-zA-Z0-9_\-.]+>', method='DELETE', callback=self.http_delete_request) self.route('/html/<trigger_id:re:[a-zA-Z0-9_\-.]+>', method='GET', callback=self.html_request) self.route('/api/<trigger_id:re:[a-zA-Z0-9_\-.]+>/message', method='POST', callback=self.verify_signed_message) self.route('/api/sign_message', method='POST', callback=self.sign_message) # Routes for QR image generation self.route('/api/qr', method='GET', callback=self.qr) # Routes for Actions self.route('/spellbook/actions', method='GET', callback=self.get_actions) self.route('/spellbook/actions/<action_id:re:[a-zA-Z0-9_\-.]+>', method='GET', callback=self.get_action) self.route('/spellbook/actions/<action_id:re:[a-zA-Z0-9_\-.]+>', method='POST', callback=self.save_action) self.route('/spellbook/actions/<action_id:re:[a-zA-Z0-9_\-.]+>', method='DELETE', callback=self.delete_action) self.route('/spellbook/actions/<action_id:re:[a-zA-Z0-9_\-.]+>/run', method='GET', callback=self.run_action) # Routes for retrieving log messages self.route('/spellbook/logs/<filter_string>', method='GET', callback=self.get_logs) # Routes for RevealSecret actions self.route('/spellbook/actions/<action_id:re:[a-zA-Z0-9_\-.]+>/reveal', method='GET', callback=self.get_reveal) # Check if there are explorers configured, this will also initialize the default explorers on first startup if len(get_explorers()) == 0: LOG.warning('No block explorers configured!') try: # start the webserver for the REST API if get_enable_ssl() is True: self.run(host=self.host, port=self.port, debug=False, server='sslwebserver') else: self.run(host=self.host, port=self.port, debug=True, server='cheroot') except Exception as ex: LOG.error('An exception occurred in the main loop: %s' % ex) error_traceback = traceback.format_exc() for line in error_traceback.split('\n'): LOG.error(line) if get_mail_on_exception() is True: variables = {'HOST': get_host(), 'TRACEBACK': error_traceback} body_template = os.path.join('server_exception') sendmail(recipients=get_notification_email(), subject='Main loop Exception occurred @ %s' % get_host(), body_template=body_template, variables=variables)
def sendmail(recipients, subject, body_template, variables=None, images=None, attachments=None): """ Send an email using the smtp settings in the spellbook.conf file :param recipients: Email address(es) of the recipient(s) separated by comma :param subject: The subject for the email :param body_template: The filename of the body template for the email without the extension (both txt and html versions will be searched for) :param variables: A dict containing the variables that will be replaced in the email body template The body template can contain variables like $MYVARIABLE$, if the dict contains a key MYVARIABLE (without $), then it will be replaced by the value of that key :param images: A dict containing the filename of the images that need to be embedded in the html email :param attachments: A dict containing the filename and path for each attachment that needs to be added to the email :return: True upon success, False upon failure """ if get_enable_smtp() is False: LOG.warning( 'SMTP is disabled, mail will not be sent! see spellbook configuration file' ) return True # Return true here so everything continues as normal # Load the smtp settings load_smtp_settings() if variables is None: variables = {} if images is None: images = {} if attachments is None: attachments = {} LOG.info('Creating new email with template %s' % body_template) # Create message container - the correct MIME type is multipart/alternative. msg = MIMEMultipart('alternative') msg['Subject'] = subject msg['From'] = FROM_ADDRESS msg['To'] = recipients.replace(',', ', ') html_template_filename = None txt_template_filename = None # Search the 'email-templates' and 'apps' directory for the template if os.path.isfile( os.path.join(TEMPLATE_DIR, '%s.html' % body_template) ): # First see if a html template is found in the main template directory html_template_filename = os.path.join(TEMPLATE_DIR, '%s.html' % body_template) elif os.path.isfile(os.path.join( APPS_DIR, '%s.html' % body_template)): # Check the app directory for a html template html_template_filename = os.path.join(APPS_DIR, '%s.html' % body_template) if os.path.isfile( os.path.join(TEMPLATE_DIR, '%s.txt' % body_template) ): # Then check if a txt template if found in the main template directory txt_template_filename = os.path.join(TEMPLATE_DIR, '%s.txt' % body_template) elif os.path.isfile( os.path.join(APPS_DIR, '%s.txt' % body_template) ): # Lastly, check the app directory for a txt template txt_template_filename = os.path.join(APPS_DIR, '%s.txt' % body_template) if html_template_filename is None and txt_template_filename is None: LOG.error('Template %s for email not found!' % body_template) return False html_body = '' if html_template_filename is not None: try: with open(html_template_filename, 'r') as input_file: html_body = input_file.read() except Exception as ex: LOG.error('Unable to read template %s: %s' % (html_template_filename, ex)) return False txt_body = '' if txt_template_filename is not None: try: with open(txt_template_filename, 'r') as input_file: txt_body = input_file.read() except Exception as ex: LOG.error('Unable to read template %s: %s' % (txt_template_filename, ex)) return False # Replace all placeholder values in the body like $myvariable$ with the correct value for variable, value in variables.items(): html_body = html_body.replace('$%s$' % str(variable), str(value)) txt_body = txt_body.replace('$%s$' % str(variable), str(value)) # Record the MIME types of both parts - text/plain and text/html. part1 = MIMEText(txt_body, 'plain') part2 = MIMEText(html_body, 'html') # Attach parts into message container. # According to RFC 2046, the last part of a multipart message, in this case # the HTML message, is best and preferred. if txt_body != '': msg.attach(part1) if html_body != '': msg.attach(part2) # Attach all images that are referenced in the html email template for image_name, image_file in images.items(): LOG.info('adding image %s' % image_file) try: fp = open(image_file, 'rb') mime_image = MIMEImage(fp.read()) fp.close() # Define the image's ID as referenced in the template mime_image.add_header('Content-ID', '<%s>' % image_name) mime_image.add_header('content-disposition', 'attachment', filename=image_name) msg.attach(mime_image) except Exception as ex: LOG.error('Unable to add image %s to email: %s' % (image_name, ex)) # Attach all attachments for attachment_name, attachment_file in attachments.items(): LOG.info('adding attachment %s' % attachment_file) try: fp = open(attachment_file, 'rb') mime_file = MIMEBase('application', "octet-stream") mime_file.set_payload(fp.read()) Encoders.encode_base64(mime_file) fp.close() # Define the image's ID as referenced in the template mime_file.add_header('content-disposition', 'attachment', filename=attachment_name) msg.attach(mime_file) except Exception as ex: LOG.error('Unable to add attachment %s to email: %s' % (attachment_name, ex)) # Attempt to connect to the smtp server and send the message. try: # Start the smtp session session = smtplib.SMTP(HOST, PORT) session.ehlo() session.starttls() session.ehlo() session.login(USER, PASSWORD) # Send the message and quit session.sendmail(FROM_ADDRESS, recipients.split(','), msg.as_string()) session.quit() LOG.info('Email sent to %s : %s (template: %s)' % (recipients.split(','), subject, body_template)) return True except Exception as ex: LOG.error('Failed sending mail: %s' % ex) return False