blob: bbe02e8bb3ec668efaeb8a488f93cbeca74d2de6 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/renderer/controlled_frame/web_url_pattern_natives.h"
#include "base/containers/contains.h"
#include "base/containers/to_value_list.h"
#include "extensions/renderer/script_context.h"
#include "third_party/blink/public/common/safe_url_pattern.h"
#include "third_party/blink/public/web/web_url_pattern_to_safe_url_pattern.h"
#include "v8/include/v8-container.h"
#include "v8/include/v8-exception.h"
#include "v8/include/v8-function-callback.h"
#include "v8/include/v8-isolate.h"
namespace controlled_frame {
namespace {
// This is for when there's no path, it gets inferred as wildcard,
// but also comes with a match group name equal to "0" that we need to ignore.
constexpr std::string kInferredWildcardPartName = "0";
constexpr const char* kAllowedProtocols[] = {"http", "https", "ws", "wss"};
bool WouldBeValidMatchPattern(const blink::SafeUrlPattern& safe_url_pattern) {
// Protocol: one part, either wildcard or fixed in allowed set
if (safe_url_pattern.protocol.size() != 1) {
return false;
}
const liburlpattern::Part& protocol_part = safe_url_pattern.protocol[0];
const bool protocol_is_wildcard =
protocol_part.type == liburlpattern::PartType::kFullWildcard;
const bool protocol_is_fixed =
protocol_part.type == liburlpattern::PartType::kFixed;
const bool protocol_allowed =
protocol_is_fixed &&
base::Contains(kAllowedProtocols, protocol_part.value);
if (!protocol_is_wildcard && !protocol_allowed) {
return false;
}
if (protocol_part.modifier != liburlpattern::Modifier::kNone) {
return false;
}
// Hostname: first part must be wildcard or fixed, rest must be fixed
const std::vector<liburlpattern::Part>& host_parts =
safe_url_pattern.hostname;
if (host_parts.empty()) {
return false;
}
if (host_parts[0].type != liburlpattern::PartType::kFullWildcard &&
host_parts[0].type != liburlpattern::PartType::kFixed) {
return false;
}
for (size_t i = 1; i < host_parts.size(); ++i) {
const bool fixed_and_starts_with_dot =
host_parts[i].type == liburlpattern::PartType::kFixed &&
host_parts[i].value[0] == '.';
if (!fixed_and_starts_with_dot) {
return false;
}
}
for (const liburlpattern::Part& host_part : host_parts) {
if (host_part.modifier != liburlpattern::Modifier::kNone) {
return false;
}
}
// Port: must be wildcard or fixed
const std::vector<liburlpattern::Part>& port_parts = safe_url_pattern.port;
if (!port_parts.empty() &&
port_parts[0].type != liburlpattern::PartType::kFixed &&
port_parts[0].type != liburlpattern::PartType::kFullWildcard) {
return false;
} else if (port_parts.size() > 1) {
return false;
// Protocol cannot be wildcard if port is fixed
} else if (!port_parts.empty() &&
port_parts[0].type == liburlpattern::PartType::kFixed &&
protocol_is_wildcard) {
return false;
} else if (!port_parts.empty() &&
port_parts[0].modifier != liburlpattern::Modifier::kNone) {
return false;
}
// Don't allow match groups.
for (const liburlpattern::Part& path_part : safe_url_pattern.pathname) {
if (!path_part.name.empty() &&
path_part.name != kInferredWildcardPartName) {
return false;
} else if (path_part.modifier != liburlpattern::Modifier::kNone) {
return false;
}
}
// Username, password: must be empty
auto part_is_empty = [](const std::vector<liburlpattern::Part>& parts) {
return parts.empty() || parts[0].value.empty() ||
parts[0].type == liburlpattern::PartType::kFullWildcard;
};
if (!part_is_empty(safe_url_pattern.username) ||
!part_is_empty(safe_url_pattern.password)) {
return false;
}
return true;
}
std::vector<std::string> MatchPatternsFromUrlPattern(
const blink::SafeUrlPattern& safe_url_pattern) {
std::vector<std::string> match_patterns;
auto append_part = [](std::string& out, const liburlpattern::Part& part) {
if (part.type == liburlpattern::PartType::kFullWildcard) {
out += part.prefix + "*" + part.suffix;
} else {
out += part.value;
}
};
std::string protocol_host_str;
const liburlpattern::Part& protocol_part = safe_url_pattern.protocol[0];
append_part(protocol_host_str, protocol_part);
protocol_host_str += "://";
for (const liburlpattern::Part& host_part : safe_url_pattern.hostname) {
append_part(protocol_host_str, host_part);
}
if (!safe_url_pattern.port.empty()) {
protocol_host_str += ":";
append_part(protocol_host_str, safe_url_pattern.port[0]);
}
std::string path_str;
for (const liburlpattern::Part& path_part : safe_url_pattern.pathname) {
append_part(path_str, path_part);
}
if (!path_str.starts_with("/")) {
path_str = "/" + path_str;
}
// If empty search params or wildcard search params,
// add a match pattern without search params,
// because match patterns with "?*" don't match empty search params.
if (safe_url_pattern.search.empty() ||
safe_url_pattern.search[0].type ==
liburlpattern::PartType::kFullWildcard) {
match_patterns.push_back(protocol_host_str + path_str);
}
// If not empty search params, add them as a separate match pattern.
if (!safe_url_pattern.search.empty()) {
std::string search_query_str = "?";
for (const liburlpattern::Part& search_part : safe_url_pattern.search) {
append_part(search_query_str, search_part);
}
match_patterns.push_back(protocol_host_str + path_str + search_query_str);
}
// If "*" protocol, supply "ws" and "wss" as matching protocols as well.
if (protocol_part.type == liburlpattern::PartType::kFullWildcard) {
std::vector<std::string> ws_wss_additional_patterns;
for (std::string& match_pattern : match_patterns) {
CHECK_EQ(match_pattern[0], '*');
std::string pattern_without_wildcard_protocol = match_pattern.substr(1);
ws_wss_additional_patterns.push_back("ws" +
pattern_without_wildcard_protocol);
ws_wss_additional_patterns.push_back("wss" +
pattern_without_wildcard_protocol);
}
match_patterns.insert(match_patterns.end(),
ws_wss_additional_patterns.begin(),
ws_wss_additional_patterns.end());
}
return match_patterns;
}
} // namespace
WebUrlPatternNatives::WebUrlPatternNatives(extensions::ScriptContext* context)
: ObjectBackedNativeHandler(context) {}
void WebUrlPatternNatives::AddRoutes() {
RouteHandlerFunction(
"URLPatternToMatchPatterns",
base::BindRepeating(&WebUrlPatternNatives::URLPatternToMatchPatterns,
base::Unretained(this)));
}
bool V8URLPatternToMatchPatterns(v8::Isolate* isolate,
v8::Local<v8::Value>& input,
std::vector<std::string>& out_match_patterns) {
std::optional<blink::SafeUrlPattern> safe_url_pattern =
blink::WebURLPatternToSafeUrlPattern(isolate, input);
if (!safe_url_pattern) {
return false;
}
if (!WouldBeValidMatchPattern(*safe_url_pattern)) {
return false;
}
out_match_patterns = MatchPatternsFromUrlPattern(*safe_url_pattern);
return true;
}
// This function sends multiple match patterns back to the js layer, even though
// it gets only one URLPattern object or string via args, because of
// the discrepancies between how URLPatterns and Match Patterns work.
// See the stringifying function above for details.
void WebUrlPatternNatives::URLPatternToMatchPatterns(
const v8::FunctionCallbackInfo<v8::Value>& args) {
CHECK_EQ(1, args.Length());
CHECK(args[0]->IsString() || args[0]->IsObject());
v8::Isolate* isolate = args.GetIsolate();
v8::Local<v8::Context> context = isolate->GetCurrentContext();
v8::Local<v8::Value> input = args[0];
std::vector<std::string> match_patterns;
if (controlled_frame::V8URLPatternToMatchPatterns(isolate, input,
match_patterns)) {
v8::Local<v8::Array> match_patterns_array =
v8::Array::New(isolate, match_patterns.size());
for (size_t i = 0; i < match_patterns.size(); ++i) {
v8::Local<v8::String> str =
v8::String::NewFromUtf8(isolate, match_patterns[i].c_str(),
v8::NewStringType::kNormal)
.ToLocalChecked();
match_patterns_array->Set(context, static_cast<uint32_t>(i), str).Check();
}
args.GetReturnValue().Set(match_patterns_array);
} else {
args.GetReturnValue().Set(isolate->ThrowException(v8::Exception::Error(
v8::String::NewFromUtf8(isolate, "Invalid URL pattern",
v8::NewStringType::kInternalized)
.ToLocalChecked())));
}
}
} // namespace controlled_frame