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