Serenity Operating System
at master 406 lines 20 kB view raw
1/* 2 * Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org> 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include <AK/Debug.h> 8#include <AK/JsonArray.h> 9#include <AK/JsonObject.h> 10#include <AK/JsonValue.h> 11#include <AK/Optional.h> 12#include <LibWeb/Loader/ResourceLoader.h> 13#include <LibWeb/WebDriver/Capabilities.h> 14#include <LibWeb/WebDriver/TimeoutsConfiguration.h> 15 16namespace Web::WebDriver { 17 18// https://w3c.github.io/webdriver/#dfn-deserialize-as-a-page-load-strategy 19static Response deserialize_as_a_page_load_strategy(JsonValue value) 20{ 21 // 1. If value is not a string return an error with error code invalid argument. 22 if (!value.is_string()) 23 return Error::from_code(ErrorCode::InvalidArgument, "Capability pageLoadStrategy must be a string"sv); 24 25 // 2. If there is no entry in the table of page load strategies with keyword value return an error with error code invalid argument. 26 if (!value.as_string().is_one_of("none"sv, "eager"sv, "normal"sv)) 27 return Error::from_code(ErrorCode::InvalidArgument, "Invalid pageLoadStrategy capability"sv); 28 29 // 3. Return success with data value. 30 return value; 31} 32 33// https://w3c.github.io/webdriver/#dfn-deserialize-as-an-unhandled-prompt-behavior 34static Response deserialize_as_an_unhandled_prompt_behavior(JsonValue value) 35{ 36 // 1. If value is not a string return an error with error code invalid argument. 37 if (!value.is_string()) 38 return Error::from_code(ErrorCode::InvalidArgument, "Capability unhandledPromptBehavior must be a string"sv); 39 40 // 2. If value is not present as a keyword in the known prompt handling approaches table return an error with error code invalid argument. 41 if (!value.as_string().is_one_of("dismiss"sv, "accept"sv, "dismiss and notify"sv, "accept and notify"sv, "ignore"sv)) 42 return Error::from_code(ErrorCode::InvalidArgument, "Invalid pageLoadStrategy capability"sv); 43 44 // 3. Return success with data value. 45 return value; 46} 47 48static Response deserialize_as_ladybird_options(JsonValue value) 49{ 50 if (!value.is_object()) 51 return Error::from_code(ErrorCode::InvalidArgument, "Extension capability serenity:ladybird must be an object"sv); 52 53 auto const& object = value.as_object(); 54 55 if (auto headless = object.get("headless"sv); headless.has_value() && !headless->is_bool()) 56 return Error::from_code(ErrorCode::InvalidArgument, "Extension capability serenity:ladybird/headless must be a boolean"sv); 57 58 return value; 59} 60 61static JsonObject default_ladybird_options() 62{ 63 JsonObject options; 64 options.set("headless"sv, false); 65 66 return options; 67} 68 69// https://w3c.github.io/webdriver/#dfn-validate-capabilities 70static ErrorOr<JsonObject, Error> validate_capabilities(JsonValue const& capability) 71{ 72 // 1. If capability is not a JSON Object return an error with error code invalid argument. 73 if (!capability.is_object()) 74 return Error::from_code(ErrorCode::InvalidArgument, "Capability is not an Object"sv); 75 76 // 2. Let result be an empty JSON Object. 77 JsonObject result; 78 79 // 3. For each enumerable own property in capability, run the following substeps: 80 TRY(capability.as_object().try_for_each_member([&](auto const& name, auto const& value) -> ErrorOr<void, Error> { 81 // a. Let name be the name of the property. 82 // b. Let value be the result of getting a property named name from capability. 83 84 // c. Run the substeps of the first matching condition: 85 JsonValue deserialized; 86 87 // -> value is null 88 if (value.is_null()) { 89 // Let deserialized be set to null. 90 } 91 92 // -> name equals "acceptInsecureCerts" 93 else if (name == "acceptInsecureCerts"sv) { 94 // If value is not a boolean return an error with error code invalid argument. Otherwise, let deserialized be set to value 95 if (!value.is_bool()) 96 return Error::from_code(ErrorCode::InvalidArgument, "Capability acceptInsecureCerts must be a boolean"sv); 97 deserialized = value; 98 } 99 100 // -> name equals "browserName" 101 // -> name equals "browserVersion" 102 // -> name equals "platformName" 103 else if (name.is_one_of("browserName"sv, "browser_version"sv, "platformName"sv)) { 104 // If value is not a string return an error with error code invalid argument. Otherwise, let deserialized be set to value. 105 if (!value.is_string()) 106 return Error::from_code(ErrorCode::InvalidArgument, DeprecatedString::formatted("Capability {} must be a string", name)); 107 deserialized = value; 108 } 109 110 // -> name equals "pageLoadStrategy" 111 else if (name == "pageLoadStrategy"sv) { 112 // Let deserialized be the result of trying to deserialize as a page load strategy with argument value. 113 deserialized = TRY(deserialize_as_a_page_load_strategy(value)); 114 } 115 116 // FIXME: -> name equals "proxy" 117 // FIXME: Let deserialized be the result of trying to deserialize as a proxy with argument value. 118 119 // -> name equals "strictFileInteractability" 120 else if (name == "strictFileInteractability"sv) { 121 // If value is not a boolean return an error with error code invalid argument. Otherwise, let deserialized be set to value 122 if (!value.is_bool()) 123 return Error::from_code(ErrorCode::InvalidArgument, "Capability strictFileInteractability must be a boolean"sv); 124 deserialized = value; 125 } 126 127 // -> name equals "timeouts" 128 else if (name == "timeouts"sv) { 129 // Let deserialized be the result of trying to JSON deserialize as a timeouts configuration the value. 130 auto timeouts = TRY(json_deserialize_as_a_timeouts_configuration(value)); 131 deserialized = JsonValue { timeouts_object(timeouts) }; 132 } 133 134 // -> name equals "unhandledPromptBehavior" 135 else if (name == "unhandledPromptBehavior"sv) { 136 // Let deserialized be the result of trying to deserialize as an unhandled prompt behavior with argument value. 137 deserialized = TRY(deserialize_as_an_unhandled_prompt_behavior(value)); 138 } 139 140 // FIXME: -> name is the name of an additional WebDriver capability 141 // FIXME: Let deserialized be the result of trying to run the additional capability deserialization algorithm for the extension capability corresponding to name, with argument value. 142 143 // -> name is the key of an extension capability 144 // If name is known to the implementation, let deserialized be the result of trying to deserialize value in an implementation-specific way. Otherwise, let deserialized be set to value. 145 else if (name == "serenity:ladybird"sv) { 146 deserialized = TRY(deserialize_as_ladybird_options(value)); 147 } 148 149 // -> The remote end is an endpoint node 150 else { 151 // Return an error with error code invalid argument. 152 return Error::from_code(ErrorCode::InvalidArgument, DeprecatedString::formatted("Unrecognized capability: {}", name)); 153 } 154 155 // d. If deserialized is not null, set a property on result with name name and value deserialized. 156 if (!deserialized.is_null()) 157 result.set(name, move(deserialized)); 158 159 return {}; 160 })); 161 162 // 4. Return success with data result. 163 return result; 164} 165 166// https://w3c.github.io/webdriver/#dfn-merging-capabilities 167static ErrorOr<JsonObject, Error> merge_capabilities(JsonObject const& primary, Optional<JsonObject const&> const& secondary) 168{ 169 // 1. Let result be a new JSON Object. 170 JsonObject result; 171 172 // 2. For each enumerable own property in primary, run the following substeps: 173 primary.for_each_member([&](auto const& name, auto const& value) { 174 // a. Let name be the name of the property. 175 // b. Let value be the result of getting a property named name from primary. 176 177 // c. Set a property on result with name name and value value. 178 result.set(name, value); 179 }); 180 181 // 3. If secondary is undefined, return result. 182 if (!secondary.has_value()) 183 return result; 184 185 // 4. For each enumerable own property in secondary, run the following substeps: 186 TRY(secondary->try_for_each_member([&](auto const& name, auto const& value) -> ErrorOr<void, Error> { 187 // a. Let name be the name of the property. 188 // b. Let value be the result of getting a property named name from secondary. 189 190 // c. Let primary value be the result of getting the property name from primary. 191 auto primary_value = primary.get(name); 192 193 // d. If primary value is not undefined, return an error with error code invalid argument. 194 if (primary_value.has_value()) 195 return Error::from_code(ErrorCode::InvalidArgument, DeprecatedString::formatted("Unable to merge capability {}", name)); 196 197 // e. Set a property on result with name name and value value. 198 result.set(name, value); 199 return {}; 200 })); 201 202 // 5. Return result. 203 return result; 204} 205 206static bool matches_browser_version(StringView requested_version, StringView required_version) 207{ 208 // FIXME: Handle relative (>, >=, <. <=) comparisons. For now, require an exact match. 209 return requested_version == required_version; 210} 211 212static bool matches_platform_name(StringView requested_platform_name, StringView required_platform_name) 213{ 214 if (requested_platform_name == required_platform_name) 215 return true; 216 217 // The following platform names are in common usage with well-understood semantics and, when matching capabilities, greatest interoperability can be achieved by honoring them as valid synonyms for well-known Operating Systems: 218 // "linux" Any server or desktop system based upon the Linux kernel. 219 // "mac" Any version of Apple’s macOS. 220 // "windows" Any version of Microsoft Windows, including desktop and mobile versions. 221 // This list is not exhaustive. 222 223 // NOTE: Of the synonyms listed in the spec, the only one that differs for us is macOS. 224 // Further, we are allowed to handle synonyms for SerenityOS. 225 if (requested_platform_name == "mac"sv && required_platform_name == "macos"sv) 226 return true; 227 if (requested_platform_name == "serenity"sv && required_platform_name == "serenityos"sv) 228 return true; 229 return false; 230} 231 232// https://w3c.github.io/webdriver/#dfn-matching-capabilities 233static JsonValue match_capabilities(JsonObject const& capabilities) 234{ 235 static auto browser_name = StringView { BROWSER_NAME, strlen(BROWSER_NAME) }.to_lowercase_string(); 236 static auto platform_name = StringView { OS_STRING, strlen(OS_STRING) }.to_lowercase_string(); 237 238 // 1. Let matched capabilities be a JSON Object with the following entries: 239 JsonObject matched_capabilities; 240 // "browserName" 241 // ASCII Lowercase name of the user agent as a string. 242 matched_capabilities.set("browserName"sv, browser_name); 243 // "browserVersion" 244 // The user agent version, as a string. 245 matched_capabilities.set("browserVersion"sv, BROWSER_VERSION); 246 // "platformName" 247 // ASCII Lowercase name of the current platform as a string. 248 matched_capabilities.set("platformName"sv, platform_name); 249 // "acceptInsecureCerts" 250 // Boolean initially set to false, indicating the session will not implicitly trust untrusted or self-signed TLS certificates on navigation. 251 matched_capabilities.set("acceptInsecureCerts"sv, false); 252 // "strictFileInteractability" 253 // Boolean initially set to false, indicating that interactability checks will be applied to <input type=file>. 254 matched_capabilities.set("strictFileInteractability"sv, false); 255 // "setWindowRect" 256 // Boolean indicating whether the remote end supports all of the resizing and positioning commands. 257 matched_capabilities.set("setWindowRect"sv, true); 258 259 // 2. Optionally add extension capabilities as entries to matched capabilities. The values of these may be elided, and there is no requirement that all extension capabilities be added. 260 matched_capabilities.set("serenity:ladybird"sv, default_ladybird_options()); 261 262 // 3. For each name and value corresponding to capability’s own properties: 263 auto result = capabilities.try_for_each_member([&](auto const& name, auto const& value) -> ErrorOr<void> { 264 // a. Let match value equal value. 265 266 // b. Run the substeps of the first matching name: 267 // -> "browserName" 268 if (name == "browserName"sv) { 269 // If value is not a string equal to the "browserName" entry in matched capabilities, return success with data null. 270 if (value.as_string() != matched_capabilities.get_deprecated_string(name).value()) 271 return AK::Error::from_string_view("browserName"sv); 272 } 273 // -> "browserVersion" 274 else if (name == "browserVersion"sv) { 275 // Compare value to the "browserVersion" entry in matched capabilities using an implementation-defined comparison algorithm. The comparison is to accept a value that places constraints on the version using the "<", "<=", ">", and ">=" operators. 276 // If the two values do not match, return success with data null. 277 if (!matches_browser_version(value.as_string(), matched_capabilities.get_deprecated_string(name).value())) 278 return AK::Error::from_string_view("browserVersion"sv); 279 } 280 // -> "platformName" 281 else if (name == "platformName"sv) { 282 // If value is not a string equal to the "platformName" entry in matched capabilities, return success with data null. 283 if (!matches_platform_name(value.as_string(), matched_capabilities.get_deprecated_string(name).value())) 284 return AK::Error::from_string_view("platformName"sv); 285 } 286 // -> "acceptInsecureCerts" 287 else if (name == "acceptInsecureCerts"sv) { 288 // If value is true and the endpoint node does not support insecure TLS certificates, return success with data null. 289 if (value.as_bool()) 290 return AK::Error::from_string_view("acceptInsecureCerts"sv); 291 } 292 // -> "proxy" 293 else if (name == "proxy"sv) { 294 // FIXME: If the endpoint node does not allow the proxy it uses to be configured, or if the proxy configuration defined in value is not one that passes the endpoint node’s implementation-specific validity checks, return success with data null. 295 } 296 // -> Otherwise 297 else { 298 // FIXME: If name is the name of an additional WebDriver capability which defines a matched capability serialization algorithm, let match value be the result of running the matched capability serialization algorithm for capability name with argument value. 299 // FIXME: Otherwise, if name is the key of an extension capability, let match value be the result of trying implementation-specific steps to match on name with value. If the match is not successful, return success with data null. 300 } 301 302 // c. Set a property on matched capabilities with name name and value match value. 303 matched_capabilities.set(name, value); 304 return {}; 305 }); 306 307 if (result.is_error()) { 308 dbgln_if(WEBDRIVER_DEBUG, "Failed to match capability: {}", result.error()); 309 return JsonValue {}; 310 } 311 312 // 4. Return success with data matched capabilities. 313 return matched_capabilities; 314} 315 316// https://w3c.github.io/webdriver/#dfn-capabilities-processing 317Response process_capabilities(JsonValue const& parameters) 318{ 319 if (!parameters.is_object()) 320 return Error::from_code(ErrorCode::InvalidArgument, "Session parameters is not an object"sv); 321 322 // 1. Let capabilities request be the result of getting the property "capabilities" from parameters. 323 // a. If capabilities request is not a JSON Object, return error with error code invalid argument. 324 auto maybe_capabilities_request = parameters.as_object().get_object("capabilities"sv); 325 if (!maybe_capabilities_request.has_value()) 326 return Error::from_code(ErrorCode::InvalidArgument, "Capabilities is not an object"sv); 327 328 auto const& capabilities_request = maybe_capabilities_request.value(); 329 330 // 2. Let required capabilities be the result of getting the property "alwaysMatch" from capabilities request. 331 // a. If required capabilities is undefined, set the value to an empty JSON Object. 332 JsonObject required_capabilities; 333 334 if (auto capability = capabilities_request.get("alwaysMatch"sv); capability.has_value()) { 335 // b. Let required capabilities be the result of trying to validate capabilities with argument required capabilities. 336 required_capabilities = TRY(validate_capabilities(*capability)); 337 } 338 339 // 3. Let all first match capabilities be the result of getting the property "firstMatch" from capabilities request. 340 JsonArray all_first_match_capabilities; 341 342 if (auto capabilities = capabilities_request.get("firstMatch"sv); capabilities.has_value()) { 343 // b. If all first match capabilities is not a JSON List with one or more entries, return error with error code invalid argument. 344 if (!capabilities->is_array() || capabilities->as_array().is_empty()) 345 return Error::from_code(ErrorCode::InvalidArgument, "Capability firstMatch must be an array with at least one entry"sv); 346 347 all_first_match_capabilities = capabilities->as_array(); 348 } else { 349 // a. If all first match capabilities is undefined, set the value to a JSON List with a single entry of an empty JSON Object. 350 all_first_match_capabilities.append(JsonObject {}); 351 } 352 353 // 4. Let validated first match capabilities be an empty JSON List. 354 JsonArray validated_first_match_capabilities; 355 validated_first_match_capabilities.ensure_capacity(all_first_match_capabilities.size()); 356 357 // 5. For each first match capabilities corresponding to an indexed property in all first match capabilities: 358 TRY(all_first_match_capabilities.try_for_each([&](auto const& first_match_capabilities) -> ErrorOr<void, Error> { 359 // a. Let validated capabilities be the result of trying to validate capabilities with argument first match capabilities. 360 auto validated_capabilities = TRY(validate_capabilities(first_match_capabilities)); 361 362 // b. Append validated capabilities to validated first match capabilities. 363 validated_first_match_capabilities.append(move(validated_capabilities)); 364 return {}; 365 })); 366 367 // 6. Let merged capabilities be an empty List. 368 JsonArray merged_capabilities; 369 merged_capabilities.ensure_capacity(validated_first_match_capabilities.size()); 370 371 // 7. For each first match capabilities corresponding to an indexed property in validated first match capabilities: 372 TRY(validated_first_match_capabilities.try_for_each([&](auto const& first_match_capabilities) -> ErrorOr<void, Error> { 373 // a. Let merged be the result of trying to merge capabilities with required capabilities and first match capabilities as arguments. 374 auto merged = TRY(merge_capabilities(required_capabilities, first_match_capabilities.as_object())); 375 376 // b. Append merged to merged capabilities. 377 merged_capabilities.append(move(merged)); 378 return {}; 379 })); 380 381 // 8. For each capabilities corresponding to an indexed property in merged capabilities: 382 for (auto const& capabilities : merged_capabilities.values()) { 383 // a. Let matched capabilities be the result of trying to match capabilities with capabilities as an argument. 384 auto matched_capabilities = match_capabilities(capabilities.as_object()); 385 386 // b. If matched capabilities is not null, return success with data matched capabilities. 387 if (!matched_capabilities.is_null()) 388 return matched_capabilities; 389 } 390 391 // 9. Return success with data null. 392 return JsonValue {}; 393} 394 395LadybirdOptions::LadybirdOptions(JsonObject const& capabilities) 396{ 397 auto options = capabilities.get_object("serenity:ladybird"sv); 398 if (!options.has_value()) 399 return; 400 401 auto headless = options->get_bool("headless"sv); 402 if (headless.has_value()) 403 this->headless = headless.value(); 404} 405 406}