finish merging new-main

master
Kevin Froman 2019-03-13 17:00:51 -05:00
commit 97e0945e12
76 changed files with 2456 additions and 1058 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@ onionr/data/config.ini
onionr/data/*.db onionr/data/*.db
onionr/data-old/* onionr/data-old/*
onionr/data* onionr/data*
onionr/testdata
onionr/*.pyc onionr/*.pyc
onionr/*.log onionr/*.log
onionr/data/hs/hostname onionr/data/hs/hostname

0
.gitlab-ci.yml Normal file → Executable file
View File

0
.gitmodules vendored
View File

View File

@ -1,5 +1,4 @@
Onionr Logo is licensed under Creative Commons Attribution-Share Alike 3.0 Unported The Onionr logo was created by [Anhar Ismail](https://github.com/anharismail) under the [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/).
https://creativecommons.org/licenses/by-sa/4.0/
GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE

View File

@ -1,3 +1,6 @@
ONIONR_HOME ?= data
all:;: '$(ONIONR_HOME)'
PREFIX = /usr/local PREFIX = /usr/local
.DEFAULT_GOAL := setup .DEFAULT_GOAL := setup

View File

@ -31,43 +31,79 @@ Onionr can be used for mail, as a social network, instant messenger, file sharin
## Main Features ## Main Features
* [X] Fully p2p/decentralized, no trackers or other single points of failure * [X] 🌐 Fully p2p/decentralized, no trackers or other single points of failure
* [X] End to end encryption of user data * [X] 🔒 End to end encryption of user data
* [X] Optional non-encrypted blocks, useful for blog posts or public file sharing * [X] 📢 Optional non-encrypted blocks, useful for blog posts or public file sharing
* [X] Easy API system for integration to websites * [X] 💻 Easy API system for integration to websites
* [X] Metadata analysis resistance * [X] 🕵️ Metadata analysis resistance and anonymity
* [X] Transport agnosticism (no internet required) * [X] 📡 Transport agnosticism (no internet required)
**Onionr API and functionality is subject to non-backwards compatible change during pre-alpha development** **Onionr API and functionality is subject to non-backwards compatible change during pre-alpha development**
# Screenshots
<img alt='Node statistics page screenshot' src='docs/onionr-1.png' width=600>
Node statistics
<img alt='Friend/contact manager screenshot' src='docs/onionr-2.png' width=600>
Friend/contact manager
<img alt='Encrypted, metadata-masking mail application screenshot' src='docs/onionr-3.png' width=600>
Encrypted, metadata-masking mail application.
# Install and Run on Linux # Install and Run on Linux
The following applies to Ubuntu Bionic. Other distros may have different package or command names. The following applies to Ubuntu Bionic. Other distros may have different package or command names.
* Have python3.5+, python3-pip, Tor (daemon, not browser) installed (python3-dev recommended) * Have python3.6+, python3-pip, Tor (daemon, not browser) installed (python3-dev recommended)
* Clone the git repo: `$ git clone https://gitlab.com/beardog/onionr` * Clone the git repo: `$ git clone https://gitlab.com/beardog/onionr`
* cd into install direction: `$ cd onionr/` * cd into install direction: `$ cd onionr/`
* Install the Python dependencies ([virtualenv strongly recommended](https://virtualenv.pypa.io/en/stable/userguide/)): `$ pip3 install -r requirements.txt` * Install the Python dependencies ([virtualenv strongly recommended](https://virtualenv.pypa.io/en/stable/userguide/)): `$ pip3 install --require-hashes -r requirements.txt`
(--require-hashes is intended to prevent exploitation via compromise of Pypi/CA certificates)
## Help out ## Help out
Everyone is welcome to help out. Help is wanted for the following: Everyone is welcome to help out. Help is wanted for the following:
* Development (Get in touch first) * Development (Get in touch first)
* Creation of a lib for use from other languages and faster proof-of-work * Creation of a shared lib for use from other languages and faster proof-of-work
* Android and IOS development * Android and IOS development
* Windows and Mac support * Windows and Mac support (already partially supported, testers needed)
* General bug fixes and development of new features * General bug fixes and development of new features
* Testing * Testing
* UI/UX design
* Running stable nodes * Running stable nodes
* Security review/audit * Security review/audit
* Automatic I2P setup * Automatic I2P setup
Bitcoin: [1onion55FXzm6h8KQw3zFw2igpHcV7LPq](bitcoin:1onion55FXzm6h8KQw3zFw2igpHcV7LPq) Contribute money:
USD: [Ko-Fi](https://www.ko-fi.com/beardogkf)
Donating at least $5 gets you cool Onionr stickers. Get in touch if you want them.
Bitcoin: [1onion55FXzm6h8KQw3zFw2igpHcV7LPq](bitcoin:1onion55FXzm6h8KQw3zFw2igpHcV7LPq) (Contact us for privacy coins like Monero)
USD (Card/Paypal): [Ko-Fi](https://www.ko-fi.com/beardogkf)
Note: probably not tax deductible
## Contact
beardog [ at ] mailbox.org
## Disclaimer ## Disclaimer
The Tor Project, I2P developers, and anyone else do not own, create, or endorse this project, and are not otherwise involved. The Tor Project and I2P developers do not own, create, or endorse this project, and are not otherwise involved.
Tor is a trademark for the Tor Project. We do not own it.
The 'open source badge' is by Maik Ellerbrock and is licensed under a Creative Commons Attribution 4.0 International License. The 'open source badge' is by Maik Ellerbrock and is licensed under a Creative Commons Attribution 4.0 International License.
## Logo
The Onionr logo was created by [Anhar Ismail](https://github.com/anharismail) under the [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/).
If you modify and redistribute our code ("forking"), please use a different logo and project name to avoid confusion. Please do not use our logo in a way that makes it seem like we endorse you without permission.

BIN
docs/network-comparison.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
docs/onionr-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
docs/onionr-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
docs/onionr-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -1,25 +1,25 @@
<p align="center"> <p align="center">
<img src="onionr-logo.png" alt="<h1>Onionr</h1>"> <img src="onionr-logo.png" alt="<h1>Onionr</h1>" width=200>
</p> </p>
<p align="center">Anonymous, Decentralized, Distributed Network</p> <p align="center">Anonymous, Decentralized, Distributed Network</p>
# Introduction # Introduction
One of the most important things in the modern world is information. The ability to communicate freely with others is crucial for maintaining personal liberties. The internet has provided humanity with the ability to spread information globally, but there are many people who try (and sometimes succeed) to stifle the flow of information. We believe that the ability to communicate freely with others is crucial for maintaining societal and personal liberty. The internet has provided humanity with the ability to spread information globally, but there are many persons and organizations who try to stifle the flow of information, sometimes with success.
Internet censorship comes in many forms, state censorship, corporate consolidation of media, threats of violence, network exploitation (e.g. denial of service attacks). Internet censorship comes in many forms, state censorship, corporate consolidation of media, threats of violence, network exploitation (e.g. denial of service attacks) and other threats.
To prevent censorship or loss of information, these measures must be in place: We hold that in order to protect individual privacy, users must have the ability to communicate anonymously and with decentralization.
* Resistance to censorship of underlying infrastructure or of network hosts We believe that in order to prevent censorship and loss of information, these measures must be in place:
* Resistance to censorship of underlying infrastructure or of particular network hosts
* Anonymization of users by default * Anonymization of users by default
* The Inability to coerce human users (personal threats/"doxxing", or totalitarian regime censorship) * The Inability to coerce users (personal threats/"doxxing", or totalitarian regime censorship)
* Economic availability. A system should not rely on a single device to be constantly online, and should not be overly expensive to use. The majority of people in the world own cell phones, but comparatively few own personal computers, particularly in developing countries. Internet connectivity can be slow or spotty in many areas. * Economic availability. A system should not rely on a single device to be constantly online, and should not be overly expensive to use. The majority of people in the world own cell phones, but comparatively few own personal computers, particularly in developing countries. Internet connectivity can be slow or spotty in many areas.
There are many great projects that tackle decentralization and privacy issues, but there are none which tackle all of the above issues. Some of the existing networks have also not worked well in practice, or are more complicated than they need to be.
# Onionr Design Goals # Onionr Design Goals
When designing Onionr we had these main goals in mind: When designing Onionr we had these main goals in mind:
@ -91,6 +91,54 @@ In addition, randomness beacons such as the one operated by [NIST](https://beaco
# Direct Connections # Direct Connections
We propose a method of using Onionr's block sync system to enable direct connections between peers by having one peer request to connect to another using the peer's public key. Since the request is within a standard block, proof of work must be used to request connection. If the requested peer is available and wishes to accept the connection,Onionr will generate a temporary .onion address for the other peer to connect to. Alternatively, a reverse connection may be formed, which is faster to establish but requires a message brokering system instead of a standard socket. We propose a method of using Onionr's block sync system to enable direct connections between peers by having one peer request to connect to another using the peer's public key. Since the request is within a standard block, proof of work must be used to request connection. If the requested peer is available and wishes to accept the connection, Onionr will generate a temporary .onion address for the other peer to connect to. Alternatively, a reverse connection may be formed, which is faster to establish but requires a message brokering system instead of a standard socket.
The benefits of such a system are increased privacy, and the ability to anonymously communicate from multiple devices at once. In a traditional onion service, one's online status can be monitored and more easily correlated. The benefits of such a system are increased privacy, and the ability to anonymously communicate from multiple devices at once. In a traditional onion service, one's online status can be monitored and more easily correlated.
# Threat Model
The goal of Onionr is to provide a method of distributing information in a manner in which the difficulty of discovering the identity of those sending and receiving the information is greatly increased. In this section we detail what information we want to protect and who we're protecting it from.
In this threat model, "protected" means available in plaintext only to those which it was intended, and regardless non-malleable
## Threat Actors
Onionr assumes that traffic/data is being surveilled by powerful actors on every level but the user's device.
We also assume that the actors are capable of the following:
* Running tens of thousands of Onionr nodes
* Surveiling most of the Tor and I2P networks
## Protected Data
We seek to protect the following information:
* Contents of private data. E.g. 'mail' messages and secret files
* Relationship metadata. Unless something is desired to be published publicly, we seek to hide the creator and recipients of such data.
* Physical location/IP address of nodes on the network
* All block data from tampering
### Data we cannot or do not protect
* Data specifically inserted as plaintext is available to the public
* The public key of signed plaintext blocks
* The fact that one is using Tor or I2P
* The fact that one is using Onionr specifically can likely be discovered using long term traffic analysis
* Intense traffic analysis may be able to discover what node created a block. For this reason we offer a high security setting to only share blocks via uploads that we recommend for those who need the best privacy.
## Assumptions
We assume that Tor onion services (v3) and I2P services cannot be trivially deanonymized, and that the underlying cryptographic primitives we employ cannot be broken in any manner faster than brute force unless a quantum computer is used.
Once quantum safe algorithms are more mature and have decent high level libraries, they will be deployed.
# Comparisons to other P2P software
Since Onionr is far from the first to implement many of these ideas (on their own), this section compares Onionr to other networks, using points we consider to be the most important.
![network comparison image](network-comparison.png)
# Conclusion
If successful, Onionr will be a complete decentralized platform for anonymous computing, complete with limited metadata exposure, both node and user anonymity, and spam prevention

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

View File

@ -21,10 +21,13 @@ from gevent.pywsgi import WSGIServer, WSGIHandler
from gevent import Timeout from gevent import Timeout
import flask, cgi, uuid import flask, cgi, uuid
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, base64, time, os, json, socket
import core import core
from onionrblockapi import Block 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
import httpapi
from httpapi import friendsapi, simplecache
import onionr
class FDSafeHandler(WSGIHandler): class FDSafeHandler(WSGIHandler):
'''Our WSGI handler. Doesn't do much non-default except timeouts''' '''Our WSGI handler. Doesn't do much non-default except timeouts'''
@ -38,9 +41,9 @@ class FDSafeHandler(WSGIHandler):
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)'''
if config.get('general.random_bind_ip', True):
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))]
data = '.'.join(hostOctets) data = '.'.join(hostOctets)
# Try to bind IP. Some platforms like Mac block non normal 127.x.x.x # Try to bind IP. Some platforms like Mac block non normal 127.x.x.x
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try: try:
@ -50,10 +53,10 @@ def setBindIP(filePath):
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()
else:
data = '127.0.0.1'
with open(filePath, 'w') as bindFile: with open(filePath, 'w') as bindFile:
bindFile.write(data) bindFile.write(data)
return data return data
class PublicAPI: class PublicAPI:
@ -173,7 +176,7 @@ class PublicAPI:
try: try:
newNode = request.form['node'].encode() newNode = request.form['node'].encode()
except KeyError: except KeyError:
logger.warn('No block specified for upload') logger.warn('No node specified for upload')
pass pass
else: else:
try: try:
@ -230,7 +233,7 @@ class PublicAPI:
while self.torAdder == '': while self.torAdder == '':
clientAPI._core.refreshFirstStartVars() clientAPI._core.refreshFirstStartVars()
self.torAdder = clientAPI._core.hsAddress self.torAdder = clientAPI._core.hsAddress
time.sleep(1) time.sleep(0.1)
self.httpServer = WSGIServer((self.host, self.bindPort), app, log=None, handler_class=FDSafeHandler) self.httpServer = WSGIServer((self.host, self.bindPort), app, log=None, handler_class=FDSafeHandler)
self.httpServer.serve_forever() self.httpServer.serve_forever()
@ -248,9 +251,6 @@ class API:
This initilization defines all of the API entry points and handlers for the endpoints and errors This initilization defines all of the API entry points and handlers for the endpoints and errors
This also saves the used host (random localhost IP address) to the data folder in host.txt This also saves the used host (random localhost IP address) to the data folder in host.txt
''' '''
# assert isinstance(onionrInst, onionr.Onionr)
# configure logger and stuff
onionr.Onionr.setupConfig('data/', self = self)
self.debug = debug self.debug = debug
self._core = onionrInst.onionrCore self._core = onionrInst.onionrCore
@ -262,7 +262,7 @@ class API:
self.bindPort = bindPort self.bindPort = bindPort
# Be extremely mindful of this. These are endpoints available without a password # 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', 'friends', 'friendsindex')
self.clientToken = config.get('client.webpassword') self.clientToken = config.get('client.webpassword')
self.timeBypassToken = base64.b16encode(os.urandom(32)).decode() self.timeBypassToken = base64.b16encode(os.urandom(32)).decode()
@ -276,6 +276,9 @@ class API:
self.pluginResponses = {} # Responses for plugin endpoints self.pluginResponses = {} # Responses for plugin endpoints
self.queueResponse = {} self.queueResponse = {}
onionrInst.setClientAPIInst(self) onionrInst.setClientAPIInst(self)
app.register_blueprint(friendsapi.friends)
app.register_blueprint(simplecache.simplecache)
httpapi.load_plugin_blueprints(app)
@app.before_request @app.before_request
def validateRequest(): def validateRequest():
@ -287,8 +290,10 @@ class API:
return return
try: try:
if not hmac.compare_digest(request.headers['token'], self.clientToken): if not hmac.compare_digest(request.headers['token'], self.clientToken):
if not hmac.compare_digest(request.form['token'], self.clientToken):
abort(403) abort(403)
except KeyError: except KeyError:
if not hmac.compare_digest(request.form['token'], self.clientToken):
abort(403) abort(403)
@app.after_request @app.after_request
@ -316,6 +321,14 @@ class API:
def loadMailIndex(): def loadMailIndex():
return send_from_directory('static-data/www/mail/', 'index.html') return send_from_directory('static-data/www/mail/', 'index.html')
@app.route('/friends/<path:path>', endpoint='friends')
def loadContacts(path):
return send_from_directory('static-data/www/friends/', path)
@app.route('/friends/', endpoint='friendsindex')
def loadContacts():
return send_from_directory('static-data/www/friends/', 'index.html')
@app.route('/board/<path:path>', endpoint='boardContent') @app.route('/board/<path:path>', endpoint='boardContent')
def boardContent(path): def boardContent(path):
return send_from_directory('static-data/www/board/', path) return send_from_directory('static-data/www/board/', path)
@ -406,14 +419,16 @@ class API:
if self._core._utils.validateHash(bHash): if self._core._utils.validateHash(bHash):
try: try:
resp = Block(bHash).bcontent resp = Block(bHash).bcontent
except onionrexceptions.NoDataAvailable:
abort(404)
except TypeError: except TypeError:
pass pass
try: try:
resp = base64.b64decode(resp) resp = base64.b64decode(resp)
except: except:
pass pass
if resp == 'Not Found': if resp == 'Not Found' or not resp:
abourt(404) abort(404)
return Response(resp) return Response(resp)
@app.route('/waitforshare/<name>', methods=['post']) @app.route('/waitforshare/<name>', methods=['post'])
@ -512,7 +527,6 @@ class API:
data = subpath.split('/') data = subpath.split('/')
if len(data) > 1: if len(data) > 1:
plName = data[0] plName = data[0]
events.event('pluginRequest', {'name': plName, 'path': subpath, 'pluginResponse': pluginResponseCode, 'postData': postData}, onionr=onionrInst) events.event('pluginRequest', {'name': plName, 'path': subpath, 'pluginResponse': pluginResponseCode, 'postData': postData}, onionr=onionrInst)
while True: while True:
try: try:

View File

@ -19,13 +19,15 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
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 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, binascii
import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block
import onionrdaemontools, onionrsockets, onionr, onionrproofs
import binascii
from dependencies import secrets from dependencies import secrets
from defusedxml import minidom
from utils import networkmerger from utils import networkmerger
import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block
from communicatorutils import onionrdaemontools
import onionrsockets, onionr, onionrproofs
from communicatorutils import onionrcommunicatortimers, proxypicker
OnionrCommunicatorTimers = onionrcommunicatortimers.OnionrCommunicatorTimers
config.reload() config.reload()
class OnionrCommunicatorDaemon: class OnionrCommunicatorDaemon:
@ -44,10 +46,6 @@ class OnionrCommunicatorDaemon:
self.proxyPort = proxyPort self.proxyPort = proxyPort
self._core = onionrInst.onionrCore self._core = onionrInst.onionrCore
# initialize NIST beacon salt and time
self.nistSaltTimestamp = 0
self.powSalt = 0
self.blocksToUpload = [] self.blocksToUpload = []
# loop time.sleep delay in seconds # loop time.sleep delay in seconds
@ -171,7 +169,8 @@ class OnionrCommunicatorDaemon:
# Validate new peers are good format and not already in queue # Validate new peers are good format and not already in queue
invalid = [] invalid = []
for x in newPeers: for x in newPeers:
if not self._core._utils.validateID(x) or x in self.newPeers: x = x.strip()
if not self._core._utils.validateID(x) or x in self.newPeers or x == self._core.hsAddress:
invalid.append(x) invalid.append(x)
for x in invalid: for x in invalid:
newPeers.remove(x) newPeers.remove(x)
@ -431,16 +430,18 @@ class OnionrCommunicatorDaemon:
for address in peerList: for address in peerList:
if not config.get('tor.v3onions') and len(address) == 62: if not config.get('tor.v3onions') and len(address) == 62:
continue continue
if address == self._core.hsAddress:
continue
if len(address) == 0 or address in tried or address in self.onlinePeers or address in self.cooldownPeer: if len(address) == 0 or address in tried or address in self.onlinePeers or address in self.cooldownPeer:
continue continue
if self.shutdown: if self.shutdown:
return return
if self.peerAction(address, 'ping') == 'pong!': if self.peerAction(address, 'ping') == 'pong!':
logger.info('Connected to ' + address)
time.sleep(0.1) time.sleep(0.1)
if address not in mainPeerList: if address not in mainPeerList:
networkmerger.mergeAdders(address, self._core) networkmerger.mergeAdders(address, self._core)
if address not in self.onlinePeers: if address not in self.onlinePeers:
logger.info('Connected to ' + address)
self.onlinePeers.append(address) self.onlinePeers.append(address)
self.connectTimes[address] = self._core._utils.getEpoch() self.connectTimes[address] = self._core._utils.getEpoch()
retData = address retData = address
@ -487,7 +488,7 @@ class OnionrCommunicatorDaemon:
score = str(self.getPeerProfileInstance(i).score) score = str(self.getPeerProfileInstance(i).score)
logger.info(i + ', score: ' + score) logger.info(i + ', score: ' + score)
def peerAction(self, peer, action, data=''): def peerAction(self, peer, action, data='', returnHeaders=False):
'''Perform a get request to a peer''' '''Perform a get request to a peer'''
if len(peer) == 0: if len(peer) == 0:
return False return False
@ -511,7 +512,7 @@ class OnionrCommunicatorDaemon:
else: else:
self._core.setAddressInfo(peer, 'lastConnect', self._core._utils.getEpoch()) self._core.setAddressInfo(peer, 'lastConnect', self._core._utils.getEpoch())
self.getPeerProfileInstance(peer).addScore(1) self.getPeerProfileInstance(peer).addScore(1)
return retData return retData # If returnHeaders, returns tuple of data, headers. if not, just data string
def getPeerProfileInstance(self, peer): def getPeerProfileInstance(self, peer):
'''Gets a peer profile instance from the list of profiles, by address name''' '''Gets a peer profile instance from the list of profiles, by address name'''
@ -603,11 +604,7 @@ class OnionrCommunicatorDaemon:
triedPeers.append(peer) triedPeers.append(peer)
url = 'http://' + peer + '/upload' url = 'http://' + peer + '/upload'
data = {'block': block.Block(bl).getRaw()} data = {'block': block.Block(bl).getRaw()}
proxyType = '' proxyType = proxypicker.pick_proxy(peer)
if peer.endswith('.onion'):
proxyType = 'tor'
elif peer.endswith('.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, post=True) self._core._utils.localCommand('waitforshare/' + bl, post=True)
@ -644,48 +641,5 @@ class OnionrCommunicatorDaemon:
self.decrementThreadCount('runCheck') self.decrementThreadCount('runCheck')
class OnionrCommunicatorTimers:
def __init__(self, daemonInstance, timerFunction, frequency, makeThread=True, threadAmount=1, maxThreads=5, requiresPeer=False):
self.timerFunction = timerFunction
self.frequency = frequency
self.threadAmount = threadAmount
self.makeThread = makeThread
self.requiresPeer = requiresPeer
self.daemonInstance = daemonInstance
self.maxThreads = maxThreads
self._core = self.daemonInstance._core
self.daemonInstance.timers.append(self)
self.count = 0
def processTimer(self):
# mark how many instances of a thread we have (decremented at thread end)
try:
self.daemonInstance.threadCounts[self.timerFunction.__name__]
except KeyError:
self.daemonInstance.threadCounts[self.timerFunction.__name__] = 0
# execute thread if it is time, and we are not missing *required* online peer
if self.count == self.frequency:
try:
if self.requiresPeer and len(self.daemonInstance.onlinePeers) == 0:
raise onionrexceptions.OnlinePeerNeeded
except onionrexceptions.OnlinePeerNeeded:
pass
else:
if self.makeThread:
for i in range(self.threadAmount):
if self.daemonInstance.threadCounts[self.timerFunction.__name__] >= self.maxThreads:
logger.debug('%s is currently using the maximum number of threads, not starting another.' % self.timerFunction.__name__)
else:
self.daemonInstance.threadCounts[self.timerFunction.__name__] += 1
newThread = threading.Thread(target=self.timerFunction)
newThread.start()
else:
self.timerFunction()
self.count = -1 # negative 1 because its incremented at bottom
self.count += 1
def startCommunicator(onionrInst, proxyPort): def startCommunicator(onionrInst, proxyPort):
OnionrCommunicatorDaemon(onionrInst, proxyPort) OnionrCommunicatorDaemon(onionrInst, proxyPort)

View File

@ -0,0 +1,63 @@
#!/usr/bin/env python3
'''
Onionr - P2P Anonymous Storage Network
This file contains timer control for the communicator
'''
'''
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 threading, onionrexceptions, logger
class OnionrCommunicatorTimers:
def __init__(self, daemonInstance, timerFunction, frequency, makeThread=True, threadAmount=1, maxThreads=5, requiresPeer=False):
self.timerFunction = timerFunction
self.frequency = frequency
self.threadAmount = threadAmount
self.makeThread = makeThread
self.requiresPeer = requiresPeer
self.daemonInstance = daemonInstance
self.maxThreads = maxThreads
self._core = self.daemonInstance._core
self.daemonInstance.timers.append(self)
self.count = 0
def processTimer(self):
# mark how many instances of a thread we have (decremented at thread end)
try:
self.daemonInstance.threadCounts[self.timerFunction.__name__]
except KeyError:
self.daemonInstance.threadCounts[self.timerFunction.__name__] = 0
# execute thread if it is time, and we are not missing *required* online peer
if self.count == self.frequency and not self.daemonInstance.shutdown:
try:
if self.requiresPeer and len(self.daemonInstance.onlinePeers) == 0:
raise onionrexceptions.OnlinePeerNeeded
except onionrexceptions.OnlinePeerNeeded:
pass
else:
if self.makeThread:
for i in range(self.threadAmount):
if self.daemonInstance.threadCounts[self.timerFunction.__name__] >= self.maxThreads:
logger.debug('%s is currently using the maximum number of threads, not starting another.' % self.timerFunction.__name__)
else:
self.daemonInstance.threadCounts[self.timerFunction.__name__] += 1
newThread = threading.Thread(target=self.timerFunction)
newThread.start()
else:
self.timerFunction()
self.count = -1 # negative 1 because its incremented at bottom
self.count += 1

View File

@ -30,16 +30,22 @@ class DaemonTools:
''' '''
def __init__(self, daemon): def __init__(self, daemon):
self.daemon = daemon self.daemon = daemon
self.announceProgress = {}
self.announceCache = {} self.announceCache = {}
def announceNode(self): def announceNode(self):
'''Announce our node to our peers''' '''Announce our node to our peers'''
retData = False retData = False
announceFail = False announceFail = False
# Do not let announceCache get too large
if len(self.announceCache) >= 10000:
self.announceCache.popitem()
if self.daemon._core.config.get('general.security_level', 0) == 0: if self.daemon._core.config.get('general.security_level', 0) == 0:
# Announce to random online peers # Announce to random online peers
for i in self.daemon.onlinePeers: for i in self.daemon.onlinePeers:
if not i in self.announceCache: if not i in self.announceCache and not i in self.announceProgress:
peer = i peer = i
break break
else: else:
@ -66,7 +72,9 @@ class DaemonTools:
elif len(existingRand) > 0: elif len(existingRand) > 0:
data['random'] = existingRand data['random'] = existingRand
else: else:
self.announceProgress[peer] = True
proof = onionrproofs.DataPOW(combinedNodes, forceDifficulty=4) proof = onionrproofs.DataPOW(combinedNodes, forceDifficulty=4)
del self.announceProgress[peer]
try: try:
data['random'] = base64.b64encode(proof.waitForResult()[1]) data['random'] = base64.b64encode(proof.waitForResult()[1])
except TypeError: except TypeError:
@ -89,6 +97,7 @@ class DaemonTools:
'''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 netutils.checkNetwork(self.daemon._core._utils, torPort=self.daemon.proxyPort): if not netutils.checkNetwork(self.daemon._core._utils, torPort=self.daemon.proxyPort):
if not self.daemon.shutdown:
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:
@ -197,7 +206,7 @@ class DaemonTools:
fakePeer = '' fakePeer = ''
chance = 10 chance = 10
if secrets.randbelow(chance) == (chance - 1): if secrets.randbelow(chance) == (chance - 1):
fakePeer = self.daemon._core._crypto.generatePubKey()[0] fakePeer = 'OVPCZLOXD6DC5JHX4EQ3PSOGAZ3T24F75HQLIUZSDSMYPEOXCPFA===='
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, meta={'subject': 'foo'}) self.daemon._core.insertBlock(data, header='pm', encryptType='asym', asymPeer=fakePeer, meta={'subject': 'foo'})
self.daemon.decrementThreadCount('insertDeniableBlock') self.daemon.decrementThreadCount('insertDeniableBlock')

View File

@ -0,0 +1,25 @@
'''
Onionr - P2P Anonymous Storage Network
Just picks a proxy
'''
'''
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/>.
'''
def pick_proxy(peer_address):
if peer_address.endswith('.onion'):
return 'tor'
elif peer_address.endswith('.i2p'):
return 'i2p'

View File

@ -19,11 +19,11 @@
''' '''
import sqlite3, os, sys, time, math, base64, tarfile, nacl, logger, json, netcontroller, math, config, uuid import sqlite3, os, sys, time, math, base64, tarfile, nacl, logger, json, netcontroller, math, config, uuid
from onionrblockapi import Block from onionrblockapi import Block
import deadsimplekv as simplekv
import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions
import onionrblacklist import onionrblacklist
from onionrusers import onionrusers from onionrusers import onionrusers
import dbcreator, onionrstorage, serializeddata import dbcreator, onionrstorage, serializeddata, subprocesspow
from etc import onionrvalues from etc import onionrvalues
if sys.version_info < (3, 6): if sys.version_info < (3, 6):
@ -65,6 +65,7 @@ class Core:
self.dataNonceFile = self.dataDir + 'block-nonces.dat' self.dataNonceFile = self.dataDir + 'block-nonces.dat'
self.dbCreate = dbcreator.DBCreator(self) self.dbCreate = dbcreator.DBCreator(self)
self.forwardKeysFile = self.dataDir + 'forward-keys.db' self.forwardKeysFile = self.dataDir + 'forward-keys.db'
self.keyStore = simplekv.DeadSimpleKV(self.dataDir + 'cachedstorage.dat', refresh_seconds=5)
# Socket data, defined here because of multithreading constraints with gevent # Socket data, defined here because of multithreading constraints with gevent
self.killSockets = False self.killSockets = False
@ -105,7 +106,6 @@ class Core:
logger.warn('Warning: address bootstrap file not found ' + self.bootstrapFileLocation) logger.warn('Warning: address bootstrap file not found ' + self.bootstrapFileLocation)
self._utils = onionrutils.OnionrUtils(self) self._utils = onionrutils.OnionrUtils(self)
self.blockCache = onionrstorage.BlockCache()
# Initialize the crypto object # Initialize the crypto object
self._crypto = onionrcrypto.OnionrCrypto(self) self._crypto = onionrcrypto.OnionrCrypto(self)
self._blacklist = onionrblacklist.OnionrBlackList(self) self._blacklist = onionrblacklist.OnionrBlackList(self)
@ -121,7 +121,6 @@ class Core:
''' '''
Hack to refresh some vars which may not be set on first start Hack to refresh some vars which may not be set on first start
''' '''
if os.path.exists(self.dataDir + '/hs/hostname'): 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:
self.hsAddress = hs.read().strip() self.hsAddress = hs.read().strip()
@ -468,14 +467,6 @@ class Core:
except TypeError: except TypeError:
pass pass
if getPow:
try:
peerList.append(self._crypto.pubKey + '-' + self._crypto.pubKeyPowToken)
except TypeError:
pass
else:
peerList.append(self._crypto.pubKey)
conn.close() conn.close()
return peerList return peerList
@ -597,10 +588,6 @@ class Core:
conn = sqlite3.connect(self.blockDB, timeout=30) conn = sqlite3.connect(self.blockDB, timeout=30)
c = conn.cursor() c = conn.cursor()
# if unsaved:
# execute = 'SELECT hash FROM hashes WHERE dataSaved != 1 ORDER BY RANDOM();'
# else:
# execute = 'SELECT hash FROM hashes ORDER BY dateReceived ASC;'
execute = 'SELECT hash FROM hashes WHERE dateReceived >= ? ORDER BY dateReceived ASC;' execute = 'SELECT hash FROM hashes WHERE dateReceived >= ? ORDER BY dateReceived ASC;'
args = (dateRec,) args = (dateRec,)
rows = list() rows = list()
@ -702,6 +689,8 @@ class Core:
return False return False
retData = False retData = False
createTime = self._utils.getRoundedEpoch()
# check nonce # check nonce
dataNonce = self._utils.bytesToStr(self._crypto.sha3Hash(data)) dataNonce = self._utils.bytesToStr(self._crypto.sha3Hash(data))
try: try:
@ -719,10 +708,7 @@ class Core:
data = str(data) data = str(data)
plaintext = data plaintext = data
plaintextMeta = {} plaintextMeta = {}
plaintextPeer = asymPeer
# Convert asym peer human readable key to base32 if set
if ' ' in asymPeer.strip():
asymPeer = self._utils.convertHumanReadableID(asymPeer)
retData = '' retData = ''
signature = '' signature = ''
@ -745,6 +731,7 @@ class Core:
pass pass
if encryptType == 'asym': if encryptType == 'asym':
meta['rply'] = createTime # Duplicate the time in encrypted messages to prevent replays
if not disableForward and sign 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)
@ -792,7 +779,7 @@ class Core:
metadata['meta'] = jsonMeta metadata['meta'] = jsonMeta
metadata['sig'] = signature metadata['sig'] = signature
metadata['signer'] = signer metadata['signer'] = signer
metadata['time'] = self._utils.getRoundedEpoch() metadata['time'] = createTime
# ensure expire is integer and of sane length # ensure expire is integer and of sane length
if type(expire) is not type(None): if type(expire) is not type(None):
@ -800,8 +787,7 @@ class Core:
metadata['expire'] = expire metadata['expire'] = expire
# send block data (and metadata) to POW module to get tokenized block data # send block data (and metadata) to POW module to get tokenized block data
proof = onionrproofs.POW(metadata, data) payload = subprocesspow.SubprocessPOW(data, metadata, self).start()
payload = proof.waitForResult()
if payload != False: if payload != False:
try: try:
retData = self.setData(payload) retData = self.setData(payload)
@ -817,6 +803,9 @@ class Core:
self.daemonQueueAdd('uploadBlock', retData) self.daemonQueueAdd('uploadBlock', retData)
if retData != False: if retData != False:
if plaintextPeer == 'OVPCZLOXD6DC5JHX4EQ3PSOGAZ3T24F75HQLIUZSDSMYPEOXCPFA====':
events.event('insertdeniable', {'content': plaintext, 'meta': plaintextMeta, 'hash': retData, 'peer': self._utils.bytesToStr(asymPeer)}, onionr = self.onionrInst, threaded = True)
else:
events.event('insertblock', {'content': plaintext, 'meta': plaintextMeta, 'hash': retData, 'peer': self._utils.bytesToStr(asymPeer)}, onionr = self.onionrInst, threaded = True) events.event('insertblock', {'content': plaintext, 'meta': plaintextMeta, 'hash': retData, 'peer': self._utils.bytesToStr(asymPeer)}, onionr = self.onionrInst, threaded = True)
return retData return retData

29
onionr/httpapi/__init__.py Executable file
View File

@ -0,0 +1,29 @@
'''
Onionr - P2P Anonymous Storage Network
This file registers plugin's flask blueprints for the client http server
'''
'''
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 onionrplugins
def load_plugin_blueprints(flaskapp):
'''Iterate enabled plugins and load any http endpoints they have'''
for plugin in onionrplugins.get_enabled_plugins():
plugin = onionrplugins.get_plugin(plugin)
try:
flaskapp.register_blueprint(getattr(plugin, 'flask_blueprint'))
except AttributeError:
pass

View File

@ -0,0 +1,56 @@
'''
Onionr - P2P Anonymous Storage Network
This file creates http endpoints for friend management
'''
'''
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 core, json
from onionrusers import contactmanager
from flask import Blueprint, Response, request, abort, redirect
friends = Blueprint('friends', __name__)
@friends.route('/friends/list')
def list_friends():
pubkey_list = {}
friend_list = contactmanager.ContactManager.list_friends(core.Core())
for friend in friend_list:
pubkey_list[friend.publicKey] = {'name': friend.get_info('name')}
return json.dumps(pubkey_list)
@friends.route('/friends/add/<pubkey>', methods=['POST'])
def add_friend(pubkey):
contactmanager.ContactManager(core.Core(), pubkey, saveUser=True).setTrust(1)
return redirect(request.referrer + '#' + request.form['token'])
@friends.route('/friends/remove/<pubkey>', methods=['POST'])
def remove_friend(pubkey):
contactmanager.ContactManager(core.Core(), pubkey).setTrust(0)
return redirect(request.referrer + '#' + request.form['token'])
@friends.route('/friends/setinfo/<pubkey>/<key>', methods=['POST'])
def set_info(pubkey, key):
data = request.form['data']
contactmanager.ContactManager(core.Core(), pubkey).set_info(key, data)
return redirect(request.referrer + '#' + request.form['token'])
@friends.route('/friends/getinfo/<pubkey>/<key>')
def get_info(pubkey, key):
retData = contactmanager.ContactManager(core.Core(), pubkey).get_info(key)
if retData is None:
abort(404)
else:
return retData

View File

@ -0,0 +1,31 @@
'''
Onionr - P2P Anonymous Storage Network
This file creates http endpoints for friend management
'''
'''
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 core
from flask import Blueprint, Response, request, abort
simplecache = Blueprint('simplecache', __name__)
@simplecache.route('/get/<key>')
def get_key(key):
return
@simplecache.route('/set/<key>', methods=['POST'])
def set_key(key):
return

View File

@ -147,7 +147,8 @@ HiddenServicePort 80 ''' + self.apiServerIP + ''':''' + str(self.hsPort)
torVersion = subprocess.Popen([self.torBinary, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) torVersion = subprocess.Popen([self.torBinary, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
for line in iter(torVersion.stdout.readline, b''): for line in iter(torVersion.stdout.readline, b''):
if 'Tor 0.2.' in line.decode(): if 'Tor 0.2.' in line.decode():
logger.warn("Running 0.2.x Tor series, no support for v3 onion peers") logger.error('Tor 0.3+ required')
sys.exit(1)
break break
torVersion.kill() torVersion.kill()
@ -162,7 +163,7 @@ HiddenServicePort 80 ''' + self.apiServerIP + ''':''' + str(self.hsPort)
logger.fatal('Failed to start Tor. Maybe a stray instance of Tor used by Onionr is still running?') logger.fatal('Failed to start Tor. Maybe a stray instance of Tor used by Onionr is still running?')
return False return False
except KeyboardInterrupt: except KeyboardInterrupt:
logger.fatal('Got keyboard interrupt.', timestamp = false, level = logger.LEVEL_IMPORTANT) logger.fatal('Got keyboard interrupt.', timestamp = False, level = logger.LEVEL_IMPORTANT)
return False return False
logger.debug('Finished starting Tor.', timestamp=True) logger.debug('Finished starting Tor.', timestamp=True)

File diff suppressed because it is too large Load Diff

View File

@ -26,7 +26,7 @@ class Block:
blockCacheOrder = list() # NEVER write your own code that writes to this! blockCacheOrder = list() # NEVER write your own code that writes to this!
blockCache = dict() # should never be accessed directly, look at Block.getCache() blockCache = dict() # should never be accessed directly, look at Block.getCache()
def __init__(self, hash = None, core = None, type = None, content = None, expire=None, decrypt=False): def __init__(self, hash = None, core = None, type = None, content = None, expire=None, decrypt=False, bypassReplayCheck=False):
# take from arguments # take from arguments
# sometimes people input a bytes object instead of str in `hash` # sometimes people input a bytes object instead of str in `hash`
if (not hash is None) and isinstance(hash, bytes): if (not hash is None) and isinstance(hash, bytes):
@ -37,6 +37,7 @@ class Block:
self.btype = type self.btype = type
self.bcontent = content self.bcontent = content
self.expire = expire self.expire = expire
self.bypassReplayCheck = bypassReplayCheck
# initialize variables # initialize variables
self.valid = True self.valid = True
@ -84,6 +85,20 @@ class Block:
self.signer = core._crypto.pubKeyDecrypt(self.signer, 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()
# Check for replay attacks
try:
if self.core._utils.getEpoch() - self.core.getBlockDate(self.hash) < 60:
assert self.core._crypto.replayTimestampValidation(self.bmetadata['rply'])
except (AssertionError, KeyError) as e:
if not self.bypassReplayCheck:
# Zero out variables to prevent reading of replays
self.bmetadata = {}
self.signer = ''
self.bheader['signer'] = ''
self.signedData = ''
self.signature = ''
raise onionrexceptions.ReplayAttack('Signature is too old. possible replay attack')
try: try:
assert self.bmetadata['forwardEnc'] is True assert self.bmetadata['forwardEnc'] is True
except (AssertionError, KeyError) as e: except (AssertionError, KeyError) as e:
@ -97,6 +112,8 @@ class Block:
except nacl.exceptions.CryptoError: except nacl.exceptions.CryptoError:
pass pass
#logger.debug('Could not decrypt block. Either invalid key or corrupted data') #logger.debug('Could not decrypt block. Either invalid key or corrupted data')
except onionrexceptions.ReplayAttack:
logger.warn('%s is possibly a replay attack' % (self.hash,))
else: else:
retData = True retData = True
self.decrypted = True self.decrypted = True
@ -139,32 +156,10 @@ class Block:
# import from file # import from file
if blockdata is None: if blockdata is None:
try:
blockdata = onionrstorage.getData(self.core, self.getHash()).decode() blockdata = onionrstorage.getData(self.core, self.getHash()).decode()
''' except AttributeError:
raise onionrexceptions.NoDataAvailable('Block does not exist')
filelocation = file
readfile = True
if filelocation is None:
if self.getHash() is None:
return False
elif self.getHash() in Block.getCache():
# get the block from cache, if it's in it
blockdata = Block.getCache(self.getHash())
readfile = False
# read from file if it's still None
if blockdata is None:
filelocation = self.core.dataDir + 'blocks/%s.dat' % self.getHash()
if readfile:
blockdata = onionrstorage.getData(self.core, self.getHash()).decode()
#with open(filelocation, 'rb') as f:
#blockdata = f.read().decode()
self.blockFile = filelocation
'''
else: else:
self.blockFile = None self.blockFile = None
# parse block # parse block
@ -200,11 +195,11 @@ class Block:
return True return True
except Exception as e: except Exception as e:
logger.error('Failed to parse block %s.' % self.getHash(), error = e, timestamp = False) logger.warn('Failed to parse block %s.' % self.getHash(), error = e, timestamp = False)
# if block can't be parsed, it's a waste of precious space. Throw it away. # if block can't be parsed, it's a waste of precious space. Throw it away.
if not self.delete(): if not self.delete():
logger.error('Failed to delete invalid block %s.' % self.getHash(), error = e) logger.warn('Failed to delete invalid block %s.' % self.getHash(), error = e)
else: else:
logger.debug('Deleted invalid block %s.' % self.getHash(), timestamp = False) logger.debug('Deleted invalid block %s.' % self.getHash(), timestamp = False)

View File

@ -0,0 +1,171 @@
'''
Onionr - P2P Anonymous Storage Network
This module defines commands for CLI usage
'''
'''
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
import webbrowser, sys
import logger
from . import pubkeymanager, onionrstatistics, daemonlaunch, filecommands, plugincommands, keyadders
def show_help(o_inst, command):
helpmenu = o_inst.getHelp()
if command is None and len(sys.argv) >= 3:
for cmd in sys.argv[2:]:
o_inst.showHelp(cmd)
elif not command is None:
if command.lower() in helpmenu:
logger.info(logger.colors.bold + command + logger.colors.reset + logger.colors.fg.blue + ' : ' + logger.colors.reset + helpmenu[command.lower()], timestamp = False)
else:
logger.warn(logger.colors.bold + command + logger.colors.reset + logger.colors.fg.blue + ' : ' + logger.colors.reset + 'No help menu entry was found', timestamp = False)
else:
o_inst.version(0)
for command, helpmessage in helpmenu.items():
o_inst.showHelp(command)
def open_home(o_inst):
try:
url = o_inst.onionrUtils.getClientAPIServer()
except FileNotFoundError:
logger.error('Onionr seems to not be running (could not get api host)')
else:
url = 'http://%s/#%s' % (url, o_inst.onionrCore.config.get('client.webpassword'))
print('If Onionr does not open automatically, use this URL:', url)
webbrowser.open_new_tab(url)
def get_commands(onionr_inst):
return {'': onionr_inst.showHelpSuggestion,
'help': onionr_inst.showHelp,
'version': onionr_inst.version,
'config': onionr_inst.configure,
'start': onionr_inst.start,
'stop': onionr_inst.killDaemon,
'status': onionr_inst.showStats,
'statistics': onionr_inst.showStats,
'stats': onionr_inst.showStats,
'details' : onionr_inst.showDetails,
'detail' : onionr_inst.showDetails,
'show-details' : onionr_inst.showDetails,
'show-detail' : onionr_inst.showDetails,
'showdetails' : onionr_inst.showDetails,
'showdetail' : onionr_inst.showDetails,
'get-details' : onionr_inst.showDetails,
'get-detail' : onionr_inst.showDetails,
'getdetails' : onionr_inst.showDetails,
'getdetail' : onionr_inst.showDetails,
'enable-plugin': onionr_inst.enablePlugin,
'enplugin': onionr_inst.enablePlugin,
'enableplugin': onionr_inst.enablePlugin,
'enmod': onionr_inst.enablePlugin,
'disable-plugin': onionr_inst.disablePlugin,
'displugin': onionr_inst.disablePlugin,
'disableplugin': onionr_inst.disablePlugin,
'dismod': onionr_inst.disablePlugin,
'reload-plugin': onionr_inst.reloadPlugin,
'reloadplugin': onionr_inst.reloadPlugin,
'reload-plugins': onionr_inst.reloadPlugin,
'reloadplugins': onionr_inst.reloadPlugin,
'create-plugin': onionr_inst.createPlugin,
'createplugin': onionr_inst.createPlugin,
'plugin-create': onionr_inst.createPlugin,
'listkeys': onionr_inst.listKeys,
'list-keys': onionr_inst.listKeys,
'addpeer': onionr_inst.addPeer,
'add-peer': onionr_inst.addPeer,
'add-address': onionr_inst.addAddress,
'add-addr': onionr_inst.addAddress,
'addaddr': onionr_inst.addAddress,
'addaddress': onionr_inst.addAddress,
'list-peers': onionr_inst.listPeers,
'blacklist-block': onionr_inst.banBlock,
'add-file': onionr_inst.addFile,
'addfile': onionr_inst.addFile,
'addhtml': onionr_inst.addWebpage,
'add-html': onionr_inst.addWebpage,
'add-site': onionr_inst.addWebpage,
'addsite': onionr_inst.addWebpage,
'openhome': onionr_inst.openHome,
'open-home': onionr_inst.openHome,
'export-block': onionr_inst.exportBlock,
'exportblock': onionr_inst.exportBlock,
'get-file': onionr_inst.getFile,
'getfile': onionr_inst.getFile,
'listconn': onionr_inst.listConn,
'list-conn': onionr_inst.listConn,
'import-blocks': onionr_inst.onionrUtils.importNewBlocks,
'importblocks': onionr_inst.onionrUtils.importNewBlocks,
'introduce': onionr_inst.onionrCore.introduceNode,
'pex': onionr_inst.doPEX,
'getpassword': onionr_inst.printWebPassword,
'get-password': onionr_inst.printWebPassword,
'getpwd': onionr_inst.printWebPassword,
'get-pwd': onionr_inst.printWebPassword,
'getpass': onionr_inst.printWebPassword,
'get-pass': onionr_inst.printWebPassword,
'getpasswd': onionr_inst.printWebPassword,
'get-passwd': onionr_inst.printWebPassword,
'friend': onionr_inst.friendCmd,
'addid': onionr_inst.addID,
'add-id': onionr_inst.addID,
'change-id': onionr_inst.changeID
}
cmd_help = {
'help': 'Displays this Onionr help menu',
'version': 'Displays the Onionr version',
'config': 'Configures something and adds it to the file',
'start': 'Starts the Onionr daemon',
'stop': 'Stops the Onionr daemon',
'stats': 'Displays node statistics',
'details': 'Displays the web password, public key, and human readable public key',
'enable-plugin': 'Enables and starts a plugin',
'disable-plugin': 'Disables and stops a plugin',
'reload-plugin': 'Reloads a plugin',
'create-plugin': 'Creates directory structure for a plugin',
'add-peer': 'Adds a peer to database',
'list-peers': 'Displays a list of peers',
'add-file': 'Create an Onionr block from a file',
'get-file': 'Get a file from Onionr blocks',
'import-blocks': 'import blocks from the disk (Onionr is transport-agnostic!)',
'listconn': 'list connected peers',
'pex': 'exchange addresses with peers (done automatically)',
'blacklist-block': 'deletes a block by hash and permanently removes it from your node',
'introduce': 'Introduce your node to the public Onionr network',
'friend': '[add|remove] [public key/id]',
'add-id': 'Generate a new ID (key pair)',
'change-id': 'Change active ID',
'open-home': 'Open your node\'s home/info screen'
}

View File

@ -0,0 +1,142 @@
'''
Onionr - P2P Anonymous Storage Network
launch the api server and communicator
'''
'''
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, time, sys, platform, sqlite3
from threading import Thread
import onionr, api, logger, communicator
import onionrevents as events
from netcontroller import NetController
def daemon(o_inst):
'''
Starts the Onionr communication daemon
'''
# remove runcheck if it exists
if os.path.isfile('data/.runcheck'):
logger.debug('Runcheck file found on daemon start, deleting in advance.')
os.remove('data/.runcheck')
Thread(target=api.API, args=(o_inst, o_inst.debug, onionr.API_VERSION)).start()
Thread(target=api.PublicAPI, args=[o_inst.getClientApi()]).start()
try:
time.sleep(0)
except KeyboardInterrupt:
logger.debug('Got keyboard interrupt, shutting down...')
time.sleep(1)
o_inst.onionrUtils.localCommand('shutdown')
apiHost = ''
while apiHost == '':
try:
with open(o_inst.onionrCore.publicApiHostFile, 'r') as hostFile:
apiHost = hostFile.read()
except FileNotFoundError:
pass
time.sleep(0.5)
onionr.Onionr.setupConfig('data/', self = o_inst)
if o_inst._developmentMode:
logger.warn('DEVELOPMENT MODE ENABLED (NOT RECOMMENDED)', timestamp = False)
net = NetController(o_inst.onionrCore.config.get('client.public.port', 59497), apiServerIP=apiHost)
logger.debug('Tor is starting...')
if not net.startTor():
o_inst.onionrUtils.localCommand('shutdown')
sys.exit(1)
if len(net.myID) > 0 and o_inst.onionrCore.config.get('general.security_level') == 0:
logger.debug('Started .onion service: %s' % (logger.colors.underline + net.myID))
else:
logger.debug('.onion service disabled')
logger.debug('Using public key: %s' % (logger.colors.underline + o_inst.onionrCore._crypto.pubKey))
time.sleep(1)
o_inst.onionrCore.torPort = net.socksPort
communicatorThread = Thread(target=communicator.startCommunicator, args=(o_inst, str(net.socksPort)))
communicatorThread.start()
while o_inst.communicatorInst is None:
time.sleep(0.1)
# print nice header thing :)
if o_inst.onionrCore.config.get('general.display_header', True):
o_inst.header()
# print out debug info
o_inst.version(verbosity = 5, function = logger.debug)
logger.debug('Python version %s' % platform.python_version())
logger.debug('Started communicator.')
events.event('daemon_start', onionr = o_inst)
try:
while True:
time.sleep(3)
# Debug to print out used FDs (regular and net)
#proc = psutil.Process()
#print('api-files:',proc.open_files(), len(psutil.net_connections()))
# Break if communicator process ends, so we don't have left over processes
if o_inst.communicatorInst.shutdown:
break
if o_inst.killed:
break # Break out if sigterm for clean exit
except KeyboardInterrupt:
pass
finally:
o_inst.onionrCore.daemonQueueAdd('shutdown')
o_inst.onionrUtils.localCommand('shutdown')
net.killTor()
time.sleep(3)
o_inst.deleteRunFiles()
return
def kill_daemon(o_inst):
'''
Shutdown the Onionr daemon
'''
logger.warn('Stopping the running daemon...', timestamp = False)
try:
events.event('daemon_stop', onionr = o_inst)
net = NetController(o_inst.onionrCore.config.get('client.port', 59496))
try:
o_inst.onionrCore.daemonQueueAdd('shutdown')
except sqlite3.OperationalError:
pass
net.killTor()
except Exception as e:
logger.error('Failed to shutdown daemon.', error = e, timestamp = False)
return
def start(o_inst, input = False, override = False):
if os.path.exists('.onionr-lock') and not override:
logger.fatal('Cannot start. Daemon is already running, or it did not exit cleanly.\n(if you are sure that there is not a daemon running, delete .onionr-lock & try again).')
else:
if not o_inst.debug and not o_inst._developmentMode:
lockFile = open('.onionr-lock', 'w')
lockFile.write('')
lockFile.close()
o_inst.running = True
o_inst.daemon()
o_inst.running = False
if not o_inst.debug and not o_inst._developmentMode:
try:
os.remove('.onionr-lock')
except FileNotFoundError:
pass

View File

@ -0,0 +1,49 @@
import base64, sys, os
import logger
from onionrblockapi import Block
def add_file(o_inst, singleBlock=False, blockType='bin'):
'''
Adds a file to the onionr network
'''
if len(sys.argv) >= 3:
filename = sys.argv[2]
contents = None
if not os.path.exists(filename):
logger.error('That file does not exist. Improper path (specify full path)?')
return
logger.info('Adding file... this might take a long time.')
try:
with open(filename, 'rb') as singleFile:
blockhash = o_inst.onionrCore.insertBlock(base64.b64encode(singleFile.read()), header=blockType)
if len(blockhash) > 0:
logger.info('File %s saved in block %s' % (filename, blockhash))
except:
logger.error('Failed to save file in block.', timestamp = False)
else:
logger.error('%s add-file <filename>' % sys.argv[0], timestamp = False)
def getFile(o_inst):
'''
Get a file from onionr blocks
'''
try:
fileName = sys.argv[2]
bHash = sys.argv[3]
except IndexError:
logger.error("Syntax %s %s" % (sys.argv[0], '/path/to/filename <blockhash>'))
else:
logger.info(fileName)
contents = None
if os.path.exists(fileName):
logger.error("File already exists")
return
if not o_inst.onionrUtils.validateHash(bHash):
logger.error('Block hash is invalid')
return
with open(fileName, 'wb') as myFile:
myFile.write(base64.b64decode(Block(bHash, core=o_inst.onionrCore).bcontent))
return

View File

@ -0,0 +1,49 @@
'''
Onionr - P2P Anonymous Storage Network
add keys (transport and pubkey)
'''
'''
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
import sys
import logger
def add_peer(o_inst):
try:
newPeer = sys.argv[2]
except IndexError:
pass
else:
if o_inst.onionrUtils.hasKey(newPeer):
logger.info('We already have that key')
return
logger.info("Adding peer: " + logger.colors.underline + newPeer)
try:
if o_inst.onionrCore.addPeer(newPeer):
logger.info('Successfully added key')
except AssertionError:
logger.error('Failed to add key')
def add_address(o_inst):
try:
newAddress = sys.argv[2]
newAddress = newAddress.replace('http:', '').replace('/', '')
except IndexError:
pass
else:
logger.info("Adding address: " + logger.colors.underline + newAddress)
if o_inst.onionrCore.addAddress(newAddress):
logger.info("Successfully added address.")
else:
logger.warn("Unable to add address.")

View File

@ -0,0 +1,110 @@
'''
Onionr - P2P Anonymous Storage Network
This module defines commands to show stats/details about the local node
'''
'''
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, uuid, time
import logger, onionrutils
from onionrblockapi import Block
import onionr
def show_stats(o_inst):
try:
# define stats messages here
totalBlocks = len(o_inst.onionrCore.getBlockList())
signedBlocks = len(Block.getBlocks(signed = True))
messages = {
# info about local client
'Onionr Daemon Status' : ((logger.colors.fg.green + 'Online') if o_inst.onionrUtils.isCommunicatorRunning(timeout = 9) else logger.colors.fg.red + 'Offline'),
# file and folder size stats
'div1' : True, # this creates a solid line across the screen, a div
'Total Block Size' : onionrutils.humanSize(onionrutils.size(o_inst.dataDir + 'blocks/')),
'Total Plugin Size' : onionrutils.humanSize(onionrutils.size(o_inst.dataDir + 'plugins/')),
'Log File Size' : onionrutils.humanSize(onionrutils.size(o_inst.dataDir + 'output.log')),
# count stats
'div2' : True,
'Known Peers' : str(len(o_inst.onionrCore.listPeers()) - 1),
'Enabled Plugins' : str(len(o_inst.onionrCore.config.get('plugins.enabled', list()))) + ' / ' + str(len(os.listdir(o_inst.dataDir + 'plugins/'))),
'Stored Blocks' : str(totalBlocks),
'Percent Blocks Signed' : str(round(100 * signedBlocks / max(totalBlocks, 1), 2)) + '%'
}
# color configuration
colors = {
'title' : logger.colors.bold,
'key' : logger.colors.fg.lightgreen,
'val' : logger.colors.fg.green,
'border' : logger.colors.fg.lightblue,
'reset' : logger.colors.reset
}
# pre-processing
maxlength = 0
width = o_inst.getConsoleWidth()
for key, val in messages.items():
if not (type(val) is bool and val is True):
maxlength = max(len(key), maxlength)
prewidth = maxlength + len(' | ')
groupsize = width - prewidth - len('[+] ')
# generate stats table
logger.info(colors['title'] + 'Onionr v%s Statistics' % onionr.ONIONR_VERSION + colors['reset'])
logger.info(colors['border'] + '-' * (maxlength + 1) + '+' + colors['reset'])
for key, val in messages.items():
if not (type(val) is bool and val is True):
val = [str(val)[i:i + groupsize] for i in range(0, len(str(val)), groupsize)]
logger.info(colors['key'] + str(key).rjust(maxlength) + colors['reset'] + colors['border'] + ' | ' + colors['reset'] + colors['val'] + str(val.pop(0)) + colors['reset'])
for value in val:
logger.info(' ' * maxlength + colors['border'] + ' | ' + colors['reset'] + colors['val'] + str(value) + colors['reset'])
else:
logger.info(colors['border'] + '-' * (maxlength + 1) + '+' + colors['reset'])
logger.info(colors['border'] + '-' * (maxlength + 1) + '+' + colors['reset'])
except Exception as e:
logger.error('Failed to generate statistics table.', error = e, timestamp = False)
def show_details(o_inst):
details = {
'Node Address' : o_inst.get_hostname(),
'Web Password' : o_inst.getWebPassword(),
'Public Key' : o_inst.onionrCore._crypto.pubKey,
'Human-readable Public Key' : o_inst.onionrCore._utils.getHumanReadableID()
}
for detail in details:
logger.info('%s%s: \n%s%s\n' % (logger.colors.fg.lightgreen, detail, logger.colors.fg.green, details[detail]), sensitive = True)
def show_peers(o_inst):
randID = str(uuid.uuid4())
o_inst.onionrCore.daemonQueueAdd('connectedPeers', responseID=randID)
while True:
try:
time.sleep(3)
peers = o_inst.onionrCore.daemonQueueGetResponse(randID)
except KeyboardInterrupt:
break
if not type(peers) is None:
if peers not in ('', 'failure', None):
if peers != False:
print(peers)
else:
print('Daemon probably not running. Unable to list connected peers.')
break

View File

@ -0,0 +1,88 @@
'''
Onionr - P2P Anonymous Storage Network
plugin CLI commands
'''
'''
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
import sys
import logger, onionrplugins as plugins
def enable_plugin(o_inst):
if len(sys.argv) >= 3:
plugin_name = sys.argv[2]
logger.info('Enabling plugin "%s"...' % plugin_name)
plugins.enable(plugin_name, o_inst)
else:
logger.info('%s %s <plugin>' % (sys.argv[0], sys.argv[1]))
def disable_plugin(o_inst):
if len(sys.argv) >= 3:
plugin_name = sys.argv[2]
logger.info('Disabling plugin "%s"...' % plugin_name)
plugins.disable(plugin_name, o_inst)
else:
logger.info('%s %s <plugin>' % (sys.argv[0], sys.argv[1]))
def reload_plugin(o_inst):
'''
Reloads (stops and starts) all plugins, or the given plugin
'''
if len(sys.argv) >= 3:
plugin_name = sys.argv[2]
logger.info('Reloading plugin "%s"...' % plugin_name)
plugins.stop(plugin_name, o_inst)
plugins.start(plugin_name, o_inst)
else:
logger.info('Reloading all plugins...')
plugins.reload(o_inst)
def create_plugin(o_inst):
'''
Creates the directory structure for a plugin name
'''
if len(sys.argv) >= 3:
try:
plugin_name = re.sub('[^0-9a-zA-Z_]+', '', str(sys.argv[2]).lower())
if not plugins.exists(plugin_name):
logger.info('Creating plugin "%s"...' % plugin_name)
os.makedirs(plugins.get_plugins_folder(plugin_name))
with open(plugins.get_plugins_folder(plugin_name) + '/main.py', 'a') as main:
contents = ''
with open('static-data/default_plugin.py', 'rb') as file:
contents = file.read().decode()
# TODO: Fix $user. os.getlogin() is B U G G Y
main.write(contents.replace('$user', 'some random developer').replace('$date', datetime.datetime.now().strftime('%Y-%m-%d')).replace('$name', plugin_name))
with open(plugins.get_plugins_folder(plugin_name) + '/info.json', 'a') as main:
main.write(json.dumps({'author' : 'anonymous', 'description' : 'the default description of the plugin', 'version' : '1.0'}))
logger.info('Enabling plugin "%s"...' % plugin_name)
plugins.enable(plugin_name, o_inst)
else:
logger.warn('Cannot create plugin directory structure; plugin "%s" exists.' % plugin_name)
except Exception as e:
logger.error('Failed to create plugin directory structure.', e)
else:
logger.info('%s %s <plugin>' % (sys.argv[0], sys.argv[1]))

View File

@ -0,0 +1,101 @@
'''
Onionr - P2P Anonymous Storage Network
This module defines ID-related CLI commands
'''
'''
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
import sys
import logger
from onionrusers import onionrusers
def add_ID(o_inst):
try:
sys.argv[2]
assert sys.argv[2] == 'true'
except (IndexError, AssertionError) as e:
newID = o_inst.onionrCore._crypto.keyManager.addKey()[0]
else:
logger.warn('Deterministic keys require random and long passphrases.')
logger.warn('If a good passphrase is not used, your key can be easily stolen.')
logger.warn('You should use a series of hard to guess words, see this for reference: https://www.xkcd.com/936/')
pass1 = getpass.getpass(prompt='Enter at least %s characters: ' % (o_inst.onionrCore._crypto.deterministicRequirement,))
pass2 = getpass.getpass(prompt='Confirm entry: ')
if o_inst.onionrCore._crypto.safeCompare(pass1, pass2):
try:
logger.info('Generating deterministic key. This can take a while.')
newID, privKey = o_inst.onionrCore._crypto.generateDeterministic(pass1)
except onionrexceptions.PasswordStrengthError:
logger.error('Must use at least 25 characters.')
sys.exit(1)
else:
logger.error('Passwords do not match.')
sys.exit(1)
o_inst.onionrCore._crypto.keyManager.addKey(pubKey=newID,
privKey=privKey)
logger.info('Added ID: %s' % (o_inst.onionrUtils.bytesToStr(newID),))
def change_ID(o_inst):
try:
key = sys.argv[2]
except IndexError:
logger.error('Specify pubkey to use')
else:
if o_inst.onionrUtils.validatePubKey(key):
if key in o_inst.onionrCore._crypto.keyManager.getPubkeyList():
o_inst.onionrCore.config.set('general.public_key', key)
o_inst.onionrCore.config.save()
logger.info('Set active key to: %s' % (key,))
logger.info('Restart Onionr if it is running.')
else:
logger.error('That key does not exist')
else:
logger.error('Invalid key %s' % (key,))
def friend_command(o_inst):
friend = ''
try:
# Get the friend command
action = sys.argv[2]
except IndexError:
logger.info('Syntax: friend add/remove/list [address]')
else:
action = action.lower()
if action == 'list':
# List out peers marked as our friend
for friend in onionrusers.OnionrUser.list_friends(o_inst.onionrCore):
logger.info(friend.publicKey + ' - ' + friend.getName())
elif action in ('add', 'remove'):
try:
friend = sys.argv[3]
if not o_inst.onionrUtils.validatePubKey(friend):
raise onionrexceptions.InvalidPubkey('Public key is invalid')
if friend not in o_inst.onionrCore.listPeers():
raise onionrexceptions.KeyNotKnown
friend = onionrusers.OnionrUser(o_inst.onionrCore, friend)
except IndexError:
logger.error('Friend ID is required.')
except onionrexceptions.KeyNotKnown:
o_inst.onionrCore.addPeer(friend)
friend = onionrusers.OnionrUser(o_inst.onionrCore, friend)
finally:
if action == 'add':
friend.setTrust(1)
logger.info('Added %s as friend.' % (friend.publicKey,))
else:
friend.setTrust(0)
logger.info('Removed %s as friend.' % (friend.publicKey,))
else:
logger.info('Syntax: friend add/remove/list [address]')

View File

@ -264,6 +264,13 @@ class OnionrCrypto:
return retData return retData
@staticmethod
def replayTimestampValidation(timestamp):
if core.Core()._utils.getEpoch() - int(timestamp) > 2419200:
return False
else:
return True
@staticmethod @staticmethod
def safeCompare(one, two): def safeCompare(one, two):
# Do encode here to avoid spawning core # Do encode here to avoid spawning core

View File

@ -67,7 +67,6 @@ def call(plugin, event_name, data = None, pluginapi = None):
return True return True
except Exception as e: except Exception as e:
logger.debug(str(e))
return False return False
else: else:
return True return True

View File

@ -45,6 +45,9 @@ class PasswordStrengthError(Exception):
# block exceptions # block exceptions
class ReplayAttack(Exception):
pass
class DifficultyTooLarge(Exception): class DifficultyTooLarge(Exception):
pass pass

View File

View File

@ -59,7 +59,6 @@ def reload(onionr = None, stop_event = True):
return False return False
def enable(name, onionr = None, start_event = True): def enable(name, onionr = None, start_event = True):
''' '''
Enables a plugin Enables a plugin
@ -73,6 +72,8 @@ def enable(name, onionr = None, start_event = True):
try: try:
events.call(get_plugin(name), 'enable', onionr) events.call(get_plugin(name), 'enable', onionr)
except ImportError: # Was getting import error on Gitlab CI test "data" except ImportError: # Was getting import error on Gitlab CI test "data"
# NOTE: If you are experiencing issues with plugins not being enabled, it might be this resulting from an error in the module
# can happen inconsistenly (especially between versions)
return False return False
else: else:
enabled_plugins.append(name) enabled_plugins.append(name)

View File

@ -17,10 +17,8 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
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 multiprocessing, nacl.encoding, nacl.hash, nacl.utils, time, math, threading, binascii, sys, base64, json
import nacl.encoding, nacl.hash, nacl.utils, time, math, threading, binascii, logger, sys, base64, json import core, onionrutils, config, logger, onionrblockapi
import core, onionrutils, config
import onionrblockapi
def getDifficultyModifier(coreOrUtilsInst=None): def getDifficultyModifier(coreOrUtilsInst=None):
'''Accepts a core or utils instance returns '''Accepts a core or utils instance returns
@ -101,7 +99,7 @@ def hashMeetsDifficulty(h):
return False return False
class DataPOW: class DataPOW:
def __init__(self, data, forceDifficulty=0, threadCount = 5): def __init__(self, data, forceDifficulty=0, threadCount = 1):
self.foundHash = False self.foundHash = False
self.difficulty = 0 self.difficulty = 0
self.data = data self.data = data
@ -200,7 +198,7 @@ class DataPOW:
return result return result
class POW: class POW:
def __init__(self, metadata, data, threadCount = 5, forceDifficulty=0, coreInst=None): def __init__(self, metadata, data, threadCount = 1, forceDifficulty=0, coreInst=None):
self.foundHash = False self.foundHash = False
self.difficulty = 0 self.difficulty = 0
self.data = data self.data = data
@ -246,6 +244,7 @@ class POW:
answer = '' answer = ''
hbCount = 0 hbCount = 0
nonce = int(binascii.hexlify(nacl.utils.random(2)), 16) nonce = int(binascii.hexlify(nacl.utils.random(2)), 16)
startNonce = nonce
while self.hashing: while self.hashing:
#token = nacl.hash.blake2b(rand + self.data).decode() #token = nacl.hash.blake2b(rand + self.data).decode()
self.metadata['powRandomToken'] = nonce self.metadata['powRandomToken'] = nonce
@ -260,6 +259,7 @@ class POW:
self.hashing = False self.hashing = False
iFound = True iFound = True
self.result = payload self.result = payload
print('count', nonce - startNonce)
break break
nonce += 1 nonce += 1

View File

@ -21,13 +21,6 @@ import core, sys, sqlite3, os, dbcreator
DB_ENTRY_SIZE_LIMIT = 10000 # Will be a config option DB_ENTRY_SIZE_LIMIT = 10000 # Will be a config option
class BlockCache:
def __init__(self):
self.blocks = {}
def cleanCache(self):
while sys.getsizeof(self.blocks) > 100000000:
self.blocks.pop(list(self.blocks.keys())[0])
def dbCreate(coreInst): def dbCreate(coreInst):
try: try:
dbcreator.DBCreator(coreInst).createBlockDataDB() dbcreator.DBCreator(coreInst).createBlockDataDB()
@ -84,7 +77,6 @@ def store(coreInst, data, blockHash=''):
else: else:
with open('%s/%s.dat' % (coreInst.blockDataLocation, blockHash), 'wb') as blockFile: with open('%s/%s.dat' % (coreInst.blockDataLocation, blockHash), 'wb') as blockFile:
blockFile.write(data) blockFile.write(data)
coreInst.blockCache.cleanCache()
def getData(coreInst, bHash): def getData(coreInst, bHash):
assert isinstance(coreInst, core.Core) assert isinstance(coreInst, core.Core)

0
onionr/onionrusers/contactmanager.py Normal file → Executable file
View File

View File

@ -17,7 +17,7 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
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 onionrblockapi, logger, onionrexceptions, json, sqlite3 import onionrblockapi, logger, onionrexceptions, json, sqlite3, time
import nacl.exceptions import nacl.exceptions
def deleteExpiredKeys(coreInst): def deleteExpiredKeys(coreInst):
@ -76,11 +76,11 @@ class OnionrUser:
return retData return retData
def encrypt(self, data): def encrypt(self, data):
encrypted = coreInst._crypto.pubKeyEncrypt(data, self.publicKey, encodedData=True) encrypted = self._core._crypto.pubKeyEncrypt(data, self.publicKey, encodedData=True)
return encrypted return encrypted
def decrypt(self, data): def decrypt(self, data):
decrypted = coreInst._crypto.pubKeyDecrypt(data, self.publicKey, encodedData=True) decrypted = self._core._crypto.pubKeyDecrypt(data, self.publicKey, encodedData=True)
return decrypted return decrypted
def forwardEncrypt(self, data): def forwardEncrypt(self, data):
@ -112,7 +112,8 @@ class OnionrUser:
conn = sqlite3.connect(self._core.peerDB, timeout=10) conn = sqlite3.connect(self._core.peerDB, timeout=10)
c = conn.cursor() c = conn.cursor()
for row in c.execute("SELECT forwardKey FROM forwardKeys WHERE peerKey = ? ORDER BY date DESC", (self.publicKey,)): # TODO: account for keys created at the same time (same epoch)
for row in c.execute("SELECT forwardKey, max(DATE) FROM forwardKeys WHERE peerKey = ?", (self.publicKey,)):
key = row[0] key = row[0]
break break
@ -126,9 +127,8 @@ class OnionrUser:
c = conn.cursor() c = conn.cursor()
keyList = [] keyList = []
for row in c.execute("SELECT forwardKey FROM forwardKeys WHERE peerKey = ? ORDER BY date DESC", (self.publicKey,)): for row in c.execute("SELECT forwardKey, date FROM forwardKeys WHERE peerKey = ? ORDER BY date DESC", (self.publicKey,)):
key = row[0] keyList.append((row[0], row[1]))
keyList.append(key)
conn.commit() conn.commit()
conn.close() conn.close()
@ -175,32 +175,36 @@ class OnionrUser:
def addForwardKey(self, newKey, expire=604800): def addForwardKey(self, newKey, expire=604800):
if not self._core._utils.validatePubKey(newKey): if not self._core._utils.validatePubKey(newKey):
# Do not add if something went wrong with the key
raise onionrexceptions.InvalidPubkey(newKey) raise onionrexceptions.InvalidPubkey(newKey)
if newKey in self._getForwardKeys():
return False
# Add a forward secrecy key for the peer
conn = sqlite3.connect(self._core.peerDB, timeout=10) conn = sqlite3.connect(self._core.peerDB, timeout=10)
c = conn.cursor() c = conn.cursor()
# Get the time we're inserting the key at
timeInsert = self._core._utils.getEpoch()
# Look at our current keys for duplicate key data or time
for entry in self._getForwardKeys():
if entry[0] == newKey:
return False
if entry[1] == timeInsert:
timeInsert += 1
time.sleep(1) # Sleep if our time is the same in order to prevent duplicate time records
# Add a forward secrecy key for the peer
# Prepare the insert # Prepare the insert
time = self._core._utils.getEpoch() command = (self.publicKey, newKey, timeInsert, timeInsert + expire)
command = (self.publicKey, newKey, time, time + expire)
c.execute("INSERT INTO forwardKeys VALUES(?, ?, ?, ?);", command) c.execute("INSERT INTO forwardKeys VALUES(?, ?, ?, ?);", command)
conn.commit() conn.commit()
conn.close() conn.close()
return return True
def findAndSetID(self): @classmethod
'''Find any info about the user from existing blocks and cache it to their DB entry''' def list_friends(cls, coreInst):
infoBlocks = [] friendList = []
for bHash in self._core.getBlocksByType('userInfo'): for x in coreInst.listPeers(trust=1):
block = onionrblockapi.Block(bHash, core=self._core) friendList.append(cls(coreInst, x))
if block.signer == self.publicKey: return list(friendList)
if block.verifySig():
newName = block.getMetadata('name')
if newName.isalnum():
logger.info('%s is now using the name %s.' % (self.publicKey, self._core._utils.escapeAnsi(newName)))
self._core.setPeerInfo(self.publicKey, 'name', newName)
else:
raise onionrexceptions.InvalidPubkey

View File

@ -278,12 +278,19 @@ class OnionrUtils:
break break
if (self.getEpoch() - metadata[i]) > maxAge: if (self.getEpoch() - metadata[i]) > maxAge:
logger.warn('Block is outdated: %s' % (metadata[i],)) logger.warn('Block is outdated: %s' % (metadata[i],))
break
elif i == 'expire': elif i == 'expire':
try: try:
assert int(metadata[i]) > self.getEpoch() assert int(metadata[i]) > self.getEpoch()
except AssertionError: except AssertionError:
logger.warn('Block is expired') logger.warn('Block is expired')
break break
elif i == 'encryptType':
try:
assert metadata[i] in ('asym', 'sym', '')
except AssertionError:
logger.warn('Invalid encryption mode')
break
else: else:
# if metadata loop gets no errors, it does not break, therefore metadata is valid # if metadata loop gets no errors, it does not break, therefore metadata is valid
# make sure we do not have another block with the same data content (prevent data duplication and replay attacks) # make sure we do not have another block with the same data content (prevent data duplication and replay attacks)
@ -408,12 +415,14 @@ class OnionrUtils:
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
''' '''
blockList = self._core.getBlockList() blockList = self._core.getBlockList()
exist = False
if scanDir == '': if scanDir == '':
scanDir = self._core.blockDataLocation scanDir = self._core.blockDataLocation
if not scanDir.endswith('/'): if not scanDir.endswith('/'):
scanDir += '/' scanDir += '/'
for block in glob.glob(scanDir + "*.dat"): for block in glob.glob(scanDir + "*.dat"):
if block.replace(scanDir, '').replace('.dat', '') not in blockList: if block.replace(scanDir, '').replace('.dat', '') not in blockList:
exist = True
logger.info('Found new block on dist %s' % block) logger.info('Found new block on dist %s' % block)
with open(block, 'rb') as newBlock: with open(block, 'rb') as newBlock:
block = block.replace(scanDir, '').replace('.dat', '') block = block.replace(scanDir, '').replace('.dat', '')
@ -423,6 +432,8 @@ class OnionrUtils:
self._core._utils.processBlockMetadata(block) self._core._utils.processBlockMetadata(block)
else: else:
logger.warn('Failed to verify hash for %s' % block) logger.warn('Failed to verify hash for %s' % block)
if not exist:
print('No blocks found to import')
def progressBar(self, value = 0, endvalue = 100, width = None): def progressBar(self, value = 0, endvalue = 100, width = None):
''' '''
@ -469,7 +480,7 @@ class OnionrUtils:
retData = False retData = False
return retData return retData
def doGetRequest(self, url, port=0, proxyType='tor', ignoreAPI=False): def doGetRequest(self, url, port=0, proxyType='tor', ignoreAPI=False, returnHeaders=False):
''' '''
Do a get request through a local tor or i2p instance Do a get request through a local tor or i2p instance
''' '''
@ -509,6 +520,9 @@ class OnionrUtils:
if not 'ConnectTimeoutError' in str(e) and not 'Request rejected or failed' in str(e): if not 'ConnectTimeoutError' in str(e) and not 'Request rejected or failed' in str(e):
logger.debug('Error: %s' % str(e)) logger.debug('Error: %s' % str(e))
retData = False retData = False
if returnHeaders:
return (retData, response_headers)
else:
return retData return retData
def strToBytes(self, data): def strToBytes(self, data):

69
onionr/setupconfig.py Normal file
View File

@ -0,0 +1,69 @@
import os, json
import config, logger
def setup_config(dataDir, o_inst = None):
data_exists = os.path.exists(dataDir)
if not data_exists:
os.mkdir(dataDir)
if os.path.exists('static-data/default_config.json'):
# this is the default config, it will be overwritten if a config file already exists. Else, it saves it
with open('static-data/default_config.json', 'r') as configReadIn:
config.set_config(json.loads(configReadIn.read()))
else:
# the default config file doesn't exist, try hardcoded config
logger.warn('Default configuration file does not exist, switching to hardcoded fallback configuration!')
config.set_config({'dev_mode': True, 'log': {'file': {'output': True, 'path': dataDir + 'output.log'}, 'console': {'output': True, 'color': True}}})
if not data_exists:
config.save()
config.reload() # this will read the configuration file into memory
settings = 0b000
if config.get('log.console.color', True):
settings = settings | logger.USE_ANSI
if config.get('log.console.output', True):
settings = settings | logger.OUTPUT_TO_CONSOLE
if config.get('log.file.output', True):
settings = settings | logger.OUTPUT_TO_FILE
logger.set_settings(settings)
if not o_inst is None:
if str(config.get('general.dev_mode', True)).lower() == 'true':
o_inst._developmentMode = True
logger.set_level(logger.LEVEL_DEBUG)
else:
o_inst._developmentMode = False
logger.set_level(logger.LEVEL_INFO)
verbosity = str(config.get('log.verbosity', 'default')).lower().strip()
if not verbosity in ['default', 'null', 'none', 'nil']:
map = {
str(logger.LEVEL_DEBUG) : logger.LEVEL_DEBUG,
'verbose' : logger.LEVEL_DEBUG,
'debug' : logger.LEVEL_DEBUG,
str(logger.LEVEL_INFO) : logger.LEVEL_INFO,
'info' : logger.LEVEL_INFO,
'information' : logger.LEVEL_INFO,
str(logger.LEVEL_WARN) : logger.LEVEL_WARN,
'warn' : logger.LEVEL_WARN,
'warning' : logger.LEVEL_WARN,
'warnings' : logger.LEVEL_WARN,
str(logger.LEVEL_ERROR) : logger.LEVEL_ERROR,
'err' : logger.LEVEL_ERROR,
'error' : logger.LEVEL_ERROR,
'errors' : logger.LEVEL_ERROR,
str(logger.LEVEL_FATAL) : logger.LEVEL_FATAL,
'fatal' : logger.LEVEL_FATAL,
str(logger.LEVEL_IMPORTANT) : logger.LEVEL_IMPORTANT,
'silent' : logger.LEVEL_IMPORTANT,
'quiet' : logger.LEVEL_IMPORTANT,
'important' : logger.LEVEL_IMPORTANT
}
if verbosity in map:
logger.set_level(map[verbosity])
else:
logger.warn('Verbosity level %s is not valid, using default verbosity.' % verbosity)
return data_exists

View File

@ -1 +0,0 @@
dd3llxdp5q6ak3zmmicoy3jnodmroouv2xr7whkygiwp3rl7nf23gdad.onion

View File

@ -19,8 +19,10 @@
''' '''
# Imports some useful libraries # Imports some useful libraries
import logger, config, threading, time, uuid, subprocess, sys import threading, time, uuid, subprocess, sys
import config, logger
from onionrblockapi import Block from onionrblockapi import Block
import onionrplugins
plugin_name = 'cliui' plugin_name = 'cliui'
PLUGIN_VERSION = '0.0.1' PLUGIN_VERSION = '0.0.1'
@ -29,7 +31,11 @@ class OnionrCLIUI:
def __init__(self, apiInst): def __init__(self, apiInst):
self.api = apiInst self.api = apiInst
self.myCore = apiInst.get_core() self.myCore = apiInst.get_core()
return self.shutdown = False
self.running = 'undetermined'
enabled = onionrplugins.get_enabled_plugins()
self.mail_enabled = 'pms' in enabled
self.flow_enabled = 'flow' in enabled
def subCommand(self, command, args=None): def subCommand(self, command, args=None):
try: try:
@ -42,26 +48,27 @@ class OnionrCLIUI:
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
def isRunning(self):
while not self.shutdown:
if self.myCore._utils.localCommand('ping', maxWait=5) == 'pong!':
self.running = 'Yes'
else:
self.running = 'No'
time.sleep(5)
def refresh(self): def refresh(self):
print('\n' * 80 + logger.colors.reset) print('\n' * 80 + logger.colors.reset)
def start(self): def start(self):
'''Main CLI UI interface menu''' '''Main CLI UI interface menu'''
showMenu = True showMenu = True
isOnline = 'No'
firstRun = True
choice = '' choice = ''
if self.myCore._utils.localCommand('ping', maxWait=10) == 'pong!': threading.Thread(target=self.isRunning).start()
firstRun = False
while showMenu: while showMenu:
if self.myCore._utils.localCommand('ping', maxWait=2) == 'pong!': print('Onionr\n------')
isOnline = "Yes" print('''Daemon Running: ''' + self.running + '''
else: 1. Flow (Anonymous public shout box, use at your own risk)
isOnline = "No"
print('''Daemon Running: ''' + isOnline + '''
1. Flow (Anonymous public chat, use at your own risk)
2. Mail (Secure email-like service) 2. Mail (Secure email-like service)
3. File Sharing 3. File Sharing
4. Quit (Does not shutdown daemon) 4. Quit (Does not shutdown daemon)
@ -72,21 +79,27 @@ class OnionrCLIUI:
choice = "quit" choice = "quit"
if choice in ("flow", "1"): if choice in ("flow", "1"):
if self.flow_enabled:
self.subCommand("flow") self.subCommand("flow")
else:
print('Plugin not enabled')
elif choice in ("2", "mail"): elif choice in ("2", "mail"):
if self.mail_enabled:
self.subCommand("mail") self.subCommand("mail")
else:
print('Plugin not enabled')
elif choice in ("3", "file sharing", "file"): elif choice in ("3", "file sharing", "file"):
filename = input("Enter full path to file: ").strip() filename = input("Enter full path to file: ").strip()
self.subCommand("addfile", filename) self.subCommand("addfile", filename)
elif choice in ("4", "quit"): elif choice in ("4", "quit"):
showMenu = False showMenu = False
self.shutdown = True
elif choice == "": elif choice == "":
pass pass
else: else:
logger.error("Invalid choice") logger.error("Invalid choice")
return return
def on_init(api, data = None): def on_init(api, data = None):
''' '''
This event is called after Onionr is initialized, but before the command This event is called after Onionr is initialized, but before the command

View File

@ -0,0 +1,5 @@
{
"name" : "contactmanager",
"version" : "1.0",
"author" : "onionr"
}

View File

@ -0,0 +1,39 @@
'''
Onionr - P2P Anonymous Storage Network
This is an interactive menu-driven CLI interface for Onionr
'''
'''
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/>.
'''
plugin_name = 'contactmanager'
class OnionrContactManager:
def __init__(self, api):
return
def on_init(api, data = None):
'''
This event is called after Onionr is initialized, but before the command
inputted is executed. Could be called when daemon is starting or when
just the client is running.
'''
# Doing this makes it so that the other functions can access the api object
# by simply referencing the variable `pluginapi`.
pluginapi = api
ui = OnionrContactManager(api)
#api.commands.register('interactive', ui.start)
return

View File

@ -19,7 +19,7 @@
''' '''
# Imports some useful libraries # Imports some useful libraries
import logger, config, threading, time, readline, datetime, sys, json import logger, config, threading, time, datetime, sys, json
from onionrblockapi import Block from onionrblockapi import Block
import onionrexceptions, onionrusers import onionrexceptions, onionrusers
import locale import locale

View File

@ -0,0 +1,13 @@
import onionrblockapi
def load_inbox(myCore):
inbox_list = []
deleted = myCore.keyStore.get('deleted_mail')
if deleted is None:
deleted = []
for blockHash in myCore.getBlocksByType('pm'):
block = onionrblockapi.Block(blockHash, core=myCore)
block.decrypt()
if block.decrypted and blockHash not in deleted:
inbox_list.append(blockHash)
return inbox_list

View File

@ -0,0 +1,65 @@
'''
Onionr - P2P Anonymous Storage Network
HTTP endpoints for mail plugin.
'''
'''
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
import sys, os, json
from flask import Response, request, redirect, Blueprint, abort
import core
from onionrusers import contactmanager
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
import loadinbox, sentboxdb
flask_blueprint = Blueprint('mail', __name__)
c = core.Core()
kv = c.keyStore
@flask_blueprint.route('/mail/ping')
def mail_ping():
return 'pong!'
@flask_blueprint.route('/mail/deletemsg/<block>', methods=['POST'])
def mail_delete(block):
if not c._utils.validateHash(block):
abort(504)
existing = kv.get('deleted_mail')
if existing is None:
existing = []
if block not in existing:
existing.append(block)
kv.put('deleted_mail', existing)
return 'success'
@flask_blueprint.route('/mail/getinbox')
def list_inbox():
return ','.join(loadinbox.load_inbox(c))
@flask_blueprint.route('/mail/getsentbox')
def list_sentbox():
sentbox_list = sentboxdb.SentBox(c).listSent()
sentbox_list_copy = list(sentbox_list)
deleted = kv.get('deleted_mail')
if deleted is None:
deleted = []
for x in range(len(sentbox_list_copy) - 1):
if sentbox_list_copy[x]['hash'] in deleted:
x -= 1
sentbox_list.pop(x)
else:
sentbox_list[x]['name'] = contactmanager.ContactManager(c, sentbox_list_copy[x]['peer'], saveUser=False).get_info('name')
return json.dumps(sentbox_list)

View File

@ -19,7 +19,7 @@
''' '''
# Imports some useful libraries # Imports some useful libraries
import logger, config, threading, time, readline, datetime import logger, config, threading, time, datetime
from onionrblockapi import Block from onionrblockapi import Block
import onionrexceptions import onionrexceptions
from onionrusers import onionrusers from onionrusers import onionrusers
@ -27,12 +27,13 @@ import locale, sys, os, json
locale.setlocale(locale.LC_ALL, '') locale.setlocale(locale.LC_ALL, '')
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
import sentboxdb # import after path insert
plugin_name = 'pms' plugin_name = 'pms'
PLUGIN_VERSION = '0.0.1' PLUGIN_VERSION = '0.0.1'
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
import sentboxdb, mailapi, loadinbox # import after path insert
flask_blueprint = mailapi.flask_blueprint
def draw_border(text): def draw_border(text):
#https://stackoverflow.com/a/20757491 #https://stackoverflow.com/a/20757491
lines = text.splitlines() lines = text.splitlines()
@ -43,7 +44,6 @@ def draw_border(text):
res.append('' + '' * width + '') res.append('' + '' * width + '')
return '\n'.join(res) return '\n'.join(res)
class MailStrings: class MailStrings:
def __init__(self, mailInstance): def __init__(self, mailInstance):
self.mailInstance = mailInstance self.mailInstance = mailInstance
@ -78,7 +78,7 @@ class OnionrMail:
displayList = [] displayList = []
subject = '' subject = ''
# this could use a lot of memory if someone has recieved a lot of messages # this could use a lot of memory if someone has received a lot of messages
for blockHash in self.myCore.getBlocksByType('pm'): for blockHash in self.myCore.getBlocksByType('pm'):
pmBlocks[blockHash] = Block(blockHash, core=self.myCore) pmBlocks[blockHash] = Block(blockHash, core=self.myCore)
pmBlocks[blockHash].decrypt() pmBlocks[blockHash].decrypt()
@ -191,7 +191,6 @@ class OnionrMail:
finally: finally:
if choice == '-q': if choice == '-q':
entering = False entering = False
return return
def get_sent_list(self, display=True): def get_sent_list(self, display=True):
@ -294,26 +293,20 @@ class OnionrMail:
logger.warn('Invalid choice.') logger.warn('Invalid choice.')
return return
def add_deleted(keyStore, bHash):
existing = keyStore.get('deleted_mail')
if existing is None:
existing = []
else:
if bHash in existing:
return
keyStore.put('deleted_mail', existing.append(bHash))
def on_insertblock(api, data={}): def on_insertblock(api, data={}):
sentboxTools = sentboxdb.SentBox(api.get_core()) sentboxTools = sentboxdb.SentBox(api.get_core())
meta = json.loads(data['meta']) meta = json.loads(data['meta'])
sentboxTools.addToSent(data['hash'], data['peer'], data['content'], meta['subject']) 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):
''' '''

View File

@ -25,11 +25,16 @@ class SentBox:
self.dbLocation = mycore.dataDir + 'sentbox.db' self.dbLocation = mycore.dataDir + 'sentbox.db'
if not os.path.exists(self.dbLocation): if not os.path.exists(self.dbLocation):
self.createDB() self.createDB()
self.conn = sqlite3.connect(self.dbLocation)
self.cursor = self.conn.cursor()
self.core = mycore self.core = mycore
return return
def connect(self):
self.conn = sqlite3.connect(self.dbLocation)
self.cursor = self.conn.cursor()
def close(self):
self.conn.close()
def createDB(self): def createDB(self):
conn = sqlite3.connect(self.dbLocation) conn = sqlite3.connect(self.dbLocation)
cursor = conn.cursor() cursor = conn.cursor()
@ -42,22 +47,29 @@ class SentBox:
); );
''') ''')
conn.commit() conn.commit()
conn.close()
return return
def listSent(self): def listSent(self):
self.connect()
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], 'subject': entry[3], 'date': entry[4]}) retData.append({'hash': entry[0], 'peer': entry[1], 'message': entry[2], 'subject': entry[3], 'date': entry[4]})
self.close()
return retData return retData
def addToSent(self, blockID, peer, message, subject=''): def addToSent(self, blockID, peer, message, subject=''):
self.connect()
args = (blockID, peer, message, subject, 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()
self.close()
return return
def removeSent(self, blockID): def removeSent(self, blockID):
self.connect()
args = (blockID,) args = (blockID,)
self.cursor.execute('DELETE FROM sent where hash=?', args) self.cursor.execute('DELETE FROM sent where hash=?', args)
self.conn.commit() self.conn.commit()
self.close()
return return

View File

@ -8,7 +8,8 @@
"security_level": 0, "security_level": 0,
"max_block_age": 2678400, "max_block_age": 2678400,
"bypass_tor_check": false, "bypass_tor_check": false,
"public_key": "" "public_key": "",
"random_bind_ip": true
}, },
"www" : { "www" : {
@ -48,7 +49,7 @@
"verbosity" : "default", "verbosity" : "default",
"file": { "file": {
"output": true, "output": false,
"path": "output.log" "path": "output.log"
}, },

View File

@ -0,0 +1,97 @@
/*
Onionr - P2P Anonymous Storage Network
This file handles the UI for managing friends/contacts
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/>.
*/
friendListDisplay = document.getElementById('friendList')
addForm = document.getElementById('addFriend')
function removeFriend(pubkey){
post_to_url('/friends/remove/' + pubkey, {'token': webpass})
}
addForm.onsubmit = function(){
var friend = document.getElementsByName('addKey')[0]
var alias = document.getElementsByName('data')[0]
fetch('/friends/add/' + friend.value, {
method: 'POST',
headers: {
"token": webpass
}}).then(function(data) {
if (alias.value.trim().length > 0){
post_to_url('/friends/setinfo/' + friend.value + '/name', {'data': alias.value, 'token': webpass})
}
})
return false
}
fetch('/friends/list', {
headers: {
"token": webpass
}})
.then((resp) => resp.json()) // Transform the data into json
.then(function(resp) {
var keys = [];
for(var k in resp) keys.push(k);
console.log(keys)
friendListDisplay.innerHTML = 'Click name to view info<br><br>'
for (var i = 0; i < keys.length; i++){
var peer = keys[i]
var name = resp[keys[i]]['name']
if (name === null || name === ''){
name = peer
}
var entry = document.createElement('div')
var nameText = document.createElement('input')
removeButton = document.createElement('button')
removeButton.classList.add('friendRemove')
removeButton.classList.add('dangerBtn')
entry.setAttribute('data-pubkey', peer)
removeButton.innerText = 'X'
nameText.value = name
nameText.readOnly = true
nameText.style.fontStyle = "italic"
entry.style.paddingTop = '8px'
entry.appendChild(removeButton)
entry.appendChild(nameText)
friendListDisplay.appendChild(entry)
entry.onclick = (function(entry, nameText, peer) {return function() {
if (nameText.length == 0){
nameText = 'Anonymous'
}
document.getElementById('friendPubkey').value = peer
document.getElementById('friendName').innerText = nameText
overlay('friendInfo')
};})(entry, nameText.value, peer);
}
// If friend delete buttons are pressed
var friendRemoveBtns = document.getElementsByClassName('friendRemove')
for (var x = 0; x < friendRemoveBtns.length; x++){
var friendKey = friendRemoveBtns[x].parentElement.getAttribute('data-pubkey')
friendRemoveBtns[x].onclick = function(){
removeFriend(friendKey)
}
}
})
document.getElementById('defriend').onclick = function(){
removeFriend(document.getElementById('friendPubkey').value)
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,41 @@
h2, h3{
font-family: Arial, Helvetica, sans-serif;
}
form{
border: 1px solid black;
border-radius: 5px;
padding: 1em;
margin-right: 10%;
}
form label{
display: block;
margin-top: 0.5em;
margin-bottom: 0.5em;
}
#friendList{
display: inline;
}
#friendList span{
text-align: center;
}
#friendList button{
display: inline;
margin-right: 10px;
}
#friendInfo .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;
}
#defriend{
display: block;
margin-top: 1em;
}

View File

@ -16,7 +16,13 @@
<div class='content'> <div class='content'>
<img class='logo' src='/shared/onionr-icon.png' alt='onionr logo'> <img class='logo' src='/shared/onionr-icon.png' alt='onionr logo'>
<span class='logoText'>Onionr Mail ✉️</span> <span class='logoText'>Onionr Mail ✉️</span>
<div>Current Used Identity: <input class='myPub' type='text' readonly> <button class='refresh'>Refresh Page</button></div> <br><br>
<div><a href='/' class='idLink'>Home</a> <button class='refresh'>Refresh Page</button></div>
<div class='mailPing'>
API server either shutdown, has disabled mail, or has experienced a bug.
</div>
<br>
<div>Current Used Identity: <input class='myPub' type='text' readonly></div>
<br><br> <br><br>
<div class="btn-group" id='tabBtns'> <div class="btn-group" id='tabBtns'>
<button class='activeTab'>Inbox</button><button>Sentbox</button><button>Send Message</button> <button class='activeTab'>Inbox</button><button>Sentbox</button><button>Send Message</button>
@ -30,6 +36,9 @@
<div> <div>
From: <input type='text' id='fromUser' readonly> Signature: <span id='sigValid'></span> From: <input type='text' id='fromUser' readonly> Signature: <span id='sigValid'></span>
</div> </div>
<div>
<button id='replyBtn' class='primaryBtn'>Reply</button>
</div>
<div id='signatureValidity'></div> <div id='signatureValidity'></div>
<div id='threadDisplay' class='pre messageContent'> <div id='threadDisplay' class='pre messageContent'>
</div> </div>
@ -45,6 +54,7 @@
</div> </div>
<div id='sendMessage' class='overlay'> <div id='sendMessage' class='overlay'>
<div class='overlayContent'> <div class='overlayContent'>
<label>Select friend: <select id='friendSelect'></select></label>
<form method='post' action='/apipoints/mail/send' id='sendForm' enctype="application/x-www-form-urlencoded"> <form method='post' action='/apipoints/mail/send' id='sendForm' enctype="application/x-www-form-urlencoded">
<span class='closeOverlay' overlay='sendMessage'></span> <span class='closeOverlay' overlay='sendMessage'></span>
To: <input id='draftID' type='text' name='to' placeholder='pubkey' required> To: <input id='draftID' type='text' name='to' placeholder='pubkey' required>

View File

@ -53,6 +53,11 @@ input{
margin: 1em; margin: 1em;
} }
.mailPing{
display: none;
color: orange;
}
.danger{ .danger{
color: red; color: red;
} }
@ -87,6 +92,17 @@ input{
color: black; color: black;
} }
#replyBtn{
margin-top: 1em;
}
.primaryBtn{
border-radius: 3px;
padding: 3px;
color: black;
width: 5%;
}
.successBtn{ .successBtn{
background-color: #28a745; background-color: #28a745;
border-radius: 3px; border-radius: 3px;

View File

@ -24,8 +24,27 @@ threadPlaceholder = document.getElementById('threadPlaceholder')
tabBtns = document.getElementById('tabBtns') tabBtns = document.getElementById('tabBtns')
threadContent = {} threadContent = {}
myPub = httpGet('/getActivePubkey') myPub = httpGet('/getActivePubkey')
replyBtn = document.getElementById('replyBtn')
function openThread(bHash, sender, date, sigBool){ function openReply(bHash){
var inbox = document.getElementsByClassName('threadEntry')
var entry = ''
var friendName = ''
var key = ''
for(var i = 0; i < inbox.length; i++) {
if (inbox[i].getAttribute('data-hash') === bHash){
entry = inbox[i]
}
}
if (entry.getAttribute('data-nameSet') == 'true'){
document.getElementById('friendSelect').value = entry.getElementsByTagName('input')[0].value
}
key = entry.getAttribute('data-pubkey')
document.getElementById('draftID').value = key
setActiveTab('send message')
}
function openThread(bHash, sender, date, sigBool, pubkey){
var messageDisplay = document.getElementById('threadDisplay') var messageDisplay = document.getElementById('threadDisplay')
var blockContent = httpGet('/getblockbody/' + bHash) var blockContent = httpGet('/getblockbody/' + bHash)
document.getElementById('fromUser').value = sender document.getElementById('fromUser').value = sender
@ -38,18 +57,22 @@ function openThread(bHash, sender, date, sigBool){
sigEl.classList.remove('danger') sigEl.classList.remove('danger')
} }
else{ else{
sigMsg = 'Bad/no ' + sigMsg + ' (message could be fake)' sigMsg = 'Bad/no ' + sigMsg + ' (message could be impersonating someone)'
sigEl.classList.add('danger') sigEl.classList.add('danger')
replyBtn.style.display = 'none'
} }
sigEl.innerText = sigMsg sigEl.innerText = sigMsg
overlay('messageDisplay') overlay('messageDisplay')
replyBtn.onclick = function(){
openReply(bHash)
}
} }
function setActiveTab(tabName){ function setActiveTab(tabName){
threadPart.innerHTML = "" threadPart.innerHTML = ""
switch(tabName){ switch(tabName){
case 'inbox': case 'inbox':
getInbox() refreshPms()
break break
case 'sentbox': case 'sentbox':
getSentbox() getSentbox()
@ -60,7 +83,39 @@ function setActiveTab(tabName){
} }
} }
function loadInboxEntrys(bHash){ function deleteMessage(bHash){
fetch('/mail/deletemsg/' + bHash, {
"method": "post",
headers: {
"token": webpass
}})
.then((resp) => resp.text()) // Transform the data into json
.then(function(resp) {
})
}
function mailPing(){
fetch('/mail/ping', {
"method": "get",
headers: {
"token": webpass
}})
.then(function(resp) {
var pings = document.getElementsByClassName('mailPing')
if (resp.ok){
for (var i=0; i < pings.length; i++){
pings[i].style.display = 'none';
}
}
else{
for (var i=0; i < pings.length; i++){
pings[i].style.display = 'block';
}
}
})
}
function loadInboxEntries(bHash){
fetch('/getblockheader/' + bHash, { fetch('/getblockheader/' + bHash, {
headers: { headers: {
"token": webpass "token": webpass
@ -74,26 +129,31 @@ function loadInboxEntrys(bHash){
var subjectLine = document.createElement('span') var subjectLine = document.createElement('span')
var dateStr = document.createElement('span') var dateStr = document.createElement('span')
var validSig = document.createElement('span') var validSig = document.createElement('span')
var deleteBtn = document.createElement('button')
var humanDate = new Date(0) var humanDate = new Date(0)
var metadata = resp['metadata'] var metadata = resp['metadata']
humanDate.setUTCSeconds(resp['meta']['time']) humanDate.setUTCSeconds(resp['meta']['time'])
validSig.style.display = 'none'
if (resp['meta']['signer'] != ''){ if (resp['meta']['signer'] != ''){
senderInput.value = httpGet('/getHumanReadable/' + resp['meta']['signer']) senderInput.value = httpGet('/friends/getinfo/' + resp['meta']['signer'] + '/name')
} }
if (resp['meta']['validSig']){ if (! resp['meta']['validSig']){
validSig.innerText = 'Signature Validity: Good' validSig.style.display = 'inline'
}
else{
validSig.innerText = 'Signature Validity: Bad' validSig.innerText = 'Signature Validity: Bad'
validSig.style.color = 'red' validSig.style.color = 'red'
} }
entry.setAttribute('data-nameSet', true)
if (senderInput.value == ''){ if (senderInput.value == ''){
senderInput.value = 'Anonymous' senderInput.value = resp['meta']['signer']
entry.setAttribute('data-nameSet', false)
} }
bHashDisplay.innerText = bHash.substring(0, 10) bHashDisplay.innerText = bHash.substring(0, 10)
entry.setAttribute('hash', bHash) entry.setAttribute('data-hash', bHash)
entry.setAttribute('data-pubkey', resp['meta']['signer'])
senderInput.readOnly = true senderInput.readOnly = true
dateStr.innerText = humanDate.toString() dateStr.innerText = humanDate.toString()
deleteBtn.innerText = 'X'
deleteBtn.classList.add('dangerBtn', 'deleteBtn')
if (metadata['subject'] === undefined || metadata['subject'] === null) { if (metadata['subject'] === undefined || metadata['subject'] === null) {
subjectLine.innerText = '()' subjectLine.innerText = '()'
} }
@ -102,15 +162,24 @@ function loadInboxEntrys(bHash){
} }
//entry.innerHTML = 'sender ' + resp['meta']['signer'] + ' - ' + resp['meta']['time'] //entry.innerHTML = 'sender ' + resp['meta']['signer'] + ' - ' + resp['meta']['time']
threadPart.appendChild(entry) threadPart.appendChild(entry)
entry.appendChild(deleteBtn)
entry.appendChild(bHashDisplay) entry.appendChild(bHashDisplay)
entry.appendChild(senderInput) entry.appendChild(senderInput)
entry.appendChild(validSig)
entry.appendChild(subjectLine) entry.appendChild(subjectLine)
entry.appendChild(dateStr) entry.appendChild(dateStr)
entry.appendChild(validSig)
entry.classList.add('threadEntry') entry.classList.add('threadEntry')
entry.onclick = function(){ entry.onclick = function(event){
openThread(entry.getAttribute('hash'), senderInput.value, dateStr.innerText, resp['meta']['validSig']) if (event.target.classList.contains('deleteBtn')){
return
}
openThread(entry.getAttribute('data-hash'), senderInput.value, dateStr.innerText, resp['meta']['validSig'], entry.getAttribute('data-pubkey'))
}
deleteBtn.onclick = function(){
entry.parentNode.removeChild(entry);
deleteMessage(entry.getAttribute('data-hash'))
} }
}.bind(bHash)) }.bind(bHash))
@ -127,7 +196,7 @@ function getInbox(){
threadPlaceholder.style.display = 'none' threadPlaceholder.style.display = 'none'
showed = true showed = true
} }
loadInboxEntrys(pms[i]) loadInboxEntries(pms[i])
} }
if (! showed){ if (! showed){
threadPlaceholder.style.display = 'block' threadPlaceholder.style.display = 'block'
@ -135,7 +204,7 @@ function getInbox(){
} }
function getSentbox(){ function getSentbox(){
fetch('/apipoints/mail/sentbox', { fetch('/mail/getsentbox', {
headers: { headers: {
"token": webpass "token": webpass
}}) }})
@ -143,25 +212,49 @@ function getSentbox(){
.then(function(resp) { .then(function(resp) {
var keys = []; var keys = [];
var entry = document.createElement('div') var entry = document.createElement('div')
var entryUsed;
for(var k in resp) keys.push(k); for(var k in resp) keys.push(k);
if (keys.length == 0){
threadPart.innerHTML = "nothing to show here yet."
}
for (var i = 0; i < keys.length; i++){ for (var i = 0; i < keys.length; i++){
var entry = document.createElement('div') var entry = document.createElement('div')
var obj = resp[i]; var obj = resp[i]
var toLabel = document.createElement('span') var toLabel = document.createElement('span')
toLabel.innerText = 'To: ' toLabel.innerText = 'To: '
var toEl = document.createElement('input') var toEl = document.createElement('input')
var sentDate = document.createElement('span')
var humanDate = new Date(0)
humanDate.setUTCSeconds(resp[i]['date'])
var preview = document.createElement('span') var preview = document.createElement('span')
var deleteBtn = document.createElement('button')
var message = resp[i]['message']
deleteBtn.classList.add('deleteBtn', 'dangerBtn')
deleteBtn.innerText = 'X'
toEl.readOnly = true toEl.readOnly = true
toEl.value = resp[keys[i]][1] sentDate.innerText = humanDate
preview.innerText = '(' + resp[keys[i]][2] + ')' if (resp[i]['name'] == null){
toEl.value = resp[i]['peer']
}
else{
toEl.value = resp[i]['name']
}
preview.innerText = '(' + resp[i]['subject'] + ')'
entry.setAttribute('data-hash', resp[i]['hash'])
entry.appendChild(deleteBtn)
entry.appendChild(toLabel) entry.appendChild(toLabel)
entry.appendChild(toEl) entry.appendChild(toEl)
entry.appendChild(preview) entry.appendChild(preview)
entryUsed = resp[keys[i]] entry.appendChild(sentDate)
entry.onclick = function(){ entry.onclick = (function(tree, el, msg) {return function() {
console.log(resp) console.log(resp)
showSentboxWindow(toEl.value, entryUsed[0]) if (! entry.classList.contains('deleteBtn')){
showSentboxWindow(el.value, msg)
}
};})(entry, toEl, message);
deleteBtn.onclick = function(){
entry.parentNode.removeChild(entry);
deleteMessage(entry.getAttribute('data-hash'))
} }
threadPart.appendChild(entry) threadPart.appendChild(entry)
} }
@ -175,15 +268,17 @@ function showSentboxWindow(to, content){
overlay('sentboxDisplay') overlay('sentboxDisplay')
} }
fetch('/getblocksbytype/pm', { function refreshPms(){
fetch('/mail/getinbox', {
headers: { headers: {
"token": webpass "token": webpass
}}) }})
.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(',')
setActiveTab('inbox') getInbox()
}) })
}
tabBtns.onclick = function(event){ tabBtns.onclick = function(event){
var children = tabBtns.children var children = tabBtns.children
@ -196,13 +291,12 @@ tabBtns.onclick = function(event){
} }
var idStrings = document.getElementsByClassName('myPub') var idStrings = document.getElementsByClassName('myPub')
var myHumanReadable = httpGet('/getHumanReadable/' + myPub)
for (var i = 0; i < idStrings.length; i++){ for (var i = 0; i < idStrings.length; i++){
if (idStrings[i].tagName.toLowerCase() == 'input'){ if (idStrings[i].tagName.toLowerCase() == 'input'){
idStrings[i].value = myHumanReadable idStrings[i].value = myPub
} }
else{ else{
idStrings[i].innerText = myHumanReadable idStrings[i].innerText = myPub
} }
} }
@ -210,9 +304,39 @@ for (var i = 0; i < document.getElementsByClassName('refresh').length; i++){
document.getElementsByClassName('refresh')[i].style.float = 'right' 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'
}
}
fetch('/friends/list', {
headers: {
"token": webpass
}})
.then((resp) => resp.json()) // Transform the data into json
.then(function(resp) {
var friendSelectParent = document.getElementById('friendSelect')
var keys = [];
var friend
for(var k in resp) keys.push(k);
friendSelectParent.appendChild(document.createElement('option'))
for (var i = 0; i < keys.length; i++) {
var option = document.createElement("option")
var name = resp[keys[i]]['name']
option.value = keys[i]
if (name.length == 0){
option.text = keys[i]
}
else{
option.text = name
}
friendSelectParent.appendChild(option)
}
for (var i = 0; i < keys.length; i++){
//friendSelectParent
//alert(resp[keys[i]]['name'])
}
})
setActiveTab('inbox')
setInterval(function(){mailPing()}, 10000)
mailPing()

28
onionr/static-data/www/mail/sendmail.js Normal file → Executable file
View File

@ -18,11 +18,16 @@
*/ */
var sendbutton = document.getElementById('sendMail') var sendbutton = document.getElementById('sendMail')
messageContent = document.getElementById('draftText')
to = document.getElementById('draftID')
subject = document.getElementById('draftSubject')
friendPicker = document.getElementById('friendSelect')
function sendMail(to, message, subject){ function sendMail(to, message, subject){
//postData = {"postData": '{"to": "' + to + '", "message": "' + message + '"}'} // galaxy brain //postData = {"postData": '{"to": "' + to + '", "message": "' + message + '"}'} // galaxy brain
postData = {'message': message, 'to': to, 'type': 'pm', 'encrypt': true, 'meta': JSON.stringify({'subject': subject})} postData = {'message': message, 'to': to, 'type': 'pm', 'encrypt': true, 'meta': JSON.stringify({'subject': subject})}
postData = JSON.stringify(postData) postData = JSON.stringify(postData)
sendForm.style.display = 'none'
fetch('/insertblock', { fetch('/insertblock', {
method: 'POST', method: 'POST',
body: postData, body: postData,
@ -32,14 +37,23 @@ function sendMail(to, message, subject){
}}) }})
.then((resp) => resp.text()) // Transform the data into json .then((resp) => resp.text()) // Transform the data into json
.then(function(data) { .then(function(data) {
sendForm.style.display = 'block'
alert('Queued for sending!')
}) })
} }
sendForm.onsubmit = function(){ var friendPicker = document.getElementById('friendSelect')
var messageContent = document.getElementById('draftText') friendPicker.onchange = function(){
var to = document.getElementById('draftID') to.value = friendPicker.value
var subject = document.getElementById('draftSubject') }
sendMail(to.value, messageContent.value, subject.value) sendForm.onsubmit = function(){
return false; if (friendPicker.value.trim().length !== 0 && to.value.trim().length !== 0){
if (friendPicker.value !== to.value){
alert('You have selected both a friend and entered a public key manually.')
return false
}
}
sendMail(to.value, messageContent.value, subject.value)
return false
} }

File diff suppressed because one or more lines are too long

View File

@ -151,3 +151,31 @@ body{
content: '❌'; content: '❌';
padding: 5px; padding: 5px;
} }
.btn, .warnBtn, .dangerBtn, .successBtn{
padding: 5px;
border-radius: 5px;
border: 2px solid black;
}
.warnBtn{
background-color: orange;
color: black;
}
.dangerBtn{
background-color: #f44336;
color: black;
}
.successBtn{
background-color: #4CAF50;
color: black;
}
.primaryBtn{
background-color:#396BAC;
}
.openSiteBtn{
padding: 5px;
border: 1px solid black;
border-radius: 5px;
}

View File

@ -20,6 +20,25 @@
webpass = document.location.hash.replace('#', '') webpass = document.location.hash.replace('#', '')
nowebpass = false nowebpass = false
function post_to_url(path, params) {
var form = document.createElement("form")
form.setAttribute("method", "POST")
form.setAttribute("action", path)
for(var key in params) {
var hiddenField = document.createElement("input")
hiddenField.setAttribute("type", "hidden")
hiddenField.setAttribute("name", key)
hiddenField.setAttribute("value", params[key])
form.appendChild(hiddenField)
}
document.body.appendChild(form)
form.submit()
}
if (typeof webpass == "undefined"){ if (typeof webpass == "undefined"){
webpass = localStorage['webpass'] webpass = localStorage['webpass']
} }
@ -67,3 +86,9 @@ for(var i = 0; i < refreshLinks.length; i++) {
location.reload() location.reload()
} }
} }
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'
}
}

View File

@ -0,0 +1,18 @@
function checkHex(str) {
regexp = /^[0-9a-fA-F]+$/
if (regexp.test(str)){
return true
}
return false
}
document.getElementById('openSite').onclick = function(){
var hash = document.getElementById('siteViewer').value
if (checkHex(hash) && hash.length == 64){
window.location.href = '/site/' + hash
}
else{
alert('Invalid site hash')
}
}

92
onionr/subprocesspow.py Executable file
View File

@ -0,0 +1,92 @@
#!/usr/bin/env python3
import subprocess, sys, os
import multiprocessing, threading, time, json, math, binascii
from multiprocessing import Pipe, Process
import core, onionrblockapi, config, onionrutils, logger, onionrproofs
class SubprocessPOW:
def __init__(self, data, metadata, core_inst=None, subprocCount=None):
if core_inst is None:
core_inst = core.Core()
if subprocCount is None:
subprocCount = os.cpu_count()
self.subprocCount = subprocCount
self.result = ''
self.shutdown = False
self.core_inst = core_inst
self.data = data
self.metadata = metadata
dataLen = len(data) + len(json.dumps(metadata))
#if forceDifficulty > 0:
# self.difficulty = forceDifficulty
#else:
# Calculate difficulty. Dumb for now, may use good algorithm in the future.
self.difficulty = onionrproofs.getDifficultyForNewBlock(dataLen)
try:
self.data = self.data.encode()
except AttributeError:
pass
logger.info('Computing POW (difficulty: %s)...' % self.difficulty)
self.mainHash = '0' * 64
self.puzzle = self.mainHash[0:min(self.difficulty, len(self.mainHash))]
self.shutdown = False
self.payload = None
def start(self):
startTime = self.core_inst._utils.getEpoch()
for x in range(self.subprocCount):
threading.Thread(target=self._spawn_proc).start()
while True:
if self.payload is None:
time.sleep(0.1)
else:
self.shutdown = True
return self.payload
def _spawn_proc(self):
parent_conn, child_conn = Pipe()
p = Process(target=self.do_pow, args=(child_conn,))
p.start()
p.join()
payload = None
try:
while True:
data = parent_conn.recv()
if len(data) >= 1:
payload = data
break
except KeyboardInterrupt:
pass
finally:
parent_conn.send('shutdown')
self.payload = payload
def do_pow(self, pipe):
nonce = int(binascii.hexlify(os.urandom(2)), 16)
nonceStart = nonce
data = self.data
metadata = self.metadata
puzzle = self.puzzle
difficulty = self.difficulty
mcore = core.Core()
while True:
metadata['powRandomToken'] = nonce
payload = json.dumps(metadata).encode() + b'\n' + data
token = mcore._crypto.sha3Hash(payload)
try:
# on some versions, token is bytes
token = token.decode()
except AttributeError:
pass
if pipe.poll() and pipe.recv() == 'shutdown':
break
if puzzle == token[0:difficulty]:
pipe.send(payload)
break
nonce += 1

0
onionr/tests/test_blocks.py Normal file → Executable file
View File

View File

@ -0,0 +1,40 @@
#!/usr/bin/env python3
import sys, os
sys.path.append(".")
import unittest, uuid, sqlite3
TEST_DIR = 'testdata/%s-%s' % (uuid.uuid4(), os.path.basename(__file__)) + '/'
print("Test directory:", TEST_DIR)
os.environ["ONIONR_HOME"] = TEST_DIR
from urllib.request import pathname2url
import core, onionr
c = core.Core()
class OnionrTests(unittest.TestCase):
def test_address_add(self):
testAddresses = ['facebookcorewwwi.onion', '56kmnycrvepfarolhnx6t2dvmldfeyg7jdymwgjb7jjzg47u2lqw2sad.onion', '5bvb5ncnfr4dlsfriwczpzcvo65kn7fnnlnt2ln7qvhzna2xaldq.b32.i2p']
for address in testAddresses:
c.addAddress(address)
dbAddresses = c.listAdders()
for address in testAddresses:
self.assertIn(address, dbAddresses)
invalidAddresses = [None, '', ' ', '\t', '\n', ' test ', 24, 'fake.onion', 'fake.b32.i2p']
for address in invalidAddresses:
try:
c.addAddress(address)
except TypeError:
pass
dbAddresses = c.listAdders()
for address in invalidAddresses:
self.assertNotIn(address, dbAddresses)
def test_address_info(self):
adder = 'nytimes3xbfgragh.onion'
c.addAddress(adder)
self.assertNotEqual(c.getAddressInfo(adder, 'success'), 1000)
c.setAddressInfo(adder, 'success', 1000)
self.assertEqual(c.getAddressInfo(adder, 'success'), 1000)
unittest.main()

0
onionr/tests/test_database_creation.py Normal file → Executable file
View File

View File

@ -0,0 +1,40 @@
#!/usr/bin/env python3
import sys, os, random
sys.path.append(".")
import unittest, uuid
TEST_DIR_1 = 'testdata/%s-%s' % (uuid.uuid4(), os.path.basename(__file__)) + '/'
TEST_DIR_2 = 'testdata/%s-%s' % (uuid.uuid4(), os.path.basename(__file__)) + '/'
import core, onionr, time
import onionrexceptions
from onionrusers import onionrusers
from onionrusers import contactmanager
class OnionrForwardSecrecyTests(unittest.TestCase):
'''
Tests both the onionrusers class and the contactmanager (which inherits it)
'''
def test_forward_encrypt(self):
os.environ["ONIONR_HOME"] = TEST_DIR_1
o = onionr.Onionr()
friend = o.onionrCore._crypto.generatePubKey()
friendUser = onionrusers.OnionrUser(o.onionrCore, friend[0], saveUser=True)
for x in range(5):
message = 'hello world %s' % (random.randint(1, 1000))
forwardKey = friendUser.generateForwardKey()
fakeForwardPair = o.onionrCore._crypto.generatePubKey()
self.assertTrue(friendUser.addForwardKey(fakeForwardPair[0]))
encrypted = friendUser.forwardEncrypt(message)
decrypted = o.onionrCore._crypto.pubKeyDecrypt(encrypted[0], privkey=fakeForwardPair[1], encodedData=True)
self.assertEqual(decrypted, message.encode())
return
unittest.main()

50
onionr/tests/test_highlevelcrypto.py Normal file → Executable file
View File

@ -1,23 +1,23 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sys, os import sys, os
sys.path.append(".") sys.path.append(".")
import unittest, uuid, hashlib import unittest, uuid, hashlib, base64
import nacl.exceptions import nacl.exceptions
import nacl.signing, nacl.hash, nacl.encoding import nacl.signing, nacl.hash, nacl.encoding
TEST_DIR = 'testdata/%s-%s' % (uuid.uuid4(), os.path.basename(__file__)) + '/' TEST_DIR = 'testdata/%s-%s' % (uuid.uuid4(), os.path.basename(__file__)) + '/'
print("Test directory:", TEST_DIR) print("Test directory:", TEST_DIR)
os.environ["ONIONR_HOME"] = TEST_DIR os.environ["ONIONR_HOME"] = TEST_DIR
import core, onionr import core, onionr, onionrexceptions
c = core.Core() c = core.Core()
crypto = c._crypto crypto = c._crypto
class OnionrCryptoTests(unittest.TestCase): class OnionrCryptoTests(unittest.TestCase):
def test_blake2b(self): def test_blake2b(self):
self.assertTrue(crypto.blake2bHash('test') == crypto.blake2bHash(b'test')) self.assertEqual(crypto.blake2bHash('test'), crypto.blake2bHash(b'test'))
self.assertTrue(crypto.blake2bHash(b'test') == crypto.blake2bHash(b'test')) self.assertEqual(crypto.blake2bHash(b'test'), crypto.blake2bHash(b'test'))
self.assertFalse(crypto.blake2bHash('') == crypto.blake2bHash(b'test')) self.assertNotEqual(crypto.blake2bHash(''), crypto.blake2bHash(b'test'))
try: try:
crypto.blake2bHash(None) crypto.blake2bHash(None)
except nacl.exceptions.TypeError: except nacl.exceptions.TypeError:
@ -25,14 +25,14 @@ class OnionrCryptoTests(unittest.TestCase):
else: else:
self.assertTrue(False) self.assertTrue(False)
self.assertTrue(nacl.hash.blake2b(b'test') == crypto.blake2bHash(b'test')) self.assertEqual(nacl.hash.blake2b(b'test'), crypto.blake2bHash(b'test'))
def test_sha3256(self): def test_sha3256(self):
hasher = hashlib.sha3_256() hasher = hashlib.sha3_256()
self.assertTrue(crypto.sha3Hash('test') == crypto.sha3Hash(b'test')) self.assertEqual(crypto.sha3Hash('test'), crypto.sha3Hash(b'test'))
self.assertTrue(crypto.sha3Hash(b'test') == crypto.sha3Hash(b'test')) self.assertEqual(crypto.sha3Hash(b'test'), crypto.sha3Hash(b'test'))
self.assertFalse(crypto.sha3Hash('') == crypto.sha3Hash(b'test')) self.assertNotEqual(crypto.sha3Hash(''), crypto.sha3Hash(b'test'))
try: try:
crypto.sha3Hash(None) crypto.sha3Hash(None)
except TypeError: except TypeError:
@ -42,7 +42,7 @@ class OnionrCryptoTests(unittest.TestCase):
hasher.update(b'test') hasher.update(b'test')
normal = hasher.hexdigest() normal = hasher.hexdigest()
self.assertTrue(crypto.sha3Hash(b'test') == normal) self.assertEqual(crypto.sha3Hash(b'test'), normal)
def valid_default_id(self): def valid_default_id(self):
self.assertTrue(c._utils.validatePubKey(crypto.pubKey)) self.assertTrue(c._utils.validatePubKey(crypto.pubKey))
@ -73,8 +73,8 @@ class OnionrCryptoTests(unittest.TestCase):
# Small chance that the randomized list will be same. Rerun test a couple times if it fails # Small chance that the randomized list will be same. Rerun test a couple times if it fails
startList = ['cat', 'dog', 'moose', 'rabbit', 'monkey', 'crab', 'human', 'dolphin', 'whale', 'etc'] * 10 startList = ['cat', 'dog', 'moose', 'rabbit', 'monkey', 'crab', 'human', 'dolphin', 'whale', 'etc'] * 10
self.assertFalse(startList == list(crypto.randomShuffle(startList))) self.assertNotEqual(startList, list(crypto.randomShuffle(startList)))
self.assertTrue(len(startList) == len(startList)) self.assertTrue(len(list(crypto.randomShuffle(startList))) == len(startList))
def test_asymmetric(self): def test_asymmetric(self):
keyPair = crypto.generatePubKey() keyPair = crypto.generatePubKey()
@ -127,4 +127,30 @@ class OnionrCryptoTests(unittest.TestCase):
else: else:
self.assertFalse(True) self.assertFalse(True)
def test_deterministic(self):
password = os.urandom(32)
gen = crypto.generateDeterministic(password)
self.assertTrue(c._utils.validatePubKey(gen[0]))
try:
crypto.generateDeterministic('weakpassword')
except onionrexceptions.PasswordStrengthError:
pass
else:
self.assertFalse(True)
try:
crypto.generateDeterministic(None)
except TypeError:
pass
else:
self.assertFalse(True)
gen = crypto.generateDeterministic('weakpassword', bypassCheck=True)
password = base64.b64encode(os.urandom(32))
gen1 = crypto.generateDeterministic(password)
gen2 = crypto.generateDeterministic(password)
self.assertFalse(gen == gen1)
self.assertTrue(gen1 == gen2)
self.assertTrue(c._utils.validatePubKey(gen1[0]))
unittest.main() unittest.main()

17
onionr/tests/test_onionrusers.py Normal file → Executable file
View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sys, os import sys, os
sys.path.append(".") sys.path.append(".")
import unittest, uuid, hashlib import unittest, uuid
import json import json
TEST_DIR = 'testdata/%s-%s' % (uuid.uuid4(), os.path.basename(__file__)) + '/' TEST_DIR = 'testdata/%s-%s' % (uuid.uuid4(), os.path.basename(__file__)) + '/'
print("Test directory:", TEST_DIR) print("Test directory:", TEST_DIR)
@ -44,7 +44,7 @@ class OnionrUserTests(unittest.TestCase):
data = data.read() data = data.read()
data = json.loads(data) data = json.loads(data)
self.assertTrue(data['alias'] == 'bob') self.assertEqual(data['alias'], 'bob')
def test_contact_get_info(self): def test_contact_get_info(self):
contact = c._crypto.generatePubKey()[0] contact = c._crypto.generatePubKey()[0]
@ -54,9 +54,16 @@ class OnionrUserTests(unittest.TestCase):
with open(fileLocation, 'w') as contactFile: with open(fileLocation, 'w') as contactFile:
contactFile.write('{"alias": "bob"}') contactFile.write('{"alias": "bob"}')
self.assertTrue(contact.get_info('alias', forceReload=True) == 'bob') self.assertEqual(contact.get_info('alias', forceReload=True), 'bob')
self.assertTrue(contact.get_info('fail', forceReload=True) == None) self.assertEqual(contact.get_info('fail', forceReload=True), None)
self.assertTrue(contact.get_info('fail') == None) self.assertEqual(contact.get_info('fail'), None)
def test_encrypt(self):
contactPair = c._crypto.generatePubKey()
contact = contactmanager.ContactManager(c, contactPair[0], saveUser=True)
encrypted = contact.encrypt('test')
decrypted = c._crypto.pubKeyDecrypt(encrypted, privkey=contactPair[1], encodedData=True).decode()
self.assertEqual('test', decrypted)
def test_delete_contact(self): def test_delete_contact(self):
contact = c._crypto.generatePubKey()[0] contact = c._crypto.generatePubKey()[0]

0
onionr/tests/test_stringvalidations.py Normal file → Executable file
View File

0
onionr/utils/netutils.py Normal file → Executable file
View File

0
onionr/utils/networkmerger.py Normal file → Executable file
View File

9
requirements.in Executable file
View File

@ -0,0 +1,9 @@
urllib3==1.23
requests==2.20.0
PyNaCl==1.2.1
gevent==1.3.6
defusedxml==0.5.0
Flask==1.0.2
PySocks==1.6.8
stem==1.6.0
deadsimplekv==0.0.1

204
requirements.txt Executable file → Normal file
View File

@ -1,8 +1,196 @@
urllib3==1.23 #
requests==2.20.0 # This file is autogenerated by pip-compile
PyNaCl==1.2.1 # To update, run:
gevent==1.3.6 #
defusedxml==0.5.0 # pip-compile --generate-hashes --output-file requirements.txt requirements.in
Flask==1.0.2 #
PySocks==1.6.8 certifi==2018.11.29 \
stem==1.6.0 --hash=sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7 \
--hash=sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033 \
# via requests
cffi==1.12.2 \
--hash=sha256:00b97afa72c233495560a0793cdc86c2571721b4271c0667addc83c417f3d90f \
--hash=sha256:0ba1b0c90f2124459f6966a10c03794082a2f3985cd699d7d63c4a8dae113e11 \
--hash=sha256:0bffb69da295a4fc3349f2ec7cbe16b8ba057b0a593a92cbe8396e535244ee9d \
--hash=sha256:21469a2b1082088d11ccd79dd84157ba42d940064abbfa59cf5f024c19cf4891 \
--hash=sha256:2e4812f7fa984bf1ab253a40f1f4391b604f7fc424a3e21f7de542a7f8f7aedf \
--hash=sha256:2eac2cdd07b9049dd4e68449b90d3ef1adc7c759463af5beb53a84f1db62e36c \
--hash=sha256:2f9089979d7456c74d21303c7851f158833d48fb265876923edcb2d0194104ed \
--hash=sha256:3dd13feff00bddb0bd2d650cdb7338f815c1789a91a6f68fdc00e5c5ed40329b \
--hash=sha256:4065c32b52f4b142f417af6f33a5024edc1336aa845b9d5a8d86071f6fcaac5a \
--hash=sha256:51a4ba1256e9003a3acf508e3b4f4661bebd015b8180cc31849da222426ef585 \
--hash=sha256:59888faac06403767c0cf8cfb3f4a777b2939b1fbd9f729299b5384f097f05ea \
--hash=sha256:59c87886640574d8b14910840327f5cd15954e26ed0bbd4e7cef95fa5aef218f \
--hash=sha256:610fc7d6db6c56a244c2701575f6851461753c60f73f2de89c79bbf1cc807f33 \
--hash=sha256:70aeadeecb281ea901bf4230c6222af0248c41044d6f57401a614ea59d96d145 \
--hash=sha256:71e1296d5e66c59cd2c0f2d72dc476d42afe02aeddc833d8e05630a0551dad7a \
--hash=sha256:8fc7a49b440ea752cfdf1d51a586fd08d395ff7a5d555dc69e84b1939f7ddee3 \
--hash=sha256:9b5c2afd2d6e3771d516045a6cfa11a8da9a60e3d128746a7fe9ab36dfe7221f \
--hash=sha256:9c759051ebcb244d9d55ee791259ddd158188d15adee3c152502d3b69005e6bd \
--hash=sha256:b4d1011fec5ec12aa7cc10c05a2f2f12dfa0adfe958e56ae38dc140614035804 \
--hash=sha256:b4f1d6332339ecc61275bebd1f7b674098a66fea11a00c84d1c58851e618dc0d \
--hash=sha256:c030cda3dc8e62b814831faa4eb93dd9a46498af8cd1d5c178c2de856972fd92 \
--hash=sha256:c2e1f2012e56d61390c0e668c20c4fb0ae667c44d6f6a2eeea5d7148dcd3df9f \
--hash=sha256:c37c77d6562074452120fc6c02ad86ec928f5710fbc435a181d69334b4de1d84 \
--hash=sha256:c8149780c60f8fd02752d0429246088c6c04e234b895c4a42e1ea9b4de8d27fb \
--hash=sha256:cbeeef1dc3c4299bd746b774f019de9e4672f7cc666c777cd5b409f0b746dac7 \
--hash=sha256:e113878a446c6228669144ae8a56e268c91b7f1fafae927adc4879d9849e0ea7 \
--hash=sha256:e21162bf941b85c0cda08224dade5def9360f53b09f9f259adb85fc7dd0e7b35 \
--hash=sha256:fb6934ef4744becbda3143d30c6604718871495a5e36c408431bf33d9c146889 \
# via pynacl
chardet==3.0.4 \
--hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \
--hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 \
# via requests
click==7.0 \
--hash=sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13 \
--hash=sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7 \
# via flask
deadsimplekv==0.0.1 \
--hash=sha256:1bb78e4feb01d975e89e81cac7b0141666a14ebefa06fffc1c2d86c3308e3930
defusedxml==0.5.0 \
--hash=sha256:24d7f2f94f7f3cb6061acb215685e5125fbcdc40a857eff9de22518820b0a4f4 \
--hash=sha256:702a91ade2968a82beb0db1e0766a6a273f33d4616a6ce8cde475d8e09853b20
flask==1.0.2 \
--hash=sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48 \
--hash=sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05
gevent==1.3.6 \
--hash=sha256:03d03ea4f33e535b0a99b6be2696fde9c7417022b8ee67fb15b78f47672a0b86 \
--hash=sha256:13a0e74432ede9efdad5fd9aed73bd30bcfc73ddcbffe719849210f4546db833 \
--hash=sha256:23d623b41a431e04a9410b046520778517f5304dfbb9bfd3b1bbcc722eeaeea5 \
--hash=sha256:2f82d8b4d09285ca4aef34ae5c093ccf966da90e7db3bd34764ffb014c8bfa68 \
--hash=sha256:3223eb697d819d73dedc9a55b3dfa0cc1931e6459df4f0bf83c7c27ca256a3bd \
--hash=sha256:3c00ade4ae707dd6a17d6d56ebac689dc56719b83389f9aeb6a10b1e01326177 \
--hash=sha256:652bdd59afb330ad95550bda6864b87876e977aa4e48b9216235d932368e1987 \
--hash=sha256:7b413c391e8ad6607b7f7540d698a94349abd64e4935184c595f7cdcc69904c6 \
--hash=sha256:7feaf556fe2dc94340b603a3bfb00fbb526aaafcb8d804960939244ace4a262f \
--hash=sha256:810ae07c1baee83cb3d54f7dca236803554659dc00ef662ac962e4e4fd3e79bb \
--hash=sha256:86fa642228b8fc6a8fa268efab20440bb26599d28814e8dcd99af5dc92da10d7 \
--hash=sha256:a9a0a61f8dc652b3dd5dc8b9f5f9ace5d2f91f5e81f368e9ef180c8eec968234 \
--hash=sha256:ac3d258521b1056acb922b3aa77031a64888bb8cda1f7f6f370692cf3e224761 \
--hash=sha256:af7b0d16541dea42f1eceac4a02815ea3ebd8fe1eb6fc714c81ab1842ec259d4 \
--hash=sha256:bafef5a426473b52648c25d0ff9027aa8806982b57f8bc03abcc5f4669bfe19f \
--hash=sha256:bc31cdec2e584106c026a4fd24f800cb575ea8ebfcce7974b630b65d61cf36df \
--hash=sha256:cc42af305cb7bf1766b0084011520a81e56315dcc5b7662c209ef71a00764634 \
--hash=sha256:e01223b43b2e9d92733ab9953038c7a99b9c3cdb32dc865b9ce94f03a2199f96 \
--hash=sha256:e57f9d267b45ef9e3eb0e234307faaffa5a79cdb1477afa1befbf04de0cd8cbe \
--hash=sha256:e9e2942704f7fe75064ef0bc17ba46b097a57ec0e70eca1d790d5a3edb691628 \
--hash=sha256:f2ca6fc669def8e622b4a10809f6f6a4b6a822a1cc1175b89ad8eb34235aaa2e \
--hash=sha256:f456a6321f0955e802e305946ce7e7d672a7da313417ea4b4add6809630d3b0e \
--hash=sha256:ff8e09696a8c9100b1c88066ee44b50fbbea367ae91d830910561c902d1e7f3c
greenlet==0.4.15 \
--hash=sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0 \
--hash=sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28 \
--hash=sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8 \
--hash=sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304 \
--hash=sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0 \
--hash=sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214 \
--hash=sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043 \
--hash=sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6 \
--hash=sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625 \
--hash=sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc \
--hash=sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638 \
--hash=sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163 \
--hash=sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4 \
--hash=sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490 \
--hash=sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248 \
--hash=sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939 \
--hash=sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87 \
--hash=sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720 \
--hash=sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656 \
# via gevent
idna==2.7 \
--hash=sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e \
--hash=sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16 \
# via requests
itsdangerous==1.1.0 \
--hash=sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19 \
--hash=sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749 \
# via flask
jinja2==2.10 \
--hash=sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd \
--hash=sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4 \
# via flask
markupsafe==1.1.1 \
--hash=sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473 \
--hash=sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161 \
--hash=sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235 \
--hash=sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5 \
--hash=sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff \
--hash=sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b \
--hash=sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1 \
--hash=sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e \
--hash=sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183 \
--hash=sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66 \
--hash=sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1 \
--hash=sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1 \
--hash=sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e \
--hash=sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b \
--hash=sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905 \
--hash=sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735 \
--hash=sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d \
--hash=sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e \
--hash=sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d \
--hash=sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c \
--hash=sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21 \
--hash=sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2 \
--hash=sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5 \
--hash=sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b \
--hash=sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6 \
--hash=sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f \
--hash=sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f \
--hash=sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7 \
# via jinja2
pycparser==2.19 \
--hash=sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3 \
# via cffi
pynacl==1.2.1 \
--hash=sha256:04e30e5bdeeb2d5b34107f28cd2f5bbfdc6c616f3be88fc6f53582ff1669eeca \
--hash=sha256:0bfa0d94d2be6874e40f896e0a67e290749151e7de767c5aefbad1121cad7512 \
--hash=sha256:11aa4e141b2456ce5cecc19c130e970793fa3a2c2e6fbb8ad65b28f35aa9e6b6 \
--hash=sha256:13bdc1fe084ff9ac7653ae5a924cae03bf4bb07c6667c9eb5b6eb3c570220776 \
--hash=sha256:14339dc233e7a9dda80a3800e64e7ff89d0878ba23360eea24f1af1b13772cac \
--hash=sha256:1d33e775fab3f383167afb20b9927aaf4961b953d76eeb271a5703a6d756b65b \
--hash=sha256:2a42b2399d0428619e58dac7734838102d35f6dcdee149e0088823629bf99fbb \
--hash=sha256:2dce05ac8b3c37b9e2f65eab56c544885607394753e9613fd159d5e2045c2d98 \
--hash=sha256:63cfccdc6217edcaa48369191ae4dca0c390af3c74f23c619e954973035948cd \
--hash=sha256:6453b0dae593163ffc6db6f9c9c1597d35c650598e2c39c0590d1757207a1ac2 \
--hash=sha256:73a5a96fb5fbf2215beee2353a128d382dbca83f5341f0d3c750877a236569ef \
--hash=sha256:8abb4ef79161a5f58848b30ab6fb98d8c466da21fdd65558ce1d7afc02c70b5f \
--hash=sha256:8ac1167195b32a8755de06efd5b2d2fe76fc864517dab66aaf65662cc59e1988 \
--hash=sha256:8f505f42f659012794414fa57c498404e64db78f1d98dfd40e318c569f3c783b \
--hash=sha256:9c8a06556918ee8e3ab48c65574f318f5a0a4d31437fc135da7ee9d4f9080415 \
--hash=sha256:a1e25fc5650cf64f01c9e435033e53a4aca9de30eb9929d099f3bb078e18f8f2 \
--hash=sha256:be71cd5fce04061e1f3d39597f93619c80cdd3558a6c9ba99a546f144a8d8101 \
--hash=sha256:c5b1a7a680218dee9da0f1b5e24072c46b3c275d35712bc1d505b85bb03441c0 \
--hash=sha256:cb785db1a9468841a1265c9215c60fe5d7af2fb1b209e3316a152704607fc582 \
--hash=sha256:cf6877124ae6a0698404e169b3ba534542cfbc43f939d46b927d956daf0a373a \
--hash=sha256:d0eb5b2795b7ee2cbcfcadacbe95a13afbda048a262bd369da9904fecb568975 \
--hash=sha256:d3a934e2b9f20abac009d5b6951067cfb5486889cb913192b4d8288b216842f1 \
--hash=sha256:d795f506bcc9463efb5ebb0f65ed77921dcc9e0a50499dedd89f208445de9ecb \
--hash=sha256:d8aaf7e5d6b0e0ef7d6dbf7abeb75085713d0100b4eb1a4e4e857de76d77ac45 \
--hash=sha256:de2aaca8386cf4d70f1796352f2346f48ddb0bed61dc43a3ce773ba12e064031 \
--hash=sha256:e0d38fa0a75f65f556fb912f2c6790d1fa29b7dd27a1d9cc5591b281321eaaa9 \
--hash=sha256:eb2acabbd487a46b38540a819ef67e477a674481f84a82a7ba2234b9ba46f752 \
--hash=sha256:eeee629828d0eb4f6d98ac41e9a3a6461d114d1d0aa111a8931c049359298da0 \
--hash=sha256:f5836463a3c0cca300295b229b6c7003c415a9d11f8f9288ddbd728e2746524c \
--hash=sha256:f5ce9e26d25eb0b2d96f3ef0ad70e1d3ae89b5d60255c462252a3e456a48c053 \
--hash=sha256:fabf73d5d0286f9e078774f3435601d2735c94ce9e514ac4fb945701edead7e4
pysocks==1.6.8 \
--hash=sha256:3fe52c55890a248676fd69dc9e3c4e811718b777834bcaab7a8125cf9deac672
requests==2.20.0 \
--hash=sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c \
--hash=sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279
six==1.12.0 \
--hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \
--hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73 \
# via pynacl
stem==1.6.0 \
--hash=sha256:d7fe1fb13ed5a94d610b5ad77e9f1b3404db0ca0586ded7a34afd323e3b849ed
urllib3==1.23 \
--hash=sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf \
--hash=sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5
werkzeug==0.14.1 \
--hash=sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c \
--hash=sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b \
# via flask

View File

@ -1,9 +1,10 @@
#!/bin/bash #!/bin/bash
cd onionr; cd onionr;
rm -rf testdata;
mkdir testdata; mkdir testdata;
ran=0 ran=0
SECONDS=0 ;
close () { close () {
rm -rf testdata; rm -rf testdata;
exit 10; exit 10;
@ -13,5 +14,4 @@ for f in tests/*.py; do
python3 "$f" || close # if needed python3 "$f" || close # if needed
let "ran++" let "ran++"
done done
rm -rf testdata; echo "ran $ran test files successfully in $SECONDS seconds"
echo "ran $ran test files successfully"