merge contacts
This commit is contained in:
commit
fc6dc0caf7
112 changed files with 1567 additions and 3821 deletions
0
onionr/__init__.py
Normal file
0
onionr/__init__.py
Normal file
208
onionr/api.py
208
onionr/api.py
|
@ -19,9 +19,7 @@
|
|||
'''
|
||||
from gevent.pywsgi import WSGIServer, WSGIHandler
|
||||
from gevent import Timeout
|
||||
#import gevent.monkey
|
||||
#gevent.monkey.patch_socket()
|
||||
import flask, cgi
|
||||
import flask, cgi, uuid
|
||||
from flask import request, Response, abort, send_from_directory
|
||||
import sys, random, threading, hmac, hashlib, base64, time, math, os, json, socket
|
||||
import core
|
||||
|
@ -29,34 +27,15 @@ from onionrblockapi import Block
|
|||
import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config, onionr
|
||||
|
||||
class FDSafeHandler(WSGIHandler):
|
||||
'''Our WSGI handler. Doesn't do much non-default except timeouts'''
|
||||
def handle(self):
|
||||
timeout = Timeout(60, exception=Exception)
|
||||
timeout.start()
|
||||
|
||||
#timeout = gevent.Timeout.start_new(3)
|
||||
try:
|
||||
WSGIHandler.handle(self)
|
||||
except Timeout as ex:
|
||||
raise
|
||||
|
||||
def guessMime(path):
|
||||
'''
|
||||
Guesses the mime type of a file from the input filename
|
||||
'''
|
||||
mimetypes = {
|
||||
'html' : 'text/html',
|
||||
'js' : 'application/javascript',
|
||||
'css' : 'text/css',
|
||||
'png' : 'image/png',
|
||||
'jpg' : 'image/jpeg'
|
||||
}
|
||||
|
||||
for mimetype in mimetypes:
|
||||
if path.endswith('.%s' % mimetype):
|
||||
return mimetypes[mimetype]
|
||||
|
||||
return 'text/plain'
|
||||
|
||||
def setBindIP(filePath):
|
||||
'''Set a random localhost IP to a specified file (intended for private or public API localhost IPs)'''
|
||||
hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))]
|
||||
|
@ -67,6 +46,7 @@ def setBindIP(filePath):
|
|||
try:
|
||||
s.bind((data, 0))
|
||||
except OSError:
|
||||
# if mac/non-bindable, show warning and default to 127.0.0.1
|
||||
logger.warn('Your platform appears to not support random local host addresses 127.x.x.x. Falling back to 127.0.0.1.')
|
||||
data = '127.0.0.1'
|
||||
s.close()
|
||||
|
@ -89,33 +69,42 @@ class PublicAPI:
|
|||
self.torAdder = clientAPI._core.hsAddress
|
||||
self.i2pAdder = clientAPI._core.i2pAddress
|
||||
self.bindPort = config.get('client.public.port')
|
||||
self.lastRequest = 0
|
||||
logger.info('Running public api on %s:%s' % (self.host, self.bindPort))
|
||||
|
||||
@app.before_request
|
||||
def validateRequest():
|
||||
'''Validate request has the correct hostname'''
|
||||
# If high security level, deny requests to public
|
||||
# If high security level, deny requests to public (HS should be disabled anyway for Tor, but might not be for I2P)
|
||||
if config.get('general.security_level', default=0) > 0:
|
||||
abort(403)
|
||||
if type(self.torAdder) is None and type(self.i2pAdder) is None:
|
||||
# abort if our hs addresses are not known
|
||||
abort(403)
|
||||
if request.host not in (self.i2pAdder, self.torAdder):
|
||||
# Disallow connection if wrong HTTP hostname, in order to prevent DNS rebinding attacks
|
||||
abort(403)
|
||||
|
||||
@app.after_request
|
||||
def sendHeaders(resp):
|
||||
'''Send api, access control headers'''
|
||||
resp.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch.
|
||||
resp.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch, since we can't fully remove the header.
|
||||
# CSP to prevent XSS. Mainly for client side attacks (if hostname protection could somehow be bypassed)
|
||||
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'"
|
||||
# Prevent click jacking
|
||||
resp.headers['X-Frame-Options'] = 'deny'
|
||||
# No sniff is possibly not needed
|
||||
resp.headers['X-Content-Type-Options'] = "nosniff"
|
||||
# Network API version
|
||||
resp.headers['X-API'] = onionr.API_VERSION
|
||||
# Close connections to limit FD use
|
||||
resp.headers['Connection'] = "close"
|
||||
self.lastRequest = clientAPI._core._utils.getRoundedEpoch(roundS=5)
|
||||
return resp
|
||||
|
||||
@app.route('/')
|
||||
def banner():
|
||||
# Display a bit of information to people who visit a node address in their browser
|
||||
try:
|
||||
with open('static-data/index.html', 'r') as html:
|
||||
resp = Response(html.read(), mimetype='text/html')
|
||||
|
@ -125,41 +114,48 @@ class PublicAPI:
|
|||
|
||||
@app.route('/getblocklist')
|
||||
def getBlockList():
|
||||
# Provide a list of our blocks, with a date offset
|
||||
dateAdjust = request.args.get('date')
|
||||
bList = clientAPI._core.getBlockList(dateRec=dateAdjust)
|
||||
for b in self.hideBlocks:
|
||||
if b in bList:
|
||||
# Don't share blocks we created if they haven't been *uploaded* yet, makes it harder to find who created a block
|
||||
bList.remove(b)
|
||||
return Response('\n'.join(bList))
|
||||
|
||||
@app.route('/getdata/<name>')
|
||||
def getBlockData(name):
|
||||
# Share data for a block if we have it
|
||||
resp = ''
|
||||
data = name
|
||||
if clientAPI._utils.validateHash(data):
|
||||
if data not in self.hideBlocks:
|
||||
if data in clientAPI._core.getBlockList():
|
||||
block = clientAPI.getBlockData(data, raw=True).encode()
|
||||
resp = base64.b64encode(block).decode()
|
||||
block = clientAPI.getBlockData(data, raw=True)
|
||||
try:
|
||||
block = block.encode()
|
||||
except AttributeError:
|
||||
abort(404)
|
||||
block = clientAPI._core._utils.strToBytes(block)
|
||||
resp = block
|
||||
#resp = base64.b64encode(block).decode()
|
||||
if len(resp) == 0:
|
||||
abort(404)
|
||||
resp = ""
|
||||
return Response(resp)
|
||||
return Response(resp, mimetype='application/octet-stream')
|
||||
|
||||
@app.route('/www/<path:path>')
|
||||
def wwwPublic(path):
|
||||
# A way to share files directly over your .onion
|
||||
if not config.get("www.public.run", True):
|
||||
abort(403)
|
||||
return send_from_directory(config.get('www.public.path', 'static-data/www/public/'), path)
|
||||
|
||||
@app.route('/ping')
|
||||
def ping():
|
||||
# Endpoint to test if nodes are up
|
||||
return Response("pong!")
|
||||
|
||||
@app.route('/getdbhash')
|
||||
def getDBHash():
|
||||
return Response(clientAPI._utils.getBlockDBHash())
|
||||
|
||||
@app.route('/pex')
|
||||
def peerExchange():
|
||||
response = ','.join(clientAPI._core.listAdders(recent=3600))
|
||||
|
@ -194,11 +190,9 @@ class PublicAPI:
|
|||
except AttributeError:
|
||||
pass
|
||||
if powHash.startswith('0000'):
|
||||
try:
|
||||
newNode = newNode.decode()
|
||||
except AttributeError:
|
||||
pass
|
||||
if clientAPI._core.addAddress(newNode):
|
||||
newNode = clientAPI._core._utils.bytesToStr(newNode)
|
||||
if clientAPI._core._utils.validateID(newNode) and not newNode in clientAPI._core.onionrInst.communicatorInst.newPeers:
|
||||
clientAPI._core.onionrInst.communicatorInst.newPeers.append(newNode)
|
||||
resp = 'Success'
|
||||
else:
|
||||
logger.warn(newNode.decode() + ' failed to meet POW: ' + powHash)
|
||||
|
@ -207,6 +201,9 @@ class PublicAPI:
|
|||
|
||||
@app.route('/upload', methods=['post'])
|
||||
def upload():
|
||||
'''Accept file uploads. In the future this will be done more often than on creation
|
||||
to speed up block sync
|
||||
'''
|
||||
resp = 'failure'
|
||||
try:
|
||||
data = request.form['block']
|
||||
|
@ -228,6 +225,7 @@ class PublicAPI:
|
|||
resp = Response(resp)
|
||||
return resp
|
||||
|
||||
# Set instances, then startup our public api server
|
||||
clientAPI.setPublicAPIInstance(self)
|
||||
while self.torAdder == '':
|
||||
clientAPI._core.refreshFirstStartVars()
|
||||
|
@ -255,7 +253,6 @@ class API:
|
|||
onionr.Onionr.setupConfig('data/', self = self)
|
||||
|
||||
self.debug = debug
|
||||
self._privateDelayTime = 3
|
||||
self._core = onionrInst.onionrCore
|
||||
self.startTime = self._core._utils.getEpoch()
|
||||
self._crypto = onionrcrypto.OnionrCrypto(self._core)
|
||||
|
@ -264,7 +261,7 @@ class API:
|
|||
bindPort = int(config.get('client.client.port', 59496))
|
||||
self.bindPort = bindPort
|
||||
|
||||
# Be extremely mindful of this
|
||||
# Be extremely mindful of this. These are endpoints available without a password
|
||||
self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'board', 'boardContent', 'sharedContent', 'mail', 'mailindex')
|
||||
|
||||
self.clientToken = config.get('client.webpassword')
|
||||
|
@ -276,12 +273,14 @@ class API:
|
|||
logger.info('Running api on %s:%s' % (self.host, self.bindPort))
|
||||
self.httpServer = ''
|
||||
|
||||
self.pluginResponses = {} # Responses for plugin endpoints
|
||||
self.queueResponse = {}
|
||||
onionrInst.setClientAPIInst(self)
|
||||
|
||||
@app.before_request
|
||||
def validateRequest():
|
||||
'''Validate request has set password and is the correct hostname'''
|
||||
# For the purpose of preventing DNS rebinding attacks
|
||||
if request.host != '%s:%s' % (self.host, self.bindPort):
|
||||
abort(403)
|
||||
if request.endpoint in self.whitelistEndpoints:
|
||||
|
@ -294,11 +293,13 @@ class API:
|
|||
|
||||
@app.after_request
|
||||
def afterReq(resp):
|
||||
#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['Content-Security-Policy'] = "default-src 'none'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'self'"
|
||||
# Security headers
|
||||
if request.endpoint == 'site':
|
||||
resp.headers['Content-Security-Policy'] = "default-src 'none'; style-src data: 'unsafe-inline'; img-src data:"
|
||||
else:
|
||||
resp.headers['Content-Security-Policy'] = "default-src 'none'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'self'"
|
||||
resp.headers['X-Frame-Options'] = 'deny'
|
||||
resp.headers['X-Content-Type-Options'] = "nosniff"
|
||||
resp.headers['X-API'] = onionr.API_VERSION
|
||||
resp.headers['Server'] = ''
|
||||
resp.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch.
|
||||
resp.headers['Connection'] = "close"
|
||||
|
@ -330,11 +331,13 @@ class API:
|
|||
|
||||
@app.route('/queueResponseAdd/<name>', methods=['post'])
|
||||
def queueResponseAdd(name):
|
||||
# Responses from the daemon. TODO: change to direct var access instead of http endpoint
|
||||
self.queueResponse[name] = request.form['data']
|
||||
return Response('success')
|
||||
|
||||
@app.route('/queueResponse/<name>')
|
||||
def queueResponse(name):
|
||||
# Fetch a daemon queue response
|
||||
resp = 'failure'
|
||||
try:
|
||||
resp = self.queueResponse[name]
|
||||
|
@ -346,10 +349,12 @@ class API:
|
|||
|
||||
@app.route('/ping')
|
||||
def ping():
|
||||
# Used to check if client api is working
|
||||
return Response("pong!")
|
||||
|
||||
@app.route('/', endpoint='onionrhome')
|
||||
def hello():
|
||||
# ui home
|
||||
return send_from_directory('static-data/www/private/', 'index.html')
|
||||
|
||||
@app.route('/getblocksbytype/<name>')
|
||||
|
@ -357,12 +362,13 @@ class API:
|
|||
blocks = self._core.getBlocksByType(name)
|
||||
return Response(','.join(blocks))
|
||||
|
||||
@app.route('/gethtmlsafeblockdata/<name>')
|
||||
def getSafeData(name):
|
||||
@app.route('/getblockbody/<name>')
|
||||
def getBlockBodyData(name):
|
||||
resp = ''
|
||||
if self._core._utils.validateHash(name):
|
||||
try:
|
||||
resp = cgi.escape(Block(name).bcontent, quote=True)
|
||||
resp = Block(name, decrypt=True).bcontent
|
||||
#resp = cgi.escape(Block(name, decrypt=True).bcontent, quote=True)
|
||||
except TypeError:
|
||||
pass
|
||||
else:
|
||||
|
@ -384,6 +390,15 @@ class API:
|
|||
abort(404)
|
||||
return Response(resp)
|
||||
|
||||
@app.route('/getblockheader/<name>')
|
||||
def getBlockHeader(name):
|
||||
resp = self.getBlockData(name, decrypt=True, headerOnly=True)
|
||||
return Response(resp)
|
||||
|
||||
@app.route('/lastconnect')
|
||||
def lastConnect():
|
||||
return Response(str(self.publicAPI.lastRequest))
|
||||
|
||||
@app.route('/site/<name>', endpoint='site')
|
||||
def site(name):
|
||||
bHash = name
|
||||
|
@ -402,7 +417,8 @@ class API:
|
|||
return Response(resp)
|
||||
|
||||
@app.route('/waitforshare/<name>', methods=['post'])
|
||||
def waitforshare():
|
||||
def waitforshare(name):
|
||||
'''Used to prevent the **public** api from sharing blocks we just created'''
|
||||
assert name.isalnum()
|
||||
if name in self.publicAPI.hideBlocks:
|
||||
self.publicAPI.hideBlocks.remove(name)
|
||||
|
@ -428,6 +444,7 @@ class API:
|
|||
|
||||
@app.route('/getstats')
|
||||
def getStats():
|
||||
# returns node stats
|
||||
#return Response("disabled")
|
||||
while True:
|
||||
try:
|
||||
|
@ -438,6 +455,78 @@ class API:
|
|||
@app.route('/getuptime')
|
||||
def showUptime():
|
||||
return Response(str(self.getUptime()))
|
||||
|
||||
@app.route('/getActivePubkey')
|
||||
def getActivePubkey():
|
||||
return Response(self._core._crypto.pubKey)
|
||||
|
||||
@app.route('/getHumanReadable/<name>')
|
||||
def getHumanReadable(name):
|
||||
return Response(self._core._utils.getHumanReadableID(name))
|
||||
|
||||
@app.route('/insertblock', methods=['POST'])
|
||||
def insertBlock():
|
||||
encrypt = False
|
||||
bData = request.get_json(force=True)
|
||||
message = bData['message']
|
||||
subject = 'temp'
|
||||
encryptType = ''
|
||||
sign = True
|
||||
meta = {}
|
||||
to = ''
|
||||
try:
|
||||
if bData['encrypt']:
|
||||
to = bData['to']
|
||||
encrypt = True
|
||||
encryptType = 'asym'
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
if not bData['sign']:
|
||||
sign = False
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
bType = bData['type']
|
||||
except KeyError:
|
||||
bType = 'bin'
|
||||
try:
|
||||
meta = json.loads(bData['meta'])
|
||||
except KeyError:
|
||||
pass
|
||||
threading.Thread(target=self._core.insertBlock, args=(message,), kwargs={'header': bType, 'encryptType': encryptType, 'sign':sign, 'asymPeer': to, 'meta': meta}).start()
|
||||
return Response('success')
|
||||
|
||||
@app.route('/apipoints/<path:subpath>', methods=['POST', 'GET'])
|
||||
def pluginEndpoints(subpath=''):
|
||||
'''Send data to plugins'''
|
||||
# TODO have a variable for the plugin to set data to that we can use for the response
|
||||
pluginResponseCode = str(uuid.uuid4())
|
||||
resp = 'success'
|
||||
responseTimeout = 20
|
||||
startTime = self._core._utils.getEpoch()
|
||||
postData = {}
|
||||
if request.method == 'POST':
|
||||
postData = request.form['postData']
|
||||
if len(subpath) > 1:
|
||||
data = subpath.split('/')
|
||||
if len(data) > 1:
|
||||
plName = data[0]
|
||||
|
||||
events.event('pluginRequest', {'name': plName, 'path': subpath, 'pluginResponse': pluginResponseCode, 'postData': postData}, onionr=onionrInst)
|
||||
while True:
|
||||
try:
|
||||
resp = self.pluginResponses[pluginResponseCode]
|
||||
except KeyError:
|
||||
time.sleep(0.2)
|
||||
if self._core._utils.getEpoch() - startTime > responseTimeout:
|
||||
abort(504)
|
||||
break
|
||||
else:
|
||||
break
|
||||
else:
|
||||
abort(404)
|
||||
return Response(resp)
|
||||
|
||||
self.httpServer = WSGIServer((self.host, bindPort), app, log=None, handler_class=FDSafeHandler)
|
||||
self.httpServer.serve_forever()
|
||||
|
@ -448,7 +537,7 @@ class API:
|
|||
|
||||
def validateToken(self, token):
|
||||
'''
|
||||
Validate that the client token matches the given token
|
||||
Validate that the client token matches the given token. Used to prevent CSRF and data exfiltration
|
||||
'''
|
||||
if len(self.clientToken) == 0:
|
||||
logger.error("client password needs to be set")
|
||||
|
@ -469,7 +558,8 @@ class API:
|
|||
# Don't error on race condition with startup
|
||||
pass
|
||||
|
||||
def getBlockData(self, bHash, decrypt=False, raw=False):
|
||||
def getBlockData(self, bHash, decrypt=False, raw=False, headerOnly=False):
|
||||
assert self._core._utils.validateHash(bHash)
|
||||
bl = Block(bHash, core=self._core)
|
||||
if decrypt:
|
||||
bl.decrypt()
|
||||
|
@ -477,12 +567,22 @@ class API:
|
|||
raise ValueError
|
||||
|
||||
if not raw:
|
||||
retData = {'meta':bl.bheader, 'metadata': bl.bmetadata, 'content': bl.bcontent}
|
||||
for x in list(retData.keys()):
|
||||
try:
|
||||
retData[x] = retData[x].decode()
|
||||
except AttributeError:
|
||||
pass
|
||||
if not headerOnly:
|
||||
retData = {'meta':bl.bheader, 'metadata': bl.bmetadata, 'content': bl.bcontent}
|
||||
for x in list(retData.keys()):
|
||||
try:
|
||||
retData[x] = retData[x].decode()
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
validSig = False
|
||||
signer = self._core._utils.bytesToStr(bl.signer)
|
||||
#print(signer, bl.isSigned(), self._core._utils.validatePubKey(signer), bl.isSigner(signer))
|
||||
if bl.isSigned() and self._core._utils.validatePubKey(signer) and bl.isSigner(signer):
|
||||
validSig = True
|
||||
bl.bheader['validSig'] = validSig
|
||||
bl.bheader['meta'] = ''
|
||||
retData = {'meta': bl.bheader, 'metadata': bl.bmetadata}
|
||||
return json.dumps(retData)
|
||||
else:
|
||||
return bl.raw
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
'''
|
||||
Onionr - P2P Anonymous Storage Network
|
||||
|
||||
Handles api data exchange, interfaced by both public and client http api
|
||||
'''
|
||||
'''
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
import config, apipublic, apiprivate, core, socket, random, threading, time
|
||||
config.reload()
|
||||
|
||||
PRIVATE_API_VERSION = 0
|
||||
PUBLIC_API_VERSION = 1
|
||||
|
||||
DEV_MODE = config.get('general.dev_mode')
|
||||
|
||||
def getOpenPort():
|
||||
'''Get a random open port'''
|
||||
p = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
p.bind(("127.0.0.1",0))
|
||||
p.listen(1)
|
||||
port = p.getsockname()[1]
|
||||
p.close()
|
||||
return port
|
||||
|
||||
def getRandomLocalIP():
|
||||
'''Get a random local ip address'''
|
||||
hostOctets = [str(127), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF)), str(random.randint(0x02, 0xFF))]
|
||||
host = '.'.join(hostOctets)
|
||||
return host
|
||||
|
||||
class APIManager:
|
||||
def __init__(self, coreInst):
|
||||
assert isinstance(coreInst, core.Core)
|
||||
self.core = coreInst
|
||||
self.utils = coreInst._utils
|
||||
self.crypto = coreInst._crypto
|
||||
|
||||
# if this gets set to true, both the public and private apis will shutdown
|
||||
self.shutdown = False
|
||||
|
||||
publicIP = '127.0.0.1'
|
||||
privateIP = '127.0.0.1'
|
||||
if DEV_MODE:
|
||||
# set private and local api servers bind IPs to random localhost (127.x.x.x), make sure not the same
|
||||
privateIP = getRandomLocalIP()
|
||||
while True:
|
||||
publicIP = getRandomLocalIP()
|
||||
if publicIP != privateIP:
|
||||
break
|
||||
|
||||
# Make official the IPs and Ports
|
||||
self.publicIP = publicIP
|
||||
self.privateIP = privateIP
|
||||
self.publicPort = config.get('client.port', 59496)
|
||||
self.privatePort = config.get('client.port', 59496)
|
||||
|
||||
# Run the API servers in new threads
|
||||
self.publicAPI = apipublic.APIPublic(self)
|
||||
self.privateAPI = apiprivate.APIPrivate(self)
|
||||
threading.Thread(target=self.publicAPI.run).start()
|
||||
threading.Thread(target=self.privateAPI.run).start()
|
||||
while not self.shutdown:
|
||||
time.sleep(1)
|
|
@ -1,41 +0,0 @@
|
|||
'''
|
||||
Onionr - P2P Anonymous Storage Network
|
||||
|
||||
Handle incoming commands from other Onionr nodes, over HTTP
|
||||
'''
|
||||
'''
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
import flask, apimanager
|
||||
from flask import request, Response, abort, send_from_directory
|
||||
from gevent.pywsgi import WSGIServer
|
||||
|
||||
|
||||
class APIPublic:
|
||||
def __init__(self, managerInst):
|
||||
assert isinstance(managerInst, apimanager.APIManager)
|
||||
app = flask.Flask(__name__)
|
||||
@app.route('/')
|
||||
def banner():
|
||||
try:
|
||||
with open('static-data/index.html', 'r') as html:
|
||||
resp = Response(html.read(), mimetype='text/html')
|
||||
except FileNotFoundError:
|
||||
resp = Response("")
|
||||
return resp
|
||||
self.httpServer = WSGIServer((managerInst.publicIP, managerInst.publicPort), app)
|
||||
|
||||
def run(self):
|
||||
self.httpServer.serve_forever()
|
||||
return
|
0
onionr/blockimporter.py
Normal file → Executable file
0
onionr/blockimporter.py
Normal file → Executable file
|
@ -21,10 +21,12 @@
|
|||
'''
|
||||
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, onionr, onionrproofs, proofofmemory
|
||||
import onionrdaemontools, onionrsockets, onionr, onionrproofs
|
||||
import binascii
|
||||
from dependencies import secrets
|
||||
from defusedxml import minidom
|
||||
from utils import networkmerger
|
||||
|
||||
config.reload()
|
||||
class OnionrCommunicatorDaemon:
|
||||
def __init__(self, onionrInst, proxyPort, developmentMode=config.get('general.dev_mode', False)):
|
||||
|
@ -38,11 +40,11 @@ class OnionrCommunicatorDaemon:
|
|||
# list of timer instances
|
||||
self.timers = []
|
||||
|
||||
# initalize core with Tor socks port being 3rd argument
|
||||
# initialize core with Tor socks port being 3rd argument
|
||||
self.proxyPort = proxyPort
|
||||
self._core = onionrInst.onionrCore
|
||||
|
||||
# intalize NIST beacon salt and time
|
||||
# initialize NIST beacon salt and time
|
||||
self.nistSaltTimestamp = 0
|
||||
self.powSalt = 0
|
||||
|
||||
|
@ -57,11 +59,12 @@ class OnionrCommunicatorDaemon:
|
|||
self.cooldownPeer = {}
|
||||
self.connectTimes = {}
|
||||
self.peerProfiles = [] # list of peer's profiles (onionrpeers.PeerProfile instances)
|
||||
self.newPeers = [] # Peers merged to us. Don't add to db until we know they're reachable
|
||||
|
||||
# amount of threads running by name, used to prevent too many
|
||||
self.threadCounts = {}
|
||||
|
||||
# set true when shutdown command recieved
|
||||
# set true when shutdown command received
|
||||
self.shutdown = False
|
||||
|
||||
# list of new blocks to download, added to when new block lists are fetched from peers
|
||||
|
@ -130,7 +133,6 @@ class OnionrCommunicatorDaemon:
|
|||
self.socketServer.start()
|
||||
self.socketClient = onionrsockets.OnionrSocketClient(self._core)
|
||||
|
||||
|
||||
# Main daemon loop, mainly for calling timers, don't do any complex operations here to avoid locking
|
||||
try:
|
||||
while not self.shutdown:
|
||||
|
@ -155,11 +157,27 @@ class OnionrCommunicatorDaemon:
|
|||
'''Lookup new peer addresses'''
|
||||
logger.info('Looking up new addresses...')
|
||||
tryAmount = 1
|
||||
newPeers = []
|
||||
for i in range(tryAmount):
|
||||
# Download new peer address list from random online peers
|
||||
if len(newPeers) > 10000:
|
||||
# Dont get new peers if we have too many queued up
|
||||
break
|
||||
peer = self.pickOnlinePeer()
|
||||
newAdders = self.peerAction(peer, action='pex')
|
||||
self._core._utils.mergeAdders(newAdders)
|
||||
try:
|
||||
newPeers = newAdders.split(',')
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
# Validate new peers are good format and not already in queue
|
||||
invalid = []
|
||||
for x in newPeers:
|
||||
if not self._core._utils.validateID(x) or x in self.newPeers:
|
||||
invalid.append(x)
|
||||
for x in invalid:
|
||||
newPeers.remove(x)
|
||||
self.newPeers.extend(newPeers)
|
||||
self.decrementThreadCount('lookupAdders')
|
||||
|
||||
def lookupBlocks(self):
|
||||
|
@ -188,40 +206,37 @@ class OnionrCommunicatorDaemon:
|
|||
break
|
||||
else:
|
||||
continue
|
||||
newDBHash = self.peerAction(peer, 'getdbhash') # get their db hash
|
||||
if newDBHash == False or not self._core._utils.validateHash(newDBHash):
|
||||
continue # if request failed, restart loop (peer is added to offline peers automatically)
|
||||
triedPeers.append(peer)
|
||||
if newDBHash != self._core.getAddressInfo(peer, 'DBHash'):
|
||||
self._core.setAddressInfo(peer, 'DBHash', newDBHash)
|
||||
# Get the last time we looked up a peer's stamp to only fetch blocks since then.
|
||||
# Saved in memory only for privacy reasons
|
||||
try:
|
||||
lastLookupTime = self.dbTimestamps[peer]
|
||||
except KeyError:
|
||||
lastLookupTime = 0
|
||||
else:
|
||||
listLookupCommand += '?date=%s' % (lastLookupTime,)
|
||||
try:
|
||||
newBlocks = self.peerAction(peer, listLookupCommand) # get list of new block hashes
|
||||
except Exception as error:
|
||||
logger.warn('Could not get new blocks from %s.' % peer, error = error)
|
||||
newBlocks = False
|
||||
else:
|
||||
self.dbTimestamps[peer] = self._core._utils.getRoundedEpoch(roundS=60)
|
||||
if newBlocks != False:
|
||||
# if request was a success
|
||||
for i in newBlocks.split('\n'):
|
||||
if self._core._utils.validateHash(i):
|
||||
# if newline seperated string is valid hash
|
||||
if not i in existingBlocks:
|
||||
# if block does not exist on disk and is not already in block queue
|
||||
if i not in self.blockQueue:
|
||||
if onionrproofs.hashMeetsDifficulty(i) and not self._core._blacklist.inBlacklist(i):
|
||||
if len(self.blockQueue) <= 1000000:
|
||||
self.blockQueue[i] = [peer] # add blocks to download queue
|
||||
else:
|
||||
if peer not in self.blockQueue[i]:
|
||||
|
||||
# Get the last time we looked up a peer's stamp to only fetch blocks since then.
|
||||
# Saved in memory only for privacy reasons
|
||||
try:
|
||||
lastLookupTime = self.dbTimestamps[peer]
|
||||
except KeyError:
|
||||
lastLookupTime = 0
|
||||
else:
|
||||
listLookupCommand += '?date=%s' % (lastLookupTime,)
|
||||
try:
|
||||
newBlocks = self.peerAction(peer, listLookupCommand) # get list of new block hashes
|
||||
except Exception as error:
|
||||
logger.warn('Could not get new blocks from %s.' % peer, error = error)
|
||||
newBlocks = False
|
||||
else:
|
||||
self.dbTimestamps[peer] = self._core._utils.getRoundedEpoch(roundS=60)
|
||||
if newBlocks != False:
|
||||
# if request was a success
|
||||
for i in newBlocks.split('\n'):
|
||||
if self._core._utils.validateHash(i):
|
||||
# if newline seperated string is valid hash
|
||||
if not i in existingBlocks:
|
||||
# if block does not exist on disk and is not already in block queue
|
||||
if i not in self.blockQueue:
|
||||
if onionrproofs.hashMeetsDifficulty(i) and not self._core._blacklist.inBlacklist(i):
|
||||
if len(self.blockQueue) <= 1000000:
|
||||
self.blockQueue[i] = [peer] # add blocks to download queue
|
||||
else:
|
||||
if peer not in self.blockQueue[i]:
|
||||
if len(self.blockQueue[i]) < 10:
|
||||
self.blockQueue[i].append(peer)
|
||||
self.decrementThreadCount('lookupBlocks')
|
||||
return
|
||||
|
@ -240,10 +255,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 block %s...' % blockHash)
|
||||
#logger.debug('Already downloading block %s...' % blockHash)
|
||||
continue
|
||||
if blockHash in self._core.getBlockList():
|
||||
logger.debug('Block %s is already saved.' % (blockHash,))
|
||||
#logger.debug('Block %s is already saved.' % (blockHash,))
|
||||
try:
|
||||
del self.blockQueue[blockHash]
|
||||
except KeyError:
|
||||
|
@ -268,10 +283,7 @@ class OnionrCommunicatorDaemon:
|
|||
content = content.encode()
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
content = base64.b64decode(content) # content is base64 encoded in transport
|
||||
except binascii.Error:
|
||||
pass
|
||||
|
||||
realHash = self._core._crypto.sha3Hash(content)
|
||||
try:
|
||||
realHash = realHash.decode() # bytes on some versions for some reason
|
||||
|
@ -402,8 +414,18 @@ class OnionrCommunicatorDaemon:
|
|||
else:
|
||||
peerList = self._core.listAdders()
|
||||
|
||||
mainPeerList = self._core.listAdders()
|
||||
peerList = onionrpeers.getScoreSortedPeerList(self._core)
|
||||
|
||||
if len(peerList) < 8 or secrets.randbelow(4) == 3:
|
||||
tryingNew = []
|
||||
for x in self.newPeers:
|
||||
if x not in peerList:
|
||||
peerList.append(x)
|
||||
tryingNew.append(x)
|
||||
for i in tryingNew:
|
||||
self.newPeers.remove(i)
|
||||
|
||||
if len(peerList) == 0 or useBootstrap:
|
||||
# Avoid duplicating bootstrap addresses in peerList
|
||||
self.addBootstrapListToPeerList(peerList)
|
||||
|
@ -418,6 +440,8 @@ class OnionrCommunicatorDaemon:
|
|||
if self.peerAction(address, 'ping') == 'pong!':
|
||||
logger.info('Connected to ' + address)
|
||||
time.sleep(0.1)
|
||||
if address not in mainPeerList:
|
||||
networkmerger.mergeAdders(address, self._core)
|
||||
if address not in self.onlinePeers:
|
||||
self.onlinePeers.append(address)
|
||||
self.connectTimes[address] = self._core._utils.getEpoch()
|
||||
|
@ -588,7 +612,7 @@ class OnionrCommunicatorDaemon:
|
|||
proxyType = 'i2p'
|
||||
logger.info("Uploading block to " + peer)
|
||||
if not self._core._utils.doPostRequest(url, data=data, proxyType=proxyType) == False:
|
||||
self._core._utils.localCommand('waitforshare/' + bl)
|
||||
self._core._utils.localCommand('waitforshare/' + bl, post=True)
|
||||
finishedUploads.append(bl)
|
||||
for x in finishedUploads:
|
||||
try:
|
||||
|
@ -605,8 +629,8 @@ class OnionrCommunicatorDaemon:
|
|||
def detectAPICrash(self):
|
||||
'''exit if the api server crashes/stops'''
|
||||
if self._core._utils.localCommand('ping', silent=False) not in ('pong', 'pong!'):
|
||||
for i in range(5):
|
||||
if self._core._utils.localCommand('ping') in ('pong', 'pong!'):
|
||||
for i in range(8):
|
||||
if self._core._utils.localCommand('ping') in ('pong', 'pong!') or self.shutdown:
|
||||
break # break for loop
|
||||
time.sleep(1)
|
||||
else:
|
||||
|
|
0
onionr/config.py
Normal file → Executable file
0
onionr/config.py
Normal file → Executable file
86
onionr/core.py
Normal file → Executable file
86
onionr/core.py
Normal file → Executable file
|
@ -21,7 +21,8 @@ import sqlite3, os, sys, time, math, base64, tarfile, nacl, logger, json, netcon
|
|||
from onionrblockapi import Block
|
||||
|
||||
import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions
|
||||
import onionrblacklist, onionrusers
|
||||
import onionrblacklist
|
||||
from onionrusers import onionrusers
|
||||
import dbcreator, onionrstorage, serializeddata
|
||||
from etc import onionrvalues
|
||||
|
||||
|
@ -56,7 +57,7 @@ class Core:
|
|||
self.privateApiHostFile = self.dataDir + 'private-host.txt'
|
||||
self.addressDB = self.dataDir + 'address.db'
|
||||
self.hsAddress = ''
|
||||
self.i2pAddress = config.get('i2p.ownAddr', None)
|
||||
self.i2pAddress = config.get('i2p.own_addr', None)
|
||||
self.bootstrapFileLocation = 'static-data/bootstrap-nodes.txt'
|
||||
self.bootstrapList = []
|
||||
self.requirements = onionrvalues.OnionrValues()
|
||||
|
@ -85,6 +86,10 @@ class Core:
|
|||
self.createBlockDB()
|
||||
if not os.path.exists(self.forwardKeysFile):
|
||||
self.dbCreate.createForwardKeyDB()
|
||||
if not os.path.exists(self.peerDB):
|
||||
self.createPeerDB()
|
||||
if not os.path.exists(self.addressDB):
|
||||
self.createAddressDB()
|
||||
|
||||
if os.path.exists(self.dataDir + '/hs/hostname'):
|
||||
with open(self.dataDir + '/hs/hostname', 'r') as hs:
|
||||
|
@ -125,6 +130,7 @@ class Core:
|
|||
'''
|
||||
Adds a public key to the key database (misleading function name)
|
||||
'''
|
||||
assert peerID not in self.listPeers()
|
||||
|
||||
# This function simply adds a peer to the DB
|
||||
if not self._utils.validatePubKey(peerID):
|
||||
|
@ -221,18 +227,8 @@ class Core:
|
|||
c.execute('Delete from hashes where hash=?;', t)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
blockFile = self.dataDir + '/blocks/%s.dat' % block
|
||||
dataSize = 0
|
||||
try:
|
||||
''' 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:
|
||||
dataSize = sys.getsizeof(data.read())
|
||||
self._utils.storageCounter.removeBytes(dataSize)
|
||||
os.remove(blockFile)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
dataSize = sys.getsizeof(onionrstorage.getData(self, block))
|
||||
self._utils.storageCounter.removeBytes(dataSize)
|
||||
|
||||
def createAddressDB(self):
|
||||
'''
|
||||
|
@ -282,15 +278,6 @@ 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')
|
||||
data = dataFile.read()
|
||||
dataFile.close()
|
||||
except FileNotFoundError:
|
||||
data = False
|
||||
'''
|
||||
data = onionrstorage.getData(self, hash)
|
||||
|
||||
return data
|
||||
|
@ -316,9 +303,6 @@ class Core:
|
|||
#raise Exception("Data is already set for " + dataHash)
|
||||
else:
|
||||
if self._utils.storageCounter.addBytes(dataSize) != False:
|
||||
#blockFile = open(blockFileName, 'wb')
|
||||
#blockFile.write(data)
|
||||
#blockFile.close()
|
||||
onionrstorage.store(self, data, blockHash=dataHash)
|
||||
conn = sqlite3.connect(self.blockDB, timeout=30)
|
||||
c = conn.cursor()
|
||||
|
@ -557,19 +541,18 @@ class Core:
|
|||
knownPeer text, 2
|
||||
speed int, 3
|
||||
success int, 4
|
||||
DBHash text, 5
|
||||
powValue 6
|
||||
failure int 7
|
||||
lastConnect 8
|
||||
trust 9
|
||||
introduced 10
|
||||
powValue 5
|
||||
failure int 6
|
||||
lastConnect 7
|
||||
trust 8
|
||||
introduced 9
|
||||
'''
|
||||
|
||||
conn = sqlite3.connect(self.addressDB, timeout=30)
|
||||
c = conn.cursor()
|
||||
|
||||
command = (address,)
|
||||
infoNumbers = {'address': 0, 'type': 1, 'knownPeer': 2, 'speed': 3, 'success': 4, 'DBHash': 5, 'powValue': 6, 'failure': 7, 'lastConnect': 8, 'trust': 9, 'introduced': 10}
|
||||
infoNumbers = {'address': 0, 'type': 1, 'knownPeer': 2, 'speed': 3, 'success': 4, 'powValue': 5, 'failure': 6, 'lastConnect': 7, 'trust': 8, 'introduced': 9}
|
||||
info = infoNumbers[info]
|
||||
iterCount = 0
|
||||
retVal = ''
|
||||
|
@ -595,7 +578,7 @@ class Core:
|
|||
|
||||
command = (data, address)
|
||||
|
||||
if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'DBHash', 'failure', 'powValue', 'lastConnect', 'lastConnectAttempt', 'trust', 'introduced'):
|
||||
if key not in ('address', 'type', 'knownPeer', 'speed', 'success', 'failure', 'powValue', 'lastConnect', 'lastConnectAttempt', 'trust', 'introduced'):
|
||||
raise Exception("Got invalid database key when setting address info")
|
||||
else:
|
||||
c.execute('UPDATE adders SET ' + key + ' = ? WHERE address=?', command)
|
||||
|
@ -680,19 +663,6 @@ class Core:
|
|||
conn.close()
|
||||
return rows
|
||||
|
||||
def setBlockType(self, hash, blockType):
|
||||
'''
|
||||
Sets the type of block
|
||||
'''
|
||||
|
||||
conn = sqlite3.connect(self.blockDB, timeout=30)
|
||||
c = conn.cursor()
|
||||
c.execute("UPDATE hashes SET dataType = ? WHERE hash = ?;", (blockType, hash))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return
|
||||
|
||||
def updateBlockInfo(self, hash, key, data):
|
||||
'''
|
||||
sets info associated with a block
|
||||
|
@ -731,6 +701,7 @@ class Core:
|
|||
logger.error(allocationReachedMessage)
|
||||
return False
|
||||
retData = False
|
||||
|
||||
# check nonce
|
||||
dataNonce = self._utils.bytesToStr(self._crypto.sha3Hash(data))
|
||||
try:
|
||||
|
@ -746,6 +717,12 @@ class Core:
|
|||
if type(data) is bytes:
|
||||
data = data.decode()
|
||||
data = str(data)
|
||||
plaintext = data
|
||||
plaintextMeta = {}
|
||||
|
||||
# Convert asym peer human readable key to base32 if set
|
||||
if ' ' in asymPeer.strip():
|
||||
asymPeer = self._utils.convertHumanReadableID(asymPeer)
|
||||
|
||||
retData = ''
|
||||
signature = ''
|
||||
|
@ -768,7 +745,7 @@ class Core:
|
|||
pass
|
||||
|
||||
if encryptType == 'asym':
|
||||
if not disableForward and asymPeer != self._crypto.pubKey:
|
||||
if not disableForward and sign and asymPeer != self._crypto.pubKey:
|
||||
try:
|
||||
forwardEncrypted = onionrusers.OnionrUser(self, asymPeer).forwardEncrypt(data)
|
||||
data = forwardEncrypted[0]
|
||||
|
@ -780,6 +757,7 @@ class Core:
|
|||
#fsKey = onionrusers.OnionrUser(self, asymPeer).getGeneratedForwardKeys().reverse()
|
||||
meta['newFSKey'] = fsKey
|
||||
jsonMeta = json.dumps(meta)
|
||||
plaintextMeta = jsonMeta
|
||||
if sign:
|
||||
signature = self._crypto.edSign(jsonMeta.encode() + data, key=self._crypto.privKey, encodeResult=True)
|
||||
signer = self._crypto.pubKey
|
||||
|
@ -802,10 +780,10 @@ class Core:
|
|||
if self._utils.validatePubKey(asymPeer):
|
||||
# Encrypt block data with forward secrecy key first, but not meta
|
||||
jsonMeta = json.dumps(meta)
|
||||
jsonMeta = self._crypto.pubKeyEncrypt(jsonMeta, asymPeer, encodedData=True, anonymous=True).decode()
|
||||
data = self._crypto.pubKeyEncrypt(data, asymPeer, encodedData=True, anonymous=True).decode()
|
||||
signature = self._crypto.pubKeyEncrypt(signature, asymPeer, encodedData=True, anonymous=True).decode()
|
||||
signer = self._crypto.pubKeyEncrypt(signer, asymPeer, encodedData=True, anonymous=True).decode()
|
||||
jsonMeta = self._crypto.pubKeyEncrypt(jsonMeta, asymPeer, encodedData=True).decode()
|
||||
data = self._crypto.pubKeyEncrypt(data, asymPeer, encodedData=True).decode()
|
||||
signature = self._crypto.pubKeyEncrypt(signature, asymPeer, encodedData=True).decode()
|
||||
signer = self._crypto.pubKeyEncrypt(signer, asymPeer, encodedData=True).decode()
|
||||
onionrusers.OnionrUser(self, asymPeer, saveUser=True)
|
||||
else:
|
||||
raise onionrexceptions.InvalidPubkey(asymPeer + ' is not a valid base32 encoded ed25519 key')
|
||||
|
@ -832,14 +810,14 @@ class Core:
|
|||
retData = False
|
||||
else:
|
||||
# Tell the api server through localCommand to wait for the daemon to upload this block to make stastical analysis more difficult
|
||||
self._utils.localCommand('waitforshare/' + retData)
|
||||
self._utils.localCommand('/waitforshare/' + retData, post=True)
|
||||
self.addToBlockDB(retData, selfInsert=True, dataSaved=True)
|
||||
#self.setBlockType(retData, meta['type'])
|
||||
self._utils.processBlockMetadata(retData)
|
||||
self.daemonQueueAdd('uploadBlock', retData)
|
||||
|
||||
if retData != False:
|
||||
events.event('insertBlock', onionr = None, threaded = False)
|
||||
events.event('insertblock', {'content': plaintext, 'meta': plaintextMeta, 'hash': retData, 'peer': self._utils.bytesToStr(asymPeer)}, onionr = self.onionrInst, threaded = True)
|
||||
return retData
|
||||
|
||||
def introduceNode(self):
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
'''
|
||||
Onionr - P2P Microblogging Platform & Social network
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
import unittest, sys, os, core, onionrcrypto, logger
|
||||
|
||||
class OnionrCryptoTests(unittest.TestCase):
|
||||
def testSymmetric(self):
|
||||
dataString = "this is a secret message"
|
||||
dataBytes = dataString.encode()
|
||||
myCore = core.Core()
|
||||
crypto = onionrcrypto.OnionrCrypto(myCore)
|
||||
key = key = b"tttttttttttttttttttttttttttttttt"
|
||||
|
||||
logger.info("Encrypting: " + dataString, timestamp=True)
|
||||
encrypted = crypto.symmetricEncrypt(dataString, key, returnEncoded=True)
|
||||
logger.info(encrypted, timestamp=True)
|
||||
logger.info('Decrypting encrypted string:', timestamp=True)
|
||||
decrypted = crypto.symmetricDecrypt(encrypted, key, encodedMessage=True)
|
||||
logger.info(decrypted, timestamp=True)
|
||||
self.assertTrue(True)
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
1
onionr/dbcreator.py
Normal file → Executable file
1
onionr/dbcreator.py
Normal file → Executable file
|
@ -39,7 +39,6 @@ class DBCreator:
|
|||
knownPeer text,
|
||||
speed int,
|
||||
success int,
|
||||
DBHash text,
|
||||
powValue text,
|
||||
failure int,
|
||||
lastConnect int,
|
||||
|
|
0
onionr/dependencies/secrets.py
Normal file → Executable file
0
onionr/dependencies/secrets.py
Normal file → Executable file
0
onionr/etc/onionrvalues.py
Normal file → Executable file
0
onionr/etc/onionrvalues.py
Normal file → Executable file
58
onionr/etc/pgpwords.py
Normal file → Executable file
58
onionr/etc/pgpwords.py
Normal file → Executable file
|
@ -1,7 +1,9 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*- (because 0xFF, even : "Yucatán")
|
||||
|
||||
import os, re, sys
|
||||
'''This file is adapted from https://github.com/thblt/pgp-words by github user 'thblt' ('Thibault Polge), GPL v3 license'''
|
||||
|
||||
import os, re, sys, binascii
|
||||
|
||||
_words = [
|
||||
["aardvark", "adroitness"],
|
||||
|
@ -259,7 +261,7 @@ _words = [
|
|||
["wayside", "Wilmington"],
|
||||
["willow", "Wyoming"],
|
||||
["woodlark", "yesteryear"],
|
||||
["Zulu", "Yucatán"]]
|
||||
["Zulu", "Yucatan"]]
|
||||
|
||||
hexre = re.compile("[a-fA-F0-9]+")
|
||||
|
||||
|
@ -275,41 +277,21 @@ def wordify(seq):
|
|||
|
||||
ret = []
|
||||
for i in range(0, len(seq), 2):
|
||||
ret.append(_words[int(seq[i:i+2], 16)][(i//2)%2])
|
||||
ret.append(_words[int(seq[i:i+2], 16)][(i//2)%2].lower())
|
||||
return ret
|
||||
|
||||
def usage():
|
||||
print("Usage:")
|
||||
print(" {0} [fingerprint...]".format(os.path.basename(sys.argv[0])))
|
||||
print("")
|
||||
print("If called with multiple arguments, they will be concatenated")
|
||||
print("and treated as a single fingerprint.")
|
||||
print("")
|
||||
print("If called with no arguments, input is read from stdin,")
|
||||
print("and each line is treated as a single fingerprint. In this")
|
||||
print("mode, invalid values are silently ignored.")
|
||||
exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
if 1 == len(sys.argv):
|
||||
fps = sys.stdin.readlines()
|
||||
else:
|
||||
fps = [" ".join(sys.argv[1:])]
|
||||
for fp in fps:
|
||||
try:
|
||||
words = wordify(fp)
|
||||
print("\n{0}: ".format(fp.strip()))
|
||||
sys.stdout.write("\t")
|
||||
for i in range(0, len(words)):
|
||||
sys.stdout.write(words[i] + " ")
|
||||
if (not (i+1) % 4) and not i == len(words)-1:
|
||||
sys.stdout.write("\n\t")
|
||||
print("")
|
||||
|
||||
except Exception as e:
|
||||
if len(fps) == 1:
|
||||
print (e)
|
||||
usage()
|
||||
|
||||
print("")
|
||||
|
||||
def hexify(seq, delim=' '):
|
||||
ret = b''
|
||||
sentence = seq
|
||||
try:
|
||||
sentence = seq.split(delim)
|
||||
except AttributeError:
|
||||
pass
|
||||
count = 0
|
||||
for word in sentence:
|
||||
count = 0
|
||||
for wordPair in _words:
|
||||
if word in wordPair:
|
||||
ret += bytes([(count)])
|
||||
count += 1
|
||||
return binascii.hexlify(ret)
|
0
onionr/keymanager.py
Normal file → Executable file
0
onionr/keymanager.py
Normal file → Executable file
7
onionr/logger.py
Normal file → Executable file
7
onionr/logger.py
Normal file → Executable file
|
@ -18,7 +18,7 @@
|
|||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import re, sys, time, traceback
|
||||
import re, sys, time, traceback, os
|
||||
|
||||
class colors:
|
||||
'''
|
||||
|
@ -64,7 +64,10 @@ class colors:
|
|||
'''
|
||||
Use the bitwise operators to merge these settings
|
||||
'''
|
||||
USE_ANSI = 0b100
|
||||
if os.name == 'nt':
|
||||
USE_ANSI = 0b000
|
||||
else:
|
||||
USE_ANSI = 0b100
|
||||
OUTPUT_TO_CONSOLE = 0b010
|
||||
OUTPUT_TO_FILE = 0b001
|
||||
|
||||
|
|
0
onionr/netcontroller.py
Normal file → Executable file
0
onionr/netcontroller.py
Normal file → Executable file
|
@ -25,7 +25,7 @@ MIN_PY_VERSION = 6
|
|||
if sys.version_info[0] == 2 or sys.version_info[1] < MIN_PY_VERSION:
|
||||
print('Error, Onionr requires Python 3.%s+' % (MIN_PY_VERSION,))
|
||||
sys.exit(1)
|
||||
import os, base64, random, getpass, shutil, subprocess, requests, time, platform, datetime, re, json, getpass, sqlite3
|
||||
import os, base64, random, getpass, shutil, time, platform, datetime, re, json, getpass, sqlite3
|
||||
import webbrowser, uuid, signal
|
||||
from threading import Thread
|
||||
import api, core, config, logger, onionrplugins as plugins, onionrevents as events
|
||||
|
@ -33,17 +33,18 @@ import onionrutils
|
|||
import netcontroller, onionrstorage
|
||||
from netcontroller import NetController
|
||||
from onionrblockapi import Block
|
||||
import onionrproofs, onionrexceptions, onionrusers, communicator
|
||||
import onionrproofs, onionrexceptions, communicator
|
||||
from onionrusers import onionrusers
|
||||
|
||||
try:
|
||||
from urllib3.contrib.socks import SOCKSProxyManager
|
||||
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_TAGLINE = 'Anonymous P2P Platform - GPLv3 - https://Onionr.net'
|
||||
ONIONR_VERSION = '0.5.0' # 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.
|
||||
API_VERSION = '5' # increments of 1; only change when something fundamental about how the API works changes. This way other nodes know how to communicate without learning too much information about you.
|
||||
|
||||
class Onionr:
|
||||
def __init__(self):
|
||||
|
@ -110,12 +111,6 @@ class Onionr:
|
|||
except:
|
||||
plugins.disable(name, onionr = self, stop_event = False)
|
||||
|
||||
if not os.path.exists(self.onionrCore.peerDB):
|
||||
self.onionrCore.createPeerDB()
|
||||
pass
|
||||
if not os.path.exists(self.onionrCore.addressDB):
|
||||
self.onionrCore.createAddressDB()
|
||||
|
||||
# Get configuration
|
||||
if type(config.get('client.webpassword')) is type(None):
|
||||
config.set('client.webpassword', base64.b16encode(os.urandom(32)).decode('utf-8'), savefile=True)
|
||||
|
@ -124,6 +119,7 @@ class Onionr:
|
|||
config.set('client.client.port', randomPort, savefile=True)
|
||||
if type(config.get('client.public.port')) is type(None):
|
||||
randomPort = netcontroller.getOpenPort()
|
||||
print(randomPort)
|
||||
config.set('client.public.port', randomPort, savefile=True)
|
||||
if type(config.get('client.participate')) is type(None):
|
||||
config.set('client.participate', True, savefile=True)
|
||||
|
@ -206,9 +202,6 @@ class Onionr:
|
|||
'connect': self.addAddress,
|
||||
'pex': self.doPEX,
|
||||
|
||||
'ui' : self.openUI,
|
||||
'gui' : self.openUI,
|
||||
|
||||
'getpassword': self.printWebPassword,
|
||||
'get-password': self.printWebPassword,
|
||||
'getpwd': self.printWebPassword,
|
||||
|
@ -297,8 +290,6 @@ class Onionr:
|
|||
with open('%s/%s.dat' % (exportDir, bHash), 'wb') as exportFile:
|
||||
exportFile.write(data)
|
||||
|
||||
|
||||
|
||||
def showDetails(self):
|
||||
details = {
|
||||
'Node Address' : self.get_hostname(),
|
||||
|
@ -562,24 +553,12 @@ class Onionr:
|
|||
if self.onionrUtils.hasKey(newPeer):
|
||||
logger.info('We already have that key')
|
||||
return
|
||||
if not '-' in newPeer:
|
||||
logger.info('Since no POW token was supplied for that key, one is being generated')
|
||||
proof = onionrproofs.DataPOW(newPeer)
|
||||
while True:
|
||||
result = proof.getResult()
|
||||
if result == False:
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
break
|
||||
newPeer += '-' + base64.b64encode(result[1]).decode()
|
||||
logger.info(newPeer)
|
||||
|
||||
logger.info("Adding peer: " + logger.colors.underline + newPeer)
|
||||
if self.onionrUtils.mergeKeys(newPeer):
|
||||
logger.info('Successfully added key')
|
||||
else:
|
||||
try:
|
||||
if self.onionrCore.addPeer(newPeer):
|
||||
logger.info('Successfully added key')
|
||||
except AssertionError:
|
||||
logger.error('Failed to add key')
|
||||
|
||||
return
|
||||
|
||||
def addAddress(self):
|
||||
|
@ -598,7 +577,6 @@ class Onionr:
|
|||
logger.info("Successfully added address.")
|
||||
else:
|
||||
logger.warn("Unable to add address.")
|
||||
|
||||
return
|
||||
|
||||
def addMessage(self, header="txt"):
|
||||
|
@ -774,7 +752,7 @@ class Onionr:
|
|||
Onionr.setupConfig('data/', self = self)
|
||||
|
||||
if self._developmentMode:
|
||||
logger.warn('DEVELOPMENT MODE ENABLED (LESS SECURE)', timestamp = False)
|
||||
logger.warn('DEVELOPMENT MODE ENABLED (NOT RECOMMENDED)', timestamp = False)
|
||||
net = NetController(config.get('client.public.port', 59497), apiServerIP=apiHost)
|
||||
logger.debug('Tor is starting...')
|
||||
if not net.startTor():
|
||||
|
@ -985,7 +963,7 @@ class Onionr:
|
|||
'''
|
||||
self.addFile(singleBlock=True, blockType='html')
|
||||
|
||||
def addFile(self, singleBlock=False, blockType='txt'):
|
||||
def addFile(self, singleBlock=False, blockType='bin'):
|
||||
'''
|
||||
Adds a file to the onionr network
|
||||
'''
|
||||
|
@ -1001,7 +979,8 @@ class Onionr:
|
|||
try:
|
||||
with open(filename, 'rb') as singleFile:
|
||||
blockhash = self.onionrCore.insertBlock(base64.b64encode(singleFile.read()), header=blockType)
|
||||
logger.info('File %s saved in block %s' % (filename, blockhash))
|
||||
if len(blockhash) > 0:
|
||||
logger.info('File %s saved in block %s' % (filename, blockhash))
|
||||
except:
|
||||
logger.error('Failed to save file in block.', timestamp = False)
|
||||
else:
|
||||
|
@ -1030,7 +1009,6 @@ class Onionr:
|
|||
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:
|
||||
|
@ -1073,12 +1051,6 @@ class Onionr:
|
|||
|
||||
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())
|
||||
|
||||
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') and logger.get_level() <= logger.LEVEL_INFO:
|
||||
with open('static-data/header.txt', 'rb') as file:
|
||||
|
|
5
onionr/onionrblacklist.py
Normal file → Executable file
5
onionr/onionrblacklist.py
Normal file → Executable file
|
@ -116,4 +116,7 @@ class OnionrBlackList:
|
|||
return
|
||||
insert = (hashed,)
|
||||
blacklistDate = self._core._utils.getEpoch()
|
||||
self._dbExecute("INSERT INTO blacklist (hash, dataType, blacklistDate, expire) VALUES(?, ?, ?, ?);", (str(hashed), dataType, blacklistDate, expire))
|
||||
try:
|
||||
self._dbExecute("INSERT INTO blacklist (hash, dataType, blacklistDate, expire) VALUES(?, ?, ?, ?);", (str(hashed), dataType, blacklistDate, expire))
|
||||
except sqlite3.IntegrityError:
|
||||
pass
|
||||
|
|
16
onionr/onionrblockapi.py
Normal file → Executable file
16
onionr/onionrblockapi.py
Normal file → Executable file
|
@ -18,8 +18,9 @@
|
|||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import core as onionrcore, logger, config, onionrexceptions, nacl.exceptions, onionrusers
|
||||
import core as onionrcore, logger, config, onionrexceptions, nacl.exceptions
|
||||
import json, os, sys, datetime, base64, onionrstorage
|
||||
from onionrusers import onionrusers
|
||||
|
||||
class Block:
|
||||
blockCacheOrder = list() # NEVER write your own code that writes to this!
|
||||
|
@ -59,7 +60,7 @@ class Block:
|
|||
|
||||
self.update()
|
||||
|
||||
def decrypt(self, anonymous = True, encodedData = True):
|
||||
def decrypt(self, encodedData = True):
|
||||
'''
|
||||
Decrypt a block, loading decrypted data into their vars
|
||||
'''
|
||||
|
@ -71,16 +72,16 @@ class Block:
|
|||
# decrypt data
|
||||
if self.getHeader('encryptType') == 'asym':
|
||||
try:
|
||||
self.bcontent = core._crypto.pubKeyDecrypt(self.bcontent, anonymous=anonymous, encodedData=encodedData)
|
||||
bmeta = core._crypto.pubKeyDecrypt(self.bmetadata, anonymous=anonymous, encodedData=encodedData)
|
||||
self.bcontent = core._crypto.pubKeyDecrypt(self.bcontent, encodedData=encodedData)
|
||||
bmeta = core._crypto.pubKeyDecrypt(self.bmetadata, encodedData=encodedData)
|
||||
try:
|
||||
bmeta = bmeta.decode()
|
||||
except AttributeError:
|
||||
# yet another bytes fix
|
||||
pass
|
||||
self.bmetadata = json.loads(bmeta)
|
||||
self.signature = core._crypto.pubKeyDecrypt(self.signature, anonymous=anonymous, encodedData=encodedData)
|
||||
self.signer = core._crypto.pubKeyDecrypt(self.signer, anonymous=anonymous, encodedData=encodedData)
|
||||
self.signature = core._crypto.pubKeyDecrypt(self.signature, encodedData=encodedData)
|
||||
self.signer = core._crypto.pubKeyDecrypt(self.signer, encodedData=encodedData)
|
||||
self.bheader['signer'] = self.signer.decode()
|
||||
self.signedData = json.dumps(self.bmetadata) + self.bcontent.decode()
|
||||
try:
|
||||
|
@ -94,7 +95,8 @@ class Block:
|
|||
logger.error(str(e))
|
||||
pass
|
||||
except nacl.exceptions.CryptoError:
|
||||
logger.debug('Could not decrypt block. Either invalid key or corrupted data')
|
||||
pass
|
||||
#logger.debug('Could not decrypt block. Either invalid key or corrupted data')
|
||||
else:
|
||||
retData = True
|
||||
self.decrypted = True
|
||||
|
|
66
onionr/onionrcrypto.py
Normal file → Executable file
66
onionr/onionrcrypto.py
Normal file → Executable file
|
@ -18,7 +18,7 @@
|
|||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
import nacl.signing, nacl.encoding, nacl.public, nacl.hash, nacl.pwhash, nacl.utils, nacl.secret, os, binascii, base64, hashlib, logger, onionrproofs, time, math, sys, hmac
|
||||
import onionrexceptions, keymanager
|
||||
import onionrexceptions, keymanager, core
|
||||
# secrets module was added into standard lib in 3.6+
|
||||
if sys.version_info[0] == 3 and sys.version_info[1] < 6:
|
||||
from dependencies import secrets
|
||||
|
@ -94,52 +94,41 @@ class OnionrCrypto:
|
|||
retData = key.sign(data).signature
|
||||
return retData
|
||||
|
||||
def pubKeyEncrypt(self, data, pubkey, anonymous=True, encodedData=False):
|
||||
def pubKeyEncrypt(self, data, pubkey, encodedData=False):
|
||||
'''Encrypt to a public key (Curve25519, taken from base32 Ed25519 pubkey)'''
|
||||
retVal = ''
|
||||
try:
|
||||
pubkey = pubkey.encode()
|
||||
except AttributeError:
|
||||
pass
|
||||
box = None
|
||||
data = self._core._utils.strToBytes(data)
|
||||
|
||||
pubkey = nacl.signing.VerifyKey(pubkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_public_key()
|
||||
|
||||
if encodedData:
|
||||
encoding = nacl.encoding.Base64Encoder
|
||||
else:
|
||||
encoding = nacl.encoding.RawEncoder
|
||||
|
||||
box = nacl.public.SealedBox(pubkey)
|
||||
retVal = box.encrypt(data, encoder=encoding)
|
||||
|
||||
if self.privKey != None and not anonymous:
|
||||
ownKey = nacl.signing.SigningKey(seed=self.privKey, encoder=nacl.encoding.Base32Encoder).to_curve25519_private_key()
|
||||
key = nacl.signing.VerifyKey(key=pubkey, encoder=nacl.encoding.Base32Encoder).to_curve25519_public_key()
|
||||
ourBox = nacl.public.Box(ownKey, key)
|
||||
retVal = ourBox.encrypt(data.encode(), encoder=encoding)
|
||||
elif anonymous:
|
||||
key = nacl.signing.VerifyKey(key=pubkey, encoder=nacl.encoding.Base32Encoder).to_curve25519_public_key()
|
||||
anonBox = nacl.public.SealedBox(key)
|
||||
try:
|
||||
data = data.encode()
|
||||
except AttributeError:
|
||||
pass
|
||||
retVal = anonBox.encrypt(data, encoder=encoding)
|
||||
return retVal
|
||||
|
||||
def pubKeyDecrypt(self, data, pubkey='', privkey='', anonymous=False, encodedData=False):
|
||||
def pubKeyDecrypt(self, data, pubkey='', privkey='', encodedData=False):
|
||||
'''pubkey decrypt (Curve25519, taken from Ed25519 pubkey)'''
|
||||
decrypted = False
|
||||
if encodedData:
|
||||
encoding = nacl.encoding.Base64Encoder
|
||||
else:
|
||||
encoding = nacl.encoding.RawEncoder
|
||||
ownKey = nacl.signing.SigningKey(seed=self.privKey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key()
|
||||
if self.privKey != None and not anonymous:
|
||||
ourBox = nacl.public.Box(ownKey, pubkey)
|
||||
decrypted = ourBox.decrypt(data, encoder=encoding)
|
||||
elif anonymous:
|
||||
if self._core._utils.validatePubKey(privkey):
|
||||
privkey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key()
|
||||
anonBox = nacl.public.SealedBox(privkey)
|
||||
else:
|
||||
anonBox = nacl.public.SealedBox(ownKey)
|
||||
decrypted = anonBox.decrypt(data, encoder=encoding)
|
||||
if privkey == '':
|
||||
privkey = self.privKey
|
||||
ownKey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key()
|
||||
|
||||
if self._core._utils.validatePubKey(privkey):
|
||||
privkey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key()
|
||||
anonBox = nacl.public.SealedBox(privkey)
|
||||
else:
|
||||
anonBox = nacl.public.SealedBox(ownKey)
|
||||
decrypted = anonBox.decrypt(data, encoder=encoding)
|
||||
return decrypted
|
||||
|
||||
def symmetricEncrypt(self, data, key, encodedKey=False, returnEncoded=True):
|
||||
|
@ -180,12 +169,6 @@ class OnionrCrypto:
|
|||
decrypted = base64.b64encode(decrypted)
|
||||
return decrypted
|
||||
|
||||
def generateSymmetricPeer(self, peer):
|
||||
'''Generate symmetric key for a peer and save it to the peer database'''
|
||||
key = self.generateSymmetric()
|
||||
self._core.setPeerInfo(peer, 'forwardKey', key)
|
||||
return
|
||||
|
||||
def generateSymmetric(self):
|
||||
'''Generate a symmetric key (bytes) and return it'''
|
||||
return binascii.hexlify(nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE))
|
||||
|
@ -283,6 +266,15 @@ class OnionrCrypto:
|
|||
|
||||
@staticmethod
|
||||
def safeCompare(one, two):
|
||||
# Do encode here to avoid spawning core
|
||||
try:
|
||||
one = one.encode()
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
two = two.encode()
|
||||
except AttributeError:
|
||||
pass
|
||||
return hmac.compare_digest(one, two)
|
||||
|
||||
@staticmethod
|
||||
|
|
77
onionr/onionrdaemontools.py
Normal file → Executable file
77
onionr/onionrdaemontools.py
Normal file → Executable file
|
@ -18,9 +18,11 @@
|
|||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import onionrexceptions, onionrpeers, onionrproofs, logger, onionrusers
|
||||
import onionrexceptions, onionrpeers, onionrproofs, logger
|
||||
import base64, sqlite3, os
|
||||
from dependencies import secrets
|
||||
from utils import netutils
|
||||
from onionrusers import onionrusers
|
||||
|
||||
class DaemonTools:
|
||||
'''
|
||||
|
@ -43,44 +45,50 @@ class DaemonTools:
|
|||
else:
|
||||
peer = self.daemon.pickOnlinePeer()
|
||||
|
||||
ourID = self.daemon._core.hsAddress.strip()
|
||||
|
||||
url = 'http://' + peer + '/announce'
|
||||
data = {'node': ourID}
|
||||
|
||||
combinedNodes = ourID + peer
|
||||
existingRand = self.daemon._core.getAddressInfo(peer, 'powValue')
|
||||
if type(existingRand) is type(None):
|
||||
existingRand = ''
|
||||
|
||||
if peer in self.announceCache:
|
||||
data['random'] = self.announceCache[peer]
|
||||
elif len(existingRand) > 0:
|
||||
data['random'] = existingRand
|
||||
else:
|
||||
proof = onionrproofs.DataPOW(combinedNodes, forceDifficulty=4)
|
||||
try:
|
||||
data['random'] = base64.b64encode(proof.waitForResult()[1])
|
||||
except TypeError:
|
||||
# Happens when we failed to produce a proof
|
||||
logger.error("Failed to produce a pow for announcing to " + peer)
|
||||
announceFail = True
|
||||
for x in range(1):
|
||||
if x == 1 and self.daemon._core.config.get('i2p.host'):
|
||||
ourID = self.daemon._core.config.get('i2p.own_addr').strip()
|
||||
else:
|
||||
self.announceCache[peer] = data['random']
|
||||
if not announceFail:
|
||||
logger.info('Announcing node to ' + url)
|
||||
if self.daemon._core._utils.doPostRequest(url, data) == 'Success':
|
||||
logger.info('Successfully introduced node to ' + peer)
|
||||
retData = True
|
||||
self.daemon._core.setAddressInfo(peer, 'introduced', 1)
|
||||
self.daemon._core.setAddressInfo(peer, 'powValue', data['random'])
|
||||
ourID = self.daemon._core.hsAddress.strip()
|
||||
|
||||
url = 'http://' + peer + '/announce'
|
||||
data = {'node': ourID}
|
||||
|
||||
combinedNodes = ourID + peer
|
||||
if ourID != 1:
|
||||
#TODO: Extend existingRand for i2p
|
||||
existingRand = self.daemon._core.getAddressInfo(peer, 'powValue')
|
||||
if type(existingRand) is type(None):
|
||||
existingRand = ''
|
||||
|
||||
if peer in self.announceCache:
|
||||
data['random'] = self.announceCache[peer]
|
||||
elif len(existingRand) > 0:
|
||||
data['random'] = existingRand
|
||||
else:
|
||||
proof = onionrproofs.DataPOW(combinedNodes, forceDifficulty=4)
|
||||
try:
|
||||
data['random'] = base64.b64encode(proof.waitForResult()[1])
|
||||
except TypeError:
|
||||
# Happens when we failed to produce a proof
|
||||
logger.error("Failed to produce a pow for announcing to " + peer)
|
||||
announceFail = True
|
||||
else:
|
||||
self.announceCache[peer] = data['random']
|
||||
if not announceFail:
|
||||
logger.info('Announcing node to ' + url)
|
||||
if self.daemon._core._utils.doPostRequest(url, data) == 'Success':
|
||||
logger.info('Successfully introduced node to ' + peer)
|
||||
retData = True
|
||||
self.daemon._core.setAddressInfo(peer, 'introduced', 1)
|
||||
self.daemon._core.setAddressInfo(peer, 'powValue', data['random'])
|
||||
self.daemon.decrementThreadCount('announceNode')
|
||||
return retData
|
||||
|
||||
def netCheck(self):
|
||||
'''Check if we are connected to the internet or not when we can't connect to any peers'''
|
||||
if len(self.daemon.onlinePeers) == 0:
|
||||
if not self.daemon._core._utils.checkNetwork(torPort=self.daemon.proxyPort):
|
||||
if not netutils.checkNetwork(self.daemon._core._utils, torPort=self.daemon.proxyPort):
|
||||
logger.warn('Network check failed, are you connected to the internet?')
|
||||
self.daemon.isOnline = False
|
||||
else:
|
||||
|
@ -186,10 +194,11 @@ class DaemonTools:
|
|||
|
||||
def insertDeniableBlock(self):
|
||||
'''Insert a fake block in order to make it more difficult to track real blocks'''
|
||||
fakePeer = self.daemon._core._crypto.generatePubKey()[0]
|
||||
fakePeer = ''
|
||||
chance = 10
|
||||
if secrets.randbelow(chance) == (chance - 1):
|
||||
fakePeer = self.daemon._core._crypto.generatePubKey()[0]
|
||||
data = secrets.token_hex(secrets.randbelow(500) + 1)
|
||||
self.daemon._core.insertBlock(data, header='pm', encryptType='asym', asymPeer=fakePeer)
|
||||
self.daemon._core.insertBlock(data, header='pm', encryptType='asym', asymPeer=fakePeer, meta={'subject': 'foo'})
|
||||
self.daemon.decrementThreadCount('insertDeniableBlock')
|
||||
return
|
3
onionr/onionrevents.py
Normal file → Executable file
3
onionr/onionrevents.py
Normal file → Executable file
|
@ -61,10 +61,9 @@ def call(plugin, event_name, data = None, pluginapi = None):
|
|||
try:
|
||||
attribute = 'on_' + str(event_name).lower()
|
||||
|
||||
# TODO: Use multithreading perhaps?
|
||||
if hasattr(plugin, attribute):
|
||||
#logger.debug('Calling event ' + str(event_name))
|
||||
getattr(plugin, attribute)(pluginapi)
|
||||
getattr(plugin, attribute)(pluginapi, data)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
|
|
9
onionr/onionrexceptions.py
Normal file → Executable file
9
onionr/onionrexceptions.py
Normal file → Executable file
|
@ -44,6 +44,10 @@ class PasswordStrengthError(Exception):
|
|||
pass
|
||||
|
||||
# block exceptions
|
||||
|
||||
class DifficultyTooLarge(Exception):
|
||||
pass
|
||||
|
||||
class InvalidMetadata(Exception):
|
||||
pass
|
||||
|
||||
|
@ -82,4 +86,9 @@ class DiskAllocationReached(Exception):
|
|||
# onionrsocket exceptions
|
||||
|
||||
class MissingAddress(Exception):
|
||||
pass
|
||||
|
||||
# Contact exceptions
|
||||
|
||||
class ContactDeleted(Exception):
|
||||
pass
|
0
onionr/onionrpeers.py
Normal file → Executable file
0
onionr/onionrpeers.py
Normal file → Executable file
0
onionr/onionrpluginapi.py
Normal file → Executable file
0
onionr/onionrpluginapi.py
Normal file → Executable file
0
onionr/onionrplugins.py
Normal file → Executable file
0
onionr/onionrplugins.py
Normal file → Executable file
22
onionr/onionrproofs.py
Normal file → Executable file
22
onionr/onionrproofs.py
Normal file → Executable file
|
@ -41,9 +41,9 @@ def getDifficultyModifier(coreOrUtilsInst=None):
|
|||
if percentUse >= 0.50:
|
||||
retData += 1
|
||||
elif percentUse >= 0.75:
|
||||
retData += 2
|
||||
retData += 2
|
||||
elif percentUse >= 0.95:
|
||||
retData += 3
|
||||
retData += 3
|
||||
|
||||
return retData
|
||||
|
||||
|
@ -64,11 +64,12 @@ def getDifficultyForNewBlock(data, ourBlock=True):
|
|||
else:
|
||||
raise ValueError('not Block, str, or int')
|
||||
if ourBlock:
|
||||
minDifficulty = config.get('general.minimum_send_pow')
|
||||
minDifficulty = config.get('general.minimum_send_pow', 4)
|
||||
else:
|
||||
minDifficulty = config.get('general.minimum_block_pow')
|
||||
minDifficulty = config.get('general.minimum_block_pow', 4)
|
||||
|
||||
retData = max(minDifficulty, math.floor(dataSize / 100000)) + getDifficultyModifier()
|
||||
|
||||
retData = max(minDifficulty, math.floor(dataSize / 1000000)) + getDifficultyModifier()
|
||||
return retData
|
||||
|
||||
def getHashDifficulty(h):
|
||||
|
@ -192,7 +193,7 @@ class DataPOW:
|
|||
if not self.hashing:
|
||||
break
|
||||
else:
|
||||
time.sleep(2)
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
self.shutdown()
|
||||
logger.warn('Got keyboard interrupt while waiting for POW result, stopping')
|
||||
|
@ -243,13 +244,11 @@ class POW:
|
|||
self.reporting = reporting
|
||||
iFound = False # if current thread is the one that found the answer
|
||||
answer = ''
|
||||
heartbeat = 200000
|
||||
hbCount = 0
|
||||
|
||||
nonce = int(binascii.hexlify(nacl.utils.random(2)), 16)
|
||||
while self.hashing:
|
||||
rand = nacl.utils.random()
|
||||
#token = nacl.hash.blake2b(rand + self.data).decode()
|
||||
self.metadata['powRandomToken'] = base64.b64encode(rand).decode()
|
||||
self.metadata['powRandomToken'] = nonce
|
||||
payload = json.dumps(self.metadata).encode() + b'\n' + self.data
|
||||
token = myCore._crypto.sha3Hash(payload)
|
||||
try:
|
||||
|
@ -262,6 +261,7 @@ class POW:
|
|||
iFound = True
|
||||
self.result = payload
|
||||
break
|
||||
nonce += 1
|
||||
|
||||
if iFound:
|
||||
endTime = math.floor(time.time())
|
||||
|
@ -299,7 +299,7 @@ class POW:
|
|||
if not self.hashing:
|
||||
break
|
||||
else:
|
||||
time.sleep(2)
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
self.shutdown()
|
||||
logger.warn('Got keyboard interrupt while waiting for POW result, stopping')
|
||||
|
|
0
onionr/onionrsockets.py
Normal file → Executable file
0
onionr/onionrsockets.py
Normal file → Executable file
|
@ -55,6 +55,21 @@ def _dbFetch(coreInst, blockHash):
|
|||
conn.close()
|
||||
return None
|
||||
|
||||
def deleteBlock(coreInst, blockHash):
|
||||
# You should call core.removeBlock if you automatically want to remove storage byte count
|
||||
assert isinstance(coreInst, core.Core)
|
||||
if os.path.exists('%s/%s.dat' % (coreInst.blockDataLocation, blockHash)):
|
||||
os.remove('%s/%s.dat' % (coreInst.blockDataLocation, blockHash))
|
||||
return True
|
||||
dbCreate(coreInst)
|
||||
conn = sqlite3.connect(coreInst.blockDataDB, timeout=10)
|
||||
c = conn.cursor()
|
||||
data = (blockHash,)
|
||||
c.execute('DELETE FROM blockData where hash = ?', data)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return True
|
||||
|
||||
def store(coreInst, data, blockHash=''):
|
||||
assert isinstance(coreInst, core.Core)
|
||||
assert coreInst._utils.validateHash(blockHash)
|
||||
|
|
72
onionr/onionrusers/contactmanager.py
Normal file
72
onionr/onionrusers/contactmanager.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
'''
|
||||
Onionr - P2P Anonymous Storage Network
|
||||
|
||||
Sets more abstract information related to a peer. Can be thought of as traditional 'contact' system
|
||||
'''
|
||||
'''
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
import os, json, onionrexceptions
|
||||
from onionrusers import onionrusers
|
||||
|
||||
class ContactManager(onionrusers.OnionrUser):
|
||||
def __init__(self, coreInst, publicKey, saveUser=False, recordExpireSeconds=5):
|
||||
super(ContactManager, self).__init__(coreInst, publicKey, saveUser=saveUser)
|
||||
self.dataDir = coreInst.dataDir + '/contacts/'
|
||||
self.dataFile = '%s/contacts/%s.json' % (coreInst.dataDir, publicKey)
|
||||
self.lastRead = 0
|
||||
self.recordExpire = recordExpireSeconds
|
||||
self.data = self._loadData()
|
||||
self.deleted = False
|
||||
|
||||
if not os.path.exists(self.dataDir):
|
||||
os.mkdir(self.dataDir)
|
||||
|
||||
def _writeData(self):
|
||||
data = json.dumps(self.data)
|
||||
with open(self.dataFile, 'w') as dataFile:
|
||||
dataFile.write(data)
|
||||
|
||||
def _loadData(self):
|
||||
self.lastRead = self._core._utils.getEpoch()
|
||||
retData = {}
|
||||
if os.path.exists(self.dataFile):
|
||||
with open(self.dataFile, 'r') as dataFile:
|
||||
retData = json.loads(dataFile.read())
|
||||
return retData
|
||||
|
||||
def set_info(self, key, value, autoWrite=True):
|
||||
if self.deleted:
|
||||
raise onionrexceptions.ContactDeleted
|
||||
|
||||
self.data[key] = value
|
||||
if autoWrite:
|
||||
self._writeData()
|
||||
return
|
||||
|
||||
def get_info(self, key, forceReload=False):
|
||||
if self.deleted:
|
||||
raise onionrexceptions.ContactDeleted
|
||||
|
||||
if (self._core._utils.getEpoch() - self.lastRead >= self.recordExpire) or forceReload:
|
||||
self.data = self._loadData()
|
||||
try:
|
||||
return self.data[key]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def delete_contact(self):
|
||||
self.deleted = True
|
||||
if os.path.exists(self.dataFile):
|
||||
os.remove(self.dataFile)
|
14
onionr/onionrusers.py → onionr/onionrusers/onionrusers.py
Normal file → Executable file
14
onionr/onionrusers.py → onionr/onionrusers/onionrusers.py
Normal file → Executable file
|
@ -40,12 +40,18 @@ class OnionrUser:
|
|||
Takes an instance of onionr core, a base32 encoded ed25519 public key, and a bool saveUser
|
||||
saveUser determines if we should add a user to our peer database or not.
|
||||
'''
|
||||
if ' ' in coreInst._utils.bytesToStr(publicKey).strip():
|
||||
publicKey = coreInst._utils.convertHumanReadableID(publicKey)
|
||||
|
||||
self.trust = 0
|
||||
self._core = coreInst
|
||||
self.publicKey = publicKey
|
||||
|
||||
if saveUser:
|
||||
self._core.addPeer(publicKey)
|
||||
try:
|
||||
self._core.addPeer(publicKey)
|
||||
except AssertionError:
|
||||
pass
|
||||
|
||||
self.trust = self._core.getPeerInfo(self.publicKey, 'trust')
|
||||
return
|
||||
|
@ -73,7 +79,7 @@ class OnionrUser:
|
|||
encrypted = coreInst._crypto.pubKeyEncrypt(data, self.publicKey, encodedData=True)
|
||||
return encrypted
|
||||
|
||||
def decrypt(self, data, anonymous=True):
|
||||
def decrypt(self, data):
|
||||
decrypted = coreInst._crypto.pubKeyDecrypt(data, self.publicKey, encodedData=True)
|
||||
return decrypted
|
||||
|
||||
|
@ -81,7 +87,7 @@ class OnionrUser:
|
|||
retData = ''
|
||||
forwardKey = self._getLatestForwardKey()
|
||||
if self._core._utils.validatePubKey(forwardKey):
|
||||
retData = self._core._crypto.pubKeyEncrypt(data, forwardKey, encodedData=True, anonymous=True)
|
||||
retData = self._core._crypto.pubKeyEncrypt(data, forwardKey, encodedData=True)
|
||||
else:
|
||||
raise onionrexceptions.InvalidPubkey("No valid forward secrecy key available for this user")
|
||||
#self.generateForwardKey()
|
||||
|
@ -91,7 +97,7 @@ class OnionrUser:
|
|||
retData = ""
|
||||
for key in self.getGeneratedForwardKeys(False):
|
||||
try:
|
||||
retData = self._core._crypto.pubKeyDecrypt(encrypted, privkey=key[1], anonymous=True, encodedData=True)
|
||||
retData = self._core._crypto.pubKeyDecrypt(encrypted, privkey=key[1], encodedData=True)
|
||||
except nacl.exceptions.CryptoError:
|
||||
retData = False
|
||||
else:
|
250
onionr/onionrutils.py
Normal file → Executable file
250
onionr/onionrutils.py
Normal file → Executable file
|
@ -18,14 +18,15 @@
|
|||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
# Misc functions that do not fit in the main api, but are useful
|
||||
import getpass, sys, requests, os, socket, hashlib, logger, sqlite3, config, binascii, time, base64, json, glob, shutil, math, json, re, urllib.parse
|
||||
import getpass, sys, requests, os, socket, hashlib, logger, sqlite3, config, binascii, time, base64, json, glob, shutil, math, json, re, urllib.parse, string
|
||||
import nacl.signing, nacl.encoding
|
||||
from onionrblockapi import Block
|
||||
import onionrexceptions
|
||||
from onionr import API_VERSION
|
||||
import onionrevents
|
||||
import onionrusers, storagecounter
|
||||
from etc import pgpwords
|
||||
import storagecounter
|
||||
from etc import pgpwords
|
||||
from onionrusers import onionrusers
|
||||
if sys.version_info < (3, 6):
|
||||
try:
|
||||
import sha3
|
||||
|
@ -67,90 +68,6 @@ class OnionrUtils:
|
|||
'''
|
||||
epoch = self.getEpoch()
|
||||
return epoch - (epoch % roundS)
|
||||
|
||||
def mergeKeys(self, newKeyList):
|
||||
'''
|
||||
Merge ed25519 key list to our database, comma seperated string
|
||||
'''
|
||||
try:
|
||||
retVal = False
|
||||
if newKeyList != False:
|
||||
for key in newKeyList.split(','):
|
||||
key = key.split('-')
|
||||
# Test if key is valid
|
||||
try:
|
||||
if len(key[0]) > 60 or len(key[1]) > 1000:
|
||||
logger.warn('%s or its pow value is too large.' % key[0])
|
||||
continue
|
||||
except IndexError:
|
||||
logger.warn('No pow token')
|
||||
continue
|
||||
try:
|
||||
value = base64.b64decode(key[1])
|
||||
except binascii.Error:
|
||||
continue
|
||||
# Load the pow token
|
||||
hashedKey = self._core._crypto.blake2bHash(key[0])
|
||||
powHash = self._core._crypto.blake2bHash(value + hashedKey)
|
||||
try:
|
||||
powHash = powHash.encode()
|
||||
except AttributeError:
|
||||
pass
|
||||
# if POW meets required difficulty, TODO make configurable/dynamic
|
||||
if powHash.startswith(b'0000'):
|
||||
# if we don't already have the key and its not our key, add it.
|
||||
if not key[0] in self._core.listPeers(randomOrder=False) and type(key) != None and key[0] != self._core._crypto.pubKey:
|
||||
if self._core.addPeer(key[0], key[1]):
|
||||
# Check if the peer has a set username already
|
||||
onionrusers.OnionrUser(self._core, key[0]).findAndSetID()
|
||||
retVal = True
|
||||
else:
|
||||
logger.warn("Failed to add key")
|
||||
else:
|
||||
pass
|
||||
#logger.debug('%s pow failed' % key[0])
|
||||
return retVal
|
||||
except Exception as error:
|
||||
logger.error('Failed to merge keys.', error=error)
|
||||
return False
|
||||
|
||||
|
||||
def mergeAdders(self, newAdderList):
|
||||
'''
|
||||
Merge peer adders list to our database
|
||||
'''
|
||||
try:
|
||||
retVal = False
|
||||
if newAdderList != False:
|
||||
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:
|
||||
continue
|
||||
if self._core.addAddress(adder):
|
||||
# Check if we have the maxmium amount of allowed stored peers
|
||||
if config.get('peers.max_stored_peers') > len(self._core.listAdders()):
|
||||
logger.info('Added %s to db.' % adder, timestamp = True)
|
||||
retVal = True
|
||||
else:
|
||||
logger.warn('Reached the maximum amount of peers in the net database as allowed by your config.')
|
||||
else:
|
||||
pass
|
||||
#logger.debug('%s is either our address or already in our DB' % adder)
|
||||
return retVal
|
||||
except Exception as error:
|
||||
logger.error('Failed to merge adders.', error = error)
|
||||
return False
|
||||
|
||||
def getMyAddress(self):
|
||||
try:
|
||||
with open('./' + self._core.dataDir + 'hs/hostname', 'r') as hostname:
|
||||
return hostname.read().strip()
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
except Exception as error:
|
||||
logger.error('Failed to read my address.', error = error)
|
||||
return None
|
||||
|
||||
def getClientAPIServer(self):
|
||||
retData = ''
|
||||
|
@ -163,7 +80,7 @@ class OnionrUtils:
|
|||
retData += '%s:%s' % (hostname, config.get('client.client.port'))
|
||||
return retData
|
||||
|
||||
def localCommand(self, command, data='', silent = True, post=False, postData = {}, maxWait=10):
|
||||
def localCommand(self, command, data='', silent = True, post=False, postData = {}, maxWait=20):
|
||||
'''
|
||||
Send a command to the local http API server, securely. Intended for local clients, DO NOT USE for remote peers.
|
||||
'''
|
||||
|
@ -185,9 +102,9 @@ class OnionrUtils:
|
|||
payload = 'http://%s/%s%s' % (hostname, command, data)
|
||||
try:
|
||||
if post:
|
||||
retData = requests.post(payload, data=postData, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, 30)).text
|
||||
retData = requests.post(payload, data=postData, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, maxWait)).text
|
||||
else:
|
||||
retData = requests.get(payload, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, 30)).text
|
||||
retData = requests.get(payload, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, maxWait)).text
|
||||
except Exception as error:
|
||||
if not silent:
|
||||
logger.error('Failed to make local request (command: %s):%s' % (command, error))
|
||||
|
@ -195,38 +112,23 @@ class OnionrUtils:
|
|||
|
||||
return retData
|
||||
|
||||
def getPassword(self, message='Enter password: ', confirm = True):
|
||||
'''
|
||||
Get a password without showing the users typing and confirm the input
|
||||
'''
|
||||
# Get a password safely with confirmation and return it
|
||||
while True:
|
||||
print(message)
|
||||
pass1 = getpass.getpass()
|
||||
if confirm:
|
||||
print('Confirm password: ')
|
||||
pass2 = getpass.getpass()
|
||||
if pass1 != pass2:
|
||||
logger.error("Passwords do not match.")
|
||||
logger.readline()
|
||||
else:
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
return pass1
|
||||
|
||||
def getHumanReadableID(self, pub=''):
|
||||
'''gets a human readable ID from a public key'''
|
||||
if pub == '':
|
||||
pub = self._core._crypto.pubKey
|
||||
pub = base64.b16encode(base64.b32decode(pub)).decode()
|
||||
return '-'.join(pgpwords.wordify(pub))
|
||||
return ' '.join(pgpwords.wordify(pub))
|
||||
|
||||
def convertHumanReadableID(self, pub):
|
||||
'''Convert a human readable pubkey id to base32'''
|
||||
pub = pub.lower()
|
||||
return self.bytesToStr(base64.b32encode(binascii.unhexlify(pgpwords.hexify(pub.strip()))))
|
||||
|
||||
def getBlockMetadataFromData(self, blockData):
|
||||
'''
|
||||
accepts block contents as string, returns a tuple of metadata, meta (meta being internal metadata, which will be returned as an encrypted base64 string if it is encrypted, dict if not).
|
||||
|
||||
accepts block contents as string, returns a tuple of
|
||||
metadata, meta (meta being internal metadata, which will be
|
||||
returned as an encrypted base64 string if it is encrypted, dict if not).
|
||||
'''
|
||||
meta = {}
|
||||
metadata = {}
|
||||
|
@ -251,34 +153,6 @@ class OnionrUtils:
|
|||
meta = metadata['meta']
|
||||
return (metadata, meta, data)
|
||||
|
||||
def checkPort(self, port, host=''):
|
||||
'''
|
||||
Checks if a port is available, returns bool
|
||||
'''
|
||||
# inspired by https://www.reddit.com/r/learnpython/comments/2i4qrj/how_to_write_a_python_script_that_checks_to_see/ckzarux/
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
retVal = False
|
||||
try:
|
||||
sock.bind((host, port))
|
||||
except OSError as e:
|
||||
if e.errno is 98:
|
||||
retVal = True
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
return retVal
|
||||
|
||||
def checkIsIP(self, ip):
|
||||
'''
|
||||
Check if a string is a valid IPv4 address
|
||||
'''
|
||||
try:
|
||||
socket.inet_aton(ip)
|
||||
except:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def processBlockMetadata(self, blockHash):
|
||||
'''
|
||||
Read metadata from a block and cache it to the block database
|
||||
|
@ -309,7 +183,8 @@ class OnionrUtils:
|
|||
else:
|
||||
self._core.updateBlockInfo(blockHash, 'expire', expireTime)
|
||||
else:
|
||||
logger.debug('Not processing metadata on encrypted block we cannot decrypt.')
|
||||
pass
|
||||
#logger.debug('Not processing metadata on encrypted block we cannot decrypt.')
|
||||
|
||||
def escapeAnsi(self, line):
|
||||
'''
|
||||
|
@ -320,21 +195,6 @@ class OnionrUtils:
|
|||
ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]')
|
||||
return ansi_escape.sub('', line)
|
||||
|
||||
def getBlockDBHash(self):
|
||||
'''
|
||||
Return a sha3_256 hash of the blocks DB
|
||||
'''
|
||||
try:
|
||||
with open(self._core.blockDB, 'rb') as data:
|
||||
data = data.read()
|
||||
hasher = hashlib.sha3_256()
|
||||
hasher.update(data)
|
||||
dataHash = hasher.hexdigest()
|
||||
|
||||
return dataHash
|
||||
except Exception as error:
|
||||
logger.error('Failed to get block DB hash.', error=error)
|
||||
|
||||
def hasBlock(self, hash):
|
||||
'''
|
||||
Check for new block in the list
|
||||
|
@ -361,7 +221,7 @@ class OnionrUtils:
|
|||
|
||||
def validateHash(self, data, length=64):
|
||||
'''
|
||||
Validate if a string is a valid hex formatted hash
|
||||
Validate if a string is a valid hash hex digest (does not compare, just checks length and charset)
|
||||
'''
|
||||
retVal = True
|
||||
if data == False or data == True:
|
||||
|
@ -450,6 +310,8 @@ class OnionrUtils:
|
|||
Validate if a string is a valid base32 encoded Ed25519 key
|
||||
'''
|
||||
retVal = False
|
||||
if type(key) is type(None):
|
||||
return False
|
||||
try:
|
||||
nacl.signing.SigningKey(seed=key, encoder=nacl.encoding.Base32Encoder)
|
||||
except nacl.exceptions.ValueError:
|
||||
|
@ -460,15 +322,6 @@ class OnionrUtils:
|
|||
retVal = True
|
||||
return retVal
|
||||
|
||||
def isIntegerString(self, data):
|
||||
'''Check if a string is a valid base10 integer (also returns true if already an int)'''
|
||||
try:
|
||||
int(data)
|
||||
except ValueError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def validateID(self, id):
|
||||
'''
|
||||
Validate if an address is a valid tor or i2p hidden service
|
||||
|
@ -513,36 +366,22 @@ class OnionrUtils:
|
|||
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())
|
||||
except binascii.Error:
|
||||
retVal = False
|
||||
for x in idNoDomain.upper():
|
||||
if x not in string.ascii_uppercase and x not in '234567':
|
||||
retVal = False
|
||||
|
||||
return retVal
|
||||
except:
|
||||
return False
|
||||
|
||||
def getPeerByHashId(self, hash):
|
||||
'''
|
||||
Return the pubkey of the user if known from the hash
|
||||
'''
|
||||
if self._core._crypto.pubKeyHashID() == hash:
|
||||
retData = self._core._crypto.pubKey
|
||||
return retData
|
||||
conn = sqlite3.connect(self._core.peerDB)
|
||||
c = conn.cursor()
|
||||
command = (hash,)
|
||||
retData = ''
|
||||
for row in c.execute('SELECT id FROM peers WHERE hashID = ?', command):
|
||||
if row[0] != '':
|
||||
retData = row[0]
|
||||
return retData
|
||||
def isIntegerString(self, data):
|
||||
'''Check if a string is a valid base10 integer (also returns true if already an int)'''
|
||||
try:
|
||||
int(data)
|
||||
except (ValueError, TypeError) as e:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def isCommunicatorRunning(self, timeout = 5, interval = 0.1):
|
||||
try:
|
||||
|
@ -564,13 +403,6 @@ class OnionrUtils:
|
|||
except:
|
||||
return False
|
||||
|
||||
def token(self, size = 32):
|
||||
'''
|
||||
Generates a secure random hex encoded token
|
||||
'''
|
||||
|
||||
return binascii.hexlify(os.urandom(size))
|
||||
|
||||
def importNewBlocks(self, scanDir=''):
|
||||
'''
|
||||
This function is intended to scan for new blocks ON THE DISK and import them
|
||||
|
@ -692,22 +524,6 @@ class OnionrUtils:
|
|||
pass
|
||||
return data
|
||||
|
||||
def checkNetwork(self, torPort=0):
|
||||
'''Check if we are connected to the internet (through Tor)'''
|
||||
retData = False
|
||||
connectURLs = []
|
||||
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, ignoreAPI=True) != False:
|
||||
retData = True
|
||||
break
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return retData
|
||||
|
||||
def size(path='.'):
|
||||
'''
|
||||
Returns the size of a folder's contents in bytes
|
||||
|
@ -732,4 +548,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)
|
||||
return "%.1f %s%s" % (num, 'Yi', suffix)
|
2
onionr/static-data/bootstrap-nodes.txt
Normal file → Executable file
2
onionr/static-data/bootstrap-nodes.txt
Normal file → Executable file
|
@ -1 +1 @@
|
|||
dd3llxdp5q6ak3zmmicoy3jnodmroouv2xr7whkygiwp3rl7nf23gdad.onion
|
||||
dd3llxdp5q6ak3zmmicoy3jnodmroouv2xr7whkygiwp3rl7nf23gdad.onion
|
||||
|
|
2
onionr/static-data/connect-check.txt
Normal file → Executable file
2
onionr/static-data/connect-check.txt
Normal file → Executable file
|
@ -1 +1 @@
|
|||
https://3g2upl4pq6kufc4m.onion/robots.txt,http://expyuzz4wqqyqhjn.onion/robots.txt,https://onionr.voidnet.tech/
|
||||
https://3g2upl4pq6kufc4m.onion/robots.txt,http://expyuzz4wqqyqhjn.onion/robots.txt,http://archivecaslytosk.onion/robots.txt
|
||||
|
|
0
onionr/static-data/default-plugins/cliui/info.json
Normal file → Executable file
0
onionr/static-data/default-plugins/cliui/info.json
Normal file → Executable file
0
onionr/static-data/default-plugins/cliui/main.py
Normal file → Executable file
0
onionr/static-data/default-plugins/cliui/main.py
Normal file → Executable file
0
onionr/static-data/default-plugins/encrypt/info.json
Normal file → Executable file
0
onionr/static-data/default-plugins/encrypt/info.json
Normal file → Executable file
4
onionr/static-data/default-plugins/encrypt/main.py
Normal file → Executable file
4
onionr/static-data/default-plugins/encrypt/main.py
Normal file → Executable file
|
@ -69,7 +69,7 @@ class PlainEncryption:
|
|||
data['data'] = plaintext
|
||||
data = json.dumps(data)
|
||||
plaintext = data
|
||||
encrypted = self.api.get_core()._crypto.pubKeyEncrypt(plaintext, pubkey, anonymous=True, encodedData=True)
|
||||
encrypted = self.api.get_core()._crypto.pubKeyEncrypt(plaintext, pubkey, encodedData=True)
|
||||
encrypted = self.api.get_core()._utils.bytesToStr(encrypted)
|
||||
logger.info('Encrypted Message: \n\nONIONR ENCRYPTED DATA %s END ENCRYPTED DATA' % (encrypted,))
|
||||
|
||||
|
@ -88,7 +88,7 @@ class PlainEncryption:
|
|||
return
|
||||
encrypted = data.replace('ONIONR ENCRYPTED DATA ', '').replace('END ENCRYPTED DATA', '')
|
||||
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)
|
||||
decrypted = self.api.get_core()._crypto.pubKeyDecrypt(encrypted, privkey=self.api.get_core()._crypto.privKey, encodedData=True)
|
||||
if decrypted == False:
|
||||
logger.error("Decryption failed")
|
||||
else:
|
||||
|
|
0
onionr/static-data/default-plugins/flow/info.json
Normal file → Executable file
0
onionr/static-data/default-plugins/flow/info.json
Normal file → Executable file
0
onionr/static-data/default-plugins/flow/main.py
Normal file → Executable file
0
onionr/static-data/default-plugins/flow/main.py
Normal file → Executable file
0
onionr/static-data/default-plugins/metadataprocessor/info.json
Normal file → Executable file
0
onionr/static-data/default-plugins/metadataprocessor/info.json
Normal file → Executable file
2
onionr/static-data/default-plugins/metadataprocessor/main.py
Normal file → Executable file
2
onionr/static-data/default-plugins/metadataprocessor/main.py
Normal file → Executable file
|
@ -41,7 +41,7 @@ def _processForwardKey(api, myBlock):
|
|||
else:
|
||||
raise onionrexceptions.InvalidPubkey("%s is nota valid pubkey key" % (key,))
|
||||
|
||||
def on_processblocks(api):
|
||||
def on_processblocks(api, data=None):
|
||||
# Generally fired by utils.
|
||||
myBlock = api.data['block']
|
||||
blockType = api.data['type']
|
||||
|
|
0
onionr/static-data/default-plugins/pluginmanager/.gitignore
vendored
Normal file → Executable file
0
onionr/static-data/default-plugins/pluginmanager/.gitignore
vendored
Normal file → Executable file
0
onionr/static-data/default-plugins/pluginmanager/LICENSE
Normal file → Executable file
0
onionr/static-data/default-plugins/pluginmanager/LICENSE
Normal file → Executable file
0
onionr/static-data/default-plugins/pluginmanager/README.md
Normal file → Executable file
0
onionr/static-data/default-plugins/pluginmanager/README.md
Normal file → Executable file
0
onionr/static-data/default-plugins/pluginmanager/info.json
Normal file → Executable file
0
onionr/static-data/default-plugins/pluginmanager/info.json
Normal file → Executable file
0
onionr/static-data/default-plugins/pluginmanager/main.py
Normal file → Executable file
0
onionr/static-data/default-plugins/pluginmanager/main.py
Normal file → Executable file
0
onionr/static-data/default-plugins/pms/info.json
Normal file → Executable file
0
onionr/static-data/default-plugins/pms/info.json
Normal file → Executable file
103
onionr/static-data/default-plugins/pms/main.py
Normal file → Executable file
103
onionr/static-data/default-plugins/pms/main.py
Normal file → Executable file
|
@ -21,8 +21,9 @@
|
|||
# Imports some useful libraries
|
||||
import logger, config, threading, time, readline, datetime
|
||||
from onionrblockapi import Block
|
||||
import onionrexceptions, onionrusers
|
||||
import locale, sys, os
|
||||
import onionrexceptions
|
||||
from onionrusers import onionrusers
|
||||
import locale, sys, os, json
|
||||
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
|
||||
|
@ -48,14 +49,14 @@ class MailStrings:
|
|||
self.mailInstance = mailInstance
|
||||
|
||||
self.programTag = 'OnionrMail v%s' % (PLUGIN_VERSION)
|
||||
choices = ['view inbox', 'view sentbox', 'send message', 'quit']
|
||||
choices = ['view inbox', 'view sentbox', 'send message', 'toggle pseudonymity', 'quit']
|
||||
self.mainMenuChoices = choices
|
||||
self.mainMenu = '''\n
|
||||
-----------------
|
||||
1. %s
|
||||
2. %s
|
||||
3. %s
|
||||
4. %s''' % (choices[0], choices[1], choices[2], choices[3])
|
||||
self.mainMenu = '''-----------------
|
||||
1. %s
|
||||
2. %s
|
||||
3. %s
|
||||
4. %s
|
||||
5. %s''' % (choices[0], choices[1], choices[2], choices[3], choices[4])
|
||||
|
||||
class OnionrMail:
|
||||
def __init__(self, pluginapi):
|
||||
|
@ -65,6 +66,7 @@ class OnionrMail:
|
|||
self.sentboxTools = sentboxdb.SentBox(self.myCore)
|
||||
self.sentboxList = []
|
||||
self.sentMessages = {}
|
||||
self.doSigs = True
|
||||
return
|
||||
|
||||
def inbox(self):
|
||||
|
@ -133,18 +135,30 @@ class OnionrMail:
|
|||
else:
|
||||
cancel = ''
|
||||
readBlock.verifySig()
|
||||
|
||||
logger.info('Message recieved from %s' % (self.myCore._utils.bytesToStr(readBlock.signer,)))
|
||||
senderDisplay = self.myCore._utils.bytesToStr(readBlock.signer)
|
||||
if len(senderDisplay.strip()) == 0:
|
||||
senderDisplay = 'Anonymous'
|
||||
logger.info('Message received from %s' % (senderDisplay,))
|
||||
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.')
|
||||
logger.warn('This message has an INVALID/NO signature. ANYONE could have sent this message.')
|
||||
cancel = logger.readline('Press enter to continue to message, or -q to not open the message (recommended).')
|
||||
print('')
|
||||
if cancel != '-q':
|
||||
print(draw_border(self.myCore._utils.escapeAnsi(readBlock.bcontent.decode().strip())))
|
||||
reply = logger.readline("Press enter to continue, or enter %s to reply" % ("-r",))
|
||||
if reply == "-r":
|
||||
self.draftMessage(self.myCore._utils.bytesToStr(readBlock.signer,))
|
||||
try:
|
||||
print(draw_border(self.myCore._utils.escapeAnsi(readBlock.bcontent.decode().strip())))
|
||||
except ValueError:
|
||||
logger.warn('Error presenting message. This is usually due to a malformed or blank message.')
|
||||
pass
|
||||
if readBlock.validSig:
|
||||
reply = logger.readline("Press enter to continue, or enter %s to reply" % ("-r",))
|
||||
print('')
|
||||
if reply == "-r":
|
||||
self.draft_message(self.myCore._utils.bytesToStr(readBlock.signer,))
|
||||
else:
|
||||
logger.readline("Press enter to continue")
|
||||
print('')
|
||||
return
|
||||
|
||||
def sentbox(self):
|
||||
|
@ -153,7 +167,7 @@ class OnionrMail:
|
|||
'''
|
||||
entering = True
|
||||
while entering:
|
||||
self.getSentList()
|
||||
self.get_sent_list()
|
||||
logger.info('Enter a block number or -q to return')
|
||||
try:
|
||||
choice = input('>')
|
||||
|
@ -180,18 +194,19 @@ class OnionrMail:
|
|||
|
||||
return
|
||||
|
||||
def getSentList(self):
|
||||
def get_sent_list(self, display=True):
|
||||
count = 1
|
||||
self.sentboxList = []
|
||||
self.sentMessages = {}
|
||||
for i in self.sentboxTools.listSent():
|
||||
self.sentboxList.append(i['hash'])
|
||||
self.sentMessages[i['hash']] = (i['message'], i['peer'])
|
||||
|
||||
logger.info('%s. %s - %s - %s' % (count, i['hash'], i['peer'][:12], i['date']))
|
||||
self.sentMessages[i['hash']] = (self.myCore._utils.bytesToStr(i['message']), i['peer'], i['subject'])
|
||||
if display:
|
||||
logger.info('%s. %s - %s - (%s) - %s' % (count, i['hash'], i['peer'][:12], i['subject'], i['date']))
|
||||
count += 1
|
||||
return json.dumps(self.sentMessages)
|
||||
|
||||
def draftMessage(self, recip=''):
|
||||
def draft_message(self, recip=''):
|
||||
message = ''
|
||||
newLine = ''
|
||||
subject = ''
|
||||
|
@ -237,14 +252,26 @@ class OnionrMail:
|
|||
if not cancelEnter:
|
||||
logger.info('Inserting encrypted message as Onionr block....')
|
||||
|
||||
blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=True, meta={'subject': subject})
|
||||
self.sentboxTools.addToSent(blockID, recip, message)
|
||||
blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=self.doSigs, meta={'subject': subject})
|
||||
|
||||
def toggle_signing(self):
|
||||
self.doSigs = not self.doSigs
|
||||
|
||||
def menu(self):
|
||||
choice = ''
|
||||
while True:
|
||||
sigMsg = 'Message Signing: %s'
|
||||
|
||||
logger.info(self.strings.programTag + '\n\nOur ID: ' + self.myCore._crypto.pubKey + self.strings.mainMenu.title()) # print out main menu
|
||||
|
||||
logger.info(self.strings.programTag + '\n\nUser ID: ' + self.myCore._crypto.pubKey)
|
||||
if self.doSigs:
|
||||
sigMsg = sigMsg % ('enabled',)
|
||||
else:
|
||||
sigMsg = sigMsg % ('disabled (Your messages cannot be trusted)',)
|
||||
if self.doSigs:
|
||||
logger.info(sigMsg)
|
||||
else:
|
||||
logger.warn(sigMsg)
|
||||
logger.info(self.strings.mainMenu.title()) # print out main menu
|
||||
try:
|
||||
choice = logger.readline('Enter 1-%s:\n' % (len(self.strings.mainMenuChoices))).lower().strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
|
@ -255,8 +282,10 @@ class OnionrMail:
|
|||
elif choice in (self.strings.mainMenuChoices[1], '2'):
|
||||
self.sentbox()
|
||||
elif choice in (self.strings.mainMenuChoices[2], '3'):
|
||||
self.draftMessage()
|
||||
self.draft_message()
|
||||
elif choice in (self.strings.mainMenuChoices[3], '4'):
|
||||
self.toggle_signing()
|
||||
elif choice in (self.strings.mainMenuChoices[4], '5'):
|
||||
logger.info('Goodbye.')
|
||||
break
|
||||
elif choice == '':
|
||||
|
@ -265,6 +294,26 @@ class OnionrMail:
|
|||
logger.warn('Invalid choice.')
|
||||
return
|
||||
|
||||
def on_insertblock(api, data={}):
|
||||
sentboxTools = sentboxdb.SentBox(api.get_core())
|
||||
meta = json.loads(data['meta'])
|
||||
sentboxTools.addToSent(data['hash'], data['peer'], data['content'], meta['subject'])
|
||||
|
||||
def on_pluginrequest(api, data=None):
|
||||
resp = ''
|
||||
subject = ''
|
||||
recip = ''
|
||||
message = ''
|
||||
postData = {}
|
||||
blockID = ''
|
||||
sentboxTools = sentboxdb.SentBox(api.get_core())
|
||||
if data['name'] == 'mail':
|
||||
path = data['path']
|
||||
cmd = path.split('/')[1]
|
||||
if cmd == 'sentbox':
|
||||
resp = OnionrMail(api).get_sent_list(display=False)
|
||||
if resp != '':
|
||||
api.get_onionr().clientAPIInst.pluginResponses[data['pluginResponse']] = resp
|
||||
|
||||
def on_init(api, data = None):
|
||||
'''
|
||||
|
|
9
onionr/static-data/default-plugins/pms/sentboxdb.py
Normal file → Executable file
9
onionr/static-data/default-plugins/pms/sentboxdb.py
Normal file → Executable file
|
@ -37,6 +37,7 @@ class SentBox:
|
|||
hash id not null,
|
||||
peer text not null,
|
||||
message text not null,
|
||||
subject text not null,
|
||||
date int not null
|
||||
);
|
||||
''')
|
||||
|
@ -46,12 +47,12 @@ class SentBox:
|
|||
def listSent(self):
|
||||
retData = []
|
||||
for entry in self.cursor.execute('SELECT * FROM sent;'):
|
||||
retData.append({'hash': entry[0], 'peer': entry[1], 'message': entry[2], 'date': entry[3]})
|
||||
retData.append({'hash': entry[0], 'peer': entry[1], 'message': entry[2], 'subject': entry[3], 'date': entry[4]})
|
||||
return retData
|
||||
|
||||
def addToSent(self, blockID, peer, message):
|
||||
args = (blockID, peer, message, self.core._utils.getEpoch())
|
||||
self.cursor.execute('INSERT INTO sent VALUES(?, ?, ?, ?)', args)
|
||||
def addToSent(self, blockID, peer, message, subject=''):
|
||||
args = (blockID, peer, message, subject, self.core._utils.getEpoch())
|
||||
self.cursor.execute('INSERT INTO sent VALUES(?, ?, ?, ?, ?)', args)
|
||||
self.conn.commit()
|
||||
return
|
||||
|
||||
|
|
7
onionr/static-data/default_config.json
Normal file → Executable file
7
onionr/static-data/default_config.json
Normal file → Executable file
|
@ -2,8 +2,8 @@
|
|||
"general" : {
|
||||
"dev_mode" : true,
|
||||
"display_header" : false,
|
||||
"minimum_block_pow": 1,
|
||||
"minimum_send_pow": 1,
|
||||
"minimum_block_pow": 4,
|
||||
"minimum_send_pow": 4,
|
||||
"socket_servers": false,
|
||||
"security_level": 0,
|
||||
"max_block_age": 2678400,
|
||||
|
@ -21,8 +21,7 @@
|
|||
"private" : {
|
||||
"run" : true,
|
||||
"path" : "static-data/www/private/",
|
||||
"guess_mime" : true,
|
||||
"timing_protection" : true
|
||||
"guess_mime" : true
|
||||
},
|
||||
|
||||
"ui" : {
|
||||
|
|
0
onionr/static-data/default_plugin.py
Normal file → Executable file
0
onionr/static-data/default_plugin.py
Normal file → Executable file
0
onionr/static-data/header.txt
Normal file → Executable file
0
onionr/static-data/header.txt
Normal file → Executable file
0
onionr/static-data/index.html
Normal file → Executable file
0
onionr/static-data/index.html
Normal file → Executable file
File diff suppressed because one or more lines are too long
|
@ -2,6 +2,101 @@
|
|||
padding-top: 1em;
|
||||
}
|
||||
.threads div span{
|
||||
<<<<<<< HEAD
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
=======
|
||||
padding-left: 0.2em;
|
||||
padding-right: 0.2em;
|
||||
}
|
||||
|
||||
#threadPlaceholder{
|
||||
display: none;
|
||||
margin-top: 1em;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
input{
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.btn-group button {
|
||||
border: 1px solid black;
|
||||
padding: 10px 24px; /* Some padding */
|
||||
cursor: pointer; /* Pointer/hand icon */
|
||||
float: left; /* Float the buttons side by side */
|
||||
}
|
||||
|
||||
.btn-group button:hover {
|
||||
background-color: darkgray;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
#tabBtns{
|
||||
margin-bottom: 3em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.activeTab{
|
||||
color: black;
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.overlayContent{
|
||||
background-color: lightgray;
|
||||
border: 3px solid black;
|
||||
border-radius: 3px;
|
||||
color: black;
|
||||
font-family: Verdana, Geneva, Tahoma, sans-serif;
|
||||
min-height: 100%;
|
||||
padding: 1em;
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.danger{
|
||||
color: red;
|
||||
}
|
||||
|
||||
.warn{
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.good{
|
||||
color: greenyellow;
|
||||
}
|
||||
|
||||
.pre{
|
||||
padding-top: 1em;
|
||||
word-wrap: break-word;
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
}
|
||||
.messageContent{
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
#draftText{
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
display: block;
|
||||
width: 50%;
|
||||
height: 75%;
|
||||
min-width: 2%;
|
||||
min-height: 5%;
|
||||
background: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.successBtn{
|
||||
background-color: #28a745;
|
||||
border-radius: 3px;
|
||||
padding: 5px;
|
||||
color: black;
|
||||
font-size: 1.5em;
|
||||
width: 10%;
|
||||
>>>>>>> contacts
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
<<<<<<< HEAD
|
||||
pms = ''
|
||||
threadPart = document.getElementById('threads')
|
||||
function getInbox(){
|
||||
|
@ -38,6 +39,183 @@ function getInbox(){
|
|||
}.bind([pms, i]))
|
||||
}
|
||||
|
||||
=======
|
||||
/*
|
||||
Onionr - P2P Anonymous Storage Network
|
||||
|
||||
This file handles the mail interface
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
pms = ''
|
||||
sentbox = ''
|
||||
threadPart = document.getElementById('threads')
|
||||
threadPlaceholder = document.getElementById('threadPlaceholder')
|
||||
tabBtns = document.getElementById('tabBtns')
|
||||
threadContent = {}
|
||||
myPub = httpGet('/getActivePubkey')
|
||||
|
||||
function openThread(bHash, sender, date, sigBool){
|
||||
var messageDisplay = document.getElementById('threadDisplay')
|
||||
var blockContent = httpGet('/getblockbody/' + bHash)
|
||||
document.getElementById('fromUser').value = sender
|
||||
messageDisplay.innerText = blockContent
|
||||
var sigEl = document.getElementById('sigValid')
|
||||
var sigMsg = 'signature'
|
||||
|
||||
if (sigBool){
|
||||
sigMsg = 'Good ' + sigMsg
|
||||
sigEl.classList.remove('danger')
|
||||
}
|
||||
else{
|
||||
sigMsg = 'Bad/no ' + sigMsg + ' (message could be fake)'
|
||||
sigEl.classList.add('danger')
|
||||
}
|
||||
sigEl.innerText = sigMsg
|
||||
overlay('messageDisplay')
|
||||
}
|
||||
|
||||
function setActiveTab(tabName){
|
||||
threadPart.innerHTML = ""
|
||||
switch(tabName){
|
||||
case 'inbox':
|
||||
getInbox()
|
||||
break
|
||||
case 'sentbox':
|
||||
getSentbox()
|
||||
break
|
||||
case 'send message':
|
||||
overlay('sendMessage')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function loadInboxEntrys(bHash){
|
||||
fetch('/getblockheader/' + bHash, {
|
||||
headers: {
|
||||
"token": webpass
|
||||
}})
|
||||
.then((resp) => resp.json()) // Transform the data into json
|
||||
.then(function(resp) {
|
||||
//console.log(resp)
|
||||
var entry = document.createElement('div')
|
||||
var bHashDisplay = document.createElement('span')
|
||||
var senderInput = document.createElement('input')
|
||||
var subjectLine = document.createElement('span')
|
||||
var dateStr = document.createElement('span')
|
||||
var validSig = document.createElement('span')
|
||||
var humanDate = new Date(0)
|
||||
var metadata = resp['metadata']
|
||||
humanDate.setUTCSeconds(resp['meta']['time'])
|
||||
if (resp['meta']['signer'] != ''){
|
||||
senderInput.value = httpGet('/getHumanReadable/' + resp['meta']['signer'])
|
||||
}
|
||||
if (resp['meta']['validSig']){
|
||||
validSig.innerText = 'Signature Validity: Good'
|
||||
}
|
||||
else{
|
||||
validSig.innerText = 'Signature Validity: Bad'
|
||||
validSig.style.color = 'red'
|
||||
}
|
||||
if (senderInput.value == ''){
|
||||
senderInput.value = 'Anonymous'
|
||||
}
|
||||
bHashDisplay.innerText = bHash.substring(0, 10)
|
||||
entry.setAttribute('hash', bHash)
|
||||
senderInput.readOnly = true
|
||||
dateStr.innerText = humanDate.toString()
|
||||
if (metadata['subject'] === undefined || metadata['subject'] === null) {
|
||||
subjectLine.innerText = '()'
|
||||
}
|
||||
else{
|
||||
subjectLine.innerText = '(' + metadata['subject'] + ')'
|
||||
}
|
||||
//entry.innerHTML = 'sender ' + resp['meta']['signer'] + ' - ' + resp['meta']['time']
|
||||
threadPart.appendChild(entry)
|
||||
entry.appendChild(bHashDisplay)
|
||||
entry.appendChild(senderInput)
|
||||
entry.appendChild(validSig)
|
||||
entry.appendChild(subjectLine)
|
||||
entry.appendChild(dateStr)
|
||||
entry.classList.add('threadEntry')
|
||||
|
||||
entry.onclick = function(){
|
||||
openThread(entry.getAttribute('hash'), senderInput.value, dateStr.innerText, resp['meta']['validSig'])
|
||||
}
|
||||
|
||||
}.bind(bHash))
|
||||
}
|
||||
|
||||
function getInbox(){
|
||||
var showed = false
|
||||
var requested = ''
|
||||
for(var i = 0; i < pms.length; i++) {
|
||||
if (pms[i].trim().length == 0){
|
||||
continue
|
||||
}
|
||||
else{
|
||||
threadPlaceholder.style.display = 'none'
|
||||
showed = true
|
||||
}
|
||||
loadInboxEntrys(pms[i])
|
||||
}
|
||||
if (! showed){
|
||||
threadPlaceholder.style.display = 'block'
|
||||
}
|
||||
}
|
||||
|
||||
function getSentbox(){
|
||||
fetch('/apipoints/mail/sentbox', {
|
||||
headers: {
|
||||
"token": webpass
|
||||
}})
|
||||
.then((resp) => resp.json()) // Transform the data into json
|
||||
.then(function(resp) {
|
||||
var keys = [];
|
||||
var entry = document.createElement('div')
|
||||
var entryUsed;
|
||||
for(var k in resp) keys.push(k);
|
||||
for (var i = 0; i < keys.length; i++){
|
||||
var entry = document.createElement('div')
|
||||
var obj = resp[i];
|
||||
var toLabel = document.createElement('span')
|
||||
toLabel.innerText = 'To: '
|
||||
var toEl = document.createElement('input')
|
||||
var preview = document.createElement('span')
|
||||
toEl.readOnly = true
|
||||
toEl.value = resp[keys[i]][1]
|
||||
preview.innerText = '(' + resp[keys[i]][2] + ')'
|
||||
entry.appendChild(toLabel)
|
||||
entry.appendChild(toEl)
|
||||
entry.appendChild(preview)
|
||||
entryUsed = resp[keys[i]]
|
||||
entry.onclick = function(){
|
||||
console.log(resp)
|
||||
showSentboxWindow(toEl.value, entryUsed[0])
|
||||
}
|
||||
threadPart.appendChild(entry)
|
||||
}
|
||||
threadPart.appendChild(entry)
|
||||
}.bind(threadPart))
|
||||
}
|
||||
|
||||
function showSentboxWindow(to, content){
|
||||
document.getElementById('toID').value = to
|
||||
document.getElementById('sentboxDisplayText').innerText = content
|
||||
overlay('sentboxDisplay')
|
||||
>>>>>>> contacts
|
||||
}
|
||||
|
||||
fetch('/getblocksbytype/pm', {
|
||||
|
@ -47,6 +225,43 @@ fetch('/getblocksbytype/pm', {
|
|||
.then((resp) => resp.text()) // Transform the data into json
|
||||
.then(function(data) {
|
||||
pms = data.split(',')
|
||||
<<<<<<< HEAD
|
||||
getInbox(pms)
|
||||
})
|
||||
|
||||
=======
|
||||
setActiveTab('inbox')
|
||||
})
|
||||
|
||||
tabBtns.onclick = function(event){
|
||||
var children = tabBtns.children
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
var btn = children[i]
|
||||
btn.classList.remove('activeTab')
|
||||
}
|
||||
event.target.classList.add('activeTab')
|
||||
setActiveTab(event.target.innerText.toLowerCase())
|
||||
}
|
||||
|
||||
var idStrings = document.getElementsByClassName('myPub')
|
||||
var myHumanReadable = httpGet('/getHumanReadable/' + myPub)
|
||||
for (var i = 0; i < idStrings.length; i++){
|
||||
if (idStrings[i].tagName.toLowerCase() == 'input'){
|
||||
idStrings[i].value = myHumanReadable
|
||||
}
|
||||
else{
|
||||
idStrings[i].innerText = myHumanReadable
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < document.getElementsByClassName('refresh').length; i++){
|
||||
document.getElementsByClassName('refresh')[i].style.float = 'right'
|
||||
}
|
||||
|
||||
for (var i = 0; i < document.getElementsByClassName('closeOverlay').length; i++){
|
||||
document.getElementsByClassName('closeOverlay')[i].onclick = function(e){
|
||||
document.getElementById(e.target.getAttribute('overlay')).style.visibility = 'hidden'
|
||||
}
|
||||
}
|
||||
|
||||
>>>>>>> contacts
|
||||
|
|
45
onionr/static-data/www/mail/sendmail.js
Normal file
45
onionr/static-data/www/mail/sendmail.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
Onionr - P2P Anonymous Storage Network
|
||||
|
||||
This file handles the mail interface
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
var sendbutton = document.getElementById('sendMail')
|
||||
|
||||
function sendMail(to, message, subject){
|
||||
//postData = {"postData": '{"to": "' + to + '", "message": "' + message + '"}'} // galaxy brain
|
||||
postData = {'message': message, 'to': to, 'type': 'pm', 'encrypt': true, 'meta': JSON.stringify({'subject': subject})}
|
||||
postData = JSON.stringify(postData)
|
||||
fetch('/insertblock', {
|
||||
method: 'POST',
|
||||
body: postData,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"token": webpass
|
||||
}})
|
||||
.then((resp) => resp.text()) // Transform the data into json
|
||||
.then(function(data) {
|
||||
})
|
||||
}
|
||||
|
||||
sendForm.onsubmit = function(){
|
||||
var messageContent = document.getElementById('draftText')
|
||||
var to = document.getElementById('draftID')
|
||||
var subject = document.getElementById('draftSubject')
|
||||
|
||||
sendMail(to.value, messageContent.value, subject.value)
|
||||
return false;
|
||||
}
|
|
@ -21,6 +21,7 @@
|
|||
<br><br><a class='idLink' href='/mail/'>Mail</a>
|
||||
<h2>Stats</h2>
|
||||
<p>Uptime: <span id='uptime'></span></p>
|
||||
<p>Last Received Connection: <span id='lastIncoming'>Unknown</span></p>
|
||||
<p>Stored Blocks: <span id='storedBlocks'></span></p>
|
||||
<p>Blocks in queue: <span id='blockQueue'></span></p>
|
||||
<p>Connected nodes:</p>
|
||||
|
|
|
@ -21,6 +21,10 @@ uptimeDisplay = document.getElementById('uptime')
|
|||
connectedDisplay = document.getElementById('connectedNodes')
|
||||
storedBlockDisplay = document.getElementById('storedBlocks')
|
||||
queuedBlockDisplay = document.getElementById('blockQueue')
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
lastIncoming = document.getElementById('lastIncoming')
|
||||
>>>>>>> contacts
|
||||
|
||||
function getStats(){
|
||||
stats = JSON.parse(httpGet('getstats', webpass))
|
||||
|
@ -28,5 +32,18 @@ function getStats(){
|
|||
connectedDisplay.innerText = stats['connectedNodes']
|
||||
storedBlockDisplay.innerText = stats['blockCount']
|
||||
queuedBlockDisplay.innerText = stats['blockQueueCount']
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
var lastConnect = httpGet('/lastconnect')
|
||||
if (lastConnect > 0){
|
||||
var humanDate = new Date(0)
|
||||
humanDate.setUTCSeconds(httpGet('/lastconnect'))
|
||||
lastConnect = humanDate.toString()
|
||||
}
|
||||
else{
|
||||
lastConnect = 'Unknown'
|
||||
}
|
||||
lastIncoming.innerText = lastConnect
|
||||
>>>>>>> contacts
|
||||
}
|
||||
getStats()
|
|
@ -37,7 +37,11 @@ body{
|
|||
align-items:center;
|
||||
}
|
||||
.logo{
|
||||
<<<<<<< HEAD
|
||||
max-width: 25%;
|
||||
=======
|
||||
max-width: 20%;
|
||||
>>>>>>> contacts
|
||||
vertical-align: middle;
|
||||
}
|
||||
.logoText{
|
||||
|
@ -132,9 +136,31 @@ body{
|
|||
left: 0px;
|
||||
top: 0px;
|
||||
width:100%;
|
||||
<<<<<<< HEAD
|
||||
opacity: 0.9;
|
||||
height:100%;
|
||||
text-align:center;
|
||||
z-index: 1000;
|
||||
background-color: black;
|
||||
}
|
||||
=======
|
||||
height:100%;
|
||||
text-align:left;
|
||||
z-index: 1000;
|
||||
background-color: #2c2b3f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.closeOverlay{
|
||||
background-color: white;
|
||||
color: black;
|
||||
border: 1px solid red;
|
||||
border-radius: 5px;
|
||||
float: right;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.closeOverlay:after{
|
||||
content: '❌';
|
||||
padding: 5px;
|
||||
}
|
||||
>>>>>>> contacts
|
||||
|
|
|
@ -1,5 +1,30 @@
|
|||
<<<<<<< HEAD
|
||||
webpass = document.location.hash.replace('#', '')
|
||||
nowebpass = false
|
||||
=======
|
||||
/*
|
||||
Onionr - P2P Anonymous Storage Network
|
||||
|
||||
This file handles the mail interface
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
webpass = document.location.hash.replace('#', '')
|
||||
nowebpass = false
|
||||
|
||||
>>>>>>> contacts
|
||||
if (typeof webpass == "undefined"){
|
||||
webpass = localStorage['webpass']
|
||||
}
|
||||
|
@ -12,6 +37,13 @@ if (typeof webpass == "undefined" || webpass == ""){
|
|||
nowebpass = true
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
function arrayContains(needle, arrhaystack) {
|
||||
return (arrhaystack.indexOf(needle) > -1);
|
||||
}
|
||||
|
||||
>>>>>>> contacts
|
||||
function httpGet(theUrl) {
|
||||
var xmlHttp = new XMLHttpRequest()
|
||||
xmlHttp.open( "GET", theUrl, false ) // false for synchronous request
|
||||
|
@ -27,6 +59,10 @@ function httpGet(theUrl) {
|
|||
function overlay(overlayID) {
|
||||
el = document.getElementById(overlayID)
|
||||
el.style.visibility = (el.style.visibility == "visible") ? "hidden" : "visible"
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
scroll(0,0)
|
||||
>>>>>>> contacts
|
||||
}
|
||||
|
||||
var passLinks = document.getElementsByClassName("idLink")
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
# Onionr UI
|
||||
|
||||
## About
|
||||
|
||||
The default GUI for Onionr
|
||||
|
||||
## Setup
|
||||
|
||||
To compile the application, simply execute the following:
|
||||
|
||||
```
|
||||
python3 compile.py
|
||||
```
|
||||
|
||||
If you are wanting to compile Onionr UI for another language, execute the following, replacing `[lang]` with the target language (supported languages include `eng` for English, `spa` para español, and `zho`为中国人):
|
||||
|
||||
```
|
||||
python3 compile.py [lang]
|
||||
```
|
||||
|
||||
## FAQ
|
||||
### Why "compile" anyway?
|
||||
This web application is compiled for a few reasons:
|
||||
1. To make it easier to update; this way, we do not have to update the header in every file if we want to change something about it.
|
||||
2. To make the application smaller in size; there is less duplicated code when the code like the header and footer can be stored in an individual file rather than every file.
|
||||
3. For multi-language support; with the Python "tags" feature, we can reference strings by variable name, and based on a language file, they can be dynamically inserted into the page on compilation.
|
||||
4. For compile-time customizations.
|
||||
|
||||
### What exactly happens when you compile?
|
||||
Upon compilation, files from the `src/` directory will be copied to `dist/` directory, header and footers will be injected in the proper places, and Python "tags" will be interpreted.
|
||||
|
||||
|
||||
### How do Python "tags" work?
|
||||
There are two types of Python "tags":
|
||||
1. Logic tags (`<$ logic $>`): These tags allow you to perform logic at compile time. Example: `<$ import datetime; lastUpdate = datetime.datetime.now() $>`: This gets the current time while compiling, then stores it in `lastUpdate`.
|
||||
2. Data tags (`<$= data $>`): These tags take whatever the return value of the statement in the tags is, and write it directly to the page. Example: `<$= 'This application was compiled at %s.' % lastUpdate $>`: This will write the message in the string in the tags to the page.
|
||||
|
||||
**Note:** Logic tags take a higher priority and will always be interpreted first.
|
||||
|
||||
### How does the language feature work?
|
||||
When you use a data tag to write a string to the page (e.g. `<$= LANG.HELLO_WORLD $>`), the language feature simply takes dictionary of the language that is currently being used from the language map file (`lang.json`), then searches for the key (being the variable name after the characters `LANG.` in the data tag, like `HELLO_WORLD` from the example before). It then writes that string to the page. Language variables are always prefixed with `LANG.` and should always be uppercase (as they are a constant).
|
||||
|
||||
### I changed a few things in the application and tried to view the updates in my browser, but nothing changed!
|
||||
You most likely forgot to compile. Try running `python3 compile.py` and check again. If you are still having issues, [open up an issue](https://gitlab.com/beardog/Onionr/issues/new?issue[title]=Onionr UI not updating after compiling).
|
File diff suppressed because one or more lines are too long
|
@ -1,19 +0,0 @@
|
|||
<!-- Modal -->
|
||||
<div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="modal-title" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modal-title"><$= LANG.MODAL_TITLE $></h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" id="modal-content"><$= LANG.MODAL_MESSAGE $></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
|
||||
<script src="js/main.js"></script>
|
|
@ -1,30 +0,0 @@
|
|||
<title><$= LANG.ONIONR_TITLE $></title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" type="text/css" href="css/main.css" />
|
||||
<link rel="stylesheet" type="text/css" href="css/themes/dark.css" />
|
||||
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
|
||||
<a class="navbar-brand" href="#">Onionr</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="index.html"><$= LANG.TIMELINE $></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="notifications.html"><$= LANG.NOTIFICATIONS $></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="messages.html"><$= LANG.MESSAGES $></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
|
@ -1,31 +0,0 @@
|
|||
<!-- POST REPLIES -->
|
||||
<div class="onionr-post-creator">
|
||||
<div class="row">
|
||||
<div class="onionr-reply-creator container">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<img class="onionr-post-creator-user-icon" id="onionr-reply-creator-user-icon">
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<div class="row">
|
||||
<div class="col col-auto">
|
||||
<a class="onionr-post-creator-user-name" id="onionr-reply-creator-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')"></a>
|
||||
<a class="onionr-post-creator-user-id" id="onionr-reply-creator-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')" data-placement="top" data-toggle="tooltip" title="$user-id"><$= LANG.REPLY_CREATOR_YOU $></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea class="onionr-post-creator-content" id="onionr-reply-creator-content" oninput="replyCreatorChange()"></textarea>
|
||||
|
||||
<div class="onionr-post-creator-content-message" id="onionr-reply-creator-content-message"></div>
|
||||
|
||||
<input type="button" onclick="makeReply()" title="<$= LANG.REPLY_CREATOR_CREATE $>" value="<$= LANG.REPLY_CREATOR_CREATE $>" id="onionr-reply-creator-create" class="onionr-post-creator-create" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div id="onionr-replies"></div>
|
||||
</div>
|
||||
<!-- END POST REPLIES -->
|
|
@ -1,32 +0,0 @@
|
|||
<!-- POST -->
|
||||
<div class="col-12">
|
||||
<div class="onionr-post" id="onionr-post-$post-hash" onclick="focusPost('$post-hash', 'user-id-url', 'user-name-url', '')">
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<img class="onionr-post-user-icon" src="$user-image">
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<div class="row">
|
||||
<div class="col col-auto">
|
||||
<a class="onionr-post-user-name" id="onionr-post-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')">$user-name</a>
|
||||
<a class="onionr-post-user-id" id="onionr-post-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')" data-placement="top" data-toggle="tooltip" title="$user-id">$user-id-truncated</a>
|
||||
</div>
|
||||
|
||||
<div class="col col-auto text-right ml-auto pl-0">
|
||||
<div class="onionr-post-date text-right" data-placement="top" data-toggle="tooltip" title="$date">$date-relative</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="onionr-post-content">
|
||||
$content
|
||||
</div>
|
||||
|
||||
<div class="onionr-post-controls pt-2">
|
||||
<a href="#!" onclick="toggleLike('$post-hash')" class="glyphicon glyphicon-heart mr-2">$liked</a>
|
||||
<a href="#!" onclick="reply('$post-hash')" class="glyphicon glyphicon-comment mr-2"><$= LANG.POST_REPLY $></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END POST -->
|
|
@ -1,30 +0,0 @@
|
|||
<!-- POST FOCUS REPLIES -->
|
||||
<div class="col-12">
|
||||
<div class="row">
|
||||
<div class="onionr-post-focus-reply-creator">
|
||||
<div class="row">
|
||||
<div class="col-1"></div>
|
||||
<div class="col-2">
|
||||
<img class="onionr-post-creator-user-icon" id="onionr-post-focus-reply-creator-user-icon">
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<div class="row">
|
||||
<div class="col col-auto">
|
||||
<a class="onionr-post-creator-user-name" id="onionr-post-focus-reply-creator-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')"></a>
|
||||
<a class="onionr-post-creator-user-id" id="onionr-post-focus-reply-creator-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')" data-placement="top" data-toggle="tooltip" title="$user-id"><$= LANG.REPLY_CREATOR_YOU $></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea class="onionr-post-creator-content" id="onionr-post-focus-reply-creator-content" oninput="focusReplyCreatorChange()"></textarea>
|
||||
|
||||
<div class="onionr-post-creator-content-message" id="onionr-post-focus-reply-creator-content-message"></div>
|
||||
|
||||
<input type="button" onclick="makeFocusReply()" title="<$= LANG.REPLY_CREATOR_CREATE $>" value="<$= LANG.REPLY_CREATOR_CREATE $>" id="onionr-post-focus-reply-creator-create" class="onionr-post-creator-create" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="onionr-post-focus-replies"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END POST FOCUS REPLIES -->
|
|
@ -1,31 +0,0 @@
|
|||
<!-- POST -->
|
||||
<div class="col-12">
|
||||
<div class="onionr-post" id="onionr-post-$post-hash" onclick="focusPost('$post-hash', 'user-id-url', 'user-name-url', '')">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<img class="onionr-post-user-icon" src="$user-image">
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<div class="row">
|
||||
<div class="col col-auto">
|
||||
<a class="onionr-post-user-name" id="onionr-post-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')">$user-name</a>
|
||||
</div>
|
||||
|
||||
<div class="col col-auto text-right ml-auto pl-0">
|
||||
<div class="onionr-post-date text-right" data-placement="top" data-toggle="tooltip" title="$date">$date-relative-truncated</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="onionr-post-content">
|
||||
$content
|
||||
</div>
|
||||
|
||||
<div class="onionr-post-controls pt-2">
|
||||
<a href="#!" onclick="toggleLike('$post-hash')" class="glyphicon glyphicon-heart mr-2">$liked</a>
|
||||
<a href="#!" onclick="reply('$post-hash')" class="glyphicon glyphicon-comment mr-2"><$= LANG.POST_REPLY $></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END POST -->
|
|
@ -1,130 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import shutil, os, re, json, traceback
|
||||
|
||||
# get user's config
|
||||
settings = {}
|
||||
with open('config.json', 'r') as file:
|
||||
settings = json.loads(file.read())
|
||||
|
||||
# "hardcoded" config, not for user to mess with
|
||||
HEADER_FILE = 'common/header.html'
|
||||
FOOTER_FILE = 'common/footer.html'
|
||||
SRC_DIR = 'src/'
|
||||
DST_DIR = 'dist/'
|
||||
HEADER_STRING = '<header />'
|
||||
FOOTER_STRING = '<footer />'
|
||||
|
||||
# remove dst folder
|
||||
shutil.rmtree(DST_DIR, ignore_errors=True)
|
||||
|
||||
# taken from https://stackoverflow.com/questions/1868714/how-do-i-copy-an-entire-directory-of-files-into-an-existing-directory-using-pyth
|
||||
def copytree(src, dst, symlinks=False, ignore=None):
|
||||
for item in os.listdir(src):
|
||||
s = os.path.join(src, item)
|
||||
d = os.path.join(dst, item)
|
||||
if os.path.isdir(s):
|
||||
shutil.copytree(s, d, symlinks, ignore)
|
||||
else:
|
||||
shutil.copy2(s, d)
|
||||
|
||||
# copy src to dst
|
||||
copytree(SRC_DIR, DST_DIR, False)
|
||||
|
||||
# load in lang map
|
||||
langmap = {}
|
||||
|
||||
with open('lang.json', 'r') as file:
|
||||
langmap = json.loads(file.read())[settings['language']]
|
||||
|
||||
LANG = type('LANG', (), langmap)
|
||||
|
||||
# templating
|
||||
class Template:
|
||||
def jsTemplate(template, filename = ''):
|
||||
with open('common/%s.html' % template, 'r') as file:
|
||||
return Template.parseTags(file.read().replace('\\', '\\\\').replace('\'', '\\\'').replace('\n', "\\\n"), filename)
|
||||
|
||||
def htmlTemplate(template, filename = ''):
|
||||
with open('common/%s.html' % template, 'r') as file:
|
||||
return Template.parseTags(file.read(), filename)
|
||||
|
||||
# tag parser
|
||||
def parseTags(contents, filename = ''):
|
||||
# <$ logic $>
|
||||
for match in re.findall(r'(<\$(?!=)(.*?)\$>)', contents):
|
||||
try:
|
||||
out = exec(match[1].strip())
|
||||
contents = contents.replace(match[0], '' if out is None else str(out))
|
||||
except Exception as e:
|
||||
print('Error: Failed to execute python tag (%s): %s\n' % (filename, match[1]))
|
||||
traceback.print_exc()
|
||||
print('\nIgnoring this error, continuing to compile...\n')
|
||||
|
||||
# <$= data $>
|
||||
for match in re.findall(r'(<\$=(.*?)\$>)', contents):
|
||||
try:
|
||||
out = eval(match[1].strip())
|
||||
contents = contents.replace(match[0], '' if out is None else str(out))
|
||||
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)
|
||||
except Exception as e:
|
||||
print('Error: Failed to execute python tag (%s): %s\n' % (filename, match[1]))
|
||||
traceback.print_exc()
|
||||
print('\nIgnoring this error, continuing to compile...\n')
|
||||
|
||||
return contents
|
||||
|
||||
def jsTemplate(contents):
|
||||
return Template.jsTemplate(contents)
|
||||
|
||||
def htmlTemplate(contents):
|
||||
return Template.htmlTemplate(contents)
|
||||
|
||||
# get header file
|
||||
with open(HEADER_FILE, 'r') as file:
|
||||
HEADER_FILE = file.read()
|
||||
if settings['python_tags']:
|
||||
HEADER_FILE = Template.parseTags(HEADER_FILE)
|
||||
|
||||
# get footer file
|
||||
with open(FOOTER_FILE, 'r') as file:
|
||||
FOOTER_FILE = file.read()
|
||||
if settings['python_tags']:
|
||||
FOOTER_FILE = Template.parseTags(FOOTER_FILE)
|
||||
|
||||
# iterate dst, replace files
|
||||
def iterate(directory):
|
||||
for filename in os.listdir(directory):
|
||||
if filename.split('.')[-1].lower() in ['htm', 'html', 'css', 'js']:
|
||||
try:
|
||||
path = os.path.join(directory, filename)
|
||||
if os.path.isdir(path):
|
||||
iterate(path)
|
||||
else:
|
||||
contents = ''
|
||||
with open(path, 'r') as file:
|
||||
# get file contents
|
||||
contents = file.read()
|
||||
|
||||
os.remove(path)
|
||||
|
||||
with open(path, 'w') as file:
|
||||
# set the header & footer
|
||||
contents = contents.replace(HEADER_STRING, HEADER_FILE)
|
||||
contents = contents.replace(FOOTER_STRING, FOOTER_FILE)
|
||||
|
||||
# do python tags
|
||||
if settings['python_tags']:
|
||||
contents = Template.parseTags(contents, filename)
|
||||
|
||||
# write file
|
||||
file.write(contents)
|
||||
except Exception as e:
|
||||
print('Error: Failed to parse file: %s\n' % filename)
|
||||
traceback.print_exc()
|
||||
print('\nIgnoring this error, continuing to compile...\n')
|
||||
|
||||
iterate(DST_DIR)
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"language" : "eng",
|
||||
"python_tags" : true
|
||||
}
|
122
onionr/static-data/www/ui/dist/css/main.css
vendored
122
onionr/static-data/www/ui/dist/css/main.css
vendored
|
@ -1,122 +0,0 @@
|
|||
/* general formatting */
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container-small {
|
||||
width: 300px;
|
||||
}
|
||||
.container-large {
|
||||
width: 970px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.container-small {
|
||||
width: 500px;
|
||||
}
|
||||
.container-large {
|
||||
width: 1170px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.container-small {
|
||||
width: 700px;
|
||||
}
|
||||
.container-large {
|
||||
width: 1500px;
|
||||
}
|
||||
}
|
||||
|
||||
.container-small, .container-large {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* navbar */
|
||||
|
||||
body {
|
||||
margin-top: 5rem;
|
||||
}
|
||||
|
||||
/* 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%;
|
||||
}
|
||||
|
||||
.onionr-post-user-name {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.onionr-post-user-id:before { content: "("; }
|
||||
.onionr-post-user-id:after { content: ")"; }
|
||||
|
||||
.onionr-post-content {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.onionr-post-user-icon {
|
||||
border-radius: 100%;
|
||||
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;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* profile */
|
||||
|
||||
.onionr-profile-user-icon {
|
||||
border-radius: 100%;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.onionr-profile-username {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.onionr-profile-save {
|
||||
width: 100%;
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
body {
|
||||
background-color: #96928f;
|
||||
color: #25383C;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
|
||||
background-color: lightgray;
|
||||
}
|
||||
|
||||
.onionr-post-user-name {
|
||||
color: green;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.onionr-post-user-id {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.onionr-post-date {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.onionr-post-content {
|
||||
font-family: sans-serif, serif;
|
||||
border-top: 1px solid black;
|
||||
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;
|
||||
}
|
BIN
onionr/static-data/www/ui/dist/img/default.png
vendored
BIN
onionr/static-data/www/ui/dist/img/default.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 6.6 KiB |
215
onionr/static-data/www/ui/dist/index.html
vendored
215
onionr/static-data/www/ui/dist/index.html
vendored
|
@ -1,215 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Onionr UI</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" type="text/css" href="css/main.css" />
|
||||
<link rel="stylesheet" type="text/css" href="css/themes/dark.css" />
|
||||
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
|
||||
<a class="navbar-brand" href="#">Onionr</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="index.html">Timeline</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="notifications.html">Notifications</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="messages.html">Messages</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="onionr-profile">
|
||||
<div class="row">
|
||||
<div class="col-4 col-lg-12">
|
||||
<img id="onionr-profile-user-icon" class="onionr-profile-user-icon" src="">
|
||||
</div>
|
||||
<div class="col-8 col-lg-12">
|
||||
<h2 maxlength="25" id="onionr-profile-username" class="onionr-profile-username text-left text-lg-center text-sm-left" data-placement="top" data-toggle="tooltip" title="unknown" data-editable></h2>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<p maxlength="128" id="onionr-profile-description" class="onionr-profile-description" data-editable></p>
|
||||
</div>
|
||||
|
||||
<div class="col-12 onionr-profile-edit" id="onionr-profile-edit" style="display: none">
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-lg-12">
|
||||
<input type="button" onclick="updateUser()" class="onionr-profile-save text-center" id="onionr-profile-save" value="Save" />
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-12">
|
||||
<input type="button" onclick="cancelUpdate()" class="onionr-profile-save text-center" id="onionr-profile-cancel" value="Cancel" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-divider pb-3 d-block d-lg-none"></div>
|
||||
|
||||
<div class="col-sm-12 col-lg-6">
|
||||
<div class="row" id="onionr-timeline-post-creator">
|
||||
<div class="col-12">
|
||||
<div class="onionr-timeline">
|
||||
<h2>Timeline</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- POST CREATOR -->
|
||||
<div class="col-12">
|
||||
<div class="onionr-post-creator">
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<img class="onionr-post-creator-user-icon" id="onionr-post-creator-user-icon">
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<div class="row">
|
||||
<div class="col col-auto">
|
||||
<a class="onionr-post-creator-user-name" id="onionr-post-creator-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')"></a>
|
||||
<a class="onionr-post-creator-user-id" id="onionr-post-creator-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')" data-placement="top" data-toggle="tooltip" title="$user-id">you</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea class="onionr-post-creator-content" id="onionr-post-creator-content" oninput="postCreatorChange()"></textarea>
|
||||
|
||||
<div class="onionr-post-creator-content-message" id="onionr-post-creator-content-message"></div>
|
||||
|
||||
<input type="button" onclick="makePost()" title="Create post" value="Create post" id="onionr-post-creator-create" class="onionr-post-creator-create" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END POST CREATOR -->
|
||||
</div>
|
||||
|
||||
<div class="row" id="onionr-timeline-posts">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-none d-lg-block col-lg-3">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="onionr-replies">
|
||||
<h2 id="onionr-replies-title"></h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="onionr-reply-creator-panel">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- POST FOCUS DIALOG -->
|
||||
<div class="modal fade" id="onionr-post-focus" tabindex="-1" role="dialog" aria-labelledby="modal-title" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="row p-3">
|
||||
<div class="col-2">
|
||||
<img src="" id="onionr-post-focus-user-icon" class="onionr-post-user-icon">
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<div class="row">
|
||||
<div class="col col-auto">
|
||||
<a class="onionr-post-user-name" id="onionr-post-focus-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url'); jQuery('#onionr-post-focus').modal('hide');">$user-name</a>
|
||||
<a class="onionr-post-user-id" id="onionr-post-focus-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url'); jQuery('#onionr-post-focus').modal('hide');" data-placement="top" data-toggle="tooltip" title="$user-id">$user-id-truncated</a>
|
||||
</div>
|
||||
|
||||
<div class="col col-auto text-right ml-auto pl-0">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="onionr-post-content" id="onionr-post-focus-content">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="col-12 onionr-post-focus-separator" />
|
||||
|
||||
<!-- POST FOCUS REPLIES -->
|
||||
<div class="col-12">
|
||||
<div class="row">
|
||||
<div class="onionr-post-focus-reply-creator">
|
||||
<div class="row">
|
||||
<div class="col-1"></div>
|
||||
<div class="col-2">
|
||||
<img class="onionr-post-creator-user-icon" id="onionr-post-focus-reply-creator-user-icon">
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<div class="row">
|
||||
<div class="col col-auto">
|
||||
<a class="onionr-post-creator-user-name" id="onionr-post-focus-reply-creator-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')"></a>
|
||||
<a class="onionr-post-creator-user-id" id="onionr-post-focus-reply-creator-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')" data-placement="top" data-toggle="tooltip" title="$user-id">you</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea class="onionr-post-creator-content" id="onionr-post-focus-reply-creator-content" oninput="focusReplyCreatorChange()"></textarea>
|
||||
|
||||
<div class="onionr-post-creator-content-message" id="onionr-post-focus-reply-creator-content-message"></div>
|
||||
|
||||
<input type="button" onclick="makeFocusReply()" title="Reply" value="Reply" id="onionr-post-focus-reply-creator-create" class="onionr-post-creator-create" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="onionr-post-focus-replies"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END POST FOCUS REPLIES -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END POST FOCUS DIALOG -->
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="modal-title" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modal-title">Loading...</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" id="modal-content">Onionr has begun performing a CPU-intensive operation. If this operation does not complete in the next 10 seconds, try reloading the page.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
<script src="js/timeline.js"></script>
|
||||
</body>
|
||||
</html>
|
491
onionr/static-data/www/ui/dist/js/timeline.js
vendored
491
onionr/static-data/www/ui/dist/js/timeline.js
vendored
|
@ -1,491 +0,0 @@
|
|||
/* just for testing rn */
|
||||
Block.getBlocks({'type' : 'onionr-post', 'signed' : true, 'reverse' : true}, function(data) {
|
||||
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 && block.getParent() === null) {
|
||||
post.setContent(blockContent['content']);
|
||||
post.setPostDate(block.getDate());
|
||||
post.setUser(user);
|
||||
|
||||
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 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 = '<!-- POST REPLIES -->\
|
||||
<div class="onionr-post-creator">\
|
||||
<div class="row">\
|
||||
<div class="onionr-reply-creator container">\
|
||||
<div class="row">\
|
||||
<div class="col-3">\
|
||||
<img class="onionr-post-creator-user-icon" id="onionr-reply-creator-user-icon">\
|
||||
</div>\
|
||||
<div class="col-9">\
|
||||
<div class="row">\
|
||||
<div class="col col-auto">\
|
||||
<a class="onionr-post-creator-user-name" id="onionr-reply-creator-user-name" href="#!" onclick="viewProfile(\'$user-id-url\', \'$user-name-url\')"></a>\
|
||||
<a class="onionr-post-creator-user-id" id="onionr-reply-creator-user-id" href="#!" onclick="viewProfile(\'$user-id-url\', \'$user-name-url\')" data-placement="top" data-toggle="tooltip" title="$user-id">you</a>\
|
||||
</div>\
|
||||
</div>\
|
||||
\
|
||||
<textarea class="onionr-post-creator-content" id="onionr-reply-creator-content" oninput="replyCreatorChange()"></textarea>\
|
||||
\
|
||||
<div class="onionr-post-creator-content-message" id="onionr-reply-creator-content-message"></div>\
|
||||
\
|
||||
<input type="button" onclick="makeReply()" title="Reply" value="Reply" id="onionr-reply-creator-create" class="onionr-post-creator-create" />\
|
||||
</div>\
|
||||
</div>\
|
||||
</div>\
|
||||
</div>\
|
||||
</div>\
|
||||
\
|
||||
<div class="row">\
|
||||
<div id="onionr-replies"></div>\
|
||||
</div>\
|
||||
<!-- END POST REPLIES -->\
|
||||
';
|
||||
|
||||
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('<input/>').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();
|
|
@ -1,65 +0,0 @@
|
|||
{
|
||||
"eng" : {
|
||||
"ONIONR_TITLE" : "Onionr UI",
|
||||
|
||||
"TIMELINE" : "Timeline",
|
||||
"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_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" : {
|
||||
"ONIONR_TITLE" : "Onionr UI",
|
||||
|
||||
"TIMELINE" : "Linea de Tiempo",
|
||||
"NOTIFICATIONS" : "Notificaciones",
|
||||
"MESSAGES" : "Mensaje",
|
||||
|
||||
"TRENDING" : "Trending",
|
||||
|
||||
"POST_LIKE" : "me gusta",
|
||||
"POST_REPLY" : "comentario"
|
||||
},
|
||||
|
||||
"zho" : {
|
||||
"ONIONR_TITLE" : "洋葱 用户界面",
|
||||
|
||||
"TIMELINE" : "时间线",
|
||||
"NOTIFICATIONS" : "通知",
|
||||
"MESSAGES" : "消息",
|
||||
|
||||
"TRENDING" : "趋势",
|
||||
|
||||
"POST_LIKE" : "喜欢",
|
||||
"POST_REPLY" : "回复"
|
||||
}
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
/* general formatting */
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container-small {
|
||||
width: 300px;
|
||||
}
|
||||
.container-large {
|
||||
width: 970px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.container-small {
|
||||
width: 500px;
|
||||
}
|
||||
.container-large {
|
||||
width: 1170px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.container-small {
|
||||
width: 700px;
|
||||
}
|
||||
.container-large {
|
||||
width: 1500px;
|
||||
}
|
||||
}
|
||||
|
||||
.container-small, .container-large {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* navbar */
|
||||
|
||||
body {
|
||||
margin-top: 5rem;
|
||||
}
|
||||
|
||||
/* 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%;
|
||||
}
|
||||
|
||||
.onionr-post-user-name {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.onionr-post-user-id:before { content: "("; }
|
||||
.onionr-post-user-id:after { content: ")"; }
|
||||
|
||||
.onionr-post-content {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.onionr-post-user-icon {
|
||||
border-radius: 100%;
|
||||
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;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* profile */
|
||||
|
||||
.onionr-profile-user-icon {
|
||||
border-radius: 100%;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.onionr-profile-username {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.onionr-profile-save {
|
||||
width: 100%;
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
body {
|
||||
background-color: #96928f;
|
||||
color: #25383C;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
|
||||
background-color: lightgray;
|
||||
}
|
||||
|
||||
.onionr-post-user-name {
|
||||
color: green;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.onionr-post-user-id {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.onionr-post-date {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.onionr-post-content {
|
||||
font-family: sans-serif, serif;
|
||||
border-top: 1px solid black;
|
||||
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;
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 6.6 KiB |
|
@ -1,136 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<header />
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="onionr-profile">
|
||||
<div class="row">
|
||||
<div class="col-4 col-lg-12">
|
||||
<img id="onionr-profile-user-icon" class="onionr-profile-user-icon" src="">
|
||||
</div>
|
||||
<div class="col-8 col-lg-12">
|
||||
<h2 maxlength="25" id="onionr-profile-username" class="onionr-profile-username text-left text-lg-center text-sm-left" data-placement="top" data-toggle="tooltip" title="unknown" data-editable></h2>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<p maxlength="128" id="onionr-profile-description" class="onionr-profile-description" data-editable></p>
|
||||
</div>
|
||||
|
||||
<div class="col-12 onionr-profile-edit" id="onionr-profile-edit" style="display: none">
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-lg-12">
|
||||
<input type="button" onclick="updateUser()" class="onionr-profile-save text-center" id="onionr-profile-save" value="<$= LANG.PROFILE_EDIT_SAVE $>" />
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-12">
|
||||
<input type="button" onclick="cancelUpdate()" class="onionr-profile-save text-center" id="onionr-profile-cancel" value="<$= LANG.PROFILE_EDIT_CANCEL $>" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-divider pb-3 d-block d-lg-none"></div>
|
||||
|
||||
<div class="col-sm-12 col-lg-6">
|
||||
<div class="row" id="onionr-timeline-post-creator">
|
||||
<div class="col-12">
|
||||
<div class="onionr-timeline">
|
||||
<h2><$= LANG.TIMELINE $></h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- POST CREATOR -->
|
||||
<div class="col-12">
|
||||
<div class="onionr-post-creator">
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<img class="onionr-post-creator-user-icon" id="onionr-post-creator-user-icon">
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<div class="row">
|
||||
<div class="col col-auto">
|
||||
<a class="onionr-post-creator-user-name" id="onionr-post-creator-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')"></a>
|
||||
<a class="onionr-post-creator-user-id" id="onionr-post-creator-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url')" data-placement="top" data-toggle="tooltip" title="$user-id"><$= LANG.POST_CREATOR_YOU $></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea class="onionr-post-creator-content" id="onionr-post-creator-content" oninput="postCreatorChange()"></textarea>
|
||||
|
||||
<div class="onionr-post-creator-content-message" id="onionr-post-creator-content-message"></div>
|
||||
|
||||
<input type="button" onclick="makePost()" title="<$= LANG.POST_CREATOR_CREATE $>" value="<$= LANG.POST_CREATOR_CREATE $>" id="onionr-post-creator-create" class="onionr-post-creator-create" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END POST CREATOR -->
|
||||
</div>
|
||||
|
||||
<div class="row" id="onionr-timeline-posts">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-none d-lg-block col-lg-3">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="onionr-replies">
|
||||
<h2 id="onionr-replies-title"></h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="onionr-reply-creator-panel">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- POST FOCUS DIALOG -->
|
||||
<div class="modal fade" id="onionr-post-focus" tabindex="-1" role="dialog" aria-labelledby="modal-title" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="row p-3">
|
||||
<div class="col-2">
|
||||
<img src="" id="onionr-post-focus-user-icon" class="onionr-post-user-icon">
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<div class="row">
|
||||
<div class="col col-auto">
|
||||
<a class="onionr-post-user-name" id="onionr-post-focus-user-name" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url'); jQuery('#onionr-post-focus').modal('hide');">$user-name</a>
|
||||
<a class="onionr-post-user-id" id="onionr-post-focus-user-id" href="#!" onclick="viewProfile('$user-id-url', '$user-name-url'); jQuery('#onionr-post-focus').modal('hide');" data-placement="top" data-toggle="tooltip" title="$user-id">$user-id-truncated</a>
|
||||
</div>
|
||||
|
||||
<div class="col col-auto text-right ml-auto pl-0">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="onionr-post-content" id="onionr-post-focus-content">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="col-12 onionr-post-focus-separator" />
|
||||
|
||||
<$= htmlTemplate('onionr-timeline-reply-creator') $>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END POST FOCUS DIALOG -->
|
||||
|
||||
<footer />
|
||||
<script src="js/timeline.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,689 +0,0 @@
|
|||
|
||||
/* handy localstorage functions for quick usage */
|
||||
|
||||
function set(key, val) {
|
||||
return localStorage.setItem(key, val);
|
||||
}
|
||||
|
||||
function get(key, df) { // df is default
|
||||
value = localStorage.getItem(key);
|
||||
if(value == null)
|
||||
value = df;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
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" */
|
||||
function timeSince(date, size) {
|
||||
// taken from https://stackoverflow.com/a/3177838/3678023
|
||||
|
||||
var seconds = Math.floor((new Date() - date) / 1000);
|
||||
var interval = Math.floor(seconds / 31536000);
|
||||
|
||||
if (size === null)
|
||||
size = 'desktop';
|
||||
|
||||
var dates = {
|
||||
'mobile' : {
|
||||
'yr' : 'yrs',
|
||||
'mo' : 'mo',
|
||||
'd' : 'd',
|
||||
'hr' : 'h',
|
||||
'min' : 'm',
|
||||
'secs' : 's',
|
||||
'sec' : 's',
|
||||
},
|
||||
|
||||
'desktop' : {
|
||||
'yr' : ' years',
|
||||
'mo' : ' months',
|
||||
'd' : ' days',
|
||||
'hr' : ' hours',
|
||||
'min' : ' minutes',
|
||||
'secs' : ' seconds',
|
||||
'sec' : ' second',
|
||||
},
|
||||
};
|
||||
|
||||
if (interval > 1)
|
||||
return interval + dates[size]['yr'];
|
||||
interval = Math.floor(seconds / 2592000);
|
||||
|
||||
if (interval > 1)
|
||||
return interval + dates[size]['mo'];
|
||||
interval = Math.floor(seconds / 86400);
|
||||
|
||||
if (interval > 1)
|
||||
return interval + dates[size]['d'];
|
||||
interval = Math.floor(seconds / 3600);
|
||||
|
||||
if (interval > 1)
|
||||
return interval + dates[size]['hr'];
|
||||
interval = Math.floor(seconds / 60);
|
||||
|
||||
if (interval > 1)
|
||||
return interval + dates[size]['min'];
|
||||
|
||||
if(Math.floor(seconds) !== 1)
|
||||
return Math.floor(seconds) + dates[size]['secs'];
|
||||
|
||||
return '1' + dates[size]['sec'];
|
||||
}
|
||||
|
||||
/* replace all instances of string */
|
||||
String.prototype.replaceAll = function(search, replacement, limit) {
|
||||
// taken from https://stackoverflow.com/a/17606289/3678023
|
||||
var target = this;
|
||||
return target.split(search, limit).join(replacement);
|
||||
};
|
||||
|
||||
/* useful functions to sanitize data */
|
||||
class Sanitize {
|
||||
/* sanitizes HTML in a string */
|
||||
static html(html) {
|
||||
return String(html).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
/* URL encodes a string */
|
||||
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 */
|
||||
function getWebPassword() {
|
||||
return get("web-password", null);
|
||||
}
|
||||
|
||||
function setWebPassword(password) {
|
||||
return set("web-password", password);
|
||||
}
|
||||
|
||||
function getTimingToken() {
|
||||
return get("timing-token", null);
|
||||
}
|
||||
|
||||
function setTimingToken(token) {
|
||||
return set("timing-token", token);
|
||||
}
|
||||
|
||||
/* user class */
|
||||
class User {
|
||||
constructor() {
|
||||
this.name = 'Unknown';
|
||||
this.id = 'unknown';
|
||||
this.image = 'img/default.png';
|
||||
}
|
||||
|
||||
setName(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
setID(id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
getID() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
setIcon(image) {
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
getIcon() {
|
||||
return this.image;
|
||||
}
|
||||
|
||||
setDescription(description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return {
|
||||
'name' : this.getName(),
|
||||
'id' : this.getID(),
|
||||
'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(type) {
|
||||
var replyTemplate = '<$= jsTemplate('onionr-timeline-reply') $>';
|
||||
var postTemplate = '<$= jsTemplate('onionr-timeline-post') $>';
|
||||
|
||||
var template = '';
|
||||
|
||||
if(type !== undefined && type !== null && type == 'reply')
|
||||
template = replyTemplate;
|
||||
else
|
||||
template = postTemplate;
|
||||
|
||||
var device = (jQuery(document).width() < 768 ? 'mobile' : 'desktop');
|
||||
|
||||
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())));
|
||||
|
||||
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('-')));
|
||||
|
||||
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', '<br />', 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());
|
||||
|
||||
if(this.getHash() in getPostMap() && getPostMap()[this.getHash()]['liked']) {
|
||||
template = template.replaceAll('$liked', '<$= LANG.POST_UNLIKE $>');
|
||||
} else {
|
||||
template = template.replaceAll('$liked', '<$= LANG.POST_LIKE $>');
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
setUser(user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
getUser() {
|
||||
return this.user;
|
||||
}
|
||||
|
||||
setContent(content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
getContent() {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
setParent(parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
getParent() {
|
||||
return this.parent;
|
||||
}
|
||||
|
||||
setPostDate(date) { // unix timestamp input
|
||||
if(date instanceof Date)
|
||||
this.date = date;
|
||||
else
|
||||
this.date = new Date(date * 1000);
|
||||
}
|
||||
|
||||
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 */
|
||||
class Block {
|
||||
constructor(type, content) {
|
||||
this.type = type;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
// returns the block hash, if any
|
||||
getHash() {
|
||||
return this.hash;
|
||||
}
|
||||
|
||||
// returns the block type
|
||||
getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
// returns the block header
|
||||
getHeader(key, df) { // df is default
|
||||
if(key !== undefined) {
|
||||
if(this.getHeader().hasOwnProperty(key))
|
||||
return this.getHeader()[key];
|
||||
else
|
||||
return (df === undefined ? null : df);
|
||||
} else
|
||||
return this.header;
|
||||
}
|
||||
|
||||
// returns the block metadata
|
||||
getMetadata(key, df) { // df is default
|
||||
if(key !== undefined) {
|
||||
if(this.getMetadata().hasOwnProperty(key))
|
||||
return this.getMetadata()[key];
|
||||
else
|
||||
return (df === undefined ? null : df);
|
||||
} else
|
||||
return this.metadata;
|
||||
}
|
||||
|
||||
// returns the block content
|
||||
getContent() {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
// returns the parent block's hash (not Block object, for performance)
|
||||
getParent() {
|
||||
// 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;
|
||||
}
|
||||
|
||||
// returns the date that the block was received
|
||||
getDate() {
|
||||
return this.date;
|
||||
}
|
||||
|
||||
// returns a boolean that indicates whether or not the block is valid
|
||||
isValid() {
|
||||
return this.valid;
|
||||
}
|
||||
|
||||
// returns a boolean thati ndicates whether or not the block is signed
|
||||
isSigned() {
|
||||
return this.signed;
|
||||
}
|
||||
|
||||
// returns the block signature
|
||||
getSignature() {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
// returns the block type
|
||||
setType(type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
// sets block metadata by key
|
||||
setMetadata(key, val) {
|
||||
this.metadata[key] = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
// sets block content
|
||||
setContent(content) {
|
||||
this.content = content;
|
||||
return this;
|
||||
}
|
||||
|
||||
// sets the block parent by hash or Block object
|
||||
setParent(parent) {
|
||||
this.parent = parent;
|
||||
return this;
|
||||
}
|
||||
|
||||
// indicates if the Block exists or not
|
||||
exists() {
|
||||
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 Block.parseBlock(hash);
|
||||
}
|
||||
|
||||
// converts an associative array to a Block
|
||||
static parseBlock(val) {
|
||||
var block = new Block();
|
||||
|
||||
block.type = val['type'];
|
||||
block.content = val['content'];
|
||||
block.header = val['header'];
|
||||
block.metadata = val['metadata'];
|
||||
block.date = new Date(val['date'] * 1000);
|
||||
block.hash = val['hash'];
|
||||
block.signature = val['signature'];
|
||||
block.signed = val['signed'];
|
||||
block.valid = val['valid'];
|
||||
block.parent = val['parent'];
|
||||
|
||||
if(block.getParent() !== null) {
|
||||
// if the block data is already in the associative array
|
||||
|
||||
/*
|
||||
if (blocks.hasOwnProperty(block.getParent()))
|
||||
block.setParent(Block.parseAssociativeArray({blocks[block.getParent()]})[0]);
|
||||
*/
|
||||
}
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
// converts an array of associative arrays to an array of Blocks
|
||||
static parseBlockArray(blocks) {
|
||||
var outputBlocks = [];
|
||||
|
||||
for(var key in blocks) {
|
||||
if(blocks.hasOwnProperty(key)) {
|
||||
var val = blocks[key];
|
||||
|
||||
var block = Block.parseBlock(val);
|
||||
|
||||
outputBlocks.push(block);
|
||||
}
|
||||
}
|
||||
|
||||
return outputBlocks;
|
||||
}
|
||||
|
||||
static getBlocks(args, callback) { // callback is optional
|
||||
args = args || {}
|
||||
|
||||
var url = '/client/?action=searchBlocks&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)['blocks']));
|
||||
}, 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)['blocks']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 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 --details`)");
|
||||
}
|
||||
|
||||
setWebPassword(password);
|
||||
}
|
||||
|
||||
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('<$= Template.jsTemplate("default-icon") $>');
|
||||
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();
|
|
@ -1,460 +0,0 @@
|
|||
/* just for testing rn */
|
||||
Block.getBlocks({'type' : 'onionr-post', 'signed' : true, 'reverse' : true}, function(data) {
|
||||
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 && block.getParent() === null) {
|
||||
post.setContent(blockContent['content']);
|
||||
post.setPostDate(block.getDate());
|
||||
post.setUser(user);
|
||||
|
||||
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 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 = '<$= LANG.POST_CREATOR_MESSAGE_MAXIMUM_NEWLINES $>';
|
||||
} else if(content.length <= maxlength) {
|
||||
// 280 max characters
|
||||
message = '<$= LANG.POST_CREATOR_MESSAGE_REMAINING $>'.replaceAll('%s', (280 - content.length));
|
||||
disable = false;
|
||||
|
||||
if(maxlength - content.length < maxlength / 4) {
|
||||
warn = true;
|
||||
}
|
||||
} else {
|
||||
message = '<$= LANG.POST_CREATOR_MESSAGE_OVER $>'.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 = '<$= LANG.POST_CREATOR_MESSAGE_MAXIMUM_NEWLINES $>';
|
||||
} else if(content.length <= maxlength) {
|
||||
// 280 max characters
|
||||
message = '<$= LANG.POST_CREATOR_MESSAGE_REMAINING $>'.replaceAll('%s', (280 - content.length));
|
||||
disable = false;
|
||||
|
||||
if(maxlength - content.length < maxlength / 4) {
|
||||
warn = true;
|
||||
}
|
||||
} else {
|
||||
message = '<$= LANG.POST_CREATOR_MESSAGE_OVER $>'.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 = '<$= LANG.POST_CREATOR_MESSAGE_MAXIMUM_NEWLINES $>';
|
||||
} else if(content.length <= maxlength) {
|
||||
// 280 max characters
|
||||
message = '<$= LANG.POST_CREATOR_MESSAGE_REMAINING $>'.replaceAll('%s', (280 - content.length));
|
||||
disable = false;
|
||||
|
||||
if(maxlength - content.length < maxlength / 4) {
|
||||
warn = true;
|
||||
}
|
||||
} else {
|
||||
message = '<$= LANG.POST_CREATOR_MESSAGE_OVER $>'.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 = '<$= LANG.REPLIES $>';
|
||||
document.getElementById('onionr-reply-creator-panel').originalPost = id;
|
||||
document.getElementById('onionr-reply-creator-panel').innerHTML = '<$= jsTemplate('onionr-reply-creator') $>';
|
||||
|
||||
document.getElementById('onionr-reply-creator-content').innerHTML = '';
|
||||
document.getElementById("onionr-reply-creator-content").placeholder = "<$= LANG.POST_CREATOR_PLACEHOLDER $>";
|
||||
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('<input/>').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 = "<$= LANG.POST_CREATOR_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 = "<$= LANG.POST_CREATOR_PLACEHOLDER $>";
|
||||
document.getElementById("onionr-post-focus-reply-creator-content").placeholder = "<$= LANG.POST_CREATOR_PLACEHOLDER $>";
|
||||
|
||||
document.getElementById("onionr-post-focus-reply-creator-user-id").innerHTML = "<$= LANG.POST_CREATOR_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();
|
8
onionr/storagecounter.py
Normal file → Executable file
8
onionr/storagecounter.py
Normal file → Executable file
|
@ -18,7 +18,7 @@
|
|||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
import config
|
||||
|
||||
config.reload()
|
||||
class StorageCounter:
|
||||
def __init__(self, coreInst):
|
||||
self._core = coreInst
|
||||
|
@ -27,7 +27,7 @@ class StorageCounter:
|
|||
|
||||
def isFull(self):
|
||||
retData = False
|
||||
if self._core.config.get('allocations.disk') <= (self.getAmount() + 1000):
|
||||
if self._core.config.get('allocations.disk', 2000000000) <= (self.getAmount() + 1000):
|
||||
retData = True
|
||||
return retData
|
||||
|
||||
|
@ -49,13 +49,13 @@ class StorageCounter:
|
|||
def getPercent(self):
|
||||
'''Return percent (decimal/float) of disk space we're using'''
|
||||
amount = self.getAmount()
|
||||
return round(amount / self._core.config.get('allocations.disk'), 2)
|
||||
return round(amount / self._core.config.get('allocations.disk', 2000000000), 2)
|
||||
|
||||
def addBytes(self, amount):
|
||||
'''Record that we are now using more disk space, unless doing so would exceed configured max'''
|
||||
newAmount = amount + self.getAmount()
|
||||
retData = newAmount
|
||||
if newAmount > self._core.config.get('allocations.disk'):
|
||||
if newAmount > self._core.config.get('allocations.disk', 2000000000):
|
||||
retData = False
|
||||
else:
|
||||
self._update(newAmount)
|
||||
|
|
243
onionr/tests.py
243
onionr/tests.py
|
@ -1,243 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
'''
|
||||
Onionr - P2P Microblogging Platform & Social network
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
import unittest, sys, os, base64, tarfile, shutil, logger
|
||||
|
||||
class OnionrTests(unittest.TestCase):
|
||||
def testPython3(self):
|
||||
if sys.version_info.major != 3:
|
||||
logger.debug('Python version: ' + sys.version_info.major)
|
||||
self.assertTrue(False)
|
||||
else:
|
||||
self.assertTrue(True)
|
||||
|
||||
def testNone(self):
|
||||
logger.debug('-'*26 + '\n')
|
||||
logger.info('Running simple program run test...')
|
||||
|
||||
blank = os.system('./onionr.py --version')
|
||||
if blank != 0:
|
||||
self.assertTrue(False)
|
||||
else:
|
||||
self.assertTrue(True)
|
||||
|
||||
def testPeer_a_DBCreation(self):
|
||||
logger.debug('-'*26 + '\n')
|
||||
logger.info('Running peer db creation test...')
|
||||
|
||||
if os.path.exists('data/peers.db'):
|
||||
os.remove('data/peers.db')
|
||||
import core
|
||||
myCore = core.Core()
|
||||
myCore.createPeerDB()
|
||||
if os.path.exists('data/peers.db'):
|
||||
self.assertTrue(True)
|
||||
else:
|
||||
self.assertTrue(False)
|
||||
|
||||
def testPeer_b_addPeerToDB(self):
|
||||
logger.debug('-'*26 + '\n')
|
||||
logger.info('Running peer db insertion test...')
|
||||
|
||||
import core
|
||||
myCore = core.Core()
|
||||
if not os.path.exists('data/peers.db'):
|
||||
myCore.createPeerDB()
|
||||
if myCore.addPeer('6M5MXL237OK57ITHVYN5WGHANPGOMKS5C3PJLHBBNKFFJQOIDOJA====', '1cSix9Ao/yQSdo0sNif8cm2uTcYnSphb4JdZL/3WkN4=') and not myCore.addPeer('NFXHMYLMNFSAU===', '1cSix9Ao/yQSdo0sNif8cm2uTcYnSphb4JdZL/3WkN4='):
|
||||
self.assertTrue(True)
|
||||
else:
|
||||
self.assertTrue(False)
|
||||
|
||||
def testConfig(self):
|
||||
logger.debug('-'*26 + '\n')
|
||||
logger.info('Running simple configuration test...')
|
||||
|
||||
import config
|
||||
|
||||
config.check()
|
||||
config.reload()
|
||||
configdata = str(config.get_config())
|
||||
|
||||
config.set('testval', 1337)
|
||||
if not config.get('testval', None) is 1337:
|
||||
self.assertTrue(False)
|
||||
|
||||
config.set('testval')
|
||||
if not config.get('testval', None) is None:
|
||||
self.assertTrue(False)
|
||||
|
||||
config.save()
|
||||
config.reload()
|
||||
|
||||
if not str(config.get_config()) == configdata:
|
||||
self.assertTrue(False)
|
||||
|
||||
self.assertTrue(True)
|
||||
'''
|
||||
def testBlockAPI(self):
|
||||
logger.debug('-'*26 + '\n')
|
||||
logger.info('Running BlockAPI test #1...')
|
||||
|
||||
content = 'Onionr test block'
|
||||
|
||||
from onionrblockapi import Block
|
||||
hash = Block(type = 'test', content = content).save()
|
||||
block = Block(hash) # test init
|
||||
|
||||
if len(Block.getBlocks(type = 'test')) == 0:
|
||||
logger.warn('Failed to find test block.')
|
||||
self.assertTrue(False)
|
||||
if not block.getContent() == content:
|
||||
logger.warn('Test block content is invalid! (%s != %s)' % (block.getContent(), content))
|
||||
self.assertTrue(False)
|
||||
|
||||
logger.debug('-'*26 + '\n')
|
||||
logger.info('Running BlockAPI test #2...')
|
||||
|
||||
original_content = 'onionr'
|
||||
|
||||
logger.debug('original: %s' % original_content)
|
||||
|
||||
blocks = Block.createChain(data = original_content, chunksize = 2, verbose = True)
|
||||
|
||||
logger.debug(blocks[1])
|
||||
|
||||
child = blocks[0]
|
||||
merged = Block.mergeChain(child)
|
||||
|
||||
logger.debug('merged blocks (child: %s): %s' % (child, merged))
|
||||
|
||||
if merged != original_content:
|
||||
self.assertTrue(False)
|
||||
self.assertTrue(True)
|
||||
|
||||
def testPluginReload(self):
|
||||
logger.debug('-'*26 + '\n')
|
||||
logger.info('Running simple plugin reload test...')
|
||||
|
||||
import onionrplugins, os
|
||||
|
||||
if not onionrplugins.exists('test'):
|
||||
os.makedirs(onionrplugins.get_plugins_folder('test'))
|
||||
with open(onionrplugins.get_plugins_folder('test') + '/main.py', 'a') as main:
|
||||
main.write("print('Running')\n\ndef on_test(pluginapi, data = None):\n print('received test event!')\n return True\n\ndef on_start(pluginapi, data = None):\n print('start event called')\n\ndef on_stop(pluginapi, data = None):\n print('stop event called')\n\ndef on_enable(pluginapi, data = None):\n print('enable event called')\n\ndef on_disable(pluginapi, data = None):\n print('disable event called')\n")
|
||||
onionrplugins.enable('test')
|
||||
|
||||
try:
|
||||
onionrplugins.reload('test')
|
||||
self.assertTrue(True)
|
||||
except:
|
||||
self.assertTrue(False)
|
||||
|
||||
def testPluginStopStart(self):
|
||||
logger.debug('-'*26 + '\n')
|
||||
logger.info('Running simple plugin restart test...')
|
||||
|
||||
import onionrplugins, os
|
||||
|
||||
if not onionrplugins.exists('test'):
|
||||
os.makedirs(onionrplugins.get_plugins_folder('test'))
|
||||
with open(onionrplugins.get_plugins_folder('test') + '/main.py', 'a') as main:
|
||||
main.write("print('Running')\n\ndef on_test(pluginapi, data = None):\n print('received test event!')\n return True\n\ndef on_start(pluginapi, data = None):\n print('start event called')\n\ndef on_stop(pluginapi, data = None):\n print('stop event called')\n\ndef on_enable(pluginapi, data = None):\n print('enable event called')\n\ndef on_disable(pluginapi, data = None):\n print('disable event called')\n")
|
||||
onionrplugins.enable('test')
|
||||
|
||||
try:
|
||||
onionrplugins.start('test')
|
||||
onionrplugins.stop('test')
|
||||
self.assertTrue(True)
|
||||
except:
|
||||
self.assertTrue(False)
|
||||
|
||||
def testPluginEvent(self):
|
||||
logger.debug('-'*26 + '\n')
|
||||
logger.info('Running plugin event test...')
|
||||
|
||||
import onionrplugins as plugins, onionrevents as events, os
|
||||
|
||||
if not plugins.exists('test'):
|
||||
os.makedirs(plugins.get_plugins_folder('test'))
|
||||
with open(plugins.get_plugins_folder('test') + '/main.py', 'a') as main:
|
||||
main.write("print('Running')\n\ndef on_test(pluginapi, data = None):\n print('received test event!')\n print('thread test started...')\n import time\n time.sleep(1)\n \n return True\n\ndef on_start(pluginapi, data = None):\n print('start event called')\n\ndef on_stop(pluginapi, data = None):\n print('stop event called')\n\ndef on_enable(pluginapi, data = None):\n print('enable event called')\n\ndef on_disable(pluginapi, data = None):\n print('disable event called')\n")
|
||||
plugins.enable('test')
|
||||
|
||||
|
||||
plugins.start('test')
|
||||
if not events.call(plugins.get_plugin('test'), 'enable'):
|
||||
self.assertTrue(False)
|
||||
|
||||
logger.debug('preparing to start thread', timestamp = False)
|
||||
thread = events.event('test', data = {'tests': self})
|
||||
logger.debug('thread running...', timestamp = False)
|
||||
thread.join()
|
||||
logger.debug('thread finished.', timestamp = False)
|
||||
|
||||
self.assertTrue(True)
|
||||
'''
|
||||
def testQueue(self):
|
||||
logger.debug('-'*26 + '\n')
|
||||
logger.info('Running daemon queue test...')
|
||||
|
||||
# test if the daemon queue can read/write data
|
||||
import core
|
||||
myCore = core.Core()
|
||||
if not os.path.exists('data/queue.db'):
|
||||
myCore.daemonQueue()
|
||||
while True:
|
||||
command = myCore.daemonQueue()
|
||||
if command == False:
|
||||
logger.debug('The queue is empty (false)')
|
||||
break
|
||||
else:
|
||||
logger.debug(command[0])
|
||||
myCore.daemonQueueAdd('testCommand', 'testData')
|
||||
command = myCore.daemonQueue()
|
||||
if command[0] == 'testCommand':
|
||||
if myCore.daemonQueue() == False:
|
||||
logger.info('Succesfully added and read command')
|
||||
|
||||
def testHashValidation(self):
|
||||
logger.debug('-'*26 + '\n')
|
||||
logger.info('Running hash validation test...')
|
||||
|
||||
import core
|
||||
myCore = core.Core()
|
||||
if not myCore._utils.validateHash("$324dfgfdg") and myCore._utils.validateHash("f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2") and not myCore._utils.validateHash("f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd$"):
|
||||
self.assertTrue(True)
|
||||
else:
|
||||
self.assertTrue(False)
|
||||
|
||||
def testAddAdder(self):
|
||||
logger.debug('-'*26 + '\n')
|
||||
logger.info('Running address add+remove test')
|
||||
import core
|
||||
myCore = core.Core()
|
||||
if not os.path.exists('data/address.db'):
|
||||
myCore.createAddressDB()
|
||||
if myCore.addAddress('facebookcorewwwi.onion') and not myCore.removeAddress('invalid'):
|
||||
if myCore.removeAddress('facebookcorewwwi.onion'):
|
||||
self.assertTrue(True)
|
||||
else:
|
||||
self.assertTrue(False)
|
||||
else:
|
||||
self.assertTrue(False) # <- annoying :(
|
||||
def testCrypto(self):
|
||||
logger.info('running cryptotests')
|
||||
if os.system('python3 cryptotests.py') == 0:
|
||||
self.assertTrue(True)
|
||||
else:
|
||||
self.assertTrue(False)
|
||||
|
||||
unittest.main()
|
19
onionr/tests/test_blocks.py
Normal file
19
onionr/tests/test_blocks.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env python3
|
||||
import sys, os
|
||||
sys.path.append(".")
|
||||
import unittest, uuid, hashlib
|
||||
import nacl.exceptions
|
||||
import nacl.signing, nacl.hash, nacl.encoding
|
||||
TEST_DIR = 'testdata/%s-%s' % (uuid.uuid4(), os.path.basename(__file__)) + '/'
|
||||
print("Test directory:", TEST_DIR)
|
||||
os.environ["ONIONR_HOME"] = TEST_DIR
|
||||
import core, onionr
|
||||
|
||||
c = core.Core()
|
||||
|
||||
class OnionrBlockTests(unittest.TestCase):
|
||||
def test_plaintext_insert(self):
|
||||
message = 'hello world'
|
||||
c.insertBlock(message)
|
||||
|
||||
unittest.main()
|
70
onionr/tests/test_database_creation.py
Normal file
70
onionr/tests/test_database_creation.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
#!/usr/bin/env python3
|
||||
import sys, os
|
||||
sys.path.append(".")
|
||||
import unittest, uuid, sqlite3
|
||||
TEST_DIR = 'testdata/%s-%s' % (uuid.uuid4(), os.path.basename(__file__)) + '/'
|
||||
print("Test directory:", TEST_DIR)
|
||||
os.environ["ONIONR_HOME"] = TEST_DIR
|
||||
from urllib.request import pathname2url
|
||||
import core, onionr
|
||||
|
||||
core.Core()
|
||||
|
||||
class OnionrTests(unittest.TestCase):
|
||||
|
||||
def test_peer_db_creation(self):
|
||||
try:
|
||||
dburi = 'file:{}?mode=rw'.format(pathname2url(TEST_DIR + 'peers.db'))
|
||||
conn = sqlite3.connect(dburi, uri=True, timeout=30)
|
||||
cursor = conn.cursor()
|
||||
conn.close()
|
||||
except sqlite3.OperationalError:
|
||||
self.assertTrue(False)
|
||||
else:
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_block_db_creation(self):
|
||||
try:
|
||||
dburi = 'file:{}?mode=rw'.format(pathname2url(TEST_DIR + 'blocks.db'))
|
||||
conn = sqlite3.connect(dburi, uri=True, timeout=30)
|
||||
cursor = conn.cursor()
|
||||
conn.close()
|
||||
except sqlite3.OperationalError:
|
||||
self.assertTrue(False)
|
||||
else:
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_forward_keys_db_creation(self):
|
||||
try:
|
||||
dburi = 'file:{}?mode=rw'.format(pathname2url(TEST_DIR + 'forward-keys.db'))
|
||||
conn = sqlite3.connect(dburi, uri=True, timeout=30)
|
||||
cursor = conn.cursor()
|
||||
conn.close()
|
||||
except sqlite3.OperationalError:
|
||||
self.assertTrue(False)
|
||||
else:
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_address_db_creation(self):
|
||||
try:
|
||||
dburi = 'file:{}?mode=rw'.format(pathname2url(TEST_DIR + 'address.db'))
|
||||
conn = sqlite3.connect(dburi, uri=True, timeout=30)
|
||||
cursor = conn.cursor()
|
||||
conn.close()
|
||||
except sqlite3.OperationalError:
|
||||
self.assertTrue(False)
|
||||
else:
|
||||
self.assertTrue(True)
|
||||
|
||||
def blacklist_db_creation(self):
|
||||
try:
|
||||
dburi = 'file:{}?mode=rw'.format(pathname2url(TEST_DIR + 'blacklist.db'))
|
||||
conn = sqlite3.connect(dburi, uri=True, timeout=30)
|
||||
cursor = conn.cursor()
|
||||
conn.close()
|
||||
except sqlite3.OperationalError:
|
||||
self.assertTrue(False)
|
||||
else:
|
||||
self.assertTrue(True)
|
||||
|
||||
unittest.main()
|
130
onionr/tests/test_highlevelcrypto.py
Normal file
130
onionr/tests/test_highlevelcrypto.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
#!/usr/bin/env python3
|
||||
import sys, os
|
||||
sys.path.append(".")
|
||||
import unittest, uuid, hashlib
|
||||
import nacl.exceptions
|
||||
import nacl.signing, nacl.hash, nacl.encoding
|
||||
TEST_DIR = 'testdata/%s-%s' % (uuid.uuid4(), os.path.basename(__file__)) + '/'
|
||||
print("Test directory:", TEST_DIR)
|
||||
os.environ["ONIONR_HOME"] = TEST_DIR
|
||||
import core, onionr
|
||||
|
||||
c = core.Core()
|
||||
crypto = c._crypto
|
||||
class OnionrCryptoTests(unittest.TestCase):
|
||||
|
||||
def test_blake2b(self):
|
||||
self.assertTrue(crypto.blake2bHash('test') == crypto.blake2bHash(b'test'))
|
||||
self.assertTrue(crypto.blake2bHash(b'test') == crypto.blake2bHash(b'test'))
|
||||
|
||||
self.assertFalse(crypto.blake2bHash('') == crypto.blake2bHash(b'test'))
|
||||
try:
|
||||
crypto.blake2bHash(None)
|
||||
except nacl.exceptions.TypeError:
|
||||
pass
|
||||
else:
|
||||
self.assertTrue(False)
|
||||
|
||||
self.assertTrue(nacl.hash.blake2b(b'test') == crypto.blake2bHash(b'test'))
|
||||
|
||||
def test_sha3256(self):
|
||||
hasher = hashlib.sha3_256()
|
||||
self.assertTrue(crypto.sha3Hash('test') == crypto.sha3Hash(b'test'))
|
||||
self.assertTrue(crypto.sha3Hash(b'test') == crypto.sha3Hash(b'test'))
|
||||
|
||||
self.assertFalse(crypto.sha3Hash('') == crypto.sha3Hash(b'test'))
|
||||
try:
|
||||
crypto.sha3Hash(None)
|
||||
except TypeError:
|
||||
pass
|
||||
else:
|
||||
self.assertTrue(False)
|
||||
|
||||
hasher.update(b'test')
|
||||
normal = hasher.hexdigest()
|
||||
self.assertTrue(crypto.sha3Hash(b'test') == normal)
|
||||
|
||||
def valid_default_id(self):
|
||||
self.assertTrue(c._utils.validatePubKey(crypto.pubKey))
|
||||
|
||||
def test_human_readable_length(self):
|
||||
human = c._utils.getHumanReadableID()
|
||||
self.assertTrue(len(human.split(' ')) == 32)
|
||||
|
||||
def test_human_readable_rebuild(self):
|
||||
return # Broken right now
|
||||
# Test if we can get the human readable id, and convert it back to valid base32 key
|
||||
human = c._utils.getHumanReadableID()
|
||||
unHuman = c._utils.convertHumanReadableID(human)
|
||||
nacl.signing.VerifyKey(c._utils.convertHumanReadableID(human), encoder=nacl.encoding.Base32Encoder)
|
||||
|
||||
def test_safe_compare(self):
|
||||
self.assertTrue(crypto.safeCompare('test', 'test'))
|
||||
self.assertTrue(crypto.safeCompare('test', b'test'))
|
||||
self.assertFalse(crypto.safeCompare('test', 'test2'))
|
||||
try:
|
||||
crypto.safeCompare('test', None)
|
||||
except TypeError:
|
||||
pass
|
||||
else:
|
||||
self.assertTrue(False)
|
||||
|
||||
def test_random_shuffle(self):
|
||||
# Small chance that the randomized list will be same. Rerun test a couple times if it fails
|
||||
startList = ['cat', 'dog', 'moose', 'rabbit', 'monkey', 'crab', 'human', 'dolphin', 'whale', 'etc'] * 10
|
||||
|
||||
self.assertFalse(startList == list(crypto.randomShuffle(startList)))
|
||||
self.assertTrue(len(startList) == len(startList))
|
||||
|
||||
def test_asymmetric(self):
|
||||
keyPair = crypto.generatePubKey()
|
||||
keyPair2 = crypto.generatePubKey()
|
||||
message = "hello world"
|
||||
|
||||
self.assertTrue(len(crypto.pubKeyEncrypt(message, keyPair2[0], encodedData=True)) > 0)
|
||||
encrypted = crypto.pubKeyEncrypt(message, keyPair2[0], encodedData=False)
|
||||
decrypted = crypto.pubKeyDecrypt(encrypted, privkey=keyPair2[1], encodedData=False)
|
||||
|
||||
self.assertTrue(decrypted.decode() == message)
|
||||
try:
|
||||
crypto.pubKeyEncrypt(None, keyPair2[0])
|
||||
except TypeError:
|
||||
pass
|
||||
else:
|
||||
self.assertTrue(False)
|
||||
|
||||
blankMessage = crypto.pubKeyEncrypt('', keyPair2[0])
|
||||
self.assertTrue('' == crypto.pubKeyDecrypt(blankMessage, privkey=keyPair2[1], encodedData=False).decode())
|
||||
# Try to encrypt arbitrary bytes
|
||||
crypto.pubKeyEncrypt(os.urandom(32), keyPair2[0])
|
||||
|
||||
def test_symmetric(self):
|
||||
dataString = "this is a secret message"
|
||||
dataBytes = dataString.encode()
|
||||
key = b"tttttttttttttttttttttttttttttttt"
|
||||
invalidKey = b'tttttttttttttttttttttttttttttttb'
|
||||
encrypted = crypto.symmetricEncrypt(dataString, key, returnEncoded=True)
|
||||
decrypted = crypto.symmetricDecrypt(encrypted, key, encodedMessage=True)
|
||||
self.assertTrue(dataString == decrypted.decode())
|
||||
|
||||
try:
|
||||
crypto.symmetricDecrypt(encrypted, invalidKey, encodedMessage=True)
|
||||
except nacl.exceptions.CryptoError:
|
||||
pass
|
||||
else:
|
||||
self.assertFalse(True)
|
||||
try:
|
||||
crypto.symmetricEncrypt(None, key, returnEncoded=True)
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self.assertFalse(True)
|
||||
crypto.symmetricEncrypt("string", key, returnEncoded=True)
|
||||
try:
|
||||
crypto.symmetricEncrypt("string", None, returnEncoded=True)
|
||||
except nacl.exceptions.TypeError:
|
||||
pass
|
||||
else:
|
||||
self.assertFalse(True)
|
||||
|
||||
unittest.main()
|
84
onionr/tests/test_onionrusers.py
Normal file
84
onionr/tests/test_onionrusers.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
#!/usr/bin/env python3
|
||||
import sys, os
|
||||
sys.path.append(".")
|
||||
import unittest, uuid, hashlib
|
||||
import json
|
||||
TEST_DIR = 'testdata/%s-%s' % (uuid.uuid4(), os.path.basename(__file__)) + '/'
|
||||
print("Test directory:", TEST_DIR)
|
||||
os.environ["ONIONR_HOME"] = TEST_DIR
|
||||
import core, onionr
|
||||
|
||||
c = core.Core()
|
||||
import onionrexceptions
|
||||
from onionrusers import onionrusers
|
||||
from onionrusers import contactmanager
|
||||
|
||||
class OnionrUserTests(unittest.TestCase):
|
||||
'''
|
||||
Tests both the onionrusers class and the contactmanager (which inherits it)
|
||||
'''
|
||||
|
||||
def test_users(self):
|
||||
keypair = c._crypto.generatePubKey()
|
||||
onionrusers.OnionrUser(c, keypair[0])
|
||||
return
|
||||
|
||||
def test_contact_init_no_save(self):
|
||||
contact = c._crypto.generatePubKey()[0]
|
||||
contact = contactmanager.ContactManager(c, contact)
|
||||
self.assertFalse(contact.publicKey in c.listPeers())
|
||||
|
||||
def test_contact_create(self):
|
||||
contact = c._crypto.generatePubKey()[0]
|
||||
contact = contactmanager.ContactManager(c, contact, saveUser=True)
|
||||
self.assertTrue(contact.publicKey in c.listPeers())
|
||||
|
||||
def test_contact_set_info(self):
|
||||
contact = c._crypto.generatePubKey()[0]
|
||||
contact = contactmanager.ContactManager(c, contact, saveUser=True)
|
||||
fileLocation = '%s/contacts/%s.json' % (c.dataDir, contact.publicKey)
|
||||
contact.set_info('alias', 'bob')
|
||||
self.assertTrue(os.path.exists(fileLocation))
|
||||
|
||||
with open(fileLocation, 'r') as data:
|
||||
data = data.read()
|
||||
|
||||
data = json.loads(data)
|
||||
self.assertTrue(data['alias'] == 'bob')
|
||||
|
||||
def test_contact_get_info(self):
|
||||
contact = c._crypto.generatePubKey()[0]
|
||||
contact = contactmanager.ContactManager(c, contact, saveUser=True)
|
||||
fileLocation = '%s/contacts/%s.json' % (c.dataDir, contact.publicKey)
|
||||
|
||||
with open(fileLocation, 'w') as contactFile:
|
||||
contactFile.write('{"alias": "bob"}')
|
||||
|
||||
self.assertTrue(contact.get_info('alias', forceReload=True) == 'bob')
|
||||
self.assertTrue(contact.get_info('fail', forceReload=True) == None)
|
||||
self.assertTrue(contact.get_info('fail') == None)
|
||||
|
||||
def test_delete_contact(self):
|
||||
contact = c._crypto.generatePubKey()[0]
|
||||
contact = contactmanager.ContactManager(c, contact, saveUser=True)
|
||||
fileLocation = '%s/contacts/%s.json' % (c.dataDir, contact.publicKey)
|
||||
self.assertFalse(os.path.exists(fileLocation))
|
||||
with open(fileLocation, 'w') as contactFile:
|
||||
contactFile.write('{"alias": "test"}')
|
||||
self.assertTrue(os.path.exists(fileLocation))
|
||||
contact.delete_contact()
|
||||
self.assertFalse(os.path.exists(fileLocation))
|
||||
try:
|
||||
contact.get_info('alias')
|
||||
except onionrexceptions.ContactDeleted:
|
||||
pass
|
||||
else:
|
||||
self.assertTrue(False)
|
||||
try:
|
||||
contact.set_info('alias', 'test2')
|
||||
except onionrexceptions.ContactDeleted:
|
||||
pass
|
||||
else:
|
||||
self.assertTrue(False)
|
||||
|
||||
unittest.main()
|
55
onionr/tests/test_stringvalidations.py
Normal file
55
onionr/tests/test_stringvalidations.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
#!/usr/bin/env python3
|
||||
import sys, os
|
||||
sys.path.append(".")
|
||||
import unittest, uuid
|
||||
TEST_DIR = 'testdata/%s-%s' % (uuid.uuid4(), os.path.basename(__file__)) + '/'
|
||||
print("Test directory:", TEST_DIR)
|
||||
os.environ["ONIONR_HOME"] = TEST_DIR
|
||||
import core, onionr
|
||||
|
||||
core.Core()
|
||||
|
||||
class OnionrValidations(unittest.TestCase):
|
||||
|
||||
def test_peer_validator(self):
|
||||
# Test hidden service domain validities
|
||||
c = core.Core()
|
||||
valid = ['facebookcorewwwi.onion', 'vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd.onion',
|
||||
'5bvb5ncnfr4dlsfriwczpzcvo65kn7fnnlnt2ln7qvhzna2xaldq.b32.i2p']
|
||||
|
||||
invalid = [None, 'dsfewjirji0ejipdfs', '', ' ', '\n', '\r\n', 'f$ce%^okc+rewwwi.onion']
|
||||
|
||||
for x in valid:
|
||||
print('testing', x)
|
||||
self.assertTrue(c._utils.validateID(x))
|
||||
|
||||
for x in invalid:
|
||||
print('testing', x)
|
||||
self.assertFalse(c._utils.validateID(x))
|
||||
|
||||
def test_pubkey_validator(self):
|
||||
# Test ed25519 public key validity
|
||||
valid = 'JZ5VE72GUS3C7BOHDRIYZX4B5U5EJMCMLKHLYCVBQQF3UKHYIRRQ===='
|
||||
invalid = [None, '', ' ', 'dfsg', '\n', 'JZ5VE72GUS3C7BOHDRIYZX4B5U5EJMCMLKHLYCVBQQF3UKHYIR$Q====']
|
||||
c = core.Core()
|
||||
print('testing', valid)
|
||||
self.assertTrue(c._utils.validatePubKey(valid))
|
||||
|
||||
for x in invalid:
|
||||
#print('testing', x)
|
||||
self.assertFalse(c._utils.validatePubKey(x))
|
||||
|
||||
def test_integer_string(self):
|
||||
valid = ["1", "100", 100, "-5", -5]
|
||||
invalid = ['test', "1d3434", "1e100", None]
|
||||
c = core.Core()
|
||||
|
||||
for x in valid:
|
||||
#print('testing', x)
|
||||
self.assertTrue(c._utils.isIntegerString(x))
|
||||
|
||||
for x in invalid:
|
||||
#print('testing', x)
|
||||
self.assertFalse(c._utils.isIntegerString(x))
|
||||
|
||||
unittest.main()
|
|
@ -1,7 +1,7 @@
|
|||
'''
|
||||
Onionr - P2P Anonymous Storage Network
|
||||
Onionr - P2P Microblogging Platform & Social network
|
||||
|
||||
Handle incoming commands from the client. Intended for localhost use
|
||||
OnionrUtils offers various useful functions to Onionr networking.
|
||||
'''
|
||||
'''
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
|
@ -17,16 +17,18 @@
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
import flask, apimanager
|
||||
from flask import request, Response, abort, send_from_directory
|
||||
from gevent.pywsgi import WSGIServer
|
||||
def checkNetwork(utilsInst, torPort=0):
|
||||
'''Check if we are connected to the internet (through Tor)'''
|
||||
retData = False
|
||||
connectURLs = []
|
||||
try:
|
||||
with open('static-data/connect-check.txt', 'r') as connectTest:
|
||||
connectURLs = connectTest.read().split(',')
|
||||
|
||||
class APIPrivate:
|
||||
def __init__(self, managerInst):
|
||||
assert isinstance(managerInst, apimanager.APIManager)
|
||||
self.app = flask.Flask(__name__) # The flask application, which recieves data from the greenlet wsgiserver
|
||||
self.httpServer = WSGIServer((managerInst.privateIP, managerInst.privatePort), self.app, log=None)
|
||||
|
||||
def run(self):
|
||||
self.httpServer.serve_forever()
|
||||
return
|
||||
for url in connectURLs:
|
||||
if utilsInst.doGetRequest(url, port=torPort, ignoreAPI=True) != False:
|
||||
retData = True
|
||||
break
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return retData
|
46
onionr/utils/networkmerger.py
Normal file
46
onionr/utils/networkmerger.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
'''
|
||||
Onionr - P2P Microblogging Platform & Social network
|
||||
|
||||
Merges peer and block lists
|
||||
'''
|
||||
'''
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
import logger
|
||||
def mergeAdders(newAdderList, coreInst):
|
||||
'''
|
||||
Merge peer adders list to our database
|
||||
'''
|
||||
try:
|
||||
retVal = False
|
||||
if newAdderList != False:
|
||||
for adder in newAdderList.split(','):
|
||||
adder = adder.strip()
|
||||
if not adder in coreInst.listAdders(randomOrder = False) and adder != coreInst.hsAddress and not coreInst._blacklist.inBlacklist(adder):
|
||||
if not coreInst.config.get('tor.v3onions') and len(adder) == 62:
|
||||
continue
|
||||
if coreInst.addAddress(adder):
|
||||
# Check if we have the maxmium amount of allowed stored peers
|
||||
if coreInst.config.get('peers.max_stored_peers') > len(coreInst.listAdders()):
|
||||
logger.info('Added %s to db.' % adder, timestamp = True)
|
||||
retVal = True
|
||||
else:
|
||||
logger.warn('Reached the maximum amount of peers in the net database as allowed by your config.')
|
||||
else:
|
||||
pass
|
||||
#logger.debug('%s is either our address or already in our DB' % adder)
|
||||
return retVal
|
||||
except Exception as error:
|
||||
logger.error('Failed to merge adders.', error = error)
|
||||
return False
|
Loading…
Add table
Add a link
Reference in a new issue