mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
Add a directory listing feature for file
URLs (#32580)
Signed-off-by: Bobulous <Bobulous@users.noreply.github.com> Signed-off-by: Martin Robinson <mrobinson@igalia.com> Co-authored-by: Bobulous <Bobulous@users.noreply.github.com>
This commit is contained in:
parent
b3d99a607f
commit
7ea894774f
14 changed files with 291 additions and 11 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4061,6 +4061,7 @@ dependencies = [
|
|||
"base64",
|
||||
"brotli",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"content-security-policy",
|
||||
"cookie 0.12.0",
|
||||
"crossbeam-channel",
|
||||
|
|
|
@ -30,6 +30,7 @@ bluetooth_traits = { path = "components/shared/bluetooth" }
|
|||
byteorder = "1.5"
|
||||
canvas_traits = { path = "components/shared/canvas" }
|
||||
cfg-if = "1.0.0"
|
||||
chrono = "0.4"
|
||||
compositing_traits = { path = "components/shared/compositing" }
|
||||
content-security-policy = { version = "0.5", features = ["serde"] }
|
||||
cookie = "0.12"
|
||||
|
|
|
@ -551,6 +551,9 @@ mod gen {
|
|||
#[serde(rename = "network.http-cache.disabled")]
|
||||
disabled: bool,
|
||||
},
|
||||
local_directory_listing: {
|
||||
enabled: bool,
|
||||
},
|
||||
mime: {
|
||||
sniff: bool,
|
||||
}
|
||||
|
|
|
@ -11,11 +11,11 @@ name = "devtools"
|
|||
path = "lib.rs"
|
||||
|
||||
[build-dependencies]
|
||||
chrono = "0.4"
|
||||
chrono = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
base = { workspace = true }
|
||||
chrono = "0.4"
|
||||
chrono = { workspace = true }
|
||||
crossbeam-channel = { workspace = true }
|
||||
devtools_traits = { workspace = true }
|
||||
embedder_traits = { workspace = true }
|
||||
|
|
|
@ -58,6 +58,7 @@ servo_config = { path = "../config" }
|
|||
servo_url = { path = "../url" }
|
||||
sha2 = "0.10"
|
||||
time = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync", "macros", "rt-multi-thread"] }
|
||||
tokio-rustls = { workspace = true }
|
||||
tokio-stream = "0.1"
|
||||
|
|
|
@ -50,6 +50,7 @@ use crate::http_loader::{
|
|||
determine_requests_referrer, http_fetch, set_default_accept, set_default_accept_language,
|
||||
HttpState,
|
||||
};
|
||||
use crate::local_directory_listing;
|
||||
use crate::subresource_integrity::is_response_integrity_valid;
|
||||
|
||||
lazy_static! {
|
||||
|
@ -729,15 +730,11 @@ async fn scheme_fetch(
|
|||
));
|
||||
}
|
||||
if let Ok(file_path) = url.to_file_path() {
|
||||
if let Ok(file) = File::open(file_path.clone()) {
|
||||
if let Ok(metadata) = file.metadata() {
|
||||
if metadata.is_dir() {
|
||||
return Response::network_error(NetworkError::Internal(
|
||||
"Opening a directory is not supported".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
if file_path.is_dir() {
|
||||
return local_directory_listing::fetch(request, url, file_path);
|
||||
}
|
||||
|
||||
if let Ok(file) = File::open(file_path.clone()) {
|
||||
// Get range bounds (if any) and try to seek to the requested offset.
|
||||
// If seeking fails, bail out with a NetworkError.
|
||||
let file_size = match file.metadata() {
|
||||
|
|
|
@ -16,6 +16,7 @@ pub mod hsts;
|
|||
pub mod http_cache;
|
||||
pub mod http_loader;
|
||||
pub mod image_cache;
|
||||
pub mod local_directory_listing;
|
||||
pub mod mime_classifier;
|
||||
pub mod resource_thread;
|
||||
mod storage_thread;
|
||||
|
|
158
components/net/local_directory_listing.rs
Normal file
158
components/net/local_directory_listing.rs
Normal file
|
@ -0,0 +1,158 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use std::fs::{DirEntry, Metadata, ReadDir};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use chrono::{DateTime, Local};
|
||||
use embedder_traits::resources::{read_string, Resource};
|
||||
use headers::{ContentType, HeaderMapExt};
|
||||
use net_traits::request::Request;
|
||||
use net_traits::response::{Response, ResponseBody};
|
||||
use net_traits::{NetworkError, ResourceFetchTiming};
|
||||
use servo_config::pref;
|
||||
use servo_url::ServoUrl;
|
||||
use url::Url;
|
||||
|
||||
pub fn fetch(request: &mut Request, url: ServoUrl, path_buf: PathBuf) -> Response {
|
||||
if !pref!(network.local_directory_listing.enabled) {
|
||||
// If you want to be able to browse local directories, configure Servo prefs so that
|
||||
// "network.local_directory_listing.enabled" is set to true.
|
||||
return Response::network_error(NetworkError::Internal(
|
||||
"Local directory listing feature has not been enabled in preferences".into(),
|
||||
));
|
||||
}
|
||||
|
||||
if !request.origin.is_opaque() {
|
||||
// Checking for an opaque origin as a shorthand for user activation
|
||||
// as opposed to a request originating from a script.
|
||||
// TODO(32534): carefully consider security of this approach.
|
||||
return Response::network_error(NetworkError::Internal(
|
||||
"Cannot request local directory listing from non-local origin.".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let directory_contents = match std::fs::read_dir(path_buf.clone()) {
|
||||
Ok(directory_contents) => directory_contents,
|
||||
Err(error) => {
|
||||
return Response::network_error(NetworkError::Internal(format!(
|
||||
"Unable to access directory: {error}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
|
||||
let output = build_html_directory_listing(url.as_url(), path_buf, directory_contents);
|
||||
|
||||
let mut response = Response::new(url, ResourceFetchTiming::new(request.timing_type()));
|
||||
response.headers.typed_insert(ContentType::html());
|
||||
*response.body.lock().unwrap() = ResponseBody::Done(output.into_bytes());
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
/// Returns an the string of an JavaScript `<script>` tag calling the `setData` function with the
|
||||
/// contents of the given [`ReadDir`] directory listing.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `url` - the original URL of the request that triggered this directory listing.
|
||||
/// * `path` - the full path to the local directory.
|
||||
/// * `directory_contents` - a [`ReadDir`] with the contents of the directory.
|
||||
pub fn build_html_directory_listing(
|
||||
url: &Url,
|
||||
path: PathBuf,
|
||||
directory_contents: ReadDir,
|
||||
) -> String {
|
||||
let mut page_html = String::with_capacity(1024);
|
||||
page_html.push_str("<!DOCTYPE html>");
|
||||
|
||||
let mut parent_url_string = String::new();
|
||||
if path.parent().is_some() {
|
||||
let mut parent_url = url.clone();
|
||||
if let Ok(mut path_segments) = parent_url.path_segments_mut() {
|
||||
path_segments.pop();
|
||||
}
|
||||
parent_url_string = parent_url.as_str().to_owned();
|
||||
}
|
||||
|
||||
page_html.push_str(&read_string(Resource::DirectoryListingHTML));
|
||||
|
||||
page_html.push_str("<script>\n");
|
||||
page_html.push_str(&format!(
|
||||
"setData({:?}, {:?}, [",
|
||||
url.as_str(),
|
||||
parent_url_string
|
||||
));
|
||||
|
||||
for directory_entry in directory_contents {
|
||||
let Ok(directory_entry) = directory_entry else {
|
||||
continue;
|
||||
};
|
||||
let Ok(metadata) = directory_entry.metadata() else {
|
||||
continue;
|
||||
};
|
||||
write_directory_entry(directory_entry, metadata, url, &mut page_html);
|
||||
}
|
||||
|
||||
page_html.push_str("]);");
|
||||
page_html.push_str("</script>\n");
|
||||
|
||||
page_html
|
||||
}
|
||||
|
||||
fn write_directory_entry(entry: DirEntry, metadata: Metadata, url: &Url, output: &mut String) {
|
||||
let Ok(name) = entry.file_name().into_string() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut file_url = url.clone();
|
||||
{
|
||||
let Ok(mut path_segments) = file_url.path_segments_mut() else {
|
||||
return;
|
||||
};
|
||||
path_segments.push(&name);
|
||||
}
|
||||
|
||||
let class = if metadata.is_dir() {
|
||||
"directory"
|
||||
} else if metadata.is_symlink() {
|
||||
"symlink"
|
||||
} else {
|
||||
"file"
|
||||
};
|
||||
|
||||
let file_url_string = &file_url.to_string();
|
||||
let file_size = metadata_to_file_size_string(&metadata);
|
||||
let last_modified = metadata
|
||||
.modified()
|
||||
.map(|time| DateTime::<Local>::from(time))
|
||||
.map(|time| time.format("%F %r").to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
output.push_str(&format!(
|
||||
"[{class:?}, {name:?}, {file_url_string:?}, {file_size:?}, {last_modified:?}],"
|
||||
));
|
||||
}
|
||||
|
||||
pub fn metadata_to_file_size_string(metadata: &Metadata) -> String {
|
||||
if !metadata.is_file() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let mut float_size = metadata.len() as f64;
|
||||
let mut prefix_power = 0;
|
||||
while float_size > 1000.0 && prefix_power < 3 {
|
||||
float_size /= 1000.0;
|
||||
prefix_power += 1;
|
||||
}
|
||||
|
||||
let prefix = match prefix_power {
|
||||
0 => "B",
|
||||
1 => "KB",
|
||||
2 => "MB",
|
||||
_ => "GB",
|
||||
};
|
||||
|
||||
return format!("{:.2} {prefix}", float_size);
|
||||
}
|
|
@ -37,7 +37,7 @@ base64 = { workspace = true }
|
|||
bitflags = { workspace = true }
|
||||
bluetooth_traits = { workspace = true }
|
||||
canvas_traits = { workspace = true }
|
||||
chrono = "0.4"
|
||||
chrono = { workspace = true }
|
||||
content-security-policy = { workspace = true }
|
||||
cookie = { workspace = true }
|
||||
crossbeam-channel = { workspace = true }
|
||||
|
|
|
@ -71,6 +71,7 @@ pub enum Resource {
|
|||
MediaControlsCSS,
|
||||
MediaControlsJS,
|
||||
CrashHTML,
|
||||
DirectoryListingHTML,
|
||||
}
|
||||
|
||||
impl Resource {
|
||||
|
@ -90,6 +91,7 @@ impl Resource {
|
|||
Resource::MediaControlsCSS => "media-controls.css",
|
||||
Resource::MediaControlsJS => "media-controls.js",
|
||||
Resource::CrashHTML => "crash.html",
|
||||
Resource::DirectoryListingHTML => "directory-listing.html",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -146,6 +148,9 @@ fn resources_for_tests() -> Box<dyn ResourceReaderMethods + Sync + Send> {
|
|||
&include_bytes!("../../../resources/media-controls.js")[..]
|
||||
},
|
||||
Resource::CrashHTML => &include_bytes!("../../../resources/crash.html")[..],
|
||||
Resource::DirectoryListingHTML => {
|
||||
&include_bytes!("../../../resources/directory-listing.html")[..]
|
||||
},
|
||||
}
|
||||
.to_owned()
|
||||
}
|
||||
|
|
|
@ -37,6 +37,12 @@ pub enum Origin {
|
|||
Origin(ImmutableOrigin),
|
||||
}
|
||||
|
||||
impl Origin {
|
||||
pub fn is_opaque(&self) -> bool {
|
||||
matches!(self, Origin::Origin(ImmutableOrigin::Opaque(_)))
|
||||
}
|
||||
}
|
||||
|
||||
/// A [referer](https://fetch.spec.whatwg.org/#concept-request-referrer)
|
||||
#[derive(Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)]
|
||||
pub enum Referrer {
|
||||
|
|
|
@ -931,6 +931,9 @@ impl ResourceReaderMethods for ResourceReaderInstance {
|
|||
&include_bytes!("../../../../resources/media-controls.js")[..]
|
||||
},
|
||||
Resource::CrashHTML => &include_bytes!("../../../../resources/crash.html")[..],
|
||||
Resource::DirectoryListingHTML => {
|
||||
&include_bytes!("../../../../resources/directory-listing.html")[..]
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
103
resources/directory-listing.html
Normal file
103
resources/directory-listing.html
Normal file
|
@ -0,0 +1,103 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Index of</title>
|
||||
<style>
|
||||
h1 {
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
.listing {
|
||||
margin: 0.5em;
|
||||
display: table;
|
||||
}
|
||||
|
||||
.listing .header,
|
||||
.listing .entry {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.listing .header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.listing .header span,
|
||||
.listing .entry span {
|
||||
display: table-cell;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.entry span:nth-child(1) {
|
||||
min-width: 20em;
|
||||
}
|
||||
|
||||
.entry span.size,
|
||||
.entry span.last-modified {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.parent_link > a:before {
|
||||
content: "📁 ";
|
||||
}
|
||||
|
||||
.parent_link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.entry.directory > .name:before {
|
||||
content: "📁 ";
|
||||
}
|
||||
|
||||
.entry.file > .name:before {
|
||||
content: "📄 ";
|
||||
}
|
||||
|
||||
.entry.symlink > .name:before {
|
||||
content: "🔗 ";
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function setData(directoryName, parentLink, rows) {
|
||||
document.title += directoryName;
|
||||
document.querySelector("h1").innerHTML += " " + directoryName;
|
||||
|
||||
if (parentLink != "") {
|
||||
document.querySelector(".parent_link > a").href = parentLink;
|
||||
document.querySelector(".parent_link").style.display = "initial";
|
||||
}
|
||||
|
||||
rows.sort((rowA, rowB) => rowA[1].localeCompare(rowB[1]));
|
||||
|
||||
let listing = document.querySelector(".listing");
|
||||
let rowTemplate = document.getElementById("rowTemplate");
|
||||
for (row of rows) {
|
||||
let rowElement = rowTemplate.content.cloneNode(true);
|
||||
rowElement.querySelector(".entry").classList.add(row[0]);
|
||||
rowElement.querySelector(".name > .link").innerText = row[1];
|
||||
rowElement.querySelector(".name > .link").href = row[2];
|
||||
rowElement.querySelector(".size").innerText = row[3];
|
||||
rowElement.querySelector(".last-modified").innerText = row[4];
|
||||
listing.appendChild(rowElement);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Index of</h1>
|
||||
<div class="parent_link"><a href="">Up to parent directory</a></div>
|
||||
<div class="listing">
|
||||
<div class="header">
|
||||
<span class="name">Name</span>
|
||||
<span class="size">Size</span>
|
||||
<span class="last-modified">Last Modified</span>
|
||||
</div>
|
||||
</div>
|
||||
<template id="rowTemplate">
|
||||
<div class="entry">
|
||||
<span class="name"><a class="link"></a></span>
|
||||
<span class="size"></span>
|
||||
<span class="last-modified"></span>
|
||||
</div>
|
||||
</template>
|
||||
</body>
|
||||
</html>
|
|
@ -111,6 +111,7 @@
|
|||
"network.enforce_tls.localhost": false,
|
||||
"network.enforce_tls.onion": false,
|
||||
"network.http-cache.disabled": false,
|
||||
"network.local_directory_listing.enabled": false,
|
||||
"network.mime.sniff": false,
|
||||
"session-history.max-length": 20,
|
||||
"shell.background-color.rgba": [1.0, 1.0, 1.0, 1.0],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue