gemini/index.js
2022-06-01 01:37:06 -07:00

134 lines
4.1 KiB
JavaScript

const fs = require("fs");
const tls = require("tls");
const http = require("http");
const files = {
style: fs.readFileSync("style.css", "utf8"),
page: fs.readFileSync("page.html", "utf8"),
input: fs.readFileSync("input.html", "utf8"),
index: fs.readFileSync("index.html", "utf8"),
error: fs.readFileSync("error.html", "utf8"),
};
function parse(data, url) {
const clean = (str) => str.replace(/&/g, "&amp;").replace(/>/g, "&gt;").replace(/</g, "&lt;");
let pre = false;
let list = false;
let html = "";
for(let line of data.split("\n")) {
const header = line.match(/^(#{1,3}) */);
const link = line.match(/^=> *(\S+) *(.+)?/);
if(line.startsWith("*")) {
if(!list) html += "<ul>\n";
list = true;
html += `\t<li>${clean(line.replace(/^\* */, ""))}</li>\n`;
continue;
} else if(list) {
html += "</ul>\n";
list = false;
}
if(line.startsWith("```")) {
const title = line.slice(3).trim();
html += `<${pre ? "/" : ""}pre${title ? ` title="${title}"` : ""}>\n`
pre = !pre;
} else if(pre) {
html += clean(line) + "\n";
} else if(header) {
const n = header[1].length;
html += `<h${n}>${clean(line.replace(/^#+ */, ""))}</h${n}>\n`;
} else if(line.startsWith(">")) {
html += `<blockquote>${clean(line.replace(/^> */, ""))}</blockquote>\n`;
} else if(link) {
let loc = new URL(link[1], url);
if (loc.protocol === "gemini:") {
loc = `/${loc.hostname}${loc.pathname}${loc.search}`;
}
html += `<p><a href="${loc}">${clean(link[2] ?? link[1])}</a></p>\n`;
} else if(line) {
html += `<p>${clean(line)}</p>\n`;
}
}
return html;
}
async function fetch(url, redirects = 5) {
const { host, href } = url;
const conn = tls.connect({
host,
port: 1965,
rejectUnauthorized: false,
});
return new Promise((res, rej) => {
let data = "";
conn.write(`${href}\r\n`);
conn.on("data", (d) => {
data += d;
if (data.length > 100 * 1024 * 1024) {
conn.close();
rej("response too large (the server's max is 100MiB!)");
}
});
conn.on("end", () => {
try {
const [_, codeStr, meta] = data.match(/([0-9]{2}) (.+?)\r\n/);
const code = parseInt(codeStr, 10);
if (code >= 10 && code < 30) return res({ data: data.replace(/.+/, "").trim(), meta, code });
if (code >= 30 && code < 40) return res(fetch(new URL(meta, url), redirects - 1));
rej(`code ${code}: ${meta}`);
} catch {
rej("invalid response");
}
});
conn.on("error", ({ code }) => {
switch(code) {
case "ENOTFOUND": return rej("domain not found");
case "ECONNREFUSED": return rej("connection refused");
case "ECONNRESET": return rej("connection reset");
default: return rej("response errored");
}
});
});
}
http.createServer((req, res) => {
if (req.method === "POST") {
req.on("data", (data) => {
const where = req.url + data.toString().replace("input=", "?").replace(/\+/g, "%20");
res.writeHead(301, { 'location': where }).end();
});
return;
}
if (req.url === "/style.css") return res.writeHead(200, { 'content-type': 'text/css' }).end(files.style);
if (req.url === "/") return res.end(files.index);
const [host, ...path] = req.url.split("/").filter((str, i) => str || i > 0);
if (host === "localhost") return res.writeHead(400).end(files.error.replace("{info}", "absolutely not"));
const url = new URL(`gemini://${host}/${path.join("/")}`);
fetch(url)
.then(({ code, meta, data }) => {
if (code < 20) {
const html = files.input
.replace("{title}", `gemini - ${url.hostname}`)
.replace("{url}", req.url)
.replace("{type}", code === 11 ? "password" : "text")
.replace("{meta}", meta);
res.writeHead(200, { 'content-type': 'text/html' }).end(html);
} else if (meta === "text/gemini") {
const charset = meta.replace(/.+?(;|$)/, "").trim() || "utf8";
const html = files.page
.replace("{title}", `gemini - ${url.hostname}`)
.replace("{charset}", charset)
.replace("{body}", parse(data, url));
res.writeHead(200, { 'content-type': 'text/html' }).end(html);
} else {
res.writeHead(200, { 'content-type': meta }).end(data);
}
})
.catch((msg) => {
res.writeHead(400).end(files.error.replace("{info}", msg));
});
}).listen(8329);