diff --git a/src/puter-js/src/index.js b/src/puter-js/src/index.js index 452b0d561..937b177a2 100644 --- a/src/puter-js/src/index.js +++ b/src/puter-js/src/index.js @@ -19,6 +19,8 @@ import { APIAccessService } from './services/APIAccess.js'; import { XDIncomingService } from './services/XDIncoming.js'; import { NoPuterYetService } from './services/NoPuterYet.js'; import { Debug } from './modules/Debug.js'; +import { PSocket, wispInfo } from './modules/networking/PSocket.js'; +import { PWispHandler } from './modules/networking/PWispHandler.js'; // TODO: This is for a safe-guard below; we should check if we can // generalize this behavior rather than hard-coding it. @@ -317,6 +319,22 @@ window.puter = (function() { await this.services.wait_for_init(['api-access']); this.p_can_request_rao_.resolve(); })(); + (async () => { + const wispToken = (await (await fetch('https://api.puter.com/wisp/relay-token/create', { + method: 'POST', + headers: { + Authorization: `Bearer ${this.authToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), + })).json())["token"]; + wispInfo.handler = new PWispHandler(wispInfo.server, wispToken); + this.net = { + Socket: PSocket + } + })(); + + } /** diff --git a/src/puter-js/src/modules/networking/PSocket.js b/src/puter-js/src/modules/networking/PSocket.js new file mode 100644 index 000000000..905409b36 --- /dev/null +++ b/src/puter-js/src/modules/networking/PSocket.js @@ -0,0 +1,44 @@ +import EventListener from "../../lib/EventListener.js"; +import {PWispHandler} from "./PWispHandler.js" + + +export let wispInfo = { + server: "wss://puter.cafe/", + handler: undefined +}; + +export class PSocket extends EventListener { + _events = new Map(); + _streamID; + constructor(host, port) { + super(["data", "drain", "open", "close"]); + const callbacks = { + dataCallBack: (data) => { + this.emit("data", data); + }, + closeCallBack: (reason) => { + this.emit("close", false); // TODO, report errors + }, + openCallBack: () => { + this.emit("open"); + } + } + + this._streamID = wispInfo.handler.register(host, port, callbacks); + + } + addListener(...args) { + this.on(...args); + } + write(data, callback) { + if (data.buffer) { // typedArray + wispInfo.handler.write(this._streamID, data); + if (callback) callback(); + } else if (data.resize) { + data.write(this._streamID, new Uint8Array(data)); + if (callback) callback(); + } else if (data.arrayBuffer) { // Oh No, a blob, I need to handle this later, maybe with https://gist.github.com/jimmywarting/65c358f878cac8e7f39cfb7d43931f62? + + } + } +} \ No newline at end of file diff --git a/src/puter-js/src/modules/networking/PWispHandler.js b/src/puter-js/src/modules/networking/PWispHandler.js new file mode 100644 index 000000000..f5e3f9d54 --- /dev/null +++ b/src/puter-js/src/modules/networking/PWispHandler.js @@ -0,0 +1,79 @@ +import {CLOSE, CONNECT, DATA, CONTINUE, INFO, TCP, UDP, createWispPacket, parseIncomingPacket, textde} from "./parsers.js" + +export class PWispHandler { + _ws; + _nextStreamID = 1; + _bufferMax; + streamMap = new Map(); + constructor(wispURL, puterAuth) { + this._ws = new WebSocket(wispURL); + this._ws.binaryType = "arraybuffer" + this._ws.onmessage = (event) => { + const parsed = parseIncomingPacket(new Uint8Array(event.data)); + switch (parsed.packetType) { + case DATA: + this.streamMap.get(parsed.streamID).dataCallBack(parsed.payload.slice(0)) // return a copy for the user to do as they please + break; + case CONTINUE: + if (parsed.streamID === 0) { + this._bufferMax = parsed.remainingBuffer; + return; + } + this.streamMap.get(parsed.streamID).buffer = parsed.remainingBuffer; + this._continue() + break; + case CLOSE: + this.streamMap.get(parsed.streamID).closeCallBack(parsed.reason); + break; + case INFO: + puterAuth && this._ws.send(createWispPacket({ + packetType: INFO, + streamID: 0, + puterAuth + })) + break; + } + } + } + _continue(streamID) { + const queue = this.streamMap.get(streamID).queue; + for (let i = 0; i < queue.length; i++) { + this.write(streamID, queue.shift()); + } + } + register(host, port, callbacks) { + const streamID = this._nextStreamID++; + this.streamMap.set(streamID, {queue: [], streamID, buffer: this._bufferMax, dataCallBack: callbacks.dataCallBack, closeCallBack: callbacks.closeCallBack, openCallBack: callbacks.openCallBack}); + this._ws.send(createWispPacket({ + packetType: CONNECT, + streamType: TCP, + streamID: streamID, + hostname: host, + port: port + })) + + return streamID; + } + + write(streamID, data) { + const streamData = this.streamMap.get(streamID); + if (streamData.buffer > 0) { + streamData.buffer--; + + this._ws.send(createWispPacket({ + packetType: DATA, + streamID: streamID, + payload: data + })) + } else { + streamData.queue.push(data) + } + } + close(streamID) { + this._ws.send(createWispPacket({ + packetType: CLOSE, + streamID: streamID, + reason: 0x02 + })) + } +} \ No newline at end of file diff --git a/src/puter-js/src/modules/networking/parsers.js b/src/puter-js/src/modules/networking/parsers.js new file mode 100644 index 000000000..288fac4ee --- /dev/null +++ b/src/puter-js/src/modules/networking/parsers.js @@ -0,0 +1,147 @@ +/* eslint-disable no-unreachable */ +/* eslint-disable no-case-declarations */ +// PACKET TYPES +export const CONNECT = 0x01; +export const DATA = 0x02; +export const CONTINUE = 0x03; +export const CLOSE = 0x04; +export const INFO = 0x05; + +// STREAM TYPES +export const TCP = 0x01; +export const UDP = 0x02; + +// Frequently used objects +export const textde = new TextDecoder(); +const texten = new TextEncoder(); + +/** + * @typedef {{packetType: number, streamID: number, streamType?: number, port?: number, hostname?: string, payload?: Uint8Array, reason?: number, remainingBuffer?: number}} ParsedWispPacket + */ + +/** + * Parses a wisp packet fully + * + * @param {Uint8Array} data + * @returns {ParsedWispPacket} Packet Info + */ + +export function parseIncomingPacket(data) { + const view = new DataView(data.buffer, data.byteOffset); + const packetType = view.getUint8(0); + const streamID = view.getUint32(1, true); + switch (packetType) { // Packet payload starts at Offset 5 + case CONNECT: + const streamType = view.getUint8(5); + const port = view.getUint16(6, true); + const hostname = textde.decode(data.subarray(8, data.length)); + return {packetType, streamID, streamType, port, hostname} + break; + case DATA: + const payload = data.subarray(5, data.length); + return {packetType, streamID, payload} + break; + case CONTINUE: + const remainingBuffer = view.getUint32(5, true); + return {packetType, streamID, remainingBuffer} + break; + case CLOSE: + const reason = view.getUint8(5) + return {packetType, streamID, reason} + break; + case INFO: + const infoObj = {}; + infoObj["version_major"] = view.getUint8(5); + infoObj["version_minor"] = view.getUint8(6); + + let ptr = 7; + while (ptr < data.length) { + const extType = view.getUint8(ptr); + const extLength = view.getUint32(ptr + 1, true); + const payload = data.subarray(ptr + 5, ptr + 5 + extLength); + infoObj[extType] = payload; + ptr += 5 + extLength; + } + return {packetType, streamID, infoObj} + break; + } +} +/** + * creates a wisp packet fully + * + * @param {ParsedWispPacket} instructions + * @returns {Uint8Array} Constructed Packet + */ + +export function createWispPacket(instructions) { + let size = 5; + switch (instructions.packetType) { // Pass 1: determine size of packet + case CONNECT: + instructions.hostEncoded = texten.encode(instructions.hostname) + size += 3 + instructions.hostEncoded.length; + break; + case DATA: + size += instructions.payload.byteLength; + break; + case CONTINUE: + size += 4; + break; + case CLOSE: + size += 1; + break; + case INFO: + size += 2; + if (instructions.password) + size += 6; + if (instructions.puterAuth) { + instructions.passwordEncoded = texten.encode(instructions.puterAuth); + size += 8 + instructions.passwordEncoded.length; + } + break; + default: + throw new Error("Not supported") + } + + let data = new Uint8Array(size); + const view = new DataView(data.buffer); + view.setUint8(0, instructions.packetType); + view.setUint32(1, instructions.streamID, true); + switch (instructions.packetType) { // Pass 2: fill out packet + case CONNECT: + view.setUint8(5, instructions.streamType); + view.setUint16(6, instructions.port, true); + data.set(instructions.hostEncoded, 8); + break; + case DATA: + data.set(instructions.payload, 5); + break; + case CONTINUE: + view.setUint32(5, instructions.remainingBuffer, true) + break; + case CLOSE: + view.setUint8(5, instructions.reason) + break; + case INFO: + // WISP 2.0 + view.setUint8(5, 2); + view.setUint8(6, 0); + + if (instructions.password) { + // PASSWORD AUTH REQUIRED + view.setUint8(7, 0x02); // Protocol ID (Password) + view.setUint32(8, 1, true); + view.setUint8(12, 0); // Password required? true + } + + if (instructions.puterAuth) { + console.log("Puter auth " + instructions.puterAuth) + // PASSWORD AUTH REQUIRED + view.setUint8(7, 0x02); // Protocol ID (Password) + view.setUint32(8, 5 + instructions.passwordEncoded.length, true); + view.setUint8(12, 0); + view.setUint16(13, instructions.passwordEncoded.length, true); + data.set(instructions.passwordEncoded, 15); + } + } + return data; +} \ No newline at end of file