使用Rust和WebAssembly构建Web应用程序


无论是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