]>
Commit | Line | Data |
---|---|---|
597641fd DM |
1 | use failure::*; |
2 | ||
3 | use http::Uri; | |
4 | use hyper::Body; | |
5 | use hyper::client::Client; | |
6 | use hyper::rt::{self, Future}; | |
7 | ||
1fdb4c6f DM |
8 | use http::Request; |
9 | use futures::stream::Stream; | |
10 | ||
11 | use serde_json::{Value}; | |
0dffe3f9 | 12 | use url::percent_encoding::{percent_encode, DEFAULT_ENCODE_SET}; |
1fdb4c6f | 13 | |
56458d97 WB |
14 | use crate::tools::tty; |
15 | ||
151c6ce2 | 16 | /// HTTP(S) API client |
597641fd | 17 | pub 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 | ||
25 | impl 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 | } |