]> git.proxmox.com Git - proxmox-backup.git/blame - proxmox-rest-server/src/api_config.rs
proxmox-rest-server: cleanup, access api_auth using a method
[proxmox-backup.git] / proxmox-rest-server / src / api_config.rs
CommitLineData
16b48b81 1use std::collections::HashMap;
2ab5acac
DC
2use std::path::PathBuf;
3use std::time::SystemTime;
4use std::fs::metadata;
fe4cc5b1 5use std::sync::{Arc, Mutex, RwLock};
6680878b
DM
6use std::future::Future;
7use std::pin::Pin;
16b48b81 8
2ab5acac 9use anyhow::{bail, Error, format_err};
7fa9a37c
DM
10use hyper::{Method, Body, Response};
11use hyper::http::request::Parts;
12
f9e3b110 13use handlebars::Handlebars;
2ab5acac 14use serde::Serialize;
16b48b81 15
3483a3b3 16use proxmox::api::{ApiMethod, Router, RpcEnvironmentType, UserInformation};
8e7e2223
TL
17use proxmox::tools::fs::{create_path, CreateOptions};
18
3483a3b3 19use crate::{ApiAuth, AuthError, FileLogger, FileLogOptions, CommandSocket};
a2479cfa 20
6680878b 21pub type GetIndexFn = &'static (dyn for<'a> Fn(Option<String>, Option<String>, &'a ApiConfig, Parts) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'a>> + Send + Sync);
7fa9a37c 22
0d5d15c9 23/// REST server configuration
16b48b81
DM
24pub struct ApiConfig {
25 basedir: PathBuf,
e63e99d6 26 router: &'static Router,
16b48b81 27 aliases: HashMap<String, PathBuf>,
02c7a755 28 env_type: RpcEnvironmentType,
2ab5acac
DC
29 templates: RwLock<Handlebars<'static>>,
30 template_files: RwLock<HashMap<String, (SystemTime, PathBuf)>>,
fe4cc5b1 31 request_log: Option<Arc<Mutex<FileLogger>>>,
36b7085e 32 auth_log: Option<Arc<Mutex<FileLogger>>>,
3483a3b3 33 api_auth: Arc<dyn ApiAuth + Send + Sync>,
7fa9a37c 34 get_index_fn: GetIndexFn,
16b48b81
DM
35}
36
37impl ApiConfig {
0d5d15c9
DM
38 /// Creates a new instance
39 ///
40 /// `basedir` - File lookups are relative to this directory.
41 ///
42 /// `router` - The REST API definition.
43 ///
44 /// `env_type` - The environment type.
45 ///
b914b947 46 /// `api_auth` - The Authentication handler
0d5d15c9
DM
47 ///
48 /// `get_index_fn` - callback to generate the root page
49 /// (index). Please note that this fuctions gets a reference to
50 /// the [ApiConfig], so it can use [Handlebars] templates
51 /// ([render_template](Self::render_template) to generate pages.
26858dba
SR
52 pub fn new<B: Into<PathBuf>>(
53 basedir: B,
54 router: &'static Router,
55 env_type: RpcEnvironmentType,
56 api_auth: Arc<dyn ApiAuth + Send + Sync>,
7fa9a37c 57 get_index_fn: GetIndexFn,
26858dba 58 ) -> Result<Self, Error> {
f9e3b110 59 Ok(Self {
2ab5acac 60 basedir: basedir.into(),
653b1ca1 61 router,
16b48b81 62 aliases: HashMap::new(),
02c7a755 63 env_type,
2ab5acac
DC
64 templates: RwLock::new(Handlebars::new()),
65 template_files: RwLock::new(HashMap::new()),
8e7e2223 66 request_log: None,
36b7085e 67 auth_log: None,
26858dba 68 api_auth,
7fa9a37c 69 get_index_fn,
26858dba 70 })
16b48b81
DM
71 }
72
6680878b 73 pub(crate) async fn get_index(
7fa9a37c
DM
74 &self,
75 auth_id: Option<String>,
76 language: Option<String>,
77 parts: Parts,
78 ) -> Response<Body> {
6680878b 79 (self.get_index_fn)(auth_id, language, self, parts).await
7fa9a37c
DM
80 }
81
3483a3b3
DM
82 pub(crate) async fn check_auth(
83 &self,
84 headers: &http::HeaderMap,
85 method: &hyper::Method,
86 ) -> Result<(String, Box<dyn UserInformation + Sync + Send>), AuthError> {
87 self.api_auth.check_auth(headers, method).await
88 }
89
0d5d15c9 90 pub(crate) fn find_method(
255f378a
DM
91 &self,
92 components: &[&str],
93 method: Method,
94 uri_param: &mut HashMap<String, String>,
95 ) -> Option<&'static ApiMethod> {
16b48b81 96
01bf3b7b 97 self.router.find_method(components, method, uri_param)
16b48b81
DM
98 }
99
0d5d15c9 100 pub(crate) fn find_alias(&self, components: &[&str]) -> PathBuf {
16b48b81
DM
101
102 let mut prefix = String::new();
103 let mut filename = self.basedir.clone();
104 let comp_len = components.len();
105 if comp_len >= 1 {
106 prefix.push_str(components[0]);
107 if let Some(subdir) = self.aliases.get(&prefix) {
108 filename.push(subdir);
382f10a0 109 components.iter().skip(1).for_each(|comp| filename.push(comp));
8adbdb0a 110 } else {
382f10a0 111 components.iter().for_each(|comp| filename.push(comp));
16b48b81
DM
112 }
113 }
114 filename
115 }
116
0d5d15c9
DM
117 /// Register a path alias
118 ///
119 /// This can be used to redirect file lookups to a specific
120 /// directory, e.g.:
121 ///
122 /// ```
123 /// use proxmox_rest_server::ApiConfig;
124 /// // let mut config = ApiConfig::new(...);
125 /// # fn fake(config: &mut ApiConfig) {
126 /// config.add_alias("extjs", "/usr/share/javascript/extjs");
127 /// # }
128 /// ```
16b48b81
DM
129 pub fn add_alias<S, P>(&mut self, alias: S, path: P)
130 where S: Into<String>,
131 P: Into<PathBuf>,
132 {
133 self.aliases.insert(alias.into(), path.into());
134 }
02c7a755 135
0d5d15c9 136 pub(crate) fn env_type(&self) -> RpcEnvironmentType {
02c7a755
DM
137 self.env_type
138 }
2ab5acac 139
0d5d15c9
DM
140 /// Register a [Handlebars] template file
141 ///
142 /// Those templates cane be use with [render_template](Self::render_template) to generate pages.
2ab5acac
DC
143 pub fn register_template<P>(&self, name: &str, path: P) -> Result<(), Error>
144 where
145 P: Into<PathBuf>
146 {
147 if self.template_files.read().unwrap().contains_key(name) {
148 bail!("template already registered");
149 }
150
151 let path: PathBuf = path.into();
152 let metadata = metadata(&path)?;
153 let mtime = metadata.modified()?;
154
155 self.templates.write().unwrap().register_template_file(name, &path)?;
156 self.template_files.write().unwrap().insert(name.to_string(), (mtime, path));
157
158 Ok(())
159 }
160
161 /// Checks if the template was modified since the last rendering
162 /// if yes, it loads a the new version of the template
163 pub fn render_template<T>(&self, name: &str, data: &T) -> Result<String, Error>
164 where
165 T: Serialize,
166 {
167 let path;
168 let mtime;
169 {
170 let template_files = self.template_files.read().unwrap();
171 let (old_mtime, old_path) = template_files.get(name).ok_or_else(|| format_err!("template not found"))?;
172
173 mtime = metadata(old_path)?.modified()?;
174 if mtime <= *old_mtime {
175 return self.templates.read().unwrap().render(name, data).map_err(|err| format_err!("{}", err));
176 }
177 path = old_path.to_path_buf();
178 }
179
180 {
181 let mut template_files = self.template_files.write().unwrap();
182 let mut templates = self.templates.write().unwrap();
183
184 templates.register_template_file(name, &path)?;
185 template_files.insert(name.to_string(), (mtime, path));
186
187 templates.render(name, data).map_err(|err| format_err!("{}", err))
188 }
189 }
8e7e2223 190
0d5d15c9
DM
191 /// Enable the access log feature
192 ///
193 /// When enabled, all requests are logged to the specified file.
194 /// This function also registers a `api-access-log-reopen`
49e25688 195 /// command one the [CommandSocket].
0d5d15c9 196 pub fn enable_access_log<P>(
fe4cc5b1
TL
197 &mut self,
198 path: P,
fd6d2438
DM
199 dir_opts: Option<CreateOptions>,
200 file_opts: Option<CreateOptions>,
49e25688 201 commando_sock: &mut CommandSocket,
fe4cc5b1 202 ) -> Result<(), Error>
8e7e2223
TL
203 where
204 P: Into<PathBuf>
205 {
206 let path: PathBuf = path.into();
207 if let Some(base) = path.parent() {
208 if !base.exists() {
fd6d2438 209 create_path(base, None, dir_opts).map_err(|err| format_err!("{}", err))?;
8e7e2223
TL
210 }
211 }
212
213 let logger_options = FileLogOptions {
214 append: true,
fd6d2438 215 file_opts: file_opts.unwrap_or(CreateOptions::default()),
8e7e2223
TL
216 ..Default::default()
217 };
fe4cc5b1
TL
218 let request_log = Arc::new(Mutex::new(FileLogger::new(&path, logger_options)?));
219 self.request_log = Some(Arc::clone(&request_log));
220
221 commando_sock.register_command("api-access-log-reopen".into(), move |_args| {
36b7085e 222 println!("re-opening access-log file");
fe4cc5b1
TL
223 request_log.lock().unwrap().reopen()?;
224 Ok(serde_json::Value::Null)
225 })?;
8e7e2223
TL
226
227 Ok(())
228 }
fe4cc5b1 229
b914b947 230 /// Enable the authentication log feature
0d5d15c9 231 ///
b914b947 232 /// When enabled, all authentication requests are logged to the
0d5d15c9 233 /// specified file. This function also registers a
49e25688 234 /// `api-auth-log-reopen` command one the [CommandSocket].
36b7085e
DM
235 pub fn enable_auth_log<P>(
236 &mut self,
237 path: P,
238 dir_opts: Option<CreateOptions>,
239 file_opts: Option<CreateOptions>,
49e25688 240 commando_sock: &mut CommandSocket,
36b7085e
DM
241 ) -> Result<(), Error>
242 where
243 P: Into<PathBuf>
244 {
245 let path: PathBuf = path.into();
246 if let Some(base) = path.parent() {
247 if !base.exists() {
248 create_path(base, None, dir_opts).map_err(|err| format_err!("{}", err))?;
249 }
250 }
251
252 let logger_options = FileLogOptions {
253 append: true,
254 prefix_time: true,
255 file_opts: file_opts.unwrap_or(CreateOptions::default()),
256 ..Default::default()
257 };
258 let auth_log = Arc::new(Mutex::new(FileLogger::new(&path, logger_options)?));
259 self.auth_log = Some(Arc::clone(&auth_log));
260
261 commando_sock.register_command("api-auth-log-reopen".into(), move |_args| {
262 println!("re-opening auth-log file");
263 auth_log.lock().unwrap().reopen()?;
264 Ok(serde_json::Value::Null)
265 })?;
266
267 Ok(())
268 }
269
0d5d15c9 270 pub(crate) fn get_access_log(&self) -> Option<&Arc<Mutex<FileLogger>>> {
8e7e2223
TL
271 self.request_log.as_ref()
272 }
36b7085e 273
0d5d15c9 274 pub(crate) fn get_auth_log(&self) -> Option<&Arc<Mutex<FileLogger>>> {
36b7085e
DM
275 self.auth_log.as_ref()
276 }
16b48b81 277}