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