mirror of
https://github.com/denoland/deno.git
synced 2024-11-25 15:29:32 -05:00
feat(unstable): Add Deno.upgradeHttp API (#13618)
This commit adds "Deno.upgradeHttp" API, which allows to "hijack" connection and switch protocols, to eg. implement WebSocket required for Node compat. Co-authored-by: crowlkats <crowlkats@toaxl.com> Co-authored-by: Ryan Dahl <ry@tinyclouds.org> Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
This commit is contained in:
parent
89a41d0a67
commit
c5270abad7
8 changed files with 187 additions and 9 deletions
14
cli/dts/lib.deno.unstable.d.ts
vendored
14
cli/dts/lib.deno.unstable.d.ts
vendored
|
@ -1333,6 +1333,20 @@ declare namespace Deno {
|
||||||
* Make the timer of the given id not blocking the event loop from finishing
|
* Make the timer of the given id not blocking the event loop from finishing
|
||||||
*/
|
*/
|
||||||
export function unrefTimer(id: number): void;
|
export function unrefTimer(id: number): void;
|
||||||
|
|
||||||
|
/** **UNSTABLE**: new API, yet to be vetter.
|
||||||
|
*
|
||||||
|
* Allows to "hijack" a connection that the request is associated with.
|
||||||
|
* Can be used to implement protocols that build on top of HTTP (eg.
|
||||||
|
* WebSockets).
|
||||||
|
*
|
||||||
|
* The returned promise returns underlying connection and first packet
|
||||||
|
* received. The promise shouldn't be awaited before responding to the
|
||||||
|
* `request`, otherwise event loop might deadlock.
|
||||||
|
*/
|
||||||
|
export function upgradeHttp(
|
||||||
|
request: Request,
|
||||||
|
): Promise<[Deno.Conn, Uint8Array]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare function fetch(
|
declare function fetch(
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
BufWriter,
|
BufWriter,
|
||||||
} from "../../../test_util/std/io/buffer.ts";
|
} from "../../../test_util/std/io/buffer.ts";
|
||||||
import { TextProtoReader } from "../../../test_util/std/textproto/mod.ts";
|
import { TextProtoReader } from "../../../test_util/std/textproto/mod.ts";
|
||||||
|
import { serve } from "../../../test_util/std/http/server.ts";
|
||||||
import {
|
import {
|
||||||
assert,
|
assert,
|
||||||
assertEquals,
|
assertEquals,
|
||||||
|
@ -1738,6 +1739,49 @@ Deno.test({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test("upgradeHttp", async () => {
|
||||||
|
async function client() {
|
||||||
|
const tcpConn = await Deno.connect({ port: 4501 });
|
||||||
|
await tcpConn.write(
|
||||||
|
new TextEncoder().encode(
|
||||||
|
"CONNECT server.example.com:80 HTTP/1.1\r\n\r\nbla bla bla\nbla bla\nbla\n",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setTimeout(async () => {
|
||||||
|
await tcpConn.write(
|
||||||
|
new TextEncoder().encode(
|
||||||
|
"bla bla bla\nbla bla\nbla\n",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
tcpConn.close();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
const abortController = new AbortController();
|
||||||
|
const signal = abortController.signal;
|
||||||
|
|
||||||
|
const server = serve((req) => {
|
||||||
|
const p = Deno.upgradeHttp(req);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const [conn, firstPacket] = await p;
|
||||||
|
const buf = new Uint8Array(1024);
|
||||||
|
const firstPacketText = new TextDecoder().decode(firstPacket);
|
||||||
|
assertEquals(firstPacketText, "bla bla bla\nbla bla\nbla\n");
|
||||||
|
const n = await conn.read(buf);
|
||||||
|
assert(n != null);
|
||||||
|
const secondPacketText = new TextDecoder().decode(buf.slice(0, n));
|
||||||
|
assertEquals(secondPacketText, "bla bla bla\nbla bla\nbla\n");
|
||||||
|
abortController.abort();
|
||||||
|
conn.close();
|
||||||
|
})();
|
||||||
|
|
||||||
|
return new Response(null, { status: 101 });
|
||||||
|
}, { port: 4501, signal });
|
||||||
|
|
||||||
|
await Promise.all([server, client()]);
|
||||||
|
});
|
||||||
|
|
||||||
function chunkedBodyReader(h: Headers, r: BufReader): Deno.Reader {
|
function chunkedBodyReader(h: Headers, r: BufReader): Deno.Reader {
|
||||||
// Based on https://tools.ietf.org/html/rfc2616#section-19.4.6
|
// Based on https://tools.ietf.org/html/rfc2616#section-19.4.6
|
||||||
const tp = new TextProtoReader(r);
|
const tp = new TextProtoReader(r);
|
||||||
|
|
|
@ -30,10 +30,14 @@
|
||||||
_idleTimeoutTimeout,
|
_idleTimeoutTimeout,
|
||||||
_serverHandleIdleTimeout,
|
_serverHandleIdleTimeout,
|
||||||
} = window.__bootstrap.webSocket;
|
} = window.__bootstrap.webSocket;
|
||||||
|
const { TcpConn } = window.__bootstrap.net;
|
||||||
|
const { TlsConn } = window.__bootstrap.tls;
|
||||||
|
const { Deferred } = window.__bootstrap.streams;
|
||||||
const {
|
const {
|
||||||
ArrayPrototypeIncludes,
|
ArrayPrototypeIncludes,
|
||||||
ArrayPrototypePush,
|
ArrayPrototypePush,
|
||||||
ArrayPrototypeSome,
|
ArrayPrototypeSome,
|
||||||
|
Error,
|
||||||
ObjectPrototypeIsPrototypeOf,
|
ObjectPrototypeIsPrototypeOf,
|
||||||
PromisePrototype,
|
PromisePrototype,
|
||||||
Set,
|
Set,
|
||||||
|
@ -53,10 +57,13 @@
|
||||||
} = window.__bootstrap.primordials;
|
} = window.__bootstrap.primordials;
|
||||||
|
|
||||||
const connErrorSymbol = Symbol("connError");
|
const connErrorSymbol = Symbol("connError");
|
||||||
|
const _deferred = Symbol("upgradeHttpDeferred");
|
||||||
|
|
||||||
class HttpConn {
|
class HttpConn {
|
||||||
#rid = 0;
|
#rid = 0;
|
||||||
#closed = false;
|
#closed = false;
|
||||||
|
#remoteAddr;
|
||||||
|
#localAddr;
|
||||||
|
|
||||||
// This set holds resource ids of resources
|
// This set holds resource ids of resources
|
||||||
// that were created during lifecycle of this request.
|
// that were created during lifecycle of this request.
|
||||||
|
@ -64,8 +71,10 @@
|
||||||
// as well.
|
// as well.
|
||||||
managedResources = new Set();
|
managedResources = new Set();
|
||||||
|
|
||||||
constructor(rid) {
|
constructor(rid, remoteAddr, localAddr) {
|
||||||
this.#rid = rid;
|
this.#rid = rid;
|
||||||
|
this.#remoteAddr = remoteAddr;
|
||||||
|
this.#localAddr = localAddr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @returns {number} */
|
/** @returns {number} */
|
||||||
|
@ -125,7 +134,13 @@
|
||||||
const signal = abortSignal.newSignal();
|
const signal = abortSignal.newSignal();
|
||||||
const request = fromInnerRequest(innerRequest, signal, "immutable");
|
const request = fromInnerRequest(innerRequest, signal, "immutable");
|
||||||
|
|
||||||
const respondWith = createRespondWith(this, streamRid);
|
const respondWith = createRespondWith(
|
||||||
|
this,
|
||||||
|
streamRid,
|
||||||
|
request,
|
||||||
|
this.#remoteAddr,
|
||||||
|
this.#localAddr,
|
||||||
|
);
|
||||||
|
|
||||||
return { request, respondWith };
|
return { request, respondWith };
|
||||||
}
|
}
|
||||||
|
@ -159,7 +174,13 @@
|
||||||
return core.opAsync("op_http_read", streamRid, buf);
|
return core.opAsync("op_http_read", streamRid, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRespondWith(httpConn, streamRid) {
|
function createRespondWith(
|
||||||
|
httpConn,
|
||||||
|
streamRid,
|
||||||
|
request,
|
||||||
|
remoteAddr,
|
||||||
|
localAddr,
|
||||||
|
) {
|
||||||
return async function respondWith(resp) {
|
return async function respondWith(resp) {
|
||||||
try {
|
try {
|
||||||
if (ObjectPrototypeIsPrototypeOf(PromisePrototype, resp)) {
|
if (ObjectPrototypeIsPrototypeOf(PromisePrototype, resp)) {
|
||||||
|
@ -282,6 +303,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deferred = request[_deferred];
|
||||||
|
if (deferred) {
|
||||||
|
const res = await core.opAsync("op_http_upgrade", streamRid);
|
||||||
|
let conn;
|
||||||
|
if (res.connType === "tcp") {
|
||||||
|
conn = new TcpConn(res.connRid, remoteAddr, localAddr);
|
||||||
|
} else if (res.connType === "tls") {
|
||||||
|
conn = new TlsConn(res.connRid, remoteAddr, localAddr);
|
||||||
|
} else {
|
||||||
|
throw new Error("unreachable");
|
||||||
|
}
|
||||||
|
|
||||||
|
deferred.resolve([conn, res.readBuf]);
|
||||||
|
}
|
||||||
const ws = resp[_ws];
|
const ws = resp[_ws];
|
||||||
if (ws) {
|
if (ws) {
|
||||||
const wsRid = await core.opAsync(
|
const wsRid = await core.opAsync(
|
||||||
|
@ -425,8 +460,14 @@
|
||||||
return { response, socket };
|
return { response, socket };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function upgradeHttp(req) {
|
||||||
|
req[_deferred] = new Deferred();
|
||||||
|
return req[_deferred].promise;
|
||||||
|
}
|
||||||
|
|
||||||
window.__bootstrap.http = {
|
window.__bootstrap.http = {
|
||||||
HttpConn,
|
HttpConn,
|
||||||
upgradeWebSocket,
|
upgradeWebSocket,
|
||||||
|
upgradeHttp,
|
||||||
};
|
};
|
||||||
})(this);
|
})(this);
|
||||||
|
|
|
@ -289,9 +289,9 @@ impl HttpAcceptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A resource representing a single HTTP request/response stream.
|
/// A resource representing a single HTTP request/response stream.
|
||||||
struct HttpStreamResource {
|
pub struct HttpStreamResource {
|
||||||
conn: Rc<HttpConnResource>,
|
conn: Rc<HttpConnResource>,
|
||||||
rd: AsyncRefCell<HttpRequestReader>,
|
pub rd: AsyncRefCell<HttpRequestReader>,
|
||||||
wr: AsyncRefCell<HttpResponseWriter>,
|
wr: AsyncRefCell<HttpResponseWriter>,
|
||||||
accept_encoding: RefCell<Encoding>,
|
accept_encoding: RefCell<Encoding>,
|
||||||
cancel_handle: CancelHandle,
|
cancel_handle: CancelHandle,
|
||||||
|
@ -324,7 +324,7 @@ impl Resource for HttpStreamResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The read half of an HTTP stream.
|
/// The read half of an HTTP stream.
|
||||||
enum HttpRequestReader {
|
pub enum HttpRequestReader {
|
||||||
Headers(Request<Body>),
|
Headers(Request<Body>),
|
||||||
Body(Peekable<Body>),
|
Body(Peekable<Body>),
|
||||||
Closed,
|
Closed,
|
||||||
|
|
|
@ -127,7 +127,7 @@ impl TlsStream {
|
||||||
Self::new(tcp, Connection::Server(tls))
|
Self::new(tcp, Connection::Server(tls))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_split(self) -> (ReadHalf, WriteHalf) {
|
pub fn into_split(self) -> (ReadHalf, WriteHalf) {
|
||||||
let shared = Shared::new(self);
|
let shared = Shared::new(self);
|
||||||
let rd = ReadHalf {
|
let rd = ReadHalf {
|
||||||
shared: shared.clone(),
|
shared: shared.clone(),
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
function serveHttp(conn) {
|
function serveHttp(conn) {
|
||||||
const rid = core.opSync("op_http_start", conn.rid);
|
const rid = core.opSync("op_http_start", conn.rid);
|
||||||
return new HttpConn(rid);
|
return new HttpConn(rid, conn.remoteAddr, conn.localAddr);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.__bootstrap.http.serveHttp = serveHttp;
|
window.__bootstrap.http.serveHttp = serveHttp;
|
||||||
|
|
|
@ -109,6 +109,7 @@
|
||||||
serveHttp: __bootstrap.http.serveHttp,
|
serveHttp: __bootstrap.http.serveHttp,
|
||||||
resolveDns: __bootstrap.net.resolveDns,
|
resolveDns: __bootstrap.net.resolveDns,
|
||||||
upgradeWebSocket: __bootstrap.http.upgradeWebSocket,
|
upgradeWebSocket: __bootstrap.http.upgradeWebSocket,
|
||||||
|
upgradeHttp: __bootstrap.http.upgradeHttp,
|
||||||
kill: __bootstrap.process.kill,
|
kill: __bootstrap.process.kill,
|
||||||
addSignalListener: __bootstrap.signals.addSignalListener,
|
addSignalListener: __bootstrap.signals.addSignalListener,
|
||||||
removeSignalListener: __bootstrap.signals.removeSignalListener,
|
removeSignalListener: __bootstrap.signals.removeSignalListener,
|
||||||
|
|
|
@ -1,18 +1,28 @@
|
||||||
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use deno_core::error::bad_resource_id;
|
use deno_core::error::bad_resource_id;
|
||||||
|
use deno_core::error::custom_error;
|
||||||
use deno_core::error::AnyError;
|
use deno_core::error::AnyError;
|
||||||
use deno_core::op;
|
use deno_core::op;
|
||||||
use deno_core::Extension;
|
use deno_core::Extension;
|
||||||
use deno_core::OpState;
|
use deno_core::OpState;
|
||||||
|
use deno_core::RcRef;
|
||||||
use deno_core::ResourceId;
|
use deno_core::ResourceId;
|
||||||
|
use deno_core::ZeroCopyBuf;
|
||||||
use deno_http::http_create_conn_resource;
|
use deno_http::http_create_conn_resource;
|
||||||
|
use deno_http::HttpRequestReader;
|
||||||
|
use deno_http::HttpStreamResource;
|
||||||
use deno_net::io::TcpStreamResource;
|
use deno_net::io::TcpStreamResource;
|
||||||
|
use deno_net::ops_tls::TlsStream;
|
||||||
use deno_net::ops_tls::TlsStreamResource;
|
use deno_net::ops_tls::TlsStreamResource;
|
||||||
|
use hyper::upgrade::Parts;
|
||||||
|
use serde::Serialize;
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
|
||||||
pub fn init() -> Extension {
|
pub fn init() -> Extension {
|
||||||
Extension::builder()
|
Extension::builder()
|
||||||
.ops(vec![op_http_start::decl()])
|
.ops(vec![op_http_start::decl(), op_http_upgrade::decl()])
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,3 +72,71 @@ fn op_http_start(
|
||||||
|
|
||||||
Err(bad_resource_id())
|
Err(bad_resource_id())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct HttpUpgradeResult {
|
||||||
|
conn_rid: ResourceId,
|
||||||
|
conn_type: &'static str,
|
||||||
|
read_buf: ZeroCopyBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[op]
|
||||||
|
async fn op_http_upgrade(
|
||||||
|
state: Rc<RefCell<OpState>>,
|
||||||
|
rid: ResourceId,
|
||||||
|
_: (),
|
||||||
|
) -> Result<HttpUpgradeResult, AnyError> {
|
||||||
|
let stream = state
|
||||||
|
.borrow_mut()
|
||||||
|
.resource_table
|
||||||
|
.get::<HttpStreamResource>(rid)?;
|
||||||
|
let mut rd = RcRef::map(&stream, |r| &r.rd).borrow_mut().await;
|
||||||
|
|
||||||
|
let request = match &mut *rd {
|
||||||
|
HttpRequestReader::Headers(request) => request,
|
||||||
|
_ => {
|
||||||
|
return Err(custom_error(
|
||||||
|
"Http",
|
||||||
|
"cannot upgrade because request body was used",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let transport = hyper::upgrade::on(request).await?;
|
||||||
|
let transport = match transport.downcast::<TcpStream>() {
|
||||||
|
Ok(Parts {
|
||||||
|
io: tcp_stream,
|
||||||
|
read_buf,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
return Ok(HttpUpgradeResult {
|
||||||
|
conn_type: "tcp",
|
||||||
|
conn_rid: state
|
||||||
|
.borrow_mut()
|
||||||
|
.resource_table
|
||||||
|
.add(TcpStreamResource::new(tcp_stream.into_split())),
|
||||||
|
read_buf: read_buf.to_vec().into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(transport) => transport,
|
||||||
|
};
|
||||||
|
match transport.downcast::<TlsStream>() {
|
||||||
|
Ok(Parts {
|
||||||
|
io: tls_stream,
|
||||||
|
read_buf,
|
||||||
|
..
|
||||||
|
}) => Ok(HttpUpgradeResult {
|
||||||
|
conn_type: "tls",
|
||||||
|
conn_rid: state
|
||||||
|
.borrow_mut()
|
||||||
|
.resource_table
|
||||||
|
.add(TlsStreamResource::new(tls_stream.into_split())),
|
||||||
|
read_buf: read_buf.to_vec().into(),
|
||||||
|
}),
|
||||||
|
Err(_) => Err(custom_error(
|
||||||
|
"Http",
|
||||||
|
"encountered unsupported transport while upgrading",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue