]>
Commit | Line | Data |
---|---|---|
16b48b81 | 1 | use std::collections::HashMap; |
2ab5acac DC |
2 | use std::path::PathBuf; |
3 | use std::time::SystemTime; | |
4 | use std::fs::metadata; | |
fe4cc5b1 | 5 | use std::sync::{Arc, Mutex, RwLock}; |
6680878b DM |
6 | use std::future::Future; |
7 | use std::pin::Pin; | |
16b48b81 | 8 | |
2ab5acac | 9 | use anyhow::{bail, Error, format_err}; |
7fa9a37c DM |
10 | use hyper::{Method, Body, Response}; |
11 | use hyper::http::request::Parts; | |
12 | ||
f9e3b110 | 13 | use handlebars::Handlebars; |
2ab5acac | 14 | use serde::Serialize; |
16b48b81 | 15 | |
3483a3b3 | 16 | use proxmox::api::{ApiMethod, Router, RpcEnvironmentType, UserInformation}; |
8e7e2223 TL |
17 | use proxmox::tools::fs::{create_path, CreateOptions}; |
18 | ||
3483a3b3 | 19 | use crate::{ApiAuth, AuthError, FileLogger, FileLogOptions, CommandSocket}; |
a2479cfa | 20 | |
6680878b | 21 | pub 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 |
24 | pub 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 | ||
37 | impl 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 | } |