🐛 Redirect to add trailing slashes for directories
parent
435232bf6c
commit
02da0ff0fc
|
@ -9,10 +9,16 @@
|
||||||
.entry {
|
.entry {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
min-width: 50%;
|
width: 100%;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.entry_list div:nth-child(odd) {
|
.grey {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.entry_list {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.entry_list tr:nth-child(even) {
|
||||||
background-color: #ddd;
|
background-color: #ddd;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -24,21 +30,30 @@
|
||||||
|
|
||||||
<p>{{path}}</p>
|
<p>{{path}}</p>
|
||||||
|
|
||||||
<div class="entry_list">
|
<table class="entry_list">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Size</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
|
||||||
<div>
|
<tr>
|
||||||
<a class="entry" href="../">📁 ../</a>
|
<td><a class="entry" href="../">📁 ../</a></td>
|
||||||
</div>
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
{{#each entries}}
|
{{#each entries}}
|
||||||
<div>
|
<tr>
|
||||||
<a class="entry" href="{{this.encoded_file_name}}{{this.trailing_slash}}">
|
<td><a class="entry" href="{{this.encoded_file_name}}{{this.trailing_slash}}">
|
||||||
{{this.icon}} {{this.file_name}}{{this.trailing_slash}}
|
{{this.icon}} {{this.file_name}}{{this.trailing_slash}}</a></td>
|
||||||
</a>
|
<td><span class="grey">{{this.size}}</span></td>
|
||||||
</div>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
</div>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -9,10 +9,10 @@
|
||||||
.entry {
|
.entry {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
min-width: 50%;
|
width: 100%;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.entry_list div:nth-child(odd) {
|
.entry_list div:nth-child(even) {
|
||||||
background-color: #ddd;
|
background-color: #ddd;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -32,6 +32,7 @@ pub struct Config {
|
||||||
struct ServerState <'a> {
|
struct ServerState <'a> {
|
||||||
config: Config,
|
config: Config,
|
||||||
handlebars: handlebars::Handlebars <'a>,
|
handlebars: handlebars::Handlebars <'a>,
|
||||||
|
hidden_path: Option <PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn status_reply <B: Into <Body>> (status: StatusCode, b: B)
|
fn status_reply <B: Into <Body>> (status: StatusCode, b: B)
|
||||||
|
@ -67,7 +68,7 @@ async fn handle_all (req: Request <Body>, state: Arc <ServerState <'static>>)
|
||||||
ptth_req.method,
|
ptth_req.method,
|
||||||
&ptth_req.uri,
|
&ptth_req.uri,
|
||||||
&ptth_req.headers,
|
&ptth_req.headers,
|
||||||
None
|
state.hidden_path.as_ref ().map (|p| p.as_path ())
|
||||||
).await;
|
).await;
|
||||||
|
|
||||||
let mut resp = Response::builder ()
|
let mut resp = Response::builder ()
|
||||||
|
@ -97,7 +98,9 @@ pub struct ConfigFile {
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main () -> Result <(), Box <dyn Error>> {
|
async fn main () -> Result <(), Box <dyn Error>> {
|
||||||
tracing_subscriber::fmt::init ();
|
tracing_subscriber::fmt::init ();
|
||||||
let config_file: ConfigFile = ptth::load_toml::load ("config/ptth_server.toml");
|
|
||||||
|
let path = PathBuf::from ("./config/ptth_server.toml");
|
||||||
|
let config_file: ConfigFile = ptth::load_toml::load (&path);
|
||||||
info! ("file_server_root: {:?}", config_file.file_server_root);
|
info! ("file_server_root: {:?}", config_file.file_server_root);
|
||||||
|
|
||||||
let addr = SocketAddr::from(([0, 0, 0, 0], 4000));
|
let addr = SocketAddr::from(([0, 0, 0, 0], 4000));
|
||||||
|
@ -109,6 +112,7 @@ async fn main () -> Result <(), Box <dyn Error>> {
|
||||||
file_server_root: config_file.file_server_root,
|
file_server_root: config_file.file_server_root,
|
||||||
},
|
},
|
||||||
handlebars,
|
handlebars,
|
||||||
|
hidden_path: Some (path),
|
||||||
});
|
});
|
||||||
|
|
||||||
let make_svc = make_service_fn (|_conn| {
|
let make_svc = make_service_fn (|_conn| {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::*,
|
collections::*,
|
||||||
convert::{TryFrom},
|
convert::{TryFrom, TryInto},
|
||||||
};
|
};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -85,11 +85,13 @@ pub struct WrappedRequest {
|
||||||
pub req: RequestParts,
|
pub req: RequestParts,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive (Debug, Deserialize, Serialize)]
|
#[derive (Debug, Deserialize, Serialize, PartialEq)]
|
||||||
pub enum StatusCode {
|
pub enum StatusCode {
|
||||||
Ok, // 200
|
Ok, // 200
|
||||||
PartialContent, // 206
|
PartialContent, // 206
|
||||||
|
|
||||||
|
TemporaryRedirect, // 307
|
||||||
|
|
||||||
BadRequest, // 400
|
BadRequest, // 400
|
||||||
Forbidden, // 403
|
Forbidden, // 403
|
||||||
NotFound, // 404
|
NotFound, // 404
|
||||||
|
@ -108,6 +110,8 @@ impl From <StatusCode> for hyper::StatusCode {
|
||||||
StatusCode::Ok => Self::OK,
|
StatusCode::Ok => Self::OK,
|
||||||
StatusCode::PartialContent => Self::PARTIAL_CONTENT,
|
StatusCode::PartialContent => Self::PARTIAL_CONTENT,
|
||||||
|
|
||||||
|
StatusCode::TemporaryRedirect => Self::TEMPORARY_REDIRECT,
|
||||||
|
|
||||||
StatusCode::BadRequest => Self::BAD_REQUEST,
|
StatusCode::BadRequest => Self::BAD_REQUEST,
|
||||||
StatusCode::Forbidden => Self::FORBIDDEN,
|
StatusCode::Forbidden => Self::FORBIDDEN,
|
||||||
StatusCode::NotFound => Self::NOT_FOUND,
|
StatusCode::NotFound => Self::NOT_FOUND,
|
||||||
|
@ -142,6 +146,24 @@ pub struct Response {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Response {
|
impl Response {
|
||||||
|
pub async fn into_bytes (self) -> Vec <u8> {
|
||||||
|
let mut body = match self.body {
|
||||||
|
None => return Vec::new (),
|
||||||
|
Some (x) => x,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut result = match self.content_length {
|
||||||
|
None => Vec::new (),
|
||||||
|
Some (x) => Vec::with_capacity (x.try_into ().unwrap ()),
|
||||||
|
};
|
||||||
|
|
||||||
|
while let Some (Ok (mut chunk)) = body.recv ().await {
|
||||||
|
result.append (&mut chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
pub fn status_code (&mut self, c: StatusCode) -> &mut Self {
|
pub fn status_code (&mut self, c: StatusCode) -> &mut Self {
|
||||||
self.parts.status_code = c;
|
self.parts.status_code = c;
|
||||||
self
|
self
|
||||||
|
@ -158,14 +180,12 @@ impl Response {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn body_bytes (&mut self, b: Vec <u8>) -> &mut Self {
|
pub fn body_bytes (&mut self, b: Vec <u8>) -> &mut Self {
|
||||||
use std::convert::TryInto;
|
|
||||||
|
|
||||||
self.content_length = b.len ().try_into ().ok ();
|
self.content_length = b.len ().try_into ().ok ();
|
||||||
self.header ("content-length".to_string (), b.len ().to_string ().into_bytes ());
|
self.header ("content-length".to_string (), b.len ().to_string ().into_bytes ());
|
||||||
|
|
||||||
let (mut tx, rx) = tokio::sync::mpsc::channel (1);
|
let (mut tx, rx) = tokio::sync::mpsc::channel (1);
|
||||||
tokio::spawn (async move {
|
tokio::spawn (async move {
|
||||||
tx.send (Ok (b)).await.unwrap ();
|
tx.send (Ok (b)).await.ok ();
|
||||||
});
|
});
|
||||||
self.body = Some (rx);
|
self.body = Some (rx);
|
||||||
|
|
||||||
|
|
|
@ -86,12 +86,10 @@ mod tests {
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use tracing::{info};
|
use tracing::{info};
|
||||||
|
|
||||||
// This should be the first line of the `tracing`
|
// Prefer this form for tests, since all tests share one process
|
||||||
// crate documentation. Their docs are awful, but you
|
// and we don't care if another test already installed a subscriber.
|
||||||
// didn't hear it from me.
|
|
||||||
|
|
||||||
tracing_subscriber::fmt::init ();
|
|
||||||
|
|
||||||
|
tracing_subscriber::fmt ().try_init ().ok ();
|
||||||
let mut rt = Runtime::new ().unwrap ();
|
let mut rt = Runtime::new ().unwrap ();
|
||||||
|
|
||||||
// Spawn the root task
|
// Spawn the root task
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Static file server that can plug into the PTTH reverse server
|
// Static file server that can plug into the PTTH reverse server
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
cmp::{min, max},
|
cmp::{min, max},
|
||||||
collections::*,
|
collections::*,
|
||||||
convert::{Infallible, TryInto},
|
convert::{Infallible, TryInto},
|
||||||
|
@ -28,7 +29,11 @@ use tracing::instrument;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
http_serde,
|
http_serde::{
|
||||||
|
Method,
|
||||||
|
Response,
|
||||||
|
StatusCode,
|
||||||
|
},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
prefix_match,
|
prefix_match,
|
||||||
};
|
};
|
||||||
|
@ -54,6 +59,8 @@ struct TemplateDirEntry {
|
||||||
|
|
||||||
encoded_file_name: String,
|
encoded_file_name: String,
|
||||||
|
|
||||||
|
size: Cow <'static, str>,
|
||||||
|
|
||||||
error: bool,
|
error: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,13 +104,28 @@ async fn read_dir_entry (entry: DirEntry) -> TemplateDirEntry
|
||||||
trailing_slash: "",
|
trailing_slash: "",
|
||||||
file_name: "File / directory name is not UTF-8".into (),
|
file_name: "File / directory name is not UTF-8".into (),
|
||||||
encoded_file_name: "".into (),
|
encoded_file_name: "".into (),
|
||||||
|
size: "".into (),
|
||||||
error: true,
|
error: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let (trailing_slash, icon) = match entry.file_type ().await {
|
let metadata = match entry.metadata ().await {
|
||||||
Ok (t) => if t.is_dir () {
|
Ok (x) => x,
|
||||||
("/", "📁")
|
Err (_) => return TemplateDirEntry {
|
||||||
|
icon: "⚠️",
|
||||||
|
trailing_slash: "",
|
||||||
|
file_name: "Could not fetch metadata".into (),
|
||||||
|
encoded_file_name: "".into (),
|
||||||
|
size: "".into (),
|
||||||
|
error: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let (trailing_slash, icon, size) = {
|
||||||
|
let t = metadata.file_type ();
|
||||||
|
|
||||||
|
if t.is_dir () {
|
||||||
|
("/", "📁", "".into ())
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let icon = if file_name.ends_with (".mp4") {
|
let icon = if file_name.ends_with (".mp4") {
|
||||||
|
@ -131,9 +153,8 @@ async fn read_dir_entry (entry: DirEntry) -> TemplateDirEntry
|
||||||
"📄"
|
"📄"
|
||||||
};
|
};
|
||||||
|
|
||||||
("", icon)
|
("", icon, pretty_print_bytes (metadata.len ()).into ())
|
||||||
},
|
}
|
||||||
Err (_) => ("", "⚠️"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use percent_encoding::*;
|
use percent_encoding::*;
|
||||||
|
@ -145,13 +166,14 @@ async fn read_dir_entry (entry: DirEntry) -> TemplateDirEntry
|
||||||
trailing_slash: &trailing_slash,
|
trailing_slash: &trailing_slash,
|
||||||
file_name,
|
file_name,
|
||||||
encoded_file_name,
|
encoded_file_name,
|
||||||
|
size,
|
||||||
error: false,
|
error: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn serve_root (
|
async fn serve_root (
|
||||||
handlebars: &Handlebars <'static>,
|
handlebars: &Handlebars <'static>,
|
||||||
) -> http_serde::Response
|
) -> Response
|
||||||
{
|
{
|
||||||
let server_info = ServerInfo {
|
let server_info = ServerInfo {
|
||||||
server_name: "PTTH file server",
|
server_name: "PTTH file server",
|
||||||
|
@ -160,7 +182,7 @@ async fn serve_root (
|
||||||
let s = handlebars.render ("file_server_root", &server_info).unwrap ();
|
let s = handlebars.render ("file_server_root", &server_info).unwrap ();
|
||||||
let body = s.into_bytes ();
|
let body = s.into_bytes ();
|
||||||
|
|
||||||
let mut resp = http_serde::Response::default ();
|
let mut resp = Response::default ();
|
||||||
resp
|
resp
|
||||||
.header ("content-type".to_string (), "text/html".to_string ().into_bytes ())
|
.header ("content-type".to_string (), "text/html".to_string ().into_bytes ())
|
||||||
.body_bytes (body)
|
.body_bytes (body)
|
||||||
|
@ -168,14 +190,12 @@ async fn serve_root (
|
||||||
resp
|
resp
|
||||||
}
|
}
|
||||||
|
|
||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
#[instrument (level = "debug", skip (handlebars, dir))]
|
#[instrument (level = "debug", skip (handlebars, dir))]
|
||||||
async fn serve_dir (
|
async fn serve_dir (
|
||||||
handlebars: &Handlebars <'static>,
|
handlebars: &Handlebars <'static>,
|
||||||
path: Cow <'_, str>,
|
path: Cow <'_, str>,
|
||||||
mut dir: ReadDir
|
mut dir: ReadDir
|
||||||
) -> http_serde::Response
|
) -> Response
|
||||||
{
|
{
|
||||||
let server_info = ServerInfo {
|
let server_info = ServerInfo {
|
||||||
server_name: "PTTH file server",
|
server_name: "PTTH file server",
|
||||||
|
@ -196,7 +216,7 @@ async fn serve_dir (
|
||||||
}).unwrap ();
|
}).unwrap ();
|
||||||
let body = s.into_bytes ();
|
let body = s.into_bytes ();
|
||||||
|
|
||||||
let mut resp = http_serde::Response::default ();
|
let mut resp = Response::default ();
|
||||||
resp
|
resp
|
||||||
.header ("content-type".to_string (), "text/html".to_string ().into_bytes ())
|
.header ("content-type".to_string (), "text/html".to_string ().into_bytes ())
|
||||||
.body_bytes (body)
|
.body_bytes (body)
|
||||||
|
@ -210,7 +230,9 @@ async fn serve_file (
|
||||||
should_send_body: bool,
|
should_send_body: bool,
|
||||||
range_start: Option <u64>,
|
range_start: Option <u64>,
|
||||||
range_end: Option <u64>
|
range_end: Option <u64>
|
||||||
) -> http_serde::Response {
|
)
|
||||||
|
-> Response
|
||||||
|
{
|
||||||
let (tx, rx) = channel (1);
|
let (tx, rx) = channel (1);
|
||||||
let body = if should_send_body {
|
let body = if should_send_body {
|
||||||
Some (rx)
|
Some (rx)
|
||||||
|
@ -271,17 +293,17 @@ async fn serve_file (
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut response = http_serde::Response::default ();
|
let mut response = Response::default ();
|
||||||
|
|
||||||
response.header (String::from ("accept-ranges"), b"bytes".to_vec ());
|
response.header (String::from ("accept-ranges"), b"bytes".to_vec ());
|
||||||
|
|
||||||
if should_send_body {
|
if should_send_body {
|
||||||
if range_start.is_none () && range_end.is_none () {
|
if range_start.is_none () && range_end.is_none () {
|
||||||
response.status_code (http_serde::StatusCode::Ok);
|
response.status_code (StatusCode::Ok);
|
||||||
response.header (String::from ("content-length"), end.to_string ().into_bytes ());
|
response.header (String::from ("content-length"), end.to_string ().into_bytes ());
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
response.status_code (http_serde::StatusCode::PartialContent);
|
response.status_code (StatusCode::PartialContent);
|
||||||
response.header (String::from ("content-range"), format! ("bytes {}-{}/{}", start, end - 1, end).into_bytes ());
|
response.header (String::from ("content-range"), format! ("bytes {}-{}/{}", start, end - 1, end).into_bytes ());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,15 +317,23 @@ async fn serve_file (
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn serve_error (
|
fn serve_error (
|
||||||
status_code: http_serde::StatusCode,
|
status_code: StatusCode,
|
||||||
msg: String
|
msg: &str
|
||||||
)
|
)
|
||||||
-> http_serde::Response
|
-> Response
|
||||||
{
|
{
|
||||||
let mut resp = http_serde::Response::default ();
|
let mut resp = Response::default ();
|
||||||
resp.status_code (status_code);
|
resp.status_code (status_code);
|
||||||
resp.body_bytes (msg.into_bytes ());
|
resp.body_bytes (msg.as_bytes ().to_vec ());
|
||||||
|
resp
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serve_307 (location: String) -> Response {
|
||||||
|
let mut resp = Response::default ();
|
||||||
|
resp.status_code (StatusCode::TemporaryRedirect);
|
||||||
|
resp.header ("location".to_string (), location.into_bytes ());
|
||||||
|
resp.body_bytes (b"Redirecting...".to_vec ());
|
||||||
resp
|
resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,18 +341,22 @@ async fn serve_error (
|
||||||
pub async fn serve_all (
|
pub async fn serve_all (
|
||||||
handlebars: &Handlebars <'static>,
|
handlebars: &Handlebars <'static>,
|
||||||
root: &Path,
|
root: &Path,
|
||||||
method: http_serde::Method,
|
method: Method,
|
||||||
uri: &str,
|
uri: &str,
|
||||||
headers: &HashMap <String, Vec <u8>>,
|
headers: &HashMap <String, Vec <u8>>,
|
||||||
hidden_path: Option <&Path>
|
hidden_path: Option <&Path>
|
||||||
)
|
)
|
||||||
-> http_serde::Response
|
-> Response
|
||||||
{
|
{
|
||||||
info! ("Client requested {}", uri);
|
info! ("Client requested {}", uri);
|
||||||
|
|
||||||
use percent_encoding::*;
|
use percent_encoding::*;
|
||||||
|
|
||||||
let uri = match prefix_match (uri, "/files/") {
|
if uri == "/favicon.ico" {
|
||||||
|
return serve_error (StatusCode::NotFound, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
let uri = match prefix_match (uri, "/files") {
|
||||||
Some (x) => x,
|
Some (x) => x,
|
||||||
None => {
|
None => {
|
||||||
return serve_root (handlebars).await;
|
return serve_root (handlebars).await;
|
||||||
|
@ -331,7 +365,7 @@ pub async fn serve_all (
|
||||||
|
|
||||||
// TODO: There is totally a dir traversal attack in here somewhere
|
// TODO: There is totally a dir traversal attack in here somewhere
|
||||||
|
|
||||||
let encoded_path = &uri [..];
|
let encoded_path = &uri [1..];
|
||||||
|
|
||||||
let path_s = percent_decode (encoded_path.as_bytes ()).decode_utf8 ().unwrap ();
|
let path_s = percent_decode (encoded_path.as_bytes ()).decode_utf8 ().unwrap ();
|
||||||
let path = Path::new (&*path_s);
|
let path = Path::new (&*path_s);
|
||||||
|
@ -343,14 +377,17 @@ pub async fn serve_all (
|
||||||
|
|
||||||
if let Some (hidden_path) = hidden_path {
|
if let Some (hidden_path) = hidden_path {
|
||||||
if full_path == hidden_path {
|
if full_path == hidden_path {
|
||||||
return serve_error (http_serde::StatusCode::Forbidden, "403 Forbidden".into ()).await;
|
return serve_error (StatusCode::Forbidden, "403 Forbidden");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if uri == "/" {
|
let has_trailing_slash = path_s.is_empty () || path_s.ends_with ("/");
|
||||||
serve_root (handlebars).await
|
|
||||||
|
if let Ok (dir) = read_dir (&full_path).await {
|
||||||
|
if ! has_trailing_slash {
|
||||||
|
return serve_307 (format! ("{}/", path.file_name ().unwrap ().to_str ().unwrap ()));
|
||||||
}
|
}
|
||||||
else if let Ok (dir) = read_dir (&full_path).await {
|
|
||||||
serve_dir (
|
serve_dir (
|
||||||
handlebars,
|
handlebars,
|
||||||
full_path.to_string_lossy (),
|
full_path.to_string_lossy (),
|
||||||
|
@ -370,11 +407,11 @@ pub async fn serve_all (
|
||||||
}
|
}
|
||||||
|
|
||||||
let should_send_body = match &method {
|
let should_send_body = match &method {
|
||||||
http_serde::Method::Get => true,
|
Method::Get => true,
|
||||||
http_serde::Method::Head => false,
|
Method::Head => false,
|
||||||
m => {
|
m => {
|
||||||
debug! ("Unsupported method {:?}", m);
|
debug! ("Unsupported method {:?}", m);
|
||||||
return serve_error (http_serde::StatusCode::MethodNotAllowed, "Unsupported method".into ()).await;
|
return serve_error (StatusCode::MethodNotAllowed, "Unsupported method");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -386,7 +423,7 @@ pub async fn serve_all (
|
||||||
).await
|
).await
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
serve_error (http_serde::StatusCode::NotFound, "404 Not Found".into ()).await
|
serve_error (StatusCode::NotFound, "404 Not Found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -406,15 +443,60 @@ pub fn load_templates ()
|
||||||
Ok (handlebars)
|
Ok (handlebars)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pretty_print_bytes (b: u64) -> String {
|
||||||
|
if b < 1024 {
|
||||||
|
format! ("{} B", b)
|
||||||
|
}
|
||||||
|
else if (b + 512) < 1024 * 1024 {
|
||||||
|
format! ("{} KiB", (b + 512) / 1024)
|
||||||
|
}
|
||||||
|
else if (b + 512 * 1024) < 1024 * 1024 * 1024 {
|
||||||
|
format! ("{} MiB", (b + 512 * 1024) / 1024 / 1024)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
format! ("{} GiB", (b + 512 * 1024 * 1024) / 1024 / 1024 / 1024)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg (test)]
|
#[cfg (test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
#[test]
|
|
||||||
fn i_hate_paths () {
|
|
||||||
use std::{
|
use std::{
|
||||||
ffi::OsStr,
|
ffi::OsStr,
|
||||||
path::{Component, Path}
|
path::{
|
||||||
|
Component,
|
||||||
|
Path,
|
||||||
|
PathBuf
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
|
use crate::http_serde::{
|
||||||
|
StatusCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pretty_print_bytes () {
|
||||||
|
for (input_after, expected_before, expected_after) in vec! [
|
||||||
|
(1, "0 B", "1 B"),
|
||||||
|
(1024, "1023 B", "1 KiB"),
|
||||||
|
(1024 + 512, "1 KiB", "2 KiB"),
|
||||||
|
(1023 * 1024 + 512, "1023 KiB", "1 MiB"),
|
||||||
|
((1024 + 512) * 1024, "1 MiB", "2 MiB"),
|
||||||
|
(1023 * 1024 * 1024 + 512 * 1024, "1023 MiB", "1 GiB"),
|
||||||
|
((1024 + 512) * 1024 * 1024, "1 GiB", "2 GiB"),
|
||||||
|
|
||||||
|
].into_iter () {
|
||||||
|
let actual = super::pretty_print_bytes (input_after - 1);
|
||||||
|
assert_eq! (&actual, expected_before);
|
||||||
|
|
||||||
|
let actual = super::pretty_print_bytes (input_after);
|
||||||
|
assert_eq! (&actual, expected_after);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn i_hate_paths () {
|
||||||
let mut components = Path::new ("/home/user").components ();
|
let mut components = Path::new ("/home/user").components ();
|
||||||
|
|
||||||
assert_eq! (components.next (), Some (Component::RootDir));
|
assert_eq! (components.next (), Some (Component::RootDir));
|
||||||
|
@ -434,4 +516,38 @@ mod tests {
|
||||||
assert_eq! (components.next (), Some (Component::CurDir));
|
assert_eq! (components.next (), Some (Component::CurDir));
|
||||||
assert_eq! (components.next (), None);
|
assert_eq! (components.next (), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_server () {
|
||||||
|
use crate::{
|
||||||
|
http_serde::Method,
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing_subscriber::fmt ().try_init ().ok ();
|
||||||
|
let mut rt = Runtime::new ().unwrap ();
|
||||||
|
|
||||||
|
rt.block_on (async {
|
||||||
|
let handlebars = super::load_templates ().unwrap ();
|
||||||
|
let file_server_root = PathBuf::from ("./");
|
||||||
|
let headers = Default::default ();
|
||||||
|
|
||||||
|
for (uri_path, expected_status) in vec! [
|
||||||
|
("/", StatusCode::Ok),
|
||||||
|
("/files/src", StatusCode::TemporaryRedirect),
|
||||||
|
("/files/src/", StatusCode::Ok),
|
||||||
|
].into_iter () {
|
||||||
|
let resp = super::serve_all (
|
||||||
|
&handlebars,
|
||||||
|
&file_server_root,
|
||||||
|
Method::Get,
|
||||||
|
uri_path,
|
||||||
|
&headers,
|
||||||
|
None
|
||||||
|
).await;
|
||||||
|
|
||||||
|
assert_eq! (resp.parts.status_code, expected_status);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
24
todo.md
24
todo.md
|
@ -1,10 +1,6 @@
|
||||||
- Not working behind Nginx (Works okay behind Caddy)
|
- Not working behind Nginx (Works okay behind Caddy)
|
||||||
- Reduce idle memory use?
|
- Reduce idle memory use?
|
||||||
|
|
||||||
- Compress bad passwords file
|
|
||||||
- Package templates into exe for release
|
|
||||||
- Redirect to add trailing slashes
|
|
||||||
- Add file size in directory listing
|
|
||||||
- Allow spaces in server names
|
- Allow spaces in server names
|
||||||
- Deny unused HTTP methods for endpoints
|
- Deny unused HTTP methods for endpoints
|
||||||
- ETag cache based on mtime
|
- ETag cache based on mtime
|
||||||
|
@ -17,11 +13,13 @@
|
||||||
|
|
||||||
- Reverse proxy to other local servers
|
- Reverse proxy to other local servers
|
||||||
|
|
||||||
Off-project stuff:
|
# Off-project stuff:
|
||||||
|
|
||||||
- Benchmark directory entry sorting
|
- Benchmark directory entry sorting
|
||||||
|
|
||||||
Known issues:
|
# Known issues:
|
||||||
|
|
||||||
|
## Graceful shutdown
|
||||||
|
|
||||||
Relay can't shut down gracefully if Firefox is connected to it, e.g. if Firefox
|
Relay can't shut down gracefully if Firefox is connected to it, e.g. if Firefox
|
||||||
kept a connection open while watching a video.
|
kept a connection open while watching a video.
|
||||||
|
@ -31,3 +29,17 @@ forced shutdown timer.
|
||||||
Sometimes I get the turtle icon in Firefox's network debugger. But this happens
|
Sometimes I get the turtle icon in Firefox's network debugger. But this happens
|
||||||
even with Caddy running a static file server, so I can't prove that it's on my
|
even with Caddy running a static file server, so I can't prove that it's on my
|
||||||
side. The VPS is cheap, and the datacenter is far away.
|
side. The VPS is cheap, and the datacenter is far away.
|
||||||
|
|
||||||
|
## Embedded asssets
|
||||||
|
|
||||||
|
The bad_passwords file is huge. Since it's static, it should only be in physical
|
||||||
|
RAM when the server first launches, and then the kernel will let it be paged
|
||||||
|
out.
|
||||||
|
|
||||||
|
Rust has some open issues with compiling assets into the exe, so I'm not
|
||||||
|
going to push on this for now, for neither bad_passwords nor the HTML assets:
|
||||||
|
|
||||||
|
https://github.com/rust-lang/rust/issues/65818
|
||||||
|
|
||||||
|
I also considered compressing the passwords file, but I couldn't even get
|
||||||
|
brotli to give it a decent ratio.
|
||||||
|
|
Loading…
Reference in New Issue