Skip to main content

pi/
http_shim.rs

1//! Node.js `http` and `https` shim — pure-JS implementation for the QuickJS
2//! extension runtime.
3//!
4//! Provides `http.request`, `http.get`, `https.request`, `https.get` that route
5//! all HTTP traffic through the capability-gated `pi.http()` hostcall. Uses the
6//! `EventEmitter` from `node:events` for the standard Node.js event-based API.
7
8/// The JS source for the `node:http` virtual module.
9pub const NODE_HTTP_JS: &str = r#"
10import EventEmitter from "node:events";
11
12// ─── STATUS_CODES ────────────────────────────────────────────────────────────
13
14const STATUS_CODES = {
15  200: 'OK', 201: 'Created', 204: 'No Content',
16  301: 'Moved Permanently', 302: 'Found', 304: 'Not Modified',
17  400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden',
18  404: 'Not Found', 405: 'Method Not Allowed', 408: 'Request Timeout',
19  500: 'Internal Server Error', 502: 'Bad Gateway', 503: 'Service Unavailable',
20};
21
22const METHODS = [
23  'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT',
24  'OPTIONS', 'TRACE', 'PATCH',
25];
26
27// ─── IncomingMessage ─────────────────────────────────────────────────────────
28
29class IncomingMessage extends EventEmitter {
30  constructor(statusCode, headers, body) {
31    super();
32    this.statusCode = statusCode;
33    this.statusMessage = STATUS_CODES[statusCode] || 'Unknown';
34    this.headers = headers || {};
35    this._body = body || '';
36    this.complete = false;
37    this.httpVersion = '1.1';
38    this.method = null;
39    this.url = '';
40  }
41
42  _deliver() {
43    if (this._body && this._body.length > 0) {
44      this.emit('data', this._body);
45    }
46    this.complete = true;
47    this.emit('end');
48  }
49
50  setEncoding(_encoding) { return this; }
51  resume() { return this; }
52  pause() { return this; }
53  destroy() { this.emit('close'); }
54}
55
56// ─── ClientRequest ───────────────────────────────────────────────────────────
57
58class ClientRequest extends EventEmitter {
59  constructor(options, callback) {
60    super();
61    this._options = options;
62    this._body = [];
63    this._ended = false;
64    this._aborted = false;
65    this.socket = { remoteAddress: '127.0.0.1', remotePort: 0 };
66    this.method = options.method || 'GET';
67    this.path = options.path || '/';
68
69    if (typeof callback === 'function') {
70      this.once('response', callback);
71    }
72  }
73
74  write(chunk) {
75    if (!this._ended && !this._aborted) {
76      this._body.push(typeof chunk === 'string' ? chunk : String(chunk));
77    }
78    return true;
79  }
80
81  end(chunk, _encoding, callback) {
82    if (typeof chunk === 'function') { callback = chunk; chunk = undefined; }
83    if (typeof _encoding === 'function') { callback = _encoding; }
84    if (chunk) this.write(chunk);
85    if (typeof callback === 'function') this.once('finish', callback);
86
87    this._ended = true;
88    this._send();
89    return this;
90  }
91
92  abort() {
93    this._aborted = true;
94    this.emit('abort');
95    this.destroy();
96  }
97
98  destroy(error) {
99    this._aborted = true;
100    if (error) this.emit('error', error);
101    this.emit('close');
102    return this;
103  }
104
105  setTimeout(ms, callback) {
106    if (typeof callback === 'function') this.once('timeout', callback);
107    this._timeoutMs = ms;
108    return this;
109  }
110
111  setNoDelay() { return this; }
112  setSocketKeepAlive() { return this; }
113  flushHeaders() {}
114  getHeader(_name) { return undefined; }
115  setHeader(_name, _value) { return this; }
116  removeHeader(_name) {}
117
118  _send() {
119    const opts = this._options;
120    const protocol = opts.protocol || 'http:';
121    const hostname = opts.hostname || opts.host || 'localhost';
122    const port = opts.port ? `:${opts.port}` : '';
123    const path = opts.path || '/';
124    const url = `${protocol}//${hostname}${port}${path}`;
125
126    const headers = {};
127    if (opts.headers) {
128      for (const [k, v] of Object.entries(opts.headers)) {
129        headers[k.toLowerCase()] = String(v);
130      }
131    }
132
133    const body = this._body.length > 0 ? this._body.join('') : undefined;
134    const method = (opts.method || 'GET').toUpperCase();
135
136    const request = { url, method, headers };
137    if (body) request.body = body;
138    if (this._timeoutMs) request.timeout = this._timeoutMs;
139
140    // Use pi.http() hostcall if available
141    if (typeof globalThis.pi === 'object' && typeof globalThis.pi.http === 'function') {
142      try {
143        const promise = globalThis.pi.http(request);
144        if (promise && typeof promise.then === 'function') {
145          promise.then(
146            (result) => this._handleResponse(result),
147            (err) => this.emit('error', typeof err === 'string' ? new Error(err) : err)
148          );
149        } else {
150          this._handleResponse(promise);
151        }
152      } catch (err) {
153        this.emit('error', err);
154      }
155    } else {
156      // No pi.http available — emit error
157      this.emit('error', new Error('HTTP requests require pi.http() hostcall'));
158    }
159
160    this.emit('finish');
161  }
162
163  _handleResponse(result) {
164    if (!result || typeof result !== 'object') {
165      this.emit('error', new Error('Invalid HTTP response from hostcall'));
166      return;
167    }
168
169    const statusCode = result.status || result.statusCode || 200;
170    const headers = result.headers || {};
171    const body = result.body || result.data || '';
172
173    const res = new IncomingMessage(statusCode, headers, body);
174    this.emit('response', res);
175    // Deliver body asynchronously (in next microtask)
176    Promise.resolve().then(() => res._deliver());
177  }
178}
179
180// ─── Module API ──────────────────────────────────────────────────────────────
181
182function _parseOptions(input, options) {
183  if (typeof input === 'string') {
184    try {
185      const url = new URL(input);
186      return {
187        protocol: url.protocol,
188        hostname: url.hostname,
189        port: url.port || undefined,
190        path: url.pathname + url.search,
191        ...(options || {}),
192      };
193    } catch (_e) {
194      return { path: input, ...(options || {}) };
195    }
196  }
197  if (input && typeof input === 'object' && !(input instanceof URL)) {
198    return input;
199  }
200  if (input instanceof URL) {
201    return {
202      protocol: input.protocol,
203      hostname: input.hostname,
204      port: input.port || undefined,
205      path: input.pathname + input.search,
206      ...(options || {}),
207    };
208  }
209  return options || {};
210}
211
212export function request(input, optionsOrCallback, callback) {
213  let options;
214  if (typeof optionsOrCallback === 'function') {
215    callback = optionsOrCallback;
216    options = _parseOptions(input);
217  } else {
218    options = _parseOptions(input, optionsOrCallback);
219  }
220  if (!options.protocol) options.protocol = 'http:';
221  return new ClientRequest(options, callback);
222}
223
224export function get(input, optionsOrCallback, callback) {
225  const req = request(input, optionsOrCallback, callback);
226  req.end();
227  return req;
228}
229
230export function createServer() {
231  throw new Error('node:http.createServer is not available in PiJS');
232}
233
234export { STATUS_CODES, METHODS, IncomingMessage, ClientRequest };
235export default { request, get, createServer, STATUS_CODES, METHODS, IncomingMessage, ClientRequest };
236"#;
237
238/// The JS source for the `node:https` virtual module.
239pub const NODE_HTTPS_JS: &str = r#"
240import EventEmitter from "node:events";
241import * as http from "node:http";
242
243export function request(input, optionsOrCallback, callback) {
244  let options;
245  if (typeof optionsOrCallback === 'function') {
246    callback = optionsOrCallback;
247    options = typeof input === 'string' || input instanceof URL
248      ? { ...(typeof input === 'string' ? (() => { try { const u = new URL(input); return { protocol: u.protocol, hostname: u.hostname, port: u.port, path: u.pathname + u.search }; } catch(_) { return { path: input }; } })() : { protocol: input.protocol, hostname: input.hostname, port: input.port, path: input.pathname + input.search }) }
249      : (input || {});
250  } else {
251    options = typeof input === 'string' || input instanceof URL
252      ? { ...(typeof input === 'string' ? (() => { try { const u = new URL(input); return { protocol: u.protocol, hostname: u.hostname, port: u.port, path: u.pathname + u.search }; } catch(_) { return { path: input }; } })() : { protocol: input.protocol, hostname: input.hostname, port: input.port, path: input.pathname + input.search }), ...(optionsOrCallback || {}) }
253      : { ...(input || {}), ...(optionsOrCallback || {}) };
254  }
255  if (!options.protocol) options.protocol = 'https:';
256  return http.request(options, callback);
257}
258
259export function get(input, optionsOrCallback, callback) {
260  const req = request(input, optionsOrCallback, callback);
261  req.end();
262  return req;
263}
264
265export function createServer() {
266  throw new Error('node:https.createServer is not available in PiJS');
267}
268
269export const globalAgent = {};
270
271export default { request, get, createServer, globalAgent };
272"#;