api.py.bak
1 # Copyright (c) 2012-2016 Jonathan Warren 2 # Copyright (c) 2012-2023 The Bitmessage developers 3 4 """ 5 This is not what you run to start the Bitmessage API. 6 Instead, `enable the API <https://bitmessage.org/wiki/API>`_ 7 and optionally `enable daemon mode <https://bitmessage.org/wiki/Daemon>`_ 8 then run the PyBitmessage. 9 10 The PyBitmessage API is provided either as 11 `XML-RPC <http://xmlrpc.scripting.com/spec.html>`_ or 12 `JSON-RPC <https://www.jsonrpc.org/specification>`_ like in bitcoin. 13 It's selected according to 'apivariant' setting in config file. 14 15 Special value ``apivariant=legacy`` is to mimic the old pre 0.6.3 16 behaviour when any results are returned as strings of json. 17 18 .. list-table:: All config settings related to API: 19 :header-rows: 0 20 21 * - apienabled = true 22 - if 'false' the `singleAPI` wont start 23 * - apiinterface = 127.0.0.1 24 - this is the recommended default 25 * - apiport = 8442 26 - the API listens apiinterface:apiport if apiport is not used, 27 random in range (32767, 65535) otherwice 28 * - apivariant = xml 29 - current default for backward compatibility, 'json' is recommended 30 * - apiusername = username 31 - set the username 32 * - apipassword = password 33 - and the password 34 * - apinotifypath = 35 - not really the API setting, this sets a path for the executable to be ran 36 when certain internal event happens 37 38 To use the API concider such simple example: 39 40 .. code-block:: python 41 42 from jsonrpclib import jsonrpc 43 44 from pybitmessage import helper_startup 45 from pybitmessage.bmconfigparser import config 46 47 helper_startup.loadConfig() # find and load local config file 48 api_uri = "http://%s:%s@127.0.0.1:%s/" % ( 49 config.safeGet('bitmessagesettings', 'apiusername'), 50 config.safeGet('bitmessagesettings', 'apipassword'), 51 config.safeGet('bitmessagesettings', 'apiport') 52 ) 53 api = jsonrpc.ServerProxy(api_uri) 54 print(api.clientStatus()) 55 56 57 For further examples please reference `.tests.test_api`. 58 """ 59 60 import base64 61 import errno 62 import hashlib 63 import json 64 import random 65 import socket 66 import subprocess # nosec B404 67 import time 68 from binascii import hexlify, unhexlify 69 from struct import pack, unpack 70 71 import six 72 from six.moves import configparser, http_client, xmlrpc_server 73 from six.moves.reprlib import repr 74 75 import helper_inbox 76 import helper_sent 77 import proofofwork 78 import protocol 79 import queues 80 import shared 81 import shutdown 82 import state 83 from addresses import (addBMIfNotPresent, decodeAddress, decodeVarint, 84 varintDecodeError) 85 from bmconfigparser import config 86 from debug import logger 87 from defaults import (networkDefaultPayloadLengthExtraBytes, 88 networkDefaultProofOfWorkNonceTrialsPerByte) 89 from helper_sql import (SqlBulkExecute, sql_ready, sqlExecute, sqlQuery, 90 sqlStoredProcedure) 91 from highlevelcrypto import calculateInventoryHash 92 93 try: 94 from network import connectionpool 95 except ImportError: 96 connectionpool = None 97 98 from network import StoppableThread, invQueue, stats 99 from version import softwareVersion 100 101 try: # TODO: write tests for XML vulnerabilities 102 from defusedxml.xmlrpc import monkey_patch 103 except ImportError: 104 logger.warning( 105 'defusedxml not available, only use API on a secure, closed network.') 106 else: 107 monkey_patch() 108 109 110 str_chan = '[chan]' 111 str_broadcast_subscribers = '[Broadcast subscribers]' 112 113 114 class ErrorCodes(type): 115 """Metaclass for :class:`APIError` documenting error codes.""" 116 _CODES = { 117 0: 'Invalid command parameters number', 118 1: 'The specified passphrase is blank.', 119 2: 'The address version number currently must be 3, 4, or 0' 120 ' (which means auto-select).', 121 3: 'The stream number must be 1 (or 0 which means' 122 ' auto-select). Others aren\'t supported.', 123 4: 'Why would you ask me to generate 0 addresses for you?', 124 5: 'You have (accidentally?) specified too many addresses to' 125 ' make. Maximum 999. This check only exists to prevent' 126 ' mischief; if you really want to create more addresses than' 127 ' this, contact the Bitmessage developers and we can modify' 128 ' the check or you can do it yourself by searching the source' 129 ' code for this message.', 130 6: 'The encoding type must be 2 or 3.', 131 7: 'Could not decode address', 132 8: 'Checksum failed for address', 133 9: 'Invalid characters in address', 134 10: 'Address version number too high (or zero)', 135 11: 'The address version number currently must be 2, 3 or 4.' 136 ' Others aren\'t supported. Check the address.', 137 12: 'The stream number must be 1. Others aren\'t supported.' 138 ' Check the address.', 139 13: 'Could not find this address in your keys.dat file.', 140 14: 'Your fromAddress is disabled. Cannot send.', 141 15: 'Invalid ackData object size.', 142 16: 'You are already subscribed to that address.', 143 17: 'Label is not valid UTF-8 data.', 144 18: 'Chan name does not match address.', 145 19: 'The length of hash should be 32 bytes (encoded in hex' 146 ' thus 64 characters).', 147 20: 'Invalid method:', 148 21: 'Unexpected API Failure', 149 22: 'Decode error', 150 23: 'Bool expected in eighteenByteRipe', 151 24: 'Chan address is already present.', 152 25: 'Specified address is not a chan address.' 153 ' Use deleteAddress API call instead.', 154 26: 'Malformed varint in address: ', 155 27: 'Message is too long.', 156 28: 'Invalid parameter' 157 } 158 159 def __new__(mcs, name, bases, namespace): 160 result = super(ErrorCodes, mcs).__new__(mcs, name, bases, namespace) 161 for code in six.iteritems(mcs._CODES): 162 # beware: the formatting is adjusted for list-table 163 result.__doc__ += """ * - %04i 164 - %s 165 """ % code 166 return result 167 168 169 class APIError(xmlrpc_server.Fault): 170 """ 171 APIError exception class 172 173 .. list-table:: Possible error values 174 :header-rows: 1 175 :widths: auto 176 177 * - Error Number 178 - Message 179 """ 180 __metaclass__ = ErrorCodes 181 182 def __str__(self): 183 return "API Error %04i: %s" % (self.faultCode, self.faultString) 184 185 186 # This thread, of which there is only one, runs the API. 187 class singleAPI(StoppableThread): 188 """API thread""" 189 190 name = "singleAPI" 191 192 def stopThread(self): 193 super(singleAPI, self).stopThread() 194 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 195 try: 196 s.connect(( 197 config.get('bitmessagesettings', 'apiinterface'), 198 config.getint('bitmessagesettings', 'apiport') 199 )) 200 s.shutdown(socket.SHUT_RDWR) 201 s.close() 202 except BaseException: 203 pass 204 205 def run(self): 206 """ 207 The instance of `SimpleXMLRPCServer.SimpleXMLRPCServer` or 208 :class:`jsonrpclib.SimpleJSONRPCServer` is created and started here 209 with `BMRPCDispatcher` dispatcher. 210 """ 211 port = config.getint('bitmessagesettings', 'apiport') 212 try: 213 getattr(errno, 'WSAEADDRINUSE') 214 except AttributeError: 215 errno.WSAEADDRINUSE = errno.EADDRINUSE 216 217 RPCServerBase = xmlrpc_server.SimpleXMLRPCServer 218 ct = 'text/xml' 219 if config.safeGet( 220 'bitmessagesettings', 'apivariant') == 'json': 221 try: 222 from jsonrpclib.SimpleJSONRPCServer import \ 223 SimpleJSONRPCServer as RPCServerBase 224 except ImportError: 225 logger.warning( 226 'jsonrpclib not available, failing back to XML-RPC') 227 else: 228 ct = 'application/json-rpc' 229 230 # Nested class. FIXME not found a better solution. 231 class StoppableRPCServer(RPCServerBase): 232 """A SimpleXMLRPCServer that honours state.shutdown""" 233 allow_reuse_address = True 234 content_type = ct 235 236 def serve_forever(self, poll_interval=None): 237 """Start the RPCServer""" 238 sql_ready.wait() 239 while state.shutdown == 0: 240 self.handle_request() 241 242 for attempt in range(50): 243 try: 244 if attempt > 0: 245 logger.warning( 246 'Failed to start API listener on port %s', port) 247 port = random.randint(32767, 65535) # nosec B311 248 se = StoppableRPCServer( 249 (config.get( 250 'bitmessagesettings', 'apiinterface'), 251 port), 252 BMXMLRPCRequestHandler, True, encoding='UTF-8') 253 except socket.error as e: 254 if e.errno in (errno.EADDRINUSE, errno.WSAEADDRINUSE): 255 continue 256 else: 257 if attempt > 0: 258 logger.warning('Setting apiport to %s', port) 259 config.set( 260 'bitmessagesettings', 'apiport', str(port)) 261 config.save() 262 break 263 264 se.register_instance(BMRPCDispatcher()) 265 se.register_introspection_functions() 266 267 apiNotifyPath = config.safeGet( 268 'bitmessagesettings', 'apinotifypath') 269 270 if apiNotifyPath: 271 logger.info('Trying to call %s', apiNotifyPath) 272 try: 273 subprocess.call([apiNotifyPath, "startingUp"]) # nosec B603 274 except OSError: 275 logger.warning( 276 'Failed to call %s, removing apinotifypath setting', 277 apiNotifyPath) 278 config.remove_option( 279 'bitmessagesettings', 'apinotifypath') 280 281 se.serve_forever() 282 283 284 class CommandHandler(type): 285 """ 286 The metaclass for `BMRPCDispatcher` which fills _handlers dict by 287 methods decorated with @command 288 """ 289 def __new__(mcs, name, bases, namespace): 290 # pylint: disable=protected-access 291 result = super(CommandHandler, mcs).__new__( 292 mcs, name, bases, namespace) 293 result.config = config 294 result._handlers = {} 295 apivariant = result.config.safeGet('bitmessagesettings', 'apivariant') 296 for func in namespace.values(): 297 try: 298 for alias in getattr(func, '_cmd'): 299 try: 300 prefix, alias = alias.split(':') 301 if apivariant != prefix: 302 continue 303 except ValueError: 304 pass 305 result._handlers[alias] = func 306 except AttributeError: 307 pass 308 return result 309 310 311 class testmode(object): # pylint: disable=too-few-public-methods 312 """Decorator to check testmode & route to command decorator""" 313 314 def __init__(self, *aliases): 315 self.aliases = aliases 316 317 def __call__(self, func): 318 """Testmode call method""" 319 320 if not state.testmode: 321 return None 322 return command(self.aliases[0]).__call__(func) 323 324 325 class command(object): # pylint: disable=too-few-public-methods 326 """Decorator for API command method""" 327 def __init__(self, *aliases): 328 self.aliases = aliases 329 330 def __call__(self, func): 331 332 if config.safeGet( 333 'bitmessagesettings', 'apivariant') == 'legacy': 334 def wrapper(*args): 335 """ 336 A wrapper for legacy apivariant which dumps the result 337 into string of json 338 """ 339 result = func(*args) 340 return result if isinstance(result, (int, str)) \ 341 else json.dumps(result, indent=4) 342 wrapper.__doc__ = func.__doc__ 343 else: 344 wrapper = func 345 # pylint: disable=protected-access 346 wrapper._cmd = self.aliases 347 wrapper.__doc__ = """Commands: *%s* 348 349 """ % ', '.join(self.aliases) + wrapper.__doc__.lstrip() 350 return wrapper 351 352 353 # This is one of several classes that constitute the API 354 # This class was written by Vaibhav Bhatia. 355 # Modified by Jonathan Warren (Atheros). 356 # Further modified by the Bitmessage developers 357 # http://code.activestate.com/recipes/501148 358 class BMXMLRPCRequestHandler(xmlrpc_server.SimpleXMLRPCRequestHandler): 359 """The main API handler""" 360 361 # pylint: disable=protected-access 362 def do_POST(self): 363 """ 364 Handles the HTTP POST request. 365 366 Attempts to interpret all HTTP POST requests as XML-RPC calls, 367 which are forwarded to the server's _dispatch method for handling. 368 369 .. note:: this method is the same as in 370 `SimpleXMLRPCServer.SimpleXMLRPCRequestHandler`, 371 just hacked to handle cookies 372 """ 373 374 # Check that the path is legal 375 if not self.is_rpc_path_valid(): 376 self.report_404() 377 return 378 379 try: 380 # Get arguments by reading body of request. 381 # We read this in chunks to avoid straining 382 # socket.read(); around the 10 or 15Mb mark, some platforms 383 # begin to have problems (bug #792570). 384 max_chunk_size = 10 * 1024 * 1024 385 size_remaining = int(self.headers["content-length"]) 386 L = [] 387 while size_remaining: 388 chunk_size = min(size_remaining, max_chunk_size) 389 chunk = self.rfile.read(chunk_size) 390 if not chunk: 391 break 392 L.append(chunk) 393 size_remaining -= len(L[-1]) 394 data = b''.join(L) 395 396 # data = self.decode_request_content(data) 397 # pylint: disable=attribute-defined-outside-init 398 self.cookies = [] 399 400 validuser = self.APIAuthenticateClient() 401 if not validuser: 402 time.sleep(2) 403 self.send_response(http_client.UNAUTHORIZED) 404 self.end_headers() 405 return 406 # "RPC Username or password incorrect or HTTP header" 407 # " lacks authentication at all." 408 else: 409 # In previous versions of SimpleXMLRPCServer, _dispatch 410 # could be overridden in this class, instead of in 411 # SimpleXMLRPCDispatcher. To maintain backwards compatibility, 412 # check to see if a subclass implements _dispatch and dispatch 413 # using that method if present. 414 415 response = self.server._marshaled_dispatch( 416 data, getattr(self, '_dispatch', None) 417 ) 418 except Exception: # This should only happen if the module is buggy 419 # internal error, report as HTTP server error 420 self.send_response(http_client.INTERNAL_SERVER_ERROR) 421 self.end_headers() 422 else: 423 # got a valid XML RPC response 424 self.send_response(http_client.OK) 425 self.send_header("Content-type", self.server.content_type) 426 self.send_header("Content-length", str(len(response))) 427 428 # HACK :start -> sends cookies here 429 if self.cookies: 430 for cookie in self.cookies: 431 self.send_header('Set-Cookie', cookie.output(header='')) 432 # HACK :end 433 434 self.end_headers() 435 self.wfile.write(response) 436 437 # shut down the connection 438 self.wfile.flush() 439 self.connection.shutdown(1) 440 441 # actually handle shutdown command after sending response 442 if state.shutdown is False: 443 shutdown.doCleanShutdown() 444 445 def APIAuthenticateClient(self): 446 """ 447 Predicate to check for valid API credentials in the request header 448 """ 449 450 if 'Authorization' in self.headers: 451 # handle Basic authentication 452 encstr = self.headers.get('Authorization').split()[1] 453 emailid, password = base64.b64decode( 454 encstr).decode('utf-8').split(':') 455 return ( 456 emailid == config.get( 457 'bitmessagesettings', 'apiusername' 458 ) and password == config.get( 459 'bitmessagesettings', 'apipassword')) 460 else: 461 logger.warning( 462 'Authentication failed because header lacks' 463 ' Authentication field') 464 time.sleep(2) 465 466 return False 467 468 469 # pylint: disable=no-self-use,no-member,too-many-public-methods 470 @six.add_metaclass(CommandHandler) 471 class BMRPCDispatcher(object): 472 """This class is used to dispatch API commands""" 473 474 @staticmethod 475 def _decode(text, decode_type): 476 try: 477 if decode_type == 'hex': 478 return unhexlify(text) 479 elif decode_type == 'base64': 480 return base64.b64decode(text) 481 except Exception as e: 482 raise APIError( 483 22, 'Decode error - %s. Had trouble while decoding string: %r' 484 % (e, text) 485 ) 486 487 def _verifyAddress(self, address): 488 status, addressVersionNumber, streamNumber, ripe = \ 489 decodeAddress(address) 490 if status != 'success': 491 if status == 'checksumfailed': 492 raise APIError(8, 'Checksum failed for address: ' + address) 493 if status == 'invalidcharacters': 494 raise APIError(9, 'Invalid characters in address: ' + address) 495 if status == 'versiontoohigh': 496 raise APIError( 497 10, 'Address version number too high (or zero) in address: ' 498 + address) 499 if status == 'varintmalformed': 500 raise APIError(26, 'Malformed varint in address: ' + address) 501 raise APIError( 502 7, 'Could not decode address: %s : %s' % (address, status)) 503 if addressVersionNumber < 2 or addressVersionNumber > 4: 504 raise APIError( 505 11, 'The address version number currently must be 2, 3 or 4.' 506 ' Others aren\'t supported. Check the address.' 507 ) 508 if streamNumber != 1: 509 raise APIError( 510 12, 'The stream number must be 1. Others aren\'t supported.' 511 ' Check the address.' 512 ) 513 514 return { 515 'status': status, 516 'addressVersion': addressVersionNumber, 517 'streamNumber': streamNumber, 518 'ripe': base64.b64encode(ripe) 519 } if self._method == 'decodeAddress' else ( 520 status, addressVersionNumber, streamNumber, ripe) 521 522 @staticmethod 523 def _dump_inbox_message( 524 msgid, toAddress, fromAddress, subject, received, 525 message, encodingtype, read): 526 subject = shared.fixPotentiallyInvalidUTF8Data(subject) 527 message = shared.fixPotentiallyInvalidUTF8Data(message) 528 return { 529 'msgid': hexlify(msgid), 530 'toAddress': toAddress, 531 'fromAddress': fromAddress, 532 'subject': base64.b64encode(subject), 533 'message': base64.b64encode(message), 534 'encodingType': encodingtype, 535 'receivedTime': received, 536 'read': read 537 } 538 539 @staticmethod 540 def _dump_sent_message( # pylint: disable=too-many-arguments 541 msgid, toAddress, fromAddress, subject, lastactiontime, 542 message, encodingtype, status, ackdata): 543 subject = shared.fixPotentiallyInvalidUTF8Data(subject) 544 message = shared.fixPotentiallyInvalidUTF8Data(message) 545 return { 546 'msgid': hexlify(msgid), 547 'toAddress': toAddress, 548 'fromAddress': fromAddress, 549 'subject': base64.b64encode(subject), 550 'message': base64.b64encode(message), 551 'encodingType': encodingtype, 552 'lastActionTime': lastactiontime, 553 'status': status, 554 'ackData': hexlify(ackdata) 555 } 556 557 @staticmethod 558 def _blackwhitelist_entries(kind='black'): 559 queryreturn = sqlQuery( 560 "SELECT label, address FROM %slist WHERE enabled = 1" % kind 561 ) 562 data = [ 563 {'label': base64.b64encode( 564 shared.fixPotentiallyInvalidUTF8Data(label)), 565 'address': address} for label, address in queryreturn 566 ] 567 return {'addresses': data} 568 569 def _blackwhitelist_add(self, address, label, kind='black'): 570 label = self._decode(label, "base64") 571 address = addBMIfNotPresent(address) 572 self._verifyAddress(address) 573 queryreturn = sqlQuery( 574 "SELECT address FROM %slist WHERE address=?" % kind, address) 575 if queryreturn != []: 576 sqlExecute( 577 "UPDATE %slist SET label=?, enabled=1 WHERE address=?" % kind, 578 address) 579 else: 580 sqlExecute( 581 "INSERT INTO %slist VALUES (?,?,1)" % kind, label, address) 582 queues.UISignalQueue.put(('rerenderBlackWhiteList', '')) 583 584 def _blackwhitelist_del(self, address, kind='black'): 585 address = addBMIfNotPresent(address) 586 self._verifyAddress(address) 587 sqlExecute("DELETE FROM %slist WHERE address=?" % kind, address) 588 queues.UISignalQueue.put(('rerenderBlackWhiteList', '')) 589 590 # Request Handlers 591 592 @command('decodeAddress') 593 def HandleDecodeAddress(self, address): 594 """ 595 Decode given address and return dict with 596 status, addressVersion, streamNumber and ripe keys 597 """ 598 return self._verifyAddress(address) 599 600 @command('listAddresses', 'listAddresses2') 601 def HandleListAddresses(self): 602 """ 603 Returns dict with a list of all used addresses with their properties 604 in the *addresses* key. 605 """ 606 data = [] 607 for address in self.config.addresses(): 608 streamNumber = decodeAddress(address)[2] 609 label = self.config.get(address, 'label') 610 if self._method == 'listAddresses2': 611 label = base64.b64encode(label) 612 data.append({ 613 'label': label, 614 'address': address, 615 'stream': streamNumber, 616 'enabled': self.config.safeGetBoolean(address, 'enabled'), 617 'chan': self.config.safeGetBoolean(address, 'chan') 618 }) 619 return {'addresses': data} 620 621 # the listAddressbook alias should be removed eventually. 622 @command('listAddressBookEntries', 'legacy:listAddressbook') 623 def HandleListAddressBookEntries(self, label=None): 624 """ 625 Returns dict with a list of all address book entries (address and label) 626 in the *addresses* key. 627 """ 628 queryreturn = sqlQuery( 629 "SELECT label, address from addressbook WHERE label = ?", 630 label 631 ) if label else sqlQuery("SELECT label, address from addressbook") 632 data = [] 633 for label, address in queryreturn: 634 label = shared.fixPotentiallyInvalidUTF8Data(label) 635 data.append({ 636 'label': base64.b64encode(label), 637 'address': address 638 }) 639 return {'addresses': data} 640 641 # the addAddressbook alias should be deleted eventually. 642 @command('addAddressBookEntry', 'legacy:addAddressbook') 643 def HandleAddAddressBookEntry(self, address, label): 644 """Add an entry to address book. label must be base64 encoded.""" 645 label = self._decode(label, "base64") 646 address = addBMIfNotPresent(address) 647 self._verifyAddress(address) 648 # TODO: add unique together constraint in the table 649 queryreturn = sqlQuery( 650 "SELECT address FROM addressbook WHERE address=?", address) 651 if queryreturn != []: 652 raise APIError( 653 16, 'You already have this address in your address book.') 654 655 sqlExecute("INSERT INTO addressbook VALUES(?,?)", label, address) 656 queues.UISignalQueue.put(('rerenderMessagelistFromLabels', '')) 657 queues.UISignalQueue.put(('rerenderMessagelistToLabels', '')) 658 queues.UISignalQueue.put(('rerenderAddressBook', '')) 659 return "Added address %s to address book" % address 660 661 # the deleteAddressbook alias should be deleted eventually. 662 @command('deleteAddressBookEntry', 'legacy:deleteAddressbook') 663 def HandleDeleteAddressBookEntry(self, address): 664 """Delete an entry from address book.""" 665 address = addBMIfNotPresent(address) 666 self._verifyAddress(address) 667 sqlExecute('DELETE FROM addressbook WHERE address=?', address) 668 queues.UISignalQueue.put(('rerenderMessagelistFromLabels', '')) 669 queues.UISignalQueue.put(('rerenderMessagelistToLabels', '')) 670 queues.UISignalQueue.put(('rerenderAddressBook', '')) 671 return "Deleted address book entry for %s if it existed" % address 672 673 @command('getBlackWhitelistKind') 674 def HandleGetBlackWhitelistKind(self): 675 """Get the list kind set in config - black or white.""" 676 return self.config.get('bitmessagesettings', 'blackwhitelist') 677 678 @command('setBlackWhitelistKind') 679 def HandleSetBlackWhitelistKind(self, kind): 680 """Set the list kind used - black or white.""" 681 blackwhitelist_kinds = ('black', 'white') 682 if kind not in blackwhitelist_kinds: 683 raise APIError( 684 28, 'Invalid kind, should be one of %s' 685 % (blackwhitelist_kinds,)) 686 return self.config.set('bitmessagesettings', 'blackwhitelist', kind) 687 688 @command('listBlacklistEntries') 689 def HandleListBlacklistEntries(self): 690 """ 691 Returns dict with a list of all blacklist entries (address and label) 692 in the *addresses* key. 693 """ 694 return self._blackwhitelist_entries('black') 695 696 @command('listWhitelistEntries') 697 def HandleListWhitelistEntries(self): 698 """ 699 Returns dict with a list of all whitelist entries (address and label) 700 in the *addresses* key. 701 """ 702 return self._blackwhitelist_entries('white') 703 704 @command('addBlacklistEntry') 705 def HandleAddBlacklistEntry(self, address, label): 706 """Add an entry to blacklist. label must be base64 encoded.""" 707 self._blackwhitelist_add(address, label, 'black') 708 return "Added address %s to blacklist" % address 709 710 @command('addWhitelistEntry') 711 def HandleAddWhitelistEntry(self, address, label): 712 """Add an entry to whitelist. label must be base64 encoded.""" 713 self._blackwhitelist_add(address, label, 'white') 714 return "Added address %s to whitelist" % address 715 716 @command('deleteBlacklistEntry') 717 def HandleDeleteBlacklistEntry(self, address): 718 """Delete an entry from blacklist.""" 719 self._blackwhitelist_del(address, 'black') 720 return "Deleted blacklist entry for %s if it existed" % address 721 722 @command('deleteWhitelistEntry') 723 def HandleDeleteWhitelistEntry(self, address): 724 """Delete an entry from whitelist.""" 725 self._blackwhitelist_del(address, 'white') 726 return "Deleted whitelist entry for %s if it existed" % address 727 728 @command('createRandomAddress') 729 def HandleCreateRandomAddress( 730 self, label, eighteenByteRipe=False, totalDifficulty=0, 731 smallMessageDifficulty=0 732 ): 733 """ 734 Create one address using the random number generator. 735 736 :param str label: base64 encoded label for the address 737 :param bool eighteenByteRipe: is telling Bitmessage whether to 738 generate an address with an 18 byte RIPE hash 739 (as opposed to a 19 byte hash). 740 """ 741 742 nonceTrialsPerByte = self.config.get( 743 'bitmessagesettings', 'defaultnoncetrialsperbyte' 744 ) if not totalDifficulty else int( 745 networkDefaultProofOfWorkNonceTrialsPerByte * totalDifficulty) 746 payloadLengthExtraBytes = self.config.get( 747 'bitmessagesettings', 'defaultpayloadlengthextrabytes' 748 ) if not smallMessageDifficulty else int( 749 networkDefaultPayloadLengthExtraBytes * smallMessageDifficulty) 750 751 if not isinstance(eighteenByteRipe, bool): 752 raise APIError( 753 23, 'Bool expected in eighteenByteRipe, saw %s instead' 754 % type(eighteenByteRipe)) 755 label = self._decode(label, "base64") 756 try: 757 label.decode('utf-8') 758 except UnicodeDecodeError: 759 raise APIError(17, 'Label is not valid UTF-8 data.') 760 queues.apiAddressGeneratorReturnQueue.queue.clear() 761 # FIXME hard coded stream no 762 streamNumberForAddress = 1 763 queues.addressGeneratorQueue.put(( 764 'createRandomAddress', 4, streamNumberForAddress, label, 1, "", 765 eighteenByteRipe, nonceTrialsPerByte, payloadLengthExtraBytes 766 )) 767 return queues.apiAddressGeneratorReturnQueue.get() 768 769 @command('createDeterministicAddresses') 770 def HandleCreateDeterministicAddresses( 771 self, passphrase, numberOfAddresses=1, addressVersionNumber=0, 772 streamNumber=0, eighteenByteRipe=False, totalDifficulty=0, 773 smallMessageDifficulty=0 774 ): 775 """ 776 Create many addresses deterministically using the passphrase. 777 778 :param str passphrase: base64 encoded passphrase 779 :param int numberOfAddresses: number of addresses to create, 780 up to 999 781 782 *addressVersionNumber* and *streamNumber* may be set to 0 783 which will tell Bitmessage to use the most up-to-date 784 address version and the most available stream. 785 """ 786 787 nonceTrialsPerByte = self.config.get( 788 'bitmessagesettings', 'defaultnoncetrialsperbyte' 789 ) if not totalDifficulty else int( 790 networkDefaultProofOfWorkNonceTrialsPerByte * totalDifficulty) 791 payloadLengthExtraBytes = self.config.get( 792 'bitmessagesettings', 'defaultpayloadlengthextrabytes' 793 ) if not smallMessageDifficulty else int( 794 networkDefaultPayloadLengthExtraBytes * smallMessageDifficulty) 795 796 if not passphrase: 797 raise APIError(1, 'The specified passphrase is blank.') 798 if not isinstance(eighteenByteRipe, bool): 799 raise APIError( 800 23, 'Bool expected in eighteenByteRipe, saw %s instead' 801 % type(eighteenByteRipe)) 802 passphrase = self._decode(passphrase, "base64") 803 # 0 means "just use the proper addressVersionNumber" 804 if addressVersionNumber == 0: 805 addressVersionNumber = 4 806 if addressVersionNumber not in (3, 4): 807 raise APIError( 808 2, 'The address version number currently must be 3, 4, or 0' 809 ' (which means auto-select). %i isn\'t supported.' 810 % addressVersionNumber) 811 if streamNumber == 0: # 0 means "just use the most available stream" 812 streamNumber = 1 # FIXME hard coded stream no 813 if streamNumber != 1: 814 raise APIError( 815 3, 'The stream number must be 1 (or 0 which means' 816 ' auto-select). Others aren\'t supported.') 817 if numberOfAddresses == 0: 818 raise APIError( 819 4, 'Why would you ask me to generate 0 addresses for you?') 820 if numberOfAddresses > 999: 821 raise APIError( 822 5, 'You have (accidentally?) specified too many addresses to' 823 ' make. Maximum 999. This check only exists to prevent' 824 ' mischief; if you really want to create more addresses than' 825 ' this, contact the Bitmessage developers and we can modify' 826 ' the check or you can do it yourself by searching the source' 827 ' code for this message.') 828 queues.apiAddressGeneratorReturnQueue.queue.clear() 829 logger.debug( 830 'Requesting that the addressGenerator create %s addresses.', 831 numberOfAddresses) 832 queues.addressGeneratorQueue.put(( 833 'createDeterministicAddresses', addressVersionNumber, streamNumber, 834 'unused API address', numberOfAddresses, passphrase, 835 eighteenByteRipe, nonceTrialsPerByte, payloadLengthExtraBytes 836 )) 837 838 return {'addresses': queues.apiAddressGeneratorReturnQueue.get()} 839 840 @command('getDeterministicAddress') 841 def HandleGetDeterministicAddress( 842 self, passphrase, addressVersionNumber, streamNumber): 843 """ 844 Similar to *createDeterministicAddresses* except that the one 845 address that is returned will not be added to the Bitmessage 846 user interface or the keys.dat file. 847 """ 848 849 numberOfAddresses = 1 850 eighteenByteRipe = False 851 if not passphrase: 852 raise APIError(1, 'The specified passphrase is blank.') 853 passphrase = self._decode(passphrase, "base64") 854 if addressVersionNumber not in (3, 4): 855 raise APIError( 856 2, 'The address version number currently must be 3 or 4. %i' 857 ' isn\'t supported.' % addressVersionNumber) 858 if streamNumber != 1: 859 raise APIError( 860 3, ' The stream number must be 1. Others aren\'t supported.') 861 queues.apiAddressGeneratorReturnQueue.queue.clear() 862 logger.debug( 863 'Requesting that the addressGenerator create %s addresses.', 864 numberOfAddresses) 865 queues.addressGeneratorQueue.put(( 866 'getDeterministicAddress', addressVersionNumber, streamNumber, 867 'unused API address', numberOfAddresses, passphrase, 868 eighteenByteRipe 869 )) 870 return queues.apiAddressGeneratorReturnQueue.get() 871 872 @command('createChan') 873 def HandleCreateChan(self, passphrase): 874 """ 875 Creates a new chan. passphrase must be base64 encoded. 876 Returns the corresponding Bitmessage address. 877 """ 878 879 passphrase = self._decode(passphrase, "base64") 880 if not passphrase: 881 raise APIError(1, 'The specified passphrase is blank.') 882 # It would be nice to make the label the passphrase but it is 883 # possible that the passphrase contains non-utf-8 characters. 884 try: 885 passphrase.decode('utf-8') 886 label = str_chan + ' ' + passphrase 887 except UnicodeDecodeError: 888 label = str_chan + ' ' + repr(passphrase) 889 890 addressVersionNumber = 4 891 streamNumber = 1 892 queues.apiAddressGeneratorReturnQueue.queue.clear() 893 logger.debug( 894 'Requesting that the addressGenerator create chan %s.', passphrase) 895 queues.addressGeneratorQueue.put(( 896 'createChan', addressVersionNumber, streamNumber, label, 897 passphrase, True 898 )) 899 queueReturn = queues.apiAddressGeneratorReturnQueue.get() 900 try: 901 return queueReturn[0] 902 except IndexError: 903 raise APIError(24, 'Chan address is already present.') 904 905 @command('joinChan') 906 def HandleJoinChan(self, passphrase, suppliedAddress): 907 """ 908 Join a chan. passphrase must be base64 encoded. Returns 'success'. 909 """ 910 911 passphrase = self._decode(passphrase, "base64") 912 if not passphrase: 913 raise APIError(1, 'The specified passphrase is blank.') 914 # It would be nice to make the label the passphrase but it is 915 # possible that the passphrase contains non-utf-8 characters. 916 try: 917 passphrase.decode('utf-8') 918 label = str_chan + ' ' + passphrase 919 except UnicodeDecodeError: 920 label = str_chan + ' ' + repr(passphrase) 921 922 self._verifyAddress(suppliedAddress) 923 suppliedAddress = addBMIfNotPresent(suppliedAddress) 924 queues.apiAddressGeneratorReturnQueue.queue.clear() 925 queues.addressGeneratorQueue.put(( 926 'joinChan', suppliedAddress, label, passphrase, True 927 )) 928 queueReturn = queues.apiAddressGeneratorReturnQueue.get() 929 try: 930 if queueReturn[0] == 'chan name does not match address': 931 raise APIError(18, 'Chan name does not match address.') 932 except IndexError: 933 raise APIError(24, 'Chan address is already present.') 934 935 return "success" 936 937 @command('leaveChan') 938 def HandleLeaveChan(self, address): 939 """ 940 Leave a chan. Returns 'success'. 941 942 .. note:: at this time, the address is still shown in the UI 943 until a restart. 944 """ 945 self._verifyAddress(address) 946 address = addBMIfNotPresent(address) 947 if not self.config.safeGetBoolean(address, 'chan'): 948 raise APIError( 949 25, 'Specified address is not a chan address.' 950 ' Use deleteAddress API call instead.') 951 try: 952 self.config.remove_section(address) 953 except configparser.NoSectionError: 954 raise APIError( 955 13, 'Could not find this address in your keys.dat file.') 956 self.config.save() 957 queues.UISignalQueue.put(('rerenderMessagelistFromLabels', '')) 958 queues.UISignalQueue.put(('rerenderMessagelistToLabels', '')) 959 return "success" 960 961 @command('deleteAddress') 962 def HandleDeleteAddress(self, address): 963 """ 964 Permanently delete the address from keys.dat file. Returns 'success'. 965 """ 966 self._verifyAddress(address) 967 address = addBMIfNotPresent(address) 968 try: 969 self.config.remove_section(address) 970 except configparser.NoSectionError: 971 raise APIError( 972 13, 'Could not find this address in your keys.dat file.') 973 self.config.save() 974 queues.UISignalQueue.put(('writeNewAddressToTable', ('', '', ''))) 975 shared.reloadMyAddressHashes() 976 return "success" 977 978 @command('enableAddress') 979 def HandleEnableAddress(self, address, enable=True): 980 """Enable or disable the address depending on the *enable* value""" 981 self._verifyAddress(address) 982 address = addBMIfNotPresent(address) 983 config.set(address, 'enabled', str(enable)) 984 self.config.save() 985 shared.reloadMyAddressHashes() 986 return "success" 987 988 @command('getAllInboxMessages') 989 def HandleGetAllInboxMessages(self): 990 """ 991 Returns a dict with all inbox messages in the *inboxMessages* key. 992 The message is a dict with such keys: 993 *msgid*, *toAddress*, *fromAddress*, *subject*, *message*, 994 *encodingType*, *receivedTime*, *read*. 995 *msgid* is hex encoded string. 996 *subject* and *message* are base64 encoded. 997 """ 998 999 queryreturn = sqlQuery( 1000 "SELECT msgid, toaddress, fromaddress, subject, received, message," 1001 " encodingtype, read FROM inbox WHERE folder='inbox'" 1002 " ORDER BY received" 1003 ) 1004 return {"inboxMessages": [ 1005 self._dump_inbox_message(*data) for data in queryreturn 1006 ]} 1007 1008 @command('getAllInboxMessageIds', 'getAllInboxMessageIDs') 1009 def HandleGetAllInboxMessageIds(self): 1010 """ 1011 The same as *getAllInboxMessages* but returns only *msgid*s, 1012 result key - *inboxMessageIds*. 1013 """ 1014 1015 queryreturn = sqlQuery( 1016 "SELECT msgid FROM inbox where folder='inbox' ORDER BY received") 1017 1018 return {"inboxMessageIds": [ 1019 {'msgid': hexlify(msgid)} for msgid, in queryreturn 1020 ]} 1021 1022 @command('getInboxMessageById', 'getInboxMessageByID') 1023 def HandleGetInboxMessageById(self, hid, readStatus=None): 1024 """ 1025 Returns a dict with list containing single message in the result 1026 key *inboxMessage*. May also return None if message was not found. 1027 1028 :param str hid: hex encoded msgid 1029 :param bool readStatus: sets the message's read status if present 1030 """ 1031 1032 msgid = self._decode(hid, "hex") 1033 if readStatus is not None: 1034 if not isinstance(readStatus, bool): 1035 raise APIError( 1036 23, 'Bool expected in readStatus, saw %s instead.' 1037 % type(readStatus)) 1038 queryreturn = sqlQuery( 1039 "SELECT read FROM inbox WHERE msgid=?", msgid) 1040 # UPDATE is slow, only update if status is different 1041 try: 1042 if (queryreturn[0][0] == 1) != readStatus: 1043 sqlExecute( 1044 "UPDATE inbox set read = ? WHERE msgid=?", 1045 readStatus, msgid) 1046 queues.UISignalQueue.put(('changedInboxUnread', None)) 1047 except IndexError: 1048 pass 1049 queryreturn = sqlQuery( 1050 "SELECT msgid, toaddress, fromaddress, subject, received, message," 1051 " encodingtype, read FROM inbox WHERE msgid=?", msgid 1052 ) 1053 try: 1054 return {"inboxMessage": [ 1055 self._dump_inbox_message(*queryreturn[0])]} 1056 except IndexError: 1057 pass # FIXME inconsistent 1058 1059 @command('getAllSentMessages') 1060 def HandleGetAllSentMessages(self): 1061 """ 1062 The same as *getAllInboxMessages* but for sent, 1063 result key - *sentMessages*. Message dict keys are: 1064 *msgid*, *toAddress*, *fromAddress*, *subject*, *message*, 1065 *encodingType*, *lastActionTime*, *status*, *ackData*. 1066 *ackData* is also a hex encoded string. 1067 """ 1068 1069 queryreturn = sqlQuery( 1070 "SELECT msgid, toaddress, fromaddress, subject, lastactiontime," 1071 " message, encodingtype, status, ackdata FROM sent" 1072 " WHERE folder='sent' ORDER BY lastactiontime" 1073 ) 1074 return {"sentMessages": [ 1075 self._dump_sent_message(*data) for data in queryreturn 1076 ]} 1077 1078 @command('getAllSentMessageIds', 'getAllSentMessageIDs') 1079 def HandleGetAllSentMessageIds(self): 1080 """ 1081 The same as *getAllInboxMessageIds* but for sent, 1082 result key - *sentMessageIds*. 1083 """ 1084 1085 queryreturn = sqlQuery( 1086 "SELECT msgid FROM sent WHERE folder='sent'" 1087 " ORDER BY lastactiontime" 1088 ) 1089 return {"sentMessageIds": [ 1090 {'msgid': hexlify(msgid)} for msgid, in queryreturn 1091 ]} 1092 1093 # after some time getInboxMessagesByAddress should be removed 1094 @command('getInboxMessagesByReceiver', 'legacy:getInboxMessagesByAddress') 1095 def HandleInboxMessagesByReceiver(self, toAddress): 1096 """ 1097 The same as *getAllInboxMessages* but returns only messages 1098 for toAddress. 1099 """ 1100 1101 queryreturn = sqlQuery( 1102 "SELECT msgid, toaddress, fromaddress, subject, received," 1103 " message, encodingtype, read FROM inbox WHERE folder='inbox'" 1104 " AND toAddress=?", toAddress) 1105 return {"inboxMessages": [ 1106 self._dump_inbox_message(*data) for data in queryreturn 1107 ]} 1108 1109 @command('getSentMessageById', 'getSentMessageByID') 1110 def HandleGetSentMessageById(self, hid): 1111 """ 1112 Similiar to *getInboxMessageById* but doesn't change message's 1113 read status (sent messages have no such field). 1114 Result key is *sentMessage* 1115 """ 1116 1117 msgid = self._decode(hid, "hex") 1118 queryreturn = sqlQuery( 1119 "SELECT msgid, toaddress, fromaddress, subject, lastactiontime," 1120 " message, encodingtype, status, ackdata FROM sent WHERE msgid=?", 1121 msgid 1122 ) 1123 try: 1124 return {"sentMessage": [ 1125 self._dump_sent_message(*queryreturn[0]) 1126 ]} 1127 except IndexError: 1128 pass # FIXME inconsistent 1129 1130 @command('getSentMessagesByAddress', 'getSentMessagesBySender') 1131 def HandleGetSentMessagesByAddress(self, fromAddress): 1132 """ 1133 The same as *getAllSentMessages* but returns only messages 1134 from fromAddress. 1135 """ 1136 1137 queryreturn = sqlQuery( 1138 "SELECT msgid, toaddress, fromaddress, subject, lastactiontime," 1139 " message, encodingtype, status, ackdata FROM sent" 1140 " WHERE folder='sent' AND fromAddress=? ORDER BY lastactiontime", 1141 fromAddress 1142 ) 1143 return {"sentMessages": [ 1144 self._dump_sent_message(*data) for data in queryreturn 1145 ]} 1146 1147 @command('getSentMessageByAckData') 1148 def HandleGetSentMessagesByAckData(self, ackData): 1149 """ 1150 Similiar to *getSentMessageById* but searches by ackdata 1151 (also hex encoded). 1152 """ 1153 1154 ackData = self._decode(ackData, "hex") 1155 queryreturn = sqlQuery( 1156 "SELECT msgid, toaddress, fromaddress, subject, lastactiontime," 1157 " message, encodingtype, status, ackdata FROM sent" 1158 " WHERE ackdata=?", ackData 1159 ) 1160 1161 try: 1162 return {"sentMessage": [ 1163 self._dump_sent_message(*queryreturn[0]) 1164 ]} 1165 except IndexError: 1166 pass # FIXME inconsistent 1167 1168 @command('trashMessage') 1169 def HandleTrashMessage(self, msgid): 1170 """ 1171 Trash message by msgid (encoded in hex). Returns a simple message 1172 saying that the message was trashed assuming it ever even existed. 1173 Prior existence is not checked. 1174 """ 1175 msgid = self._decode(msgid, "hex") 1176 # Trash if in inbox table 1177 helper_inbox.trash(msgid) 1178 # Trash if in sent table 1179 sqlExecute("UPDATE sent SET folder='trash' WHERE msgid=?", msgid) 1180 return 'Trashed message (assuming message existed).' 1181 1182 @command('trashInboxMessage') 1183 def HandleTrashInboxMessage(self, msgid): 1184 """Trash inbox message by msgid (encoded in hex).""" 1185 msgid = self._decode(msgid, "hex") 1186 helper_inbox.trash(msgid) 1187 return 'Trashed inbox message (assuming message existed).' 1188 1189 @command('trashSentMessage') 1190 def HandleTrashSentMessage(self, msgid): 1191 """Trash sent message by msgid (encoded in hex).""" 1192 msgid = self._decode(msgid, "hex") 1193 sqlExecute('''UPDATE sent SET folder='trash' WHERE msgid=?''', msgid) 1194 return 'Trashed sent message (assuming message existed).' 1195 1196 @command('sendMessage') 1197 def HandleSendMessage( 1198 self, toAddress, fromAddress, subject, message, 1199 encodingType=2, TTL=4 * 24 * 60 * 60 1200 ): 1201 """ 1202 Send the message and return ackdata (hex encoded string). 1203 subject and message must be encoded in base64 which may optionally 1204 include line breaks. TTL is specified in seconds; values outside 1205 the bounds of 3600 to 2419200 will be moved to be within those 1206 bounds. TTL defaults to 4 days. 1207 """ 1208 # pylint: disable=too-many-locals 1209 if encodingType not in (2, 3): 1210 raise APIError(6, 'The encoding type must be 2 or 3.') 1211 subject = self._decode(subject, "base64") 1212 message = self._decode(message, "base64") 1213 if len(subject + message) > (2 ** 18 - 500): 1214 raise APIError(27, 'Message is too long.') 1215 if TTL < 60 * 60: 1216 TTL = 60 * 60 1217 if TTL > 28 * 24 * 60 * 60: 1218 TTL = 28 * 24 * 60 * 60 1219 toAddress = addBMIfNotPresent(toAddress) 1220 fromAddress = addBMIfNotPresent(fromAddress) 1221 self._verifyAddress(fromAddress) 1222 try: 1223 fromAddressEnabled = self.config.getboolean(fromAddress, 'enabled') 1224 except configparser.NoSectionError: 1225 raise APIError( 1226 13, 'Could not find your fromAddress in the keys.dat file.') 1227 if not fromAddressEnabled: 1228 raise APIError(14, 'Your fromAddress is disabled. Cannot send.') 1229 1230 ackdata = helper_sent.insert( 1231 toAddress=toAddress, fromAddress=fromAddress, 1232 subject=subject, message=message, encoding=encodingType, ttl=TTL) 1233 1234 toLabel = '' 1235 queryreturn = sqlQuery( 1236 "SELECT label FROM addressbook WHERE address=?", toAddress) 1237 try: 1238 toLabel = queryreturn[0][0] 1239 except IndexError: 1240 pass 1241 1242 queues.UISignalQueue.put(('displayNewSentMessage', ( 1243 toAddress, toLabel, fromAddress, subject, message, ackdata))) 1244 queues.workerQueue.put(('sendmessage', toAddress)) 1245 1246 return hexlify(ackdata) 1247 1248 @command('sendBroadcast') 1249 def HandleSendBroadcast( 1250 self, fromAddress, subject, message, encodingType=2, 1251 TTL=4 * 24 * 60 * 60): 1252 """Send the broadcast message. Similiar to *sendMessage*.""" 1253 1254 if encodingType not in (2, 3): 1255 raise APIError(6, 'The encoding type must be 2 or 3.') 1256 1257 subject = self._decode(subject, "base64") 1258 message = self._decode(message, "base64") 1259 if len(subject + message) > (2 ** 18 - 500): 1260 raise APIError(27, 'Message is too long.') 1261 if TTL < 60 * 60: 1262 TTL = 60 * 60 1263 if TTL > 28 * 24 * 60 * 60: 1264 TTL = 28 * 24 * 60 * 60 1265 fromAddress = addBMIfNotPresent(fromAddress) 1266 self._verifyAddress(fromAddress) 1267 try: 1268 fromAddressEnabled = self.config.getboolean(fromAddress, 'enabled') 1269 except configparser.NoSectionError: 1270 raise APIError( 1271 13, 'Could not find your fromAddress in the keys.dat file.') 1272 if not fromAddressEnabled: 1273 raise APIError(14, 'Your fromAddress is disabled. Cannot send.') 1274 1275 toAddress = str_broadcast_subscribers 1276 1277 ackdata = helper_sent.insert( 1278 fromAddress=fromAddress, subject=subject, 1279 message=message, status='broadcastqueued', 1280 encoding=encodingType) 1281 1282 toLabel = str_broadcast_subscribers 1283 queues.UISignalQueue.put(('displayNewSentMessage', ( 1284 toAddress, toLabel, fromAddress, subject, message, ackdata))) 1285 queues.workerQueue.put(('sendbroadcast', '')) 1286 1287 return hexlify(ackdata) 1288 1289 @command('getStatus') 1290 def HandleGetStatus(self, ackdata): 1291 """ 1292 Get the status of sent message by its ackdata (hex encoded). 1293 Returns one of these strings: notfound, msgqueued, 1294 broadcastqueued, broadcastsent, doingpubkeypow, awaitingpubkey, 1295 doingmsgpow, forcepow, msgsent, msgsentnoackexpected or ackreceived. 1296 """ 1297 1298 if len(ackdata) < 76: 1299 # The length of ackData should be at least 38 bytes (76 hex digits) 1300 raise APIError(15, 'Invalid ackData object size.') 1301 ackdata = self._decode(ackdata, "hex") 1302 queryreturn = sqlQuery( 1303 "SELECT status FROM sent where ackdata=?", ackdata) 1304 try: 1305 return queryreturn[0][0] 1306 except IndexError: 1307 return 'notfound' 1308 1309 @command('addSubscription') 1310 def HandleAddSubscription(self, address, label=''): 1311 """Subscribe to the address. label must be base64 encoded.""" 1312 1313 if label: 1314 label = self._decode(label, "base64") 1315 try: 1316 label.decode('utf-8') 1317 except UnicodeDecodeError: 1318 raise APIError(17, 'Label is not valid UTF-8 data.') 1319 self._verifyAddress(address) 1320 address = addBMIfNotPresent(address) 1321 # First we must check to see if the address is already in the 1322 # subscriptions list. 1323 queryreturn = sqlQuery( 1324 "SELECT * FROM subscriptions WHERE address=?", address) 1325 if queryreturn: 1326 raise APIError(16, 'You are already subscribed to that address.') 1327 sqlExecute( 1328 "INSERT INTO subscriptions VALUES (?,?,?)", label, address, True) 1329 shared.reloadBroadcastSendersForWhichImWatching() 1330 queues.UISignalQueue.put(('rerenderMessagelistFromLabels', '')) 1331 queues.UISignalQueue.put(('rerenderSubscriptions', '')) 1332 return 'Added subscription.' 1333 1334 @command('deleteSubscription') 1335 def HandleDeleteSubscription(self, address): 1336 """ 1337 Unsubscribe from the address. The program does not check whether 1338 you were subscribed in the first place. 1339 """ 1340 1341 address = addBMIfNotPresent(address) 1342 sqlExecute("DELETE FROM subscriptions WHERE address=?", address) 1343 shared.reloadBroadcastSendersForWhichImWatching() 1344 queues.UISignalQueue.put(('rerenderMessagelistFromLabels', '')) 1345 queues.UISignalQueue.put(('rerenderSubscriptions', '')) 1346 return 'Deleted subscription if it existed.' 1347 1348 @command('listSubscriptions') 1349 def ListSubscriptions(self): 1350 """ 1351 Returns dict with a list of all subscriptions 1352 in the *subscriptions* key. 1353 """ 1354 1355 queryreturn = sqlQuery( 1356 "SELECT label, address, enabled FROM subscriptions") 1357 data = [] 1358 for label, address, enabled in queryreturn: 1359 label = shared.fixPotentiallyInvalidUTF8Data(label) 1360 data.append({ 1361 'label': base64.b64encode(label), 1362 'address': address, 1363 'enabled': enabled == 1 1364 }) 1365 return {'subscriptions': data} 1366 1367 @command('disseminatePreEncryptedMsg', 'disseminatePreparedObject') 1368 def HandleDisseminatePreparedObject( 1369 self, encryptedPayload, 1370 nonceTrialsPerByte=networkDefaultProofOfWorkNonceTrialsPerByte, 1371 payloadLengthExtraBytes=networkDefaultPayloadLengthExtraBytes 1372 ): 1373 """ 1374 Handle a request to disseminate an encrypted message. 1375 1376 The device issuing this command to PyBitmessage supplies an object 1377 that has already been encrypted but which may still need the PoW 1378 to be done. PyBitmessage accepts this object and sends it out 1379 to the rest of the Bitmessage network as if it had generated 1380 the message itself. 1381 1382 *encryptedPayload* is a hex encoded string starting with the nonce, 1383 8 zero bytes in case of no PoW done. 1384 """ 1385 encryptedPayload = self._decode(encryptedPayload, "hex") 1386 1387 nonce, = unpack('>Q', encryptedPayload[:8]) 1388 objectType, toStreamNumber, expiresTime = \ 1389 protocol.decodeObjectParameters(encryptedPayload) 1390 1391 if nonce == 0: # Let us do the POW and attach it to the front 1392 encryptedPayload = encryptedPayload[8:] 1393 TTL = expiresTime - time.time() + 300 # a bit of extra padding 1394 # Let us do the POW and attach it to the front 1395 logger.debug("expiresTime: %s", expiresTime) 1396 logger.debug("TTL: %s", TTL) 1397 logger.debug("objectType: %s", objectType) 1398 logger.info( 1399 '(For msg message via API) Doing proof of work. Total required' 1400 ' difficulty: %s\nRequired small message difficulty: %s', 1401 float(nonceTrialsPerByte) 1402 / networkDefaultProofOfWorkNonceTrialsPerByte, 1403 float(payloadLengthExtraBytes) 1404 / networkDefaultPayloadLengthExtraBytes, 1405 ) 1406 powStartTime = time.time() 1407 trialValue, nonce = proofofwork.calculate( 1408 encryptedPayload, TTL, 1409 nonceTrialsPerByte, payloadLengthExtraBytes 1410 ) 1411 logger.info( 1412 '(For msg message via API) Found proof of work %s\nNonce: %s\n' 1413 'POW took %s seconds. %s nonce trials per second.', 1414 trialValue, nonce, int(time.time() - powStartTime), 1415 nonce / (time.time() - powStartTime) 1416 ) 1417 encryptedPayload = pack('>Q', nonce) + encryptedPayload 1418 1419 inventoryHash = calculateInventoryHash(encryptedPayload) 1420 state.Inventory[inventoryHash] = ( 1421 objectType, toStreamNumber, encryptedPayload, expiresTime, b'') 1422 logger.info( 1423 'Broadcasting inv for msg(API disseminatePreEncryptedMsg' 1424 ' command): %s', hexlify(inventoryHash)) 1425 invQueue.put((toStreamNumber, inventoryHash)) 1426 return hexlify(inventoryHash).decode() 1427 1428 @command('trashSentMessageByAckData') 1429 def HandleTrashSentMessageByAckDAta(self, ackdata): 1430 """Trash a sent message by ackdata (hex encoded)""" 1431 # This API method should only be used when msgid is not available 1432 ackdata = self._decode(ackdata, "hex") 1433 sqlExecute("UPDATE sent SET folder='trash' WHERE ackdata=?", ackdata) 1434 return 'Trashed sent message (assuming message existed).' 1435 1436 @command('disseminatePubkey') 1437 def HandleDissimatePubKey(self, payload): 1438 """Handle a request to disseminate a public key""" 1439 1440 # The device issuing this command to PyBitmessage supplies a pubkey 1441 # object to be disseminated to the rest of the Bitmessage network. 1442 # PyBitmessage accepts this pubkey object and sends it out to the rest 1443 # of the Bitmessage network as if it had generated the pubkey object 1444 # itself. Please do not yet add this to the api doc. 1445 payload = self._decode(payload, "hex") 1446 1447 # Let us do the POW 1448 target = 2 ** 64 / (( 1449 len(payload) + networkDefaultPayloadLengthExtraBytes + 8 1450 ) * networkDefaultProofOfWorkNonceTrialsPerByte) 1451 logger.info('(For pubkey message via API) Doing proof of work...') 1452 initialHash = hashlib.sha512(payload).digest() 1453 trialValue, nonce = proofofwork.run(target, initialHash) 1454 logger.info( 1455 '(For pubkey message via API) Found proof of work %s Nonce: %s', 1456 trialValue, nonce 1457 ) 1458 payload = pack('>Q', nonce) + payload 1459 1460 pubkeyReadPosition = 8 # bypass the nonce 1461 if payload[pubkeyReadPosition:pubkeyReadPosition + 4] == \ 1462 '\x00\x00\x00\x00': # if this pubkey uses 8 byte time 1463 pubkeyReadPosition += 8 1464 else: 1465 pubkeyReadPosition += 4 1466 addressVersionLength = decodeVarint( 1467 payload[pubkeyReadPosition:pubkeyReadPosition + 10])[1] 1468 pubkeyReadPosition += addressVersionLength 1469 pubkeyStreamNumber = decodeVarint( 1470 payload[pubkeyReadPosition:pubkeyReadPosition + 10])[0] 1471 inventoryHash = calculateInventoryHash(payload) 1472 objectType = 1 # .. todo::: support v4 pubkeys 1473 TTL = 28 * 24 * 60 * 60 1474 state.Inventory[inventoryHash] = ( 1475 objectType, pubkeyStreamNumber, payload, int(time.time()) + TTL, '' 1476 ) 1477 logger.info( 1478 'broadcasting inv within API command disseminatePubkey with' 1479 ' hash: %s', hexlify(inventoryHash)) 1480 invQueue.put((pubkeyStreamNumber, inventoryHash)) 1481 1482 @command( 1483 'getMessageDataByDestinationHash', 'getMessageDataByDestinationTag') 1484 def HandleGetMessageDataByDestinationHash(self, requestedHash): 1485 """Handle a request to get message data by destination hash""" 1486 1487 # Method will eventually be used by a particular Android app to 1488 # select relevant messages. Do not yet add this to the api 1489 # doc. 1490 if len(requestedHash) != 32: 1491 raise APIError( 1492 19, 'The length of hash should be 32 bytes (encoded in hex' 1493 ' thus 64 characters).') 1494 requestedHash = self._decode(requestedHash, "hex") 1495 1496 # This is not a particularly commonly used API function. Before we 1497 # use it we'll need to fill out a field in our inventory database 1498 # which is blank by default (first20bytesofencryptedmessage). 1499 queryreturn = sqlQuery( 1500 "SELECT hash, payload FROM inventory WHERE tag = ''" 1501 " and objecttype = 2") 1502 with SqlBulkExecute() as sql: 1503 for hash01, payload in queryreturn: 1504 readPosition = 16 # Nonce length + time length 1505 # Stream Number length 1506 readPosition += decodeVarint( 1507 payload[readPosition:readPosition + 10])[1] 1508 t = (payload[readPosition:readPosition + 32], hash01) 1509 sql.execute("UPDATE inventory SET tag=? WHERE hash=?", *t) 1510 1511 queryreturn = sqlQuery( 1512 "SELECT payload FROM inventory WHERE tag = ?", requestedHash) 1513 return {"receivedMessageDatas": [ 1514 {'data': hexlify(payload)} for payload, in queryreturn 1515 ]} 1516 1517 @command('clientStatus') 1518 def HandleClientStatus(self): 1519 """ 1520 Returns the bitmessage status as dict with keys *networkConnections*, 1521 *numberOfMessagesProcessed*, *numberOfBroadcastsProcessed*, 1522 *numberOfPubkeysProcessed*, *pendingDownload*, *networkStatus*, 1523 *softwareName*, *softwareVersion*. *networkStatus* will be one of 1524 these strings: "notConnected", 1525 "connectedButHaveNotReceivedIncomingConnections", 1526 or "connectedAndReceivingIncomingConnections". 1527 """ 1528 1529 connections_num = len(stats.connectedHostsList()) 1530 1531 if connections_num == 0: 1532 networkStatus = 'notConnected' 1533 elif state.clientHasReceivedIncomingConnections: 1534 networkStatus = 'connectedAndReceivingIncomingConnections' 1535 else: 1536 networkStatus = 'connectedButHaveNotReceivedIncomingConnections' 1537 return { 1538 'networkConnections': connections_num, 1539 'numberOfMessagesProcessed': state.numberOfMessagesProcessed, 1540 'numberOfBroadcastsProcessed': state.numberOfBroadcastsProcessed, 1541 'numberOfPubkeysProcessed': state.numberOfPubkeysProcessed, 1542 'pendingDownload': stats.pendingDownload(), 1543 'networkStatus': networkStatus, 1544 'softwareName': 'PyBitmessage', 1545 'softwareVersion': softwareVersion 1546 } 1547 1548 @command('listConnections') 1549 def HandleListConnections(self): 1550 """ 1551 Returns bitmessage connection information as dict with keys *inbound*, 1552 *outbound*. 1553 """ 1554 if connectionpool is None: 1555 raise APIError(21, 'Could not import BMConnectionPool.') 1556 inboundConnections = [] 1557 outboundConnections = [] 1558 for i in connectionpool.pool.inboundConnections.values(): 1559 inboundConnections.append({ 1560 'host': i.destination.host, 1561 'port': i.destination.port, 1562 'fullyEstablished': i.fullyEstablished, 1563 'userAgent': str(i.userAgent) 1564 }) 1565 for i in connectionpool.pool.outboundConnections.values(): 1566 outboundConnections.append({ 1567 'host': i.destination.host, 1568 'port': i.destination.port, 1569 'fullyEstablished': i.fullyEstablished, 1570 'userAgent': str(i.userAgent) 1571 }) 1572 return { 1573 'inbound': inboundConnections, 1574 'outbound': outboundConnections 1575 } 1576 1577 @command('helloWorld') 1578 def HandleHelloWorld(self, a, b): 1579 """Test two string params""" 1580 return a + '-' + b 1581 1582 @command('add') 1583 def HandleAdd(self, a, b): 1584 """Test two numeric params""" 1585 return a + b 1586 1587 @command('statusBar') 1588 def HandleStatusBar(self, message): 1589 """Update GUI statusbar message""" 1590 queues.UISignalQueue.put(('updateStatusBar', message)) 1591 return "success" 1592 1593 @testmode('undeleteMessage') 1594 def HandleUndeleteMessage(self, msgid): 1595 """Undelete message""" 1596 msgid = self._decode(msgid, "hex") 1597 helper_inbox.undeleteMessage(msgid) 1598 return "Undeleted message" 1599 1600 @command('deleteAndVacuum') 1601 def HandleDeleteAndVacuum(self): 1602 """Cleanup trashes and vacuum messages database""" 1603 sqlStoredProcedure('deleteandvacuume') 1604 return 'done' 1605 1606 @command('shutdown') 1607 def HandleShutdown(self): 1608 """Shutdown the bitmessage. Returns 'done'.""" 1609 # backward compatible trick because False == 0 is True 1610 state.shutdown = False 1611 return 'done' 1612 1613 def _handle_request(self, method, params): 1614 try: 1615 # pylint: disable=attribute-defined-outside-init 1616 self._method = method 1617 func = self._handlers[method] 1618 return func(self, *params) 1619 except KeyError: 1620 raise APIError(20, 'Invalid method: %s' % method) 1621 except TypeError as e: 1622 msg = 'Unexpected API Failure - %s' % e 1623 if 'argument' not in str(e): 1624 raise APIError(21, msg) 1625 argcount = len(params) 1626 maxcount = func.func_code.co_argcount 1627 if argcount > maxcount: 1628 msg = ( 1629 'Command %s takes at most %s parameters (%s given)' 1630 % (method, maxcount, argcount)) 1631 else: 1632 mincount = maxcount - len(func.func_defaults or []) 1633 if argcount < mincount: 1634 msg = ( 1635 'Command %s takes at least %s parameters (%s given)' 1636 % (method, mincount, argcount)) 1637 raise APIError(0, msg) 1638 finally: 1639 state.last_api_response = time.time() 1640 1641 def _dispatch(self, method, params): 1642 _fault = None 1643 1644 try: 1645 return self._handle_request(method, params) 1646 except APIError as e: 1647 _fault = e 1648 except varintDecodeError as e: 1649 logger.error(e) 1650 _fault = APIError( 1651 26, 'Data contains a malformed varint. Some details: %s' % e) 1652 except Exception as e: 1653 logger.exception(e) 1654 _fault = APIError(21, 'Unexpected API Failure - %s' % e) 1655 1656 if _fault: 1657 if self.config.safeGet( 1658 'bitmessagesettings', 'apivariant') == 'legacy': 1659 return str(_fault) 1660 else: 1661 raise _fault # pylint: disable=raising-bad-type 1662 1663 def _listMethods(self): 1664 """List all API commands""" 1665 return self._handlers.keys() 1666 1667 def _methodHelp(self, method): 1668 return self._handlers[method].__doc__