Wildcard domains

* Add support for wildcard domains (constrained to patterns you can get wildcard TLS certs for)
* Clean up some console.logs
* Rename some path variables
This commit is contained in:
MatthiasSaihttam 2021-08-28 22:34:32 -04:00
parent f4408190f0
commit 39b8d8f715
3 changed files with 75 additions and 20 deletions

View File

@ -3,6 +3,27 @@ import tls from "tls";
import fs from "fs";
import DefaultHandler from "./handlers/default.js";
/**
* wildHostMatches("*.example.com", "example.com") => false
* wildHostMatches("*.example.com", "hello.example.com") => true
* wildHostMatches("*.example.com", "test.hello.example.com") => false
* wildHostMatches("*.*.example.com", "test.hello.example.com") => false
*
* This follows from what wildcard TLS certs you're able to get
* That is *.example.com, or *.www.example.com or *.test.www.example.com
* But the * must be the left most element and there must be only one
*/
function wildHostMatches(wildHost, hostInstance) {
if (wildHost[0] === "*") {
//Return if everything after the first . matches
//TODO: what happens if I'm dumb and enter *ww.example.com
return wildHost.slice(wildHost.indexOf(".")) === hostInstance.slice(hostInstance.indexOf("."))
}else {
//If there's no wildcard, just return if they match
return wildHost === hostInstance;
}
}
export default class GeminiServer {
constructor(options) {
this.DefaultHandler = options.DefaultHandler || DefaultHandler;
@ -40,16 +61,16 @@ export default class GeminiServer {
];
}
registerPath(p, handler) {
registerPath(basePath, handler) {
let hostname;
if (!p.startsWith("/")) {
if (!basePath.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("/"))
hostname = basePath.slice(0, basePath.indexOf("/"));
basePath = basePath.slice(basePath.indexOf("/"))
}else {
// Hostname not specified
// Check that we're not doing SNI
@ -60,8 +81,8 @@ export default class GeminiServer {
}
//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 (basePath !== path.posix.resolve(basePath)) {
throw new Error(`Path ${basePath} passed to registerPath not normalized. (Remove trailing /'s and ensure there's a single root "/")`);
}
if (typeof handler === "string") {
@ -73,22 +94,26 @@ export default class GeminiServer {
this.pathRegistry.push({
hostname: hostname,
path: p,
basePath: basePath,
handler: handler
});
}
handleRequestLine(url, socket) {
handleRequest(url, socket) {
const urlPath = url.pathname;
for (const p of this.pathRegistry) {
//TODO: Wildcard hostnames
if (p.hostname === socket.servername) {
if (wildHostMatches(p.hostname, socket.servername)) {
if (p.handler.matchesSubpaths) {
//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("..");
const isSubPath = !path.posix.relative(p.basePath, urlPath).startsWith("..");
console.log(isSubPath);
}else {
if (path.posix.resolve(url.pathname) === p.path) {
const res = p.handler.handle(url, p.path);
console.log(`Attempting to match "${path.posix.resolve(url.pathname)}" with "${p.basePath}"`);
if (path.posix.resolve(url.pathname) === p.basePath) {
const res = p.handler.handle(url, p.basePath);
socket.write(res);
socket.end();
return;
@ -96,6 +121,10 @@ export default class GeminiServer {
}
}
}
// If we haven't returned yet, 404
console.log(`No matches found for host ${socket.servername} and path ${urlPath}`)
socket.write("51 Not Found\r\n");
socket.end();
}
/**
@ -105,9 +134,13 @@ export default class GeminiServer {
const options = {
//SNI is mandatory for Gemini clients, so I'm not going to bother providing a fallback key/cert
SNICallback: (servername, callback) => {
console.log();
console.log(`Negotiating SNI for ${servername}`);
//Find the cert that matches this servername
//TODO: Support wildcard hostnames
const cert = this.certificates[servername];
//Support wildcard hostnames
const hostnamePattern = Object.keys(this.certificates).find(cert => wildHostMatches(cert, servername));
console.log(`Found hostname pattern ${hostnamePattern}`);
const cert = this.certificates[hostnamePattern];
if (!cert) {
/*
* The first param to callback is supposed to be an error
@ -117,6 +150,7 @@ export default class GeminiServer {
* And so tries to connect with no certificate
* This obviously fails. But the browser I tested in then shows >TLS/SSL handshake failed
*/
console.log(`No matching hostname found. Aborting handshake.`);
callback(null, null);
}else {
callback(null, tls.createSecureContext(cert));
@ -133,11 +167,10 @@ export default class GeminiServer {
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
console.log(`< ${requestLine.trim()}\\r\\n`);
//If this is true we have data after the first \n\r, be mean and error
if (requestLine.indexOf("\r\n") !== requestLine.length - 2) {
//TODO: make this line not a joke
socket.write("40 Heck you\r\n");
socket.write("59 Bad Request\r\n");
socket.end();
}else {
//TODO: What if the URL doesn't have a protocol or a domain at all?
@ -149,7 +182,7 @@ export default class GeminiServer {
socket.end();
}else {
//Responding
this.handleRequestLine(url, socket);
this.handleRequest(url, socket);
}
}
}

View File

@ -1,4 +1,18 @@
There are a lot of paths.
A `urlPath` is the absolute path given in the Gemini request (new URL().pathname)
A `basePath` is the first argument to register path. We match the `urlPath` against a basePath when determining how to handle a request.
A `relativePath` is the difference between a `basePath` and a `urlPath`. It's normally `""`.
Sometimes these paths will include their hostnames
A path in the pathRegistry is a `p`, which is an object with a hostname, a basePath, and a handler
### Creating certificates for dev
```sh

View File

@ -12,7 +12,7 @@ const server = new GeminiServer({
//Option 2 (if you want virtual hosting)
host: [
{
hostnames: ["localhost"],
hostnames: ["localhost", "127.0.0.1"],
key: "private-key.pem",
cert: "public-cert.pem"
},
@ -20,6 +20,11 @@ const server = new GeminiServer({
hostnames: ["MacBookGamma.local"],
key: "mac-gamma-key.pem",
cert: "mac-gamma-pub-cert.pem",
},
{
hostnames: ["*.example.com"],
key: "private-key.pem",
cert: "ex-public-cert.pem"
}
]
});
@ -29,6 +34,9 @@ server.registerPath("localhost/", "### Hello, world");
//Anything before the first slash is a domain name and is used in SNI matching if you passed the `host` option
server.registerPath("MacBookGamma.local/", "### Hello from my Mac!");
server.registerPath("127.0.0.1/", "Hello, world.");
server.registerPath("*.example.com/", "This is a catch-all domain");
//Otherwise, you can subclass DefaultHandler, which has a .handle method, that takes a url
//request has everything you would expect, .servername, .path,
//You have to provide the entire response inc. content type