Compare commits

...

8 Commits

Author SHA1 Message Date
Kevin F 8e730cef98 WOT API can now serialize identities 2022-12-02 21:42:11 +00:00
Kevin F 738fa0c361 Ping the RPC api before using the wot CLI 2022-12-02 21:41:48 +00:00
Kevin F b3eb0caffd Add result plugin to reduce confusing use of exceptions 2022-12-02 21:41:10 +00:00
Kevin F 446662cc60 Fix RPC plugin server binding incorrectly to TCP and doubling up JSON encoding 2022-12-02 21:40:33 +00:00
Kevin F 180116a55d Fix killdaemon logging 2022-12-02 21:19:42 +00:00
Kevin F bc3d6571bb merge seattle work 2022-11-27 01:03:44 +00:00
Kevin F 30d50ceacf merge seattle work 2022-11-27 01:03:36 +00:00
Kevin F e84ad93de7 work on wot cli 2022-11-22 00:57:14 -05:00
19 changed files with 155 additions and 143 deletions

View File

@ -1,124 +0,0 @@
# Onionr HTTP API
All HTTP interfaces in the Onionr reference client use the [Flask](http://flask.pocoo.org/) web framework with the [gevent](http://www.gevent.org/) WSGI server.
## Client & Public difference
The client API server is a locked down interface intended for authenticated local communication.
The public API server is available only remotely from Tor & I2P. It is the interface in which peers use to communicate with one another.
# Client API
Please note: endpoints that simply provide static web app files are not documented here.
* /serviceactive/pubkey
- Methods: GET
- Returns true or false based on if a given public key has an active direct connection service.
* /queueResponseAdd/key (DEPRECATED)
- Methods: POST
- Accepts form key 'data' to set queue response information from a plugin
- Returns success if no error occurs
* /queueResponse/key (DEPRECATED)
- Methods: GET
- Returns the queue response for a key. Returns failure with a 404 code if a code is not set.
* /ping
- Methods: GET
- Returns "pong!"
* /getblocksbytype/type
- Methods: GET
- Returns a list of stored blocks by a given type
* /getblockbody/hash
- Methods: GET
- Returns the main data section of a block
* /getblockdata/hash
- Methods: GET
- Returns the entire data contents of a block, including metadata.
* /getblockheader/hash
- Methods: GET
- Returns the header (metadata section) of a block.
* /gethidden/
- Methods: GET
- Returns line separated list of hidden blocks
* /hitcount
- Methods: GET
- Return the amount of requests the public api server has received this session
* /lastconnect
- Methods: GET
- Returns the epoch timestamp of when the last incoming connection to the public API server was logged
* /site/hash
- Methods: GET
- Returns HTML content out of a block
* /waitforshare/hash
- Methods: POST
- Prevents the public API server from listing or sharing a block until it has been uploaded to at least 1 peer.
* /shutdown
- Methods: GET
- Shutdown Onionr. You should probably use /shutdownclean instead.
* /shutdownclean
- Methods: GET
- Tells the communicator daemon to shutdown Onionr. Slower but cleaner.
* /getstats
- Methods: GET
- Returns some JSON serialized statistics
* /getuptime
- Methods: GET
- Returns uptime in seconds
* /getActivePubkey
- Methods: GET
- Returns the current active public key in base32 format
* /getHumanReadable/pubkey
- Methods: GET
- Echos the specified public key in mnemonic format
* /insertblock
- Methods: POST
- Accepts JSON data for creating a new block. 'message' contains the block data, 'to' specifies the peer's public key to encrypt the data to, 'sign' is a boolean for signing the message.
* /torready
- Methods: POST
- Returns boolean if Tor is started or not
# Public API
v0
* /
- Methods: GET
- Returns a basic HTML informational banner describing Onionr.
* /getblocklist
- Methods: GET
- URI Parameters:
- date: unix epoch timestamp for offset
- Returns a list of block hashes stored on the node since an offset (all blocks if no timestamp is specified)
* /getdata/block-hash
- Methods: GET
- Returns data for a block based on a provided hash
* /www/file-path
- Methods: GET
- Returns file data. Intended for manually sharing file data directly from an Onionr node.
* /ping
- Methods: GET
- Returns 'pong!'
* /pex
- Methods: GET
- Returns a list of peer addresses reached within recent time
* /announce
- Methods: POST
- Accepts form data for 'node' (valid node address) and 'random' which is a nonce when hashed (blake2b_256) in the format `hash(peerAddress+serverAddress+nonce)`, begins with at least 5 zeros.
- Returns 200 with 'Success' if no error occurs. If the post is invalid, 'failure' with code 406 is returned.
* /upload
- Methods: POST
- Accepts form data for 'block' as a 'file' upload.
- Returns 200 with 'success' if no error occurs. If the block cannot be accepted, 'failure' with 400 is returned.
# Direct Connection API
These are constant endpoints available on direct connection servers. Plugin endpoints for direct connections are not documented here.
* /ping
- Methods: GET
- Returns 200 with 'pong!'
* /close
- Methods: GET
- Kills the direct connection server, destroying the onion address.
- Returns 200 with 'goodbye'

View File

@ -2,6 +2,8 @@
ORIG_ONIONR_RUN_DIR=`pwd` ORIG_ONIONR_RUN_DIR=`pwd`
export ORIG_ONIONR_RUN_DIR export ORIG_ONIONR_RUN_DIR
export PYTHONDONTWRITEBYTECODE=1 export PYTHONDONTWRITEBYTECODE=1
export PYTHONUNBUFFERED=1
export PYTHONOPTIMIZE="true"
cd "$(dirname "$0")" cd "$(dirname "$0")"
cd src cd src
./__init__.py "$@" ./__init__.py "$@"

View File

@ -197,6 +197,10 @@ pynacl==1.5.0 \
--hash=sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b \ --hash=sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b \
--hash=sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543 --hash=sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543
# via onionrblocks # via onionrblocks
result==0.8.0 \
--hash=sha256:c48c909e92181a075ba358228a3fe161e26d205dad416ad81f27f23515a5626d \
--hash=sha256:d6a6258f32c057a4e0478999c6ce43dcadaf8ea435f58ac601ae2768f93ef243
# via -r requirements-base.in
ujson==5.5.0 \ ujson==5.5.0 \
--hash=sha256:0762a4fdf86e01f3f8d8b6b7158d01fdd870799ff3f402b676e358fcd879e7eb \ --hash=sha256:0762a4fdf86e01f3f8d8b6b7158d01fdd870799ff3f402b676e358fcd879e7eb \
--hash=sha256:10095160dbe6bba8059ad6677a01da251431f4c68041bf796dcac0956b34f8f7 \ --hash=sha256:10095160dbe6bba8059ad6677a01da251431f4c68041bf796dcac0956b34f8f7 \

View File

@ -1,6 +1,7 @@
psutil==5.9.3 psutil==5.9.3
ujson==5.5.0 ujson==5.5.0
ordered-set==4.1.0 ordered-set==4.1.0
result==0.8.0
# These two are also by Kevin # These two are also by Kevin
filenuke==0.0.0 filenuke==0.0.0
onionrblocks==7.0.0 onionrblocks==7.0.0

View File

@ -345,6 +345,10 @@ pytz==2022.2.1 \
--hash=sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197 \ --hash=sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197 \
--hash=sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5 --hash=sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5
# via tempora # via tempora
result==0.8.0 \
--hash=sha256:c48c909e92181a075ba358228a3fe161e26d205dad416ad81f27f23515a5626d \
--hash=sha256:d6a6258f32c057a4e0478999c6ce43dcadaf8ea435f58ac601ae2768f93ef243
# via -r requirements-base.in
secretstorage==3.3.3 \ secretstorage==3.3.3 \
--hash=sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77 \ --hash=sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77 \
--hash=sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99 --hash=sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99

View File

@ -112,6 +112,7 @@ def onionr_main():
Entrypoint for daemon is commands/daemonlaunch/__init__.py Entrypoint for daemon is commands/daemonlaunch/__init__.py
""" """
events.event('beforecmdparsing', threaded=False)
parser.register() parser.register()

View File

@ -8,8 +8,6 @@ if TYPE_CHECKING:
from onionrplugins import onionrevents from onionrplugins import onionrevents
from logger import log as logging from logger import log as logging
from socks import GeneralProxyError
from ..peer import Peer from ..peer import Peer
from ..commands import GossipCommands, command_to_byte from ..commands import GossipCommands, command_to_byte
from ..constants import PEER_AMOUNT_TO_ASK, TRANSPORT_SIZE_BYTES from ..constants import PEER_AMOUNT_TO_ASK, TRANSPORT_SIZE_BYTES

View File

@ -30,8 +30,7 @@ def kill_daemon():
os.kill(int(pid.read()), SIGTERM) os.kill(int(pid.read()), SIGTERM)
except FileNotFoundError: except FileNotFoundError:
logging.error("Daemon not running/pid file missing") logging.error("Daemon not running/pid file missing")
logging.warn('Stopping the running daemon, if one exists...', timestamp=False, logging.warn('Stopping the running daemon, if one exists...')
)
kill_daemon.onionr_help = "Gracefully stops the " # type: ignore kill_daemon.onionr_help = "Gracefully stops the " # type: ignore

View File

@ -17,7 +17,8 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os, re, importlib import os, re
import importlib.util
import traceback import traceback
from . import onionrevents as events from . import onionrevents as events

View File

@ -42,18 +42,22 @@ socket_file_path = identifyhome.identify_home() + 'rpc.sock'
from jsonrpc import JSONRPCResponseManager, dispatcher from jsonrpc import JSONRPCResponseManager, dispatcher
import jsonrpc import jsonrpc
import ujson import ujson
import requests_unixsocket
import requests
jsonrpc.manager.json = ujson jsonrpc.manager.json = ujson
from onionrplugins import plugin_apis
# RPC modules map Onionr APIs to the RPC dispacher # RPC modules map Onionr APIs to the RPC dispacher
from rpc import blocks, pluginrpcmethods from rpc import blocks, pluginrpcmethods
from rpc.addmodule import add_module_to_api from rpc.addmodule import add_module_to_api
plugin_apis['rpc.add_module_to_api'] = add_module_to_api plugin_apis['rpc.add_module_to_api'] = add_module_to_api
class OnionrRPC(object): class OnionrRPC(object):
@cherrypy.expose @cherrypy.expose
@cherrypy.tools.json_out()
def rpc(self): def rpc(self):
# Dispatcher is dictionary {<method_name>: callable} # Dispatcher is dictionary {<method_name>: callable}
data = cherrypy.request.body.read().decode('utf-8') data = cherrypy.request.body.read().decode('utf-8')
@ -62,6 +66,20 @@ class OnionrRPC(object):
return response.json return response.json
def rpc_client(*args, **kwargs):
if config.get('rpc.use_sock_file', True):
session = requests_unixsocket.Session()
return session.post(
'http+unix://' + config.get('rpc.sock_file_path', socket_file_path).replace('/', '%2F') + '/rpc',
*args, **kwargs)
else:
return requests.post(
f'http://{config.get("rpc.bind_host")}:{config.get("rpc.bind_port")}/rpc',
*args, **kwargs)
def on_beforecmdparsing(api, data=None):
plugin_apis['rpc.rpc_client'] = rpc_client
def on_afterinit(api, data=None): def on_afterinit(api, data=None):
def ping(): def ping():
return "pong" return "pong"

View File

@ -1,11 +1,52 @@
import tty import tty
import sys import sys
import subprocess import subprocess
import traceback
import ujson as json
import result
import requests
import requests_unixsocket
from logger import log as logging
import onionrplugins.pluginapis
def do_quit(): raise KeyboardInterrupt def do_quit(): raise KeyboardInterrupt
rpc_payload = {
"method": "echo",
"params": ["example"],
"jsonrpc": "2.0",
"id": 0,
}
def list_idens(): def list_idens():
print('Listing identities') print('Listing identities')
payload = dict(rpc_payload)
payload['method'] = 'wot.serialize_identity_set'
del payload['params']
print(onionrplugins.pluginapis.plugin_apis['rpc.rpc_client'](json=payload).text)
def ping_api() -> result.Result:
payload = dict(rpc_payload)
payload['method'] = 'ping'
del payload['params']
try:
_ping_res = onionrplugins.pluginapis.plugin_apis['rpc.rpc_client'](json=payload).text
except requests.exceptions.ConnectionError:
logging.debug(traceback.format_exc())
return result.Err('Could not connect to Onionr RPC server. Please ensure the RPC plugin is enabled and the Onionr daemon is running')
except:
logging.error(traceback.format_exc())
return result.Err('Unknown error occurred while connecting to Onionr RPC server')
_ping_res = json.loads(_ping_res)
if _ping_res['result'] == 'pong':
return result.Ok()
else:
return result.Err('API not responding. Try restarting Onionr')
main_menu = { main_menu = {
@ -14,18 +55,28 @@ main_menu = {
} }
def main_ui(): def main_ui():
tty.setraw(sys.stdin) #tty.setraw(sys.stdin)
try:
onionrplugins.pluginapis.plugin_apis['rpc.rpc_client']
except KeyError:
logging.error("Web of trust CLI requires RPC plugin to be enabled")
return
ping_result: result.Result = ping_api()
if not isinstance(ping_result, result.Ok):
logging.error(ping_result)
return
while True: while True:
# move cursor to the beginning # move cursor to the beginning
print('\r', end='') print('\r', end='')
key = sys.stdin.read(1)
try: try:
main_menu[key][1]() key = sys.stdin.read(1)
main_menu[key][0]()
except KeyError: except KeyError:
pass pass
except KeyboardInterrupt: except KeyboardInterrupt:
break break
subprocess.Popen(['reset'], stdout=subprocess.PIPE) #subprocess.Popen(['reset'], stdout=subprocess.PIPE)

View File

@ -0,0 +1,31 @@
import base64
import keyring.errors
from nacl.signing import SigningKey
from logger import log as logging
import config
from wot import wotkeyring
from wot.identity import Identity
def create_new_iden():
iden = Identity(
SigningKey.generate(),
input('Enter a name for your identity: '))
try:
wotkeyring.set_identity(iden)
except keyring.errors.NoKeyringError:
logging.warn(
"Could not use secure keyring to store your WOT " +
"private key, using config.")
logging.info("Using config file to store identity private key")
config.set(
'wot.identity.{iden.name}',
base64.b85encode(
bytes(iden.private_key)).decode('utf-8'), savefile=True)
config.set(
'wot.active_identity_name', iden.name, savefile=True)
logging.info(
'Identity created and automatically set as active. ' +
'Restart Onionr to use it.')

View File

@ -0,0 +1 @@
def trust_identity():

View File

@ -80,9 +80,17 @@ def on_init(api, data=None):
"<name>' and restart Onionr") "<name>' and restart Onionr")
return return
try: if config.get('wot.use_system_keyring', True):
try:
iden = wotkeyring.get_identity_by_name(active_identity)
except KeyError:
logging.error(
f"Could not load identity {active_identity} " +
"from keyring despite configuration choice to do so")
else:
# load from file
iden = load_identity_from_config(active_identity) iden = load_identity_from_config(active_identity)
except KeyError:
try: try:
iden = wotkeyring.get_identity_by_name(active_identity) iden = wotkeyring.get_identity_by_name(active_identity)
except KeyError: except KeyError:
@ -91,6 +99,8 @@ def on_init(api, data=None):
return return
logging.info('Loaded active identity: ' + iden.name) logging.info('Loaded active identity: ' + iden.name)
identities.add(iden)
def on_wot_cmd(api, data=None): def on_wot_cmd(api, data=None):
def _create_new_iden(): def _create_new_iden():

View File

@ -6,3 +6,4 @@ from .identity import Identity
from .getbykey import get_identity_by_key from .getbykey import get_identity_by_key
from .identity import identities from .identity import identities
from .identity.identityset import serialize_identity_set from .identity.identityset import serialize_identity_set

View File

@ -1,3 +1,5 @@
import base64
class IdentitySet(set): class IdentitySet(set):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -32,7 +34,7 @@ identities = IdentitySet()
def serialize_identity_set(): def serialize_identity_set():
serialized_idens = [] serialized_idens = []
for identity in list(identities): for identity in list(identities):
serialized_idens.append(identity.serialize()) serialized_idens.append(base64.b85encode(identity.serialize()).decode('utf-8'))
return serialized_idens return serialized_idens
serialize_identity_set.json_compatible = True serialize_identity_set.json_compatible = True

View File

@ -11,6 +11,7 @@ from wot.identity import Identity, identities
from wot.exceptions import IdentitySerializationError from wot.exceptions import IdentitySerializationError
from wot.getbykey import get_identity_by_key from wot.getbykey import get_identity_by_key
from wot.identityprocessing import processtrustsignature from wot.identityprocessing import processtrustsignature
import wot.wotcommand
def load_identity_from_block(block) -> Identity: def load_identity_from_block(block) -> Identity:
@ -26,7 +27,7 @@ def load_identities_from_blocks() -> Generator[Identity, None, None]:
def load_signatures_from_blocks() -> None: def load_signatures_from_blocks() -> None:
for block in blockdb.get_blocks_by_type(b'wots'): for block in blockdb.get_blocks_by_type(wot.wotcommand.block_type_map.trust):
try: try:
# If good signature, # If good signature,
# it adds the signature to the signed identity's trust set # it adds the signature to the signed identity's trust set

View File

@ -1,7 +1,15 @@
from enum import IntEnum, auto from enum import IntEnum, auto
from types import SimpleNamespace
class WotCommand(IntEnum): class WotCommand(IntEnum):
TRUST = 1 TRUST = 1
REVOKE_TRUST = auto() REVOKE_TRUST = auto()
ANNOUNCE = auto() ANNOUNCE = auto()
REVOKE = auto() REVOKE = auto()
_block_type_map = {
'trust': b'wots'
}
block_type_map = SimpleNamespace(**_block_type_map)

View File

@ -1,16 +1,19 @@
import base64 import base64
import keyring
from wot.identity import Identity import keyring
import nacl.signing
import wot.identity
def get_identity_by_name(name: str) -> 'Identity': def get_identity_by_name(name: str) -> 'Identity':
iden_key = keyring.get_credential('onionr.wot', name) iden_key = keyring.get_credential('onionr.wot', name)
iden_key = base64.b85decode(iden_key)
if not iden_key: if not iden_key:
raise KeyError('Identity not found') raise KeyError('Identity not found')
return Identity(iden_key, name) iden_key = base64.b85decode(iden_key.password)
iden_key = nacl.signing.SigningKey(iden_key)
return wot.identity.Identity(iden_key, name)
def set_identity(identity: 'Identity') -> None: def set_identity(identity: 'Identity') -> None: