Skip to content

Commit 5d176b9

Browse files
author
Radu Carpa
committed
[Core] Implement SSL peers support
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.
1 parent 1751d62 commit 5d176b9

File tree

9 files changed

+483
-33
lines changed

9 files changed

+483
-33
lines changed

deluge/common.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,12 +1201,9 @@ def __lt__(self, other):
12011201
AUTH_LEVEL_DEFAULT = AUTH_LEVEL_NORMAL
12021202

12031203

1204-
def create_auth_file():
1204+
def create_auth_file(auth_file):
12051205
import stat
12061206

1207-
import deluge.configmanager
1208-
1209-
auth_file = deluge.configmanager.get_config_dir('auth')
12101207
# Check for auth file and create if necessary
12111208
if not os.path.exists(auth_file):
12121209
with open(auth_file, 'w', encoding='utf8') as _file:
@@ -1216,29 +1213,34 @@ def create_auth_file():
12161213
os.chmod(auth_file, stat.S_IREAD | stat.S_IWRITE)
12171214

12181215

1219-
def create_localclient_account(append=False):
1216+
def create_localclient_account(append=False, auth_file=None):
12201217
import random
12211218
from hashlib import sha1 as sha
12221219

12231220
import deluge.configmanager
12241221

1225-
auth_file = deluge.configmanager.get_config_dir('auth')
1222+
if not auth_file:
1223+
auth_file = deluge.configmanager.get_config_dir('auth')
1224+
12261225
if not os.path.exists(auth_file):
1227-
create_auth_file()
1226+
create_auth_file(auth_file)
12281227

1228+
username = 'localclient'
1229+
password = sha(str(random.random()).encode('utf8')).hexdigest()
12291230
with open(auth_file, 'a' if append else 'w', encoding='utf8') as _file:
12301231
_file.write(
12311232
':'.join(
12321233
[
1233-
'localclient',
1234-
sha(str(random.random()).encode('utf8')).hexdigest(),
1234+
username,
1235+
password,
12351236
str(AUTH_LEVEL_ADMIN),
12361237
]
12371238
)
12381239
+ '\n'
12391240
)
12401241
_file.flush()
12411242
os.fsync(_file.fileno())
1243+
return username, password
12421244

12431245

12441246
def get_localhost_auth():

deluge/conftest.py

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,44 @@ async def client(request, config_dir, monkeypatch, listen_port):
8585

8686

8787
@pytest_twisted.async_yield_fixture
88-
async def daemon(request, config_dir, tmp_path):
88+
async def daemon_factory():
89+
created_daemons = []
90+
91+
async def _make_daemon(listen_port, logfile=None, custom_script='', config_dir=''):
92+
for dummy in range(10):
93+
try:
94+
d, daemon = common.start_core(
95+
listen_port=listen_port,
96+
logfile=logfile,
97+
timeout=5,
98+
timeout_msg='Timeout!',
99+
custom_script=custom_script,
100+
print_stdout=True,
101+
print_stderr=True,
102+
config_directory=config_dir,
103+
)
104+
await d
105+
daemon.listen_port = listen_port
106+
created_daemons.append(daemon)
107+
return daemon
108+
except CannotListenError as ex:
109+
exception_error = ex
110+
listen_port += 1
111+
except (KeyboardInterrupt, SystemExit):
112+
raise
113+
else:
114+
break
115+
else:
116+
raise exception_error
117+
118+
yield _make_daemon
119+
120+
for d in created_daemons:
121+
await d.kill()
122+
123+
124+
@pytest_twisted.async_yield_fixture
125+
async def daemon(request, config_dir, tmp_path, daemon_factory):
89126
listen_port = DEFAULT_LISTEN_PORT
90127
logfile = tmp_path / 'daemon.log'
91128

@@ -94,29 +131,12 @@ async def daemon(request, config_dir, tmp_path):
94131
else:
95132
custom_script = ''
96133

97-
for dummy in range(10):
98-
try:
99-
d, daemon = common.start_core(
100-
listen_port=listen_port,
101-
logfile=logfile,
102-
timeout=5,
103-
timeout_msg='Timeout!',
104-
custom_script=custom_script,
105-
print_stdout=True,
106-
print_stderr=True,
107-
config_directory=config_dir,
108-
)
109-
await d
110-
except CannotListenError as ex:
111-
exception_error = ex
112-
listen_port += 1
113-
except (KeyboardInterrupt, SystemExit):
114-
raise
115-
else:
116-
break
117-
else:
118-
raise exception_error
119-
daemon.listen_port = listen_port
134+
daemon = await daemon_factory(
135+
listen_port=listen_port,
136+
logfile=logfile,
137+
custom_script=custom_script,
138+
config_dir=config_dir,
139+
)
120140
yield daemon
121141
await daemon.kill()
122142

deluge/core/core.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import shutil
1414
import tempfile
1515
from base64 import b64decode, b64encode
16+
from pathlib import Path
1617
from typing import Any, Dict, List, Optional, Tuple, Union
1718
from urllib.request import URLError, urlopen
1819

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

678+
@export
679+
def set_ssl_torrent_cert(
680+
self,
681+
torrent_id: str,
682+
certificate: str,
683+
private_key: str,
684+
dh_params: str,
685+
save_to_disk: bool = True,
686+
):
687+
"""
688+
Set the SSL certificates used to connect to SSL peers of the given torrent.
689+
"""
690+
log.debug('adding ssl certificate to %s', torrent_id)
691+
if save_to_disk:
692+
(
693+
crt_file,
694+
key_file,
695+
dh_params_file,
696+
) = self.torrentmanager.ssl_file_paths_for_torrent(torrent_id)
697+
698+
cert_dir = Path(self.config['ssl_torrents_certs'])
699+
if not cert_dir.exists():
700+
cert_dir.mkdir(exist_ok=True)
701+
702+
for file, content in (
703+
(crt_file, certificate),
704+
(key_file, private_key),
705+
(dh_params_file, dh_params),
706+
):
707+
try:
708+
with open(file, 'w') as f:
709+
f.write(content)
710+
except OSError as err:
711+
log.warning('Error writing file %f to disk: %s', file, err)
712+
return
713+
714+
if not self.torrentmanager[torrent_id].set_ssl_certificate(
715+
str(crt_file), str(key_file), str(dh_params_file)
716+
):
717+
log.warning('Error adding certificate to %s', torrent_id)
718+
else:
719+
try:
720+
if not self.torrentmanager[torrent_id].set_ssl_certificate_buffer(
721+
certificate, private_key, dh_params
722+
):
723+
log.warning('Error adding certificate to %s', torrent_id)
724+
except AttributeError:
725+
log.warning(
726+
'libtorrent version >=2.0.10 required to set ssl torrent cert without writing to disk'
727+
)
728+
677729
@export
678730
def move_storage(self, torrent_ids: List[str], dest: str):
679731
log.debug('Moving storage %s to %s', torrent_ids, dest)
@@ -821,6 +873,17 @@ def get_listen_port(self) -> int:
821873
"""Returns the active listen port"""
822874
return self.session.listen_port()
823875

876+
@export
877+
def get_ssl_listen_port(self) -> int:
878+
"""Returns the active SSL listen port"""
879+
try:
880+
return self.session.ssl_listen_port()
881+
except AttributeError:
882+
log.warning(
883+
'libtorrent version >=2.0.10 required to get active SSL listen port'
884+
)
885+
return -1
886+
824887
@export
825888
def get_proxy(self) -> Dict[str, Any]:
826889
"""Returns the proxy settings
@@ -999,6 +1062,7 @@ def create_torrent(
9991062
trackers=None,
10001063
add_to_session=False,
10011064
torrent_format=metafile.TorrentFormat.V1,
1065+
ca_cert=None,
10021066
):
10031067
if isinstance(torrent_format, str):
10041068
torrent_format = metafile.TorrentFormat(torrent_format)
@@ -1017,6 +1081,7 @@ def create_torrent(
10171081
trackers=trackers,
10181082
add_to_session=add_to_session,
10191083
torrent_format=torrent_format,
1084+
ca_cert=ca_cert,
10201085
)
10211086

10221087
def _create_torrent_thread(
@@ -1032,6 +1097,7 @@ def _create_torrent_thread(
10321097
trackers,
10331098
add_to_session,
10341099
torrent_format,
1100+
ca_cert,
10351101
):
10361102
from deluge import metafile
10371103

@@ -1045,6 +1111,7 @@ def _create_torrent_thread(
10451111
created_by=created_by,
10461112
trackers=trackers,
10471113
torrent_format=torrent_format,
1114+
ca_cert=ca_cert,
10481115
)
10491116

10501117
write_file = False

deluge/core/preferencesmanager.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@
4848
'listen_random_port': None,
4949
'listen_use_sys_port': False,
5050
'listen_reuse_port': True,
51+
'ssl_torrents': False,
52+
'ssl_listen_ports': [6892, 6896],
53+
'ssl_torrents_certs': os.path.join(
54+
deluge.configmanager.get_config_dir(), 'ssl_torrents_certs'
55+
),
5156
'outgoing_ports': [0, 0],
5257
'random_outgoing_ports': True,
5358
'copy_torrent_file': False,
@@ -224,6 +229,24 @@ def __set_listen_on(self):
224229
f'{interface}:{port}'
225230
for port in range(listen_ports[0], listen_ports[1] + 1)
226231
]
232+
233+
if self.config['ssl_torrents']:
234+
if self.config['random_port']:
235+
ssl_listen_ports = [self.config['listen_random_port'] + 1] * 2
236+
else:
237+
ssl_listen_ports = self.config['ssl_listen_ports']
238+
interfaces.extend(
239+
[
240+
f'{interface}:{port}s'
241+
for port in range(ssl_listen_ports[0], ssl_listen_ports[1] + 1)
242+
]
243+
)
244+
log.debug(
245+
'SSL listen Interface: %s, Ports: %s',
246+
interface,
247+
listen_ports,
248+
)
249+
227250
self.core.apply_session_settings(
228251
{
229252
'listen_system_port_fallback': self.config['listen_use_sys_port'],

deluge/core/torrent.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,6 +1276,56 @@ def connect_peer(self, peer_ip, peer_port):
12761276
return False
12771277
return True
12781278

1279+
def set_ssl_certificate(
1280+
self,
1281+
certificate_path: str,
1282+
private_key_path: str,
1283+
dh_params_path: str,
1284+
password: str = '',
1285+
):
1286+
"""add a peer to the torrent
1287+
1288+
Args:
1289+
certificate_path(str) : Path to the PEM-encoded x509 certificate
1290+
private_key_path(str) : Path to the PEM-encoded private key
1291+
dh_params_path(str) : Path to the PEM-encoded Diffie-Hellman parameter
1292+
password(str) : (Optional) password used to decrypt the private key
1293+
1294+
Returns:
1295+
bool: True is successful, otherwise False
1296+
"""
1297+
try:
1298+
self.handle.set_ssl_certificate(
1299+
certificate_path, private_key_path, dh_params_path, password
1300+
)
1301+
except RuntimeError as ex:
1302+
log.error('Unable to set ssl certificate from file: %s', ex)
1303+
return False
1304+
return True
1305+
1306+
def set_ssl_certificate_buffer(
1307+
self,
1308+
certificate: str,
1309+
private_key: str,
1310+
dh_params: str,
1311+
):
1312+
"""add a peer to the torrent
1313+
1314+
Args:
1315+
certificate(str) : PEM-encoded content of the x509 certificate
1316+
private_key(str) : PEM-encoded content of the private key
1317+
dh_params(str) : PEM-encoded content of the Diffie-Hellman parameters
1318+
1319+
Returns:
1320+
bool: True is successful, otherwise False
1321+
"""
1322+
try:
1323+
self.handle.set_ssl_certificate_buffer(certificate, private_key, dh_params)
1324+
except RuntimeError as ex:
1325+
log.error('Unable to set ssl certificate from buffer: %s', ex)
1326+
return False
1327+
return True
1328+
12791329
def move_storage(self, dest):
12801330
"""Move a torrent's storage location
12811331

0 commit comments

Comments
 (0)