From 3df1bc75ce24019a4b8891b9cf95b058dbff16c1 Mon Sep 17 00:00:00 2001 From: Martin Rotter Date: Fri, 7 May 2021 06:56:21 +0200 Subject: [PATCH] save --- .../desktop/com.github.rssguard.appdata.xml | 2 +- resources/rssguard.qrc | 1 + resources/scripts/adblock/adblock-server.js | 144 +++++++++-------- src/librssguard/definitions/definitions.h | 5 +- src/librssguard/miscellaneous/application.cpp | 5 +- src/librssguard/miscellaneous/skinfactory.cpp | 5 +- src/librssguard/miscellaneous/skinfactory.h | 2 +- .../network-web/adblock/adblockmanager.cpp | 153 ++++++++++++++++-- .../network-web/adblock/adblockmanager.h | 16 +- .../adblock/adblockurlinterceptor.cpp | 2 +- src/librssguard/network-web/webpage.cpp | 4 +- 11 files changed, 248 insertions(+), 91 deletions(-) diff --git a/resources/desktop/com.github.rssguard.appdata.xml b/resources/desktop/com.github.rssguard.appdata.xml index 652be1387..27d94511c 100644 --- a/resources/desktop/com.github.rssguard.appdata.xml +++ b/resources/desktop/com.github.rssguard.appdata.xml @@ -30,7 +30,7 @@ https://martinrotter.github.io/donate/ - + none diff --git a/resources/rssguard.qrc b/resources/rssguard.qrc index 664f365ca..12f0ff705 100755 --- a/resources/rssguard.qrc +++ b/resources/rssguard.qrc @@ -6,6 +6,7 @@ text/COPYING_GNU_GPL text/COPYING_GNU_GPL_HTML + scripts/adblock/adblock-server.js scripts/public_suffix_list.dat graphics/rssguard.ico diff --git a/resources/scripts/adblock/adblock-server.js b/resources/scripts/adblock/adblock-server.js index 98eaa3852..cc6ec728e 100755 --- a/resources/scripts/adblock/adblock-server.js +++ b/resources/scripts/adblock/adblock-server.js @@ -3,7 +3,7 @@ // How to install: // npm i -g @cliqz/adblocker // npm i -g concat-stream -// npm i -g psl +// npm i -g tldts-experimental // npm i -g node-fetch // // How to run: @@ -19,84 +19,100 @@ // }' 'http://localhost:' const fs = require('fs'); -const psl = require('psl'); +const tldts = require('tldts-experimental'); const adblock = require('@cliqz/adblocker') const http = require('http'); const concat = require('concat-stream'); const constants = require('node:http2'); const fetch = require("node-fetch"); +const cluster = require('cluster'); +const numCPUs = require('os').cpus().length; const port = process.argv[2]; const filtersFile = process.argv[3]; const engine = adblock.FiltersEngine.parse(fs.readFileSync(filtersFile, 'utf-8')); const hostname = '127.0.0.1'; -const server = http.createServer((req, res) => { - try { - console.log(new Date()); +if (cluster.isPrimary) { + console.log(`Primary ${process.pid} is running`); - const chunks = []; - req.on('data', chunk => chunks.push(chunk)); - req.on('end', () => { + // Fork workers. + for (let i = 0; i < numCPUs; i++) { + cluster.fork(); + } + + cluster.on('exit', (worker, code, signal) => { + console.log(`worker ${worker.process.pid} died`); + }); +} +else { + const server = http.createServer((req, res) => { + try { console.log(new Date()); - try { - const jsonData = Buffer.concat(chunks); - const jsonStruct = JSON.parse(jsonData.toString()); - - const askUrl = jsonStruct['url']; - const askFilter = jsonStruct['filter']; - const askCosmetic = jsonStruct['cosmetic']; - const askUrlType = jsonStruct['url_type']; - const fullUrl = new URL(askUrl); - - resultJson = {}; - - if (askFilter) { - const adblockMatch = engine.match(adblock.Request.fromRawDetails({ - type: askUrlType, - url: askUrl, - })); - - resultJson["filter"] = adblockMatch; - console.log(`adblocker: Filter is:\n${JSON.stringify(adblockMatch)}.`) - } - - if (askCosmetic) { - const adblockCosmetic = engine.getCosmeticsFilters({ - url: askUrl, - hostname: fullUrl.hostname, - domain: psl.parse(fullUrl.hostname).domain - }); - - resultJson["cosmetic"] = adblockCosmetic; - console.log(`adblocker: Cosmetic is:\n${JSON.stringify(adblockCosmetic)}.`) - } - - res.statusCode = 200; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify(resultJson)); - + const chunks = []; + req.on('data', chunk => chunks.push(chunk)); + req.on('end', () => { console.log(new Date()); - } - catch (inner_error) { - console.error(`adblocker: ${inner_error}.`); - res.statusCode = 500; - res.setHeader('Content-Type', 'text/plain'); - res.end(String(inner_error)); - } - }) - } - catch (error) { - console.error(`adblocker: ${inner_error}.`); + try { + const jsonData = Buffer.concat(chunks); + const jsonStruct = JSON.parse(jsonData.toString()); - res.statusCode = 500; - res.setHeader('Content-Type', 'text/plain'); - res.end(String(error)); - } -}); + const askUrl = jsonStruct['url']; + const askFilter = jsonStruct['filter']; + const askCosmetic = jsonStruct['cosmetic']; + const askUrlType = jsonStruct['url_type']; + const fullUrl = new URL(askUrl); -server.listen(port, hostname, () => { - console.log(`adblocker: Server started at local port ${port}.`); -}); \ No newline at end of file + resultJson = {}; + + if (askFilter) { + const adblockMatch = engine.match(adblock.Request.fromRawDetails({ + type: askUrlType, + url: askUrl, + })); + + resultJson["filter"] = adblockMatch; + console.log(`adblocker: Filter is:\n${JSON.stringify(adblockMatch)}.`) + } + + if (askCosmetic) { + const adblockCosmetic = engine.getCosmeticsFilters({ + url: askUrl, + hostname: fullUrl.hostname, + domain: tldts.parse(fullUrl).domain + }); + + resultJson["cosmetic"] = adblockCosmetic; + console.log(`adblocker: Cosmetic is:\n${JSON.stringify(adblockCosmetic)}.`) + } + + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(resultJson)); + + console.log(new Date()); + } + catch (inner_error) { + console.error(`adblocker: ${inner_error}.`); + + res.statusCode = 500; + res.setHeader('Content-Type', 'text/plain'); + res.end(String(inner_error)); + } + }) + } + catch (error) { + console.error(`adblocker: ${inner_error}.`); + + res.statusCode = 500; + res.setHeader('Content-Type', 'text/plain'); + res.end(String(error)); + } + }); + + server.listen(port, hostname, () => { + console.log(`adblocker: Server started at local port ${port}.`); + }); +} \ No newline at end of file diff --git a/src/librssguard/definitions/definitions.h b/src/librssguard/definitions/definitions.h index 540ab4613..46eea00c9 100755 --- a/src/librssguard/definitions/definitions.h +++ b/src/librssguard/definitions/definitions.h @@ -14,13 +14,10 @@ #define SERVICE_CODE_INOREADER "inoreader" #define SERVICE_CODE_GMAIL "gmail" +#define ADBLOCK_SERVER_PORT "48484" #define ADBLOCK_HOWTO "https://github.com/martinrotter/rssguard/blob/master/resources/docs/Documentation.md#adblock" -#define ADBLOCK_UPDATE_DAYS_INTERVAL 14 #define ADBLOCK_ICON_ACTIVE "adblock" #define ADBLOCK_ICON_DISABLED "adblock-disabled" -#define ADBLOCK_CUSTOMLIST_NAME "customlist.txt" -#define ADBLOCK_LISTS_SUBDIRECTORY "adblock" -#define ADBLOCK_EASYLIST_URL "https://easylist.to/easylist/easylist.txt" #define OAUTH_DECRYPTION_KEY 11451167756100761335ul #define OAUTH_REDIRECT_URI "http://localhost" diff --git a/src/librssguard/miscellaneous/application.cpp b/src/librssguard/miscellaneous/application.cpp index 77b77da81..48831ff5a 100755 --- a/src/librssguard/miscellaneous/application.cpp +++ b/src/librssguard/miscellaneous/application.cpp @@ -78,7 +78,10 @@ Application::Application(const QString& id, int& argc, char** argv) #if defined(USE_WEBENGINE) m_webFactory->urlIinterceptor()->load(); - m_webFactory->adBlock()->load(true); + + QTimer::singleShot(3000, this, [=]() { + m_webFactory->adBlock()->load(true); + }); #endif qDebugNN << LOGSEC_CORE diff --git a/src/librssguard/miscellaneous/skinfactory.cpp b/src/librssguard/miscellaneous/skinfactory.cpp index c43de3de9..4e69412db 100644 --- a/src/librssguard/miscellaneous/skinfactory.cpp +++ b/src/librssguard/miscellaneous/skinfactory.cpp @@ -60,9 +60,10 @@ QString SkinFactory::selectedSkinName() const { return qApp->settings()->value(GROUP(GUI), SETTING(GUI::Skin)).toString(); } -QString SkinFactory::adBlockedPage(const QString& url) { +QString SkinFactory::adBlockedPage(const QString& url, const QString& filter) { const QString& adblocked = currentSkin().m_adblocked.arg(tr("This page was blocked by AdBlock"), - tr(R"(Blocked URL: "%1")").arg(url)); + tr(R"(Blocked URL: "%1"
Used filter: "%2")").arg(url, + filter)); return currentSkin().m_layoutMarkupWrapper.arg(tr("This page was blocked by AdBlock"), adblocked); } diff --git a/src/librssguard/miscellaneous/skinfactory.h b/src/librssguard/miscellaneous/skinfactory.h index 9471ddcd9..404d5a5d3 100644 --- a/src/librssguard/miscellaneous/skinfactory.h +++ b/src/librssguard/miscellaneous/skinfactory.h @@ -50,7 +50,7 @@ class RSSGUARD_DLLSPEC SkinFactory : public QObject { // after application restart. QString selectedSkinName() const; - QString adBlockedPage(const QString& url); + QString adBlockedPage(const QString& url, const QString& filter); // Gets skin about a particular skin. Skin skinInfo(const QString& skin_name, bool* ok = nullptr) const; diff --git a/src/librssguard/network-web/adblock/adblockmanager.cpp b/src/librssguard/network-web/adblock/adblockmanager.cpp index 8fbf1ab3b..29619761c 100644 --- a/src/librssguard/network-web/adblock/adblockmanager.cpp +++ b/src/librssguard/network-web/adblock/adblockmanager.cpp @@ -3,6 +3,7 @@ #include "network-web/adblock/adblockmanager.h" #include "exceptions/applicationexception.h" +#include "exceptions/networkexception.h" #include "miscellaneous/application.h" #include "miscellaneous/settings.h" #include "network-web/adblock/adblockdialog.h" @@ -15,7 +16,10 @@ #include #include +#include +#include #include +#include #include #include #include @@ -24,24 +28,43 @@ AdBlockManager::AdBlockManager(QObject* parent) : QObject(parent), m_loaded(false), m_enabled(false), m_interceptor(new AdBlockUrlInterceptor(this)) { m_adblockIcon = new AdBlockIcon(this); m_adblockIcon->setObjectName(QSL("m_adblockIconAction")); - m_unifiedFiltersFile = qApp->userDataFolder() + QDir::separator() + QSL("adblock-unified-filters.txt"); + m_serverProcess = new QProcess(this); } -bool AdBlockManager::block(const AdblockRequestInfo& request) const { +AdBlockManager::~AdBlockManager() { + if (m_serverProcess->state() == QProcess::ProcessState::Running) { + m_serverProcess->kill(); + } +} + +BlockingResult AdBlockManager::block(const AdblockRequestInfo& request) const { if (!isEnabled()) { - return false; + return { false }; } const QString url_string = request.requestUrl().toEncoded().toLower(); const QString url_scheme = request.requestUrl().scheme().toLower(); if (!canRunOnScheme(url_scheme)) { - return false; + return { false }; } else { - // TODO: start server if needed, call it. - return false; + if (m_serverProcess->state() == QProcess::ProcessState::Running) { + try { + auto result = askServerIfBlocked(url_string); + + return result; + } + catch (const ApplicationException& ex) { + qCriticalNN << LOGSEC_ADBLOCK + << "HTTP error when calling server:" + << QUOTE_W_SPACE_DOT(ex.message()); + } + } + else { + return { false }; + } } } @@ -123,8 +146,106 @@ void AdBlockManager::showDialog() { AdBlockDialog(qApp->mainFormWidget()).exec(); } +BlockingResult AdBlockManager::askServerIfBlocked(const QString& url) const { + QJsonObject req_obj; + QByteArray out; + QElapsedTimer tmr; + + req_obj["url"] = url; + req_obj["filter"] = true; + + tmr.start(); + + auto network_res = NetworkFactory::performNetworkOperation(QSL("http://%1:%2").arg(QHostAddress(QHostAddress::SpecialAddress::LocalHost).toString(), + ADBLOCK_SERVER_PORT), + 500, + QJsonDocument(req_obj).toJson(), + out, + QNetworkAccessManager::Operation::PostOperation, + { { + QSL(HTTP_HEADERS_CONTENT_TYPE).toLocal8Bit(), + QSL("application/json").toLocal8Bit() } }); + + if (network_res.first == QNetworkReply::NetworkError::NoError) { + qDebugNN << LOGSEC_ADBLOCK + << "Query to server took " + << tmr.elapsed() + << " ms."; + + QJsonObject out_obj = QJsonDocument::fromJson(out).object(); + bool blocking = out_obj["filter"].toObject()["match"].toBool(); + + return { + blocking, + blocking + ? out_obj["filter"].toObject()["filter"].toObject()["filter"].toString() + : QString() + }; + } + else { + throw NetworkException(network_res.first); + } +} + void AdBlockManager::restartServer() { - // TODO: + if (m_serverProcess->state() == QProcess::ProcessState::Running) { + m_serverProcess->kill(); + + if (!m_serverProcess->waitForFinished(1000)) { + m_serverProcess->deleteLater(); + m_serverProcess = new QProcess(this); + } + } + + QString temp_server = QDir::toNativeSeparators(IOFactory::getSystemFolder(QStandardPaths::StandardLocation::TempLocation)) + + QDir::separator() + + QSL("adblock-server.js"); + + if (!IOFactory::copyFile(QSL(":/scripts/adblock/adblock-server.js"), temp_server)) { + qCriticalNN << LOGSEC_ADBLOCK << "Failed to copy server file to TEMP."; + } + +#if defined(Q_OS_WIN) + m_serverProcess->setProgram(QSL("node.exe")); +#else + m_serverProcess->setProgram(QSL("node")); +#endif + + m_serverProcess->setArguments({ + QDir::toNativeSeparators(temp_server), + ADBLOCK_SERVER_PORT, + QDir::toNativeSeparators(m_unifiedFiltersFile) + }); + + m_serverProcess->setProcessEnvironment(QProcessEnvironment::systemEnvironment()); + + auto pe = m_serverProcess->processEnvironment(); + QString node_path = +#if defined(Q_OS_WIN) + pe.value(QSL("APPDATA")) + +#elif defined(Q_OS_LINUX) + QSL("/usr/local/lib/node_modules") + +#else + QDir::toNativeSeparators(IOFactory::getSystemFolder(QStandardPaths::StandardLocation::GenericDataLocation)) + +#endif + QDir::separator() + + QSL("npm") + + QDir::separator() + + QSL("node_modules"); + + if (!pe.contains(QSL("NODE_PATH"))) { + pe.insert(QSL("NODE_PATH"), node_path); + } + + m_serverProcess->setProcessEnvironment(pe); + m_serverProcess->setProcessChannelMode(QProcess::ProcessChannelMode::ForwardedErrorChannel); + + if (!m_serverProcess->open()) { + qWarningNN << LOGSEC_ADBLOCK << "Failed to start server."; + } + else { + qDebugNN << LOGSEC_ADBLOCK << "Started server."; + } } void AdBlockManager::updateUnifiedFiltersFile() { @@ -132,8 +253,7 @@ void AdBlockManager::updateUnifiedFiltersFile() { QFile::remove(m_unifiedFiltersFile); } - // TODO: generate file - QByteArray unified_contents; + QString unified_contents; auto filter_lists = filterLists(); // Download filters one by one and append. @@ -142,10 +262,16 @@ void AdBlockManager::updateUnifiedFiltersFile() { auto res = NetworkFactory::performNetworkOperation(filter_list_url, 2000, {}, - out, QNetworkAccessManager::Operation::GetOperation); + out, + QNetworkAccessManager::Operation::GetOperation); if (res.first == QNetworkReply::NetworkError::NoError) { - unified_contents += out; + unified_contents = unified_contents.append(QString::fromUtf8(out)); + unified_contents = unified_contents.append('\n'); + + qDebugNN << LOGSEC_ADBLOCK + << "Downloaded filter list from" + << QUOTE_W_SPACE_DOT(filter_list_url); } else { qWarningNN << LOGSEC_ADBLOCK @@ -156,7 +282,7 @@ void AdBlockManager::updateUnifiedFiltersFile() { } } - unified_contents += customFilters().join(QSL("\n")).toUtf8(); + unified_contents = unified_contents.append(customFilters().join(QSL("\n"))); // Save. m_unifiedFiltersFile = IOFactory::getSystemFolder(QStandardPaths::StandardLocation::TempLocation) + @@ -164,10 +290,9 @@ void AdBlockManager::updateUnifiedFiltersFile() { QSL("adblock.filters"); try { - IOFactory::writeFile(m_unifiedFiltersFile, unified_contents); + IOFactory::writeFile(m_unifiedFiltersFile, unified_contents.toUtf8()); if (m_enabled) { - // TODO: re-start nodejs adblock server. restartServer(); } } diff --git a/src/librssguard/network-web/adblock/adblockmanager.h b/src/librssguard/network-web/adblock/adblockmanager.h index 2e856642c..161fd8a41 100644 --- a/src/librssguard/network-web/adblock/adblockmanager.h +++ b/src/librssguard/network-web/adblock/adblockmanager.h @@ -6,15 +6,26 @@ #include class QUrl; +class QProcess; class AdblockRequestInfo; class AdBlockUrlInterceptor; class AdBlockIcon; +struct BlockingResult { + bool m_blocked; + QString m_blockedByFilter; + + BlockingResult(bool blocked, QString blocked_by_filter = {}) + : m_blocked(blocked), m_blockedByFilter(std::move(blocked_by_filter)) {} + +}; + class AdBlockManager : public QObject { Q_OBJECT public: explicit AdBlockManager(QObject* parent = nullptr); + virtual ~AdBlockManager(); // If "initial_load" is false, then we want to explicitly turn off // Adblock if it is running or turn on when not running. @@ -27,7 +38,7 @@ class AdBlockManager : public QObject { AdBlockIcon* adBlockIcon() const; // General methods for adblocking. - bool block(const AdblockRequestInfo& request) const; + BlockingResult block(const AdblockRequestInfo& request) const; QString elementHidingRulesForDomain(const QUrl& url) const; QStringList filterLists() const; @@ -47,6 +58,8 @@ class AdBlockManager : public QObject { void enabledChanged(bool enabled); private: + BlockingResult askServerIfBlocked(const QString& url) const; + void restartServer(); private: @@ -55,6 +68,7 @@ class AdBlockManager : public QObject { AdBlockIcon* m_adblockIcon; AdBlockUrlInterceptor* m_interceptor; QString m_unifiedFiltersFile; + QProcess* m_serverProcess; }; inline AdBlockIcon* AdBlockManager::adBlockIcon() const { diff --git a/src/librssguard/network-web/adblock/adblockurlinterceptor.cpp b/src/librssguard/network-web/adblock/adblockurlinterceptor.cpp index ee26e0a00..a33df98ba 100644 --- a/src/librssguard/network-web/adblock/adblockurlinterceptor.cpp +++ b/src/librssguard/network-web/adblock/adblockurlinterceptor.cpp @@ -27,7 +27,7 @@ AdBlockUrlInterceptor::AdBlockUrlInterceptor(AdBlockManager* manager) : UrlInterceptor(manager), m_manager(manager) {} void AdBlockUrlInterceptor::interceptRequest(QWebEngineUrlRequestInfo& info) { - if (m_manager->block(AdblockRequestInfo(info))) { + if (m_manager->block(AdblockRequestInfo(info)).m_blocked) { info.block(true); qWarningNN << LOGSEC_ADBLOCK << "Blocked request:" << QUOTE_W_SPACE_DOT(info.requestUrl().toString()); diff --git a/src/librssguard/network-web/webpage.cpp b/src/librssguard/network-web/webpage.cpp index 0d6cf8f42..059695512 100644 --- a/src/librssguard/network-web/webpage.cpp +++ b/src/librssguard/network-web/webpage.cpp @@ -48,9 +48,9 @@ bool WebPage::acceptNavigationRequest(const QUrl& url, NavigationType type, bool if (is_main_frame) { auto blocked = qApp->web()->adBlock()->block(AdblockRequestInfo(url)); - if (blocked) { + if (blocked.m_blocked) { // This website is entirely blocked. - setHtml(qApp->skins()->adBlockedPage(url.toString()), + setHtml(qApp->skins()->adBlockedPage(url.toString(), blocked.m_blockedByFilter), QUrl::fromUserInput(INTERNAL_URL_ADBLOCKED)); return false; }