1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-25 00:29:09 -05:00

feat(ext/fetch): support fetching local files (#12545)

Closes #11925
Closes #2150

Co-authored-by: Bert Belder <bertbelder@gmail.com>
This commit is contained in:
Kitson Kelly 2021-11-01 15:29:46 +11:00 committed by GitHub
parent d080f1c965
commit d3662e487d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 196 additions and 10 deletions

View file

@ -23,7 +23,7 @@ unitTest(
unitTest({ permissions: { net: true } }, async function fetchProtocolError() { unitTest({ permissions: { net: true } }, async function fetchProtocolError() {
await assertRejects( await assertRejects(
async () => { async () => {
await fetch("file:///"); await fetch("ftp://localhost:21/a/file");
}, },
TypeError, TypeError,
"not supported", "not supported",
@ -1360,3 +1360,59 @@ unitTest(
client.close(); client.close();
}, },
); );
unitTest(async function fetchFilePerm() {
await assertRejects(async () => {
await fetch(new URL("../testdata/subdir/json_1.json", import.meta.url));
}, Deno.errors.PermissionDenied);
});
unitTest(async function fetchFilePermDoesNotExist() {
await assertRejects(async () => {
await fetch(new URL("./bad.json", import.meta.url));
}, Deno.errors.PermissionDenied);
});
unitTest(
{ permissions: { read: true } },
async function fetchFileBadMethod() {
await assertRejects(
async () => {
await fetch(
new URL("../testdata/subdir/json_1.json", import.meta.url),
{
method: "POST",
},
);
},
TypeError,
"Fetching files only supports the GET method. Received POST.",
);
},
);
unitTest(
{ permissions: { read: true } },
async function fetchFileDoesNotExist() {
await assertRejects(
async () => {
await fetch(new URL("./bad.json", import.meta.url));
},
TypeError,
);
},
);
unitTest(
{ permissions: { read: true } },
async function fetchFile() {
const res = await fetch(
new URL("../testdata/subdir/json_1.json", import.meta.url),
);
assert(res.ok);
const fixture = await Deno.readTextFile(
"cli/tests/testdata/subdir/json_1.json",
);
assertEquals(await res.text(), fixture);
},
);

View file

@ -0,0 +1,52 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
use crate::CancelHandle;
use crate::CancelableResponseFuture;
use crate::FetchHandler;
use crate::FetchRequestBodyResource;
use deno_core::error::type_error;
use deno_core::futures::FutureExt;
use deno_core::futures::TryFutureExt;
use deno_core::url::Url;
use deno_core::CancelFuture;
use reqwest::StatusCode;
use std::rc::Rc;
use tokio_util::io::ReaderStream;
/// An implementation which tries to read file URLs from the file system via
/// tokio::fs.
#[derive(Clone)]
pub struct FsFetchHandler;
impl FetchHandler for FsFetchHandler {
fn fetch_file(
&mut self,
url: Url,
) -> (
CancelableResponseFuture,
Option<FetchRequestBodyResource>,
Option<Rc<CancelHandle>>,
) {
let cancel_handle = CancelHandle::new_rc();
let response_fut = async move {
let path = url.to_file_path()?;
let file = tokio::fs::File::open(path).map_err(|_| ()).await?;
let stream = ReaderStream::new(file);
let body = reqwest::Body::wrap_stream(stream);
let response = http::Response::builder()
.status(StatusCode::OK)
.body(body)
.map_err(|_| ())?
.into();
Ok::<_, ()>(response)
}
.map_err(move |_| {
type_error("NetworkError when attempting to fetch resource.")
})
.or_cancel(&cancel_handle)
.boxed_local();
(response_fut, None, Some(cancel_handle))
}
}

View file

@ -1,5 +1,7 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
mod fs_fetch_handler;
use data_url::DataUrl; use data_url::DataUrl;
use deno_core::error::type_error; use deno_core::error::type_error;
use deno_core::error::AnyError; use deno_core::error::AnyError;
@ -52,14 +54,21 @@ use tokio_util::io::StreamReader;
pub use data_url; pub use data_url;
pub use reqwest; pub use reqwest;
pub fn init<P: FetchPermissions + 'static>( pub use fs_fetch_handler::FsFetchHandler;
pub fn init<FP, FH>(
user_agent: String, user_agent: String,
root_cert_store: Option<RootCertStore>, root_cert_store: Option<RootCertStore>,
proxy: Option<Proxy>, proxy: Option<Proxy>,
request_builder_hook: Option<fn(RequestBuilder) -> RequestBuilder>, request_builder_hook: Option<fn(RequestBuilder) -> RequestBuilder>,
unsafely_ignore_certificate_errors: Option<Vec<String>>, unsafely_ignore_certificate_errors: Option<Vec<String>>,
client_cert_chain_and_key: Option<(String, String)>, client_cert_chain_and_key: Option<(String, String)>,
) -> Extension { file_fetch_handler: FH,
) -> Extension
where
FP: FetchPermissions + 'static,
FH: FetchHandler + 'static,
{
Extension::builder() Extension::builder()
.js(include_js_files!( .js(include_js_files!(
prefix "deno:ext/fetch", prefix "deno:ext/fetch",
@ -73,13 +82,13 @@ pub fn init<P: FetchPermissions + 'static>(
"26_fetch.js", "26_fetch.js",
)) ))
.ops(vec![ .ops(vec![
("op_fetch", op_sync(op_fetch::<P>)), ("op_fetch", op_sync(op_fetch::<FP, FH>)),
("op_fetch_send", op_async(op_fetch_send)), ("op_fetch_send", op_async(op_fetch_send)),
("op_fetch_request_write", op_async(op_fetch_request_write)), ("op_fetch_request_write", op_async(op_fetch_request_write)),
("op_fetch_response_read", op_async(op_fetch_response_read)), ("op_fetch_response_read", op_async(op_fetch_response_read)),
( (
"op_fetch_custom_client", "op_fetch_custom_client",
op_sync(op_fetch_custom_client::<P>), op_sync(op_fetch_custom_client::<FP>),
), ),
]) ])
.state(move |state| { .state(move |state| {
@ -103,6 +112,7 @@ pub fn init<P: FetchPermissions + 'static>(
.clone(), .clone(),
client_cert_chain_and_key: client_cert_chain_and_key.clone(), client_cert_chain_and_key: client_cert_chain_and_key.clone(),
}); });
state.put::<FH>(file_fetch_handler.clone());
Ok(()) Ok(())
}) })
.build() .build()
@ -117,6 +127,45 @@ pub struct HttpClientDefaults {
pub client_cert_chain_and_key: Option<(String, String)>, pub client_cert_chain_and_key: Option<(String, String)>,
} }
pub type CancelableResponseFuture =
Pin<Box<dyn Future<Output = CancelableResponseResult>>>;
pub trait FetchHandler: Clone {
// Return the result of the fetch request consisting of a tuple of the
// cancelable response result, the optional fetch body resource and the
// optional cancel handle.
fn fetch_file(
&mut self,
url: Url,
) -> (
CancelableResponseFuture,
Option<FetchRequestBodyResource>,
Option<Rc<CancelHandle>>,
);
}
/// A default implementation which will error for every request.
#[derive(Clone)]
pub struct DefaultFileFetchHandler;
impl FetchHandler for DefaultFileFetchHandler {
fn fetch_file(
&mut self,
_url: Url,
) -> (
CancelableResponseFuture,
Option<FetchRequestBodyResource>,
Option<Rc<CancelHandle>>,
) {
let fut = async move {
Ok(Err(type_error(
"NetworkError when attempting to fetch resource.",
)))
};
(Box::pin(fut), None, None)
}
}
pub trait FetchPermissions { pub trait FetchPermissions {
fn check_net_url(&mut self, _url: &Url) -> Result<(), AnyError>; fn check_net_url(&mut self, _url: &Url) -> Result<(), AnyError>;
fn check_read(&mut self, _p: &Path) -> Result<(), AnyError>; fn check_read(&mut self, _p: &Path) -> Result<(), AnyError>;
@ -145,13 +194,14 @@ pub struct FetchReturn {
cancel_handle_rid: Option<ResourceId>, cancel_handle_rid: Option<ResourceId>,
} }
pub fn op_fetch<FP>( pub fn op_fetch<FP, FH>(
state: &mut OpState, state: &mut OpState,
args: FetchArgs, args: FetchArgs,
data: Option<ZeroCopyBuf>, data: Option<ZeroCopyBuf>,
) -> Result<FetchReturn, AnyError> ) -> Result<FetchReturn, AnyError>
where where
FP: FetchPermissions + 'static, FP: FetchPermissions + 'static,
FH: FetchHandler + 'static,
{ {
let client = if let Some(rid) = args.client_rid { let client = if let Some(rid) = args.client_rid {
let r = state.resource_table.get::<HttpClientResource>(rid)?; let r = state.resource_table.get::<HttpClientResource>(rid)?;
@ -167,6 +217,31 @@ where
// Check scheme before asking for net permission // Check scheme before asking for net permission
let scheme = url.scheme(); let scheme = url.scheme();
let (request_rid, request_body_rid, cancel_handle_rid) = match scheme { let (request_rid, request_body_rid, cancel_handle_rid) = match scheme {
"file" => {
let path = url.to_file_path().map_err(|_| {
type_error("NetworkError when attempting to fetch resource.")
})?;
let permissions = state.borrow_mut::<FP>();
permissions.check_read(&path)?;
if method != Method::GET {
return Err(type_error(format!(
"Fetching files only supports the GET method. Received {}.",
method
)));
}
let file_fetch_handler = state.borrow_mut::<FH>();
let (request, maybe_request_body, maybe_cancel_handle) =
file_fetch_handler.fetch_file(url);
let request_rid = state.resource_table.add(FetchRequestResource(request));
let maybe_request_body_rid =
maybe_request_body.map(|r| state.resource_table.add(r));
let maybe_cancel_handle_rid = maybe_cancel_handle
.map(|ch| state.resource_table.add(FetchCancelHandle(ch)));
(request_rid, maybe_request_body_rid, maybe_cancel_handle_rid)
}
"http" | "https" => { "http" | "https" => {
let permissions = state.borrow_mut::<FP>(); let permissions = state.borrow_mut::<FP>();
permissions.check_net_url(&url)?; permissions.check_net_url(&url)?;
@ -400,7 +475,7 @@ impl Resource for FetchCancelHandle {
} }
} }
struct FetchRequestBodyResource { pub struct FetchRequestBodyResource {
body: AsyncRefCell<mpsc::Sender<std::io::Result<Vec<u8>>>>, body: AsyncRefCell<mpsc::Sender<std::io::Result<Vec<u8>>>>,
cancel: CancelHandle, cancel: CancelHandle,
} }

View file

@ -121,13 +121,14 @@ mod not_docs {
deno_url::init(), deno_url::init(),
deno_tls::init(), deno_tls::init(),
deno_web::init(deno_web::BlobStore::default(), Default::default()), deno_web::init(deno_web::BlobStore::default(), Default::default()),
deno_fetch::init::<Permissions>( deno_fetch::init::<Permissions, deno_fetch::DefaultFileFetchHandler>(
"".to_owned(), "".to_owned(),
None, None,
None, None,
None, None,
None, None,
None, None,
deno_fetch::DefaultFileFetchHandler, // No enable_file_fetch
), ),
deno_websocket::init::<Permissions>("".to_owned(), None, None), deno_websocket::init::<Permissions>("".to_owned(), None, None),
deno_webstorage::init(None), deno_webstorage::init(None),

View file

@ -317,13 +317,14 @@ impl WebWorker {
deno_console::init(), deno_console::init(),
deno_url::init(), deno_url::init(),
deno_web::init(options.blob_store.clone(), Some(main_module.clone())), deno_web::init(options.blob_store.clone(), Some(main_module.clone())),
deno_fetch::init::<Permissions>( deno_fetch::init::<Permissions, deno_fetch::FsFetchHandler>(
options.user_agent.clone(), options.user_agent.clone(),
options.root_cert_store.clone(), options.root_cert_store.clone(),
None, None,
None, None,
options.unsafely_ignore_certificate_errors.clone(), options.unsafely_ignore_certificate_errors.clone(),
None, None,
deno_fetch::FsFetchHandler,
), ),
deno_websocket::init::<Permissions>( deno_websocket::init::<Permissions>(
options.user_agent.clone(), options.user_agent.clone(),

View file

@ -101,13 +101,14 @@ impl MainWorker {
options.blob_store.clone(), options.blob_store.clone(),
options.bootstrap.location.clone(), options.bootstrap.location.clone(),
), ),
deno_fetch::init::<Permissions>( deno_fetch::init::<Permissions, deno_fetch::FsFetchHandler>(
options.user_agent.clone(), options.user_agent.clone(),
options.root_cert_store.clone(), options.root_cert_store.clone(),
None, None,
None, None,
options.unsafely_ignore_certificate_errors.clone(), options.unsafely_ignore_certificate_errors.clone(),
None, None,
deno_fetch::FsFetchHandler,
), ),
deno_websocket::init::<Permissions>( deno_websocket::init::<Permissions>(
options.user_agent.clone(), options.user_agent.clone(),