rainier_framework/
signed.rs1use std::sync::Arc;
22
23use rainier_crypt::UrlSigner;
24use rainier_http::{IntoResponse, Request, Response};
25use rainier_middleware::{Middleware, MiddlewareStack, Next};
26use rainier_routing::UrlGenerator;
27use rainier_support::Result;
28
29pub struct SignedUrls {
35 urls: Arc<UrlGenerator>,
36 signer: Arc<UrlSigner>,
37}
38
39impl SignedUrls {
40 pub fn new(urls: Arc<UrlGenerator>, signer: Arc<UrlSigner>) -> Self {
42 Self { urls, signer }
43 }
44
45 pub fn route(&self, name: &str, params: &[(&str, &str)]) -> Result<String> {
56 self.signer.sign(&self.urls.route(name, params)?)
57 }
58
59 pub fn temporary_route(
68 &self,
69 name: &str,
70 expires_at: i64,
71 params: &[(&str, &str)],
72 ) -> Result<String> {
73 self.signer.sign_until(&self.urls.route(name, params)?, expires_at)
74 }
75
76 pub fn absolute_route(&self, name: &str, params: &[(&str, &str)]) -> Result<String> {
81 let absolute = self.urls.absolute(name, params)?;
82 self.signer.sign(&absolute)
83 }
84
85 pub fn temporary_absolute_route(
87 &self,
88 name: &str,
89 expires_at: i64,
90 params: &[(&str, &str)],
91 ) -> Result<String> {
92 let absolute = self.urls.absolute(name, params)?;
93 self.signer.sign_until(&absolute, expires_at)
94 }
95
96 pub fn sign(&self, url: &str) -> Result<String> {
98 self.signer.sign(url)
99 }
100
101 pub fn signer(&self) -> &Arc<UrlSigner> {
103 &self.signer
104 }
105}
106
107impl std::fmt::Debug for SignedUrls {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 f.write_str("SignedUrls")
110 }
111}
112
113pub struct ValidateSignature {
123 signer: Arc<UrlSigner>,
124}
125
126impl ValidateSignature {
127 pub fn new(signer: Arc<UrlSigner>) -> Self {
129 Self { signer }
130 }
131
132 pub fn resolved() -> MiddlewareStack {
137 MiddlewareStack::new().resolved(|signer: Arc<UrlSigner>| Self::new(signer))
138 }
139}
140
141#[async_trait::async_trait]
142impl Middleware for ValidateSignature {
143 async fn handle(&self, request: Request, next: Next) -> Response {
144 let url = match request.uri().query() {
147 Some(query) => format!("{}?{}", request.uri().path(), query),
148 None => request.uri().path().to_string(),
149 };
150
151 match self.signer.verify(&url) {
152 Ok(()) => next.run(request).await,
153 Err(e) => e.into_response(),
154 }
155 }
156
157 fn name(&self) -> &'static str {
158 "ValidateSignature"
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use rainier_crypt::{Key, KeyRing};
166 use rainier_http::{Method, StatusCode};
167 use rainier_middleware::Pipeline;
168
169 fn signer() -> Arc<UrlSigner> {
170 Arc::new(UrlSigner::new(KeyRing::new(Key::generate())))
171 }
172
173 fn urls() -> Arc<UrlGenerator> {
174 Arc::new(UrlGenerator::from_routes([
175 ("unsubscribe".to_string(), "/unsubscribe/{user}".to_string()),
176 ("verify".to_string(), "/verify".to_string()),
177 ]))
178 }
179
180 fn in_an_hour() -> i64 {
181 std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as i64
182 + 3600
183 }
184
185 async fn through(signer: Arc<UrlSigner>, url: &str) -> StatusCode {
186 Pipeline::new()
187 .through(ValidateSignature::new(signer))
188 .then(|_| async { Response::ok("followed") })
189 .run(Request::builder().method(Method::GET).uri(url).build())
190 .await
191 .status()
192 }
193
194 #[tokio::test]
195 async fn a_signed_route_is_followed() {
196 let signer = signer();
197 let signed = SignedUrls::new(urls(), Arc::clone(&signer));
198
199 let link = signed.route("unsubscribe", &[("user", "42")]).unwrap();
200
201 assert!(link.starts_with("/unsubscribe/42?"), "{link}");
202 assert_eq!(through(signer, &link).await, StatusCode::OK);
203 }
204
205 #[tokio::test]
206 async fn an_unsigned_request_to_the_same_route_is_refused() {
207 assert_eq!(through(signer(), "/unsubscribe/42").await, StatusCode::FORBIDDEN);
208 }
209
210 #[tokio::test]
211 async fn tampering_with_the_id_is_refused() {
212 let signer = signer();
214 let signed = SignedUrls::new(urls(), Arc::clone(&signer));
215
216 let link = signed.route("unsubscribe", &[("user", "42")]).unwrap();
217 let tampered = link.replace("/unsubscribe/42", "/unsubscribe/43");
218
219 assert_eq!(through(signer, &tampered).await, StatusCode::FORBIDDEN);
220 }
221
222 #[tokio::test]
223 async fn an_expired_link_is_refused_and_says_so() {
224 let signer = signer();
225 let signed = SignedUrls::new(urls(), Arc::clone(&signer));
226
227 let expired = signed.temporary_route("verify", 1, &[("user", "42")]).unwrap();
228
229 let response = Pipeline::new()
230 .through(ValidateSignature::new(signer))
231 .then(|_| async { Response::ok("followed") })
232 .run(Request::builder().method(Method::GET).uri(&expired).build())
233 .await;
234
235 assert_eq!(response.status(), StatusCode::FORBIDDEN);
236 assert!(response.into_string().await.unwrap().contains("expired"));
239 }
240
241 #[tokio::test]
242 async fn a_live_temporary_link_is_followed() {
243 let signer = signer();
244 let signed = SignedUrls::new(urls(), Arc::clone(&signer));
245
246 let link = signed.temporary_route("verify", in_an_hour(), &[("user", "42")]).unwrap();
247
248 assert!(link.contains("expires="));
249 assert_eq!(through(signer, &link).await, StatusCode::OK);
250 }
251
252 #[tokio::test]
253 async fn an_absolute_link_verifies_by_its_path() {
254 let signer = signer();
257 let signed = SignedUrls::new(
258 Arc::new(
259 UrlGenerator::from_routes([("verify".to_string(), "/verify".to_string())])
260 .with_base("https://app.example.com"),
261 ),
262 Arc::clone(&signer),
263 );
264
265 let absolute = signed.absolute_route("verify", &[("user", "42")]).unwrap();
266 assert!(absolute.starts_with("https://app.example.com/verify"), "{absolute}");
267
268 let path = absolute.trim_start_matches("https://app.example.com");
269 assert_eq!(through(signer, path).await, StatusCode::OK);
270 }
271
272 #[tokio::test]
273 async fn an_unknown_route_name_is_an_error_rather_than_an_unsigned_link() {
274 let signed = SignedUrls::new(urls(), signer());
275
276 assert!(signed.route("nope", &[]).is_err());
277 }
278}