work on adding peers, improved documentation, improved the way the daemon is run, daemon can now be killed from background

This commit is contained in:
Kevin Froman 2018-01-14 02:48:23 -06:00
parent a69e1d2d72
commit 7131902a74
No known key found for this signature in database
GPG Key ID: 0D414D0FE405B63B
5 changed files with 107 additions and 18 deletions

35
api.py
View File

@ -1,5 +1,9 @@
''' '''
Onionr - P2P Microblogging Platform & Social network 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 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 it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
@ -15,20 +19,27 @@
''' '''
import flask import flask
from flask import request, Response, abort from flask import request, Response, abort
from multiprocessing import Process
import configparser, sys, random, threading, hmac, hashlib, base64, time, math, gnupg, os import configparser, sys, random, threading, hmac, hashlib, base64, time, math, gnupg, os
from core import Core from core import Core
'''
Main API
'''
class API: class API:
''' Main http api (flask)'''
def validateToken(self, token): def validateToken(self, token):
'''
Validate if the client token (hmac) matches the given token
'''
if self.clientToken != token: if self.clientToken != token:
return False return False
else: else:
return True return True
def __init__(self, config, debug): 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'): if os.path.exists('dev-enabled'):
print('DEVELOPMENT MODE ENABLED (THIS IS LESS SECURE!)') print('DEVELOPMENT MODE ENABLED (THIS IS LESS SECURE!)')
self._developmentMode = True self._developmentMode = True
@ -44,14 +55,20 @@ class API:
self.clientToken = self.config['CLIENT']['CLIENT HMAC'] self.clientToken = self.config['CLIENT']['CLIENT HMAC']
print(self.clientToken) 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)] 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]) self.host = '127.' + str(hostNums[0]) + '.' + str(hostNums[1]) + '.' + str(hostNums[2])
else: else:
self.host = '127.0.0.1' self.host = '127.0.0.1'
hostFile = open('data/host.txt', 'w')
hostFile.write(self.host)
hostFile.close()
@app.before_request @app.before_request
def beforeReq(): def beforeReq():
'''
Simply define the request as not having yet failed, before every request.
'''
self.requestFailed = False self.requestFailed = False
return return
@ -78,6 +95,9 @@ class API:
self.validateHost('private') self.validateHost('private')
if action == 'hello': if action == 'hello':
resp = Response('Hello, World! ' + request.host) resp = Response('Hello, World! ' + request.host)
elif action == 'shutdown':
request.environ.get('werkzeug.server.shutdown')()
resp = Response('Goodbye')
elif action == 'stats': elif action == 'stats':
resp = Response('something') resp = Response('something')
elif action == 'init': elif action == 'init':
@ -100,7 +120,6 @@ class API:
requestingPeer = request.args.get('myID') requestingPeer = request.args.get('myID')
if action == 'firstConnect': if action == 'firstConnect':
pass pass
@app.errorhandler(404) @app.errorhandler(404)
@ -121,6 +140,12 @@ class API:
app.run(host=self.host, port=bindPort, debug=True, threaded=True) app.run(host=self.host, port=bindPort, debug=True, threaded=True)
def validateHost(self, hostType): 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: if self.debug:
return return
# Validate host header, to protect against DNS rebinding attacks # Validate host header, to protect against DNS rebinding attacks

View File

@ -28,6 +28,14 @@ class OnionrCommunicate:
This class handles communication with nodes in the Onionr network. This class handles communication with nodes in the Onionr network.
''' '''
self._core = core.Core() 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 return
def getRemotePeerKey(self, peerID): def getRemotePeerKey(self, peerID):
'''This function contacts a peer and gets their main PGP key. '''This function contacts a peer and gets their main PGP key.

26
core.py
View File

@ -1,5 +1,9 @@
''' '''
Onionr - P2P Microblogging Platform & Social network 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 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 it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
@ -20,6 +24,9 @@ from Crypto import Random
class Core: class Core:
def __init__(self): def __init__(self):
'''
Initialize Core Onionr library
'''
self.queueDB = 'data/queue.db' self.queueDB = 'data/queue.db'
self.peerDB = 'data/peers.db' self.peerDB = 'data/peers.db'
@ -27,6 +34,8 @@ class Core:
return return
def generateMainPGP(self): 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 # Generate main pgp key
gpg = gnupg.GPG(gnupghome='data/pgp/') 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') 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 return
def addPeer(self, peerID, name=''): 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 # This function simply adds a peer to the DB
conn = sqlite3.connect(self.peerDB) conn = sqlite3.connect(self.peerDB)
c = conn.cursor() c = conn.cursor()
@ -44,6 +55,9 @@ class Core:
return True return True
def createPeerDB(self): def createPeerDB(self):
'''
Generate the peer sqlite3 database and populate it with the peers table.
'''
# generate the peer database # generate the peer database
conn = sqlite3.connect(self.peerDB) conn = sqlite3.connect(self.peerDB)
c = conn.cursor() c = conn.cursor()
@ -61,6 +75,9 @@ class Core:
conn.close() conn.close()
def dataDirEncrypt(self, password): def dataDirEncrypt(self, password):
'''
Encrypt the data directory on Onionr shutdown
'''
# Encrypt data directory (don't delete it in this function) # Encrypt data directory (don't delete it in this function)
if os.path.exists('data.tar'): if os.path.exists('data.tar'):
os.remove('data.tar') os.remove('data.tar')
@ -74,6 +91,9 @@ class Core:
os.remove('data.tar') os.remove('data.tar')
return return
def dataDirDecrypt(self, password): def dataDirDecrypt(self, password):
'''
Decrypt the data directory on startup
'''
# Decrypt data directory # Decrypt data directory
if not os.path.exists('data-encrypted.dat'): if not os.path.exists('data-encrypted.dat'):
return (False, 'encrypted archive does not exist') return (False, 'encrypted archive does not exist')
@ -89,6 +109,9 @@ class Core:
tar.close() tar.close()
return (True, '') return (True, '')
def daemonQueue(self): def daemonQueue(self):
'''
Gives commands to the communication proccess/daemon by reading an sqlite3 database
'''
# This function intended to be used by the client # This function intended to be used by the client
# Queue to exchange data between "client" and server. # Queue to exchange data between "client" and server.
retData = False retData = False
@ -113,6 +136,9 @@ class Core:
return retData return retData
def daemonQueueAdd(self, command, data=''): 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 # Intended to be used by the web server
date = math.floor(time.time()) date = math.floor(time.time())
conn = sqlite3.connect(self.queueDB) conn = sqlite3.connect(self.queueDB)

View File

@ -20,7 +20,7 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
''' '''
import sys, os, configparser, base64, random, getpass, shutil, subprocess import sys, os, configparser, base64, random, getpass, shutil, subprocess, requests
import gui, api, colors, core import gui, api, colors, core
from onionrutils import OnionrUtils from onionrutils import OnionrUtils
from colors import Colors from colors import Colors
@ -31,7 +31,6 @@ class Onionr:
In general, external programs and plugins should not use this class. In general, external programs and plugins should not use this class.
''' '''
self.runningDaemon = False
if os.path.exists('dev-enabled'): if os.path.exists('dev-enabled'):
print('DEVELOPMENT MODE ENABLED (THIS IS LESS SECURE!)') print('DEVELOPMENT MODE ENABLED (THIS IS LESS SECURE!)')
self._developmentMode = True self._developmentMode = True
@ -40,8 +39,8 @@ class Onionr:
colors = Colors() colors = Colors()
onionrCore = core.Core() self.onionrCore = core.Core()
onionrUtils = OnionrUtils() self.onionrUtils = OnionrUtils()
# Get configuration and Handle commands # Get configuration and Handle commands
@ -55,7 +54,7 @@ class Onionr:
while True: while True:
print('Enter password to decrypt:') print('Enter password to decrypt:')
password = getpass.getpass() password = getpass.getpass()
result = onionrCore.dataDirDecrypt(password) result = self.onionrCore.dataDirDecrypt(password)
if os.path.exists('data/'): if os.path.exists('data/'):
break break
else: else:
@ -65,7 +64,7 @@ class Onionr:
os.mkdir('data/') os.mkdir('data/')
if not os.path.exists('data/peers.db'): if not os.path.exists('data/peers.db'):
onionrCore.createPeerDB() self.onionrCore.createPeerDB()
pass pass
# Get configuration # Get configuration
@ -89,7 +88,16 @@ class Onionr:
command = '' command = ''
finally: finally:
if command == 'start': 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': elif command == 'stop':
self.killDaemon() self.killDaemon()
elif command == 'stats': elif command == 'stats':
@ -102,8 +110,8 @@ class Onionr:
print(colors.RED, 'Invalid Command', colors.RESET) print(colors.RED, 'Invalid Command', colors.RESET)
if not self._developmentMode: if not self._developmentMode:
encryptionPassword = onionrUtils.getPassword('Enter password to encrypt directory.') encryptionPassword = self.onionrUtils.getPassword('Enter password to encrypt directory.')
onionrCore.dataDirEncrypt(encryptionPassword) self.onionrCore.dataDirEncrypt(encryptionPassword)
shutil.rmtree('data/') shutil.rmtree('data/')
return return
def daemon(self): def daemon(self):
@ -115,13 +123,20 @@ class Onionr:
api.API(self.config, self.debug) api.API(self.config, self.debug)
return return
def killDaemon(self): def killDaemon(self):
if self.runningDaemon == False: '''Shutdown the Onionr Daemon'''
onionrUtils.printErr('No known daemon is running') print('Killing the running daemon')
sys.exit(1) try:
self.onionrUtils.localCommand('shutdown')
except requests.exceptions.ConnectionError:
pass
else:
self.onionrCore.daemonQueueAdd('shutdown')
return return
def showStats(self): def showStats(self):
'''Display statistics and exit'''
return return
def showHelp(self): def showHelp(self):
'''Show help for Onionr'''
return return
Onionr() Onionr()

View File

@ -1,5 +1,9 @@
''' '''
Onionr - P2P Microblogging Platform & Social network 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 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 it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
@ -14,13 +18,24 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
''' '''
# Misc functions that do not fit in the main api, but are useful # Misc functions that do not fit in the main api, but are useful
import getpass, sys import getpass, sys, requests, configparser, os
class OnionrUtils(): class OnionrUtils():
'''Various useful functions'''
def __init__(self): def __init__(self):
return return
def printErr(self, text='an error occured'): def printErr(self, text='an error occured'):
'''Print an error message to stderr with a new line'''
sys.stderr.write(text + '\n') 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: '): 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 # Get a password safely with confirmation and return it
while True: while True:
print(message) print(message)