mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-29 12:50:59 +00:00
dev: implement puter.http
This commit is contained in:
@@ -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 });
|
||||
})();
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user