LibWebView: Share content blocker lists as buffers

Add a repeatable blocker-list option that reads local list files in the
browser process. The files are concatenated into one buffer and shared
with WebContent through the content blocker IPC path when view options
are applied.

Parse the buffer in WebContent and reject malformed UTF-8 without
replacing the currently installed rules.
This commit is contained in:
Andreas Kling 2026-05-21 17:46:17 +02:00 committed by Andreas Kling
parent c974e616c0
commit 6be5e80025
9 changed files with 100 additions and 5 deletions

View file

@ -4,10 +4,13 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Checked.h>
#include <AK/Debug.h>
#include <AK/Time.h>
#include <LibCore/AnonymousBuffer.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/Environment.h>
#include <LibCore/File.h>
#include <LibCore/StandardPaths.h>
#include <LibCore/System.h>
#include <LibCore/TimeZoneWatcher.h>
@ -161,6 +164,7 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
bool disable_http_memory_cache = false;
bool disable_http_disk_cache = false;
bool disable_content_blocker = false;
Vector<StringView> content_blocker_list_paths;
Optional<StringView> resource_substitution_map_path;
bool enable_autoplay = false;
bool expose_experimental_interfaces = false;
@ -234,6 +238,19 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
args_parser.add_option(disable_http_memory_cache, "Disable HTTP memory cache", "disable-http-memory-cache");
args_parser.add_option(disable_http_disk_cache, "Disable HTTP disk cache", "disable-http-disk-cache");
args_parser.add_option(disable_content_blocker, "Disable content blocker", "disable-content-blocker");
args_parser.add_option(Core::ArgsParser::Option {
.argument_mode = Core::ArgsParser::OptionArgumentMode::Required,
.help_string = "Path to a content blocker list. May be specified multiple times.",
.long_name = "content-blocker-list",
.value_name = "path",
.accept_value = [&](StringView value) {
if (value.is_empty())
return false;
content_blocker_list_paths.append(value);
return true;
},
});
args_parser.add_option(enable_autoplay, "Enable multimedia autoplay", "enable-autoplay");
args_parser.add_option(expose_experimental_interfaces, "Expose experimental IDL interfaces", "expose-experimental-interfaces");
args_parser.add_option(expose_internals_object, "Expose internals object", "expose-internals-object");
@ -309,6 +326,11 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
if (profile_process.has_value())
profile_process_type = process_type_from_name(*profile_process);
Vector<ByteString> content_blocker_list_paths_as_byte_strings;
TRY(content_blocker_list_paths_as_byte_strings.try_ensure_capacity(content_blocker_list_paths.size()));
for (auto path : content_blocker_list_paths)
content_blocker_list_paths_as_byte_strings.unchecked_append(path);
// Disable site isolation when debugging WebContent. Otherwise, the process swap may interfere with the gdb session.
if (debug_process_types.contains_slow(ProcessType::WebContent))
disable_site_isolation = true;
@ -331,6 +353,7 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
: OptionalNone()),
.devtools_port = devtools_port,
.enable_content_blocker = disable_content_blocker ? EnableContentBlocker::No : EnableContentBlocker::Yes,
.content_blocker_list_paths = move(content_blocker_list_paths_as_byte_strings),
};
if (screenshot_delay.has_value())
@ -387,6 +410,8 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
if (m_web_content_options.file_scheme_urls_have_tuple_origins == FileSchemeUrlsHaveTupleOrigins::Yes)
URL::set_file_scheme_urls_have_tuple_origins();
TRY(load_content_blocker_lists());
initialize_actions();
m_event_loop = create_platform_event_loop();
@ -395,6 +420,39 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
return {};
}
ErrorOr<void> Application::load_content_blocker_lists()
{
if (m_browser_options.content_blocker_list_paths.is_empty())
return {};
Checked<size_t> total_size = 0;
for (auto const& path : m_browser_options.content_blocker_list_paths) {
auto file = TRY(Core::File::open(path, Core::File::OpenMode::Read));
total_size += TRY(file->size());
total_size += 1;
}
if (total_size.has_overflow())
return Error::from_string_literal("Content blocker lists are too large");
auto blocker_list_buffer = TRY(Core::AnonymousBuffer::create_with_size(total_size.value()));
auto bytes = Bytes { blocker_list_buffer.data<u8>(), blocker_list_buffer.size() };
size_t offset = 0;
for (auto const& path : m_browser_options.content_blocker_list_paths) {
auto file = TRY(Core::File::open(path, Core::File::OpenMode::Read));
auto file_size = TRY(file->size());
TRY(file->read_until_filled(bytes.slice(offset, file_size)));
offset += file_size;
bytes[offset++] = '\n';
}
VERIFY(offset == bytes.size());
m_content_blocker_list_buffer = move(blocker_list_buffer);
return {};
}
void Application::open_url_in_new_tab(URL::URL const& url, Web::HTML::ActivateTab activate_tab) const
{
if (auto view = open_blank_new_tab(activate_tab); view.has_value())
@ -1242,6 +1300,8 @@ void Application::apply_view_options(Badge<ViewImplementation>, ViewImplementati
view.debug_request("set-line-box-borders"sv, m_show_line_box_borders_action->checked() ? "on"sv : "off"sv);
view.debug_request("scripting"sv, m_enable_scripting_action->checked() ? "on"sv : "off"sv);
view.debug_request("content-blocking"sv, m_enable_content_blocking_action->checked() ? "on"sv : "off"sv);
if (m_content_blocker_list_buffer.has_value())
view.set_content_blockers(*m_content_blocker_list_buffer);
view.debug_request("block-pop-ups"sv, m_block_pop_ups_action->checked() ? "on"sv : "off"sv);
view.debug_request("spoof-user-agent"sv, m_user_agent_string);
view.debug_request("navigator-compatibility-mode"sv, m_navigator_compatibility_mode);

View file

@ -10,6 +10,7 @@
#include <AK/Function.h>
#include <AK/LexicalPath.h>
#include <AK/Optional.h>
#include <LibCore/AnonymousBuffer.h>
#include <LibCore/EventLoop.h>
#include <LibCore/Forward.h>
#include <LibDatabase/Forward.h>
@ -209,6 +210,7 @@ private:
ErrorOr<void> launch_request_server();
ErrorOr<void> launch_image_decoder_server();
ErrorOr<void> launch_devtools_server();
ErrorOr<void> load_content_blocker_lists();
void initialize_actions();
@ -271,6 +273,7 @@ private:
BrowserOptions m_browser_options;
RequestServerOptions m_request_server_options;
WebContentOptions m_web_content_options;
Optional<Core::AnonymousBuffer> m_content_blocker_list_buffer;
RefPtr<Requests::RequestClient> m_request_server_client;
RefPtr<ImageDecoderClient::Client> m_image_decoder_client;

View file

@ -92,6 +92,7 @@ struct BrowserOptions {
Optional<DNSSettings> dns_settings {};
Optional<u16> devtools_port;
EnableContentBlocker enable_content_blocker { EnableContentBlocker::Yes };
Vector<ByteString> content_blocker_list_paths {};
};
enum class HTTPDiskCacheMode {

View file

@ -566,6 +566,11 @@ void ViewImplementation::debug_request(ByteString const& request, ByteString con
client().async_debug_request(page_id(), request, argument);
}
void ViewImplementation::set_content_blockers(Core::AnonymousBuffer const& patterns)
{
client().async_set_content_blockers(page_id(), patterns);
}
void ViewImplementation::run_javascript(String const& js_source)
{
client().async_run_javascript(page_id(), js_source);

View file

@ -15,6 +15,7 @@
#include <AK/Queue.h>
#include <AK/String.h>
#include <AK/Utf16String.h>
#include <LibCore/AnonymousBuffer.h>
#include <LibCore/Forward.h>
#include <LibCore/Promise.h>
#include <LibCore/SharedVersion.h>
@ -143,6 +144,7 @@ public:
void request_style_sheet_source(Web::CSS::StyleSheetIdentifier const&);
void debug_request(ByteString const& request, ByteString const& argument = {});
void set_content_blockers(Core::AnonymousBuffer const& patterns);
void run_javascript(String const&);
void js_console_input(String const&);

View file

@ -1144,11 +1144,33 @@ void ConnectionFromClient::paste(u64 page_id, Utf16String text)
page->page().focused_navigable().paste(text);
}
void ConnectionFromClient::set_content_blockers(u64 page_id, Vector<String> patterns)
static ErrorOr<Vector<String>> parse_content_blocker_patterns(Core::AnonymousBuffer const& patterns_buffer)
{
Vector<String> patterns;
for (auto line : StringView { patterns_buffer.bytes() }.split_view('\n', SplitBehavior::Nothing)) {
if (line.ends_with('\r'))
line = line.substring_view(0, line.length() - 1);
if (line.is_empty())
continue;
patterns.append(TRY(String::from_utf8(line)));
}
return patterns;
}
void ConnectionFromClient::set_content_blockers(u64 page_id, Core::AnonymousBuffer patterns_buffer)
{
auto patterns_or_error = parse_content_blocker_patterns(patterns_buffer);
if (patterns_or_error.is_error()) {
dbgln("Failed to set content blockers: {}", patterns_or_error.error());
return;
}
auto& blocker = Web::ContentBlocker::the();
auto had_cosmetic_rules = blocker.has_cosmetic_rules();
blocker.set_patterns(patterns).release_value_but_fixme_should_propagate_errors();
blocker.set_patterns(patterns_or_error.value()).release_value_but_fixme_should_propagate_errors();
if (had_cosmetic_rules || blocker.has_cosmetic_rules()) {
if (auto page = this->page(page_id); page.has_value())

View file

@ -11,6 +11,7 @@
#include <AK/HashMap.h>
#include <AK/Queue.h>
#include <AK/SourceLocation.h>
#include <LibCore/AnonymousBuffer.h>
#include <LibGC/Root.h>
#include <LibIPC/ConnectionFromClient.h>
#include <LibJS/Forward.h>
@ -107,7 +108,7 @@ private:
virtual void clone_dom_node(u64 page_id, Web::UniqueNodeID node_id) override;
virtual void remove_dom_node(u64 page_id, Web::UniqueNodeID node_id) override;
virtual void set_content_blockers(u64 page_id, Vector<String> patterns) override;
virtual void set_content_blockers(u64 page_id, Core::AnonymousBuffer patterns) override;
virtual void set_autoplay_allowed_on_all_websites(u64 page_id) override;
virtual void set_autoplay_allowlist(u64 page_id, Vector<String> allowlist) override;
virtual void set_proxy_mappings(u64 page_id, Vector<ByteString>, HashMap<ByteString, size_t>) override;

View file

@ -97,7 +97,7 @@ endpoint WebContentServer
find_in_page_next_match(u64 page_id) =|
find_in_page_previous_match(u64 page_id) =|
set_content_blockers(u64 page_id, Vector<String> patterns) =|
set_content_blockers(u64 page_id, Core::AnonymousBuffer patterns) =|
set_autoplay_allowed_on_all_websites(u64 page_id) =|
set_autoplay_allowlist(u64 page_id, Vector<String> allowlist) =|
set_proxy_mappings(u64 page_id, Vector<ByteString> proxies, HashMap<ByteString, size_t> mappings) =|

View file

@ -6,6 +6,7 @@
#include "TestWebView.h"
#include <LibCore/AnonymousBuffer.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/ShareableBitmap.h>
@ -27,7 +28,7 @@ TestWebView::TestWebView(Core::AnonymousBuffer theme, Web::DevicePixelSize viewp
void TestWebView::clear_content_blockers()
{
client().async_set_content_blockers(m_client_state.page_index, {});
client().async_set_content_blockers(m_client_state.page_index, MUST(Core::AnonymousBuffer::create_with_size(0)));
}
pid_t TestWebView::web_content_pid() const