diff --git a/ext/node/lib.rs b/ext/node/lib.rs index a17e5f0ab5..b1a893086d 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -326,6 +326,7 @@ deno_core::extension!(deno_node, ops::zlib::brotli::op_brotli_decompress_stream_end, ops::http::op_node_http_fetch_response_upgrade, ops::http::op_node_http_request_with_conn

, + ops::http::op_node_http_request_with_tls_conn

, ops::http::op_node_http_await_response, ops::http::op_node_http_wait_for_connection, ops::http2::op_http2_connect, diff --git a/ext/node/ops/http.rs b/ext/node/ops/http.rs index 6380ecfe03..0fd088d235 100644 --- a/ext/node/ops/http.rs +++ b/ext/node/ops/http.rs @@ -35,6 +35,7 @@ use deno_core::Resource; use deno_core::ResourceId; use deno_fetch::ResBody; use deno_net::io::TcpStreamResource; +use deno_net::ops_tls::TlsStreamResource; use http::header::HeaderMap; use http::header::HeaderName; use http::header::HeaderValue; @@ -199,6 +200,120 @@ where Ok((rid, conn_rid)) } +// TODO(@satyarohith): deduplicate the code. +#[op2(async)] +#[serde] +pub async fn op_node_http_request_with_tls_conn

( + state: Rc>, + #[serde] method: ByteString, + #[string] url: String, + #[serde] headers: Vec<(ByteString, ByteString)>, + #[smi] body: Option, + #[smi] conn_rid: ResourceId, +) -> Result<(ResourceId, ResourceId), AnyError> +where + P: crate::NodePermissions + 'static, +{ + // Establish the connection/client. + let resource_rc = state + .borrow_mut() + .resource_table + .take::(conn_rid)?; + let resource = Rc::try_unwrap(resource_rc) + .map_err(|_e| bad_resource("TLS stream is currently in use"))?; + let (read_half, write_half) = resource.into_inner(); + let tls_stream = read_half.unsplit(write_half); + let io = TokioIo::new(tls_stream); + let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await?; + + let (notify, receiver) = tokio::sync::oneshot::channel::<()>(); + + // Spawn a task to poll the connection, driving the HTTP state + let _handle = tokio::task::spawn(async move { + let _ = notify.send(()); + conn.await?; + Ok::<_, AnyError>(()) + }); + + // Create the request. + let method = Method::from_bytes(&method)?; + let mut url_parsed = Url::parse(&url)?; + let maybe_authority = deno_fetch::extract_authority(&mut url_parsed); + + { + let mut state_ = state.borrow_mut(); + let permissions = state_.borrow_mut::

(); + permissions.check_net_url(&url_parsed, "ClientRequest")?; + } + + let mut header_map = HeaderMap::new(); + for (key, value) in headers { + let name = HeaderName::from_bytes(&key) + .map_err(|err| type_error(err.to_string()))?; + let v = HeaderValue::from_bytes(&value) + .map_err(|err| type_error(err.to_string()))?; + + header_map.append(name, v); + } + + let (body, con_len) = if let Some(body) = body { + ( + BodyExt::boxed(NodeHttpResourceToBodyAdapter::new( + state.borrow_mut().resource_table.take_any(body)?, + )), + None, + ) + } else { + // POST and PUT requests should always have a 0 length content-length, + // if there is no body. https://fetch.spec.whatwg.org/#http-network-or-cache-fetch + let len = if matches!(method, Method::POST | Method::PUT) { + Some(0) + } else { + None + }; + ( + http_body_util::Empty::new() + .map_err(|never| match never {}) + .boxed(), + len, + ) + }; + + let mut request = http::Request::new(body); + *request.method_mut() = method.clone(); + *request.uri_mut() = url_parsed + .path() + .to_string() + .parse() + .map_err(|_| type_error("Invalid URL"))?; + *request.headers_mut() = header_map; + + if let Some((username, password)) = maybe_authority { + request.headers_mut().insert( + AUTHORIZATION, + deno_fetch::basic_auth(&username, password.as_deref()), + ); + } + if let Some(len) = con_len { + request.headers_mut().insert(CONTENT_LENGTH, len.into()); + } + + let res = sender.send_request(request).map_err(Error::from).boxed(); + let rid = state + .borrow_mut() + .resource_table + .add(NodeHttpClientResponse { + response: res, + url: url.clone(), + }); + let conn_rid = state + .borrow_mut() + .resource_table + .add(NodeHttpConnReady { recv: receiver }); + + Ok((rid, conn_rid)) +} + #[op2(async)] #[serde] pub async fn op_node_http_wait_for_connection( diff --git a/ext/node/polyfills/http.ts b/ext/node/polyfills/http.ts index d26192a47d..0e4459f637 100644 --- a/ext/node/polyfills/http.ts +++ b/ext/node/polyfills/http.ts @@ -8,7 +8,9 @@ import { op_node_http_await_response, op_node_http_fetch_response_upgrade, op_node_http_request_with_conn, + op_node_http_request_with_tls_conn, op_node_http_wait_for_connection, + op_tls_start, } from "ext:core/ops"; import { TextEncoder } from "ext:deno_web/08_text_encoding.js"; @@ -447,12 +449,28 @@ class ClientRequest extends OutgoingMessage { (async () => { try { - const [rid, connRid] = await op_node_http_request_with_conn( + const parsedUrl = new URL(url); + const encrypted = parsedUrl.protocol === "https:"; + let encryptedRid; + if (encrypted) { + const { 0: rid, 1: localAddr, 2: remoteAddr } = op_tls_start({ + rid: this.socket.rid, + hostname: parsedUrl.hostname, + caCerts: [], + alpnProtocols: ["http/1.0", "http/1.1"], + }); + encryptedRid = rid; + } + const op = encrypted + ? op_node_http_request_with_tls_conn + : op_node_http_request_with_conn; + const connectionRid = encrypted ? encryptedRid : this.socket.rid; + const [rid, connRid] = await op( this.method, url, headers, this._bodyWriteRid, - this.socket.rid, + connectionRid, ); // Emit request ready to let the request body to be written. await op_node_http_wait_for_connection(connRid); diff --git a/tests/unit_node/http_test.ts b/tests/unit_node/http_test.ts index fabd53748d..ee52d431c0 100644 --- a/tests/unit_node/http_test.ts +++ b/tests/unit_node/http_test.ts @@ -687,9 +687,7 @@ Deno.test("[node/http] ClientRequest handle non-string headers", async () => { assertEquals(headers!["1"], "2"); }); -Deno.test("[node/http] ClientRequest uses HTTP/1.1", { - ignore: true, -}, async () => { +Deno.test("[node/https] ClientRequest uses HTTP/1.1", async () => { let body = ""; const { promise, resolve, reject } = Promise.withResolvers(); const req = https.request("https://localhost:5545/http_version", {