actix_web_query_method_middleware/
lib.rs1use std::future::{ready, Ready};
55use std::rc::Rc;
56use std::str::FromStr;
57
58use actix_web::body::EitherBody;
59use actix_web::dev::{Service, Transform};
60use actix_web::dev::{ServiceRequest, ServiceResponse};
61use actix_web::http::{uri::PathAndQuery, Method, Uri};
62use actix_web::{Error, HttpResponse};
63use futures::future::LocalBoxFuture;
64use qstring::QString;
65
66#[derive(Clone, Debug)]
67pub struct QueryMethod {
73 parameter_name: String,
74 strict_mode: bool,
75}
76
77impl Default for QueryMethod {
78 fn default() -> Self {
79 Self {
80 parameter_name: "_method".to_string(),
81 strict_mode: false,
82 }
83 }
84}
85
86impl QueryMethod {
87 #[must_use]
89 pub fn new() -> Self {
90 Self::default()
91 }
92
93 #[must_use]
98 pub fn parameter_name(&mut self, name: &str) -> Self {
99 self.parameter_name = name.to_string();
100 self.clone()
101 }
102
103 #[must_use]
106 pub fn enable_strict_mode(&mut self) -> Self {
107 self.strict_mode = true;
108 self.clone()
109 }
110
111 #[must_use]
114 pub fn disable_strict_mode(&mut self) -> Self {
115 self.strict_mode = false;
116 self.clone()
117 }
118}
119
120impl<S, B> Transform<S, ServiceRequest> for QueryMethod
121where
122 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
123 S::Future: 'static,
124{
125 type Response = ServiceResponse<EitherBody<B>>;
126 type Error = Error;
127 type InitError = ();
128 type Transform = QueryMethodMiddleware<S>;
129 type Future = Ready<Result<Self::Transform, Self::InitError>>;
130
131 fn new_transform(&self, service: S) -> Self::Future {
132 ready(Ok(QueryMethodMiddleware {
133 service: Rc::new(service),
134 options: self.clone(),
135 }))
136 }
137}
138pub struct QueryMethodMiddleware<S> {
139 service: Rc<S>,
140 options: QueryMethod,
141}
142
143fn query_string_drop(query: QString, drop: &str) -> QString {
145 QString::new(
146 query
147 .into_iter()
148 .filter(|(k, _)| k.ne(drop))
149 .collect::<Vec<(String, String)>>(),
150 )
151}
152
153impl<S, B> Service<ServiceRequest> for QueryMethodMiddleware<S>
154where
155 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
156 S::Future: 'static,
157{
158 type Response = ServiceResponse<EitherBody<B>>;
159 type Error = Error;
160 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
161
162 actix_service::forward_ready!(service);
163
164 fn call(&self, mut req: ServiceRequest) -> Self::Future {
165 let uri = req.head().uri.clone();
166 let mut uri_parts = uri.into_parts();
167 let (path, query_string) = uri_parts.path_and_query.map_or_else(
168 || ("".to_string(), "".to_string()),
169 |pq| {
170 (
171 pq.path().to_string(),
172 pq.query()
173 .map_or_else(|| "".to_string(), ToString::to_string),
174 )
175 },
176 );
177 let query = QString::from(query_string.as_str());
178
179 if let Some(value) = query.clone().get(&self.options.parameter_name) {
180 let original_method = req.method();
182 if original_method.eq(&Method::POST) {
183 #[cfg(feature = "logging_tracing")]
184 tracing::debug!(
185 parameter_value = value,
186 path = req.path(),
187 original_method = original_method.as_str(),
188 "Rerouting request method"
189 );
190 #[cfg(feature = "logging_log")]
191 log::debug!("Rerouting request for {} to method {}", req.path(), value);
192 if let Ok(new_method) = Method::from_str(value) {
193 req.head_mut().method = new_method;
194 uri_parts.path_and_query = Some(
195 PathAndQuery::from_str(&format!(
196 "{}{}",
197 path,
198 query_string_drop(query, &self.options.parameter_name)
199 ))
200 .unwrap(),
206 );
207 req.head_mut().uri = Uri::from_parts(uri_parts).unwrap();
210 } else {
211 #[cfg(feature = "logging_tracing")]
212 tracing::warn!(
213 parameter_name = &self.options.parameter_name,
214 parameter_value = value,
215 path = req.path(),
216 original_method = original_method.as_str(),
217 "Received a bad method query parameter"
218 );
219 #[cfg(feature = "logging_log")]
220 log::warn!(
221 "Received a bad method query parameter {} for path {}",
222 value,
223 req.path(),
224 );
225 let value = value.to_string();
226 return Box::pin(async move {
227 let response = HttpResponse::BadRequest()
228 .body(format!("Method query parameter value {} is bad", value))
229 .map_into_right_body();
230 let (request, _) = req.into_parts();
231 Ok(ServiceResponse::new(request, response))
232 });
233 }
234 } else {
235 #[cfg(feature = "logging_tracing")]
236 tracing::warn!(
237 parameter_name = &self.options.parameter_name,
238 parameter_value = value,
239 path = req.path(),
240 original_method = original_method.as_str(),
241 "Received a non-POST request with the method query parameter"
242 );
243 #[cfg(feature = "logging_log")]
244 log::warn!(
245 "Received a {} {} request with the method query parameter",
246 original_method.as_str(),
247 req.path(),
248 );
249 if self.options.strict_mode {
250 let original_method = original_method.clone();
251 return Box::pin(async move {
252 let response = HttpResponse::BadRequest()
253 .body(format!(
254 "Method {} can not be rerouted with a query parameter",
255 original_method.as_str()
256 ))
257 .map_into_right_body();
258 let (request, _) = req.into_parts();
259 Ok(ServiceResponse::new(request, response))
260 });
261 }
262 }
263 }
264
265 let service = self.service.clone();
266 Box::pin(async move {
267 service
268 .call(req)
269 .await
270 .map(ServiceResponse::map_into_left_body)
271 })
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use actix_service::ServiceFactory;
279 use actix_web::{body::MessageBody, test, web, App, HttpRequest};
280
281 fn setup_test_app() -> App<
282 impl ServiceFactory<
283 ServiceRequest,
284 Response = ServiceResponse<impl MessageBody>,
285 Config = (),
286 InitError = (),
287 Error = Error,
288 >,
289 > {
290 App::new()
291 .wrap(QueryMethod::new())
292 .route(
293 "/",
294 web::get().to(|req: HttpRequest| {
295 let query_string = req.query_string().to_string();
296 async move { format!("GET {}", query_string) }
297 }),
298 )
299 .route(
300 "/",
301 web::put().to(|req: HttpRequest| {
302 let query_string = req.query_string().to_string();
303 async move { format!("PUT {}", query_string) }
304 }),
305 )
306 .route(
307 "/",
308 web::post().to(|req: HttpRequest| {
309 let query_string = req.query_string().to_string();
310 async move { format!("POST {}", query_string) }
311 }),
312 )
313 }
314
315 #[test_log::test(actix_web::test)]
316 async fn test_post_rerouted() {
317 let app = test::init_service(setup_test_app()).await;
318 let req = test::TestRequest::post().uri("/?_method=PUT").to_request();
319 let resp = test::call_and_read_body(&app, req).await;
320 let resp_text = String::from_utf8_lossy(&resp[..]);
321 assert_eq!(resp_text, "PUT ", "POST request rerouted to PUT");
322 }
323
324 #[test_log::test(actix_web::test)]
325 async fn test_post_not_rerouted_with_query_missing() {
326 let app = test::init_service(setup_test_app()).await;
327 let req = test::TestRequest::post().uri("/").to_request();
328 let resp = test::call_and_read_body(&app, req).await;
329 let resp_text = String::from_utf8_lossy(&resp[..]);
330 assert_eq!(resp_text, "POST ", "not rerouted");
331 }
332
333 #[test_log::test(actix_web::test)]
334 async fn test_post_not_rerouted_with_query_different() {
335 let app = test::init_service(setup_test_app()).await;
336 let req = test::TestRequest::post()
337 .uri("/?method=PUT")
339 .to_request();
340 let resp = test::call_and_read_body(&app, req).await;
341 let resp_text = String::from_utf8_lossy(&resp[..]);
342 assert_eq!(resp_text, "POST method=PUT", "not rerouted");
343 }
344
345 #[test_log::test(actix_web::test)]
346 async fn test_get_request_not_rerouted() {
347 let app = test::init_service(setup_test_app()).await;
348 let req = test::TestRequest::get().uri("/?_method=PUT").to_request();
349 let resp = test::call_and_read_body(&app, req).await;
350 let resp_text = String::from_utf8_lossy(&resp[..]);
351 assert_eq!(resp_text, "GET _method=PUT", "not rerouted");
352 }
353
354 #[test_log::test(actix_web::test)]
355 async fn test_get_request_failed_with_bad_method_value() {
356 let app = test::init_service(setup_test_app()).await;
357 let req = test::TestRequest::post()
358 .uri("/?_method=NO:METHOD")
359 .to_request();
360 let resp = test::call_service(&app, req).await;
361 assert_eq!(resp.status(), 400, "Request failed due to bad method value");
362 }
363
364 #[test_log::test(actix_web::test)]
365 async fn test_get_request_failed_in_strict_mode() {
366 let app = test::init_service(
367 App::new()
368 .wrap(QueryMethod::new().enable_strict_mode())
369 .route("/", web::get().to(|| async { "GET" }))
370 .route("/", web::post().to(|| async { "POST" }))
371 .route("/", web::put().to(|| async { "PUT" })),
372 )
373 .await;
374 let req = test::TestRequest::get().uri("/?_method=POST").to_request();
375 let resp = test::call_service(&app, req).await;
376 assert_eq!(resp.status(), 400, "Request failed in strict mode");
377 }
378
379 #[test_log::test(actix_web::test)]
380 async fn test_post_rerouted_with_nondefault_parameter_name() {
381 let app = test::init_service(
382 App::new()
383 .wrap(QueryMethod::new().parameter_name("_my_hidden_method"))
384 .route("/", web::get().to(|| async { "GET" }))
385 .route("/", web::post().to(|| async { "POST" }))
386 .route("/", web::put().to(|| async { "PUT" })),
387 )
388 .await;
389 let req = test::TestRequest::post()
390 .uri("/?_my_hidden_method=PUT")
391 .to_request();
392 let resp = test::call_and_read_body(&app, req).await;
393 let resp_text = String::from_utf8_lossy(&resp[..]);
394 assert_eq!(resp_text, "PUT", "POST request rerouted to PUT");
395 }
396
397 #[test_log::test(actix_web::test)]
398 async fn test_post_not_rerouted_with_nondefault_parameter_name_and_different_query() {
399 let app = test::init_service(
400 App::new()
401 .wrap(QueryMethod::new().parameter_name("_my_hidden_method"))
402 .route("/", web::get().to(|| async { "GET" }))
403 .route("/", web::post().to(|| async { "POST" }))
404 .route("/", web::put().to(|| async { "PUT" })),
405 )
406 .await;
407 let req = test::TestRequest::post()
408 .uri("/?_some_other_method=PUT")
409 .to_request();
410 let resp = test::call_and_read_body(&app, req).await;
411 let resp_text = String::from_utf8_lossy(&resp[..]);
412 assert_eq!(resp_text, "POST", "not rerouted");
413 }
414
415 #[test_log::test(actix_web::test)]
416 async fn test_post_reroutes_with_custom_method() {
417 let app = test::init_service(
418 App::new()
419 .wrap(QueryMethod::new())
420 .route("/", web::get().to(|| async { "GET" }))
421 .route("/", web::post().to(|| async { "POST" }))
422 .route(
423 "/",
424 web::method(Method::from_str("LIST").unwrap()).to(|| async { "LIST" }),
425 ),
426 )
427 .await;
428 let req = test::TestRequest::post().uri("/?_method=LIST").to_request();
429 let resp = test::call_and_read_body(&app, req).await;
430 let resp_text = String::from_utf8_lossy(&resp[..]);
431 assert_eq!(resp_text, "LIST", "POST request rerouted to LIST");
432 }
433
434 #[test_log::test(actix_web::test)]
435 async fn test_post_not_rerouted_with_bad_method_value() {
436 let app = test::init_service(setup_test_app()).await;
437 let req = test::TestRequest::post()
438 .uri("/?_method=LIST:ITEMS")
439 .to_request();
440 let resp = test::call_service(&app, req).await;
441 assert_eq!(resp.status(), 400, "Bad method value is rejected");
442 }
443}