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}