openzeppelin_relayer/config/
server_config.rs1use std::env;
3
4use crate::{constants::MINIMUM_SECRET_VALUE_LENGTH, models::SecretString};
5
6#[derive(Debug, Clone)]
7pub struct ServerConfig {
8 pub host: String,
10 pub port: u16,
12 pub redis_url: String,
14 pub config_file_path: String,
16 pub api_key: SecretString,
18 pub rate_limit_requests_per_second: u64,
20 pub rate_limit_burst_size: u32,
22 pub metrics_port: u16,
24 pub enable_swagger: bool,
26 pub redis_connection_timeout_ms: u64,
28 pub rpc_timeout_ms: u64,
30 pub provider_max_retries: u8,
32 pub provider_retry_base_delay_ms: u64,
34 pub provider_retry_max_delay_ms: u64,
36 pub provider_max_failovers: u8,
38}
39
40impl ServerConfig {
41 pub fn from_env() -> Self {
61 let conf_dir = if env::var("IN_DOCKER")
62 .map(|val| val == "true")
63 .unwrap_or(false)
64 {
65 "config/".to_string()
66 } else {
67 env::var("CONFIG_DIR").unwrap_or_else(|_| "./config".to_string())
68 };
69
70 let conf_dir = format!("{}/", conf_dir.trim_end_matches('/'));
71
72 let config_file_name =
74 env::var("CONFIG_FILE_NAME").unwrap_or_else(|_| "config.json".to_string());
75
76 let config_file_path = format!("{}{}", conf_dir, config_file_name);
78
79 let api_key = SecretString::new(&env::var("API_KEY").expect("API_KEY must be set"));
80
81 if !api_key.has_minimum_length(MINIMUM_SECRET_VALUE_LENGTH) {
82 panic!(
83 "Security error: API_KEY must be at least {} characters long",
84 MINIMUM_SECRET_VALUE_LENGTH
85 );
86 }
87
88 Self {
89 host: env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string()),
90 port: env::var("APP_PORT")
91 .unwrap_or_else(|_| "8080".to_string())
92 .parse()
93 .unwrap_or(8080),
94 redis_url: env::var("REDIS_URL").expect("REDIS_URL must be set"),
95 config_file_path,
96 api_key,
97 rate_limit_requests_per_second: env::var("RATE_LIMIT_REQUESTS_PER_SECOND")
98 .unwrap_or_else(|_| "100".to_string())
99 .parse()
100 .unwrap_or(100),
101 rate_limit_burst_size: env::var("RATE_LIMIT_BURST_SIZE")
102 .unwrap_or_else(|_| "300".to_string())
103 .parse()
104 .unwrap_or(300),
105 metrics_port: env::var("METRICS_PORT")
106 .unwrap_or_else(|_| "8081".to_string())
107 .parse()
108 .unwrap_or(8081),
109 enable_swagger: env::var("ENABLE_SWAGGER")
110 .map(|v| v.to_lowercase() == "true")
111 .unwrap_or(false),
112 redis_connection_timeout_ms: env::var("REDIS_CONNECTION_TIMEOUT_MS")
113 .unwrap_or_else(|_| "10000".to_string())
114 .parse()
115 .unwrap_or(10000),
116 rpc_timeout_ms: env::var("RPC_TIMEOUT_MS")
117 .unwrap_or_else(|_| "10000".to_string())
118 .parse()
119 .unwrap_or(10000),
120 provider_max_retries: env::var("PROVIDER_MAX_RETRIES")
121 .unwrap_or_else(|_| "3".to_string())
122 .parse()
123 .unwrap_or(3),
124 provider_retry_base_delay_ms: env::var("PROVIDER_RETRY_BASE_DELAY_MS")
125 .unwrap_or_else(|_| "100".to_string())
126 .parse()
127 .unwrap_or(100),
128 provider_retry_max_delay_ms: env::var("PROVIDER_RETRY_MAX_DELAY_MS")
129 .unwrap_or_else(|_| "2000".to_string())
130 .parse()
131 .unwrap_or(2000),
132 provider_max_failovers: env::var("PROVIDER_MAX_FAILOVERS")
133 .unwrap_or_else(|_| "3".to_string())
134 .parse()
135 .unwrap_or(3),
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use lazy_static::lazy_static;
144 use std::env;
145 use std::sync::Mutex;
146
147 lazy_static! {
149 static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
150 }
151
152 fn setup() {
153 env::remove_var("HOST");
155 env::remove_var("APP_PORT");
156 env::remove_var("REDIS_URL");
157 env::remove_var("CONFIG_DIR");
158 env::remove_var("CONFIG_FILE_NAME");
159 env::remove_var("CONFIG_FILE_PATH");
160 env::remove_var("API_KEY");
161 env::remove_var("RATE_LIMIT_REQUESTS_PER_SECOND");
162 env::remove_var("RATE_LIMIT_BURST_SIZE");
163 env::remove_var("METRICS_PORT");
164 env::remove_var("REDIS_CONNECTION_TIMEOUT_MS");
165 env::remove_var("RPC_TIMEOUT_MS");
166 env::remove_var("PROVIDER_MAX_RETRIES");
167 env::remove_var("PROVIDER_RETRY_BASE_DELAY_MS");
168 env::remove_var("PROVIDER_RETRY_MAX_DELAY_MS");
169 env::remove_var("PROVIDER_MAX_FAILOVERS");
170 env::set_var("REDIS_URL", "redis://localhost:6379");
172 env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
173 env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "5000");
174 }
175
176 #[test]
177 fn test_default_values() {
178 let _lock = match ENV_MUTEX.lock() {
179 Ok(guard) => guard,
180 Err(poisoned) => poisoned.into_inner(),
181 };
182 setup();
183
184 let config = ServerConfig::from_env();
185
186 assert_eq!(config.host, "0.0.0.0");
187 assert_eq!(config.port, 8080);
188 assert_eq!(config.redis_url, "redis://localhost:6379");
189 assert_eq!(config.config_file_path, "./config/config.json");
190 assert_eq!(
191 config.api_key,
192 SecretString::new("7EF1CB7C-5003-4696-B384-C72AF8C3E15D")
193 );
194 assert_eq!(config.rate_limit_requests_per_second, 100);
195 assert_eq!(config.rate_limit_burst_size, 300);
196 assert_eq!(config.metrics_port, 8081);
197 assert_eq!(config.redis_connection_timeout_ms, 5000);
198 assert_eq!(config.rpc_timeout_ms, 10000);
199 assert_eq!(config.provider_max_retries, 3);
200 assert_eq!(config.provider_retry_base_delay_ms, 100);
201 assert_eq!(config.provider_retry_max_delay_ms, 2000);
202 assert_eq!(config.provider_max_failovers, 3);
203 }
204
205 #[test]
206 fn test_invalid_port_values() {
207 let _lock = match ENV_MUTEX.lock() {
208 Ok(guard) => guard,
209 Err(poisoned) => poisoned.into_inner(),
210 };
211 setup();
212 env::set_var("REDIS_URL", "redis://localhost:6379");
213 env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
214 env::set_var("APP_PORT", "not_a_number");
215 env::set_var("METRICS_PORT", "also_not_a_number");
216 env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "invalid");
217 env::set_var("RATE_LIMIT_BURST_SIZE", "invalid");
218 env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "invalid");
219 env::set_var("RPC_TIMEOUT_MS", "invalid");
220 env::set_var("PROVIDER_MAX_RETRIES", "invalid");
221 env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "invalid");
222 env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "invalid");
223 env::set_var("PROVIDER_MAX_FAILOVERS", "invalid");
224 let config = ServerConfig::from_env();
225
226 assert_eq!(config.port, 8080);
228 assert_eq!(config.metrics_port, 8081);
229 assert_eq!(config.rate_limit_requests_per_second, 100);
230 assert_eq!(config.rate_limit_burst_size, 300);
231 assert_eq!(config.redis_connection_timeout_ms, 10000);
232 assert_eq!(config.rpc_timeout_ms, 10000);
233 assert_eq!(config.provider_max_retries, 3);
234 assert_eq!(config.provider_retry_base_delay_ms, 100);
235 assert_eq!(config.provider_retry_max_delay_ms, 2000);
236 assert_eq!(config.provider_max_failovers, 3);
237 }
238
239 #[test]
240 fn test_custom_values() {
241 let _lock = match ENV_MUTEX.lock() {
242 Ok(guard) => guard,
243 Err(poisoned) => poisoned.into_inner(),
244 };
245 setup();
246
247 env::set_var("HOST", "127.0.0.1");
248 env::set_var("APP_PORT", "9090");
249 env::set_var("REDIS_URL", "redis://custom:6379");
250 env::set_var("CONFIG_DIR", "custom");
251 env::set_var("CONFIG_FILE_NAME", "path.json");
252 env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D");
253 env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "200");
254 env::set_var("RATE_LIMIT_BURST_SIZE", "500");
255 env::set_var("METRICS_PORT", "9091");
256 env::set_var("REDIS_CONNECTION_TIMEOUT_MS", "10000");
257 env::set_var("RPC_TIMEOUT_MS", "33333");
258 env::set_var("PROVIDER_MAX_RETRIES", "5");
259 env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "200");
260 env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "3000");
261 env::set_var("PROVIDER_MAX_FAILOVERS", "4");
262 let config = ServerConfig::from_env();
263
264 assert_eq!(config.host, "127.0.0.1");
265 assert_eq!(config.port, 9090);
266 assert_eq!(config.redis_url, "redis://custom:6379");
267 assert_eq!(config.config_file_path, "custom/path.json");
268 assert_eq!(
269 config.api_key,
270 SecretString::new("7EF1CB7C-5003-4696-B384-C72AF8C3E15D")
271 );
272 assert_eq!(config.rate_limit_requests_per_second, 200);
273 assert_eq!(config.rate_limit_burst_size, 500);
274 assert_eq!(config.metrics_port, 9091);
275 assert_eq!(config.redis_connection_timeout_ms, 10000);
276 assert_eq!(config.rpc_timeout_ms, 33333);
277 assert_eq!(config.provider_max_retries, 5);
278 assert_eq!(config.provider_retry_base_delay_ms, 200);
279 assert_eq!(config.provider_retry_max_delay_ms, 3000);
280 assert_eq!(config.provider_max_failovers, 4);
281 }
282
283 #[test]
284 #[should_panic(expected = "Security error: API_KEY must be at least 32 characters long")]
285 fn test_invalid_api_key_length() {
286 let _lock = match ENV_MUTEX.lock() {
287 Ok(guard) => guard,
288 Err(poisoned) => poisoned.into_inner(),
289 };
290 setup();
291 env::set_var("REDIS_URL", "redis://localhost:6379");
292 env::set_var("API_KEY", "insufficient_length");
293 env::set_var("APP_PORT", "8080");
294 env::set_var("RATE_LIMIT_REQUESTS_PER_SECOND", "100");
295 env::set_var("RATE_LIMIT_BURST_SIZE", "300");
296 env::set_var("METRICS_PORT", "9091");
297
298 let _ = ServerConfig::from_env();
299
300 panic!("Test should have panicked before reaching here");
301 }
302}