Skip to content
This repository was archived by the owner on Sep 26, 2022. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contrib/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@
"APPOINTMENTS_FOLDER_NAME": {"value": "appointment_receipts", "type": str, "path": True},
"USER_PRIVATE_KEY": {"value": "user_sk.der", "type": str, "path": True},
"TEOS_PUBLIC_KEY": {"value": "teos_pk.der", "type": str, "path": True},
"SOCKS_PORT": {"value": 9050, "type": int},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we use PROXY following the same naming used by bitcoind?

https://github.com/bitcoin/bitcoin/blob/master/doc/tor.md

}
1 change: 1 addition & 0 deletions contrib/client/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
cryptography>=2.8
requests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure requests[socks] builds on top of requests, so by installing the latter the former is also installed. Could you test it in a fresh virtualenv and see if after removing requests from requirements, things still work?

requests[socks]
structlog
43 changes: 28 additions & 15 deletions contrib/client/teos_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
logger = logging.getLogger()


def register(user_id, teos_id, teos_url):
def register(user_id, teos_id, teos_url, socks_port=9050):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstrings missing for socks_port. Furthermore, the default value is still there.

"""
Registers the user to the tower.

Expand All @@ -63,7 +63,7 @@ def register(user_id, teos_id, teos_url):
data = {"public_key": user_id}

logger.info("Registering in the Eye of Satoshi")
response = process_post_response(post_request(data, register_endpoint))
response = process_post_response(post_request(data, register_endpoint, socks_port))

available_slots = response.get("available_slots")
subscription_expiry = response.get("subscription_expiry")
Expand Down Expand Up @@ -115,7 +115,7 @@ def create_appointment(appointment_data):
return Appointment.from_dict(appointment_data)


def add_appointment(appointment, user_sk, teos_id, teos_url):
def add_appointment(appointment, user_sk, teos_id, teos_url, socks_port=9050):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as register

"""
Manages the add_appointment command. The life cycle of the function is as follows:
- Sign the appointment
Expand Down Expand Up @@ -144,7 +144,7 @@ def add_appointment(appointment, user_sk, teos_id, teos_url):
# Send appointment to the server.
logger.info("Sending appointment to the Eye of Satoshi")
add_appointment_endpoint = "{}/add_appointment".format(teos_url)
response = process_post_response(post_request(data, add_appointment_endpoint))
response = process_post_response(post_request(data, add_appointment_endpoint, socks_port))

tower_signature = response.get("signature")
start_block = response.get("start_block")
Expand All @@ -164,7 +164,7 @@ def add_appointment(appointment, user_sk, teos_id, teos_url):
return start_block, tower_signature


def get_appointment(locator, user_sk, teos_id, teos_url):
def get_appointment(locator, user_sk, teos_id, teos_url, socks_port=9050):
"""
Gets information about an appointment from the tower.

Expand Down Expand Up @@ -195,12 +195,12 @@ def get_appointment(locator, user_sk, teos_id, teos_url):
# Send request to the server.
get_appointment_endpoint = "{}/get_appointment".format(teos_url)
logger.info("Requesting appointment from the Eye of Satoshi")
response = process_post_response(post_request(data, get_appointment_endpoint))
response = process_post_response(post_request(data, get_appointment_endpoint, socks_port))

return response


def get_subscription_info(user_sk, teos_id, teos_url):
def get_subscription_info(user_sk, teos_id, teos_url, socks_port=9050):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as register

"""
Gets information about a user's subscription status from the tower.

Expand All @@ -225,7 +225,7 @@ def get_subscription_info(user_sk, teos_id, teos_url):
# Send request to the server.
get_subscription_info_endpoint = "{}/get_subscription_info".format(teos_url)
logger.info("Requesting subscription information from the Eye of Satoshi")
response = process_post_response(post_request(data, get_subscription_info_endpoint))
response = process_post_response(post_request(data, get_subscription_info_endpoint, socks_port))

return response

Expand Down Expand Up @@ -290,7 +290,7 @@ def load_teos_id(teos_pk_path):
return teos_id


def post_request(data, endpoint):
def post_request(data, endpoint, socks_port=9050):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as register

"""
Sends a post request to the tower.

Expand All @@ -306,7 +306,12 @@ def post_request(data, endpoint):
"""

try:
return requests.post(url=endpoint, json=data, timeout=5)
if ".onion" in endpoint:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering whether this is the best approach for using Tor with the client.

With the current approach, if the tower is accessible from a regular address (non-onion) but the user still wants to use Tor, the client will not allow it.

I think it may be best to think about Tor for both sides separately:

  • The client may want to use Tor to connect to both .onion and regular addresses.
  • The tower may offer both .onion and regular endpoints.

Therefore, if a client enables Tor on their side, it will use Tor to connect to whatever endpoint they are trying to reach.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we define the PROXY config option (defaulting to None) then you'll only need to check whether a proxy has been provided here or not, no matter the address type.

proxies = {"http": f"socks5h://127.0.0.1:{socks_port}", "https": f"socks5h://127.0.0.1:{socks_port}"}

return requests.post(url=endpoint, json=data, timeout=15, proxies=proxies)
else:
return requests.post(url=endpoint, json=data, timeout=5)

except Timeout:
message = "Cannot connect to the Eye of Satoshi's API. Connection timeout"
Expand Down Expand Up @@ -449,6 +454,8 @@ def main(command, args, command_line_conf):
if not teos_url.startswith("http"):
teos_url = "http://" + teos_url

socks_port = config.get("SOCKS_PORT")

try:
if os.path.exists(config.get("USER_PRIVATE_KEY")):
logger.debug("Client id found. Loading keys")
Expand All @@ -468,7 +475,7 @@ def main(command, args, command_line_conf):
if not is_compressed_pk(teos_id):
raise InvalidParameter("Cannot register. Tower id has invalid format")

available_slots, subscription_expiry = register(user_id, teos_id, teos_url)
available_slots, subscription_expiry = register(user_id, teos_id, teos_url, socks_port)
logger.info("Registration succeeded. Available slots: {}".format(available_slots))
logger.info("Subscription expires at block {}".format(subscription_expiry))

Expand All @@ -479,7 +486,7 @@ def main(command, args, command_line_conf):
teos_id = load_teos_id(config.get("TEOS_PUBLIC_KEY"))
appointment_data = parse_add_appointment_args(args)
appointment = create_appointment(appointment_data)
start_block, signature = add_appointment(appointment, user_sk, teos_id, teos_url)
start_block, signature = add_appointment(appointment, user_sk, teos_id, teos_url, socks_port)
save_appointment_receipt(
appointment.to_dict(), start_block, signature, config.get("APPOINTMENTS_FOLDER_NAME")
)
Expand All @@ -495,7 +502,7 @@ def main(command, args, command_line_conf):
sys.exit(help_get_appointment())

teos_id = load_teos_id(config.get("TEOS_PUBLIC_KEY"))
appointment_data = get_appointment(arg_opt, user_sk, teos_id, teos_url)
appointment_data = get_appointment(arg_opt, user_sk, teos_id, teos_url, socks_port)
if appointment_data:
logger.info(json.dumps(appointment_data, indent=4))

Expand All @@ -507,7 +514,7 @@ def main(command, args, command_line_conf):
sys.exit(help_get_subscription_info())

teos_id = load_teos_id(config.get("TEOS_PUBLIC_KEY"))
subscription_info = get_subscription_info(user_sk, teos_id, teos_url)
subscription_info = get_subscription_info(user_sk, teos_id, teos_url, socks_port)
if subscription_info:
logger.info(json.dumps(subscription_info, indent=4))

Expand All @@ -533,7 +540,13 @@ def main(command, args, command_line_conf):
else:
sys.exit(show_usage())

except (FileNotFoundError, IOError, ConnectionError, ValueError, BasicException,) as e:
except (
FileNotFoundError,
IOError,
ConnectionError,
ValueError,
BasicException,
) as e:
logger.error(str(e))
except Exception as e:
logger.error("Unknown error occurred", error=str(e))
Expand Down
18 changes: 18 additions & 0 deletions contrib/client/test/test_teos_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,24 @@ def test_post_request():
assert response


@responses.activate
def test_post_request_onion_address():
onion_url = "http://4e2vhhgmozhi2ncuxk247j5zba3r5f3x33b3vy5z5tvdddwbijc2vhqd.onion:9814"
add_appointment_onion_endpoint = "{}/add_appointment".format(onion_url)

response = {
"locator": dummy_appointment.to_dict()["locator"],
"signature": Cryptographer.sign(dummy_appointment.serialize(), dummy_teos_sk),
}

responses.add(responses.POST, add_appointment_onion_endpoint, json=response, status=200)
response = teos_client.post_request(json.dumps(dummy_appointment_data), add_appointment_onion_endpoint)

assert len(responses.calls) == 1
assert responses.calls[0].request.url == add_appointment_onion_endpoint
assert response


def test_post_request_connection_error():
with pytest.raises(ConnectionError):
teos_client.post_request(json.dumps(dummy_appointment_data), add_appointment_endpoint)
Expand Down
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ flake8
responses
riemann-tx
grpcio-tools
stem
40 changes: 38 additions & 2 deletions test/teos/e2e/conftest.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import os
import shutil
import pytest
from shutil import rmtree
from time import sleep
import multiprocessing
from grpc import RpcError
from multiprocessing import Process
from stem.control import Controller
from stem.process import launch_tor_with_config

from teos.teosd import main
from teos.cli.teos_cli import RPCClient
Expand Down Expand Up @@ -33,7 +35,7 @@ def teosd(run_bitcoind):
pass

teosd_process.join()
shutil.rmtree(".teos")
rmtree(".teos")

# FIXME: wait some time, otherwise it might fail when multiple e2e tests are ran in the same session. Not sure why.
sleep(1)
Expand Down Expand Up @@ -67,3 +69,37 @@ def build_appointment_data(commitment_tx_id, penalty_tx):
appointment_data = {"tx": penalty_tx, "tx_id": commitment_tx_id, "to_self_delay": 20}

return appointment_data


@pytest.fixture(scope="session")
def run_tor():
dirname = ".test_tor"

# Run Tor in a separate folder
os.makedirs(dirname, exist_ok=True)

curr_dir = os.getcwd()
data_dir = f"{curr_dir}/{dirname}"

tor_process = launch_tor_with_config(
config={
"SocksPort": "9060",
"ControlPort": "9061",
"DataDirectory": data_dir,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove trailing comma

}
)

yield

tor_process.kill()
rmtree(dirname)


def create_hidden_service():
with Controller.from_port(port=9061) as controller:
controller.authenticate()

hidden_service_dir = os.path.join(controller.get_conf("DataDirectory", "/tmp"), "onion_test")
result = controller.create_hidden_service(hidden_service_dir, 9814, target_port=9814)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll be nice to get the config from the config file, in a similar manner that is done for run_teosd.

Here it basically applied to the http port


return result.hostname
20 changes: 19 additions & 1 deletion test/teos/e2e/test_client_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
generate_blocks,
config,
)
from test.teos.e2e.conftest import build_appointment_data, run_teosd
from test.teos.e2e.conftest import build_appointment_data, run_teosd, create_hidden_service

teos_base_endpoint = "http://{}:{}".format(config.get("API_BIND"), config.get("API_PORT"))
teos_add_appointment_endpoint = "{}/add_appointment".format(teos_base_endpoint)
Expand Down Expand Up @@ -559,3 +559,21 @@ def test_appointment_shutdown_teos_trigger_while_offline(teosd):
# The appointment should have been moved to the Responder
appointment_info = get_appointment_info(teos_id, locator)
assert appointment_info.get("status") == AppointmentStatus.DISPUTE_RESPONDED


def test_register_request_to_onion_service(teosd, run_tor):
_, teos_id = teosd

run_tor
sleep(3)

onion_address = create_hidden_service()
onion_endpoint = f"http://{onion_address}:9814"

# See if a user can connect to the hidden service.
tmp_user_sk = PrivateKey()
tmp_user_id = Cryptographer.get_compressed_pk(tmp_user_sk.public_key)

available_slots, subscription_expiry = teos_client.register(tmp_user_id, teos_id, onion_endpoint, socks_port=9060)

assert available_slots == 100
14 changes: 11 additions & 3 deletions watchtower-plugin/net/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def send_appointment(tower_id, tower, appointment_dict, signature):
return response


def post_request(data, endpoint, tower_id):
def post_request(data, endpoint, tower_id, socks_port=9050):
"""
Sends a post request to the tower.

Expand All @@ -110,13 +110,21 @@ def post_request(data, endpoint, tower_id):
"""

try:
return requests.post(url=endpoint, json=data, timeout=5)
if ".onion" in endpoint:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as for contrib/client

proxies = {"http": f"socks5h://127.0.0.1:{socks_port}", "https": f"socks5h://127.0.0.1:{socks_port}"}

return requests.post(url=endpoint, json=data, timeout=15, proxies=proxies)
else:
return requests.post(url=endpoint, json=data, timeout=5)

except ConnectTimeout:
message = f"Cannot connect to {tower_id}. Connection timeout"

except ConnectionError:
message = f"Cannot connect to {tower_id}. Tower cannot be reached"
if ".onion" in endpoint:
message = f"Cannot connect to {tower_id}. Trying to connect to a Tor onion address. Are you running Tor?"
else:
message = f"Cannot connect to {tower_id}. Tower cannot be reached"

except (InvalidSchema, MissingSchema, InvalidURL):
message = f"Invalid URL. No schema, or invalid schema, found (url={endpoint}, tower_id={tower_id})"
Expand Down
2 changes: 2 additions & 0 deletions watchtower-plugin/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pyln-client
pyln-testing
requests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as with contrib/requirements.txt

requests[socks]
coincurve
cryptography>=2.8
pyzbase32
Expand Down
12 changes: 9 additions & 3 deletions watchtower-plugin/watchtower.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"APPOINTMENTS_FOLDER_NAME": {"value": "appointment_receipts", "type": str, "path": True},
"TOWERS_DB": {"value": "towers", "type": str, "path": True},
"PRIVATE_KEY": {"value": "sk.der", "type": str, "path": True},
"SOCKS_PORT": {"value": 9050, "type": int},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as for contrib/client

}


Expand All @@ -45,7 +46,7 @@ class WTClient:
"""
Holds all the data regarding the watchtower client.

Fires an additional tread to take care of retries.
Fires an additional thread to take care of retries.

Args:
sk (:obj:`PrivateKey): the user private key. Used to sign appointment sent to the towers.
Expand All @@ -69,6 +70,7 @@ def __init__(self, sk, user_id, config):
self.retrier = Retrier(config.get("MAX_RETRIES"), Queue())
self.config = config
self.lock = Lock()
self.socks_port = config.get("SOCKS_PORT")

# Populate the towers dict with data from the db
for tower_id, tower_info in self.db_manager.load_all_tower_records().items():
Expand Down Expand Up @@ -170,7 +172,9 @@ def register(plugin, tower_id, host=None, port=None):

plugin.log(f"Registering in the Eye of Satoshi (tower_id={tower_id})")

response = process_post_response(post_request(data, register_endpoint, tower_id))
response = process_post_response(
post_request(data, register_endpoint, tower_id, socks_port=plugin.wt_client.socks_port)
)
available_slots = response.get("available_slots")
subscription_expiry = response.get("subscription_expiry")
tower_signature = response.get("subscription_signature")
Expand Down Expand Up @@ -234,7 +238,9 @@ def get_appointment(plugin, tower_id, locator):
get_appointment_endpoint = f"{tower_netaddr}/get_appointment"
plugin.log(f"Requesting appointment from {tower_id}")

response = process_post_response(post_request(data, get_appointment_endpoint, tower_id))
response = process_post_response(
post_request(data, get_appointment_endpoint, tower_id, socks_port=plugin.wt_client.socks_port)
)
return response

except (InvalidParameter, TowerConnectionError, TowerResponseError) as e:
Expand Down