【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” }

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

【Rust】Argon2でパスワードハッシュを作ろう

use argon2::password_hash::{self, SaltString};
use argon2::{Argon2, PasswordHasher, Algorithm, Version, Params};

fn compute_password_hash(password: String) -> Result<String, password_hash::Error> {
    let salt = SaltString::generate(&mut rand::thread_rng());
    let password_hash = Argon2::new(
        Algorithm::Argon2id,
        Version::V0x13,
        Params::new(15000, 2, 1, None).unwrap(),
    )
    .hash_password(password.as_bytes(), &salt)?
    .to_string();
    Ok(password_hash)
}

fn main() {
    let password = "thisispassword".to_string();
    let hash = compute_password_hash(password);
    println!("{:?}", hash);
}

Compiling login v0.1.0 (/home/vagrant/dev/rust/login)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.34s
Running `target/debug/login`
Ok(“$argon2id$v=19$m=15000,t=2,p=1$ZsxlUs1xh+gmG6VkFrCUBw$lTNPO8Dkp7yrhkRmeuVp7WzWHnf4TAm/HjRUnhQz5T8”)

なんやこれ、ますます迷走してきた…

use argon2::password_hash::{self, SaltString};
use argon2::{Argon2, PasswordHasher, Algorithm, Version, Params, PasswordHash, PasswordVerifier};

fn compute_password_hash(password: String) -> Result<String, password_hash::Error> {
    let salt = SaltString::generate(&mut rand::thread_rng());
    let password_hash = Argon2::new(
        Algorithm::Argon2id,
        Version::V0x13,
        Params::new(15000, 2, 1, None).unwrap(),
    )
    .hash_password(password.as_bytes(), &salt)?
    .to_string();
    Ok(password_hash)
}

fn verify_password_hash(
    password: String,
    expected_password_hash: String,
) -> Result<(), password_hash::Error> {
    let expected_password_hash = PasswordHash::new(expected_password_hash.as_str())?;
    Argon2::default().verify_password(password.as_bytes(), &expected_password_hash)
}

fn main() {
    let password = "thisispassword".to_string();
    let hash = compute_password_hash(password.clone());
    println!("{:?}", hash);
    verify_password_hash(password.clone(), hash.unwrap());
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.34s
Running `target/debug/login`
Ok(“$argon2id$v=19$m=15000,t=2,p=1$SN77yEDgYTqh6QT2M9nhEw$5P53ZMpHwg0Ap/S3C2CKsZ8jjEyEPL12gscFrGf80l0”)

まぁ、そりゃそうやろ

【Rust】axumでログイン機能を作りたい

axum-loginのページをそのまま書きます。
https://docs.rs/axum-login/latest/axum_login/

use std::collections::HashMap;

use async_trait::async_trait;
use axum_login::{AuthUser, AuthnBackend, UserId};

#[derive(Debug, Clone)]
struct User {
    id: i64,
    pw_hash: Vec<u8>,
}

impl AuthUser for User {
    type Id = i64;

    fn id(&self) -> Self::Id {
        self.id
    }

    fn session_auth_hash(&self) -> &[u8] {
        &self.pw_hash
    }
}

#[derive(Clone, Default)]
struct Backend {
    users: HashMap<i64, User>,
}

#[derive(Clone)]
struct Credentials {
    user_id: i64,
}

#[async_trait]
impl AuthnBackend for Backend {
    type User = User;
    type Credentials = Credentials;
    type Error = std::convert::Infallible;

    async fn authenticate(
        &self,
        Credentials { user_id }: Self::Credentials,
    ) -> Result<Option<Self::User>, Self::Error> {
        Ok(self.users.get(&user_id).cloned())
    }

    async fn get_user(
        &self,
        user_id: &UserId<Self>,
    ) -> Result<Option<Self::User>, Self::Error> {
        Ok(self.users.get(user_id).cloned())
    }
}

fn main() {

}

ちょっと待って、最初のPassword HashのVecがわからん…
調べ物を理解するために付随するものを調べるの永遠ループが止まらん…

【Rust】axumのauthとmiddleware

use std::{collections::HashMap, sync::Arc};

use axum::{
    async_trait,
    extract::{FromRequestParts, Request, State},
    middleware::Next,
    response::Response,
    RequestExt as _,
};
use axum_extra::{
    headers::{authorization::Bearer, Authorization},
    TypedHeader,
};
use http::{request::Parts, StatusCode};

pub type Token = String;
pub type UserMap = HashMap<Token, User>;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct User {
    pub username: String,
}

pub fn build_user_map() -> UserMap {
    let mut user_map = HashMap::new();
    user_map.insert(
        "aaa".to_string(),
        User {
            username: "Andy".to_string(),
        },
    );
    user_map.insert(
        "bbb".to_string(),
        User {
            username: "Bella".to_string(),
        },
    );
    user_map.insert(
        "ccc".to_string(),
        User {
            username: "Callie".to_string(),
        },
    );
    user_map.insert(
        "ddd".to_string(),
        User {
            username: "Daren".to_string(),
        },
    );
    user_map
}

#[async_trait]
impl<S> FromRequestParts<S> for User
where
    S: Send + Sync,
{
    type Rejection = StatusCode;

    async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
        let user = parts
            .extensions
            .get::<Self>()
            .expect("User not found. Did you add auth_middleware?");
        Ok(user.clone())
    }
}

pub async fn auth_middleware(
    State(user_map): State<Arc<UserMap>>,
    mut request: Request,
    next: Next,
) -> axum::response::Result<Response> {
    let bearer = request
        .extract_parts::<TypedHeader<Authorization<Bearer>>>()
        .await
        .map_err(|_| StatusCode::BAD_REQUEST)?;
    let token = bearer.token();

    let user = user_map.get(token).ok_or(StatusCode::UNAUTHORIZED)?;
    request.extensions_mut().insert(user.clone());

    Ok(next.run(request).await)
}
mod auth;

use std::sync::Arc;
use axum::{middleware::from_fn_with_state, routing::get, Router};
use auth::{auth_middleware, build_user_map, User, UserMap};


#[tokio::main]
async fn main() {
    let user_map = build_user_map();
    let app = build_app(Arc::new(user_map));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

#[rustfmt::skip]
fn build_app(user_map: Arc<UserMap>) -> Router {
    let public_router = Router::new()
        .route("/public", get(public));

    let private_router = Router::new()
        .route("/private", get(private))
        .route("/your-name", get(your_name))
        .route_layer(from_fn_with_state(user_map.clone(), auth_middleware));

    Router::new()
        .nest("/", public_router)
        .nest("/", private_router)
        .with_state(user_map)
}

async fn public() -> &'static str {
    "This is public."
}

async fn private() -> &'static str {
    "This is public."
}

async fn your_name(user: User) -> String {
    format!("Your name is {}.", user.username)
}

curl http://192.168.33.10:3000/public
curl -I http://192.168.33.10:3000/private
curl -H ‘Authorization: Bearer aaa’ http://192.168.33.10:3000/your-name

やりたいことはギリわかるが、Bearer認証およびtraitのextensionとauth middlewareのinputとその先の内容がイマイチ理解できん…