1use crate::{
2 anyhow::{bail, ensure, Context, Result},
3 camino::Utf8Path,
4 clap, Watch,
5};
6use derive_more::Debug;
7use std::{
8 ffi, fs,
9 io::prelude::*,
10 net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener, TcpStream},
11 path::{Path, PathBuf},
12 process,
13 sync::Arc,
14 thread,
15};
16
17type RequestHandler = Arc<dyn Fn(Request) -> Result<()> + Send + Sync + 'static>;
18
19#[non_exhaustive]
21pub struct Request<'a> {
22 pub stream: &'a mut TcpStream,
24 pub path: &'a str,
26 pub header: &'a str,
28 pub dist_dir_path: &'a Path,
30 pub not_found_path: Option<&'a Path>,
33}
34
35#[non_exhaustive]
78#[derive(Debug, clap::Parser)]
79#[clap(
80 about = "A simple HTTP server useful during development.",
81 long_about = "A simple HTTP server useful during development.\n\
82 It can watch the source code for changes."
83)]
84pub struct DevServer {
85 #[clap(long, default_value = "127.0.0.1")]
87 pub ip: IpAddr,
88 #[clap(long, default_value = "8000")]
90 pub port: u16,
91
92 #[clap(flatten)]
98 pub watch: Watch,
99
100 #[clap(skip)]
102 pub command: Option<process::Command>,
103
104 #[clap(skip)]
106 pub not_found_path: Option<PathBuf>,
107
108 #[clap(skip)]
110 #[debug(skip)]
111 request_handler: Option<RequestHandler>,
112}
113
114impl DevServer {
115 pub fn address(mut self, ip: IpAddr, port: u16) -> Self {
117 self.ip = ip;
118 self.port = port;
119
120 self
121 }
122
123 pub fn command(mut self, command: process::Command) -> Self {
125 self.command = Some(command);
126 self
127 }
128
129 pub fn arg<S: AsRef<ffi::OsStr>>(mut self, arg: S) -> Self {
134 self.set_xtask_command().arg(arg);
135 self
136 }
137
138 pub fn args<I, S>(mut self, args: I) -> Self
143 where
144 I: IntoIterator<Item = S>,
145 S: AsRef<ffi::OsStr>,
146 {
147 self.set_xtask_command().args(args);
148 self
149 }
150
151 pub fn not_found(mut self, path: impl Into<PathBuf>) -> Self {
153 self.not_found_path.replace(path.into());
154 self
155 }
156
157 pub fn request_handler<F>(mut self, handler: F) -> Self
159 where
160 F: Fn(Request) -> Result<()> + Send + Sync + 'static,
161 {
162 self.request_handler.replace(Arc::new(handler));
163 self
164 }
165
166 pub fn start(self, dist_dir_path: impl Into<PathBuf>) -> Result<()> {
171 let dist_dir_path = dist_dir_path.into();
172
173 let watch_process = if let Some(command) = self.command {
174 let _ = std::fs::create_dir_all(&dist_dir_path);
176 let watch = self.watch.exclude_path(&dist_dir_path);
177 let handle = std::thread::spawn(|| match watch.run(command) {
178 Ok(()) => log::trace!("Starting to watch"),
179 Err(err) => log::error!("an error occurred when starting to watch: {}", err),
180 });
181
182 Some(handle)
183 } else {
184 None
185 };
186
187 if let Some(handler) = self.request_handler {
188 serve(
189 self.ip,
190 self.port,
191 dist_dir_path,
192 self.not_found_path,
193 handler,
194 )
195 .context("an error occurred when starting to serve")?;
196 } else {
197 serve(
198 self.ip,
199 self.port,
200 dist_dir_path,
201 self.not_found_path,
202 Arc::new(default_request_handler),
203 )
204 .context("an error occurred when starting to serve")?;
205 }
206
207 if let Some(handle) = watch_process {
208 handle.join().expect("an error occurred when exiting watch");
209 }
210
211 Ok(())
212 }
213
214 fn set_xtask_command(&mut self) -> &mut process::Command {
215 if self.command.is_none() {
216 self.command = Some(crate::xtask_command());
217 }
218 self.command.as_mut().unwrap()
219 }
220}
221
222impl Default for DevServer {
223 fn default() -> DevServer {
224 DevServer {
225 ip: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
226 port: 8000,
227 watch: Default::default(),
228 command: None,
229 not_found_path: None,
230 request_handler: None,
231 }
232 }
233}
234
235fn serve(
236 ip: IpAddr,
237 port: u16,
238 dist_dir_path: PathBuf,
239 not_found_path: Option<PathBuf>,
240 handler: RequestHandler,
241) -> Result<()> {
242 let address = SocketAddr::new(ip, port);
243 let listener = TcpListener::bind(address).context("cannot bind to the given address")?;
244
245 log::info!("Development server running at: http://{}", &address);
246
247 macro_rules! warn_not_fail {
248 ($expr:expr) => {{
249 match $expr {
250 Ok(res) => res,
251 Err(err) => {
252 log::warn!("Malformed request's header: {}", err);
253 return;
254 }
255 }
256 }};
257 }
258
259 for mut stream in listener.incoming().filter_map(Result::ok) {
260 let handler = handler.clone();
261 let dist_dir_path = dist_dir_path.clone();
262 let not_found_path = not_found_path.clone();
263 thread::spawn(move || {
264 let header = warn_not_fail!(read_header(&stream));
265 let request = Request {
266 stream: &mut stream,
267 header: header.as_ref(),
268 path: warn_not_fail!(parse_request_path(&header)),
269 dist_dir_path: dist_dir_path.as_ref(),
270 not_found_path: not_found_path.as_deref(),
271 };
272
273 (handler)(request).unwrap_or_else(|e| {
274 let _ = stream.write("HTTP/1.1 500 INTERNAL SERVER ERROR\r\n\r\n".as_bytes());
275 log::error!("an error occurred: {}", e);
276 });
277 });
278 }
279
280 Ok(())
281}
282
283fn read_header(mut stream: &TcpStream) -> Result<String> {
284 let mut header = Vec::with_capacity(64 * 1024);
285 let mut peek_buffer = [0u8; 4096];
286
287 loop {
288 let n = stream.peek(&mut peek_buffer)?;
289 ensure!(n > 0, "Unexpected EOF");
290
291 let data = &mut peek_buffer[..n];
292 if let Some(i) = data.windows(4).position(|x| x == b"\r\n\r\n") {
293 let data = &mut peek_buffer[..(i + 4)];
294 stream.read_exact(data)?;
295 header.extend(&*data);
296 break;
297 } else {
298 stream.read_exact(data)?;
299 header.extend(&*data);
300 }
301 }
302
303 Ok(String::from_utf8(header)?)
304}
305
306fn parse_request_path(header: &str) -> Result<&str> {
307 let content = header.split('\r').next().unwrap();
308 let requested_path = content
309 .split_whitespace()
310 .nth(1)
311 .context("could not find path in request")?;
312 Ok(requested_path
313 .split_once('?')
314 .map(|(prefix, _suffix)| prefix)
315 .unwrap_or(requested_path))
316}
317
318pub fn default_request_handler(request: Request) -> Result<()> {
320 let requested_path = request.path;
321
322 log::debug!("<-- {}", requested_path);
323
324 let rel_path = Path::new(requested_path.trim_matches('/'));
325 let mut full_path = request.dist_dir_path.join(rel_path);
326
327 if full_path.is_dir() {
328 if full_path.join("index.html").exists() {
329 full_path = full_path.join("index.html")
330 } else if full_path.join("index.htm").exists() {
331 full_path = full_path.join("index.htm")
332 } else {
333 bail!("no index.html in {}", full_path.display());
334 }
335 }
336
337 if let Some(path) = request.not_found_path {
338 if !full_path.is_file() {
339 full_path = request.dist_dir_path.join(path);
340 }
341 }
342
343 if full_path.is_file() {
344 log::debug!("--> {}", full_path.display());
345 let full_path_extension = Utf8Path::from_path(&full_path)
346 .context("request path contains non-utf8 characters")?
347 .extension();
348
349 let content_type = match full_path_extension {
350 Some("html") => "text/html;charset=utf-8",
351 Some("css") => "text/css;charset=utf-8",
352 Some("js") => "application/javascript",
353 Some("wasm") => "application/wasm",
354 _ => "application/octet-stream",
355 };
356
357 request
358 .stream
359 .write(
360 format!(
361 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: {}\r\n\r\n",
362 full_path.metadata()?.len(),
363 content_type,
364 )
365 .as_bytes(),
366 )
367 .context("cannot write response")?;
368
369 std::io::copy(&mut fs::File::open(&full_path)?, request.stream)?;
370 } else {
371 log::error!("--> {} (404 NOT FOUND)", full_path.display());
372 request
373 .stream
374 .write("HTTP/1.1 404 NOT FOUND\r\n\r\n".as_bytes())
375 .context("cannot write response")?;
376 }
377
378 Ok(())
379}