use std::io::{self, Read, Write};
use std::net::TcpStream;
use std::time::{Duration, Instant};
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
pub struct HttpResponse {
pub status: u16,
pub body: Vec<u8>,
}
impl HttpResponse {
pub fn body_string(&self) -> String {
String::from_utf8_lossy(&self.body).to_string()
}
pub fn is_success(&self) -> bool {
self.status >= 200 && self.status < 300
}
}
fn parse_http_response(raw: &[u8]) -> Result<HttpResponse, io::Error> {
let sep = b"\r\n\r\n";
let header_end = raw
.windows(sep.len())
.position(|w| w == sep)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Malformed HTTP response (no header terminator)"))?;
let header_bytes = &raw[..header_end];
let body = raw[header_end + sep.len()..].to_vec();
let header_str = String::from_utf8_lossy(header_bytes);
let first_line = header_str.lines().next().unwrap_or("");
let status = first_line
.split_whitespace()
.nth(1)
.and_then(|s| s.parse::<u16>().ok())
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Malformed HTTP status line"))?;
Ok(HttpResponse { status, body })
}
pub fn http_get(ip_addr: &str, port: u16, path: &str) -> Result<HttpResponse, Box<dyn std::error::Error>> {
let addr = format!("{}:{}", ip_addr, port);
let mut stream = TcpStream::connect(&addr)?;
stream.set_read_timeout(Some(DEFAULT_TIMEOUT))?;
stream.set_write_timeout(Some(DEFAULT_TIMEOUT))?;
let request = format!(
"GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\nAccept: */*\r\n\r\n",
path, ip_addr
);
stream.write_all(request.as_bytes())?;
stream.flush()?;
let mut raw = Vec::new();
stream.read_to_end(&mut raw)?;
Ok(parse_http_response(&raw)?)
}
pub type ProgressFn<'a> = dyn FnMut(u64, u64, f64) + 'a;
pub fn http_post_multipart_file(
ip_addr: &str,
port: u16,
path: &str,
field_name: &str,
upload_filename: &str,
file_path: &str,
mut progress: Option<&mut ProgressFn>,
) -> Result<HttpResponse, Box<dyn std::error::Error>> {
use std::fs::File;
use std::io::BufReader;
let metadata = std::fs::metadata(file_path)?;
let file_size = metadata.len();
let file = File::open(file_path)?;
let mut reader = BufReader::new(file);
let addr = format!("{}:{}", ip_addr, port);
let mut stream = TcpStream::connect(&addr)?;
stream.set_read_timeout(Some(DEFAULT_TIMEOUT))?;
stream.set_write_timeout(Some(DEFAULT_TIMEOUT))?;
let boundary = "----RaftCLIBoundary7c3a1f9e";
let start_boundary = format!("--{}\r\n", boundary);
let content_disposition = format!(
"Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n",
field_name, upload_filename
);
let content_type_part = "Content-Type: application/octet-stream\r\n\r\n";
let end_boundary = format!("\r\n--{}--\r\n", boundary);
let headers_length = start_boundary.len() + content_disposition.len() + content_type_part.len();
let content_length = headers_length + file_size as usize + end_boundary.len();
let request = format!(
"POST {} HTTP/1.1\r\n\
Host: {}\r\n\
Content-Type: multipart/form-data; boundary={}\r\n\
Content-Length: {}\r\n\
Connection: close\r\n\r\n",
path, ip_addr, boundary, content_length
);
stream.write_all(request.as_bytes())?;
stream.write_all(start_boundary.as_bytes())?;
stream.write_all(content_disposition.as_bytes())?;
stream.write_all(content_type_part.as_bytes())?;
stream.flush()?;
let mut buf = vec![0u8; 4096];
let mut sent: u64 = 0;
let start = Instant::now();
loop {
let n = reader.read(&mut buf)?;
if n == 0 {
break;
}
stream.write_all(&buf[..n])?;
sent += n as u64;
if let Some(cb) = progress.as_deref_mut() {
let elapsed = start.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { sent as f64 / elapsed } else { 0.0 };
cb(sent, file_size, rate);
}
}
stream.write_all(end_boundary.as_bytes())?;
stream.flush()?;
let mut raw = Vec::new();
stream.read_to_end(&mut raw)?;
Ok(parse_http_response(&raw)?)
}