Misc changes

* Static handler can match sub paths
* Handler.handle can be asnyc
* Better hostname checking at the start of basePaths
* Document the SNI requirement
This commit is contained in:
MatthiasSaihttam 2021-08-29 12:12:34 -04:00
parent 39b8d8f715
commit 69e49e7f84
4 changed files with 76 additions and 30 deletions

View File

@ -44,6 +44,7 @@ export default class GeminiServer {
}
}
}else {
console.log(`Listening on hostname ${options.hostname}`);
this.usingSNI = false;
this.certificates[options.hostname] = {
hostname: options.hostname,
@ -65,11 +66,13 @@ export default class GeminiServer {
let hostname;
if (!basePath.startsWith("/")) {
//Then we're dealing with a hostname
if (!this.usingSNI) {
throw new Error("Passed non-absolute path to registerPath.");
hostname = basePath.slice(0, basePath.indexOf("/"));
// Make sure it's a configured hostname
if (!Object.keys(this.certificates).includes(hostname)) {
throw new Error(`Passed non-absolute path or unrecognized hostname ${hostname} (in path ${basePath}) to registerPath.`);
}
hostname = basePath.slice(0, basePath.indexOf("/"));
basePath = basePath.slice(basePath.indexOf("/"))
}else {
// Hostname not specified
@ -104,20 +107,22 @@ export default class GeminiServer {
for (const p of this.pathRegistry) {
//TODO: Wildcard hostnames
if (wildHostMatches(p.hostname, socket.servername)) {
let matches;
if (p.handler.matchesSubpaths) {
console.log(`Attempting to sub-path match "${path.posix.resolve(url.pathname)}" with "${p.basePath}"`);
//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.basePath, urlPath).startsWith("..");
console.log(isSubPath);
matches = !path.posix.relative(p.basePath, urlPath).startsWith("..");
}else {
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);
matches = path.posix.resolve(url.pathname) === p.basePath;
}
if (matches) {
Promise.resolve(p.handler.handle(url, p.basePath)).then(res => {
socket.write(res);
socket.end();
return;
}
});
return;
}
}
}
@ -160,6 +165,7 @@ export default class GeminiServer {
const server = tls.createServer(options);
server.on("secureConnection", socket => {
console.log("Connection, looking for " + socket.servername);
socket.setEncoding("utf8");
let requestLine = ""

View File

@ -1,4 +1,18 @@
### We require SNI!
The Gemini spec requires clients to implement SNI. This server requires SNI to connect.
```sh
echo "gemini://localhost/\r" | openssl s_client -connect "localhost:1965" # WILL FAIL
```
If you want to use `openssl s_client` to debug, you must pass the `-servername` option so that openSSL will send a hostname to the server.
```sh
echo "gemini://localhost/\r" | openssl s_client -connect "localhost:1965" -servername "localhost" # All good!
```
There are a lot of paths.
A `urlPath` is the absolute path given in the Gemini request (new URL().pathname)

View File

@ -1,16 +1,17 @@
import GeminiServer from "./GeminiServer.js";
import StaticHandler from "./handlers/static.js";
import DefaultHandler from "./handlers/default.js";
// import {, staticFileHandler} from "./main.js";
const server = new GeminiServer({
// port: 1965
//Option 1 (a single hostname):
// hostname:
// key:
// cert
hostname: "localhost",
key: "private-key.pem",
cert: "public-cert.pem",
//Option 2 (if you want virtual hosting)
host: [
/*host: [
{
hostnames: ["localhost", "127.0.0.1"],
key: "private-key.pem",
@ -26,26 +27,38 @@ const server = new GeminiServer({
key: "private-key.pem",
cert: "ex-public-cert.pem"
}
]
]*/
});
//Registering a path with a string returns the string as Gemini text
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("MacBookGamma.local/", "### Hello from my Mac!");
server.registerPath("127.0.0.1/", "Hello, world.");
server.registerPath("*.example.com/", "This is a catch-all domain");
// server.registerPath("127.0.0.1/", "Hello, world.");
//Wild-card domains are supported like they are by TLS, only one "*" and it must be first.
//Wild-card's aren't supported in paths or anywhere else
// 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
// server.registerPath("localhost/allFiles", request => "20 text/txt\r\nHello, World\r\n");
//You can subclass DefaultHandler, which has a .handle method, that takes a url and a basePath
//You have to provide the entire response including content type
class ExampleHandler extends DefaultHandler {
constructor() {
super();
}
// We provide some convenient handlers for static, CGI, and reverse proxy
handle(url, basePath) {
return "20 text/txt\r\nHello, World\r\n";
}
}
server.registerPath("localhost/allFiles", new ExampleHandler());
// We will 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
server.registerPath("localhost/static/about.gmi", new StaticHandler("/tmp/content/about.gmi" /*, {options}*/));
server.registerPath("localhost/static", new StaticHandler("/tmp/content/" /*, {options}*/));
// server.registerPath("localhost/static", new StaticHandler("/tmp/yourmom/" /*, {options}*/));
//The file passed to CGI handler must exist and be executable at run time
//You know what, CGIHandler hashes the file

View File

@ -3,17 +3,15 @@ import * as fs from "fs";
import DefaultHandler from "./default.js";
// This is not the handler. This is a function that takes a filePath
// and returns a handler
export default class StaticHandler extends DefaultHandler {
constructor(basePath) {
constructor(basePath, directoryList = false) {
super();
this.isSingleFile = true;
this.basePath = basePath;
this.directoryList = directoryList;
//This will throw if the file is a directory or doesn't exist
fs.readFileSync(this.basePath);
//This will throw if the file doesn't exist
this.isSingleFile = !fs.statSync(basePath).isDirectory();
this.matchesSubpaths = !this.isSingleFile;
}
@ -28,6 +26,21 @@ export default class StaticHandler extends DefaultHandler {
return "50"
}
console.log(`Attempting to serve ${toServe}.`);
try {
if (fs.statSync(toServe).isDirectory()) {
console.log(`File is a directory and directoryList is ${this.directoryList}.`);
if (this.directoryList) {
} else {
return "51 Not Found\r\n";
}
}
}catch (e) {
//If the file doesn't exist
return "51 Not Found\r\n";
}
//TODO: import mmmagic
//TODO: convert line endings
const data = fs.readFileSync(toServe);