【Rust】フレームワーク自作: 静的ファイルハンドリング

静的ファイルはstaticに置くルールとする

    if path.starts_with("/static/") {
        let file_path = format!(".{}", path);

        match fs::read(&file_path) {
            Ok(contents) => {
                let content_type = get_content_type(path);
                let response = format!(
                    "HTTP/1.1 200 OK\r\nContent-Type: {}\r\nContent-Length: {}\r\n\r\n",
                    content_type,
                    contents.len()
                );
                let _ = stream.write_all(response.as_bytes());
                let _ = stream.write_all(&contents);
            }
            Err(_) => {
                let response = http_response(404, "text/plain", "Static file not found");
                let _ = stream.write_all(response.as_bytes());
            }
        }
        stream.flush().unwrap();
        return;
    }
// 

fn get_content_type(path: &str) -> &'static str {
    if path.ends_with(".css") {
        "text/css"
    } else if path.ends_with(".js") {
        "application/javascript"
    } else if path.ends_with(".png") {
        "image/png"
    } else if path.ends_with(".jpg") || path.ends_with(".jpeg") {
        "image/jpeg"
    } else if path.ends_with(".svg") {
        "image/svg+xml"
    } else if path.ends_with(".woff2") {
        "font/woff2"
    } else {
        "application/octet-stream"
    }
}

templatesフォルダのファイルが、以下対応できるようになります。

<link rel="stylesheet" href="/static/styles.css">
<script src="/static/app.js"></script>
<h1>Welcome to the Rust server!</h1>
<p>hello, {{ name }}</p>

【Rust】フレームワーク自作: 匿名セッションによるCSRF対策を実装したい

get methodもhashmapではなく、HttpResponseに変更することで、get methodでもcookieをセットできるようになる。

use std::collections::HashMap;
mod ares;
use ares::{Router, HttpResponse, parse, signup, logup, redirect, render_template_from_file, render_template_from_file_empty};

fn main() {
    let mut router = Router::new();

    router.get("index", handle_index);
    router.get("hello", handle_hello);
    router.post("test", handle_test);

    router.get("signin", handle_signin);
    router.post("signup", handle_signup);

    router.get("login", handle_login);
    router.post("logup", handle_logup);
    router.get("logout", handle_logout);

    let needs_auth = Some(vec!["/index", "/hello"]);
    router.up("192.168.33.10:8000", needs_auth);
}


fn handle_index() -> HttpResponse {
    let mut content = HashMap::new();
    content.insert(
        "title",
        "This is index page!",
    );
    let html = render_template_from_file("index", &content);
    HttpResponse::new(200, &html)
}

fn handle_hello() -> HttpResponse {
    let mut data = HashMap::new();
    data.insert("name", "taro");
    let html = render_template_from_file("hello", &data);
    HttpResponse::new(200, &html)
}

fn handle_signin() -> HttpResponse {
    let html = render_template_from_file_empty("signin");
    HttpResponse::new(200, &html)
}

fn handle_signup(body: String) -> HttpResponse {
    let form =  parse(&body);
    let binding = "<unknown>".to_string();
    let name = form.get("name").unwrap_or(&binding);
    let password = form.get("password").unwrap_or(&binding);
    let _csrf_token = form.get("csrf_token").unwrap_or(&"".to_string());
    let redirect_path = "/index";

    signup(name, password, redirect_path)
}

fn handle_login() -> HttpResponse {
    let html = render_template_from_file_empty("login");
    HttpResponse::new(200, &html)
}

fn handle_logup(body: String) -> HttpResponse {
    let form =  parse(&body);
    let binding = "<unknown>".to_string();
    let name = form.get("name").unwrap_or(&binding);
    let password = form.get("password").unwrap_or(&binding);

    let login_success_path = "/index";
    let login_failure_path = "/login";

    logup(name, password, login_success_path, login_failure_path)
}

fn handle_test(_body: String) -> HttpResponse {
    redirect("/index");
    HttpResponse::new(200, "<h1>This is Test!</h1>")
}

fn handle_logout() -> HttpResponse {
    let html = render_template_from_file_empty("logout");
    HttpResponse::new(200, &html)
}

以下のようにする

fn handle_login() -> HttpResponse {
    let (csrf_token, mut response) = create_csrf_token();
    let mut context = HashMap::new();
    context.insert("csrf_token", csrf_token);

    let html = render_template_from_file("login", &context);
    response.body = html;
    response
}
pub fn create_csrf_token() -> (&'static str, HttpResponse) {
    let (session_token, csrf_token) = create_session_token_and_csrf();
    if let Ok(mut client) = psql_connect() {
        let _ = client.execute(
            "INSERT INTO ares_sessions (session_token, csrf_token) VALUES ($1, $2)",
            &[&session_token, &csrf_token],
        );
    }
    let mut response = HttpResponse::new(200, "");
    response.headers.insert(
        "Set-Cookie".to_string(),
        format!("session_token={}; Path=/; HttpOnly", session_token),
    );
    let csrf_token_leaked = Box::leak(csrf_token.into_boxed_str());
    (csrf_token_leaked, response)
}

CSRFとセッションのチェックはライブラリ側で行いたい

【Rust】フレームワーク自作: セッション管理はフレームワーク側で実行する

ユーザ側は必要最低限の実装で済むようにする

use std::collections::HashMap;
mod ares;
use ares::{Router, HttpResponse, parse, signup, logup, redirect};

fn main() {
    let mut router = Router::new();

    router.get("index", handle_index);
    router.get("hello", handle_hello);
    router.get("signin", handle_signin);
    router.post("signup", handle_signup);
    router.get("login", handle_login);
    router.post("logup", handle_logup);
    router.post("test", handle_test);

    let needs_auth = Some(vec!["/index", "/hello"]);
    router.up("192.168.33.10:8000", needs_auth);
}


fn handle_index() -> Option<HashMap<&'static str, &'static str>> {

    let mut content1 = HashMap::new();
    content1.insert(
        "title",
        "This is index page!",
    );
    return Some(content1);
}

fn handle_hello() -> Option<HashMap<&'static str, &'static str>> {
    return None;
}

fn handle_signin() -> Option<HashMap<&'static str, &'static str>> {
    return None;
}

fn handle_signup(body: String) -> HttpResponse {
    let form =  parse(&body);
    let binding = "<unknown>".to_string();
    let name = form.get("name").unwrap_or(&binding);
    let password = form.get("password").unwrap_or(&binding);
    let redirect_path = "/index";

    signup(name, password, redirect_path)
}

fn handle_login() -> Option<HashMap<&'static str, &'static str>> {
    return None;
}

fn handle_logup(body: String) -> HttpResponse {
    let form =  parse(&body);
    let binding = "<unknown>".to_string();
    let name = form.get("name").unwrap_or(&binding);
    let password = form.get("password").unwrap_or(&binding);

    let login_success_path = "/index";
    let login_failure_path = "/login";

    logup(name, password, login_success_path, login_failure_path)
}

fn handle_test(_body: String) -> HttpResponse {
    redirect("/index");
    HttpResponse::new(200, "<h1>This is Test!</h1>")
}

大分簡素になりました。

【Rust】フレームワーク自作: セッションが無ければリダイレクト処理

fn handle_connection(mut stream: TcpStream, get_routes: HashMap<String, String>, post_routes: HashMap<String, fn(String) -> HttpResponse>)  {
    let mut buffer = [0; 1024];
    stream.read(&mut buffer).unwrap();
 
    let request = String::from_utf8_lossy(&buffer);
    let request_line = request.lines().next().unwrap_or("");
    let mut parts = request_line.split_whitespace();
    let method = parts.next().unwrap_or("");
    let path = parts.next().unwrap_or("/");

    // println!("Received {} request for {}", method, path);

    let needs_auth = ["/index", "/hello"];
    let is_protected = needs_auth.contains(&path);

    let session_token = extract_cookie_token(&request);

    if is_protected && session_token.is_none() {
        let redirect_response = http_response_custom(redirect("/login"));
        stream.write_all(redirect_response.as_bytes()).unwrap();
        stream.flush().unwrap();
        return;
    }
 

    let response = match method {
        "GET" => {
            let body = get_routes.get(path).cloned().unwrap_or_else(|| "<h1>404 Not Found</h1>".to_string());
            http_response(200, "text/html", &body)
        }
        "POST" => {
            let body = request.split("\r\n\r\n").nth(1).unwrap_or("").to_string();
            match post_routes.get(path) {
                Some(handler) => {
                    let response = handler(body);
                    http_response_custom(response)
                },
                None => http_response(404, "text/html", "<h1>404 Not Found</h1>"),
            }
        }
        _ => http_response(405, "text/html", "<h1>405 Method Not Allowed</h1>"),
    };
 
    stream.write_all(response.as_bytes()).unwrap();
    stream.flush().unwrap();
 }

main.rs

use std::collections::HashMap;
use rand::SeedableRng; // トレイトは rand にある
use rand::rngs::OsRng;
use rand::RngCore;
use rand_chacha::ChaCha8Rng;
use pwhash::bcrypt;
mod ares;
use ares::{Router, HttpResponse, redirect, psql_connect, parse, remove_null_bytes, create_session_token};

fn main() {
    let mut router = Router::new();

    router.get("index", handle_index);
    router.get("hello", handle_hello);
    router.get("signin", handle_signin);
    router.post("signup", handle_signup);
    router.get("login", handle_login);
    router.post("logup", handle_logup);

    router.up("192.168.33.10:8000");
}


fn handle_index() -> Option<HashMap<&'static str, &'static str>> {

    let mut content1 = HashMap::new();
    content1.insert(
        "title",
        "This is index page!",
    );
    return Some(content1);
}

fn handle_hello() -> Option<HashMap<&'static str, &'static str>> {
    return None;
}

fn handle_signin() -> Option<HashMap<&'static str, &'static str>> {
    return None;
}

fn handle_signup(body: String) -> HttpResponse {
    let form =  parse(&body);
    let binding = "<unknown>".to_string();
    let name = form.get("name").unwrap_or(&binding);
    let password = form.get("password").unwrap_or(&binding);

    let pass_hash = bcrypt::hash(password).unwrap();

    let mut client = psql_connect().unwrap();
    let _ = client.execute(
        "INSERT INTO test (username, password) VALUES ($1, $2)",
        &[&remove_null_bytes(&name), &remove_null_bytes(&pass_hash)],
    ).unwrap();
    HttpResponse::new(200, "<h1>Success Signin!</h1>")
}

fn handle_login() -> Option<HashMap<&'static str, &'static str>> {
    return None;
}

fn handle_logup(body: String) -> HttpResponse {
    let form =  parse(&body);
    let binding = "<unknown>".to_string();
    let name = form.get("name").unwrap_or(&binding);
    let password = form.get("password").unwrap_or(&binding);

    let mut client = psql_connect().unwrap();
    let row = client.query(
        "SELECT * from test where username=$1",
        &[&name],
    ).unwrap();
    if row.is_empty() {
        return redirect("/login");
    }
    let value: String = row[0].get(2);

    if bcrypt::verify(password, &value) {
        let session_token = create_session_token();
        println!("{}", session_token);
        let mut response = HttpResponse::new(200, "<h1>Success Login!</h1>");
        response.headers.insert(
            "Set-Cookie".to_string(),
            format!("session_token={}; Path=/; HttpOnly", session_token),
        );
        response
    } else {
        redirect("/login")
    }
}

– どのページをログイン必須にするかは、ユーザ側で決めたい
– cookieのセットはフレームワーク側で行いたい

【Rust】フレームワーク自作: HttpResponse, Redirectを実装する

利用側: main.rs

use std::collections::HashMap;
use pwhash::bcrypt;
mod ares;
use ares::{Router, HttpResponse, redirect, psql_connect, parse, remove_null_bytes};

fn main() {
    let mut router = Router::new();

    router.get("index", handle_index);
    router.get("hello", handle_hello);
    router.get("signin", handle_signin);
    router.post("signup", handle_signup);
    router.get("login", handle_login);
    router.post("logup", handle_logup);

    router.up("192.168.33.10:8000");
}


fn handle_index() -> Option<HashMap<&'static str, &'static str>> {

    let mut content1 = HashMap::new();
    content1.insert(
        "title",
        "This is index page!",
    );
    return Some(content1);
}

fn handle_hello() -> Option<HashMap<&'static str, &'static str>> {
    return None;
}

fn handle_signin() -> Option<HashMap<&'static str, &'static str>> {
    return None;
}

fn handle_signup(body: String) -> HttpResponse {
    let form =  parse(&body);
    let binding = "<unknown>".to_string();
    let name = form.get("name").unwrap_or(&binding);
    let password = form.get("password").unwrap_or(&binding);

    let pass_hash = bcrypt::hash(password).unwrap();

    let mut client = psql_connect().unwrap();
    let _ = client.execute(
        "INSERT INTO test (username, password) VALUES ($1, $2)",
        &[&remove_null_bytes(&name), &remove_null_bytes(&pass_hash)],
    ).unwrap();
    HttpResponse::new(200, "<h1>Success Signin!</h1>")
}

fn handle_login() -> Option<HashMap<&'static str, &'static str>> {
    return None;
}

fn handle_logup(body: String) -> HttpResponse {
    let form =  parse(&body);
    let binding = "<unknown>".to_string();
    let name = form.get("name").unwrap_or(&binding);
    let password = form.get("password").unwrap_or(&binding);

    let mut client = psql_connect().unwrap();
    let row = client.query(
        "SELECT * from test where username=$1",
        &[&name],
    ).unwrap();
    let value: String = row[0].get(2);
    if bcrypt::verify(password, &value) {
        HttpResponse::new(200, "<h1>Success Login!</h1>")
    } else {
        redirect("/login")
    }
}

フレームワーク側

use std::fs;
use std::io::prelude::*;
use std::net::{TcpListener, TcpStream};
use std::collections::HashMap;
use postgres::{Client, NoTls};
use std::env;
use dotenv::dotenv;

pub struct HttpResponse {
    pub status_code: u16,
    pub headers: HashMap<String, String>,
    pub body: String,
}

impl HttpResponse {
    pub fn new(status_code: u16, body: &str) -> Self {
        let mut headers = HashMap::new();
        headers.insert("Content-Type".to_string(), "text/html".to_string());

        HttpResponse {
            status_code,
            headers,
            body: body.to_string(),
        }
    }
}

pub struct Router {
    get_routes: HashMap<String, String>,
    post_routes: HashMap<String, fn(String) -> HttpResponse>,
}

impl Router {

    pub fn new() -> Self {
        Router {
            get_routes: HashMap::new(),
            post_routes: HashMap::new(),
        }
    }

    pub fn get(&mut self, path: &str, f: fn()-> Option<HashMap<&'static str, &'static str>>){
        // let mut content: Option<HashMap<&str, &str>> = Some(HashMap::new());
        let content = f();

        let temp_path = format!("./templates/{}.html", path);
        let mut html = fs::read_to_string(temp_path).unwrap();

        match content {
            Some(data) => {
                for (key, value) in data {
                    let k = format!("{{{{ {} }}}}", key);
                    html = html.replace(&k, value);
                }
            },
            None => {},
        }
        let route_path = format!("/{}", path);
        self.get_routes.insert(route_path, html);
    }

    pub fn post(&mut self, path: &str, handler: fn(String) -> HttpResponse) {
        self.post_routes.insert(format!("/{}", path), handler);
    }

    pub fn up(&self, ip: &str) {
        let listenr = TcpListener::bind(ip).unwrap();
        for stream in listenr.incoming() {
            match stream {
                Ok(stream) => {
                    let _ = handle_connection(
                        stream, 
                        self.get_routes.clone(),
                        self.post_routes.clone(),
                    );
                }
                Err(e) => {
                    println!("Connection failed: {}", e);
                }
            }
        }
    }
}

fn handle_connection(mut stream: TcpStream, get_routes: HashMap<String, String>, post_routes: HashMap<String, fn(String) -> HttpResponse>)  {
    let mut buffer = [0; 1024];
    stream.read(&mut buffer).unwrap();
 
    let request = String::from_utf8_lossy(&buffer);
    let request_line = request.lines().next().unwrap_or("");
    let mut parts = request_line.split_whitespace();
    let method = parts.next().unwrap_or("");
    let path = parts.next().unwrap_or("/");

    // println!("Received {} request for {}", method, path);
 

    let response = match method {
        "GET" => {
            let body = get_routes.get(path).cloned().unwrap_or_else(|| "<h1>404 Not Found</h1>".to_string());
            http_response(200, "text/html", &body)
        }
        "POST" => {
            let body = request.split("\r\n\r\n").nth(1).unwrap_or("").to_string();
            match post_routes.get(path) {
                Some(handler) => {
                    let response = handler(body);
                    http_response_custom(response)
                },
                None => http_response(404, "text/html", "<h1>404 Not Found</h1>"),
            }
        }
        _ => http_response(405, "text/html", "<h1>405 Method Not Allowed</h1>"),
    };
 
    stream.write_all(response.as_bytes()).unwrap();
    stream.flush().unwrap();
 }
 
 fn http_response(status_code: u16, content_type: &str, body: &str) -> String {
    format!(
        "HTTP/1.1 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\n\r\n{}",
        status_code,
        get_status_text(status_code),
        content_type,
        body.len(),
        body
    )
 }
 
 fn http_response_custom(resp: HttpResponse) -> String {
    let mut response = format!(
        "HTTP/1.1 {} {}\r\n",
        resp.status_code,
        get_status_text(resp.status_code)
    );

    let mut has_location = false;
    for (k, v) in &resp.headers {
        if k.to_lowercase() == "location" {
            has_location = true;
        }
        response.push_str(&format!("{}: {}\r\n", k, v));
    }

    if !has_location {
        response.push_str("Content-Type: text/html\r\n");
    }

    response.push_str(&format!("Content-Length: {}\r\n", resp.body.len()));
    response.push_str("\r\n"); 

    response.push_str(&resp.body);

    response
}


 fn get_status_text(code: u16) -> &'static str {
    match code {
        200 => "OK",
        404 => "Not Found",
        _ => "Unknown",
    }
 }

pub fn parse(body: &str) -> HashMap<String, String> {
    let mut data = HashMap::new();

    for pair in body.split('&') {
        let mut iter = pair.splitn(2, '=');
        if let (Some(key), Some(value)) = (iter.next(), iter.next()) {
            let key = url_decode(key);
            let value = url_decode(value);
            data.insert(key, value);
        }
    }
    data
}

fn url_decode(s: &str) -> String {
    s.replace("+", " ")
        .replace("%20", " ") 
}

pub fn psql_connect() -> Result<Client, Box<dyn std::error::Error>> {
    let _ = dotenv();
    let conn_str = format!("host=localhost user=postgres password={}", env::var("PSQL_PASSWORD").unwrap());
    let client = Client::connect(&conn_str, NoTls)?;

    Ok(client)
}

pub fn redirect(location: &str) -> HttpResponse {
    let mut headers = HashMap::new();
    headers.insert("Location".to_string(), location.to_string());

    HttpResponse {
        status_code: 302,
        headers,
        body: String::new(), // もしくは軽いメッセージを入れてもOK
    }
}

pub fn remove_null_bytes(s: &str) -> String {
    s.chars().filter(|&c| c != '\0').collect()
}

これに、cookieの機能をつけたい

サッカーゲームをRustで書く

サッカーゲームというより、単なるPK戦だなぁ〜
もう少しリアリティのあるゲームにしたい…

use std::io;
use rand::Rng;

struct Game {
   player_score: i32,
   cpu_score: i32,
}

impl Game {

   fn new() -> Self {
      Game {
         player_score: 0,
         cpu_score: 0,
      }
   }

   fn player_attack() -> i32 {
      println!("攻撃! シュートする方向を選んでください:");
      println!("1: 左\n2: 中央\n3: 右");
      let mut input = String::new();
      io::stdin()
         .read_line(&mut input)
         .expect("入力の読み取りに失敗しました");
      let dir: i32 = match input.trim().parse() {
         Ok(num) => num,
         Err(_) => {
            println!("無効な入力です。数字を入力してください。");
            return 0;
         }
      };
      return dir;
   }

   fn cpu_defend() -> i32 {
      let mut rng = rand::thread_rng();
      return rng.gen_range(1..=3);
   }

   fn cpu_attack() -> i32 {
      let mut rng = rand::thread_rng();
      return rng.gen_range(1..=3);
   }

   fn player_defend() -> i32 {
      println!("守備!相手のシュートを読む方向を選んでください:");
      println!("1: 左\n2: 中央\n3: 右");
      let mut input = String::new();
      io::stdin()
         .read_line(&mut input)
         .expect("入力の読み取りに失敗しました");
      let dir: i32 = match input.trim().parse() {
         Ok(num) => num,
         Err(_) => {
            println!("無効な入力です。数字を入力してください。");
            return 0;
         }
      };
      return dir;
   }
}

fn main() {
   let mut game1 = Game::new();
   println!("{}", game1.player_score);

   for turn in 1..4 {
      println!("--- 第{}ターン ---", turn);
      let player_dir = Game::player_attack();
      let cpu_dir = Game::cpu_defend();

      if player_dir != cpu_dir {
         println!("ゴール!! 🎉");
         game1.player_score += 1;
      } else {
         println!("セーブされました...🧤");
      }

      let cpu_dir = Game::cpu_attack();
      let player_dir = Game::player_defend();

      if player_dir != cpu_dir {
         println!("ゴール!! 😱");
         game1.cpu_score += 1;
      } else {
         println!("ナイスセーブ!! 🧤");
      }
   }

   println!("=== 試合終了 ===");

   println!("あなた: {}, CPU: {}", game1.player_score, game1.cpu_score);
}

— 第1ターン —
攻撃! シュートする方向を選んでください:
1: 左
2: 中央
3: 右
1
ゴール!! 🎉
守備!相手のシュートを読む方向を選んでください:
1: 左
2: 中央
3: 右
2
ゴール!! 😱
— 第2ターン —
攻撃! シュートする方向を選んでください:
1: 左
2: 中央
3: 右
1
ゴール!! 🎉
守備!相手のシュートを読む方向を選んでください:
1: 左
2: 中央
3: 右
1
ゴール!! 😱
— 第3ターン —
攻撃! シュートする方向を選んでください:
1: 左
2: 中央
3: 右
1
セーブされました…🧤
守備!相手のシュートを読む方向を選んでください:
1: 左
2: 中央
3: 右
1
ゴール!! 😱
=== 試合終了 ===
あなた: 2, CPU: 3

サッカーゲーム

import random

def player_attack():
    print("攻撃!シュートする方向を選んでください:")
    print("1: 左\n2: 中央\n3: 右")
    try:
        choice = int(input("あなたの選択: "))
        if choice not in [1, 2, 3]:
            raise ValueError
    except ValueError:
        print("無効な入力です。中央にします。")
        choice = 2
    return choice

def cpu_defend():
    return random.randint(1, 3)

def cpu_attack():
    return random.randint(1, 3)

def player_defend():
    print("守備!相手のシュートを読む方向を選択してください:")
    print("1: 左\n2: 中央\n3: 右")
    try:
        choice = int(input("あなたの選択: "))
        if choice not in [1, 2, 3]:
            raise ValueError
    except ValueError:
        print("無効な入力です。中央にします。")
        choice = 2
    return choice

def game():
    player_score = 0
    cpu_score = 0

    print("== サッカー対決: 3ターンマッチ ==")
    for turn in range(1, 4):
        print(f"\n--- 第{turn}ターン ---")

        attack_dir = player_attack()
        defend_dir = cpu_defend()
        if attack_dir != defend_dir:
            print("ゴール!! 🎉")
            player_score += 1
        else:
            print("セーブされた!")

        attack_dir = cpu_attack()
        defend_dir = player_defend()
        if attack_dir != defend_dir:
            print("CPUがゴール!! 😱")
            cpu_score += 1
        else:
            print("ナイスセーブ!! 🎉")

    print(f"\n== 試合終了==\n あなた: {player_score}点\n CPU:{cpu_score}点")
    if player_score > cpu_score:
        print("勝利")
    elif player_score < cpu_score:
        print("敗北")
    else:
        print("引き分け")

if __name__ == "__main__":
    game()

$ python3 football.py
== サッカー対決: 3ターンマッチ ==

— 第1ターン —
攻撃!シュートする方向を選んでください:
1: 左
2: 中央
3: 右
あなたの選択: 1
ゴール!! 🎉
守備!相手のシュートを読む方向を選択してください:
1: 左
2: 中央
3: 右
あなたの選択: 2
CPUがゴール!! 😱

— 第2ターン —
攻撃!シュートする方向を選んでください:
1: 左
2: 中央
3: 右
あなたの選択: 1
ゴール!! 🎉
守備!相手のシュートを読む方向を選択してください:
1: 左
2: 中央
3: 右
あなたの選択: 3
ナイスセーブ!! 🎉

— 第3ターン —
攻撃!シュートする方向を選んでください:
1: 左
2: 中央
3: 右
あなたの選択: 2
セーブされた!
守備!相手のシュートを読む方向を選択してください:
1: 左
2: 中央
3: 右
あなたの選択: 1
ナイスセーブ!! 🎉

== 試合終了==
あなた: 2点
CPU:1点
勝利

【Rust】フレームワーク自作: Postメソッドを実装する

getとpostを分ける

use std::fs;
use std::io;
use std::io::prelude::*;
use std::net::{TcpListener, TcpStream};
use std::collections::HashMap;
use crate::main;

pub struct Router {
    get_routes: HashMap<String, String>,
    post_routes: HashMap<String, fn(String) -> String>,
}

impl Router {

    pub fn new() -> Self {
        Router {
            get_routes: HashMap::new(),
            post_routes: HashMap::new(),
        }
    }

    pub fn add_get(&mut self, path: &str, f: fn()-> Option<HashMap<&'static str, &'static str>>){
        let mut content: Option<HashMap<&str, &str>> = Some(HashMap::new());
        content = f();

        let temp_path = format!("./templates/{}.html", path);
        let mut html = fs::read_to_string(temp_path).unwrap();

        match content {
            Some(data) => {
                for (key, value) in data {
                    let k = format!("{{{{ {} }}}}", key);
                    html = html.replace(&k, value);
                }
            },
            None => {},
        }
        let route_path = format!("/{}", path);
        self.get_routes.insert(route_path, html);
    }

    pub fn add_post(&mut self, path: &str, handler: fn(String) -> String) {
        self.post_routes.insert(format!("/{}", path), handler);
    }

    pub fn up(&self, ip: &str) {
        let listenr = TcpListener::bind(ip).unwrap();
        for stream in listenr.incoming() {
            match stream {
                Ok(stream) => {
                    let _ = handle_connection(
                        stream, 
                        self.get_routes.clone(),
                        self.post_routes.clone(),
                    );
                }
                Err(e) => {
                    println!("Connection failed: {}", e);
                }
            }
        }
    }
}

fn handle_connection(mut stream: TcpStream, get_routes: HashMap<String, String>, post_routes: HashMap<String, fn(String) -> String>)  {
    let mut buffer = [0; 1024];
    stream.read(&mut buffer).unwrap();
 
    let request = String::from_utf8_lossy(&buffer);
    let request_line = request.lines().next().unwrap_or("");
    let mut parts = request_line.split_whitespace();
    let method = parts.next().unwrap_or("");
    let path = parts.next().unwrap_or("/");

    println!("Received {} request for {}", method, path);
 

    let response_body = match method {
        "GET" => get_routes.get(path).cloned().unwrap_or_else(|| "<h1>404 Not Found</h1>".to_string()),
        "POST" => {
            let body = request.split("\r\n\r\n").nth(1).unwrap_or("").to_string();
            match post_routes.get(path) {
                Some(handler) => handler(body),
                None => "<h1>404 Not Found</h1>".to_string(),
            }
        }
        _ => "<h1>405 Method Not Allowed</h1>".to_string(),
    };
 
    let response = http_response(200, "text/html", &response_body);
 
    stream.write_all(response.as_bytes()).unwrap();
    stream.flush().unwrap();
 }
 
 fn http_response(status_code: u16, content_type: &str, body: &str) -> String {
    format!(
        "HTTP/1.1 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\n\r\n{}",
        status_code,
        get_status_text(status_code),
        content_type,
        body.len(),
        body
    )
 }
 
 fn get_status_text(code: u16) -> &'static str {
    match code {
        200 => "OK",
        404 => "Not Found",
        _ => "Unknown",
    }
 }

### 利用側

use std::collections::HashMap;
mod ares;

fn main() {
    let mut router = ares::Router::new();

    router.add_get("index", handle_index);
    router.add_get("hello", handle_hello);
    router.add_get("world", handle_world);
    router.add_post("submit", handle_post);

    router.up("192.168.33.10:8000");
}


fn handle_index() -> Option<HashMap<&'static str, &'static str>> {

    let mut content1 = HashMap::new();
    content1.insert(
        "title",
        "index page",
    );
    return Some(content1);
}

fn handle_hello() -> Option<HashMap<&'static str, &'static str>> {

    return None;
}

fn handle_world() -> Option<HashMap<&'static str, &'static str>> {

    return None;
}

fn handle_post(body: String) -> String {
    println!("{}", body);
    format!("<h1>POST 受信しました</h1><p>内容: {}</p>", body)
}

ほう…

【Rust】関数名を引数にして関数を呼び出したい

fn()とすることで、引数を関数にすることができる。

fn main() {
    run_function(handle_index);
}

fn run_function(f: fn()-> Option<HashMap<&'static str, &'static str>>) {
    let mut content: Option<HashMap<&str, &str>> = Some(HashMap::new());
    content = f();
    match content {
        Some(data) => {
            println!("{:?}", data);
        },
        None => {},
    }
}

fn handle_index() -> Option<HashMap<&'static str, &'static str>> {

    let mut content1 = HashMap::new();
    content1.insert(
        "title",
        "index page",
    );
    return Some(content1);
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
Running `target/debug/ares`
{“title”: “index page”}

なるほどー、中々やるやんけ

Rustで簡単なフレームワークを自作したい5

main.rs

use std::collections::HashMap;
mod ares;

fn main() {
    
    let mut content = HashMap::new();
    content.insert(
        "title",
        "index page",
    );
    content.insert(
        "data1",
        "10000",
    );

    let _ = ares::Router::new("index", Some(content));
    
}

### フレームワーク側
ares.rs

use std::fs;
use std::io;
use std::io::prelude::*;
use std::net::{TcpListener, TcpStream};
use std::collections::HashMap;

pub struct Router;

impl Router {
    pub fn new(path: &str, content: Option<HashMap<&str, &str>>) {

        let mut routes = HashMap::new();
        let temp_path = format!("./templates/{}.html", path);
        let mut html = fs::read_to_string(temp_path).unwrap();

        match content {
            Some(data) => {
                for (key, value) in data {
                    let k = format!("{{{{ {} }}}}", key);
                    html = html.replace(&k, value);
                }
            },
            None => {},
        }
        let route_path = format!("/{}", path);
        routes.insert(
            route_path, html,
        );
        println!("{:?}", routes);


        let listenr = TcpListener::bind("192.168.33.10:8000").unwrap();
        for stream in listenr.incoming() {
            match stream {
                Ok(stream) => {
                    let _ = handle_connection(stream, routes.clone());
                }
                Err(e) => {
                    println!("Connection failed: {}", e);
                }
            }
        }
    }
}

fn handle_connection(mut stream: TcpStream, routes: HashMap<String, String>)  {
    let mut buffer = [0; 1024];
    stream.read(&mut buffer).unwrap();
 
    let request = String::from_utf8_lossy(&buffer);
    let request_line = request.lines().next().unwrap_or("");
    println!("Received request:\n{}", request);
 
    let path = request_line
         .split_whitespace()
         .nth(1)
         .unwrap_or("/");
    
    let response_body = routes.get(path)
         .cloned()
         .unwrap_or_else(|| "<h1>404 Not Found</h1>".to_string());
 
    let response = http_response(200, "text/html", &response_body);
 
    stream.write_all(response.as_bytes()).unwrap();
    stream.flush().unwrap();
 }
 
 fn http_response(status_code: u16, content_type: &str, body: &str) -> String {
    format!(
        "HTTP/1.1 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\n\r\n{}",
        status_code,
        get_status_text(status_code),
        content_type,
        body.len(),
        body
    )
 }
 
 fn get_status_text(code: u16) -> &'static str {
    match code {
        200 => "OK",
        404 => "Not Found",
        _ => "Unknown",
    }
 }

複数パスに対応できるようにしたい。

### 複数パスに対応
hashmapにパスとコンテンツを追加するように実装する

use std::collections::HashMap;
mod ares;

fn main() {
    let mut router = ares::Router::new();

    let mut content1 = HashMap::new();
    content1.insert(
        "title",
        "index page",
    );

    let mut content2 = HashMap::new();
    content2.insert(
        "title",
        "this is title",
    );

    router.add("index", Some(content1));
    router.add("hello", None);
    router.add("world", Some(content2));

    router.up("192.168.33.10:8000");   
}
pub struct Router {
    route: HashMap<String, String>,
}

impl Router {

    pub fn new() -> Self {
        Router {
            route: HashMap::new(),
        }
    }

    pub fn add(&mut self, path: &str, content: Option<HashMap<&str, &str>>){
        let temp_path = format!("./templates/{}.html", path);
        let mut html = fs::read_to_string(temp_path).unwrap();

        match content {
            Some(data) => {
                for (key, value) in data {
                    let k = format!("{{{{ {} }}}}", key);
                    html = html.replace(&k, value);
                }
            },
            None => {},
        }
        let route_path = format!("/{}", path);
        self.route.insert(route_path, html);
    }

    pub fn up(&self, ip: &str) {
        let listenr = TcpListener::bind(ip).unwrap();
        for stream in listenr.incoming() {
            match stream {
                Ok(stream) => {
                    let _ = handle_connection(stream, self.route.clone());
                }
                Err(e) => {
                    println!("Connection failed: {}", e);
                }
            }
        }
    }
}

MVCモデルにしたい…