use std::io; use std::io::Write; use std::net::{IpAddr, SocketAddr, TcpListener}; use std::path::{Path, PathBuf}; use std::thread; use std::time::Duration; use actix_files::NamedFile; use actix_web::body::BoxBody; use actix_web::middleware::Compat; use actix_web::web; use actix_web::{http::header::ContentType, Responder}; use actix_web::{middleware, App, HttpRequest, HttpResponse}; use actix_web_httpauth::middleware::HttpAuthentication; use anyhow::Result; use clap::{crate_version, IntoApp, Parser}; use clap_generate::generate; use log::{error, warn}; use qrcodegen::{QrCode, QrCodeEcc}; use yansi::{Color, Paint}; mod archive; mod args; mod auth; mod config; mod errors; mod file_upload; mod listing; mod pipe; mod renderer; use crate::config::MiniserveConfig; use crate::errors::ContextualError; fn main() -> Result<()> { let args = args::CliArgs::parse(); if let Some(shell) = args.print_completions { let mut clap_app = args::CliArgs::into_app(); let app_name = clap_app.get_name().to_string(); generate(shell, &mut clap_app, app_name, &mut io::stdout()); return Ok(()); } let miniserve_config = MiniserveConfig::try_from_args(args)?; run(miniserve_config).map_err(|e| { errors::log_error_chain(e.to_string()); e })?; Ok(()) } #[actix_web::main(miniserve)] async fn run(miniserve_config: MiniserveConfig) -> Result<(), ContextualError> { if cfg!(windows) && !Paint::enable_windows_ascii() { Paint::disable(); } let log_level = if miniserve_config.verbose { simplelog::LevelFilter::Info } else { simplelog::LevelFilter::Warn }; if simplelog::TermLogger::init( log_level, simplelog::Config::default(), simplelog::TerminalMode::Mixed, simplelog::ColorChoice::Auto, ) .is_err() { simplelog::SimpleLogger::init(log_level, simplelog::Config::default()) .expect("Couldn't initialize logger") } if miniserve_config.no_symlinks { let is_symlink = miniserve_config .path .symlink_metadata() .map_err(|e| { ContextualError::IoError("Failed to retrieve symlink's metadata".to_string(), e) })? .file_type() .is_symlink(); if is_symlink { return Err(ContextualError::NoSymlinksOptionWithSymlinkServePath( miniserve_config.path.to_string_lossy().to_string(), )); } } let inside_config = miniserve_config.clone(); let canon_path = miniserve_config.path.canonicalize().map_err(|e| { ContextualError::IoError("Failed to resolve path to be served".to_string(), e) })?; check_file_exists(&canon_path, &miniserve_config.index); let path_string = canon_path.to_string_lossy(); println!( "{name} v{version}", name = Paint::new("miniserve").bold(), version = crate_version!() ); if !miniserve_config.path_explicitly_chosen { // If the path to serve has NOT been explicitly chosen and if this is NOT an interactive // terminal, we should refuse to start for security reasons. This would be the case when // running miniserve as a service but forgetting to set the path. This could be pretty // dangerous if given with an undesired context path (for instance /root or /). if !atty::is(atty::Stream::Stdout) { return Err(ContextualError::NoExplicitPathAndNoTerminal); } warn!("miniserve has been invoked without an explicit path so it will serve the current directory after a short delay."); warn!( "Invoke with -h|--help to see options or invoke as `miniserve .` to hide this advice." ); print!("Starting server in "); io::stdout() .flush() .map_err(|e| ContextualError::IoError("Failed to write data".to_string(), e))?; for c in "3… 2… 1… \n".chars() { print!("{}", c); io::stdout() .flush() .map_err(|e| ContextualError::IoError("Failed to write data".to_string(), e))?; thread::sleep(Duration::from_millis(500)); } } let display_urls = { let (mut ifaces, wildcard): (Vec<_>, Vec<_>) = miniserve_config .interfaces .clone() .into_iter() .partition(|addr| !addr.is_unspecified()); // Replace wildcard addresses with local interface addresses if !wildcard.is_empty() { let all_ipv4 = wildcard.iter().any(|addr| addr.is_ipv4()); let all_ipv6 = wildcard.iter().any(|addr| addr.is_ipv6()); ifaces = get_if_addrs::get_if_addrs() .unwrap_or_else(|e| { error!("Failed to get local interface addresses: {}", e); Default::default() }) .into_iter() .map(|iface| iface.ip()) .filter(|ip| (all_ipv4 && ip.is_ipv4()) || (all_ipv6 && ip.is_ipv6())) .collect(); ifaces.sort(); } ifaces .into_iter() .map(|addr| match addr { IpAddr::V4(_) => format!("{}:{}", addr, miniserve_config.port), IpAddr::V6(_) => format!("[{}]:{}", addr, miniserve_config.port), }) .map(|addr| match miniserve_config.tls_rustls_config { Some(_) => format!("https://{}", addr), None => format!("http://{}", addr), }) .map(|url| match miniserve_config.random_route { Some(ref random_route) => format!("{}/{}", url, random_route), None => url, }) .collect::>() }; let socket_addresses = miniserve_config .interfaces .iter() .map(|&interface| SocketAddr::new(interface, miniserve_config.port)) .collect::>(); let display_sockets = socket_addresses .iter() .map(|sock| Color::Green.paint(sock.to_string()).bold().to_string()) .collect::>(); let srv = actix_web::HttpServer::new(move || { App::new() .wrap(configure_header(&inside_config.clone())) .app_data(inside_config.clone()) .wrap_fn(errors::error_page_middleware) .wrap(middleware::Logger::default()) .route( &format!("/{}", inside_config.favicon_route), web::get().to(favicon), ) .route(&format!("/{}", inside_config.css_route), web::get().to(css)) .service( web::scope(inside_config.random_route.as_deref().unwrap_or("")) .wrap(middleware::Condition::new( !inside_config.auth.is_empty(), Compat::new(HttpAuthentication::basic(auth::handle_auth)), )) .configure(|c| configure_app(c, &inside_config)), ) .default_service(web::get().to(error_404)) }); let srv = socket_addresses.iter().try_fold(srv, |srv, addr| { let listener = create_tcp_listener(*addr).map_err(|e| { ContextualError::IoError(format!("Failed to bind server to {}", addr), e) })?; #[cfg(feature = "tls")] let srv = match &miniserve_config.tls_rustls_config { Some(tls_config) => srv.listen_rustls(listener, tls_config.clone()), None => srv.listen(listener), }; #[cfg(not(feature = "tls"))] let srv = srv.listen(listener); srv.map_err(|e| ContextualError::IoError(format!("Failed to bind server to {}", addr), e)) })?; let srv = srv.shutdown_timeout(0).run(); println!("Bound to {}", display_sockets.join(", ")); println!("Serving path {}", Color::Yellow.paint(path_string).bold()); println!( "Available at (non-exhaustive list):\n {}\n", display_urls .iter() .map(|url| Color::Green.paint(url).bold().to_string()) .collect::>() .join("\n "), ); // print QR code to terminal if miniserve_config.show_qrcode && atty::is(atty::Stream::Stdout) { for url in display_urls .iter() .filter(|url| !url.contains("//127.0.0.1:") && !url.contains("//[::1]:")) { match QrCode::encode_text(url, QrCodeEcc::Low) { Ok(qr) => { println!("QR code for {}:", Color::Green.paint(url).bold()); print_qr(&qr); } Err(e) => { error!("Failed to render QR to terminal: {}", e); } }; } } if atty::is(atty::Stream::Stdout) { println!("Quit by pressing CTRL-C"); } srv.await .map_err(|e| ContextualError::IoError("".to_owned(), e)) } fn check_file_exists(canon_path: &Path, file_option: &Option) { if let Some(file_path) = file_option { let file_path: &Path = file_path.as_ref(); let has_file: std::path::PathBuf = [canon_path, file_path].iter().collect(); if !has_file.exists() { error!( "The file '{}' provided for option --index could not be found.", file_path.to_string_lossy(), ); } } } /// Allows us to set low-level socket options /// /// This mainly used to set `set_only_v6` socket option /// to get a consistent behavior across platforms. /// see: https://github.com/svenstaro/miniserve/pull/500 fn create_tcp_listener(addr: SocketAddr) -> io::Result { use socket2::{Domain, Protocol, Socket, Type}; let socket = Socket::new(Domain::for_address(addr), Type::STREAM, Some(Protocol::TCP))?; if addr.is_ipv6() { socket.set_only_v6(true)?; } socket.set_reuse_address(true)?; socket.bind(&addr.into())?; socket.listen(1024 /* Default backlog */)?; Ok(TcpListener::from(socket)) } fn configure_header(conf: &MiniserveConfig) -> middleware::DefaultHeaders { conf.header.iter().flatten().fold( middleware::DefaultHeaders::new(), |headers, (header_name, header_value)| headers.add((header_name, header_value)), ) } /// Configures the Actix application fn configure_app(app: &mut web::ServiceConfig, conf: &MiniserveConfig) { let files_service = || { let files = actix_files::Files::new("", &conf.path); let files = match &conf.index { Some(index_file) => { if conf.spa { files .index_file(index_file.to_string_lossy()) .default_handler( NamedFile::open(&conf.path.join(index_file)) .expect("Cant open SPA index file."), ) } else { files.index_file(index_file.to_string_lossy()) } } None => { if conf.spa { unreachable!("This can't be reached since we require --index to be provided if --spa is given via clap"); } else { files } } }; let files = match conf.show_hidden { true => files.use_hidden_files(), false => files, }; files .show_files_listing() .files_listing_renderer(listing::directory_listing) .prefer_utf8(true) .redirect_to_slash_directory() }; if !conf.path.is_file() { if conf.file_upload { // Allow file upload app.service(web::resource("/upload").route(web::post().to(file_upload::upload_file))); } // Handle directories app.service(files_service()); } else { // Handle single files app.service(web::resource(["", "/"]).route(web::to(listing::file_handler))); } } async fn error_404(req: HttpRequest) -> Result { Err(ContextualError::RouteNotFoundError(req.path().to_string())) } async fn favicon() -> impl Responder { let logo = include_str!("../data/logo.svg"); HttpResponse::Ok() .insert_header(ContentType(mime::IMAGE_SVG)) .body(logo) } async fn css() -> impl Responder { let css = include_str!(concat!(env!("OUT_DIR"), "/style.css")); HttpResponse::Ok() .insert_header(ContentType(mime::TEXT_CSS)) .message_body(BoxBody::new(css)) } // Prints to the console two inverted QrCodes side by side. fn print_qr(qr: &QrCode) { let border = 4; let size = qr.size() + 2 * border; for y in (0..size).step_by(2) { for x in 0..2 * size { let inverted = x >= size; let (x, y) = (x % size - border, y - border); //each char represents two vertical modules let (mod1, mod2) = match inverted { false => (qr.get_module(x, y), qr.get_module(x, y + 1)), true => (!qr.get_module(x, y), !qr.get_module(x, y + 1)), }; let c = match (mod1, mod2) { (false, false) => ' ', (true, false) => '▀', (false, true) => '▄', (true, true) => '█', }; print!("{0}", c); } println!(); } println!(); }