diff --git a/Cargo.toml b/Cargo.toml index fefd43d0..bda503f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,4 +5,9 @@ members = [ "proxmox-api-macro", "proxmox-sys", "proxmox", + +# This is an api server test and may be temporarily broken by changes to +# proxmox-api or proxmox-api-macro, but should ultimately be updated to work +# again as it's supposed to serve as an example code! + "api-test", ] diff --git a/api-test/Cargo.toml b/api-test/Cargo.toml new file mode 100644 index 00000000..cec46bf5 --- /dev/null +++ b/api-test/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "api-test" +edition = "2018" +version = "0.1.0" +authors = [ + "Dietmar Maurer ", + "Wolfgang Bumiller ", +] + +[dependencies] +bytes = "0.4" +endian_trait = { version = "0.6", features = [ "arrays" ] } +failure = "0.1" +futures-01 = { package = "futures", version = "0.1" } +futures-preview = { version = "0.3.0-alpha.16", features = [ "compat", "io-compat" ] } +http = "0.1" +hyper = "0.12" +proxmox = { path = "../proxmox" } +serde_json = "1.0" +tokio = "0.1" diff --git a/api-test/src/api.rs b/api-test/src/api.rs new file mode 100644 index 00000000..74de0772 --- /dev/null +++ b/api-test/src/api.rs @@ -0,0 +1,95 @@ +use std::io; +use std::path::Path; + +use failure::{bail, Error}; +use futures::compat::AsyncRead01CompatExt; +use futures::compat::Future01CompatExt; +use futures::io::AsyncReadExt; +use http::Response; +use hyper::Body; + +use proxmox::api::{api, router}; + +#[api({ + description: "Hello API call", +})] +async fn hello() -> Result, Error> { + Ok(http::Response::builder() + .status(200) + .header("content-type", "text/html") + .body(Body::from("Hello"))?) +} + +static mut WWW_DIR: Option = None; + +pub fn www_dir() -> &'static str { + unsafe { + WWW_DIR + .as_ref() + .expect("expected WWW_DIR to be initialized") + .as_str() + } +} + +pub fn set_www_dir(dir: String) { + unsafe { + assert!(WWW_DIR.is_none(), "WWW_DIR must only be initialized once!"); + + WWW_DIR = Some(dir); + } +} + +#[api({ + description: "Get a file from the www/ subdirectory.", + parameters: { + path: "Path to the file to fetch", + }, +})] +async fn get_www(path: String) -> Result, Error> { + if path.contains("..") { + bail!("illegal path"); + } + + let mut file = match tokio::fs::File::open(format!("{}/{}", www_dir(), path)) + .compat() + .await + { + Ok(file) => file, + Err(ref err) if err.kind() == io::ErrorKind::NotFound => { + return Ok(http::Response::builder() + .status(404) + .body(Body::from(format!("No such file or directory: {}", path)))?); + } + Err(e) => return Err(e.into()), + } + .compat(); + + let mut data = Vec::new(); + file.read_to_end(&mut data).await?; + + let mut response = http::Response::builder(); + response.status(200); + + let content_type = match Path::new(&path).extension().and_then(|e| e.to_str()) { + Some("html") => Some("text/html"), + Some("css") => Some("text/css"), + Some("js") => Some("application/javascript"), + Some("txt") => Some("text/plain"), + // ... + _ => None, + }; + if let Some(content_type) = content_type { + response.header("content-type", content_type); + } + + Ok(response.body(Body::from(data))?) +} + +router! { + pub static ROUTER: Router = { + GET: hello, + /www/{path}*: { GET: get_www }, + /api/1: { + } + }; +} diff --git a/api-test/src/main.rs b/api-test/src/main.rs new file mode 100644 index 00000000..01c8b37d --- /dev/null +++ b/api-test/src/main.rs @@ -0,0 +1,70 @@ +#![feature(async_await)] + +use failure::{format_err, Error}; +use http::Request; +use http::Response; +use hyper::service::service_fn; +use hyper::{Body, Server}; +use serde_json::Value; + +mod api; +pub static FOO: u32 = 3; + +async fn run_request(request: Request) -> Result, hyper::Error> { + match route_request(request).await { + Ok(r) => Ok(r), + Err(err) => Ok(Response::builder() + .status(400) + .body(Body::from(format!("ERROR: {}", err))) + .expect("building an error response...")), + } +} + +async fn route_request(request: Request) -> Result, Error> { + let path = request.uri().path(); + + let (target, params) = api::ROUTER + .lookup(path) + .ok_or_else(|| format_err!("missing path: {}", path))?; + + let handler = target + .get + .as_ref() + .ok_or_else(|| format_err!("no GET method for: {}", path))? + .handler(); + + Ok(handler(params.unwrap_or(Value::Null)).await?) +} + +type BoxFut = Box, Error = hyper::Error> + Send>; + +fn main() { + // We expect a path, where to find our files we expose via the www/ dir: + let mut args = std::env::args(); + + // real code should have better error handling + let _program_name = args.next(); + let www_dir = args.next().expect("expected a www/ subdirectory"); + api::set_www_dir(www_dir.to_string()); + + // Construct our SocketAddr to listen on... + let addr = ([0, 0, 0, 0], 3000).into(); + + // And a MakeService to handle each connection... + let make_service = || { + service_fn(|req| { + let fut: BoxFut = Box::new(futures::compat::Compat::new(Box::pin(run_request(req)))); + fut + }) + }; + + // Then bind and serve... + let server = { + use futures_01::Future; + Server::bind(&addr) + .serve(make_service) + .map_err(|err| eprintln!("server error: {}", err)) + }; + + tokio::run(server); +}