Capabilities.cpp 22 KB

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