]> git.proxmox.com Git - proxmox-backup.git/blame - src/client/http_client.rs
src/tools/broadcast_future.rs: add new constructor new_oneshot()
[proxmox-backup.git] / src / client / http_client.rs
CommitLineData
597641fd
DM
1use failure::*;
2
3use http::Uri;
4use hyper::Body;
5use hyper::client::Client;
6use hyper::rt::{self, Future};
ba3a60b2
DM
7use xdg::BaseDirectories;
8use chrono::Utc;
597641fd 9
1fdb4c6f
DM
10use http::Request;
11use futures::stream::Stream;
12
ba3a60b2 13use serde_json::{json, Value};
0dffe3f9 14use url::percent_encoding::{percent_encode, DEFAULT_ENCODE_SET};
1fdb4c6f 15
ba3a60b2 16use crate::tools::{self, tty};
56458d97 17
151c6ce2 18/// HTTP(S) API client
597641fd 19pub struct HttpClient {
0dffe3f9 20 username: String,
597641fd 21 server: String,
a477d688
DM
22
23 ticket: Option<String>,
24 token: Option<String>
597641fd
DM
25}
26
ba3a60b2
DM
27fn 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
49cf9f3d 36 let mut data = tools::file_get_json(&path, Some(json!({})))?;
ba3a60b2
DM
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
63fn 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
49cf9f3d
DM
75 let data = match tools::file_get_json(&path, None) {
76 Ok(v) => v,
77 _ => return None,
78 };
ba3a60b2
DM
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,
21ea0158 95 };
ba3a60b2
DM
96 return Some((ticket.to_owned(), token.to_owned()));
97 }
98 }
99 }
100
101 None
102}
103
597641fd
DM
104impl HttpClient {
105
0dffe3f9 106 pub fn new(server: &str, username: &str) -> Self {
597641fd
DM
107 Self {
108 server: String::from(server),
0dffe3f9 109 username: String::from(username),
a477d688
DM
110 ticket: None,
111 token: None,
597641fd
DM
112 }
113 }
114
56458d97
WB
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
a6b75513 133 fn build_client() -> Result<Client<hyper_tls::HttpsConnector<hyper::client::HttpConnector>>, Error> {
4a3f6517
WB
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);
a6b75513
DM
143 Ok(client)
144 }
145
146 fn run_request(
147 request: Request<Body>,
148 ) -> Result<Value, Error> {
149 let client = Self::build_client()?;
597641fd 150
1adb353d 151 let (tx, rx) = std::sync::mpsc::channel();
597641fd
DM
152
153 let future = client
154 .request(request)
fc7f0352 155 .map_err(Error::from)
597641fd
DM
156 .and_then(|resp| {
157
158 let status = resp.status();
159
fc7f0352 160 resp.into_body().concat2().map_err(Error::from)
597641fd
DM
161 .and_then(move |data| {
162
163 let text = String::from_utf8(data.to_vec()).unwrap();
164 if status.is_success() {
1fdb4c6f
DM
165 if text.len() > 0 {
166 let value: Value = serde_json::from_str(&text)?;
167 Ok(value)
168 } else {
169 Ok(Value::Null)
170 }
597641fd 171 } else {
1fdb4c6f 172 bail!("HTTP Error {}: {}", status, text);
597641fd 173 }
597641fd
DM
174 })
175 })
1adb353d
DM
176 .then(move |res| {
177 tx.send(res).unwrap();
178 Ok(())
597641fd
DM
179 });
180
181 // drop client, else client keeps connectioon open (keep-alive feature)
182 drop(client);
183
184 rt::run(future);
185
1adb353d 186 rx.recv().unwrap()
1fdb4c6f
DM
187 }
188
a6b75513
DM
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
b005ed12 202 let _status = resp.status(); // fixme: ??
a6b75513
DM
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
a477d688 244 pub fn get(&mut self, path: &str) -> Result<Value, Error> {
1fdb4c6f 245
591f570b 246 let path = path.trim_matches('/');
4a3f6517 247 let url: Uri = format!("https://{}:8007/{}", self.server, path).parse()?;
1fdb4c6f 248
a4a5c78c 249 let (ticket, _token) = self.login()?;
0dffe3f9
DM
250
251 let enc_ticket = percent_encode(ticket.as_bytes(), DEFAULT_ENCODE_SET).to_string();
252
1fdb4c6f
DM
253 let request = Request::builder()
254 .method("GET")
255 .uri(url)
256 .header("User-Agent", "proxmox-backup-client/1.0")
024f11bb
DM
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")
0dffe3f9 296 .header("Cookie", format!("PBSAuthCookie={}", enc_ticket))
1fdb4c6f
DM
297 .body(Body::empty())?;
298
299 Self::run_request(request)
81da38c1
DM
300 }
301
6f62c924
DM
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
a477d688 322 pub fn post(&mut self, path: &str) -> Result<Value, Error> {
81da38c1
DM
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)
0ffbccce 337 .header(hyper::header::CONTENT_TYPE, "application/x-www-form-urlencoded")
81da38c1
DM
338 .body(Body::empty())?;
339
340 Self::run_request(request)
1fdb4c6f
DM
341 }
342
0ffbccce
DM
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
ba3a60b2 364 fn try_login(&mut self, password: &str) -> Result<(String, String), Error> {
0dffe3f9
DM
365
366 let url: Uri = format!("https://{}:8007/{}", self.server, "/api2/json/access/ticket").parse()?;
367
0dffe3f9
DM
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 };
a4a5c78c
DM
386 let token = match auth_res["data"]["CSRFPreventionToken"].as_str() {
387 Some(t) => t,
388 None => bail!("got unexpected respose for login request."),
389 };
0dffe3f9 390
ba3a60b2
DM
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);
a477d688 413
a4a5c78c 414 Ok((ticket.to_owned(), token.to_owned()))
0dffe3f9
DM
415 }
416
a477d688 417 pub fn upload(&mut self, content_type: &str, body: Body, path: &str) -> Result<Value, Error> {
1fdb4c6f 418
591f570b 419 let path = path.trim_matches('/');
4a3f6517 420 let url: Uri = format!("https://{}:8007/{}", self.server, path).parse()?;
1fdb4c6f 421
a4a5c78c 422 let (ticket, token) = self.login()?;
0dffe3f9
DM
423
424 let enc_ticket = percent_encode(ticket.as_bytes(), DEFAULT_ENCODE_SET).to_string();
425
1fdb4c6f
DM
426 let request = Request::builder()
427 .method("POST")
428 .uri(url)
429 .header("User-Agent", "proxmox-backup-client/1.0")
0dffe3f9 430 .header("Cookie", format!("PBSAuthCookie={}", enc_ticket))
a4a5c78c 431 .header("CSRFPreventionToken", token)
1fdb4c6f
DM
432 .header("Content-Type", content_type)
433 .body(body)?;
434
435 Self::run_request(request)
597641fd
DM
436 }
437}