Make Onionr more user friendly

This commit mostly just made messages more readable, worked on logger, and fixed a few bugs
master
Arinerron 2018-11-10 19:25:40 -08:00
parent 22115891f2
commit bb08162019
No known key found for this signature in database
GPG Key ID: 99383627861C62F0
16 changed files with 245 additions and 152 deletions

View File

@ -24,7 +24,7 @@ from gevent.pywsgi import WSGIServer
import sys, random, threading, hmac, hashlib, base64, time, math, os, json import sys, random, threading, hmac, hashlib, base64, time, math, os, json
import core import core
from onionrblockapi import Block from onionrblockapi import Block
import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config, onionr
class API: class API:
''' '''
@ -75,14 +75,8 @@ class API:
This also saves the used host (random localhost IP address) to the data folder in host.txt This also saves the used host (random localhost IP address) to the data folder in host.txt
''' '''
config.reload() # configure logger and stuff
onionr.Onionr.setupConfig('data/', self = self)
if config.get('dev_mode', True):
self._developmentMode = True
logger.set_level(logger.LEVEL_DEBUG)
else:
self._developmentMode = False
logger.set_level(logger.LEVEL_INFO)
self.debug = debug self.debug = debug
self._privateDelayTime = 3 self._privateDelayTime = 3
@ -131,14 +125,14 @@ class API:
resp.headers["Content-Security-Policy"] = "default-src 'none'; script-src 'none'; object-src 'none'; style-src data: 'unsafe-inline'; img-src data:; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'none'" resp.headers["Content-Security-Policy"] = "default-src 'none'; script-src 'none'; object-src 'none'; style-src data: 'unsafe-inline'; img-src data:; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'none'"
resp.headers['X-Frame-Options'] = 'deny' resp.headers['X-Frame-Options'] = 'deny'
resp.headers['X-Content-Type-Options'] = "nosniff" resp.headers['X-Content-Type-Options'] = "nosniff"
resp.headers['api'] = API_VERSION resp.headers['X-API'] = API_VERSION
# reset to text/plain to help prevent browser attacks # reset to text/plain to help prevent browser attacks
self.mimeType = 'text/plain' self.mimeType = 'text/plain'
self.overrideCSP = False self.overrideCSP = False
return resp return resp
@app.route('/www/private/<path:path>') @app.route('/www/private/<path:path>')
def www_private(path): def www_private(path):
startTime = math.floor(time.time()) startTime = math.floor(time.time())

View File

@ -21,12 +21,14 @@
''' '''
import sys, os, core, config, json, requests, time, logger, threading, base64, onionr, uuid import sys, os, core, config, json, requests, time, logger, threading, base64, onionr, uuid
import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block
import onionrdaemontools, onionrsockets, onionrchat import onionrdaemontools, onionrsockets, onionrchat, onionr
from dependencies import secrets from dependencies import secrets
from defusedxml import minidom from defusedxml import minidom
class OnionrCommunicatorDaemon: class OnionrCommunicatorDaemon:
def __init__(self, debug, developmentMode): def __init__(self, debug, developmentMode):
# configure logger and stuff
onionr.Onionr.setupConfig('data/', self = self)
self.isOnline = True # Assume we're connected to the internet self.isOnline = True # Assume we're connected to the internet
@ -303,9 +305,11 @@ class OnionrCommunicatorDaemon:
self.decrementThreadCount('clearOfflinePeer') self.decrementThreadCount('clearOfflinePeer')
def getOnlinePeers(self): def getOnlinePeers(self):
'''Manages the self.onlinePeers attribute list, connects to more peers if we have none connected''' '''
Manages the self.onlinePeers attribute list, connects to more peers if we have none connected
'''
logger.info('Refreshing peer pool.') logger.debug('Refreshing peer pool...')
maxPeers = int(config.get('peers.max_connect', 10)) maxPeers = int(config.get('peers.max_connect', 10))
needed = maxPeers - len(self.onlinePeers) needed = maxPeers - len(self.onlinePeers)
@ -318,11 +322,13 @@ class OnionrCommunicatorDaemon:
break break
else: else:
if len(self.onlinePeers) == 0: if len(self.onlinePeers) == 0:
logger.warn('Could not connect to any peer.') logger.debug('Couldn\'t connect to any peers.')
self.decrementThreadCount('getOnlinePeers') self.decrementThreadCount('getOnlinePeers')
def addBootstrapListToPeerList(self, peerList): def addBootstrapListToPeerList(self, peerList):
'''Add the bootstrap list to the peer list (no duplicates)''' '''
Add the bootstrap list to the peer list (no duplicates)
'''
for i in self._core.bootstrapList: for i in self._core.bootstrapList:
if i not in peerList and i not in self.offlinePeers and i != self._core.hsAddress and len(str(i).strip()) > 0: if i not in peerList and i not in self.offlinePeers and i != self._core.hsAddress and len(str(i).strip()) > 0:
peerList.append(i) peerList.append(i)
@ -440,11 +446,13 @@ class OnionrCommunicatorDaemon:
def heartbeat(self): def heartbeat(self):
'''Show a heartbeat debug message''' '''Show a heartbeat debug message'''
currentTime = self._core._utils.getEpoch() - self.startTime currentTime = self._core._utils.getEpoch() - self.startTime
logger.debug('heartbeat, running seconds: ' + str(currentTime)) logger.debug('Heartbeat. Node online for %s.' % self.daemonTools.humanReadableTime(currentTime))
self.decrementThreadCount('heartbeat') self.decrementThreadCount('heartbeat')
def daemonCommands(self): def daemonCommands(self):
'''process daemon commands from daemonQueue''' '''
Process daemon commands from daemonQueue
'''
cmd = self._core.daemonQueue() cmd = self._core.daemonQueue()
if cmd is not False: if cmd is not False:

View File

@ -73,6 +73,7 @@ LEVEL_INFO = 2
LEVEL_WARN = 3 LEVEL_WARN = 3
LEVEL_ERROR = 4 LEVEL_ERROR = 4
LEVEL_FATAL = 5 LEVEL_FATAL = 5
LEVEL_IMPORTANT = 6
_type = OUTPUT_TO_CONSOLE | USE_ANSI # the default settings for logging _type = OUTPUT_TO_CONSOLE | USE_ANSI # the default settings for logging
_level = LEVEL_DEBUG # the lowest level to log _level = LEVEL_DEBUG # the lowest level to log
@ -201,36 +202,36 @@ def confirm(default = 'y', message = 'Are you sure %s? '):
return default == 'y' return default == 'y'
# debug: when there is info that could be useful for debugging purposes only # debug: when there is info that could be useful for debugging purposes only
def debug(data, error = None, timestamp = True, prompt = True, sensitive = False): def debug(data, error = None, timestamp = True, prompt = True, sensitive = False, level = LEVEL_DEBUG):
if get_level() <= LEVEL_DEBUG: if get_level() <= level:
log('/', data, timestamp=timestamp, prompt = prompt, sensitive = sensitive) log('/', data, timestamp = timestamp, prompt = prompt, sensitive = sensitive)
if not error is None: if not error is None:
debug('Error: ' + str(error) + parse_error()) debug('Error: ' + str(error) + parse_error())
# info: when there is something to notify the user of, such as the success of a process # info: when there is something to notify the user of, such as the success of a process
def info(data, timestamp = False, prompt = True, sensitive = False): def info(data, timestamp = False, prompt = True, sensitive = False, level = LEVEL_INFO):
if get_level() <= LEVEL_INFO: if get_level() <= level:
log('+', data, colors.fg.green, timestamp = timestamp, prompt = prompt, sensitive = sensitive) log('+', data, colors.fg.green, timestamp = timestamp, prompt = prompt, sensitive = sensitive)
# warn: when there is a potential for something bad to happen # warn: when there is a potential for something bad to happen
def warn(data, error = None, timestamp = True, prompt = True, sensitive = False): def warn(data, error = None, timestamp = True, prompt = True, sensitive = False, level = LEVEL_WARN):
if not error is None: if not error is None:
debug('Error: ' + str(error) + parse_error()) debug('Error: ' + str(error) + parse_error())
if get_level() <= LEVEL_WARN: if get_level() <= level:
log('!', data, colors.fg.orange, timestamp = timestamp, prompt = prompt, sensitive = sensitive) log('!', data, colors.fg.orange, timestamp = timestamp, prompt = prompt, sensitive = sensitive)
# error: when only one function, module, or process of the program encountered a problem and must stop # error: when only one function, module, or process of the program encountered a problem and must stop
def error(data, error = None, timestamp = True, prompt = True, sensitive = False): def error(data, error = None, timestamp = True, prompt = True, sensitive = False, level = LEVEL_ERROR):
if get_level() <= LEVEL_ERROR: if get_level() <= level:
log('-', data, colors.fg.red, timestamp = timestamp, fd = sys.stderr, prompt = prompt, sensitive = sensitive) log('-', data, colors.fg.red, timestamp = timestamp, fd = sys.stderr, prompt = prompt, sensitive = sensitive)
if not error is None: if not error is None:
debug('Error: ' + str(error) + parse_error()) debug('Error: ' + str(error) + parse_error())
# fatal: when the something so bad has happened that the program must stop # fatal: when the something so bad has happened that the program must stop
def fatal(data, error = None, timestamp=True, prompt = True, sensitive = False): def fatal(data, error = None, timestamp=True, prompt = True, sensitive = False, level = LEVEL_FATAL):
if not error is None: if not error is None:
debug('Error: ' + str(error) + parse_error(), sensitive = sensitive) debug('Error: ' + str(error) + parse_error(), sensitive = sensitive)
if get_level() <= LEVEL_FATAL: if get_level() <= level:
log('#', data, colors.bg.red + colors.fg.green + colors.bold, timestamp = timestamp, fd = sys.stderr, prompt = prompt, sensitive = sensitive) log('#', data, colors.bg.red + colors.fg.green + colors.bold, timestamp = timestamp, fd = sys.stderr, prompt = prompt, sensitive = sensitive)
# returns a formatted error message # returns a formatted error message

View File

@ -141,7 +141,7 @@ HashedControlPassword ''' + str(password) + '''
logger.fatal('Failed to start Tor. Maybe a stray instance of Tor used by Onionr is still running?') logger.fatal('Failed to start Tor. Maybe a stray instance of Tor used by Onionr is still running?')
return False return False
except KeyboardInterrupt: except KeyboardInterrupt:
logger.fatal("Got keyboard interrupt.") logger.fatal('Got keyboard interrupt.', timestamp = false, level = logger.LEVEL_IMPORTANT)
return False return False
logger.debug('Finished starting Tor.', timestamp=True) logger.debug('Finished starting Tor.', timestamp=True)

View File

@ -65,36 +65,7 @@ class Onionr:
# Load global configuration data # Load global configuration data
data_exists = os.path.exists(self.dataDir) data_exists = Onionr.setupConfig(self.dataDir, self = self)
if not data_exists:
os.mkdir(self.dataDir)
if os.path.exists('static-data/default_config.json'):
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({'dev_mode': True, 'log': {'file': {'output': True, 'path': self.dataDir + '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):
settings = settings | logger.USE_ANSI
if config.get('log.console.output', True):
settings = settings | logger.OUTPUT_TO_CONSOLE
if config.get('log.file.output', True):
settings = settings | logger.OUTPUT_TO_FILE
logger.set_file(config.get('log.file.path', '/tmp/onionr.log').replace('data/', self.dataDir))
logger.set_settings(settings)
if str(config.get('general.dev_mode', True)).lower() == 'true':
self._developmentMode = True
logger.set_level(logger.LEVEL_DEBUG)
else:
self._developmentMode = False
logger.set_level(logger.LEVEL_INFO)
self.onionrCore = core.Core() self.onionrCore = core.Core()
self.onionrUtils = onionrutils.OnionrUtils(self.onionrCore) self.onionrUtils = onionrutils.OnionrUtils(self.onionrCore)
@ -222,14 +193,18 @@ class Onionr:
'help': 'Displays this Onionr help menu', 'help': 'Displays this Onionr help menu',
'version': 'Displays the Onionr version', 'version': 'Displays the Onionr version',
'config': 'Configures something and adds it to the file', 'config': 'Configures something and adds it to the file',
'start': 'Starts the Onionr daemon', 'start': 'Starts the Onionr daemon',
'stop': 'Stops the Onionr daemon', 'stop': 'Stops the Onionr daemon',
'stats': 'Displays node statistics', 'stats': 'Displays node statistics',
'get-password': 'Displays the web password', 'details': 'Displays the web password, public key, and human readable public key',
'enable-plugin': 'Enables and starts a plugin', 'enable-plugin': 'Enables and starts a plugin',
'disable-plugin': 'Disables and stops a plugin', 'disable-plugin': 'Disables and stops a plugin',
'reload-plugin': 'Reloads a plugin', 'reload-plugin': 'Reloads a plugin',
'create-plugin': 'Creates directory structure for a plugin', 'create-plugin': 'Creates directory structure for a plugin',
'add-peer': 'Adds a peer to database', 'add-peer': 'Adds a peer to database',
'list-peers': 'Displays a list of peers', 'list-peers': 'Displays a list of peers',
'add-file': 'Create an Onionr block from a file', 'add-file': 'Create an Onionr block from a file',
@ -357,7 +332,7 @@ class Onionr:
return config.get('client.hmac') return config.get('client.hmac')
def printWebPassword(self): def printWebPassword(self):
print(self.getWebPassword()) logger.info(self.getWebPassword(), sensitive = True)
def getHelp(self): def getHelp(self):
return self.cmdhelp return self.cmdhelp
@ -410,16 +385,16 @@ class Onionr:
THIS SECTION DEFINES THE COMMANDS THIS SECTION DEFINES THE COMMANDS
''' '''
def version(self, verbosity=5): def version(self, verbosity = 5, function = logger.info):
''' '''
Displays the Onionr version Displays the Onionr version
''' '''
logger.info('Onionr %s (%s) - API v%s' % (ONIONR_VERSION, platform.machine(), API_VERSION)) function('Onionr v%s (%s) (API v%s)' % (ONIONR_VERSION, platform.machine(), API_VERSION))
if verbosity >= 1: if verbosity >= 1:
logger.info(ONIONR_TAGLINE) function(ONIONR_TAGLINE)
if verbosity >= 2: if verbosity >= 2:
logger.info('Running on %s %s' % (platform.platform(), platform.release())) function('Running on %s %s' % (platform.platform(), platform.release()))
return return
@ -635,35 +610,47 @@ class Onionr:
logger.debug('Runcheck file found on daemon start, deleting in advance.') logger.debug('Runcheck file found on daemon start, deleting in advance.')
os.remove('data/.runcheck') os.remove('data/.runcheck')
apiThread = Thread(target=api.API, args=(self.debug,API_VERSION)) apiThread = Thread(target = api.API, args = (self.debug, API_VERSION))
apiThread.start() apiThread.start()
try: try:
time.sleep(3) time.sleep(3)
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info('Got keyboard interrupt') logger.debug('Got keyboard interrupt, shutting down...')
time.sleep(1) time.sleep(1)
self.onionrUtils.localCommand('shutdown') self.onionrUtils.localCommand('shutdown')
else: else:
if apiThread.isAlive(): if apiThread.isAlive():
# configure logger and stuff
Onionr.setupConfig('data/', self = self)
if self._developmentMode: if self._developmentMode:
logger.warn('DEVELOPMENT MODE ENABLED (THIS IS LESS SECURE!)', timestamp = False) logger.warn('DEVELOPMENT MODE ENABLED (THIS IS LESS SECURE!)', timestamp = False)
net = NetController(config.get('client.port', 59496)) net = NetController(config.get('client.port', 59496))
logger.info('Tor is starting...') logger.debug('Tor is starting...')
if not net.startTor(): if not net.startTor():
sys.exit(1) sys.exit(1)
logger.info('Started .onion service: ' + logger.colors.underline + net.myID) logger.debug('Started .onion service: %s' % (logger.colors.underline + net.myID))
logger.info('Our Public key: ' + self.onionrCore._crypto.pubKey) logger.debug('Using public key: %s' % (logger.colors.underline + self.onionrCore._crypto.pubKey))
time.sleep(1) time.sleep(1)
#TODO make runable on windows # TODO: make runable on windows
communicatorProc = subprocess.Popen([communicatorDaemon, "run", str(net.socksPort)]) communicatorProc = subprocess.Popen([communicatorDaemon, 'run', str(net.socksPort)])
# Print nice header thing :)
# print nice header thing :)
if config.get('general.display_header', True): if config.get('general.display_header', True):
self.header() self.header()
logger.debug('Started communicator')
# print out debug info
self.version(verbosity = 5, function = logger.debug)
logger.debug('Python version %s' % platform.python_version())
logger.debug('Started communicator.')
events.event('daemon_start', onionr = self) events.event('daemon_start', onionr = self)
try: try:
while True: while True:
time.sleep(5) time.sleep(5)
# Break if communicator process ends, so we don't have left over processes # Break if communicator process ends, so we don't have left over processes
if communicatorProc.poll() is not None: if communicatorProc.poll() is not None:
break break
@ -810,7 +797,7 @@ class Onionr:
except IndexError: except IndexError:
logger.error("Syntax %s %s" % (sys.argv[0], '/path/to/filename <blockhash>')) logger.error("Syntax %s %s" % (sys.argv[0], '/path/to/filename <blockhash>'))
else: else:
print(fileName) logger.info(fileName)
contents = None contents = None
if os.path.exists(fileName): if os.path.exists(fileName):
logger.error("File already exists") logger.error("File already exists")
@ -842,17 +829,83 @@ class Onionr:
else: else:
logger.error('%s add-file <filename>' % sys.argv[0], timestamp = False) logger.error('%s add-file <filename>' % sys.argv[0], timestamp = False)
def setupConfig(dataDir, self = None):
data_exists = os.path.exists(dataDir)
if not data_exists:
os.mkdir(dataDir)
if os.path.exists('static-data/default_config.json'):
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
logger.warn('Default configuration file does not exist, switching to hardcoded fallback configuration!')
config.set_config({'dev_mode': True, 'log': {'file': {'output': True, 'path': dataDir + '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):
settings = settings | logger.USE_ANSI
if config.get('log.console.output', True):
settings = settings | logger.OUTPUT_TO_CONSOLE
if config.get('log.file.output', True):
settings = settings | logger.OUTPUT_TO_FILE
logger.set_file(config.get('log.file.path', '/tmp/onionr.log').replace('data/', dataDir))
logger.set_settings(settings)
if not self is None:
if str(config.get('general.dev_mode', True)).lower() == 'true':
self._developmentMode = True
logger.set_level(logger.LEVEL_DEBUG)
else:
self._developmentMode = False
logger.set_level(logger.LEVEL_INFO)
verbosity = str(config.get('log.verbosity', 'default')).lower().strip()
if not verbosity in ['default', 'null', 'none', 'nil']:
map = {
str(logger.LEVEL_DEBUG) : logger.LEVEL_DEBUG,
'verbose' : logger.LEVEL_DEBUG,
'debug' : logger.LEVEL_DEBUG,
str(logger.LEVEL_INFO) : logger.LEVEL_INFO,
'info' : logger.LEVEL_INFO,
'information' : logger.LEVEL_INFO,
str(logger.LEVEL_WARN) : logger.LEVEL_WARN,
'warn' : logger.LEVEL_WARN,
'warning' : logger.LEVEL_WARN,
'warnings' : logger.LEVEL_WARN,
str(logger.LEVEL_ERROR) : logger.LEVEL_ERROR,
'err' : logger.LEVEL_ERROR,
'error' : logger.LEVEL_ERROR,
'errors' : logger.LEVEL_ERROR,
str(logger.LEVEL_FATAL) : logger.LEVEL_FATAL,
'fatal' : logger.LEVEL_FATAL,
str(logger.LEVEL_IMPORTANT) : logger.LEVEL_IMPORTANT,
'silent' : logger.LEVEL_IMPORTANT,
'quiet' : logger.LEVEL_IMPORTANT,
'important' : logger.LEVEL_IMPORTANT
}
if verbosity in map:
logger.set_level(map[verbosity])
else:
logger.warn('Verbosity level %s is not valid, using default verbosity.' % verbosity)
return data_exists
def openUI(self): def openUI(self):
url = 'http://127.0.0.1:%s/ui/index.html?timingToken=%s' % (config.get('client.port', 59496), self.onionrUtils.getTimeBypassToken()) url = 'http://127.0.0.1:%s/ui/index.html?timingToken=%s' % (config.get('client.port', 59496), self.onionrUtils.getTimeBypassToken())
print('Opening %s ...' % url) logger.info('Opening %s ...' % url)
webbrowser.open(url, new = 1, autoraise = True) webbrowser.open(url, new = 1, autoraise = True)
def header(self, message = logger.colors.fg.pink + logger.colors.bold + 'Onionr' + logger.colors.reset + logger.colors.fg.pink + ' has started.'): 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'): if os.path.exists('static-data/header.txt') and logger.get_level() <= logger.LEVEL_INFO:
with open('static-data/header.txt', 'rb') as file: with open('static-data/header.txt', 'rb') as file:
# only to stdout, not file or log or anything # only to stdout, not file or log or anything
sys.stderr.write(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').replace('B', logger.colors.bold).replace('V', ONIONR_VERSION)) sys.stderr.write(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').replace('B', logger.colors.bold).replace('A', '%s' % API_VERSION).replace('V', ONIONR_VERSION))
logger.info(logger.colors.fg.lightgreen + '-> ' + str(message) + logger.colors.reset + logger.colors.fg.lightgreen + ' <-\n') logger.info(logger.colors.fg.lightgreen + '-> ' + str(message) + logger.colors.reset + logger.colors.fg.lightgreen + ' <-\n')
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -46,4 +46,4 @@ class OnionrChat:
self.communicator.socketClient.sendData(peer, "lol") self.communicator.socketClient.sendData(peer, "lol")
except: except:
pass pass
time.sleep(2) time.sleep(2)

View File

@ -140,3 +140,23 @@ class DaemonTools:
return True return True
return False return False
def humanReadableTime(self, seconds):
build = ''
units = {
'year' : 31557600,
'month' : (31557600 / 12),
'day' : 86400,
'hour' : 3600,
'minute' : 60,
'second' : 1
}
for unit in units:
amnt_unit = int(seconds / units[unit])
if amnt_unit >= 1:
seconds -= amnt_unit * units[unit]
build += '%s %s' % (amnt_unit, unit) + ('s' if amnt_unit != 1 else '') + ' '
return build.strip()

View File

@ -77,7 +77,7 @@ def enable(name, onionr = None, start_event = True):
else: else:
enabled_plugins.append(name) enabled_plugins.append(name)
config.set('plugins.enabled', enabled_plugins, True) config.set('plugins.enabled', enabled_plugins, True)
if start_event is True: if start_event is True:
start(name) start(name)
return True return True
@ -234,7 +234,7 @@ def check():
config.reload() config.reload()
if not config.is_set('plugins'): if not config.is_set('plugins'):
logger.debug('Generating plugin config data...') logger.debug('Generating plugin configuration data...')
config.set('plugins', {'enabled': []}, True) config.set('plugins', {'enabled': []}, True)
if not os.path.exists(os.path.dirname(get_plugins_folder())): if not os.path.exists(os.path.dirname(get_plugins_folder())):

View File

@ -72,7 +72,7 @@ class OnionrSocketServer:
self._core.socketServerResponseData[myPeer] = '' self._core.socketServerResponseData[myPeer] = ''
return retData return retData
def socketStarter(self): def socketStarter(self):
while not self._core.killSockets: while not self._core.killSockets:
try: try:
@ -87,14 +87,14 @@ class OnionrSocketServer:
def detectShutdown(self): def detectShutdown(self):
while not self._core.killSockets: while not self._core.killSockets:
time.sleep(5) time.sleep(5)
logger.info('Killing socket server') logger.debug('Killing socket server...')
self.http_server.stop() self.http_server.stop()
def addSocket(self, peer, reason=''): def addSocket(self, peer, reason=''):
bindPort = 1337 bindPort = 1337
assert len(reason) <= 12 assert len(reason) <= 12
with stem.control.Controller.from_port(port=config.get('tor.controlPort')) as controller: with stem.control.Controller.from_port(port=config.get('tor.controlPort')) as controller:
controller.authenticate(config.get('tor.controlpassword')) controller.authenticate(config.get('tor.controlpassword'))
@ -106,7 +106,7 @@ class OnionrSocketServer:
self._core.insertBlock(str(uuid.uuid4()), header='socket', sign=True, encryptType='asym', asymPeer=peer, meta={'reason': reason, 'address': socket.service_id + '.onion'}) self._core.insertBlock(str(uuid.uuid4()), header='socket', sign=True, encryptType='asym', asymPeer=peer, meta={'reason': reason, 'address': socket.service_id + '.onion'})
self._core.socketReasons[peer] = reason self._core.socketReasons[peer] = reason
return return
class OnionrSocketClient: class OnionrSocketClient:
def __init__(self, coreInst): def __init__(self, coreInst):
self.sockets = {} # pubkey: tor address self.sockets = {} # pubkey: tor address
@ -158,7 +158,7 @@ class OnionrSocketClient:
postData = {'data': data} postData = {'data': data}
self.connPool[peer] = {'date': self._core._utils.getEpoch(), 'data': self._core._utils.doPostRequest('http://' + address + '/dc/', data=postData)} self.connPool[peer] = {'date': self._core._utils.getEpoch(), 'data': self._core._utils.doPostRequest('http://' + address + '/dc/', data=postData)}
time.sleep(2) time.sleep(2)
def getResponse(self, peer): def getResponse(self, peer):
retData = '' retData = ''
try: try:
@ -166,6 +166,6 @@ class OnionrSocketClient:
except KeyError: except KeyError:
pass pass
return return
def sendData(self, peer, data): def sendData(self, peer, data):
self.sendData[peer] = data self.sendData[peer] = data

View File

@ -527,7 +527,7 @@ class OnionrUtils:
while True: while True:
time.sleep(interval) time.sleep(interval)
if not os.path.isfile(runcheck_file): if not os.path.isfile(runcheck_file):
return True return True
elif time.time() - starttime >= timeout: elif time.time() - starttime >= timeout:
@ -622,12 +622,14 @@ class OnionrUtils:
else: else:
return return
headers = {'user-agent': 'PyOnionr'} headers = {'user-agent': 'PyOnionr'}
response_headers = dict()
try: try:
proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)}
r = requests.get(url, headers=headers, proxies=proxies, allow_redirects=False, timeout=(15, 30)) r = requests.get(url, headers=headers, proxies=proxies, allow_redirects=False, timeout=(15, 30))
# Check server is using same API version as us # Check server is using same API version as us
try: try:
if r.headers['api'] != str(API_VERSION): response_headers = r.headers
if r.headers['X-API'] != str(API_VERSION):
raise onionrexceptions.InvalidAPIVersion raise onionrexceptions.InvalidAPIVersion
except KeyError: except KeyError:
raise onionrexceptions.InvalidAPIVersion raise onionrexceptions.InvalidAPIVersion
@ -635,9 +637,12 @@ class OnionrUtils:
except KeyboardInterrupt: except KeyboardInterrupt:
raise KeyboardInterrupt raise KeyboardInterrupt
except ValueError as e: except ValueError as e:
logger.debug('Failed to make request', error = e) logger.debug('Failed to make GET request to %s' % url, error = e, sensitive = True)
except onionrexceptions.InvalidAPIVersion: except onionrexceptions.InvalidAPIVersion:
logger.debug("Node is using different API version :(") if 'X-API' in response_headers:
logger.debug('Using API version %s. Cannot communicate with node\'s API version of %s.' % (API_VERSION, response_headers['X-API']))
else:
logger.debug('Using API version %s. API version was not sent with the request.' % API_VERSION)
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
if not 'ConnectTimeoutError' in str(e) and not 'Request rejected or failed' in str(e): if not 'ConnectTimeoutError' in str(e) and not 'Request rejected or failed' in str(e):
logger.debug('Error: %s' % str(e)) logger.debug('Error: %s' % str(e))
@ -656,12 +661,12 @@ class OnionrUtils:
retData = '' retData = ''
curTime = self.getRoundedEpoch(rounding) curTime = self.getRoundedEpoch(rounding)
self.nistSaltTimestamp = curTime self.nistSaltTimestamp = curTime
data = self.doGetRequest('https://beacon.nist.gov/rest/record/' + str(curTime), port=torPort) 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) dataXML = minidom.parseString(data, forbid_dtd = True, forbid_entities = True, forbid_external = True)
try: try:
retData = dataXML.getElementsByTagName('outputValue')[0].childNodes[0].data retData = dataXML.getElementsByTagName('outputValue')[0].childNodes[0].data
except ValueError: except ValueError:
logger.warn('Could not get NIST beacon value') logger.warn('Failed to get the NIST beacon value.')
else: else:
self.powSalt = retData self.powSalt = retData
return retData return retData

View File

@ -38,13 +38,12 @@ class OnionrCLIUI:
pass pass
def refresh(self): def refresh(self):
for i in range(100): print('\n' * 80 + logger.colors.reset)
print('')
def start(self): def start(self):
'''Main CLI UI interface menu''' '''Main CLI UI interface menu'''
showMenu = True showMenu = True
isOnline = "No" isOnline = 'No'
firstRun = True firstRun = True
choice = '' choice = ''
@ -53,7 +52,7 @@ class OnionrCLIUI:
while showMenu: while showMenu:
if firstRun: if firstRun:
logger.info("please wait while Onionr starts...") logger.info('Please wait while Onionr starts...'')
daemon = subprocess.Popen(["./onionr.py", "start"], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL) daemon = subprocess.Popen(["./onionr.py", "start"], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL)
time.sleep(30) time.sleep(30)
firstRun = False firstRun = False
@ -63,9 +62,8 @@ class OnionrCLIUI:
else: else:
isOnline = "No" isOnline = "No"
print(''' logger.info('''Daemon Running: ''' + isOnline + '''
Daemon Running: ''' + isOnline + '''
1. Flow (Anonymous public chat, use at your own risk) 1. Flow (Anonymous public chat, use at your own risk)
2. Mail (Secure email-like service) 2. Mail (Secure email-like service)
3. File Sharing 3. File Sharing
@ -83,7 +81,7 @@ Daemon Running: ''' + isOnline + '''
elif choice in ("2", "mail"): elif choice in ("2", "mail"):
self.subCommand("mail") self.subCommand("mail")
elif choice in ("3", "file sharing", "file"): elif choice in ("3", "file sharing", "file"):
print("Not supported yet") logger.warn("Not supported yet")
elif choice in ("4", "user settings", "settings"): elif choice in ("4", "user settings", "settings"):
try: try:
self.setName() self.setName()
@ -91,21 +89,21 @@ Daemon Running: ''' + isOnline + '''
pass pass
elif choice in ("5", "daemon"): elif choice in ("5", "daemon"):
if isOnline == "Yes": if isOnline == "Yes":
print("Onionr daemon will shutdown...") logger.info("Onionr daemon will shutdown...")
self.myCore.daemonQueueAdd('shutdown') self.myCore.daemonQueueAdd('shutdown')
try: try:
daemon.kill() daemon.kill()
except UnboundLocalError: except UnboundLocalError:
pass pass
else: else:
print("Starting Daemon...") logger.info("Starting Daemon...")
daemon = subprocess.Popen(["./onionr.py", "start"], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) daemon = subprocess.Popen(["./onionr.py", "start"], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
elif choice in ("6", "quit"): elif choice in ("6", "quit"):
showMenu = False showMenu = False
elif choice == "": elif choice == "":
pass pass
else: else:
print("Invalid choice") logger.error("Invalid choice")
return return
def setName(self): def setName(self):

View File

@ -71,7 +71,7 @@ class PlainEncryption:
plaintext = data plaintext = data
encrypted = self.api.get_core()._crypto.pubKeyEncrypt(plaintext, pubkey, anonymous=True, encodedData=True) encrypted = self.api.get_core()._crypto.pubKeyEncrypt(plaintext, pubkey, anonymous=True, encodedData=True)
encrypted = self.api.get_core()._utils.bytesToStr(encrypted) encrypted = self.api.get_core()._utils.bytesToStr(encrypted)
print('ONIONR ENCRYPTED DATA %s END ENCRYPTED DATA' % (encrypted,)) logger.info('Encrypted Message: \n\nONIONR ENCRYPTED DATA %s END ENCRYPTED DATA' % (encrypted,))
def decrypt(self): def decrypt(self):
plaintext = "" plaintext = ""
data = "" data = ""
@ -89,10 +89,10 @@ class PlainEncryption:
myPub = self.api.get_core()._crypto.pubKey myPub = self.api.get_core()._crypto.pubKey
decrypted = self.api.get_core()._crypto.pubKeyDecrypt(encrypted, privkey=self.api.get_core()._crypto.privKey, anonymous=True, encodedData=True) decrypted = self.api.get_core()._crypto.pubKeyDecrypt(encrypted, privkey=self.api.get_core()._crypto.privKey, anonymous=True, encodedData=True)
if decrypted == False: if decrypted == False:
print("Decryption failed") logger.error("Decryption failed")
else: else:
data = json.loads(decrypted) data = json.loads(decrypted)
print(data['data']) logger.info('Decrypted Message: \n\n%s' % data['data'])
try: try:
logger.info("Signing public key: %s" % (data['signer'],)) logger.info("Signing public key: %s" % (data['signer'],))
assert self.api.get_core()._crypto.edVerify(data['data'], data['signer'], data['sig']) != False assert self.api.get_core()._crypto.edVerify(data['data'], data['signer'], data['sig']) != False
@ -101,7 +101,7 @@ class PlainEncryption:
else: else:
logger.info("Message has good signature.") logger.info("Message has good signature.")
return return
def on_init(api, data = None): def on_init(api, data = None):
''' '''
@ -114,4 +114,4 @@ def on_init(api, data = None):
encrypt = PlainEncryption(pluginapi) encrypt = PlainEncryption(pluginapi)
api.commands.register(['encrypt'], encrypt.encrypt) api.commands.register(['encrypt'], encrypt.encrypt)
api.commands.register(['decrypt'], encrypt.decrypt) api.commands.register(['decrypt'], encrypt.decrypt)
return return

View File

@ -132,10 +132,10 @@ def createRepository(plugins):
contents = {'plugins' : plugins, 'author' : getpass.getuser(), 'compiled-by' : plugin_name} contents = {'plugins' : plugins, 'author' : getpass.getuser(), 'compiled-by' : plugin_name}
block = Block(core = pluginapi.get_core()) block = Block(core = pluginapi.get_core())
block.setType('repository') block.setType('repository')
block.setContent(json.dumps(contents)) block.setContent(json.dumps(contents))
return block.save(True) return block.save(True)
def check(): def check():
@ -217,7 +217,7 @@ def pluginToBlock(plugin, import_block = True):
info = '' info = ''
with open(directory + 'info.json').read() as file: with open(directory + 'info.json').read() as file:
info = json.loads(file.read()) info = json.loads(file.read())
if 'author' in info: if 'author' in info:
author = info['author'] author = info['author']
if 'description' in info: if 'description' in info:
@ -228,10 +228,10 @@ 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} 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}
block = Block(core = pluginapi.get_core()) block = Block(core = pluginapi.get_core())
block.setType('plugin') block.setType('plugin')
block.setContent(json.dumps(metadata)) block.setContent(json.dumps(metadata))
hash = block.save(True) hash = block.save(True)
# hash = pluginapi.get_core().insertBlock(, header = 'plugin', sign = True) # hash = pluginapi.get_core().insertBlock(, header = 'plugin', sign = True)
@ -390,12 +390,12 @@ def commandInstallPlugin():
except Exception as e: except Exception as e:
logger.warn('Failed to lookup plugin in repositories.', timestamp = False) logger.warn('Failed to lookup plugin in repositories.', timestamp = False)
logger.error('asdf', error = e, timestamp = False) logger.error('asdf', error = e, timestamp = False)
return True return True
if pkobh is None: if pkobh is None:
logger.error('No key for this plugin found in keystore or repositories, please specify.', timestamp = False) logger.error('No key for this plugin found in keystore or repositories, please specify.', timestamp = False)
return True return True
valid_hash = pluginapi.get_utils().validateHash(pkobh) valid_hash = pluginapi.get_utils().validateHash(pkobh)
@ -552,49 +552,48 @@ def commandPublishPlugin():
logger.error('Plugin %s does not exist.' % pluginname, timestamp = False) logger.error('Plugin %s does not exist.' % pluginname, timestamp = False)
else: else:
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' <plugin>') logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' <plugin>')
def commandCreateRepository(): def commandCreateRepository():
if len(sys.argv) >= 3: if len(sys.argv) >= 3:
check() check()
plugins = list() plugins = list()
script = sys.argv[0] script = sys.argv[0]
del sys.argv[:2] del sys.argv[:2]
success = True success = True
for pluginname in sys.argv: for pluginname in sys.argv:
distributor = None distributor = None
if ':' in pluginname: if ':' in pluginname:
split = pluginname.split(':') split = pluginname.split(':')
pluginname = split[0] pluginname = split[0]
distributor = split[1] distributor = split[1]
pluginname = sanitize(pluginname) pluginname = sanitize(pluginname)
if distributor is None: if distributor is None:
distributor = getKey(pluginname) distributor = getKey(pluginname)
if distributor is None: if distributor is None:
logger.error('No distributor key was found for the plugin %s.' % pluginname, timestamp = False) logger.error('No distributor key was found for the plugin %s.' % pluginname, timestamp = False)
success = False success = False
plugins.append([pluginname, distributor]) plugins.append([pluginname, distributor])
if not success: if not success:
logger.error('Please correct the above errors, then recreate the repository.') logger.error('Please correct the above errors, then recreate the repository.')
return True return True
blockhash = createRepository(plugins) blockhash = createRepository(plugins)
print(blockhash)
if not blockhash is None: 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)) logger.info('Successfully created repository. Execute the following command to add the repository:\n ' + logger.colors.underline + '%s --add-repository %s' % (script, blockhash))
else: else:
logger.error('Failed to create repository, an unknown error occurred.') logger.error('Failed to create repository, an unknown error occurred.')
else: else:
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [plugins...]') logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [plugins...]')
return True return True
# event listeners # event listeners
def on_init(api, data = None): def on_init(api, data = None):

View File

@ -66,7 +66,7 @@ class OnionrMail:
self.sentboxList = [] self.sentboxList = []
self.sentMessages = {} self.sentMessages = {}
return return
def inbox(self): def inbox(self):
blockCount = 0 blockCount = 0
pmBlockMap = {} pmBlockMap = {}
@ -87,7 +87,7 @@ class OnionrMail:
continue continue
blockCount += 1 blockCount += 1
pmBlockMap[blockCount] = blockHash pmBlockMap[blockCount] = blockHash
block = pmBlocks[blockHash] block = pmBlocks[blockHash]
senderKey = block.signer senderKey = block.signer
try: try:
@ -102,7 +102,7 @@ class OnionrMail:
displayList.append('%s. %s - %s: %s' % (blockCount, blockDate, senderDisplay[:12], blockHash)) displayList.append('%s. %s - %s: %s' % (blockCount, blockDate, senderDisplay[:12], blockHash))
#displayList.reverse() #displayList.reverse()
for i in displayList: for i in displayList:
print(i) logger.info(i)
try: try:
choice = logger.readline('Enter a block number, -r to refresh, or -q to stop: ').strip().lower() choice = logger.readline('Enter a block number, -r to refresh, or -q to stop: ').strip().lower()
except (EOFError, KeyboardInterrupt): except (EOFError, KeyboardInterrupt):
@ -129,16 +129,16 @@ class OnionrMail:
else: else:
cancel = '' cancel = ''
readBlock.verifySig() readBlock.verifySig()
print('Message recieved from %s' % (self.myCore._utils.bytesToStr(readBlock.signer,))) logger.info('Message recieved from %s' % (self.myCore._utils.bytesToStr(readBlock.signer,)))
print('Valid signature:', readBlock.validSig) logger.info('Valid signature: %s' % readBlock.validSig)
if not readBlock.validSig: if not readBlock.validSig:
logger.warn('This message has an INVALID signature. ANYONE could have sent this message.') logger.warn('This message has an INVALID signature. ANYONE could have sent this message.')
cancel = logger.readline('Press enter to continue to message, or -q to not open the message (recommended).') cancel = logger.readline('Press enter to continue to message, or -q to not open the message (recommended).')
if cancel != '-q': if cancel != '-q':
print(draw_border(self.myCore._utils.escapeAnsi(readBlock.bcontent.decode().strip()))) print(draw_border(self.myCore._utils.escapeAnsi(readBlock.bcontent.decode().strip())))
input("Press enter to continue") logger.readline("Press enter to continue")
return return
def sentbox(self): def sentbox(self):
''' '''
Display sent mail messages Display sent mail messages
@ -146,7 +146,7 @@ class OnionrMail:
entering = True entering = True
while entering: while entering:
self.getSentList() self.getSentList()
print('Enter block number or -q to return') logger.info('Enter block number or -q to return')
try: try:
choice = input('>') choice = input('>')
except (EOFError, KeyboardInterrupt) as e: except (EOFError, KeyboardInterrupt) as e:
@ -158,21 +158,21 @@ class OnionrMail:
try: try:
self.sentboxList[int(choice) - 1] self.sentboxList[int(choice) - 1]
except IndexError: except IndexError:
print('Invalid block') logger.warn('Invalid block.')
else: else:
logger.info('Sent to: ' + self.sentMessages[self.sentboxList[int(choice) - 1]][1]) logger.info('Sent to: ' + self.sentMessages[self.sentboxList[int(choice) - 1]][1])
# Print ansi escaped sent message # Print ansi escaped sent message
print(self.myCore._utils.escapeAnsi(self.sentMessages[self.sentboxList[int(choice) - 1]][0])) logger.info(self.myCore._utils.escapeAnsi(self.sentMessages[self.sentboxList[int(choice) - 1]][0]))
input('Press enter to continue...') input('Press enter to continue...')
return return
def getSentList(self): def getSentList(self):
count = 1 count = 1
for i in self.sentboxTools.listSent(): for i in self.sentboxTools.listSent():
self.sentboxList.append(i['hash']) self.sentboxList.append(i['hash'])
self.sentMessages[i['hash']] = (i['message'], i['peer']) self.sentMessages[i['hash']] = (i['message'], i['peer'])
print('%s. %s - %s - %s' % (count, i['hash'], i['peer'][:12], i['date'])) logger.info('%s. %s - %s - %s' % (count, i['hash'], i['peer'][:12], i['date']))
count += 1 count += 1
def draftMessage(self): def draftMessage(self):
@ -198,7 +198,7 @@ class OnionrMail:
# if -q or ctrl-c/d, exit function here, otherwise we successfully got the public key # if -q or ctrl-c/d, exit function here, otherwise we successfully got the public key
return return
print('Enter your message, stop by entering -q on a new line.') logger.info('Enter your message, stop by entering -q on a new line.')
while newLine != '-q': while newLine != '-q':
try: try:
newLine = input() newLine = input()
@ -209,7 +209,7 @@ class OnionrMail:
newLine += '\n' newLine += '\n'
message += newLine message += newLine
print('Inserting encrypted message as Onionr block....') logger.info('Inserting encrypted message as Onionr block....')
blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=True) blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=True)
self.sentboxTools.addToSent(blockID, recip, message) self.sentboxTools.addToSent(blockID, recip, message)
@ -217,7 +217,7 @@ class OnionrMail:
choice = '' choice = ''
while True: while True:
print(self.strings.programTag + '\n\nOur ID: ' + self.myCore._crypto.pubKey + self.strings.mainMenu.title()) # print out main menu logger.info(self.strings.programTag + '\n\nOur ID: ' + self.myCore._crypto.pubKey + self.strings.mainMenu.title()) # print out main menu
try: try:
choice = logger.readline('Enter 1-%s:\n' % (len(self.strings.mainMenuChoices))).lower().strip() choice = logger.readline('Enter 1-%s:\n' % (len(self.strings.mainMenuChoices))).lower().strip()
@ -251,4 +251,4 @@ def on_init(api, data = None):
mail = OnionrMail(pluginapi) mail = OnionrMail(pluginapi)
api.commands.register(['mail'], mail.menu) api.commands.register(['mail'], mail.menu)
api.commands.register_help('mail', 'Interact with OnionrMail') api.commands.register_help('mail', 'Interact with OnionrMail')
return return

View File

@ -2,6 +2,7 @@
"general" : { "general" : {
"dev_mode" : true, "dev_mode" : true,
"display_header" : true, "display_header" : true,
"minimum_block_pow": 5, "minimum_block_pow": 5,
"minimum_send_pow": 5, "minimum_send_pow": 5,
@ -34,7 +35,20 @@
"client" : { "client" : {
}, },
"log": {
"plugins" : {
"enabled" : {
},
"disabled" : {
}
},
"log" : {
"verbosity" : "default",
"file": { "file": {
"output": false, "output": false,
"path": "data/output.log" "path": "data/output.log"

View File

@ -3,9 +3,9 @@ P G'
P G'' P G''
P G'' ' P G'' '
P G'''''' P G''''''
P :G;'''''P: P :G''''''P:
P ::G;'''P:: P ::G''''P::
P :::G;;P::: P :::G''P:::
P :::::::: P ::::::::
P :::::::::::: P ::::::::::::
P ::::::::::::::: P :::::::::::::::
@ -20,6 +20,7 @@ P :::: ::::: ::::: ::: W :::: :: :: :: ::::: :: :: :: ::
P :::: :::::: :::::: :::: P :::: :::::: :::::: ::::
P :::: :::::::::::: :::: GvPBV P :::: :::::::::::: :::: GvPBV
P ::::: :::::::: :::: P ::::: :::::::: ::::
P ::::: :::::: P ::::: :::::
P :::::::::::::::: P ::::::::::::::::
P ::::::: P :::::::