2014-11-24 21:05:09 +01:00
|
|
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
|
|
|
|
2016-01-04 07:12:39 +01:00
|
|
|
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
2014-11-24 21:05:09 +01:00
|
|
|
#
|
|
|
|
# This file is part of qutebrowser.
|
|
|
|
#
|
|
|
|
# qutebrowser is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# qutebrowser is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
2015-03-31 20:49:29 +02:00
|
|
|
"""Functions related to ad blocking."""
|
2014-11-24 21:05:09 +01:00
|
|
|
|
|
|
|
import io
|
|
|
|
import os.path
|
|
|
|
import functools
|
|
|
|
import posixpath
|
|
|
|
import zipfile
|
2015-09-17 06:46:27 +02:00
|
|
|
import fnmatch
|
2014-11-24 21:05:09 +01:00
|
|
|
|
|
|
|
from qutebrowser.config import config
|
2014-11-24 21:07:03 +01:00
|
|
|
from qutebrowser.utils import objreg, standarddir, log, message
|
2015-05-16 22:12:27 +02:00
|
|
|
from qutebrowser.commands import cmdutils, cmdexc
|
2014-11-24 21:05:09 +01:00
|
|
|
|
|
|
|
|
2014-12-02 12:26:13 +01:00
|
|
|
def guess_zip_filename(zf):
|
|
|
|
"""Guess which file to use inside a zip file.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
zf: A ZipFile instance.
|
|
|
|
"""
|
|
|
|
files = zf.namelist()
|
|
|
|
if len(files) == 1:
|
|
|
|
return files[0]
|
|
|
|
else:
|
|
|
|
for e in files:
|
|
|
|
if posixpath.splitext(e)[0].lower() == 'hosts':
|
|
|
|
return e
|
|
|
|
raise FileNotFoundError("No hosts file found in zip")
|
|
|
|
|
2014-12-04 21:35:32 +01:00
|
|
|
|
2014-12-02 12:26:13 +01:00
|
|
|
def get_fileobj(byte_io):
|
2016-07-05 08:34:03 +02:00
|
|
|
"""Get a usable file object to read the hosts file from."""
|
2014-12-02 12:26:13 +01:00
|
|
|
byte_io.seek(0) # rewind downloaded file
|
|
|
|
if zipfile.is_zipfile(byte_io):
|
|
|
|
byte_io.seek(0) # rewind what zipfile.is_zipfile did
|
|
|
|
zf = zipfile.ZipFile(byte_io)
|
|
|
|
filename = guess_zip_filename(zf)
|
|
|
|
byte_io = zf.open(filename, mode='r')
|
|
|
|
else:
|
|
|
|
byte_io.seek(0) # rewind what zipfile.is_zipfile did
|
|
|
|
return io.TextIOWrapper(byte_io, encoding='utf-8')
|
|
|
|
|
|
|
|
|
2015-09-17 06:46:27 +02:00
|
|
|
def is_whitelisted_host(host):
|
|
|
|
"""Check if the given host is on the adblock whitelist.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
host: The host of the request as string.
|
|
|
|
"""
|
|
|
|
whitelist = config.get('content', 'host-blocking-whitelist')
|
|
|
|
if whitelist is None:
|
|
|
|
return False
|
|
|
|
|
|
|
|
for pattern in whitelist:
|
|
|
|
if fnmatch.fnmatch(host, pattern.lower()):
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2014-11-24 21:05:09 +01:00
|
|
|
class FakeDownload:
|
|
|
|
|
|
|
|
"""A download stub to use on_download_finished with local files."""
|
|
|
|
|
|
|
|
def __init__(self, fileobj):
|
|
|
|
self.basename = os.path.basename(fileobj.name)
|
|
|
|
self.fileobj = fileobj
|
|
|
|
self.successful = True
|
|
|
|
|
|
|
|
|
|
|
|
class HostBlocker:
|
|
|
|
|
|
|
|
"""Manage blocked hosts based from /etc/hosts-like files.
|
|
|
|
|
|
|
|
Attributes:
|
2015-09-17 06:46:27 +02:00
|
|
|
_blocked_hosts: A set of blocked hosts.
|
2016-03-29 06:59:23 +02:00
|
|
|
_config_blocked_hosts: A set of blocked hosts from ~/.config.
|
2014-11-24 21:05:09 +01:00
|
|
|
_in_progress: The DownloadItems which are currently downloading.
|
|
|
|
_done_count: How many files have been read successfully.
|
2016-03-29 06:59:23 +02:00
|
|
|
_local_hosts_file: The path to the blocked-hosts file.
|
|
|
|
_config_hosts_file: The path to a blocked-hosts in ~/.config
|
2014-11-24 21:05:09 +01:00
|
|
|
|
|
|
|
Class attributes:
|
|
|
|
WHITELISTED: Hosts which never should be blocked.
|
|
|
|
"""
|
|
|
|
|
|
|
|
WHITELISTED = ('localhost', 'localhost.localdomain', 'broadcasthost',
|
|
|
|
'local')
|
|
|
|
|
|
|
|
def __init__(self):
|
2015-09-17 06:46:27 +02:00
|
|
|
self._blocked_hosts = set()
|
2016-03-29 06:59:23 +02:00
|
|
|
self._config_blocked_hosts = set()
|
2014-11-24 21:05:09 +01:00
|
|
|
self._in_progress = []
|
|
|
|
self._done_count = 0
|
2016-03-29 06:59:23 +02:00
|
|
|
|
2015-05-16 22:12:27 +02:00
|
|
|
data_dir = standarddir.data()
|
|
|
|
if data_dir is None:
|
2016-03-29 06:59:23 +02:00
|
|
|
self._local_hosts_file = None
|
|
|
|
else:
|
|
|
|
self._local_hosts_file = os.path.join(data_dir, 'blocked-hosts')
|
2016-05-07 23:35:30 +02:00
|
|
|
self.on_config_changed()
|
2016-03-29 06:59:23 +02:00
|
|
|
|
|
|
|
config_dir = standarddir.config()
|
|
|
|
if config_dir is None:
|
|
|
|
self._config_hosts_file = None
|
2015-05-16 22:12:27 +02:00
|
|
|
else:
|
2016-03-29 06:59:23 +02:00
|
|
|
self._config_hosts_file = os.path.join(config_dir, 'blocked-hosts')
|
|
|
|
|
2014-11-24 21:05:09 +01:00
|
|
|
objreg.get('config').changed.connect(self.on_config_changed)
|
|
|
|
|
2015-09-17 06:46:27 +02:00
|
|
|
def is_blocked(self, url):
|
|
|
|
"""Check if the given URL (as QUrl) is blocked."""
|
|
|
|
if not config.get('content', 'host-blocking-enabled'):
|
|
|
|
return False
|
|
|
|
host = url.host()
|
2016-03-29 06:59:23 +02:00
|
|
|
return ((host in self._blocked_hosts or
|
|
|
|
host in self._config_blocked_hosts) and
|
|
|
|
not is_whitelisted_host(host))
|
|
|
|
|
|
|
|
def _read_hosts_file(self, filename, target):
|
|
|
|
"""Read hosts from the given filename.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
filename: The file to read.
|
|
|
|
target: The set to store the hosts in.
|
|
|
|
|
|
|
|
Return:
|
|
|
|
True if a read was attempted, False otherwise
|
|
|
|
"""
|
2016-03-29 07:43:11 +02:00
|
|
|
if filename is None or not os.path.exists(filename):
|
2016-03-29 06:59:23 +02:00
|
|
|
return False
|
|
|
|
|
|
|
|
try:
|
|
|
|
with open(filename, 'r', encoding='utf-8') as f:
|
|
|
|
for line in f:
|
|
|
|
target.add(line.strip())
|
|
|
|
except OSError:
|
|
|
|
log.misc.exception("Failed to read host blocklist!")
|
|
|
|
|
|
|
|
return True
|
2015-09-17 06:46:27 +02:00
|
|
|
|
2014-11-24 21:05:09 +01:00
|
|
|
def read_hosts(self):
|
|
|
|
"""Read hosts from the existing blocked-hosts file."""
|
2015-09-17 06:46:27 +02:00
|
|
|
self._blocked_hosts = set()
|
2016-03-29 06:59:23 +02:00
|
|
|
|
|
|
|
if self._local_hosts_file is None:
|
2015-05-16 22:12:27 +02:00
|
|
|
return
|
2016-03-29 06:59:23 +02:00
|
|
|
|
|
|
|
self._read_hosts_file(self._config_hosts_file,
|
|
|
|
self._config_blocked_hosts)
|
|
|
|
|
|
|
|
found = self._read_hosts_file(self._local_hosts_file,
|
|
|
|
self._blocked_hosts)
|
|
|
|
|
|
|
|
if not found:
|
2015-05-17 01:09:33 +02:00
|
|
|
args = objreg.get('args')
|
|
|
|
if (config.get('content', 'host-block-lists') is not None and
|
|
|
|
args.basedir is None):
|
2015-03-22 22:39:56 +01:00
|
|
|
message.info('current',
|
|
|
|
"Run :adblock-update to get adblock lists.")
|
2014-11-24 21:05:09 +01:00
|
|
|
|
2016-05-10 19:51:11 +02:00
|
|
|
@cmdutils.register(instance='host-blocker')
|
|
|
|
@cmdutils.argument('win_id', win_id=True)
|
2015-04-20 19:29:29 +02:00
|
|
|
def adblock_update(self, win_id):
|
2016-03-29 06:59:23 +02:00
|
|
|
"""Update the adblock block lists.
|
|
|
|
|
|
|
|
This updates ~/.local/share/qutebrowser/blocked-hosts with downloaded
|
|
|
|
host lists and re-reads ~/.config/qutebrowser/blocked-hosts.
|
|
|
|
"""
|
|
|
|
self._read_hosts_file(self._config_hosts_file,
|
|
|
|
self._config_blocked_hosts)
|
|
|
|
if self._local_hosts_file is None:
|
2015-05-16 22:12:27 +02:00
|
|
|
raise cmdexc.CommandError("No data storage is configured!")
|
2015-09-17 06:46:27 +02:00
|
|
|
self._blocked_hosts = set()
|
2014-11-24 21:05:09 +01:00
|
|
|
self._done_count = 0
|
2014-11-26 21:16:27 +01:00
|
|
|
urls = config.get('content', 'host-block-lists')
|
2014-11-24 21:05:09 +01:00
|
|
|
download_manager = objreg.get('download-manager', scope='window',
|
|
|
|
window='last-focused')
|
|
|
|
if urls is None:
|
|
|
|
return
|
|
|
|
for url in urls:
|
|
|
|
if url.scheme() == 'file':
|
2014-12-10 18:00:49 +01:00
|
|
|
try:
|
|
|
|
fileobj = open(url.path(), 'rb')
|
2015-01-09 06:53:00 +01:00
|
|
|
except OSError as e:
|
|
|
|
message.error(win_id, "adblock: Error while reading {}: "
|
|
|
|
"{}".format(url.path(), e.strerror))
|
|
|
|
continue
|
2014-11-24 21:05:09 +01:00
|
|
|
download = FakeDownload(fileobj)
|
|
|
|
self._in_progress.append(download)
|
|
|
|
self.on_download_finished(download)
|
|
|
|
else:
|
|
|
|
fobj = io.BytesIO()
|
|
|
|
fobj.name = 'adblock: ' + url.host()
|
2014-12-16 13:44:09 +01:00
|
|
|
download = download_manager.get(url, fileobj=fobj,
|
|
|
|
auto_remove=True)
|
2014-11-24 21:05:09 +01:00
|
|
|
self._in_progress.append(download)
|
|
|
|
download.finished.connect(
|
|
|
|
functools.partial(self.on_download_finished, download))
|
|
|
|
|
|
|
|
def _merge_file(self, byte_io):
|
|
|
|
"""Read and merge host files.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
byte_io: The BytesIO object of the completed download.
|
|
|
|
|
|
|
|
Return:
|
|
|
|
A set of the merged hosts.
|
|
|
|
"""
|
|
|
|
error_count = 0
|
|
|
|
line_count = 0
|
|
|
|
try:
|
2014-12-02 12:26:13 +01:00
|
|
|
f = get_fileobj(byte_io)
|
2014-12-10 18:00:49 +01:00
|
|
|
except (OSError, UnicodeDecodeError, zipfile.BadZipFile,
|
2014-11-24 21:05:09 +01:00
|
|
|
zipfile.LargeZipFile) as e:
|
2015-03-22 22:39:56 +01:00
|
|
|
message.error('current', "adblock: Error while reading {}: {} - "
|
|
|
|
"{}".format(byte_io.name, e.__class__.__name__, e))
|
2014-11-24 21:05:09 +01:00
|
|
|
return
|
|
|
|
for line in f:
|
|
|
|
line_count += 1
|
|
|
|
# Remove comments
|
|
|
|
try:
|
|
|
|
hash_idx = line.index('#')
|
|
|
|
line = line[:hash_idx]
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
line = line.strip()
|
|
|
|
# Skip empty lines
|
|
|
|
if not line:
|
|
|
|
continue
|
|
|
|
parts = line.split()
|
|
|
|
if len(parts) == 1:
|
|
|
|
# "one host per line" format
|
|
|
|
host = parts[0]
|
|
|
|
elif len(parts) == 2:
|
|
|
|
# /etc/hosts format
|
|
|
|
host = parts[1]
|
|
|
|
else:
|
|
|
|
error_count += 1
|
|
|
|
continue
|
|
|
|
if host not in self.WHITELISTED:
|
2015-09-17 06:46:27 +02:00
|
|
|
self._blocked_hosts.add(host)
|
2014-11-24 21:05:09 +01:00
|
|
|
log.misc.debug("{}: read {} lines".format(byte_io.name, line_count))
|
|
|
|
if error_count > 0:
|
2015-03-22 22:39:56 +01:00
|
|
|
message.error('current', "adblock: {} read errors for {}".format(
|
|
|
|
error_count, byte_io.name))
|
2014-11-24 21:05:09 +01:00
|
|
|
|
|
|
|
def on_lists_downloaded(self):
|
|
|
|
"""Install block lists after files have been downloaded."""
|
2016-03-29 06:59:23 +02:00
|
|
|
with open(self._local_hosts_file, 'w', encoding='utf-8') as f:
|
2015-09-17 06:46:27 +02:00
|
|
|
for host in sorted(self._blocked_hosts):
|
2014-11-24 21:05:09 +01:00
|
|
|
f.write(host + '\n')
|
2015-03-22 22:39:56 +01:00
|
|
|
message.info('current', "adblock: Read {} hosts from {} sources."
|
2015-09-17 06:46:27 +02:00
|
|
|
.format(len(self._blocked_hosts), self._done_count))
|
2014-11-24 21:05:09 +01:00
|
|
|
|
2014-11-26 21:16:27 +01:00
|
|
|
@config.change_filter('content', 'host-block-lists')
|
2014-11-24 21:05:09 +01:00
|
|
|
def on_config_changed(self):
|
|
|
|
"""Update files when the config changed."""
|
2014-11-26 21:16:27 +01:00
|
|
|
urls = config.get('content', 'host-block-lists')
|
2016-05-07 23:35:30 +02:00
|
|
|
if urls is None and self._local_hosts_file is not None:
|
2014-11-24 21:05:09 +01:00
|
|
|
try:
|
2016-03-29 06:59:23 +02:00
|
|
|
os.remove(self._local_hosts_file)
|
2016-05-07 23:30:32 +02:00
|
|
|
except FileNotFoundError:
|
|
|
|
pass
|
|
|
|
except OSError as e:
|
|
|
|
log.misc.exception("Failed to delete hosts file: {}".format(e))
|
2014-11-24 21:05:09 +01:00
|
|
|
|
|
|
|
def on_download_finished(self, download):
|
|
|
|
"""Check if all downloads are finished and if so, trigger reading.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
download: The finished DownloadItem.
|
|
|
|
"""
|
|
|
|
self._in_progress.remove(download)
|
|
|
|
if download.successful:
|
|
|
|
self._done_count += 1
|
|
|
|
try:
|
|
|
|
self._merge_file(download.fileobj)
|
|
|
|
finally:
|
|
|
|
download.fileobj.close()
|
|
|
|
if not self._in_progress:
|
2014-12-10 18:00:49 +01:00
|
|
|
try:
|
|
|
|
self.on_lists_downloaded()
|
|
|
|
except OSError:
|
|
|
|
log.misc.exception("Failed to write host block list!")
|