Skip to content

Commit

Permalink
Add a directory listing feature for file URLs (servo#32580)
Browse files Browse the repository at this point in the history
Signed-off-by: Bobulous <[email protected]>
Signed-off-by: Martin Robinson <[email protected]>
Co-authored-by: Bobulous <[email protected]>
  • Loading branch information
mrobinson and Bobulous authored Jun 26, 2024
1 parent b3d99a6 commit 7ea8947
Show file tree
Hide file tree
Showing 14 changed files with 291 additions and 11 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions components/config/prefs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,9 @@ mod gen {
#[serde(rename = "network.http-cache.disabled")]
disabled: bool,
},
local_directory_listing: {
enabled: bool,
},
mime: {
sniff: bool,
}
Expand Down
4 changes: 2 additions & 2 deletions components/devtools/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
1 change: 1 addition & 0 deletions components/net/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 5 additions & 8 deletions components/net/fetch/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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! {
Expand Down Expand Up @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions components/net/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
158 changes: 158 additions & 0 deletions components/net/local_directory_listing.rs
Original file line number Diff line number Diff line change
@@ -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);
}
2 changes: 1 addition & 1 deletion components/script/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
5 changes: 5 additions & 0 deletions components/shared/embedder/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ pub enum Resource {
MediaControlsCSS,
MediaControlsJS,
CrashHTML,
DirectoryListingHTML,
}

impl Resource {
Expand All @@ -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",
}
}
}
Expand Down Expand Up @@ -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()
}
Expand Down
6 changes: 6 additions & 0 deletions components/shared/net/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions ports/servoshell/egl/android/simpleservo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")[..]
},
})
}

Expand Down
Loading

0 comments on commit 7ea8947

Please sign in to comment.