diff --git a/Makefile b/Makefile index 13d9c0f9..9ee7df8d 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,7 @@ soft-reset: reset: @echo "Hard-resetting Onionr..." rm -rf onionr/data/ | true > /dev/null 2>&1 + cd onionr/static-data/www/ui/; rm -rf ./dist; python compile.py #@./RUN-LINUX.sh version | grep -v "Failed" --color=always plugins-reset: diff --git a/readme.md b/README.md similarity index 96% rename from readme.md rename to README.md index 7486bcac..a01a65b2 100644 --- a/readme.md +++ b/README.md @@ -1,12 +1,12 @@ ![Onionr logo](./docs/onionr-logo.png) +v0.3.0 (***experimental, not safe or easy to use yet***) + [![Open Source Love](https://badges.frapsoft.com/os/v3/open-source.png?v=103)](https://github.com/ellerbrock/open-source-badges/) Anonymous P2P platform, using Tor & I2P. -***Experimental, not safe or easy to use yet*** -
**The main repo for this software is at https://gitlab.com/beardog/Onionr/** @@ -45,4 +45,4 @@ Bitcoin/Bitcoin Cash: 1onion55FXzm6h8KQw3zFw2igpHcV7LPq The Tor Project, I2P developers, and anyone else do not own, create, or endorse this project, and are not otherwise involved. -The badges (besides travis-ci build) are by Maik Ellerbrock is licensed under a Creative Commons Attribution 4.0 International License. \ No newline at end of file +The badges (besides travis-ci build) are by Maik Ellerbrock is licensed under a Creative Commons Attribution 4.0 International License. diff --git a/onionr/api.py b/onionr/api.py index f4b4cbe9..6e637136 100755 --- a/onionr/api.py +++ b/onionr/api.py @@ -24,7 +24,7 @@ from gevent.pywsgi import WSGIServer import sys, random, threading, hmac, hashlib, base64, time, math, os, json import core from onionrblockapi import Block -import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config +import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config, onionr class API: ''' @@ -62,14 +62,9 @@ class API: } for mimetype in mimetypes: - logger.debug(path + ' endswith .' + mimetype + '?') if path.endswith('.%s' % mimetype): - logger.debug('- True!') return mimetypes[mimetype] - else: - logger.debug('- no') - logger.debug('%s not in %s' % (path, mimetypes)) return 'text/plain' def __init__(self, debug, API_VERSION): @@ -80,14 +75,8 @@ class API: This also saves the used host (random localhost IP address) to the data folder in host.txt ''' - config.reload() - - if config.get('dev_mode', True): - self._developmentMode = True - logger.set_level(logger.LEVEL_DEBUG) - else: - self._developmentMode = False - logger.set_level(logger.LEVEL_INFO) + # configure logger and stuff + onionr.Onionr.setupConfig('data/', self = self) self.debug = debug self._privateDelayTime = 3 @@ -138,14 +127,14 @@ class API: resp.headers["Content-Security-Policy"] = "default-src 'none'; script-src 'none'; object-src 'none'; style-src data: 'unsafe-inline'; img-src data:; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'none'" resp.headers['X-Frame-Options'] = 'deny' resp.headers['X-Content-Type-Options'] = "nosniff" - resp.headers['api'] = API_VERSION + resp.headers['X-API'] = API_VERSION # reset to text/plain to help prevent browser attacks self.mimeType = 'text/plain' self.overrideCSP = False return resp - + @app.route('/www/private/') def www_private(path): startTime = math.floor(time.time()) @@ -160,14 +149,17 @@ class API: self.validateHost('private') + if config.get('www.public.guess_mime', True): + self.mimeType = API.guessMime(path) + endTime = math.floor(time.time()) elapsed = endTime - startTime if not hmac.compare_digest(timingToken, self.timeBypassToken): - if elapsed < self._privateDelayTime: + if (elapsed < self._privateDelayTime) and config.get('www.private.timing_protection', True): time.sleep(self._privateDelayTime - elapsed) - return send_from_directory('static-data/www/private/', path) + return send_from_directory(config.get('www.private.path', 'static-data/www/private/'), path) @app.route('/www/public/') def www_public(path): @@ -176,7 +168,10 @@ class API: self.validateHost('public') - return send_from_directory('static-data/www/public/', path) + if config.get('www.public.guess_mime', True): + self.mimeType = API.guessMime(path) + + return send_from_directory(config.get('www.public.path', 'static-data/www/public/'), path) @app.route('/ui/') def ui_private(path): @@ -206,11 +201,11 @@ class API: time.sleep(self._privateDelayTime - elapsed) ''' - logger.debug('Serving %s' % path) - self.mimeType = API.guessMime(path) self.overrideCSP = True + logger.debug('Serving %s (mime: %s)' % (path, self.mimeType)) + return send_from_directory('static-data/www/ui/dist/', path, mimetype = API.guessMime(path)) @app.route('/client/') @@ -253,6 +248,16 @@ class API: resp = Response('Goodbye') elif action == 'ping': resp = Response('pong') + elif action == 'site': + block = data + siteData = self._core.getData(data) + response = 'not found' + if siteData != '' and siteData != False: + self.mimeType = 'text/html' + response = siteData.split(b'-', 2)[-1] + resp = Response(response) + elif action == 'info': + resp = Response(json.dumps({'pubkey' : self._core._crypto.pubKey, 'host' : self._core.hsAddress})) elif action == "insertBlock": response = {'success' : False, 'reason' : 'An unknown error occurred'} @@ -450,7 +455,7 @@ class API: else: logger.warn(newNode.decode() + ' failed to meet POW: ' + powHash) resp = Response(resp) - return resp + return resp @app.route('/public/') def public_handler(): diff --git a/onionr/communicator2.py b/onionr/communicator2.py index 3048b321..08527f8f 100755 --- a/onionr/communicator2.py +++ b/onionr/communicator2.py @@ -21,12 +21,14 @@ ''' import sys, os, core, config, json, requests, time, logger, threading, base64, onionr, uuid import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block -import onionrdaemontools, onionrsockets, onionrchat +import onionrdaemontools, onionrsockets, onionrchat, onionr from dependencies import secrets from defusedxml import minidom class OnionrCommunicatorDaemon: def __init__(self, debug, developmentMode): + # configure logger and stuff + onionr.Onionr.setupConfig('data/', self = self) self.isOnline = True # Assume we're connected to the internet @@ -87,9 +89,8 @@ class OnionrCommunicatorDaemon: # Set timers, function reference, seconds # requiresPeer True means the timer function won't fire if we have no connected peers - OnionrCommunicatorTimers(self, self.daemonCommands, 5) - OnionrCommunicatorTimers(self, self.detectAPICrash, 5) peerPoolTimer = OnionrCommunicatorTimers(self, self.getOnlinePeers, 60, maxThreads=1) + OnionrCommunicatorTimers(self, self.runCheck, 1) OnionrCommunicatorTimers(self, self.lookupBlocks, self._core.config.get('timers.lookupBlocks'), requiresPeer=True, maxThreads=1) OnionrCommunicatorTimers(self, self.getBlocks, self._core.config.get('timers.getBlocks'), requiresPeer=True) OnionrCommunicatorTimers(self, self.clearOfflinePeer, 58) @@ -134,7 +135,7 @@ class OnionrCommunicatorDaemon: def lookupAdders(self): '''Lookup new peer addresses''' - logger.info('LOOKING UP NEW ADDRESSES') + logger.info('Looking up new addresses...') tryAmount = 1 for i in range(tryAmount): # Download new peer address list from random online peers @@ -145,7 +146,7 @@ class OnionrCommunicatorDaemon: def lookupBlocks(self): '''Lookup new blocks & add them to download queue''' - logger.info('LOOKING UP NEW BLOCKS') + logger.info('Looking up new blocks...') tryAmount = 2 newBlocks = '' existingBlocks = self._core.getBlockList() @@ -176,7 +177,7 @@ class OnionrCommunicatorDaemon: try: newBlocks = self.peerAction(peer, 'getBlockHashes') # get list of new block hashes except Exception as error: - logger.warn("could not get new blocks with " + peer, error=error) + logger.warn('Could not get new blocks from %s.' % peer, error = error) newBlocks = False if newBlocks != False: # if request was a success @@ -200,10 +201,10 @@ class OnionrCommunicatorDaemon: break # Do not download blocks being downloaded or that are already saved (edge cases) if blockHash in self.currentDownloading: - logger.debug('ALREADY DOWNLOADING ' + blockHash) + logger.debug('Already downloading block %s...' % blockHash) continue if blockHash in self._core.getBlockList(): - logger.debug('%s is already saved' % (blockHash,)) + logger.debug('Block %s is already saved.' % (blockHash,)) self.blockQueue.remove(blockHash) continue if self._core._blacklist.inBlacklist(blockHash): @@ -232,22 +233,22 @@ class OnionrCommunicatorDaemon: #meta = metas[1] if self._core._utils.validateMetadata(metadata, metas[2]): # check if metadata is valid, and verify nonce if self._core._crypto.verifyPow(content): # check if POW is enough/correct - logger.info('Block passed proof, attempting save.') + logger.info('Attempting to save block %s...' % blockHash) try: self._core.setData(content) except onionrexceptions.DiskAllocationReached: - logger.error("Reached disk allocation allowance, cannot save this block.") + logger.error('Reached disk allocation allowance, cannot save block %s.' % blockHash) removeFromQueue = False else: self._core.addToBlockDB(blockHash, dataSaved=True) self._core._utils.processBlockMetadata(blockHash) # caches block metadata values to block database else: - logger.warn('POW failed for block ' + blockHash) + logger.warn('POW failed for block %s.' % blockHash) else: if self._core._blacklist.inBlacklist(realHash): - logger.warn('%s is blacklisted' % (realHash,)) + logger.warn('Block %s is blacklisted.' % (realHash,)) else: - logger.warn('Metadata for ' + blockHash + ' is invalid.') + logger.warn('Metadata for block %s is invalid.' % blockHash) self._core._blacklist.addToDB(blockHash) else: # if block didn't meet expected hash @@ -257,7 +258,7 @@ class OnionrCommunicatorDaemon: except AttributeError: pass # Punish peer for sharing invalid block (not always malicious, but is bad regardless) - onionrpeers.PeerProfiles(peerUsed, self._core).addScore(-50) + onionrpeers.PeerProfiles(peerUsed, self._core).addScore(-50) logger.warn('Block hash validation failed for ' + blockHash + ' got ' + tempHash) if removeFromQueue: try: @@ -303,10 +304,12 @@ class OnionrCommunicatorDaemon: self.decrementThreadCount('clearOfflinePeer') def getOnlinePeers(self): - '''Manages the self.onlinePeers attribute list, connects to more peers if we have none connected''' + ''' + Manages the self.onlinePeers attribute list, connects to more peers if we have none connected + ''' - logger.info('Refreshing peer pool.') - maxPeers = int(config.get('peers.maxConnect')) + logger.debug('Refreshing peer pool...') + maxPeers = int(config.get('peers.max_connect', 10)) needed = maxPeers - len(self.onlinePeers) for i in range(needed): @@ -318,13 +321,15 @@ class OnionrCommunicatorDaemon: break else: if len(self.onlinePeers) == 0: - logger.warn('Could not connect to any peer.') + logger.debug('Couldn\'t connect to any peers.') self.decrementThreadCount('getOnlinePeers') def addBootstrapListToPeerList(self, peerList): - '''Add the bootstrap list to the peer list (no duplicates)''' + ''' + Add the bootstrap list to the peer list (no duplicates) + ''' for i in self._core.bootstrapList: - if i not in peerList and i not in self.offlinePeers and i != self._core.hsAddress: + if i not in peerList and i not in self.offlinePeers and i != self._core.hsAddress and len(str(i).strip()) > 0: peerList.append(i) self._core.addAddress(i) @@ -339,7 +344,7 @@ class OnionrCommunicatorDaemon: raise onionrexceptions.InvalidAddress('Will not attempt connection test to invalid address') else: peerList = self._core.listAdders() - + peerList = onionrpeers.getScoreSortedPeerList(self._core) if len(peerList) == 0 or useBootstrap: @@ -347,7 +352,7 @@ class OnionrCommunicatorDaemon: self.addBootstrapListToPeerList(peerList) for address in peerList: - if not config.get('tor.v3onions') and len(address) == 62: + if not config.get('tor.v3_onions') and len(address) == 62: continue if len(address) == 0 or address in tried or address in self.onlinePeers or address in self.cooldownPeer: continue @@ -360,7 +365,7 @@ class OnionrCommunicatorDaemon: self.onlinePeers.append(address) self.connectTimes[address] = self._core._utils.getEpoch() retData = address - + # add peer to profile list if they're not in it for profile in self.peerProfiles: if profile.address == address: @@ -424,7 +429,7 @@ class OnionrCommunicatorDaemon: self._core.setAddressInfo(peer, 'lastConnect', self._core._utils.getEpoch()) self.getPeerProfileInstance(peer).addScore(1) return retData - + def getPeerProfileInstance(self, peer): '''Gets a peer profile instance from the list of profiles, by address name''' for i in self.peerProfiles: @@ -440,11 +445,13 @@ class OnionrCommunicatorDaemon: def heartbeat(self): '''Show a heartbeat debug message''' currentTime = self._core._utils.getEpoch() - self.startTime - logger.debug('heartbeat, running seconds: ' + str(currentTime)) + logger.debug('Heartbeat. Node online for %s.' % self.daemonTools.humanReadableTime(currentTime)) self.decrementThreadCount('heartbeat') def daemonCommands(self): - '''process daemon commands from daemonQueue''' + ''' + Process daemon commands from daemonQueue + ''' cmd = self._core.daemonQueue() if cmd is not False: @@ -457,7 +464,7 @@ class OnionrCommunicatorDaemon: self.announce(cmd[1]) else: logger.warn("Not introducing, since I have no connected nodes.") - elif cmd[0] == 'runCheck': + elif cmd[0] == 'runCheck': # deprecated logger.debug('Status check; looks good.') open(self._core.dataDir + '.runcheck', 'w+').close() elif cmd[0] == 'connectedPeers': @@ -538,6 +545,12 @@ class OnionrCommunicatorDaemon: self.shutdown = True self.decrementThreadCount('detectAPICrash') + def runCheck(self): + if self.daemonTools.runCheck(): + logger.debug('Status check; looks good.') + + self.decrementThreadCount('runCheck') + class OnionrCommunicatorTimers: def __init__(self, daemonInstance, timerFunction, frequency, makeThread=True, threadAmount=1, maxThreads=5, requiresPeer=False): self.timerFunction = timerFunction @@ -571,7 +584,7 @@ class OnionrCommunicatorTimers: if self.makeThread: for i in range(self.threadAmount): if self.daemonInstance.threadCounts[self.timerFunction.__name__] >= self.maxThreads: - logger.warn(self.timerFunction.__name__ + ' has too many current threads to start anymore.') + logger.warn('%s is currently using the maximum number of threads, not starting another.' % self.timerFunction.__name__) else: self.daemonInstance.threadCounts[self.timerFunction.__name__] += 1 newThread = threading.Thread(target=self.timerFunction) diff --git a/onionr/core.py b/onionr/core.py index cb209061..781c6527 100644 --- a/onionr/core.py +++ b/onionr/core.py @@ -23,6 +23,7 @@ from onionrblockapi import Block import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions, onionrvalues import onionrblacklist, onionrchat, onionrusers import dbcreator + if sys.version_info < (3, 6): try: import sha3 @@ -42,7 +43,7 @@ class Core: self.dataDir += '/' except KeyError: self.dataDir = 'data/' - + try: self.queueDB = self.dataDir + 'queue.db' self.peerDB = self.dataDir + 'peers.db' @@ -104,7 +105,10 @@ class Core: return def refreshFirstStartVars(self): - '''Hack to refresh some vars which may not be set on first start''' + ''' + Hack to refresh some vars which may not be set on first start + ''' + if os.path.exists(self.dataDir + '/hs/hostname'): with open(self.dataDir + '/hs/hostname', 'r') as hs: self.hsAddress = hs.read().strip() @@ -113,6 +117,7 @@ class Core: ''' Adds a public key to the key database (misleading function name) ''' + # This function simply adds a peer to the DB if not self._utils.validatePubKey(peerID): return False @@ -121,13 +126,13 @@ class Core: return False events.event('pubkey_add', data = {'key': peerID}, onionr = None) - + conn = sqlite3.connect(self.peerDB, timeout=10) hashID = self._crypto.pubKeyHashID(peerID) c = conn.cursor() t = (peerID, name, 'unknown', hashID, powID, 0) - for i in c.execute("SELECT * FROM PEERS where id = '" + peerID + "';"): + for i in c.execute("SELECT * FROM peers WHERE id = ?;", (peerID,)): try: if i[0] == peerID: conn.close() @@ -146,8 +151,8 @@ class Core: ''' Add an address to the address database (only tor currently) ''' - if address == config.get('i2p.ownAddr', None) or address == self.hsAddress: + if address == config.get('i2p.ownAddr', None) or address == self.hsAddress: return False if self._utils.validateID(address): conn = sqlite3.connect(self.addressDB, timeout=10) @@ -155,7 +160,7 @@ class Core: # check if address is in database # this is safe to do because the address is validated above, but we strip some chars here too just in case address = address.replace('\'', '').replace(';', '').replace('"', '').replace('\\', '') - for i in c.execute("SELECT * FROM adders where address = '" + address + "';"): + for i in c.execute("SELECT * FROM adders WHERE address = ?;", (address,)): try: if i[0] == address: conn.close() @@ -174,13 +179,14 @@ class Core: return True else: - logger.debug('Invalid ID') + logger.debug('Invalid ID: %s' % address) return False def removeAddress(self, address): ''' Remove an address from the address database ''' + if self._utils.validateID(address): conn = sqlite3.connect(self.addressDB, timeout=10) c = conn.cursor() @@ -200,6 +206,7 @@ class Core: **You may want blacklist.addToDB(blockHash) ''' + if self._utils.validateHash(block): conn = sqlite3.connect(self.blockDB, timeout=10) c = conn.cursor() @@ -207,10 +214,10 @@ class Core: c.execute('Delete from hashes where hash=?;', t) conn.commit() conn.close() - blockFile = self.dataDir + '/blocks/' + block + '.dat' + blockFile = self.dataDir + '/blocks/%s.dat' % block dataSize = 0 try: - ''' Get size of data when loaded as an object/var, rather than on disk, + ''' Get size of data when loaded as an object/var, rather than on disk, to avoid conflict with getsizeof when saving blocks ''' with open(blockFile, 'r') as data: @@ -224,18 +231,21 @@ class Core: ''' Generate the address database ''' + self.dbCreate.createAddressDB() def createPeerDB(self): ''' Generate the peer sqlite3 database and populate it with the peers table. ''' + self.dbCreate.createPeerDB() def createBlockDB(self): ''' Create a database for blocks ''' + self.dbCreate.createBlockDB() def addToBlockDB(self, newHash, selfInsert=False, dataSaved=False): @@ -244,6 +254,7 @@ class Core: Should be in hex format! ''' + if not os.path.exists(self.blockDB): raise Exception('Block db does not exist') if self._utils.hasBlock(newHash): @@ -266,6 +277,7 @@ class Core: ''' Simply return the data associated to a hash ''' + try: # logger.debug('Opening %s' % (str(self.blockDataLocation) + str(hash) + '.dat')) dataFile = open(self.blockDataLocation + hash + '.dat', 'rb') @@ -280,12 +292,13 @@ class Core: ''' Set the data assciated with a hash ''' + data = data dataSize = sys.getsizeof(data) if not type(data) is bytes: data = data.encode() - + dataHash = self._crypto.sha3Hash(data) if type(dataHash) is bytes: @@ -301,7 +314,7 @@ class Core: blockFile.close() conn = sqlite3.connect(self.blockDB, timeout=10) c = conn.cursor() - c.execute("UPDATE hashes SET dataSaved=1 WHERE hash = '" + dataHash + "';") + c.execute("UPDATE hashes SET dataSaved=1 WHERE hash = ?;", (dataHash,)) conn.commit() conn.close() with open(self.dataNonceFile, 'a') as nonceFile: @@ -317,6 +330,7 @@ class Core: 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): self.dbCreate.createDaemonDB() @@ -343,12 +357,15 @@ class Core: ''' Add a command to the daemon queue, used by the communication daemon (communicator.py) ''' + retData = True # Intended to be used by the web server + date = self._utils.getEpoch() conn = sqlite3.connect(self.queueDB, timeout=10) c = conn.cursor() t = (command, data, date) + try: c.execute('INSERT INTO commands (command, data, date) VALUES(?, ?, ?)', t) conn.commit() @@ -366,11 +383,13 @@ class Core: ''' conn = sqlite3.connect(self.queueDB, timeout=10) c = conn.cursor() + try: c.execute('DELETE FROM commands;') conn.commit() except: pass + conn.close() events.event('queue_clear', onionr = None) @@ -401,16 +420,21 @@ class Core: ''' conn = sqlite3.connect(self.peerDB, timeout=10) c = conn.cursor() - payload = "" + + payload = '' + if trust not in (0, 1, 2): logger.error('Tried to select invalid trust.') return + if randomOrder: - payload = 'SELECT * FROM peers where trust >= %s ORDER BY RANDOM();' % (trust,) + payload = 'SELECT * FROM peers WHERE trust >= ? ORDER BY RANDOM();' else: - payload = 'SELECT * FROM peers where trust >= %s;' % (trust,) + payload = 'SELECT * FROM peers WHERE trust >= ?;' + peerList = [] - for i in c.execute(payload): + + for i in c.execute(payload, (trust,)): try: if len(i[0]) != 0: if getPow: @@ -419,6 +443,7 @@ class Core: peerList.append(i[0]) except TypeError: pass + if getPow: try: peerList.append(self._crypto.pubKey + '-' + self._crypto.pubKeyPowToken) @@ -426,7 +451,9 @@ class Core: pass else: peerList.append(self._crypto.pubKey) + conn.close() + return peerList def getPeerInfo(self, peer, info): @@ -445,18 +472,22 @@ class Core: ''' conn = sqlite3.connect(self.peerDB, timeout=10) c = conn.cursor() + command = (peer,) + infoNumbers = {'id': 0, 'name': 1, 'adders': 2, 'dateSeen': 3, 'bytesStored': 4, 'trust': 5, 'pubkeyExchanged': 6, 'hashID': 7} info = infoNumbers[info] iterCount = 0 retVal = '' - for row in c.execute('SELECT * from peers where id=?;', command): + + for row in c.execute('SELECT * FROM peers WHERE id=?;', command): for i in row: if iterCount == info: retVal = i break else: iterCount += 1 + conn.close() return retVal @@ -465,15 +496,20 @@ class Core: ''' Update a peer for a key ''' + conn = sqlite3.connect(self.peerDB, timeout=10) c = conn.cursor() + command = (data, peer) + # TODO: validate key on whitelist if key not in ('id', 'name', 'pubkey', 'blockDBHash', 'forwardKey', 'dateSeen', 'bytesStored', 'trust'): raise Exception("Got invalid database key when setting peer info") + c.execute('UPDATE peers SET ' + key + ' = ? WHERE id=?', command) conn.commit() conn.close() + return def getAddressInfo(self, address, info): @@ -489,14 +525,17 @@ class Core: failure int 6 lastConnect 7 ''' + conn = sqlite3.connect(self.addressDB, timeout=10) c = conn.cursor() + command = (address,) infoNumbers = {'address': 0, 'type': 1, 'knownPeer': 2, 'speed': 3, 'success': 4, 'DBHash': 5, 'failure': 6, 'lastConnect': 7} info = infoNumbers[info] iterCount = 0 retVal = '' - for row in c.execute('SELECT * from adders where address=?;', command): + + for row in c.execute('SELECT * FROM adders WHERE address=?;', command): for i in row: if iterCount == info: retVal = i @@ -504,15 +543,19 @@ class Core: else: iterCount += 1 conn.close() + return retVal def setAddressInfo(self, address, key, data): ''' Update an address for a key ''' + conn = sqlite3.connect(self.addressDB, timeout=10) c = conn.cursor() + command = (data, address) + # TODO: validate key on whitelist if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'DBHash', 'failure', 'lastConnect', 'lastConnectAttempt'): raise Exception("Got invalid database key when setting address info") @@ -520,18 +563,22 @@ class Core: c.execute('UPDATE adders SET ' + key + ' = ? WHERE address=?', command) conn.commit() conn.close() + return def getBlockList(self, unsaved = False): # TODO: Use unsaved?? ''' Get list of our blocks ''' + conn = sqlite3.connect(self.blockDB, timeout=10) c = conn.cursor() + if unsaved: execute = 'SELECT hash FROM hashes WHERE dataSaved != 1 ORDER BY RANDOM();' else: execute = 'SELECT hash FROM hashes ORDER BY dateReceived ASC;' + rows = list() for row in c.execute(execute): for i in row: @@ -543,8 +590,10 @@ class Core: ''' Returns the date a block was received ''' + conn = sqlite3.connect(self.blockDB, timeout=10) c = conn.cursor() + execute = 'SELECT dateReceived FROM hashes WHERE hash=?;' args = (blockHash,) for row in c.execute(execute, args): @@ -557,17 +606,22 @@ class Core: ''' Returns a list of blocks by the type ''' + conn = sqlite3.connect(self.blockDB, timeout=10) c = conn.cursor() + if orderDate: execute = 'SELECT hash FROM hashes WHERE dataType=? ORDER BY dateReceived;' else: execute = 'SELECT hash FROM hashes WHERE dataType=?;' + args = (blockType,) rows = list() + for row in c.execute(execute, args): for i in row: rows.append(i) + return rows def getExpiredBlocks(self): @@ -582,7 +636,7 @@ class Core: for row in c.execute(execute): for i in row: rows.append(i) - return rows + return rows def setBlockType(self, hash, blockType): ''' @@ -591,9 +645,10 @@ class Core: conn = sqlite3.connect(self.blockDB, timeout=10) c = conn.cursor() - c.execute("UPDATE hashes SET dataType='" + blockType + "' WHERE hash = '" + hash + "';") + c.execute("UPDATE hashes SET dataType = ? WHERE hash = ?;", (blockType, hash)) conn.commit() conn.close() + return def updateBlockInfo(self, hash, key, data): @@ -621,6 +676,7 @@ class Core: c.execute("UPDATE hashes SET " + key + " = ? where hash = ?;", args) conn.commit() conn.close() + return True def insertBlock(self, data, header='txt', sign=False, encryptType='', symKey='', asymPeer='', meta = None, expire=None): @@ -628,6 +684,7 @@ class Core: Inserts a block into the network encryptType must be specified to encrypt a block ''' + retData = False # check nonce @@ -642,9 +699,6 @@ class Core: with open(self.dataNonceFile, 'a') as nonceFile: nonceFile.write(dataNonce + '\n') - if meta is None: - meta = dict() - if type(data) is bytes: data = data.decode() data = str(data) @@ -722,7 +776,7 @@ class Core: if type(expire) is not type(None): assert len(str(int(expire))) < 14 metadata['expire'] = expire - + # send block data (and metadata) to POW module to get tokenized block data proof = onionrproofs.POW(metadata, data) payload = proof.waitForResult() diff --git a/onionr/logger.py b/onionr/logger.py index 1c299054..7a8172b3 100644 --- a/onionr/logger.py +++ b/onionr/logger.py @@ -73,6 +73,7 @@ LEVEL_INFO = 2 LEVEL_WARN = 3 LEVEL_ERROR = 4 LEVEL_FATAL = 5 +LEVEL_IMPORTANT = 6 _type = OUTPUT_TO_CONSOLE | USE_ANSI # the default settings for logging _level = LEVEL_DEBUG # the lowest level to log @@ -123,18 +124,18 @@ def get_file(): return _outputfile -def raw(data, fd = sys.stdout): +def raw(data, fd = sys.stdout, sensitive = False): ''' Outputs raw data to console without formatting ''' if get_settings() & OUTPUT_TO_CONSOLE: ts = fd.write('%s\n' % data) - if get_settings() & OUTPUT_TO_FILE: + if get_settings() & OUTPUT_TO_FILE and not sensitive: with open(_outputfile, "a+") as f: f.write(colors.filter(data) + '\n') -def log(prefix, data, color = '', timestamp=True, fd = sys.stdout, prompt = True): +def log(prefix, data, color = '', timestamp=True, fd = sys.stdout, prompt = True, sensitive = False): ''' Logs the data prefix : The prefix to the output @@ -149,7 +150,7 @@ def log(prefix, data, color = '', timestamp=True, fd = sys.stdout, prompt = True if not get_settings() & USE_ANSI: output = colors.filter(output) - raw(output, fd = fd) + raw(output, fd = fd, sensitive = sensitive) def readline(message = ''): ''' @@ -201,37 +202,37 @@ def confirm(default = 'y', message = 'Are you sure %s? '): return default == 'y' # debug: when there is info that could be useful for debugging purposes only -def debug(data, error = None, timestamp = True, prompt = True): - if get_level() <= LEVEL_DEBUG: - log('/', data, timestamp=timestamp, prompt = prompt) +def debug(data, error = None, timestamp = True, prompt = True, sensitive = False, level = LEVEL_DEBUG): + if get_level() <= level: + log('/', data, timestamp = timestamp, prompt = prompt, sensitive = sensitive) if not error is None: debug('Error: ' + str(error) + parse_error()) # info: when there is something to notify the user of, such as the success of a process -def info(data, timestamp = False, prompt = True): - if get_level() <= LEVEL_INFO: - log('+', data, colors.fg.green, timestamp = timestamp, prompt = prompt) +def info(data, timestamp = False, prompt = True, sensitive = False, level = LEVEL_INFO): + if get_level() <= level: + log('+', data, colors.fg.green, timestamp = timestamp, prompt = prompt, sensitive = sensitive) # warn: when there is a potential for something bad to happen -def warn(data, error = None, timestamp = True, prompt = True): +def warn(data, error = None, timestamp = True, prompt = True, sensitive = False, level = LEVEL_WARN): if not error is None: debug('Error: ' + str(error) + parse_error()) - if get_level() <= LEVEL_WARN: - log('!', data, colors.fg.orange, timestamp = timestamp, prompt = prompt) + if get_level() <= level: + log('!', data, colors.fg.orange, timestamp = timestamp, prompt = prompt, sensitive = sensitive) # error: when only one function, module, or process of the program encountered a problem and must stop -def error(data, error = None, timestamp = True, prompt = True): - if get_level() <= LEVEL_ERROR: - log('-', data, colors.fg.red, timestamp = timestamp, fd = sys.stderr, prompt = prompt) +def error(data, error = None, timestamp = True, prompt = True, sensitive = False, level = LEVEL_ERROR): + if get_level() <= level: + log('-', data, colors.fg.red, timestamp = timestamp, fd = sys.stderr, prompt = prompt, sensitive = sensitive) if not error is None: debug('Error: ' + str(error) + parse_error()) # fatal: when the something so bad has happened that the program must stop -def fatal(data, error = None, timestamp=True, prompt = True): +def fatal(data, error = None, timestamp=True, prompt = True, sensitive = False, level = LEVEL_FATAL): if not error is None: - debug('Error: ' + str(error) + parse_error()) - if get_level() <= LEVEL_FATAL: - log('#', data, colors.bg.red + colors.fg.green + colors.bold, timestamp=timestamp, fd = sys.stderr, prompt = prompt) + debug('Error: ' + str(error) + parse_error(), sensitive = sensitive) + if get_level() <= level: + log('#', data, colors.bg.red + colors.fg.green + colors.bold, timestamp = timestamp, fd = sys.stderr, prompt = prompt, sensitive = sensitive) # returns a formatted error message def parse_error(): diff --git a/onionr/netcontroller.py b/onionr/netcontroller.py index 97e73296..a6141aeb 100644 --- a/onionr/netcontroller.py +++ b/onionr/netcontroller.py @@ -65,7 +65,7 @@ class NetController: Generate a torrc file for our tor instance ''' hsVer = '# v2 onions' - if config.get('tor.v3onions'): + if config.get('tor.v3_onions'): hsVer = 'HiddenServiceVersion 3' logger.info('Using v3 onions :)') @@ -141,7 +141,7 @@ HashedControlPassword ''' + str(password) + ''' logger.fatal('Failed to start Tor. Maybe a stray instance of Tor used by Onionr is still running?') return False except KeyboardInterrupt: - logger.fatal("Got keyboard interrupt.") + logger.fatal('Got keyboard interrupt.', timestamp = false, level = logger.LEVEL_IMPORTANT) return False logger.debug('Finished starting Tor.', timestamp=True) diff --git a/onionr/onionr.py b/onionr/onionr.py index e886d4d1..d7117421 100755 --- a/onionr/onionr.py +++ b/onionr/onionr.py @@ -40,7 +40,7 @@ except ImportError: raise Exception("You need the PySocks module (for use with socks5 proxy to use Tor)") ONIONR_TAGLINE = 'Anonymous P2P Platform - GPLv3 - https://Onionr.VoidNet.Tech' -ONIONR_VERSION = '0.3.1' # for debugging and stuff +ONIONR_VERSION = '0.3.2' # for debugging and stuff ONIONR_VERSION_TUPLE = tuple(ONIONR_VERSION.split('.')) # (MAJOR, MINOR, VERSION) API_VERSION = '5' # increments of 1; only change when something fundemental about how the API works changes. This way other nodes know how to communicate without learning too much information about you. @@ -64,37 +64,7 @@ class Onionr: self.dataDir = 'data/' # Load global configuration data - - data_exists = os.path.exists(self.dataDir) - - if not data_exists: - os.mkdir(self.dataDir) - - if os.path.exists('static-data/default_config.json'): - config.set_config(json.loads(open('static-data/default_config.json').read())) # this is the default config, it will be overwritten if a config file already exists. Else, it saves it - else: - # the default config file doesn't exist, try hardcoded config - config.set_config({'dev_mode': True, 'log': {'file': {'output': True, 'path': self.dataDir + 'output.log'}, 'console': {'output': True, 'color': True}}}) - if not data_exists: - config.save() - config.reload() # this will read the configuration file into memory - - settings = 0b000 - if config.get('log.console.color', True): - settings = settings | logger.USE_ANSI - if config.get('log.console.output', True): - settings = settings | logger.OUTPUT_TO_CONSOLE - if config.get('log.file.output', True): - settings = settings | logger.OUTPUT_TO_FILE - logger.set_file(config.get('log.file.path', '/tmp/onionr.log').replace('data/', self.dataDir)) - logger.set_settings(settings) - - if str(config.get('general.dev_mode', True)).lower() == 'true': - self._developmentMode = True - logger.set_level(logger.LEVEL_DEBUG) - else: - self._developmentMode = False - logger.set_level(logger.LEVEL_INFO) + data_exists = Onionr.setupConfig(self.dataDir, self = self) self.onionrCore = core.Core() self.onionrUtils = onionrutils.OnionrUtils(self.onionrCore) @@ -144,7 +114,7 @@ class Onionr: config.set('client.participate', True, savefile=True) if type(config.get('client.api_version')) is type(None): config.set('client.api_version', API_VERSION, savefile=True) - + self.cmds = { '': self.showHelpSuggestion, @@ -156,6 +126,16 @@ class Onionr: 'status': self.showStats, 'statistics': self.showStats, 'stats': self.showStats, + 'details' : self.showDetails, + 'detail' : self.showDetails, + 'show-details' : self.showDetails, + 'show-detail' : self.showDetails, + 'showdetails' : self.showDetails, + 'showdetail' : self.showDetails, + 'get-details' : self.showDetails, + 'get-detail' : self.showDetails, + 'getdetails' : self.showDetails, + 'getdetail' : self.showDetails, 'enable-plugin': self.enablePlugin, 'enplugin': self.enablePlugin, @@ -204,6 +184,7 @@ class Onionr: 'ui' : self.openUI, 'gui' : self.openUI, + 'chat': self.startChat, 'getpassword': self.printWebPassword, 'get-password': self.printWebPassword, @@ -223,14 +204,18 @@ class Onionr: 'help': 'Displays this Onionr help menu', 'version': 'Displays the Onionr version', 'config': 'Configures something and adds it to the file', + 'start': 'Starts the Onionr daemon', 'stop': 'Stops the Onionr daemon', + 'stats': 'Displays node statistics', - 'get-password': 'Displays the web password', + 'details': 'Displays the web password, public key, and human readable public key', + 'enable-plugin': 'Enables and starts a plugin', 'disable-plugin': 'Disables and stops a plugin', 'reload-plugin': 'Reloads a plugin', 'create-plugin': 'Creates directory structure for a plugin', + 'add-peer': 'Adds a peer to database', 'list-peers': 'Displays a list of peers', 'add-file': 'Create an Onionr block from a file', @@ -261,6 +246,17 @@ class Onionr: THIS SECTION HANDLES THE COMMANDS ''' + def showDetails(self): + details = { + 'Node Address' : self.get_hostname(), + 'Web Password' : self.getWebPassword(), + 'Public Key' : self.onionrCore._crypto.pubKey, + 'Human-readable Public Key' : self.onionrCore._utils.getHumanReadableID() + } + + for detail in details: + logger.info('%s%s: \n%s%s\n' % (logger.colors.fg.lightgreen, detail, logger.colors.fg.green, details[detail]), sensitive = True) + def startChat(self): try: data = json.dumps({'peer': sys.argv[2], 'reason': 'chat'}) @@ -271,7 +267,49 @@ class Onionr: def getCommands(self): return self.cmds - + + def friendCmd(self): + '''List, add, or remove friend(s) + Changes their peer DB entry. + ''' + friend = '' + try: + # Get the friend command + action = sys.argv[2] + except IndexError: + logger.info('Syntax: friend add/remove/list [address]') + else: + action = action.lower() + if action == 'list': + # List out peers marked as our friend + for friend in self.onionrCore.listPeers(randomOrder=False, trust=1): + if friend == self.onionrCore._crypto.pubKey: # do not list our key + continue + friendProfile = onionrusers.OnionrUser(self.onionrCore, friend) + logger.info(friend + ' - ' + friendProfile.getName()) + elif action in ('add', 'remove'): + try: + friend = sys.argv[3] + if not self.onionrUtils.validatePubKey(friend): + raise onionrexceptions.InvalidPubkey('Public key is invalid') + if friend not in self.onionrCore.listPeers(): + raise onionrexceptions.KeyNotKnown + friend = onionrusers.OnionrUser(self.onionrCore, friend) + except IndexError: + logger.error('Friend ID is required.') + except onionrexceptions.KeyNotKnown: + logger.error('That peer is not in our database') + else: + if action == 'add': + friend.setTrust(1) + logger.info('Added %s as friend.' % (friend.publicKey,)) + else: + friend.setTrust(0) + logger.info('Removed %s as friend.' % (friend.publicKey,)) + else: + logger.info('Syntax: friend add/remove/list [address]') + + def friendCmd(self): '''List, add, or remove friend(s) Changes their peer DB entry. @@ -346,7 +384,7 @@ class Onionr: return config.get('client.hmac') def printWebPassword(self): - print(self.getWebPassword()) + logger.info(self.getWebPassword(), sensitive = True) def getHelp(self): return self.cmdhelp @@ -399,16 +437,16 @@ class Onionr: THIS SECTION DEFINES THE COMMANDS ''' - def version(self, verbosity=5): + def version(self, verbosity = 5, function = logger.info): ''' Displays the Onionr version ''' - logger.info('Onionr %s (%s) - API v%s' % (ONIONR_VERSION, platform.machine(), API_VERSION)) + function('Onionr v%s (%s) (API v%s)' % (ONIONR_VERSION, platform.machine(), API_VERSION)) if verbosity >= 1: - logger.info(ONIONR_TAGLINE) + function(ONIONR_TAGLINE) if verbosity >= 2: - logger.info('Running on %s %s' % (platform.platform(), platform.release())) + function('Running on %s %s' % (platform.platform(), platform.release())) return @@ -427,9 +465,7 @@ class Onionr: Displays a list of keys (used to be called peers) (?) ''' - logger.info('Public keys in database:\n') - for i in self.onionrCore.listPeers(): - logger.info(i) + logger.info('%sPublic keys in database: \n%s%s' % (logger.colors.fg.lightgreen, logger.colors.fg.green, '\n'.join(self.onionrCore.listPeers()))) def addPeer(self): ''' @@ -618,37 +654,56 @@ class Onionr: ''' Starts the Onionr communication daemon ''' + communicatorDaemon = './communicator2.py' - apiThread = Thread(target=api.API, args=(self.debug,API_VERSION)) + # remove runcheck if it exists + if os.path.isfile('data/.runcheck'): + logger.debug('Runcheck file found on daemon start, deleting in advance.') + os.remove('data/.runcheck') + + apiThread = Thread(target = api.API, args = (self.debug, API_VERSION)) apiThread.start() + try: time.sleep(3) except KeyboardInterrupt: - logger.info('Got keyboard interrupt') + logger.debug('Got keyboard interrupt, shutting down...') time.sleep(1) self.onionrUtils.localCommand('shutdown') else: if apiThread.isAlive(): + # configure logger and stuff + Onionr.setupConfig('data/', self = self) + if self._developmentMode: logger.warn('DEVELOPMENT MODE ENABLED (THIS IS LESS SECURE!)', timestamp = False) net = NetController(config.get('client.port', 59496)) - logger.info('Tor is starting...') + logger.debug('Tor is starting...') if not net.startTor(): sys.exit(1) - logger.info('Started .onion service: ' + logger.colors.underline + net.myID) - logger.info('Our Public key: ' + self.onionrCore._crypto.pubKey) + logger.debug('Started .onion service: %s' % (logger.colors.underline + net.myID)) + logger.debug('Using public key: %s' % (logger.colors.underline + self.onionrCore._crypto.pubKey)) time.sleep(1) - #TODO make runable on windows - communicatorProc = subprocess.Popen([communicatorDaemon, "run", str(net.socksPort)]) - # Print nice header thing :) + + # TODO: make runable on windows + communicatorProc = subprocess.Popen([communicatorDaemon, 'run', str(net.socksPort)]) + + # print nice header thing :) if config.get('general.display_header', True): self.header() - logger.debug('Started communicator') + + # print out debug info + self.version(verbosity = 5, function = logger.debug) + logger.debug('Python version %s' % platform.python_version()) + + logger.debug('Started communicator.') + events.event('daemon_start', onionr = self) try: while True: time.sleep(5) + # Break if communicator process ends, so we don't have left over processes if communicatorProc.poll() is not None: break @@ -689,9 +744,6 @@ class Onionr: messages = { # info about local client 'Onionr Daemon Status' : ((logger.colors.fg.green + 'Online') if self.onionrUtils.isCommunicatorRunning(timeout = 9) else logger.colors.fg.red + 'Offline'), - 'Public Key' : self.onionrCore._crypto.pubKey, - 'Human readable public key' : self.onionrCore._utils.getHumanReadableID(), - 'Node Address' : self.get_hostname(), # file and folder size stats 'div1' : True, # this creates a solid line across the screen, a div @@ -798,7 +850,8 @@ class Onionr: except IndexError: logger.error("Syntax %s %s" % (sys.argv[0], '/path/to/filename ')) else: - print(fileName) + logger.info(fileName) + contents = None if os.path.exists(fileName): logger.error("File already exists") @@ -806,6 +859,7 @@ class Onionr: if not self.onionrUtils.validateHash(bHash): logger.error('Block hash is invalid') return + Block.mergeChain(bHash, fileName) return @@ -830,17 +884,83 @@ class Onionr: else: logger.error('%s add-file ' % sys.argv[0], timestamp = False) + def setupConfig(dataDir, self = None): + data_exists = os.path.exists(dataDir) + + if not data_exists: + os.mkdir(dataDir) + + if os.path.exists('static-data/default_config.json'): + config.set_config(json.loads(open('static-data/default_config.json').read())) # this is the default config, it will be overwritten if a config file already exists. Else, it saves it + else: + # the default config file doesn't exist, try hardcoded config + logger.warn('Default configuration file does not exist, switching to hardcoded fallback configuration!') + config.set_config({'dev_mode': True, 'log': {'file': {'output': True, 'path': dataDir + 'output.log'}, 'console': {'output': True, 'color': True}}}) + if not data_exists: + config.save() + config.reload() # this will read the configuration file into memory + + settings = 0b000 + if config.get('log.console.color', True): + settings = settings | logger.USE_ANSI + if config.get('log.console.output', True): + settings = settings | logger.OUTPUT_TO_CONSOLE + if config.get('log.file.output', True): + settings = settings | logger.OUTPUT_TO_FILE + logger.set_file(config.get('log.file.path', '/tmp/onionr.log').replace('data/', dataDir)) + logger.set_settings(settings) + + if not self is None: + if str(config.get('general.dev_mode', True)).lower() == 'true': + self._developmentMode = True + logger.set_level(logger.LEVEL_DEBUG) + else: + self._developmentMode = False + logger.set_level(logger.LEVEL_INFO) + + verbosity = str(config.get('log.verbosity', 'default')).lower().strip() + if not verbosity in ['default', 'null', 'none', 'nil']: + map = { + str(logger.LEVEL_DEBUG) : logger.LEVEL_DEBUG, + 'verbose' : logger.LEVEL_DEBUG, + 'debug' : logger.LEVEL_DEBUG, + str(logger.LEVEL_INFO) : logger.LEVEL_INFO, + 'info' : logger.LEVEL_INFO, + 'information' : logger.LEVEL_INFO, + str(logger.LEVEL_WARN) : logger.LEVEL_WARN, + 'warn' : logger.LEVEL_WARN, + 'warning' : logger.LEVEL_WARN, + 'warnings' : logger.LEVEL_WARN, + str(logger.LEVEL_ERROR) : logger.LEVEL_ERROR, + 'err' : logger.LEVEL_ERROR, + 'error' : logger.LEVEL_ERROR, + 'errors' : logger.LEVEL_ERROR, + str(logger.LEVEL_FATAL) : logger.LEVEL_FATAL, + 'fatal' : logger.LEVEL_FATAL, + str(logger.LEVEL_IMPORTANT) : logger.LEVEL_IMPORTANT, + 'silent' : logger.LEVEL_IMPORTANT, + 'quiet' : logger.LEVEL_IMPORTANT, + 'important' : logger.LEVEL_IMPORTANT + } + + if verbosity in map: + logger.set_level(map[verbosity]) + else: + logger.warn('Verbosity level %s is not valid, using default verbosity.' % verbosity) + + return data_exists + def openUI(self): url = 'http://127.0.0.1:%s/ui/index.html?timingToken=%s' % (config.get('client.port', 59496), self.onionrUtils.getTimeBypassToken()) - print('Opening %s ...' % url) + logger.info('Opening %s ...' % url) webbrowser.open(url, new = 1, autoraise = True) def header(self, message = logger.colors.fg.pink + logger.colors.bold + 'Onionr' + logger.colors.reset + logger.colors.fg.pink + ' has started.'): - if os.path.exists('static-data/header.txt'): + if os.path.exists('static-data/header.txt') and logger.get_level() <= logger.LEVEL_INFO: with open('static-data/header.txt', 'rb') as file: # only to stdout, not file or log or anything - sys.stderr.write(file.read().decode().replace('P', logger.colors.fg.pink).replace('W', logger.colors.reset + logger.colors.bold).replace('G', logger.colors.fg.green).replace('\n', logger.colors.reset + '\n').replace('B', logger.colors.bold).replace('V', ONIONR_VERSION)) + sys.stderr.write(file.read().decode().replace('P', logger.colors.fg.pink).replace('W', logger.colors.reset + logger.colors.bold).replace('G', logger.colors.fg.green).replace('\n', logger.colors.reset + '\n').replace('B', logger.colors.bold).replace('A', '%s' % API_VERSION).replace('V', ONIONR_VERSION)) logger.info(logger.colors.fg.lightgreen + '-> ' + str(message) + logger.colors.reset + logger.colors.fg.lightgreen + ' <-\n') if __name__ == "__main__": diff --git a/onionr/onionrblacklist.py b/onionr/onionrblacklist.py index 1d7e83f7..8736f78f 100644 --- a/onionr/onionrblacklist.py +++ b/onionr/onionrblacklist.py @@ -22,34 +22,37 @@ class OnionrBlackList: def __init__(self, coreInst): self.blacklistDB = coreInst.dataDir + 'blacklist.db' self._core = coreInst - + if not os.path.exists(self.blacklistDB): self.generateDB() return - + def inBlacklist(self, data): hashed = self._core._utils.bytesToStr(self._core._crypto.sha3Hash(data)) retData = False + if not hashed.isalnum(): raise Exception("Hashed data is not alpha numeric") if len(hashed) > 64: raise Exception("Hashed data is too large") - for i in self._dbExecute("select * from blacklist where hash='%s'" % (hashed,)): + + for i in self._dbExecute("SELECT * FROM blacklist WHERE hash = ?", (hashed,)): retData = True # this only executes if an entry is present by that hash break + return retData - def _dbExecute(self, toExec): + def _dbExecute(self, toExec, params = ()): conn = sqlite3.connect(self.blacklistDB) c = conn.cursor() - retData = c.execute(toExec) + retData = c.execute(toExec, params) conn.commit() return retData - + def deleteBeforeDate(self, date): # TODO, delete blacklist entries before date return - + def deleteExpired(self, dataType=0): '''Delete expired entries''' deleteList = [] @@ -60,13 +63,13 @@ class OnionrBlackList: except AttributeError: raise TypeError("dataType must be int") - for i in self._dbExecute('select * from blacklist where dataType=%s' % (dataType,)): + for i in self._dbExecute('SELECT * FROM blacklist WHERE dataType = ?', (dataType,)): if i[1] == dataType: if (curTime - i[2]) >= i[3]: deleteList.append(i[0]) - + for thing in deleteList: - self._dbExecute("delete from blacklist where hash='%s'" % (thing,)) + self._dbExecute("DELETE FROM blacklist WHERE hash = ?", (thing,)) def generateDB(self): self._dbExecute('''CREATE TABLE blacklist( @@ -77,12 +80,12 @@ class OnionrBlackList: ); ''') return - + def clearDB(self): - self._dbExecute('''delete from blacklist;);''') + self._dbExecute('''DELETE FROM blacklist;);''') def getList(self): - data = self._dbExecute('select * from blacklist') + data = self._dbExecute('SELECT * FROM blacklist') myList = [] for i in data: myList.append(i[0]) @@ -113,4 +116,4 @@ class OnionrBlackList: return insert = (hashed,) blacklistDate = self._core._utils.getEpoch() - self._dbExecute("insert into blacklist (hash, dataType, blacklistDate, expire) VALUES('%s', %s, %s, %s);" % (hashed, dataType, blacklistDate, expire)) + self._dbExecute("INSERT INTO blacklist (hash, dataType, blacklistDate, expire) VALUES(?, ?, ?, ?);", (str(hashed), dataType, blacklistDate, expire)) diff --git a/onionr/onionrblockapi.py b/onionr/onionrblockapi.py index dbc05a0a..e6506faf 100644 --- a/onionr/onionrblockapi.py +++ b/onionr/onionrblockapi.py @@ -73,6 +73,7 @@ class Block: ''' Decrypt a block, loading decrypted data into their vars ''' + if self.decrypted: return True retData = False @@ -114,6 +115,7 @@ class Block: ''' 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): @@ -173,7 +175,7 @@ class Block: self.raw = str(blockdata) self.bheader = json.loads(self.getRaw()[:self.getRaw().index('\n')]) self.bcontent = self.getRaw()[self.getRaw().index('\n') + 1:] - if self.bheader['encryptType'] in ('asym', 'sym'): + if ('encryptType' in self.bheader) and (self.bheader['encryptType'] in ('asym', 'sym')): self.bmetadata = self.getHeader('meta', None) self.isEncrypted = True else: @@ -199,7 +201,13 @@ class Block: return True except Exception as e: - logger.error('Failed to update block data.', error = e, timestamp = False) + logger.error('Failed to parse block %s.' % self.getHash(), error = e, timestamp = False) + + # if block can't be parsed, it's a waste of precious space. Throw it away. + if not self.delete(): + logger.error('Failed to delete invalid block %s.' % self.getHash(), error = e) + else: + logger.debug('Deleted invalid block %s.' % self.getHash(), timestamp = False) self.valid = False return False @@ -214,7 +222,7 @@ class Block: if self.exists(): os.remove(self.getBlockFile()) - removeBlock(self.getHash()) + self.getCore().removeBlock(self.getHash()) return True return False @@ -235,10 +243,10 @@ class Block: if (not self.getBlockFile() is None) and (recreate is True): with open(self.getBlockFile(), 'wb') as blockFile: blockFile.write(self.getRaw().encode()) - self.update() else: - self.hash = self.getCore().insertBlock(self.getContent(), header = self.getType(), sign = sign, expire=self.getExpire()) - self.update() + self.hash = self.getCore().insertBlock(self.getContent(), header = self.getType(), sign = sign, meta = self.getMetadata(), expire = self.getExpire()) + + self.update() return self.getHash() else: @@ -782,7 +790,7 @@ class Block: return False # dump old cached blocks if the size exeeds the maximum - if sys.getsizeof(Block.blockCacheOrder) >= config.get('allocations.blockCacheTotal', 50000000): # 50MB default cache size + if sys.getsizeof(Block.blockCacheOrder) >= config.get('allocations.block_cache_total', 50000000): # 50MB default cache size del Block.blockCache[blockCacheOrder.pop(0)] # cache block content diff --git a/onionr/onionrchat.py b/onionr/onionrchat.py index 80569e6e..84483295 100644 --- a/onionr/onionrchat.py +++ b/onionr/onionrchat.py @@ -46,4 +46,5 @@ class OnionrChat: self.communicator.socketClient.sendData(peer, "lol") except: pass - time.sleep(2) \ No newline at end of file + + time.sleep(2) diff --git a/onionr/onionrdaemontools.py b/onionr/onionrdaemontools.py index cac06f9f..28d7f964 100644 --- a/onionr/onionrdaemontools.py +++ b/onionr/onionrdaemontools.py @@ -17,8 +17,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . ''' -import onionrexceptions, onionrpeers, onionrproofs, base64, logger, onionrusers, sqlite3 + +import onionrexceptions, onionrpeers, onionrproofs, logger, onionrusers +import base64, sqlite3, os from dependencies import secrets + class DaemonTools: def __init__(self, daemon): self.daemon = daemon @@ -64,7 +67,7 @@ class DaemonTools: logger.warn('Network check failed, are you connected to the internet?') self.daemon.isOnline = False self.daemon.decrementThreadCount('netCheck') - + def cleanOldBlocks(self): '''Delete old blocks if our disk allocation is full/near full, and also expired blocks''' @@ -73,22 +76,25 @@ class DaemonTools: self.daemon._core._blacklist.addToDB(oldest) self.daemon._core.removeBlock(oldest) logger.info('Deleted block: %s' % (oldest,)) + # Delete expired blocks for bHash in self.daemon._core.getExpiredBlocks(): self.daemon._core._blacklist.addToDB(bHash) self.daemon._core.removeBlock(bHash) + self.daemon.decrementThreadCount('cleanOldBlocks') - + def cleanKeys(self): '''Delete expired forward secrecy keys''' conn = sqlite3.connect(self.daemon._core.peerDB, timeout=10) c = conn.cursor() time = self.daemon._core._utils.getEpoch() deleteKeys = [] - for entry in c.execute("SELECT * FROM forwardKeys where expire <= ?", (time,)): + + for entry in c.execute("SELECT * FROM forwardKeys WHERE expire <= ?", (time,)): logger.info(entry[1]) deleteKeys.append(entry[1]) - + for key in deleteKeys: logger.info('Deleting forward key '+ key) c.execute("DELETE from forwardKeys where forwardKey = ?", (key,)) @@ -114,8 +120,9 @@ class DaemonTools: del self.daemon.cooldownPeer[peer] # Cool down a peer, if we have max connections alive for long enough - if onlinePeerAmount >= self.daemon._core.config.get('peers.maxConnect'): + if onlinePeerAmount >= self.daemon._core.config.get('peers.max_connect', 10): finding = True + while finding: try: toCool = min(tempConnectTimes, key=tempConnectTimes.get) @@ -128,4 +135,32 @@ class DaemonTools: else: self.daemon.removeOnlinePeer(toCool) self.daemon.cooldownPeer[toCool] = self.daemon._core._utils.getEpoch() - self.daemon.decrementThreadCount('cooldownPeer') \ No newline at end of file + + self.daemon.decrementThreadCount('cooldownPeer') + + def runCheck(self): + if os.path.isfile('data/.runcheck'): + os.remove('data/.runcheck') + return True + + return False + + def humanReadableTime(self, seconds): + build = '' + + units = { + 'year' : 31557600, + 'month' : (31557600 / 12), + 'day' : 86400, + 'hour' : 3600, + 'minute' : 60, + 'second' : 1 + } + + for unit in units: + amnt_unit = int(seconds / units[unit]) + if amnt_unit >= 1: + seconds -= amnt_unit * units[unit] + build += '%s %s' % (amnt_unit, unit) + ('s' if amnt_unit != 1 else '') + ' ' + + return build.strip() diff --git a/onionr/onionrpeers.py b/onionr/onionrpeers.py index 322db9ba..62716eee 100644 --- a/onionr/onionrpeers.py +++ b/onionr/onionrpeers.py @@ -44,7 +44,7 @@ class PeerProfiles: except (TypeError, ValueError) as e: self.success = 0 self.score = self.success - + def saveScore(self): '''Save the node's score to the database''' self.coreInst.setAddressInfo(self.address, 'success', self.score) @@ -79,8 +79,8 @@ def peerCleanup(coreInst): logger.info('Cleaning peers...') config.reload() - minScore = int(config.get('peers.minimumScore')) - maxPeers = int(config.get('peers.maxStoredPeers')) + minScore = int(config.get('peers.minimum_score', -100)) + maxPeers = int(config.get('peers.max_stored', 5000)) adders = getScoreSortedPeerList(coreInst) adders.reverse() @@ -102,4 +102,4 @@ def peerCleanup(coreInst): logger.warn('Removed address ' + address + '.') # Unban probably not malicious peers TODO improve - coreInst._blacklist.deleteExpired(dataType=1) \ No newline at end of file + coreInst._blacklist.deleteExpired(dataType=1) diff --git a/onionr/onionrplugins.py b/onionr/onionrplugins.py index ce038856..aa45dcca 100644 --- a/onionr/onionrplugins.py +++ b/onionr/onionrplugins.py @@ -77,7 +77,7 @@ def enable(name, onionr = None, start_event = True): else: enabled_plugins.append(name) config.set('plugins.enabled', enabled_plugins, True) - + if start_event is True: start(name) return True @@ -234,7 +234,7 @@ def check(): config.reload() if not config.is_set('plugins'): - logger.debug('Generating plugin config data...') + logger.debug('Generating plugin configuration data...') config.set('plugins', {'enabled': []}, True) if not os.path.exists(os.path.dirname(get_plugins_folder())): diff --git a/onionr/onionrsockets.py b/onionr/onionrsockets.py index f20ad4f8..aa02c3db 100644 --- a/onionr/onionrsockets.py +++ b/onionr/onionrsockets.py @@ -72,7 +72,7 @@ class OnionrSocketServer: self._core.socketServerResponseData[myPeer] = '' return retData - + def socketStarter(self): while not self._core.killSockets: try: @@ -87,14 +87,15 @@ class OnionrSocketServer: def detectShutdown(self): while not self._core.killSockets: time.sleep(5) - logger.info('Killing socket server') + + logger.debug('Killing socket server...') self.http_server.stop() def addSocket(self, peer, reason=''): bindPort = 1337 assert len(reason) <= 12 - + with stem.control.Controller.from_port(port=config.get('tor.controlPort')) as controller: controller.authenticate(config.get('tor.controlpassword')) @@ -106,7 +107,7 @@ class OnionrSocketServer: self._core.insertBlock(str(uuid.uuid4()), header='socket', sign=True, encryptType='asym', asymPeer=peer, meta={'reason': reason, 'address': socket.service_id + '.onion'}) self._core.socketReasons[peer] = reason return - + class OnionrSocketClient: def __init__(self, coreInst): self.sockets = {} # pubkey: tor address @@ -158,7 +159,7 @@ class OnionrSocketClient: postData = {'data': data} self.connPool[peer] = {'date': self._core._utils.getEpoch(), 'data': self._core._utils.doPostRequest('http://' + address + '/dc/', data=postData)} time.sleep(2) - + def getResponse(self, peer): retData = '' try: @@ -166,6 +167,6 @@ class OnionrSocketClient: except KeyError: pass return - + def sendData(self, peer, data): - self.sendData[peer] = data \ No newline at end of file + self.sendData[peer] = data diff --git a/onionr/onionrusers.py b/onionr/onionrusers.py index 93ecd6b0..b9bc3c2c 100644 --- a/onionr/onionrusers.py +++ b/onionr/onionrusers.py @@ -40,7 +40,7 @@ class OnionrUser: self.trust = self._core.getPeerInfo(self.publicKey, 'trust') return - + def setTrust(self, newTrust): '''Set the peers trust. 0 = not trusted, 1 = friend, 2 = ultimate''' self._core.setPeerInfo(self.publicKey, 'trust', newTrust) @@ -49,7 +49,7 @@ class OnionrUser: if self._core.getPeerInfo(self.publicKey, 'trust') == 1: return True return False - + def getName(self): retData = 'anonymous' name = self._core.getPeerInfo(self.publicKey, 'name') @@ -63,11 +63,11 @@ class OnionrUser: def encrypt(self, data): encrypted = coreInst._crypto.pubKeyEncrypt(data, self.publicKey, encodedData=True) return encrypted - + def decrypt(self, data, anonymous=True): decrypted = coreInst._crypto.pubKeyDecrypt(data, self.publicKey, encodedData=True) return decrypted - + def forwardEncrypt(self, data): retData = '' forwardKey = self._getLatestForwardKey() @@ -78,7 +78,7 @@ class OnionrUser: raise onionrexceptions.InvalidPubkey("No valid forward key available for this user") #self.generateForwardKey() return (retData, forwardKey) - + def forwardDecrypt(self, encrypted): retData = "" #logger.error(self.publicKey) @@ -101,19 +101,21 @@ class OnionrUser: conn = sqlite3.connect(self._core.peerDB, timeout=10) c = conn.cursor() - for row in c.execute("SELECT forwardKey FROM forwardKeys WHERE peerKey = ? order by date desc", (self.publicKey,)): + for row in c.execute("SELECT forwardKey FROM forwardKeys WHERE peerKey = ? ORDER BY date DESC", (self.publicKey,)): key = row[0] break conn.commit() conn.close() + return key - + def _getForwardKeys(self): conn = sqlite3.connect(self._core.peerDB, timeout=10) c = conn.cursor() keyList = [] - for row in c.execute("SELECT forwardKey FROM forwardKeys WHERE peerKey = ? order by date desc", (self.publicKey,)): + + for row in c.execute("SELECT forwardKey FROM forwardKeys WHERE peerKey = ? ORDER BY date DESC", (self.publicKey,)): key = row[0] keyList.append(key) @@ -150,8 +152,10 @@ class OnionrUser: pubkey = self._core._utils.bytesToStr(pubkey) command = (pubkey,) keyList = [] # list of tuples containing pub, private for peer - for result in c.execute("SELECT * FROM myForwardKeys where peer=?", command): + + for result in c.execute("SELECT * FROM myForwardKeys WHERE peer = ?", command): keyList.append((result[1], result[2])) + if len(keyList) == 0: if genNew: self.generateForwardKey() @@ -173,7 +177,7 @@ class OnionrUser: conn.commit() conn.close() return - + def findAndSetID(self): '''Find any info about the user from existing blocks and cache it to their DB entry''' infoBlocks = [] @@ -186,4 +190,4 @@ class OnionrUser: logger.info('%s is now using the name %s.' % (self.publicKey, self._core._utils.escapeAnsi(newName))) self._core.setPeerInfo(self.publicKey, 'name', newName) else: - raise onionrexceptions.InvalidPubkey \ No newline at end of file + raise onionrexceptions.InvalidPubkey diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index eac4f7dc..b185b0bb 100644 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -125,11 +125,11 @@ class OnionrUtils: for adder in newAdderList.split(','): adder = adder.strip() if not adder in self._core.listAdders(randomOrder = False) and adder != self.getMyAddress() and not self._core._blacklist.inBlacklist(adder): - if not config.get('tor.v3onions') and len(adder) == 62: + if not config.get('tor.v3_onions') and len(adder) == 62: continue if self._core.addAddress(adder): # Check if we have the maxmium amount of allowed stored peers - if config.get('peers.maxStoredPeers') > len(self._core.listAdders()): + if config.get('peers.max_stored') > len(self._core.listAdders()): logger.info('Added %s to db.' % adder, timestamp = True) retVal = True else: @@ -276,9 +276,9 @@ class OnionrUtils: if myBlock.getMetadata('newFSKey') is not None: onionrusers.OnionrUser(self._core, signer).addForwardKey(myBlock.getMetadata('newFSKey')) else: - logger.debug('FS not used for this block') + logger.warn('FS not used for this encrypted block') logger.info(myBlock.bmetadata) - + try: if len(blockType) <= 10: self._core.updateBlockInfo(blockHash, 'dataType', blockType) @@ -330,7 +330,7 @@ class OnionrUtils: c = conn.cursor() if not self.validateHash(hash): raise Exception("Invalid hash") - for result in c.execute("SELECT COUNT() FROM hashes where hash='" + hash + "'"): + for result in c.execute("SELECT COUNT() FROM hashes WHERE hash = ?", (hash,)): if result[0] >= 1: conn.commit() conn.close() @@ -404,7 +404,7 @@ class OnionrUtils: logger.warn('Block is expired') break else: - # if metadata loop gets no errors, it does not break, therefore metadata is valid + # if metadata loop gets no errors, it does not break, therefore metadata is valid # make sure we do not have another block with the same data content (prevent data duplication and replay attacks) nonce = self._core._utils.bytesToStr(self._core._crypto.sha3Hash(blockData)) try: @@ -490,7 +490,13 @@ class OnionrUtils: retVal = False if not idNoDomain.isalnum(): retVal = False - + + # Validate address is valid base32 (when capitalized and minus extension); v2/v3 onions and .b32.i2p use base32 + try: + base64.b32decode(idNoDomain.upper().encode()) + except binascii.Error: + retVal = False + # Validate address is valid base32 (when capitalized and minus extension); v2/v3 onions and .b32.i2p use base32 try: base64.b32decode(idNoDomain.upper().encode()) @@ -512,7 +518,7 @@ class OnionrUtils: c = conn.cursor() command = (hash,) retData = '' - for row in c.execute('SELECT ID FROM peers where hashID=?', command): + for row in c.execute('SELECT id FROM peers WHERE hashID = ?', command): if row[0] != '': retData = row[0] return retData @@ -521,18 +527,16 @@ class OnionrUtils: try: runcheck_file = self._core.dataDir + '.runcheck' - if os.path.isfile(runcheck_file): - os.remove(runcheck_file) - logger.debug('%s file appears to have existed before the run check.' % runcheck_file, timestamp = False) + if not os.path.isfile(runcheck_file): + open(runcheck_file, 'w+').close() - self._core.daemonQueueAdd('runCheck') + # self._core.daemonQueueAdd('runCheck') # deprecated starttime = time.time() while True: time.sleep(interval) - if os.path.isfile(runcheck_file): - os.remove(runcheck_file) + if not os.path.isfile(runcheck_file): return True elif time.time() - starttime >= timeout: return False @@ -543,6 +547,7 @@ class OnionrUtils: ''' Generates a secure random hex encoded token ''' + return binascii.hexlify(os.urandom(size)) def importNewBlocks(self, scanDir=''): @@ -625,12 +630,14 @@ class OnionrUtils: else: return headers = {'user-agent': 'PyOnionr'} + response_headers = dict() try: proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)} r = requests.get(url, headers=headers, proxies=proxies, allow_redirects=False, timeout=(15, 30)) # Check server is using same API version as us try: - if r.headers['api'] != str(API_VERSION): + response_headers = r.headers + if r.headers['X-API'] != str(API_VERSION): raise onionrexceptions.InvalidAPIVersion except KeyError: raise onionrexceptions.InvalidAPIVersion @@ -638,9 +645,12 @@ class OnionrUtils: except KeyboardInterrupt: raise KeyboardInterrupt except ValueError as e: - logger.debug('Failed to make request', error = e) + logger.debug('Failed to make GET request to %s' % url, error = e, sensitive = True) except onionrexceptions.InvalidAPIVersion: - logger.debug("Node is using different API version :(") + if 'X-API' in response_headers: + logger.debug('Using API version %s. Cannot communicate with node\'s API version of %s.' % (API_VERSION, response_headers['X-API'])) + else: + logger.debug('Using API version %s. API version was not sent with the request.' % API_VERSION) except requests.exceptions.RequestException as e: if not 'ConnectTimeoutError' in str(e) and not 'Request rejected or failed' in str(e): logger.debug('Error: %s' % str(e)) @@ -659,16 +669,16 @@ class OnionrUtils: retData = '' 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) + 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) try: retData = dataXML.getElementsByTagName('outputValue')[0].childNodes[0].data except ValueError: - logger.warn('Could not get NIST beacon value') + logger.warn('Failed to get the NIST beacon value.') else: self.powSalt = retData return retData - + def strToBytes(self, data): try: data = data.encode() @@ -681,7 +691,7 @@ class OnionrUtils: except AttributeError: pass return data - + def checkNetwork(self, torPort=0): '''Check if we are connected to the internet (through Tor)''' retData = False @@ -689,7 +699,7 @@ class OnionrUtils: try: with open('static-data/connect-check.txt', 'r') as connectTest: connectURLs = connectTest.read().split(',') - + for url in connectURLs: if self.doGetRequest(url, port=torPort) != False: retData = True @@ -722,4 +732,4 @@ def humanSize(num, suffix='B'): if abs(num) < 1024.0: return "%.1f %s%s" % (num, unit, suffix) num /= 1024.0 - return "%.1f %s%s" % (num, 'Yi', suffix) \ No newline at end of file + return "%.1f %s%s" % (num, 'Yi', suffix) diff --git a/onionr/static-data/default-plugins/cliui/main.py b/onionr/static-data/default-plugins/cliui/main.py index 646aa7c2..f9fb691f 100644 --- a/onionr/static-data/default-plugins/cliui/main.py +++ b/onionr/static-data/default-plugins/cliui/main.py @@ -38,13 +38,12 @@ class OnionrCLIUI: pass def refresh(self): - for i in range(100): - print('') + print('\n' * 80 + logger.colors.reset) def start(self): '''Main CLI UI interface menu''' showMenu = True - isOnline = "No" + isOnline = 'No' firstRun = True choice = '' @@ -53,7 +52,7 @@ class OnionrCLIUI: while showMenu: if firstRun: - logger.info("please wait while Onionr starts...") + logger.info('Please wait while Onionr starts...') daemon = subprocess.Popen(["./onionr.py", "start"], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL) time.sleep(30) firstRun = False @@ -63,9 +62,7 @@ class OnionrCLIUI: else: isOnline = "No" - print(''' -Daemon Running: ''' + isOnline + ''' - + logger.info('''Daemon Running: ''' + isOnline + ''' 1. Flow (Anonymous public chat, use at your own risk) 2. Mail (Secure email-like service) 3. File Sharing @@ -83,7 +80,7 @@ Daemon Running: ''' + isOnline + ''' elif choice in ("2", "mail"): self.subCommand("mail") elif choice in ("3", "file sharing", "file"): - print("Not supported yet") + logger.warn("Not supported yet") elif choice in ("4", "user settings", "settings"): try: self.setName() @@ -91,21 +88,22 @@ Daemon Running: ''' + isOnline + ''' pass elif choice in ("5", "daemon"): if isOnline == "Yes": - print("Onionr daemon will shutdown...") + logger.info("Onionr daemon will shutdown...") self.myCore.daemonQueueAdd('shutdown') + try: daemon.kill() except UnboundLocalError: pass else: - print("Starting Daemon...") + logger.info("Starting Daemon...") daemon = subprocess.Popen(["./onionr.py", "start"], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) elif choice in ("6", "quit"): showMenu = False elif choice == "": pass else: - print("Invalid choice") + logger.error("Invalid choice") return def setName(self): diff --git a/onionr/static-data/default-plugins/encrypt/main.py b/onionr/static-data/default-plugins/encrypt/main.py index 2aa87dcb..01eabd81 100644 --- a/onionr/static-data/default-plugins/encrypt/main.py +++ b/onionr/static-data/default-plugins/encrypt/main.py @@ -71,7 +71,8 @@ class PlainEncryption: plaintext = data encrypted = self.api.get_core()._crypto.pubKeyEncrypt(plaintext, pubkey, anonymous=True, encodedData=True) encrypted = self.api.get_core()._utils.bytesToStr(encrypted) - print('ONIONR ENCRYPTED DATA %s END ENCRYPTED DATA' % (encrypted,)) + logger.info('Encrypted Message: \n\nONIONR ENCRYPTED DATA %s END ENCRYPTED DATA' % (encrypted,)) + def decrypt(self): plaintext = "" data = "" @@ -89,10 +90,10 @@ class PlainEncryption: myPub = self.api.get_core()._crypto.pubKey decrypted = self.api.get_core()._crypto.pubKeyDecrypt(encrypted, privkey=self.api.get_core()._crypto.privKey, anonymous=True, encodedData=True) if decrypted == False: - print("Decryption failed") + logger.error("Decryption failed") else: data = json.loads(decrypted) - print(data['data']) + logger.info('Decrypted Message: \n\n%s' % data['data']) try: logger.info("Signing public key: %s" % (data['signer'],)) assert self.api.get_core()._crypto.edVerify(data['data'], data['signer'], data['sig']) != False @@ -101,7 +102,6 @@ class PlainEncryption: else: logger.info("Message has good signature.") return - def on_init(api, data = None): ''' @@ -114,4 +114,5 @@ def on_init(api, data = None): encrypt = PlainEncryption(pluginapi) api.commands.register(['encrypt'], encrypt.encrypt) api.commands.register(['decrypt'], encrypt.decrypt) - return \ No newline at end of file + + return diff --git a/onionr/static-data/default-plugins/pluginmanager/main.py b/onionr/static-data/default-plugins/pluginmanager/main.py index 4812cc90..45f2b47f 100644 --- a/onionr/static-data/default-plugins/pluginmanager/main.py +++ b/onionr/static-data/default-plugins/pluginmanager/main.py @@ -132,10 +132,10 @@ def createRepository(plugins): contents = {'plugins' : plugins, 'author' : getpass.getuser(), 'compiled-by' : plugin_name} block = Block(core = pluginapi.get_core()) - + block.setType('repository') block.setContent(json.dumps(contents)) - + return block.save(True) def check(): @@ -217,7 +217,7 @@ def pluginToBlock(plugin, import_block = True): info = '' with open(directory + 'info.json').read() as file: info = json.loads(file.read()) - + if 'author' in info: author = info['author'] if 'description' in info: @@ -228,10 +228,10 @@ def pluginToBlock(plugin, import_block = True): metadata = {'author' : author, 'date' : str(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')), 'name' : plugin, 'info' : info, 'compiled-by' : plugin_name, 'content' : data.decode('utf-8'), 'description' : description} block = Block(core = pluginapi.get_core()) - + block.setType('plugin') block.setContent(json.dumps(metadata)) - + hash = block.save(True) # hash = pluginapi.get_core().insertBlock(, header = 'plugin', sign = True) @@ -390,12 +390,12 @@ def commandInstallPlugin(): except Exception as e: logger.warn('Failed to lookup plugin in repositories.', timestamp = False) logger.error('asdf', error = e, timestamp = False) - + return True if pkobh is None: logger.error('No key for this plugin found in keystore or repositories, please specify.', timestamp = False) - + return True valid_hash = pluginapi.get_utils().validateHash(pkobh) @@ -552,49 +552,48 @@ def commandPublishPlugin(): logger.error('Plugin %s does not exist.' % pluginname, timestamp = False) else: logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' ') - + def commandCreateRepository(): if len(sys.argv) >= 3: check() - + plugins = list() script = sys.argv[0] - + del sys.argv[:2] success = True for pluginname in sys.argv: distributor = None - + if ':' in pluginname: split = pluginname.split(':') pluginname = split[0] distributor = split[1] - + pluginname = sanitize(pluginname) - + if distributor is None: distributor = getKey(pluginname) if distributor is None: logger.error('No distributor key was found for the plugin %s.' % pluginname, timestamp = False) success = False - + plugins.append([pluginname, distributor]) - + if not success: logger.error('Please correct the above errors, then recreate the repository.') return True - + blockhash = createRepository(plugins) - print(blockhash) if not blockhash is None: logger.info('Successfully created repository. Execute the following command to add the repository:\n ' + logger.colors.underline + '%s --add-repository %s' % (script, blockhash)) else: logger.error('Failed to create repository, an unknown error occurred.') else: logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [plugins...]') - + return True - + # event listeners def on_init(api, data = None): diff --git a/onionr/static-data/default-plugins/pms/main.py b/onionr/static-data/default-plugins/pms/main.py index d05ff36f..5ba910eb 100644 --- a/onionr/static-data/default-plugins/pms/main.py +++ b/onionr/static-data/default-plugins/pms/main.py @@ -66,7 +66,7 @@ class OnionrMail: self.sentboxList = [] self.sentMessages = {} return - + def inbox(self): blockCount = 0 pmBlockMap = {} @@ -87,7 +87,7 @@ class OnionrMail: continue blockCount += 1 pmBlockMap[blockCount] = blockHash - + block = pmBlocks[blockHash] senderKey = block.signer try: @@ -102,7 +102,7 @@ class OnionrMail: displayList.append('%s. %s - %s: %s' % (blockCount, blockDate, senderDisplay[:12], blockHash)) #displayList.reverse() for i in displayList: - print(i) + logger.info(i) try: choice = logger.readline('Enter a block number, -r to refresh, or -q to stop: ').strip().lower() except (EOFError, KeyboardInterrupt): @@ -129,16 +129,18 @@ class OnionrMail: else: cancel = '' readBlock.verifySig() - print('Message recieved from %s' % (self.myCore._utils.bytesToStr(readBlock.signer,))) - print('Valid signature:', readBlock.validSig) + + logger.info('Message recieved from %s' % (self.myCore._utils.bytesToStr(readBlock.signer,))) + logger.info('Valid signature: %s' % readBlock.validSig) + if not readBlock.validSig: logger.warn('This message has an INVALID signature. ANYONE could have sent this message.') cancel = logger.readline('Press enter to continue to message, or -q to not open the message (recommended).') if cancel != '-q': print(draw_border(self.myCore._utils.escapeAnsi(readBlock.bcontent.decode().strip()))) - input("Press enter to continue") + logger.readline("Press enter to continue") return - + def sentbox(self): ''' Display sent mail messages @@ -146,7 +148,7 @@ class OnionrMail: entering = True while entering: self.getSentList() - print('Enter block number or -q to return') + logger.info('Enter block number or -q to return') try: choice = input('>') except (EOFError, KeyboardInterrupt) as e: @@ -158,21 +160,22 @@ class OnionrMail: try: self.sentboxList[int(choice) - 1] except IndexError: - print('Invalid block') + logger.warn('Invalid block.') else: logger.info('Sent to: ' + self.sentMessages[self.sentboxList[int(choice) - 1]][1]) # Print ansi escaped sent message - print(self.myCore._utils.escapeAnsi(self.sentMessages[self.sentboxList[int(choice) - 1]][0])) + logger.info(self.myCore._utils.escapeAnsi(self.sentMessages[self.sentboxList[int(choice) - 1]][0])) input('Press enter to continue...') return - + def getSentList(self): count = 1 for i in self.sentboxTools.listSent(): self.sentboxList.append(i['hash']) self.sentMessages[i['hash']] = (i['message'], i['peer']) - print('%s. %s - %s - %s' % (count, i['hash'], i['peer'][:12], i['date'])) + + logger.info('%s. %s - %s - %s' % (count, i['hash'], i['peer'][:12], i['date'])) count += 1 def draftMessage(self): @@ -198,7 +201,7 @@ class OnionrMail: # if -q or ctrl-c/d, exit function here, otherwise we successfully got the public key return - print('Enter your message, stop by entering -q on a new line.') + logger.info('Enter your message, stop by entering -q on a new line.') while newLine != '-q': try: newLine = input() @@ -209,7 +212,7 @@ class OnionrMail: newLine += '\n' message += newLine - print('Inserting encrypted message as Onionr block....') + logger.info('Inserting encrypted message as Onionr block....') blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=True) self.sentboxTools.addToSent(blockID, recip, message) @@ -217,7 +220,7 @@ class OnionrMail: choice = '' while True: - print(self.strings.programTag + '\n\nOur ID: ' + self.myCore._crypto.pubKey + self.strings.mainMenu.title()) # print out main menu + logger.info(self.strings.programTag + '\n\nOur ID: ' + self.myCore._crypto.pubKey + self.strings.mainMenu.title()) # print out main menu try: choice = logger.readline('Enter 1-%s:\n' % (len(self.strings.mainMenuChoices))).lower().strip() @@ -251,4 +254,4 @@ def on_init(api, data = None): mail = OnionrMail(pluginapi) api.commands.register(['mail'], mail.menu) api.commands.register_help('mail', 'Interact with OnionrMail') - return \ No newline at end of file + return diff --git a/onionr/static-data/default_config.json b/onionr/static-data/default_config.json index 8ce16361..82dbece0 100644 --- a/onionr/static-data/default_config.json +++ b/onionr/static-data/default_config.json @@ -1,10 +1,13 @@ { "general" : { - "dev_mode": true, + "dev_mode" : true, "display_header" : true, "minimum_block_pow": 5, "minimum_send_pow": 5, + "minimum_block_pow": 5, + "minimum_send_pow": 5, + "direct_connect" : { "respond" : true, "execute_callbacks" : true @@ -13,11 +16,16 @@ "www" : { "public" : { - "run" : true + "run" : true, + "path" : "static-data/www/public/", + "guess_mime" : true }, "private" : { - "run" : true + "run" : true, + "path" : "static-data/www/private/", + "guess_mime" : true, + "timing_protection" : true }, "ui" : { @@ -30,41 +38,55 @@ }, - "log": { + "plugins" : { + "enabled" : { + + }, + + "disabled" : { + + } + }, + + "log" : { + "verbosity" : "default", + "file": { "output": false, "path": "data/output.log" }, - "console": { - "output": true, - "color": true + "console" : { + "output" : true, + "color" : true } }, "tor" : { - "v3onions": false + "v3onions" : false }, - "i2p":{ - "host": false, - "connect": true, - "ownAddr": "" + "i2p" : { + "host" : false, + "connect" : true, + "own_addr" : "" }, - "allocations":{ - "disk": 10000000000, - "netTotal": 1000000000, - "blockCache": 5000000, - "blockCacheTotal": 50000000 + "allocations" : { + "disk" : 10000000000, + "net_total" : 1000000000, + "blockCache" : 5000000, + "blockCacheTotal" : 50000000 }, - "peers":{ - "minimumScore": -100, - "maxStoredPeers": 5000, - "maxConnect": 10 + + "peers" : { + "minimum_score" : -100, + "max_stored_peers" : 5000, + "max_connect" : 10 }, - "timers":{ - "lookupBlocks": 25, - "getBlocks": 30 + + "timers" : { + "lookup_blocks" : 25, + "get_blocks" : 30 } } diff --git a/onionr/static-data/header.txt b/onionr/static-data/header.txt index 92664951..e9366187 100644 --- a/onionr/static-data/header.txt +++ b/onionr/static-data/header.txt @@ -3,9 +3,9 @@ P G' P G'' P G'' ' P G'''''' -P :G;'''''P: -P ::G;'''P:: -P :::G;;P::: +P :G''''''P: +P ::G''''P:: +P :::G''P::: P :::::::: P :::::::::::: P ::::::::::::::: @@ -20,6 +20,7 @@ P :::: ::::: ::::: ::: W :::: :: :: :: ::::: :: :: :: :: P :::: :::::: :::::: :::: P :::: :::::::::::: :::: GvPBV P ::::: :::::::: :::: -P ::::: :::::: +P ::::: ::::: P :::::::::::::::: P ::::::: + diff --git a/onionr/static-data/www/ui/common/default-icon.html b/onionr/static-data/www/ui/common/default-icon.html new file mode 100644 index 00000000..86ccf773 --- /dev/null +++ b/onionr/static-data/www/ui/common/default-icon.html @@ -0,0 +1 @@ +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 diff --git a/onionr/static-data/www/ui/common/footer.html b/onionr/static-data/www/ui/common/footer.html index 6b5cfb06..0143c2d8 100644 --- a/onionr/static-data/www/ui/common/footer.html +++ b/onionr/static-data/www/ui/common/footer.html @@ -1,4 +1,19 @@ - + + + + diff --git a/onionr/static-data/www/ui/common/onionr-reply-creator.html b/onionr/static-data/www/ui/common/onionr-reply-creator.html new file mode 100644 index 00000000..aafc8557 --- /dev/null +++ b/onionr/static-data/www/ui/common/onionr-reply-creator.html @@ -0,0 +1,31 @@ + +
+
+
+
+
+ +
+
+ + + + +
+ + +
+
+
+
+
+ +
+
+
+ diff --git a/onionr/static-data/www/ui/common/onionr-timeline-post.html b/onionr/static-data/www/ui/common/onionr-timeline-post.html index 68440a01..67ec158c 100644 --- a/onionr/static-data/www/ui/common/onionr-timeline-post.html +++ b/onionr/static-data/www/ui/common/onionr-timeline-post.html @@ -1,6 +1,6 @@
-
+
@@ -8,8 +8,8 @@ diff --git a/onionr/static-data/www/ui/common/onionr-timeline-reply-creator.html b/onionr/static-data/www/ui/common/onionr-timeline-reply-creator.html new file mode 100644 index 00000000..4cb95b02 --- /dev/null +++ b/onionr/static-data/www/ui/common/onionr-timeline-reply-creator.html @@ -0,0 +1,30 @@ + +
+
+
+
+
+
+ +
+
+ + + + +
+ + +
+
+
+ +
+
+
+ diff --git a/onionr/static-data/www/ui/common/onionr-timeline-reply.html b/onionr/static-data/www/ui/common/onionr-timeline-reply.html new file mode 100644 index 00000000..cc8a312e --- /dev/null +++ b/onionr/static-data/www/ui/common/onionr-timeline-reply.html @@ -0,0 +1,31 @@ + +
+
+
+
+ +
+
+
+ + +
+ +
+
+ +
+ $content +
+ +
+ $liked + <$= LANG.POST_REPLY $> +
+
+
+
+
+ diff --git a/onionr/static-data/www/ui/compile.py b/onionr/static-data/www/ui/compile.py index 2667b210..e991af08 100755 --- a/onionr/static-data/www/ui/compile.py +++ b/onionr/static-data/www/ui/compile.py @@ -41,16 +41,16 @@ LANG = type('LANG', (), langmap) # templating class Template: - def jsTemplate(template): + def jsTemplate(template, filename = ''): with open('common/%s.html' % template, 'r') as file: - return Template.parseTags(file.read().replace('\\', '\\\\').replace('\'', '\\\'').replace('\n', "\\\n")) + return Template.parseTags(file.read().replace('\\', '\\\\').replace('\'', '\\\'').replace('\n', "\\\n"), filename) - def htmlTemplate(template): + def htmlTemplate(template, filename = ''): with open('common/%s.html' % template, 'r') as file: - return Template.parseTags(file.read()) + return Template.parseTags(file.read(), filename) # tag parser - def parseTags(contents): + def parseTags(contents, filename = ''): # <$ logic $> for match in re.findall(r'(<\$(?!=)(.*?)\$>)', contents): try: @@ -66,7 +66,7 @@ class Template: try: out = eval(match[1].strip()) contents = contents.replace(match[0], '' if out is None else str(out)) - except NameError as e: + except (NameError, AttributeError) as e: name = match[1].strip() print('Warning: %s does not exist, treating as an str' % name) contents = contents.replace(match[0], name) @@ -118,7 +118,7 @@ def iterate(directory): # do python tags if settings['python_tags']: - contents = Template.parseTags(contents) + contents = Template.parseTags(contents, filename) # write file file.write(contents) diff --git a/onionr/static-data/www/ui/dist/css/main.css b/onionr/static-data/www/ui/dist/css/main.css index dab080ef..8854fc44 100644 --- a/onionr/static-data/www/ui/dist/css/main.css +++ b/onionr/static-data/www/ui/dist/css/main.css @@ -37,10 +37,20 @@ body { /* timeline */ +.onionr-post-focus-separator { + width: 100%; + + padding: 1rem; + padding-left: 0; + padding-right: 0; +} + .onionr-post { padding: 1rem; margin-bottom: 1rem; + cursor: pointer; + width: 100%; } @@ -60,6 +70,35 @@ body { width: 100%; } +.onionr-post-creator { + padding: 1rem; + margin-bottom: 1rem; + + width: 100%; +} + +.onionr-post-creator-user-name { + display: inline; +} + +.onionr-post-creator-user-id:before { content: "("; } +.onionr-post-creator-user-id:after { content: ")"; } + +.onionr-post-creator-content { + word-wrap: break-word; + width: 100%; +} + +.onionr-post-creator-user-icon { + border-radius: 100%; + width: 100%; +} + +.onionr-post-creator-create { + width: 100%; + text-align: center; +} + .h-divider { margin: 5px 15px; height: 1px; @@ -77,3 +116,7 @@ body { .onionr-profile-username { text-align: center; } + +.onionr-profile-save { + width: 100%; +} diff --git a/onionr/static-data/www/ui/dist/css/themes/dark.css b/onionr/static-data/www/ui/dist/css/themes/dark.css index b0473390..a81e25cf 100644 --- a/onionr/static-data/www/ui/dist/css/themes/dark.css +++ b/onionr/static-data/www/ui/dist/css/themes/dark.css @@ -5,6 +5,17 @@ body { /* timeline */ +.onionr-post-focus-separator { + border-color: black; +} + +.modal-content { + border: 1px solid black; + border-radius: 1rem; + + background-color: lightgray; +} + .onionr-post { border: 1px solid black; border-radius: 1rem; @@ -31,6 +42,35 @@ body { font-size: 15pt; } +.onionr-post-creator { + border: 1px solid black; + border-radius: 1rem; + + background-color: lightgray; +} + +.onionr-post-creator-user-name { + color: green; + font-weight: bold; +} + +.onionr-post-creator-user-id { + color: gray; +} + +.onionr-post-creator-date { + color: gray; +} + +.onionr-post-creator-content { + font-family: sans-serif, serif; + border-top: 1px solid black; + font-size: 15pt; + background-color: lightgray; + color: black; + border-width: 0px; +} + .h-divider { border-top:1px solid gray; } diff --git a/onionr/static-data/www/ui/dist/index.html b/onionr/static-data/www/ui/dist/index.html index 6efaac1b..576f1b14 100644 --- a/onionr/static-data/www/ui/dist/index.html +++ b/onionr/static-data/www/ui/dist/index.html @@ -40,10 +40,24 @@
- +
-

arinerron

+

+
+
+

+
+ +
@@ -52,6 +66,40 @@
+
+
+
+

Timeline

+
+
+ + +
+
+
+
+ +
+
+
+
+ + you +
+
+ + + +
+ + +
+
+
+
+ +
+
@@ -60,11 +108,99 @@
- + +
+ +
+ +
+ +
+
+
+
+ + + + + + + diff --git a/onionr/static-data/www/ui/dist/js/main.js b/onionr/static-data/www/ui/dist/js/main.js index 75cb7f81..6fc7c3d1 100644 --- a/onionr/static-data/www/ui/dist/js/main.js +++ b/onionr/static-data/www/ui/dist/js/main.js @@ -17,18 +17,56 @@ function remove(key) { return localStorage.removeItem(key); } +function getParameter(name) { + var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search); + return match && decodeURIComponent(match[1].replace(/\+/g, ' ')); +} + +/* usermap localStorage stuff */ + var usermap = JSON.parse(get('usermap', '{}')); +var postmap = JSON.parse(get('postmap', '{}')) function getUserMap() { return usermap; } +function getPostMap(hash) { + if(hash !== undefined) { + if(hash in postmap) + return postmap[hash]; + return null; + } + + return postmap; +} + function deserializeUser(id) { + if(!(id in getUserMap())) + return null; + var serialized = getUserMap()[id] var user = new User(); + user.setName(serialized['name']); user.setID(serialized['id']); user.setIcon(serialized['icon']); + user.setDescription(serialized['description']); + + return user; +} + +function getCurrentUser() { + var user = get('currentUser', null); + + if(user === null) + return null; + + return User.getUser(user, function() {}); +} + +function setCurrentUser(user) { + set('currentUser', user.getID()); } /* returns a relative date format, e.g. "5 minutes" */ @@ -89,10 +127,10 @@ function timeSince(date, size) { } /* replace all instances of string */ -String.prototype.replaceAll = function(search, replacement) { +String.prototype.replaceAll = function(search, replacement, limit) { // taken from https://stackoverflow.com/a/17606289/3678023 var target = this; - return target.split(search).join(replacement); + return target.split(search, limit).join(replacement); }; /* useful functions to sanitize data */ @@ -106,6 +144,16 @@ class Sanitize { static url(url) { return encodeURIComponent(url); } + + /* usernames */ + static username(username) { + return String(username).replace(/[\W_]+/g, " ").substring(0, 25); + } + + /* profile descriptions */ + static description(description) { + return String(description).substring(0, 128); + } } /* config stuff */ @@ -157,27 +205,119 @@ class User { return this.image; } + setDescription(description) { + this.description = description; + } + + getDescription() { + return this.description; + } + serialize() { return { 'name' : this.getName(), 'id' : this.getID(), - 'icon' : this.getIcon() + 'icon' : this.getIcon(), + 'description' : this.getDescription() }; } + /* save in usermap */ remember() { usermap[this.getID()] = this.serialize(); set('usermap', JSON.stringify(usermap)); } + + /* save as a block */ + save(callback) { + var block = new Block(); + + block.setType('onionr-user'); + block.setContent(JSON.stringify(this.serialize())); + + return block.save(true, callback); + } + + static getUser(id, callback) { + // console.log(callback); + var user = deserializeUser(id); + if(user === null) { + Block.getBlocks({'type' : 'onionr-user-info', 'signed' : true, 'reverse' : true}, function(data) { + if(data.length !== 0) { + try { + user = new User(); + + var userInfo = JSON.parse(data[0].getContent()); + + if(userInfo['id'] === id) { + user.setName(userInfo['name']); + user.setIcon(userInfo['icon']); + user.setDescription(userInfo['description']); + user.setID(id); + + user.remember(); + // console.log(callback); + callback(user); + return user; + } + } catch(e) { + console.log(e); + + callback(null); + return null; + } + } else { + callback(null); + return null; + } + }); + } else { + // console.log(callback); + callback(user); + return user; + } + } } /* post class */ class Post { /* returns the html content of a post */ - getHTML() { + getHTML(type) { + var replyTemplate = '\ +
\ +
\ +
\ +
\ + \ +
\ +
\ +
\ +
\ + $user-name\ +
\ +\ +
\ + \ +
\ +
\ +\ +
\ + $content\ +
\ +\ +
\ + $liked\ + reply\ +
\ +
\ +
\ +
\ +
\ +\ +'; var postTemplate = '\
\ -
\ +
\
\
\ \ @@ -185,8 +325,8 @@ class Post {
\
\ \ \
\ @@ -199,8 +339,8 @@ class Post {
\ \
\ - like\ - reply\ + $liked\ + reply\
\
\
\ @@ -209,22 +349,37 @@ class Post { \ '; + var template = ''; + + if(type !== undefined && type !== null && type == 'reply') + template = replyTemplate; + else + template = postTemplate; + var device = (jQuery(document).width() < 768 ? 'mobile' : 'desktop'); - postTemplate = postTemplate.replaceAll('$user-name-url', Sanitize.html(Sanitize.url(this.getUser().getName()))); - postTemplate = postTemplate.replaceAll('$user-name', Sanitize.html(this.getUser().getName())); - postTemplate = postTemplate.replaceAll('$user-id-url', Sanitize.html(Sanitize.url(this.getUser().getID()))); + template = template.replaceAll('$user-name-url', Sanitize.html(Sanitize.url(this.getUser().getName()))); + template = template.replaceAll('$user-name', Sanitize.html(this.getUser().getName())); + template = template.replaceAll('$user-id-url', Sanitize.html(Sanitize.url(this.getUser().getID()))); - postTemplate = postTemplate.replaceAll('$user-id-truncated', Sanitize.html(this.getUser().getID().substring(0, 12) + '...')); - // postTemplate = postTemplate.replaceAll('$user-id-truncated', Sanitize.html(this.getUser().getID().split('-').slice(0, 4).join('-'))); + template = template.replaceAll('$user-id-truncated', Sanitize.html(this.getUser().getID().substring(0, 12) + '...')); + // template = template.replaceAll('$user-id-truncated', Sanitize.html(this.getUser().getID().split('-').slice(0, 4).join('-'))); - postTemplate = postTemplate.replaceAll('$user-id', Sanitize.html(this.getUser().getID())); - postTemplate = postTemplate.replaceAll('$user-image', Sanitize.html(this.getUser().getIcon())); - postTemplate = postTemplate.replaceAll('$content', Sanitize.html(this.getContent())); - postTemplate = postTemplate.replaceAll('$date-relative', timeSince(this.getPostDate(), device) + (device === 'desktop' ? ' ago' : '')); - postTemplate = postTemplate.replaceAll('$date', this.getPostDate().toLocaleString()); + template = template.replaceAll('$user-id', Sanitize.html(this.getUser().getID())); + template = template.replaceAll('$user-image', "data:image/jpeg;base64," + Sanitize.html(this.getUser().getIcon())); + template = template.replaceAll('$content', Sanitize.html(this.getContent()).replaceAll('\n', '
', 16)); // Maximum of 16 lines + template = template.replaceAll('$post-hash', this.getHash()); + template = template.replaceAll('$date-relative-truncated', timeSince(this.getPostDate(), 'mobile')); + template = template.replaceAll('$date-relative', timeSince(this.getPostDate(), device) + (device === 'desktop' ? ' ago' : '')); + template = template.replaceAll('$date', this.getPostDate().toLocaleString()); - return postTemplate; + if(this.getHash() in getPostMap() && getPostMap()[this.getHash()]['liked']) { + template = template.replaceAll('$liked', 'unlike'); + } else { + template = template.replaceAll('$liked', 'like'); + } + + return template; } setUser(user) { @@ -243,6 +398,14 @@ class Post { return this.content; } + setParent(parent) { + this.parent = parent; + } + + getParent() { + return this.parent; + } + setPostDate(date) { // unix timestamp input if(date instanceof Date) this.date = date; @@ -253,6 +416,51 @@ class Post { getPostDate() { return this.date; } + + setHash(hash) { + this.hash = hash; + } + + getHash() { + return this.hash; + } + + save(callback) { + var args = {'type' : 'onionr-post', 'sign' : true, 'content' : JSON.stringify({'content' : this.getContent()})}; + + if(this.getParent() !== undefined && this.getParent() !== null) + args['parent'] = (this.getParent() instanceof Post ? this.getParent().getHash() : (this.getParent() instanceof Block ? this.getParent().getHash() : this.getParent())); + + var url = '/client/?action=insertBlock&data=' + Sanitize.url(JSON.stringify(args)) + '&token=' + Sanitize.url(getWebPassword()) + '&timingToken=' + Sanitize.url(getTimingToken()); + + console.log(url); + + var http = new XMLHttpRequest(); + + if(callback !== undefined) { + // async + + var thisObject = this; + + http.addEventListener('load', function() { + thisObject.setHash(Block.parseBlockArray(JSON.parse(http.responseText)['hash'])); + callback(thisObject.getHash()); + }, false); + + http.open('GET', url, true); + http.timeout = 5000; + http.send(null); + } else { + // sync + + http.open('GET', url, false); + http.send(null); + + this.setHash(Block.parseBlockArray(JSON.parse(http.responseText)['hash'])); + + return this.getHash(); + } + } } /* block class */ @@ -301,8 +509,12 @@ class Block { // returns the parent block's hash (not Block object, for performance) getParent() { - if(!(this.parent instanceof Block) && this.parent !== undefined && this.parent !== null) - this.parent = Block.openBlock(this.parent); // convert hash to Block object + // console.log(this.parent); + + // TODO: Create a function to fetch the block contents and parse it from the server; right now it is only possible to search for types of blocks (see Block.getBlocks), so it is impossible to return a Block object here + + // if(!(this.parent instanceof Block) && this.parent !== undefined && this.parent !== null) + // this.parent = Block.openBlock(this.parent); // convert hash to Block object return this.parent; } @@ -355,11 +567,57 @@ class Block { return !(this.hash === null || this.hash === undefined); } + // saves the block, returns the hash + save(sign, callback) { + var type = this.getType(); + var content = this.getContent(); + var parent = this.getParent(); + + if(content !== undefined && content !== null && type !== '') { + var args = {'content' : content}; + + if(type !== undefined && type !== null && type !== '') + args['type'] = type; + if(parent !== undefined && parent !== null && parent.getHash() !== undefined && parent.getHash() !== null && parent.getHash() !== '') + args['parent'] = parent.getHash(); + if(sign !== undefined && sign !== null) + args['sign'] = String(sign) !== 'false' + + + var url = '/client/?action=insertBlock&data=' + Sanitize.url(JSON.stringify(args)) + '&token=' + Sanitize.url(getWebPassword()) + '&timingToken=' + Sanitize.url(getTimingToken()); + + console.log(url); + + var http = new XMLHttpRequest(); + + if(callback !== undefined) { + // async + + http.addEventListener('load', function() { + callback(Block.parseBlockArray(JSON.parse(http.responseText)['hash'])); + }, false); + + http.open('GET', url, true); + http.timeout = 5000; + http.send(null); + } else { + // sync + + http.open('GET', url, false); + http.send(null); + + return Block.parseBlockArray(JSON.parse(http.responseText)['hash']); + } + } + + return false; + } + /* static functions */ // recreates a block by hash static openBlock(hash) { - return parseBlock(response); + return Block.parseBlock(hash); } // converts an associative array to a Block @@ -438,14 +696,58 @@ class Block { /* temporary code */ +var tt = getParameter("timingToken"); +if(tt !== null && tt !== undefined) { + setTimingToken(tt); +} + if(getWebPassword() === null) { var password = ""; while(password.length != 64) { password = prompt("Please enter the web password (run `./RUN-LINUX.sh --get-password`)"); } - setTimingToken(prompt("Please enter the timing token (optional)")); - setWebPassword(password); - window.location.reload(true); } + +if(getCurrentUser() === null) { + jQuery('#modal').modal('show'); + + var url = '/client/?action=info&token=' + Sanitize.url(getWebPassword()) + '&timingToken=' + Sanitize.url(getTimingToken()); + + console.log(url); + + var http = new XMLHttpRequest(); + + // sync + + http.addEventListener('load', function() { + var id = JSON.parse(http.responseText)['pubkey']; + + User.getUser(id, function(data) { + if(data === null || data === undefined) { + var user = new User(); + + user.setName('New User'); + user.setID(id); + user.setIcon('/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAcFBQYFBAcGBQYIBwcIChELCgkJChUPEAwRGBUaGRgVGBcbHichGx0lHRcYIi4iJSgpKywrGiAvMy8qMicqKyr/2wBDAQcICAoJChQLCxQqHBgcKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKir/wAARCACAAIADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDrtTvrlL51jlkyGPANUZNSuvJZ2uJFYHjB6UmpTE6jcZUH5iCR0FQQLHvww3An8K8jmuz0lHQvwXV1gNLcSBmGcZqcXtwo/wBe/X1rzqw1e/stWmaTdKpcl1Le9dqmoJc2qupxnoCOauUWkOzRpnULhsATMPXmoptSuFGPPfjvms8Xew4OaY7NOSEyAT3rK9w5bFn+0rlmCrPIvqc9KRL+9UGVrr5ew39aoN5qkRhjt9Vp0Vv5bFmHJ6Z7Ucz2KsjXi1K4kUYmk6Z61Ot1Owz5z9OOayYcquGZgw59sVaikZ1OSQB0FUmQ0XftVwP+WznjoDS/bZx83msBjpmqobb1IBPv1prOpGD+lVzE2LP9ozEHEznPvTDe3JBImbaO4NZ0jlfliGM52jHWlW2nEO6eRuBnCU7jsXft068+dIR9amtLycupaduvOTWH/aIPyqjxkHBDd/pV2BiZEYdAacZJ7Eyi0QXC7dVn3Nw0hzxxTRPCgAXAZucY+9RewzDUpjuYp5h7VGLZW+VAVJ6Fj0rn5pX2Nkkc/qFuV1KbdGHiLb1ZcZTPYj61JazNbNtfJib+HofqD6ioPEQ+y6lAQziTZ9/djvwM0z7XfSRhJj8hxnzAMj8a9CDUqepErp6G0uriOdYNQOQRmKZRw49x2PrWnHd2/lZDqufeuIulcWpjlYb433IR0B6EUnmyMu55AFiHrzz0rzpO0rI6uRNXO08yNySGVv8AgXWpTKEXaRg+9cLZvIzM7s+M/L61Oby5+0eXG7ZXknqFHqTSE6Z10ksUMZknJVR7Vg3viCV/3dngAHl/Wsh759QuPKDmSJT8x3Ec1pRQReSViKMf7prtp0rq7MZWi9SvpmsTvrEKTuWDNt4OcZrs1kaBVcweYpPU1w2n2Dt4mtsqFAffgH0rugSr4Y7j168fhWdRcrKmlpYJJy2H2IHHpwB/9eoxO5G0ZxjpnrSGNpW5ZVGePb1p3ynKMPn6ZHGKzWpGiIVt/mwycjJPrVi2ZvMA3dcAEelOAYEHBdTwfWnwxATgldqE9B1FaqyehndvcsXSk6hNzxuNRpFuyCQO/Spr35b6Tp944xVaeby4GkH8Kkn8BUDOU8QvG2p+Qy7wqjk96rtes0KJsGMYBI6j0qCwf+0J2u7hgCx+X3H9K1xpp+0RkkFO/wDhVXk1ZGlktzAu1kdyMLleFyeuapSWbrsjYnO4Bs9/f+laNxKsk7vkeX9q8pCO2AS1XNMRbtby5lTekOGII5J7AD8BWPLd2OhSsiitnLDeFGUkeSD+JNWEQ7Xixt3dcHPNS7ZVvnWQ7p3jDOPTvj9f0pwTeBwQwPPHSp21HqzIltDY3BZdylz8oUEnP4VBHqzyXot7uHysdJGOOfwroy7iP5iQBxkHFYl/YWzXsZZXJZhliMd+wrtp1FYx5XzanQ+F7b/iZXHmIS6fL5jd/YVu3cLxyBdzZP3eM8VBpMUYdjHn52GPwAH9K6aS0ElqCy/Mo4qV+8bMqsuV3MJLVduJJMfhxVxYovL/ANpeMFeKx7vXLSzmZJHbKHoqGs6TxZBI22KOV29+AKy5lHcPZylsdMu9EG3I5zjFQ/a1imXzWyVG3k5rlf7bvLudU8zyYs8hD1/Gty3jWSNORjjrVKd9gdNrc0bqVRfT7sg7yR71A7edGYzIoDqRyarXjeXfzebwd7Z+b+lQM7KodcMvrjFLqI4nSbC0ivpoNQmdGZiI8OVxg+orJ1TWfEfhnWnS2uWuLYPgRSLv3Iff1966LUlP26RGVnw+QpH3gecg+orS06yTVLHyNRtvtEUYIVnOGQezDqK0pvldmrlzXNG9zmtK1F7qGxIiPlM7srP1Vxncp/xr0bw7p6WukzvMhKzPuxj0rz2ztxb3I06yiZktbh5mbOQC+Bt/nXsNor23h2NLeESXZjPlRFgNx9ee3rWlOMXN2MqspKKPOb3WtN0fxRevqd2tv5qKkKYLMeOTgdPTmtC31PQ7qEraXsbSYztbgn35FUNS+FGq3zTSzzQzSXMnmyT7yrof6/hWtpGk6f4dR4riJr27nULLM6YUAdFGf51M6UILUuNRyegxHhnUhWXHoCDzSWwAkwyrwepHSobnQ3l1BrvRIjbso+ZcYVqYL1kcCdfKlxhlYYFcTTTOlNNaHWaU5MyIETIPUADFdVJgx9O1cl4fuFuSNrAleu2uivL1Le3LyHAArtwzsmzhxGskjzPxNCiazOqdM5xXOBGWZiMDNdLqRW7ee+bA3EhQeuPWsA8MecZAwDXFLWbZ6MNIpMnhV2ZWD9+wrr7fKRxqik9Msa4pYmEyMsyo2eATj8q6XT7i8QoG2FOxV60j3M6hraope/n3cfOcVnOpPVsj0ra1CaJLybC7iXOfasm6dWUBAMk5JxitNDlVzF1SEZEykgrwR6irtjqiW9jLFIhTzY9qHHU9qrXQzCQ+CD2z0rHMrO3llyjKeCDgNWsJWE1cTw8IvtVw8r+XN5xUknJ4PP416DHq9/N4hguLOAyW1nH5LZHDEj9DivOprSCTWreUymJLg7bkL1YAdRjuRxXrGk6jZWemx29lHEkCjIG4j8+DzWkKbfWxVapFJaXZuvdo8AK4BK52nqPwrnbyO3aYyttYHtkirrXkNxC7K0cbKM8S5H6isKQSSyHy1+U9HByK2l7y1OOF4vQs7UuWCGFfL6Ehzx9BTH0C2m/ds8j+m4D5adZRT+Z8rAj124rSMqW6Evkc4Yk1HJF7ov2klsS2Gn22nW4SHC+9YXiW+MrpZqQQxwxq7qWpR2tqXLowYcDPWuBe9ka/M4PsFNYV5KEeWJvQg5y5mXtYmiW1WJChGduB1Fc+qqyyZDGMdDnIzVnU7mUzfOHiOPmJHWpI4zHpOIwu5upyOfwriWrO/ZGZmeGeNjHuGeAB1H41vWOpxzypKgGeCV2jqD6VzpNzGwLOjKrZGByv4VVe6aG+Zo+CjBgQB0zyPpWiFJXPStSnAv5wso3Bzxj3rOkkWUAnBZOQ2/vUWpysdTuBk7jKw+ZfeqsjfZ1KzEH3XmtDjK9/MkYGZD83UA9KxXuEfnd0PBPU1ZvZYip2tgnqCKwHlJuRGjBueMVSd9CraHS209tKuJEUnP0zWxDIkIAhuJl7gbyRXHrbzBgcEt2UdquwSTRnbI/19q2i2ZyR2UF7JwJJGYdAM5ratImMW/hRn5lHQ++K5Ow1BWVGdduBxkdTWtDqbvKY4+MdDWqZhJHUxyxqgCcMOfrVHVb9LG1eWTDs3QepAqhHelbd5ZjsYfpXHarq8mpzkI5WIEhlz0/zioqVOVF0qTm9SeXUXv7kmRwEY/Lt4zUkNsC4D4Ii+Y4PSqVqMN5eBmQcAdh/StC4aKzsGRGUsfbOa86TcnqeitNEOkmWexkbbjnA2nkfUVlqkluoizhX5GcYp8DkgPIrbT97aMg1JcwRuRK67oiOuc4pLUrYytSiSJlAJGeSFPzL/jVJ2TIlz5xAABC4P196u3EUN8PsxfKKcod2CtVLqBrKQwsS2xcHPXkitVawtUdfqrSrq9y4XOJG4P1rLuJywbcu3nBGK6HUS51OcKgZfMJJU/55rB1CN47dmdl3ZzgNyKlSVznsc/qW5d25+f7tcxevKkwaMmNvXPSuqvNQiVSmGP8As7OWFcve/vWLRmTrjb6VvTbuElodf4Zu7K5gSLzmaVR8+/qa61dPhdQFA/DvXkmibk1EiaM8rwFOP1r0zQL47VXb06sZQ1dCkk7HPOLtdGoukKu2RsEpyoPAzVqCwWNshwWI9OTVuEedbl5BgnocVCJJJJTHEOFOGOcYrTQx1ZmeIbxljW1TgyfKNo6+9cwbRYju3bvJBL55AP8A9aut1C1Es8sqSbzCm3IHAJ6gfQVyt/GttGyI24bcEeue3+NcdS97s7aVrWQtpKyTGaTkdFGT+dTXd5PecYQRn1BzWPNMYLZVQkZASPPrV7S5fMuxFNs3Rgbmc8A/Tua52n0OlW3Ztmymi0pXhypx36H61n263NwxiWIKD1y/BrohLatbiOWcOcemB+QrHvI5EkAt5EKj+HdjH4UnsTGWupYTwzEyF5QEkHO5Gzj8KwdVsmtroywskoAGec47YI96s3M1+8Yj3TADoyAisW6hvba4WWVXKS8MfU9Rk+tVFodn1Z3Gp3jf2ldCRWwJWGBxnmqYjLJlFRycnkcj610F/pmL6Yht+ZCeVqmbGRCHji3EDjCmqtbY5eY5q90gSqBMCfRvSufutJ8uQkKMDuetd5LDPtIuEIwOMLjNY1xGskb79yH+4y0RZdzj7C2WfWI43Xf2KkYr1LTdOe1t1Nv5MSD0QH/CuDhtY49YjZgwU8Y3EE16JptneXMai2sGSMfxyMR+ldtOKauc9WTNq3wIgWcE46CnSBHGSvBGOKsJaSR24MsRYrztVMVMLSQrkLhupXHGD6VvZnNc5XVLdrUSiHJSQ5Cgd65i+tp4dKedQiTsdoLjhfU4716LqGnuVw6MD1VgOlchqFgyXkT3GXVHyA+dufeuedNPU6adS2hxtxFOIS3lsZZASiMvfoGqlNb31g0dtnZu+ZnH3vr9a7V7iKW6WK0ge7nkON5Xauf8BVTW7CSDT5jdkRSS5LSY5I/oPaudw5TrjUuZOnX9lt2G4leUDBO7j8RWxaX1urj/AEWE+jp6+4NcCYDcaiyWaKijptX5vwPua0H0y/gVZcXicfeLZFZSj5mySZ6OmpwiEyRLl1+9C67SP8+tYuo61a6nFJAEktpPQ9DWXpFprGqbbd/MaMcFmToPr1rpD4OijVTN50zDH3RyfxqbtbE8sYvU/9k=\ +'); + user.setDescription('A new OnionrUI user'); + + user.remember(); + user.save(); + + setCurrentUser(user); + } else { + setCurrentUser(data); + } + + window.location.reload(); + }); + }, false); + + http.open('GET', url, true); + http.send(null); +} + +currentUser = getCurrentUser(); diff --git a/onionr/static-data/www/ui/dist/js/timeline.js b/onionr/static-data/www/ui/dist/js/timeline.js index 0dbd634a..e9ca8a13 100644 --- a/onionr/static-data/www/ui/dist/js/timeline.js +++ b/onionr/static-data/www/ui/dist/js/timeline.js @@ -4,24 +4,488 @@ Block.getBlocks({'type' : 'onionr-post', 'signed' : true, 'reverse' : true}, fun try { var block = data[i]; - var post = new Post(); - var user = new User(); + var finished = false; + User.getUser(new String(block.getHeader('signer', 'unknown')), function(user) { + var post = new Post(); - var blockContent = JSON.parse(block.getContent()); + var blockContent = JSON.parse(block.getContent()); - user.setName('unknown'); - user.setID(new String(block.getHeader('signer', 'unknown'))); - post.setContent(blockContent['content']); - post.setPostDate(block.getDate()); - post.setUser(user); + // just ignore anything shorter than 280 characters + if(String(blockContent['content']).length <= 280 && block.getParent() === null) { + post.setContent(blockContent['content']); + post.setPostDate(block.getDate()); + post.setUser(user); - document.getElementById('onionr-timeline-posts').innerHTML += post.getHTML(); + post.setHash(block.getHash()); + + document.getElementById('onionr-timeline-posts').innerHTML += post.getHTML(); + } + + finished = true; + }); + + while(!finished); } catch(e) { + console.log('Troublemaker block: ' + data[i].getHash()); console.log(e); } } }); -function viewProfile(id, name) { - document.getElementById("onionr-profile-username").innerHTML = Sanitize.html(decodeURIComponent(name)); +function toggleLike(hash) { + var post = getPostMap(hash); + if(post === null || !getPostMap()[hash]['liked']) { + console.log('Liking ' + hash + '...'); + + if(post === null) + getPostMap()[hash] = {}; + + getPostMap()[hash]['liked'] = true; + + set('postmap', JSON.stringify(getPostMap())); + + var block = new Block(); + + block.setType('onionr-post-like'); + block.setContent(JSON.stringify({'hash' : hash})); + block.save(true, function(hash) {}); + } else { + console.log('Unliking ' + hash + '...'); + } } + +function postCreatorChange() { + var content = document.getElementById('onionr-post-creator-content').value; + var message = ''; + + var maxlength = 280; + + var disable = true; + var warn = false; + + if(content.length !== 0) { + if(content.length - content.replaceAll('\n', '').length > 16) { + // 16 max newlines + message = 'Please use less than 16 newlines'; + } else if(content.length <= maxlength) { + // 280 max characters + message = '%s characters remaining'.replaceAll('%s', (280 - content.length)); + disable = false; + + if(maxlength - content.length < maxlength / 4) { + warn = true; + } + } else { + message = '%s characters over maximum'.replaceAll('%s', (content.length - maxlength)); + } + } + + var element = document.getElementById('onionr-post-creator-content-message'); + var button = document.getElementById("onionr-post-creator-create"); + + if(message === '') + element.style.visibility = 'hidden'; + else { + element.style.visibility = 'visible'; + + element.innerHTML = message; + + if(disable) + element.style.color = 'red'; + else if(warn) + element.style.color = '#FF8C00'; + else + element.style.color = 'gray'; + } + + if(disable) + button.disabled = true; + else + button.disabled = false; +} + +function replyCreatorChange() { + var content = document.getElementById('onionr-reply-creator-content').value; + var message = ''; + + var maxlength = 280; + + var disable = true; + var warn = false; + + if(content.length !== 0) { + if(content.length - content.replaceAll('\n', '').length > 16) { + // 16 max newlines + message = 'Please use less than 16 newlines'; + } else if(content.length <= maxlength) { + // 280 max characters + message = '%s characters remaining'.replaceAll('%s', (280 - content.length)); + disable = false; + + if(maxlength - content.length < maxlength / 4) { + warn = true; + } + } else { + message = '%s characters over maximum'.replaceAll('%s', (content.length - maxlength)); + } + } + + var element = document.getElementById('onionr-reply-creator-content-message'); + var button = document.getElementById("onionr-reply-creator-create"); + + if(message === '') + element.style.visibility = 'hidden'; + else { + element.style.visibility = 'visible'; + + element.innerHTML = message; + + if(disable) + element.style.color = 'red'; + else if(warn) + element.style.color = '#FF8C00'; + else + element.style.color = 'gray'; + } + + if(disable) + button.disabled = true; + else + button.disabled = false; +} + +function focusReplyCreatorChange() { + var content = document.getElementById('onionr-post-focus-reply-creator-content').value; + var message = ''; + + var maxlength = 280; + + var disable = true; + var warn = false; + + if(content.length !== 0) { + if(content.length - content.replaceAll('\n', '').length > 16) { + // 16 max newlines + message = 'Please use less than 16 newlines'; + } else if(content.length <= maxlength) { + // 280 max characters + message = '%s characters remaining'.replaceAll('%s', (280 - content.length)); + disable = false; + + if(maxlength - content.length < maxlength / 4) { + warn = true; + } + } else { + message = '%s characters over maximum'.replaceAll('%s', (content.length - maxlength)); + } + } + + var element = document.getElementById('onionr-post-focus-reply-creator-content-message'); + var button = document.getElementById("onionr-post-focus-reply-creator-create"); + + if(message === '') + element.style.visibility = 'hidden'; + else { + element.style.visibility = 'visible'; + + element.innerHTML = message; + + if(disable) + element.style.color = 'red'; + else if(warn) + element.style.color = '#FF8C00'; + else + element.style.color = 'gray'; + } + + if(disable) + button.disabled = true; + else + button.disabled = false; +} + +function viewProfile(id, name) { + id = decodeURIComponent(id); + document.getElementById("onionr-profile-username").innerHTML = Sanitize.html(decodeURIComponent(name)); + + User.getUser(id, function(data) { + if(data !== null) { + document.getElementById("onionr-profile-user-icon").src = "data:image/jpeg;base64," + Sanitize.html(data.getIcon()); + document.getElementById("onionr-profile-user-icon").b64 = Sanitize.html(data.getIcon()); + document.getElementById("onionr-profile-username").innerHTML = Sanitize.html(Sanitize.username(data.getName())); + document.getElementById("onionr-profile-username").title = Sanitize.html(data.getID()); + document.getElementById("onionr-profile-description").innerHTML = Sanitize.html(Sanitize.description(data.getDescription())); + } + }); +} + +function updateUser() { + toggleSaveButton(false); + + // jQuery('#modal').modal('show'); + + var name = jQuery('#onionr-profile-username').text(); + var id = document.getElementById("onionr-profile-username").title; + var icon = document.getElementById("onionr-profile-user-icon").b64; + var description = jQuery("#onionr-profile-description").text(); + + var user = new User(); + + user.setName(name); + user.setID(id); + user.setIcon(icon); + user.setDescription(Sanitize.description(description)); + + user.remember(); + user.save(function() { + setCurrentUser(user); + + window.location.reload(); + }); +} + +function cancelUpdate() { + toggleSaveButton(false); + + var name = jQuery('#onionr-profile-username').text(); + var id = document.getElementById("onionr-profile-username").title; + + viewProfile(id, name); +} + +function toggleSaveButton(show) { + document.getElementById("onionr-profile-edit").style.display = (show ? 'block' : 'none'); +} + +function makePost() { + var content = document.getElementById("onionr-post-creator-content").value; + + if(content.trim() !== '') { + var post = new Post(); + + post.setUser(getCurrentUser()); + post.setContent(content); + post.setPostDate(new Date()); + + post.save(function(data) {}); // async, but no function + + document.getElementById('onionr-timeline-posts').innerHTML = post.getHTML() + document.getElementById('onionr-timeline-posts').innerHTML; + + document.getElementById("onionr-post-creator-content").value = ""; + document.getElementById("onionr-post-creator-content").focus(); + postCreatorChange(); + } else { + console.log('Not making empty post.'); + } +} + +function getReplies(id, callback) { + Block.getBlocks({'type' : 'onionr-post', 'parent' : id, 'signed' : true, 'reverse' : true}, callback); +} + +function focusPost(id) { + viewReplies(id); +} + +function viewRepliesMobile(id) { + var post = document.getElementById('onionr-post-' + id); + + var user_name = ''; + var user_id = ''; + var user_id_trunc = ''; + var user_icon = ''; + var post_content = ''; + + if(post !== null && post !== undefined) { + // if the post is in the timeline, get the data from it + user_name = post.getElementsByClassName('onionr-post-user-name')[0].innerHTML; + user_id = post.getElementsByClassName('onionr-post-user-id')[0].title; + user_id_trunc = post.getElementsByClassName('onionr-post-user-id')[0].innerHTML; + user_icon = post.getElementsByClassName('onionr-post-user-icon')[0].src; + post_content = post.getElementsByClassName('onionr-post-content')[0].innerHTML; + } else { + // otherwise, fetch the data + } + + document.getElementById('onionr-post-focus-user-icon').src = user_icon; + document.getElementById('onionr-post-focus-user-name').innerHTML = user_name; + document.getElementById('onionr-post-focus-user-id').innerHTML = user_id_trunc; + document.getElementById('onionr-post-focus-user-id').title = user_id; + document.getElementById('onionr-post-focus-content').innerHTML = post_content; + + document.getElementById('onionr-post-focus-reply-creator-user-name').innerHTML = Sanitize.html(Sanitize.username(getCurrentUser().getName())); + document.getElementById('onionr-post-focus-reply-creator-user-icon').src = "data:image/jpeg;base64," + Sanitize.html(getCurrentUser().getIcon()); + document.getElementById('onionr-post-focus-reply-creator-content').value = ''; + document.getElementById('onionr-post-focus-reply-creator-content-message').value = ''; + + jQuery('#onionr-post-focus').modal('show'); +} + +function viewReplies(id) { + document.getElementById('onionr-replies-title').innerHTML = 'Replies'; + document.getElementById('onionr-reply-creator-panel').originalPost = id; + document.getElementById('onionr-reply-creator-panel').innerHTML = '\ +
\ +
\ +
\ +
\ +
\ + \ +
\ +
\ +
\ +
\ + \ + you\ +
\ +
\ +\ + \ +\ +
\ +\ + \ +
\ +
\ +
\ +
\ +
\ +\ +
\ +
\ +
\ +\ +'; + + document.getElementById('onionr-reply-creator-content').innerHTML = ''; + document.getElementById("onionr-reply-creator-content").placeholder = "Enter a message here..."; + document.getElementById('onionr-reply-creator-user-name').innerHTML = Sanitize.html(Sanitize.username(getCurrentUser().getName())); + document.getElementById('onionr-reply-creator-user-icon').src = "data:image/jpeg;base64," + Sanitize.html(getCurrentUser().getIcon()); + + document.getElementById('onionr-replies').innerHTML = ''; + getReplies(id, function(data) { + var replies = document.getElementById('onionr-replies'); + + replies.innerHTML = ''; + + for(var i = 0; i < data.length; i++) { + try { + var block = data[i]; + + var finished = false; + User.getUser(new String(block.getHeader('signer', 'unknown')), function(user) { + var post = new Post(); + + var blockContent = JSON.parse(block.getContent()); + + // just ignore anything shorter than 280 characters + if(String(blockContent['content']).length <= 280) { + post.setContent(blockContent['content']); + post.setPostDate(block.getDate()); + post.setUser(user); + + post.setHash(block.getHash()); + + replies.innerHTML += post.getHTML('reply'); + } + + finished = true; + }); + + while(!finished); + } catch(e) { + console.log('Troublemaker block: ' + data[i].getHash()); + console.log(e); + } + } + }); +} + +function makeReply() { + var content = document.getElementById("onionr-reply-creator-content").value; + + if(content.trim() !== '') { + var post = new Post(); + + var originalPost = document.getElementById('onionr-reply-creator-panel').originalPost; + + console.log('Original post hash: ' + originalPost); + + post.setUser(getCurrentUser()); + post.setParent(originalPost); + post.setContent(content); + post.setPostDate(new Date()); + + post.save(function(data) {}); // async, but no function + + document.getElementById('onionr-replies').innerHTML = post.getHTML('reply') + document.getElementById('onionr-replies').innerHTML; + + document.getElementById("onionr-reply-creator-content").value = ""; + document.getElementById("onionr-reply-creator-content").focus(); + replyCreatorChange(); + } else { + console.log('Not making empty reply.'); + } +} + +jQuery('body').on('click', '[data-editable]', function() { + var el = jQuery(this); + var txt = el.text(); + var maxlength = el.attr("maxlength"); + + var input = jQuery('').val(txt); + input.attr('maxlength', maxlength); + el.replaceWith(input); + + var save = function() { + var newTxt = input.val(); + + if(el.attr('id') === 'onionr-profile-username') + newTxt = Sanitize.username(newTxt); + if(el.attr('id') === 'onionr-profile-description') + newTxt = Sanitize.description(newTxt); + + var p = el.text(newTxt); + + input.replaceWith(p); + + if(newTxt !== txt) + toggleSaveButton(true); + }; + + var saveEnter = function(event) { + console.log(event); + console.log(event.keyCode); + if (event.keyCode === 13) + save(); + }; + + input.one('blur', save).bind('keyup', saveEnter).focus(); +}); +//viewProfile('$user-id-url', '$user-name-url') +// jQuery('#onionr-post-user-id').on('click', function(e) { alert(3);}); +//jQuery('#onionr-post *').on('click', function(e) { e.stopPropagation(); }); +// jQuery('#onionr-post').click(function(e) { alert(1); }); + +currentUser = getCurrentUser(); +if(currentUser !== undefined && currentUser !== null) { + document.getElementById("onionr-post-creator-user-name").innerHTML = Sanitize.html(currentUser.getName()); + document.getElementById("onionr-post-creator-user-id").innerHTML = "you"; + document.getElementById("onionr-post-creator-user-icon").src = "data:image/jpeg;base64," + Sanitize.html(currentUser.getIcon()); + document.getElementById("onionr-post-creator-user-id").title = currentUser.getID(); + + document.getElementById("onionr-post-creator-content").placeholder = "Enter a message here..."; + document.getElementById("onionr-post-focus-reply-creator-content").placeholder = "Enter a message here..."; + + document.getElementById("onionr-post-focus-reply-creator-user-id").innerHTML = "you"; +} + +viewCurrentProfile = function() { + viewProfile(encodeURIComponent(currentUser.getID()), encodeURIComponent(currentUser.getName())); +} + +document.getElementById("onionr-post-creator-user-id").onclick = viewCurrentProfile; +document.getElementById("onionr-post-creator-user-name").onclick = viewCurrentProfile; + +// on some browsers it saves the user input on reload. So, it should also recheck the input. +postCreatorChange(); diff --git a/onionr/static-data/www/ui/lang.json b/onionr/static-data/www/ui/lang.json index 28606a56..eb8a1403 100644 --- a/onionr/static-data/www/ui/lang.json +++ b/onionr/static-data/www/ui/lang.json @@ -6,10 +6,35 @@ "NOTIFICATIONS" : "Notifications", "MESSAGES" : "Messages", + "LATEST" : "Latest...", "TRENDING" : "Trending", + "REPLIES" : "Replies", + + "MODAL_TITLE" : "Loading...", + "MODAL_MESSAGE" : "Onionr has begun performing a CPU-intensive operation. If this operation does not complete in the next 10 seconds, try reloading the page.", "POST_LIKE" : "like", - "POST_REPLY" : "reply" + "POST_UNLIKE" : "unlike", + "POST_REPLY" : "reply", + + "POST_CREATOR_YOU" : "you", + "POST_CREATOR_PLACEHOLDER" : "Enter a message here...", + "POST_CREATOR_CREATE" : "Create post", + + "REPLY_CREATOR_YOU" : "you", + "REPLY_CREATOR_PLACEHOLDER" : "Enter reply here...", + "REPLY_CREATOR_CREATE" : "Reply", + + "POST_CREATOR_MESSAGE_MAXIMUM_NEWLINES" : "Please use less than 16 newlines", + "POST_CREATOR_MESSAGE_REMAINING" : "%s characters remaining", + "POST_CREATOR_MESSAGE_OVER" : "%s characters over maximum", + + "REPLY_CREATOR_MESSAGE_MAXIMUM_NEWLINES" : "Please use less than 16 newlines", + "REPLY_CREATOR_MESSAGE_REMAINING" : "%s characters remaining", + "REPLY_CREATOR_MESSAGE_OVER" : "%s characters over maximum", + + "PROFILE_EDIT_SAVE" : "Save", + "PROFILE_EDIT_CANCEL" : "Cancel" }, "spa" : { diff --git a/onionr/static-data/www/ui/src/css/main.css b/onionr/static-data/www/ui/src/css/main.css index dab080ef..8854fc44 100644 --- a/onionr/static-data/www/ui/src/css/main.css +++ b/onionr/static-data/www/ui/src/css/main.css @@ -37,10 +37,20 @@ body { /* timeline */ +.onionr-post-focus-separator { + width: 100%; + + padding: 1rem; + padding-left: 0; + padding-right: 0; +} + .onionr-post { padding: 1rem; margin-bottom: 1rem; + cursor: pointer; + width: 100%; } @@ -60,6 +70,35 @@ body { width: 100%; } +.onionr-post-creator { + padding: 1rem; + margin-bottom: 1rem; + + width: 100%; +} + +.onionr-post-creator-user-name { + display: inline; +} + +.onionr-post-creator-user-id:before { content: "("; } +.onionr-post-creator-user-id:after { content: ")"; } + +.onionr-post-creator-content { + word-wrap: break-word; + width: 100%; +} + +.onionr-post-creator-user-icon { + border-radius: 100%; + width: 100%; +} + +.onionr-post-creator-create { + width: 100%; + text-align: center; +} + .h-divider { margin: 5px 15px; height: 1px; @@ -77,3 +116,7 @@ body { .onionr-profile-username { text-align: center; } + +.onionr-profile-save { + width: 100%; +} diff --git a/onionr/static-data/www/ui/src/css/themes/dark.css b/onionr/static-data/www/ui/src/css/themes/dark.css index b0473390..a81e25cf 100644 --- a/onionr/static-data/www/ui/src/css/themes/dark.css +++ b/onionr/static-data/www/ui/src/css/themes/dark.css @@ -5,6 +5,17 @@ body { /* timeline */ +.onionr-post-focus-separator { + border-color: black; +} + +.modal-content { + border: 1px solid black; + border-radius: 1rem; + + background-color: lightgray; +} + .onionr-post { border: 1px solid black; border-radius: 1rem; @@ -31,6 +42,35 @@ body { font-size: 15pt; } +.onionr-post-creator { + border: 1px solid black; + border-radius: 1rem; + + background-color: lightgray; +} + +.onionr-post-creator-user-name { + color: green; + font-weight: bold; +} + +.onionr-post-creator-user-id { + color: gray; +} + +.onionr-post-creator-date { + color: gray; +} + +.onionr-post-creator-content { + font-family: sans-serif, serif; + border-top: 1px solid black; + font-size: 15pt; + background-color: lightgray; + color: black; + border-width: 0px; +} + .h-divider { border-top:1px solid gray; } diff --git a/onionr/static-data/www/ui/src/index.html b/onionr/static-data/www/ui/src/index.html index e5791cb9..3fd62f87 100644 --- a/onionr/static-data/www/ui/src/index.html +++ b/onionr/static-data/www/ui/src/index.html @@ -10,10 +10,24 @@
- +
-

arinerron

+

+
+
+

+
+ +
@@ -22,6 +36,40 @@
+
+
+
+

<$= LANG.TIMELINE $>

+
+
+ + +
+
+
+
+ +
+
+ + + + +
+ + +
+
+
+
+ +
+
@@ -30,15 +78,58 @@
- + +
+ +
+
+ +
+
+ + + +