Start fleshing it out
* Start a static handler * GeminiServer can now load certs and runs a TLS server * The example now runs!
This commit is contained in:
parent
a6f5a2cd62
commit
af9ce9f61f
104
GeminiServer.js
104
GeminiServer.js
|
@ -1,14 +1,21 @@
|
||||||
|
import * as path from "path";
|
||||||
|
import tls from "tls";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
export default class GeminiServer {
|
export default class GeminiServer {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
this.certificates = {};
|
this.certificates = {};
|
||||||
if (options.hasOwnProperty("host")) {
|
if (options.hasOwnProperty("host")) {
|
||||||
this.usingSNI = true;
|
this.usingSNI = true;
|
||||||
for (const certificate of options.host) {
|
for (const certificate of options.host) {
|
||||||
for (const hostname of options.host.hostnames) {
|
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] = {
|
this.certificates[hostname] = {
|
||||||
hostname,
|
hostname,
|
||||||
key: certificate.key,
|
key: fs.readFileSync(certificate.key),
|
||||||
cert: certificate.cert
|
cert: fs.readFileSync(certificate.cert)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,24 +23,101 @@ export default class GeminiServer {
|
||||||
this.usingSNI = false;
|
this.usingSNI = false;
|
||||||
this.certificates[options.hostname] = {
|
this.certificates[options.hostname] = {
|
||||||
hostname: options.hostname,
|
hostname: options.hostname,
|
||||||
key: options.key,
|
key: fs.readFileSync(options.key),
|
||||||
cert: options.cert
|
cert: fs.readFileSync(options.cert)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//A list of registered paths and their handlers. The first one that matches is used
|
//A list of registered paths and their handlers. The first one that matches is used
|
||||||
this.pathRegistry = [];
|
this.pathRegistry = [ //{
|
||||||
|
// hostname: a hostname that we serve
|
||||||
|
// path: absolute path, no trailing slash
|
||||||
|
// handler: the handler function
|
||||||
|
// }
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerPath(p, handler) {
|
||||||
|
let hostname;
|
||||||
registerPath(path, handler) {
|
if (!p.startsWith("/")) {
|
||||||
if (!path.startsWith("/")) {
|
|
||||||
//Then we're dealing with a hostname
|
//Then we're dealing with a hostname
|
||||||
if (!this.usingSNI) {
|
if (!this.usingSNI) {
|
||||||
throw new Error("Passed non-absolute path to registerPath.");
|
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)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pathRegistry.push({
|
||||||
|
hostname: hostname,
|
||||||
|
path: p
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
console.log("Responding");
|
||||||
|
socket.write("20 text/gemini\r\n");
|
||||||
|
socket.write("Hello, world!\r\n");
|
||||||
|
socket.write(`You're connecting from ${socket.servername}`)
|
||||||
|
socket.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
server.listen(1965);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
23
example.js
23
example.js
|
@ -1,5 +1,7 @@
|
||||||
const GeminiServer = require("./main.js").GeminiServer;
|
import GeminiServer from "./GeminiServer.js";
|
||||||
const staticFileHandler = require("./main.js").staticFileHandler;
|
import staticFiles from "./handlers/static.js";
|
||||||
|
|
||||||
|
// import {, staticFileHandler} from "./main.js";
|
||||||
|
|
||||||
const server = new GeminiServer({
|
const server = new GeminiServer({
|
||||||
// port: 1965
|
// port: 1965
|
||||||
|
@ -11,8 +13,13 @@ const server = new GeminiServer({
|
||||||
host: [
|
host: [
|
||||||
{
|
{
|
||||||
hostnames: ["localhost"],
|
hostnames: ["localhost"],
|
||||||
key: "key",
|
key: "private-key.pem",
|
||||||
cert: "",
|
cert: "public-cert.pem"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostnames: ["MacBookGamma.local"],
|
||||||
|
key: "mac-gamma-key.pem",
|
||||||
|
cert: "mac-gamma-pub-cert.pem",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -30,7 +37,7 @@ server.registerPath("localhost/allFiles", request => "text/txt\r\nHello, World\r
|
||||||
// We provide some convenient handlers for static, CGI, and reverse proxy
|
// We provide some convenient handlers for static, CGI, and reverse proxy
|
||||||
|
|
||||||
//if the passed file is a single file, it's a file. If it's a directory, all sub-files are auto-included
|
//if the passed file is a single file, it's a file. If it's a directory, all sub-files are auto-included
|
||||||
server.registerPath("localhost/static", staticFileHandler("/file/root" /*, {options}*/));
|
server.registerPath("localhost/static", staticFiles("/file/root" /*, {options}*/));
|
||||||
|
|
||||||
//The file passed to CGI handler must exist and be executable at run time
|
//The file passed to CGI handler must exist and be executable at run time
|
||||||
//You know what, CGIHandler hashes the file
|
//You know what, CGIHandler hashes the file
|
||||||
|
@ -38,10 +45,10 @@ server.registerPath("localhost/static", staticFileHandler("/file/root" /*, {opti
|
||||||
//No enabling CGI running for a certain file extension
|
//No enabling CGI running for a certain file extension
|
||||||
//No enabling CGI running for a certain path. This must take a single file
|
//No enabling CGI running for a certain path. This must take a single file
|
||||||
//500 CGI files, you better write 500 registerPath's (or write a custom handler)
|
//500 CGI files, you better write 500 registerPath's (or write a custom handler)
|
||||||
server.registerPath("localhost/run", CGIhandler("/path/to/test.py"));
|
// server.registerPath("localhost/run", CGIhandler("/path/to/test.py"));
|
||||||
|
|
||||||
//Kinda cursed but whatever
|
//Kinda cursed but whatever
|
||||||
server.registerPath("localhost/thoughts", proxyHandler("gemini://localhost:8888"));
|
// server.registerPath("localhost/thoughts", proxyHandler("gemini://localhost:8888"));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
server.run();
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import * as path from "path"
|
||||||
|
|
||||||
|
// This is not the handler. This is a function that takes a filePath
|
||||||
|
// and returns a handler
|
||||||
|
export default function staticFiles (basePath) {
|
||||||
|
// This is the handler
|
||||||
|
return function (request) {
|
||||||
|
//Concat and normalize the passed URL as being relative to the base path
|
||||||
|
const toServe = path.join(basePath, request)
|
||||||
|
//If the resulting path is a parent, relative to basePath, disallow that
|
||||||
|
if (path.relative(basePath, toServe).startsWith("..")) {
|
||||||
|
return "50"
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: import mmmagic
|
||||||
|
//TODO: convert line endings
|
||||||
|
const data = fs.readFileSync(toServe);
|
||||||
|
return "20 text/plain\r\n" + data;
|
||||||
|
}
|
||||||
|
}
|
4
main.js
4
main.js
|
@ -1,7 +1,7 @@
|
||||||
import tls from "tls";
|
import tls from "tls";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
|
||||||
import GeminiServer from "./GeminiServer"
|
import GeminiServer from "./GeminiServer.js"
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
//cert: fs.readFileSync("public-cert.pem"),
|
//cert: fs.readFileSync("public-cert.pem"),
|
||||||
|
@ -31,3 +31,5 @@ server.on("secureConnection", socket => {
|
||||||
socket.end();
|
socket.end();
|
||||||
});
|
});
|
||||||
server.listen(1965);
|
server.listen(1965);
|
||||||
|
|
||||||
|
export { GeminiServer }
|
||||||
|
|
Loading…
Reference in New Issue