/* Gopher.js - Backend for fetching, storing and manipulating data over the Gopher network. (c) 2020 Senri Software Requires an environment with mozTCPSocket. Primarily intended for KaiOS. This is pre-alpha quality software, and a "sneak peek" of what to come. I don't yet feel comfortable licensing Gopher.js. */ "use strict"; let defaultentry = { "type": "1", "name": "Gopher root", "path": "", "host": "192.168.1.44", "port": 70, "query": "" }; let emptyentry = { "type": "", "name": "", "path": "", "host": "", "port": 0, "query": "" }; let decoder = new TextDecoder(); let encoder = new TextEncoder(); function completeGopherEntry(entry) { /* Takes an incomplete GopherEntry and returns a new GopherEntry with unset values set to default. */ let newentry = Object.assign({}, emptyentry, entry); for (let p in newentry) { if (!newentry[p]) { newentry[p] = defaultentry[p]; }; }; return newentry; }; function GopherSocket(entry) { /* Returns a GopherSocket connecting to the given GopherEntry. */ entry = completeGopherEntry(entry); let datapackets = []; let obj = { "socket": navigator.mozTCPSocket.open(entry.host, entry.port, {binaryType: "arraybuffer"}), "entry": entry, "received": 0, "data": new Uint8Array(0) }; obj.socket.ondata = function(event) { datapackets.push(event.data); obj.received += event.data.byteLength; console.log("%d bytes received", obj.received); }; obj.socket.onopen = function() { console.log("Connected to %s:%d. Sending selector \"%s\" with query \"%s\"", entry.host, entry.port, entry.path, entry.query); let outbuffer if (entry.query) { outbuffer = encoder.encode(entry.path+"\t"+entry.query+"\r\n"); } else { outbuffer = encoder.encode(entry.path+"\r\n"); }; obj.socket.send(outbuffer.buffer); }; obj.socket.onclose = function() { console.log("Disconnected from %s:%d.", entry.host, entry.port); obj.data = new Uint8Array(obj.received); let acc = 0; datapackets.forEach( function(packet) { obj.data.set(new Uint8Array(packet), acc); acc += packet.byteLength; }); datapackets = []; if (entry.type === "1" || entry.type === "7") { obj.dir = rawToGopherDirectory(obj.data); }; }; obj.socket.onerror = function() { console.error("A connection error occurred (%s:%d). Disconnecting...", entry.host, entry.port); obj.socket.close(); }; return obj; }; function rawToGopherEntry(dirline) { /* Gets a single gopher entry as a raw string (trimming \r\n if necessary) and returns a GopherEntry. */ dirline = dirline.trim(); if (dirline.length < 2) { return null; }; let direntry = dirline.split("\t"); if (direntry.length < 4) { console.error("Malformed gopher entry (not enough fields specified): %o", direntry); return { "type": "3", "name": "(Client-side error: Malformed entry)", "path": "", "host": "error.invalid", "port": 1 }; }; return { "type": direntry[0][0], "name": direntry[0].slice(1), "path": direntry[1], "host": direntry[2], "port": parseInt(direntry[3],10) }; }; function rawToGopherDirectory(directory) { /* Gets a gopher directory as a Uint8Array and returns a GopherDirectory. */ var dirarray = decoder.decode(directory).split("\n"); dirarray.forEach(function(entry, i, array) { array.splice(i, 1, rawToGopherEntry(entry)); }); return dirarray; }; function GopherEntryToURL(entry) { /* Gets a GopherEntry and returns its URL. */ let port; if (entry.port === 70 || !entry.port) { port=""; } else { port=":" + String(entry.port); }; let path = entry.path; if (entry.query) { path += "\t" + entry.query; }; return encodeURI("gopher://" + entry.host + port + "/" + entry.type + path); }; function URLToGopherEntry(url) { /* Gets a gopher:// URL and returns its GopherEntry, filling the name with host+path. */ if (!url.startsWith("gopher://")) { return null; }; url = decodeURI(url.slice(9)); let urlarray = url.split(/\/(.*)/); let server = urlarray[0].split(":"); let selector; if (urlarray[1]) { selector = urlarray[1].split("\t"); } else { selector = ["", ""]; }; return { "type": selector[0][0] || "1", "name": server[0] + "/" + selector[0].slice(1), "path": selector[0].slice(1), "host": server[0], "port": parseInt(server[1],10) || 70, "query": selector[1] || "" }; };