diff options
author | Luke Wilde <lukew@serenityos.org> | 2023-02-08 23:35:52 +0000 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2023-02-10 22:18:19 +0000 |
commit | 349c126d8d5547b323e71bebe074f4abaec02f3c (patch) | |
tree | 4147a6c7f9644a27b23dcc44ec9021204b00b07b /Userland/Libraries/LibWeb/Fetch | |
parent | d9d556fbabd58cac630cd2e28c77f499460fb724 (diff) | |
download | serenity-349c126d8d5547b323e71bebe074f4abaec02f3c.zip |
LibWeb/Fetch: Implement CORS preflight
The main things missing is the CORS preflight cache and making
extract_header_list_values properly parse, validate and return split
values for the Access-Control headers.
Diffstat (limited to 'Userland/Libraries/LibWeb/Fetch')
-rw-r--r-- | Userland/Libraries/LibWeb/Fetch/Fetching/Fetching.cpp | 282 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/Fetch/Fetching/Fetching.h | 1 |
2 files changed, 266 insertions, 17 deletions
diff --git a/Userland/Libraries/LibWeb/Fetch/Fetching/Fetching.cpp b/Userland/Libraries/LibWeb/Fetch/Fetching/Fetching.cpp index bba71ef085..2c1829aee8 100644 --- a/Userland/Libraries/LibWeb/Fetch/Fetching/Fetching.cpp +++ b/Userland/Libraries/LibWeb/Fetch/Fetching/Fetching.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2022, Linus Groh <linusg@serenityos.org> + * Copyright (c) 2023, Luke Wilde <lukew@serenityos.org> * * SPDX-License-Identifier: BSD-2-Clause */ @@ -812,6 +813,8 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<PendingResponse>> http_fetch(JS::Realm& rea JS::GCPtr<PendingResponse> pending_actual_response; + auto returned_pending_response = PendingResponse::create(vm, request); + // 5. If response is null, then: if (!response) { // 1. If makeCORSPreflight is true and one of these conditions is true: @@ -819,31 +822,57 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<PendingResponse>> http_fetch(JS::Realm& rea // CORS-preflight fetch which, if successful, populates the cache. The purpose of the CORS-preflight // fetch is to ensure the fetched resource is familiar with the CORS protocol. The cache is there to // minimize the number of CORS-preflight fetches. + JS::GCPtr<PendingResponse> pending_preflight_response; if (make_cors_preflight == MakeCORSPreflight::Yes && ( - // FIXME: - There is no method cache entry match for request’s method using request, and either request’s - // method is not a CORS-safelisted method or request’s use-CORS-preflight flag is set. - false - // FIXME: - There is at least one item in the CORS-unsafe request-header names with request’s header list for - // which there is no header-name cache entry match using request. - || false)) { - // FIXME: 1. Let preflightResponse be the result of running CORS-preflight fetch given request. - // FIXME: 2. If preflightResponse is a network error, then return preflightResponse. + // - There is no method cache entry match for request’s method using request, and either request’s + // method is not a CORS-safelisted method or request’s use-CORS-preflight flag is set. + // FIXME: We currently have no cache, so there will always be no method cache entry. + (!Infrastructure::is_cors_safelisted_method(request->method()) || request->use_cors_preflight()) + // - There is at least one item in the CORS-unsafe request-header names with request’s header list for + // which there is no header-name cache entry match using request. + // FIXME: We currently have no cache, so there will always be no header-name cache entry. + || !TRY_OR_THROW_OOM(vm, Infrastructure::get_cors_unsafe_header_names(request->header_list())).is_empty())) { + // 1. Let preflightResponse be the result of running CORS-preflight fetch given request. + pending_preflight_response = TRY(cors_preflight_fetch(realm, request)); + + // NOTE: Step 2 is performed in pending_preflight_response's load callback below. } - // 2. If request’s redirect mode is "follow", then set request’s service-workers mode to "none". - // NOTE: Redirects coming from the network (as opposed to from a service worker) are not to be exposed to a - // service worker. - if (request->redirect_mode() == Infrastructure::Request::RedirectMode::Follow) - request->set_service_workers_mode(Infrastructure::Request::ServiceWorkersMode::None); + auto fetch_main_content = [request = JS::make_handle(request), realm = JS::make_handle(realm), fetch_params = JS::make_handle(const_cast<Infrastructure::FetchParams&>(fetch_params))]() -> WebIDL::ExceptionOr<JS::NonnullGCPtr<PendingResponse>> { + // 2. If request’s redirect mode is "follow", then set request’s service-workers mode to "none". + // NOTE: Redirects coming from the network (as opposed to from a service worker) are not to be exposed to a + // service worker. + if (request->redirect_mode() == Infrastructure::Request::RedirectMode::Follow) + request->set_service_workers_mode(Infrastructure::Request::ServiceWorkersMode::None); + + // 3. Set response and actualResponse to the result of running HTTP-network-or-cache fetch given fetchParams. + return http_network_or_cache_fetch(*realm, *fetch_params); + }; + + if (pending_preflight_response) { + pending_actual_response = PendingResponse::create(vm, request); + pending_preflight_response->when_loaded([returned_pending_response, pending_actual_response, fetch_main_content = move(fetch_main_content)](JS::NonnullGCPtr<Infrastructure::Response> preflight_response) { + dbgln_if(WEB_FETCH_DEBUG, "Fetch: Running 'HTTP fetch' pending_preflight_response load callback"); - // 3. Set response and actualResponse to the result of running HTTP-network-or-cache fetch given fetchParams. - pending_actual_response = TRY(http_network_or_cache_fetch(realm, fetch_params)); + // 2. If preflightResponse is a network error, then return preflightResponse. + if (preflight_response->is_network_error()) { + returned_pending_response->resolve(preflight_response); + return; + } + + auto pending_main_content_response = TRY_OR_IGNORE(fetch_main_content()); + pending_main_content_response->when_loaded([pending_actual_response](JS::NonnullGCPtr<Infrastructure::Response> main_content_response) { + dbgln_if(WEB_FETCH_DEBUG, "Fetch: Running 'HTTP fetch' pending_main_content_response load callback"); + pending_actual_response->resolve(main_content_response); + }); + }); + } else { + pending_actual_response = TRY(fetch_main_content()); + } } else { pending_actual_response = PendingResponse::create(vm, request, Infrastructure::Response::create(vm)); } - auto returned_pending_response = PendingResponse::create(vm, request); - pending_actual_response->when_loaded([&realm, &vm, &fetch_params, request, response, actual_response, returned_pending_response, response_was_null = !response](JS::NonnullGCPtr<Infrastructure::Response> resolved_actual_response) mutable { dbgln_if(WEB_FETCH_DEBUG, "Fetch: Running 'HTTP fetch' pending_actual_response load callback"); if (response_was_null) { @@ -1646,4 +1675,223 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<PendingResponse>> nonstandard_resource_load return pending_response; } +// https://fetch.spec.whatwg.org/#cors-preflight-fetch-0 +WebIDL::ExceptionOr<JS::NonnullGCPtr<PendingResponse>> cors_preflight_fetch(JS::Realm& realm, Infrastructure::Request const& request) +{ + dbgln_if(WEB_FETCH_DEBUG, "Fetch: Running 'CORS-preflight fetch' with request @ {}", &request); + + auto& vm = realm.vm(); + + // 1. Let preflight be a new request whose method is `OPTIONS`, URL list is a clone of request’s URL list, initiator is + // request’s initiator, destination is request’s destination, origin is request’s origin, referrer is request’s referrer, + // referrer policy is request’s referrer policy, mode is "cors", and response tainting is "cors". + auto preflight = Fetch::Infrastructure::Request::create(vm); + preflight->set_method(TRY_OR_THROW_OOM(vm, ByteBuffer::copy("OPTIONS"sv.bytes()))); + preflight->set_url_list(request.url_list()); + preflight->set_initiator(request.initiator()); + preflight->set_destination(request.destination()); + preflight->set_origin(request.origin()); + preflight->set_referrer(request.referrer()); + preflight->set_referrer_policy(request.referrer_policy()); + preflight->set_mode(Infrastructure::Request::Mode::CORS); + preflight->set_response_tainting(Infrastructure::Request::ResponseTainting::CORS); + + // 2. Append (`Accept`, `*/*`) to preflight’s header list. + auto temp_header = TRY_OR_THROW_OOM(vm, Infrastructure::Header::from_string_pair("Accept"sv, "*/*"sv)); + TRY_OR_THROW_OOM(vm, preflight->header_list()->append(move(temp_header))); + + // 3. Append (`Access-Control-Request-Method`, request’s method) to preflight’s header list. + temp_header = TRY_OR_THROW_OOM(vm, Infrastructure::Header::from_string_pair("Access-Control-Request-Method"sv, request.method())); + TRY_OR_THROW_OOM(vm, preflight->header_list()->append(move(temp_header))); + + // 4. Let headers be the CORS-unsafe request-header names with request’s header list. + auto headers = TRY_OR_THROW_OOM(vm, Infrastructure::get_cors_unsafe_header_names(request.header_list())); + + // 5. If headers is not empty, then: + if (!headers.is_empty()) { + // 1. Let value be the items in headers separated from each other by `,`. + // NOTE: This intentionally does not use combine, as 0x20 following 0x2C is not the way this was implemented, + // for better or worse. + ByteBuffer value; + + bool first = true; + for (auto const& header : headers) { + if (!first) + TRY_OR_THROW_OOM(vm, value.try_append(',')); + TRY_OR_THROW_OOM(vm, value.try_append(header)); + first = false; + } + + // 2. Append (`Access-Control-Request-Headers`, value) to preflight’s header list. + temp_header = Infrastructure::Header { + .name = TRY_OR_THROW_OOM(vm, ByteBuffer::copy("Access-Control-Request-Headers"sv.bytes())), + .value = move(value), + }; + TRY_OR_THROW_OOM(vm, preflight->header_list()->append(move(temp_header))); + } + + // 6. Let response be the result of running HTTP-network-or-cache fetch given a new fetch params whose request is preflight. + // FIXME: The spec doesn't say anything about timing_info here, but FetchParams requires a non-null FetchTimingInfo object. + auto timing_info = Infrastructure::FetchTimingInfo::create(vm); + auto fetch_params = Infrastructure::FetchParams::create(vm, preflight, timing_info); + + auto returned_pending_response = PendingResponse::create(vm, request); + + auto preflight_response = TRY(http_network_or_cache_fetch(realm, fetch_params)); + + preflight_response->when_loaded([&vm, &request, returned_pending_response](JS::NonnullGCPtr<Infrastructure::Response> response) { + dbgln_if(WEB_FETCH_DEBUG, "Fetch: Running 'CORS-preflight fetch' preflight_response load callback"); + + // 7. If a CORS check for request and response returns success and response’s status is an ok status, then: + // NOTE: The CORS check is done on request rather than preflight to ensure the correct credentials mode is used. + if (TRY_OR_IGNORE(cors_check(request, response)) && Infrastructure::is_ok_status(response->status())) { + // 1. Let methods be the result of extracting header list values given `Access-Control-Allow-Methods` and response’s header list. + auto methods_or_failure = TRY_OR_IGNORE(Infrastructure::extract_header_list_values("Access-Control-Allow-Methods"sv.bytes(), response->header_list())); + + // 2. Let headerNames be the result of extracting header list values given `Access-Control-Allow-Headers` and + // response’s header list. + auto header_names_or_failure = TRY_OR_IGNORE(Infrastructure::extract_header_list_values("Access-Control-Allow-Headers"sv.bytes(), response->header_list())); + + // 3. If either methods or headerNames is failure, return a network error. + if (methods_or_failure.has<Infrastructure::ExtractHeaderParseFailure>()) { + returned_pending_response->resolve(Infrastructure::Response::network_error(vm, "The Access-Control-Allow-Methods in the CORS-preflight response is syntactically invalid"sv)); + return; + } + + if (header_names_or_failure.has<Infrastructure::ExtractHeaderParseFailure>()) { + returned_pending_response->resolve(Infrastructure::Response::network_error(vm, "The Access-Control-Allow-Headers in the CORS-preflight response is syntactically invalid"sv)); + return; + } + + // NOTE: We treat "methods_or_failure" being `Empty` as empty Vector here. + auto methods = methods_or_failure.has<Vector<ByteBuffer>>() ? methods_or_failure.get<Vector<ByteBuffer>>() : Vector<ByteBuffer> {}; + + // NOTE: We treat "header_names_or_failure" being `Empty` as empty Vector here. + auto header_names = header_names_or_failure.has<Vector<ByteBuffer>>() ? header_names_or_failure.get<Vector<ByteBuffer>>() : Vector<ByteBuffer> {}; + + // FIXME: Remove this once extract_header_list_values validates the header and returns multiple values. + if (!methods.is_empty()) { + VERIFY(methods.size() == 1); + + auto split_methods = StringView { methods.first() }.split_view(','); + Vector<ByteBuffer> trimmed_methods; + + for (auto const& method : split_methods) { + auto trimmed_method = method.trim(" \t"sv); + auto trimmed_method_as_byte_buffer = TRY_OR_IGNORE(ByteBuffer::copy(trimmed_method.bytes())); + TRY_OR_IGNORE(trimmed_methods.try_append(move(trimmed_method_as_byte_buffer))); + } + + methods = move(trimmed_methods); + } + + // FIXME: Remove this once extract_header_list_values validates the header and returns multiple values. + if (!header_names.is_empty()) { + VERIFY(header_names.size() == 1); + + auto split_header_names = StringView { header_names.first() }.split_view(','); + Vector<ByteBuffer> trimmed_header_names; + + for (auto const& header_name : split_header_names) { + auto trimmed_header_name = header_name.trim(" \t"sv); + auto trimmed_header_name_as_byte_buffer = TRY_OR_IGNORE(ByteBuffer::copy(trimmed_header_name.bytes())); + TRY_OR_IGNORE(trimmed_header_names.try_append(move(trimmed_header_name_as_byte_buffer))); + } + + header_names = move(trimmed_header_names); + } + + // 4. If methods is null and request’s use-CORS-preflight flag is set, then set methods to a new list containing request’s method. + // NOTE: This ensures that a CORS-preflight fetch that happened due to request’s use-CORS-preflight flag being set is cached. + if (methods.is_empty() && request.use_cors_preflight()) + methods = Vector { TRY_OR_IGNORE(ByteBuffer::copy(request.method())) }; + + // 5. If request’s method is not in methods, request’s method is not a CORS-safelisted method, and request’s credentials mode + // is "include" or methods does not contain `*`, then return a network error. + if (!methods.contains_slow(request.method()) && !Infrastructure::is_cors_safelisted_method(request.method())) { + if (request.credentials_mode() == Infrastructure::Request::CredentialsMode::Include) { + returned_pending_response->resolve(Infrastructure::Response::network_error(vm, DeprecatedString::formatted("Non-CORS-safelisted method '{}' not found in the CORS-preflight response's Access-Control-Allow-Methods header (the header may be missing). '*' is not allowed as the main request includes credentials."sv, StringView { request.method() }))); + return; + } + + if (!methods.contains_slow("*"sv.bytes())) { + returned_pending_response->resolve(Infrastructure::Response::network_error(vm, DeprecatedString::formatted("Non-CORS-safelisted method '{}' not found in the CORS-preflight response's Access-Control-Allow-Methods header and there was no '*' entry. The header may be missing."sv, StringView { request.method() }))); + return; + } + } + + // 6. If one of request’s header list’s names is a CORS non-wildcard request-header name and is not a byte-case-insensitive match + // for an item in headerNames, then return a network error. + for (auto const& header : *request.header_list()) { + if (Infrastructure::is_cors_non_wildcard_request_header_name(header.name)) { + bool is_in_header_names = false; + + for (auto const& allowed_header_name : header_names) { + if (StringView { allowed_header_name }.equals_ignoring_case(header.name)) { + is_in_header_names = true; + break; + } + } + + if (!is_in_header_names) { + returned_pending_response->resolve(Infrastructure::Response::network_error(vm, DeprecatedString::formatted("Main request contains the header '{}' that is not specified in the CORS-preflight response's Access-Control-Allow-Headers header (the header may be missing). '*' does not capture this header."sv, StringView { header.name }))); + return; + } + } + } + + // 7. For each unsafeName of the CORS-unsafe request-header names with request’s header list, if unsafeName is not a + // byte-case-insensitive match for an item in headerNames and request’s credentials mode is "include" or headerNames + // does not contain `*`, return a network error. + auto unsafe_names = TRY_OR_IGNORE(Infrastructure::get_cors_unsafe_header_names(request.header_list())); + for (auto const& unsafe_name : unsafe_names) { + bool is_in_header_names = false; + + for (auto const& header_name : header_names) { + if (StringView { unsafe_name }.equals_ignoring_case(header_name)) { + is_in_header_names = true; + break; + } + } + + if (!is_in_header_names) { + if (request.credentials_mode() == Infrastructure::Request::CredentialsMode::Include) { + returned_pending_response->resolve(Infrastructure::Response::network_error(vm, DeprecatedString::formatted("CORS-unsafe request-header '{}' not found in the CORS-preflight response's Access-Control-Allow-Headers header (the header may be missing). '*' is not allowed as the main request includes credentials."sv, StringView { unsafe_name }))); + return; + } + + if (!header_names.contains_slow("*"sv.bytes())) { + returned_pending_response->resolve(Infrastructure::Response::network_error(vm, DeprecatedString::formatted("CORS-unsafe request-header '{}' not found in the CORS-preflight response's Access-Control-Allow-Headers header and there was no '*' entry. The header may be missing."sv, StringView { unsafe_name }))); + return; + } + } + } + + // FIXME: 8. Let max-age be the result of extracting header list values given `Access-Control-Max-Age` and response’s header list. + // FIXME: 9. If max-age is failure or null, then set max-age to 5. + // FIXME: 10. If max-age is greater than an imposed limit on max-age, then set max-age to the imposed limit. + + // 11. If the user agent does not provide for a cache, then return response. + // NOTE: Since we don't currently have a cache, this is always true. + returned_pending_response->resolve(response); + return; + + // FIXME: 12. For each method in methods for which there is a method cache entry match using request, set matching entry’s max-age + // to max-age. + // FIXME: 13. For each method in methods for which there is no method cache entry match using request, create a new cache entry + // with request, max-age, method, and null. + // FIXME: 14. For each headerName in headerNames for which there is a header-name cache entry match using request, set matching + // entry’s max-age to max-age. + // FIXME: 15. For each headerName in headerNames for which there is no header-name cache entry match using request, create a + // new cache entry with request, max-age, null, and headerName. + // FIXME: 16. Return response. + } + + // 8. Otherwise, return a network error. + returned_pending_response->resolve(Infrastructure::Response::network_error(vm, "CORS-preflight check failed")); + }); + + return returned_pending_response; +} + } diff --git a/Userland/Libraries/LibWeb/Fetch/Fetching/Fetching.h b/Userland/Libraries/LibWeb/Fetch/Fetching/Fetching.h index ea7a5bc210..04d4d46ee4 100644 --- a/Userland/Libraries/LibWeb/Fetch/Fetching/Fetching.h +++ b/Userland/Libraries/LibWeb/Fetch/Fetching/Fetching.h @@ -37,5 +37,6 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<PendingResponse>> http_fetch(JS::Realm&, In WebIDL::ExceptionOr<JS::NonnullGCPtr<PendingResponse>> http_redirect_fetch(JS::Realm&, Infrastructure::FetchParams const&, Infrastructure::Response const&); WebIDL::ExceptionOr<JS::NonnullGCPtr<PendingResponse>> http_network_or_cache_fetch(JS::Realm&, Infrastructure::FetchParams const&, IsAuthenticationFetch is_authentication_fetch = IsAuthenticationFetch::No, IsNewConnectionFetch is_new_connection_fetch = IsNewConnectionFetch::No); WebIDL::ExceptionOr<JS::NonnullGCPtr<PendingResponse>> nonstandard_resource_loader_http_network_fetch(JS::Realm&, Infrastructure::FetchParams const&, IncludeCredentials include_credentials = IncludeCredentials::No, IsNewConnectionFetch is_new_connection_fetch = IsNewConnectionFetch::No); +WebIDL::ExceptionOr<JS::NonnullGCPtr<PendingResponse>> cors_preflight_fetch(JS::Realm&, Infrastructure::Request const&); } |