1mod err;
2
3use native_tls::TlsConnector;
4use std::io::{Read, Write};
5use std::net::TcpStream;
6use std::time::Duration;
7pub use crate::err::HttpClientError;
8
9#[derive(Debug)]
10pub enum HttpMethod {
11 GET,
12 POST,
13}
14
15const HOST: &str = "api.xyo.financial";
16const PORT: i32 = 443;
17const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(10);
18
19#[derive(Debug)]
20pub struct HttpResponse {
21 pub status_code: i16,
22 pub body: String,
23}
24
25mod http_message {
26 use crate::{HttpMethod, HOST};
27
28 pub fn new(method: HttpMethod, path: &str, headers: &[(&str, &str)], data: &str) -> String {
30 let method_str = match method {
31 HttpMethod::GET => "GET",
32 HttpMethod::POST => "POST",
33 };
34
35 let mut req = format!(
37 "{} {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n",
38 method_str, path, HOST
39 );
40
41 for (k, v) in headers {
43 req.push_str(&format!("{}: {}\r\n", k, v));
44 }
45
46 if !headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("Content-Type")) && !data.is_empty() {
48 req.push_str("Content-Type: application/json\r\n");
49 }
50 if !headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("Accept")) {
51 req.push_str("Accept: application/json\r\n");
52 }
53
54 if !data.is_empty() {
55 req.push_str(&format!("Content-Length: {}\r\n", data.as_bytes().len()));
56 }
57
58 req.push_str("\r\n");
60
61 if !data.is_empty() {
63 req.push_str(data);
64 }
65
66 req
67 }
68}
69
70pub fn request(
72 method: HttpMethod,
73 path: &str,
74 headers: &[(&str, &str)],
75 data: &str,
76) -> Result<HttpResponse, HttpClientError> {
77 let tcp_stream_socket = TcpStream::connect(format!("{}:{}", HOST, PORT)).map_err(|e| {
78 HttpClientError {
79 code: 503,
80 message: format!("could not connect to host: {} and port number {}: {}", HOST, PORT, e),
81 }
82 })?;
83
84 tcp_stream_socket
85 .set_read_timeout(Some(DEFAULT_TIMEOUT_DURATION))
86 .map_err(|e| HttpClientError {
87 code: 500,
88 message: format!("failed to set read timeout: {}", e),
89 })?;
90
91 let connector = TlsConnector::new().map_err(|e| HttpClientError {
92 code: 500,
93 message: format!("failed to create TLS connector: {}", e),
94 })?;
95
96 let mut secure_stream = connector
97 .connect(HOST, tcp_stream_socket)
98 .map_err(|e| HttpClientError {
99 code: 500,
100 message: format!("failed TLS handshake with {}: {}", HOST, e),
101 })?;
102
103 let req_payload = http_message::new(method, path, headers, data);
104
105 secure_stream
106 .write_all(req_payload.as_bytes())
107 .map_err(|e| HttpClientError {
108 code: 500,
109 message: format!("failed to write to socket: {}", e),
110 })?;
111
112 let mut resp_bytes = Vec::new();
114 secure_stream
115 .read_to_end(&mut resp_bytes)
116 .map_err(|e| HttpClientError {
117 code: 500,
118 message: format!("failed to read from socket: {}", e),
119 })?;
120
121 let _ = secure_stream.flush();
122
123 let response_str = String::from_utf8_lossy(&resp_bytes);
125 let mut parts = response_str.splitn(2, "\r\n\r\n");
126 let header_part = parts.next().unwrap_or("");
127 let body_part = parts.next().unwrap_or("").to_string();
128
129 let status_line = header_part.lines().next().unwrap_or("");
130 let status_parts: Vec<&str> = status_line.splitn(3, ' ').collect();
131 let status_code: i16 = if status_parts.len() >= 2 {
132 status_parts[1].parse().unwrap_or(500)
133 } else {
134 500
135 };
136
137 Ok(HttpResponse {
138 status_code,
139 body: body_part,
140 })
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn it_works_without_body_content() {
149 let result = request(HttpMethod::GET, "/healthz", &[], "");
151 assert!(result.is_ok());
152 }
153}