Skip to content
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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,17 @@ from torpy import TorClient
hostname = 'ifconfig.me' # It's possible use onion hostname here as well
with TorClient() as tor:
# Choose random guard node and create 3-hops circuit
with tor.create_circuit(3) as circuit:
with tor.create_circuit(3, exit_country='FR') as circuit:
# Create tor stream to host
with circuit.create_stream((hostname, 80)) as stream:
# Now we can communicate with host
stream.send(b'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % hostname.encode())
recv = stream.recv(1024)
```

The `exit_country` behaviour isn't available out-of-the-box, but can be enabled via `pip install torpy[exit_country]`.


TorHttpAdapter is a convenient Tor adapter for the [requests library](https://2.python-requests.org/en/master/user/advanced/#transport-adapters).
The following example shows the usage of TorHttpAdapter for multi-threaded HTTP requests:
```python
Expand Down
5 changes: 4 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@
packages=find_packages(exclude=['tests']),
python_requires='>=3.6',
install_requires=['cryptography>=3.2'],
extras_require={'requests': 'requests>2.9,!=2.17.0,!=2.18.0'},
extras_require={
'requests': 'requests>2.9,!=2.17.0,!=2.18.0',
'exit_country': 'location_ipfire_db_reader',
},
entry_points={'console_scripts': ['torpy_cli=torpy.cli.console:main',
'torpy_socks=torpy.cli.socks:main']
},
Expand Down
2 changes: 1 addition & 1 deletion torpy/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ def build_hops(self, hops_count):
logger.info('Building %i hops circuit...', hops_count)
while self.nodes_count < hops_count:
if self.nodes_count == hops_count - 1:
router = self._guard.consensus.get_random_exit_node()
router = self._guard.consensus.get_random_exit_node(in_country=self._guard.exit_country)
else:
router = self._guard.consensus.get_random_middle_node()

Expand Down
8 changes: 4 additions & 4 deletions torpy/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ def create(cls, authorities=None, cache_class=None, cache_kwargs=None, auth_data
msg='Retry with another guard...',
no_traceback=(socket.timeout, TorSocketConnectError,))
)
def get_guard(self, by_flags=None):
def get_guard(self, by_flags=None, exit_country=None):
# TODO: add another stuff to filter guards
guard_router = self._consensus.get_random_guard_node(by_flags)
return TorGuard(guard_router, purpose='TorClient', consensus=self._consensus, auth_data=self._auth_data)
return TorGuard(guard_router, purpose='TorClient', consensus=self._consensus, auth_data=self._auth_data, exit_country=exit_country)

@contextmanager
def create_circuit(self, hops_count=3, guard_by_flags=None) -> 'ContextManager[TorCircuit]':
with self.get_guard(guard_by_flags) as guard:
def create_circuit(self, hops_count=3, exit_country=None, guard_by_flags=None) -> 'ContextManager[TorCircuit]':
with self.get_guard(guard_by_flags, exit_country=exit_country) as guard:
yield guard.create_circuit(hops_count)

def __enter__(self):
Expand Down
34 changes: 29 additions & 5 deletions torpy/consesus.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@
from torpy.documents.dir_key_certificate import DirKeyCertificateList
from torpy.documents.network_status_diff import NetworkStatusDiffDocument
from torpy.dirs import AUTHORITY_DIRS, FALLBACK_DIRS
try:
from location_ipfire_db_reader import LocationDatabase
HAVE_IPFIRE = True
locdb = None
except ImportError as ex:
HAVE_IPFIRE = False
class NoRouterFound(Exception):
...

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -305,7 +313,7 @@ def get_router(self, fingerprint) -> Router:
fingerprint_b = b32decode(fingerprint.upper())
return next(onion_router for onion_router in self.document.routers if onion_router.fingerprint == fingerprint_b)

def get_routers(self, flags=None, has_dir_port=True, with_renew=True):
def get_routers(self, flags=None, has_dir_port=True, with_renew=True, in_country=None):
"""
Select consensus routers that satisfy certain parameters.

Expand All @@ -321,11 +329,13 @@ def get_routers(self, flags=None, has_dir_port=True, with_renew=True):
continue
if has_dir_port and not onion_router.dir_port:
continue
if not self._is_router_in_country(onion_router, in_country):
continue
results.append(onion_router)

return results

def get_random_router(self, flags=None, has_dir_port=None, with_renew=True):
def get_random_router(self, flags=None, has_dir_port=None, with_renew=True, in_country=None):
"""
Select a random consensus router that satisfy certain parameters.

Expand All @@ -334,16 +344,18 @@ def get_random_router(self, flags=None, has_dir_port=None, with_renew=True):
:param with_renew: Do renew consensus if old
:return: router
"""
routers = self.get_routers(flags, has_dir_port, with_renew)
routers = self.get_routers(flags, has_dir_port, with_renew, in_country=in_country)
if not routers:
raise NoRouterFound((flags, in_country))
return random.choice(routers)

def get_random_guard_node(self, different_flags=None):
flags = different_flags or [RouterFlags.Guard]
return self.get_random_router(flags)

def get_random_exit_node(self):
def get_random_exit_node(self, in_country=None):
flags = [RouterFlags.Fast, RouterFlags.Running, RouterFlags.Valid, RouterFlags.Exit]
return self.get_random_router(flags)
return self.get_random_router(flags, in_country=in_country)

def get_random_middle_node(self):
flags = [RouterFlags.Fast, RouterFlags.Running, RouterFlags.Valid]
Expand Down Expand Up @@ -453,3 +465,15 @@ def get_responsibles(self, hidden_service):
idx = (i + 1 + j) % len(hsdir_router_list)
yield hsdir_router_list[idx]
break
def _is_router_in_country(self, onion_router, in_country):
if in_country is None or not bool(in_country):
return True

if not HAVE_IPFIRE:
return True

global locdb
if locdb is None:
locdb = LocationDatabase('location.db')

return locdb.find_country(onion_router.ip).upper() in in_country
11 changes: 10 additions & 1 deletion torpy/guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,17 @@ def send(self, cell):


class TorGuard:
def __init__(self, router, purpose=None, consensus=None, auth_data=None):
def __init__(self, router, purpose=None, consensus=None, auth_data=None, exit_country=None):
self._router = router
self._purpose = purpose
self._consensus = consensus
self._auth_data = auth_data
if isinstance(exit_country, str) and exit_country.strip():
self._exit_country = (exit_country.upper().strip(),)
elif isinstance(exit_country, (list, tuple)):
self._exit_country = tuple(ec.upper().strip() for ec in exit_country)
else:
self._exit_country = None

self._state = GuardState.Connecting
logger.info('Connecting to guard node %s... (%s)', self._router, self._purpose)
Expand Down Expand Up @@ -89,6 +95,9 @@ def router(self):
@property
def auth_data(self):
return self._auth_data
@property
def exit_country(self):
return self._exit_country

def __enter__(self):
"""Return Guard object."""
Expand Down