aboutsummaryrefslogtreecommitdiff
path: root/plugins/HostlistFilter
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/HostlistFilter')
-rw-r--r--plugins/HostlistFilter/corpus/apple.txt1
-rw-r--r--plugins/HostlistFilter/corpus/banana.txt1
-rw-r--r--plugins/HostlistFilter/corpus/kiwi.txt1
-rw-r--r--plugins/HostlistFilter/corpus/orange.txt1
-rw-r--r--plugins/HostlistFilter/filterlist.cpp58
-rw-r--r--plugins/HostlistFilter/filterlist.h58
-rw-r--r--plugins/HostlistFilter/meson.build43
-rw-r--r--plugins/HostlistFilter/plugin/plugin.cpp32
-rw-r--r--plugins/HostlistFilter/plugin/plugin.h28
-rw-r--r--plugins/HostlistFilter/plugin/smolblokHostlistPlugin.json4
-rw-r--r--plugins/HostlistFilter/test/filterlist.cpp29
-rw-r--r--plugins/HostlistFilter/test/hostlist.txt6
-rw-r--r--plugins/HostlistFilter/test/plugin.cpp27
-rw-r--r--plugins/HostlistFilter/test/rule.cpp57
14 files changed, 346 insertions, 0 deletions
diff --git a/plugins/HostlistFilter/corpus/apple.txt b/plugins/HostlistFilter/corpus/apple.txt
new file mode 100644
index 0000000..3a8973b
--- /dev/null
+++ b/plugins/HostlistFilter/corpus/apple.txt
@@ -0,0 +1 @@
+127.0.0.1 localhost.localdomain
diff --git a/plugins/HostlistFilter/corpus/banana.txt b/plugins/HostlistFilter/corpus/banana.txt
new file mode 100644
index 0000000..c30aa84
--- /dev/null
+++ b/plugins/HostlistFilter/corpus/banana.txt
@@ -0,0 +1 @@
+0.0.0.0 blockeddomain.com
diff --git a/plugins/HostlistFilter/corpus/kiwi.txt b/plugins/HostlistFilter/corpus/kiwi.txt
new file mode 100644
index 0000000..77c325c
--- /dev/null
+++ b/plugins/HostlistFilter/corpus/kiwi.txt
@@ -0,0 +1 @@
+# This is a comment, and after it comes a blank line
diff --git a/plugins/HostlistFilter/corpus/orange.txt b/plugins/HostlistFilter/corpus/orange.txt
new file mode 100644
index 0000000..583273d
--- /dev/null
+++ b/plugins/HostlistFilter/corpus/orange.txt
@@ -0,0 +1 @@
+0.0.0.0 blockeddomain.first blockeddomain.second
diff --git a/plugins/HostlistFilter/filterlist.cpp b/plugins/HostlistFilter/filterlist.cpp
new file mode 100644
index 0000000..a0fd414
--- /dev/null
+++ b/plugins/HostlistFilter/filterlist.cpp
@@ -0,0 +1,58 @@
+/*
+ * This file is part of smolbote. It's copyrighted by the contributors recorded
+ * in the version control history of the file, available from its original
+ * location: https://library.iserlohn-fortress.net/aqua/smolbote.git
+ *
+ * SPDX-License-Identifier: GPL-3.0
+ */
+
+#include "filterlist.h"
+#include <QIODevice>
+#include <QTextStream>
+
+using namespace Hostlist;
+
+#ifdef FUZZER
+extern "C" int LLVMFuzzerTestOneInput(const char *Data, long long Size)
+{
+ Filterlist::parseRule(QString::fromLatin1(Data, Size));
+ return 0;
+}
+#endif
+
+std::map<Filterlist::DomainHash, Filterlist::Rule> Filterlist::parseRule(const QString &line)
+{
+ if(line.isEmpty() || line.at(0) == '#') {
+ return {};
+ }
+
+ auto parts = line.trimmed().split(' ');
+ if(parts.size() < 2) {
+ return {};
+ }
+
+ const auto redirect = (parts[0] == "0.0.0.0") ? QString() : parts[0];
+
+ std::map<DomainHash, Rule> r;
+ for(int i = 1; i < parts.size(); ++i) {
+ r.emplace(qHash(parts[i], 0), Filterlist::Rule{ parts[i], redirect });
+ }
+ return r;
+}
+
+bool Filterlist::load(QIODevice &from)
+{
+ if(!from.isReadable() || !from.isTextModeEnabled()) {
+ return false;
+ }
+
+ while(from.bytesAvailable() > 0) {
+ const auto line = from.readLine(512).trimmed();
+ auto r = parseRule(line);
+ if(!r.empty()) {
+ qDebug("merging in %lu rules", r.size());
+ rules.merge(r);
+ }
+ }
+ return true;
+}
diff --git a/plugins/HostlistFilter/filterlist.h b/plugins/HostlistFilter/filterlist.h
new file mode 100644
index 0000000..7301f20
--- /dev/null
+++ b/plugins/HostlistFilter/filterlist.h
@@ -0,0 +1,58 @@
+/*
+ * This file is part of smolbote. It's copyrighted by the contributors recorded
+ * in the version control history of the file, available from its original
+ * location: https://library.iserlohn-fortress.net/aqua/smolbote.git
+ *
+ * SPDX-License-Identifier: GPL-3.0
+ */
+
+#pragma once
+
+#include <map>
+#include <smolbote/filterinterface.hpp>
+
+namespace Hostlist
+{
+
+class Filterlist final : public FilterList
+{
+public:
+ typedef uint DomainHash;
+ struct Rule {
+ QString domain;
+ QString redirect;
+ };
+
+ Filterlist() = default;
+ ~Filterlist() = default;
+
+ [[nodiscard]] bool findMatch(const QString &domain) const
+ {
+ const auto hash = qHash(domain, 0);
+ const auto found = rules.find(hash);
+ if(found != rules.end()) {
+ return true;
+ }
+ return false;
+ }
+ int count() const
+ {
+ return rules.size();
+ }
+
+ [[nodiscard]] bool filter(QWebEngineUrlRequestInfo &info) const
+ {
+ return false;
+ }
+ [[nodiscard]] bool isUpToDate() const
+ {
+ return true;
+ }
+
+ bool load(QIODevice &device);
+ [[nodiscard]] static std::map<DomainHash, Rule> parseRule(const QString &line);
+
+private:
+ std::map<DomainHash, Rule> rules;
+};
+} // namespace Hostlist
diff --git a/plugins/HostlistFilter/meson.build b/plugins/HostlistFilter/meson.build
new file mode 100644
index 0000000..1562524
--- /dev/null
+++ b/plugins/HostlistFilter/meson.build
@@ -0,0 +1,43 @@
+dep_hostlistfilter = declare_dependency(
+ include_directories: [ '.', smolbote_interfaces ],
+ link_with: static_library('hostlistfilter', [ 'filterlist.cpp' ], include_directories: smolbote_interfaces, dependencies: [dep_qt5])
+)
+
+# plugin
+plugin = shared_library('smolblokHostlistPlugin',
+ [ 'plugin/plugin.cpp', mod_qt5.preprocess(include_directories: smolbote_interfaces, moc_headers: 'plugin/plugin.h', dependencies: dep_qt5) ],
+ include_directories: smolbote_interfaces,
+ dependencies: [ dep_hostlistfilter, dep_qt5 ],
+ install: true,
+ install_dir: get_option('libdir')/'smolbote/plugins'
+)
+
+# tests
+test('rule', executable('rule', sources: 'test/rule.cpp', dependencies: [dep_qt5, dep_catch, dep_hostlistfilter]), suite: 'hostlist')
+
+test('filterlist', executable('filterlist',
+ sources: 'test/filterlist.cpp',
+ dependencies: [dep_qt5, dep_catch, dep_hostlistfilter]),
+ env: 'HOSTLIST_TXT='+meson.current_source_dir()/'test/hostlist.txt',
+ suite: 'hostlist'
+)
+test('plugin', executable('filterlist-plugin',
+ sources: [ 'test/plugin.cpp', 'plugin/plugin.cpp',
+ mod_qt5.preprocess(include_directories: smolbote_interfaces, moc_headers: 'plugin/plugin.h', dependencies: dep_qt5) ],
+ dependencies: [dep_qt5, dep_catch, dep_hostlistfilter]),
+ env: 'HOSTLIST_TXT='+meson.current_source_dir()/'test/hostlist.txt',
+ suite: 'hostlist'
+)
+
+test('smolblok-load', smolblok_load, workdir: meson.build_root(), args: plugin.full_path(), suite: 'hostlist')
+
+# fuzzer
+if meson.get_compiler('cpp').has_multi_arguments('-g', '-fsanitize=fuzzer')
+executable('hostlist-fuzzer',
+ sources: 'filterlist.cpp',
+ include_directories: smolbote_interfaces,
+ dependencies: dep_qt5,
+ cpp_args: [ '-g', '-fsanitize=fuzzer', '-DFUZZER' ],
+ link_args: [ '-fsanitize=fuzzer' ]
+)
+endif
diff --git a/plugins/HostlistFilter/plugin/plugin.cpp b/plugins/HostlistFilter/plugin/plugin.cpp
new file mode 100644
index 0000000..28a7706
--- /dev/null
+++ b/plugins/HostlistFilter/plugin/plugin.cpp
@@ -0,0 +1,32 @@
+/*
+ * This file is part of smolbote. It's copyrighted by the contributors recorded
+ * in the version control history of the file, available from its original
+ * location: https://library.iserlohn-fortress.net/aqua/smolbote.git
+ *
+ * SPDX-License-Identifier: GPL-3.0
+ */
+
+#include "plugin.h"
+#include "filterlist.h"
+
+FilterList* HostlistFilterPlugin::load(QIODevice &from) const
+{
+ if(!from.isOpen())
+ return nullptr;
+
+ auto *list = new Hostlist::Filterlist;
+ return list;
+}
+
+bool HostlistFilterPlugin::parse(FilterList *list, QIODevice &from) const
+{
+ if(list == nullptr || !from.isOpen()) {
+ return false;
+ }
+ auto *l = dynamic_cast<Hostlist::Filterlist*>(list);
+ if(l == nullptr) {
+ return false;
+ }
+ return l->load(from);
+}
+
diff --git a/plugins/HostlistFilter/plugin/plugin.h b/plugins/HostlistFilter/plugin/plugin.h
new file mode 100644
index 0000000..53b5d36
--- /dev/null
+++ b/plugins/HostlistFilter/plugin/plugin.h
@@ -0,0 +1,28 @@
+/*
+ * This file is part of smolbote. It's copyrighted by the contributors recorded
+ * in the version control history of the file, available from its original
+ * location: https://library.iserlohn-fortress.net/aqua/smolbote.git
+ *
+ * SPDX-License-Identifier: GPL-3.0
+ */
+
+#ifndef SMOLBLOK_FILTER_PLUGIN_H
+#define SMOLBLOK_FILTER_PLUGIN_H
+
+#include <smolbote/filterinterface.hpp>
+
+class HostlistFilterPlugin : public QObject, public FilterPlugin
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID FilterPluginIid FILE "smolblokHostlistPlugin.json")
+ Q_INTERFACES(FilterPlugin)
+
+public:
+ ~HostlistFilterPlugin() = default;
+
+ FilterList *load(QIODevice &from) const override;
+ bool parse(FilterList *list, QIODevice &from) const override;
+};
+
+#endif // SMOLBLOK_FILTER_PLUGIN_H
+
diff --git a/plugins/HostlistFilter/plugin/smolblokHostlistPlugin.json b/plugins/HostlistFilter/plugin/smolblokHostlistPlugin.json
new file mode 100644
index 0000000..aa53cdd
--- /dev/null
+++ b/plugins/HostlistFilter/plugin/smolblokHostlistPlugin.json
@@ -0,0 +1,4 @@
+{
+ "name": "smolblok Hostlist filter plugin",
+ "author": "Aqua <aqua@iserlohn-fortress.net>"
+}
diff --git a/plugins/HostlistFilter/test/filterlist.cpp b/plugins/HostlistFilter/test/filterlist.cpp
new file mode 100644
index 0000000..4aa532b
--- /dev/null
+++ b/plugins/HostlistFilter/test/filterlist.cpp
@@ -0,0 +1,29 @@
+#define CATCH_CONFIG_MAIN
+#include "filterlist.h"
+#include <QFile>
+#include <catch2/catch.hpp>
+
+using namespace Hostlist;
+
+TEST_CASE("Hostlist")
+{
+ Filterlist list;
+
+ const QString filename(qgetenv("HOSTLIST_TXT"));
+ REQUIRE(!filename.isEmpty());
+
+ QFile f(filename);
+ REQUIRE(f.open(QIODevice::ReadOnly | QIODevice::Text));
+
+ REQUIRE(list.load(f));
+ f.close();
+
+ REQUIRE(list.count() == 4);
+
+ REQUIRE(list.findMatch("blockeddomain.first"));
+ REQUIRE(list.findMatch("blockeddomain.second"));
+
+ REQUIRE(list.findMatch("localhost.localdomain"));
+
+ REQUIRE(!list.findMatch("other.domain"));
+}
diff --git a/plugins/HostlistFilter/test/hostlist.txt b/plugins/HostlistFilter/test/hostlist.txt
new file mode 100644
index 0000000..a0b4e5c
--- /dev/null
+++ b/plugins/HostlistFilter/test/hostlist.txt
@@ -0,0 +1,6 @@
+# This is a comment, and after it comes a blank line
+
+127.0.0.1 localhost.localdomain
+
+0.0.0.0 blockeddomain.com
+0.0.0.0 blockeddomain.first blockeddomain.second
diff --git a/plugins/HostlistFilter/test/plugin.cpp b/plugins/HostlistFilter/test/plugin.cpp
new file mode 100644
index 0000000..fad34f2
--- /dev/null
+++ b/plugins/HostlistFilter/test/plugin.cpp
@@ -0,0 +1,27 @@
+#define CATCH_CONFIG_MAIN
+#include "plugin/plugin.h"
+#include <QFile>
+#include <catch2/catch.hpp>
+
+TEST_CASE("Hostlist")
+{
+ HostlistFilterPlugin plugin;
+
+ const QString filename(qgetenv("HOSTLIST_TXT"));
+ REQUIRE(!filename.isEmpty());
+ QFile f(filename);
+
+ // shouldn't be able to load an unopened QIODevice
+ REQUIRE(plugin.load(f) == nullptr);
+
+ REQUIRE(f.open(QIODevice::ReadOnly | QIODevice::Text));
+
+ auto *list = plugin.load(f);
+ REQUIRE(list != nullptr);
+ f.seek(0);
+ REQUIRE_FALSE(plugin.parse(nullptr, f));
+ REQUIRE(plugin.parse(list, f));
+ f.close();
+ REQUIRE_FALSE(plugin.parse(list, f));
+}
+
diff --git a/plugins/HostlistFilter/test/rule.cpp b/plugins/HostlistFilter/test/rule.cpp
new file mode 100644
index 0000000..b5ba6e0
--- /dev/null
+++ b/plugins/HostlistFilter/test/rule.cpp
@@ -0,0 +1,57 @@
+#define CATCH_CONFIG_MAIN
+#include "filterlist.h"
+#include <catch2/catch.hpp>
+
+using namespace Hostlist;
+
+SCENARIO("Hostlist::Rule")
+{
+ GIVEN("an invalid rule")
+ {
+ const auto rule = Filterlist::parseRule("0.0.0.0 ");
+ REQUIRE(rule.empty());
+ }
+ GIVEN("127.0.0.1 localhost.localdomain")
+ {
+ auto rule = Filterlist::parseRule("127.0.0.1 localhost.localdomain");
+
+ REQUIRE(!rule.empty());
+ REQUIRE(rule.size() == 1);
+
+ // note: you need to force it to hash a string, rather than the address itself
+ const auto index = qHash(QString("localhost.localdomain"), 0);
+ REQUIRE(rule[index].domain == "localhost.localdomain");
+ REQUIRE(rule[index].redirect == "127.0.0.1");
+ }
+
+ GIVEN("0.0.0.0 blockeddomain.com")
+ {
+ auto rule = Filterlist::parseRule("0.0.0.0 blockeddomain.com");
+
+ REQUIRE(!rule.empty());
+ REQUIRE(rule.size() == 1);
+
+ const auto index = qHash(QString("blockeddomain.com"), 0);
+ REQUIRE(rule[index].domain == "blockeddomain.com");
+ REQUIRE(rule[index].redirect.isEmpty());
+ ;
+ }
+
+ GIVEN("0.0.0.0 blockeddomain.first blockeddomain.second")
+ {
+ auto rule = Filterlist::parseRule("0.0.0.0 blockeddomain.first blockeddomain.second");
+
+ REQUIRE(!rule.empty());
+ REQUIRE(rule.size() == 2);
+ {
+ const auto index = qHash(QString("blockeddomain.first"), 0);
+ REQUIRE(rule[index].domain == "blockeddomain.first");
+ REQUIRE(rule[index].redirect.isEmpty());
+ }
+ {
+ const auto index = qHash(QString("blockeddomain.second"), 0);
+ REQUIRE(rule[index].domain == "blockeddomain.second");
+ REQUIRE(rule[index].redirect.isEmpty());
+ }
+ }
+}