use crate::proto::digest;
use crate::proto::http::{HttpRequest, HttpResponse};
use crate::raop::handlers_ap1::{self as handlers, RaopConnection};
#[cfg(feature = "ap2")]
use crate::raop::handlers_ap2;
#[cfg(feature = "hls")]
use crate::raop::handlers_hls;
type Handler = fn(&mut RaopConnection, &HttpRequest, &mut HttpResponse) -> Option<Vec<u8>>;
enum RouteResolution {
NoBody,
Handler(Handler),
}
struct Route {
method: &'static str,
path: &'static str,
handler: Handler,
}
const ROUTES: &[Route] = &[
#[cfg(feature = "ap2")]
Route {
method: "POST",
path: "/pair-setup",
handler: handlers_ap2::handle_pair_setup,
},
#[cfg(not(feature = "ap2"))]
Route {
method: "POST",
path: "/pair-setup",
handler: handlers::handle_pair_setup,
},
#[cfg(feature = "ap2")]
Route {
method: "POST",
path: "/pair-verify",
handler: handlers_ap2::handle_pair_verify,
},
#[cfg(not(feature = "ap2"))]
Route {
method: "POST",
path: "/pair-verify",
handler: handlers::handle_pair_verify,
},
Route {
method: "POST",
path: "/fp-setup",
handler: handlers::handle_fp_setup,
},
#[cfg(feature = "ap2")]
Route {
method: "POST",
path: "/feedback",
handler: handlers_ap2::handle_feedback,
},
#[cfg(feature = "ap2")]
Route {
method: "POST",
path: "/command",
handler: handlers_ap2::handle_command,
},
#[cfg(feature = "ap2")]
Route {
method: "POST",
path: "/audioMode",
handler: handlers_ap2::handle_audio_mode,
},
Route {
method: "OPTIONS",
path: "*",
handler: handlers::handle_options,
},
Route {
method: "ANNOUNCE",
path: "*",
handler: handlers::handle_announce,
},
Route {
method: "GET_PARAMETER",
path: "*",
handler: handlers::handle_get_parameter,
},
Route {
method: "SET_PARAMETER",
path: "*",
handler: handlers::handle_set_parameter,
},
#[cfg(feature = "ap2")]
Route {
method: "SETRATEANCHORTIME",
path: "*",
handler: handlers_ap2::handle_set_rate_anchor_time,
},
#[cfg(feature = "ap2")]
Route {
method: "SETPEERS",
path: "*",
handler: handlers_ap2::handle_set_peers,
},
#[cfg(feature = "ap2")]
Route {
method: "SETPEERSX",
path: "*",
handler: handlers_ap2::handle_set_peers,
},
#[cfg(feature = "ap2")]
Route {
method: "FLUSHBUFFERED",
path: "*",
handler: handlers_ap2::handle_flush_buffered,
},
#[cfg(feature = "ap2")]
Route {
method: "GET",
path: "/info",
handler: handlers_ap2::handle_info,
},
#[cfg(feature = "hls")]
Route {
method: "GET",
path: "/server-info",
handler: handlers_hls::handle_server_info,
},
#[cfg(feature = "hls")]
Route {
method: "POST",
path: "/play",
handler: handlers_hls::handle_play,
},
#[cfg(feature = "hls")]
Route {
method: "GET",
path: "/playback-info",
handler: handlers_hls::handle_playback_info,
},
#[cfg(feature = "hls")]
Route {
method: "POST",
path: "/stop",
handler: handlers_hls::handle_stop,
},
#[cfg(feature = "hls")]
Route {
method: "POST",
path: "/scrub",
handler: handlers_hls::handle_scrub,
},
#[cfg(feature = "hls")]
Route {
method: "POST",
path: "/rate",
handler: handlers_hls::handle_rate,
},
];
pub(crate) fn dispatch(conn: &mut RaopConnection, request: &HttpRequest) -> HttpResponse {
let method = request.method().unwrap_or("");
let url = request.url().unwrap_or("");
let cseq = request.header("CSeq").unwrap_or("0");
let mut response = HttpResponse::new("RTSP/1.0", 200, "OK");
response.add_header("CSeq", cseq);
response.add_header("Apple-Jack-Status", "connected; type=analog");
if method != "OPTIONS" && !conn.password.is_empty() {
let authorization = request.header("Authorization");
if !digest::is_valid("airplay", &conn.password, &conn.nonce, method, url, authorization) {
let auth_str = format!("Digest realm=\"airplay\", nonce=\"{}\"", conn.nonce);
response = HttpResponse::new("RTSP/1.0", 401, "Unauthorized");
response.add_header("CSeq", cseq);
response.add_header("WWW-Authenticate", &auth_str);
response.finish(None);
return response;
}
}
if let Some(challenge) = request.header("Apple-Challenge")
&& let Ok(sig) = conn.rsakey.sign_challenge(challenge, &conn.local_addr, &conn.hwaddr)
{
response.add_header("Apple-Response", &sig);
}
let response_data = match resolve_handler(conn, request, method, url) {
Some(RouteResolution::Handler(handler)) => handler(conn, request, &mut response),
Some(RouteResolution::NoBody) => None,
None => {
tracing::debug!(method, url, "Unhandled RTSP request");
response = HttpResponse::new("RTSP/1.0", 404, "Not Found");
response.add_header("CSeq", cseq);
response.finish(None);
return response;
}
};
response.finish(response_data.as_deref());
response
}
fn resolve_handler(
conn: &mut RaopConnection,
request: &HttpRequest,
method: &str,
url: &str,
) -> Option<RouteResolution> {
for route in ROUTES {
if route.method == method {
let path = url.split('?').next().unwrap_or(url);
if route.path == "*" || route.path == path {
return Some(RouteResolution::Handler(route.handler));
}
}
}
match method {
"SETUP" => resolve_setup(conn, request).map(RouteResolution::Handler),
"RECORD" => resolve_record(conn).map(RouteResolution::Handler),
"FLUSH" => {
handle_flush_inline(conn, request);
Some(RouteResolution::NoBody)
}
"TEARDOWN" => Some(RouteResolution::Handler(handle_teardown as Handler)),
_ => None,
}
}
fn resolve_setup(conn: &RaopConnection, request: &HttpRequest) -> Option<Handler> {
#[cfg(feature = "ap2")]
{
let is_plist = request.data().map(|d| d.starts_with(b"bplist")).unwrap_or(false);
if conn.is_ap2 || is_plist {
return Some(handlers_ap2::handle_setup);
}
}
let _ = (conn, request); Some(handlers::handle_setup)
}
fn resolve_record(conn: &RaopConnection) -> Option<Handler> {
#[cfg(feature = "ap2")]
if conn.is_ap2 {
return Some(handlers_ap2::handle_record);
}
let _ = conn;
None
}
fn handle_flush_inline(conn: &mut RaopConnection, request: &HttpRequest) {
if let Some(rtp_info) = request.header("RTP-Info")
&& let Some(seq_str) = rtp_info.strip_prefix("seq=")
&& let Ok(next_seq) = seq_str.parse::<i32>()
&& let Some(rtp) = &conn.raop_rtp
{
rtp.flush(next_seq);
}
}
fn handle_teardown(conn: &mut RaopConnection, _request: &HttpRequest, response: &mut HttpResponse) -> Option<Vec<u8>> {
response.add_header("Connection", "close");
response.set_disconnect(true);
if let Some(mut rtp) = conn.raop_rtp.take() {
rtp.stop();
}
#[cfg(feature = "ap2")]
if let Some(cmd) = &conn.playout_cmd {
let _ = cmd.send(crate::raop::buffered_audio::PlayoutCommand::Stop);
}
None
}