From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 036281FF13C for ; Thu, 30 Apr 2026 14:49:46 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id D62027711; Thu, 30 Apr 2026 14:49:45 +0200 (CEST) From: Christoph Heiss To: pdm-devel@lists.proxmox.com Subject: [PATCH installer v4 26/40] common: http: allow passing custom headers to post() Date: Thu, 30 Apr 2026 14:46:55 +0200 Message-ID: <20260430124712.1614305-27-c.heiss@proxmox.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260430124712.1614305-1-c.heiss@proxmox.com> References: <20260430124712.1614305-1-c.heiss@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1777553278991 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.075 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment 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: GSPMLXINAZ5GMP4M4UU2JUOEQAHPDU53 X-Message-ID-Hash: GSPMLXINAZ5GMP4M4UU2JUOEQAHPDU53 X-MailFrom: c.heiss@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 Datacenter Manager development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Add an additional parameter to allow passing in additional headers. No functional changes. Signed-off-by: Christoph Heiss --- Changes v3 -> v4: * replace http response tuple with struct * move unrelated formatting changes to own patch Changes v2 -> v3: * new patch .../src/fetch_plugins/http.rs | 12 ++- proxmox-installer-common/src/http.rs | 77 +++++++++++++++++-- proxmox-post-hook/src/main.rs | 4 +- 3 files changed, 83 insertions(+), 10 deletions(-) diff --git a/proxmox-fetch-answer/src/fetch_plugins/http.rs b/proxmox-fetch-answer/src/fetch_plugins/http.rs index e2fd633..163ebcd 100644 --- a/proxmox-fetch-answer/src/fetch_plugins/http.rs +++ b/proxmox-fetch-answer/src/fetch_plugins/http.rs @@ -7,6 +7,7 @@ use std::{ }; use proxmox_auto_installer::{sysinfo::SysInfo, utils::HttpOptions}; +use proxmox_installer_common::http::{self, header::HeaderMap}; static ANSWER_URL_SUBDOMAIN: &str = "proxmox-auto-installer"; static ANSWER_CERT_FP_SUBDOMAIN: &str = "proxmox-auto-installer-cert-fingerprint"; @@ -130,9 +131,14 @@ impl FetchFromHTTP { let payload = HttpFetchPayload::as_json()?; info!("Sending POST request to '{answer_url}'."); - let answer = - proxmox_installer_common::http::post(&answer_url, fingerprint.as_deref(), payload)?; - Ok(answer) + + Ok(http::post( + &answer_url, + fingerprint.as_deref(), + HeaderMap::new(), + payload, + )? + .body) } /// Fetches search domain from resolv.conf file diff --git a/proxmox-installer-common/src/http.rs b/proxmox-installer-common/src/http.rs index 7662673..445d6c4 100644 --- a/proxmox-installer-common/src/http.rs +++ b/proxmox-installer-common/src/http.rs @@ -4,6 +4,7 @@ use rustls::{ClientConfig, ClientConnection, StreamOwned}; use sha2::{Digest, Sha256}; use std::fmt; use std::io::{Read, Write}; +use std::str::FromStr; use std::sync::Arc; use std::time::Duration; use ureq::Agent; @@ -13,6 +14,9 @@ use ureq::unversioned::transport::{ Transport, TransportAdapter, }; +// Re-export for conviencence when using post() +pub use ureq::http::header; + /// Builds an [`Agent`] with TLS suitable set up, depending whether a custom fingerprint was /// supplied or not. If a fingerprint was supplied, only matching certificates will be accepted. /// Otherwise, the system certificate store is loaded. @@ -88,6 +92,39 @@ pub fn get_as_bytes(url: &str, fingerprint: Option<&str>, max_size: usize) -> Re Ok(result) } +/// Content type of the body as returned in the `Content-Type` in the HTTP response, if present. +#[derive(Clone, PartialEq, Eq)] +pub enum ContentType { + /// application/json + Json, + /// application/toml + Toml, + /// Any other content type, unparsed. + Other(String), +} + +impl FromStr for ContentType { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + if s.starts_with("application/json") { + Ok(ContentType::Json) + } else if s.starts_with("application/toml") { + Ok(ContentType::Toml) + } else { + Ok(ContentType::Other(s.to_owned())) + } + } +} + +/// HTTP response of a successful HTTP POST request. +pub struct Response { + /// Raw body content received. + pub body: String, + /// Content type, as given in the `Content-Type` response header, if present. + pub content_type: Option, +} + /// Issues a POST request with the payload (JSON). Optionally a SHA256 fingerprint can be used to /// check the cert against it, instead of the regular cert validation. /// To gather the sha256 fingerprint you can use the following command: @@ -95,18 +132,46 @@ pub fn get_as_bytes(url: &str, fingerprint: Option<&str>, max_size: usize) -> Re /// openssl s_client -connect :443 < /dev/null 2>/dev/null | openssl x509 -fingerprint -sha256 -noout -in /dev/stdin /// ``` /// +/// The `Content-Type` header is automatically set to `application/json` for the request. +/// /// # Arguments /// * `url` - URL to call /// * `fingerprint` - SHA256 cert fingerprint if certificate pinning should be used. Optional. +/// * `headers` - Additional headers to add to the request. /// * `payload` - The payload to send to the server. Expected to be a JSON formatted string. -pub fn post(url: &str, fingerprint: Option<&str>, payload: String) -> Result { +/// +/// # Returns +/// +/// A [`Response`] holding the body contents and the `Content-Type` header, if present. +pub fn post( + url: &str, + fingerprint: Option<&str>, + headers: header::HeaderMap, + payload: String, +) -> Result { // TODO: read_to_string limits the size to 10 MB, should be increase that? - Ok(build_agent(fingerprint)? + + let mut request = build_agent(fingerprint)? .post(url) - .header("Content-Type", "application/json; charset=utf-8") - .send(&payload)? - .body_mut() - .read_to_string()?) + .header("Content-Type", "application/json; charset=utf-8"); + + for (name, value) in headers.iter() { + request = request.header(name, value); + } + + let mut response = request.send(&payload)?; + + let content_type = response + .headers() + .get(header::CONTENT_TYPE) + .and_then(|h| h.to_str().ok()) + .map(ContentType::from_str) + .transpose()?; + + Ok(Response { + body: response.body_mut().read_to_string()?, + content_type, + }) } #[derive(Debug)] diff --git a/proxmox-post-hook/src/main.rs b/proxmox-post-hook/src/main.rs index a792b6d..b1aab45 100644 --- a/proxmox-post-hook/src/main.rs +++ b/proxmox-post-hook/src/main.rs @@ -27,6 +27,7 @@ use proxmox_auto_installer::{ }, udevinfo::{UdevInfo, UdevProperties}, }; +use proxmox_installer_common::http::{self, header::HeaderMap}; use proxmox_installer_common::{ options::{Disk, FsType, NetworkOptions}, setup::{ @@ -727,9 +728,10 @@ fn do_main() -> Result<()> { ); } - proxmox_installer_common::http::post( + http::post( url, cert_fingerprint.as_deref(), + HeaderMap::new(), serde_json::to_string(&info)?, )?; } else { -- 2.53.0