Rust研究：CORS

Rust によるアプリケーションサーバの CORS (Cross-Origin Resource Sharing) への対応方法を調べました。

具体的にはクロスオリジンでリソースアクセスを行う際にＷｅｂブラウザが行う "preflight" リクエストの処理方法についてのメモです。

フレームワークは tide クレートを使用しました。このあたりの関連クレートのインストール方法は省略します。

最初に申し上げておきますが、CORS の仕組みについてはこのメモでは解説しません。参考書籍を以下にあげておきます。どちらもお勧めです。

* 『体系的に学ぶ安全なＷｅｂアプリケーションの作り方』（徳丸浩著）
* 『Ｗｅｂブラウザセキュリティ』（米内貴志著）

# 想定するアプリの条件等

今回はクライアントから二つの整数値を受け取ってその足し算の結果を返す「足し算アプリ」を想定します。

以下、動作条件を記します。

## 入力

クライアントから POST メソッドで以下のような JSON 形式で二つの整数値を受け取ります。

```json
{
    "operand1": 整数値,
    "operand2": 整数値
}
```

整数値の型は i32 を想定します。

## 出力

足し算の結果は以下のような JSON 形式でクライアントに受け渡します。

```json
{
    "status": 文字列,
    "value": 整数値
}
```

足し算アプリの処理が成功した場合はstatus に "OK"、value に足し算結果の整数値を指定します。
失敗した場合は status に "NG" を指定します。

整数値の型は i32 を想定します。

## 入出力の例

クライアントから次の入力がくると

```json
{
    "operand1": 111,
    "operand2": 222
}
```

足し算アプリは次の結果を返します。

```json
{
    "status": "OK",
    "value": 333
}
```

## アドレス条件等

* アプリケーションサーバのURLは http：//api.hogege.com:8081/add とします。
* アプリケーションサーバをクロスオリジンで使用できるサイトは http：//www.mekamaru.com:8080/ とし、これ以外のオリジンからのアクセスは行わないものとします。

# 実装

```rust
// main.rs

// アプリケーションサーバのアドレス
const PORT:&str = "api.hogege.com:8081";

// クロスオリジンでのアクセスを許可するオリジン
const ALLOW_ORIGIN:&str = "http://www.mekamaru.com:8080";

#[async_std::main]
async fn main() -> tide::Result<()> {
    eprintln!("# Running on {}", PORT);

    let mut app = tide::new();
    app.at("/add")
        .options(preflight_request_handler)
        .post(add_handler);
        
    app.listen(PORT).await?;

    Ok(())
}
```

パス "/add" で OPTIONS メソッドと POST メソッドを受付けます。

POST メソッドはブラウザからのアプリへのアクセスの受け付けに使われます。
OPTIONS メソッドは、事前にブラウザがアプリに対してクロスオリジンでアクセスしてよいか確認する際に使用する preflight リクエストの受け付けに使われます。

```rust
// クロスオリジンでのアクセスを許容する条件を返す
async fn preflight_request_handler (req: tide::Request<()>) -> tide::Result {
    // Debug: メソッド・URL・バージョンの表示
    eprintln!("# {} {} {}", req.method(), req.url(), req.version().unwrap());
    // Debug: ヘッダの表示 
    req.iter().for_each(|(name, value)|eprintln!("#   {}: {}", name, value));
    
    Ok(
        tide::Response::builder(200)
            // 許容するオリジンの指定
            .header("Access-Control-Allow-Origin", ALLOW_ORIGIN)
            // 許容するメソッドの指定
            .header("Access-Control-Allow-Methods", "POST")
            // 許容するヘッダの指定
            .header("Access-Control-Allow-Headers", "Content-Type")
            .build()
    )
}
```

preflight リクエストに対して、クロスオリジンでのアクセスを許容する条件をレスポンスヘッダに指定して返答します。

クロスオリジンを許容する条件として以下を設定します。

* オリジンが "http：//www.mekamaru.com：8080" であること
* POST メソッドを使用すること
* Content-Type ヘッダを使用すること（ JSON のコンテンツタイプ "application/json" を指定するのに必要）

以下は足し算アプリの実装です。

```rust
// アプリケーションが受け付けるパラメータ
#[derive(tide::prelude::Deserialize)]
struct Parameter {
    operand1: i32,
    operand2: i32,
}

// 足し算アプリを処理する
async fn add_handler (mut req: tide::Request<()>) -> tide::Result {
    // Debug: メソッド・URL・バージョンの表示
    eprintln!("# {} {} {}", req.method(), req.url(), req.version().unwrap());
    // Debug: ヘッダの表示 
    req.iter().for_each(|(name, value)|eprintln!("#   {}: {}", name, value));

    // 足し算アプリのパラメータの取得。（JSON 形式の Unmarshal）
    let param:Parameter = req.body_json().await?;

    // オリジンのチェック結果が false のときはエラー
    if !check_origin(req) {
        // Forbidden 403 を使用したがこれが適切かは未確認
        return Err(
            tide::Error::from_str(
                tide::StatusCode::Forbidden, 
                "not allowed origin"))
    }

    // 足し算
    let result = add(param.operand1, param.operand2);
    
    // 結果を JSON 形式で返す (JSON 形式の Marshal）
    Ok(
        tide::Response::builder(200)
            .body(tide::prelude::json!({
                "status": "OK",
                "value": result
            }))
            // 許容するオリジンの指定。
            // ★クロスオリジン時にこのヘッダがないとブラウザがレスポンスを受け付けないので注意
            .header("Access-Control-Allow-Origin", ALLOW_ORIGIN)
            .content_type(tide::http::mime::JSON)
            .build()
    )
}

fn add (operand1:i32, operand2:i32) -> i32 {
    // [MEMO] 整数オーバーフロー対策は省略
    operand1 + operand2
}
```

req.body_json() で JSON 形式のデータをパースしてアプリへのパラメータを取得し、アプリの結果を json!マクロを使って返します。

エラー処理としてはオリジンのチェックを行う部分のみ実装しました。

```rust
// オリジンのチェック
fn check_origin(req:tide::Request<()>) -> bool {
    // origin ヘッダがあって、
    if let Some(origin) = req.header("origin") {
        if origin != ALLOW_ORIGIN {
            // アクセスを許容するオリジンでないとき
            return false
        }
    }

    // origin ヘッダがないか、または、
    // origin ヘッダがあってアクセスを許可するオリジンのとき
    return true
}
```

Webブラウザからクロスオリジンでアクセスするときには Origin ヘッダが付加されるのでその値でオリジンのチェックを行っています。

Origin ヘッダがなかったときにはとくにチェックは行いません。
もしもこのアプリがクロスオリジンだけで使用する条件ならば Origin ヘッダがない時には false を返すようにすればよいと思います。

最後になりますが、動作確認には Webブラウザとして Chrome を使用しました。Edge や FireFox は試していません。

# 任意のクロスオリジンを許容するには

任意のクロスオリジンからアプリを使用させたい場合には、上のコードを以下のように修正する必要があります。

* 定数 ALLOW_ORIGIN の値を "*" （ワイルドカード）に変更する。
* add_handler 内の check_origin(req) を呼び出している if 文をまるごとコメントアウトするか、関数 check_origin が常に true を返すように変更する。