aboutsummaryrefslogtreecommitdiffstats
path: root/deluge/core
diff options
context:
space:
mode:
authorLibravatarDaniel Baumann <daniel@debian.org>2025-11-30 18:42:19 +0100
committerLibravatarDaniel Baumann <daniel@debian.org>2025-11-30 18:42:19 +0100
commit5d5f9bd85b94db8d64148435c30f7fb819bcd255 (patch)
treed406aeebcdc5d273c9e30eb08b59d9c498cd991a /deluge/core
parent3e1888c3d656906263b0cd08875885166efcfb63 (diff)
Merging upstream version 2.2.1~dev0+20250824.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'deluge/core')
-rw-r--r--deluge/core/authmanager.py70
-rw-r--r--deluge/core/core.py67
-rw-r--r--deluge/core/preferencesmanager.py23
-rw-r--r--deluge/core/torrent.py50
-rw-r--r--deluge/core/torrentmanager.py55
5 files changed, 244 insertions, 21 deletions
diff --git a/deluge/core/authmanager.py b/deluge/core/authmanager.py
index 3ff8a3a..291a206 100644
--- a/deluge/core/authmanager.py
+++ b/deluge/core/authmanager.py
@@ -21,7 +21,13 @@ from deluge.common import (
AUTH_LEVEL_READONLY,
create_localclient_account,
)
-from deluge.error import AuthenticationRequired, AuthManagerError, BadLoginError
+from deluge.error import (
+ AuthenticationRequired,
+ AuthManagerError,
+ BadLoginError,
+ InvalidHashError,
+)
+from deluge.security import check_password_hash, generate_password_hash
log = logging.getLogger(__name__)
@@ -86,15 +92,15 @@ class AuthManager(component.Component):
log.info('Auth file changed, reloading it!')
self.__load_auth_file()
- def authorize(self, username, password):
+ def authorize(self, username: str, password: str) -> int:
"""Authorizes users based on username and password.
Args:
- username (str): Username
- password (str): Password
+ username: Username
+ password: Password
Returns:
- int: The auth level for this user.
+ The auth level for this user.
Raises:
AuthenticationRequired: If additional details are required to authenticate.
@@ -112,14 +118,33 @@ class AuthManager(component.Component):
if username not in self.__auth:
raise BadLoginError('Username does not exist', username)
- if self.__auth[username].password == password:
- # Return the users auth level
- return self.__auth[username].authlevel
- elif not password and self.__auth[username].password:
+ stored_password = self.__auth[username].password
+ if not password and stored_password:
raise AuthenticationRequired('Password is required', username)
- else:
+
+ if not self._verify_password(username, password, stored_password):
raise BadLoginError('Password does not match', username)
+ return self.__auth[username].authlevel
+
+ @staticmethod
+ def _verify_password(username: str, password: str, stored_password: str) -> bool:
+ """Verifies a user's password either as hash or plaintext."""
+ if username == 'localclient':
+ # Plaintext validation to maintain localclient autologin compatibility
+ return stored_password == password
+
+ try:
+ return check_password_hash(stored_password, password)
+ except InvalidHashError as ex:
+ log.warning(
+ 'Invalid hash method in password for user %s: %s'
+ ' Falling back to plaintext validation.',
+ username,
+ ex.method,
+ )
+ return stored_password == password
+
def has_account(self, username):
return username in self.__auth
@@ -129,13 +154,15 @@ class AuthManager(component.Component):
return [account.data() for account in self.__auth.values()]
def create_account(self, username, password, authlevel):
+ password_hash = generate_password_hash(password)
+
if username in self.__auth:
raise AuthManagerError('Username in use.', username)
if authlevel not in AUTH_LEVELS_MAPPING:
- raise AuthManagerError('Invalid auth level: %s' % authlevel)
+ raise AuthManagerError('Invalid auth level: %s' % authlevel, username)
try:
self.__auth[username] = Account(
- username, password, AUTH_LEVELS_MAPPING[authlevel]
+ username, password_hash, AUTH_LEVELS_MAPPING[authlevel]
)
self.write_auth_file()
return True
@@ -144,13 +171,18 @@ class AuthManager(component.Component):
raise ex
def update_account(self, username, password, authlevel):
+ # If the username is 'localclient', we don't hash the password
+ # to keep compatability with the current localclient autologin.
+ password_hash = None
+ if username != 'localclient':
+ password_hash = generate_password_hash(password)
if username not in self.__auth:
raise AuthManagerError('Username not known', username)
if authlevel not in AUTH_LEVELS_MAPPING:
- raise AuthManagerError('Invalid auth level: %s' % authlevel)
+ raise AuthManagerError('Invalid auth level: %s' % authlevel, username)
try:
self.__auth[username].username = username
- self.__auth[username].password = password
+ self.__auth[username].password = password_hash or password
self.__auth[username].authlevel = AUTH_LEVELS_MAPPING[authlevel]
self.write_auth_file()
return True
@@ -213,13 +245,14 @@ class AuthManager(component.Component):
create_localclient_account()
return self.__load_auth_file()
- auth_file_modification_time = os.stat(auth_file).st_mtime
+ auth_file_modification_time = os.stat(auth_file).st_mtime_ns
if self.__auth_modification_time is None:
self.__auth_modification_time = auth_file_modification_time
elif self.__auth_modification_time == auth_file_modification_time:
- # File didn't change, no need for re-parsing's
+ log.debug('Auth file unchanged, skipping re-parsing.')
return
+ file_data = []
for _filepath in (auth_file, auth_file_bak):
log.info('Opening %s for load: %s', filename, _filepath)
try:
@@ -227,7 +260,6 @@ class AuthManager(component.Component):
file_data = _file.readlines()
except OSError as ex:
log.warning('Unable to load %s: %s', _filepath, ex)
- file_data = []
else:
log.info('Successfully loaded %s: %s', filename, _filepath)
break
@@ -265,13 +297,13 @@ class AuthManager(component.Component):
authlevel = int(authlevel)
except ValueError:
try:
- authlevel = AUTH_LEVELS_MAPPING[authlevel]
+ authlevel = AUTH_LEVELS_MAPPING[str(authlevel)]
except KeyError:
log.error(
'Your auth file is malformed: %r is not a valid auth level',
authlevel,
)
- continue
+ continue
self.__auth[username] = Account(username, password, authlevel)
diff --git a/deluge/core/core.py b/deluge/core/core.py
index e621a45..66392b6 100644
--- a/deluge/core/core.py
+++ b/deluge/core/core.py
@@ -13,6 +13,7 @@ import os
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
@@ -676,6 +677,57 @@ class Core(component.Component):
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)
for torrent_id in torrent_ids:
@@ -823,6 +875,17 @@ class Core(component.Component):
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
@@ -1000,6 +1063,7 @@ class Core(component.Component):
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)
@@ -1018,6 +1082,7 @@ class Core(component.Component):
trackers=trackers,
add_to_session=add_to_session,
torrent_format=torrent_format,
+ ca_cert=ca_cert,
)
def _create_torrent_thread(
@@ -1033,6 +1098,7 @@ class Core(component.Component):
trackers,
add_to_session,
torrent_format,
+ ca_cert,
):
from deluge import metafile
@@ -1046,6 +1112,7 @@ class Core(component.Component):
created_by=created_by,
trackers=trackers,
torrent_format=torrent_format,
+ ca_cert=ca_cert,
)
write_file = False
diff --git a/deluge/core/preferencesmanager.py b/deluge/core/preferencesmanager.py
index 4dbf4d1..0699384 100644
--- a/deluge/core/preferencesmanager.py
+++ b/deluge/core/preferencesmanager.py
@@ -48,6 +48,11 @@ DEFAULT_PREFS = {
'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,
@@ -227,6 +232,24 @@ class PreferencesManager(component.Component):
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'],
diff --git a/deluge/core/torrent.py b/deluge/core/torrent.py
index dbcf8f1..c7c7349 100644
--- a/deluge/core/torrent.py
+++ b/deluge/core/torrent.py
@@ -1275,6 +1275,56 @@ class Torrent:
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
diff --git a/deluge/core/torrentmanager.py b/deluge/core/torrentmanager.py
index 1233553..a659298 100644
--- a/deluge/core/torrentmanager.py
+++ b/deluge/core/torrentmanager.py
@@ -15,6 +15,7 @@ import os
import pickle
import time
from base64 import b64encode
+from pathlib import Path
from tempfile import gettempdir
from typing import Dict, List, NamedTuple, Tuple
@@ -210,6 +211,7 @@ class TorrentManager(component.Component):
'torrent_finished',
'torrent_paused',
'torrent_checked',
+ 'torrent_need_cert',
'torrent_resumed',
'tracker_reply',
'tracker_announce',
@@ -338,8 +340,8 @@ class TorrentManager(component.Component):
if log.isEnabledFor(logging.DEBUG):
log.debug('Attempting to extract torrent_info from %s', filepath)
try:
- torrent_info = lt.torrent_info(filepath)
- except RuntimeError as ex:
+ torrent_info = lt.torrent_info(Path(filepath).read_bytes())
+ except (RuntimeError, OSError) as ex:
log.warning('Unable to open torrent file %s: %s', filepath, ex)
else:
return torrent_info
@@ -767,6 +769,11 @@ class TorrentManager(component.Component):
torrent_name,
component.get('RPCServer').get_session_user(),
)
+
+ for file in self.ssl_file_paths_for_torrent(torrent_id):
+ if file.is_file():
+ file.unlink()
+
return True
def fixup_state(self, state):
@@ -1340,6 +1347,50 @@ class TorrentManager(component.Component):
torrent.update_state()
+ def ssl_file_paths_for_torrent(self, torrent_id):
+ certs_dir = Path(self.config['ssl_torrents_certs'])
+
+ crt_file = certs_dir / f'{torrent_id}.crt.pem'
+ key_file = certs_dir / f'{torrent_id}.key.pem'
+ dh_params_file = certs_dir / f'{torrent_id}.dh.pem'
+
+ return crt_file, key_file, dh_params_file
+
+ def on_alert_torrent_need_cert(self, alert):
+ """Alert handler for libtorrent torrent_need_cert_alert"""
+
+ if not self.config['ssl_torrents']:
+ return
+
+ torrent_id = str(alert.handle.info_hash())
+
+ certs_dir = Path(self.config['ssl_torrents_certs'])
+ crt_file, key_file, dh_params_file = self.ssl_file_paths_for_torrent(torrent_id)
+ if not crt_file.is_file() or not key_file.is_file():
+ crt_file = certs_dir / 'default.crt.pem'
+ key_file = certs_dir / 'default.key.pem'
+ if not dh_params_file.is_file():
+ dh_params_file = certs_dir / 'default.dh.pem'
+ if not (crt_file.is_file() and key_file.is_file() and dh_params_file.is_file()):
+ log.error('Unable to load certs for SSL Torrent %s', torrent_id)
+ return
+
+ try:
+ # Cannot use the handle via self.torrents.
+ # torrent_need_cert_alert is raised before add_torrent_alert
+ alert.handle.set_ssl_certificate(
+ str(crt_file), str(key_file), str(dh_params_file)
+ )
+ except RuntimeError as err:
+ log.error(
+ 'Unable to set ssl certificate for %s from files %s:%s:%s: %s',
+ torrent_id,
+ crt_file,
+ key_file,
+ dh_params_file,
+ err,
+ )
+
def on_alert_tracker_reply(self, alert):
"""Alert handler for libtorrent tracker_reply_alert"""
try: