merge contacts

This commit is contained in:
Kevin Froman 2019-02-17 15:47:06 -06:00
commit fc6dc0caf7
112 changed files with 1567 additions and 3821 deletions

0
.dockerignore Normal file → Executable file
View File

0
.gitignore vendored Normal file → Executable file
View File

7
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,7 @@
test:
image: ubuntu:bionic
script:
- apt-get update -qy
- apt-get install -y python3-pip tor
- pip3 install -r requirements.txt
- make test

0
.gitmodules vendored Normal file → Executable file
View File

2
CODE_OF_CONDUCT.md Normal file → Executable file
View File

@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe
## Enforcement ## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at beardog@firemail.cc. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at beardog at mailbox.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.

0
CONTRIBUTING.md Normal file → Executable file
View File

0
Dockerfile Normal file → Executable file
View File

0
ISSUE_TEMPLATE.md Normal file → Executable file
View File

0
LICENSE.txt Normal file → Executable file
View File

14
Makefile Normal file → Executable file
View File

@ -18,26 +18,20 @@ uninstall:
rm -f $(DESTDIR)$(PREFIX)/bin/onionr rm -f $(DESTDIR)$(PREFIX)/bin/onionr
test: test:
@./onionr.sh stop ./run_tests.sh
@sleep 1
@rm -rf onionr/data-backup
@mv onionr/data onionr/data-backup | true > /dev/null 2>&1
-@cd onionr; ./tests.py;
@rm -rf onionr/data
@mv onionr/data-backup onionr/data | true > /dev/null 2>&1
soft-reset: soft-reset:
@echo "Soft-resetting Onionr..." @echo "Soft-resetting Onionr..."
rm -f onionr/data/blocks/*.dat onionr/data/*.db onionr/data/block-nonces.dat | true > /dev/null 2>&1 rm -f onionr/$(ONIONR_HOME)/blocks/*.dat onionr/data/*.db onionr/$(ONIONR_HOME)/block-nonces.dat | true > /dev/null 2>&1
@./onionr.sh version | grep -v "Failed" --color=always @./onionr.sh version | grep -v "Failed" --color=always
reset: reset:
@echo "Hard-resetting Onionr..." @echo "Hard-resetting Onionr..."
rm -rf onionr/data/ | true > /dev/null 2>&1 rm -rf onionr/$(ONIONR_HOME)/ | true > /dev/null 2>&1
cd onionr/static-data/www/ui/; rm -rf ./dist; python compile.py cd onionr/static-data/www/ui/; rm -rf ./dist; python compile.py
#@./onionr.sh.sh version | grep -v "Failed" --color=always #@./onionr.sh.sh version | grep -v "Failed" --color=always
plugins-reset: plugins-reset:
@echo "Resetting plugins..." @echo "Resetting plugins..."
rm -rf onionr/data/plugins/ | true > /dev/null 2>&1 rm -rf onionr/$(ONIONR_HOME)/plugins/ | true > /dev/null 2>&1
@./onionr.sh version | grep -v "Failed" --color=always @./onionr.sh version | grep -v "Failed" --color=always

2
README.md Normal file → Executable file
View File

@ -11,6 +11,7 @@
(***pre-alpha & experimental, not well tested or easy to use yet***) (***pre-alpha & experimental, not well tested or easy to use yet***)
[![Open Source Love](https://badges.frapsoft.com/os/v3/open-source.png?v=103)](https://github.com/ellerbrock/open-source-badges/) [![Open Source Love](https://badges.frapsoft.com/os/v3/open-source.png?v=103)](https://github.com/ellerbrock/open-source-badges/)
<img src='https://gitlab.com/beardog/Onionr/badges/master/build.svg'>
<hr> <hr>
@ -60,6 +61,7 @@ Everyone is welcome to help out. Help is wanted for the following:
* Testing * Testing
* Running stable nodes * Running stable nodes
* Security review/audit * Security review/audit
* Automatic I2P setup
Bitcoin: [1onion55FXzm6h8KQw3zFw2igpHcV7LPq](bitcoin:1onion55FXzm6h8KQw3zFw2igpHcV7LPq) Bitcoin: [1onion55FXzm6h8KQw3zFw2igpHcV7LPq](bitcoin:1onion55FXzm6h8KQw3zFw2igpHcV7LPq)
USD: [Ko-Fi](https://www.ko-fi.com/beardogkf) USD: [Ko-Fi](https://www.ko-fi.com/beardogkf)

0
docs/api.md Normal file → Executable file
View File

0
docs/onionr-logo.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

0
docs/onionr-web.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

0
docs/whitepaper.md Normal file → Executable file
View File

0
onionr/__init__.py Normal file
View File

View File

@ -19,9 +19,7 @@
''' '''
from gevent.pywsgi import WSGIServer, WSGIHandler from gevent.pywsgi import WSGIServer, WSGIHandler
from gevent import Timeout from gevent import Timeout
#import gevent.monkey import flask, cgi, uuid
#gevent.monkey.patch_socket()
import flask, cgi
from flask import request, Response, abort, send_from_directory from flask import request, Response, abort, send_from_directory
import sys, random, threading, hmac, hashlib, base64, time, math, os, json, socket import sys, random, threading, hmac, hashlib, base64, time, math, os, json, socket
import core import core
@ -29,34 +27,15 @@ from onionrblockapi import Block
import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config, onionr import onionrutils, onionrexceptions, onionrcrypto, blockimporter, onionrevents as events, logger, config, onionr
class FDSafeHandler(WSGIHandler): class FDSafeHandler(WSGIHandler):
'''Our WSGI handler. Doesn't do much non-default except timeouts'''
def handle(self): def handle(self):
timeout = Timeout(60, exception=Exception) timeout = Timeout(60, exception=Exception)
timeout.start() timeout.start()
#timeout = gevent.Timeout.start_new(3)
try: try:
WSGIHandler.handle(self) WSGIHandler.handle(self)
except Timeout as ex: except Timeout as ex:
raise 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): def setBindIP(filePath):
'''Set a random localhost IP to a specified file (intended for private or public API localhost IPs)''' '''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))] 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: try:
s.bind((data, 0)) s.bind((data, 0))
except OSError: 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.') 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' data = '127.0.0.1'
s.close() s.close()
@ -89,33 +69,42 @@ class PublicAPI:
self.torAdder = clientAPI._core.hsAddress self.torAdder = clientAPI._core.hsAddress
self.i2pAdder = clientAPI._core.i2pAddress self.i2pAdder = clientAPI._core.i2pAddress
self.bindPort = config.get('client.public.port') self.bindPort = config.get('client.public.port')
self.lastRequest = 0
logger.info('Running public api on %s:%s' % (self.host, self.bindPort)) logger.info('Running public api on %s:%s' % (self.host, self.bindPort))
@app.before_request @app.before_request
def validateRequest(): def validateRequest():
'''Validate request has the correct hostname''' '''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: if config.get('general.security_level', default=0) > 0:
abort(403) abort(403)
if type(self.torAdder) is None and type(self.i2pAdder) is None: if type(self.torAdder) is None and type(self.i2pAdder) is None:
# abort if our hs addresses are not known # abort if our hs addresses are not known
abort(403) abort(403)
if request.host not in (self.i2pAdder, self.torAdder): if request.host not in (self.i2pAdder, self.torAdder):
# Disallow connection if wrong HTTP hostname, in order to prevent DNS rebinding attacks
abort(403) abort(403)
@app.after_request @app.after_request
def sendHeaders(resp): def sendHeaders(resp):
'''Send api, access control headers''' '''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'" 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' resp.headers['X-Frame-Options'] = 'deny'
# No sniff is possibly not needed
resp.headers['X-Content-Type-Options'] = "nosniff" resp.headers['X-Content-Type-Options'] = "nosniff"
# Network API version
resp.headers['X-API'] = onionr.API_VERSION resp.headers['X-API'] = onionr.API_VERSION
# Close connections to limit FD use
resp.headers['Connection'] = "close" resp.headers['Connection'] = "close"
self.lastRequest = clientAPI._core._utils.getRoundedEpoch(roundS=5)
return resp return resp
@app.route('/') @app.route('/')
def banner(): def banner():
# Display a bit of information to people who visit a node address in their browser
try: try:
with open('static-data/index.html', 'r') as html: with open('static-data/index.html', 'r') as html:
resp = Response(html.read(), mimetype='text/html') resp = Response(html.read(), mimetype='text/html')
@ -125,41 +114,48 @@ class PublicAPI:
@app.route('/getblocklist') @app.route('/getblocklist')
def getBlockList(): def getBlockList():
# Provide a list of our blocks, with a date offset
dateAdjust = request.args.get('date') dateAdjust = request.args.get('date')
bList = clientAPI._core.getBlockList(dateRec=dateAdjust) bList = clientAPI._core.getBlockList(dateRec=dateAdjust)
for b in self.hideBlocks: for b in self.hideBlocks:
if b in bList: 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) bList.remove(b)
return Response('\n'.join(bList)) return Response('\n'.join(bList))
@app.route('/getdata/<name>') @app.route('/getdata/<name>')
def getBlockData(name): def getBlockData(name):
# Share data for a block if we have it
resp = '' resp = ''
data = name data = name
if clientAPI._utils.validateHash(data): if clientAPI._utils.validateHash(data):
if data not in self.hideBlocks: if data not in self.hideBlocks:
if data in clientAPI._core.getBlockList(): if data in clientAPI._core.getBlockList():
block = clientAPI.getBlockData(data, raw=True).encode() block = clientAPI.getBlockData(data, raw=True)
resp = base64.b64encode(block).decode() 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: if len(resp) == 0:
abort(404) abort(404)
resp = "" resp = ""
return Response(resp) return Response(resp, mimetype='application/octet-stream')
@app.route('/www/<path:path>') @app.route('/www/<path:path>')
def wwwPublic(path): def wwwPublic(path):
# A way to share files directly over your .onion
if not config.get("www.public.run", True): if not config.get("www.public.run", True):
abort(403) abort(403)
return send_from_directory(config.get('www.public.path', 'static-data/www/public/'), path) return send_from_directory(config.get('www.public.path', 'static-data/www/public/'), path)
@app.route('/ping') @app.route('/ping')
def ping(): def ping():
# Endpoint to test if nodes are up
return Response("pong!") return Response("pong!")
@app.route('/getdbhash')
def getDBHash():
return Response(clientAPI._utils.getBlockDBHash())
@app.route('/pex') @app.route('/pex')
def peerExchange(): def peerExchange():
response = ','.join(clientAPI._core.listAdders(recent=3600)) response = ','.join(clientAPI._core.listAdders(recent=3600))
@ -194,11 +190,9 @@ class PublicAPI:
except AttributeError: except AttributeError:
pass pass
if powHash.startswith('0000'): if powHash.startswith('0000'):
try: newNode = clientAPI._core._utils.bytesToStr(newNode)
newNode = newNode.decode() if clientAPI._core._utils.validateID(newNode) and not newNode in clientAPI._core.onionrInst.communicatorInst.newPeers:
except AttributeError: clientAPI._core.onionrInst.communicatorInst.newPeers.append(newNode)
pass
if clientAPI._core.addAddress(newNode):
resp = 'Success' resp = 'Success'
else: else:
logger.warn(newNode.decode() + ' failed to meet POW: ' + powHash) logger.warn(newNode.decode() + ' failed to meet POW: ' + powHash)
@ -207,6 +201,9 @@ class PublicAPI:
@app.route('/upload', methods=['post']) @app.route('/upload', methods=['post'])
def upload(): def upload():
'''Accept file uploads. In the future this will be done more often than on creation
to speed up block sync
'''
resp = 'failure' resp = 'failure'
try: try:
data = request.form['block'] data = request.form['block']
@ -228,6 +225,7 @@ class PublicAPI:
resp = Response(resp) resp = Response(resp)
return resp return resp
# Set instances, then startup our public api server
clientAPI.setPublicAPIInstance(self) clientAPI.setPublicAPIInstance(self)
while self.torAdder == '': while self.torAdder == '':
clientAPI._core.refreshFirstStartVars() clientAPI._core.refreshFirstStartVars()
@ -255,7 +253,6 @@ class API:
onionr.Onionr.setupConfig('data/', self = self) onionr.Onionr.setupConfig('data/', self = self)
self.debug = debug self.debug = debug
self._privateDelayTime = 3
self._core = onionrInst.onionrCore self._core = onionrInst.onionrCore
self.startTime = self._core._utils.getEpoch() self.startTime = self._core._utils.getEpoch()
self._crypto = onionrcrypto.OnionrCrypto(self._core) self._crypto = onionrcrypto.OnionrCrypto(self._core)
@ -264,7 +261,7 @@ class API:
bindPort = int(config.get('client.client.port', 59496)) bindPort = int(config.get('client.client.port', 59496))
self.bindPort = bindPort 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.whitelistEndpoints = ('site', 'www', 'onionrhome', 'board', 'boardContent', 'sharedContent', 'mail', 'mailindex')
self.clientToken = config.get('client.webpassword') self.clientToken = config.get('client.webpassword')
@ -276,12 +273,14 @@ class API:
logger.info('Running api on %s:%s' % (self.host, self.bindPort)) logger.info('Running api on %s:%s' % (self.host, self.bindPort))
self.httpServer = '' self.httpServer = ''
self.pluginResponses = {} # Responses for plugin endpoints
self.queueResponse = {} self.queueResponse = {}
onionrInst.setClientAPIInst(self) onionrInst.setClientAPIInst(self)
@app.before_request @app.before_request
def validateRequest(): def validateRequest():
'''Validate request has set password and is the correct hostname''' '''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): if request.host != '%s:%s' % (self.host, self.bindPort):
abort(403) abort(403)
if request.endpoint in self.whitelistEndpoints: if request.endpoint in self.whitelistEndpoints:
@ -294,11 +293,13 @@ class API:
@app.after_request @app.after_request
def afterReq(resp): 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'" # Security headers
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'" 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-Frame-Options'] = 'deny'
resp.headers['X-Content-Type-Options'] = "nosniff" resp.headers['X-Content-Type-Options'] = "nosniff"
resp.headers['X-API'] = onionr.API_VERSION
resp.headers['Server'] = '' 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['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch.
resp.headers['Connection'] = "close" resp.headers['Connection'] = "close"
@ -330,11 +331,13 @@ class API:
@app.route('/queueResponseAdd/<name>', methods=['post']) @app.route('/queueResponseAdd/<name>', methods=['post'])
def queueResponseAdd(name): def queueResponseAdd(name):
# Responses from the daemon. TODO: change to direct var access instead of http endpoint
self.queueResponse[name] = request.form['data'] self.queueResponse[name] = request.form['data']
return Response('success') return Response('success')
@app.route('/queueResponse/<name>') @app.route('/queueResponse/<name>')
def queueResponse(name): def queueResponse(name):
# Fetch a daemon queue response
resp = 'failure' resp = 'failure'
try: try:
resp = self.queueResponse[name] resp = self.queueResponse[name]
@ -346,10 +349,12 @@ class API:
@app.route('/ping') @app.route('/ping')
def ping(): def ping():
# Used to check if client api is working
return Response("pong!") return Response("pong!")
@app.route('/', endpoint='onionrhome') @app.route('/', endpoint='onionrhome')
def hello(): def hello():
# ui home
return send_from_directory('static-data/www/private/', 'index.html') return send_from_directory('static-data/www/private/', 'index.html')
@app.route('/getblocksbytype/<name>') @app.route('/getblocksbytype/<name>')
@ -357,12 +362,13 @@ class API:
blocks = self._core.getBlocksByType(name) blocks = self._core.getBlocksByType(name)
return Response(','.join(blocks)) return Response(','.join(blocks))
@app.route('/gethtmlsafeblockdata/<name>') @app.route('/getblockbody/<name>')
def getSafeData(name): def getBlockBodyData(name):
resp = '' resp = ''
if self._core._utils.validateHash(name): if self._core._utils.validateHash(name):
try: 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: except TypeError:
pass pass
else: else:
@ -384,6 +390,15 @@ class API:
abort(404) abort(404)
return Response(resp) 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') @app.route('/site/<name>', endpoint='site')
def site(name): def site(name):
bHash = name bHash = name
@ -402,7 +417,8 @@ class API:
return Response(resp) return Response(resp)
@app.route('/waitforshare/<name>', methods=['post']) @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() assert name.isalnum()
if name in self.publicAPI.hideBlocks: if name in self.publicAPI.hideBlocks:
self.publicAPI.hideBlocks.remove(name) self.publicAPI.hideBlocks.remove(name)
@ -428,6 +444,7 @@ class API:
@app.route('/getstats') @app.route('/getstats')
def getStats(): def getStats():
# returns node stats
#return Response("disabled") #return Response("disabled")
while True: while True:
try: try:
@ -439,6 +456,78 @@ class API:
def showUptime(): def showUptime():
return Response(str(self.getUptime())) 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 = WSGIServer((self.host, bindPort), app, log=None, handler_class=FDSafeHandler)
self.httpServer.serve_forever() self.httpServer.serve_forever()
@ -448,7 +537,7 @@ class API:
def validateToken(self, token): 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: if len(self.clientToken) == 0:
logger.error("client password needs to be set") logger.error("client password needs to be set")
@ -469,7 +558,8 @@ class API:
# Don't error on race condition with startup # Don't error on race condition with startup
pass 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) bl = Block(bHash, core=self._core)
if decrypt: if decrypt:
bl.decrypt() bl.decrypt()
@ -477,12 +567,22 @@ class API:
raise ValueError raise ValueError
if not raw: if not raw:
retData = {'meta':bl.bheader, 'metadata': bl.bmetadata, 'content': bl.bcontent} if not headerOnly:
for x in list(retData.keys()): retData = {'meta':bl.bheader, 'metadata': bl.bmetadata, 'content': bl.bcontent}
try: for x in list(retData.keys()):
retData[x] = retData[x].decode() try:
except AttributeError: retData[x] = retData[x].decode()
pass 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) return json.dumps(retData)
else: else:
return bl.raw return bl.raw

View File

@ -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)

View File

@ -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
View File

View File

@ -21,10 +21,12 @@
''' '''
import sys, os, core, config, json, requests, time, logger, threading, base64, onionr, uuid 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 onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block
import onionrdaemontools, onionrsockets, onionr, onionrproofs, proofofmemory import onionrdaemontools, onionrsockets, onionr, onionrproofs
import binascii import binascii
from dependencies import secrets from dependencies import secrets
from defusedxml import minidom from defusedxml import minidom
from utils import networkmerger
config.reload() config.reload()
class OnionrCommunicatorDaemon: class OnionrCommunicatorDaemon:
def __init__(self, onionrInst, proxyPort, developmentMode=config.get('general.dev_mode', False)): def __init__(self, onionrInst, proxyPort, developmentMode=config.get('general.dev_mode', False)):
@ -38,11 +40,11 @@ class OnionrCommunicatorDaemon:
# list of timer instances # list of timer instances
self.timers = [] self.timers = []
# initalize core with Tor socks port being 3rd argument # initialize core with Tor socks port being 3rd argument
self.proxyPort = proxyPort self.proxyPort = proxyPort
self._core = onionrInst.onionrCore self._core = onionrInst.onionrCore
# intalize NIST beacon salt and time # initialize NIST beacon salt and time
self.nistSaltTimestamp = 0 self.nistSaltTimestamp = 0
self.powSalt = 0 self.powSalt = 0
@ -57,11 +59,12 @@ class OnionrCommunicatorDaemon:
self.cooldownPeer = {} self.cooldownPeer = {}
self.connectTimes = {} self.connectTimes = {}
self.peerProfiles = [] # list of peer's profiles (onionrpeers.PeerProfile instances) 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 # amount of threads running by name, used to prevent too many
self.threadCounts = {} self.threadCounts = {}
# set true when shutdown command recieved # set true when shutdown command received
self.shutdown = False self.shutdown = False
# list of new blocks to download, added to when new block lists are fetched from peers # 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.socketServer.start()
self.socketClient = onionrsockets.OnionrSocketClient(self._core) self.socketClient = onionrsockets.OnionrSocketClient(self._core)
# Main daemon loop, mainly for calling timers, don't do any complex operations here to avoid locking # Main daemon loop, mainly for calling timers, don't do any complex operations here to avoid locking
try: try:
while not self.shutdown: while not self.shutdown:
@ -155,11 +157,27 @@ class OnionrCommunicatorDaemon:
'''Lookup new peer addresses''' '''Lookup new peer addresses'''
logger.info('Looking up new addresses...') logger.info('Looking up new addresses...')
tryAmount = 1 tryAmount = 1
newPeers = []
for i in range(tryAmount): for i in range(tryAmount):
# Download new peer address list from random online peers # 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() peer = self.pickOnlinePeer()
newAdders = self.peerAction(peer, action='pex') 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') self.decrementThreadCount('lookupAdders')
def lookupBlocks(self): def lookupBlocks(self):
@ -188,40 +206,37 @@ class OnionrCommunicatorDaemon:
break break
else: else:
continue 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) 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.
# Get the last time we looked up a peer's stamp to only fetch blocks since then. # Saved in memory only for privacy reasons
# Saved in memory only for privacy reasons try:
try: lastLookupTime = self.dbTimestamps[peer]
lastLookupTime = self.dbTimestamps[peer] except KeyError:
except KeyError: lastLookupTime = 0
lastLookupTime = 0 else:
else: listLookupCommand += '?date=%s' % (lastLookupTime,)
listLookupCommand += '?date=%s' % (lastLookupTime,) try:
try: newBlocks = self.peerAction(peer, listLookupCommand) # get list of new block hashes
newBlocks = self.peerAction(peer, listLookupCommand) # get list of new block hashes except Exception as error:
except Exception as error: logger.warn('Could not get new blocks from %s.' % peer, error = error)
logger.warn('Could not get new blocks from %s.' % peer, error = error) newBlocks = False
newBlocks = False else:
else: self.dbTimestamps[peer] = self._core._utils.getRoundedEpoch(roundS=60)
self.dbTimestamps[peer] = self._core._utils.getRoundedEpoch(roundS=60) if newBlocks != False:
if newBlocks != False: # if request was a success
# if request was a success for i in newBlocks.split('\n'):
for i in newBlocks.split('\n'): if self._core._utils.validateHash(i):
if self._core._utils.validateHash(i): # if newline seperated string is valid hash
# if newline seperated string is valid hash if not i in existingBlocks:
if not i in existingBlocks: # if block does not exist on disk and is not already in block queue
# if block does not exist on disk and is not already in block queue if i not in self.blockQueue:
if i not in self.blockQueue: if onionrproofs.hashMeetsDifficulty(i) and not self._core._blacklist.inBlacklist(i):
if onionrproofs.hashMeetsDifficulty(i) and not self._core._blacklist.inBlacklist(i): if len(self.blockQueue) <= 1000000:
if len(self.blockQueue) <= 1000000: self.blockQueue[i] = [peer] # add blocks to download queue
self.blockQueue[i] = [peer] # add blocks to download queue else:
else: if peer not in self.blockQueue[i]:
if peer not in self.blockQueue[i]: if len(self.blockQueue[i]) < 10:
self.blockQueue[i].append(peer) self.blockQueue[i].append(peer)
self.decrementThreadCount('lookupBlocks') self.decrementThreadCount('lookupBlocks')
return return
@ -240,10 +255,10 @@ class OnionrCommunicatorDaemon:
break break
# Do not download blocks being downloaded or that are already saved (edge cases) # Do not download blocks being downloaded or that are already saved (edge cases)
if blockHash in self.currentDownloading: if blockHash in self.currentDownloading:
logger.debug('Already downloading block %s...' % blockHash) #logger.debug('Already downloading block %s...' % blockHash)
continue continue
if blockHash in self._core.getBlockList(): if blockHash in self._core.getBlockList():
logger.debug('Block %s is already saved.' % (blockHash,)) #logger.debug('Block %s is already saved.' % (blockHash,))
try: try:
del self.blockQueue[blockHash] del self.blockQueue[blockHash]
except KeyError: except KeyError:
@ -268,10 +283,7 @@ class OnionrCommunicatorDaemon:
content = content.encode() content = content.encode()
except AttributeError: except AttributeError:
pass pass
try:
content = base64.b64decode(content) # content is base64 encoded in transport
except binascii.Error:
pass
realHash = self._core._crypto.sha3Hash(content) realHash = self._core._crypto.sha3Hash(content)
try: try:
realHash = realHash.decode() # bytes on some versions for some reason realHash = realHash.decode() # bytes on some versions for some reason
@ -402,8 +414,18 @@ class OnionrCommunicatorDaemon:
else: else:
peerList = self._core.listAdders() peerList = self._core.listAdders()
mainPeerList = self._core.listAdders()
peerList = onionrpeers.getScoreSortedPeerList(self._core) 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: if len(peerList) == 0 or useBootstrap:
# Avoid duplicating bootstrap addresses in peerList # Avoid duplicating bootstrap addresses in peerList
self.addBootstrapListToPeerList(peerList) self.addBootstrapListToPeerList(peerList)
@ -418,6 +440,8 @@ class OnionrCommunicatorDaemon:
if self.peerAction(address, 'ping') == 'pong!': if self.peerAction(address, 'ping') == 'pong!':
logger.info('Connected to ' + address) logger.info('Connected to ' + address)
time.sleep(0.1) time.sleep(0.1)
if address not in mainPeerList:
networkmerger.mergeAdders(address, self._core)
if address not in self.onlinePeers: if address not in self.onlinePeers:
self.onlinePeers.append(address) self.onlinePeers.append(address)
self.connectTimes[address] = self._core._utils.getEpoch() self.connectTimes[address] = self._core._utils.getEpoch()
@ -588,7 +612,7 @@ class OnionrCommunicatorDaemon:
proxyType = 'i2p' proxyType = 'i2p'
logger.info("Uploading block to " + peer) logger.info("Uploading block to " + peer)
if not self._core._utils.doPostRequest(url, data=data, proxyType=proxyType) == False: 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) finishedUploads.append(bl)
for x in finishedUploads: for x in finishedUploads:
try: try:
@ -605,8 +629,8 @@ class OnionrCommunicatorDaemon:
def detectAPICrash(self): def detectAPICrash(self):
'''exit if the api server crashes/stops''' '''exit if the api server crashes/stops'''
if self._core._utils.localCommand('ping', silent=False) not in ('pong', 'pong!'): if self._core._utils.localCommand('ping', silent=False) not in ('pong', 'pong!'):
for i in range(5): for i in range(8):
if self._core._utils.localCommand('ping') in ('pong', 'pong!'): if self._core._utils.localCommand('ping') in ('pong', 'pong!') or self.shutdown:
break # break for loop break # break for loop
time.sleep(1) time.sleep(1)
else: else:

0
onionr/config.py Normal file → Executable file
View File

86
onionr/core.py Normal file → Executable file
View File

@ -21,7 +21,8 @@ import sqlite3, os, sys, time, math, base64, tarfile, nacl, logger, json, netcon
from onionrblockapi import Block from onionrblockapi import Block
import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions
import onionrblacklist, onionrusers import onionrblacklist
from onionrusers import onionrusers
import dbcreator, onionrstorage, serializeddata import dbcreator, onionrstorage, serializeddata
from etc import onionrvalues from etc import onionrvalues
@ -56,7 +57,7 @@ class Core:
self.privateApiHostFile = self.dataDir + 'private-host.txt' self.privateApiHostFile = self.dataDir + 'private-host.txt'
self.addressDB = self.dataDir + 'address.db' self.addressDB = self.dataDir + 'address.db'
self.hsAddress = '' 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.bootstrapFileLocation = 'static-data/bootstrap-nodes.txt'
self.bootstrapList = [] self.bootstrapList = []
self.requirements = onionrvalues.OnionrValues() self.requirements = onionrvalues.OnionrValues()
@ -85,6 +86,10 @@ class Core:
self.createBlockDB() self.createBlockDB()
if not os.path.exists(self.forwardKeysFile): if not os.path.exists(self.forwardKeysFile):
self.dbCreate.createForwardKeyDB() 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'): if os.path.exists(self.dataDir + '/hs/hostname'):
with open(self.dataDir + '/hs/hostname', 'r') as hs: 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) 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 # This function simply adds a peer to the DB
if not self._utils.validatePubKey(peerID): if not self._utils.validatePubKey(peerID):
@ -221,18 +227,8 @@ class Core:
c.execute('Delete from hashes where hash=?;', t) c.execute('Delete from hashes where hash=?;', t)
conn.commit() conn.commit()
conn.close() conn.close()
blockFile = self.dataDir + '/blocks/%s.dat' % block dataSize = sys.getsizeof(onionrstorage.getData(self, block))
dataSize = 0 self._utils.storageCounter.removeBytes(dataSize)
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
def createAddressDB(self): def createAddressDB(self):
''' '''
@ -282,15 +278,6 @@ class Core:
Simply return the data associated to a hash 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) data = onionrstorage.getData(self, hash)
return data return data
@ -316,9 +303,6 @@ class Core:
#raise Exception("Data is already set for " + dataHash) #raise Exception("Data is already set for " + dataHash)
else: else:
if self._utils.storageCounter.addBytes(dataSize) != False: if self._utils.storageCounter.addBytes(dataSize) != False:
#blockFile = open(blockFileName, 'wb')
#blockFile.write(data)
#blockFile.close()
onionrstorage.store(self, data, blockHash=dataHash) onionrstorage.store(self, data, blockHash=dataHash)
conn = sqlite3.connect(self.blockDB, timeout=30) conn = sqlite3.connect(self.blockDB, timeout=30)
c = conn.cursor() c = conn.cursor()
@ -557,19 +541,18 @@ class Core:
knownPeer text, 2 knownPeer text, 2
speed int, 3 speed int, 3
success int, 4 success int, 4
DBHash text, 5 powValue 5
powValue 6 failure int 6
failure int 7 lastConnect 7
lastConnect 8 trust 8
trust 9 introduced 9
introduced 10
''' '''
conn = sqlite3.connect(self.addressDB, timeout=30) conn = sqlite3.connect(self.addressDB, timeout=30)
c = conn.cursor() c = conn.cursor()
command = (address,) 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] info = infoNumbers[info]
iterCount = 0 iterCount = 0
retVal = '' retVal = ''
@ -595,7 +578,7 @@ class Core:
command = (data, address) 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") raise Exception("Got invalid database key when setting address info")
else: else:
c.execute('UPDATE adders SET ' + key + ' = ? WHERE address=?', command) c.execute('UPDATE adders SET ' + key + ' = ? WHERE address=?', command)
@ -680,19 +663,6 @@ class Core:
conn.close() conn.close()
return rows 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): def updateBlockInfo(self, hash, key, data):
''' '''
sets info associated with a block sets info associated with a block
@ -731,6 +701,7 @@ class Core:
logger.error(allocationReachedMessage) logger.error(allocationReachedMessage)
return False return False
retData = False retData = False
# check nonce # check nonce
dataNonce = self._utils.bytesToStr(self._crypto.sha3Hash(data)) dataNonce = self._utils.bytesToStr(self._crypto.sha3Hash(data))
try: try:
@ -746,6 +717,12 @@ class Core:
if type(data) is bytes: if type(data) is bytes:
data = data.decode() data = data.decode()
data = str(data) 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 = '' retData = ''
signature = '' signature = ''
@ -768,7 +745,7 @@ class Core:
pass pass
if encryptType == 'asym': if encryptType == 'asym':
if not disableForward and asymPeer != self._crypto.pubKey: if not disableForward and sign and asymPeer != self._crypto.pubKey:
try: try:
forwardEncrypted = onionrusers.OnionrUser(self, asymPeer).forwardEncrypt(data) forwardEncrypted = onionrusers.OnionrUser(self, asymPeer).forwardEncrypt(data)
data = forwardEncrypted[0] data = forwardEncrypted[0]
@ -780,6 +757,7 @@ class Core:
#fsKey = onionrusers.OnionrUser(self, asymPeer).getGeneratedForwardKeys().reverse() #fsKey = onionrusers.OnionrUser(self, asymPeer).getGeneratedForwardKeys().reverse()
meta['newFSKey'] = fsKey meta['newFSKey'] = fsKey
jsonMeta = json.dumps(meta) jsonMeta = json.dumps(meta)
plaintextMeta = jsonMeta
if sign: if sign:
signature = self._crypto.edSign(jsonMeta.encode() + data, key=self._crypto.privKey, encodeResult=True) signature = self._crypto.edSign(jsonMeta.encode() + data, key=self._crypto.privKey, encodeResult=True)
signer = self._crypto.pubKey signer = self._crypto.pubKey
@ -802,10 +780,10 @@ class Core:
if self._utils.validatePubKey(asymPeer): if self._utils.validatePubKey(asymPeer):
# Encrypt block data with forward secrecy key first, but not meta # Encrypt block data with forward secrecy key first, but not meta
jsonMeta = json.dumps(meta) jsonMeta = json.dumps(meta)
jsonMeta = self._crypto.pubKeyEncrypt(jsonMeta, asymPeer, encodedData=True, anonymous=True).decode() jsonMeta = self._crypto.pubKeyEncrypt(jsonMeta, asymPeer, encodedData=True).decode()
data = self._crypto.pubKeyEncrypt(data, asymPeer, encodedData=True, anonymous=True).decode() data = self._crypto.pubKeyEncrypt(data, asymPeer, encodedData=True).decode()
signature = self._crypto.pubKeyEncrypt(signature, asymPeer, encodedData=True, anonymous=True).decode() signature = self._crypto.pubKeyEncrypt(signature, asymPeer, encodedData=True).decode()
signer = self._crypto.pubKeyEncrypt(signer, asymPeer, encodedData=True, anonymous=True).decode() signer = self._crypto.pubKeyEncrypt(signer, asymPeer, encodedData=True).decode()
onionrusers.OnionrUser(self, asymPeer, saveUser=True) onionrusers.OnionrUser(self, asymPeer, saveUser=True)
else: else:
raise onionrexceptions.InvalidPubkey(asymPeer + ' is not a valid base32 encoded ed25519 key') raise onionrexceptions.InvalidPubkey(asymPeer + ' is not a valid base32 encoded ed25519 key')
@ -832,14 +810,14 @@ class Core:
retData = False retData = False
else: else:
# Tell the api server through localCommand to wait for the daemon to upload this block to make stastical analysis more difficult # 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.addToBlockDB(retData, selfInsert=True, dataSaved=True)
#self.setBlockType(retData, meta['type']) #self.setBlockType(retData, meta['type'])
self._utils.processBlockMetadata(retData) self._utils.processBlockMetadata(retData)
self.daemonQueueAdd('uploadBlock', retData) self.daemonQueueAdd('uploadBlock', retData)
if retData != False: 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 return retData
def introduceNode(self): def introduceNode(self):

View File

@ -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
View File

@ -39,7 +39,6 @@ class DBCreator:
knownPeer text, knownPeer text,
speed int, speed int,
success int, success int,
DBHash text,
powValue text, powValue text,
failure int, failure int,
lastConnect int, lastConnect int,

0
onionr/dependencies/secrets.py Normal file → Executable file
View File

0
onionr/etc/onionrvalues.py Normal file → Executable file
View File

58
onionr/etc/pgpwords.py Normal file → Executable file
View File

@ -1,7 +1,9 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- (because 0xFF, even : "Yucatán") # -*- 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 = [ _words = [
["aardvark", "adroitness"], ["aardvark", "adroitness"],
@ -259,7 +261,7 @@ _words = [
["wayside", "Wilmington"], ["wayside", "Wilmington"],
["willow", "Wyoming"], ["willow", "Wyoming"],
["woodlark", "yesteryear"], ["woodlark", "yesteryear"],
["Zulu", "Yucatán"]] ["Zulu", "Yucatan"]]
hexre = re.compile("[a-fA-F0-9]+") hexre = re.compile("[a-fA-F0-9]+")
@ -275,41 +277,21 @@ def wordify(seq):
ret = [] ret = []
for i in range(0, len(seq), 2): 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 return ret
def usage(): def hexify(seq, delim=' '):
print("Usage:") ret = b''
print(" {0} [fingerprint...]".format(os.path.basename(sys.argv[0]))) sentence = seq
print("") try:
print("If called with multiple arguments, they will be concatenated") sentence = seq.split(delim)
print("and treated as a single fingerprint.") except AttributeError:
print("") pass
print("If called with no arguments, input is read from stdin,") count = 0
print("and each line is treated as a single fingerprint. In this") for word in sentence:
print("mode, invalid values are silently ignored.") count = 0
exit(1) for wordPair in _words:
if word in wordPair:
if __name__ == '__main__': ret += bytes([(count)])
if 1 == len(sys.argv): count += 1
fps = sys.stdin.readlines() return binascii.hexlify(ret)
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("")

0
onionr/keymanager.py Normal file → Executable file
View File

7
onionr/logger.py Normal file → Executable file
View File

@ -18,7 +18,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
''' '''
import re, sys, time, traceback import re, sys, time, traceback, os
class colors: class colors:
''' '''
@ -64,7 +64,10 @@ class colors:
''' '''
Use the bitwise operators to merge these settings 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_CONSOLE = 0b010
OUTPUT_TO_FILE = 0b001 OUTPUT_TO_FILE = 0b001

0
onionr/netcontroller.py Normal file → Executable file
View File

View File

@ -25,7 +25,7 @@ MIN_PY_VERSION = 6
if sys.version_info[0] == 2 or sys.version_info[1] < MIN_PY_VERSION: if sys.version_info[0] == 2 or sys.version_info[1] < MIN_PY_VERSION:
print('Error, Onionr requires Python 3.%s+' % (MIN_PY_VERSION,)) print('Error, Onionr requires Python 3.%s+' % (MIN_PY_VERSION,))
sys.exit(1) 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 import webbrowser, uuid, signal
from threading import Thread from threading import Thread
import api, core, config, logger, onionrplugins as plugins, onionrevents as events import api, core, config, logger, onionrplugins as plugins, onionrevents as events
@ -33,17 +33,18 @@ import onionrutils
import netcontroller, onionrstorage import netcontroller, onionrstorage
from netcontroller import NetController from netcontroller import NetController
from onionrblockapi import Block from onionrblockapi import Block
import onionrproofs, onionrexceptions, onionrusers, communicator import onionrproofs, onionrexceptions, communicator
from onionrusers import onionrusers
try: try:
from urllib3.contrib.socks import SOCKSProxyManager from urllib3.contrib.socks import SOCKSProxyManager
except ImportError: except ImportError:
raise Exception("You need the PySocks module (for use with socks5 proxy to use Tor)") 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 = '0.5.0' # for debugging and stuff
ONIONR_VERSION_TUPLE = tuple(ONIONR_VERSION.split('.')) # (MAJOR, MINOR, VERSION) 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: class Onionr:
def __init__(self): def __init__(self):
@ -110,12 +111,6 @@ class Onionr:
except: except:
plugins.disable(name, onionr = self, stop_event = False) 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 # Get configuration
if type(config.get('client.webpassword')) is type(None): if type(config.get('client.webpassword')) is type(None):
config.set('client.webpassword', base64.b16encode(os.urandom(32)).decode('utf-8'), savefile=True) 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) config.set('client.client.port', randomPort, savefile=True)
if type(config.get('client.public.port')) is type(None): if type(config.get('client.public.port')) is type(None):
randomPort = netcontroller.getOpenPort() randomPort = netcontroller.getOpenPort()
print(randomPort)
config.set('client.public.port', randomPort, savefile=True) config.set('client.public.port', randomPort, savefile=True)
if type(config.get('client.participate')) is type(None): if type(config.get('client.participate')) is type(None):
config.set('client.participate', True, savefile=True) config.set('client.participate', True, savefile=True)
@ -206,9 +202,6 @@ class Onionr:
'connect': self.addAddress, 'connect': self.addAddress,
'pex': self.doPEX, 'pex': self.doPEX,
'ui' : self.openUI,
'gui' : self.openUI,
'getpassword': self.printWebPassword, 'getpassword': self.printWebPassword,
'get-password': self.printWebPassword, 'get-password': self.printWebPassword,
'getpwd': self.printWebPassword, 'getpwd': self.printWebPassword,
@ -297,8 +290,6 @@ class Onionr:
with open('%s/%s.dat' % (exportDir, bHash), 'wb') as exportFile: with open('%s/%s.dat' % (exportDir, bHash), 'wb') as exportFile:
exportFile.write(data) exportFile.write(data)
def showDetails(self): def showDetails(self):
details = { details = {
'Node Address' : self.get_hostname(), 'Node Address' : self.get_hostname(),
@ -562,24 +553,12 @@ class Onionr:
if self.onionrUtils.hasKey(newPeer): if self.onionrUtils.hasKey(newPeer):
logger.info('We already have that key') logger.info('We already have that key')
return 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) logger.info("Adding peer: " + logger.colors.underline + newPeer)
if self.onionrUtils.mergeKeys(newPeer): try:
logger.info('Successfully added key') if self.onionrCore.addPeer(newPeer):
else: logger.info('Successfully added key')
except AssertionError:
logger.error('Failed to add key') logger.error('Failed to add key')
return return
def addAddress(self): def addAddress(self):
@ -598,7 +577,6 @@ class Onionr:
logger.info("Successfully added address.") logger.info("Successfully added address.")
else: else:
logger.warn("Unable to add address.") logger.warn("Unable to add address.")
return return
def addMessage(self, header="txt"): def addMessage(self, header="txt"):
@ -774,7 +752,7 @@ class Onionr:
Onionr.setupConfig('data/', self = self) Onionr.setupConfig('data/', self = self)
if self._developmentMode: 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) net = NetController(config.get('client.public.port', 59497), apiServerIP=apiHost)
logger.debug('Tor is starting...') logger.debug('Tor is starting...')
if not net.startTor(): if not net.startTor():
@ -985,7 +963,7 @@ class Onionr:
''' '''
self.addFile(singleBlock=True, blockType='html') 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 Adds a file to the onionr network
''' '''
@ -1001,7 +979,8 @@ class Onionr:
try: try:
with open(filename, 'rb') as singleFile: with open(filename, 'rb') as singleFile:
blockhash = self.onionrCore.insertBlock(base64.b64encode(singleFile.read()), header=blockType) 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: except:
logger.error('Failed to save file in block.', timestamp = False) logger.error('Failed to save file in block.', timestamp = False)
else: else:
@ -1030,7 +1009,6 @@ class Onionr:
settings = settings | logger.OUTPUT_TO_CONSOLE settings = settings | logger.OUTPUT_TO_CONSOLE
if config.get('log.file.output', True): if config.get('log.file.output', True):
settings = settings | logger.OUTPUT_TO_FILE settings = settings | logger.OUTPUT_TO_FILE
logger.set_file(config.get('log.file.path', '/tmp/onionr.log').replace('data/', dataDir))
logger.set_settings(settings) logger.set_settings(settings)
if not self is None: if not self is None:
@ -1073,12 +1051,6 @@ class Onionr:
return data_exists 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.'): 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: if os.path.exists('static-data/header.txt') and logger.get_level() <= logger.LEVEL_INFO:
with open('static-data/header.txt', 'rb') as file: with open('static-data/header.txt', 'rb') as file:

5
onionr/onionrblacklist.py Normal file → Executable file
View File

@ -116,4 +116,7 @@ class OnionrBlackList:
return return
insert = (hashed,) insert = (hashed,)
blacklistDate = self._core._utils.getEpoch() 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
View File

@ -18,8 +18,9 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
''' '''
import 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 import json, os, sys, datetime, base64, onionrstorage
from onionrusers import onionrusers
class Block: class Block:
blockCacheOrder = list() # NEVER write your own code that writes to this! blockCacheOrder = list() # NEVER write your own code that writes to this!
@ -59,7 +60,7 @@ class Block:
self.update() self.update()
def decrypt(self, anonymous = True, encodedData = True): def decrypt(self, encodedData = True):
''' '''
Decrypt a block, loading decrypted data into their vars Decrypt a block, loading decrypted data into their vars
''' '''
@ -71,16 +72,16 @@ class Block:
# decrypt data # decrypt data
if self.getHeader('encryptType') == 'asym': if self.getHeader('encryptType') == 'asym':
try: try:
self.bcontent = core._crypto.pubKeyDecrypt(self.bcontent, anonymous=anonymous, encodedData=encodedData) self.bcontent = core._crypto.pubKeyDecrypt(self.bcontent, encodedData=encodedData)
bmeta = core._crypto.pubKeyDecrypt(self.bmetadata, anonymous=anonymous, encodedData=encodedData) bmeta = core._crypto.pubKeyDecrypt(self.bmetadata, encodedData=encodedData)
try: try:
bmeta = bmeta.decode() bmeta = bmeta.decode()
except AttributeError: except AttributeError:
# yet another bytes fix # yet another bytes fix
pass pass
self.bmetadata = json.loads(bmeta) self.bmetadata = json.loads(bmeta)
self.signature = core._crypto.pubKeyDecrypt(self.signature, anonymous=anonymous, encodedData=encodedData) self.signature = core._crypto.pubKeyDecrypt(self.signature, encodedData=encodedData)
self.signer = core._crypto.pubKeyDecrypt(self.signer, anonymous=anonymous, encodedData=encodedData) self.signer = core._crypto.pubKeyDecrypt(self.signer, encodedData=encodedData)
self.bheader['signer'] = self.signer.decode() self.bheader['signer'] = self.signer.decode()
self.signedData = json.dumps(self.bmetadata) + self.bcontent.decode() self.signedData = json.dumps(self.bmetadata) + self.bcontent.decode()
try: try:
@ -94,7 +95,8 @@ class Block:
logger.error(str(e)) logger.error(str(e))
pass pass
except nacl.exceptions.CryptoError: 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: else:
retData = True retData = True
self.decrypted = True self.decrypted = True

66
onionr/onionrcrypto.py Normal file → Executable file
View File

@ -18,7 +18,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
''' '''
import 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 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+ # secrets module was added into standard lib in 3.6+
if sys.version_info[0] == 3 and sys.version_info[1] < 6: if sys.version_info[0] == 3 and sys.version_info[1] < 6:
from dependencies import secrets from dependencies import secrets
@ -94,52 +94,41 @@ class OnionrCrypto:
retData = key.sign(data).signature retData = key.sign(data).signature
return retData 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)''' '''Encrypt to a public key (Curve25519, taken from base32 Ed25519 pubkey)'''
retVal = '' retVal = ''
try: box = None
pubkey = pubkey.encode() data = self._core._utils.strToBytes(data)
except AttributeError:
pass pubkey = nacl.signing.VerifyKey(pubkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_public_key()
if encodedData: if encodedData:
encoding = nacl.encoding.Base64Encoder encoding = nacl.encoding.Base64Encoder
else: else:
encoding = nacl.encoding.RawEncoder encoding = nacl.encoding.RawEncoder
if self.privKey != None and not anonymous: box = nacl.public.SealedBox(pubkey)
ownKey = nacl.signing.SigningKey(seed=self.privKey, encoder=nacl.encoding.Base32Encoder).to_curve25519_private_key() retVal = box.encrypt(data, encoder=encoding)
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 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)''' '''pubkey decrypt (Curve25519, taken from Ed25519 pubkey)'''
decrypted = False decrypted = False
if encodedData: if encodedData:
encoding = nacl.encoding.Base64Encoder encoding = nacl.encoding.Base64Encoder
else: else:
encoding = nacl.encoding.RawEncoder encoding = nacl.encoding.RawEncoder
ownKey = nacl.signing.SigningKey(seed=self.privKey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key() if privkey == '':
if self.privKey != None and not anonymous: privkey = self.privKey
ourBox = nacl.public.Box(ownKey, pubkey) ownKey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key()
decrypted = ourBox.decrypt(data, encoder=encoding)
elif anonymous: if self._core._utils.validatePubKey(privkey):
if self._core._utils.validatePubKey(privkey): privkey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key()
privkey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key() anonBox = nacl.public.SealedBox(privkey)
anonBox = nacl.public.SealedBox(privkey) else:
else: anonBox = nacl.public.SealedBox(ownKey)
anonBox = nacl.public.SealedBox(ownKey) decrypted = anonBox.decrypt(data, encoder=encoding)
decrypted = anonBox.decrypt(data, encoder=encoding)
return decrypted return decrypted
def symmetricEncrypt(self, data, key, encodedKey=False, returnEncoded=True): def symmetricEncrypt(self, data, key, encodedKey=False, returnEncoded=True):
@ -180,12 +169,6 @@ class OnionrCrypto:
decrypted = base64.b64encode(decrypted) decrypted = base64.b64encode(decrypted)
return 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): def generateSymmetric(self):
'''Generate a symmetric key (bytes) and return it''' '''Generate a symmetric key (bytes) and return it'''
return binascii.hexlify(nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE)) return binascii.hexlify(nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE))
@ -283,6 +266,15 @@ class OnionrCrypto:
@staticmethod @staticmethod
def safeCompare(one, two): 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) return hmac.compare_digest(one, two)
@staticmethod @staticmethod

77
onionr/onionrdaemontools.py Normal file → Executable file
View File

@ -18,9 +18,11 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
''' '''
import onionrexceptions, onionrpeers, onionrproofs, logger, onionrusers import onionrexceptions, onionrpeers, onionrproofs, logger
import base64, sqlite3, os import base64, sqlite3, os
from dependencies import secrets from dependencies import secrets
from utils import netutils
from onionrusers import onionrusers
class DaemonTools: class DaemonTools:
''' '''
@ -43,44 +45,50 @@ class DaemonTools:
else: else:
peer = self.daemon.pickOnlinePeer() peer = self.daemon.pickOnlinePeer()
ourID = self.daemon._core.hsAddress.strip() for x in range(1):
if x == 1 and self.daemon._core.config.get('i2p.host'):
url = 'http://' + peer + '/announce' ourID = self.daemon._core.config.get('i2p.own_addr').strip()
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
else: else:
self.announceCache[peer] = data['random'] ourID = self.daemon._core.hsAddress.strip()
if not announceFail:
logger.info('Announcing node to ' + url) url = 'http://' + peer + '/announce'
if self.daemon._core._utils.doPostRequest(url, data) == 'Success': data = {'node': ourID}
logger.info('Successfully introduced node to ' + peer)
retData = True combinedNodes = ourID + peer
self.daemon._core.setAddressInfo(peer, 'introduced', 1) if ourID != 1:
self.daemon._core.setAddressInfo(peer, 'powValue', data['random']) #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') self.daemon.decrementThreadCount('announceNode')
return retData return retData
def netCheck(self): def netCheck(self):
'''Check if we are connected to the internet or not when we can't connect to any peers''' '''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 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?') logger.warn('Network check failed, are you connected to the internet?')
self.daemon.isOnline = False self.daemon.isOnline = False
else: else:
@ -186,10 +194,11 @@ class DaemonTools:
def insertDeniableBlock(self): def insertDeniableBlock(self):
'''Insert a fake block in order to make it more difficult to track real blocks''' '''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 chance = 10
if secrets.randbelow(chance) == (chance - 1): if secrets.randbelow(chance) == (chance - 1):
fakePeer = self.daemon._core._crypto.generatePubKey()[0]
data = secrets.token_hex(secrets.randbelow(500) + 1) 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') self.daemon.decrementThreadCount('insertDeniableBlock')
return return

3
onionr/onionrevents.py Normal file → Executable file
View File

@ -61,10 +61,9 @@ def call(plugin, event_name, data = None, pluginapi = None):
try: try:
attribute = 'on_' + str(event_name).lower() attribute = 'on_' + str(event_name).lower()
# TODO: Use multithreading perhaps?
if hasattr(plugin, attribute): if hasattr(plugin, attribute):
#logger.debug('Calling event ' + str(event_name)) #logger.debug('Calling event ' + str(event_name))
getattr(plugin, attribute)(pluginapi) getattr(plugin, attribute)(pluginapi, data)
return True return True
except Exception as e: except Exception as e:

9
onionr/onionrexceptions.py Normal file → Executable file
View File

@ -44,6 +44,10 @@ class PasswordStrengthError(Exception):
pass pass
# block exceptions # block exceptions
class DifficultyTooLarge(Exception):
pass
class InvalidMetadata(Exception): class InvalidMetadata(Exception):
pass pass
@ -83,3 +87,8 @@ class DiskAllocationReached(Exception):
class MissingAddress(Exception): class MissingAddress(Exception):
pass pass
# Contact exceptions
class ContactDeleted(Exception):
pass

0
onionr/onionrpeers.py Normal file → Executable file
View File

0
onionr/onionrpluginapi.py Normal file → Executable file
View File

0
onionr/onionrplugins.py Normal file → Executable file
View File

22
onionr/onionrproofs.py Normal file → Executable file
View File

@ -41,9 +41,9 @@ def getDifficultyModifier(coreOrUtilsInst=None):
if percentUse >= 0.50: if percentUse >= 0.50:
retData += 1 retData += 1
elif percentUse >= 0.75: elif percentUse >= 0.75:
retData += 2 retData += 2
elif percentUse >= 0.95: elif percentUse >= 0.95:
retData += 3 retData += 3
return retData return retData
@ -64,11 +64,12 @@ def getDifficultyForNewBlock(data, ourBlock=True):
else: else:
raise ValueError('not Block, str, or int') raise ValueError('not Block, str, or int')
if ourBlock: if ourBlock:
minDifficulty = config.get('general.minimum_send_pow') minDifficulty = config.get('general.minimum_send_pow', 4)
else: 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 return retData
def getHashDifficulty(h): def getHashDifficulty(h):
@ -192,7 +193,7 @@ class DataPOW:
if not self.hashing: if not self.hashing:
break break
else: else:
time.sleep(2) time.sleep(1)
except KeyboardInterrupt: except KeyboardInterrupt:
self.shutdown() self.shutdown()
logger.warn('Got keyboard interrupt while waiting for POW result, stopping') logger.warn('Got keyboard interrupt while waiting for POW result, stopping')
@ -243,13 +244,11 @@ class POW:
self.reporting = reporting self.reporting = reporting
iFound = False # if current thread is the one that found the answer iFound = False # if current thread is the one that found the answer
answer = '' answer = ''
heartbeat = 200000
hbCount = 0 hbCount = 0
nonce = int(binascii.hexlify(nacl.utils.random(2)), 16)
while self.hashing: while self.hashing:
rand = nacl.utils.random()
#token = nacl.hash.blake2b(rand + self.data).decode() #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 payload = json.dumps(self.metadata).encode() + b'\n' + self.data
token = myCore._crypto.sha3Hash(payload) token = myCore._crypto.sha3Hash(payload)
try: try:
@ -262,6 +261,7 @@ class POW:
iFound = True iFound = True
self.result = payload self.result = payload
break break
nonce += 1
if iFound: if iFound:
endTime = math.floor(time.time()) endTime = math.floor(time.time())
@ -299,7 +299,7 @@ class POW:
if not self.hashing: if not self.hashing:
break break
else: else:
time.sleep(2) time.sleep(1)
except KeyboardInterrupt: except KeyboardInterrupt:
self.shutdown() self.shutdown()
logger.warn('Got keyboard interrupt while waiting for POW result, stopping') logger.warn('Got keyboard interrupt while waiting for POW result, stopping')

0
onionr/onionrsockets.py Normal file → Executable file
View File

View File

@ -55,6 +55,21 @@ def _dbFetch(coreInst, blockHash):
conn.close() conn.close()
return None 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=''): def store(coreInst, data, blockHash=''):
assert isinstance(coreInst, core.Core) assert isinstance(coreInst, core.Core)
assert coreInst._utils.validateHash(blockHash) assert coreInst._utils.validateHash(blockHash)

View 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)

View File

@ -40,12 +40,18 @@ class OnionrUser:
Takes an instance of onionr core, a base32 encoded ed25519 public key, and a bool saveUser 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. 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.trust = 0
self._core = coreInst self._core = coreInst
self.publicKey = publicKey self.publicKey = publicKey
if saveUser: if saveUser:
self._core.addPeer(publicKey) try:
self._core.addPeer(publicKey)
except AssertionError:
pass
self.trust = self._core.getPeerInfo(self.publicKey, 'trust') self.trust = self._core.getPeerInfo(self.publicKey, 'trust')
return return
@ -73,7 +79,7 @@ class OnionrUser:
encrypted = coreInst._crypto.pubKeyEncrypt(data, self.publicKey, encodedData=True) encrypted = coreInst._crypto.pubKeyEncrypt(data, self.publicKey, encodedData=True)
return encrypted return encrypted
def decrypt(self, data, anonymous=True): def decrypt(self, data):
decrypted = coreInst._crypto.pubKeyDecrypt(data, self.publicKey, encodedData=True) decrypted = coreInst._crypto.pubKeyDecrypt(data, self.publicKey, encodedData=True)
return decrypted return decrypted
@ -81,7 +87,7 @@ class OnionrUser:
retData = '' retData = ''
forwardKey = self._getLatestForwardKey() forwardKey = self._getLatestForwardKey()
if self._core._utils.validatePubKey(forwardKey): 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: else:
raise onionrexceptions.InvalidPubkey("No valid forward secrecy key available for this user") raise onionrexceptions.InvalidPubkey("No valid forward secrecy key available for this user")
#self.generateForwardKey() #self.generateForwardKey()
@ -91,7 +97,7 @@ class OnionrUser:
retData = "" retData = ""
for key in self.getGeneratedForwardKeys(False): for key in self.getGeneratedForwardKeys(False):
try: 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: except nacl.exceptions.CryptoError:
retData = False retData = False
else: else:

246
onionr/onionrutils.py Normal file → Executable file
View File

@ -18,14 +18,15 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
''' '''
# Misc functions that do not fit in the main api, but are useful # Misc functions that do not fit in the main api, but are useful
import getpass, sys, 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 import nacl.signing, nacl.encoding
from onionrblockapi import Block from onionrblockapi import Block
import onionrexceptions import onionrexceptions
from onionr import API_VERSION from onionr import API_VERSION
import onionrevents import onionrevents
import onionrusers, storagecounter import storagecounter
from etc import pgpwords from etc import pgpwords
from onionrusers import onionrusers
if sys.version_info < (3, 6): if sys.version_info < (3, 6):
try: try:
import sha3 import sha3
@ -68,90 +69,6 @@ class OnionrUtils:
epoch = self.getEpoch() epoch = self.getEpoch()
return epoch - (epoch % roundS) 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): def getClientAPIServer(self):
retData = '' retData = ''
try: try:
@ -163,7 +80,7 @@ class OnionrUtils:
retData += '%s:%s' % (hostname, config.get('client.client.port')) retData += '%s:%s' % (hostname, config.get('client.client.port'))
return retData 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. 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) payload = 'http://%s/%s%s' % (hostname, command, data)
try: try:
if post: 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: 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: except Exception as error:
if not silent: if not silent:
logger.error('Failed to make local request (command: %s):%s' % (command, error)) logger.error('Failed to make local request (command: %s):%s' % (command, error))
@ -195,38 +112,23 @@ class OnionrUtils:
return retData 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=''): def getHumanReadableID(self, pub=''):
'''gets a human readable ID from a public key''' '''gets a human readable ID from a public key'''
if pub == '': if pub == '':
pub = self._core._crypto.pubKey pub = self._core._crypto.pubKey
pub = base64.b16encode(base64.b32decode(pub)).decode() 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): 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 = {} meta = {}
metadata = {} metadata = {}
@ -251,34 +153,6 @@ class OnionrUtils:
meta = metadata['meta'] meta = metadata['meta']
return (metadata, meta, data) 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): def processBlockMetadata(self, blockHash):
''' '''
Read metadata from a block and cache it to the block database Read metadata from a block and cache it to the block database
@ -309,7 +183,8 @@ class OnionrUtils:
else: else:
self._core.updateBlockInfo(blockHash, 'expire', expireTime) self._core.updateBlockInfo(blockHash, 'expire', expireTime)
else: 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): def escapeAnsi(self, line):
''' '''
@ -320,21 +195,6 @@ class OnionrUtils:
ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]') ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]')
return ansi_escape.sub('', line) 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): def hasBlock(self, hash):
''' '''
Check for new block in the list Check for new block in the list
@ -361,7 +221,7 @@ class OnionrUtils:
def validateHash(self, data, length=64): 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 retVal = True
if data == False or data == True: if data == False or data == True:
@ -450,6 +310,8 @@ class OnionrUtils:
Validate if a string is a valid base32 encoded Ed25519 key Validate if a string is a valid base32 encoded Ed25519 key
''' '''
retVal = False retVal = False
if type(key) is type(None):
return False
try: try:
nacl.signing.SigningKey(seed=key, encoder=nacl.encoding.Base32Encoder) nacl.signing.SigningKey(seed=key, encoder=nacl.encoding.Base32Encoder)
except nacl.exceptions.ValueError: except nacl.exceptions.ValueError:
@ -460,15 +322,6 @@ class OnionrUtils:
retVal = True retVal = True
return retVal 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): def validateID(self, id):
''' '''
Validate if an address is a valid tor or i2p hidden service Validate if an address is a valid tor or i2p hidden service
@ -513,36 +366,22 @@ class OnionrUtils:
retVal = False retVal = False
# Validate address is valid base32 (when capitalized and minus extension); v2/v3 onions and .b32.i2p use base32 # Validate address is valid base32 (when capitalized and minus extension); v2/v3 onions and .b32.i2p use base32
try: for x in idNoDomain.upper():
base64.b32decode(idNoDomain.upper().encode()) if x not in string.ascii_uppercase and x not in '234567':
except binascii.Error: retVal = False
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
return retVal return retVal
except: except:
return False return False
def getPeerByHashId(self, hash): def isIntegerString(self, data):
''' '''Check if a string is a valid base10 integer (also returns true if already an int)'''
Return the pubkey of the user if known from the hash try:
''' int(data)
if self._core._crypto.pubKeyHashID() == hash: except (ValueError, TypeError) as e:
retData = self._core._crypto.pubKey return False
return retData else:
conn = sqlite3.connect(self._core.peerDB) return True
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 isCommunicatorRunning(self, timeout = 5, interval = 0.1): def isCommunicatorRunning(self, timeout = 5, interval = 0.1):
try: try:
@ -564,13 +403,6 @@ class OnionrUtils:
except: except:
return False return False
def token(self, size = 32):
'''
Generates a secure random hex encoded token
'''
return binascii.hexlify(os.urandom(size))
def importNewBlocks(self, scanDir=''): def importNewBlocks(self, scanDir=''):
''' '''
This function is intended to scan for new blocks ON THE DISK and import them This function is intended to scan for new blocks ON THE DISK and import them
@ -692,22 +524,6 @@ class OnionrUtils:
pass pass
return data 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='.'): def size(path='.'):
''' '''
Returns the size of a folder's contents in bytes Returns the size of a folder's contents in bytes

0
onionr/static-data/bootstrap-nodes.txt Normal file → Executable file
View File

2
onionr/static-data/connect-check.txt Normal file → Executable file
View 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
View File

0
onionr/static-data/default-plugins/cliui/main.py Normal file → Executable file
View File

0
onionr/static-data/default-plugins/encrypt/info.json Normal file → Executable file
View File

4
onionr/static-data/default-plugins/encrypt/main.py Normal file → Executable file
View File

@ -69,7 +69,7 @@ class PlainEncryption:
data['data'] = plaintext data['data'] = plaintext
data = json.dumps(data) data = json.dumps(data)
plaintext = 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) encrypted = self.api.get_core()._utils.bytesToStr(encrypted)
logger.info('Encrypted Message: \n\nONIONR ENCRYPTED DATA %s END ENCRYPTED DATA' % (encrypted,)) logger.info('Encrypted Message: \n\nONIONR ENCRYPTED DATA %s END ENCRYPTED DATA' % (encrypted,))
@ -88,7 +88,7 @@ class PlainEncryption:
return return
encrypted = data.replace('ONIONR ENCRYPTED DATA ', '').replace('END ENCRYPTED DATA', '') encrypted = data.replace('ONIONR ENCRYPTED DATA ', '').replace('END ENCRYPTED DATA', '')
myPub = self.api.get_core()._crypto.pubKey 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: if decrypted == False:
logger.error("Decryption failed") logger.error("Decryption failed")
else: else:

0
onionr/static-data/default-plugins/flow/info.json Normal file → Executable file
View File

0
onionr/static-data/default-plugins/flow/main.py Normal file → Executable file
View File

View File

View File

@ -41,7 +41,7 @@ def _processForwardKey(api, myBlock):
else: else:
raise onionrexceptions.InvalidPubkey("%s is nota valid pubkey key" % (key,)) raise onionrexceptions.InvalidPubkey("%s is nota valid pubkey key" % (key,))
def on_processblocks(api): def on_processblocks(api, data=None):
# Generally fired by utils. # Generally fired by utils.
myBlock = api.data['block'] myBlock = api.data['block']
blockType = api.data['type'] blockType = api.data['type']

0
onionr/static-data/default-plugins/pluginmanager/.gitignore vendored Normal file → Executable file
View File

View File

View File

View File

View File

0
onionr/static-data/default-plugins/pms/info.json Normal file → Executable file
View File

103
onionr/static-data/default-plugins/pms/main.py Normal file → Executable file
View File

@ -21,8 +21,9 @@
# Imports some useful libraries # Imports some useful libraries
import logger, config, threading, time, readline, datetime import logger, config, threading, time, readline, datetime
from onionrblockapi import Block from onionrblockapi import Block
import onionrexceptions, onionrusers import onionrexceptions
import locale, sys, os from onionrusers import onionrusers
import locale, sys, os, json
locale.setlocale(locale.LC_ALL, '') locale.setlocale(locale.LC_ALL, '')
@ -48,14 +49,14 @@ class MailStrings:
self.mailInstance = mailInstance self.mailInstance = mailInstance
self.programTag = 'OnionrMail v%s' % (PLUGIN_VERSION) 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.mainMenuChoices = choices
self.mainMenu = '''\n self.mainMenu = '''-----------------
----------------- 1. %s
1. %s 2. %s
2. %s 3. %s
3. %s 4. %s
4. %s''' % (choices[0], choices[1], choices[2], choices[3]) 5. %s''' % (choices[0], choices[1], choices[2], choices[3], choices[4])
class OnionrMail: class OnionrMail:
def __init__(self, pluginapi): def __init__(self, pluginapi):
@ -65,6 +66,7 @@ class OnionrMail:
self.sentboxTools = sentboxdb.SentBox(self.myCore) self.sentboxTools = sentboxdb.SentBox(self.myCore)
self.sentboxList = [] self.sentboxList = []
self.sentMessages = {} self.sentMessages = {}
self.doSigs = True
return return
def inbox(self): def inbox(self):
@ -133,18 +135,30 @@ class OnionrMail:
else: else:
cancel = '' cancel = ''
readBlock.verifySig() readBlock.verifySig()
senderDisplay = self.myCore._utils.bytesToStr(readBlock.signer)
logger.info('Message recieved from %s' % (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) logger.info('Valid signature: %s' % readBlock.validSig)
if not 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).') cancel = logger.readline('Press enter to continue to message, or -q to not open the message (recommended).')
print('')
if cancel != '-q': if cancel != '-q':
print(draw_border(self.myCore._utils.escapeAnsi(readBlock.bcontent.decode().strip()))) try:
reply = logger.readline("Press enter to continue, or enter %s to reply" % ("-r",)) print(draw_border(self.myCore._utils.escapeAnsi(readBlock.bcontent.decode().strip())))
if reply == "-r": except ValueError:
self.draftMessage(self.myCore._utils.bytesToStr(readBlock.signer,)) 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 return
def sentbox(self): def sentbox(self):
@ -153,7 +167,7 @@ class OnionrMail:
''' '''
entering = True entering = True
while entering: while entering:
self.getSentList() self.get_sent_list()
logger.info('Enter a block number or -q to return') logger.info('Enter a block number or -q to return')
try: try:
choice = input('>') choice = input('>')
@ -180,18 +194,19 @@ class OnionrMail:
return return
def getSentList(self): def get_sent_list(self, display=True):
count = 1 count = 1
self.sentboxList = [] self.sentboxList = []
self.sentMessages = {} self.sentMessages = {}
for i in self.sentboxTools.listSent(): for i in self.sentboxTools.listSent():
self.sentboxList.append(i['hash']) self.sentboxList.append(i['hash'])
self.sentMessages[i['hash']] = (i['message'], i['peer']) self.sentMessages[i['hash']] = (self.myCore._utils.bytesToStr(i['message']), i['peer'], i['subject'])
if display:
logger.info('%s. %s - %s - %s' % (count, i['hash'], i['peer'][:12], i['date'])) logger.info('%s. %s - %s - (%s) - %s' % (count, i['hash'], i['peer'][:12], i['subject'], i['date']))
count += 1 count += 1
return json.dumps(self.sentMessages)
def draftMessage(self, recip=''): def draft_message(self, recip=''):
message = '' message = ''
newLine = '' newLine = ''
subject = '' subject = ''
@ -237,14 +252,26 @@ class OnionrMail:
if not cancelEnter: if not cancelEnter:
logger.info('Inserting encrypted message as Onionr block....') logger.info('Inserting encrypted message as Onionr block....')
blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=True, meta={'subject': subject}) blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=self.doSigs, meta={'subject': subject})
self.sentboxTools.addToSent(blockID, recip, message)
def toggle_signing(self):
self.doSigs = not self.doSigs
def menu(self): def menu(self):
choice = '' choice = ''
while True: 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: try:
choice = logger.readline('Enter 1-%s:\n' % (len(self.strings.mainMenuChoices))).lower().strip() choice = logger.readline('Enter 1-%s:\n' % (len(self.strings.mainMenuChoices))).lower().strip()
except (KeyboardInterrupt, EOFError): except (KeyboardInterrupt, EOFError):
@ -255,8 +282,10 @@ class OnionrMail:
elif choice in (self.strings.mainMenuChoices[1], '2'): elif choice in (self.strings.mainMenuChoices[1], '2'):
self.sentbox() self.sentbox()
elif choice in (self.strings.mainMenuChoices[2], '3'): elif choice in (self.strings.mainMenuChoices[2], '3'):
self.draftMessage() self.draft_message()
elif choice in (self.strings.mainMenuChoices[3], '4'): elif choice in (self.strings.mainMenuChoices[3], '4'):
self.toggle_signing()
elif choice in (self.strings.mainMenuChoices[4], '5'):
logger.info('Goodbye.') logger.info('Goodbye.')
break break
elif choice == '': elif choice == '':
@ -265,6 +294,26 @@ class OnionrMail:
logger.warn('Invalid choice.') logger.warn('Invalid choice.')
return 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): def on_init(api, data = None):
''' '''

9
onionr/static-data/default-plugins/pms/sentboxdb.py Normal file → Executable file
View File

@ -37,6 +37,7 @@ class SentBox:
hash id not null, hash id not null,
peer text not null, peer text not null,
message text not null, message text not null,
subject text not null,
date int not null date int not null
); );
''') ''')
@ -46,12 +47,12 @@ class SentBox:
def listSent(self): def listSent(self):
retData = [] retData = []
for entry in self.cursor.execute('SELECT * FROM sent;'): 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 return retData
def addToSent(self, blockID, peer, message): def addToSent(self, blockID, peer, message, subject=''):
args = (blockID, peer, message, self.core._utils.getEpoch()) args = (blockID, peer, message, subject, self.core._utils.getEpoch())
self.cursor.execute('INSERT INTO sent VALUES(?, ?, ?, ?)', args) self.cursor.execute('INSERT INTO sent VALUES(?, ?, ?, ?, ?)', args)
self.conn.commit() self.conn.commit()
return return

7
onionr/static-data/default_config.json Normal file → Executable file
View File

@ -2,8 +2,8 @@
"general" : { "general" : {
"dev_mode" : true, "dev_mode" : true,
"display_header" : false, "display_header" : false,
"minimum_block_pow": 1, "minimum_block_pow": 4,
"minimum_send_pow": 1, "minimum_send_pow": 4,
"socket_servers": false, "socket_servers": false,
"security_level": 0, "security_level": 0,
"max_block_age": 2678400, "max_block_age": 2678400,
@ -21,8 +21,7 @@
"private" : { "private" : {
"run" : true, "run" : true,
"path" : "static-data/www/private/", "path" : "static-data/www/private/",
"guess_mime" : true, "guess_mime" : true
"timing_protection" : true
}, },
"ui" : { "ui" : {

0
onionr/static-data/default_plugin.py Normal file → Executable file
View File

0
onionr/static-data/header.txt Normal file → Executable file
View File

0
onionr/static-data/index.html Normal file → Executable file
View File

File diff suppressed because one or more lines are too long

View File

@ -2,6 +2,101 @@
padding-top: 1em; padding-top: 1em;
} }
.threads div span{ .threads div span{
<<<<<<< HEAD
padding-left: 0.5em; padding-left: 0.5em;
padding-right: 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
} }

View File

@ -1,3 +1,4 @@
<<<<<<< HEAD
pms = '' pms = ''
threadPart = document.getElementById('threads') threadPart = document.getElementById('threads')
function getInbox(){ function getInbox(){
@ -38,6 +39,183 @@ function getInbox(){
}.bind([pms, i])) }.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', { fetch('/getblocksbytype/pm', {
@ -47,6 +225,43 @@ fetch('/getblocksbytype/pm', {
.then((resp) => resp.text()) // Transform the data into json .then((resp) => resp.text()) // Transform the data into json
.then(function(data) { .then(function(data) {
pms = data.split(',') pms = data.split(',')
<<<<<<< HEAD
getInbox(pms) 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

View 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;
}

View File

@ -21,6 +21,7 @@
<br><br><a class='idLink' href='/mail/'>Mail</a> <br><br><a class='idLink' href='/mail/'>Mail</a>
<h2>Stats</h2> <h2>Stats</h2>
<p>Uptime: <span id='uptime'></span></p> <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>Stored Blocks: <span id='storedBlocks'></span></p>
<p>Blocks in queue: <span id='blockQueue'></span></p> <p>Blocks in queue: <span id='blockQueue'></span></p>
<p>Connected nodes:</p> <p>Connected nodes:</p>

View File

@ -21,6 +21,10 @@ uptimeDisplay = document.getElementById('uptime')
connectedDisplay = document.getElementById('connectedNodes') connectedDisplay = document.getElementById('connectedNodes')
storedBlockDisplay = document.getElementById('storedBlocks') storedBlockDisplay = document.getElementById('storedBlocks')
queuedBlockDisplay = document.getElementById('blockQueue') queuedBlockDisplay = document.getElementById('blockQueue')
<<<<<<< HEAD
=======
lastIncoming = document.getElementById('lastIncoming')
>>>>>>> contacts
function getStats(){ function getStats(){
stats = JSON.parse(httpGet('getstats', webpass)) stats = JSON.parse(httpGet('getstats', webpass))
@ -28,5 +32,18 @@ function getStats(){
connectedDisplay.innerText = stats['connectedNodes'] connectedDisplay.innerText = stats['connectedNodes']
storedBlockDisplay.innerText = stats['blockCount'] storedBlockDisplay.innerText = stats['blockCount']
queuedBlockDisplay.innerText = stats['blockQueueCount'] 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() getStats()

View File

@ -37,7 +37,11 @@ body{
align-items:center; align-items:center;
} }
.logo{ .logo{
<<<<<<< HEAD
max-width: 25%; max-width: 25%;
=======
max-width: 20%;
>>>>>>> contacts
vertical-align: middle; vertical-align: middle;
} }
.logoText{ .logoText{
@ -132,9 +136,31 @@ body{
left: 0px; left: 0px;
top: 0px; top: 0px;
width:100%; width:100%;
<<<<<<< HEAD
opacity: 0.9; opacity: 0.9;
height:100%; height:100%;
text-align:center; text-align:center;
z-index: 1000; z-index: 1000;
background-color: black; 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

View File

@ -1,5 +1,30 @@
<<<<<<< HEAD
webpass = document.location.hash.replace('#', '') webpass = document.location.hash.replace('#', '')
nowebpass = false 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"){ if (typeof webpass == "undefined"){
webpass = localStorage['webpass'] webpass = localStorage['webpass']
} }
@ -12,6 +37,13 @@ if (typeof webpass == "undefined" || webpass == ""){
nowebpass = true nowebpass = true
} }
<<<<<<< HEAD
=======
function arrayContains(needle, arrhaystack) {
return (arrhaystack.indexOf(needle) > -1);
}
>>>>>>> contacts
function httpGet(theUrl) { function httpGet(theUrl) {
var xmlHttp = new XMLHttpRequest() var xmlHttp = new XMLHttpRequest()
xmlHttp.open( "GET", theUrl, false ) // false for synchronous request xmlHttp.open( "GET", theUrl, false ) // false for synchronous request
@ -27,6 +59,10 @@ function httpGet(theUrl) {
function overlay(overlayID) { function overlay(overlayID) {
el = document.getElementById(overlayID) el = document.getElementById(overlayID)
el.style.visibility = (el.style.visibility == "visible") ? "hidden" : "visible" el.style.visibility = (el.style.visibility == "visible") ? "hidden" : "visible"
<<<<<<< HEAD
=======
scroll(0,0)
>>>>>>> contacts
} }
var passLinks = document.getElementsByClassName("idLink") var passLinks = document.getElementsByClassName("idLink")

View File

@ -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

View File

@ -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">&times;</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>

View File

@ -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>

View File

@ -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 -->

View File

@ -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 -->

View File

@ -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 -->

View File

@ -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 -->

View File

@ -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)

View File

@ -1,4 +0,0 @@
{
"language" : "eng",
"python_tags" : true
}

View File

@ -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%;
}

View File

@ -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

View File

@ -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">&times;</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">&times;</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>

View File

@ -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();

View File

@ -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" : "回复"
}
}

View File

@ -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%;
}

View File

@ -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

View File

@ -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">&times;</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>

View File

@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/* 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();

View File

@ -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();

Some files were not shown because too many files have changed in this diff Show More