]> git.proxmox.com Git - proxmox-backup.git/blame - src/client/http_client.rs
src/server/state.rs: use new BroadcastData helper
[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
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}