【Rust】axumでjsonデータを受け取ってjsonで返す

値を整形せずに、そのまま返しています。

async fn handle_service(extract::Json(payload): extract::Json<Service>) -> Json<Service>{

    println!("{:?}", payload);
    Json(payload)
}

curl -X POST -H “Content-Type: application/json” -d “{\”version\”:\”1\”,\”nonce\”:\”1482680747\”,\”connections\”:21}” 127.0.0.1:3000/service
{“version”:”1″,”nonce”:”1482680747″,”connections”:21}

なるほどー

【Rust】自分のIPアドレスを取得する

IPを返却するサイトからIPアドレスを取得します。
httpbinにreqwest::getすると、String型で返却されるので、serde_jsonを使ってIpの構造体に変換しています。

use reqwest::Client;
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Ip {
    origin: String,
}

#[tokio::main]
async fn main() {
    let _ = get_ip().await;
}

// get ip
async fn get_ip() -> Result<(), Box<dyn std::error::Error>> {

    let ip: Ip = serde_json::from_str(&reqwest::get("https://httpbin.org/ip")
        .await?
        .text()
        .await?)?;
    
    println!("ip = {:?}", ip.origin);
    Ok(())
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.08s
Running `target/debug/sample`
ip = “***.***.*.***”

うーむ、なんだかな〜

【Rust】トランザクションにnftデータを追加する

UnsignedTransaction, SignedTransactionの構造体に、nft_dataと、nft_originを追加する。
nft_dataは、nftとして保存するデータ。
nft_originはnftを発行したトランザクションのhashデータ。
NFTを譲渡する場合は、nft_dataを空にして、nft_originに値を入れる。

#[derive(Serialize, Deserialize, Clone, Debug)]
struct UnsignedTransaction {
    time: String,
    sender: String,
    receiver: String,
    amount: i32,
    nft_data: String,
    nft_origin: String,
}

nft_holderというhashmapを作成して、そこにnftを発行したトランザクションのhashデータと、最後にNTFを譲渡されたreceiverの連想配列を入れていく。
誰が何を持っているかは、トランザクションのhashデータをデコードして、nft_dataの値を取り出す。

#[tokio::main]
async fn main(){

    let mut transaction_pool: Vec<SignedTransaction> = Vec::new();
    let transaction1 = UnsignedTransaction {time:Utc::now().to_string(), sender:"047683c00f6z".to_string(), receiver:"DyH5SHvezz".to_string(), amount: 0, nft_data:"hello world".to_string(),nft_origin:"".to_string()};
    let transaction2 = UnsignedTransaction {time:Utc::now().to_string(), sender:"DyH5SHvezz".to_string(), receiver:"655EFC80ss".to_string(), amount: 0, nft_data:"".to_string(),nft_origin:"eyJ0aW1lIjoiMjAyNS0wMS0xNyAwODoyOTo0Ni42NzgzNDU3OTQgVVRDIiwic2VuZGVyIjoiMDQ3NjgzYzAwZjZ6IiwicmVjZWl2ZXIiOiJEeUg1U0h2ZXp6IiwiYW1vdW50IjowLCJuZnRfZGF0YSI6ImhlbGxvIHdvcmxkIiwibmZ0X29yaWdpbiI6IiJ9".to_string()};
    let transaction3 = UnsignedTransaction {time:Utc::now().to_string(), sender:"047683c00f6z".to_string(), receiver:"DyH5SHvezz".to_string(), amount: 0, nft_data:"Milk Cafe".to_string(),nft_origin:"".to_string()};
    let transaction4 = UnsignedTransaction {time:Utc::now().to_string(), sender:"047683c00f6z".to_string(), receiver:"DyH5SHvezz".to_string(), amount: 1000, nft_data:"".to_string(),nft_origin:"".to_string()};

    // println!("{}", BASE64_STANDARD.encode(serde_json::to_vec(&transaction1.clone()).unwrap()));
    // println!("{}", base64_decode(&str));

    transaction_pool.push(sign_transaction(&transaction1));
    transaction_pool.push(sign_transaction(&transaction2));
    transaction_pool.push(sign_transaction(&transaction3));
    transaction_pool.push(sign_transaction(&transaction4));

    let nft_holder:HashMap<String, String> = nft_calc(transaction_pool);
    for (k, v) in nft_holder {
        let transaction_str = base64_decode(&k);
        let transaction:UnsignedTransaction = serde_json::from_str(&transaction_str).unwrap();
        println!("保有者:{}, NFT:{}", v, transaction.nft_data);
    }
}

fn nft_calc(transaction_pool: Vec<SignedTransaction>) -> HashMap<String, String> {
    let mut nft_holder: HashMap<String, String> = HashMap::new();
    for transaction in transaction_pool {
        if transaction.amount == 0 {
            let transaction_hash: String = BASE64_STANDARD.encode(serde_json::to_vec(&transaction.clone()).unwrap());
            if transaction.nft_origin == "" && transaction.nft_data != "" && (nft_holder.get(&transaction_hash) == None) {
                nft_holder.insert(transaction_hash, transaction.receiver);
            } else if (nft_holder.get(&transaction_hash) == Some(&transaction.sender)) && transaction.nft_data == "" {
                nft_holder.insert(transaction.nft_origin, transaction.receiver);
            }
        }
    }
    return nft_holder
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.13s
Running `target/debug/sample`
保有者:DyH5SHvezz, NFT:hello world
保有者:DyH5SHvezz, NFT:Milk Cafe

これは中々凄いな…

【Rust】hashmapのキーの存在チェック

map.get(&target) != None でチェックできる。

    let mut holder: HashMap<String, String> = HashMap::new();

    holder.insert("yamada".to_string(), "A".to_string());
    holder.insert("sato".to_string(), "B".to_string());
    holder.insert("tanaka".to_string(), "C".to_string());

    for (name, item) in &holder {
        println!("{}:{}", name, item);
    }

    let target = "yamada".to_string();
    if holder.get(&target) != None {
        println!("{}", holder["yamada"]);
    }

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.33s
Running `target/debug/my_web_app`
yamada:A
tanaka:C
sato:B
A

pythonのように target in map ではチェックできない。
なるほど…

【Rust】axumでログアウトの処理

async fn handle_logout()-> impl IntoResponse{

    let tera = tera::Tera::new("templates/*").unwrap();

    let mut context = tera::Context::new();
    context.insert("title", "Index page");
    context.insert("data", "hoge");

    let output = tera.render("index.html", &context);
    let cookie:String = format!("session_token=_");
    let headers = AppendHeaders(vec![("Set-Cookie", cookie)]);
    (headers, axum::response::Html(output.unwrap()))
}

特にコメントなし。
さー次行こう^^

【Rust】セッションをcookieに保存したい

AppendHeadersに中々辿り着けず、layerを堂々巡りした挙句、chromのcookieの設定などをミスって、これ作るのに丸2日かかりました。。。シクシク、泣きたい。

async fn handle_index()-> impl IntoResponse {
    
    let mut random = ChaCha8Rng::seed_from_u64(OsRng.next_u64());
    let mut u128_pool = [0u8; 16];
    random.fill_bytes(&mut u128_pool);
    let session_token = u128::from_le_bytes(u128_pool);
    println!("{:?}", &session_token);

    let tera = tera::Tera::new("templates/*").unwrap();

    let mut context = tera::Context::new();
    context.insert("title", "Index page");

    let output = tera.render("index.html", &context);
    let cookie:String = format!("session_token={}; Max-Age=600", &session_token);
    let headers = AppendHeaders(vec![("Set-Cookie", cookie)]);
    (headers, axum::response::Html(output.unwrap()))
}

psqlにセッションテーブルを作って、ログインしたタイミングでセッションテーブルにもsessionのvalueを保存するようにする。

【Rust】ChaCha8Rngでセッションを作りたい

use rand_core::OsRng;
use rand_core::RngCore;
use rand_chacha::ChaCha8Rng;
use rand_chacha::rand_core::SeedableRng;


fn main(){
    let mut random = ChaCha8Rng::seed_from_u64(OsRng.next_u64());

    let mut u128_pool = [0u8; 16];
    random.fill_bytes(&mut u128_pool);

    let session_token = u128::from_le_bytes(u128_pool);
    println!("{:?}", session_token);
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.03s
Running `target/debug/sample`
68291255163114295989027254289217915346

いまいち意味わかってないけどすげえ…

【Rust】Axumでなんとかログイン機能を作りたい(その3)

axumでユーザの登録画面、insert文を作っていきます。
L formで送られてきたパスワードをハッシュ化して、usrnameと一緒に保存します。

async fn handle_save(form: Form<SignUp>)-> axum::response::Html<String> {
    let register: SignUp = form.0;
    let password_hash = hashing_password(&register.password);

    println!("{}", &password_hash);

    let _ = db_save(&register.username.to_string(), &password_hash).await.unwrap();

    let tera = tera::Tera::new("templates/*").unwrap();

    let mut context = tera::Context::new();
    context.insert("title", "Index page");

    let output = tera.render("index.html", &context);
    axum::response::Html(output.unwrap())
}

async fn db_save(name: &String, password: &String) -> Result<(), Box<dyn std::error::Error>> {
    let (client, connection) = tokio_postgres::connect("host=localhost user=postgres password=password", NoTls).await?;

    tokio::spawn(async move {
        if let Err(e) = connection.await {
            eprintln!("connection error: {}", e);
        }
    });
    client.execute(
        "INSERT INTO users (username, password) VALUES ($1, $2)",
        &[&name, &password],
    ).await?;

    for row in client.query("SELECT id, username, password From users", &[]).await? {
        let id: i32 = row.get(0);
        let name: String = row.get(1);
        let password: String = row.get(2);

        println!("{} {} {}", id, name, password);
    }
    Ok(())
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.95s
Running `target/debug/login`
$2b$08$OxMrzm88ag/iPtKahE3kveHNmmUI5uAuYGuY.lcCybGGqN8cYx/km
1 hpscript $2b$08$bKVELv/My0WaKvoyTltoLe3pOp7VJ4UxtxuA3C5qurnAblnqXbcfi
2 yamada $2b$08$OxMrzm88ag/iPtKahE3kveHNmmUI5uAuYGuY.lcCybGGqN8cYx/km

ここまでは割と簡単なんだけど、問題はここからなんだよな〜

【Rust】Axumでなんとかログイン機能を作りたい(その2)

postgres=# INSERT INTO users (username, password) VALUES (‘hpscript’, ‘$2b$08$bKVELv/My0WaKvoyTltoLe3pOp7VJ4UxtxuA3C5qurnAblnqXbcfi’);
INSERT 0 1
postgres=# select * from users;
id | username | password
—-+———-+————————————————————–
1 | hpscript | $2b$08$bKVELv/My0WaKvoyTltoLe3pOp7VJ4UxtxuA3C5qurnAblnqXbcfi

DBに入っているパスワードハッシュ値と、Postされてきたpasswordの一致を検証する。
asyncでpsqlを使う場合は、postgresではなく、tokio_postgres出ないとエラーになる。
参考: https://docs.rs/tokio-postgres/latest/tokio_postgres/

async fn handle_signup(form: Form<SignUp>)-> axum::response::Html<String> {
    let signup: SignUp = form.0;

    let hash = db_check(&signup.username.to_string()).await.unwrap();
    println!("{}", &hash);

    let password = &signup.password;
    println!("{}", verify(password, &hash));

    println!("{:?}", signup);

    // 省略
}
fn verify(password: &String, hash: &String) -> bool {
    bcrypt::verify(password, hash)
}

async fn db_check(name: &String) -> Result<String, Box<dyn std::error::Error>> {
    let (client, connection) = tokio_postgres::connect("host=localhost user=postgres password=password", NoTls).await?;

    tokio::spawn(async move {
        if let Err(e) = connection.await {
            eprintln!("connection error: {}", e);
        }
    });
    let row = client.query("SELECT * From users where username=$1", &[&name]).await?;
    let hash: String = row[0].get(2);
    println!("{}",hash);
    Ok(hash)
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.44s
Running `target/debug/login`
$2b$08$bKVELv/My0WaKvoyTltoLe3pOp7VJ4UxtxuA3C5qurnAblnqXbcfi
$2b$08$bKVELv/My0WaKvoyTltoLe3pOp7VJ4UxtxuA3C5qurnAblnqXbcfi
true
SignUp { username: “hpscript”, password: “asdf1234” }

よしよしよーし。セッションは後からやるとして、先にpsqlへのinsertとregister画面から作ります。

【Rust】Axumでなんとかログイン機能を作りたい(その1)

### usersテーブルの作成

sudo -u postgres psql

CREATE TABLE users (
	id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
	username text NOT NULL UNIQUE,
	password text NOT NULL
);

axumでformでpostされたデータを受け取ります。

use axum::{Router, routing::get, routing::post, extract::Form};
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Clone, Debug)]
struct SignUp {
    username: String,
    password: String,
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", axum::routing::get(handle_index))
        .route("/signup", axum::routing::post(handle_signup));

    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn handle_index()-> axum::response::Html<String> {
    let tera = tera::Tera::new("templates/*").unwrap();

    let mut context = tera::Context::new();
    context.insert("title", "Index page");
    context.insert("data", "hoge");

    let output = tera.render("index.html", &context);
    axum::response::Html(output.unwrap())
}

async fn handle_signup(form: Form<SignUp>)-> axum::response::Html<String> {
    let signup: SignUp = form.0;
    println!("{:?}", signup);

    let tera = tera::Tera::new("templates/*").unwrap();

    let mut context = tera::Context::new();
    context.insert("title", "Index page");
    context.insert("data", "hoge");

    let output = tera.render("index.html", &context);
    axum::response::Html(output.unwrap())
}

コマンドラインからPostします。
$ curl -X POST -d ‘username=山田太郎’ -d ‘password=asdf1234’ 127.0.0.1:3000/signup

Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.65s
Running `target/debug/login`
SignUp { username: “山田太郎”, password: “asdf1234” }

なるほど、なんとかここまでできた。ミドルウェアのところと、セッションの箇所をどうするか悩ましいが、まぁよしとしよう。