Skip to content

Commit

Permalink
Merge pull request #1085 from ProgrammerIn-wonderland/main
Browse files Browse the repository at this point in the history
networking API beginnings
  • Loading branch information
jelveh authored Jan 9, 2025
2 parents d9a8ea0 + 1ed3bcc commit d65ee6c
Show file tree
Hide file tree
Showing 4 changed files with 288 additions and 0 deletions.
18 changes: 18 additions & 0 deletions src/puter-js/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
})();


}

/**
Expand Down
44 changes: 44 additions & 0 deletions src/puter-js/src/modules/networking/PSocket.js
Original file line number Diff line number Diff line change
@@ -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?

}
}
}
79 changes: 79 additions & 0 deletions src/puter-js/src/modules/networking/PWispHandler.js
Original file line number Diff line number Diff line change
@@ -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
}))
}
}
147 changes: 147 additions & 0 deletions src/puter-js/src/modules/networking/parsers.js
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit d65ee6c

Please sign in to comment.