tlaternet-webserver/src/main.rs

164 lines
4.5 KiB
Rust

#![allow(dead_code)]
use std::net::SocketAddr;
use std::path::PathBuf;
use actix_files::NamedFile;
use actix_web::http::{Method, StatusCode};
use actix_web::middleware::{self, ErrorHandlers};
use actix_web::{get, post, web, App, HttpRequest, HttpResponse, HttpServer, Responder};
use clap::Parser;
use handlebars::Handlebars;
use log::info;
use serde::Deserialize;
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)?,
}
}
#[derive(Clone, Debug, Deserialize)]
struct Mail {
mail: String,
subject: String,
message: String,
}
#[post("/mail.html")]
async fn mail_post(
shared: web::Data<SharedData<'_>>,
form: web::Form<Mail>,
) -> actix_web::Result<impl Responder> {
info!("{:?}", form);
if shared.handlebars.has_template("mail") {
let body = shared
.handlebars
.render("mail", &())
.map_err(|_| UserError::InternalError)?;
Ok(HttpResponse::Ok().body(body))
} else {
Err(UserError::InternalError)?
}
}
#[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)
.service(mail_post)
.default_service(web::route().method(Method::GET))
})
.bind(config.address)?
.run()
.await
}