]>
Commit | Line | Data |
---|---|---|
597641fd DM |
1 | use failure::*; |
2 | ||
3 | use http::Uri; | |
4 | use hyper::Body; | |
5 | use hyper::client::Client; | |
ba3a60b2 DM |
6 | use xdg::BaseDirectories; |
7 | use chrono::Utc; | |
597641fd | 8 | |
1fdb4c6f | 9 | use http::Request; |
5a2df000 DM |
10 | use http::header::HeaderValue; |
11 | ||
12 | use futures::Future; | |
1fdb4c6f DM |
13 | use futures::stream::Stream; |
14 | ||
ba3a60b2 | 15 | use serde_json::{json, Value}; |
0dffe3f9 | 16 | use url::percent_encoding::{percent_encode, DEFAULT_ENCODE_SET}; |
1fdb4c6f | 17 | |
5a2df000 DM |
18 | use crate::tools::{self, BroadcastFuture, tty}; |
19 | ||
20 | #[derive(Clone)] | |
21 | struct AuthInfo { | |
22 | username: String, | |
23 | ticket: String, | |
24 | token: String, | |
25 | } | |
56458d97 | 26 | |
151c6ce2 | 27 | /// HTTP(S) API client |
597641fd | 28 | pub struct HttpClient { |
5a2df000 | 29 | client: Client<hyper_tls::HttpsConnector<hyper::client::HttpConnector>>, |
597641fd | 30 | server: String, |
5a2df000 | 31 | auth: BroadcastFuture<AuthInfo>, |
597641fd DM |
32 | } |
33 | ||
ba3a60b2 DM |
34 | fn store_ticket_info(server: &str, username: &str, ticket: &str, token: &str) -> Result<(), Error> { |
35 | ||
36 | let base = BaseDirectories::with_prefix("proxmox-backup")?; | |
37 | ||
38 | // usually /run/user/<uid>/... | |
39 | let path = base.place_runtime_file("tickets")?; | |
40 | ||
41 | let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600); | |
42 | ||
49cf9f3d | 43 | let mut data = tools::file_get_json(&path, Some(json!({})))?; |
ba3a60b2 DM |
44 | |
45 | let now = Utc::now().timestamp(); | |
46 | ||
47 | data[server][username] = json!({ "timestamp": now, "ticket": ticket, "token": token}); | |
48 | ||
49 | let mut new_data = json!({}); | |
50 | ||
51 | let ticket_lifetime = tools::ticket::TICKET_LIFETIME - 60; | |
52 | ||
53 | let empty = serde_json::map::Map::new(); | |
54 | for (server, info) in data.as_object().unwrap_or(&empty) { | |
55 | for (_user, uinfo) in info.as_object().unwrap_or(&empty) { | |
56 | if let Some(timestamp) = uinfo["timestamp"].as_i64() { | |
57 | let age = now - timestamp; | |
58 | if age < ticket_lifetime { | |
59 | new_data[server][username] = uinfo.clone(); | |
60 | } | |
61 | } | |
62 | } | |
63 | } | |
64 | ||
65 | tools::file_set_contents(path, new_data.to_string().as_bytes(), Some(mode))?; | |
66 | ||
67 | Ok(()) | |
68 | } | |
69 | ||
70 | fn load_ticket_info(server: &str, username: &str) -> Option<(String, String)> { | |
71 | let base = match BaseDirectories::with_prefix("proxmox-backup") { | |
72 | Ok(b) => b, | |
73 | _ => return None, | |
74 | }; | |
75 | ||
76 | // usually /run/user/<uid>/... | |
77 | let path = match base.place_runtime_file("tickets") { | |
78 | Ok(p) => p, | |
79 | _ => return None, | |
80 | }; | |
81 | ||
49cf9f3d DM |
82 | let data = match tools::file_get_json(&path, None) { |
83 | Ok(v) => v, | |
84 | _ => return None, | |
85 | }; | |
ba3a60b2 DM |
86 | |
87 | let now = Utc::now().timestamp(); | |
88 | ||
89 | let ticket_lifetime = tools::ticket::TICKET_LIFETIME - 60; | |
90 | ||
91 | if let Some(uinfo) = data[server][username].as_object() { | |
92 | if let Some(timestamp) = uinfo["timestamp"].as_i64() { | |
93 | let age = now - timestamp; | |
94 | if age < ticket_lifetime { | |
95 | let ticket = match uinfo["ticket"].as_str() { | |
96 | Some(t) => t, | |
97 | None => return None, | |
98 | }; | |
99 | let token = match uinfo["token"].as_str() { | |
100 | Some(t) => t, | |
101 | None => return None, | |
21ea0158 | 102 | }; |
ba3a60b2 DM |
103 | return Some((ticket.to_owned(), token.to_owned())); |
104 | } | |
105 | } | |
106 | } | |
107 | ||
108 | None | |
109 | } | |
110 | ||
597641fd DM |
111 | impl HttpClient { |
112 | ||
45cdce06 | 113 | pub fn new(server: &str, username: &str) -> Result<Self, Error> { |
5a2df000 | 114 | let client = Self::build_client(); |
5a2df000 | 115 | |
45cdce06 DM |
116 | let password = if let Some((ticket, _token)) = load_ticket_info(server, username) { |
117 | ticket | |
118 | } else { | |
119 | Self::get_password(&username)? | |
120 | }; | |
121 | ||
122 | let login = Self::credentials(client.clone(), server.to_owned(), username.to_owned(), password); | |
123 | ||
124 | Ok(Self { | |
5a2df000 | 125 | client, |
597641fd | 126 | server: String::from(server), |
5a2df000 | 127 | auth: BroadcastFuture::new(login), |
45cdce06 | 128 | }) |
597641fd DM |
129 | } |
130 | ||
5a2df000 | 131 | fn get_password(_username: &str) -> Result<String, Error> { |
56458d97 WB |
132 | use std::env::VarError::*; |
133 | match std::env::var("PBS_PASSWORD") { | |
134 | Ok(p) => return Ok(p), | |
135 | Err(NotUnicode(_)) => bail!("PBS_PASSWORD contains bad characters"), | |
136 | Err(NotPresent) => { | |
137 | // Try another method | |
138 | } | |
139 | } | |
140 | ||
141 | // If we're on a TTY, query the user for a password | |
142 | if tty::stdin_isatty() { | |
143 | return Ok(String::from_utf8(tty::read_password("Password: ")?)?); | |
144 | } | |
145 | ||
146 | bail!("no password input mechanism available"); | |
147 | } | |
148 | ||
5a2df000 | 149 | fn build_client() -> Client<hyper_tls::HttpsConnector<hyper::client::HttpConnector>> { |
4a3f6517 WB |
150 | let mut builder = native_tls::TlsConnector::builder(); |
151 | // FIXME: We need a CLI option for this! | |
152 | builder.danger_accept_invalid_certs(true); | |
5a2df000 | 153 | let tlsconnector = builder.build().unwrap(); |
4a3f6517 WB |
154 | let mut httpc = hyper::client::HttpConnector::new(1); |
155 | httpc.enforce_http(false); // we want https... | |
156 | let mut https = hyper_tls::HttpsConnector::from((httpc, tlsconnector)); | |
157 | https.https_only(true); // force it! | |
5a2df000 | 158 | Client::builder().build::<_, Body>(https) |
a6b75513 DM |
159 | } |
160 | ||
5a2df000 | 161 | pub fn request(&self, mut req: Request<Body>) -> impl Future<Item=Value, Error=Error> { |
597641fd | 162 | |
5a2df000 | 163 | let login = self.auth.listen(); |
597641fd | 164 | |
5a2df000 | 165 | let client = self.client.clone(); |
597641fd | 166 | |
5a2df000 | 167 | login.and_then(move |auth| { |
597641fd | 168 | |
5a2df000 DM |
169 | let enc_ticket = format!("PBSAuthCookie={}", percent_encode(auth.ticket.as_bytes(), DEFAULT_ENCODE_SET)); |
170 | req.headers_mut().insert("Cookie", HeaderValue::from_str(&enc_ticket).unwrap()); | |
171 | req.headers_mut().insert("CSRFPreventionToken", HeaderValue::from_str(&auth.token).unwrap()); | |
597641fd | 172 | |
5a2df000 | 173 | let request = Self::api_request(client, req); |
597641fd | 174 | |
5a2df000 DM |
175 | request |
176 | }) | |
1fdb4c6f DM |
177 | } |
178 | ||
5a2df000 | 179 | pub fn get(&self, path: &str) -> impl Future<Item=Value, Error=Error> { |
a6b75513 | 180 | |
5a2df000 DM |
181 | let req = Self::request_builder(&self.server, "GET", path, None).unwrap(); |
182 | self.request(req) | |
a6b75513 DM |
183 | } |
184 | ||
5a2df000 | 185 | pub fn delete(&mut self, path: &str) -> impl Future<Item=Value, Error=Error> { |
a6b75513 | 186 | |
5a2df000 DM |
187 | let req = Self::request_builder(&self.server, "DELETE", path, None).unwrap(); |
188 | self.request(req) | |
a6b75513 DM |
189 | } |
190 | ||
5a2df000 | 191 | pub fn post(&mut self, path: &str, data: Option<Value>) -> impl Future<Item=Value, Error=Error> { |
024f11bb | 192 | |
5a2df000 DM |
193 | let req = Self::request_builder(&self.server, "POST", path, data).unwrap(); |
194 | self.request(req) | |
024f11bb DM |
195 | } |
196 | ||
5a2df000 | 197 | pub fn download(&mut self, path: &str, mut output: Box<dyn std::io::Write + Send>) -> impl Future<Item=(), Error=Error> { |
024f11bb | 198 | |
5a2df000 | 199 | let mut req = Self::request_builder(&self.server, "GET", path, None).unwrap(); |
024f11bb | 200 | |
5a2df000 | 201 | let login = self.auth.listen(); |
024f11bb | 202 | |
5a2df000 | 203 | let client = self.client.clone(); |
1fdb4c6f | 204 | |
5a2df000 | 205 | login.and_then(move |auth| { |
81da38c1 | 206 | |
5a2df000 DM |
207 | let enc_ticket = format!("PBSAuthCookie={}", percent_encode(auth.ticket.as_bytes(), DEFAULT_ENCODE_SET)); |
208 | req.headers_mut().insert("Cookie", HeaderValue::from_str(&enc_ticket).unwrap()); | |
6f62c924 | 209 | |
5a2df000 DM |
210 | client.request(req) |
211 | .map_err(Error::from) | |
212 | .and_then(|resp| { | |
6f62c924 | 213 | |
5a2df000 | 214 | let _status = resp.status(); // fixme: ?? |
6f62c924 | 215 | |
5a2df000 DM |
216 | resp.into_body() |
217 | .map_err(Error::from) | |
218 | .for_each(move |chunk| { | |
219 | output.write_all(&chunk)?; | |
220 | Ok(()) | |
221 | }) | |
6f62c924 | 222 | |
5a2df000 DM |
223 | }) |
224 | }) | |
6f62c924 DM |
225 | } |
226 | ||
5a2df000 | 227 | pub fn upload(&mut self, content_type: &str, body: Body, path: &str) -> impl Future<Item=Value, Error=Error> { |
81da38c1 DM |
228 | |
229 | let path = path.trim_matches('/'); | |
5a2df000 | 230 | let url: Uri = format!("https://{}:8007/{}", &self.server, path).parse().unwrap(); |
81da38c1 | 231 | |
5a2df000 | 232 | let req = Request::builder() |
81da38c1 DM |
233 | .method("POST") |
234 | .uri(url) | |
235 | .header("User-Agent", "proxmox-backup-client/1.0") | |
5a2df000 DM |
236 | .header("Content-Type", content_type) |
237 | .body(body).unwrap(); | |
81da38c1 | 238 | |
5a2df000 | 239 | self.request(req) |
1fdb4c6f DM |
240 | } |
241 | ||
cf639a47 DM |
242 | pub fn h2upgrade(&mut self, path: &str) -> impl Future<Item=h2::client::SendRequest<bytes::Bytes>, Error=Error> { |
243 | ||
244 | let mut req = Self::request_builder(&self.server, "GET", path, None).unwrap(); | |
245 | ||
246 | let login = self.auth.listen(); | |
247 | ||
248 | let client = self.client.clone(); | |
249 | ||
250 | login.and_then(move |auth| { | |
251 | ||
252 | let enc_ticket = format!("PBSAuthCookie={}", percent_encode(auth.ticket.as_bytes(), DEFAULT_ENCODE_SET)); | |
253 | req.headers_mut().insert("Cookie", HeaderValue::from_str(&enc_ticket).unwrap()); | |
254 | req.headers_mut().insert("UPGRADE", HeaderValue::from_str("proxmox-backup-protocol-h2").unwrap()); | |
255 | ||
256 | client.request(req) | |
257 | .map_err(Error::from) | |
258 | .and_then(|resp| { | |
259 | ||
260 | let status = resp.status(); | |
261 | if status != http::StatusCode::SWITCHING_PROTOCOLS { | |
262 | bail!("h2upgrade failed with status {:?}", status); | |
263 | } | |
264 | ||
265 | Ok(resp.into_body().on_upgrade().map_err(Error::from)) | |
266 | }) | |
267 | .flatten() | |
268 | .and_then(|upgraded| { | |
269 | println!("upgraded"); | |
270 | ||
271 | h2::client::handshake(upgraded).map_err(Error::from) | |
272 | }) | |
273 | .and_then(|(h2, connection)| { | |
274 | let connection = connection | |
275 | .map_err(|_| panic!("HTTP/2.0 connection failed")); | |
276 | ||
277 | // Spawn a new task to drive the connection state | |
278 | hyper::rt::spawn(connection); | |
279 | ||
280 | // Wait until the `SendRequest` handle has available capacity. | |
281 | h2.ready().map_err(Error::from) | |
282 | }) | |
283 | }) | |
284 | } | |
285 | ||
5a2df000 DM |
286 | fn credentials( |
287 | client: Client<hyper_tls::HttpsConnector<hyper::client::HttpConnector>>, | |
45cdce06 DM |
288 | server: String, |
289 | username: String, | |
290 | password: String, | |
5a2df000 | 291 | ) -> Box<Future<Item=AuthInfo, Error=Error> + Send> { |
0ffbccce | 292 | |
45cdce06 | 293 | let server2 = server.clone(); |
0ffbccce | 294 | |
5a2df000 | 295 | let create_request = futures::future::lazy(move || { |
45cdce06 | 296 | let data = json!({ "username": username, "password": password }); |
5a2df000 | 297 | let req = Self::request_builder(&server, "POST", "/api2/json/access/ticket", Some(data)).unwrap(); |
45cdce06 | 298 | Self::api_request(client, req) |
5a2df000 | 299 | }); |
0dffe3f9 | 300 | |
5a2df000 DM |
301 | let login_future = create_request |
302 | .and_then(move |cred| { | |
303 | let auth = AuthInfo { | |
304 | username: cred["data"]["username"].as_str().unwrap().to_owned(), | |
305 | ticket: cred["data"]["ticket"].as_str().unwrap().to_owned(), | |
306 | token: cred["data"]["CSRFPreventionToken"].as_str().unwrap().to_owned(), | |
307 | }; | |
0dffe3f9 | 308 | |
5a2df000 | 309 | let _ = store_ticket_info(&server2, &auth.username, &auth.ticket, &auth.token); |
0dffe3f9 | 310 | |
5a2df000 DM |
311 | Ok(auth) |
312 | }); | |
0dffe3f9 | 313 | |
5a2df000 | 314 | Box::new(login_future) |
ba3a60b2 DM |
315 | } |
316 | ||
5a2df000 DM |
317 | fn api_request( |
318 | client: Client<hyper_tls::HttpsConnector<hyper::client::HttpConnector>>, | |
319 | req: Request<Body> | |
320 | ) -> impl Future<Item=Value, Error=Error> { | |
ba3a60b2 | 321 | |
5a2df000 DM |
322 | client.request(req) |
323 | .map_err(Error::from) | |
324 | .and_then(|resp| { | |
ba3a60b2 | 325 | |
5a2df000 | 326 | let status = resp.status(); |
ba3a60b2 | 327 | |
5a2df000 DM |
328 | resp |
329 | .into_body() | |
330 | .concat2() | |
331 | .map_err(Error::from) | |
332 | .and_then(move |data| { | |
a477d688 | 333 | |
5a2df000 DM |
334 | let text = String::from_utf8(data.to_vec()).unwrap(); |
335 | if status.is_success() { | |
336 | if text.len() > 0 { | |
337 | let value: Value = serde_json::from_str(&text)?; | |
338 | Ok(value) | |
339 | } else { | |
340 | Ok(Value::Null) | |
341 | } | |
342 | } else { | |
343 | bail!("HTTP Error {}: {}", status, text); | |
344 | } | |
345 | }) | |
346 | }) | |
0dffe3f9 DM |
347 | } |
348 | ||
5a2df000 | 349 | pub fn request_builder(server: &str, method: &str, path: &str, data: Option<Value>) -> Result<Request<Body>, Error> { |
591f570b | 350 | let path = path.trim_matches('/'); |
5a2df000 DM |
351 | let url: Uri = format!("https://{}:8007/{}", server, path).parse()?; |
352 | ||
353 | if let Some(data) = data { | |
354 | if method == "POST" { | |
355 | let request = Request::builder() | |
356 | .method(method) | |
357 | .uri(url) | |
358 | .header("User-Agent", "proxmox-backup-client/1.0") | |
359 | .header(hyper::header::CONTENT_TYPE, "application/json") | |
360 | .body(Body::from(data.to_string()))?; | |
361 | return Ok(request); | |
362 | } else { | |
363 | unimplemented!(); | |
364 | } | |
0dffe3f9 | 365 | |
5a2df000 | 366 | } |
0dffe3f9 | 367 | |
1fdb4c6f | 368 | let request = Request::builder() |
5a2df000 | 369 | .method(method) |
1fdb4c6f DM |
370 | .uri(url) |
371 | .header("User-Agent", "proxmox-backup-client/1.0") | |
5a2df000 DM |
372 | .header(hyper::header::CONTENT_TYPE, "application/x-www-form-urlencoded") |
373 | .body(Body::empty())?; | |
1fdb4c6f | 374 | |
5a2df000 | 375 | Ok(request) |
597641fd DM |
376 | } |
377 | } |