Split Rust code into domain modules
This commit is contained in:
178
src/server/http.rs
Normal file
178
src/server/http.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use super::*;
|
||||
|
||||
pub(super) fn send_sse_headers(stream: &mut impl Write) -> Result<(), String> {
|
||||
stream
|
||||
.write_all(
|
||||
b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n",
|
||||
)
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
pub(super) fn send_sse(stream: &mut impl Write, value: &Value) -> Result<(), String> {
|
||||
let body = serde_json::to_vec(value).map_err(|error| error.to_string())?;
|
||||
stream
|
||||
.write_all(b"data: ")
|
||||
.map_err(|error| error.to_string())?;
|
||||
stream.write_all(&body).map_err(|error| error.to_string())?;
|
||||
stream.write_all(b"\n\n").map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
pub(super) fn send_sse_error(stream: &mut impl Write, message: &str) -> Result<(), String> {
|
||||
let body = serde_json::to_vec(&json!({"error": {"message": message, "type": "server_error"}}))
|
||||
.map_err(|error| error.to_string())?;
|
||||
stream
|
||||
.write_all(b"event: error\ndata: ")
|
||||
.map_err(|error| error.to_string())?;
|
||||
stream.write_all(&body).map_err(|error| error.to_string())?;
|
||||
stream.write_all(b"\n\n").map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
pub(super) fn send_json(stream: &mut TcpStream, code: u16, value: &Value) -> Result<(), String> {
|
||||
let mut body = serde_json::to_vec(value).map_err(|error| error.to_string())?;
|
||||
body.push(b'\n');
|
||||
send_response(stream, code, Some("application/json"), &body)
|
||||
}
|
||||
|
||||
pub(super) fn send_error(stream: &mut TcpStream, code: u16, message: &str) -> Result<(), String> {
|
||||
send_json(
|
||||
stream,
|
||||
code,
|
||||
&json!({"error": {"message": message, "type": "invalid_request_error"}}),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn send_response(
|
||||
stream: &mut TcpStream,
|
||||
code: u16,
|
||||
content_type: Option<&str>,
|
||||
body: &[u8],
|
||||
) -> Result<(), String> {
|
||||
let reason = match code {
|
||||
200 => "OK",
|
||||
204 => "No Content",
|
||||
400 => "Bad Request",
|
||||
404 => "Not Found",
|
||||
409 => "Conflict",
|
||||
500 => "Internal Server Error",
|
||||
_ => "Error",
|
||||
};
|
||||
let mut header = format!(
|
||||
"HTTP/1.1 {code} {reason}\r\nContent-Length: {}\r\n",
|
||||
body.len()
|
||||
);
|
||||
if let Some(content_type) = content_type {
|
||||
header.push_str("Content-Type: ");
|
||||
header.push_str(content_type);
|
||||
header.push_str("\r\n");
|
||||
}
|
||||
header.push_str("Connection: close\r\n\r\n");
|
||||
stream
|
||||
.write_all(header.as_bytes())
|
||||
.and_then(|()| stream.write_all(body))
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
pub(super) fn read_request(stream: &mut TcpStream) -> Result<HttpRequest, String> {
|
||||
let mut bytes = Vec::new();
|
||||
let header_end = loop {
|
||||
if bytes.len() >= MAX_HEADER_BYTES {
|
||||
return Err("HTTP headers are too large".into());
|
||||
}
|
||||
let mut chunk = [0_u8; 4096];
|
||||
let read = stream.read(&mut chunk).map_err(|error| error.to_string())?;
|
||||
if read == 0 {
|
||||
return Err("bad HTTP request".into());
|
||||
}
|
||||
bytes.extend_from_slice(&chunk[..read]);
|
||||
if let Some(end) = find_header_end(&bytes) {
|
||||
break end;
|
||||
}
|
||||
};
|
||||
let header = std::str::from_utf8(&bytes[..header_end])
|
||||
.map_err(|_| "HTTP headers are not UTF-8".to_owned())?;
|
||||
let mut lines = header.lines();
|
||||
let request_line = lines.next().ok_or_else(|| "bad HTTP request".to_owned())?;
|
||||
let mut parts = request_line.split_whitespace();
|
||||
let method = parts
|
||||
.next()
|
||||
.ok_or_else(|| "bad HTTP request".to_owned())?
|
||||
.to_owned();
|
||||
let path = parts
|
||||
.next()
|
||||
.ok_or_else(|| "bad HTTP request".to_owned())?
|
||||
.to_owned();
|
||||
let length = lines
|
||||
.find_map(|line| {
|
||||
line.split_once(':').and_then(|(name, value)| {
|
||||
name.eq_ignore_ascii_case("content-length")
|
||||
.then(|| value.trim().parse::<usize>().ok())
|
||||
.flatten()
|
||||
})
|
||||
})
|
||||
.unwrap_or(0);
|
||||
if length > MAX_BODY_BYTES {
|
||||
return Err("HTTP body is too large".into());
|
||||
}
|
||||
while bytes.len() < header_end + length {
|
||||
let mut chunk = [0_u8; 8192];
|
||||
let read = stream.read(&mut chunk).map_err(|error| error.to_string())?;
|
||||
if read == 0 {
|
||||
return Err("incomplete HTTP body".into());
|
||||
}
|
||||
bytes.extend_from_slice(&chunk[..read]);
|
||||
}
|
||||
Ok(HttpRequest {
|
||||
method,
|
||||
path: path.split('?').next().unwrap_or(&path).to_owned(),
|
||||
body: bytes[header_end..header_end + length].to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
fn find_header_end(bytes: &[u8]) -> Option<usize> {
|
||||
bytes
|
||||
.windows(4)
|
||||
.position(|window| window == b"\r\n\r\n")
|
||||
.map(|position| position + 4)
|
||||
.or_else(|| {
|
||||
bytes
|
||||
.windows(2)
|
||||
.position(|window| window == b"\n\n")
|
||||
.map(|position| position + 2)
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn unix_time() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
pub(super) fn random_id(prefix: &str) -> String {
|
||||
random_hex_id::<12>(prefix)
|
||||
}
|
||||
|
||||
pub(super) fn random_tool_id(prefix: &str) -> String {
|
||||
random_hex_id::<16>(prefix)
|
||||
}
|
||||
|
||||
fn random_hex_id<const N: usize>(prefix: &str) -> String {
|
||||
let mut bytes = [0_u8; N];
|
||||
if File::open("/dev/urandom")
|
||||
.and_then(|mut file| file.read_exact(&mut bytes))
|
||||
.is_err()
|
||||
{
|
||||
let fallback = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos();
|
||||
bytes.copy_from_slice(&fallback.to_le_bytes()[..N]);
|
||||
}
|
||||
let mut id = String::with_capacity(prefix.len() + N * 2);
|
||||
id.push_str(prefix);
|
||||
for byte in bytes {
|
||||
use std::fmt::Write as _;
|
||||
let _ = write!(id, "{byte:02x}");
|
||||
}
|
||||
id
|
||||
}
|
||||
Reference in New Issue
Block a user