-
Notifications
You must be signed in to change notification settings - Fork 14
Allow client to connect to Tor onion addresses #267
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| cryptography>=2.8 | ||
| requests | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm pretty sure |
||
| requests[socks] | ||
| structlog | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. docstrings missing for |
||
| """ | ||
| Registers the user to the tower. | ||
|
|
||
|
|
@@ -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") | ||
|
|
@@ -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): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as |
||
| """ | ||
| Manages the add_appointment command. The life cycle of the function is as follows: | ||
| - Sign the appointment | ||
|
|
@@ -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") | ||
|
|
@@ -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. | ||
|
|
||
|
|
@@ -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): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as |
||
| """ | ||
| Gets information about a user's subscription status from the tower. | ||
|
|
||
|
|
@@ -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 | ||
|
|
||
|
|
@@ -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): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as |
||
| """ | ||
| Sends a post request to the tower. | ||
|
|
||
|
|
@@ -306,7 +306,12 @@ def post_request(data, endpoint): | |
| """ | ||
|
|
||
| try: | ||
| return requests.post(url=endpoint, json=data, timeout=5) | ||
| if ".onion" in endpoint: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
Therefore, if a client enables Tor on their side, it will use Tor to connect to whatever endpoint they are trying to reach.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we define the |
||
| 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" | ||
|
|
@@ -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") | ||
|
|
@@ -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)) | ||
|
|
||
|
|
@@ -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") | ||
| ) | ||
|
|
@@ -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)) | ||
|
|
||
|
|
@@ -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)) | ||
|
|
||
|
|
@@ -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)) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,3 +5,4 @@ flake8 | |
| responses | ||
| riemann-tx | ||
| grpcio-tools | ||
| stem | ||
| 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 | ||
|
|
@@ -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) | ||
|
|
@@ -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, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Here it basically applied to the |
||
|
|
||
| return result.hostname | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
|
||
|
|
@@ -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: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as for |
||
| 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})" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| pyln-client | ||
| pyln-testing | ||
| requests | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as with |
||
| requests[socks] | ||
| coincurve | ||
| cryptography>=2.8 | ||
| pyzbase32 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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}, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as for |
||
| } | ||
|
|
||
|
|
||
|
|
@@ -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. | ||
|
|
@@ -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(): | ||
|
|
@@ -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") | ||
|
|
@@ -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: | ||
|
|
||
There was a problem hiding this comment.
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