Skip to content

Commit 33f4606

Browse files
Merge pull request #15 from BitcoinErrorLog/phase1-security-hardening
Phase 1: Add rate limiting and enhance error types
2 parents 4771ec5 + 1800472 commit 33f4606

File tree

4 files changed

+616
-14
lines changed

4 files changed

+616
-14
lines changed

examples/storage_queue.rs

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,15 @@ fn main() {
7575
let default_config = RetryConfig::default();
7676
println!(" Default config:");
7777
println!(" max_retries: {}", default_config.max_retries);
78-
println!(" initial_backoff_ms: {}", default_config.initial_backoff_ms);
78+
println!(
79+
" initial_backoff_ms: {}",
80+
default_config.initial_backoff_ms
81+
);
7982
println!(" max_backoff_ms: {}", default_config.max_backoff_ms);
80-
println!(" operation_timeout_ms: {}", default_config.operation_timeout_ms);
83+
println!(
84+
" operation_timeout_ms: {}",
85+
default_config.operation_timeout_ms
86+
);
8187

8288
// Custom retry configuration for different scenarios
8389
let aggressive_config = RetryConfig {
@@ -96,11 +102,17 @@ fn main() {
96102

97103
println!("\n Aggressive config (quick retries):");
98104
println!(" max_retries: {}", aggressive_config.max_retries);
99-
println!(" initial_backoff_ms: {}", aggressive_config.initial_backoff_ms);
105+
println!(
106+
" initial_backoff_ms: {}",
107+
aggressive_config.initial_backoff_ms
108+
);
100109

101110
println!("\n Conservative config (patient retries):");
102111
println!(" max_retries: {}", conservative_config.max_retries);
103-
println!(" initial_backoff_ms: {}", conservative_config.initial_backoff_ms);
112+
println!(
113+
" initial_backoff_ms: {}",
114+
conservative_config.initial_backoff_ms
115+
);
104116

105117
// =========================================================================
106118
// Storage-Backed Messaging Setup
@@ -197,9 +209,9 @@ fn main() {
197209
println!(" // Receive messages (polls peer's repository)");
198210
println!(" let messages = messaging.receive_messages(Some(10)).await?;");
199211
println!("\n // Process each message");
200-
println!(" for message in messages {");
201-
println!(" println!(\"Received: {:?}\", message);");
202-
println!(" }");
212+
println!(" for message in messages {{");
213+
println!(" println!(\"Received: {{:?}}\", message);");
214+
println!(" }}");
203215
println!("\n // Get updated counter for persistence");
204216
println!(" let counter = messaging.read_counter();");
205217
println!(" persist_counter(counter);");
@@ -222,9 +234,9 @@ fn main() {
222234
println!(" // Enqueue a message");
223235
println!(" messaging.enqueue(b\"Hello\").await?;");
224236
println!("\n // Dequeue messages");
225-
println!(" while let Some(message) = messaging.dequeue().await? {");
237+
println!(" while let Some(message) = messaging.dequeue().await? {{");
226238
println!(" process_message(message);");
227-
println!(" }");
239+
println!(" }}");
228240
println!(" ```");
229241

230242
// =========================================================================
@@ -240,13 +252,13 @@ fn main() {
240252

241253
println!("\n Error handling pattern:");
242254
println!(" ```rust");
243-
println!(" match messaging.send_message(data).await {");
255+
println!(" match messaging.send_message(data).await {{");
244256
println!(" Ok(_) => println!(\"Message sent\"),");
245-
println!(" Err(NoiseError::Storage(msg)) => {");
257+
println!(" Err(NoiseError::Storage(msg)) => {{");
246258
println!(" // Retry or handle storage error");
247-
println!(" }");
259+
println!(" }}");
248260
println!(" Err(e) => handle_error(e),");
249-
println!(" }");
261+
println!(" }}");
250262
println!(" ```");
251263

252264
// =========================================================================

src/errors.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,20 @@ pub enum NoiseErrorCode {
1818
RemoteStaticMissing = 5001,
1919
/// Policy violation
2020
Policy = 6000,
21+
/// Rate limited
22+
RateLimited = 6001,
23+
/// Maximum sessions exceeded
24+
MaxSessionsExceeded = 6002,
25+
/// Session expired or not found
26+
SessionExpired = 6003,
2127
/// Invalid peer key
2228
InvalidPeerKey = 7000,
2329
/// Network error
2430
Network = 8000,
2531
/// Timeout error
2632
Timeout = 8001,
33+
/// Connection reset
34+
ConnectionReset = 8002,
2735
/// Storage error
2836
Storage = 9000,
2937
/// Decryption error
@@ -55,6 +63,15 @@ pub enum NoiseError {
5563
#[error("policy violation: {0}")]
5664
Policy(String),
5765

66+
#[error("rate limited: {0}")]
67+
RateLimited(String),
68+
69+
#[error("maximum sessions exceeded for this identity")]
70+
MaxSessionsExceeded,
71+
72+
#[error("session expired or not found: {0}")]
73+
SessionExpired(String),
74+
5875
#[error("invalid peer static or shared secret")]
5976
InvalidPeerKey,
6077

@@ -64,6 +81,9 @@ pub enum NoiseError {
6481
#[error("timeout: {0}")]
6582
Timeout(String),
6683

84+
#[error("connection reset: {0}")]
85+
ConnectionReset(String),
86+
6787
#[error("storage error: {0}")]
6888
Storage(String),
6989

@@ -85,9 +105,13 @@ impl NoiseError {
85105
Self::IdentityVerify => NoiseErrorCode::IdentityVerify,
86106
Self::RemoteStaticMissing => NoiseErrorCode::RemoteStaticMissing,
87107
Self::Policy(_) => NoiseErrorCode::Policy,
108+
Self::RateLimited(_) => NoiseErrorCode::RateLimited,
109+
Self::MaxSessionsExceeded => NoiseErrorCode::MaxSessionsExceeded,
110+
Self::SessionExpired(_) => NoiseErrorCode::SessionExpired,
88111
Self::InvalidPeerKey => NoiseErrorCode::InvalidPeerKey,
89112
Self::Network(_) => NoiseErrorCode::Network,
90113
Self::Timeout(_) => NoiseErrorCode::Timeout,
114+
Self::ConnectionReset(_) => NoiseErrorCode::ConnectionReset,
91115
Self::Storage(_) => NoiseErrorCode::Storage,
92116
Self::Decryption(_) => NoiseErrorCode::Decryption,
93117
Self::Other(_) => NoiseErrorCode::Other,
@@ -98,6 +122,31 @@ impl NoiseError {
98122
pub fn message(&self) -> String {
99123
self.to_string()
100124
}
125+
126+
/// Returns true if this error is potentially recoverable by retrying.
127+
pub fn is_retryable(&self) -> bool {
128+
matches!(
129+
self,
130+
Self::Network(_)
131+
| Self::Timeout(_)
132+
| Self::ConnectionReset(_)
133+
| Self::RateLimited(_)
134+
| Self::Storage(_)
135+
)
136+
}
137+
138+
/// Returns a suggested retry delay in milliseconds, if applicable.
139+
pub fn retry_after_ms(&self) -> Option<u64> {
140+
match self {
141+
Self::RateLimited(msg) => {
142+
// Try to parse retry-after from message if encoded
143+
msg.parse().ok()
144+
}
145+
Self::Network(_) | Self::Timeout(_) | Self::ConnectionReset(_) => Some(1000),
146+
Self::Storage(_) => Some(500),
147+
_ => None,
148+
}
149+
}
101150
}
102151

103152
impl From<snow::Error> for NoiseError {

src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub mod mobile_manager;
3333
pub mod pkarr;
3434
#[cfg(feature = "pubky-sdk")]
3535
pub mod pubky_ring;
36+
pub mod rate_limiter;
3637
pub mod ring;
3738
pub mod server;
3839
pub mod session_id;
@@ -51,8 +52,9 @@ pub use mobile_manager::{ConnectionStatus, MobileConfig, NoiseManager, SessionSt
5152
pub use pkarr::{DummyPkarr, PkarrNoiseRecord, PkarrResolver};
5253
#[cfg(feature = "pubky-sdk")]
5354
pub use pubky_ring::PubkyRingProvider;
55+
pub use rate_limiter::{RateLimitReason, RateLimitResult, RateLimiter, RateLimiterConfig};
5456
pub use ring::{DummyRing, RingKeyFiller, RingKeyProvider};
55-
pub use server::NoiseServer;
57+
pub use server::{NoiseServer, ServerPolicy};
5658
pub use session_id::SessionId;
5759
pub use session_manager::{NoiseRole, NoiseSessionManager, ThreadSafeSessionManager};
5860
#[cfg(feature = "storage-queue")]

0 commit comments

Comments
 (0)