1 + | /*
|
2 + | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3 + | * SPDX-License-Identifier: Apache-2.0
|
4 + | */
|
5 + |
|
6 + | use crate::error::{ErrorKind, SigningError};
|
7 + | use crate::key::PrivateKey;
|
8 + | use crate::policy::Policy;
|
9 + | use aws_smithy_types::DateTime;
|
10 + | use std::borrow::Cow;
|
11 + | use std::fmt;
|
12 + | use std::time::Duration;
|
13 + |
|
14 + | /// CloudFront-specific base64 encoding.
|
15 + | /// Standard base64 with: `+` → `-`, `=` → `_`, `/` → `~`
|
16 + | fn cloudfront_base64(data: &[u8]) -> String {
|
17 + | base64_simd::STANDARD
|
18 + | .encode_to_string(data)
|
19 + | .replace('+', "-")
|
20 + | .replace('=', "_")
|
21 + | .replace('/', "~")
|
22 + | }
|
23 + |
|
24 + | const COOKIE_POLICY: &str = "CloudFront-Policy";
|
25 + | const COOKIE_SIGNATURE: &str = "CloudFront-Signature";
|
26 + | const COOKIE_KEY_PAIR_ID: &str = "CloudFront-Key-Pair-Id";
|
27 + | const COOKIE_EXPIRES: &str = "CloudFront-Expires";
|
28 + |
|
29 + | #[derive(Debug, Clone)]
|
30 + | enum Expiration {
|
31 + | DateTime(DateTime),
|
32 + | Duration(Duration),
|
33 + | }
|
34 + |
|
35 + | /// Request to sign a CloudFront URL or generate signed cookies.
|
36 + | #[derive(Debug, Clone)]
|
37 + | pub struct SigningRequest {
|
38 + | pub(crate) resource_url: String,
|
39 + | pub(crate) resource_pattern: Option<String>,
|
40 + | pub(crate) key_pair_id: String,
|
41 + | pub(crate) private_key: PrivateKey,
|
42 + | pub(crate) expiration: DateTime,
|
43 + | pub(crate) active_date: Option<DateTime>,
|
44 + | pub(crate) ip_range: Option<String>,
|
45 + | }
|
46 + |
|
47 + | impl SigningRequest {
|
48 + | /// Creates a new builder for constructing a signing request.
|
49 + | pub fn builder() -> SigningRequestBuilder {
|
50 + | SigningRequestBuilder::default()
|
51 + | }
|
52 + | }
|
53 + |
|
54 + | /// Builder for [`SigningRequest`].
|
55 + | #[derive(Default, Debug)]
|
56 + | pub struct SigningRequestBuilder {
|
57 + | resource_url: Option<String>,
|
58 + | resource_pattern: Option<String>,
|
59 + | key_pair_id: Option<String>,
|
60 + | private_key: Option<PrivateKey>,
|
61 + | expiration: Option<Expiration>,
|
62 + | active_date: Option<DateTime>,
|
63 + | ip_range: Option<String>,
|
64 + | time_source: Option<aws_smithy_async::time::SharedTimeSource>,
|
65 + | }
|
66 + |
|
67 + | impl SigningRequestBuilder {
|
68 + | /// Sets the CloudFront resource URL to sign.
|
69 + | pub fn resource_url(mut self, url: impl Into<String>) -> Self {
|
70 + | self.resource_url = Some(url.into());
|
71 + | self
|
72 + | }
|
73 + |
|
74 + | /// Sets a wildcard pattern for the policy's `Resource` field.
|
75 + | ///
|
76 + | /// Use this when you want the signed URL to grant access to multiple resources
|
77 + | /// matching a pattern. If not set, `resource_url` is used in the policy.
|
78 + | ///
|
79 + | /// Wildcards:
|
80 + | /// - `*` matches zero or more characters
|
81 + | /// - `?` matches exactly one character
|
82 + | ///
|
83 + | /// # Example
|
84 + | /// ```ignore
|
85 + | /// // Sign a specific URL but grant access to all files under /videos/
|
86 + | /// SigningRequest::builder()
|
87 + | /// .resource_url("https://d111111abcdef8.cloudfront.net/videos/intro.mp4")
|
88 + | /// .resource_pattern("https://d111111abcdef8.cloudfront.net/videos/*")
|
89 + | /// // ...
|
90 + | /// ```
|
91 + | pub fn resource_pattern(mut self, pattern: impl Into<String>) -> Self {
|
92 + | self.resource_pattern = Some(pattern.into());
|
93 + | self
|
94 + | }
|
95 + |
|
96 + | /// Sets the CloudFront key pair ID.
|
97 + | pub fn key_pair_id(mut self, id: impl Into<String>) -> Self {
|
98 + | self.key_pair_id = Some(id.into());
|
99 + | self
|
100 + | }
|
101 + |
|
102 + | /// Sets the private key for signing.
|
103 + | pub fn private_key(mut self, key: PrivateKey) -> Self {
|
104 + | self.private_key = Some(key);
|
105 + | self
|
106 + | }
|
107 + |
|
108 + | /// Sets an absolute expiration time.
|
109 + | pub fn expires_at(mut self, time: DateTime) -> Self {
|
110 + | self.expiration = Some(Expiration::DateTime(time));
|
111 + | self
|
112 + | }
|
113 + |
|
114 + | /// Sets a relative expiration time from now.
|
115 + | pub fn expires_in(mut self, duration: Duration) -> Self {
|
116 + | self.expiration = Some(Expiration::Duration(duration));
|
117 + | self
|
118 + | }
|
119 + |
|
120 + | /// Sets an activation time (not-before date) for custom policy.
|
121 + | pub fn active_at(mut self, time: DateTime) -> Self {
|
122 + | self.active_date = Some(time);
|
123 + | self
|
124 + | }
|
125 + |
|
126 + | /// Sets an IP range restriction (CIDR notation) for custom policy.
|
127 + | pub fn ip_range(mut self, cidr: impl Into<String>) -> Self {
|
128 + | self.ip_range = Some(cidr.into());
|
129 + | self
|
130 + | }
|
131 + |
|
132 + | /// Builds the signing request.
|
133 + | pub fn build(self) -> Result<SigningRequest, SigningError> {
|
134 + | let resource_url = self
|
135 + | .resource_url
|
136 + | .ok_or_else(|| SigningError::invalid_input("resource_url is required"))?;
|
137 + |
|
138 + | let key_pair_id = self
|
139 + | .key_pair_id
|
140 + | .ok_or_else(|| SigningError::invalid_input("key_pair_id is required"))?;
|
141 + |
|
142 + | let private_key = self
|
143 + | .private_key
|
144 + | .ok_or_else(|| SigningError::invalid_input("private_key is required"))?;
|
145 + |
|
146 + | let expiration = self.expiration.ok_or_else(|| {
|
147 + | SigningError::invalid_input("expiration is required (use expires_at or expires_in)")
|
148 + | })?;
|
149 + |
|
150 + | let expiration = match expiration {
|
151 + | Expiration::DateTime(dt) => dt,
|
152 + | Expiration::Duration(dur) => {
|
153 + | let time_source = self.time_source.unwrap_or_default();
|
154 + | let now = DateTime::from(time_source.now());
|
155 + | DateTime::from_secs(now.secs() + dur.as_secs() as i64)
|
156 + | }
|
157 + | };
|
158 + |
|
159 + | // Validate that activeDate is before expirationDate
|
160 + | if let Some(active) = self.active_date {
|
161 + | if active.secs() >= expiration.secs() {
|
162 + | return Err(SigningError::invalid_input(
|
163 + | "active_at must be before expiration",
|
164 + | ));
|
165 + | }
|
166 + | }
|
167 + |
|
168 + | Ok(SigningRequest {
|
169 + | resource_url,
|
170 + | resource_pattern: self.resource_pattern,
|
171 + | key_pair_id,
|
172 + | private_key,
|
173 + | expiration,
|
174 + | active_date: self.active_date,
|
175 + | ip_range: self.ip_range,
|
176 + | })
|
177 + | }
|
178 + | }
|
179 + |
|
180 + | /// A signed CloudFront URL.
|
181 + | #[derive(Debug, Clone)]
|
182 + | pub struct SignedUrl {
|
183 + | url: url::Url,
|
184 + | }
|
185 + |
|
186 + | impl SignedUrl {
|
187 + | pub(crate) fn new(url: String) -> Result<Self, SigningError> {
|
188 + | let url = url::Url::parse(&url).map_err(|e| {
|
189 + | SigningError::new(
|
190 + | ErrorKind::InvalidInput,
|
191 + | Some(Box::new(e)),
|
192 + | Some("failed to parse URL".into()),
|
193 + | )
|
194 + | })?;
|
195 + | Ok(Self { url })
|
196 + | }
|
197 + |
|
198 + | /// Returns the complete signed URL as a string.
|
199 + | pub fn as_str(&self) -> &str {
|
200 + | self.url.as_str()
|
201 + | }
|
202 + |
|
203 + | /// Returns a reference to the parsed URL.
|
204 + | pub fn as_url(&self) -> &url::Url {
|
205 + | &self.url
|
206 + | }
|
207 + |
|
208 + | /// Consumes self and returns the parsed URL.
|
209 + | pub fn into_url(self) -> url::Url {
|
210 + | self.url
|
211 + | }
|
212 + | }
|
213 + |
|
214 + | impl fmt::Display for SignedUrl {
|
215 + | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
216 + | write!(f, "{}", self.url)
|
217 + | }
|
218 + | }
|
219 + |
|
220 + | impl AsRef<str> for SignedUrl {
|
221 + | fn as_ref(&self) -> &str {
|
222 + | self.url.as_str()
|
223 + | }
|
224 + | }
|
225 + |
|
226 + | impl AsRef<url::Url> for SignedUrl {
|
227 + | fn as_ref(&self) -> &url::Url {
|
228 + | &self.url
|
229 + | }
|
230 + | }
|
231 + |
|
232 + | #[cfg(feature = "http-1x")]
|
233 + | impl TryFrom<SignedUrl> for http::Request<()> {
|
234 + | type Error = http::Error;
|
235 + |
|
236 + | fn try_from(signed_url: SignedUrl) -> Result<Self, Self::Error> {
|
237 + | http::Request::builder()
|
238 + | .uri(signed_url.url.as_str())
|
239 + | .body(())
|
240 + | }
|
241 + | }
|
242 + |
|
243 + | #[cfg(feature = "http-1x")]
|
244 + | impl TryFrom<&SignedUrl> for http::Request<()> {
|
245 + | type Error = http::Error;
|
246 + |
|
247 + | fn try_from(signed_url: &SignedUrl) -> Result<Self, Self::Error> {
|
248 + | http::Request::builder()
|
249 + | .uri(signed_url.url.as_str())
|
250 + | .body(())
|
251 + | }
|
252 + | }
|
253 + |
|
254 + | /// Signed cookies for CloudFront.
|
255 + | #[derive(Debug, Clone)]
|
256 + | pub struct SignedCookies {
|
257 + | cookies: Vec<(Cow<'static, str>, String)>,
|
258 + | }
|
259 + |
|
260 + | impl SignedCookies {
|
261 + | pub(crate) fn new(cookies: Vec<(Cow<'static, str>, String)>) -> Self {
|
262 + | Self { cookies }
|
263 + | }
|
264 + |
|
265 + | /// Returns all cookies as name-value pairs.
|
266 + | pub fn cookies(&self) -> &[(Cow<'static, str>, String)] {
|
267 + | &self.cookies
|
268 + | }
|
269 + |
|
270 + | /// Gets a specific cookie value by name.
|
271 + | pub fn get(&self, name: &str) -> Option<&str> {
|
272 + | self.cookies
|
273 + | .iter()
|
274 + | .find(|(n, _)| n == name)
|
275 + | .map(|(_, v)| v.as_str())
|
276 + | }
|
277 + |
|
278 + | /// Returns an iterator over cookies.
|
279 + | pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
|
280 + | self.cookies.iter().map(|(n, v)| (n.as_ref(), v.as_str()))
|
281 + | }
|
282 + | }
|
283 + |
|
284 + | // Internal signing implementation
|
285 + | impl SigningRequest {
|
286 + | /// Returns true if this request should use a canned policy.
|
287 + | /// Canned policy is used only when there's no resource_pattern, active_date, or ip_range.
|
288 + | fn use_canned_policy(&self) -> bool {
|
289 + | self.resource_pattern.is_none() && self.active_date.is_none() && self.ip_range.is_none()
|
290 + | }
|
291 + |
|
292 + | pub(crate) fn sign_url(&self) -> Result<SignedUrl, SigningError> {
|
293 + | let policy = self.build_policy()?;
|
294 + | let policy_json = policy.to_json();
|
295 + | let signature = self.private_key.sign(policy_json.as_bytes())?;
|
296 + | let signature_b64 = cloudfront_base64(&signature);
|
297 + |
|
298 + | let separator = if self.resource_url.contains('?') {
|
299 + | "&"
|
300 + | } else {
|
301 + | "?"
|
302 + | };
|
303 + |
|
304 + | let signed_url = if self.use_canned_policy() {
|
305 + | format!(
|
306 + | "{}{}Expires={}&Signature={}&Key-Pair-Id={}",
|
307 + | self.resource_url,
|
308 + | separator,
|
309 + | self.expiration.secs(),
|
310 + | signature_b64,
|
311 + | self.key_pair_id
|
312 + | )
|
313 + | } else {
|
314 + | let policy_b64 = policy.to_cloudfront_base64();
|
315 + | format!(
|
316 + | "{}{}Policy={}&Signature={}&Key-Pair-Id={}",
|
317 + | self.resource_url, separator, policy_b64, signature_b64, self.key_pair_id
|
318 + | )
|
319 + | };
|
320 + |
|
321 + | SignedUrl::new(signed_url)
|
322 + | }
|
323 + |
|
324 + | pub(crate) fn sign_cookies(&self) -> Result<SignedCookies, SigningError> {
|
325 + | let policy = self.build_policy()?;
|
326 + | let policy_json = policy.to_json();
|
327 + | let signature = self.private_key.sign(policy_json.as_bytes())?;
|
328 + | let signature_b64 = cloudfront_base64(&signature);
|
329 + |
|
330 + | let cookies = if self.use_canned_policy() {
|
331 + | vec![
|
332 + | (
|
333 + | Cow::Borrowed(COOKIE_EXPIRES),
|
334 + | self.expiration.secs().to_string(),
|
335 + | ),
|
336 + | (Cow::Borrowed(COOKIE_SIGNATURE), signature_b64),
|
337 + | (Cow::Borrowed(COOKIE_KEY_PAIR_ID), self.key_pair_id.clone()),
|
338 + | ]
|
339 + | } else {
|
340 + | let policy_b64 = policy.to_cloudfront_base64();
|
341 + | vec![
|
342 + | (Cow::Borrowed(COOKIE_POLICY), policy_b64),
|
343 + | (Cow::Borrowed(COOKIE_SIGNATURE), signature_b64),
|
344 + | (Cow::Borrowed(COOKIE_KEY_PAIR_ID), self.key_pair_id.clone()),
|
345 + | ]
|
346 + | };
|
347 + |
|
348 + | Ok(SignedCookies::new(cookies))
|
349 + | }
|
350 + |
|
351 + | fn build_policy(&self) -> Result<Policy, SigningError> {
|
352 + | // Use resource_pattern for policy if set, otherwise use resource_url
|
353 + | let policy_resource = self.resource_pattern.as_ref().unwrap_or(&self.resource_url);
|
354 + |
|
355 + | let mut builder = Policy::builder()
|
356 + | .resource(policy_resource)
|
357 + | .expires_at(self.expiration);
|
358 + |
|
359 + | if let Some(active) = self.active_date {
|
360 + | builder = builder.starts_at(active);
|
361 + | }
|
362 + | if let Some(ref ip) = self.ip_range {
|
363 + | builder = builder.ip_range(ip);
|
364 + | }
|
365 + |
|
366 + | builder.build()
|
367 + | }
|
368 + | }
|
369 + |
|
370 + | #[cfg(test)]
|
371 + | mod tests {
|
372 + | use super::*;
|
373 + |
|
374 + | const TEST_RSA_KEY: &[u8] = b"-----BEGIN RSA PRIVATE KEY-----
|
375 + | MIIBPAIBAAJBANW8WjQksUoX/7nwOfRDNt1XQpLCueHoXSt91MASMOSAqpbzZvXO
|
376 + | g2hW2gCFUIFUPCByMXPoeRe6iUZ5JtjepssCAwEAAQJBALR7ybwQY/lKTLKJrZab
|
377 + | D4BXCCt/7ZFbMxnftsC+W7UHef4S4qFW8oOOLeYfmyGZK1h44rXf2AIp4PndKUID
|
378 + | 1zECIQD1suunYw5U22Pa0+2dFThp1VMXdVbPuf/5k3HT2/hSeQIhAN6yX0aT/N6G
|
379 + | gb1XlBKw6GQvhcM0fXmP+bVXV+RtzFJjAiAP+2Z2yeu5u1egeV6gdCvqPnUcNobC
|
380 + | FmA/NMcXt9xMSQIhALEMMJEFAInNeAIXSYKeoPNdkMPDzGnD3CueuCLEZCevAiEA
|
381 + | j+KnJ7pJkTvOzFwE8RfNLli9jf6/OhyYaLL4et7Ng5k=
|
382 + | -----END RSA PRIVATE KEY-----";
|
383 + |
|
384 + | #[test]
|
385 + | fn test_sign_url_canned_policy() {
|
386 + | let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap();
|
387 + | let request = SigningRequest::builder()
|
388 + | .resource_url("https://d111111abcdef8.cloudfront.net/image.jpg")
|
389 + | .key_pair_id("APKAEXAMPLE")
|
390 + | .private_key(key)
|
391 + | .expires_at(DateTime::from_secs(1767290400))
|
392 + | .build()
|
393 + | .unwrap();
|
394 + |
|
395 + | let signed_url = request.sign_url().unwrap();
|
396 + | let url = signed_url.as_str();
|
397 + |
|
398 + | assert!(url.contains("Expires=1767290400"));
|
399 + | assert!(url.contains("Signature="));
|
400 + | assert!(url.contains("Key-Pair-Id=APKAEXAMPLE"));
|
401 + | assert!(!url.contains("Policy="));
|
402 + | }
|
403 + |
|
404 + | #[test]
|
405 + | fn test_sign_url_custom_policy() {
|
406 + | let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap();
|
407 + | let request = SigningRequest::builder()
|
408 + | .resource_url("https://d111111abcdef8.cloudfront.net/*")
|
409 + | .key_pair_id("APKAEXAMPLE")
|
410 + | .private_key(key)
|
411 + | .expires_at(DateTime::from_secs(1767290400))
|
412 + | .active_at(DateTime::from_secs(1767200000))
|
413 + | .build()
|
414 + | .unwrap();
|
415 + |
|
416 + | let signed_url = request.sign_url().unwrap();
|
417 + | let url = signed_url.as_str();
|
418 + |
|
419 + | assert!(url.contains("Policy="));
|
420 + | assert!(url.contains("Signature="));
|
421 + | assert!(url.contains("Key-Pair-Id=APKAEXAMPLE"));
|
422 + | assert!(!url.contains("Expires="));
|
423 + | }
|
424 + |
|
425 + | #[test]
|
426 + | fn test_sign_url_with_existing_params() {
|
427 + | let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap();
|
428 + | let request = SigningRequest::builder()
|
429 + | .resource_url("https://d111111abcdef8.cloudfront.net/image.jpg?size=large")
|
430 + | .key_pair_id("APKAEXAMPLE")
|
431 + | .private_key(key)
|
432 + | .expires_at(DateTime::from_secs(1767290400))
|
433 + | .build()
|
434 + | .unwrap();
|
435 + |
|
436 + | let signed_url = request.sign_url().unwrap();
|
437 + | let url = signed_url.as_str();
|
438 + |
|
439 + | assert!(url.contains("size=large"));
|
440 + | assert!(url.contains("&Expires="));
|
441 + | }
|
442 + |
|
443 + | #[test]
|
444 + | fn test_sign_cookies_canned_policy() {
|
445 + | let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap();
|
446 + | let request = SigningRequest::builder()
|
447 + | .resource_url("https://d111111abcdef8.cloudfront.net/*")
|
448 + | .key_pair_id("APKAEXAMPLE")
|
449 + | .private_key(key)
|
450 + | .expires_at(DateTime::from_secs(1767290400))
|
451 + | .build()
|
452 + | .unwrap();
|
453 + |
|
454 + | let cookies = request.sign_cookies().unwrap();
|
455 + |
|
456 + | assert_eq!(cookies.get("CloudFront-Expires"), Some("1767290400"));
|
457 + | assert!(cookies.get("CloudFront-Signature").is_some());
|
458 + | assert_eq!(cookies.get("CloudFront-Key-Pair-Id"), Some("APKAEXAMPLE"));
|
459 + | assert!(cookies.get("CloudFront-Policy").is_none());
|
460 + | }
|
461 + |
|
462 + | #[test]
|
463 + | fn test_sign_cookies_custom_policy() {
|
464 + | let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap();
|
465 + | let request = SigningRequest::builder()
|
466 + | .resource_url("https://d111111abcdef8.cloudfront.net/*")
|
467 + | .key_pair_id("APKAEXAMPLE")
|
468 + | .private_key(key)
|
469 + | .expires_at(DateTime::from_secs(1767290400))
|
470 + | .ip_range("192.0.2.0/24")
|
471 + | .build()
|
472 + | .unwrap();
|
473 + |
|
474 + | let cookies = request.sign_cookies().unwrap();
|
475 + |
|
476 + | assert!(cookies.get("CloudFront-Policy").is_some());
|
477 + | assert!(cookies.get("CloudFront-Signature").is_some());
|
478 + | assert_eq!(cookies.get("CloudFront-Key-Pair-Id"), Some("APKAEXAMPLE"));
|
479 + | assert!(cookies.get("CloudFront-Expires").is_none());
|
480 + | }
|
481 + |
|
482 + | #[test]
|
483 + | fn test_sign_url_with_resource_pattern() {
|
484 + | let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap();
|
485 + | let request = SigningRequest::builder()
|
486 + | .resource_url("https://d111111abcdef8.cloudfront.net/videos/intro.mp4")
|
487 + | .resource_pattern("https://d111111abcdef8.cloudfront.net/videos/*")
|
488 + | .key_pair_id("APKAEXAMPLE")
|
489 + | .private_key(key)
|
490 + | .expires_at(DateTime::from_secs(1767290400))
|
491 + | .build()
|
492 + | .unwrap();
|
493 + |
|
494 + | let signed_url = request.sign_url().unwrap();
|
495 + | let url = signed_url.as_str();
|
496 + |
|
497 + | // Should use custom policy format (Policy param) because resource_pattern is set
|
498 + | assert!(url.contains("Policy="));
|
499 + | assert!(url.contains("Signature="));
|
500 + | assert!(url.contains("Key-Pair-Id=APKAEXAMPLE"));
|
501 + | assert!(!url.contains("Expires="));
|
502 + | // The actual URL should be the resource_url, not the pattern
|
503 + | assert!(url.starts_with("https://d111111abcdef8.cloudfront.net/videos/intro.mp4?"));
|
504 + | }
|
505 + |
|
506 + | #[test]
|
507 + | fn test_sign_cookies_with_resource_pattern() {
|
508 + | let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap();
|
509 + | let request = SigningRequest::builder()
|
510 + | .resource_url("https://d111111abcdef8.cloudfront.net/videos/*")
|
511 + | .resource_pattern("https://d111111abcdef8.cloudfront.net/videos/*")
|
512 + | .key_pair_id("APKAEXAMPLE")
|
513 + | .private_key(key)
|
514 + | .expires_at(DateTime::from_secs(1767290400))
|
515 + | .build()
|
516 + | .unwrap();
|
517 + |
|
518 + | let cookies = request.sign_cookies().unwrap();
|
519 + |
|
520 + | // Should use custom policy format because resource_pattern is set
|
521 + | assert!(cookies.get("CloudFront-Policy").is_some());
|
522 + | assert!(cookies.get("CloudFront-Signature").is_some());
|
523 + | assert_eq!(cookies.get("CloudFront-Key-Pair-Id"), Some("APKAEXAMPLE"));
|
524 + | assert!(cookies.get("CloudFront-Expires").is_none());
|
525 + | }
|
526 + |
|
527 + | #[test]
|
528 + | fn test_signed_url_accessors() {
|
529 + | let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap();
|
530 + | let request = SigningRequest::builder()
|
531 + | .resource_url("https://d111111abcdef8.cloudfront.net/image.jpg")
|
532 + | .key_pair_id("APKAEXAMPLE")
|
533 + | .private_key(key)
|
534 + | .expires_at(DateTime::from_secs(1767290400))
|
535 + | .build()
|
536 + | .unwrap();
|
537 + |
|
538 + | let signed_url = request.sign_url().unwrap();
|
539 + |
|
540 + | // Test as_str()
|
541 + | let url_str = signed_url.as_str();
|
542 + | assert!(url_str.starts_with("https://d111111abcdef8.cloudfront.net/image.jpg"));
|
543 + | assert!(url_str.contains("Expires="));
|
544 + |
|
545 + | // Test as_url()
|
546 + | let url_ref = signed_url.as_url();
|
547 + | assert_eq!(url_ref.scheme(), "https");
|
548 + | assert_eq!(url_ref.host_str(), Some("d111111abcdef8.cloudfront.net"));
|
549 + | assert_eq!(url_ref.path(), "/image.jpg");
|
550 + |
|
551 + | // Test Display
|
552 + | let displayed = format!("{}", signed_url);
|
553 + | assert_eq!(displayed, url_str);
|
554 + |
|
555 + | // Test AsRef<str>
|
556 + | let as_ref_str: &str = signed_url.as_ref();
|
557 + | assert_eq!(as_ref_str, url_str);
|
558 + |
|
559 + | // Test AsRef<url::Url>
|
560 + | let as_ref_url: &url::Url = signed_url.as_ref();
|
561 + | assert_eq!(as_ref_url, url_ref);
|
562 + |
|
563 + | // Test into_url()
|
564 + | let url = signed_url.into_url();
|
565 + | assert_eq!(url.scheme(), "https");
|
566 + | assert_eq!(url.host_str(), Some("d111111abcdef8.cloudfront.net"));
|
567 + | }
|
568 + |
|
569 + | #[cfg(feature = "http-1x")]
|
570 + | #[test]
|
571 + | fn test_signed_url_to_http_request() {
|
572 + | let key = PrivateKey::from_pem(TEST_RSA_KEY).unwrap();
|
573 + | let request = SigningRequest::builder()
|
574 + | .resource_url("https://d111111abcdef8.cloudfront.net/image.jpg")
|
575 + | .key_pair_id("APKAEXAMPLE")
|
576 + | .private_key(key)
|
577 + | .expires_at(DateTime::from_secs(1767290400))
|
578 + | .build()
|
579 + | .unwrap();
|
580 + |
|
581 + | let signed_url = request.sign_url().unwrap();
|
582 + |
|
583 + | // Test TryFrom<SignedUrl>
|
584 + | let http_req: http::Request<()> = signed_url.clone().try_into().unwrap();
|
585 + | assert_eq!(http_req.method(), http::Method::GET);
|
586 + | assert!(http_req.uri().to_string().contains("Expires="));
|
587 + |
|
588 + | // Test TryFrom<&SignedUrl>
|
589 + | let http_req: http::Request<()> = (&signed_url).try_into().unwrap();
|
590 + | assert_eq!(http_req.method(), http::Method::GET);
|
591 + | assert!(http_req.uri().to_string().contains("Expires="));
|
592 + | }
|
593 + | }
|