使用Rust和WebAssembly構建Web應用程式

banq發表於2022-06-18

無論是React、VueJS、Angular,還是Rust,現代網路應用都是由3種碎片組成的。
  1. 元件
  2. 頁面
  3. 服務

客戶端網路應用的架構

元件是可重複使用的部件和UI元素。例如,一個輸入欄位,或一個按鈕。

頁面是元件的集合體。它們與路由(URLs)相匹配。例如,登入頁面與/login路線相匹配。主頁與/路線相匹配。

最後,服務是輔助工具,用於包裝低階別的功能或外部服務,如HTTP客戶端、儲存......

我們的應用程式的目標很簡單。這是一個入口網站,受害者將在這裡輸入他們的證書(認為這是一個合法的表格),證書將被儲存在一個SQLite資料庫中,然後我們將受害者重定向到一個錯誤頁面,讓他們認為該服務暫時不可用,他們應該以後再嘗試。

安裝工具鏈
wasm-pack可以幫助你構建Rust生成的WebAssembly包,並在瀏覽器或Node.js中使用它。

$ cargo install -f wasm-pack

模型
請注意,在後端使用與前端相同的語言的一個好處是能夠重複使用模型。

ch_09/phishing/common/src/api.rs

pub mod model {
    use serde::{Deserialize, Serialize};

    #[derive(Debug, Clone, Serialize, Deserialize)]
    #[serde(rename_all = "snake_case")]
    pub struct Login {
        pub email: String,
        pub password: String,
    }

    #[derive(Debug, Clone, Serialize, Deserialize)]
    #[serde(rename_all = "snake_case")]
    pub struct LoginResponse {
        pub ok: bool,
    }
}

pub mod routes {
    pub const LOGIN: &str = "/api/login";
}


元件
一開始,有元件。元件是可重用的功能或設計。
為了構建我們的元件,我們使用yew, crate ,在我寫這篇文章的時候,它是最先進和受支援的 Rust 前端框架。
Properties(或Props)可以看作是一個元件的引數。例如,函式fn factorial(x: u64) -> u64有一個引數x。對於元件,它是同樣的事情。如果我們想用特定資料渲染它們,我們使用Properties.

ch_09/phishing/webapp/src/components/error_alert.rs

use yew::{html, Component, ComponentLink, Html, Properties, ShouldRender};

pub struct ErrorAlert {
    props: Props,
}

#[derive(Properties, Clone)]
pub struct Props {
    #[prop_or_default]
    pub error: Option<crate::Error>,
}

impl Component for ErrorAlert {
    type Message = ();
    type Properties = Props;

    fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
        ErrorAlert { props }
    }

    fn update(&mut self, _: Self::Message) -> ShouldRender {
        true
    }

    fn change(&mut self, props: Self::Properties) -> ShouldRender {
        self.props = props;
        true
    }

    fn view(&self) -> Html {
        if let Some(error) = &self.props.error {
            html! {
                <div class="alert alert-danger" role="alert">
                    {error}
                </div>
            }
        } else {
            html! {}
        }
    }
}

非常類似於(老式)React,不是嗎?
另一個元件是LoginForm包裝邏輯以捕獲和儲存憑據的元件。

ch_09/phishing/webapp/src/components/login_form.rs


最後是view函式(類似於render其他框架)。

  fn view(&self) -> Html {
        let onsubmit = self.link.callback(|ev: FocusEvent| {
            ev.prevent_default(); /* Prevent event propagation */
            Msg::Submit
        });
        let oninput_email = self
            .link
            .callback(|ev: InputData| Msg::UpdateEmail(ev.value));
        let oninput_password = self
            .link
            .callback(|ev: InputData| Msg::UpdatePassword(ev.value));


rorAlert您可以像任何其他 HTML 元素一樣嵌入其他元件(此處):

        html! {
            <div>
                <components::ErrorAlert error=&self.error />
                <form onsubmit=onsubmit>
                    <div class="mb-3">
                        <input
                            class="form-control form-control-lg"
                            type="email"
                            placeholder="Email"
                            value=self.email.clone()
                            oninput=oninput_email
                            id="email-input"
                        />
                    </div>
                    <div class="mb-3">
                        <input
                            class="form-control form-control-lg"
                            type="password"
                            placeholder="Password"
                            value=self.password.clone()
                            oninput=oninput_password
                        />
                    </div>
                    <button
                        class="btn btn-lg btn-primary pull-xs-right"
                        type="submit"
                        disabled=false>
                        { "Sign in" }
                    </button>
                </form>
            </div>
        }
    }
}


頁面
頁面是元件的集合,並且是 yew 中的元件本身。

ch_09/phishing/webapp/src/pages/login.rs

pub struct Login {}

impl Component for Login {
    type Message = ();
    type Properties = ();

    // ...

    fn view(&self) -> Html {
        html! {
            <div>
                <div class="container text-center mt-5">
                    <div class="row justify-content-md-center mb-5">
                        <div class="col col-md-8">
                            <h1>{ "My Awesome intranet" }</h1>
                        </div>
                    </div>
                    <div class="row justify-content-md-center">
                        <div class="col col-md-8">
                            <LoginForm />
                        </div>
                    </div>
                </div>
            </div>
        }
    }
}


路由
然後我們宣告應用程式的所有可能路由。
正如我們之前看到的,路由將 URL 對映到頁面。

ch_09/phishing/webapp/src/lib.rs

#[derive(Switch, Debug, Clone)]
pub enum Route {
    #[to = "*"]
    Fallback,
    #[to = "/error"]
    Error,
    #[to = "/"]
    Login,
}



服務
發出 HTTP 請求
發出 HTTP 請求有點困難,因為我們需要回撥並反序列化響應。

ch_09/phishing/webapp/src/services/http_client.rs

#[derive(Default, Debug)]
pub struct HttpClient {}

impl HttpClient {
    pub fn new() -> Self {
        Self {}
    }

    pub fn post<B, T>(
        &mut self,
        url: String,
        body: B,
        callback: Callback<Result<T, Error>>,
    ) -> FetchTask
    where
        for<'de> T: Deserialize<'de> + 'static + std::fmt::Debug,
        B: Serialize,
    {
        let handler = move |response: Response<Text>| {
            if let (meta, Ok(data)) = response.into_parts() {
                if meta.status.is_success() {
                    let data: Result<T, _> = serde_json::from_str(&data);
                    if let Ok(data) = data {
                        callback.emit(Ok(data))
                    } else {
                        callback.emit(Err(Error::DeserializeError))
                    }
                } else {
                    match meta.status.as_u16() {
                        401 => callback.emit(Err(Error::Unauthorized)),
                        403 => callback.emit(Err(Error::Forbidden)),
                        404 => callback.emit(Err(Error::NotFound)),
                        500 => callback.emit(Err(Error::InternalServerError)),
                        _ => callback.emit(Err(Error::RequestError)),
                    }
                }
            } else {
                callback.emit(Err(Error::RequestError))
            }
        };

        let body: Text = Json(&body).into();
        let builder = Request::builder()
            .method("POST")
            .uri(url.as_str())
            .header("Content-Type", "application/json");
        let request = builder.body(body).unwrap();

        FetchService::fetch(request, handler.into()).unwrap()
    }
}


話雖如此,它的優點是非常健壯,因為所有可能的錯誤都得到了處理。不再有您永遠不會知道的未捕獲的執行時錯誤。

應用程式
然後是App元件,它包裝了所有內容並呈現了路線。

ch_09/phishing/webapp/src/lib.rs

pub struct App {}

impl Component for App {
    type Message = ();
    type Properties = ();

    // ...

    fn view(&self) -> Html {
        let render = Router::render(|switch: Route| match switch {
            Route::Login | Route::Fallback => html! {<pages::Login/>},
            Route::Error => html! {<pages::Error/>},
        });

        html! {
            <Router<Route, ()> render=render/>
        }
    }
}

最後,掛載和啟動 webapp 的入口點:

#[wasm_bindgen(start)]
pub fn run_app() {
    yew::App::<App>::new().mount_to_body();
}



您可以透過執行以下命令來執行新構建的 Web 應用程式:

$ make webapp_debug
$ make serve


程式碼在 GitHub 上
像往常一樣,您可以在 GitHub 上找到程式碼:github.com/skerkour/black-hat-rust

相關文章