1use regex::Regex;
2use reqwest::header::HeaderMap;
3use serde::{de::DeserializeOwned, Serialize};
4use serde_json::json;
5use std::fmt::Write;
6use std::{collections::HashMap, error::Error};
7use tracing::{debug, trace};
8use zeroize::Zeroizing;
9
10pub mod api;
11pub mod objects;
12pub mod panorama;
13pub mod policies;
14
15fn form_params(args: HashMap<&str, &str>) -> String {
17 let mut params = String::new();
18 for (k, v) in args {
19 trace!("Writing key {k} with value {v} to query parameters");
20 if !params.starts_with('?') {
21 _ = write!(params, "?{k}={v}")
22 } else {
23 _ = write!(params, "&{k}={v}");
24 }
25 }
26
27 params
28}
29
30fn common_params(
32 location: Option<api::Location>,
33 device_group: Option<&str>,
34 name: &str,
35) -> String {
36 let mut args = HashMap::new();
37 if let Some(location) = location {
38 args.insert("location", location.to_str());
39 }
40 if let Some(device_group) = device_group {
41 args.insert("device-group", device_group);
42 }
43 args.insert("name", name);
44
45 form_params(args)
46}
47
48pub struct Session {
49 pub client: reqwest::Client,
50 pub base_url: String,
51}
52
53impl Session {
54 pub async fn new(
56 ip: &str,
57 user: &str,
58 password: String,
59 allow_insecure: bool,
60 ) -> Result<Session, Box<dyn Error>> {
61 let password = Zeroizing::new(password);
62
63 trace!("Building temp client to retrieve API key");
65 let temp_client = reqwest::Client::builder()
66 .danger_accept_invalid_certs(allow_insecure)
67 .build()?;
68
69 debug!("Sending request to {ip} as {user} for API key");
70 let key_re = Regex::new(r"<key>(.+)</key>").unwrap();
71 let key_resp = temp_client
72 .get(format!(
73 "https://{ip}/api?type=keygen&user={user}&password={}",
74 urlencoding::encode(password.as_str())
75 ))
76 .send()
77 .await?;
78
79 let mut content = key_resp.text().await?;
80 let mut captures = key_re.captures(&content).unwrap();
81 let api_key = if captures.len() > 1 {
82 &captures[1]
83 } else {
84 return Err(Box::new(api::Error {
85 code: 999,
86 message: String::from("Unable to find key, validate provided credentials"),
87 }));
88 };
89
90 trace!("Building client with API key as a default header");
91 let mut headers = HeaderMap::new();
93 headers.insert("X-PAN-KEY", api_key.parse()?);
94 let client = reqwest::Client::builder()
95 .default_headers(headers)
96 .danger_accept_invalid_certs(allow_insecure)
97 .build()?;
98
99 debug!("Sending request to {ip} as {user} for PanOS version");
100 let vers_re = Regex::new(r"<sw-version>(\d+\.\d)(.+)?</sw-version>").unwrap();
102 let vers_resp = client
103 .get(format!("https://{ip}/api?type=version&key={api_key}"))
104 .send()
105 .await?;
106 content = vers_resp.text().await?;
107 captures = vers_re.captures(&content).unwrap();
108 let panos_version = if captures.len() > 1 {
109 &captures[1]
110 } else {
111 return Err(Box::new(api::Error {
112 code: 999,
113 message: String::from("Unable to find version, validate provided API key"),
114 }));
115 };
116
117 Ok(Self {
118 client,
119 base_url: format!("https://{ip}/restapi/v{panos_version}/"),
120 })
121 }
122
123 pub async fn get_all<T: DeserializeOwned + api::Endpoint>(
125 &self,
126 location: Option<api::Location>,
127 device_group: Option<&str>,
128 ) -> Result<Option<Vec<T>>, Box<dyn Error>> {
129 let params = {
130 let mut args = HashMap::new();
131 if let Some(location) = location {
132 args.insert("location", location.to_str());
133 }
134 if let Some(device_group) = device_group {
135 args.insert("device-group", device_group);
136 }
137
138 form_params(args)
139 };
140
141 let uri = T::get_uri();
142 debug!("Sending request to endpoint {uri}");
143 let resp = self
144 .client
145 .get(format!("{}{uri}{params}", self.base_url))
146 .send()
147 .await?;
148 if !resp.status().is_success() {
149 return Err(Box::new(resp.json::<api::Error>().await?));
150 }
151
152 let api_resp: api::ApiResponse<T> = resp.json().await?;
153 if api_resp.result.entry.is_some() {
154 Ok(Some(api_resp.result.entry.unwrap()))
155 } else {
156 Ok(None)
157 }
158 }
159
160 pub async fn get<T: DeserializeOwned + api::Endpoint>(
162 &self,
163 location: Option<api::Location>,
164 device_group: Option<&str>,
165 name: &str,
166 ) -> Result<Option<T>, Box<dyn Error>> {
167 let params = common_params(location, device_group, name);
168
169 let uri = T::get_uri();
170 debug!("Sending request to endpoint {uri}");
171 let resp = self
172 .client
173 .get(format!("{}{uri}{params}", self.base_url))
174 .send()
175 .await?;
176 if !resp.status().is_success() {
177 return Err(Box::new(resp.json::<api::Error>().await?));
178 }
179
180 let api_resp: api::ApiResponse<T> = resp.json().await?;
181 let entries = api_resp.result.entry.unwrap_or_default();
182 if entries.len() == 1 {
183 Ok(Some(entries.into_iter().next().unwrap()))
184 } else {
185 Ok(None)
186 }
187 }
188
189 pub async fn create<T: Serialize + api::Endpoint>(
191 &self,
192 location: Option<api::Location>,
193 device_group: Option<&str>,
194 name: &str,
195 obj: T,
196 ) -> Result<(), Box<dyn Error>> {
197 let params = common_params(location, device_group, name);
198
199 let uri = T::get_uri();
200 debug!("Sending request to endpoint {uri}");
201 let resp = self
202 .client
203 .post(format!("{}{uri}{params}", self.base_url))
204 .json(&json!({ "entry": obj }))
205 .send()
206 .await?;
207 if !resp.status().is_success() {
208 return Err(Box::new(resp.json::<api::Error>().await?));
209 }
210
211 Ok(())
212 }
213
214 pub async fn edit<T: Serialize + api::Endpoint>(
216 &self,
217 location: Option<api::Location>,
218 device_group: Option<&str>,
219 name: &str,
220 obj: T,
221 ) -> Result<(), Box<dyn Error>> {
222 let params = common_params(location, device_group, name);
223
224 let uri = T::get_uri();
225 debug!("Sending request to endpoint {uri}");
226 let resp = self
227 .client
228 .put(format!("{}{uri}{params}", self.base_url))
229 .json(&json!({ "entry": obj }))
230 .send()
231 .await?;
232 if !resp.status().is_success() {
233 return Err(Box::new(resp.json::<api::Error>().await?));
234 }
235
236 Ok(())
237 }
238
239 pub async fn delete<T: api::Endpoint>(
241 &self,
242 location: Option<api::Location>,
243 device_group: Option<&str>,
244 name: &str,
245 ) -> Result<(), Box<dyn Error>> {
246 let params = common_params(location, device_group, name);
247
248 let uri = T::get_uri();
249 debug!("Sending request to endpoint {uri}");
250 let resp = self
251 .client
252 .delete(format!("{}{uri}{params}", self.base_url))
253 .send()
254 .await?;
255 if !resp.status().is_success() {
256 return Err(Box::new(resp.json::<api::Error>().await?));
257 }
258
259 Ok(())
260 }
261
262 pub async fn rename<T: api::Endpoint>(
264 &self,
265 location: api::Location,
266 device_group: Option<&str>,
267 name: &str,
268 new_name: &str,
269 ) -> Result<(), Box<dyn Error>> {
270 let params = {
271 let mut args = HashMap::new();
272 args.insert("location", location.to_str());
273 if let Some(device_group) = device_group {
274 args.insert("device-group", device_group);
275 }
276 args.insert("name", name);
277 args.insert("new-name", new_name);
278
279 form_params(args)
280 };
281
282 let uri = T::get_uri();
283 debug!("Sending request to endpoint {uri}");
284 let resp = self
285 .client
286 .post(format!("{}{uri}:rename{params}", self.base_url))
287 .send()
288 .await?;
289 if !resp.status().is_success() {
290 return Err(Box::new(resp.json::<api::Error>().await?));
291 }
292
293 Ok(())
294 }
295
296 pub async fn relocate<T: api::Endpoint>(
298 &self,
299 location: api::Location,
300 device_group: Option<&str>,
301 where_to: api::Where,
302 dst: Option<&str>,
303 ) -> Result<(), Box<dyn Error>> {
304 let params = {
305 let mut args = HashMap::new();
306 args.insert("location", location.to_str());
307 if let Some(device_group) = device_group {
308 args.insert("device-group", device_group);
309 }
310 args.insert("where", where_to.to_str());
311
312 match where_to {
313 api::Where::Before | api::Where::After => {
314 if let Some(dst) = dst {
315 args.insert("dst", dst);
316 } else {
317 let err = api::Error {
318 code: 999,
319 message: String::from("dst is required when using before or after"),
320 };
321
322 return Err(Box::new(err));
323 }
324 }
325 _ => (),
326 }
327
328 form_params(args)
329 };
330
331 let uri = T::get_uri();
332 debug!("Sending request to endpoint {uri}");
333 let resp = self
334 .client
335 .post(format!("{}{uri}:move{params}", self.base_url))
336 .send()
337 .await?;
338 if !resp.status().is_success() {
339 return Err(Box::new(resp.json::<api::Error>().await?));
340 }
341
342 Ok(())
343 }
344}