summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuca Casonato <lucacasonato@yahoo.com>2020-08-05 20:44:03 +0200
committerGitHub <noreply@github.com>2020-08-05 20:44:03 +0200
commitce7808baf092e130ba1c5f073544072c5db958e7 (patch)
treecc8f9a167973aed549194c4ac5c8291a82c17cdf
parent91ed614aa80f5a08669be3fe5031a95e6e75f194 (diff)
feat(cli): custom http client for fetch (#6918)
-rw-r--r--cli/dts/lib.deno.unstable.d.ts41
-rw-r--r--cli/ops/fetch.rs60
-rw-r--r--cli/rt/26_fetch.js52
-rw-r--r--cli/rt/90_deno_ns.js2
-rw-r--r--cli/tests/unit/fetch_test.ts18
-rw-r--r--cli/tsc/99_main_compiler.js128
6 files changed, 183 insertions, 118 deletions
diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts
index d23536c42..a7203f778 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<Response>;
diff --git a/cli/ops/fetch.rs b/cli/ops/fetch.rs
index 869c7c5b8..539316260 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<String>,
url: String,
headers: Vec<(String, String)>,
+ client_rid: Option<u32>,
}
pub fn op_fetch(
@@ -32,8 +40,17 @@ pub fn op_fetch(
) -> Result<JsonOp, OpError> {
let args: FetchArgs = serde_json::from_value(args)?;
let url = args.url;
-
- let client = &state.borrow().http_client;
+ let resource_table_ = isolate_state.resource_table.borrow();
+ let state_ = state.borrow();
+
+ let client = if let Some(rid) = args.client_rid {
+ let r = resource_table_
+ .get::<HttpClientResource>(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<String>,
+}
+
+fn op_create_http_client(
+ isolate_state: &mut CoreIsolateState,
+ state: &State,
+ args: Value,
+ _zero_copy: &mut [ZeroCopyBuf],
+) -> Result<JsonOp, OpError> {
+ 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 2aee7c457..9e34aa8d8 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 bb556146c..ac22410f6 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 9562c48c7..012ce7b34 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 04638d60c..9481f2ba0 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