diff --git a/onionr/api.py b/onionr/api.py index df5d2236..4175ecdf 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -20,7 +20,7 @@ import flask from flask import request, Response, abort from multiprocessing import Process -import configparser, sys, random, threading, hmac, hashlib, base64, time, math, os, logger +import sys, random, threading, hmac, hashlib, base64, time, math, os, logger, config from core import Core import onionrutils, onionrcrypto @@ -37,31 +37,32 @@ class API: else: return True - def __init__(self, config, debug): + def __init__(self, debug): ''' Initialize the api server, preping variables for later use This initilization defines all of the API entry points and handlers for the endpoints and errors This also saves the used host (random localhost IP address) to the data folder in host.txt ''' - if os.path.exists('dev-enabled'): + + config.reload() + + if config.get('devmode', True): self._developmentMode = True logger.set_level(logger.LEVEL_DEBUG) - #logger.warn('DEVELOPMENT MODE ENABLED (THIS IS LESS SECURE!)') else: self._developmentMode = False logger.set_level(logger.LEVEL_INFO) - self.config = config self.debug = debug self._privateDelayTime = 3 self._core = Core() self._crypto = onionrcrypto.OnionrCrypto(self._core) self._utils = onionrutils.OnionrUtils(self._core) app = flask.Flask(__name__) - bindPort = int(self.config['CLIENT']['PORT']) + bindPort = int(config.get('CLIENT')['PORT']) self.bindPort = bindPort - self.clientToken = self.config['CLIENT']['CLIENT HMAC'] + self.clientToken = config.get('CLIENT')['CLIENT HMAC'] if not os.environ.get("WERKZEUG_RUN_MAIN") == "true": logger.debug('Your HMAC token: ' + logger.colors.underline + self.clientToken) diff --git a/onionr/config.py b/onionr/config.py new file mode 100644 index 00000000..793d8b9b --- /dev/null +++ b/onionr/config.py @@ -0,0 +1,110 @@ +''' + Onionr - P2P Microblogging Platform & Social network + + This file deals with configuration management. +''' +''' + 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 os, json, logger + +_configfile = os.path.abspath('data/config.json') +_config = {} + +def get(key, default = None): + ''' + Gets the key from configuration, or returns `default` + ''' + if is_set(key): + return get_config()[key] + return default + +def set(key, value = None, savefile = False): + ''' + Sets the key in configuration to `value` + ''' + + global _config + _config[key] = value + + if savefile: + save() + +def is_set(key): + return key in get_config() and not get_config()[key] is None + +def check(): + ''' + Checks if the configuration file exists, creates it if not + ''' + + try: + if not os.path.exists(os.path.dirname(get_config_file())): + os.path.mkdirs(os.path.dirname(get_config_file())) + if not os.path.isfile(get_config_file()): + open(get_config_file(), 'a', encoding="utf8").close() + save() + except: + logger.warn('Failed to check configuration file.') + +def save(): + ''' + Saves the configuration data to the configuration file + ''' + + check() + try: + with open(get_config_file(), 'w', encoding="utf8") as configfile: + json.dump(get_config(), configfile, indent=2, sort_keys=True) + except: + logger.warn('Failed to write to configuration file.') + +def reload(): + ''' + Reloads the configuration data in memory from the file + ''' + + check() + try: + with open(get_config_file(), 'r', encoding="utf8") as configfile: + set_config(json.loads(configfile.read())) + except: + logger.warn('Failed to parse configuration file.') + +def get_config(): + ''' + Gets the entire configuration as an array + ''' + return _config + +def set_config(config): + ''' + Sets the configuration to the array in arguments + ''' + global _config + _config = config + +def get_config_file(): + ''' + Returns the absolute path to the configuration file + ''' + return _configfile + +def set_config_file(configfile): + ''' + Sets the path to the configuration file + ''' + global _configfile + _configfile = os.abs.abspath(configfile) diff --git a/onionr/logger.py b/onionr/logger.py index 96d30ad1..12b61702 100644 --- a/onionr/logger.py +++ b/onionr/logger.py @@ -108,6 +108,21 @@ def get_level(): return _level +def set_file(outputfile): + ''' + Set the file to output to, if enabled + ''' + + global _outputfile + _outputfile = outputfile + +def get_file(): + ''' + Get the file to output to + ''' + + return _outputfile + def raw(data): ''' Outputs raw data to console without formatting diff --git a/onionr/onionr.py b/onionr/onionr.py index 3098fe82..8591322b 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -20,8 +20,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import sys, os, configparser, base64, random, getpass, shutil, subprocess, requests, time, logger, platform -import api, core, gui +import sys, os, base64, random, getpass, shutil, subprocess, requests, time, platform +import api, core, gui, config, logger from onionrutils import OnionrUtils from netcontroller import NetController @@ -39,12 +39,29 @@ class Onionr: Main Onionr class. This is for the CLI program, and does not handle much of the logic. In general, external programs and plugins should not use this class. ''' + try: os.chdir(sys.path[0]) except FileNotFoundError: pass - if os.path.exists('dev-enabled'): + # Load global configuration data + + exists = os.path.exists(config.get_config_file()) + config.set_config({'devmode': True, 'log.file': True, 'log.console': True, 'log.outputfile': 'data/output.log', 'log.color': True}) # this is the default config, it will be overwritten if a config file already exists. Else, it saves it + config.reload() # this will read the configuration file into memory + + settings = 0b000 + if config.get('log.color', True): + settings = settings | logger.USE_ANSI + if config.get('log.console', True): + settings = settings | logger.OUTPUT_TO_CONSOLE + if config.get('log.file', False): + settings = settings | logger.OUTPUT_TO_FILE + logger.set_file(config.get('log.outputfile', 'data/output.log')) + logger.set_settings(settings) + + if config.get('devmode', True): self._developmentMode = True logger.set_level(logger.LEVEL_DEBUG) else: @@ -54,7 +71,7 @@ class Onionr: self.onionrCore = core.Core() self.onionrUtils = OnionrUtils(self.onionrCore) - # Get configuration and Handle commands + # Handle commands self.debug = False # Whole application debugging @@ -79,10 +96,8 @@ class Onionr: self.onionrCore.createAddressDB() # Get configuration - self.config = configparser.ConfigParser() - if os.path.exists('data/config.ini'): - self.config.read('data/config.ini') - else: + + if not exists: # Generate default config # Hostname should only be set if different from 127.x.x.x. Important for DNS rebinding attack prevention. if self.debug: @@ -92,9 +107,7 @@ class Onionr: randomPort = random.randint(1024, 65535) if self.onionrUtils.checkPort(randomPort): break - self.config['CLIENT'] = {'participate': 'true', 'CLIENT HMAC': base64.b64encode(os.urandom(32)).decode('utf-8'), 'PORT': randomPort, 'API VERSION': API_VERSION} - with open('data/config.ini', 'w') as configfile: - self.config.write(configfile) + config.set('CLIENT', {'participate': 'true', 'CLIENT HMAC': base64.b64encode(os.urandom(32)).decode('utf-8'), 'PORT': randomPort, 'API VERSION': API_VERSION}, True) command = '' try: @@ -117,13 +130,14 @@ class Onionr: def getCommands(self): return { + 'help': self.showHelp, + 'version': self.version, + 'config': self.configure, 'start': self.start, 'stop': self.killDaemon, - 'version': self.version, + 'stats': self.showStats, 'listpeers': self.listPeers, 'list-peers': self.listPeers, - 'stats': self.showStats, - 'help': self.showHelp, '': self.showHelpSuggestion, 'addmsg': self.addMessage, 'addmessage': self.addMessage, @@ -139,6 +153,7 @@ class Onionr: return { 'help': 'Displays this Onionr help menu', 'version': 'Displays the Onionr version', + 'config': 'Configures something and adds it to the file', 'start': 'Starts the Onionr daemon', 'stop': 'Stops the Onionr daemon', 'stats': 'Displays node statistics', @@ -149,6 +164,23 @@ class Onionr: 'gui': 'Opens a graphical interface for Onionr' } + def configure(self): + ''' + Displays something from the configuration file, or sets it + ''' + + if len(sys.argv) >= 4: + config.reload() + config.set(sys.argv[2], sys.argv[3], True) + logger.debug('Configuration file updated.') + elif len(sys.argv) >= 3: + config.reload() + logger.info(logger.colors.bold + sys.argv[2] + ': ' + logger.colors.reset + str(config.get(sys.argv[2], logger.colors.fg.red + 'Not set.'))) + else: + logger.info(logger.colors.bold + 'Get a value: ' + logger.colors.reset + sys.argv[0] + ' ' + sys.argv[1] + ' ') + logger.info(logger.colors.bold + 'Set a value: ' + logger.colors.reset + sys.argv[0] + ' ' + sys.argv[1] + ' ') + + def execute(self, argument): ''' Executes a command @@ -271,7 +303,7 @@ class Onionr: if not os.environ.get("WERKZEUG_RUN_MAIN") == "true": if self._developmentMode: logger.warn('DEVELOPMENT MODE ENABLED (THIS IS LESS SECURE!)') - net = NetController(self.config['CLIENT']['PORT']) + net = NetController(config.get('CLIENT')['PORT']) logger.info('Tor is starting...') if not net.startTor(): sys.exit(1) @@ -280,7 +312,7 @@ class Onionr: time.sleep(1) subprocess.Popen(["./communicator.py", "run", str(net.socksPort)]) logger.debug('Started communicator') - api.API(self.config, self.debug) + api.API(self.debug) return @@ -290,7 +322,7 @@ class Onionr: ''' logger.warn('Killing the running daemon') - net = NetController(self.config['CLIENT']['PORT']) + net = NetController(config.get('CLIENT')['PORT']) try: self.onionrUtils.localCommand('shutdown') except requests.exceptions.ConnectionError: diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index b000e799..35ba38e3 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -18,30 +18,34 @@ along with this program. If not, see . ''' # Misc functions that do not fit in the main api, but are useful -import getpass, sys, requests, configparser, os, socket, hashlib, logger, sqlite3 +import getpass, sys, requests, os, socket, hashlib, logger, sqlite3, config import nacl.signing, nacl.encoding + if sys.version_info < (3, 6): try: import sha3 except ModuleNotFoundError: logger.fatal('On Python 3 versions prior to 3.6.x, you need the sha3 module') sys.exit(1) + class OnionrUtils: - '''Various useful functions''' + ''' + Various useful function + ''' def __init__(self, coreInstance): self.fingerprintFile = 'data/own-fingerprint.txt' self._core = coreInstance return + def localCommand(self, command): ''' Send a command to the local http API server, securely. Intended for local clients, DO NOT USE for remote peers. ''' - config = configparser.ConfigParser() - if os.path.exists('data/config.ini'): - config.read('data/config.ini') - else: - return - requests.get('http://' + open('data/host.txt', 'r').read() + ':' + str(config['CLIENT']['PORT']) + '/client/?action=' + command + '&token=' + config['CLIENT']['CLIENT HMAC']) + + config.reload() + + # TODO: URL encode parameters, just as an extra measure. May not be needed, but should be added regardless. + requests.get('http://' + open('data/host.txt', 'r').read() + ':' + str(config.get('CLIENT')['PORT']) + '/client/?action=' + command + '&token=' + config.get('CLIENT')['CLIENT HMAC']) return @@ -141,7 +145,7 @@ class OnionrUtils: retVal = False return retVal - + def validatePubKey(self, key): '''Validate if a string is a valid base32 encoded Ed25519 key''' retVal = False @@ -195,5 +199,5 @@ class OnionrUtils: retVal = False if not idNoDomain.isalnum(): retVal = False - - return retVal \ No newline at end of file + + return retVal