Compare commits

...

5 Commits

Author SHA1 Message Date
Matthias Portzel 6a412a2dec Update README 2022-10-09 22:40:17 -04:00
Matthias Portzel c5f4d281d5 Reverse proxy fixes and changes 2022-09-24 20:35:14 -04:00
Matthias Portzel 3063358ecf Move to Unlicense 2022-02-28 11:12:36 -05:00
MatthiasSaihttam 34ef03a43c Make hostname matching case insensitive 2021-11-15 10:44:52 -05:00
MatthiasSaihttam 2dabbc0969 Non gemtext static files
* Static file handler now attempts to use "file-type" if it is available 
to read the magic bytes of a file and get Mime type
* Fixed a crash if the client closed the socket prematurely
* Move README to gmi
2021-10-09 21:05:56 -04:00
10 changed files with 369 additions and 16 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
*.pem
node_modules

View File

@ -17,10 +17,10 @@ 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("."))
return wildHost.toLowerCase().slice(wildHost.indexOf(".")) === hostInstance.toLowerCase().slice(hostInstance.indexOf("."))
}else {
//If there's no wildcard, just return if they match
return wildHost === hostInstance;
return wildHost.toLowerCase() === hostInstance.toLowerCase();
}
}
@ -144,7 +144,9 @@ export default class GeminiServer {
if (matches) {
Promise.resolve(p.handler.handle(url, p.basePath, socket)).then(res => {
socket.write(res);
if (res) {
socket.write(res);
}
socket.end();
}).catch(console.error);
return;
@ -220,6 +222,9 @@ export default class GeminiServer {
}
}
});
// This can be just ECONNRESET
socket.on("error", console.error);
});
server.listen(1965);
}

22
LICENSE Normal file
View File

@ -0,0 +1,22 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
The software is provided "as is", without warranty of any kind,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose and noninfringement.
In no event shall the authors be liable for any claim, damages or
other liability, whether in an action of contract, tort or otherwise,
arising from, out of or in connection with the software or the use or
other dealings in the software.

View File

@ -1,3 +1,15 @@
# Astronomical Theater
A Node.js Gemini server and proxy.
## Do not use.
This is known to be buggy.
I use it because the promise of Gemini is that people are able to write their own software, and that's an ideal that I believe in.
And I open-source it because I believe code should be free.
### Known bugs
* It reads the certificates into memory at start up, and never again. This means that if you (like me) use Let's Encrypt and rotate certs, then after three months the server needs to be restarted.
* It suffers from a Node JS bug which causes it to allocate ~10.8GB of virtual memory (not actual memory so this theoretically doesn't matter).
* Most importantly, it sometimes drops requests that it's proxying before they've completed. I kind of suspect this is also a Node.js bug, but I don't have a minimum reproduction case.
### We require SNI!
The Gemini spec requires clients to implement SNI. This server requires SNI to connect.
@ -23,7 +35,7 @@ A `relativePath` is the difference between a `basePath` and a `urlPath`. It's no
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
A path in the pathRegistry is a `p`, which is an object with a hostname, a basePath, and a handler
@ -37,6 +49,12 @@ openssl genrsa -out private-key.pem 2048
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
# For debugging clients, it can be useful to start a openssl server with these certs:
openssl s_server -key private-key.pem -cert public-cert.pem -accept 1965
# And of course creating a client with openssl. The -servername is needed for SNI
openssl s_client -connect example.com:1965 -servername example.com
```
@ -44,4 +62,4 @@ openssl x509 -req -in csr.pem -signkey private-key.pem -out public-cert.pem
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.
Similarly, SNI is not supported with IP addresses.

View File

@ -4,6 +4,8 @@ import DefaultHandler from "./handlers/default.js";
import ReverseProxyHandler from "./handlers/revproxy.js";
// import {, staticFileHandler} from "./main.js";
// If you'r going to be reverse-proxying a server with a self-signed cert, you need
process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0;
const server = new GeminiServer({
// port: 1965
@ -57,8 +59,8 @@ 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/about.gmi", new StaticHandler("README.gmi" /*, {options}*/));
server.registerPath("localhost/static", new StaticHandler("static/" /*, {options}*/));
// server.registerPath("localhost/static", new StaticHandler("/tmp/yourmom/" /*, {options}*/));
//The file passed to CGI handler must exist and be executable at run time

View File

@ -81,8 +81,10 @@ export default class ReverseProxyHandler extends DefaultHandler {
console.log(`Attempting to proxy ${toServe}.`);
try {
await geminiReq(toServe, socket)
return "";
// Add back url.search
await geminiReq(toServe + url.search, socket);
// geminiReq has already handled writing data back to the stream
return false;
}catch (err) {
console.log("Something went wrong with the proxy.");
console.error(err);

View File

@ -3,6 +3,9 @@ import * as fs from "fs";
import DefaultHandler from "./default.js";
let fileTypeFromFile;
import("file-type").then(ft => fileTypeFromFile = ft.default.fromFile).catch(console.error);
export default class StaticHandler extends DefaultHandler {
constructor(basePath, directoryList = false) {
super();
@ -18,7 +21,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) {
async handle (url, p, socket) {
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.basePath, relativePath);
@ -32,7 +35,7 @@ export default class StaticHandler extends DefaultHandler {
if (fs.statSync(toServe).isDirectory()) {
console.log(`File is a directory and directoryList is ${this.directoryList}.`);
if (this.directoryList) {
//TODO: Static content directory listing
} else {
return "51 Not Found\r\n";
}
@ -43,9 +46,31 @@ export default class StaticHandler extends DefaultHandler {
return "51 Not Found\r\n";
}
//TODO: import mmmagic
let mimeType = "text/gemini";
if (fileTypeFromFile) {
const type = await fileTypeFromFile(toServe);
// `type` will be undefined if the file is plaintext
// In which case we want to fall down to serving plaintext
if (type) {
mimeType = type.mime;
}
}
//TODO: some file-ending based logic, please
//So we can serve md and txt
//TODO: convert line endings
const data = fs.readFileSync(toServe);
return "20 text/gemini\r\n" + data;
// I read somewhere that `readFileSync` on every request is a bad idea, but I don't care
socket.write(`20 ${mimeType}\r\n`);
const fileReader = fs.createReadStream(toServe);
//This function (handle) needs to return after we're done writing the file to the stream
//Because the place that called this is going to handle .end() the socket
//So we don't end the socket here, and we make a promise to wait for the reader to end before returning
//It would be great if we could just not return from this function but I don't think that's how this works
fileReader.pipe(socket, {end: false});
await new Promise((res, rej) => fileReader.on("end", res));
return "";
}
}

275
package-lock.json generated Normal file
View File

@ -0,0 +1,275 @@
{
"name": "astronomical-theater",
"version": "v2.0.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "astronomical-theater",
"version": "v2.0.5",
"license": "CC0-1.0",
"optionalDependencies": {
"file-type": "^16.5.3"
}
},
"node_modules/@tokenizer/token": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
"optional": true
},
"node_modules/file-type": {
"version": "16.5.3",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.3.tgz",
"integrity": "sha512-uVsl7iFhHSOY4bEONLlTK47iAHtNsFHWP5YE4xJfZ4rnX7S1Q3wce09XgqSC7E/xh8Ncv/be1lNoyprlUH/x6A==",
"optional": true,
"dependencies": {
"readable-web-to-node-stream": "^3.0.0",
"strtok3": "^6.2.4",
"token-types": "^4.1.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/file-type?sponsor=1"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"optional": true
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"optional": true
},
"node_modules/peek-readable": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.0.1.tgz",
"integrity": "sha512-7qmhptnR0WMSpxT5rMHG9bW/mYSR1uqaPFj2MHvT+y/aOUu6msJijpKt5SkTDKySwg65OWG2JwTMBlgcbwMHrQ==",
"optional": true,
"engines": {
"node": ">=8"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"optional": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/readable-web-to-node-stream": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz",
"integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==",
"optional": true,
"dependencies": {
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"optional": true
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"optional": true,
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/strtok3": {
"version": "6.2.4",
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.2.4.tgz",
"integrity": "sha512-GO8IcFF9GmFDvqduIspUBwCzCbqzegyVKIsSymcMgiZKeCfrN9SowtUoi8+b59WZMAjIzVZic/Ft97+pynR3Iw==",
"optional": true,
"dependencies": {
"@tokenizer/token": "^0.3.0",
"peek-readable": "^4.0.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/token-types": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/token-types/-/token-types-4.1.1.tgz",
"integrity": "sha512-hD+QyuUAyI2spzsI0B7gf/jJ2ggR4RjkAo37j3StuePhApJUwcWDjnHDOFdIWYSwNR28H14hpwm4EI+V1Ted1w==",
"optional": true,
"dependencies": {
"@tokenizer/token": "^0.3.0",
"ieee754": "^1.2.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"optional": true
}
},
"dependencies": {
"@tokenizer/token": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
"optional": true
},
"file-type": {
"version": "16.5.3",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.3.tgz",
"integrity": "sha512-uVsl7iFhHSOY4bEONLlTK47iAHtNsFHWP5YE4xJfZ4rnX7S1Q3wce09XgqSC7E/xh8Ncv/be1lNoyprlUH/x6A==",
"optional": true,
"requires": {
"readable-web-to-node-stream": "^3.0.0",
"strtok3": "^6.2.4",
"token-types": "^4.1.1"
}
},
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"optional": true
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"optional": true
},
"peek-readable": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.0.1.tgz",
"integrity": "sha512-7qmhptnR0WMSpxT5rMHG9bW/mYSR1uqaPFj2MHvT+y/aOUu6msJijpKt5SkTDKySwg65OWG2JwTMBlgcbwMHrQ==",
"optional": true
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"optional": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
},
"readable-web-to-node-stream": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz",
"integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==",
"optional": true,
"requires": {
"readable-stream": "^3.6.0"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"optional": true
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"optional": true,
"requires": {
"safe-buffer": "~5.2.0"
}
},
"strtok3": {
"version": "6.2.4",
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.2.4.tgz",
"integrity": "sha512-GO8IcFF9GmFDvqduIspUBwCzCbqzegyVKIsSymcMgiZKeCfrN9SowtUoi8+b59WZMAjIzVZic/Ft97+pynR3Iw==",
"optional": true,
"requires": {
"@tokenizer/token": "^0.3.0",
"peek-readable": "^4.0.1"
}
},
"token-types": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/token-types/-/token-types-4.1.1.tgz",
"integrity": "sha512-hD+QyuUAyI2spzsI0B7gf/jJ2ggR4RjkAo37j3StuePhApJUwcWDjnHDOFdIWYSwNR28H14hpwm4EI+V1Ted1w==",
"optional": true,
"requires": {
"@tokenizer/token": "^0.3.0",
"ieee754": "^1.2.1"
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"optional": true
}
}
}

View File

@ -1,11 +1,14 @@
{
"name": "astronomical-theater",
"version": "v2.0.5",
"version": "v2.1.0",
"author": "Matthias",
"license": "CC0-1.0",
"license": "UNLICENSE",
"type": "module",
"main": "main.js",
"scripts": {
"start": "node main.js"
},
"optionalDependencies": {
"file-type": "^16.5.3"
}
}

BIN
static/small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB