-
Notifications
You must be signed in to change notification settings - Fork 60
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
introduce rusk version middleware layer
- check version compatibility on requests - send version on all responses Resolves #717
- Loading branch information
Showing
5 changed files
with
201 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |