use std::io::{BufRead, BufReader, Read, Write};
use std::time::{Duration, Instant};
use serde_json::{Value, json};
use crate::key::keys_match;
use crate::protocol::Reply;
pub const DEFAULT_MAX_LINE: usize = 4 * 1024 * 1024;
#[derive(Debug, PartialEq)]
pub(crate) enum Line {
Request {
id: Option<Value>,
key: Option<String>,
method: String,
params: Value,
},
Blank,
Reject(String),
}
pub(crate) fn parse_line(bytes: &[u8]) -> Line {
let Ok(text) = std::str::from_utf8(bytes) else {
return Line::Reject("not a control request (invalid UTF-8); closing the connection".into());
};
let text = text.trim();
if text.is_empty() {
return Line::Blank;
}
let msg = match serde_json::from_str::<Value>(text) {
Ok(v) => v,
Err(e) => return Line::Reject(format!("bad JSON ({e}); expected one JSON request object per line; closing the connection")),
};
let Value::Object(mut obj) = msg else {
return Line::Reject("not a control request (expected a JSON object); closing the connection".into());
};
let Some(Value::String(method)) = obj.remove("method") else {
return Line::Reject("not a control request (missing string `method`); closing the connection".into());
};
let key = match obj.remove("key") {
Some(Value::String(k)) => Some(k),
_ => None,
};
let params = match obj.remove("params") {
None | Some(Value::Null) => json!({}),
Some(p) => p,
};
Line::Request { id: obj.remove("id"), key, method, params }
}
#[derive(Debug, PartialEq)]
pub(crate) enum ReadError {
TooLong,
Io,
}
pub(crate) fn read_bounded_line(reader: &mut impl BufRead, buf: &mut Vec<u8>, max: usize) -> Result<bool, ReadError> {
buf.clear();
let limit = u64::try_from(max).unwrap_or(u64::MAX).saturating_add(1);
let n = reader.take(limit).read_until(b'\n', buf).map_err(|_| ReadError::Io)?;
if n == 0 {
return Ok(false);
}
if buf.last() == Some(&b'\n') {
buf.pop();
if buf.last() == Some(&b'\r') {
buf.pop();
}
return Ok(true);
}
if buf.len() > max {
return Err(ReadError::TooLong);
}
Ok(true)
}
pub(crate) trait Source: Read {
fn set_deadline(&mut self, _deadline: Option<Instant>) {}
}
pub(crate) struct Plain<R>(pub R);
impl<R: Read> Read for Plain<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.0.read(buf)
}
}
impl<R: Read> Source for Plain<R> {}
fn discard_rest_of_line<S: Source>(r: &mut BufReader<S>) {
r.get_mut().set_deadline(Some(Instant::now() + Duration::from_secs(2)));
let mut left: usize = 64 << 20;
while left > 0 {
let (used, done) = match r.fill_buf() {
Ok([]) | Err(_) => return,
Ok(b) => match b.iter().position(|&c| c == b'\n') {
Some(i) => (i.saturating_add(1), true),
None => (b.len(), false),
},
};
r.consume(used);
if done {
return;
}
left = left.saturating_sub(used);
}
}
pub(crate) enum Auth<'a> {
None,
Key(&'a str),
}
pub(crate) fn serve_lines<S: Source, W: Write>(
reader: &mut BufReader<S>,
mut out: W,
max_line: usize,
auth: Auth<'_>,
mut handle: impl FnMut(String, Value) -> Option<Reply>,
) {
let mut buf = Vec::new();
let mut authorised = false;
let mut conn_key: Option<String> = None;
let send = |out: &mut W, v: Value| {
let mut line = v.to_string();
line.push('\n');
out.write_all(line.as_bytes()).and_then(|()| out.flush()).is_ok()
};
loop {
let (id, key, method, params) = match read_bounded_line(reader, &mut buf, max_line) {
Ok(false) | Err(ReadError::Io) => return,
Err(ReadError::TooLong) => {
let _ = send(&mut out, Reply::err(format!("request line longer than {max_line} bytes; closing the connection")).to_json(None));
discard_rest_of_line(reader);
return;
}
Ok(true) => match parse_line(&buf) {
Line::Blank => continue,
Line::Reject(e) => {
let _ = send(&mut out, Reply::err(e).to_json(None));
return;
}
Line::Request { id, key, method, params } => (id, key, method, params),
},
};
if let Auth::Key(app_key) = auth {
let given = key.or_else(|| if method == "auth" { params.get("key").and_then(Value::as_str).map(str::to_string) } else { None });
let k = given.as_deref().or(conn_key.as_deref()).unwrap_or("");
if !keys_match(k, app_key) {
let _ = send(&mut out, Reply::err("unauthorized").to_json(id.as_ref()));
return;
}
if !authorised {
authorised = true;
reader.get_mut().set_deadline(None);
}
if method == "auth" {
conn_key = Some(k.to_string());
if !send(&mut out, Reply::ok(json!({})).to_json(id.as_ref())) {
return;
}
continue;
}
} else if method == "auth" {
if !send(&mut out, Reply::ok(json!({})).to_json(id.as_ref())) {
return;
}
continue;
}
let Some(r) = handle(method, params) else { return };
if !send(&mut out, r.to_json(id.as_ref())) {
return;
}
}
}
pub fn serve_stream(input: impl Read, output: impl Write, max_line: usize, mut handle: impl FnMut(&str, Value) -> Reply) {
let mut reader = BufReader::new(Plain(input));
serve_lines(&mut reader, output, max_line, Auth::None, |m, p| Some(handle(&m, p)));
}
#[cfg(test)]
mod tests {
use super::*;
fn run(input: &[u8], auth: Auth<'_>) -> (Vec<Value>, Vec<String>) {
let mut out = Vec::new();
let mut calls = Vec::new();
let mut reader = BufReader::new(Plain(input));
serve_lines(&mut reader, &mut out, 64, auth, |m, _p| {
calls.push(m);
Some(Reply::ok(Value::Null))
});
let replies = String::from_utf8(out).unwrap().lines().map(|l| serde_json::from_str::<Value>(l).unwrap()).collect();
(replies, calls)
}
#[test]
fn requests_are_answered_in_order_with_ids() {
let (r, calls) = run(b"{\"id\":1,\"method\":\"a\"}\n\n{\"id\":\"x\",\"method\":\"b\",\"params\":{}}\r\n{\"method\":\"c\"}", Auth::None);
assert_eq!(calls, ["a", "b", "c"]);
assert_eq!(
r,
[json!({"id": 1, "ok": true, "result": null}), json!({"id": "x", "ok": true, "result": null}), json!({"ok": true, "result": null})]
);
}
#[test]
fn non_requests_close_the_connection() {
for junk in [
&b"[1,2]\n{\"method\":\"a\"}\n"[..],
b"\"method\"\n{\"method\":\"a\"}\n",
b"42\n{\"method\":\"a\"}\n",
b"null\n{\"method\":\"a\"}\n",
b"{\"id\":1}\n{\"method\":\"a\"}\n",
b"{\"method\":5}\n{\"method\":\"a\"}\n",
b"\xff\xfe\x00\x01\n{\"method\":\"a\"}\n",
b"GET / HTTP/1.1\r\n{\"method\":\"a\"}\n",
b"{\"method\":\"a\"\n{\"method\":\"a\"}\n",
] {
let (r, calls) = run(junk, Auth::None);
assert!(calls.is_empty(), "{calls:?}");
assert_eq!(r.len(), 1);
assert_eq!(r[0]["ok"], false);
assert!(r[0]["error"].as_str().unwrap().contains("closing the connection"));
}
}
#[test]
fn a_request_before_junk_still_runs() {
let (r, calls) = run(b"{\"method\":\"a\"}\nGET / HTTP/1.1\n{\"method\":\"b\"}\n", Auth::None);
assert_eq!(calls, ["a"]);
assert_eq!(r.len(), 2);
assert_eq!(r[1]["ok"], false);
}
#[test]
fn overlong_line_is_refused() {
let mut input = vec![b'x'; 65];
input.extend_from_slice(b"\n{\"method\":\"a\"}\n");
let (r, calls) = run(&input, Auth::None);
assert!(calls.is_empty());
assert_eq!(r.len(), 1);
assert!(r[0]["error"].as_str().unwrap().contains("longer than 64"));
}
#[test]
fn line_at_the_limit_is_accepted() {
let mut buf = Vec::new();
let mut input = vec![b'a'; 10];
input.push(b'\n');
assert_eq!(read_bounded_line(&mut &input[..], &mut buf, 10), Ok(true));
assert_eq!(buf, [b'a'; 10]);
assert_eq!(read_bounded_line(&mut &[b'a'; 11][..], &mut buf, 10), Err(ReadError::TooLong));
}
#[test]
fn keys_are_checked_on_every_request() {
let (r, calls) = run(b"{\"id\":1,\"key\":\"k\",\"method\":\"a\"}\n{\"id\":2,\"method\":\"b\"}\n{\"method\":\"c\"}\n", Auth::Key("k"));
assert_eq!(calls, ["a"]);
assert_eq!(r[1], json!({"id": 2, "ok": false, "error": "unauthorized"}));
assert_eq!(r.len(), 2);
let (r, calls) = run(b"{\"method\":\"auth\",\"params\":{\"key\":\"k\"}}\n{\"method\":\"b\",\"params\":{\"key\":\"x\"}}\n", Auth::Key("k"));
assert_eq!(calls, ["b"]);
assert_eq!(r[0]["ok"], true);
let (_, calls) = run(b"{\"method\":\"b\",\"params\":{\"key\":\"k\"}}\n", Auth::Key("k"));
assert!(calls.is_empty());
let (r, calls) = run(b"{\"method\":\"auth\",\"key\":\"k\"}\n{\"key\":\"bad\",\"method\":\"b\"}\n{\"method\":\"c\"}\n", Auth::Key("k"));
assert!(calls.is_empty());
assert_eq!(r[1]["error"], "unauthorized");
assert_eq!(r.len(), 2);
}
#[test]
fn stream_serving_answers_synchronously() {
let mut out = Vec::new();
serve_stream(&b"{\"id\":1,\"method\":\"app.ping\"}\n"[..], &mut out, 1024, |m, _| Reply::ok(json!(m)));
assert_eq!(String::from_utf8(out).unwrap(), "{\"id\":1,\"ok\":true,\"result\":\"app.ping\"}\n");
}
}