From eb8c309626444da9b8b64fd42d47ea5ad7114445 Mon Sep 17 00:00:00 2001 From: Benjamin Levy Date: Fri, 20 Jul 2018 17:49:03 -0400 Subject: [PATCH 01/55] Update the Makefile to comply with the DESTDIR/PREFIX convention --- Makefile | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 472ffc2d..a9c5a6f5 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,20 @@ +PREFIX = /usr/local + .DEFAULT_GOAL := setup setup: - sudo pip3 install -r requirements.txt + pip3 install -r requirements.txt install: - sudo rm -rf /usr/share/onionr/ - sudo rm -f /usr/bin/onionr - sudo cp -rp ./onionr /usr/share/onionr - sudo sh -c "echo \"#!/bin/sh\ncd /usr/share/onionr/\n./onionr.py \\\"\\\$$@\\\"\" > /usr/bin/onionr" - sudo chmod +x /usr/bin/onionr - sudo chown -R `whoami` /usr/share/onionr/ + cp -rfp ./onionr $(DESTDIR)$(PREFIX)/share/onionr + echo '#!/bin/sh' > $(DESTDIR)$(PREFIX)/bin/onionr + echo 'cd $(DESTDIR)$(PREFIX)/share/onionr' > $(DESTDIR)$(PREFIX)/bin/onionr + echo './onionr \\\"\\\$$@\\\"\"' > $(DESTDIR)$(PREFIX)/bin/onionr + chmod +x $(DESTDIR)$(PREFIX)/bin/onionr uninstall: - sudo rm -rf /usr/share/onionr - sudo rm -f /usr/bin/onionr + rm -rf $(DESTDIR)$(PREFIX)/share/onionr + rm -f $(DESTDIR)$(PREFIX)/bin/onionr test: @./RUN-LINUX.sh stop From e59d4645e1e7662afc91042090f0712cf95d1084 Mon Sep 17 00:00:00 2001 From: Benjamin Levy Date: Fri, 20 Jul 2018 17:55:03 -0400 Subject: [PATCH 02/55] Fix onionr start script --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a9c5a6f5..cb290800 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ install: cp -rfp ./onionr $(DESTDIR)$(PREFIX)/share/onionr echo '#!/bin/sh' > $(DESTDIR)$(PREFIX)/bin/onionr echo 'cd $(DESTDIR)$(PREFIX)/share/onionr' > $(DESTDIR)$(PREFIX)/bin/onionr - echo './onionr \\\"\\\$$@\\\"\"' > $(DESTDIR)$(PREFIX)/bin/onionr + echo './onionr "$$@"' > $(DESTDIR)$(PREFIX)/bin/onionr chmod +x $(DESTDIR)$(PREFIX)/bin/onionr uninstall: From 8e1b6e1e7e6742fc55471d1a121941c09c28e7cb Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sat, 21 Jul 2018 19:20:28 -0500 Subject: [PATCH 03/55] added forcedifficulty --- onionr/api.py | 2 ++ onionr/onionrproofs.py | 13 ++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index bf592c59..018f2472 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -198,6 +198,8 @@ class API: resp = Response('\n'.join(self._core.getBlockList())) elif action == 'directMessage': resp = Response(self._core.handle_direct_connection(data)) + #elif action == 'nodeProof': + elif action == 'announce': if data != '': # TODO: require POW for this diff --git a/onionr/onionrproofs.py b/onionr/onionrproofs.py index 194b37e9..b93d5724 100644 --- a/onionr/onionrproofs.py +++ b/onionr/onionrproofs.py @@ -22,16 +22,19 @@ import nacl.encoding, nacl.hash, nacl.utils, time, math, threading, binascii, lo import core class DataPOW: - def __init__(self, data, threadCount = 5): + def __init__(self, data, forceDifficulty=0, threadCount = 5): self.foundHash = False self.difficulty = 0 self.data = data self.threadCount = threadCount - dataLen = sys.getsizeof(data) - self.difficulty = math.floor(dataLen / 1000000) - if self.difficulty <= 2: - self.difficulty = 4 + if forceDifficulty == 0: + dataLen = sys.getsizeof(data) + self.difficulty = math.floor(dataLen / 1000000) + if self.difficulty <= 2: + self.difficulty = 4 + else: + self.difficulty = forceDifficulty try: self.data = self.data.encode() From 71007a2d0af8961fb2cf37e5821cd3b96464c532 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 23 Jul 2018 02:43:10 -0500 Subject: [PATCH 04/55] + added reverse block insertion * handle downloading of blocks better when peer goes offline * bumped default disk allocation * added post request util --- onionr/api.py | 24 +++++++-- onionr/communicator2.py | 30 +++++++++-- onionr/core.py | 5 +- onionr/onionr.py | 5 +- onionr/onionrexceptions.py | 4 ++ onionr/onionrutils.py | 73 +++++++++++++++++--------- onionr/static-data/default_config.json | 2 +- 7 files changed, 106 insertions(+), 37 deletions(-) diff --git a/onionr/api.py b/onionr/api.py index 018f2472..d6540b62 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -24,7 +24,7 @@ from gevent.wsgi import WSGIServer import sys, random, threading, hmac, hashlib, base64, time, math, os, logger, config from core import Core from onionrblockapi import Block -import onionrutils, onionrcrypto +import onionrutils, onionrcrypto, blockimporter class API: ''' @@ -141,9 +141,6 @@ class API: resp = Response('Goodbye') elif action == 'ping': resp = Response('pong') - elif action == 'stats': - resp = Response('me_irl') - raise Exception elif action == 'site': block = data siteData = self._core.getData(data) @@ -175,6 +172,24 @@ class API: resp = Response("") return resp + @app.route('/public/upload/', methods=['POST']) + def blockUpload(): + self.validateHost('public') + resp = 'failure' + try: + data = request.form['block'] + except KeyError: + logger.warn('No block specified for upload') + pass + else: + if sys.getsizeof(data) < 100000000: + if blockimporter.importBlockFromData(data, self._core): + resp = 'success' + else: + logger.warn('Error encountered importing uploaded block') + + resp = Response(resp) + return resp @app.route('/public/') def public_handler(): # Public means it is publicly network accessible @@ -198,7 +213,6 @@ class API: resp = Response('\n'.join(self._core.getBlockList())) elif action == 'directMessage': resp = Response(self._core.handle_direct_connection(data)) - #elif action == 'nodeProof': elif action == 'announce': if data != '': diff --git a/onionr/communicator2.py b/onionr/communicator2.py index b4a32649..31d59bf6 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -36,6 +36,8 @@ class OnionrCommunicatorDaemon: # intalize NIST beacon salt and time self.nistSaltTimestamp = 0 self.powSalt = 0 + + self.blockToUpload = '' # loop time.sleep delay in seconds self.delay = 1 @@ -84,7 +86,7 @@ class OnionrCommunicatorDaemon: OnionrCommunicatorTimers(self, self.lookupAdders, 60, requiresPeer=True) # set loop to execute instantly to load up peer pool (replaced old pool init wait) - peerPoolTimer.count = (peerPoolTimer.frequency - 1) + peerPoolTimer.count = (peerPoolTimer.frequency - 1) # Main daemon loop, mainly for calling timers, don't do any complex operations here to avoid locking try: @@ -101,7 +103,7 @@ class OnionrCommunicatorDaemon: logger.info('Goodbye.') self._core._utils.localCommand('shutdown') time.sleep(0.5) - + def lookupKeys(self): '''Lookup new keys''' logger.debug('Looking up new keys...') @@ -111,7 +113,6 @@ class OnionrCommunicatorDaemon: peer = self.pickOnlinePeer() newKeys = self.peerAction(peer, action='kex') self._core._utils.mergeKeys(newKeys) - self.decrementThreadCount('lookupKeys') return @@ -196,7 +197,7 @@ class OnionrCommunicatorDaemon: pass logger.warn('Block hash validation failed for ' + blockHash + ' got ' + tempHash) self.blockQueue.remove(blockHash) # remove from block queue both if success or false - self.currentDownloading.remove(blockHash) + self.currentDownloading.remove(blockHash) self.decrementThreadCount('getBlocks') return @@ -339,10 +340,31 @@ class OnionrCommunicatorDaemon: for i in self.timers: if i.timerFunction.__name__ == 'lookupKeys': i.count = (i.frequency - 1) + elif cmd[0] == 'uploadBlock': + self.blockToUpload = cmd[1] + threading.Thread(target=self.uploadBlock).start() else: logger.info('Recieved daemonQueue command:' + cmd[0]) self.decrementThreadCount('daemonCommands') + def uploadBlock(self): + tiredPeers = [] + if not self._core._utils.validateHash(self.blockToUpload): + logger.warn('Requested to upload invalid block') + return + for i in max(len(self.onlinePeers), 2): + while True: + peer = self.pickOnlinePeer() + if peer + url = 'http://' + peer + '/public/upload/' + data = {'block': block.Block(self.blockToUpload).getRaw()} + if peer.endswith('.onion'): + proxyType = 'tor' + elif peer.endswith('.i2p'): + proxyType = 'i2p' + logger.info("Uploading block") + self._core._utils.doPostRequest(url, data=data, proxyType=proxyType) + def announce(self, peer): '''Announce to peers our address''' announceCount = 0 diff --git a/onionr/core.py b/onionr/core.py index fbe9cc80..2efa0d6c 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -41,10 +41,12 @@ class Core: self.blockDataLocation = 'data/blocks/' self.addressDB = 'data/address.db' self.hsAdder = '' - self.bootstrapFileLocation = 'static-data/bootstrap-nodes.txt' self.bootstrapList = [] self.requirements = onionrvalues.OnionrValues() + self.torPort = torPort + + self.usageFile = 'data/disk-usage.txt' if not os.path.exists('data/'): os.mkdir('data/') @@ -757,6 +759,7 @@ class Core: retData = self.setData(payload) self.addToBlockDB(retData, selfInsert=True, dataSaved=True) self.setBlockType(retData, meta['type']) + self.daemonQueueAdd('uploadBlock', retData) if retData != False: events.event('insertBlock', onionr = None, threaded = False) diff --git a/onionr/onionr.py b/onionr/onionr.py index 10aa54ce..11db4351 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -199,7 +199,7 @@ class Onionr: 'connect': self.addAddress, 'kex': self.doKEX, - 'getpassword': self.getWebPassword + 'getpassword': self.printWebPassword } self.cmdhelp = { @@ -258,6 +258,9 @@ class Onionr: def getWebPassword(self): return config.get('client.hmac') + + def printWebPassword(self): + print(self.getWebPassword()) def getHelp(self): return self.cmdhelp diff --git a/onionr/onionrexceptions.py b/onionr/onionrexceptions.py index c97849c1..d0a6d248 100644 --- a/onionr/onionrexceptions.py +++ b/onionr/onionrexceptions.py @@ -42,6 +42,10 @@ class InvalidHexHash(Exception): '''When a string is not a valid hex string of appropriate length for a hash value''' pass +class InvalidProof(Exception): + '''When a proof is invalid or inadequate''' + pass + # network level exceptions class MissingPort(Exception): pass diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index 33e3f51d..2b6c6e5a 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -54,21 +54,12 @@ class OnionrUtils: except Exception as error: logger.error('Failed to fetch time bypass token.', error=error) - def sendPM(self, pubkey, message): + def getRoundedEpoch(self, roundS=60): ''' - High level function to encrypt a message to a peer and insert it as a block - ''' - - self._core.insertBlock(message, header='pm', sign=True, encryptType='asym', asymPeer=pubkey) - - return - - def getCurrentHourEpoch(self): - ''' - Returns the current epoch, rounded down to the hour + Returns the epoch, rounded down to given seconds (Default 60) ''' epoch = self.getEpoch() - return epoch - (epoch % 3600) + return epoch - (epoch % roundS) def incrementAddressSuccess(self, address): ''' @@ -134,9 +125,10 @@ class OnionrUtils: if newAdderList != False: for adder in newAdderList.split(','): if not adder in self._core.listAdders(randomOrder = False) and adder.strip() != self.getMyAddress(): - if self._core.addAddress(adder): - logger.info('Added %s to db.' % adder, timestamp = True) - retVal = True + if adder[:4] == '0000': + if self._core.addAddress(adder): + logger.info('Added %s to db.' % adder, timestamp = True) + retVal = True else: pass #logger.debug('%s is either our address or already in our DB' % adder) @@ -210,19 +202,26 @@ class OnionrUtils: ''' meta = {} + metadata = {} + data = blockData try: blockData = blockData.encode() except AttributeError: pass - metadata = json.loads(blockData[:blockData.find(b'\n')].decode()) - data = blockData[blockData.find(b'\n'):].decode() + + try: + metadata = json.loads(blockData[:blockData.find(b'\n')].decode()) + except json.decoder.JSONDecodeError: + pass + else: + data = blockData[blockData.find(b'\n'):].decode() - if not metadata['encryptType'] in ('asym', 'sym'): - try: - meta = json.loads(metadata['meta']) - except KeyError: - pass - meta = metadata['meta'] + if not metadata['encryptType'] in ('asym', 'sym'): + try: + meta = json.loads(metadata['meta']) + except KeyError: + pass + meta = metadata['meta'] return (metadata, meta, data) def checkPort(self, port, host=''): @@ -525,6 +524,30 @@ class OnionrUtils: '''returns epoch''' return math.floor(time.time()) + def doPostRequest(self, url, data={}, port=0, proxyType='tor'): + ''' + Do a POST request through a local tor or i2p instance + ''' + if proxyType == 'tor': + if port == 0: + port = self._core.torPort + proxies = {'http': 'socks5://127.0.0.1:' + str(port), 'https': 'socks5://127.0.0.1:' + str(port)} + elif proxyType == 'i2p': + proxies = {'http': 'http://127.0.0.1:4444'} + else: + return + headers = {'user-agent': 'PyOnionr'} + try: + proxies = {'http': 'socks5h://127.0.0.1:' + str(port), 'https': 'socks5h://127.0.0.1:' + str(port)} + r = requests.post(url, data=data, headers=headers, proxies=proxies, allow_redirects=False, timeout=(15, 30)) + retData = r.text + except KeyboardInterrupt: + raise KeyboardInterrupt + except requests.exceptions.RequestException as e: + logger.debug('Error: %s' % str(e)) + retData = False + return retData + def doGetRequest(self, url, port=0, proxyType='tor'): ''' Do a get request through a local tor or i2p instance @@ -549,7 +572,7 @@ class OnionrUtils: retData = False return retData - def getNistBeaconSalt(self, torPort=0): + def getNistBeaconSalt(self, torPort=0, rounding=3600): ''' Get the token for the current hour from the NIST randomness beacon ''' @@ -559,7 +582,7 @@ class OnionrUtils: except IndexError: raise onionrexceptions.MissingPort('Missing Tor socks port') retData = '' - curTime = self._core._utils.getCurrentHourEpoch + curTime = self.getRoundedEpoch(rounding) self.nistSaltTimestamp = curTime 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) diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index 9188aa66..db86bbe5 100644 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -33,7 +33,7 @@ }, "allocations":{ - "disk": 1000000000, + "disk": 9000000000, "netTotal": 1000000000, "blockCache" : 5000000, "blockCacheTotal" : 50000000 From 0beffab96e0c1e46922981ce8c418d61ff4dba98 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 23 Jul 2018 02:45:48 -0500 Subject: [PATCH 05/55] + added blockimporter.py * removed outdated direct connection handler --- onionr/blockimporter.py | 40 ++++++++++++++++++++++++++++++++++++++++ onionr/core.py | 19 ------------------- 2 files changed, 40 insertions(+), 19 deletions(-) create mode 100644 onionr/blockimporter.py diff --git a/onionr/blockimporter.py b/onionr/blockimporter.py new file mode 100644 index 00000000..a2695093 --- /dev/null +++ b/onionr/blockimporter.py @@ -0,0 +1,40 @@ +''' + Onionr - P2P Microblogging Platform & Social network + + Import block data and save it +''' +''' + 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 core, onionrexceptions, logger +def importBlockFromData(content, coreInst): + retData = False + if not isinstance(coreInst, core.Core): + raise Exception("coreInst must be an Onionr core instance") + + try: + content = content.encode() + except AttributeError: + pass + + metas = coreInst._utils.getBlockMetadataFromData(content) # returns tuple(metadata, meta), meta is also in metadata + metadata = metas[0] + if coreInst._utils.validateMetadata(metadata): # check if metadata is valid + if coreInst._crypto.verifyPow(content): # check if POW is enough/correct + logger.info('Block passed proof, saving.') + blockHash = coreInst.setData(content) + blockHash = coreInst.addToBlockDB(blockHash, dataSaved=True) + coreInst._utils.processBlockMetadata(blockHash) # caches block metadata values to block database + retData = True + return retData \ No newline at end of file diff --git a/onionr/core.py b/onionr/core.py index 2efa0d6c..b8b33c73 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -580,25 +580,6 @@ class Core: conn.close() return - def handle_direct_connection(self, data): - ''' - Handles direct messages - ''' - try: - data = json.loads(data) - - # TODO: Determine the sender, verify, etc - if ('callback' in data) and (data['callback'] is True): - # then this is a response to the message we sent earlier - self.daemonQueueAdd('checkCallbacks', json.dumps(data)) - else: - # then we should handle it and respond accordingly - self.daemonQueueAdd('incomingDirectConnection', json.dumps(data)) - except Exception as e: - logger.warn('Failed to handle incoming direct message: %s' % str(e)) - - return - def getBlockList(self, unsaved = False): # TODO: Use unsaved?? ''' Get list of our blocks From ca122dc1ba0e6b421cedefd2d48d5180fd5482f5 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 23 Jul 2018 15:04:36 -0500 Subject: [PATCH 06/55] upload to multiple peers --- onionr/communicator2.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/onionr/communicator2.py b/onionr/communicator2.py index 31d59bf6..a4c3e023 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -348,14 +348,15 @@ class OnionrCommunicatorDaemon: self.decrementThreadCount('daemonCommands') def uploadBlock(self): - tiredPeers = [] + triedPeers = [] if not self._core._utils.validateHash(self.blockToUpload): logger.warn('Requested to upload invalid block') return for i in max(len(self.onlinePeers), 2): - while True: - peer = self.pickOnlinePeer() - if peer + peer = self.pickOnlinePeer() + if peer in triedPeers: + continue + triedPeers.append(peer) url = 'http://' + peer + '/public/upload/' data = {'block': block.Block(self.blockToUpload).getRaw()} if peer.endswith('.onion'): From 5f1a02e42d73227a63a6b7c1f0d592533986e246 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Mon, 23 Jul 2018 15:23:32 -0500 Subject: [PATCH 07/55] upload to multiple peers --- onionr/communicator2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onionr/communicator2.py b/onionr/communicator2.py index a4c3e023..ba24991b 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -352,7 +352,7 @@ class OnionrCommunicatorDaemon: if not self._core._utils.validateHash(self.blockToUpload): logger.warn('Requested to upload invalid block') return - for i in max(len(self.onlinePeers), 2): + for i in range(max(len(self.onlinePeers), 2)): peer = self.pickOnlinePeer() if peer in triedPeers: continue From afdee2a7a5bea6018805561a41db80bbe0ff1892 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Thu, 26 Jul 2018 22:07:50 -0500 Subject: [PATCH 08/55] work on new peer profiling system --- docs/api.md | 34 +-------------- docs/onionr-draft.md | 57 -------------------------- onionr/communicator2.py | 16 +++++++- onionr/core.py | 7 ++-- onionr/onionrpeers.py | 57 +++++++++++++++++++++++++- onionr/static-data/default_config.json | 3 ++ 6 files changed, 77 insertions(+), 97 deletions(-) delete mode 100644 docs/onionr-draft.md diff --git a/docs/api.md b/docs/api.md index 7f9128a5..52a55368 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,34 +1,2 @@ -BLOCK HEADERS (simple ID system to identify block type) ------------------------------------------------ --crypt- (encrypted block) --bin- (binary file) --txt- (plaintext) - HTTP API ------------------------------------------------- -/client/ (Private info, not publicly accessible) - -- hello - - hello world -- shutdown - - exit onionr -- stats - - show node stats - -/public/ - -- firstConnect - - initialize with peer -- ping - - pong -- setHMAC - - set a created symmetric key -- getDBHash - - get the hash of the current hash database state -- getPGP - - export node's PGP public key -- getData - - get a data block -- getBlockHashes - - get a list of the node's hashes -------------------------------------------------- +TODO diff --git a/docs/onionr-draft.md b/docs/onionr-draft.md deleted file mode 100644 index acce39e7..00000000 --- a/docs/onionr-draft.md +++ /dev/null @@ -1,57 +0,0 @@ -# Onionr Protocol Spec v2 - -A P2P platform for Tor & I2P - -# Overview - -Onionr is an encrypted microblogging & mailing system designed in the spirit of Twitter. -There are no central servers and all traffic is peer to peer by default (routed via Tor or I2P). -User IDs are simply Tor onion service/I2P host id + Ed25519 key fingerprint. -Private blocks are only able to be read by the intended peer. -All traffic is over Tor/I2P, connecting only to Tor onion and I2P hidden services. - -## Goals: - • Selective sharing of information - • Secure & semi-anonymous direct messaging - • Forward secrecy - • Defense in depth - • Data should be secure for years to come - • Decentralization - * Avoid browser-based exploits that plague similar software - * Avoid timing attacks & unexpected metadata leaks - -## Protocol - -Onionr nodes use HTTP (over Tor/I2P) to exchange keys, metadata, and blocks. Blocks are identified by their sha3_256 hash. Nodes sync a table of blocks hashes and attempt to download blocks they do not yet have from random peers. - -Blocks may be encrypted using Curve25519 or Salsa20. - -Blocks have IDs in the following format: - --Optional hash of public key of publisher (base64)-optional signature (non-optional if publisher is specified) (Base64)-block type-block hash(sha3-256) - -pubkeyHash-signature-type-hash - -## Connections - -When a node first comes online, it attempts to bootstrap using a default list provided by a client. -When two peers connect, they exchange Ed25519 keys (if applicable) then Salsa20 keys. - -Salsa20 keys are regenerated either every X many communications with a peer or every X minutes. - -Every 100kb or every 2 hours is a recommended default. - -All valid requests with HMAC should be recorded until used HMAC's expiry to prevent replay attacks. -Peer Types - * Friends: - * Encrypted ‘friends only’ posts to one another - * Usually less strict rate & storage limits - * Strangers: - * Used for storage of encrypted or public information - * Can only read public posts - * Usually stricter rate & storage limits - -## Spam mitigation - -To send or receive data, a node can optionally request that the other node generate a hash that when in hexadecimal representation contains a random string at a random location in the string. Clients will configure what difficulty to request, and what difficulty is acceptable for themselves to perform. Difficulty should correlate with recent network & disk usage and data size. Friends can be configured to have less strict (to non existent) limits, separately from strangers. (proof of work). -Rate limits can be strict, as Onionr is not intended to be an instant messaging application. \ No newline at end of file diff --git a/onionr/communicator2.py b/onionr/communicator2.py index ba24991b..b3734cbc 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -20,7 +20,7 @@ along with this program. If not, see . ''' import sys, os, core, config, json, onionrblockapi as block, requests, time, logger, threading, onionrplugins as plugins, base64, onionr -import onionrexceptions +import onionrexceptions, onionrpeers from defusedxml import minidom class OnionrCommunicatorDaemon: @@ -48,6 +48,7 @@ class OnionrCommunicatorDaemon: # lists of connected peers and peers we know we can't reach currently self.onlinePeers = [] self.offlinePeers = [] + self.peerProfiles = [] # list of peer's profiles (onionrpeers.PeerProfile instances) # amount of threads running by name, used to prevent too many self.threadCounts = {} @@ -262,6 +263,7 @@ class OnionrCommunicatorDaemon: '''Adds a new random online peer to self.onlinePeers''' retData = False tried = self.offlinePeers + peerScores = {} if peer != '': if self._core._utils.validateID(peer): peerList = [peer] @@ -274,6 +276,14 @@ class OnionrCommunicatorDaemon: # Avoid duplicating bootstrap addresses in peerList self.addBootstrapListToPeerList(peerList) + for address in peerList: + # Load peer's profiles into a list + profile = onionrpeers.PeerProfiles(address, self._core) + peerScores[address] = profile.score + + # Sort peers by their score, greatest to least + peerList = sorted(peerScores, key=peerScores.get, reverse=True) + for address in peerList: if len(address) == 0 or address in tried or address in self.onlinePeers: continue @@ -299,7 +309,7 @@ class OnionrCommunicatorDaemon: logger.info(i) def peerAction(self, peer, action, data=''): - '''Perform a get request to a peer''' + '''Perform a get request to a peer''' if len(peer) == 0: return False logger.info('Performing ' + action + ' with ' + peer + ' on port ' + str(self.proxyPort)) @@ -348,6 +358,8 @@ class OnionrCommunicatorDaemon: self.decrementThreadCount('daemonCommands') def uploadBlock(self): + '''Upload our block to a few peers''' + # when inserting a block, we try to upload it to a few peers to add some deniability triedPeers = [] if not self._core._utils.validateHash(self.blockToUpload): logger.warn('Requested to upload invalid block') diff --git a/onionr/core.py b/onionr/core.py index b8b33c73..e147dc72 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -575,9 +575,10 @@ class Core: # TODO: validate key on whitelist if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'DBHash', 'failure', 'lastConnect'): raise Exception("Got invalid database key when setting address info") - c.execute('UPDATE adders SET ' + key + ' = ? WHERE address=?', command) - conn.commit() - conn.close() + else: + c.execute('UPDATE adders SET ' + key + ' = ? WHERE address=?', command) + conn.commit() + conn.close() return def getBlockList(self, unsaved = False): # TODO: Use unsaved?? diff --git a/onionr/onionrpeers.py b/onionr/onionrpeers.py index b6ed72ec..b83fa9bc 100644 --- a/onionr/onionrpeers.py +++ b/onionr/onionrpeers.py @@ -1,7 +1,7 @@ ''' Onionr - P2P Microblogging Platform & Social network. - This file contains both the OnionrCommunicate class for communcating with peers + This file contains both the PeerProfiles class for network profiling of Onionr nodes ''' ''' This program is free software: you can redistribute it and/or modify @@ -16,4 +16,57 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -''' \ No newline at end of file +''' +import core +class PeerProfiles: + ''' + PeerProfiles + ''' + def __init__(self, address, coreInst): + self.address = address # node address + self.score = None + self.friendSigCount = 0 + self.success = 0 + self.failure = 0 + + if not isinstance(coreInst, core.Core): + raise TypeError("coreInst must be a type of core.Core") + self.coreInst = coreInst + assert isinstance(self.coreInst, core.Core) + + self.loadScore() + return + + def loadScore(self): + '''Load the node's score from the database''' + try: + self.success = int(self.coreInst.getAddressInfo('success')) + except TypeError: + self.success = 0 + try: + self.failure = int(self.coreInst.getAddressInfo('failure')) + except TypeError: + self.failure = 0 + self.score = self.success - self.failure + + def saveScore(self): + '''Save the node's score to the database''' + self.coreInst.setAddressInfo(self.address, 'success', self.success) + self.coreInst.setAddressInfo(self.address, 'failure', self.failure) + return + +def getScoreSortedPeerList(coreInst): + if not type(coreInst is core.Core): + raise TypeError('coreInst must be instance of core.Core') + + peerList = coreInst.listAdders() + peerScores = {} + + for address in peerList: + # Load peer's profiles into a list + profile = PeerProfiles(address, coreInst) + peerScores[address] = profile.score + + # Sort peers by their score, greatest to least + peerList = sorted(peerScores, key=peerScores.get, reverse=True) + return peerList \ No newline at end of file diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index db86bbe5..3a08f7db 100644 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -37,5 +37,8 @@ "netTotal": 1000000000, "blockCache" : 5000000, "blockCacheTotal" : 50000000 + }, + "peers":{ + "minimumScore": 5 } } From d39208d64838701ea5302cfb2ac8d0bc8e58409f Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 27 Jul 2018 00:48:22 -0500 Subject: [PATCH 09/55] added static dir and serving for web ui --- onionr/api.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/onionr/api.py b/onionr/api.py index d6540b62..43e7cabc 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -18,7 +18,7 @@ along with this program. If not, see . ''' import flask -from flask import request, Response, abort +from flask import request, Response, abort, send_from_directory from multiprocessing import Process from gevent.wsgi import WSGIServer import sys, random, threading, hmac, hashlib, base64, time, math, os, logger, config @@ -113,6 +113,21 @@ class API: return resp + @app.route('/client/ui/') + def webUI(path): + startTime = math.floor(time.time()) + if request.args.get('timingToken') is None: + timingToken = '' + else: + timingToken = request.args.get('timingToken') + self.validateHost('private') + endTime = math.floor(time.time()) + elapsed = endTime - startTime + if not hmac.compare_digest(timingToken, self.timeBypassToken): + if elapsed < self._privateDelayTime: + time.sleep(self._privateDelayTime - elapsed) + return send_from_directory('static-data/ui/', path) + @app.route('/client/') def private_handler(): if request.args.get('timingToken') is None: From d90be83776f016c9ed2f80b5e9c6354d03f31dc9 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Fri, 27 Jul 2018 18:04:30 -0500 Subject: [PATCH 10/55] work on new whitepaper --- docs/onionr-logo.png | Bin 12273 -> 7223 bytes docs/onionr-web.png | Bin 0 -> 41162 bytes docs/whitepaper.md | 25 +++++++++++++++++++++++++ onionr/static-data/ui/readme.txt | 1 + 4 files changed, 26 insertions(+) create mode 100644 docs/onionr-web.png create mode 100644 docs/whitepaper.md create mode 100644 onionr/static-data/ui/readme.txt diff --git a/docs/onionr-logo.png b/docs/onionr-logo.png index d0860f6dcbba467b09050a6dc45337135fefbd74..b6c3c9b556ca0a650db683d0910b6d0c728f2722 100644 GIT binary patch literal 7223 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 XytiM$meax91On6`+Ddf_HqrkF%>eJr literal 12273 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�lwzF(Cbh#3`Bm_c2g%pP;a5>A6Q5nn?alq8nt9 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

V=9!;xK?yqLT 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 OM7r8=ttzNZ#Qy+Gv#A^a literal 0 HcmV?d00001 diff --git a/docs/whitepaper.md b/docs/whitepaper.md new file mode 100644 index 00000000..7c05d95b --- /dev/null +++ b/docs/whitepaper.md @@ -0,0 +1,25 @@ +

+ <h1>Onionr</h1> +

+

Anonymous, Decentralized, Distributed Network

+ +# Introduction + +The most important thing in the modern world is information. The ability to communicate freely with others. The internet has provided humanity with the ability to spread information globally, but there are many people who try (and sometimes succeed) to stifle the flow of information. + +Internet censorship comes in many forms, state censorship, corporate consoldiation of media, threats of violence, network exploitation (e.g. denial of service attacks). + +To prevent censorship or loss of information, these measures must be in place: + +* Resistence to censorship of underlying infrastructure or hosts + +* Inability to violently coerce human(s) behind the information (ex. law enforcement or personal threats "doxxing") + + * To reduce metadata to a minimum, a system should anonymize its users by default + +* Economic availability. A system should not rely on a single device to be constantly online, and should not be overtly expensive to use. The majority of people in the world own cell phones, but comparatively few own personal computers, particuarly in developing countries. + + +# Issues With Existing Software + +There are many great projects that tackle decentralization and privacy issues, but there are none which tackle all of the above issues. \ No newline at end of file diff --git a/onionr/static-data/ui/readme.txt b/onionr/static-data/ui/readme.txt new file mode 100644 index 00000000..40dea268 --- /dev/null +++ b/onionr/static-data/ui/readme.txt @@ -0,0 +1 @@ +Static files for Onionr's web ui, change at your own risk. From 942d3e8cab7c32a7305a66479da5208c61131d2e Mon Sep 17 00:00:00 2001 From: Kevin Date: Sat, 28 Jul 2018 00:53:46 -0500 Subject: [PATCH 11/55] more work on whitepaper --- docs/whitepaper.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/whitepaper.md b/docs/whitepaper.md index 7c05d95b..25c5c3fc 100644 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -22,4 +22,8 @@ To prevent censorship or loss of information, these measures must be in place: # Issues With Existing Software -There are many great projects that tackle decentralization and privacy issues, but there are none which tackle all of the above issues. \ No newline at end of file +There are many great projects that tackle decentralization and privacy issues, but there are none which tackle all of the above issues. + +## Tor/I2P + +Tor and I2P are both great "onion routers" that protect privacy, however these are mainly transport projects. Tor and I2P do not provide decentralization of data on their own, as they only transport data and provide a rendevous. "Hidden services", being hosted on central servers, often do not last long, as they are reliant on only 1 machine being online, which also increases an attacker's ability to unmask them. \ No newline at end of file From 18d075a018f49f80414b37514b96430714d79653 Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Sun, 29 Jul 2018 02:34:04 -0500 Subject: [PATCH 12/55] more work on whitepaper --- docs/whitepaper.md | 52 +++++++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/docs/whitepaper.md b/docs/whitepaper.md index 25c5c3fc..07566004 100644 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -1,5 +1,5 @@

- <h1>Onionr</h1> + <h1>Onionr</h1>

Anonymous, Decentralized, Distributed Network

@@ -7,23 +7,47 @@ The most important thing in the modern world is information. The ability to communicate freely with others. The internet has provided humanity with the ability to spread information globally, but there are many people who try (and sometimes succeed) to stifle the flow of information. -Internet censorship comes in many forms, state censorship, corporate consoldiation of media, threats of violence, network exploitation (e.g. denial of service attacks). +Internet censorship comes in many forms, state censorship, corporate consolidation of media, threats of violence, network exploitation (e.g. denial of service attacks). To prevent censorship or loss of information, these measures must be in place: -* Resistence to censorship of underlying infrastructure or hosts +* Resistance to censorship of underlying infrastructure or of network hosts -* Inability to violently coerce human(s) behind the information (ex. law enforcement or personal threats "doxxing") - - * To reduce metadata to a minimum, a system should anonymize its users by default +* Anonymization of users by default + * The Inability to violently coerce human users (personal threats/"doxxing", or totalitarian regime censorship) -* Economic availability. A system should not rely on a single device to be constantly online, and should not be overtly expensive to use. The majority of people in the world own cell phones, but comparatively few own personal computers, particuarly in developing countries. +* Economic availability. A system should not rely on a single device to be constantly online, and should not be overtly expensive to use. The majority of people in the world own cell phones, but comparatively few own personal computers, particularly in developing countries. + +There are many great projects that tackle decentralization and privacy issues, but there are none which tackle all of the above issue. Some of the existing networks have also not worked well in practice, or are more complicated than they need to be. + +# Onionr Design Goals + +When designing Onionr we had these goals in mind: + +* Anonymous Blocks + + * Difficult to determine block creator or users regardless of transport used +* Default Anonymous Transport Layer + * Tor and I2P +* Transport agnosticism +* Default global sync, but can configure what blocks to seed +* Spam resistance +* Encrypted blocks + +# Onionr Design + +(See the spec for specific details) + +## General Overview + +At its core, Onionr is merely a description for storing data in self-verifying packages ("blocks"). These blocks can be encrypted to a user (or self), encrypted symmetrically, or not at all. Blocks can be signed by their creator, but regardless, they are self-verifying due to being identified by a sha3-256 hash value; once a block is created, it cannot be modified. + +Onionr exchanges a list of blocks between all nodes. By default, all nodes download and share all other blocks, however this is configurable. + +## Blocks + +Onionr blocks are very simple. They are structured in two main parts: a metadata section and a data section, with a line feed delimiting where metadata ends and data begins. Metadata defines what kind of data is in a block, signature data, encryption settings, and other arbitrary information. + +For encryption, Onionr uses ephemeral Curve25519 keys for key exchange and XSalsa20-Poly1305 as a symmetric cipher, or optionally using only XSalsa20-Poly1305. -# Issues With Existing Software - -There are many great projects that tackle decentralization and privacy issues, but there are none which tackle all of the above issues. - -## Tor/I2P - -Tor and I2P are both great "onion routers" that protect privacy, however these are mainly transport projects. Tor and I2P do not provide decentralization of data on their own, as they only transport data and provide a rendevous. "Hidden services", being hosted on central servers, often do not last long, as they are reliant on only 1 machine being online, which also increases an attacker's ability to unmask them. \ No newline at end of file From 695cd7503b40dda7956e10d88a990c28305c52e1 Mon Sep 17 00:00:00 2001 From: Arinerron Date: Sun, 29 Jul 2018 16:27:03 -0700 Subject: [PATCH 13/55] Fix spelling issues --- docs/whitepaper.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/whitepaper.md b/docs/whitepaper.md index 07566004..adb0ba1a 100644 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -16,9 +16,9 @@ To prevent censorship or loss of information, these measures must be in place: * Anonymization of users by default * The Inability to violently coerce human users (personal threats/"doxxing", or totalitarian regime censorship) -* Economic availability. A system should not rely on a single device to be constantly online, and should not be overtly expensive to use. The majority of people in the world own cell phones, but comparatively few own personal computers, particularly in developing countries. +* Economic availability. A system should not rely on a single device to be constantly online, and should not be overly expensive to use. The majority of people in the world own cell phones, but comparatively few own personal computers, particularly in developing countries. -There are many great projects that tackle decentralization and privacy issues, but there are none which tackle all of the above issue. Some of the existing networks have also not worked well in practice, or are more complicated than they need to be. +There are many great projects that tackle decentralization and privacy issues, but there are none which tackle all of the above issues. Some of the existing networks have also not worked well in practice, or are more complicated than they need to be. # Onionr Design Goals From 215fbcba6872cefe7923f0a7a508b2a9125022cb Mon Sep 17 00:00:00 2001 From: Arinerron Date: Sun, 29 Jul 2018 17:37:12 -0700 Subject: [PATCH 14/55] Add web api callbacks --- onionr/api.py | 71 +++++++++++++++++++ onionr/communicator2.py | 4 +- onionr/onionr.py | 6 +- onionr/onionrblockapi.py | 19 +++-- onionr/onionrpluginapi.py | 20 ++++++ .../static-data/ui/{readme.txt => README.md} | 0 6 files changed, 107 insertions(+), 13 deletions(-) rename onionr/static-data/ui/{readme.txt => README.md} (100%) diff --git a/onionr/api.py b/onionr/api.py index 43e7cabc..256f55f9 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -30,6 +30,9 @@ class API: ''' Main HTTP API (Flask) ''' + + callbacks = {'public' : {}, 'private' : {}, 'ui' : {}} + def validateToken(self, token): ''' Validate that the client token matches the given token @@ -164,6 +167,44 @@ class API: self.mimeType = 'text/html' response = siteData.split(b'-', 2)[-1] resp = Response(response) + elif action == "insertBlock": + response = {'success' : False, 'reason' : 'An unknown error occurred'} + + try: + decoded = json.loads(data) + + block = Block() + + sign = False + + for key in decoded: + val = decoded[key] + + key = key.lower() + + if key == 'type': + block.setType(val) + elif key in ['body', 'content']: + block.setContent(val) + elif key == 'parent': + block.setParent(val) + elif key == 'sign': + sign = (str(val).lower() == 'true') + + hash = block.save(sign = sign) + + if not hash is False: + response['success'] = true + response['hash'] = hash + response['reason'] = 'Successfully wrote block to file' + else: + response['reason'] = 'Faield to save the block' + except Exception as e: + logger.debug('insertBlock api request failed', error = e) + + resp = Response(json.dumps(response)) + elif action in callbacks['private']: + resp = Response(str(getCallback(action, scope = 'private')(request))) else: resp = Response('(O_o) Dude what? (invalid command)') endTime = math.floor(time.time()) @@ -258,6 +299,8 @@ class API: peers = self._core.listPeers(getPow=True) response = ','.join(peers) resp = Response(response) + elif action in callbacks['public']: + resp = Response(str(getCallback(action, scope = 'public')(request))) else: resp = Response("") @@ -328,3 +371,31 @@ class API: # we exit rather than abort to avoid fingerprinting logger.debug('Avoiding fingerprinting, exiting...') sys.exit(1) + + def setCallback(action, callback, scope = 'public'): + if not scope in callbacks: + return False + + callbacks[scope][action] = callback + + return True + + def removeCallback(action, scope = 'public'): + if (not scope in callbacks) or (not action in callbacks[scope]): + return False + + del callbacks[scope][action] + + return True + + def getCallback(action, scope = 'public'): + if (not scope in callbacks) or (not action in callbacks[scope]): + return None + + return callbacks[scope][action] + + def getCallbacks(scope = None): + if (not scope is None) and (scope in callbacks): + return callbacks[scope] + + return callbacks diff --git a/onionr/communicator2.py b/onionr/communicator2.py index b3734cbc..b420fcba 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -36,7 +36,7 @@ class OnionrCommunicatorDaemon: # intalize NIST beacon salt and time self.nistSaltTimestamp = 0 self.powSalt = 0 - + self.blockToUpload = '' # loop time.sleep delay in seconds @@ -309,7 +309,7 @@ class OnionrCommunicatorDaemon: logger.info(i) def peerAction(self, peer, action, data=''): - '''Perform a get request to a peer''' + '''Perform a get request to a peer''' if len(peer) == 0: return False logger.info('Performing ' + action + ' with ' + peer + ' on port ' + str(self.proxyPort)) diff --git a/onionr/onionr.py b/onionr/onionr.py index 11db4351..ec3ec468 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -91,8 +91,6 @@ class Onionr: self.onionrCore = core.Core() self.onionrUtils = OnionrUtils(self.onionrCore) - self.userOS = platform.system() - # Handle commands self.debug = False # Whole application debugging @@ -258,7 +256,7 @@ class Onionr: def getWebPassword(self): return config.get('client.hmac') - + def printWebPassword(self): print(self.getWebPassword()) @@ -542,7 +540,7 @@ class Onionr: subprocess.Popen([communicatorDaemon, "run", str(net.socksPort)]) logger.debug('Started communicator') events.event('daemon_start', onionr = self) - api.API(self.debug) + self.api = api.API(self.debug) return diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py index 695d41ab..a49210d3 100644 --- a/onionr/onionrblockapi.py +++ b/onionr/onionrblockapi.py @@ -38,7 +38,6 @@ class Block: self.btype = type self.bcontent = content - # initialize variables self.valid = True self.raw = None @@ -71,8 +70,10 @@ class Block: # logic - def decrypt(self, anonymous=True, encodedData=True): - '''Decrypt a block, loading decrypted data into their vars''' + def decrypt(self, anonymous = True, encodedData = True): + ''' + Decrypt a block, loading decrypted data into their vars + ''' if self.decrypted: return True retData = False @@ -100,9 +101,11 @@ class Block: else: logger.warn('symmetric decryption is not yet supported by this API') return retData - + def verifySig(self): - '''Verify if a block's signature is signed by its claimed signer''' + ''' + Verify if a block's signature is signed by its claimed signer + ''' core = self.getCore() if core._crypto.edVerify(data=self.signedData, key=self.signer, sig=self.signature, encodedData=True): @@ -227,12 +230,14 @@ class Block: else: self.hash = self.getCore().insertBlock(self.getContent(), header = self.getType(), sign = sign) self.update() + return self.getHash() else: logger.warn('Not writing block; it is invalid.') except Exception as e: logger.error('Failed to save block.', error = e, timestamp = False) - return False + + return False # getters @@ -533,7 +538,7 @@ class Block: if relevant: relevant_blocks.append(block) - + if bool(reverse): relevant_blocks.reverse() diff --git a/onionr/onionrpluginapi.py b/onionr/onionrpluginapi.py index bfaf73e8..0120dad7 100644 --- a/onionr/onionrpluginapi.py +++ b/onionr/onionrpluginapi.py @@ -130,6 +130,22 @@ class CommandAPI: def get_commands(self): return self.pluginapi.get_onionr().getCommands() +class WebAPI: + def __init__(self, pluginapi): + self.pluginapi = pluginapi + + def register_callback(self, action, callback, scope = 'public'): + return self.pluginapi.get_onionr().api.setCallback(action, callback, scope = scope) + + def unregister_callback(self, action, scope = 'public'): + return self.pluginapi.get_onionr().api.removeCallback(action, scope = scope) + + def get_callback(self, action, scope = 'public'): + return self.pluginapi.get_onionr().api.getCallback(action, scope= scope) + + def get_callbacks(self, scope = None): + return self.pluginapi.get_onionr().api.getCallbacks(scope = scope) + class pluginapi: def __init__(self, onionr, data): self.onionr = onionr @@ -142,6 +158,7 @@ class pluginapi: self.daemon = DaemonAPI(self) self.plugins = PluginAPI(self) self.commands = CommandAPI(self) + self.web = WebAPI(self) def get_onionr(self): return self.onionr @@ -167,5 +184,8 @@ class pluginapi: def get_commandapi(self): return self.commands + def get_webapi(self): + return self.web + def is_development_mode(self): return self.get_onionr()._developmentMode diff --git a/onionr/static-data/ui/readme.txt b/onionr/static-data/ui/README.md similarity index 100% rename from onionr/static-data/ui/readme.txt rename to onionr/static-data/ui/README.md From 88df88204c982caf7b011d240ee65b1430a0a9bb Mon Sep 17 00:00:00 2001 From: Arinerron Date: Sun, 29 Jul 2018 17:43:28 -0700 Subject: [PATCH 15/55] Add files --- Makefile | 1 + onionr/api.py | 2 +- onionr/static-data/ui/common/footer.html | 4 + onionr/static-data/ui/common/header.html | 30 ++++ .../ui/common/onionr-timeline-post.html | 31 ++++ onionr/static-data/ui/compile.py | 123 +++++++++++++ onionr/static-data/ui/config.json | 4 + onionr/static-data/ui/dist/css/main.css | 74 ++++++++ .../static-data/ui/dist/css/themes/dark.css | 32 ++++ onionr/static-data/ui/dist/img/default.png | Bin 0 -> 6758 bytes onionr/static-data/ui/dist/index.html | 77 ++++++++ onionr/static-data/ui/dist/js/main.js | 170 ++++++++++++++++++ onionr/static-data/ui/dist/js/timeline.js | 20 +++ onionr/static-data/ui/lang.json | 31 ++++ onionr/static-data/ui/src/css/main.css | 74 ++++++++ onionr/static-data/ui/src/css/themes/dark.css | 32 ++++ onionr/static-data/ui/src/img/default.png | Bin 0 -> 6758 bytes onionr/static-data/ui/src/index.html | 43 +++++ onionr/static-data/ui/src/js/main.js | 167 +++++++++++++++++ onionr/static-data/ui/src/js/timeline.js | 20 +++ 20 files changed, 934 insertions(+), 1 deletion(-) create mode 100644 onionr/static-data/ui/common/footer.html create mode 100644 onionr/static-data/ui/common/header.html create mode 100644 onionr/static-data/ui/common/onionr-timeline-post.html create mode 100755 onionr/static-data/ui/compile.py create mode 100644 onionr/static-data/ui/config.json create mode 100644 onionr/static-data/ui/dist/css/main.css create mode 100644 onionr/static-data/ui/dist/css/themes/dark.css create mode 100644 onionr/static-data/ui/dist/img/default.png create mode 100644 onionr/static-data/ui/dist/index.html create mode 100644 onionr/static-data/ui/dist/js/main.js create mode 100644 onionr/static-data/ui/dist/js/timeline.js create mode 100644 onionr/static-data/ui/lang.json create mode 100644 onionr/static-data/ui/src/css/main.css create mode 100644 onionr/static-data/ui/src/css/themes/dark.css create mode 100644 onionr/static-data/ui/src/img/default.png create mode 100644 onionr/static-data/ui/src/index.html create mode 100644 onionr/static-data/ui/src/js/main.js create mode 100644 onionr/static-data/ui/src/js/timeline.js diff --git a/Makefile b/Makefile index 472ffc2d..c51fc72b 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ setup: sudo pip3 install -r requirements.txt + -@cd onionr/static-data/ui/; ./compile.py install: sudo rm -rf /usr/share/onionr/ diff --git a/onionr/api.py b/onionr/api.py index 256f55f9..705a4781 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -129,7 +129,7 @@ class API: if not hmac.compare_digest(timingToken, self.timeBypassToken): if elapsed < self._privateDelayTime: time.sleep(self._privateDelayTime - elapsed) - return send_from_directory('static-data/ui/', path) + return send_from_directory('static-data/ui/dist/', path) @app.route('/client/') def private_handler(): diff --git a/onionr/static-data/ui/common/footer.html b/onionr/static-data/ui/common/footer.html new file mode 100644 index 00000000..6b5cfb06 --- /dev/null +++ b/onionr/static-data/ui/common/footer.html @@ -0,0 +1,4 @@ + + + + diff --git a/onionr/static-data/ui/common/header.html b/onionr/static-data/ui/common/header.html new file mode 100644 index 00000000..2a2b4f56 --- /dev/null +++ b/onionr/static-data/ui/common/header.html @@ -0,0 +1,30 @@ +<$= LANG.ONIONR_TITLE $> + + + + + + + + + + diff --git a/onionr/static-data/ui/common/onionr-timeline-post.html b/onionr/static-data/ui/common/onionr-timeline-post.html new file mode 100644 index 00000000..ceff5c65 --- /dev/null +++ b/onionr/static-data/ui/common/onionr-timeline-post.html @@ -0,0 +1,31 @@ + +
+
+
+
+ +
+
+
+ +
+ +
+
+ +
+ $content +
+ +
+ like + comment +
+
+
+
+
+ diff --git a/onionr/static-data/ui/compile.py b/onionr/static-data/ui/compile.py new file mode 100755 index 00000000..c93e4aa7 --- /dev/null +++ b/onionr/static-data/ui/compile.py @@ -0,0 +1,123 @@ +#!/usr/bin/python3 + +import shutil, os, re, json, traceback + +# get user's config +settings = {} +with open('config.json', 'r') as file: + settings = json.loads(file.read()) + +# "hardcoded" config, not for user to mess with +HEADER_FILE = 'common/header.html' +FOOTER_FILE = 'common/footer.html' +SRC_DIR = 'src/' +DST_DIR = 'dist/' +HEADER_STRING = '
' +FOOTER_STRING = '