Merge pull request #10 from beardog108/communicator-multithreading

Communicator multithreading
master
Kevin Froman 2018-05-18 18:16:12 -05:00 committed by GitHub
commit e69971bc27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1674 additions and 514 deletions

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ onionr/gnupg/*
run.sh run.sh
onionr/data-encrypted.dat onionr/data-encrypted.dat
onionr/.onionr-lock onionr/.onionr-lock
core

View File

@ -16,6 +16,8 @@ uninstall:
sudo rm -f /usr/bin/onionr sudo rm -f /usr/bin/onionr
test: test:
@./RUN-LINUX.sh stop
@sleep 1
@rm -rf onionr/data-backup @rm -rf onionr/data-backup
@mv onionr/data onionr/data-backup | true > /dev/null 2>&1 @mv onionr/data onionr/data-backup | true > /dev/null 2>&1
-@cd onionr; ./tests.py; ./cryptotests.py; -@cd onionr; ./tests.py; ./cryptotests.py;
@ -30,4 +32,9 @@ soft-reset:
reset: reset:
@echo "Hard-resetting Onionr..." @echo "Hard-resetting Onionr..."
rm -rf onionr/data/ | true > /dev/null 2>&1 rm -rf onionr/data/ | true > /dev/null 2>&1
#@./RUN-LINUX.sh version | grep -v "Failed" --color=always
plugins-reset:
@echo "Resetting plugins..."
rm -rf onionr/data/plugins/ | true > /dev/null 2>&1
@./RUN-LINUX.sh version | grep -v "Failed" --color=always @./RUN-LINUX.sh version | grep -v "Failed" --color=always

View File

@ -50,7 +50,7 @@ class API:
''' '''
config.reload() config.reload()
if config.get('devmode', True): if config.get('devmode', True):
self._developmentMode = True self._developmentMode = True
logger.set_level(logger.LEVEL_DEBUG) logger.set_level(logger.LEVEL_DEBUG)
@ -227,8 +227,8 @@ class API:
response = 'none' response = 'none'
resp = Response(response) resp = Response(response)
elif action == 'kex': elif action == 'kex':
peers = self._core.listPeers() peers = self._core.listPeers(getPow=True)
response = ','.join(self._core.listPeers()) response = ','.join(peers)
resp = Response(response) resp = Response(response)
else: else:
resp = Response("") resp = Response("")

@ -1 +0,0 @@
Subproject commit a74e826e9c69e643ead7950f9f76a05ab8664ddc

View File

@ -1,44 +0,0 @@
'''
Onionr - P2P Microblogging Platform & Social network
Handle bitcoin operations
'''
'''
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
from bitpeer.node import *
from bitpeer.storage.shelve import ShelveStorage
import logging, time
import socks, sys
class OnionrBTC:
def __init__(self, lastBlock='00000000000000000021ee6242d08e3797764c9258e54e686bc2afff51baf599', lastHeight=510613, torP=9050):
stream = logging.StreamHandler()
logger = logging.getLogger('halfnode')
logger.addHandler(stream)
logger.setLevel (10)
LASTBLOCK = lastBlock
LASTBLOCKINDEX = lastHeight
self.node = Node ('BTC', ShelveStorage ('data/btc-blocks.db'), lastblockhash=LASTBLOCK, lastblockheight=LASTBLOCKINDEX, torPort=torP)
self.node.bootstrap ()
self.node.connect ()
self.node.loop ()
if __name__ == "__main__":
torPort = int(sys.argv[1])
bitcoin = OnionrBTC(torPort)
while True:
print(bitcoin.node.getBlockHash(bitcoin.node.getLastBlockHeight())) # Using print on purpose, do not change to logger
time.sleep(5)

View File

@ -1,9 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
''' '''
Onionr - P2P Microblogging Platform & Social network. Onionr - P2P Microblogging Platform & Social network.
This file contains both the OnionrCommunicate class for communcating with peers This file contains both the OnionrCommunicate class for communcating with peers
and code to operate as a daemon, getting commands from the command queue database (see core.Core.daemonQueue) and code to operate as a daemon, getting commands from the command queue database (see core.Core.daemonQueue)
''' '''
''' '''
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
@ -19,8 +19,8 @@ and code to operate as a daemon, getting commands from the command queue databas
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
''' '''
import sqlite3, requests, hmac, hashlib, time, sys, os, math, logger, urllib.parse, base64, binascii, random, json import sqlite3, requests, hmac, hashlib, time, sys, os, math, logger, urllib.parse, base64, binascii, random, json, threading
import core, onionrutils, onionrcrypto, netcontroller, onionrproofs, btc, config, onionrplugins as plugins import core, onionrutils, onionrcrypto, netcontroller, onionrproofs, config, onionrplugins as plugins
class OnionrCommunicate: class OnionrCommunicate:
def __init__(self, debug, developmentMode): def __init__(self, debug, developmentMode):
@ -40,28 +40,27 @@ class OnionrCommunicate:
self.ignoredHashes = [] self.ignoredHashes = []
self.highFailureAmount = 7 self.highFailureAmount = 7
'''
logger.info('Starting Bitcoin Node... with Tor socks port:' + str(sys.argv[2]), timestamp=True)
try:
self.bitcoin = btc.OnionrBTC(torP=int(sys.argv[2]))
except _gdbm.error:
pass
logger.info('Bitcoin Node started, on block: ' + self.bitcoin.node.getBlockHash(self.bitcoin.node.getLastBlockHeight()), timestamp=True)
'''
#except:
#logger.fatal('Failed to start Bitcoin Node, exiting...')
#exit(1)
blockProcessTimer = 0 self.communicatorThreads = 0
blockProcessAmount = 5 self.maxThreads = 75
highFailureTimer = 0 self.processBlocksThreads = 0
highFailureRate = 10 self.lookupBlocksThreads = 0
heartBeatTimer = 0
heartBeatRate = 0 self.blocksProcessing = [] # list of blocks currently processing, to avoid trying a block twice at once in 2 seperate threads
pexTimer = 25 # How often we should check for new peers self.peerStatus = {} # network actions (active requests) for peers used mainly to prevent conflicting actions in threads
pexCount = 0
self.communicatorTimers = {} # communicator timers, name: rate (in seconds)
self.communicatorTimerCounts = {}
self.communicatorTimerFuncs = {}
self.registerTimer('blockProcess', 20)
self.registerTimer('highFailure', 10)
self.registerTimer('heartBeat', 10)
self.registerTimer('pex', 120)
logger.debug('Communicator debugging enabled.') logger.debug('Communicator debugging enabled.')
torID = open('data/hs/hostname').read()
with open('data/hs/hostname', 'r') as torID:
todID = torID.read()
apiRunningCheckRate = 10 apiRunningCheckRate = 10
apiRunningCheckCount = 0 apiRunningCheckCount = 0
@ -77,24 +76,44 @@ class OnionrCommunicate:
while True: while True:
command = self._core.daemonQueue() command = self._core.daemonQueue()
# Process blocks based on a timer # Process blocks based on a timer
blockProcessTimer += 1 self.timerTick()
heartBeatTimer += 1 # TODO: migrate below if statements to be own functions which are called in the above timerTick() function
pexCount += 1 if self.communicatorTimers['highFailure'] == self.communicatorTimerCounts['highFailure']:
if highFailureTimer == highFailureRate: self.communicatorTimerCounts['highFailure'] = 0
highFailureTimer = 0
for i in self.peerData: for i in self.peerData:
if self.peerData[i]['failCount'] >= self.highFailureAmount: if self.peerData[i]['failCount'] >= self.highFailureAmount:
self.peerData[i]['failCount'] -= 1 self.peerData[i]['failCount'] -= 1
if pexTimer == pexCount: if self.communicatorTimers['pex'] == self.communicatorTimerCounts['pex']:
self.getNewPeers() pT1 = threading.Thread(target=self.getNewPeers, name="pT1")
pexCount = 0 # TODO: do not reset timer if low peer count pT1.start()
if heartBeatRate == heartBeatTimer: pT2 = threading.Thread(target=self.getNewPeers, name="pT2")
pT2.start()
self.communicatorTimerCounts['pex'] = 0# TODO: do not reset timer if low peer count
if self.communicatorTimers['heartBeat'] == self.communicatorTimerCounts['heartBeat']:
logger.debug('Communicator heartbeat') logger.debug('Communicator heartbeat')
heartBeatTimer = 0 self.communicatorTimerCounts['heartBeat'] = 0
if blockProcessTimer == blockProcessAmount: if self.communicatorTimers['blockProcess'] == self.communicatorTimerCounts['blockProcess']:
self.lookupBlocks() lT1 = threading.Thread(target=self.lookupBlocks, name="lt1", args=(True,))
self.processBlocks() lT2 = threading.Thread(target=self.lookupBlocks, name="lt2", args=(True,))
blockProcessTimer = 0 lT3 = threading.Thread(target=self.lookupBlocks, name="lt3", args=(True,))
lT4 = threading.Thread(target=self.lookupBlocks, name="lt4", args=(True,))
pbT1 = threading.Thread(target=self.processBlocks, name='pbT1', args=(True,))
pbT2 = threading.Thread(target=self.processBlocks, name='pbT2', args=(True,))
pbT3 = threading.Thread(target=self.processBlocks, name='pbT3', args=(True,))
pbT4 = threading.Thread(target=self.processBlocks, name='pbT4', args=(True,))
if (self.maxThreads - 8) >= threading.active_count():
lT1.start()
lT2.start()
lT3.start()
lT4.start()
pbT1.start()
pbT2.start()
pbT3.start()
pbT4.start()
self.communicatorTimerCounts['blockProcess'] = 0
else:
logger.debug(threading.active_count())
logger.debug('Too many threads.')
if command != False: if command != False:
if command[0] == 'shutdown': if command[0] == 'shutdown':
logger.info('Daemon received exit command.', timestamp=True) logger.info('Daemon received exit command.', timestamp=True)
@ -114,6 +133,8 @@ class OnionrCommunicate:
elif command[0] == 'runCheck': elif command[0] == 'runCheck':
logger.info('Status check; looks good.') logger.info('Status check; looks good.')
open('data/.runcheck', 'w+').close() open('data/.runcheck', 'w+').close()
elif command[0] == 'kex':
self.pexCount = pexTimer - 1
elif command[0] == 'event': elif command[0] == 'event':
# todo # todo
pass pass
@ -165,6 +186,28 @@ class OnionrCommunicate:
connection_handlers = {} connection_handlers = {}
id_peer_cache = {} id_peer_cache = {}
def registerTimer(self, timerName, rate, timerFunc=None):
'''Register a communicator timer'''
self.communicatorTimers[timerName] = rate
self.communicatorTimerCounts[timerName] = 0
self.communicatorTimerFuncs[timerName] = timerFunc
def timerTick(self):
'''Increments timers "ticks" and calls funcs if applicable'''
tName = ''
for i in self.communicatorTimers.items():
tName = i[0]
self.communicatorTimerCounts[tName] += 1
if self.communicatorTimerCounts[tName] == self.communicatorTimers[tName]:
try:
self.communicatorTimerFuncs[tName]()
except TypeError:
pass
else:
self.communicatorTimerCounts[tName] = 0
def get_connection_handlers(self, name = None): def get_connection_handlers(self, name = None):
''' '''
Returns a list of callback handlers by name, or, if name is None, it returns all handlers. Returns a list of callback handlers by name, or, if name is None, it returns all handlers.
@ -174,7 +217,7 @@ class OnionrCommunicate:
return self.connection_handlers return self.connection_handlers
elif name in self.connection_handlers: elif name in self.connection_handlers:
return self.connection_handlers[name] return self.connection_handlers[name]
else else:
return list() return list()
def add_connection_handler(self, name, handler): def add_connection_handler(self, name, handler):
@ -274,7 +317,7 @@ class OnionrCommunicate:
events.event('outgoing_direct_connection', data = {'callback' : True, 'communicator' : self, 'data' : data, 'id' : identifier, 'token' : token, 'peer' : peer, 'callback' : callback, 'log' : log}) events.event('outgoing_direct_connection', data = {'callback' : True, 'communicator' : self, 'data' : data, 'id' : identifier, 'token' : token, 'peer' : peer, 'callback' : callback, 'log' : log})
logger.debug('Direct connection (identifier: "%s"): %s' + (identifier, data_str)) logger.debug('Direct connection (identifier: "%s"): %s' % (identifier, data_str))
try: try:
self.performGet('directMessage', peer, data_str) self.performGet('directMessage', peer, data_str)
except: except:
@ -360,10 +403,10 @@ class OnionrCommunicate:
def getNewPeers(self): def getNewPeers(self):
''' '''
Get new peers and keys Get new peers and ed25519 keys
''' '''
peersCheck = 5 # Amount of peers to ask for new peers + keys peersCheck = 1 # Amount of peers to ask for new peers + keys
peersChecked = 0 peersChecked = 0
peerList = list(self._core.listAdders()) # random ordered list of peers peerList = list(self._core.listAdders()) # random ordered list of peers
newKeys = [] newKeys = []
@ -380,40 +423,49 @@ class OnionrCommunicate:
while peersCheck > peersChecked: while peersCheck > peersChecked:
#i = secrets.randbelow(maxN) # cant use prior to 3.6 #i = secrets.randbelow(maxN) # cant use prior to 3.6
i = random.randint(0, maxN) i = random.randint(0, maxN)
logger.info('Using ' + peerList[i] + ' to find new peers', timestamp=True)
try:
if self.peerStatusTaken(peerList[i], 'pex') or self.peerStatusTaken(peerList[i], 'kex'):
continue
except IndexError:
pass
logger.info('Using %s to find new peers...' % peerList[i], timestamp=True)
try: try:
newAdders = self.performGet('pex', peerList[i], skipHighFailureAddress=True) newAdders = self.performGet('pex', peerList[i], skipHighFailureAddress=True)
logger.debug('Attempting to merge address: ') if not newAdders is False: # keep the is False thing in there, it might not be bool
logger.debug(newAdders) logger.debug('Attempting to merge address: %s' % str(newAdders))
self._utils.mergeAdders(newAdders) self._utils.mergeAdders(newAdders)
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
logger.info(peerList[i] + ' connection failed', timestamp=True) logger.info('%s connection failed' % peerList[i], timestamp=True)
continue continue
else: else:
try: try:
logger.info('Using ' + peerList[i] + ' to find new keys') logger.info('Using %s to find new keys...' % peerList[i])
newKeys = self.performGet('kex', peerList[i], skipHighFailureAddress=True) newKeys = self.performGet('kex', peerList[i], skipHighFailureAddress=True)
logger.debug('Attempting to merge pubkey: ') logger.debug('Attempting to merge pubkey: %s' % str(newKeys))
logger.debug(newKeys)
# TODO: Require keys to come with POW token (very large amount of POW) # TODO: Require keys to come with POW token (very large amount of POW)
self._utils.mergeKeys(newKeys) self._utils.mergeKeys(newKeys)
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
logger.info(peerList[i] + ' connection failed', timestamp=True) logger.info('%s connection failed' % peerList[i], timestamp=True)
continue continue
else: else:
peersChecked += 1 peersChecked += 1
return return
def lookupBlocks(self): def lookupBlocks(self, isThread=False):
''' '''
Lookup blocks and merge new ones Lookup blocks and merge new ones
''' '''
if isThread:
self.lookupBlocksThreads += 1
peerList = self._core.listAdders() peerList = self._core.listAdders()
blocks = '' blockList = list()
for i in peerList: for i in peerList:
if self.peerStatusTaken(i, 'getBlockHashes') or self.peerStatusTaken(i, 'getDBHash'):
continue
try: try:
if self.peerData[i]['failCount'] >= self.highFailureAmount: if self.peerData[i]['failCount'] >= self.highFailureAmount:
continue continue
@ -423,66 +475,70 @@ class OnionrCommunicate:
lastDB = self._core.getAddressInfo(i, 'DBHash') lastDB = self._core.getAddressInfo(i, 'DBHash')
if lastDB == None: if lastDB == None:
logger.debug('Fetching hash from ' + str(i) + ', no previous known.') logger.debug('Fetching hash from %s, no previous known.' % str(i))
else: else:
logger.debug('Fetching hash from ' + str(i) + ', ' + str(lastDB) + ' last known') logger.debug('Fetching hash from %s, %s last known' % (str(i), str(lastDB)))
currentDB = self.performGet('getDBHash', i) currentDB = self.performGet('getDBHash', i)
if currentDB != False: if currentDB != False:
logger.debug(i + " hash db (from request): " + currentDB) logger.debug('%s hash db (from request): %s' % (str(i), str(currentDB)))
else: else:
logger.warn("Error getting hash db status for " + i) logger.warn('Failed to get hash db status for %s' % str(i))
if currentDB != False: if currentDB != False:
if lastDB != currentDB: if lastDB != currentDB:
logger.debug('Fetching hash from ' + i + ' - ' + currentDB + ' current hash.') logger.debug('Fetching hash from %s - %s current hash.' % (str(i), currentDB))
try: try:
blocks += self.performGet('getBlockHashes', i) blockList.append(self.performGet('getBlockHashes', i))
except TypeError: except TypeError:
logger.warn('Failed to get data hash from ' + i) logger.warn('Failed to get data hash from %s' % str(i))
self.peerData[i]['failCount'] -= 1 self.peerData[i]['failCount'] -= 1
if self._utils.validateHash(currentDB): if self._utils.validateHash(currentDB):
self._core.setAddressInfo(i, "DBHash", currentDB) self._core.setAddressInfo(i, "DBHash", currentDB)
if len(blocks.strip()) != 0: if len(blockList) != 0:
pass pass
#logger.debug('BLOCKS:' + blocks)
blockList = blocks.split('\n')
for i in blockList: for i in blockList:
if len(i.strip()) == 0: if len(i.strip()) == 0:
continue continue
if self._utils.hasBlock(i): try:
continue if self._utils.hasBlock(i):
continue
except:
logger.warn('Invalid hash') # TODO: move below validate hash check below
pass
if i in self.ignoredHashes: if i in self.ignoredHashes:
continue continue
#logger.debug('Exchanged block (blockList): ' + i) #logger.debug('Exchanged block (blockList): ' + i)
if not self._utils.validateHash(i): if not self._utils.validateHash(i):
# skip hash if it isn't valid # skip hash if it isn't valid
logger.warn('Hash ' + i + ' is not valid') logger.warn('Hash %s is not valid' % str(i))
continue continue
else: else:
self.newHashes[i] = 0 self.newHashes[i] = 0
logger.debug('Adding ' + i + ' to hash database...') logger.debug('Adding %s to hash database...' % str(i))
self._core.addToBlockDB(i) self._core.addToBlockDB(i)
self.lookupBlocksThreads -= 1
return return
def processBlocks(self): def processBlocks(self, isThread=False):
''' '''
Work with the block database and download any missing blocks Work with the block database and download any missing blocks
This is meant to be called from the communicator daemon on its timer. This is meant to be called from the communicator daemon on its timer.
''' '''
if isThread:
for i in self._core.getBlockList(unsaved=True).split("\n"): self.processBlocksThreads += 1
for i in self._core.getBlockList(unsaved = True):
if i != "": if i != "":
if i in self.ignoredHashes: if i in self.blocksProcessing or i in self.ignoredHashes:
#logger.debug('already processing ' + i)
continue continue
else:
self.blocksProcessing.append(i)
try: try:
self.newHashes[i] self.newHashes[i]
except KeyError: except KeyError:
@ -490,61 +546,78 @@ class OnionrCommunicate:
# check if a new hash has been around too long, delete it from database and add it to ignore list # check if a new hash has been around too long, delete it from database and add it to ignore list
if self.newHashes[i] >= self.keepNewHash: if self.newHashes[i] >= self.keepNewHash:
logger.warn('Ignoring block ' + i + ' because it took to long to get valid data.') logger.warn('Ignoring block %s because it took to long to get valid data.' % str(i))
del self.newHashes[i] del self.newHashes[i]
self._core.removeBlock(i) self._core.removeBlock(i)
self.ignoredHashes.append(i) self.ignoredHashes.append(i)
continue continue
self.newHashes[i] += 1 self.newHashes[i] += 1
logger.warn('UNSAVED BLOCK: ' + i) logger.warn('Block is unsaved: %s' % str(i))
data = self.downloadBlock(i) data = self.downloadBlock(i)
# if block was successfull gotten (hash already verified) # if block was successfully gotten (hash already verified)
if data: if data:
del self.newHashes[i] # remove from probation list del self.newHashes[i] # remove from probation list
# deal with block metadata # deal with block metadata
blockContent = self._core.getData(i) blockContent = self._core.getData(i)
try:
blockContent = blockContent.encode()
except AttributeError:
pass
try: try:
#blockMetadata = json.loads(self._core.getData(i)).split('}')[0] + '}' #blockMetadata = json.loads(self._core.getData(i)).split('}')[0] + '}'
blockMetadata = self._core.getData(i).split(b'}')[0] blockMetadata = json.loads(blockContent[:blockContent.find(b'\n')].decode())
try: try:
blockMetadata = blockMetadata.decode() blockMeta2 = json.loads(blockMetadata['meta'])
except KeyError:
blockMeta2 = {'type': ''}
pass
blockContent = blockContent[blockContent.find(b'\n') + 1:]
try:
blockContent = blockContent.decode()
except AttributeError: except AttributeError:
pass pass
blockMetadata = json.loads(blockMetadata + '}') if not self._crypto.verifyPow(blockContent, blockMeta2):
logger.warn("%s has invalid or insufficient proof of work token, deleting..." % str(i))
try: self._core.removeBlock(i)
blockMetadata['sig'] continue
blockMetadata['id']
except KeyError:
pass
else: else:
creator = self._utils.getPeerByHashId(blockMetadata['id']) if (('sig' in blockMetadata) and ('id' in blockMeta2)): # id doesn't exist in blockMeta2, so this won't workin the first place
try:
creator = creator.decode()
except AttributeError:
pass
if self._core._crypto.edVerify(blockContent.split(b'}')[1], creator, blockMetadata['sig'], encodedData=True): #blockData = json.dumps(blockMetadata['meta']) + blockMetadata[blockMetadata.rfind(b'}') + 1:]
self._core.updateBlockInfo(i, 'sig', 'true')
else: creator = self._utils.getPeerByHashId(blockMeta2['id'])
self._core.updateBlockInfo(i, 'sig', 'false') try:
creator = creator.decode()
except AttributeError:
pass
if self._core._crypto.edVerify(blockMetadata['meta'] + blockContent, creator, blockMetadata['sig'], encodedData=True):
logger.info('%s was signed' % str(i))
self._core.updateBlockInfo(i, 'sig', 'true')
else:
logger.warn('%s has an invalid signature' % str(i))
self._core.updateBlockInfo(i, 'sig', 'false')
try: try:
logger.info('Block type is ' + blockMetadata['type']) logger.info('Block type is %s' % str(blockMeta2['type']))
self._core.updateBlockInfo(i, 'dataType', blockMetadata['type']) self._core.updateBlockInfo(i, 'dataType', blockMeta2['type'])
self.removeBlockFromProcessingList(i)
self.removeBlockFromProcessingList(i)
except KeyError: except KeyError:
logger.warn('Block has no type') logger.warn('Block has no type')
pass pass
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
logger.warn('Could not decode block metadata') logger.warn('Could not decode block metadata')
pass self.removeBlockFromProcessingList(i)
self.processBlocksThreads -= 1
return return
def removeBlockFromProcessingList(self, block):
return block in blocksProcessing
def downloadBlock(self, hash, peerTries=3): def downloadBlock(self, hash, peerTries=3):
''' '''
Download a block from random order of peers Download a block from random order of peers
@ -556,8 +629,11 @@ class OnionrCommunicate:
peerTryCount = 0 peerTryCount = 0
for i in peerList: for i in peerList:
if self.peerData[i]['failCount'] >= self.highFailureAmount: try:
continue if self.peerData[i]['failCount'] >= self.highFailureAmount:
continue
except KeyError:
pass
if peerTryCount >= peerTries: if peerTryCount >= peerTries:
break break
@ -581,17 +657,11 @@ class OnionrCommunicate:
if digest == hash.strip(): if digest == hash.strip():
self._core.setData(data) self._core.setData(data)
logger.info('Successfully obtained data for ' + hash, timestamp=True) logger.info('Successfully obtained data for %s' % str(hash), timestamp=True)
retVal = True retVal = True
break break
'''
if data.startswith(b'-txt-'):
self._core.setBlockType(hash, 'txt')
if len(data) < 120:
logger.debug('Block text:\n' + data.decode())
'''
else: else:
logger.warn("Failed to validate " + hash + " " + " hash calculated was " + digest) logger.warn("Failed to validate %s -- hash calculated was %s" % (hash, digest))
peerTryCount += 1 peerTryCount += 1
return retVal return retVal
@ -614,12 +684,12 @@ class OnionrCommunicate:
raise Exception("Could not perform self address check in performGet due to not knowing our address") raise Exception("Could not perform self address check in performGet due to not knowing our address")
if selfCheck: if selfCheck:
if peer.replace('/', '') == self._core.hsAdder: if peer.replace('/', '') == self._core.hsAdder:
logger.warn('Tried to performget to own hidden service, but selfCheck was not set to false') logger.warn('Tried to performGet to own hidden service, but selfCheck was not set to false')
return return
# Store peer in peerData dictionary (non permanent) # Store peer in peerData dictionary (non permanent)
if not peer in self.peerData: if not peer in self.peerData:
self.peerData[peer] = {'connectCount': 0, 'failCount': 0, 'lastConnectTime': math.floor(time.time())} self.peerData[peer] = {'connectCount': 0, 'failCount': 0, 'lastConnectTime': self._utils.getEpoch()}
socksPort = sys.argv[2] socksPort = sys.argv[2]
'''We use socks5h to use tor as DNS''' '''We use socks5h to use tor as DNS'''
proxies = {'http': 'socks5://127.0.0.1:' + str(socksPort), 'https': 'socks5://127.0.0.1:' + str(socksPort)} proxies = {'http': 'socks5://127.0.0.1:' + str(socksPort), 'https': 'socks5://127.0.0.1:' + str(socksPort)}
@ -630,13 +700,14 @@ class OnionrCommunicate:
try: try:
if skipHighFailureAddress and self.peerData[peer]['failCount'] > self.highFailureAmount: if skipHighFailureAddress and self.peerData[peer]['failCount'] > self.highFailureAmount:
retData = False retData = False
logger.debug('Skipping ' + peer + ' because of high failure rate') logger.debug('Skipping %s because of high failure rate.' % peer)
else: else:
logger.debug('Contacting ' + peer + ' on port ' + socksPort) self.peerStatus[peer] = action
logger.debug('Contacting %s on port %s' % (peer, str(socksPort)))
r = requests.get(url, headers=headers, proxies=proxies, timeout=(15, 30)) r = requests.get(url, headers=headers, proxies=proxies, timeout=(15, 30))
retData = r.text retData = r.text
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
logger.warn(action + " failed with peer " + peer + ": " + str(e)) logger.debug("%s failed with peer %s" % (action, peer))
retData = False retData = False
if not retData: if not retData:
@ -644,9 +715,20 @@ class OnionrCommunicate:
else: else:
self.peerData[peer]['connectCount'] += 1 self.peerData[peer]['connectCount'] += 1
self.peerData[peer]['failCount'] -= 1 self.peerData[peer]['failCount'] -= 1
self.peerData[peer]['lastConnectTime'] = math.floor(time.time()) self.peerData[peer]['lastConnectTime'] = self._utils.getEpoch()
self._core.setAddressInfo(peer, 'lastConnect', self._utils.getEpoch())
return retData return retData
def peerStatusTaken(self, peer, status):
'''
Returns if we are currently performing a specific action with a peer.
'''
try:
if self.peerStatus[peer] == status:
return True
except KeyError:
pass
return False
shouldRun = False shouldRun = False
debug = True debug = True

View File

@ -17,11 +17,11 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
''' '''
import sqlite3, os, sys, time, math, base64, tarfile, getpass, simplecrypt, hashlib, nacl, logger, json, netcontroller import sqlite3, os, sys, time, math, base64, tarfile, getpass, simplecrypt, hashlib, nacl, logger, json, netcontroller, math
#from Crypto.Cipher import AES #from Crypto.Cipher import AES
#from Crypto import Random #from Crypto import Random
import onionrutils, onionrcrypto, btc, onionrevents as events import onionrutils, onionrcrypto, onionrproofs, onionrevents as events
if sys.version_info < (3, 6): if sys.version_info < (3, 6):
try: try:
@ -73,22 +73,24 @@ class Core:
except Exception as error: except Exception as error:
logger.error('Failed to initialize core Onionr library.', error=error) logger.error('Failed to initialize core Onionr library.', error=error)
logger.fatal('Cannot recover from error.') logger.fatal('Cannot recover from error.')
exit(1) sys.exit(1)
return return
def addPeer(self, peerID, name=''): def addPeer(self, peerID, powID, name=''):
''' '''
Adds a public key to the key database (misleading function name) Adds a public key to the key database (misleading function name)
DOES NO SAFETY CHECKS if the ID is valid, but prepares the insertion
''' '''
# This function simply adds a peer to the DB # This function simply adds a peer to the DB
if not self._utils.validatePubKey(peerID): if not self._utils.validatePubKey(peerID):
return False return False
if sys.getsizeof(powID) > 60:
logger.warn("POW token for pubkey base64 representation exceeded 60 bytes")
return False
conn = sqlite3.connect(self.peerDB) conn = sqlite3.connect(self.peerDB)
hashID = self._crypto.pubKeyHashID(peerID) hashID = self._crypto.pubKeyHashID(peerID)
c = conn.cursor() c = conn.cursor()
t = (peerID, name, 'unknown', hashID) t = (peerID, name, 'unknown', hashID, powID)
for i in c.execute("SELECT * FROM PEERS where id = '" + peerID + "';"): for i in c.execute("SELECT * FROM PEERS where id = '" + peerID + "';"):
try: try:
@ -99,7 +101,7 @@ class Core:
pass pass
except IndexError: except IndexError:
pass pass
c.execute('INSERT INTO peers (id, name, dateSeen, hashID) VALUES(?, ?, ?, ?);', t) c.execute('INSERT INTO peers (id, name, dateSeen, pow, hashID) VALUES(?, ?, ?, ?, ?);', t)
conn.commit() conn.commit()
conn.close() conn.close()
@ -189,7 +191,8 @@ class Core:
speed int, speed int,
success int, success int,
DBHash text, DBHash text,
failure int failure int,
lastConnect int
); );
''') ''')
conn.commit() conn.commit()
@ -212,7 +215,8 @@ class Core:
bytesStored int, bytesStored int,
trust int, trust int,
pubkeyExchanged int, pubkeyExchanged int,
hashID); hashID text,
pow text not null);
''') ''')
conn.commit() conn.commit()
conn.close() conn.close()
@ -251,7 +255,7 @@ class Core:
return return
def addToBlockDB(self, newHash, selfInsert=False): def addToBlockDB(self, newHash, selfInsert=False, dataSaved=False):
''' '''
Add a hash value to the block db Add a hash value to the block db
@ -263,8 +267,8 @@ class Core:
return return
conn = sqlite3.connect(self.blockDB) conn = sqlite3.connect(self.blockDB)
c = conn.cursor() c = conn.cursor()
currentTime = math.floor(time.time()) currentTime = self._utils.getEpoch()
if selfInsert: if selfInsert or dataSaved:
selfInsert = 1 selfInsert = 1
else: else:
selfInsert = 0 selfInsert = 0
@ -275,11 +279,12 @@ class Core:
return return
def getData(self,hash): def getData(self, hash):
''' '''
Simply return the data associated to a hash Simply return the data associated to a hash
''' '''
try: try:
# logger.debug('Opening %s' % (str(self.blockDataLocation) + str(hash) + '.dat'))
dataFile = open(self.blockDataLocation + hash + '.dat', 'rb') dataFile = open(self.blockDataLocation + hash + '.dat', 'rb')
data = dataFile.read() data = dataFile.read()
dataFile.close() dataFile.close()
@ -387,7 +392,7 @@ class Core:
Add a command to the daemon queue, used by the communication daemon (communicator.py) Add a command to the daemon queue, used by the communication daemon (communicator.py)
''' '''
# Intended to be used by the web server # Intended to be used by the web server
date = math.floor(time.time()) date = self._utils.getEpoch()
conn = sqlite3.connect(self.queueDB) conn = sqlite3.connect(self.queueDB)
c = conn.cursor() c = conn.cursor()
t = (command, data, date) t = (command, data, date)
@ -431,7 +436,7 @@ class Core:
conn.close() conn.close()
return addressList return addressList
def listPeers(self, randomOrder=True): def listPeers(self, randomOrder=True, getPow=False):
''' '''
Return a list of public keys (misleading function name) Return a list of public keys (misleading function name)
@ -448,10 +453,19 @@ class Core:
for i in c.execute(payload): for i in c.execute(payload):
try: try:
if len(i[0]) != 0: if len(i[0]) != 0:
peerList.append(i[0]) if getPow:
peerList.append(i[0] + '-' + i[1])
else:
peerList.append(i[0])
except TypeError: except TypeError:
pass pass
peerList.append(self._crypto.pubKey) if getPow:
try:
peerList.append(self._crypto.pubKey + '-' + self._crypto.pubKeyPowToken)
except TypeError:
pass
else:
peerList.append(self._crypto.pubKey)
conn.close() conn.close()
return peerList return peerList
@ -513,11 +527,12 @@ class Core:
success int, 4 success int, 4
DBHash text, 5 DBHash text, 5
failure int 6 failure int 6
lastConnect 7
''' '''
conn = sqlite3.connect(self.addressDB) conn = sqlite3.connect(self.addressDB)
c = conn.cursor() c = conn.cursor()
command = (address,) command = (address,)
infoNumbers = {'address': 0, 'type': 1, 'knownPeer': 2, 'speed': 3, 'success': 4, 'DBHash': 5, 'failure': 6} infoNumbers = {'address': 0, 'type': 1, 'knownPeer': 2, 'speed': 3, 'success': 4, 'DBHash': 5, 'failure': 6, 'lastConnect': 7}
info = infoNumbers[info] info = infoNumbers[info]
iterCount = 0 iterCount = 0
retVal = '' retVal = ''
@ -539,7 +554,7 @@ class Core:
c = conn.cursor() c = conn.cursor()
command = (data, address) command = (data, address)
# TODO: validate key on whitelist # TODO: validate key on whitelist
if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'DBHash', 'failure'): if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'DBHash', 'failure', 'lastConnect'):
raise Exception("Got invalid database key when setting address info") raise Exception("Got invalid database key when setting address info")
c.execute('UPDATE adders SET ' + key + ' = ? WHERE address=?', command) c.execute('UPDATE adders SET ' + key + ' = ? WHERE address=?', command)
conn.commit() conn.commit()
@ -565,22 +580,36 @@ class Core:
return return
def getBlockList(self, unsaved = False): def getBlockList(self, unsaved = False): # TODO: Use unsaved
''' '''
Get list of our blocks Get list of our blocks
''' '''
conn = sqlite3.connect(self.blockDB) conn = sqlite3.connect(self.blockDB)
c = conn.cursor() c = conn.cursor()
retData = ''
if unsaved: if unsaved:
execute = 'SELECT hash FROM hashes WHERE dataSaved != 1 ORDER BY RANDOM();' execute = 'SELECT hash FROM hashes WHERE dataSaved != 1 ORDER BY RANDOM();'
else: else:
execute = 'SELECT hash FROM hashes ORDER BY RANDOM();' execute = 'SELECT hash FROM hashes ORDER BY RANDOM();'
rows = list()
for row in c.execute(execute): for row in c.execute(execute):
for i in row: for i in row:
retData += i + "\n" rows.append(i)
return retData return rows
def getBlockDate(self, blockHash):
'''
Returns the date a block was received
'''
conn = sqlite3.connect(self.blockDB)
c = conn.cursor()
execute = 'SELECT dateReceived FROM hashes WHERE hash=?;'
args = (blockHash,)
for row in c.execute(execute, args):
for i in row:
return int(i)
return None
def getBlocksByType(self, blockType): def getBlocksByType(self, blockType):
''' '''
@ -588,14 +617,14 @@ class Core:
''' '''
conn = sqlite3.connect(self.blockDB) conn = sqlite3.connect(self.blockDB)
c = conn.cursor() c = conn.cursor()
retData = ''
execute = 'SELECT hash FROM hashes WHERE dataType=?;' execute = 'SELECT hash FROM hashes WHERE dataType=?;'
args = (blockType,) args = (blockType,)
rows = list()
for row in c.execute(execute, args): for row in c.execute(execute, args):
for i in row: for i in row:
retData += i + "\n" rows.append(i)
return retData.split('\n') return rows
def setBlockType(self, hash, blockType): def setBlockType(self, hash, blockType):
''' '''
@ -630,32 +659,57 @@ class Core:
Inserts a block into the network Inserts a block into the network
''' '''
powProof = onionrproofs.POW(data)
powToken = ''
# wait for proof to complete
try:
while True:
powToken = powProof.getResult()
if powToken == False:
time.sleep(0.3)
continue
powHash = powToken[0]
powToken = base64.b64encode(powToken[1])
try:
powToken = powToken.decode()
except AttributeError:
pass
finally:
break
except KeyboardInterrupt:
logger.warn("Got keyboard interrupt while working on inserting block, stopping.")
powProof.shutdown()
return ''
try: try:
data.decode() data.decode()
except AttributeError: except AttributeError:
data = data.encode() data = data.encode()
retData = '' retData = ''
metadata = {'type': header} metadata = {'type': header, 'powHash': powHash, 'powToken': powToken}
sig = {}
metadata = json.dumps(metadata)
metadata = metadata.encode()
signature = ''
if sign: if sign:
signature = self._crypto.edSign(data, self._crypto.privKey, encodeResult=True) signature = self._crypto.edSign(metadata + b'\n' + data, self._crypto.privKey, encodeResult=True)
ourID = self._crypto.pubKeyHashID() ourID = self._crypto.pubKeyHashID()
# Convert from bytes on some py versions? # Convert from bytes on some py versions?
try: try:
ourID = ourID.decode() ourID = ourID.decode()
except AttributeError: except AttributeError:
pass pass
metadata['id'] = ourID metadata = {'sig': signature, 'meta': metadata.decode()}
metadata['sig'] = signature
metadata = json.dumps(metadata) metadata = json.dumps(metadata)
metadata = metadata.encode() metadata = metadata.encode()
if len(data) == 0: if len(data) == 0:
logger.error('Will not insert empty block') logger.error('Will not insert empty block')
else: else:
addedHash = self.setData(metadata + data) addedHash = self.setData(metadata + b'\n' + data)
self.addToBlockDB(addedHash, selfInsert=True) self.addToBlockDB(addedHash, selfInsert=True)
self.setBlockType(addedHash, header) self.setBlockType(addedHash, header)
retData = addedHash retData = addedHash

View File

@ -1,141 +0,0 @@
'''
This is the future Onionr plugin manager. TODO: Add better description.
'''
# useful libraries
import logger, config
import os, sys, json
plugin_name = 'pluginmanager'
keys_data = {'keys' : {}}
# key functions
def writeKeys():
'''
Serializes and writes the keystore in memory to file
'''
file = open(keys_file, 'w')
file.write(json.dumps(keys_data, indent=4, sort_keys=True))
file.close()
def readKeys():
'''
Loads the keystore into memory
'''
global keys_data
keys_data = json.loads(open(keys_file).read())
return keys_data
def getKey(plugin):
'''
Returns the public key for a given plugin
'''
readKeys()
return (keys_data['keys'][plugin] if plugin in keys_data['keys'] else None)
def saveKey(plugin, key):
'''
Saves the public key for a plugin to keystore
'''
keys_data['keys'][plugin] = key
writeKeys()
def check():
'''
Checks to make sure the keystore file still exists
'''
global keys_file
keys_file = pluginapi.plugins.get_data_folder(plugin_name) + 'keystore.json'
if not os.path.isfile(keys_file):
writeKeys()
# command handlers
def help():
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' <plugin> [public key/block hash]')
def commandInstallPlugin():
logger.warn('This feature is not functional or is still in development.')
if len(sys.argv) >= 3:
check()
pluginname = sys.argv[2]
pkobh = None # public key or block hash
if len(sys.argv) >= 4:
# public key or block hash specified
pkobh = sys.argv[3]
else:
# none specified, check if in config file
pkobh = getKey(pluginname)
if pkobh is None:
logger.error('No key for this plugin found in keystore, please specify.')
help()
return True
valid_hash = pluginapi.get_utils().validateHash(pkobh)
real_block = False
valid_key = pluginapi.get_utils().validatePubKey(pkobh)
real_key = False
if valid_hash:
real_block = pluginapi.get_utils().hasBlock(pkobh)
elif valid_key:
real_key = pluginapi.get_utils().hasKey(pkobh)
blockhash = None
if valid_hash and not real_block:
logger.error('Block hash not found. Perhaps it has not been synced yet?')
logger.debug('Is valid hash, but does not belong to a known block.')
return True
elif valid_hash and real_block:
blockhash = str(pkobh)
logger.debug('Using block %s...' % blockhash)
elif valid_key and not real_key:
logger.error('Public key not found. Try adding the node by address manually, if possible.')
logger.debug('Is valid key, but the key is not a known one.')
elif valid_key and real_key:
publickey = str(pkobh)
logger.debug('Using public key %s...' % publickey)
saveKey(pluginname, pkobh)
else:
logger.error('Unknown data "%s"; must be public key or block hash.' % str(pkobh))
return
else:
help()
return True
def commandUninstallPlugin():
logger.info('This feature has not been created yet. Please check back later.')
return
def commandSearchPlugin():
logger.info('This feature has not been created yet. Please check back later.')
return
# event listeners
def on_init(api, data = None):
global pluginapi
pluginapi = api
check()
# register some commands
api.commands.register(['install-plugin', 'installplugin', 'plugin-install', 'install', 'plugininstall'], commandInstallPlugin)
api.commands.register(['remove-plugin', 'removeplugin', 'plugin-remove', 'uninstall-plugin', 'uninstallplugin', 'plugin-uninstall', 'uninstall', 'remove', 'pluginremove'], commandUninstallPlugin)
api.commands.register(['search', 'filter-plugins', 'search-plugins', 'searchplugins', 'search-plugin', 'searchplugin', 'findplugin', 'find-plugin', 'filterplugin', 'plugin-search', 'pluginsearch'], commandSearchPlugin)
# add help menus once the features are actually implemented
return

View File

@ -222,7 +222,7 @@ def error(data, error=None, timestamp=True):
if not error is None: if not error is None:
debug('Error: ' + str(error) + parse_error()) debug('Error: ' + str(error) + parse_error())
# fatal: when the something so bad has happened that the prorgam must stop # fatal: when the something so bad has happened that the program must stop
def fatal(data, timestamp=True): def fatal(data, timestamp=True):
if get_level() <= LEVEL_FATAL: if get_level() <= LEVEL_FATAL:
log('#', data, colors.bg.red + colors.fg.green + colors.bold, timestamp=timestamp) log('#', data, colors.bg.red + colors.fg.green + colors.bold, timestamp=timestamp)

View File

@ -89,13 +89,17 @@ DataDirectory data/tordata/
torVersion.kill() torVersion.kill()
# wait for tor to get to 100% bootstrap # wait for tor to get to 100% bootstrap
for line in iter(tor.stdout.readline, b''): try:
if 'Bootstrapped 100%: Done' in line.decode(): for line in iter(tor.stdout.readline, b''):
break if 'Bootstrapped 100%: Done' in line.decode():
elif 'Opening Socks listener' in line.decode(): break
logger.debug(line.decode().replace('\n', '')) elif 'Opening Socks listener' in line.decode():
else: logger.debug(line.decode().replace('\n', ''))
logger.fatal('Failed to start Tor. Try killing any other Tor processes owned by this user.') else:
logger.fatal('Failed to start Tor. Try killing any other Tor processes owned by this user.')
return False
except KeyboardInterrupt:
logger.fatal("Got keyboard interrupt")
return False return False
logger.info('Finished starting Tor', timestamp=True) logger.info('Finished starting Tor', timestamp=True)

View File

@ -25,9 +25,10 @@ import sys
if sys.version_info[0] == 2 or sys.version_info[1] < 5: if sys.version_info[0] == 2 or sys.version_info[1] < 5:
print('Error, Onionr requires Python 3.4+') print('Error, Onionr requires Python 3.4+')
sys.exit(1) sys.exit(1)
import os, base64, random, getpass, shutil, subprocess, requests, time, platform, datetime, re import os, base64, random, getpass, shutil, subprocess, requests, time, platform, datetime, re, json, getpass
from threading import Thread from threading import Thread
import api, core, config, logger, onionrplugins as plugins, onionrevents as events import api, core, config, logger, onionrplugins as plugins, onionrevents as events
import onionrutils
from onionrutils import OnionrUtils from onionrutils import OnionrUtils
from netcontroller import NetController from netcontroller import NetController
@ -64,7 +65,7 @@ class Onionr:
else: else:
# the default config file doesn't exist, try hardcoded config # the default config file doesn't exist, try hardcoded config
config.set_config({'devmode': True, 'log': {'file': {'output': True, 'path': 'data/output.log'}, 'console': {'output': True, 'color': True}}}) config.set_config({'devmode': True, 'log': {'file': {'output': True, 'path': 'data/output.log'}, 'console': {'output': True, 'color': True}}})
if not exists: if not data_exists:
config.save() config.save()
config.reload() # this will read the configuration file into memory config.reload() # this will read the configuration file into memory
@ -107,12 +108,11 @@ class Onionr:
if not os.path.exists('data/blocks/'): if not os.path.exists('data/blocks/'):
os.mkdir('data/blocks/') os.mkdir('data/blocks/')
# Copy default plugins into plugins folder # Copy default plugins into plugins folder
if not os.path.exists(plugins.get_plugins_folder()): if not os.path.exists(plugins.get_plugins_folder()):
if os.path.exists('default-plugins/'): if os.path.exists('static-data/default-plugins/'):
names = [f for f in os.listdir("default-plugins/") if not os.path.isfile(f)] names = [f for f in os.listdir("static-data/default-plugins/") if not os.path.isfile(f)]
shutil.copytree('default-plugins/', plugins.get_plugins_folder()) shutil.copytree('static-data/default-plugins/', plugins.get_plugins_folder())
# Enable plugins # Enable plugins
for name in names: for name in names:
@ -134,7 +134,7 @@ class Onionr:
# Get configuration # Get configuration
if not exists: if not data_exists:
# Generate default config # Generate default config
# Hostname should only be set if different from 127.x.x.x. Important for DNS rebinding attack prevention. # Hostname should only be set if different from 127.x.x.x. Important for DNS rebinding attack prevention.
if self.debug: if self.debug:
@ -153,6 +153,8 @@ class Onionr:
'config': self.configure, 'config': self.configure,
'start': self.start, 'start': self.start,
'stop': self.killDaemon, 'stop': self.killDaemon,
'status': self.showStats,
'statistics': self.showStats,
'stats': self.showStats, 'stats': self.showStats,
'enable-plugin': self.enablePlugin, 'enable-plugin': self.enablePlugin,
@ -191,6 +193,8 @@ class Onionr:
'addaddress': self.addAddress, 'addaddress': self.addAddress,
'addfile': self.addFile, 'addfile': self.addFile,
'importblocks': self.onionrUtils.importNewBlocks,
'introduce': self.onionrCore.introduceNode, 'introduce': self.onionrCore.introduceNode,
'connect': self.addAddress 'connect': self.addAddress
} }
@ -212,11 +216,12 @@ class Onionr:
'pm': 'Adds a private message to block', 'pm': 'Adds a private message to block',
'get-pms': 'Shows private messages sent to you', 'get-pms': 'Shows private messages sent to you',
'addfile': 'Create an Onionr block from a file', 'addfile': 'Create an Onionr block from a file',
'importblocks': 'import blocks from the disk (Onionr is transport-agnostic!)',
'introduce': 'Introduce your node to the public Onionr network', 'introduce': 'Introduce your node to the public Onionr network',
} }
# initialize plugins # initialize plugins
events.event('init', onionr = self) events.event('init', onionr = self, threaded = False)
command = '' command = ''
try: try:
@ -389,7 +394,8 @@ class Onionr:
addedHash = self.onionrCore.insertBlock(messageToAdd, header='txt') addedHash = self.onionrCore.insertBlock(messageToAdd, header='txt')
#self.onionrCore.addToBlockDB(addedHash, selfInsert=True) #self.onionrCore.addToBlockDB(addedHash, selfInsert=True)
#self.onionrCore.setBlockType(addedHash, 'txt') #self.onionrCore.setBlockType(addedHash, 'txt')
logger.info("Message inserted as as block %s" % addedHash) if addedHash != '':
logger.info("Message inserted as as block %s" % addedHash)
return return
def getPMs(self): def getPMs(self):
@ -457,7 +463,10 @@ class Onionr:
os.makedirs(plugins.get_plugins_folder(plugin_name)) os.makedirs(plugins.get_plugins_folder(plugin_name))
with open(plugins.get_plugins_folder(plugin_name) + '/main.py', 'a') as main: with open(plugins.get_plugins_folder(plugin_name) + '/main.py', 'a') as main:
main.write(open('static-data/default_plugin.py').read().replace('$user', os.getlogin()).replace('$date', datetime.datetime.now().strftime('%Y-%m-%d'))) main.write(open('static-data/default_plugin.py').read().replace('$user', os.getlogin()).replace('$date', datetime.datetime.now().strftime('%Y-%m-%d')).replace('$name', plugin_name))
with open(plugins.get_plugins_folder(plugin_name) + '/info.json', 'a') as main:
main.write(json.dumps({'author' : 'anonymous', 'description' : 'the default description of the plugin', 'version' : '1.0'}))
logger.info('Enabling plugin "%s"...' % plugin_name) logger.info('Enabling plugin "%s"...' % plugin_name)
plugins.enable(plugin_name, self) plugins.enable(plugin_name, self)
@ -550,8 +559,54 @@ class Onionr:
Displays statistics and exits Displays statistics and exits
''' '''
logger.info('Our pubkey: ' + self.onionrCore._crypto.pubKey) try:
logger.info('Our address: ' + self.get_hostname()) # define stats messages here
messages = {
# info about local client
'Onionr Daemon Status' : ((logger.colors.fg.green + 'Online') if self.onionrUtils.isCommunicatorRunning(timeout = 2) else logger.colors.fg.red + 'Offline'),
'Public Key' : self.onionrCore._crypto.pubKey,
'Address' : self.get_hostname(),
# file and folder size stats
'div1' : True, # this creates a solid line across the screen, a div
'Total Block Size' : onionrutils.humanSize(onionrutils.size('data/blocks/')),
'Total Plugin Size' : onionrutils.humanSize(onionrutils.size('data/plugins/')),
'Log File Size' : onionrutils.humanSize(onionrutils.size('data/output.log')),
# count stats
'div2' : True,
'Known Peers Count' : str(len(self.onionrCore.listPeers())),
'Enabled Plugins Count' : str(len(config.get('plugins')['enabled'])) + ' / ' + str(len(os.listdir('data/plugins/')))
}
# color configuration
colors = {
'title' : logger.colors.bold,
'key' : logger.colors.fg.lightgreen,
'val' : logger.colors.fg.green,
'border' : logger.colors.fg.lightblue,
'reset' : logger.colors.reset
}
# pre-processing
maxlength = 0
for key, val in messages.items():
if not (type(val) is bool and val is True):
maxlength = max(len(key), maxlength)
# generate stats table
logger.info(colors['title'] + 'Onionr v%s Statistics' % ONIONR_VERSION + colors['reset'])
logger.info(colors['border'] + '' * (maxlength + 1) + '' + colors['reset'])
for key, val in messages.items():
if not (type(val) is bool and val is True):
logger.info(colors['key'] + str(key).rjust(maxlength) + colors['reset'] + colors['border'] + '' + colors['reset'] + colors['val'] + str(val) + colors['reset'])
else:
logger.info(colors['border'] + '' * (maxlength + 1) + '' + colors['reset'])
logger.info(colors['border'] + '' * (maxlength + 1) + '' + colors['reset'])
except Exception as e:
logger.error('Failed to generate statistics table.', error = e, timestamp = False)
return return
def showHelp(self, command = None): def showHelp(self, command = None):

443
onionr/onionrblockapi.py Normal file
View File

@ -0,0 +1,443 @@
'''
Onionr - P2P Microblogging Platform & Social network.
This class contains the OnionrBlocks class which is a class for working with Onionr blocks
'''
'''
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
import core as onionrcore, logger
import json, os, datetime
class Block:
def __init__(self, hash = None, core = None):
'''
Initializes Onionr
Inputs:
- hash (str): the hash of the block to be imported, if any
- core (Core/str):
- if (Core): this is the Core instance to be used, don't create a new one
- if (str): treat `core` as the block content, and instead, treat `hash` as the block type
Outputs:
- (Block): the new Block instance
'''
# input from arguments
if (type(hash) == str) and (type(core) == str):
self.btype = hash
self.bcontent = core
self.hash = None
self.core = None
else:
self.btype = ''
self.bcontent = ''
self.hash = hash
self.core = core
# initialize variables
self.valid = True
self.raw = None
self.powHash = None
self.powToken = None
self.signed = False
self.signature = None
self.signedData = None
self.blockFile = None
self.bheader = {}
self.bmetadata = {}
# handle arguments
if self.getCore() is None:
self.core = onionrcore.Core()
if not self.getHash() is None:
self.update()
# logic
def update(self, data = None, file = None):
'''
Loads data from a block in to the current object.
Inputs:
- data (str):
- if None: will load from file by hash
- else: will load from `data` string
- file (str):
- if None: will load from file specified in this parameter
- else: will load from wherever block is stored by hash
Outputs:
- (bool): indicates whether or not the operation was successful
'''
try:
# import from string
blockdata = data
# import from file
if blockdata is None:
filelocation = file
if filelocation is None:
if self.getHash() is None:
return False
filelocation = 'data/blocks/%s.dat' % self.getHash()
blockdata = open(filelocation, 'rb').read().decode('utf-8')
self.blockFile = filelocation
else:
self.blockFile = None
# parse block
self.raw = str(blockdata)
self.bheader = json.loads(self.getRaw()[:self.getRaw().index('\n')])
self.bcontent = self.getRaw()[self.getRaw().index('\n') + 1:]
self.bmetadata = json.loads(self.getHeader('meta'))
self.btype = self.getMetadata('type')
self.powHash = self.getMetadata('powHash')
self.powToken = self.getMetadata('powToken')
self.signed = ('sig' in self.getHeader() and self.getHeader('sig') != '')
self.signature = (None if not self.isSigned() else self.getHeader('sig'))
self.signedData = (None if not self.isSigned() else self.getHeader('meta') + '\n' + self.getContent())
self.date = self.getCore().getBlockDate(self.getHash())
if not self.getDate() is None:
self.date = datetime.datetime.fromtimestamp(self.getDate())
self.valid = True
return True
except Exception as e:
logger.error('Failed to update block data.', error = e, timestamp = False)
self.valid = False
return False
def delete(self):
'''
Deletes the block's file and records, if they exist
Outputs:
- (bool): whether or not the operation was successful
'''
if self.exists():
os.remove(self.getBlockFile())
removeBlock(self.getHash())
return True
return False
def save(self, sign = False, recreate = True):
'''
Saves a block to file and imports it into Onionr
Inputs:
- sign (bool): whether or not to sign the block before saving
- recreate (bool): if the block already exists, whether or not to recreate the block and save under a new hash
Outputs:
- (bool): whether or not the operation was successful
'''
try:
if self.isValid() is True:
if (not self.getBlockFile() is None) and (recreate is True):
with open(self.getBlockFile(), 'wb') as blockFile:
blockFile.write(self.getRaw().encode())
self.update()
else:
self.hash = self.getCore().insertBlock(self.getContent(), header = self.getType(), sign = sign)
self.update()
return True
else:
logger.warn('Not writing block; it is invalid.')
except Exception as e:
logger.error('Failed to save block.', error = e, timestamp = False)
return False
# getters
def getHash(self):
'''
Returns the hash of the block if saved to file
Outputs:
- (str): the hash of the block, or None
'''
return self.hash
def getCore(self):
'''
Returns the Core instance being used by the Block
Outputs:
- (Core): the Core instance
'''
return self.core
def getType(self):
'''
Returns the type of the block
Outputs:
- (str): the type of the block
'''
return self.btype
def getRaw(self):
'''
Returns the raw contents of the block, if saved to file
Outputs:
- (str): the raw contents of the block, or None
'''
return str(self.raw)
def getHeader(self, key = None):
'''
Returns the header information
Inputs:
- key (str): only returns the value of the key in the header
Outputs:
- (dict/str): either the whole header as a dict, or one value
'''
if not key is None:
return self.getHeader()[key]
else:
return self.bheader
def getMetadata(self, key = None):
'''
Returns the metadata information
Inputs:
- key (str): only returns the value of the key in the metadata
Outputs:
- (dict/str): either the whole metadata as a dict, or one value
'''
if not key is None:
return self.getMetadata()[key]
else:
return self.bmetadata
def getContent(self):
'''
Returns the contents of the block
Outputs:
- (str): the contents of the block
'''
return str(self.bcontent)
def getDate(self):
'''
Returns the date that the block was received, if loaded from file
Outputs:
- (datetime): the date that the block was received
'''
return self.date
def getBlockFile(self):
'''
Returns the location of the block file if it is saved
Outputs:
- (str): the location of the block file, or None
'''
return self.blockFile
def isValid(self):
'''
Checks if the block is valid
Outputs:
- (bool): whether or not the block is valid
'''
return self.valid
def isSigned(self):
'''
Checks if the block was signed
Outputs:
- (bool): whether or not the block is signed
'''
return self.signed
def getSignature(self):
'''
Returns the base64-encoded signature
Outputs:
- (str): the signature, or None
'''
return self.signature
def getSignedData(self):
'''
Returns the data that was signed
Outputs:
- (str): the data that was signed, or None
'''
return self.signedData
def isSigner(self, signer, encodedData = True):
'''
Checks if the block was signed by the signer inputted
Inputs:
- signer (str): the public key of the signer to check against
- encodedData (bool): whether or not the `signer` argument is base64 encoded
Outputs:
- (bool): whether or not the signer of the block is the signer inputted
'''
try:
if (not self.isSigned()) or (not self.getCore()._utils.validatePubKey(signer)):
return False
return bool(self.getCore()._crypto.edVerify(self.getSignedData(), signer, self.getSignature(), encodedData = encodedData))
except:
return False
# setters
def setType(self, btype):
'''
Sets the type of the block
Inputs:
- btype (str): the type of block to be set to
Outputs:
- (Block): the block instance
'''
self.btype = btype
return self
def setContent(self, bcontent):
'''
Sets the contents of the block
Inputs:
- bcontent (str): the contents to be set to
Outputs:
- (Block): the block instance
'''
self.bcontent = str(bcontent)
return self
# static
def getBlocks(type = None, signer = None, signed = None, reverse = False, core = None):
'''
Returns a list of Block objects based on supplied filters
Inputs:
- type (str): filters by block type
- signer (str/list): filters by signer (one in the list has to be a signer)
- signed (bool): filters out by whether or not the block is signed
- reverse (bool): reverses the list if True
- core (Core): lets you optionally supply a core instance so one doesn't need to be started
Outputs:
- (list): a list of Block objects that match the input
'''
try:
core = (core if not core is None else onionrcore.Core())
relevant_blocks = list()
blocks = (core.getBlockList() if type is None else core.getBlocksByType(type))
for block in blocks:
if Block.exists(block):
block = Block(block, core = core)
relevant = True
if (not signed is None) and (block.isSigned() != bool(signed)):
relevant = False
if not signer is None:
if isinstance(signer, (str,)):
signer = [signer]
isSigner = False
for key in signer:
if block.isSigner(key):
isSigner = True
break
if not isSigner:
relevant = False
if relevant:
relevant_blocks.append(block)
if bool(reverse):
relevant_blocks.reverse()
return relevant_blocks
except Exception as e:
logger.debug(('Failed to get blocks: %s' % str(e)) + logger.parse_error())
return list()
def exists(hash):
'''
Checks if a block is saved to file or not
Inputs:
- hash (str/Block):
- if (Block): check if this block is saved to file
- if (str): check if a block by this hash is in file
Outputs:
- (bool): whether or not the block file exists
'''
if hash is None:
return False
elif type(hash) == Block:
blockfile = hash.getBlockFile()
else:
blockfile = 'data/blocks/%s.dat' % hash
return os.path.exists(blockfile) and os.path.isfile(blockfile)

View File

@ -1,26 +0,0 @@
'''
Onionr - P2P Microblogging Platform & Social network.
This class contains the OnionrBlocks class which is a class for working with Onionr blocks
'''
'''
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
import json
class OnionrBlocks:
def __init__(self, coreInstance):
return
def metadataGenerate(self):
return

View File

@ -17,15 +17,19 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
''' '''
import nacl.signing, nacl.encoding, nacl.public, nacl.secret, os, binascii, base64, hashlib, logger import nacl.signing, nacl.encoding, nacl.public, nacl.hash, nacl.secret, os, binascii, base64, hashlib, logger, onionrproofs, time, math
class OnionrCrypto: class OnionrCrypto:
def __init__(self, coreInstance): def __init__(self, coreInstance):
self._core = coreInstance self._core = coreInstance
self._keyFile = 'data/keys.txt' self._keyFile = 'data/keys.txt'
self.keyPowFile = 'data/keyPow.txt'
self.pubKey = None self.pubKey = None
self.privKey = None self.privKey = None
self.pubKeyPowToken = None
#self.pubKeyPowHash = None
self.HASH_ID_ROUNDS = 2000 self.HASH_ID_ROUNDS = 2000
# Load our own pub/priv Ed25519 keys, gen & save them if they don't exist # Load our own pub/priv Ed25519 keys, gen & save them if they don't exist
@ -34,12 +38,29 @@ class OnionrCrypto:
keys = keys.read().split(',') keys = keys.read().split(',')
self.pubKey = keys[0] self.pubKey = keys[0]
self.privKey = keys[1] self.privKey = keys[1]
try:
with open(self.keyPowFile, 'r') as powFile:
data = powFile.read()
self.pubKeyPowToken = data
except (FileNotFoundError, IndexError):
pass
else: else:
keys = self.generatePubKey() keys = self.generatePubKey()
self.pubKey = keys[0] self.pubKey = keys[0]
self.privKey = keys[1] self.privKey = keys[1]
with open(self._keyFile, 'w') as keyfile: with open(self._keyFile, 'w') as keyfile:
keyfile.write(self.pubKey + ',' + self.privKey) keyfile.write(self.pubKey + ',' + self.privKey)
with open(self.keyPowFile, 'w') as keyPowFile:
proof = onionrproofs.POW(self.pubKey)
logger.info('Doing necessary work to insert our public key')
while True:
time.sleep(0.2)
powToken = proof.getResult()
if powToken != False:
break
keyPowFile.write(base64.b64encode(powToken[1]).decode())
self.pubKeyPowToken = powToken[1]
self.pubKeyPowHash = powToken[0]
return return
def edVerify(self, data, key, sig, encodedData=True): def edVerify(self, data, key, sig, encodedData=True):
@ -60,14 +81,13 @@ class OnionrCrypto:
retData = key.verify(data, sig) # .encode() is not the same as nacl.encoding retData = key.verify(data, sig) # .encode() is not the same as nacl.encoding
except nacl.exceptions.BadSignatureError: except nacl.exceptions.BadSignatureError:
pass pass
else: else:
try: try:
retData = key.verify(data, sig) retData = key.verify(data, sig)
except nacl.exceptions.BadSignatureError: except nacl.exceptions.BadSignatureError:
pass pass
return retData return retData
def edSign(self, data, key, encodeResult=False): def edSign(self, data, key, encodeResult=False):
'''Ed25519 sign data''' '''Ed25519 sign data'''
try: try:
@ -138,8 +158,6 @@ class OnionrCrypto:
decrypted = self.symmetricDecrypt(data, key, encodedKey=True) decrypted = self.symmetricDecrypt(data, key, encodedKey=True)
return decrypted return decrypted
return
def symmetricEncrypt(self, data, key, encodedKey=False, returnEncoded=True): def symmetricEncrypt(self, data, key, encodedKey=False, returnEncoded=True):
'''Encrypt data to a 32-byte key (Salsa20-Poly1305 MAC)''' '''Encrypt data to a 32-byte key (Salsa20-Poly1305 MAC)'''
if encodedKey: if encodedKey:
@ -177,7 +195,7 @@ class OnionrCrypto:
if returnEncoded: if returnEncoded:
decrypted = base64.b64encode(decrypted) decrypted = base64.b64encode(decrypted)
return decrypted return decrypted
def generateSymmetricPeer(self, peer): def generateSymmetricPeer(self, peer):
'''Generate symmetric key for a peer and save it to the peer database''' '''Generate symmetric key for a peer and save it to the peer database'''
key = self.generateSymmetric() key = self.generateSymmetric()
@ -193,7 +211,7 @@ class OnionrCrypto:
private_key = nacl.signing.SigningKey.generate() private_key = nacl.signing.SigningKey.generate()
public_key = private_key.verify_key.encode(encoder=nacl.encoding.Base32Encoder()) public_key = private_key.verify_key.encode(encoder=nacl.encoding.Base32Encoder())
return (public_key.decode(), private_key.encode(encoder=nacl.encoding.Base32Encoder()).decode()) return (public_key.decode(), private_key.encode(encoder=nacl.encoding.Base32Encoder()).decode())
def pubKeyHashID(self, pubkey=''): def pubKeyHashID(self, pubkey=''):
'''Accept a ed25519 public key, return a truncated result of X many sha3_256 hash rounds''' '''Accept a ed25519 public key, return a truncated result of X many sha3_256 hash rounds'''
if pubkey == '': if pubkey == '':
@ -209,4 +227,49 @@ class OnionrCrypto:
hasher.update(pubkey + prev) hasher.update(pubkey + prev)
prev = hasher.hexdigest() prev = hasher.hexdigest()
result = prev result = prev
return result return result
def sha3Hash(self, data):
hasher = hashlib.sha3_256()
hasher.update(data)
return hasher.hexdigest()
def blake2bHash(self, data):
try:
data = data.encode()
except AttributeError:
pass
return nacl.hash.blake2b(data)
def verifyPow(self, blockContent, metadata):
'''
Verifies the proof of work associated with a block
'''
retData = False
if not (('powToken' in metadata) and ('powHash' in metadata)):
return False
dataLen = len(blockContent)
expectedHash = self.blake2bHash(base64.b64decode(metadata['powToken']) + self.blake2bHash(blockContent.encode()))
difficulty = 0
try:
expectedHash = expectedHash.decode()
except AttributeError:
pass
if metadata['powHash'] == expectedHash:
difficulty = math.floor(dataLen / 1000000)
mainHash = '0000000000000000000000000000000000000000000000000000000000000000'#nacl.hash.blake2b(nacl.utils.random()).decode()
puzzle = mainHash[:difficulty]
if metadata['powHash'][:difficulty] == puzzle:
# logger.debug('Validated block pow')
retData = True
else:
logger.debug("Invalid token (#1)")
else:
logger.debug('Invalid token (#2): Expected hash %s, got hash %s...' % (metadata['powHash'], expectedHash))
return retData

View File

@ -18,7 +18,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
''' '''
import onionrplugins as plugins, logger import onionrplugins, core as onionrcore, logger
class DaemonAPI: class DaemonAPI:
def __init__(self, pluginapi): def __init__(self, pluginapi):
@ -40,9 +40,7 @@ class DaemonAPI:
return return
def local_command(self, command): def local_command(self, command):
self.pluginapi.get_utils().localCommand(self, command) return self.pluginapi.get_utils().localCommand(self, command)
return
def queue_pop(self): def queue_pop(self):
return self.get_core().daemonQueue() return self.get_core().daemonQueue()
@ -52,34 +50,34 @@ class PluginAPI:
self.pluginapi = pluginapi self.pluginapi = pluginapi
def start(self, name): def start(self, name):
plugins.start(name) onionrplugins.start(name)
def stop(self, name): def stop(self, name):
plugins.stop(name) onionrplugins.stop(name)
def reload(self, name): def reload(self, name):
plugins.reload(name) onionrplugins.reload(name)
def enable(self, name): def enable(self, name):
plugins.enable(name) onionrplugins.enable(name)
def disable(self, name): def disable(self, name):
plugins.disable(name) onionrplugins.disable(name)
def event(self, name, data = {}): def event(self, name, data = {}):
events.event(name, data = data, onionr = self.pluginapi.get_onionr()) events.event(name, data = data, onionr = self.pluginapi.get_onionr())
def is_enabled(self, name): def is_enabled(self, name):
return plugins.is_enabled(name) return onionrplugins.is_enabled(name)
def get_enabled_plugins(self): def get_enabled_plugins(self):
return plugins.get_enabled() return onionrplugins.get_enabled()
def get_folder(self, name = None, absolute = True): def get_folder(self, name = None, absolute = True):
return plugins.get_plugins_folder(name = name, absolute = absolute) return onionrplugins.get_plugins_folder(name = name, absolute = absolute)
def get_data_folder(self, name, absolute = True): def get_data_folder(self, name, absolute = True):
return plugins.get_plugin_data_folder(name, absolute = absolute) return onionrplugins.get_plugin_data_folder(name, absolute = absolute)
def daemon_event(self, event, plugin = None): def daemon_event(self, event, plugin = None):
return # later make local command like /client/?action=makeEvent&event=eventname&module=modulename return # later make local command like /client/?action=makeEvent&event=eventname&module=modulename
@ -136,6 +134,10 @@ class pluginapi:
def __init__(self, onionr, data): def __init__(self, onionr, data):
self.onionr = onionr self.onionr = onionr
self.data = data self.data = data
if self.onionr is None:
self.core = onionrcore.Core()
else:
self.core = self.onionr.onionrCore
self.daemon = DaemonAPI(self) self.daemon = DaemonAPI(self)
self.plugins = PluginAPI(self) self.plugins = PluginAPI(self)
@ -148,10 +150,13 @@ class pluginapi:
return self.data return self.data
def get_core(self): def get_core(self):
return self.get_onionr().onionrCore return self.core
def get_utils(self): def get_utils(self):
return self.get_onionr().onionrUtils return self.get_core()._utils
def get_crypto(self):
return self.get_core()._crypto
def get_daemonapi(self): def get_daemonapi(self):
return self.daemon return self.daemon

View File

@ -62,17 +62,20 @@ def enable(name, onionr = None, start_event = True):
if exists(name): if exists(name):
enabled_plugins = get_enabled_plugins() enabled_plugins = get_enabled_plugins()
enabled_plugins.append(name) if not name in enabled_plugins:
config_plugins = config.get('plugins') enabled_plugins.append(name)
config_plugins['enabled'] = enabled_plugins config_plugins = config.get('plugins')
config.set('plugins', config_plugins, True) config_plugins['enabled'] = enabled_plugins
config.set('plugins', config_plugins, True)
events.call(get_plugin(name), 'enable', onionr) events.call(get_plugin(name), 'enable', onionr)
if start_event is True: if start_event is True:
start(name) start(name)
return True return True
else:
return False
else: else:
logger.error('Failed to enable plugin \"' + name + '\", disabling plugin.') logger.error('Failed to enable plugin \"' + name + '\", disabling plugin.')
disable(name) disable(name)

View File

@ -18,8 +18,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
''' '''
import nacl.encoding, nacl.hash, nacl.utils, time, math, threading, binascii, logger import nacl.encoding, nacl.hash, nacl.utils, time, math, threading, binascii, logger, sys
import btc import core
class POW: class POW:
def pow(self, reporting = False): def pow(self, reporting = False):
@ -30,20 +30,10 @@ class POW:
answer = '' answer = ''
heartbeat = 200000 heartbeat = 200000
hbCount = 0 hbCount = 0
blockCheck = 300000 # How often the hasher should check if the bitcoin block is updated (slows hashing but prevents less wasted work) myCore = core.Core()
blockCheckCount = 0
block = '' #self.bitcoinNode.getBlockHash(self.bitcoinNode.getLastBlockHeight())
while self.hashing: while self.hashing:
''' rand = nacl.utils.random()
if blockCheckCount == blockCheck: token = nacl.hash.blake2b(rand + self.data).decode()
if self.reporting:
logger.debug('Refreshing Bitcoin block')
block = '' #self.bitcoinNode.getBlockHash(self.bitcoinNode.getLastBlockHeight())
blockCheckCount = 0
blockCheckCount += 1
hbCount += 1
'''
token = nacl.hash.blake2b(nacl.utils.random()).decode()
#print(token) #print(token)
if self.puzzle == token[0:self.difficulty]: if self.puzzle == token[0:self.difficulty]:
self.hashing = False self.hashing = False
@ -56,17 +46,28 @@ class POW:
if self.reporting: if self.reporting:
logger.info('Found token ' + token, timestamp=True) logger.info('Found token ' + token, timestamp=True)
logger.info('took ' + str(endTime - startTime) + ' seconds', timestamp=True) logger.info('took ' + str(endTime - startTime) + ' seconds', timestamp=True)
self.result = token self.result = (token, rand)
def __init__(self, difficulty, bitcoinNode=''): def __init__(self, data):
self.foundHash = False self.foundHash = False
self.difficulty = difficulty self.difficulty = 0
self.data = data
dataLen = sys.getsizeof(data)
self.difficulty = math.floor(dataLen/1000000)
if self.difficulty <= 2:
self.difficulty = 4
try:
self.data = self.data.encode()
except AttributeError:
pass
self.data = nacl.hash.blake2b(self.data)
logger.debug('Computing difficulty of ' + str(self.difficulty)) logger.debug('Computing difficulty of ' + str(self.difficulty))
self.mainHash = '0000000000000000000000000000000000000000000000000000000000000000'#nacl.hash.blake2b(nacl.utils.random()).decode() self.mainHash = '0000000000000000000000000000000000000000000000000000000000000000'#nacl.hash.blake2b(nacl.utils.random()).decode()
self.puzzle = self.mainHash[0:self.difficulty] self.puzzle = self.mainHash[0:self.difficulty]
self.bitcoinNode = bitcoinNode
#logger.debug('trying to find ' + str(self.mainHash)) #logger.debug('trying to find ' + str(self.mainHash))
tOne = threading.Thread(name='one', target=self.pow, args=(True,)) tOne = threading.Thread(name='one', target=self.pow, args=(True,))
tTwo = threading.Thread(name='two', target=self.pow, args=(True,)) tTwo = threading.Thread(name='two', target=self.pow, args=(True,))

View File

@ -18,7 +18,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
''' '''
# Misc functions that do not fit in the main api, but are useful # Misc functions that do not fit in the main api, but are useful
import getpass, sys, requests, os, socket, hashlib, logger, sqlite3, config, binascii, time, base64, json import getpass, sys, requests, os, socket, hashlib, logger, sqlite3, config, binascii, time, base64, json, glob, shutil, math
import nacl.signing, nacl.encoding import nacl.signing, nacl.encoding
if sys.version_info < (3, 6): if sys.version_info < (3, 6):
@ -71,7 +71,7 @@ class OnionrUtils:
if block == '': if block == '':
logger.error('Could not send PM') logger.error('Could not send PM')
else: else:
logger.info('Sent PM, hash: ' + block) logger.info('Sent PM, hash: %s' % block)
except Exception as error: except Exception as error:
logger.error('Failed to send PM.', error=error) logger.error('Failed to send PM.', error=error)
@ -101,9 +101,26 @@ class OnionrUtils:
retVal = False retVal = False
if newKeyList != False: if newKeyList != False:
for key in newKeyList.split(','): for key in newKeyList.split(','):
if not key in self._core.listPeers(randomOrder=False) and type(key) != None and key != self._core._crypto.pubKey: key = key.split('-')
if self._core.addPeer(key): try:
retVal = True if len(key[0]) > 60 or len(key[1]) > 1000:
logger.warn('%s or its pow value is too large.' % key[0])
continue
except IndexError:
logger.warn('No pow token')
continue
powHash = self._core._crypto.blake2bHash(base64.b64decode(key[1]) + self._core._crypto.blake2bHash(key[0].encode()))
try:
powHash = powHash.encode()
except AttributeError:
pass
if powHash.startswith(b'0000'):
if not key[0] in self._core.listPeers(randomOrder=False) and type(key) != None and key[0] != self._core._crypto.pubKey:
if self._core.addPeer(key[0], key[1]):
retVal = True
else:
logger.warn(powHash)
logger.warn('%s pow failed' % key[0])
return retVal return retVal
except Exception as error: except Exception as error:
logger.error('Failed to merge keys.', error=error) logger.error('Failed to merge keys.', error=error)
@ -118,15 +135,15 @@ class OnionrUtils:
retVal = False retVal = False
if newAdderList != False: if newAdderList != False:
for adder in newAdderList.split(','): for adder in newAdderList.split(','):
if not adder in self._core.listAdders(randomOrder=False) and adder.strip() != self.getMyAddress(): if not adder in self._core.listAdders(randomOrder = False) and adder.strip() != self.getMyAddress():
if self._core.addAddress(adder): if self._core.addAddress(adder):
logger.info('Added ' + adder + ' to db.', timestamp=True) logger.info('Added %s to db.' % adder, timestamp = True)
retVal = True retVal = True
else: else:
logger.debug(adder + " is either our address or already in our DB") logger.debug('%s is either our address or already in our DB' % adder)
return retVal return retVal
except Exception as error: except Exception as error:
logger.error('Failed to merge adders.', error=error) logger.error('Failed to merge adders.', error = error)
return False return False
def getMyAddress(self): def getMyAddress(self):
@ -134,7 +151,7 @@ class OnionrUtils:
with open('./data/hs/hostname', 'r') as hostname: with open('./data/hs/hostname', 'r') as hostname:
return hostname.read().strip() return hostname.read().strip()
except Exception as error: except Exception as error:
logger.error('Failed to read my address.', error=error) logger.error('Failed to read my address.', error = error)
return None return None
def localCommand(self, command, silent = True): def localCommand(self, command, silent = True):
@ -149,7 +166,7 @@ class OnionrUtils:
retData = requests.get('http://' + open('data/host.txt', 'r').read() + ':' + str(config.get('client')['port']) + '/client/?action=' + command + '&token=' + str(config.get('client')['client_hmac']) + '&timingToken=' + self.timingToken).text retData = requests.get('http://' + open('data/host.txt', 'r').read() + ':' + str(config.get('client')['port']) + '/client/?action=' + command + '&token=' + str(config.get('client')['client_hmac']) + '&timingToken=' + self.timingToken).text
except Exception as error: except Exception as error:
if not silent: if not silent:
logger.error('Failed to make local request (command: ' + str(command) + ').', error=error) logger.error('Failed to make local request (command: %s).' % command, error=error)
retData = False retData = False
return retData return retData
@ -327,7 +344,7 @@ class OnionrUtils:
''' '''
Find, decrypt, and return array of PMs (array of dictionary, {from, text}) Find, decrypt, and return array of PMs (array of dictionary, {from, text})
''' '''
#blocks = self._core.getBlockList().split('\n') #blocks = self._core.getBlockList()
blocks = self._core.getBlocksByType('pm') blocks = self._core.getBlocksByType('pm')
message = '' message = ''
sender = '' sender = ''
@ -336,52 +353,43 @@ class OnionrUtils:
continue continue
try: try:
with open('data/blocks/' + i + '.dat', 'r') as potentialMessage: with open('data/blocks/' + i + '.dat', 'r') as potentialMessage:
data = potentialMessage.read().split('}') potentialMessage = potentialMessage.read()
message = data[1] blockMetadata = json.loads(potentialMessage[:potentialMessage.find('\n')])
sigResult = '' blockContent = potentialMessage[potentialMessage.find('\n') + 1:]
signer = ''
try: try:
metadata = json.loads(data[0] + '}') message = self._core._crypto.pubKeyDecrypt(blockContent, encodedData=True, anonymous=True)
except json.decoder.JSONDecodeError:
metadata = {}
'''
sigResult = self._core._crypto.edVerify(message, signer, sig, encodedData=True)
#sigResult = False
if sigResult != False:
sigResult = 'Valid signature by ' + signer
else:
sigResult = 'Invalid signature by ' + signer
'''
try:
message = self._core._crypto.pubKeyDecrypt(message, encodedData=True, anonymous=True)
except nacl.exceptions.CryptoError as e: except nacl.exceptions.CryptoError as e:
logger.error('Unable to decrypt ' + i, error=e) pass
else: else:
try: try:
message = json.loads(message.decode()) message = message.decode()
message['msg'] except AttributeError:
message['id'] pass
message['sig']
try:
message = json.loads(message)
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
logger.error('Could not decode PM JSON') pass
except KeyError:
logger.error('PM is missing JSON keys')
else: else:
if self.validatePubKey(message['id']): logger.info('Decrypted %s:' % i)
sigResult = self._core._crypto.edVerify(message['msg'], message['id'], message['sig'], encodedData=True) logger.info(message["msg"])
logger.info('-----------------------------------')
logger.info('Recieved message: ' + message['msg']) signer = message["id"]
if sigResult: sig = message["sig"]
logger.info('Valid signature by ' + message['id'])
if self.validatePubKey(signer):
if self._core._crypto.edVerify(message["msg"], signer, sig, encodedData=True):
logger.info("Good signature by %s" % signer)
else: else:
logger.warn('Invalid signature by ' + message['id']) logger.warn("Bad signature by %s" % signer)
else:
logger.warn('Bad sender id: %s' % signer)
except FileNotFoundError: except FileNotFoundError:
pass pass
except Exception as error: except Exception as error:
logger.error('Failed to open block ' + str(i) + '.', error=error) logger.error('Failed to open block %s.' % i, error=error)
return return
def getPeerByHashId(self, hash): def getPeerByHashId(self, hash):
@ -423,4 +431,74 @@ class OnionrUtils:
return False return False
def token(self, size = 32): def token(self, size = 32):
'''
Generates a secure random hex encoded token
'''
return binascii.hexlify(os.urandom(size)) return binascii.hexlify(os.urandom(size))
def importNewBlocks(self, scanDir=''):
'''
This function is intended to scan for new blocks ON THE DISK and import them
'''
blockList = self._core.getBlockList()
if scanDir == '':
scanDir = self._core.blockDataLocation
if not scanDir.endswith('/'):
scanDir += '/'
for block in glob.glob(scanDir + "*.dat"):
if block.replace(scanDir, '').replace('.dat', '') not in blockList:
logger.info('Found new block on dist %s' % block)
with open(block, 'rb') as newBlock:
block = block.replace(scanDir, '').replace('.dat', '')
if self._core._crypto.sha3Hash(newBlock.read()) == block.replace('.dat', ''):
self._core.addToBlockDB(block.replace('.dat', ''), dataSaved=True)
logger.info('Imported block %s.' % block)
else:
logger.warn('Failed to verify hash for %s' % block)
def progressBar(self, value = 0, endvalue = 100, width = None):
'''
Outputs a progress bar with a percentage. Write \n after use.
'''
if width is None or height is None:
width, height = shutil.get_terminal_size((80, 24))
bar_length = width - 6
percent = float(value) / endvalue
arrow = '' * int(round(percent * bar_length)-1) + '>'
spaces = ' ' * (bar_length - len(arrow))
sys.stdout.write("\r{0}{1}%".format(arrow + spaces, int(round(percent * 100))))
sys.stdout.flush()
def getEpoch(self):
'''returns epoch'''
return math.floor(time.time())
def size(path='.'):
'''
Returns the size of a folder's contents in bytes
'''
total = 0
if os.path.exists(path):
if os.path.isfile(path):
total = os.path.getsize(path)
else:
for entry in os.scandir(path):
if entry.is_file():
total += entry.stat().st_size
elif entry.is_dir():
total += size(entry.path)
return total
def humanSize(num, suffix='B'):
'''
Converts from bytes to a human readable format.
'''
for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']:
if abs(num) < 1024.0:
return "%.1f %s%s" % (num, unit, suffix)
num /= 1024.0
return "%.1f %s%s" % (num, 'Yi', suffix)

View File

@ -0,0 +1,5 @@
{
"name" : "gui",
"version" : "1.0",
"author" : "onionr"
}

View File

@ -16,46 +16,54 @@
''' '''
# Imports some useful libraries # Imports some useful libraries
import logger, config import logger, config, core
import os, sqlite3, core import os, sqlite3, threading
from onionrblockapi import Block
plugin_name = 'gui'
def send():
global message
block = Block()
block.setType('txt')
block.setContent(message)
logger.debug('Sent message in block %s.' % block.save(sign = True))
def sendMessage(): def sendMessage():
global sendEntry global sendEntry
messageToAdd = '-txt-' + sendEntry.get() global message
#addedHash = pluginapi.get_core().setData(messageToAdd) message = sendEntry.get()
#pluginapi.get_core().addToBlockDB(addedHash, selfInsert=True)
#pluginapi.get_core().setBlockType(addedHash, 'txt') t = threading.Thread(target = send)
pluginapi.get_core().insertBlock(messageToAdd, header='txt', sign=True) t.start()
sendEntry.delete(0, END)
sendEntry.delete(0, len(message))
def update(): def update():
global listedBlocks, listbox, runningCheckDelayCount, runningCheckDelay, root, daemonStatus global listedBlocks, listbox, runningCheckDelayCount, runningCheckDelay, root, daemonStatus
# TO DO: migrate to new header format for i in Block.getBlocks(type = 'txt'):
for i in pluginapi.get_core().getBlocksByType('txt'): if i.getContent().strip() == '' or i.getHash() in listedBlocks:
if i.strip() == '' or i in listedBlocks:
continue continue
blockFile = open('./data/blocks/' + i + '.dat') listbox.insert(99999, str(i.getContent()))
listbox.insert(END, str(blockFile.read().replace('-txt-', ''))) listedBlocks.append(i.getHash())
blockFile.close() listbox.see(99999)
listedBlocks.append(i)
listbox.see(END)
blocksList = os.listdir('./data/blocks/') # dir is your directory path
number_blocks = len(blocksList)
runningCheckDelayCount += 1 runningCheckDelayCount += 1
if runningCheckDelayCount == runningCheckDelay: if runningCheckDelayCount == runningCheckDelay:
resp = pluginapi.get_core()._utils.localCommand('ping') resp = pluginapi.daemon.local_command('ping')
if resp == 'pong': if resp == 'pong':
daemonStatus.config(text="Onionr Daemon Status: Running") daemonStatus.config(text = "Onionr Daemon Status: Running")
else: else:
daemonStatus.config(text="Onionr Daemon Status: Not Running") daemonStatus.config(text = "Onionr Daemon Status: Not Running")
runningCheckDelayCount = 0 runningCheckDelayCount = 0
root.after(10000, update) root.after(10000, update)
def openGUI(): def reallyOpenGUI():
import tkinter import tkinter
global root, runningCheckDelay, runningCheckDelayCount, scrollbar, listedBlocks, nodeInfo, keyInfo, idText, idEntry, pubKeyEntry, listbox, daemonStatus, sendEntry global root, runningCheckDelay, runningCheckDelayCount, scrollbar, listedBlocks, nodeInfo, keyInfo, idText, idEntry, pubKeyEntry, listbox, daemonStatus, sendEntry
@ -74,11 +82,12 @@ def openGUI():
nodeInfo = tkinter.Frame(root) nodeInfo = tkinter.Frame(root)
keyInfo = tkinter.Frame(root) keyInfo = tkinter.Frame(root)
print(pluginapi.get_onionr().get_hostname()) hostname = pluginapi.get_onionr().get_hostname()
idText = pluginapi.get_onionr().get_hostname() logger.debug('Onionr Hostname: %s' % hostname)
idText = hostname
idEntry = tkinter.Entry(nodeInfo) idEntry = tkinter.Entry(nodeInfo)
tkinter.Label(nodeInfo, text="Node Address: ").pack(side=tkinter.LEFT) tkinter.Label(nodeInfo, text = "Node Address: ").pack(side=tkinter.LEFT)
idEntry.pack() idEntry.pack()
idEntry.insert(0, idText.strip()) idEntry.insert(0, idText.strip())
idEntry.configure(state="readonly") idEntry.configure(state="readonly")
@ -100,17 +109,22 @@ def openGUI():
sendEntry.pack(side=tkinter.TOP, pady=5) sendEntry.pack(side=tkinter.TOP, pady=5)
sendBtn.pack(side=tkinter.TOP) sendBtn.pack(side=tkinter.TOP)
listbox = tkinter.Listbox(root, yscrollcommand=tkinter.scrollbar.set, height=15) listbox = tkinter.Listbox(root, yscrollcommand=tkinter.Scrollbar.set, height=15)
listbox.pack(fill=tkinter.BOTH, pady=25) listbox.pack(fill=tkinter.BOTH, pady=25)
daemonStatus = tkinter.Label(root, text="Onionr Daemon Status: unknown") daemonStatus = tkinter.Label(root, text="Onionr Daemon Status: unknown")
daemonStatus.pack() daemonStatus.pack()
scrollbar.config(command=tkinter.listbox.yview) scrollbar.config(command=tkinter.Listbox.yview)
root.after(2000, update) root.after(2000, update)
root.mainloop() root.mainloop()
def openGUI():
t = threading.Thread(target = reallyOpenGUI)
t.daemon = False
t.start()
def on_init(api, data = None): def on_init(api, data = None):
global pluginapi global pluginapi
pluginapi = api pluginapi = api

View File

@ -0,0 +1,5 @@
{
"name" : "pluginmanager",
"version" : "1.0",
"author" : "onionr"
}

View File

@ -0,0 +1,546 @@
'''
Onionr - P2P Microblogging Platform & Social network.
This plugin acts as a plugin manager, and allows the user to install other plugins distributed over Onionr.
'''
'''
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
# useful libraries
import logger, config
import os, sys, json, time, random, shutil, base64, getpass, datetime, re
from onionrblockapi import Block
plugin_name = 'pluginmanager'
keys_data = {'keys' : {}, 'plugins' : [], 'repositories' : {}}
# key functions
def writeKeys():
'''
Serializes and writes the keystore in memory to file
'''
file = open(keys_file, 'w')
file.write(json.dumps(keys_data, indent=4, sort_keys=True))
file.close()
def readKeys():
'''
Loads the keystore into memory
'''
global keys_data
keys_data = json.loads(open(keys_file).read())
return keys_data
def getKey(plugin):
'''
Returns the public key for a given plugin
'''
global keys_data
readKeys()
return (keys_data['keys'][plugin] if plugin in keys_data['keys'] else None)
def saveKey(plugin, key):
'''
Saves the public key for a plugin to keystore
'''
global keys_data
readKeys()
keys_data['keys'][plugin] = key
writeKeys()
def getPlugins():
'''
Returns a list of plugins installed by the plugin manager
'''
global keys_data
readKeys()
return keys_data['plugins']
def addPlugin(plugin):
'''
Saves the plugin name, to remember that it was installed by the pluginmanager
'''
global keys_data
readKeys()
if not plugin in keys_data['plugins']:
keys_data['plugins'].append(plugin)
writeKeys()
def removePlugin(plugin):
'''
Removes the plugin name from the pluginmanager's records
'''
global keys_data
readKeys()
if plugin in keys_data['plugins']:
keys_data['plugins'].remove(plugin)
writeKeys()
def getRepositories():
'''
Returns a list of plugins installed by the plugin manager
'''
global keys_data
readKeys()
return keys_data['repositories']
def addRepository(repositories, data):
'''
Saves the plugin name, to remember that it was installed by the pluginmanager
'''
global keys_data
readKeys()
keys_data['repositories'][repositories] = data
writeKeys()
def removeRepository(repositories):
'''
Removes the plugin name from the pluginmanager's records
'''
global keys_data
readKeys()
if plugin in keys_data['repositories']:
del keys_data['repositories'][repositories]
writeKeys()
def check():
'''
Checks to make sure the keystore file still exists
'''
global keys_file
keys_file = pluginapi.plugins.get_data_folder(plugin_name) + 'keystore.json'
if not os.path.isfile(keys_file):
writeKeys()
# plugin management
def sanitize(name):
return re.sub('[^0-9a-zA-Z]+', '', str(name).lower())[:255]
def blockToPlugin(block):
try:
block = Block(block)
blockContent = json.loads(block.getContent())
name = sanitize(blockContent['name'])
author = blockContent['author']
date = blockContent['date']
version = None
if 'version' in blockContent['info']:
version = blockContent['info']['version']
content = base64.b64decode(blockContent['content'].encode())
source = pluginapi.plugins.get_data_folder(plugin_name) + 'plugin.zip'
destination = pluginapi.plugins.get_folder(name)
with open(source, 'wb') as f:
f.write(content)
if os.path.exists(destination) and not os.path.isfile(destination):
shutil.rmtree(destination)
shutil.unpack_archive(source, destination)
pluginapi.plugins.enable(name)
logger.info('Installation of %s complete.' % name)
return True
except Exception as e:
logger.error('Failed to install plugin.', error = e, timestamp = False)
return False
def pluginToBlock(plugin, import_block = True):
try:
plugin = sanitize(plugin)
directory = pluginapi.get_pluginapi().get_folder(plugin)
data_directory = pluginapi.get_pluginapi().get_data_folder(plugin)
zipfile = pluginapi.get_pluginapi().get_data_folder(plugin_name) + 'plugin.zip'
if os.path.exists(directory) and not os.path.isfile(directory):
if os.path.exists(data_directory) and not os.path.isfile(data_directory):
shutil.rmtree(data_directory)
if os.path.exists(zipfile) and os.path.isfile(zipfile):
os.remove(zipfile)
if os.path.exists(directory + '__pycache__') and not os.path.isfile(directory + '__pycache__'):
shutil.rmtree(directory + '__pycache__')
shutil.make_archive(zipfile[:-4], 'zip', directory)
data = base64.b64encode(open(zipfile, 'rb').read())
author = getpass.getuser()
description = 'Default plugin description'
info = {"name" : plugin}
try:
if os.path.exists(directory + 'info.json'):
info = json.loads(open(directory + 'info.json').read())
if 'author' in info:
author = info['author']
if 'description' in info:
description = info['description']
except:
pass
metadata = {'author' : author, 'date' : str(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')), 'name' : plugin, 'info' : info, 'compiled-by' : plugin_name, 'content' : data.decode('utf-8'), 'description' : description}
hash = pluginapi.get_core().insertBlock(json.dumps(metadata), header = 'plugin', sign = True)
if import_block:
pluginapi.get_utils().importNewBlocks()
return hash
else:
logger.error('Plugin %s does not exist.' % plugin)
except Exception as e:
logger.error('Failed to convert plugin to block.', error = e, timestamp = False)
return False
def installBlock(block):
try:
block = Block(block)
blockContent = json.loads(block.getContent())
name = sanitize(blockContent['name'])
author = blockContent['author']
date = blockContent['date']
version = None
if 'version' in blockContent['info']:
version = blockContent['info']['version']
install = False
logger.info(('Will install %s' + (' v' + version if not version is None else '') + ' (%s), by %s') % (name, date, author))
# TODO: Convert to single line if statement
if os.path.exists(pluginapi.plugins.get_folder(name)):
install = logger.confirm(message = 'Continue with installation (will overwrite existing plugin) %s?')
else:
install = logger.confirm(message = 'Continue with installation %s?')
if install:
blockToPlugin(block.getHash())
addPlugin(name)
else:
logger.info('Installation cancelled.')
return False
return True
except Exception as e:
logger.error('Failed to install plugin.', error = e, timestamp = False)
return False
def uninstallPlugin(plugin):
try:
plugin = sanitize(plugin)
pluginFolder = pluginapi.plugins.get_folder(plugin)
exists = (os.path.exists(pluginFolder) and not os.path.isfile(pluginFolder))
installedByPluginManager = plugin in getPlugins()
remove = False
if not exists:
logger.warn('Plugin %s does not exist.' % plugin, timestamp = False)
return False
default = 'y'
if not installedByPluginManager:
logger.warn('The plugin %s was not installed by %s.' % (plugin, plugin_name), timestamp = False)
default = 'n'
remove = logger.confirm(message = 'All plugin data will be lost. Are you sure you want to proceed %s?', default = default)
if remove:
if installedByPluginManager:
removePlugin(plugin)
pluginapi.plugins.disable(plugin)
shutil.rmtree(pluginFolder)
logger.info('Uninstallation of %s complete.' % plugin)
return True
else:
logger.info('Uninstallation cancelled.')
except Exception as e:
logger.error('Failed to uninstall plugin.', error = e)
return False
# command handlers
def help():
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' <plugin> [public key/block hash]')
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' <plugin> [public key/block hash]')
def commandInstallPlugin():
if len(sys.argv) >= 3:
check()
pluginname = sys.argv[2]
pkobh = None # public key or block hash
version = None
if ':' in pluginname:
details = pluginname
pluginname = sanitize(details[0])
version = details[1]
sanitize(pluginname)
if len(sys.argv) >= 4:
# public key or block hash specified
pkobh = sys.argv[3]
else:
# none specified, check if in config file
pkobh = getKey(pluginname)
if pkobh is None:
# still nothing found, try searching repositories
logger.info('Searching for public key in repositories...')
try:
repos = getRepositories()
distributors = list()
for repo, records in repos.items():
if pluginname in records:
logger.debug('Found %s in repository %s for plugin %s.' % (records[pluginname], repo, pluginname))
distributors.append(records[pluginname])
if len(distributors) != 0:
distributor = None
if len(distributors) == 1:
logger.info('Found distributor: %s' % distributors[0])
distributor = distributors[0]
else:
distributors_message = ''
index = 1
for dist in distributors:
distributors_message += ' ' + logger.colors.bold + str(index) + ') ' + logger.colors.reset + str(dist) + '\n'
index += 1
logger.info((logger.colors.bold + 'Found distributors (%s):' + logger.colors.reset + '\n' + distributors_message) % len(distributors))
valid = False
while not valid:
choice = logger.readline('Select the number of the key to use, from 1 to %s, or press Ctrl+C to cancel:' % (index - 1))
try:
if int(choice) < index and int(choice) >= 1:
distributor = distributors[int(choice)]
valid = True
except KeyboardInterrupt:
logger.info('Installation cancelled.')
return True
except:
pass
if not distributor is None:
pkobh = distributor
except Exception as e:
logger.warn('Failed to lookup plugin in repositories.', timestamp = False)
logger.error('asdf', error = e, timestamp = False)
if pkobh is None:
logger.error('No key for this plugin found in keystore or repositories, please specify.')
help()
return True
valid_hash = pluginapi.get_utils().validateHash(pkobh)
real_block = False
valid_key = pluginapi.get_utils().validatePubKey(pkobh)
real_key = False
if valid_hash:
real_block = Block.exists(pkobh)
elif valid_key:
real_key = pluginapi.get_utils().hasKey(pkobh)
blockhash = None
if valid_hash and not real_block:
logger.error('Block hash not found. Perhaps it has not been synced yet?')
logger.debug('Is valid hash, but does not belong to a known block.')
return True
elif valid_hash and real_block:
blockhash = str(pkobh)
logger.debug('Using block %s...' % blockhash)
installBlock(blockhash)
elif valid_key and not real_key:
logger.error('Public key not found. Try adding the node by address manually, if possible.')
logger.debug('Is valid key, but the key is not a known one.')
elif valid_key and real_key:
publickey = str(pkobh)
logger.debug('Using public key %s...' % publickey)
saveKey(pluginname, pkobh)
signedBlocks = Block.getBlocks(type = 'plugin', signed = True, signer = publickey)
mostRecentTimestamp = None
mostRecentVersionBlock = None
for block in signedBlocks:
try:
blockContent = json.loads(block.getContent())
if not (('author' in blockContent) and ('info' in blockContent) and ('date' in blockContent) and ('name' in blockContent)):
raise ValueError('Missing required parameter `date` in block %s.' % block.getHash())
blockDatetime = datetime.datetime.strptime(blockContent['date'], '%Y-%m-%d %H:%M:%S')
if blockContent['name'] == pluginname:
if ('version' in blockContent['info']) and (blockContent['info']['version'] == version) and (not version is None):
mostRecentTimestamp = blockDatetime
mostRecentVersionBlock = block.getHash()
break
elif mostRecentTimestamp is None:
mostRecentTimestamp = blockDatetime
mostRecentVersionBlock = block.getHash()
elif blockDatetime > mostRecentTimestamp:
mostRecentTimestamp = blockDatetime
mostRecentVersionBlock = block.getHash()
except Exception as e:
pass
logger.warn('Only continue the installation is you are absolutely certain that you trust the plugin distributor. Public key of plugin distributor: %s' % publickey, timestamp = False)
installBlock(mostRecentVersionBlock)
else:
logger.error('Unknown data "%s"; must be public key or block hash.' % str(pkobh))
return
else:
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' <plugin> [public key/block hash]')
return True
def commandUninstallPlugin():
if len(sys.argv) >= 3:
uninstallPlugin(sys.argv[2])
else:
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' <plugin>')
return True
def commandSearchPlugin():
logger.info('This feature has not been created yet. Please check back later.')
return True
def commandAddRepository():
if len(sys.argv) >= 3:
check()
blockhash = sys.argv[2]
if pluginapi.get_utils().validateHash(blockhash):
if Block.exists(blockhash):
try:
blockContent = json.loads(Block(blockhash).getContent())
pluginslist = dict()
for pluginname, distributor in blockContent['plugins'].items():
if pluginapi.get_utils().validatePubKey(distributor):
pluginslist[pluginname] = distributor
logger.debug('Found %s records in repository.' % len(pluginslist))
if len(pluginslist) != 0:
addRepository(blockhash, pluginslist)
logger.info('Successfully added repository.')
else:
logger.error('Repository contains no records, not importing.')
except Exception as e:
logger.error('Failed to parse block.', error = e)
else:
logger.error('Block hash not found. Perhaps it has not been synced yet?')
logger.debug('Is valid hash, but does not belong to a known block.')
else:
logger.error('Unknown data "%s"; must be block hash.' % str(pkobh))
else:
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [block hash]')
return True
def commandRemoveRepository():
if len(sys.argv) >= 3:
check()
blockhash = sys.argv[2]
if pluginapi.get_utils().validateHash(blockhash):
if blockhash in getRepositories():
try:
removeRepository(blockhash)
except Exception as e:
logger.error('Failed to parse block.', error = e)
else:
logger.error('Repository has not been imported, nothing to remove.')
else:
logger.error('Unknown data "%s"; must be block hash.' % str(pkobh))
else:
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [block hash]')
return True
def commandPublishPlugin():
if len(sys.argv) >= 3:
check()
pluginname = sanitize(sys.argv[2])
pluginfolder = pluginapi.plugins.get_folder(pluginname)
if os.path.exists(pluginfolder) and not os.path.isfile(pluginfolder):
block = pluginToBlock(pluginname)
logger.info('Plugin saved in block %s.' % block)
else:
logger.error('Plugin %s does not exist.' % pluginname, timestamp = False)
else:
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' <plugin>')
# event listeners
def on_init(api, data = None):
global pluginapi
pluginapi = api
check()
# register some commands
api.commands.register(['install-plugin', 'installplugin', 'plugin-install', 'install', 'plugininstall'], commandInstallPlugin)
api.commands.register(['remove-plugin', 'removeplugin', 'plugin-remove', 'uninstall-plugin', 'uninstallplugin', 'plugin-uninstall', 'uninstall', 'remove', 'pluginremove'], commandUninstallPlugin)
api.commands.register(['search', 'filter-plugins', 'search-plugins', 'searchplugins', 'search-plugin', 'searchplugin', 'findplugin', 'find-plugin', 'filterplugin', 'plugin-search', 'pluginsearch'], commandSearchPlugin)
api.commands.register(['add-repo', 'add-repository', 'addrepo', 'addrepository', 'repository-add', 'repo-add', 'repoadd', 'addrepository', 'add-plugin-repository', 'add-plugin-repo', 'add-pluginrepo', 'add-pluginrepository', 'addpluginrepo', 'addpluginrepository'], commandAddRepository)
api.commands.register(['remove-repo', 'remove-repository', 'removerepo', 'removerepository', 'repository-remove', 'repo-remove', 'reporemove', 'removerepository', 'remove-plugin-repository', 'remove-plugin-repo', 'remove-pluginrepo', 'remove-pluginrepository', 'removepluginrepo', 'removepluginrepository', 'rm-repo', 'rm-repository', 'rmrepo', 'rmrepository', 'repository-rm', 'repo-rm', 'reporm', 'rmrepository', 'rm-plugin-repository', 'rm-plugin-repo', 'rm-pluginrepo', 'rm-pluginrepository', 'rmpluginrepo', 'rmpluginrepository'], commandRemoveRepository)
api.commands.register(['publish-plugin', 'plugin-publish', 'publishplugin', 'pluginpublish', 'publish'], commandPublishPlugin)
# add help menus once the features are actually implemented
return

View File

@ -12,5 +12,9 @@
"output": true, "output": true,
"color": true "color": true
} }
},
"allocations":{
"disk": 1000000000,
"netTotal": 1000000000
} }
} }

View File

@ -1,11 +1,13 @@
''' '''
Default plugin template file $name plugin template file.
Generated on $date by $user. Generated on $date by $user.
''' '''
# Imports some useful libraries # Imports some useful libraries
import logger, config import logger, config
plugin_name = '$name'
def on_init(api, data = None): def on_init(api, data = None):
''' '''
This event is called after Onionr is initialized, but before the command This event is called after Onionr is initialized, but before the command

View File

@ -14,7 +14,7 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
''' '''
import unittest, sys, os, base64, tarfile, shutil, simplecrypt, logger, btc import unittest, sys, os, base64, tarfile, shutil, simplecrypt, logger #, btc
class OnionrTests(unittest.TestCase): class OnionrTests(unittest.TestCase):
def testPython3(self): def testPython3(self):
@ -56,7 +56,7 @@ class OnionrTests(unittest.TestCase):
myCore = core.Core() myCore = core.Core()
if not os.path.exists('data/peers.db'): if not os.path.exists('data/peers.db'):
myCore.createPeerDB() myCore.createPeerDB()
if myCore.addPeer('6M5MXL237OK57ITHVYN5WGHANPGOMKS5C3PJLHBBNKFFJQOIDOJA====') and not myCore.addPeer('NFXHMYLMNFSAU==='): if myCore.addPeer('6M5MXL237OK57ITHVYN5WGHANPGOMKS5C3PJLHBBNKFFJQOIDOJA====', '1cSix9Ao/yQSdo0sNif8cm2uTcYnSphb4JdZL/3WkN4=') and not myCore.addPeer('NFXHMYLMNFSAU===', '1cSix9Ao/yQSdo0sNif8cm2uTcYnSphb4JdZL/3WkN4='):
self.assertTrue(True) self.assertTrue(True)
else: else:
self.assertTrue(False) self.assertTrue(False)