dev: implement puter.http

This commit is contained in:
KernelDeimos
2025-01-22 16:16:36 -05:00
parent 6f39365b24
commit 2b505caf98
3 changed files with 167 additions and 0 deletions
+12
View File
@@ -22,6 +22,7 @@ import { Debug } from './modules/Debug.js';
import { PSocket, wispInfo } from './modules/networking/PSocket.js';
import { PTLSSocket } from "./modules/networking/PTLS.js"
import { PWispHandler } from './modules/networking/PWispHandler.js';
import { make_http_api } from './lib/http.js';
// TODO: This is for a safe-guard below; we should check if we can
// generalize this behavior rather than hard-coding it.
@@ -320,7 +321,12 @@ window.puter = (function() {
await this.services.wait_for_init(['api-access']);
this.p_can_request_rao_.resolve();
})();
// TODO: This should be separated into modules called "Net" and "Http".
// Modules need to be refactored first because right now they
// are too tightly-coupled with authentication state.
(async () => {
// === puter.net ===
const { token: wispToken, server: wispServer } = (await (await fetch(this.APIOrigin + '/wisp/relay-token/create', {
method: 'POST',
headers: {
@@ -336,6 +342,12 @@ window.puter = (function() {
TLSSocket: PTLSSocket
}
}
// === puter.http ===
this.http = make_http_api(
{ Socket: this.net.Socket, DEFAULT_PORT: 80 });
this.https = make_http_api(
{ Socket: this.net.tls.TLSSocket, DEFAULT_PORT: 443 });
})();
+2
View File
@@ -33,6 +33,7 @@ export default class EventListener {
return;
}
this.#eventListeners[eventName].push(callback);
return this;
}
off(eventName, callback) {
@@ -45,5 +46,6 @@ export default class EventListener {
if (index !== -1) {
listeners.splice(index, 1);
}
return this;
}
}
+153
View File
@@ -0,0 +1,153 @@
import EventListener from "./EventListener";
// TODO: this inheritance is an anti-pattern; we should use
// a trait or mixin for event emitters.
export class HTTPRequest extends EventListener {
constructor ({ options, callback }) {
super(['data','end','error']);
this.options = options;
this.callback = callback;
}
end () {
//
}
}
export const make_http_api = ({ Socket, DEFAULT_PORT }) => {
// Helper to create an EventEmitter-like object
const api = {};
api.request = (options, callback) => {
const sock = new Socket(options.hostname, options.port ?? DEFAULT_PORT);
const encoder = new TextEncoder();
const decoder = new TextDecoder();
// Request object
const req = new HTTPRequest([
'data',
'end',
'error',
]);
// Response object
const res = new EventListener([
'data',
'end',
'error',
]);
res.headers = {};
res.statusCode = null;
res.statusMessage = '';
let buffer = '';
let amount = 0;
const TRANSFER_CONTENT_LENGTH = {
data: data => {
const contentLength = parseInt(res.headers['content-length'], 10);
if ( buffer ) {
const bin = encoder.encode(buffer);
data = new Uint8Array([...bin, ...data]);
buffer = '';
}
amount += data.length;
res.emit('data', decoder.decode(data));
if (amount >= contentLength) {
sock.close();
}
}
};
const TRANSFER_CHUNKED = {
data: data => {
// TODO
throw new Error('Chunked transfer encoding not implemented');
}
};
let transfer = null;
const STATE_HEADERS = {
data: data => {
data = decoder.decode(data);
buffer += data;
const headerEndIndex = buffer.indexOf('\r\n\r\n');
if ( headerEndIndex === -1 ) return;
// Parse headers
const headersString = buffer.substring(0, headerEndIndex);
const headerLines = headersString.split('\r\n');
// Remove headers from buffer
buffer = buffer.substring(headerEndIndex + 4);
// Parse status line
const [httpVersion, statusCode, ...statusMessageParts] = headerLines[0].split(' ');
res.statusCode = parseInt(statusCode, 10);
res.statusMessage = statusMessageParts.join(' ');
// Parse headers
for (let i = 1; i < headerLines.length; i++) {
const [key, ...valueParts] = headerLines[i].split(':');
if (key) {
res.headers[key.toLowerCase().trim()] = valueParts.join(':').trim();
}
}
if ( res.headers['transfer-encoding'] === 'chunked' ) {
transfer = TRANSFER_CHUNKED;
} else if ( res.headers['transfer-encoding'] ) {
throw new Error('Unsupported transfer encoding');
} else if ( res.headers['content-length'] ) {
transfer = TRANSFER_CONTENT_LENGTH;
} else {
throw new Error('No content length or transfer encoding');
}
state = STATE_BODY;
callback(res);
}
};
const STATE_BODY = {
data: data => {
transfer.data(data);
}
};
let state = STATE_HEADERS;
sock.on('data', (data) => {
state.data(data);
});
sock.on('error', (err) => {
req.emit('error', err);
});
sock.on('close', () => {
res.emit('end');
});
// Construct and send HTTP request
const method = options.method || 'GET';
const path = options.path || '/';
const headers = options.headers || {};
headers['Host'] = options.hostname;
let requestString = `${method} ${path} HTTP/1.1\r\n`;
for (const [key, value] of Object.entries(headers)) {
requestString += `${key}: ${value}\r\n`;
}
requestString += '\r\n';
if (options.data) {
requestString += options.data;
}
sock.write(encoder.encode(requestString));
return req;
};
return api;
};