]>
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 | ||
0dffe3f9 | 113 | pub fn new(server: &str, username: &str) -> Self { |
5a2df000 DM |
114 | let client = Self::build_client(); |
115 | let login = Self::credentials(client.clone(), server, username); | |
116 | ||
597641fd | 117 | Self { |
5a2df000 | 118 | client, |
597641fd | 119 | server: String::from(server), |
5a2df000 | 120 | auth: BroadcastFuture::new(login), |
597641fd DM |
121 | } |
122 | } | |
123 | ||
5a2df000 | 124 | fn get_password(_username: &str) -> Result<String, Error> { |
56458d97 WB |
125 | use std::env::VarError::*; |
126 | match std::env::var("PBS_PASSWORD") { | |
127 | Ok(p) => return Ok(p), | |
128 | Err(NotUnicode(_)) => bail!("PBS_PASSWORD contains bad characters"), | |
129 | Err(NotPresent) => { | |
130 | // Try another method | |
131 | } | |
132 | } | |
133 | ||
134 | // If we're on a TTY, query the user for a password | |
135 | if tty::stdin_isatty() { | |
136 | return Ok(String::from_utf8(tty::read_password("Password: ")?)?); | |
137 | } | |
138 | ||
139 | bail!("no password input mechanism available"); | |
140 | } | |
141 | ||
5a2df000 | 142 | fn build_client() -> Client<hyper_tls::HttpsConnector<hyper::client::HttpConnector>> { |
4a3f6517 WB |
143 | let mut builder = native_tls::TlsConnector::builder(); |
144 | // FIXME: We need a CLI option for this! | |
145 | builder.danger_accept_invalid_certs(true); | |
5a2df000 | 146 | let tlsconnector = builder.build().unwrap(); |
4a3f6517 WB |
147 | let mut httpc = hyper::client::HttpConnector::new(1); |
148 | httpc.enforce_http(false); // we want https... | |
149 | let mut https = hyper_tls::HttpsConnector::from((httpc, tlsconnector)); | |
150 | https.https_only(true); // force it! | |
5a2df000 | 151 | Client::builder().build::<_, Body>(https) |
a6b75513 DM |
152 | } |
153 | ||
5a2df000 | 154 | pub fn request(&self, mut req: Request<Body>) -> impl Future<Item=Value, Error=Error> { |
597641fd | 155 | |
5a2df000 | 156 | let login = self.auth.listen(); |
597641fd | 157 | |
5a2df000 | 158 | let client = self.client.clone(); |
597641fd | 159 | |
5a2df000 | 160 | login.and_then(move |auth| { |
597641fd | 161 | |
5a2df000 DM |
162 | let enc_ticket = format!("PBSAuthCookie={}", percent_encode(auth.ticket.as_bytes(), DEFAULT_ENCODE_SET)); |
163 | req.headers_mut().insert("Cookie", HeaderValue::from_str(&enc_ticket).unwrap()); | |
164 | req.headers_mut().insert("CSRFPreventionToken", HeaderValue::from_str(&auth.token).unwrap()); | |
597641fd | 165 | |
5a2df000 | 166 | let request = Self::api_request(client, req); |
597641fd | 167 | |
5a2df000 DM |
168 | request |
169 | }) | |
1fdb4c6f DM |
170 | } |
171 | ||
5a2df000 | 172 | pub fn get(&self, path: &str) -> impl Future<Item=Value, Error=Error> { |
a6b75513 | 173 | |
5a2df000 DM |
174 | let req = Self::request_builder(&self.server, "GET", path, None).unwrap(); |
175 | self.request(req) | |
a6b75513 DM |
176 | } |
177 | ||
5a2df000 | 178 | pub fn delete(&mut self, path: &str) -> impl Future<Item=Value, Error=Error> { |
a6b75513 | 179 | |
5a2df000 DM |
180 | let req = Self::request_builder(&self.server, "DELETE", path, None).unwrap(); |
181 | self.request(req) | |
a6b75513 DM |
182 | } |
183 | ||
5a2df000 | 184 | pub fn post(&mut self, path: &str, data: Option<Value>) -> impl Future<Item=Value, Error=Error> { |
024f11bb | 185 | |
5a2df000 DM |
186 | let req = Self::request_builder(&self.server, "POST", path, data).unwrap(); |
187 | self.request(req) | |
024f11bb DM |
188 | } |
189 | ||
5a2df000 | 190 | pub fn download(&mut self, path: &str, mut output: Box<dyn std::io::Write + Send>) -> impl Future<Item=(), Error=Error> { |
024f11bb | 191 | |
5a2df000 | 192 | let mut req = Self::request_builder(&self.server, "GET", path, None).unwrap(); |
024f11bb | 193 | |
5a2df000 | 194 | let login = self.auth.listen(); |
024f11bb | 195 | |
5a2df000 | 196 | let client = self.client.clone(); |
1fdb4c6f | 197 | |
5a2df000 | 198 | login.and_then(move |auth| { |
81da38c1 | 199 | |
5a2df000 DM |
200 | let enc_ticket = format!("PBSAuthCookie={}", percent_encode(auth.ticket.as_bytes(), DEFAULT_ENCODE_SET)); |
201 | req.headers_mut().insert("Cookie", HeaderValue::from_str(&enc_ticket).unwrap()); | |
6f62c924 | 202 | |
5a2df000 DM |
203 | client.request(req) |
204 | .map_err(Error::from) | |
205 | .and_then(|resp| { | |
6f62c924 | 206 | |
5a2df000 | 207 | let _status = resp.status(); // fixme: ?? |
6f62c924 | 208 | |
5a2df000 DM |
209 | resp.into_body() |
210 | .map_err(Error::from) | |
211 | .for_each(move |chunk| { | |
212 | output.write_all(&chunk)?; | |
213 | Ok(()) | |
214 | }) | |
6f62c924 | 215 | |
5a2df000 DM |
216 | }) |
217 | }) | |
6f62c924 DM |
218 | } |
219 | ||
5a2df000 | 220 | pub fn upload(&mut self, content_type: &str, body: Body, path: &str) -> impl Future<Item=Value, Error=Error> { |
81da38c1 DM |
221 | |
222 | let path = path.trim_matches('/'); | |
5a2df000 | 223 | let url: Uri = format!("https://{}:8007/{}", &self.server, path).parse().unwrap(); |
81da38c1 | 224 | |
5a2df000 | 225 | let req = Request::builder() |
81da38c1 DM |
226 | .method("POST") |
227 | .uri(url) | |
228 | .header("User-Agent", "proxmox-backup-client/1.0") | |
5a2df000 DM |
229 | .header("Content-Type", content_type) |
230 | .body(body).unwrap(); | |
81da38c1 | 231 | |
5a2df000 | 232 | self.request(req) |
1fdb4c6f DM |
233 | } |
234 | ||
cf639a47 DM |
235 | pub fn h2upgrade(&mut self, path: &str) -> impl Future<Item=h2::client::SendRequest<bytes::Bytes>, Error=Error> { |
236 | ||
237 | let mut req = Self::request_builder(&self.server, "GET", path, None).unwrap(); | |
238 | ||
239 | let login = self.auth.listen(); | |
240 | ||
241 | let client = self.client.clone(); | |
242 | ||
243 | login.and_then(move |auth| { | |
244 | ||
245 | let enc_ticket = format!("PBSAuthCookie={}", percent_encode(auth.ticket.as_bytes(), DEFAULT_ENCODE_SET)); | |
246 | req.headers_mut().insert("Cookie", HeaderValue::from_str(&enc_ticket).unwrap()); | |
247 | req.headers_mut().insert("UPGRADE", HeaderValue::from_str("proxmox-backup-protocol-h2").unwrap()); | |
248 | ||
249 | client.request(req) | |
250 | .map_err(Error::from) | |
251 | .and_then(|resp| { | |
252 | ||
253 | let status = resp.status(); | |
254 | if status != http::StatusCode::SWITCHING_PROTOCOLS { | |
255 | bail!("h2upgrade failed with status {:?}", status); | |
256 | } | |
257 | ||
258 | Ok(resp.into_body().on_upgrade().map_err(Error::from)) | |
259 | }) | |
260 | .flatten() | |
261 | .and_then(|upgraded| { | |
262 | println!("upgraded"); | |
263 | ||
264 | h2::client::handshake(upgraded).map_err(Error::from) | |
265 | }) | |
266 | .and_then(|(h2, connection)| { | |
267 | let connection = connection | |
268 | .map_err(|_| panic!("HTTP/2.0 connection failed")); | |
269 | ||
270 | // Spawn a new task to drive the connection state | |
271 | hyper::rt::spawn(connection); | |
272 | ||
273 | // Wait until the `SendRequest` handle has available capacity. | |
274 | h2.ready().map_err(Error::from) | |
275 | }) | |
276 | }) | |
277 | } | |
278 | ||
5a2df000 DM |
279 | fn credentials( |
280 | client: Client<hyper_tls::HttpsConnector<hyper::client::HttpConnector>>, | |
281 | server: &str, | |
282 | username: &str, | |
283 | ) -> Box<Future<Item=AuthInfo, Error=Error> + Send> { | |
0ffbccce | 284 | |
5a2df000 DM |
285 | let server = server.to_owned(); |
286 | let server2 = server.to_owned(); | |
287 | let username = username.to_owned(); | |
0ffbccce | 288 | |
5a2df000 | 289 | let create_request = futures::future::lazy(move || { |
0ffbccce | 290 | |
5a2df000 DM |
291 | let data = if let Some((ticket, _token)) = load_ticket_info(&server, &username) { |
292 | json!({ "username": username, "password": ticket }) | |
293 | } else { | |
0ffbccce | 294 | |
5a2df000 DM |
295 | let password = match Self::get_password(&username) { |
296 | Ok(p) => p, | |
297 | Err(err) => { | |
298 | return futures::future::Either::A(futures::future::err(err)); | |
299 | } | |
300 | }; | |
0ffbccce | 301 | |
5a2df000 DM |
302 | json!({ "username": username, "password": password }) |
303 | }; | |
0dffe3f9 | 304 | |
5a2df000 | 305 | let req = Self::request_builder(&server, "POST", "/api2/json/access/ticket", Some(data)).unwrap(); |
0dffe3f9 | 306 | |
5a2df000 DM |
307 | futures::future::Either::B(Self::api_request(client, req)) |
308 | }); | |
0dffe3f9 | 309 | |
5a2df000 DM |
310 | let login_future = create_request |
311 | .and_then(move |cred| { | |
312 | let auth = AuthInfo { | |
313 | username: cred["data"]["username"].as_str().unwrap().to_owned(), | |
314 | ticket: cred["data"]["ticket"].as_str().unwrap().to_owned(), | |
315 | token: cred["data"]["CSRFPreventionToken"].as_str().unwrap().to_owned(), | |
316 | }; | |
0dffe3f9 | 317 | |
5a2df000 | 318 | let _ = store_ticket_info(&server2, &auth.username, &auth.ticket, &auth.token); |
0dffe3f9 | 319 | |
5a2df000 DM |
320 | Ok(auth) |
321 | }); | |
0dffe3f9 | 322 | |
5a2df000 | 323 | Box::new(login_future) |
ba3a60b2 DM |
324 | } |
325 | ||
5a2df000 DM |
326 | fn api_request( |
327 | client: Client<hyper_tls::HttpsConnector<hyper::client::HttpConnector>>, | |
328 | req: Request<Body> | |
329 | ) -> impl Future<Item=Value, Error=Error> { | |
ba3a60b2 | 330 | |
5a2df000 DM |
331 | client.request(req) |
332 | .map_err(Error::from) | |
333 | .and_then(|resp| { | |
ba3a60b2 | 334 | |
5a2df000 | 335 | let status = resp.status(); |
ba3a60b2 | 336 | |
5a2df000 DM |
337 | resp |
338 | .into_body() | |
339 | .concat2() | |
340 | .map_err(Error::from) | |
341 | .and_then(move |data| { | |
a477d688 | 342 | |
5a2df000 DM |
343 | let text = String::from_utf8(data.to_vec()).unwrap(); |
344 | if status.is_success() { | |
345 | if text.len() > 0 { | |
346 | let value: Value = serde_json::from_str(&text)?; | |
347 | Ok(value) | |
348 | } else { | |
349 | Ok(Value::Null) | |
350 | } | |
351 | } else { | |
352 | bail!("HTTP Error {}: {}", status, text); | |
353 | } | |
354 | }) | |
355 | }) | |
0dffe3f9 DM |
356 | } |
357 | ||
5a2df000 | 358 | pub fn request_builder(server: &str, method: &str, path: &str, data: Option<Value>) -> Result<Request<Body>, Error> { |
591f570b | 359 | let path = path.trim_matches('/'); |
5a2df000 DM |
360 | let url: Uri = format!("https://{}:8007/{}", server, path).parse()?; |
361 | ||
362 | if let Some(data) = data { | |
363 | if method == "POST" { | |
364 | let request = Request::builder() | |
365 | .method(method) | |
366 | .uri(url) | |
367 | .header("User-Agent", "proxmox-backup-client/1.0") | |
368 | .header(hyper::header::CONTENT_TYPE, "application/json") | |
369 | .body(Body::from(data.to_string()))?; | |
370 | return Ok(request); | |
371 | } else { | |
372 | unimplemented!(); | |
373 | } | |
0dffe3f9 | 374 | |
5a2df000 | 375 | } |
0dffe3f9 | 376 | |
1fdb4c6f | 377 | let request = Request::builder() |
5a2df000 | 378 | .method(method) |
1fdb4c6f DM |
379 | .uri(url) |
380 | .header("User-Agent", "proxmox-backup-client/1.0") | |
5a2df000 DM |
381 | .header(hyper::header::CONTENT_TYPE, "application/x-www-form-urlencoded") |
382 | .body(Body::empty())?; | |
1fdb4c6f | 383 | |
5a2df000 | 384 | Ok(request) |
597641fd DM |
385 | } |
386 | } |