]> git.proxmox.com Git - proxmox-backup.git/blob - src/client/http_client.rs
src/client/http_client.rs: impl download
[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).unwrap_or(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 = tools::file_get_json(&path).unwrap_or(json!({}));
76
77 let now = Utc::now().timestamp();
78
79 let ticket_lifetime = tools::ticket::TICKET_LIFETIME - 60;
80
81 if let Some(uinfo) = data[server][username].as_object() {
82 if let Some(timestamp) = uinfo["timestamp"].as_i64() {
83 let age = now - timestamp;
84 if age < ticket_lifetime {
85 let ticket = match uinfo["ticket"].as_str() {
86 Some(t) => t,
87 None => return None,
88 };
89 let token = match uinfo["token"].as_str() {
90 Some(t) => t,
91 None => return None,
92 };
93 return Some((ticket.to_owned(), token.to_owned()));
94 }
95 }
96 }
97
98 None
99 }
100
101 impl HttpClient {
102
103 pub fn new(server: &str, username: &str) -> Self {
104 Self {
105 server: String::from(server),
106 username: String::from(username),
107 ticket: None,
108 token: None,
109 }
110 }
111
112 fn get_password(&self) -> Result<String, Error> {
113 use std::env::VarError::*;
114 match std::env::var("PBS_PASSWORD") {
115 Ok(p) => return Ok(p),
116 Err(NotUnicode(_)) => bail!("PBS_PASSWORD contains bad characters"),
117 Err(NotPresent) => {
118 // Try another method
119 }
120 }
121
122 // If we're on a TTY, query the user for a password
123 if tty::stdin_isatty() {
124 return Ok(String::from_utf8(tty::read_password("Password: ")?)?);
125 }
126
127 bail!("no password input mechanism available");
128 }
129
130 fn build_client() -> Result<Client<hyper_tls::HttpsConnector<hyper::client::HttpConnector>>, Error> {
131 let mut builder = native_tls::TlsConnector::builder();
132 // FIXME: We need a CLI option for this!
133 builder.danger_accept_invalid_certs(true);
134 let tlsconnector = builder.build()?;
135 let mut httpc = hyper::client::HttpConnector::new(1);
136 httpc.enforce_http(false); // we want https...
137 let mut https = hyper_tls::HttpsConnector::from((httpc, tlsconnector));
138 https.https_only(true); // force it!
139 let client = Client::builder().build::<_, Body>(https);
140 Ok(client)
141 }
142
143 fn run_request(
144 request: Request<Body>,
145 ) -> Result<Value, Error> {
146 let client = Self::build_client()?;
147
148 let (tx, rx) = std::sync::mpsc::channel();
149
150 let future = client
151 .request(request)
152 .map_err(Error::from)
153 .and_then(|resp| {
154
155 let status = resp.status();
156
157 resp.into_body().concat2().map_err(Error::from)
158 .and_then(move |data| {
159
160 let text = String::from_utf8(data.to_vec()).unwrap();
161 if status.is_success() {
162 if text.len() > 0 {
163 let value: Value = serde_json::from_str(&text)?;
164 Ok(value)
165 } else {
166 Ok(Value::Null)
167 }
168 } else {
169 bail!("HTTP Error {}: {}", status, text);
170 }
171 })
172 })
173 .then(move |res| {
174 tx.send(res).unwrap();
175 Ok(())
176 });
177
178 // drop client, else client keeps connectioon open (keep-alive feature)
179 drop(client);
180
181 rt::run(future);
182
183 rx.recv().unwrap()
184 }
185
186 fn run_download(
187 request: Request<Body>,
188 mut output: Box<dyn std::io::Write + Send>,
189 ) -> Result<(), Error> {
190 let client = Self::build_client()?;
191
192 let (tx, rx) = std::sync::mpsc::channel();
193
194 let future = client
195 .request(request)
196 .map_err(Error::from)
197 .and_then(move |resp| {
198
199 let status = resp.status();
200
201 resp.into_body()
202 .map_err(Error::from)
203 .for_each(move |chunk| {
204 output.write_all(&chunk)?;
205 Ok(())
206 })
207
208 })
209 .then(move |res| {
210 tx.send(res).unwrap();
211 Ok(())
212 });
213
214 // drop client, else client keeps connectioon open (keep-alive feature)
215 drop(client);
216
217 rt::run(future);
218
219 rx.recv().unwrap()
220 }
221
222 pub fn download(&mut self, path: &str, output: Box<dyn std::io::Write + Send>) -> Result<(), Error> {
223
224 let path = path.trim_matches('/');
225 let url: Uri = format!("https://{}:8007/{}", self.server, path).parse()?;
226
227 let (ticket, _token) = self.login()?;
228
229 let enc_ticket = percent_encode(ticket.as_bytes(), DEFAULT_ENCODE_SET).to_string();
230
231 let request = Request::builder()
232 .method("GET")
233 .uri(url)
234 .header("User-Agent", "proxmox-backup-client/1.0")
235 .header("Cookie", format!("PBSAuthCookie={}", enc_ticket))
236 .body(Body::empty())?;
237
238 Self::run_download(request, output)
239 }
240
241 pub fn get(&mut self, path: &str) -> Result<Value, Error> {
242
243 let path = path.trim_matches('/');
244 let url: Uri = format!("https://{}:8007/{}", self.server, path).parse()?;
245
246 let (ticket, _token) = self.login()?;
247
248 let enc_ticket = percent_encode(ticket.as_bytes(), DEFAULT_ENCODE_SET).to_string();
249
250 let request = Request::builder()
251 .method("GET")
252 .uri(url)
253 .header("User-Agent", "proxmox-backup-client/1.0")
254 .header("Cookie", format!("PBSAuthCookie={}", enc_ticket))
255 .body(Body::empty())?;
256
257 Self::run_request(request)
258 }
259
260 pub fn delete(&mut self, path: &str) -> Result<Value, Error> {
261
262 let path = path.trim_matches('/');
263 let url: Uri = format!("https://{}:8007/{}", self.server, path).parse()?;
264
265 let (ticket, token) = self.login()?;
266
267 let enc_ticket = percent_encode(ticket.as_bytes(), DEFAULT_ENCODE_SET).to_string();
268
269 let request = Request::builder()
270 .method("DELETE")
271 .uri(url)
272 .header("User-Agent", "proxmox-backup-client/1.0")
273 .header("Cookie", format!("PBSAuthCookie={}", enc_ticket))
274 .header("CSRFPreventionToken", token)
275 .body(Body::empty())?;
276
277 Self::run_request(request)
278 }
279
280 pub fn post(&mut self, path: &str) -> Result<Value, Error> {
281
282 let path = path.trim_matches('/');
283 let url: Uri = format!("https://{}:8007/{}", self.server, path).parse()?;
284
285 let (ticket, token) = self.login()?;
286
287 let enc_ticket = percent_encode(ticket.as_bytes(), DEFAULT_ENCODE_SET).to_string();
288
289 let request = Request::builder()
290 .method("POST")
291 .uri(url)
292 .header("User-Agent", "proxmox-backup-client/1.0")
293 .header("Cookie", format!("PBSAuthCookie={}", enc_ticket))
294 .header("CSRFPreventionToken", token)
295 .header(hyper::header::CONTENT_TYPE, "application/x-www-form-urlencoded")
296 .body(Body::empty())?;
297
298 Self::run_request(request)
299 }
300
301 pub fn post_json(&mut self, path: &str, data: Value) -> Result<Value, Error> {
302
303 let path = path.trim_matches('/');
304 let url: Uri = format!("https://{}:8007/{}", self.server, path).parse()?;
305
306 let (ticket, token) = self.login()?;
307
308 let enc_ticket = percent_encode(ticket.as_bytes(), DEFAULT_ENCODE_SET).to_string();
309
310 let request = Request::builder()
311 .method("POST")
312 .uri(url)
313 .header("User-Agent", "proxmox-backup-client/1.0")
314 .header("Cookie", format!("PBSAuthCookie={}", enc_ticket))
315 .header("CSRFPreventionToken", token)
316 .header(hyper::header::CONTENT_TYPE, "application/json")
317 .body(Body::from(data.to_string()))?;
318
319 Self::run_request(request)
320 }
321
322 fn try_login(&mut self, password: &str) -> Result<(String, String), Error> {
323
324 let url: Uri = format!("https://{}:8007/{}", self.server, "/api2/json/access/ticket").parse()?;
325
326 let query = url::form_urlencoded::Serializer::new(String::new())
327 .append_pair("username", &self.username)
328 .append_pair("password", &password)
329 .finish();
330
331 let request = Request::builder()
332 .method("POST")
333 .uri(url)
334 .header("User-Agent", "proxmox-backup-client/1.0")
335 .header("Content-Type", "application/x-www-form-urlencoded")
336 .body(Body::from(query))?;
337
338 let auth_res = Self::run_request(request)?;
339
340 let ticket = match auth_res["data"]["ticket"].as_str() {
341 Some(t) => t,
342 None => bail!("got unexpected respose for login request."),
343 };
344 let token = match auth_res["data"]["CSRFPreventionToken"].as_str() {
345 Some(t) => t,
346 None => bail!("got unexpected respose for login request."),
347 };
348
349 Ok((ticket.to_owned(), token.to_owned()))
350 }
351
352 pub fn login(&mut self) -> Result<(String, String), Error> {
353
354 if let Some(ref ticket) = self.ticket {
355 if let Some(ref token) = self.token {
356 return Ok((ticket.clone(), token.clone()));
357 }
358 }
359
360 if let Some((ticket, _token)) = load_ticket_info(&self.server, &self.username) {
361 if let Ok((ticket, token)) = self.try_login(&ticket) {
362 let _ = store_ticket_info(&self.server, &self.username, &ticket, &token);
363 return Ok((ticket.to_owned(), token.to_owned()))
364 }
365 }
366
367 let password = self.get_password()?;
368 let (ticket, token) = self.try_login(&password)?;
369
370 let _ = store_ticket_info(&self.server, &self.username, &ticket, &token);
371
372 Ok((ticket.to_owned(), token.to_owned()))
373 }
374
375 pub fn upload(&mut self, content_type: &str, body: Body, path: &str) -> Result<Value, Error> {
376
377 let path = path.trim_matches('/');
378 let url: Uri = format!("https://{}:8007/{}", self.server, path).parse()?;
379
380 let (ticket, token) = self.login()?;
381
382 let enc_ticket = percent_encode(ticket.as_bytes(), DEFAULT_ENCODE_SET).to_string();
383
384 let request = Request::builder()
385 .method("POST")
386 .uri(url)
387 .header("User-Agent", "proxmox-backup-client/1.0")
388 .header("Cookie", format!("PBSAuthCookie={}", enc_ticket))
389 .header("CSRFPreventionToken", token)
390 .header("Content-Type", content_type)
391 .body(body)?;
392
393 Self::run_request(request)
394 }
395 }