1use super::HttpTask;
20
21use bytes::Bytes;
22use log::{debug, warn};
23use pingora_error::{ErrorType, Result};
24use pingora_http::{RequestHeader, ResponseHeader};
25use std::time::Duration;
26
27use strum::EnumCount;
28use strum_macros::EnumCount as EnumCountMacro;
29
30mod brotli;
31mod gzip;
32mod zstd;
33
34pub use zstd::{DCZ_HEADER_SIZE, DCZ_MAGIC};
36
37pub const COMPRESSION_ERROR: ErrorType = ErrorType::new("CompressionError");
39
40pub trait Encode {
43 fn encode(&mut self, input: &[u8], end: bool) -> Result<Bytes>;
47 fn stat(&self) -> (&'static str, usize, usize, Duration);
50}
51
52pub struct ResponseCompressionCtx(CtxInner);
69
70#[derive(Clone, Debug)]
72pub struct DictionaryData {
73 pub bytes: Bytes,
74 pub hash: [u8; 32],
75}
76
77enum CtxInner {
78 HeaderPhase {
79 accept_encoding: Vec<Algorithm>,
81 encoding_levels: [u32; Algorithm::COUNT],
82 decompress_enable: [bool; Algorithm::COUNT],
83 preserve_etag: [bool; Algorithm::COUNT],
84 dictionary: Option<DictionaryData>,
86 },
87 BodyPhase(Option<Box<dyn Encode + Send + Sync>>),
88}
89
90impl ResponseCompressionCtx {
91 pub fn new(compression_level: u32, decompress_enable: bool, preserve_etag: bool) -> Self {
97 Self(CtxInner::HeaderPhase {
98 accept_encoding: Vec::new(),
99 encoding_levels: [compression_level; Algorithm::COUNT],
100 decompress_enable: [decompress_enable; Algorithm::COUNT],
101 preserve_etag: [preserve_etag; Algorithm::COUNT],
102 dictionary: None,
103 })
104 }
105
106 pub fn is_enabled(&self) -> bool {
109 match &self.0 {
110 CtxInner::HeaderPhase {
111 decompress_enable,
112 encoding_levels: levels,
113 ..
114 } => levels.iter().any(|l| *l != 0) || decompress_enable.iter().any(|d| *d),
115 CtxInner::BodyPhase(c) => c.is_some(),
116 }
117 }
118
119 pub fn get_info(&self) -> Option<(&'static str, usize, usize, Duration)> {
122 match &self.0 {
123 CtxInner::HeaderPhase { .. } => None,
124 CtxInner::BodyPhase(c) => c.as_ref().map(|c| c.stat()),
125 }
126 }
127
128 pub fn adjust_level(&mut self, new_level: u32) {
132 match &mut self.0 {
133 CtxInner::HeaderPhase {
134 encoding_levels: levels,
135 ..
136 } => {
137 *levels = [new_level; Algorithm::COUNT];
138 }
139 CtxInner::BodyPhase(_) => panic!("Wrong phase: BodyPhase"),
140 }
141 }
142
143 pub fn adjust_algorithm_level(&mut self, algorithm: Algorithm, new_level: u32) {
147 match &mut self.0 {
148 CtxInner::HeaderPhase {
149 encoding_levels: levels,
150 ..
151 } => {
152 levels[algorithm.index()] = new_level;
153 }
154 CtxInner::BodyPhase(_) => panic!("Wrong phase: BodyPhase"),
155 }
156 }
157
158 pub fn adjust_decompression(&mut self, enabled: bool) {
162 match &mut self.0 {
163 CtxInner::HeaderPhase {
164 decompress_enable, ..
165 } => {
166 *decompress_enable = [enabled; Algorithm::COUNT];
167 }
168 CtxInner::BodyPhase(_) => panic!("Wrong phase: BodyPhase"),
169 }
170 }
171
172 pub fn adjust_algorithm_decompression(&mut self, algorithm: Algorithm, enabled: bool) {
176 match &mut self.0 {
177 CtxInner::HeaderPhase {
178 decompress_enable, ..
179 } => {
180 decompress_enable[algorithm.index()] = enabled;
181 }
182 CtxInner::BodyPhase(_) => panic!("Wrong phase: BodyPhase"),
183 }
184 }
185
186 pub fn adjust_preserve_etag(&mut self, enabled: bool) {
190 match &mut self.0 {
191 CtxInner::HeaderPhase { preserve_etag, .. } => {
192 *preserve_etag = [enabled; Algorithm::COUNT];
193 }
194 CtxInner::BodyPhase(_) => panic!("Wrong phase: BodyPhase"),
195 }
196 }
197
198 pub fn adjust_algorithm_preserve_etag(&mut self, algorithm: Algorithm, enabled: bool) {
202 match &mut self.0 {
203 CtxInner::HeaderPhase { preserve_etag, .. } => {
204 preserve_etag[algorithm.index()] = enabled;
205 }
206 CtxInner::BodyPhase(_) => panic!("Wrong phase: BodyPhase"),
207 }
208 }
209
210 pub fn set_dictionary(&mut self, dictionary_bytes: Bytes, dictionary_hash: [u8; 32]) {
214 match &mut self.0 {
215 CtxInner::HeaderPhase { dictionary, .. } => {
216 *dictionary = Some(DictionaryData {
217 bytes: dictionary_bytes,
218 hash: dictionary_hash,
219 });
220 }
221 CtxInner::BodyPhase(_) => panic!("Wrong phase: BodyPhase"),
222 }
223 }
224
225 pub fn has_dictionary(&self) -> bool {
227 match &self.0 {
228 CtxInner::HeaderPhase { dictionary, .. } => dictionary.is_some(),
229 CtxInner::BodyPhase(_) => false,
230 }
231 }
232
233 pub fn clear_dictionary(&mut self) {
237 match &mut self.0 {
238 CtxInner::HeaderPhase { dictionary, .. } => {
239 *dictionary = None;
240 }
241 CtxInner::BodyPhase(_) => panic!("Wrong phase: BodyPhase"),
242 }
243 }
244
245 pub fn request_filter(&mut self, req: &RequestHeader) {
247 if !self.is_enabled() {
248 return;
249 }
250 match &mut self.0 {
251 CtxInner::HeaderPhase {
252 accept_encoding, ..
253 } => parse_accept_encoding(
254 req.headers.get(http::header::ACCEPT_ENCODING),
255 accept_encoding,
256 ),
257 CtxInner::BodyPhase(_) => panic!("Wrong phase: BodyPhase"),
258 }
259 }
260
261 pub fn response_header_filter(&mut self, resp: &mut ResponseHeader, end: bool) {
263 if !self.is_enabled() {
264 return;
265 }
266 match &self.0 {
267 CtxInner::HeaderPhase {
268 decompress_enable,
269 preserve_etag,
270 accept_encoding,
271 encoding_levels: levels,
272 dictionary,
273 } => {
274 if resp.status.is_informational() {
275 if resp.status == http::status::StatusCode::SWITCHING_PROTOCOLS {
276 self.0 = CtxInner::BodyPhase(None);
278 }
279 return;
281 }
282 if end {
284 self.0 = CtxInner::BodyPhase(None);
285 return;
286 }
287
288 if depends_on_accept_encoding(
289 resp,
290 levels.iter().any(|level| *level != 0),
291 decompress_enable,
292 ) {
293 add_vary_header(resp, &http::header::ACCEPT_ENCODING);
297 }
298
299 let action = decide_action(resp, accept_encoding);
300 debug!("compression action: {action:?}");
301 let (encoder, preserve_etag) = match action {
302 Action::Noop => (None, false),
303 Action::Compress(algorithm) => {
304 let idx = algorithm.index();
305 let compressor = match algorithm {
306 Algorithm::Dcz => {
307 let enc = dictionary.as_ref().and_then(|d| {
311 algorithm.maybe_compressor_with_dictionary(levels[idx], d)
312 });
313 if enc.is_some() {
314 add_vary_header(resp, &AVAILABLE_DICTIONARY);
315 }
316 enc
317 }
318 _ => algorithm.compressor(levels[idx]),
319 };
320 (compressor, preserve_etag[idx])
321 }
322 Action::Decompress(algorithm) => {
323 let idx = algorithm.index();
324 (
325 algorithm.decompressor(decompress_enable[idx]),
326 preserve_etag[idx],
327 )
328 }
329 };
330 if encoder.is_some() {
331 adjust_response_header(resp, &action, preserve_etag);
332 }
333 self.0 = CtxInner::BodyPhase(encoder);
334 }
335 CtxInner::BodyPhase(_) => panic!("Wrong phase: BodyPhase"),
336 }
337 }
338
339 pub fn response_body_filter(&mut self, data: Option<&Bytes>, end: bool) -> Option<Bytes> {
344 match &mut self.0 {
345 CtxInner::HeaderPhase { .. } => panic!("Wrong phase: HeaderPhase"),
346 CtxInner::BodyPhase(compressor) => {
347 let result = compressor
348 .as_mut()
349 .map(|c| {
350 let data = if let Some(b) = data { b.as_ref() } else { &[] };
353 c.encode(data, end)
354 })
355 .transpose();
356 result.unwrap_or_else(|e| {
357 warn!("Failed to compress, compression disabled, {}", e);
358 self.0 = CtxInner::BodyPhase(None);
360 None
361 })
362 }
363 }
364 }
365
366 pub fn response_filter(&mut self, t: &mut HttpTask) {
370 if !self.is_enabled() {
371 return;
372 }
373 match t {
374 HttpTask::Header(resp, end) => self.response_header_filter(resp, *end),
375 HttpTask::Body(data, end) => {
376 let compressed = self.response_body_filter(data.as_ref(), *end);
377 if compressed.is_some() {
378 *t = HttpTask::Body(compressed, *end);
379 }
380 }
381 HttpTask::Done => {
382 let compressed = self.response_body_filter(None, true);
384 if compressed.is_some() {
385 *t = HttpTask::Body(compressed, true);
387 }
388 }
389 _ => { }
390 }
391 }
392}
393
394#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumCountMacro)]
395pub enum Algorithm {
396 Any, Gzip,
398 Brotli,
399 Zstd,
400 Dcb,
401 Dcz,
402 Other, }
406
407impl Algorithm {
408 pub fn as_str(&self) -> &'static str {
409 match self {
410 Algorithm::Gzip => "gzip",
411 Algorithm::Brotli => "br",
412 Algorithm::Zstd => "zstd",
413 Algorithm::Dcb => "dcb",
414 Algorithm::Dcz => "dcz",
415 Algorithm::Any => "*",
416 Algorithm::Other => "other",
417 }
418 }
419
420 pub fn compressor(&self, level: u32) -> Option<Box<dyn Encode + Send + Sync>> {
421 if level == 0 {
422 None
423 } else {
424 match self {
425 Self::Gzip => Some(Box::new(gzip::Compressor::new(level))),
426 Self::Brotli => Some(Box::new(brotli::Compressor::new(level))),
427 Self::Zstd => Some(Box::new(zstd::Compressor::new(level))),
428 _ => None, }
430 }
431 }
432
433 pub fn maybe_compressor_with_dictionary(
434 &self,
435 level: u32,
436 dictionary: &DictionaryData,
437 ) -> Option<Box<dyn Encode + Send + Sync>> {
438 if level == 0 {
439 None
440 } else {
441 match self {
442 Self::Dcz => {
443 match zstd::DictionaryCompressor::new(level, &dictionary.bytes, dictionary.hash)
444 {
445 Ok(c) => Some(Box::new(c)),
446 Err(e) => {
447 warn!("Failed to create DCZ compressor: {e}");
448 None
449 }
450 }
451 }
452 _ => None,
453 }
454 }
455 }
456
457 pub fn decompressor(&self, enabled: bool) -> Option<Box<dyn Encode + Send + Sync>> {
458 if !enabled {
459 None
460 } else {
461 match self {
462 Self::Gzip => Some(Box::new(gzip::Decompressor::new())),
463 Self::Brotli => Some(Box::new(brotli::Decompressor::new())),
464 _ => None, }
466 }
467 }
468
469 pub fn index(&self) -> usize {
470 *self as usize
471 }
472}
473
474impl From<&str> for Algorithm {
475 fn from(s: &str) -> Self {
476 use unicase::UniCase;
477
478 let coding = UniCase::new(s);
479 if coding == UniCase::ascii("gzip") {
480 Algorithm::Gzip
481 } else if coding == UniCase::ascii("br") {
482 Algorithm::Brotli
483 } else if coding == UniCase::ascii("zstd") {
484 Algorithm::Zstd
485 } else if coding == UniCase::ascii("dcb") {
486 Algorithm::Dcb
487 } else if coding == UniCase::ascii("dcz") {
488 Algorithm::Dcz
489 } else if s.is_empty() {
490 Algorithm::Any
491 } else {
492 Algorithm::Other
493 }
494 }
495}
496
497#[derive(Debug, PartialEq, Eq, Clone, Copy)]
498enum Action {
499 Noop, Compress(Algorithm),
501 Decompress(Algorithm),
502}
503
504fn parse_accept_encoding(accept_encoding: Option<&http::HeaderValue>, list: &mut Vec<Algorithm>) {
506 if let Some(ac) = accept_encoding {
508 if ac.as_bytes() == b"gzip" {
510 list.push(Algorithm::Gzip);
511 return;
512 }
513 match sfv::Parser::parse_list(ac.as_bytes()) {
515 Ok(parsed) => {
516 for item in parsed {
517 if let sfv::ListEntry::Item(i) = item {
518 if let Some(s) = i.bare_item.as_token() {
519 let algorithm = Algorithm::from(s);
521 if algorithm != Algorithm::Other {
523 list.push(Algorithm::from(s));
524 }
525 }
526 }
527 }
528 }
529 Err(e) => {
530 warn!("Failed to parse accept-encoding {ac:?}, {e}")
531 }
532 }
533 } else {
534 }
537}
538
539#[test]
540fn test_accept_encoding_req_header() {
541 let mut header = RequestHeader::build("GET", b"/", None).unwrap();
542 let mut ac_list = Vec::new();
543 parse_accept_encoding(
544 header.headers.get(http::header::ACCEPT_ENCODING),
545 &mut ac_list,
546 );
547 assert!(ac_list.is_empty());
548
549 let mut ac_list = Vec::new();
550 header.insert_header("accept-encoding", "gzip").unwrap();
551 parse_accept_encoding(
552 header.headers.get(http::header::ACCEPT_ENCODING),
553 &mut ac_list,
554 );
555 assert_eq!(ac_list[0], Algorithm::Gzip);
556
557 let mut ac_list = Vec::new();
558 header
559 .insert_header("accept-encoding", "what, br, gzip")
560 .unwrap();
561 parse_accept_encoding(
562 header.headers.get(http::header::ACCEPT_ENCODING),
563 &mut ac_list,
564 );
565 assert_eq!(ac_list[0], Algorithm::Brotli);
566 assert_eq!(ac_list[1], Algorithm::Gzip);
567}
568
569fn depends_on_accept_encoding(
571 resp: &ResponseHeader,
572 compress_enabled: bool,
573 decompress_enabled: &[bool],
574) -> bool {
575 use http::header::CONTENT_ENCODING;
576
577 (decompress_enabled.iter().any(|enabled| *enabled)
578 && resp.headers.get(CONTENT_ENCODING).is_some())
579 || (compress_enabled && compressible(resp))
580}
581
582#[test]
583fn test_decide_on_accept_encoding() {
584 let mut resp = ResponseHeader::build(200, None).unwrap();
585 resp.insert_header("content-length", "50").unwrap();
586 resp.insert_header("content-type", "text/html").unwrap();
587 resp.insert_header("content-encoding", "gzip").unwrap();
588
589 assert!(depends_on_accept_encoding(&resp, false, &[true]));
591
592 assert!(!depends_on_accept_encoding(&resp, false, &[false]));
594
595 resp.remove_header("content-encoding");
597 assert!(!depends_on_accept_encoding(&resp, false, &[true]));
598
599 assert!(depends_on_accept_encoding(&resp, true, &[false]));
601
602 assert!(!depends_on_accept_encoding(&resp, false, &[false]));
604
605 resp.insert_header("content-type", "text/html+zip").unwrap();
607 assert!(!depends_on_accept_encoding(&resp, true, &[false]));
608}
609
610fn decide_action(resp: &ResponseHeader, accept_encoding: &[Algorithm]) -> Action {
612 use http::header::CONTENT_ENCODING;
613
614 let content_encoding = if let Some(ce) = resp.headers.get(CONTENT_ENCODING) {
615 if let Ok(ce_str) = std::str::from_utf8(ce.as_bytes()) {
617 Some(Algorithm::from(ce_str))
618 } else {
619 Some(Algorithm::Other)
621 }
622 } else {
623 None
625 };
626
627 if let Some(ce) = content_encoding {
628 if accept_encoding.contains(&ce) {
629 Action::Noop
631 } else {
632 Action::Decompress(ce)
638 }
639 } else if accept_encoding.is_empty() || !compressible(resp) || accept_encoding[0] == Algorithm::Any
642 {
643 Action::Noop
644 } else {
645 Action::Compress(accept_encoding[0])
648 }
649}
650
651#[test]
652fn test_decide_action() {
653 use Action::*;
654 use Algorithm::*;
655
656 let header = ResponseHeader::build(200, None).unwrap();
657 assert_eq!(decide_action(&header, &[]), Noop);
659
660 let mut header = ResponseHeader::build(200, None).unwrap();
662 header.insert_header("content-type", "text/html").unwrap();
663 header.insert_header("content-encoding", "gzip").unwrap();
664 assert_eq!(decide_action(&header, &[Gzip]), Noop);
665
666 let mut header = ResponseHeader::build(200, None).unwrap();
668 header.insert_header("content-encoding", "GzIp").unwrap();
669 header.insert_header("content-type", "text/html").unwrap();
670 assert_eq!(decide_action(&header, &[Gzip]), Noop);
671
672 let mut header = ResponseHeader::build(200, None).unwrap();
675 header.insert_header("content-length", "20").unwrap();
676 header.insert_header("content-type", "text/html").unwrap();
677 assert_eq!(decide_action(&header, &[Gzip]), Compress(Gzip));
678
679 let mut header = ResponseHeader::build(200, None).unwrap();
681 header.insert_header("content-length", "19").unwrap();
682 header.insert_header("content-type", "text/html").unwrap();
683 assert_eq!(decide_action(&header, &[Gzip]), Noop);
684
685 let mut header = ResponseHeader::build(200, None).unwrap();
687 header.insert_header("content-length", "20").unwrap();
688 header
689 .insert_header("content-type", "text/html+zip")
690 .unwrap();
691 assert_eq!(decide_action(&header, &[Gzip]), Noop);
692
693 let mut header = ResponseHeader::build(200, None).unwrap();
695 header.insert_header("content-length", "20").unwrap();
696 header.insert_header("content-type", "image/jpg").unwrap();
697 assert_eq!(decide_action(&header, &[Gzip]), Noop);
698
699 let mut header = ResponseHeader::build(200, None).unwrap();
701 header.insert_header("content-encoding", "gzip").unwrap();
702 assert_eq!(decide_action(&header, &[]), Decompress(Gzip));
703
704 let mut header = ResponseHeader::build(200, None).unwrap();
706 header.insert_header("content-encoding", "gzip").unwrap();
707 assert_eq!(decide_action(&header, &[Brotli]), Decompress(Gzip));
708
709 let mut header = ResponseHeader::build(200, None).unwrap();
711 header.insert_header("content-encoding", "gzip").unwrap();
712 assert_eq!(decide_action(&header, &[Brotli, Gzip]), Noop);
713
714 let mut header = ResponseHeader::build(200, None).unwrap();
716 header.insert_header("content-encoding", "dcb").unwrap();
717 assert_eq!(decide_action(&header, &[Dcb, Brotli]), Noop);
718
719 let mut header = ResponseHeader::build(200, None).unwrap();
721 header.insert_header("content-encoding", "dcz").unwrap();
722 assert_eq!(decide_action(&header, &[Dcz, Zstd]), Noop);
723
724 let mut header = ResponseHeader::build(200, None).unwrap();
726 header.insert_header("content-encoding", "br").unwrap();
727 assert_eq!(decide_action(&header, &[Dcz]), Decompress(Brotli));
728
729 let mut header = ResponseHeader::build(200, None).unwrap();
731 header.insert_header("content-encoding", "zstd").unwrap();
732 assert_eq!(decide_action(&header, &[Dcz]), Decompress(Zstd));
733
734 let mut header = ResponseHeader::build(200, None).unwrap();
736 header.insert_header("content-encoding", "gzip").unwrap();
737 assert_eq!(decide_action(&header, &[Dcb]), Decompress(Gzip));
738
739 let mut header = ResponseHeader::build(200, None).unwrap();
741 header.insert_header("content-encoding", "br").unwrap();
742 assert_eq!(decide_action(&header, &[Dcb]), Decompress(Brotli));
743}
744
745use once_cell::sync::Lazy;
746use regex::Regex;
747
748static MIME_CHECK: Lazy<Regex> = Lazy::new(|| {
751 Regex::new(r"^(?:text/|application/|font/|image/(?:x-icon|svg\+xml|nd\.microsoft\.icon)|binary/octet-stream)")
752 .unwrap()
753});
754
755fn compressible(resp: &ResponseHeader) -> bool {
757 const MIN_COMPRESS_LEN: usize = 20;
762
763 if let Some(cl) = resp.headers.get(http::header::CONTENT_LENGTH) {
765 if let Some(cl_num) = std::str::from_utf8(cl.as_bytes())
766 .ok()
767 .and_then(|v| v.parse::<usize>().ok())
768 {
769 if cl_num < MIN_COMPRESS_LEN {
770 return false;
771 }
772 }
773 }
774 if let Some(ct) = resp.headers.get(http::header::CONTENT_TYPE) {
776 if let Ok(ct_str) = std::str::from_utf8(ct.as_bytes()) {
777 if ct_str.contains("zip") {
778 false
780 } else {
781 MIME_CHECK.find(ct_str).is_some()
783 }
784 } else {
785 false }
787 } else {
788 false }
790}
791
792static AVAILABLE_DICTIONARY: http::HeaderName =
797 http::HeaderName::from_static("available-dictionary");
798
799fn add_vary_header(resp: &mut ResponseHeader, value: &http::header::HeaderName) {
801 use http::header::{HeaderValue, VARY};
802
803 let already_present = resp.headers.get_all(VARY).iter().any(|existing| {
804 existing
805 .as_bytes()
806 .split(|b| *b == b',')
807 .map(|mut v| {
808 while let [first, rest @ ..] = v {
810 if first.is_ascii_whitespace() {
811 v = rest;
812 } else {
813 break;
814 }
815 }
816 while let [rest @ .., last] = v {
817 if last.is_ascii_whitespace() {
818 v = rest;
819 } else {
820 break;
821 }
822 }
823 v
824 })
825 .any(|v| v == b"*" || v.eq_ignore_ascii_case(value.as_ref()))
826 });
827
828 if !already_present {
829 resp.append_header(&VARY, HeaderValue::from_name(value.clone()))
830 .unwrap();
831 }
832}
833
834#[test]
835fn test_add_vary_header() {
836 let mut header = ResponseHeader::build(200, None).unwrap();
837 add_vary_header(&mut header, &http::header::ACCEPT_ENCODING);
838 assert_eq!(
839 header
840 .headers
841 .get_all("Vary")
842 .into_iter()
843 .collect::<Vec<_>>(),
844 vec!["accept-encoding"]
845 );
846
847 let mut header = ResponseHeader::build(200, None).unwrap();
848 header.insert_header("Vary", "Accept-Language").unwrap();
849 add_vary_header(&mut header, &http::header::ACCEPT_ENCODING);
850 assert_eq!(
851 header
852 .headers
853 .get_all("Vary")
854 .into_iter()
855 .collect::<Vec<_>>(),
856 vec!["Accept-Language", "accept-encoding"]
857 );
858
859 let mut header = ResponseHeader::build(200, None).unwrap();
860 header
861 .insert_header("Vary", "Accept-Language, Accept-Encoding")
862 .unwrap();
863 add_vary_header(&mut header, &http::header::ACCEPT_ENCODING);
864 assert_eq!(
865 header
866 .headers
867 .get_all("Vary")
868 .into_iter()
869 .collect::<Vec<_>>(),
870 vec!["Accept-Language, Accept-Encoding"]
871 );
872
873 let mut header = ResponseHeader::build(200, None).unwrap();
874 header.insert_header("Vary", "*").unwrap();
875 add_vary_header(&mut header, &http::header::ACCEPT_ENCODING);
876 assert_eq!(
877 header
878 .headers
879 .get_all("Vary")
880 .into_iter()
881 .collect::<Vec<_>>(),
882 vec!["*"]
883 );
884}
885
886fn adjust_response_header(resp: &mut ResponseHeader, action: &Action, preserve_etag: bool) {
887 use http::header::{
888 HeaderValue, ACCEPT_RANGES, CONTENT_ENCODING, CONTENT_LENGTH, ETAG, TRANSFER_ENCODING,
889 };
890
891 fn set_stream_headers(resp: &mut ResponseHeader) {
892 resp.remove_header(&CONTENT_LENGTH);
894 resp.remove_header(&ACCEPT_RANGES);
896
897 resp.insert_header(&TRANSFER_ENCODING, HeaderValue::from_static("chunked"))
899 .unwrap();
900 }
901
902 fn weaken_or_clear_etag(resp: &mut ResponseHeader) {
903 if let Some(etag) = resp.headers.get(&ETAG) {
911 let etag_bytes = etag.as_bytes();
912 if etag_bytes.starts_with(b"W/") {
913 } else if etag_bytes.starts_with(b"\"") {
915 let weakened_etag = HeaderValue::from_bytes(&[b"W/", etag_bytes].concat())
917 .expect("valid header value prefixed with \"W/\" should remain valid");
918 resp.insert_header(&ETAG, weakened_etag)
919 .expect("can insert weakened etag when etag was already valid");
920 } else {
921 resp.remove_header(&ETAG);
925 }
926 }
927 }
928
929 match action {
930 Action::Noop => { }
931 Action::Decompress(_) => {
932 resp.remove_header(&CONTENT_ENCODING);
933 set_stream_headers(resp);
934 if !preserve_etag {
935 weaken_or_clear_etag(resp);
936 }
937 }
938 Action::Compress(a) => {
939 resp.insert_header(&CONTENT_ENCODING, HeaderValue::from_static(a.as_str()))
940 .unwrap();
941 set_stream_headers(resp);
942 if !preserve_etag {
943 weaken_or_clear_etag(resp);
944 }
945 }
946 }
947}
948
949#[test]
950fn test_adjust_response_header() {
951 use Action::*;
952 use Algorithm::*;
953
954 let mut header = ResponseHeader::build(200, None).unwrap();
956 header.insert_header("content-length", "20").unwrap();
957 header.insert_header("content-encoding", "gzip").unwrap();
958 header.insert_header("accept-ranges", "bytes").unwrap();
959 header.insert_header("etag", "\"abc123\"").unwrap();
960 adjust_response_header(&mut header, &Noop, false);
961 assert_eq!(
962 header.headers.get("content-encoding").unwrap().as_bytes(),
963 b"gzip"
964 );
965 assert_eq!(
966 header.headers.get("content-length").unwrap().as_bytes(),
967 b"20"
968 );
969 assert_eq!(
970 header.headers.get("etag").unwrap().as_bytes(),
971 b"\"abc123\""
972 );
973 assert!(header.headers.get("transfer-encoding").is_none());
974
975 let mut header = ResponseHeader::build(200, None).unwrap();
977 header.insert_header("content-length", "20").unwrap();
978 header.insert_header("content-encoding", "gzip").unwrap();
979 header.insert_header("accept-ranges", "bytes").unwrap();
980 header.insert_header("etag", "\"abc123\"").unwrap();
981 adjust_response_header(&mut header, &Decompress(Gzip), false);
982 assert!(header.headers.get("content-encoding").is_none());
983 assert!(header.headers.get("content-length").is_none());
984 assert_eq!(
985 header.headers.get("transfer-encoding").unwrap().as_bytes(),
986 b"chunked"
987 );
988 assert!(header.headers.get("accept-ranges").is_none());
989 assert_eq!(
990 header.headers.get("etag").unwrap().as_bytes(),
991 b"W/\"abc123\""
992 );
993 header.insert_header("etag", "\"abc123\"").unwrap();
995 adjust_response_header(&mut header, &Decompress(Gzip), true);
996 assert_eq!(
997 header.headers.get("etag").unwrap().as_bytes(),
998 b"\"abc123\""
999 );
1000
1001 let mut header = ResponseHeader::build(200, None).unwrap();
1003 header.insert_header("content-length", "20").unwrap();
1004 header.insert_header("accept-ranges", "bytes").unwrap();
1005 header.insert_header("etag", "abc123").unwrap();
1007 adjust_response_header(&mut header, &Compress(Gzip), false);
1008 assert_eq!(
1009 header.headers.get("content-encoding").unwrap().as_bytes(),
1010 b"gzip"
1011 );
1012 assert!(header.headers.get("content-length").is_none());
1013 assert!(header.headers.get("accept-ranges").is_none());
1014 assert_eq!(
1015 header.headers.get("transfer-encoding").unwrap().as_bytes(),
1016 b"chunked"
1017 );
1018 assert!(header.headers.get("etag").is_none());
1019 header.insert_header("etag", "abc123").unwrap();
1021 adjust_response_header(&mut header, &Compress(Gzip), true);
1022 assert_eq!(header.headers.get("etag").unwrap().as_bytes(), b"abc123");
1023}
1024
1025#[cfg(test)]
1026mod tests_dictionary_compression {
1027 use super::*;
1028
1029 const TEST_DICTIONARY: &[u8] = b"The quick brown fox jumps over the lazy dog. \
1030 Common HTTP headers: Content-Type, Accept-Encoding, Cache-Control. \
1031 JSON patterns: {\"key\": \"value\"}, [\"array\", \"items\"].";
1032
1033 fn test_dictionary_hash() -> [u8; 32] {
1034 let mut hash = [0u8; 32];
1035 for (i, byte) in TEST_DICTIONARY.iter().take(32).enumerate() {
1036 hash[i] = *byte;
1037 }
1038 hash
1039 }
1040
1041 #[test]
1042 fn set_and_clear_dictionary() {
1043 let mut ctx = ResponseCompressionCtx::new(3, false, false);
1044 assert!(!ctx.has_dictionary());
1045
1046 ctx.set_dictionary(Bytes::from_static(TEST_DICTIONARY), test_dictionary_hash());
1047 assert!(ctx.has_dictionary());
1048
1049 ctx.clear_dictionary();
1050 assert!(!ctx.has_dictionary());
1051 }
1052
1053 #[test]
1054 fn dcz_compression_with_dictionary() {
1055 let mut ctx = ResponseCompressionCtx::new(3, false, false);
1056 let hash = test_dictionary_hash();
1057 ctx.set_dictionary(Bytes::from_static(TEST_DICTIONARY), hash);
1058
1059 let mut req = RequestHeader::build("GET", b"/test.js", None).unwrap();
1060 req.insert_header("accept-encoding", "dcz, br, gzip")
1061 .unwrap();
1062 ctx.request_filter(&req);
1063
1064 let mut resp = ResponseHeader::build(200, None).unwrap();
1065 resp.insert_header("content-type", "application/javascript")
1066 .unwrap();
1067 resp.insert_header("content-length", "1000").unwrap();
1068 ctx.response_header_filter(&mut resp, false);
1069
1070 assert_eq!(
1071 resp.headers.get("content-encoding").unwrap().as_bytes(),
1072 b"dcz"
1073 );
1074 assert!(resp.headers.get_all("vary").iter().any(|v| v
1076 .as_bytes()
1077 .split(|b| *b == b',')
1078 .any(|t| t.trim_ascii().eq_ignore_ascii_case(b"available-dictionary"))));
1079
1080 let input = Bytes::from_static(b"The quick brown fox jumps over the lazy dog again.");
1081 let compressed = ctx.response_body_filter(Some(&input), true).unwrap();
1082
1083 assert!(compressed.len() >= 40);
1084 assert_eq!(&compressed[..8], &zstd::DCZ_MAGIC);
1085 assert_eq!(&compressed[8..40], &hash);
1086 }
1087
1088 #[test]
1089 fn dcz_without_dictionary_no_compression() {
1090 let mut ctx = ResponseCompressionCtx::new(3, false, false);
1091
1092 let mut req = RequestHeader::build("GET", b"/test.js", None).unwrap();
1093 req.insert_header("accept-encoding", "dcz").unwrap();
1094 ctx.request_filter(&req);
1095
1096 let mut resp = ResponseHeader::build(200, None).unwrap();
1097 resp.insert_header("content-type", "application/javascript")
1098 .unwrap();
1099 resp.insert_header("content-length", "1000").unwrap();
1100 ctx.response_header_filter(&mut resp, false);
1101
1102 assert!(resp.headers.get("content-encoding").is_none());
1104 assert!(!resp.headers.get_all("vary").iter().any(|v| v
1106 .as_bytes()
1107 .split(|b| *b == b',')
1108 .any(|t| t.trim_ascii().eq_ignore_ascii_case(b"available-dictionary"))));
1109 }
1110
1111 #[test]
1112 fn dcz_no_fallback_without_dictionary() {
1113 let mut ctx = ResponseCompressionCtx::new(3, false, false);
1114
1115 let mut req = RequestHeader::build("GET", b"/test.js", None).unwrap();
1116 req.insert_header("accept-encoding", "dcz, br, gzip")
1117 .unwrap();
1118 ctx.request_filter(&req);
1119
1120 let mut resp = ResponseHeader::build(200, None).unwrap();
1121 resp.insert_header("content-type", "application/javascript")
1122 .unwrap();
1123 resp.insert_header("content-length", "1000").unwrap();
1124 ctx.response_header_filter(&mut resp, false);
1125
1126 assert!(resp.headers.get("content-encoding").is_none());
1128 assert!(!resp.headers.get_all("vary").iter().any(|v| v
1130 .as_bytes()
1131 .split(|b| *b == b',')
1132 .any(|t| t.trim_ascii().eq_ignore_ascii_case(b"available-dictionary"))));
1133 }
1134
1135 #[test]
1136 fn maybe_compressor_with_dictionary_dcz_only() {
1137 let dict_data = DictionaryData {
1138 bytes: Bytes::from_static(TEST_DICTIONARY),
1139 hash: test_dictionary_hash(),
1140 };
1141
1142 assert!(Algorithm::Dcz
1144 .maybe_compressor_with_dictionary(3, &dict_data)
1145 .is_some());
1146 assert!(Algorithm::Gzip
1147 .maybe_compressor_with_dictionary(3, &dict_data)
1148 .is_none());
1149 assert!(Algorithm::Brotli
1150 .maybe_compressor_with_dictionary(3, &dict_data)
1151 .is_none());
1152 assert!(Algorithm::Zstd
1153 .maybe_compressor_with_dictionary(3, &dict_data)
1154 .is_none());
1155 assert!(Algorithm::Dcz
1157 .maybe_compressor_with_dictionary(0, &dict_data)
1158 .is_none());
1159 }
1160
1161 #[test]
1162 fn dcz_full_flow() {
1163 let mut ctx = ResponseCompressionCtx::new(3, false, false);
1164 let hash = test_dictionary_hash();
1165 ctx.set_dictionary(Bytes::from_static(TEST_DICTIONARY), hash);
1166
1167 let mut req = RequestHeader::build("GET", b"/app.js", None).unwrap();
1168 req.insert_header("accept-encoding", "dcz").unwrap();
1169 ctx.request_filter(&req);
1170
1171 let mut resp = ResponseHeader::build(200, None).unwrap();
1172 resp.insert_header("content-type", "application/javascript")
1173 .unwrap();
1174 resp.insert_header("content-length", "500").unwrap();
1175 ctx.response_header_filter(&mut resp, false);
1176
1177 assert_eq!(
1178 resp.headers.get("content-encoding").unwrap().as_bytes(),
1179 b"dcz"
1180 );
1181 assert!(resp.headers.get("content-length").is_none());
1182 assert_eq!(
1183 resp.headers.get("transfer-encoding").unwrap().as_bytes(),
1184 b"chunked"
1185 );
1186 assert!(resp.headers.get_all("vary").iter().any(|v| v
1188 .as_bytes()
1189 .split(|b| *b == b',')
1190 .any(|t| t.trim_ascii().eq_ignore_ascii_case(b"available-dictionary"))));
1191
1192 let chunk1 = Bytes::from_static(b"First chunk. ");
1193 let output1 = ctx.response_body_filter(Some(&chunk1), false);
1194 assert!(output1.is_some());
1195
1196 let chunk2 = Bytes::from_static(b"Second chunk.");
1197 let output2 = ctx.response_body_filter(Some(&chunk2), true);
1198 assert!(output2.is_some());
1199
1200 let (name, total_in, total_out, _) = ctx.get_info().unwrap();
1201 assert_eq!(name, "dcz");
1202 assert_eq!(total_in, chunk1.len() + chunk2.len());
1203 assert!(total_out > 0);
1204 }
1205
1206 #[test]
1207 fn regular_compression_no_available_dictionary_vary() {
1208 let mut ctx = ResponseCompressionCtx::new(3, false, false);
1211
1212 let mut req = RequestHeader::build("GET", b"/page.html", None).unwrap();
1213 req.insert_header("accept-encoding", "gzip").unwrap();
1214 ctx.request_filter(&req);
1215
1216 let mut resp = ResponseHeader::build(200, None).unwrap();
1217 resp.insert_header("content-type", "text/html").unwrap();
1218 resp.insert_header("content-length", "1000").unwrap();
1219 ctx.response_header_filter(&mut resp, false);
1220
1221 assert_eq!(
1222 resp.headers.get("content-encoding").unwrap().as_bytes(),
1223 b"gzip"
1224 );
1225 assert!(resp.headers.get_all("vary").iter().any(|v| v
1226 .as_bytes()
1227 .split(|b| *b == b',')
1228 .any(|t| t.trim_ascii().eq_ignore_ascii_case(b"accept-encoding"))));
1229 assert!(!resp.headers.get_all("vary").iter().any(|v| v
1230 .as_bytes()
1231 .split(|b| *b == b',')
1232 .any(|t| t.trim_ascii().eq_ignore_ascii_case(b"available-dictionary"))));
1233 }
1234}