diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts index d23536c428..a7203f778c 100644 --- a/cli/dts/lib.deno.unstable.d.ts +++ b/cli/dts/lib.deno.unstable.d.ts @@ -1214,4 +1214,45 @@ declare namespace Deno { * The pid of the current process's parent. */ export const ppid: number; + + /** **UNSTABLE**: New API, yet to be vetted. + * A custom HttpClient for use with `fetch`. + * + * ```ts + * const client = new Deno.createHttpClient({ caFile: "./ca.pem" }); + * const req = await fetch("https://myserver.com", { client }); + * ``` + */ + export class HttpClient { + rid: number; + close(): void; + } + + /** **UNSTABLE**: New API, yet to be vetted. + * The options used when creating a [HttpClient]. + */ + interface CreateHttpClientOptions { + /** A certificate authority to use when validating TLS certificates. + * + * Requires `allow-read` permission. + */ + caFile?: string; + } + + /** **UNSTABLE**: New API, yet to be vetted. + * Create a custom HttpClient for to use with `fetch`. + * + * ```ts + * const client = new Deno.createHttpClient({ caFile: "./ca.pem" }); + * const req = await fetch("https://myserver.com", { client }); + * ``` + */ + export function createHttpClient( + options: CreateHttpClientOptions, + ): HttpClient; } + +declare function fetch( + input: Request | URL | string, + init?: RequestInit & { client: Deno.HttpClient }, +): Promise; diff --git a/cli/ops/fetch.rs b/cli/ops/fetch.rs index 869c7c5b88..5393162601 100644 --- a/cli/ops/fetch.rs +++ b/cli/ops/fetch.rs @@ -1,7 +1,7 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use super::dispatch_json::{Deserialize, JsonOp, Value}; use super::io::{StreamResource, StreamResourceHolder}; -use crate::http_util::HttpBody; +use crate::http_util::{create_http_client, HttpBody}; use crate::op_error::OpError; use crate::state::State; use deno_core::CoreIsolate; @@ -11,17 +11,25 @@ use futures::future::FutureExt; use http::header::HeaderName; use http::header::HeaderValue; use http::Method; +use reqwest::Client; use std::convert::From; +use std::path::PathBuf; pub fn init(i: &mut CoreIsolate, s: &State) { i.register_op("op_fetch", s.stateful_json_op2(op_fetch)); + i.register_op( + "op_create_http_client", + s.stateful_json_op2(op_create_http_client), + ); } #[derive(Deserialize)] +#[serde(rename_all = "camelCase")] struct FetchArgs { method: Option, url: String, headers: Vec<(String, String)>, + client_rid: Option, } pub fn op_fetch( @@ -32,8 +40,17 @@ pub fn op_fetch( ) -> Result { let args: FetchArgs = serde_json::from_value(args)?; let url = args.url; + let resource_table_ = isolate_state.resource_table.borrow(); + let state_ = state.borrow(); - let client = &state.borrow().http_client; + let client = if let Some(rid) = args.client_rid { + let r = resource_table_ + .get::(rid) + .ok_or_else(OpError::bad_resource_id)?; + &r.client + } else { + &state_.http_client + }; let method = match args.method { Some(method_str) => Method::from_bytes(method_str.as_bytes()) @@ -100,3 +117,40 @@ pub fn op_fetch( Ok(JsonOp::Async(future.boxed_local())) } + +struct HttpClientResource { + client: Client, +} + +impl HttpClientResource { + fn new(client: Client) -> Self { + Self { client } + } +} + +#[derive(Deserialize, Default, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(default)] +struct CreateHttpClientOptions { + ca_file: Option, +} + +fn op_create_http_client( + isolate_state: &mut CoreIsolateState, + state: &State, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: CreateHttpClientOptions = serde_json::from_value(args)?; + let mut resource_table = isolate_state.resource_table.borrow_mut(); + + if let Some(ca_file) = args.ca_file.clone() { + state.check_read(&PathBuf::from(ca_file))?; + } + + let client = create_http_client(args.ca_file).unwrap(); + + let rid = + resource_table.add("httpClient", Box::new(HttpClientResource::new(client))); + Ok(JsonOp::Sync(json!(rid))) +} diff --git a/cli/rt/26_fetch.js b/cli/rt/26_fetch.js index 2aee7c457f..9e34aa8d8f 100644 --- a/cli/rt/26_fetch.js +++ b/cli/rt/26_fetch.js @@ -6,16 +6,30 @@ const { Blob, bytesSymbol: blobBytesSymbol } = window.__bootstrap.blob; const { read } = window.__bootstrap.io; const { close } = window.__bootstrap.resources; - const { sendAsync } = window.__bootstrap.dispatchJson; + const { sendSync, sendAsync } = window.__bootstrap.dispatchJson; const Body = window.__bootstrap.body; const { ReadableStream } = window.__bootstrap.streams; const { MultipartBuilder } = window.__bootstrap.multipart; const { Headers } = window.__bootstrap.headers; - function opFetch( - args, - body, - ) { + function createHttpClient(options) { + return new HttpClient(opCreateHttpClient(options)); + } + + function opCreateHttpClient(args) { + return sendSync("op_create_http_client", args); + } + + class HttpClient { + constructor(rid) { + this.rid = rid; + } + close() { + close(this.rid); + } + } + + function opFetch(args, body) { let zeroCopy; if (body != null) { zeroCopy = new Uint8Array(body.buffer, body.byteOffset, body.byteLength); @@ -169,12 +183,7 @@ } } - function sendFetchReq( - url, - method, - headers, - body, - ) { + function sendFetchReq(url, method, headers, body, clientRid) { let headerArray = []; if (headers) { headerArray = Array.from(headers.entries()); @@ -184,19 +193,18 @@ method, url, headers: headerArray, + clientRid, }; return opFetch(args, body); } - async function fetch( - input, - init, - ) { + async function fetch(input, init) { let url; let method = null; let headers = null; let body; + let clientRid = null; let redirected = false; let remRedirectCount = 20; // TODO: use a better way to handle @@ -250,6 +258,10 @@ headers.set("content-type", contentType); } } + + if (init.client instanceof HttpClient) { + clientRid = init.client.rid; + } } } else { url = input.url; @@ -264,7 +276,13 @@ let responseBody; let responseInit = {}; while (remRedirectCount) { - const fetchResponse = await sendFetchReq(url, method, headers, body); + const fetchResponse = await sendFetchReq( + url, + method, + headers, + body, + clientRid, + ); if ( NULL_BODY_STATUS.includes(fetchResponse.status) || @@ -366,5 +384,7 @@ window.__bootstrap.fetch = { fetch, Response, + HttpClient, + createHttpClient, }; })(this); diff --git a/cli/rt/90_deno_ns.js b/cli/rt/90_deno_ns.js index bb556146c1..ac22410f62 100644 --- a/cli/rt/90_deno_ns.js +++ b/cli/rt/90_deno_ns.js @@ -126,4 +126,6 @@ __bootstrap.denoNsUnstable = { fdatasync: __bootstrap.fs.fdatasync, fsyncSync: __bootstrap.fs.fsyncSync, fsync: __bootstrap.fs.fsync, + HttpClient: __bootstrap.fetch.HttpClient, + createHttpClient: __bootstrap.fetch.createHttpClient, }; diff --git a/cli/tests/unit/fetch_test.ts b/cli/tests/unit/fetch_test.ts index 9562c48c75..012ce7b345 100644 --- a/cli/tests/unit/fetch_test.ts +++ b/cli/tests/unit/fetch_test.ts @@ -938,3 +938,21 @@ unitTest(function fetchResponseEmptyConstructor(): void { assertEquals(response.bodyUsed, false); assertEquals([...response.headers], []); }); + +unitTest( + { perms: { net: true, read: true } }, + async function fetchCustomHttpClientSuccess(): Promise< + void + > { + const client = Deno.createHttpClient( + { caFile: "./cli/tests/tls/RootCA.crt" }, + ); + const response = await fetch( + "https://localhost:5545/cli/tests/fixture.json", + { client }, + ); + const json = await response.json(); + assertEquals(json.name, "deno"); + client.close(); + }, +); diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index 04638d60c3..9481f2ba0b 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -100,6 +100,8 @@ delete Object.prototype.__proto__; "PermissionStatus", "hostname", "ppid", + "HttpClient", + "createHttpClient", ]; function transformMessageText(messageText, code) { @@ -139,9 +141,7 @@ delete Object.prototype.__proto__; return messageText; } - function fromDiagnosticCategory( - category, - ) { + function fromDiagnosticCategory(category) { switch (category) { case ts.DiagnosticCategory.Error: return DiagnosticCategory.Error; @@ -160,11 +160,7 @@ delete Object.prototype.__proto__; } } - function getSourceInformation( - sourceFile, - start, - length, - ) { + function getSourceInformation(sourceFile, start, length) { const scriptResourceName = sourceFile.fileName; const { line: lineNumber, @@ -196,9 +192,7 @@ delete Object.prototype.__proto__; }; } - function fromDiagnosticMessageChain( - messageChain, - ) { + function fromDiagnosticMessageChain(messageChain) { if (!messageChain) { return undefined; } @@ -214,9 +208,7 @@ delete Object.prototype.__proto__; }); } - function parseDiagnostic( - item, - ) { + function parseDiagnostic(item) { const { messageText, category: sourceCategory, @@ -254,9 +246,7 @@ delete Object.prototype.__proto__; return sourceInfo ? { ...base, ...sourceInfo } : base; } - function parseRelatedInformation( - relatedInformation, - ) { + function parseRelatedInformation(relatedInformation) { const result = []; for (const item of relatedInformation) { result.push(parseDiagnostic(item)); @@ -264,9 +254,7 @@ delete Object.prototype.__proto__; return result; } - function fromTypeScriptDiagnostic( - diagnostics, - ) { + function fromTypeScriptDiagnostic(diagnostics) { const items = []; for (const sourceDiagnostic of diagnostics) { const item = parseDiagnostic(sourceDiagnostic); @@ -489,12 +477,7 @@ delete Object.prototype.__proto__; */ const RESOLVED_SPECIFIER_CACHE = new Map(); - function configure( - defaultOptions, - source, - path, - cwd, - ) { + function configure(defaultOptions, source, path, cwd) { const { config, error } = ts.parseConfigFileTextToJson(path, source); if (error) { return { diagnostics: [error], options: defaultOptions }; @@ -540,11 +523,7 @@ delete Object.prototype.__proto__; return SOURCE_FILE_CACHE.get(url); } - static cacheResolvedUrl( - resolvedUrl, - rawModuleSpecifier, - containingFile, - ) { + static cacheResolvedUrl(resolvedUrl, rawModuleSpecifier, containingFile) { containingFile = containingFile || ""; let innerCache = RESOLVED_SPECIFIER_CACHE.get(containingFile); if (!innerCache) { @@ -554,10 +533,7 @@ delete Object.prototype.__proto__; innerCache.set(rawModuleSpecifier, resolvedUrl); } - static getResolvedUrl( - moduleSpecifier, - containingFile, - ) { + static getResolvedUrl(moduleSpecifier, containingFile) { const containingCache = RESOLVED_SPECIFIER_CACHE.get(containingFile); if (containingCache) { return containingCache.get(moduleSpecifier); @@ -621,11 +597,7 @@ delete Object.prototype.__proto__; return this.#options; } - configure( - cwd, - path, - configurationText, - ) { + configure(cwd, path, configurationText) { log("compiler::host.configure", path); const { options, ...result } = configure( this.#options, @@ -718,10 +690,7 @@ delete Object.prototype.__proto__; return notImplemented(); } - resolveModuleNames( - moduleNames, - containingFile, - ) { + resolveModuleNames(moduleNames, containingFile) { log("compiler::host.resolveModuleNames", { moduleNames, containingFile, @@ -760,13 +729,7 @@ delete Object.prototype.__proto__; return true; } - writeFile( - fileName, - data, - _writeByteOrderMark, - _onError, - sourceFiles, - ) { + writeFile(fileName, data, _writeByteOrderMark, _onError, sourceFiles) { log("compiler::host.writeFile", fileName); this.#writeFile(fileName, data, sourceFiles); } @@ -848,9 +811,7 @@ delete Object.prototype.__proto__; const SYSTEM_LOADER = getAsset("system_loader.js"); const SYSTEM_LOADER_ES5 = getAsset("system_loader_es5.js"); - function buildLocalSourceFileCache( - sourceFileMap, - ) { + function buildLocalSourceFileCache(sourceFileMap) { for (const entry of Object.values(sourceFileMap)) { assert(entry.sourceCode.length > 0); SourceFile.addToCache({ @@ -902,9 +863,7 @@ delete Object.prototype.__proto__; } } - function buildSourceFileCache( - sourceFileMap, - ) { + function buildSourceFileCache(sourceFileMap) { for (const entry of Object.values(sourceFileMap)) { SourceFile.addToCache({ url: entry.url, @@ -974,11 +933,7 @@ delete Object.prototype.__proto__; }; function createBundleWriteFile(state) { - return function writeFile( - _fileName, - data, - sourceFiles, - ) { + return function writeFile(_fileName, data, sourceFiles) { assert(sourceFiles != null); assert(state.host); // we only support single root names for bundles @@ -992,14 +947,8 @@ delete Object.prototype.__proto__; }; } - function createCompileWriteFile( - state, - ) { - return function writeFile( - fileName, - data, - sourceFiles, - ) { + function createCompileWriteFile(state) { + return function writeFile(fileName, data, sourceFiles) { const isBuildInfo = fileName === TS_BUILD_INFO; if (isBuildInfo) { @@ -1017,14 +966,8 @@ delete Object.prototype.__proto__; }; } - function createRuntimeCompileWriteFile( - state, - ) { - return function writeFile( - fileName, - data, - sourceFiles, - ) { + function createRuntimeCompileWriteFile(state) { + return function writeFile(fileName, data, sourceFiles) { assert(sourceFiles); assert(sourceFiles.length === 1); state.emitMap[fileName] = { @@ -1169,10 +1112,7 @@ delete Object.prototype.__proto__; ts.performance.enable(); } - function performanceProgram({ - program, - fileCount, - }) { + function performanceProgram({ program, fileCount }) { if (program) { if ("getProgram" in program) { program = program.getProgram(); @@ -1211,15 +1151,14 @@ delete Object.prototype.__proto__; } // TODO(Bartlomieju): this check should be done in Rust; there should be no - function processConfigureResponse( - configResult, - configPath, - ) { + function processConfigureResponse(configResult, configPath) { const { ignoredOptions, diagnostics } = configResult; if (ignoredOptions) { const msg = `Unsupported compiler options in "${configPath}"\n The following options were ignored:\n ${ - ignoredOptions.map((value) => value).join(", ") + ignoredOptions + .map((value) => value) + .join(", ") }\n`; core.print(msg, true); } @@ -1319,12 +1258,7 @@ delete Object.prototype.__proto__; } } - function buildBundle( - rootName, - data, - sourceFiles, - target, - ) { + function buildBundle(rootName, data, sourceFiles, target) { // when outputting to AMD and a single outfile, TypeScript makes up the module // specifiers which are used to define the modules, and doesn't expose them // publicly, so we have to try to replicate @@ -1664,9 +1598,7 @@ delete Object.prototype.__proto__; return result; } - function runtimeCompile( - request, - ) { + function runtimeCompile(request) { const { options, rootNames, target, unstable, sourceFileMap } = request; log(">>> runtime compile start", { @@ -1808,9 +1740,7 @@ delete Object.prototype.__proto__; }; } - function runtimeTranspile( - request, - ) { + function runtimeTranspile(request) { const result = {}; const { sources, options } = request; const compilerOptions = options