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