# SPDX-FileCopyrightText: 2022 The Tor Project, Inc.
#
# SPDX-License-Identifier: BSD-3-Clause
import asyncio
import functools
import logging
import time
import stem
from stem.control import EventType
from onbasca.onbasca.torcontrol import TorControl
from onbrisca import config
from onbrisca.models.bridge import Bridge
logger = logging.getLogger(__name__)
[docs]
class BridgeTorControl(TorControl):
def __init__(self, tor_config=None, controller=None):
super().__init__(tor_config, controller)
self.attach_stream_lock = asyncio.Lock()
[docs]
def launch_or_connect_tor(
self,
port=None,
socket=None,
pw=None,
tor_config=config.TOR_CONFIG,
):
controller = super().launch_or_connect_tor(
port, socket, pw, tor_config
)
self.controller = controller
return self.controller
[docs]
def try_set_bridgelines(
self, bridgelines, sleep_secs=config.SET_BRIDGE_SLEEP_SECS
):
"""
Return valid e invalid bridgelines after trying to set them to tor
via the control port.
"""
valid_bridgelines = invalid_brigelines = []
# Set bridges one by one so that if a line can not be parsed, it is
# ignored and the rest can be set.
for bridgeline in bridgelines:
try:
self.controller.set_conf("Bridge", bridgeline)
except stem.InvalidRequest as e:
logger.warning(
"Ignoring invalid bridge line %s: %s", bridgeline, e
)
invalid_brigelines.append(bridgeline)
except Exception as e:
logger.exception(
"Exception adding bridgeline %s: %s", bridgeline, e
)
invalid_brigelines.append(bridgeline)
else:
valid_bridgelines.append(bridgeline)
# Sleep because otherwise tor's socket get closed setting many
# bridges in a row.
time.sleep(sleep_secs)
return valid_bridgelines, invalid_brigelines
[docs]
def set_bridgelines(self, bridges, try_set=config.TRY_SET_BRIDGELINES):
if not bridges:
return
bridgelines = Bridge.objects.bridgelines_from_bridges(bridges)
# Obtain first the bridges already set to do not set duplicated bridges
tor_bridgelines = self.controller.get_conf("Bridge", multiple=True)
logger.debug("tor has %s bridgelines", len(tor_bridgelines))
new_bridgelines = set(bridgelines).difference(set(tor_bridgelines))
# Now validate the bridgelines either trying to set one by one with
# ``try_set_bridgelines`` or parsing them first with
# ``validate_bridgelines``.
if try_set:
valid_bridgelines, _ = self.try_set_bridgelines(new_bridgelines)
else:
from onbasca.bridgeline import validate_bridgelines
valid_bridgelines, _ = validate_bridgelines(new_bridgelines)
# Now set all the valid bridgelines
logger.debug("Setting %s bridgelines.", len(valid_bridgelines))
if valid_bridgelines:
self.controller.set_conf("Bridge", valid_bridgelines)
self.controller.set_conf("UseBridges", "1")
logger.info(
"Set bridges %s", self.controller.get_conf("Bridge", multiple=True)
)
[docs]
async def afetch_http_head(self, path, http_client, url):
logger.debug("Creating circuit with path %s.", path)
try:
circuit_id = self.controller.new_circuit(path, await_build=True)
except (
stem.InvalidRequest,
stem.CircuitExtensionFailed,
stem.ProtocolError,
stem.Timeout,
stem.SocketClosed,
) as e:
logger.debug(e)
return None, e
except Exception as e:
logger.debug(e)
return None, e
logger.debug("Created circuit %s with path %s", circuit_id, path)
async with self.attach_stream_lock:
logger.debug("Got lock to attach stream to circuit %s", circuit_id)
stream_listener = functools.partial(
self.attach_stream_to_circuit, circuit_id
)
logger.debug(
"Adding STREAM event listener for circuit %s.", circuit_id
)
self.controller.add_event_listener(
stream_listener, EventType.STREAM
)
logger.debug("Obtaining HTTP HEAD via circuit %s.", circuit_id)
try:
response = await http_client.ahead(url)
except Exception as e:
# Max retries exceeded with url
logger.debug(e)
return circuit_id, e
else:
logger.debug(
"Attached stream to circuit %s and fetched HTTP HEAD.",
circuit_id,
)
return circuit_id, response
finally:
logger.debug(
"Removing stream listener to attach circuit %s", circuit_id
)
self.controller.remove_event_listener(stream_listener)
logger.debug(
"Released lock to attach circuit %s to stream.", circuit_id
)