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}