Merge branch 'pom' into 'master'
Merge POM See merge request beardog/Onionr!18
This commit is contained in:
commit
bff09e38d3
@ -1,6 +0,0 @@
|
|||||||
test:
|
|
||||||
script:
|
|
||||||
- apt-get update -qy
|
|
||||||
- apt-get install -y python3-dev python3-pip tor
|
|
||||||
- pip3 install -r requirements.txt
|
|
||||||
- make test
|
|
@ -1,8 +0,0 @@
|
|||||||
language: python
|
|
||||||
python:
|
|
||||||
- "3.6.4"
|
|
||||||
# install dependencies
|
|
||||||
install:
|
|
||||||
- sudo apt install tor
|
|
||||||
- pip install -r requirements.txt
|
|
||||||
script: make test
|
|
@ -27,7 +27,7 @@ And most importantly, please be patient. Onionr is an open source project done b
|
|||||||
|
|
||||||
## Asking Questions
|
## Asking Questions
|
||||||
|
|
||||||
If you need help with Onionr, you can ask in our
|
If you need help with Onionr, you can contact the devs (be polite and remember this is a volunteer-driven non-profit project).
|
||||||
|
|
||||||
## Contributing Code
|
## Contributing Code
|
||||||
|
|
||||||
|
8
Makefile
8
Makefile
@ -18,7 +18,7 @@ uninstall:
|
|||||||
rm -f $(DESTDIR)$(PREFIX)/bin/onionr
|
rm -f $(DESTDIR)$(PREFIX)/bin/onionr
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@./run-linux stop
|
@./onionr.sh stop
|
||||||
@sleep 1
|
@sleep 1
|
||||||
@rm -rf onionr/data-backup
|
@rm -rf onionr/data-backup
|
||||||
@mv onionr/data onionr/data-backup | true > /dev/null 2>&1
|
@mv onionr/data onionr/data-backup | true > /dev/null 2>&1
|
||||||
@ -29,15 +29,15 @@ test:
|
|||||||
soft-reset:
|
soft-reset:
|
||||||
@echo "Soft-resetting Onionr..."
|
@echo "Soft-resetting Onionr..."
|
||||||
rm -f onionr/data/blocks/*.dat onionr/data/*.db onionr/data/block-nonces.dat | true > /dev/null 2>&1
|
rm -f onionr/data/blocks/*.dat onionr/data/*.db onionr/data/block-nonces.dat | true > /dev/null 2>&1
|
||||||
@./run-linux version | grep -v "Failed" --color=always
|
@./onionr.sh version | grep -v "Failed" --color=always
|
||||||
|
|
||||||
reset:
|
reset:
|
||||||
@echo "Hard-resetting Onionr..."
|
@echo "Hard-resetting Onionr..."
|
||||||
rm -rf onionr/data/ | true > /dev/null 2>&1
|
rm -rf onionr/data/ | true > /dev/null 2>&1
|
||||||
cd onionr/static-data/www/ui/; rm -rf ./dist; python compile.py
|
cd onionr/static-data/www/ui/; rm -rf ./dist; python compile.py
|
||||||
#@./RUN-LINUX.sh version | grep -v "Failed" --color=always
|
#@./onionr.sh.sh version | grep -v "Failed" --color=always
|
||||||
|
|
||||||
plugins-reset:
|
plugins-reset:
|
||||||
@echo "Resetting plugins..."
|
@echo "Resetting plugins..."
|
||||||
rm -rf onionr/data/plugins/ | true > /dev/null 2>&1
|
rm -rf onionr/data/plugins/ | true > /dev/null 2>&1
|
||||||
@./run-linux version | grep -v "Failed" --color=always
|
@./onionr.sh version | grep -v "Failed" --color=always
|
||||||
|
61
README.md
61
README.md
@ -1,48 +1,71 @@
|
|||||||
![Onionr logo](./docs/onionr-logo.png)
|
<p align="center">
|
||||||
|
|
||||||
(***experimental, not safe or easy to use yet***)
|
<img src="./docs/onionr-logo.png" width='250'>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
Anonymous P2P storage network 🕵️
|
||||||
|
</p>
|
||||||
|
|
||||||
|
(***pre-alpha & experimental, not well tested or easy to use yet***)
|
||||||
|
|
||||||
[![Open Source Love](https://badges.frapsoft.com/os/v3/open-source.png?v=103)](https://github.com/ellerbrock/open-source-badges/)
|
[![Open Source Love](https://badges.frapsoft.com/os/v3/open-source.png?v=103)](https://github.com/ellerbrock/open-source-badges/)
|
||||||
|
|
||||||
|
|
||||||
Anonymous P2P platform, using Tor & I2P.
|
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
**The main repo for this software is at https://gitlab.com/beardog/Onionr/**
|
**The main repository for this software is at https://gitlab.com/beardog/Onionr/**
|
||||||
|
|
||||||
|
|
||||||
# Summary
|
# Summary
|
||||||
|
|
||||||
Onionr is a decentralized, peer-to-peer data storage network, designed to be anonymous and resistant to (meta)data analysis and spam.
|
Onionr is a decentralized, peer-to-peer data storage network, designed to be anonymous and resistant to (meta)data analysis and spam/disruption.
|
||||||
|
|
||||||
|
Onionr stores data in independent packages referred to as 'blocks'. The blocks are synced to all other nodes in the network. Blocks and user IDs cannot be easily proven to have been created by particular nodes (only inferred). Even if there is enough evidence to believe a particular node created a block, nodes still operate behind Tor or I2P and as such are not trivially known to be at a particular IP address.
|
||||||
|
|
||||||
|
Users are identified by ed25519 public keys, which can be used to sign blocks or send encrypted data.
|
||||||
|
|
||||||
Onionr can be used for mail, as a social network, instant messenger, file sharing software, or for encrypted group discussion.
|
Onionr can be used for mail, as a social network, instant messenger, file sharing software, or for encrypted group discussion.
|
||||||
|
|
||||||
# Roadmap/features
|
![Tor stinks slide image](docs/tor-stinks-02.png)
|
||||||
|
|
||||||
Check the [Gitlab Project](https://gitlab.com/beardog/Onionr/milestones/1) to see progress towards the alpha release.
|
## Main Features
|
||||||
|
|
||||||
## Core internal 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
|
||||||
* [ ] Metadata analysis resistance (being improved)
|
* [X] Metadata analysis resistance
|
||||||
|
* [X] Transport agnosticism (no internet required)
|
||||||
|
|
||||||
## Other features
|
|
||||||
|
|
||||||
**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**
|
||||||
|
|
||||||
|
# Install and Run on Linux
|
||||||
|
|
||||||
|
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)
|
||||||
|
* Clone the git repo: `$ git clone https://gitlab.com/beardog/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`
|
||||||
|
|
||||||
## Help out
|
## Help out
|
||||||
|
|
||||||
Everyone is welcome to help out. Please get in touch first if you are making non-trivial changes. If you can't help with programming, you can write documentation or guides.
|
Everyone is welcome to help out. Help is wanted for the following:
|
||||||
|
|
||||||
Bitcoin/Bitcoin Cash: 1onion55FXzm6h8KQw3zFw2igpHcV7LPq
|
* Development (Get in touch first)
|
||||||
|
* Creation of a lib for use from other languages and faster proof-of-work
|
||||||
|
* Android and IOS development
|
||||||
|
* Windows and Mac support
|
||||||
|
* General bug fixes and development of new features
|
||||||
|
* Testing
|
||||||
|
* Running stable nodes
|
||||||
|
* Security review/audit
|
||||||
|
|
||||||
|
Bitcoin: [1onion55FXzm6h8KQw3zFw2igpHcV7LPq](bitcoin:1onion55FXzm6h8KQw3zFw2igpHcV7LPq)
|
||||||
|
USD: [Ko-Fi](https://www.ko-fi.com/beardogkf)
|
||||||
|
|
||||||
## 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, I2P developers, and anyone else do not own, create, or endorse this project, and are not otherwise involved.
|
||||||
|
|
||||||
The badges (besides travis-ci build) are by Maik Ellerbrock is licensed under a Creative Commons Attribution 4.0 International License.
|
The 'open source badge' is by Maik Ellerbrock and is licensed under a Creative Commons Attribution 4.0 International License.
|
Binary file not shown.
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 189 KiB |
BIN
docs/onionr-logo.png~
Normal file
BIN
docs/onionr-logo.png~
Normal file
Binary file not shown.
After Width: | Height: | Size: 191 KiB |
BIN
docs/tor-stinks-02.png
Normal file
BIN
docs/tor-stinks-02.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
# Introduction
|
# Introduction
|
||||||
|
|
||||||
The most important thing in the modern world is information. The ability to communicate freely with others. The internet has provided humanity with the ability to spread information globally, but there are many people who try (and sometimes succeed) to stifle the flow of information.
|
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.
|
||||||
|
|
||||||
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).
|
||||||
|
|
||||||
@ -14,25 +14,22 @@ To prevent censorship or loss of information, these measures must be in place:
|
|||||||
* Resistance to censorship of underlying infrastructure or of network hosts
|
* Resistance to censorship of underlying infrastructure or of network hosts
|
||||||
|
|
||||||
* Anonymization of users by default
|
* Anonymization of users by default
|
||||||
* The Inability to violently coerce human users (personal threats/"doxxing", or totalitarian regime censorship)
|
* The Inability to coerce human users (personal threats/"doxxing", or totalitarian regime censorship)
|
||||||
|
|
||||||
* Economic availability. A system should not rely on a single device to be constantly online, and should not be overly expensive to use. The majority of people in the world own cell phones, but comparatively few own personal computers, particularly in developing countries.
|
* Economic availability. A system should not rely on a single device to be constantly online, and should not be overly expensive to use. The majority of people in the world own cell phones, but comparatively few own personal computers, particularly in developing countries. 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.
|
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 goals in mind:
|
When designing Onionr we had these main goals in mind:
|
||||||
|
|
||||||
* Anonymous Blocks
|
* Anonymous Blocks
|
||||||
|
|
||||||
* Difficult to determine block creator or users regardless of transport used
|
* Difficult to determine block creator or users regardless of transport used
|
||||||
* Default Anonymous Transport Layer
|
* Node-anonymity
|
||||||
* Tor and I2P
|
|
||||||
* Transport agnosticism
|
* Transport agnosticism
|
||||||
* Default global sync, but can configure what blocks to seed
|
* Default global sync, but configurable
|
||||||
* Spam resistance
|
* Spam resistance
|
||||||
* Encrypted blocks
|
|
||||||
|
|
||||||
# Onionr Design
|
# Onionr Design
|
||||||
|
|
||||||
@ -40,23 +37,23 @@ When designing Onionr we had these goals in mind:
|
|||||||
|
|
||||||
## General Overview
|
## General Overview
|
||||||
|
|
||||||
At its core, Onionr is merely a description for storing data in self-verifying packages ("blocks"). These blocks can be encrypted to a user (or self), encrypted symmetrically, or not at all. Blocks can be signed by their creator, but regardless, they are self-verifying due to being identified by a sha3-256 hash value; once a block is created, it cannot be modified.
|
At its core, Onionr is merely a description for storing data in self-verifying packages ("blocks"). These blocks can be encrypted to a user (or for one's self), encrypted symmetrically, or not at all. Blocks can be signed by their creator, but regardless, they are self-verifying due to being identified by a sha3-256 hash value; once a block is created, it cannot be modified.
|
||||||
|
|
||||||
Onionr exchanges a list of blocks between all nodes. By default, all nodes download and share all other blocks, however this is configurable.
|
Onionr exchanges a list of blocks between all nodes. By default, all nodes download and share all other blocks, however this is configurable. Blocks do not rely on any particular order of receipt or transport mechanism.
|
||||||
|
|
||||||
## User IDs
|
## User IDs
|
||||||
|
|
||||||
User IDs are simply Ed25519 public keys. They are represented in Base32 format, or encoded using the [PGP Word List](https://en.wikipedia.org/wiki/PGP_word_list).
|
User IDs are simply Ed25519 public keys. They are represented in Base32 format, or encoded using the [PGP Word List](https://en.wikipedia.org/wiki/PGP_word_list).
|
||||||
|
|
||||||
Public keys can be generated deterministicly with a password using a key derivation function (Argon2id). This password can be shared between many users in order to share data anonymously among a group, using only 1 password. This is useful in some cases, but is risky, as if one user causes the key to be compromised and does not notify the group or revoke the key, there is no way to know.
|
Public keys can be generated deterministically with a password using a key derivation function (Argon2id). This password can be shared between many users in order to share data anonymously among a group, using only 1 password. This is useful in some cases, but is risky, as if one user causes the key to be compromised and does not notify the group or revoke the key, there is no way to know.
|
||||||
|
|
||||||
## Nodes
|
## Nodes
|
||||||
|
|
||||||
Although Onionr is transport agnostic, the only supported transports in the reference implemetation are Tor .onion services and I2P hidden services. Nodes announce their address on creation.
|
Although Onionr is transport agnostic, the only supported transports in the reference implementation are Tor .onion services and I2P hidden services. Nodes announce their address on creation by connecting to peers specified in a bootstrap file. Peers in the bootstrap file have no special permissions aside from being default peers.
|
||||||
|
|
||||||
### Node Profiling
|
### Node Profiling
|
||||||
|
|
||||||
To mitigate maliciously slow or unreliable nodes, Onionr builds a profile on nodes it connects to. Nodes are assigned a score, which raises based on the amount of successful block transfers, speed, and reliabilty of a node, and reduces based on how unreliable a node is. If a node is unreachable for over 24 hours after contact, it is forgotten. Onionr can also prioritize connection to 'friend' nodes.
|
To mitigate maliciously slow or unreliable nodes, Onionr builds a profile on nodes it connects to. Nodes are assigned a score, which raises based on the amount of successful block transfers, speed, and reliability of a node, and reduces the score based on how unreliable a node is. If a node is unreachable for over 24 hours after contact, it is forgotten. Onionr can also prioritize connection to 'friend' nodes.
|
||||||
|
|
||||||
## Block Format
|
## Block Format
|
||||||
|
|
||||||
@ -90,8 +87,10 @@ Onionr can provide evidence of when a block was inserted by requesting other use
|
|||||||
|
|
||||||
This can be done either by the creator of the block prior to generation, or by any node after insertion.
|
This can be done either by the creator of the block prior to generation, or by any node after insertion.
|
||||||
|
|
||||||
In addition, randomness beacons such as the one operated by [NIST](https://beacon.nist.gov/home) or the hash of the latest blocks in a cryptocurrency network could be used to affirm that a block was at least not *created* before a given time.
|
In addition, randomness beacons such as the one operated by [NIST](https://beacon.nist.gov/home), [Chile](https://beacon.clcert.cl/), or the hash of the latest blocks in a cryptocurrency network could be used to affirm that a block was at least not *created* before a given time.
|
||||||
|
|
||||||
# Direct Connections
|
# Direct Connections
|
||||||
|
|
||||||
We propose a system to
|
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.
|
197
onionr/api.py
197
onionr/api.py
@ -17,14 +17,28 @@
|
|||||||
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 flask
|
from gevent.pywsgi import WSGIServer, WSGIHandler
|
||||||
|
from gevent import Timeout
|
||||||
|
#import gevent.monkey
|
||||||
|
#gevent.monkey.patch_socket()
|
||||||
|
import flask, cgi
|
||||||
from flask import request, Response, abort, send_from_directory
|
from flask import request, Response, abort, send_from_directory
|
||||||
from gevent.pywsgi import WSGIServer
|
import sys, random, threading, hmac, hashlib, base64, time, math, os, json, socket
|
||||||
import sys, random, threading, hmac, hashlib, base64, time, math, os, json
|
|
||||||
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, onionr
|
||||||
|
|
||||||
|
class FDSafeHandler(WSGIHandler):
|
||||||
|
def handle(self):
|
||||||
|
timeout = Timeout(60, exception=Exception)
|
||||||
|
timeout.start()
|
||||||
|
|
||||||
|
#timeout = gevent.Timeout.start_new(3)
|
||||||
|
try:
|
||||||
|
WSGIHandler.handle(self)
|
||||||
|
except Timeout as ex:
|
||||||
|
raise
|
||||||
|
|
||||||
def guessMime(path):
|
def guessMime(path):
|
||||||
'''
|
'''
|
||||||
Guesses the mime type of a file from the input filename
|
Guesses the mime type of a file from the input filename
|
||||||
@ -48,8 +62,18 @@ def setBindIP(filePath):
|
|||||||
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
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
try:
|
||||||
|
s.bind((data, 0))
|
||||||
|
except OSError:
|
||||||
|
logger.warn('Your platform appears to not support random local host addresses 127.x.x.x. Falling back to 127.0.0.1.')
|
||||||
|
data = '127.0.0.1'
|
||||||
|
s.close()
|
||||||
|
|
||||||
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:
|
||||||
@ -70,6 +94,9 @@ class PublicAPI:
|
|||||||
@app.before_request
|
@app.before_request
|
||||||
def validateRequest():
|
def validateRequest():
|
||||||
'''Validate request has the correct hostname'''
|
'''Validate request has the correct hostname'''
|
||||||
|
# If high security level, deny requests to public
|
||||||
|
if config.get('general.security_level', default=0) > 0:
|
||||||
|
abort(403)
|
||||||
if type(self.torAdder) is None and type(self.i2pAdder) is None:
|
if type(self.torAdder) is None and type(self.i2pAdder) is None:
|
||||||
# abort if our hs addresses are not known
|
# abort if our hs addresses are not known
|
||||||
abort(403)
|
abort(403)
|
||||||
@ -84,6 +111,7 @@ class PublicAPI:
|
|||||||
resp.headers['X-Frame-Options'] = 'deny'
|
resp.headers['X-Frame-Options'] = 'deny'
|
||||||
resp.headers['X-Content-Type-Options'] = "nosniff"
|
resp.headers['X-Content-Type-Options'] = "nosniff"
|
||||||
resp.headers['X-API'] = onionr.API_VERSION
|
resp.headers['X-API'] = onionr.API_VERSION
|
||||||
|
resp.headers['Connection'] = "close"
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
@ -97,7 +125,8 @@ class PublicAPI:
|
|||||||
|
|
||||||
@app.route('/getblocklist')
|
@app.route('/getblocklist')
|
||||||
def getBlockList():
|
def getBlockList():
|
||||||
bList = clientAPI._core.getBlockList()
|
dateAdjust = request.args.get('date')
|
||||||
|
bList = clientAPI._core.getBlockList(dateRec=dateAdjust)
|
||||||
for b in self.hideBlocks:
|
for b in self.hideBlocks:
|
||||||
if b in bList:
|
if b in bList:
|
||||||
bList.remove(b)
|
bList.remove(b)
|
||||||
@ -109,9 +138,9 @@ class PublicAPI:
|
|||||||
data = name
|
data = name
|
||||||
if clientAPI._utils.validateHash(data):
|
if clientAPI._utils.validateHash(data):
|
||||||
if data not in self.hideBlocks:
|
if data not in self.hideBlocks:
|
||||||
if os.path.exists(clientAPI._core.dataDir + 'blocks/' + data + '.dat'):
|
if data in clientAPI._core.getBlockList():
|
||||||
block = Block(hash=data.encode(), core=clientAPI._core)
|
block = clientAPI.getBlockData(data, raw=True).encode()
|
||||||
resp = base64.b64encode(block.getRaw().encode()).decode()
|
resp = base64.b64encode(block).decode()
|
||||||
if len(resp) == 0:
|
if len(resp) == 0:
|
||||||
abort(404)
|
abort(404)
|
||||||
resp = ""
|
resp = ""
|
||||||
@ -133,9 +162,9 @@ class PublicAPI:
|
|||||||
|
|
||||||
@app.route('/pex')
|
@app.route('/pex')
|
||||||
def peerExchange():
|
def peerExchange():
|
||||||
response = ','.join(clientAPI._core.listAdders())
|
response = ','.join(clientAPI._core.listAdders(recent=3600))
|
||||||
if len(response) == 0:
|
if len(response) == 0:
|
||||||
response = 'none'
|
response = ''
|
||||||
return Response(response)
|
return Response(response)
|
||||||
|
|
||||||
@app.route('/announce', methods=['post'])
|
@app.route('/announce', methods=['post'])
|
||||||
@ -204,7 +233,7 @@ class PublicAPI:
|
|||||||
clientAPI._core.refreshFirstStartVars()
|
clientAPI._core.refreshFirstStartVars()
|
||||||
self.torAdder = clientAPI._core.hsAddress
|
self.torAdder = clientAPI._core.hsAddress
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
self.httpServer = WSGIServer((self.host, self.bindPort), app, log=None)
|
self.httpServer = WSGIServer((self.host, self.bindPort), app, log=None, handler_class=FDSafeHandler)
|
||||||
self.httpServer.serve_forever()
|
self.httpServer.serve_forever()
|
||||||
|
|
||||||
class API:
|
class API:
|
||||||
@ -221,19 +250,23 @@ 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
|
# configure logger and stuff
|
||||||
onionr.Onionr.setupConfig('data/', self = self)
|
onionr.Onionr.setupConfig('data/', self = self)
|
||||||
|
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
self._privateDelayTime = 3
|
self._privateDelayTime = 3
|
||||||
self._core = core.Core()
|
self._core = onionrInst.onionrCore
|
||||||
|
self.startTime = self._core._utils.getEpoch()
|
||||||
self._crypto = onionrcrypto.OnionrCrypto(self._core)
|
self._crypto = onionrcrypto.OnionrCrypto(self._core)
|
||||||
self._utils = onionrutils.OnionrUtils(self._core)
|
self._utils = onionrutils.OnionrUtils(self._core)
|
||||||
app = flask.Flask(__name__)
|
app = flask.Flask(__name__)
|
||||||
bindPort = int(config.get('client.client.port', 59496))
|
bindPort = int(config.get('client.client.port', 59496))
|
||||||
self.bindPort = bindPort
|
self.bindPort = bindPort
|
||||||
|
|
||||||
|
# Be extremely mindful of this
|
||||||
|
self.whitelistEndpoints = ('site', 'www', 'onionrhome', 'board', 'boardContent', 'sharedContent', 'mail', 'mailindex')
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
@ -242,6 +275,8 @@ class API:
|
|||||||
self.host = setBindIP(self._core.privateApiHostFile)
|
self.host = setBindIP(self._core.privateApiHostFile)
|
||||||
logger.info('Running api on %s:%s' % (self.host, self.bindPort))
|
logger.info('Running api on %s:%s' % (self.host, self.bindPort))
|
||||||
self.httpServer = ''
|
self.httpServer = ''
|
||||||
|
|
||||||
|
self.queueResponse = {}
|
||||||
onionrInst.setClientAPIInst(self)
|
onionrInst.setClientAPIInst(self)
|
||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
@ -249,6 +284,8 @@ class API:
|
|||||||
'''Validate request has set password and is the correct hostname'''
|
'''Validate request has set password and is the correct hostname'''
|
||||||
if request.host != '%s:%s' % (self.host, self.bindPort):
|
if request.host != '%s:%s' % (self.host, self.bindPort):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
if request.endpoint in self.whitelistEndpoints:
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
if not hmac.compare_digest(request.headers['token'], self.clientToken):
|
if not hmac.compare_digest(request.headers['token'], self.clientToken):
|
||||||
abort(403)
|
abort(403)
|
||||||
@ -257,28 +294,105 @@ class API:
|
|||||||
|
|
||||||
@app.after_request
|
@app.after_request
|
||||||
def afterReq(resp):
|
def afterReq(resp):
|
||||||
resp.headers["Content-Security-Policy"] = "default-src 'none'; script-src 'none'; object-src 'none'; style-src data: 'unsafe-inline'; img-src data:; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'none'"
|
#resp.headers["Content-Security-Policy"] = "default-src 'none'; script-src 'none'; object-src 'none'; style-src data: 'unsafe-inline'; img-src data:; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'none'"
|
||||||
|
resp.headers['Content-Security-Policy'] = "default-src 'none'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'none'; frame-src 'none'; font-src 'none'; connect-src 'self'"
|
||||||
resp.headers['X-Frame-Options'] = 'deny'
|
resp.headers['X-Frame-Options'] = 'deny'
|
||||||
resp.headers['X-Content-Type-Options'] = "nosniff"
|
resp.headers['X-Content-Type-Options'] = "nosniff"
|
||||||
resp.headers['X-API'] = onionr.API_VERSION
|
resp.headers['X-API'] = onionr.API_VERSION
|
||||||
resp.headers['Server'] = ''
|
resp.headers['Server'] = ''
|
||||||
resp.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch.
|
resp.headers['Date'] = 'Thu, 1 Jan 1970 00:00:00 GMT' # Clock info is probably useful to attackers. Set to unix epoch.
|
||||||
|
resp.headers['Connection'] = "close"
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@app.route('/board/', endpoint='board')
|
||||||
|
def loadBoard():
|
||||||
|
return send_from_directory('static-data/www/board/', "index.html")
|
||||||
|
|
||||||
|
@app.route('/mail/<path:path>', endpoint='mail')
|
||||||
|
def loadMail(path):
|
||||||
|
return send_from_directory('static-data/www/mail/', path)
|
||||||
|
@app.route('/mail/', endpoint='mailindex')
|
||||||
|
def loadMailIndex():
|
||||||
|
return send_from_directory('static-data/www/mail/', 'index.html')
|
||||||
|
|
||||||
|
@app.route('/board/<path:path>', endpoint='boardContent')
|
||||||
|
def boardContent(path):
|
||||||
|
return send_from_directory('static-data/www/board/', path)
|
||||||
|
@app.route('/shared/<path:path>', endpoint='sharedContent')
|
||||||
|
def sharedContent(path):
|
||||||
|
return send_from_directory('static-data/www/shared/', path)
|
||||||
|
|
||||||
|
@app.route('/www/<path:path>', endpoint='www')
|
||||||
|
def wwwPublic(path):
|
||||||
|
if not config.get("www.private.run", True):
|
||||||
|
abort(403)
|
||||||
|
return send_from_directory(config.get('www.private.path', 'static-data/www/private/'), path)
|
||||||
|
|
||||||
|
@app.route('/queueResponseAdd/<name>', methods=['post'])
|
||||||
|
def queueResponseAdd(name):
|
||||||
|
self.queueResponse[name] = request.form['data']
|
||||||
|
return Response('success')
|
||||||
|
|
||||||
|
@app.route('/queueResponse/<name>')
|
||||||
|
def queueResponse(name):
|
||||||
|
resp = 'failure'
|
||||||
|
try:
|
||||||
|
resp = self.queueResponse[name]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
del self.queueResponse[name]
|
||||||
|
return Response(resp)
|
||||||
|
|
||||||
@app.route('/ping')
|
@app.route('/ping')
|
||||||
def ping():
|
def ping():
|
||||||
return Response("pong!")
|
return Response("pong!")
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/', endpoint='onionrhome')
|
||||||
def hello():
|
def hello():
|
||||||
return Response("hello client")
|
return send_from_directory('static-data/www/private/', 'index.html')
|
||||||
|
|
||||||
@app.route('/site/<name>')
|
@app.route('/getblocksbytype/<name>')
|
||||||
def site():
|
def getBlocksByType(name):
|
||||||
bHash = block
|
blocks = self._core.getBlocksByType(name)
|
||||||
|
return Response(','.join(blocks))
|
||||||
|
|
||||||
|
@app.route('/gethtmlsafeblockdata/<name>')
|
||||||
|
def getSafeData(name):
|
||||||
|
resp = ''
|
||||||
|
if self._core._utils.validateHash(name):
|
||||||
|
try:
|
||||||
|
resp = cgi.escape(Block(name).bcontent, quote=True)
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
return Response(resp)
|
||||||
|
|
||||||
|
@app.route('/getblockdata/<name>')
|
||||||
|
def getData(name):
|
||||||
|
resp = ""
|
||||||
|
if self._core._utils.validateHash(name):
|
||||||
|
if name in self._core.getBlockList():
|
||||||
|
try:
|
||||||
|
resp = self.getBlockData(name, decrypt=True)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
return Response(resp)
|
||||||
|
|
||||||
|
@app.route('/site/<name>', endpoint='site')
|
||||||
|
def site(name):
|
||||||
|
bHash = name
|
||||||
resp = 'Not Found'
|
resp = 'Not Found'
|
||||||
if self._core._utils.validateHash(bHash):
|
if self._core._utils.validateHash(bHash):
|
||||||
|
try:
|
||||||
resp = Block(bHash).bcontent
|
resp = Block(bHash).bcontent
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
try:
|
try:
|
||||||
resp = base64.b64decode(resp)
|
resp = base64.b64decode(resp)
|
||||||
except:
|
except:
|
||||||
@ -306,7 +420,26 @@ class API:
|
|||||||
pass
|
pass
|
||||||
return Response("bye")
|
return Response("bye")
|
||||||
|
|
||||||
self.httpServer = WSGIServer((self.host, bindPort), app, log=None)
|
@app.route('/shutdownclean')
|
||||||
|
def shutdownClean():
|
||||||
|
# good for calling from other clients
|
||||||
|
self._core.daemonQueueAdd('shutdown')
|
||||||
|
return Response("bye")
|
||||||
|
|
||||||
|
@app.route('/getstats')
|
||||||
|
def getStats():
|
||||||
|
#return Response("disabled")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return Response(self._core.serializer.getStats())
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@app.route('/getuptime')
|
||||||
|
def showUptime():
|
||||||
|
return Response(str(self.getUptime()))
|
||||||
|
|
||||||
|
self.httpServer = WSGIServer((self.host, bindPort), app, log=None, handler_class=FDSafeHandler)
|
||||||
self.httpServer.serve_forever()
|
self.httpServer.serve_forever()
|
||||||
|
|
||||||
def setPublicAPIInstance(self, inst):
|
def setPublicAPIInstance(self, inst):
|
||||||
@ -327,3 +460,29 @@ class API:
|
|||||||
return True
|
return True
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def getUptime(self):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return self._utils.getEpoch - startTime
|
||||||
|
except AttributeError:
|
||||||
|
# Don't error on race condition with startup
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getBlockData(self, bHash, decrypt=False, raw=False):
|
||||||
|
bl = Block(bHash, core=self._core)
|
||||||
|
if decrypt:
|
||||||
|
bl.decrypt()
|
||||||
|
if bl.isEncrypted and not bl.decrypted:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
if not raw:
|
||||||
|
retData = {'meta':bl.bheader, 'metadata': bl.bmetadata, 'content': bl.bcontent}
|
||||||
|
for x in list(retData.keys()):
|
||||||
|
try:
|
||||||
|
retData[x] = retData[x].decode()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
return json.dumps(retData)
|
||||||
|
else:
|
||||||
|
return bl.raw
|
||||||
|
@ -21,15 +21,17 @@
|
|||||||
'''
|
'''
|
||||||
import sys, os, core, config, json, requests, time, logger, threading, base64, onionr, uuid
|
import sys, os, core, config, json, requests, time, logger, threading, base64, onionr, uuid
|
||||||
import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block
|
import onionrexceptions, onionrpeers, onionrevents as events, onionrplugins as plugins, onionrblockapi as block
|
||||||
import onionrdaemontools, onionrsockets, onionrchat, onionr, onionrproofs
|
import onionrdaemontools, onionrsockets, onionr, onionrproofs, proofofmemory
|
||||||
import binascii
|
import binascii
|
||||||
from dependencies import secrets
|
from dependencies import secrets
|
||||||
from defusedxml import minidom
|
from defusedxml import minidom
|
||||||
|
config.reload()
|
||||||
class OnionrCommunicatorDaemon:
|
class OnionrCommunicatorDaemon:
|
||||||
def __init__(self, debug, developmentMode):
|
def __init__(self, onionrInst, proxyPort, developmentMode=config.get('general.dev_mode', False)):
|
||||||
|
onionrInst.communicatorInst = self
|
||||||
# configure logger and stuff
|
# configure logger and stuff
|
||||||
onionr.Onionr.setupConfig('data/', self = self)
|
onionr.Onionr.setupConfig('data/', self = self)
|
||||||
|
self.proxyPort = proxyPort
|
||||||
|
|
||||||
self.isOnline = True # Assume we're connected to the internet
|
self.isOnline = True # Assume we're connected to the internet
|
||||||
|
|
||||||
@ -37,8 +39,8 @@ class OnionrCommunicatorDaemon:
|
|||||||
self.timers = []
|
self.timers = []
|
||||||
|
|
||||||
# initalize core with Tor socks port being 3rd argument
|
# initalize core with Tor socks port being 3rd argument
|
||||||
self.proxyPort = sys.argv[2]
|
self.proxyPort = proxyPort
|
||||||
self._core = core.Core(torPort=self.proxyPort)
|
self._core = onionrInst.onionrCore
|
||||||
|
|
||||||
# intalize NIST beacon salt and time
|
# intalize NIST beacon salt and time
|
||||||
self.nistSaltTimestamp = 0
|
self.nistSaltTimestamp = 0
|
||||||
@ -49,9 +51,6 @@ class OnionrCommunicatorDaemon:
|
|||||||
# loop time.sleep delay in seconds
|
# loop time.sleep delay in seconds
|
||||||
self.delay = 1
|
self.delay = 1
|
||||||
|
|
||||||
# time app started running for info/statistics purposes
|
|
||||||
self.startTime = self._core._utils.getEpoch()
|
|
||||||
|
|
||||||
# lists of connected peers and peers we know we can't reach currently
|
# lists of connected peers and peers we know we can't reach currently
|
||||||
self.onlinePeers = []
|
self.onlinePeers = []
|
||||||
self.offlinePeers = []
|
self.offlinePeers = []
|
||||||
@ -66,7 +65,7 @@ class OnionrCommunicatorDaemon:
|
|||||||
self.shutdown = False
|
self.shutdown = False
|
||||||
|
|
||||||
# list of new blocks to download, added to when new block lists are fetched from peers
|
# list of new blocks to download, added to when new block lists are fetched from peers
|
||||||
self.blockQueue = []
|
self.blockQueue = {}
|
||||||
|
|
||||||
# list of blocks currently downloading, avoid s
|
# list of blocks currently downloading, avoid s
|
||||||
self.currentDownloading = []
|
self.currentDownloading = []
|
||||||
@ -74,6 +73,9 @@ class OnionrCommunicatorDaemon:
|
|||||||
# timestamp when the last online node was seen
|
# timestamp when the last online node was seen
|
||||||
self.lastNodeSeen = None
|
self.lastNodeSeen = None
|
||||||
|
|
||||||
|
# Dict of time stamps for peer's block list lookup times, to avoid downloading full lists all the time
|
||||||
|
self.dbTimestamps = {}
|
||||||
|
|
||||||
# Clear the daemon queue for any dead messages
|
# Clear the daemon queue for any dead messages
|
||||||
if os.path.exists(self._core.queueDB):
|
if os.path.exists(self._core.queueDB):
|
||||||
self._core.clearDaemonQueue()
|
self._core.clearDaemonQueue()
|
||||||
@ -81,33 +83,36 @@ class OnionrCommunicatorDaemon:
|
|||||||
# Loads in and starts the enabled plugins
|
# Loads in and starts the enabled plugins
|
||||||
plugins.reload()
|
plugins.reload()
|
||||||
|
|
||||||
|
self.proofofmemory = proofofmemory.ProofOfMemory(self)
|
||||||
|
|
||||||
# daemon tools are misc daemon functions, e.g. announce to online peers
|
# daemon tools are misc daemon functions, e.g. announce to online peers
|
||||||
# intended only for use by OnionrCommunicatorDaemon
|
# intended only for use by OnionrCommunicatorDaemon
|
||||||
self.daemonTools = onionrdaemontools.DaemonTools(self)
|
self.daemonTools = onionrdaemontools.DaemonTools(self)
|
||||||
|
|
||||||
self._chat = onionrchat.OnionrChat(self)
|
# time app started running for info/statistics purposes
|
||||||
|
self.startTime = self._core._utils.getEpoch()
|
||||||
|
|
||||||
if debug or developmentMode:
|
if developmentMode:
|
||||||
OnionrCommunicatorTimers(self, self.heartbeat, 30)
|
OnionrCommunicatorTimers(self, self.heartbeat, 30)
|
||||||
|
|
||||||
# Set timers, function reference, seconds
|
# Set timers, function reference, seconds
|
||||||
# requiresPeer True means the timer function won't fire if we have no connected peers
|
# requiresPeer True means the timer function won't fire if we have no connected peers
|
||||||
peerPoolTimer = OnionrCommunicatorTimers(self, self.getOnlinePeers, 60, maxThreads=1)
|
peerPoolTimer = OnionrCommunicatorTimers(self, self.getOnlinePeers, 60, maxThreads=1)
|
||||||
OnionrCommunicatorTimers(self, self.runCheck, 1)
|
OnionrCommunicatorTimers(self, self.runCheck, 2, maxThreads=1)
|
||||||
OnionrCommunicatorTimers(self, self.lookupBlocks, self._core.config.get('timers.lookupBlocks'), requiresPeer=True, maxThreads=1)
|
OnionrCommunicatorTimers(self, self.lookupBlocks, self._core.config.get('timers.lookupBlocks'), requiresPeer=True, maxThreads=1)
|
||||||
OnionrCommunicatorTimers(self, self.getBlocks, self._core.config.get('timers.getBlocks'), requiresPeer=True)
|
OnionrCommunicatorTimers(self, self.getBlocks, self._core.config.get('timers.getBlocks'), requiresPeer=True, maxThreads=2)
|
||||||
OnionrCommunicatorTimers(self, self.clearOfflinePeer, 58)
|
OnionrCommunicatorTimers(self, self.clearOfflinePeer, 58)
|
||||||
OnionrCommunicatorTimers(self, self.daemonTools.cleanOldBlocks, 65)
|
OnionrCommunicatorTimers(self, self.daemonTools.cleanOldBlocks, 65)
|
||||||
OnionrCommunicatorTimers(self, self.lookupAdders, 60, requiresPeer=True)
|
OnionrCommunicatorTimers(self, self.lookupAdders, 60, requiresPeer=True)
|
||||||
OnionrCommunicatorTimers(self, self.daemonTools.cooldownPeer, 30, requiresPeer=True)
|
OnionrCommunicatorTimers(self, self.daemonTools.cooldownPeer, 30, requiresPeer=True)
|
||||||
OnionrCommunicatorTimers(self, self.uploadBlock, 10, requiresPeer=True, maxThreads=1)
|
OnionrCommunicatorTimers(self, self.uploadBlock, 10, requiresPeer=True, maxThreads=1)
|
||||||
OnionrCommunicatorTimers(self, self.daemonCommands, 6, maxThreads=1)
|
OnionrCommunicatorTimers(self, self.daemonCommands, 6, maxThreads=1)
|
||||||
OnionrCommunicatorTimers(self, self.detectAPICrash, 5, maxThreads=1)
|
OnionrCommunicatorTimers(self, self.detectAPICrash, 30, maxThreads=1)
|
||||||
deniableBlockTimer = OnionrCommunicatorTimers(self, self.daemonTools.insertDeniableBlock, 180, requiresPeer=True, maxThreads=1)
|
deniableBlockTimer = OnionrCommunicatorTimers(self, self.daemonTools.insertDeniableBlock, 180, requiresPeer=True, maxThreads=1)
|
||||||
|
|
||||||
netCheckTimer = OnionrCommunicatorTimers(self, self.daemonTools.netCheck, 600)
|
netCheckTimer = OnionrCommunicatorTimers(self, self.daemonTools.netCheck, 600)
|
||||||
if config.get('general.security_level') == 0:
|
if config.get('general.security_level') == 0:
|
||||||
announceTimer = OnionrCommunicatorTimers(self, self.daemonTools.announceNode, 86400, requiresPeer=True, maxThreads=1)
|
announceTimer = OnionrCommunicatorTimers(self, self.daemonTools.announceNode, 3600, requiresPeer=True, maxThreads=1)
|
||||||
announceTimer.count = (announceTimer.frequency - 120)
|
announceTimer.count = (announceTimer.frequency - 120)
|
||||||
else:
|
else:
|
||||||
logger.debug('Will not announce node.')
|
logger.debug('Will not announce node.')
|
||||||
@ -125,8 +130,6 @@ class OnionrCommunicatorDaemon:
|
|||||||
self.socketServer.start()
|
self.socketServer.start()
|
||||||
self.socketClient = onionrsockets.OnionrSocketClient(self._core)
|
self.socketClient = onionrsockets.OnionrSocketClient(self._core)
|
||||||
|
|
||||||
# Loads chat messages into memory
|
|
||||||
threading.Thread(target=self._chat.chatHandler).start()
|
|
||||||
|
|
||||||
# Main daemon loop, mainly for calling timers, don't do any complex operations here to avoid locking
|
# Main daemon loop, mainly for calling timers, don't do any complex operations here to avoid locking
|
||||||
try:
|
try:
|
||||||
@ -136,6 +139,9 @@ class OnionrCommunicatorDaemon:
|
|||||||
break
|
break
|
||||||
i.processTimer()
|
i.processTimer()
|
||||||
time.sleep(self.delay)
|
time.sleep(self.delay)
|
||||||
|
# Debug to print out used FDs (regular and net)
|
||||||
|
#proc = psutil.Process()
|
||||||
|
#print(proc.open_files(), len(psutil.net_connections()))
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
self.shutdown = True
|
self.shutdown = True
|
||||||
pass
|
pass
|
||||||
@ -164,7 +170,9 @@ class OnionrCommunicatorDaemon:
|
|||||||
existingBlocks = self._core.getBlockList()
|
existingBlocks = self._core.getBlockList()
|
||||||
triedPeers = [] # list of peers we've tried this time around
|
triedPeers = [] # list of peers we've tried this time around
|
||||||
maxBacklog = 1560 # Max amount of *new* block hashes to have already in queue, to avoid memory exhaustion
|
maxBacklog = 1560 # Max amount of *new* block hashes to have already in queue, to avoid memory exhaustion
|
||||||
|
lastLookupTime = 0 # Last time we looked up a particular peer's list
|
||||||
for i in range(tryAmount):
|
for i in range(tryAmount):
|
||||||
|
listLookupCommand = 'getblocklist' # This is defined here to reset it each time
|
||||||
if len(self.blockQueue) >= maxBacklog:
|
if len(self.blockQueue) >= maxBacklog:
|
||||||
break
|
break
|
||||||
if not self.isOnline:
|
if not self.isOnline:
|
||||||
@ -186,11 +194,21 @@ class OnionrCommunicatorDaemon:
|
|||||||
triedPeers.append(peer)
|
triedPeers.append(peer)
|
||||||
if newDBHash != self._core.getAddressInfo(peer, 'DBHash'):
|
if newDBHash != self._core.getAddressInfo(peer, 'DBHash'):
|
||||||
self._core.setAddressInfo(peer, 'DBHash', newDBHash)
|
self._core.setAddressInfo(peer, 'DBHash', newDBHash)
|
||||||
|
# Get the last time we looked up a peer's stamp to only fetch blocks since then.
|
||||||
|
# Saved in memory only for privacy reasons
|
||||||
try:
|
try:
|
||||||
newBlocks = self.peerAction(peer, 'getblocklist') # get list of new block hashes
|
lastLookupTime = self.dbTimestamps[peer]
|
||||||
|
except KeyError:
|
||||||
|
lastLookupTime = 0
|
||||||
|
else:
|
||||||
|
listLookupCommand += '?date=%s' % (lastLookupTime,)
|
||||||
|
try:
|
||||||
|
newBlocks = self.peerAction(peer, listLookupCommand) # get list of new block hashes
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logger.warn('Could not get new blocks from %s.' % peer, error = error)
|
logger.warn('Could not get new blocks from %s.' % peer, error = error)
|
||||||
newBlocks = False
|
newBlocks = False
|
||||||
|
else:
|
||||||
|
self.dbTimestamps[peer] = self._core._utils.getRoundedEpoch(roundS=60)
|
||||||
if newBlocks != False:
|
if newBlocks != False:
|
||||||
# if request was a success
|
# if request was a success
|
||||||
for i in newBlocks.split('\n'):
|
for i in newBlocks.split('\n'):
|
||||||
@ -198,15 +216,24 @@ class OnionrCommunicatorDaemon:
|
|||||||
# if newline seperated string is valid hash
|
# if newline seperated string is valid hash
|
||||||
if not i in existingBlocks:
|
if not i in existingBlocks:
|
||||||
# if block does not exist on disk and is not already in block queue
|
# if block does not exist on disk and is not already in block queue
|
||||||
if i not in self.blockQueue and not self._core._blacklist.inBlacklist(i):
|
if i not in self.blockQueue:
|
||||||
if onionrproofs.hashMeetsDifficulty(i):
|
if onionrproofs.hashMeetsDifficulty(i) and not self._core._blacklist.inBlacklist(i):
|
||||||
self.blockQueue.append(i) # add blocks to download queue
|
if len(self.blockQueue) <= 1000000:
|
||||||
|
self.blockQueue[i] = [peer] # add blocks to download queue
|
||||||
|
else:
|
||||||
|
if peer not in self.blockQueue[i]:
|
||||||
|
self.blockQueue[i].append(peer)
|
||||||
self.decrementThreadCount('lookupBlocks')
|
self.decrementThreadCount('lookupBlocks')
|
||||||
return
|
return
|
||||||
|
|
||||||
def getBlocks(self):
|
def getBlocks(self):
|
||||||
'''download new blocks in queue'''
|
'''download new blocks in queue'''
|
||||||
for blockHash in self.blockQueue:
|
for blockHash in list(self.blockQueue):
|
||||||
|
triedQueuePeers = [] # List of peers we've tried for a block
|
||||||
|
try:
|
||||||
|
blockPeers = list(self.blockQueue[blockHash])
|
||||||
|
except KeyError:
|
||||||
|
blockPeers = []
|
||||||
removeFromQueue = True
|
removeFromQueue = True
|
||||||
if self.shutdown or not self.isOnline:
|
if self.shutdown or not self.isOnline:
|
||||||
# Exit loop if shutting down or offline
|
# Exit loop if shutting down or offline
|
||||||
@ -217,15 +244,24 @@ class OnionrCommunicatorDaemon:
|
|||||||
continue
|
continue
|
||||||
if blockHash in self._core.getBlockList():
|
if blockHash in self._core.getBlockList():
|
||||||
logger.debug('Block %s is already saved.' % (blockHash,))
|
logger.debug('Block %s is already saved.' % (blockHash,))
|
||||||
self.blockQueue.remove(blockHash)
|
try:
|
||||||
|
del self.blockQueue[blockHash]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
continue
|
continue
|
||||||
if self._core._blacklist.inBlacklist(blockHash):
|
if self._core._blacklist.inBlacklist(blockHash):
|
||||||
continue
|
continue
|
||||||
if self._core._utils.storageCounter.isFull():
|
if self._core._utils.storageCounter.isFull():
|
||||||
break
|
break
|
||||||
self.currentDownloading.append(blockHash) # So we can avoid concurrent downloading in other threads of same block
|
self.currentDownloading.append(blockHash) # So we can avoid concurrent downloading in other threads of same block
|
||||||
logger.info("Attempting to download %s..." % blockHash)
|
if len(blockPeers) == 0:
|
||||||
peerUsed = self.pickOnlinePeer()
|
peerUsed = self.pickOnlinePeer()
|
||||||
|
else:
|
||||||
|
blockPeers = self._core._crypto.randomShuffle(blockPeers)
|
||||||
|
peerUsed = blockPeers.pop(0)
|
||||||
|
|
||||||
|
if not self.shutdown and peerUsed.strip() != '':
|
||||||
|
logger.info("Attempting to download %s from %s..." % (blockHash[:12], peerUsed))
|
||||||
content = self.peerAction(peerUsed, 'getdata/' + blockHash) # block content from random peer (includes metadata)
|
content = self.peerAction(peerUsed, 'getdata/' + blockHash) # block content from random peer (includes metadata)
|
||||||
if content != False and len(content) > 0:
|
if content != False and len(content) > 0:
|
||||||
try:
|
try:
|
||||||
@ -247,7 +283,7 @@ class OnionrCommunicatorDaemon:
|
|||||||
metadata = metas[0]
|
metadata = metas[0]
|
||||||
if self._core._utils.validateMetadata(metadata, metas[2]): # check if metadata is valid, and verify nonce
|
if self._core._utils.validateMetadata(metadata, metas[2]): # check if metadata is valid, and verify nonce
|
||||||
if self._core._crypto.verifyPow(content): # check if POW is enough/correct
|
if self._core._crypto.verifyPow(content): # check if POW is enough/correct
|
||||||
logger.info('Attempting to save block %s...' % blockHash)
|
logger.info('Attempting to save block %s...' % blockHash[:12])
|
||||||
try:
|
try:
|
||||||
self._core.setData(content)
|
self._core.setData(content)
|
||||||
except onionrexceptions.DiskAllocationReached:
|
except onionrexceptions.DiskAllocationReached:
|
||||||
@ -273,11 +309,15 @@ class OnionrCommunicatorDaemon:
|
|||||||
pass
|
pass
|
||||||
# Punish peer for sharing invalid block (not always malicious, but is bad regardless)
|
# Punish peer for sharing invalid block (not always malicious, but is bad regardless)
|
||||||
onionrpeers.PeerProfiles(peerUsed, self._core).addScore(-50)
|
onionrpeers.PeerProfiles(peerUsed, self._core).addScore(-50)
|
||||||
|
if tempHash != 'ed55e34cb828232d6c14da0479709bfa10a0923dca2b380496e6b2ed4f7a0253':
|
||||||
|
# Dumb hack for 404 response from peer. Don't log it if 404 since its likely not malicious or a critical error.
|
||||||
logger.warn('Block hash validation failed for ' + blockHash + ' got ' + tempHash)
|
logger.warn('Block hash validation failed for ' + blockHash + ' got ' + tempHash)
|
||||||
|
else:
|
||||||
|
removeFromQueue = False # Don't remove from queue if 404
|
||||||
if removeFromQueue:
|
if removeFromQueue:
|
||||||
try:
|
try:
|
||||||
self.blockQueue.remove(blockHash) # remove from block queue both if success or false
|
del self.blockQueue[blockHash] # remove from block queue both if success or false
|
||||||
except ValueError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
self.currentDownloading.remove(blockHash)
|
self.currentDownloading.remove(blockHash)
|
||||||
self.decrementThreadCount('getBlocks')
|
self.decrementThreadCount('getBlocks')
|
||||||
@ -401,6 +441,10 @@ class OnionrCommunicatorDaemon:
|
|||||||
del self.connectTimes[peer]
|
del self.connectTimes[peer]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
try:
|
||||||
|
del self.dbTimestamps[peer]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
try:
|
try:
|
||||||
self.onlinePeers.remove(peer)
|
self.onlinePeers.remove(peer)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -459,10 +503,12 @@ class OnionrCommunicatorDaemon:
|
|||||||
retData = onionrpeers.PeerProfiles(peer, self._core)
|
retData = onionrpeers.PeerProfiles(peer, self._core)
|
||||||
return retData
|
return retData
|
||||||
|
|
||||||
|
def getUptime(self):
|
||||||
|
return self._core._utils.getEpoch() - self.startTime
|
||||||
|
|
||||||
def heartbeat(self):
|
def heartbeat(self):
|
||||||
'''Show a heartbeat debug message'''
|
'''Show a heartbeat debug message'''
|
||||||
currentTime = self._core._utils.getEpoch() - self.startTime
|
logger.debug('Heartbeat. Node running for %s.' % self.daemonTools.humanReadableTime(self.getUptime()))
|
||||||
logger.debug('Heartbeat. Node running for %s.' % self.daemonTools.humanReadableTime(currentTime))
|
|
||||||
self.decrementThreadCount('heartbeat')
|
self.decrementThreadCount('heartbeat')
|
||||||
|
|
||||||
def daemonCommands(self):
|
def daemonCommands(self):
|
||||||
@ -470,7 +516,7 @@ class OnionrCommunicatorDaemon:
|
|||||||
Process daemon commands from daemonQueue
|
Process daemon commands from daemonQueue
|
||||||
'''
|
'''
|
||||||
cmd = self._core.daemonQueue()
|
cmd = self._core.daemonQueue()
|
||||||
|
response = ''
|
||||||
if cmd is not False:
|
if cmd is not False:
|
||||||
events.event('daemon_command', onionr = None, data = {'cmd' : cmd})
|
events.event('daemon_command', onionr = None, data = {'cmd' : cmd})
|
||||||
if cmd[0] == 'shutdown':
|
if cmd[0] == 'shutdown':
|
||||||
@ -484,7 +530,11 @@ class OnionrCommunicatorDaemon:
|
|||||||
logger.debug('Status check; looks good.')
|
logger.debug('Status check; looks good.')
|
||||||
open(self._core.dataDir + '.runcheck', 'w+').close()
|
open(self._core.dataDir + '.runcheck', 'w+').close()
|
||||||
elif cmd[0] == 'connectedPeers':
|
elif cmd[0] == 'connectedPeers':
|
||||||
self.printOnlinePeers()
|
response = '\n'.join(list(self.onlinePeers)).strip()
|
||||||
|
if response == '':
|
||||||
|
response = 'none'
|
||||||
|
elif cmd[0] == 'localCommand':
|
||||||
|
response = self._core._utils.localCommand(cmd[1])
|
||||||
elif cmd[0] == 'pex':
|
elif cmd[0] == 'pex':
|
||||||
for i in self.timers:
|
for i in self.timers:
|
||||||
if i.timerFunction.__name__ == 'lookupAdders':
|
if i.timerFunction.__name__ == 'lookupAdders':
|
||||||
@ -505,6 +555,11 @@ class OnionrCommunicatorDaemon:
|
|||||||
else:
|
else:
|
||||||
logger.info('Recieved daemonQueue command:' + cmd[0])
|
logger.info('Recieved daemonQueue command:' + cmd[0])
|
||||||
|
|
||||||
|
if cmd[0] not in ('', None):
|
||||||
|
if response != '':
|
||||||
|
self._core._utils.localCommand('queueResponseAdd/' + cmd[4], post=True, postData={'data': response})
|
||||||
|
response = ''
|
||||||
|
|
||||||
self.decrementThreadCount('daemonCommands')
|
self.decrementThreadCount('daemonCommands')
|
||||||
|
|
||||||
def uploadBlock(self):
|
def uploadBlock(self):
|
||||||
@ -512,13 +567,14 @@ class OnionrCommunicatorDaemon:
|
|||||||
# when inserting a block, we try to upload it to a few peers to add some deniability
|
# when inserting a block, we try to upload it to a few peers to add some deniability
|
||||||
triedPeers = []
|
triedPeers = []
|
||||||
finishedUploads = []
|
finishedUploads = []
|
||||||
|
self.blocksToUpload = self._core._crypto.randomShuffle(self.blocksToUpload)
|
||||||
if len(self.blocksToUpload) != 0:
|
if len(self.blocksToUpload) != 0:
|
||||||
for bl in self.blocksToUpload:
|
for bl in self.blocksToUpload:
|
||||||
if not self._core._utils.validateHash(bl):
|
if not self._core._utils.validateHash(bl):
|
||||||
logger.warn('Requested to upload invalid block')
|
logger.warn('Requested to upload invalid block')
|
||||||
self.decrementThreadCount('uploadBlock')
|
self.decrementThreadCount('uploadBlock')
|
||||||
return
|
return
|
||||||
for i in range(max(len(self.onlinePeers), 2)):
|
for i in range(min(len(self.onlinePeers), 6)):
|
||||||
peer = self.pickOnlinePeer()
|
peer = self.pickOnlinePeer()
|
||||||
if peer in triedPeers:
|
if peer in triedPeers:
|
||||||
continue
|
continue
|
||||||
@ -534,7 +590,6 @@ class OnionrCommunicatorDaemon:
|
|||||||
if not self._core._utils.doPostRequest(url, data=data, proxyType=proxyType) == False:
|
if not self._core._utils.doPostRequest(url, data=data, proxyType=proxyType) == False:
|
||||||
self._core._utils.localCommand('waitforshare/' + bl)
|
self._core._utils.localCommand('waitforshare/' + bl)
|
||||||
finishedUploads.append(bl)
|
finishedUploads.append(bl)
|
||||||
break
|
|
||||||
for x in finishedUploads:
|
for x in finishedUploads:
|
||||||
try:
|
try:
|
||||||
self.blocksToUpload.remove(x)
|
self.blocksToUpload.remove(x)
|
||||||
@ -610,18 +665,5 @@ class OnionrCommunicatorTimers:
|
|||||||
self.count = -1 # negative 1 because its incremented at bottom
|
self.count = -1 # negative 1 because its incremented at bottom
|
||||||
self.count += 1
|
self.count += 1
|
||||||
|
|
||||||
shouldRun = False
|
def startCommunicator(onionrInst, proxyPort):
|
||||||
debug = True
|
OnionrCommunicatorDaemon(onionrInst, proxyPort)
|
||||||
developmentMode = False
|
|
||||||
if config.get('general.dev_mode', True):
|
|
||||||
developmentMode = True
|
|
||||||
try:
|
|
||||||
if sys.argv[1] == 'run':
|
|
||||||
shouldRun = True
|
|
||||||
except IndexError:
|
|
||||||
pass
|
|
||||||
if shouldRun:
|
|
||||||
try:
|
|
||||||
OnionrCommunicatorDaemon(debug, developmentMode)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error('Error occured in Communicator', error = e, timestamp = False)
|
|
@ -105,7 +105,8 @@ def check():
|
|||||||
open(get_config_file(), 'a', encoding="utf8").close()
|
open(get_config_file(), 'a', encoding="utf8").close()
|
||||||
save()
|
save()
|
||||||
except:
|
except:
|
||||||
logger.warn('Failed to check configuration file.')
|
pass
|
||||||
|
#logger.debug('Failed to check configuration file.')
|
||||||
|
|
||||||
def save():
|
def save():
|
||||||
'''
|
'''
|
||||||
@ -129,7 +130,8 @@ def reload():
|
|||||||
with open(get_config_file(), 'r', encoding="utf8") as configfile:
|
with open(get_config_file(), 'r', encoding="utf8") as configfile:
|
||||||
set_config(json.loads(configfile.read()))
|
set_config(json.loads(configfile.read()))
|
||||||
except:
|
except:
|
||||||
logger.warn('Failed to parse configuration file.')
|
pass
|
||||||
|
#logger.debug('Failed to parse configuration file.')
|
||||||
|
|
||||||
def get_config():
|
def get_config():
|
||||||
'''
|
'''
|
||||||
|
161
onionr/core.py
161
onionr/core.py
@ -17,12 +17,13 @@
|
|||||||
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 sqlite3, os, sys, time, math, base64, tarfile, nacl, logger, json, netcontroller, math, config
|
import sqlite3, os, sys, time, math, base64, tarfile, nacl, logger, json, netcontroller, math, config, uuid
|
||||||
from onionrblockapi import Block
|
from onionrblockapi import Block
|
||||||
|
|
||||||
import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions, onionrvalues
|
import onionrutils, onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions
|
||||||
import onionrblacklist, onionrchat, onionrusers
|
import onionrblacklist, onionrusers
|
||||||
import dbcreator
|
import dbcreator, onionrstorage, serializeddata
|
||||||
|
from etc import onionrvalues
|
||||||
|
|
||||||
if sys.version_info < (3, 6):
|
if sys.version_info < (3, 6):
|
||||||
try:
|
try:
|
||||||
@ -45,10 +46,12 @@ class Core:
|
|||||||
self.dataDir = 'data/'
|
self.dataDir = 'data/'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
self.onionrInst = None
|
||||||
self.queueDB = self.dataDir + 'queue.db'
|
self.queueDB = self.dataDir + 'queue.db'
|
||||||
self.peerDB = self.dataDir + 'peers.db'
|
self.peerDB = self.dataDir + 'peers.db'
|
||||||
self.blockDB = self.dataDir + 'blocks.db'
|
self.blockDB = self.dataDir + 'blocks.db'
|
||||||
self.blockDataLocation = self.dataDir + 'blocks/'
|
self.blockDataLocation = self.dataDir + 'blocks/'
|
||||||
|
self.blockDataDB = self.blockDataLocation + 'block-data.db'
|
||||||
self.publicApiHostFile = self.dataDir + 'public-host.txt'
|
self.publicApiHostFile = self.dataDir + 'public-host.txt'
|
||||||
self.privateApiHostFile = self.dataDir + 'private-host.txt'
|
self.privateApiHostFile = self.dataDir + 'private-host.txt'
|
||||||
self.addressDB = self.dataDir + 'address.db'
|
self.addressDB = self.dataDir + 'address.db'
|
||||||
@ -97,9 +100,11 @@ 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)
|
||||||
|
self.serializer = serializeddata.SerializedData(self)
|
||||||
|
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logger.error('Failed to initialize core Onionr library.', error=error)
|
logger.error('Failed to initialize core Onionr library.', error=error)
|
||||||
@ -127,7 +132,7 @@ class Core:
|
|||||||
|
|
||||||
events.event('pubkey_add', data = {'key': peerID}, onionr = None)
|
events.event('pubkey_add', data = {'key': peerID}, onionr = None)
|
||||||
|
|
||||||
conn = sqlite3.connect(self.peerDB, timeout=10)
|
conn = sqlite3.connect(self.peerDB, timeout=30)
|
||||||
hashID = self._crypto.pubKeyHashID(peerID)
|
hashID = self._crypto.pubKeyHashID(peerID)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
t = (peerID, name, 'unknown', hashID, 0)
|
t = (peerID, name, 'unknown', hashID, 0)
|
||||||
@ -157,7 +162,7 @@ class Core:
|
|||||||
if type(address) is None or len(address) == 0:
|
if type(address) is None or len(address) == 0:
|
||||||
return False
|
return False
|
||||||
if self._utils.validateID(address):
|
if self._utils.validateID(address):
|
||||||
conn = sqlite3.connect(self.addressDB, timeout=10)
|
conn = sqlite3.connect(self.addressDB, timeout=30)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
# check if address is in database
|
# check if address is in database
|
||||||
# this is safe to do because the address is validated above, but we strip some chars here too just in case
|
# this is safe to do because the address is validated above, but we strip some chars here too just in case
|
||||||
@ -181,7 +186,7 @@ class Core:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.debug('Invalid ID: %s' % address)
|
#logger.debug('Invalid ID: %s' % address)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def removeAddress(self, address):
|
def removeAddress(self, address):
|
||||||
@ -190,7 +195,7 @@ class Core:
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
if self._utils.validateID(address):
|
if self._utils.validateID(address):
|
||||||
conn = sqlite3.connect(self.addressDB, timeout=10)
|
conn = sqlite3.connect(self.addressDB, timeout=30)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
t = (address,)
|
t = (address,)
|
||||||
c.execute('Delete from adders where address=?;', t)
|
c.execute('Delete from adders where address=?;', t)
|
||||||
@ -210,7 +215,7 @@ class Core:
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
if self._utils.validateHash(block):
|
if self._utils.validateHash(block):
|
||||||
conn = sqlite3.connect(self.blockDB, timeout=10)
|
conn = sqlite3.connect(self.blockDB, timeout=30)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
t = (block,)
|
t = (block,)
|
||||||
c.execute('Delete from hashes where hash=?;', t)
|
c.execute('Delete from hashes where hash=?;', t)
|
||||||
@ -258,7 +263,7 @@ class Core:
|
|||||||
raise Exception('Block db does not exist')
|
raise Exception('Block db does not exist')
|
||||||
if self._utils.hasBlock(newHash):
|
if self._utils.hasBlock(newHash):
|
||||||
return
|
return
|
||||||
conn = sqlite3.connect(self.blockDB, timeout=10)
|
conn = sqlite3.connect(self.blockDB, timeout=30)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
currentTime = self._utils.getEpoch() + self._crypto.secrets.randbelow(301)
|
currentTime = self._utils.getEpoch() + self._crypto.secrets.randbelow(301)
|
||||||
if selfInsert or dataSaved:
|
if selfInsert or dataSaved:
|
||||||
@ -277,6 +282,7 @@ class Core:
|
|||||||
Simply return the data associated to a hash
|
Simply return the data associated to a hash
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
'''
|
||||||
try:
|
try:
|
||||||
# logger.debug('Opening %s' % (str(self.blockDataLocation) + str(hash) + '.dat'))
|
# logger.debug('Opening %s' % (str(self.blockDataLocation) + str(hash) + '.dat'))
|
||||||
dataFile = open(self.blockDataLocation + hash + '.dat', 'rb')
|
dataFile = open(self.blockDataLocation + hash + '.dat', 'rb')
|
||||||
@ -284,6 +290,8 @@ class Core:
|
|||||||
dataFile.close()
|
dataFile.close()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
data = False
|
data = False
|
||||||
|
'''
|
||||||
|
data = onionrstorage.getData(self, hash)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@ -308,10 +316,11 @@ class Core:
|
|||||||
#raise Exception("Data is already set for " + dataHash)
|
#raise Exception("Data is already set for " + dataHash)
|
||||||
else:
|
else:
|
||||||
if self._utils.storageCounter.addBytes(dataSize) != False:
|
if self._utils.storageCounter.addBytes(dataSize) != False:
|
||||||
blockFile = open(blockFileName, 'wb')
|
#blockFile = open(blockFileName, 'wb')
|
||||||
blockFile.write(data)
|
#blockFile.write(data)
|
||||||
blockFile.close()
|
#blockFile.close()
|
||||||
conn = sqlite3.connect(self.blockDB, timeout=10)
|
onionrstorage.store(self, data, blockHash=dataHash)
|
||||||
|
conn = sqlite3.connect(self.blockDB, timeout=30)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute("UPDATE hashes SET dataSaved=1 WHERE hash = ?;", (dataHash,))
|
c.execute("UPDATE hashes SET dataSaved=1 WHERE hash = ?;", (dataHash,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@ -334,10 +343,10 @@ class Core:
|
|||||||
if not os.path.exists(self.queueDB):
|
if not os.path.exists(self.queueDB):
|
||||||
self.dbCreate.createDaemonDB()
|
self.dbCreate.createDaemonDB()
|
||||||
else:
|
else:
|
||||||
conn = sqlite3.connect(self.queueDB, timeout=10)
|
conn = sqlite3.connect(self.queueDB, timeout=30)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
try:
|
try:
|
||||||
for row in c.execute('SELECT command, data, date, min(ID) FROM commands group by id'):
|
for row in c.execute('SELECT command, data, date, min(ID), responseID FROM commands group by id'):
|
||||||
retData = row
|
retData = row
|
||||||
break
|
break
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
@ -352,34 +361,58 @@ class Core:
|
|||||||
|
|
||||||
return retData
|
return retData
|
||||||
|
|
||||||
def daemonQueueAdd(self, command, data=''):
|
def daemonQueueAdd(self, command, data='', responseID=''):
|
||||||
'''
|
'''
|
||||||
Add a command to the daemon queue, used by the communication daemon (communicator.py)
|
Add a command to the daemon queue, used by the communication daemon (communicator.py)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
retData = True
|
retData = True
|
||||||
# Intended to be used by the web server
|
|
||||||
|
|
||||||
date = self._utils.getEpoch()
|
date = self._utils.getEpoch()
|
||||||
conn = sqlite3.connect(self.queueDB, timeout=10)
|
conn = sqlite3.connect(self.queueDB, timeout=30)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
t = (command, data, date)
|
t = (command, data, date, responseID)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
c.execute('INSERT INTO commands (command, data, date) VALUES(?, ?, ?)', t)
|
c.execute('INSERT INTO commands (command, data, date, responseID) VALUES(?, ?, ?, ?)', t)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
retData = False
|
retData = False
|
||||||
self.daemonQueue()
|
self.daemonQueue()
|
||||||
events.event('queue_push', data = {'command': command, 'data': data}, onionr = None)
|
events.event('queue_push', data = {'command': command, 'data': data}, onionr = None)
|
||||||
|
conn.close()
|
||||||
return retData
|
return retData
|
||||||
|
|
||||||
|
def daemonQueueGetResponse(self, responseID=''):
|
||||||
|
'''
|
||||||
|
Get a response sent by communicator to the API, by requesting to the API
|
||||||
|
'''
|
||||||
|
assert len(responseID) > 0
|
||||||
|
resp = self._utils.localCommand('queueResponse/' + responseID)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def daemonQueueWaitForResponse(self, responseID='', checkFreqSecs=1):
|
||||||
|
resp = 'failure'
|
||||||
|
while resp == 'failure':
|
||||||
|
resp = self.daemonQueueGetResponse(responseID)
|
||||||
|
time.sleep(1)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def daemonQueueSimple(self, command, data='', checkFreqSecs=1):
|
||||||
|
'''
|
||||||
|
A simplified way to use the daemon queue. Will register a command (with optional data) and wait, return the data
|
||||||
|
Not always useful, but saves time + LOC in some cases.
|
||||||
|
This is a blocking function, so be careful.
|
||||||
|
'''
|
||||||
|
responseID = str(uuid.uuid4()) # generate unique response ID
|
||||||
|
self.daemonQueueAdd(command, data=data, responseID=responseID)
|
||||||
|
return self.daemonQueueWaitForResponse(responseID, checkFreqSecs)
|
||||||
|
|
||||||
def clearDaemonQueue(self):
|
def clearDaemonQueue(self):
|
||||||
'''
|
'''
|
||||||
Clear the daemon queue (somewhat dangerous)
|
Clear the daemon queue (somewhat dangerous)
|
||||||
'''
|
'''
|
||||||
conn = sqlite3.connect(self.queueDB, timeout=10)
|
conn = sqlite3.connect(self.queueDB, timeout=30)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -393,11 +426,11 @@ class Core:
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def listAdders(self, randomOrder=True, i2p=True):
|
def listAdders(self, randomOrder=True, i2p=True, recent=0):
|
||||||
'''
|
'''
|
||||||
Return a list of addresses
|
Return a list of addresses
|
||||||
'''
|
'''
|
||||||
conn = sqlite3.connect(self.addressDB, timeout=10)
|
conn = sqlite3.connect(self.addressDB, timeout=30)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
if randomOrder:
|
if randomOrder:
|
||||||
addresses = c.execute('SELECT * FROM adders ORDER BY RANDOM();')
|
addresses = c.execute('SELECT * FROM adders ORDER BY RANDOM();')
|
||||||
@ -405,8 +438,17 @@ class Core:
|
|||||||
addresses = c.execute('SELECT * FROM adders;')
|
addresses = c.execute('SELECT * FROM adders;')
|
||||||
addressList = []
|
addressList = []
|
||||||
for i in addresses:
|
for i in addresses:
|
||||||
|
if len(i[0].strip()) == 0:
|
||||||
|
continue
|
||||||
addressList.append(i[0])
|
addressList.append(i[0])
|
||||||
conn.close()
|
conn.close()
|
||||||
|
testList = list(addressList) # create new list to iterate
|
||||||
|
for address in testList:
|
||||||
|
try:
|
||||||
|
if recent > 0 and (self._utils.getEpoch() - self.getAddressInfo(address, 'lastConnect')) > recent:
|
||||||
|
raise TypeError # If there is no last-connected date or it was too long ago, don't add peer to list if recent is not 0
|
||||||
|
except TypeError:
|
||||||
|
addressList.remove(address)
|
||||||
return addressList
|
return addressList
|
||||||
|
|
||||||
def listPeers(self, randomOrder=True, getPow=False, trust=0):
|
def listPeers(self, randomOrder=True, getPow=False, trust=0):
|
||||||
@ -416,7 +458,7 @@ class Core:
|
|||||||
randomOrder determines if the list should be in a random order
|
randomOrder determines if the list should be in a random order
|
||||||
trust sets the minimum trust to list
|
trust sets the minimum trust to list
|
||||||
'''
|
'''
|
||||||
conn = sqlite3.connect(self.peerDB, timeout=10)
|
conn = sqlite3.connect(self.peerDB, timeout=30)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
|
||||||
payload = ''
|
payload = ''
|
||||||
@ -465,7 +507,7 @@ class Core:
|
|||||||
trust int 4
|
trust int 4
|
||||||
hashID text 5
|
hashID text 5
|
||||||
'''
|
'''
|
||||||
conn = sqlite3.connect(self.peerDB, timeout=10)
|
conn = sqlite3.connect(self.peerDB, timeout=30)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
|
||||||
command = (peer,)
|
command = (peer,)
|
||||||
@ -491,7 +533,7 @@ class Core:
|
|||||||
Update a peer for a key
|
Update a peer for a key
|
||||||
'''
|
'''
|
||||||
|
|
||||||
conn = sqlite3.connect(self.peerDB, timeout=10)
|
conn = sqlite3.connect(self.peerDB, timeout=30)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
|
||||||
command = (data, peer)
|
command = (data, peer)
|
||||||
@ -523,7 +565,7 @@ class Core:
|
|||||||
introduced 10
|
introduced 10
|
||||||
'''
|
'''
|
||||||
|
|
||||||
conn = sqlite3.connect(self.addressDB, timeout=10)
|
conn = sqlite3.connect(self.addressDB, timeout=30)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
|
||||||
command = (address,)
|
command = (address,)
|
||||||
@ -548,7 +590,7 @@ class Core:
|
|||||||
Update an address for a key
|
Update an address for a key
|
||||||
'''
|
'''
|
||||||
|
|
||||||
conn = sqlite3.connect(self.addressDB, timeout=10)
|
conn = sqlite3.connect(self.addressDB, timeout=30)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
|
||||||
command = (data, address)
|
command = (data, address)
|
||||||
@ -562,24 +604,27 @@ class Core:
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def getBlockList(self, unsaved = False): # TODO: Use unsaved??
|
def getBlockList(self, dateRec = None, unsaved = False):
|
||||||
'''
|
'''
|
||||||
Get list of our blocks
|
Get list of our blocks
|
||||||
'''
|
'''
|
||||||
|
if dateRec == None:
|
||||||
|
dateRec = 0
|
||||||
|
|
||||||
conn = sqlite3.connect(self.blockDB, timeout=10)
|
conn = sqlite3.connect(self.blockDB, timeout=30)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
|
||||||
if unsaved:
|
# if unsaved:
|
||||||
execute = 'SELECT hash FROM hashes WHERE dataSaved != 1 ORDER BY RANDOM();'
|
# execute = 'SELECT hash FROM hashes WHERE dataSaved != 1 ORDER BY RANDOM();'
|
||||||
else:
|
# else:
|
||||||
execute = 'SELECT hash FROM hashes ORDER BY dateReceived ASC;'
|
# execute = 'SELECT hash FROM hashes ORDER BY dateReceived ASC;'
|
||||||
|
execute = 'SELECT hash FROM hashes WHERE dateReceived >= ? ORDER BY dateReceived ASC;'
|
||||||
|
args = (dateRec,)
|
||||||
rows = list()
|
rows = list()
|
||||||
for row in c.execute(execute):
|
for row in c.execute(execute, args):
|
||||||
for i in row:
|
for i in row:
|
||||||
rows.append(i)
|
rows.append(i)
|
||||||
|
conn.close()
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
def getBlockDate(self, blockHash):
|
def getBlockDate(self, blockHash):
|
||||||
@ -587,7 +632,7 @@ class Core:
|
|||||||
Returns the date a block was received
|
Returns the date a block was received
|
||||||
'''
|
'''
|
||||||
|
|
||||||
conn = sqlite3.connect(self.blockDB, timeout=10)
|
conn = sqlite3.connect(self.blockDB, timeout=30)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
|
||||||
execute = 'SELECT dateReceived FROM hashes WHERE hash=?;'
|
execute = 'SELECT dateReceived FROM hashes WHERE hash=?;'
|
||||||
@ -595,7 +640,7 @@ class Core:
|
|||||||
for row in c.execute(execute, args):
|
for row in c.execute(execute, args):
|
||||||
for i in row:
|
for i in row:
|
||||||
return int(i)
|
return int(i)
|
||||||
|
conn.close()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def getBlocksByType(self, blockType, orderDate=True):
|
def getBlocksByType(self, blockType, orderDate=True):
|
||||||
@ -603,7 +648,7 @@ class Core:
|
|||||||
Returns a list of blocks by the type
|
Returns a list of blocks by the type
|
||||||
'''
|
'''
|
||||||
|
|
||||||
conn = sqlite3.connect(self.blockDB, timeout=10)
|
conn = sqlite3.connect(self.blockDB, timeout=30)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
|
||||||
if orderDate:
|
if orderDate:
|
||||||
@ -617,12 +662,12 @@ class Core:
|
|||||||
for row in c.execute(execute, args):
|
for row in c.execute(execute, args):
|
||||||
for i in row:
|
for i in row:
|
||||||
rows.append(i)
|
rows.append(i)
|
||||||
|
conn.close()
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
def getExpiredBlocks(self):
|
def getExpiredBlocks(self):
|
||||||
'''Returns a list of expired blocks'''
|
'''Returns a list of expired blocks'''
|
||||||
conn = sqlite3.connect(self.blockDB, timeout=10)
|
conn = sqlite3.connect(self.blockDB, timeout=30)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
date = int(self._utils.getEpoch())
|
date = int(self._utils.getEpoch())
|
||||||
|
|
||||||
@ -632,6 +677,7 @@ class Core:
|
|||||||
for row in c.execute(execute):
|
for row in c.execute(execute):
|
||||||
for i in row:
|
for i in row:
|
||||||
rows.append(i)
|
rows.append(i)
|
||||||
|
conn.close()
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
def setBlockType(self, hash, blockType):
|
def setBlockType(self, hash, blockType):
|
||||||
@ -639,7 +685,7 @@ class Core:
|
|||||||
Sets the type of block
|
Sets the type of block
|
||||||
'''
|
'''
|
||||||
|
|
||||||
conn = sqlite3.connect(self.blockDB, timeout=10)
|
conn = sqlite3.connect(self.blockDB, timeout=30)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute("UPDATE hashes SET dataType = ? WHERE hash = ?;", (blockType, hash))
|
c.execute("UPDATE hashes SET dataType = ? WHERE hash = ?;", (blockType, hash))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@ -666,7 +712,7 @@ class Core:
|
|||||||
if key not in ('dateReceived', 'decrypted', 'dataType', 'dataFound', 'dataSaved', 'sig', 'author', 'dateClaimed', 'expire'):
|
if key not in ('dateReceived', 'decrypted', 'dataType', 'dataFound', 'dataSaved', 'sig', 'author', 'dateClaimed', 'expire'):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
conn = sqlite3.connect(self.blockDB, timeout=10)
|
conn = sqlite3.connect(self.blockDB, timeout=30)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
args = (data, hash)
|
args = (data, hash)
|
||||||
c.execute("UPDATE hashes SET " + key + " = ? where hash = ?;", args)
|
c.execute("UPDATE hashes SET " + key + " = ? where hash = ?;", args)
|
||||||
@ -675,12 +721,15 @@ class Core:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def insertBlock(self, data, header='txt', sign=False, encryptType='', symKey='', asymPeer='', meta = {}, expire=None):
|
def insertBlock(self, data, header='txt', sign=False, encryptType='', symKey='', asymPeer='', meta = {}, expire=None, disableForward=False):
|
||||||
'''
|
'''
|
||||||
Inserts a block into the network
|
Inserts a block into the network
|
||||||
encryptType must be specified to encrypt a block
|
encryptType must be specified to encrypt a block
|
||||||
'''
|
'''
|
||||||
|
allocationReachedMessage = 'Cannot insert block, disk allocation reached.'
|
||||||
|
if self._utils.storageCounter.isFull():
|
||||||
|
logger.error(allocationReachedMessage)
|
||||||
|
return False
|
||||||
retData = False
|
retData = False
|
||||||
# check nonce
|
# check nonce
|
||||||
dataNonce = self._utils.bytesToStr(self._crypto.sha3Hash(data))
|
dataNonce = self._utils.bytesToStr(self._crypto.sha3Hash(data))
|
||||||
@ -719,15 +768,17 @@ class Core:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
if encryptType == 'asym':
|
if encryptType == 'asym':
|
||||||
|
if not disableForward and asymPeer != self._crypto.pubKey:
|
||||||
try:
|
try:
|
||||||
forwardEncrypted = onionrusers.OnionrUser(self, asymPeer).forwardEncrypt(data)
|
forwardEncrypted = onionrusers.OnionrUser(self, asymPeer).forwardEncrypt(data)
|
||||||
data = forwardEncrypted[0]
|
data = forwardEncrypted[0]
|
||||||
meta['forwardEnc'] = True
|
meta['forwardEnc'] = True
|
||||||
except onionrexceptions.InvalidPubkey:
|
except onionrexceptions.InvalidPubkey:
|
||||||
onionrusers.OnionrUser(self, asymPeer).generateForwardKey()
|
pass
|
||||||
onionrusers.OnionrUser(self, asymPeer).generateForwardKey()
|
#onionrusers.OnionrUser(self, asymPeer).generateForwardKey()
|
||||||
fsKey = onionrusers.OnionrUser(self, asymPeer).getGeneratedForwardKeys()[0]
|
fsKey = onionrusers.OnionrUser(self, asymPeer).generateForwardKey()
|
||||||
meta['newFSKey'] = fsKey[0]
|
#fsKey = onionrusers.OnionrUser(self, asymPeer).getGeneratedForwardKeys().reverse()
|
||||||
|
meta['newFSKey'] = fsKey
|
||||||
jsonMeta = json.dumps(meta)
|
jsonMeta = json.dumps(meta)
|
||||||
if sign:
|
if sign:
|
||||||
signature = self._crypto.edSign(jsonMeta.encode() + data, key=self._crypto.privKey, encodeResult=True)
|
signature = self._crypto.edSign(jsonMeta.encode() + data, key=self._crypto.privKey, encodeResult=True)
|
||||||
@ -774,7 +825,12 @@ class Core:
|
|||||||
proof = onionrproofs.POW(metadata, data)
|
proof = onionrproofs.POW(metadata, data)
|
||||||
payload = proof.waitForResult()
|
payload = proof.waitForResult()
|
||||||
if payload != False:
|
if payload != False:
|
||||||
|
try:
|
||||||
retData = self.setData(payload)
|
retData = self.setData(payload)
|
||||||
|
except onionrexceptions.DiskAllocationReached:
|
||||||
|
logger.error(allocationReachedMessage)
|
||||||
|
retData = False
|
||||||
|
else:
|
||||||
# Tell the api server through localCommand to wait for the daemon to upload this block to make stastical analysis more difficult
|
# Tell the api server through localCommand to wait for the daemon to upload this block to make stastical analysis more difficult
|
||||||
self._utils.localCommand('waitforshare/' + retData)
|
self._utils.localCommand('waitforshare/' + retData)
|
||||||
self.addToBlockDB(retData, selfInsert=True, dataSaved=True)
|
self.addToBlockDB(retData, selfInsert=True, dataSaved=True)
|
||||||
@ -813,5 +869,4 @@ class Core:
|
|||||||
else:
|
else:
|
||||||
logger.error('Onionr daemon is not running.')
|
logger.error('Onionr daemon is not running.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -92,7 +92,7 @@ class DBCreator:
|
|||||||
expire int - block expire date in epoch
|
expire int - block expire date in epoch
|
||||||
'''
|
'''
|
||||||
if os.path.exists(self.core.blockDB):
|
if os.path.exists(self.core.blockDB):
|
||||||
raise Exception("Block database already exists")
|
raise FileExistsError("Block database already exists")
|
||||||
conn = sqlite3.connect(self.core.blockDB)
|
conn = sqlite3.connect(self.core.blockDB)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute('''CREATE TABLE hashes(
|
c.execute('''CREATE TABLE hashes(
|
||||||
@ -112,12 +112,25 @@ class DBCreator:
|
|||||||
conn.close()
|
conn.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def createBlockDataDB(self):
|
||||||
|
if os.path.exists(self.core.blockDataDB):
|
||||||
|
raise FileExistsError("Block data database already exists")
|
||||||
|
conn = sqlite3.connect(self.core.blockDataDB)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('''CREATE TABLE blockData(
|
||||||
|
hash text not null,
|
||||||
|
data blob not null
|
||||||
|
);
|
||||||
|
''')
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
def createForwardKeyDB(self):
|
def createForwardKeyDB(self):
|
||||||
'''
|
'''
|
||||||
Create the forward secrecy key db (*for *OUR* keys*)
|
Create the forward secrecy key db (*for *OUR* keys*)
|
||||||
'''
|
'''
|
||||||
if os.path.exists(self.core.forwardKeysFile):
|
if os.path.exists(self.core.forwardKeysFile):
|
||||||
raise Exception("Block database already exists")
|
raise FileExistsError("Block database already exists")
|
||||||
conn = sqlite3.connect(self.core.forwardKeysFile)
|
conn = sqlite3.connect(self.core.forwardKeysFile)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute('''CREATE TABLE myForwardKeys(
|
c.execute('''CREATE TABLE myForwardKeys(
|
||||||
@ -139,7 +152,6 @@ class DBCreator:
|
|||||||
conn = sqlite3.connect(self.core.queueDB, timeout=10)
|
conn = sqlite3.connect(self.core.queueDB, timeout=10)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
# Create table
|
# Create table
|
||||||
c.execute('''CREATE TABLE commands
|
c.execute('''CREATE TABLE commands (id integer primary key autoincrement, command text, data text, date text, responseID text)''')
|
||||||
(id integer primary key autoincrement, command text, data text, date text)''')
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
@ -132,8 +132,11 @@ def raw(data, fd = sys.stdout, sensitive = False):
|
|||||||
if get_settings() & OUTPUT_TO_CONSOLE:
|
if get_settings() & OUTPUT_TO_CONSOLE:
|
||||||
ts = fd.write('%s\n' % data)
|
ts = fd.write('%s\n' % data)
|
||||||
if get_settings() & OUTPUT_TO_FILE and not sensitive:
|
if get_settings() & OUTPUT_TO_FILE and not sensitive:
|
||||||
|
try:
|
||||||
with open(_outputfile, "a+") as f:
|
with open(_outputfile, "a+") as f:
|
||||||
f.write(colors.filter(data) + '\n')
|
f.write(colors.filter(data) + '\n')
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
def log(prefix, data, color = '', timestamp=True, fd = sys.stdout, prompt = True, sensitive = False):
|
def log(prefix, data, color = '', timestamp=True, fd = sys.stdout, prompt = True, sensitive = False):
|
||||||
'''
|
'''
|
||||||
|
@ -22,6 +22,7 @@ import subprocess, os, random, sys, logger, time, signal, config, base64, socket
|
|||||||
from stem.control import Controller
|
from stem.control import Controller
|
||||||
from onionrblockapi import Block
|
from onionrblockapi import Block
|
||||||
from dependencies import secrets
|
from dependencies import secrets
|
||||||
|
from shutil import which
|
||||||
|
|
||||||
def getOpenPort():
|
def getOpenPort():
|
||||||
# taken from (but modified) https://stackoverflow.com/a/2838309
|
# taken from (but modified) https://stackoverflow.com/a/2838309
|
||||||
@ -31,6 +32,14 @@ def getOpenPort():
|
|||||||
port = s.getsockname()[1]
|
port = s.getsockname()[1]
|
||||||
s.close()
|
s.close()
|
||||||
return port
|
return port
|
||||||
|
|
||||||
|
def torBinary():
|
||||||
|
'''Return tor binary path or none if not exists'''
|
||||||
|
torPath = './tor'
|
||||||
|
if not os.path.exists(torPath):
|
||||||
|
torPath = which('tor')
|
||||||
|
return torPath
|
||||||
|
|
||||||
class NetController:
|
class NetController:
|
||||||
'''
|
'''
|
||||||
This class handles hidden service setup on Tor and I2P
|
This class handles hidden service setup on Tor and I2P
|
||||||
|
179
onionr/onionr.py
179
onionr/onionr.py
@ -21,18 +21,19 @@
|
|||||||
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
|
import sys
|
||||||
if sys.version_info[0] == 2 or sys.version_info[1] < 5:
|
MIN_PY_VERSION = 6
|
||||||
print('Error, Onionr requires Python 3.5+')
|
if sys.version_info[0] == 2 or sys.version_info[1] < MIN_PY_VERSION:
|
||||||
|
print('Error, Onionr requires Python 3.%s+' % (MIN_PY_VERSION,))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
import os, base64, random, getpass, shutil, subprocess, requests, time, platform, datetime, re, json, getpass, sqlite3
|
import os, base64, random, getpass, shutil, subprocess, requests, time, platform, datetime, re, json, getpass, sqlite3
|
||||||
import webbrowser
|
import webbrowser, uuid, signal
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
import api, core, config, logger, onionrplugins as plugins, onionrevents as events
|
import api, core, config, logger, onionrplugins as plugins, onionrevents as events
|
||||||
import onionrutils
|
import onionrutils
|
||||||
import netcontroller
|
import netcontroller, onionrstorage
|
||||||
from netcontroller import NetController
|
from netcontroller import NetController
|
||||||
from onionrblockapi import Block
|
from onionrblockapi import Block
|
||||||
import onionrproofs, onionrexceptions, onionrusers
|
import onionrproofs, onionrexceptions, onionrusers, communicator
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from urllib3.contrib.socks import SOCKSProxyManager
|
from urllib3.contrib.socks import SOCKSProxyManager
|
||||||
@ -51,6 +52,7 @@ class Onionr:
|
|||||||
In general, external programs and plugins should not use this class.
|
In general, external programs and plugins should not use this class.
|
||||||
'''
|
'''
|
||||||
self.userRunDir = os.getcwd() # Directory user runs the program from
|
self.userRunDir = os.getcwd() # Directory user runs the program from
|
||||||
|
self.killed = False
|
||||||
try:
|
try:
|
||||||
os.chdir(sys.path[0])
|
os.chdir(sys.path[0])
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@ -66,13 +68,21 @@ class Onionr:
|
|||||||
# Load global configuration data
|
# Load global configuration data
|
||||||
data_exists = Onionr.setupConfig(self.dataDir, self = self)
|
data_exists = Onionr.setupConfig(self.dataDir, self = self)
|
||||||
|
|
||||||
|
if netcontroller.torBinary() is None:
|
||||||
|
logger.error('Tor is not installed')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
self.communicatorInst = None
|
||||||
self.onionrCore = core.Core()
|
self.onionrCore = core.Core()
|
||||||
|
self.onionrCore.onionrInst = self
|
||||||
#self.deleteRunFiles()
|
#self.deleteRunFiles()
|
||||||
self.onionrUtils = onionrutils.OnionrUtils(self.onionrCore)
|
self.onionrUtils = onionrutils.OnionrUtils(self.onionrCore)
|
||||||
|
|
||||||
self.clientAPIInst = '' # Client http api instance
|
self.clientAPIInst = '' # Client http api instance
|
||||||
self.publicAPIInst = '' # Public http api instance
|
self.publicAPIInst = '' # Public http api instance
|
||||||
|
|
||||||
|
signal.signal(signal.SIGTERM, self.exitSigterm)
|
||||||
|
|
||||||
# Handle commands
|
# Handle commands
|
||||||
|
|
||||||
self.debug = False # Whole application debugging
|
self.debug = False # Whole application debugging
|
||||||
@ -177,6 +187,12 @@ class Onionr:
|
|||||||
'add-site': self.addWebpage,
|
'add-site': self.addWebpage,
|
||||||
'addsite': self.addWebpage,
|
'addsite': self.addWebpage,
|
||||||
|
|
||||||
|
'openhome': self.openHome,
|
||||||
|
'open-home': self.openHome,
|
||||||
|
|
||||||
|
'export-block': self.exportBlock,
|
||||||
|
'exportblock': self.exportBlock,
|
||||||
|
|
||||||
'get-file': self.getFile,
|
'get-file': self.getFile,
|
||||||
'getfile': self.getFile,
|
'getfile': self.getFile,
|
||||||
|
|
||||||
@ -192,7 +208,6 @@ class Onionr:
|
|||||||
|
|
||||||
'ui' : self.openUI,
|
'ui' : self.openUI,
|
||||||
'gui' : self.openUI,
|
'gui' : self.openUI,
|
||||||
'chat': self.startChat,
|
|
||||||
|
|
||||||
'getpassword': self.printWebPassword,
|
'getpassword': self.printWebPassword,
|
||||||
'get-password': self.printWebPassword,
|
'get-password': self.printWebPassword,
|
||||||
@ -203,8 +218,6 @@ class Onionr:
|
|||||||
'getpasswd': self.printWebPassword,
|
'getpasswd': self.printWebPassword,
|
||||||
'get-passwd': self.printWebPassword,
|
'get-passwd': self.printWebPassword,
|
||||||
|
|
||||||
'chat': self.startChat,
|
|
||||||
|
|
||||||
'friend': self.friendCmd,
|
'friend': self.friendCmd,
|
||||||
'add-id': self.addID,
|
'add-id': self.addID,
|
||||||
'change-id': self.changeID
|
'change-id': self.changeID
|
||||||
@ -237,7 +250,8 @@ class Onionr:
|
|||||||
'introduce': 'Introduce your node to the public Onionr network',
|
'introduce': 'Introduce your node to the public Onionr network',
|
||||||
'friend': '[add|remove] [public key/id]',
|
'friend': '[add|remove] [public key/id]',
|
||||||
'add-id': 'Generate a new ID (key pair)',
|
'add-id': 'Generate a new ID (key pair)',
|
||||||
'change-id': 'Change active ID'
|
'change-id': 'Change active ID',
|
||||||
|
'open-home': 'Open your node\'s home/info screen'
|
||||||
}
|
}
|
||||||
|
|
||||||
# initialize plugins
|
# initialize plugins
|
||||||
@ -253,10 +267,38 @@ class Onionr:
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def exitSigterm(self, signum, frame):
|
||||||
|
self.killed = True
|
||||||
|
|
||||||
'''
|
'''
|
||||||
THIS SECTION HANDLES THE COMMANDS
|
THIS SECTION HANDLES THE COMMANDS
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
def exportBlock(self):
|
||||||
|
exportDir = self.dataDir + 'block-export/'
|
||||||
|
try:
|
||||||
|
assert self.onionrUtils.validateHash(sys.argv[2])
|
||||||
|
except (IndexError, AssertionError):
|
||||||
|
logger.error('No valid block hash specified.')
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
bHash = sys.argv[2]
|
||||||
|
try:
|
||||||
|
path = sys.argv[3]
|
||||||
|
except (IndexError):
|
||||||
|
if not os.path.exists(exportDir):
|
||||||
|
if os.path.exists(self.dataDir):
|
||||||
|
os.mkdir(exportDir)
|
||||||
|
else:
|
||||||
|
logger.error('Onionr not initialized')
|
||||||
|
sys.exit(1)
|
||||||
|
path = exportDir
|
||||||
|
data = onionrstorage.getData(self.onionrCore, bHash)
|
||||||
|
with open('%s/%s.dat' % (exportDir, bHash), 'wb') as exportFile:
|
||||||
|
exportFile.write(data)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def showDetails(self):
|
def showDetails(self):
|
||||||
details = {
|
details = {
|
||||||
'Node Address' : self.get_hostname(),
|
'Node Address' : self.get_hostname(),
|
||||||
@ -268,6 +310,14 @@ class Onionr:
|
|||||||
for detail in details:
|
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)
|
logger.info('%s%s: \n%s%s\n' % (logger.colors.fg.lightgreen, detail, logger.colors.fg.green, details[detail]), sensitive = True)
|
||||||
|
|
||||||
|
def openHome(self):
|
||||||
|
try:
|
||||||
|
url = self.onionrUtils.getClientAPIServer()
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.error('Onionr seems to not be running (could not get api host)')
|
||||||
|
else:
|
||||||
|
webbrowser.open_new_tab('http://%s/#%s' % (url, config.get('client.webpassword')))
|
||||||
|
|
||||||
def addID(self):
|
def addID(self):
|
||||||
try:
|
try:
|
||||||
sys.argv[2]
|
sys.argv[2]
|
||||||
@ -276,7 +326,8 @@ class Onionr:
|
|||||||
newID = self.onionrCore._crypto.keyManager.addKey()[0]
|
newID = self.onionrCore._crypto.keyManager.addKey()[0]
|
||||||
else:
|
else:
|
||||||
logger.warn('Deterministic keys require random and long passphrases.')
|
logger.warn('Deterministic keys require random and long passphrases.')
|
||||||
logger.warn('If a good password is not used, your key can be easily stolen.')
|
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: ' % (self.onionrCore._crypto.deterministicRequirement,))
|
pass1 = getpass.getpass(prompt='Enter at least %s characters: ' % (self.onionrCore._crypto.deterministicRequirement,))
|
||||||
pass2 = getpass.getpass(prompt='Confirm entry: ')
|
pass2 = getpass.getpass(prompt='Confirm entry: ')
|
||||||
if self.onionrCore._crypto.safeCompare(pass1, pass2):
|
if self.onionrCore._crypto.safeCompare(pass1, pass2):
|
||||||
@ -310,14 +361,6 @@ class Onionr:
|
|||||||
else:
|
else:
|
||||||
logger.error('Invalid key %s' % (key,))
|
logger.error('Invalid key %s' % (key,))
|
||||||
|
|
||||||
def startChat(self):
|
|
||||||
try:
|
|
||||||
data = json.dumps({'peer': sys.argv[2], 'reason': 'chat'})
|
|
||||||
except IndexError:
|
|
||||||
logger.error('Must specify peer to chat with.')
|
|
||||||
else:
|
|
||||||
self.onionrCore.daemonQueueAdd('startSocket', data)
|
|
||||||
|
|
||||||
def getCommands(self):
|
def getCommands(self):
|
||||||
return self.cmds
|
return self.cmds
|
||||||
|
|
||||||
@ -351,46 +394,9 @@ class Onionr:
|
|||||||
except IndexError:
|
except IndexError:
|
||||||
logger.error('Friend ID is required.')
|
logger.error('Friend ID is required.')
|
||||||
except onionrexceptions.KeyNotKnown:
|
except onionrexceptions.KeyNotKnown:
|
||||||
logger.error('That peer is not in our database')
|
self.onionrCore.addPeer(friend)
|
||||||
else:
|
|
||||||
if action == 'add':
|
|
||||||
friend.setTrust(1)
|
|
||||||
logger.info('Added %s as friend.' % (friend.publicKey,))
|
|
||||||
else:
|
|
||||||
friend.setTrust(0)
|
|
||||||
logger.info('Removed %s as friend.' % (friend.publicKey,))
|
|
||||||
else:
|
|
||||||
logger.info('Syntax: friend add/remove/list [address]')
|
|
||||||
|
|
||||||
|
|
||||||
def friendCmd(self):
|
|
||||||
'''List, add, or remove friend(s)
|
|
||||||
Changes their peer DB entry.
|
|
||||||
'''
|
|
||||||
friend = ''
|
|
||||||
try:
|
|
||||||
# Get the friend command
|
|
||||||
action = sys.argv[2]
|
|
||||||
except IndexError:
|
|
||||||
logger.info('Syntax: friend add/remove/list [address]')
|
|
||||||
else:
|
|
||||||
action = action.lower()
|
|
||||||
if action == 'list':
|
|
||||||
# List out peers marked as our friend
|
|
||||||
for friend in self.onionrCore.listPeers(randomOrder=False, trust=1):
|
|
||||||
if friend == self.onionrCore._crypto.pubKey: # do not list our key
|
|
||||||
continue
|
|
||||||
friendProfile = onionrusers.OnionrUser(self.onionrCore, friend)
|
|
||||||
logger.info(friend + ' - ' + friendProfile.getName())
|
|
||||||
elif action in ('add', 'remove'):
|
|
||||||
try:
|
|
||||||
friend = sys.argv[3]
|
|
||||||
if not self.onionrUtils.validatePubKey(friend):
|
|
||||||
raise onionrexceptions.InvalidPubkey('Public key is invalid')
|
|
||||||
friend = onionrusers.OnionrUser(self.onionrCore, friend)
|
friend = onionrusers.OnionrUser(self.onionrCore, friend)
|
||||||
except IndexError:
|
finally:
|
||||||
logger.error('Friend ID is required.')
|
|
||||||
else:
|
|
||||||
if action == 'add':
|
if action == 'add':
|
||||||
friend.setTrust(1)
|
friend.setTrust(1)
|
||||||
logger.info('Added %s as friend.' % (friend.publicKey,))
|
logger.info('Added %s as friend.' % (friend.publicKey,))
|
||||||
@ -400,6 +406,15 @@ class Onionr:
|
|||||||
else:
|
else:
|
||||||
logger.info('Syntax: friend add/remove/list [address]')
|
logger.info('Syntax: friend add/remove/list [address]')
|
||||||
|
|
||||||
|
def deleteRunFiles(self):
|
||||||
|
try:
|
||||||
|
os.remove(self.onionrCore.publicApiHostFile)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
os.remove(self.onionrCore.privateApiHostFile)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
def deleteRunFiles(self):
|
def deleteRunFiles(self):
|
||||||
try:
|
try:
|
||||||
@ -432,7 +447,21 @@ class Onionr:
|
|||||||
return
|
return
|
||||||
|
|
||||||
def listConn(self):
|
def listConn(self):
|
||||||
self.onionrCore.daemonQueueAdd('connectedPeers')
|
randID = str(uuid.uuid4())
|
||||||
|
self.onionrCore.daemonQueueAdd('connectedPeers', responseID=randID)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
time.sleep(3)
|
||||||
|
peers = self.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
|
||||||
|
|
||||||
def listPeers(self):
|
def listPeers(self):
|
||||||
logger.info('Peer transport address list:')
|
logger.info('Peer transport address list:')
|
||||||
@ -720,8 +749,6 @@ class Onionr:
|
|||||||
Starts the Onionr communication daemon
|
Starts the Onionr communication daemon
|
||||||
'''
|
'''
|
||||||
|
|
||||||
communicatorDaemon = './communicator2.py'
|
|
||||||
|
|
||||||
# remove runcheck if it exists
|
# remove runcheck if it exists
|
||||||
if os.path.isfile('data/.runcheck'):
|
if os.path.isfile('data/.runcheck'):
|
||||||
logger.debug('Runcheck file found on daemon start, deleting in advance.')
|
logger.debug('Runcheck file found on daemon start, deleting in advance.')
|
||||||
@ -760,8 +787,12 @@ class Onionr:
|
|||||||
logger.debug('Using public key: %s' % (logger.colors.underline + self.onionrCore._crypto.pubKey))
|
logger.debug('Using public key: %s' % (logger.colors.underline + self.onionrCore._crypto.pubKey))
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
# TODO: make runable on windows
|
self.onionrCore.torPort = net.socksPort
|
||||||
communicatorProc = subprocess.Popen([communicatorDaemon, 'run', str(net.socksPort)])
|
communicatorThread = Thread(target=communicator.startCommunicator, args=(self, str(net.socksPort)))
|
||||||
|
communicatorThread.start()
|
||||||
|
|
||||||
|
while self.communicatorInst is None:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
# print nice header thing :)
|
# print nice header thing :)
|
||||||
if config.get('general.display_header', True):
|
if config.get('general.display_header', True):
|
||||||
@ -776,17 +807,23 @@ class Onionr:
|
|||||||
events.event('daemon_start', onionr = self)
|
events.event('daemon_start', onionr = self)
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
time.sleep(5)
|
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
|
# Break if communicator process ends, so we don't have left over processes
|
||||||
if communicatorProc.poll() is not None:
|
if self.communicatorInst.shutdown:
|
||||||
break
|
break
|
||||||
|
if self.killed:
|
||||||
|
break # Break out if sigterm for clean exit
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
self.onionrCore.daemonQueueAdd('shutdown')
|
self.onionrCore.daemonQueueAdd('shutdown')
|
||||||
self.onionrUtils.localCommand('shutdown')
|
self.onionrUtils.localCommand('shutdown')
|
||||||
|
net.killTor()
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
self.deleteRunFiles()
|
self.deleteRunFiles()
|
||||||
net.killTor()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def killDaemon(self):
|
def killDaemon(self):
|
||||||
@ -815,7 +852,7 @@ class Onionr:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# define stats messages here
|
# define stats messages here
|
||||||
totalBlocks = len(Block.getBlocks())
|
totalBlocks = len(self.onionrCore.getBlockList())
|
||||||
signedBlocks = len(Block.getBlocks(signed = True))
|
signedBlocks = len(Block.getBlocks(signed = True))
|
||||||
messages = {
|
messages = {
|
||||||
# info about local client
|
# info about local client
|
||||||
@ -938,7 +975,8 @@ class Onionr:
|
|||||||
logger.error('Block hash is invalid')
|
logger.error('Block hash is invalid')
|
||||||
return
|
return
|
||||||
|
|
||||||
Block.mergeChain(bHash, fileName)
|
with open(fileName, 'wb') as myFile:
|
||||||
|
myFile.write(base64.b64decode(Block(bHash, core=self.onionrCore).bcontent))
|
||||||
return
|
return
|
||||||
|
|
||||||
def addWebpage(self):
|
def addWebpage(self):
|
||||||
@ -961,12 +999,9 @@ class Onionr:
|
|||||||
return
|
return
|
||||||
logger.info('Adding file... this might take a long time.')
|
logger.info('Adding file... this might take a long time.')
|
||||||
try:
|
try:
|
||||||
if singleBlock:
|
|
||||||
with open(filename, 'rb') as singleFile:
|
with open(filename, 'rb') as singleFile:
|
||||||
blockhash = self.onionrCore.insertBlock(base64.b64encode(singleFile.read()), header=blockType)
|
blockhash = self.onionrCore.insertBlock(base64.b64encode(singleFile.read()), header=blockType)
|
||||||
else:
|
logger.info('File %s saved in block %s' % (filename, blockhash))
|
||||||
blockhash = Block.createChain(file = filename)
|
|
||||||
logger.info('File %s saved in block %s.' % (filename, blockhash))
|
|
||||||
except:
|
except:
|
||||||
logger.error('Failed to save file in block.', timestamp = False)
|
logger.error('Failed to save file in block.', timestamp = False)
|
||||||
else:
|
else:
|
||||||
|
@ -82,7 +82,7 @@ class OnionrBlackList:
|
|||||||
return
|
return
|
||||||
|
|
||||||
def clearDB(self):
|
def clearDB(self):
|
||||||
self._dbExecute('''DELETE FROM blacklist;);''')
|
self._dbExecute('''DELETE FROM blacklist;''')
|
||||||
|
|
||||||
def getList(self):
|
def getList(self):
|
||||||
data = self._dbExecute('SELECT * FROM blacklist')
|
data = self._dbExecute('SELECT * FROM blacklist')
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
'''
|
'''
|
||||||
Onionr - P2P Anonymous Storage Network
|
Onionr - P2P Anonymous Storage Network
|
||||||
|
|
||||||
This class contains the OnionrBlocks class which is a class for working with Onionr blocks
|
This file contains the OnionrBlocks class which is a class for working with Onionr blocks
|
||||||
'''
|
'''
|
||||||
'''
|
'''
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
@ -19,13 +19,13 @@
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
import core as onionrcore, logger, config, onionrexceptions, nacl.exceptions, onionrusers
|
import core as onionrcore, logger, config, onionrexceptions, nacl.exceptions, onionrusers
|
||||||
import json, os, sys, datetime, base64
|
import json, os, sys, datetime, base64, onionrstorage
|
||||||
|
|
||||||
class Block:
|
class Block:
|
||||||
blockCacheOrder = list() # NEVER write your own code that writes to this!
|
blockCacheOrder = list() # NEVER write your own code that writes to this!
|
||||||
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):
|
def __init__(self, hash = None, core = None, type = None, content = None, expire=None, decrypt=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):
|
||||||
@ -51,23 +51,13 @@ class Block:
|
|||||||
self.decrypted = False
|
self.decrypted = False
|
||||||
self.signer = None
|
self.signer = None
|
||||||
self.validSig = False
|
self.validSig = False
|
||||||
|
self.autoDecrypt = decrypt
|
||||||
|
|
||||||
# handle arguments
|
# handle arguments
|
||||||
if self.getCore() is None:
|
if self.getCore() is None:
|
||||||
self.core = onionrcore.Core()
|
self.core = onionrcore.Core()
|
||||||
|
|
||||||
# update the blocks' contents if it exists
|
self.update()
|
||||||
if not self.getHash() is None:
|
|
||||||
if not self.core._utils.validateHash(self.hash):
|
|
||||||
logger.debug('Block hash %s is invalid.' % self.getHash())
|
|
||||||
raise onionrexceptions.InvalidHexHash('Block hash is invalid.')
|
|
||||||
elif not self.update():
|
|
||||||
logger.debug('Failed to open block %s.' % self.getHash())
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
#logger.debug('Did not update block.')
|
|
||||||
|
|
||||||
# logic
|
|
||||||
|
|
||||||
def decrypt(self, anonymous = True, encodedData = True):
|
def decrypt(self, anonymous = True, encodedData = True):
|
||||||
'''
|
'''
|
||||||
@ -91,6 +81,7 @@ class Block:
|
|||||||
self.bmetadata = json.loads(bmeta)
|
self.bmetadata = json.loads(bmeta)
|
||||||
self.signature = core._crypto.pubKeyDecrypt(self.signature, anonymous=anonymous, encodedData=encodedData)
|
self.signature = core._crypto.pubKeyDecrypt(self.signature, anonymous=anonymous, encodedData=encodedData)
|
||||||
self.signer = core._crypto.pubKeyDecrypt(self.signer, anonymous=anonymous, encodedData=encodedData)
|
self.signer = core._crypto.pubKeyDecrypt(self.signer, anonymous=anonymous, encodedData=encodedData)
|
||||||
|
self.bheader['signer'] = self.signer.decode()
|
||||||
self.signedData = json.dumps(self.bmetadata) + self.bcontent.decode()
|
self.signedData = json.dumps(self.bmetadata) + self.bcontent.decode()
|
||||||
try:
|
try:
|
||||||
assert self.bmetadata['forwardEnc'] is True
|
assert self.bmetadata['forwardEnc'] is True
|
||||||
@ -140,13 +131,15 @@ class Block:
|
|||||||
Outputs:
|
Outputs:
|
||||||
- (bool): indicates whether or not the operation was successful
|
- (bool): indicates whether or not the operation was successful
|
||||||
'''
|
'''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# import from string
|
# import from string
|
||||||
blockdata = data
|
blockdata = data
|
||||||
|
|
||||||
# import from file
|
# import from file
|
||||||
if blockdata is None:
|
if blockdata is None:
|
||||||
|
blockdata = onionrstorage.getData(self.core, self.getHash()).decode()
|
||||||
|
'''
|
||||||
|
|
||||||
filelocation = file
|
filelocation = file
|
||||||
|
|
||||||
readfile = True
|
readfile = True
|
||||||
@ -164,13 +157,14 @@ class Block:
|
|||||||
filelocation = self.core.dataDir + 'blocks/%s.dat' % self.getHash()
|
filelocation = self.core.dataDir + 'blocks/%s.dat' % self.getHash()
|
||||||
|
|
||||||
if readfile:
|
if readfile:
|
||||||
with open(filelocation, 'rb') as f:
|
blockdata = onionrstorage.getData(self.core, self.getHash()).decode()
|
||||||
blockdata = f.read().decode()
|
#with open(filelocation, 'rb') as f:
|
||||||
|
#blockdata = f.read().decode()
|
||||||
|
|
||||||
self.blockFile = filelocation
|
self.blockFile = filelocation
|
||||||
|
'''
|
||||||
else:
|
else:
|
||||||
self.blockFile = None
|
self.blockFile = None
|
||||||
|
|
||||||
# parse block
|
# parse block
|
||||||
self.raw = str(blockdata)
|
self.raw = str(blockdata)
|
||||||
self.bheader = json.loads(self.getRaw()[:self.getRaw().index('\n')])
|
self.bheader = json.loads(self.getRaw()[:self.getRaw().index('\n')])
|
||||||
@ -199,6 +193,9 @@ class Block:
|
|||||||
if len(self.getRaw()) <= config.get('allocations.blockCache', 500000):
|
if len(self.getRaw()) <= config.get('allocations.blockCache', 500000):
|
||||||
self.cache()
|
self.cache()
|
||||||
|
|
||||||
|
if self.autoDecrypt:
|
||||||
|
self.decrypt()
|
||||||
|
|
||||||
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.error('Failed to parse block %s.' % self.getHash(), error = e, timestamp = False)
|
||||||
@ -240,12 +237,15 @@ class Block:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if self.isValid() is True:
|
if self.isValid() is True:
|
||||||
|
'''
|
||||||
if (not self.getBlockFile() is None) and (recreate is True):
|
if (not self.getBlockFile() is None) and (recreate is True):
|
||||||
with open(self.getBlockFile(), 'wb') as blockFile:
|
onionrstorage.store(self.core, self.getRaw().encode())
|
||||||
blockFile.write(self.getRaw().encode())
|
#with open(self.getBlockFile(), 'wb') as blockFile:
|
||||||
|
# blockFile.write(self.getRaw().encode())
|
||||||
else:
|
else:
|
||||||
self.hash = self.getCore().insertBlock(self.getContent(), header = self.getType(), sign = sign, meta = self.getMetadata(), expire = self.getExpire())
|
'''
|
||||||
|
self.hash = self.getCore().insertBlock(self.getRaw(), header = self.getType(), sign = sign, meta = self.getMetadata(), expire = self.getExpire())
|
||||||
|
if self.hash != False:
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
return self.getHash()
|
return self.getHash()
|
||||||
@ -585,7 +585,7 @@ class Block:
|
|||||||
|
|
||||||
return list()
|
return list()
|
||||||
|
|
||||||
def mergeChain(child, file = None, maximumFollows = 32, core = None):
|
def mergeChain(child, file = None, maximumFollows = 1000, core = None):
|
||||||
'''
|
'''
|
||||||
Follows a child Block to its root parent Block, merging content
|
Follows a child Block to its root parent Block, merging content
|
||||||
|
|
||||||
@ -632,7 +632,7 @@ class Block:
|
|||||||
|
|
||||||
blocks.append(block.getHash())
|
blocks.append(block.getHash())
|
||||||
|
|
||||||
buffer = ''
|
buffer = b''
|
||||||
|
|
||||||
# combine block contents
|
# combine block contents
|
||||||
for hash in blocks:
|
for hash in blocks:
|
||||||
@ -641,100 +641,18 @@ class Block:
|
|||||||
contents = base64.b64decode(contents.encode())
|
contents = base64.b64decode(contents.encode())
|
||||||
|
|
||||||
if file is None:
|
if file is None:
|
||||||
buffer += contents.decode()
|
try:
|
||||||
|
buffer += contents.encode()
|
||||||
|
except AttributeError:
|
||||||
|
buffer += contents
|
||||||
else:
|
else:
|
||||||
file.write(contents)
|
file.write(contents)
|
||||||
|
if file is not None:
|
||||||
|
file.close()
|
||||||
|
|
||||||
return (None if not file is None else buffer)
|
return (None if not file is None else buffer)
|
||||||
|
|
||||||
def createChain(data = None, chunksize = 99800, file = None, type = 'chunk', sign = True, encrypt = False, verbose = False):
|
def exists(bHash):
|
||||||
'''
|
|
||||||
Creates a chain of blocks to store larger amounts of data
|
|
||||||
|
|
||||||
The chunksize is set to 99800 because it provides the least amount of PoW for the most amount of data.
|
|
||||||
|
|
||||||
Inputs:
|
|
||||||
- data (*): if `file` is None, the data to be stored in blocks
|
|
||||||
- file (file/str): the filename or file object to read from (or None to read `data` instead)
|
|
||||||
- chunksize (int): the number of bytes per block chunk
|
|
||||||
- type (str): the type header for each of the blocks
|
|
||||||
- sign (bool): whether or not to sign each block
|
|
||||||
- encrypt (str): the public key to encrypt to, or False to disable encryption
|
|
||||||
- verbose (bool): whether or not to return a tuple containing more info
|
|
||||||
|
|
||||||
Outputs:
|
|
||||||
- if `verbose`:
|
|
||||||
- (tuple):
|
|
||||||
- (str): the child block hash
|
|
||||||
- (list): all block hashes associated with storing the file
|
|
||||||
- if not `verbose`:
|
|
||||||
- (str): the child block hash
|
|
||||||
'''
|
|
||||||
|
|
||||||
blocks = list()
|
|
||||||
|
|
||||||
# initial datatype checks
|
|
||||||
if data is None and file is None:
|
|
||||||
return blocks
|
|
||||||
elif not (file is None or (isinstance(file, str) and os.path.exists(file))):
|
|
||||||
return blocks
|
|
||||||
elif isinstance(file, str):
|
|
||||||
file = open(file, 'rb')
|
|
||||||
if not isinstance(data, str):
|
|
||||||
data = str(data)
|
|
||||||
|
|
||||||
if not file is None:
|
|
||||||
filesize = os.stat(file.name).st_size
|
|
||||||
offset = filesize % chunksize
|
|
||||||
maxtimes = int(filesize / chunksize)
|
|
||||||
|
|
||||||
for times in range(0, maxtimes + 1):
|
|
||||||
# read chunksize bytes from the file (end -> beginning)
|
|
||||||
if times < maxtimes:
|
|
||||||
file.seek(- ((times + 1) * chunksize), 2)
|
|
||||||
content = file.read(chunksize)
|
|
||||||
else:
|
|
||||||
file.seek(0, 0)
|
|
||||||
content = file.read(offset)
|
|
||||||
|
|
||||||
# encode it- python is really bad at handling certain bytes that
|
|
||||||
# are often present in binaries.
|
|
||||||
content = base64.b64encode(content).decode()
|
|
||||||
|
|
||||||
# if it is the end of the file, exit
|
|
||||||
if not content:
|
|
||||||
break
|
|
||||||
|
|
||||||
# create block
|
|
||||||
block = Block()
|
|
||||||
block.setType(type)
|
|
||||||
block.setContent(content)
|
|
||||||
block.setParent((blocks[-1] if len(blocks) != 0 else None))
|
|
||||||
hash = block.save(sign = sign)
|
|
||||||
|
|
||||||
# remember the hash in cache
|
|
||||||
blocks.append(hash)
|
|
||||||
elif not data is None:
|
|
||||||
for content in reversed([data[n:n + chunksize] for n in range(0, len(data), chunksize)]):
|
|
||||||
# encode chunk with base64
|
|
||||||
content = base64.b64encode(content.encode()).decode()
|
|
||||||
|
|
||||||
# create block
|
|
||||||
block = Block()
|
|
||||||
block.setType(type)
|
|
||||||
block.setContent(content)
|
|
||||||
block.setParent((blocks[-1] if len(blocks) != 0 else None))
|
|
||||||
hash = block.save(sign = sign)
|
|
||||||
|
|
||||||
# remember the hash in cache
|
|
||||||
blocks.append(hash)
|
|
||||||
|
|
||||||
# return different things depending on verbosity
|
|
||||||
if verbose:
|
|
||||||
return (blocks[-1], blocks)
|
|
||||||
return blocks[-1]
|
|
||||||
|
|
||||||
def exists(hash):
|
|
||||||
'''
|
'''
|
||||||
Checks if a block is saved to file or not
|
Checks if a block is saved to file or not
|
||||||
|
|
||||||
@ -748,15 +666,20 @@ class Block:
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
# no input data? scrap it.
|
# no input data? scrap it.
|
||||||
if hash is None:
|
if bHash is None:
|
||||||
return False
|
return False
|
||||||
|
'''
|
||||||
if type(hash) == Block:
|
if type(hash) == Block:
|
||||||
blockfile = hash.getBlockFile()
|
blockfile = hash.getBlockFile()
|
||||||
else:
|
else:
|
||||||
blockfile = onionrcore.Core().dataDir + 'blocks/%s.dat' % hash
|
blockfile = onionrcore.Core().dataDir + 'blocks/%s.dat' % hash
|
||||||
|
'''
|
||||||
|
if isinstance(bHash, Block):
|
||||||
|
bHash = bHash.getHash()
|
||||||
|
|
||||||
return os.path.exists(blockfile) and os.path.isfile(blockfile)
|
ret = isinstance(onionrstorage.getData(onionrcore.Core(), bHash), type(None))
|
||||||
|
|
||||||
|
return not ret
|
||||||
|
|
||||||
def getCache(hash = None):
|
def getCache(hash = None):
|
||||||
# give a list of the hashes of the cached blocks
|
# give a list of the hashes of the cached blocks
|
||||||
@ -789,7 +712,7 @@ class Block:
|
|||||||
if block.getHash() in Block.getCache() and not override:
|
if block.getHash() in Block.getCache() and not override:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# dump old cached blocks if the size exeeds the maximum
|
# dump old cached blocks if the size exceeds the maximum
|
||||||
if sys.getsizeof(Block.blockCacheOrder) >= config.get('allocations.block_cache_total', 50000000): # 50MB default cache size
|
if sys.getsizeof(Block.blockCacheOrder) >= config.get('allocations.block_cache_total', 50000000): # 50MB default cache size
|
||||||
del Block.blockCache[blockCacheOrder.pop(0)]
|
del Block.blockCache[blockCacheOrder.pop(0)]
|
||||||
|
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
'''
|
|
||||||
Onionr - P2P Anonymous Storage Network
|
|
||||||
|
|
||||||
Onionr Chat Messages
|
|
||||||
'''
|
|
||||||
'''
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
'''
|
|
||||||
import logger, time
|
|
||||||
|
|
||||||
class OnionrChat:
|
|
||||||
def __init__(self, communicatorInst):
|
|
||||||
'''OnionrChat uses onionrsockets (handled by the communicator) to exchange direct chat messages'''
|
|
||||||
self.communicator = communicatorInst
|
|
||||||
self._core = self.communicator._core
|
|
||||||
self._utils = self._core._utils
|
|
||||||
|
|
||||||
self.chats = {} # {'peer': {'date': date, message': message}}
|
|
||||||
self.chatSend = {}
|
|
||||||
|
|
||||||
def chatHandler(self):
|
|
||||||
while not self.communicator.shutdown:
|
|
||||||
for peer in self._core.socketServerConnData:
|
|
||||||
try:
|
|
||||||
assert self._core.socketReasons[peer] == "chat"
|
|
||||||
except (AssertionError, KeyError) as e:
|
|
||||||
logger.warn('Peer is not for chat')
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
self.chats[peer] = {'date': self._core.socketServerConnData[peer]['date'], 'data': self._core.socketServerConnData[peer]['data']}
|
|
||||||
logger.info("CHAT MESSAGE RECIEVED: %s" % self.chats[peer]['data'])
|
|
||||||
for peer in self.communicator.socketClient.sockets:
|
|
||||||
try:
|
|
||||||
logger.info(self.communicator.socketClient.connPool[peer]['data'])
|
|
||||||
self.communicator.socketClient.sendData(peer, "lol")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
time.sleep(2)
|
|
@ -210,12 +210,9 @@ class OnionrCrypto:
|
|||||||
ops = nacl.pwhash.argon2id.OPSLIMIT_SENSITIVE
|
ops = nacl.pwhash.argon2id.OPSLIMIT_SENSITIVE
|
||||||
mem = nacl.pwhash.argon2id.MEMLIMIT_SENSITIVE
|
mem = nacl.pwhash.argon2id.MEMLIMIT_SENSITIVE
|
||||||
|
|
||||||
key = kdf(nacl.secret.SecretBox.KEY_SIZE, passphrase, salt, opslimit=ops, memlimit=mem)
|
key = kdf(32, passphrase, salt, opslimit=ops, memlimit=mem) # Generate seed for ed25519 key
|
||||||
key = nacl.public.PrivateKey(key, nacl.encoding.RawEncoder())
|
key = nacl.signing.SigningKey(key)
|
||||||
publicKey = key.public_key
|
return (key.verify_key.encode(nacl.encoding.Base32Encoder).decode(), key.encode(nacl.encoding.Base32Encoder).decode())
|
||||||
|
|
||||||
return (publicKey.encode(encoder=nacl.encoding.Base32Encoder()),
|
|
||||||
key.encode(encoder=nacl.encoding.Base32Encoder()))
|
|
||||||
|
|
||||||
def pubKeyHashID(self, pubkey=''):
|
def pubKeyHashID(self, pubkey=''):
|
||||||
'''Accept a ed25519 public key, return a truncated result of X many sha3_256 hash rounds'''
|
'''Accept a ed25519 public key, return a truncated result of X many sha3_256 hash rounds'''
|
||||||
@ -269,7 +266,8 @@ class OnionrCrypto:
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
difficulty = math.floor(dataLen / 1000000)
|
difficulty = onionrproofs.getDifficultyForNewBlock(blockContent, ourBlock=False)
|
||||||
|
|
||||||
if difficulty < int(config.get('general.minimum_block_pow')):
|
if difficulty < int(config.get('general.minimum_block_pow')):
|
||||||
difficulty = int(config.get('general.minimum_block_pow'))
|
difficulty = int(config.get('general.minimum_block_pow'))
|
||||||
mainHash = '0000000000000000000000000000000000000000000000000000000000000000'#nacl.hash.blake2b(nacl.utils.random()).decode()
|
mainHash = '0000000000000000000000000000000000000000000000000000000000000000'#nacl.hash.blake2b(nacl.utils.random()).decode()
|
||||||
@ -283,5 +281,20 @@ class OnionrCrypto:
|
|||||||
|
|
||||||
return retData
|
return retData
|
||||||
|
|
||||||
def safeCompare(self, one, two):
|
@staticmethod
|
||||||
|
def safeCompare(one, two):
|
||||||
return hmac.compare_digest(one, two)
|
return hmac.compare_digest(one, two)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def randomShuffle(theList):
|
||||||
|
myList = list(theList)
|
||||||
|
shuffledList = []
|
||||||
|
myListLength = len(myList) + 1
|
||||||
|
while myListLength > 0:
|
||||||
|
removed = secrets.randbelow(myListLength)
|
||||||
|
try:
|
||||||
|
shuffledList.append(myList.pop(removed))
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
myListLength = len(myList)
|
||||||
|
return shuffledList
|
@ -34,7 +34,7 @@ class DaemonTools:
|
|||||||
'''Announce our node to our peers'''
|
'''Announce our node to our peers'''
|
||||||
retData = False
|
retData = False
|
||||||
announceFail = False
|
announceFail = False
|
||||||
|
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:
|
||||||
|
@ -53,6 +53,9 @@ class BlacklistedBlock(Exception):
|
|||||||
class DataExists(Exception):
|
class DataExists(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class NoDataAvailable(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
class InvalidHexHash(Exception):
|
class InvalidHexHash(Exception):
|
||||||
'''When a string is not a valid hex string of appropriate length for a hash value'''
|
'''When a string is not a valid hex string of appropriate length for a hash value'''
|
||||||
pass
|
pass
|
||||||
|
73
onionr/onionrfragment.py
Normal file
73
onionr/onionrfragment.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
'''
|
||||||
|
Onionr - P2P Anonymous Storage Network
|
||||||
|
|
||||||
|
This file contains the OnionrFragment class which implements the fragment system
|
||||||
|
'''
|
||||||
|
'''
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
'''
|
||||||
|
|
||||||
|
# onionr:10ch+10ch+10chgdecryptionkey
|
||||||
|
import core, sys, binascii, os
|
||||||
|
|
||||||
|
FRAGMENT_SIZE = 0.25
|
||||||
|
TRUNCATE_LENGTH = 30
|
||||||
|
|
||||||
|
class OnionrFragment:
|
||||||
|
def __init__(self, uri=None):
|
||||||
|
uri = uri.replace('onionr:', '')
|
||||||
|
count = 0
|
||||||
|
blocks = []
|
||||||
|
appendData = ''
|
||||||
|
key = ''
|
||||||
|
for x in uri:
|
||||||
|
if x == 'k':
|
||||||
|
key = uri[uri.index('k') + 1:]
|
||||||
|
appendData += x
|
||||||
|
if count == TRUNCATE_LENGTH:
|
||||||
|
blocks.append(appendData)
|
||||||
|
appendData = ''
|
||||||
|
count = 0
|
||||||
|
count += 1
|
||||||
|
self.key = key
|
||||||
|
self.blocks = blocks
|
||||||
|
return
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generateFragments(data=None, coreInst=None):
|
||||||
|
if coreInst is None:
|
||||||
|
coreInst = core.Core()
|
||||||
|
|
||||||
|
key = os.urandom(32)
|
||||||
|
data = coreInst._crypto.symmetricEncrypt(data, key).decode()
|
||||||
|
blocks = []
|
||||||
|
blockData = b""
|
||||||
|
uri = "onionr:"
|
||||||
|
total = sys.getsizeof(data)
|
||||||
|
for x in data:
|
||||||
|
blockData += x.encode()
|
||||||
|
if round(len(blockData) / len(data), 3) > FRAGMENT_SIZE:
|
||||||
|
blocks.append(core.Core().insertBlock(blockData))
|
||||||
|
blockData = b""
|
||||||
|
|
||||||
|
for bl in blocks:
|
||||||
|
uri += bl[:TRUNCATE_LENGTH]
|
||||||
|
uri += "k"
|
||||||
|
uri += binascii.hexlify(key).decode()
|
||||||
|
return (uri, key)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
uri = OnionrFragment.generateFragments("test")[0]
|
||||||
|
print(uri)
|
||||||
|
OnionrFragment(uri)
|
@ -1,58 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
from tkinter import *
|
|
||||||
import core
|
|
||||||
class OnionrGUI:
|
|
||||||
def __init__(self):
|
|
||||||
self.dataDir = "/programming/onionr/data/"
|
|
||||||
self.root = Tk()
|
|
||||||
self.root.geometry("450x250")
|
|
||||||
self.core = core.Core()
|
|
||||||
menubar = Menu(self.root)
|
|
||||||
|
|
||||||
# create a pulldown menu, and add it to the menu bar
|
|
||||||
filemenu = Menu(menubar, tearoff=0)
|
|
||||||
filemenu.add_command(label="Open", command=None)
|
|
||||||
filemenu.add_command(label="Save", command=None)
|
|
||||||
filemenu.add_separator()
|
|
||||||
filemenu.add_command(label="Exit", command=self.root.quit)
|
|
||||||
menubar.add_cascade(label="File", menu=filemenu)
|
|
||||||
|
|
||||||
settingsmenu = Menu(menubar, tearoff=0)
|
|
||||||
menubar.add_cascade(label="Settings", menu=settingsmenu)
|
|
||||||
|
|
||||||
helpmenu = Menu(menubar, tearoff=0)
|
|
||||||
menubar.add_cascade(label="Help", menu=helpmenu)
|
|
||||||
|
|
||||||
self.root.config(menu=menubar)
|
|
||||||
|
|
||||||
self.menuFrame = Frame(self.root)
|
|
||||||
self.mainButton = Button(self.menuFrame, text="Main View")
|
|
||||||
self.mainButton.grid(row=0, column=0, padx=0, pady=2, sticky=N+W)
|
|
||||||
self.tabButton1 = Button(self.menuFrame, text="Mail")
|
|
||||||
self.tabButton1.grid(row=0, column=1, padx=0, pady=2, sticky=N+W)
|
|
||||||
self.tabButton2 = Button(self.menuFrame, text="Message Flow")
|
|
||||||
self.tabButton2.grid(row=0, column=3, padx=0, pady=2, sticky=N+W)
|
|
||||||
|
|
||||||
self.menuFrame.grid(row=0, column=0, padx=2, pady=0, sticky=N+W)
|
|
||||||
|
|
||||||
|
|
||||||
self.idFrame = Frame(self.root)
|
|
||||||
|
|
||||||
self.ourIDLabel = Label(self.idFrame, text="ID: ")
|
|
||||||
self.ourIDLabel.grid(row=2, column=0, padx=1, pady=1, sticky=N+W)
|
|
||||||
self.ourID = Entry(self.idFrame)
|
|
||||||
self.ourID.insert(0, self.core._crypto.pubKey)
|
|
||||||
self.ourID.grid(row=2, column=1, padx=1, pady=1, sticky=N+W)
|
|
||||||
self.ourID.config(state='readonly')
|
|
||||||
self.idFrame.grid(row=1, column=0, padx=2, pady=2, sticky=N+W)
|
|
||||||
|
|
||||||
self.syncStatus = Label(self.root, text="Sync Status: 15/100")
|
|
||||||
self.syncStatus.place(relx=1.0, rely=1.0, anchor=S+E)
|
|
||||||
self.peerCount = Label(self.root, text="Connected Peers: 3")
|
|
||||||
self.peerCount.place(relx=0.0, rely=1.0, anchor='sw')
|
|
||||||
|
|
||||||
self.root.wm_title("Onionr")
|
|
||||||
self.root.mainloop()
|
|
||||||
return
|
|
||||||
|
|
||||||
OnionrGUI()
|
|
@ -28,6 +28,7 @@ class PeerProfiles:
|
|||||||
self.friendSigCount = 0
|
self.friendSigCount = 0
|
||||||
self.success = 0
|
self.success = 0
|
||||||
self.failure = 0
|
self.failure = 0
|
||||||
|
self.connectTime = None
|
||||||
|
|
||||||
if not isinstance(coreInst, core.Core):
|
if not isinstance(coreInst, core.Core):
|
||||||
raise TypeError("coreInst must be a type of core.Core")
|
raise TypeError("coreInst must be a type of core.Core")
|
||||||
@ -35,6 +36,7 @@ class PeerProfiles:
|
|||||||
assert isinstance(self.coreInst, core.Core)
|
assert isinstance(self.coreInst, core.Core)
|
||||||
|
|
||||||
self.loadScore()
|
self.loadScore()
|
||||||
|
self.getConnectTime()
|
||||||
return
|
return
|
||||||
|
|
||||||
def loadScore(self):
|
def loadScore(self):
|
||||||
@ -45,6 +47,12 @@ class PeerProfiles:
|
|||||||
self.success = 0
|
self.success = 0
|
||||||
self.score = self.success
|
self.score = self.success
|
||||||
|
|
||||||
|
def getConnectTime(self):
|
||||||
|
try:
|
||||||
|
self.connectTime = int(self.coreInst.getAddressInfo(self.address, 'lastConnect'))
|
||||||
|
except (KeyError, ValueError, TypeError) as e:
|
||||||
|
pass
|
||||||
|
|
||||||
def saveScore(self):
|
def saveScore(self):
|
||||||
'''Save the node's score to the database'''
|
'''Save the node's score to the database'''
|
||||||
self.coreInst.setAddressInfo(self.address, 'success', self.score)
|
self.coreInst.setAddressInfo(self.address, 'success', self.score)
|
||||||
@ -61,14 +69,20 @@ def getScoreSortedPeerList(coreInst):
|
|||||||
|
|
||||||
peerList = coreInst.listAdders()
|
peerList = coreInst.listAdders()
|
||||||
peerScores = {}
|
peerScores = {}
|
||||||
|
peerTimes = {}
|
||||||
|
|
||||||
for address in peerList:
|
for address in peerList:
|
||||||
# Load peer's profiles into a list
|
# Load peer's profiles into a list
|
||||||
profile = PeerProfiles(address, coreInst)
|
profile = PeerProfiles(address, coreInst)
|
||||||
peerScores[address] = profile.score
|
peerScores[address] = profile.score
|
||||||
|
if not isinstance(profile.connectTime, type(None)):
|
||||||
|
peerTimes[address] = profile.connectTime
|
||||||
|
else:
|
||||||
|
peerTimes[address] = 9000
|
||||||
|
|
||||||
# Sort peers by their score, greatest to least
|
# Sort peers by their score, greatest to least, and then last connected time
|
||||||
peerList = sorted(peerScores, key=peerScores.get, reverse=True)
|
peerList = sorted(peerScores, key=peerScores.get, reverse=True)
|
||||||
|
peerList = sorted(peerTimes, key=peerTimes.get, reverse=True)
|
||||||
return peerList
|
return peerList
|
||||||
|
|
||||||
def peerCleanup(coreInst):
|
def peerCleanup(coreInst):
|
||||||
|
@ -19,7 +19,57 @@
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
import nacl.encoding, nacl.hash, nacl.utils, time, math, threading, binascii, logger, sys, base64, json
|
import nacl.encoding, nacl.hash, nacl.utils, time, math, threading, binascii, logger, sys, base64, json
|
||||||
import core, config
|
import core, onionrutils, config
|
||||||
|
import onionrblockapi
|
||||||
|
|
||||||
|
def getDifficultyModifier(coreOrUtilsInst=None):
|
||||||
|
'''Accepts a core or utils instance returns
|
||||||
|
the difficulty modifier for block storage based
|
||||||
|
on a variety of factors, currently only disk use.
|
||||||
|
'''
|
||||||
|
classInst = coreOrUtilsInst
|
||||||
|
retData = 0
|
||||||
|
if isinstance(classInst, core.Core):
|
||||||
|
useFunc = classInst._utils.storageCounter.getPercent
|
||||||
|
elif isinstance(classInst, onionrutils.OnionrUtils):
|
||||||
|
useFunc = classInst.storageCounter.getPercent
|
||||||
|
else:
|
||||||
|
useFunc = core.Core()._utils.storageCounter.getPercent
|
||||||
|
|
||||||
|
percentUse = useFunc()
|
||||||
|
|
||||||
|
if percentUse >= 0.50:
|
||||||
|
retData += 1
|
||||||
|
elif percentUse >= 0.75:
|
||||||
|
retData += 2
|
||||||
|
elif percentUse >= 0.95:
|
||||||
|
retData += 3
|
||||||
|
|
||||||
|
return retData
|
||||||
|
|
||||||
|
def getDifficultyForNewBlock(data, ourBlock=True):
|
||||||
|
'''
|
||||||
|
Get difficulty for block. Accepts size in integer, Block instance, or str/bytes full block contents
|
||||||
|
'''
|
||||||
|
retData = 0
|
||||||
|
dataSize = 0
|
||||||
|
if isinstance(data, onionrblockapi.Block):
|
||||||
|
dataSize = len(data.getRaw().encode('utf-8'))
|
||||||
|
elif isinstance(data, str):
|
||||||
|
dataSize = len(data.encode('utf-8'))
|
||||||
|
elif isinstance(data, bytes):
|
||||||
|
dataSize = len(data)
|
||||||
|
elif isinstance(data, int):
|
||||||
|
dataSize = data
|
||||||
|
else:
|
||||||
|
raise ValueError('not Block, str, or int')
|
||||||
|
if ourBlock:
|
||||||
|
minDifficulty = config.get('general.minimum_send_pow')
|
||||||
|
else:
|
||||||
|
minDifficulty = config.get('general.minimum_block_pow')
|
||||||
|
|
||||||
|
retData = max(minDifficulty, math.floor(dataSize / 1000000)) + getDifficultyModifier()
|
||||||
|
return retData
|
||||||
|
|
||||||
def getHashDifficulty(h):
|
def getHashDifficulty(h):
|
||||||
'''
|
'''
|
||||||
@ -55,6 +105,7 @@ class DataPOW:
|
|||||||
self.difficulty = 0
|
self.difficulty = 0
|
||||||
self.data = data
|
self.data = data
|
||||||
self.threadCount = threadCount
|
self.threadCount = threadCount
|
||||||
|
self.rounds = 0
|
||||||
config.reload()
|
config.reload()
|
||||||
|
|
||||||
if forceDifficulty == 0:
|
if forceDifficulty == 0:
|
||||||
@ -96,6 +147,7 @@ class DataPOW:
|
|||||||
while self.hashing:
|
while self.hashing:
|
||||||
rand = nacl.utils.random()
|
rand = nacl.utils.random()
|
||||||
token = nacl.hash.blake2b(rand + self.data).decode()
|
token = nacl.hash.blake2b(rand + self.data).decode()
|
||||||
|
self.rounds += 1
|
||||||
#print(token)
|
#print(token)
|
||||||
if self.puzzle == token[0:self.difficulty]:
|
if self.puzzle == token[0:self.difficulty]:
|
||||||
self.hashing = False
|
self.hashing = False
|
||||||
@ -106,6 +158,7 @@ class DataPOW:
|
|||||||
endTime = math.floor(time.time())
|
endTime = math.floor(time.time())
|
||||||
if self.reporting:
|
if self.reporting:
|
||||||
logger.debug('Found token after %s seconds: %s' % (endTime - startTime, token), timestamp=True)
|
logger.debug('Found token after %s seconds: %s' % (endTime - startTime, token), timestamp=True)
|
||||||
|
logger.debug('Round count: %s' % (self.rounds,))
|
||||||
self.result = (token, rand)
|
self.result = (token, rand)
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
@ -146,17 +199,27 @@ class DataPOW:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
class POW:
|
class POW:
|
||||||
def __init__(self, metadata, data, threadCount = 5):
|
def __init__(self, metadata, data, threadCount = 5, forceDifficulty=0, coreInst=None):
|
||||||
self.foundHash = False
|
self.foundHash = False
|
||||||
self.difficulty = 0
|
self.difficulty = 0
|
||||||
self.data = data
|
self.data = data
|
||||||
self.metadata = metadata
|
self.metadata = metadata
|
||||||
self.threadCount = threadCount
|
self.threadCount = threadCount
|
||||||
|
|
||||||
|
try:
|
||||||
|
assert isinstance(coreInst, core.Core)
|
||||||
|
except AssertionError:
|
||||||
|
myCore = core.Core()
|
||||||
|
else:
|
||||||
|
myCore = coreInst
|
||||||
|
|
||||||
dataLen = len(data) + len(json.dumps(metadata))
|
dataLen = len(data) + len(json.dumps(metadata))
|
||||||
self.difficulty = math.floor(dataLen / 1000000)
|
|
||||||
if self.difficulty <= 2:
|
if forceDifficulty > 0:
|
||||||
self.difficulty = int(config.get('general.minimum_block_pow'))
|
self.difficulty = forceDifficulty
|
||||||
|
else:
|
||||||
|
# Calculate difficulty. Dumb for now, may use good algorithm in the future.
|
||||||
|
self.difficulty = getDifficultyForNewBlock(dataLen)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.data = self.data.encode()
|
self.data = self.data.encode()
|
||||||
@ -168,7 +231,6 @@ class POW:
|
|||||||
self.mainHash = '0' * 64
|
self.mainHash = '0' * 64
|
||||||
self.puzzle = self.mainHash[0:min(self.difficulty, len(self.mainHash))]
|
self.puzzle = self.mainHash[0:min(self.difficulty, len(self.mainHash))]
|
||||||
|
|
||||||
myCore = core.Core()
|
|
||||||
for i in range(max(1, threadCount)):
|
for i in range(max(1, threadCount)):
|
||||||
t = threading.Thread(name = 'thread%s' % i, target = self.pow, args = (True,myCore))
|
t = threading.Thread(name = 'thread%s' % i, target = self.pow, args = (True,myCore))
|
||||||
t.start()
|
t.start()
|
||||||
|
90
onionr/onionrstorage.py
Normal file
90
onionr/onionrstorage.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
'''
|
||||||
|
Onionr - P2P Anonymous Storage Network
|
||||||
|
|
||||||
|
This file handles block storage, providing an abstraction for storing blocks between file system and database
|
||||||
|
'''
|
||||||
|
'''
|
||||||
|
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, sys, sqlite3, os, dbcreator
|
||||||
|
|
||||||
|
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):
|
||||||
|
try:
|
||||||
|
dbcreator.DBCreator(coreInst).createBlockDataDB()
|
||||||
|
except FileExistsError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _dbInsert(coreInst, blockHash, data):
|
||||||
|
assert isinstance(coreInst, core.Core)
|
||||||
|
dbCreate(coreInst)
|
||||||
|
conn = sqlite3.connect(coreInst.blockDataDB, timeout=10)
|
||||||
|
c = conn.cursor()
|
||||||
|
data = (blockHash, data)
|
||||||
|
c.execute('INSERT INTO blockData (hash, data) VALUES(?, ?);', data)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def _dbFetch(coreInst, blockHash):
|
||||||
|
assert isinstance(coreInst, core.Core)
|
||||||
|
dbCreate(coreInst)
|
||||||
|
conn = sqlite3.connect(coreInst.blockDataDB, timeout=10)
|
||||||
|
c = conn.cursor()
|
||||||
|
for i in c.execute('SELECT data from blockData where hash = ?', (blockHash,)):
|
||||||
|
return i[0]
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def store(coreInst, data, blockHash=''):
|
||||||
|
assert isinstance(coreInst, core.Core)
|
||||||
|
assert coreInst._utils.validateHash(blockHash)
|
||||||
|
ourHash = coreInst._crypto.sha3Hash(data)
|
||||||
|
if blockHash != '':
|
||||||
|
assert ourHash == blockHash
|
||||||
|
else:
|
||||||
|
blockHash = ourHash
|
||||||
|
|
||||||
|
if DB_ENTRY_SIZE_LIMIT >= sys.getsizeof(data):
|
||||||
|
_dbInsert(coreInst, blockHash, data)
|
||||||
|
else:
|
||||||
|
with open('%s/%s.dat' % (coreInst.blockDataLocation, blockHash), 'wb') as blockFile:
|
||||||
|
blockFile.write(data)
|
||||||
|
coreInst.blockCache.cleanCache()
|
||||||
|
|
||||||
|
def getData(coreInst, bHash):
|
||||||
|
assert isinstance(coreInst, core.Core)
|
||||||
|
assert coreInst._utils.validateHash(bHash)
|
||||||
|
|
||||||
|
bHash = coreInst._utils.bytesToStr(bHash)
|
||||||
|
|
||||||
|
# First check DB for data entry by hash
|
||||||
|
# if no entry, check disk
|
||||||
|
# If no entry in either, raise an exception
|
||||||
|
retData = None
|
||||||
|
fileLocation = '%s/%s.dat' % (coreInst.blockDataLocation, bHash)
|
||||||
|
if os.path.exists(fileLocation):
|
||||||
|
with open(fileLocation, 'rb') as block:
|
||||||
|
retData = block.read()
|
||||||
|
else:
|
||||||
|
retData = _dbFetch(coreInst, bHash)
|
||||||
|
return retData
|
@ -83,7 +83,7 @@ class OnionrUser:
|
|||||||
if self._core._utils.validatePubKey(forwardKey):
|
if self._core._utils.validatePubKey(forwardKey):
|
||||||
retData = self._core._crypto.pubKeyEncrypt(data, forwardKey, encodedData=True, anonymous=True)
|
retData = self._core._crypto.pubKeyEncrypt(data, forwardKey, encodedData=True, anonymous=True)
|
||||||
else:
|
else:
|
||||||
raise onionrexceptions.InvalidPubkey("No valid forward key available for this user")
|
raise onionrexceptions.InvalidPubkey("No valid forward secrecy key available for this user")
|
||||||
#self.generateForwardKey()
|
#self.generateForwardKey()
|
||||||
return (retData, forwardKey)
|
return (retData, forwardKey)
|
||||||
|
|
||||||
@ -169,7 +169,9 @@ 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):
|
||||||
raise onionrexceptions.InvalidPubkey
|
raise onionrexceptions.InvalidPubkey(newKey)
|
||||||
|
if newKey in self._getForwardKeys():
|
||||||
|
return False
|
||||||
# Add a forward secrecy key for the peer
|
# 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()
|
||||||
|
@ -24,7 +24,8 @@ from onionrblockapi import Block
|
|||||||
import onionrexceptions
|
import onionrexceptions
|
||||||
from onionr import API_VERSION
|
from onionr import API_VERSION
|
||||||
import onionrevents
|
import onionrevents
|
||||||
import pgpwords, onionrusers, storagecounter
|
import onionrusers, storagecounter
|
||||||
|
from etc import pgpwords
|
||||||
if sys.version_info < (3, 6):
|
if sys.version_info < (3, 6):
|
||||||
try:
|
try:
|
||||||
import sha3
|
import sha3
|
||||||
@ -151,27 +152,42 @@ class OnionrUtils:
|
|||||||
logger.error('Failed to read my address.', error = error)
|
logger.error('Failed to read my address.', error = error)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def localCommand(self, command, data='', silent = True):
|
def getClientAPIServer(self):
|
||||||
'''
|
retData = ''
|
||||||
Send a command to the local http API server, securely. Intended for local clients, DO NOT USE for remote peers.
|
|
||||||
'''
|
|
||||||
|
|
||||||
config.reload()
|
|
||||||
self.getTimeBypassToken()
|
|
||||||
# TODO: URL encode parameters, just as an extra measure. May not be needed, but should be added regardless.
|
|
||||||
hostname = ''
|
|
||||||
while hostname == '':
|
|
||||||
try:
|
try:
|
||||||
with open(self._core.privateApiHostFile, 'r') as host:
|
with open(self._core.privateApiHostFile, 'r') as host:
|
||||||
hostname = host.read()
|
hostname = host.read()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print('wat')
|
raise FileNotFoundError
|
||||||
|
else:
|
||||||
|
retData += '%s:%s' % (hostname, config.get('client.client.port'))
|
||||||
|
return retData
|
||||||
|
|
||||||
|
def localCommand(self, command, data='', silent = True, post=False, postData = {}, maxWait=10):
|
||||||
|
'''
|
||||||
|
Send a command to the local http API server, securely. Intended for local clients, DO NOT USE for remote peers.
|
||||||
|
'''
|
||||||
|
config.reload()
|
||||||
|
self.getTimeBypassToken()
|
||||||
|
# TODO: URL encode parameters, just as an extra measure. May not be needed, but should be added regardless.
|
||||||
|
hostname = ''
|
||||||
|
waited = 0
|
||||||
|
while hostname == '':
|
||||||
|
try:
|
||||||
|
hostname = self.getClientAPIServer()
|
||||||
|
except FileNotFoundError:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
waited += 1
|
||||||
|
if waited == maxWait:
|
||||||
|
return False
|
||||||
if data != '':
|
if data != '':
|
||||||
data = '&data=' + urllib.parse.quote_plus(data)
|
data = '&data=' + urllib.parse.quote_plus(data)
|
||||||
payload = 'http://%s:%s/%s%s' % (hostname, config.get('client.client.port'), command, data)
|
payload = 'http://%s/%s%s' % (hostname, command, data)
|
||||||
try:
|
try:
|
||||||
retData = requests.get(payload, headers={'token': config.get('client.webpassword')}).text
|
if post:
|
||||||
|
retData = requests.post(payload, data=postData, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, 30)).text
|
||||||
|
else:
|
||||||
|
retData = requests.get(payload, headers={'token': config.get('client.webpassword'), 'Connection':'close'}, timeout=(maxWait, 30)).text
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
if not silent:
|
if not silent:
|
||||||
logger.error('Failed to make local request (command: %s):%s' % (command, error))
|
logger.error('Failed to make local request (command: %s):%s' % (command, error))
|
||||||
@ -365,6 +381,7 @@ class OnionrUtils:
|
|||||||
'''Validate metadata meets onionr spec (does not validate proof value computation), take in either dictionary or json string'''
|
'''Validate metadata meets onionr spec (does not validate proof value computation), take in either dictionary or json string'''
|
||||||
# TODO, make this check sane sizes
|
# TODO, make this check sane sizes
|
||||||
retData = False
|
retData = False
|
||||||
|
maxClockDifference = 60
|
||||||
|
|
||||||
# convert to dict if it is json string
|
# convert to dict if it is json string
|
||||||
if type(metadata) is str:
|
if type(metadata) is str:
|
||||||
@ -393,13 +410,14 @@ class OnionrUtils:
|
|||||||
break
|
break
|
||||||
if i == 'time':
|
if i == 'time':
|
||||||
if not self.isIntegerString(metadata[i]):
|
if not self.isIntegerString(metadata[i]):
|
||||||
logger.warn('Block metadata time stamp is not integer string')
|
logger.warn('Block metadata time stamp is not integer string or int')
|
||||||
break
|
break
|
||||||
if (metadata[i] - self.getEpoch()) > 30:
|
isFuture = (metadata[i] - self.getEpoch())
|
||||||
logger.warn('Block metadata time stamp is set for the future, which is not allowed.')
|
if isFuture > maxClockDifference:
|
||||||
|
logger.warn('Block timestamp is skewed to the future over the max %s: %s' (maxClockDifference, isFuture))
|
||||||
break
|
break
|
||||||
if (self.getEpoch() - metadata[i]) > maxAge:
|
if (self.getEpoch() - metadata[i]) > maxAge:
|
||||||
logger.warn('Block is older than allowed: %s' % (maxAge,))
|
logger.warn('Block is outdated: %s' % (metadata[i],))
|
||||||
elif i == 'expire':
|
elif i == 'expire':
|
||||||
try:
|
try:
|
||||||
assert int(metadata[i]) > self.getEpoch()
|
assert int(metadata[i]) > self.getEpoch()
|
||||||
@ -443,7 +461,7 @@ class OnionrUtils:
|
|||||||
return retVal
|
return retVal
|
||||||
|
|
||||||
def isIntegerString(self, data):
|
def isIntegerString(self, data):
|
||||||
'''Check if a string is a valid base10 integer'''
|
'''Check if a string is a valid base10 integer (also returns true if already an int)'''
|
||||||
try:
|
try:
|
||||||
int(data)
|
int(data)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -607,7 +625,7 @@ class OnionrUtils:
|
|||||||
proxies = {'http': 'http://127.0.0.1:4444'}
|
proxies = {'http': 'http://127.0.0.1:4444'}
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
headers = {'user-agent': 'PyOnionr'}
|
headers = {'user-agent': 'PyOnionr', 'Connection':'close'}
|
||||||
try:
|
try:
|
||||||
proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)}
|
proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)}
|
||||||
r = requests.post(url, data=data, headers=headers, proxies=proxies, allow_redirects=False, timeout=(15, 30))
|
r = requests.post(url, data=data, headers=headers, proxies=proxies, allow_redirects=False, timeout=(15, 30))
|
||||||
@ -632,11 +650,11 @@ class OnionrUtils:
|
|||||||
proxies = {'http': 'http://127.0.0.1:4444'}
|
proxies = {'http': 'http://127.0.0.1:4444'}
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
headers = {'user-agent': 'PyOnionr'}
|
headers = {'user-agent': 'PyOnionr', 'Connection':'close'}
|
||||||
response_headers = dict()
|
response_headers = dict()
|
||||||
try:
|
try:
|
||||||
proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)}
|
proxies = {'http': 'socks4a://127.0.0.1:' + str(port), 'https': 'socks4a://127.0.0.1:' + str(port)}
|
||||||
r = requests.get(url, headers=headers, proxies=proxies, allow_redirects=False, timeout=(15, 30))
|
r = requests.get(url, headers=headers, proxies=proxies, allow_redirects=False, timeout=(15, 30), )
|
||||||
# Check server is using same API version as us
|
# Check server is using same API version as us
|
||||||
if not ignoreAPI:
|
if not ignoreAPI:
|
||||||
try:
|
try:
|
||||||
|
29
onionr/proofofmemory.py
Normal file
29
onionr/proofofmemory.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
'''
|
||||||
|
Onionr - P2P Anonymous Storage Network
|
||||||
|
|
||||||
|
This file handles proof of memory functionality
|
||||||
|
'''
|
||||||
|
'''
|
||||||
|
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/>.
|
||||||
|
'''
|
||||||
|
|
||||||
|
class ProofOfMemory:
|
||||||
|
def __init__(self, commInst):
|
||||||
|
self.communicator = commInst
|
||||||
|
return
|
||||||
|
|
||||||
|
def checkRandomPeer(self):
|
||||||
|
return
|
||||||
|
def checkPeer(self, peer):
|
||||||
|
return
|
43
onionr/serializeddata.py
Normal file
43
onionr/serializeddata.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
'''
|
||||||
|
Onionr - P2P Anonymous Storage Network
|
||||||
|
|
||||||
|
This module serializes various data pieces for use in other modules, in particular the web api
|
||||||
|
'''
|
||||||
|
'''
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import core, api, uuid, json
|
||||||
|
|
||||||
|
class SerializedData:
|
||||||
|
def __init__(self, coreInst):
|
||||||
|
'''
|
||||||
|
Serialized data is in JSON format:
|
||||||
|
{
|
||||||
|
'success': bool,
|
||||||
|
'foo': 'bar',
|
||||||
|
etc
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
assert isinstance(coreInst, core.Core)
|
||||||
|
self._core = coreInst
|
||||||
|
|
||||||
|
def getStats(self):
|
||||||
|
'''Return statistics about our node'''
|
||||||
|
stats = {}
|
||||||
|
stats['uptime'] = self._core.onionrInst.communicatorInst.getUptime()
|
||||||
|
stats['connectedNodes'] = '\n'.join(self._core.onionrInst.communicatorInst.onlinePeers)
|
||||||
|
stats['blockCount'] = len(self._core.getBlockList())
|
||||||
|
stats['blockQueueCount'] = len(self._core.onionrInst.communicatorInst.blockQueue)
|
||||||
|
return json.dumps(stats)
|
@ -0,0 +1 @@
|
|||||||
|
dd3llxdp5q6ak3zmmicoy3jnodmroouv2xr7whkygiwp3rl7nf23gdad.onion
|
@ -19,7 +19,7 @@
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
# Imports some useful libraries
|
# Imports some useful libraries
|
||||||
import logger, config, threading, time, uuid, subprocess
|
import logger, config, threading, time, uuid, subprocess, sys
|
||||||
from onionrblockapi import Block
|
from onionrblockapi import Block
|
||||||
|
|
||||||
plugin_name = 'cliui'
|
plugin_name = 'cliui'
|
||||||
@ -31,10 +31,13 @@ class OnionrCLIUI:
|
|||||||
self.myCore = apiInst.get_core()
|
self.myCore = apiInst.get_core()
|
||||||
return
|
return
|
||||||
|
|
||||||
def subCommand(self, command):
|
def subCommand(self, command, args=None):
|
||||||
try:
|
try:
|
||||||
#subprocess.run(["./onionr.py", command])
|
#subprocess.run(["./onionr.py", command])
|
||||||
#subprocess.Popen(['./onionr.py', command], stdin=subprocess.STD, stdout=subprocess.STDOUT, stderr=subprocess.STDOUT)
|
#subprocess.Popen(['./onionr.py', command], stdin=subprocess.STD, stdout=subprocess.STDOUT, stderr=subprocess.STDOUT)
|
||||||
|
if args != None:
|
||||||
|
subprocess.call(['./onionr.py', command, args])
|
||||||
|
else:
|
||||||
subprocess.call(['./onionr.py', command])
|
subprocess.call(['./onionr.py', command])
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
@ -48,12 +51,11 @@ class OnionrCLIUI:
|
|||||||
isOnline = 'No'
|
isOnline = 'No'
|
||||||
firstRun = True
|
firstRun = True
|
||||||
choice = ''
|
choice = ''
|
||||||
|
if self.myCore._utils.localCommand('ping', maxWait=10) == 'pong!':
|
||||||
if self.myCore._utils.localCommand('ping') == 'pong':
|
|
||||||
firstRun = False
|
firstRun = False
|
||||||
|
|
||||||
while showMenu:
|
while showMenu:
|
||||||
if self.myCore._utils.localCommand('ping') == 'pong':
|
if self.myCore._utils.localCommand('ping', maxWait=2) == 'pong!':
|
||||||
isOnline = "Yes"
|
isOnline = "Yes"
|
||||||
else:
|
else:
|
||||||
isOnline = "No"
|
isOnline = "No"
|
||||||
@ -62,8 +64,7 @@ class OnionrCLIUI:
|
|||||||
1. Flow (Anonymous public chat, use at your own risk)
|
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. User Settings
|
4. Quit (Does not shutdown daemon)
|
||||||
5. Quit (Does not shutdown daemon)
|
|
||||||
''')
|
''')
|
||||||
try:
|
try:
|
||||||
choice = input(">").strip().lower()
|
choice = input(">").strip().lower()
|
||||||
@ -75,13 +76,9 @@ class OnionrCLIUI:
|
|||||||
elif choice in ("2", "mail"):
|
elif choice in ("2", "mail"):
|
||||||
self.subCommand("mail")
|
self.subCommand("mail")
|
||||||
elif choice in ("3", "file sharing", "file"):
|
elif choice in ("3", "file sharing", "file"):
|
||||||
print("Not supported yet")
|
filename = input("Enter full path to file: ").strip()
|
||||||
elif choice in ("4", "user settings", "settings"):
|
self.subCommand("addfile", filename)
|
||||||
try:
|
elif choice in ("4", "quit"):
|
||||||
self.setName()
|
|
||||||
except (KeyboardInterrupt, EOFError) as e:
|
|
||||||
pass
|
|
||||||
elif choice in ("5", "quit"):
|
|
||||||
showMenu = False
|
showMenu = False
|
||||||
elif choice == "":
|
elif choice == "":
|
||||||
pass
|
pass
|
||||||
@ -89,14 +86,6 @@ class OnionrCLIUI:
|
|||||||
logger.error("Invalid choice")
|
logger.error("Invalid choice")
|
||||||
return
|
return
|
||||||
|
|
||||||
def setName(self):
|
|
||||||
try:
|
|
||||||
name = input("Enter your name: ")
|
|
||||||
if name != "":
|
|
||||||
self.myCore.insertBlock("userInfo-" + str(uuid.uuid1()), sign=True, header='userInfo', meta={'name': name})
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
return
|
|
||||||
|
|
||||||
def on_init(api, data = None):
|
def on_init(api, data = None):
|
||||||
'''
|
'''
|
||||||
|
@ -54,9 +54,10 @@ class OnionrFlow:
|
|||||||
self.flowRunning = False
|
self.flowRunning = False
|
||||||
expireTime = self.myCore._utils.getEpoch() + 43200
|
expireTime = self.myCore._utils.getEpoch() + 43200
|
||||||
if len(message) > 0:
|
if len(message) > 0:
|
||||||
insertBL = Block(content = message, type = 'txt', expire=expireTime, core = self.myCore)
|
self.myCore.insertBlock(message, header='txt', expire=expireTime, meta={'ch': self.channel})
|
||||||
insertBL.setMetadata('ch', self.channel)
|
#insertBL = Block(content = message, type = 'txt', expire=expireTime, core = self.myCore)
|
||||||
insertBL.save()
|
#insertBL.setMetadata('ch', self.channel)
|
||||||
|
#insertBL.save()
|
||||||
|
|
||||||
logger.info("Flow is exiting, goodbye")
|
logger.info("Flow is exiting, goodbye")
|
||||||
return
|
return
|
||||||
@ -66,10 +67,13 @@ class OnionrFlow:
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
try:
|
try:
|
||||||
while self.flowRunning:
|
while self.flowRunning:
|
||||||
for block in Block.getBlocks(type = 'txt', core = self.myCore):
|
for block in self.myCore.getBlocksByType('txt'):
|
||||||
|
block = Block(block)
|
||||||
if block.getMetadata('ch') != self.channel:
|
if block.getMetadata('ch') != self.channel:
|
||||||
|
#print('not chan', block.getMetadata('ch'))
|
||||||
continue
|
continue
|
||||||
if block.getHash() in self.alreadyOutputed:
|
if block.getHash() in self.alreadyOutputed:
|
||||||
|
#print('already')
|
||||||
continue
|
continue
|
||||||
if not self.flowRunning:
|
if not self.flowRunning:
|
||||||
break
|
break
|
||||||
|
@ -28,24 +28,6 @@ plugin_name = 'metadataprocessor'
|
|||||||
|
|
||||||
# event listeners
|
# event listeners
|
||||||
|
|
||||||
def _processUserInfo(api, newBlock):
|
|
||||||
'''
|
|
||||||
Set the username for a particular user, from a signed block by them
|
|
||||||
'''
|
|
||||||
myBlock = newBlock
|
|
||||||
peerName = myBlock.getMetadata('name')
|
|
||||||
try:
|
|
||||||
if len(peerName) > 20:
|
|
||||||
raise onionrexceptions.InvalidMetdata('Peer name specified is too large')
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
except onionrexceptions.InvalidMetadata:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if signer in self.api.get_core().listPeers():
|
|
||||||
api.get_core().setPeerInfo(signer, 'name', peerName)
|
|
||||||
logger.info('%s is now using the name %s.' % (signer, api.get_utils().escapeAnsi(peerName)))
|
|
||||||
|
|
||||||
def _processForwardKey(api, myBlock):
|
def _processForwardKey(api, myBlock):
|
||||||
'''
|
'''
|
||||||
Get the forward secrecy key specified by the user for us to use
|
Get the forward secrecy key specified by the user for us to use
|
||||||
@ -67,12 +49,8 @@ def on_processblocks(api):
|
|||||||
|
|
||||||
# Process specific block types
|
# Process specific block types
|
||||||
|
|
||||||
# userInfo blocks, such as for setting username
|
|
||||||
if blockType == 'userInfo':
|
|
||||||
if api.data['validSig'] == True: # we use == True for type safety
|
|
||||||
_processUserInfo(api, myBlock)
|
|
||||||
# forwardKey blocks, add a new forward secrecy key for a peer
|
# forwardKey blocks, add a new forward secrecy key for a peer
|
||||||
elif blockType == 'forwardKey':
|
if blockType == 'forwardKey':
|
||||||
if api.data['validSig'] == True:
|
if api.data['validSig'] == True:
|
||||||
_processForwardKey(api, myBlock)
|
_processForwardKey(api, myBlock)
|
||||||
# socket blocks
|
# socket blocks
|
||||||
|
@ -74,6 +74,7 @@ class OnionrMail:
|
|||||||
logger.info('Decrypting messages...')
|
logger.info('Decrypting messages...')
|
||||||
choice = ''
|
choice = ''
|
||||||
displayList = []
|
displayList = []
|
||||||
|
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 recieved a lot of messages
|
||||||
for blockHash in self.myCore.getBlocksByType('pm'):
|
for blockHash in self.myCore.getBlocksByType('pm'):
|
||||||
@ -97,7 +98,12 @@ class OnionrMail:
|
|||||||
senderDisplay = senderKey
|
senderDisplay = senderKey
|
||||||
|
|
||||||
blockDate = pmBlocks[blockHash].getDate().strftime("%m/%d %H:%M")
|
blockDate = pmBlocks[blockHash].getDate().strftime("%m/%d %H:%M")
|
||||||
displayList.append('%s. %s - %s: %s' % (blockCount, blockDate, senderDisplay[:12], blockHash))
|
try:
|
||||||
|
subject = pmBlocks[blockHash].bmetadata['subject']
|
||||||
|
except KeyError:
|
||||||
|
subject = ''
|
||||||
|
|
||||||
|
displayList.append('%s. %s - %s - <%s>: %s' % (blockCount, blockDate, senderDisplay[:12], subject[:10], blockHash))
|
||||||
while choice not in ('-q', 'q', 'quit'):
|
while choice not in ('-q', 'q', 'quit'):
|
||||||
for i in displayList:
|
for i in displayList:
|
||||||
logger.info(i)
|
logger.info(i)
|
||||||
@ -188,6 +194,7 @@ class OnionrMail:
|
|||||||
def draftMessage(self, recip=''):
|
def draftMessage(self, recip=''):
|
||||||
message = ''
|
message = ''
|
||||||
newLine = ''
|
newLine = ''
|
||||||
|
subject = ''
|
||||||
entering = False
|
entering = False
|
||||||
if len(recip) == 0:
|
if len(recip) == 0:
|
||||||
entering = True
|
entering = True
|
||||||
@ -207,21 +214,30 @@ class OnionrMail:
|
|||||||
else:
|
else:
|
||||||
# if -q or ctrl-c/d, exit function here, otherwise we successfully got the public key
|
# if -q or ctrl-c/d, exit function here, otherwise we successfully got the public key
|
||||||
return
|
return
|
||||||
|
try:
|
||||||
|
subject = logger.readline('Message subject: ')
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
pass
|
||||||
|
|
||||||
logger.info('Enter your message, stop by entering -q on a new line.')
|
cancelEnter = False
|
||||||
|
logger.info('Enter your message, stop by entering -q on a new line. -c to cancel')
|
||||||
while newLine != '-q':
|
while newLine != '-q':
|
||||||
try:
|
try:
|
||||||
newLine = input()
|
newLine = input()
|
||||||
except (KeyboardInterrupt, EOFError):
|
except (KeyboardInterrupt, EOFError):
|
||||||
pass
|
cancelEnter = True
|
||||||
|
if newLine == '-c':
|
||||||
|
cancelEnter = True
|
||||||
|
break
|
||||||
if newLine == '-q':
|
if newLine == '-q':
|
||||||
continue
|
continue
|
||||||
newLine += '\n'
|
newLine += '\n'
|
||||||
message += newLine
|
message += newLine
|
||||||
|
|
||||||
|
if not cancelEnter:
|
||||||
logger.info('Inserting encrypted message as Onionr block....')
|
logger.info('Inserting encrypted message as Onionr block....')
|
||||||
|
|
||||||
blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=True)
|
blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=True, meta={'subject': subject})
|
||||||
self.sentboxTools.addToSent(blockID, recip, message)
|
self.sentboxTools.addToSent(blockID, recip, message)
|
||||||
def menu(self):
|
def menu(self):
|
||||||
choice = ''
|
choice = ''
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"general" : {
|
"general" : {
|
||||||
"dev_mode" : true,
|
"dev_mode" : true,
|
||||||
"display_header" : true,
|
"display_header" : false,
|
||||||
"minimum_block_pow": 5,
|
"minimum_block_pow": 1,
|
||||||
"minimum_send_pow": 5,
|
"minimum_send_pow": 1,
|
||||||
"socket_servers": false,
|
"socket_servers": false,
|
||||||
"security_level": 0,
|
"security_level": 0,
|
||||||
"max_block_age": 2678400,
|
"max_block_age": 2678400,
|
||||||
"public_key": "",
|
"bypass_tor_check": false,
|
||||||
"use_new_api_server": false
|
"public_key": ""
|
||||||
},
|
},
|
||||||
|
|
||||||
"www" : {
|
"www" : {
|
||||||
@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
"file": {
|
"file": {
|
||||||
"output": true,
|
"output": true,
|
||||||
"path": "data/output.log"
|
"path": "output.log"
|
||||||
},
|
},
|
||||||
|
|
||||||
"console" : {
|
"console" : {
|
||||||
@ -70,7 +70,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"allocations" : {
|
"allocations" : {
|
||||||
"disk" : 10000000000,
|
"disk" : 100000000,
|
||||||
"net_total" : 1000000000,
|
"net_total" : 1000000000,
|
||||||
"blockCache" : 5000000,
|
"blockCache" : 5000000,
|
||||||
"blockCacheTotal" : 50000000
|
"blockCacheTotal" : 50000000
|
||||||
@ -79,7 +79,7 @@
|
|||||||
"peers" : {
|
"peers" : {
|
||||||
"minimum_score" : -100,
|
"minimum_score" : -100,
|
||||||
"max_stored_peers" : 5000,
|
"max_stored_peers" : 5000,
|
||||||
"max_connect" : 10
|
"max_connect" : 1000
|
||||||
},
|
},
|
||||||
|
|
||||||
"timers" : {
|
"timers" : {
|
||||||
|
58
onionr/static-data/www/board/board.js
Normal file
58
onionr/static-data/www/board/board.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
webpassword = ''
|
||||||
|
requested = []
|
||||||
|
|
||||||
|
document.getElementById('webpassWindow').style.display = 'block';
|
||||||
|
|
||||||
|
var windowHeight = window.innerHeight;
|
||||||
|
document.getElementById('webpassWindow').style.height = windowHeight + "px";
|
||||||
|
|
||||||
|
function httpGet(theUrl) {
|
||||||
|
var xmlHttp = new XMLHttpRequest()
|
||||||
|
xmlHttp.open( "GET", theUrl, false ) // false for synchronous request
|
||||||
|
xmlHttp.setRequestHeader('token', webpassword)
|
||||||
|
xmlHttp.send( null )
|
||||||
|
if (xmlHttp.status == 200){
|
||||||
|
return xmlHttp.responseText
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function appendMessages(msg){
|
||||||
|
el = document.createElement('div')
|
||||||
|
el.className = 'entry'
|
||||||
|
el.innerText = msg
|
||||||
|
document.getElementById('feed').appendChild(el)
|
||||||
|
document.getElementById('feed').appendChild(document.createElement('br'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBlocks(){
|
||||||
|
if (document.getElementById('none') !== null){
|
||||||
|
document.getElementById('none').remove();
|
||||||
|
|
||||||
|
}
|
||||||
|
var feedText = httpGet('/getblocksbytype/txt')
|
||||||
|
var blockList = feedText.split(',')
|
||||||
|
for (i = 0; i < blockList.length; i++){
|
||||||
|
if (! requested.includes(blockList[i])){
|
||||||
|
bl = httpGet('/gethtmlsafeblockdata/' + blockList[i])
|
||||||
|
appendMessages(bl)
|
||||||
|
requested.push(blockList[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('registerPassword').onclick = function(){
|
||||||
|
webpassword = document.getElementById('webpassword').value
|
||||||
|
if (httpGet('/ping') === 'pong!'){
|
||||||
|
document.getElementById('webpassWindow').style.display = 'none'
|
||||||
|
getBlocks()
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
alert('Sorry, but that password appears invalid.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('refreshFeed').onclick = function(){
|
||||||
|
getBlocks()
|
||||||
|
}
|
22
onionr/static-data/www/board/index.html
Normal file
22
onionr/static-data/www/board/index.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'>
|
||||||
|
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||||
|
<title>
|
||||||
|
OnionrBoard
|
||||||
|
</title>
|
||||||
|
<link rel='stylesheet' href='theme.css'>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id='webpassWindow' class='hidden'>
|
||||||
|
<p>Welcome to OnionrBoard</p>
|
||||||
|
<p>Please enter the webpassword. You can get this from running the 'details' command in Onionr.</p>
|
||||||
|
<input id='webpassword' type='password' placeholder="Web password for daemon" value='CBF15ED9782FB482339E5F5B9DDCF3E58E523E71E8E9EF480596817AB5EA2E63'>
|
||||||
|
<button id='registerPassword'>Unlock Onionr</button>
|
||||||
|
</div>
|
||||||
|
<input type='button' id='refreshFeed' value='Refresh Feed'>
|
||||||
|
<div id='feed'><span id='none'>None Yet :)</span></div>
|
||||||
|
<script src='board.js'></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
31
onionr/static-data/www/board/theme.css
Normal file
31
onionr/static-data/www/board/theme.css
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
h1, h2, h3{
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
.hidden{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
p{
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
#webpassWindow{
|
||||||
|
background-color: black;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry{
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
#feed{
|
||||||
|
margin-left: 2%;
|
||||||
|
margin-right: 25%;
|
||||||
|
margin-top: 1em;
|
||||||
|
border: 2px solid black;
|
||||||
|
padding: 5px;
|
||||||
|
min-height: 50px;
|
||||||
|
}
|
24
onionr/static-data/www/mail/index.html
Normal file
24
onionr/static-data/www/mail/index.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'>
|
||||||
|
<title>
|
||||||
|
Onionr Mail
|
||||||
|
</title>
|
||||||
|
<link rel='stylesheet' href='/shared/style/modal.css'>
|
||||||
|
<link rel='stylesheet' href='/shared/main/style.css'>
|
||||||
|
<link rel='stylesheet' href='/mail/mail.css'>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="infoOverlay" class='overlay'>
|
||||||
|
</div>
|
||||||
|
<img class='logo' src='/shared/onionr-icon.png' alt='onionr logo'>
|
||||||
|
<span class='logoText'>Onionr Mail</span>
|
||||||
|
<div class='content'>
|
||||||
|
<button class='refresh'>Refresh</button>
|
||||||
|
<div id='threads' class='threads'></div>
|
||||||
|
</div>
|
||||||
|
<script src='/shared/misc.js'></script>
|
||||||
|
<script src='/mail/mail.js'></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
7
onionr/static-data/www/mail/mail.css
Normal file
7
onionr/static-data/www/mail/mail.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.threads div{
|
||||||
|
padding-top: 1em;
|
||||||
|
}
|
||||||
|
.threads div span{
|
||||||
|
padding-left: 0.5em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
}
|
52
onionr/static-data/www/mail/mail.js
Normal file
52
onionr/static-data/www/mail/mail.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
pms = ''
|
||||||
|
threadPart = document.getElementById('threads')
|
||||||
|
function getInbox(){
|
||||||
|
for(var i = 0; i < pms.length; i++) {
|
||||||
|
fetch('/getblockdata/' + pms[i], {
|
||||||
|
headers: {
|
||||||
|
"token": webpass
|
||||||
|
}})
|
||||||
|
.then((resp) => resp.json()) // Transform the data into json
|
||||||
|
.then(function(resp) {
|
||||||
|
|
||||||
|
var entry = document.createElement('div')
|
||||||
|
|
||||||
|
var bHashDisplay = document.createElement('a')
|
||||||
|
var senderInput = document.createElement('input')
|
||||||
|
var subjectLine = document.createElement('span')
|
||||||
|
var dateStr = document.createElement('span')
|
||||||
|
var humanDate = new Date(0)
|
||||||
|
humanDate.setUTCSeconds(resp['meta']['time'])
|
||||||
|
senderInput.value = resp['meta']['signer']
|
||||||
|
bHashDisplay.innerText = pms[i - 1].substring(0, 10)
|
||||||
|
bHashDisplay.setAttribute('hash', pms[i - 1]);
|
||||||
|
senderInput.readOnly = true
|
||||||
|
dateStr.innerText = humanDate.toString()
|
||||||
|
if (resp['metadata']['subject'] === undefined || resp['metadata']['subject'] === null) {
|
||||||
|
subjectLine.innerText = '()'
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
subjectLine.innerText = '(' + resp['metadata']['subject'] + ')'
|
||||||
|
}
|
||||||
|
//entry.innerHTML = 'sender ' + resp['meta']['signer'] + ' - ' + resp['meta']['time']
|
||||||
|
threadPart.appendChild(entry)
|
||||||
|
entry.appendChild(bHashDisplay)
|
||||||
|
entry.appendChild(senderInput)
|
||||||
|
entry.appendChild(subjectLine)
|
||||||
|
entry.appendChild(dateStr)
|
||||||
|
|
||||||
|
}.bind([pms, i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/getblocksbytype/pm', {
|
||||||
|
headers: {
|
||||||
|
"token": webpass
|
||||||
|
}})
|
||||||
|
.then((resp) => resp.text()) // Transform the data into json
|
||||||
|
.then(function(data) {
|
||||||
|
pms = data.split(',')
|
||||||
|
getInbox(pms)
|
||||||
|
})
|
||||||
|
|
33
onionr/static-data/www/private/index.html
Normal file
33
onionr/static-data/www/private/index.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'>
|
||||||
|
<title>
|
||||||
|
Onionr
|
||||||
|
</title>
|
||||||
|
<link rel='stylesheet' href='/shared/style/modal.css'>
|
||||||
|
<link rel='stylesheet' href='/shared/main/style.css'>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="shutdownNotice" class='overlay'>
|
||||||
|
<div>
|
||||||
|
<p>Your node will shutdown. Thank you for using Onionr.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<img class='logo' src='/shared/onionr-icon.png' alt='onionr logo'>
|
||||||
|
<span class='logoText'>Onionr Web Control Panel</span>
|
||||||
|
<div class='content'>
|
||||||
|
<button id='shutdownNode'>Shutdown Node</button> <button id='refreshStats'>Refresh Stats</button>
|
||||||
|
<br><br><a class='idLink' href='/mail/'>Mail</a>
|
||||||
|
<h2>Stats</h2>
|
||||||
|
<p>Uptime: <span id='uptime'></span></p>
|
||||||
|
<p>Stored Blocks: <span id='storedBlocks'></span></p>
|
||||||
|
<p>Blocks in queue: <span id='blockQueue'></span></p>
|
||||||
|
<p>Connected nodes:</p>
|
||||||
|
<pre id='connectedNodes'></pre>
|
||||||
|
</div>
|
||||||
|
<script src='/shared/misc.js'></script>
|
||||||
|
<script src='/shared/main/stats.js'></script>
|
||||||
|
<script src='/shared/panel.js'></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
32
onionr/static-data/www/shared/main/stats.js
Normal file
32
onionr/static-data/www/shared/main/stats.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
Onionr - P2P Anonymous Storage Network
|
||||||
|
|
||||||
|
This file loads stats to show on the main node web page
|
||||||
|
|
||||||
|
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/>
|
||||||
|
*/
|
||||||
|
uptimeDisplay = document.getElementById('uptime')
|
||||||
|
connectedDisplay = document.getElementById('connectedNodes')
|
||||||
|
storedBlockDisplay = document.getElementById('storedBlocks')
|
||||||
|
queuedBlockDisplay = document.getElementById('blockQueue')
|
||||||
|
|
||||||
|
function getStats(){
|
||||||
|
stats = JSON.parse(httpGet('getstats', webpass))
|
||||||
|
uptimeDisplay.innerText = stats['uptime'] + ' seconds'
|
||||||
|
connectedDisplay.innerText = stats['connectedNodes']
|
||||||
|
storedBlockDisplay.innerText = stats['blockCount']
|
||||||
|
queuedBlockDisplay.innerText = stats['blockQueueCount']
|
||||||
|
}
|
||||||
|
getStats()
|
140
onionr/static-data/www/shared/main/style.css
Normal file
140
onionr/static-data/www/shared/main/style.css
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
body{
|
||||||
|
background-color: #2c2b3f;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, a:visited{
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.center{
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
footer{
|
||||||
|
margin-top: 2em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
body{
|
||||||
|
margin-left: 3em;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
.onionrMenu{
|
||||||
|
max-width: 25%;
|
||||||
|
margin-left: 2%;
|
||||||
|
margin-right: 10%;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
.onionrMenu li{
|
||||||
|
list-style-type: none;
|
||||||
|
margin-top: 3px;
|
||||||
|
font-size: 125%;
|
||||||
|
}
|
||||||
|
.onionrMenu li:hover{
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.box {
|
||||||
|
display: flex;
|
||||||
|
align-items:center;
|
||||||
|
}
|
||||||
|
.logo{
|
||||||
|
max-width: 25%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.logoText{
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 2em;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-left: 1%;
|
||||||
|
}
|
||||||
|
.main{
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content{
|
||||||
|
margin-top: 3em;
|
||||||
|
margin-left: 0%;
|
||||||
|
margin-right: 40%;
|
||||||
|
background-color: white;
|
||||||
|
color: black;
|
||||||
|
padding-right: 5%;
|
||||||
|
padding-left: 3%;
|
||||||
|
padding-bottom: 2em;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 10px;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
.content p{
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
.content img{
|
||||||
|
max-width: 35%;
|
||||||
|
}
|
||||||
|
.content a, .content a:visited{
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats{
|
||||||
|
margin-top: 1em;
|
||||||
|
background-color: #0c1049;
|
||||||
|
padding: 5px;
|
||||||
|
margin-right: 45%;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
.statDesc{
|
||||||
|
background-color: black;
|
||||||
|
padding: 5px;
|
||||||
|
margin-right: 1%;
|
||||||
|
margin-left: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats noscript{
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statItem{
|
||||||
|
padding-left: 10px;
|
||||||
|
float: right;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warn{
|
||||||
|
color: orangered;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 640px) {
|
||||||
|
.onionrMenu{
|
||||||
|
margin-left: 0%;
|
||||||
|
}
|
||||||
|
body{
|
||||||
|
margin-left: 0em;
|
||||||
|
}
|
||||||
|
.content{
|
||||||
|
margin-left: 1%;
|
||||||
|
margin-right: 2%;
|
||||||
|
}
|
||||||
|
.content img{
|
||||||
|
max-width: 85%;
|
||||||
|
}
|
||||||
|
.stats{
|
||||||
|
margin-right: 1%;
|
||||||
|
}
|
||||||
|
.statItem{
|
||||||
|
float: initial;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*https://stackoverflow.com/a/16778646*/
|
||||||
|
.overlay {
|
||||||
|
visibility: hidden;
|
||||||
|
position: absolute;
|
||||||
|
left: 0px;
|
||||||
|
top: 0px;
|
||||||
|
width:100%;
|
||||||
|
opacity: 0.9;
|
||||||
|
height:100%;
|
||||||
|
text-align:center;
|
||||||
|
z-index: 1000;
|
||||||
|
background-color: black;
|
||||||
|
}
|
44
onionr/static-data/www/shared/misc.js
Normal file
44
onionr/static-data/www/shared/misc.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
webpass = document.location.hash.replace('#', '')
|
||||||
|
nowebpass = false
|
||||||
|
if (typeof webpass == "undefined"){
|
||||||
|
webpass = localStorage['webpass']
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
localStorage['webpass'] = webpass
|
||||||
|
//document.location.hash = ''
|
||||||
|
}
|
||||||
|
if (typeof webpass == "undefined" || webpass == ""){
|
||||||
|
alert('Web password was not found in memory or URL')
|
||||||
|
nowebpass = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function httpGet(theUrl) {
|
||||||
|
var xmlHttp = new XMLHttpRequest()
|
||||||
|
xmlHttp.open( "GET", theUrl, false ) // false for synchronous request
|
||||||
|
xmlHttp.setRequestHeader('token', webpass)
|
||||||
|
xmlHttp.send( null )
|
||||||
|
if (xmlHttp.status == 200){
|
||||||
|
return xmlHttp.responseText
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function overlay(overlayID) {
|
||||||
|
el = document.getElementById(overlayID)
|
||||||
|
el.style.visibility = (el.style.visibility == "visible") ? "hidden" : "visible"
|
||||||
|
}
|
||||||
|
|
||||||
|
var passLinks = document.getElementsByClassName("idLink")
|
||||||
|
for(var i = 0; i < passLinks.length; i++) {
|
||||||
|
passLinks[i].href += '#' + webpass
|
||||||
|
}
|
||||||
|
|
||||||
|
var refreshLinks = document.getElementsByClassName("refresh")
|
||||||
|
|
||||||
|
for(var i = 0; i < refreshLinks.length; i++) {
|
||||||
|
//Can't use .reload because of webpass
|
||||||
|
refreshLinks[i].onclick = function(){
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
}
|
BIN
onionr/static-data/www/shared/onionr-icon.png
Normal file
BIN
onionr/static-data/www/shared/onionr-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
7
onionr/static-data/www/shared/onionrblocks.js
Normal file
7
onionr/static-data/www/shared/onionrblocks.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
class Block {
|
||||||
|
constructor(hash, raw) {
|
||||||
|
this.hash = hash;
|
||||||
|
this.raw = raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
12
onionr/static-data/www/shared/panel.js
Normal file
12
onionr/static-data/www/shared/panel.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
shutdownBtn = document.getElementById('shutdownNode')
|
||||||
|
refreshStatsBtn = document.getElementById('refreshStats')
|
||||||
|
shutdownBtn.onclick = function(){
|
||||||
|
if (! nowebpass){
|
||||||
|
httpGet('shutdownclean')
|
||||||
|
overlay('shutdownNotice')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshStatsBtn.onclick = function(){
|
||||||
|
getStats()
|
||||||
|
}
|
2
onionr/static-data/www/ui/dist/js/main.js
vendored
2
onionr/static-data/www/ui/dist/js/main.js
vendored
@ -704,7 +704,7 @@ if(tt !== null && tt !== undefined) {
|
|||||||
if(getWebPassword() === null) {
|
if(getWebPassword() === null) {
|
||||||
var password = "";
|
var password = "";
|
||||||
while(password.length != 64) {
|
while(password.length != 64) {
|
||||||
password = prompt("Please enter the web password (run `./RUN-LINUX.sh --get-password`)");
|
password = prompt("Please enter the web password (run `./RUN-LINUX.sh --details`)");
|
||||||
}
|
}
|
||||||
|
|
||||||
setWebPassword(password);
|
setWebPassword(password);
|
||||||
|
@ -42,8 +42,15 @@ class StorageCounter:
|
|||||||
retData = int(dataFile.read())
|
retData = int(dataFile.read())
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
except ValueError:
|
||||||
|
pass # Possibly happens when the file is empty
|
||||||
return retData
|
return retData
|
||||||
|
|
||||||
|
def getPercent(self):
|
||||||
|
'''Return percent (decimal/float) of disk space we're using'''
|
||||||
|
amount = self.getAmount()
|
||||||
|
return round(amount / self._core.config.get('allocations.disk'), 2)
|
||||||
|
|
||||||
def addBytes(self, amount):
|
def addBytes(self, amount):
|
||||||
'''Record that we are now using more disk space, unless doing so would exceed configured max'''
|
'''Record that we are now using more disk space, unless doing so would exceed configured max'''
|
||||||
newAmount = amount + self.getAmount()
|
newAmount = amount + self.getAmount()
|
||||||
|
5
start-daemon.sh
Executable file
5
start-daemon.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/bash
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
echo "starting Onionr daemon..."
|
||||||
|
echo "run onionr.sh stop to stop the daemon, or onionr.sh start to get output"
|
||||||
|
nohup ./onionr.sh start & disown > /dev/null
|
Loading…
Reference in New Issue
Block a user