From 2d4317d5f06036d7b2cb443d0a33cb827e6bf8bd Mon Sep 17 00:00:00 2001 From: quran Date: Thu, 8 Jan 2026 23:44:17 +0800 Subject: [PATCH 1/2] feat(security): deprecate weak hash algorithms and enforce strong secrets - Mark SHA-1 and MD5 as deprecated in HashAlgorithm enum with security warnings - Add minimum 32-character requirement for JWT secret keys to prevent brute-force attacks - Implement secret length validation in jwt_sign function with detailed error messages - Update JWT examples to use strong secrets with at least 32 characters - Add comprehensive tests for weak secret rejection and minimum valid secret acceptance - Remove deprecated test binary configuration and source file - Add request body size validation in HTTP client to prevent DoS attacks - Introduce max_request_size configuration with 10MB default and 100MB maximum - Add validation for request size limits in HTTP configuration - Reduce WebSocket pong timeout from 90 seconds to 30 seconds for faster zombie detection - Improve circuit breaker failure recording with atomic state transitions - Enhance rate limiter calculations using u128 to prevent overflow for long uptimes - Update documentation comments and test descriptions with security considerations --- Cargo.toml | 4 - ccxt-core/src/auth.rs | 217 ++++++++++++++++++++++++++--- ccxt-core/src/base_exchange/mod.rs | 1 + ccxt-core/src/circuit_breaker.rs | 92 ++++++++---- ccxt-core/src/http_client.rs | 85 ++++++++++- ccxt-core/src/precision.rs | 4 +- ccxt-core/src/rate_limiter.rs | 38 +++-- ccxt-core/src/retry_strategy.rs | 2 +- ccxt-core/src/test_config.rs | 2 +- ccxt-core/src/types/financial.rs | 2 +- ccxt-core/src/types/mark_price.rs | 4 +- ccxt-core/src/ws_client/config.rs | 2 +- ccxt-core/src/ws_client/mod.rs | 12 ++ test_u64_since.rs | 24 ---- 14 files changed, 389 insertions(+), 100 deletions(-) delete mode 100644 test_u64_since.rs diff --git a/Cargo.toml b/Cargo.toml index b2634b6..e0a126b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -141,10 +141,6 @@ url = { workspace = true } regex = { workspace = true } paste = { workspace = true } -[[bin]] -name = "test_u64_since" -path = "test_u64_since.rs" - [dev-dependencies] tokio-test = { workspace = true } criterion = { workspace = true } diff --git a/ccxt-core/src/auth.rs b/ccxt-core/src/auth.rs index 537a1ff..d736bff 100644 --- a/ccxt-core/src/auth.rs +++ b/ccxt-core/src/auth.rs @@ -23,18 +23,50 @@ use sha3::Keccak256; use std::fmt; /// Supported cryptographic hash algorithms. +/// +/// # Security Warning +/// +/// **MD5 and SHA-1 are deprecated** due to known cryptographic vulnerabilities. +/// Use [Sha256](Self::Sha256) or stronger algorithms for new implementations. +/// The deprecated variants are maintained only for backward compatibility with +/// legacy exchanges that still require them. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HashAlgorithm { /// SHA-1 hash algorithm + /// + /// # Security Warning + /// + /// **DEPRECATED** - SHA-1 has been cryptographically broken since 2017. + /// Collision attacks are practical. Do NOT use for new implementations. + /// Use [Sha256](Self::Sha256) instead. + #[deprecated( + since = "0.1.3", + note = "SHA-1 is cryptographically broken, use Sha256 instead" + )] Sha1, - /// SHA-256 hash algorithm + + /// SHA-256 hash algorithm (recommended) Sha256, + /// SHA-384 hash algorithm Sha384, + /// SHA-512 hash algorithm Sha512, + /// MD5 hash algorithm + /// + /// # Security Warning + /// + /// **DEPRECATED** - MD5 has been cryptographically broken since 2004. + /// Collision attacks are practical. Do NOT use for new implementations. + /// Use [Sha256](Self::Sha256) instead. + #[deprecated( + since = "0.1.3", + note = "MD5 is cryptographically broken, use Sha256 instead" + )] Md5, + /// Keccak-256 (SHA-3) hash algorithm Keccak, } @@ -386,7 +418,7 @@ pub fn eddsa_sign(data: &str, secret_key: &[u8]) -> Result { /// /// # Arguments /// * `payload` - JWT payload as JSON object -/// * `secret` - Secret key for signing +/// * `secret` - Secret key for signing (must be at least 32 characters for security) /// * `algorithm` - Hash algorithm for HMAC (supports Sha256, Sha384, Sha512) /// * `header_options` - Optional additional header fields /// @@ -398,6 +430,14 @@ pub fn eddsa_sign(data: &str, secret_key: &[u8]) -> Result { /// - JSON serialization fails /// - Signing fails /// - Unsupported algorithm is provided (only HS256, HS384, HS512 are supported) +/// - Secret key is less than 32 characters (security requirement) +/// +/// # Security +/// +/// **Minimum secret length: 32 characters** +/// +/// Short secrets are vulnerable to brute-force attacks. The minimum 32-character +/// requirement ensures sufficient entropy for secure HMAC signatures. /// /// # Examples /// ``` @@ -409,11 +449,21 @@ pub fn eddsa_sign(data: &str, secret_key: &[u8]) -> Result { /// "exp": 1234567890 /// }); /// -/// // Using HS256 -/// let token = jwt_sign(&payload, "secret", HashAlgorithm::Sha256, None).unwrap(); +/// // Using HS256 with a strong secret (at least 32 characters) +/// let token = jwt_sign( +/// &payload, +/// "my-very-secure-secret-key-at-least-32-chars", +/// HashAlgorithm::Sha256, +/// None +/// ).unwrap(); /// /// // Using HS512 -/// let token_512 = jwt_sign(&payload, "secret", HashAlgorithm::Sha512, None).unwrap(); +/// let token_512 = jwt_sign( +/// &payload, +/// "my-very-secure-secret-key-at-least-32-chars", +/// HashAlgorithm::Sha512, +/// None +/// ).unwrap(); /// ``` pub fn jwt_sign( payload: &serde_json::Value, @@ -421,6 +471,18 @@ pub fn jwt_sign( algorithm: HashAlgorithm, header_options: Option>, ) -> Result { + // Validate secret key strength (minimum 32 characters for security) + const MIN_SECRET_LENGTH: usize = 32; + + if secret.len() < MIN_SECRET_LENGTH { + return Err(Error::invalid_argument(format!( + "JWT secret must be at least {MIN_SECRET_LENGTH} characters for security. \ + Provided: {} characters. \ + Use a longer secret to protect against brute-force attacks.", + secret.len() + ))); + } + // Map HashAlgorithm to JWT algorithm string let alg_str = match algorithm { HashAlgorithm::Sha256 => "HS256", @@ -552,7 +614,7 @@ mod tests { #[test] fn test_hash_keccak() { let result = hash("test", HashAlgorithm::Keccak, DigestFormat::Hex).unwrap(); - assert_eq!(result.len(), 64); // Keccak256输出32字节=64个hex字符 + assert_eq!(result.len(), 64); // Keccak256 outputs 32 bytes = 64 hex characters } #[test] @@ -578,9 +640,16 @@ mod tests { "exp": 1234567890 }); - let token = jwt_sign(&payload, "secret", HashAlgorithm::Sha256, None).unwrap(); + // Use a strong secret (at least 32 characters) + let token = jwt_sign( + &payload, + "my-very-secure-secret-key-at-least-32-chars", + HashAlgorithm::Sha256, + None, + ) + .unwrap(); - // JWT应该有3部分,用.分隔 + // JWT should have 3 parts separated by . let parts: Vec<&str> = token.split('.').collect(); assert_eq!(parts.len(), 3); @@ -599,20 +668,22 @@ mod tests { "exp": 1234567890 }); + let strong_secret = "my-very-secure-secret-key-at-least-32-chars"; + // Test HS256 - let token_256 = jwt_sign(&payload, "secret", HashAlgorithm::Sha256, None).unwrap(); + let token_256 = jwt_sign(&payload, strong_secret, HashAlgorithm::Sha256, None).unwrap(); let parts_256: Vec<&str> = token_256.split('.').collect(); let header_256 = String::from_utf8(base64url_decode(parts_256[0]).unwrap()).unwrap(); assert!(header_256.contains("HS256")); // Test HS384 - let token_384 = jwt_sign(&payload, "secret", HashAlgorithm::Sha384, None).unwrap(); + let token_384 = jwt_sign(&payload, strong_secret, HashAlgorithm::Sha384, None).unwrap(); let parts_384: Vec<&str> = token_384.split('.').collect(); let header_384 = String::from_utf8(base64url_decode(parts_384[0]).unwrap()).unwrap(); assert!(header_384.contains("HS384")); // Test HS512 - let token_512 = jwt_sign(&payload, "secret", HashAlgorithm::Sha512, None).unwrap(); + let token_512 = jwt_sign(&payload, strong_secret, HashAlgorithm::Sha512, None).unwrap(); let parts_512: Vec<&str> = token_512.split('.').collect(); let header_512 = String::from_utf8(base64url_decode(parts_512[0]).unwrap()).unwrap(); assert!(header_512.contains("HS512")); @@ -628,16 +699,31 @@ mod tests { }); // SHA1 is not supported for JWT - let result = jwt_sign(&payload, "secret", HashAlgorithm::Sha1, None); + let result = jwt_sign( + &payload, + "my-very-secure-secret-key-at-least-32-chars", + HashAlgorithm::Sha1, + None, + ); assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("does not support")); // MD5 is not supported for JWT - let result = jwt_sign(&payload, "secret", HashAlgorithm::Md5, None); + let result = jwt_sign( + &payload, + "my-very-secure-secret-key-at-least-32-chars", + HashAlgorithm::Md5, + None, + ); assert!(result.is_err()); // Keccak is not supported for JWT - let result = jwt_sign(&payload, "secret", HashAlgorithm::Keccak, None); + let result = jwt_sign( + &payload, + "my-very-secure-secret-key-at-least-32-chars", + HashAlgorithm::Keccak, + None, + ); assert!(result.is_err()); } @@ -659,18 +745,115 @@ mod tests { assert_eq!(DigestFormat::from_str("hex"), DigestFormat::Hex); assert_eq!(DigestFormat::from_str("base64"), DigestFormat::Base64); assert_eq!(DigestFormat::from_str("binary"), DigestFormat::Binary); - assert_eq!(DigestFormat::from_str("unknown"), DigestFormat::Hex); // 默认 + assert_eq!(DigestFormat::from_str("unknown"), DigestFormat::Hex); // defaults to Hex } #[test] fn test_hmac_sha512() { let result = hmac_sign("test", "secret", HashAlgorithm::Sha512, DigestFormat::Hex).unwrap(); - assert_eq!(result.len(), 128); // SHA512输出64字节=128个hex字符 + assert_eq!(result.len(), 128); // SHA512 outputs 64 bytes = 128 hex characters } #[test] fn test_hash_md5() { let result = hash("test", HashAlgorithm::Md5, DigestFormat::Hex).unwrap(); - assert_eq!(result.len(), 32); // MD5输出16字节=32个hex字符 + assert_eq!(result.len(), 32); // MD5 outputs 16 bytes = 32 hex characters + } + + #[test] + fn test_jwt_sign_weak_secret_rejected() { + use serde_json::json; + + let payload = json!({ + "user_id": "123", + "exp": 1234567890 + }); + + // Test various weak secrets (less than 32 characters) + let weak_secrets = vec![ + "", // empty + "a", // 1 character + "short", // 5 characters + "this-is-still-too-short-123", // 31 characters (just below threshold) + ]; + + for weak_secret in weak_secrets { + let result = jwt_sign(&payload, weak_secret, HashAlgorithm::Sha256, None); + assert!( + result.is_err(), + "Secret with {} characters should be rejected", + weak_secret.len() + ); + + if let Err(e) = result { + let error_msg = e.to_string(); + assert!( + error_msg.contains("32 characters"), + "Error message should mention 32 character requirement" + ); + assert!( + error_msg.contains("security"), + "Error message should mention security" + ); + } + } + } + + #[test] + fn test_jwt_sign_minimum_valid_secret() { + use serde_json::json; + + let payload = json!({ + "user_id": "123", + "exp": 1234567890 + }); + + // Test exactly 32 characters (should succeed) + let exactly_32_chars = "12345678901234567890123456789012"; // exactly 32 chars + let result = jwt_sign(&payload, exactly_32_chars, HashAlgorithm::Sha256, None); + assert!( + result.is_ok(), + "Secret with exactly 32 characters should be accepted" + ); + + // Test 33 characters (should succeed) + let exactly_33_chars = "123456789012345678901234567890123"; // exactly 33 chars + let result = jwt_sign(&payload, exactly_33_chars, HashAlgorithm::Sha256, None); + assert!( + result.is_ok(), + "Secret with 33 characters should be accepted" + ); + } + + #[test] + fn test_jwt_sign_with_custom_header_and_strong_secret() { + use serde_json::json; + + let payload = json!({ + "user_id": "123", + "exp": 1234567890 + }); + + let mut custom_header = serde_json::Map::new(); + custom_header.insert( + "kid".to_string(), + serde_json::Value::String("key-123".to_string()), + ); + + let strong_secret = "my-very-secure-secret-key-at-least-32-chars"; + let token = jwt_sign( + &payload, + strong_secret, + HashAlgorithm::Sha256, + Some(custom_header), + ) + .unwrap(); + + let parts: Vec<&str> = token.split('.').collect(); + assert_eq!(parts.len(), 3); + + let header_bytes = base64url_decode(parts[0]).unwrap(); + let header_str = String::from_utf8(header_bytes).unwrap(); + assert!(header_str.contains("\"kid\":\"key-123\"")); } } diff --git a/ccxt-core/src/base_exchange/mod.rs b/ccxt-core/src/base_exchange/mod.rs index 69b909e..0534669 100644 --- a/ccxt-core/src/base_exchange/mod.rs +++ b/ccxt-core/src/base_exchange/mod.rs @@ -94,6 +94,7 @@ impl BaseExchange { ..crate::retry_strategy::RetryConfig::default() }), max_response_size: 10 * 1024 * 1024, // 10MB default + max_request_size: 10 * 1024 * 1024, // 10MB default circuit_breaker: None, // Disabled by default for backward compatibility pool_max_idle_per_host: 10, // Default: 10 idle connections per host pool_idle_timeout: Duration::from_secs(90), // Default: 90 seconds diff --git a/ccxt-core/src/circuit_breaker.rs b/ccxt-core/src/circuit_breaker.rs index 18276d3..aa83351 100644 --- a/ccxt-core/src/circuit_breaker.rs +++ b/ccxt-core/src/circuit_breaker.rs @@ -511,40 +511,80 @@ impl CircuitBreaker { /// breaker.record_failure(); /// ``` pub fn record_failure(&self) { - let state = self.state(); + // ATOMIC STATE TRANSITION with failure counting + // We use a CAS loop that combines state check and transition to avoid race conditions + // where multiple threads could update last_failure_time inconsistently. + let mut current_state = self.state.load(Ordering::Acquire); + + loop { + if current_state == CircuitState::Open as u8 { + // Already Open - just increment failure count for monitoring, no state transition needed + let prev_failures = self.failure_count.fetch_add(1, Ordering::AcqRel); + let current_failures = prev_failures + 1; - match state { - CircuitState::Closed => { - let count = self.failure_count.fetch_add(1, Ordering::AcqRel) + 1; debug!( - failure_count = count, + failure_count = current_failures, threshold = self.config.failure_threshold, - "Circuit breaker: Failure recorded in Closed state" + "Circuit breaker: Failure recorded (already Open)" ); - self.emit_event(CircuitBreakerEvent::FailureRecorded { count }); + self.emit_event(CircuitBreakerEvent::FailureRecorded { + count: current_failures, + }); + return; + } - if count >= self.config.failure_threshold { - warn!( - failure_count = count, - "Circuit breaker: Failure threshold reached, transitioning to Open" - ); - self.transition_to(CircuitState::Open); - self.last_failure_time - .store(chrono::Utc::now().timestamp_millis(), Ordering::Release); + // For Closed/HalfOpen states: atomically increment and check threshold + let prev_failures = self.failure_count.fetch_add(1, Ordering::AcqRel); + let current_failures = prev_failures + 1; + + debug!( + failure_count = current_failures, + threshold = self.config.failure_threshold, + "Circuit breaker: Failure recorded" + ); + + self.emit_event(CircuitBreakerEvent::FailureRecorded { + count: current_failures, + }); + + // Check if we should transition to Open + if current_failures >= self.config.failure_threshold { + // CAS: Atomically check and transition state + // Only the thread that successfully transitions will update last_failure_time + match self.state.compare_exchange_weak( + current_state, + CircuitState::Open as u8, + Ordering::AcqRel, + Ordering::Acquire, + ) { + Ok(_) => { + // Successfully transitioned to Open state - only this thread updates timestamp + warn!( + failure_count = current_failures, + "Circuit breaker: Transitioned to Open state (atomic)" + ); + self.last_failure_time + .store(chrono::Utc::now().timestamp_millis(), Ordering::Release); + return; + } + Err(actual) => { + // CAS failed: another thread modified state + // If it's now Open, we're done (another thread handled the transition) + if actual == CircuitState::Open as u8 { + return; + } + // Otherwise retry with the new state + current_state = actual; + // Note: failure_count was already incremented, which is correct + // We just need to retry the state transition + continue; + } } } - CircuitState::HalfOpen => { - warn!("Circuit breaker: Failure in HalfOpen state, transitioning back to Open"); - self.transition_to(CircuitState::Open); - self.last_failure_time - .store(chrono::Utc::now().timestamp_millis(), Ordering::Release); - self.success_count.store(0, Ordering::Release); - } - CircuitState::Open => { - // No effect in Open state - debug!("Circuit breaker: Failure recorded in Open state (no effect)"); - } + + // Threshold not reached, we're done + return; } } diff --git a/ccxt-core/src/http_client.rs b/ccxt-core/src/http_client.rs index 29ea0de..05ed10c 100644 --- a/ccxt-core/src/http_client.rs +++ b/ccxt-core/src/http_client.rs @@ -56,6 +56,12 @@ pub struct HttpConfig { /// This protects against malicious or abnormal responses that could exhaust memory. pub max_response_size: usize, + /// Maximum request body size in bytes (default: 10MB) + /// + /// Request bodies exceeding this limit will be rejected BEFORE serialization. + /// This protects against DoS attacks via oversized request payloads. + pub max_request_size: usize, + /// Optional circuit breaker configuration. /// /// When enabled, the circuit breaker will track request failures and @@ -97,6 +103,7 @@ impl Default for HttpConfig { enable_rate_limit: true, retry_config: None, // Uses default retry configuration max_response_size: 10 * 1024 * 1024, // 10MB default + max_request_size: 10 * 1024 * 1024, // 10MB default circuit_breaker: None, // Disabled by default for backward compatibility pool_max_idle_per_host: 10, // Default: 10 idle connections per host pool_idle_timeout: Duration::from_secs(90), // Default: 90 seconds @@ -137,6 +144,9 @@ impl HttpConfig { /// assert!(result.is_err()); /// ``` pub fn validate(&self) -> std::result::Result { + // Maximum reasonable request size (100MB) - defined at the start to satisfy clippy + const MAX_REASONABLE_REQUEST_SIZE: usize = 100 * 1024 * 1024; + let mut warnings = Vec::new(); // Validate timeout <= 5 minutes @@ -157,6 +167,23 @@ impl HttpConfig { )); } + // Validate max_request_size > 0 + if self.max_request_size == 0 { + return Err(ConfigValidationError::invalid( + "max_request_size", + "max_request_size cannot be zero", + )); + } + + // Validate max_request_size <= 100MB (prevent memory exhaustion attacks) + if self.max_request_size > MAX_REASONABLE_REQUEST_SIZE { + return Err(ConfigValidationError::too_high( + "max_request_size", + format!("{}", self.max_request_size), + "100MB (104857600 bytes)", + )); + } + Ok(ValidationResult::with_warnings(warnings)) } } @@ -457,7 +484,19 @@ impl HttpClient { } if let Some(ref body) = body { - request = request.json(&body); + // Validate request body size BEFORE serialization (DoS protection) + let body_str = serde_json::to_string(body) + .map_err(|e| Error::invalid_request(format!("JSON serialization failed: {e}")))?; + + if body_str.len() > self.config.max_request_size { + return Err(Error::invalid_request(format!( + "Request body {} bytes exceeds limit {} bytes", + body_str.len(), + self.config.max_request_size + ))); + } + + request = request.body(body_str); } if self.config.verbose { @@ -1223,6 +1262,50 @@ mod tests { assert!(!result.unwrap().warnings.is_empty()); } + #[test] + fn test_http_config_validate_max_request_size_zero() { + let config = HttpConfig { + max_request_size: 0, + ..Default::default() + }; + let result = config.validate(); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.field_name(), "max_request_size"); + } + + #[test] + fn test_http_config_validate_max_request_size_too_large() { + // max_request_size > 100MB should be rejected + let config = HttpConfig { + max_request_size: 101 * 1024 * 1024, // 101MB + ..Default::default() + }; + let result = config.validate(); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.field_name(), "max_request_size"); + } + + #[test] + fn test_http_config_validate_max_request_size_boundary() { + // max_request_size = 100MB should be valid + let config = HttpConfig { + max_request_size: 100 * 1024 * 1024, // 100MB + ..Default::default() + }; + let result = config.validate(); + assert!(result.is_ok()); + + // max_request_size = 100MB + 1 byte should be rejected + let config = HttpConfig { + max_request_size: 100 * 1024 * 1024 + 1, + ..Default::default() + }; + let result = config.validate(); + assert!(result.is_err()); + } + #[test] fn test_http_config_max_response_size_default() { let config = HttpConfig::default(); diff --git a/ccxt-core/src/precision.rs b/ccxt-core/src/precision.rs index 76fb148..362b29c 100644 --- a/ccxt-core/src/precision.rs +++ b/ccxt-core/src/precision.rs @@ -169,7 +169,7 @@ pub fn truncate(value: Decimal, precision: i32) -> Decimal { /// /// let num = Decimal::from_str("123.456789").unwrap(); /// -/// // 保留2位小数,四舍五入 +/// // Round to 2 decimal places /// let result = decimal_to_precision( /// num, /// RoundingMode::Round, @@ -179,7 +179,7 @@ pub fn truncate(value: Decimal, precision: i32) -> Decimal { /// ).unwrap(); /// assert_eq!(result, "123.46"); /// -/// // 截断到2位小数 +/// // Truncate to 2 decimal places /// let result = decimal_to_precision( /// num, /// RoundingMode::RoundDown, diff --git a/ccxt-core/src/rate_limiter.rs b/ccxt-core/src/rate_limiter.rs index 6d74a23..f05474d 100644 --- a/ccxt-core/src/rate_limiter.rs +++ b/ccxt-core/src/rate_limiter.rs @@ -177,38 +177,32 @@ impl RateLimiterState { /// /// The `remainder_nanos` field tracks fractional periods that haven't yet /// accumulated to a full refill period, ensuring no time is lost between refills. - #[allow(clippy::cast_possible_truncation)] + /// + /// Uses u128 for all intermediate calculations to prevent overflow for 10+ years of uptime. fn refill(&mut self) { let now = Instant::now(); - // Note: as_nanos() returns u128, but for practical durations (< 584 years), - // u64 is sufficient. We use saturating conversion to handle edge cases. - let elapsed_nanos = now - .duration_since(self.last_refill) - .as_nanos() - .min(u128::from(u64::MAX)) as u64; - let period_nanos = self - .config - .refill_period - .as_nanos() - .min(u128::from(u64::MAX)) as u64; + // Use u128 for all time calculations (prevents overflow for 10+ years) + let elapsed_nanos = now.duration_since(self.last_refill).as_nanos(); // u128 + let period_nanos = self.config.refill_period.as_nanos(); // u128 // Avoid division by zero (should not happen with valid config) if period_nanos == 0 { return; } - // Add elapsed time to accumulated remainder - let total_nanos = self.remainder_nanos.saturating_add(elapsed_nanos); + // Add elapsed time to accumulated remainder using saturating_add to prevent overflow + // Even though u128 is very large, using saturating_add ensures safety in extreme edge cases + let total_nanos = u128::from(self.remainder_nanos).saturating_add(elapsed_nanos); // Calculate complete periods using integer division let complete_periods = total_nanos / period_nanos; if complete_periods > 0 { - // Calculate tokens to add using integer arithmetic - // Use u64 for intermediate calculation to avoid overflow - let tokens_to_add = complete_periods - .saturating_mul(u64::from(self.config.refill_amount)) - .min(u64::from(u32::MAX)) as u32; + // Calculate tokens to add using u128 arithmetic (prevents overflow) + // Truncation is safe here because we explicitly cap at u32::MAX + #[allow(clippy::cast_possible_truncation)] + let tokens_to_add = (complete_periods * u128::from(self.config.refill_amount)) + .min(u128::from(u32::MAX)) as u32; // Add tokens up to capacity self.tokens = self @@ -217,7 +211,11 @@ impl RateLimiterState { .min(self.config.capacity); // Store remainder for next refill (preserves fractional periods) - self.remainder_nanos = total_nanos % period_nanos; + // Truncation is safe: remainder is always < period_nanos, which fits in u64 + // (period_nanos comes from Duration::as_nanos() which is typically < u64::MAX) + #[allow(clippy::cast_possible_truncation)] + let remainder = (total_nanos % period_nanos) as u64; + self.remainder_nanos = remainder; self.last_refill = now; } } diff --git a/ccxt-core/src/retry_strategy.rs b/ccxt-core/src/retry_strategy.rs index f1554e9..65ef3d3 100644 --- a/ccxt-core/src/retry_strategy.rs +++ b/ccxt-core/src/retry_strategy.rs @@ -693,7 +693,7 @@ mod tests { fn test_rate_limit_error_minimum_delay() { let config = RetryConfig { strategy_type: RetryStrategyType::Fixed, - base_delay_ms: 100, // 很短的基础延迟 + base_delay_ms: 100, // very short base delay jitter_factor: 0.0, ..Default::default() }; diff --git a/ccxt-core/src/test_config.rs b/ccxt-core/src/test_config.rs index de25b1f..7bd0b9b 100644 --- a/ccxt-core/src/test_config.rs +++ b/ccxt-core/src/test_config.rs @@ -518,7 +518,7 @@ mod tests { #[test] fn test_from_env_with_defaults() { - // 测试在没有环境变量时使用默认值 + // test using defaults when no env vars are set let config = TestConfig::from_env().unwrap(); assert_eq!(config.test_timeout_ms, 30000); assert_eq!(config.test_data.fixtures_dir, "tests/fixtures"); diff --git a/ccxt-core/src/types/financial.rs b/ccxt-core/src/types/financial.rs index c03c177..d55124c 100644 --- a/ccxt-core/src/types/financial.rs +++ b/ccxt-core/src/types/financial.rs @@ -852,7 +852,7 @@ mod tests { let doubled = price * dec!(2); assert_eq!(doubled.as_decimal(), dec!(100000.0)); - // 测试交换律 + // test commutativity let doubled2 = dec!(2) * price; assert_eq!(doubled2.as_decimal(), dec!(100000.0)); } diff --git a/ccxt-core/src/types/mark_price.rs b/ccxt-core/src/types/mark_price.rs index 3f664a3..8a36af3 100644 --- a/ccxt-core/src/types/mark_price.rs +++ b/ccxt-core/src/types/mark_price.rs @@ -331,7 +331,7 @@ mod tests { None, None, None, - Some(1637008000000), // 8000秒后 + Some(1637008000000), // 8000 seconds later None, 1637000000000, ); @@ -342,7 +342,7 @@ mod tests { fn test_is_deviation_excessive() { let mark_price = MarkPrice::new( "BTC/USDT".to_string(), - dec!(50600), // 1.2% 偏离 + dec!(50600), // 1.2% deviation Some(dec!(50000)), None, None, diff --git a/ccxt-core/src/ws_client/config.rs b/ccxt-core/src/ws_client/config.rs index afc8fd0..11330d0 100644 --- a/ccxt-core/src/ws_client/config.rs +++ b/ccxt-core/src/ws_client/config.rs @@ -168,7 +168,7 @@ impl Default for WsConfig { max_reconnect_attempts: 5, auto_reconnect: true, enable_compression: false, - pong_timeout: 90000, + pong_timeout: 30000, // 30 seconds default (detects zombie connections faster) backoff_config: BackoffConfig::default(), max_subscriptions: DEFAULT_MAX_SUBSCRIPTIONS, shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT, diff --git a/ccxt-core/src/ws_client/mod.rs b/ccxt-core/src/ws_client/mod.rs index 793ce8a..1c589dc 100644 --- a/ccxt-core/src/ws_client/mod.rs +++ b/ccxt-core/src/ws_client/mod.rs @@ -714,7 +714,18 @@ impl WsClient { let elapsed = now - last_pong; #[allow(clippy::cast_possible_wrap)] if elapsed > pong_timeout_ms as i64 { + // Pong timeout detected - log detailed diagnostics + error!( + pong_timeout_ms = pong_timeout_ms, + elapsed_ms = elapsed, + last_pong_time = last_pong, + current_time = now, + "WebSocket pong timeout detected - connection appears unresponsive (zombie connection)" + ); ping_state.store(WsConnectionState::Error.as_u8(), Ordering::Release); + debug!( + "WebSocket state set to Error due to pong timeout - AutoReconnectCoordinator will trigger reconnection if enabled" + ); break; } } @@ -726,6 +737,7 @@ impl WsClient { .try_send(Message::Ping(vec![].into())) .is_err() { + debug!("WebSocket write channel closed, stopping ping loop"); break; } } diff --git a/test_u64_since.rs b/test_u64_since.rs deleted file mode 100644 index 8964ac1..0000000 --- a/test_u64_since.rs +++ /dev/null @@ -1,24 +0,0 @@ -use ccxt_core::ExchangeConfig; -use ccxt_exchanges::binance::Binance; - -#[tokio::main] -async fn main() -> Result<(), Box> { - // 创建一个 Binance 实例 - let config = ExchangeConfig::default(); - let _binance = Binance::new(config)?; - - // 测试 u64 类型的 since 参数 - let since: Option = Some(1609459200000); // 2021-01-01 00:00:00 UTC - let limit: Option = Some(10); - - println!("Testing u64 since parameter types:"); - println!("since: {:?}", since); - println!("limit: {:?}", limit); - - // 这些调用会编译成功,证明我们的类型更改是正确的 - // 注意:这些调用会失败,因为我们没有提供 API 凭据,但重要的是类型检查通过了 - - println!("✅ All type checks passed! u64 since parameters are working correctly."); - - Ok(()) -} From 539dcc616394765fbd23253e2319e00b414ce76c Mon Sep 17 00:00:00 2001 From: quran Date: Thu, 8 Jan 2026 23:52:16 +0800 Subject: [PATCH 2/2] chore(auth): add deprecated attribute allowances for hash algorithm implementations - Added #[allow(deprecated)] attribute to HashAlgorithm::from_str method - Added #[allow(deprecated)] attribute to HashAlgorithm Display implementation - Added #[allow(deprecated)] attribute to hmac signing function - Added #[allow(deprecated)] attribute to hash function implementation --- ccxt-core/src/auth.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ccxt-core/src/auth.rs b/ccxt-core/src/auth.rs index d736bff..000cae7 100644 --- a/ccxt-core/src/auth.rs +++ b/ccxt-core/src/auth.rs @@ -82,6 +82,7 @@ impl HashAlgorithm { // Lint: should_implement_trait // Reason: This method returns Result with custom error type, not compatible with FromStr trait #[allow(clippy::should_implement_trait)] + #[allow(deprecated)] pub fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "sha1" => Ok(HashAlgorithm::Sha1), @@ -98,6 +99,7 @@ impl HashAlgorithm { } impl fmt::Display for HashAlgorithm { + #[allow(deprecated)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { HashAlgorithm::Sha1 => "sha1", @@ -177,6 +179,7 @@ pub fn hmac_sign( algorithm: HashAlgorithm, digest: DigestFormat, ) -> Result { + #[allow(deprecated)] let signature = match algorithm { HashAlgorithm::Sha256 => hmac_sha256(message.as_bytes(), secret.as_bytes()), HashAlgorithm::Sha512 => hmac_sha512(message.as_bytes(), secret.as_bytes()), @@ -269,6 +272,7 @@ fn hmac_md5(data: &[u8], secret: &[u8]) -> Vec { /// let hashed = hash("test", HashAlgorithm::Sha256, DigestFormat::Hex).unwrap(); /// ``` pub fn hash(data: &str, algorithm: HashAlgorithm, digest: DigestFormat) -> Result { + #[allow(deprecated)] let hash_bytes = match algorithm { HashAlgorithm::Sha256 => hash_sha256(data.as_bytes()), HashAlgorithm::Sha512 => hash_sha512(data.as_bytes()),