From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id BFD131FF0AB for ; Wed, 07 Oct 2026 14:34:38 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 4034721488; Wed, 07 Oct 2026 14:34:34 +0200 (CEST) From: Christian Ebner To: pbs-devel@lists.proxmox.com Subject: [PATCH proxmox 3/3] s3-client: update request time and signature on retries Date: Wed, 7 Oct 2026 14:34:06 +0200 Message-ID: <20261007123406.429342-4-c.ebner@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261007123406.429342-1-c.ebner@proxmox.com> References: <20261007123406.429342-1-c.ebner@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1791376468854 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.574 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: PC6UEHZG745TRKWIVQI27F5C6NYMPL3H X-Message-ID-Hash: PC6UEHZG745TRKWIVQI27F5C6NYMPL3H X-MailFrom: c.ebner@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Backup Server development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Currently request being retried do not update the request time related information, keeping the initial requests state. This might however cause issues if in-between the retries lays a long period of time, resulting in `RequestTimeTooSkewed` errors. To fix this, inline the prepare() helper into its only call site, allowing to efficiently reuse already calculated payload digest and size information within the send() helper. By early splitting the request into parts and keeping the body as cheaply (via Bytes) clone()-able state, the request is prepared setting common headers, only calculating request time and signature right before sending individual requests, including retries. Signed-off-by: Christian Ebner --- proxmox-s3-client/src/client.rs | 103 ++++++++++++++------------------ 1 file changed, 46 insertions(+), 57 deletions(-) diff --git a/proxmox-s3-client/src/client.rs b/proxmox-s3-client/src/client.rs index 631bbd0f..1cfac893 100644 --- a/proxmox-s3-client/src/client.rs +++ b/proxmox-s3-client/src/client.rs @@ -398,59 +398,6 @@ impl S3Client { )) } - /// Prepare API request by adding commonly required headers and perform request signing - async fn prepare(&self, mut request: Request) -> Result, Error> { - let host_header = request - .uri() - .authority() - .ok_or_else(|| format_err!("request missing authority"))? - .to_string(); - - // Content verification for aws s3 signature - let mut hasher = Sha256::new(); - let contents = request - .body() - .as_bytes() - .ok_or_else(|| format_err!("cannot prepare request with streaming body"))?; - hasher.update(contents); - // Use MD5 as upload integrity check, as other methods are not supported by all S3 object - // store providers and might be ignored and this is recommended by AWS as described in - // https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html#API_PutObject_RequestSyntax - let payload_md5 = md5::compute(contents); - let payload_digest = hex::encode(hasher.finish()); - let payload_len = contents.len(); - - request - .headers_mut() - .insert("host", HeaderValue::from_str(&host_header)?); - request.headers_mut().insert( - "x-amz-content-sha256", - HeaderValue::from_str(&payload_digest)?, - ); - - let set_content_length_header = match request.method() { - &Method::PUT | &Method::POST => true, - &Method::DELETE if payload_len > 0 => true, - _ => false, - }; - if set_content_length_header { - request.headers_mut().insert( - header::CONTENT_LENGTH, - HeaderValue::from_str(&payload_len.to_string())?, - ); - } - if payload_len > 0 { - let md5_digest = proxmox_base64::encode(*payload_md5); - request - .headers_mut() - .insert("Content-MD5", HeaderValue::from_str(&md5_digest)?); - } - - self.sign_request(&mut request, &payload_digest)?; - - Ok(request) - } - /// Set or update the `x-amz-date` header to the current time, calculate the request signature /// based on the provided payload digest and set or update the authorization header accordingly. fn sign_request(&self, request: &mut Request, payload_digest: &str) -> Result<(), Error> { @@ -475,17 +422,57 @@ impl S3Client { request: Request, timeout: Option, ) -> Result, Error> { - let request = self.prepare(request).await?; - - let (parts, body) = request.into_parts(); + let (mut parts, body) = request.into_parts(); let body_bytes = body .bytes() .ok_or_else(|| format_err!("cannot prepare request with streaming body"))?; + let host_header = parts + .uri + .authority() + .ok_or_else(|| format_err!("request missing authority"))? + .to_string(); + + // Content verification for aws s3 signature + let mut hasher = Sha256::new(); + hasher.update(&body_bytes); + // Use MD5 as upload integrity check, as other methods are not supported by all S3 object + // store providers and might be ignored and this is recommended by AWS as described in + // https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html#API_PutObject_RequestSyntax + let payload_md5 = md5::compute(&body_bytes); + let payload_digest = hex::encode(hasher.finish()); + let payload_len = body_bytes.len(); + + parts + .headers + .insert("host", HeaderValue::from_str(&host_header)?); + parts.headers.insert( + "x-amz-content-sha256", + HeaderValue::from_str(&payload_digest)?, + ); + + let set_content_length_header = match parts.method { + Method::PUT | Method::POST => true, + Method::DELETE if payload_len > 0 => true, + _ => false, + }; + if set_content_length_header { + parts.headers.insert( + header::CONTENT_LENGTH, + HeaderValue::from_str(&payload_len.to_string())?, + ); + } + if payload_len > 0 { + let md5_digest = proxmox_base64::encode(*payload_md5); + parts + .headers + .insert("Content-MD5", HeaderValue::from_str(&md5_digest)?); + } + let deadline = timeout.map(|timeout| tokio::time::Instant::now() + timeout); for retry in 0..MAX_S3_HTTP_REQUEST_RETRY { - let request = Request::from_parts(parts.clone(), Body::from(body_bytes.clone())); + let mut request = Request::from_parts(parts.clone(), Body::from(body_bytes.clone())); if let Some(limiter) = &self.active_request_rate_limiter { if matches!(parts.method, Method::PUT | Method::POST | Method::DELETE) { let sleep = limiter.register_traffic(Instant::now(), 1); @@ -509,6 +496,8 @@ impl S3Client { } } + self.sign_request(&mut request, &payload_digest)?; + let response = if let Some(deadline) = deadline { tokio::time::timeout_at(deadline, self.client.request(request)) .await -- 2.47.3