diff options
| author | 2025-11-30 18:42:19 +0100 | |
|---|---|---|
| committer | 2025-11-30 18:42:19 +0100 | |
| commit | 5d5f9bd85b94db8d64148435c30f7fb819bcd255 (patch) | |
| tree | d406aeebcdc5d273c9e30eb08b59d9c498cd991a /deluge/core | |
| parent | 3e1888c3d656906263b0cd08875885166efcfb63 (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.py | 70 | ||||
| -rw-r--r-- | deluge/core/core.py | 67 | ||||
| -rw-r--r-- | deluge/core/preferencesmanager.py | 23 | ||||
| -rw-r--r-- | deluge/core/torrent.py | 50 | ||||
| -rw-r--r-- | deluge/core/torrentmanager.py | 55 |
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: |
