於是接著上一篇 Rust 學習筆記,讓上次寫的 Breezin 用上 RESTful API 和 Access Token~之前的 HTTP API 的話就是特別樸素的那種,比如
http://10.0.1.2:2275/get?name=fan1 http://10.0.1.2:2275/set?name=fan1&value=2000 http://10.0.1.2:2275/set?name=fan1&value=auto
並且上面的都是 GET 請求,好處就是在瀏覽器裡手動輸入相應的 API 和引數就能呼叫;壞處就是非常不 RESTful,表示是否成功的狀態碼只在返回的 JSON 中,而 HTTP 的狀態碼都是 HTTP 200 OK
;其次,動詞 set
與 get
都在 URL 中出現,而不是像 RESTful API 規範的那樣,體現在 HTTP Method 上。
使用 RESTful API 的話,我們的請求就是如下樣子的了~
请求数据
HTTP Method | API Endpoint | Description |
GET | http://10.0.1.2:2275/api/v1/fans | Get all fans status |
GET | http://10.0.1.2:2275/api/v1/fans/:id | Get fan status of given :id |
GET | http://10.0.1.2:2275/api/v1/temps | Get all smc temperature sensors' status |
PUT | http://10.0.1.2:2275/api/v1/fans/:id | Update specified property of fan with :id |
當然,更新風扇的屬性的話,實際上可寫入的就只有 3 個 —— min
, manual
和 output
。那麼要傳值的話,肯定就是放在 PUT
方法的 body 裡面了~
例如需要設定 fan1
的最低 RPM 為 2000 的話,那麼就使用 PUT
方法訪問的 API Endpoint 是 http://10.0.1.2:2275/api/v1/fans/1
,其 body 為
{ "property": "min", "value": 2000 }
同時,因為選擇哪一個風扇是在 URI 上確定的,因此也需要用一下正則表達式去匹配。這裡我們用到的正則表達式如下~
#[macro_use] extern crate lazy_static; use regex::Regex; // using `lazy_static` to ensure that all regex is compiled exactly once // https://docs.rs/regex/1.3.1/regex/#example-avoid-compiling-the-same-regex-in-a-loop lazy_static! { // Breezin RESTful API FAN URI Regex static ref BREEZIN_RESTFUL_FAN_URI_RE: Regex = Regex::new(r"(^/api/v1(/fans)/?(\d+)?$)").unwrap(); }
另外,要放在公網上的話,就需要加上 Access Token 了(雖然很可能也沒人發現這個是做什麼的)。既然要加 Access Token 的話,那必然也不能是那種一直固定的 Access Token。因為固定的 Access Token 的話,攻擊者一嗅探就可以隨便使用了。因此我們需要用 UNIX 時間戳來當作計算 HMAC-SHA265 時候的 Salt,以及一個 API SECRET 作為加密。
那麼我們的最終發到伺服器的 HMAC-SHA265 的計算方式如下~
use hmac::{Hmac, Mac}; use sha2::Sha256; use std::time::{SystemTime, UNIX_EPOCH}; // API Access Token (replace the string below) static APITOKEN: &str = "applesmc"; // API Access Secret (replace the string below) static APISECRET: &str = "secret"; // alias for HMAC-SHA256 type HmacSha256 = Hmac<Sha256>; /// Get UNIX timestamp in mill seconds /// /// # Examples /// /// ``` /// println!("{}", unix_timestamp_millis()); /// ``` fn unix_timestamp_millis() -> String { let start = SystemTime::now(); let since_the_epoch = start.duration_since(UNIX_EPOCH).expect("Time went backwards"); let millis_since_the_epoch = since_the_epoch.as_millis(); format!("{}", millis_since_the_epoch) } /// Generate HMAC-SHA265 of given message, secret and salt /// /// # Examples /// /// ``` /// println!("{}", generate_hmac("message", "secret", "salt")); /// ``` fn generate_hmac(message: &str, secret: &str, salt: &str) -> String { // Create HMAC-SHA256 instance which implements `Mac` trait let mut mac = HmacSha256::new_varkey(format!("{}{}", secret, salt).as_bytes()) .expect("HMAC can take key of any size"); mac.input(message.as_bytes()); // `result` has type `MacResult` which is a thin wrapper around array of // bytes for providing constant time equality check let mut stringfied_hmac = String::new(); for code in mac.result().code() { stringfied_hmac.push_str(&format!("{:x}", code)); } stringfied_hmac }
那麼根據 RESTful API 的規範,這個 Access Token 應該放在 HTTP 請求的 Header 裡,那麼在實際使用的話,HTTP 庫就選擇了 reqwest
,專案名就叫 lazy
好啦
cargo new lazy cd lazy
lazy
對應的 Cargo.toml
如下
[package] name = "lazy" version = "0.1.0" authors = ["Ryza<[data deleted]>"] edition = "2018" [dependencies] clap = "2.33.0" hmac = "0.7" reqwest = "0.9" serde = "1.0" serde_derive = "1.0" serde_json = "1.0" sha2 = "0.8"
客戶端 lazy
的 src/main.rs
雖然看著略有點長,其實主要是 clap
定義的部分比較長啦,在程式碼之前先放一個圖吧~
![](/wp-content/uploads/2019/12/breezin-restful-lazy-help.png)
例如要將 http://10.0.1.2:2275
上的 fan1
的最低 RPM 設定為 2333
的話,則可以
lazy --api http://10.0.1.2:2275 set --name 1 --property min --value 2333
要获取http://10.0.1.2:2275
上 fan1
的信息的话,则是
lazy --api http://10.0.1.2:2275 get --name 1
客戶端 lazy
的 src/main.rs
如下~ (GitHub Repo: http://github.com/lazy)
use clap::{Arg, App, SubCommand}; use hmac::{Hmac, Mac}; use reqwest::{Client, header}; use sha2::Sha256; use std::time::{SystemTime, UNIX_EPOCH}; // API Access Token (replace the string below) static APITOKEN: &str = "applesmc"; // API Access Secret (replace the string below) static APISECRET: &str = "secret"; // alias for HMAC-SHA256 type HmacSha256 = Hmac<Sha256>; /// Get UNIX timestamp in mill seconds /// /// # Examples /// /// ``` /// println!("{}", unix_timestamp_millis()); /// ``` fn unix_timestamp_millis() -> String { let start = SystemTime::now(); let since_the_epoch = start.duration_since(UNIX_EPOCH).expect("Time went backwards"); let millis_since_the_epoch = since_the_epoch.as_millis(); format!("{}", millis_since_the_epoch) } /// Generate HMAC-SHA265 of given message, secret and salt /// /// # Examples /// /// ``` /// println!("{}", generate_hmac("message", "secret", "salt")); /// ``` fn generate_hmac(message: &str, secret: &str, salt: &str) -> String { // Create HMAC-SHA256 instance which implements `Mac` trait let mut mac = HmacSha256::new_varkey(format!("{}{}", secret, salt).as_bytes()) .expect("HMAC can take key of any size"); mac.input(message.as_bytes()); // `result` has type `MacResult` which is a thin wrapper around array of // bytes for providing constant time equality check let mut stringfied_hmac = String::new(); for code in mac.result().code() { stringfied_hmac.push_str(&format!("{:x}", code)); } stringfied_hmac } fn main() { let matches = App::new("breezin") .version("1.1") .author("Ryza<[data deleted]>") .about("Remote control fan speed on a Mac which runs Linux") .arg(Arg::with_name("api") .short("a") .long("api") .value_name("API") .help("API server IP:Port") .required(true) ) .subcommand(SubCommand::with_name("get") .about("Get infomation") .version("1.1") .author("Ryza<[data deleted]>") .arg(Arg::with_name("type") .short("t") .long("type") .value_name("TYPE") .help("[fan|temp]") .required(true) ) .arg(Arg::with_name("name") .short("n") .long("name") .value_name("NAME") .help("name OR no name to get all") .required(false) ) ) .subcommand(SubCommand::with_name("set") .about("Set value of property to specified fan") .version("1.1") .author("Ryza<[data deleted]>") .arg(Arg::with_name("name") .short("n") .long("name") .value_name("NAME") .help("name") .required(true) ) .arg(Arg::with_name("property") .short("p") .long("property") .value_name("PROPERTY") .help("property") .required(true) ) .arg(Arg::with_name("value") .short("v") .long("value") .value_name("VALUE") .help("set value to property") .required(true) ) ) .get_matches(); // get UNIX timestamp let timestamp = unix_timestamp_millis(); // generate access token let access_token = generate_hmac(APITOKEN, APISECRET, ×tamp); // customize headers let mut headers = header::HeaderMap::new(); headers.insert(header::AUTHORIZATION, header::HeaderValue::from_str(&access_token).unwrap()); headers.insert("TIMESTAMP", header::HeaderValue::from_str(×tamp).unwrap()); // build custom client with access token let client = Client::builder() .default_headers(headers) .build().unwrap(); // get api server let api = matches.value_of("api").unwrap().to_string(); // build api endpoint let mut api_endpoint: String = String::new(); api_endpoint.push_str(&api); if api_endpoint.chars().last().unwrap() != '/' { api_endpoint.push_str("/"); } api_endpoint.push_str("api/v1/"); // check subcommand if let Some(matches) = matches.subcommand_matches("set") { // get name, property and value let name = matches.value_of("name").unwrap().to_string(); let property = matches.value_of("property").unwrap().to_string(); let value = matches.value_of("value").unwrap().to_string(); // given that only fans can be controlled api_endpoint.push_str("fans/"); api_endpoint.push_str(&name); // generate json body let mut json_body: serde_json::Value = serde_json::from_str("{}").unwrap(); json_body["property"] = serde_json::Value::from(property); json_body["value"] = serde_json::Value::from(value); let json = serde_json::to_string(&json_body).unwrap(); // send PUT request let response = client.put(&api_endpoint) .body(json) .send(); // print response match response { Ok(mut res) => { match res.text() { Ok(content) => { println!("{}", content); }, Err(e) => { eprintln!("[ERROR] {:?}", e); } } }, Err(e) => { eprintln!("[ERROR] {:?}", e); } } } else if let Some(matches) = matches.subcommand_matches("get") { // fan or temp let type_get = matches.value_of("type").unwrap().to_string(); if &type_get == "fan" { // we can get a single fan's status // if specified name if matches.is_present("name") { // get the specified name let name = matches.value_of("name").unwrap().to_string(); // build api endpoint for specified fan api_endpoint.push_str("fans/"); api_endpoint.push_str(&name); } else { // build api endpoint for all fans api_endpoint.push_str("fans"); } } else { // build api endpoint for all temps api_endpoint.push_str("temps"); } // send GET request let response = client.get(&api_endpoint).send(); // print response match response { Ok(mut res) => { match res.text() { Ok(content) => { println!("{}", content); }, Err(e) => { eprintln!("[ERROR] {:?}", e); } } }, Err(e) => { eprintln!("[ERROR] {:?}", e); } } } }
![](/wp-content/uploads/2019/12/breezin-restful-lazy-get-set.png)
对应的服务端 Breezin
的 src/main.rs
则是~ (GitHub Repo: http://github.com/breezin)
#![deny(warnings)] #[macro_use] extern crate lazy_static; use clap::{Arg, App}; use futures::{future, Future, Stream}; use glob::glob; use hmac::{Hmac, Mac}; use hyper::{Body, Method, Request, Response, Server, StatusCode, header}; use hyper::service::service_fn; use regex::Regex; use sha2::Sha256; use std::io; use std::io::{Read, Write}; use std::fs::File; // using `lazy_static` to ensure that all regex is compiled exactly once // https://docs.rs/regex/1.3.1/regex/#example-avoid-compiling-the-same-regex-in-a-loop lazy_static! { // Breezin RESTful API FAN URI Regex static ref BREEZIN_RESTFUL_FAN_URI_RE: Regex = Regex::new(r"(^/api/v1(/fans)/?(\d+)?$)").unwrap(); } // SMC location in Ubuntu Linux static APPLESMC: &str = "/sys/devices/platform/applesmc.768"; // API Access Token (replace the string below) static APITOKEN: &str = "applesmc"; // API Access Secret (replace the string below) static APISECRET: &str = "secret"; // alias for HMAC-SHA256 type HmacSha256 = Hmac<Sha256>; // alias for generice error of hyper type GenericError = Box<dyn std::error::Error + Send + Sync>; // alias for response type of hyper type ResponseFuture = Box<dyn Future<Item=Response<Body>, Error=GenericError> + Send>; /// Read all content from the given file. /// /// # Examples /// /// ``` /// match read_from_file("/PATH/TO/FILE") { /// Ok(content) => println!("{}", content), /// Err(e) => println!("[ERROR] {:?}", e), /// } /// ``` fn read_from_file(file: &String) -> Result<String, io::Error> { let mut s = String::new(); File::open(file)?.read_to_string(&mut s)?; Ok(s) } /// Read all content from SMC with given prefix /// /// # Examples /// /// ``` /// match read_smc("fan") { /// Ok(content) => println!("{:?}", content), /// Err(e) => println!("[ERROR] {:?}", e), /// } /// ``` /// /// ``` /// match read_smc("temp") { /// Ok(content) => println!("{:?}", content), /// Err(e) => println!("[ERROR] {:?}", e), /// } /// ``` fn read_smc(prefix: &str) -> Result<serde_json::Value, io::Error> { // init a JSON dict let mut readouts : serde_json::Value = serde_json::from_str("{}")?; // generate glob string let glob_string = &format!("{}/{}*", APPLESMC, prefix); // glob all files of given query for entry in glob(glob_string).expect("[ERROR] Failed to read SMC status") { // match each entry match entry { Ok(matched) => { // get path to the file let filepath = matched.to_str().unwrap().to_string(); // get its name let filename = matched.file_name().unwrap().to_str().unwrap().to_string(); // read its content match read_from_file(&filepath) { Ok(readout) => { // save in the JSON dict readouts[filename] = serde_json::Value::from(readout.trim().to_string()); }, Err(_) => () } }, Err(e) => { // let's handle possible error for entry here println!("[ERROR] {:?}", e); } } } // return requested readouts Ok(readouts) } /// Read all status from all SMC fans /// /// # Examples /// /// ``` /// match read_all_smc_fan() { /// Ok(readouts) => println!("{:?}", readouts), /// Err(e) => println!("[ERROR] {:?}", e), /// } /// ``` fn read_all_smc_fan() -> Result<serde_json::Value, io::Error> { read_smc("fan") } /// Read all status from requested SMC fan /// /// # Examples /// /// ``` /// match read_smc_fan("1") { /// Ok(readouts) => println!("{:?}", readouts), /// Err(e) => println!("[ERROR] {:?}", e), /// } /// ``` fn read_smc_fan(number: &str) -> Result<serde_json::Value, io::Error> { match read_smc(&format!("fan{}", number)) { Ok(readouts) => { // try to get the label property of the requested fan // if label exists, we can assume that we do have this fan if !readouts[&format!("fan{}_label", number)].is_null() { Ok(readouts) } else { // otherwise report that no such value Ok(error_with_status_and_reason(400, "no such fan exists")) } }, Err(e) => Ok(error_with_status_and_reason(404, &format!("{:?}", e))) } } /// Read safe or max rpm for smc fan /// /// # Examples /// /// ``` /// match read_smc_safe_or_max_rpm("1") { /// Ok(safe_or_max_rpm) => println!("safe_or_max_rpm: {}", safe_or_max_rpm), /// Err(e) => eprintln!("[ERROR] {:?}", e), /// } /// ``` fn read_smc_safe_or_max_rpm(fan: &str) -> Result<u32, String> { // safe rpm file name let safe_file = &format!("{}/fan{}_safe", APPLESMC, fan); // try to read safe rpm for this fan match read_from_file(safe_file) { // parse into integer Ok(safe_rpm_str) => { match safe_rpm_str.trim().to_string().parse::<u32>() { Ok(safe_rpm) => Ok(safe_rpm), Err(_) => Err("safe rpm readout is not a number".to_string()) } }, Err(_) => { // max rpm file name let max_file = &format!("{}/fan{}_max", APPLESMC, fan); // try to read max rpm for this fan match read_from_file(max_file) { Ok(max_rpm_str) => { match max_rpm_str.trim().to_string().parse::<u32>() { Ok(max_rpm) => Ok(max_rpm), Err(_) => Err("max rpm readout is not a number".to_string()) } }, Err(_) => Err(format!("cannot read neither safe nor max rpm for `{}`", fan)) } } } } /// Set property of requested `fan` /// /// # Examples /// /// ``` /// match set_smc_fan("1", "manual", "1\0") { /// Ok(_) => println!("succeeded"), /// Err(e) => eprintln!("[ERROR] {:?}", e), /// } /// ``` fn set_smc_fan(fan: &str, property: &str, content: &str) -> Result<(), io::Error> { // file name let file = &format!("{}/fan{}_{}", APPLESMC, fan, property); // write property File::create(file)?.write_all(content.as_bytes())?; Ok(()) } /// Control maunal mode status of requested `fan` /// /// # Examples /// /// ``` /// match set_smc_fan_manual("1", true) { /// Ok(_) => println!("succeeded"), /// Err(e) => eprintln!("[ERROR] {:?}", e), /// } /// ``` fn set_smc_fan_manual(fan: &str, manual: bool) -> Result<(), io::Error> { match manual { // enable manual mode true => set_smc_fan(fan, "manual", "1\n")?, // disable manual mode false => set_smc_fan(fan, "manual", "0\n")?, } Ok(()) } /// Write the requested `rpm` for `fan` /// /// # Examples /// /// ``` /// match set_smc_fan_rpm("1", 2000) { /// Ok(_) => println!("succeeded"), /// Err(e) => eprintln!("[ERROR] {:?}", e), /// } /// ``` fn set_smc_fan_rpm(fan: &str, rpm: u32) -> Result<(), io::Error> { // try to enable manual mode set_smc_fan_manual(fan, true)?; // set `output` of requested fan set_smc_fan(fan, "output", &format!("{}\n", rpm))?; Ok(()) } /// Read all measured temperature from SMC sensors /// /// # Examples /// /// ``` /// match read_all_smc_temp() { /// Ok(readouts) => println!("{:?}", readouts), /// Err(e) => println!("[ERROR] {:?}", e), /// } /// ``` fn read_all_smc_temp() -> Result<serde_json::Value, io::Error> { read_smc("temp") } /// Generate the JSON result /// /// # Examples /// /// ``` /// error_with_status_and_reason(-1, "some error message") /// ``` fn error_with_status_and_reason(status: i32, reason: &str) -> serde_json::Value { // init JSON dict for reporting error let mut error : serde_json::Value = serde_json::from_str("{}").unwrap(); // set the status error["status"] = serde_json::Value::from(status); // set the reason error["reason"] = serde_json::Value::from(reason); error } /// Stringify the JSON result and put into box /// /// # Examples /// /// ``` /// let data : serde_json::Value = serde_json::from_str("{}").unwrap(); /// stringify_result(&data) /// ``` fn stringify_result(result: &serde_json::Value) -> ResponseFuture { // stringify the JSON dict let json = serde_json::to_string_pretty(result).unwrap(); // return the result let body = Body::from(json); if let Some(http_status_code) = result["status"].as_i64() { match StatusCode::from_u16(http_status_code as u16) { Ok(http_status_code) => { return Box::new(future::ok(Response::builder() .status(http_status_code) .header(header::CONTENT_TYPE, "application/json") .body(body).unwrap() )); }, _ => { return Box::new(future::ok(Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .header(header::CONTENT_TYPE, "application/json") .body(body).unwrap() )); } } } Box::new(future::ok(Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .header(header::CONTENT_TYPE, "application/json") .body(body).unwrap() )) } /// Generate HMAC-SHA265 of given message, secret and salt /// /// # Examples /// /// ``` /// println!("{}", generate_hmac("message", "secret", "salt")); /// ``` fn generate_hmac(message: &str, secret: &str, salt: &str) -> String { // Create HMAC-SHA256 instance which implements `Mac` trait let mut mac = HmacSha256::new_varkey(format!("{}{}", secret, salt).as_bytes()) .expect("HMAC can take key of any size"); mac.input(message.as_bytes()); // `result` has type `MacResult` which is a thin wrapper around array of // bytes for providing constant time equality check let mut stringfied_hmac = String::new(); for code in mac.result().code() { stringfied_hmac.push_str(&format!("{:x}", code)); } stringfied_hmac } /// Handle HTTP GET request fn api_get_response(req: Request<Body>) -> Result<serde_json::Value, io::Error> { // match GET URI path match req.uri().path() { "/api/v1/temps" => { // GET - http://10.0.1.2:2275/api/v1/temps // get all measured temperature read_all_smc_temp() }, _ => { if let Some(matched) = BREEZIN_RESTFUL_FAN_URI_RE.captures(req.uri().path()) { match (matched.get(2), matched.get(3)) { (None, _) => { // no such API Ok(error_with_status_and_reason(-13, "no such API exists")) }, (Some(_), None) => { // GET - http://10.0.1.2:2275/api/v1/fans // get all fans' status read_all_smc_fan() } (Some(_), Some(query)) => { // GET - http://10.0.1.2:2275/api/v1/fans/:id // get requested fan status read_smc_fan(query.as_str()) } } } else { Ok(error_with_status_and_reason(-12, "error occured while parsing URI")) } } } } /// Generate HTTP 200 OK response /// /// # Examples /// /// ``` /// println!("{:?}", response_with_success()); /// ``` fn response_with_success() -> Response<Body> { Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "application/json") .body(Body::from("{\n \"status\": 0\n}")).unwrap() } /// Generate failed response with HTTP status code and reason /// /// # Examples /// /// ``` /// println!("{:?}", response_with_error(400, StatusCode::BAD_REQUEST, "parameter missing")); /// ``` fn response_with_error(error: i32, http_status_code: StatusCode, reason: &str) -> Response<Body> { let result = error_with_status_and_reason(error, reason); Response::builder() .status(http_status_code) .header(header::CONTENT_TYPE, "application/json") .body(Body::from(serde_json::to_string_pretty(&result).unwrap())).unwrap() } /// Handle HTTP PUT request fn api_put_response(req: Request<Body>, uri: String) -> ResponseFuture { Box::new(req.into_body().concat2().from_err() .and_then(move |entire_body| { if let Some(matched) = BREEZIN_RESTFUL_FAN_URI_RE.captures(&uri) { match (matched.get(2), matched.get(3)) { (None, _) => { // no such API exists Ok(response_with_error(404, StatusCode::NOT_FOUND, "no such API exists")) }, (Some(_), None) => { // PUT - https://api.macmini.ayauchida.best/fans // specific fan required Ok(response_with_error(400, StatusCode::BAD_REQUEST, "specific fan required")) }, (Some(_), Some(query)) => { // PUT - https://api.macmini.ayauchida.best/fans/:id // updating requested fan let fan = query.as_str(); // convert from bytes to string let body = String::from_utf8(entire_body.to_vec()).unwrap(); // try to parse json string let json: std::result::Result<serde_json::Value, serde_json::error::Error> = serde_json::from_str(&body); match json { // if succeeded Ok(request) => { // does `property` exists in json data let property = &request["property"]; if property.is_null() { // if not defined return Ok(response_with_error(406, StatusCode::NOT_ACCEPTABLE, "`property` is null")); } // check whether requested property supported or not let property = property.as_str().unwrap(); match property { "min" | "output" => (), _ => { // if property is not supported return Ok(response_with_error(400, StatusCode::BAD_REQUEST, &format!("property `{}` is not supported for updating or not exists", property))); } }; // does `value` exists in json data let value = &request["value"]; if value.is_null() { // if not defined return Ok(response_with_error(406, StatusCode::NOT_ACCEPTABLE, "`value` is null")); } // cannot set `auto` for `min` let value = value.as_str().unwrap(); match (property, value) { ("min", "auto") => return Ok(response_with_error(406, StatusCode::NOT_ACCEPTABLE, "cannot set `auto` for `min`")), ("output", "auto") => { match set_smc_fan_manual(fan, false) { Ok(_) => { return Ok(response_with_success()); }, Err(e) => { return Ok(response_with_error(500, StatusCode::INTERNAL_SERVER_ERROR, &format!("failed while set `auto` for fan{}: {}", fan, e))); } } }, (_, _) => { // read its safe or max rpm let upperlimit = match read_smc_safe_or_max_rpm(fan) { // if we have either safe or max rpm Ok(safe_or_max_rpm) => safe_or_max_rpm, // otherwise Err(_) => { return Ok(response_with_error(404, StatusCode::NOT_ACCEPTABLE, "cannot read either safe or max limitation, or the fan requested does not exist")); } }; // parse value into integer let requested_rpm = match value.parse::<u32>() { // if succeeded Ok(rpm) => rpm, // otherwise Err(e) => { return Ok(response_with_error(400, StatusCode::BAD_REQUEST, &format!("{}", e))); } }; // check whether the requested rpm is in range or not if requested_rpm > upperlimit { return Ok(response_with_error(400, StatusCode::BAD_REQUEST, &format!("max rpm is `{}`, however the requested rpm is `{}`", upperlimit, requested_rpm))); } if property == "output" { // enable manual mode if the property is output // and then set output rpm match set_smc_fan_rpm(fan, requested_rpm) { Ok(_) => { return Ok(response_with_success()); }, Err(e) => { return Ok(response_with_error(500, StatusCode::INTERNAL_SERVER_ERROR, &format!("failed while set rpm to `{}` for fan{}_output: {}", requested_rpm, fan, e))); } } } else { // try to set requested property for fan match set_smc_fan(fan, property, &format!("{}\n", requested_rpm)) { Ok(_) => { return Ok(response_with_success()); }, Err(e) => { return Ok(response_with_error(500, StatusCode::INTERNAL_SERVER_ERROR, &format!("failed while set rpm to `{}` for fan{}_{}: {}", requested_rpm, fan, property, e))); } } // match set_smc_fan(fan, property, &format!("{}\n", requested_rpm)) } // property == "output" } // (_, _) } // match (property, value) }, // Ok(request) // parse failed Err(e) => { Ok(response_with_error(400, StatusCode::BAD_REQUEST, &format!("illformatted data: {}", e))) } // Err(e) } // match json } // (Some(_), Some(query)) } // match (matched.get(2), matched.get(3)) } else { Ok(response_with_error(500, StatusCode::INTERNAL_SERVER_ERROR, "error occured while parsing URI")) } }) // and_then ) // Box::new } /// Verify RESTful API Authentication /// /// # Examples /// /// ``` /// println!("{}", generate_hmac("message", "secret", "salt")); /// ``` fn verify_authentication(req: &Request<Body>) -> (bool, std::option::Option<serde_json::Value>) { // try to get `authorization` header if let Some(authorization) = req.headers().get("AUTHORIZATION") { // try to get `timestamp` header if let Some(timestamp) = req.headers().get("TIMESTAMP") { // calculate corresponding hmac let hmac = generate_hmac(APITOKEN, APISECRET, timestamp.to_str().unwrap()); // verify hmac if hmac == authorization.to_str().unwrap() { (true, None) } else { (false, Some(error_with_status_and_reason(401, "invalid authorization"))) } } else { (false, Some(error_with_status_and_reason(401, "TIMESTAMP header required"))) } } else { (false, Some(error_with_status_and_reason(401, "AUTHORIZATION header required"))) } } /// breezin service fn breezin(req: Request<Body>) -> ResponseFuture { // Check API Access Token match verify_authentication(&req) { // verified successfully (true, _) => { let result = match req.method() { // handle GET request &Method::GET => api_get_response(req), &Method::PUT => { // copy URI path let path = String::from(req.uri().path()); // handle PUT request return api_put_response(req, path); }, // other HTTP methods _ => Ok(error_with_status_and_reason(405, "method not allowed")) }; match result { // stringify the JSON dict and return the result Ok(json) => stringify_result(&json), Err(e) => { // return error let error = error_with_status_and_reason(500, &format!("{:?}", e)); stringify_result(&error) } } }, // verification failed (false, Some(error)) => stringify_result(&error), // technically impossible unless been hacked (false, None) => stringify_result(&error_with_status_and_reason(500, "internal server error")) } } /// Parse command line arguments fn parseargs() -> (String, String) { // https://github.com/clap-rs/clap#quick-example let matches = App::new("breezin") .version("1.0") .author("Ryza<[data deleted]>") .about("Remote control fan speed on a Mac which runs Linux") .arg(Arg::with_name("bind") .short("b") .long("bind") .value_name("IP") .help("binding IP address") .required(true) ) .arg(Arg::with_name("port") .short("p") .long("port") .value_name("PORT") .help("listen at port") .required(true) ) .get_matches(); // directly use `unwrap()` because they were set to be required let bind = matches.value_of("bind").unwrap().to_string(); let port = matches.value_of("port").unwrap().to_string(); (bind, port) } fn main() { // parse command line args let (bind, port) = parseargs(); // try to parse binding ip and port let addr = format!("{}:{}", bind, port).parse().unwrap(); hyper::rt::run(future::lazy(move || { // bind IP address and serve breezin service! let server = Server::bind(&addr) .serve(move || { service_fn(move |req| { // create breezin service breezin(req) } )}) .map_err(|e| eprintln!("[ERROR] server error: {}", e)); println!("[INFO] Breezin' on http://{}", addr); server })); }