1use std::sync::Arc;
50
51use axum::body::Body;
52use axum::extract::Request;
53use axum::http::{header, HeaderValue, Method, StatusCode};
54use axum::middleware::Next;
55use axum::response::Response;
56use axum::Router;
57
58#[derive(Clone, Debug)]
63pub struct MethodRestrictLayer {
64 allowed: Arc<Vec<Method>>,
65 allow_header: Arc<String>,
68}
69
70impl MethodRestrictLayer {
71 #[must_use]
75 pub fn any_of<I, M>(methods: I) -> Self
76 where
77 I: IntoIterator<Item = M>,
78 M: Into<Method>,
79 {
80 let allowed: Vec<Method> = methods.into_iter().map(Into::into).collect();
81 let allow_header = allowed
82 .iter()
83 .map(Method::as_str)
84 .collect::<Vec<_>>()
85 .join(", ");
86 Self {
87 allowed: Arc::new(allowed),
88 allow_header: Arc::new(allow_header),
89 }
90 }
91
92 #[must_use]
94 pub fn get_only() -> Self {
95 Self::any_of([Method::GET])
96 }
97
98 #[must_use]
100 pub fn post_only() -> Self {
101 Self::any_of([Method::POST])
102 }
103
104 #[must_use]
108 pub fn safe_only() -> Self {
109 Self::any_of([Method::GET, Method::HEAD, Method::OPTIONS])
110 }
111}
112
113pub trait MethodRestrictRouterExt {
116 #[must_use]
118 fn require_methods<I, M>(self, methods: I) -> Self
119 where
120 I: IntoIterator<Item = M>,
121 M: Into<Method>;
122
123 #[must_use]
125 fn require_get(self) -> Self;
126
127 #[must_use]
129 fn require_post(self) -> Self;
130
131 #[must_use]
133 fn require_safe(self) -> Self;
134}
135
136impl<S> MethodRestrictRouterExt for Router<S>
137where
138 S: Clone + Send + Sync + 'static,
139{
140 fn require_methods<I, M>(self, methods: I) -> Self
141 where
142 I: IntoIterator<Item = M>,
143 M: Into<Method>,
144 {
145 attach(self, MethodRestrictLayer::any_of(methods))
146 }
147 fn require_get(self) -> Self {
148 attach(self, MethodRestrictLayer::get_only())
149 }
150 fn require_post(self) -> Self {
151 attach(self, MethodRestrictLayer::post_only())
152 }
153 fn require_safe(self) -> Self {
154 attach(self, MethodRestrictLayer::safe_only())
155 }
156}
157
158fn attach<S: Clone + Send + Sync + 'static>(
159 router: Router<S>,
160 layer: MethodRestrictLayer,
161) -> Router<S> {
162 router.layer(axum::middleware::from_fn(
163 move |req: Request<Body>, next: Next| {
164 let layer = layer.clone();
165 async move { handle(layer, req, next).await }
166 },
167 ))
168}
169
170async fn handle(layer: MethodRestrictLayer, req: Request<Body>, next: Next) -> Response {
171 if layer.allowed.contains(req.method()) {
172 return next.run(req).await;
173 }
174 let mut resp = Response::builder()
175 .status(StatusCode::METHOD_NOT_ALLOWED)
176 .body(Body::empty())
177 .unwrap_or_else(|_| Response::new(Body::empty()));
178 if let Ok(v) = HeaderValue::from_str(&layer.allow_header) {
179 resp.headers_mut().insert(header::ALLOW, v);
180 }
181 resp
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use axum::routing::any;
188 use tower::ServiceExt;
189
190 fn app(layer: MethodRestrictLayer) -> Router {
191 attach(Router::new().route("/r", any(|| async { "ok" })), layer)
192 }
193
194 fn req(method: Method) -> Request<Body> {
195 Request::builder()
196 .method(method)
197 .uri("/r")
198 .body(Body::empty())
199 .unwrap()
200 }
201
202 #[tokio::test]
203 async fn require_methods_accepts_listed() {
204 let res = app(MethodRestrictLayer::any_of([Method::GET, Method::POST]))
205 .oneshot(req(Method::POST))
206 .await
207 .unwrap();
208 assert_eq!(res.status(), 200);
209 }
210
211 #[tokio::test]
212 async fn require_methods_rejects_unlisted_with_405() {
213 let res = app(MethodRestrictLayer::any_of([Method::GET]))
214 .oneshot(req(Method::POST))
215 .await
216 .unwrap();
217 assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED);
218 }
219
220 #[tokio::test]
221 async fn rejection_includes_allow_header() {
222 let res = app(MethodRestrictLayer::any_of([Method::GET, Method::POST]))
223 .oneshot(req(Method::DELETE))
224 .await
225 .unwrap();
226 let allow = res
227 .headers()
228 .get(header::ALLOW)
229 .and_then(|v| v.to_str().ok())
230 .unwrap()
231 .to_owned();
232 assert!(allow.contains("GET"));
233 assert!(allow.contains("POST"));
234 assert!(!allow.contains("DELETE"));
235 }
236
237 #[tokio::test]
238 async fn get_only_rejects_post() {
239 let res = app(MethodRestrictLayer::get_only())
240 .oneshot(req(Method::POST))
241 .await
242 .unwrap();
243 assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED);
244 }
245
246 #[tokio::test]
247 async fn post_only_accepts_post() {
248 let res = app(MethodRestrictLayer::post_only())
249 .oneshot(req(Method::POST))
250 .await
251 .unwrap();
252 assert_eq!(res.status(), 200);
253 }
254
255 #[tokio::test]
256 async fn safe_only_accepts_get_head_options() {
257 for m in [Method::GET, Method::HEAD, Method::OPTIONS] {
258 let res = app(MethodRestrictLayer::safe_only())
259 .oneshot(req(m.clone()))
260 .await
261 .unwrap();
262 assert_eq!(res.status(), 200, "method {m:?} should be accepted");
263 }
264 }
265
266 #[tokio::test]
267 async fn safe_only_rejects_post() {
268 let res = app(MethodRestrictLayer::safe_only())
269 .oneshot(req(Method::POST))
270 .await
271 .unwrap();
272 assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED);
273 }
274
275 #[tokio::test]
276 async fn router_ext_require_methods() {
277 let app: Router = Router::new()
278 .route("/r", any(|| async { "ok" }))
279 .require_methods([Method::POST]);
280 let res = app.oneshot(req(Method::GET)).await.unwrap();
281 assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED);
282 }
283
284 #[tokio::test]
285 async fn router_ext_require_get() {
286 let app: Router = Router::new()
287 .route("/r", any(|| async { "ok" }))
288 .require_get();
289 let ok = app.clone().oneshot(req(Method::GET)).await.unwrap();
290 assert_eq!(ok.status(), 200);
291 let bad = app.oneshot(req(Method::POST)).await.unwrap();
292 assert_eq!(bad.status(), StatusCode::METHOD_NOT_ALLOWED);
293 }
294
295 #[tokio::test]
296 async fn router_ext_require_post() {
297 let app: Router = Router::new()
298 .route("/r", any(|| async { "ok" }))
299 .require_post();
300 let bad = app.clone().oneshot(req(Method::GET)).await.unwrap();
301 assert_eq!(bad.status(), StatusCode::METHOD_NOT_ALLOWED);
302 let ok = app.oneshot(req(Method::POST)).await.unwrap();
303 assert_eq!(ok.status(), 200);
304 }
305
306 #[tokio::test]
307 async fn empty_methods_list_rejects_everything() {
308 let app = app(MethodRestrictLayer::any_of(Vec::<Method>::new()));
311 let res = app.oneshot(req(Method::GET)).await.unwrap();
312 assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED);
313 let allow = res
314 .headers()
315 .get(header::ALLOW)
316 .and_then(|v| v.to_str().ok())
317 .unwrap();
318 assert!(allow.is_empty());
319 }
320
321 #[test]
322 fn allow_header_lists_methods_in_construction_order() {
323 let layer = MethodRestrictLayer::any_of([Method::POST, Method::GET, Method::DELETE]);
324 assert_eq!(layer.allow_header.as_str(), "POST, GET, DELETE");
325 }
326}