Closed11

RustでWebアプリを初めて開発する

Shige OsadaShige Osada

https://www.amazon.co.jp/dp/4798061700/

Rustプログラミング入門の第5章に沿って入門。第1章〜第4章は、ざっと目を通したレベルで着手。
実施日: 2020年12月25日
Rust version: 1.48

[ 20-12-25 17:39 ] ~/workspace/srictf
osada@mbp20i% cargo add actix-web actix-rt                      
error: no such subcommand: `add`

	Did you mean `b`?

素直にやると、cargo addがないと怒られる。
対応として、cargo-editをインストールする(これは2章に書いてある内容)

[ 20-12-25 17:45 ] ~/workspace/srictf
osada@mbp20i% cargo install cargo-edit    
    Updating crates.io index
  Downloaded cargo-edit v0.7.0
(省略)
   Installed package `cargo-edit v0.7.0` (executables `cargo-add`, `cargo-rm`, `cargo-upgrade`)

こうすると、cargo addなどが使えるようになる。

[ 20-12-25 17:48 ] ~/workspace/srictf
osada@mbp20i% cargo add actix-web actix-rt
    Updating 'https://github.com/rust-lang/crates.io-index' index
      Adding actix-web v3.3.2 to dependencies
      Adding actix-rt v1.1.1 to dependencies
Shige OsadaShige Osada

セミコロンの使い方

Wikipediaによると、以下とのこと。

ブロックコード内の命令文のセパレータにはセミコロン(;)を用いるが、C言語のそれと異なりRustのセミコロンは直前の命令文がブロックコードで括られる式の途中式であることを宣言するためのものである。セミコロンを末尾に置かない命令文はブロックコードの最終的な評価式として扱われ、その式の結果がブロックコードの外へ戻り値として返される[30]。

なるほど。セミコロンがないと、その関数の戻り値となるのね。
ちょっとrubyに似ている?

Shige OsadaShige Osada

HTTPサーバのサンプルコード

main.rs
use actix_web::{get, App, HttpResponse, HttpServer};

#[get("/")]
async fn index() -> Result<HttpResponse, actix_web::Error> {
    let response_body = "Hello world!";
    Ok(HttpResponse::Ok().body(response_body))

}

#[actix_rt::main]
async fn main() -> Result<(), actix_web::Error> {
    HttpServer::new(move || App::new().service(index))
        .bind("0.0.0.0:8080")?
        .run()
        .await?;
    Ok(())
}

IDE(IntelliJのplugin)で書くと楽チン。

[ 20-12-25 19:09 ] ~/workspace/srictf
osada@mbp20i% cargo run
   Compiling srictf v0.1.0 (/Users/osada/workspace/srictf)
    Finished dev [unoptimized + debuginfo] target(s) in 6.45s
     Running `target/debug/srictf`

初回起動時は、色々なものがインストールされる。
Cargoのすごいところなんだろう(よくわからん)

Shige OsadaShige Osada

IDE IntelliJ Rust

Standard libraryがおかしいと言われる。よくわからない。。
きっと、コード補完やハイライトがおかしいのはこのせいなんだろうな。

10:23	Cargo project update failed:
			corrupted standard library: /Users/osada/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust/src

https://www.mmbyte.com/article/101548.html
色々調べてたが、Answer1では解決しなかった。(via rustupが, IntelliJ 2019.3ではない模様)

Shige OsadaShige Osada

シンプルなViewテンプレート

template/index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>SRI CTF</title>
</head>
<body>

<h1>SRI CTF</h1>

<div>
    {% for entry in entries %}
    <div>
        <div>id: {{ entry.id }}, text: {{ entry.text }}</div>
        <form action="/answer" method="post">
            <input type="hidden" name="id" value="{{ entry.id }}">
            Flag:
            <input type="text" name="answer">
            <button>Answer</button>
        </form>
    </div>
    {% endfor %}
</div>

<hr>

<h2>Add Question</h2>

<form action="/add" method="post">
    <div>
        Question: <input type="text" name="text">
    </div>
    <div>
        Flag: <input type="text" name="flag">
    </div>
    <div>
        <button>Add Question</button>
    </div>
</form>
</body>
</html>

これを後ろ側で動かす仕組み

src/main.rs
use actix_web::{get, http::header, post, web, App, HttpResponse, HttpServer, ResponseError};
use askama::Template;
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::params;
use serde::Deserialize;
use thiserror::Error;

#[derive(Deserialize)]
struct AddParams {
    text: String,
    flag: String,
}

#[derive(Deserialize)]
struct AnswerParams {
    id: u32,
    text: String,
}

// text -> flag? or explain?
struct QuestionEntry {
    id: u32,
    text: String,
}

#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate {
    entries: Vec<QuestionEntry>,
}

#[derive(Error, Debug)]
enum MyError {
    #[error("failed to render HTML")]
    AskamaError(#[from] askama::Error),

    #[error("failed to get connection")]
    ConnectionPoolError(#[from] r2d2::Error),

    #[error("failed SQL execution")]
    SQLiteError(#[from] rusqlite::Error),
}

impl ResponseError for MyError {}

// ----------

#[post("/add")]
async fn add_question(
    params: web::Form<AddParams>,
    db: web::Data<r2d2::Pool<SqliteConnectionManager>>,
) -> Result<HttpResponse, MyError> {
    let conn = db.get()?;
    conn.execute("INSERT INTO question (text, flag) VALUES (?1, ?2)", &[&params.text, &params.flag])?;
    Ok(HttpResponse::SeeOther()
        .header(header::LOCATION, "/")
        .finish())
}

#[post("/answer")]
async fn answer_question(
    params: web::Form<AnswerParams>,
    db: web::Data<r2d2::Pool<SqliteConnectionManager>>,
) -> Result<HttpResponse, MyError> {
    let conn = db.get()?;
    // Todo
    // let mut statement = conn.prepare("SELECT COUNT(*) FROM question WHERE id = ? AND flag = ?",
    // &[&params.id], &[&params.text])?;

    // Do nothing
    Ok(HttpResponse::SeeOther()
        .header(header::LOCATION, "/")
        .finish())
}


#[get("/")]
async fn index(db: web::Data<Pool<SqliteConnectionManager>>) -> Result<HttpResponse, MyError> {
    // DB接続
    let conn = db.get()?;
    let mut statement = conn.prepare("SELECT id, text FROM question")?;
    // DBクエリ結果をrowsに収納。ラベル付けも同時にする。
    let rows = statement.query_map(params![], |row| {
        let id = row.get(0)?;
        let text = row.get(1)?;
        Ok(QuestionEntry {id, text})
    })?;

    let mut entries = Vec::new();
    for row in rows {
        entries.push(row?);
    }

    // htmlテンプレートにentriesを渡す。
    let html = IndexTemplate { entries };
    let response_body = html.render()?;
    Ok(HttpResponse::Ok()
        .content_type("text/html")
        .body(response_body))
}

#[actix_rt::main]
async fn main() -> Result<(), actix_web::Error> {
    let manager = SqliteConnectionManager::file("srictf.db");
    let pool = Pool::new(manager).expect("Failed to initialize the connection pool.");
    let conn = pool
        .get()
        .expect("Failed to get the connection from the pool");

    conn.execute(
        "CREATE TABLE IF NOT EXISTS question (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            text TEXT NOT NULL,
            flag TEXT NOT NULL
        )",
        params![],)
        .expect("Failed to create a table `question`.");

    HttpServer::new(move || {
        App::new()
            .service(index)
            .service(add_question)
            .service(answer_question)
            .data(pool.clone())
        })
        .bind("0.0.0.0:8080")?
        .run()
        .await?;
    Ok(())
}

Answer部分はこれから実装。

rusqliteは、このサイトを参考にした。
https://qiita.com/ekzemplaro/items/218eef74506e09bb03f3

Shige OsadaShige Osada

ToDo 2020-12-27

  • デバッグ方法
  • rusqliteの状態(特にselectの検索とRustの内部処理のバランス)
  • Dockerイメージ化
  • IntelliJのエラー対応
Shige OsadaShige Osada

Errorハンドリング

サンプルコードの解説(書店サイト)
https://blog.logrocket.com/template-rendering-in-rust/
サンプルコードのgithub.com
https://github.com/zupzup/rust-templating-example

これ、DBをメモリにしか持たないので、rusqliteを組み込もう。

コンソールでのdebugは、安直に以下で良いぽい。

    println!("hello");
    dbg!("This is debug");

こんな感じ。

hello
[src/main.rs:55] "This is debug" = "This is debug"
Shige OsadaShige Osada

Parse Errorの対応方法

  1. templateのformで渡す変数名と、paramsで受け取る変数名があっているか確認しよう。

てなわけで、rusqliteで行くことにする。
executeやquery_mapで引数の渡し方も、なんとなくわかってきた。
(仕組みは説明できないけど)

Shige OsadaShige Osada

プレースホルダ利用時のquery

プレースホルダに変数を渡すときは、すべてを型指定しておくこと。
中途半端に指定すると、第一引数にすべて型推論されて型不一致時にコンパイルエラーになる。

    let mut cached_statement = conn.prepare_cached(
        "SELECT id, text, flag FROM question WHERE id = ?1 AND flag = ?2")?;

// これはコンパイルできる
    let rows = cached_statement.query_map(&[&params.id.to_string(), &params.flag.to_string()], |row| {Ok(1)})?;

// これはコンパイルできない。
//  第2
    // let rows = statement.query_map(&[&params.id, &params.flag.to_string()], |row| {Ok(1)})?;

コンパイルエラーのときの出力メッセージ

77 |     let rows = cached_statement.query_map(&[&params.id, &params.flag.to_string()], |row| {Ok(1)})?;
   |                                                         ^^^^^^^^^^^^^^^^^^^^^^^^ expected `u32`, found struct `std::string::String`

第一引数がu32で指定しているから、第二引数もu32(本当はString型)でチェックが入る模様。

・・・すごい・・・。

Shige OsadaShige Osada

IntelliJ Rust

1.48.0でどうしてもこれが出続ける。

Cargo project update failed: corrupted standard library

https://rustcc.cn/article?id=c045705b-3f93-4721-85b5-048207c0f5c8

これを読むと、雰囲気で、

lib/rustlib/src/rust/library

が正しいらしい。

lib/rustlib/src/rust/src
(llvm-project/libunwindがあるだけ)

ではないらしい。

が、IntelliJ 側で設定を変えられない。なぜだ。

アップデートしてからまだ1ヶ月も立っていなので、このあたりの変更はプラグインが追いついていないような気もする。今日は、深追いは一旦やめる。入門者にコードアシストツールがないのは、なかなかつらいところ。昔のようなプログラミング学習の時代を体験しているようで、ありといえばあり・・・と前向きに捉えることにする。

このスクラップは2020/12/30にクローズされました