###### 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を書く ```toml [dependencies] rocket = "0.4.2" ``` # Hello, world ```rust= #![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!マクロがコード生成できないため ```rust= 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型として書く ```rust= #[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を行う ```rust= #[get("/hello/<name>/<age>/<cool>")] fn hello(name: String, age: u8, cool: bool) -> String { ... } ``` - 合わなかったら、次の優先度のハンドラへ - ハンドラの優先度は、mounte()のroutes!マクロの左順 ## Fowarding - パスのパラメータで取った値が合わなかったら、次にマッチしたrouteにリクエストを回す - routeがなくなるまで続ける - なくなったら、404errorを返す ## routeのランク 同じようなrouteがあった場合、ランクが高い方を優先してアクセスする。 routeアトリビュートに、rank = で明示できる ```rust= #[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 静的ファイルのホスティングは、以下のコードでできる。 ```rust= rocket.mount("/public", StaticFiles::from("/static")) ``` /static/ フォルダにあるファイルを /public/path でアクセスできるようにする。 ## Option パラメータ あってもなくてもいいパスのときは、ハンドラで Option型で受け取る。 ```rust= #[get("/hello?wave&<name>")] fn hello(name: Option<&RawStr>) -> String { name.map(|name| format!("Hi, {}!", name)) .unwrap_or_else(|| "Hello!".into()) } ``` ## 複数セグメント ?...&...&...のような、複数クエリを取って #[derive(FromForm)] を実装した構造体に割り当てられる ```rust= 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つのリクエストガードを使う(そんな名前のないけど) ```rust= #[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 はカスタムリクエストガード) ```rust= #[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 ```rust= 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" となる ```rust= #[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を記述するべき ```rust= #[get("/user/<id>", format = "json")] fn user(id: usize) -> Json<User> { ... } ``` ## Bodyデータのの受け取り routeアトリビュートで data="\<変数名>" にして ハンドラの同名引数で受け取る FromDataトレイトを実装する型で受け取れる ```rust= #[post("/", data = "<input>")] fn new(input: T) -> String { ... } ``` # Forms httpのformで入力したキーバリューを含んだpostの場合、 #[derive(FromForm)]を実装した構造体に、formの値を割り当てられる ```rust= #[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型でも受け取れる ```rust= #[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 ```rust= #[derive(FromForm)] struct Task { .. } #[post("/todo", data = "<task>")] fn new(task: LenientForm<Task>) { .. } ``` ## フィールドのリネーム structのフィールドについて、Rustのプログラム上で使う名前と Form\<T>などで割り当てられる名前が一致しない場合、 Form割当のとき使う名前をアトリビュートで設定できる。 ```rust= #[derive(FromForm)] struct External { #[form(field = "type")] api_type: String } ``` # Formのバリデーション 1. 構造体のフィールドの型を独自の1要素タプル構造体にする 2. 独自のタプル構造体に対して、FromFormValue トレイトを実装する。 - from_form_value()メソッドはFormデータを構造体に割り当てるときに呼ばれる。 ```rust= 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 ```rust= #[derive(FromFormValue)] enum MyValue { First, Second, Third, } ``` # Jsonの取扱い ```rust= #[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" ```rust= #[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()メソッドを使うのが簡単 ```rust= data.open().take(LIMIT) ``` # Errorキャッチャー ルーティングは失敗する可能性をはらんでいる - レスポンスガード - matchするrouteがない - Responderが失敗する これらが発生したら、Rocketはエラーをクライアントに返す。 その時、httpステータスコードに応じてcatcherハンドラーを実行する。 デフォルトで設定されているが、書き換えることができる。 ```rust= #[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![] : マクロ