diff --git a/Makefile b/Makefile index 0055abd..0b4ffdc 100644 --- a/Makefile +++ b/Makefile @@ -51,7 +51,8 @@ gen: $(call package_template,PVE,pve_rs) perl ./scripts/genpackage.pl Common \ Proxmox::RS::APT::Repositories \ - Proxmox::RS::CalendarEvent + Proxmox::RS::CalendarEvent \ + Proxmox::RS::Subscription perl ./scripts/genpackage.pl PVE \ PVE::RS::APT::Repositories \ PVE::RS::OpenId \ diff --git a/common/src/mod.rs b/common/src/mod.rs index 738657e..b8b843e 100644 --- a/common/src/mod.rs +++ b/common/src/mod.rs @@ -1,2 +1,3 @@ pub mod apt; mod calendar_event; +mod subscription; diff --git a/common/src/subscription.rs b/common/src/subscription.rs new file mode 100644 index 0000000..7d93cc0 --- /dev/null +++ b/common/src/subscription.rs @@ -0,0 +1,165 @@ +mod client { + use anyhow::{format_err, Error}; + use http::Response; + + pub(crate) struct UreqClient { + pub user_agent: String, + pub proxy: Option, + } + + impl UreqClient { + fn agent(&self) -> Result { + let mut builder = ureq::AgentBuilder::new(); + if let Some(proxy) = &self.proxy { + builder = builder.proxy(ureq::Proxy::new(proxy)?); + } + + Ok(builder.build()) + } + + fn exec_request( + &self, + req: ureq::Request, + body: Option<&str>, + ) -> Result, Error> { + let req = req.set("User-Agent", &self.user_agent); + let res = match body { + Some(body) => req.send_string(body), + None => req.call(), + }?; + + let mut builder = http::response::Builder::new() + .status(http::status::StatusCode::from_u16(res.status())?); + + for header in res.headers_names() { + if let Some(value) = res.header(&header) { + builder = builder.header(header, value); + } + } + builder + .body(res.into_string()?) + .map_err(|err| format_err!("Failed to convert HTTP response - {err}")) + } + } + + impl proxmox_http::HttpClient for UreqClient { + fn get(&self, uri: &str) -> Result, Error> { + let req = self.agent()?.get(uri); + + self.exec_request(req, None) + } + + fn post( + &self, + uri: &str, + body: Option<&str>, + content_type: Option<&str>, + ) -> Result, Error> { + let mut req = self.agent()?.post(uri); + if let Some(content_type) = content_type { + req = req.set("Content-Type", content_type); + } + + self.exec_request(req, body) + } + } +} + +#[perlmod::package(name = "Proxmox::RS::Subscription")] +mod export { + use anyhow::{bail, format_err, Error}; + + use proxmox_subscription::SubscriptionInfo; + use proxmox_sys::fs::{file_get_contents, CreateOptions}; + + use super::client::UreqClient; + + #[export] + fn read_subscription(path: String) -> Result, Error> { + let key = file_get_contents("/usr/share/keyrings/proxmox-offline-signing-key.pub")?; + let key = openssl::pkey::PKey::public_key_from_pem(&key) + .map_err(|err| format_err!("Failed to parse public key - {err}"))?; + + proxmox_subscription::files::read_subscription(path, &key) + } + + #[export] + fn write_subscription( + path: String, + apt_path: String, + url: &str, + info: SubscriptionInfo, + ) -> Result<(), Error> { + let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640); + let www_data = nix::unistd::Group::from_name("www-data")? + .ok_or(format_err!("no 'www-data' group found!"))? + .gid; + let opts = CreateOptions::new() + .perm(mode) + .owner(nix::unistd::ROOT) + .group(www_data); + + let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600); + let apt_opts = CreateOptions::new().perm(mode).owner(nix::unistd::ROOT); + + proxmox_subscription::files::write_subscription(path, opts, &info).and_then(|_| { + proxmox_subscription::files::update_apt_auth( + apt_path, + apt_opts, + url, + info.key, + info.serverid, + ) + }) + } + + #[export] + fn delete_subscription(path: String, apt_path: String, url: &str) -> Result<(), Error> { + let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600); + let apt_opts = CreateOptions::new().perm(mode).owner(nix::unistd::ROOT); + + proxmox_subscription::files::delete_subscription(path).and_then(|_| { + proxmox_subscription::files::update_apt_auth(apt_path, apt_opts, url, None, None) + }) + } + + #[export] + fn check_subscription( + key: String, + server_id: String, + product_url: String, + user_agent: String, + proxy: Option, + ) -> Result { + let client = UreqClient { user_agent, proxy }; + + proxmox_subscription::check::check_subscription(key, server_id, product_url, client) + } + + #[export] + fn check_server_id(mut info: SubscriptionInfo) -> SubscriptionInfo { + info.check_server_id(); + info + } + + #[export] + fn check_age(mut info: SubscriptionInfo, re_check: bool) -> SubscriptionInfo { + info.check_age(re_check); + info + } + + #[export] + fn check_signature(mut info: SubscriptionInfo) -> Result { + if !info.is_signed() { + bail!("SubscriptionInfo is not signed!"); + } + + let key = file_get_contents("/usr/share/keyrings/proxmox-offline-signing-key.pub")?; + let key = openssl::pkey::PKey::public_key_from_pem(&key) + .map_err(|err| format_err!("Failed to parse public key - {err}"))?; + + info.check_signature(&key); + + Ok(info) + } +} diff --git a/pmg-rs/Cargo.toml b/pmg-rs/Cargo.toml index 8d9a9bc..7b6f706 100644 --- a/pmg-rs/Cargo.toml +++ b/pmg-rs/Cargo.toml @@ -21,16 +21,22 @@ crate-type = [ "cdylib" ] [dependencies] anyhow = "1.0" hex = "0.4" +http = "0.2.7" libc = "0.2" nix = "0.24" +openssl = "0.10.40" serde = "1.0" serde_bytes = "0.11.3" serde_json = "1.0" +ureq = { version = "2.4", features = ["native-certs"] } url = "2" perlmod = { version = "0.13", features = [ "exporter" ] } proxmox-acme-rs = { version = "0.4", features = ["client"] } proxmox-apt = "0.8.0" +proxmox-http = { version = "0.6.3", features = ["client-trait"] } +proxmox-subscription = "0.1" +proxmox-sys = "0.3" proxmox-tfa = { version = "2", features = ["api"] } proxmox-time = "1.1.3" diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml index 8764405..3f82de6 100644 --- a/pve-rs/Cargo.toml +++ b/pve-rs/Cargo.toml @@ -19,16 +19,22 @@ anyhow = "1.0" base32 = "0.4" base64 = "0.13" hex = "0.4" +http = "0.2.7" libc = "0.2" nix = "0.24" +openssl = "0.10.40" serde = "1.0" serde_bytes = "0.11" serde_json = "1.0" +ureq = { version = "2.4", features = ["native-certs"] } url = "2" perlmod = { version = "0.13", features = [ "exporter" ] } proxmox-apt = "0.8" +proxmox-http = { version = "0.6.3", features = ["client-trait"] } proxmox-openid = "0.9.5" +proxmox-subscription = "0.1" +proxmox-sys = "0.3" proxmox-tfa = { version = "2", features = ["api"] } proxmox-time = "1.1.3"