]> git.proxmox.com Git - proxmox-backup.git/blame - src/client/http_client.rs
src/api2/admin/datastore/backup.rs: ignore errors from last_backup
[proxmox-backup.git] / src / client / http_client.rs
CommitLineData
597641fd
DM
1use failure::*;
2
3use http::Uri;
4use hyper::Body;
5use hyper::client::Client;
ba3a60b2
DM
6use xdg::BaseDirectories;
7use chrono::Utc;
597641fd 8
1fdb4c6f 9use http::Request;
5a2df000
DM
10use http::header::HeaderValue;
11
12use futures::Future;
1fdb4c6f
DM
13use futures::stream::Stream;
14
ba3a60b2 15use serde_json::{json, Value};
0dffe3f9 16use url::percent_encoding::{percent_encode, DEFAULT_ENCODE_SET};
1fdb4c6f 17
5a2df000
DM
18use crate::tools::{self, BroadcastFuture, tty};
19
20#[derive(Clone)]
21struct AuthInfo {
22 username: String,
23 ticket: String,
24 token: String,
25}
56458d97 26
151c6ce2 27/// HTTP(S) API client
597641fd 28pub 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
34fn 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
70fn 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
111impl 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}