Removed old block system

This commit is contained in:
Kevin F 2022-01-30 19:39:24 -06:00
parent e99056afac
commit 1d22b43ef9
89 changed files with 10 additions and 5759 deletions

View File

@ -70,7 +70,6 @@ createdirs.create_dirs()
import bigbrother # noqa
from onionrcommands import parser # noqa
from onionrplugins import onionrevents as events # noqa
from onionrblocks.deleteplaintext import delete_plaintext_no_blacklist # noqa
setup.setup_config()
@ -84,8 +83,6 @@ if config.get('advanced.security_auditing', True):
except onionrexceptions.PythonVersion:
pass
if not config.get('general.store_plaintext_blocks', True):
delete_plaintext_no_blacklist()
setup.setup_default_plugins()

View File

@ -68,7 +68,6 @@ class PrivateAPI:
self.httpServer = ''
self.queueResponse = {}
self.get_block_data = httpapi.apiutils.GetBlockData(self)
register_private_blueprints.register_private_blueprints(self, app)
httpapi.load_plugin_blueprints(app)
self.app = app

View File

@ -6,7 +6,7 @@ from threading import Thread
from gevent import sleep
from httpapi import security, friendsapi, configapi
from httpapi import miscclientapi, onionrsitesapi, apiutils
from httpapi import miscclientapi, apiutils
from httpapi import themeapi
from httpapi import fileoffsetreader
from httpapi.sse.private import private_sse_blueprint
@ -35,8 +35,6 @@ def register_private_blueprints(private_api, app):
app.register_blueprint(configapi.config_BP)
app.register_blueprint(miscclientapi.endpoints.PrivateEndpoints(
private_api).private_endpoints_bp)
app.register_blueprint(miscclientapi.motd.bp)
app.register_blueprint(onionrsitesapi.site_api)
app.register_blueprint(apiutils.shutdown.shutdown_bp)
app.register_blueprint(miscclientapi.staticfiles.static_files_bp)
app.register_blueprint(themeapi.theme_blueprint)

View File

@ -1 +1 @@
from . import keydb, blockmetadb
from . import keydb

View File

@ -1,84 +0,0 @@
"""Onionr - Private P2P Communication.
Work with information relating to blocks stored on the node
"""
import sqlite3
from etc import onionrvalues
from . import expiredblocks, updateblockinfo, add
from .. import dbfiles
"""
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/>.
"""
update_block_info = updateblockinfo.update_block_info
add_to_block_DB = add.add_to_block_DB
def get_block_list(date_rec=None, unsaved=False):
"""Get list of our blocks."""
if date_rec is None:
date_rec = 0
conn = sqlite3.connect(
dbfiles.block_meta_db, timeout=onionrvalues.DATABASE_LOCK_TIMEOUT)
c = conn.cursor()
execute = 'SELECT hash FROM hashes WHERE dateReceived' + \
' >= ? ORDER BY dateReceived ASC;'
args = (date_rec,)
rows = list()
for row in c.execute(execute, args):
for i in row:
rows.append(i)
conn.close()
return rows
def get_block_date(blockHash):
"""Return the date a block was received."""
conn = sqlite3.connect(
dbfiles.block_meta_db, timeout=onionrvalues.DATABASE_LOCK_TIMEOUT)
c = conn.cursor()
execute = 'SELECT dateReceived FROM hashes WHERE hash=?;'
args = (blockHash,)
for row in c.execute(execute, args):
for i in row:
return int(i)
conn.close()
return None
def get_blocks_by_type(blockType, orderDate=True):
"""Return a list of blocks by the type."""
conn = sqlite3.connect(
dbfiles.block_meta_db, timeout=onionrvalues.DATABASE_LOCK_TIMEOUT)
c = conn.cursor()
if orderDate:
execute = 'SELECT hash FROM hashes WHERE dataType=? ORDER BY dateReceived;'
else:
execute = 'SELECT hash FROM hashes WHERE dataType=?;'
args = (blockType,)
rows = list()
for row in c.execute(execute, args):
for i in row:
rows.append(i)
conn.close()
return rows

View File

@ -1,49 +0,0 @@
"""Onionr - Private P2P Communication.
Add an entry to the block metadata database
"""
import sqlite3
import secrets
from onionrutils import epoch
from onionrblocks import blockmetadata
from etc import onionrvalues
from .. import dbfiles
from onionrexceptions import BlockMetaEntryExists
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
def add_to_block_DB(newHash, selfInsert=False, dataSaved=False):
"""
Add a hash value to the block db
Should be in hex format!
"""
if blockmetadata.has_block(newHash):
raise BlockMetaEntryExists
conn = sqlite3.connect(
dbfiles.block_meta_db, timeout=onionrvalues.DATABASE_LOCK_TIMEOUT)
c = conn.cursor()
currentTime = epoch.get_epoch() + secrets.randbelow(61)
if selfInsert or dataSaved:
selfInsert = 1
else:
selfInsert = 0
data = (newHash, currentTime, '', selfInsert)
c.execute(
'INSERT INTO hashes (hash, dateReceived, dataType, dataSaved) VALUES(?, ?, ?, ?);', data)
conn.commit()
conn.close()

View File

@ -1,41 +0,0 @@
"""Onionr - Private P2P Communication.
Get a list of expired blocks still stored
"""
import sqlite3
from onionrutils import epoch
from .. import dbfiles
from etc import onionrvalues
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
def get_expired_blocks():
"""Return a list of expired blocks."""
conn = sqlite3.connect(
dbfiles.block_meta_db, timeout=onionrvalues.DATABASE_LOCK_TIMEOUT)
c = conn.cursor()
date = int(epoch.get_epoch())
compiled = (date,)
execute = 'SELECT hash FROM hashes WHERE ' + \
'expire <= ? ORDER BY dateReceived;'
rows = list()
for row in c.execute(execute, compiled):
for i in row:
rows.append(i)
conn.close()
return rows

View File

@ -1,52 +0,0 @@
"""Onionr - Private P2P Communication.
Update block information in the metadata database by a field name
"""
import sqlite3
from .. import dbfiles
from etc import onionrvalues
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
def update_block_info(hash, key, data):
"""set info associated with a block
hash - the hash of a block
dateReceived - the date the block was recieved, not necessarily when it was created
decrypted - if we can successfully decrypt the block
dataType - data type of the block
dataFound - if the data has been found for the block
dataSaved - if the data has been saved for the block
sig - defunct
author - defunct
dateClaimed - timestamp claimed inside the block, only as trustworthy as the block author is
expire - expire date for a block
"""
if key not in ('dateReceived', 'decrypted', 'dataType', 'dataFound',
'dataSaved', 'sig', 'author', 'dateClaimed', 'expire'):
raise ValueError('Key must be in the allowed list')
conn = sqlite3.connect(dbfiles.block_meta_db,
timeout=onionrvalues.DATABASE_LOCK_TIMEOUT)
c = conn.cursor()
args = (data, hash)
# Unfortunately, not really possible to prepare this statement
c.execute("UPDATE hashes SET " + key + " = ? where hash = ?;", args)
conn.commit()
conn.close()
return True

View File

@ -3,8 +3,6 @@ import filepaths
home = identifyhome.identify_home()
if not home.endswith('/'): home += '/'
block_meta_db = '%sblock-metadata.db' % (home)
block_data_db = '%s/block-data.db' % (filepaths.block_data_location,)
address_info_db = '%saddress.db' % (home,)
user_id_info_db = '%susers.db' % (home,)
forward_keys_db = '%sforward-keys.db' % (home,)

View File

@ -1,3 +1 @@
from . import shutdown, setbindip, getblockdata
GetBlockData = getblockdata.GetBlockData
from . import shutdown, setbindip

View File

@ -1,38 +0,0 @@
import ujson as json
from onionrblocks import onionrblockapi
from onionrutils import bytesconverter, stringvalidators
import onionrexceptions
class GetBlockData:
def __init__(self, client_api_inst=None):
return
def get_block_data(self, bHash, decrypt=False, raw=False, headerOnly=False):
if not stringvalidators.validate_hash(bHash):
raise onionrexceptions.InvalidHexHash(
"block hash not valid hash format")
bl = onionrblockapi.Block(bHash)
if decrypt:
bl.decrypt()
if bl.isEncrypted and not bl.decrypted:
raise ValueError
if not raw:
if not headerOnly:
retData = {'meta':bl.bheader, 'metadata': bl.bmetadata, 'content': bl.bcontent}
for x in list(retData.keys()):
try:
retData[x] = retData[x].decode()
except AttributeError:
pass
else:
validSig = False
signer = bytesconverter.bytes_to_str(bl.signer)
if bl.isSigned() and stringvalidators.validate_pub_key(signer) and bl.isSigner(signer):
validSig = True
bl.bheader['validSig'] = validSig
bl.bheader['meta'] = ''
retData = {'meta': bl.bheader, 'metadata': bl.bmetadata}
return json.dumps(retData)
else:
return bl.raw

View File

@ -1 +1 @@
from . import staticfiles, endpoints, motd
from . import staticfiles, endpoints

View File

@ -78,10 +78,6 @@ class PrivateEndpoints:
subprocess.Popen([SCRIPT_NAME, 'restart'])
return Response("bye")
@private_endpoints_bp.route('/gethidden')
def get_hidden_blocks():
return Response('\n'.join(client_api.publicAPI.hideBlocks))
@private_endpoints_bp.route('/getuptime')
def show_uptime():

View File

@ -1,27 +0,0 @@
from flask import Blueprint
from flask import Response
import unpaddedbase32
from coredb import blockmetadb
import onionrblocks
from etc import onionrvalues
import config
from onionrutils import bytesconverter
bp = Blueprint('motd', __name__)
signer = config.get("motd.motd_key", onionrvalues.MOTD_SIGN_KEY)
@bp.route('/getmotd')
def get_motd()->Response:
motds = blockmetadb.get_blocks_by_type("motd")
newest_time = 0
message = "No MOTD currently present."
for x in motds:
bl = onionrblocks.onionrblockapi.Block(x)
if not bl.verifySig() or bl.signer != bytesconverter.bytes_to_str(unpaddedbase32.repad(bytesconverter.str_to_bytes(signer))): continue
if not bl.isSigner(signer): continue
if bl.claimedTime > newest_time:
newest_time = bl.claimedTime
message = bl.bcontent
return Response(message, headers={"Content-Type": "text/plain"})

View File

@ -1,94 +0,0 @@
"""Onionr - Private P2P Communication.
view and interact with onionr sites
"""
import base64
import binascii
import mimetypes
import unpaddedbase32
from flask import Blueprint, Response, request, abort
from onionrblocks import onionrblockapi
import onionrexceptions
from onionrutils import stringvalidators
from onionrutils import mnemonickeys
from . import sitefiles
"""
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/>.
"""
site_api = Blueprint('siteapi', __name__)
@site_api.route('/site/<name>/', endpoint='site')
def site(name: str)->Response:
"""Accept a site 'name', if pubkey then show multi-page site, if hash show single page site"""
resp: str = 'Not Found'
mime_type = 'text/html'
# If necessary convert the name to base32 from mnemonic
if mnemonickeys.DELIMITER in name:
name = mnemonickeys.get_base32(name)
# Now make sure the key is regardless a valid base32 format ed25519 key (readding padding if necessary)
if stringvalidators.validate_pub_key(name):
name = unpaddedbase32.repad(name)
resp = sitefiles.get_file(name, 'index.html')
elif stringvalidators.validate_hash(name):
try:
resp = onionrblockapi.Block(name).bcontent
except onionrexceptions.NoDataAvailable:
abort(404)
except TypeError:
pass
try:
resp = base64.b64decode(resp)
except binascii.Error:
pass
if resp == 'Not Found' or not resp:
abort(404)
return Response(resp)
@site_api.route('/site/<name>/<path:file>', endpoint='siteFile')
def site_file(name: str, file: str)->Response:
"""Accept a site 'name', if pubkey then show multi-page site, if hash show single page site"""
resp: str = 'Not Found'
mime_type = mimetypes.MimeTypes().guess_type(file)[0]
# If necessary convert the name to base32 from mnemonic
if mnemonickeys.DELIMITER in name:
name = mnemonickeys.get_base32(name)
# Now make sure the key is regardless a valid base32 format ed25519 key (readding padding if necessary)
if stringvalidators.validate_pub_key(name):
name = unpaddedbase32.repad(name)
resp = sitefiles.get_file(name, file)
elif stringvalidators.validate_hash(name):
try:
resp = onionrblockapi.Block(name).bcontent
except onionrexceptions.NoDataAvailable:
abort(404)
except TypeError:
pass
try:
resp = base64.b64decode(resp)
except binascii.Error:
pass
if resp == 'Not Found' or not resp:
abort(404)
return Response(resp, mimetype=mime_type)

View File

@ -1,49 +0,0 @@
"""
Onionr - Private P2P Communication
view and interact with onionr sites
"""
from typing import Union
import onionrexceptions
from onionrutils import mnemonickeys
from onionrutils import stringvalidators
from coredb import blockmetadb
from onionrblocks.onionrblockapi import Block
from onionrtypes import BlockHash
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
def find_site(user_id: str) -> Union[BlockHash, None]:
"""Returns block hash str for latest block for a site by a given user id"""
# If mnemonic delim in key, convert to base32 version
if mnemonickeys.DELIMITER in user_id:
user_id = mnemonickeys.get_base32(user_id)
if not stringvalidators.validate_pub_key(user_id):
raise onionrexceptions.InvalidPubkey
found_site = None
sites = blockmetadb.get_blocks_by_type('osite')
# Find site by searching all site blocks. eww O(N) ☹️, TODO: event based
for site in sites:
site = Block(site)
if site.isSigner(user_id) and site.verifySig():
found_site = site.hash
return found_site

View File

@ -1,79 +0,0 @@
"""Onionr - Private P2P Communication.
Read onionr site files
"""
from typing import Union, Tuple
import tarfile
import io
import os
import unpaddedbase32
from coredb import blockmetadb
from onionrblocks import onionrblockapi
from onionrblocks import insert
# Import types. Just for type hiting
from onionrtypes import UserID, DeterministicKeyPassphrase, BlockHash
from onionrcrypto import generate_deterministic
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
def find_site_gzip(user_id: str)->tarfile.TarFile:
"""Return verified site tar object"""
sites = blockmetadb.get_blocks_by_type('osite')
user_site = None
unpadded_user = user_id
user_id = unpaddedbase32.repad(user_id)
for site in sites:
block = onionrblockapi.Block(site)
if block.isSigner(user_id) or block.isSigner(unpadded_user):
user_site = block
if not user_site is None:
return tarfile.open(fileobj=io.BytesIO(user_site.bcontent), mode='r')
return None
def get_file(user_id, file)->Union[bytes, None]:
"""Get a site file content"""
ret_data = ""
site = find_site_gzip(user_id)
if file.endswith('/'):
file += 'index.html'
if site is None: return None
for t_file in site.getmembers():
if t_file.name.replace('./', '') == file:
return site.extractfile(t_file)
return None
def create_site(admin_pass: DeterministicKeyPassphrase, directory:str='.')->Tuple[UserID, BlockHash]:
public_key, private_key = generate_deterministic(admin_pass)
raw_tar = io.BytesIO()
tar = tarfile.open(mode='x:gz', fileobj=raw_tar)
tar.add(directory)
tar.close()
raw_tar.seek(0)
block_hash = insert(raw_tar.read(), header='osite', signing_key=private_key, sign=True)
return (public_key, block_hash)

View File

@ -9,9 +9,6 @@ from gevent import sleep
import gevent
import ujson
from onionrblocks.onionrblockapi import Block
from coredb.dbfiles import block_meta_db
from coredb.blockmetadb import get_block_list
from onionrutils.epoch import get_epoch
from .. import wrapper
"""
@ -42,27 +39,3 @@ def stream_hello():
sleep(1)
return SSEWrapper.handle_sse_request(print_hello)
@private_sse_blueprint.route('/recentblocks')
def stream_recent_blocks():
def _compile_json(b_list):
js = {}
block_obj = None
for block in b_list:
block_obj = Block(block)
if block_obj.isEncrypted:
js[block] = 'encrypted'
else:
js[block] = Block(block).btype
return ujson.dumps({"blocks": js}, reject_bytes=True)
def _stream_recent():
last_time = Path(block_meta_db).stat().st_ctime
while True:
if Path(block_meta_db).stat().st_ctime != last_time:
last_time = Path(block_meta_db).stat().st_ctime
yield "data: " + _compile_json(get_block_list(get_epoch() - 5)) + "\n\n"
else:
yield "data: none" + "\n\n"
sleep(5)
return SSEWrapper.handle_sse_request(_stream_recent)

View File

@ -1,5 +0,0 @@
from . import insert
from .insert import time_insert
from .blocklist import BlockList
insert = insert.insert_block
time_insert = time_insert

View File

@ -1,11 +0,0 @@
import ujson
import nacl.utils
from nacl.public import PrivateKey, SealedBox
from .blockmetadata import get_block_metadata_from_data
def block_decrypt(raw_block) -> DecryptedBlock:
block_header, user_meta, block_data = get_block_metadata_from_data(
raw_block)

View File

@ -1,69 +0,0 @@
"""Onionr - Private P2P Communication.
Import block data and save it
"""
from onionrexceptions import BlacklistedBlock
from onionrexceptions import DiskAllocationReached
from onionrexceptions import InvalidProof
from onionrexceptions import InvalidMetadata
import logger
from onionrutils import validatemetadata
from onionrutils import bytesconverter
from coredb import blockmetadb
from onionrblocks import blockmetadata
import onionrstorage
import onionrcrypto as crypto
from . import onionrblacklist
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
def import_block_from_data(content):
blacklist = onionrblacklist.OnionrBlackList()
ret_data = False
content = bytesconverter.str_to_bytes(content)
data_hash = crypto.hashers.sha3_hash(content)
if blacklist.inBlacklist(data_hash):
raise BlacklistedBlock(f'%s is a blacklisted block {data_hash}')
# returns tuple(metadata, meta), meta is also in metadata
metas = blockmetadata.get_block_metadata_from_data(content)
metadata = metas[0]
# check if metadata is valid
if validatemetadata.validate_metadata(metadata, metas[2]):
# check if POW is enough/correct
if crypto.cryptoutils.verify_POW(content):
logger.info(f'Imported block passed proof, saving: {data_hash}.',
terminal=True)
try:
block_hash = onionrstorage.set_data(content)
except DiskAllocationReached:
logger.warn('Failed to save block due to full disk allocation')
raise
else:
blockmetadb.add_to_block_DB(block_hash, dataSaved=True)
# caches block metadata values to block database
blockmetadata.process_block_metadata(block_hash)
ret_data = block_hash
else:
raise InvalidProof
else:
raise InvalidMetadata
return ret_data

View File

@ -1,60 +0,0 @@
"""Onionr - Private P2P Communication.
Get an auto updating list of blocks
"""
from threading import Thread
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from utils.identifyhome import identify_home
from coredb.dbfiles import block_meta_db
from coredb.blockmetadb import get_block_list, get_blocks_by_type
from onionrutils.epoch import get_epoch
"""
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 BlockList:
def __init__(self, auto_refresh=True, block_type=''):
self.block_type = block_type
self.refresh_db()
self.check_time = get_epoch()
class Refresher(FileSystemEventHandler):
@staticmethod
def on_modified(event):
if event.src_path != block_meta_db:
return
self.refresh_db()
if auto_refresh:
def auto_refresher():
observer = Observer()
observer.schedule(
Refresher(), identify_home(), recursive=False)
observer.start()
while observer.is_alive():
# call import func with timeout
observer.join(120)
Thread(target=auto_refresher, daemon=True).start()
def get(self):
return self.block_list
def refresh_db(self):
self.check_time = get_epoch()
if not self.block_type:
self.block_list = get_block_list()
else:
self.block_list = get_blocks_by_type(self.block_type)

View File

@ -1,24 +0,0 @@
'''
Onionr - Private P2P Communication
Module to work with block metadata
'''
'''
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/>.
'''
from . import hasblock, fromdata, process
has_block = hasblock.has_block
process_block_metadata = process.process_block_metadata
get_block_metadata_from_data = fromdata.get_block_metadata_from_data

View File

@ -1,51 +0,0 @@
"""Onionr - Private P2P Communication.
Return a useful tuple of (metadata (header), meta, and data) by accepting raw block data
"""
from json import JSONDecodeError
import ujson as json
from onionrutils import bytesconverter
import logger
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
def get_block_metadata_from_data(block_data):
"""
accepts block contents as string, returns a tuple of
metadata, meta (meta being internal metadata, which will be
returned as an encrypted base64 string if it is encrypted, dict if not).
"""
meta = {}
metadata = {}
data = block_data
try:
block_data = block_data.encode()
except AttributeError:
pass
try:
metadata = json.loads(bytesconverter.bytes_to_str(block_data[:block_data.find(b'\n')]))
except JSONDecodeError:
pass
except ValueError:
logger.warn("Could not get metadata from:", terminal=True)
logger.warn(block_data, terminal=True)
else:
data = block_data[block_data.find(b'\n'):]
meta = metadata['meta']
return (metadata, meta, data)

View File

@ -1,43 +0,0 @@
"""Onionr - Private P2P Communication.
Return a bool if a block is in the block metadata db or not
"""
import sqlite3
from coredb import dbfiles
import onionrexceptions
from onionrutils import stringvalidators
from etc import onionrvalues
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
def has_block(hash: str) -> bool:
"""Check for new block in the block meta db."""
conn = sqlite3.connect(
dbfiles.block_meta_db,
timeout=onionrvalues.DATABASE_LOCK_TIMEOUT)
c = conn.cursor()
if not stringvalidators.validate_hash(hash):
raise onionrexceptions.InvalidHexHash("Invalid hash")
for result in c.execute("SELECT COUNT() FROM hashes WHERE hash = ?", (hash,)):
if result[0] >= 1:
conn.commit()
conn.close()
return True
else:
conn.commit()
conn.close()
return False
return False

View File

@ -1,71 +0,0 @@
"""Onionr - Private P2P Communication.
Process block metadata with relevant actions
"""
from etc import onionrvalues
from onionrblocks import onionrblockapi
from onionrutils import epoch, bytesconverter
from coredb import blockmetadb
import logger
from onionrplugins import onionrevents
import onionrexceptions
from onionrusers import onionrusers
from onionrutils import updater
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
def process_block_metadata(blockHash: str):
"""
Read metadata from a block and cache it to the block database.
blockHash -> sha3_256 hex formatted hash of Onionr block
"""
curTime = epoch.get_rounded_epoch(roundS=60)
myBlock = onionrblockapi.Block(blockHash)
if myBlock.isEncrypted:
myBlock.decrypt()
if (myBlock.isEncrypted and myBlock.decrypted) or (not myBlock.isEncrypted):
blockType = myBlock.getMetadata('type') # we would use myBlock.getType() here, but it is bugged with encrypted blocks
signer = bytesconverter.bytes_to_str(myBlock.signer)
valid = myBlock.verifySig()
if valid:
if myBlock.getMetadata('newFSKey') is not None:
try:
onionrusers.OnionrUser(signer).addForwardKey(myBlock.getMetadata('newFSKey'))
except onionrexceptions.InvalidPubkey:
logger.warn('%s has invalid forward secrecy key to add: %s' % (signer, myBlock.getMetadata('newFSKey')))
try:
if len(blockType) <= onionrvalues.MAX_BLOCK_TYPE_LENGTH:
blockmetadb.update_block_info(blockHash, 'dataType', blockType)
except TypeError:
logger.warn("Missing block information")
pass
# Set block expire time if specified
try:
expireTime = int(myBlock.getHeader('expire'))
# test that expire time is an integer of sane length (for epoch)
# doesn't matter if its too large because of the min() func below
if not len(str(expireTime)) < 20: raise ValueError('timestamp invalid')
except (ValueError, TypeError) as e:
expireTime = onionrvalues.DEFAULT_EXPIRE + curTime
finally:
expireTime = min(expireTime, curTime + onionrvalues.DEFAULT_EXPIRE)
blockmetadb.update_block_info(blockHash, 'expire', expireTime)
if blockType == 'update': updater.update_event(myBlock)
onionrevents.event('processblocks', data = {'block': myBlock, 'type': blockType, 'signer': signer, 'validSig': valid})

View File

@ -1,35 +0,0 @@
"""Onionr - P2P Anonymous Storage Network.
Delete but do not blacklist plaintext blocks
"""
from coredb import blockmetadb
from onionrstorage.removeblock import remove_block
import onionrstorage
from .onionrblockapi import Block
import onionrexceptions
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
def delete_plaintext_no_blacklist():
"""Delete, but do not blacklist, plaintext blocks."""
block_list = blockmetadb.get_block_list()
for block in block_list:
block = Block(hash=block, decrypt=False)
if not block.isEncrypted:
remove_block(block.hash) # delete metadata entry
onionrstorage.deleteBlock(block.hash) # delete block data

View File

@ -1,4 +0,0 @@
from . import main, timeinsert
insert_block = main.insert_block
time_insert = timeinsert.time_insert

View File

@ -1,267 +0,0 @@
"""Onionr - Private P2P Communication.
Create and insert Onionr blocks
"""
from typing import Union
import ujson as json
from gevent import spawn
from onionrutils import bytesconverter, epoch
import filepaths
import onionrstorage
from .. import storagecounter
from onionrplugins import onionrevents as events
from etc import onionrvalues
import config
import onionrcrypto as crypto
import onionrexceptions
from onionrusers import onionrusers
from onionrutils import localcommand, stringvalidators
from .. import blockmetadata
import coredb
from onionrproofs import subprocesspow
import logger
from onionrtypes import UserIDSecretKey
"""
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/>.
"""
storage_counter = storagecounter.StorageCounter()
def _check_upload_queue():
"""
Return the current upload queue len.
raises OverflowError if max, false if api not running
"""
max_upload_queue: int = 5000
queue = localcommand.local_command('/gethidden', max_wait=10)
up_queue = False
try:
up_queue = len(queue.splitlines())
except AttributeError:
pass
else:
if up_queue >= max_upload_queue:
raise OverflowError
return up_queue
def insert_block(data: Union[str, bytes], header: str = 'txt',
sign: bool = False, encryptType: str = '', symKey: str = '',
asymPeer: str = '', meta: dict = {},
expire: Union[int, None] = None, disableForward: bool = False,
signing_key: UserIDSecretKey = '') -> Union[str, bool]:
"""
Create and insert a block into the network.
encryptType must be specified to encrypt a block
if expire is less than date, assumes seconds into future.
if not assume exact epoch
"""
our_private_key = crypto.priv_key
our_pub_key = crypto.pub_key
allocationReachedMessage = 'Cannot insert block, disk allocation reached.'
if storage_counter.is_full():
logger.error(allocationReachedMessage)
raise onionrexceptions.DiskAllocationReached
if signing_key != '':
# if it was specified to use an alternative private key
our_private_key = signing_key
our_pub_key = bytesconverter.bytes_to_str(
crypto.cryptoutils.get_pub_key_from_priv(our_private_key))
retData = False
if type(data) is None:
raise ValueError('Data cannot be none')
createTime = epoch.get_epoch()
dataNonce = bytesconverter.bytes_to_str(crypto.hashers.sha3_hash(data))
try:
with open(filepaths.data_nonce_file, 'r') as nonces:
if dataNonce in nonces:
return retData
except FileNotFoundError:
pass
# record nonce
with open(filepaths.data_nonce_file, 'a') as nonce_file:
nonce_file.write(dataNonce + '\n')
plaintext = data
plaintextMeta = {}
plaintextPeer = asymPeer
retData = ''
signature = ''
signer = ''
metadata = {}
# metadata is full block metadata
# meta is internal, user specified metadata
# only use header if not set in provided meta
meta['type'] = str(header)
if encryptType in ('asym', 'sym'):
metadata['encryptType'] = encryptType
else:
if not config.get('general.store_plaintext_blocks', True):
raise onionrexceptions.InvalidMetadata(
"Plaintext blocks are disabled, " +
"yet a plaintext block was being inserted")
if encryptType not in ('', None):
raise onionrexceptions.InvalidMetadata(
'encryptType must be asym or sym, or blank')
try:
data = data.encode()
except AttributeError:
pass
if encryptType == 'asym':
# Duplicate the time in encrypted messages to help prevent replays
meta['rply'] = createTime
if sign and asymPeer != our_pub_key:
try:
forwardEncrypted = onionrusers.OnionrUser(
asymPeer).forwardEncrypt(data)
data = forwardEncrypted[0]
meta['forwardEnc'] = True
# Expire time of key. no sense keeping block after that
expire = forwardEncrypted[2]
except onionrexceptions.InvalidPubkey:
pass
if not disableForward:
fsKey = onionrusers.OnionrUser(asymPeer).generateForwardKey()
meta['newFSKey'] = fsKey
jsonMeta = json.dumps(meta)
plaintextMeta = jsonMeta
if sign:
signature = crypto.signing.ed_sign(
jsonMeta.encode() + data, key=our_private_key, encodeResult=True)
signer = our_pub_key
if len(jsonMeta) > 1000:
raise onionrexceptions.InvalidMetadata(
'meta in json encoded form must not exceed 1000 bytes')
# encrypt block metadata/sig/content
if encryptType == 'sym':
raise NotImplementedError("not yet implemented")
elif encryptType == 'asym':
if stringvalidators.validate_pub_key(asymPeer):
# Encrypt block data with forward secrecy key first, but not meta
jsonMeta = json.dumps(meta)
jsonMeta = crypto.encryption.pub_key_encrypt(
jsonMeta, asymPeer, encodedData=True).decode()
data = crypto.encryption.pub_key_encrypt(
data, asymPeer, encodedData=False)
signature = crypto.encryption.pub_key_encrypt(
signature, asymPeer, encodedData=True).decode()
signer = crypto.encryption.pub_key_encrypt(
signer, asymPeer, encodedData=True).decode()
try:
onionrusers.OnionrUser(asymPeer, saveUser=True)
except ValueError:
# if peer is already known
pass
else:
logger.warn(f"{asymPeer} is not a valid key to make a block to")
raise onionrexceptions.InvalidPubkey(
'tried to make block to invalid key is not a valid base32 encoded ed25519 key')
# compile metadata
metadata['meta'] = jsonMeta
if len(signature) > 0: # I don't like not pattern
metadata['sig'] = signature
metadata['signer'] = signer
metadata['time'] = createTime
# ensure expire is integer and of sane length
if type(expire) is not type(None): # noqa
if not len(str(int(expire))) < 20:
raise ValueError(
'expire must be valid int less than 20 digits in length')
# if expire is less than date, assume seconds into future
if expire < epoch.get_epoch():
expire = epoch.get_epoch() + expire
metadata['expire'] = expire
# send block data (and metadata) to POW module to get tokenized block data
payload = subprocesspow.SubprocessPOW(data, metadata).start()
if payload != False: # noqa
try:
retData = onionrstorage.set_data(payload)
except onionrexceptions.DiskAllocationReached:
logger.error(allocationReachedMessage)
retData = False
else:
if disableForward:
logger.warn(
f'{retData} asym encrypted block created w/o ephemerality')
"""
Tell the api server through localCommand to wait for the daemon to
upload this block to make statistical analysis more difficult
"""
spawn(
localcommand.local_command,
'/daemon-event/upload_event',
post=True,
is_json=True,
post_data={'block': retData}
).get(timeout=5)
coredb.blockmetadb.add.add_to_block_DB(
retData, selfInsert=True, dataSaved=True)
if expire is None:
coredb.blockmetadb.update_block_info(
retData, 'expire',
createTime + onionrvalues.DEFAULT_EXPIRE)
else:
coredb.blockmetadb.update_block_info(retData, 'expire', expire)
blockmetadata.process_block_metadata(retData)
if retData != False: # noqa
if plaintextPeer == onionrvalues.DENIABLE_PEER_ADDRESS:
events.event('insertdeniable',
{'content': plaintext, 'meta': plaintextMeta,
'hash': retData,
'peer': bytesconverter.bytes_to_str(asymPeer)},
threaded=True)
else:
events.event('insertblock',
{'content': plaintext, 'meta': plaintextMeta,
'hash': retData,
'peer': bytesconverter.bytes_to_str(asymPeer)},
threaded=True)
spawn(
localcommand.local_command,
'/daemon-event/remove_from_insert_queue_wrapper',
post=True,
post_data={'block_hash':
bytesconverter.bytes_to_str(
crypto.hashers.sha3_hash(data))},
is_json=True
).get(timeout=5)
return retData

View File

@ -1,51 +0,0 @@
"""Onionr - Private P2P Communication.
Wrapper to insert blocks with variable delay
"""
from . import main
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
def time_insert(*args, **kwargs):
"""Block insert wrapper to allow for insertions independent of mixmate.
Takes exact args as insert_block, with additional keyword:
delay=n; where n=seconds to tell initial nodes to delay share for.
defaults to 0 or previously set value in current block meta
"""
try:
kwargs['meta']
except KeyError:
kwargs['meta'] = {}
try:
delay = int(kwargs['meta']['dly'])
except KeyError:
delay = 0
try:
delay = kwargs['delay']
del kwargs['delay']
except KeyError:
delay = 0
# Ensure delay >=0
if delay < 0:
raise ValueError('delay cannot be less than 0')
kwargs['meta']['dly'] = delay
return main.insert_block(*args, **kwargs)

View File

@ -1,130 +0,0 @@
"""Onionr - Private P2P Communication.
Handle maintenance of a blacklist database, for blocks and peers
"""
import sqlite3
import os
from onionrplugins.onionrevents import event
import onionrcrypto
from onionrutils import epoch, bytesconverter
from coredb import dbfiles
from etc.onionrvalues import DATABASE_LOCK_TIMEOUT
"""
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 OnionrBlackList:
def __init__(self):
self.blacklistDB = dbfiles.blacklist_db
if not os.path.exists(dbfiles.blacklist_db):
self.generateDB()
return
def inBlacklist(self, data):
hashed = bytesconverter.bytes_to_str(
onionrcrypto.hashers.sha3_hash(data))
retData = False
if not hashed.isalnum():
raise Exception("Hashed data is not alpha numeric")
if len(hashed) > 64:
raise Exception("Hashed data is too large")
for i in self._dbExecute(
"SELECT * FROM blacklist WHERE hash = ?", (hashed,)):
# this only executes if an entry is present by that hash
retData = True
break
return retData
def _dbExecute(self, toExec, params=()):
conn = sqlite3.connect(self.blacklistDB, timeout=DATABASE_LOCK_TIMEOUT)
c = conn.cursor()
retData = c.execute(toExec, params)
conn.commit()
return retData
def deleteBeforeDate(self, date):
# TODO, delete blacklist entries before date
return
def deleteExpired(self, dataType=0):
"""Delete expired entries"""
deleteList = []
curTime = epoch.get_epoch()
try:
int(dataType)
except AttributeError:
raise TypeError("dataType must be int")
for i in self._dbExecute(
'SELECT * FROM blacklist WHERE dataType = ?', (dataType,)):
if i[1] == dataType:
if (curTime - i[2]) >= i[3]:
deleteList.append(i[0])
for thing in deleteList:
self._dbExecute("DELETE FROM blacklist WHERE hash = ?", (thing,))
def generateDB(self):
return
def clearDB(self):
self._dbExecute("""DELETE FROM blacklist;""")
def getList(self):
data = self._dbExecute('SELECT * FROM blacklist')
myList = []
for i in data:
myList.append(i[0])
return myList
def addToDB(self, data, dataType=0, expire=0):
"""Add to the blacklist. Intended to be block hash, block data, peers, or transport addresses
0=block
1=peer
2=pubkey
"""
# we hash the data so we can remove data entirely from our node's disk
hashed = bytesconverter.bytes_to_str(onionrcrypto.hashers.sha3_hash(data))
event('blacklist_add', data={'data': data, 'hash': hashed})
if len(hashed) > 64:
raise Exception("Hashed data is too large")
if not hashed.isalnum():
raise Exception("Hashed data is not alpha numeric")
try:
int(dataType)
except ValueError:
raise Exception("dataType is not int")
try:
int(expire)
except ValueError:
raise Exception("expire is not int")
if self.inBlacklist(hashed):
return
insert = (hashed,)
blacklistDate = epoch.get_epoch()
try:
self._dbExecute("INSERT INTO blacklist (hash, dataType, blacklistDate, expire) VALUES(?, ?, ?, ?);", (str(hashed), dataType, blacklistDate, expire))
except sqlite3.IntegrityError:
pass

View File

@ -1,443 +0,0 @@
"""Onionr - P2P Anonymous Storage Network.
OnionrBlocks class for abstraction of blocks
"""
import datetime
import onionrstorage
import unpaddedbase32
import ujson as json
import nacl.exceptions
import logger
import onionrexceptions
from onionrusers import onionrusers
from onionrutils import stringvalidators, epoch
from coredb import blockmetadb
from onionrutils import bytesconverter
import onionrblocks
from onionrcrypto import encryption, cryptoutils as cryptoutils, signing
"""
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 Block:
blockCacheOrder = list() # NEVER write your own code that writes to this!
blockCache = dict() # should never be accessed directly, look at Block.getCache()
def __init__(self, hash = None, type = None, content = None, expire=None, decrypt=False, bypassReplayCheck=False):
# take from arguments
# sometimes people input a bytes object instead of str in `hash`
if (not hash is None) and isinstance(hash, bytes):
hash = hash.decode()
self.hash = hash
self.btype = type
self.bcontent = content
self.expire = expire
self.bypassReplayCheck = bypassReplayCheck
# initialize variables
self.valid = True
self.raw = None
self.signed = False
self.signature = None
self.signedData = None
self.blockFile = None
self.bheader = {}
self.bmetadata = {}
self.isEncrypted = False
self.decrypted = False
self.signer = None
self.validSig = False
self.autoDecrypt = decrypt
self.claimedTime = None
self.update()
def decrypt(self, encodedData = True):
"""
Decrypt a block, loading decrypted data into their vars
"""
if self.decrypted:
return True
retData = False
# decrypt data
if self.getHeader('encryptType') == 'asym':
try:
self.bcontent = encryption.pub_key_decrypt(self.bcontent, encodedData=False)
bmeta = encryption.pub_key_decrypt(self.bmetadata, encodedData=encodedData)
try:
bmeta = bmeta.decode()
except AttributeError:
# yet another bytes fix
pass
self.bmetadata = json.loads(bmeta)
self.signature = encryption.pub_key_decrypt(self.signature, encodedData=encodedData)
self.signer = encryption.pub_key_decrypt(self.signer, encodedData=encodedData)
self.bheader['signer'] = self.signer.decode()
self.signedData = json.dumps(self.bmetadata).encode() + self.bcontent
if not self.signer is None:
if not self.verifySig():
raise onionrexceptions.SignatureError("Block has invalid signature")
# Check for replay attacks
try:
if epoch.get_epoch() - blockmetadb.get_block_date(self.hash) > 60:
if not cryptoutils.replay_validator(self.bmetadata['rply']): raise onionrexceptions.ReplayAttack
except (AssertionError, KeyError, TypeError, onionrexceptions.ReplayAttack) as e:
if not self.bypassReplayCheck:
# Zero out variables to prevent reading of replays
self.bmetadata = {}
self.signer = ''
self.bheader['signer'] = ''
self.signedData = ''
self.signature = ''
raise onionrexceptions.ReplayAttack('Signature is too old. possible replay attack')
try:
if not self.bmetadata['forwardEnc']: raise KeyError
except (AssertionError, KeyError) as e:
pass
else:
try:
self.bcontent = onionrusers.OnionrUser(self.signer).forwardDecrypt(self.bcontent)
except (onionrexceptions.DecryptionError, nacl.exceptions.CryptoError) as e:
logger.error(str(e))
pass
except (nacl.exceptions.CryptoError,) as e:
logger.debug(f'Could not decrypt block. encodedData: {encodedData}. Either invalid key or corrupted data ' + str(e))
except onionrexceptions.ReplayAttack:
logger.warn('%s is possibly a replay attack' % (self.hash,))
else:
retData = True
self.decrypted = True
return retData
def verifySig(self):
"""
Verify if a block's signature is signed by its claimed signer
"""
if self.signer is None:
return False
if signing.ed_verify(data=self.signedData, key=self.signer, sig=self.signature, encodedData=True):
self.validSig = True
else:
self.validSig = False
return self.validSig
def update(self, data = None, file = None):
"""
Loads data from a block in to the current object.
Inputs:
- data (str):
- if None: will load from file by hash
- else: will load from `data` string
- file (str):
- if None: will load from file specified in this parameter
- else: will load from wherever block is stored by hash
Outputs:
- (bool): indicates whether or not the operation was successful
"""
try:
# import from string
blockdata = data
# import from file
if blockdata is None:
try:
blockdata = onionrstorage.getData(self.getHash())#.decode()
except AttributeError:
raise onionrexceptions.NoDataAvailable('Block does not exist')
else:
self.blockFile = None
# parse block
self.raw = blockdata
self.bheader = json.loads(self.getRaw()[:self.getRaw().index(b'\n')])
self.bcontent = self.getRaw()[self.getRaw().index(b'\n') + 1:]
if ('encryptType' in self.bheader) and (self.bheader['encryptType'] in ('asym', 'sym')):
self.bmetadata = self.getHeader('meta', None)
self.isEncrypted = True
else:
self.bmetadata = json.loads(self.getHeader('meta', None))
self.btype = self.getMetadata('type', None)
self.signed = ('sig' in self.getHeader() and self.getHeader('sig') != '')
# TODO: detect if signer is hash of pubkey or not
self.signer = self.getHeader('signer', None)
self.signature = self.getHeader('sig', None)
# signed data is jsonMeta + block content (no linebreak)
self.signedData = (None if not self.isSigned() else self.getHeader('meta').encode() + self.getContent())
self.date = blockmetadb.get_block_date(self.getHash())
self.claimedTime = self.getHeader('time', None)
if not self.getDate() is None:
self.date = datetime.datetime.fromtimestamp(self.getDate())
self.valid = True
if self.autoDecrypt:
self.decrypt()
return True
except Exception as e:
logger.warn('Failed to parse block %s' % self.getHash(), error = e, timestamp = False)
self.valid = False
return False
# getters
def getExpire(self):
"""
Returns the expire time for a block
Outputs:
- (int): the expire time for a block, or None
"""
return self.expire
def getHash(self):
"""
Returns the hash of the block if saved to file
Outputs:
- (str): the hash of the block, or None
"""
return self.hash
def getType(self):
"""
Returns the type of the block
Outputs:
- (str): the type of the block
"""
return self.btype
def getRaw(self):
"""
Returns the raw contents of the block, if saved to file
Outputs:
- (bytes): the raw contents of the block, or None
"""
return self.raw
def getHeader(self, key = None, default = None):
"""
Returns the header information
Inputs:
- key (str): only returns the value of the key in the header
Outputs:
- (dict/str): either the whole header as a dict, or one value
"""
if not key is None:
if key in self.getHeader():
return self.getHeader()[key]
return default
return self.bheader
def getMetadata(self, key = None, default = None):
"""
Returns the metadata information
Inputs:
- key (str): only returns the value of the key in the metadata
Outputs:
- (dict/str): either the whole metadata as a dict, or one value
"""
if not key is None:
if key in self.getMetadata():
return self.getMetadata()[key]
return default
return self.bmetadata
def getContent(self):
"""
Returns the contents of the block
Outputs:
- (str): the contents of the block
"""
return self.bcontent
def getDate(self):
"""
Returns the date that the block was received, if loaded from file
Outputs:
- (datetime): the date that the block was received
"""
return self.date
def getBlockFile(self):
"""
Returns the location of the block file if it is saved
Outputs:
- (str): the location of the block file, or None
"""
return self.blockFile
def isValid(self):
"""
Checks if the block is valid
Outputs:
- (bool): whether or not the block is valid
"""
return self.valid
def isSigned(self):
"""
Checks if the block was signed
Outputs:
- (bool): whether or not the block is signed
"""
return self.signed
def getSignature(self):
"""
Returns the base64-encoded signature
Outputs:
- (str): the signature, or None
"""
return self.signature
def getSignedData(self):
"""
Returns the data that was signed
Outputs:
- (str): the data that was signed, or None
"""
return self.signedData
def isSigner(self, signer, encodedData = True):
"""
Checks if the block was signed by the signer inputted
Inputs:
- signer (str): the public key of the signer to check against
- encodedData (bool): whether or not the `signer` argument is base64 encoded
Outputs:
- (bool): whether or not the signer of the block is the signer inputted
"""
signer = unpaddedbase32.repad(bytesconverter.str_to_bytes(signer))
try:
if (not self.isSigned()) or (not stringvalidators.validate_pub_key(signer)):
return False
return bool(signing.ed_verify(self.getSignedData(), signer, self.getSignature(), encodedData = encodedData))
except:
return False
# setters
def setType(self, btype):
"""
Sets the type of the block
Inputs:
- btype (str): the type of block to be set to
Outputs:
- (Block): the Block instance
"""
self.btype = btype
return self
def setMetadata(self, key, val):
"""
Sets a custom metadata value
Metadata should not store block-specific data structures.
Inputs:
- key (str): the key
- val: the value (type is irrelevant)
Outputs:
- (Block): the Block instance
"""
self.bmetadata[key] = val
return self
def setContent(self, bcontent):
"""
Sets the contents of the block
Inputs:
- bcontent (str): the contents to be set to
Outputs:
- (Block): the Block instance
"""
self.bcontent = str(bcontent)
return self
# static functions
def exists(bHash):
"""
Checks if a block is saved to file or not
Inputs:
- hash (str/Block):
- if (Block): check if this block is saved to file
- if (str): check if a block by this hash is in file
Outputs:
- (bool): whether or not the block file exists
"""
# no input data? scrap it.
if bHash is None:
return False
if isinstance(bHash, Block):
bHash = bHash.getHash()
ret = isinstance(onionrstorage.getData(bHash), type(None))
return not ret

View File

@ -1,96 +0,0 @@
"""
Onionr - Private P2P Communication.
Keep track of how much disk space we're using
"""
from pathlib import Path
from threading import Thread
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
import config
from filepaths import usage_file
"""
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/>.
"""
config.reload()
def _read_data_file(f) -> int:
amount = 0
try:
with open(f, 'r') as f:
amount = int(f.read())
except FileNotFoundError:
pass
except ValueError:
pass # Possibly happens when the file is empty
return amount
class StorageCounter:
def __init__(self):
self.data_file = usage_file
self.amount: int = None
Path(self.data_file).touch()
def auto_refresher():
class Refresher(FileSystemEventHandler):
@staticmethod
def on_modified(event):
self.amount = _read_data_file(self.data_file)
observer = Observer()
observer.schedule(Refresher(), usage_file)
observer.start()
while observer.is_alive():
# call import func with timeout
observer.join(120)
Thread(target=auto_refresher, daemon=True).start()
self.amount = _read_data_file(self.data_file)
def is_full(self) -> bool:
"""Returns if the allocated disk space is full (this is Onionr config,
not true FS capacity)"""
ret_data = False
if config.get('allocations.disk', 1073741824) <= (self.amount + 1000):
ret_data = True
return ret_data
def _update(self, data):
with open(self.data_file, 'w') as data_file:
data_file.write(str(data))
def get_percent(self) -> int:
"""Return percent (decimal/float) of disk space we're using"""
amount = self.amount
return round(amount / config.get('allocations.disk', 2000000000), 2)
def add_bytes(self, amount) -> int:
"""Record that we are now using more disk space,
unless doing so would exceed configured max"""
new_amount = amount + self.amount
ret_data = new_amount
if new_amount > config.get('allocations.disk', 2000000000):
ret_data = 0
else:
self._update(new_amount)
return ret_data
def remove_bytes(self, amount) -> int:
"""Record that we are now using less disk space"""
new_amount = self.amount - amount
self._update(new_amount)
return new_amount

View File

@ -1,56 +0,0 @@
"""Onionr - Private P2P Communication.
This file contains the command for banning blocks from the node
"""
import sys
import logger
from onionrutils import stringvalidators
from onionrstorage import removeblock
from onionrstorage import deleteBlock
from onionrblocks import onionrblacklist
from utils import reconstructhash
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
def ban_block():
"""Delete a block, permanently blacklisting it."""
blacklist = onionrblacklist.OnionrBlackList()
try:
ban = sys.argv[2]
except IndexError:
# Get the hash if its not provided as a CLI argument
ban = logger.readline('Enter a block hash:').strip()
# Make sure the hash has no truncated zeroes
ban = reconstructhash.reconstruct_hash(ban)
if stringvalidators.validate_hash(ban):
if not blacklist.inBlacklist(ban):
try:
blacklist.addToDB(ban)
removeblock.remove_block(ban)
deleteBlock(ban)
except Exception as error: # pylint: disable=W0703
logger.error('Could not blacklist block',
error=error, terminal=True)
else:
logger.info('Block blacklisted', terminal=True)
else:
logger.warn('That block is already blacklisted', terminal=True)
else:
logger.error('Invalid block hash', terminal=True)
ban_block.onionr_help = "<block hash>: " # type: ignore
ban_block.onionr_help += "deletes and blacklists a block" # type: ignore

View File

@ -35,7 +35,6 @@ from utils.bettersleep import better_sleep
from .killdaemon import kill_daemon # noqa
from .showlogo import show_logo
from sneakernet import sneakernet_import_thread
from setupkvvars import setup_kv
"""
This program is free software: you can redistribute it and/or modify
@ -142,9 +141,6 @@ def daemon():
events.event('init', threaded=False)
events.event('daemon_start')
if config.get('transports.sneakernet', True):
Thread(target=sneakernet_import_thread, daemon=True).start()
better_sleep(5)

View File

@ -1,59 +0,0 @@
"""Onionr - Private P2P Communication.
This file handles the command for exporting blocks to disk
"""
import sys
import logger
import onionrstorage
from utils import createdirs
from onionrutils import stringvalidators
from etc.onionrvalues import BLOCK_EXPORT_FILE_EXT
import filepaths
import os
from coredb import blockmetadb
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
def _do_export(b_hash):
createdirs.create_dirs()
data = onionrstorage.getData(b_hash)
with open('%s/%s%s' % (filepaths.export_location,
b_hash, BLOCK_EXPORT_FILE_EXT), 'wb') as export:
export.write(data)
logger.info('Block exported as file', terminal=True)
def export_block(*args):
"""Export block based on hash from stdin or argv."""
if args:
b_hash = args[0]
else:
try:
if not stringvalidators.validate_hash(sys.argv[2]):
raise ValueError
except (IndexError, ValueError):
logger.error('No valid block hash specified.', terminal=True)
sys.exit(1)
else:
b_hash = sys.argv[2]
_do_export(b_hash)
export_block.onionr_help = "<block hash>: Export block to " # type: ignore
export_block.onionr_help += "a file. Export directory is in " # type: ignore
export_block.onionr_help += "Onionr home under block-export" # type: ignore

View File

@ -1,106 +0,0 @@
"""Onionr - Private P2P Communication.
This file handles the commands for adding
and getting files from the Onionr network
"""
import sys
import os
import logger
from onionrblocks.onionrblockapi import Block
import onionrexceptions
from onionrutils import stringvalidators
from etc import onionrvalues
from onionrblocks import insert
"""
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/>.
"""
_ORIG_DIR = onionrvalues.ORIG_RUN_DIR_ENV_VAR
def _get_dir(path: str) -> str:
if not os.getenv(_ORIG_DIR) is None:
return os.getenv(_ORIG_DIR) + '/' + path # type: ignore
else:
return path
def add_html(singleBlock=True, blockType='html'):
"""Create one-off web page from HTML file, no ext resources."""
add_file(blockType=blockType)
add_html.onionr_help = "Adds an HTML file into Onionr. Does " # type: ignore
add_html.onionr_help += "not include dependant resources" # type: ignore
def add_file(blockType='bin'):
"""Add a file to the onionr network."""
if len(sys.argv) >= 3:
filename = sys.argv[2]
if not os.path.exists(_get_dir(filename)):
logger.error(
'That file does not exist. Improper path (specify full path)?',
terminal=True)
return
logger.info('Adding file, this might take a long time.',
terminal=True)
try:
with open(_get_dir(filename), 'rb') as single_file:
blockhash = insert(single_file.read(), header=blockType)
if len(blockhash) > 0:
logger.info('File %s saved in block %s' %
(filename, blockhash), terminal=True)
except Exception as err: # pylint: disable=W0703
logger.error('Failed to save file in block ' +
str(err), timestamp=False, terminal=True)
else:
logger.error('%s add-file <filename>' %
sys.argv[0], timestamp=False, terminal=True)
add_file.onionr_help = "<file path> Add a file into " # type: ignore
add_file.onionr_help += "the Onionr network" # type: ignore
def get_file():
"""Get a file from onionr blocks."""
try:
file_name = _get_dir(sys.argv[2])
bHash = sys.argv[3]
except IndexError:
logger.error("Syntax %s %s" % (
sys.argv[0], '/path/to/filename <blockhash>'), terminal=True)
else:
logger.info(file_name, terminal=True)
if os.path.exists(file_name):
logger.error("File already exists", terminal=True)
return
if not stringvalidators.validate_hash(bHash):
logger.error('Block hash is invalid', terminal=True)
return
try:
with open(file_name, 'wb') as my_file:
my_file.write(Block(bHash).bcontent)
except onionrexceptions.NoDataAvailable:
logger.error(
'That block is not available. Trying again later may work.',
terminal=True)
get_file.onionr_help = "<file path> <block hash>: Download " # type: ignore
get_file.onionr_help += "a file from the onionr network." # type: ignore

View File

@ -1,37 +0,0 @@
"""Onionr - Private P2P Communication.
Dumb listing of Onionr sites
"""
from coredb.blockmetadb import get_blocks_by_type
from onionrblocks.onionrblockapi import Block
import logger
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
def print_site_list():
"""Create a new MOTD message for the Onionr network."""
block_list = get_blocks_by_type('osite')
if not block_list:
logger.info('No sites saved right now', terminal=True)
for block in block_list:
block = Block(block)
if block.isSigned():
logger.info(block.signer.replace('=', ''), terminal=True)
else:
logger.info(block.hash, terminal=True)
print_site_list.onionr_help = "Dumbly list all Onionr sites currently saved" # type: ignore

View File

@ -4,10 +4,9 @@ This module defines commands to show stats/details about the local node
"""
import os
import logger
from onionrblocks import onionrblacklist
from onionrutils import mnemonickeys
from utils import sizeutils, getconsolewidth, identifyhome
from coredb import blockmetadb, keydb
from coredb import keydb
import onionrcrypto
import config
from etc import onionrvalues
@ -43,9 +42,8 @@ def show_stats():
"""Print/log statistic info about our Onionr install."""
try:
# define stats messages here
totalBlocks = len(blockmetadb.get_block_list())
home = identifyhome.identify_home()
totalBanned = len(onionrblacklist.OnionrBlackList().getList())
messages = {
# info about local client
@ -69,9 +67,7 @@ def show_stats():
'div2': True,
'Enabled Plugins':
str(len(config.get('plugins.enabled', list()))) + ' / ' +
str(len(os.listdir(home + 'plugins/'))),
'Stored Blocks': str(totalBlocks),
'Deleted Blocks': str(totalBanned)
str(len(os.listdir(home + 'plugins/')))
}
# color configuration

View File

@ -6,19 +6,14 @@ from typing import Callable
from .. import onionrstatistics, version, daemonlaunch
from .. import openwebinterface
from .. import banblocks # Command to blacklist a block by its hash
from .. import filecommands # commands to share files with onionr
from .. import exportblocks # commands to export blocks
from .. import pubkeymanager # commands to add or change id
from .. import resetplugins # command to reinstall default plugins
from .. import softreset # command to delete onionr blocks
from .. import restartonionr # command to restart Onionr
from .. import runtimetestcmd # cmd to execute the runtime integration tests
from .. import sitecreator # cmd to create multi-page sites
from ..listsites import print_site_list # cmd to list list ids
import onionrexceptions
from onionrutils import importnewblocks # func to import new blocks
"""
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
@ -42,8 +37,6 @@ def get_arguments() -> dict:
dynamically modify them with plugins
"""
args = {
('blacklist', 'blacklist-block', 'remove-block',
'removeblock', 'banblock', 'ban-block'): banblocks.ban_block,
('details', 'info'): onionrstatistics.show_details,
('stats', 'statistics'): onionrstatistics.show_stats,
('version',): version.version,
@ -53,15 +46,6 @@ def get_arguments() -> dict:
('openhome', 'gui', 'openweb',
'open-home', 'open-web'): openwebinterface.open_home,
('get-url', 'url', 'get-web'): openwebinterface.get_url,
('addhtml', 'add-html'): filecommands.add_html,
('addsite', 'add-site',
'update-site', 'updatesite'): sitecreator.create_multipage_site,
('listsites', 'list-sites'): print_site_list,
('addfile', 'add-file'): filecommands.add_file,
('get-file', 'getfile'): filecommands.get_file,
('export-block', 'exportblock'): exportblocks.export_block,
('importblocks',
'import-blocks', 'import-block'): importnewblocks.import_new_blocks,
('addid', 'add-id'): pubkeymanager.add_ID,
('changeid', 'change-id'): pubkeymanager.change_ID,
('add-vanity', 'addvanity'): pubkeymanager.add_vanity,

View File

@ -1,72 +0,0 @@
"""Onionr - Private P2P Communication.
Command to create Onionr mutli-page sites
"""
import sys
import os
import getpass
from niceware import generate_passphrase
from httpapi import onionrsitesapi
import logger
from etc import onionrvalues
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
def create_multipage_site():
"""Command to create mutlipage sites with specified dir and password."""
error_encountered = False
orig_dir = os.getcwd()
try:
directory = sys.argv[2]
os.chdir(directory)
directory = '.'
except IndexError:
directory = '.'
try:
passphrase = sys.argv[3]
except IndexError:
logger.warn('''It is critical that this passphrase is long.
If you want to update your site later you must remember the passphrase.''',
terminal=True)
passphrase = "-".join(generate_passphrase(32))
print("Site restore phrase:", passphrase)
if len(passphrase) < onionrvalues.PASSWORD_LENGTH:
error_encountered = True
logger.error(
f'Passphrase must be at least {onionrvalues.PASSWORD_LENGTH}' +
' characters.', terminal=True)
if error_encountered:
sys.exit(1)
logger.info('Generating site...', terminal=True)
results = onionrsitesapi.sitefiles.create_site(
passphrase, directory=directory)
results = (results[0].replace('=', ''), results[1])
logger.info(f'Site address {results[0]}', terminal=True)
logger.info(f'Block for this version {results[1]}', terminal=True)
os.chdir(orig_dir)
create_multipage_site.onionr_help = "[directory path " # type: ignore
create_multipage_site.onionr_help += "(default relative)] " # type: ignore
create_multipage_site.onionr_help += "- packages a whole " # type: ignore
create_multipage_site.onionr_help += "directory and makes " # type: ignore
create_multipage_site.onionr_help += "it available as " # type: ignore
create_multipage_site.onionr_help += "an Onionr site." # type: ignore

View File

@ -1,7 +1,6 @@
from . import safecompare, replayvalidation, verifypow
from . import safecompare, replayvalidation
from . import getpubfrompriv
replay_validator = replayvalidation.replay_timestamp_validation
safe_compare = safecompare.safe_compare
verify_POW = verifypow.verify_POW
get_pub_key_from_priv = getpubfrompriv.get_pub_key_from_priv

View File

@ -1,67 +0,0 @@
"""Onionr - Private P2P Communication.
Proof of work module
"""
import multiprocessing, time, math, threading, binascii, sys, json
import nacl.encoding, nacl.hash, nacl.utils
import config
import logger
from onionrblocks import onionrblockapi
from onionrutils import bytesconverter
from onionrcrypto import hashers
from .blocknoncestart import BLOCK_NONCE_START_INT
from .vdf import create_vdf
"""
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/>.
"""
config.reload()
def getDifficultyForNewBlock(data):
"""
Get difficulty for block. Accepts size in integer, Block instance, or str/bytes full block contents
"""
if isinstance(data, onionrblockapi.Block):
dataSizeInBytes = len(bytesconverter.str_to_bytes(data.getRaw()))
else:
dataSizeInBytes = len(bytesconverter.str_to_bytes(data))
minDifficulty = config.get('general.minimum_send_pow', 4)
totalDifficulty = max(minDifficulty, math.floor(dataSizeInBytes / 1000000.0))
return totalDifficulty
def getHashDifficulty(h: str):
"""
Return the amount of leading zeroes in a hex hash string (hexHash)
"""
return len(h) - len(h.lstrip('0'))
def hashMeetsDifficulty(hexHash):
"""
Return bool for a hash string to see if it meets pow difficulty defined in config
"""
hashDifficulty = getHashDifficulty(hexHash)
try:
expected = int(config.get('general.minimum_block_pow'))
except TypeError:
raise ValueError('Missing general.minimum_block_pow config')
return hashDifficulty >= expected

View File

@ -1 +0,0 @@
BLOCK_NONCE_START_INT = -10000000

View File

@ -1,149 +0,0 @@
#!/usr/bin/env python3
"""Onionr - Private P2P Communication.
Multiprocess proof of work
"""
import os
from multiprocessing import Pipe, Process
import threading
import time
import secrets
import onionrproofs
import ujson as json
import logger
from onionrutils import bytesconverter
from onionrcrypto.hashers import sha3_hash
"""
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 SubprocessPOW:
def __init__(self, data, metadata, subproc_count=None):
"""
Onionr proof of work using multiple processes
Accepts block data, block metadata
if subproc_count is not set,
os.cpu_count() is used to determine the number of processes
Due to Python GIL multiprocessing/use of external libraries
is necessary to accelerate CPU bound tasks
"""
# No known benefit to using more processes than there are cores.
# Note: os.cpu_count perhaps not always accurate
if subproc_count is None:
subproc_count = os.cpu_count()
self.subproc_count = subproc_count
self.result = ''
self.shutdown = False
self.data = data
self.metadata = metadata
"""dump dict to measure bytes of json metadata
Cannot reuse later bc the pow token must be added
"""
json_metadata = json.dumps(metadata).encode()
self.data = bytesconverter.str_to_bytes(data)
compiled_data = bytes(json_metadata + b'\n' + self.data)
# Calculate difficulty. May use better algorithm in the future.
self.difficulty = onionrproofs.getDifficultyForNewBlock(compiled_data)
logger.info('Computing POW (difficulty: %s)...' % (self.difficulty,))
self.main_hash = '0' * 64
self.puzzle = self.main_hash[0:min(self.difficulty,
len(self.main_hash))]
self.shutdown = False
self.payload = None
def start(self):
"""spawn the multiproc handler threads"""
# Create a new thread for each subprocess
for _ in range(self.subproc_count): # noqa
threading.Thread(target=self._spawn_proc, daemon=True).start()
# Monitor the processes for a payload, shut them down when its found
while True:
if self.payload is None:
time.sleep(0.1)
else:
self.shutdown = True
return self.payload
def _spawn_proc(self):
"""Create a child proof of work process
wait for data and send shutdown signal when its found"""
# The importerror started happening in 3.9.x
# not worth fixing because this POW will be replaced by VDF
try:
parent_conn, child_conn = Pipe()
p = Process(target=self.do_pow, args=(child_conn,), daemon=True)
p.start()
except ImportError:
logger.error(
"Error in subprocess module when getting new POW " +
"pipe.\nThis is related to a problem in 3.9.x", terminal=True)
return
payload = None
try:
while True:
data = parent_conn.recv()
if len(data) >= 1:
payload = data
break
except KeyboardInterrupt:
pass
finally:
p.terminate()
self.payload = payload
def do_pow(self, pipe):
"""find partial hash colision generating nonce for a block"""
nonce = 0
data = self.data
metadata = self.metadata
metadata['n'] = secrets.randbits(16)
puzzle = self.puzzle
difficulty = self.difficulty
try:
while True:
#logger.info('still running', terminal=True)
# Break if shutdown received
try:
if pipe.poll() and pipe.recv() == 'shutdown':
break
except KeyboardInterrupt:
break
# Load nonce into block metadata
metadata['c'] = nonce
# Serialize metadata, combine with block data
payload = json.dumps(metadata).encode() + b'\n' + data
# Check sha3_256 hash of block, compare to puzzle
# Send payload if puzzle finished
token = sha3_hash(payload)
# ensure token is string
token = bytesconverter.bytes_to_str(token)
if puzzle == token[0:difficulty]:
pipe.send(payload)
break
nonce += 1
except KeyboardInterrupt:
pass

View File

@ -1,43 +0,0 @@
import multiprocessing
import mimcvdf
def _wrap_vdf_create(queue, block_data_bytes, rounds):
queue.put(mimcvdf.vdf_create(block_data_bytes, rounds))
def _wrap_vdf_verify(queue, block_data_bytes, block_hash_hex, rounds):
queue.put(mimcvdf.vdf_verify(block_data_bytes, block_hash_hex, rounds))
def rounds_for_bytes(byte_count: int):
return byte_count * 1000
def create_vdf(block_data_bytes):
rounds = rounds_for_bytes(block_data_bytes)
queue = multiprocessing.Queue()
vdf_proc = multiprocessing.Process(
target=_wrap_vdf_create,
args=(queue, block_data_bytes, rounds))
vdf_proc.start()
vdf_proc.join()
return queue.get()
def verify_vdf(block_hash_hex, block_data_bytes):
rounds = rounds_for_bytes(block_data_bytes)
if rounds < 10 ** 6:
# >million rounds it starts to take long enough to warrant a subprocess
queue = multiprocessing.Queue()
vdf_proc = multiprocessing.Process(
target=_wrap_vdf_verify,
args=(queue, block_data_bytes, block_hash_hex, rounds))
vdf_proc.start()
vdf_proc.join()
return queue.get()
return mimcvdf.vdf_verify(block_data_bytes, block_hash_hex, rounds)

View File

@ -77,57 +77,6 @@ def createPeerDB():
conn.close()
return
def createBlockDB():
'''
Create a database for blocks
hash - the hash of a block
dateReceived - the date the block was recieved, not necessarily when it was created
decrypted - if we can successfully decrypt the block (does not describe its current state)
dataType - data type of the block
dataFound - if the data has been found for the block
dataSaved - if the data has been saved for the block
sig - optional signature by the author (not optional if author is specified)
author - multi-round partial sha3-256 hash of authors public key
dateClaimed - timestamp claimed inside the block, only as trustworthy as the block author is
expire int - block expire date in epoch
'''
if os.path.exists(dbfiles.block_meta_db):
raise FileExistsError("Block database already exists")
conn = sqlite3.connect(dbfiles.block_meta_db)
c = conn.cursor()
c.execute('''CREATE TABLE hashes(
hash text not null,
dateReceived int,
decrypted int,
dataType text,
dataFound int,
dataSaved int,
sig text,
author text,
dateClaimed int,
expire int
);
''')
conn.commit()
conn.close()
return
def createBlockDataDB():
if os.path.exists(dbfiles.block_data_db):
raise FileExistsError("Block data database already exists")
else:
if not os.path.exists(filepaths.block_data_location):
os.mkdir(filepaths.block_data_location)
conn = sqlite3.connect(dbfiles.block_data_db)
c = conn.cursor()
c.execute('''CREATE TABLE blockData(
hash text not null,
data blob not null
);
''')
conn.commit()
conn.close()
def createForwardKeyDB():
'''
@ -168,5 +117,4 @@ def create_blacklist_db():
create_funcs = [createAddressDB, createPeerDB,
createBlockDB, createBlockDataDB,
createForwardKeyDB, create_blacklist_db]

View File

@ -1,116 +0,0 @@
"""Onionr - Private P2P Communication.
Handle block storage, providing an abstraction for
storing blocks between file system and database
"""
import sys
import sqlite3
import os
from onionrutils import bytesconverter
from onionrutils import stringvalidators
from coredb import dbfiles
from filepaths import block_data_location
import onionrexceptions
from onionrcrypto import hashers
from . import setdata, removeblock
from etc.onionrvalues import DATABASE_LOCK_TIMEOUT, BLOCK_EXPORT_FILE_EXT
"""
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/>.
"""
DB_ENTRY_SIZE_LIMIT = 10000 # Will be a config option
set_data = setdata.set_data
def _dbInsert(block_hash, data):
conn = sqlite3.connect(dbfiles.block_data_db,
timeout=DATABASE_LOCK_TIMEOUT)
c = conn.cursor()
data = (block_hash, data)
c.execute('INSERT INTO blockData (hash, data) VALUES(?, ?);', data)
conn.commit()
conn.close()
def _dbFetch(block_hash):
conn = sqlite3.connect(dbfiles.block_data_db,
timeout=DATABASE_LOCK_TIMEOUT)
c = conn.cursor()
for i in c.execute(
'SELECT data from blockData where hash = ?', (block_hash,)):
return i[0]
conn.commit()
conn.close()
return None
def deleteBlock(block_hash):
# Call removeblock.remove_block to automatically want to remove storage byte count
if os.path.exists(f'{block_data_location}/{block_hash}{BLOCK_EXPORT_FILE_EXT}'):
os.remove(f'{block_data_location}/{block_hash}{BLOCK_EXPORT_FILE_EXT}')
return True
conn = sqlite3.connect(dbfiles.block_data_db,
timeout=DATABASE_LOCK_TIMEOUT)
c = conn.cursor()
data = (block_hash,)
c.execute('DELETE FROM blockData where hash = ?', data)
conn.commit()
conn.close()
return True
def store(data, block_hash=''):
if not stringvalidators.validate_hash(block_hash):
raise ValueError
ourHash = hashers.sha3_hash(data)
if block_hash != '':
if not ourHash == block_hash:
raise ValueError('Hash specified does not meet internal hash check')
else:
block_hash = ourHash
if DB_ENTRY_SIZE_LIMIT >= sys.getsizeof(data):
_dbInsert(block_hash, data)
else:
with open(
f'{block_data_location}/{block_hash}{BLOCK_EXPORT_FILE_EXT}', 'wb') as blck_file:
blck_file.write(data)
def getData(bHash):
if not stringvalidators.validate_hash(bHash):
raise ValueError
bHash = bytesconverter.bytes_to_str(bHash)
bHash = bHash.strip()
# First check DB for data entry by hash
# if no entry, check disk
# If no entry in either, raise an exception
ret_data = None
fileLocation = '%s/%s%s' % (
block_data_location,
bHash, BLOCK_EXPORT_FILE_EXT)
not_found_msg = "Block data not found for: " + str(bHash)
if os.path.exists(fileLocation):
with open(fileLocation, 'rb') as block:
ret_data = block.read()
else:
ret_data = _dbFetch(bHash)
if ret_data is None:
raise onionrexceptions.NoDataAvailable(not_found_msg)
return ret_data

View File

@ -1,52 +0,0 @@
"""Onionr - Private P2P Communication.
remove onionr block from meta db
"""
import sys
import sqlite3
import onionrexceptions
import onionrstorage
from onionrutils import stringvalidators
from coredb import dbfiles
from onionrblocks import storagecounter
from etc.onionrvalues import DATABASE_LOCK_TIMEOUT
"""
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/>.
"""
storage_counter = storagecounter.StorageCounter()
def remove_block(block):
"""Remove a block from this node.
(does not automatically blacklist).
**You may want blacklist.addToDB(blockHash)
"""
if stringvalidators.validate_hash(block):
try:
data_size = sys.getsizeof(onionrstorage.getData(block))
except onionrexceptions.NoDataAvailable:
data_size = 0
conn = sqlite3.connect(
dbfiles.block_meta_db, timeout=DATABASE_LOCK_TIMEOUT)
c = conn.cursor()
t = (block,)
c.execute('Delete from hashes where hash=?;', t)
conn.commit()
conn.close()
if data_size:
storage_counter.remove_bytes(data_size)
else:
raise onionrexceptions.InvalidHexHash

View File

@ -1,70 +0,0 @@
"""Onionr - Private P2P Communication.
Test Onionr as it is running
"""
import sys
import sqlite3
import onionrstorage
import onionrexceptions
import onionrcrypto as crypto
import filepaths
from onionrblocks import storagecounter, blockmetadata
from coredb import dbfiles
from onionrutils import bytesconverter
from etc.onionrvalues import DATABASE_LOCK_TIMEOUT
from onionrtypes import BlockHash
"""
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/>.
"""
storage_counter = storagecounter.StorageCounter()
def set_data(data):
"""Set the data assciated with a hash."""
dataSize = sys.getsizeof(data)
nonce_hash = crypto.hashers.sha3_hash(
bytesconverter.str_to_bytes(
blockmetadata.fromdata.get_block_metadata_from_data(data)[2]))
nonce_hash = bytesconverter.bytes_to_str(nonce_hash)
if not type(data) is bytes:
data = data.encode()
dataHash = crypto.hashers.sha3_hash(data)
if type(dataHash) is bytes:
dataHash = dataHash.decode()
try:
onionrstorage.getData(dataHash)
except onionrexceptions.NoDataAvailable:
if storage_counter.add_bytes(dataSize):
onionrstorage.store(data, block_hash=dataHash)
conn = sqlite3.connect(
dbfiles.block_meta_db, timeout=DATABASE_LOCK_TIMEOUT)
c = conn.cursor()
c.execute(
"UPDATE hashes SET dataSaved=1 WHERE hash = ?;",
(dataHash,))
conn.commit()
conn.close()
with open(filepaths.data_nonce_file, 'a') as nonceFile:
nonceFile.write(nonce_hash + '\n')
else:
raise onionrexceptions.DiskAllocationReached
else:
raise onionrexceptions.DataExists(
"Data is already set for " + dataHash)
return dataHash

View File

@ -1,60 +0,0 @@
"""Onionr - Private P2P Communication.
import new blocks from disk, providing transport agnosticism
"""
import glob
import logger
from onionrblocks import blockmetadata
from coredb import blockmetadb
import filepaths
import onionrcrypto as crypto
from etc.onionrvalues import BLOCK_EXPORT_FILE_EXT
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
def import_new_blocks(scanDir=''):
"""Scan for new blocks ON THE DISK and import them"""
blockList = blockmetadb.get_block_list()
exist = False
if scanDir == '':
scanDir = filepaths.block_data_location
if not scanDir.endswith('/'):
scanDir += '/'
for block in glob.glob(scanDir + "*%s" % (BLOCK_EXPORT_FILE_EXT,)):
if block.replace(scanDir, '').replace(BLOCK_EXPORT_FILE_EXT, '') \
not in blockList:
exist = True
logger.info('Found new block on dist %s' % block, terminal=True)
with open(block, 'rb') as newBlock:
block = block.replace(scanDir, '').replace(
BLOCK_EXPORT_FILE_EXT, '')
if crypto.hashers.sha3_hash(newBlock.read()) == block.replace(
BLOCK_EXPORT_FILE_EXT, ''):
blockmetadb.add_to_block_DB(block.replace(
BLOCK_EXPORT_FILE_EXT, ''), dataSaved=True)
logger.info('Imported block %s' % block, terminal=True)
blockmetadata.process_block_metadata(block)
else:
logger.warn('Failed to verify hash for %s' % block,
terminal=True)
if not exist:
logger.info('No blocks found to import', terminal=True)
import_new_blocks.onionr_help = \
f"Scan the Onionr data directory under {filepaths.block_data_location}" + \
"for new block files (.db not supported) to import"

View File

@ -8,12 +8,9 @@ from secrets import SystemRandom
import logger
from onionrutils import epoch
from . import uicheck, inserttest, stresstest
from . import uicheck
from .webpasstest import webpass_test
from .osver import test_os_ver_endpoint
from .clearnettor import test_clearnet_tor_request
from .housekeeping import test_inserted_housekeeping
from .sneakernettest import test_sneakernet_import
from .dnsrebindingtest import test_dns_rebinding
"""
This program is free software: you can redistribute it and/or modify
@ -31,14 +28,8 @@ from .dnsrebindingtest import test_dns_rebinding
"""
RUN_TESTS = [uicheck.check_ui,
inserttest.insert_bin_test,
stresstest.stress_test_block_insert,
webpass_test,
test_os_ver_endpoint,
test_clearnet_tor_request,
test_inserted_housekeeping,
sneakernettest.test_sneakernet_import,
test_dns_rebinding
]
SUCCESS_FILE = os.path.dirname(os.path.realpath(__file__)) + '/../../tests/runtime-result.txt'

View File

@ -1,58 +0,0 @@
"""Onionr - Private P2P Communication.
Ensure that clearnet cannot be reached
"""
from onionrutils.basicrequests import do_get_request
from onionrutils import localcommand
import logger
import config
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
def test_clearnet_tor_request(testmanager):
"""Ensure that Tor cannot request clearnet address.
Does not run if Tor is being reused
"""
config.reload()
leak_result = ""
if config.get('tor.use_existing_tor', False):
logger.warn(
"Can't ensure Tor reqs to clearnet won't happen when reusing Tor")
return
socks_port = localcommand.local_command('/gettorsocks')
# Don't worry, this request isn't meant to go through,
# but if it did it would be through Tor
try:
leak_result: str = do_get_request(
'https://example.com/notvalidpage',
port=socks_port, ignoreAPI=True).lower()
except AttributeError:
leak_result = ""
except Exception as e:
logger.warn(str(e))
try:
if 'example' in leak_result:
logger.error('Tor was able to request a clearnet site')
raise ValueError('Tor was able to request a clearnet site')
except TypeError:
pass

View File

@ -1,25 +0,0 @@
import os
from gevent import sleep
from onionrblocks import insert
import logger
from coredb.blockmetadb import get_block_list
from onionrutils import epoch
def test_inserted_housekeeping(testmanager):
"""Tests that inserted blocks are proprely deleted"""
bl = insert('testdata', expire=12)
wait_seconds = 132 # Wait two minutes plus expire time
count = 0
if bl in get_block_list():
while count < wait_seconds:
if bl in get_block_list():
sleep(0.8)
count += 1
else:
return
raise ValueError('Inserted block with expiry not erased')
else:
raise ValueError('Inserted block in expiry test not present in list')

View File

@ -1,20 +0,0 @@
import os
import time
import onionrblocks
import logger
import coredb
def _check_remote_node(testmanager):
return
def insert_bin_test(testmanager):
data = os.urandom(32)
b_hash = onionrblocks.insert(data)
time.sleep(0.3)
if b_hash not in testmanager._too_many.get_by_string("PublicAPI").hideBlocks:
raise ValueError("Block not hidden")
if b_hash not in coredb.blockmetadb.get_block_list():
logger.error(str(b_hash) + 'is not in bl')
raise ValueError

View File

@ -1,27 +0,0 @@
import os
from shutil import move
from onionrblocks import insert
from onionrstorage import deleteBlock
from onionrcommands.exportblocks import export_block
from filepaths import export_location, block_data_location, data_nonce_file
from etc.onionrvalues import BLOCK_EXPORT_FILE_EXT
from onionrstorage.removeblock import remove_block
from onionrstorage import deleteBlock
from coredb.blockmetadb import get_block_list
from utils import bettersleep
from gevent import sleep
def test_sneakernet_import(test_manager):
in_db = lambda b: b in get_block_list()
bl = insert(os.urandom(10))
assert in_db(bl)
export_block(bl)
assert os.path.exists(export_location + bl + BLOCK_EXPORT_FILE_EXT)
remove_block(bl)
deleteBlock(bl)
assert not in_db(bl)
os.remove(data_nonce_file)
move(export_location + bl + BLOCK_EXPORT_FILE_EXT, block_data_location)
sleep(1)
assert in_db(bl)

View File

@ -1,17 +0,0 @@
import os
import onionrblocks
import logger
import coredb
from onionrutils import epoch
def stress_test_block_insert(testmanager):
return
start = epoch.get_epoch()
count = 100
max_insert_speed = 120
for x in range(count): onionrblocks.insert(os.urandom(32))
speed = epoch.get_epoch() - start
if speed < max_insert_speed:
raise ValueError(f'{count} blocks inserted too fast, {max_insert_speed}, got {speed}')
logger.info(f'runtest stress block insertion: {count} blocks inserted in {speed}s')

View File

@ -1,68 +0,0 @@
"""Onionr - Private P2P Communication.
Detect new block files in a given directory
"""
import os
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
import config
from filepaths import block_data_location
from etc.onionrvalues import BLOCK_EXPORT_FILE_EXT
from onionrblocks.blockimporter import import_block_from_data
import onionrexceptions
"""
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/>.
"""
watch_paths = config.get('transports.sneakernet.paths', list([]))
if block_data_location not in watch_paths:
watch_paths.append(block_data_location)
class _Importer(FileSystemEventHandler):
@staticmethod
def on_created(event):
if not event.src_path.endswith(BLOCK_EXPORT_FILE_EXT):
return
try:
with open(event.src_path, 'rb') as block_file:
block_data = block_file.read()
except FileNotFoundError:
return
os.remove(event.src_path)
try:
import_block_from_data(block_data)
except( # noqa
onionrexceptions.DataExists,
onionrexceptions.BlockMetaEntryExists,
onionrexceptions.InvalidMetadata) as _:
return
if block_data_location in event.src_path:
try:
os.remove(event.src_path)
except FileNotFoundError:
pass
def sneakernet_import_thread():
"""Add block data dir & confed paths to fs observer to watch for new bls"""
observer = Observer()
for path in watch_paths:
observer.schedule(_Importer(), path, recursive=True)
observer.start()
while observer.is_alive():
# call import func with timeout
observer.join(60)

View File

@ -1,123 +0,0 @@
"""Onionr - Private P2P Communication.
This file primarily serves to allow specific fetching of circles board messages
"""
import operator
import os
import ujson as json
from flask import Response, Blueprint
from flask import send_from_directory
from deadsimplekv import DeadSimpleKV
from utils import identifyhome
"""
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/>.
"""
flask_blueprint = Blueprint('circles', __name__)
root = os.path.dirname(os.path.realpath(__file__))
with open(
os.path.dirname(
os.path.realpath(__file__)) + '/info.json', 'r') as info_file:
data = info_file.read().strip()
version = json.loads(data)['version']
BOARD_CACHE_FILE = identifyhome.identify_home() + '/board-index.cache.json'
read_only_cache = DeadSimpleKV(
BOARD_CACHE_FILE,
flush_on_exit=False,
refresh_seconds=30)
@flask_blueprint.route('/board/<path:path>', endpoint='circlesstatic')
def load_mail(path):
return send_from_directory(root + '/web/', path)
@flask_blueprint.route('/board/', endpoint='circlesindex')
def load_mail_index():
return send_from_directory(root + '/web/', 'index.html')
@flask_blueprint.route('/circles/getpostsbyboard/<board>')
def get_post_by_board(board):
board_cache = DeadSimpleKV(
BOARD_CACHE_FILE,
flush_on_exit=False)
board_cache.refresh()
posts = board_cache.get(board)
if posts is None:
posts = ''
else:
posts = ','.join(posts)
return Response(posts)
@flask_blueprint.route('/circles/getpostsbyboard/<board>/<offset>')
def get_post_by_board_with_offset(board, offset):
offset = int(offset)
OFFSET_COUNT = 10
board_cache = DeadSimpleKV(
BOARD_CACHE_FILE,
flush_on_exit=False)
board_cache.refresh()
posts = board_cache.get(board)
if posts is None:
posts = ''
else:
posts.reverse()
posts = ','.join(posts[offset:offset + OFFSET_COUNT])
return Response(posts)
@flask_blueprint.route('/circles/version')
def get_version():
return Response(version)
@flask_blueprint.route('/circles/removefromcache/<board>/<name>',
methods=['POST'])
def remove_from_cache(board, name):
board_cache = DeadSimpleKV(BOARD_CACHE_FILE,
flush_on_exit=False)
board_cache.refresh()
posts = board_cache.get(board)
try:
posts.remove(name)
except ValueError:
pass
board_cache.put(board, posts)
return Response('success')
@flask_blueprint.route('/circles/getpopular/<count>')
def get_popular(count):
read_only_cache.refresh()
boards = json.loads(read_only_cache.get_raw_json())
for board in boards:
boards[board] = len(boards[board])
top_boards = sorted(boards.items(), key=operator.itemgetter(1), reverse=True)[:int(count)]
only_board_names = []
for b in top_boards:
only_board_names.append(b[0])
return Response(','.join(only_board_names), content_type='text/csv')

View File

@ -1,4 +0,0 @@
{ "name": "circles",
"version": "1.0.0",
"author": "onionr"
}

View File

@ -1,176 +0,0 @@
"""Onionr - Private P2P Communication.
This default plugin handles "flow" messages
(global chatroom style communication)
"""
import sys
import os
import deadsimplekv as simplekv
from utils import identifyhome, reconstructhash
from coredb import blockmetadb
import threading
import time
import locale
from onionrblocks.onionrblockapi import Block
import logger
import onionrblocks
from onionrutils import escapeansi, epoch, bytesconverter
locale.setlocale(locale.LC_ALL, '')
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
# import after path insert
import flowapi # noqa
"""
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/>.
"""
flask_blueprint = flowapi.flask_blueprint
security_whitelist = ['circles.circlesstatic', 'circles.circlesindex']
plugin_name = 'circles'
PLUGIN_VERSION = '0.1.0'
EXPIRE_TIME = 43200
class OnionrFlow:
def __init__(self):
self.alreadyOutputed = []
self.flowRunning = False
self.channel = ""
return
def start(self):
logger.warn(
"Please note: everything said here is public, " +
"even if a random channel name is used.", terminal=True)
message = ""
self.flowRunning = True
try:
self.channel = logger.readline(
"Enter a channel name or none for default:").strip()
except (KeyboardInterrupt, EOFError):
self.flowRunning = False
newThread = threading.Thread(target=self.showOutput, daemon=True)
newThread.start()
while self.flowRunning:
if self.channel == "":
self.channel = "global"
try:
message = logger.readline(f'\nInsert message into {plugin_name}:').strip().replace(
'\n', '\\n').replace('\r', '\\r')
except EOFError:
pass
except KeyboardInterrupt:
self.flowRunning = False
else:
if message == "q":
self.flowRunning = False
expireTime = epoch.get_epoch() + EXPIRE_TIME
if len(message) > 0:
logger.info('Inserting message as block...', terminal=True)
onionrblocks.insert(message, header='brd',
expire=expireTime,
meta = {
'ch': self.channel})
logger.info(f"{plugin_name} is exiting, goodbye", terminal=True)
return
def showOutput(self):
while isinstance(self.channel, type(None)) and self.flowRunning:
time.sleep(1)
try:
while self.flowRunning:
for block in blockmetadb.get_blocks_by_type('brd'):
if block in self.alreadyOutputed:
continue
block = Block(block)
b_hash = bytesconverter.bytes_to_str(block.getHash())
if block.getMetadata('ch') != self.channel:
continue
if not self.flowRunning:
break
logger.info('\n------------------------',
prompt=False, terminal=True)
content = block.getContent()
# Escape new lines, remove trailing whitespace, and escape ansi sequences
content = escapeansi.escape_ANSI(content.replace(
b'\n', b'\\n').replace(b'\r', b'\\r').strip().decode('utf-8'))
logger.info(block.getDate().strftime(
"%m/%d %H:%M") + ' - ' +
logger.colors.reset + content,
prompt=False, terminal=True)
self.alreadyOutputed.append(b_hash)
time.sleep(5)
except KeyboardInterrupt:
self.flowRunning = False
def on_circles_cmd(api, data=None):
OnionrFlow().start()
def on_circlesend_cmd(api, data=None):
err_msg = "Second arg is board name, third is quoted message"
try:
sys.argv[2]
except IndexError:
logger.error(err_msg, terminal=True)
try:
sys.argv[3]
except IndexError:
logger.error(err_msg, terminal=True)
bl = onionrblocks.insert(sys.argv[3], header='brd',
expire=(EXPIRE_TIME + epoch.get_epoch()),
meta={'ch': sys.argv[2]})
print(bl)
def on_softreset(api, data=None):
try:
os.remove(identifyhome.identify_home() + '/board-index.cache.json')
logger.info('Cleared Circles board cache')
except FileNotFoundError:
pass
def on_processblocks(api, data=None):
metadata = data['block'].bmetadata # Get the block metadata
if data['type'] != 'brd':
return
b_hash = reconstructhash.deconstruct_hash(
data['block'].hash) # Get the 0-truncated block hash
board_cache = simplekv.DeadSimpleKV(identifyhome.identify_home(
) + '/board-index.cache.json', flush_on_exit=False) # get the board index cache
board_cache.refresh()
# Validate the channel name is sane for caching
try:
ch = metadata['ch']
except KeyError:
ch = 'global'
ch_len = len(ch)
if ch_len == 0:
ch = 'global'
elif ch_len > 12:
return
existing_posts = board_cache.get(ch)
if existing_posts is None:
existing_posts = []
existing_posts.append(data['block'].hash)
board_cache.put(ch, existing_posts)
board_cache.flush()

View File

@ -1,44 +0,0 @@
/*
Onionr - Private P2P Communication
Auto refresh board posts
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
var checkbox = document.getElementById('refreshCheckbox')
function autoRefresh(){
if (! checkbox.checked || document.hidden){return}
getBlocks()
}
function setupInterval(){
if (checkbox.checked){
refreshInterval = setInterval(autoRefresh, 3000)
autoRefresh()
return
}
clearInterval(refreshInterval)
}
var refreshInterval = setInterval(autoRefresh, 3000)
setupInterval()
checkbox.onchange = function(){setupInterval}
document.addEventListener("visibilitychange", function() {
if (document.visibilityState === 'visible') {
autoRefresh()
}
})

View File

@ -1,265 +0,0 @@
/*
Onionr - Private P2P Communication
This file handles the boards/circles interface
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
requested = []
newPostForm = document.getElementById('addMsg')
firstLoad = true
lastLoadedBoard = 'global'
loadingMessage = document.getElementById('loadingBoard')
loadedAny = false
loadingTimeout = 8000
let toggleLoadingMessage = function(){
switch (loadingMessage.style.display){
case "inline-block":
loadingMessage.style.display = "none"
break;
default:
loadingMessage.style.display = "initial"
break;
}
}
fetch('/circles/version', {
method: 'GET',
headers: {
"token": webpass
}})
.then((ver) => ver.text())
.then(function(ver) {
document.getElementById('circlesVersion').innerText = ver
})
function appendMessages(msg, blockHash, beforeHash, channel) {
if (channel !== document.getElementById('feedIDInput').value) return // ignore if channel name isn't matching
if (msg.length == 0) return // ignore empty messages
var humanDate = new Date(0)
var msgDate = msg['meta']['time']
var feed = document.getElementById("feed")
var beforeEl = null
if (msgDate === undefined){
msgDate = 'unknown'
} else {
humanDate.setUTCSeconds(msgDate)
msgDate = humanDate.toLocaleString("en-US", {timeZone: "Etc/GMT"})
}
var el = document.createElement('div')
el.innerText = msg['content']
if (beforeHash !== null) {
for (i = 0; i < feed.children.length; i++) {
if (feed.children[i].getAttribute('data-bl') === beforeHash) {
beforeEl = feed.children[i]
}
}
}
/* Template Test */
// Test to see if the browser supports the HTML template element by checking
// for the presence of the template element's content attribute.
if ('content' in document.createElement('template')) {
// Instantiate the table with the existing HTML tbody
// and the row with the template
var template = document.getElementById('cMsgTemplate')
// Clone the new row and insert it into the table
var clone = document.importNode(template.content, true)
var div = clone.querySelectorAll("div")
var identicon = clone.querySelectorAll("img")
div[0].classList.add('entry')
div[0].setAttribute('timestamp', msg['meta']['time'])
div[0].setAttribute('data-bl', blockHash)
div[2].textContent = msg['content']
if (typeof msg['meta']['signer'] != 'undefined' && msg['meta']['signer'].length > 0){
div[3].textContent = msg['meta']['signer'].substr(0, 5)
setHumanReadableIDOnPost(div[3], msg['meta']['signer'])
div[3].onclick = function(){
navigator.clipboard.writeText(div[3].title).then(function() {
PNotify.notice("Copied poster identity to clipboard")
})
}
div[3].title = msg['meta']['signer']
userIcon(msg['meta']['signer']).then(function(data){
identicon[0].src = "data:image/svg+xml;base64," + data
})
}
else{
identicon[0].remove()
}
div[4].textContent = msgDate
loadingMessage.style.display = "none"
loadedAny = true
if (firstLoad){
//feed.appendChild(clone)
feed.prepend(clone)
firstLoad = false
}
else{
if (beforeEl === null){
feed.prepend(clone)
}
else{
beforeEl.insertAdjacentElement("beforebegin", clone.children[0])
}
}
}
}
function getBlocks(){
var feed = document.getElementById("feed")
var ch = document.getElementById('feedIDInput').value
if (lastLoadedBoard !== ch){
requested = []
toggleLoadingMessage()
loadedAny = false
while (feed.firstChild) feed.removeChild(feed.firstChild); // remove all messages from feed
setTimeout(function(){
if (! loadedAny && ch == document.getElementById('feedIDInput').value){
PNotify.notice("There are no posts for " + ch + ". You can be the first!")
}
}, loadingTimeout)
}
lastLoadedBoard = ch
if (document.getElementById('none') !== null){
document.getElementById('none').remove();
}
fetch('/circles/getpostsbyboard/' + ch, {
method: 'GET',
headers: {
"token": webpass
}})
.then((resp) => resp.text())
.then(function(feedText) {
var blockList = feedText.split(',')
for (i = 0; i < blockList.length; i++){
blockList[i] = "0".repeat(64 - blockList[i].length) + blockList[i] // pad hash with zeroes
if (! requested.includes(blockList[i])){
if (blockList[i].length == 0) continue
else requested.push(blockList[i])
loadMessage(blockList[i], blockList, i, ch);
}
}
sortEntries()
})
}
function loadMessage(blockHash, blockList, count, channel){
if (blockHash == '0000000000000000000000000000000000000000000000000000000000000000'){
return
}
fetch('/getblockdata/' + blockHash, {
method: 'GET',
headers: {
"token": webpass
}}).then(function(response) {
if (!response.ok) {
let on404 = function() {
if (response.status == 404){
fetch('/circles/removefromcache/' + channel + '/' + blockHash, {
method: 'POST',
headers: {
"content-type": "application/json",
"token": webpass
}
})
}
else{
console.log(error)
}
}()
return
}
response.json().then(function(data){
let before = blockList[count - 1]
let delay = 2000
if (typeof before == "undefined"){
before = null
} else {
let existing = document.getElementsByClassName('cMsgBox')
for (x = 0; x < existing.length; x++){
if (existing[x].getAttribute('data-bl') === before){
delay = 0
}
}
}
setTimeout(function(){appendMessages(data, blockHash, before, channel)}, delay)
})
return response;
})
}
document.getElementById('refreshFeed').onclick = function() {
getBlocks()
}
newPostForm.onsubmit = function(){
var message = document.getElementById('newMsgText').value
var channel = document.getElementById('feedIDInput').value
var meta = {'ch': channel}
let doSign = document.getElementById('postAnon').checked
var postData = {'message': message, 'sign': doSign, 'type': 'brd', 'encrypt': false, 'meta': JSON.stringify(meta)}
postData = JSON.stringify(postData)
newPostForm.style.display = 'none'
fetch('/insertblock', {
method: 'POST',
body: postData,
headers: {
"content-type": "application/json",
"token": webpass
}
})
.then((resp) => resp.text())
.then(function(data) {
newPostForm.style.display = 'block'
if (data == 'failure due to duplicate insert'){
PNotify.error({
text: "This message is already queued"
})
return
}
PNotify.success({
text: "Message queued for posting"
})
setTimeout(function(){getBlocks()}, 500)
})
return false
}
resetCirclePickers = function(){
document.getElementById('recommendedBoards').value = ""
document.getElementById('popularBoards').value = ""
}
document.getElementById('feedIDInput').onchange = resetCirclePickers

View File

@ -1,26 +0,0 @@
/*
Onionr - Private P2P Communication
Handle default board picker
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/>.
*/
recommendedIDs = document.getElementById('recommendedBoards')
recommendedIDs.onchange = function(){
document.getElementById('feedIDInput').value = recommendedIDs.value
getBlocks()
resetCirclePickers()
}

View File

@ -1,35 +0,0 @@
/*
Onionr - Private P2P Communication
detect for Circles if plaintext insert/storage is enabled
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/>.
*/
plaintext_enabled = null
fetch('/config/get/general.store_plaintext_blocks', {
method: 'GET',
headers: {
"token": webpass
}})
.then((resp) => resp.text())
.then(function(data) {
plaintext_enabled = true
if (data == "false"){
plaintext_enabled = false
PNotify.error({
text: "Plaintext storage is disabled. You will not be able to see new posts or make posts yourself"
})
}
})

View File

@ -1,196 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset='utf-8'>
<!--Mobile responsive-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>
Circles
</title>
<link rel="shortcut icon" type="image/ico" href="/shared/images/favicon.ico">
<link rel="stylesheet" href="/shared/main/PNotifyBrightTheme.css">
<link rel="stylesheet" href="/shared/fontawesome-free-5.10.2/css/all.min.css">
<link rel="stylesheet" href="/gettheme">
<link rel="stylesheet" href="theme.css">
<script defer src="/shared/base32.js"></script>
<script defer src="/shared/identicon.js"></script>
<script defer src="/shared/node_modules/pnotify/dist/iife/PNotify.js"></script>
<script defer src="/shared/node_modules/pnotify/dist/iife/PNotifyButtons.js"></script>
<script defer src="/shared/useridenticons.js"></script>
<script defer src="/shared/misc.js"></script>
<script defer src="/shared/navbar.js"></script>
<script defer src="/shared/main/apicheck.js"></script>
<script defer src="detect-plaintext-storage.js"></script>
<script defer src="sethumanreadable.js"></script>
<script defer src="default-circle-picker.js"></script>
<script defer src="sort-posts.js"></script>
<script defer src="board.js"></script>
<script defer src="autorefresh.js"></script>
<script defer src="popular.js"></script>
</head>
<body>
<nav class="navbar is-dark" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item idLink" href="/">
<img src="/shared/images/favicon.ico" class="navbarLogo">
</a>
<a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false"
data-target="navbarBasic">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbarBasic" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item idLink" href="/mail/">Mail</a>
<a class="navbar-item idLink" href="/friends/">Friends</a>
<a class="navbar-item idLink" href="/board/">Circles</a>
</div>
</div>
</nav>
<!--Hero (Dark Section)-->
<section class="hero is-small is-dark">
<div class="hero-body">
<div class="container">
<div class="columns">
<div class="column">
<h1 class="title">
Circles <span class="is-pulled-right">v<span id='circlesVersion'></span></span>
</h1>
<h2 class="subtitle">
Anonymous message boards
</h2>
</div>
</div>
</div>
</section>
<br>
<!--Start Content-->
<div class="container">
<div class="columns">
<!--Add Friend-->
<div class="column is-one-third">
<div class="card">
<form method='POST' action='/' id='addMsg'>
<header class="card-header">
<p class="card-header-title">
Post message
</p>
</header>
<div class="card-content">
<div class="content">
<textarea id='newMsgText' class="textarea" name='newMsgText' rows=10 cols=50 required
minlength="2"></textarea>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item">
<input class='button is-primary' type='submit' value='Post'>
</a>
</footer>
</form>
</div>
</div>
<!--Feed-->
<div class="column">
<div class="card">
<header class="card-header">
<p class="card-header-title">
Feed
</p>
</header>
<div class="card-content">
<div class="content">
<div class="field">
<div class="field has-addons">
<p class="control">
<a class="button is-static">Circle Name</a>
</p>
<p class="control is-expanded">
<input id="feedIDInput" class="input" placeholder="Board name" value="global">
</p>
<p class="control">
<a class="button is-success" id="refreshFeed">Refresh Feed</a>
</p>
</div>
<div class="field">
<div class="columns">
<div class="column is-2">
<div class="control">
<label for="recommendedBoards" class="label">Default Circles:</label>
<div class="select">
<select id="recommendedBoards">
<option value=""></option>
<option value="global">Global</option>
<option value="onionr">Onionr</option>
<option value="games">Games</option>
<option value="politics">Politics</option>
<option value="tech">Tech</option>
<option value="random">Random</option>
<option value="privacy">Privacy</option>
</select>
</div>
</div>
</div>
<div class="column is-2">
<div class="control">
<label for="popularBoards" class="label">Popular Circles:</label>
<div class="select">
<select id="popularBoards">
<option value="">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</option>
</select>
</div>
</div>
</div>
</div>
</div>
<p class="control">
<br>
Note: All posts in Circles are publicly accessible.
</p>
<input type="checkbox" class="checkbox" id="refreshCheckbox" checked>
<label for="refreshCheckbox">Auto refresh feed</label>
<br>
<input type="checkbox" class="checkbox" id="postAnon" checked>
<label for="postAnon">Sign posts</label>
</div>
</div>
<div class="content">
<span id='loadingBoard'><i class="fas fa-yin-yang fa-spin"></i></span>
<div id='feed'>
<span id='none'>None yet, try refreshing 😃</span>
<!--Message Items are appended here based on template-->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!--Template markup for Circle message-->
<template id="cMsgTemplate">
<div class="box cMsgBox">
<div class="columns">
<div class="column cMsg">
Message
</div>
<div class="column cAuthor is-narrow"></div>
<img class="identicon image is-48x48" alt="user icon" src="/shared/images/anon.svg">
<div class="column is-narrow cMsgDate">
Date
</div>
</div>
</div>
</template>
</body>
</html>

View File

@ -1,45 +0,0 @@
/*
Onionr - Private P2P Communication
Load popular boards and show them in the UI. Handle selections of popular boards.
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/>.
*/
fetch('/circles/getpopular/8', {
method: 'GET',
headers: {
"token": webpass
}})
.then((popular) => popular.text())
.then(function(popular) {
var popularSelect = document.getElementById('popularBoards')
let boards = popular.split(',')
for (board of boards){
let newOption = document.createElement('option')
if (board == ""){continue}
newOption.value = board
newOption.innerText = board.charAt(0).toUpperCase() + board.slice(1)
console.debug(board)
popularSelect.appendChild(newOption)
}
})
document.getElementById('popularBoards').onchange = function(){
document.getElementById('feedIDInput').value = document.getElementById('popularBoards').value
getBlocks()
resetCirclePickers()
}

View File

@ -1,39 +0,0 @@
/*
Onionr - Private P2P Communication
Set human readable public keys onto post author elements
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/>.
*/
humanReadableKeys = {}
function setHumanReadableIDOnPost(el, key){
if (typeof humanReadableKeys[key] == "undefined"){
fetch('/getHumanReadable/' + key, {
method: 'GET',
headers: {
"token": webpass
}})
.then((resp) => resp.text()) // Transform the data into json
.then(function(data) {
if (data.includes('HTML')){
return
}
humanReadableKeys[key] = data
setHumanReadableIDOnPost(el, key)
})
return
}
el.innerText = humanReadableKeys[key].split('-').slice(0, 3).join(' ')
}

View File

@ -1,30 +0,0 @@
/*
Onionr - Private P2P Communication
Sort post entries
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/>.
*/
function sortEntries() {
var entries = document.getElementsByClassName('entry')
if (entries.length > 1) {
const sortBy = 'timestamp'
const parent = entries[0].parentNode
const sorted = Array.from(entries).sort((a, b) => b.getAttribute(sortBy) - a.getAttribute(sortBy))
sorted.forEach(element => parent.appendChild(element))
}
}

View File

@ -1,12 +0,0 @@
.cMsg{
word-wrap:break-word;
word-break:break-word;
white-space: pre-wrap;
overflow: hidden;
}
body{
background-color: #212224;
color: white;
}

View File

@ -1,44 +0,0 @@
"""Onionr - Private P2P Communication.
This file primarily serves to allow specific fetching of circles board messages
"""
from flask import Response, Blueprint, g
from deadsimplekv import DeadSimpleKV
"""
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/>.
"""
flask_blueprint = Blueprint('debugAPI', __name__)
@flask_blueprint.route('/debug/dump_shared_state')
def get_shared_state() -> Response:
"""Return somewhat human-readable dump of shared toomanyobjects."""
resp = ""
for i in g.too_many.objects.keys:
resp += i + dir(g.too_many.objects.keys[i]) + "\n"
return Response(resp)
@flask_blueprint.route('/debug/dump_shared_vars')
def get_shared_vars() -> Response:
"""Return somewhat human-readable dump of pseudo globals (DeadSimpleKV)."""
kv: DeadSimpleKV = g.too_many.get(DeadSimpleKV)
resp = ""
for i in kv.keys:
resp += i + dir(g.too_many.objects.keys[i]) + "\n"
return Response(resp)

View File

@ -1,4 +0,0 @@
{ "name": "debug",
"version": "0.0.0",
"author": "onionr"
}

View File

@ -1,31 +0,0 @@
"""Onionr - Private P2P Communication.
Client HTTP API debug endpoints
"""
import sys
import os
import locale
locale.setlocale(locale.LC_ALL, '')
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
# import after path insert
import debugapi # noqa
"""
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/>.
"""
flask_blueprint = debugapi.flask_blueprint
plugin_name = 'debuginfo'
PLUGIN_VERSION = '0.0.0'

View File

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

View File

@ -1,130 +0,0 @@
'''
Onionr - Private P2P Communication
This default plugin allows users to encrypt/decrypt messages without using blocks
'''
'''
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/>.
'''
# Imports some useful libraries
import logger, config, threading, time, datetime, sys
import ujson as json
from nacl.exceptions import TypeError as NaclTypeError
from onionrutils import stringvalidators, bytesconverter
from onionrcrypto import encryption, keypair, signing, getourkeypair
import onionrexceptions, onionrusers
import locale
locale.setlocale(locale.LC_ALL, '')
import binascii
plugin_name = 'encrypt'
class PlainEncryption:
def __init__(self, api):
self.api = api
return
def encrypt(self):
# peer, data
plaintext = ""
encrypted = ""
# detect if signing is enabled
sign = True
try:
if sys.argv[3].lower() == 'false':
sign = False
except IndexError:
pass
try:
if not stringvalidators.validate_pub_key(sys.argv[2]):
raise onionrexceptions.InvalidPubkey
except (ValueError, IndexError) as e:
logger.error("Peer public key not specified", terminal=True)
except onionrexceptions.InvalidPubkey:
logger.error("Invalid public key", terminal=True)
else:
pubkey = sys.argv[2]
# Encrypt if public key is valid
logger.info("Please enter your message (ctrl-d or -q to stop):", terminal=True)
try:
for line in sys.stdin:
if line == '-q\n':
break
plaintext += line
except KeyboardInterrupt:
sys.exit(1)
# Build Message to encrypt
data = {}
myPub = keypair[0]
if sign:
data['sig'] = signing.ed_sign(plaintext, key=keypair[1], encodeResult=True)
data['sig'] = bytesconverter.bytes_to_str(data['sig'])
data['signer'] = myPub
data['data'] = plaintext
data = json.dumps(data)
plaintext = data
encrypted = encryption.pub_key_encrypt(plaintext, pubkey, encodedData=True)
encrypted = bytesconverter.bytes_to_str(encrypted)
logger.info('Encrypted Message: \n\nONIONR ENCRYPTED DATA %s END ENCRYPTED DATA' % (encrypted,), terminal=True)
def decrypt(self):
plaintext = ""
data = ""
logger.info("Please enter your message (ctrl-d or -q to stop):", terminal=True)
keypair = getourkeypair.get_keypair()
try:
for line in sys.stdin:
if line == '-q\n':
break
data += line
except KeyboardInterrupt:
sys.exit(1)
if len(data) <= 1:
return
encrypted = data.replace('ONIONR ENCRYPTED DATA ', '').replace('END ENCRYPTED DATA', '')
myPub = keypair[0]
decrypted = encryption.pub_key_decrypt(encrypted, privkey=keypair[1], encodedData=True)
if decrypted == False:
logger.error("Decryption failed", terminal=True)
else:
data = json.loads(decrypted)
logger.info('Decrypted Message: \n\n%s' % data['data'], terminal=True)
try:
logger.info("Signing public key: %s" % (data['signer'],), terminal=True)
if not signing.ed_verify(data['data'], data['signer'], data['sig']): raise ValueError
except (ValueError, KeyError) as e:
logger.warn("WARNING: THIS MESSAGE HAS A MISSING OR INVALID SIGNATURE", terminal=True)
else:
logger.info("Message has good signature.", terminal=True)
return
def on_decrypt_cmd(api, data=None):
try:
PlainEncryption(api).decrypt()
except binascii.Error:
logger.error("Invalid ciphertext padding", terminal=True)
except NaclTypeError:
logger.error("Ciphertext too short.", terminal=True)
def on_encrypt_cmd(api, data=None):
PlainEncryption(api).encrypt()
on_encrypt_cmd.onionr_help = """encrypt <user_key>\nEncrypt text data to an Onionr user key. Similar to PGP"""
on_decrypt_cmd.onionr_help = """decrypt\nDecrypt text data with your Onionr key. Similar to PGP"""
ONIONR_COMMANDS = ['encrypt', 'decrypt']

View File

@ -1,5 +0,0 @@
{
"name" : "pms",
"version" : "0.1.2",
"author" : "onionr"
}

View File

@ -1,42 +0,0 @@
"""Onionr - Private P2P Communication.
Load the user's inbox and return it as a list
"""
from onionrblocks import onionrblockapi
from coredb import blockmetadb
from utils import reconstructhash, identifyhome
import deadsimplekv as simplekv
"""
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/>.
"""
inbox_cache = {}
def load_inbox():
inbox_list = []
deleted = simplekv.DeadSimpleKV(identifyhome.identify_home() + '/mailcache.dat').get('deleted_mail')
if deleted is None:
deleted = []
for blockHash in blockmetadb.get_blocks_by_type('pm'):
if blockHash in deleted:
continue
if blockHash in inbox_cache:
inbox_list.append(blockHash)
continue
block = onionrblockapi.Block(blockHash)
block.decrypt()
if block.decrypted and reconstructhash.deconstruct_hash(blockHash):
inbox_cache[blockHash] = True
inbox_list.append(blockHash)
return inbox_list

View File

@ -1,97 +0,0 @@
"""Onionr - Private P2P Communication.
HTTP endpoints for mail plugin
"""
import sys
import os
import ujson as json
from flask import Response, request, redirect, Blueprint, abort
from flask import send_from_directory
import deadsimplekv as simplekv
from httpapi.sse.wrapper import SSEWrapper
from onionrusers import contactmanager
from onionrutils import stringvalidators
from utils import reconstructhash, identifyhome
from utils.bettersleep import better_sleep as sleep
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
import loadinbox
import sentboxdb
"""
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/>.
"""
flask_blueprint = Blueprint('mail', __name__)
kv = simplekv.DeadSimpleKV(identifyhome.identify_home() + '/mailcache.dat')
root = os.path.dirname(os.path.realpath(__file__))
sse_wrapper = SSEWrapper()
@flask_blueprint.route('/mail/<path:path>', endpoint='mailstatic')
def load_mail(path):
return send_from_directory(root + '/web/', path)
@flask_blueprint.route('/mail/', endpoint='mailindex')
def load_mail_index():
return send_from_directory(root + '/web/', 'index.html')
@flask_blueprint.route('/mail/ping')
def mail_ping():
return 'pong!'
@flask_blueprint.route('/mail/deletemsg/<block>', methods=['POST'])
def mail_delete(block):
if not stringvalidators.validate_hash(block):
abort(504)
block = reconstructhash.deconstruct_hash(block)
existing = kv.get('deleted_mail')
if existing is None:
existing = []
if block not in existing:
existing.append(block)
kv.put('deleted_mail', existing)
kv.flush()
return 'success'
@flask_blueprint.route('/mail/getinbox')
def list_inbox():
return ','.join(loadinbox.load_inbox())
@flask_blueprint.route('/mail/streaminbox')
def stream_inbox():
def _stream():
while True:
yield "data: " + ','.join(loadinbox.load_inbox()) + "\n\n"
sleep(1)
return sse_wrapper.handle_sse_request(_stream)
@flask_blueprint.route('/mail/getsentbox')
def list_sentbox():
kv.refresh()
sentbox_list = sentboxdb.SentBox().listSent()
list_copy = list(sentbox_list)
deleted = kv.get('deleted_mail')
if deleted is None:
deleted = []
for sent in list_copy:
if sent['hash'] in deleted:
sentbox_list.remove(sent)
continue
sent['name'] = contactmanager.ContactManager(sent['peer'], saveUser=False).get_info('name')
return json.dumps(sentbox_list)

View File

@ -1,87 +0,0 @@
"""Onionr - Private P2P Communication.
Private messages in an email like fashion
"""
import locale
import sys
import os
import ujson as json
from onionrusers import contactmanager
from utils import reconstructhash
from onionrutils import bytesconverter
import config
import notifier
"""
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/>.
"""
locale.setlocale(locale.LC_ALL, '')
plugin_name = 'pms'
PLUGIN_VERSION = '0.1.2'
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
import sentboxdb, mailapi, loadinbox # import after path insert
from onblacklist import on_blacklist_add
flask_blueprint = mailapi.flask_blueprint
security_whitelist = ['mail.mailstatic', 'mail.mailindex']
def add_deleted(keyStore, b_hash):
existing = keyStore.get('deleted_mail')
bHash = reconstructhash.reconstruct_hash(b_hash)
if existing is None:
existing = []
else:
if bHash in existing:
return
keyStore.put('deleted_mail', existing.append(b_hash))
def on_insertblock(api, data={}):
meta = json.loads(data['meta'])
if meta['type'] == 'pm':
sentboxTools = sentboxdb.SentBox()
sentboxTools.addToSent(data['hash'], data['peer'], data['content'], meta['subject'])
def on_processblocks(api, data=None):
if data['type'] != 'pm':
return
notification_func = notifier.notify
data['block'].decrypt()
metadata = data['block'].bmetadata
signer = bytesconverter.bytes_to_str(data['block'].signer)
user = contactmanager.ContactManager(signer, saveUser=False)
name = user.get_info("name")
if name != 'anonymous' and name != None:
signer = name.title()
else:
signer = signer[:5]
if data['block'].decrypted:
config.reload()
if config.get('mail.notificationSound', True):
notification_func = notifier.notification_with_sound
if config.get('mail.notificationSetting', True):
if not config.get('mail.strangersNotification', True):
if not user.isFriend():
return
notification_func(title="Onionr Mail - New Message", message="From: %s\n\nSubject: %s" % (signer, metadata['subject']))

View File

@ -1,12 +0,0 @@
from threading import Thread
from onionrutils import localcommand
def on_blacklist_add(api, data=None):
blacklisted_data = data['data']
def remove():
localcommand.local_command(f'/mail/deletemsg/{blacklisted_data}', post=True)
Thread(target=remove).start()

View File

@ -1,79 +0,0 @@
"""
Onionr - Private P2P Communication
This file handles the sentbox for the mail plugin
"""
import sqlite3
import os
from onionrutils import epoch
from utils import identifyhome, reconstructhash
"""
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 SentBox:
def __init__(self):
self.dbLocation = identifyhome.identify_home() + '/sentbox.db'
if not os.path.exists(self.dbLocation):
self.createDB()
return
def connect(self):
self.conn = sqlite3.connect(self.dbLocation, timeout=30)
self.cursor = self.conn.cursor()
def close(self):
self.conn.close()
def createDB(self):
conn = sqlite3.connect(self.dbLocation)
cursor = conn.cursor()
cursor.execute("""CREATE TABLE sent(
hash id not null,
peer text not null,
message text not null,
subject text not null,
date int not null
);
""")
conn.commit()
conn.close()
return
def listSent(self):
self.connect()
retData = []
for entry in self.cursor.execute('SELECT * FROM sent;'):
retData.append({'hash': entry[0], 'peer': entry[1], 'message': entry[2], 'subject': entry[3], 'date': entry[4]})
self.close()
return retData
def addToSent(self, blockID, peer, message, subject=''):
blockID = reconstructhash.deconstruct_hash(blockID)
self.connect()
args = (blockID, peer, message, subject, epoch.get_epoch())
self.cursor.execute('INSERT INTO sent VALUES(?, ?, ?, ?, ?)', args)
self.conn.commit()
self.close()
return
def removeSent(self, blockID):
blockID = reconstructhash.deconstruct_hash(blockID)
self.connect()
args = (blockID,)
self.cursor.execute('DELETE FROM sent where hash=?', args)
self.conn.commit()
self.close()
return

View File

@ -1,28 +0,0 @@
/*
Onionr - Private P2P Communication
Settings modal closing
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/>.
*/
document.getElementById('closeSettingsModalButton').onclick = function(){
document.getElementById('settingsModal').classList.remove('is-active')
setActiveTab('inbox')
}
document.querySelector("#settingsModal .modal-background").onclick = function(){
document.getElementById('settingsModal').classList.remove('is-active')
setActiveTab('inbox')
}

View File

@ -1,262 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>
Onionr Mail
</title>
<link rel="shortcut icon" type="image/ico" href="/shared/images/favicon.ico">
<link rel="stylesheet" href="/shared/fontawesome-free-5.10.2/css/all.min.css">
<link rel="stylesheet" href="/shared/main/PNotifyBrightTheme.css">
<link rel="stylesheet" href="/gettheme">
<link rel="stylesheet" href="/shared/node_modules/bulma-switch/dist/css/bulma-switch.min.css">
<link rel="stylesheet" href="/mail/mail.css">
<script defer src="/shared/node_modules/pnotify/dist/iife/PNotify.js"></script>
<script defer src="/shared/node_modules/pnotify/dist/iife/PNotifyButtons.js"></script>
<script defer src="/shared/eventsource.js"></script>
<script defer src="/shared/main/apicheck.js"></script>
<script defer src="/shared/misc.js"></script>
<script defer src="/mail/sethumanreadable.js"></script>
<script defer src="/mail/loadsettings.js"></script>
<script defer src="/mail/mail.js"></script>
<script defer src="/mail/sendmail.js"></script>
<script defer src="/mail/closesettings.js"></script>
<script defer src="/mail/settings.js"></script>
<script defer src="/shared/navbar.js"></script>
</head>
<body>
<nav class="navbar is-dark" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item idLink" href="/">
<img src="/shared/images/favicon.ico" class="navbarLogo">
</a>
<a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false"
data-target="navbarBasic">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbarBasic" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item idLink" href="/mail/">Mail</a>
<a class="navbar-item idLink" href="/friends/">Friends</a>
<a class="navbar-item idLink" href="/board/">Circles</a>
</div>
</div>
</nav>
<!--Hero (Dark Section)-->
<section class="hero is-small is-dark">
<div class="hero-body">
<div class="container">
<div class="columns">
<div class="column">
<h1 class="title">
Mail
</h1>
<h2 class="subtitle">
Private and safe messages
</h2>
</div>
<div class="column is-7">
<div class="field">
<div class="field has-addons">
<p class="control">
<a class="button is-static">
<i class="fas fa-fingerprint"></i>
</a>
</p>
<p class="control is-expanded">
<input id="myPub" class="input myPub" type="text" readonly>
</p>
<p class="control">
<a id="myPubCopy" class="button is-primary"><i class="fas fa-copy"></i></a>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<br>
<div id="messageDisplay" class="overlay">
<div class="overlayContent">
<span class="closeOverlay" overlay="messageDisplay"></span>
<div>
From: <input type="text" id="fromUser" readonly> Signature: <span id="sigValid"></span> <span
id="addUnknownContact"><button class="button is-primary">Add to Contacts</button></span>
</div>
<div class="break-up">
Subject: <span id="subjectView"></span>
</div>
<div>
<button id="replyBtn" class="button is-primary break-up">Reply</button>
</div>
<div id="signatureValidity"></div>
<div id="threadDisplay" class="pre messageContent">
</div>
</div>
</div>
<div id="sentboxDisplay" class="overlay">
<div class="overlayContent">
<span class="closeOverlay" overlay="sentboxDisplay"></span>
To: <input id="toID" readonly type="text">
<div id="sentboxDisplayText" class="pre messageContent">
</div>
</div>
</div>
<div id="settingsModal" class="modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<button id="closeSettingsModalButton" class="closeSettingsModal delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<div class="columns">
<div class="column">
<div>Ask senders to use forward-secrecy</div>
<small>Turn off if using 1 ID on more than 1 device, or have Onionr data erasure on exit enabled.</small>
</div>
<div class="column is-2">
<div class="field">
<input id="forwardSecrecySetting" type="checkbox"
class="switch is-rounded is-danger">
<label for="forwardSecrecySetting"></label>
</div>
</div>
</div>
<div class="columns">
<div class="column">
<div>Message padding</div>
<small>Improves privacy slightly for a small performance hit.</small>
</div>
<div class="column is-2">
<div class="field">
<input id="messagePaddingSetting" type="checkbox"
class="switch is-rounded is-warning">
<label for="messagePaddingSetting"></label>
</div>
</div>
</div>
<div class="columns">
<div class="column">
Inbox notifications
</div>
<div class="column is-2">
<div class="field">
<input id="notificationSetting" type="checkbox"
class="switch is-rounded" checked>
<label for="notificationSetting"></label>
</div>
</div>
</div>
<div class="columns notificationSetting">
<div class="column">
Notifications for stranger's messages
</div>
<div class="column is-2">
<div class="field">
<input id="strangersNotification" type="checkbox"
class="switch is-rounded" checked>
<label for="strangersNotification"></label>
</div>
</div>
</div>
<div class="columns notificationSetting">
<div class="column">
Notification sound
</div>
<div class="column is-2">
<div class="field">
<input id="notificationSound" type="checkbox"
class="switch is-rounded" checked>
<label for="notificationSound"></label>
</div>
</div>
</div>
Signature
<textarea id="mailSignatureSetting" class="textarea" placeholder="Signature to add to every message" rows="5"></textarea>
</section>
</div>
</div>
<!--Start of content-->
<div class="container">
<div class="tabs" id="tabBtns">
<ul>
<li class="is-active">
<a>
<span class="icon">
<i class="fa fa-envelope"></i>
</span>
<span id="inboxTab">Inbox</span>
</a>
</li>
<li>
<a>
<span class="icon">
<i class="fa fa-paper-plane"></i>
</span>
<span>Sent</span>
</a>
</li>
<li>
<a>
<span class="icon">
<i class="fa fa-pen"></i>
</span>
<span>Compose</span>
</a>
</li>
<li>
<a>
<span class="icon">
<i class="fa fa-cog"></i>
</span>
<span>Settings</span>
</a>
</li>
</ul>
</div>
<div class="content" id="noInbox">No messages to show ¯\_(ツ)_/¯</div>
<div class="content">
<div class="mailPing">
API server either shutdown, has disabled mail, or has experienced a bug.
</div>
<div id="threads" class="threads">
<div id="threadPlaceholder">Nothing here yet 😞</div>
</div>
</div>
</div>
<div id="sendMessage">
<div class="container">
<div class="field">
<label><i class="fas fa-user"></i> Select friend: <select id="friendSelect"></select></label>
</div>
<form method="post" action="" id="sendForm" enctype="application/x-www-form-urlencoded">
<div class="field">
To: <input id="draftID" type="text" name="to" placeholder="pubkey or select above" autocomplete="off" required>
</div>
Subject: <input name="subject" id="draftSubject" maxlength="25" type="text"
placeholder="message subject" autocomplete="off">
<div class="field">
<textarea name="message" class="textarea" placeholder="type your message..." id="draftText" required></textarea>
</div>
<input type="submit" value="Send" class="button is-primary successBtn">
</form>
</div>
</div>
<div id="infoOverlay" class="overlay">
</div>
</body>
</html>

View File

@ -1,29 +0,0 @@
mailSettings = {}
fetch('/config/get/mail', {
headers: {
"content-type": "application/json",
"token": webpass
}})
.then((resp) => resp.json())
.then(function(settings) {
mailSettings = settings || {}
if (mailSettings.default_forward_secrecy === false){
document.getElementById('forwardSecrecySetting').checked = false
}
if (mailSettings.use_padding === false){
document.getElementById('messagePaddingSetting').checked = false
}
if (mailSettings.notificationSetting === false){
document.getElementById('notificationSetting').checked = false
}
if (mailSettings.notificationSound === false){
document.getElementById('notificationSound').checked = false
}
if (typeof mailSettings.signature != undefined && mailSettings.signature != null && mailSettings.signature != ""){
document.getElementById('mailSignatureSetting').value = mailSettings.signature
}
if (mailSettings.strangersNotification == false){
document.getElementById('strangersNotification').checked = false
}
})

View File

@ -1,60 +0,0 @@
.threadEntry button{
margin-right: 1%;
}
.threadEntry span, .sentboxList span{
padding-left: 1%;
}
.threadEntry, .sentboxList{
cursor: pointer;
}
.overlayContent{
background-color: lightgray;
border: 3px solid black;
border-radius: 3px;
color: black;
font-family: Verdana, Geneva, Tahoma, sans-serif;
min-height: 100%;
padding: 1em;
margin: 1em;
}
#draftText{
margin-top: 1em;
margin-bottom: 1em;
display: block;
width: 50%;
height: 75%;
min-width: 2%;
min-height: 5%;
background: white;
color: black;
}
.sentboxList{
padding-top: 1em;
}
.messageContent{
padding-top: 1em;
}
#tabBtns ul .icon {
pointer-events: none;
}
#settingsModal .modal-card-body{
font-size: 150%;
min-height: 300px;
}
#settingsModal textarea{
resize: none;
margin-top: 1em;
}
#settingsModal small{
font-size: 0.5em;
}

View File

@ -1,467 +0,0 @@
/*
Onionr - Private P2P Communication
This file handles the mail interface
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
pms = ''
sentbox = ''
threadPart = document.getElementById('threads')
threadPlaceholder = document.getElementById('threadPlaceholder')
tabBtns = document.getElementById('tabBtns')
threadContent = {}
replyBtn = document.getElementById('replyBtn')
addUnknownContact = document.getElementById('addUnknownContact')
noInbox = document.getElementById('noInbox')
humanReadableCache = {}
function stripEndZeroes(str){
str = str.split("")
let zeroCount = 0
for (x = str.length - 1; x != 0; x--){
if (str[x] == "0"){
zeroCount += 1
}
else{
break
}
}
str.splice(str.length - zeroCount, zeroCount)
str = str.join("")
return str
}
async function addContact(pubkey, friendName){
fetch('/friends/add/' + pubkey, {
method: 'POST',
headers: {
"token": webpass
}}).then(function(data) {
if (friendName.trim().length > 0){
post_to_url('/friends/setinfo/' + pubkey + '/name', {'data': friendName, 'token': webpass})
}
})
}
function openReply(bHash, quote, subject){
var inbox = document.getElementsByClassName('threadEntry')
var entry = ''
var friendName = ''
var key = ''
for(var i = 0; i < inbox.length; i++) {
if (inbox[i].getAttribute('data-hash') === bHash){
entry = inbox[i]
}
}
if (entry.getAttribute('data-nameset') == 'true'){
document.getElementById('friendSelect').value = document.getElementById('fromUser').value
}
key = entry.getAttribute('data-pubkey')
document.getElementById('draftID').value = key
document.getElementById('draftSubject').value = 'RE: ' + subject
// Add quoted reply
var splitQuotes = quote.split('\n')
for (var x = 0; x < splitQuotes.length; x++){
splitQuotes[x] = '> ' + splitQuotes[x]
}
if (typeof humanReadableCache[key] != 'undefined'){
document.getElementById('draftID').value = humanReadableCache[key]
quote = '\n' + humanReadableCache[key].split('-').slice(0,3).join('-') + ' wrote:\n' + splitQuotes.join('\n')
}
else{
quote = '\n' + key.substring(0, 12) + ' wrote:' + '\n' + splitQuotes.join('\n')
}
document.getElementById('draftText').value = quote
setActiveTab('compose')
}
function openThread(bHash, sender, date, sigBool, pubkey, subjectLine){
addUnknownContact.style.display = 'none'
var messageDisplay = document.getElementById('threadDisplay')
fetch('/getblockbody/' + bHash, {
"method": "get",
headers: {
"token": webpass
}})
.then((resp) => resp.text())
.then(function(resp) {
document.getElementById('fromUser').value = sender || 'Anonymous'
document.getElementById('fromUser').value = pubkey || ''
document.getElementById('subjectView').innerText = subjectLine
resp = stripEndZeroes(resp)
messageDisplay.innerText = resp
var sigEl = document.getElementById('sigValid')
var sigMsg = 'signature'
// show add unknown contact button if peer is unknown
if (sender !== myPub && sigBool){
addUnknownContact.style.display = 'inline'
}
if (sigBool){
sigMsg = 'Good ' + sigMsg
sigEl.classList.remove('danger')
}
else{
sigMsg = 'Bad/no ' + sigMsg + ' (message could be impersonating someone)'
sigEl.classList.add('danger')
replyBtn.style.display = 'none'
}
sigEl.innerText = sigMsg
overlay('messageDisplay')
replyBtn.onclick = function(){
document.getElementById('messageDisplay').style.visibility = 'hidden'
openReply(bHash, messageDisplay.innerText, subjectLine)
}
addUnknownContact.onclick = function(){
var friendName = prompt("Enter an alias for this contact:")
if (friendName === null || friendName.length == 0){
return
}
addContact(pubkey, friendName)
}
})
}
function setActiveTab(tabName){
threadPart.innerHTML = ""
noInbox.style.display = 'none'
window.inboxActive = false
document.getElementById('sendMessage').classList.add('is-hidden')
switch(tabName){
case 'inbox':
window.inboxActive = true
refreshPms()
getInbox()
break
case 'sent':
getSentbox()
break
case 'compose':
document.getElementById('sendMessage').classList.remove('is-hidden')
break
case 'settings':
document.getElementById('settingsModal').classList.add('is-active')
break
}
}
function deleteMessage(bHash){
fetch('/mail/deletemsg/' + bHash, {
"method": "post",
headers: {
"token": webpass
}})
}
function mailPing(){
fetch('/mail/ping', {
"method": "get",
headers: {
"token": webpass
}})
.then(function(resp) {
var pings = document.getElementsByClassName('mailPing')
if (resp.ok){
for (var i=0; i < pings.length; i++){
pings[i].style.display = 'none';
}
}
else{
for (var i=0; i < pings.length; i++){
pings[i].style.display = 'block';
}
}
})
}
function loadInboxEntries(bHash){
fetch('/getblockheader/' + bHash, {
headers: {
"token": webpass
}})
.then((resp) => resp.json()) // Transform the data into json
.then(function(resp) {
//console.log(resp)
var entry = document.createElement('div')
var bHashDisplay = document.createElement('span')
//var senderInput = document.createElement('input')
var senderInput = document.createElement('span')
var subjectLine = document.createElement('span')
var dateStr = document.createElement('span')
var validSig = document.createElement('span')
var deleteBtn = document.createElement('button')
var humanDate = new Date(0)
var metadata = resp['metadata']
humanDate.setUTCSeconds(resp['meta']['time'])
humanDate = humanDate.toString()
validSig.style.display = 'none'
if (typeof resp['meta']['signer'] != 'undefined' && resp['meta']['signer'] != ''){
fetch('/friends/getinfo/' + resp['meta']['signer'] + '/name', {
headers: {
"token": webpass
}})
.then(function(resp2){
if (!resp2.ok){
setHumanReadableValue(senderInput, resp['meta']['signer'])
entry.setAttribute('data-nameSet', false)
}
else{
resp2.text().then(function(resp2){
loadHumanReadableToCache(resp['meta']['signer'])
senderInput.innerText = resp2
entry.setAttribute('data-nameSet', true)
})
}
})
}
else{
senderInput.innerText = 'Anonymous'
entry.setAttribute('data-nameSet', false)
}
if (! resp['meta']['validSig']){
validSig.style.display = 'inline'
validSig.innerText = 'Signature Validity: Bad'
validSig.style.color = 'red'
}
//bHashDisplay.innerText = bHash.substring(0, 10)
entry.setAttribute('data-hash', bHash)
entry.setAttribute('data-pubkey', resp['meta']['signer'])
senderInput.readOnly = true
dateStr.innerText = humanDate.substring(0, humanDate.indexOf('('))
deleteBtn.classList.add('delete', 'deleteBtn')
if (metadata['subject'] === undefined || metadata['subject'] === null) {
subjectLine.innerText = '()'
}
else{
subjectLine.innerText = '(' + metadata['subject'] + ')'
}
//entry.innerHTML = 'sender ' + resp['meta']['signer'] + ' - ' + resp['meta']['time']
threadPart.appendChild(entry)
entry.appendChild(deleteBtn)
entry.appendChild(bHashDisplay)
entry.appendChild(senderInput)
entry.appendChild(subjectLine)
entry.appendChild(dateStr)
entry.appendChild(validSig)
entry.classList.add('threadEntry')
entry.onclick = function(event){
if (event.target.classList.contains('deleteBtn')){
return
}
openThread(entry.getAttribute('data-hash'), senderInput.innerText, dateStr.innerText, resp['meta']['validSig'], entry.getAttribute('data-pubkey'), subjectLine.innerText)
}
deleteBtn.onclick = function(){
entry.parentNode.removeChild(entry);
deleteMessage(entry.getAttribute('data-hash'))
}
}.bind(bHash))
}
function getInbox(){
if (! window.inboxActive){
return
}
var els = document.getElementsByClassName('threadEntry')
var showed = false
for(var i = 0; i < pms.length; i++) {
var add = true
if (pms[i].trim().length == 0){
noInbox.style.display = 'block'
continue
}
else{
threadPlaceholder.style.display = 'none'
showed = true
}
for (var x = 0; x < els.length; x++){
if (pms[i] === els[x].getAttribute('data-hash')){
add = false
}
}
if (add && window.inboxActive) {
loadInboxEntries(pms[i])
}
}
if (! showed){
threadPlaceholder.style.display = 'block'
}
}
function getSentbox(){
fetch('/mail/getsentbox', {
headers: {
"token": webpass
}})
.then((resp) => resp.json()) // Transform the data into json
.then(function(resp) {
var keys = [];
var entry = document.createElement('div')
for(var k in resp) keys.push(k);
if (keys.length == 0){
threadPart.innerHTML = "nothing to show here yet."
}
for (var i = keys.length - 1; i > -1; i--) (function(i, resp){
var entry = document.createElement('div')
var toLabel = document.createElement('span')
toLabel.innerText = 'To: '
var toEl = document.createElement('span')
toEl.classList.add('toElement')
var sentDate = document.createElement('span')
var humanDate = new Date(0)
humanDate.setUTCSeconds(resp[i]['date'])
humanDate = humanDate.toString()
var preview = document.createElement('span')
var deleteBtn = document.createElement('button')
var message = resp[i]['message']
deleteBtn.classList.add('deleteBtn', 'delete')
sentDate.innerText = humanDate.substring(0, humanDate.indexOf('('))
if (resp[i]['name'] == null || resp[i]['name'].toLowerCase() == 'anonymous'){
toEl.innerText = resp[i]['peer']
setHumanReadableValue(toEl, resp[i]['peer'])
}
else{
toEl.innerText = resp[i]['name']
}
preview.innerText = '(' + resp[i]['subject'] + ')'
entry.classList.add('sentboxList')
entry.setAttribute('data-hash', resp[i]['hash'])
entry.appendChild(deleteBtn)
entry.appendChild(toLabel)
entry.appendChild(toEl)
entry.appendChild(preview)
entry.appendChild(sentDate)
threadPart.appendChild(entry)
entry.onclick = function(e){
if (e.target.classList.contains('deleteBtn')){
deleteMessage(e.target.parentNode.getAttribute('data-hash'))
e.target.parentNode.parentNode.removeChild(e.target.parentNode)
return
}
showSentboxWindow(toEl.innerText, message)
}
})(i, resp)
threadPart.appendChild(entry)
}.bind(threadPart))
}
function showSentboxWindow(to, content){
content = stripEndZeroes(content)
document.getElementById('toID').value = to
document.getElementById('sentboxDisplayText').innerText = content
overlay('sentboxDisplay')
}
function refreshPms(callNext){
if (! window.inboxActive){
return
}
if (document.hidden){
return
}
fetch('/mail/getinbox', {
headers: {
"token": webpass
}})
.then((resp) => resp.text())
.then(function(data) {
pms = data.split(',')
if (pms.length > 0){
noInbox.style.display = 'none'
}
if (callNext){
getInbox()
}
})
}
tabBtns.onclick = function(event){
var children = tabBtns.children[0].children
for (var i = 0; i < children.length; i++) {
var btn = children[i]
btn.classList.remove('is-active')
}
event.target.parentElement.parentElement.classList.add('is-active')
setActiveTab(event.target.innerText.toLowerCase())
}
for (var i = 0; i < document.getElementsByClassName('refresh').length; i++){
document.getElementsByClassName('refresh')[i].style.float = 'right'
}
fetch('/friends/list', {
headers: {
"token": webpass
}})
.then((resp) => resp.json()) // Transform the data into json
.then(function(resp) {
var friendSelectParent = document.getElementById('friendSelect')
var keys = [];
for(var k in resp) keys.push(k);
friendSelectParent.appendChild(document.createElement('option'))
for (var i = 0; i < keys.length; i++) {
var option = document.createElement("option")
var name = resp[keys[i]]['name'] || ""
option.value = keys[i]
if (name.length == 0){
option.text = keys[i]
}
else{
option.text = name
}
friendSelectParent.appendChild(option)
}
})
setActiveTab('inbox')
setInterval(function(){mailPing()}, 10000)
mailPing()
window.inboxInterval = setInterval(function(){refreshPms(true)}, 3000)
refreshPms(true)
document.addEventListener("visibilitychange", function() {
if (document.visibilityState === 'visible') {
refreshPms()
}
})
/*
let mailStream = function(){
var streamSource = new EventSourcePolyfill('/mail/streaminbox', {
headers: {
"token": webpass
}
})
streamSource.onmessage = function(e){
console.debug(e.data)
}
}
mailStream()
*/

View File

@ -1,109 +0,0 @@
/*
Onionr - Private P2P Communication
This file handles the mail interface
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
var sendbutton = document.getElementById('sendMail')
messageContent = document.getElementById('draftText')
to = document.getElementById('draftID')
subject = document.getElementById('draftSubject')
friendPicker = document.getElementById('friendSelect')
function utf8Length(s) {
var size = encodeURIComponent(s).match(/%[89ABab]/g);
return s.length + (size ? size.length : 0);
}
function padString(string_data, round_nearest_byte_exponent = 3){
if (utf8Length(string_data) === 0){
string_data += '0'
}
let round_size = 10 ** round_nearest_byte_exponent
while (utf8Length(string_data) % round_size > 0){
string_data += '0'
}
return string_data
}
function sendMail(toData, message, subject){
let meta = {'subject': subject}
if (document.getElementById('messagePaddingSetting').checked){
message = padString(message)
}
if (document.getElementById('mailSignatureSetting').value !== false){
message += "\n"
message += document.getElementById('mailSignatureSetting').value
}
postData = {'message': message, 'to': toData, 'type': 'pm', 'encrypt': true, 'meta': JSON.stringify(meta)}
postData.forward = document.getElementById('forwardSecrecySetting').checked
postData = JSON.stringify(postData)
sendForm.style.display = 'none'
fetch('/insertblock', {
method: 'POST',
body: postData,
headers: {
"content-type": "application/json",
"token": webpass
}}).then(function(resp){
if (!resp.ok){
PNotify.error({
text: 'Malformed input'
})
sendForm.style.display = 'block'
throw new Error("Malformed input in sendmail")
}
return resp
})
.then((resp) => resp.text()) // Transform the data into text
.then(function(data) {
sendForm.style.display = 'block'
PNotify.success({
text: 'Queued for sending!',
delay: 3500,
mouseReset: false
})
to.value = subject.value = messageContent.value = ""
friendPicker.value = ""
subject.value = ""
})
}
var friendPicker = document.getElementById('friendSelect')
friendPicker.onchange = function(){
to.value = friendPicker.value
}
sendForm.onsubmit = function(){
let getInstances = function(string, word) {
return string.split(word).length - 1;
}
if(getInstances(to.value, '-') != 15){
if (to.value.length != 56 && to.value.length != 52){
PNotify.error({
text: 'User ID is not valid'
})
return false
}
}
sendMail(to.value, messageContent.value, subject.value)
return false
}

View File

@ -1,40 +0,0 @@
/*
Onionr - Private P2P Communication
Load human readable public keys into a cache
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/>.
*/
function loadHumanReadableToCache(key){
fetch('/getHumanReadable/' + key, {
headers: {
"token": webpass
}})
.then((resp) => resp.text())
.then(function(resp) {
humanReadableCache[key] = resp
})
}
function setHumanReadableValue(el, key){
if (typeof humanReadableCache[key] != 'undefined'){
el.innerText = humanReadableCache[key].split('-').slice(0,3).join('-')
return
}
else{
loadHumanReadableToCache(key)
setTimeout(function(){setHumanReadableValue(el, key)}, 100)
return
}
}

View File

@ -1,120 +0,0 @@
/*
Onionr - Private P2P Communication
Handle mail settings
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
var notificationSetting = document.getElementById('notificationSetting')
var friendOnlyNotification = document.getElementById('strangersNotification')
var notificationSound = document.getElementById('notificationSound')
var sigSetting = document.getElementById('mailSignatureSetting')
document.getElementById('forwardSecrecySetting').onchange = function(e){
postData = JSON.stringify({"default_forward_secrecy": e.target.checked})
fetch('/config/set/mail', {
method: 'POST',
body: postData,
headers: {
"content-type": "application/json",
"token": webpass
}})
.then((resp) => resp.text())
.then(function(data) {
mailSettings['forwardSecrecy'] = document.getElementById('forwardSecrecySetting').checked
PNotify.success({
text: 'Successfully toggled default forward secrecy'
})
})
}
notificationSound.onchange = function(e){
var postData = JSON.stringify({"notificationSound": e.target.checked})
fetch('/config/set/mail', {
method: 'POST',
body: postData,
headers: {
"content-type": "application/json",
"token": webpass
}})
.then(function(data) {
mailSettings['notificationSound'] = notificationSound.checked
PNotify.success({
text: 'Successfully notification sound'
})
})
}
friendOnlyNotification.onchange = function(e){
var postData = JSON.stringify({"strangersNotification": e.target.checked})
fetch('/config/set/mail', {
method: 'POST',
body: postData,
headers: {
"content-type": "application/json",
"token": webpass
}})
.then(function(data) {
mailSettings['strangersNotification'] = friendOnlyNotification.checked
PNotify.success({
text: 'Successfully toggled notifications from strangers'
})
})
}
notificationSetting.onchange = function(e){
var notificationSettings = document.getElementsByClassName('notificationSetting')
if (e.target.checked){
for (i = 0; i < notificationSettings.length; i++){
notificationSettings[i].style.display = "flex"
}
}
else{
for (i = 0; i < notificationSettings.length; i++){
notificationSettings[i].style.display = "none"
}
}
var postData = JSON.stringify({"notificationSetting": e.target.checked})
fetch('/config/set/mail', {
method: 'POST',
body: postData,
headers: {
"content-type": "application/json",
"token": webpass
}})
.then(function(data) {
mailSettings['notificationSetting'] = notificationSetting.checked
PNotify.success({
text: 'Successfully toggled default mail notifications'
})
})
}
sigSetting.onchange = function(){
var postData = JSON.stringify({"signature": sigSetting.value})
fetch('/config/set/mail', {
method: 'POST',
body: postData,
headers: {
"content-type": "application/json",
"token": webpass
}})
.then(function(data) {
mailSettings['signature'] = sigSetting.value
PNotify.success({
text: 'Set mail signature'
})
})
}