1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-03 04:48:52 -05:00

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. * The pid of the current process's parent.
*/ */
export const ppid: number; 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. // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use super::dispatch_json::{Deserialize, JsonOp, Value}; use super::dispatch_json::{Deserialize, JsonOp, Value};
use super::io::{StreamResource, StreamResourceHolder}; 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::op_error::OpError;
use crate::state::State; use crate::state::State;
use deno_core::CoreIsolate; use deno_core::CoreIsolate;
@ -11,17 +11,25 @@ use futures::future::FutureExt;
use http::header::HeaderName; use http::header::HeaderName;
use http::header::HeaderValue; use http::header::HeaderValue;
use http::Method; use http::Method;
use reqwest::Client;
use std::convert::From; use std::convert::From;
use std::path::PathBuf;
pub fn init(i: &mut CoreIsolate, s: &State) { pub fn init(i: &mut CoreIsolate, s: &State) {
i.register_op("op_fetch", s.stateful_json_op2(op_fetch)); 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)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct FetchArgs { struct FetchArgs {
method: Option<String>, method: Option<String>,
url: String, url: String,
headers: Vec<(String, String)>, headers: Vec<(String, String)>,
client_rid: Option<u32>,
} }
pub fn op_fetch( pub fn op_fetch(
@ -32,8 +40,17 @@ pub fn op_fetch(
) -> Result<JsonOp, OpError> { ) -> Result<JsonOp, OpError> {
let args: FetchArgs = serde_json::from_value(args)?; let args: FetchArgs = serde_json::from_value(args)?;
let url = args.url; 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 { let method = match args.method {
Some(method_str) => Method::from_bytes(method_str.as_bytes()) Some(method_str) => Method::from_bytes(method_str.as_bytes())
@ -100,3 +117,40 @@ pub fn op_fetch(
Ok(JsonOp::Async(future.boxed_local())) 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 { Blob, bytesSymbol: blobBytesSymbol } = window.__bootstrap.blob;
const { read } = window.__bootstrap.io; const { read } = window.__bootstrap.io;
const { close } = window.__bootstrap.resources; const { close } = window.__bootstrap.resources;
const { sendAsync } = window.__bootstrap.dispatchJson; const { sendSync, sendAsync } = window.__bootstrap.dispatchJson;
const Body = window.__bootstrap.body; const Body = window.__bootstrap.body;
const { ReadableStream } = window.__bootstrap.streams; const { ReadableStream } = window.__bootstrap.streams;
const { MultipartBuilder } = window.__bootstrap.multipart; const { MultipartBuilder } = window.__bootstrap.multipart;
const { Headers } = window.__bootstrap.headers; const { Headers } = window.__bootstrap.headers;
function opFetch( function createHttpClient(options) {
args, return new HttpClient(opCreateHttpClient(options));
body, }
) {
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; let zeroCopy;
if (body != null) { if (body != null) {
zeroCopy = new Uint8Array(body.buffer, body.byteOffset, body.byteLength); zeroCopy = new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
@ -169,12 +183,7 @@
} }
} }
function sendFetchReq( function sendFetchReq(url, method, headers, body, clientRid) {
url,
method,
headers,
body,
) {
let headerArray = []; let headerArray = [];
if (headers) { if (headers) {
headerArray = Array.from(headers.entries()); headerArray = Array.from(headers.entries());
@ -184,19 +193,18 @@
method, method,
url, url,
headers: headerArray, headers: headerArray,
clientRid,
}; };
return opFetch(args, body); return opFetch(args, body);
} }
async function fetch( async function fetch(input, init) {
input,
init,
) {
let url; let url;
let method = null; let method = null;
let headers = null; let headers = null;
let body; let body;
let clientRid = null;
let redirected = false; let redirected = false;
let remRedirectCount = 20; // TODO: use a better way to handle let remRedirectCount = 20; // TODO: use a better way to handle
@ -250,6 +258,10 @@
headers.set("content-type", contentType); headers.set("content-type", contentType);
} }
} }
if (init.client instanceof HttpClient) {
clientRid = init.client.rid;
}
} }
} else { } else {
url = input.url; url = input.url;
@ -264,7 +276,13 @@
let responseBody; let responseBody;
let responseInit = {}; let responseInit = {};
while (remRedirectCount) { while (remRedirectCount) {
const fetchResponse = await sendFetchReq(url, method, headers, body); const fetchResponse = await sendFetchReq(
url,
method,
headers,
body,
clientRid,
);
if ( if (
NULL_BODY_STATUS.includes(fetchResponse.status) || NULL_BODY_STATUS.includes(fetchResponse.status) ||
@ -366,5 +384,7 @@
window.__bootstrap.fetch = { window.__bootstrap.fetch = {
fetch, fetch,
Response, Response,
HttpClient,
createHttpClient,
}; };
})(this); })(this);

View file

@ -126,4 +126,6 @@ __bootstrap.denoNsUnstable = {
fdatasync: __bootstrap.fs.fdatasync, fdatasync: __bootstrap.fs.fdatasync,
fsyncSync: __bootstrap.fs.fsyncSync, fsyncSync: __bootstrap.fs.fsyncSync,
fsync: __bootstrap.fs.fsync, 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.bodyUsed, false);
assertEquals([...response.headers], []); 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", "PermissionStatus",
"hostname", "hostname",
"ppid", "ppid",
"HttpClient",
"createHttpClient",
]; ];
function transformMessageText(messageText, code) { function transformMessageText(messageText, code) {
@ -139,9 +141,7 @@ delete Object.prototype.__proto__;
return messageText; return messageText;
} }
function fromDiagnosticCategory( function fromDiagnosticCategory(category) {
category,
) {
switch (category) { switch (category) {
case ts.DiagnosticCategory.Error: case ts.DiagnosticCategory.Error:
return DiagnosticCategory.Error; return DiagnosticCategory.Error;
@ -160,11 +160,7 @@ delete Object.prototype.__proto__;
} }
} }
function getSourceInformation( function getSourceInformation(sourceFile, start, length) {
sourceFile,
start,
length,
) {
const scriptResourceName = sourceFile.fileName; const scriptResourceName = sourceFile.fileName;
const { const {
line: lineNumber, line: lineNumber,
@ -196,9 +192,7 @@ delete Object.prototype.__proto__;
}; };
} }
function fromDiagnosticMessageChain( function fromDiagnosticMessageChain(messageChain) {
messageChain,
) {
if (!messageChain) { if (!messageChain) {
return undefined; return undefined;
} }
@ -214,9 +208,7 @@ delete Object.prototype.__proto__;
}); });
} }
function parseDiagnostic( function parseDiagnostic(item) {
item,
) {
const { const {
messageText, messageText,
category: sourceCategory, category: sourceCategory,
@ -254,9 +246,7 @@ delete Object.prototype.__proto__;
return sourceInfo ? { ...base, ...sourceInfo } : base; return sourceInfo ? { ...base, ...sourceInfo } : base;
} }
function parseRelatedInformation( function parseRelatedInformation(relatedInformation) {
relatedInformation,
) {
const result = []; const result = [];
for (const item of relatedInformation) { for (const item of relatedInformation) {
result.push(parseDiagnostic(item)); result.push(parseDiagnostic(item));
@ -264,9 +254,7 @@ delete Object.prototype.__proto__;
return result; return result;
} }
function fromTypeScriptDiagnostic( function fromTypeScriptDiagnostic(diagnostics) {
diagnostics,
) {
const items = []; const items = [];
for (const sourceDiagnostic of diagnostics) { for (const sourceDiagnostic of diagnostics) {
const item = parseDiagnostic(sourceDiagnostic); const item = parseDiagnostic(sourceDiagnostic);
@ -489,12 +477,7 @@ delete Object.prototype.__proto__;
*/ */
const RESOLVED_SPECIFIER_CACHE = new Map(); const RESOLVED_SPECIFIER_CACHE = new Map();
function configure( function configure(defaultOptions, source, path, cwd) {
defaultOptions,
source,
path,
cwd,
) {
const { config, error } = ts.parseConfigFileTextToJson(path, source); const { config, error } = ts.parseConfigFileTextToJson(path, source);
if (error) { if (error) {
return { diagnostics: [error], options: defaultOptions }; return { diagnostics: [error], options: defaultOptions };
@ -540,11 +523,7 @@ delete Object.prototype.__proto__;
return SOURCE_FILE_CACHE.get(url); return SOURCE_FILE_CACHE.get(url);
} }
static cacheResolvedUrl( static cacheResolvedUrl(resolvedUrl, rawModuleSpecifier, containingFile) {
resolvedUrl,
rawModuleSpecifier,
containingFile,
) {
containingFile = containingFile || ""; containingFile = containingFile || "";
let innerCache = RESOLVED_SPECIFIER_CACHE.get(containingFile); let innerCache = RESOLVED_SPECIFIER_CACHE.get(containingFile);
if (!innerCache) { if (!innerCache) {
@ -554,10 +533,7 @@ delete Object.prototype.__proto__;
innerCache.set(rawModuleSpecifier, resolvedUrl); innerCache.set(rawModuleSpecifier, resolvedUrl);
} }
static getResolvedUrl( static getResolvedUrl(moduleSpecifier, containingFile) {
moduleSpecifier,
containingFile,
) {
const containingCache = RESOLVED_SPECIFIER_CACHE.get(containingFile); const containingCache = RESOLVED_SPECIFIER_CACHE.get(containingFile);
if (containingCache) { if (containingCache) {
return containingCache.get(moduleSpecifier); return containingCache.get(moduleSpecifier);
@ -621,11 +597,7 @@ delete Object.prototype.__proto__;
return this.#options; return this.#options;
} }
configure( configure(cwd, path, configurationText) {
cwd,
path,
configurationText,
) {
log("compiler::host.configure", path); log("compiler::host.configure", path);
const { options, ...result } = configure( const { options, ...result } = configure(
this.#options, this.#options,
@ -718,10 +690,7 @@ delete Object.prototype.__proto__;
return notImplemented(); return notImplemented();
} }
resolveModuleNames( resolveModuleNames(moduleNames, containingFile) {
moduleNames,
containingFile,
) {
log("compiler::host.resolveModuleNames", { log("compiler::host.resolveModuleNames", {
moduleNames, moduleNames,
containingFile, containingFile,
@ -760,13 +729,7 @@ delete Object.prototype.__proto__;
return true; return true;
} }
writeFile( writeFile(fileName, data, _writeByteOrderMark, _onError, sourceFiles) {
fileName,
data,
_writeByteOrderMark,
_onError,
sourceFiles,
) {
log("compiler::host.writeFile", fileName); log("compiler::host.writeFile", fileName);
this.#writeFile(fileName, data, sourceFiles); this.#writeFile(fileName, data, sourceFiles);
} }
@ -848,9 +811,7 @@ delete Object.prototype.__proto__;
const SYSTEM_LOADER = getAsset("system_loader.js"); const SYSTEM_LOADER = getAsset("system_loader.js");
const SYSTEM_LOADER_ES5 = getAsset("system_loader_es5.js"); const SYSTEM_LOADER_ES5 = getAsset("system_loader_es5.js");
function buildLocalSourceFileCache( function buildLocalSourceFileCache(sourceFileMap) {
sourceFileMap,
) {
for (const entry of Object.values(sourceFileMap)) { for (const entry of Object.values(sourceFileMap)) {
assert(entry.sourceCode.length > 0); assert(entry.sourceCode.length > 0);
SourceFile.addToCache({ SourceFile.addToCache({
@ -902,9 +863,7 @@ delete Object.prototype.__proto__;
} }
} }
function buildSourceFileCache( function buildSourceFileCache(sourceFileMap) {
sourceFileMap,
) {
for (const entry of Object.values(sourceFileMap)) { for (const entry of Object.values(sourceFileMap)) {
SourceFile.addToCache({ SourceFile.addToCache({
url: entry.url, url: entry.url,
@ -974,11 +933,7 @@ delete Object.prototype.__proto__;
}; };
function createBundleWriteFile(state) { function createBundleWriteFile(state) {
return function writeFile( return function writeFile(_fileName, data, sourceFiles) {
_fileName,
data,
sourceFiles,
) {
assert(sourceFiles != null); assert(sourceFiles != null);
assert(state.host); assert(state.host);
// we only support single root names for bundles // we only support single root names for bundles
@ -992,14 +947,8 @@ delete Object.prototype.__proto__;
}; };
} }
function createCompileWriteFile( function createCompileWriteFile(state) {
state, return function writeFile(fileName, data, sourceFiles) {
) {
return function writeFile(
fileName,
data,
sourceFiles,
) {
const isBuildInfo = fileName === TS_BUILD_INFO; const isBuildInfo = fileName === TS_BUILD_INFO;
if (isBuildInfo) { if (isBuildInfo) {
@ -1017,14 +966,8 @@ delete Object.prototype.__proto__;
}; };
} }
function createRuntimeCompileWriteFile( function createRuntimeCompileWriteFile(state) {
state, return function writeFile(fileName, data, sourceFiles) {
) {
return function writeFile(
fileName,
data,
sourceFiles,
) {
assert(sourceFiles); assert(sourceFiles);
assert(sourceFiles.length === 1); assert(sourceFiles.length === 1);
state.emitMap[fileName] = { state.emitMap[fileName] = {
@ -1169,10 +1112,7 @@ delete Object.prototype.__proto__;
ts.performance.enable(); ts.performance.enable();
} }
function performanceProgram({ function performanceProgram({ program, fileCount }) {
program,
fileCount,
}) {
if (program) { if (program) {
if ("getProgram" in program) { if ("getProgram" in program) {
program = program.getProgram(); program = program.getProgram();
@ -1211,15 +1151,14 @@ delete Object.prototype.__proto__;
} }
// TODO(Bartlomieju): this check should be done in Rust; there should be no // TODO(Bartlomieju): this check should be done in Rust; there should be no
function processConfigureResponse( function processConfigureResponse(configResult, configPath) {
configResult,
configPath,
) {
const { ignoredOptions, diagnostics } = configResult; const { ignoredOptions, diagnostics } = configResult;
if (ignoredOptions) { if (ignoredOptions) {
const msg = const msg =
`Unsupported compiler options in "${configPath}"\n The following options were ignored:\n ${ `Unsupported compiler options in "${configPath}"\n The following options were ignored:\n ${
ignoredOptions.map((value) => value).join(", ") ignoredOptions
.map((value) => value)
.join(", ")
}\n`; }\n`;
core.print(msg, true); core.print(msg, true);
} }
@ -1319,12 +1258,7 @@ delete Object.prototype.__proto__;
} }
} }
function buildBundle( function buildBundle(rootName, data, sourceFiles, target) {
rootName,
data,
sourceFiles,
target,
) {
// when outputting to AMD and a single outfile, TypeScript makes up the module // 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 // specifiers which are used to define the modules, and doesn't expose them
// publicly, so we have to try to replicate // publicly, so we have to try to replicate
@ -1664,9 +1598,7 @@ delete Object.prototype.__proto__;
return result; return result;
} }
function runtimeCompile( function runtimeCompile(request) {
request,
) {
const { options, rootNames, target, unstable, sourceFileMap } = request; const { options, rootNames, target, unstable, sourceFileMap } = request;
log(">>> runtime compile start", { log(">>> runtime compile start", {
@ -1808,9 +1740,7 @@ delete Object.prototype.__proto__;
}; };
} }
function runtimeTranspile( function runtimeTranspile(request) {
request,
) {
const result = {}; const result = {};
const { sources, options } = request; const { sources, options } = request;
const compilerOptions = options const compilerOptions = options