use actix_web::{error::BlockingError, web, Error, HttpRequest, HttpResponse};
use futures::Future;
#[cfg(feature = "authorization")]
use crate::admin::rest_api::CIRCUIT_READ_PERMISSION;
use crate::admin::store::AdminServiceStore;
use crate::rest_api::{
actix_web_1::{Method, ProtocolVersionRangeGuard, Resource},
ErrorResponse, SPLINTER_PROTOCOL_VERSION,
};
use super::super::error::CircuitFetchError;
use super::super::resources;
const ADMIN_FETCH_CIRCUIT_MIN: u32 = 1;
pub fn make_fetch_circuit_resource(store: Box<dyn AdminServiceStore>) -> Resource {
let resource = Resource::build("/admin/circuits/{circuit_id}").add_request_guard(
ProtocolVersionRangeGuard::new(ADMIN_FETCH_CIRCUIT_MIN, SPLINTER_PROTOCOL_VERSION),
);
#[cfg(feature = "authorization")]
{
resource.add_method(Method::Get, CIRCUIT_READ_PERMISSION, move |r, _| {
fetch_circuit(r, web::Data::new(store.clone()))
})
}
#[cfg(not(feature = "authorization"))]
{
resource.add_method(Method::Get, move |r, _| {
fetch_circuit(r, web::Data::new(store.clone()))
})
}
}
fn fetch_circuit(
request: HttpRequest,
store: web::Data<Box<dyn AdminServiceStore>>,
) -> Box<dyn Future<Item = HttpResponse, Error = Error>> {
let circuit_id = request
.match_info()
.get("circuit_id")
.unwrap_or("")
.to_string();
let protocol_version = match request.headers().get("SplinterProtocolVersion") {
Some(header_value) => match header_value.to_str() {
Ok(protocol_version) => Ok(protocol_version.to_string()),
Err(_) => Err(CircuitFetchError::BadRequest(
"Unable to get SplinterProtocolVersion".to_string(),
)),
},
None => Ok(format!("{}", SPLINTER_PROTOCOL_VERSION)),
};
Box::new(
web::block(move || {
let circuit = store
.get_circuit(&circuit_id)
.map_err(|err| CircuitFetchError::CircuitStoreError(err.to_string()))?
.ok_or_else(|| {
CircuitFetchError::NotFound(format!("Unable to find circuit: {}", circuit_id))
})?;
Ok((circuit, protocol_version?))
})
.then(|res| match res {
Ok((circuit, protocol_version)) => match protocol_version.as_str() {
"1" => Ok(HttpResponse::Ok().json(
resources::v1::circuits_circuit_id::CircuitResponse::from(&circuit),
)),
"2" => Ok(HttpResponse::Ok().json(
resources::v2::circuits_circuit_id::CircuitResponse::from(&circuit),
)),
_ => Ok(
HttpResponse::BadRequest().json(ErrorResponse::bad_request(&format!(
"Unsupported SplinterProtocolVersion: {}",
protocol_version
))),
),
},
Err(err) => match err {
BlockingError::Error(err) => match err {
CircuitFetchError::CircuitStoreError(err) => {
error!("{}", err);
Ok(HttpResponse::InternalServerError()
.json(ErrorResponse::internal_error()))
}
CircuitFetchError::NotFound(err) => {
Ok(HttpResponse::NotFound().json(ErrorResponse::not_found(&err)))
}
CircuitFetchError::BadRequest(err) => {
Ok(HttpResponse::BadRequest().json(ErrorResponse::bad_request(&err)))
}
},
_ => {
error!("{}", err);
Ok(HttpResponse::InternalServerError().json(ErrorResponse::internal_error()))
}
},
}),
)
}
#[cfg(test)]
mod tests {
use super::*;
use diesel::{
r2d2::{ConnectionManager as DieselConnectionManager, Pool},
sqlite::SqliteConnection,
};
use reqwest::{blocking::Client, StatusCode, Url};
use serde_json::{to_value, Value as JsonValue};
use crate::admin::store::diesel::DieselAdminServiceStore;
use crate::admin::store::{
AuthorizationType, Circuit, CircuitBuilder, CircuitNode, CircuitNodeBuilder,
DurabilityType, PersistenceType, RouteType, ServiceBuilder,
};
use crate::migrations::run_sqlite_migrations;
use crate::rest_api::actix_web_1::{RestApiBuilder, RestApiShutdownHandle};
#[test]
fn test_fetch_circuit_ok() {
let (shutdown_handle, join_handle, bind_url) =
run_rest_api_on_open_port(vec![make_fetch_circuit_resource(filled_splinter_state())]);
let url = Url::parse(&format!(
"http://{}/admin/circuits/{}",
bind_url,
get_circuit_1().0.circuit_id()
))
.expect("Failed to parse URL");
let req = Client::new()
.get(url)
.header("SplinterProtocolVersion", SPLINTER_PROTOCOL_VERSION);
let resp = req.send().expect("Failed to perform request");
assert_eq!(resp.status(), StatusCode::OK);
let circuit: JsonValue = resp.json().expect("Failed to deserialize body");
assert_eq!(
circuit,
to_value(resources::v2::circuits_circuit_id::CircuitResponse::from(
&get_circuit_1().0
))
.expect("failed to convert expected circuit"),
);
shutdown_handle
.shutdown()
.expect("unable to shutdown rest api");
join_handle.join().expect("Unable to join rest api thread");
}
#[test]
fn test_fetch_circuit_ok_v1() {
let (shutdown_handle, join_handle, bind_url) =
run_rest_api_on_open_port(vec![make_fetch_circuit_resource(filled_splinter_state())]);
let url = Url::parse(&format!(
"http://{}/admin/circuits/{}",
bind_url,
get_circuit_1().0.circuit_id()
))
.expect("Failed to parse URL");
let req = Client::new()
.get(url)
.header("SplinterProtocolVersion", "1");
let resp = req.send().expect("Failed to perform request");
assert_eq!(resp.status(), StatusCode::OK);
let circuit: JsonValue = resp.json().expect("Failed to deserialize body");
assert_eq!(
circuit,
to_value(resources::v1::circuits_circuit_id::CircuitResponse::from(
&get_circuit_1().0
))
.expect("failed to convert expected circuit"),
);
shutdown_handle
.shutdown()
.expect("unable to shutdown rest api");
join_handle.join().expect("Unable to join rest api thread");
}
#[test]
fn test_fetch_circuit_not_found() {
let (shutdown_handle, join_handle, bind_url) =
run_rest_api_on_open_port(vec![make_fetch_circuit_resource(filled_splinter_state())]);
let url = Url::parse(&format!(
"http://{}/admin/circuits/Circuit-not-valid",
bind_url,
))
.expect("Failed to parse URL");
let req = Client::new()
.get(url)
.header("SplinterProtocolVersion", SPLINTER_PROTOCOL_VERSION);
let resp = req.send().expect("Failed to perform request");
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
shutdown_handle
.shutdown()
.expect("unable to shutdown rest api");
join_handle.join().expect("Unable to join rest api thread");
}
fn get_circuit_1() -> (Circuit, Vec<CircuitNode>) {
let service = ServiceBuilder::new()
.with_service_id("aaaa")
.with_service_type("type_a")
.with_node_id("node_1")
.build()
.expect("Unable to build service");
let nodes = vec![
CircuitNodeBuilder::new()
.with_node_id("node_1")
.with_endpoints(&["tcp://localhost:8000".to_string()])
.build()
.expect("Unable to build node"),
CircuitNodeBuilder::new()
.with_node_id("node_2")
.with_endpoints(&["tcp://localhost:8001".to_string()])
.build()
.expect("Unable to build node"),
];
(
CircuitBuilder::new()
.with_circuit_id("abcde-12345".into())
.with_authorization_type(&AuthorizationType::Trust)
.with_members(&nodes)
.with_roster(&[service])
.with_persistence(&PersistenceType::Any)
.with_durability(&DurabilityType::NoDurability)
.with_routes(&RouteType::Any)
.with_circuit_management_type("circuit_1_type")
.with_display_name("test_display")
.build()
.expect("Should have built a correct circuit"),
nodes,
)
}
fn get_circuit_2() -> (Circuit, Vec<CircuitNode>) {
let service = ServiceBuilder::new()
.with_service_id("bbbb")
.with_service_type("other_type")
.with_node_id("node_3")
.build()
.expect("unable to build service");
let nodes = vec![
CircuitNodeBuilder::new()
.with_node_id("node_3")
.with_endpoints(&["tcp://localhost:8000".to_string()])
.build()
.expect("Unable to build node"),
CircuitNodeBuilder::new()
.with_node_id("node_4")
.with_endpoints(&["tcp://localhost:8001".to_string()])
.build()
.expect("Unable to build node"),
];
(
CircuitBuilder::new()
.with_circuit_id("efghi-56789")
.with_authorization_type(&AuthorizationType::Trust)
.with_members(&nodes)
.with_roster(&[service])
.with_persistence(&PersistenceType::Any)
.with_durability(&DurabilityType::NoDurability)
.with_routes(&RouteType::Any)
.with_circuit_management_type("circuit_2_type")
.build()
.expect("Should have built a correct circuit"),
nodes,
)
}
fn setup_admin_service_store() -> Box<dyn AdminServiceStore> {
let connection_manager = DieselConnectionManager::<SqliteConnection>::new(":memory:");
let pool = Pool::builder()
.max_size(1)
.build(connection_manager)
.expect("Failed to build connection pool");
run_sqlite_migrations(&*pool.get().expect("Failed to get connection for migrations"))
.expect("Failed to run migrations");
Box::new(DieselAdminServiceStore::new(pool))
}
fn filled_splinter_state() -> Box<dyn AdminServiceStore> {
let admin_store = setup_admin_service_store();
let (circuit, nodes) = get_circuit_1();
admin_store
.add_circuit(circuit, nodes)
.expect("Unable to add circuit_1");
let (circuit, nodes) = get_circuit_2();
admin_store
.add_circuit(circuit, nodes)
.expect("Unable to add circuit_2");
admin_store
}
fn run_rest_api_on_open_port(
resources: Vec<Resource>,
) -> (RestApiShutdownHandle, std::thread::JoinHandle<()>, String) {
#[cfg(not(feature = "https-bind"))]
let bind = "127.0.0.1:0";
#[cfg(feature = "https-bind")]
let bind = crate::rest_api::BindConfig::Http("127.0.0.1:0".into());
let result = RestApiBuilder::new()
.with_bind(bind)
.add_resources(resources.clone())
.build_insecure()
.expect("Failed to build REST API")
.run_insecure();
match result {
Ok((shutdown_handle, join_handle)) => {
let port = shutdown_handle.port_numbers()[0];
(shutdown_handle, join_handle, format!("127.0.0.1:{}", port))
}
Err(err) => panic!("Failed to run REST API: {}", err),
}
}
}