]> git.proxmox.com Git - proxmox-backup.git/blob - src/client/http_client.rs
src/tools/broadcast_future.rs: add new constructor new_oneshot()
[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 hyper::rt::{self, Future};
7 use xdg::BaseDirectories;
8 use chrono::Utc;
9
10 use http::Request;
11 use futures::stream::Stream;
12
13 use serde_json::{json, Value};
14 use url::percent_encoding::{percent_encode, DEFAULT_ENCODE_SET};
15
16 use crate::tools::{self, tty};
17
18 /// HTTP(S) API client
19 pub struct HttpClient {
20 username: String,
21 server: String,
22
23 ticket: Option<String>,
24 token: Option<String>
25 }
26
27 fn store_ticket_info(server: &str, username: &str, ticket: &str, token: &str) -> Result<(), Error> {
28
29 let base = BaseDirectories::with_prefix("proxmox-backup")?;
30
31 // usually /run/user/<uid>/...
32 let path = base.place_runtime_file("tickets")?;
33
34 let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600);
35
36 let mut data = tools::file_get_json(&path, Some(json!({})))?;
37
38 let now = Utc::now().timestamp();
39
40 data[server][username] = json!({ "timestamp": now, "ticket": ticket, "token": token});
41
42 let mut new_data = json!({});
43
44 let ticket_lifetime = tools::ticket::TICKET_LIFETIME - 60;
45
46 let empty = serde_json::map::Map::new();
47 for (server, info) in data.as_object().unwrap_or(&empty) {
48 for (_user, uinfo) in info.as_object().unwrap_or(&empty) {
49 if let Some(timestamp) = uinfo["timestamp"].as_i64() {
50 let age = now - timestamp;
51 if age < ticket_lifetime {
52 new_data[server][username] = uinfo.clone();
53 }
54 }
55 }
56 }
57
58 tools::file_set_contents(path, new_data.to_string().as_bytes(), Some(mode))?;
59
60 Ok(())
61 }
62
63 fn load_ticket_info(server: &str, username: &str) -> Option<(String, String)> {
64 let base = match BaseDirectories::with_prefix("proxmox-backup") {
65 Ok(b) => b,
66 _ => return None,
67 };
68
69 // usually /run/user/<uid>/...
70 let path = match base.place_runtime_file("tickets") {
71 Ok(p) => p,
72 _ => return None,
73 };
74
75 let data = match tools::file_get_json(&path, None) {
76 Ok(v) => v,
77 _ => return None,
78 };
79
80 let now = Utc::now().timestamp();
81
82 let ticket_lifetime = tools::ticket::TICKET_LIFETIME - 60;
83
84 if let Some(uinfo) = data[server][username].as_object() {
85 if let Some(timestamp) = uinfo["timestamp"].as_i64() {
86 let age = now - timestamp;
87 if age < ticket_lifetime {
88 let ticket = match uinfo["ticket"].as_str() {
89 Some(t) => t,
90 None => return None,
91 };
92 let token = match uinfo["token"].as_str() {
93 Some(t) => t,
94 None => return None,
95 };
96 return Some((ticket.to_owned(), token.to_owned()));
97 }
98 }
99 }
100
101 None
102 }
103
104 impl HttpClient {
105
106 pub fn new(server: &str, username: &str) -> Self {
107 Self {
108 server: String::from(server),
109 username: String::from(username),
110 ticket: None,
111 token: None,
112 }
113 }
114
115 fn get_password(&self) -> Result<String, Error> {
116 use std::env::VarError::*;
117 match std::env::var("PBS_PASSWORD") {
118 Ok(p) => return Ok(p),
119 Err(NotUnicode(_)) => bail!("PBS_PASSWORD contains bad characters"),
120 Err(NotPresent) => {
121 // Try another method
122 }
123 }
124
125 // If we're on a TTY, query the user for a password
126 if tty::stdin_isatty() {
127 return Ok(String::from_utf8(tty::read_password("Password: ")?)?);
128 }
129
130 bail!("no password input mechanism available");
131 }
132
133 fn build_client() -> Result<Client<hyper_tls::HttpsConnector<hyper::client::HttpConnector>>, Error> {
134 let mut builder = native_tls::TlsConnector::builder();
135 // FIXME: We need a CLI option for this!
136 builder.danger_accept_invalid_certs(true);
137 let tlsconnector = builder.build()?;
138 let mut httpc = hyper::client::HttpConnector::new(1);
139 httpc.enforce_http(false); // we want https...
140 let mut https = hyper_tls::HttpsConnector::from((httpc, tlsconnector));
141 https.https_only(true); // force it!
142 let client = Client::builder().build::<_, Body>(https);
143 Ok(client)
144 }
145
146 fn run_request(
147 request: Request<Body>,
148 ) -> Result<Value, Error> {
149 let client = Self::build_client()?;
150
151 let (tx, rx) = std::sync::mpsc::channel();
152
153 let future = client
154 .request(request)
155 .map_err(Error::from)
156 .and_then(|resp| {
157
158 let status = resp.status();
159
160 resp.into_body().concat2().map_err(Error::from)
161 .and_then(move |data| {
162
163 let text = String::from_utf8(data.to_vec()).unwrap();
164 if status.is_success() {
165 if text.len() > 0 {
166 let value: Value = serde_json::from_str(&text)?;
167 Ok(value)
168 } else {
169 Ok(Value::Null)
170 }
171 } else {
172 bail!("HTTP Error {}: {}", status, text);
173 }
174 })
175 })
176 .then(move |res| {
177 tx.send(res).unwrap();
178 Ok(())
179 });
180
181 // drop client, else client keeps connectioon open (keep-alive feature)
182 drop(client);
183
184 rt::run(future);
185
186 rx.recv().unwrap()
187 }
188
189 fn run_download(
190 request: Request<Body>,
191 mut output: Box<dyn std::io::Write + Send>,
192 ) -> Result<(), Error> {
193 let client = Self::build_client()?;
194
195 let (tx, rx) = std::sync::mpsc::channel();
196
197 let future = client
198 .request(request)
199 .map_err(Error::from)
200 .and_then(move |resp| {
201
202 let _status = resp.status(); // fixme: ??
203
204 resp.into_body()
205 .map_err(Error::from)
206 .for_each(move |chunk| {
207 output.write_all(&chunk)?;
208 Ok(())
209 })
210
211 })
212 .then(move |res| {
213 tx.send(res).unwrap();
214 Ok(())
215 });
216
217 // drop client, else client keeps connectioon open (keep-alive feature)
218 drop(client);
219
220 rt::run(future);
221
222 rx.recv().unwrap()
223 }
224
225 pub fn download(&mut self, path: &str, output: Box<dyn std::io::Write + Send>) -> Result<(), Error> {
226
227 let path = path.trim_matches('/');
228 let url: Uri = format!("https://{}:8007/{}", self.server, path).parse()?;
229
230 let (ticket, _token) = self.login()?;
231
232 let enc_ticket = percent_encode(ticket.as_bytes(), DEFAULT_ENCODE_SET).to_string();
233
234 let request = Request::builder()
235 .method("GET")
236 .uri(url)
237 .header("User-Agent", "proxmox-backup-client/1.0")
238 .header("Cookie", format!("PBSAuthCookie={}", enc_ticket))
239 .body(Body::empty())?;
240
241 Self::run_download(request, output)
242 }
243
244 pub fn get(&mut self, path: &str) -> Result<Value, Error> {
245
246 let path = path.trim_matches('/');
247 let url: Uri = format!("https://{}:8007/{}", self.server, path).parse()?;
248
249 let (ticket, _token) = self.login()?;
250
251 let enc_ticket = percent_encode(ticket.as_bytes(), DEFAULT_ENCODE_SET).to_string();
252
253 let request = Request::builder()
254 .method("GET")
255 .uri(url)
256 .header("User-Agent", "proxmox-backup-client/1.0")
257 .header("Cookie", format!("PBSAuthCookie={}", enc_ticket))
258 .body(Body::empty())?;
259
260 Self::run_request(request)
261 }
262
263 /// like get(), but use existing credentials (never asks for password).
264 /// this simply fails when there is no ticket
265 pub fn try_get(&mut self, path: &str) -> Result<Value, Error> {
266
267 let path = path.trim_matches('/');
268 let url: Uri = format!("https://{}:8007/{}", self.server, path).parse()?;
269
270 let mut credentials = None;
271
272 if let Some(ref ticket) = self.ticket {
273 if let Some(ref token) = self.token {
274 credentials = Some((ticket.clone(), token.clone()));
275 }
276 }
277
278 if credentials == None {
279 if let Some((ticket, token)) = load_ticket_info(&self.server, &self.username) {
280 credentials = Some((ticket.clone(), token.clone()));
281 }
282 }
283
284 if credentials == None {
285 bail!("unable to get credentials");
286 }
287
288 let (ticket, _token) = credentials.unwrap();
289
290 let enc_ticket = percent_encode(ticket.as_bytes(), DEFAULT_ENCODE_SET).to_string();
291
292 let request = Request::builder()
293 .method("GET")
294 .uri(url)
295 .header("User-Agent", "proxmox-backup-client/1.0")
296 .header("Cookie", format!("PBSAuthCookie={}", enc_ticket))
297 .body(Body::empty())?;
298
299 Self::run_request(request)
300 }
301
302 pub fn delete(&mut self, path: &str) -> Result<Value, Error> {
303
304 let path = path.trim_matches('/');
305 let url: Uri = format!("https://{}:8007/{}", self.server, path).parse()?;
306
307 let (ticket, token) = self.login()?;
308
309 let enc_ticket = percent_encode(ticket.as_bytes(), DEFAULT_ENCODE_SET).to_string();
310
311 let request = Request::builder()
312 .method("DELETE")
313 .uri(url)
314 .header("User-Agent", "proxmox-backup-client/1.0")
315 .header("Cookie", format!("PBSAuthCookie={}", enc_ticket))
316 .header("CSRFPreventionToken", token)
317 .body(Body::empty())?;
318
319 Self::run_request(request)
320 }
321
322 pub fn post(&mut self, path: &str) -> Result<Value, Error> {
323
324 let path = path.trim_matches('/');
325 let url: Uri = format!("https://{}:8007/{}", self.server, path).parse()?;
326
327 let (ticket, token) = self.login()?;
328
329 let enc_ticket = percent_encode(ticket.as_bytes(), DEFAULT_ENCODE_SET).to_string();
330
331 let request = Request::builder()
332 .method("POST")
333 .uri(url)
334 .header("User-Agent", "proxmox-backup-client/1.0")
335 .header("Cookie", format!("PBSAuthCookie={}", enc_ticket))
336 .header("CSRFPreventionToken", token)
337 .header(hyper::header::CONTENT_TYPE, "application/x-www-form-urlencoded")
338 .body(Body::empty())?;
339
340 Self::run_request(request)
341 }
342
343 pub fn post_json(&mut self, path: &str, data: Value) -> Result<Value, Error> {
344
345 let path = path.trim_matches('/');
346 let url: Uri = format!("https://{}:8007/{}", self.server, path).parse()?;
347
348 let (ticket, token) = self.login()?;
349
350 let enc_ticket = percent_encode(ticket.as_bytes(), DEFAULT_ENCODE_SET).to_string();
351
352 let request = Request::builder()
353 .method("POST")
354 .uri(url)
355 .header("User-Agent", "proxmox-backup-client/1.0")
356 .header("Cookie", format!("PBSAuthCookie={}", enc_ticket))
357 .header("CSRFPreventionToken", token)
358 .header(hyper::header::CONTENT_TYPE, "application/json")
359 .body(Body::from(data.to_string()))?;
360
361 Self::run_request(request)
362 }
363
364 fn try_login(&mut self, password: &str) -> Result<(String, String), Error> {
365
366 let url: Uri = format!("https://{}:8007/{}", self.server, "/api2/json/access/ticket").parse()?;
367
368 let query = url::form_urlencoded::Serializer::new(String::new())
369 .append_pair("username", &self.username)
370 .append_pair("password", &password)
371 .finish();
372
373 let request = Request::builder()
374 .method("POST")
375 .uri(url)
376 .header("User-Agent", "proxmox-backup-client/1.0")
377 .header("Content-Type", "application/x-www-form-urlencoded")
378 .body(Body::from(query))?;
379
380 let auth_res = Self::run_request(request)?;
381
382 let ticket = match auth_res["data"]["ticket"].as_str() {
383 Some(t) => t,
384 None => bail!("got unexpected respose for login request."),
385 };
386 let token = match auth_res["data"]["CSRFPreventionToken"].as_str() {
387 Some(t) => t,
388 None => bail!("got unexpected respose for login request."),
389 };
390
391 Ok((ticket.to_owned(), token.to_owned()))
392 }
393
394 pub fn login(&mut self) -> Result<(String, String), Error> {
395
396 if let Some(ref ticket) = self.ticket {
397 if let Some(ref token) = self.token {
398 return Ok((ticket.clone(), token.clone()));
399 }
400 }
401
402 if let Some((ticket, _token)) = load_ticket_info(&self.server, &self.username) {
403 if let Ok((ticket, token)) = self.try_login(&ticket) {
404 let _ = store_ticket_info(&self.server, &self.username, &ticket, &token);
405 return Ok((ticket.to_owned(), token.to_owned()))
406 }
407 }
408
409 let password = self.get_password()?;
410 let (ticket, token) = self.try_login(&password)?;
411
412 let _ = store_ticket_info(&self.server, &self.username, &ticket, &token);
413
414 Ok((ticket.to_owned(), token.to_owned()))
415 }
416
417 pub fn upload(&mut self, content_type: &str, body: Body, path: &str) -> Result<Value, Error> {
418
419 let path = path.trim_matches('/');
420 let url: Uri = format!("https://{}:8007/{}", self.server, path).parse()?;
421
422 let (ticket, token) = self.login()?;
423
424 let enc_ticket = percent_encode(ticket.as_bytes(), DEFAULT_ENCODE_SET).to_string();
425
426 let request = Request::builder()
427 .method("POST")
428 .uri(url)
429 .header("User-Agent", "proxmox-backup-client/1.0")
430 .header("Cookie", format!("PBSAuthCookie={}", enc_ticket))
431 .header("CSRFPreventionToken", token)
432 .header("Content-Type", content_type)
433 .body(body)?;
434
435 Self::run_request(request)
436 }
437 }