1use std::{collections::HashMap, net::SocketAddr, time::Duration};
46
47pub fn parse_byte_size(size: &str) -> Option<usize> {
49 let size = size.trim();
50 let size = size.to_uppercase();
51
52 let size = if size.ends_with("B") {
53 size[..size.len() - 1].to_string()
54 } else {
55 size
56 };
57
58 let size = size.replace(",", "");
59
60 let size = if size.ends_with("K") {
61 size[..size.len() - 1].parse::<f64>().unwrap_or(0.0) * 1024.0
62 } else if size.ends_with("M") {
63 size[..size.len() - 1].parse::<f64>().unwrap_or(0.0) * 1024.0 * 1024.0
64 } else if size.ends_with("G") {
65 size[..size.len() - 1].parse::<f64>().unwrap_or(0.0) * 1024.0 * 1024.0 * 1024.0
66 } else {
67 size.parse::<f64>().unwrap_or(0.0)
68 };
69
70 Some(size as usize)
71}
72
73#[derive(Debug, PartialEq, Clone)]
74pub struct ApplicationProperties {
75 pub server: Server,
76 pub environment: String,
77 pub banner: Banner,
78
79 pub etc: HashMap<String, String>,
80}
81
82impl Default for ApplicationProperties {
83 fn default() -> Self {
84 ApplicationProperties {
85 server: Server::default(),
86 environment: "dev".to_string(),
87 etc: HashMap::new(),
88 banner: Banner::default(),
89 }
90 }
91}
92
93#[derive(Debug, PartialEq, Clone)]
94pub enum CompressionAlgorithm {
95 Gzip,
96 Deflate,
97 Unknown(String),
98}
99
100#[derive(Debug, PartialEq, Clone)]
101pub struct Compression {
102 pub enabled: bool,
103 pub mime_types: Vec<String>,
104 pub min_response_size: usize,
105 pub algorithm: CompressionAlgorithm,
106}
107
108impl ToString for CompressionAlgorithm {
109 fn to_string(&self) -> String {
110 match self {
111 CompressionAlgorithm::Gzip => "gzip".to_string(),
112 CompressionAlgorithm::Deflate => "deflate".to_string(),
113 CompressionAlgorithm::Unknown(s) => s.to_string(),
114 }
115 }
116}
117
118impl From<String> for CompressionAlgorithm {
119 fn from(s: String) -> Self {
120 match s.as_str() {
121 "gzip" => CompressionAlgorithm::Gzip,
122 "deflate" => CompressionAlgorithm::Deflate,
123 _ => CompressionAlgorithm::Unknown(s),
124 }
125 }
126}
127
128impl Default for Compression {
129 fn default() -> Self {
130 Compression {
131 enabled: false,
132 mime_types: [
133 "text/html",
134 "text/xml",
135 "text/plain",
136 "text/css",
137 "text/javascript",
138 "application/javascript",
139 "application/json",
140 "application/xml",
141 ]
142 .iter()
143 .map(|s| s.to_string())
144 .collect(),
145 min_response_size: 2048, algorithm: CompressionAlgorithm::Gzip,
147 }
148 }
149}
150
151#[derive(Debug, PartialEq, Clone)]
152pub struct Banner {
153 pub enabled: bool,
154 pub charset: String,
155 pub location: Option<String>,
156}
157
158impl Default for Banner {
159 fn default() -> Self {
160 Banner {
161 enabled: true,
162 charset: "UTF-8".to_string(),
163 location: None,
164 }
165 }
166}
167
168#[derive(Debug, PartialEq, Clone)]
169pub enum ShutdownType {
170 Immediate,
171 Graceful,
172}
173
174impl From<String> for ShutdownType {
175 fn from(s: String) -> Self {
176 match s.as_str() {
177 "immediate" => ShutdownType::Immediate,
178 "graceful" => ShutdownType::Graceful,
179 _ => ShutdownType::Immediate,
180 }
181 }
182}
183
184#[derive(Debug, PartialEq, Clone, Default)]
185pub struct SSL {
186 pub key: String,
187 pub cert: String,
188}
189
190#[derive(Debug, PartialEq, Clone, Default)]
191pub struct Http1 {
192 pub keep_alive: bool,
193}
194
195#[derive(Debug, PartialEq, Clone)]
196pub struct Multipart {
197 pub auto_parsing_enabled: bool,
198}
199
200impl Default for Multipart {
201 fn default() -> Self {
202 Multipart {
203 auto_parsing_enabled: true,
204 }
205 }
206}
207
208#[derive(Debug, PartialEq, Clone)]
209pub struct Cookie {
210 pub auto_parsing_enabled: bool,
211}
212
213impl Default for Cookie {
214 fn default() -> Self {
215 Cookie {
216 auto_parsing_enabled: true,
217 }
218 }
219}
220
221#[derive(Debug, PartialEq, Clone, Default)]
222pub struct RequestURIConfig {
223 pub max_length: Option<usize>,
224}
225
226#[derive(Debug, PartialEq, Clone, Default)]
227pub struct RequestHeaderConfig {
228 pub max_length: Option<usize>,
229 pub max_number_of_headers: Option<usize>,
230}
231
232#[derive(Debug, PartialEq, Clone)]
233pub struct RequestBodyConfig {
234 pub max_length: usize,
235}
236
237impl Default for RequestBodyConfig {
238 fn default() -> Self {
239 RequestBodyConfig {
240 max_length: 2 * 1000 * 1000, }
242 }
243}
244
245#[derive(Debug, PartialEq, Clone, Default)]
246pub struct RequestConfig {
247 pub uri: RequestURIConfig,
248 pub header: RequestHeaderConfig,
249 pub body: RequestBodyConfig,
250}
251
252#[derive(Debug, PartialEq, Clone)]
254pub struct Server {
255 pub address: String,
256 pub port: u16,
257 pub compression: Compression,
258 pub shutdown: ShutdownType,
259 pub timeout_per_shutdown_phase: String,
260 pub thread_limit: Option<usize>,
261 pub request_timeout: Option<Duration>,
262 pub http1: Http1,
263 pub ssl: SSL,
264 pub multipart: Multipart,
265 pub cookie: Cookie,
266 pub request: RequestConfig,
267}
268
269impl Server {
270 pub fn make_address(&self) -> anyhow::Result<SocketAddr> {
271 use std::net::{IpAddr, SocketAddr};
272 use std::str::FromStr;
273
274 let port = self.port;
275 let host = self.address.clone();
276
277 let ip = IpAddr::from_str(host.as_str())?;
278
279 let socket_addr = SocketAddr::new(ip, port);
280
281 Ok(socket_addr)
282 }
283}
284
285impl Default for Server {
286 fn default() -> Self {
287 Server {
288 address: "0.0.0.0".to_string(),
289 port: 3000,
290 compression: Compression::default(),
291 shutdown: ShutdownType::Immediate,
292 timeout_per_shutdown_phase: "30s".to_string(),
293 thread_limit: None,
294 request_timeout: None,
295 http1: Http1::default(),
296 ssl: Default::default(),
297 multipart: Default::default(),
298 cookie: Default::default(),
299 request: Default::default(),
300 }
301 }
302}
303
304impl Server {
305 pub fn is_graceful_shutdown(&self) -> bool {
306 self.shutdown == ShutdownType::Graceful
307 }
308
309 pub fn shutdown_timeout_duration(&self) -> std::time::Duration {
310 let timeout = self
311 .timeout_per_shutdown_phase
312 .trim_end_matches(|c| !char::is_numeric(c));
313 let timeout = timeout.parse::<u64>().unwrap_or(30);
314
315 let duration = match self.timeout_per_shutdown_phase.chars().last() {
316 Some('s') => std::time::Duration::from_secs(timeout),
317 Some('m') => std::time::Duration::from_secs(timeout * 60),
318 Some('h') => std::time::Duration::from_secs(timeout * 60 * 60),
319 _ => std::time::Duration::from_secs(30),
320 };
321
322 duration
323 }
324}
325
326impl ApplicationProperties {
327 pub fn from_properties(text: String) -> ApplicationProperties {
328 let mut server = Server::default();
329 let mut environment = "dev".to_string();
330 let mut etc = HashMap::new();
331 let mut banner = Banner::default();
332
333 let mut key_values = HashMap::new();
334
335 for line in text.lines() {
337 let mut parts = line.split("=");
338
339 let key = match parts.next() {
340 Some(key) => key.trim().to_owned(),
341 None => continue,
342 };
343 let value = match parts.next() {
344 Some(value) => value.trim().to_owned(),
345 None => continue,
346 };
347
348 let value = if value.starts_with('"') && value.ends_with('"') {
350 value[1..value.len() - 1].to_string()
351 } else {
352 value.to_string()
353 };
354
355 key_values.insert(key, value);
356 }
357
358 let env = std::env::vars().collect::<HashMap<_, _>>();
360 for (key, value) in env {
361 if key_values.contains_key(&key) {
362 continue;
363 }
364
365 key_values.insert(key, value);
366 }
367
368 for (key, value) in key_values {
370 match key.as_str() {
372 "server.port" => {
373 if let Ok(value) = value.parse::<u16>() {
374 server.port = value;
375 }
376 }
377 "server.address" => {
378 server.address = value.to_string();
379 }
380 "server.shutdown" => server.shutdown = value.into(),
381 "server.timeout-per-shutdown-phase" => {
382 server.timeout_per_shutdown_phase = value.to_string();
383 }
384 "server.compression.enabled" => {
385 if let Ok(value) = value.parse::<bool>() {
386 server.compression.enabled = value;
387 }
388 }
389 "server.compression.mime-types" => {
390 server.compression.mime_types =
391 value.split(",").map(|s| s.to_string()).collect();
392 }
393 "server.compression.min-response-size" => {
394 if let Ok(value) = value.parse::<usize>() {
395 server.compression.min_response_size = value;
396 }
397 }
398 "server.compression.algorithm" => {
399 server.compression.algorithm = value.into();
400 }
401 "server.thread.limit" => {
402 if let Ok(value) = value.parse::<usize>() {
403 server.thread_limit = Some(value);
404 }
405 }
406 "server.request-timeout" => {
407 let timeout = value.trim_end_matches(|c| !char::is_numeric(c));
411
412 if let Ok(timeout) = timeout.parse::<u64>() {
413 if timeout == 0 {
414 continue;
415 }
416
417 let duration = match value.chars().last() {
418 Some('s') => std::time::Duration::from_secs(timeout),
419 Some('m') => std::time::Duration::from_secs(timeout * 60),
420 _ => std::time::Duration::from_millis(timeout),
421 };
422
423 server.request_timeout = Some(duration);
424 }
425 }
426 "server.request.uri.max-length" => {
427 if let Ok(value) = value.parse::<usize>() {
428 server.request.uri.max_length = Some(value);
429 }
430 }
431 "server.request.header.max-length" => {
432 if let Ok(value) = value.parse::<usize>() {
433 server.request.header.max_length = Some(value);
434 }
435 }
436 "server.request.header.max-number-of-headers" => {
437 if let Ok(value) = value.parse::<usize>() {
438 server.request.header.max_number_of_headers = Some(value);
439 }
440 }
441 "server.request.body.max-length" => {
442 if let Some(value) = parse_byte_size(value.as_str()) {
443 server.request.body.max_length = value;
444 }
445 }
446 "server.http1.keep-alive" => {
447 if let Ok(value) = value.parse::<bool>() {
448 server.http1.keep_alive = value;
449 }
450 }
451 "server.ssl.key" => {
452 server.ssl.key = value.to_string();
453 }
454 "server.multipart.auto-parsing-enabled" => {
455 if let Ok(value) = value.parse::<bool>() {
456 server.multipart.auto_parsing_enabled = value;
457 }
458 }
459 "server.cookie.auto-parsing-enabled" => {
460 if let Ok(value) = value.parse::<bool>() {
461 server.cookie.auto_parsing_enabled = value;
462 }
463 }
464 "server.ssl.cert" => {
465 server.ssl.cert = value.to_string();
466 }
467 "environment" => {
468 environment = value.to_string();
469 }
470 "banner.enabled" => {
471 banner.enabled = value.parse::<bool>().unwrap_or(true);
472 }
473 "banner.location" => {
474 banner.location = Some(value.to_string());
475 }
476 "banner.charset" => {
477 banner.charset = value.to_string();
478 }
479 _ => {
480 etc.insert(key, value);
481 }
482 }
483 }
484
485 ApplicationProperties {
486 server,
487 etc,
488 environment,
489 banner,
490 }
491 }
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497
498 fn remove_all_env() {
499 for (key, _) in std::env::vars() {
500 std::env::remove_var(key);
501 }
502 }
503
504 #[test]
505 fn test_from_properties() {
506 struct TestCase {
507 name: String,
508 input: String,
509 before: fn() -> (),
510 expected: ApplicationProperties,
511 }
512
513 let test_cases = vec![
514 TestCase {
515 name: "일반적인 기본 속성 바인딩".to_string(),
516 input: r#"
517 server.port=8080
518 server.address=127.0.0.1
519 "#
520 .to_string(),
521 expected: ApplicationProperties {
522 server: Server {
523 address: "127.0.0.1".to_string(),
524 port: 8080,
525 ..Default::default()
526 },
527 etc: HashMap::new(),
528 environment: "dev".to_string(),
529 ..Default::default()
530 },
531 before: || {
532 remove_all_env();
533 },
534 },
535 TestCase {
536 name: "추가 속성 바인딩".to_string(),
537 input: r#"
538 server.port=8080
539 server.address=127.0.0.1
540 foo.bar=hello
541 "#
542 .to_string(),
543 expected: ApplicationProperties {
544 server: Server {
545 address: "127.0.0.1".to_string(),
546 port: 8080,
547 ..Default::default()
548 },
549 environment: "dev".to_string(),
550 etc: HashMap::from([("foo.bar".to_string(), "hello".to_string())]),
551 ..Default::default()
552 },
553 before: || {
554 remove_all_env();
555 },
556 },
557 TestCase {
558 name: "추가 속성 바인딩 - 환경변수".to_string(),
559 input: r#"
560 server.port=8080
561 server.address=127.0.0.1
562 "#
563 .to_string(),
564 expected: ApplicationProperties {
565 server: Server {
566 address: "127.0.0.1".to_string(),
567 port: 8080,
568 ..Default::default()
569 },
570 environment: "dev".to_string(),
571 etc: HashMap::from([("asdf.fdsa".to_string(), "!!".to_string())]),
572 ..Default::default()
573 },
574 before: || {
575 remove_all_env();
576 std::env::set_var("asdf.fdsa", "!!");
577 },
578 },
579 TestCase {
580 name: "따옴표로 감싸기".to_string(),
581 input: r#"
582 server.port=8080
583 server.address="127.0.0.1"
584 "#
585 .to_string(),
586 expected: ApplicationProperties {
587 server: Server {
588 address: "127.0.0.1".to_string(),
589 port: 8080,
590 ..Default::default()
591 },
592 environment: "dev".to_string(),
593 etc: HashMap::new(),
594 ..Default::default()
595 },
596 before: || {
597 remove_all_env();
598 },
599 },
600 TestCase {
601 name: "중간에 띄어쓰기".to_string(),
602 input: r#"
603 server.port=8080
604 server.address= 127.0.0.1
605 "#
606 .to_string(),
607 expected: ApplicationProperties {
608 server: Server {
609 address: "127.0.0.1".to_string(),
610 port: 8080,
611 ..Default::default()
612 },
613 environment: "dev".to_string(),
614 etc: HashMap::new(),
615 ..Default::default()
616 },
617 before: || {
618 remove_all_env();
619 },
620 },
621 TestCase {
622 name: "포트 파싱 실패".to_string(),
623 input: r#"
624 server.port=80#@#@80
625 server.address= 127.0.0.1
626 "#
627 .to_string(),
628 expected: ApplicationProperties {
629 server: Server {
630 address: "127.0.0.1".to_string(),
631 port: 3000,
632 ..Default::default()
633 },
634 environment: "dev".to_string(),
635 etc: HashMap::new(),
636 ..Default::default()
637 },
638 before: || {
639 remove_all_env();
640 },
641 },
642 TestCase {
643 name: "environment 바인딩".to_string(),
644 input: r#"
645 server.port=80#@#@80
646 server.address= 127.0.0.1
647 environment=prod
648 "#
649 .to_string(),
650 expected: ApplicationProperties {
651 server: Server {
652 address: "127.0.0.1".to_string(),
653 port: 3000,
654 ..Default::default()
655 },
656 environment: "prod".to_string(),
657 etc: HashMap::new(),
658 ..Default::default()
659 },
660 before: || {
661 remove_all_env();
662 },
663 },
664 ];
665
666 for tc in test_cases {
667 (tc.before)();
668
669 let got = ApplicationProperties::from_properties(tc.input.clone());
670 assert_eq!(
671 got, tc.expected,
672 "{} - input: {:?}, actual: {:?}",
673 tc.name, tc.input, got
674 );
675 }
676 }
677}
678
679pub fn load_application_properties_from_all() -> ApplicationProperties {
681 if let Ok(text) = std::fs::read_to_string("application.properties") {
683 return ApplicationProperties::from_properties(text);
684 }
685
686 let exe_path = std::env::current_exe().expect("Failed to get current executable path");
688 let exe_dir = exe_path
689 .parent()
690 .expect("Failed to get executable directory");
691
692 let exe_properties_path = exe_dir.join("application.properties");
693 if let Ok(text) = std::fs::read_to_string(exe_properties_path) {
694 return ApplicationProperties::from_properties(text);
695 }
696
697 println!("application.properties Not Found. Use default properties.");
698
699 ApplicationProperties::default()
700}