]> git.proxmox.com Git - pve-installer.git/blob - proxmox-auto-installer/src/fetch_plugins/http.rs
auto-installer: fetch: add http plugin to fetch answer
[pve-installer.git] / proxmox-auto-installer / src / fetch_plugins / http.rs
1 use anyhow::{bail, Error, Result};
2 use log::info;
3 use std::{
4 fs::{self, read_to_string},
5 path::Path,
6 process::Command,
7 };
8
9 use crate::fetch_plugins::utils::{post, sysinfo};
10
11 use super::utils;
12
13 static CERT_FINGERPRINT_FILE: &str = "cert_fingerprint.txt";
14 static ANSWER_SUBDOMAIN: &str = "proxmoxinst";
15 static ANSWER_SUBDOMAIN_FP: &str = "proxmoxinst-fp";
16
17 // It is possible to set custom DHPC options. Option numbers 224 to 254 [0].
18 // To use them with dhclient, we need to configure it to request them and what they should be
19 // called.
20 //
21 // e.g. /etc/dhcp/dhclient.conf:
22 // ```
23 // option proxmoxinst-url code 250 = text;
24 // option proxmoxinst-fp code 251 = text;
25 // also request proxmoxinst-url, proxmoxinst-fp;
26 // ```
27 //
28 // The results will end up in the /var/lib/dhcp/dhclient.leases file from where we can fetch them
29 //
30 // [0] https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml
31 static DHCP_URL_OPTION: &str = "proxmoxinst-url";
32 static DHCP_FP_OPTION: &str = "proxmoxinst-fp";
33 static DHCP_LEASE_FILE: &str = "/var/lib/dhcp/dhclient.leases";
34
35 pub struct FetchFromHTTP;
36
37 impl FetchFromHTTP {
38 /// Will try to fetch the answer.toml by sending a HTTP POST request. The URL can be configured
39 /// either via DHCP or DNS.
40 /// DHCP options are checked first. The SSL certificate need to be either trusted by the root
41 /// certs or a SHA256 fingerprint needs to be provided. The SHA256 SSL fingerprint can either
42 /// be placed in a `cert_fingerprint.txt` file in the `proxmoxinst` partition, as DHCP option,
43 /// or as DNS TXT record. If provided, the `cert_fingerprint.txt` file has preference.
44 pub fn get_answer() -> Result<String> {
45 info!("Checking for certificate fingerprint in file.");
46 let mut fingerprint: Option<String> = match Self::get_cert_fingerprint_from_file() {
47 Ok(fp) => Some(fp),
48 Err(err) => {
49 info!("{err}");
50 None
51 }
52 };
53
54 let answer_url: String;
55
56 (answer_url, fingerprint) = match Self::fetch_dhcp(fingerprint.clone()) {
57 Ok((url, fp)) => (url, fp),
58 Err(err) => {
59 info!("{err}");
60 Self::fetch_dns(fingerprint.clone())?
61 }
62 };
63
64 if fingerprint.is_some() {
65 let fp = fingerprint.clone();
66 fs::write("/tmp/cert_fingerprint", fp.unwrap()).ok();
67 }
68
69 info!("Gathering system information.");
70 let payload = sysinfo::get_sysinfo(false)?;
71 info!("Sending POST request to '{answer_url}'.");
72 let answer = post::call(answer_url, fingerprint.as_deref(), payload)?;
73 Ok(answer)
74 }
75
76 /// Reads certificate fingerprint from file
77 pub fn get_cert_fingerprint_from_file() -> Result<String> {
78 let mount_path = utils::mount_proxmoxinst_part()?;
79 let cert_path = Path::new(mount_path.as_str()).join(CERT_FINGERPRINT_FILE);
80 match cert_path.try_exists() {
81 Ok(true) => {
82 info!("Found certifacte fingerprint file.");
83 Ok(fs::read_to_string(cert_path)?.trim().into())
84 }
85 _ => Err(Error::msg(format!(
86 "could not find cert fingerprint file expected at: {}",
87 cert_path.display()
88 ))),
89 }
90 }
91
92 /// Fetches search domain from resolv.conf file
93 fn get_search_domain() -> Result<String> {
94 info!("Retrieving default search domain.");
95 for line in read_to_string("/etc/resolv.conf")?.lines() {
96 if let Some((key, value)) = line.split_once(' ') {
97 if key == "search" {
98 return Ok(value.trim().into());
99 }
100 }
101 }
102 Err(Error::msg("Could not find search domain in resolv.conf."))
103 }
104
105 /// Runs a TXT DNS query on the domain provided
106 fn query_txt_record(query: String) -> Result<String> {
107 info!("Querying TXT record for '{query}'");
108 let url: String;
109 match Command::new("dig")
110 .args(["txt", "+short"])
111 .arg(&query)
112 .output()
113 {
114 Ok(output) => {
115 if output.status.success() {
116 url = String::from_utf8(output.stdout)?
117 .replace('"', "")
118 .trim()
119 .into();
120 if url.is_empty() {
121 bail!("Got empty response.");
122 }
123 } else {
124 bail!(
125 "Error querying DNS record '{query}' : {}",
126 String::from_utf8(output.stderr)?
127 );
128 }
129 }
130 Err(err) => bail!("Error querying DNS record '{query}': {err}"),
131 }
132 info!("Found: '{url}'");
133 Ok(url)
134 }
135
136 /// Tries to fetch answer URL and SSL fingerprint info from DNS
137 fn fetch_dns(mut fingerprint: Option<String>) -> Result<(String, Option<String>)> {
138 let search_domain = Self::get_search_domain()?;
139
140 let answer_url = match Self::query_txt_record(format!("{ANSWER_SUBDOMAIN}.{search_domain}"))
141 {
142 Ok(url) => url,
143 Err(err) => bail!("{err}"),
144 };
145
146 if fingerprint.is_none() {
147 fingerprint =
148 match Self::query_txt_record(format!("{ANSWER_SUBDOMAIN_FP}.{search_domain}")) {
149 Ok(fp) => Some(fp),
150 Err(err) => {
151 info!("{err}");
152 None
153 }
154 };
155 }
156 Ok((answer_url, fingerprint))
157 }
158
159 /// Tries to fetch answer URL and SSL fingerprint info from DHCP options
160 fn fetch_dhcp(mut fingerprint: Option<String>) -> Result<(String, Option<String>)> {
161 let leases = fs::read_to_string(DHCP_LEASE_FILE)?;
162
163 let mut answer_url: Option<String> = None;
164
165 let url_match = format!("option {DHCP_URL_OPTION}");
166 let fp_match = format!("option {DHCP_FP_OPTION}");
167
168 for line in leases.lines() {
169 if answer_url.is_none() && line.trim().starts_with(url_match.as_str()) {
170 answer_url = Self::strip_dhcp_option(line.split(' ').nth_back(0));
171 }
172 if fingerprint.is_none() && line.trim().starts_with(fp_match.as_str()) {
173 fingerprint = Self::strip_dhcp_option(line.split(' ').nth_back(0));
174 }
175 }
176
177 let answer_url = match answer_url {
178 None => bail!("No DHCP option found for fetch URL."),
179 Some(url) => url,
180 };
181
182 Ok((answer_url, fingerprint))
183 }
184
185 /// Clean DHCP option string
186 fn strip_dhcp_option(value: Option<&str>) -> Option<String> {
187 // value is expected to be in format: "value";
188 value.map(|value| String::from(&value[1..value.len() - 2]))
189 }
190 }