import * as path from "path"; import tls from "tls"; import fs from "fs"; import geminiText from "./handlers/gemini.js"; export default class GeminiServer { constructor(options) { this.defaultHandler = options.defaultHandler || geminiText; this.certificates = {}; if (options.hasOwnProperty("host")) { this.usingSNI = true; for (const certificate of options.host) { 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}`); this.certificates[hostname] = { hostname, key: fs.readFileSync(certificate.key), cert: fs.readFileSync(certificate.cert) } } } }else { this.usingSNI = false; this.certificates[options.hostname] = { hostname: options.hostname, key: fs.readFileSync(options.key), cert: fs.readFileSync(options.cert) } } //A list of registered paths and their handlers. The first one that matches is used this.pathRegistry = [ //{ // hostname: a hostname that we serve // path: absolute path, no trailing slash // handler: the handler function // } ]; } registerPath(p, handler) { let hostname; if (!p.startsWith("/")) { //Then we're dealing with a hostname if (!this.usingSNI) { throw new Error("Passed non-absolute path to registerPath."); } 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)`); } if (typeof handler !== "function") { handler = this.defaultHandler(handler); } this.pathRegistry.push({ hostname: hostname, path: p, handler: handler }); } 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) { const res = p.handler({ path: url }) socket.write(res); socket.end(); return; } } } } /** * 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 { //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); } } } }); }); server.listen(1965); } }