mirror of
https://github.com/meilisearch/meilisearch.git
synced 2025-09-04 19:56:30 +00:00
Add URL and header validity checks
This commit is contained in:
@ -1,5 +1,9 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use actix_http::header::{
|
||||||
|
HeaderName, HeaderValue, InvalidHeaderName as ActixInvalidHeaderName,
|
||||||
|
InvalidHeaderValue as ActixInvalidHeaderValue,
|
||||||
|
};
|
||||||
use actix_web::web::{self, Data, Path};
|
use actix_web::web::{self, Data, Path};
|
||||||
use actix_web::{HttpRequest, HttpResponse};
|
use actix_web::{HttpRequest, HttpResponse};
|
||||||
use deserr::actix_web::AwebJson;
|
use deserr::actix_web::AwebJson;
|
||||||
@ -13,6 +17,7 @@ use meilisearch_types::milli::update::Setting;
|
|||||||
use meilisearch_types::webhooks::Webhook;
|
use meilisearch_types::webhooks::Webhook;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
use url::Url;
|
||||||
use utoipa::{OpenApi, ToSchema};
|
use utoipa::{OpenApi, ToSchema};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@ -184,6 +189,12 @@ enum WebhooksError {
|
|||||||
ReservedWebhook(Uuid),
|
ReservedWebhook(Uuid),
|
||||||
#[error("Webhook `{0}` not found.")]
|
#[error("Webhook `{0}` not found.")]
|
||||||
WebhookNotFound(Uuid),
|
WebhookNotFound(Uuid),
|
||||||
|
#[error("Invalid header name `{0}`: {1}")]
|
||||||
|
InvalidHeaderName(String, ActixInvalidHeaderName),
|
||||||
|
#[error("Invalid header value `{0}`: {1}")]
|
||||||
|
InvalidHeaderValue(String, ActixInvalidHeaderValue),
|
||||||
|
#[error("Invalid URL `{0}`: {1}")]
|
||||||
|
InvalidUrl(String, url::ParseError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ErrorCode for WebhooksError {
|
impl ErrorCode for WebhooksError {
|
||||||
@ -194,6 +205,9 @@ impl ErrorCode for WebhooksError {
|
|||||||
TooManyHeaders(_) => meilisearch_types::error::Code::InvalidWebhooksHeaders,
|
TooManyHeaders(_) => meilisearch_types::error::Code::InvalidWebhooksHeaders,
|
||||||
ReservedWebhook(_) => meilisearch_types::error::Code::ReservedWebhook,
|
ReservedWebhook(_) => meilisearch_types::error::Code::ReservedWebhook,
|
||||||
WebhookNotFound(_) => meilisearch_types::error::Code::WebhookNotFound,
|
WebhookNotFound(_) => meilisearch_types::error::Code::WebhookNotFound,
|
||||||
|
InvalidHeaderName(_, _) => meilisearch_types::error::Code::InvalidWebhooksHeaders,
|
||||||
|
InvalidHeaderValue(_, _) => meilisearch_types::error::Code::InvalidWebhooksHeaders,
|
||||||
|
InvalidUrl(_, _) => meilisearch_types::error::Code::InvalidWebhooksUrl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -239,6 +253,32 @@ fn patch_webhook_inner(
|
|||||||
Ok(Webhook { url, headers })
|
Ok(Webhook { url, headers })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_changed(uuid: Uuid, webhook: &Webhook) -> Result<(), WebhooksError> {
|
||||||
|
if uuid.is_nil() {
|
||||||
|
return Err(ReservedWebhook(uuid));
|
||||||
|
}
|
||||||
|
|
||||||
|
if webhook.url.is_empty() {
|
||||||
|
return Err(MissingUrl(uuid));
|
||||||
|
}
|
||||||
|
|
||||||
|
if webhook.headers.len() > 200 {
|
||||||
|
return Err(TooManyHeaders(uuid));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (header, value) in &webhook.headers {
|
||||||
|
HeaderName::from_bytes(header.as_bytes())
|
||||||
|
.map_err(|e| InvalidHeaderName(header.to_owned(), e))?;
|
||||||
|
HeaderValue::from_str(value).map_err(|e| InvalidHeaderValue(header.to_owned(), e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = Url::parse(&webhook.url) {
|
||||||
|
return Err(InvalidUrl(webhook.url.to_owned(), e));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/{uuid}",
|
path = "/{uuid}",
|
||||||
@ -320,6 +360,8 @@ async fn post_webhook(
|
|||||||
.map(|h| h.into_iter().map(|(k, v)| (k, v.set().unwrap_or_default())).collect())
|
.map(|h| h.into_iter().map(|(k, v)| (k, v.set().unwrap_or_default())).collect())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
check_changed(uuid, &webhook)?;
|
||||||
webhooks.webhooks.insert(uuid, webhook.clone());
|
webhooks.webhooks.insert(uuid, webhook.clone());
|
||||||
index_scheduler.put_webhooks(webhooks)?;
|
index_scheduler.put_webhooks(webhooks)?;
|
||||||
|
|
||||||
@ -363,18 +405,11 @@ async fn patch_webhook(
|
|||||||
let webhook_settings = webhook_settings.into_inner();
|
let webhook_settings = webhook_settings.into_inner();
|
||||||
debug!(parameters = ?(uuid, &webhook_settings), "Patch webhook");
|
debug!(parameters = ?(uuid, &webhook_settings), "Patch webhook");
|
||||||
|
|
||||||
if uuid.is_nil() {
|
|
||||||
return Err(ReservedWebhook(uuid).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut webhooks = index_scheduler.webhooks();
|
let mut webhooks = index_scheduler.webhooks();
|
||||||
let old_webhook = webhooks.webhooks.remove(&uuid);
|
let old_webhook = webhooks.webhooks.remove(&uuid);
|
||||||
let webhook = patch_webhook_inner(&uuid, old_webhook, webhook_settings)?;
|
let webhook = patch_webhook_inner(&uuid, old_webhook, webhook_settings)?;
|
||||||
|
|
||||||
if webhook.headers.len() > 200 {
|
check_changed(uuid, &webhook)?;
|
||||||
return Err(TooManyHeaders(uuid).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
webhooks.webhooks.insert(uuid, webhook.clone());
|
webhooks.webhooks.insert(uuid, webhook.clone());
|
||||||
index_scheduler.put_webhooks(webhooks)?;
|
index_scheduler.put_webhooks(webhooks)?;
|
||||||
|
|
||||||
|
@ -469,3 +469,54 @@ async fn patch() {
|
|||||||
}
|
}
|
||||||
"#);
|
"#);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn invalid_url_and_headers() {
|
||||||
|
let server = Server::new().await;
|
||||||
|
|
||||||
|
// Test invalid URL format
|
||||||
|
let (value, code) = server.create_webhook(json!({ "url": "not-a-valid-url" })).await;
|
||||||
|
snapshot!(code, @"400 Bad Request");
|
||||||
|
snapshot!(value, @r#"
|
||||||
|
{
|
||||||
|
"message": "Invalid URL `not-a-valid-url`: relative URL without a base",
|
||||||
|
"code": "invalid_webhooks_url",
|
||||||
|
"type": "invalid_request",
|
||||||
|
"link": "https://docs.meilisearch.com/errors#invalid_webhooks_url"
|
||||||
|
}
|
||||||
|
"#);
|
||||||
|
|
||||||
|
// Test invalid header name (containing spaces)
|
||||||
|
let (value, code) = server
|
||||||
|
.create_webhook(json!({
|
||||||
|
"url": "https://example.com/hook",
|
||||||
|
"headers": { "invalid header name": "value" }
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
snapshot!(code, @"400 Bad Request");
|
||||||
|
snapshot!(value, @r#"
|
||||||
|
{
|
||||||
|
"message": "Invalid header name `invalid header name`: invalid HTTP header name",
|
||||||
|
"code": "invalid_webhooks_headers",
|
||||||
|
"type": "invalid_request",
|
||||||
|
"link": "https://docs.meilisearch.com/errors#invalid_webhooks_headers"
|
||||||
|
}
|
||||||
|
"#);
|
||||||
|
|
||||||
|
// Test invalid header value (containing control characters)
|
||||||
|
let (value, code) = server
|
||||||
|
.create_webhook(json!({
|
||||||
|
"url": "https://example.com/hook",
|
||||||
|
"headers": { "authorization": "token\nwith\nnewlines" }
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
snapshot!(code, @"400 Bad Request");
|
||||||
|
snapshot!(value, @r#"
|
||||||
|
{
|
||||||
|
"message": "Invalid header value `authorization`: failed to parse header value",
|
||||||
|
"code": "invalid_webhooks_headers",
|
||||||
|
"type": "invalid_request",
|
||||||
|
"link": "https://docs.meilisearch.com/errors#invalid_webhooks_headers"
|
||||||
|
}
|
||||||
|
"#);
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user