improved project structuring

This commit is contained in:
Kevin Froman 2018-01-14 22:57:23 -06:00
parent a89435cfd3
commit 813e98a801
No known key found for this signature in database
GPG key ID: 0D414D0FE405B63B
20 changed files with 64 additions and 14 deletions

1
onionr/__init__.py Normal file
View file

@ -0,0 +1 @@
#

165
onionr/api.py Executable file
View file

@ -0,0 +1,165 @@
'''
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
(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 <https://www.gnu.org/licenses/>.
'''
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
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
else:
self._developmentMode = False
self.config = config
self.debug = debug
self._privateDelayTime = 3
self._core = Core()
app = flask.Flask(__name__)
bindPort = int(self.config['CLIENT']['PORT'])
self.bindPort = bindPort
self.clientToken = self.config['CLIENT']['CLIENT HMAC']
print(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])
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
@app.after_request
def afterReq(resp):
if not self.requestFailed:
resp.headers['Access-Control-Allow-Origin'] = '*'
else:
resp.headers['server'] = 'Onionr'
resp.headers['content-type'] = 'text/plain'
resp.headers["Content-Security-Policy"] = "default-src 'none'"
resp.headers['x-frame-options'] = 'deny'
return resp
@app.route('/client/')
def private_handler():
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')
if not self.validateToken(token):
abort(403)
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':
# generate PGP key
self._core.generateMainPGP()
pass
else:
resp = Response('(O_o) Dude what? (invalid command)')
endTime = math.floor(time.time())
elapsed = endTime - startTime
if elapsed < self._privateDelayTime:
time.sleep(self._privateDelayTime - elapsed)
return resp
@app.route('/public/')
def public_handler():
# Public means it is publicly network accessible
self.validateHost('public')
action = request.args.get('action')
requestingPeer = request.args.get('myID')
if action == 'firstConnect':
pass
@app.errorhandler(404)
def notfound(err):
self.requestFailed = True
resp = Response("")
#resp.headers = getHeaders(resp)
return resp
@app.errorhandler(403)
def authFail(err):
self.requestFailed = True
resp = Response("403")
return resp
print('Starting client on ' + self.host + ':' + str(bindPort))
print('Client token:', self.clientToken)
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
host = self.host
if hostType == 'private':
if not request.host.startswith('127'):
abort(403)
elif hostType == 'public':
if not request.host.endswith('onion') and not request.hosst.endswith('i2p'):
abort(403)
# Validate x-requested-with, to protect against CSRF/metadata leaks
if not self._developmentMode:
try:
request.headers['x-requested-with']
except:
# we exit rather than abort to avoid fingerprinting
sys.exit(1)

23
onionr/colors.py Normal file
View file

@ -0,0 +1,23 @@
'''
Simply define terminal control codes (mainly colors)
'''
class Colors:
def __init__(self):
'''
PURPLE='\033[95m'
BLUE='\033[94m'
GREEN='\033[92m'
YELLOW='\033[93m'
RED='\033[91m'
BOLD='\033[1m'
UNDERLINE='\033[4m'
RESET="\x1B[m"
'''
self.PURPLE='\033[95m'
self.BLUE='\033[94m'
self.GREEN='\033[92m'
self.YELLOW='\033[93m'
self.RED='\033[91m'
self.BOLD='\033[1m'
self.UNDERLINE='\033[4m'
self.RESET="\x1B[m"

60
onionr/communicator.py Executable file
View file

@ -0,0 +1,60 @@
#!/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 <https://www.gnu.org/licenses/>.
'''
import sqlite3, requests, hmac, hashlib, time, sys, os
import core
class OnionrCommunicate:
def __init__(self):
''' 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.
This is safe because Tor or I2P is used, but it does not ensure that the person is who they say they are
'''
url = 'http://' + peerID + '/public/?action=getPGP'
r = requests.get(url, headers=headers)
response = r.text
return response
shouldRun = False
debug = False
developmentMode = False
if os.path.exists('dev-enabled'):
developmentMode = True
try:
if sys.argv[1] == 'run':
shouldRun = True
except IndexError:
pass
if shouldRun:
OnionrCommunicate(debug, developmentMode)

150
onionr/core.py Normal file
View file

@ -0,0 +1,150 @@
'''
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
(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 <https://www.gnu.org/licenses/>.
'''
import sqlite3, os, time, math, gnupg, base64, tarfile, getpass, simplecrypt
from Crypto.Cipher import AES
from Crypto import Random
class Core:
def __init__(self):
'''
Initialize Core Onionr library
'''
self.queueDB = 'data/queue.db'
self.peerDB = 'data/peers.db'
#self.daemonQueue() # Call to create the DB if it doesn't exist
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')
key = gpg.gen_key(input_data)
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()
t = (peerID, name, 'unknown')
c.execute('Insert into users (id, name, dateSeen) values(?, ?, ?);', t)
conn.commit()
conn.close()
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()
c.execute('''
create table users(
ID text not null,
name text,
pgpKey text,
hmacKey text,
forwardKey text,
dateSeen not null,
trust int);
''')
conn.commit()
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')
tar = tarfile.open("data.tar", "w")
for name in ['data']:
tar.add(name)
tar.close()
tarData = open('data.tar', 'r', encoding = "ISO-8859-1").read()
encrypted = simplecrypt.encrypt(password, tarData)
open('data-encrypted.dat', 'wb').write(encrypted)
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')
data = open('data-encrypted.dat', 'rb').read()
try:
decrypted = simplecrypt.decrypt(password, data)
except simplecrypt.DecryptionException:
return (False, 'wrong password (or corrupted archive)')
else:
open('data.tar', 'wb').write(decrypted)
tar = tarfile.open('data.tar')
tar.extractall()
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
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()
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],))
conn.commit()
conn.close()
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)
c = conn.cursor()
t = (command, data, date)
c.execute('INSERT into commands (command, data, date) values (?, ?, ?)', t)
conn.commit()
conn.close()
return

16
onionr/gui.py Executable file
View file

@ -0,0 +1,16 @@
#!/usr/bin/python
'''
Onionr - P2P Microblogging Platform & Social network
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 <https://www.gnu.org/licenses/>.
'''

142
onionr/onionr.py Executable file
View file

@ -0,0 +1,142 @@
#!/usr/bin/env python3
'''
Onionr - P2P Microblogging Platform & Social network.
Onionr is the name for both the protocol and the original/reference software.
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 <https://www.gnu.org/licenses/>.
'''
import sys, os, configparser, base64, random, getpass, shutil, subprocess, requests
import gui, api, colors, core
from onionrutils import OnionrUtils
from colors import Colors
class Onionr:
def __init__(self):
'''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.
'''
if os.path.exists('dev-enabled'):
print('DEVELOPMENT MODE ENABLED (THIS IS LESS SECURE!)')
self._developmentMode = True
else:
self._developmentMode = False
colors = Colors()
self.onionrCore = core.Core()
self.onionrUtils = OnionrUtils()
# Get configuration and Handle commands
self.debug = False # Whole application debugging
try:
os.chdir(sys.path[0])
except FileNotFoundError:
pass
if os.path.exists('data-encrypted.dat'):
while True:
print('Enter password to decrypt:')
password = getpass.getpass()
result = self.onionrCore.dataDirDecrypt(password)
if os.path.exists('data/'):
break
else:
print('Failed to decrypt: ' + result[1])
else:
if not os.path.exists('data/'):
os.mkdir('data/')
if not os.path.exists('data/peers.db'):
self.onionrCore.createPeerDB()
pass
# Get configuration
self.config = configparser.ConfigParser()
if os.path.exists('data/config.ini'):
self.config.read('data/config.ini')
else:
# Generate default config
# Hostname should only be set if different from 127.x.x.x. Important for DNS rebinding attack prevention.
if self.debug:
randomPort = 8080
else:
randomPort = random.randint(1024, 65535)
self.config['CLIENT'] = {'CLIENT HMAC': base64.b64encode(os.urandom(32)).decode('utf-8'), 'PORT': randomPort, 'API VERSION': '0.0.0'}
with open('data/config.ini', 'w') as configfile:
self.config.write(configfile)
command = ''
try:
command = sys.argv[1].lower()
except IndexError:
command = ''
finally:
if command == 'start':
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':
self.showStats()
elif command == 'help' or command == '--help':
self.showHelp()
elif command == '':
print('Do', sys.argv[0], ' --help for Onionr help.')
else:
print(colors.RED, 'Invalid Command', colors.RESET)
if not self._developmentMode:
encryptionPassword = self.onionrUtils.getPassword('Enter password to encrypt directory.')
self.onionrCore.dataDirEncrypt(encryptionPassword)
shutil.rmtree('data/')
return
def daemon(self):
''' Start the Onionr communication daemon
'''
if not os.environ.get("WERKZEUG_RUN_MAIN") == "true":
subprocess.Popen(["./communicator.py", "run"])
print('Started communicator')
api.API(self.config, self.debug)
return
def killDaemon(self):
'''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()

50
onionr/onionrutils.py Normal file
View file

@ -0,0 +1,50 @@
'''
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
(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 <https://www.gnu.org/licenses/>.
'''
# Misc functions that do not fit in the main api, but are useful
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)
pass1 = getpass.getpass()
print('Confirm password: ')
pass2 = getpass.getpass()
if pass1 != pass2:
print("Passwords do not match.")
input()
else:
break
return pass1

113
onionr/tests.py Executable file
View file

@ -0,0 +1,113 @@
#!/usr/bin/env python3
'''
Onionr - P2P Microblogging Platform & Social network
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 <https://www.gnu.org/licenses/>.
'''
import unittest, sys, os, base64, tarfile, shutil, simplecrypt
class OnionrTests(unittest.TestCase):
def testPython3(self):
if sys.version_info.major != 3:
print(sys.version_info.major)
self.assertTrue(False)
else:
self.assertTrue(True)
def testNone(self):
print('--------------------------')
print('Running simple program run test')
# Test just running ./onionr with no arguments
blank = os.system('./onionr.py')
if blank != 0:
self.assertTrue(False)
else:
self.assertTrue(True)
def testPeer_a_DBCreation(self):
print('--------------------------')
print('Running peer db creation test')
if os.path.exists('data/peers.db'):
os.remove('data/peers.db')
import core
myCore = core.Core()
myCore.createPeerDB()
if os.path.exists('data/peers.db'):
self.assertTrue(True)
else:
self.assertTrue(False)
def testPeer_b_addPeerToDB(self):
print('--------------------------')
print('Running peer db insertion test')
import core
myCore = core.Core()
if not os.path.exists('data/peers.db'):
myCore.createPeerDB()
if myCore.addPeer('test'):
self.assertTrue(True)
else:
self.assertTrue(False)
def testData_b_Encrypt(self):
self.assertTrue(True)
return
print('--------------------------')
print('Running data dir encrypt test')
import core
myCore = core.Core()
myCore.dataDirEncrypt('password')
if os.path.exists('data-encrypted.dat'):
self.assertTrue(True)
else:
self.assertTrue(False)
def testData_a_Decrypt(self):
self.assertTrue(True)
return
print('--------------------------')
print('Running data dir decrypt test')
import core
myCore = core.Core()
myCore.dataDirDecrypt('password')
if os.path.exists('data/'):
self.assertTrue(True)
else:
self.assertTrue(False)
def testPGPGen(self):
print('--------------------------')
print('Testing PGP key generation')
if os.path.exists('data/pgp/'):
self.assertTrue(True)
else:
import core
myCore = core.Core()
myCore.generateMainPGP()
if os.path.exists('data/pgp/'):
self.assertTrue(True)
def testQueue(self):
print('--------------------------')
print('running daemon queue test')
# test if the daemon queue can read/write data
import core
myCore = core.Core()
if not os.path.exists('data/queue.db'):
myCore.daemonQueue()
while True:
command = myCore.daemonQueue()
if command == False:
print('The queue is empty (false)')
break
else:
print(command[0])
myCore.daemonQueueAdd('testCommand', 'testData')
command = myCore.daemonQueue()
if command[0] == 'testCommand':
if myCore.daemonQueue() == False:
print('Succesfully added and read command')
unittest.main()

43
onionr/timedHmac.py Normal file
View file

@ -0,0 +1,43 @@
'''
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 <https://www.gnu.org/licenses/>.
'''
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
else:
return false