Like in GitLab Pages, can bypass html extensions

This commit is contained in:
Pierre HUBERT 2022-04-03 14:30:44 +02:00
parent 8fd0981e78
commit d2daeb5879

View File

@ -42,6 +42,10 @@ struct Args {
/// Optional proxy IP /// Optional proxy IP
#[clap(short, long, env)] #[clap(short, long, env)]
proxy_ip: Option<String>, proxy_ip: Option<String>,
/// Specify whether HTML extensions can be bypassed
#[clap(short, long, env)]
can_bypass_html_ext: bool,
} }
impl Args { impl Args {
@ -52,7 +56,7 @@ impl Args {
pub fn default_handler_path(&self) -> Option<PathBuf> { pub fn default_handler_path(&self) -> Option<PathBuf> {
match self.not_found_file.is_empty() { match self.not_found_file.is_empty() {
true => None, true => None,
false => Some(self.storage_path().join(&self.not_found_file)) false => Some(self.storage_path().join(&self.not_found_file)),
} }
} }
} }
@ -75,7 +79,6 @@ pub fn match_ip(pattern: &str, ip: &str) -> bool {
false false
} }
/// Get the remote IP address /// Get the remote IP address
fn get_remote_ip(req: &HttpRequest, args: &Args) -> String { fn get_remote_ip(req: &HttpRequest, args: &Args) -> String {
let mut ip = req.peer_addr().unwrap().ip().to_string(); let mut ip = req.peer_addr().unwrap().ip().to_string();
@ -102,14 +105,19 @@ fn get_remote_ip(req: &HttpRequest, args: &Args) -> String {
ip ip
} }
/// Replace all the files of the website /// Replace all the files of the website
async fn replace_files(args: web::Data<Args>, req: HttpRequest, mut payload: Multipart) -> Result<HttpResponse, Error> { async fn replace_files(
args: web::Data<Args>,
req: HttpRequest,
mut payload: Multipart,
) -> Result<HttpResponse, Error> {
// Validate remote IP // Validate remote IP
let remote_ip = get_remote_ip(&req, &args); let remote_ip = get_remote_ip(&req, &args);
if !match_ip(&args.allowed_ips_for_update, &remote_ip) { if !match_ip(&args.allowed_ips_for_update, &remote_ip) {
log::warn!("Block unauthorized attempt to perform site update from {}", remote_ip); log::warn!(
"Block unauthorized attempt to perform site update from {}",
remote_ip
);
return Err(ErrorUnauthorized("You are not allowed to perform updates!")); return Err(ErrorUnauthorized("You are not allowed to perform updates!"));
} }
@ -118,8 +126,9 @@ async fn replace_files(args: web::Data<Args>, req: HttpRequest, mut payload: Mul
None => { None => {
return Err(ErrorUnauthorized("Token required!")); return Err(ErrorUnauthorized("Token required!"));
} }
Some(t) => t.to_str() Some(t) => t
.map_err(|_| ErrorInternalServerError("Failed to parse token!"))? .to_str()
.map_err(|_| ErrorInternalServerError("Failed to parse token!"))?,
}; };
if !token.eq(&args.update_token) || args.update_token.is_empty() { if !token.eq(&args.update_token) || args.update_token.is_empty() {
@ -129,8 +138,9 @@ async fn replace_files(args: web::Data<Args>, req: HttpRequest, mut payload: Mul
// Get base folder to keep from tar-file // Get base folder to keep from tar-file
let base_uri = match req.headers().get("BaseURI") { let base_uri = match req.headers().get("BaseURI") {
None => "/", None => "/",
Some(t) => t.to_str() Some(t) => t
.map_err(|_| ErrorInternalServerError("Failed to parse base URI to keep!"))? .to_str()
.map_err(|_| ErrorInternalServerError("Failed to parse base URI to keep!"))?,
}; };
let mut new_files = Vec::new(); let mut new_files = Vec::new();
@ -145,8 +155,10 @@ async fn replace_files(args: web::Data<Args>, req: HttpRequest, mut payload: Mul
} }
let mut archive = tar::Archive::new(b.as_ref()); let mut archive = tar::Archive::new(b.as_ref());
for entry in archive.entries() for entry in archive
.map_err(|_| ErrorInternalServerError("Failed to parse TAR archive!"))? { .entries()
.map_err(|_| ErrorInternalServerError("Failed to parse TAR archive!"))?
{
let mut file = entry?; let mut file = entry?;
let inner_path = file.header().path()?; let inner_path = file.header().path()?;
let inner_path_str = inner_path.to_string_lossy(); let inner_path_str = inner_path.to_string_lossy();
@ -205,6 +217,41 @@ async fn replace_files(args: web::Data<Args>, req: HttpRequest, mut payload: Mul
Ok(HttpResponse::Ok().into()) Ok(HttpResponse::Ok().into())
} }
async fn default_files_handler(req: ServiceRequest) -> Result<ServiceResponse, Error> {
let (req, _) = req.into_parts();
let args: &web::Data<Args> = req.app_data().unwrap();
// Search for alternate paths
if args.can_bypass_html_ext
&& !req.path().ends_with(".html")
&& !req.path().ends_with("/")
&& !req.path().is_empty() {
let alt_file = args.storage_path()
.join(format!("{}.html", &req.path()[1..]));
if alt_file.exists() {
let file = NamedFile::open_async(alt_file).await?;
let res = file.into_response(&req);
return Ok(ServiceResponse::new(req, res));
}
}
// Default handler
if let Some(h) = args.default_handler_path() {
let file = NamedFile::open_async(h).await?;
let res = file.into_response(&req);
Ok(ServiceResponse::new(req, res))
}
// Dummy response
else {
Ok(ServiceResponse::new(
req,
HttpResponse::NotFound().body("404 Not found"),
))
}
}
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
let args: Args = Args::parse(); let args: Args = Args::parse();
@ -219,33 +266,17 @@ async fn main() -> std::io::Result<()> {
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
// Update service // Update service
.service(web::resource("/_mgmt/replace_files") .service(web::resource("/_mgmt/replace_files").route(web::post().to(replace_files)))
.route(web::post().to(replace_files)))
// Serve a tree of static files at the web root and specify the index file. // Serve a tree of static files at the web root and specify the index file.
// Note that the root path should always be defined as the last item. The paths are // Note that the root path should always be defined as the last item. The paths are
// resolved in the order they are defined. If this would be placed before the `/images` // resolved in the order they are defined. If this would be placed before the `/images`
// path then the service for the static images would never be reached. // path then the service for the static images would never be reached.
.service(Files::new("/", &args.files_path) .service(
Files::new("/", &args.files_path)
.index_file(&args.index_file) .index_file(&args.index_file)
.default_handler(fn_service(default_files_handler)),
.default_handler(fn_service(|req: ServiceRequest| async {
let (req, _) = req.into_parts();
let args: &web::Data<Args> = req.app_data().unwrap();
if let Some(h) = args.default_handler_path() {
let file = NamedFile::open_async(h).await?;
let res = file.into_response(&req);
Ok(ServiceResponse::new(req, res))
} else {
Ok(ServiceResponse::new(req, HttpResponse::NotFound()
.body("404 Not found")))
}
}))
) )
// Enable the logger. // Enable the logger.
.wrap(Logger::default()) .wrap(Logger::default())
.app_data(web::Data::new(args.clone())) .app_data(web::Data::new(args.clone()))
@ -253,4 +284,4 @@ async fn main() -> std::io::Result<()> {
.bind(listen_address)? .bind(listen_address)?
.run() .run()
.await .await
} }