merge: 同步主线并解决用户侧验证冲突

This commit is contained in:
Entropy.Xu
2026-05-16 01:25:42 +08:00
349 changed files with 1737 additions and 73751 deletions

View File

@@ -407,7 +407,6 @@ pub(crate) fn admin_provider_pool_config_from_config_value(
overload_cooldown_seconds: 30,
health_policy_enabled: true,
probing_enabled: false,
probing_interval_minutes: 10,
probing_target_percent: None,
probing_target_count: None,
probe_concurrency: 4,
@@ -475,12 +474,6 @@ pub(crate) fn admin_provider_pool_config_from_config_value(
.get("probing_enabled")
.and_then(Value::as_bool)
.unwrap_or(false),
probing_interval_minutes: pool_advanced
.get("probing_interval_minutes")
.and_then(json_u64)
.filter(|value| *value > 0)
.map(|value| value.min(1440))
.unwrap_or(10),
probing_target_percent: parse_pool_probe_target_percent(pool_advanced),
probing_target_count: parse_pool_probe_target_count(pool_advanced),
probe_concurrency: pool_advanced
@@ -592,7 +585,6 @@ mod tests {
"overload_cooldown_seconds": 45,
"health_policy_enabled": false,
"probing_enabled": true,
"probing_interval_minutes": 20,
"probing_target_percent": 25,
"probing_target_count": 3,
"probe_concurrency": 6,
@@ -634,7 +626,6 @@ mod tests {
assert_eq!(config.overload_cooldown_seconds, 45);
assert!(!config.health_policy_enabled);
assert!(config.probing_enabled);
assert_eq!(config.probing_interval_minutes, 20);
assert_eq!(config.probing_target_percent, Some(25.0));
assert_eq!(config.probing_target_count, Some(3));
assert_eq!(config.probe_concurrency, 6);
@@ -656,7 +647,7 @@ mod tests {
}
#[test]
fn clamps_pool_quota_probe_interval_to_python_range() {
fn ignores_legacy_pool_quota_probe_interval() {
let provider = sample_provider(json!({
"pool_advanced": {
"probing_enabled": true,
@@ -664,7 +655,7 @@ mod tests {
}
}));
let config = admin_provider_pool_config(&provider).expect("pool config should exist");
assert_eq!(config.probing_interval_minutes, 1440);
assert!(config.probing_enabled);
let provider = sample_provider(json!({
"pool_advanced": {
@@ -673,7 +664,7 @@ mod tests {
}
}));
let config = admin_provider_pool_config(&provider).expect("pool config should exist");
assert_eq!(config.probing_interval_minutes, 10);
assert!(config.probing_enabled);
}
#[test]

View File

@@ -614,7 +614,6 @@ mod tests {
overload_cooldown_seconds: 30,
health_policy_enabled: true,
probing_enabled: false,
probing_interval_minutes: 10,
probing_target_percent: None,
probing_target_count: None,
probe_concurrency: 4,

View File

@@ -45,7 +45,6 @@ pub(crate) struct AdminProviderPoolConfig {
pub(crate) overload_cooldown_seconds: u64,
pub(crate) health_policy_enabled: bool,
pub(crate) probing_enabled: bool,
pub(crate) probing_interval_minutes: u64,
pub(crate) probing_target_percent: Option<f64>,
pub(crate) probing_target_count: Option<u64>,
pub(crate) probe_concurrency: u64,

View File

@@ -1054,6 +1054,10 @@ pub(crate) async fn proxy_request(
&state,
&request_context,
&parts.headers,
parts
.extensions
.get::<crate::middleware::CfConnectingIp>()
.map(|value| value.0.as_str()),
local_proxy_body.as_ref(),
)
.await

View File

@@ -110,6 +110,7 @@ pub(crate) async fn maybe_build_local_public_support_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
headers: &http::HeaderMap,
cf_connecting_ip: Option<&str>,
request_body: Option<&Bytes>,
) -> Option<Response<Body>> {
let decision = request_context.control_decision.as_ref()?;
@@ -118,8 +119,14 @@ pub(crate) async fn maybe_build_local_public_support_response(
}
if decision.route_family.as_deref() == Some("auth") {
return maybe_build_local_auth_response(state, request_context, headers, request_body)
.await;
return maybe_build_local_auth_response(
state,
request_context,
headers,
cf_connecting_ip,
request_body,
)
.await;
}
if decision.route_family.as_deref() == Some("oauth") {

View File

@@ -262,6 +262,7 @@ pub(super) async fn maybe_build_local_auth_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
headers: &http::HeaderMap,
cf_connecting_ip: Option<&str>,
request_body: Option<&axum::body::Bytes>,
) -> Option<Response<Body>> {
let decision = request_context.control_decision.as_ref()?;
@@ -273,13 +274,16 @@ pub(super) async fn maybe_build_local_auth_response(
Some("send_verification_code")
if request_context.request_path == "/api/auth/send-verification-code" =>
{
Some(handle_auth_send_verification_code(state, request_body).await)
Some(
handle_auth_send_verification_code(state, headers, cf_connecting_ip, request_body)
.await,
)
}
Some("login") if request_context.request_path == "/api/auth/login" => {
Some(handle_auth_login(state, request_context, headers, request_body).await)
}
Some("register") if request_context.request_path == "/api/auth/register" => {
Some(handle_auth_register(state, request_body).await)
Some(handle_auth_register(state, headers, cf_connecting_ip, request_body).await)
}
Some("verify_email") if request_context.request_path == "/api/auth/verify-email" => {
Some(handle_auth_verify_email(state, request_body).await)
@@ -329,10 +333,15 @@ mod tests {
async fn auth_unhandled_route_returns_local_not_implemented_response() {
let state = AppState::new().expect("gateway should build");
let request_context = request_context(Method::POST, "/api/auth/login/history", "login");
let response =
maybe_build_local_auth_response(&state, &request_context, &HeaderMap::new(), None)
.await
.expect("auth handler should return response");
let response = maybe_build_local_auth_response(
&state,
&request_context,
&HeaderMap::new(),
None,
None,
)
.await
.expect("auth handler should return response");
assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED);
let body = to_bytes(response.into_body(), usize::MAX)

View File

@@ -1,7 +1,7 @@
use super::{
auth_turnstile_public_settings, http, json, ldap_module_config_is_valid,
module_available_from_env, system_config_bool, system_config_string, AppState, Body,
GatewayError, GatewayPublicRequestContext, IntoResponse, Json, Response,
http, json, ldap_module_config_is_valid, module_available_from_env, system_config_bool,
system_config_string, AppState, Body, GatewayError, GatewayPublicRequestContext, IntoResponse,
Json, Response,
};
pub(crate) async fn build_auth_registration_settings_payload(
@@ -20,6 +20,12 @@ pub(crate) async fn build_auth_registration_settings_payload(
let password_policy_level_config = state
.read_system_config_json_value("password_policy_level")
.await?;
let turnstile_enabled_config = state
.read_system_config_json_value("turnstile_enabled")
.await?;
let turnstile_site_key_config = state
.read_system_config_json_value("turnstile_site_key")
.await?;
let email_configured = smtp_host
.as_ref()
@@ -40,17 +46,17 @@ pub(crate) async fn build_auth_registration_settings_payload(
Some(value) if matches!(value.as_str(), "weak" | "medium" | "strong") => value,
_ => "weak".to_string(),
};
let turnstile_settings = auth_turnstile_public_settings(state)
.await
.map_err(|err| GatewayError::Internal(err.detail))?;
let turnstile_enabled = system_config_bool(turnstile_enabled_config.as_ref(), false);
let turnstile_site_key = system_config_string(turnstile_site_key_config.as_ref());
Ok(json!({
"enable_registration": enable_registration,
"require_email_verification": require_email_verification,
"email_configured": email_configured,
"password_policy_level": password_policy_level,
"turnstile_enabled": turnstile_settings.enabled,
"turnstile_site_key": turnstile_settings.site_key,
"turnstile_enabled": turnstile_enabled,
"turnstile_site_key": turnstile_site_key,
"turnstile_required_actions": ["send_verification_code", "register"],
}))
}
@@ -295,6 +301,17 @@ pub(super) fn auth_client_ip(headers: &http::HeaderMap) -> Option<String> {
})
}
pub(super) fn auth_client_ip_with_cf(
headers: &http::HeaderMap,
cf_connecting_ip: Option<&str>,
) -> Option<String> {
cf_connecting_ip
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|value| value.chars().take(45).collect())
.or_else(|| auth_client_ip(headers))
}
pub(super) fn normalize_auth_login_identifier(value: &str) -> String {
let normalized = value.trim();
if normalized.contains('@') {

View File

@@ -5,8 +5,8 @@ use super::{
clear_auth_email_pending_code, clear_auth_email_verification, generate_auth_verification_code,
http, json, mark_auth_email_verified, read_auth_email_verification_code, read_auth_smtp_config,
send_auth_email, store_auth_email_verification_code, system_config_bool, system_config_f64,
system_config_string, system_config_string_list, verify_auth_turnstile_token, AppState, Body,
GatewayError, Regex, Response,
system_config_string, system_config_string_list, verify_auth_turnstile, AppState,
AuthTurnstileAction, Body, GatewayError, Regex, Response,
};
use serde::Deserialize;
@@ -179,6 +179,8 @@ async fn validate_auth_email_suffix(
pub(super) async fn handle_auth_send_verification_code(
state: &AppState,
headers: &http::HeaderMap,
cf_connecting_ip: Option<&str>,
request_body: Option<&axum::body::Bytes>,
) -> Response<Body> {
let Some(request_body) = request_body else {
@@ -198,6 +200,18 @@ pub(super) async fn handle_auth_send_verification_code(
return build_auth_error_response(http::StatusCode::BAD_REQUEST, "邮箱格式无效", false);
};
if let Err(response) = verify_auth_turnstile(
state,
headers,
cf_connecting_ip,
payload.turnstile_token.as_deref(),
AuthTurnstileAction::SendVerificationCode,
)
.await
{
return response;
}
match validate_auth_email_suffix(state, &email).await {
Ok(Ok(())) => {}
Ok(Err(detail)) => {
@@ -212,12 +226,6 @@ pub(super) async fn handle_auth_send_verification_code(
}
}
if let Err(err) =
verify_auth_turnstile_token(state, payload.turnstile_token.as_deref(), None).await
{
return build_auth_error_response(err.status, err.detail, false);
}
if state
.find_user_auth_by_identifier(&email)
.await
@@ -325,6 +333,8 @@ pub(super) async fn handle_auth_send_verification_code(
pub(super) async fn handle_auth_register(
state: &AppState,
headers: &http::HeaderMap,
cf_connecting_ip: Option<&str>,
request_body: Option<&axum::body::Bytes>,
) -> Response<Body> {
let Some(request_body) = request_body else {
@@ -379,6 +389,18 @@ pub(super) async fn handle_auth_register(
return build_auth_error_response(http::StatusCode::FORBIDDEN, "系统暂不开放注册", false);
}
if let Err(response) = verify_auth_turnstile(
state,
headers,
cf_connecting_ip,
payload.turnstile_token.as_deref(),
AuthTurnstileAction::Register,
)
.await
{
return response;
}
let email_configured = match auth_registration_email_configured(state).await {
Ok(value) => value,
Err(err) => {
@@ -430,10 +452,6 @@ pub(super) async fn handle_auth_register(
);
}
}
} else if let Err(err) =
verify_auth_turnstile_token(state, payload.turnstile_token.as_deref(), None).await
{
return build_auth_error_response(err.status, err.detail, false);
}
if let Some(email) = email.as_deref() {
match validate_auth_email_suffix(state, email).await {

View File

@@ -1,22 +1,28 @@
use super::{
decrypt_catalog_secret_with_fallbacks, http, system_config_bool, system_config_string,
system_config_string_list, AppState,
auth_client_ip_with_cf, build_auth_error_response, decrypt_catalog_secret_with_fallbacks, http,
system_config_bool, system_config_string, system_config_string_list, AppState, Body, Response,
};
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use tracing::warn;
const TURNSTILE_SITEVERIFY_URL: &str = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
const TURNSTILE_TOKEN_MAX_LEN: usize = 2048;
const TURNSTILE_SITEVERIFY_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Debug, Clone)]
pub(super) struct AuthTurnstilePublicSettings {
pub(super) enabled: bool,
pub(super) site_key: Option<String>,
#[derive(Debug, Clone, Copy)]
pub(super) enum AuthTurnstileAction {
SendVerificationCode,
Register,
}
#[derive(Debug)]
pub(super) struct AuthTurnstileError {
pub(super) status: http::StatusCode,
pub(super) detail: String,
impl AuthTurnstileAction {
pub(super) const fn as_str(self) -> &'static str {
match self {
Self::SendVerificationCode => "send_verification_code",
Self::Register => "register",
}
}
}
#[derive(Debug)]
@@ -24,61 +30,220 @@ struct AuthTurnstileConfig {
enabled: bool,
site_key: Option<String>,
secret_key: Option<String>,
siteverify_url: String,
allowed_hostnames: Vec<String>,
}
#[derive(Debug, Serialize)]
struct TurnstileSiteverifyRequest<'a> {
secret: &'a str,
response: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
remoteip: Option<&'a str>,
idempotency_key: String,
}
#[derive(Debug, Deserialize)]
struct TurnstileSiteverifyResponse {
#[serde(default)]
success: bool,
#[serde(default)]
action: Option<String>,
#[serde(default)]
hostname: Option<String>,
#[serde(default, rename = "error-codes")]
error_codes: Vec<String>,
}
fn turnstile_error(status: http::StatusCode, detail: impl Into<String>) -> AuthTurnstileError {
AuthTurnstileError {
status,
detail: detail.into(),
enum AuthTurnstileFailure {
BadRequest(&'static str),
ServiceUnavailable(&'static str),
}
impl AuthTurnstileFailure {
fn into_response(self) -> Response<Body> {
match self {
Self::BadRequest(detail) => {
build_auth_error_response(http::StatusCode::BAD_REQUEST, detail, false)
}
Self::ServiceUnavailable(detail) => {
build_auth_error_response(http::StatusCode::SERVICE_UNAVAILABLE, detail, false)
}
}
}
}
fn turnstile_config_error(detail: impl Into<String>) -> AuthTurnstileError {
turnstile_error(http::StatusCode::INTERNAL_SERVER_ERROR, detail)
pub(super) async fn verify_auth_turnstile(
state: &AppState,
headers: &http::HeaderMap,
cf_connecting_ip: Option<&str>,
token: Option<&str>,
action: AuthTurnstileAction,
) -> Result<(), Response<Body>> {
match verify_auth_turnstile_inner(state, headers, cf_connecting_ip, token, action).await {
Ok(()) => Ok(()),
Err(err) => Err(err.into_response()),
}
}
async fn verify_auth_turnstile_inner(
state: &AppState,
headers: &http::HeaderMap,
cf_connecting_ip: Option<&str>,
token: Option<&str>,
action: AuthTurnstileAction,
) -> Result<(), AuthTurnstileFailure> {
let config = read_auth_turnstile_config(state).await?;
if !config.enabled {
return Ok(());
}
let (Some(_site_key), Some(secret_key)) =
(config.site_key.as_deref(), config.secret_key.as_deref())
else {
warn!("turnstile is enabled but site key or secret key is missing");
return Err(AuthTurnstileFailure::ServiceUnavailable(
"人机验证服务暂不可用,请稍后重试",
));
};
let token = token
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or(AuthTurnstileFailure::BadRequest("请先完成人机验证"))?;
if token.len() > TURNSTILE_TOKEN_MAX_LEN {
warn!(
token_len = token.len(),
"turnstile token exceeds maximum length"
);
return Err(AuthTurnstileFailure::BadRequest("人机验证失败,请重试"));
}
let remoteip = auth_client_ip_with_cf(headers, cf_connecting_ip);
let siteverify_request = TurnstileSiteverifyRequest {
secret: secret_key,
response: token,
remoteip: remoteip.as_deref(),
idempotency_key: uuid::Uuid::new_v4().to_string(),
};
let siteverify_url = turnstile_siteverify_url(state);
let response = tokio::time::timeout(
turnstile_siteverify_timeout(state),
state
.client
.post(siteverify_url)
.form(&siteverify_request)
.send(),
)
.await
.map_err(|_| {
warn!("turnstile siteverify request timed out");
AuthTurnstileFailure::ServiceUnavailable("人机验证服务暂不可用,请稍后重试")
})?
.map_err(|err| {
warn!(error = %err, "turnstile siteverify request failed");
AuthTurnstileFailure::ServiceUnavailable("人机验证服务暂不可用,请稍后重试")
})?;
if !response.status().is_success() {
let status = response.status().as_u16();
warn!(
status,
"turnstile siteverify returned non-success HTTP status"
);
return Err(AuthTurnstileFailure::ServiceUnavailable(
"人机验证服务暂不可用,请稍后重试",
));
}
let payload = response
.json::<TurnstileSiteverifyResponse>()
.await
.map_err(|err| {
warn!(error = %err, "turnstile siteverify response decode failed");
AuthTurnstileFailure::ServiceUnavailable("人机验证服务暂不可用,请稍后重试")
})?;
if !payload.success {
warn!(
error_codes = ?payload.error_codes,
action = ?payload.action,
hostname = ?payload.hostname,
"turnstile siteverify rejected token"
);
if turnstile_siteverify_error_is_service_unavailable(&payload.error_codes) {
return Err(AuthTurnstileFailure::ServiceUnavailable(
"人机验证服务暂不可用,请稍后重试",
));
}
return Err(AuthTurnstileFailure::BadRequest("人机验证失败,请重试"));
}
if payload.action.as_deref() != Some(action.as_str()) {
warn!(
expected_action = action.as_str(),
actual_action = ?payload.action,
"turnstile siteverify action mismatch"
);
return Err(AuthTurnstileFailure::BadRequest("人机验证失败,请重试"));
}
if !config.allowed_hostnames.is_empty() {
let Some(hostname) = payload.hostname.as_deref().map(str::to_ascii_lowercase) else {
warn!("turnstile siteverify response missing hostname");
return Err(AuthTurnstileFailure::BadRequest("人机验证失败,请重试"));
};
if !config
.allowed_hostnames
.iter()
.any(|allowed| allowed == &hostname)
{
warn!(
hostname = %hostname,
allowed_hostnames = ?config.allowed_hostnames,
"turnstile siteverify hostname mismatch"
);
return Err(AuthTurnstileFailure::BadRequest("人机验证失败,请重试"));
}
}
Ok(())
}
fn turnstile_siteverify_error_is_service_unavailable(error_codes: &[String]) -> bool {
error_codes.iter().any(|code| {
matches!(
code.trim().to_ascii_lowercase().as_str(),
"missing-input-secret" | "invalid-input-secret" | "internal-error"
)
})
}
async fn read_auth_turnstile_config(
state: &AppState,
) -> Result<AuthTurnstileConfig, AuthTurnstileError> {
) -> Result<AuthTurnstileConfig, AuthTurnstileFailure> {
let enabled = state
.read_system_config_json_value("turnstile_enabled")
.await
.map_err(|err| {
turnstile_config_error(format!("auth turnstile settings lookup failed: {err:?}"))
warn!(error = ?err, "turnstile enabled config lookup failed");
AuthTurnstileFailure::ServiceUnavailable("人机验证服务暂不可用,请稍后重试")
})?;
let site_key = state
.read_system_config_json_value("turnstile_site_key")
.await
.map_err(|err| {
turnstile_config_error(format!("auth turnstile settings lookup failed: {err:?}"))
warn!(error = ?err, "turnstile site key config lookup failed");
AuthTurnstileFailure::ServiceUnavailable("人机验证服务暂不可用,请稍后重试")
})?;
let secret_key = state
.read_system_config_json_value("turnstile_secret_key")
.await
.map_err(|err| {
turnstile_config_error(format!("auth turnstile settings lookup failed: {err:?}"))
})?;
let siteverify_url = state
.read_system_config_json_value("turnstile_siteverify_url")
.await
.map_err(|err| {
turnstile_config_error(format!("auth turnstile settings lookup failed: {err:?}"))
warn!(error = ?err, "turnstile secret key config lookup failed");
AuthTurnstileFailure::ServiceUnavailable("人机验证服务暂不可用,请稍后重试")
})?;
let allowed_hostnames = state
.read_system_config_json_value("turnstile_allowed_hostnames")
.await
.map_err(|err| {
turnstile_config_error(format!("auth turnstile settings lookup failed: {err:?}"))
warn!(error = ?err, "turnstile hostname config lookup failed");
AuthTurnstileFailure::ServiceUnavailable("人机验证服务暂不可用,请稍后重试")
})?;
let secret_key = system_config_string(secret_key.as_ref()).map(|value| {
@@ -89,121 +254,22 @@ async fn read_auth_turnstile_config(
enabled: system_config_bool(enabled.as_ref(), false),
site_key: system_config_string(site_key.as_ref()),
secret_key,
siteverify_url: system_config_string(siteverify_url.as_ref())
.unwrap_or_else(|| TURNSTILE_SITEVERIFY_URL.to_string()),
allowed_hostnames: system_config_string_list(allowed_hostnames.as_ref()),
})
}
pub(super) async fn auth_turnstile_public_settings(
state: &AppState,
) -> Result<AuthTurnstilePublicSettings, AuthTurnstileError> {
let config = read_auth_turnstile_config(state).await?;
let enabled = config.enabled && config.site_key.is_some();
Ok(AuthTurnstilePublicSettings {
enabled,
site_key: enabled.then_some(config.site_key).flatten(),
})
fn turnstile_siteverify_url(state: &AppState) -> &str {
#[cfg(test)]
if let Some(url) = state.turnstile_siteverify_url_override.as_deref() {
return url;
}
TURNSTILE_SITEVERIFY_URL
}
fn turnstile_service_error(error_codes: &[String]) -> bool {
error_codes.iter().any(|code| {
matches!(
code.trim(),
"missing-input-secret" | "invalid-input-secret" | "internal-error"
)
})
}
fn turnstile_hostname_allowed(hostname: Option<&str>, allowed_hostnames: &[String]) -> bool {
if allowed_hostnames.is_empty() {
return true;
}
let Some(hostname) = hostname.map(str::trim).filter(|value| !value.is_empty()) else {
return false;
};
let hostname = hostname.to_ascii_lowercase();
allowed_hostnames.iter().any(|allowed| allowed == &hostname)
}
pub(super) async fn verify_auth_turnstile_token(
state: &AppState,
token: Option<&str>,
remote_ip: Option<&str>,
) -> Result<(), AuthTurnstileError> {
let config = read_auth_turnstile_config(state).await?;
if !config.enabled || config.site_key.is_none() {
return Ok(());
}
let token = token
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| turnstile_error(http::StatusCode::BAD_REQUEST, "请先完成人机验证"))?;
let secret_key = config.secret_key.as_deref().ok_or_else(|| {
turnstile_error(http::StatusCode::SERVICE_UNAVAILABLE, "人机验证服务未配置")
})?;
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(8))
.build()
.map_err(|err| {
turnstile_error(
http::StatusCode::SERVICE_UNAVAILABLE,
format!("人机验证服务暂不可用: {err}"),
)
})?;
let mut form = vec![
("secret", secret_key.to_string()),
("response", token.to_string()),
];
if let Some(remote_ip) = remote_ip.map(str::trim).filter(|value| !value.is_empty()) {
form.push(("remoteip", remote_ip.to_string()));
}
let response = client
.post(config.siteverify_url)
.form(&form)
.send()
.await
.map_err(|_| {
turnstile_error(
http::StatusCode::SERVICE_UNAVAILABLE,
"人机验证服务暂不可用",
)
})?;
if !response.status().is_success() {
return Err(turnstile_error(
http::StatusCode::SERVICE_UNAVAILABLE,
"人机验证服务暂不可用",
));
}
let payload = response
.json::<TurnstileSiteverifyResponse>()
.await
.map_err(|_| {
turnstile_error(
http::StatusCode::SERVICE_UNAVAILABLE,
"人机验证服务暂不可用",
)
})?;
if payload.success {
if turnstile_hostname_allowed(payload.hostname.as_deref(), &config.allowed_hostnames) {
return Ok(());
}
return Err(turnstile_error(
http::StatusCode::BAD_REQUEST,
"人机验证失败,请重试",
));
}
if turnstile_service_error(&payload.error_codes) {
return Err(turnstile_error(
http::StatusCode::SERVICE_UNAVAILABLE,
"人机验证服务暂不可用",
));
}
Err(turnstile_error(
http::StatusCode::BAD_REQUEST,
"人机验证失败,请重试",
))
fn turnstile_siteverify_timeout(state: &AppState) -> Duration {
#[cfg(test)]
if let Some(timeout) = state.turnstile_siteverify_timeout_override {
return timeout;
}
TURNSTILE_SITEVERIFY_TIMEOUT
}