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 18, 2022
1 parent 4c791e1 commit e228535
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 6 deletions.
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
13 changes: 10 additions & 3 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::{CompatibilityInterceptor, RuskVersionLayer};
use rusk::{Result, Rusk};
use rustc_tools_util::{get_version_info, VersionInfo};
use tonic::transport::Server;
Expand Down Expand Up @@ -82,11 +83,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
config.kadcast_test,
);

let network = NetworkServer::new(kadcast);
let state = StateServer::new(rusk);
let prover = ProverServer::new(RuskProver::default());
let network =
NetworkServer::with_interceptor(kadcast, CompatibilityInterceptor);
let state =
StateServer::with_interceptor(rusk, CompatibilityInterceptor);
let prover = ProverServer::with_interceptor(
RuskProver::default(),
CompatibilityInterceptor,
);

Server::builder()
.layer(RuskVersionLayer::default())
.add_service(network)
.add_service(state)
.add_service(prover)
Expand Down
5 changes: 3 additions & 2 deletions rusk/src/bin/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use super::unix;

use futures::TryFutureExt;
use rusk::services::version::RuskVersionLayer;
use std::path::Path;
use tokio::net::UnixListener;

Expand All @@ -20,7 +21,7 @@ type TonicError = Box<dyn std::error::Error + Send + Sync>;

#[cfg(not(target_os = "windows"))]
pub(crate) async fn startup_with_uds<S, A>(
router: Router<S, A>,
router: Router<S, A, RuskVersionLayer>,
socket: &str,
) -> Result<(), Box<dyn std::error::Error>>
where
Expand Down Expand Up @@ -52,7 +53,7 @@ where
}

pub(crate) async fn startup_with_tcp_ip<S, A>(
router: Router<S, A>,
router: Router<S, A, RuskVersionLayer>,
host: &str,
port: &str,
) -> Result<(), Box<dyn std::error::Error>>
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
158 changes: 158 additions & 0 deletions rusk/src/lib/services/version.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// 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::header::HeaderValue;
use hyper::Body;
use semver::{Version, VersionReq};
use tonic::service::Interceptor;
use tonic::{body::BoxBody, Status};
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)]
pub 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();
match metadata.get("x-rusk-version") {
Some(header_v) => {
// extract a string value
let mut client_version = "unknown";
let is_compat = match header_v.to_str() {
Ok(req_v) => {
// check for compatibility
client_version = req_v;
is_compatible(req_v, VERSION)
}
Err(_) => false,
};
if !is_compat {
return Err(Status::failed_precondition(
format!("Requested rusk version is not supported. Expected {} but got {}!", VERSION, client_version),
));
}
}
None => {
return Err(Status::unavailable(
"Missing \"x-rusk-version\" header, please update client!",
))
}
}
Ok(request)
}
}

/// Returns true if `client_version` is compatible with
/// current crate version.
///
/// Compatibility is defined according to basic semver
/// rules. If an error occurs or there's no version present
/// the function will return false.
fn is_compatible(req_v: &str, rusk_v: &'static str) -> bool {
let req_v = Version::parse(req_v);
let req = VersionReq::parse(rusk_v);
match (req, req_v) {
(Ok(req), Ok(req_v)) => req.matches(&req_v),
_ => false,
}
}

#[cfg(test)]
mod tests {

use super::*;

#[test]
fn semver_test() {
let vr = VersionReq::parse("0.4").unwrap();
let v = Version::parse("0.5.0").unwrap();
assert!(!vr.matches(&v));

let vr = VersionReq::parse("0.4.0-rc.1").unwrap();
let v = Version::parse("0.4.1").unwrap();
assert!(vr.matches(&v));
}

#[test]
fn compatibility_test() {
assert!(is_compatible("0.5.0-rc.0", "0.5.0-rc.0"));
assert!(is_compatible("0.4.0", "0.4.0"));
assert!(is_compatible("4.2.0", "4.1.0"));
assert!(!is_compatible("0.4.0", "0.5.1"));
assert!(!is_compatible("0.4.0", "4.0.0"));
assert!(!is_compatible("4.0.0", "0.5.0"));
}
}

0 comments on commit e228535

Please sign in to comment.