diff --git a/api.py b/api.py index fa2b2fa3..ba3c05b5 100755 --- a/api.py +++ b/api.py @@ -1,5 +1,9 @@ ''' Onionr - P2P Microblogging Platform & Social network + + This file handles all incoming http requests to the client, using Flask +''' +''' 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 @@ -15,20 +19,27 @@ ''' import flask from flask import request, Response, abort +from multiprocessing import Process import configparser, sys, random, threading, hmac, hashlib, base64, time, math, gnupg, os from core import Core -''' -Main API -''' class API: + ''' Main http api (flask)''' def validateToken(self, token): + ''' + Validate if the client token (hmac) matches the given token + ''' if self.clientToken != token: return False else: return True def __init__(self, config, 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'): print('DEVELOPMENT MODE ENABLED (THIS IS LESS SECURE!)') self._developmentMode = True @@ -44,14 +55,20 @@ class API: self.clientToken = self.config['CLIENT']['CLIENT HMAC'] print(self.clientToken) - if not debug: + 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]) else: self.host = '127.0.0.1' + hostFile = open('data/host.txt', 'w') + hostFile.write(self.host) + hostFile.close() @app.before_request def beforeReq(): + ''' + Simply define the request as not having yet failed, before every request. + ''' self.requestFailed = False return @@ -78,6 +95,9 @@ class API: self.validateHost('private') if action == 'hello': resp = Response('Hello, World! ' + request.host) + elif action == 'shutdown': + request.environ.get('werkzeug.server.shutdown')() + resp = Response('Goodbye') elif action == 'stats': resp = Response('something') elif action == 'init': @@ -100,7 +120,6 @@ class API: requestingPeer = request.args.get('myID') if action == 'firstConnect': - pass @app.errorhandler(404) @@ -121,6 +140,12 @@ class API: app.run(host=self.host, port=bindPort, debug=True, threaded=True) def validateHost(self, hostType): + ''' Validate various features of the request including: + If private (/client/), is the host header local? + If public (/public/), is the host header onion or i2p? + + Was x-request-with used? + ''' if self.debug: return # Validate host header, to protect against DNS rebinding attacks diff --git a/communicator.py b/communicator.py index 0b9e582a..13178458 100755 --- a/communicator.py +++ b/communicator.py @@ -28,6 +28,14 @@ class OnionrCommunicate: This class handles communication with nodes in the Onionr network. ''' self._core = core.Core() + while True: + command = self._core.daemonQueue() + print('Daemon heartbeat') + if command != False: + if command[0] == 'shutdown': + print('Daemon recieved exit command.') + break + time.sleep(1) return def getRemotePeerKey(self, peerID): '''This function contacts a peer and gets their main PGP key. diff --git a/core.py b/core.py index ad37b877..82cfb6d5 100644 --- a/core.py +++ b/core.py @@ -1,5 +1,9 @@ ''' Onionr - P2P Microblogging Platform & Social network + + Core Onionr library, useful for external programs. Handles peer processing and cryptography. +''' +''' 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 @@ -20,6 +24,9 @@ from Crypto import Random class Core: def __init__(self): + ''' + Initialize Core Onionr library + ''' self.queueDB = 'data/queue.db' self.peerDB = 'data/peers.db' @@ -27,6 +34,8 @@ class Core: return def generateMainPGP(self): + ''' Generate the main PGP key for our client. Should not be done often. + Uses own PGP home folder in the data/ directory. ''' # Generate main pgp key gpg = gnupg.GPG(gnupghome='data/pgp/') input_data = gpg.gen_key_input(key_type="RSA", key_length=2048, name_real='anon', name_comment='Onionr key', name_email='anon@onionr') @@ -34,6 +43,8 @@ class Core: return def addPeer(self, peerID, name=''): + ''' Add a peer by their ID, with an optional name, to the peer database.''' + ''' DOES NO SAFETY CHECKS if the ID is valid, but prepares the insertion. ''' # This function simply adds a peer to the DB conn = sqlite3.connect(self.peerDB) c = conn.cursor() @@ -44,6 +55,9 @@ class Core: return True def createPeerDB(self): + ''' + Generate the peer sqlite3 database and populate it with the peers table. + ''' # generate the peer database conn = sqlite3.connect(self.peerDB) c = conn.cursor() @@ -61,6 +75,9 @@ class Core: conn.close() def dataDirEncrypt(self, password): + ''' + Encrypt the data directory on Onionr shutdown + ''' # Encrypt data directory (don't delete it in this function) if os.path.exists('data.tar'): os.remove('data.tar') @@ -74,6 +91,9 @@ class Core: os.remove('data.tar') return def dataDirDecrypt(self, password): + ''' + Decrypt the data directory on startup + ''' # Decrypt data directory if not os.path.exists('data-encrypted.dat'): return (False, 'encrypted archive does not exist') @@ -89,6 +109,9 @@ class Core: tar.close() return (True, '') def daemonQueue(self): + ''' + Gives commands to the communication proccess/daemon by reading an sqlite3 database + ''' # This function intended to be used by the client # Queue to exchange data between "client" and server. retData = False @@ -113,6 +136,9 @@ class Core: return retData def daemonQueueAdd(self, command, data=''): + ''' + Add a command to the daemon queue, used by the communication daemon (communicator.py) + ''' # Intended to be used by the web server date = math.floor(time.time()) conn = sqlite3.connect(self.queueDB) diff --git a/onionr.py b/onionr.py index c9201186..9e92b302 100755 --- a/onionr.py +++ b/onionr.py @@ -20,7 +20,7 @@ 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 +import sys, os, configparser, base64, random, getpass, shutil, subprocess, requests import gui, api, colors, core from onionrutils import OnionrUtils from colors import Colors @@ -31,7 +31,6 @@ class Onionr: In general, external programs and plugins should not use this class. ''' - self.runningDaemon = False if os.path.exists('dev-enabled'): print('DEVELOPMENT MODE ENABLED (THIS IS LESS SECURE!)') self._developmentMode = True @@ -40,8 +39,8 @@ class Onionr: colors = Colors() - onionrCore = core.Core() - onionrUtils = OnionrUtils() + self.onionrCore = core.Core() + self.onionrUtils = OnionrUtils() # Get configuration and Handle commands @@ -55,7 +54,7 @@ class Onionr: while True: print('Enter password to decrypt:') password = getpass.getpass() - result = onionrCore.dataDirDecrypt(password) + result = self.onionrCore.dataDirDecrypt(password) if os.path.exists('data/'): break else: @@ -65,7 +64,7 @@ class Onionr: os.mkdir('data/') if not os.path.exists('data/peers.db'): - onionrCore.createPeerDB() + self.onionrCore.createPeerDB() pass # Get configuration @@ -89,7 +88,16 @@ class Onionr: command = '' finally: if command == 'start': - self.daemon() + if os.path.exists('.onionr-lock'): + self.onionrUtils.printErr('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: + lockFile = open('.onionr-lock', 'w') + lockFile.write('') + lockFile.close() + self.daemon() + if not self.debug and not self._developmentMode: + os.remove('.onionr-lock') elif command == 'stop': self.killDaemon() elif command == 'stats': @@ -102,8 +110,8 @@ class Onionr: print(colors.RED, 'Invalid Command', colors.RESET) if not self._developmentMode: - encryptionPassword = onionrUtils.getPassword('Enter password to encrypt directory.') - onionrCore.dataDirEncrypt(encryptionPassword) + encryptionPassword = self.onionrUtils.getPassword('Enter password to encrypt directory.') + self.onionrCore.dataDirEncrypt(encryptionPassword) shutil.rmtree('data/') return def daemon(self): @@ -115,13 +123,20 @@ class Onionr: api.API(self.config, self.debug) return def killDaemon(self): - if self.runningDaemon == False: - onionrUtils.printErr('No known daemon is running') - sys.exit(1) + '''Shutdown the Onionr Daemon''' + print('Killing the running daemon') + try: + self.onionrUtils.localCommand('shutdown') + except requests.exceptions.ConnectionError: + pass + else: + self.onionrCore.daemonQueueAdd('shutdown') return def showStats(self): + '''Display statistics and exit''' return def showHelp(self): + '''Show help for Onionr''' return Onionr() \ No newline at end of file diff --git a/onionrutils.py b/onionrutils.py index 49e98e1c..57c7e2d3 100644 --- a/onionrutils.py +++ b/onionrutils.py @@ -1,5 +1,9 @@ ''' Onionr - P2P Microblogging Platform & Social network + + OnionrUtils offers various useful functions to Onionr. Relatively misc. +''' +''' 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 @@ -14,13 +18,24 @@ along with this program. If not, see . ''' # Misc functions that do not fit in the main api, but are useful -import getpass, sys +import getpass, sys, requests, configparser, os class OnionrUtils(): + '''Various useful functions''' def __init__(self): return def printErr(self, text='an error occured'): + '''Print an error message to stderr with a new line''' sys.stderr.write(text + '\n') + 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']) def getPassword(self, message='Enter password: '): + '''Get a password without showing the users typing and confirm the input''' # Get a password safely with confirmation and return it while True: print(message)