use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::rc::Rc;
use std::time::Duration;
use crate::error::{DogeError, DogeResult};
use crate::ordered_map::OrderedMap;
use crate::stdlib::{int_arg, str_arg};
use crate::value::{SocketData, SocketState, Value};
const HTTP_TIMEOUT: Duration = Duration::from_secs(30);
const LINE_CHUNK: usize = 1024;
fn socket_arg<'a>(fname: &str, v: &'a Value) -> DogeResult<&'a Rc<SocketData>> {
match v {
Value::Socket(s) => Ok(s),
_ => Err(DogeError::type_error(format!(
"howl.{fname} needs a Socket, got {}",
v.describe()
))),
}
}
fn host_port<'a>(fname: &str, host: &'a Value, port: &Value) -> DogeResult<(&'a str, u16)> {
let host = str_arg("howl", fname, host)?;
let port = int_arg("howl", fname, port)?;
let port = u16::try_from(port).map_err(|_| {
DogeError::value_error(format!("a port must be between 0 and 65535, got {port}"))
})?;
Ok((host, port))
}
pub fn howl_listen(host: &Value, port: &Value) -> DogeResult {
let (host, port) = host_port("listen", host, port)?;
match TcpListener::bind((host, port)) {
Ok(listener) => Ok(Value::socket(SocketState::Listener(listener))),
Err(err) => Err(DogeError::io_error(format!(
"cannot listen on {host}:{port}: {err}"
))),
}
}
pub fn howl_connect(host: &Value, port: &Value) -> DogeResult {
let (host, port) = host_port("connect", host, port)?;
match TcpStream::connect((host, port)) {
Ok(stream) => Ok(Value::socket(SocketState::Conn {
stream,
buf: Vec::new(),
})),
Err(err) => Err(DogeError::io_error(format!(
"cannot connect to {host}:{port}: {err}"
))),
}
}
pub fn howl_accept(listener: &Value) -> DogeResult {
let sock = socket_arg("accept", listener)?;
let state = sock.state.borrow();
match &*state {
SocketState::Listener(l) => match l.accept() {
Ok((stream, _)) => Ok(Value::socket(SocketState::Conn {
stream,
buf: Vec::new(),
})),
Err(err) => Err(DogeError::io_error(format!("cannot accept: {err}"))),
},
SocketState::Conn { .. } => Err(DogeError::type_error(
"howl.accept needs a listening socket, not a connection",
)),
SocketState::Closed => Err(closed()),
}
}
pub fn howl_port(sock: &Value) -> DogeResult {
let sock = socket_arg("port", sock)?;
let state = sock.state.borrow();
let addr = match &*state {
SocketState::Listener(l) => l.local_addr(),
SocketState::Conn { stream, .. } => stream.local_addr(),
SocketState::Closed => return Err(closed()),
};
match addr {
Ok(addr) => Ok(Value::int(addr.port())),
Err(err) => Err(DogeError::io_error(format!("cannot read the port: {err}"))),
}
}
pub fn howl_send(conn: &Value, text: &Value) -> DogeResult {
let sock = socket_arg("send", conn)?;
let text = str_arg("howl", "send", text)?;
let mut state = sock.state.borrow_mut();
match &mut *state {
SocketState::Conn { stream, .. } => match stream.write_all(text.as_bytes()) {
Ok(()) => Ok(Value::None),
Err(err) => Err(DogeError::io_error(format!("cannot send: {err}"))),
},
SocketState::Listener(_) => Err(DogeError::type_error(
"howl.send needs a connection, not a listening socket",
)),
SocketState::Closed => Err(closed()),
}
}
pub fn howl_recv(conn: &Value, max_bytes: &Value) -> DogeResult {
let sock = socket_arg("recv", conn)?;
let max = int_arg("howl", "recv", max_bytes)?;
if max <= 0 {
return Err(DogeError::value_error(format!(
"howl.recv needs a positive byte count, got {max}"
)));
}
let max = max as usize;
let mut state = sock.state.borrow_mut();
match &mut *state {
SocketState::Conn { stream, buf } => {
loop {
let valid_len = match std::str::from_utf8(buf) {
Ok(_) => buf.len(),
Err(e) => {
if e.error_len().is_some() {
return Err(DogeError::io_error("received bytes were not valid text"));
}
e.valid_up_to()
}
};
if valid_len > 0 {
let bytes: Vec<u8> = buf.drain(..valid_len).collect();
let text = String::from_utf8(bytes)
.expect("compiler bug: valid_up_to bytes are valid UTF-8");
return Ok(Value::str(text));
}
let mut chunk = vec![0u8; max];
match stream.read(&mut chunk) {
Ok(0) => {
if buf.is_empty() {
return Ok(Value::None);
}
return Err(DogeError::io_error(
"connection closed in the middle of a character",
));
}
Ok(got) => buf.extend_from_slice(&chunk[..got]),
Err(err) => return Err(DogeError::io_error(format!("cannot recv: {err}"))),
}
}
}
SocketState::Listener(_) => Err(DogeError::type_error(
"howl.recv needs a connection, not a listening socket",
)),
SocketState::Closed => Err(closed()),
}
}
pub fn howl_recv_line(conn: &Value) -> DogeResult {
let sock = socket_arg("recv_line", conn)?;
let mut state = sock.state.borrow_mut();
match &mut *state {
SocketState::Conn { stream, buf } => loop {
if let Some(pos) = buf.iter().position(|&b| b == b'\n') {
let mut line: Vec<u8> = buf.drain(..=pos).collect();
line.pop(); if line.last() == Some(&b'\r') {
line.pop();
}
return line_to_value(line);
}
let mut chunk = [0u8; LINE_CHUNK];
match stream.read(&mut chunk) {
Ok(0) => {
if buf.is_empty() {
return Ok(Value::None);
}
return line_to_value(std::mem::take(buf));
}
Ok(got) => buf.extend_from_slice(&chunk[..got]),
Err(err) => return Err(DogeError::io_error(format!("cannot recv: {err}"))),
}
},
SocketState::Listener(_) => Err(DogeError::type_error(
"howl.recv_line needs a connection, not a listening socket",
)),
SocketState::Closed => Err(closed()),
}
}
pub fn howl_close(sock: &Value) -> DogeResult {
let sock = socket_arg("close", sock)?;
*sock.state.borrow_mut() = SocketState::Closed;
Ok(Value::None)
}
pub fn howl_get(url: &Value) -> DogeResult {
let url = str_arg("howl", "get", url)?;
request_result(url, agent().get(url).call())
}
pub fn howl_post(url: &Value, body: &Value) -> DogeResult {
let url = str_arg("howl", "post", url)?;
let body = str_arg("howl", "post", body)?;
request_result(
url,
agent()
.post(url)
.set("Content-Type", "text/plain; charset=utf-8")
.send_string(body),
)
}
fn agent() -> ureq::Agent {
ureq::AgentBuilder::new().timeout(HTTP_TIMEOUT).build()
}
fn request_result(url: &str, result: Result<ureq::Response, ureq::Error>) -> DogeResult {
match result {
Ok(response) => response_dict(response),
Err(ureq::Error::Status(_, response)) => response_dict(response),
Err(ureq::Error::Transport(transport)) => Err(DogeError::io_error(format!(
"cannot fetch {url}: {}",
transport.message().unwrap_or("the request failed")
))),
}
}
fn response_dict(response: ureq::Response) -> DogeResult {
let status = response.status() as i64;
let body = response
.into_string()
.map_err(|err| DogeError::io_error(format!("cannot read the response: {err}")))?;
let mut entries = OrderedMap::new();
entries.insert("status".to_string(), Value::int(status));
entries.insert("body".to_string(), Value::str(body));
Ok(Value::dict(entries))
}
fn closed() -> DogeError {
DogeError::io_error("socket is closed")
}
fn line_to_value(bytes: Vec<u8>) -> DogeResult {
String::from_utf8(bytes)
.map(Value::str)
.map_err(|_| DogeError::io_error("received bytes were not valid text"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ErrorKind;
use bigdecimal::ToPrimitive;
use std::thread;
fn loopback() -> (Value, u16) {
let listener = howl_listen(&Value::str("127.0.0.1"), &Value::int(0)).unwrap();
let port = match howl_port(&listener).unwrap() {
Value::Int(p) => p.to_u16().unwrap(),
_ => panic!("port is an Int"),
};
(listener, port)
}
fn recv_str(conn: &Value, n: i64) -> Option<String> {
match howl_recv(conn, &Value::int(n)).unwrap() {
Value::Str(s) => Some(s.to_string()),
Value::None => None,
other => panic!("recv gave {}", other.type_name()),
}
}
#[test]
fn tcp_round_trip_and_close() {
let (listener, port) = loopback();
let client = howl_connect(&Value::str("127.0.0.1"), &Value::int(port as i64)).unwrap();
let server = howl_accept(&listener).unwrap();
howl_send(&client, &Value::str("much hello\n")).unwrap();
assert_eq!(howl_recv_line(&server).unwrap().to_string(), "much hello");
howl_send(&server, &Value::str("wow\n")).unwrap();
assert_eq!(howl_recv_line(&client).unwrap().to_string(), "wow");
howl_close(&server).unwrap();
assert!(matches!(howl_recv_line(&client).unwrap(), Value::None));
assert_eq!(
howl_send(&server, &Value::str("x")).unwrap_err().kind,
ErrorKind::IOError
);
howl_close(&server).unwrap();
}
#[test]
fn recv_reassembles_a_split_multibyte_character() {
let (listener, port) = loopback();
let client = howl_connect(&Value::str("127.0.0.1"), &Value::int(port as i64)).unwrap();
let server = howl_accept(&listener).unwrap();
howl_send(&client, &Value::str("é")).unwrap();
assert_eq!(recv_str(&server, 1).as_deref(), Some("é"));
}
#[test]
fn recv_reports_eof_as_none() {
let (listener, port) = loopback();
let client = howl_connect(&Value::str("127.0.0.1"), &Value::int(port as i64)).unwrap();
let server = howl_accept(&listener).unwrap();
howl_close(&client).unwrap();
assert_eq!(recv_str(&server, 16), None);
}
#[test]
fn wrong_socket_role_and_types_are_catchable() {
let (listener, port) = loopback();
let client = howl_connect(&Value::str("127.0.0.1"), &Value::int(port as i64)).unwrap();
assert_eq!(howl_accept(&client).unwrap_err().kind, ErrorKind::TypeError);
assert_eq!(
howl_send(&listener, &Value::str("x")).unwrap_err().kind,
ErrorKind::TypeError
);
assert_eq!(
howl_connect(&Value::int(1), &Value::int(port as i64))
.unwrap_err()
.kind,
ErrorKind::TypeError
);
assert_eq!(
howl_send(&Value::int(1), &Value::str("x"))
.unwrap_err()
.kind,
ErrorKind::TypeError
);
assert_eq!(
howl_recv(&client, &Value::int(0)).unwrap_err().kind,
ErrorKind::ValueError
);
assert_eq!(
howl_listen(&Value::str("127.0.0.1"), &Value::int(99999))
.unwrap_err()
.kind,
ErrorKind::ValueError
);
}
#[test]
fn connection_refused_is_a_catchable_io_error() {
let port = {
let (listener, port) = loopback();
howl_close(&listener).unwrap();
port
};
let err = howl_connect(&Value::str("127.0.0.1"), &Value::int(port as i64)).unwrap_err();
assert_eq!(err.kind, ErrorKind::IOError);
}
#[test]
fn http_get_returns_status_and_body() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let handle = thread::spawn(move || {
if let Ok((mut stream, _)) = listener.accept() {
let mut buf = [0u8; 1024];
let _ = stream.read(&mut buf);
let body = "much not found";
let response = format!(
"HTTP/1.1 404 Not Found\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
let _ = stream.write_all(response.as_bytes());
}
});
let url = Value::str(format!("http://127.0.0.1:{port}/"));
let result = howl_get(&url).unwrap();
handle.join().unwrap();
match result {
Value::Dict(entries) => {
let entries = entries.borrow();
assert!(entries
.get("status")
.is_some_and(|v| crate::values_equal(v, &Value::int(404))));
assert!(
matches!(entries.get("body"), Some(Value::Str(s)) if &**s == "much not found")
);
}
other => panic!("expected a Dict, got {}", other.type_name()),
}
}
#[test]
fn http_get_on_a_dead_port_is_a_catchable_io_error() {
let port = {
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
l.local_addr().unwrap().port()
};
let err = howl_get(&Value::str(format!("http://127.0.0.1:{port}/"))).unwrap_err();
assert_eq!(err.kind, ErrorKind::IOError);
}
}