actix_web_query_method_middleware/
lib.rs

1//! An Actix Web middleware that allows you to reroute `POST` requests to other
2//! methods like `PUT` or `DELETE` using a query parameter.
3//!
4//! This is useful in HTML forms where you can't use methods other than `GET` or
5//! `POST`. By adding this middleware to your server, you can submit the form to
6//! endpoints with methods other than `POST` by adding a query parameter like
7//! `/your/url?_method=PUT`.
8//!
9//! For example, in the HTML:
10//!
11//! ```html
12//! <form method="post" action="/path/to/endpoint?_method=DELETE">
13//!   <input type="submit" value="Delete this item" />
14//! </form>
15//! ```
16//!
17//! Then in your rust code:
18//!
19//! ```rs
20//! App::new()
21//!      .wrap(QueryMethod::default())
22//!      // ...
23//! ```
24//!
25//! The middleware will strip off the `_method` query parameter when rerouting
26//! your request, so the rerouting is transparent to your server code.
27//!
28//! Note that this middleware only applies to `POST` requests. Any other request
29//! like `GET` or `HEAD` will not be changed, because it would risk opening the
30//! server up to XSRF attacks. Requests like `PUT` and `DELETE` are also not
31//! changed because the parameter was likely included accidentally. By default
32//! the middleware will allow these requests to continue to your server
33//! unchanged, but you can enable the `strict_mode` parameter to reject such
34//! requests.
35//!
36//! The middleware will also reject any request where the method parameter
37//! specifies an invalid method that Actix Web doesn't accept. You *can* use
38//! custom HTTP methods like `LIST`, but not `LIST:ITEMS`. See the
39//! [HTTP spec for details](https://www.w3.org/Protocols/HTTP/1.1/draft-ietf-http-v11-spec-01#Method).
40//!
41//! This middleware uses [tracing](https://docs.rs/tracing/latest/tracing/) for
42//! logging. It will log warning events for bad requests (for example, GET
43//! request with method parameter), and will log debug events for good requests
44//! that have been modified by the middleware. If you prefer the `log` crate for
45//! your logging, you can enable it with the `logging_log` feature. You can also
46//! disable logging entirely.
47//!
48//! ```toml
49//! # To use `log` for logging
50//! actix-web-query-method-middleware = { version = "1.0", default-features = false, features = ["logging_log"] }
51//! # To disable logging entirely
52//! actix-web-query-method-middleware = { version = "1.0", default-features = false }
53//! ```
54use 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)]
67/// A middleware to pick HTTP method (PUT, DELETE, ...) with a query parameter.
68///
69/// This is useful for HTML forms which only support GET and POST methods. Using
70/// a query parameter, you can have this middleware route the request to another
71/// method.
72pub 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    /// Create the middleware with the default settings.
88    #[must_use]
89    pub fn new() -> Self {
90        Self::default()
91    }
92
93    /// The parameter name to use. By default this is `_method`, meaning that
94    /// you need to send your request like `/path?_method=POST` to use this
95    /// middleware. If you happen to already use `_method` in your application,
96    /// you can override the parameter name used here to pick something else.
97    #[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    /// Disabled by default. When enabled, the middleware will respond to
104    /// non-POST requests by rejecting them with a 400 code response.
105    #[must_use]
106    pub fn enable_strict_mode(&mut self) -> Self {
107        self.strict_mode = true;
108        self.clone()
109    }
110
111    /// Disabled by default. When disabled, the middleware will allow non-POST
112    /// requests that have
113    #[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
143/// Drop a parameter from the query string, if any. Returns a new query string.
144fn 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            // Method parameter specified, try to redirect
181            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                        // This unwrap is safe, since the string we're
201                        // making the path an query out of is the path and
202                        // query the server had already parsed and accepted.
203                        // Our modification here should not break things,
204                        // and we test for it as well.
205                        .unwrap(),
206                    );
207                    // This unwrap is also safe since we're just
208                    // reconstructing the uri from it's own old parts.
209                    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            // method instead of _method
338            .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}