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.DefaultHandler = options.DefaultHandler || DefaultHandler;
|
||||||
|
|
||||||
this.certificates = {};
|
this.certificates = {};
|
||||||
|
//TODO: change the host option to "virtualHosts" or something
|
||||||
if (options.hasOwnProperty("host")) {
|
if (options.hasOwnProperty("host")) {
|
||||||
this.usingSNI = true;
|
this.usingSNI = true;
|
||||||
for (const certificate of options.host) {
|
for (const certificate of options.host) {
|
||||||
|
@ -64,9 +65,14 @@ export default class GeminiServer {
|
||||||
|
|
||||||
registerPath(basePath, handler) {
|
registerPath(basePath, handler) {
|
||||||
let hostname;
|
let hostname;
|
||||||
|
//TODO: What happens if we get an empty basepath?
|
||||||
if (!basePath.startsWith("/")) {
|
if (!basePath.startsWith("/")) {
|
||||||
//Then we're dealing with a hostname
|
//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
|
// Make sure it's a configured hostname
|
||||||
if (!Object.keys(this.certificates).includes(hostname)) {
|
if (!Object.keys(this.certificates).includes(hostname)) {
|
||||||
|
@ -103,10 +109,28 @@ export default class GeminiServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRequest(url, socket) {
|
handleRequest(url, socket) {
|
||||||
|
let urlPath;
|
||||||
//In the case of "gemini://example.com" url.pathname is "", which is a problem when we later .resolve
|
//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) {
|
for (const p of this.pathRegistry) {
|
||||||
//TODO: Wildcard hostnames
|
|
||||||
if (wildHostMatches(p.hostname, socket.servername)) {
|
if (wildHostMatches(p.hostname, socket.servername)) {
|
||||||
let matches;
|
let matches;
|
||||||
if (p.handler.matchesSubpaths) {
|
if (p.handler.matchesSubpaths) {
|
||||||
|
@ -119,10 +143,10 @@ export default class GeminiServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matches) {
|
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.write(res);
|
||||||
socket.end();
|
socket.end();
|
||||||
});
|
}).catch(console.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,13 +193,13 @@ export default class GeminiServer {
|
||||||
console.log("Connection, looking for " + socket.servername);
|
console.log("Connection, looking for " + socket.servername);
|
||||||
socket.setEncoding("utf8");
|
socket.setEncoding("utf8");
|
||||||
|
|
||||||
let requestLine = ""
|
let requestLine = "";
|
||||||
socket.on("data", d => {
|
socket.on("data", d => {
|
||||||
requestLine += d;
|
requestLine += d;
|
||||||
//If we have the end of the request
|
//If we have the end of the request
|
||||||
if (requestLine.includes("\r\n")) {
|
if (requestLine.includes("\r\n")) {
|
||||||
console.log(`< ${requestLine.trim()}\\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) {
|
if (requestLine.indexOf("\r\n") !== requestLine.length - 2) {
|
||||||
socket.write("59 Bad Request\r\n");
|
socket.write("59 Bad Request\r\n");
|
||||||
socket.end();
|
socket.end();
|
||||||
|
|
|
@ -38,3 +38,10 @@ openssl req -new -sha256 -key private-key.pem -out csr.pem
|
||||||
# Self-sign, generating cert
|
# Self-sign, generating cert
|
||||||
openssl x509 -req -in csr.pem -signkey private-key.pem -out public-cert.pem
|
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 GeminiServer from "./GeminiServer.js";
|
||||||
import StaticHandler from "./handlers/static.js";
|
import StaticHandler from "./handlers/static.js";
|
||||||
import DefaultHandler from "./handlers/default.js";
|
import DefaultHandler from "./handlers/default.js";
|
||||||
|
import ReverseProxyHandler from "./handlers/revproxy.js";
|
||||||
|
|
||||||
// import {, staticFileHandler} from "./main.js";
|
// import {, staticFileHandler} from "./main.js";
|
||||||
|
|
||||||
|
@ -70,6 +71,8 @@ server.registerPath("localhost/static", new StaticHandler("/tmp/content/" /*, {o
|
||||||
|
|
||||||
//Kinda cursed but whatever
|
//Kinda cursed but whatever
|
||||||
// server.registerPath("localhost/thoughts", proxyHandler("gemini://localhost:8888"));
|
// 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();
|
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
|
// This is the handler
|
||||||
|
// url is the URL object, with url.pathname being the path. p is the 'client-side' basepath
|
||||||
handle (url, p) {
|
handle (url, p) {
|
||||||
const relativePath = path.relative(p, url.pathname);
|
const relativePath = path.relative(p, url.pathname);
|
||||||
//Concat and normalize the passed URL as being relative to the base path
|
//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) {
|
}catch (e) {
|
||||||
//If the file doesn't exist
|
//If the file doesn't exist
|
||||||
|
console.log(`File doesn't exist or can't be opened.`)
|
||||||
return "51 Not Found\r\n";
|
return "51 Not Found\r\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
7
main.js
7
main.js
|
@ -1,4 +1,9 @@
|
||||||
import GeminiServer from "./GeminiServer.js"
|
import GeminiServer from "./GeminiServer.js"
|
||||||
import StaticHandler from "./handlers/static.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",
|
"name": "astronomical-theater",
|
||||||
"version": "v1.0.0",
|
"version": "v2.0.1",
|
||||||
"author": "Matthias",
|
"author": "Matthias",
|
||||||
"license": "CC0-1.0",
|
"license": "CC0-1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
Loading…
Reference in New Issue