2021-08-28 21:34:09 +00:00
|
|
|
import * as path from "path";
|
|
|
|
import tls from "tls";
|
|
|
|
import fs from "fs";
|
2021-08-28 23:39:40 +00:00
|
|
|
import DefaultHandler from "./handlers/default.js";
|
2021-08-28 21:34:09 +00:00
|
|
|
|
2021-08-28 20:09:02 +00:00
|
|
|
export default class GeminiServer {
|
|
|
|
constructor(options) {
|
2021-08-28 23:39:40 +00:00
|
|
|
this.DefaultHandler = options.DefaultHandler || DefaultHandler;
|
2021-08-28 22:32:24 +00:00
|
|
|
|
2021-08-28 20:09:02 +00:00
|
|
|
this.certificates = {};
|
|
|
|
if (options.hasOwnProperty("host")) {
|
|
|
|
this.usingSNI = true;
|
|
|
|
for (const certificate of options.host) {
|
2021-08-28 21:34:09 +00:00
|
|
|
for (const hostname of certificate.hostnames) {
|
|
|
|
console.log(`For hostname ${hostname}:`);
|
|
|
|
console.log(` Using cert from ${certificate.cert}`);
|
|
|
|
console.log(` Using key from ${certificate.key}`);
|
2021-08-28 20:09:02 +00:00
|
|
|
this.certificates[hostname] = {
|
|
|
|
hostname,
|
2021-08-28 21:34:09 +00:00
|
|
|
key: fs.readFileSync(certificate.key),
|
|
|
|
cert: fs.readFileSync(certificate.cert)
|
2021-08-28 20:09:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}else {
|
|
|
|
this.usingSNI = false;
|
|
|
|
this.certificates[options.hostname] = {
|
|
|
|
hostname: options.hostname,
|
2021-08-28 21:34:09 +00:00
|
|
|
key: fs.readFileSync(options.key),
|
|
|
|
cert: fs.readFileSync(options.cert)
|
2021-08-28 20:09:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//A list of registered paths and their handlers. The first one that matches is used
|
2021-08-28 21:34:09 +00:00
|
|
|
this.pathRegistry = [ //{
|
|
|
|
// hostname: a hostname that we serve
|
|
|
|
// path: absolute path, no trailing slash
|
|
|
|
// handler: the handler function
|
|
|
|
// }
|
|
|
|
];
|
2021-08-28 20:09:02 +00:00
|
|
|
}
|
|
|
|
|
2021-08-28 21:34:09 +00:00
|
|
|
registerPath(p, handler) {
|
|
|
|
let hostname;
|
|
|
|
if (!p.startsWith("/")) {
|
2021-08-28 20:09:02 +00:00
|
|
|
//Then we're dealing with a hostname
|
|
|
|
if (!this.usingSNI) {
|
|
|
|
throw new Error("Passed non-absolute path to registerPath.");
|
|
|
|
}
|
|
|
|
|
2021-08-28 21:34:09 +00:00
|
|
|
hostname = p.slice(0, p.indexOf("/"));
|
|
|
|
p = p.slice(p.indexOf("/"))
|
|
|
|
}else {
|
|
|
|
// Hostname not specified
|
|
|
|
// Check that we're not doing SNI
|
|
|
|
if (this.usingSNI) {
|
|
|
|
throw new Error("Virtual hosting is being used but registerPath was called without a hostname.");
|
|
|
|
}
|
|
|
|
hostname = Object.keys(this.certificates)[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
//Make sure to use POSIX paths for URLs. On Windows, path.resolve uses \
|
|
|
|
if (p !== path.posix.resolve(p)) {
|
|
|
|
throw new Error(`Path ${p} passed to registerPath not normalized. (Remove trailing /'s and ..'s)`);
|
2021-08-28 20:09:02 +00:00
|
|
|
}
|
2021-08-28 21:34:09 +00:00
|
|
|
|
2021-08-28 23:39:40 +00:00
|
|
|
if (typeof handler === "string") {
|
|
|
|
handler = new this.DefaultHandler(handler);
|
|
|
|
}
|
|
|
|
if (!(handler instanceof DefaultHandler)) {
|
|
|
|
throw new Error("Handler must be an instance of DefaultHandler or a subclass.");
|
2021-08-28 22:32:24 +00:00
|
|
|
}
|
|
|
|
|
2021-08-28 21:34:09 +00:00
|
|
|
this.pathRegistry.push({
|
|
|
|
hostname: hostname,
|
2021-08-28 22:32:24 +00:00
|
|
|
path: p,
|
|
|
|
handler: handler
|
2021-08-28 21:34:09 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-08-28 22:32:24 +00:00
|
|
|
handleRequestLine(url, socket) {
|
|
|
|
for (const p of this.pathRegistry) {
|
|
|
|
//TODO: Wildcard hostnames
|
|
|
|
if (p.hostname === socket.servername) {
|
|
|
|
//If the requested path is a sub path (e.g. equal to or more specific than the path in the current registry entry)
|
|
|
|
const isSubPath = !path.posix.relative(p.path, url.pathname).startsWith("..");
|
|
|
|
|
|
|
|
if (path.posix.resolve(url.pathname) === p.path) {
|
2021-08-28 23:39:40 +00:00
|
|
|
console.log(p.handler);
|
|
|
|
// console.log(p.handler instance);
|
|
|
|
const res = p.handler.handle(url);
|
2021-08-28 22:32:24 +00:00
|
|
|
socket.write(res);
|
|
|
|
socket.end();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-28 21:34:09 +00:00
|
|
|
/**
|
|
|
|
* Start running the server
|
|
|
|
*/
|
|
|
|
run() {
|
|
|
|
const options = {
|
|
|
|
//SNI is mandatory for Gemini clients, so I'm not going to bother providing a fallback key/cert
|
|
|
|
SNICallback: (servername, callback) => {
|
|
|
|
//Find the cert that matches this servername
|
|
|
|
//TODO: Support wildcard hostnames
|
|
|
|
const cert = this.certificates[servername];
|
|
|
|
if (!cert) {
|
|
|
|
/*
|
|
|
|
* The first param to callback is supposed to be an error
|
|
|
|
* So I could do callback("No hostname found")
|
|
|
|
* Empirically, that closes the socket leaving most Gemini browsers confused (showing the previous page)
|
|
|
|
* Passing null means node falls back to the default (which also isn't provided, see above)
|
|
|
|
* And so tries to connect with no certificate
|
|
|
|
* This obviously fails. But the browser I tested in then shows >TLS/SSL handshake failed
|
|
|
|
*/
|
|
|
|
callback(null, null);
|
|
|
|
}else {
|
|
|
|
callback(null, tls.createSecureContext(cert));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const server = tls.createServer(options);
|
|
|
|
server.on("secureConnection", socket => {
|
|
|
|
socket.setEncoding("utf8");
|
|
|
|
|
|
|
|
let requestLine = ""
|
|
|
|
socket.on("data", d => {
|
|
|
|
requestLine += d;
|
|
|
|
//If we have the end of the request
|
|
|
|
if (requestLine.includes("\r\n")) {
|
|
|
|
console.log("Got line: `" + requestLine + "`")
|
|
|
|
//If this is true we have data after the first \n\r, be mean and 40 or whatever
|
|
|
|
if (requestLine.indexOf("\r\n") !== requestLine.length - 2) {
|
|
|
|
//TODO: make this line not a joke
|
|
|
|
socket.write("40 Heck you\r\n");
|
|
|
|
socket.end();
|
|
|
|
}else {
|
2021-08-28 22:32:24 +00:00
|
|
|
//TODO: What if the URL doesn't have a protocol or a domain at all?
|
|
|
|
//Make sure that the SNI negotiated name matches the hostname in the request
|
|
|
|
const url = new URL(requestLine.slice(0, -2))
|
|
|
|
if (url.host !== socket.servername) {
|
|
|
|
//TODO: make this line not a joke
|
|
|
|
socket.write("40 Heck you\r\n");
|
|
|
|
socket.end();
|
|
|
|
}else {
|
|
|
|
//Responding
|
|
|
|
this.handleRequestLine(url, socket);
|
|
|
|
}
|
2021-08-28 21:34:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
server.listen(1965);
|
2021-08-28 20:09:02 +00:00
|
|
|
}
|
|
|
|
}
|