Skip to main content

euv_cli/server/
impl.rs

1use crate::*;
2
3/// Implements `ServerHook` for `RequestMiddleware` to inject cache-control headers.
4impl ServerHook for RequestMiddleware {
5    /// Creates a new `RequestMiddleware` instance.
6    ///
7    /// # Arguments
8    /// - `&mut Stream` - The connection stream (unused).
9    /// - `&mut Context` - The request context (unused).
10    ///
11    /// # Returns
12    /// - `RequestMiddleware` - A new instance with no internal state.
13    async fn new(_: &mut Stream, _ctx: &mut Context) -> Self {
14        Self
15    }
16
17    /// Injects cache-control headers to prevent stale WASM assets during development.
18    ///
19    /// Sets `Cache-Control: no-cache, no-store, must-revalidate`,
20    /// `Pragma: no-cache`, and `Expires: 0` on the response.
21    ///
22    /// # Arguments
23    /// - `self` - The consumed middleware instance.
24    /// - `&mut Stream` - The connection stream (unused).
25    /// - `&mut Context` - The request context used to set response headers.
26    ///
27    /// # Returns
28    /// - `Status` - The hook processing result.
29    async fn handle(self, _: &mut Stream, ctx: &mut Context) -> Status {
30        ctx.get_mut_response()
31            .set_status_code(200)
32            .set_header(CACHE_CONTROL, NO_CACHE_NO_STORE_MUST_REVALIDATE)
33            .set_header(PRAGMA, NO_CACHE)
34            .set_header(EXPIRES, EXPIRES_DISABLED);
35        Status::Continue
36    }
37}
38
39/// Implements `ServerHook` for `ResponseMiddleware` to serialize and send the response.
40impl ServerHook for ResponseMiddleware {
41    /// Creates a new `ResponseMiddleware` instance.
42    ///
43    /// # Arguments
44    /// - `&mut Stream` - The connection stream (unused).
45    /// - `&mut Context` - The request context (unused).
46    ///
47    /// # Returns
48    /// - `ResponseMiddleware` - A new instance with no internal state.
49    async fn new(_: &mut Stream, _ctx: &mut Context) -> Self {
50        Self
51    }
52
53    /// Builds the HTTP response bytes and sends them through the connection stream.
54    ///
55    /// If the send fails, marks the stream as closed and rejects the request.
56    ///
57    /// # Arguments
58    /// - `self` - The consumed middleware instance.
59    /// - `&mut Stream` - The connection stream used to send the response bytes.
60    /// - `&mut Context` - The request context used to build the response.
61    ///
62    /// # Returns
63    /// - `Status` - The hook processing result.
64    async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
65        let response: Vec<u8> = ctx.get_mut_response().build();
66        if stream.try_send(&response).await.is_err() {
67            stream.set_closed(true);
68            return Status::Reject;
69        }
70        Status::Continue
71    }
72}
73
74/// Implements `ServerHook` for `IndexRoute` to serve the development HTML and static assets.
75///
76/// When the request targets `index.html`, returns the in-memory HTML
77/// that has the live-reload script injected. For all other files the
78/// handler reads the content from disk with path-traversal protection.
79impl ServerHook for IndexRoute {
80    /// Creates a new `IndexRoute` instance.
81    ///
82    /// # Arguments
83    /// - `&mut Stream` - The connection stream (unused).
84    /// - `&mut Context` - The request context (unused).
85    ///
86    /// # Returns
87    /// - `IndexRoute` - A new instance with no internal state.
88    async fn new(_: &mut Stream, _ctx: &mut Context) -> Self {
89        Self
90    }
91
92    /// Handles requests for the index page and static assets with path-traversal protection.
93    ///
94    /// - Empty path or `index.html` - serves the in-memory HTML with live-reload script.
95    /// - Other paths: reads the file from disk, validates the canonical path is within
96    ///   the www directory, and sets the appropriate `Content-Type` header.
97    ///
98    /// # Arguments
99    /// - `self` - The consumed route instance.
100    /// - `&mut Stream` - The connection stream (unused).
101    /// - `&mut Context` - The request context used to read route params and write the response.
102    ///
103    /// # Returns
104    /// - `Status` - The hook processing result.
105    ///
106    /// # Panics
107    ///
108    /// Does not panic; all error cases set an appropriate HTTP status code.
109    async fn handle(self, _: &mut Stream, ctx: &mut Context) -> Status {
110        let path_opt: Option<String> = ctx.try_get_route_param("path");
111        let path: String = path_opt.unwrap_or_default();
112        if path.contains("..") || path.starts_with('/') || path.starts_with('\\') {
113            ctx.get_mut_response().set_status_code(403);
114            return Status::Continue;
115        }
116        let state: Arc<AppState> = match get_global_state() {
117            Some(state) => state,
118            None => {
119                ctx.get_mut_response().set_status_code(500);
120                return Status::Continue;
121            }
122        };
123        if path.is_empty() || path == INDEX_HTML_FILE_NAME {
124            let html: String = state.get_html_content().read().await.clone();
125            ctx.get_mut_response()
126                .set_body(&html)
127                .set_header(CONTENT_TYPE, TEXT_HTML);
128            return Status::Continue;
129        }
130        let serving_root: PathBuf = resolve_serving_root(state.get_args()).await;
131        let file_path: PathBuf = match resolve_file_in_base(&serving_root, &path).await {
132            Some(resolved) => resolved,
133            None => {
134                ctx.get_mut_response().set_status_code(404);
135                return Status::Continue;
136            }
137        };
138        match read(&file_path).await {
139            Ok(content) => {
140                let extension: String = FileExtension::get_extension_name(&path);
141                let content_type: &'static str =
142                    FileExtension::parse(&extension).get_content_type();
143                ctx.get_mut_response()
144                    .set_body(&content)
145                    .set_header(CONTENT_TYPE, content_type);
146            }
147            Err(_) => {
148                ctx.get_mut_response().set_status_code(404);
149            }
150        }
151        Status::Continue
152    }
153}
154
155/// Implements `ServerHook` for `ReloadRoute` to provide long-polling reload notifications.
156///
157/// Uses long-polling: holds the connection open until a reload event
158/// is broadcast, then returns a single JSON response so the client
159/// can distinguish between a successful rebuild and an error.
160impl ServerHook for ReloadRoute {
161    /// Creates a new `ReloadRoute` instance.
162    ///
163    /// # Arguments
164    /// - `&mut Stream` - The connection stream (unused).
165    /// - `&mut Context` - The request context (unused).
166    ///
167    /// # Returns
168    /// - `ReloadRoute` - A new instance with no internal state.
169    async fn new(_: &mut Stream, _ctx: &mut Context) -> Self {
170        Self
171    }
172
173    /// Waits for a reload event and returns it as a JSON response.
174    ///
175    /// Subscribes to the broadcast channel and blocks until a `ReloadEvent`
176    /// is received, then serializes it as the response body.
177    ///
178    /// # Arguments
179    /// - `self` - The consumed route instance.
180    /// - `&mut Stream` - The connection stream (unused).
181    /// - `&mut Context` - The request context used to write the response.
182    ///
183    /// # Returns
184    /// - `Status` - The hook processing result.
185    async fn handle(self, _: &mut Stream, ctx: &mut Context) -> Status {
186        let state: Arc<AppState> = match get_global_state() {
187            Some(state) => state,
188            None => {
189                ctx.get_mut_response()
190                    .set_status_code(500)
191                    .set_body(ERROR_SERVER_NOT_READY);
192                return Status::Continue;
193            }
194        };
195        let mut rx: broadcast::Receiver<ReloadEvent> = state.get_reload_tx().subscribe();
196        let event: ReloadEvent = match rx.recv().await {
197            Ok(event) => event,
198            Err(_) => {
199                ctx.get_mut_response().set_status_code(503);
200                return Status::Continue;
201            }
202        };
203        let body: String = match serde_json::to_string(&event) {
204            Ok(json) => json,
205            Err(_) => {
206                ctx.get_mut_response().set_status_code(500);
207                return Status::Continue;
208            }
209        };
210        ctx.get_mut_response().set_body(&body);
211        Status::Continue
212    }
213}