#[macro_use]
extern crate validators;
#[macro_use]
extern crate lazy_static;
extern crate regex;
extern crate easy_http_request;
extern crate chrono;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
extern crate rocket;
extern crate rocket_client_addr;
mod errors;
mod fairing;
mod verification;
use std::collections::HashMap;
use std::marker::PhantomData;
use std::str::FromStr;
use easy_http_request::HttpRequest;
use validators::ValidatedCustomizedStringError;
use regex::Regex;
use chrono::prelude::*;
pub use rocket_client_addr::ClientRealAddr;
pub use errors::ReCaptchaError;
use fairing::ReCaptchaFairing;
pub use verification::ReCaptchaVerification;
use verification::ReCaptchaVerificationInner;
const API_URL: &str = "https://www.google.com/recaptcha/api/siteverify";
lazy_static! {
static ref RE_KEY: Regex = { Regex::new(r"^[0-9a-zA-Z\-_]{40}$").unwrap() };
static ref RE_TOKEN: Regex = { Regex::new(r"^[0-9a-zA-Z\-_]+$").unwrap() };
}
validated_customized_regex_string!(pub ReCaptchaKey, ref RE_KEY);
validated_customized_regex_string!(pub ReCaptchaToken, ref RE_TOKEN);
pub trait ReCaptchaVariant: Sync + Send + 'static {
fn get_version_str(&self) -> &'static str;
}
#[derive(Debug, Clone, Copy)]
pub struct V3;
impl ReCaptchaVariant for V3 {
#[inline]
fn get_version_str(&self) -> &'static str {
"v3"
}
}
#[derive(Debug, Clone, Copy)]
pub struct V2;
impl ReCaptchaVariant for V2 {
#[inline]
fn get_version_str(&self) -> &'static str {
"v2"
}
}
#[derive(Debug, Clone)]
pub struct ReCaptcha<V: ReCaptchaVariant = V3> {
html_key: Option<ReCaptchaKey>,
secret_key: ReCaptchaKey,
phantom: PhantomData<V>,
}
impl<V: ReCaptchaVariant> ReCaptcha<V> {
#[inline]
pub fn new(html_key: Option<ReCaptchaKey>, secret_key: ReCaptchaKey) -> ReCaptcha<V> {
ReCaptcha {
html_key,
secret_key,
phantom: PhantomData,
}
}
#[inline]
pub fn from_str<S1: AsRef<str>, S2: AsRef<str>>(
html_key: Option<S1>,
secret_key: S2,
) -> Result<ReCaptcha<V>, ValidatedCustomizedStringError> {
let html_key = match html_key {
Some(html_key) => Some(ReCaptchaKey::from_str(html_key.as_ref())?),
None => None,
};
let secret_key = ReCaptchaKey::from_str(secret_key.as_ref())?;
Ok(ReCaptcha::<V>::new(html_key, secret_key))
}
#[inline]
pub fn from_string<S1: Into<String>, S2: Into<String>>(
html_key: Option<S1>,
secret_key: S2,
) -> Result<ReCaptcha<V>, ValidatedCustomizedStringError> {
let html_key = match html_key {
Some(html_key) => Some(ReCaptchaKey::from_string(html_key.into())?),
None => None,
};
let secret_key = ReCaptchaKey::from_string(secret_key.into())?;
Ok(ReCaptcha::<V>::new(html_key, secret_key))
}
#[inline]
pub fn get_html_key_as_str(&self) -> Option<&str> {
self.html_key.as_ref().map(|k| k.as_str())
}
#[inline]
pub fn get_secret_key_as_str(&self) -> &str {
self.secret_key.as_str()
}
}
impl ReCaptcha {
#[inline]
pub fn fairing() -> ReCaptchaFairing<V3> {
ReCaptchaFairing::<V3>::new()
}
#[inline]
pub fn fairing_v2() -> ReCaptchaFairing<V2> {
ReCaptchaFairing::<V2>::new()
}
}
impl<V: ReCaptchaVariant> ReCaptcha<V> {
pub fn verify(
&self,
recaptcha_token: &ReCaptchaToken,
remote_ip: Option<&ClientRealAddr>,
) -> Result<ReCaptchaVerification, ReCaptchaError> {
let mut request: HttpRequest<&str, String, &str, &str, &str, &str> =
HttpRequest::post_from_url_str(API_URL).unwrap();
request.query = Some({
let mut map = HashMap::new();
map.insert("secret", self.get_secret_key_as_str().to_string());
map.insert("response", recaptcha_token.as_str().to_string());
if let Some(remote_ip) = remote_ip {
map.insert("remoteip", remote_ip.ip.to_string());
}
map
});
let response =
request.send().map_err(|err| ReCaptchaError::InternalError(format!("{:?}", err)))?;
if response.status_code == 200 {
let body = response.body;
let result: ReCaptchaVerificationInner = serde_json::from_slice(&body)
.map_err(|err| ReCaptchaError::InternalError(err.to_string()))?;
if result.success {
let score = result.score.unwrap_or(1.0);
let action = result.action;
let challenge_ts = result.challenge_ts.ok_or_else(|| {
ReCaptchaError::InternalError("There is no `challenge_ts` field.".to_string())
})?;
let hostname = result.hostname.ok_or_else(|| {
ReCaptchaError::InternalError("There is no `hostname` field.".to_string())
})?;
let challenge_ts = DateTime::from_str(&challenge_ts).map_err(|_| {
ReCaptchaError::InternalError(format!(
"The format of the timestamp `{}` is incorrect.",
challenge_ts
))
})?;
Ok(ReCaptchaVerification {
score,
action,
challenge_ts,
hostname,
})
} else {
match result.error_codes {
Some(error_codes) => {
if error_codes.contains(&"invalid-input-secret".to_string()) {
Err(ReCaptchaError::InvalidInputSecret)
} else if error_codes.contains(&"invalid-input-response".to_string()) {
Err(ReCaptchaError::InvalidReCaptchaToken)
} else if error_codes.contains(&"timeout-or-duplicate".to_string()) {
Err(ReCaptchaError::TimeoutOrDuplicate)
} else {
Err(ReCaptchaError::InternalError(
"No expected error codes.".to_string(),
))
}
}
None => Err(ReCaptchaError::InternalError("No error codes.".to_string())),
}
}
} else {
Err(ReCaptchaError::InternalError(format!(
"The response status code of the `siteverify` API is {}.",
response.status_code
)))
}
}
}