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}