1use crate::Error;
61use crate::Request;
62use crate::Response;
63use reqwest::Url;
64
65mod form;
66
67use form::Form;
68
69pub const VERIFY_URL: &str = "https://hcaptcha.com/siteverify";
71
72#[cfg_attr(docsrs, allow(rustdoc::missing_doc_code_examples))]
74#[derive(Debug)]
75pub struct Client {
76 client: reqwest::Client,
78 url: Url,
80}
81
82#[cfg_attr(docsrs, allow(rustdoc::missing_doc_code_examples))]
83impl Default for Client {
84 fn default() -> Client {
85 Client::new()
86 }
87}
88
89#[cfg_attr(docsrs, allow(rustdoc::missing_doc_code_examples))]
90impl Client {
91 #[allow(unknown_lints)]
105 #[cfg_attr(docsrs, allow(rustdoc::bare_urls))]
106 pub fn new() -> Client {
107 Client {
108 client: reqwest::Client::new(),
109 url: Url::parse(VERIFY_URL).expect("API url string corrupt"),
110 }
111 }
112
113 pub fn new_with(url: &str) -> Result<Client, url::ParseError> {
127 Ok(Client {
128 client: reqwest::Client::new(),
129 url: Url::parse(url)?,
130 })
131 }
132
133 pub fn set_url(mut self, url: &str) -> Result<Self, Error> {
152 self.url = Url::parse(url)?;
153 Ok(self)
154 }
155
156 #[allow(dead_code)]
225 #[cfg_attr(
226 feature = "trace",
227 tracing::instrument(
228 name = "Request verification from hcaptcha.",
229 skip(self),
230 level = "debug"
231 )
232 )]
233 #[deprecated(since = "3.0.0", note = "please use `verify` instead")]
234 pub async fn verify_client_response(self, request: Request) -> Result<Response, Error> {
235 let form: Form = request.into();
236 #[cfg(feature = "trace")]
237 tracing::debug!(
238 "The form to submit to Hcaptcha API: {:?}",
239 serde_urlencoded::to_string(&form).unwrap_or_else(|_| "form corrupted".to_owned())
240 );
241 let response = self
242 .client
243 .post(self.url.clone())
244 .form(&form)
245 .send()
246 .await?
247 .json::<Response>()
248 .await?;
249 #[cfg(feature = "trace")]
250 tracing::debug!("The response is: {:?}", response);
251 response.check_error()?;
252 Ok(response)
253 }
254
255 #[allow(dead_code)]
324 #[cfg_attr(
325 feature = "trace",
326 tracing::instrument(
327 name = "Request verification from hcaptcha.",
328 skip(self),
329 level = "debug"
330 )
331 )]
332 pub async fn verify(self, request: Request) -> Result<Response, Error> {
333 let form: Form = request.into();
334 #[cfg(feature = "trace")]
335 tracing::debug!(
336 "The form to submit to Hcaptcha API: {:?}",
337 serde_urlencoded::to_string(&form).unwrap_or_else(|_| "form corrupted".to_owned())
338 );
339 let response = self
340 .client
341 .post(self.url.clone())
342 .form(&form)
343 .send()
344 .await?
345 .json::<Response>()
346 .await?;
347 #[cfg(feature = "trace")]
348 tracing::debug!("The response is: {:?}", response);
349 response.check_error()?;
350 Ok(response)
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357 use crate::{Code, Error};
358 use chrono::{TimeDelta, Utc};
359 use claims::{assert_err, assert_ok};
360 use rand::distributions::Alphanumeric;
361 use rand::{thread_rng, Rng};
362 use serde_json::json;
363 use std::iter;
364 #[cfg(feature = "trace")]
365 use tracing_test::traced_test;
366 use wiremock::matchers::{body_string, method, path};
367 use wiremock::{Mock, MockServer, ResponseTemplate};
368
369 fn random_string(characters: usize) -> String {
375 let mut rng = thread_rng();
376 iter::repeat(())
377 .map(|()| rng.sample(Alphanumeric))
378 .map(char::from)
379 .take(characters)
380 .collect()
381 }
382
383 #[tokio::test]
384 #[cfg_attr(feature = "trace", traced_test)]
385 async fn hcaptcha_mock_verify() {
386 let token = random_string(100);
387 let secret = format!("0x{}", hex::encode(random_string(20)));
388 let request = Request::new_from_response(&secret, &token).unwrap();
389
390 let expected_body = format!("response={}&secret={}", &token, &secret);
391
392 let timestamp = Utc::now()
393 .checked_sub_signed(TimeDelta::try_minutes(10).unwrap())
394 .unwrap()
395 .to_rfc3339();
396
397 let response_template = ResponseTemplate::new(200).set_body_json(json!({
398 "success": true,
399 "challenge_ts": timestamp,
400 "hostname": "test-host",
401 }));
402 let mock_server = MockServer::start().await;
403 Mock::given(method("POST"))
404 .and(path("/siteverify"))
405 .and(body_string(&expected_body))
406 .respond_with(response_template)
407 .mount(&mock_server)
408 .await;
409 let uri = format!("{}{}", mock_server.uri(), "/siteverify");
410
411 let client = Client::new_with(&uri).unwrap();
412 let response = client.verify(request).await;
413 assert_ok!(&response);
414 let response = response.unwrap();
415 assert!(&response.success());
416 assert_eq!(&response.timestamp().unwrap(), ×tamp);
417 #[cfg(feature = "trace")]
418 assert!(logs_contain("Hcaptcha API"));
419 #[cfg(feature = "trace")]
420 assert!(logs_contain("The response is"));
421 }
422
423 #[tokio::test]
424 #[cfg_attr(feature = "trace", traced_test)]
425 async fn hcaptcha_mock_verify_not_found() {
426 let token = random_string(100);
427 let secret = format!("0x{}", hex::encode(random_string(20)));
428 let request = Request::new_from_response(&secret, &token).unwrap();
429
430 let expected_body = format!("response={}&secret={}", &token, &secret);
431
432 let response_template = ResponseTemplate::new(404);
433 let mock_server = MockServer::start().await;
434 Mock::given(method("POST"))
435 .and(path("/siteverify"))
436 .and(body_string(&expected_body))
437 .respond_with(response_template)
438 .mount(&mock_server)
439 .await;
440 let uri = format!("{}{}", mock_server.uri(), "/siteverify");
441
442 let client = Client::new_with(&uri).unwrap();
443 let response = client.verify(request).await;
444 assert_err!(&response);
445 }
446
447 #[tokio::test]
448 #[cfg_attr(feature = "trace", traced_test)]
449 async fn hcaptcha_mock_verify_client_response() {
450 let token = random_string(100);
451 let secret = format!("0x{}", hex::encode(random_string(20)));
452 let request = Request::new_from_response(&secret, &token).unwrap();
453
454 let expected_body = format!("response={}&secret={}", &token, &secret);
455
456 let timestamp = Utc::now()
457 .checked_sub_signed(TimeDelta::try_minutes(10).unwrap())
458 .unwrap()
459 .to_rfc3339();
460
461 let response_template = ResponseTemplate::new(200).set_body_json(json!({
462 "success": true,
463 "challenge_ts": timestamp,
464 "hostname": "test-host",
465 }));
466 let mock_server = MockServer::start().await;
467 Mock::given(method("POST"))
468 .and(path("/siteverify"))
469 .and(body_string(&expected_body))
470 .respond_with(response_template)
471 .mount(&mock_server)
472 .await;
473 let uri = format!("{}{}", mock_server.uri(), "/siteverify");
474
475 let client = Client::new_with(&uri).unwrap();
476 #[allow(deprecated)]
477 let response = client.verify_client_response(request).await;
478 assert_ok!(&response);
479 let response = response.unwrap();
480 assert!(&response.success());
481 assert_eq!(&response.timestamp().unwrap(), ×tamp);
482 #[cfg(feature = "trace")]
483 assert!(logs_contain("Hcaptcha API"));
484 #[cfg(feature = "trace")]
485 assert!(logs_contain("The response is"));
486 }
487
488 #[tokio::test]
489 #[cfg_attr(feature = "trace", traced_test)]
490 async fn hcaptcha_mock_verify_client_response_not_found() {
491 let token = random_string(100);
492 let secret = format!("0x{}", hex::encode(random_string(20)));
493 let request = Request::new_from_response(&secret, &token).unwrap();
494
495 let expected_body = format!("response={}&secret={}", &token, &secret);
496
497 let response_template = ResponseTemplate::new(404);
498 let mock_server = MockServer::start().await;
499 Mock::given(method("POST"))
500 .and(path("/siteverify"))
501 .and(body_string(&expected_body))
502 .respond_with(response_template)
503 .mount(&mock_server)
504 .await;
505 let uri = format!("{}{}", mock_server.uri(), "/siteverify");
506
507 let client = Client::new_with(&uri).unwrap();
508 #[allow(deprecated)]
509 let response = client.verify_client_response(request).await;
510 assert_err!(&response);
511 }
512
513 #[tokio::test]
514 #[cfg_attr(feature = "trace", traced_test)]
515 async fn hcaptcha_mock_with_remoteip() {
516 let token = random_string(100);
517 let secret = format!("0x{}", hex::encode(random_string(20)));
518 let remoteip = mockd::internet::ipv4_address();
519 let request = Request::new_from_response(&secret, &token)
520 .unwrap()
521 .set_remoteip(&remoteip)
522 .unwrap();
523
524 let expected_body = format!(
525 "response={}&remoteip={}&secret={}",
526 &token, &remoteip, &secret
527 );
528
529 let timestamp = Utc::now()
530 .checked_sub_signed(TimeDelta::try_minutes(10).unwrap())
531 .unwrap()
532 .to_rfc3339();
533
534 let response_template = ResponseTemplate::new(200).set_body_json(json!({
535 "success": true,
536 "challenge_ts": timestamp,
537 "hostname": "test-host",
538 }));
539 let mock_server = MockServer::start().await;
540 Mock::given(method("POST"))
541 .and(path("/siteverify"))
542 .and(body_string(&expected_body))
543 .respond_with(response_template)
544 .mount(&mock_server)
545 .await;
546 let uri = format!("{}{}", mock_server.uri(), "/siteverify");
547
548 let client = Client::new_with(&uri).unwrap();
549 let response = client.verify(request).await;
550 assert_ok!(&response);
551 let response = response.unwrap();
552 assert!(&response.success());
553 assert_eq!(&response.timestamp().unwrap(), ×tamp);
554 #[cfg(feature = "trace")]
555 assert!(logs_contain("Hcaptcha API"));
556 #[cfg(feature = "trace")]
557 assert!(logs_contain("The response is"));
558 }
559
560 #[tokio::test]
561 #[cfg_attr(feature = "trace", traced_test)]
562 async fn hcaptcha_mock_with_sitekey() {
563 let token = random_string(100);
564 let secret = format!("0x{}", hex::encode(random_string(20)));
565 let sitekey = mockd::unique::uuid_v4();
566 let request = Request::new_from_response(&secret, &token)
567 .unwrap()
568 .set_sitekey(&sitekey)
569 .unwrap();
570
571 let expected_body = format!(
572 "response={}&sitekey={}&secret={}",
573 &token, &sitekey, &secret
574 );
575
576 let timestamp = Utc::now()
577 .checked_sub_signed(TimeDelta::try_minutes(10).unwrap())
578 .unwrap()
579 .to_rfc3339();
580
581 let response_template = ResponseTemplate::new(200).set_body_json(json!({
582 "success": true,
583 "challenge_ts": timestamp,
584 "hostname": "test-host",
585 }));
586 let mock_server = MockServer::start().await;
587 Mock::given(method("POST"))
588 .and(path("/siteverify"))
589 .and(body_string(&expected_body))
590 .respond_with(response_template)
591 .mount(&mock_server)
592 .await;
593 let uri = format!("{}{}", mock_server.uri(), "/siteverify");
594
595 let client = Client::new_with(&uri).unwrap();
596 let response = client.verify(request).await;
597 assert_ok!(&response);
598 let response = response.unwrap();
599 assert!(&response.success());
600 assert_eq!(&response.timestamp().unwrap(), ×tamp);
601 #[cfg(feature = "trace")]
602 assert!(logs_contain("Hcaptcha API"));
603 #[cfg(feature = "trace")]
604 assert!(logs_contain("The response is"));
605 }
606
607 #[test]
608 fn test_success_response() {
609 let api_response = json!({
610 "success": true,
611 "challenge_ts": "2020-11-11T23:27:00Z",
612 "hostname": "my-host.ie",
613 "credit": true,
614 "error-codes": [],
615 "score": null,
616 "score_reason": [],
617 });
618 let response: Response = serde_json::from_value(api_response).unwrap();
619 assert!(response.success());
620 assert_eq!(
621 response.timestamp(),
622 Some("2020-11-11T23:27:00Z".to_owned())
623 );
624 assert_eq!(response.hostname(), Some("my-host.ie".to_owned()));
625 }
626
627 #[test]
628 fn test_error_response() {
629 let api_response = json!({
630 "success": false,
631 "challenge_ts": null,
632 "hostname": null,
633 "credit": null,
634 "error-codes": ["missing-input-secret", "foo"],
635 "score": null,
636 "score_reason": [],
637 });
638 let response: Response = serde_json::from_value(api_response).unwrap();
639 assert!(!response.success());
640 assert!(response.error_codes().is_some());
641 if let Some(hash_set) = response.error_codes() {
642 assert_eq!(hash_set.len(), 2);
643 assert!(hash_set.contains(&Code::MissingSecret));
644 assert!(hash_set.contains(&Code::Unknown("foo".to_owned())));
645 }
646 }
647
648 #[test]
649 fn test_hcaptcha_client_default_initialization() {
650 let client = Client::default();
651 assert!(matches!(client, Client { .. }));
652 }
653
654 #[test]
655 fn test_hcaptcha_client_default_calls_new() {
656 let client = Client::default();
659 let expected_value = Url::parse(VERIFY_URL).unwrap();
662 assert!(client.url == expected_value);
663 }
664
665 #[test]
666 fn test_set_url_with_valid_url() {
667 let client = Client::default();
668 let result = client.set_url("https://example.com");
669 assert!(result.is_ok());
670 assert_eq!(result.unwrap().url.as_str(), "https://example.com/");
671 }
672
673 #[test]
674 fn test_set_url_with_invalid_url() {
675 let client = Client::default();
676 let result = client.set_url("invalid-url");
677 assert!(result.is_err());
678 match result {
679 Err(Error::Url(_)) => (),
680 _ => panic!("Expected UrlParseError"),
681 }
682 }
683
684 #[test]
685 fn test_set_url_with_empty_string() {
686 let client = Client::default();
687 let result = client.set_url("");
688 assert!(result.is_err());
689 match result {
690 Err(Error::Url(_)) => (),
691 _ => panic!("Expected UrlParseError"),
692 }
693 }
694}