无论是React、VueJS、Angular,还是Rust,现代网络应用都是由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