从零开始的 Rust 学习笔记(11) —— 让 Breezin 用上 RESTful API 和 Access Token

於是接著上一篇 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;其次,動詞 setget 都在 URL 中出現,而不是像 RESTful API 規範的那樣,體現在 HTTP Method 上。

使用 RESTful API 的話,我們的請求就是如下樣子的了~

请求数据

HTTP MethodAPI EndpointDescription
GEThttp://10.0.1.2:2275/api/v1/fansGet all fans status
GEThttp://10.0.1.2:2275/api/v1/fans/:idGet fan status of given :id
GEThttp://10.0.1.2:2275/api/v1/tempsGet all smc temperature sensors' status
PUThttp://10.0.1.2:2275/api/v1/fans/:idUpdate specified property of fan with :id

當然,更新風扇的屬性的話,實際上可寫入的就只有 3 個 —— min, manualoutput。那麼要傳值的話,肯定就是放在 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"

客戶端 lazysrc/main.rs 雖然看著略有點長,其實主要是 clap 定義的部分比較長啦,在程式碼之前先放一個圖吧~

例如要將 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:2275fan1 的信息的话,则是

lazy --api http://10.0.1.2:2275 get --name 1

客戶端 lazysrc/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, &timestamp);
    
    // 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(&timestamp).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);
            }
        }
    }
}

对应的服务端 Breezinsrc/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
    }));
}

Leave a Reply

Your email address will not be published. Required fields are marked *

four × 4 =