aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/args.rs27
-rw-r--r--src/config.rs27
-rw-r--r--src/errors.rs12
-rw-r--r--src/main.rs46
-rw-r--r--src/renderer.rs94
5 files changed, 150 insertions, 56 deletions
diff --git a/src/args.rs b/src/args.rs
index f7b42f1..c78e08c 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -41,6 +41,14 @@ pub struct CliArgs {
#[arg(long, requires = "index", env = "MINISERVE_SPA")]
pub spa: bool,
+ /// Activate Pretty URLs mode
+ ///
+ /// This will cause the server to serve the equivalent `.html` file indicated by the path.
+ ///
+ /// `/about` will try to find `about.html` and serve it.
+ #[arg(long, env = "MINISERVE_PRETTY_URLS")]
+ pub pretty_urls: bool,
+
/// Port to use
#[arg(
short = 'p',
@@ -60,7 +68,9 @@ pub struct CliArgs {
)]
pub interfaces: Vec<IpAddr>,
- /// Set authentication. Currently supported formats:
+ /// Set authentication
+ ///
+ /// Currently supported formats:
/// username:password, username:sha256:hash, username:sha512:hash
/// (e.g. joe:123, joe:sha256:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3)
#[arg(
@@ -68,10 +78,21 @@ pub struct CliArgs {
long = "auth",
value_parser(parse_auth),
num_args(1),
- env = "MINISERVE_AUTH"
+ env = "MINISERVE_AUTH",
+ verbatim_doc_comment
)]
pub auth: Vec<auth::RequiredAuth>,
+ /// Read authentication values from a file
+ ///
+ /// Example file content:
+ ///
+ /// joe:123
+ /// bob:sha256:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3
+ /// bill:
+ #[arg(long, value_hint = ValueHint::FilePath, env = "MINISERVE_AUTH_FILE", verbatim_doc_comment)]
+ pub auth_file: Option<PathBuf>,
+
/// Use a specific route prefix
#[arg(long = "route-prefix", env = "MINISERVE_ROUTE_PREFIX")]
pub route_prefix: Option<String>,
@@ -241,7 +262,7 @@ fn parse_interface(src: &str) -> Result<IpAddr, std::net::AddrParseError> {
}
/// Parse authentication requirement
-fn parse_auth(src: &str) -> Result<auth::RequiredAuth, ContextualError> {
+pub fn parse_auth(src: &str) -> Result<auth::RequiredAuth, ContextualError> {
let mut split = src.splitn(3, ':');
let invalid_auth_format = Err(ContextualError::InvalidAuthFormat);
diff --git a/src/config.rs b/src/config.rs
index 0959e81..8a8a876 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,6 +1,6 @@
-#[cfg(feature = "tls")]
-use std::{fs::File, io::BufReader};
use std::{
+ fs::File,
+ io::{BufRead, BufReader},
net::{IpAddr, Ipv4Addr, Ipv6Addr},
path::PathBuf,
};
@@ -14,7 +14,7 @@ use http::HeaderMap;
use rustls_pemfile as pemfile;
use crate::{
- args::{CliArgs, MediaType},
+ args::{parse_auth, CliArgs, MediaType},
auth::RequiredAuth,
file_utils::sanitize_path,
renderer::ThemeSlug,
@@ -80,6 +80,13 @@ pub struct MiniserveConfig {
/// allow the SPA router to handle the request instead.
pub spa: bool,
+ /// Activate Pretty URLs mode
+ ///
+ /// This will cause the server to serve the equivalent `.html` file indicated by the path.
+ ///
+ /// `/about` will try to find `about.html` and serve it.
+ pub pretty_urls: bool,
+
/// Enable QR code display
pub show_qrcode: bool,
@@ -157,6 +164,17 @@ impl MiniserveConfig {
_ => "".to_owned(),
};
+ let mut auth = args.auth;
+
+ if let Some(path) = args.auth_file {
+ let file = File::open(path)?;
+ let lines = BufReader::new(file).lines();
+
+ for line in lines {
+ auth.push(parse_auth(line?.as_str())?);
+ }
+ }
+
// Generate some random routes for the favicon and css so that they are very unlikely to conflict with
// real files.
// If --random-route is enabled , in order to not leak the random generated route, we must not use it
@@ -254,7 +272,7 @@ impl MiniserveConfig {
path: args.path.unwrap_or_else(|| PathBuf::from(".")),
port,
interfaces,
- auth: args.auth,
+ auth,
path_explicitly_chosen,
no_symlinks: args.no_symlinks,
show_hidden: args.hidden,
@@ -265,6 +283,7 @@ impl MiniserveConfig {
default_color_scheme_dark,
index: args.index,
spa: args.spa,
+ pretty_urls: args.pretty_urls,
overwrite_files: args.overwrite_files,
show_qrcode: args.qrcode,
mkdir_enabled: args.mkdir_enabled,
diff --git a/src/errors.rs b/src/errors.rs
index e502634..6875b90 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -5,6 +5,7 @@ use actix_web::{
HttpRequest, HttpResponse, ResponseError,
};
use futures::prelude::*;
+use std::str::FromStr;
use thiserror::Error;
use crate::{renderer::render_error, MiniserveConfig};
@@ -131,8 +132,15 @@ where
let res = fut.await?.map_into_boxed_body();
if (res.status().is_client_error() || res.status().is_server_error())
- && res.headers().get(header::CONTENT_TYPE).map(AsRef::as_ref)
- == Some(mime::TEXT_PLAIN_UTF_8.essence_str().as_bytes())
+ && res
+ .headers()
+ .get(header::CONTENT_TYPE)
+ .map(AsRef::as_ref)
+ .and_then(|s| std::str::from_utf8(s).ok())
+ .and_then(|s| mime::Mime::from_str(s).ok())
+ .as_ref()
+ .map(mime::Mime::essence_str)
+ == Some(mime::TEXT_PLAIN.as_ref())
{
let req = res.request().clone();
Ok(res.map_body(|head, body| map_error_page(&req, head, body)))
diff --git a/src/main.rs b/src/main.rs
index 2a4d402..6bbebd7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,7 +5,9 @@ use std::time::Duration;
use actix_files::NamedFile;
use actix_web::{
- http::header::ContentType, middleware, web, App, HttpRequest, HttpResponse, Responder,
+ dev::{fn_service, ServiceRequest, ServiceResponse},
+ http::header::ContentType,
+ middleware, web, App, HttpRequest, HttpResponse, Responder,
};
use actix_web_httpauth::middleware::HttpAuthentication;
use anyhow::Result;
@@ -29,6 +31,8 @@ mod renderer;
use crate::config::MiniserveConfig;
use crate::errors::ContextualError;
+static STYLESHEET: &str = grass::include!("data/style.scss");
+
fn main() -> Result<()> {
let args = args::CliArgs::parse();
@@ -182,10 +186,20 @@ async fn run(miniserve_config: MiniserveConfig) -> Result<(), ContextualError> {
.map(|sock| sock.to_string().green().bold().to_string())
.collect::<Vec<_>>();
+ let stylesheet = web::Data::new(
+ [
+ STYLESHEET,
+ inside_config.default_color_scheme.css(),
+ inside_config.default_color_scheme_dark.css_dark().as_str(),
+ ]
+ .join("\n"),
+ );
+
let srv = actix_web::HttpServer::new(move || {
App::new()
.wrap(configure_header(&inside_config.clone()))
.app_data(inside_config.clone())
+ .app_data(stylesheet.clone())
.wrap_fn(errors::error_page_middleware)
.wrap(middleware::Logger::default())
.route(&inside_config.favicon_route, web::get().to(favicon))
@@ -305,6 +319,31 @@ fn configure_app(app: &mut web::ServiceConfig, conf: &MiniserveConfig) {
}
}
+ // Handle --pretty-urls options.
+ //
+ // We rewrite the request to append ".html" to the path and serve the file. If the
+ // path ends with a `/`, we remove it before appending ".html".
+ //
+ // This is done to allow for pretty URLs, e.g. "/about" instead of "/about.html".
+ if conf.pretty_urls {
+ files = files.default_handler(fn_service(|req: ServiceRequest| async {
+ let (req, _) = req.into_parts();
+ let conf = req
+ .app_data::<MiniserveConfig>()
+ .expect("Could not get miniserve config");
+ let mut path_base = req.path()[1..].to_string();
+ if path_base.ends_with('/') {
+ path_base.pop();
+ }
+ if !path_base.ends_with("html") {
+ path_base = format!("{}.html", path_base);
+ }
+ let file = NamedFile::open_async(conf.path.join(path_base)).await?;
+ let res = file.into_response(&req);
+ Ok(ServiceResponse::new(req, res))
+ }));
+ }
+
if conf.show_hidden {
files = files.use_hidden_files();
}
@@ -346,9 +385,8 @@ async fn favicon() -> impl Responder {
.body(logo)
}
-async fn css() -> impl Responder {
- let css = include_str!(concat!(env!("OUT_DIR"), "/style.css"));
+async fn css(stylesheet: web::Data<String>) -> impl Responder {
HttpResponse::Ok()
.insert_header(ContentType(mime::TEXT_CSS))
- .body(css)
+ .body(stylesheet.to_string())
}
diff --git a/src/renderer.rs b/src/renderer.rs
index b94817d..5a591f0 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -56,33 +56,7 @@ pub fn page(
(page_header(&title_path, conf.file_upload, &conf.favicon_route, &conf.css_route))
body #drop-container
- .(format!("default_theme_{}", conf.default_color_scheme))
- .(format!("default_theme_dark_{}", conf.default_color_scheme_dark)) {
-
- (PreEscaped(r#"
- <script>
- // read theme from local storage and apply it to body
- const body = document.body;
- var theme = localStorage.getItem('theme');
-
- if (theme != null && theme != 'default') {
- body.classList.add('theme_' + theme);
- }
-
- // updates the color scheme by replacing the appropriate class
- // on body and saving the new theme to local storage
- function updateColorScheme(name) {
- body.classList.remove.apply(body.classList, Array.from(body.classList).filter(v=>v.startsWith("theme_")));
-
- if (name != "default") {
- body.classList.add('theme_' + name);
- }
-
- localStorage.setItem('theme', name);
- }
- </script>
- "#))
-
+ {
div.toolbar_box_group {
@if conf.file_upload {
div.form {
@@ -293,8 +267,10 @@ fn wget_footer(abs_path: &Uri, root_dir_name: Option<&str>, current_user: Option
None => String::new(),
};
- let command =
- format!("wget -rcnHp -R 'index.html*'{cut_dirs}{user_params} '{abs_path}?raw=true'");
+ let encoded_abs_path = abs_path.to_string().replace('\'', "%27");
+ let command = format!(
+ "wget -rcnHp -R 'index.html*'{cut_dirs}{user_params} '{encoded_abs_path}?raw=true'"
+ );
let click_to_copy = format!("navigator.clipboard.writeText(\"{command}\")");
html! {
@@ -348,6 +324,21 @@ pub enum ThemeSlug {
Monokai,
}
+impl ThemeSlug {
+ pub fn css(&self) -> &str {
+ match self {
+ ThemeSlug::Squirrel => grass::include!("data/themes/squirrel.scss"),
+ ThemeSlug::Archlinux => grass::include!("data/themes/archlinux.scss"),
+ ThemeSlug::Zenburn => grass::include!("data/themes/zenburn.scss"),
+ ThemeSlug::Monokai => grass::include!("data/themes/monokai.scss"),
+ }
+ }
+
+ pub fn css_dark(&self) -> String {
+ format!("@media (prefers-color-scheme: dark) {{\n{}}}", self.css())
+ }
+}
+
/// Partial: qr code spoiler
fn qr_spoiler(show_qrcode: bool, content: &Uri) -> Markup {
html! {
@@ -377,7 +368,7 @@ fn color_scheme_selector(hide_theme_selector: bool) -> Markup {
}
ul.theme {
@for color_scheme in THEME_PICKER_CHOICES {
- li.(format!("theme_{}", color_scheme.1)) {
+ li data-theme=(color_scheme.1) {
(color_scheme_link(color_scheme))
}
}
@@ -586,12 +577,40 @@ fn page_header(title: &str, file_upload: bool, favicon_route: &str, css_route: &
meta charset="utf-8";
meta http-equiv="X-UA-Compatible" content="IE=edge";
meta name="viewport" content="width=device-width, initial-scale=1";
+ meta name="color-scheme" content="dark light";
link rel="icon" type="image/svg+xml" href={ (favicon_route) };
link rel="stylesheet" href={ (css_route) };
title { (title) }
+ (PreEscaped(r#"
+ <script>
+ // updates the color scheme by setting the theme data attribute
+ // on body and saving the new theme to local storage
+ function updateColorScheme(name) {
+ if (name && name != "default") {
+ localStorage.setItem('theme', name);
+ document.body.setAttribute("data-theme", name)
+ } else {
+ localStorage.removeItem('theme');
+ document.body.removeAttribute("data-theme")
+ }
+ }
+
+ // read theme from local storage and apply it to body
+ function loadColorScheme() {
+ var name = localStorage.getItem('theme');
+ updateColorScheme(name);
+ }
+
+ // load saved theme on page load
+ addEventListener("load", loadColorScheme);
+ // load saved theme when local storage is changed (synchronize between tabs)
+ addEventListener("storage", loadColorScheme);
+ </script>
+ "#))
+
@if file_upload {
(PreEscaped(r#"
<script>
@@ -660,19 +679,8 @@ pub fn render_error(
html {
(page_header(&error_code.to_string(), false, &conf.favicon_route, &conf.css_route))
- body.(format!("default_theme_{}", conf.default_color_scheme))
- .(format!("default_theme_dark_{}", conf.default_color_scheme_dark)) {
-
- (PreEscaped(r#"
- <script>
- // read theme from local storage and apply it to body
- var theme = localStorage.getItem('theme');
- if (theme != null && theme != 'default') {
- document.body.classList.add('theme_' + theme);
- }
- </script>
- "#))
-
+ body
+ {
div.error {
p { (error_code.to_string()) }
@for error in error_description.lines() {