Skip to content

Commit

Permalink
api: Make API configuration modular
Browse files Browse the repository at this point in the history
Move the API configuration to a dedicated file and make it modular.

The API configuration is separated for each supported version.
Currently only the latest API version (v2.2) is supported.

This is a preparation to support multiple API versions.

Signed-off-by: Anderson Toshiyuki Sasaki <[email protected]>
  • Loading branch information
ansasaki committed Oct 18, 2024
1 parent dbd3503 commit 2f506b4
Show file tree
Hide file tree
Showing 3 changed files with 229 additions and 112 deletions.
174 changes: 174 additions & 0 deletions keylime-agent/src/api.rs
Original file line number Diff line number Diff line change
@@ -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<Scope, APIError> {
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<Value> = 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<Value> = 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(),
"GET, POST"
); //#[allow_ci]

let result: JsonWrapper<Value> = test::read_body_json(resp).await;

assert_eq!(result.results, json!({}));
assert_eq!(result.code, 405);
}
}
47 changes: 0 additions & 47 deletions keylime-agent/src/errors_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<APIVersion>,
Expand Down Expand Up @@ -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,
Expand Down
120 changes: 55 additions & 65 deletions keylime-agent/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#![allow(unused, missing_docs)]

mod agent_handler;
mod api;
mod common;
mod config;
mod error;
Expand Down Expand Up @@ -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;

Expand Down

0 comments on commit 2f506b4

Please sign in to comment.