diff --git a/.gitmodules b/.gitmodules index d0f0bc0fe96157..4fa24ad0b68a91 100644 --- a/.gitmodules +++ b/.gitmodules @@ -9,6 +9,8 @@ [submodule "tests/node_compat/runner/suite"] path = tests/node_compat/runner/suite url = https://github.com/denoland/node_test.git + shallow = true [submodule "cli/bench/testdata/lsp_benchdata"] path = cli/bench/testdata/lsp_benchdata url = https://github.com/denoland/deno_lsp_benchdata.git + shallow = true diff --git a/cli/tools/registry/pm.rs b/cli/tools/registry/pm.rs index a69a474e7f7347..699b476cb9b68f 100644 --- a/cli/tools/registry/pm.rs +++ b/cli/tools/registry/pm.rs @@ -109,8 +109,14 @@ pub async fn add(flags: Flags, add_flags: AddFlags) -> Result<(), AnyError> { } } - let config_file_contents = - tokio::fs::read_to_string(&config_file_path).await.unwrap(); + let config_file_contents = { + let contents = tokio::fs::read_to_string(&config_file_path).await.unwrap(); + if contents.trim().is_empty() { + "{}\n".into() + } else { + contents + } + }; let ast = jsonc_parser::parse_to_ast( &config_file_contents, &Default::default(), diff --git a/cli/tools/vendor/mod.rs b/cli/tools/vendor/mod.rs index 2abdf6e99cd7a1..cf10b77c75a8c0 100644 --- a/cli/tools/vendor/mod.rs +++ b/cli/tools/vendor/mod.rs @@ -310,6 +310,7 @@ fn update_config_text( ) -> Result { use jsonc_parser::ast::ObjectProp; use jsonc_parser::ast::Value; + let text = if text.trim().is_empty() { "{}\n" } else { text }; let ast = jsonc_parser::parse_to_ast(text, &Default::default(), &Default::default())?; let obj = match ast.value { diff --git a/cli/tsc/dts/lib.deno.ns.d.ts b/cli/tsc/dts/lib.deno.ns.d.ts index 1d6b398f58a146..c63b4a2612e3ac 100644 --- a/cli/tsc/dts/lib.deno.ns.d.ts +++ b/cli/tsc/dts/lib.deno.ns.d.ts @@ -6277,11 +6277,23 @@ declare namespace Deno { * @category HTTP Server */ export interface ServeTlsOptions extends ServeOptions { - /** Server private key in PEM format */ - cert: string; + /** + * Server private key in PEM format. Use {@linkcode TlsCertifiedKeyOptions} instead. + * + * @deprecated This will be removed in Deno 2.0. See the + * {@link https://docs.deno.com/runtime/manual/advanced/migrate_deprecations | Deno 1.x to 2.x Migration Guide} + * for migration instructions. + */ + cert?: string; - /** Cert chain in PEM format */ - key: string; + /** + * Cert chain in PEM format. Use {@linkcode TlsCertifiedKeyOptions} instead. + * + * @deprecated This will be removed in Deno 2.0. See the + * {@link https://docs.deno.com/runtime/manual/advanced/migrate_deprecations | Deno 1.x to 2.x Migration Guide} + * for migration instructions. + */ + key?: string; } /** @@ -6490,7 +6502,10 @@ declare namespace Deno { * @category HTTP Server */ export function serve( - options: ServeOptions | ServeTlsOptions, + options: + | ServeOptions + | ServeTlsOptions + | (ServeTlsOptions & TlsCertifiedKeyOptions), handler: ServeHandler, ): HttpServer; /** Serves HTTP requests with the given option bag. @@ -6546,6 +6561,12 @@ declare namespace Deno { * @category HTTP Server */ export function serve( - options: ServeInit & (ServeOptions | ServeTlsOptions), + options: + & ServeInit + & ( + | ServeOptions + | ServeTlsOptions + | (ServeTlsOptions & TlsCertifiedKeyOptions) + ), ): HttpServer; } diff --git a/cli/tsc/dts/lib.deno.unstable.d.ts b/cli/tsc/dts/lib.deno.unstable.d.ts index 056d8e6099ddb7..ae3f60d2833b6f 100644 --- a/cli/tsc/dts/lib.deno.unstable.d.ts +++ b/cli/tsc/dts/lib.deno.unstable.d.ts @@ -882,10 +882,6 @@ declare namespace Deno { caCerts?: string[]; /** A HTTP proxy to use for new connections. */ proxy?: Proxy; - /** Cert chain in PEM format. */ - cert?: string; - /** Server private key in PEM format. */ - key?: string; /** Sets the maximum numer of idle connections per host allowed in the pool. */ poolMaxIdlePerHost?: number; /** Set an optional timeout for idle sockets being kept-alive. @@ -962,6 +958,27 @@ declare namespace Deno { options: CreateHttpClientOptions, ): HttpClient; + /** **UNSTABLE**: New API, yet to be vetted. + * + * Create a custom HttpClient to use with {@linkcode fetch}. This is an + * extension of the web platform Fetch API which allows Deno to use custom + * TLS certificates and connect via a proxy while using `fetch()`. + * + * @example ```ts + * const caCert = await Deno.readTextFile("./ca.pem"); + * // Load a client key and certificate that we'll use to connect + * const key = await Deno.readTextFile("./key.key"); + * const cert = await Deno.readTextFile("./cert.crt"); + * const client = Deno.createHttpClient({ caCerts: [ caCert ], key, cert }); + * const response = await fetch("https://myserver.com", { client }); + * ``` + * + * @category Fetch API + */ + export function createHttpClient( + options: CreateHttpClientOptions & TlsCertifiedKeyOptions, + ): HttpClient; + /** **UNSTABLE**: New API, yet to be vetted. * * Represents membership of a IPv4 multicast group. diff --git a/ext/fetch/22_http_client.js b/ext/fetch/22_http_client.js index e1389bbe1f3b39..061a3dda8938de 100644 --- a/ext/fetch/22_http_client.js +++ b/ext/fetch/22_http_client.js @@ -25,12 +25,7 @@ const { ObjectDefineProperty } = primordials; */ function createHttpClient(options) { options.caCerts ??= []; - const keyPair = loadTlsKeyPair( - options.cert, - undefined, - options.key, - undefined, - ); + const keyPair = loadTlsKeyPair("Deno.createHttpClient", options); return new HttpClient( op_fetch_custom_client( options, diff --git a/ext/http/01_http.js b/ext/http/01_http.js index a312cf60ed7fc0..b41c36446ab94b 100644 --- a/ext/http/01_http.js +++ b/ext/http/01_http.js @@ -4,11 +4,13 @@ import { core, internals, primordials } from "ext:core/mod.js"; const { BadResourcePrototype, InterruptedPrototype, + internalRidSymbol, } = core; import { op_http_accept, op_http_headers, op_http_shutdown, + op_http_start, op_http_upgrade_websocket, op_http_websocket_accept_header, op_http_write, @@ -71,7 +73,6 @@ import { readableStreamForRid, ReadableStreamPrototype, } from "ext:deno_web/06_streams.js"; -import { serve } from "ext:deno_http/00_serve.js"; import { SymbolDispose } from "ext:deno_web/00_infra.js"; const connErrorSymbol = Symbol("connError"); @@ -557,4 +558,14 @@ function buildCaseInsensitiveCommaValueFinder(checkText) { internals.buildCaseInsensitiveCommaValueFinder = buildCaseInsensitiveCommaValueFinder; -export { _ws, HttpConn, serve, upgradeWebSocket }; +function serveHttp(conn) { + internals.warnOnDeprecatedApi( + "Deno.serveHttp()", + new Error().stack, + "Use `Deno.serve()` instead.", + ); + const rid = op_http_start(conn[internalRidSymbol]); + return new HttpConn(rid, conn.remoteAddr, conn.localAddr); +} + +export { _ws, HttpConn, serveHttp, upgradeWebSocket }; diff --git a/ext/net/02_tls.js b/ext/net/02_tls.js index 8d43e8604fbe93..4216cbe22b92e4 100644 --- a/ext/net/02_tls.js +++ b/ext/net/02_tls.js @@ -51,54 +51,46 @@ async function connectTls({ port, hostname = "127.0.0.1", transport = "tcp", - certFile = undefined, caCerts = [], - certChain = undefined, - privateKey = undefined, + alpnProtocols = undefined, + keyFormat = undefined, cert = undefined, + certFile = undefined, + certChain = undefined, key = undefined, - alpnProtocols = undefined, + keyFile = undefined, + privateKey = undefined, }) { - if (certFile !== undefined) { - internals.warnOnDeprecatedApi( - "Deno.ConnectTlsOptions.certFile", - new Error().stack, - "Pass the cert file contents to the `Deno.ConnectTlsOptions.cert` option instead.", - ); - } - if (certChain !== undefined) { - internals.warnOnDeprecatedApi( - "Deno.ConnectTlsOptions.certChain", - new Error().stack, - "Use the `Deno.ConnectTlsOptions.cert` option instead.", - ); - } - if (privateKey !== undefined) { - internals.warnOnDeprecatedApi( - "Deno.ConnectTlsOptions.privateKey", - new Error().stack, - "Use the `Deno.ConnectTlsOptions.key` option instead.", - ); - } if (transport !== "tcp") { throw new TypeError(`Unsupported transport: '${transport}'`); } - if (certChain !== undefined && cert !== undefined) { - throw new TypeError( - "Cannot specify both `certChain` and `cert`", - ); - } - if (privateKey !== undefined && key !== undefined) { - throw new TypeError( - "Cannot specify both `privateKey` and `key`", + let deprecatedCertFile = undefined; + + // Deno.connectTls has an irregular option where you can just pass `certFile` and + // not `keyFile`. In this case it's used for `caCerts` rather than the client key. + if (certFile !== undefined && keyFile === undefined) { + internals.warnOnDeprecatedApi( + "Deno.ConnectTlsOptions.certFile", + new Error().stack, + "Pass the cert file's contents to the `Deno.ConnectTlsOptions.caCerts` option instead.", ); + + deprecatedCertFile = certFile; + certFile = undefined; } - cert ??= certChain; - key ??= privateKey; - const keyPair = loadTlsKeyPair(cert, undefined, key, undefined); + + const keyPair = loadTlsKeyPair("Deno.connectTls", { + keyFormat, + cert, + certFile, + certChain, + key, + keyFile, + privateKey, + }); const { 0: rid, 1: localAddr, 2: remoteAddr } = await op_net_connect_tls( { hostname, port }, - { certFile, caCerts, cert, key, alpnProtocols }, + { certFile: deprecatedCertFile, caCerts, alpnProtocols }, keyPair, ); localAddr.transport = "tcp"; @@ -137,29 +129,96 @@ class TlsListener extends Listener { } } +/** + * Returns true if this object has the shape of one of the certified key material + * interfaces. + */ function hasTlsKeyPairOptions(options) { return (ReflectHas(options, "cert") || ReflectHas(options, "key") || ReflectHas(options, "certFile") || - ReflectHas(options, "keyFile")); + ReflectHas(options, "keyFile") || ReflectHas(options, "privateKey") || + ReflectHas(options, "certChain")); } -function loadTlsKeyPair( +/** + * Loads a TLS keypair from one of the various options. If no key material is provided, + * returns a special Null keypair. + */ +function loadTlsKeyPair(api, { + keyFormat, cert, certFile, + certChain, key, keyFile, -) { - if ((certFile !== undefined) ^ (keyFile !== undefined)) { - throw new TypeError( - "If certFile is specified, keyFile must also be specified", - ); + privateKey, +}) { + // Check for "pem" format + if (keyFormat !== undefined && keyFormat !== "pem") { + throw new TypeError('If `keyFormat` is specified, it must be "pem"'); } - if ((cert !== undefined) ^ (key !== undefined)) { - throw new TypeError("If cert is specified, key must also be specified"); + + function exclusive(a1, a1v, a2, a2v) { + if (a1v !== undefined && a2v !== undefined) { + throw new TypeError( + `Cannot specify both \`${a1}\` and \`${a2}\` for \`${api}\`.`, + ); + } } + // Ensure that only one pair is valid + exclusive("certChain", certChain, "cert", cert); + exclusive("certChain", certChain, "certFile", certFile); + exclusive("key", key, "keyFile", keyFile); + exclusive("key", key, "privateKey", privateKey); + + function both(a1, a1v, a2, a2v) { + if (a1v !== undefined && a2v === undefined) { + throw new TypeError( + `If \`${a1}\` is specified, \`${a2}\` must be specified as well for \`${api}\`.`, + ); + } + if (a1v === undefined && a2v !== undefined) { + throw new TypeError( + `If \`${a2}\` is specified, \`${a1}\` must be specified as well for \`${api}\`.`, + ); + } + } + + // Pick one pair of cert/key, certFile/keyFile or certChain/privateKey + both("cert", cert, "key", key); + both("certFile", certFile, "keyFile", keyFile); + both("certChain", certChain, "privateKey", privateKey); + if (certFile !== undefined) { - return op_tls_key_static_from_file("Deno.listenTls", certFile, keyFile); + internals.warnOnDeprecatedApi( + "Deno.TlsCertifiedKeyOptions.keyFile", + new Error().stack, + "Pass the key file's contents to the `Deno.TlsCertifiedKeyPem.key` option instead.", + ); + internals.warnOnDeprecatedApi( + "Deno.TlsCertifiedKeyOptions.certFile", + new Error().stack, + "Pass the cert file's contents to the `Deno.TlsCertifiedKeyPem.cert` option instead.", + ); + return op_tls_key_static_from_file(api, certFile, keyFile); + } else if (certChain !== undefined) { + if (api !== "Deno.connectTls") { + throw new TypeError( + `Invalid options 'certChain' and 'privateKey' for ${api}`, + ); + } + internals.warnOnDeprecatedApi( + "Deno.TlsCertifiedKeyOptions.privateKey", + new Error().stack, + "Use the `Deno.TlsCertifiedKeyPem.key` option instead.", + ); + internals.warnOnDeprecatedApi( + "Deno.TlsCertifiedKeyOptions.certChain", + new Error().stack, + "Use the `Deno.TlsCertifiedKeyPem.cert` option instead.", + ); + return op_tls_key_static(certChain, privateKey); } else if (cert !== undefined) { return op_tls_key_static(cert, key); } else { @@ -169,10 +228,6 @@ function loadTlsKeyPair( function listenTls({ port, - cert, - certFile, - key, - keyFile, hostname = "0.0.0.0", transport = "tcp", alpnProtocols = undefined, @@ -181,22 +236,13 @@ function listenTls({ if (transport !== "tcp") { throw new TypeError(`Unsupported transport: '${transport}'`); } - if (keyFile !== undefined) { - internals.warnOnDeprecatedApi( - "Deno.ListenTlsOptions.keyFile", - new Error().stack, - "Pass the key file contents to the `Deno.ListenTlsOptions.key` option instead.", - ); - } - if (certFile !== undefined) { - internals.warnOnDeprecatedApi( - "Deno.ListenTlsOptions.certFile", - new Error().stack, - "Pass the cert file contents to the `Deno.ListenTlsOptions.cert` option instead.", + + if (!hasTlsKeyPairOptions(arguments[0])) { + throw new TypeError( + "A key and certificate are required for `Deno.listenTls`", ); } - - const keyPair = loadTlsKeyPair(cert, certFile, key, keyFile); + const keyPair = loadTlsKeyPair("Deno.listenTls", arguments[0]); const { 0: rid, 1: localAddr } = op_net_listen_tls( { hostname, port: Number(port) }, { alpnProtocols, reusePort }, diff --git a/ext/net/lib.deno_net.d.ts b/ext/net/lib.deno_net.d.ts index 597b3d3483fa58..4b88b154aa9d3f 100644 --- a/ext/net/lib.deno_net.d.ts +++ b/ext/net/lib.deno_net.d.ts @@ -197,12 +197,50 @@ declare namespace Deno { options: UnixListenOptions & { transport: "unix" }, ): UnixListener; - /** @category Network */ - export interface ListenTlsOptions extends TcpListenOptions { - /** Server private key in PEM format */ - key?: string; - /** Cert chain in PEM format */ - cert?: string; + /** Provides TLS certified keys, ie: a key that has been certified by a trusted certificate authority. + * A certified key generally consists of a private key and certificate part. + * + * @category Network + */ + export type TlsCertifiedKeyOptions = + | TlsCertifiedKeyPem + | TlsCertifiedKeyFromFile + | TlsCertifiedKeyConnectTls; + + /** + * Provides certified key material from strings. The key material is provided in + * `PEM`-format (Privacy Enhanced Mail, https://www.rfc-editor.org/rfc/rfc1422) which can be identified by having + * `-----BEGIN-----` and `-----END-----` markers at the beginning and end of the strings. This type of key is not compatible + * with `DER`-format keys which are binary. + * + * Deno supports RSA, EC, and PKCS8-format keys. + * + * ```ts + * const key = { + * key: "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n", + * cert: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n" } + * }; + * ``` + * + * @category Network + */ + export interface TlsCertifiedKeyPem { + /** The format of this key material, which must be PEM. */ + keyFormat?: "pem"; + /** Private key in `PEM` format. RSA, EC, and PKCS8-format keys are supported. */ + key: string; + /** Certificate chain in `PEM` format. */ + cert: string; + } + + /** + * @deprecated This will be removed in Deno 2.0. See the + * {@link https://docs.deno.com/runtime/manual/advanced/migrate_deprecations | Deno 1.x to 2.x Migration Guide} + * for migration instructions. + * + * @category Network + */ + export interface TlsCertifiedKeyFromFile { /** Path to a file containing a PEM formatted CA certificate. Requires * `--allow-read`. * @@ -211,16 +249,45 @@ declare namespace Deno { * {@link https://docs.deno.com/runtime/manual/advanced/migrate_deprecations | Deno 1.x to 2.x Migration Guide} * for migration instructions. */ - certFile?: string; - /** Server private key file. Requires `--allow-read`. + certFile: string; + /** Path to a file containing a private key file. Requires `--allow-read`. * * @tags allow-read * @deprecated This will be removed in Deno 2.0. See the * {@link https://docs.deno.com/runtime/manual/advanced/migrate_deprecations | Deno 1.x to 2.x Migration Guide} * for migration instructions. */ - keyFile?: string; + keyFile: string; + } + /** + * @deprecated This will be removed in Deno 2.0. See the + * {@link https://docs.deno.com/runtime/manual/advanced/migrate_deprecations | Deno 1.x to 2.x Migration Guide} + * for migration instructions. + * + * @category Network + */ + export interface TlsCertifiedKeyConnectTls { + /** + * Certificate chain in `PEM` format. + * + * @deprecated This will be removed in Deno 2.0. See the + * {@link https://docs.deno.com/runtime/manual/advanced/migrate_deprecations | Deno 1.x to 2.x Migration Guide} + * for migration instructions. + */ + certChain: string; + /** + * Private key in `PEM` format. RSA, EC, and PKCS8-format keys are supported. + * + * @deprecated This will be removed in Deno 2.0. See the + * {@link https://docs.deno.com/runtime/manual/advanced/migrate_deprecations | Deno 1.x to 2.x Migration Guide} + * for migration instructions. + */ + privateKey: string; + } + + /** @category Network */ + export interface ListenTlsOptions extends TcpListenOptions { transport?: "tcp"; /** Application-Layer Protocol Negotiation (ALPN) protocols to announce to @@ -246,7 +313,9 @@ declare namespace Deno { * @tags allow-net * @category Network */ - export function listenTls(options: ListenTlsOptions): TlsListener; + export function listenTls( + options: ListenTlsOptions & TlsCertifiedKeyOptions, + ): TlsListener; /** @category Network */ export interface ConnectOptions { @@ -343,9 +412,11 @@ declare namespace Deno { * * @default {"127.0.0.1"} */ hostname?: string; - /** - * Server certificate file. + /** Path to a file containing a PEM formatted list of root certificates that will + * be used in addition to the default root certificates to verify the peer's certificate. Requires + * `--allow-read`. * + * @tags allow-read * @deprecated This will be removed in Deno 2.0. See the * {@link https://docs.deno.com/runtime/manual/advanced/migrate_deprecations | Deno 1.x to 2.x Migration Guide} * for migration instructions. @@ -361,26 +432,6 @@ declare namespace Deno { * TLS handshake. */ alpnProtocols?: string[]; - /** - * PEM formatted client certificate chain. - * - * @deprecated This will be removed in Deno 2.0. See the - * {@link https://docs.deno.com/runtime/manual/advanced/migrate_deprecations | Deno 1.x to 2.x Migration Guide} - * for migration instructions. - */ - certChain?: string; - /** - * PEM formatted (RSA or PKCS8) private key of client certificate. - * - * @deprecated This will be removed in Deno 2.0. See the - * {@link https://docs.deno.com/runtime/manual/advanced/migrate_deprecations | Deno 1.x to 2.x Migration Guide} - * for migration instructions. - */ - privateKey?: string; - /** Server private key in PEM format. */ - key?: string; - /** Cert chain in PEM format. */ - cert?: string; } /** Establishes a secure connection over TLS (transport layer security) using @@ -403,6 +454,30 @@ declare namespace Deno { */ export function connectTls(options: ConnectTlsOptions): Promise; + /** Establishes a secure connection over TLS (transport layer security) using + * an optional cert file, client certificate, hostname (default is "127.0.0.1") and + * port. The cert file is optional and if not included Mozilla's root certificates will + * be used (see also https://github.com/ctz/webpki-roots for specifics) + * + * ```ts + * const caCert = await Deno.readTextFile("./certs/my_custom_root_CA.pem"); + * const key = "----BEGIN PRIVATE KEY----..."; + * const cert = "----BEGIN CERTIFICATE----..."; + * const conn1 = await Deno.connectTls({ port: 80, key, cert }); + * const conn2 = await Deno.connectTls({ caCerts: [caCert], hostname: "192.0.2.1", port: 80, key, cert }); + * const conn3 = await Deno.connectTls({ hostname: "[2001:db8::1]", port: 80, key, cert }); + * const conn4 = await Deno.connectTls({ caCerts: [caCert], hostname: "golang.org", port: 80, key, cert }); + * ``` + * + * Requires `allow-net` permission. + * + * @tags allow-net + * @category Network + */ + export function connectTls( + options: ConnectTlsOptions & TlsCertifiedKeyOptions, + ): Promise; + /** @category Network */ export interface StartTlsOptions { /** A literal IP address or host name that can be resolved to an IP address. diff --git a/ext/net/ops_tls.rs b/ext/net/ops_tls.rs index c0ac315865af4e..487adf3bc732ce 100644 --- a/ext/net/ops_tls.rs +++ b/ext/net/ops_tls.rs @@ -10,6 +10,7 @@ use crate::tcp::TcpListener; use crate::DefaultTlsOptions; use crate::NetPermissions; use crate::UnsafelyIgnoreCertificateErrors; +use deno_core::anyhow::anyhow; use deno_core::error::bad_resource; use deno_core::error::custom_error; use deno_core::error::generic_error; @@ -448,18 +449,13 @@ where .with_no_client_auth(); let mut tls_config = match keys { - TlsKeys::Null => { - unreachable!() - } - TlsKeys::Static(TlsKey(cert, key)) => { - tls_config.with_single_cert(cert.clone(), key.clone()) - } + TlsKeys::Null => Err(anyhow!("Deno.listenTls requires a key")), + TlsKeys::Static(TlsKey(cert, key)) => tls_config + .with_single_cert(cert.clone(), key.clone()) + .map_err(|e| anyhow!(e)), } .map_err(|e| { - custom_error( - "InvalidData", - format!("Error creating TLS certificate: {:?}", e), - ) + custom_error("InvalidData", "Error creating TLS certificate").context(e) })?; if let Some(alpn_protocols) = args.alpn_protocols { diff --git a/ext/node/polyfills/_tls_wrap.ts b/ext/node/polyfills/_tls_wrap.ts index a70dd29f14e61e..ed2bdd0a3943e8 100644 --- a/ext/node/polyfills/_tls_wrap.ts +++ b/ext/node/polyfills/_tls_wrap.ts @@ -96,7 +96,7 @@ export class TLSSocket extends net.Socket { caCerts = [new TextDecoder().decode(caCerts)]; } tlsOptions.caCerts = caCerts; - tlsOptions.alpnProtocols = ["h2", "http/1.1"]; + tlsOptions.alpnProtocols = opts.ALPNProtocols; super({ handle: _wrapHandle(tlsOptions, socket), @@ -114,7 +114,7 @@ export class TLSSocket extends net.Socket { this.secureConnecting = true; this._SNICallback = null; this.servername = null; - this.alpnProtocols = tlsOptions.alpnProtocols; + this.alpnProtocols = tlsOptions.ALPNProtocols; this.authorized = false; this.authorizationError = null; this[kRes] = null; diff --git a/ext/node/polyfills/http2.ts b/ext/node/polyfills/http2.ts index 2856d39381b6c3..023b6acd3faabc 100644 --- a/ext/node/polyfills/http2.ts +++ b/ext/node/polyfills/http2.ts @@ -1677,7 +1677,10 @@ export function connect( case "https:": // TODO(bartlomieju): handle `initializeTLSOptions` here url = `https://${host}${port == 443 ? "" : (":" + port)}`; - socket = tlsConnect(port, host, { manualStart: true }); + socket = tlsConnect(port, host, { + manualStart: true, + ALPNProtocols: ["h2", "http/1.1"], + }); break; default: throw new ERR_HTTP2_UNSUPPORTED_PROTOCOL(protocol); diff --git a/runtime/js/40_http.js b/runtime/js/40_http.js deleted file mode 100644 index 675d428cdf4bac..00000000000000 --- a/runtime/js/40_http.js +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -import { core, internals } from "ext:core/mod.js"; -import { op_http_start } from "ext:core/ops"; -const { internalRidSymbol } = core; - -import { HttpConn } from "ext:deno_http/01_http.js"; - -function serveHttp(conn) { - internals.warnOnDeprecatedApi( - "Deno.serveHttp()", - new Error().stack, - "Use `Deno.serve()` instead.", - ); - const rid = op_http_start(conn[internalRidSymbol]); - return new HttpConn(rid, conn.remoteAddr, conn.localAddr); -} - -export { serveHttp }; diff --git a/runtime/js/90_deno_ns.js b/runtime/js/90_deno_ns.js index 9f403e901e2a42..aa52b0c3379bb2 100644 --- a/runtime/js/90_deno_ns.js +++ b/runtime/js/90_deno_ns.js @@ -13,6 +13,7 @@ import * as console from "ext:deno_console/01_console.js"; import * as ffi from "ext:deno_ffi/00_ffi.js"; import * as net from "ext:deno_net/01_net.js"; import * as tls from "ext:deno_net/02_tls.js"; +import * as serve from "ext:deno_http/00_serve.js"; import * as http from "ext:deno_http/01_http.js"; import * as errors from "ext:runtime/01_errors.js"; import * as version from "ext:runtime/01_version.ts"; @@ -25,8 +26,6 @@ import * as fsEvents from "ext:runtime/40_fs_events.js"; import * as process from "ext:runtime/40_process.js"; import * as signals from "ext:runtime/40_signals.js"; import * as tty from "ext:runtime/40_tty.js"; -// TODO(bartlomieju): this is funky we have two `http` imports -import * as httpRuntime from "ext:runtime/40_http.js"; import * as kv from "ext:deno_kv/01_db.ts"; import * as cron from "ext:deno_cron/01_cron.ts"; import * as webgpuSurface from "ext:deno_webgpu/02_surface.js"; @@ -225,9 +224,8 @@ const denoNs = { permissions: permissions.permissions, Permissions: permissions.Permissions, PermissionStatus: permissions.PermissionStatus, - // TODO(bartlomieju): why is this not in one of extensions? - serveHttp: httpRuntime.serveHttp, - serve: http.serve, + serveHttp: http.serveHttp, + serve: serve.serve, resolveDns: net.resolveDns, upgradeWebSocket: http.upgradeWebSocket, utime: fs.utime, @@ -292,8 +290,6 @@ denoNsUnstableById[unstableIds.fs] = { denoNsUnstableById[unstableIds.http] = { HttpClient: httpClient.HttpClient, createHttpClient: httpClient.createHttpClient, - // TODO(bartlomieju): why is it needed? - http, }; denoNsUnstableById[unstableIds.kv] = { @@ -328,8 +324,6 @@ const denoNsUnstable = { umask: fs.umask, HttpClient: httpClient.HttpClient, createHttpClient: httpClient.createHttpClient, - // TODO(bartlomieju): why is it needed? - http, dlopen: ffi.dlopen, UnsafeCallback: ffi.UnsafeCallback, UnsafePointer: ffi.UnsafePointer, diff --git a/runtime/shared.rs b/runtime/shared.rs index 946f33c6fb7a4a..61f9ca22500ba1 100644 --- a/runtime/shared.rs +++ b/runtime/shared.rs @@ -45,7 +45,6 @@ extension!(runtime, "13_buffer.js", "30_os.js", "40_fs_events.js", - "40_http.js", "40_process.js", "40_signals.js", "40_tty.js", diff --git a/tests/integration/pm_tests.rs b/tests/integration/pm_tests.rs index a8af67e5b7486d..613ceef3265e99 100644 --- a/tests/integration/pm_tests.rs +++ b/tests/integration/pm_tests.rs @@ -49,6 +49,26 @@ fn add_basic_no_deno_json() { temp_dir.join("deno.json").assert_matches_text(expected); } +#[test] +fn add_basic_with_empty_deno_json() { + let context = pm_context_builder().build(); + let temp_dir = context.temp_dir(); + temp_dir.write("deno.json", ""); + + let output = context.new_command().args("add @denotest/add").run(); + output.assert_exit_code(0); + let output = output.combined_output(); + assert_contains!(output, "Add @denotest/add"); + temp_dir + .path() + .join("deno.json") + .assert_matches_json(json!({ + "imports": { + "@denotest/add": "jsr:@denotest/add@^1.0.0" + } + })); +} + #[test] fn add_version_contraint() { let context = pm_context_builder().build(); diff --git a/tests/integration/vendor_tests.rs b/tests/integration/vendor_tests.rs index ab1119fe8eba2b..ce6aa704437c78 100644 --- a/tests/integration/vendor_tests.rs +++ b/tests/integration/vendor_tests.rs @@ -528,6 +528,39 @@ fn update_existing_config_test() { assert!(output.status.success()); } +#[test] +fn update_existing_empty_config_test() { + let _server = http_server(); + let t = TempDir::new(); + t.write( + "my_app.ts", + "import {Logger} from 'http://localhost:4545/vendor/logger.ts'; new Logger().log('outputted');", + ); + t.write("deno.json", ""); + + let deno = util::deno_cmd() + .current_dir(t.path()) + .arg("vendor") + .arg("my_app.ts") + .arg("--output") + .arg("vendor2") + .env("NO_COLOR", "1") + .piped_output() + .spawn() + .unwrap(); + let output = deno.wait_with_output().unwrap(); + assert_eq!( + String::from_utf8_lossy(&output.stderr).trim(), + format!( + "Download http://localhost:4545/vendor/logger.ts\n{}\n\n{}", + vendored_text("1 module", "vendor2"), + success_text_updated_deno_json("vendor2",) + ) + ); + assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), ""); + assert!(output.status.success()); +} + #[test] fn vendor_npm_node_specifiers() { let context = TestContextBuilder::for_npm().use_temp_cwd().build(); diff --git a/tests/testdata/run/unstable_http.disabled.out b/tests/testdata/run/unstable_http.disabled.out index 6eb83832e1bc02..3562f72fdd40b3 100644 --- a/tests/testdata/run/unstable_http.disabled.out +++ b/tests/testdata/run/unstable_http.disabled.out @@ -1,12 +1,4 @@ main undefined main undefined -main undefined -main undefined -main undefined -main undefined -worker undefined -worker undefined -worker undefined -worker undefined worker undefined worker undefined diff --git a/tests/testdata/run/unstable_http.enabled.out b/tests/testdata/run/unstable_http.enabled.out index 4f3c6562532434..f7aa776e9efe2f 100644 --- a/tests/testdata/run/unstable_http.enabled.out +++ b/tests/testdata/run/unstable_http.enabled.out @@ -1,12 +1,4 @@ main [class HttpClient] main [Function: createHttpClient] -main [class HttpConn] -main Symbol("[[associated_ws]]") -main [Function: serve] -main [Function: upgradeWebSocket] worker [class HttpClient] worker [Function: createHttpClient] -worker [class HttpConn] -worker Symbol("[[associated_ws]]") -worker [Function: serve] -worker [Function: upgradeWebSocket] diff --git a/tests/testdata/run/unstable_http.js b/tests/testdata/run/unstable_http.js index 1a3ddb2d3a09ae..7ad09aec57be68 100644 --- a/tests/testdata/run/unstable_http.js +++ b/tests/testdata/run/unstable_http.js @@ -2,10 +2,6 @@ const scope = import.meta.url.slice(-7) === "#worker" ? "worker" : "main"; console.log(scope, Deno.HttpClient); console.log(scope, Deno.createHttpClient); -console.log(scope, Deno.http?.HttpConn); -console.log(scope, Deno.http?._ws); -console.log(scope, Deno.http?.serve); -console.log(scope, Deno.http?.upgradeWebSocket); if (scope === "worker") { postMessage("done"); diff --git a/tests/unit/fetch_test.ts b/tests/unit/fetch_test.ts index c33503bdfc4c4a..3202d40fa73a50 100644 --- a/tests/unit/fetch_test.ts +++ b/tests/unit/fetch_test.ts @@ -1249,44 +1249,16 @@ Deno.test({}, function fetchWritableRespProps() { Deno.test( { permissions: { net: true } }, - async function fetchFilterOutCustomHostHeader(): Promise< - void - > { + async function fetchFilterOutCustomHostHeader() { const addr = `127.0.0.1:${listenPort}`; - const [hostname, port] = addr.split(":"); - const listener = Deno.listen({ - hostname, - port: Number(port), - }) as Deno.Listener; - - let httpConn: Deno.HttpConn; - listener.accept().then(async (conn: Deno.Conn) => { - httpConn = Deno.serveHttp(conn); - - await httpConn.nextRequest() - .then(async (requestEvent: Deno.RequestEvent | null) => { - const hostHeader = requestEvent?.request.headers.get("Host"); - const headersToReturn = hostHeader - ? { "Host": hostHeader } - : undefined; - - await requestEvent?.respondWith( - new Response("", { - status: 200, - headers: headersToReturn, - }), - ); - }); + const server = Deno.serve({ port: listenPort }, (req) => { + return new Response(`Host header was ${req.headers.get("Host")}`); }); - const response = await fetch(`http://${addr}/`, { headers: { "Host": "example.com" }, }); - await response.text(); - listener.close(); - httpConn!.close(); - - assertEquals(response.headers.get("Host"), addr); + assertEquals(await response.text(), `Host header was ${addr}`); + await server.shutdown(); }, ); diff --git a/tests/unit/tls_test.ts b/tests/unit/tls_test.ts index 8f0a296c72ff88..5be05b73e3fb53 100644 --- a/tests/unit/tls_test.ts +++ b/tests/unit/tls_test.ts @@ -1336,7 +1336,7 @@ Deno.test( }); }, TypeError, - "Cannot specify both `privateKey` and `key`", + "Cannot specify both `key` and `privateKey` for `Deno.connectTls`.", ); }, ); diff --git a/tests/unit_node/http_test.ts b/tests/unit_node/http_test.ts index 049cdbbbc7b403..6672b97474248d 100644 --- a/tests/unit_node/http_test.ts +++ b/tests/unit_node/http_test.ts @@ -9,7 +9,6 @@ import { assertSpyCalls, spy } from "@std/testing/mock.ts"; import { gzip } from "node:zlib"; import { Buffer } from "node:buffer"; -import { serve } from "@std/http/server.ts"; import { execCode } from "../unit/test_util.ts"; Deno.test("[node/http listen]", async () => { @@ -338,20 +337,18 @@ Deno.test("[node/http] send request with non-chunked body", async () => { const hostname = "localhost"; const port = 4505; - // NOTE: Instead of node/http.createServer(), serve() in std/http/server.ts is used. - // https://github.com/denoland/deno_std/pull/2755#discussion_r1005592634 const handler = async (req: Request) => { requestHeaders = req.headers; requestBody = await req.text(); return new Response("ok"); }; const abortController = new AbortController(); - const servePromise = serve(handler, { + const servePromise = Deno.serve({ hostname, port, signal: abortController.signal, onListen: undefined, - }); + }, handler).finished; const opts: RequestOptions = { host: hostname, @@ -393,20 +390,18 @@ Deno.test("[node/http] send request with chunked body", async () => { const hostname = "localhost"; const port = 4505; - // NOTE: Instead of node/http.createServer(), serve() in std/http/server.ts is used. - // https://github.com/denoland/deno_std/pull/2755#discussion_r1005592634 const handler = async (req: Request) => { requestHeaders = req.headers; requestBody = await req.text(); return new Response("ok"); }; const abortController = new AbortController(); - const servePromise = serve(handler, { + const servePromise = Deno.serve({ hostname, port, signal: abortController.signal, onListen: undefined, - }); + }, handler).finished; const opts: RequestOptions = { host: hostname, @@ -442,20 +437,18 @@ Deno.test("[node/http] send request with chunked body as default", async () => { const hostname = "localhost"; const port = 4505; - // NOTE: Instead of node/http.createServer(), serve() in std/http/server.ts is used. - // https://github.com/denoland/deno_std/pull/2755#discussion_r1005592634 const handler = async (req: Request) => { requestHeaders = req.headers; requestBody = await req.text(); return new Response("ok"); }; const abortController = new AbortController(); - const servePromise = serve(handler, { + const servePromise = Deno.serve({ hostname, port, signal: abortController.signal, onListen: undefined, - }); + }, handler).finished; const opts: RequestOptions = { host: hostname, diff --git a/tests/unit_node/net_test.ts b/tests/unit_node/net_test.ts index eac4633cbcdb33..e08b24c028c078 100644 --- a/tests/unit_node/net_test.ts +++ b/tests/unit_node/net_test.ts @@ -55,6 +55,7 @@ Deno.test("[node/net] net.connect().unref() works", async () => { const ctl = new AbortController(); const server = Deno.serve({ signal: ctl.signal, + port: 0, // any available port will do handler: () => new Response("hello"), onListen: async ({ port, hostname }) => { const { stdout, stderr } = await new Deno.Command(Deno.execPath(), { diff --git a/tests/unit_node/tls_test.ts b/tests/unit_node/tls_test.ts index 7b4b35b98419b4..db954f328697f4 100644 --- a/tests/unit_node/tls_test.ts +++ b/tests/unit_node/tls_test.ts @@ -3,7 +3,6 @@ import { assertEquals, assertInstanceOf } from "@std/assert/mod.ts"; import { delay } from "@std/async/delay.ts"; import { fromFileUrl, join } from "@std/path/mod.ts"; -import { serveTls } from "@std/http/server.ts"; import * as tls from "node:tls"; import * as net from "node:net"; import * as stream from "node:stream"; @@ -13,24 +12,61 @@ const tlsTestdataDir = fromFileUrl( ); const keyFile = join(tlsTestdataDir, "localhost.key"); const certFile = join(tlsTestdataDir, "localhost.crt"); -const key = await Deno.readTextFile(keyFile); -const cert = await Deno.readTextFile(certFile); -const rootCaCert = await Deno.readTextFile(join(tlsTestdataDir, "RootCA.pem")); +const key = Deno.readTextFileSync(keyFile); +const cert = Deno.readTextFileSync(certFile); +const rootCaCert = Deno.readTextFileSync(join(tlsTestdataDir, "RootCA.pem")); + +for ( + const [alpnServer, alpnClient, expected] of [ + [["a", "b"], ["a"], ["a"]], + [["a", "b"], ["b"], ["b"]], + [["a", "b"], ["a", "b"], ["a"]], + [["a", "b"], [], []], + [[], ["a", "b"], []], + ] +) { + Deno.test(`tls.connect sends correct ALPN: '${alpnServer}' + '${alpnClient}' = '${expected}'`, async () => { + const listener = Deno.listenTls({ + port: 0, + key, + cert, + alpnProtocols: alpnServer, + }); + const outgoing = tls.connect({ + host: "localhost", + port: listener.addr.port, + ALPNProtocols: alpnClient, + secureContext: { + ca: rootCaCert, + // deno-lint-ignore no-explicit-any + } as any, + }); + + const conn = await listener.accept(); + const handshake = await conn.handshake(); + assertEquals(handshake.alpnProtocol, expected[0] || null); + conn.close(); + outgoing.destroy(); + listener.close(); + }); +} Deno.test("tls.connect makes tls connection", async () => { const ctl = new AbortController(); - const serve = serveTls(() => new Response("hello"), { - port: 8443, + let port; + const serve = Deno.serve({ + port: 0, key, cert, signal: ctl.signal, - }); + onListen: (listen) => port = listen.port, + }, () => new Response("hello")); await delay(200); const conn = tls.connect({ host: "localhost", - port: 8443, + port, secureContext: { ca: rootCaCert, // deno-lint-ignore no-explicit-any @@ -41,26 +77,29 @@ Host: localhost Connection: close `); - conn.on("data", (chunk) => { - const text = new TextDecoder().decode(chunk); - const bodyText = text.split("\r\n\r\n").at(-1)?.trim(); - assertEquals(bodyText, "hello"); + const chunk = Promise.withResolvers(); + conn.on("data", (received) => { conn.destroy(); ctl.abort(); + chunk.resolve(received); }); - await serve; + await serve.finished; + + const text = new TextDecoder().decode(await chunk.promise); + const bodyText = text.split("\r\n\r\n").at(-1)?.trim(); + assertEquals(bodyText, "hello"); }); // https://github.com/denoland/deno/pull/20120 Deno.test("tls.connect mid-read tcp->tls upgrade", async () => { const ctl = new AbortController(); - const serve = serveTls(() => new Response("hello"), { + const serve = Deno.serve({ port: 8443, key, cert, signal: ctl.signal, - }); + }, () => new Response("hello")); await delay(200); @@ -81,7 +120,7 @@ Deno.test("tls.connect mid-read tcp->tls upgrade", async () => { ctl.abort(); }); - await serve; + await serve.finished; }); Deno.test("tls.createServer creates a TLS server", async () => { diff --git a/tools/core_import_map.json b/tools/core_import_map.json index 078808aa0db8b1..463095de8a1c20 100644 --- a/tools/core_import_map.json +++ b/tools/core_import_map.json @@ -237,7 +237,6 @@ "ext:runtime/13_buffer.js": "../runtime/js/13_buffer.js", "ext:runtime/30_os.js": "../runtime/js/30_os.js", "ext:runtime/40_fs_events.js": "../runtime/js/40_fs_events.js", - "ext:runtime/40_http.js": "../runtime/js/40_http.js", "ext:runtime/40_process.js": "../runtime/js/40_process.js", "ext:runtime/40_signals.js": "../runtime/js/40_signals.js", "ext:runtime/40_tty.js": "../runtime/js/40_tty.js",