mirror of
https://github.com/servo/servo.git
synced 2025-07-22 23:03:42 +01: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",
|
"base64",
|
||||||
"brotli",
|
"brotli",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"chrono",
|
||||||
"content-security-policy",
|
"content-security-policy",
|
||||||
"cookie 0.12.0",
|
"cookie 0.12.0",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
|
|
|
@ -30,6 +30,7 @@ bluetooth_traits = { path = "components/shared/bluetooth" }
|
||||||
byteorder = "1.5"
|
byteorder = "1.5"
|
||||||
canvas_traits = { path = "components/shared/canvas" }
|
canvas_traits = { path = "components/shared/canvas" }
|
||||||
cfg-if = "1.0.0"
|
cfg-if = "1.0.0"
|
||||||
|
chrono = "0.4"
|
||||||
compositing_traits = { path = "components/shared/compositing" }
|
compositing_traits = { path = "components/shared/compositing" }
|
||||||
content-security-policy = { version = "0.5", features = ["serde"] }
|
content-security-policy = { version = "0.5", features = ["serde"] }
|
||||||
cookie = "0.12"
|
cookie = "0.12"
|
||||||
|
|
|
@ -551,6 +551,9 @@ mod gen {
|
||||||
#[serde(rename = "network.http-cache.disabled")]
|
#[serde(rename = "network.http-cache.disabled")]
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
},
|
},
|
||||||
|
local_directory_listing: {
|
||||||
|
enabled: bool,
|
||||||
|
},
|
||||||
mime: {
|
mime: {
|
||||||
sniff: bool,
|
sniff: bool,
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,11 +11,11 @@ name = "devtools"
|
||||||
path = "lib.rs"
|
path = "lib.rs"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
chrono = "0.4"
|
chrono = { workspace = true }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base = { workspace = true }
|
base = { workspace = true }
|
||||||
chrono = "0.4"
|
chrono = { workspace = true }
|
||||||
crossbeam-channel = { workspace = true }
|
crossbeam-channel = { workspace = true }
|
||||||
devtools_traits = { workspace = true }
|
devtools_traits = { workspace = true }
|
||||||
embedder_traits = { workspace = true }
|
embedder_traits = { workspace = true }
|
||||||
|
|
|
@ -58,6 +58,7 @@ servo_config = { path = "../config" }
|
||||||
servo_url = { path = "../url" }
|
servo_url = { path = "../url" }
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
time = { workspace = true }
|
time = { workspace = true }
|
||||||
|
chrono = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["sync", "macros", "rt-multi-thread"] }
|
tokio = { workspace = true, features = ["sync", "macros", "rt-multi-thread"] }
|
||||||
tokio-rustls = { workspace = true }
|
tokio-rustls = { workspace = true }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
|
|
|
@ -50,6 +50,7 @@ use crate::http_loader::{
|
||||||
determine_requests_referrer, http_fetch, set_default_accept, set_default_accept_language,
|
determine_requests_referrer, http_fetch, set_default_accept, set_default_accept_language,
|
||||||
HttpState,
|
HttpState,
|
||||||
};
|
};
|
||||||
|
use crate::local_directory_listing;
|
||||||
use crate::subresource_integrity::is_response_integrity_valid;
|
use crate::subresource_integrity::is_response_integrity_valid;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
@ -729,15 +730,11 @@ async fn scheme_fetch(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if let Ok(file_path) = url.to_file_path() {
|
if let Ok(file_path) = url.to_file_path() {
|
||||||
if let Ok(file) = File::open(file_path.clone()) {
|
if file_path.is_dir() {
|
||||||
if let Ok(metadata) = file.metadata() {
|
return local_directory_listing::fetch(request, url, file_path);
|
||||||
if metadata.is_dir() {
|
}
|
||||||
return Response::network_error(NetworkError::Internal(
|
|
||||||
"Opening a directory is not supported".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if let Ok(file) = File::open(file_path.clone()) {
|
||||||
// Get range bounds (if any) and try to seek to the requested offset.
|
// Get range bounds (if any) and try to seek to the requested offset.
|
||||||
// If seeking fails, bail out with a NetworkError.
|
// If seeking fails, bail out with a NetworkError.
|
||||||
let file_size = match file.metadata() {
|
let file_size = match file.metadata() {
|
||||||
|
|
|
@ -16,6 +16,7 @@ pub mod hsts;
|
||||||
pub mod http_cache;
|
pub mod http_cache;
|
||||||
pub mod http_loader;
|
pub mod http_loader;
|
||||||
pub mod image_cache;
|
pub mod image_cache;
|
||||||
|
pub mod local_directory_listing;
|
||||||
pub mod mime_classifier;
|
pub mod mime_classifier;
|
||||||
pub mod resource_thread;
|
pub mod resource_thread;
|
||||||
mod storage_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 }
|
bitflags = { workspace = true }
|
||||||
bluetooth_traits = { workspace = true }
|
bluetooth_traits = { workspace = true }
|
||||||
canvas_traits = { workspace = true }
|
canvas_traits = { workspace = true }
|
||||||
chrono = "0.4"
|
chrono = { workspace = true }
|
||||||
content-security-policy = { workspace = true }
|
content-security-policy = { workspace = true }
|
||||||
cookie = { workspace = true }
|
cookie = { workspace = true }
|
||||||
crossbeam-channel = { workspace = true }
|
crossbeam-channel = { workspace = true }
|
||||||
|
|
|
@ -71,6 +71,7 @@ pub enum Resource {
|
||||||
MediaControlsCSS,
|
MediaControlsCSS,
|
||||||
MediaControlsJS,
|
MediaControlsJS,
|
||||||
CrashHTML,
|
CrashHTML,
|
||||||
|
DirectoryListingHTML,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Resource {
|
impl Resource {
|
||||||
|
@ -90,6 +91,7 @@ impl Resource {
|
||||||
Resource::MediaControlsCSS => "media-controls.css",
|
Resource::MediaControlsCSS => "media-controls.css",
|
||||||
Resource::MediaControlsJS => "media-controls.js",
|
Resource::MediaControlsJS => "media-controls.js",
|
||||||
Resource::CrashHTML => "crash.html",
|
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")[..]
|
&include_bytes!("../../../resources/media-controls.js")[..]
|
||||||
},
|
},
|
||||||
Resource::CrashHTML => &include_bytes!("../../../resources/crash.html")[..],
|
Resource::CrashHTML => &include_bytes!("../../../resources/crash.html")[..],
|
||||||
|
Resource::DirectoryListingHTML => {
|
||||||
|
&include_bytes!("../../../resources/directory-listing.html")[..]
|
||||||
|
},
|
||||||
}
|
}
|
||||||
.to_owned()
|
.to_owned()
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,12 @@ pub enum Origin {
|
||||||
Origin(ImmutableOrigin),
|
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)
|
/// A [referer](https://fetch.spec.whatwg.org/#concept-request-referrer)
|
||||||
#[derive(Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)]
|
||||||
pub enum Referrer {
|
pub enum Referrer {
|
||||||
|
|
|
@ -931,6 +931,9 @@ impl ResourceReaderMethods for ResourceReaderInstance {
|
||||||
&include_bytes!("../../../../resources/media-controls.js")[..]
|
&include_bytes!("../../../../resources/media-controls.js")[..]
|
||||||
},
|
},
|
||||||
Resource::CrashHTML => &include_bytes!("../../../../resources/crash.html")[..],
|
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.localhost": false,
|
||||||
"network.enforce_tls.onion": false,
|
"network.enforce_tls.onion": false,
|
||||||
"network.http-cache.disabled": false,
|
"network.http-cache.disabled": false,
|
||||||
|
"network.local_directory_listing.enabled": false,
|
||||||
"network.mime.sniff": false,
|
"network.mime.sniff": false,
|
||||||
"session-history.max-length": 20,
|
"session-history.max-length": 20,
|
||||||
"shell.background-color.rgba": [1.0, 1.0, 1.0, 1.0],
|
"shell.background-color.rgba": [1.0, 1.0, 1.0, 1.0],
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue