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:
parent
39b8d8f715
commit
69e49e7f84
|
@ -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,23 +107,25 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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");
|
||||
|
@ -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 = ""
|
||||
|
|
14
README.md
14
README.md
|
@ -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)
|
||||
|
|
39
example.js
39
example.js
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue