134 lines
4.1 KiB
JavaScript
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, "&").replace(/>/g, ">").replace(/</g, "<");
|
|
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);
|