Try   HackMD
tags: Rust Rocket Request

Rocket Framework 入門 1

Rocketとは

  • Rustで書かれたweb framework
  • 書きやすい
  • 速い
  • 安全
  • 柔軟性や使いやすさ、型安全を犠牲にしない

Get Started

Nightly Rustでしか動作しない
rustup default nightly

開発用ツールもインストールしとく
rustup component add rls rust-src rust-analysis

Cargo.toml にrocketを書く

[dependencies]
rocket = "0.4.2"

Hello, world

#![feature(proc_macro_hygiene, decl_macro)] #[macro_use] extern crate rocket; #[get("/")] fn index() -> &'static str { "Hello, world!" } fn main() { rocket::ignite().mount("/", routes![index]).launch(); }

解説

  • #![feature()] : nightlyの機能を有効にするのに使う
  • #[macro_use] : extern crate の上で使うと、そのクレートのマクロをロードする
  • #[get()] : routeアトリビュート。ハンドラのルートを決める
  • rocket::
    • ignite() : Rocketオブジェクトを作成して返す
    • mount("namespace", [handlers]) : リクエストハンドラーをマウントする。
    • launch() : サーバーのlistenを開始

ロケットに積荷を搭載して発射するイメージ

概要

ライフサイクル

httpリクエストに対する処理の流れ

  1. ルーティング : urlに対するリクエストをmountされたハンドラに渡す
  2. validation : リクエストのタイプがあっているか検証。あっていなかったら、次のルートに行くかエラーハンドラーを呼ぶ
  3. processing : ハンドラに定義したビジネス路切っくを実行
  4. Response : 適切なhttpレスポンスを返す。

Namespacing

mod 機能を使って、ハンドラーをモジュール化するときは
module名まで含めないとエラーになる。
route!マクロがコード生成できないため

mod other { #[get("/world")] pub fn world() -> &'static str { "Hello, world!" } } #[get("/hello")] pub fn hello() -> &'static str { "Hello, outside world!" } use other::world; fn main() { // error[E0425]: cannot find value `static_rocket_route_info_for_world` in this scope rocket::ignite().mount("/hello", routes![hello, world]); } // こっちで書く rocket::ignite().mount("/hello", routes![hello, other::world]);

Requests

全部訳すとだるいので要点だけ

route attr

  • #[get()]などのrouteアトリビュートでパスを記述
    • get
    • put
    • post
    • delete
    • head
    • patch
    • option

Dynamic Path

  • Dynamic Path は、パスの一部が任意の引数になる
    • "/hello/<name>"みたいに書く
    • routeアトリビュートに書いたあと、fn の引数に&RawStr型として書く
#[get("/hello/<name>")] fn hello(name: &RawStr) -> String { format!("Hello, {}!", name.as_str()) }
  • 何個でも書ける
  • pathのパラメータとして取れる型は、rocket::request::FromParamトレイトによれば
    • i/u 8/15/32/64/128
    • bool
    • IpAddr, Ipv4Addr, Ipv6Addr
    • SocketAddr, SocketAddrV4, SocketAddrV6
    • &RawStr : デコードされていない生の文字列
    • String : デコードされた文字列
    • Cow
    • Option<T> where T:FromParam
    • Result<T, T::Error> where T: FromParam
  • 標準ライブラリにあるデータ型はだいたいパスパラメータとして取れる
  • Rocketだけの特殊な型もある
  • ハンドラの引数に型を書くことで、自動的にParseとValidateを行う
#[get("/hello/<name>/<age>/<cool>")] fn hello(name: String, age: u8, cool: bool) -> String { ... }
  • 合わなかったら、次の優先度のハンドラへ
  • ハンドラの優先度は、mounte()のroutes!マクロの左順

Fowarding

  • パスのパラメータで取った値が合わなかったら、次にマッチしたrouteにリクエストを回す
  • routeがなくなるまで続ける
  • なくなったら、404errorを返す

routeのランク

同じようなrouteがあった場合、ランクが高い方を優先してアクセスする。
routeアトリビュートに、rank = で明示できる

#[get("/user/<id>")] fn user(id: usize) -> T { ... } #[get("/user/<id>", rank = 2)] fn user_int(id: isize) -> T { ... } #[get("/user/<id>", rank = 3)] fn user_str(id: &RawStr) -> T { ... } fn main() { rocket::ignite() .mount("/", routes![user, user_int, user_str]) .launch(); }

デフォルトランキング

-6 ~ -1まで
変数を含んでいなかったり、? から始まるクエリパラメータを
含んでいると高くなるというルール。

static file

静的ファイルのホスティングは、以下のコードでできる。

rocket.mount("/public", StaticFiles::from("/static"))

/static/ フォルダにあるファイルを /public/path
でアクセスできるようにする。

Option パラメータ

あってもなくてもいいパスのときは、ハンドラで
Option型で受け取る。

#[get("/hello?wave&<name>")] fn hello(name: Option<&RawStr>) -> String { name.map(|name| format!("Hi, {}!", name)) .unwrap_or_else(|| "Hello!".into()) }

複数セグメント

?..&&のような、複数クエリを取って
#[derive(FromForm)] を実装した構造体に割り当てられる

use rocket::request::Form; #[derive(FromForm)] struct User { name: String, account: usize, } #[get("/item?<id>&<user..>")] fn item(id: usize, user: Form<User>) { /* ... */ }

url例
/item?id=100&name=sandal&account=400

リクエストガード

リクエストに含まれる情報に基づいて、誤ってハンドラを
呼び出さないようにする。

もっと詳しく言えば、リクエストガードは任意の検証ポリシーである
FromRequestトレイトとして実装する。

リクエストガードはハンドラへの入力として扱われる。
Rocketはハンドラを呼び出す前に、リクエストガードの実装を自動的に呼び出す。
すべてのガードを通過したとき、ハンドラにリクエストをディスパッチする。

下記のハンドラでは、A, B, Cの3つのリクエストガードを使う(そんな名前のないけど)

[get("/ <param>"] fn index (param:isize 、a:A 、b:B 、c:C-> ... { ... }

いくつかのFromRequest実装は、Rocketに組み込みで用意されている

  • Method : 現在のリクエストからhttpメソッドを抽出
  • &Origin : リクエストの原点のURI
  • &Route
  • Cookies
  • ContentType : ContentTypeを抽出する。指定されないときはリクエストが次のrouteに行く
  • SocketAddr : SocketAddrを抽出する。指定されないときはリクエストが次のrouteに行く
  • Option
  • Result

リクエストガードの使いみち

主に、httpリクエストのヘッダに含まれる情報を抽出するのに使える

  • ヘッダに認証コードを含んで、API認証
  • APIkey

カスタムリクエストガード

実装方法はrocketのapiドキュメントの rocket::request::FromRequest トレイト
の実装例を見る

簡単な認証システムの実装

User と AdminUser の2つのリクエストガードを使って、
管理ユーザーしか見られないページを実装する
(User と AdminUser はカスタムリクエストガード)

#[get("/admin")] fn admin_panel(admin: AdminUser) -> &'static str { "Hello, administrator. This is the admin panel!" } #[get("/admin", rank = 2)] fn admin_panel_user(user: User) -> &'static str { "Sorry, you must be an administrator to access this page." } #[get("/admin", rank = 3)] fn admin_panel_redirect() -> Redirect { Redirect::to("/login") }

Cookies

use rocket::http::Cookies; #[get("/")] fn index(cookies: Cookies) -> Option<String> { cookies.get("message") .map(|value| format!("Message: {}", value)) }

Format

routeアトリビュートは、リクエストが送ってくるデータフォーマットを定義できる
payload を設定できる put, post, delete, patchメソッドでの
Jsonリクエストの場合は format="application/json" となる

#[post("/user", format = "application/json", data = "<user>")] fn new_user(user: Json<User>) -> T { ... }

dataは <user> パラメータで受け取っているので、引数では user: Json<T> 型で
受け取れる

また、 "application/json" の部分は、 "json" と省略もできる
利用できる略称のリストは、 (rocket::http::ConentType::parse_flexible())[https://api.rocket.rs/v0.4/rocket/http/struct.ContentType.html#method.parse_flexible]
の説明を見る

payloadがないタイプのメソッドでも、HttpレスポンスのAcceptの値がjsonに
なってるかどうかをチェックする。
ハンドラの戻り値がjsonである必要があるならformatを記述するべき

#[get("/user/<id>", format = "json")] fn user(id: usize) -> Json<User> { ... }

Bodyデータのの受け取り

routeアトリビュートで data="<変数名>" にして ハンドラの同名引数で受け取る
FromDataトレイトを実装する型で受け取れる

#[post("/", data = "<input>")] fn new(input: T) -> String { ... }

Forms

httpのformで入力したキーバリューを含んだpostの場合、
#[derive(FromForm)]を実装した構造体に、formの値を割り当てられる

#[derive(FromForm)] struct Task { complete: bool, description: String, } #[post("/todo", data = "<task>")] fn new(task: Form<Task>) -> String { ... }

Form<T> のTに、構造体の型を入れる
formの値は、Task構造体にパースされ
ハンドラの中で使えるようになる。
パースできなかったり不正な値のときは
400 - Bad Request or 422 - Unprocessable Entity

Option型やResult型でも受け取れる

#[post("/todo", data = "<task>")] fn new(task: Option<Form<Task>>) -> String { ... }

Lenient Parsing

lenient = ゆるい、寛大な
Form<T>型で受け取ると、formのキーと値のペアが割り当てる構造体に
過不足ない状態でないとエラーになる
LenientForm<T>で受け取ると、フィールドが多すぎる場合は問題なく
必要な部分だけパースするといったことを行ってくれる
例.

  • 構造体のフィールド : a, c
  • フォームの入力 : a, b, c
    こういう場合はok
#[derive(FromForm)] struct Task { .. } #[post("/todo", data = "<task>")] fn new(task: LenientForm<Task>) { .. }

フィールドのリネーム

structのフィールドについて、Rustのプログラム上で使う名前と
Form<T>などで割り当てられる名前が一致しない場合、
Form割当のとき使う名前をアトリビュートで設定できる。

#[derive(FromForm)] struct External { #[form(field = "type")] api_type: String }

Formのバリデーション

  1. 構造体のフィールドの型を独自の1要素タプル構造体にする
  2. 独自のタプル構造体に対して、FromFormValue トレイトを実装する。
    • from_form_value()メソッドはFormデータを構造体に割り当てるときに呼ばれる。
struct AdultAge(usize); impl<'v> FromFormValue<'v> for AdultAge { type Error = &'v RawStr; fn from_form_value(form_value: &'v RawStr) -> Result<AdultAge, &'v RawStr> { match form_value.parse::<usize>() { Ok(age) if age >= 21 => Ok(AdultAge(age)), _ => Err(form_value), } } } #[derive(FromForm)] struct Person { age: AdultAge }

もしもInvalidなら、ハンドラが呼ばれない

FromFormValueトレイトは、enum型にも実装できる
その際は#[derive(FromFormValue)]でいける
Valiantに含まれる値以外ならinvalid

#[derive(FromFormValue)] enum MyValue { First, Second, Third, }

Jsonの取扱い

#[derive(Deserialize)] struct Task { description: String, complete: bool } #[post("/todo", data = "<task>")] fn new(task: Json<Task>) -> String { ... }

構造体に、#[derive(Deserialize)] を実装すると
httpリクエストのbodyのjson文字列から、構造体に直せる

Json type は rocket_contriveに含まれる

Deserializeトレイとは Serde というクレートに依存している。
githubのrocket getstarted json example をみる

Streaming

streaming (流れるように送られてくる)データを扱いたい場合
Rocketが用意している Data 型を使う
format は "plane"

#[post("/upload", format = "plain", data = "<data>")] fn upload(data: Data) -> io::Result<String> { data.stream_to_file("/tmp/upload.txt").map(|n| n.to_string()) }

注意 : streaming data は必ず take()を使う

DoS攻撃を防止するためにも、受信するデータ量は制限する必要がある。
take()メソッドを使うのが簡単

data.open().take(LIMIT)

Errorキャッチャー

ルーティングは失敗する可能性をはらんでいる

  • レスポンスガード
  • matchするrouteがない
  • Responderが失敗する
    これらが発生したら、Rocketはエラーをクライアントに返す。
    その時、httpステータスコードに応じてcatcherハンドラーを実行する。
    デフォルトで設定されているが、書き換えることができる。
#[catch(404)] fn not_found(req: &Request) -> T { .. } #[catch(404)] fn not_found(req: &Request) -> String { format!("Sorry, '{}' is not a valid path.", req.uri()) } // 設定したら、registerメソッドでRocketオブジェクトに関数を登録する rocket::ignite().register(catchers![not_found])
  • register() : chartcherのリストを登録する
  • catcher![] : マクロ