feat(cli): custom http client for fetch (#6918)

This commit is contained in:
Luca Casonato 2020-08-05 20:44:03 +02:00 committed by GitHub
parent 91ed614aa8
commit ce7808baf0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 182 additions and 117 deletions

View File

@ -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>;

View File

@ -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 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::<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)))
}

View File

@ -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);

View File

@ -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,
};

View File

@ -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();
},
);

View File

@ -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