mirror of
https://github.com/aljazceru/python-teos.git
synced 2025-12-17 06:04:21 +01:00
Removes apps folder
This commit is contained in:
80
cli/DEPENDENCIES.md
Normal file
80
cli/DEPENDENCIES.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Dependencies
|
||||
|
||||
`teos_cli` has both system-wide and Python dependencies. This document walks you through how to satisfy them.
|
||||
|
||||
## System-wide dependencies
|
||||
|
||||
`teos_cli` has the following system-wide dependencies:
|
||||
|
||||
- `python3`
|
||||
- `pip3`
|
||||
|
||||
### Checking if the dependencies are already satisfied
|
||||
|
||||
Most UNIX systems ship with `python3` already installed, whereas OSX systems tend to ship with `python2`. In order to check our python version we should run:
|
||||
|
||||
python --version
|
||||
|
||||
For what we will get something like:
|
||||
|
||||
Python 2.X.X
|
||||
|
||||
Or
|
||||
|
||||
Python 3.X.X
|
||||
|
||||
It is also likely that, if `python3` is installed in our system, the `python` alias is not set to it but instead to `python2`. In order to check so, we can run:
|
||||
|
||||
python3 --version
|
||||
|
||||
If `python3` is installed but the `python` alias is not set to it, we should either set it, or use `python3` to run `teos_cli`.
|
||||
|
||||
Regarding `pip`, we can check what version is installed in our system (if any) by running:
|
||||
|
||||
pip --version
|
||||
|
||||
For what we will get something like:
|
||||
|
||||
pip X.X.X from /usr/local/lib/python2.X/dist-packages/pip (python 2.X)
|
||||
|
||||
Or
|
||||
|
||||
pip X.X.X from /usr/local/lib/python3.X/dist-packages/pip (python 3.X)
|
||||
|
||||
A similar thing to the `python` alias applies to the `pip` alias. We can check if pip3 is install by running:
|
||||
|
||||
pip3 --version
|
||||
|
||||
And, if it happens to be installed, change the alias to `pip3`, or use `pip3` instead of `pip`.
|
||||
|
||||
|
||||
### Installing the dependencies
|
||||
|
||||
`python3` ca be downloaded from the [Python official website](https://www.python.org/downloads/) or installed using a package manager, depending on your distribution. Examples for both UNIX-like and OSX systems are provided.
|
||||
|
||||
#### Ubuntu
|
||||
|
||||
`python3` can be installed using `apt` as follows:
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install python3
|
||||
|
||||
and for `pip3`:
|
||||
|
||||
sudo apt-get install python3-pip
|
||||
pip install --upgrade pip==9.0.3
|
||||
|
||||
#### OSX
|
||||
|
||||
`python3` can be installed using `Homebrew` as follows:
|
||||
|
||||
brew install python3
|
||||
|
||||
`pip3` will be installed alongside `python3` in this case.
|
||||
|
||||
## Python dependencies
|
||||
|
||||
`teos_cli` has the following dependencies (which can be satisfied by using `pip install -r requirements.txt`):
|
||||
|
||||
- `cryptography`
|
||||
- `requests`
|
||||
35
cli/INSTALL.md
Normal file
35
cli/INSTALL.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Install
|
||||
|
||||
`teos_cli` has some dependencies that can be satisfied by following [DEPENDENCIES.md](DEPENDENCIES.md). If your system already satisfies the dependencies, you can skip that part.
|
||||
|
||||
There are two ways of running `teos_cli`: adding the library to the `PYTHONPATH` env variable, or running it as a module.
|
||||
|
||||
## Modifying `PYTHONPATH`
|
||||
In order to run `teos_cli`, you should set your `PYTHONPATH` env variable to include the folder that contains the `apps` folder. You can do so by running:
|
||||
|
||||
export PYTHONPATH=$PYTHONPATH:<absolute_path_to_apps>
|
||||
|
||||
For example, for user alice running a UNIX system and having `apps` in her home folder, she would run:
|
||||
|
||||
export PYTHONPATH=$PYTHONPATH:/home/alice/
|
||||
|
||||
You should also include the command in your `.bashrc` to avoid having to run it every time you open a new terminal. You can do it by running:
|
||||
|
||||
echo 'export PYTHONPATH=$PYTHONPATH:<absolute_path_to_apps>' >> ~/.bashrc
|
||||
|
||||
Once the `PYTHONPATH` is set, you should be able to run `teos_cli` straightaway. Try it by running:
|
||||
|
||||
cd <absolute_path_to_apps>/apps/cli
|
||||
python teos_cli.py -h
|
||||
|
||||
## Running `teos_cli` as a module
|
||||
Python code can be also run as a module, to do so you need to use `python -m`. From `apps` **parent** directory run:
|
||||
|
||||
python -m apps.cli.teos_cli -h
|
||||
|
||||
Notice that if you run `teos_cli` as a module, you'll need to replace all the calls from `python teos_cli.py <argument>` to `python -m apps.cli.teos_cli <argument>`
|
||||
|
||||
## Modify configuration parameters
|
||||
If you'd like to modify some of the configuration defaults (such as the user directory, where the logs and appointment receipts will be stored) you can do so in the config file located at:
|
||||
|
||||
<absolute_path_to_apps>/apps/cli/conf.py
|
||||
158
cli/README.md
Normal file
158
cli/README.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# teos_cli
|
||||
|
||||
`teos_cli` is a command line interface to interact with the Eye of Satoshi watchtower server, written in Python3.
|
||||
|
||||
## Dependencies
|
||||
Refer to [DEPENDENCIES.md](DEPENDENCIES.md)
|
||||
|
||||
## Installation
|
||||
|
||||
Refer to [INSTALL.md](INSTALL.md)
|
||||
|
||||
## Usage
|
||||
|
||||
python teos_cli.py [global options] command [command options] [arguments]
|
||||
|
||||
#### Global options
|
||||
|
||||
- `-s, --server`: API server where to send the requests. Defaults to https://teos.pisa.watch (modifiable in conf.py)
|
||||
- `-p, --port` : API port where to send the requests. Defaults to 443 (modifiable in conf.py)
|
||||
- `-h --help`: shows a list of commands or help for a specific command.
|
||||
|
||||
#### Commands
|
||||
|
||||
The command line interface has, currently, three commands:
|
||||
|
||||
- `add_appointment`: registers a json formatted appointment to the tower.
|
||||
- `get_appointment`: gets json formatted data about an appointment from the tower.
|
||||
- `help`: shows a list of commands or help for a specific command.
|
||||
|
||||
### add_appointment
|
||||
|
||||
This command is used to register appointments to the watchtower. Appointments **must** be `json` encoded, and match the following format:
|
||||
|
||||
{ "tx": tx,
|
||||
"tx_id": tx_id,
|
||||
"start_time": s,
|
||||
"end_time": e,
|
||||
"to_self_delay": d }
|
||||
|
||||
`tx` **must** be the raw penalty transaction that will be encrypted before sent to the watchtower. `type(tx) = hex encoded str`
|
||||
|
||||
`tx_id` **must** match the **commitment transaction id**, and will be used to encrypt the **penalty transaction** and **generate the locator**. `type(tx_id) = hex encoded str`
|
||||
|
||||
`s` is the time when the watchtower will start watching your transaction, and will normally match to whenever you will be offline. `s` is measured in block height, and must be **higher than the current block height**. `type(s) = int`
|
||||
|
||||
`e` is the time where the watchtower will stop watching your transaction, and will normally match with whenever you should be back online. `e` is also measured in block height, and must be **higher than** `s`. `type(e) = int`
|
||||
|
||||
`d` is the time the watchtower would have to respond with the **penalty transaction** once the **dispute transaction** is seen in the blockchain. `d` must match with the `OP_CSV` specified in the dispute transaction. If the to\_self\_delay does not match the `OP_CSV`, the watchtower will try to respond with the penalty transaction anyway, but success is not guaranteed. `d` is measured in blocks and should be at least `20`. `type(d) = int`
|
||||
|
||||
The API will return a `application/json` HTTP response code `200/OK` if the appointment is accepted, with the locator encoded in the response text, or a `400/Bad Request` if the appointment is rejected, with the rejection reason encoded in the response text.
|
||||
|
||||
### Alpha release restrictions
|
||||
The alpha release does not have authentication, payments nor rate limiting, therefore some self imposed restrictions apply:
|
||||
|
||||
- `start_time` should be within the next 6 blocks `[current_time+1, current_time+6]`.
|
||||
- `end_time` cannot be bigger than (roughly) a month. That is `4320` blocks on top of `start_time`.
|
||||
- `encrypted_blob`s are limited to `2 kib`.
|
||||
|
||||
|
||||
#### Usage
|
||||
|
||||
python teos_cli.py add_appointment [command options] <appointment>/<path_to_appointment_file>
|
||||
|
||||
if `-f, --file` **is** specified, then the command expects a path to a json file instead of a json encoded string as parameter.
|
||||
|
||||
#### Options
|
||||
- `-f, --file path_to_json_file` loads the appointment data from the specified json file instead of command line.
|
||||
|
||||
### get_appointment
|
||||
|
||||
This command is used to get information about an specific appointment from the Eye of Satoshi.
|
||||
|
||||
**Appointment can be in three states:**
|
||||
|
||||
- `not_found`: meaning the locator is not recognised by the tower. This can either mean the locator is wrong, or the appointment has already been fulfilled (the tower does not keep track of completed appointments for now).
|
||||
- `being_watched`: the appointment has been accepted by the tower and it's being watched at the moment. This stage means that the dispute transaction has not been seen yet, and therefore no penalty transaction has been broadcast.
|
||||
- `dispute_responded`: the dispute was found by the watcher and the corresponding penalty transaction has been broadcast by the node. In this stage the tower is actively monitoring until the penalty transaction reaches enough confirmations and making sure no fork occurs in the meantime.
|
||||
|
||||
**Response formats**
|
||||
|
||||
**not_found**
|
||||
|
||||
[{"locator": appointment_locator,
|
||||
"status":"not_found"}]
|
||||
|
||||
**being_watched**
|
||||
|
||||
[{"encrypted_blob": eb,
|
||||
"end_time": e,
|
||||
"locator": appointment_locator,
|
||||
"start_time": s,
|
||||
"status": "being_watched",
|
||||
"to_self_delay": d}]
|
||||
|
||||
**dispute_responded**
|
||||
|
||||
[{"appointment_end": e,
|
||||
"dispute_txid": dispute_txid,
|
||||
"locator": appointment_locator,
|
||||
"penalty_rawtx": penalty_rawtx,
|
||||
"penalty_txid": penalty_txid,
|
||||
"status": "dispute_responded"}]
|
||||
|
||||
#### Usage
|
||||
|
||||
python teos_cli.py get_appointment <appointment_locator>
|
||||
|
||||
|
||||
|
||||
### help
|
||||
|
||||
Shows the list of commands or help about how to run a specific command.
|
||||
|
||||
#### Usage
|
||||
python teos_cli.py help
|
||||
|
||||
or
|
||||
|
||||
python teos_cli.py help command
|
||||
|
||||
## Example
|
||||
|
||||
1. Generate a new dummy appointment. **Note:** this appointment will never be fulfilled (it will eventually expire) since it does not corresopond to a valid transaction. However it can be used to interact with the Eye of Satoshi's API.
|
||||
|
||||
```
|
||||
echo '{"tx": "4615a58815475ab8145b6bb90b1268a0dbb02e344ddd483f45052bec1f15b1951c1ee7f070a0993da395a5ee92ea3a1c184b5ffdb2507164bf1f8c1364155d48bdbc882eee0868ca69864a807f213f538990ad16f56d7dfb28a18e69e3f31ae9adad229e3244073b7d643b4597ec88bf247b9f73f301b0f25ae8207b02b7709c271da98af19f1db276ac48ba64f099644af1ae2c90edb7def5e8589a1bb17cc72ac42ecf07dd29cff91823938fd0d772c2c92b7ab050f8837efd46197c9b2b3f", "tx_id": "0b9510d92a50c1d67c6f7fc5d47908d96b3eccdea093d89bcbaf05bcfebdd951", "start_time": 0, "end_time": 0, "to_self_delay": 20}' > dummy_appointment_data.json
|
||||
```
|
||||
|
||||
That will create a json file that follows the appointment data structure filled with dummy data and store it in `dummy_appointment_data.json`. **Note**: You'll need to update the `start_time` and `end_time` to match valid block heights.
|
||||
|
||||
2. Send the appointment to the tower API. Which will then start monitoring for matching transactions.
|
||||
|
||||
```
|
||||
python teos_cli.py add_appointment -f dummy_appointment_data.json
|
||||
```
|
||||
|
||||
This returns a appointment locator that can be used to get updates about this appointment from the tower.
|
||||
|
||||
3. Test that the tower is still watching the appointment by replacing the appointment locator received into the following command:
|
||||
|
||||
```
|
||||
python teos_cli.py get_appointment <appointment_locator>
|
||||
```
|
||||
|
||||
## the Eye of Satoshi's API
|
||||
|
||||
If you wish to read about the underlying API, and how to write your own tool to interact with it, refer to [tEOS-API.md](TEOS-API.md).
|
||||
|
||||
## Are you reckless? Try me on mainnet
|
||||
Would you like to try me on `mainnet` instead of `testnet`? Add `-s https://mainnet.teos.pisa.watch` to your calls, for example:
|
||||
|
||||
```
|
||||
python teos_cli.py -s https://teosmainnet.pisa.watch add_appointment -f dummy_appointment_data.json
|
||||
```
|
||||
|
||||
You can also change the config file to avoid specifying the server every time:
|
||||
|
||||
`DEFAULT_TEOS_API_SERVER = "https://teosmainnet.pisa.watch"`
|
||||
96
cli/TEOS-API.md
Normal file
96
cli/TEOS-API.md
Normal file
@@ -0,0 +1,96 @@
|
||||
## TEOS-API
|
||||
|
||||
### Disclaimer: Everything in here is experimental and subject to change.
|
||||
|
||||
The Eye of Satoshi's REST API consists, currently, of two endpoints: `/` and `/get_appointment`
|
||||
|
||||
`/` is the default endpoint, and is where the appointments should be sent to. `/` accepts `HTTP POST` requests only, with json request body, where data must match the following format:
|
||||
|
||||
{"locator": l, "start_time": s, "end_time": e,
|
||||
"to_self_delay": d, "encrypted_blob": eb}
|
||||
|
||||
We'll discuss the parameters one by one in the following:
|
||||
|
||||
The locator, `l`, is the first half of the **dispute transaction id** (i.e. the 16 MSB of the dispute_txid encoded in hex). `type(l) = hex encoded str`
|
||||
|
||||
The start\_time, `s`, is the time when the tower will start watching your transaction, and will normally match with whenever you will be offline. `s` is measured in block height, and must be **higher than the current block height**. `type(s) = int`
|
||||
|
||||
The end\_time, `e`, is the time where the tower will stop watching your transaction, and will normally match with whenever you should be back online. `e` is also measured in block height, and must be **higher than** `s`. `type(e) = int`
|
||||
|
||||
The to\_self\_delay, `d`, is the time the tower would have to respond with the **penalty transaction** once the **dispute transaction** is seen in the blockchain. `d` must match with the `OP_CSV` specified in the dispute transaction. If the dispute_delta does not match the `OP_CSV `, the tower would try to respond with the penalty transaction anyway, but success is not guaranteed. `d` is measured in blocks and should be, at least, `20`. `type(d) = int`
|
||||
|
||||
The encrypted\_blob, `eb`, is a data blob containing the `raw penalty transaction` and it is encrypted using `CHACHA20-POLY1305`. The `encryption key` used by the cipher is the sha256 of the **dispute transaction id**, and the `nonce` is a 12-byte long zero byte array:
|
||||
|
||||
sk = sha256(unhexlify(secret)).digest()
|
||||
nonce = bytearray(12) # b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
|
||||
Finally, the encrypted blob must be hex encoded. `type(eb) = hex encoded str`
|
||||
|
||||
The API will return a `application/json` HTTP response code `200/OK` if the appointment is accepted, with the locator encoded in the response text, or a `400/Bad Request` if the appointment is rejected, with the rejection reason encoded in the response text.
|
||||
|
||||
### Alpha release restrictions
|
||||
The alpha release does not have authentication, payments nor rate limiting, therefore some self imposed restrictions apply:
|
||||
|
||||
- `start_time` should be within the next 6 blocks `[current_time+1, current_time+6]`.
|
||||
- `end_time` cannot be bigger than (roughtly) a month. That is `4320` blocks on top of `start_time`.
|
||||
- `encrypted_blob`s are limited to `2 kib`.
|
||||
|
||||
#### Appointment example
|
||||
|
||||
{"locator": "3c3375883f01027e5ca14f9760a8b853824ca4ebc0258c00e7fae4bae2571a80",
|
||||
"start_time": 1568118,
|
||||
"end_time": 1568120,
|
||||
"to_self_delay": 20,
|
||||
"encrypted_blob": "6c7687a97e874363e1c2b9a08386125e09ea000a9b4330feb33a5c698265f3565c267554e6fdd7b0544ced026aaab73c255bcc97c18eb9fa704d9cc5f1c83adaf921de7ba62b2b6ddb1bda7775288019ec3708642e738eddc22882abf5b3f4e34ef2d4077ed23e135f7fe22caaec845982918e7df4a3f949cadd2d3e7c541b1dbf77daf64e7ed61531aaa487b468581b5aa7b1da81e2617e351c9d5cf445e3391c3fea4497aaa7ad286552759791b9caa5e4c055d1b38adfceddb1ef2b99e3b467dd0b0b13ce863c1bf6b6f24543c30d"}
|
||||
|
||||
# Get appointment
|
||||
|
||||
`/get_appointment` is an endpoint provided to check the status of the appointments sent to the tower. The endpoint is accessible without any type of authentication for now. `/get_appointment` accepts `HTTP GET` requests only, where the data to be provided must be the **locator** of an appointment. The query must match the following format:
|
||||
|
||||
`https://teos_server:teos_port/get_appointment?locator=appointment_locator`
|
||||
|
||||
**Appointment can be in three states**:
|
||||
|
||||
- `not_found`: meaning the locator is not recognised by the API. This could either mean the locator is wrong, or the appointment has already been fulfilled.
|
||||
- `being_watched`: the appointment has been accepted by the tower server and it's being watched at the moment. This stage means that the dispute transaction has not been seen yet, and therefore no penalty transaction has been published.
|
||||
- `dispute_responded`: the dispute was found by the watcher and the corresponding penalty transaction has been broadcast by the node. In this stage the tower is actively monitoring until the penalty transaction reaches enough confirmations and making sure no fork occurs in the meantime.
|
||||
|
||||
### Get appointment response formats
|
||||
|
||||
`/get_appointment` will always reply with `json` containing the information about the requested appointment. The structure is as follows:
|
||||
|
||||
**not_found**
|
||||
|
||||
[{"locator": appointment_locator,
|
||||
"status":"not_found"}]
|
||||
|
||||
**being_watched**
|
||||
|
||||
[{"encrypted_blob": eb,
|
||||
"end_time": e,
|
||||
"locator": appointment_locator,
|
||||
"start_time": s,
|
||||
"status": "being_watched",
|
||||
"to_self_delay": d}]
|
||||
|
||||
**dispute_responded**
|
||||
|
||||
[{"appointment_end": e,
|
||||
"dispute_txid": dispute_txid,
|
||||
"locator": appointment_locator,
|
||||
"penalty_rawtx": penalty_rawtx,
|
||||
"penalty_txid": penalty_txid,
|
||||
"status": "dispute_responded"}]
|
||||
|
||||
Notice that the response json always contains a list. Why? It is possible for both parties to send the “same locator” to our service:
|
||||
|
||||
Alice wants to hire us to watch Bob’s commitment transaction.
|
||||
Bob wants to front-run Alice by creating a job for his “commitment transaction” with a bad encrypted blob.
|
||||
|
||||
In the above scenario, Bob can hire our service with a bad encrypted blob for the locator that should be used by Alice. Our service will try to decrypt both encrypted blobs, find the valid transaction and send it out. More generally, this potential DoS attack is possible of locators are publicly known (i.e. other watching services).
|
||||
|
||||
### Data persistence
|
||||
|
||||
The Eye of Satoshi keeps track of the appointment while they are being monitored, but data is wiped once an appointment has been completed with enough confirmations. Notice that during the alpha there will be no authentication, so data may be wiped periodically.
|
||||
|
||||
|
||||
28
cli/__init__.py
Normal file
28
cli/__init__.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import os
|
||||
import cli.conf as conf
|
||||
from common.tools import extend_paths, check_conf_fields, setup_logging, setup_data_folder
|
||||
|
||||
LOG_PREFIX = "cli"
|
||||
|
||||
# Load config fields
|
||||
conf_fields = {
|
||||
"DEFAULT_TEOS_API_SERVER": {"value": conf.DEFAULT_TEOS_API_SERVER, "type": str},
|
||||
"DEFAULT_TEOS_API_PORT": {"value": conf.DEFAULT_TEOS_API_PORT, "type": int},
|
||||
"DATA_FOLDER": {"value": conf.DATA_FOLDER, "type": str},
|
||||
"CLIENT_LOG_FILE": {"value": conf.CLIENT_LOG_FILE, "type": str, "path": True},
|
||||
"APPOINTMENTS_FOLDER_NAME": {"value": conf.APPOINTMENTS_FOLDER_NAME, "type": str, "path": True},
|
||||
"CLI_PUBLIC_KEY": {"value": conf.CLI_PUBLIC_KEY, "type": str, "path": True},
|
||||
"CLI_PRIVATE_KEY": {"value": conf.CLI_PRIVATE_KEY, "type": str, "path": True},
|
||||
"TEOS_PUBLIC_KEY": {"value": conf.TEOS_PUBLIC_KEY, "type": str, "path": True},
|
||||
}
|
||||
|
||||
# Expand user (~) if found and check fields are correct
|
||||
conf_fields["DATA_FOLDER"]["value"] = os.path.expanduser(conf_fields["DATA_FOLDER"]["value"])
|
||||
# Extend relative paths
|
||||
conf_fields = extend_paths(conf_fields["DATA_FOLDER"]["value"], conf_fields)
|
||||
|
||||
# Sanity check fields and build config dictionary
|
||||
config = check_conf_fields(conf_fields)
|
||||
|
||||
setup_data_folder(config.get("DATA_FOLDER"))
|
||||
setup_logging(config.get("CLIENT_LOG_FILE"), LOG_PREFIX)
|
||||
25
cli/help.py
Normal file
25
cli/help.py
Normal file
@@ -0,0 +1,25 @@
|
||||
def help_add_appointment():
|
||||
return (
|
||||
"NAME:"
|
||||
"\tpython teos_cli add_appointment - Registers a json formatted appointment to the tower."
|
||||
"\n\nUSAGE:"
|
||||
"\tpython teos_cli add_appointment [command options] appointment/path_to_appointment_file"
|
||||
"\n\nDESCRIPTION:"
|
||||
"\n\n\tRegisters a json formatted appointment to the tower."
|
||||
"\n\tif -f, --file *is* specified, then the command expects a path to a json file instead of a json encoded "
|
||||
"\n\tstring as parameter."
|
||||
"\n\nOPTIONS:"
|
||||
"\n\t -f, --file path_to_json_file\t loads the appointment data from the specified json file instead of"
|
||||
"\n\t\t\t\t\t command line"
|
||||
)
|
||||
|
||||
|
||||
def help_get_appointment():
|
||||
return (
|
||||
"NAME:"
|
||||
"\tpython teos_cli get_appointment - Gets json formatted data about an appointment from the tower."
|
||||
"\n\nUSAGE:"
|
||||
"\tpython teos_cli get_appointment appointment_locator"
|
||||
"\n\nDESCRIPTION:"
|
||||
"\n\n\tGets json formatted data about an appointment from the tower.\n"
|
||||
)
|
||||
3
cli/requirements-dev.txt
Normal file
3
cli/requirements-dev.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
responses
|
||||
pytest
|
||||
black
|
||||
2
cli/requirements.txt
Normal file
2
cli/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
cryptography
|
||||
requests
|
||||
14
cli/sample_conf.py
Normal file
14
cli/sample_conf.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# TEOS-SERVER
|
||||
DEFAULT_TEOS_API_SERVER = "https://teos.pisa.watch"
|
||||
DEFAULT_TEOS_API_PORT = 443
|
||||
|
||||
# WT-CLI
|
||||
DATA_FOLDER = "~/.teos_cli/"
|
||||
|
||||
CLIENT_LOG_FILE = "cli.log"
|
||||
APPOINTMENTS_FOLDER_NAME = "appointment_receipts"
|
||||
|
||||
# KEYS
|
||||
TEOS_PUBLIC_KEY = "teos_pk.der"
|
||||
CLI_PRIVATE_KEY = "cli_sk.der"
|
||||
CLI_PUBLIC_KEY = "cli_pk.der"
|
||||
460
cli/teos_cli.py
Normal file
460
cli/teos_cli.py
Normal file
@@ -0,0 +1,460 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import requests
|
||||
import time
|
||||
import binascii
|
||||
from sys import argv
|
||||
from uuid import uuid4
|
||||
from coincurve import PublicKey
|
||||
from getopt import getopt, GetoptError
|
||||
from requests import ConnectTimeout, ConnectionError
|
||||
|
||||
from cli import config, LOG_PREFIX
|
||||
from cli.help import help_add_appointment, help_get_appointment
|
||||
from common.blob import Blob
|
||||
|
||||
import common.cryptographer
|
||||
from common import constants
|
||||
from common.logger import Logger
|
||||
from common.appointment import Appointment
|
||||
from common.cryptographer import Cryptographer
|
||||
from common.tools import check_sha256_hex_format, check_locator_format, compute_locator
|
||||
|
||||
logger = Logger(actor="Client", log_name_prefix=LOG_PREFIX)
|
||||
common.cryptographer.logger = Logger(actor="Cryptographer", log_name_prefix=LOG_PREFIX)
|
||||
|
||||
|
||||
def load_keys(teos_pk_path, cli_sk_path, cli_pk_path):
|
||||
"""
|
||||
Loads all the keys required so sign, send, and verify the appointment.
|
||||
|
||||
Args:
|
||||
teos_pk_path (:obj:`str`): path to the TEOS public key file.
|
||||
cli_sk_path (:obj:`str`): path to the client private key file.
|
||||
cli_pk_path (:obj:`str`): path to the client public key file.
|
||||
|
||||
Returns:
|
||||
:obj:`tuple` or ``None``: a three item tuple containing a teos_pk object, cli_sk object and the cli_sk_der
|
||||
encoded key if all keys can be loaded. ``None`` otherwise.
|
||||
"""
|
||||
|
||||
if teos_pk_path is None:
|
||||
logger.error("TEOS's public key file not found. Please check your settings")
|
||||
return None
|
||||
|
||||
if cli_sk_path is None:
|
||||
logger.error("Client's private key file not found. Please check your settings")
|
||||
return None
|
||||
|
||||
if cli_pk_path is None:
|
||||
logger.error("Client's public key file not found. Please check your settings")
|
||||
return None
|
||||
|
||||
try:
|
||||
teos_pk_der = Cryptographer.load_key_file(teos_pk_path)
|
||||
teos_pk = PublicKey(teos_pk_der)
|
||||
|
||||
except ValueError:
|
||||
logger.error("TEOS public key is invalid or cannot be parsed")
|
||||
return None
|
||||
|
||||
cli_sk_der = Cryptographer.load_key_file(cli_sk_path)
|
||||
cli_sk = Cryptographer.load_private_key_der(cli_sk_der)
|
||||
|
||||
if cli_sk is None:
|
||||
logger.error("Client private key is invalid or cannot be parsed")
|
||||
return None
|
||||
|
||||
try:
|
||||
cli_pk_der = Cryptographer.load_key_file(cli_pk_path)
|
||||
PublicKey(cli_pk_der)
|
||||
|
||||
except ValueError:
|
||||
logger.error("Client public key is invalid or cannot be parsed")
|
||||
return None
|
||||
|
||||
return teos_pk, cli_sk, cli_pk_der
|
||||
|
||||
|
||||
def add_appointment(args):
|
||||
"""
|
||||
Manages the add_appointment command, from argument parsing, trough sending the appointment to the tower, until
|
||||
saving the appointment receipt.
|
||||
|
||||
The life cycle of the function is as follows:
|
||||
- Load the add_appointment arguments
|
||||
- Check that the given commitment_txid is correct (proper format and not missing)
|
||||
- Check that the transaction is correct (not missing)
|
||||
- Create the appointment locator and encrypted blob from the commitment_txid and the penalty_tx
|
||||
- Load the client private key and sign the appointment
|
||||
- Send the appointment to the tower
|
||||
- Wait for the response
|
||||
- Check the tower's response and signature
|
||||
- Store the receipt (appointment + signature) on disk
|
||||
|
||||
If any of the above-mentioned steps fails, the method returns false, otherwise it returns true.
|
||||
|
||||
Args:
|
||||
args (:obj:`list`): a list of arguments to pass to ``parse_add_appointment_args``. Must contain a json encoded
|
||||
appointment, or the file option and the path to a file containing a json encoded appointment.
|
||||
|
||||
Returns:
|
||||
:obj:`bool`: True if the appointment is accepted by the tower and the receipt is properly stored, false if any
|
||||
error occurs during the process.
|
||||
"""
|
||||
|
||||
teos_pk, cli_sk, cli_pk_der = load_keys(
|
||||
config.get("TEOS_PUBLIC_KEY"), config.get("CLI_PRIVATE_KEY"), config.get("CLI_PUBLIC_KEY")
|
||||
)
|
||||
|
||||
try:
|
||||
hex_pk_der = binascii.hexlify(cli_pk_der)
|
||||
|
||||
except binascii.Error as e:
|
||||
logger.error("Could not successfully encode public key as hex", error=str(e))
|
||||
return False
|
||||
|
||||
if teos_pk is None:
|
||||
return False
|
||||
|
||||
# Get appointment data from user.
|
||||
appointment_data = parse_add_appointment_args(args)
|
||||
|
||||
if appointment_data is None:
|
||||
logger.error("The provided appointment JSON is empty")
|
||||
return False
|
||||
|
||||
valid_txid = check_sha256_hex_format(appointment_data.get("tx_id"))
|
||||
|
||||
if not valid_txid:
|
||||
logger.error("The provided txid is not valid")
|
||||
return False
|
||||
|
||||
tx_id = appointment_data.get("tx_id")
|
||||
tx = appointment_data.get("tx")
|
||||
|
||||
if None not in [tx_id, tx]:
|
||||
appointment_data["locator"] = compute_locator(tx_id)
|
||||
appointment_data["encrypted_blob"] = Cryptographer.encrypt(Blob(tx), tx_id)
|
||||
|
||||
else:
|
||||
logger.error("Appointment data is missing some fields")
|
||||
return False
|
||||
|
||||
appointment = Appointment.from_dict(appointment_data)
|
||||
signature = Cryptographer.sign(appointment.serialize(), cli_sk)
|
||||
|
||||
if not (appointment and signature):
|
||||
return False
|
||||
|
||||
data = {"appointment": appointment.to_dict(), "signature": signature, "public_key": hex_pk_der.decode("utf-8")}
|
||||
|
||||
# Send appointment to the server.
|
||||
server_response = post_appointment(data)
|
||||
if server_response is None:
|
||||
return False
|
||||
|
||||
response_json = process_post_appointment_response(server_response)
|
||||
|
||||
if response_json is None:
|
||||
return False
|
||||
|
||||
signature = response_json.get("signature")
|
||||
# Check that the server signed the appointment as it should.
|
||||
if signature is None:
|
||||
logger.error("The response does not contain the signature of the appointment")
|
||||
return False
|
||||
|
||||
rpk = Cryptographer.recover_pk(appointment.serialize(), signature)
|
||||
if not Cryptographer.verify_rpk(teos_pk, rpk):
|
||||
logger.error("The returned appointment's signature is invalid")
|
||||
return False
|
||||
|
||||
logger.info("Appointment accepted and signed by the Eye of Satoshi")
|
||||
|
||||
# All good, store appointment and signature
|
||||
return save_appointment_receipt(appointment.to_dict(), signature)
|
||||
|
||||
|
||||
def parse_add_appointment_args(args):
|
||||
"""
|
||||
Parses the arguments of the add_appointment command.
|
||||
|
||||
Args:
|
||||
args (:obj:`list`): a list of arguments to pass to ``parse_add_appointment_args``. Must contain a json encoded
|
||||
appointment, or the file option and the path to a file containing a json encoded appointment.
|
||||
|
||||
Returns:
|
||||
:obj:`dict` or :obj:`None`: A dictionary containing the appointment data if it can be loaded. ``None``
|
||||
otherwise.
|
||||
"""
|
||||
|
||||
use_help = "Use 'help add_appointment' for help of how to use the command"
|
||||
|
||||
if not args:
|
||||
logger.error("No appointment data provided. " + use_help)
|
||||
return None
|
||||
|
||||
arg_opt = args.pop(0)
|
||||
|
||||
try:
|
||||
if arg_opt in ["-h", "--help"]:
|
||||
sys.exit(help_add_appointment())
|
||||
|
||||
if arg_opt in ["-f", "--file"]:
|
||||
fin = args.pop(0)
|
||||
if not os.path.isfile(fin):
|
||||
logger.error("Can't find file", filename=fin)
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(fin) as f:
|
||||
appointment_data = json.load(f)
|
||||
|
||||
except IOError as e:
|
||||
logger.error("I/O error", errno=e.errno, error=e.strerror)
|
||||
return None
|
||||
else:
|
||||
appointment_data = json.loads(arg_opt)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.error("Non-JSON encoded data provided as appointment. " + use_help)
|
||||
return None
|
||||
|
||||
return appointment_data
|
||||
|
||||
|
||||
def post_appointment(data):
|
||||
"""
|
||||
Sends appointment data to add_appointment endpoint to be processed by the tower.
|
||||
|
||||
Args:
|
||||
data (:obj:`dict`): a dictionary containing three fields: an appointment, the client-side signature, and the
|
||||
der-encoded client public key.
|
||||
|
||||
Returns:
|
||||
:obj:`dict` or ``None``: a json-encoded dictionary with the server response if the data can be posted.
|
||||
None otherwise.
|
||||
"""
|
||||
|
||||
logger.info("Sending appointment to the Eye of Satoshi")
|
||||
|
||||
try:
|
||||
add_appointment_endpoint = "{}:{}".format(teos_api_server, teos_api_port)
|
||||
return requests.post(url=add_appointment_endpoint, json=json.dumps(data), timeout=5)
|
||||
|
||||
except ConnectTimeout:
|
||||
logger.error("Can't connect to the Eye of Satoshi's API. Connection timeout")
|
||||
return None
|
||||
|
||||
except ConnectionError:
|
||||
logger.error("Can't connect to the Eye of Satoshi's API. Server cannot be reached")
|
||||
return None
|
||||
|
||||
except requests.exceptions.InvalidSchema:
|
||||
logger.error("No transport protocol found. Have you missed http(s):// in the server url?")
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error("The request timed out")
|
||||
|
||||
|
||||
def process_post_appointment_response(response):
|
||||
"""
|
||||
Processes the server response to an add_appointment request.
|
||||
|
||||
Args:
|
||||
response (:obj:`requests.models.Response`): a ``Response` object obtained from the sent request.
|
||||
|
||||
Returns:
|
||||
:obj:`dict` or :obj:`None`: a dictionary containing the tower's response data if it can be properly parsed and
|
||||
the response type is ``HTTP_OK``. ``None`` otherwise.
|
||||
"""
|
||||
|
||||
try:
|
||||
response_json = response.json()
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.error(
|
||||
"The server returned a non-JSON response", status_code=response.status_code, reason=response.reason
|
||||
)
|
||||
return None
|
||||
|
||||
if response.status_code != constants.HTTP_OK:
|
||||
if "error" not in response_json:
|
||||
logger.error(
|
||||
"The server returned an error status code but no error description", status_code=response.status_code
|
||||
)
|
||||
else:
|
||||
error = response_json["error"]
|
||||
logger.error(
|
||||
"The server returned an error status code with an error description",
|
||||
status_code=response.status_code,
|
||||
description=error,
|
||||
)
|
||||
return None
|
||||
|
||||
return response_json
|
||||
|
||||
|
||||
def save_appointment_receipt(appointment, signature):
|
||||
"""
|
||||
Saves an appointment receipt to disk. A receipt consists in an appointment and a signature from the tower.
|
||||
|
||||
Args:
|
||||
appointment (:obj:`Appointment <common.appointment.Appointment>`): the appointment to be saved on disk.
|
||||
signature (:obj:`str`): the signature of the appointment performed by the tower.
|
||||
|
||||
Returns:
|
||||
:obj:`bool`: True if the appointment if properly saved, false otherwise.
|
||||
|
||||
Raises:
|
||||
IOError: if an error occurs whilst writing the file on disk.
|
||||
"""
|
||||
|
||||
# Create the appointments directory if it doesn't already exist
|
||||
os.makedirs(config.get("APPOINTMENTS_FOLDER_NAME"), exist_ok=True)
|
||||
|
||||
timestamp = int(time.time())
|
||||
locator = appointment["locator"]
|
||||
uuid = uuid4().hex # prevent filename collisions
|
||||
|
||||
filename = "{}/appointment-{}-{}-{}.json".format(config.get("APPOINTMENTS_FOLDER_NAME"), timestamp, locator, uuid)
|
||||
data = {"appointment": appointment, "signature": signature}
|
||||
|
||||
try:
|
||||
with open(filename, "w") as f:
|
||||
json.dump(data, f)
|
||||
logger.info("Appointment saved at {}".format(filename))
|
||||
return True
|
||||
|
||||
except IOError as e:
|
||||
logger.error("There was an error while saving the appointment", error=e)
|
||||
return False
|
||||
|
||||
|
||||
def get_appointment(locator):
|
||||
"""
|
||||
Gets information about an appointment from the tower.
|
||||
|
||||
Args:
|
||||
locator (:obj:`str`): the appointment locator used to identify it.
|
||||
|
||||
Returns:
|
||||
:obj:`dict` or :obj:`None`: a dictionary containing thew appointment data if the locator is valid and the tower
|
||||
responds. ``None`` otherwise.
|
||||
"""
|
||||
|
||||
valid_locator = check_locator_format(locator)
|
||||
|
||||
if not valid_locator:
|
||||
logger.error("The provided locator is not valid", locator=locator)
|
||||
return None
|
||||
|
||||
get_appointment_endpoint = "{}:{}/get_appointment".format(teos_api_server, teos_api_port)
|
||||
parameters = "?locator={}".format(locator)
|
||||
|
||||
try:
|
||||
r = requests.get(url=get_appointment_endpoint + parameters, timeout=5)
|
||||
return r.json()
|
||||
|
||||
except ConnectTimeout:
|
||||
logger.error("Can't connect to the Eye of Satoshi's API. Connection timeout")
|
||||
return None
|
||||
|
||||
except ConnectionError:
|
||||
logger.error("Can't connect to the Eye of Satoshi's API. Server cannot be reached")
|
||||
return None
|
||||
|
||||
except requests.exceptions.InvalidSchema:
|
||||
logger.error("No transport protocol found. Have you missed http(s):// in the server url?")
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error("The request timed out")
|
||||
|
||||
|
||||
def show_usage():
|
||||
return (
|
||||
"USAGE: "
|
||||
"\n\tpython teos_cli.py [global options] command [command options] [arguments]"
|
||||
"\n\nCOMMANDS:"
|
||||
"\n\tadd_appointment \tRegisters a json formatted appointment with the tower."
|
||||
"\n\tget_appointment \tGets json formatted data about an appointment from the tower."
|
||||
"\n\thelp \t\t\tShows a list of commands or help for a specific command."
|
||||
"\n\nGLOBAL OPTIONS:"
|
||||
"\n\t-s, --server \tAPI server where to send the requests. Defaults to https://teos.pisa.watch (modifiable in "
|
||||
"config.py)"
|
||||
"\n\t-p, --port \tAPI port where to send the requests. Defaults to 443 (modifiable in conf.py)"
|
||||
"\n\t-d, --debug \tshows debug information and stores it in teos_cli.log"
|
||||
"\n\t-h --help \tshows this message."
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
teos_api_server = config.get("DEFAULT_TEOS_API_SERVER")
|
||||
teos_api_port = config.get("DEFAULT_TEOS_API_PORT")
|
||||
commands = ["add_appointment", "get_appointment", "help"]
|
||||
|
||||
try:
|
||||
opts, args = getopt(argv[1:], "s:p:h", ["server", "port", "help"])
|
||||
|
||||
for opt, arg in opts:
|
||||
if opt in ["-s", "server"]:
|
||||
if arg:
|
||||
teos_api_server = arg
|
||||
|
||||
if opt in ["-p", "--port"]:
|
||||
if arg:
|
||||
teos_api_port = int(arg)
|
||||
|
||||
if opt in ["-h", "--help"]:
|
||||
sys.exit(show_usage())
|
||||
|
||||
if args:
|
||||
command = args.pop(0)
|
||||
|
||||
if command in commands:
|
||||
if command == "add_appointment":
|
||||
add_appointment(args)
|
||||
|
||||
elif command == "get_appointment":
|
||||
if not args:
|
||||
logger.error("No arguments were given")
|
||||
|
||||
else:
|
||||
arg_opt = args.pop(0)
|
||||
|
||||
if arg_opt in ["-h", "--help"]:
|
||||
sys.exit(help_get_appointment())
|
||||
|
||||
appointment_data = get_appointment(arg_opt)
|
||||
if appointment_data:
|
||||
print(appointment_data)
|
||||
|
||||
elif command == "help":
|
||||
if args:
|
||||
command = args.pop(0)
|
||||
|
||||
if command == "add_appointment":
|
||||
sys.exit(help_add_appointment())
|
||||
|
||||
elif command == "get_appointment":
|
||||
sys.exit(help_get_appointment())
|
||||
|
||||
else:
|
||||
logger.error("Unknown command. Use help to check the list of available commands")
|
||||
|
||||
else:
|
||||
sys.exit(show_usage())
|
||||
|
||||
else:
|
||||
logger.error("Unknown command. Use help to check the list of available commands")
|
||||
|
||||
else:
|
||||
logger.error("No command provided. Use help to check the list of available commands")
|
||||
|
||||
except GetoptError as e:
|
||||
logger.error("{}".format(e))
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error("Non-JSON encoded appointment passed as parameter")
|
||||
Reference in New Issue
Block a user