diff --git a/keylime-agent/src/api.rs b/keylime-agent/src/api.rs new file mode 100644 index 00000000..9e0eb402 --- /dev/null +++ b/keylime-agent/src/api.rs @@ -0,0 +1,174 @@ +use crate::{ + agent_handler, + common::{JsonWrapper, API_VERSION}, + config, errors_handler, keys_handler, notifications_handler, + quotes_handler, +}; +use actix_web::{http, web, HttpRequest, HttpResponse, Responder, Scope}; +use log::*; +use thiserror::Error; + +pub const SUPPORTED_API_VERSIONS: &[&str] = &[API_VERSION]; + +#[derive(Error, Debug, PartialEq)] +pub enum APIError { + #[error("API version \"{0}\" not supported")] + UnsupportedVersion(String), +} + +/// Handles the default case for the API version scope +async fn api_default(req: HttpRequest) -> impl Responder { + let error; + let response; + let message; + + match req.head().method { + http::Method::GET => { + error = 400; + message = + "Not Implemented: Use /agent, /keys, or /quotes interfaces"; + response = HttpResponse::BadRequest() + .json(JsonWrapper::error(error, message)); + } + http::Method::POST => { + error = 400; + message = + "Not Implemented: Use /keys or /notifications interfaces"; + response = HttpResponse::BadRequest() + .json(JsonWrapper::error(error, message)); + } + _ => { + error = 405; + message = "Method is not supported"; + response = HttpResponse::MethodNotAllowed() + .insert_header(http::header::Allow(vec![ + http::Method::GET, + http::Method::POST, + ])) + .json(JsonWrapper::error(error, message)); + } + }; + + warn!( + "{} returning {} response. {}", + req.head().method, + error, + message + ); + + response +} + +/// Configure the endpoints supported by API version 2.1 +/// +/// Version 2.1 is the base API version +fn configure_api_v2_1(cfg: &mut web::ServiceConfig) { + _ = cfg + .service( + web::scope("/keys") + .configure(keys_handler::configure_keys_endpoints), + ) + .service(web::scope("/notifications").configure( + notifications_handler::configure_notifications_endpoints, + )) + .service( + web::scope("/quotes") + .configure(quotes_handler::configure_quotes_endpoints), + ) + .default_service(web::to(api_default)) +} + +/// Configure the endpoints supported by API version 2.2 +/// +/// The version 2.2 added the /agent/info endpoint +fn configure_api_v2_2(cfg: &mut web::ServiceConfig) { + // Configure the endpoints shared with version 2.1 + configure_api_v2_1(cfg); + + // Configure added endpoints + _ = cfg.service( + web::scope("/agent") + .configure(agent_handler::configure_agent_endpoints), + ) +} + +/// Get a scope configured for the given API version +pub(crate) fn get_api_scope(version: &str) -> Result { + match version { + "v2.1" => Ok(web::scope(version).configure(configure_api_v2_1)), + "v2.2" => Ok(web::scope(version).configure(configure_api_v2_2)), + _ => Err(APIError::UnsupportedVersion(version.into())), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use actix_web::{test, web, App}; + use serde_json::{json, Value}; + + #[actix_rt::test] + async fn test_configure_api() { + // Test that invalid version results in error + let result = get_api_scope("invalid"); + assert!(result.is_err()); + if let Err(e) = result { + assert_eq!(e, APIError::UnsupportedVersion("invalid".into())); + } + + // Test that a valid version is successful + let version = SUPPORTED_API_VERSIONS.last().unwrap(); //#[allow_ci] + let result = get_api_scope(version); + assert!(result.is_ok()); + let scope = result.unwrap(); //#[allow_ci] + } + + #[actix_rt::test] + async fn test_api_default() { + let mut app = test::init_service( + App::new().service(web::resource("/").to(api_default)), + ) + .await; + + let req = test::TestRequest::get().uri("/").to_request(); + + let resp = test::call_service(&app, req).await; + assert!(resp.status().is_client_error()); + + let result: JsonWrapper = test::read_body_json(resp).await; + + assert_eq!(result.results, json!({})); + assert_eq!(result.code, 400); + + let req = test::TestRequest::post() + .uri("/") + .data("some data") + .to_request(); + + let resp = test::call_service(&app, req).await; + assert!(resp.status().is_client_error()); + + let result: JsonWrapper = test::read_body_json(resp).await; + + assert_eq!(result.results, json!({})); + assert_eq!(result.code, 400); + + let req = test::TestRequest::delete().uri("/").to_request(); + + let resp = test::call_service(&app, req).await; + assert!(resp.status().is_client_error()); + + let headers = resp.headers(); + + assert!(headers.contains_key("allow")); + assert_eq!( + headers.get("allow").unwrap().to_str().unwrap(), //#[allow_ci] + "GET, POST" + ); + + let result: JsonWrapper = test::read_body_json(resp).await; + + assert_eq!(result.results, json!({})); + assert_eq!(result.code, 405); + } +} diff --git a/keylime-agent/src/errors_handler.rs b/keylime-agent/src/errors_handler.rs index 6e20bf44..0d218af1 100644 --- a/keylime-agent/src/errors_handler.rs +++ b/keylime-agent/src/errors_handler.rs @@ -54,48 +54,6 @@ pub(crate) async fn app_default(req: HttpRequest) -> impl Responder { response } -pub(crate) async fn api_default(req: HttpRequest) -> impl Responder { - let error; - let response; - let message; - - match req.head().method { - http::Method::GET => { - error = 400; - message = - "Not Implemented: Use /agent, /keys, or /quotes interfaces"; - response = HttpResponse::BadRequest() - .json(JsonWrapper::error(error, message)); - } - http::Method::POST => { - error = 400; - message = - "Not Implemented: Use /keys or /notifications interfaces"; - response = HttpResponse::BadRequest() - .json(JsonWrapper::error(error, message)); - } - _ => { - error = 405; - message = "Method is not supported"; - response = HttpResponse::MethodNotAllowed() - .insert_header(http::header::Allow(vec![ - http::Method::GET, - http::Method::POST, - ])) - .json(JsonWrapper::error(error, message)); - } - }; - - warn!( - "{} returning {} response. {}", - req.head().method, - error, - message - ); - - response -} - pub(crate) async fn version_not_supported( req: HttpRequest, version: web::Path, @@ -219,11 +177,6 @@ mod tests { test_default(web::resource("/").to(app_default), "GET, POST").await } - #[actix_rt::test] - async fn test_api_default() { - test_default(web::resource("/").to(api_default), "GET, POST").await - } - #[derive(Serialize, Deserialize)] struct DummyQuery { param: String, diff --git a/keylime-agent/src/main.rs b/keylime-agent/src/main.rs index ca5c0c69..e331807b 100644 --- a/keylime-agent/src/main.rs +++ b/keylime-agent/src/main.rs @@ -32,6 +32,7 @@ #![allow(unused, missing_docs)] mod agent_handler; +mod api; mod common; mod config; mod error; @@ -863,71 +864,60 @@ async fn main() -> Result<()> { secure_mount: PathBuf::from(&mount), }); - let actix_server = - HttpServer::new(move || { - App::new() - .wrap(middleware::ErrorHandlers::new().handler( - http::StatusCode::NOT_FOUND, - errors_handler::wrap_404, - )) - .wrap(middleware::Logger::new( - "%r from %a result %s (took %D ms)", - )) - .wrap_fn(|req, srv| { - info!( - "{} invoked from {:?} with uri {}", - req.head().method, - req.connection_info().peer_addr().unwrap(), //#[allow_ci] - req.uri() - ); - srv.call(req) - }) - .app_data(quotedata.clone()) - .app_data( - web::JsonConfig::default() - .error_handler(errors_handler::json_parser_error), - ) - .app_data( - web::QueryConfig::default() - .error_handler(errors_handler::query_parser_error), - ) - .app_data( - web::PathConfig::default() - .error_handler(errors_handler::path_parser_error), - ) - .service( - web::scope(&format!("/{API_VERSION}")) - .service(web::scope("/agent").configure( - agent_handler::configure_agent_endpoints, - )) - .service(web::scope("/keys").configure( - keys_handler::configure_keys_endpoints, - )) - .service( - web::scope("/notifications").configure( - notifications_handler::configure_notifications_endpoints, - )) - .service(web::scope("/quotes").configure( - quotes_handler::configure_quotes_endpoints, - )) - .default_service(web::to( - errors_handler::api_default, - )), - ) - .service( - web::resource("/version") - .route(web::get().to(version_handler::version)), - ) - .service( - web::resource(r"/v{major:\d+}.{minor:\d+}{tail}*") - .to(errors_handler::version_not_supported), - ) - .default_service(web::to(errors_handler::app_default)) - }) - // Disable default signal handlers. See: - // https://github.com/actix/actix-web/issues/2739 - // for details. - .disable_signals(); + let actix_server = HttpServer::new(move || { + let mut app = App::new() + .wrap(middleware::ErrorHandlers::new().handler( + http::StatusCode::NOT_FOUND, + errors_handler::wrap_404, + )) + .wrap(middleware::Logger::new( + "%r from %a result %s (took %D ms)", + )) + .wrap_fn(|req, srv| { + info!( + "{} invoked from {:?} with uri {}", + req.head().method, + req.connection_info().peer_addr().unwrap(), //#[allow_ci] + req.uri() + ); + srv.call(req) + }) + .app_data(quotedata.clone()) + .app_data( + web::JsonConfig::default() + .error_handler(errors_handler::json_parser_error), + ) + .app_data( + web::QueryConfig::default() + .error_handler(errors_handler::query_parser_error), + ) + .app_data( + web::PathConfig::default() + .error_handler(errors_handler::path_parser_error), + ); + + let enabled_api_versions = api::SUPPORTED_API_VERSIONS; + + for version in enabled_api_versions { + // This should never fail, thus unwrap should never panic + let scope = api::get_api_scope(version).unwrap(); //#[allow_ci] + app = app.service(scope); + } + + app.service( + web::resource("/version") + .route(web::get().to(version_handler::version)), + ) + .service( + web::resource(r"/v{major:\d+}.{minor:\d+}{tail}*") + .to(errors_handler::version_not_supported), + ) + .default_service(web::to(errors_handler::app_default)) + }) + // Disable default signal handlers. See: + // https://github.com/actix/actix-web/issues/2739 + // for details. + .disable_signals(); let server;