diff --git a/cli/tests/unit/fetch_test.ts b/cli/tests/unit/fetch_test.ts index bc61d67b52da38..98134728e43940 100644 --- a/cli/tests/unit/fetch_test.ts +++ b/cli/tests/unit/fetch_test.ts @@ -23,7 +23,7 @@ unitTest( unitTest({ permissions: { net: true } }, async function fetchProtocolError() { await assertRejects( async () => { - await fetch("file:///"); + await fetch("ftp://localhost:21/a/file"); }, TypeError, "not supported", @@ -1360,3 +1360,59 @@ unitTest( 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); + }, +); diff --git a/ext/fetch/fs_fetch_handler.rs b/ext/fetch/fs_fetch_handler.rs new file mode 100644 index 00000000000000..82cbc4ecb39305 --- /dev/null +++ b/ext/fetch/fs_fetch_handler.rs @@ -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, + Option>, + ) { + 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)) + } +} diff --git a/ext/fetch/lib.rs b/ext/fetch/lib.rs index 13adae1a761ac5..b4bffb6de4e8fe 100644 --- a/ext/fetch/lib.rs +++ b/ext/fetch/lib.rs @@ -1,5 +1,7 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +mod fs_fetch_handler; + use data_url::DataUrl; use deno_core::error::type_error; use deno_core::error::AnyError; @@ -52,14 +54,21 @@ use tokio_util::io::StreamReader; pub use data_url; pub use reqwest; -pub fn init( +pub use fs_fetch_handler::FsFetchHandler; + +pub fn init( user_agent: String, root_cert_store: Option, proxy: Option, request_builder_hook: Option RequestBuilder>, unsafely_ignore_certificate_errors: Option>, client_cert_chain_and_key: Option<(String, String)>, -) -> Extension { + file_fetch_handler: FH, +) -> Extension +where + FP: FetchPermissions + 'static, + FH: FetchHandler + 'static, +{ Extension::builder() .js(include_js_files!( prefix "deno:ext/fetch", @@ -73,13 +82,13 @@ pub fn init( "26_fetch.js", )) .ops(vec![ - ("op_fetch", op_sync(op_fetch::

)), + ("op_fetch", op_sync(op_fetch::)), ("op_fetch_send", op_async(op_fetch_send)), ("op_fetch_request_write", op_async(op_fetch_request_write)), ("op_fetch_response_read", op_async(op_fetch_response_read)), ( "op_fetch_custom_client", - op_sync(op_fetch_custom_client::

), + op_sync(op_fetch_custom_client::), ), ]) .state(move |state| { @@ -103,6 +112,7 @@ pub fn init( .clone(), client_cert_chain_and_key: client_cert_chain_and_key.clone(), }); + state.put::(file_fetch_handler.clone()); Ok(()) }) .build() @@ -117,6 +127,45 @@ pub struct HttpClientDefaults { pub client_cert_chain_and_key: Option<(String, String)>, } +pub type CancelableResponseFuture = + Pin>>; + +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, + Option>, + ); +} + +/// 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, + Option>, + ) { + let fut = async move { + Ok(Err(type_error( + "NetworkError when attempting to fetch resource.", + ))) + }; + (Box::pin(fut), None, None) + } +} + pub trait FetchPermissions { fn check_net_url(&mut self, _url: &Url) -> Result<(), AnyError>; fn check_read(&mut self, _p: &Path) -> Result<(), AnyError>; @@ -145,13 +194,14 @@ pub struct FetchReturn { cancel_handle_rid: Option, } -pub fn op_fetch( +pub fn op_fetch( state: &mut OpState, args: FetchArgs, data: Option, ) -> Result where FP: FetchPermissions + 'static, + FH: FetchHandler + 'static, { let client = if let Some(rid) = args.client_rid { let r = state.resource_table.get::(rid)?; @@ -167,6 +217,31 @@ where // Check scheme before asking for net permission let scheme = url.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::(); + 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::(); + 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" => { let permissions = state.borrow_mut::(); permissions.check_net_url(&url)?; @@ -400,7 +475,7 @@ impl Resource for FetchCancelHandle { } } -struct FetchRequestBodyResource { +pub struct FetchRequestBodyResource { body: AsyncRefCell>>>, cancel: CancelHandle, } diff --git a/runtime/build.rs b/runtime/build.rs index b1d4fa8cbaf657..b0af848ba65c52 100644 --- a/runtime/build.rs +++ b/runtime/build.rs @@ -121,13 +121,14 @@ mod not_docs { deno_url::init(), deno_tls::init(), deno_web::init(deno_web::BlobStore::default(), Default::default()), - deno_fetch::init::( + deno_fetch::init::( "".to_owned(), None, None, None, None, None, + deno_fetch::DefaultFileFetchHandler, // No enable_file_fetch ), deno_websocket::init::("".to_owned(), None, None), deno_webstorage::init(None), diff --git a/runtime/web_worker.rs b/runtime/web_worker.rs index 31fc30fbc4524d..8d3fcbb35a89f1 100644 --- a/runtime/web_worker.rs +++ b/runtime/web_worker.rs @@ -317,13 +317,14 @@ impl WebWorker { deno_console::init(), deno_url::init(), deno_web::init(options.blob_store.clone(), Some(main_module.clone())), - deno_fetch::init::( + deno_fetch::init::( options.user_agent.clone(), options.root_cert_store.clone(), None, None, options.unsafely_ignore_certificate_errors.clone(), None, + deno_fetch::FsFetchHandler, ), deno_websocket::init::( options.user_agent.clone(), diff --git a/runtime/worker.rs b/runtime/worker.rs index af4095b7d2df2a..1588896c8d59ed 100644 --- a/runtime/worker.rs +++ b/runtime/worker.rs @@ -101,13 +101,14 @@ impl MainWorker { options.blob_store.clone(), options.bootstrap.location.clone(), ), - deno_fetch::init::( + deno_fetch::init::( options.user_agent.clone(), options.root_cert_store.clone(), None, None, options.unsafely_ignore_certificate_errors.clone(), None, + deno_fetch::FsFetchHandler, ), deno_websocket::init::( options.user_agent.clone(),