mirror of
https://github.com/aljazceru/python-teos.git
synced 2025-12-17 14:14:22 +01:00
refactors project structure
This commit is contained in:
0
apps/__init__.py
Normal file
0
apps/__init__.py
Normal file
80
apps/cli/DEPENDENCIES.md
Normal file
80
apps/cli/DEPENDENCIES.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Dependencies
|
||||
|
||||
`pisa-cli` has both system-wide and Python dependencies. This document walks you trough how to satisfy them.
|
||||
|
||||
## System-wide dependencies
|
||||
|
||||
`pisa-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 `pisa-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
|
||||
|
||||
`pisa-cli` has the following dependencies (which can be satisfied by using `pip install -r requirements.txt`):
|
||||
|
||||
- `cryptography`
|
||||
- `requests`
|
||||
20
apps/cli/INSTALL.md
Normal file
20
apps/cli/INSTALL.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Install
|
||||
|
||||
`pisa-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.
|
||||
|
||||
In order to run `pisa-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 `.bash_rc` 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>' >> ~/.bash_rc
|
||||
|
||||
Once the `PYTHONPATH` is set, you should be able to run `pisa-cli` straightaway. Try it by running:
|
||||
|
||||
cd <absolute_path_to_apps>/apps/cli
|
||||
python pisa-cli.py -h
|
||||
97
apps/cli/PISA-API.md
Normal file
97
apps/cli/PISA-API.md
Normal file
@@ -0,0 +1,97 @@
|
||||
## PISA-API
|
||||
|
||||
### Disclaimer: Everything in here is experimental and subject to change.
|
||||
|
||||
The PISA REST API consists, currently, of two endpoints: `/` and `/check_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,
|
||||
"dispute_delta": d, "encrypted_blob": eb, "cipher":
|
||||
c, "hash_function": h}
|
||||
|
||||
We'll discuss the parameters one by one in the following:
|
||||
|
||||
The locator, `l`, is the `sha256` hex representation of the **dispute transaction id** (i.e. the sha256 of the byte representation of the dispute transaction id encoded in hex). `type(l) = hex encoded str`
|
||||
|
||||
The start\_time, `s`, is the time when the PISA server 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** and not too close to it. `type(s) = int`
|
||||
|
||||
The end\_time, `e`, is the time where the PISA server will stop watching your transaction, and will normally match which whenever you should be back online. `e` is also measured in block height, and must be **higher than** `s`. `type(e) = int`
|
||||
|
||||
The dispute\_delta, `d`, is the time PISA would have to respond with the **justice 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 `, PISA would try to respond with the justice 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 justice transaction` and it is encrypted using `AES-GCM-128`. The `encryption key` and `nonce` used by the cipher are **derived from the justice transaction id** as follows:
|
||||
|
||||
master_key = SHA256(tx_id|tx_id)
|
||||
sk = master_key[:16]
|
||||
nonce = master_key[16:]
|
||||
|
||||
where `| `represents concatenation, `[:16]` represent the first half (16 bytes), and `[16:]` represents the second half of the master key. Finally, the encrypted blob must be hex encoded. `type(eb) = hex encoded str`
|
||||
|
||||
The cipher, `c`, represents the cipher used to encrypt `eb`. The only cipher supported, for now, is `AES-GCM-128`. `type(c) = str`
|
||||
|
||||
The hash\_function, `h`, represents the hash function used to derive the encryption key and the nonce used to create `eb`. The only hash function supported, for now, is `SHA256`. `type(h) = str`
|
||||
|
||||
The API will return a `text/plain` 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.
|
||||
|
||||
#### Appointment example
|
||||
|
||||
{"locator": "3c3375883f01027e5ca14f9760a8b853824ca4ebc0258c00e7fae4bae2571a80",
|
||||
"start_time": 1568118,
|
||||
"end_time": 1568120,
|
||||
"dispute_delta": 20,
|
||||
"encrypted_blob": "6c7687a97e874363e1c2b9a08386125e09ea000a9b4330feb33a5c698265f3565c267554e6fdd7b0544ced026aaab73c255bcc97c18eb9fa704d9cc5f1c83adaf921de7ba62b2b6ddb1bda7775288019ec3708642e738eddc22882abf5b3f4e34ef2d4077ed23e135f7fe22caaec845982918e7df4a3f949cadd2d3e7c541b1dbf77daf64e7ed61531aaa487b468581b5aa7b1da81e2617e351c9d5cf445e3391c3fea4497aaa7ad286552759791b9caa5e4c055d1b38adfceddb1ef2b99e3b467dd0b0b13ce863c1bf6b6f24543c30d",
|
||||
"cipher": "AES-GCM-128",
|
||||
"hash_function": "SHA256"}
|
||||
|
||||
# Check appointment
|
||||
|
||||
`/check_appointment` is a testing endpoint provided to check the status of the appointments sent to PISA. The endpoint is accessible without any type of authentication for now. `/check_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:
|
||||
|
||||
`http://pisa_server:pisa_port/check_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 (the PISA server does not have any kind of data persistency for now).
|
||||
- `being_watched`: the appointment has been accepted by the PISA server and it's being watched at the moment. This stage means that the dispute transaction has now been seen yet, and therefore no justice transaction has been published.
|
||||
- `dispute_responded`: the dispute was found by the watcher and the corresponding justice transaction has been broadcast by the node. In this stage PISA is actively monitoring until the justice transaction reaches enough confirmations and making sure no fork occurs in the meantime.
|
||||
|
||||
### Check appointment response formats
|
||||
|
||||
`/check_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
|
||||
[{"cipher": "AES-GCM-128",
|
||||
"dispute_delta": d,
|
||||
"encrypted_blob": eb,
|
||||
"end_time": e,
|
||||
"hash_function": "SHA256",
|
||||
"locator": appointment_locator,
|
||||
"start_time": s,
|
||||
"status": "being_watched"}]
|
||||
|
||||
#### dispute_responded
|
||||
|
||||
[{"locator": appointment_locator,
|
||||
"justice_rawtx": j,
|
||||
"appointment_end": e,
|
||||
"status": "dispute_responded"
|
||||
"confirmations": c}]
|
||||
|
||||
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
|
||||
|
||||
As mentioned earlier, our service has no data persistence. this means that fulfilled appointments cannot be queried from `/check_appointment`. On top of that, if our service is restarted, all jobs are lost. This is only temporary and we are currently working on it. Do not use this service for production-ready software yet and please consider it as an early-stage demo to better understand how our API will work.
|
||||
|
||||
|
||||
144
apps/cli/README.md
Normal file
144
apps/cli/README.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# pisa-cli
|
||||
|
||||
`pisa-cli` is a command line interface to interact with the PISA server, written in Python3.
|
||||
|
||||
## Dependencies
|
||||
Refer to [DEPENDENCIES.md](DEPENDENCIES.md)
|
||||
|
||||
## Installation
|
||||
|
||||
Refer to [INSTALL.md](INSTALL.md)
|
||||
|
||||
## Usage
|
||||
|
||||
python pisa-cli.py [global options] command [command options] [arguments]
|
||||
|
||||
#### Global options
|
||||
|
||||
- `-s, --server`: API server where to send the requests. Defaults to btc.pisa.watch (modifiable in \_\_init\_\_.py)
|
||||
- `-p, --port` : API port where to send the requests. Defaults to 9814 (modifiable in \_\_init\_\_.py)
|
||||
- `-d, --debug`: shows debug information and stores it in pisa.log
|
||||
- `-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 PISA server.
|
||||
- `get_appointment`: gets json formatted data about an appointment from the PISA server.
|
||||
- `help`: shows a list of commands or help for a specific command.
|
||||
|
||||
### add_appointment
|
||||
|
||||
This command is used to register appointments to the PISA server. Appointments **must** be `json` encoded, and match the following format:
|
||||
|
||||
{ "tx": tx,
|
||||
"tx_id": tx_id,
|
||||
"start_time": s,
|
||||
"end_time": e,
|
||||
"dispute_delta": d
|
||||
}
|
||||
|
||||
`tx` **must** be the raw justice transaction that will be encrypted before sent to the PISA server. `type(tx) = hex encoded str`
|
||||
|
||||
`tx_id` **must** match the **commitment transaction id**, and will be used to encrypt the **justice transaction** and **generate the locator**. `type(tx_id) = hex encoded str`
|
||||
|
||||
`s` is the time when the PISA server 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** and not too close to it. `type(s) = int`
|
||||
|
||||
`e` is the time where the PISA server will stop watching your transaction, and will normally match which 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 PISA would have to respond with the **justice 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 `, PISA would try to respond with the justice 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 `text/plain` 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.
|
||||
|
||||
|
||||
#### Usage
|
||||
|
||||
python pisa-cli 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 PISA server.
|
||||
|
||||
**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 (the PISA server does not have any kind of data persistency for now).
|
||||
- `being_watched`: the appointment has been accepted by the PISA server and it's being watched at the moment. This stage means that the dispute transaction has now been seen yet, and therefore no justice transaction has been published.
|
||||
- `dispute_responded`: the dispute was found by the watcher and the corresponding justice transaction has been broadcast by the node. In this stage PISA is actively monitoring until the justice 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**
|
||||
|
||||
[{"cipher": "AES-GCM-128",
|
||||
"dispute_delta": d,
|
||||
"encrypted_blob": eb,
|
||||
"end_time": e,
|
||||
"hash_function": "SHA256",
|
||||
"locator": appointment_locator,
|
||||
"start_time": s,
|
||||
"status": "being_watched"}]
|
||||
|
||||
**dispute_responded**
|
||||
|
||||
[{"locator": appointment_locator,
|
||||
"justice_rawtx": j,
|
||||
"appointment_end": e,
|
||||
"status": "dispute_responded"
|
||||
"confirmations": c}]
|
||||
|
||||
#### Usage
|
||||
|
||||
python pisa-cli get_appointment <appointment_locator>
|
||||
|
||||
|
||||
|
||||
### help
|
||||
|
||||
Shows the list of commands or help about how to run a specific command.
|
||||
|
||||
#### Usage
|
||||
python pisa-cli help
|
||||
|
||||
or
|
||||
|
||||
python pisa-cli 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 PISA API.
|
||||
|
||||
```
|
||||
python pisa-cli.py generate_dummy_appointment
|
||||
```
|
||||
|
||||
That will create a json file that follows the appointment data structure filled with dummy data and store it in `dummy_appointment_data.json`.
|
||||
|
||||
2. Send the appointment to the PISA API. Which will then start monitoring for matching transactions.
|
||||
|
||||
```
|
||||
python pisa-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 PISA.
|
||||
|
||||
3. Test that PISA is still watching the appointment by replacing the appointment locator received into the following command:
|
||||
|
||||
```
|
||||
python pisa-cli.py get_appointment <appointment_locator>
|
||||
```
|
||||
|
||||
## PISA API
|
||||
|
||||
If you wish to read about the underlying API, and how to write your own tool to interact with it, refer to [PISA-API.md](PISA-API.md)
|
||||
10
apps/cli/__init__.py
Normal file
10
apps/cli/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# PISA-SERVER
|
||||
DEFAULT_PISA_API_SERVER = 'btc.pisa.watch'
|
||||
DEFAULT_PISA_API_PORT = 9814
|
||||
|
||||
# PISA-CLI
|
||||
CLIENT_LOG_FILE = 'pisa.log'
|
||||
|
||||
# CRYPTO
|
||||
SUPPORTED_HASH_FUNCTIONS = ["SHA256"]
|
||||
SUPPORTED_CIPHERS = ["AES-GCM-128"]
|
||||
51
apps/cli/blob.py
Normal file
51
apps/cli/blob.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from binascii import hexlify, unhexlify
|
||||
from hashlib import sha256
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
from apps.cli import SUPPORTED_HASH_FUNCTIONS, SUPPORTED_CIPHERS
|
||||
|
||||
|
||||
class Blob:
|
||||
def __init__(self, data, cipher, hash_function):
|
||||
self.data = data
|
||||
self.cipher = cipher
|
||||
self.hash_function = hash_function
|
||||
|
||||
# FIXME: We only support SHA256 for now
|
||||
if self.hash_function.upper() not in SUPPORTED_HASH_FUNCTIONS:
|
||||
raise Exception("Hash function not supported ({}). Supported Hash functions: {}"
|
||||
.format(self.hash_function, SUPPORTED_HASH_FUNCTIONS))
|
||||
|
||||
# FIXME: We only support AES-GCM-128 for now
|
||||
if self.cipher.upper() not in SUPPORTED_CIPHERS:
|
||||
raise Exception("Cipher not supported ({}). Supported ciphers: {}".format(self.hash_function,
|
||||
SUPPORTED_CIPHERS))
|
||||
|
||||
def encrypt(self, tx_id, debug, logging):
|
||||
# Transaction to be encrypted
|
||||
# FIXME: The blob data should contain more things that just the transaction. Leaving like this for now.
|
||||
tx = unhexlify(self.data)
|
||||
|
||||
# FIXME: tx_id should not be necessary (can be derived from tx SegWit-like). Passing it for now
|
||||
# Extend the key using HKDF
|
||||
tx_id = unhexlify(tx_id)
|
||||
|
||||
# master_key = H(tx_id | tx_id)
|
||||
master_key = sha256(tx_id + tx_id).digest()
|
||||
|
||||
# The 16 MSB of the master key will serve as the AES GCM 128 secret key. The 16 LSB will serve as the IV.
|
||||
sk = master_key[:16]
|
||||
nonce = master_key[16:]
|
||||
|
||||
# Encrypt the data
|
||||
aesgcm = AESGCM(sk)
|
||||
encrypted_blob = aesgcm.encrypt(nonce=nonce, data=tx, associated_data=None)
|
||||
encrypted_blob = hexlify(encrypted_blob).decode()
|
||||
|
||||
if debug:
|
||||
logging.info("[Client] creating new blob")
|
||||
logging.info("[Client] master key: {}".format(hexlify(master_key).decode()))
|
||||
logging.info("[Client] sk: {}".format(hexlify(sk).decode()))
|
||||
logging.info("[Client] nonce: {}".format(hexlify(nonce).decode()))
|
||||
logging.info("[Client] encrypted_blob: {}".format(encrypted_blob))
|
||||
|
||||
return encrypted_blob
|
||||
21
apps/cli/help.py
Normal file
21
apps/cli/help.py
Normal file
@@ -0,0 +1,21 @@
|
||||
def help_add_appointment():
|
||||
return "NAME:" \
|
||||
"\tpython pisa-cli add_appointment - Registers a json formatted appointment to the PISA server." \
|
||||
"\n\nUSAGE:" \
|
||||
"\tpython pisa-cli add_appointment [command options] appointment/path_to_appointment_file" \
|
||||
"\n\nDESCRIPTION:" \
|
||||
"\n\n\tRegisters a json formatted appointment to the PISA server." \
|
||||
"\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 pisa-cli get_appointment - Gets json formatted data about an appointment from the PISA server." \
|
||||
"\n\nUSAGE:" \
|
||||
"\tpython pisa-cli get_appointment appointment_locator" \
|
||||
"\n\nDESCRIPTION:" \
|
||||
"\n\n\tGets json formatted data about an appointment from the PISA server.\n"
|
||||
239
apps/cli/pisa-cli.py
Normal file
239
apps/cli/pisa-cli.py
Normal file
@@ -0,0 +1,239 @@
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
from sys import argv
|
||||
from getopt import getopt, GetoptError
|
||||
from hashlib import sha256
|
||||
from binascii import hexlify, unhexlify
|
||||
from requests import ConnectTimeout, ConnectionError
|
||||
from apps.cli import DEFAULT_PISA_API_SERVER, DEFAULT_PISA_API_PORT, CLIENT_LOG_FILE
|
||||
from apps.cli.blob import Blob
|
||||
from apps.cli.help import help_add_appointment, help_get_appointment
|
||||
|
||||
|
||||
def show_message(message, debug, logging):
|
||||
if debug:
|
||||
logging.error('[Client] ' + message[0].lower() + message[1:])
|
||||
else:
|
||||
sys.exit(message)
|
||||
|
||||
|
||||
# FIXME: TESTING ENDPOINT, WON'T BE THERE IN PRODUCTION
|
||||
def generate_dummy_appointment():
|
||||
get_block_count_end_point = "http://{}:{}/get_block_count".format(pisa_api_server, pisa_api_port)
|
||||
r = requests.get(url=get_block_count_end_point, timeout=5)
|
||||
|
||||
current_height = r.json().get("block_count")
|
||||
|
||||
dummy_appointment_data = {"tx": hexlify(os.urandom(192)).decode('utf-8'),
|
||||
"tx_id": hexlify(os.urandom(32)).decode('utf-8'), "start_time": current_height + 5,
|
||||
"end_time": current_height + 10, "dispute_delta": 20}
|
||||
|
||||
print('Generating dummy appointment data:''\n\n' + json.dumps(dummy_appointment_data, indent=4, sort_keys=True))
|
||||
|
||||
json.dump(dummy_appointment_data, open('dummy_appointment_data.json', 'w'))
|
||||
|
||||
print('\nData stored in dummy_appointment_data.json')
|
||||
|
||||
|
||||
def add_appointment(args, debug, logging):
|
||||
appointment_data = None
|
||||
use_help = "Use 'help add_appointment' for help of how to use the command."
|
||||
|
||||
if args:
|
||||
arg_opt = args.pop(0)
|
||||
|
||||
try:
|
||||
if arg_opt in ['-h', '--help']:
|
||||
sys.exit(help_add_appointment())
|
||||
|
||||
if arg_opt in ['-f', '--file']:
|
||||
if args:
|
||||
fin = args.pop(0)
|
||||
if os.path.isfile(fin):
|
||||
appointment_data = json.load(open(fin))
|
||||
else:
|
||||
show_message("Can't find file " + fin, debug, logging)
|
||||
else:
|
||||
show_message("No file provided as appointment. " + use_help, debug, logging)
|
||||
else:
|
||||
appointment_data = json.loads(arg_opt)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
show_message("Non-JSON encoded data provided as appointment. " + use_help, debug, logging)
|
||||
|
||||
if appointment_data:
|
||||
valid_locator = check_txid_format(appointment_data.get('tx_id'))
|
||||
|
||||
if valid_locator:
|
||||
add_appointment_endpoint = "http://{}:{}".format(pisa_api_server, pisa_api_port)
|
||||
appointment = build_appointment(appointment_data.get('tx'), appointment_data.get('tx_id'),
|
||||
appointment_data.get('start_time'), appointment_data.get('end_time'),
|
||||
appointment_data.get('dispute_delta'), debug, logging)
|
||||
|
||||
if debug:
|
||||
logging.info("[Client] sending appointment to PISA")
|
||||
|
||||
try:
|
||||
r = requests.post(url=add_appointment_endpoint, json=json.dumps(appointment), timeout=5)
|
||||
|
||||
show_message("{} (code: {}).".format(r.text, r.status_code), debug, logging)
|
||||
|
||||
except ConnectTimeout:
|
||||
show_message("Can't connect to pisa API. Connection timeout.", debug, logging)
|
||||
|
||||
except ConnectionError:
|
||||
show_message("Can't connect to pisa API. Server cannot be reached.", debug, logging)
|
||||
else:
|
||||
show_message("The provided locator is not valid.", debug, logging)
|
||||
else:
|
||||
show_message("No appointment data provided. " + use_help, debug, logging)
|
||||
|
||||
|
||||
def get_appointment(args, debug, logging):
|
||||
if args:
|
||||
arg_opt = args.pop(0)
|
||||
|
||||
if arg_opt in ['-h', '--help']:
|
||||
sys.exit(help_get_appointment())
|
||||
else:
|
||||
locator = arg_opt
|
||||
valid_locator = check_txid_format(locator)
|
||||
|
||||
if valid_locator:
|
||||
get_appointment_endpoint = "http://{}:{}/get_appointment".format(pisa_api_server, pisa_api_port)
|
||||
parameters = "?locator={}".format(locator)
|
||||
try:
|
||||
r = requests.get(url=get_appointment_endpoint + parameters, timeout=5)
|
||||
|
||||
print(json.dumps(r.json(), indent=4, sort_keys=True))
|
||||
|
||||
except ConnectTimeout:
|
||||
show_message("Can't connect to pisa API. Connection timeout.", debug, logging)
|
||||
|
||||
except ConnectionError:
|
||||
show_message("Can't connect to pisa API. Server cannot be reached.", debug, logging)
|
||||
|
||||
else:
|
||||
show_message("The provided locator is not valid.", debug, logging)
|
||||
else:
|
||||
show_message("The provided locator is not valid.", debug, logging)
|
||||
|
||||
|
||||
def build_appointment(tx, tx_id, start_block, end_block, dispute_delta, debug, logging):
|
||||
locator = sha256(unhexlify(tx_id)).hexdigest()
|
||||
|
||||
cipher = "AES-GCM-128"
|
||||
hash_function = "SHA256"
|
||||
|
||||
# FIXME: The blob data should contain more things that just the transaction. Leaving like this for now.
|
||||
blob = Blob(tx, cipher, hash_function)
|
||||
|
||||
# FIXME: tx_id should not be necessary (can be derived from tx SegWit-like). Passing it for now
|
||||
encrypted_blob = blob.encrypt(tx_id, debug, logging)
|
||||
|
||||
appointment = {"locator": locator, "start_time": start_block, "end_time": end_block,
|
||||
"dispute_delta": dispute_delta, "encrypted_blob": encrypted_blob, "cipher": cipher, "hash_function":
|
||||
hash_function}
|
||||
|
||||
return appointment
|
||||
|
||||
|
||||
def check_txid_format(txid):
|
||||
if len(txid) != 64:
|
||||
sys.exit("locator does not matches the expected size (32-byte / 64 hex chars).")
|
||||
|
||||
# TODO: Check this regexp
|
||||
return re.search(r'^[0-9A-Fa-f]+$', txid) is not None
|
||||
|
||||
|
||||
def show_usage():
|
||||
return ('USAGE: '
|
||||
'\n\tpython pisa-cli.py [global options] command [command options] [arguments]'
|
||||
'\n\nCOMMANDS:'
|
||||
'\n\tadd_appointment \tRegisters a json formatted appointment to the PISA server.'
|
||||
'\n\tget_appointment \tGets json formatted data about an appointment from the PISA server.'
|
||||
'\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 btc.pisa.watch (modifiable in '
|
||||
'__init__.py)'
|
||||
'\n\t-p, --port \tAPI port where to send the requests. Defaults to 9814 (modifiable in __init__.py)'
|
||||
'\n\t-d, --debug \tshows debug information and stores it in pisa.log'
|
||||
'\n\t-h --help \tshows this message.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
debug = False
|
||||
pisa_api_server = DEFAULT_PISA_API_SERVER
|
||||
pisa_api_port = DEFAULT_PISA_API_PORT
|
||||
commands = ['add_appointment', 'get_appointment', 'help']
|
||||
testing_commands = ['generate_dummy_appointment']
|
||||
|
||||
try:
|
||||
opts, args = getopt(argv[1:], 's:p:dh', ['server', 'port', 'debug', 'help'])
|
||||
|
||||
for opt, arg in opts:
|
||||
if opt in ['-s', 'server']:
|
||||
if arg:
|
||||
pisa_api_server = arg
|
||||
|
||||
if opt in ['-p', '--port']:
|
||||
if arg:
|
||||
pisa_api_port = int(arg)
|
||||
|
||||
if opt in ['-d', '--debug']:
|
||||
debug = True
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO, handlers=[
|
||||
logging.FileHandler(CLIENT_LOG_FILE),
|
||||
logging.StreamHandler()
|
||||
])
|
||||
|
||||
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, debug, logging)
|
||||
|
||||
elif command == 'get_appointment':
|
||||
get_appointment(args, debug, logging)
|
||||
|
||||
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:
|
||||
show_message("Unknown command. Use help to check the list of available commands.", debug,
|
||||
logging)
|
||||
else:
|
||||
sys.exit(show_usage())
|
||||
|
||||
# FIXME: testing command, not for production
|
||||
elif command in testing_commands:
|
||||
if command == 'generate_dummy_appointment':
|
||||
generate_dummy_appointment()
|
||||
|
||||
else:
|
||||
show_message("Unknown command. Use help to check the list of available commands.", debug, logging)
|
||||
else:
|
||||
show_message("No command provided. Use help to check the list of available commands.", debug, logging)
|
||||
|
||||
except GetoptError as e:
|
||||
show_message(e, debug, logging)
|
||||
except json.JSONDecodeError as e:
|
||||
show_message('Non-JSON encoded appointment passed as parameter.', debug, logging)
|
||||
2
apps/cli/requirements.txt
Normal file
2
apps/cli/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
cryptography
|
||||
requests
|
||||
Reference in New Issue
Block a user