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
).unwrap_or(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
= tools
::file_get_json(&path
).unwrap_or(json
!({}
));
77 let now
= Utc
::now().timestamp();
79 let ticket_lifetime
= tools
::ticket
::TICKET_LIFETIME
- 60;
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() {
89 let token
= match uinfo
["token"].as_str() {
93 return Some((ticket
.to_owned(), token
.to_owned()));
103 pub fn new(server
: &str, username
: &str) -> Self {
105 server
: String
::from(server
),
106 username
: String
::from(username
),
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"),
118 // Try another method
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: ")?
)?
);
127 bail
!("no password input mechanism available");
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
);
144 request
: Request
<Body
>,
145 ) -> Result
<Value
, Error
> {
146 let client
= Self::build_client()?
;
148 let (tx
, rx
) = std
::sync
::mpsc
::channel();
152 .map_err(Error
::from
)
155 let status
= resp
.status();
157 resp
.into_body().concat2().map_err(Error
::from
)
158 .and_then(move |data
| {
160 let text
= String
::from_utf8(data
.to_vec()).unwrap();
161 if status
.is_success() {
163 let value
: Value
= serde_json
::from_str(&text
)?
;
169 bail
!("HTTP Error {}: {}", status
, text
);
174 tx
.send(res
).unwrap();
178 // drop client, else client keeps connectioon open (keep-alive feature)
187 request
: Request
<Body
>,
188 mut output
: Box
<dyn std
::io
::Write
+ Send
>,
189 ) -> Result
<(), Error
> {
190 let client
= Self::build_client()?
;
192 let (tx
, rx
) = std
::sync
::mpsc
::channel();
196 .map_err(Error
::from
)
197 .and_then(move |resp
| {
199 let status
= resp
.status();
202 .map_err(Error
::from
)
203 .for_each(move |chunk
| {
204 output
.write_all(&chunk
)?
;
210 tx
.send(res
).unwrap();
214 // drop client, else client keeps connectioon open (keep-alive feature)
222 pub fn download(&mut self, path
: &str, output
: Box
<dyn std
::io
::Write
+ Send
>) -> Result
<(), Error
> {
224 let path
= path
.trim_matches('
/'
);
225 let url
: Uri
= format
!("https://{}:8007/{}", self.server
, path
).parse()?
;
227 let (ticket
, _token
) = self.login()?
;
229 let enc_ticket
= percent_encode(ticket
.as_bytes(), DEFAULT_ENCODE_SET
).to_string();
231 let request
= Request
::builder()
234 .header("User-Agent", "proxmox-backup-client/1.0")
235 .header("Cookie", format
!("PBSAuthCookie={}", enc_ticket
))
236 .body(Body
::empty())?
;
238 Self::run_download(request
, output
)
241 pub fn get(&mut self, path
: &str) -> Result
<Value
, Error
> {
243 let path
= path
.trim_matches('
/'
);
244 let url
: Uri
= format
!("https://{}:8007/{}", self.server
, path
).parse()?
;
246 let (ticket
, _token
) = self.login()?
;
248 let enc_ticket
= percent_encode(ticket
.as_bytes(), DEFAULT_ENCODE_SET
).to_string();
250 let request
= Request
::builder()
253 .header("User-Agent", "proxmox-backup-client/1.0")
254 .header("Cookie", format
!("PBSAuthCookie={}", enc_ticket
))
255 .body(Body
::empty())?
;
257 Self::run_request(request
)
260 pub fn delete(&mut self, path
: &str) -> Result
<Value
, Error
> {
262 let path
= path
.trim_matches('
/'
);
263 let url
: Uri
= format
!("https://{}:8007/{}", self.server
, path
).parse()?
;
265 let (ticket
, token
) = self.login()?
;
267 let enc_ticket
= percent_encode(ticket
.as_bytes(), DEFAULT_ENCODE_SET
).to_string();
269 let request
= Request
::builder()
272 .header("User-Agent", "proxmox-backup-client/1.0")
273 .header("Cookie", format
!("PBSAuthCookie={}", enc_ticket
))
274 .header("CSRFPreventionToken", token
)
275 .body(Body
::empty())?
;
277 Self::run_request(request
)
280 pub fn post(&mut self, path
: &str) -> Result
<Value
, Error
> {
282 let path
= path
.trim_matches('
/'
);
283 let url
: Uri
= format
!("https://{}:8007/{}", self.server
, path
).parse()?
;
285 let (ticket
, token
) = self.login()?
;
287 let enc_ticket
= percent_encode(ticket
.as_bytes(), DEFAULT_ENCODE_SET
).to_string();
289 let request
= Request
::builder()
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())?
;
298 Self::run_request(request
)
301 pub fn post_json(&mut self, path
: &str, data
: Value
) -> Result
<Value
, Error
> {
303 let path
= path
.trim_matches('
/'
);
304 let url
: Uri
= format
!("https://{}:8007/{}", self.server
, path
).parse()?
;
306 let (ticket
, token
) = self.login()?
;
308 let enc_ticket
= percent_encode(ticket
.as_bytes(), DEFAULT_ENCODE_SET
).to_string();
310 let request
= Request
::builder()
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()))?
;
319 Self::run_request(request
)
322 fn try_login(&mut self, password
: &str) -> Result
<(String
, String
), Error
> {
324 let url
: Uri
= format
!("https://{}:8007/{}", self.server
, "/api2/json/access/ticket").parse()?
;
326 let query
= url
::form_urlencoded
::Serializer
::new(String
::new())
327 .append_pair("username", &self.username
)
328 .append_pair("password", &password
)
331 let request
= Request
::builder()
334 .header("User-Agent", "proxmox-backup-client/1.0")
335 .header("Content-Type", "application/x-www-form-urlencoded")
336 .body(Body
::from(query
))?
;
338 let auth_res
= Self::run_request(request
)?
;
340 let ticket
= match auth_res
["data"]["ticket"].as_str() {
342 None
=> bail
!("got unexpected respose for login request."),
344 let token
= match auth_res
["data"]["CSRFPreventionToken"].as_str() {
346 None
=> bail
!("got unexpected respose for login request."),
349 Ok((ticket
.to_owned(), token
.to_owned()))
352 pub fn login(&mut self) -> Result
<(String
, String
), Error
> {
354 if let Some(ref ticket
) = self.ticket
{
355 if let Some(ref token
) = self.token
{
356 return Ok((ticket
.clone(), token
.clone()));
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()))
367 let password
= self.get_password()?
;
368 let (ticket
, token
) = self.try_login(&password
)?
;
370 let _
= store_ticket_info(&self.server
, &self.username
, &ticket
, &token
);
372 Ok((ticket
.to_owned(), token
.to_owned()))
375 pub fn upload(&mut self, content_type
: &str, body
: Body
, path
: &str) -> Result
<Value
, Error
> {
377 let path
= path
.trim_matches('
/'
);
378 let url
: Uri
= format
!("https://{}:8007/{}", self.server
, path
).parse()?
;
380 let (ticket
, token
) = self.login()?
;
382 let enc_ticket
= percent_encode(ticket
.as_bytes(), DEFAULT_ENCODE_SET
).to_string();
384 let request
= Request
::builder()
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
)
393 Self::run_request(request
)