]> git.proxmox.com Git - proxmox-backup.git/blame - src/client/http_client.rs
src/bin/proxmox-backup-client.rs: login before starting 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;
6use hyper::rt::{self, Future};
7
1fdb4c6f
DM
8use http::Request;
9use futures::stream::Stream;
10
11use serde_json::{Value};
0dffe3f9 12use url::percent_encoding::{percent_encode, DEFAULT_ENCODE_SET};
1fdb4c6f 13
56458d97
WB
14use crate::tools::tty;
15
151c6ce2 16/// HTTP(S) API client
597641fd 17pub struct HttpClient {
0dffe3f9 18 username: String,
597641fd 19 server: String,
a477d688
DM
20
21 ticket: Option<String>,
22 token: Option<String>
597641fd
DM
23}
24
25impl HttpClient {
26
0dffe3f9 27 pub fn new(server: &str, username: &str) -> Self {
597641fd
DM
28 Self {
29 server: String::from(server),
0dffe3f9 30 username: String::from(username),
a477d688
DM
31 ticket: None,
32 token: None,
597641fd
DM
33 }
34 }
35
56458d97
WB
36 fn get_password(&self) -> Result<String, Error> {
37 use std::env::VarError::*;
38 match std::env::var("PBS_PASSWORD") {
39 Ok(p) => return Ok(p),
40 Err(NotUnicode(_)) => bail!("PBS_PASSWORD contains bad characters"),
41 Err(NotPresent) => {
42 // Try another method
43 }
44 }
45
46 // If we're on a TTY, query the user for a password
47 if tty::stdin_isatty() {
48 return Ok(String::from_utf8(tty::read_password("Password: ")?)?);
49 }
50
51 bail!("no password input mechanism available");
52 }
53
4a3f6517
WB
54 fn run_request(
55 request: Request<Body>,
56 ) -> Result<Value, Error> {
57 let mut builder = native_tls::TlsConnector::builder();
58 // FIXME: We need a CLI option for this!
59 builder.danger_accept_invalid_certs(true);
60 let tlsconnector = builder.build()?;
61 let mut httpc = hyper::client::HttpConnector::new(1);
62 httpc.enforce_http(false); // we want https...
63 let mut https = hyper_tls::HttpsConnector::from((httpc, tlsconnector));
64 https.https_only(true); // force it!
65 let client = Client::builder().build::<_, Body>(https);
597641fd 66
1adb353d 67 let (tx, rx) = std::sync::mpsc::channel();
597641fd
DM
68
69 let future = client
70 .request(request)
fc7f0352 71 .map_err(Error::from)
597641fd
DM
72 .and_then(|resp| {
73
74 let status = resp.status();
75
fc7f0352 76 resp.into_body().concat2().map_err(Error::from)
597641fd
DM
77 .and_then(move |data| {
78
79 let text = String::from_utf8(data.to_vec()).unwrap();
80 if status.is_success() {
1fdb4c6f
DM
81 if text.len() > 0 {
82 let value: Value = serde_json::from_str(&text)?;
83 Ok(value)
84 } else {
85 Ok(Value::Null)
86 }
597641fd 87 } else {
1fdb4c6f 88 bail!("HTTP Error {}: {}", status, text);
597641fd 89 }
597641fd
DM
90 })
91 })
1adb353d
DM
92 .then(move |res| {
93 tx.send(res).unwrap();
94 Ok(())
597641fd
DM
95 });
96
97 // drop client, else client keeps connectioon open (keep-alive feature)
98 drop(client);
99
100 rt::run(future);
101
1adb353d 102 rx.recv().unwrap()
1fdb4c6f
DM
103 }
104
a477d688 105 pub fn get(&mut self, path: &str) -> Result<Value, Error> {
1fdb4c6f 106
591f570b 107 let path = path.trim_matches('/');
4a3f6517 108 let url: Uri = format!("https://{}:8007/{}", self.server, path).parse()?;
1fdb4c6f 109
a4a5c78c 110 let (ticket, _token) = self.login()?;
0dffe3f9
DM
111
112 let enc_ticket = percent_encode(ticket.as_bytes(), DEFAULT_ENCODE_SET).to_string();
113
1fdb4c6f
DM
114 let request = Request::builder()
115 .method("GET")
116 .uri(url)
117 .header("User-Agent", "proxmox-backup-client/1.0")
0dffe3f9 118 .header("Cookie", format!("PBSAuthCookie={}", enc_ticket))
1fdb4c6f
DM
119 .body(Body::empty())?;
120
121 Self::run_request(request)
81da38c1
DM
122 }
123
a477d688 124 pub fn post(&mut self, path: &str) -> Result<Value, Error> {
81da38c1
DM
125
126 let path = path.trim_matches('/');
127 let url: Uri = format!("https://{}:8007/{}", self.server, path).parse()?;
128
129 let (ticket, token) = self.login()?;
130
131 let enc_ticket = percent_encode(ticket.as_bytes(), DEFAULT_ENCODE_SET).to_string();
132
133 let request = Request::builder()
134 .method("POST")
135 .uri(url)
136 .header("User-Agent", "proxmox-backup-client/1.0")
137 .header("Cookie", format!("PBSAuthCookie={}", enc_ticket))
138 .header("CSRFPreventionToken", token)
0ffbccce 139 .header(hyper::header::CONTENT_TYPE, "application/x-www-form-urlencoded")
81da38c1
DM
140 .body(Body::empty())?;
141
142 Self::run_request(request)
1fdb4c6f
DM
143 }
144
0ffbccce
DM
145 pub fn post_json(&mut self, path: &str, data: Value) -> Result<Value, Error> {
146
147 let path = path.trim_matches('/');
148 let url: Uri = format!("https://{}:8007/{}", self.server, path).parse()?;
149
150 let (ticket, token) = self.login()?;
151
152 let enc_ticket = percent_encode(ticket.as_bytes(), DEFAULT_ENCODE_SET).to_string();
153
154 let request = Request::builder()
155 .method("POST")
156 .uri(url)
157 .header("User-Agent", "proxmox-backup-client/1.0")
158 .header("Cookie", format!("PBSAuthCookie={}", enc_ticket))
159 .header("CSRFPreventionToken", token)
160 .header(hyper::header::CONTENT_TYPE, "application/json")
161 .body(Body::from(data.to_string()))?;
162
163 Self::run_request(request)
164 }
165
51144821 166 pub fn login(&mut self) -> Result<(String, String), Error> {
a477d688
DM
167
168 if let Some(ref ticket) = self.ticket {
169 if let Some(ref token) = self.token {
170 return Ok((ticket.clone(), token.clone()));
171 }
172 }
0dffe3f9
DM
173
174 let url: Uri = format!("https://{}:8007/{}", self.server, "/api2/json/access/ticket").parse()?;
175
56458d97 176 let password = self.get_password()?;
0dffe3f9
DM
177
178 let query = url::form_urlencoded::Serializer::new(String::new())
179 .append_pair("username", &self.username)
180 .append_pair("password", &password)
181 .finish();
182
183 let request = Request::builder()
184 .method("POST")
185 .uri(url)
186 .header("User-Agent", "proxmox-backup-client/1.0")
187 .header("Content-Type", "application/x-www-form-urlencoded")
188 .body(Body::from(query))?;
189
190 let auth_res = Self::run_request(request)?;
191
192 let ticket = match auth_res["data"]["ticket"].as_str() {
193 Some(t) => t,
194 None => bail!("got unexpected respose for login request."),
195 };
a4a5c78c
DM
196 let token = match auth_res["data"]["CSRFPreventionToken"].as_str() {
197 Some(t) => t,
198 None => bail!("got unexpected respose for login request."),
199 };
0dffe3f9 200
a477d688
DM
201 self.ticket = Some(ticket.to_owned());
202 self.token = Some(token.to_owned());
203
a4a5c78c 204 Ok((ticket.to_owned(), token.to_owned()))
0dffe3f9
DM
205 }
206
a477d688 207 pub fn upload(&mut self, content_type: &str, body: Body, path: &str) -> Result<Value, Error> {
1fdb4c6f 208
591f570b 209 let path = path.trim_matches('/');
4a3f6517 210 let url: Uri = format!("https://{}:8007/{}", self.server, path).parse()?;
1fdb4c6f 211
a4a5c78c 212 let (ticket, token) = self.login()?;
0dffe3f9
DM
213
214 let enc_ticket = percent_encode(ticket.as_bytes(), DEFAULT_ENCODE_SET).to_string();
215
1fdb4c6f
DM
216 let request = Request::builder()
217 .method("POST")
218 .uri(url)
219 .header("User-Agent", "proxmox-backup-client/1.0")
0dffe3f9 220 .header("Cookie", format!("PBSAuthCookie={}", enc_ticket))
a4a5c78c 221 .header("CSRFPreventionToken", token)
1fdb4c6f
DM
222 .header("Content-Type", content_type)
223 .body(body)?;
224
225 Self::run_request(request)
597641fd
DM
226 }
227}