xref: /cloud-hypervisor/api_client/src/lib.rs (revision 1968805ba291ae08e07abf0ef8c0ade4cf11ab68)
1 // Copyright © 2020 Intel Corporation
2 //
3 // SPDX-License-Identifier: Apache-2.0
4 //
5 
6 use std::io::{Read, Write};
7 use std::os::unix::io::RawFd;
8 
9 use thiserror::Error;
10 use vmm_sys_util::sock_ctrl_msg::ScmSocket;
11 
12 #[derive(Debug, Error)]
13 pub enum Error {
14     #[error("Error writing to or reading from HTTP socket: {0}")]
15     Socket(std::io::Error),
16     #[error("Error sending file descriptors: {0}")]
17     SocketSendFds(vmm_sys_util::errno::Error),
18     #[error("Error parsing HTTP status code: {0}")]
19     StatusCodeParsing(std::num::ParseIntError),
20     #[error("HTTP output is missing protocol statement")]
21     MissingProtocol,
22     #[error("Error parsing HTTP Content-Length field: {0}")]
23     ContentLengthParsing(std::num::ParseIntError),
24     #[error("Server responded with an error: {0:?}: {1:?}")]
25     ServerResponse(StatusCode, Option<String>),
26 }
27 
28 #[derive(Clone, Copy, Debug)]
29 pub enum StatusCode {
30     Continue,
31     Ok,
32     NoContent,
33     BadRequest,
34     NotFound,
35     TooManyRequests,
36     InternalServerError,
37     NotImplemented,
38     Unknown,
39 }
40 
41 impl StatusCode {
42     fn from_raw(code: usize) -> StatusCode {
43         match code {
44             100 => StatusCode::Continue,
45             200 => StatusCode::Ok,
46             204 => StatusCode::NoContent,
47             400 => StatusCode::BadRequest,
48             404 => StatusCode::NotFound,
49             429 => StatusCode::TooManyRequests,
50             500 => StatusCode::InternalServerError,
51             501 => StatusCode::NotImplemented,
52             _ => StatusCode::Unknown,
53         }
54     }
55 
56     fn parse(code: &str) -> Result<StatusCode, Error> {
57         Ok(StatusCode::from_raw(
58             code.trim().parse().map_err(Error::StatusCodeParsing)?,
59         ))
60     }
61 
62     fn is_server_error(self) -> bool {
63         !matches!(
64             self,
65             StatusCode::Ok | StatusCode::Continue | StatusCode::NoContent
66         )
67     }
68 }
69 
70 fn get_header<'a>(res: &'a str, header: &'a str) -> Option<&'a str> {
71     let header_str = format!("{header}: ");
72     res.find(&header_str)
73         .map(|o| &res[o + header_str.len()..o + res[o..].find('\r').unwrap()])
74 }
75 
76 fn get_status_code(res: &str) -> Result<StatusCode, Error> {
77     if let Some(o) = res.find("HTTP/1.1") {
78         Ok(StatusCode::parse(
79             &res[o + "HTTP/1.1 ".len()..res[o..].find('\r').unwrap()],
80         )?)
81     } else {
82         Err(Error::MissingProtocol)
83     }
84 }
85 
86 fn parse_http_response(socket: &mut dyn Read) -> Result<Option<String>, Error> {
87     let mut res = String::new();
88     let mut body_offset = None;
89     let mut content_length: Option<usize> = None;
90     loop {
91         let mut bytes = vec![0; 256];
92         let count = socket.read(&mut bytes).map_err(Error::Socket)?;
93         // If the return value is 0, the peer has performed an orderly shutdown.
94         if count == 0 {
95             break;
96         }
97         res.push_str(std::str::from_utf8(&bytes[0..count]).unwrap());
98 
99         // End of headers
100         if let Some(o) = res.find("\r\n\r\n") {
101             body_offset = Some(o + "\r\n\r\n".len());
102 
103             // With all headers available we can see if there is any body
104             content_length = if let Some(length) = get_header(&res, "Content-Length") {
105                 Some(length.trim().parse().map_err(Error::ContentLengthParsing)?)
106             } else {
107                 None
108             };
109 
110             if content_length.is_none() {
111                 break;
112             }
113         }
114 
115         if let Some(body_offset) = body_offset {
116             if let Some(content_length) = content_length {
117                 if res.len() >= content_length + body_offset {
118                     break;
119                 }
120             }
121         }
122     }
123     let body_string = content_length.and(body_offset.map(|o| String::from(&res[o..])));
124     let status_code = get_status_code(&res)?;
125 
126     if status_code.is_server_error() {
127         Err(Error::ServerResponse(status_code, body_string))
128     } else {
129         Ok(body_string)
130     }
131 }
132 
133 /// Make an API request using the fully qualified command name.
134 /// For example, full_command could be "vm.create" or "vmm.ping".
135 pub fn simple_api_full_command_with_fds_and_response<T: Read + Write + ScmSocket>(
136     socket: &mut T,
137     method: &str,
138     full_command: &str,
139     request_body: Option<&str>,
140     request_fds: Vec<RawFd>,
141 ) -> Result<Option<String>, Error> {
142     socket
143         .send_with_fds(
144             &[format!(
145                 "{method} /api/v1/{full_command} HTTP/1.1\r\nHost: localhost\r\nAccept: */*\r\n"
146             )
147             .as_bytes()],
148             &request_fds,
149         )
150         .map_err(Error::SocketSendFds)?;
151 
152     if let Some(request_body) = request_body {
153         socket
154             .write_all(format!("Content-Length: {}\r\n", request_body.len()).as_bytes())
155             .map_err(Error::Socket)?;
156     }
157 
158     socket.write_all(b"\r\n").map_err(Error::Socket)?;
159 
160     if let Some(request_body) = request_body {
161         socket
162             .write_all(request_body.as_bytes())
163             .map_err(Error::Socket)?;
164     }
165 
166     socket.flush().map_err(Error::Socket)?;
167 
168     parse_http_response(socket)
169 }
170 
171 pub fn simple_api_full_command_with_fds<T: Read + Write + ScmSocket>(
172     socket: &mut T,
173     method: &str,
174     full_command: &str,
175     request_body: Option<&str>,
176     request_fds: Vec<RawFd>,
177 ) -> Result<(), Error> {
178     let response = simple_api_full_command_with_fds_and_response(
179         socket,
180         method,
181         full_command,
182         request_body,
183         request_fds,
184     )?;
185 
186     if response.is_some() {
187         println!("{}", response.unwrap());
188     }
189 
190     Ok(())
191 }
192 
193 pub fn simple_api_full_command<T: Read + Write + ScmSocket>(
194     socket: &mut T,
195     method: &str,
196     full_command: &str,
197     request_body: Option<&str>,
198 ) -> Result<(), Error> {
199     simple_api_full_command_with_fds(socket, method, full_command, request_body, Vec::new())
200 }
201 
202 pub fn simple_api_full_command_and_response<T: Read + Write + ScmSocket>(
203     socket: &mut T,
204     method: &str,
205     full_command: &str,
206     request_body: Option<&str>,
207 ) -> Result<Option<String>, Error> {
208     simple_api_full_command_with_fds_and_response(
209         socket,
210         method,
211         full_command,
212         request_body,
213         Vec::new(),
214     )
215 }
216 
217 pub fn simple_api_command_with_fds<T: Read + Write + ScmSocket>(
218     socket: &mut T,
219     method: &str,
220     c: &str,
221     request_body: Option<&str>,
222     request_fds: Vec<RawFd>,
223 ) -> Result<(), Error> {
224     // Create the full VM command. For VMM commands, use
225     // simple_api_full_command().
226     let full_command = format!("vm.{c}");
227 
228     simple_api_full_command_with_fds(socket, method, &full_command, request_body, request_fds)
229 }
230 
231 pub fn simple_api_command<T: Read + Write + ScmSocket>(
232     socket: &mut T,
233     method: &str,
234     c: &str,
235     request_body: Option<&str>,
236 ) -> Result<(), Error> {
237     simple_api_command_with_fds(socket, method, c, request_body, Vec::new())
238 }
239