LibWeb: Support SRI for import maps

See:
- https://github.com/whatwg/html/commit/b2fdca1
This commit is contained in:
Jamie Mansfield 2024-06-01 11:25:58 +01:00 committed by Tim Ledbetter
parent 124779a376
commit bab086694a
Notes: github-actions[bot] 2024-08-14 20:07:19 +00:00
6 changed files with 127 additions and 17 deletions

View file

@ -422,11 +422,11 @@ ErrorOr<void> initialize_main_thread_vm(HTML::EventLoop::Type type)
// 3. Let referencingScript be null.
Optional<HTML::Script&> referencing_script;
// FIXME: 4. Let fetchOptions be the default classic script fetch options.
auto fetch_options = HTML::default_classic_script_fetch_options();
// 4. Let originalFetchOptions be the default classic script fetch options.
auto original_fetch_options = HTML::default_classic_script_fetch_options();
// 5. Let fetchReferrer be "client".
auto fetch_referrer = Fetch::Infrastructure::Request::Referrer::Client;
Fetch::Infrastructure::Request::ReferrerType fetch_referrer = Fetch::Infrastructure::Request::Referrer::Client;
// 6. If referrer is a Script Record or a Module Record, then:
if (referrer.has<JS::NonnullGCPtr<JS::Script>>() || referrer.has<JS::NonnullGCPtr<JS::CyclicModule>>()) {
@ -436,11 +436,10 @@ ErrorOr<void> initialize_main_thread_vm(HTML::EventLoop::Type type)
// 2. Set settingsObject to referencingScript's settings object.
settings_object = referencing_script->settings_object();
// FIXME: 3. Set fetchOptions to the new descendant script fetch options for referencingScript's fetch options.
// 3. Set fetchReferrer to referencingScript's base URL.
fetch_referrer = referencing_script->base_url();
// FIXME: 4. Assert: fetchOptions is not null, as referencingScript is a classic script or a JavaScript module script.
// FIXME: 5. Set fetchReferrer to referrer's base URL.
// FIXME: 4. Set originalFetchOptions to referencingScript's fetch options.
}
// 7. Disallow further import maps given settingsObject.
@ -463,13 +462,16 @@ ErrorOr<void> initialize_main_thread_vm(HTML::EventLoop::Type type)
return;
}
// 10. Let destination be "script".
// 10. Let fetchOptions be the result of getting the descendant script fetch options given originalFetchOptions, url, and settingsObject.
auto fetch_options = MUST(HTML::get_descendant_script_fetch_options(original_fetch_options, url.value(), *settings_object));
// 11. Let destination be "script".
auto destination = Fetch::Infrastructure::Request::Destination::Script;
// 11. Let fetchClient be settingsObject.
// 12. Let fetchClient be settingsObject.
JS::NonnullGCPtr fetch_client { *settings_object };
// 12. If loadState is not undefined, then:
// 13. If loadState is not undefined, then:
HTML::PerformTheFetchHook perform_fetch;
if (load_state) {
auto& fetch_context = static_cast<HTML::FetchContext&>(*load_state);
@ -537,7 +539,7 @@ ErrorOr<void> initialize_main_thread_vm(HTML::EventLoop::Type type)
vm.pop_execution_context();
});
// 13. Fetch a single imported module script given url, fetchClient, destination, fetchOptions, settingsObject, fetchReferrer,
// 14. Fetch a single imported module script given url, fetchClient, destination, fetchOptions, settingsObject, fetchReferrer,
// moduleRequest, and onSingleFetchComplete as defined below.
// If loadState is not undefined and loadState.[[PerformFetch]] is not null, pass loadState.[[PerformFetch]] along as well.
HTML::fetch_single_imported_module_script(realm, url.release_value(), *fetch_client, destination, fetch_options, *settings_object, fetch_referrer, module_request, perform_fetch, on_single_fetch_complete);

View file

@ -416,6 +416,10 @@ void HTMLScriptElement::prepare_script()
}
// -> "module"
else if (m_script_type == ScriptType::Module) {
// If el does not have an integrity attribute, then set options's integrity metadata to the result of resolving a module integrity metadata with url and settings object.
if (!has_attribute(HTML::AttributeNames::integrity))
options.integrity_metadata = MUST(resolve_a_module_integrity_metadata(url, settings_object));
// Fetch an external module script graph given url, settings object, options, and onComplete.
fetch_external_module_script_graph(realm(), url, settings_object, options, on_complete);
}

View file

@ -280,6 +280,43 @@ static void set_up_module_script_request(Fetch::Infrastructure::Request& request
request.set_priority(options.fetch_priority);
}
// https://html.spec.whatwg.org/multipage/webappapis.html#get-the-descendant-script-fetch-options
WebIDL::ExceptionOr<ScriptFetchOptions> get_descendant_script_fetch_options(ScriptFetchOptions const& original_options, URL::URL const& url, EnvironmentSettingsObject& settings_object)
{
// 1. Let newOptions be a copy of originalOptions.
auto new_options = original_options;
// 2. Let integrity be the empty string.
String integrity;
// 3. If settingsObject's global object is a Window object, then set integrity to the result of resolving a module integrity metadata with url and settingsObject.
if (is<Window>(settings_object.global_object()))
integrity = TRY(resolve_a_module_integrity_metadata(url, settings_object));
// 4. Set newOptions's integrity metadata to integrity.
new_options.integrity_metadata = integrity;
// 5. Set newOptions's fetch priority to "auto".
new_options.fetch_priority = Fetch::Infrastructure::Request::Priority::Auto;
// 6. Return newOptions.
return new_options;
}
// https://html.spec.whatwg.org/multipage/webappapis.html#resolving-a-module-integrity-metadata
WebIDL::ExceptionOr<String> resolve_a_module_integrity_metadata(const URL::URL& url, EnvironmentSettingsObject& settings_object)
{
// 1. Assert: settingsObject's global object is a Window object.
VERIFY(is<Window>(settings_object.global_object()));
// 2. Let map be settingsObject's global object's import map.
auto map = static_cast<Window const&>(settings_object.global_object()).import_map();
// 3. If map's integrity[url] does not exist, then return the empty string.
// 4. Return map's integrity[url].
return MUST(String::from_byte_string(map.integrity().get(url).value_or("")));
}
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
WebIDL::ExceptionOr<void> fetch_classic_script(JS::NonnullGCPtr<HTMLScriptElement> element, URL::URL const& url, EnvironmentSettingsObject& settings_object, ScriptFetchOptions options, CORSSettingAttribute cors_setting, String character_encoding, OnFetchScriptComplete on_complete)
{
@ -858,7 +895,7 @@ void fetch_single_imported_module_script(JS::Realm& realm,
Fetch::Infrastructure::Request::Destination destination,
ScriptFetchOptions const& options,
EnvironmentSettingsObject& settings_object,
Fetch::Infrastructure::Request::Referrer referrer,
Fetch::Infrastructure::Request::ReferrerType referrer,
JS::ModuleRequest const& module_request,
PerformTheFetchHook perform_fetch,
OnFetchScriptComplete on_complete)

View file

@ -86,7 +86,8 @@ ByteString module_type_from_module_request(JS::ModuleRequest const&);
WebIDL::ExceptionOr<URL::URL> resolve_module_specifier(Optional<Script&> referring_script, ByteString const& specifier);
WebIDL::ExceptionOr<Optional<URL::URL>> resolve_imports_match(ByteString const& normalized_specifier, Optional<URL::URL> as_url, ModuleSpecifierMap const&);
Optional<URL::URL> resolve_url_like_module_specifier(ByteString const& specifier, URL::URL const& base_url);
WebIDL::ExceptionOr<ScriptFetchOptions> get_descendant_script_fetch_options(ScriptFetchOptions const& original_options, URL::URL const& url, EnvironmentSettingsObject& settings_object);
WebIDL::ExceptionOr<String> resolve_a_module_integrity_metadata(URL::URL const& url, EnvironmentSettingsObject& settings_object);
WebIDL::ExceptionOr<void> fetch_classic_script(JS::NonnullGCPtr<HTMLScriptElement>, URL::URL const&, EnvironmentSettingsObject& settings_object, ScriptFetchOptions options, CORSSettingAttribute cors_setting, String character_encoding, OnFetchScriptComplete on_complete);
WebIDL::ExceptionOr<void> fetch_classic_worker_script(URL::URL const&, EnvironmentSettingsObject& fetch_client, Fetch::Infrastructure::Request::Destination, EnvironmentSettingsObject& settings_object, PerformTheFetchHook, OnFetchScriptComplete);
WebIDL::ExceptionOr<JS::NonnullGCPtr<ClassicScript>> fetch_a_classic_worker_imported_script(URL::URL const&, HTML::EnvironmentSettingsObject&, PerformTheFetchHook = nullptr);
@ -95,7 +96,7 @@ WebIDL::ExceptionOr<void> fetch_worklet_module_worker_script_graph(URL::URL cons
void fetch_internal_module_script_graph(JS::Realm&, JS::ModuleRequest const& module_request, EnvironmentSettingsObject& fetch_client_settings_object, Fetch::Infrastructure::Request::Destination, ScriptFetchOptions const&, Script& referring_script, HashTable<ModuleLocationTuple> const& visited_set, PerformTheFetchHook, OnFetchScriptComplete on_complete);
void fetch_external_module_script_graph(JS::Realm&, URL::URL const&, EnvironmentSettingsObject& settings_object, ScriptFetchOptions const&, OnFetchScriptComplete on_complete);
void fetch_inline_module_script_graph(JS::Realm&, ByteString const& filename, ByteString const& source_text, URL::URL const& base_url, EnvironmentSettingsObject& settings_object, OnFetchScriptComplete on_complete);
void fetch_single_imported_module_script(JS::Realm&, URL::URL const&, EnvironmentSettingsObject& fetch_client, Fetch::Infrastructure::Request::Destination, ScriptFetchOptions const&, EnvironmentSettingsObject& settings_object, Fetch::Infrastructure::Request::Referrer, JS::ModuleRequest const&, PerformTheFetchHook, OnFetchScriptComplete on_complete);
void fetch_single_imported_module_script(JS::Realm&, URL::URL const&, EnvironmentSettingsObject& fetch_client, Fetch::Infrastructure::Request::Destination, ScriptFetchOptions const&, EnvironmentSettingsObject& settings_object, Fetch::Infrastructure::Request::ReferrerType, JS::ModuleRequest const&, PerformTheFetchHook, OnFetchScriptComplete on_complete);
void fetch_descendants_of_a_module_script(JS::Realm&, JavaScriptModuleScript& module_script, EnvironmentSettingsObject& fetch_client_settings_object, Fetch::Infrastructure::Request::Destination, HashTable<ModuleLocationTuple> visited_set, PerformTheFetchHook, OnFetchScriptComplete callback);
void fetch_descendants_of_and_link_a_module_script(JS::Realm&, JavaScriptModuleScript&, EnvironmentSettingsObject&, Fetch::Infrastructure::Request::Destination, PerformTheFetchHook, OnFetchScriptComplete on_complete);

View file

@ -58,9 +58,24 @@ WebIDL::ExceptionOr<ImportMap> parse_import_map_string(JS::Realm& realm, ByteStr
sorted_and_normalised_scopes = TRY(sort_and_normalise_scopes(realm, scopes.as_object(), base_url));
}
// 7. If parsed's keys contains any items besides "imports" or "scopes", then the user agent should report a warning to the console indicating that an invalid top-level key was present in the import map.
// 7. Let normalizedIntegrity be an empty ordered map.
ModuleIntegrityMap normalised_integrity;
// 8. If parsed["integrity"] exists, then:
if (TRY(parsed_object.has_property("integrity"))) {
auto integrity = TRY(parsed_object.get("integrity"));
// 1. If parsed["integrity"] is not an ordered map, then throw a TypeError indicating that the value for the "integrity" top-level key needs to be a JSON object.
if (!integrity.is_object())
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, String::formatted("The 'integrity' top-level value of an importmap needs to be a JSON object.").release_value_but_fixme_should_propagate_errors() };
// 2. Set normalizedIntegrity to the result of normalizing a module integrity map given parsed["integrity"] and baseURL.
normalised_integrity = TRY(normalize_module_integrity_map(realm, integrity.as_object(), base_url));
}
// 9. If parsed's keys contains any items besides "imports", "scopes", or "integrity", then the user agent should report a warning to the console indicating that an invalid top-level key was present in the import map.
for (auto& key : parsed_object.shape().property_table().keys()) {
if (key.as_string().is_one_of("imports", "scopes"))
if (key.as_string().is_one_of("imports", "scopes", "integrity"))
continue;
auto& console = realm.intrinsics().console_object()->console();
@ -68,10 +83,11 @@ WebIDL::ExceptionOr<ImportMap> parse_import_map_string(JS::Realm& realm, ByteStr
TRY_OR_THROW_OOM(realm.vm(), String::formatted("An invalid top-level key ({}) was present in the import map", key.as_string())));
}
// 8. Return an import map whose imports are sortedAndNormalizedImports and whose scopes are sortedAndNormalizedScopes.
// 10. Return an import map whose imports are sortedAndNormalizedImports, whose scopes are sortedAndNormalizedScopes, and whose integrity are normalizedIntegrity.
ImportMap import_map;
import_map.set_imports(sorted_and_normalised_imports);
import_map.set_scopes(sorted_and_normalised_scopes);
import_map.set_integrity(normalised_integrity);
return import_map;
}
@ -209,4 +225,47 @@ WebIDL::ExceptionOr<HashMap<URL::URL, ModuleSpecifierMap>> sort_and_normalise_sc
return normalised;
}
// https://html.spec.whatwg.org/multipage/webappapis.html#normalizing-a-module-integrity-map
WebIDL::ExceptionOr<ModuleIntegrityMap> normalize_module_integrity_map(JS::Realm& realm, JS::Object& original_map, URL::URL base_url)
{
// 1. Let normalized be an empty ordered map.
ModuleIntegrityMap normalised;
// 2. For each key → value of originalMap:
for (auto& key : original_map.shape().property_table().keys()) {
auto value = TRY(original_map.get(key.as_string()));
// 1. Let resolvedURL be the result of resolving a URL-like module specifier given key and baseURL.
auto resolved_url = resolve_url_like_module_specifier(key.as_string(), base_url);
// 2. If resolvedURL is null, then:
if (!resolved_url.has_value()) {
// 1. The user agent may report a warning to the console indicating that the key failed to resolve.
auto& console = realm.intrinsics().console_object()->console();
console.output_debug_message(JS::Console::LogLevel::Warn,
TRY_OR_THROW_OOM(realm.vm(), String::formatted("Failed to resolve key ({})", key.as_string())));
// 2. Continue.
continue;
}
// 3. If value is not a string, then:
if (!value.is_string()) {
// 1. The user agent may report a warning to the console indicating that integrity metadata values need to be strings.
auto& console = realm.intrinsics().console_object()->console();
console.output_debug_message(JS::Console::LogLevel::Warn,
TRY_OR_THROW_OOM(realm.vm(), String::formatted("Integrity metadata value for '{}' needs to be a string", key.as_string())));
// 2. Continue.
continue;
}
// 4. Set normalized[resolvedURL] to value.
normalised.set(resolved_url.release_value(), value.as_string().byte_string());
}
// 3. Return normalized.
return normalised;
}
}

View file

@ -14,6 +14,7 @@
namespace Web::HTML {
using ModuleSpecifierMap = HashMap<ByteString, Optional<URL::URL>>;
using ModuleIntegrityMap = HashMap<URL::URL, ByteString>;
// https://html.spec.whatwg.org/multipage/webappapis.html#import-map
class ImportMap {
@ -28,14 +29,20 @@ public:
HashMap<URL::URL, ModuleSpecifierMap>& scopes() { return m_scopes; }
void set_scopes(HashMap<URL::URL, ModuleSpecifierMap> const& scopes) { m_scopes = scopes; }
ModuleIntegrityMap const& integrity() const { return m_integrity; }
ModuleIntegrityMap integrity() { return m_integrity; }
void set_integrity(ModuleIntegrityMap const& integrity) { m_integrity = integrity; }
private:
ModuleSpecifierMap m_imports;
HashMap<URL::URL, ModuleSpecifierMap> m_scopes;
ModuleIntegrityMap m_integrity;
};
WebIDL::ExceptionOr<ImportMap> parse_import_map_string(JS::Realm& realm, ByteString const& input, URL::URL base_url);
WebIDL::ExceptionOr<Optional<DeprecatedFlyString>> normalise_specifier_key(JS::Realm& realm, DeprecatedFlyString specifier_key, URL::URL base_url);
WebIDL::ExceptionOr<ModuleSpecifierMap> sort_and_normalise_module_specifier_map(JS::Realm& realm, JS::Object& original_map, URL::URL base_url);
WebIDL::ExceptionOr<HashMap<URL::URL, ModuleSpecifierMap>> sort_and_normalise_scopes(JS::Realm& realm, JS::Object& original_map, URL::URL base_url);
WebIDL::ExceptionOr<ModuleIntegrityMap> normalize_module_integrity_map(JS::Realm& realm, JS::Object& original_map, URL::URL base_url);
}