136 lines
3.9 KiB
Rust
136 lines
3.9 KiB
Rust
#![allow(dead_code)]
|
|
use std::net::SocketAddr;
|
|
use std::path::PathBuf;
|
|
|
|
use actix_files::NamedFile;
|
|
use actix_web::{get, web, App, HttpRequest, HttpResponse, HttpServer, Responder};
|
|
use actix_web::http::{Method, StatusCode};
|
|
use actix_web::middleware::{self, ErrorHandlers};
|
|
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(r"/")]
|
|
async fn template_index(shared: web::Data<SharedData<'_>>) -> actix_web::Result<impl Responder> {
|
|
if shared.handlebars.has_template("index") {
|
|
let body = shared
|
|
.handlebars
|
|
.render("index", &())
|
|
.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(middleware::NormalizePath::trim())
|
|
.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)
|
|
.service(template_index)
|
|
.default_service(web::route().method(Method::GET))
|
|
})
|
|
.bind(config.address)?
|
|
.run()
|
|
.await
|
|
}
|