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:
parent
f4408190f0
commit
39b8d8f715
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
14
README.md
14
README.md
|
@ -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
|
||||
|
|
10
example.js
10
example.js
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue