aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/bun.js/http.exports.js540
-rw-r--r--src/bun.js/javascript.zig18
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"},