]>
Commit | Line | Data |
---|---|---|
41310cb9 WB |
1 | //! This module provides a `Connector` used to log into a Proxmox Backup API server and connect to |
2 | //! the proxmox protocol via an HTTP Upgrade request. | |
3 | ||
4 | use std::io::{Read, Write}; | |
5 | use std::net::TcpStream; | |
6 | ||
7 | use failure::{bail, format_err, Error}; | |
8 | use openssl::ssl::{self, SslStream}; | |
9 | use url::form_urlencoded; | |
10 | ||
11 | use crate::Client; | |
12 | ||
13 | enum Authentication { | |
14 | Password(String), | |
15 | Ticket(String, String), | |
16 | } | |
17 | ||
18 | /// Connector used to log into a Proxmox Backup API server and open a backup protocol connection. | |
19 | /// If successful, this will create a `Client` used to communicate over the Proxmox Backup | |
20 | /// Protocol. | |
21 | pub struct Connector { | |
22 | user: String, | |
23 | server: String, | |
24 | store: String, | |
25 | auth: Option<Authentication>, | |
26 | certificate_validation: bool, | |
27 | } | |
28 | ||
29 | fn build_login(host: &str, user: &str, pass: &str) -> Vec<u8> { | |
30 | let formdata = form_urlencoded::Serializer::new(String::new()) | |
31 | .append_pair("username", user) | |
32 | .append_pair("password", pass) | |
33 | .finish(); | |
34 | ||
35 | format!("\ | |
36 | POST /api2/json/access/ticket HTTP/1.1\r\n\ | |
37 | host: {}\r\n\ | |
38 | content-length: {}\r\n\ | |
39 | content-type: application/x-www-form-urlencoded\r\n\ | |
40 | \r\n\ | |
41 | {}", | |
42 | host, | |
43 | formdata.as_bytes().len(), | |
44 | formdata, | |
45 | ) | |
46 | .into_bytes() | |
47 | } | |
48 | ||
49 | fn build_protocol_connect(host: &str, store: &str, ticket: &str, token: &str) -> Vec<u8> { | |
50 | format!("\ | |
51 | GET /api2/json/admin/datastore/{}/test-upload HTTP/1.1\r\n\ | |
52 | host: {}\r\n\ | |
53 | connection: upgrade\r\n\ | |
54 | upgrade: proxmox-backup-protocol-1\r\n\ | |
55 | cookie: PBSAuthCookie={}\r\n\ | |
56 | CSRFPreventionToken: {}\r\n\ | |
57 | \r\n", | |
58 | store, | |
59 | host, | |
60 | ticket, | |
61 | token, | |
62 | ) | |
63 | .into_bytes() | |
64 | } | |
65 | ||
66 | // Minimalistic http response reader. The only things we care about here are the status code and | |
67 | // the payload... | |
68 | fn read_http_response<T: Read>(sock: T) -> Result<(u16, Vec<u8>), Error> { | |
69 | use std::io::BufRead; | |
70 | let mut reader = std::io::BufReader::new(sock); | |
71 | ||
72 | let mut status = String::new(); | |
73 | reader.read_line(&mut status)?; | |
74 | ||
75 | let status = status.trim_end(); | |
76 | let mut parts = status.splitn(3, ' '); | |
77 | let _version = parts | |
78 | .next() | |
79 | .ok_or_else(|| format_err!("bad http response (missing version)"))?; | |
80 | let code = parts | |
81 | .next() | |
82 | .ok_or_else(|| format_err!("bad http response (missing status code)"))?; | |
83 | let _reason = parts.next(); | |
84 | ||
85 | let code: u16 = code.parse()?; | |
86 | ||
87 | // We need the payload's length if there is one: | |
88 | let mut length: Option<u32> = None; | |
89 | let mut line = String::new(); | |
90 | loop { | |
91 | line.clear(); | |
92 | reader.read_line(&mut line)?; | |
93 | let line = line.trim_end(); | |
94 | ||
95 | if line.len() == 0 { | |
96 | break; | |
97 | } | |
98 | ||
99 | let parts: Vec<&str> = line.splitn(2, ':').collect(); | |
100 | if parts.len() != 2 { | |
101 | bail!("invalid header in http response"); | |
102 | } | |
103 | ||
104 | let name = parts[0].trim().to_lowercase().to_string(); | |
105 | let value = parts[1].trim(); | |
106 | ||
107 | // The only important header (important to know how much we need to read!) | |
108 | if name == "content-length" { | |
109 | length = Some(value.parse()?); | |
110 | } | |
111 | ||
112 | // Don't care about any other header contents currently... | |
113 | } | |
114 | ||
115 | match length { | |
116 | None => Ok((code, Vec::new())), | |
117 | Some(length) => { | |
118 | let length = length as usize; | |
119 | ||
120 | let mut out = Vec::with_capacity(length); | |
121 | unsafe { | |
122 | out.set_len(length); | |
123 | } | |
124 | ||
125 | reader.read_exact(&mut out)?; | |
126 | Ok((code, out)) | |
127 | }, | |
128 | } | |
129 | } | |
130 | ||
131 | fn parse_login_response(data: &[u8]) -> Result<(String, String), Error> { | |
132 | let value: serde_json::Value = serde_json::from_slice(data)?; | |
133 | let ticket = value["data"]["ticket"] | |
134 | .as_str() | |
135 | .ok_or_else(|| format_err!("no ticket found in login response"))? | |
136 | .to_string(); | |
137 | let token = value["data"]["CSRFPreventionToken"] | |
138 | .as_str() | |
139 | .ok_or_else(|| format_err!("no token found in login response"))? | |
140 | .to_string(); | |
141 | Ok((ticket, token)) | |
142 | } | |
143 | ||
144 | impl Connector { | |
145 | /// Create a new connector for a specified user, server and remote backup store. | |
146 | pub fn new(user: String, server: String, store: String) -> Self { | |
147 | Self { | |
148 | user, | |
149 | server, | |
150 | store, | |
151 | auth: None, | |
152 | certificate_validation: true, | |
153 | } | |
154 | } | |
155 | ||
156 | /// Use a password to authenticate with the remote server. | |
157 | pub fn password(mut self, pass: String) -> Self { | |
158 | self.auth = Some(Authentication::Password(pass)); | |
159 | self | |
160 | } | |
161 | ||
162 | /// Use an already existing ticket to connect to the server. | |
163 | pub fn ticket(mut self, ticket: String, token: String) -> Self { | |
164 | self.auth = Some(Authentication::Ticket(ticket, token)); | |
165 | self | |
166 | } | |
167 | ||
168 | /// Disable TLS certificate validation. | |
169 | pub fn certificate_validation(mut self, on: bool) -> Self { | |
170 | self.certificate_validation = on; | |
171 | self | |
172 | } | |
173 | ||
174 | pub(crate) fn do_connect(self) -> Result<SslStream<TcpStream>, Error> { | |
175 | if self.auth.is_none() { | |
176 | bail!("missing authentication"); | |
177 | } | |
178 | ||
179 | let stream = TcpStream::connect(&self.server)?; | |
180 | ||
181 | let mut connector = ssl::SslConnector::builder(ssl::SslMethod::tls())?; | |
182 | if !self.certificate_validation { | |
183 | connector.set_verify(ssl::SslVerifyMode::NONE); | |
184 | } | |
185 | let connector = connector.build(); | |
186 | ||
187 | let mut stream = connector.connect(&self.server, stream)?; | |
188 | let (ticket, token) = match self.auth { | |
189 | None => unreachable!(), // checked above | |
190 | Some(Authentication::Password(password)) => { | |
191 | let login_request = build_login(&self.server, &self.user, &password); | |
192 | stream.write_all(&login_request)?; | |
193 | ||
194 | let (code, ticket) = read_http_response(&mut stream)?; | |
195 | if code != 200 { | |
196 | bail!("login failed"); | |
197 | } | |
198 | ||
199 | parse_login_response(&ticket)? | |
200 | } | |
201 | Some(Authentication::Ticket(ticket, token)) => (ticket, token), | |
202 | }; | |
203 | ||
204 | let protocol_request = build_protocol_connect(&self.server, &self.store, &ticket, &token); | |
205 | stream.write_all(&protocol_request)?; | |
206 | let (code, _empty_body) = read_http_response(&mut stream)?; | |
207 | if code != 101 { | |
208 | bail!("expected 101 Switching Protocol, received code: {}", code); | |
209 | } | |
210 | ||
211 | Ok(stream) | |
212 | } | |
213 | ||
214 | /// This creates creates a synchronous client (via blocking I/O), tries to authenticate with | |
215 | /// the server and connect to the protocol endpoint. On success, a `Client` is returned. | |
216 | pub fn connect(self) -> Result<Client<SslStream<TcpStream>>, Error> { | |
217 | let stream = self.do_connect()?; | |
218 | ||
219 | let mut client = Client::new(stream); | |
220 | if !client.wait_for_handshake()? { | |
221 | bail!("handshake failed"); | |
222 | } | |
223 | Ok(client) | |
224 | } | |
225 | } |