add(http): custom error type for http request
This commit is contained in:
Generated
+2
-1
@@ -2866,7 +2866,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "server"
|
name = "server"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"async-broadcast",
|
"async-broadcast",
|
||||||
@@ -2887,6 +2887,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"str0m",
|
"str0m",
|
||||||
"symphonia",
|
"symphonia",
|
||||||
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|||||||
@@ -36,3 +36,4 @@ opus = "0.3.1"
|
|||||||
rubato = "3.0.0"
|
rubato = "3.0.0"
|
||||||
chrono = "0.4.45"
|
chrono = "0.4.45"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
|
thiserror = "2.0.18"
|
||||||
|
|||||||
+95
-129
@@ -46,6 +46,7 @@ use tracing::{debug, info, warn};
|
|||||||
use crate::{
|
use crate::{
|
||||||
AppState,
|
AppState,
|
||||||
hash::{hash_password, verify_password},
|
hash::{hash_password, verify_password},
|
||||||
|
http_error::HttpError,
|
||||||
webrtc_ingest::handle_whip_injest,
|
webrtc_ingest::handle_whip_injest,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,11 +86,6 @@ impl HttpServer {
|
|||||||
.allow_credentials(true)
|
.allow_credentials(true)
|
||||||
.allow_origin(origins);
|
.allow_origin(origins);
|
||||||
|
|
||||||
// TODO: Add an error type that impls IntoResponse, enum HttpError {}; impl IntoResponse for
|
|
||||||
// HttpError
|
|
||||||
// https://docs.rs/thiserror/latest/thiserror/
|
|
||||||
//
|
|
||||||
// Its rather shitty
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/api/catalog", get(catalog_handler))
|
.route("/api/catalog", get(catalog_handler))
|
||||||
.route("/api/user", post(create_user_handler))
|
.route("/api/user", post(create_user_handler))
|
||||||
@@ -136,42 +132,38 @@ async fn edit_stream_key(
|
|||||||
State(state): State<Arc<HttpServer>>,
|
State(state): State<Arc<HttpServer>>,
|
||||||
auth: AuthUser,
|
auth: AuthUser,
|
||||||
Json(payload): Json<EditStreamKeyRequest>,
|
Json(payload): Json<EditStreamKeyRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> Result<impl IntoResponse, HttpError> {
|
||||||
// Trim surrounding whitespace so labels aren't stored with leading/trailing spaces.
|
// Trim surrounding whitespace so labels aren't stored with leading/trailing spaces.
|
||||||
let new_label = payload.new.trim();
|
let new_label = payload.new.trim();
|
||||||
// Length (1..=67) and allowed charset ([A-Za-z0-9 _-], space included).
|
// Length (1..=67) and allowed charset ([A-Za-z0-9 _-], space included).
|
||||||
if !KEY_RE.is_match(new_label) {
|
if !KEY_RE.is_match(new_label) {
|
||||||
return (StatusCode::BAD_REQUEST, "invalid label").into_response();
|
return Err(HttpError::BadRequest("invalid label".into()));
|
||||||
}
|
}
|
||||||
// Replaces the JS lookahead: label must contain at least one letter.
|
// Replaces the JS lookahead: label must contain at least one letter.
|
||||||
if !new_label.chars().any(|c| c.is_ascii_alphabetic()) {
|
if !new_label.chars().any(|c| c.is_ascii_alphabetic()) {
|
||||||
return (StatusCode::BAD_REQUEST, "label must contain a letter").into_response();
|
return Err(HttpError::BadRequest("label must contain a letter".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let stream_key_id_result = stream_key::Entity::find_by_id(payload.id)
|
let stream_key = stream_key::Entity::find_by_id(payload.id)
|
||||||
.one(&state.db)
|
.one(&state.db)
|
||||||
.await;
|
.await?
|
||||||
if stream_key_id_result.is_err() {
|
.ok_or(HttpError::NotFound)?;
|
||||||
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
|
||||||
}
|
|
||||||
let stream_key = stream_key_id_result.unwrap().unwrap();
|
|
||||||
// Do they own the stream key
|
// Do they own the stream key
|
||||||
if stream_key.user_id != auth.0.id {
|
if stream_key.user_id != auth.0.id {
|
||||||
return StatusCode::FORBIDDEN.into_response();
|
return Err(HttpError::Forbidden);
|
||||||
}
|
}
|
||||||
stream_key
|
stream_key
|
||||||
.into_active_model()
|
.into_active_model()
|
||||||
.change_label_value(&state.db, new_label.to_string())
|
.change_label_value(&state.db, new_label.to_string())
|
||||||
.await
|
.await?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
StatusCode::OK.into_response()
|
Ok(StatusCode::OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn catalog_handler(State(state): State<Arc<HttpServer>>) -> impl IntoResponse {
|
async fn catalog_handler(
|
||||||
let streams = stream_session::Model::get_all_active_sessions(&state.db)
|
State(state): State<Arc<HttpServer>>,
|
||||||
.await
|
) -> Result<impl IntoResponse, HttpError> {
|
||||||
.unwrap();
|
let streams = stream_session::Model::get_all_active_sessions(&state.db).await?;
|
||||||
debug!("{:#?}", streams);
|
debug!("{:#?}", streams);
|
||||||
let catalog: Vec<StreamListing> = futures::future::join_all(streams.iter().map(|listing| {
|
let catalog: Vec<StreamListing> = futures::future::join_all(streams.iter().map(|listing| {
|
||||||
let db = state.db.clone();
|
let db = state.db.clone();
|
||||||
@@ -180,14 +172,12 @@ async fn catalog_handler(State(state): State<Arc<HttpServer>>) -> impl IntoRespo
|
|||||||
async move {
|
async move {
|
||||||
let key_info = stream_key::Entity::find_by_id(stream_key_id)
|
let key_info = stream_key::Entity::find_by_id(stream_key_id)
|
||||||
.one(&db)
|
.one(&db)
|
||||||
.await
|
.await?
|
||||||
.unwrap()
|
.ok_or(HttpError::NotFound)?;
|
||||||
.unwrap();
|
|
||||||
let user = users::Entity::find_by_id(key_info.user_id)
|
let user = users::Entity::find_by_id(key_info.user_id)
|
||||||
.one(&db)
|
.one(&db)
|
||||||
.await
|
.await?
|
||||||
.unwrap()
|
.ok_or(HttpError::NotFound)?;
|
||||||
.unwrap();
|
|
||||||
let meow = state2
|
let meow = state2
|
||||||
.appstate
|
.appstate
|
||||||
.clone()
|
.clone()
|
||||||
@@ -195,18 +185,20 @@ async fn catalog_handler(State(state): State<Arc<HttpServer>>) -> impl IntoRespo
|
|||||||
.await
|
.await
|
||||||
.stream_sessions
|
.stream_sessions
|
||||||
.get(&key_info.id)
|
.get(&key_info.id)
|
||||||
.unwrap()
|
.ok_or(HttpError::NotFound)?
|
||||||
.started_at;
|
.started_at;
|
||||||
StreamListing {
|
Ok::<_, HttpError>(StreamListing {
|
||||||
id: stream_key_id,
|
id: stream_key_id,
|
||||||
label: key_info.label,
|
label: key_info.label,
|
||||||
user: user.username,
|
user: user.username,
|
||||||
started_at: meow,
|
started_at: meow,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.await;
|
.await
|
||||||
Json(catalog)
|
.into_iter()
|
||||||
|
.collect::<Result<Vec<_>, HttpError>>()?;
|
||||||
|
Ok(Json(catalog))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -233,18 +225,17 @@ fn extract_session_token(headers: &HeaderMap) -> Option<String> {
|
|||||||
struct AuthUser(entity::users::Model);
|
struct AuthUser(entity::users::Model);
|
||||||
|
|
||||||
impl FromRequestParts<Arc<HttpServer>> for AuthUser {
|
impl FromRequestParts<Arc<HttpServer>> for AuthUser {
|
||||||
type Rejection = StatusCode;
|
type Rejection = HttpError;
|
||||||
|
|
||||||
async fn from_request_parts(
|
async fn from_request_parts(
|
||||||
parts: &mut Parts,
|
parts: &mut Parts,
|
||||||
state: &Arc<HttpServer>,
|
state: &Arc<HttpServer>,
|
||||||
) -> Result<Self, StatusCode> {
|
) -> Result<Self, HttpError> {
|
||||||
let token = extract_session_token(&parts.headers).ok_or(StatusCode::UNAUTHORIZED)?;
|
let token = extract_session_token(&parts.headers).ok_or(HttpError::Unauthorized)?;
|
||||||
|
|
||||||
let user = users::Entity::find_by_auth_session(&state.db, token)
|
let user = users::Entity::find_by_auth_session(&state.db, token)
|
||||||
.await
|
.await?
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
.ok_or(HttpError::Unauthorized)?;
|
||||||
.ok_or(StatusCode::UNAUTHORIZED)?;
|
|
||||||
|
|
||||||
Ok(AuthUser(user))
|
Ok(AuthUser(user))
|
||||||
}
|
}
|
||||||
@@ -254,12 +245,11 @@ async fn create_stream_key_handler(
|
|||||||
State(state): State<Arc<HttpServer>>,
|
State(state): State<Arc<HttpServer>>,
|
||||||
auth: AuthUser,
|
auth: AuthUser,
|
||||||
Json(payload): Json<CreateStreamKeyBody>,
|
Json(payload): Json<CreateStreamKeyBody>,
|
||||||
) -> impl IntoResponse {
|
) -> Result<impl IntoResponse, HttpError> {
|
||||||
let uuid = uuid::Uuid::new_v4();
|
let uuid = uuid::Uuid::new_v4();
|
||||||
let value = format!("stream-key-{uuid}");
|
let value = format!("stream-key-{uuid}");
|
||||||
let key_amount = stream_key::Entity::find_by_user(&state.db, auth.0.id)
|
let key_amount = stream_key::Entity::find_by_user(&state.db, auth.0.id)
|
||||||
.await
|
.await?
|
||||||
.unwrap()
|
|
||||||
.len();
|
.len();
|
||||||
if payload.label.is_empty() || payload.label.len() > MAX_LABEL_LEN {
|
if payload.label.is_empty() || payload.label.len() > MAX_LABEL_LEN {
|
||||||
warn!(
|
warn!(
|
||||||
@@ -268,7 +258,7 @@ async fn create_stream_key_handler(
|
|||||||
max = MAX_LABEL_LEN,
|
max = MAX_LABEL_LEN,
|
||||||
"stream key creation rejected: label length invalid"
|
"stream key creation rejected: label length invalid"
|
||||||
);
|
);
|
||||||
return StatusCode::UNPROCESSABLE_ENTITY;
|
return Err(HttpError::Unprocessable("label length invalid".into()));
|
||||||
}
|
}
|
||||||
if key_amount >= auth.0.stream_key_limit.try_into().unwrap() {
|
if key_amount >= auth.0.stream_key_limit.try_into().unwrap() {
|
||||||
warn!(
|
warn!(
|
||||||
@@ -276,16 +266,12 @@ async fn create_stream_key_handler(
|
|||||||
limit = auth.0.stream_key_limit,
|
limit = auth.0.stream_key_limit,
|
||||||
"stream key limit reached"
|
"stream key limit reached"
|
||||||
);
|
);
|
||||||
return StatusCode::NOT_ACCEPTABLE;
|
return Err(HttpError::NotAcceptable("stream key limit reached".into()));
|
||||||
}
|
}
|
||||||
let key = stream_key::Entity::create(&state.db, auth.0.id, value, payload.label, false).await;
|
let key = stream_key::Entity::create(&state.db, auth.0.id, value, payload.label, false).await?;
|
||||||
|
|
||||||
if let Ok(ref k) = key {
|
info!(user_id = auth.0.id, stream_key_id = key.id, label = %key.label, "stream key created");
|
||||||
info!(user_id = auth.0.id, stream_key_id = k.id, label = %k.label, "stream key created");
|
Ok(StatusCode::CREATED)
|
||||||
StatusCode::CREATED
|
|
||||||
} else {
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StreamKeys {
|
struct StreamKeys {
|
||||||
@@ -301,15 +287,9 @@ struct StreamKey {
|
|||||||
async fn get_all_stream_keys(
|
async fn get_all_stream_keys(
|
||||||
State(state): State<Arc<HttpServer>>,
|
State(state): State<Arc<HttpServer>>,
|
||||||
auth: AuthUser,
|
auth: AuthUser,
|
||||||
) -> impl IntoResponse {
|
) -> Result<impl IntoResponse, HttpError> {
|
||||||
// let user_keys = stream_key::Entity::find_by_user(&state.db, auth.0.id)
|
let keys = stream_key::Entity::find_by_user(&state.db, auth.0.id).await?;
|
||||||
// .await
|
Ok(Json(keys))
|
||||||
// .unwrap();
|
|
||||||
|
|
||||||
match stream_key::Entity::find_by_user(&state.db, auth.0.id).await {
|
|
||||||
Ok(keys) => Json(keys).into_response(),
|
|
||||||
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -326,41 +306,33 @@ struct LoginResponse {
|
|||||||
async fn login_handler(
|
async fn login_handler(
|
||||||
State(state): State<Arc<HttpServer>>,
|
State(state): State<Arc<HttpServer>>,
|
||||||
Json(payload): Json<LoginForm>,
|
Json(payload): Json<LoginForm>,
|
||||||
) -> impl IntoResponse {
|
) -> Result<impl IntoResponse, HttpError> {
|
||||||
if let Ok(x) = users::Entity::find_by_username(&state.db, payload.username.clone()).await {
|
let user = users::Entity::find_by_username(&state.db, payload.username.clone())
|
||||||
if let Some(x) = x {
|
.await?
|
||||||
let pass = verify_password(&payload.password, &x.hashed_password);
|
.ok_or_else(|| {
|
||||||
if !pass {
|
|
||||||
warn!(username = %payload.username, "login failed: wrong password");
|
|
||||||
let mut meow = Response::new("".to_string());
|
|
||||||
*meow.status_mut() = StatusCode::UNAUTHORIZED;
|
|
||||||
return meow;
|
|
||||||
};
|
|
||||||
info!(user_id = x.id, username = %x.username, "login successful");
|
|
||||||
let auth = auth_session::Entity::create(&state.db, x.id).await.unwrap();
|
|
||||||
let token = auth.value;
|
|
||||||
let mut meow = Response::new("".to_string());
|
|
||||||
// TODO: Replace with axum cookie jar https://docs.rs/axum-extra/latest/axum_extra/extract/cookie/struct.CookieJar.html
|
|
||||||
meow.headers_mut().insert(
|
|
||||||
SET_COOKIE,
|
|
||||||
format!("session={token}; SameSite=Strict; Path=/; Max-Age=2592000")
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
*meow.status_mut() = StatusCode::OK;
|
|
||||||
return meow;
|
|
||||||
} else {
|
|
||||||
warn!(username = %payload.username, "login failed: user not found");
|
warn!(username = %payload.username, "login failed: user not found");
|
||||||
let mut meow = Response::new("".to_string());
|
HttpError::Unauthorized
|
||||||
*meow.status_mut() = StatusCode::UNAUTHORIZED;
|
})?;
|
||||||
return meow;
|
|
||||||
}
|
if !verify_password(&payload.password, &user.hashed_password) {
|
||||||
} else {
|
warn!(username = %payload.username, "login failed: wrong password");
|
||||||
warn!(username = %payload.username, "login failed: DB error");
|
return Err(HttpError::Unauthorized);
|
||||||
let mut meow = Response::new("".to_string());
|
|
||||||
*meow.status_mut() = StatusCode::UNAUTHORIZED;
|
|
||||||
return meow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info!(user_id = user.id, username = %user.username, "login successful");
|
||||||
|
let auth = auth_session::Entity::create(&state.db, user.id).await?;
|
||||||
|
let token = auth.value;
|
||||||
|
|
||||||
|
let mut meow = Response::new("".to_string());
|
||||||
|
// TODO: Replace with axum cookie jar https://docs.rs/axum-extra/latest/axum_extra/extract/cookie/struct.CookieJar.html
|
||||||
|
meow.headers_mut().insert(
|
||||||
|
SET_COOKIE,
|
||||||
|
format!("session={token}; SameSite=Strict; Path=/; Max-Age=2592000")
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
*meow.status_mut() = StatusCode::OK;
|
||||||
|
Ok(meow)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -373,34 +345,33 @@ struct CreateUserForm {
|
|||||||
async fn create_user_handler(
|
async fn create_user_handler(
|
||||||
State(state): State<Arc<HttpServer>>,
|
State(state): State<Arc<HttpServer>>,
|
||||||
Json(payload): Json<CreateUserForm>,
|
Json(payload): Json<CreateUserForm>,
|
||||||
) -> (HeaderMap, StatusCode) {
|
) -> Result<(HeaderMap, StatusCode), HttpError> {
|
||||||
if state.config.signup_code.is_empty() || payload.ref_token != state.config.signup_code {
|
if state.config.signup_code.is_empty() || payload.ref_token != state.config.signup_code {
|
||||||
warn!(username = %payload.username, "signup rejected: invalid signup code");
|
warn!(username = %payload.username, "signup rejected: invalid signup code");
|
||||||
return (HeaderMap::new(), StatusCode::UNAUTHORIZED);
|
return Err(HttpError::Unauthorized);
|
||||||
}
|
}
|
||||||
if payload.username.is_empty() || payload.username.len() > MAX_USERNAME_LEN {
|
if payload.username.is_empty() || payload.username.len() > MAX_USERNAME_LEN {
|
||||||
warn!(username = %payload.username, max = MAX_USERNAME_LEN, "signup rejected: username length invalid");
|
warn!(username = %payload.username, max = MAX_USERNAME_LEN, "signup rejected: username length invalid");
|
||||||
return (HeaderMap::new(), StatusCode::UNPROCESSABLE_ENTITY);
|
return Err(HttpError::Unprocessable("username length invalid".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let meow = users::Entity::create(
|
let user = users::Entity::create(
|
||||||
&state.db,
|
&state.db,
|
||||||
payload.username.clone(),
|
payload.username.clone(),
|
||||||
hash_password(&payload.password).unwrap(),
|
hash_password(&payload.password).map_err(|_| HttpError::Hash)?,
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
// A create failure here is almost always a username unique-constraint
|
||||||
|
// violation, so surface it as a conflict rather than a 500.
|
||||||
|
warn!(username = %payload.username, error = %e, "user creation failed (likely username conflict)");
|
||||||
|
HttpError::Conflict
|
||||||
|
})?;
|
||||||
|
|
||||||
// Create session
|
info!(user_id = user.id, username = %user.username, "user created");
|
||||||
let session: auth_session::Model = if let Ok(ref user) = meow {
|
let session = auth_session::Entity::create(&state.db, user.id).await?;
|
||||||
info!(user_id = user.id, username = %user.username, "user created");
|
|
||||||
auth_session::Entity::create(&state.db, user.id)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
} else {
|
|
||||||
warn!(username = %payload.username, "user creation failed (likely username conflict)");
|
|
||||||
return (HeaderMap::new(), StatusCode::CONFLICT);
|
|
||||||
};
|
|
||||||
let token = session.value;
|
let token = session.value;
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
// TODO: Replace with axum cookie jar https://docs.rs/axum-extra/latest/axum_extra/extract/cookie/struct.CookieJar.html
|
// TODO: Replace with axum cookie jar https://docs.rs/axum-extra/latest/axum_extra/extract/cookie/struct.CookieJar.html
|
||||||
headers.insert(
|
headers.insert(
|
||||||
@@ -409,14 +380,14 @@ async fn create_user_handler(
|
|||||||
.parse()
|
.parse()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
(headers, StatusCode::OK)
|
Ok((headers, StatusCode::OK))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn stream_handler(
|
async fn stream_handler(
|
||||||
State(state): State<Arc<HttpServer>>,
|
State(state): State<Arc<HttpServer>>,
|
||||||
Path(slug): Path<String>,
|
Path(slug): Path<String>,
|
||||||
body: String,
|
body: String,
|
||||||
) -> impl IntoResponse {
|
) -> Result<impl IntoResponse, HttpError> {
|
||||||
let request_id_clone = state.request_count.fetch_add(1, Ordering::Relaxed);
|
let request_id_clone = state.request_count.fetch_add(1, Ordering::Relaxed);
|
||||||
|
|
||||||
let stream_key_id = {
|
let stream_key_id = {
|
||||||
@@ -427,47 +398,42 @@ async fn stream_handler(
|
|||||||
.map(|e| *e.key())
|
.map(|e| *e.key())
|
||||||
};
|
};
|
||||||
|
|
||||||
let stream_key_id = if let Some(id) = stream_key_id {
|
let stream_key_id = stream_key_id.ok_or_else(|| {
|
||||||
id
|
|
||||||
} else {
|
|
||||||
warn!(slug = %slug, "WHEP request for unknown or inactive stream");
|
warn!(slug = %slug, "WHEP request for unknown or inactive stream");
|
||||||
return Response::builder()
|
HttpError::NotFound
|
||||||
.status(StatusCode::NOT_FOUND)
|
})?;
|
||||||
.header("content-type", "application/text")
|
|
||||||
.body("".to_string())
|
|
||||||
.unwrap();
|
|
||||||
};
|
|
||||||
|
|
||||||
info!(request_id = request_id_clone, slug = %slug, stream_key_id, "WHEP offer received");
|
info!(request_id = request_id_clone, slug = %slug, stream_key_id, "WHEP offer received");
|
||||||
let mut accept_rx = state.accept_rx.activate_cloned();
|
let mut accept_rx = state.accept_rx.activate_cloned();
|
||||||
let _ = state
|
// The webrtc worker owning the receiver died if this fails.
|
||||||
|
state
|
||||||
.offer_tx
|
.offer_tx
|
||||||
.send((request_id_clone, stream_key_id, body))
|
.send((request_id_clone, stream_key_id, body))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.map_err(|_| HttpError::Internal)?;
|
||||||
debug!(
|
debug!(
|
||||||
request_id = request_id_clone,
|
request_id = request_id_clone,
|
||||||
"offer sent, waiting for answer"
|
"offer sent, waiting for answer"
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut reply_body = String::new();
|
let reply_body = String::new();
|
||||||
while let Ok(answer) = accept_rx.recv().await {
|
while let Ok(answer) = accept_rx.recv().await {
|
||||||
debug!(request_id = request_id_clone, "received answer candidate");
|
debug!(request_id = request_id_clone, "received answer candidate");
|
||||||
if let Some(reply) = answer.1 {
|
if let Some(reply) = answer.1 {
|
||||||
if answer.0 == request_id_clone {
|
if answer.0 == request_id_clone {
|
||||||
return Response::builder()
|
return Ok(Response::builder()
|
||||||
.status(StatusCode::CREATED)
|
.status(StatusCode::CREATED)
|
||||||
.header("content-type", "application/sdp")
|
.header("content-type", "application/sdp")
|
||||||
.body(reply)
|
.body(reply)
|
||||||
.unwrap();
|
.unwrap());
|
||||||
} else {
|
} else {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else if answer.0 == request_id_clone {
|
} else if answer.0 == request_id_clone {
|
||||||
return Response::builder()
|
return Ok(Response::builder()
|
||||||
.status(StatusCode::UNSUPPORTED_MEDIA_TYPE)
|
.status(StatusCode::UNSUPPORTED_MEDIA_TYPE)
|
||||||
.body(String::new())
|
.body(String::new())
|
||||||
.unwrap();
|
.unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
info!(
|
info!(
|
||||||
@@ -475,11 +441,11 @@ async fn stream_handler(
|
|||||||
"answer channel closed without a match"
|
"answer channel closed without a match"
|
||||||
);
|
);
|
||||||
|
|
||||||
Response::builder()
|
Ok(Response::builder()
|
||||||
.status(StatusCode::CREATED)
|
.status(StatusCode::CREATED)
|
||||||
.header("content-type", "application/sdp")
|
.header("content-type", "application/sdp")
|
||||||
.body(reply_body)
|
.body(reply_body)
|
||||||
.unwrap()
|
.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn meow_handler() -> &'static str {
|
async fn meow_handler() -> &'static str {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
http::{Response, StatusCode},
|
||||||
|
response::IntoResponse,
|
||||||
|
};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum HttpError {
|
||||||
|
#[error("db shit itself: {0}")]
|
||||||
|
DbErr(#[from] sea_orm::DbErr),
|
||||||
|
#[error("hashing failed")]
|
||||||
|
Hash,
|
||||||
|
#[error("resource not found")]
|
||||||
|
NotFound,
|
||||||
|
#[error("unauthorized")]
|
||||||
|
Unauthorized,
|
||||||
|
#[error("forbidden")]
|
||||||
|
Forbidden,
|
||||||
|
#[error("conflict")]
|
||||||
|
Conflict,
|
||||||
|
#[error("bad request: {0}")]
|
||||||
|
BadRequest(String),
|
||||||
|
#[error("unprocessable entity: {0}")]
|
||||||
|
Unprocessable(String),
|
||||||
|
#[error("not acceptable: {0}")]
|
||||||
|
NotAcceptable(String),
|
||||||
|
#[error("internal error")]
|
||||||
|
Internal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpError {
|
||||||
|
fn status(&self) -> StatusCode {
|
||||||
|
match self {
|
||||||
|
Self::DbErr(_) | Self::Hash | Self::Internal => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Self::NotFound => StatusCode::NOT_FOUND,
|
||||||
|
Self::Unauthorized => StatusCode::UNAUTHORIZED,
|
||||||
|
Self::Forbidden => StatusCode::FORBIDDEN,
|
||||||
|
Self::Conflict => StatusCode::CONFLICT,
|
||||||
|
Self::BadRequest(_) => StatusCode::BAD_REQUEST,
|
||||||
|
Self::Unprocessable(_) => StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
Self::NotAcceptable(_) => StatusCode::NOT_ACCEPTABLE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for HttpError {
|
||||||
|
fn into_response(self) -> Response<Body> {
|
||||||
|
let status = self.status();
|
||||||
|
// Only surface a message body for client (4xx) errors; keep an empty
|
||||||
|
// body for 5xx so internal details aren't leaked.
|
||||||
|
let body = if status.is_client_error() {
|
||||||
|
self.to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
(status, body).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ mod audio;
|
|||||||
mod codec;
|
mod codec;
|
||||||
mod hash;
|
mod hash;
|
||||||
mod http;
|
mod http;
|
||||||
|
mod http_error;
|
||||||
mod rtmp;
|
mod rtmp;
|
||||||
mod webrtc;
|
mod webrtc;
|
||||||
mod webrtc_ingest;
|
mod webrtc_ingest;
|
||||||
|
|||||||
Reference in New Issue
Block a user