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 www_absolute: PathBuf = state
131            .get_args()
132            .get_crate_path()
133            .join(state.get_args().get_www_dir());
134        let www_absolute: PathBuf = resolve_www_dir(&www_absolute).await;
135        let file_path: PathBuf = www_absolute.join(&path);
136        let canonical_path: PathBuf = match canonicalize(&file_path).await {
137            Ok(p) => p,
138            Err(_) => {
139                ctx.get_mut_response().set_status_code(404);
140                return Status::Continue;
141            }
142        };
143        let base_canonical: PathBuf = match canonicalize(&www_absolute).await {
144            Ok(p) => p,
145            Err(_) => {
146                ctx.get_mut_response().set_status_code(500);
147                return Status::Continue;
148            }
149        };
150        if !canonical_path.starts_with(&base_canonical) {
151            ctx.get_mut_response().set_status_code(403);
152            return Status::Continue;
153        }
154        match read(&file_path).await {
155            Ok(content) => {
156                let extension: String = FileExtension::get_extension_name(&path);
157                let content_type: &'static str =
158                    FileExtension::parse(&extension).get_content_type();
159                ctx.get_mut_response()
160                    .set_body(&content)
161                    .set_header(CONTENT_TYPE, content_type);
162            }
163            Err(_) => {
164                ctx.get_mut_response().set_status_code(404);
165            }
166        }
167        Status::Continue
168    }
169}
170
171/// Implements `ServerHook` for `ReloadRoute` to provide long-polling reload notifications.
172///
173/// Uses long-polling: holds the connection open until a reload event
174/// is broadcast, then returns a single JSON response so the client
175/// can distinguish between a successful rebuild and an error.
176impl ServerHook for ReloadRoute {
177    /// Creates a new `ReloadRoute` instance.
178    ///
179    /// # Arguments
180    /// - `&mut Stream` - The connection stream (unused).
181    /// - `&mut Context` - The request context (unused).
182    ///
183    /// # Returns
184    /// - `ReloadRoute` - A new instance with no internal state.
185    async fn new(_: &mut Stream, _ctx: &mut Context) -> Self {
186        Self
187    }
188
189    /// Waits for a reload event and returns it as a JSON response.
190    ///
191    /// Subscribes to the broadcast channel and blocks until a `ReloadEvent`
192    /// is received, then serializes it as the response body.
193    ///
194    /// # Arguments
195    /// - `self` - The consumed route instance.
196    /// - `&mut Stream` - The connection stream (unused).
197    /// - `&mut Context` - The request context used to write the response.
198    ///
199    /// # Returns
200    /// - `Status` - The hook processing result.
201    async fn handle(self, _: &mut Stream, ctx: &mut Context) -> Status {
202        let state: Arc<AppState> = match get_global_state() {
203            Some(state) => state,
204            None => {
205                ctx.get_mut_response()
206                    .set_status_code(500)
207                    .set_body(ERROR_SERVER_NOT_READY);
208                return Status::Continue;
209            }
210        };
211        let mut rx: broadcast::Receiver<ReloadEvent> = state.get_reload_tx().subscribe();
212        let event: ReloadEvent = match rx.recv().await {
213            Ok(event) => event,
214            Err(_) => {
215                ctx.get_mut_response().set_status_code(503);
216                return Status::Continue;
217            }
218        };
219        let body: String = match serde_json::to_string(&event) {
220            Ok(json) => json,
221            Err(_) => {
222                ctx.get_mut_response().set_status_code(500);
223                return Status::Continue;
224            }
225        };
226        ctx.get_mut_response().set_body(&body);
227        Status::Continue
228    }
229}