1use crate::{
2 Asset, AssetCertificationError, AssetCertificationResult, AssetConfig, AssetEncoding,
3 AssetFallbackConfig, AssetMap, AssetRedirectKind, CertifiedAssetResponse,
4 NormalizedAssetConfig, RequestKey,
5};
6use ic_http_certification::{
7 utils::add_v2_certificate_header, DefaultCelBuilder, DefaultResponseCertification, Hash,
8 HttpCertification, HttpCertificationPath, HttpCertificationTree, HttpCertificationTreeEntry,
9 HttpRequest, HttpResponse, StatusCode, CERTIFICATE_EXPRESSION_HEADER_NAME,
10};
11use std::{borrow::Cow, cell::RefCell, cmp, collections::HashMap, rc::Rc};
12
13#[derive(Debug)]
123pub struct AssetRouter<'content> {
124 tree: Rc<RefCell<HttpCertificationTree>>,
125 responses: HashMap<RequestKey, CertifiedAssetResponse<'content>>,
126 fallback_responses: HashMap<RequestKey, CertifiedAssetResponse<'content>>,
127}
128
129#[derive(Debug, PartialEq)]
130struct RangeRequestValues {
131 pub range_begin: usize,
132 #[allow(unused)]
133 pub range_end: Option<usize>,
134}
135
136pub const ASSET_CHUNK_SIZE: usize = 2_000_000;
138
139fn encoding_str(maybe_encoding: Option<AssetEncoding>) -> Option<String> {
140 maybe_encoding.map(|enc| enc.to_string())
141}
142
143fn parse_range_header_str(range_str: &str) -> Result<RangeRequestValues, String> {
144 let str_value = range_str.trim();
146 if !str_value.starts_with("bytes=") {
147 return Err(format!("Invalid Range header '{range_str}'").to_string());
148 }
149 let str_value = str_value.trim_start_matches("bytes=");
150 let range_header_parts = str_value.split('-').collect::<Vec<_>>();
151 if range_header_parts.is_empty() || range_header_parts.len() != 2 {
152 return Err(format!("Invalid Range header '{range_str}'").to_string());
153 }
154
155 let range_begin = range_header_parts[0]
156 .parse::<usize>()
157 .map_err(|e| format!("Malformed range_begin in Range header '{range_str}': {e}"))?;
158 let range_end = match range_header_parts[1] {
159 "" => None,
160 _ => Some(
161 range_header_parts[1]
162 .parse::<usize>()
163 .map_err(|e| format!("Malformed range_end in Range header '{range_str}': {e}"))?,
164 ),
165 };
166
167 if range_begin > range_end.unwrap_or(usize::MAX) {
168 return Err(format!("Invalid values in Range header '{range_str}'").to_string());
169 }
170 Ok(RangeRequestValues {
171 range_begin,
172 range_end,
173 })
174}
175
176impl<'content> AssetRouter<'content> {
177 pub fn new() -> Self {
179 AssetRouter {
180 tree: Default::default(),
181 responses: HashMap::new(),
182 fallback_responses: HashMap::new(),
183 }
184 }
185
186 pub fn with_tree(tree: Rc<RefCell<HttpCertificationTree>>) -> Self {
190 AssetRouter {
191 tree,
192 responses: HashMap::new(),
193 fallback_responses: HashMap::new(),
194 }
195 }
196
197 fn maybe_get_range_begin(request: &HttpRequest) -> AssetCertificationResult<Option<usize>> {
198 if let Some(range_str) = Self::get_range_header(request) {
199 parse_range_header_str(range_str)
200 .map(|e| Some(e.range_begin))
201 .map_err(AssetCertificationError::RequestError)
202 } else {
203 Ok(None)
204 }
205 }
206
207 pub fn serve_asset(
227 &self,
228 data_certificate: &[u8],
229 request: &HttpRequest,
230 ) -> AssetCertificationResult<HttpResponse<'content>> {
231 let preferred_encodings = self.get_preferred_encodings(request);
232 let request_url = request.get_path()?;
233 let maybe_range_begin = Self::maybe_get_range_begin(request)?;
234 let mut cert_response = self
235 .get_asset_for_request(&request_url, preferred_encodings, maybe_range_begin)
236 .cloned()?;
237 let witness = self
238 .tree
239 .borrow()
240 .witness(&cert_response.tree_entry, &request_url)?;
241 let expr_path = cert_response.tree_entry.path.to_expr_path();
242 add_v2_certificate_header(
243 data_certificate,
244 &mut cert_response.response,
245 &witness,
246 &expr_path,
247 );
248 Ok(cert_response.response.clone())
249 }
250
251 pub fn get_assets(&self) -> &impl AssetMap<'content> {
258 &self.responses
259 }
260
261 pub fn get_fallback_assets(&self) -> &impl AssetMap<'content> {
268 &self.fallback_responses
269 }
270
271 pub fn certify_assets<'path>(
283 &mut self,
284 assets: impl IntoIterator<Item = Asset<'content, 'path>>,
285 asset_configs: impl IntoIterator<Item = AssetConfig>,
286 ) -> AssetCertificationResult {
287 let asset_configs: Vec<NormalizedAssetConfig> = asset_configs
288 .into_iter()
289 .map(TryInto::try_into)
290 .collect::<AssetCertificationResult<_>>()?;
291
292 let asset_map = assets
293 .into_iter()
294 .map(|asset| (asset.path.clone(), asset))
295 .collect::<HashMap<_, _>>();
296
297 for asset in asset_map.values() {
298 let asset_config = asset_configs.iter().find(|e| e.matches_asset(asset));
299 for (encoding, postfix) in asset_config
300 .map(|e| match e {
301 NormalizedAssetConfig::File { encodings, .. } => encodings.clone(),
302 NormalizedAssetConfig::Pattern { encodings, .. } => encodings.clone(),
303 _ => vec![],
304 })
305 .unwrap_or_default()
306 {
307 let encoded_asset_path = format!("{}{}", asset.path, postfix);
308 let encoded_asset = asset_map.get(encoded_asset_path.as_str()).cloned();
309 if let Some(mut encoded_asset) = encoded_asset {
310 encoded_asset.url.clone_from(&asset.url);
311
312 self.certify_asset_impl(encoded_asset, asset_config, Some(encoding))?;
313 }
314 }
315
316 self.certify_asset_impl(asset.clone(), asset_config, None)?;
317 }
318
319 for asset_config in asset_configs {
320 if let NormalizedAssetConfig::Redirect {
321 from,
322 to,
323 kind,
324 headers,
325 } = asset_config
326 {
327 self.insert_redirect(from, to, kind, headers)?;
328 }
329 }
330
331 Ok(())
332 }
333
334 pub fn delete_assets<'path>(
343 &mut self,
344 assets: impl IntoIterator<Item = Asset<'content, 'path>>,
345 asset_configs: impl IntoIterator<Item = AssetConfig>,
346 ) -> AssetCertificationResult {
347 let asset_configs: Vec<NormalizedAssetConfig> = asset_configs
348 .into_iter()
349 .map(TryInto::try_into)
350 .collect::<AssetCertificationResult<_>>()?;
351
352 let asset_map = assets
353 .into_iter()
354 .map(|asset| (asset.path.clone(), asset))
355 .collect::<HashMap<_, _>>();
356
357 for asset in asset_map.values() {
358 let asset_config = asset_configs.iter().find(|e| e.matches_asset(asset));
359
360 for (encoding, postfix) in asset_config
361 .map(|e| match e {
362 NormalizedAssetConfig::File { encodings, .. } => encodings.clone(),
363 NormalizedAssetConfig::Pattern { encodings, .. } => encodings.clone(),
364 _ => vec![],
365 })
366 .unwrap_or_default()
367 {
368 let encoded_asset_path = format!("{}{}", asset.path, postfix);
369 let encoded_asset = asset_map.get(encoded_asset_path.as_str()).cloned();
370
371 if let Some(mut encoded_asset) = encoded_asset {
372 encoded_asset.url.clone_from(&asset.url);
373
374 self.delete_asset_impl(encoded_asset, asset_config, Some(encoding))?;
375 }
376 }
377
378 self.delete_asset_impl(asset.clone(), asset_config, None)?;
379 }
380
381 for asset_config in asset_configs {
382 if let NormalizedAssetConfig::Redirect {
383 from,
384 to,
385 kind,
386 headers,
387 } = asset_config
388 {
389 self.delete_redirect(from, to, kind, headers)?;
390 }
391 }
392
393 Ok(())
394 }
395
396 pub fn delete_assets_by_path<'path>(
411 &mut self,
412 asset_paths: impl IntoIterator<Item = &'path str>,
413 ) {
414 for asset_path in asset_paths {
415 self.responses
416 .remove(&RequestKey::new(asset_path, None, None));
417 self.tree
418 .borrow_mut()
419 .delete_by_path(&HttpCertificationPath::exact(asset_path));
420 }
421 }
422
423 pub fn delete_fallback_assets_by_path<'path>(
431 &mut self,
432 asset_paths: impl IntoIterator<Item = &'path str>,
433 ) {
434 for asset_path in asset_paths {
435 self.fallback_responses
436 .remove(&RequestKey::new(asset_path, None, None));
437 self.tree
438 .borrow_mut()
439 .delete_by_path(&HttpCertificationPath::wildcard(asset_path));
440 }
441 }
442
443 pub fn delete_all_assets(&mut self) {
448 self.responses.clear();
449 self.fallback_responses.clear();
450 self.tree.borrow_mut().clear();
451 }
452
453 pub fn root_hash(&self) -> Hash {
456 self.tree.borrow().root_hash()
457 }
458
459 fn get_asset_for_request<'a>(
460 &self,
461 req_path: &'a str,
462 preferred_encodings: Vec<&'a str>,
463 maybe_range_begin: Option<usize>,
464 ) -> AssetCertificationResult<&CertifiedAssetResponse<'content>> {
465 if let Some(response) =
466 self.get_encoded_asset(&preferred_encodings, req_path, maybe_range_begin)
467 {
468 return Ok(response);
469 }
470
471 if let Some(response) =
472 self.responses
473 .get(&RequestKey::new(req_path, None, maybe_range_begin))
474 {
475 if response.response.body().len() > ASSET_CHUNK_SIZE {
476 if let Some(first_chunk_response) =
477 self.responses
478 .get(&RequestKey::new(req_path, None, Some(0)))
479 {
480 return Ok(first_chunk_response);
481 }
482 } else {
483 return Ok(response);
484 }
485 }
486
487 let mut url_scopes = req_path.split('/').collect::<Vec<_>>();
488 url_scopes.pop();
489
490 while !url_scopes.is_empty() {
491 let mut scope = url_scopes.join("/");
492 scope.push('/');
493
494 if let Some(response) = self.get_encoded_fallback_asset(&preferred_encodings, &scope) {
495 return Ok(response);
496 }
497
498 if let Some(response) = self
499 .fallback_responses
500 .get(&RequestKey::new(&scope, None, None))
501 {
502 return Ok(response);
503 }
504
505 scope.pop();
506
507 if let Some(response) = self.get_encoded_fallback_asset(&preferred_encodings, &scope) {
508 return Ok(response);
509 }
510
511 if let Some(response) = self
512 .fallback_responses
513 .get(&RequestKey::new(&scope, None, None))
514 {
515 return Ok(response);
516 }
517
518 url_scopes.pop();
519 }
520 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
521 request_url: req_path.to_string(),
522 })
523 }
524
525 fn certify_asset_impl<'path>(
526 &mut self,
527 asset: Asset<'content, 'path>,
528 asset_config: Option<&NormalizedAssetConfig>,
529 encoding: Option<AssetEncoding>,
530 ) -> AssetCertificationResult {
531 match asset_config {
532 Some(NormalizedAssetConfig::Pattern {
533 content_type,
534 headers,
535 ..
536 }) => {
537 self.insert_static_asset(asset, content_type.clone(), headers.clone(), encoding)?;
538 }
539 Some(NormalizedAssetConfig::File {
540 content_type,
541 headers,
542 fallback_for,
543 aliased_by,
544 ..
545 }) => {
546 self.insert_static_asset(
547 asset.clone(),
548 content_type.clone(),
549 headers.clone(),
550 encoding,
551 )?;
552
553 for fallback_for in fallback_for.iter() {
554 self.insert_fallback_asset(
555 asset.clone(),
556 content_type.clone(),
557 headers.clone(),
558 fallback_for.clone(),
559 encoding,
560 )?;
561 }
562
563 for aliased_by in aliased_by.iter() {
564 let mut aliased_asset = asset.clone();
565 aliased_asset.url = Cow::Owned(aliased_by.clone());
566
567 self.insert_static_asset(
568 aliased_asset,
569 content_type.clone(),
570 headers.clone(),
571 encoding,
572 )?;
573 }
574 }
575 _ => {
576 self.insert_static_asset(asset, None, vec![], encoding)?;
577 }
578 }
579
580 Ok(())
581 }
582
583 fn delete_asset_impl<'path>(
584 &mut self,
585 asset: Asset<'content, 'path>,
586 asset_config: Option<&NormalizedAssetConfig>,
587 encoding: Option<AssetEncoding>,
588 ) -> AssetCertificationResult {
589 match asset_config {
590 Some(NormalizedAssetConfig::Pattern {
591 content_type,
592 headers,
593 ..
594 }) => {
595 self.delete_static_asset(asset, content_type.clone(), headers.clone(), encoding)?;
596 }
597 Some(NormalizedAssetConfig::File {
598 content_type,
599 headers,
600 fallback_for,
601 aliased_by,
602 ..
603 }) => {
604 self.delete_static_asset(
605 asset.clone(),
606 content_type.clone(),
607 headers.clone(),
608 encoding,
609 )?;
610
611 for fallback_for in fallback_for.iter() {
612 self.delete_fallback_asset(
613 asset.clone(),
614 content_type.clone(),
615 headers.clone(),
616 fallback_for.clone(),
617 encoding,
618 )?;
619 }
620
621 for aliased_by in aliased_by.iter() {
622 let mut aliased_asset = asset.clone();
623 aliased_asset.url = Cow::Owned(aliased_by.clone());
624
625 self.delete_static_asset(
626 aliased_asset,
627 content_type.clone(),
628 headers.clone(),
629 encoding,
630 )?;
631 }
632 }
633 _ => {
634 self.delete_static_asset(asset, None, vec![], encoding)?;
635 }
636 }
637
638 Ok(())
639 }
640
641 fn insert_static_asset<'path>(
642 &mut self,
643 asset: Asset<'content, 'path>,
644 content_type: Option<String>,
645 additional_headers: Vec<(String, String)>,
646 encoding: Option<AssetEncoding>,
647 ) -> AssetCertificationResult<()> {
648 let asset_url = asset.url.to_string();
649 let total_length = asset.content.len();
650
651 if total_length > ASSET_CHUNK_SIZE {
652 let mut range_begin = 0;
653 while range_begin < asset.content.len() {
654 let response = Self::prepare_static_asset(
655 asset.clone(),
656 content_type.clone(),
657 additional_headers.clone(),
658 encoding,
659 Some(range_begin),
660 )?;
661 self.tree.borrow_mut().insert(&response.tree_entry);
662 self.responses.insert(
663 RequestKey::new(&asset_url, encoding_str(encoding), Some(range_begin)),
664 response,
665 );
666 range_begin += ASSET_CHUNK_SIZE;
667 }
668 }
669
670 let response =
671 Self::prepare_static_asset(asset, content_type, additional_headers, encoding, None)?;
672
673 self.tree.borrow_mut().insert(&response.tree_entry);
674 self.responses.insert(
675 RequestKey::new(&asset_url, encoding_str(encoding), None),
676 response,
677 );
678 Ok(())
679 }
680
681 fn delete_static_asset<'path>(
682 &mut self,
683 asset: Asset<'content, 'path>,
684 content_type: Option<String>,
685 additional_headers: Vec<(String, String)>,
686 encoding: Option<AssetEncoding>,
687 ) -> AssetCertificationResult<()> {
688 let asset_url = asset.url.to_string();
689 let response =
690 Self::prepare_static_asset(asset, content_type, additional_headers, encoding, None)?;
691
692 self.tree.borrow_mut().delete(&response.tree_entry);
693 self.responses
694 .remove(&RequestKey::new(&asset_url, encoding_str(encoding), None));
695
696 if response.response.body().len() > ASSET_CHUNK_SIZE {
697 let mut range_begin: usize = 0;
699 while range_begin < response.response.body().len() {
700 self.responses.remove(&RequestKey::new(
701 &asset_url,
702 encoding_str(encoding),
703 Some(range_begin),
704 ));
705 range_begin += ASSET_CHUNK_SIZE;
706 }
707 }
708
709 Ok(())
710 }
711
712 fn prepare_static_asset<'path>(
713 asset: Asset<'content, 'path>,
714 content_type: Option<String>,
715 additional_headers: Vec<(String, String)>,
716 encoding: Option<AssetEncoding>,
717 range_begin: Option<usize>,
718 ) -> AssetCertificationResult<CertifiedAssetResponse<'content>> {
719 let asset_url = asset.url.to_string();
720
721 let (response, certification) = Self::prepare_asset_response_and_certification(
722 asset,
723 additional_headers,
724 content_type,
725 encoding,
726 range_begin,
727 None,
728 )?;
729
730 let tree_entry =
731 HttpCertificationTreeEntry::new(HttpCertificationPath::exact(asset_url), certification);
732
733 Ok(CertifiedAssetResponse {
734 response,
735 tree_entry,
736 })
737 }
738
739 fn insert_fallback_asset<'path>(
740 &mut self,
741 asset: Asset<'content, 'path>,
742 content_type: Option<String>,
743 additional_headers: Vec<(String, String)>,
744 fallback_for: AssetFallbackConfig,
745 encoding: Option<AssetEncoding>,
746 ) -> AssetCertificationResult<()> {
747 let response = Self::prepare_fallback_asset(
748 asset,
749 additional_headers,
750 content_type,
751 fallback_for.clone(),
752 encoding,
753 )?;
754
755 self.tree.borrow_mut().insert(&response.tree_entry);
756 self.fallback_responses.insert(
757 RequestKey::new(&fallback_for.scope, encoding_str(encoding), None),
758 response,
759 );
760 Ok(())
761 }
762
763 fn delete_fallback_asset<'path>(
764 &mut self,
765 asset: Asset<'content, 'path>,
766 content_type: Option<String>,
767 additional_headers: Vec<(String, String)>,
768 fallback_for: AssetFallbackConfig,
769 encoding: Option<AssetEncoding>,
770 ) -> AssetCertificationResult<()> {
771 let response = Self::prepare_fallback_asset(
772 asset,
773 additional_headers,
774 content_type,
775 fallback_for.clone(),
776 encoding,
777 )?;
778
779 self.tree.borrow_mut().delete(&response.tree_entry);
780 self.fallback_responses.remove(&RequestKey::new(
781 &fallback_for.scope,
782 encoding_str(encoding),
783 None,
784 ));
785 Ok(())
786 }
787
788 fn prepare_fallback_asset<'path>(
789 asset: Asset<'content, 'path>,
790 additional_headers: Vec<(String, String)>,
791 content_type: Option<String>,
792 fallback_for: AssetFallbackConfig,
793 encoding: Option<AssetEncoding>,
794 ) -> AssetCertificationResult<CertifiedAssetResponse<'content>> {
795 let (response, certification) = Self::prepare_asset_response_and_certification(
796 asset,
797 additional_headers,
798 content_type,
799 encoding,
800 None,
801 fallback_for.status_code,
802 )?;
803
804 let tree_entry = HttpCertificationTreeEntry::new(
805 HttpCertificationPath::wildcard(fallback_for.scope.clone()),
806 certification,
807 );
808
809 Ok(CertifiedAssetResponse {
810 response,
811 tree_entry,
812 })
813 }
814
815 fn insert_redirect(
816 &mut self,
817 from: String,
818 to: String,
819 kind: AssetRedirectKind,
820 additional_headers: Vec<(String, String)>,
821 ) -> AssetCertificationResult<()> {
822 let response = Self::prepare_redirect(from.clone(), to, kind, additional_headers)?;
823
824 self.tree.borrow_mut().insert(&response.tree_entry);
825
826 self.responses
827 .insert(RequestKey::new(&from, None, None), response);
828
829 Ok(())
830 }
831
832 fn delete_redirect(
833 &mut self,
834 from: String,
835 to: String,
836 kind: AssetRedirectKind,
837 addtional_headers: Vec<(String, String)>,
838 ) -> AssetCertificationResult<()> {
839 let response = Self::prepare_redirect(from.clone(), to, kind, addtional_headers)?;
840
841 self.tree.borrow_mut().delete(&response.tree_entry);
842 self.responses.remove(&RequestKey::new(&from, None, None));
843
844 Ok(())
845 }
846
847 fn prepare_redirect(
848 from: String,
849 to: String,
850 kind: AssetRedirectKind,
851 addtional_headers: Vec<(String, String)>,
852 ) -> AssetCertificationResult<CertifiedAssetResponse<'content>> {
853 let status_code = match kind {
854 AssetRedirectKind::Permanent => StatusCode::MOVED_PERMANENTLY,
855 AssetRedirectKind::Temporary => StatusCode::TEMPORARY_REDIRECT,
856 };
857
858 let mut headers = vec![("location".to_string(), to)];
859 headers.extend(addtional_headers);
860
861 let (response, certification) = Self::prepare_response_and_certification(
862 from.clone(),
863 status_code,
864 Cow::Owned(vec![]),
865 headers,
866 vec![],
867 )?;
868
869 Ok(CertifiedAssetResponse {
870 response,
871 tree_entry: HttpCertificationTreeEntry::new(
872 HttpCertificationPath::exact(from),
873 certification,
874 ),
875 })
876 }
877
878 fn prepare_asset_response_and_certification<'path>(
879 asset: Asset<'content, 'path>,
880 additional_headers: Vec<(String, String)>,
881 content_type: Option<String>,
882 encoding: Option<AssetEncoding>,
883 range_begin: Option<usize>,
884 status_code: Option<StatusCode>,
885 ) -> AssetCertificationResult<(HttpResponse<'content>, HttpCertification)> {
886 let mut content = asset.content;
887 let mut status_code = status_code.unwrap_or(StatusCode::OK);
888 let mut headers = vec![];
889 headers.extend(additional_headers);
890
891 if let Some(content_type) = content_type {
892 headers.push(("content-type".to_string(), content_type));
893 }
894
895 if let Some(encoding) = encoding {
896 headers.push(("content-encoding".to_string(), encoding.to_string()));
897 }
898
899 let mut request_headers = vec![];
900 if let Some(range_begin) = range_begin {
901 let total_length = content.len();
902 let range_end = cmp::min(range_begin + ASSET_CHUNK_SIZE, total_length) - 1;
903 content = content[range_begin..(range_end + 1)].to_owned().into();
904 status_code = StatusCode::PARTIAL_CONTENT;
905 headers.push((
906 http::header::CONTENT_RANGE.to_string(),
907 format!("bytes {range_begin}-{range_end}/{total_length}"),
908 ));
909
910 if range_begin != 0 {
913 request_headers.push((
914 http::header::RANGE.to_string(),
915 format!("bytes={range_begin}-"),
916 ));
917 }
918 };
919
920 Self::prepare_response_and_certification(
921 asset.url.to_string(),
922 status_code,
923 content,
924 headers,
925 request_headers,
926 )
927 }
928
929 fn prepare_response_and_certification(
930 url: String,
931 status_code: StatusCode,
932 body: Cow<'content, [u8]>,
933 additional_response_headers: Vec<(String, String)>,
934 certified_request_headers: Vec<(String, String)>,
935 ) -> AssetCertificationResult<(HttpResponse<'content>, HttpCertification)> {
936 let mut headers = vec![("content-length".to_string(), body.len().to_string())];
937
938 headers.extend(additional_response_headers);
939 let cel_expr = DefaultCelBuilder::full_certification()
940 .with_request_headers(
941 certified_request_headers
942 .iter()
943 .map(|(s, _)| s.as_str())
944 .collect::<Vec<&str>>(),
945 )
946 .with_response_certification(DefaultResponseCertification::response_header_exclusions(
947 vec![],
948 ))
949 .build();
950 let cel_expr_str = cel_expr.to_string();
951 headers.push((CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(), cel_expr_str));
952
953 let request = HttpRequest::get(url)
954 .with_headers(certified_request_headers.clone())
955 .build();
956
957 let response = HttpResponse::builder()
958 .with_status_code(status_code)
959 .with_body(body)
960 .with_headers(headers)
961 .build();
962
963 let certification = HttpCertification::full(&cel_expr, &request, &response, None)?;
964
965 Ok((response, certification))
966 }
967
968 fn get_encoded_asset(
969 &self,
970 preferred_encodings: &[&str],
971 url: &str,
972 maybe_range_begin: Option<usize>,
973 ) -> Option<&CertifiedAssetResponse<'content>> {
974 for encoding in preferred_encodings {
975 if let Some(response) = self.responses.get(&RequestKey::new(
976 url,
977 Some(encoding.to_string()),
978 maybe_range_begin,
979 )) {
980 if response.response.body().len() > ASSET_CHUNK_SIZE {
981 if let Some(first_chunk_response) = self.responses.get(&RequestKey::new(
982 url,
983 Some(encoding.to_string()),
984 Some(0),
985 )) {
986 return Some(first_chunk_response);
987 } else {
988 return None;
989 }
990 } else {
991 return Some(response);
992 }
993 }
994 }
995
996 None
997 }
998
999 fn get_encoded_fallback_asset(
1000 &self,
1001 preferred_encodings: &[&str],
1002 scope: &str,
1003 ) -> Option<&CertifiedAssetResponse<'content>> {
1004 for encoding in preferred_encodings {
1005 if let Some(response) = self.fallback_responses.get(&RequestKey::new(
1006 scope,
1007 Some(encoding.to_string()),
1008 None,
1009 )) {
1010 return Some(response);
1011 }
1012 }
1013
1014 None
1015 }
1016
1017 fn get_range_header<'a>(request: &'a HttpRequest) -> Option<&'a str> {
1018 for (name, value) in request.headers().iter() {
1019 if name.to_lowercase().eq(&http::header::RANGE.as_str()) {
1020 return Some(value);
1021 }
1022 }
1023 None
1024 }
1025
1026 fn get_preferred_encodings<'a>(&self, request: &'a HttpRequest) -> Vec<&'a str> {
1027 for (name, value) in request.headers().iter() {
1028 if name.to_lowercase() == "accept-encoding" {
1029 return Self::prioritized_encodings(value)
1030 .iter()
1031 .map(|(encoding, _quality)| *encoding)
1032 .collect();
1033 }
1034 }
1035
1036 vec![]
1037 }
1038
1039 fn prioritized_encodings(encodings: &str) -> Vec<(&str, f32)> {
1040 let mut encodings = encodings
1041 .split(',')
1042 .filter_map(|encoding| {
1043 encoding
1044 .split(';')
1045 .collect::<Vec<_>>()
1046 .first()
1047 .map(|s| s.trim())
1048 .map(|s| (s, Self::default_encoding_quality(s)))
1049 })
1050 .collect::<Vec<_>>();
1051
1052 encodings.sort_unstable_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap());
1055
1056 encodings
1057 }
1058
1059 fn default_encoding_quality(encoding: &str) -> f32 {
1060 if encoding.eq_ignore_ascii_case("br") {
1061 return 1.0;
1062 }
1063
1064 if encoding.eq_ignore_ascii_case("zstd") {
1065 return 0.9;
1066 }
1067
1068 if encoding.eq_ignore_ascii_case("gzip") {
1069 return 0.8;
1070 }
1071
1072 if encoding.eq_ignore_ascii_case("deflate") {
1073 return 0.7;
1074 }
1075
1076 if encoding.eq_ignore_ascii_case("identity") {
1077 return 0.5;
1078 }
1079
1080 0.6
1081 }
1082}
1083
1084impl Default for AssetRouter<'_> {
1085 fn default() -> Self {
1086 Self::new()
1087 }
1088}
1089
1090#[cfg(test)]
1091mod tests {
1092 use super::*;
1093 use crate::AssetFallbackConfig;
1094 use assert_matches::assert_matches;
1095 use ic_certification::{hash_tree::SubtreeLookupResult, HashTree};
1096 use ic_http_certification::{
1097 cel::DefaultFullCelExpressionBuilder, HeaderField, CERTIFICATE_HEADER_NAME,
1098 };
1099 use ic_response_verification::CertificateHeader;
1100 use ic_response_verification_test_utils::{base64_decode, hash};
1101 use rand_chacha::rand_core::{RngCore, SeedableRng};
1102 use rand_chacha::ChaCha20Rng;
1103 use rstest::*;
1104 use std::vec;
1105
1106 const ONE_CHUNK_ASSET_LEN: usize = ASSET_CHUNK_SIZE;
1107 const TWO_CHUNKS_ASSET_LEN: usize = ASSET_CHUNK_SIZE + 1;
1108 const SIX_CHUNKS_ASSET_LEN: usize = 5 * ASSET_CHUNK_SIZE + 12;
1109 const TEN_CHUNKS_ASSET_LEN: usize = 10 * ASSET_CHUNK_SIZE;
1110
1111 const ONE_CHUNK_ASSET_NAME: &str = "long_asset_one_chunk";
1112 const TWO_CHUNKS_ASSET_NAME: &str = "long_asset_two_chunks";
1113 const SIX_CHUNKS_ASSET_NAME: &str = "long_asset_six_chunks";
1114 const TEN_CHUNKS_ASSET_NAME: &str = "long_asset_ten_chunks";
1115
1116 #[rstest]
1117 #[case(0, None)]
1118 #[case(ASSET_CHUNK_SIZE, None)]
1119 #[case(ASSET_CHUNK_SIZE*4, None)]
1120 #[case(0, Some(0))]
1121 #[case(100, Some(2000))]
1122 #[case(10_000, Some(300_000))]
1123 #[case(ASSET_CHUNK_SIZE, Some(2 * ASSET_CHUNK_SIZE - 1))]
1124 fn should_parse_range_header_str(#[case] range_begin: usize, #[case] range_end: Option<usize>) {
1125 let input = if let Some(range_end) = range_end {
1126 format!("bytes={range_begin}-{range_end}")
1127 } else {
1128 format!("bytes={range_begin}-")
1129 };
1130 let result = parse_range_header_str(&input);
1131 let output = result.unwrap_or_else(|e| panic!("failed parsing '{input}': {e:?}"));
1132 assert_eq!(
1133 RangeRequestValues {
1134 range_begin,
1135 range_end
1136 },
1137 output
1138 );
1139 }
1140
1141 #[rstest]
1142 #[case("")]
1143 #[case("byte=1-2")]
1144 #[case("bites=2-4")]
1145 #[case("bytes 7-11")]
1146 #[case("bytes=12345")]
1147 #[case("something else")]
1148 #[case("bytes=-5-19")]
1149 fn should_fail_parse_range_header_str_on_invalid_input(#[case] malformed_input: &str) {
1150 let result = parse_range_header_str(malformed_input);
1151 assert_matches!(result, Err(e) if e.to_string().contains("Invalid Range header"));
1152 }
1153
1154 #[rstest]
1155 #[case("bytes=100-end")]
1156 #[case("bytes=dead-beef")]
1157 fn should_fail_parse_range_header_str_on_malformed_input(#[case] malformed_input: &str) {
1158 let result = parse_range_header_str(malformed_input);
1159 assert_matches!(result, Err(e) if e.to_string().contains("Malformed range_"));
1160 }
1161
1162 #[rstest]
1163 #[case("bytes=100-20")]
1164 #[case("bytes=20-19")]
1165 fn should_fail_parse_range_header_str_on_invalid_values(#[case] malformed_input: &str) {
1166 let result = parse_range_header_str(malformed_input);
1167 assert_matches!(result, Err(e) if e.to_string().contains("Invalid values in Range header"));
1168 }
1169
1170 #[rstest]
1171 #[case("/")]
1172 #[case("https://internetcomputer.org/")]
1173 fn test_index_html(mut asset_router: AssetRouter, #[case] req_url: &str) {
1174 let request = HttpRequest::get(req_url).build();
1175
1176 let mut expected_response = expected_index_html_response();
1177
1178 let response = asset_router
1179 .serve_asset(&data_certificate(), &request)
1180 .unwrap();
1181 let (witness, expr_path) = extract_witness_expr_path(&response);
1182 add_v2_certificate_header(
1183 &data_certificate(),
1184 &mut expected_response,
1185 &witness,
1186 &expr_path,
1187 );
1188
1189 assert_eq!(expr_path, vec!["http_expr", "", "<$>"]);
1190 assert_matches!(
1191 witness.lookup_subtree(&expr_path),
1192 SubtreeLookupResult::Found(_)
1193 );
1194 assert_eq!(response, expected_response);
1195
1196 asset_router
1197 .delete_assets(
1198 vec![Asset::new("index.html", index_html_body())],
1199 vec![index_html_config()],
1200 )
1201 .unwrap();
1202
1203 let result = asset_router.serve_asset(&data_certificate(), &request);
1204 assert_matches!(
1205 result,
1206 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
1207 request_url,
1208 }) if request_url == request.get_path().unwrap()
1209 );
1210 }
1211
1212 #[test]
1213 fn test_one_chunk_long_asset_served_in_full() {
1214 let asset_name = ONE_CHUNK_ASSET_NAME;
1215 let long_asset_router =
1216 long_asset_router_with_params(&[asset_name], &[AssetEncoding::Identity]);
1217 let req_url = format!("/{asset_name}");
1218 let asset_body = long_asset_body(asset_name);
1219 let request = HttpRequest::get(&req_url).build();
1221 let mut expected_response = build_200_response(
1222 asset_body,
1223 asset_cel_expr(),
1224 vec![
1225 (
1226 "cache-control".to_string(),
1227 "public, no-cache, no-store".to_string(),
1228 ),
1229 ("content-type".to_string(), "text/html".to_string()),
1230 ],
1231 );
1232
1233 let response = long_asset_router
1234 .serve_asset(&data_certificate(), &request)
1235 .unwrap();
1236 let (witness, expr_path) = extract_witness_expr_path(&response);
1237 add_v2_certificate_header(
1238 &data_certificate(),
1239 &mut expected_response,
1240 &witness,
1241 &expr_path,
1242 );
1243
1244 assert_eq!(expr_path, vec!["http_expr", &req_url[1..], "<$>"]);
1245 assert_matches!(
1246 witness.lookup_subtree(&expr_path),
1247 SubtreeLookupResult::Found(_)
1248 );
1249 assert_eq!(response, expected_response);
1250 }
1251
1252 #[rstest]
1253 #[case(TWO_CHUNKS_ASSET_NAME)]
1254 #[case(SIX_CHUNKS_ASSET_NAME)]
1255 #[case(TEN_CHUNKS_ASSET_NAME)]
1256 fn test_long_asset_served_in_chunks(#[case] asset_name: &str) {
1257 let long_asset_router =
1258 long_asset_router_with_params(&[asset_name], &[AssetEncoding::Identity]);
1259 let req_url = format!("/{asset_name}");
1260 let asset_body = long_asset_body(asset_name);
1261 let asset_len = asset_body.len();
1262 let request = HttpRequest::get(&req_url).build();
1264 let mut expected_response = build_206_response(
1265 asset_body[0..ASSET_CHUNK_SIZE].to_vec(),
1266 asset_cel_expr(),
1267 vec![
1268 (
1269 "cache-control".to_string(),
1270 "public, no-cache, no-store".to_string(),
1271 ),
1272 ("content-type".to_string(), "text/html".to_string()),
1273 (
1274 "content-range".to_string(),
1275 format!("bytes 0-{}/{}", ASSET_CHUNK_SIZE - 1, asset_len),
1276 ),
1277 ],
1278 );
1279
1280 let response = long_asset_router
1281 .serve_asset(&data_certificate(), &request)
1282 .unwrap();
1283 let (witness, expr_path) = extract_witness_expr_path(&response);
1284 add_v2_certificate_header(
1285 &data_certificate(),
1286 &mut expected_response,
1287 &witness,
1288 &expr_path,
1289 );
1290
1291 assert_eq!(expr_path, vec!["http_expr", &req_url[1..], "<$>"]);
1292 assert_matches!(
1293 witness.lookup_subtree(&expr_path),
1294 SubtreeLookupResult::Found(_)
1295 );
1296 assert_eq!(response, expected_response);
1297
1298 let expected_number_of_chunks =
1300 (asset_len as f32 / ASSET_CHUNK_SIZE as f32).ceil() as usize;
1301 let mut asset_len_so_far = response.body().len();
1302 let mut number_of_chunks_so_far = 1;
1303 while asset_len_so_far < asset_len {
1304 let chunk_request = HttpRequest::get(&req_url)
1305 .with_headers(vec![(
1306 "range".to_string(),
1307 format!("bytes={asset_len_so_far}-"),
1308 )])
1309 .build();
1310 let expected_range_end = cmp::min(asset_len_so_far + ASSET_CHUNK_SIZE, asset_len) - 1;
1311 let mut expected_response = build_206_response(
1312 asset_body[asset_len_so_far..=expected_range_end].to_vec(),
1313 asset_range_chunk_cel_expr(),
1314 vec![
1315 (
1316 "cache-control".to_string(),
1317 "public, no-cache, no-store".to_string(),
1318 ),
1319 ("content-type".to_string(), "text/html".to_string()),
1320 (
1321 "content-range".to_string(),
1322 format!("bytes {asset_len_so_far}-{expected_range_end}/{asset_len}"),
1323 ),
1324 ],
1325 );
1326 let response = long_asset_router
1327 .serve_asset(&data_certificate(), &chunk_request)
1328 .unwrap();
1329 let (witness, expr_path) = extract_witness_expr_path(&response);
1330 assert_matches!(
1331 witness.lookup_subtree(&expr_path),
1332 SubtreeLookupResult::Found(_)
1333 );
1334 add_v2_certificate_header(
1335 &data_certificate(),
1336 &mut expected_response,
1337 &witness,
1338 &expr_path,
1339 );
1340 assert_eq!(response, expected_response);
1341 asset_len_so_far += response.body().len();
1342 number_of_chunks_so_far += 1;
1343 }
1344 assert_eq!(number_of_chunks_so_far, expected_number_of_chunks)
1345 }
1346
1347 #[rstest]
1348 #[case(TWO_CHUNKS_ASSET_NAME)]
1349 #[case(SIX_CHUNKS_ASSET_NAME)]
1350 #[case(TEN_CHUNKS_ASSET_NAME)]
1351 fn test_long_asset_deletion_removes_chunks(#[case] asset_name: &str) {
1352 let mut long_asset_router =
1353 long_asset_router_with_params(&[asset_name], &[AssetEncoding::Identity]);
1354 let req_url = format!("/{asset_name}");
1355 let asset_body = long_asset_body(asset_name);
1356 let asset_len = asset_body.len();
1357 let mut all_requests = vec![];
1358 let request = HttpRequest::get(&req_url).build();
1361 let response = long_asset_router
1362 .serve_asset(&data_certificate(), &request)
1363 .unwrap();
1364 let (witness, expr_path) = extract_witness_expr_path(&response);
1365
1366 assert_eq!(expr_path, vec!["http_expr", &req_url[1..], "<$>"]);
1367 assert_matches!(
1368 witness.lookup_subtree(&expr_path),
1369 SubtreeLookupResult::Found(_)
1370 );
1371 assert_eq!(response.status_code(), StatusCode::PARTIAL_CONTENT);
1372 all_requests.push(request);
1373
1374 let expected_number_of_chunks =
1376 (asset_len as f32 / ASSET_CHUNK_SIZE as f32).ceil() as usize;
1377 let mut asset_len_so_far = response.body().len();
1378 let mut number_of_chunks_so_far = 1;
1379 while asset_len_so_far < asset_len {
1380 let chunk_request = HttpRequest::get(&req_url)
1381 .with_headers(vec![(
1382 "range".to_string(),
1383 format!("bytes={asset_len_so_far}-"),
1384 )])
1385 .build();
1386 let response = long_asset_router
1387 .serve_asset(&data_certificate(), &chunk_request)
1388 .unwrap();
1389 let (witness, expr_path) = extract_witness_expr_path(&response);
1390 assert_matches!(
1391 witness.lookup_subtree(&expr_path),
1392 SubtreeLookupResult::Found(_)
1393 );
1394 assert_eq!(response.status_code(), StatusCode::PARTIAL_CONTENT);
1395 asset_len_so_far += response.body().len();
1396 number_of_chunks_so_far += 1;
1397 all_requests.push(chunk_request);
1398 }
1399 assert_eq!(number_of_chunks_so_far, expected_number_of_chunks);
1400 assert_eq!(all_requests.len(), expected_number_of_chunks);
1401
1402 long_asset_router
1404 .delete_assets(
1405 vec![Asset::new(&req_url, asset_body)],
1406 vec![long_asset_config(asset_name)],
1407 )
1408 .expect("Asset deletion failed");
1409
1410 for request in all_requests {
1412 let result = long_asset_router.serve_asset(&data_certificate(), &request);
1413 assert_matches!(
1414 result,
1415 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
1416 request_url,
1417 }) if request_url == request.get_path().unwrap()
1418 );
1419 }
1420 }
1421
1422 #[rstest]
1423 #[case(SIX_CHUNKS_ASSET_NAME, "deflate", AssetEncoding::Deflate)]
1424 #[case(SIX_CHUNKS_ASSET_NAME, "deflate, identity", AssetEncoding::Deflate)]
1425 #[case(SIX_CHUNKS_ASSET_NAME, "gzip", AssetEncoding::Gzip)]
1426 #[case(SIX_CHUNKS_ASSET_NAME, "gzip, identity", AssetEncoding::Gzip)]
1427 #[case(SIX_CHUNKS_ASSET_NAME, "gzip, deflate", AssetEncoding::Gzip)]
1428 #[case(SIX_CHUNKS_ASSET_NAME, "gzip, deflate, identity", AssetEncoding::Gzip)]
1429 #[case(SIX_CHUNKS_ASSET_NAME, "br", AssetEncoding::Brotli)]
1430 #[case(
1431 SIX_CHUNKS_ASSET_NAME,
1432 "br, gzip, deflate, identity",
1433 AssetEncoding::Brotli
1434 )]
1435 #[case(
1436 SIX_CHUNKS_ASSET_NAME,
1437 "gzip, deflate, identity, br",
1438 AssetEncoding::Brotli
1439 )]
1440 fn test_encoded_long_asset_served_in_encoded_chunks(
1441 #[case] asset_name: &str,
1442 #[case] accept_encoding: &str,
1443 #[case] expected_encoding: AssetEncoding,
1444 ) {
1445 let (_, expected_encoding_suffix) = expected_encoding.default_config();
1446 let long_asset_router = long_asset_router_with_params(
1447 &[asset_name],
1448 &[AssetEncoding::Identity, expected_encoding],
1449 );
1450 let req_url = format!("/{asset_name}");
1451 let encoded_asset_name = format!("{asset_name}{expected_encoding_suffix}");
1452 let asset_body = long_asset_body(&encoded_asset_name);
1453 let asset_len = asset_body.len();
1454
1455 let request = HttpRequest::get(&req_url)
1456 .with_headers(vec![(
1457 "accept-encoding".to_string(),
1458 accept_encoding.to_string(),
1459 )])
1460 .build();
1461 let mut expected_response = build_206_response(
1462 asset_body[0..ASSET_CHUNK_SIZE].to_vec(),
1463 asset_cel_expr(),
1464 vec![
1465 (
1466 "cache-control".to_string(),
1467 "public, no-cache, no-store".to_string(),
1468 ),
1469 ("content-type".to_string(), "text/html".to_string()),
1470 (
1471 "content-encoding".to_string(),
1472 expected_encoding.to_string(),
1473 ),
1474 (
1475 "content-range".to_string(),
1476 format!("bytes 0-{}/{}", ASSET_CHUNK_SIZE - 1, asset_len),
1477 ),
1478 ],
1479 );
1480 let response = long_asset_router
1481 .serve_asset(&data_certificate(), &request)
1482 .unwrap();
1483 let (witness, expr_path) = extract_witness_expr_path(&response);
1484 add_v2_certificate_header(
1485 &data_certificate(),
1486 &mut expected_response,
1487 &witness,
1488 &expr_path,
1489 );
1490
1491 assert_eq!(
1492 expr_path,
1493 HttpCertificationPath::exact(req_url.clone()).to_expr_path()
1494 );
1495 assert_matches!(
1496 witness.lookup_subtree(&expr_path),
1497 SubtreeLookupResult::Found(_)
1498 );
1499 assert_eq!(response, expected_response);
1500
1501 let expected_number_of_chunks =
1503 (asset_len as f32 / ASSET_CHUNK_SIZE as f32).ceil() as usize;
1504 let mut asset_len_so_far = response.body().len();
1505 let mut number_of_chunks_so_far = 1;
1506 while asset_len_so_far < asset_len {
1507 let chunk_request = HttpRequest::get(&req_url)
1508 .with_headers(vec![
1509 ("range".to_string(), format!("bytes={asset_len_so_far}-")),
1510 ("accept-encoding".to_string(), accept_encoding.to_string()),
1511 ])
1512 .build();
1513 let expected_range_end = cmp::min(asset_len_so_far + ASSET_CHUNK_SIZE, asset_len) - 1;
1514 let mut expected_response = build_206_response(
1515 asset_body[asset_len_so_far..=expected_range_end].to_vec(),
1516 encoded_range_chunk_asset_cel_expr(),
1517 vec![
1518 (
1519 "cache-control".to_string(),
1520 "public, no-cache, no-store".to_string(),
1521 ),
1522 ("content-type".to_string(), "text/html".to_string()),
1523 (
1524 "content-encoding".to_string(),
1525 expected_encoding.to_string(),
1526 ),
1527 (
1528 "content-range".to_string(),
1529 format!("bytes {asset_len_so_far}-{expected_range_end}/{asset_len}"),
1530 ),
1531 ],
1532 );
1533 let response = long_asset_router
1534 .serve_asset(&data_certificate(), &chunk_request)
1535 .unwrap();
1536 let (witness, expr_path) = extract_witness_expr_path(&response);
1537 assert_matches!(
1538 witness.lookup_subtree(&expr_path),
1539 SubtreeLookupResult::Found(_)
1540 );
1541 add_v2_certificate_header(
1542 &data_certificate(),
1543 &mut expected_response,
1544 &witness,
1545 &expr_path,
1546 );
1547 assert_eq!(response, expected_response);
1548 asset_len_so_far += response.body().len();
1549 number_of_chunks_so_far += 1;
1550 }
1551 assert_eq!(number_of_chunks_so_far, expected_number_of_chunks)
1552 }
1553
1554 #[rstest]
1555 #[case(TWO_CHUNKS_ASSET_NAME, AssetEncoding::Brotli)]
1556 #[case(TWO_CHUNKS_ASSET_NAME, AssetEncoding::Gzip)]
1557 #[case(TWO_CHUNKS_ASSET_NAME, AssetEncoding::Deflate)]
1558 fn test_encoded_long_asset_deletion_removes_encoded_chunks(
1559 #[case] asset_name: &str,
1560 #[case] encoding: AssetEncoding,
1561 ) {
1562 let (_, encoding_suffix) = encoding.default_config();
1563 let mut long_asset_router =
1564 long_asset_router_with_params(&[asset_name], &[AssetEncoding::Identity, encoding]);
1565 let req_url = format!("/{asset_name}");
1566 let encoded_asset_name = format!("{asset_name}{encoding_suffix}");
1567 let encoded_asset_body = long_asset_body(&encoded_asset_name);
1568 let asset_len = encoded_asset_body.len();
1569 let mut all_requests = vec![];
1570 let request = HttpRequest::get(&req_url)
1573 .with_headers(vec![("accept-encoding".to_string(), encoding.to_string())])
1574 .build();
1575 let response = long_asset_router
1576 .serve_asset(&data_certificate(), &request)
1577 .unwrap();
1578 let (witness, expr_path) = extract_witness_expr_path(&response);
1579
1580 assert_eq!(expr_path, vec!["http_expr", &req_url[1..], "<$>"]);
1581 assert_matches!(
1582 witness.lookup_subtree(&expr_path),
1583 SubtreeLookupResult::Found(_)
1584 );
1585 assert_eq!(response.status_code(), StatusCode::PARTIAL_CONTENT);
1586 all_requests.push(request);
1587
1588 let expected_number_of_chunks =
1590 (asset_len as f32 / ASSET_CHUNK_SIZE as f32).ceil() as usize;
1591 let mut asset_len_so_far = response.body().len();
1592 let mut number_of_chunks_so_far = 1;
1593 while asset_len_so_far < asset_len {
1594 let chunk_request = HttpRequest::get(&req_url)
1595 .with_headers(vec![
1596 ("range".to_string(), format!("bytes={asset_len_so_far}-")),
1597 ("accept-encoding".to_string(), encoding.to_string()),
1598 ])
1599 .build();
1600 let response = long_asset_router
1601 .serve_asset(&data_certificate(), &chunk_request)
1602 .unwrap();
1603 let (witness, expr_path) = extract_witness_expr_path(&response);
1604 assert_matches!(
1605 witness.lookup_subtree(&expr_path),
1606 SubtreeLookupResult::Found(_)
1607 );
1608 assert_eq!(response.status_code(), StatusCode::PARTIAL_CONTENT);
1609 asset_len_so_far += response.body().len();
1610 number_of_chunks_so_far += 1;
1611 all_requests.push(chunk_request);
1612 }
1613 assert_eq!(number_of_chunks_so_far, expected_number_of_chunks);
1614 assert_eq!(all_requests.len(), expected_number_of_chunks);
1615
1616 long_asset_router
1618 .delete_assets(
1619 vec![
1620 Asset::new(&req_url, long_asset_body(asset_name)),
1621 Asset::new(format!("/{encoded_asset_name}"), encoded_asset_body),
1622 ],
1623 vec![long_asset_config(&req_url)],
1624 )
1625 .expect("Asset deletion failed");
1626
1627 for request in all_requests {
1629 let result = long_asset_router.serve_asset(&data_certificate(), &request);
1630 assert_matches!(
1631 result,
1632 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
1633 request_url,
1634 }) if request_url == request.get_path().unwrap()
1635 );
1636 }
1637 }
1638
1639 #[rstest]
1640 #[case(index_html_zz_body(), "/", "deflate", "deflate")]
1641 #[case(index_html_zz_body(), "/", "deflate, identity", "deflate")]
1642 #[case(index_html_gz_body(), "/", "gzip", "gzip")]
1643 #[case(index_html_gz_body(), "/", "gzip, identity", "gzip")]
1644 #[case(index_html_gz_body(), "/", "gzip, deflate", "gzip")]
1645 #[case(index_html_gz_body(), "/", "gzip, deflate, identity", "gzip")]
1646 #[case(index_html_br_body(), "/", "br", "br")]
1647 #[case(index_html_br_body(), "/", "br, gzip, deflate, identity", "br")]
1648 #[case(index_html_br_body(), "/", "gzip, deflate, identity, br", "br")]
1649 #[case(index_html_zz_body(), "/index.html", "deflate", "deflate")]
1650 #[case(index_html_zz_body(), "/index.html", "deflate, identity", "deflate")]
1651 #[case(index_html_gz_body(), "/index.html", "gzip", "gzip")]
1652 #[case(index_html_gz_body(), "/index.html", "gzip, identity", "gzip")]
1653 #[case(index_html_gz_body(), "/index.html", "gzip, deflate", "gzip")]
1654 #[case(index_html_gz_body(), "/index.html", "gzip, deflate, identity", "gzip")]
1655 #[case(index_html_br_body(), "/index.html", "br", "br")]
1656 #[case(
1657 index_html_br_body(),
1658 "/index.html",
1659 "br, gzip, deflate, identity",
1660 "br"
1661 )]
1662 #[case(
1663 index_html_br_body(),
1664 "/index.html",
1665 "gzip, deflate, identity, br",
1666 "br"
1667 )]
1668 fn test_encoded_index_html(
1669 #[case] expected_body: Vec<u8>,
1670 #[case] req_url: &str,
1671 #[case] accept_encoding: &str,
1672 #[case] expected_encoding: &str,
1673 mut asset_router: AssetRouter,
1674 ) {
1675 let request = HttpRequest::get(req_url)
1676 .with_headers(vec![(
1677 "accept-encoding".to_string(),
1678 accept_encoding.to_string(),
1679 )])
1680 .build();
1681 let mut expected_response = build_200_response(
1682 expected_body,
1683 encoded_asset_cel_expr(),
1684 vec![
1685 (
1686 "cache-control".to_string(),
1687 "public, no-cache, no-store".to_string(),
1688 ),
1689 ("content-type".to_string(), "text/html".to_string()),
1690 (
1691 "content-encoding".to_string(),
1692 expected_encoding.to_string(),
1693 ),
1694 ],
1695 );
1696 let response = asset_router
1697 .serve_asset(&data_certificate(), &request)
1698 .unwrap();
1699 let (witness, expr_path) = extract_witness_expr_path(&response);
1700 add_v2_certificate_header(
1701 &data_certificate(),
1702 &mut expected_response,
1703 &witness,
1704 &expr_path,
1705 );
1706
1707 assert_eq!(
1708 expr_path,
1709 HttpCertificationPath::exact(req_url).to_expr_path()
1710 );
1711 assert_matches!(
1712 witness.lookup_subtree(&expr_path),
1713 SubtreeLookupResult::Found(_)
1714 );
1715 assert_eq!(response, expected_response);
1716
1717 asset_router
1718 .delete_assets(
1719 vec![
1720 Asset::new("index.html", index_html_body()),
1721 Asset::new("index.html.gz", index_html_gz_body()),
1722 Asset::new("index.html.zz", index_html_zz_body()),
1723 Asset::new("index.html.br", index_html_br_body()),
1724 ],
1725 vec![index_html_config()],
1726 )
1727 .unwrap();
1728
1729 let result = asset_router.serve_asset(&data_certificate(), &request);
1730 assert_matches!(
1731 result,
1732 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
1733 request_url,
1734 }) if request_url == req_url
1735 );
1736 }
1737
1738 #[rstest]
1739 #[case(index_html_zz_body(), "/something", "deflate", "deflate")]
1740 #[case(index_html_zz_body(), "/something", "deflate, identity", "deflate")]
1741 #[case(index_html_gz_body(), "/something", "gzip", "gzip")]
1742 #[case(index_html_gz_body(), "/something", "gzip, identity", "gzip")]
1743 #[case(index_html_gz_body(), "/something", "gzip, deflate", "gzip")]
1744 #[case(index_html_gz_body(), "/something", "gzip, deflate, identity", "gzip")]
1745 #[case(index_html_br_body(), "/something", "br", "br")]
1746 #[case(
1747 index_html_br_body(),
1748 "/something",
1749 "br, gzip, deflate, identity",
1750 "br"
1751 )]
1752 #[case(
1753 index_html_br_body(),
1754 "/something",
1755 "gzip, deflate, identity, br",
1756 "br"
1757 )]
1758 #[case(index_html_zz_body(), "/assets/css/app.css", "deflate", "deflate")]
1759 #[case(
1760 index_html_zz_body(),
1761 "/assets/css/app.css",
1762 "deflate, identity",
1763 "deflate"
1764 )]
1765 #[case(index_html_gz_body(), "/assets/css/app.css", "gzip", "gzip")]
1766 #[case(index_html_gz_body(), "/assets/css/app.css", "gzip, identity", "gzip")]
1767 #[case(index_html_gz_body(), "/assets/css/app.css", "gzip, deflate", "gzip")]
1768 #[case(
1769 index_html_gz_body(),
1770 "/assets/css/app.css",
1771 "gzip, deflate, identity",
1772 "gzip"
1773 )]
1774 #[case(index_html_br_body(), "/assets/css/app.css", "br", "br")]
1775 #[case(
1776 index_html_br_body(),
1777 "/assets/css/app.css",
1778 "br, gzip, deflate, identity",
1779 "br"
1780 )]
1781 #[case(
1782 index_html_br_body(),
1783 "/assets/css/app.css",
1784 "gzip, deflate, identity, br",
1785 "br"
1786 )]
1787 fn test_encoded_index_html_fallback(
1788 #[case] expected_body: Vec<u8>,
1789 #[case] req_url: &str,
1790 #[case] accept_encoding: &str,
1791 #[case] expected_encoding: &str,
1792 mut asset_router: AssetRouter,
1793 ) {
1794 let request = HttpRequest::get(req_url)
1795 .with_headers(vec![(
1796 "accept-encoding".to_string(),
1797 accept_encoding.to_string(),
1798 )])
1799 .build();
1800 let mut expected_response = build_200_response(
1801 expected_body,
1802 encoded_asset_cel_expr(),
1803 vec![
1804 (
1805 "cache-control".to_string(),
1806 "public, no-cache, no-store".to_string(),
1807 ),
1808 ("content-type".to_string(), "text/html".to_string()),
1809 (
1810 "content-encoding".to_string(),
1811 expected_encoding.to_string(),
1812 ),
1813 ],
1814 );
1815
1816 let response = asset_router
1817 .serve_asset(&data_certificate(), &request)
1818 .unwrap();
1819 let (witness, expr_path) = extract_witness_expr_path(&response);
1820 add_v2_certificate_header(
1821 &data_certificate(),
1822 &mut expected_response,
1823 &witness,
1824 &expr_path,
1825 );
1826
1827 let requested_expr_path = HttpCertificationPath::exact(req_url).to_expr_path();
1828 assert_eq!(expr_path, vec!["http_expr", "", "<*>"]);
1829 assert_matches!(
1830 witness.lookup_subtree(&expr_path),
1831 SubtreeLookupResult::Found(_)
1832 );
1833 assert_matches!(
1834 witness.lookup_subtree(&requested_expr_path),
1835 SubtreeLookupResult::Absent
1836 );
1837 assert_eq!(response, expected_response);
1838
1839 asset_router
1840 .delete_assets(
1841 vec![
1842 Asset::new("index.html", index_html_body()),
1843 Asset::new("index.html.gz", index_html_gz_body()),
1844 Asset::new("index.html.zz", index_html_zz_body()),
1845 Asset::new("index.html.br", index_html_br_body()),
1846 ],
1847 vec![index_html_config()],
1848 )
1849 .unwrap();
1850
1851 let result = asset_router.serve_asset(&data_certificate(), &request);
1852 assert_matches!(
1853 result,
1854 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
1855 request_url,
1856 }) if request_url == req_url
1857 );
1858 }
1859
1860 #[rstest]
1861 #[case("/something", "/something")]
1862 #[case("https://internetcomputer.org/something", "/something")]
1863 fn test_index_html_root_fallback(
1864 mut asset_router: AssetRouter,
1865 #[case] req_url: &str,
1866 #[case] req_path: &str,
1867 ) {
1868 let mut expected_response = expected_index_html_response();
1869
1870 let request = HttpRequest::get(req_url).build();
1871 let requested_expr_path = HttpCertificationPath::exact(req_path).to_expr_path();
1872
1873 let response = asset_router
1874 .serve_asset(&data_certificate(), &request)
1875 .unwrap();
1876 let (witness, expr_path) = extract_witness_expr_path(&response);
1877 add_v2_certificate_header(
1878 &data_certificate(),
1879 &mut expected_response,
1880 &witness,
1881 &expr_path,
1882 );
1883
1884 assert_eq!(expr_path, vec!["http_expr", "", "<*>"]);
1885 assert_matches!(
1886 witness.lookup_subtree(&expr_path),
1887 SubtreeLookupResult::Found(_)
1888 );
1889 assert_matches!(
1890 witness.lookup_subtree(&requested_expr_path),
1891 SubtreeLookupResult::Absent
1892 );
1893 assert_eq!(response, expected_response);
1894
1895 asset_router
1896 .delete_assets(
1897 vec![
1898 Asset::new("index.html", index_html_body()),
1899 Asset::new("index.html.gz", index_html_gz_body()),
1900 Asset::new("index.html.zz", index_html_zz_body()),
1901 Asset::new("index.html.br", index_html_br_body()),
1902 ],
1903 vec![index_html_config()],
1904 )
1905 .unwrap();
1906
1907 let result = asset_router.serve_asset(&data_certificate(), &request);
1908 assert_matches!(
1909 result,
1910 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
1911 request_url,
1912 }) if request_url == req_path
1913 );
1914 }
1915
1916 #[rstest]
1917 #[case("/assets/css/app.css", "/assets/css/app.css")]
1918 #[case(
1919 "https://internetcomputer.org/assets/css/app.css",
1920 "/assets/css/app.css"
1921 )]
1922 fn test_index_html_nested_fallback(
1923 mut asset_router: AssetRouter,
1924 #[case] req_url: &str,
1925 #[case] req_path: &str,
1926 ) {
1927 let mut expected_response = expected_index_html_response();
1928
1929 let request = HttpRequest::get(req_url).build();
1930 let requested_expr_path = HttpCertificationPath::exact(req_path).to_expr_path();
1931
1932 let response = asset_router
1933 .serve_asset(&data_certificate(), &request)
1934 .unwrap();
1935 let (witness, expr_path) = extract_witness_expr_path(&response);
1936 add_v2_certificate_header(
1937 &data_certificate(),
1938 &mut expected_response,
1939 &witness,
1940 &expr_path,
1941 );
1942
1943 assert_eq!(expr_path, vec!["http_expr", "", "<*>"]);
1944 assert_matches!(
1945 witness.lookup_subtree(&expr_path),
1946 SubtreeLookupResult::Found(_)
1947 );
1948 assert_matches!(
1949 witness.lookup_subtree(&requested_expr_path),
1950 SubtreeLookupResult::Absent
1951 );
1952 assert_eq!(response, expected_response);
1953
1954 asset_router
1955 .delete_assets(
1956 vec![
1957 Asset::new("index.html", index_html_body()),
1958 Asset::new("index.html.gz", index_html_gz_body()),
1959 Asset::new("index.html.zz", index_html_zz_body()),
1960 Asset::new("index.html.br", index_html_br_body()),
1961 ],
1962 vec![index_html_config()],
1963 )
1964 .unwrap();
1965
1966 let result = asset_router.serve_asset(&data_certificate(), &request);
1967 assert_matches!(
1968 result,
1969 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
1970 request_url,
1971 }) if request_url == req_path
1972 );
1973 }
1974
1975 #[rstest]
1976 #[case("/css/app-ba74b708.css")]
1977 #[case("https://internetcomputer.org/css/app-ba74b708.css")]
1978 fn text_app_css(mut asset_router: AssetRouter, #[case] req_url: &str) {
1979 let request = HttpRequest::get(req_url).build();
1980 let mut expected_response = build_200_response(
1981 app_css_body(),
1982 asset_cel_expr(),
1983 vec![
1984 (
1985 "cache-control".to_string(),
1986 "public, max-age=31536000, immutable".to_string(),
1987 ),
1988 ("content-type".to_string(), "text/css".to_string()),
1989 ],
1990 );
1991
1992 let response = asset_router
1993 .serve_asset(&data_certificate(), &request)
1994 .unwrap();
1995 let (witness, expr_path) = extract_witness_expr_path(&response);
1996 add_v2_certificate_header(
1997 &data_certificate(),
1998 &mut expected_response,
1999 &witness,
2000 &expr_path,
2001 );
2002
2003 assert_eq!(
2004 expr_path,
2005 vec!["http_expr", "css", "app-ba74b708.css", "<$>"]
2006 );
2007 assert_matches!(
2008 witness.lookup_subtree(&expr_path),
2009 SubtreeLookupResult::Found(_)
2010 );
2011 assert_eq!(response, expected_response);
2012
2013 asset_router
2014 .delete_assets(
2015 vec![Asset::new("css/app-ba74b708.css", app_css_body())],
2016 vec![css_config()],
2017 )
2018 .unwrap();
2019 let mut expected_response = build_response(
2020 StatusCode::NOT_FOUND,
2021 not_found_html_body(),
2022 asset_cel_expr(),
2023 vec![
2024 (
2025 "cache-control".to_string(),
2026 "public, no-cache, no-store".to_string(),
2027 ),
2028 ("content-type".to_string(), "text/html".to_string()),
2029 ],
2030 );
2031
2032 let response = asset_router
2033 .serve_asset(&data_certificate(), &request)
2034 .unwrap();
2035 let (witness, expr_path) = extract_witness_expr_path(&response);
2036 add_v2_certificate_header(
2037 &data_certificate(),
2038 &mut expected_response,
2039 &witness,
2040 &expr_path,
2041 );
2042
2043 assert_eq!(expr_path, vec!["http_expr", "css", "<*>"]);
2044 assert_matches!(
2045 witness.lookup_subtree(&expr_path),
2046 SubtreeLookupResult::Found(_)
2047 );
2048 assert_eq!(response, expected_response);
2049
2050 asset_router
2051 .delete_assets(
2052 vec![
2053 Asset::new("not-found.html", not_found_html_body()),
2054 Asset::new("not-found.html.gz", not_found_html_gz_body()),
2055 Asset::new("not-found.html.zz", not_found_html_zz_body()),
2056 Asset::new("not-found.html.br", not_found_html_br_body()),
2057 ],
2058 vec![not_found_html_config()],
2059 )
2060 .unwrap();
2061 let mut expected_response = expected_index_html_response();
2062
2063 let response = asset_router
2064 .serve_asset(&data_certificate(), &request)
2065 .unwrap();
2066 let (witness, expr_path) = extract_witness_expr_path(&response);
2067 add_v2_certificate_header(
2068 &data_certificate(),
2069 &mut expected_response,
2070 &witness,
2071 &expr_path,
2072 );
2073
2074 assert_eq!(expr_path, vec!["http_expr", "", "<*>"]);
2075 assert_matches!(
2076 witness.lookup_subtree(&expr_path),
2077 SubtreeLookupResult::Found(_)
2078 );
2079 assert_eq!(response, expected_response);
2080
2081 asset_router
2082 .delete_assets(
2083 vec![
2084 Asset::new("index.html", index_html_body()),
2085 Asset::new("index.html.gz", index_html_gz_body()),
2086 Asset::new("index.html.zz", index_html_zz_body()),
2087 Asset::new("index.html.br", index_html_br_body()),
2088 ],
2089 vec![index_html_config()],
2090 )
2091 .unwrap();
2092
2093 let result = asset_router.serve_asset(&data_certificate(), &request);
2094 assert_matches!(
2095 result,
2096 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2097 request_url,
2098 }) if request_url == request.get_path().unwrap()
2099 );
2100 }
2101
2102 #[rstest]
2103 #[case("/css/core-8d4jhgy2.js")]
2104 #[case("https://internetcomputer.org/css/core-8d4jhgy2.js")]
2105 fn test_not_found_css(mut asset_router: AssetRouter, #[case] req_url: &str) {
2106 let request = HttpRequest::get(req_url).build();
2107 let mut expected_response = build_response(
2108 StatusCode::NOT_FOUND,
2109 not_found_html_body(),
2110 asset_cel_expr(),
2111 vec![
2112 (
2113 "cache-control".to_string(),
2114 "public, no-cache, no-store".to_string(),
2115 ),
2116 ("content-type".to_string(), "text/html".to_string()),
2117 ],
2118 );
2119
2120 let response = asset_router
2121 .serve_asset(&data_certificate(), &request)
2122 .unwrap();
2123 let (witness, expr_path) = extract_witness_expr_path(&response);
2124 add_v2_certificate_header(
2125 &data_certificate(),
2126 &mut expected_response,
2127 &witness,
2128 &expr_path,
2129 );
2130
2131 assert_eq!(expr_path, vec!["http_expr", "css", "<*>"]);
2132 assert_matches!(
2133 witness.lookup_subtree(&expr_path),
2134 SubtreeLookupResult::Found(_)
2135 );
2136 assert_eq!(response, expected_response);
2137
2138 asset_router
2139 .delete_assets(
2140 vec![
2141 Asset::new("not-found.html", not_found_html_body()),
2142 Asset::new("not-found.html.gz", not_found_html_gz_body()),
2143 Asset::new("not-found.html.zz", not_found_html_zz_body()),
2144 Asset::new("not-found.html.br", not_found_html_br_body()),
2145 ],
2146 vec![not_found_html_config()],
2147 )
2148 .unwrap();
2149 let mut expected_response = expected_index_html_response();
2150
2151 let response = asset_router
2152 .serve_asset(&data_certificate(), &request)
2153 .unwrap();
2154 let (witness, expr_path) = extract_witness_expr_path(&response);
2155 add_v2_certificate_header(
2156 &data_certificate(),
2157 &mut expected_response,
2158 &witness,
2159 &expr_path,
2160 );
2161
2162 assert_eq!(expr_path, vec!["http_expr", "", "<*>"]);
2163 assert_matches!(
2164 witness.lookup_subtree(&expr_path),
2165 SubtreeLookupResult::Found(_)
2166 );
2167 assert_eq!(response, expected_response);
2168
2169 asset_router
2170 .delete_assets(
2171 vec![
2172 Asset::new("index.html", index_html_body()),
2173 Asset::new("index.html.gz", index_html_gz_body()),
2174 Asset::new("index.html.zz", index_html_zz_body()),
2175 Asset::new("index.html.br", index_html_br_body()),
2176 ],
2177 vec![index_html_config()],
2178 )
2179 .unwrap();
2180
2181 let result = asset_router.serve_asset(&data_certificate(), &request);
2182 assert_matches!(
2183 result,
2184 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2185 request_url,
2186 }) if request_url == request.get_path().unwrap()
2187 );
2188 }
2189
2190 #[rstest]
2191 #[case("/js/app-488df671.js")]
2192 #[case("https://internetcomputer.org/js/app-488df671.js")]
2193 fn test_app_js(mut asset_router: AssetRouter, #[case] req_url: &str) {
2194 let request = HttpRequest::get(req_url).build();
2195 let mut expected_response = build_200_response(
2196 app_js_body(),
2197 asset_cel_expr(),
2198 vec![
2199 (
2200 "cache-control".to_string(),
2201 "public, max-age=31536000, immutable".to_string(),
2202 ),
2203 ("content-type".to_string(), "text/javascript".to_string()),
2204 ],
2205 );
2206
2207 let response = asset_router
2208 .serve_asset(&data_certificate(), &request)
2209 .unwrap();
2210 let (witness, expr_path) = extract_witness_expr_path(&response);
2211 add_v2_certificate_header(
2212 &data_certificate(),
2213 &mut expected_response,
2214 &witness,
2215 &expr_path,
2216 );
2217
2218 assert_eq!(expr_path, vec!["http_expr", "js", "app-488df671.js", "<$>"]);
2219 assert_matches!(
2220 witness.lookup_subtree(&expr_path),
2221 SubtreeLookupResult::Found(_)
2222 );
2223 assert_eq!(response, expected_response);
2224
2225 asset_router
2226 .delete_assets(
2227 vec![
2228 Asset::new("js/app-488df671.js", app_js_body()),
2229 Asset::new("js/app-488df671.js.gz", app_js_gz_body()),
2230 Asset::new("js/app-488df671.js.zz", app_js_zz_body()),
2231 Asset::new("js/app-488df671.js.br", app_js_br_body()),
2232 ],
2233 vec![js_config()],
2234 )
2235 .unwrap();
2236 let mut expected_response = build_response(
2237 StatusCode::NOT_FOUND,
2238 not_found_html_body(),
2239 asset_cel_expr(),
2240 vec![
2241 (
2242 "cache-control".to_string(),
2243 "public, no-cache, no-store".to_string(),
2244 ),
2245 ("content-type".to_string(), "text/html".to_string()),
2246 ],
2247 );
2248
2249 let response = asset_router
2250 .serve_asset(&data_certificate(), &request)
2251 .unwrap();
2252 let (witness, expr_path) = extract_witness_expr_path(&response);
2253 add_v2_certificate_header(
2254 &data_certificate(),
2255 &mut expected_response,
2256 &witness,
2257 &expr_path,
2258 );
2259
2260 assert_eq!(expr_path, vec!["http_expr", "js", "<*>"]);
2261 assert_matches!(
2262 witness.lookup_subtree(&expr_path),
2263 SubtreeLookupResult::Found(_)
2264 );
2265 assert_eq!(response, expected_response);
2266
2267 asset_router
2268 .delete_assets(
2269 vec![
2270 Asset::new("not-found.html", not_found_html_body()),
2271 Asset::new("not-found.html.gz", not_found_html_gz_body()),
2272 Asset::new("not-found.html.zz", not_found_html_zz_body()),
2273 Asset::new("not-found.html.br", not_found_html_br_body()),
2274 ],
2275 vec![not_found_html_config()],
2276 )
2277 .unwrap();
2278 let mut expected_response = expected_index_html_response();
2279
2280 let response = asset_router
2281 .serve_asset(&data_certificate(), &request)
2282 .unwrap();
2283 let (witness, expr_path) = extract_witness_expr_path(&response);
2284 add_v2_certificate_header(
2285 &data_certificate(),
2286 &mut expected_response,
2287 &witness,
2288 &expr_path,
2289 );
2290
2291 assert_eq!(expr_path, vec!["http_expr", "", "<*>"]);
2292 assert_matches!(
2293 witness.lookup_subtree(&expr_path),
2294 SubtreeLookupResult::Found(_)
2295 );
2296 assert_eq!(response, expected_response);
2297
2298 asset_router
2299 .delete_assets(
2300 vec![
2301 Asset::new("index.html", index_html_body()),
2302 Asset::new("index.html.gz", index_html_gz_body()),
2303 Asset::new("index.html.zz", index_html_zz_body()),
2304 Asset::new("index.html.br", index_html_br_body()),
2305 ],
2306 vec![index_html_config()],
2307 )
2308 .unwrap();
2309
2310 let result = asset_router.serve_asset(&data_certificate(), &request);
2311 assert_matches!(
2312 result,
2313 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2314 request_url,
2315 }) if request_url == request.get_path().unwrap()
2316 );
2317 }
2318
2319 #[rstest]
2320 #[case(
2321 app_js_zz_body(),
2322 not_found_html_zz_body(),
2323 index_html_zz_body(),
2324 "deflate",
2325 "deflate"
2326 )]
2327 #[case(
2328 app_js_zz_body(),
2329 not_found_html_zz_body(),
2330 index_html_zz_body(),
2331 "deflate, identity",
2332 "deflate"
2333 )]
2334 #[case(
2335 app_js_zz_body(),
2336 not_found_html_zz_body(),
2337 index_html_zz_body(),
2338 "identity, deflate",
2339 "deflate"
2340 )]
2341 #[case(
2342 app_js_gz_body(),
2343 not_found_html_gz_body(),
2344 index_html_gz_body(),
2345 "gzip",
2346 "gzip"
2347 )]
2348 #[case(
2349 app_js_gz_body(),
2350 not_found_html_gz_body(),
2351 index_html_gz_body(),
2352 "gzip, identity",
2353 "gzip"
2354 )]
2355 #[case(
2356 app_js_gz_body(),
2357 not_found_html_gz_body(),
2358 index_html_gz_body(),
2359 "identity, gzip",
2360 "gzip"
2361 )]
2362 #[case(
2363 app_js_gz_body(),
2364 not_found_html_gz_body(),
2365 index_html_gz_body(),
2366 "gzip, deflate",
2367 "gzip"
2368 )]
2369 #[case(
2370 app_js_gz_body(),
2371 not_found_html_gz_body(),
2372 index_html_gz_body(),
2373 "deflate, gzip",
2374 "gzip"
2375 )]
2376 #[case(
2377 app_js_gz_body(),
2378 not_found_html_gz_body(),
2379 index_html_gz_body(),
2380 "gzip, deflate, identity",
2381 "gzip"
2382 )]
2383 #[case(
2384 app_js_gz_body(),
2385 not_found_html_gz_body(),
2386 index_html_gz_body(),
2387 "gzip, identity, deflate",
2388 "gzip"
2389 )]
2390 #[case(
2391 app_js_gz_body(),
2392 not_found_html_gz_body(),
2393 index_html_gz_body(),
2394 "identity, gzip, deflate",
2395 "gzip"
2396 )]
2397 #[case(
2398 app_js_gz_body(),
2399 not_found_html_gz_body(),
2400 index_html_gz_body(),
2401 "identity, deflate, gzip",
2402 "gzip"
2403 )]
2404 #[case(
2405 app_js_gz_body(),
2406 not_found_html_gz_body(),
2407 index_html_gz_body(),
2408 "deflate, gzip, identity",
2409 "gzip"
2410 )]
2411 #[case(
2412 app_js_gz_body(),
2413 not_found_html_gz_body(),
2414 index_html_gz_body(),
2415 "deflate, identity, gzip",
2416 "gzip"
2417 )]
2418 #[case(
2419 app_js_br_body(),
2420 not_found_html_br_body(),
2421 index_html_br_body(),
2422 "br",
2423 "br"
2424 )]
2425 #[case(
2426 app_js_br_body(),
2427 not_found_html_br_body(),
2428 index_html_br_body(),
2429 "br, gzip, deflate, identity",
2430 "br"
2431 )]
2432 #[case(
2433 app_js_br_body(),
2434 not_found_html_br_body(),
2435 index_html_br_body(),
2436 "gzip, deflate, identity, br",
2437 "br"
2438 )]
2439 fn test_encoded_app_js(
2440 #[case] expected_body: Vec<u8>,
2441 #[case] expected_not_found_body: Vec<u8>,
2442 #[case] expected_index_body: Vec<u8>,
2443 #[case] accept_encoding: &str,
2444 #[case] expected_encoding: &str,
2445 mut asset_router: AssetRouter,
2446 ) {
2447 let request = HttpRequest::get("/js/app-488df671.js")
2448 .with_headers(vec![(
2449 "accept-encoding".to_string(),
2450 accept_encoding.to_string(),
2451 )])
2452 .build();
2453
2454 let mut expected_response = build_200_response(
2455 expected_body,
2456 encoded_asset_cel_expr(),
2457 vec![
2458 (
2459 "cache-control".to_string(),
2460 "public, max-age=31536000, immutable".to_string(),
2461 ),
2462 ("content-type".to_string(), "text/javascript".to_string()),
2463 (
2464 "content-encoding".to_string(),
2465 expected_encoding.to_string(),
2466 ),
2467 ],
2468 );
2469
2470 let response = asset_router
2471 .serve_asset(&data_certificate(), &request)
2472 .unwrap();
2473 let (witness, expr_path) = extract_witness_expr_path(&response);
2474 add_v2_certificate_header(
2475 &data_certificate(),
2476 &mut expected_response,
2477 &witness,
2478 &expr_path,
2479 );
2480
2481 assert_eq!(expr_path, vec!["http_expr", "js", "app-488df671.js", "<$>"]);
2482 assert_matches!(
2483 witness.lookup_subtree(&expr_path),
2484 SubtreeLookupResult::Found(_)
2485 );
2486 assert_eq!(response, expected_response);
2487
2488 asset_router
2489 .delete_assets(
2490 vec![
2491 Asset::new("js/app-488df671.js", app_js_body()),
2492 Asset::new("js/app-488df671.js.gz", app_js_gz_body()),
2493 Asset::new("js/app-488df671.js.zz", app_js_zz_body()),
2494 Asset::new("js/app-488df671.js.br", app_js_br_body()),
2495 ],
2496 vec![js_config()],
2497 )
2498 .unwrap();
2499 let mut expected_response = build_response(
2500 StatusCode::NOT_FOUND,
2501 expected_not_found_body,
2502 encoded_asset_cel_expr(),
2503 vec![
2504 (
2505 "cache-control".to_string(),
2506 "public, no-cache, no-store".to_string(),
2507 ),
2508 ("content-type".to_string(), "text/html".to_string()),
2509 (
2510 "content-encoding".to_string(),
2511 expected_encoding.to_string(),
2512 ),
2513 ],
2514 );
2515
2516 let response = asset_router
2517 .serve_asset(&data_certificate(), &request)
2518 .unwrap();
2519 let (witness, expr_path) = extract_witness_expr_path(&response);
2520 add_v2_certificate_header(
2521 &data_certificate(),
2522 &mut expected_response,
2523 &witness,
2524 &expr_path,
2525 );
2526
2527 assert_eq!(expr_path, vec!["http_expr", "js", "<*>"]);
2528 assert_matches!(
2529 witness.lookup_subtree(&expr_path),
2530 SubtreeLookupResult::Found(_)
2531 );
2532 assert_eq!(response, expected_response);
2533
2534 asset_router
2535 .delete_assets(
2536 vec![
2537 Asset::new("not-found.html", not_found_html_body()),
2538 Asset::new("not-found.html.gz", not_found_html_gz_body()),
2539 Asset::new("not-found.html.zz", not_found_html_zz_body()),
2540 Asset::new("not-found.html.br", not_found_html_br_body()),
2541 ],
2542 vec![not_found_html_config()],
2543 )
2544 .unwrap();
2545 let mut expected_response = build_200_response(
2546 expected_index_body,
2547 encoded_asset_cel_expr(),
2548 vec![
2549 (
2550 "cache-control".to_string(),
2551 "public, no-cache, no-store".to_string(),
2552 ),
2553 ("content-type".to_string(), "text/html".to_string()),
2554 (
2555 "content-encoding".to_string(),
2556 expected_encoding.to_string(),
2557 ),
2558 ],
2559 );
2560
2561 let response = asset_router
2562 .serve_asset(&data_certificate(), &request)
2563 .unwrap();
2564 let (witness, expr_path) = extract_witness_expr_path(&response);
2565 add_v2_certificate_header(
2566 &data_certificate(),
2567 &mut expected_response,
2568 &witness,
2569 &expr_path,
2570 );
2571
2572 assert_eq!(expr_path, vec!["http_expr", "", "<*>"]);
2573 assert_matches!(
2574 witness.lookup_subtree(&expr_path),
2575 SubtreeLookupResult::Found(_)
2576 );
2577 assert_eq!(response, expected_response);
2578
2579 asset_router
2580 .delete_assets(
2581 vec![
2582 Asset::new("index.html", index_html_body()),
2583 Asset::new("index.html.gz", index_html_gz_body()),
2584 Asset::new("index.html.zz", index_html_zz_body()),
2585 Asset::new("index.html.br", index_html_br_body()),
2586 ],
2587 vec![index_html_config()],
2588 )
2589 .unwrap();
2590
2591 let result = asset_router.serve_asset(&data_certificate(), &request);
2592 assert_matches!(
2593 result,
2594 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2595 request_url,
2596 }) if request_url == request.get_path().unwrap()
2597 );
2598 }
2599
2600 #[rstest]
2601 #[case("/js/core-7dk12y45.js")]
2602 #[case("https://internetcomputer.org/js/core-7dk12y45.js")]
2603 fn test_not_found_js(mut asset_router: AssetRouter, #[case] req_url: &str) {
2604 let request = HttpRequest::get(req_url).build();
2605 let mut expected_response = build_response(
2606 StatusCode::NOT_FOUND,
2607 not_found_html_body(),
2608 asset_cel_expr(),
2609 vec![
2610 (
2611 "cache-control".to_string(),
2612 "public, no-cache, no-store".to_string(),
2613 ),
2614 ("content-type".to_string(), "text/html".to_string()),
2615 ],
2616 );
2617
2618 let response = asset_router
2619 .serve_asset(&data_certificate(), &request)
2620 .unwrap();
2621 let (witness, expr_path) = extract_witness_expr_path(&response);
2622 add_v2_certificate_header(
2623 &data_certificate(),
2624 &mut expected_response,
2625 &witness,
2626 &expr_path,
2627 );
2628
2629 assert_eq!(expr_path, vec!["http_expr", "js", "<*>"]);
2630 assert_matches!(
2631 witness.lookup_subtree(&expr_path),
2632 SubtreeLookupResult::Found(_)
2633 );
2634 assert_eq!(response, expected_response);
2635
2636 asset_router
2637 .delete_assets(
2638 vec![
2639 Asset::new("not-found.html", not_found_html_body()),
2640 Asset::new("not-found.html.gz", not_found_html_gz_body()),
2641 Asset::new("not-found.html.zz", not_found_html_zz_body()),
2642 Asset::new("not-found.html.br", not_found_html_br_body()),
2643 ],
2644 vec![not_found_html_config()],
2645 )
2646 .unwrap();
2647
2648 let mut expected_response = expected_index_html_response();
2649
2650 let response = asset_router
2651 .serve_asset(&data_certificate(), &request)
2652 .unwrap();
2653 let (witness, expr_path) = extract_witness_expr_path(&response);
2654 add_v2_certificate_header(
2655 &data_certificate(),
2656 &mut expected_response,
2657 &witness,
2658 &expr_path,
2659 );
2660
2661 assert_eq!(expr_path, vec!["http_expr", "", "<*>"]);
2662 assert_matches!(
2663 witness.lookup_subtree(&expr_path),
2664 SubtreeLookupResult::Found(_)
2665 );
2666 assert_eq!(response, expected_response);
2667
2668 asset_router
2669 .delete_assets(
2670 vec![
2671 Asset::new("index.html", index_html_body()),
2672 Asset::new("index.html.gz", index_html_gz_body()),
2673 Asset::new("index.html.zz", index_html_zz_body()),
2674 Asset::new("index.html.br", index_html_br_body()),
2675 ],
2676 vec![index_html_config()],
2677 )
2678 .unwrap();
2679
2680 let result = asset_router.serve_asset(&data_certificate(), &request);
2681 assert_matches!(
2682 result,
2683 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2684 request_url,
2685 }) if request_url == request.get_path().unwrap()
2686 );
2687 }
2688
2689 #[rstest]
2690 #[case("/404")]
2691 #[case("https://internetcomputer.org/404")]
2692 #[case("/404/")]
2693 #[case("https://internetcomputer.org/404/")]
2694 #[case("/404.html")]
2695 #[case("https://internetcomputer.org/404.html")]
2696 #[case("/not-found")]
2697 #[case("https://internetcomputer.org/not-found")]
2698 #[case("/not-found/")]
2699 #[case("https://internetcomputer.org/not-found/")]
2700 #[case("/not-found/index.html")]
2701 #[case("https://internetcomputer.org/not-found/index.html")]
2702 fn test_not_found_alias(mut asset_router: AssetRouter, #[case] req_url: &str) {
2703 let request = HttpRequest::get(req_url).build();
2704 let mut expected_response = build_200_response(
2705 not_found_html_body(),
2706 asset_cel_expr(),
2707 vec![
2708 (
2709 "cache-control".to_string(),
2710 "public, no-cache, no-store".to_string(),
2711 ),
2712 ("content-type".to_string(), "text/html".to_string()),
2713 ],
2714 );
2715
2716 let response = asset_router
2717 .serve_asset(&data_certificate(), &request)
2718 .unwrap();
2719 let (witness, expr_path) = extract_witness_expr_path(&response);
2720 add_v2_certificate_header(
2721 &data_certificate(),
2722 &mut expected_response,
2723 &witness,
2724 &expr_path,
2725 );
2726
2727 assert_eq!(
2728 expr_path,
2729 HttpCertificationPath::exact(request.get_path().unwrap()).to_expr_path()
2730 );
2731 assert_matches!(
2732 witness.lookup_subtree(&expr_path),
2733 SubtreeLookupResult::Found(_)
2734 );
2735 assert_eq!(response, expected_response);
2736
2737 asset_router
2738 .delete_assets(
2739 vec![
2740 Asset::new("not-found.html", not_found_html_body()),
2741 Asset::new("not-found.html.gz", not_found_html_gz_body()),
2742 Asset::new("not-found.html.zz", not_found_html_zz_body()),
2743 Asset::new("not-found.html.br", not_found_html_br_body()),
2744 ],
2745 vec![not_found_html_config()],
2746 )
2747 .unwrap();
2748
2749 let mut expected_response = expected_index_html_response();
2750
2751 let response = asset_router
2752 .serve_asset(&data_certificate(), &request)
2753 .unwrap();
2754 let (witness, expr_path) = extract_witness_expr_path(&response);
2755 add_v2_certificate_header(
2756 &data_certificate(),
2757 &mut expected_response,
2758 &witness,
2759 &expr_path,
2760 );
2761
2762 assert_eq!(expr_path, vec!["http_expr", "", "<*>"]);
2763 assert_matches!(
2764 witness.lookup_subtree(&expr_path),
2765 SubtreeLookupResult::Found(_)
2766 );
2767 assert_eq!(response, expected_response);
2768
2769 asset_router
2770 .delete_assets(
2771 vec![
2772 Asset::new("index.html", index_html_body()),
2773 Asset::new("index.html.gz", index_html_gz_body()),
2774 Asset::new("index.html.zz", index_html_zz_body()),
2775 Asset::new("index.html.br", index_html_br_body()),
2776 ],
2777 vec![index_html_config()],
2778 )
2779 .unwrap();
2780
2781 let result = asset_router.serve_asset(&data_certificate(), &request);
2782 assert_matches!(
2783 result,
2784 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2785 request_url,
2786 }) if request_url == request.get_path().unwrap()
2787 );
2788 }
2789
2790 #[rstest]
2791 fn test_delete_all_assets() {
2792 let mut asset_router = asset_router();
2793
2794 let request = HttpRequest::get("/index.html").build();
2795
2796 let mut expected_response = expected_index_html_response();
2797
2798 let response = asset_router
2799 .serve_asset(&data_certificate(), &request)
2800 .unwrap();
2801 let (witness, expr_path) = extract_witness_expr_path(&response);
2802 add_v2_certificate_header(
2803 &data_certificate(),
2804 &mut expected_response,
2805 &witness,
2806 &expr_path,
2807 );
2808
2809 assert_eq!(response, expected_response);
2810
2811 asset_router.delete_all_assets();
2812
2813 assert_matches!(
2814 asset_router.serve_asset(
2815 &data_certificate(),
2816 &request,
2817 ),
2818 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2819 request_url,
2820 }) if request_url == "/index.html"
2821 );
2822
2823 let assets: Vec<_> = asset_router.get_assets().iter().collect();
2824 assert!(assets.is_empty());
2825 }
2826
2827 #[rstest]
2828 fn test_delete_by_path() {
2829 let mut asset_router = asset_router();
2830
2831 let index_request = HttpRequest::get("/index.html").build();
2832 let mut expected_index_response = expected_index_html_response();
2833 let index_response = asset_router
2834 .serve_asset(&data_certificate(), &index_request)
2835 .unwrap();
2836 let (witness, expr_path) = extract_witness_expr_path(&index_response);
2837 add_v2_certificate_header(
2838 &data_certificate(),
2839 &mut expected_index_response,
2840 &witness,
2841 &expr_path,
2842 );
2843 assert_eq!(index_response, expected_index_response);
2844
2845 let alias_index_request = HttpRequest::get("/").build();
2846 let mut expected_alias_index_response = expected_index_html_response();
2847 let alias_index_response = asset_router
2848 .serve_asset(&data_certificate(), &alias_index_request)
2849 .unwrap();
2850 let (witness, expr_path) = extract_witness_expr_path(&alias_index_response);
2851 add_v2_certificate_header(
2852 &data_certificate(),
2853 &mut expected_alias_index_response,
2854 &witness,
2855 &expr_path,
2856 );
2857 assert_eq!(alias_index_response, expected_alias_index_response);
2858
2859 let fallback_index_request = HttpRequest::get("/non-existent").build();
2860 let mut expected_fallback_index_response = expected_index_html_response();
2861 let fallback_index_response = asset_router
2862 .serve_asset(&data_certificate(), &fallback_index_request)
2863 .unwrap();
2864 let (witness, expr_path) = extract_witness_expr_path(&fallback_index_response);
2865 add_v2_certificate_header(
2866 &data_certificate(),
2867 &mut expected_fallback_index_response,
2868 &witness,
2869 &expr_path,
2870 );
2871 assert_eq!(fallback_index_response, expected_fallback_index_response);
2872
2873 asset_router.delete_fallback_assets_by_path(vec!["/"]);
2874
2875 let index_request = HttpRequest::get("/index.html").build();
2876 let mut expected_index_response = expected_index_html_response();
2877 let index_response = asset_router
2878 .serve_asset(&data_certificate(), &index_request)
2879 .unwrap();
2880 let (witness, expr_path) = extract_witness_expr_path(&index_response);
2881 add_v2_certificate_header(
2882 &data_certificate(),
2883 &mut expected_index_response,
2884 &witness,
2885 &expr_path,
2886 );
2887 assert_eq!(index_response, expected_index_response);
2888
2889 let alias_index_request = HttpRequest::get("/").build();
2890 let mut expected_alias_index_response = expected_index_html_response();
2891 let alias_index_response = asset_router
2892 .serve_asset(&data_certificate(), &alias_index_request)
2893 .unwrap();
2894 let (witness, expr_path) = extract_witness_expr_path(&alias_index_response);
2895 add_v2_certificate_header(
2896 &data_certificate(),
2897 &mut expected_alias_index_response,
2898 &witness,
2899 &expr_path,
2900 );
2901 assert_eq!(alias_index_response, expected_alias_index_response);
2902
2903 assert_matches!(
2904 asset_router.serve_asset(
2905 &data_certificate(),
2906 &fallback_index_request,
2907 ),
2908 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2909 request_url,
2910 }) if request_url == "/non-existent"
2911 );
2912
2913 asset_router.delete_assets_by_path(vec!["/index.html", "/"]);
2914
2915 assert_matches!(
2916 asset_router.serve_asset(
2917 &data_certificate(),
2918 &index_request,
2919 ),
2920 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2921 request_url,
2922 }) if request_url == "/index.html"
2923 );
2924 assert_matches!(
2925 asset_router.serve_asset(
2926 &data_certificate(),
2927 &alias_index_request,
2928 ),
2929 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2930 request_url,
2931 }) if request_url == "/"
2932 );
2933 assert_matches!(
2934 asset_router.serve_asset(
2935 &data_certificate(),
2936 &fallback_index_request,
2937 ),
2938 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
2939 request_url,
2940 }) if request_url == "/non-existent"
2941 );
2942 }
2943
2944 #[rstest]
2945 fn test_asset_map(mut asset_router: AssetRouter) {
2946 let index_html_response = asset_router.get_assets().get("/index.html", None, None);
2947 assert_matches!(
2948 index_html_response,
2949 Some(index_html_response) if index_html_response == &expected_index_html_response()
2950 );
2951
2952 let index_html_fallback_response = asset_router.get_fallback_assets().get("/", None, None);
2953 assert_matches!(
2954 index_html_fallback_response,
2955 Some(index_html_fallback_response) if index_html_fallback_response == &expected_index_html_response()
2956 );
2957
2958 let index_html_gz_response =
2959 asset_router
2960 .get_assets()
2961 .get("/index.html", Some(AssetEncoding::Gzip), None);
2962 assert_matches!(
2963 index_html_gz_response,
2964 Some(index_html_gz_response) if index_html_gz_response == &expected_index_html_gz_response()
2965 );
2966
2967 let index_html_gz_fallback_response =
2968 asset_router
2969 .get_fallback_assets()
2970 .get("/", Some(AssetEncoding::Gzip), None);
2971 assert_matches!(
2972 index_html_gz_fallback_response,
2973 Some(index_html_gz_fallback_response) if index_html_gz_fallback_response == &expected_index_html_gz_response()
2974 );
2975
2976 let index_html_zz_response =
2977 asset_router
2978 .get_assets()
2979 .get("/index.html", Some(AssetEncoding::Deflate), None);
2980 assert_matches!(
2981 index_html_zz_response,
2982 Some(index_html_zz_response) if index_html_zz_response == &expected_index_html_zz_response()
2983 );
2984
2985 let index_html_zz_fallback_response =
2986 asset_router
2987 .get_fallback_assets()
2988 .get("/", Some(AssetEncoding::Deflate), None);
2989 assert_matches!(
2990 index_html_zz_fallback_response,
2991 Some(index_html_zz_fallback_response) if index_html_zz_fallback_response == &expected_index_html_zz_response()
2992 );
2993
2994 let index_html_br_response =
2995 asset_router
2996 .get_assets()
2997 .get("/index.html", Some(AssetEncoding::Brotli), None);
2998 assert_matches!(
2999 index_html_br_response,
3000 Some(index_html_br_response) if index_html_br_response == &expected_index_html_br_response()
3001 );
3002
3003 let index_html_br_fallback_response =
3004 asset_router
3005 .get_fallback_assets()
3006 .get("/", Some(AssetEncoding::Brotli), None);
3007 assert_matches!(
3008 index_html_br_fallback_response,
3009 Some(index_html_br_fallback_response) if index_html_br_fallback_response == &expected_index_html_br_response()
3010 );
3011
3012 asset_router
3013 .delete_assets(
3014 vec![
3015 Asset::new("index.html", index_html_body()),
3016 Asset::new("index.html.gz", index_html_gz_body()),
3017 Asset::new("index.html.zz", index_html_zz_body()),
3018 Asset::new("index.html.br", index_html_br_body()),
3019 ],
3020 vec![index_html_config()],
3021 )
3022 .unwrap();
3023
3024 let index_html_response = asset_router.get_assets().get("/index.html", None, None);
3025 assert_matches!(index_html_response, None);
3026
3027 let index_html_fallback_response = asset_router.get_fallback_assets().get("/", None, None);
3028 assert_matches!(index_html_fallback_response, None);
3029
3030 let index_html_gz_response =
3031 asset_router
3032 .get_assets()
3033 .get("/index.html", Some(AssetEncoding::Gzip), None);
3034 assert_matches!(index_html_gz_response, None);
3035
3036 let index_html_gz_fallback_response =
3037 asset_router
3038 .get_fallback_assets()
3039 .get("/", Some(AssetEncoding::Gzip), None);
3040 assert_matches!(index_html_gz_fallback_response, None);
3041
3042 let index_html_zz_response =
3043 asset_router
3044 .get_assets()
3045 .get("/index.html", Some(AssetEncoding::Deflate), None);
3046 assert_matches!(index_html_zz_response, None);
3047
3048 let index_html_zz_fallback_response =
3049 asset_router
3050 .get_fallback_assets()
3051 .get("/", Some(AssetEncoding::Deflate), None);
3052 assert_matches!(index_html_zz_fallback_response, None);
3053
3054 let index_html_br_response =
3055 asset_router
3056 .get_assets()
3057 .get("/index.html", Some(AssetEncoding::Brotli), None);
3058 assert_matches!(index_html_br_response, None);
3059
3060 let index_html_br_fallback_response =
3061 asset_router
3062 .get_fallback_assets()
3063 .get("/", Some(AssetEncoding::Brotli), None);
3064 assert_matches!(index_html_br_fallback_response, None);
3065 }
3066
3067 #[rstest]
3068 fn test_asset_map_chunked_responses() {
3069 let mut asset_router = long_asset_router_with_params(
3070 &[TWO_CHUNKS_ASSET_NAME],
3071 &[AssetEncoding::Gzip, AssetEncoding::Identity],
3072 );
3073
3074 let full_body = long_asset_body(TWO_CHUNKS_ASSET_NAME);
3075 let (_, gzip_suffix) = AssetEncoding::Gzip.default_config();
3076 let full_gz_body = long_asset_body(&format!("{TWO_CHUNKS_ASSET_NAME}{gzip_suffix}"));
3077
3078 let first_chunk_body = &full_body[0..ASSET_CHUNK_SIZE];
3079 let first_chunk_response =
3080 asset_router
3081 .get_assets()
3082 .get(format!("/{TWO_CHUNKS_ASSET_NAME}"), None, Some(0));
3083 let expected_first_chunk_response = build_206_response(
3084 first_chunk_body.to_vec(),
3085 asset_cel_expr(),
3086 vec![
3087 (
3088 "cache-control".to_string(),
3089 "public, no-cache, no-store".to_string(),
3090 ),
3091 ("content-type".to_string(), "text/html".to_string()),
3092 (
3093 "content-range".to_string(),
3094 format!("bytes 0-{}/{}", first_chunk_body.len() - 1, full_body.len()),
3095 ),
3096 ],
3097 );
3098 assert_matches!(
3099 first_chunk_response,
3100 Some(first_chunk_response) if first_chunk_response == &expected_first_chunk_response
3101 );
3102
3103 let first_chunk_gzip_body = &full_gz_body[0..ASSET_CHUNK_SIZE];
3104 let first_chunk_gzip_response = asset_router.get_assets().get(
3105 format!("/{TWO_CHUNKS_ASSET_NAME}"),
3106 Some(AssetEncoding::Gzip),
3107 Some(0),
3108 );
3109 let expected_first_chunk_gzip_response = build_206_response(
3110 first_chunk_gzip_body.to_vec(),
3111 encoded_asset_cel_expr(),
3112 vec![
3113 (
3114 "cache-control".to_string(),
3115 "public, no-cache, no-store".to_string(),
3116 ),
3117 ("content-type".to_string(), "text/html".to_string()),
3118 ("content-encoding".to_string(), "gzip".to_string()),
3119 (
3120 "content-range".to_string(),
3121 format!(
3122 "bytes 0-{}/{}",
3123 first_chunk_gzip_body.len() - 1,
3124 full_gz_body.len()
3125 ),
3126 ),
3127 ],
3128 );
3129 assert_matches!(
3130 first_chunk_gzip_response,
3131 Some(first_chunk_gzip_response) if first_chunk_gzip_response == &expected_first_chunk_gzip_response
3132 );
3133
3134 let second_chunk_body = &full_body[ASSET_CHUNK_SIZE..full_body.len()];
3135 let second_chunk_response = asset_router.get_assets().get(
3136 format!("/{TWO_CHUNKS_ASSET_NAME}"),
3137 None,
3138 Some(ASSET_CHUNK_SIZE),
3139 );
3140 let expected_second_chunk_response = build_206_response(
3141 second_chunk_body.to_vec(),
3142 asset_range_chunk_cel_expr(),
3143 vec![
3144 (
3145 "cache-control".to_string(),
3146 "public, no-cache, no-store".to_string(),
3147 ),
3148 ("content-type".to_string(), "text/html".to_string()),
3149 (
3150 "content-range".to_string(),
3151 format!(
3152 "bytes {}-{}/{}",
3153 first_chunk_body.len(),
3154 first_chunk_body.len() + second_chunk_body.len() - 1,
3155 full_body.len()
3156 ),
3157 ),
3158 ],
3159 );
3160 assert_matches!(
3161 second_chunk_response,
3162 Some(second_chunk_response) if second_chunk_response == &expected_second_chunk_response
3163 );
3164
3165 let second_chunk_gzip_body = &full_gz_body[ASSET_CHUNK_SIZE..full_gz_body.len()];
3166 let second_chunk_gzip_response = asset_router.get_assets().get(
3167 format!("/{TWO_CHUNKS_ASSET_NAME}"),
3168 Some(AssetEncoding::Gzip),
3169 Some(ASSET_CHUNK_SIZE),
3170 );
3171 let expected_second_chunk_gzip_response = build_206_response(
3172 second_chunk_gzip_body.to_vec(),
3173 asset_range_chunk_cel_expr(),
3174 vec![
3175 (
3176 "cache-control".to_string(),
3177 "public, no-cache, no-store".to_string(),
3178 ),
3179 ("content-type".to_string(), "text/html".to_string()),
3180 ("content-encoding".to_string(), "gzip".to_string()),
3181 (
3182 "content-range".to_string(),
3183 format!(
3184 "bytes {}-{}/{}",
3185 first_chunk_gzip_body.len(),
3186 first_chunk_gzip_body.len() + second_chunk_gzip_body.len() - 1,
3187 full_gz_body.len()
3188 ),
3189 ),
3190 ],
3191 );
3192
3193 assert_matches!(
3194 second_chunk_gzip_response,
3195 Some(second_chunk_gzip_response) if second_chunk_gzip_response == &expected_second_chunk_gzip_response
3196 );
3197
3198 asset_router
3199 .delete_assets(
3200 vec![
3201 long_asset(TWO_CHUNKS_ASSET_NAME.to_string()),
3202 long_asset(format!("{TWO_CHUNKS_ASSET_NAME}{gzip_suffix}")),
3203 ],
3204 vec![
3205 long_asset_config(TWO_CHUNKS_ASSET_NAME),
3206 long_asset_config(&format!("{TWO_CHUNKS_ASSET_NAME}{gzip_suffix}")),
3207 ],
3208 )
3209 .unwrap();
3210
3211 let first_chunk_response =
3212 asset_router
3213 .get_assets()
3214 .get(format!("/{TWO_CHUNKS_ASSET_NAME}"), None, Some(0));
3215 assert_matches!(first_chunk_response, None);
3216
3217 let first_chunk_gzip_response = asset_router.get_assets().get(
3218 format!("/{TWO_CHUNKS_ASSET_NAME}"),
3219 Some(AssetEncoding::Gzip),
3220 Some(0),
3221 );
3222 assert_matches!(first_chunk_gzip_response, None);
3223
3224 let second_chunk_response = asset_router.get_assets().get(
3225 format!("/{TWO_CHUNKS_ASSET_NAME}"),
3226 None,
3227 Some(ASSET_CHUNK_SIZE),
3228 );
3229 assert_matches!(second_chunk_response, None);
3230
3231 let second_chunk_gzip_response = asset_router.get_assets().get(
3232 format!("/{TWO_CHUNKS_ASSET_NAME}"),
3233 Some(AssetEncoding::Gzip),
3234 Some(ASSET_CHUNK_SIZE),
3235 );
3236 assert_matches!(second_chunk_gzip_response, None);
3237 }
3238
3239 #[rstest]
3240 fn test_asset_map_iter() {
3241 let asset_router =
3242 long_asset_router_with_params(&[TWO_CHUNKS_ASSET_NAME], &[AssetEncoding::Identity]);
3243 let full_body = long_asset_body(TWO_CHUNKS_ASSET_NAME);
3244
3245 let assets: Vec<_> = asset_router.get_assets().iter().collect();
3246 assert!(assets.len() == 3);
3247
3248 let first_chunk_body = &full_body[0..ASSET_CHUNK_SIZE];
3249 let expected_first_chunk_response = build_206_response(
3250 first_chunk_body.to_vec(),
3251 asset_cel_expr(),
3252 vec![
3253 (
3254 "cache-control".to_string(),
3255 "public, no-cache, no-store".to_string(),
3256 ),
3257 ("content-type".to_string(), "text/html".to_string()),
3258 (
3259 "content-range".to_string(),
3260 format!("bytes 0-{}/{}", first_chunk_body.len() - 1, full_body.len()),
3261 ),
3262 ],
3263 );
3264
3265 assert!(assets.contains(&(
3266 (&format!("/{TWO_CHUNKS_ASSET_NAME}"), None, Some(0)),
3267 &expected_first_chunk_response
3268 )));
3269
3270 let second_chunk_body = &full_body[ASSET_CHUNK_SIZE..full_body.len()];
3271 let expected_second_chunk_response = build_206_response(
3272 second_chunk_body.to_vec(),
3273 asset_range_chunk_cel_expr(),
3274 vec![
3275 (
3276 "cache-control".to_string(),
3277 "public, no-cache, no-store".to_string(),
3278 ),
3279 ("content-type".to_string(), "text/html".to_string()),
3280 (
3281 "content-range".to_string(),
3282 format!(
3283 "bytes {}-{}/{}",
3284 first_chunk_body.len(),
3285 first_chunk_body.len() + second_chunk_body.len() - 1,
3286 full_body.len()
3287 ),
3288 ),
3289 ],
3290 );
3291 assert!(assets.contains(&(
3292 (
3293 &format!("/{TWO_CHUNKS_ASSET_NAME}"),
3294 None,
3295 Some(ASSET_CHUNK_SIZE)
3296 ),
3297 &expected_second_chunk_response
3298 )));
3299
3300 let expected_full_response = build_200_response(
3301 full_body,
3302 asset_cel_expr(),
3303 vec![
3304 (
3305 "cache-control".to_string(),
3306 "public, no-cache, no-store".to_string(),
3307 ),
3308 ("content-type".to_string(), "text/html".to_string()),
3309 ],
3310 );
3311 assert!(assets.contains(&(
3312 (&format!("/{TWO_CHUNKS_ASSET_NAME}"), None, None),
3313 &expected_full_response
3314 )));
3315 }
3316
3317 #[rstest]
3318 fn test_redirects(mut asset_router: AssetRouter) {
3319 let cel_expr = DefaultFullCelExpressionBuilder::default()
3320 .with_response_certification(DefaultResponseCertification::response_header_exclusions(
3321 vec![],
3322 ))
3323 .build()
3324 .to_string();
3325
3326 let css_request = HttpRequest::get("/css/app.css").build();
3327 let old_url_request = HttpRequest::get("/old-url").build();
3328
3329 let mut expected_css_response = HttpResponse::builder()
3330 .with_status_code(StatusCode::TEMPORARY_REDIRECT)
3331 .with_headers(vec![
3332 ("content-length".to_string(), "0".to_string()),
3333 ("location".to_string(), "/css/app-ba74b708.css".to_string()),
3334 (
3335 CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),
3336 cel_expr.clone(),
3337 ),
3338 (
3339 "content-type".to_string(),
3340 "text/plain; charset=utf-8".to_string(),
3341 ),
3342 ])
3343 .build();
3344 let mut expected_old_url_response = HttpResponse::builder()
3345 .with_status_code(StatusCode::MOVED_PERMANENTLY)
3346 .with_headers(vec![
3347 ("content-length".to_string(), "0".to_string()),
3348 ("location".to_string(), "/".to_string()),
3349 (CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(), cel_expr),
3350 (
3351 "content-type".to_string(),
3352 "text/plain; charset=utf-8".to_string(),
3353 ),
3354 ])
3355 .build();
3356
3357 let css_response = asset_router
3358 .serve_asset(&data_certificate(), &css_request)
3359 .unwrap();
3360 let (css_witness, css_expr_path) = extract_witness_expr_path(&css_response);
3361 add_v2_certificate_header(
3362 &data_certificate(),
3363 &mut expected_css_response,
3364 &css_witness,
3365 &css_expr_path,
3366 );
3367 let old_url_response = asset_router
3368 .serve_asset(&data_certificate(), &old_url_request)
3369 .unwrap();
3370 let (old_url_witness, old_url_expr_path) = extract_witness_expr_path(&old_url_response);
3371 add_v2_certificate_header(
3372 &data_certificate(),
3373 &mut expected_old_url_response,
3374 &old_url_witness,
3375 &old_url_expr_path,
3376 );
3377
3378 assert_eq!(css_expr_path, vec!["http_expr", "css", "app.css", "<$>"]);
3379 assert_matches!(
3380 css_witness.lookup_subtree(&css_expr_path),
3381 SubtreeLookupResult::Found(_)
3382 );
3383 assert_eq!(css_response, expected_css_response);
3384
3385 assert_eq!(old_url_expr_path, vec!["http_expr", "old-url", "<$>"]);
3386 assert_matches!(
3387 old_url_witness.lookup_subtree(&old_url_expr_path),
3388 SubtreeLookupResult::Found(_)
3389 );
3390 assert_eq!(old_url_response, expected_old_url_response);
3391
3392 asset_router
3393 .delete_assets(
3394 vec![],
3395 vec![old_url_redirect_config(), css_redirect_config()],
3396 )
3397 .unwrap();
3398 let mut expected_css_response = build_response(
3399 StatusCode::NOT_FOUND,
3400 not_found_html_body(),
3401 asset_cel_expr(),
3402 vec![
3403 (
3404 "cache-control".to_string(),
3405 "public, no-cache, no-store".to_string(),
3406 ),
3407 ("content-type".to_string(), "text/html".to_string()),
3408 ],
3409 );
3410 let mut expected_old_url_response = expected_index_html_response();
3411
3412 let css_response = asset_router
3413 .serve_asset(&data_certificate(), &css_request)
3414 .unwrap();
3415 let (css_witness, css_expr_path) = extract_witness_expr_path(&css_response);
3416 add_v2_certificate_header(
3417 &data_certificate(),
3418 &mut expected_css_response,
3419 &css_witness,
3420 &css_expr_path,
3421 );
3422 let old_url_response = asset_router
3423 .serve_asset(&data_certificate(), &old_url_request)
3424 .unwrap();
3425 let (old_url_witness, old_url_expr_path) = extract_witness_expr_path(&old_url_response);
3426 add_v2_certificate_header(
3427 &data_certificate(),
3428 &mut expected_old_url_response,
3429 &old_url_witness,
3430 &old_url_expr_path,
3431 );
3432
3433 assert_eq!(css_expr_path, vec!["http_expr", "css", "<*>"]);
3434 assert_matches!(
3435 css_witness.lookup_subtree(&css_expr_path),
3436 SubtreeLookupResult::Found(_)
3437 );
3438 assert_eq!(css_response, expected_css_response);
3439
3440 assert_eq!(old_url_expr_path, vec!["http_expr", "", "<*>"]);
3441 assert_matches!(
3442 old_url_witness.lookup_subtree(&old_url_expr_path),
3443 SubtreeLookupResult::Found(_)
3444 );
3445 assert_eq!(old_url_response, expected_old_url_response);
3446
3447 asset_router
3448 .delete_assets(
3449 vec![
3450 Asset::new("not-found.html", not_found_html_body()),
3451 Asset::new("not-found.html.gz", not_found_html_gz_body()),
3452 Asset::new("not-found.html.zz", not_found_html_zz_body()),
3453 Asset::new("not-found.html.br", not_found_html_br_body()),
3454 ],
3455 vec![not_found_html_config()],
3456 )
3457 .unwrap();
3458 let mut expected_css_response = expected_index_html_response();
3459 let mut expected_old_url_response = expected_index_html_response();
3460
3461 let css_response = asset_router
3462 .serve_asset(&data_certificate(), &css_request)
3463 .unwrap();
3464 let (css_witness, css_expr_path) = extract_witness_expr_path(&css_response);
3465 add_v2_certificate_header(
3466 &data_certificate(),
3467 &mut expected_css_response,
3468 &css_witness,
3469 &css_expr_path,
3470 );
3471 let old_url_response = asset_router
3472 .serve_asset(&data_certificate(), &old_url_request)
3473 .unwrap();
3474 let (old_url_witness, old_url_expr_path) = extract_witness_expr_path(&old_url_response);
3475 add_v2_certificate_header(
3476 &data_certificate(),
3477 &mut expected_old_url_response,
3478 &old_url_witness,
3479 &old_url_expr_path,
3480 );
3481
3482 assert_eq!(css_expr_path, vec!["http_expr", "", "<*>"]);
3483 assert_matches!(
3484 css_witness.lookup_subtree(&css_expr_path),
3485 SubtreeLookupResult::Found(_)
3486 );
3487 assert_eq!(css_response, expected_css_response);
3488
3489 assert_eq!(old_url_expr_path, vec!["http_expr", "", "<*>"]);
3490 assert_matches!(
3491 old_url_witness.lookup_subtree(&old_url_expr_path),
3492 SubtreeLookupResult::Found(_)
3493 );
3494 assert_eq!(old_url_response, expected_old_url_response);
3495
3496 asset_router
3497 .delete_assets(
3498 vec![
3499 Asset::new("index.html", index_html_body()),
3500 Asset::new("index.html.gz", index_html_gz_body()),
3501 Asset::new("index.html.zz", index_html_zz_body()),
3502 Asset::new("index.html.br", index_html_br_body()),
3503 ],
3504 vec![index_html_config()],
3505 )
3506 .unwrap();
3507
3508 let css_result = asset_router.serve_asset(&data_certificate(), &css_request);
3509 let old_url_result = asset_router.serve_asset(&data_certificate(), &old_url_request);
3510
3511 assert_matches!(
3512 css_result,
3513 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
3514 request_url,
3515 }) if request_url == css_request.get_path().unwrap()
3516 );
3517 assert_matches!(
3518 old_url_result,
3519 Err(AssetCertificationError::NoAssetMatchingRequestUrl {
3520 request_url,
3521 }) if request_url == old_url_request.get_path().unwrap()
3522 );
3523 }
3524
3525 #[rstest]
3526 fn test_init_with_tree(index_html_body: Vec<u8>, asset_cel_expr: String) {
3527 let http_certification_tree: Rc<RefCell<HttpCertificationTree>> = Default::default();
3528 let mut asset_router = AssetRouter::with_tree(http_certification_tree.clone());
3529
3530 let index_html_asset = Asset::new("index.html", &index_html_body);
3531 let index_html_config = AssetConfig::File {
3532 path: "index.html".to_string(),
3533 content_type: Some("text/html".to_string()),
3534 headers: vec![(
3535 "cache-control".to_string(),
3536 "public, no-cache, no-store".to_string(),
3537 )],
3538 fallback_for: vec![AssetFallbackConfig {
3539 scope: "/".to_string(),
3540 status_code: Some(StatusCode::OK),
3541 }],
3542 aliased_by: vec!["/".to_string()],
3543 encodings: vec![],
3544 };
3545
3546 asset_router
3547 .certify_assets(vec![index_html_asset], vec![index_html_config])
3548 .unwrap();
3549
3550 let request = HttpRequest::get("/").build();
3551
3552 let mut expected_response = build_200_response(
3553 index_html_body.clone(),
3554 asset_cel_expr,
3555 vec![
3556 (
3557 "cache-control".to_string(),
3558 "public, no-cache, no-store".to_string(),
3559 ),
3560 ("content-type".to_string(), "text/html".to_string()),
3561 ],
3562 );
3563
3564 let response = asset_router
3565 .serve_asset(&data_certificate(), &request)
3566 .unwrap();
3567 let (witness, expr_path) = extract_witness_expr_path(&response);
3568 add_v2_certificate_header(
3569 &data_certificate(),
3570 &mut expected_response,
3571 &witness,
3572 &expr_path,
3573 );
3574
3575 assert_eq!(expr_path, vec!["http_expr", "", "<$>"]);
3576 assert_matches!(
3577 witness.lookup_subtree(&expr_path),
3578 SubtreeLookupResult::Found(_)
3579 );
3580 assert_eq!(response, expected_response);
3581 assert_eq!(
3582 asset_router.root_hash(),
3583 http_certification_tree.borrow().root_hash()
3584 );
3585 }
3586
3587 fn long_asset_body(asset_name: &str) -> Vec<u8> {
3588 let asset_length = match asset_name {
3589 s if s.contains(ONE_CHUNK_ASSET_NAME) => ONE_CHUNK_ASSET_LEN,
3590 s if s.contains(TWO_CHUNKS_ASSET_NAME) => TWO_CHUNKS_ASSET_LEN,
3591 s if s.contains(SIX_CHUNKS_ASSET_NAME) => SIX_CHUNKS_ASSET_LEN,
3592 s if s.contains(TEN_CHUNKS_ASSET_NAME) => TEN_CHUNKS_ASSET_LEN,
3593 _ => ASSET_CHUNK_SIZE * 3 + 1,
3594 };
3595 let mut rng = ChaCha20Rng::from_seed(hash(asset_name));
3596 let mut body = vec![0u8; asset_length];
3597 rng.fill_bytes(&mut body);
3598 body
3599 }
3600
3601 #[fixture]
3602 fn index_html_body() -> Vec<u8> {
3603 b"<html><body><h1>Hello World!</h1></body></html>".to_vec()
3604 }
3605
3606 #[fixture]
3607 fn expected_index_html_response() -> HttpResponse<'static> {
3608 build_200_response(
3609 index_html_body(),
3610 asset_cel_expr(),
3611 vec![
3612 (
3613 "cache-control".to_string(),
3614 "public, no-cache, no-store".to_string(),
3615 ),
3616 ("content-type".to_string(), "text/html".to_string()),
3617 ],
3618 )
3619 }
3620
3621 #[fixture]
3625 fn index_html_gz_body() -> Vec<u8> {
3626 vec![
3627 31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 179, 201, 40, 201, 205, 177, 179, 73, 202, 79, 169,
3628 180, 179, 201, 48, 180, 243, 72, 205, 201, 201, 87, 8, 207, 47, 202, 73, 81, 180, 209,
3629 7, 10, 216, 232, 67, 228, 244, 193, 10, 1, 28, 178, 8, 152, 47, 0, 0, 0,
3630 ]
3631 }
3632
3633 #[fixture]
3634 fn expected_index_html_gz_response() -> HttpResponse<'static> {
3635 build_200_response(
3636 index_html_gz_body(),
3637 asset_cel_expr(),
3638 vec![
3639 (
3640 "cache-control".to_string(),
3641 "public, no-cache, no-store".to_string(),
3642 ),
3643 ("content-type".to_string(), "text/html".to_string()),
3644 ("content-encoding".to_string(), "gzip".to_string()),
3645 ],
3646 )
3647 }
3648
3649 #[fixture]
3653 fn index_html_zz_body() -> Vec<u8> {
3654 vec![
3655 78, 9, 3, 9, 28, 9, 1, 3, 49, 4, 9, 4, 3, 9, 30, 4, 3, 48, 9, 9, 57, 8, 2, 49, 51, 4,
3656 1, 7, 0, 8, 8, 43, 4, 4, 1, 0, 1, 7, 8, 0, 9,
3657 ]
3658 }
3659
3660 #[fixture]
3661 fn expected_index_html_zz_response() -> HttpResponse<'static> {
3662 build_200_response(
3663 index_html_zz_body(),
3664 asset_cel_expr(),
3665 vec![
3666 (
3667 "cache-control".to_string(),
3668 "public, no-cache, no-store".to_string(),
3669 ),
3670 ("content-type".to_string(), "text/html".to_string()),
3671 ("content-encoding".to_string(), "deflate".to_string()),
3672 ],
3673 )
3674 }
3675
3676 #[fixture]
3680 fn index_html_br_body() -> Vec<u8> {
3681 vec![
3682 1, 2, 0, 8, 1, 9, 36, 2, 72, 65, 4, 25, 0, 5, 84, 0, 5, 18, 1, 64, 14, 5, 4, 1, 9, 0,
3683 3, 4, 42, 2, 1, 59, 13, 14, 3, 19, 69, 18,
3684 ]
3685 }
3686
3687 #[fixture]
3688 fn expected_index_html_br_response() -> HttpResponse<'static> {
3689 build_200_response(
3690 index_html_br_body(),
3691 asset_cel_expr(),
3692 vec![
3693 (
3694 "cache-control".to_string(),
3695 "public, no-cache, no-store".to_string(),
3696 ),
3697 ("content-type".to_string(), "text/html".to_string()),
3698 ("content-encoding".to_string(), "br".to_string()),
3699 ],
3700 )
3701 }
3702
3703 #[fixture]
3704 fn app_js_body() -> Vec<u8> {
3705 b"console.log('Hello World!');".to_vec()
3706 }
3707
3708 #[fixture]
3712 fn app_js_gz_body() -> Vec<u8> {
3713 vec![
3714 31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 206, 207, 43, 206, 207, 73, 213, 203, 201, 79,
3715 215, 80, 247, 72, 205, 201, 201, 87, 8, 207, 47, 202, 73, 81, 84, 215, 180, 6, 0, 186,
3716 42, 111, 142, 28, 0, 0, 0,
3717 ]
3718 }
3719
3720 #[fixture]
3724 fn app_js_zz_body() -> Vec<u8> {
3725 vec![
3726 120, 156, 75, 206, 207, 43, 206, 207, 73, 213, 203, 201, 79, 215, 80, 247, 72, 205,
3727 201, 201, 87, 8, 207, 47, 202, 73, 81, 84, 215, 180, 6, 0, 148, 149, 9, 123,
3728 ]
3729 }
3730
3731 #[fixture]
3735 fn app_js_br_body() -> Vec<u8> {
3736 vec![
3737 8, 0, 80, 63, 6, 6, 73, 6, 6, 65, 2, 6, 6, 67, 28, 27, 48, 65, 6, 6, 6, 20, 57, 6, 72,
3738 6, 64, 21, 27, 29, 3, 3,
3739 ]
3740 }
3741
3742 #[fixture]
3743 fn app_css_body() -> Vec<u8> {
3744 b"html,body{min-height:100vh;}".to_vec()
3745 }
3746
3747 #[fixture]
3748 fn not_found_html_body() -> Vec<u8> {
3749 b"<html><body><h1>404 Not Found!</h1></body></html>".to_vec()
3750 }
3751
3752 #[fixture]
3756 fn not_found_html_gz_body() -> Vec<u8> {
3757 vec![
3758 49, 19, 9, 8, 0, 0, 0, 0, 0, 0, 3, 23, 9, 32, 1, 64, 32, 1, 32, 5, 23, 7, 23, 9, 115,
3759 32, 2, 121, 22, 9, 24, 0, 23, 9, 32, 1, 72, 24, 0, 81, 73, 72, 129, 36, 0, 32, 3, 71,
3760 129, 17, 2, 32, 3, 71, 32, 5, 117, 129, 24, 0, 32, 9, 7, 16, 33, 7, 35, 2, 103, 16, 0,
3761 36, 5, 25, 3, 116, 1, 37, 4, 36, 9, 17, 0, 114, 73, 0, 0, 0,
3762 ]
3763 }
3764
3765 #[fixture]
3769 fn not_found_html_zz_body() -> Vec<u8> {
3770 vec![
3771 120, 156, 179, 201, 40, 201, 205, 177, 179, 73, 202, 79, 169, 180, 179, 201, 48, 180,
3772 51, 49, 48, 81, 240, 203, 47, 81, 112, 203, 47, 205, 75, 81, 180, 209, 7, 10, 217, 232,
3773 67, 100, 245, 193, 74, 1, 133, 210, 15, 136,
3774 ]
3775 }
3776
3777 #[fixture]
3781 fn not_found_html_br_body() -> Vec<u8> {
3782 vec![
3783 27, 48, 0, 248, 45, 14, 108, 27, 88, 249, 245, 45, 213, 3, 233, 193, 146, 199, 9, 173,
3784 64, 104, 10, 230, 173, 67, 124, 216, 218, 84, 12, 93, 47, 66, 139, 48, 3, 233, 78, 128,
3785 105, 198, 36, 242, 83, 62, 179, 122, 129, 33, 16, 12,
3786 ]
3787 }
3788
3789 #[fixture]
3790 fn asset_cel_expr() -> String {
3791 DefaultFullCelExpressionBuilder::default()
3792 .with_response_certification(DefaultResponseCertification::response_header_exclusions(
3793 vec![],
3794 ))
3795 .build()
3796 .to_string()
3797 }
3798
3799 #[fixture]
3800 fn asset_range_chunk_cel_expr() -> String {
3801 DefaultFullCelExpressionBuilder::default()
3802 .with_request_headers(vec!["range"])
3803 .with_response_certification(DefaultResponseCertification::response_header_exclusions(
3804 vec![],
3805 ))
3806 .build()
3807 .to_string()
3808 }
3809
3810 #[fixture]
3811 fn encoded_asset_cel_expr() -> String {
3812 DefaultFullCelExpressionBuilder::default()
3813 .with_response_certification(DefaultResponseCertification::response_header_exclusions(
3814 vec![],
3815 ))
3816 .build()
3817 .to_string()
3818 }
3819
3820 #[fixture]
3821 fn encoded_range_chunk_asset_cel_expr() -> String {
3822 DefaultFullCelExpressionBuilder::default()
3823 .with_request_headers(vec!["range"])
3824 .with_response_certification(DefaultResponseCertification::response_header_exclusions(
3825 vec![],
3826 ))
3827 .build()
3828 .to_string()
3829 }
3830
3831 #[fixture]
3832 fn index_html_config() -> AssetConfig {
3833 AssetConfig::File {
3834 path: "index.html".to_string(),
3835 content_type: Some("text/html".to_string()),
3836 headers: vec![(
3837 "cache-control".to_string(),
3838 "public, no-cache, no-store".to_string(),
3839 )],
3840 fallback_for: vec![AssetFallbackConfig {
3841 scope: "/".to_string(),
3842 status_code: Some(StatusCode::OK),
3843 }],
3844 aliased_by: vec!["/".to_string()],
3845 encodings: vec![
3846 AssetEncoding::Gzip.default_config(),
3847 AssetEncoding::Deflate.default_config(),
3848 AssetEncoding::Brotli.default_config(),
3849 ],
3850 }
3851 }
3852
3853 #[fixture]
3854 fn js_config() -> AssetConfig {
3855 AssetConfig::Pattern {
3856 pattern: r"**/*.js".to_string(),
3857 content_type: Some("text/javascript".to_string()),
3858 headers: vec![(
3859 "cache-control".to_string(),
3860 "public, max-age=31536000, immutable".to_string(),
3861 )],
3862 encodings: vec![
3863 AssetEncoding::Gzip.default_config(),
3864 AssetEncoding::Deflate.default_config(),
3865 AssetEncoding::Brotli.default_config(),
3866 ],
3867 }
3868 }
3869
3870 #[fixture]
3871 fn css_config() -> AssetConfig {
3872 AssetConfig::Pattern {
3873 pattern: "**/*.css".to_string(),
3874 content_type: Some("text/css".to_string()),
3875 headers: vec![(
3876 "cache-control".to_string(),
3877 "public, max-age=31536000, immutable".to_string(),
3878 )],
3879 encodings: vec![
3880 AssetEncoding::Gzip.default_config(),
3881 AssetEncoding::Deflate.default_config(),
3882 AssetEncoding::Brotli.default_config(),
3883 ],
3884 }
3885 }
3886
3887 #[fixture]
3888 fn not_found_html_config() -> AssetConfig {
3889 AssetConfig::File {
3890 path: "not-found.html".to_string(),
3891 content_type: Some("text/html".to_string()),
3892 headers: vec![(
3893 "cache-control".to_string(),
3894 "public, no-cache, no-store".to_string(),
3895 )],
3896 fallback_for: vec![
3897 AssetFallbackConfig {
3898 scope: "/js".to_string(),
3899 status_code: Some(StatusCode::NOT_FOUND),
3900 },
3901 AssetFallbackConfig {
3902 scope: "/css".to_string(),
3903 status_code: Some(StatusCode::NOT_FOUND),
3904 },
3905 ],
3906 aliased_by: vec![
3907 "/404".to_string(),
3908 "/404/".to_string(),
3909 "/404.html".to_string(),
3910 "/not-found".to_string(),
3911 "/not-found/".to_string(),
3912 "/not-found/index.html".to_string(),
3913 ],
3914 encodings: vec![
3915 AssetEncoding::Gzip.default_config(),
3916 AssetEncoding::Deflate.default_config(),
3917 AssetEncoding::Brotli.default_config(),
3918 ],
3919 }
3920 }
3921
3922 #[fixture]
3923 fn old_url_redirect_config() -> AssetConfig {
3924 AssetConfig::Redirect {
3925 from: "/old-url".to_string(),
3926 to: "/".to_string(),
3927 kind: AssetRedirectKind::Permanent,
3928 headers: vec![(
3929 "content-type".to_string(),
3930 "text/plain; charset=utf-8".to_string(),
3931 )],
3932 }
3933 }
3934
3935 #[fixture]
3936 fn css_redirect_config() -> AssetConfig {
3937 AssetConfig::Redirect {
3938 from: "/css/app.css".to_string(),
3939 to: "/css/app-ba74b708.css".to_string(),
3940 kind: AssetRedirectKind::Temporary,
3941 headers: vec![(
3942 "content-type".to_string(),
3943 "text/plain; charset=utf-8".to_string(),
3944 )],
3945 }
3946 }
3947
3948 fn long_asset_config(path: &str) -> AssetConfig {
3949 AssetConfig::File {
3950 path: path.to_string(),
3951 content_type: Some("text/html".to_string()),
3952 headers: vec![(
3953 "cache-control".to_string(),
3954 "public, no-cache, no-store".to_string(),
3955 )],
3956 fallback_for: vec![],
3957 aliased_by: vec![],
3958 encodings: vec![
3959 AssetEncoding::Brotli.default_config(),
3960 AssetEncoding::Deflate.default_config(),
3961 AssetEncoding::Gzip.default_config(),
3962 ],
3963 }
3964 }
3965
3966 #[fixture]
3967 fn asset_router<'a>() -> AssetRouter<'a> {
3968 let mut asset_router = AssetRouter::default();
3969
3970 let assets = vec![
3971 Asset::new("index.html", index_html_body()),
3972 Asset::new("index.html.gz", index_html_gz_body()),
3973 Asset::new("index.html.zz", index_html_zz_body()),
3974 Asset::new("index.html.br", index_html_br_body()),
3975 Asset::new("js/app-488df671.js", app_js_body()),
3976 Asset::new("js/app-488df671.js.gz", app_js_gz_body()),
3977 Asset::new("js/app-488df671.js.zz", app_js_zz_body()),
3978 Asset::new("js/app-488df671.js.br", app_js_br_body()),
3979 Asset::new("css/app-ba74b708.css", app_css_body()),
3980 Asset::new("not-found.html", not_found_html_body()),
3981 Asset::new("not-found.html.gz", not_found_html_gz_body()),
3982 Asset::new("not-found.html.zz", not_found_html_zz_body()),
3983 Asset::new("not-found.html.br", not_found_html_br_body()),
3984 ];
3985
3986 let asset_configs = vec![
3987 index_html_config(),
3988 js_config(),
3989 css_config(),
3990 not_found_html_config(),
3991 old_url_redirect_config(),
3992 css_redirect_config(),
3993 ];
3994
3995 asset_router.certify_assets(assets, asset_configs).unwrap();
3996
3997 asset_router
3998 }
3999
4000 fn long_asset(name: String) -> Asset<'static, 'static> {
4001 let body = long_asset_body(&name);
4002 Asset::new(name, body)
4003 }
4004
4005 fn long_asset_router_with_params<'a>(
4006 asset_names: &[&str],
4007 encodings: &[AssetEncoding],
4008 ) -> AssetRouter<'a> {
4009 let mut asset_router = AssetRouter::default();
4010 let mut assets = vec![];
4011 let mut asset_configs = vec![];
4012
4013 for name in asset_names {
4014 for encoding in encodings {
4015 let (_, encoding_suffix) = encoding.default_config();
4016 let full_name = format!("{name}{encoding_suffix}");
4017 assets.push(long_asset(full_name));
4018 }
4019 asset_configs.push(long_asset_config(name));
4020 }
4021 asset_router.certify_assets(assets, asset_configs).unwrap();
4022 asset_router
4023 }
4024
4025 fn build_200_response<'a>(
4026 body: Vec<u8>,
4027 cel_expr: String,
4028 headers: Vec<HeaderField>,
4029 ) -> HttpResponse<'a> {
4030 build_response(StatusCode::OK, body, cel_expr, headers)
4031 }
4032
4033 fn build_206_response<'a>(
4034 body: Vec<u8>,
4035 cel_expr: String,
4036 headers: Vec<HeaderField>,
4037 ) -> HttpResponse<'a> {
4038 build_response(StatusCode::PARTIAL_CONTENT, body, cel_expr, headers)
4039 }
4040
4041 #[fixture]
4045 fn data_certificate() -> Vec<u8> {
4046 base64_decode("2dn3o2R0cmVlgwGDAYMBggRYIKZa+jjiJCKIY6ieu3PP5Vz5wLXmyPh1bDmIzXg5dl6LgwJIY2FuaXN0ZXKDAYMBggRYIM/g0MyVpl3VttUo8bFaIM3krNFLeWDQlazn4vVmbs12gwGDAYMBgwGDAYIEWCCrjGIsFv7RroK/KT/vV4dT/8o6c6Q8uFH3A372mLl7I4MBggRYIIILxwOzUO8JeUy1GuQk1oRnBKc7mlApt5csrszervQDgwGDAYIEWCBlMbtdoKygLVFQVorKucJVgVLHtLscN5S8BBykjfmQ+4MBgwGCBFgg680ttF9A023RfxGHUK7ceDdxxyHb6Cbg4qjinLrq+6mDAYIEWCCURcqsdMxTygYlQwS+KseXWp9QWrCCtb446pyKsj3lOoMCSgAAAAABgAmUAQGDAYMBgwJOY2VydGlmaWVkX2RhdGGCA1ggk/T8pnqZAQeSmaKDG8U/GSSBWQTEZfior9A2Wo/FZTSCBFgggt6vD1DPdgxNs+gKICbk3nRcQI5Tkp5syXYnM9GFJmCCBFggP4ZGeJfdyQHomGieqWx6e2QVSVgmctlGDoHCTcTrXWOCBFggILD+6U1L9j+vw02XY6JkB17xt0k6Esl/mcWDSifUPGyCBFggmUXnX4i82sN5AJqyUuIq4i9ErZ47rVK637kTCrOT7eSCBFggNqmXjq9JIe/CbAZW3wpfG12ofFrV8a7+tL5SL0tQ5ymCBFggFI5GZ/bYvjr0BzJiwPwH1yI7Rmd6jam1yj81cX8EqCWCBFggHHprIPk9m3zr3vFaSCA2JJiHSbLLfHzd+a0Rs90Ay7eCBFgg/5TQFWqdhgGiUF6Vy8qFpoKghCMgdXuYrc6pm7nSdpSCBFggzWD0/+Mf+jkRU4F1Xegk5vpPLOuDIfmneS92N03AdpyCBFggtNKNmO74J7jAsbkLgC5DQTEvIelYUdINiDtCOwUPLOWDAYIEWCC8BmcZ/cRsbnrLwEIfk310vWtBYg1iZAh9uLo+rxEtQYMCRHRpbWWCA0nZ/7r03qCj+hdpc2lnbmF0dXJlWDCU4tWKn7kUwxBiXeWihdpAEsfRN8YXvlz8/U7/NzTeb9t3mIdUlyj+YcQVEu9nxBdqZGVsZWdhdGlvbqJpc3VibmV0X2lkWB0QtkczSlQGmHeWsvi2sUzTLwh20/1PyEUhBYMlAmtjZXJ0aWZpY2F0ZVkCfdnZ96JkdHJlZYMBggRYIEe+kyDbAopvNlKUKa90j7vq/mnGs3p+1NUR03ZZjauCgwGDAYIEWCDSkWhnwtRPFDffqILBSu0cTmpQgjY9a5IEFfY8yKUxfIMCRnN1Ym5ldIMBgwGDAYMBgwGCBFgg0dOP/K78SbZBfvb185tLrkYoD1X09di+aBvUgAg+iJeDAYMCWB0QtkczSlQGmHeWsvi2sUzTLwh20/1PyEUhBYMlAoMBgwJPY2FuaXN0ZXJfcmFuZ2VzggNYG9nZ94GCSgAAAAABgAAAAQFKAAAAAAGP//8BAYMCSnB1YmxpY19rZXmCA1iFMIGCMB0GDSsGAQQBgtx8BQMBAgEGDCsGAQQBgtx8BQMCAQNhAKkibyL1MUdfYEy6XJin1b3PH7O2dsscd7G1feb0chACzBZOV+iXsL2AkjXe5cpqmwj6EZ3voSNz0aXeskewXOtFE4acjAp8pqDHx5EzUer76iqMIwraljfu94OFUhGBZIIEWCDGWnxJxMbpfrLKS1SIeWornghRMHDsKLoA4Ht6k/jqo4IEWCC8jzyQpYOJ/fqhwEmB2toxxu/hn7B9fcDMuS1/S/bYA4IEWCCI/qDbafOPnPP7qI+KBA88rcmud3L6GkBqbqRk+oWLnoIEWCBpYe8TfCruCwRnCC7208EsA+kwE7YCpMtiFCcOSEhj8YIEWCCbMGsgdP3vD+1UB+zEnG32tlisH7P9tl/aAiIVQIKEM4MCRHRpbWWCA0nEoN6ai4me+hdpc2lnbmF0dXJlWDCxK5IGNwccdX4Xs5HPctDb3AjfHV2QPScDneQJ7VFFOVN1+47TiJCYsFDPVSkKXVs=")
4047 }
4048
4049 fn build_response<'a>(
4050 status_code: StatusCode,
4051 body: Vec<u8>,
4052 cel_expr: String,
4053 headers: Vec<HeaderField>,
4054 ) -> HttpResponse<'a> {
4055 let combined_headers = headers
4056 .into_iter()
4057 .chain(vec![
4058 ("content-length".to_string(), body.len().to_string()),
4059 (CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(), cel_expr),
4060 ])
4061 .collect();
4062
4063 HttpResponse::builder()
4064 .with_status_code(status_code)
4065 .with_body(body)
4066 .with_headers(combined_headers)
4067 .build()
4068 }
4069
4070 fn extract_witness_expr_path(response: &HttpResponse) -> (HashTree, Vec<String>) {
4071 let (_, certificate_header_str) = response
4072 .headers()
4073 .iter()
4074 .find(|(name, _)| name.to_lowercase() == CERTIFICATE_HEADER_NAME.to_lowercase())
4075 .unwrap();
4076
4077 let certificate_header = CertificateHeader::from(certificate_header_str).unwrap();
4078 (
4079 certificate_header.tree,
4080 certificate_header.expr_path.unwrap(),
4081 )
4082 }
4083}