]> git.proxmox.com Git - proxmox-backup.git/blob - src/client/http_client.rs
src/client/http_client.rs: directly return H2Client on upgrade
[proxmox-backup.git] / src / client / http_client.rs
1 use failure::*;
2
3 use http::Uri;
4 use hyper::Body;
5 use hyper::client::Client;
6 use xdg::BaseDirectories;
7 use chrono::Utc;
8
9 use http::{Request, Response};
10 use http::header::HeaderValue;
11
12 use futures::Future;
13 use futures::stream::Stream;
14
15 use serde_json::{json, Value};
16 use url::percent_encoding::{percent_encode, DEFAULT_ENCODE_SET};
17
18 use crate::tools::{self, BroadcastFuture, tty};
19
20 #[derive(Clone)]
21 struct AuthInfo {
22 username: String,
23 ticket: String,
24 token: String,
25 }
26
27 /// HTTP(S) API client
28 pub struct HttpClient {
29 client: Client<hyper_tls::HttpsConnector<hyper::client::HttpConnector>>,
30 server: String,
31 auth: BroadcastFuture<AuthInfo>,
32 }
33
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
43 let mut data = tools::file_get_json(&path, Some(json!({})))?;
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
82 let data = match tools::file_get_json(&path, None) {
83 Ok(v) => v,
84 _ => return None,
85 };
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,
102 };
103 return Some((ticket.to_owned(), token.to_owned()));
104 }
105 }
106 }
107
108 None
109 }
110
111 impl HttpClient {
112
113 pub fn new(server: &str, username: &str) -> Result<Self, Error> {
114 let client = Self::build_client();
115
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 {
125 client,
126 server: String::from(server),
127 auth: BroadcastFuture::new(login),
128 })
129 }
130
131 fn get_password(_username: &str) -> Result<String, Error> {
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
149 fn build_client() -> Client<hyper_tls::HttpsConnector<hyper::client::HttpConnector>> {
150 let mut builder = native_tls::TlsConnector::builder();
151 // FIXME: We need a CLI option for this!
152 builder.danger_accept_invalid_certs(true);
153 let tlsconnector = builder.build().unwrap();
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!
158 Client::builder().build::<_, Body>(https)
159 }
160
161 pub fn request(&self, mut req: Request<Body>) -> impl Future<Item=Value, Error=Error> {
162
163 let login = self.auth.listen();
164
165 let client = self.client.clone();
166
167 login.and_then(move |auth| {
168
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());
172
173 let request = Self::api_request(client, req);
174
175 request
176 })
177 }
178
179 pub fn get(&self, path: &str, data: Option<Value>) -> impl Future<Item=Value, Error=Error> {
180
181 let req = Self::request_builder(&self.server, "GET", path, data).unwrap();
182 self.request(req)
183 }
184
185 pub fn delete(&mut self, path: &str, data: Option<Value>) -> impl Future<Item=Value, Error=Error> {
186
187 let req = Self::request_builder(&self.server, "DELETE", path, data).unwrap();
188 self.request(req)
189 }
190
191 pub fn post(&mut self, path: &str, data: Option<Value>) -> impl Future<Item=Value, Error=Error> {
192
193 let req = Self::request_builder(&self.server, "POST", path, data).unwrap();
194 self.request(req)
195 }
196
197 pub fn download(&mut self, path: &str, mut output: Box<dyn std::io::Write + Send>) -> impl Future<Item=(), Error=Error> {
198
199 let mut req = Self::request_builder(&self.server, "GET", path, None).unwrap();
200
201 let login = self.auth.listen();
202
203 let client = self.client.clone();
204
205 login.and_then(move |auth| {
206
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());
209
210 client.request(req)
211 .map_err(Error::from)
212 .and_then(|resp| {
213
214 let _status = resp.status(); // fixme: ??
215
216 resp.into_body()
217 .map_err(Error::from)
218 .for_each(move |chunk| {
219 output.write_all(&chunk)?;
220 Ok(())
221 })
222
223 })
224 })
225 }
226
227 pub fn upload(&mut self, content_type: &str, body: Body, path: &str) -> impl Future<Item=Value, Error=Error> {
228
229 let path = path.trim_matches('/');
230 let url: Uri = format!("https://{}:8007/{}", &self.server, path).parse().unwrap();
231
232 let req = Request::builder()
233 .method("POST")
234 .uri(url)
235 .header("User-Agent", "proxmox-backup-client/1.0")
236 .header("Content-Type", content_type)
237 .body(body).unwrap();
238
239 self.request(req)
240 }
241
242 pub fn h2upgrade(
243 &mut self, path:
244 &str, param: Option<Value>
245 ) -> impl Future<Item=H2Client, Error=Error> {
246
247 let mut req = Self::request_builder(&self.server, "GET", path, param).unwrap();
248
249 let login = self.auth.listen();
250
251 let client = self.client.clone();
252
253 login.and_then(move |auth| {
254
255 let enc_ticket = format!("PBSAuthCookie={}", percent_encode(auth.ticket.as_bytes(), DEFAULT_ENCODE_SET));
256 req.headers_mut().insert("Cookie", HeaderValue::from_str(&enc_ticket).unwrap());
257 req.headers_mut().insert("UPGRADE", HeaderValue::from_str("proxmox-backup-protocol-h2").unwrap());
258
259 client.request(req)
260 .map_err(Error::from)
261 .and_then(|resp| {
262
263 let status = resp.status();
264 if status != http::StatusCode::SWITCHING_PROTOCOLS {
265 bail!("h2upgrade failed with status {:?}", status);
266 }
267
268 Ok(resp.into_body().on_upgrade().map_err(Error::from))
269 })
270 .flatten()
271 .and_then(|upgraded| {
272 h2::client::handshake(upgraded).map_err(Error::from)
273 })
274 .and_then(|(h2, connection)| {
275 let connection = connection
276 .map_err(|_| panic!("HTTP/2.0 connection failed"));
277
278 // Spawn a new task to drive the connection state
279 hyper::rt::spawn(connection);
280
281 // Wait until the `SendRequest` handle has available capacity.
282 h2.ready()
283 .map(H2Client::new)
284 .map_err(Error::from)
285 })
286 })
287 }
288
289 fn credentials(
290 client: Client<hyper_tls::HttpsConnector<hyper::client::HttpConnector>>,
291 server: String,
292 username: String,
293 password: String,
294 ) -> Box<Future<Item=AuthInfo, Error=Error> + Send> {
295
296 let server2 = server.clone();
297
298 let create_request = futures::future::lazy(move || {
299 let data = json!({ "username": username, "password": password });
300 let req = Self::request_builder(&server, "POST", "/api2/json/access/ticket", Some(data)).unwrap();
301 Self::api_request(client, req)
302 });
303
304 let login_future = create_request
305 .and_then(move |cred| {
306 let auth = AuthInfo {
307 username: cred["data"]["username"].as_str().unwrap().to_owned(),
308 ticket: cred["data"]["ticket"].as_str().unwrap().to_owned(),
309 token: cred["data"]["CSRFPreventionToken"].as_str().unwrap().to_owned(),
310 };
311
312 let _ = store_ticket_info(&server2, &auth.username, &auth.ticket, &auth.token);
313
314 Ok(auth)
315 });
316
317 Box::new(login_future)
318 }
319
320 fn api_request(
321 client: Client<hyper_tls::HttpsConnector<hyper::client::HttpConnector>>,
322 req: Request<Body>
323 ) -> impl Future<Item=Value, Error=Error> {
324
325 client.request(req)
326 .map_err(Error::from)
327 .and_then(|resp| {
328
329 let status = resp.status();
330
331 resp
332 .into_body()
333 .concat2()
334 .map_err(Error::from)
335 .and_then(move |data| {
336
337 let text = String::from_utf8(data.to_vec()).unwrap();
338 if status.is_success() {
339 if text.len() > 0 {
340 let value: Value = serde_json::from_str(&text)?;
341 Ok(value)
342 } else {
343 Ok(Value::Null)
344 }
345 } else {
346 bail!("HTTP Error {}: {}", status, text);
347 }
348 })
349 })
350 }
351
352 pub fn request_builder(server: &str, method: &str, path: &str, data: Option<Value>) -> Result<Request<Body>, Error> {
353 let path = path.trim_matches('/');
354 let url: Uri = format!("https://{}:8007/{}", server, path).parse()?;
355
356 if let Some(data) = data {
357 if method == "POST" {
358 let request = Request::builder()
359 .method(method)
360 .uri(url)
361 .header("User-Agent", "proxmox-backup-client/1.0")
362 .header(hyper::header::CONTENT_TYPE, "application/json")
363 .body(Body::from(data.to_string()))?;
364 return Ok(request);
365 } else {
366 let query = tools::json_object_to_query(data)?;
367 let url: Uri = format!("https://{}:8007/{}?{}", server, path, query).parse()?;
368 let request = Request::builder()
369 .method(method)
370 .uri(url)
371 .header("User-Agent", "proxmox-backup-client/1.0")
372 .header(hyper::header::CONTENT_TYPE, "application/x-www-form-urlencoded")
373 .body(Body::empty())?;
374 return Ok(request);
375 }
376 }
377
378 let request = Request::builder()
379 .method(method)
380 .uri(url)
381 .header("User-Agent", "proxmox-backup-client/1.0")
382 .header(hyper::header::CONTENT_TYPE, "application/x-www-form-urlencoded")
383 .body(Body::empty())?;
384
385 Ok(request)
386 }
387 }
388
389 pub struct H2Client {
390 h2: h2::client::SendRequest<bytes::Bytes>,
391 }
392
393 impl H2Client {
394
395 pub fn new(h2: h2::client::SendRequest<bytes::Bytes>) -> Self {
396 Self { h2 }
397 }
398
399 pub fn get(&self, path: &str, param: Option<Value>) -> impl Future<Item=Value, Error=Error> {
400 let req = Self::request_builder("localhost", "GET", path, param).unwrap();
401 self.request(req)
402 }
403
404 pub fn post(&self, path: &str, param: Option<Value>) -> impl Future<Item=Value, Error=Error> {
405 let req = Self::request_builder("localhost", "POST", path, param).unwrap();
406 self.request(req)
407 }
408
409 fn request(
410 &self,
411 request: Request<()>,
412 ) -> impl Future<Item=Value, Error=Error> {
413
414 self.h2.clone()
415 .ready()
416 .map_err(Error::from)
417 .and_then(move |mut send_request| {
418 // fixme: what about stream/upload?
419 let (response, _stream) = send_request.send_request(request, true).unwrap();
420 response
421 .map_err(Error::from)
422 .and_then(Self::h2api_response)
423 })
424 }
425
426 fn h2api_response(response: Response<h2::RecvStream>) -> impl Future<Item=Value, Error=Error> {
427
428 let status = response.status();
429
430 let (_head, mut body) = response.into_parts();
431
432 // The `release_capacity` handle allows the caller to manage
433 // flow control.
434 //
435 // Whenever data is received, the caller is responsible for
436 // releasing capacity back to the server once it has freed
437 // the data from memory.
438 let mut release_capacity = body.release_capacity().clone();
439
440 body
441 .map(move |chunk| {
442 // Let the server send more data.
443 let _ = release_capacity.release_capacity(chunk.len());
444 chunk
445 })
446 .concat2()
447 .map_err(Error::from)
448 .and_then(move |data| {
449 let text = String::from_utf8(data.to_vec()).unwrap();
450 if status.is_success() {
451 if text.len() > 0 {
452 let mut value: Value = serde_json::from_str(&text)?;
453 if let Some(map) = value.as_object_mut() {
454 if let Some(data) = map.remove("data") {
455 return Ok(data);
456 }
457 }
458 bail!("got result without data property");
459 } else {
460 Ok(Value::Null)
461 }
462 } else {
463 bail!("HTTP Error {}: {}", status, text);
464 }
465 })
466 }
467
468 pub fn request_builder(server: &str, method: &str, path: &str, data: Option<Value>) -> Result<Request<()>, Error> {
469 let path = path.trim_matches('/');
470 let url: Uri = format!("https://{}:8007/{}", server, path).parse()?;
471
472 if let Some(data) = data {
473 let query = tools::json_object_to_query(data)?;
474 let url: Uri = format!("https://{}:8007/{}?{}", server, path, query).parse()?;
475 let request = Request::builder()
476 .method(method)
477 .uri(url)
478 .header("User-Agent", "proxmox-backup-client/1.0")
479 .header(hyper::header::CONTENT_TYPE, "application/x-www-form-urlencoded")
480 .body(())?;
481 return Ok(request);
482 }
483
484 let request = Request::builder()
485 .method(method)
486 .uri(url)
487 .header("User-Agent", "proxmox-backup-client/1.0")
488 .header(hyper::header::CONTENT_TYPE, "application/x-www-form-urlencoded")
489 .body(())?;
490
491 Ok(request)
492 }
493 }