diff options
Diffstat (limited to 'deluge/plugins/Blocklist/deluge_blocklist')
14 files changed, 2324 insertions, 0 deletions
diff --git a/deluge/plugins/Blocklist/deluge_blocklist/__init__.py b/deluge/plugins/Blocklist/deluge_blocklist/__init__.py new file mode 100644 index 0000000..40ce1d1 --- /dev/null +++ b/deluge/plugins/Blocklist/deluge_blocklist/__init__.py @@ -0,0 +1,33 @@ +# +# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +from deluge.plugins.init import PluginInitBase + + +class CorePlugin(PluginInitBase): + def __init__(self, plugin_name): + from .core import Core as _pluginCls + + self._plugin_cls = _pluginCls + super().__init__(plugin_name) + + +class GtkUIPlugin(PluginInitBase): + def __init__(self, plugin_name): + from .gtkui import GtkUI as _pluginCls + + self._plugin_cls = _pluginCls + super().__init__(plugin_name) + + +class WebUIPlugin(PluginInitBase): + def __init__(self, plugin_name): + from .webui import WebUI as _pluginCls + + self._plugin_cls = _pluginCls + super().__init__(plugin_name) diff --git a/deluge/plugins/Blocklist/deluge_blocklist/common.py b/deluge/plugins/Blocklist/deluge_blocklist/common.py new file mode 100644 index 0000000..35b2f87 --- /dev/null +++ b/deluge/plugins/Blocklist/deluge_blocklist/common.py @@ -0,0 +1,172 @@ +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# 2007-2009 Andrew Resch <andrewresch@gmail.com> +# 2009 Damien Churchill <damoxc@gmail.com> +# 2010 Pedro Algarvio <pedro@algarvio.me> +# 2017 Calum Lind <calumlind+deluge@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import os.path +from functools import wraps +from sys import exc_info + +from pkg_resources import resource_filename + + +def get_resource(filename): + return resource_filename(__package__, os.path.join('data', filename)) + + +def raises_errors_as(error): + """Factory class that returns a decorator which wraps the decorated + function to raise all exceptions as the specified error type. + + """ + + def decorator(func): + """Returns a function which wraps the given func to raise all exceptions as error.""" + + @wraps(func) + def wrapper(self, *args, **kwargs): + """Wraps the function in a try..except block and calls it with the specified args. + + Raises: + Any exceptions as error preserving the message and traceback. + + """ + try: + return func(self, *args, **kwargs) + except Exception: + (value, tb) = exc_info()[1:] + raise error(value).with_traceback(tb) from None + + return wrapper + + return decorator + + +def remove_zeros(ip): + """Removes unneeded zeros from ip addresses. + + Args: + ip (str): The ip address. + + Returns: + str: The ip address without the unneeded zeros. + + Example: + 000.000.000.003 -> 0.0.0.3 + + """ + return '.'.join([part.lstrip('0').zfill(1) for part in ip.split('.')]) + + +class BadIP(Exception): + _message = None + + def __init__(self, message): + super().__init__(message) + + def __set_message(self, message): + self._message = message + + def __get_message(self): + return self._message + + message = property(__get_message, __set_message) + del __get_message, __set_message + + +class IP: + __slots__ = ('q1', 'q2', 'q3', 'q4', '_long') + + def __init__(self, q1, q2, q3, q4): + self.q1 = q1 + self.q2 = q2 + self.q3 = q3 + self.q4 = q4 + self._long = 0 + for q in self.quadrants(): + self._long = (self._long << 8) | int(q) + + @property + def address(self): + return '.'.join([str(q) for q in [self.q1, self.q2, self.q3, self.q4]]) + + @property + def long(self): + return self._long + + @classmethod + def parse(cls, ip): + try: + q1, q2, q3, q4 = (int(q) for q in ip.split('.')) + except ValueError: + raise BadIP(_('The IP address "%s" is badly formed' % ip)) + if q1 < 0 or q2 < 0 or q3 < 0 or q4 < 0: + raise BadIP(_('The IP address "%s" is badly formed' % ip)) + elif q1 > 255 or q2 > 255 or q3 > 255 or q4 > 255: + raise BadIP(_('The IP address "%s" is badly formed' % ip)) + return cls(q1, q2, q3, q4) + + def quadrants(self): + return (self.q1, self.q2, self.q3, self.q4) + + # def next_ip(self): + # (q1, q2, q3, q4) = self.quadrants() + # if q4 >= 255: + # if q3 >= 255: + # if q2 >= 255: + # if q1 >= 255: + # raise BadIP(_('There is not a next IP address')) + # q1 += 1 + # else: + # q2 += 1 + # else: + # q3 += 1 + # else: + # q4 += 1 + # return IP(q1, q2, q3, q4) + # + # def previous_ip(self): + # (q1, q2, q3, q4) = self.quadrants() + # if q4 <= 1: + # if q3 <= 1: + # if q2 <= 1: + # if q1 <= 1: + # raise BadIP(_('There is not a previous IP address')) + # q1 -= 1 + # else: + # q2 -= 1 + # else: + # q3 -= 1 + # else: + # q4 -= 1 + # return IP(q1, q2, q3, q4) + + def __lt__(self, other): + if isinstance(other, ''.__class__): + other = IP.parse(other) + return self.long < other.long + + def __gt__(self, other): + if isinstance(other, ''.__class__): + other = IP.parse(other) + return self.long > other.long + + def __eq__(self, other): + if isinstance(other, ''.__class__): + other = IP.parse(other) + return self.long == other.long + + def __repr__(self): + return '<{} long={} address="{}">'.format( + self.__class__.__name__, + self.long, + self.address, + ) diff --git a/deluge/plugins/Blocklist/deluge_blocklist/core.py b/deluge/plugins/Blocklist/deluge_blocklist/core.py new file mode 100644 index 0000000..1765767 --- /dev/null +++ b/deluge/plugins/Blocklist/deluge_blocklist/core.py @@ -0,0 +1,549 @@ +# +# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com> +# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import logging +import os +import shutil +import time +from datetime import datetime, timedelta +from email.utils import formatdate +from urllib.parse import urljoin + +from twisted.internet import defer, threads +from twisted.internet.task import LoopingCall +from twisted.web import error + +import deluge.component as component +import deluge.configmanager +from deluge.common import is_url +from deluge.core.rpcserver import export +from deluge.httpdownloader import download_file +from deluge.plugins.pluginbase import CorePluginBase + +from .common import IP, BadIP +from .detect import UnknownFormatError, create_reader, detect_compression, detect_format +from .readers import ReaderParseError + +# TODO: check return values for deferred callbacks +# TODO: review class attributes for redundancy + +log = logging.getLogger(__name__) + +DEFAULT_PREFS = { + 'url': '', + 'load_on_start': False, + 'check_after_days': 4, + 'list_compression': '', + 'list_type': '', + 'last_update': 0.0, + 'list_size': 0, + 'timeout': 180, + 'try_times': 3, + 'whitelisted': [], +} + +# Constants +ALLOW_RANGE = 0 +BLOCK_RANGE = 1 + + +class Core(CorePluginBase): + def enable(self): + log.debug('Blocklist: Plugin enabled...') + + self.is_url = True + self.is_downloading = False + self.is_importing = False + self.has_imported = False + self.up_to_date = False + self.need_to_resume_session = False + self.num_whited = 0 + self.num_blocked = 0 + self.file_progress = 0.0 + + self.core = component.get('Core') + self.config = deluge.configmanager.ConfigManager( + 'blocklist.conf', DEFAULT_PREFS + ) + if 'whitelisted' not in self.config: + self.config['whitelisted'] = [] + + self.reader = create_reader( + self.config['list_type'], self.config['list_compression'] + ) + + if not isinstance(self.config['last_update'], float): + self.config.config['last_update'] = 0.0 + + update_now = False + if self.config['load_on_start']: + self.pause_session() + if self.config['last_update']: + last_update = datetime.fromtimestamp(self.config['last_update']) + check_period = timedelta(days=self.config['check_after_days']) + if ( + not self.config['last_update'] + or last_update + check_period < datetime.now() + ): + update_now = True + else: + d = self.import_list( + deluge.configmanager.get_config_dir('blocklist.cache') + ) + d.addCallbacks(self.on_import_complete, self.on_import_error) + if self.need_to_resume_session: + d.addBoth(self.resume_session) + + # This function is called every 'check_after_days' days, to download + # and import a new list if needed. + self.update_timer = LoopingCall(self.check_import) + if self.config['check_after_days'] > 0: + self.update_timer.start( + self.config['check_after_days'] * 24 * 60 * 60, update_now + ) + + def disable(self): + self.config.save() + log.debug('Reset IP filter') + self.core.session.get_ip_filter().add_rule( + '0.0.0.0', '255.255.255.255', ALLOW_RANGE + ) + log.debug('Blocklist: Plugin disabled') + + def update(self): + pass + + # Exported RPC methods # + @export + def check_import(self, force=False): + """Imports latest blocklist specified by blocklist url. + + Args: + force (bool, optional): Force the download/import, default is False. + + Returns: + Deferred: A Deferred which fires when the blocklist has been imported. + + """ + if not self.config['url']: + return + + # Reset variables + self.filename = None + self.force_download = force + self.failed_attempts = 0 + self.auto_detected = False + self.up_to_date = False + if force: + self.reader = None + self.is_url = is_url(self.config['url']) + + # Start callback chain + if self.is_url: + d = self.download_list() + d.addCallbacks(self.on_download_complete, self.on_download_error) + d.addCallback(self.import_list) + else: + d = self.import_list(self.config['url']) + d.addCallbacks(self.on_import_complete, self.on_import_error) + if self.need_to_resume_session: + d.addBoth(self.resume_session) + + return d + + @export + def get_config(self): + """Gets the blocklist config dictionary. + + Returns: + dict: The config dictionary. + + """ + return self.config.config + + @export + def set_config(self, config): + """Sets the blocklist config. + + Args: + config (dict): config to set. + + """ + needs_blocklist_import = False + for key in config: + if key == 'whitelisted': + saved = set(self.config[key]) + update = set(config[key]) + diff = saved.symmetric_difference(update) + if diff: + log.debug('Whitelist changed. Updating...') + added = update.intersection(diff) + removed = saved.intersection(diff) + if added: + for ip in added: + try: + ip = IP.parse(ip) + self.blocklist.add_rule( + ip.address, ip.address, ALLOW_RANGE + ) + saved.add(ip.address) + log.debug('Added %s to whitelisted', ip) + self.num_whited += 1 + except BadIP as ex: + log.error('Bad IP: %s', ex) + continue + if removed: + needs_blocklist_import = True + for ip in removed: + try: + ip = IP.parse(ip) + saved.remove(ip.address) + log.debug('Removed %s from whitelisted', ip) + except BadIP as ex: + log.error('Bad IP: %s', ex) + continue + + self.config[key] = list(saved) + continue + elif key == 'check_after_days': + if self.config[key] != config[key]: + self.config[key] = config[key] + update_now = False + if self.config['last_update']: + last_update = datetime.fromtimestamp(self.config['last_update']) + check_period = timedelta(days=self.config['check_after_days']) + if ( + not self.config['last_update'] + or last_update + check_period < datetime.now() + ): + update_now = True + if self.update_timer.running: + self.update_timer.stop() + if self.config['check_after_days'] > 0: + self.update_timer.start( + self.config['check_after_days'] * 24 * 60 * 60, update_now + ) + continue + self.config[key] = config[key] + + if needs_blocklist_import: + log.debug( + 'IP addresses were removed from the whitelist. Since we ' + 'do not know if they were blocked before. Re-import ' + 'current blocklist and re-add whitelisted.' + ) + self.has_imported = False + d = self.import_list(deluge.configmanager.get_config_dir('blocklist.cache')) + d.addCallbacks(self.on_import_complete, self.on_import_error) + + @export + def get_status(self): + """Get the status of the plugin. + + Returns: + dict: The status dict of the plugin. + + """ + status = {} + if self.is_downloading: + status['state'] = 'Downloading' + elif self.is_importing: + status['state'] = 'Importing' + else: + status['state'] = 'Idle' + + status['up_to_date'] = self.up_to_date + status['num_whited'] = self.num_whited + status['num_blocked'] = self.num_blocked + status['file_progress'] = self.file_progress + status['file_url'] = self.config['url'] + status['file_size'] = self.config['list_size'] + status['file_date'] = self.config['last_update'] + status['file_type'] = self.config['list_type'] + status['whitelisted'] = self.config['whitelisted'] + if self.config['list_compression']: + status['file_type'] += ' (%s)' % self.config['list_compression'] + return status + + #### + + def update_info(self, blocklist): + """Updates blocklist info. + + Args: + blocklist (str): Path of blocklist. + + Returns: + str: Path of blocklist. + + """ + log.debug('Updating blocklist info: %s', blocklist) + self.config['last_update'] = time.time() + self.config['list_size'] = os.path.getsize(blocklist) + self.filename = blocklist + return blocklist + + def download_list(self, url=None): + """Downloads the blocklist specified by 'url' in the config. + + Args: + url (str, optional): url to download from, defaults to config value. + + Returns: + Deferred: a Deferred which fires once the blocklist has been downloaded. + + """ + + def on_retrieve_data(data, current_length, total_length): + if total_length: + fp = current_length / total_length + if fp > 1.0: + fp = 1.0 + else: + fp = 0.0 + + self.file_progress = fp + + import socket + + socket.setdefaulttimeout(self.config['timeout']) + + if not url: + url = self.config['url'] + + headers = {} + if self.config['last_update'] and not self.force_download: + headers['If-Modified-Since'] = formatdate( + self.config['last_update'], usegmt=True + ) + + log.debug('Attempting to download blocklist %s', url) + log.debug('Sending headers: %s', headers) + self.is_downloading = True + return download_file( + url, + deluge.configmanager.get_config_dir('blocklist.download'), + on_retrieve_data, + headers, + ) + + def on_download_complete(self, blocklist): + """Runs any download clean up functions. + + Args: + blocklist (str): Path of blocklist. + + Returns: + Deferred: a Deferred which fires when clean up is done. + + """ + log.debug('Blocklist download complete: %s', blocklist) + self.is_downloading = False + return threads.deferToThread(self.update_info, blocklist) + + def on_download_error(self, f): + """Recovers from download error. + + Args: + f (Failure): Failure that occurred. + + Returns: + Deferred or Failure: A Deferred if recovery was possible else original Failure. + + """ + self.is_downloading = False + error_msg = f.getErrorMessage() + d = f + if f.check(error.PageRedirect): + # Handle redirect errors + location = urljoin(self.config['url'], error_msg.split(' to ')[1]) + if 'Moved Permanently' in error_msg: + log.debug('Setting blocklist url to %s', location) + self.config['url'] = location + d = self.download_list(location) + d.addCallbacks(self.on_download_complete, self.on_download_error) + else: + if 'Not Modified' in error_msg: + log.debug('Blocklist is up-to-date!') + self.up_to_date = True + blocklist = deluge.configmanager.get_config_dir('blocklist.cache') + d = threads.deferToThread(self.update_info, blocklist) + else: + log.warning('Blocklist download failed: %s', error_msg) + if self.failed_attempts < self.config['try_times']: + log.debug( + 'Try downloading blocklist again... (%s/%s)', + self.failed_attempts, + self.config['try_times'], + ) + self.failed_attempts += 1 + d = self.download_list() + d.addCallbacks(self.on_download_complete, self.on_download_error) + return d + + def import_list(self, blocklist): + """Imports the downloaded blocklist into the session. + + Args: + blocklist (str): path of blocklist. + + Returns: + Deferred: A Deferred that fires when the blocklist has been imported. + + """ + log.trace('on import_list') + + def on_read_ip_range(start, end): + """Add ip range to blocklist""" + # log.trace('Adding ip range %s - %s to ipfilter as blocked', start, end) + self.blocklist.add_rule(start.address, end.address, BLOCK_RANGE) + self.num_blocked += 1 + + def on_finish_read(result): + """Add any whitelisted IP's and add the blocklist to session""" + # White listing happens last because the last rules added have + # priority + log.info('Added %d ranges to ipfilter as blocked', self.num_blocked) + for ip in self.config['whitelisted']: + ip = IP.parse(ip) + self.blocklist.add_rule(ip.address, ip.address, ALLOW_RANGE) + self.num_whited += 1 + log.trace('Added %s to the ipfiler as white-listed', ip.address) + log.info('Added %d ranges to ipfilter as white-listed', self.num_whited) + self.core.session.set_ip_filter(self.blocklist) + return result + + # TODO: double check logic + if self.up_to_date and self.has_imported: + log.debug('Latest blocklist is already imported') + return defer.succeed(blocklist) + + self.is_importing = True + self.num_blocked = 0 + self.num_whited = 0 + self.blocklist = self.core.session.get_ip_filter() + + if not blocklist: + blocklist = self.filename + + if not self.reader: + self.auto_detect(blocklist) + self.auto_detected = True + + def on_reader_failure(failure): + log.error('Failed to read!!!!!!') + log.exception(failure) + + log.debug('Importing using reader: %s', self.reader) + log.debug( + 'Reader type: %s compression: %s', + self.config['list_type'], + self.config['list_compression'], + ) + log.debug('Clearing current ip filtering') + # self.blocklist.add_rule('0.0.0.0', '255.255.255.255', ALLOW_RANGE) + d = threads.deferToThread(self.reader(blocklist).read, on_read_ip_range) + d.addCallback(on_finish_read).addErrback(on_reader_failure) + + return d + + def on_import_complete(self, blocklist): + """Runs any import clean up functions. + + Args: + blocklist (str): Path of blocklist. + + Returns: + Deferred: A Deferred that fires when clean up is done. + + """ + log.trace('on_import_list_complete') + d = blocklist + self.is_importing = False + self.has_imported = True + log.debug('Blocklist import complete!') + cache = deluge.configmanager.get_config_dir('blocklist.cache') + if blocklist != cache: + if self.is_url: + log.debug('Moving %s to %s', blocklist, cache) + d = threads.deferToThread(shutil.move, blocklist, cache) + else: + log.debug('Copying %s to %s', blocklist, cache) + d = threads.deferToThread(shutil.copy, blocklist, cache) + return d + + def on_import_error(self, f): + """Recovers from import error. + + Args: + f (Failure): Failure that occurred. + + Returns: + Deferred or Failure: A Deferred if recovery was possible else original Failure. + + """ + log.trace('on_import_error: %s', f) + d = f + self.is_importing = False + try_again = False + cache = deluge.configmanager.get_config_dir('blocklist.cache') + + if f.check(ReaderParseError) and not self.auto_detected: + # Invalid / corrupt list, let's detect it + log.warning('Invalid / corrupt blocklist') + self.reader = None + blocklist = None + try_again = True + elif self.filename != cache and os.path.exists(cache): + # If we have a backup and we haven't already used it + log.warning('Error reading blocklist: %s', f.getErrorMessage()) + blocklist = cache + try_again = True + + if try_again: + d = self.import_list(blocklist) + d.addCallbacks(self.on_import_complete, self.on_import_error) + + return d + + def auto_detect(self, blocklist): + """Attempts to auto-detect the blocklist type. + + Args: + blocklist (str): Path of blocklist. + + Raises: + UnknownFormatError: If the format cannot be detected. + + """ + self.config['list_compression'] = detect_compression(blocklist) + self.config['list_type'] = detect_format( + blocklist, self.config['list_compression'] + ) + log.debug( + 'Auto-detected type: %s compression: %s', + self.config['list_type'], + self.config['list_compression'], + ) + if not self.config['list_type']: + self.config['list_compression'] = '' + raise UnknownFormatError + else: + self.reader = create_reader( + self.config['list_type'], self.config['list_compression'] + ) + + def pause_session(self): + self.need_to_resume_session = not self.core.session.is_paused() + self.core.pause_session() + + def resume_session(self, result): + self.core.resume_session() + self.need_to_resume_session = False + return result diff --git a/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist.js b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist.js new file mode 100644 index 0000000..3c10b81 --- /dev/null +++ b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist.js @@ -0,0 +1,429 @@ +/** + * blocklist.js + * + * Copyright (C) Omar Alvarez 2014 <omar.alvarez@udc.es> + * + * This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with + * the additional special exception to link portions of this program with the OpenSSL library. + * See LICENSE for more details. + * + */ + +Ext.ns('Deluge.ux.preferences'); + +/** + * @class Deluge.ux.preferences.BlocklistPage + * @extends Ext.Panel + */ +Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, { + title: _('Blocklist'), + header: false, + layout: 'fit', + border: false, + autoScroll: true, + + initComponent: function () { + Deluge.ux.preferences.BlocklistPage.superclass.initComponent.call(this); + + this.URLFset = this.add({ + xtype: 'fieldset', + border: false, + title: _('General'), + autoHeight: true, + defaultType: 'textfield', + style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;', + autoWidth: true, + labelWidth: 40, + }); + + this.URL = this.URLFset.add({ + fieldLabel: _('URL:'), + labelSeparator: '', + name: 'url', + width: '80%', + }); + + this.SettingsFset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Settings'), + autoHeight: true, + defaultType: 'spinnerfield', + style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;', + autoWidth: true, + labelWidth: 160, + }); + + this.checkListDays = this.SettingsFset.add({ + fieldLabel: _('Check for new list every (days):'), + labelSeparator: '', + name: 'check_list_days', + value: 4, + decimalPrecision: 0, + width: 80, + }); + + this.chkImportOnStart = this.SettingsFset.add({ + xtype: 'checkbox', + fieldLabel: _('Import blocklist on startup'), + name: 'check_import_startup', + }); + + this.OptionsFset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Options'), + autoHeight: true, + defaultType: 'button', + style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;', + autoWidth: false, + width: '80%', + labelWidth: 0, + }); + + this.checkDownload = this.OptionsFset.add({ + fieldLabel: _(''), + name: 'check_download', + xtype: 'container', + layout: 'hbox', + margins: '4 0 0 5', + items: [ + { + xtype: 'button', + text: ' Check Download and Import ', + scale: 'medium', + }, + { + xtype: 'box', + autoEl: { + tag: 'img', + src: '../icons/ok.png', + }, + margins: '4 0 0 3', + }, + ], + }); + + this.forceDownload = this.OptionsFset.add({ + fieldLabel: _(''), + name: 'force_download', + text: ' Force Download and Import ', + margins: '2 0 0 0', + //icon: '../icons/blocklist_import24.png', + scale: 'medium', + }); + + this.ProgressFset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Info'), + autoHeight: true, + defaultType: 'progress', + style: 'margin-top: 1px; margin-bottom: 0px; padding-bottom: 0px;', + autoWidth: true, + labelWidth: 0, + hidden: true, + }); + + this.downProgBar = this.ProgressFset.add({ + fieldLabel: _(''), + name: 'progress_bar', + width: '90%', + }); + + this.InfoFset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Info'), + autoHeight: true, + defaultType: 'label', + style: 'margin-top: 0px; margin-bottom: 0px; padding-bottom: 0px;', + labelWidth: 60, + }); + + this.lblFileSize = this.InfoFset.add({ + fieldLabel: _('File Size:'), + labelSeparator: '', + name: 'file_size', + }); + + this.lblDate = this.InfoFset.add({ + fieldLabel: _('Date:'), + labelSeparator: '', + name: 'date', + }); + + this.lblType = this.InfoFset.add({ + fieldLabel: _('Type:'), + labelSeparator: '', + name: 'type', + }); + + this.lblURL = this.InfoFset.add({ + fieldLabel: _('URL:'), + labelSeparator: '', + name: 'lbl_URL', + }); + + this.WhitelistFset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Whitelist'), + autoHeight: true, + defaultType: 'editorgrid', + style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;', + autoWidth: true, + labelWidth: 0, + items: [ + { + fieldLabel: _(''), + name: 'whitelist', + margins: '2 0 5 5', + height: 100, + width: 260, + autoExpandColumn: 'ip', + viewConfig: { + emptyText: _('Add an IP...'), + deferEmptyText: false, + }, + colModel: new Ext.grid.ColumnModel({ + columns: [ + { + id: 'ip', + header: _('IP'), + dataIndex: 'ip', + sortable: true, + hideable: false, + editable: true, + editor: { + xtype: 'textfield', + }, + }, + ], + }), + selModel: new Ext.grid.RowSelectionModel({ + singleSelect: false, + moveEditorOnEnter: false, + }), + store: new Ext.data.ArrayStore({ + autoDestroy: true, + fields: [{ name: 'ip' }], + }), + listeners: { + afteredit: function (e) { + e.record.commit(); + }, + }, + setEmptyText: function (text) { + if (this.viewReady) { + this.getView().emptyText = text; + this.getView().refresh(); + } else { + Ext.apply(this.viewConfig, { emptyText: text }); + } + }, + loadData: function (data) { + this.getStore().loadData(data); + if (this.viewReady) { + this.getView().updateHeaders(); + } + }, + }, + ], + }); + + this.ipButtonsContainer = this.WhitelistFset.add({ + xtype: 'container', + layout: 'hbox', + margins: '4 0 0 5', + items: [ + { + xtype: 'button', + text: ' Add IP ', + margins: '0 5 0 0', + }, + { + xtype: 'button', + text: ' Delete IP ', + }, + ], + }); + + this.updateTask = Ext.TaskMgr.start({ + interval: 2000, + run: this.onUpdate, + scope: this, + }); + + this.on('show', this.updateConfig, this); + + this.ipButtonsContainer.getComponent(0).setHandler(this.addIP, this); + this.ipButtonsContainer.getComponent(1).setHandler(this.deleteIP, this); + + this.checkDownload.getComponent(0).setHandler(this.checkDown, this); + this.forceDownload.setHandler(this.forceDown, this); + }, + + onApply: function () { + var config = {}; + + config['url'] = this.URL.getValue(); + config['check_after_days'] = this.checkListDays.getValue(); + config['load_on_start'] = this.chkImportOnStart.getValue(); + + var ipList = []; + var store = this.WhitelistFset.getComponent(0).getStore(); + + for (var i = 0; i < store.getCount(); i++) { + var record = store.getAt(i); + var ip = record.get('ip'); + ipList.push(ip); + } + + config['whitelisted'] = ipList; + + deluge.client.blocklist.set_config(config); + }, + + onOk: function () { + this.onApply(); + }, + + onUpdate: function () { + deluge.client.blocklist.get_status({ + success: function (status) { + if (status['state'] == 'Downloading') { + this.InfoFset.hide(); + this.checkDownload.getComponent(0).setDisabled(true); + this.checkDownload.getComponent(1).hide(); + this.forceDownload.setDisabled(true); + + this.ProgressFset.show(); + this.downProgBar.updateProgress( + status['file_progress'], + 'Downloading ' + .concat((status['file_progress'] * 100).toFixed(2)) + .concat('%'), + true + ); + } else if (status['state'] == 'Importing') { + this.InfoFset.hide(); + this.checkDownload.getComponent(0).setDisabled(true); + this.checkDownload.getComponent(1).hide(); + this.forceDownload.setDisabled(true); + + this.ProgressFset.show(); + this.downProgBar.updateText( + 'Importing '.concat(status['num_blocked']) + ); + } else if (status['state'] == 'Idle') { + this.ProgressFset.hide(); + this.checkDownload.getComponent(0).setDisabled(false); + this.forceDownload.setDisabled(false); + if (status['up_to_date']) { + this.checkDownload.getComponent(1).show(); + this.checkDownload.doLayout(); + } else { + this.checkDownload.getComponent(1).hide(); + } + this.InfoFset.show(); + this.lblFileSize.setText(fsize(status['file_size'])); + this.lblDate.setText(fdate(status['file_date'])); + this.lblType.setText(status['file_type']); + this.lblURL.setText( + status['file_url'].substr(0, 40).concat('...') + ); + } + }, + scope: this, + }); + }, + + checkDown: function () { + this.onApply(); + deluge.client.blocklist.check_import(); + }, + + forceDown: function () { + this.onApply(); + deluge.client.blocklist.check_import((force = true)); + }, + + updateConfig: function () { + deluge.client.blocklist.get_config({ + success: function (config) { + this.URL.setValue(config['url']); + this.checkListDays.setValue(config['check_after_days']); + this.chkImportOnStart.setValue(config['load_on_start']); + + var data = []; + var keys = Ext.keys(config['whitelisted']); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + data.push([config['whitelisted'][key]]); + } + + this.WhitelistFset.getComponent(0).loadData(data); + }, + scope: this, + }); + + deluge.client.blocklist.get_status({ + success: function (status) { + this.lblFileSize.setText(fsize(status['file_size'])); + this.lblDate.setText(fdate(status['file_date'])); + this.lblType.setText(status['file_type']); + this.lblURL.setText( + status['file_url'].substr(0, 40).concat('...') + ); + }, + scope: this, + }); + }, + + addIP: function () { + var store = this.WhitelistFset.getComponent(0).getStore(); + var IP = store.recordType; + var i = new IP({ + ip: '', + }); + this.WhitelistFset.getComponent(0).stopEditing(); + store.insert(0, i); + this.WhitelistFset.getComponent(0).startEditing(0, 0); + }, + + deleteIP: function () { + var selections = this.WhitelistFset.getComponent(0) + .getSelectionModel() + .getSelections(); + var store = this.WhitelistFset.getComponent(0).getStore(); + + this.WhitelistFset.getComponent(0).stopEditing(); + for (var i = 0; i < selections.length; i++) store.remove(selections[i]); + store.commitChanges(); + }, + + onDestroy: function () { + Ext.TaskMgr.stop(this.updateTask); + + deluge.preferences.un('show', this.updateConfig, this); + + Deluge.ux.preferences.BlocklistPage.superclass.onDestroy.call(this); + }, +}); + +Deluge.plugins.BlocklistPlugin = Ext.extend(Deluge.Plugin, { + name: 'Blocklist', + + onDisable: function () { + deluge.preferences.removePage(this.prefsPage); + }, + + onEnable: function () { + this.prefsPage = deluge.preferences.addPage( + new Deluge.ux.preferences.BlocklistPage() + ); + }, +}); + +Deluge.registerPlugin('Blocklist', Deluge.plugins.BlocklistPlugin); diff --git a/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist16.png b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist16.png Binary files differnew file mode 100644 index 0000000..15b4299 --- /dev/null +++ b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist16.png diff --git a/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_download24.png b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_download24.png Binary files differnew file mode 100644 index 0000000..6de3a0d --- /dev/null +++ b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_download24.png diff --git a/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_import24.png b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_import24.png Binary files differnew file mode 100644 index 0000000..f1a02e7 --- /dev/null +++ b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_import24.png diff --git a/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_pref.ui b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_pref.ui new file mode 100644 index 0000000..8c1f7a7 --- /dev/null +++ b/deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_pref.ui @@ -0,0 +1,603 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkAdjustment" id="adjustment1"> + <property name="lower">1</property> + <property name="upper">100</property> + <property name="value">1</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkWindow" id="window1"> + <property name="can_focus">False</property> + <child> + <placeholder/> + </child> + <child> + <object class="GtkBox" id="blocklist_prefs_box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkFrame" id="frame1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">12</property> + <child> + <object class="GtkBox" id="hbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">URL:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_url"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">●</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="ypad">5</property> + <property name="label" translatable="yes"><b>General</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">12</property> + <child> + <object class="GtkBox" id="vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkTable" id="table1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_columns">3</property> + <property name="column_spacing">5</property> + <property name="row_spacing">5</property> + <child> + <object class="GtkLabel" id="label8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Days</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="spin_check_days"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">adjustment1</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="label4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Check for new list every (days):</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_import_on_start"> + <property name="label" translatable="yes">Import blocklist on startup</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label10"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="ypad">5</property> + <property name="label" translatable="yes"><b>Settings</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="xscale">0</property> + <property name="left_padding">12</property> + <child> + <object class="GtkBox" id="hbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkBox" id="vbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkButton" id="button_check_download"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Download the blocklist file if necessary and import the file.</property> + <signal name="clicked" handler="on_button_check_download_clicked" swapped="no"/> + <child> + <object class="GtkBox" id="hbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image_download"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="stock">gtk-missing-image</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label12"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Check Download and Import</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_force_download"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Download a new blocklist file and import it.</property> + <signal name="clicked" handler="on_button_force_download_clicked" swapped="no"/> + <child> + <object class="GtkBox" id="hbox5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkImage" id="image_import"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="stock">gtk-missing-image</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Force Download and Import</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkImage" id="image_up_to_date"> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">Blocklist is up to date</property> + <property name="yalign">0.15000000596046448</property> + <property name="xpad">2</property> + <property name="stock">gtk-yes</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label11"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="ypad">5</property> + <property name="label" translatable="yes"><b>Options</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="frame4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="top_padding">5</property> + <property name="left_padding">12</property> + <child> + <object class="GtkBox" id="vbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkProgressBar" id="progressbar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkTable" id="table_info"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">4</property> + <property name="n_columns">2</property> + <property name="column_spacing">5</property> + <child> + <object class="GtkLabel" id="label_url"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_type"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_modified"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_filesize"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label17"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">URL:</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="x_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label16"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Type:</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label15"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Date:</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label14"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">File Size:</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><b>Info</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkFrame" id="whitelist_frame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkAlignment" id="alignment5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="left_padding">12</property> + <child> + <object class="GtkBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkTreeView" id="whitelist_treeview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="headers_visible">False</property> + <property name="headers_clickable">False</property> + <child internal-child="selection"> + <object class="GtkTreeSelection"/> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVButtonBox" id="vbuttonbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="homogeneous">True</property> + <property name="layout_style">start</property> + <child> + <object class="GtkButton" id="whitelist_add"> + <property name="label">gtk-add</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="on_whitelist_add_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="whitelist_delete"> + <property name="label">gtk-delete</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="on_whitelist_remove_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><b>Whitelist</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/deluge/plugins/Blocklist/deluge_blocklist/decompressers.py b/deluge/plugins/Blocklist/deluge_blocklist/decompressers.py new file mode 100644 index 0000000..cd2ee8c --- /dev/null +++ b/deluge/plugins/Blocklist/deluge_blocklist/decompressers.py @@ -0,0 +1,44 @@ +# +# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# +# pylint: disable=redefined-builtin + +import bz2 +import gzip +import zipfile + + +def Zipped(reader): # NOQA: N802 + """Blocklist reader for zipped blocklists""" + + def _open(self): + z = zipfile.ZipFile(self.file) + f = z.open(z.namelist()[0]) + return f + + reader.open = _open + return reader + + +def GZipped(reader): # NOQA: N802 + """Blocklist reader for gzipped blocklists""" + + def _open(self): + return gzip.open(self.file) + + reader.open = _open + return reader + + +def BZipped2(reader): # NOQA: N802 + """Blocklist reader for bzipped2 blocklists""" + + def _open(self): + return bz2.BZ2File(self.file) + + reader.open = _open + return reader diff --git a/deluge/plugins/Blocklist/deluge_blocklist/detect.py b/deluge/plugins/Blocklist/deluge_blocklist/detect.py new file mode 100644 index 0000000..43ad305 --- /dev/null +++ b/deluge/plugins/Blocklist/deluge_blocklist/detect.py @@ -0,0 +1,48 @@ +# +# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +from .decompressers import BZipped2, GZipped, Zipped +from .readers import EmuleReader, PeerGuardianReader, SafePeerReader + +COMPRESSION_TYPES = {b'PK': 'Zip', b'\x1f\x8b': 'GZip', b'BZ': 'BZip2'} + +DECOMPRESSERS = {'Zip': Zipped, 'GZip': GZipped, 'BZip2': BZipped2} + +READERS = { + 'Emule': EmuleReader, + 'SafePeer': SafePeerReader, + 'PeerGuardian': PeerGuardianReader, +} + + +class UnknownFormatError(Exception): + pass + + +def detect_compression(filename): + with open(filename, 'rb') as _file: + magic_number = _file.read(2) + return COMPRESSION_TYPES.get(magic_number, '') + + +def detect_format(filename, compression=''): + file_format = '' + for reader in READERS: + if create_reader(reader, compression)(filename).is_valid(): + file_format = reader + break + return file_format + + +def create_reader(file_format, compression=''): + reader = READERS.get(file_format) + if reader and compression: + decompressor = DECOMPRESSERS.get(compression) + if decompressor: + reader = decompressor(reader) + return reader diff --git a/deluge/plugins/Blocklist/deluge_blocklist/gtkui.py b/deluge/plugins/Blocklist/deluge_blocklist/gtkui.py new file mode 100644 index 0000000..e6105cd --- /dev/null +++ b/deluge/plugins/Blocklist/deluge_blocklist/gtkui.py @@ -0,0 +1,254 @@ +# +# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import logging +from datetime import datetime + +import gi # isort:skip (Required before Gtk import). + +gi.require_version('Gtk', '3.0') + +# isort:imports-thirdparty +from gi.repository import Gtk + +# isort:imports-firstparty +import deluge.common +import deluge.component as component +from deluge.plugins.pluginbase import Gtk3PluginBase +from deluge.ui.client import client + +# isort:imports-localfolder +from . import common + +log = logging.getLogger(__name__) + + +class GtkUI(Gtk3PluginBase): + def enable(self): + log.debug('Blocklist GtkUI enable..') + self.plugin = component.get('PluginManager') + + self.load_preferences_page() + + self.status_item = component.get('StatusBar').add_item( + image=common.get_resource('blocklist16.png'), + text='', + callback=self._on_status_item_clicked, + tooltip=_('Blocked IP Ranges /Whitelisted IP Ranges'), + ) + + # Register some hooks + self.plugin.register_hook('on_apply_prefs', self._on_apply_prefs) + self.plugin.register_hook('on_show_prefs', self._on_show_prefs) + + def disable(self): + log.debug('Blocklist GtkUI disable..') + + # Remove the preferences page + self.plugin.remove_preferences_page(_('Blocklist')) + + # Remove status item + component.get('StatusBar').remove_item(self.status_item) + del self.status_item + + # Deregister the hooks + self.plugin.deregister_hook('on_apply_prefs', self._on_apply_prefs) + self.plugin.deregister_hook('on_show_prefs', self._on_show_prefs) + + del self.glade + + def update(self): + def _on_get_status(status): + if status['state'] == 'Downloading': + self.table_info.hide() + self.builder.get_object('button_check_download').set_sensitive(False) + self.builder.get_object('button_force_download').set_sensitive(False) + self.builder.get_object('image_up_to_date').hide() + + self.status_item.set_text( + 'Downloading %.2f%%' % (status['file_progress'] * 100) + ) + self.progress_bar.set_text( + 'Downloading %.2f%%' % (status['file_progress'] * 100) + ) + self.progress_bar.set_fraction(status['file_progress']) + self.progress_bar.show() + + elif status['state'] == 'Importing': + self.table_info.hide() + self.builder.get_object('button_check_download').set_sensitive(False) + self.builder.get_object('button_force_download').set_sensitive(False) + self.builder.get_object('image_up_to_date').hide() + + self.status_item.set_text('Importing ' + str(status['num_blocked'])) + self.progress_bar.set_text('Importing %s' % (status['num_blocked'])) + self.progress_bar.pulse() + self.progress_bar.show() + + elif status['state'] == 'Idle': + self.progress_bar.hide() + self.builder.get_object('button_check_download').set_sensitive(True) + self.builder.get_object('button_force_download').set_sensitive(True) + if status['up_to_date']: + self.builder.get_object('image_up_to_date').show() + else: + self.builder.get_object('image_up_to_date').hide() + + self.table_info.show() + self.status_item.set_text('%(num_blocked)s/%(num_whited)s' % status) + + self.builder.get_object('label_filesize').set_text( + deluge.common.fsize(status['file_size']) + ) + self.builder.get_object('label_modified').set_text( + datetime.fromtimestamp(status['file_date']).strftime('%c') + ) + self.builder.get_object('label_type').set_text(status['file_type']) + self.builder.get_object('label_url').set_text(status['file_url']) + + client.blocklist.get_status().addCallback(_on_get_status) + + def _on_show_prefs(self): + def _on_get_config(config): + log.trace('Loaded config: %s', config) + self.builder.get_object('entry_url').set_text(config['url']) + self.builder.get_object('spin_check_days').set_value( + config['check_after_days'] + ) + self.builder.get_object('chk_import_on_start').set_active( + config['load_on_start'] + ) + self.populate_whitelist(config['whitelisted']) + + client.blocklist.get_config().addCallback(_on_get_config) + + def _on_apply_prefs(self): + config = {} + config['url'] = self.builder.get_object('entry_url').get_text().strip() + config['check_after_days'] = self.builder.get_object( + 'spin_check_days' + ).get_value_as_int() + config['load_on_start'] = self.builder.get_object( + 'chk_import_on_start' + ).get_active() + config['whitelisted'] = [ + ip[0] for ip in self.whitelist_model if ip[0] != 'IP HERE' + ] + client.blocklist.set_config(config) + + def _on_button_check_download_clicked(self, widget): + self._on_apply_prefs() + client.blocklist.check_import() + + def _on_button_force_download_clicked(self, widget): + self._on_apply_prefs() + client.blocklist.check_import(force=True) + + def _on_status_item_clicked(self, widget, event): + component.get('Preferences').show(_('Blocklist')) + + def load_preferences_page(self): + """Initializes the preferences page and adds it to the preferences dialog""" + # Load the preferences page + self.builder = Gtk.Builder() + self.builder.add_from_file(common.get_resource('blocklist_pref.ui')) + + self.whitelist_frame = self.builder.get_object('whitelist_frame') + self.progress_bar = self.builder.get_object('progressbar') + self.table_info = self.builder.get_object('table_info') + + # Hide the progress bar initially + self.progress_bar.hide() + self.table_info.show() + + # Create the whitelisted model + self.build_whitelist_model_treeview() + + self.builder.connect_signals( + { + 'on_button_check_download_clicked': self._on_button_check_download_clicked, + 'on_button_force_download_clicked': self._on_button_force_download_clicked, + 'on_whitelist_add_clicked': ( + self.on_add_button_clicked, + self.whitelist_treeview, + ), + 'on_whitelist_remove_clicked': ( + self.on_delete_button_clicked, + self.whitelist_treeview, + ), + } + ) + + # Set button icons + self.builder.get_object('image_download').set_from_file( + common.get_resource('blocklist_download24.png') + ) + + self.builder.get_object('image_import').set_from_file( + common.get_resource('blocklist_import24.png') + ) + + # Update the preferences page with config values from the core + self._on_show_prefs() + + # Add the page to the preferences dialog + self.plugin.add_preferences_page( + _('Blocklist'), self.builder.get_object('blocklist_prefs_box') + ) + + def build_whitelist_model_treeview(self): + self.whitelist_treeview = self.builder.get_object('whitelist_treeview') + treeview_selection = self.whitelist_treeview.get_selection() + treeview_selection.connect( + 'changed', self.on_whitelist_treeview_selection_changed + ) + self.whitelist_model = Gtk.ListStore(str, bool) + renderer = Gtk.CellRendererText() + renderer.connect('edited', self.on_cell_edited, self.whitelist_model) + renderer.ip = 0 + + column = Gtk.TreeViewColumn('IPs', renderer, text=0, editable=1) + column.set_expand(True) + self.whitelist_treeview.append_column(column) + self.whitelist_treeview.set_model(self.whitelist_model) + + def on_cell_edited(self, cell, path_string, new_text, model): + # iter = model.get_iter_from_string(path_string) + # path = model.get_path(iter)[0] + try: + ip = common.IP.parse(new_text) + model.set(model.get_iter_from_string(path_string), 0, ip.address) + except common.BadIP as ex: + model.remove(model.get_iter_from_string(path_string)) + from deluge.ui.gtkui import dialogs + + d = dialogs.ErrorDialog(_('Bad IP address'), ex.message) + d.run() + + def on_whitelist_treeview_selection_changed(self, selection): + model, selected_connection_iter = selection.get_selected() + if selected_connection_iter: + self.builder.get_object('whitelist_delete').set_property('sensitive', True) + else: + self.builder.get_object('whitelist_delete').set_property('sensitive', False) + + def on_add_button_clicked(self, widget, treeview): + model = treeview.get_model() + model.set(model.append(), 0, 'IP HERE', 1, True) + + def on_delete_button_clicked(self, widget, treeview): + selection = treeview.get_selection() + model, selected_iter = selection.get_selected() + if selected_iter: + # path = model.get_path(iter)[0] + model.remove(selected_iter) + + def populate_whitelist(self, whitelist): + self.whitelist_model.clear() + for ip in whitelist: + self.whitelist_model.set(self.whitelist_model.append(), 0, ip, 1, True) diff --git a/deluge/plugins/Blocklist/deluge_blocklist/peerguardian.py b/deluge/plugins/Blocklist/deluge_blocklist/peerguardian.py new file mode 100644 index 0000000..b5fb181 --- /dev/null +++ b/deluge/plugins/Blocklist/deluge_blocklist/peerguardian.py @@ -0,0 +1,66 @@ +# +# Copyright (C) 2007 Steve 'Tarka' Smith (tarka@internode.on.net) +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import gzip +import logging +import socket +from struct import unpack + +log = logging.getLogger(__name__) + + +class PGException(Exception): + pass + + +# Incrementally reads PeerGuardian blocklists v1 and v2. +# See http://wiki.phoenixlabs.org/wiki/P2B_Format +class PGReader: + def __init__(self, filename): + log.debug('PGReader loading: %s', filename) + + try: + with gzip.open(filename, 'rb') as _file: + self.fd = _file + except OSError: + log.debug('Blocklist: PGReader: Incorrect file type or list is corrupt') + + # 4 bytes, should be 0xffffffff + buf = self.fd.read(4) + hdr = unpack('l', buf)[0] + if hdr != -1: + raise PGException(_('Invalid leader') + ' %d' % hdr) + + magic = self.fd.read(3) + if magic != 'P2B': + raise PGException(_('Invalid magic code')) + + buf = self.fd.read(1) + ver = ord(buf) + if ver != 1 and ver != 2: + raise PGException(_('Invalid version') + ' %d' % ver) + + def __next__(self): + # Skip over the string + buf = -1 + while buf != 0: + buf = self.fd.read(1) + if buf == '': # EOF + return False + buf = ord(buf) + + buf = self.fd.read(4) + start = socket.inet_ntoa(buf) + + buf = self.fd.read(4) + end = socket.inet_ntoa(buf) + + return (start, end) + + def close(self): + self.fd.close() diff --git a/deluge/plugins/Blocklist/deluge_blocklist/readers.py b/deluge/plugins/Blocklist/deluge_blocklist/readers.py new file mode 100644 index 0000000..14230ed --- /dev/null +++ b/deluge/plugins/Blocklist/deluge_blocklist/readers.py @@ -0,0 +1,99 @@ +# +# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import logging +import re + +from deluge.common import decode_bytes + +from .common import IP, BadIP, raises_errors_as + +log = logging.getLogger(__name__) + + +class ReaderParseError(Exception): + pass + + +class BaseReader: + """Base reader for blocklist files""" + + def __init__(self, _file): + """Creates a new BaseReader given a file""" + self.file = _file + + def open(self): + """Opens the associated file for reading""" + return open(self.file) + + def parse(self, line): + """Extracts ip range from given line""" + raise NotImplementedError + + def read(self, callback): + """Calls callback on each ip range in the file""" + for start, end in self.readranges(): + try: + callback(IP.parse(start), IP.parse(end)) + except BadIP as ex: + log.error('Failed to parse IP: %s', ex) + return self.file + + def is_ignored(self, line): + """Ignore commented lines and blank lines""" + line = line.strip() + return line.startswith('#') or not line + + def is_valid(self): + """Determines whether file is valid for this reader""" + blocklist = self.open() + valid = True + for line in blocklist: + line = decode_bytes(line) + if not self.is_ignored(line): + try: + (start, end) = self.parse(line) + if not re.match(r'^(\d{1,3}\.){4}$', start + '.') or not re.match( + r'^(\d{1,3}\.){4}$', end + '.' + ): + valid = False + except Exception: + valid = False + break + blocklist.close() + return valid + + @raises_errors_as(ReaderParseError) + def readranges(self): + """Yields each ip range from the file""" + blocklist = self.open() + for line in blocklist: + line = decode_bytes(line) + if not self.is_ignored(line): + yield self.parse(line) + blocklist.close() + + +class EmuleReader(BaseReader): + """Blocklist reader for emule style blocklists""" + + def parse(self, line): + return line.strip().split(' , ')[0].split(' - ') + + +class SafePeerReader(BaseReader): + """Blocklist reader for SafePeer style blocklists""" + + def parse(self, line): + return line.strip().split(':')[-1].split('-') + + +class PeerGuardianReader(SafePeerReader): + """Blocklist reader for PeerGuardian style blocklists""" + + pass diff --git a/deluge/plugins/Blocklist/deluge_blocklist/webui.py b/deluge/plugins/Blocklist/deluge_blocklist/webui.py new file mode 100644 index 0000000..8ba4911 --- /dev/null +++ b/deluge/plugins/Blocklist/deluge_blocklist/webui.py @@ -0,0 +1,27 @@ +# +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import logging + +from deluge.plugins.pluginbase import WebPluginBase + +from .common import get_resource + +log = logging.getLogger(__name__) + +FORMAT_LIST = [ + ('gzmule', _('Emule IP list (GZip)')), + ('spzip', _('SafePeer Text (Zipped)')), + ('pgtext', _('PeerGuardian Text (Uncompressed)')), + ('p2bgz', _('PeerGuardian P2B (GZip)')), +] + + +class WebUI(WebPluginBase): + scripts = [get_resource('blocklist.js')] + debug_scripts = scripts |