Skip to content

Commit 2963c8d

Browse files
committed
Add configurable request body size with 1GB hard limit
Add VssServiceConfig to make the maximum request body size configurable through the server configuration file, with a hard limit of 1GB. Additionally, add test coverage for 1GB maximum supported value size and verifies that storage backends can handle the configured maximum value size.
1 parent 605168a commit 2963c8d

File tree

5 files changed

+189
-16
lines changed

5 files changed

+189
-16
lines changed

rust/impls/src/postgres_store.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,10 @@ mod tests {
699699
use super::{drop_database, DUMMY_MIGRATION, MIGRATIONS};
700700
use crate::postgres_store::PostgresPlaintextBackend;
701701
use api::define_kv_store_tests;
702+
use api::kv_store::KvStore;
703+
use api::types::{DeleteObjectRequest, GetObjectRequest, KeyValue, PutObjectRequest};
704+
705+
use bytes::Bytes;
702706
use tokio::sync::OnceCell;
703707
use tokio_postgres::NoTls;
704708

@@ -814,4 +818,70 @@ mod tests {
814818

815819
drop_database(POSTGRES_ENDPOINT, DEFAULT_DB, vss_db, NoTls).await.unwrap();
816820
}
821+
822+
#[tokio::test]
823+
async fn supports_objects_up_to_non_large_object_threshold() {
824+
let vss_db = "supports_objects_up_to_non_large_object_threshold";
825+
let _ = drop_database(POSTGRES_ENDPOINT, DEFAULT_DB, vss_db, NoTls).await;
826+
827+
const MAXIMUM_SUPPORTED_VALUE_SIZE: usize = 1024 * 1024 * 1024;
828+
const PROTOCOL_OVERHEAD_MARGIN: usize = 150;
829+
830+
// Construct entry that's for a field that's the maximum size of a non-"large_object" object
831+
let large_value = vec![0u8; MAXIMUM_SUPPORTED_VALUE_SIZE - PROTOCOL_OVERHEAD_MARGIN];
832+
let kv = KeyValue { key: "k1".into(), version: 0, value: Bytes::from(large_value) };
833+
834+
{
835+
let store =
836+
PostgresPlaintextBackend::new(POSTGRES_ENDPOINT, DEFAULT_DB, vss_db).await.unwrap();
837+
let (start, end) = store.migrate_vss_database(MIGRATIONS).await.unwrap();
838+
assert_eq!(start, MIGRATIONS_START);
839+
assert_eq!(end, MIGRATIONS_END);
840+
assert_eq!(store.get_upgrades_list().await, [MIGRATIONS_START]);
841+
assert_eq!(store.get_schema_version().await, MIGRATIONS_END);
842+
843+
// Round trip with non-large_object of threshold size
844+
845+
store
846+
.put(
847+
"token".to_string(),
848+
PutObjectRequest {
849+
store_id: "store_id".to_string(),
850+
global_version: None,
851+
transaction_items: vec![kv],
852+
delete_items: vec![],
853+
},
854+
)
855+
.await
856+
.unwrap();
857+
858+
let resp_kv = store
859+
.get(
860+
"token".to_string(),
861+
GetObjectRequest { store_id: "store_id".to_string(), key: "k1".to_string() },
862+
)
863+
.await
864+
.unwrap()
865+
.value
866+
.unwrap();
867+
assert_eq!(
868+
resp_kv.value.len(),
869+
MAXIMUM_SUPPORTED_VALUE_SIZE - PROTOCOL_OVERHEAD_MARGIN
870+
);
871+
assert!(resp_kv.value.iter().all(|&b| b == 0));
872+
873+
store
874+
.delete(
875+
"token".to_string(),
876+
DeleteObjectRequest {
877+
store_id: "store_id".to_string(),
878+
key_value: Some(resp_kv),
879+
},
880+
)
881+
.await
882+
.unwrap();
883+
};
884+
885+
drop_database(POSTGRES_ENDPOINT, DEFAULT_DB, vss_db, NoTls).await.unwrap();
886+
}
817887
}

rust/server/src/main.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use auth_impls::jwt::JWTAuthorizer;
2929
use auth_impls::signature::SignatureValidatingAuthorizer;
3030
use impls::postgres_store::{PostgresPlaintextBackend, PostgresTlsBackend};
3131
use util::logger::ServerLogger;
32-
use vss_service::VssService;
32+
use vss_service::{VssService, VssServiceConfig};
3333

3434
mod util;
3535
mod vss_service;
@@ -42,6 +42,16 @@ fn main() {
4242
eprintln!("Failed to load configuration: {}", e);
4343
std::process::exit(-1);
4444
});
45+
let vss_service_config = match &config.max_request_body_size {
46+
Some(size) => match VssServiceConfig::new(*size) {
47+
Ok(config) => config,
48+
Err(e) => {
49+
eprintln!("Configuration validation error: {}", e);
50+
std::process::exit(-1);
51+
},
52+
},
53+
None => VssServiceConfig::default(),
54+
};
4555

4656
let logger = match ServerLogger::init(config.log_level, &config.log_file) {
4757
Ok(logger) => logger,
@@ -162,7 +172,7 @@ fn main() {
162172
match res {
163173
Ok((stream, _)) => {
164174
let io_stream = TokioIo::new(stream);
165-
let vss_service = VssService::new(Arc::clone(&store), Arc::clone(&authorizer));
175+
let vss_service = VssService::new(Arc::clone(&store), Arc::clone(&authorizer), vss_service_config);
166176
runtime.spawn(async move {
167177
if let Err(err) = http1::Builder::new().serve_connection(io_stream, vss_service).await {
168178
warn!("Failed to serve connection: {}", err);

rust/server/src/util/config.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use log::LevelFilter;
22
use serde::Deserialize;
3-
use std::net::SocketAddr;
43
use std::path::PathBuf;
54

65
const BIND_ADDR_VAR: &str = "VSS_BIND_ADDRESS";
6+
const MAX_REQUEST_BODY_SIZE_VAR: &str = "VSS_MAX_REQUEST_BODY_SIZE";
77
const LOG_FILE_VAR: &str = "VSS_LOG_FILE";
88
const LOG_LEVEL_VAR: &str = "VSS_LOG_LEVEL";
99
const JWT_RSA_PEM_VAR: &str = "VSS_JWT_RSA_PEM";
@@ -28,6 +28,7 @@ struct TomlConfig {
2828
#[derive(Deserialize)]
2929
struct ServerConfig {
3030
bind_address: Option<String>,
31+
max_request_body_size: Option<usize>,
3132
}
3233

3334
#[derive(Deserialize)]
@@ -59,6 +60,7 @@ struct LogConfig {
5960
// Encapsulates the result of reading both the environment variables and the config file.
6061
pub(crate) struct Configuration {
6162
pub(crate) bind_address: String,
63+
pub(crate) max_request_body_size: Option<usize>,
6264
pub(crate) rsa_pem: Option<String>,
6365
pub(crate) postgresql_prefix: String,
6466
pub(crate) default_db: String,
@@ -99,10 +101,21 @@ pub(crate) fn load_configuration(config_file_path: Option<&str>) -> Result<Confi
99101
None => TomlConfig::default(), // All fields are set to `None`
100102
};
101103

102-
let bind_address_env = read_env(BIND_ADDR_VAR)?;
104+
let (bind_address_config, max_request_body_size_config) = match server_config {
105+
Some(c) => (c.bind_address, c.max_request_body_size),
106+
None => (None, None),
107+
};
108+
109+
let bind_address_env = read_env(BIND_ADDR_VAR)?
110+
.map(|addr| {
111+
addr.parse().map_err(|e| {
112+
format!("Unable to parse the bind address environment variable: {}", e)
113+
})
114+
})
115+
.transpose()?;
103116
let bind_address = read_config(
104117
bind_address_env,
105-
server_config.and_then(|c| c.bind_address),
118+
bind_address_config,
106119
"VSS server bind address",
107120
BIND_ADDR_VAR,
108121
)?;
@@ -135,6 +148,15 @@ pub(crate) fn load_configuration(config_file_path: Option<&str>) -> Result<Confi
135148
let log_file_config: Option<PathBuf> = log_config.and_then(|config| config.file);
136149
let log_file = log_file_env.or(log_file_config).unwrap_or(PathBuf::from("vss.log"));
137150

151+
let max_request_body_size_env = read_env(MAX_REQUEST_BODY_SIZE_VAR)?
152+
.map(|mrbs| {
153+
mrbs.parse::<usize>().map_err(|e| {
154+
format!("Unable to parse the maximum request body size environment variable: {}", e)
155+
})
156+
})
157+
.transpose()?;
158+
let max_request_body_size = max_request_body_size_env.or(max_request_body_size_config);
159+
138160
let rsa_pem_env = read_env(JWT_RSA_PEM_VAR)?;
139161
let rsa_pem = rsa_pem_env.or(jwt_auth_config.and_then(|config| config.rsa_pem));
140162

@@ -187,6 +209,7 @@ pub(crate) fn load_configuration(config_file_path: Option<&str>) -> Result<Confi
187209

188210
Ok(Configuration {
189211
bind_address,
212+
max_request_body_size,
190213
log_file,
191214
log_level,
192215
rsa_pem,

rust/server/src/vss_service.rs

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use http_body_util::{BodyExt, Full};
1+
use http_body_util::{BodyExt, Full, Limited};
22
use hyper::body::{Bytes, Incoming};
33
use hyper::service::Service;
44
use hyper::{Request, Response, StatusCode};
@@ -22,15 +22,44 @@ use log::{debug, trace};
2222

2323
use crate::util::KeyValueVecKeyPrinter;
2424

25+
const MAXIMUM_REQUEST_BODY_SIZE: usize = 1024 * 1024 * 1024;
26+
27+
#[derive(Clone, Copy)]
28+
pub(crate) struct VssServiceConfig {
29+
maximum_request_body_size: usize,
30+
}
31+
32+
impl VssServiceConfig {
33+
pub fn new(maximum_request_body_size: usize) -> Result<Self, String> {
34+
if maximum_request_body_size > MAXIMUM_REQUEST_BODY_SIZE {
35+
return Err(format!(
36+
"Maximum request body size {} exceeds maximum {}",
37+
maximum_request_body_size, MAXIMUM_REQUEST_BODY_SIZE
38+
));
39+
}
40+
41+
Ok(Self { maximum_request_body_size })
42+
}
43+
}
44+
45+
impl Default for VssServiceConfig {
46+
fn default() -> Self {
47+
Self { maximum_request_body_size: MAXIMUM_REQUEST_BODY_SIZE }
48+
}
49+
}
50+
2551
#[derive(Clone)]
2652
pub struct VssService {
2753
store: Arc<dyn KvStore>,
2854
authorizer: Arc<dyn Authorizer>,
55+
config: VssServiceConfig,
2956
}
3057

3158
impl VssService {
32-
pub(crate) fn new(store: Arc<dyn KvStore>, authorizer: Arc<dyn Authorizer>) -> Self {
33-
Self { store, authorizer }
59+
pub(crate) fn new(
60+
store: Arc<dyn KvStore>, authorizer: Arc<dyn Authorizer>, config: VssServiceConfig,
61+
) -> Self {
62+
Self { store, authorizer, config }
3463
}
3564
}
3665

@@ -45,22 +74,51 @@ impl Service<Request<Incoming>> for VssService {
4574
let store = Arc::clone(&self.store);
4675
let authorizer = Arc::clone(&self.authorizer);
4776
let path = req.uri().path().to_owned();
77+
let maximum_request_body_size = self.config.maximum_request_body_size;
4878

4979
Box::pin(async move {
5080
let prefix_stripped_path = path.strip_prefix(BASE_PATH_PREFIX).unwrap_or_default();
5181

5282
match prefix_stripped_path {
5383
"/getObject" => {
54-
handle_request(store, authorizer, req, handle_get_object_request).await
84+
handle_request(
85+
store,
86+
authorizer,
87+
req,
88+
maximum_request_body_size,
89+
handle_get_object_request,
90+
)
91+
.await
5592
},
5693
"/putObjects" => {
57-
handle_request(store, authorizer, req, handle_put_object_request).await
94+
handle_request(
95+
store,
96+
authorizer,
97+
req,
98+
maximum_request_body_size,
99+
handle_put_object_request,
100+
)
101+
.await
58102
},
59103
"/deleteObject" => {
60-
handle_request(store, authorizer, req, handle_delete_object_request).await
104+
handle_request(
105+
store,
106+
authorizer,
107+
req,
108+
maximum_request_body_size,
109+
handle_delete_object_request,
110+
)
111+
.await
61112
},
62113
"/listKeyVersions" => {
63-
handle_request(store, authorizer, req, handle_list_object_request).await
114+
handle_request(
115+
store,
116+
authorizer,
117+
req,
118+
maximum_request_body_size,
119+
handle_list_object_request,
120+
)
121+
.await
64122
},
65123
_ => {
66124
let error_msg = "Invalid request path.".as_bytes();
@@ -140,7 +198,7 @@ async fn handle_request<
140198
Fut: Future<Output = Result<R, VssError>> + Send,
141199
>(
142200
store: Arc<dyn KvStore>, authorizer: Arc<dyn Authorizer>, request: Request<Incoming>,
143-
handler: F,
201+
maximum_request_body_size: usize, handler: F,
144202
) -> Result<<VssService as Service<Request<Incoming>>>::Response, hyper::Error> {
145203
let (parts, body) = request.into_parts();
146204
let headers_map = parts
@@ -155,8 +213,17 @@ async fn handle_request<
155213
Ok(auth_response) => auth_response.user_token,
156214
Err(e) => return Ok(build_error_response(e)),
157215
};
158-
// TODO: we should bound the amount of data we read to avoid allocating too much memory.
159-
let bytes = body.collect().await?.to_bytes();
216+
217+
let limited_body = Limited::new(body, maximum_request_body_size);
218+
let bytes = match limited_body.collect().await {
219+
Ok(body) => body.to_bytes(),
220+
Err(_) => {
221+
return Ok(Response::builder()
222+
.status(StatusCode::PAYLOAD_TOO_LARGE)
223+
.body(Full::new(Bytes::from("Request body too large")))
224+
.unwrap());
225+
},
226+
};
160227
match T::decode(bytes) {
161228
Ok(request) => match handler(store.clone(), user_token, request).await {
162229
Ok(response) => Ok(Response::builder()

rust/server/vss-server-config.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
[server_config]
22
bind_address = "127.0.0.1:8080" # Optional in TOML, can be overridden by env var `VSS_BIND_ADDRESS`
3+
# Maximum request body size in bytes. Can be set here or be overridden by env var 'VSS_MAX_REQUEST_BODY_SIZE'
4+
# Defaults to the maximum possible value of 1 GB if unset.
5+
# max_request_body_size = 1073741824
36

47
# Uncomment the table below to verify JWT tokens in the HTTP Authorization header against the given RSA public key,
58
# can be overridden by env var `VSS_JWT_RSA_PEM`
@@ -28,4 +31,4 @@ vss_database = "vss" # Optional in TOML, can be overridden by env var
2831

2932
# [log_config]
3033
# level = "debug" # Uncomment, or set env var `VSS_LOG_LEVEL` to set the log level, the default is "debug"
31-
# file = "vss.log" # Uncomment, or set env var `VSS_LOG_FILE` to set the log file path, the default is "vss.log"
34+
# file = "vss.log" # Uncomment, or set env var `VSS_LOG_FILE` to set the log file path, the default is "vss.log"

0 commit comments

Comments
 (0)