From 8de3bd728cd3015ad6e8005ada29b0d7b7be6a46 Mon Sep 17 00:00:00 2001 From: Rob Bradford Date: Fri, 23 Oct 2020 11:20:37 +0100 Subject: [PATCH] ch-remote, api_client: Split HTTP/API client code into new crate Split out the HTTP request handling code from ch-remote into a new crate which can be used in other places where talking to the API server by HTTP is necessary. Signed-off-by: Rob Bradford --- Cargo.lock | 5 ++ Cargo.toml | 2 + api_client/Cargo.toml | 5 ++ api_client/src/lib.rs | 175 +++++++++++++++++++++++++++++++++++++++ src/bin/ch-remote.rs | 187 ++++++------------------------------------ 5 files changed, 213 insertions(+), 161 deletions(-) create mode 100644 api_client/Cargo.toml create mode 100644 api_client/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 9798dcbeb..f30b49512 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,6 +46,10 @@ version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1fd36ffbb1fb7c834eac128ea8d0e310c5aeb635548f9d58861e1308d46e71c" +[[package]] +name = "api_client" +version = "0.1.0" + [[package]] name = "arc-swap" version = "0.4.7" @@ -208,6 +212,7 @@ dependencies = [ name = "cloud-hypervisor" version = "0.10.0" dependencies = [ + "api_client", "clap", "credibility", "dirs", diff --git a/Cargo.toml b/Cargo.toml index 8444d3b0d..68805164c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ homepage = "https://github.com/cloud-hypervisor/cloud-hypervisor" lto = true [dependencies] +api_client = { path = "api_client" } clap = { version = "2.33.3", features = ["wrap_help"] } hypervisor = { path = "hypervisor" } libc = "0.2.79" @@ -62,6 +63,7 @@ integration_tests = [] [workspace] members = [ "acpi_tables", + "api_client", "arch", "arch_gen", "block_util", diff --git a/api_client/Cargo.toml b/api_client/Cargo.toml new file mode 100644 index 000000000..b3f76206d --- /dev/null +++ b/api_client/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "api_client" +version = "0.1.0" +authors = ["The Cloud Hypervisor Authors"] +edition = "2018" diff --git a/api_client/src/lib.rs b/api_client/src/lib.rs new file mode 100644 index 000000000..54e8f96b3 --- /dev/null +++ b/api_client/src/lib.rs @@ -0,0 +1,175 @@ +// Copyright © 2020 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +use std::fmt; +use std::io::{Read, Write}; + +#[derive(Debug)] +pub enum Error { + Socket(std::io::Error), + StatusCodeParsing(std::num::ParseIntError), + MissingProtocol, + ContentLengthParsing(std::num::ParseIntError), + ServerResponse(StatusCode, Option), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use Error::*; + match self { + Socket(e) => write!(f, "Error writing to or reading from HTTP socket: {}", e), + StatusCodeParsing(e) => write!(f, "Error parsing HTTP status code: {}", e), + MissingProtocol => write!(f, "HTTP output is missing protocol statement"), + ContentLengthParsing(e) => write!(f, "Error parsing HTTP Content-Length field: {}", e), + ServerResponse(s, o) => { + if let Some(o) = o { + write!(f, "Server responded with an error: {:?}: {}", s, o) + } else { + write!(f, "Server responded with an error: {:?}", s) + } + } + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum StatusCode { + Continue, + OK, + NoContent, + BadRequest, + NotFound, + InternalServerError, + NotImplemented, + Unknown, +} + +impl StatusCode { + fn from_raw(code: usize) -> StatusCode { + match code { + 100 => StatusCode::Continue, + 200 => StatusCode::OK, + 204 => StatusCode::NoContent, + 400 => StatusCode::BadRequest, + 404 => StatusCode::NotFound, + 500 => StatusCode::InternalServerError, + 501 => StatusCode::NotImplemented, + _ => StatusCode::Unknown, + } + } + + fn parse(code: &str) -> Result { + Ok(StatusCode::from_raw( + code.trim().parse().map_err(Error::StatusCodeParsing)?, + )) + } + + fn is_server_error(self) -> bool { + !matches!( + self, + StatusCode::OK | StatusCode::Continue | StatusCode::NoContent + ) + } +} + +fn get_header<'a>(res: &'a str, header: &'a str) -> Option<&'a str> { + let header_str = format!("{}: ", header); + if let Some(o) = res.find(&header_str) { + Some(&res[o + header_str.len()..o + res[o..].find('\r').unwrap()]) + } else { + None + } +} + +fn get_status_code(res: &str) -> Result { + if let Some(o) = res.find("HTTP/1.1") { + Ok(StatusCode::parse( + &res[o + "HTTP/1.1 ".len()..res[o..].find('\r').unwrap()], + )?) + } else { + Err(Error::MissingProtocol) + } +} + +fn parse_http_response(socket: &mut dyn Read) -> Result, Error> { + let mut res = String::new(); + let mut body_offset = None; + let mut content_length: Option = None; + loop { + let mut bytes = vec![0; 256]; + let count = socket.read(&mut bytes).map_err(Error::Socket)?; + res.push_str(std::str::from_utf8(&bytes[0..count]).unwrap()); + + // End of headers + if let Some(o) = res.find("\r\n\r\n") { + body_offset = Some(o + "\r\n\r\n".len()); + + // With all headers available we can see if there is any body + content_length = if let Some(length) = get_header(&res, "Content-Length") { + Some(length.trim().parse().map_err(Error::ContentLengthParsing)?) + } else { + None + }; + + if content_length.is_none() { + break; + } + } + + if let Some(body_offset) = body_offset { + if let Some(content_length) = content_length { + if res.len() >= content_length + body_offset { + break; + } + } + } + } + let body_string = content_length.and(Some(String::from(&res[body_offset.unwrap()..]))); + let status_code = get_status_code(&res)?; + + if status_code.is_server_error() { + Err(Error::ServerResponse(status_code, body_string)) + } else { + Ok(body_string) + } +} + +pub fn simple_api_command( + socket: &mut T, + method: &str, + c: &str, + request_body: Option<&str>, +) -> Result<(), Error> { + socket + .write_all( + format!( + "{} /api/v1/vm.{} HTTP/1.1\r\nHost: localhost\r\nAccept: */*\r\n", + method, c + ) + .as_bytes(), + ) + .map_err(Error::Socket)?; + + if let Some(request_body) = request_body { + socket + .write_all(format!("Content-Length: {}\r\n", request_body.len()).as_bytes()) + .map_err(Error::Socket)?; + } + + socket.write_all(b"\r\n").map_err(Error::Socket)?; + + if let Some(request_body) = request_body { + socket + .write_all(request_body.as_bytes()) + .map_err(Error::Socket)?; + } + + socket.flush().map_err(Error::Socket)?; + + if let Some(body) = parse_http_response(socket)? { + println!("{}", body); + } + Ok(()) +} diff --git a/src/bin/ch-remote.rs b/src/bin/ch-remote.rs index e598b63a4..45611c898 100644 --- a/src/bin/ch-remote.rs +++ b/src/bin/ch-remote.rs @@ -5,23 +5,22 @@ #[macro_use(crate_authors)] extern crate clap; +extern crate api_client; extern crate serde_json; extern crate vmm; +use api_client::simple_api_command; +use api_client::Error as ApiClientError; use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; use option_parser::{ByteSized, ByteSizedParseError}; use std::fmt; -use std::io::{Read, Write}; use std::os::unix::net::UnixStream; use std::process; #[derive(Debug)] enum Error { - Socket(std::io::Error), - StatusCodeParsing(std::num::ParseIntError), - MissingProtocol, - ContentLengthParsing(std::num::ParseIntError), - ServerResponse(StatusCode, Option), + Connect(std::io::Error), + ApiClient(ApiClientError), InvalidCPUCount(std::num::ParseIntError), InvalidMemorySize(ByteSizedParseError), InvalidBalloonSize(ByteSizedParseError), @@ -38,17 +37,8 @@ impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use Error::*; match self { - Socket(e) => write!(f, "Error writing to HTTP socket: {}", e), - StatusCodeParsing(e) => write!(f, "Error parsing HTTP status code: {}", e), - MissingProtocol => write!(f, "HTTP output is missing protocol statement"), - ContentLengthParsing(e) => write!(f, "Error parsing HTTP Content-Length field: {}", e), - ServerResponse(s, o) => { - if let Some(o) = o { - write!(f, "Server responded with an error: {:?}: {}", s, o) - } else { - write!(f, "Server responded with an error: {:?}", s) - } - } + ApiClient(e) => e.fmt(f), + Connect(e) => write!(f, "Error openning HTTP socket: {}", e), InvalidCPUCount(e) => write!(f, "Error parsing CPU count: {}", e), InvalidMemorySize(e) => write!(f, "Error parsing memory size: {:?}", e), InvalidBalloonSize(e) => write!(f, "Error parsing balloon size: {:?}", e), @@ -63,146 +53,6 @@ impl fmt::Display for Error { } } -#[derive(Clone, Copy, Debug)] -pub enum StatusCode { - Continue, - OK, - NoContent, - BadRequest, - NotFound, - InternalServerError, - NotImplemented, - Unknown, -} - -impl StatusCode { - fn from_raw(code: usize) -> StatusCode { - match code { - 100 => StatusCode::Continue, - 200 => StatusCode::OK, - 204 => StatusCode::NoContent, - 400 => StatusCode::BadRequest, - 404 => StatusCode::NotFound, - 500 => StatusCode::InternalServerError, - 501 => StatusCode::NotImplemented, - _ => StatusCode::Unknown, - } - } - - fn parse(code: &str) -> Result { - Ok(StatusCode::from_raw( - code.trim().parse().map_err(Error::StatusCodeParsing)?, - )) - } - - fn is_server_error(self) -> bool { - !matches!( - self, - StatusCode::OK | StatusCode::Continue | StatusCode::NoContent - ) - } -} - -fn get_header<'a>(res: &'a str, header: &'a str) -> Option<&'a str> { - let header_str = format!("{}: ", header); - if let Some(o) = res.find(&header_str) { - Some(&res[o + header_str.len()..o + res[o..].find('\r').unwrap()]) - } else { - None - } -} - -fn get_status_code(res: &str) -> Result { - if let Some(o) = res.find("HTTP/1.1") { - Ok(StatusCode::parse( - &res[o + "HTTP/1.1 ".len()..res[o..].find('\r').unwrap()], - )?) - } else { - Err(Error::MissingProtocol) - } -} - -fn parse_http_response(socket: &mut dyn Read) -> Result, Error> { - let mut res = String::new(); - let mut body_offset = None; - let mut content_length: Option = None; - loop { - let mut bytes = vec![0; 256]; - let count = socket.read(&mut bytes).map_err(Error::Socket)?; - res.push_str(std::str::from_utf8(&bytes[0..count]).unwrap()); - - // End of headers - if let Some(o) = res.find("\r\n\r\n") { - body_offset = Some(o + "\r\n\r\n".len()); - - // With all headers available we can see if there is any body - content_length = if let Some(length) = get_header(&res, "Content-Length") { - Some(length.trim().parse().map_err(Error::ContentLengthParsing)?) - } else { - None - }; - - if content_length.is_none() { - break; - } - } - - if let Some(body_offset) = body_offset { - if let Some(content_length) = content_length { - if res.len() >= content_length + body_offset { - break; - } - } - } - } - let body_string = content_length.and(Some(String::from(&res[body_offset.unwrap()..]))); - let status_code = get_status_code(&res)?; - - if status_code.is_server_error() { - Err(Error::ServerResponse(status_code, body_string)) - } else { - Ok(body_string) - } -} - -fn simple_api_command( - socket: &mut T, - method: &str, - c: &str, - request_body: Option<&str>, -) -> Result<(), Error> { - socket - .write_all( - format!( - "{} /api/v1/vm.{} HTTP/1.1\r\nHost: localhost\r\nAccept: */*\r\n", - method, c - ) - .as_bytes(), - ) - .map_err(Error::Socket)?; - - if let Some(request_body) = request_body { - socket - .write_all(format!("Content-Length: {}\r\n", request_body.len()).as_bytes()) - .map_err(Error::Socket)?; - } - - socket.write_all(b"\r\n").map_err(Error::Socket)?; - - if let Some(request_body) = request_body { - socket - .write_all(request_body.as_bytes()) - .map_err(Error::Socket)?; - } - - socket.flush().map_err(Error::Socket)?; - - if let Some(body) = parse_http_response(socket)? { - println!("{}", body); - } - Ok(()) -} - fn resize_api_command( socket: &mut UnixStream, cpus: Option<&str>, @@ -249,6 +99,7 @@ fn resize_api_command( "resize", Some(&serde_json::to_string(&resize).unwrap()), ) + .map_err(Error::ApiClient) } fn resize_zone_api_command(socket: &mut UnixStream, id: &str, size: &str) -> Result<(), Error> { @@ -266,6 +117,7 @@ fn resize_zone_api_command(socket: &mut UnixStream, id: &str, size: &str) -> Res "resize-zone", Some(&serde_json::to_string(&resize_zone).unwrap()), ) + .map_err(Error::ApiClient) } fn add_device_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error> { @@ -277,6 +129,7 @@ fn add_device_api_command(socket: &mut UnixStream, config: &str) -> Result<(), E "add-device", Some(&serde_json::to_string(&device_config).unwrap()), ) + .map_err(Error::ApiClient) } fn remove_device_api_command(socket: &mut UnixStream, id: &str) -> Result<(), Error> { @@ -288,6 +141,7 @@ fn remove_device_api_command(socket: &mut UnixStream, id: &str) -> Result<(), Er "remove-device", Some(&serde_json::to_string(&remove_device_data).unwrap()), ) + .map_err(Error::ApiClient) } fn add_disk_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error> { @@ -299,6 +153,7 @@ fn add_disk_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Err "add-disk", Some(&serde_json::to_string(&disk_config).unwrap()), ) + .map_err(Error::ApiClient) } fn add_fs_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error> { @@ -310,6 +165,7 @@ fn add_fs_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error "add-fs", Some(&serde_json::to_string(&fs_config).unwrap()), ) + .map_err(Error::ApiClient) } fn add_pmem_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error> { @@ -321,6 +177,7 @@ fn add_pmem_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Err "add-pmem", Some(&serde_json::to_string(&pmem_config).unwrap()), ) + .map_err(Error::ApiClient) } fn add_net_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error> { @@ -332,6 +189,7 @@ fn add_net_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Erro "add-net", Some(&serde_json::to_string(&net_config).unwrap()), ) + .map_err(Error::ApiClient) } fn add_vsock_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error> { @@ -343,6 +201,7 @@ fn add_vsock_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Er "add-vsock", Some(&serde_json::to_string(&vsock_config).unwrap()), ) + .map_err(Error::ApiClient) } fn snapshot_api_command(socket: &mut UnixStream, url: &str) -> Result<(), Error> { @@ -356,6 +215,7 @@ fn snapshot_api_command(socket: &mut UnixStream, url: &str) -> Result<(), Error> "snapshot", Some(&serde_json::to_string(&snapshot_config).unwrap()), ) + .map_err(Error::ApiClient) } fn restore_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Error> { @@ -367,15 +227,20 @@ fn restore_api_command(socket: &mut UnixStream, config: &str) -> Result<(), Erro "restore", Some(&serde_json::to_string(&restore_config).unwrap()), ) + .map_err(Error::ApiClient) } fn do_command(matches: &ArgMatches) -> Result<(), Error> { let mut socket = - UnixStream::connect(matches.value_of("api-socket").unwrap()).map_err(Error::Socket)?; + UnixStream::connect(matches.value_of("api-socket").unwrap()).map_err(Error::Connect)?; match matches.subcommand_name() { - Some("info") => simple_api_command(&mut socket, "GET", "info", None), - Some("counters") => simple_api_command(&mut socket, "GET", "counters", None), + Some("info") => { + simple_api_command(&mut socket, "GET", "info", None).map_err(Error::ApiClient) + } + Some("counters") => { + simple_api_command(&mut socket, "GET", "counters", None).map_err(Error::ApiClient) + } Some("resize") => resize_api_command( &mut socket, matches @@ -476,7 +341,7 @@ fn do_command(matches: &ArgMatches) -> Result<(), Error> { .value_of("restore_config") .unwrap(), ), - Some(c) => simple_api_command(&mut socket, "PUT", c, None), + Some(c) => simple_api_command(&mut socket, "PUT", c, None).map_err(Error::ApiClient), None => unreachable!(), } }