Skip to content

Commit

Permalink
introduce rusk version middleware layer
Browse files Browse the repository at this point in the history
- check version compatibility on requests
- send version on all responses

Resolves #717
  • Loading branch information
t00ts committed May 16, 2022
1 parent ce7e43a commit 896a5aa
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 1 deletion.
4 changes: 3 additions & 1 deletion rusk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ tokio-util = { version = "0.7", features = ["rt"] }
async-stream = "0.3"
tracing = "0.1"
tracing-subscriber = { version = "0.3.0", features = ["fmt", "env-filter"] }
hyper = { version = "0.14", features = ["full"] }
clap = { version = "3.1", features = ["env"] }
prost = "0.9"
tower = "0.4"
futures = "0.3"
semver = "1.0"
anyhow = "1.0"
rustc_tools_util = "0.2"
rand = "0.8"
Expand Down Expand Up @@ -60,7 +63,6 @@ rusk-abi = { path = "../rusk-abi" }
rusk-schema = { path = "../rusk-schema" }

[dev-dependencies]
tower = "0.4"
test-context = "0.1"
async-trait = "0.1"
tempfile = "3.2"
Expand Down
3 changes: 3 additions & 0 deletions rusk/src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use rusk::services::network::KadcastDispatcher;
use rusk::services::network::NetworkServer;
use rusk::services::prover::{ProverServer, RuskProver};
use rusk::services::state::StateServer;
use rusk::services::version::RuskVersionLayer;
use rusk::{Result, Rusk};
use rustc_tools_util::{get_version_info, VersionInfo};
use tonic::transport::Server;
Expand Down Expand Up @@ -87,6 +88,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let prover = ProverServer::new(RuskProver::default());

Server::builder()
//.layer(RuskVersionLayer::default())
//.add_service(mvc)
.add_service(network)
.add_service(state)
.add_service(prover)
Expand Down
1 change: 1 addition & 0 deletions rusk/src/lib/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use tonic::{Request, Response, Status};
pub mod network;
pub mod prover;
pub mod state;
pub mod version;

/// A trait that defines the general workflow that the handlers for every
/// GRPC request should follow.
Expand Down
131 changes: 131 additions & 0 deletions rusk/src/lib/services/version.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) DUSK NETWORK. All rights reserved.

use std::task::{Context, Poll};

use hyper::Body;
use hyper::header::HeaderValue;
use tonic::{body::BoxBody, Status};
use tonic::service::Interceptor;
use semver::Version;
use tower::{Layer, Service};

use std::error::Error;

/// Rusk version
const VERSION: &str = env!("CARGO_PKG_VERSION");

#[derive(Debug, Clone, Default)]
pub struct RuskVersionLayer;

impl<S> Layer<S> for RuskVersionLayer {
type Service = RuskVersionMiddleware<S>;

fn layer(&self, service: S) -> Self::Service {
RuskVersionMiddleware { inner: service }
}
}

/// This middleware adds `x-rusk-version` to response headers
/// for any server response, be it the result of a sucessful
/// request or not.
#[derive(Debug, Clone)]
pub struct RuskVersionMiddleware<S> {
inner: S,
}

impl<S> Service<hyper::Request<Body>> for RuskVersionMiddleware<S>
where
S: Service<hyper::Request<Body>, Response = hyper::Response<BoxBody>> + Clone + Send + 'static,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = futures::future::BoxFuture<'static, Result<Self::Response, Self::Error>>;

fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}

fn call(&mut self, req: hyper::Request<Body>) -> Self::Future {
// This is necessary because tonic internally uses `tower::buffer::Buffer`.
// See https://github.com/tower-rs/tower/issues/547#issuecomment-767629149
// for details on why this is necessary
let clone = self.inner.clone();
let mut inner = std::mem::replace(&mut self.inner, clone);

Box::pin(async move {
let mut response = inner.call(req).await?;
let headers = response.headers_mut();
headers.append("x-rusk-version", HeaderValue::from_static(VERSION));
Ok(response)
})
}
}


/// Checks incoming requests for compatibility with running
/// Rusk version using an Interceptor.
#[derive(Clone)]
struct CompatibilityInterceptor;

impl Interceptor for CompatibilityInterceptor {
fn call(&mut self, request: tonic::Request<()>) -> Result<tonic::Request<()>, Status> {

// attempt to extract `x-rusk-version` header metadata
let metadata = request.metadata();
if let Some(header_v) = metadata.get("x-rusk-version") {
if let Ok(req_v) = header_v.to_str() {
if let Ok(res) = is_compatible(&req_v) {
match res {
true => (),
false => return Err(Status::unavailable("version not supported"))
}
}
}
}

Ok(request)
}
}



/// Returns true if `client_version` is compatible with
/// current crate version.
///
/// Compatibility is defined according to basic semver
/// rules unless major is `0`. In those cases, minor is
/// used as breaking change threshold.
///
/// If an error occurs or there's no version present
/// the function will return true regardless.
fn is_compatible(client_version: &str) -> Result<bool, Box<dyn Error>> {

let cli_v = get_version(client_version)?;
let rusk_v = get_version(VERSION)?;
let max_v = {
let mut v = rusk_v.clone();
v.major += 1;
v
};

return Ok(cli_v >= rusk_v && cli_v < max_v)

}

/// Parses a semver version string
/// When major is `0`, it'll use the minor as major to
/// ease comparison for compatibility rules.
fn get_version(v: &str) -> Result<Version, Box<dyn Error>> {
let version = Version::parse(v)?;
if version.major == 0 {
Ok(Version::new(version.minor, version.patch, 0))
} else {
Ok(version)
}
}

0 comments on commit 896a5aa

Please sign in to comment.