Skip to content

Commit

Permalink
dev: implement puter.http
Browse files Browse the repository at this point in the history
  • Loading branch information
KernelDeimos committed Jan 22, 2025
1 parent 6f39365 commit 2b505ca
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 0 deletions.
12 changes: 12 additions & 0 deletions src/puter-js/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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: {
Expand All @@ -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 });
})();


Expand Down
2 changes: 2 additions & 0 deletions src/puter-js/src/lib/EventListener.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default class EventListener {
return;
}
this.#eventListeners[eventName].push(callback);
return this;
}

off(eventName, callback) {
Expand All @@ -45,5 +46,6 @@ export default class EventListener {
if (index !== -1) {
listeners.splice(index, 1);
}
return this;
}
}
153 changes: 153 additions & 0 deletions src/puter-js/src/lib/http.js
Original file line number Diff line number Diff line change
@@ -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;
};

0 comments on commit 2b505ca

Please sign in to comment.