diff options
author | 2022-07-13 10:08:57 -0400 | |
---|---|---|
committer | 2022-07-13 07:08:57 -0700 | |
commit | bf197591882359a567c7f16792fb7185a678d74e (patch) | |
tree | b4e903b65ccfcb50dcb534f56fb011f03e93bea6 /src | |
parent | 0bcd812bd5820888d453b5ade90762391822827a (diff) | |
download | bun-bf197591882359a567c7f16792fb7185a678d74e.tar.gz bun-bf197591882359a567c7f16792fb7185a678d74e.tar.zst bun-bf197591882359a567c7f16792fb7185a678d74e.zip |
add node:http Server polyfill (#572)
* node:http polyfill
* remove @ts-ignore
* reuse emitter instance
* requested changes
* cleanup
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/bun.js/http.exports.js | 540 | ||||
-rw-r--r-- | src/bun.js/javascript.zig | 18 |
2 files changed, 556 insertions, 2 deletions
diff --git a/src/bun.js/http.exports.js b/src/bun.js/http.exports.js new file mode 100644 index 000000000..c8edc9cf5 --- /dev/null +++ b/src/bun.js/http.exports.js @@ -0,0 +1,540 @@ +import { EventEmitter } from "node:events"; +import { Readable, Writable } from "node:stream"; + +export function createServer(options, callback) { + return new Server(options, callback); +} + +export class Server extends EventEmitter { + #server; + #options; + + constructor(options, callback) { + super(); + + if (typeof options === "function") { + callback = options; + options = {}; + } else if (options == null || typeof options === "object") { + options = { ...options }; + } else { + throw new Error("bun-http-polyfill: invalid arguments"); + } + + this.#options = options; + if (callback) this.on("request", callback); + } + + close() { + if (this.#server) { + this.emit("close"); + this.#server.stop(); + this.#server = undefined; + } + } + + listen(...args) { + const server = this; + const [options, listening_cb] = _normalizeArgs(args); + const res_class = this.#options.ServerResponse || ServerResponse; + const req_class = this.#options.IncomingMessage || IncomingMessage; + + try { + this.#server = Bun.serve({ + port: options.port, + hostname: options.host, + + fetch(req) { + return new Promise((reply, reject) => { + const http_req = new req_class(req); + const http_res = new res_class({ reply, req: http_req }); + + http_req.once("error", (err) => reject(err)); + http_res.once("error", (err) => reject(err)); + + server.emit("request", http_req, http_res); + }); + }, + }); + + if (listening_cb) listening_cb(); + } catch (err) { + this.emit( + "error", + new Error(`bun-http-polyfill: Bun.serve failed: ${err.message}`) + ); + } + } +} + +export class IncomingMessage extends Readable { + constructor(req) { + const rawHeaders = []; + const method = req.method; + const headers = Object.create(null); + + for (const key of req.headers.keys()) { + const value = req.headers.get(key); + + headers[key] = value; + rawHeaders.push(key, value); + } + + super(); + + const url = new URL(req.url); + // TODO: reuse trailer object? + // TODO: get hostname and port from Bun.serve and calculate substring() offset + + this._req = req; + this.method = method; + this.complete = false; + this._body_offset = 0; + this.headers = headers; + this._body = undefined; + this._socket = undefined; + this.rawHeaders = rawHeaders; + this.url = url.pathname + url.search; + this._no_body = + "GET" === method || + "HEAD" === method || + "TRACE" === method || + "CONNECT" === method || + "OPTIONS" === method; + } + + _construct(callback) { + // TODO: streaming + (async () => { + if (!this._no_body) + try { + this._body = Buffer.from(await this._req.arrayBuffer()); + + callback(); + } catch (err) { + callback(err); + } + })(); + } + + _read(size) { + if (this._no_body) { + this.push(null); + this.complete = true; + } else { + if (this._body_offset >= this._body.length) { + this.push(null); + this.complete = true; + } else { + this.push( + this._body.subarray(this._body_offset, (this._body_offset += size)) + ); + } + } + } + + get aborted() { + throw new Error("not implemented"); + } + + get connection() { + throw new Error("not implemented"); + } + + get statusCode() { + throw new Error("not implemented"); + } + + get statusMessage() { + throw new Error("not implemented"); + } + + get httpVersion() { + return 1.1; + } + + get rawTrailers() { + return []; + } + + get httpVersionMajor() { + return 1; + } + + get httpVersionMinor() { + return 1; + } + + get trailers() { + return Object.create(null); + } + + get socket() { + if (this._socket) return this._socket; + + this._socket = new EventEmitter(); + this.on("end", () => duplex.emit("end")); + this.on("close", () => duplex.emit("close")); + + return this._socket; + } + + setTimeout(msecs, callback) { + throw new Error("not implemented"); + } +} + +export class ServerResponse extends Writable { + constructor({ req, reply }) { + const headers = new Headers(); + const sink = new Bun.ArrayBufferSink(); + sink.start({ stream: false, asUint8Array: true }); + + super(); + this.req = req; + this._sink = sink; + this._reply = reply; + this.sendDate = true; + this.statusCode = 200; + this._headers = headers; + this.headersSent = false; + this.statusMessage = undefined; + } + + _write(chunk, encoding, callback) { + this.headersSent = true; + this._sink.write(chunk); + + callback(); + } + + _writev(chunks, callback) { + this.headersSent = true; + + for (const chunk of chunks) { + this._sink.write(chunk.chunk); + } + + callback(); + } + + _final(callback) { + callback(); + this.headersSent = true; + + if (this.sendDate && !this._headers.has("date")) { + this._headers.set("date", new Date().toUTCString()); + } + + this._reply( + new Response(this._sink.end(), { + headers: this._headers, + status: this.statusCode, + statusText: this.statusMessage ?? STATUS_CODES[this.statusCode], + }) + ); + } + + get socket() { + throw new Error("not implemented"); + } + + get connection() { + throw new Error("not implemented"); + } + + writeProcessing() { + throw new Error("not implemented"); + } + + addTrailers(headers) { + throw new Error("not implemented"); + } + + assignSocket(socket) { + throw new Error("not implemented"); + } + + detachSocket(socket) { + throw new Error("not implemented"); + } + + writeContinue(callback) { + throw new Error("not implemented"); + } + + setTimeout(msecs, callback) { + throw new Error("not implemented"); + } + + get shouldKeepAlive() { + return true; + } + + get chunkedEncoding() { + return false; + } + + set chunkedEncoding(value) { + // throw new Error('not implemented'); + } + + set shouldKeepAlive(value) { + // throw new Error('not implemented'); + } + + get useChunkedEncodingByDefault() { + return true; + } + + set useChunkedEncodingByDefault(value) { + // throw new Error('not implemented'); + } + + flushHeaders() {} + + removeHeader(name) { + this._headers.delete(name); + } + + getHeader(name) { + return this._headers.get(name); + } + + hasHeader(name) { + return this._headers.has(name); + } + + getHeaderNames() { + return Array.from(this._headers.keys()); + } + + setHeader(name, value) { + this._headers.set(name, value); + + return this; + } + + writeHead(statusCode, statusMessage, headers) { + _writeHead(statusCode, statusMessage, headers, this); + + return this; + } + + getHeaders() { + const headers = Object.create(null); + + for (const key of this._headers.keys()) { + headers[key] = this._headers.get(key); + } + + return headers; + } +} + +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +export const METHODS = [ + "ACL", + "BIND", + "CHECKOUT", + "CONNECT", + "COPY", + "DELETE", + "GET", + "HEAD", + "LINK", + "LOCK", + "M-SEARCH", + "MERGE", + "MKACTIVITY", + "MKCALENDAR", + "MKCOL", + "MOVE", + "NOTIFY", + "OPTIONS", + "PATCH", + "POST", + "PROPFIND", + "PROPPATCH", + "PURGE", + "PUT", + "REBIND", + "REPORT", + "SEARCH", + "SOURCE", + "SUBSCRIBE", + "TRACE", + "UNBIND", + "UNLINK", + "UNLOCK", + "UNSUBSCRIBE", +]; + +export const STATUS_CODES = { + 100: "Continue", + 101: "Switching Protocols", + 102: "Processing", + 103: "Early Hints", + 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", + 417: "Expectation Failed", + 418: "I'm a Teapot", + 421: "Misdirected Request", + 422: "Unprocessable Entity", + 423: "Locked", + 424: "Failed Dependency", + 425: "Too Early", + 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", + 509: "Bandwidth Limit Exceeded", + 510: "Not Extended", + 511: "Network Authentication Required", +}; + +function _normalizeArgs(args) { + let arr; + + if (args.length === 0) { + arr = [{}, null]; + // arr[normalizedArgsSymbol] = true; + return arr; + } + + const arg0 = args[0]; + let options = {}; + if (typeof arg0 === "object" && arg0 !== null) { + // (options[...][, cb]) + options = arg0; + // } else if (isPipeName(arg0)) { + // (path[...][, cb]) + // options.path = arg0; + } else { + // ([port][, host][...][, cb]) + options.port = arg0; + if (args.length > 1 && typeof args[1] === "string") { + options.host = args[1]; + } + } + + const cb = args[args.length - 1]; + if (typeof cb !== "function") arr = [options, null]; + else arr = [options, cb]; + + // arr[normalizedArgsSymbol] = true; + return arr; +} + +function _writeHead(statusCode, reason, obj, response) { + statusCode |= 0; + if (statusCode < 100 || statusCode > 999) { + throw new Error("status code must be between 100 and 999"); + } + + if (typeof reason === "string") { + // writeHead(statusCode, reasonPhrase[, headers]) + response.statusMessage = reason; + } else { + // writeHead(statusCode[, headers]) + if (!response.statusMessage) + response.statusMessage = STATUS_CODES[statusCode] || "unknown"; + obj = reason; + } + response.statusCode = statusCode; + + { + // Slow-case: when progressive API and header fields are passed. + let k; + if (Array.isArray(obj)) { + if (obj.length % 2 !== 0) { + throw new Error("raw headers must have an even number of elements"); + } + + for (let n = 0; n < obj.length; n += 2) { + k = obj[n + 0]; + if (k) response.setHeader(k, obj[n + 1]); + } + } else if (obj) { + const keys = Object.keys(obj); + // Retain for(;;) loop for performance reasons + // Refs: https://github.com/nodejs/node/pull/30958 + for (let i = 0; i < keys.length; i++) { + k = keys[i]; + if (k) response.setHeader(k, obj[k]); + } + } + } +} + +export default { + Server, + METHODS, + STATUS_CODES, + createServer, + ServerResponse, + IncomingMessage, +}; diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 8ee586024..e084f01b2 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -956,6 +956,17 @@ pub const VirtualMachine = struct { .hash = 0, }; }, + .@"node:http" => { + return ResolvedSource{ + .allocator = null, + .source_code = ZigString.init( + @as(string, @embedFile("./http.exports.js")), + ), + .specifier = ZigString.init("node:http"), + .source_url = ZigString.init("node:http"), + .hash = 0, + }; + }, .@"depd" => { return ResolvedSource{ .allocator = null, @@ -2724,6 +2735,7 @@ pub const HardcodedModule = enum { @"depd", @"detect-libc", @"node:fs", + @"node:http", @"node:fs/promises", @"node:module", @"node:path", @@ -2747,8 +2759,10 @@ pub const HardcodedModule = enum { .{ "detect-libc", HardcodedModule.@"detect-libc" }, .{ "ffi", HardcodedModule.@"bun:ffi" }, .{ "fs", HardcodedModule.@"node:fs" }, + .{ "http", HardcodedModule.@"node:http" }, .{ "module", HardcodedModule.@"node:module" }, .{ "node:fs", HardcodedModule.@"node:fs" }, + .{ "node:http", HardcodedModule.@"node:http" }, .{ "node:fs/promises", HardcodedModule.@"node:fs/promises" }, .{ "node:module", HardcodedModule.@"node:module" }, .{ "node:path", HardcodedModule.@"node:path" }, @@ -2778,9 +2792,11 @@ pub const HardcodedModule = enum { .{ "detect-libc/lib/detect-libc.js", "detect-libc" }, .{ "ffi", "bun:ffi" }, .{ "fs", "node:fs" }, + .{ "http", "node:http" }, .{ "fs/promises", "node:fs/promises" }, .{ "module", "node:module" }, .{ "node:fs", "node:fs" }, + .{ "node:http", "node:http" }, .{ "node:fs/promises", "node:fs/promises" }, .{ "node:module", "node:module" }, .{ "node:path", "node:path" }, @@ -2812,10 +2828,8 @@ pub const DisabledModule = bun.ComptimeStringMap( void, .{ .{"child_process"}, - .{"http"}, .{"https"}, .{"node:child_process"}, - .{"node:http"}, .{"node:https"}, .{"node:tls"}, .{"node:worker_threads"}, |