Add ReverseProxy
* Restructure imports/exports * Add a default export * Jump to 2.0 per semver * Add a ReverseProxy handler * Change the behavior of registerPath around root hostnames
This commit is contained in:
parent
e53fb7a8fe
commit
3d6fc7b211
|
@ -29,6 +29,7 @@ export default class GeminiServer {
|
|||
this.DefaultHandler = options.DefaultHandler || DefaultHandler;
|
||||
|
||||
this.certificates = {};
|
||||
//TODO: change the host option to "virtualHosts" or something
|
||||
if (options.hasOwnProperty("host")) {
|
||||
this.usingSNI = true;
|
||||
for (const certificate of options.host) {
|
||||
|
@ -64,9 +65,14 @@ export default class GeminiServer {
|
|||
|
||||
registerPath(basePath, handler) {
|
||||
let hostname;
|
||||
//TODO: What happens if we get an empty basepath?
|
||||
if (!basePath.startsWith("/")) {
|
||||
//Then we're dealing with a hostname
|
||||
hostname = basePath.slice(0, basePath.indexOf("/"));
|
||||
//TODO: This means that "localhost" and "localhost/" are both accepted
|
||||
//It's wierd to force one over the other, but I might in the future
|
||||
if (hostname.includes("/")) {
|
||||
hostname = basePath.slice(0, basePath.indexOf("/"));
|
||||
}
|
||||
|
||||
// Make sure it's a configured hostname
|
||||
if (!Object.keys(this.certificates).includes(hostname)) {
|
||||
|
@ -103,10 +109,28 @@ export default class GeminiServer {
|
|||
}
|
||||
|
||||
handleRequest(url, socket) {
|
||||
let urlPath;
|
||||
//In the case of "gemini://example.com" url.pathname is "", which is a problem when we later .resolve
|
||||
const urlPath = url.pathname || "/";
|
||||
if (url.pathname === "") {
|
||||
console.log("Got empty pathname, redirecting to \"/\"");
|
||||
socket.write(`30 ${url.protocol}//${url.hostname}/${url.search}${url.hash}\r\n`);
|
||||
socket.end();
|
||||
return;
|
||||
}else {
|
||||
urlPath = path.posix.resolve(url.pathname);
|
||||
|
||||
//Unslash
|
||||
if (urlPath !== url.pathname) {
|
||||
console.log(`Requested path ${url.pathname} doesn't match normalized path ${urlPath}, redirecting`);
|
||||
//Return a redirect to add a slash
|
||||
//Ignore URL segments: /abc/xyz/;test isn't redirected; /abc/xyz;test/ => /abc/xyz;test
|
||||
socket.write(`30 ${url.protocol}//${url.hostname}${urlPath}${url.search}${url.hash}\r\n`);
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const p of this.pathRegistry) {
|
||||
//TODO: Wildcard hostnames
|
||||
if (wildHostMatches(p.hostname, socket.servername)) {
|
||||
let matches;
|
||||
if (p.handler.matchesSubpaths) {
|
||||
|
@ -119,10 +143,10 @@ export default class GeminiServer {
|
|||
}
|
||||
|
||||
if (matches) {
|
||||
Promise.resolve(p.handler.handle(url, p.basePath)).then(res => {
|
||||
Promise.resolve(p.handler.handle(url, p.basePath, socket)).then(res => {
|
||||
socket.write(res);
|
||||
socket.end();
|
||||
});
|
||||
}).catch(console.error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -169,13 +193,13 @@ export default class GeminiServer {
|
|||
console.log("Connection, looking for " + socket.servername);
|
||||
socket.setEncoding("utf8");
|
||||
|
||||
let requestLine = ""
|
||||
let requestLine = "";
|
||||
socket.on("data", d => {
|
||||
requestLine += d;
|
||||
//If we have the end of the request
|
||||
if (requestLine.includes("\r\n")) {
|
||||
console.log(`< ${requestLine.trim()}\\r\\n`);
|
||||
//If this is true we have data after the first \n\r, be mean and error
|
||||
//If this is true, we have data after the first \r\n, be mean and error
|
||||
if (requestLine.indexOf("\r\n") !== requestLine.length - 2) {
|
||||
socket.write("59 Bad Request\r\n");
|
||||
socket.end();
|
||||
|
|
|
@ -38,3 +38,10 @@ openssl req -new -sha256 -key private-key.pem -out csr.pem
|
|||
# Self-sign, generating cert
|
||||
openssl x509 -req -in csr.pem -signkey private-key.pem -out public-cert.pem
|
||||
```
|
||||
|
||||
|
||||
# TODO
|
||||
It's possible to imagine a situation where astronomical theater is behind a proxy and the proxy doesn't do address translation, passing the raw gemini request to us.
|
||||
In this case, our behavior is undefined.
|
||||
|
||||
Similarly, SNI is not supported with IP addresses.
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import GeminiServer from "./GeminiServer.js";
|
||||
import StaticHandler from "./handlers/static.js";
|
||||
import DefaultHandler from "./handlers/default.js";
|
||||
import ReverseProxyHandler from "./handlers/revproxy.js";
|
||||
|
||||
// import {, staticFileHandler} from "./main.js";
|
||||
|
||||
|
@ -70,6 +71,8 @@ server.registerPath("localhost/static", new StaticHandler("/tmp/content/" /*, {o
|
|||
|
||||
//Kinda cursed but whatever
|
||||
// server.registerPath("localhost/thoughts", proxyHandler("gemini://localhost:8888"));
|
||||
server.registerPath("/blaseball", new ReverseProxyHandler("localhost:1973"));
|
||||
server.registerPath("/thoughts", new ReverseProxyHandler("thoughts.learnerpages.com/about"));
|
||||
|
||||
|
||||
server.run();
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
import DefaultHandler from "./default.js"
|
||||
import path from "path";
|
||||
import * as tls from "tls";
|
||||
import { pipeline, Readable } from "stream";
|
||||
|
||||
/**
|
||||
* async wrapper around tls.connect
|
||||
*/
|
||||
async function tlsConnect(...args) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
try {
|
||||
const connection = tls.connect(...args, function () {
|
||||
console.log("Got connection");
|
||||
resolve(connection);
|
||||
});
|
||||
connection.on("error", function (err) {
|
||||
console.error(err);
|
||||
reject(err);
|
||||
});
|
||||
}catch (err) {
|
||||
console.error(err);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Make Gemini request to URL
|
||||
// Return the content
|
||||
async function geminiReq(toServe, socket) {
|
||||
const url = new URL(`gemini://${toServe}`);
|
||||
|
||||
const options = {
|
||||
port: url.port || 1965,
|
||||
host: url.hostname,
|
||||
servername: url.hostname, //The servername used in SNI
|
||||
};
|
||||
|
||||
console.log(options);
|
||||
const connection = await tlsConnect(options);
|
||||
|
||||
// connection.write("gemini://example.com:1973/testdata\r\n");
|
||||
|
||||
await new Promise(function (resolve, reject) {
|
||||
pipeline(
|
||||
Readable.from((function* () {
|
||||
const req = `${url.toString()}\r\n`;
|
||||
console.log(`Sending proxy request ${req}`);
|
||||
yield req;
|
||||
})()),
|
||||
connection,
|
||||
socket,
|
||||
function (err) {
|
||||
if (err) {
|
||||
console.log("Pipeline failed");
|
||||
reject(err);
|
||||
}else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default class ReverseProxyHandler extends DefaultHandler {
|
||||
constructor (proxyRoot) {
|
||||
super();
|
||||
|
||||
this.proxyRoot = proxyRoot;
|
||||
this.subpathMatches = true;
|
||||
}
|
||||
|
||||
async handle (url, p, socket) {
|
||||
console.log(`${url} matched, attempting to proxy to ${this.proxyRoot}`);
|
||||
const relativePath = path.relative(p, url.pathname);
|
||||
//Concat and normalize the passed URL as being relative to the base path
|
||||
const toServe = path.join(this.proxyRoot, relativePath);
|
||||
//If the resulting path is a parent, relative to proxyRoot, disallow that
|
||||
if (path.relative(this.proxyRoot, toServe).startsWith("..")) {
|
||||
return "50";
|
||||
}
|
||||
|
||||
console.log(`Attempting to proxy ${toServe}.`);
|
||||
try {
|
||||
await geminiReq(toServe, socket)
|
||||
return "";
|
||||
}catch (err) {
|
||||
console.log("Something went wrong with the proxy.");
|
||||
console.error(err);
|
||||
// You could debate whether this is a 43 or a 42 or a 41
|
||||
return "42 Internal Proxy Error\r\n"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ export default class StaticHandler extends DefaultHandler {
|
|||
}
|
||||
|
||||
// This is the handler
|
||||
// url is the URL object, with url.pathname being the path. p is the 'client-side' basepath
|
||||
handle (url, p) {
|
||||
const relativePath = path.relative(p, url.pathname);
|
||||
//Concat and normalize the passed URL as being relative to the base path
|
||||
|
@ -38,6 +39,7 @@ export default class StaticHandler extends DefaultHandler {
|
|||
}
|
||||
}catch (e) {
|
||||
//If the file doesn't exist
|
||||
console.log(`File doesn't exist or can't be opened.`)
|
||||
return "51 Not Found\r\n";
|
||||
}
|
||||
|
||||
|
|
7
main.js
7
main.js
|
@ -1,4 +1,9 @@
|
|||
import GeminiServer from "./GeminiServer.js"
|
||||
import StaticHandler from "./handlers/static.js"
|
||||
import DefaultHandler from "./handlers/default.js"
|
||||
import ReverseProxyHandler from "./handlers/revproxy.js"
|
||||
|
||||
export { GeminiServer, StaticHandler };
|
||||
export default GeminiServer;
|
||||
export {
|
||||
StaticHandler, DefaultHandler, ReverseProxyHandler
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "astronomical-theater",
|
||||
"version": "v1.0.0",
|
||||
"version": "v2.0.1",
|
||||
"author": "Matthias",
|
||||
"license": "CC0-1.0",
|
||||
"type": "module",
|
||||
|
|
Loading…
Reference in New Issue