#[cfg(not(target_arch = "wasm32"))] #[macro_use] extern crate clap; #[cfg(not(target_arch = "wasm32"))] #[macro_use] extern crate log; #[cfg(not(target_arch = "wasm32"))] use { clap::{App, Arg}, hmac::{Hmac, Mac, NewMac}, iron::prelude::*, iron::{method::Method, typemap, AfterMiddleware, BeforeMiddleware}, mount::Mount, router::Router, rusoto_credential::AwsCredentials, rusoto_s3::util::PreSignedRequest, rusoto_s3::util::PreSignedRequestOption, rusoto_s3::GetObjectRequest, serde::Serialize, sha2::Sha256, std::str::FromStr, time::{Duration, OffsetDateTime}, }; pub mod built_info { include!(concat!(env!("OUT_DIR"), "/built.rs")); } #[cfg(not(target_arch = "wasm32"))] struct ResponseTime; #[cfg(not(target_arch = "wasm32"))] impl typemap::Key for ResponseTime { type Value = Duration; } #[cfg(not(target_arch = "wasm32"))] impl BeforeMiddleware for ResponseTime { fn before(&self, req: &mut Request) -> IronResult<()> { let duration = OffsetDateTime::now_utc() - OffsetDateTime::unix_epoch(); req.extensions.insert::(duration); Ok(()) } } #[cfg(not(target_arch = "wasm32"))] impl AfterMiddleware for ResponseTime { fn after(&self, req: &mut Request, res: Response) -> IronResult { let duration = OffsetDateTime::now_utc() - OffsetDateTime::unix_epoch(); let delta = duration - *req.extensions.get::().unwrap(); info!( "{} {} Signing query took: {:.3} ms", req.method, req.url, delta.as_seconds_f64() * 1000.0 ); Ok(res) } } #[cfg(not(target_arch = "wasm32"))] #[derive(Debug)] struct Configuration { private_key: Option, aws_access_key_id: Option, aws_secret_access_key: Option, aws_region: Option, aws_hostname: Option, } #[cfg(not(target_arch = "wasm32"))] static mut CONFIGURATION: Configuration = Configuration { private_key: None, aws_access_key_id: None, aws_secret_access_key: None, aws_region: None, aws_hostname: None, }; #[cfg(not(target_arch = "wasm32"))] fn sign(req: &mut Request) -> IronResult { if req.method == Method::Options { return Ok(build_response("")); } match ( req.headers.get_raw("x-datetime"), req.headers.get_raw("x-token"), ) { (Some(datetime_data), Some(token_data)) => { let datetime = String::from_utf8(datetime_data.first().unwrap().to_vec()).unwrap(); let token = String::from_utf8(token_data.first().unwrap().to_vec()).unwrap(); let mut to_sign = "MIO4-HMAC-SHA256\n".to_string(); to_sign.push_str(&datetime); to_sign.push_str("\n"); to_sign.push_str(&token); type HmacSha256 = Hmac; unsafe { let private_key = CONFIGURATION.private_key.clone(); let mut mac = HmacSha256::new_varkey(private_key.unwrap().as_str().as_bytes()) .expect("HMAC can take key of any size"); mac.update(to_sign.as_bytes()); let result = mac.finalize().into_bytes(); Ok(build_response(&hex::encode(result))) } } (None, Some(_)) => Ok(Response::with(( iron::status::UnprocessableEntity, "missing x-datetime header", ))), (Some(_), None) => Ok(Response::with(( iron::status::UnprocessableEntity, "missing x-token header", ))), (None, None) => Ok(Response::with(( iron::status::UnprocessableEntity, "missing x-datetime and x-token header", ))), } } #[cfg(not(target_arch = "wasm32"))] fn option_sign(_req: &mut Request) -> IronResult { Ok(build_response("")) } #[derive(Debug, Serialize)] struct PresignedUrlResponse { url: String, } #[cfg(not(target_arch = "wasm32"))] fn s3_presign_url(req: &mut Request) -> IronResult { unsafe { let map = req.get_ref::().unwrap(); let aws_bucket = if let params::Value::String(value) = map.get("bucket").unwrap() { value.to_string() } else { "".to_string() }; let key = if let params::Value::String(value) = map.get("path").unwrap() { value.to_string() } else { "".to_string() }; let aws_region_value = CONFIGURATION.aws_region.clone().unwrap(); let aws_region = if let Some(aws_hostname) = &CONFIGURATION.aws_hostname { rusoto_signature::region::Region::Custom { name: aws_region_value, endpoint: aws_hostname.to_owned(), } } else { rusoto_signature::region::Region::from_str(&aws_region_value).unwrap() }; let aws_access_key_id = CONFIGURATION.aws_access_key_id.clone(); let aws_access_key_id = aws_access_key_id .ok_or_else(|| "missing AWS Access Key ID".to_string()) .unwrap(); let aws_secret_access_key = CONFIGURATION.aws_secret_access_key.clone(); let aws_secret_access_key = aws_secret_access_key .ok_or_else(|| "missing AWS Secret Access Key".to_string()) .unwrap(); let credentials = AwsCredentials::new(aws_access_key_id, aws_secret_access_key, None, None); let get_object_request = GetObjectRequest { bucket: aws_bucket, key: key.to_owned(), ..Default::default() }; let presigned_url = get_object_request.get_presigned_url( &aws_region, &credentials, &PreSignedRequestOption::default(), ); let response = PresignedUrlResponse { url: presigned_url }; Ok(build_response(&serde_json::to_string(&response).unwrap())) } } #[cfg(not(target_arch = "wasm32"))] fn build_response(body: &str) -> Response { let mut response = Response::with((iron::status::Ok, body)); response .headers .append_raw("Access-Control-Allow-Origin", b"*".to_vec()); response .headers .append_raw("Access-Control-Allow-Methods", b"GET".to_vec()); response.headers.append_raw( "Access-Control-Allow-Headers", b"x-token,x-datetime".to_vec(), ); response } #[cfg(not(target_arch = "wasm32"))] fn main() { simple_logger::init_with_level(log::Level::Info).unwrap(); let matches = App::new("Media-IO signer") .version("1.0") .author("Marc-Antoine Arnaud ") .about("Signer server for Media-IO license") .arg( Arg::with_name("private-key") .long("private-key") .value_name("PRIVATE_KEY") .help("Sets the Private Key given by the Support platform") .required(true), ) .arg( Arg::with_name("aws-access-key-id") .long("aws-access-key-id") .value_name("AWS_ACCESS_KEY_ID") .help("Sets the AWS Access Key ID (required to enable AWS signer endpoint)"), ) .arg( Arg::with_name("aws-secret-access-key") .long("aws-secret-access-key") .value_name("AWS_SECRET_ACCESS_KEY") .help("Sets the AWS Secret Access Key (required to enable AWS signer endpoint)"), ) .arg( Arg::with_name("aws-region") .long("aws-region") .value_name("AWS_REGION") .help("Sets the AWS Region (required to enable AWS signer endpoint)"), ) .arg( Arg::with_name("aws-hostname") .long("aws-hostname") .value_name("AWS_HOSTNAME") .help("Sets the AWS Hostname (required for non-AWS S3 endpoint)"), ) .arg( Arg::with_name("port") .long("port") .value_name("PORT") .help("Sets the port number to server the signer (default: 8000)"), ) .get_matches(); let private_key = matches .value_of("private-key") .expect("missing Private Key argument"); unsafe { CONFIGURATION.private_key = Some(private_key.to_string()); } let mut router = Router::new(); router.get("/api/sign", sign, "license_signer"); router.options("/api/sign", option_sign, "option_sign"); let aws_enabled = if matches.value_of("aws-access-key-id").is_some() && matches.value_of("aws-secret-access-key").is_some() && matches.value_of("aws-region").is_some() { unsafe { CONFIGURATION.aws_access_key_id = matches.value_of("aws-access-key-id").map(|s| s.to_string()); CONFIGURATION.aws_secret_access_key = matches .value_of("aws-secret-access-key") .map(|s| s.to_string()); CONFIGURATION.aws_region = matches.value_of("aws-region").map(|s| s.to_string()); CONFIGURATION.aws_hostname = matches.value_of("aws-hostname").map(|s| s.to_string()); } router.get("/api/s3_presign_url", s3_presign_url, "s3_presign_url"); router.options("/api/s3_presign_url", option_sign, "option_s3_presign_url"); true } else { false }; let port = value_t!(matches, "port", u32).unwrap_or(8000); println!( "Start serving Media-IO signer (v{}) on 0.0.0.0:{} with private key {:?}", built_info::PKG_VERSION, port, private_key ); if aws_enabled { println!("|> AWS signer endpoint enabled"); } let mut mount = Mount::new(); mount.mount("/", router); let mut chain = Chain::new(mount); chain.link_before(ResponseTime); chain.link_after(ResponseTime); Iron::new(chain).http(&format!("0.0.0.0:{}", port)).unwrap(); } #[cfg(target_arch = "wasm32")] fn main() { println!("Signer application not available for WebAssembly target"); }