use std::marker::PhantomData;
use std::sync::Arc;
use hotaru_core::{
app::common::RuntimeConfig,
connection::{ConnStream, HotaruRead, HotaruWrite, Outbound, TransportSpec},
protocol::{
Channel, CtxError, Protocol, ProtocolError, ProtocolFlow, ProtocolRole, RequestContext,
},
url::UrlRoot,
};
use hotaru_io_tokio::TcpStream;
use crate::{
channel::{Http1Channel, HttpChannel},
context::HttpContext,
protocol::{
error::HttpError,
helpers::{error_response_from, is_keep_alive, is_response_keep_alive, not_found_response},
},
security::safety::HttpSafety,
};
pub type DefaultHttpTransport = hotaru_io_tokio::TcpTransport;
pub type HTTP = Http1Protocol<TcpStream, DefaultHttpTransport>;
pub type Http1TcpProtocol = Http1Protocol<TcpStream, DefaultHttpTransport>;
#[cfg(feature = "tls")]
pub type Http1TlsProtocol = Http1Protocol<hotaru_tls::TlsStream, hotaru_tls::TlsTransport>;
#[cfg(feature = "tls")]
pub type HTTPS = Http1TlsProtocol;
pub struct Http1Protocol<
W: ConnStream = TcpStream,
TS: TransportSpec<Wire = W> = DefaultHttpTransport,
> {
role: ProtocolRole,
safety: Arc<HttpSafety>,
_wire: PhantomData<fn() -> W>,
_ts: PhantomData<fn() -> TS>,
}
impl<W: ConnStream, TS: TransportSpec<Wire = W>> Clone for Http1Protocol<W, TS> {
fn clone(&self) -> Self {
Self {
role: self.role,
safety: self.safety.clone(),
_wire: PhantomData,
_ts: PhantomData,
}
}
}
impl<W: ConnStream, TS: TransportSpec<Wire = W>> Http1Protocol<W, TS> {
pub fn server(safety: HttpSafety) -> Self {
Self {
role: ProtocolRole::Server,
safety: Arc::new(safety),
_wire: PhantomData,
_ts: PhantomData,
}
}
pub fn client(safety: HttpSafety) -> Self {
Self {
role: ProtocolRole::Client,
safety: Arc::new(safety),
_wire: PhantomData,
_ts: PhantomData,
}
}
pub fn safety(&self) -> &HttpSafety {
&self.safety
}
}
impl<W: ConnStream, TS: TransportSpec<Wire = W>> Protocol for Http1Protocol<W, TS>
where
HttpError: From<<TS as TransportSpec>::IoError>,
W::ReadHalf: HotaruRead<Error = std::io::Error>,
W::WriteHalf: HotaruWrite<Error = std::io::Error>,
{
type Wire = W;
type TS = TS;
type Channel = Http1Channel<W>;
type Stream = ();
type Message = ();
type Context = HttpContext<TS>;
fn name(&self) -> &'static str {
"http"
}
fn role(&self) -> ProtocolRole {
self.role
}
fn lit_parser<'a>(input: &'a str) -> Vec<&'a str> {
if input.is_empty() {
Vec::new()
} else {
input.split('/').collect()
}
}
fn detect(initial_bytes: &[u8]) -> bool {
initial_bytes.starts_with(b"GET ")
|| initial_bytes.starts_with(b"POST ")
|| initial_bytes.starts_with(b"PUT ")
|| initial_bytes.starts_with(b"DELETE ")
|| initial_bytes.starts_with(b"HEAD ")
|| initial_bytes.starts_with(b"OPTIONS ")
|| initial_bytes.starts_with(b"PATCH ")
|| initial_bytes.starts_with(b"CONNECT ")
|| initial_bytes.starts_with(b"TRACE ")
}
fn open_channel(
self,
reader: <<<Self::TS as TransportSpec>::Wire as ConnStream>::ReadHalf as HotaruRead>::Buffered,
writer: <<<Self::TS as TransportSpec>::Wire as ConnStream>::WriteHalf as HotaruWrite>::Buffered,
meta: <<Self::TS as TransportSpec>::Wire as ConnStream>::Meta,
) -> Self::Channel {
let safety = self.safety.clone();
Http1Channel::new(reader, writer, meta, safety)
}
async fn handle(
channel: &Self::Channel,
runtime: Arc<RuntimeConfig>,
root: Arc<UrlRoot<Self::Context, Self::TS>>,
) -> Result<ProtocolFlow, <Self::Context as RequestContext>::Error> {
let request = channel.parse_request(channel.safety()).await?;
let keep_alive = is_keep_alive(&request);
let path = request.meta.path();
let endpoint = match root.walk_str(&path).await {
Some(node) => node,
None => {
channel.send_response(not_found_response()).await?;
return Ok(if keep_alive {
ProtocolFlow::Continue
} else {
ProtocolFlow::Close
});
}
};
let mut ctx = HttpContext::new_server(
runtime.clone(),
endpoint.clone(),
request,
channel.remote_addr(),
channel.local_addr(),
channel.safety().clone(),
);
ctx.install_channel(channel.clone());
match endpoint.run(ctx).await {
Ok(ctx) => {
channel.send_response(ctx.response).await?;
Ok(if keep_alive {
ProtocolFlow::Continue
} else {
ProtocolFlow::Close
})
}
Err(err) if err.can_continue() => {
channel.send_response(error_response_from(&err)).await?;
Ok(if keep_alive {
ProtocolFlow::Continue
} else {
ProtocolFlow::Close
})
}
Err(_) => Ok(ProtocolFlow::Close),
}
}
async fn acquire_channel(
&self,
_runtime: &Arc<RuntimeConfig>,
outbound: Arc<<Self::TS as TransportSpec>::Outbound>,
) -> Result<Self::Channel, CtxError<Self>> {
let wire = outbound.connect().await?;
let (read, write, meta) = wire.split();
let reader = read.into_buf();
let writer = write.into_buf_write();
Ok(Http1Channel::new(reader, writer, meta, self.safety.clone()))
}
async fn send(
mut ctx: Self::Context,
) -> Result<Self::Context, <Self::Context as RequestContext>::Error> {
let channel = ctx.channel().cloned().ok_or_else(|| {
HttpError::ProtocolViolation("outpoint channel is not installed".to_string())
})?;
let safety = ctx.safety.clone();
let request = ctx.take_request();
channel.send_request(request).await?;
let response = channel.parse_response(&safety).await?;
let keep_alive = is_response_keep_alive(&response);
ctx.set_response(response);
if !keep_alive {
channel.close();
}
Ok(ctx)
}
fn install_channel(ctx: &mut Self::Context, channel: Self::Channel) {
ctx.install_channel(channel);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::message::http_value::StatusCode;
use crate::message::meta::HeaderValue;
use crate::message::request::HttpRequest;
#[test]
fn test_http1_detection() {
assert!(HTTP::detect(b"GET / HTTP/1.1\r\n"));
assert!(HTTP::detect(b"POST /api HTTP/1.1\r\n"));
assert!(HTTP::detect(b"PUT /resource HTTP/1.1\r\n"));
assert!(!HTTP::detect(b"INVALID REQUEST\r\n"));
assert!(!HTTP::detect(b""));
}
#[test]
fn test_is_keep_alive() {
let mut request = HttpRequest::default();
assert!(is_keep_alive(&request));
request.meta.header.insert(
"connection".to_string(),
HeaderValue::Single("close".to_string()),
);
assert!(!is_keep_alive(&request));
request.meta.header.insert(
"connection".to_string(),
HeaderValue::Single("keep-alive".to_string()),
);
assert!(is_keep_alive(&request));
}
#[test]
fn test_not_found_response() {
let resp = not_found_response();
assert_eq!(resp.meta.start_line.status_code(), StatusCode::NOT_FOUND);
}
#[test]
fn pattern_and_literal_sides_align() {
use hotaru_core::url::tokens_to_patterns;
let tokens = HTTP::tokenize_url("/users/<int:id>").unwrap();
let (patterns, _names) = tokens_to_patterns(&tokens).unwrap();
let segments = HTTP::lit_parser("/users/42");
assert_eq!(
patterns.len(),
segments.len(),
"leading-slash arity mismatch"
);
for (pat, seg) in patterns.iter().zip(segments.iter()) {
assert!(
pat.matches(seg),
"pattern {:?} did not match segment {:?}",
pat,
seg
);
}
}
#[test]
fn root_slash_aligns() {
use hotaru_core::url::tokens_to_patterns;
let tokens = HTTP::tokenize_url("/").unwrap();
let (patterns, _) = tokens_to_patterns(&tokens).unwrap();
let segments = HTTP::lit_parser("/");
assert_eq!(patterns.len(), segments.len());
for (pat, seg) in patterns.iter().zip(segments.iter()) {
assert!(pat.matches(seg));
}
}
}