diff --git a/api client.py b/api client.py new file mode 100644 index 00000000..4e896691 --- /dev/null +++ b/api client.py @@ -0,0 +1,54 @@ +# This is an example of how to connect to and use the Bitmessage API. +# See https://bitmessage.org/wiki/API_Reference + +import xmlrpclib +import json + +api = xmlrpclib.ServerProxy("http://bradley:password@localhost:8442/") + +print 'Let\'s test the API first.' +inputstr1 = "hello" +inputstr2 = "world" +print api.helloWorld(inputstr1, inputstr2) +print api.add(2,3) + +print 'Let\'s set the status bar message.' +print api.statusbar("new status bar message") + +print 'Let\'s list our addresses:' +print api.listAddresses() + +print 'Let\'s list our address again, but this time let\'s parse the json data into a Python data structure:' +jsonAddresses = json.loads(api.listAddresses()) +print jsonAddresses +print 'Now that we have our address data in a nice Python data structure, let\'s look at the first address (index 0) and print its label:' +print jsonAddresses['addresses'][0]['label'] + +print 'Uncomment this next line to create a new random address.' +#print api.createRandomAddress('new address label') + +print 'Uncomment these next three lines to create new new deterministic addresses.' +#jsonDeterministicAddresses = api.createDeterministicAddresses('asdfasdfqwerasdf', 2, 2, 1, False) +#print jsonDeterministicAddresses +#print json.loads(jsonDeterministicAddresses) + +print 'Let\'s now print all of our inbox messages:' +print api.getAllInboxMessages() +inboxMessages = json.loads(api.getAllInboxMessages()) +print inboxMessages + +print 'Uncomment this next line to decode the actual message data in the first message:' +#print inboxMessages['inboxMessages'][0]['message'].decode('base64') + +print 'Uncomment this next line in the code to delete a message' +#print api.trashMessage('584e5826947242a82cb883c8b39ac4a14959f14c228c0fbe6399f73e2cba5b59') + +"""print 'Now let\'s send a message. The example addresses are invalid. You will have to put your own in.' +subject = 'subject!'.encode('base64') +message = 'Hello, this is the message'.encode('base64') +print api.sendMessage('BM-oqmocYzqK74y3qSRi8c3YqyenyEKiMyLB', 'BM-omzGU4MtzSUCQhMNm5kPR6UNrJ4Q4zeFe', subject,message)""" + +"""print 'Now let\'s send a broadcast. The example address is invalid; you will have to put your own in.' +subject = 'subject within broadcast'.encode('base64') +message = 'Hello, this is the message within a broadcast.'.encode('base64') +print api.sendBroadcast('BM-onf6V1RELPgeNN6xw9yhpAiNiRexSRD4e', subject,message)""" \ No newline at end of file diff --git a/bitmessagemain.py b/bitmessagemain.py index 82f74d3e..37b2b8f4 100755 --- a/bitmessagemain.py +++ b/bitmessagemain.py @@ -6,7 +6,7 @@ #Right now, PyBitmessage only support connecting to stream 1. It doesn't yet contain logic to expand into further streams. -softwareVersion = '0.2.6' +softwareVersion = '0.2.7' verbose = 2 maximumAgeOfAnObjectThatIAmWillingToAccept = 216000 #Equals two days and 12 hours. lengthOfTimeToLeaveObjectsInInventory = 237600 #Equals two days and 18 hours. This should be longer than maximumAgeOfAnObjectThatIAmWillingToAccept so that we don't process messages twice. @@ -58,6 +58,12 @@ import highlevelcrypto from pyelliptic.openssl import OpenSSL import ctypes from pyelliptic import arithmetic +#The next 5 are used for the API +import uuid +import Cookie +from SimpleXMLRPCServer import * +import json +from subprocess import call #used when the API must execute an outside program #For each stream to which we connect, one outgoingSynSender thread will exist and will create 8 connections with peers. class outgoingSynSender(QThread): @@ -646,6 +652,15 @@ class receiveDataThread(QThread): sqlLock.release() self.emit(SIGNAL("displayNewInboxMessage(PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject)"),self.inventoryHash,toAddress,fromAddress,subject,body) + #If we are behaving as an API then we might need to run an outside command to let some program know that a new message has arrived. + if safeConfigGetBoolean('bitmessagesettings','apienabled'): + try: + apiNotifyPath = config.get('bitmessagesettings','apinotifypath') + except: + apiNotifyPath = '' + if apiNotifyPath != '': + call([apiNotifyPath, "newBroadcast"]) + #Display timing data printLock.acquire() print 'Time spent processing this interesting broadcast:', time.time()- self.messageProcessingStartTime @@ -1183,12 +1198,17 @@ class receiveDataThread(QThread): sqlLock.release() self.emit(SIGNAL("displayNewInboxMessage(PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject)"),self.inventoryHash,toAddress,fromAddress,subject,body) + #If we are behaving as an API then we might need to run an outside command to let some program know that a new message has arrived. + if safeConfigGetBoolean('bitmessagesettings','apienabled'): + try: + apiNotifyPath = config.get('bitmessagesettings','apinotifypath') + except: + apiNotifyPath = '' + if apiNotifyPath != '': + call([apiNotifyPath, "newMessage"]) + #Let us now check and see whether our receiving address is behaving as a mailing list - try: - isMailingList = config.getboolean(toAddress, 'mailinglist') - except: - isMailingList = False - if isMailingList: + if safeConfigGetBoolean(toAddress,'mailinglist'): try: mailingListName = config.get(toAddress, 'mailinglistname') except: @@ -1208,9 +1228,9 @@ class receiveDataThread(QThread): sqlReturnQueue.get() sqlLock.release() - workerQueue.put(('sendbroadcast',(fromAddress,subject,message))) self.emit(SIGNAL("displayNewSentMessage(PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject)"),toAddress,'[Broadcast subscribers]',fromAddress,subject,message,ackdata) - + workerQueue.put(('sendbroadcast',(fromAddress,subject,message))) + #Now let's consider sending the acknowledgement. We'll need to make sure that our client will properly process the ackData; if the packet is malformed, we could clear out self.data and an attacker could use that behavior to determine that we were capable of decoding this message. ackDataValidThusFar = True if len(ackData) < 24: @@ -1730,7 +1750,8 @@ class receiveDataThread(QThread): #print 'Within recaddr(): IP', recaddrIP, ', Port', recaddrPort, ', i', i hostFromAddrMessage = socket.inet_ntoa(self.data[52+lengthOfNumberOfAddresses+(34*i):56+lengthOfNumberOfAddresses+(34*i)]) #print 'hostFromAddrMessage', hostFromAddrMessage - if hostFromAddrMessage == '127.0.0.1': + if self.data[52+lengthOfNumberOfAddresses+(34*i)] == '\x7F': + print 'Ignoring IP address in loopback range:', hostFromAddrMessage continue timeSomeoneElseReceivedMessageFromThisNode, = unpack('>I',self.data[24+lengthOfNumberOfAddresses+(34*i):28+lengthOfNumberOfAddresses+(34*i)]) #This is the 'time' value in the received addr message. if recaddrStream not in knownNodes: #knownNodes is a dictionary of dictionaries with one outer dictionary for each stream. If the outer stream dictionary doesn't exist yet then we must make it. @@ -2208,6 +2229,15 @@ def calculateTestnetAddressFromPubkey(pubkey): base58encoded = arithmetic.changebase(binaryBitcoinAddress,256,58) return "1"*numberOfZeroBytesOnBinaryBitcoinAddress + base58encoded +def safeConfigGetBoolean(section,field): + try: + if config.getboolean(section,field): + return True + else: + return False + except: + return False + def lookupAppdataFolder(): APPNAME = "PyBitmessage" from os import path, environ @@ -2453,6 +2483,10 @@ class singleWorker(QThread): self.sendMsg(toRipe) else: print 'We don\'t need this pub key. We didn\'t ask for it. Pubkey hash:', toRipe.encode('hex') + else: + printLock.acquire() + sys.stderr.write('Probable programming error: The command sent to the workerThread is weird. It is: %s\n' % command) + printLock.release() workerQueue.task_done() @@ -2522,8 +2556,12 @@ class singleWorker(QThread): status,addressVersionNumber,streamNumber,ripe = decodeAddress(fromaddress) if addressVersionNumber == 2: #We need to convert our private keys to public keys in order to include them. - privSigningKeyBase58 = config.get(fromaddress, 'privsigningkey') - privEncryptionKeyBase58 = config.get(fromaddress, 'privencryptionkey') + try: + privSigningKeyBase58 = config.get(fromaddress, 'privsigningkey') + privEncryptionKeyBase58 = config.get(fromaddress, 'privencryptionkey') + except: + self.emit(SIGNAL("updateSentItemStatusByAckdata(PyQt_PyObject,PyQt_PyObject)"),ackdata,'Error! Could not find sender address (your address) in the keys.dat file.') + continue privSigningKeyHex = decodeWalletImportFormat(privSigningKeyBase58).encode('hex') privEncryptionKeyHex = decodeWalletImportFormat(privEncryptionKeyBase58).encode('hex') @@ -2551,6 +2589,7 @@ class singleWorker(QThread): trialValue = 99999999999999999999 target = 2**64 / ((len(payload)+payloadLengthExtraBytes+8) * averageProofOfWorkNonceTrialsPerByte) print '(For broadcast message) Doing proof of work...' + self.emit(SIGNAL("updateSentItemStatusByAckdata(PyQt_PyObject,PyQt_PyObject)"),ackdata,'Doing work necessary to send broadcast...') initialHash = hashlib.sha512(payload).digest() while trialValue > target: nonce += 1 @@ -2671,8 +2710,12 @@ class singleWorker(QThread): payload += '\x00\x00\x00\x01' #Bitfield of features and behaviors that can be expected from me. (See https://bitmessage.org/wiki/Protocol_specification#Pubkey_bitfield_features ) #We need to convert our private keys to public keys in order to include them. - privSigningKeyBase58 = config.get(fromaddress, 'privsigningkey') - privEncryptionKeyBase58 = config.get(fromaddress, 'privencryptionkey') + try: + privSigningKeyBase58 = config.get(fromaddress, 'privsigningkey') + privEncryptionKeyBase58 = config.get(fromaddress, 'privencryptionkey') + except: + self.emit(SIGNAL("updateSentItemStatusByAckdata(PyQt_PyObject,PyQt_PyObject)"),ackdata,'Error! Could not find sender address (your address) in the keys.dat file.') + continue privSigningKeyHex = decodeWalletImportFormat(privSigningKeyBase58).encode('hex') privEncryptionKeyHex = decodeWalletImportFormat(privEncryptionKeyBase58).encode('hex') @@ -2805,7 +2848,10 @@ class singleWorker(QThread): nonce += 1 trialValue, = unpack('>Q',hashlib.sha512(hashlib.sha512(pack('>Q',nonce) + initialHash).digest()).digest()[0:8]) print '(For msg message) Found proof of work', trialValue, 'Nonce:', nonce - print 'POW took', int(time.time()-powStartTime), 'seconds.', nonce/(time.time()-powStartTime), 'nonce trials per second.' + try: + print 'POW took', int(time.time()-powStartTime), 'seconds.', nonce/(time.time()-powStartTime), 'nonce trials per second.' + except: + pass payload = pack('>Q',nonce) + payload inventoryHash = calculateInventoryHash(payload) @@ -2880,7 +2926,10 @@ class singleWorker(QThread): trialValue, = unpack('>Q',hashlib.sha512(hashlib.sha512(pack('>Q',nonce) + initialHash).digest()).digest()[0:8]) printLock.acquire() print '(For ack message) Found proof of work', trialValue, 'Nonce:', nonce - print 'POW took', int(time.time()-powStartTime), 'seconds.', nonce/(time.time()-powStartTime), 'nonce trials per second.' + try: + print 'POW took', int(time.time()-powStartTime), 'seconds.', nonce/(time.time()-powStartTime), 'nonce trials per second.' + except: + pass printLock.release() payload = pack('>Q',nonce) + payload headerData = '\xe9\xbe\xb4\xd9' #magic bits, slighly different from Bitcoin's magic bits. @@ -2959,6 +3008,9 @@ class addressGenerator(QThread): config.set(address,'privEncryptionKey',privEncryptionKeyWIF) with open(appdata + 'keys.dat', 'wb') as configfile: config.write(configfile) + + #It may be the case that this address is being generated as a result of a call to the API. Let us put the result in the necessary queue. + apiAddressGeneratorReturnQueue.put(address) self.emit(SIGNAL("updateStatusBar(PyQt_PyObject)"),'Done generating address. Doing work necessary to broadcast it...') self.emit(SIGNAL("writeNewAddressToTable(PyQt_PyObject,PyQt_PyObject,PyQt_PyObject)"),self.label,address,str(self.streamNumber)) @@ -2970,6 +3022,8 @@ class addressGenerator(QThread): self.emit(SIGNAL("updateStatusBar(PyQt_PyObject)"),statusbar) signingKeyNonce = 0 encryptionKeyNonce = 1 + listOfNewAddressesToSendOutThroughTheAPI = [] #We fill out this list no matter what although we only need it if we end up passing the info to the API. + for i in range(self.numberOfAddressesToMake): #This next section is a little bit strange. We're going to generate keys over and over until we #find one that has a RIPEMD hash that starts with either \x00 or \x00\x00. Then when we pack them @@ -3028,8 +3082,11 @@ class addressGenerator(QThread): config.write(configfile) self.emit(SIGNAL("writeNewAddressToTable(PyQt_PyObject,PyQt_PyObject,PyQt_PyObject)"),self.label,address,str(self.streamNumber)) + listOfNewAddressesToSendOutThroughTheAPI.append(address) except: print address,'already exists. Not adding it again.' + #It may be the case that this address is being generated as a result of a call to the API. Let us put the result in the necessary queue. + apiAddressGeneratorReturnQueue.put(listOfNewAddressesToSendOutThroughTheAPI) self.emit(SIGNAL("updateStatusBar(PyQt_PyObject)"),'Done generating address') reloadMyAddressHashes() @@ -3094,6 +3151,439 @@ class addressGenerator(QThread): OpenSSL.EC_KEY_free(k) return mb.raw +#This is one of several classes that constitute the API +#This class was written by Vaibhav Bhatia +#http://code.activestate.com/recipes/501148-xmlrpc-serverclient-which-does-cookie-handling-and/ +class APIUserManagement: + def __init__(self): + #self.d = shelve.open('machines.shv') + self.d = {} + + # register a list of valid machine names/email id's + validconfig = {config.get('bitmessagesettings', 'apiusername'):config.get('bitmessagesettings', 'apipassword')} + for k,v in validconfig.items(): + self.generateUuid(k,v) + + def generateUuid(self, email_id, machine_name): + """ return a uuid which uniquely identifies machinename and email id """ + uuidstr = None + + if machine_name not in self.d: + myNamespace = uuid.uuid3(uuid.NAMESPACE_URL, machine_name) + uuidstr = str(uuid.uuid3(myNamespace, email_id)) + + self.d[machine_name] = (machine_name, uuidstr, email_id) + self.d[uuidstr] = (machine_name, uuidstr ,email_id) + else: + (machine_name, uuidstr, email_id) = self.d[machine_name] + + return uuidstr + + def checkMe(self, id): + if id in self.d: + return self.d[id] + return (None,None,None) + + #def __del__(self): + # self.d.close() + +#This is used only for the API +def APIAuthenticate(id): + sk = APIUserManagement() + return sk.checkMe(id) + +#This is one of several classes that constitute the API +#This class was written by Vaibhav Bhatia +#http://code.activestate.com/recipes/501148-xmlrpc-serverclient-which-does-cookie-handling-and/ +class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): + def setCookie(self, key=None ,value=None): + if key : + c1 = Cookie.SimpleCookie() + c1[key] = value + cinfo = self.getDefaultCinfo() + for attr,val in cinfo.items(): + c1[key][attr] = val + + if c1 not in self.cookies: + self.cookies.append(c1) + + def getDefaultCinfo(self): + cinfo = {} + + cinfo['expires'] = 30*24*60*60 + cinfo['path'] = '/RPC2/' + cinfo['comment'] = 'comment!' + cinfo['domain'] = '.localhost.local' + cinfo['max-age'] = 30*24*60*60 + cinfo['secure'] = '' + cinfo['version']= 1 + + return cinfo + + def do_POST(self): + #Handles the HTTP POST request. + #Attempts to interpret all HTTP POST requests as XML-RPC calls, + #which are forwarded to the server's _dispatch method for handling. + + #Note: this method is the same as in SimpleXMLRPCRequestHandler, + #just hacked to handle cookies + + # Check that the path is legal + if not self.is_rpc_path_valid(): + self.report_404() + return + + try: + # Get arguments by reading body of request. + # We read this in chunks to avoid straining + # socket.read(); around the 10 or 15Mb mark, some platforms + # begin to have problems (bug #792570). + max_chunk_size = 10*1024*1024 + size_remaining = int(self.headers["content-length"]) + L = [] + while size_remaining: + chunk_size = min(size_remaining, max_chunk_size) + L.append(self.rfile.read(chunk_size)) + size_remaining -= len(L[-1]) + data = ''.join(L) + + # In previous versions of SimpleXMLRPCServer, _dispatch + # could be overridden in this class, instead of in + # SimpleXMLRPCDispatcher. To maintain backwards compatibility, + # check to see if a subclass implements _dispatch and dispatch + # using that method if present. + response = self.server._marshaled_dispatch( + data, getattr(self, '_dispatch', None) + ) + except: # This should only happen if the module is buggy + # internal error, report as HTTP server error + self.send_response(500) + self.end_headers() + else: + # got a valid XML RPC response + self.send_response(200) + self.send_header("Content-type", "text/xml") + self.send_header("Content-length", str(len(response))) + + # HACK :start -> sends cookies here + if self.cookies: + for cookie in self.cookies: + self.send_header('Set-Cookie',cookie.output(header='')) + # HACK :end + + self.end_headers() + self.wfile.write(response) + + # shut down the connection + self.wfile.flush() + self.connection.shutdown(1) + + + def APIAuthenticateClient(self): + validuser = False + + if self.headers.has_key('Authorization'): + # handle Basic authentication + (enctype, encstr) = self.headers.get('Authorization').split() + (emailid, machine_name) = encstr.decode('base64').split(':') + (auth_machine, auth_uuidstr, auth_email) = APIAuthenticate(machine_name) + + if emailid == auth_email: + print "Authenticated" + # set authentication cookies on client machines + validuser = True + if auth_uuidstr: + self.setCookie('UUID',auth_uuidstr) + + elif self.headers.has_key('UUID'): + # handle cookie based authentication + id = self.headers.get('UUID') + (auth_machine, auth_uuidstr, auth_email) = APIAuthenticate(id) + + if auth_uuidstr : + print "Authenticated" + validuser = True + else: + print 'Authentication failed' + time.sleep(2) + + return validuser + + def _dispatch(self, method, params): + self.cookies = [] + + validuser = self.APIAuthenticateClient() + if not validuser: + time.sleep(2) + return "RPC Username or password incorrect." + # handle request + if method == 'helloWorld': + (a,b) = params + return a+'-'+b + elif method == 'add': + (a,b) = params + return a+b + elif method == 'statusbar': + message, = params + apiSignalQueue.put(('updateStatusBar',message)) + elif method == 'listAddresses': + data = '{"addresses":[' + configSections = config.sections() + for addressInKeysFile in configSections: + if addressInKeysFile <> 'bitmessagesettings': + status,addressVersionNumber,streamNumber,hash = decodeAddress(addressInKeysFile) + data + if len(data) > 20: + data += ',' + data += json.dumps({'label':config.get(addressInKeysFile,'label'),'address':addressInKeysFile,'stream':streamNumber,'enabled':config.getboolean(addressInKeysFile,'enabled')},indent=4, separators=(',', ': ')) + data += ']}' + return data + elif method == 'createRandomAddress': + if len(params) == 0: + return 'API Error 0000: I need parameters!' + elif len(params) == 1: + label, = params + eighteenByteRipe = False + elif len(params) == 2: + label, eighteenByteRipe = params + apiAddressGeneratorReturnQueue.queue.clear() + apiSignalQueue.put(('createRandomAddress',(label, eighteenByteRipe))) #params should be a twopul which equals (eighteenByteRipe, label) + return apiAddressGeneratorReturnQueue.get() + elif method == 'createDeterministicAddresses': + if len(params) == 0: + return 'API Error 0000: I need parameters!' + elif len(params) == 1: + passphrase, = params + numberOfAddresses = 1 + addressVersionNumber = 0 + streamNumber = 0 + eighteenByteRipe = False + elif len(params) == 2: + passphrase, numberOfAddresses = params + addressVersionNumber = 0 + streamNumber = 0 + eighteenByteRipe = False + elif len(params) == 3: + passphrase, numberOfAddresses, addressVersionNumber = params + streamNumber = 0 + eighteenByteRipe = False + elif len(params) == 4: + passphrase, numberOfAddresses, addressVersionNumber, streamNumber = params + eighteenByteRipe = False + elif len(params) == 5: + passphrase, numberOfAddresses, addressVersionNumber, streamNumber, eighteenByteRipe = params + if len(passphrase) == 0: + return 'API Error: the specified passphrase is blank.' + if addressVersionNumber == 0: #0 means "just use the proper addressVersionNumber" + addressVersionNumber == 2 + if addressVersionNumber != 2: + return 'API Error: the address version number currently must be 2 (or 0 which means auto-select). Others aren\'t supported.' + if streamNumber == 0: #0 means "just use the most available stream" + streamNumber = 1 + if streamNumber != 1: + return 'API Error: the stream number must be 1 (or 0 which means auto-select). Others aren\'t supported.' + if numberOfAddresses == 0: + return 'API Error: Why would you ask me to generate 0 addresses for you?' + if numberOfAddresses > 9999: + return 'API Error: You have (accidentially?) specified too many addresses to make. Maximum 9999. This check only exists to prevent mischief; if you really want to create more addresses than this, contact the Bitmessage developers and we can modify the check or you can do it yourself by searching the source code for this message.' + apiAddressGeneratorReturnQueue.queue.clear() + print 'about to send numberOfAddresses', numberOfAddresses + apiSignalQueue.put(('createDeterministicAddresses',(passphrase, numberOfAddresses, addressVersionNumber, streamNumber, eighteenByteRipe))) + data = '{"addresses":[' + queueReturn = apiAddressGeneratorReturnQueue.get() + for item in queueReturn: + if len(data) > 20: + data += ',' + data += "\""+item+ "\"" + data += ']}' + return data + elif method == 'getAllInboxMessages': + sqlLock.acquire() + sqlSubmitQueue.put('''SELECT msgid, toaddress, fromaddress, subject, received, message FROM inbox where folder='inbox' ORDER BY received''') + sqlSubmitQueue.put('') + queryreturn = sqlReturnQueue.get() + sqlLock.release() + data = '{"inboxMessages":[' + for row in queryreturn: + msgid, toAddress, fromAddress, subject, received, message, = row + if len(data) > 25: + data += ',' + data += json.dumps({'msgid':msgid.encode('hex'),'toAddress':toAddress,'fromAddress':fromAddress,'subject':subject.encode('base64'),'message':message.encode('base64'),'encodingType':2,'receivedTime':received},indent=4, separators=(',', ': ')) + data += ']}' + return data + elif method == 'trashMessage': + msgid = params[0].decode('hex') + t = (msgid,) + sqlLock.acquire() + sqlSubmitQueue.put('''UPDATE inbox SET folder='trash' WHERE msgid=?''') + sqlSubmitQueue.put(t) + sqlReturnQueue.get() + sqlLock.release() + apiSignalQueue.put(('updateStatusBar','Per API: Trashed message (assuming message existed). UI not updated.')) + return 'Trashed message (assuming message existed). UI not updated. To double check, run getAllInboxMessages to see that the message disappeared, or restart Bitmessage and look in the normal Bitmessage GUI.' + elif method == 'sendMessage': + if len(params) == 4: + toAddress, fromAddress, subject, message = params + encodingType = 2 + if len(params) == 5: + toAddress, fromAddress, subject, message, encodingType = params + if encodingType != 2: + return 'API Error: The encoding type must be 2 because that is the only one this program currently supports.' + subject = subject.decode('base64') + message = message.decode('base64') + status,addressVersionNumber,streamNumber,toRipe = decodeAddress(toAddress) + if status <> 'success': + printLock.acquire() + print 'API Error: Could not decode address:', toAddress, ':', status + printLock.release() + if status == 'checksumfailed': + return 'API Error: Checksum failed for address: ' + toAddress + if status == 'invalidcharacters': + return 'API Error: Invalid characters in address: '+ toAddress + if status == 'versiontoohigh': + return 'API Error: Address version number too high (or zero) in address: ' + toAddress + if addressVersionNumber != 2: + return 'API Error: the address version number currently must be 2. Others aren\'t supported. Check the toAddress.' + if streamNumber != 1: + return 'API Error: the stream number must be 1. Others aren\'t supported. Check the toAddress.' + status,addressVersionNumber,streamNumber,fromRipe = decodeAddress(fromAddress) + if status <> 'success': + printLock.acquire() + print 'API Error: Could not decode address:', fromAddress, ':', status + printLock.release() + if status == 'checksumfailed': + return 'API Error: Checksum failed for address: ' + fromAddress + if status == 'invalidcharacters': + return 'API Error: Invalid characters in address: '+ fromAddress + if status == 'versiontoohigh': + return 'API Error: Address version number too high (or zero) in address: ' + fromAddress + if addressVersionNumber != 2: + return 'API Error: the address version number currently must be 2. Others aren\'t supported. Check the fromAddress.' + if streamNumber != 1: + return 'API Error: the stream number must be 1. Others aren\'t supported. Check the fromAddress.' + toAddress = addBMIfNotPresent(toAddress) + fromAddress = addBMIfNotPresent(fromAddress) + try: + fromAddressEnabled = config.getboolean(fromAddress,'enabled') + except: + return 'API Error: could not find your fromAddress in the keys.dat file.' + if not fromAddressEnabled: + return 'API Error: your fromAddress is disabled. Cannot send.' + + ackdata = OpenSSL.rand(32) + sqlLock.acquire() + t = ('',toAddress,toRipe,fromAddress,subject,message,ackdata,int(time.time()),'findingpubkey',1,1,'sent') + sqlSubmitQueue.put('''INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?)''') + sqlSubmitQueue.put(t) + sqlReturnQueue.get() + sqlLock.release() + + toLabel = '' + t = (toAddress,) + sqlLock.acquire() + sqlSubmitQueue.put('''select label from addressbook where address=?''') + sqlSubmitQueue.put(t) + queryreturn = sqlReturnQueue.get() + sqlLock.release() + if queryreturn <> []: + for row in queryreturn: + toLabel, = row + apiSignalQueue.put(('displayNewSentMessage',(toAddress,toLabel,fromAddress,subject,message,ackdata))) + + workerQueue.put(('sendmessage',toAddress)) + + return ackdata.encode('hex') + + elif method == 'sendBroadcast': + if len(params) == 3: + fromAddress, subject, message = params + encodingType = 2 + if len(params) == 4: + fromAddress, subject, message, encodingType = params + if encodingType != 2: + return 'API Error: The encoding type must be 2 because that is the only one this program currently supports.' + subject = subject.decode('base64') + message = message.decode('base64') + + status,addressVersionNumber,streamNumber,fromRipe = decodeAddress(fromAddress) + if status <> 'success': + printLock.acquire() + print 'API Error: Could not decode address:', fromAddress, ':', status + printLock.release() + if status == 'checksumfailed': + return 'API Error: Checksum failed for address: ' + fromAddress + if status == 'invalidcharacters': + return 'API Error: Invalid characters in address: '+ fromAddress + if status == 'versiontoohigh': + return 'API Error: Address version number too high (or zero) in address: ' + fromAddress + if addressVersionNumber != 2: + return 'API Error: the address version number currently must be 2. Others aren\'t supported. Check the fromAddress.' + if streamNumber != 1: + return 'API Error: the stream number must be 1. Others aren\'t supported. Check the fromAddress.' + fromAddress = addBMIfNotPresent(fromAddress) + try: + fromAddressEnabled = config.getboolean(fromAddress,'enabled') + except: + return 'API Error: could not find your fromAddress in the keys.dat file.' + ackdata = OpenSSL.rand(32) + toAddress = '[Broadcast subscribers]' + ripe = '' + + sqlLock.acquire() + t = ('',toAddress,ripe,fromAddress,subject,message,ackdata,int(time.time()),'broadcastpending',1,1,'sent') + sqlSubmitQueue.put('''INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?)''') + sqlSubmitQueue.put(t) + sqlReturnQueue.get() + sqlLock.release() + + toLabel = '[Broadcast subscribers]' + apiSignalQueue.put(('displayNewSentMessage',(toAddress,toLabel,fromAddress,subject,message,ackdata))) + + workerQueue.put(('sendbroadcast',(fromAddress,subject,message))) + + return ackdata.encode('hex') + + ########################### + else: + return 'Invalid Method: %s'%method + +#This thread, of which there is only one, runs the API. +class singleAPI(QThread): + def __init__(self, parent = None): + QThread.__init__(self, parent) + + def run(self): + se = SimpleXMLRPCServer((config.get('bitmessagesettings', 'apiinterface'),config.getint('bitmessagesettings', 'apiport')), MySimpleXMLRPCRequestHandler, True, True) + se.register_introspection_functions() + se.serve_forever() + +#The MySimpleXMLRPCRequestHandler class cannot emit signals (or at least I don't know how) because it is not a QT thread. It therefore puts data in a queue which this thread monitors and emits the signals on its behalf. +class singleAPISignalHandler(QThread): + def __init__(self, parent = None): + QThread.__init__(self, parent) + + def run(self): + while True: + command, data = apiSignalQueue.get() + if command == 'updateStatusBar': + self.emit(SIGNAL("updateStatusBar(PyQt_PyObject)"),data) + elif command == 'createRandomAddress': + label, eighteenByteRipe = data + streamNumberForAddress = 1 + self.addressGenerator = addressGenerator() + self.addressGenerator.setup(2,streamNumberForAddress,label,1,"",eighteenByteRipe) + self.emit(SIGNAL("passAddressGeneratorObjectThrough(PyQt_PyObject)"),self.addressGenerator) + self.addressGenerator.start() + elif command == 'createDeterministicAddresses': + passphrase, numberOfAddresses, addressVersionNumber, streamNumber, eighteenByteRipe = data + self.addressGenerator = addressGenerator() + self.addressGenerator.setup(addressVersionNumber,streamNumber,'unused API address',numberOfAddresses,passphrase,eighteenByteRipe) + self.emit(SIGNAL("passAddressGeneratorObjectThrough(PyQt_PyObject)"),self.addressGenerator) + self.addressGenerator.start() + elif command == 'displayNewSentMessage': + toAddress,toLabel,fromAddress,subject,message,ackdata = data + self.emit(SIGNAL("displayNewSentMessage(PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject)"),toAddress,toLabel,fromAddress,subject,message,ackdata) + class iconGlossaryDialog(QtGui.QDialog): def __init__(self,parent): QtGui.QWidget.__init__(self, parent) @@ -3200,11 +3690,7 @@ class SpecialAddressBehaviorDialog(QtGui.QDialog): self.parent = parent currentRow = parent.ui.tableWidgetYourIdentities.currentRow() addressAtCurrentRow = str(parent.ui.tableWidgetYourIdentities.item(currentRow,1).text()) - try: - isMailingList = config.getboolean(addressAtCurrentRow, 'mailinglist') - except: - isMailingList = False - if isMailingList: + if safeConfigGetBoolean(addressAtCurrentRow,'mailinglist'): self.ui.radioButtonBehaviorMailingList.click() else: self.ui.radioButtonBehaveNormalAddress.click() @@ -3425,11 +3911,8 @@ class MyForm(QtGui.QMainWindow): newItem.setFlags( QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled ) if not isEnabled: newItem.setTextColor(QtGui.QColor(128,128,128)) - try: - if config.getboolean(addressInKeysFile,'mailinglist'): - newItem.setTextColor(QtGui.QColor(137,04,177))#magenta - except: - pass #The 'mailinglist' + if safeConfigGetBoolean(addressInKeysFile,'mailinglist'): + newItem.setTextColor(QtGui.QColor(137,04,177))#magenta self.ui.tableWidgetYourIdentities.setItem(0, 1, newItem) newItem = QtGui.QTableWidgetItem(str(addressStream(addressInKeysFile))) newItem.setFlags( QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled ) @@ -3478,11 +3961,8 @@ class MyForm(QtGui.QMainWindow): newItem = QtGui.QTableWidgetItem(unicode(toLabel,'utf-8')) newItem.setFlags( QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled ) newItem.setData(Qt.UserRole,str(toAddress)) - try: - if config.getboolean(toAddress,'mailinglist'): - newItem.setTextColor(QtGui.QColor(137,04,177)) - except: - pass #the 'mailinglist' setting was not found for this address. + if safeConfigGetBoolean(toAddress,'mailinglist'): + newItem.setTextColor(QtGui.QColor(137,04,177)) self.ui.tableWidgetInbox.setItem(0,0,newItem) if fromLabel == '': newItem = QtGui.QTableWidgetItem(unicode(fromAddress,'utf-8')) @@ -3647,6 +4127,25 @@ class MyForm(QtGui.QMainWindow): QtCore.QObject.connect(self.workerThread, QtCore.SIGNAL("updateSentItemStatusByAckdata(PyQt_PyObject,PyQt_PyObject)"), self.updateSentItemStatusByAckdata) QtCore.QObject.connect(self.workerThread, QtCore.SIGNAL("updateStatusBar(PyQt_PyObject)"), self.updateStatusBar) + self.singleAPIThread = singleAPI() + self.singleAPIThread.start() + + if safeConfigGetBoolean('bitmessagesettings','apienabled'): + try: + apiNotifyPath = config.get('bitmessagesettings','apinotifypath') + except: + apiNotifyPath = '' + if apiNotifyPath != '': + printLock.acquire() + print 'Trying to call', apiNotifyPath + printLock.release() + call([apiNotifyPath, "startingUp"]) + self.singleAPISignalHandlerThread = singleAPISignalHandler() + self.singleAPISignalHandlerThread.start() + QtCore.QObject.connect(self.singleAPISignalHandlerThread, QtCore.SIGNAL("updateStatusBar(PyQt_PyObject)"), self.updateStatusBar) + QtCore.QObject.connect(self.singleAPISignalHandlerThread, QtCore.SIGNAL("passAddressGeneratorObjectThrough(PyQt_PyObject)"), self.connectObjectToAddressGeneratorSignals) + QtCore.QObject.connect(self.singleAPISignalHandlerThread, QtCore.SIGNAL("displayNewSentMessage(PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject,PyQt_PyObject)"), self.displayNewSentMessage) + def click_actionManageKeys(self): if 'darwin' in sys.platform or 'linux' in sys.platform: if appdata == '': @@ -3814,12 +4313,9 @@ class MyForm(QtGui.QMainWindow): toLabel = toAddress self.ui.tableWidgetInbox.item(i,0).setText(unicode(toLabel,'utf-8')) #Set the color according to whether it is the address of a mailing list or not. - try: - if config.getboolean(toAddress,'mailinglist'): - self.ui.tableWidgetInbox.item(i,0).setTextColor(QtGui.QColor(137,04,177)) - else: - self.ui.tableWidgetInbox.item(i,0).setTextColor(QtGui.QColor(0,0,0)) - except: + if safeConfigGetBoolean(toAddress,'mailinglist'): + self.ui.tableWidgetInbox.item(i,0).setTextColor(QtGui.QColor(137,04,177)) + else: self.ui.tableWidgetInbox.item(i,0).setTextColor(QtGui.QColor(0,0,0)) def rerenderSentFromLabels(self): @@ -3901,14 +4397,16 @@ class MyForm(QtGui.QMainWindow): sqlSubmitQueue.put(t) sqlReturnQueue.get() sqlLock.release() - workerQueue.put(('sendmessage',toAddress)) - try: + + + + """try: fromLabel = config.get(fromAddress, 'label') except: fromLabel = '' if fromLabel == '': - fromLabel = fromAddress + fromLabel = fromAddress""" toLabel = '' @@ -3921,8 +4419,11 @@ class MyForm(QtGui.QMainWindow): if queryreturn <> []: for row in queryreturn: toLabel, = row + + self.displayNewSentMessage(toAddress,toLabel,fromAddress, subject, message, ackdata) + workerQueue.put(('sendmessage',toAddress)) - self.ui.tableWidgetSent.insertRow(0) + """self.ui.tableWidgetSent.insertRow(0) if toLabel == '': newItem = QtGui.QTableWidgetItem(unicode(toAddress,'utf-8')) else: @@ -3944,7 +4445,7 @@ class MyForm(QtGui.QMainWindow): newItem.setData(33,int(time.time())) self.ui.tableWidgetSent.setItem(0,3,newItem) - self.ui.textEditSentMessage.setText(self.ui.tableWidgetSent.item(0,2).data(Qt.UserRole).toPyObject()) + self.ui.textEditSentMessage.setText(self.ui.tableWidgetSent.item(0,2).data(Qt.UserRole).toPyObject())""" self.ui.comboBoxSendFrom.setCurrentIndex(0) self.ui.labelFrom.setText('') @@ -3997,7 +4498,7 @@ class MyForm(QtGui.QMainWindow): newItem.setData(Qt.UserRole,unicode(message,'utf-8)')) self.ui.tableWidgetSent.setItem(0,2,newItem) #newItem = QtGui.QTableWidgetItem('Doing work necessary to send broadcast...'+strftime(config.get('bitmessagesettings', 'timeformat'),localtime(int(time.time())))) - newItem = myTableWidgetItem('Doing work necessary to send broadcast...') + newItem = myTableWidgetItem('Work is queued.') newItem.setData(Qt.UserRole,QByteArray(ackdata)) newItem.setData(33,int(time.time())) self.ui.tableWidgetSent.setItem(0,3,newItem) @@ -4071,6 +4572,11 @@ class MyForm(QtGui.QMainWindow): QtCore.QObject.connect(object, QtCore.SIGNAL("incrementNumberOfBroadcastsProcessed()"), self.incrementNumberOfBroadcastsProcessed) QtCore.QObject.connect(object, QtCore.SIGNAL("setStatusIcon(PyQt_PyObject)"), self.setStatusIcon) + #This function exists because of the API. The API thread starts an address generator thread and must somehow connect the address generator's signals to the QApplication thread. This function is used to connect the slots and signals. + def connectObjectToAddressGeneratorSignals(self,object): + QtCore.QObject.connect(object, SIGNAL("writeNewAddressToTable(PyQt_PyObject,PyQt_PyObject,PyQt_PyObject)"), self.writeNewAddressToTable) + QtCore.QObject.connect(object, QtCore.SIGNAL("updateStatusBar(PyQt_PyObject)"), self.updateStatusBar) + #This function is called by the processmsg function when that function receives a message to an address that is acting as a pseudo-mailing-list. The message will be broadcast out. This function puts the message on the 'Sent' tab. def displayNewSentMessage(self,toAddress,toLabel,fromAddress,subject,message,ackdata): try: @@ -4081,7 +4587,10 @@ class MyForm(QtGui.QMainWindow): fromLabel = fromAddress self.ui.tableWidgetSent.insertRow(0) - newItem = QtGui.QTableWidgetItem(unicode(toLabel,'utf-8')) + if toLabel == '': + newItem = QtGui.QTableWidgetItem(unicode(toAddress,'utf-8')) + else: + newItem = QtGui.QTableWidgetItem(unicode(toLabel,'utf-8')) newItem.setData(Qt.UserRole,str(toAddress)) self.ui.tableWidgetSent.setItem(0,0,newItem) if fromLabel == '': @@ -4094,7 +4603,7 @@ class MyForm(QtGui.QMainWindow): newItem.setData(Qt.UserRole,unicode(message,'utf-8)')) self.ui.tableWidgetSent.setItem(0,2,newItem) #newItem = QtGui.QTableWidgetItem('Doing work necessary to send broadcast...'+strftime(config.get('bitmessagesettings', 'timeformat'),localtime(int(time.time())))) - newItem = myTableWidgetItem('Doing work necessary to send broadcast...') + newItem = myTableWidgetItem('Work is queued. '+strftime(config.get('bitmessagesettings', 'timeformat'),localtime(int(time.time())))) newItem.setData(Qt.UserRole,QByteArray(ackdata)) newItem.setData(33,int(time.time())) self.ui.tableWidgetSent.setItem(0,3,newItem) @@ -4141,11 +4650,8 @@ class MyForm(QtGui.QMainWindow): #msgid, toaddress, fromaddress, subject, received, message = row newItem = QtGui.QTableWidgetItem(unicode(toLabel,'utf-8')) newItem.setData(Qt.UserRole,str(toAddress)) - try: - if config.getboolean(str(toAddress),'mailinglist'): - newItem.setTextColor(QtGui.QColor(137,04,177)) - except: - pass #the 'mailinglist' setting was not found for this address. + if safeConfigGetBoolean(str(toAddress),'mailinglist'): + newItem.setTextColor(QtGui.QColor(137,04,177)) self.ui.tableWidgetInbox.insertRow(0) self.ui.tableWidgetInbox.setItem(0,0,newItem) @@ -4709,11 +5215,8 @@ class MyForm(QtGui.QMainWindow): self.ui.tableWidgetYourIdentities.item(currentRow,0).setTextColor(QtGui.QColor(0,0,0)) self.ui.tableWidgetYourIdentities.item(currentRow,1).setTextColor(QtGui.QColor(0,0,0)) self.ui.tableWidgetYourIdentities.item(currentRow,2).setTextColor(QtGui.QColor(0,0,0)) - try: - if config.getboolean(addressAtCurrentRow,'mailinglist'): - self.ui.tableWidgetYourIdentities.item(currentRow,1).setTextColor(QtGui.QColor(137,04,177)) - except: - pass + if safeConfigGetBoolean(addressAtCurrentRow,'mailinglist'): + self.ui.tableWidgetYourIdentities.item(currentRow,1).setTextColor(QtGui.QColor(137,04,177)) reloadMyAddressHashes() def on_action_YourIdentitiesDisable(self): currentRow = self.ui.tableWidgetYourIdentities.currentRow() @@ -4722,12 +5225,8 @@ class MyForm(QtGui.QMainWindow): self.ui.tableWidgetYourIdentities.item(currentRow,0).setTextColor(QtGui.QColor(128,128,128)) self.ui.tableWidgetYourIdentities.item(currentRow,1).setTextColor(QtGui.QColor(128,128,128)) self.ui.tableWidgetYourIdentities.item(currentRow,2).setTextColor(QtGui.QColor(128,128,128)) - try: - if config.getboolean(addressAtCurrentRow,'mailinglist'): - self.ui.tableWidgetYourIdentities.item(currentRow,1).setTextColor(QtGui.QColor(137,04,177)) - except: - pass - + if safeConfigGetBoolean(addressAtCurrentRow,'mailinglist'): + self.ui.tableWidgetYourIdentities.item(currentRow,1).setTextColor(QtGui.QColor(137,04,177)) with open(appdata + 'keys.dat', 'wb') as configfile: config.write(configfile) reloadMyAddressHashes() @@ -4848,6 +5347,8 @@ eightBytesOfRandomDataUsedToDetectConnectionsToSelf = pack('>Q',random.randrange connectedHostsList = {} #List of hosts to which we are connected. Used to guarantee that the outgoingSynSender thread won't connect to the same remote node twice. neededPubkeys = {} successfullyDecryptMessageTimings = [] #A list of the amounts of time it took to successfully decrypt msg messages +apiSignalQueue = Queue.Queue() #The singleAPI thread uses this queue to pass messages to a QT thread which can emit signals to do things like display a message in the UI. +apiAddressGeneratorReturnQueue = Queue.Queue() #The address generator thread uses this queue to get information back to the API thread. #These constants are not at the top because if changed they will cause particularly unexpected behavior: You won't be able to either send or receive messages because the proof of work you do (or demand) won't match that done or demanded by others. Don't change them! averageProofOfWorkNonceTrialsPerByte = 320 #The amount of work that should be performed (and demanded) per byte of the payload. Double this number to double the work. diff --git a/messages.dat reader.py b/messages.dat reader.py index ec116a1e..7958ecdb 100644 --- a/messages.dat reader.py +++ b/messages.dat reader.py @@ -90,8 +90,8 @@ def takeSentMessagesOutOfTrash(): #takeInboxMessagesOutOfTrash() #takeSentMessagesOutOfTrash() -#readInbox() -readSent() +readInbox() +#readSent() #readPubkeys() #readSubscriptions() #readInventory()