Skip to content

Commit

Permalink
add the cloudflare turnstile feature
Browse files Browse the repository at this point in the history
  • Loading branch information
huangcheng committed Dec 19, 2023
1 parent 77acc0e commit cecb5ad
Show file tree
Hide file tree
Showing 16 changed files with 128 additions and 9 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jsonwebtoken = { version = "9.1.0", default-features = false }
log = "0.4.20"
redis = { version = "0.24.0", features = ["tokio-comp"] }
regex = "1.10.2"
reqwest = { version = "0.11.23", features = ["json"], optional = true }
rocket = { version = "0.5.0", features = ["json", "uuid"] }
rocket_db_pools = { version = "0.1.0", features = ["sqlx_mysql", "deadpool_redis"] }
serde = { version = "1.0.193", features = ["derive"] }
Expand All @@ -28,6 +29,9 @@ sqlx = { version = "0.7", features = [ "runtime-tokio", "mysql", "migrate", "uui
tokio = { version = "1.34.0", features = ["fs"] }
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics"] }

[features]
turnstile = ["dep:reqwest"]

[[bin]]
name = "server"

Expand Down
2 changes: 2 additions & 0 deletions Rocket.toml.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
[default]
upload_dir = "upload/"
upload_url = "/upload"
turnstile_secret = ""
turnstile_url = ""

[default.databases.startpage]
url = "mysql://startpage:startpage@127.0.0.1/startpage"
Expand Down
2 changes: 2 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ pub struct Config {
pub jwt: Jwt,
pub upload_dir: PathBuf,
pub upload_url: String,
pub turnstile_secret: Option<String>,
pub turnstile_url: Option<String>,
}
2 changes: 2 additions & 0 deletions src/guards.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod jwt;
pub mod remote_ip;
3 changes: 2 additions & 1 deletion src/middlewares/jwt.rs → src/guards/jwt.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use rocket::http::Status;
use rocket::request::{FromRequest, Outcome};
use rocket::Request;
use rocket_db_pools::deadpool_redis::redis::AsyncCommands;

use crate::config::Config;
Expand All @@ -23,7 +24,7 @@ pub enum JwtError {
impl<'r> FromRequest<'r> for Middleware {
type Error = JwtError;

async fn from_request(request: &'r rocket::Request<'_>) -> Outcome<Self, Self::Error> {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let config = match request.rocket().figment().extract::<Config>() {
Ok(config) => config,
Err(_) => return Outcome::Error((Status::InternalServerError, JwtError::ConfigError)),
Expand Down
18 changes: 18 additions & 0 deletions src/guards/remote_ip.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use rocket::request::{FromRequest, Outcome};
use rocket::Request;

pub struct Ip(pub(crate) Option<String>);

#[rocket::async_trait]
impl<'r> FromRequest<'r> for Ip {
type Error = ();

async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let ip = request.headers().get_one("CF-Connecting-IP");

match ip {
Some(ip) => Outcome::Success(Ip(Some(ip.to_string()))),
None => Outcome::Success(Ip(None)),
}
}
}
72 changes: 72 additions & 0 deletions src/handlers/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ use rocket_db_pools::Connection;
use sqlx::query_as;
use uuid::Uuid;

#[cfg(feature = "turnstile")]
use reqwest;

use crate::config::Config;
use crate::errors::ServiceError;
use crate::request;
Expand All @@ -15,13 +18,82 @@ use crate::utils::calculate_expires;
use crate::Claims;
use crate::{models, MySQLDb, RedisDb};

#[cfg(feature = "turnstile")]
use crate::response::auth::TurnstileResponse;

pub async fn login(
user: &request::auth::User<'_>,
state: &AppState,
config: &Config,
_remote_ip: Option<String>,
db: &mut Connection<MySQLDb>,
cache: &mut Connection<RedisDb>,
) -> Result<String, ServiceError> {
#[cfg(feature = "turnstile")]
{
let token = match user.token {
Some(token) => token,
None => {
return Err(ServiceError::BadRequest(String::from(
"Invalid challenge token",
)))
}
};

let secret = match &config.turnstile_secret {
Some(secret) => secret,
None => {
return Err(ServiceError::BadRequest(String::from(
"Missing Turnstile secret",
)))
}
};

let url = match &config.turnstile_url {
Some(url) => url,
None => {
return Err(ServiceError::BadRequest(String::from(
"Missing Turnstile URL",
)))
}
};

let idempotency_key = Uuid::new_v4().to_string();

let params: [(&str, Option<&str>); 4] = [
("secret", Some(secret)),
("response", Some(token)),
("remoteip", _remote_ip.as_deref()),
("idempotency_key", Some(&idempotency_key)),
];

let client = reqwest::Client::new();

let response = client
.post(url)
.form(&params)
.send()
.map_err(|e| {
error!("Failed to send request to Turnstile: {}", e);

ServiceError::InternalServerError
})
.await?
.json::<TurnstileResponse>()
.map_err(|e| {
error!("Failed to parse Turnstile response: {}", e);

ServiceError::InternalServerError
})
.await?;

if !response.success {
return Err(ServiceError::BadRequest(String::from(
"Invalid challenge token",
)));
}
}

let record = query_as::<_, models::user::User>(
r#"SELECT username, nickname, password, avatar, email FROM user WHERE username = ?"#,
)
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ pub struct MySQLDb(MySqlPool);
pub struct RedisDb(deadpool_redis::Pool);

pub mod errors;
pub mod guards;
pub mod handlers;
pub mod middlewares;
pub mod models;
pub mod routes;
pub mod state;
Expand Down
1 change: 0 additions & 1 deletion src/middlewares.rs

This file was deleted.

1 change: 1 addition & 0 deletions src/request/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ use serde::Deserialize;
pub struct User<'r> {
pub username: &'r str,
pub password: &'r str,
pub token: Option<&'r str>,
}
15 changes: 15 additions & 0 deletions src/response/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ use cookie::time::Duration;
use rocket::http::{ContentType, Cookie};
use rocket::response::Responder;

#[cfg(feature = "turnstile")]
use serde::Deserialize;

const COOKIE_MAX_AGE: i64 = 2147483647;

#[derive(Debug)]
Expand Down Expand Up @@ -40,3 +43,15 @@ impl<'r> Responder<'r, 'static> for Logout {
.ok()
}
}

#[cfg(feature = "turnstile")]
#[derive(Debug, Deserialize)]
pub struct TurnstileResponse {
pub success: bool,
#[serde(rename(deserialize = "error-codes"))]
pub error_codes: Vec<String>,
pub challenge_ts: Option<String>,
pub hostname: Option<String>,
pub action: Option<String>,
pub cdata: Option<String>,
}
7 changes: 5 additions & 2 deletions src/routes/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use rocket_db_pools::deadpool_redis::redis::AsyncCommands;
use rocket_db_pools::Connection;

use crate::config::Config;
use crate::middlewares::jwt::Middleware;
use crate::guards::{jwt::Middleware, remote_ip::Ip};
use crate::response::auth::Logout;
use crate::state::AppState;
use crate::{handlers, request, response, MySQLDb, RedisDb};
Expand All @@ -19,10 +19,13 @@ pub async fn login(
user: Json<request::auth::User<'_>>,
state: &State<AppState>,
config: &State<Config>,
remote_ip: Ip,
mut db: Connection<MySQLDb>,
mut cache: Connection<RedisDb>,
) -> Result<response::auth::JwtToken, Status> {
let token = handlers::auth::login(user.deref(), state, config, &mut db, &mut cache)
let remote_ip = remote_ip.0;

let token = handlers::auth::login(user.deref(), state, config, remote_ip, &mut db, &mut cache)
.await
.map_err(|e| {
error!("{}", e);
Expand Down
2 changes: 1 addition & 1 deletion src/routes/category.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ use rocket::{delete, get, post, put, State};
use rocket_db_pools::Connection;

use crate::config::Config;
use crate::guards::jwt::Middleware;
use crate::handlers::category::{
self, add_category, delete_category, get_categories, sort_categories, sort_category_sites,
update_category,
};
use crate::middlewares::jwt::Middleware;
use crate::request::category::{CreateCategory, SortCategory, UpdateCategory};
use crate::response::category::Category;
use crate::response::site::Site;
Expand Down
2 changes: 1 addition & 1 deletion src/routes/site.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ use rocket::{delete, get, post, put, State};
use rocket_db_pools::Connection;

use crate::config::Config;
use crate::guards::jwt::Middleware;
use crate::handlers::site;
use crate::handlers::site::get_sites;
use crate::middlewares::jwt::Middleware;
use crate::request::site::{CreateSite, UpdateSite};
use crate::response::site::SiteWithCategory;
use crate::response::WithTotal;
Expand Down
2 changes: 1 addition & 1 deletion src/routes/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use rocket::serde::json::Json;
use rocket::{post, FromForm, State};

use crate::config::Config;
use crate::guards::jwt::Middleware;
use crate::handlers;
use crate::middlewares::jwt::Middleware;

#[derive(FromForm)]
pub struct Upload<'r> {
Expand Down
2 changes: 1 addition & 1 deletion src/routes/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use rocket_db_pools::deadpool_redis::redis::AsyncCommands;
use rocket_db_pools::Connection;

use crate::config::Config;
use crate::guards::jwt::Middleware;
use crate::handlers::user::{get_user, update_user, update_user_password};
use crate::middlewares::jwt::Middleware;
use crate::request::user::{UpdatePassword, UpdateUser};
use crate::response::auth::Logout;
use crate::response::user::User;
Expand Down

0 comments on commit cecb5ad

Please sign in to comment.