Skip to content

Commit

Permalink
[Core] Implement SSL peers support
Browse files Browse the repository at this point in the history
This feature is interesting when multiple deluge instances are
managed by the same administrator who uses it to transfer private
data across a non-secure network.

A separate port has to be allocated for incoming SSL connections
from peers. Libtorrent already supports this. It's enough to add
the suffix 's' when configuring libtorrent's listen_interfaces.
Implement a way to activate listening on an SSL port via the
configuration.

To actually allow SSL connection between peers, one has to also
configure a x509 certificate, private_key and diffie-hellman
for each affected torrent. This is achieved by calling libtorrent's
handle->set_ssl_certificate. Add a new exported method to perform
this goal. By default, this method will persist certificates on
disk. Allowing them to be re-loaded automatically on restart.
Cleanup the certificates of a torrent when it is removed.
  • Loading branch information
rcarpa authored and cas-- committed Sep 8, 2024
1 parent c88f750 commit 98c6fd7
Show file tree
Hide file tree
Showing 9 changed files with 483 additions and 33 deletions.
20 changes: 11 additions & 9 deletions deluge/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1231,12 +1231,9 @@ def __lt__(self, other):
AUTH_LEVEL_DEFAULT = AUTH_LEVEL_NORMAL


def create_auth_file():
def create_auth_file(auth_file):
import stat

import deluge.configmanager

auth_file = deluge.configmanager.get_config_dir('auth')
# Check for auth file and create if necessary
if not os.path.exists(auth_file):
with open(auth_file, 'w', encoding='utf8') as _file:
Expand All @@ -1246,29 +1243,34 @@ def create_auth_file():
os.chmod(auth_file, stat.S_IREAD | stat.S_IWRITE)


def create_localclient_account(append=False):
def create_localclient_account(append=False, auth_file=None):
import random
from hashlib import sha1 as sha

import deluge.configmanager

auth_file = deluge.configmanager.get_config_dir('auth')
if not auth_file:
auth_file = deluge.configmanager.get_config_dir('auth')

if not os.path.exists(auth_file):
create_auth_file()
create_auth_file(auth_file)

username = 'localclient'
password = sha(str(random.random()).encode('utf8')).hexdigest()
with open(auth_file, 'a' if append else 'w', encoding='utf8') as _file:
_file.write(
':'.join(
[
'localclient',
sha(str(random.random()).encode('utf8')).hexdigest(),
username,
password,
str(AUTH_LEVEL_ADMIN),
]
)
+ '\n'
)
_file.flush()
os.fsync(_file.fileno())
return username, password


def get_localhost_auth():
Expand Down
68 changes: 44 additions & 24 deletions deluge/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,44 @@ async def client(request, config_dir, monkeypatch, listen_port):


@pytest_twisted.async_yield_fixture
async def daemon(request, config_dir, tmp_path):
async def daemon_factory():
created_daemons = []

async def _make_daemon(listen_port, logfile=None, custom_script='', config_dir=''):
for dummy in range(10):
try:
d, daemon = common.start_core(
listen_port=listen_port,
logfile=logfile,
timeout=5,
timeout_msg='Timeout!',
custom_script=custom_script,
print_stdout=True,
print_stderr=True,
config_directory=config_dir,
)
await d
daemon.listen_port = listen_port
created_daemons.append(daemon)
return daemon
except CannotListenError as ex:
exception_error = ex
listen_port += 1
except (KeyboardInterrupt, SystemExit):
raise
else:
break
else:
raise exception_error

yield _make_daemon

for d in created_daemons:
await d.kill()


@pytest_twisted.async_yield_fixture
async def daemon(request, config_dir, tmp_path, daemon_factory):
listen_port = DEFAULT_LISTEN_PORT
logfile = tmp_path / 'daemon.log'

Expand All @@ -97,29 +134,12 @@ async def daemon(request, config_dir, tmp_path):
else:
custom_script = ''

for dummy in range(10):
try:
d, daemon = common.start_core(
listen_port=listen_port,
logfile=logfile,
timeout=5,
timeout_msg='Timeout!',
custom_script=custom_script,
print_stdout=True,
print_stderr=True,
config_directory=config_dir,
)
await d
except CannotListenError as ex:
exception_error = ex
listen_port += 1
except (KeyboardInterrupt, SystemExit):
raise
else:
break
else:
raise exception_error
daemon.listen_port = listen_port
daemon = await daemon_factory(
listen_port=listen_port,
logfile=logfile,
custom_script=custom_script,
config_dir=config_dir,
)
yield daemon
try:
await daemon.kill()
Expand Down
67 changes: 67 additions & 0 deletions deluge/core/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import shutil
import tempfile
from base64 import b64decode, b64encode
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union
from urllib.request import URLError, urlopen

Expand Down Expand Up @@ -674,6 +675,57 @@ def connect_peer(self, torrent_id: str, ip: str, port: int):
if not self.torrentmanager[torrent_id].connect_peer(ip, port):
log.warning('Error adding peer %s:%s to %s', ip, port, torrent_id)

@export
def set_ssl_torrent_cert(
self,
torrent_id: str,
certificate: str,
private_key: str,
dh_params: str,
save_to_disk: bool = True,
):
"""
Set the SSL certificates used to connect to SSL peers of the given torrent.
"""
log.debug('adding ssl certificate to %s', torrent_id)
if save_to_disk:
(
crt_file,
key_file,
dh_params_file,
) = self.torrentmanager.ssl_file_paths_for_torrent(torrent_id)

cert_dir = Path(self.config['ssl_torrents_certs'])
if not cert_dir.exists():
cert_dir.mkdir(exist_ok=True)

for file, content in (
(crt_file, certificate),
(key_file, private_key),
(dh_params_file, dh_params),
):
try:
with open(file, 'w') as f:
f.write(content)
except OSError as err:
log.warning('Error writing file %f to disk: %s', file, err)
return

if not self.torrentmanager[torrent_id].set_ssl_certificate(
str(crt_file), str(key_file), str(dh_params_file)
):
log.warning('Error adding certificate to %s', torrent_id)
else:
try:
if not self.torrentmanager[torrent_id].set_ssl_certificate_buffer(
certificate, private_key, dh_params
):
log.warning('Error adding certificate to %s', torrent_id)
except AttributeError:
log.warning(
'libtorrent version >=2.0.10 required to set ssl torrent cert without writing to disk'
)

@export
def move_storage(self, torrent_ids: List[str], dest: str):
log.debug('Moving storage %s to %s', torrent_ids, dest)
Expand Down Expand Up @@ -821,6 +873,17 @@ def get_listen_port(self) -> int:
"""Returns the active listen port"""
return self.session.listen_port()

@export
def get_ssl_listen_port(self) -> int:
"""Returns the active SSL listen port"""
try:
return self.session.ssl_listen_port()
except AttributeError:
log.warning(
'libtorrent version >=2.0.10 required to get active SSL listen port'
)
return -1

@export
def get_proxy(self) -> Dict[str, Any]:
"""Returns the proxy settings
Expand Down Expand Up @@ -999,6 +1062,7 @@ def create_torrent(
trackers=None,
add_to_session=False,
torrent_format=metafile.TorrentFormat.V1,
ca_cert=None,
):
if isinstance(torrent_format, str):
torrent_format = metafile.TorrentFormat(torrent_format)
Expand All @@ -1017,6 +1081,7 @@ def create_torrent(
trackers=trackers,
add_to_session=add_to_session,
torrent_format=torrent_format,
ca_cert=ca_cert,
)

def _create_torrent_thread(
Expand All @@ -1032,6 +1097,7 @@ def _create_torrent_thread(
trackers,
add_to_session,
torrent_format,
ca_cert,
):
from deluge import metafile

Expand All @@ -1045,6 +1111,7 @@ def _create_torrent_thread(
created_by=created_by,
trackers=trackers,
torrent_format=torrent_format,
ca_cert=ca_cert,
)

write_file = False
Expand Down
23 changes: 23 additions & 0 deletions deluge/core/preferencesmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@
'listen_random_port': None,
'listen_use_sys_port': False,
'listen_reuse_port': True,
'ssl_torrents': False,
'ssl_listen_ports': [6892, 6896],
'ssl_torrents_certs': os.path.join(
deluge.configmanager.get_config_dir(), 'ssl_torrents_certs'
),
'outgoing_ports': [0, 0],
'random_outgoing_ports': True,
'copy_torrent_file': False,
Expand Down Expand Up @@ -224,6 +229,24 @@ def __set_listen_on(self):
f'{interface}:{port}'
for port in range(listen_ports[0], listen_ports[1] + 1)
]

if self.config['ssl_torrents']:
if self.config['random_port']:
ssl_listen_ports = [self.config['listen_random_port'] + 1] * 2
else:
ssl_listen_ports = self.config['ssl_listen_ports']
interfaces.extend(
[
f'{interface}:{port}s'
for port in range(ssl_listen_ports[0], ssl_listen_ports[1] + 1)
]
)
log.debug(
'SSL listen Interface: %s, Ports: %s',
interface,
listen_ports,
)

self.core.apply_session_settings(
{
'listen_system_port_fallback': self.config['listen_use_sys_port'],
Expand Down
50 changes: 50 additions & 0 deletions deluge/core/torrent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1276,6 +1276,56 @@ def connect_peer(self, peer_ip, peer_port):
return False
return True

def set_ssl_certificate(
self,
certificate_path: str,
private_key_path: str,
dh_params_path: str,
password: str = '',
):
"""add a peer to the torrent
Args:
certificate_path(str) : Path to the PEM-encoded x509 certificate
private_key_path(str) : Path to the PEM-encoded private key
dh_params_path(str) : Path to the PEM-encoded Diffie-Hellman parameter
password(str) : (Optional) password used to decrypt the private key
Returns:
bool: True is successful, otherwise False
"""
try:
self.handle.set_ssl_certificate(
certificate_path, private_key_path, dh_params_path, password
)
except RuntimeError as ex:
log.error('Unable to set ssl certificate from file: %s', ex)
return False
return True

def set_ssl_certificate_buffer(
self,
certificate: str,
private_key: str,
dh_params: str,
):
"""add a peer to the torrent
Args:
certificate(str) : PEM-encoded content of the x509 certificate
private_key(str) : PEM-encoded content of the private key
dh_params(str) : PEM-encoded content of the Diffie-Hellman parameters
Returns:
bool: True is successful, otherwise False
"""
try:
self.handle.set_ssl_certificate_buffer(certificate, private_key, dh_params)
except RuntimeError as ex:
log.error('Unable to set ssl certificate from buffer: %s', ex)
return False
return True

def move_storage(self, dest):
"""Move a torrent's storage location
Expand Down
Loading

0 comments on commit 98c6fd7

Please sign in to comment.