1use crate::common::MAX_SAFE_INTEGER;
6use crate::error::{ErrorStatus, WebDriverError, WebDriverResult};
7use serde_json::{Map, Value};
8use url::Url;
9
10pub type Capabilities = Map<String, Value>;
11
12pub trait BrowserCapabilities {
21 fn init(&mut self, _: &Capabilities);
25
26 fn browser_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>>;
28
29 fn browser_version(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>>;
31
32 fn compare_browser_version(&mut self, version: &str, comparison: &str)
38 -> WebDriverResult<bool>;
39
40 fn platform_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>>;
42
43 fn accept_insecure_certs(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
45
46 fn set_window_rect(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
49
50 fn strict_file_interactability(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
52
53 fn web_socket_url(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
55
56 fn webauthn_virtual_authenticators(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
58
59 fn webauthn_extension_uvm(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
62
63 fn webauthn_extension_prf(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
66
67 fn webauthn_extension_large_blob(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
70
71 fn webauthn_extension_cred_blob(&mut self, _: &Capabilities) -> WebDriverResult<bool>;
74
75 fn accept_proxy(
76 &mut self,
77 proxy_settings: &Map<String, Value>,
78 _: &Capabilities,
79 ) -> WebDriverResult<bool>;
80
81 fn validate_custom(&mut self, name: &str, value: &Value) -> WebDriverResult<()>;
87
88 fn accept_custom(
93 &mut self,
94 name: &str,
95 value: &Value,
96 merged: &Capabilities,
97 ) -> WebDriverResult<bool>;
98}
99
100pub trait CapabilitiesMatching {
105 fn match_browser<T: BrowserCapabilities>(
111 &self,
112 browser_capabilities: &mut T,
113 ) -> WebDriverResult<Option<Capabilities>>;
114}
115
116#[derive(Debug, PartialEq, Serialize, Deserialize)]
117pub struct SpecNewSessionParameters {
118 #[serde(default = "Capabilities::default")]
119 pub alwaysMatch: Capabilities,
120 #[serde(default = "firstMatch_default")]
121 pub firstMatch: Vec<Capabilities>,
122}
123
124impl Default for SpecNewSessionParameters {
125 fn default() -> Self {
126 SpecNewSessionParameters {
127 alwaysMatch: Capabilities::new(),
128 firstMatch: vec![Capabilities::new()],
129 }
130 }
131}
132
133fn firstMatch_default() -> Vec<Capabilities> {
134 vec![Capabilities::default()]
135}
136
137impl SpecNewSessionParameters {
138 fn validate<T: BrowserCapabilities>(
139 &self,
140 mut capabilities: Capabilities,
141 browser_capabilities: &mut T,
142 ) -> WebDriverResult<Capabilities> {
143 let null_entries = capabilities
145 .iter()
146 .filter(|&(_, value)| *value == Value::Null)
147 .map(|(k, _)| k.clone())
148 .collect::<Vec<String>>();
149 for key in null_entries {
150 capabilities.remove(&key);
151 }
152
153 for (key, value) in &capabilities {
154 match &**key {
155 x @ "acceptInsecureCerts"
156 | x @ "setWindowRect"
157 | x @ "strictFileInteractability"
158 | x @ "webSocketUrl"
159 | x @ "webauthn:virtualAuthenticators"
160 | x @ "webauthn:extension:uvm"
161 | x @ "webauthn:extension:prf"
162 | x @ "webauthn:extension:largeBlob"
163 | x @ "webauthn:extension:credBlob" => {
164 if !value.is_boolean() {
165 return Err(WebDriverError::new(
166 ErrorStatus::InvalidArgument,
167 format!("{} is not boolean: {}", x, value),
168 ));
169 }
170 }
171 x @ "browserName" | x @ "browserVersion" | x @ "platformName" => {
172 if !value.is_string() {
173 return Err(WebDriverError::new(
174 ErrorStatus::InvalidArgument,
175 format!("{} is not a string: {}", x, value),
176 ));
177 }
178 }
179 "pageLoadStrategy" => SpecNewSessionParameters::validate_page_load_strategy(value)?,
180 "proxy" => SpecNewSessionParameters::validate_proxy(value)?,
181 "timeouts" => SpecNewSessionParameters::validate_timeouts(value)?,
182 "unhandledPromptBehavior" => {
183 SpecNewSessionParameters::validate_unhandled_prompt_behavior(value)?
184 }
185 x => {
186 if !x.contains(':') {
187 return Err(WebDriverError::new(
188 ErrorStatus::InvalidArgument,
189 format!(
190 "{} is not the name of a known capability or extension capability",
191 x
192 ),
193 ));
194 } else {
195 browser_capabilities.validate_custom(x, value)?
196 }
197 }
198 }
199 }
200
201 if let Some(Value::Bool(false)) = capabilities.get(&"webSocketUrl".to_string()) {
203 capabilities.remove(&"webSocketUrl".to_string());
204 }
205
206 Ok(capabilities)
207 }
208
209 fn validate_page_load_strategy(value: &Value) -> WebDriverResult<()> {
210 match value {
211 Value::String(x) => match &**x {
212 "normal" | "eager" | "none" => {}
213 x => {
214 return Err(WebDriverError::new(
215 ErrorStatus::InvalidArgument,
216 format!("Invalid page load strategy: {}", x),
217 ))
218 }
219 },
220 _ => {
221 return Err(WebDriverError::new(
222 ErrorStatus::InvalidArgument,
223 "pageLoadStrategy is not a string",
224 ))
225 }
226 }
227 Ok(())
228 }
229
230 fn validate_proxy(proxy_value: &Value) -> WebDriverResult<()> {
231 let obj = try_opt!(
232 proxy_value.as_object(),
233 ErrorStatus::InvalidArgument,
234 "proxy is not an object"
235 );
236
237 for (key, value) in obj {
238 match &**key {
239 "proxyType" => match value.as_str() {
240 Some("pac") | Some("direct") | Some("autodetect") | Some("system")
241 | Some("manual") => {}
242 Some(x) => {
243 return Err(WebDriverError::new(
244 ErrorStatus::InvalidArgument,
245 format!("Invalid proxyType value: {}", x),
246 ))
247 }
248 None => {
249 return Err(WebDriverError::new(
250 ErrorStatus::InvalidArgument,
251 format!("proxyType is not a string: {}", value),
252 ))
253 }
254 },
255
256 "proxyAutoconfigUrl" => match value.as_str() {
257 Some(x) => {
258 Url::parse(x).map_err(|_| {
259 WebDriverError::new(
260 ErrorStatus::InvalidArgument,
261 format!("proxyAutoconfigUrl is not a valid URL: {}", x),
262 )
263 })?;
264 }
265 None => {
266 return Err(WebDriverError::new(
267 ErrorStatus::InvalidArgument,
268 "proxyAutoconfigUrl is not a string",
269 ))
270 }
271 },
272
273 "ftpProxy" => SpecNewSessionParameters::validate_host(value, "ftpProxy")?,
274 "httpProxy" => SpecNewSessionParameters::validate_host(value, "httpProxy")?,
275 "noProxy" => SpecNewSessionParameters::validate_no_proxy(value)?,
276 "sslProxy" => SpecNewSessionParameters::validate_host(value, "sslProxy")?,
277 "socksProxy" => SpecNewSessionParameters::validate_host(value, "socksProxy")?,
278 "socksVersion" => {
279 if !value.is_number() {
280 return Err(WebDriverError::new(
281 ErrorStatus::InvalidArgument,
282 format!("socksVersion is not a number: {}", value),
283 ));
284 }
285 }
286
287 x => {
288 return Err(WebDriverError::new(
289 ErrorStatus::InvalidArgument,
290 format!("Invalid proxy configuration entry: {}", x),
291 ))
292 }
293 }
294 }
295
296 Ok(())
297 }
298
299 fn validate_no_proxy(value: &Value) -> WebDriverResult<()> {
300 match value.as_array() {
301 Some(hosts) => {
302 for host in hosts {
303 match host.as_str() {
304 Some(_) => {}
305 None => {
306 return Err(WebDriverError::new(
307 ErrorStatus::InvalidArgument,
308 format!("noProxy item is not a string: {}", host),
309 ))
310 }
311 }
312 }
313 }
314 None => {
315 return Err(WebDriverError::new(
316 ErrorStatus::InvalidArgument,
317 format!("noProxy is not an array: {}", value),
318 ))
319 }
320 }
321
322 Ok(())
323 }
324
325 fn validate_host(value: &Value, entry: &str) -> WebDriverResult<()> {
328 match value.as_str() {
329 Some(host) => {
330 if host.contains("://") {
331 return Err(WebDriverError::new(
332 ErrorStatus::InvalidArgument,
333 format!("{} must not contain a scheme: {}", entry, host),
334 ));
335 }
336
337 let url = Url::parse(&format!("http://{}", host)).map_err(|_| {
339 WebDriverError::new(
340 ErrorStatus::InvalidArgument,
341 format!("{} is not a valid URL: {}", entry, host),
342 )
343 })?;
344
345 if url.username() != ""
346 || url.password().is_some()
347 || url.path() != "/"
348 || url.query().is_some()
349 || url.fragment().is_some()
350 {
351 return Err(WebDriverError::new(
352 ErrorStatus::InvalidArgument,
353 format!("{} is not of the form host[:port]: {}", entry, host),
354 ));
355 }
356 }
357
358 None => {
359 return Err(WebDriverError::new(
360 ErrorStatus::InvalidArgument,
361 format!("{} is not a string: {}", entry, value),
362 ))
363 }
364 }
365
366 Ok(())
367 }
368
369 fn validate_timeouts(value: &Value) -> WebDriverResult<()> {
370 let obj = try_opt!(
371 value.as_object(),
372 ErrorStatus::InvalidArgument,
373 "timeouts capability is not an object"
374 );
375
376 for (key, value) in obj {
377 match &**key {
378 _x @ "script" if value.is_null() => {}
379
380 x @ "script" | x @ "pageLoad" | x @ "implicit" => {
381 let timeout = try_opt!(
382 value.as_f64(),
383 ErrorStatus::InvalidArgument,
384 format!("{} timeouts value is not a number: {}", x, value)
385 );
386 if timeout < 0.0 || timeout.fract() != 0.0 {
387 return Err(WebDriverError::new(
388 ErrorStatus::InvalidArgument,
389 format!(
390 "'{}' timeouts value is not a positive Integer: {}",
391 x, timeout
392 ),
393 ));
394 }
395 if (timeout as u64) > MAX_SAFE_INTEGER {
396 return Err(WebDriverError::new(
397 ErrorStatus::InvalidArgument,
398 format!(
399 "'{}' timeouts value is greater than maximum safe integer: {}",
400 x, timeout
401 ),
402 ));
403 }
404 }
405
406 x => {
407 return Err(WebDriverError::new(
408 ErrorStatus::InvalidArgument,
409 format!("Invalid timeouts capability entry: {}", x),
410 ))
411 }
412 }
413 }
414
415 Ok(())
416 }
417
418 fn validate_unhandled_prompt_behavior(value: &Value) -> WebDriverResult<()> {
419 match value {
420 Value::Object(obj) => {
421 for (key, value) in obj {
423 match &**key {
424 x @ "alert"
425 | x @ "beforeUnload"
426 | x @ "confirm"
427 | x @ "default"
428 | x @ "prompt" => {
429 let behavior = try_opt!(
430 value.as_str(),
431 ErrorStatus::InvalidArgument,
432 format!(
433 "'{}' unhandledPromptBehavior value is not a string: {}",
434 x, value
435 )
436 );
437
438 match behavior {
439 "accept" | "accept and notify" | "dismiss"
440 | "dismiss and notify" | "ignore" => {}
441 x => {
442 return Err(WebDriverError::new(
443 ErrorStatus::InvalidArgument,
444 format!(
445 "'{}' unhandledPromptBehavior value is invalid: {}",
446 x, behavior
447 ),
448 ))
449 }
450 }
451 }
452 x => {
453 return Err(WebDriverError::new(
454 ErrorStatus::InvalidArgument,
455 format!("Invalid unhandledPromptBehavior entry: {}", x),
456 ))
457 }
458 }
459 }
460 }
461 Value::String(behavior) => match behavior.as_str() {
462 "accept" | "accept and notify" | "dismiss" | "dismiss and notify" | "ignore" => {}
463 x => {
464 return Err(WebDriverError::new(
465 ErrorStatus::InvalidArgument,
466 format!("Invalid unhandledPromptBehavior value: {}", x),
467 ))
468 }
469 },
470 _ => {
471 return Err(WebDriverError::new(
472 ErrorStatus::InvalidArgument,
473 format!(
474 "unhandledPromptBehavior is neither an object nor a string: {}",
475 value
476 ),
477 ))
478 }
479 }
480
481 Ok(())
482 }
483}
484
485impl CapabilitiesMatching for SpecNewSessionParameters {
486 fn match_browser<T: BrowserCapabilities>(
487 &self,
488 browser_capabilities: &mut T,
489 ) -> WebDriverResult<Option<Capabilities>> {
490 let default = vec![Map::new()];
491 let capabilities_list = if self.firstMatch.is_empty() {
492 &default
493 } else {
494 &self.firstMatch
495 };
496
497 let merged_capabilities = capabilities_list
498 .iter()
499 .map(|first_match_entry| {
500 if first_match_entry
501 .keys()
502 .any(|k| self.alwaysMatch.contains_key(k))
503 {
504 return Err(WebDriverError::new(
505 ErrorStatus::InvalidArgument,
506 "firstMatch key shadowed a value in alwaysMatch",
507 ));
508 }
509 let mut merged = self.alwaysMatch.clone();
510 for (key, value) in first_match_entry.clone() {
511 merged.insert(key, value);
512 }
513 Ok(merged)
514 })
515 .map(|merged| merged.and_then(|x| self.validate(x, browser_capabilities)))
516 .collect::<WebDriverResult<Vec<Capabilities>>>()?;
517
518 let selected = merged_capabilities
519 .iter()
520 .find(|merged| {
521 browser_capabilities.init(merged);
522
523 for (key, value) in merged.iter() {
524 match &**key {
525 "browserName" => {
526 let browserValue = browser_capabilities
527 .browser_name(merged)
528 .ok()
529 .and_then(|x| x);
530
531 if value.as_str() != browserValue.as_deref() {
532 return false;
533 }
534 }
535 "browserVersion" => {
536 let browserValue = browser_capabilities
537 .browser_version(merged)
538 .ok()
539 .and_then(|x| x);
540 let version_cond = value.as_str().unwrap_or("");
542 if let Some(version) = browserValue {
543 if !browser_capabilities
544 .compare_browser_version(&version, version_cond)
545 .unwrap_or(false)
546 {
547 return false;
548 }
549 } else {
550 return false;
551 }
552 }
553 "platformName" => {
554 let browserValue = browser_capabilities
555 .platform_name(merged)
556 .ok()
557 .and_then(|x| x);
558 if value.as_str() != browserValue.as_deref() {
559 return false;
560 }
561 }
562 "acceptInsecureCerts" => {
563 if value.as_bool().unwrap_or(false)
564 && !browser_capabilities
565 .accept_insecure_certs(merged)
566 .unwrap_or(false)
567 {
568 return false;
569 }
570 }
571 "setWindowRect" => {
572 if value.as_bool().unwrap_or(false)
573 && !browser_capabilities
574 .set_window_rect(merged)
575 .unwrap_or(false)
576 {
577 return false;
578 }
579 }
580 "strictFileInteractability" => {
581 if value.as_bool().unwrap_or(false)
582 && !browser_capabilities
583 .strict_file_interactability(merged)
584 .unwrap_or(false)
585 {
586 return false;
587 }
588 }
589 "proxy" => {
590 let default = Map::new();
591 let proxy = value.as_object().unwrap_or(&default);
592 if !browser_capabilities
593 .accept_proxy(proxy, merged)
594 .unwrap_or(false)
595 {
596 return false;
597 }
598 }
599 "webSocketUrl" => {
600 if value.as_bool().unwrap_or(false)
601 && !browser_capabilities.web_socket_url(merged).unwrap_or(false)
602 {
603 return false;
604 }
605 }
606 "webauthn:virtualAuthenticators" => {
607 if value.as_bool().unwrap_or(false)
608 && !browser_capabilities
609 .webauthn_virtual_authenticators(merged)
610 .unwrap_or(false)
611 {
612 return false;
613 }
614 }
615 "webauthn:extension:uvm" => {
616 if value.as_bool().unwrap_or(false)
617 && !browser_capabilities
618 .webauthn_extension_uvm(merged)
619 .unwrap_or(false)
620 {
621 return false;
622 }
623 }
624 "webauthn:extension:prf" => {
625 if value.as_bool().unwrap_or(false)
626 && !browser_capabilities
627 .webauthn_extension_prf(merged)
628 .unwrap_or(false)
629 {
630 return false;
631 }
632 }
633 "webauthn:extension:largeBlob" => {
634 if value.as_bool().unwrap_or(false)
635 && !browser_capabilities
636 .webauthn_extension_large_blob(merged)
637 .unwrap_or(false)
638 {
639 return false;
640 }
641 }
642 "webauthn:extension:credBlob" => {
643 if value.as_bool().unwrap_or(false)
644 && !browser_capabilities
645 .webauthn_extension_cred_blob(merged)
646 .unwrap_or(false)
647 {
648 return false;
649 }
650 }
651 name => {
652 if name.contains(':') {
653 if !browser_capabilities
654 .accept_custom(name, value, merged)
655 .unwrap_or(false)
656 {
657 return false;
658 }
659 } else {
660 }
662 }
663 }
664 }
665
666 true
667 })
668 .cloned();
669 Ok(selected)
670 }
671}
672
673#[cfg(test)]
674mod tests {
675 use super::*;
676 use crate::test::assert_de;
677 use serde_json::{self, json};
678
679 #[test]
680 fn test_json_spec_new_session_parameters_alwaysMatch_only() {
681 let caps = SpecNewSessionParameters {
682 alwaysMatch: Capabilities::new(),
683 firstMatch: vec![Capabilities::new()],
684 };
685 assert_de(&caps, json!({"alwaysMatch": {}}));
686 }
687
688 #[test]
689 fn test_json_spec_new_session_parameters_firstMatch_only() {
690 let caps = SpecNewSessionParameters {
691 alwaysMatch: Capabilities::new(),
692 firstMatch: vec![Capabilities::new()],
693 };
694 assert_de(&caps, json!({"firstMatch": [{}]}));
695 }
696
697 #[test]
698 fn test_json_spec_new_session_parameters_alwaysMatch_null() {
699 let json = json!({
700 "alwaysMatch": null,
701 "firstMatch": [{}],
702 });
703 assert!(serde_json::from_value::<SpecNewSessionParameters>(json).is_err());
704 }
705
706 #[test]
707 fn test_json_spec_new_session_parameters_firstMatch_null() {
708 let json = json!({
709 "alwaysMatch": {},
710 "firstMatch": null,
711 });
712 assert!(serde_json::from_value::<SpecNewSessionParameters>(json).is_err());
713 }
714
715 #[test]
716 fn test_json_spec_new_session_parameters_both_empty() {
717 let json = json!({
718 "alwaysMatch": {},
719 "firstMatch": [{}],
720 });
721 let caps = SpecNewSessionParameters {
722 alwaysMatch: Capabilities::new(),
723 firstMatch: vec![Capabilities::new()],
724 };
725
726 assert_de(&caps, json);
727 }
728
729 #[test]
730 fn test_json_spec_new_session_parameters_both_with_capability() {
731 let json = json!({
732 "alwaysMatch": {"foo": "bar"},
733 "firstMatch": [{"foo2": "bar2"}],
734 });
735 let mut caps = SpecNewSessionParameters {
736 alwaysMatch: Capabilities::new(),
737 firstMatch: vec![Capabilities::new()],
738 };
739 caps.alwaysMatch.insert("foo".into(), "bar".into());
740 caps.firstMatch[0].insert("foo2".into(), "bar2".into());
741
742 assert_de(&caps, json);
743 }
744
745 #[test]
746 fn test_validate_unhandled_prompt_behavior() {
747 fn validate_prompt_behavior(v: Value) -> WebDriverResult<()> {
748 SpecNewSessionParameters::validate_unhandled_prompt_behavior(&v)
749 }
750
751 validate_prompt_behavior(json!("accept")).unwrap();
753 validate_prompt_behavior(json!("accept and notify")).unwrap();
754 validate_prompt_behavior(json!("dismiss")).unwrap();
755 validate_prompt_behavior(json!("dismiss and notify")).unwrap();
756 validate_prompt_behavior(json!("ignore")).unwrap();
757 assert!(validate_prompt_behavior(json!("foo")).is_err());
758
759 let types = ["alert", "beforeUnload", "confirm", "default", "prompt"];
761 let handlers = [
762 "accept",
763 "accept and notify",
764 "dismiss",
765 "dismiss and notify",
766 "ignore",
767 ];
768 for promptType in types {
769 assert!(validate_prompt_behavior(json!({promptType: "foo"})).is_err());
770 for handler in handlers {
771 validate_prompt_behavior(json!({promptType: handler})).unwrap();
772 }
773 }
774
775 for handler in handlers {
776 assert!(validate_prompt_behavior(json!({"foo": handler})).is_err());
777 }
778 }
779
780 #[test]
781 fn test_validate_proxy() {
782 fn validate_proxy(v: Value) -> WebDriverResult<()> {
783 SpecNewSessionParameters::validate_proxy(&v)
784 }
785
786 validate_proxy(json!({"httpProxy": "127.0.0.1"})).unwrap();
788 validate_proxy(json!({"httpProxy": "127.0.0.1:"})).unwrap();
789 validate_proxy(json!({"httpProxy": "127.0.0.1:3128"})).unwrap();
790 validate_proxy(json!({"httpProxy": "localhost"})).unwrap();
791 validate_proxy(json!({"httpProxy": "localhost:3128"})).unwrap();
792 validate_proxy(json!({"httpProxy": "[2001:db8::1]"})).unwrap();
793 validate_proxy(json!({"httpProxy": "[2001:db8::1]:3128"})).unwrap();
794 validate_proxy(json!({"httpProxy": "example.org"})).unwrap();
795 validate_proxy(json!({"httpProxy": "example.org:3128"})).unwrap();
796
797 assert!(validate_proxy(json!({"httpProxy": "http://example.org"})).is_err());
798 assert!(validate_proxy(json!({"httpProxy": "example.org:-1"})).is_err());
799 assert!(validate_proxy(json!({"httpProxy": "2001:db8::1"})).is_err());
800
801 validate_proxy(json!({"noProxy": ["foo"]})).unwrap();
803
804 assert!(validate_proxy(json!({"noProxy": "foo"})).is_err());
805 assert!(validate_proxy(json!({"noProxy": [42]})).is_err());
806 }
807}