deno.land / x / deno@v1.28.2 / ext / flash / 01_http.js
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license."use strict";
((window) => { const { BlobPrototype } = window.__bootstrap.file; const { TcpConn } = window.__bootstrap.net; const { fromFlashRequest, toInnerResponse, _flash } = window.__bootstrap.fetch; const core = window.Deno.core; const { Event } = window.__bootstrap.event; const { ReadableStream, ReadableStreamPrototype, getReadableStreamResourceBacking, readableStreamClose, _state, } = window.__bootstrap.streams; const { WebSocket, _rid, _readyState, _eventLoop, _protocol, _idleTimeoutDuration, _idleTimeoutTimeout, _serverHandleIdleTimeout, } = window.__bootstrap.webSocket; const { _ws } = window.__bootstrap.http; const { Function, ObjectPrototypeIsPrototypeOf, Promise, PromisePrototypeCatch, PromisePrototypeThen, SafePromiseAll, TypedArrayPrototypeSubarray, TypeError, Uint8Array, Uint8ArrayPrototype, } = window.__bootstrap.primordials;
const statusCodes = { 100: "Continue", 101: "Switching Protocols", 102: "Processing", 200: "OK", 201: "Created", 202: "Accepted", 203: "Non Authoritative Information", 204: "No Content", 205: "Reset Content", 206: "Partial Content", 207: "Multi-Status", 208: "Already Reported", 226: "IM Used", 300: "Multiple Choices", 301: "Moved Permanently", 302: "Found", 303: "See Other", 304: "Not Modified", 305: "Use Proxy", 307: "Temporary Redirect", 308: "Permanent Redirect", 400: "Bad Request", 401: "Unauthorized", 402: "Payment Required", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed", 406: "Not Acceptable", 407: "Proxy Authentication Required", 408: "Request Timeout", 409: "Conflict", 410: "Gone", 411: "Length Required", 412: "Precondition Failed", 413: "Payload Too Large", 414: "URI Too Long", 415: "Unsupported Media Type", 416: "Range Not Satisfiable", 418: "I'm a teapot", 421: "Misdirected Request", 422: "Unprocessable Entity", 423: "Locked", 424: "Failed Dependency", 426: "Upgrade Required", 428: "Precondition Required", 429: "Too Many Requests", 431: "Request Header Fields Too Large", 451: "Unavailable For Legal Reasons", 500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout", 505: "HTTP Version Not Supported", 506: "Variant Also Negotiates", 507: "Insufficient Storage", 508: "Loop Detected", 510: "Not Extended", 511: "Network Authentication Required", };
const methods = { 0: "GET", 1: "HEAD", 2: "CONNECT", 3: "PUT", 4: "DELETE", 5: "OPTIONS", 6: "TRACE", 7: "POST", 8: "PATCH", };
let dateInterval; let date;
// Construct an HTTP response message. // All HTTP/1.1 messages consist of a start-line followed by a sequence // of octets. // // HTTP-message = start-line // *( header-field CRLF ) // CRLF // [ message-body ] // function http1Response( method, status, headerList, body, bodyLen, earlyEnd = false, ) { // HTTP uses a "<major>.<minor>" numbering scheme // HTTP-version = HTTP-name "/" DIGIT "." DIGIT // HTTP-name = %x48.54.54.50 ; "HTTP", case-sensitive // // status-line = HTTP-version SP status-code SP reason-phrase CRLF // Date header: https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.2 let str = `HTTP/1.1 ${status} ${statusCodes[status]}\r\nDate: ${date}\r\n`; for (const [name, value] of headerList) { // header-field = field-name ":" OWS field-value OWS str += `${name}: ${value}\r\n`; }
// https://datatracker.ietf.org/doc/html/rfc7231#section-6.3.6 if (status === 205 || status === 304) { // MUST NOT generate a payload in a 205 response. // indicate a zero-length body for the response by // including a Content-Length header field with a value of 0. str += "Content-Length: 0\r\n\r\n"; return str; }
// MUST NOT send Content-Length or Transfer-Encoding if status code is 1xx or 204. if (status === 204 || status < 200) { str += "\r\n"; return str; }
if (earlyEnd === true) { return str; }
// null body status is validated by inititalizeAResponse in ext/fetch if (body !== null && body !== undefined) { str += `Content-Length: ${bodyLen}\r\n\r\n`; } else { str += "Transfer-Encoding: chunked\r\n\r\n"; return str; }
// A HEAD request. if (method === 1) return str;
if (typeof body === "string") { str += body ?? ""; } else { const head = core.encode(str); const response = new Uint8Array(head.byteLength + body.byteLength); response.set(head, 0); response.set(body, head.byteLength); return response; }
return str; }
function prepareFastCalls(serverId) { return core.ops.op_flash_make_request(serverId); }
function hostnameForDisplay(hostname) { // If the hostname is "0.0.0.0", we display "localhost" in console // because browsers in Windows don't resolve "0.0.0.0". // See the discussion in https://github.com/denoland/deno_std/issues/1165 return hostname === "0.0.0.0" ? "localhost" : hostname; }
function writeFixedResponse( server, requestId, response, responseLen, end, respondFast, ) { let nwritten = 0; // TypedArray if (typeof response !== "string") { nwritten = respondFast(requestId, response, end); } else { // string nwritten = core.ops.op_flash_respond( server, requestId, response, end, ); }
if (nwritten < responseLen) { core.opAsync( "op_flash_respond_async", server, requestId, response.slice(nwritten), end, ); } }
// TODO(@littledivy): Woah woah, cut down the number of arguments. async function handleResponse( req, resp, body, hasBody, method, serverId, i, respondFast, respondChunked, tryRespondChunked, ) { // there might've been an HTTP upgrade. if (resp === undefined) { return; } const innerResp = toInnerResponse(resp); // If response body length is known, it will be sent synchronously in a // single op, in other case a "response body" resource will be created and // we'll be streaming it. /** @type {ReadableStream<Uint8Array> | Uint8Array | null} */ let respBody = null; let isStreamingResponseBody = false; if (innerResp.body !== null) { if (typeof innerResp.body.streamOrStatic?.body === "string") { if (innerResp.body.streamOrStatic.consumed === true) { throw new TypeError("Body is unusable."); } innerResp.body.streamOrStatic.consumed = true; respBody = innerResp.body.streamOrStatic.body; isStreamingResponseBody = false; } else if ( ObjectPrototypeIsPrototypeOf( ReadableStreamPrototype, innerResp.body.streamOrStatic, ) ) { if (innerResp.body.unusable()) { throw new TypeError("Body is unusable."); } if ( innerResp.body.length === null || ObjectPrototypeIsPrototypeOf( BlobPrototype, innerResp.body.source, ) ) { respBody = innerResp.body.stream; } else { const reader = innerResp.body.stream.getReader(); const r1 = await reader.read(); if (r1.done) { respBody = new Uint8Array(0); } else { respBody = r1.value; const r2 = await reader.read(); if (!r2.done) throw new TypeError("Unreachable"); } } isStreamingResponseBody = !( typeof respBody === "string" || ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, respBody) ); } else { if (innerResp.body.streamOrStatic.consumed === true) { throw new TypeError("Body is unusable."); } innerResp.body.streamOrStatic.consumed = true; respBody = innerResp.body.streamOrStatic.body; } } else { respBody = new Uint8Array(0); }
const ws = resp[_ws]; if (isStreamingResponseBody === false) { const length = respBody.byteLength || core.byteLength(respBody); const responseStr = http1Response( method, innerResp.status ?? 200, innerResp.headerList, respBody, length, ); writeFixedResponse( serverId, i, responseStr, length, !ws, // Don't close socket if there is a deferred websocket upgrade. respondFast, ); }
(async () => { if (!ws) { if (hasBody && body[_state] !== "closed") { // TODO(@littledivy): Optimize by draining in a single op. try { await req.arrayBuffer(); } catch { /* pass */ } } }
if (isStreamingResponseBody === true) { const resourceBacking = getReadableStreamResourceBacking(respBody); if (resourceBacking) { if (respBody.locked) { throw new TypeError("ReadableStream is locked."); } const reader = respBody.getReader(); // Aquire JS lock. try { PromisePrototypeThen( core.opAsync( "op_flash_write_resource", http1Response( method, innerResp.status ?? 200, innerResp.headerList, 0, // Content-Length will be set by the op. null, true, ), serverId, i, resourceBacking.rid, resourceBacking.autoClose, ), () => { // Release JS lock. readableStreamClose(respBody); }, ); } catch (error) { await reader.cancel(error); throw error; } } else { const reader = respBody.getReader();
// Best case: sends headers + first chunk in a single go. const { value, done } = await reader.read(); writeFixedResponse( serverId, i, http1Response( method, innerResp.status ?? 200, innerResp.headerList, respBody.byteLength, null, ), respBody.byteLength, false, respondFast, );
await tryRespondChunked( i, value, done, );
if (!done) { while (true) { const chunk = await reader.read(); await respondChunked( i, chunk.value, chunk.done, ); if (chunk.done) break; } } } }
if (ws) { const wsRid = await core.opAsync( "op_flash_upgrade_websocket", serverId, i, ); ws[_rid] = wsRid; ws[_protocol] = resp.headers.get("sec-websocket-protocol");
ws[_readyState] = WebSocket.OPEN; const event = new Event("open"); ws.dispatchEvent(event);
ws[_eventLoop](); if (ws[_idleTimeoutDuration]) { ws.addEventListener( "close", () => clearTimeout(ws[_idleTimeoutTimeout]), ); } ws[_serverHandleIdleTimeout](); } })(); }
function createServe(opFn) { return async function serve(arg1, arg2) { let options = undefined; let handler = undefined; if (arg1 instanceof Function) { handler = arg1; options = arg2; } else if (arg2 instanceof Function) { handler = arg2; options = arg1; } else { options = arg1; } if (handler === undefined) { if (options === undefined) { throw new TypeError( "No handler was provided, so an options bag is mandatory.", ); } handler = options.handler; } if (!(handler instanceof Function)) { throw new TypeError("A handler function must be provided."); } if (options === undefined) { options = {}; }
const signal = options.signal;
const onError = options.onError ?? function (error) { console.error(error); return new Response("Internal Server Error", { status: 500 }); };
const onListen = options.onListen ?? function ({ port }) { console.log( `Listening on http://${ hostnameForDisplay(listenOpts.hostname) }:${port}/`, ); };
const listenOpts = { hostname: options.hostname ?? "127.0.0.1", port: options.port ?? 9000, reuseport: options.reusePort ?? false, }; if (options.cert || options.key) { if (!options.cert || !options.key) { throw new TypeError( "Both cert and key must be provided to enable HTTPS.", ); } listenOpts.cert = options.cert; listenOpts.key = options.key; }
const serverId = opFn(listenOpts); const serverPromise = core.opAsync("op_flash_drive_server", serverId); const listenPromise = PromisePrototypeThen( core.opAsync("op_flash_wait_for_listening", serverId), (port) => { onListen({ hostname: listenOpts.hostname, port }); }, ); const finishedPromise = PromisePrototypeCatch(serverPromise, () => {});
const server = { id: serverId, transport: listenOpts.cert && listenOpts.key ? "https" : "http", hostname: listenOpts.hostname, port: listenOpts.port, closed: false, finished: finishedPromise, async close() { if (server.closed) { return; } server.closed = true; core.ops.op_flash_close_server(serverId); await server.finished; }, async serve() { let offset = 0; while (true) { if (server.closed) { break; }
let tokens = nextRequestSync(); if (tokens === 0) { tokens = await core.opAsync("op_flash_next_async", serverId); if (server.closed) { break; } }
for (let i = offset; i < offset + tokens; i++) { let body = null; // There might be a body, but we don't expose it for GET/HEAD requests. // It will be closed automatically once the request has been handled and // the response has been sent. const method = getMethodSync(i); let hasBody = method > 2; // Not GET/HEAD/CONNECT if (hasBody) { body = createRequestBodyStream(serverId, i); if (body === null) { hasBody = false; } }
const req = fromFlashRequest( serverId, /* streamRid */ i, body, /* methodCb */ () => methods[method], /* urlCb */ () => { const path = core.ops.op_flash_path(serverId, i); return `${server.transport}://${server.hostname}:${server.port}${path}`; }, /* headersCb */ () => core.ops.op_flash_headers(serverId, i), );
let resp; try { resp = handler(req); if (resp instanceof Promise) { PromisePrototypeCatch( PromisePrototypeThen( resp, (resp) => handleResponse( req, resp, body, hasBody, method, serverId, i, respondFast, respondChunked, tryRespondChunked, ), ), onError, ); continue; } else if (typeof resp?.then === "function") { resp.then((resp) => handleResponse( req, resp, body, hasBody, method, serverId, i, respondFast, respondChunked, tryRespondChunked, ) ).catch(onError); continue; } } catch (e) { resp = await onError(e); }
handleResponse( req, resp, body, hasBody, method, serverId, i, respondFast, respondChunked, tryRespondChunked, ); }
offset += tokens; } await server.finished; }, };
signal?.addEventListener("abort", () => { clearInterval(dateInterval); server.close(); }, { once: true, });
function tryRespondChunked(token, chunk, shutdown) { const nwritten = core.ops.op_try_flash_respond_chuncked( serverId, token, chunk ?? new Uint8Array(), shutdown, ); if (nwritten > 0) { return core.opAsync( "op_flash_respond_chuncked", serverId, token, chunk, shutdown, nwritten, ); } }
function respondChunked(token, chunk, shutdown) { return core.opAsync( "op_flash_respond_chuncked", serverId, token, chunk, shutdown, ); }
const fastOp = prepareFastCalls(serverId); let nextRequestSync = () => fastOp.nextRequest(); let getMethodSync = (token) => fastOp.getMethod(token); let respondFast = (token, response, shutdown) => fastOp.respond(token, response, shutdown); if (serverId > 0) { nextRequestSync = () => core.ops.op_flash_next_server(serverId); getMethodSync = (token) => core.ops.op_flash_method(serverId, token); respondFast = (token, response, shutdown) => core.ops.op_flash_respond(serverId, token, response, null, shutdown); }
if (!dateInterval) { date = new Date().toUTCString(); dateInterval = setInterval(() => { date = new Date().toUTCString(); }, 1000); }
await SafePromiseAll([ listenPromise, PromisePrototypeCatch(server.serve(), console.error), ]); }; }
function createRequestBodyStream(serverId, token) { // The first packet is left over bytes after parsing the request const firstRead = core.ops.op_flash_first_packet( serverId, token, ); if (!firstRead) return null; let firstEnqueued = firstRead.byteLength == 0;
return new ReadableStream({ type: "bytes", async pull(controller) { try { if (firstEnqueued === false) { controller.enqueue(firstRead); firstEnqueued = true; return; } // This is the largest possible size for a single packet on a TLS // stream. const chunk = new Uint8Array(16 * 1024 + 256); const read = await core.opAsync( "op_flash_read_body", serverId, token, chunk, ); if (read > 0) { // We read some data. Enqueue it onto the stream. controller.enqueue(TypedArrayPrototypeSubarray(chunk, 0, read)); } else { // We have reached the end of the body, so we close the stream. controller.close(); } } catch (err) { // There was an error while reading a chunk of the body, so we // error. controller.error(err); controller.close(); } }, }); }
function upgradeHttpRaw(req) { if (!req[_flash]) { throw new TypeError( "Non-flash requests can not be upgraded with `upgradeHttpRaw`. Use `upgradeHttp` instead.", ); }
// NOTE(bartlomieju): // Access these fields so they are cached on `req` object, otherwise // they wouldn't be available after the connection gets upgraded. req.url; req.method; req.headers;
const { serverId, streamRid } = req[_flash]; const connRid = core.ops.op_flash_upgrade_http(streamRid, serverId); // TODO(@littledivy): return already read first packet too. return [new TcpConn(connRid), new Uint8Array()]; }
window.__bootstrap.flash = { createServe, upgradeHttpRaw, };})(this);
Version Info