5 use hyper
::client
::Client
;
6 use hyper
::rt
::{self, Future}
;
7 use xdg
::BaseDirectories
;
11 use futures
::stream
::Stream
;
13 use serde_json
::{json, Value}
;
14 use url
::percent_encoding
::{percent_encode, DEFAULT_ENCODE_SET}
;
16 use crate::tools
::{self, tty}
;
18 /// HTTP(S) API client
19 pub struct HttpClient
{
23 ticket
: Option
<String
>,
27 fn store_ticket_info(server
: &str, username
: &str, ticket
: &str, token
: &str) -> Result
<(), Error
> {
29 let base
= BaseDirectories
::with_prefix("proxmox-backup")?
;
31 // usually /run/user/<uid>/...
32 let path
= base
.place_runtime_file("tickets")?
;
34 let mode
= nix
::sys
::stat
::Mode
::from_bits_truncate(0o0600);
36 let mut data
= tools
::file_get_json(&path
, Some(json
!({}
)))?
;
38 let now
= Utc
::now().timestamp();
40 data
[server
][username
] = json
!({ "timestamp": now, "ticket": ticket, "token": token}
);
42 let mut new_data
= json
!({}
);
44 let ticket_lifetime
= tools
::ticket
::TICKET_LIFETIME
- 60;
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();
58 tools
::file_set_contents(path
, new_data
.to_string().as_bytes(), Some(mode
))?
;
63 fn load_ticket_info(server
: &str, username
: &str) -> Option
<(String
, String
)> {
64 let base
= match BaseDirectories
::with_prefix("proxmox-backup") {
69 // usually /run/user/<uid>/...
70 let path
= match base
.place_runtime_file("tickets") {
75 let data
= match tools
::file_get_json(&path
, None
) {
80 let now
= Utc
::now().timestamp();
82 let ticket_lifetime
= tools
::ticket
::TICKET_LIFETIME
- 60;
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() {
92 let token
= match uinfo
["token"].as_str() {
96 return Some((ticket
.to_owned(), token
.to_owned()));
106 pub fn new(server
: &str, username
: &str) -> Self {
108 server
: String
::from(server
),
109 username
: String
::from(username
),
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"),
121 // Try another method
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: ")?
)?
);
130 bail
!("no password input mechanism available");
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
);
147 request
: Request
<Body
>,
148 ) -> Result
<Value
, Error
> {
149 let client
= Self::build_client()?
;
151 let (tx
, rx
) = std
::sync
::mpsc
::channel();
155 .map_err(Error
::from
)
158 let status
= resp
.status();
160 resp
.into_body().concat2().map_err(Error
::from
)
161 .and_then(move |data
| {
163 let text
= String
::from_utf8(data
.to_vec()).unwrap();
164 if status
.is_success() {
166 let value
: Value
= serde_json
::from_str(&text
)?
;
172 bail
!("HTTP Error {}: {}", status
, text
);
177 tx
.send(res
).unwrap();
181 // drop client, else client keeps connectioon open (keep-alive feature)
190 request
: Request
<Body
>,
191 mut output
: Box
<dyn std
::io
::Write
+ Send
>,
192 ) -> Result
<(), Error
> {
193 let client
= Self::build_client()?
;
195 let (tx
, rx
) = std
::sync
::mpsc
::channel();
199 .map_err(Error
::from
)
200 .and_then(move |resp
| {
202 let _status
= resp
.status(); // fixme: ??
205 .map_err(Error
::from
)
206 .for_each(move |chunk
| {
207 output
.write_all(&chunk
)?
;
213 tx
.send(res
).unwrap();
217 // drop client, else client keeps connectioon open (keep-alive feature)
225 pub fn download(&mut self, path
: &str, output
: Box
<dyn std
::io
::Write
+ Send
>) -> Result
<(), Error
> {
227 let path
= path
.trim_matches('
/'
);
228 let url
: Uri
= format
!("https://{}:8007/{}", self.server
, path
).parse()?
;
230 let (ticket
, _token
) = self.login()?
;
232 let enc_ticket
= percent_encode(ticket
.as_bytes(), DEFAULT_ENCODE_SET
).to_string();
234 let request
= Request
::builder()
237 .header("User-Agent", "proxmox-backup-client/1.0")
238 .header("Cookie", format
!("PBSAuthCookie={}", enc_ticket
))
239 .body(Body
::empty())?
;
241 Self::run_download(request
, output
)
244 pub fn get(&mut self, path
: &str) -> Result
<Value
, Error
> {
246 let path
= path
.trim_matches('
/'
);
247 let url
: Uri
= format
!("https://{}:8007/{}", self.server
, path
).parse()?
;
249 let (ticket
, _token
) = self.login()?
;
251 let enc_ticket
= percent_encode(ticket
.as_bytes(), DEFAULT_ENCODE_SET
).to_string();
253 let request
= Request
::builder()
256 .header("User-Agent", "proxmox-backup-client/1.0")
257 .header("Cookie", format
!("PBSAuthCookie={}", enc_ticket
))
258 .body(Body
::empty())?
;
260 Self::run_request(request
)
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
> {
267 let path
= path
.trim_matches('
/'
);
268 let url
: Uri
= format
!("https://{}:8007/{}", self.server
, path
).parse()?
;
270 let mut credentials
= None
;
272 if let Some(ref ticket
) = self.ticket
{
273 if let Some(ref token
) = self.token
{
274 credentials
= Some((ticket
.clone(), token
.clone()));
278 if credentials
== None
{
279 if let Some((ticket
, token
)) = load_ticket_info(&self.server
, &self.username
) {
280 credentials
= Some((ticket
.clone(), token
.clone()));
284 if credentials
== None
{
285 bail
!("unable to get credentials");
288 let (ticket
, _token
) = credentials
.unwrap();
290 let enc_ticket
= percent_encode(ticket
.as_bytes(), DEFAULT_ENCODE_SET
).to_string();
292 let request
= Request
::builder()
295 .header("User-Agent", "proxmox-backup-client/1.0")
296 .header("Cookie", format
!("PBSAuthCookie={}", enc_ticket
))
297 .body(Body
::empty())?
;
299 Self::run_request(request
)
302 pub fn delete(&mut self, path
: &str) -> Result
<Value
, Error
> {
304 let path
= path
.trim_matches('
/'
);
305 let url
: Uri
= format
!("https://{}:8007/{}", self.server
, path
).parse()?
;
307 let (ticket
, token
) = self.login()?
;
309 let enc_ticket
= percent_encode(ticket
.as_bytes(), DEFAULT_ENCODE_SET
).to_string();
311 let request
= Request
::builder()
314 .header("User-Agent", "proxmox-backup-client/1.0")
315 .header("Cookie", format
!("PBSAuthCookie={}", enc_ticket
))
316 .header("CSRFPreventionToken", token
)
317 .body(Body
::empty())?
;
319 Self::run_request(request
)
322 pub fn post(&mut self, path
: &str) -> Result
<Value
, Error
> {
324 let path
= path
.trim_matches('
/'
);
325 let url
: Uri
= format
!("https://{}:8007/{}", self.server
, path
).parse()?
;
327 let (ticket
, token
) = self.login()?
;
329 let enc_ticket
= percent_encode(ticket
.as_bytes(), DEFAULT_ENCODE_SET
).to_string();
331 let request
= Request
::builder()
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())?
;
340 Self::run_request(request
)
343 pub fn post_json(&mut self, path
: &str, data
: Value
) -> Result
<Value
, Error
> {
345 let path
= path
.trim_matches('
/'
);
346 let url
: Uri
= format
!("https://{}:8007/{}", self.server
, path
).parse()?
;
348 let (ticket
, token
) = self.login()?
;
350 let enc_ticket
= percent_encode(ticket
.as_bytes(), DEFAULT_ENCODE_SET
).to_string();
352 let request
= Request
::builder()
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()))?
;
361 Self::run_request(request
)
364 fn try_login(&mut self, password
: &str) -> Result
<(String
, String
), Error
> {
366 let url
: Uri
= format
!("https://{}:8007/{}", self.server
, "/api2/json/access/ticket").parse()?
;
368 let query
= url
::form_urlencoded
::Serializer
::new(String
::new())
369 .append_pair("username", &self.username
)
370 .append_pair("password", &password
)
373 let request
= Request
::builder()
376 .header("User-Agent", "proxmox-backup-client/1.0")
377 .header("Content-Type", "application/x-www-form-urlencoded")
378 .body(Body
::from(query
))?
;
380 let auth_res
= Self::run_request(request
)?
;
382 let ticket
= match auth_res
["data"]["ticket"].as_str() {
384 None
=> bail
!("got unexpected respose for login request."),
386 let token
= match auth_res
["data"]["CSRFPreventionToken"].as_str() {
388 None
=> bail
!("got unexpected respose for login request."),
391 Ok((ticket
.to_owned(), token
.to_owned()))
394 pub fn login(&mut self) -> Result
<(String
, String
), Error
> {
396 if let Some(ref ticket
) = self.ticket
{
397 if let Some(ref token
) = self.token
{
398 return Ok((ticket
.clone(), token
.clone()));
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()))
409 let password
= self.get_password()?
;
410 let (ticket
, token
) = self.try_login(&password
)?
;
412 let _
= store_ticket_info(&self.server
, &self.username
, &ticket
, &token
);
414 Ok((ticket
.to_owned(), token
.to_owned()))
417 pub fn upload(&mut self, content_type
: &str, body
: Body
, path
: &str) -> Result
<Value
, Error
> {
419 let path
= path
.trim_matches('
/'
);
420 let url
: Uri
= format
!("https://{}:8007/{}", self.server
, path
).parse()?
;
422 let (ticket
, token
) = self.login()?
;
424 let enc_ticket
= percent_encode(ticket
.as_bytes(), DEFAULT_ENCODE_SET
).to_string();
426 let request
= Request
::builder()
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
)
435 Self::run_request(request
)