181 lines
5.6 KiB
Rust
181 lines
5.6 KiB
Rust
//! Minimal HTTP/1.1 parser for signer's HTTP listener mode.
|
|
//!
|
|
//! Port of `http_listener.c`. Only supports POST with a JSON body.
|
|
//! No chunked encoding, no keep-alive, one request per connection.
|
|
|
|
use std::io::{self, Read, Write};
|
|
|
|
/// Read an HTTP POST request and return the JSON body.
|
|
///
|
|
/// Returns `Ok(body)` on success, `Err` on parse error / non-POST.
|
|
pub fn recv_request<R: Read>(reader: &mut R) -> io::Result<String> {
|
|
let mut line = String::new();
|
|
let mut content_length: usize = 0;
|
|
let mut is_post = false;
|
|
|
|
// Read header lines until empty line
|
|
loop {
|
|
line.clear();
|
|
read_line(reader, &mut line)?;
|
|
let trimmed = line.trim_end();
|
|
if trimmed.is_empty() {
|
|
break; // End of headers
|
|
}
|
|
|
|
if trimmed.starts_with("POST ") {
|
|
is_post = true;
|
|
} else if let Some(cl) = trimmed.strip_prefix("Content-Length: ").or_else(|| trimmed.strip_prefix("content-length: ")) {
|
|
content_length = cl.parse().unwrap_or(0);
|
|
}
|
|
}
|
|
|
|
if !is_post {
|
|
return Err(io::Error::new(io::ErrorKind::InvalidData, "not a POST request"));
|
|
}
|
|
|
|
if content_length == 0 || content_length > super::transport::MAX_MSG_SIZE {
|
|
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid content length"));
|
|
}
|
|
|
|
let mut body = vec![0u8; content_length];
|
|
reader.read_exact(&mut body)?;
|
|
String::from_utf8(body).map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid UTF-8"))
|
|
}
|
|
|
|
/// Send an HTTP 200 response with a JSON body and CORS headers.
|
|
pub fn send_response<W: Write>(writer: &mut W, json_body: &str) -> io::Result<()> {
|
|
let response = format!(
|
|
"HTTP/1.1 200 OK\r\n\
|
|
Content-Type: application/json\r\n\
|
|
Content-Length: {}\r\n\
|
|
Access-Control-Allow-Origin: *\r\n\
|
|
Access-Control-Allow-Methods: POST, OPTIONS\r\n\
|
|
Access-Control-Allow-Headers: Content-Type\r\n\
|
|
Connection: close\r\n\
|
|
\r\n\
|
|
{}",
|
|
json_body.len(),
|
|
json_body
|
|
);
|
|
writer.write_all(response.as_bytes())?;
|
|
writer.flush()?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Send an HTTP error response.
|
|
pub fn send_error<W: Write>(writer: &mut W, code: u16, message: &str) -> io::Result<()> {
|
|
let response = format!(
|
|
"HTTP/1.1 {} {}\r\n\
|
|
Content-Type: application/json\r\n\
|
|
Content-Length: {}\r\n\
|
|
Connection: close\r\n\
|
|
\r\n\
|
|
{{\"error\":\"{}\"}}",
|
|
code,
|
|
message,
|
|
message.len() + 12,
|
|
message
|
|
);
|
|
writer.write_all(response.as_bytes())?;
|
|
writer.flush()?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Send a CORS preflight response (for OPTIONS requests).
|
|
pub fn send_cors_preflight<W: Write>(writer: &mut W) -> io::Result<()> {
|
|
let response = "HTTP/1.1 204 No Content\r\n\
|
|
Access-Control-Allow-Origin: *\r\n\
|
|
Access-Control-Allow-Methods: POST, OPTIONS\r\n\
|
|
Access-Control-Allow-Headers: Content-Type\r\n\
|
|
Access-Control-Max-Age: 86400\r\n\
|
|
Content-Length: 0\r\n\
|
|
Connection: close\r\n\
|
|
\r\n";
|
|
writer.write_all(response.as_bytes())?;
|
|
writer.flush()?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Read a line from a reader (up to \r\n).
|
|
fn read_line<R: Read>(reader: &mut R, buf: &mut String) -> io::Result<()> {
|
|
buf.clear();
|
|
let mut byte = [0u8; 1];
|
|
loop {
|
|
match reader.read(&mut byte) {
|
|
Ok(0) => break,
|
|
Ok(_) => {
|
|
if byte[0] == b'\n' {
|
|
buf.push('\n');
|
|
break;
|
|
}
|
|
buf.push(byte[0] as char);
|
|
}
|
|
Err(e) => return Err(e),
|
|
}
|
|
if buf.len() > 8192 {
|
|
return Err(io::Error::new(io::ErrorKind::InvalidData, "header line too long"));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::io::Cursor;
|
|
|
|
#[test]
|
|
fn test_recv_post_request() {
|
|
let body = r#"{"id":"1","method":"get_info","params":[]}"#;
|
|
let request = format!(
|
|
"POST / HTTP/1.1\r\n\
|
|
Host: localhost\r\n\
|
|
Content-Type: application/json\r\n\
|
|
Content-Length: {}\r\n\
|
|
\r\n\
|
|
{}",
|
|
body.len(),
|
|
body
|
|
);
|
|
let mut cursor = Cursor::new(request.into_bytes());
|
|
let received = recv_request(&mut cursor).unwrap();
|
|
assert_eq!(received, body);
|
|
}
|
|
|
|
#[test]
|
|
fn test_recv_non_post_rejected() {
|
|
let request = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";
|
|
let mut cursor = Cursor::new(request.as_bytes());
|
|
assert!(recv_request(&mut cursor).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_send_response() {
|
|
let mut buf = Vec::new();
|
|
let body = r#"{"result":"ok"}"#;
|
|
send_response(&mut buf, body).unwrap();
|
|
let response = String::from_utf8(buf).unwrap();
|
|
assert!(response.starts_with("HTTP/1.1 200 OK"));
|
|
assert!(response.contains("Content-Type: application/json"));
|
|
assert!(response.contains("Access-Control-Allow-Origin: *"));
|
|
assert!(response.contains(body));
|
|
}
|
|
|
|
#[test]
|
|
fn test_send_error() {
|
|
let mut buf = Vec::new();
|
|
send_error(&mut buf, 400, "Bad Request").unwrap();
|
|
let response = String::from_utf8(buf).unwrap();
|
|
assert!(response.starts_with("HTTP/1.1 400 Bad Request"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_send_cors_preflight() {
|
|
let mut buf = Vec::new();
|
|
send_cors_preflight(&mut buf).unwrap();
|
|
let response = String::from_utf8(buf).unwrap();
|
|
assert!(response.starts_with("HTTP/1.1 204 No Content"));
|
|
assert!(response.contains("Access-Control-Allow-Origin: *"));
|
|
}
|
|
}
|