]> git.proxmox.com Git - proxmox-backup.git/blame - proxmox-protocol/src/connect.rs
more formatting & use statement fixups
[proxmox-backup.git] / proxmox-protocol / src / connect.rs
CommitLineData
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
4use std::io::{Read, Write};
5use std::net::TcpStream;
6
7use failure::{bail, format_err, Error};
8use openssl::ssl::{self, SslStream};
9use url::form_urlencoded;
10
11use crate::Client;
12
13enum 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.
21pub struct Connector {
22 user: String,
23 server: String,
24 store: String,
25 auth: Option<Authentication>,
26 certificate_validation: bool,
27}
28
29fn 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
49fn 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...
68fn 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
131fn 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
144impl 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}