#![allow(dead_code)] use std::net::SocketAddr; use std::path::PathBuf; use actix_files::NamedFile; use actix_web::http::StatusCode; use actix_web::middleware::ErrorHandlers; use actix_web::{get, web, App, HttpRequest, HttpResponse, HttpServer, Responder}; use clap::Parser; use handlebars::Handlebars; mod errors; use errors::{generic_error, UserError}; #[derive(Parser, Debug, Clone)] struct Config { #[clap(long, value_parser)] /// The directory from which to serve static content and /// handlebars templates template_directory: PathBuf, #[clap(long, default_value = "127.0.0.1:8000", value_parser)] /// The address on which to listen address: SocketAddr, #[clap(long, action)] /// Whether to start the server in dev mode; this enables some /// nice handlebars features that are not intended for production dev_mode: bool, } #[derive(Debug)] struct SharedData<'a> { handlebars: Handlebars<'a>, config: Config, } #[get(r"/{filename:.*\.html}")] async fn template( shared: web::Data<SharedData<'_>>, req: HttpRequest, ) -> actix_web::Result<impl Responder> { let path = req .match_info() .query("filename") .strip_suffix(".html") .expect("only paths with this suffix should get here"); if shared.handlebars.has_template(path) { let body = shared .handlebars .render(path, &()) .map_err(|_| UserError::InternalError)?; Ok(HttpResponse::Ok().body(body)) } else { Err(UserError::NotFound)? } } #[get("/{filename:.*[^/]+}")] async fn static_file( shared: web::Data<SharedData<'_>>, req: HttpRequest, ) -> actix_web::Result<impl Responder> { let requested = req.match_info().query("filename"); match shared .config .template_directory .join(requested) .canonicalize() { // We only want to serve paths that are both valid *and* in // the template directory. // // i.e., don't serve up /etc/passwd Ok(path) if path.starts_with(&shared.config.template_directory) => { let file = NamedFile::open_async(path) .await .map_err(|_| UserError::NotFound)?; Ok(file.use_last_modified(false).respond_to(&req)) } // Any other cases should 404 _ => Err(UserError::NotFound)?, } } #[actix_web::main] async fn main() -> Result<(), std::io::Error> { let mut config = Config::parse(); config.template_directory = config.template_directory.canonicalize()?; env_logger::init(); let mut handlebars = Handlebars::new(); handlebars .register_templates_directory(".html", config.template_directory.clone()) .expect("templates should compile correctly"); handlebars.set_dev_mode(config.dev_mode); let shared_data = web::Data::new(SharedData { handlebars, config: config.clone(), }); HttpServer::new(move || { App::new() .wrap( ErrorHandlers::new() .handler(StatusCode::NOT_FOUND, generic_error) .handler(StatusCode::INTERNAL_SERVER_ERROR, generic_error), ) .app_data(shared_data.clone()) .service(template) .service(static_file) }) .bind(config.address)? .run() .await }