initial commit

This commit is contained in:
tezlm 2022-06-01 01:06:01 -07:00
commit b5c616ad14
6 changed files with 195 additions and 0 deletions

12
error.html Normal file
View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>error</title>
<link rel="stylesheet" href="/style.css" />
</head>
<body>
<h1>error</h1>
<p>{info}</p>
</body>
</html>

10
index.html Normal file
View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>gemini proxy</title>
<link rel="stylesheet" href="/style.css" />
</head>
<body>
<p>welcome to gem.celery.eu.org</p>
</body>
</html>

134
index.js Normal file
View file

@ -0,0 +1,134 @@
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}`;
}
html += `<p><a href="${loc}">${clean(link[2])}</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);

15
input.html Normal file
View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>{title}</title>
<link rel="stylesheet" href="/style.css" />
</head>
<body onload="document.querySelector('input').focus()">
<h1>{meta}</h1>
<form method="POST" action={url}>
<input type="{type}" name="input" style="width:100%" />
<br /><br />
<input type="submit">
</form>
</body>
</html>

11
page.html Normal file
View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>{title}</title>
<link rel="stylesheet" href="/style.css">
<meta charset="{charset}">
</head>
<body>
{body}
</body>
</html>

13
style.css Normal file
View file

@ -0,0 +1,13 @@
body {
max-width: 650px;
margin: 0 auto;
padding: 2em;
}
pre {
background: #f4f4f4;
border-radius: 4px;
padding: 4px;
margin: 0 -4px;
overflow-x: auto;
}