diff --git a/.gitignore b/.gitignore
index 3c70da06..6edc23ff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ run.sh
onionr/data-encrypted.dat
onionr/.onionr-lock
core
+.vscode/*
diff --git a/onionr/api.py b/onionr/api.py
index 27b7cc1b..bf592c59 100755
--- a/onionr/api.py
+++ b/onionr/api.py
@@ -22,16 +22,17 @@ from flask import request, Response, abort
from multiprocessing import Process
from gevent.wsgi import WSGIServer
import sys, random, threading, hmac, hashlib, base64, time, math, os, logger, config
-
from core import Core
+from onionrblockapi import Block
import onionrutils, onionrcrypto
+
class API:
'''
Main HTTP API (Flask)
'''
def validateToken(self, token):
'''
- Validate that the client token (hmac) matches the given token
+ Validate that the client token matches the given token
'''
try:
if not hmac.compare_digest(self.clientToken, token):
@@ -50,8 +51,8 @@ class API:
'''
config.reload()
-
- if config.get('devmode', True):
+
+ if config.get('dev_mode', True):
self._developmentMode = True
logger.set_level(logger.LEVEL_DEBUG)
else:
@@ -64,29 +65,26 @@ class API:
self._crypto = onionrcrypto.OnionrCrypto(self._core)
self._utils = onionrutils.OnionrUtils(self._core)
app = flask.Flask(__name__)
- bindPort = int(config.get('client')['port'])
+ bindPort = int(config.get('client.port', 59496))
self.bindPort = bindPort
- self.clientToken = config.get('client')['client_hmac']
+ self.clientToken = config.get('client.hmac')
self.timeBypassToken = base64.b16encode(os.urandom(32)).decode()
- self.i2pEnabled = config.get('i2p')['host']
+ self.i2pEnabled = config.get('i2p.host', False)
self.mimeType = 'text/plain'
with open('data/time-bypass.txt', 'w') as bypass:
bypass.write(self.timeBypassToken)
- if not os.environ.get("WERKZEUG_RUN_MAIN") == "true":
- logger.debug('Your web password (KEEP SECRET): ' + logger.colors.underline + self.clientToken)
-
if not debug and not self._developmentMode:
- hostNums = [random.randint(1, 255), random.randint(1, 255), random.randint(1, 255)]
- self.host = '127.' + str(hostNums[0]) + '.' + str(hostNums[1]) + '.' + str(hostNums[2])
+ hostOctets = [127, random.randint(0x02, 0xFF), random.randint(0x02, 0xFF), random.randint(0x02, 0xFF)]
+ self.host = '.'.join(hostOctets)
else:
self.host = '127.0.0.1'
- hostFile = open('data/host.txt', 'w')
- hostFile.write(self.host)
- hostFile.close()
+
+ with open('data/host.txt', 'w') as file:
+ file.write(self.host)
@app.before_request
def beforeReq():
@@ -127,7 +125,7 @@ class API:
except:
data = ''
startTime = math.floor(time.time())
- # we should keep a hash DB of requests (with hmac) to prevent replays
+
action = request.args.get('action')
#if not self.debug:
token = request.args.get('token')
@@ -192,8 +190,6 @@ class API:
pass
elif action == 'ping':
resp = Response("pong!")
- elif action == 'getHMAC':
- resp = Response(self._crypto.generateSymmetric())
elif action == 'getSymmetric':
resp = Response(self._crypto.generateSymmetric())
elif action == 'getDBHash':
@@ -213,13 +209,12 @@ class API:
resp = Response('')
# setData should be something the communicator initiates, not this api
elif action == 'getData':
+ resp = ''
if self._utils.validateHash(data):
if not os.path.exists('data/blocks/' + data + '.db'):
- try:
- resp = base64.b64encode(self._core.getData(data))
- except TypeError:
- resp = ""
- if resp == False:
+ block = Block(hash=data.encode(), core=self._core)
+ resp = base64.b64encode(block.getRaw().encode()).decode()
+ if len(resp) == 0:
abort(404)
resp = ""
resp = Response(resp)
@@ -258,7 +253,7 @@ class API:
return resp
if not os.environ.get("WERKZEUG_RUN_MAIN") == "true":
- logger.info('Starting client on ' + self.host + ':' + str(bindPort) + '...', timestamp=True)
+ logger.info('Starting client on ' + self.host + ':' + str(bindPort) + '...', timestamp=False)
try:
self.http_server = WSGIServer((self.host, bindPort), app)
diff --git a/onionr/communicator.py b/onionr/communicator.py
index defe4c0a..82d5a3c2 100755
--- a/onionr/communicator.py
+++ b/onionr/communicator.py
@@ -21,6 +21,7 @@
'''
import sqlite3, requests, hmac, hashlib, time, sys, os, math, logger, urllib.parse, base64, binascii, random, json, threading
import core, onionrutils, onionrcrypto, netcontroller, onionrproofs, config, onionrplugins as plugins
+from onionrblockapi import Block
class OnionrCommunicate:
def __init__(self, debug, developmentMode):
@@ -73,10 +74,14 @@ class OnionrCommunicate:
# Loads in and starts the enabled plugins
plugins.reload()
+ # Print nice header thing :)
+ if config.get('general.display_header', True):
+ self.header()
+
while True:
command = self._core.daemonQueue()
# Process blocks based on a timer
- self.timerTick()
+ self.timerTick()
# TODO: migrate below if statements to be own functions which are called in the above timerTick() function
if self.communicatorTimers['highFailure'] == self.communicatorTimerCounts['highFailure']:
self.communicatorTimerCounts['highFailure'] = 0
@@ -122,16 +127,16 @@ class OnionrCommunicate:
announceAttempts = 3
announceAttemptCount = 0
announceVal = False
- logger.info('Announcing node to ' + command[1], timestamp=True)
+ logger.info('Announcing node to %s...' % command[1], timestamp=True)
while not announceVal:
announceAttemptCount += 1
announceVal = self.performGet('announce', command[1], data=self._core.hsAdder.replace('\n', ''), skipHighFailureAddress=True)
- logger.info(announceVal)
+ # logger.info(announceVal)
if announceAttemptCount >= announceAttempts:
- logger.warn('Unable to announce to ' + command[1])
+ logger.warn('Unable to announce to %s' % command[1])
break
elif command[0] == 'runCheck':
- logger.info('Status check; looks good.')
+ logger.debug('Status check; looks good.')
open('data/.runcheck', 'w+').close()
elif command[0] == 'kex':
self.pexCount = pexTimer - 1
@@ -144,7 +149,7 @@ class OnionrCommunicate:
logger.info('Checking for callbacks with connection %s...' % data['id'])
- self.check_callbacks(data, config.get('dc_execcallbacks', True))
+ self.check_callbacks(data, config.get('general.dc_execcallbacks', True))
events.event('incoming_direct_connection', data = {'callback' : True, 'communicator' : self, 'data' : data})
except Exception as e:
@@ -187,13 +192,17 @@ class OnionrCommunicate:
id_peer_cache = {}
def registerTimer(self, timerName, rate, timerFunc=None):
- '''Register a communicator timer'''
+ '''
+ 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'''
+ '''
+ Increments timers "ticks" and calls funcs if applicable
+ '''
tName = ''
for i in self.communicatorTimers.items():
tName = i[0]
@@ -340,7 +349,7 @@ class OnionrCommunicate:
If yet another callback is requested, it can be put in the `callback` parameter.
'''
- if config.get('dc_response', True):
+ if config.get('general.dc_response', True):
data['id'] = identifier
data['sender'] = open('data/hs/hostname').read()
data['callback'] = True
@@ -475,9 +484,9 @@ class OnionrCommunicate:
lastDB = self._core.getAddressInfo(i, 'DBHash')
if lastDB == None:
- logger.debug('Fetching hash from %s, no previous known.' % str(i))
+ logger.debug('Fetching db hash from %s, no previous known.' % str(i))
else:
- logger.debug('Fetching hash from %s, %s last known' % (str(i), str(lastDB)))
+ logger.debug('Fetching db hash from %s, %s last known' % (str(i), str(lastDB)))
currentDB = self.performGet('getDBHash', i)
@@ -616,7 +625,9 @@ class OnionrCommunicate:
return
def removeBlockFromProcessingList(self, block):
- '''Remove a block from the processing list'''
+ '''
+ Remove a block from the processing list
+ '''
try:
self.blocksProcessing.remove(block)
except ValueError:
@@ -723,7 +734,8 @@ class OnionrCommunicate:
r = requests.get(url, headers=headers, proxies=proxies, allow_redirects=False, timeout=(15, 30))
retData = r.text
except requests.exceptions.RequestException as e:
- logger.debug("%s failed with peer %s" % (action, peer))
+ logger.debug('%s failed with peer %s' % (action, peer))
+ logger.debug('Error: %s' % str(e))
retData = False
if not retData:
@@ -746,10 +758,17 @@ class OnionrCommunicate:
pass
return False
+ def header(self, message = logger.colors.fg.pink + logger.colors.bold + 'Onionr' + logger.colors.reset + logger.colors.fg.pink + ' has started.'):
+ if os.path.exists('static-data/header.txt'):
+ with open('static-data/header.txt', 'rb') as file:
+ # only to stdout, not file or log or anything
+ print(file.read().decode().replace('P', logger.colors.fg.pink).replace('W', logger.colors.reset + logger.colors.bold).replace('G', logger.colors.fg.green).replace('\n', logger.colors.reset + '\n'))
+ logger.info(logger.colors.fg.lightgreen + '-> ' + str(message) + logger.colors.reset + logger.colors.fg.lightgreen + ' <-\n')
+
shouldRun = False
debug = True
developmentMode = False
-if config.get('devmode', True):
+if config.get('general.dev_mode', True):
developmentMode = True
try:
if sys.argv[1] == 'run':
diff --git a/onionr/communicator2.py b/onionr/communicator2.py
new file mode 100755
index 00000000..01103bb5
--- /dev/null
+++ b/onionr/communicator2.py
@@ -0,0 +1,365 @@
+#!/usr/bin/env python3
+'''
+ Onionr - P2P Microblogging Platform & Social network.
+
+ 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)
+'''
+'''
+ 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 .
+'''
+import sys, os, core, config, json, onionrblockapi as block, requests, time, logger, threading, onionrplugins as plugins, base64
+import onionrexceptions
+from defusedxml import minidom
+
+class OnionrCommunicatorDaemon:
+ def __init__(self, debug, developmentMode):
+ logger.warn('New (unstable) communicator is being used.')
+
+ self.timers = []
+ self._core = core.Core(torPort=sys.argv[2])
+ self.nistSaltTimestamp = 0
+ self.powSalt = 0
+ self.delay = 1
+ self.proxyPort = sys.argv[2]
+ self.startTime = self._core._utils.getEpoch()
+
+ self.onlinePeers = []
+ self.offlinePeers = []
+
+ self.threadCounts = {}
+
+ self.shutdown = False
+
+ self.blockQueue = [] # list of new blocks to download
+
+ # Clear the daemon queue for any dead messages
+ if os.path.exists(self._core.queueDB):
+ self._core.clearDaemonQueue()
+
+ # Loads in and starts the enabled plugins
+ plugins.reload()
+
+ # Print nice header thing :)
+ if config.get('general.display_header', True):
+ self.header()
+
+ if debug or developmentMode:
+ OnionrCommunicatorTimers(self, self.heartbeat, 10)
+
+ self.getOnlinePeers()
+ OnionrCommunicatorTimers(self, self.daemonCommands, 5)
+ OnionrCommunicatorTimers(self, self.detectAPICrash, 5)
+ OnionrCommunicatorTimers(self, self.getOnlinePeers, 60)
+ OnionrCommunicatorTimers(self, self.lookupBlocks, 7)
+ OnionrCommunicatorTimers(self, self.getBlocks, 10)
+ OnionrCommunicatorTimers(self, self.clearOfflinePeer, 120)
+ OnionrCommunicatorTimers(self, self.lookupKeys, 125)
+ OnionrCommunicatorTimers(self, self.lookupAdders, 600)
+
+ # Main daemon loop, mainly for calling timers, do not do any complex operations here
+ while not self.shutdown:
+ for i in self.timers:
+ i.processTimer()
+ time.sleep(self.delay)
+ logger.info('Goodbye.')
+
+ def lookupKeys(self):
+ '''Lookup new keys'''
+ logger.info('LOOKING UP NEW KEYS')
+ tryAmount = 1
+ for i in range(tryAmount):
+ peer = self.pickOnlinePeer()
+ newKeys = self.peerAction(peer, action='kex')
+ self._core._utils.mergeKeys(newKeys)
+
+ self.decrementThreadCount('lookupKeys')
+ return
+
+ def lookupAdders(self):
+ '''Lookup new peer addresses'''
+ logger.info('LOOKING UP NEW ADDRESSES')
+ tryAmount = 1
+ for i in range(tryAmount):
+ peer = self.pickOnlinePeer()
+ newAdders = self.peerAction(peer, action='pex')
+ self._core._utils.mergeAdders(newAdders)
+
+ self.decrementThreadCount('lookupKeys')
+ def lookupBlocks(self):
+ '''Lookup new blocks'''
+ logger.info('LOOKING UP NEW BLOCKS')
+ tryAmount = 2
+ newBlocks = ''
+ for i in range(tryAmount):
+ peer = self.pickOnlinePeer()
+ newDBHash = self.peerAction(peer, 'getDBHash')
+ if newDBHash == False:
+ continue
+ if newDBHash != self._core.getAddressInfo(peer, 'DBHash'):
+ self._core.setAddressInfo(peer, 'DBHash', newDBHash)
+ newBlocks = self.peerAction(peer, 'getBlockHashes')
+ if newBlocks != False:
+ # if request was a success
+ for i in newBlocks.split('\n'):
+ if self._core._utils.validateHash(i):
+ # if newline seperated string is valid hash
+ if not os.path.exists('data/blocks/' + i + '.db'):
+ # if block does not exist on disk and is not already in block queue
+ if i not in self.blockQueue:
+ self.blockQueue.append(i)
+ self.decrementThreadCount('lookupBlocks')
+ return
+
+ def getBlocks(self):
+ '''download new blocks'''
+ for blockHash in self.blockQueue:
+ logger.info("ATTEMPTING TO DOWNLOAD " + blockHash)
+ content = self.peerAction(self.pickOnlinePeer(), 'getData', data=blockHash)
+ if content != False:
+ try:
+ content = content.encode()
+ except AttributeError:
+ pass
+ content = base64.b64decode(content)
+ if self._core._crypto.sha3Hash(content) == blockHash:
+ content = content.decode() # decode here because sha3Hash needs bytes above
+ metas = self._core._utils.getBlockMetadataFromData(content) # returns tuple(metadata, meta), meta is also in metadata
+ metadata = metas[0]
+ meta = metas[1]
+ if self._core._utils.validateMetadata(metadata):
+ if self._core._crypto.verifyPow(metas[2], metadata):
+ logger.info('Block passed proof, saving.')
+ self._core.setData(content)
+ self._core.addToBlockDB(blockHash, dataSaved=True)
+ else:
+ logger.warn('POW failed for block ' + blockHash)
+ else:
+ logger.warn('Metadata for ' + blockHash + ' is invalid.')
+ self.blockQueue.remove(blockHash)
+ else:
+ logger.warn('Block hash validation failed for ' + blockHash + ' got ' + self._core._crypto.sha3Hash(content))
+ self.decrementThreadCount('getBlocks')
+ return
+
+ def pickOnlinePeer(self):
+ '''randomly picks peer from pool without bias (using secrets module)'''
+ retData = ''
+ while True:
+ peerLength = len(self.onlinePeers)
+ if peerLength <= 0:
+ break
+ try:
+ # get a random online peer, securely. May get stuck in loop if network is lost or if all peers in pool magically disconnect at once
+ retData = self.onlinePeers[self._core._crypto.secrets.randbelow(peerLength)]
+ except IndexError:
+ pass
+ else:
+ break
+ return retData
+
+ def decrementThreadCount(self, threadName):
+ '''Decrement amount of a thread name if more than zero, called when a function meant to be run in a thread ends'''
+ try:
+ if self.threadCounts[threadName] > 0:
+ self.threadCounts[threadName] -= 1
+ except KeyError:
+ pass
+
+ def clearOfflinePeer(self):
+ '''Removes the longest offline peer to retry later'''
+ try:
+ removed = self.offlinePeers.pop(0)
+ except IndexError:
+ pass
+ else:
+ logger.debug('removed ' + removed + ' from offline list to try them again.')
+ self.decrementThreadCount('clearOfflinePeer')
+
+ def getOnlinePeers(self):
+ '''Manages the self.onlinePeers attribute list'''
+ logger.info('Refreshing peer pool.')
+ maxPeers = 4
+ needed = maxPeers - len(self.onlinePeers)
+
+ for i in range(needed):
+ self.connectNewPeer()
+ self.decrementThreadCount('getOnlinePeers')
+
+ def connectNewPeer(self, peer=''):
+ '''Adds a new random online peer to self.onlinePeers'''
+ retData = False
+ tried = self.offlinePeers
+ if peer != '':
+ if self._core._utils.validateID(peer):
+ peerList = [peer]
+ else:
+ raise onionrexceptions.InvalidAddress('Will not attempt connection test to invalid address')
+ else:
+ peerList = self._core.listAdders()
+
+ if len(peerList) == 0:
+ peerList.extend(self._core.bootstrapList)
+
+ for address in peerList:
+ if len(address) == 0 or address in tried or address in self.onlinePeers:
+ continue
+ if self.peerAction(address, 'ping') == 'pong!':
+ logger.info('connected to ' + address)
+ self.onlinePeers.append(address)
+ retData = address
+ break
+ else:
+ tried.append(address)
+ logger.debug('failed to connect to ' + address)
+ else:
+ if len(self.onlinePeers) == 0:
+ logger.warn('Could not connect to any peer')
+ return retData
+
+ def printOnlinePeers(self):
+ '''logs online peer list'''
+ if len(self.onlinePeers) == 0:
+ logger.warn('No online peers')
+ return
+ for i in self.onlinePeers:
+ logger.info(self.onlinePeers[i])
+
+ def peerAction(self, peer, action, data=''):
+ '''Perform a get request to a peer'''
+ if len(peer) == 0:
+ return False
+ logger.info('Performing ' + action + ' with ' + peer + ' on port ' + str(self.proxyPort))
+ url = 'http://' + peer + '/public/?action=' + action
+ if len(data) > 0:
+ url += '&data=' + data
+ retData = self._core._utils.doGetRequest(url, port=self.proxyPort)
+ if retData == False:
+ try:
+ self.onlinePeers.remove(peer)
+ self.getOnlinePeers() # Will only add a new peer to pool if needed
+ except ValueError:
+ pass
+ return retData
+
+ def heartbeat(self):
+ '''Show a heartbeat debug message'''
+ currentTime = self._core._utils.getEpoch() - self.startTime
+ logger.debug('heartbeat, running seconds: ' + str(currentTime))
+ self.decrementThreadCount('heartbeat')
+
+ def daemonCommands(self):
+ '''process daemon commands from daemonQueue'''
+ cmd = self._core.daemonQueue()
+
+ if cmd is not False:
+ if cmd[0] == 'shutdown':
+ self.shutdown = True
+ elif cmd[0] == 'announceNode':
+ self.announce(cmd[1])
+ elif cmd[0] == 'runCheck':
+ logger.debug('Status check; looks good.')
+ open('data/.runcheck', 'w+').close()
+ elif cmd[0] == 'connectedPeers':
+ self.printOnlinePeers()
+ else:
+ logger.info('Recieved daemonQueue command:' + cmd[0])
+ self.decrementThreadCount('daemonCommands')
+
+ def announce(self, peer):
+ '''Announce to peers'''
+ announceCount = 0
+ announceAmount = 2
+ for peer in self._core.listAdders():
+ announceCount += 1
+ if self.peerAction(peer, 'announce', self._core.hsAdder) == 'Success':
+ logger.info('Successfully introduced node to ' + peer)
+ break
+ else:
+ if announceCount == announceAmount:
+ logger.warn('Could not introduce node. Try again soon')
+ break
+
+ def detectAPICrash(self):
+ '''exit if the api server crashes/stops'''
+ if self._core._utils.localCommand('ping', silent=False) != 'pong':
+ for i in range(5):
+ if self._core._utils.localCommand('ping') == 'pong':
+ break # break for loop
+ time.sleep(1)
+ else:
+ # This executes if the api is NOT detected to be running
+ logger.error('Daemon detected API crash (or otherwise unable to reach API after long time), stopping...')
+ self.shutdown = True
+ self.decrementThreadCount('detectAPICrash')
+
+ def header(self, message = logger.colors.fg.pink + logger.colors.bold + 'Onionr' + logger.colors.reset + logger.colors.fg.pink + ' has started.'):
+ if os.path.exists('static-data/header.txt'):
+ with open('static-data/header.txt', 'rb') as file:
+ # only to stdout, not file or log or anything
+ print(file.read().decode().replace('P', logger.colors.fg.pink).replace('W', logger.colors.reset + logger.colors.bold).replace('G', logger.colors.fg.green).replace('\n', logger.colors.reset + '\n'))
+ logger.info(logger.colors.fg.lightgreen + '-> ' + str(message) + logger.colors.reset + logger.colors.fg.lightgreen + ' <-\n')
+
+class OnionrCommunicatorTimers:
+ def __init__(self, daemonInstance, timerFunction, frequency, makeThread=True, threadAmount=1, maxThreads=5):
+ self.timerFunction = timerFunction
+ self.frequency = frequency
+ self.threadAmount = threadAmount
+ self.makeThread = makeThread
+ self.daemonInstance = daemonInstance
+ self.maxThreads = maxThreads
+ self._core = self.daemonInstance._core
+
+ self.daemonInstance.timers.append(self)
+ self.count = 0
+
+ def processTimer(self):
+ self.count += 1
+ try:
+ self.daemonInstance.threadCounts[self.timerFunction.__name__]
+ except KeyError:
+ self.daemonInstance.threadCounts[self.timerFunction.__name__] = 0
+
+ if self.count == self.frequency:
+ if self.makeThread:
+ for i in range(self.threadAmount):
+ if self.daemonInstance.threadCounts[self.timerFunction.__name__] >= self.maxThreads:
+ logger.warn(self.timerFunction.__name__ + ' has too many current threads to start anymore.')
+ else:
+ self.daemonInstance.threadCounts[self.timerFunction.__name__] += 1
+ newThread = threading.Thread(target=self.timerFunction)
+ newThread.start()
+ else:
+ self.timerFunction()
+ self.count = 0
+
+
+shouldRun = False
+debug = True
+developmentMode = False
+if config.get('general.dev_mode', True):
+ developmentMode = True
+try:
+ if sys.argv[1] == 'run':
+ shouldRun = True
+except IndexError:
+ pass
+if shouldRun:
+ try:
+ OnionrCommunicatorDaemon(debug, developmentMode)
+ except KeyboardInterrupt:
+ sys.exit(1)
+ pass
+ except Exception as e:
+ logger.error('Error occured in Communicator', error = e, timestamp = False)
diff --git a/onionr/config.py b/onionr/config.py
index fb8ac161..880b4dba 100644
--- a/onionr/config.py
+++ b/onionr/config.py
@@ -28,9 +28,20 @@ def get(key, default = None):
Gets the key from configuration, or returns `default`
'''
- if is_set(key):
- return get_config()[key]
- return default
+ key = str(key).split('.')
+ data = _config
+
+ last = key.pop()
+
+ for item in key:
+ if (not item in data) or (not type(data[item]) == dict):
+ return default
+ data = data[item]
+
+ if not last in data:
+ return default
+
+ return data[last]
def set(key, value = None, savefile = False):
'''
@@ -38,16 +49,40 @@ def set(key, value = None, savefile = False):
'''
global _config
+
+ key = str(key).split('.')
+ data = _config
+
+ last = key.pop()
+
+ for item in key:
+ if (not item in data) or (not type(data[item]) == dict):
+ data[item] = dict()
+ data = data[item]
+
if value is None:
- del _config[key]
+ del data[last]
else:
- _config[key] = value
+ data[last] = value
if savefile:
save()
def is_set(key):
- return key in get_config() and not get_config()[key] is None
+ key = str(key).split('.')
+ data = _config
+
+ last = key.pop()
+
+ for item in key:
+ if (not item in data) or (not type(data[item]) == dict):
+ return False
+ data = data[item]
+
+ if not last in data:
+ return False
+
+ return True
def check():
'''
@@ -71,7 +106,7 @@ def save():
check()
try:
with open(get_config_file(), 'w', encoding="utf8") as configfile:
- json.dump(get_config(), configfile, indent=2, sort_keys=True)
+ json.dump(get_config(), configfile, indent=2)
except:
logger.warn('Failed to write to configuration file.')
diff --git a/onionr/core.py b/onionr/core.py
index 9b78ee1a..fe1147b7 100644
--- a/onionr/core.py
+++ b/onionr/core.py
@@ -18,10 +18,9 @@
along with this program. If not, see .
'''
import sqlite3, os, sys, time, math, base64, tarfile, getpass, simplecrypt, hashlib, nacl, logger, json, netcontroller, math, config
-#from Crypto.Cipher import AES
-#from Crypto import Random
+from onionrblockapi import Block
-import onionrutils, onionrcrypto, onionrproofs, onionrevents as events
+import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions, onionrvalues
if sys.version_info < (3, 6):
try:
@@ -31,7 +30,7 @@ if sys.version_info < (3, 6):
sys.exit(1)
class Core:
- def __init__(self):
+ def __init__(self, torPort=0):
'''
Initialize Core Onionr library
'''
@@ -45,6 +44,7 @@ class Core:
self.bootstrapFileLocation = 'static-data/bootstrap-nodes.txt'
self.bootstrapList = []
+ self.requirements = onionrvalues.OnionrValues()
if not os.path.exists('data/'):
os.mkdir('data/')
@@ -111,7 +111,8 @@ class Core:
'''
Add an address to the address database (only tor currently)
'''
- if address == config.get('i2p')['ownAddr']:
+ if address == config.get('i2p.ownAddr', None):
+
return False
if self._utils.validateID(address):
conn = sqlite3.connect(self.addressDB)
@@ -139,6 +140,7 @@ class Core:
return True
else:
+ logger.debug('Invalid ID')
return False
def removeAddress(self, address):
@@ -193,6 +195,7 @@ class Core:
speed int,
success int,
DBHash text,
+ powValue text,
failure int,
lastConnect int
);
@@ -368,26 +371,35 @@ class Core:
'''
retData = False
if not os.path.exists(self.queueDB):
- conn = sqlite3.connect(self.queueDB)
- c = conn.cursor()
- # Create table
- c.execute('''CREATE TABLE commands
- (id integer primary key autoincrement, command text, data text, date text)''')
- conn.commit()
+ self.makeDaemonDB()
else:
conn = sqlite3.connect(self.queueDB)
c = conn.cursor()
- for row in c.execute('SELECT command, data, date, min(ID) FROM commands group by id'):
- retData = row
- break
- if retData != False:
- c.execute('DELETE FROM commands WHERE id=?;', (retData[3],))
+ try:
+ for row in c.execute('SELECT command, data, date, min(ID) FROM commands group by id'):
+ retData = row
+ break
+ except sqlite3.OperationalError:
+ self.makeDaemonDB()
+ else:
+ if retData != False:
+ c.execute('DELETE FROM commands WHERE id=?;', (retData[3],))
conn.commit()
conn.close()
events.event('queue_pop', data = {'data': retData}, onionr = None)
return retData
+
+ def makeDaemonDB(self):
+ '''generate the daemon queue db'''
+ conn = sqlite3.connect(self.queueDB)
+ c = conn.cursor()
+ # Create table
+ c.execute('''CREATE TABLE commands
+ (id integer primary key autoincrement, command text, data text, date text)''')
+ conn.commit()
+ conn.close()
def daemonQueueAdd(self, command, data=''):
'''
@@ -484,6 +496,7 @@ class Core:
trust int 6
pubkeyExchanged int 7
hashID text 8
+ pow text 9
'''
conn = sqlite3.connect(self.peerDB)
c = conn.cursor()
@@ -656,65 +669,81 @@ class Core:
conn.close()
return True
- def insertBlock(self, data, header='txt', sign=False):
+ def insertBlock(self, data, header='txt', sign=False, encryptType='', symKey='', asymPeer='', meta = {}):
'''
Inserts a block into the network
+ encryptType must be specified to encrypt a block
'''
- 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:
data.decode()
except AttributeError:
data = data.encode()
retData = ''
- metadata = {'type': header, 'powHash': powHash, 'powToken': powToken}
- sig = {}
-
- metadata = json.dumps(metadata)
- metadata = metadata.encode()
signature = ''
+ signer = ''
+ metadata = {}
- if sign:
- signature = self._crypto.edSign(metadata + b'\n' + data, self._crypto.privKey, encodeResult=True)
- ourID = self._crypto.pubKeyHashID()
- # Convert from bytes on some py versions?
- try:
- ourID = ourID.decode()
- except AttributeError:
- pass
- metadata = {'sig': signature, 'meta': metadata.decode()}
- metadata = json.dumps(metadata)
- metadata = metadata.encode()
+ # only use header if not set in provided meta
+ try:
+ meta['type']
+ except KeyError:
+ meta['type'] = header # block type
- if len(data) == 0:
- logger.error('Will not insert empty block')
+ jsonMeta = json.dumps(meta)
+
+ if encryptType in ('asym', 'sym', ''):
+ metadata['encryptType'] = encryptType
else:
- addedHash = self.setData(metadata + b'\n' + data)
- self.addToBlockDB(addedHash, selfInsert=True)
- self.setBlockType(addedHash, header)
- retData = addedHash
+ raise onionrexceptions.InvalidMetadata('encryptType must be asym or sym, or blank')
+
+ # sign before encrypt, as unauthenticated crypto should not be a problem here
+ if sign:
+ signature = self._crypto.edSign(jsonMeta + data, key=self._crypto.privKey, encodeResult=True)
+ signer = self._crypto.pubKeyHashID()
+
+ if len(jsonMeta) > 1000:
+ raise onionrexceptions.InvalidMetadata('meta in json encoded form must not exceed 1000 bytes')
+
+ # encrypt block metadata/sig/content
+ if encryptType == 'sym':
+ if len(symKey) < self.requirements.passwordLength:
+ raise onionrexceptions.SecurityError('Weak encryption key')
+ jsonMeta = self._crypto.symmetricEncrypt(jsonMeta, key=symKey, returnEncoded=True)
+ data = self._crypto.symmetricEncrypt(data, key=symKey, returnEncoded=True)
+ signature = self._crypto.symmetricEncrypt(signature, key=symKey, returnEncoded=True)
+ signer = self._crypto.symmetricEncrypt(signer, key=symKey, returnEncoded=True)
+ elif encryptType == 'asym':
+ if self._utils.validatePubKey(asymPeer):
+ jsonMeta = self._crypto.pubKeyEncrypt(jsonMeta, asymPeer, encodedData=True)
+ data = self._crypto.pubKeyEncrypt(data, asymPeer, encodedData=True)
+ signature = self._crypto.pubKeyEncrypt(signature, asymPeer, encodedData=True)
+ else:
+ raise onionrexceptions.InvalidPubkey(asymPeer + ' is not a valid base32 encoded ed25519 key')
+
+ powProof = onionrproofs.POW(data)
+
+ # wait for proof to complete
+ powToken = powProof.waitForResult()
+
+ powToken = base64.b64encode(powToken[1])
+ try:
+ powToken = powToken.decode()
+ except AttributeError:
+ pass
+
+ # compile metadata
+ metadata['meta'] = jsonMeta
+ metadata['sig'] = signature
+ metadata['signer'] = signer
+ metadata['powRandomToken'] = powToken
+ metadata['time'] = str(self._utils.getEpoch())
+
+ payload = json.dumps(metadata).encode() + b'\n' + data
+ retData = self.setData(payload)
+ self.addToBlockDB(retData, selfInsert=True, dataSaved=True)
+
return retData
def introduceNode(self):
diff --git a/onionr/dependencies/secrets.py b/onionr/dependencies/secrets.py
new file mode 100644
index 00000000..9b2eb61b
--- /dev/null
+++ b/onionr/dependencies/secrets.py
@@ -0,0 +1,331 @@
+"""Generate cryptographically strong pseudo-random numbers suitable for
+managing secrets such as account authentication, tokens, and similar.
+
+See PEP 506 for more information.
+https://www.python.org/dev/peps/pep-0506/
+
+
+A. HISTORY OF THE SOFTWARE
+==========================
+
+Python was created in the early 1990s by Guido van Rossum at Stichting
+Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands
+as a successor of a language called ABC. Guido remains Python's
+principal author, although it includes many contributions from others.
+
+In 1995, Guido continued his work on Python at the Corporation for
+National Research Initiatives (CNRI, see http://www.cnri.reston.va.us)
+in Reston, Virginia where he released several versions of the
+software.
+
+In May 2000, Guido and the Python core development team moved to
+BeOpen.com to form the BeOpen PythonLabs team. In October of the same
+year, the PythonLabs team moved to Digital Creations, which became
+Zope Corporation. In 2001, the Python Software Foundation (PSF, see
+https://www.python.org/psf/) was formed, a non-profit organization
+created specifically to own Python-related Intellectual Property.
+Zope Corporation was a sponsoring member of the PSF.
+
+All Python releases are Open Source (see http://www.opensource.org for
+the Open Source Definition). Historically, most, but not all, Python
+releases have also been GPL-compatible; the table below summarizes
+the various releases.
+
+ Release Derived Year Owner GPL-
+ from compatible? (1)
+
+ 0.9.0 thru 1.2 1991-1995 CWI yes
+ 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
+ 1.6 1.5.2 2000 CNRI no
+ 2.0 1.6 2000 BeOpen.com no
+ 1.6.1 1.6 2001 CNRI yes (2)
+ 2.1 2.0+1.6.1 2001 PSF no
+ 2.0.1 2.0+1.6.1 2001 PSF yes
+ 2.1.1 2.1+2.0.1 2001 PSF yes
+ 2.1.2 2.1.1 2002 PSF yes
+ 2.1.3 2.1.2 2002 PSF yes
+ 2.2 and above 2.1.1 2001-now PSF yes
+
+Footnotes:
+
+(1) GPL-compatible doesn't mean that we're distributing Python under
+ the GPL. All Python licenses, unlike the GPL, let you distribute
+ a modified version without making your changes open source. The
+ GPL-compatible licenses make it possible to combine Python with
+ other software that is released under the GPL; the others don't.
+
+(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
+ because its license has a choice of law clause. According to
+ CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
+ is "not incompatible" with the GPL.
+
+Thanks to the many outside volunteers who have worked under Guido's
+direction to make these releases possible.
+
+
+B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
+===============================================================
+
+PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
+--------------------------------------------
+
+1. This LICENSE AGREEMENT is between the Python Software Foundation
+("PSF"), and the Individual or Organization ("Licensee") accessing and
+otherwise using this software ("Python") in source or binary form and
+its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, PSF hereby
+grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
+analyze, test, perform and/or display publicly, prepare derivative works,
+distribute, and otherwise use Python alone or in any derivative version,
+provided, however, that PSF's License Agreement and PSF's notice of copyright,
+i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
+2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 Python Software Foundation; All
+Rights Reserved" are retained in Python alone or in any derivative version
+prepared by Licensee.
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python.
+
+4. PSF is making Python available to Licensee on an "AS IS"
+basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. Nothing in this License Agreement shall be deemed to create any
+relationship of agency, partnership, or joint venture between PSF and
+Licensee. This License Agreement does not grant permission to use PSF
+trademarks or trade name in a trademark sense to endorse or promote
+products or services of Licensee, or any third party.
+
+8. By copying, installing or otherwise using Python, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+
+
+BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
+-------------------------------------------
+
+BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
+
+1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
+office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
+Individual or Organization ("Licensee") accessing and otherwise using
+this software in source or binary form and its associated
+documentation ("the Software").
+
+2. Subject to the terms and conditions of this BeOpen Python License
+Agreement, BeOpen hereby grants Licensee a non-exclusive,
+royalty-free, world-wide license to reproduce, analyze, test, perform
+and/or display publicly, prepare derivative works, distribute, and
+otherwise use the Software alone or in any derivative version,
+provided, however, that the BeOpen Python License is retained in the
+Software, alone or in any derivative version prepared by Licensee.
+
+3. BeOpen is making the Software available to Licensee on an "AS IS"
+basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
+SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
+AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
+DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+5. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+6. This License Agreement shall be governed by and interpreted in all
+respects by the law of the State of California, excluding conflict of
+law provisions. Nothing in this License Agreement shall be deemed to
+create any relationship of agency, partnership, or joint venture
+between BeOpen and Licensee. This License Agreement does not grant
+permission to use BeOpen trademarks or trade names in a trademark
+sense to endorse or promote products or services of Licensee, or any
+third party. As an exception, the "BeOpen Python" logos available at
+http://www.pythonlabs.com/logos.html may be used according to the
+permissions granted on that web page.
+
+7. By copying, installing or otherwise using the software, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+
+
+CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
+---------------------------------------
+
+1. This LICENSE AGREEMENT is between the Corporation for National
+Research Initiatives, having an office at 1895 Preston White Drive,
+Reston, VA 20191 ("CNRI"), and the Individual or Organization
+("Licensee") accessing and otherwise using Python 1.6.1 software in
+source or binary form and its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, CNRI
+hereby grants Licensee a nonexclusive, royalty-free, world-wide
+license to reproduce, analyze, test, perform and/or display publicly,
+prepare derivative works, distribute, and otherwise use Python 1.6.1
+alone or in any derivative version, provided, however, that CNRI's
+License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
+1995-2001 Corporation for National Research Initiatives; All Rights
+Reserved" are retained in Python 1.6.1 alone or in any derivative
+version prepared by Licensee. Alternately, in lieu of CNRI's License
+Agreement, Licensee may substitute the following text (omitting the
+quotes): "Python 1.6.1 is made available subject to the terms and
+conditions in CNRI's License Agreement. This Agreement together with
+Python 1.6.1 may be located on the Internet using the following
+unique, persistent identifier (known as a handle): 1895.22/1013. This
+Agreement may also be obtained from a proxy server on the Internet
+using the following URL: http://hdl.handle.net/1895.22/1013".
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python 1.6.1 or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python 1.6.1.
+
+4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
+basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. This License Agreement shall be governed by the federal
+intellectual property law of the United States, including without
+limitation the federal copyright law, and, to the extent such
+U.S. federal law does not apply, by the law of the Commonwealth of
+Virginia, excluding Virginia's conflict of law provisions.
+Notwithstanding the foregoing, with regard to derivative works based
+on Python 1.6.1 that incorporate non-separable material that was
+previously distributed under the GNU General Public License (GPL), the
+law of the Commonwealth of Virginia shall govern this License
+Agreement only as to issues arising under or with respect to
+Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
+License Agreement shall be deemed to create any relationship of
+agency, partnership, or joint venture between CNRI and Licensee. This
+License Agreement does not grant permission to use CNRI trademarks or
+trade name in a trademark sense to endorse or promote products or
+services of Licensee, or any third party.
+
+8. By clicking on the "ACCEPT" button where indicated, or by copying,
+installing or otherwise using Python 1.6.1, Licensee agrees to be
+bound by the terms and conditions of this License Agreement.
+
+ ACCEPT
+
+
+CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
+--------------------------------------------------
+
+Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
+The Netherlands. All rights reserved.
+
+Permission to use, copy, modify, and distribute this software and its
+documentation for any purpose and without fee is hereby granted,
+provided that the above copyright notice appear in all copies and that
+both that copyright notice and this permission notice appear in
+supporting documentation, and that the name of Stichting Mathematisch
+Centrum or CWI not be used in advertising or publicity pertaining to
+distribution of the software without specific, written prior
+permission.
+
+STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
+THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
+FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+
+"""
+
+__all__ = ['choice', 'randbelow', 'randbits', 'SystemRandom',
+ 'token_bytes', 'token_hex', 'token_urlsafe',
+ 'compare_digest',
+ ]
+
+
+import base64
+import binascii
+import os
+
+from hmac import compare_digest
+from random import SystemRandom
+
+_sysrand = SystemRandom()
+
+randbits = _sysrand.getrandbits
+choice = _sysrand.choice
+
+def randbelow(exclusive_upper_bound):
+ """Return a random int in the range [0, n)."""
+ if exclusive_upper_bound <= 0:
+ raise ValueError("Upper bound must be positive.")
+ return _sysrand._randbelow(exclusive_upper_bound)
+
+DEFAULT_ENTROPY = 32 # number of bytes to return by default
+
+def token_bytes(nbytes=None):
+ """Return a random byte string containing *nbytes* bytes.
+
+ If *nbytes* is ``None`` or not supplied, a reasonable
+ default is used.
+
+ >>> token_bytes(16) #doctest:+SKIP
+ b'\\xebr\\x17D*t\\xae\\xd4\\xe3S\\xb6\\xe2\\xebP1\\x8b'
+
+ """
+ if nbytes is None:
+ nbytes = DEFAULT_ENTROPY
+ return os.urandom(nbytes)
+
+def token_hex(nbytes=None):
+ """Return a random text string, in hexadecimal.
+
+ The string has *nbytes* random bytes, each byte converted to two
+ hex digits. If *nbytes* is ``None`` or not supplied, a reasonable
+ default is used.
+
+ >>> token_hex(16) #doctest:+SKIP
+ 'f9bf78b9a18ce6d46a0cd2b0b86df9da'
+
+ """
+ return binascii.hexlify(token_bytes(nbytes)).decode('ascii')
+
+def token_urlsafe(nbytes=None):
+ """Return a random URL-safe text string, in Base64 encoding.
+
+ The string has *nbytes* random bytes. If *nbytes* is ``None``
+ or not supplied, a reasonable default is used.
+
+ >>> token_urlsafe(16) #doctest:+SKIP
+ 'Drmhze6EPcv0fN_81Bj-nA'
+
+ """
+ tok = token_bytes(nbytes)
+ return base64.urlsafe_b64encode(tok).rstrip(b'=').decode('ascii')
+
diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py
index 4c25a7fc..7ad74cc0 100644
--- a/onionr/netcontroller.py
+++ b/onionr/netcontroller.py
@@ -19,6 +19,7 @@
'''
import subprocess, os, random, sys, logger, time, signal
+from onionrblockapi import Block
class NetController:
'''
@@ -102,7 +103,7 @@ DataDirectory data/tordata/
logger.fatal("Got keyboard interrupt")
return False
- logger.info('Finished starting Tor', timestamp=True)
+ logger.debug('Finished starting Tor.', timestamp=True)
self.readyState = True
myID = open('data/hs/hostname', 'r')
diff --git a/onionr/onionr.py b/onionr/onionr.py
index bc33e8fb..914dded9 100755
--- a/onionr/onionr.py
+++ b/onionr/onionr.py
@@ -31,6 +31,8 @@ import api, core, config, logger, onionrplugins as plugins, onionrevents as even
import onionrutils
from onionrutils import OnionrUtils
from netcontroller import NetController
+from onionrblockapi import Block
+import onionrproofs
try:
from urllib3.contrib.socks import SOCKSProxyManager
@@ -38,8 +40,9 @@ except ImportError:
raise Exception("You need the PySocks module (for use with socks5 proxy to use Tor)")
ONIONR_TAGLINE = 'Anonymous P2P Platform - GPLv3 - https://Onionr.VoidNet.Tech'
-ONIONR_VERSION = '0.0.0' # for debugging and stuff
-API_VERSION = '2' # increments of 1; only change when something fundemental about how the API works changes. This way other nodes knows how to communicate without learning too much information about you.
+ONIONR_VERSION = '0.1.0' # for debugging and stuff
+ONIONR_VERSION_TUPLE = tuple(ONIONR_VERSION.split('.')) # (MAJOR, MINOR, VERSION)
+API_VERSION = '3' # increments of 1; only change when something fundemental about how the API works changes. This way other nodes knows how to communicate without learning too much information about you.
class Onionr:
def __init__(self):
@@ -64,22 +67,22 @@ class Onionr:
config.set_config(json.loads(open('static-data/default_config.json').read())) # this is the default config, it will be overwritten if a config file already exists. Else, it saves it
else:
# 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({'dev_mode': True, 'log': {'file': {'output': True, 'path': 'data/output.log'}, 'console': {'output': True, 'color': True}}})
if not data_exists:
config.save()
config.reload() # this will read the configuration file into memory
settings = 0b000
- if config.get('log', {'console': {'color': True}})['console']['color']:
+ if config.get('log.console.color', True):
settings = settings | logger.USE_ANSI
- if config.get('log', {'console': {'output': True}})['console']['output']:
+ if config.get('log.console.output', True):
settings = settings | logger.OUTPUT_TO_CONSOLE
- if config.get('log', {'file': {'output': True}})['file']['output']:
+ if config.get('log.file.output', True):
settings = settings | logger.OUTPUT_TO_FILE
- logger.set_file(config.get('log', {'file': {'path': 'data/output.log'}})['file']['path'])
+ logger.set_file(config.get('log.file.path', '/tmp/onionr.log'))
logger.set_settings(settings)
- if str(config.get('devmode', True)).lower() == 'true':
+ if str(config.get('general.dev_mode', True)).lower() == 'true':
self._developmentMode = True
logger.set_level(logger.LEVEL_DEBUG)
else:
@@ -89,6 +92,8 @@ class Onionr:
self.onionrCore = core.Core()
self.onionrUtils = OnionrUtils(self.onionrCore)
+ self.userOS = platform.system()
+
# Handle commands
self.debug = False # Whole application debugging
@@ -144,7 +149,7 @@ class Onionr:
randomPort = random.randint(1024, 65535)
if self.onionrUtils.checkPort(randomPort):
break
- config.set('client', {'participate': 'true', 'client_hmac': base64.b16encode(os.urandom(32)).decode('utf-8'), 'port': randomPort, 'api_version': API_VERSION}, True)
+ config.set('client', {'participate': True, 'hmac': base64.b16encode(os.urandom(32)).decode('utf-8'), 'port': randomPort, 'api_version': API_VERSION}, True)
self.cmds = {
'': self.showHelpSuggestion,
@@ -191,12 +196,18 @@ class Onionr:
'add-addr': self.addAddress,
'addaddr': self.addAddress,
'addaddress': self.addAddress,
- 'addfile': self.addFile,
+ 'add-file': self.addFile,
+ 'addfile': self.addFile,
+ 'listconn': self.listConn,
+
+ 'import-blocks': self.onionrUtils.importNewBlocks,
'importblocks': self.onionrUtils.importNewBlocks,
'introduce': self.onionrCore.introduceNode,
- 'connect': self.addAddress
+ 'connect': self.addAddress,
+
+ 'getpassword': self.getWebPassword
}
self.cmdhelp = {
@@ -206,6 +217,7 @@ class Onionr:
'start': 'Starts the Onionr daemon',
'stop': 'Stops the Onionr daemon',
'stats': 'Displays node statistics',
+ 'getpassword': 'Displays the web password',
'enable-plugin': 'Enables and starts a plugin',
'disable-plugin': 'Disables and stops a plugin',
'reload-plugin': 'Reloads a plugin',
@@ -215,8 +227,9 @@ class Onionr:
'add-msg': 'Broadcasts a message to the Onionr network',
'pm': 'Adds a private message to block',
'get-pms': 'Shows private messages sent to you',
- 'addfile': 'Create an Onionr block from a file',
- 'importblocks': 'import blocks from the disk (Onionr is transport-agnostic!)',
+ 'add-file': 'Create an Onionr block from a file',
+ 'import-blocks': 'import blocks from the disk (Onionr is transport-agnostic!)',
+ 'listconn': 'list connected peers',
'introduce': 'Introduce your node to the public Onionr network',
}
@@ -245,6 +258,12 @@ class Onionr:
def getCommands(self):
return self.cmds
+ def listConn(self):
+ self.onionrCore.daemonQueueAdd('connectedPeers')
+
+ def getWebPassword(self):
+ return config.get('client.hmac')
+
def getHelp(self):
return self.cmdhelp
@@ -348,14 +367,31 @@ class Onionr:
'''
Adds a peer (?)
'''
-
try:
newPeer = sys.argv[2]
except:
pass
else:
+ if self.onionrUtils.hasKey(newPeer):
+ logger.info('We already have that key')
+ return
+ if not '-' in newPeer:
+ logger.info('Since no POW token was supplied for that key, one is being generated')
+ proof = onionrproofs.POW(newPeer)
+ while True:
+ result = proof.getResult()
+ if result == False:
+ time.sleep(0.5)
+ else:
+ break
+ newPeer += '-' + base64.b64encode(result[1]).decode()
+ logger.info(newPeer)
+
logger.info("Adding peer: " + logger.colors.underline + newPeer)
- self.onionrCore.addPeer(newPeer)
+ if self.onionrUtils.mergeKeys(newPeer):
+ logger.info('Successfully added key')
+ else:
+ logger.error('Failed to add key')
return
@@ -390,12 +426,12 @@ class Onionr:
except KeyboardInterrupt:
return
- #addedHash = self.onionrCore.setData(messageToAdd)
- addedHash = self.onionrCore.insertBlock(messageToAdd, header='txt')
- #self.onionrCore.addToBlockDB(addedHash, selfInsert=True)
- #self.onionrCore.setBlockType(addedHash, 'txt')
- if addedHash != '':
+ #addedHash = Block(type = 'txt', content = messageToAdd).save()
+ addedHash = self.onionrCore.insertBlock(messageToAdd)
+ if addedHash != None:
logger.info("Message inserted as as block %s" % addedHash)
+ else:
+ logger.error('Failed to insert block.', timestamp = False)
return
def getPMs(self):
@@ -463,7 +499,12 @@ class Onionr:
os.makedirs(plugins.get_plugins_folder(plugin_name))
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')).replace('$name', plugin_name))
+ contents = ''
+ with open('static-data/default_plugin.py', 'rb') as file:
+ contents = file.read().decode()
+
+ # TODO: Fix $user. os.getlogin() is B U G G Y
+ main.write(contents.replace('$user', 'some random developer').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'}))
@@ -494,12 +535,12 @@ class Onionr:
logger.info('Do ' + logger.colors.bold + sys.argv[0] + ' --help' + logger.colors.reset + logger.colors.fg.green + ' for Onionr help.')
- def start(self, input = False):
+ def start(self, input = False, override = False):
'''
Starts the Onionr daemon
'''
- if os.path.exists('.onionr-lock'):
+ if os.path.exists('.onionr-lock') and not override:
logger.fatal('Cannot start. Daemon is already running, or it did not exit cleanly.\n(if you are sure that there is not a daemon running, delete .onionr-lock & try again).')
else:
if not self.debug and not self._developmentMode:
@@ -516,18 +557,25 @@ class Onionr:
'''
Starts the Onionr communication daemon
'''
-
+ communicatorDaemon = './communicator.py'
if not os.environ.get("WERKZEUG_RUN_MAIN") == "true":
if self._developmentMode:
- logger.warn('DEVELOPMENT MODE ENABLED (THIS IS LESS SECURE!)')
- net = NetController(config.get('client')['port'])
+ logger.warn('DEVELOPMENT MODE ENABLED (THIS IS LESS SECURE!)', timestamp = False)
+ net = NetController(config.get('client.port', 59496))
logger.info('Tor is starting...')
if not net.startTor():
sys.exit(1)
- logger.info('Started Tor .onion service: ' + logger.colors.underline + net.myID)
+ logger.info('Started .onion service: ' + logger.colors.underline + net.myID)
logger.info('Our Public key: ' + self.onionrCore._crypto.pubKey)
time.sleep(1)
- subprocess.Popen(["./communicator.py", "run", str(net.socksPort)])
+ try:
+ if config.get('general.newCommunicator', False):
+ communicatorDaemon = './communicator2.py'
+ logger.info('Using new communicator')
+ except NameError:
+ pass
+ #TODO make runable on windows
+ subprocess.Popen([communicatorDaemon, "run", str(net.socksPort)])
logger.debug('Started communicator')
events.event('daemon_start', onionr = self)
api.API(self.debug)
@@ -542,7 +590,7 @@ class Onionr:
logger.warn('Killing the running daemon...', timestamp = False)
try:
events.event('daemon_stop', onionr = self)
- net = NetController(config.get('client')['port'])
+ net = NetController(config.get('client.port', 59496))
try:
self.onionrUtils.localCommand('shutdown')
except requests.exceptions.ConnectionError:
@@ -561,11 +609,16 @@ class Onionr:
try:
# define stats messages here
+ totalBlocks = len(Block.getBlocks())
+ signedBlocks = len(Block.getBlocks(signed = True))
+ powToken = self.onionrCore._crypto.pubKeyPowToken
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(),
+ 'POW Token' : powToken,
+ 'Combined' : self.onionrCore._crypto.pubKey + '-' + powToken,
+ 'Node Address' : self.get_hostname(),
# file and folder size stats
'div1' : True, # this creates a solid line across the screen, a div
@@ -576,7 +629,9 @@ class Onionr:
# count stats
'div2' : True,
'Known Peers Count' : str(len(self.onionrCore.listPeers()) - 1),
- 'Enabled Plugins Count' : str(len(config.get('plugins')['enabled'])) + ' / ' + str(len(os.listdir('data/plugins/')))
+ 'Enabled Plugins Count' : str(len(config.get('plugins.enabled', list()))) + ' / ' + str(len(os.listdir('data/plugins/'))),
+ 'Known Blocks Count' : str(totalBlocks),
+ 'Percent Blocks Signed' : str(round(100 * signedBlocks / max(totalBlocks, 1), 2)) + '%'
}
# color configuration
@@ -591,19 +646,27 @@ class Onionr:
# pre-processing
maxlength = 0
+ width = self.getConsoleWidth()
for key, val in messages.items():
if not (type(val) is bool and val is True):
maxlength = max(len(key), maxlength)
+ prewidth = maxlength + len(' | ')
+ groupsize = width - prewidth - len('[+] ')
# generate stats table
logger.info(colors['title'] + 'Onionr v%s Statistics' % ONIONR_VERSION + colors['reset'])
- logger.info(colors['border'] + '─' * (maxlength + 1) + '┐' + 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'])
+ val = [str(val)[i:i + groupsize] for i in range(0, len(str(val)), groupsize)]
+
+ logger.info(colors['key'] + str(key).rjust(maxlength) + colors['reset'] + colors['border'] + ' | ' + colors['reset'] + colors['val'] + str(val.pop(0)) + colors['reset'])
+
+ for value in val:
+ logger.info(' ' * maxlength + colors['border'] + ' | ' + colors['reset'] + colors['val'] + str(value) + colors['reset'])
else:
- logger.info(colors['border'] + '─' * (maxlength + 1) + '┤' + colors['reset'])
- logger.info(colors['border'] + '─' * (maxlength + 1) + '┘' + colors['reset'])
+ 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)
@@ -637,19 +700,40 @@ class Onionr:
except Exception:
return None
+ def getConsoleWidth(self):
+ '''
+ Returns an integer, the width of the terminal/cmd window
+ '''
+
+ columns = 80
+
+ try:
+ columns = int(os.popen('stty size', 'r').read().split()[1])
+ except:
+ # if it errors, it's probably windows, so default to 80.
+ pass
+
+ return columns
+
def addFile(self):
- '''command to add a file to the onionr network'''
- if len(sys.argv) >= 2:
- newFile = sys.argv[2]
- logger.info('Attempting to add file...')
- try:
- with open(newFile, 'rb') as new:
- new = new.read()
- except FileNotFoundError:
+ '''
+ Adds a file to the onionr network
+ '''
+
+ if len(sys.argv) >= 3:
+ filename = sys.argv[2]
+ contents = None
+
+ if not os.path.exists(filename):
logger.warn('That file does not exist. Improper path?')
- else:
- logger.debug(new)
- logger.info(self.onionrCore.insertBlock(new, header='bin'))
+
+ try:
+ blockhash = Block.createChain(file = filename)
+ logger.info('File %s saved in block %s.' % (filename, blockhash))
+ except:
+ logger.error('Failed to save file in block.', timestamp = False)
+ else:
+ logger.error('%s add-file ' % sys.argv[0], timestamp = False)
Onionr()
diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py
index a20a4853..90a32b9a 100644
--- a/onionr/onionrblockapi.py
+++ b/onionr/onionrblockapi.py
@@ -18,35 +18,25 @@
along with this program. If not, see .
'''
-import core as onionrcore, logger
-import json, os, datetime
+import core as onionrcore, logger, config
+import json, os, sys, datetime, base64
class Block:
- def __init__(self, hash = None, core = None):
- '''
- Initializes Onionr
+ blockCacheOrder = list() # NEVER write your own code that writes to this!
+ blockCache = dict() # should never be accessed directly, look at Block.getCache()
- 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
+ def __init__(self, hash = None, core = None, type = None, content = None):
+ # take from arguments
+ # sometimes people input a bytes object instead of str in `hash`
+ try:
+ hash = hash.decode()
+ except AttributeError:
+ pass
+ self.hash = hash
+ self.core = core
+ self.btype = type
+ self.bcontent = content
- 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
@@ -57,14 +47,20 @@ class Block:
self.signature = None
self.signedData = None
self.blockFile = None
+ self.parent = None
self.bheader = {}
self.bmetadata = {}
# handle arguments
if self.getCore() is None:
self.core = onionrcore.Core()
+
+ # update the blocks' contents if it exists
if not self.getHash() is None:
- self.update()
+ if not self.update():
+ logger.debug('Failed to open block %s.' % self.getHash())
+ else:
+ logger.debug('Did not update block')
# logic
@@ -92,13 +88,23 @@ class Block:
if blockdata is None:
filelocation = file
+ readfile = True
+
if filelocation is None:
if self.getHash() is None:
return False
+ elif self.getHash() in Block.getCache():
+ # get the block from cache, if it's in it
+ blockdata = Block.getCache(self.getHash())
+ readfile = False
- filelocation = 'data/blocks/%s.dat' % self.getHash()
+ # read from file if it's still None
+ if blockdata is None:
+ filelocation = 'data/blocks/%s.dat' % self.getHash()
- blockdata = open(filelocation, 'rb').read().decode('utf-8')
+ if readfile:
+ with open(filelocation, 'rb') as f:
+ blockdata = f.read().decode()
self.blockFile = filelocation
else:
@@ -108,12 +114,13 @@ class 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.bmetadata = json.loads(self.getHeader('meta', None))
+ self.parent = self.getMetadata('parent', None)
+ self.btype = self.getMetadata('type', None)
+ self.powHash = self.getMetadata('powHash', None)
+ self.powToken = self.getMetadata('powToken', None)
self.signed = ('sig' in self.getHeader() and self.getHeader('sig') != '')
- self.signature = (None if not self.isSigned() else self.getHeader('sig'))
+ self.signature = self.getHeader('sig', None)
self.signedData = (None if not self.isSigned() else self.getHeader('meta') + '\n' + self.getContent())
self.date = self.getCore().getBlockDate(self.getHash())
@@ -121,6 +128,10 @@ class Block:
self.date = datetime.datetime.fromtimestamp(self.getDate())
self.valid = True
+
+ if len(self.getRaw()) <= config.get('allocations.blockCache', 500000):
+ self.cache()
+
return True
except Exception as e:
logger.error('Failed to update block data.', error = e, timestamp = False)
@@ -163,7 +174,7 @@ class Block:
else:
self.hash = self.getCore().insertBlock(self.getContent(), header = self.getType(), sign = sign)
self.update()
- return True
+ return self.getHash()
else:
logger.warn('Not writing block; it is invalid.')
except Exception as e:
@@ -212,7 +223,7 @@ class Block:
return str(self.raw)
- def getHeader(self, key = None):
+ def getHeader(self, key = None, default = None):
'''
Returns the header information
@@ -224,11 +235,12 @@ class Block:
'''
if not key is None:
- return self.getHeader()[key]
- else:
- return self.bheader
+ if key in self.getHeader():
+ return self.getHeader()[key]
+ return default
+ return self.bheader
- def getMetadata(self, key = None):
+ def getMetadata(self, key = None, default = None):
'''
Returns the metadata information
@@ -240,9 +252,10 @@ class Block:
'''
if not key is None:
- return self.getMetadata()[key]
- else:
- return self.bmetadata
+ if key in self.getMetadata():
+ return self.getMetadata()[key]
+ return default
+ return self.bmetadata
def getContent(self):
'''
@@ -254,6 +267,24 @@ class Block:
return str(self.bcontent)
+ def getParent(self):
+ '''
+ Returns the Block's parent Block, or None
+
+ Outputs:
+ - (Block): the Block's parent
+ '''
+
+ if type(self.parent) == str:
+ if self.parent == self.getHash():
+ self.parent = self
+ elif Block.exists(self.parent):
+ self.parent = Block(self.getMetadata('parent'), core = self.getCore())
+ else:
+ self.parent = None
+
+ return self.parent
+
def getDate(self):
'''
Returns the date that the block was received, if loaded from file
@@ -344,12 +375,32 @@ class Block:
- btype (str): the type of block to be set to
Outputs:
- - (Block): the block instance
+ - (Block): the Block instance
'''
self.btype = btype
return self
+ def setMetadata(self, key, val):
+ '''
+ Sets a custom metadata value
+
+ Metadata should not store block-specific data structures.
+
+ Inputs:
+ - key (str): the key
+ - val: the value (type is irrelevant)
+
+ Outputs:
+ - (Block): the Block instance
+ '''
+
+ if key == 'parent' and (not val is None) and (not val == self.getParent().getHash()):
+ self.setParent(val)
+ else:
+ self.bmetadata[key] = val
+ return self
+
def setContent(self, bcontent):
'''
Sets the contents of the block
@@ -358,13 +409,31 @@ class Block:
- bcontent (str): the contents to be set to
Outputs:
- - (Block): the block instance
+ - (Block): the Block instance
'''
self.bcontent = str(bcontent)
return self
- # static
+ def setParent(self, parent):
+ '''
+ Sets the Block's parent
+
+ Inputs:
+ - parent (Block/str): the Block's parent, to be stored in metadata
+
+ Outputs:
+ - (Block): the Block instance
+ '''
+
+ if type(parent) == str:
+ parent = Block(parent, core = self.getCore())
+
+ self.parent = parent
+ self.setMetadata('parent', (None if parent is None else self.getParent().getHash()))
+ return self
+
+ # static functions
def getBlocks(type = None, signer = None, signed = None, reverse = False, core = None):
'''
@@ -410,7 +479,6 @@ class Block:
if relevant:
relevant_blocks.append(block)
-
if bool(reverse):
relevant_blocks.reverse()
@@ -420,6 +488,156 @@ class Block:
return list()
+ def mergeChain(child, file = None, maximumFollows = 32, core = None):
+ '''
+ Follows a child Block to its root parent Block, merging content
+
+ Inputs:
+ - child (str/Block): the child Block to be followed
+ - file (str/file): the file to write the content to, instead of returning it
+ - maximumFollows (int): the maximum number of Blocks to follow
+
+ '''
+
+ # validate data and instantiate Core
+ core = (core if not core is None else onionrcore.Core())
+ maximumFollows = max(0, maximumFollows)
+
+ # type conversions
+ if type(child) == list:
+ child = child[-1]
+ if type(child) == str:
+ child = Block(child)
+ if (not file is None) and (type(file) == str):
+ file = open(file, 'ab')
+
+ # only store hashes to avoid intensive memory usage
+ blocks = [child.getHash()]
+
+ # generate a list of parent Blocks
+ while True:
+ # end if the maximum number of follows has been exceeded
+ if len(blocks) - 1 >= maximumFollows:
+ break
+
+ block = Block(blocks[-1], core = core).getParent()
+
+ # end if there is no parent Block
+ if block is None:
+ break
+
+ # end if the Block is pointing to a previously parsed Block
+ if block.getHash() in blocks:
+ break
+
+ # end if the block is not valid
+ if not block.isValid():
+ break
+
+ blocks.append(block.getHash())
+
+ buffer = ''
+
+ # combine block contents
+ for hash in blocks:
+ block = Block(hash, core = core)
+ contents = block.getContent()
+ contents = base64.b64decode(contents.encode())
+
+ if file is None:
+ buffer += contents.decode()
+ else:
+ file.write(contents)
+
+ return (None if not file is None else buffer)
+
+ def createChain(data = None, chunksize = 99800, file = None, type = 'chunk', sign = True, encrypt = False, verbose = False):
+ '''
+ Creates a chain of blocks to store larger amounts of data
+
+ The chunksize is set to 99800 because it provides the least amount of PoW for the most amount of data.
+
+ Inputs:
+ - data (*): if `file` is None, the data to be stored in blocks
+ - file (file/str): the filename or file object to read from (or None to read `data` instead)
+ - chunksize (int): the number of bytes per block chunk
+ - type (str): the type header for each of the blocks
+ - sign (bool): whether or not to sign each block
+ - encrypt (str): the public key to encrypt to, or False to disable encryption
+ - verbose (bool): whether or not to return a tuple containing more info
+
+ Outputs:
+ - if `verbose`:
+ - (tuple):
+ - (str): the child block hash
+ - (list): all block hashes associated with storing the file
+ - if not `verbose`:
+ - (str): the child block hash
+ '''
+
+ blocks = list()
+
+ # initial datatype checks
+ if data is None and file is None:
+ return blocks
+ elif not (file is None or (isinstance(file, str) and os.path.exists(file))):
+ return blocks
+ elif isinstance(file, str):
+ file = open(file, 'rb')
+ if not isinstance(data, str):
+ data = str(data)
+
+ if not file is None:
+ filesize = os.stat(file.name).st_size
+ offset = filesize % chunksize
+ maxtimes = int(filesize / chunksize)
+
+ for times in range(0, maxtimes + 1):
+ # read chunksize bytes from the file (end -> beginning)
+ if times < maxtimes:
+ file.seek(- ((times + 1) * chunksize), 2)
+ content = file.read(chunksize)
+ else:
+ file.seek(0, 0)
+ content = file.read(offset)
+
+ # encode it- python is really bad at handling certain bytes that
+ # are often present in binaries.
+ content = base64.b64encode(content).decode()
+
+ # if it is the end of the file, exit
+ if not content:
+ break
+
+ # create block
+ block = Block()
+ block.setType(type)
+ block.setContent(content)
+ block.setParent((blocks[-1] if len(blocks) != 0 else None))
+ hash = block.save(sign = sign)
+
+ # remember the hash in cache
+ blocks.append(hash)
+ elif not data is None:
+ for content in reversed([data[n:n + chunksize] for n in range(0, len(data), chunksize)]):
+ # encode chunk with base64
+ content = base64.b64encode(content.encode()).decode()
+
+ # create block
+ block = Block()
+ block.setType(type)
+ block.setContent(content)
+ block.setParent((blocks[-1] if len(blocks) != 0 else None))
+ hash = block.save(sign = sign)
+
+ # remember the hash in cache
+ blocks.append(hash)
+
+ # return different things depending on verbosity
+ if verbose:
+ return (blocks[-1], blocks)
+ return blocks[-1]
+
def exists(hash):
'''
Checks if a block is saved to file or not
@@ -433,11 +651,54 @@ class Block:
- (bool): whether or not the block file exists
'''
+ # no input data? scrap it.
if hash is None:
return False
- elif type(hash) == Block:
+
+ if type(hash) == Block:
blockfile = hash.getBlockFile()
else:
blockfile = 'data/blocks/%s.dat' % hash
return os.path.exists(blockfile) and os.path.isfile(blockfile)
+
+ def getCache(hash = None):
+ # give a list of the hashes of the cached blocks
+ if hash is None:
+ return list(Block.blockCache.keys())
+
+ # if they inputted self or a Block, convert to hash
+ if type(hash) == Block:
+ hash = hash.getHash()
+
+ # just to make sure someone didn't put in a bool or something lol
+ hash = str(hash)
+
+ # if it exists, return its content
+ if hash in Block.getCache():
+ return Block.blockCache[hash]
+
+ return None
+
+ def cache(block, override = False):
+ # why even bother if they're giving bad data?
+ if not type(block) == Block:
+ return False
+
+ # only cache if written to file
+ if block.getHash() is None:
+ return False
+
+ # if it's already cached, what are we here for?
+ if block.getHash() in Block.getCache() and not override:
+ return False
+
+ # dump old cached blocks if the size exeeds the maximum
+ if sys.getsizeof(Block.blockCacheOrder) >= config.get('allocations.blockCacheTotal', 50000000): # 50MB default cache size
+ del Block.blockCache[blockCacheOrder.pop(0)]
+
+ # cache block content
+ Block.blockCache[block.getHash()] = block.getRaw()
+ Block.blockCacheOrder.append(block.getHash())
+
+ return True
diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py
index 84e9827f..a8b67f22 100644
--- a/onionr/onionrcrypto.py
+++ b/onionr/onionrcrypto.py
@@ -17,7 +17,13 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see .
'''
-import nacl.signing, nacl.encoding, nacl.public, nacl.hash, nacl.secret, os, binascii, base64, hashlib, logger, onionrproofs, time, math
+import nacl.signing, nacl.encoding, nacl.public, nacl.hash, nacl.secret, os, binascii, base64, hashlib, logger, onionrproofs, time, math, sys
+
+# secrets module was added into standard lib in 3.6+
+if sys.version_info[0] == 3 and sys.version_info[1] < 6:
+ from dependencies import secrets
+elif sys.version_info[0] == 3 and sys.version_info[1] >= 6:
+ import secrets
class OnionrCrypto:
def __init__(self, coreInstance):
@@ -27,6 +33,8 @@ class OnionrCrypto:
self.pubKey = None
self.privKey = None
+ self.secrets = secrets
+
self.pubKeyPowToken = None
#self.pubKeyPowHash = None
@@ -102,7 +110,7 @@ class OnionrCrypto:
retData = key.sign(data).signature
return retData
- def pubKeyEncrypt(self, data, pubkey, anonymous=False, encodedData=False):
+ def pubKeyEncrypt(self, data, pubkey, anonymous=True, encodedData=False):
'''Encrypt to a public key (Curve25519, taken from base32 Ed25519 pubkey)'''
retVal = ''
@@ -247,29 +255,28 @@ class OnionrCrypto:
'''
retData = False
- if not (('powToken' in metadata) and ('powHash' in metadata)):
+ if not 'powRandomToken' in metadata:
+ logger.warn('No powRandomToken')
return False
dataLen = len(blockContent)
- expectedHash = self.blake2bHash(base64.b64decode(metadata['powToken']) + self.blake2bHash(blockContent.encode()))
+ expectedHash = self.blake2bHash(base64.b64decode(metadata['powRandomToken']) + 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]
+ difficulty = math.floor(dataLen / 1000000)
- if metadata['powHash'][:difficulty] == puzzle:
- # logger.debug('Validated block pow')
- retData = True
- else:
- logger.debug("Invalid token (#1)")
+ mainHash = '0000000000000000000000000000000000000000000000000000000000000000'#nacl.hash.blake2b(nacl.utils.random()).decode()
+ puzzle = mainHash[:difficulty]
+
+ if metadata['powRandomToken'][:difficulty] == puzzle:
+ # logger.debug('Validated block pow')
+ retData = True
else:
- logger.debug('Invalid token (#2): Expected hash %s, got hash %s...' % (metadata['powHash'], expectedHash))
+ logger.debug("Invalid token, bad proof")
return retData
diff --git a/onionr/onionrexceptions.py b/onionr/onionrexceptions.py
new file mode 100644
index 00000000..dc6485a1
--- /dev/null
+++ b/onionr/onionrexceptions.py
@@ -0,0 +1,41 @@
+'''
+ Onionr - P2P Microblogging Platform & Social network.
+
+ This file contains exceptions for 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 .
+'''
+
+# general exceptions
+class NotFound(Exception):
+ pass
+class Unknown(Exception):
+ pass
+class Invalid(Exception):
+ pass
+
+# crypto exceptions
+class InvalidPubkey(Exception):
+ pass
+
+# block exceptions
+class InvalidMetadata(Exception):
+ pass
+
+# network level exceptions
+class MissingPort(Exception):
+ pass
+class InvalidAddress(Exception):
+ pass
diff --git a/onionr/onionrpeers.py b/onionr/onionrpeers.py
new file mode 100644
index 00000000..b6ed72ec
--- /dev/null
+++ b/onionr/onionrpeers.py
@@ -0,0 +1,19 @@
+'''
+ Onionr - P2P Microblogging Platform & Social network.
+
+ This file contains both the OnionrCommunicate class for communcating with peers
+'''
+'''
+ 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 .
+'''
\ No newline at end of file
diff --git a/onionr/onionrplugins.py b/onionr/onionrplugins.py
index 175b2336..6160838e 100644
--- a/onionr/onionrplugins.py
+++ b/onionr/onionrplugins.py
@@ -64,9 +64,7 @@ def enable(name, onionr = None, start_event = True):
enabled_plugins = get_enabled_plugins()
if not name in enabled_plugins:
enabled_plugins.append(name)
- config_plugins = config.get('plugins')
- config_plugins['enabled'] = enabled_plugins
- config.set('plugins', config_plugins, True)
+ config.set('plugins.enabled', enabled_plugins, True)
events.call(get_plugin(name), 'enable', onionr)
@@ -77,7 +75,7 @@ def enable(name, onionr = None, start_event = True):
else:
return False
else:
- logger.error('Failed to enable plugin \"' + name + '\", disabling plugin.')
+ logger.error('Failed to enable plugin \"%s\", disabling plugin.' % name)
disable(name)
return False
@@ -93,9 +91,7 @@ def disable(name, onionr = None, stop_event = True):
if is_enabled(name):
enabled_plugins = get_enabled_plugins()
enabled_plugins.remove(name)
- config_plugins = config.get('plugins')
- config_plugins['enabled'] = enabled_plugins
- config.set('plugins', config_plugins, True)
+ config.set('plugins.enabled', enabled_plugins, True)
if exists(name):
events.call(get_plugin(name), 'disable', onionr)
@@ -121,9 +117,9 @@ def start(name, onionr = None):
return plugin
except:
- logger.error('Failed to start module \"' + name + '\".')
+ logger.error('Failed to start module \"%s\".' % name)
else:
- logger.error('Failed to start nonexistant module \"' + name + '\".')
+ logger.error('Failed to start nonexistant module \"%s\".' % name)
return None
@@ -145,9 +141,9 @@ def stop(name, onionr = None):
return plugin
except:
- logger.error('Failed to stop module \"' + name + '\".')
+ logger.error('Failed to stop module \"%s\".' % name)
else:
- logger.error('Failed to stop nonexistant module \"' + name + '\".')
+ logger.error('Failed to stop nonexistant module \"%s\".' % name)
return None
@@ -187,7 +183,7 @@ def get_enabled_plugins():
config.reload()
- return config.get('plugins')['enabled']
+ return config.get('plugins.enabled', list())
def is_enabled(name):
'''
diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py
index bf749ad6..7f65bff7 100644
--- a/onionr/onionrproofs.py
+++ b/onionr/onionrproofs.py
@@ -22,7 +22,37 @@ import nacl.encoding, nacl.hash, nacl.utils, time, math, threading, binascii, lo
import core
class POW:
- def pow(self, reporting = False):
+ def __init__(self, data, threadCount = 5):
+ self.foundHash = False
+ self.difficulty = 0
+ self.data = data
+ self.threadCount = threadCount
+
+ 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.info('Computing POW (difficulty: %s)...' % self.difficulty)
+
+ self.mainHash = '0' * 70
+ self.puzzle = self.mainHash[0:min(self.difficulty, len(self.mainHash))]
+
+ myCore = core.Core()
+ for i in range(max(1, threadCount)):
+ t = threading.Thread(name = 'thread%s' % i, target = self.pow, args = (True,myCore))
+ t.start()
+
+ return
+
+ def pow(self, reporting = False, myCore = None):
startTime = math.floor(time.time())
self.hashing = True
self.reporting = reporting
@@ -30,7 +60,7 @@ class POW:
answer = ''
heartbeat = 200000
hbCount = 0
- myCore = core.Core()
+
while self.hashing:
rand = nacl.utils.random()
token = nacl.hash.blake2b(rand + self.data).decode()
@@ -39,45 +69,14 @@ class POW:
self.hashing = False
iFound = True
break
- else:
- logger.debug('POW thread exiting, another thread found result')
+
if iFound:
endTime = math.floor(time.time())
if self.reporting:
- logger.info('Found token ' + token, timestamp=True)
- logger.info('rand value: ' + base64.b64encode(rand).decode())
- logger.info('took ' + str(endTime - startTime) + ' seconds', timestamp=True)
+ logger.debug('Found token after %s seconds: %s' % (endTime - startTime, token), timestamp=True)
+ logger.debug('Random value was: %s' % base64.b64encode(rand).decode())
self.result = (token, rand)
- def __init__(self, data):
- self.foundHash = False
- 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))
-
- self.mainHash = '0000000000000000000000000000000000000000000000000000000000000000'#nacl.hash.blake2b(nacl.utils.random()).decode()
- self.puzzle = self.mainHash[0:self.difficulty]
- #logger.debug('trying to find ' + str(self.mainHash))
- tOne = threading.Thread(name='one', target=self.pow, args=(True,))
- tTwo = threading.Thread(name='two', target=self.pow, args=(True,))
- tThree = threading.Thread(name='three', target=self.pow, args=(True,))
- tOne.start()
- tTwo.start()
- tThree.start()
- return
-
def shutdown(self):
self.hashing = False
self.puzzle = ''
@@ -89,9 +88,28 @@ class POW:
'''
Returns the result then sets to false, useful to automatically clear the result
'''
+
try:
retVal = self.result
except AttributeError:
retVal = False
+
self.result = False
return retVal
+
+ def waitForResult(self):
+ '''
+ Returns the result only when it has been found, False if not running and not found
+ '''
+ result = False
+ try:
+ while True:
+ result = self.getResult()
+ if not self.hashing:
+ break
+ else:
+ time.sleep(2)
+ except KeyboardInterrupt:
+ self.shutdown()
+ logger.warn('Got keyboard interrupt while waiting for POW result, stopping')
+ return result
\ No newline at end of file
diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py
index e96cabee..84225b04 100644
--- a/onionr/onionrutils.py
+++ b/onionr/onionrutils.py
@@ -18,8 +18,11 @@
along with this program. If not, see .
'''
# 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, glob, shutil, math
+import getpass, sys, requests, os, socket, hashlib, logger, sqlite3, config, binascii, time, base64, json, glob, shutil, math, json
import nacl.signing, nacl.encoding
+from onionrblockapi import Block
+import onionrexceptions
+from defusedxml import minidom
if sys.version_info < (3, 6):
try:
@@ -77,6 +80,13 @@ class OnionrUtils:
return
+ def getCurrentHourEpoch(self):
+ '''
+ Returns the current epoch, rounded down to the hour
+ '''
+ epoch = self.getEpoch()
+ return epoch - (epoch % 3600)
+
def incrementAddressSuccess(self, address):
'''
Increase the recorded sucesses for an address
@@ -95,7 +105,7 @@ class OnionrUtils:
def mergeKeys(self, newKeyList):
'''
- Merge ed25519 key list to our database
+ Merge ed25519 key list to our database, comma seperated string
'''
try:
retVal = False
@@ -121,8 +131,9 @@ class OnionrUtils:
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("Failed to add key")
else:
- logger.warn(powHash)
logger.warn('%s pow failed' % key[0])
return retVal
except Exception as error:
@@ -165,8 +176,11 @@ class OnionrUtils:
config.reload()
self.getTimeBypassToken()
# TODO: URL encode parameters, just as an extra measure. May not be needed, but should be added regardless.
+ with open('data/host.txt', 'r') as host:
+ hostname = host.read()
+ payload = 'http://%s:%s/client/?action=%s&token=%s&timingToken=%s' % (hostname, config.get('client.port'), command, config.get('client.hmac'), self.timingToken)
try:
- 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(payload).text
except Exception as error:
if not silent:
logger.error('Failed to make local request (command: %s).' % command, error=error)
@@ -195,6 +209,22 @@ class OnionrUtils:
return pass1
+ def getBlockMetadataFromData(self, blockData):
+ '''
+ accepts block contents as string and returns a tuple of metadata, meta (meta being internal metadata)
+ '''
+ try:
+ blockData = blockData.encode()
+ except AttributeError:
+ pass
+ metadata = json.loads(blockData[:blockData.find(b'\n')].decode())
+ data = blockData[blockData.find(b'\n'):].decode()
+ try:
+ meta = json.loads(metadata['meta'])
+ except KeyError:
+ meta = {}
+ return (metadata, meta, data)
+
def checkPort(self, port, host=''):
'''
Checks if a port is available, returns bool
@@ -279,6 +309,38 @@ class OnionrUtils:
retVal = False
return retVal
+
+ def validateMetadata(metadata):
+ '''Validate metadata meets onionr spec (does not validate proof value computation), take in either dictionary or json string'''
+ # TODO, make this check sane sizes
+ retData = False
+
+ # convert to dict if it is json string
+ if type(metadata) is str:
+ try:
+ metadata = json.loads(metadata)
+ except json.JSONDecodeError:
+ pass
+
+ # Validate metadata dict for invalid keys to sizes that are too large
+ if type(metadata) is dict:
+ for i in metadata:
+ try:
+ self._core.requirements.blockMetadataLengths[i]
+ except KeyError:
+ logger.warn('Block has invalid metadata key ' + i)
+ break
+ else:
+ if self._core.requirements.blockMetadataLengths[i] < len(metadata[i]):
+ logger.warn('Block metadata key ' + i + ' exceeded maximum size')
+ break
+ else:
+ # if metadata loop gets no errors, it does not break, therefore metadata is valid
+ retData = True
+ else:
+ logger.warn('In call to utils.validateMetadata, metadata must be JSON string or a dictionary object')
+
+ return retData
def validatePubKey(self, key):
'''
@@ -347,47 +409,41 @@ class OnionrUtils:
'''
Find, decrypt, and return array of PMs (array of dictionary, {from, text})
'''
- #blocks = self._core.getBlockList()
- blocks = self._core.getBlocksByType('pm')
+ blocks = Block.getBlocks(type = 'pm', core = self._core)
message = ''
sender = ''
for i in blocks:
- if len (i) == 0:
- continue
try:
- with open('data/blocks/' + i + '.dat', 'r') as potentialMessage:
- potentialMessage = potentialMessage.read()
- blockMetadata = json.loads(potentialMessage[:potentialMessage.find('\n')])
- blockContent = potentialMessage[potentialMessage.find('\n') + 1:]
+ blockContent = i.getContent()
+
+ try:
+ message = self._core._crypto.pubKeyDecrypt(blockContent, encodedData=True, anonymous=True)
+ except nacl.exceptions.CryptoError as e:
+ pass
+ else:
+ try:
+ message = message.decode()
+ except AttributeError:
+ pass
try:
- message = self._core._crypto.pubKeyDecrypt(blockContent, encodedData=True, anonymous=True)
- except nacl.exceptions.CryptoError as e:
+ message = json.loads(message)
+ except json.decoder.JSONDecodeError:
pass
else:
- try:
- message = message.decode()
- except AttributeError:
- pass
+ logger.debug('Decrypted %s:' % i.getHash())
+ logger.info(message["msg"])
- try:
- message = json.loads(message)
- except json.decoder.JSONDecodeError:
- pass
- else:
- logger.info('Decrypted %s:' % i)
- logger.info(message["msg"])
+ signer = message["id"]
+ sig = message["sig"]
- signer = message["id"]
- sig = message["sig"]
-
- if self.validatePubKey(signer):
- if self._core._crypto.edVerify(message["msg"], signer, sig, encodedData=True):
- logger.info("Good signature by %s" % signer)
- else:
- logger.warn("Bad signature by %s" % signer)
+ if self.validatePubKey(signer):
+ if self._core._crypto.edVerify(message["msg"], signer, sig, encodedData=True):
+ logger.info("Good signature by %s" % signer)
else:
- logger.warn('Bad sender id: %s' % signer)
+ logger.warn("Bad signature by %s" % signer)
+ else:
+ logger.warn('Bad sender id: %s' % signer)
except FileNotFoundError:
pass
@@ -475,11 +531,55 @@ class OnionrUtils:
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 doGetRequest(self, url, port=0, proxyType='tor'):
+ '''
+ Do a get request through a local tor or i2p instance
+ '''
+ if proxyType == 'tor':
+ if port == 0:
+ raise onionrexceptions.MissingPort('Socks port required for Tor HTTP get request')
+ proxies = {'http': 'socks5://127.0.0.1:' + str(port), 'https': 'socks5://127.0.0.1:' + str(port)}
+ elif proxyType == 'i2p':
+ proxies = {'http': 'http://127.0.0.1:4444'}
+ else:
+ return
+ headers = {'user-agent': 'PyOnionr'}
+ try:
+ proxies = {'http': 'socks5h://127.0.0.1:' + str(port), 'https': 'socks5h://127.0.0.1:' + str(port)}
+ r = requests.get(url, headers=headers, proxies=proxies, allow_redirects=False, timeout=(15, 30))
+ retData = r.text
+ except requests.exceptions.RequestException as e:
+ logger.debug('Error: %s' % str(e))
+ retData = False
+ return retData
+
+ def getNistBeaconSalt(self, torPort=0):
+ '''
+ Get the token for the current hour from the NIST randomness beacon
+ '''
+ if torPort == 0:
+ try:
+ sys.argv[2]
+ except IndexError:
+ raise onionrexceptions.MissingPort('Missing Tor socks port')
+ retData = ''
+ curTime = self._core._utils.getCurrentHourEpoch
+ self.nistSaltTimestamp = curTime
+ data = self.doGetRequest('https://beacon.nist.gov/rest/record/' + str(curTime), port=torPort)
+ dataXML = minidom.parseString(data, forbid_dtd=True, forbid_entities=True, forbid_external=True)
+ try:
+ retData = dataXML.getElementsByTagName('outputValue')[0].childNodes[0].data
+ except ValueError:
+ logger.warn('Could not get NIST beacon value')
+ else:
+ self.powSalt = retData
+ return retData
+
def size(path='.'):
'''
Returns the size of a folder's contents in bytes
@@ -504,4 +604,4 @@ def humanSize(num, suffix='B'):
if abs(num) < 1024.0:
return "%.1f %s%s" % (num, unit, suffix)
num /= 1024.0
- return "%.1f %s%s" % (num, 'Yi', suffix)
\ No newline at end of file
+ return "%.1f %s%s" % (num, 'Yi', suffix)
diff --git a/onionr/onionrvalues.py b/onionr/onionrvalues.py
new file mode 100644
index 00000000..b9fd4a2b
--- /dev/null
+++ b/onionr/onionrvalues.py
@@ -0,0 +1,24 @@
+'''
+ Onionr - P2P Microblogging Platform & Social network
+
+ This file defines values and requirements used by 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 .
+'''
+
+class OnionrValues:
+ def __init__(self):
+ self.passwordLength = 20
+ self.blockMetadataLengths = {'meta': 1000, 'sig': 88, 'signer': 64, 'time': 10, 'powRandomToken': '1000'}
\ No newline at end of file
diff --git a/onionr/static-data/default-plugins/pluginmanager/.gitignore b/onionr/static-data/default-plugins/pluginmanager/.gitignore
new file mode 100644
index 00000000..894a44cc
--- /dev/null
+++ b/onionr/static-data/default-plugins/pluginmanager/.gitignore
@@ -0,0 +1,104 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
diff --git a/onionr/static-data/default-plugins/pluginmanager/LICENSE b/onionr/static-data/default-plugins/pluginmanager/LICENSE
new file mode 100644
index 00000000..94a9ed02
--- /dev/null
+++ b/onionr/static-data/default-plugins/pluginmanager/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ 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 .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/onionr/static-data/default-plugins/pluginmanager/README.md b/onionr/static-data/default-plugins/pluginmanager/README.md
new file mode 100644
index 00000000..141896aa
--- /dev/null
+++ b/onionr/static-data/default-plugins/pluginmanager/README.md
@@ -0,0 +1,2 @@
+# pluginmanager
+Onionr's plugin manager source code
diff --git a/onionr/static-data/default-plugins/pluginmanager/main.py b/onionr/static-data/default-plugins/pluginmanager/main.py
index 59c23d44..4812cc90 100644
--- a/onionr/static-data/default-plugins/pluginmanager/main.py
+++ b/onionr/static-data/default-plugins/pluginmanager/main.py
@@ -34,9 +34,9 @@ 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()
+ with open(keys_file, 'w') as file:
+ file.write(json.dumps(keys_data, indent=4, sort_keys=True))
+ file.close()
def readKeys():
'''
@@ -44,7 +44,8 @@ def readKeys():
'''
global keys_data
- keys_data = json.loads(open(keys_file).read())
+ with open(keys_file) as file:
+ keys_data = json.loads(file.read())
return keys_data
def getKey(plugin):
@@ -106,27 +107,37 @@ def getRepositories():
readKeys()
return keys_data['repositories']
-def addRepository(repositories, data):
+def addRepository(blockhash, data):
'''
Saves the plugin name, to remember that it was installed by the pluginmanager
'''
global keys_data
readKeys()
- keys_data['repositories'][repositories] = data
+ keys_data['repositories'][blockhash] = data
writeKeys()
-def removeRepository(repositories):
+def removeRepository(blockhash):
'''
Removes the plugin name from the pluginmanager's records
'''
global keys_data
readKeys()
- if plugin in keys_data['repositories']:
- del keys_data['repositories'][repositories]
+ if blockhash in keys_data['repositories']:
+ del keys_data['repositories'][blockhash]
writeKeys()
+def createRepository(plugins):
+ contents = {'plugins' : plugins, 'author' : getpass.getuser(), 'compiled-by' : plugin_name}
+
+ block = Block(core = pluginapi.get_core())
+
+ block.setType('repository')
+ block.setContent(json.dumps(contents))
+
+ return block.save(True)
+
def check():
'''
Checks to make sure the keystore file still exists
@@ -144,7 +155,7 @@ def sanitize(name):
def blockToPlugin(block):
try:
- block = Block(block)
+ block = Block(block, core = pluginapi.get_core())
blockContent = json.loads(block.getContent())
name = sanitize(blockContent['name'])
@@ -194,14 +205,19 @@ def pluginToBlock(plugin, import_block = True):
shutil.rmtree(directory + '__pycache__')
shutil.make_archive(zipfile[:-4], 'zip', directory)
- data = base64.b64encode(open(zipfile, 'rb').read())
+ data = ''
+ with open(zipfile, 'rb') as file:
+ data = base64.b64encode(file.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())
+ info = ''
+ with open(directory + 'info.json').read() as file:
+ info = json.loads(file.read())
+
if 'author' in info:
author = info['author']
if 'description' in info:
@@ -211,7 +227,13 @@ def pluginToBlock(plugin, import_block = True):
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)
+ block = Block(core = pluginapi.get_core())
+
+ block.setType('plugin')
+ block.setContent(json.dumps(metadata))
+
+ hash = block.save(True)
+ # hash = pluginapi.get_core().insertBlock(, header = 'plugin', sign = True)
if import_block:
pluginapi.get_utils().importNewBlocks()
@@ -226,7 +248,7 @@ def pluginToBlock(plugin, import_block = True):
def installBlock(block):
try:
- block = Block(block)
+ block = Block(block, core = pluginapi.get_core())
blockContent = json.loads(block.getContent())
name = sanitize(blockContent['name'])
@@ -353,7 +375,8 @@ def commandInstallPlugin():
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:
+ choice = int(choice)
+ if choice <= index and choice >= 1:
distributor = distributors[int(choice)]
valid = True
except KeyboardInterrupt:
@@ -367,10 +390,12 @@ def commandInstallPlugin():
except Exception as e:
logger.warn('Failed to lookup plugin in repositories.', timestamp = False)
logger.error('asdf', error = e, timestamp = False)
+
+ return True
if pkobh is None:
- logger.error('No key for this plugin found in keystore or repositories, please specify.')
- help()
+ logger.error('No key for this plugin found in keystore or repositories, please specify.', timestamp = False)
+
return True
valid_hash = pluginapi.get_utils().validateHash(pkobh)
@@ -386,7 +411,7 @@ def commandInstallPlugin():
blockhash = None
if valid_hash and not real_block:
- logger.error('Block hash not found. Perhaps it has not been synced yet?')
+ logger.error('Block hash not found. Perhaps it has not been synced yet?', timestamp = False)
logger.debug('Is valid hash, but does not belong to a known block.')
return True
@@ -396,7 +421,7 @@ def commandInstallPlugin():
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.error('Public key not found. Try adding the node by address manually, if possible.', timestamp = False)
logger.debug('Is valid key, but the key is not a known one.')
elif valid_key and real_key:
publickey = str(pkobh)
@@ -432,10 +457,11 @@ def commandInstallPlugin():
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)
+ logger.warn('Only continue the installation if you are absolutely certain that you trust the plugin distributor. Public key of plugin distributor: %s' % publickey, timestamp = False)
+ logger.debug('Most recent block matching parameters is %s' % mostRecentVersionBlock)
installBlock(mostRecentVersionBlock)
else:
- logger.error('Unknown data "%s"; must be public key or block hash.' % str(pkobh))
+ logger.error('Unknown data "%s"; must be public key or block hash.' % str(pkobh), timestamp = False)
return
else:
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [public key/block hash]')
@@ -463,11 +489,11 @@ def commandAddRepository():
if pluginapi.get_utils().validateHash(blockhash):
if Block.exists(blockhash):
try:
- blockContent = json.loads(Block(blockhash).getContent())
+ blockContent = json.loads(Block(blockhash, core = pluginapi.get_core()).getContent())
pluginslist = dict()
- for pluginname, distributor in blockContent['plugins'].items():
+ for pluginname, distributor in blockContent['plugins']:
if pluginapi.get_utils().validatePubKey(distributor):
pluginslist[pluginname] = distributor
@@ -477,14 +503,14 @@ def commandAddRepository():
addRepository(blockhash, pluginslist)
logger.info('Successfully added repository.')
else:
- logger.error('Repository contains no records, not importing.')
+ logger.error('Repository contains no records, not importing.', timestamp = False)
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.error('Block hash not found. Perhaps it has not been synced yet?', timestamp = False)
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))
+ logger.error('Unknown data "%s"; must be block hash.' % str(pkobh), timestamp = False)
else:
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [block hash]')
@@ -500,10 +526,11 @@ def commandRemoveRepository():
if blockhash in getRepositories():
try:
removeRepository(blockhash)
+ logger.info('Successfully removed repository.')
except Exception as e:
logger.error('Failed to parse block.', error = e)
else:
- logger.error('Repository has not been imported, nothing to remove.')
+ logger.error('Repository has not been imported, nothing to remove.', timestamp = False)
else:
logger.error('Unknown data "%s"; must be block hash.' % str(pkobh))
else:
@@ -525,7 +552,49 @@ def commandPublishPlugin():
logger.error('Plugin %s does not exist.' % pluginname, timestamp = False)
else:
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' ')
-
+
+def commandCreateRepository():
+ if len(sys.argv) >= 3:
+ check()
+
+ plugins = list()
+ script = sys.argv[0]
+
+ del sys.argv[:2]
+ success = True
+ for pluginname in sys.argv:
+ distributor = None
+
+ if ':' in pluginname:
+ split = pluginname.split(':')
+ pluginname = split[0]
+ distributor = split[1]
+
+ pluginname = sanitize(pluginname)
+
+ if distributor is None:
+ distributor = getKey(pluginname)
+ if distributor is None:
+ logger.error('No distributor key was found for the plugin %s.' % pluginname, timestamp = False)
+ success = False
+
+ plugins.append([pluginname, distributor])
+
+ if not success:
+ logger.error('Please correct the above errors, then recreate the repository.')
+ return True
+
+ blockhash = createRepository(plugins)
+ print(blockhash)
+ if not blockhash is None:
+ logger.info('Successfully created repository. Execute the following command to add the repository:\n ' + logger.colors.underline + '%s --add-repository %s' % (script, blockhash))
+ else:
+ logger.error('Failed to create repository, an unknown error occurred.')
+ else:
+ logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [plugins...]')
+
+ return True
+
# event listeners
def on_init(api, data = None):
@@ -540,6 +609,7 @@ def on_init(api, data = None):
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)
+ api.commands.register(['create-repository', 'create-repo', 'createrepo', 'createrepository', 'repocreate'], commandCreateRepository)
# add help menus once the features are actually implemented
diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json
index 1dae4f12..40958e97 100644
--- a/onionr/static-data/default_config.json
+++ b/onionr/static-data/default_config.json
@@ -1,6 +1,17 @@
{
- "devmode": true,
- "dc_response": true,
+ "general" : {
+ "dev_mode": true,
+ "display_header" : true,
+
+ "newCommunicator": false,
+
+ "dc_response": true,
+ "dc_execcallbacks" : true
+ },
+
+ "client" : {
+
+ },
"log": {
"file": {
@@ -13,13 +24,21 @@
"color": true
}
},
+
+ "tor" : {
+
+ },
+
"i2p":{
- "host": false,
- "connect": true,
- "ownAddr": ""
+ "host": false,
+ "connect": true,
+ "ownAddr": ""
},
+
"allocations":{
"disk": 1000000000,
- "netTotal": 1000000000
+ "netTotal": 1000000000,
+ "blockCache" : 5000000,
+ "blockCacheTotal" : 50000000
}
}
diff --git a/onionr/static-data/default_plugin.py b/onionr/static-data/default_plugin.py
index cc6d1d20..7704d32b 100644
--- a/onionr/static-data/default_plugin.py
+++ b/onionr/static-data/default_plugin.py
@@ -5,6 +5,7 @@
# Imports some useful libraries
import logger, config
+from onionrblockapi import Block
plugin_name = '$name'
diff --git a/onionr/static-data/header.txt b/onionr/static-data/header.txt
new file mode 100644
index 00000000..045c8aa1
--- /dev/null
+++ b/onionr/static-data/header.txt
@@ -0,0 +1,25 @@
+
+P G'
+P G''
+P G'' '
+P G''''''
+P :G;'''''P:
+P ::G;'''P::
+P :::G;;P:::
+P ::::::::
+P ::::::::::::
+P :::::::::::::::
+P ::::: :::::
+P ::::: :::::::: ::::
+P ::: :::::: ::::::: :::: W:::::: :: :: :: ::::: ::: :: ::::::
+P ::: :::: :::: ::::: ::: W:: :: ::: :: :: :: :: :::: :: :: ::
+P ::: ::::: :::::: :::: :::: W:: :: :::::: :: :: :: :::: :: :: ::
+P ::: :::: ::::::: :::: :::: W:: :: :: ::: :: :: :: :: :::: :::::
+P ::: ::::: :::::: :::: :::: W:: :: :: ::: :: :: :: :: ::: :: :::
+P :::: ::::: ::::: ::: W :::: :: :: :: ::::: :: :: :: ::
+P :::: :::::: :::::: ::::
+P :::: :::::::::::: ::::
+P ::::: :::::::: ::::
+P ::::: ::::::
+P ::::::::::::::::
+P :::::::
diff --git a/onionr/tests.py b/onionr/tests.py
index db62dcb3..4a83a330 100755
--- a/onionr/tests.py
+++ b/onionr/tests.py
@@ -117,6 +117,44 @@ class OnionrTests(unittest.TestCase):
self.assertTrue(True)
+ def testBlockAPI(self):
+ logger.debug('-'*26 + '\n')
+ logger.info('Running BlockAPI test #1...')
+
+ content = 'Onionr test block'
+
+ from onionrblockapi import Block
+ hash = Block(type = 'test', content = content).save()
+ block = Block(hash) # test init
+
+ if len(Block.getBlocks(type = 'test')) == 0:
+ logger.warn('Failed to find test block.')
+ self.assertTrue(False)
+ if not block.getContent() == content:
+ logger.warn('Test block content is invalid! (%s != %s)' % (block.getContent(), content))
+ self.assertTrue(False)
+
+ logger.debug('-'*26 + '\n')
+ logger.info('Running BlockAPI test #2...')
+
+ original_content = 'onionr'
+
+ logger.debug('original: %s' % original_content)
+
+ blocks = Block.createChain(data = original_content, chunksize = 2, verbose = True)
+
+ logger.debug(blocks[1])
+
+ child = blocks[0]
+ merged = Block.mergeChain(child)
+
+ logger.debug('merged blocks (child: %s): %s' % (child, merged))
+
+ if merged != original_content:
+ self.assertTrue(False)
+ self.assertTrue(True)
+
+
def testBitcoinNode(self):
# temporarily disabled- this takes a lot of time the CI doesn't have
self.assertTrue(True)
@@ -234,6 +272,6 @@ class OnionrTests(unittest.TestCase):
else:
self.assertTrue(False)
else:
- self.assertTrue(False)
+ self.assertTrue(False) # <- annoying :(
unittest.main()
diff --git a/onionr/timedhmac.py b/onionr/timedhmac.py
deleted file mode 100644
index a6163ead..00000000
--- a/onionr/timedhmac.py
+++ /dev/null
@@ -1,50 +0,0 @@
-'''
- Onionr - P2P Microblogging Platform & Social network. Run with 'help' for usage.
- 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 .
-'''
-
-import hmac, base64, time, math
-
-class TimedHMAC:
- def __init__(self, base64Key, data, hashAlgo):
- '''
- base64Key = base64 encoded key
- data = data to hash
- expire = time expiry in epoch
- hashAlgo = string in hashlib.algorithms_available
-
- Maximum of 10 seconds grace period
- '''
-
- self.data = data
- self.expire = math.floor(time.time())
- self.hashAlgo = hashAlgo
- self.b64Key = base64Key
- generatedHMAC = hmac.HMAC(base64.b64decode(base64Key).decode(), digestmod=self.hashAlgo)
- generatedHMAC.update(data + expire)
- self.HMACResult = generatedHMAC.hexdigest()
-
- return
-
- def check(self, data):
- '''
- Check a hash (and verify time is sane)
- '''
-
- testHash = hmac.HMAC(base64.b64decode(base64Key).decode(), digestmod=self.hashAlgo)
- testHash.update(data + math.floor(time.time()))
- testHash = testHash.hexdigest()
- if hmac.compare_digest(testHash, self.HMACResult):
- return true
-
- return false
diff --git a/requirements.txt b/requirements.txt
index ffdcdf77..e6bad4ab 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,5 +7,6 @@ sha3==0.2.1
simple_crypt==4.1.7
ecdsa==0.13
requests==2.12.4
+defusedxml==0.5.0
SocksiPy_branch==1.01
sphinx_rtd_theme==0.3.0