mirror of
https://github.com/denoland/deno.git
synced 2024-12-21 23:04:45 -05:00
feat(cli): custom http client for fetch (#6918)
This commit is contained in:
parent
91ed614aa8
commit
ce7808baf0
6 changed files with 182 additions and 117 deletions
41
cli/dts/lib.deno.unstable.d.ts
vendored
41
cli/dts/lib.deno.unstable.d.ts
vendored
|
@ -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>;
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue