Files
signer/src/http.rs
T

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: *"));
}
}