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 17, 2022
1 parent ce7e43a commit 3e1fdbd
Show file tree
Hide file tree
Showing 5 changed files with 201 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
184 changes: 184 additions & 0 deletions rusk/src/lib/services/version.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// 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 is_compat = match header_v.to_str() {
Ok(req_v) => {
// check for compatibility
is_compatible(req_v).unwrap_or(false)
}
Err(_) => false,
};
if !is_compat {
return Err(Status::failed_precondition("version not supported"));
}
}
None => {
return Err(Status::unavailable("missing \"x-rusk-version\" header"))
}
}
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(req_v: &str) -> Result<bool, Box<dyn Error>> {
let req_v = get_version(req_v)?;
let rusk_v = get_version(VERSION)?;
let cmp = VersionReq::parse(
format!("{}.{}", rusk_v.major, rusk_v.minor).as_str(),
);
let valid = match cmp {
Ok(req) => req.matches(&req_v),
Err(_) => false,
};
Ok(valid)
}

/// 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 {
let nv = Version {
major: version.minor,
minor: version.patch,
patch: 0,
pre: version.pre,
build: version.build,
};
Ok(nv)
} else {
Ok(version)
}
}

#[cfg(test)]
mod tests {

use super::*;

#[test]
fn semver_major_flip() {
let eq = Version::new(4, 5, 0);
assert_eq!(get_version("0.4.5").unwrap(), eq);
let eq = Version::new(5, 0, 3);
assert_eq!(get_version("5.0.3").unwrap(), eq);
}

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

#[test]
fn compatibility_test() {
// Rusk is at 0.4.0
assert!(is_compatible("0.4.0").unwrap());
assert!(is_compatible("0.4.1").unwrap());
assert!(!is_compatible("0.4.0-rc.0").unwrap());
assert!(!is_compatible("0.3.9").unwrap());
assert!(!is_compatible("1.0.0").unwrap());
}
}

0 comments on commit 3e1fdbd

Please sign in to comment.