compression_module/
lib.rs

1// Copyright 2024 Wladimir Palant
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! # Compression Module for Pingora
16//!
17//! This crate helps configure Pingora’s built-in compression mechanism. It provides two
18//! configuration options:
19//!
20//! * `compression_level` (`--compression-level` as command-line option): If present, will enable
21//!   dynamic downstream compression and use the specified compression level (same level for all
22//!   compression algorithms, see
23//!   [Pingora issue #228](https://github.com/cloudflare/pingora/issues/228)).
24//! * `decompress_upstream` (`--decompress-upstream` as command-line flag): If `true`,
25//!   decompression of upstream responses will be enabled.
26//!
27//! ## Code example
28//!
29//! You will usually want to merge Pingora’s command-line options and configuration settings with
30//! the ones provided by this crate:
31//!
32//! ```rust
33//! use compression_module::{CompressionConf, CompressionHandler, CompressionOpt};
34//! use module_utils::{merge_conf, merge_opt, FromYaml};
35//! use pingora_core::server::Server;
36//! use pingora_core::server::configuration::{Opt as ServerOpt, ServerConf};
37//! use structopt::StructOpt;
38//!
39//! #[merge_opt]
40//! struct Opt {
41//!     server: ServerOpt,
42//!     compression: CompressionOpt,
43//! }
44//!
45//! #[merge_conf]
46//! struct Conf {
47//!     server: ServerConf,
48//!     compression: CompressionConf,
49//! }
50//!
51//! let opt = Opt::from_args();
52//! let mut conf = opt
53//!     .server
54//!     .conf
55//!     .as_ref()
56//!     .and_then(|path| Conf::load_from_yaml(path).ok())
57//!     .unwrap_or_else(Conf::default);
58//! conf.compression.merge_with_opt(opt.compression);
59//!
60//! let mut server = Server::new_with_opt_and_conf(opt.server, conf.server);
61//! server.bootstrap();
62//!
63//! let compression_handler: CompressionHandler = conf.compression.try_into().unwrap();
64//! ```
65//!
66//! You can then use that handler in your server implementation:
67//!
68//! ```rust
69//! use async_trait::async_trait;
70//! use compression_module::CompressionHandler;
71//! use module_utils::RequestFilter;
72//! use pingora_core::Error;
73//! use pingora_core::upstreams::peer::HttpPeer;
74//! use pingora_proxy::{ProxyHttp, Session};
75//!
76//! pub struct MyServer {
77//!     compression_handler: CompressionHandler,
78//! }
79//!
80//! #[async_trait]
81//! impl ProxyHttp for MyServer {
82//!     type CTX = <CompressionHandler as RequestFilter>::CTX;
83//!     fn new_ctx(&self) -> Self::CTX {
84//!         CompressionHandler::new_ctx()
85//!     }
86//!
87//!     async fn request_filter(
88//!         &self,
89//!         session: &mut Session,
90//!         ctx: &mut Self::CTX,
91//!     ) -> Result<bool, Box<Error>> {
92//!         // Enable compression according to settings
93//!         self.compression_handler.handle(session, ctx).await
94//!     }
95//!
96//!     async fn upstream_peer(
97//!         &self,
98//!         _session: &mut Session,
99//!         _ctx: &mut Self::CTX,
100//!     ) -> Result<Box<HttpPeer>, Box<Error>> {
101//!         Ok(Box::new(HttpPeer::new(
102//!             "example.com:443",
103//!             true,
104//!             "example.com".to_owned(),
105//!         )))
106//!     }
107//! }
108//! ```
109//!
110//! For complete and more realistic code, see `single-static-root` example in the repository.
111
112use async_trait::async_trait;
113use module_utils::{RequestFilter, RequestFilterResult};
114use pingora_core::Error;
115use pingora_proxy::Session;
116use serde::Deserialize;
117use structopt::StructOpt;
118
119/// Command line options of the compression module
120#[derive(Debug, Default, StructOpt)]
121pub struct CompressionOpt {
122    /// Compression level to be used for dynamic compression (omit to disable compression)
123    #[structopt(long)]
124    pub compression_level: Option<u32>,
125
126    /// Decompress upstream responses before passing them on
127    #[structopt(long)]
128    pub decompress_upstream: bool,
129}
130
131/// Configuration settings of the compression module
132#[derive(Debug, Default, Deserialize)]
133#[serde(default)]
134pub struct CompressionConf {
135    /// Compression level to be used for dynamic compression (omit to disable compression).
136    pub compression_level: Option<u32>,
137
138    /// If `true`, upstream responses will be decompressed
139    pub decompress_upstream: bool,
140}
141
142impl CompressionConf {
143    /// Merges the command line options into the current configuration. Any command line options
144    /// present overwrite existing settings.
145    pub fn merge_with_opt(&mut self, opt: CompressionOpt) {
146        if opt.compression_level.is_some() {
147            self.compression_level = opt.compression_level;
148        }
149
150        if opt.decompress_upstream {
151            self.decompress_upstream = opt.decompress_upstream;
152        }
153    }
154}
155
156/// Handler for Pingora’s `request_filter` phase
157#[derive(Debug)]
158pub struct CompressionHandler {
159    conf: CompressionConf,
160}
161
162impl TryFrom<CompressionConf> for CompressionHandler {
163    type Error = Box<Error>;
164
165    fn try_from(conf: CompressionConf) -> Result<Self, Self::Error> {
166        Ok(Self { conf })
167    }
168}
169
170#[async_trait]
171impl RequestFilter for CompressionHandler {
172    type Conf = CompressionConf;
173    type CTX = ();
174    fn new_ctx() -> Self::CTX {}
175
176    async fn request_filter(
177        &self,
178        session: &mut Session,
179        _ctx: &mut Self::CTX,
180    ) -> Result<RequestFilterResult, Box<Error>> {
181        if let Some(level) = self.conf.compression_level {
182            session.downstream_compression.adjust_level(level);
183        }
184
185        if self.conf.decompress_upstream {
186            session.upstream_compression.adjust_decompression(true);
187        }
188
189        Ok(RequestFilterResult::Unhandled)
190    }
191}