├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── examplePages ├── index.gemini └── test.gemini ├── examples └── example.ts ├── gemini.sh ├── lib ├── Request.ts ├── Response.ts ├── TitanRequest.ts ├── index.ts ├── middleware.ts └── status.ts ├── makeKeys ├── package-lock.json ├── package.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | *.pem 10 | *~ 11 | .DS_Store 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # Snowpack dependency directory (https://snowpack.dev/) 49 | web_modules/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | .parcel-cache 82 | 83 | # Next.js build output 84 | .next 85 | out 86 | 87 | # Nuxt.js build / generate output 88 | .nuxt 89 | dist 90 | 91 | # Gatsby files 92 | .cache/ 93 | # Comment in the public line in if your project uses Gatsby and not Next.js 94 | # https://nextjs.org/blog/next-9-1#public-directory-support 95 | # public 96 | 97 | # vuepress build output 98 | .vuepress/dist 99 | 100 | # Serverless directories 101 | .serverless/ 102 | 103 | # FuseBox cache 104 | .fusebox/ 105 | 106 | # DynamoDB Local files 107 | .dynamodb/ 108 | 109 | # TernJS port file 110 | .tern-port 111 | 112 | # Stores VSCode versions used for testing VSCode extensions 113 | .vscode-test 114 | 115 | # yarn v2 116 | .yarn/cache 117 | .yarn/unplugged 118 | .yarn/build-state.yml 119 | .yarn/install-state.gz 120 | .pnp.* 121 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | dist/examples -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) [year], [fullname] 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gemini Server 2 | 3 | This is a server framework in Node.js for the 4 | [Gemini Protocol](https://gemini.circumlunar.space/) based on Express. 5 | 6 | Typescript type definitions are included. 7 | 8 | TLS is a required part of the Gemini protocol. You can generate 9 | keys/certificates using 10 | `openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=localhost'` 11 | 12 | ## Install 13 | 14 | `npm install --save gemini-server` 15 | 16 | ## Usage 17 | 18 | ### Create a gemini server 19 | 20 | ```javascript 21 | const gemini = require("gemini-server").default; 22 | 23 | const app = gemini(options); 24 | ``` 25 | 26 | where options is an object with the following properties: 27 | 28 | cert: certificate file key: key file 29 | 30 | ### Handle a request to a route 31 | 32 | This uses the express conventions, with the modification that the only method is 33 | `on`. 34 | 35 | ```javascript 36 | app.on(path, handler); 37 | ``` 38 | 39 | or 40 | 41 | ```javascript 42 | app.on(path, ...middleware, handler); 43 | ``` 44 | 45 | Where `handler` is a function which takes in a `Request` and a `Response`, and 46 | returns nothing. And `middleware` is 0 or more functions which take 47 | `(req, res, next)` and return nothing. A middleware function should call 48 | `next()` to continue onto the next function for handling. It should not call 49 | `next` and should instead set the `res` object if it should return. NOTE: 50 | middleware looks similar to express middleware, but isn't compatible. 51 | 52 | Examples of path: `/` for the root of the site `/foo` `/foo/bar` `/user/:name` 53 | `*` 54 | 55 | Examples of handler: `function(req, res){ // do stuff }` general form 56 | `gemini.static(dir)` serve a directory `gemini.redirect(url)` redirect to a url 57 | 58 | Examples of middleware: `gemini.requireInput(prompt="Input requested")` Proceed 59 | to handler only if user input is given, otherwise request input. 60 | 61 | `gemini.requireCert` Proceed to handler only if user certificate is provided, 62 | otherwise request a certificate. 63 | 64 | ### Listen for connections 65 | ```javascript 66 | app.listen(); 67 | ``` 68 | or 69 | ```javascript 70 | app.listen(port); 71 | ``` 72 | or 73 | ```javascript 74 | app.listen(callback); 75 | ``` 76 | 77 | 78 | ### Static 79 | 80 | `gemini.serveStatic(path, ?options)` serves the files in a directory 81 | | Options | Description | default | 82 | |---------|-------------|---------| 83 | |index|Serves files named `index` with extensions specified in `indexExtensions` when accessing a directory without specifying any file|`true` 84 | |indexExtensions|Defines the extensions to be served by the option `index`|`['.gemini', '.gmi']` 85 | |redirectOnDirectory|Redirects an user to URL with `/` appended if the supplied path is the name of a directory|`true` 86 | 87 | If another handler/middleware is chained behind the static middleware it will get called in case of a file/directory being unaccessable, making it possible to supply a custom "Not Found"-pages. 88 | 89 | ```javascript 90 | app.on("/someFiles", gemini.serveStatic("src/files")); 91 | ``` 92 | 93 | ### Redirect 94 | 95 | `gemini.redirect(url)` will redirect to the specified url 96 | 97 | ```javascript 98 | app.on("/redirectMe", gemini.redirect("/goingHereInstead")); 99 | 100 | app.on("/redirectOther", gemini.redirect("http://example.com")); 101 | ``` 102 | 103 | ### Request object 104 | 105 | The request object is passed to request handlers. 106 | `req.url` The URL object of the request 107 | `req.path` The path component of the url 108 | `req.query` The query component of the url (used for handling input) 109 | `req.params` The params of a matched route 110 | `req.cert` The certificate object, if the client sent one 111 | `req.fingerprint` The fingerprint of the certificate object, if the client sent one 112 | 113 | ### Response object 114 | 115 | The response object is passed to request handlers. Methods on it can be chained, 116 | as each both returns and modifies the request object. `res.status(s)` Set the 117 | status of the response (see the gemini docs). s is an int `res.getStatus()` 118 | return the current status associated with the response 119 | 120 | `res.file(filename)` serve the specified file 121 | 122 | `res.error(status, message)` or `res.error(message)` Alert the client of an error in processing 123 | 124 | `res.data(d)` or `res.data(d, mimeType='text/plain')` Serve raw data as text, or 125 | as whatever format you want if you specify the mime type. 126 | 127 | `res.input(prompt, sensitive=false)` Prompt the client for input. Prompt should 128 | be a string. `sensitive` defaults to false, and should be true if the input is 129 | sensitive. 130 | 131 | `res.certify(info="Please include a certificate.")` Request a certificate from 132 | the client. Useful for sessions or login. Optional info message. 133 | 134 | `res.redirect(url)` Redirect the client to the specified url. 135 | 136 | ### Middleware 137 | 138 | A middleware registered through `app.use` will be executed before every route 139 | handler. If a path is given as the first argument, only that path will be 140 | affected. 141 | 142 | ```javascript 143 | app.use((req, res, next) => { 144 | console.log(`Route ${req.path} was called`); 145 | }); 146 | app.use("/foo", (req, res, next) => { 147 | console.log(`Foo route was called`); 148 | }); 149 | ``` 150 | 151 | ## Titan 152 | We include support for the [titan protocol](https://communitywiki.org/wiki/Titan), a sister protocol to gemini. This can be enabled by creating the gemini server with 153 | ```javascript 154 | const options = { 155 | cert: readFileSync("cert.pem"), 156 | key: readFileSync("key.pem"), 157 | titanEnabled: true 158 | }; 159 | 160 | const app = gemini(options); 161 | ``` 162 | 163 | or simply by defining a titan route. 164 | 165 | `app.titan` can be used similarly to `app.on`, to include handlers for titan routes. 166 | In such a handler, the request object will also have the following properties: 167 | `data: Buffer | null` 168 | `uploadSize: number` 169 | `token: string | null` 170 | `mimeType: string | null` 171 | 172 | `app.use` will also work for titan routes. 173 | 174 | ## Example Server (Typescript) 175 | 176 | ```typescript 177 | import { readFileSync } from "fs"; 178 | import gemini, { Request, Response, TitanRequest, NextFunction } from "../lib/index"; 179 | 180 | const options = { 181 | cert: readFileSync("cert.pem"), 182 | key: readFileSync("key.pem"), 183 | titanEnabled: true 184 | }; 185 | 186 | const app = gemini(options); 187 | 188 | app.use((req: Request, _res: Response, next: NextFunction) => { 189 | console.log("Handling path", req.path); 190 | next(); 191 | }); 192 | 193 | app.on("/", (_req: Request, res: Response) => { 194 | res.file("examplePages/test.gemini"); 195 | }); 196 | 197 | app.on("/input", (req: Request, res: Response) => { 198 | if (req.query) { 199 | res.data("you typed " + req.query); 200 | } else { 201 | res.input("type something"); 202 | } 203 | }); 204 | 205 | app.on("/paramTest/:foo", (req: Request, res: Response) => { 206 | res.data("you went to " + req.params.foo); 207 | }); 208 | 209 | app.on("/async", async (req: Request, res: Response) => { 210 | if (req.query) { 211 | return new Promise(r => { 212 | setTimeout(r, 500); 213 | }).then(() => { 214 | res.data("you typed " + req.query); 215 | }); 216 | } else { 217 | res.input("type something"); 218 | } 219 | }); 220 | 221 | app.on( 222 | "/testMiddleware", 223 | gemini.requireInput("enter something"), 224 | (req: Request, res: Response) => { 225 | res.data("thanks. you typed " + req.query); 226 | }, 227 | ); 228 | 229 | app.on("/other", (_req: Request, res: Response) => { 230 | res.data("welcome to the other page"); 231 | }); 232 | 233 | app.use("/static", gemini.serveStatic("./examplePages")); 234 | 235 | app.on("/redirectMe", gemini.redirect("/other")); 236 | 237 | app.on("/cert", (req: Request, res: Response) => { 238 | if (!req.fingerprint) { 239 | res.certify(); 240 | } else { 241 | res.data("thanks for the cert"); 242 | } 243 | }); 244 | 245 | app.on("/protected", gemini.requireCert, (_req: Request, res: Response) => { 246 | res.data("only clients with certificates can get here"); 247 | }); 248 | 249 | app.titan("/titan", (req: TitanRequest, res: Response) => { 250 | console.log(req); 251 | res.data("Titan Data: \n" + req.data?.toString("utf-8")); 252 | }); 253 | 254 | app.titan("/titanCert", gemini.requireCert, (req: TitanRequest, res: Response) => { 255 | res.data("You can use gemini middleware in a titan request"); 256 | }); 257 | 258 | app.on("/titan", (_req: Request, res: Response) => { 259 | res.data("not a titan request!"); 260 | }); 261 | 262 | app.use("/titan", (req: Request | TitanRequest, _res: Response, next: () => void) => { 263 | console.log(req.constructor.name); 264 | console.log(`Is TitanRequest? ${req instanceof TitanRequest}`) 265 | next(); 266 | }); 267 | 268 | app.listen(() => { 269 | console.log("Listening..."); 270 | }); 271 | ``` 272 | 273 | ## Todo 274 | 275 | - [x] Documentation 276 | - [x] Utility functions 277 | - [x] Static directory serving 278 | - [x] automatic index file 279 | - [x] Certificates 280 | - [x] Middleware support 281 | - [ ] Session helper functions 282 | - [ ] Router 283 | - [ ] View engines 284 | -------------------------------------------------------------------------------- /examplePages/index.gemini: -------------------------------------------------------------------------------- 1 | example index file -------------------------------------------------------------------------------- /examplePages/test.gemini: -------------------------------------------------------------------------------- 1 | hi. this is a test 2 | 3 | # squish 4 | foobar 5 | 6 | => gemini://gemini.circumlunar.space test link 7 | 8 | * fish 9 | * foosh 10 | * fwish 11 | 12 | > quote example 13 | -------------------------------------------------------------------------------- /examples/example.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs"; 2 | import gemini, { Request, Response, TitanRequest, NextFunction } from "../lib"; 3 | 4 | const options = { 5 | cert: readFileSync("cert.pem"), 6 | key: readFileSync("key.pem"), 7 | titanEnabled: true 8 | }; 9 | 10 | const app = gemini(options); 11 | 12 | app.use((req: Request, _res: Response, next: NextFunction) => { 13 | console.log("Handling path", req.path); 14 | next(); 15 | }); 16 | 17 | app.on("/", (_req: Request, res: Response) => { 18 | res.file("examplePages/test.gemini"); 19 | }); 20 | 21 | app.on("/input", (req: Request, res: Response) => { 22 | if (req.query) { 23 | res.data("you typed " + req.query); 24 | } else { 25 | res.input("type something"); 26 | } 27 | }); 28 | 29 | app.on("/paramTest/:foo", (req: Request, res: Response) => { 30 | res.data("you went to " + req.params.foo); 31 | }); 32 | 33 | app.on("/async", async (req: Request, res: Response) => { 34 | if (req.query) { 35 | return new Promise(r => { 36 | setTimeout(r, 500); 37 | }).then(() => { 38 | res.data("you typed " + req.query); 39 | }); 40 | } else { 41 | res.input("type something"); 42 | } 43 | }); 44 | 45 | app.on( 46 | "/testMiddleware", 47 | gemini.requireInput("enter something"), 48 | (req: Request, res: Response) => { 49 | res.data("thanks. you typed " + req.query); 50 | }, 51 | ); 52 | 53 | app.on("/other", (_req: Request, res: Response) => { 54 | res.data("welcome to the other page"); 55 | }); 56 | 57 | app.use("/static", gemini.serveStatic("./examplePages")); 58 | 59 | app.on("/redirectMe", gemini.redirect("/other")); 60 | 61 | app.on("/cert", (req: Request, res: Response) => { 62 | if (!req.fingerprint) { 63 | res.certify(); 64 | } else { 65 | res.data("thanks for the cert"); 66 | } 67 | }); 68 | 69 | app.on("/protected", gemini.requireCert, (_req: Request, res: Response) => { 70 | res.data("only clients with certificates can get here"); 71 | }); 72 | 73 | app.titan("/titan", (req: TitanRequest, res: Response) => { 74 | console.log(req); 75 | res.data("Titan Data: \n" + req.data?.toString("utf-8")); 76 | }); 77 | 78 | app.titan("/titanCert", gemini.requireCert, (req: TitanRequest, res: Response) => { 79 | res.data("You can use gemini middleware in a titan request"); 80 | }); 81 | 82 | app.on("/titan", (_req: Request, res: Response) => { 83 | res.data("not a titan request!"); 84 | }); 85 | 86 | app.use("/titan", (req: Request | TitanRequest, _res: Response, next: NextFunction) => { 87 | console.log(req.constructor.name); 88 | console.log(`Is TitanRequest? ${req instanceof TitanRequest}`) 89 | next(); 90 | }); 91 | 92 | // app.on("*", (req, res) => { 93 | // res.data("nyaa"); 94 | // }); 95 | app.listen(() => { 96 | console.log("Listening..."); 97 | }); 98 | -------------------------------------------------------------------------------- /gemini.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (C) 2001-2020 Alex Schroeder 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, but 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Gemini and Titan 18 | # 19 | # This code declares two bash functions with which to read and write 20 | # Gemini sites. 21 | # 22 | # Here's how to read "gemini://alexschroeder/Test": 23 | # 24 | # gemini gemini://alexschroeder.ch:1965/Test 25 | # 26 | # The scheme and port are optional: 27 | # 28 | # gemini alexschroeder.ch/Test 29 | # 30 | # Here's how to edit "titan://alexschroeder.ch/raw/Test" (the exact 31 | # URLs to use depend on the site): 32 | # 33 | # echo hello | titan titan://alexschroeder.ch:1965/raw/Test hello 34 | # 35 | # Again, the scheme and port are optional: 36 | # 37 | # date | titan alexschroeder.ch/raw/Test hello 38 | # 39 | # You can also post a text file: 40 | # 41 | # titan alexschroeder.ch/raw/Test hello test.txt 42 | # 43 | # So there's your workflow: 44 | # 45 | # gemini alexschroeder.ch/Test > test.txt 46 | # vim test.txt 47 | # titan alexschroeder.ch/raw/Test hello test.txt 48 | # 49 | # To install, source this file from your ~/.bashrc file: 50 | # 51 | # source ~/src/gemini-titan/gemini.sh 52 | 53 | function gemini () { 54 | if [[ $1 =~ ^((gemini)://)?([^/:]+)(:([0-9]+))?/(.*)$ ]]; then 55 | gschema=${BASH_REMATCH[2]:-gemini} 56 | ghost=${BASH_REMATCH[3]} 57 | gport=${BASH_REMATCH[5]:-1965} 58 | gpath=${BASH_REMATCH[6]} 59 | echo Contacting $ghost:$gport... 60 | echo -e "$gschema://$ghost:$gport/$gpath\r\n" \ 61 | | openssl s_client -quiet -connect "$ghost:$gport" 2>/dev/null 62 | else 63 | echo $1 is not a Gemini URL 64 | fi 65 | } 66 | 67 | function titan () { 68 | if [[ -z "$2" ]]; then 69 | echo Usage: titan URL TOKEN [FILE] 70 | return 71 | else 72 | gtoken=$2 73 | fi 74 | if [[ "$1" =~ ^((titan)://)?([^/:]+)(:([0-9]+))?/(.*)$ ]]; then 75 | gschema=${BASH_REMATCH[2]:-titan} 76 | ghost=${BASH_REMATCH[3]} 77 | gport=${BASH_REMATCH[5]:-1965} 78 | gpath=${BASH_REMATCH[6]} 79 | gremove=0 80 | if [[ -z "$3" ]]; then 81 | echo Type you text and end your input with Ctrl+D 82 | gfile=$(mktemp) 83 | gremove=1 84 | cat - > "$gfile" 85 | else 86 | gfile="$3" 87 | fi 88 | gmime=$(file --brief --mime-type "$gfile") 89 | gsize=$(wc -c < "$gfile") 90 | echo Posting $gsize bytes of $gmime to $ghost:$gport... 91 | (echo -e "$gschema://$ghost:$gport/$gpath;token=$gtoken;mime=$gmime;size=$gsize\r"; cat "$gfile") \ 92 | | openssl s_client -quiet -connect $ghost:$gport 2>/dev/null 93 | if [[ $gremove == "1" ]]; then 94 | rm "$gfile" 95 | fi 96 | else 97 | echo $1 is not a Titan URL 98 | fi 99 | } 100 | -------------------------------------------------------------------------------- /lib/Request.ts: -------------------------------------------------------------------------------- 1 | import tls from "tls"; 2 | import url from "url"; 3 | 4 | export default class Request { 5 | url: url.URL; 6 | path: string | null; 7 | query: string | null; 8 | cert: tls.PeerCertificate | tls.DetailedPeerCertificate; 9 | fingerprint: string; 10 | params: Record; 11 | baseUrl: string; 12 | 13 | constructor(u: url.URL, c: tls.PeerCertificate | tls.DetailedPeerCertificate){ 14 | this.url = u; 15 | this.path = u.pathname; 16 | this.query = u.search?.slice(1) || null; 17 | this.cert = c; 18 | this.fingerprint = c.fingerprint; 19 | this.params = {}; 20 | this.baseUrl = ''; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/Response.ts: -------------------------------------------------------------------------------- 1 | import { status } from "./status"; 2 | import truncate from "truncate-utf8-bytes"; 3 | import mime from "mime"; 4 | import fs from "fs"; 5 | 6 | mime.define({ "text/gemini": ["gemini", "gmi"] }); 7 | 8 | export default class Response { 9 | _status: status = 20; 10 | _meta: string = ""; 11 | _body: Uint8Array | string | Buffer | null = null; 12 | 13 | _setMeta(m: string): void { 14 | this._meta = truncate(m, 1024); 15 | } 16 | constructor(status: status = 20, meta: string = "") { 17 | this._status = status; 18 | this._setMeta(meta); 19 | } 20 | 21 | status(s: status): Response { 22 | this._status = s; 23 | return this; 24 | } 25 | 26 | getStatus(): status { 27 | return this._status; 28 | } 29 | 30 | error(s: status = 40, msg: string) : Response { 31 | this.status(s); 32 | this._setMeta(msg); 33 | return this; 34 | } 35 | 36 | data(d: Uint8Array | string | Buffer, mimeType: string = "text/plain"): Response { 37 | this.status(20); 38 | this._body = d; 39 | this._setMeta(mimeType); 40 | return this; 41 | } 42 | //for success, The line is a MIME media type which applies to the response body. 43 | //for redirect, is a new URL for the requested resource. The URL may be absolute or relative. 44 | //for 4* and 5*, The contents of may provide additional information on the failure, and should be displayed to human users. 45 | file(filename: string): Response { // might throw error if file doesn't exist 46 | const mimetype = mime.getType(filename); 47 | if(mimetype == null){ 48 | console.error("mime type of file", filename, "not found"); 49 | return this; 50 | } else { 51 | this._body = fs.readFileSync(filename); 52 | this.status(20); 53 | this._setMeta(mimetype); 54 | return this; 55 | } 56 | } 57 | 58 | input(prompt: string, sensitive: boolean = false): Response { //client should re-request same url with input as a query param 59 | this.status(sensitive ? 11 : 10); 60 | this._setMeta(prompt); 61 | return this; 62 | } 63 | 64 | certify(info: string = "Please include a certificate."): Response { //request certificate from client 65 | this._setMeta(info); 66 | this.status(60); 67 | return this; 68 | } 69 | 70 | redirect(url: string): Response { 71 | this.status(30); 72 | this._setMeta(url); 73 | return this; 74 | } 75 | 76 | format_header(): string { 77 | return `${this._status} ${this._meta}\r\n`; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/TitanRequest.ts: -------------------------------------------------------------------------------- 1 | import Request from "./Request"; 2 | import tls from "tls"; 3 | import url from "url"; 4 | 5 | export default class TitanRequest extends Request { 6 | data: Buffer | null; 7 | uploadSize: number; 8 | token: string | null; 9 | mimeType: string | null; 10 | 11 | constructor(u: url.URL, c: tls.PeerCertificate | tls.DetailedPeerCertificate){ 12 | super(u, c); 13 | this.data = null; 14 | this.uploadSize = 0; 15 | this.token = null; 16 | this.mimeType = null; 17 | } 18 | } -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import tls from "tls"; 2 | import url, { URL } from "url"; 3 | import { 4 | middleware, 5 | serveStatic, 6 | titanMiddleware, 7 | } from "./middleware"; 8 | import { pathToRegexp, match, MatchFunction } from "path-to-regexp"; 9 | import Request from "./Request.js"; 10 | import Response from "./Response.js"; 11 | import TitanRequest from "./TitanRequest"; 12 | import truncate from "truncate-utf8-bytes"; 13 | 14 | type Route = { 15 | regexp: RegExp | null; 16 | match: MatchFunction>; 17 | handlers: middleware[]; 18 | fast_star: boolean; 19 | mountPath: string | null; 20 | } 21 | 22 | type RouteNoHandlers = Omit; 23 | 24 | type titanParams = { size: number, mime: string | null, token: string | null } 25 | 26 | function starMatch() { 27 | return { 28 | path: "*", 29 | index: 0, 30 | params: {} 31 | }; 32 | } 33 | 34 | class Server { 35 | _key: string | Buffer | Array | undefined; 36 | _cert: string | Buffer | Array | undefined; 37 | _stack: Route[]; 38 | _titanStack: Route[]; 39 | _middlewares: Route[]; 40 | _titanEnabled: boolean; 41 | 42 | constructor(key: string | Buffer | Array, 43 | cert: string | Buffer | Array, 44 | titanEnabled: boolean = false) { 45 | this._key = key; 46 | this._cert = cert; 47 | this._stack = []; 48 | this._titanStack = []; 49 | this._middlewares = []; 50 | this._titanEnabled = titanEnabled; 51 | } 52 | 53 | listen(port: number, callback?: (() => void)): tls.Server; 54 | listen(callback?: (() => void)): tls.Server; 55 | listen(portOrCallback: number | (() => void) = 1965, callback?: (() => void)): tls.Server { 56 | let port = 1965; 57 | if (typeof portOrCallback === "number") { 58 | port = portOrCallback; 59 | } else { 60 | callback = portOrCallback; 61 | } 62 | //try catch the handler. if error, respond with error 63 | const s = tls.createServer({ 64 | key: this._key, 65 | cert: this._cert, 66 | requestCert: true, 67 | rejectUnauthorized: false, 68 | }, (conn) => { 69 | conn.on("error", (err) => { 70 | if (err && err.code === "ECONNRESET") return; 71 | console.error(err); 72 | }); 73 | const chunks: any = []; 74 | let byteCount = 0; 75 | let isURLReceived = false; 76 | let t: titanParams = { 77 | token: null, 78 | size: 0, 79 | mime: null 80 | }; 81 | let u: URL, ulength: number; 82 | let protocol: "gemini" | "titan" = "gemini"; 83 | conn.on("data", async (data: any) => { 84 | // Route Matcher, checks whether a route matches the path 85 | const getMatch = (route: RouteNoHandlers) => 86 | route.fast_star || 87 | route.regexp != null && route.match(u.pathname as string); //TODO: move this to after u is defined? 88 | const isMatch = (route: RouteNoHandlers) => getMatch(route) != false; 89 | 90 | byteCount += data.length 91 | // data is Buffer | String 92 | // data can be incomplete 93 | // Store data until we receive 94 | if (isURLReceived && byteCount < (ulength + t.size)) return; 95 | 96 | chunks.push(data); 97 | if (!data.toString("utf8").includes('\r\n')) return; 98 | 99 | //A url is at most 1024 bytes followed by 100 | let uStr = truncate(Buffer.concat(chunks).toString("utf-8").split(/\r\n/, 1)[0], 1024); 101 | let req: Request | TitanRequest; 102 | if (!u) { 103 | try { 104 | u = new url.URL(uStr.split(';')[0]); 105 | } catch (error) { 106 | conn.write("59 Invalid URL.\r\n"); 107 | conn.destroy(); 108 | return; 109 | } 110 | 111 | ulength = uStr.length + 2; 112 | isURLReceived = true; 113 | req = new Request(u, conn.getPeerCertificate()); 114 | if (!["gemini", "gemini:", "titan", "titan:"].includes(u.protocol as string) || ["titan", "titan:"].includes(u.protocol as string) && !this._titanEnabled) { 115 | //error 116 | conn.write("59 Invalid protocol.\r\n"); 117 | conn.destroy(); 118 | return; 119 | } 120 | if (["titan", "titan:"].includes(u.protocol as string)) { 121 | protocol = "titan"; 122 | } else { 123 | protocol = "gemini"; 124 | } 125 | } else { 126 | return; 127 | } 128 | if (protocol == "titan") { 129 | let titanreq = new TitanRequest(u, conn.getPeerCertificate()); 130 | let concatenatedBuffer = Buffer.concat(chunks); 131 | 132 | for (const param of uStr.split(';').slice(1)) { 133 | let [k, v] = param.split('='); 134 | if (k === "token" || k === "mime") { 135 | t[k] = v; 136 | } else if (k === "size") { 137 | t[k] = parseInt(v) || 0; 138 | } 139 | 140 | } 141 | if (this._titanStack.some(isMatch) && (byteCount < (ulength + t.size))) return; // Stop listening when no titan handler exists 142 | // console.log(titanreq.data.toString("utf-8")) 143 | if (t.size > 0) titanreq.data = Buffer.from(concatenatedBuffer.slice(concatenatedBuffer.indexOf("\r\n") + 2)); 144 | titanreq.uploadSize = t.size; 145 | titanreq.token = t.token; 146 | titanreq.mimeType = t.mime; 147 | req = titanreq; 148 | } else { 149 | req = new Request(u, conn.getPeerCertificate()); 150 | } 151 | 152 | const res = new Response(51, "Not Found."); 153 | const middlewares = this._middlewares.filter(isMatch); 154 | // const middlewareHandlers = middlewares.flatMap(({ handlers }) => 155 | // handlers 156 | // ); 157 | 158 | 159 | async function handle(handlers: middleware[], request: R) { 160 | if (handlers.length > 0) { 161 | await handlers[0](request, res, () => handle(handlers.slice(1), request)); 162 | } 163 | }; 164 | async function handleMiddleware(m: Route[], request: R) { 165 | if (m.length > 0) { 166 | request.baseUrl = m[0].mountPath || ''; 167 | await handle(m[0].handlers, request); 168 | await handleMiddleware(m.slice(1), request); 169 | } 170 | } 171 | 172 | await handleMiddleware(middlewares, req); 173 | 174 | // await handle(middlewareHandlers, req); 175 | 176 | if (protocol === "gemini") { 177 | for (const route of this._stack) { 178 | if (isMatch(route)) { 179 | let m = getMatch(route); 180 | if (typeof m !== "boolean") { 181 | req.params = m.params; 182 | } 183 | await handle(route.handlers, req); 184 | break; 185 | } 186 | } 187 | } else { 188 | for (const route of this._titanStack) { 189 | if (isMatch(route)) { 190 | let m = getMatch(route); 191 | if (typeof m !== "boolean") { 192 | req.params = m.params; 193 | } 194 | await handle(route.handlers, req as TitanRequest); 195 | break; 196 | } 197 | } 198 | } 199 | 200 | conn.write(res.format_header()); 201 | if (res.getStatus() == 20 && res._body != null) { 202 | //send body 203 | conn.write(res._body); 204 | conn.end(); 205 | } else { 206 | conn.destroy(); 207 | } 208 | }); 209 | }); 210 | 211 | return s.listen(port, callback); 212 | } 213 | 214 | on(path: string, ...handlers: middleware[]): void { 215 | this._stack.push({ 216 | regexp: path === "*" ? null : pathToRegexp(path, [], { 217 | sensitive: true, 218 | strict: false, 219 | end: true 220 | }), 221 | match: path === "*" 222 | ? starMatch 223 | : match(path, { encode: encodeURI, decode: decodeURIComponent }), 224 | handlers: handlers, 225 | fast_star: path === "*", 226 | mountPath: path 227 | }); 228 | } 229 | 230 | titan(path: string, ...handlers: titanMiddleware[]): void { 231 | this._titanEnabled = true; 232 | this._titanStack.push({ 233 | regexp: path === "*" ? null : pathToRegexp(path, [], { 234 | sensitive: true, 235 | strict: false, 236 | end: true 237 | }), 238 | match: path === "*" 239 | ? starMatch 240 | : match(path, { encode: encodeURI, decode: decodeURIComponent }), 241 | handlers: handlers, 242 | fast_star: path === "*", 243 | mountPath: path 244 | }); 245 | } 246 | 247 | //TODO: make use allow titan middleware? 248 | use(...params: middleware[]): void; 249 | use(path: string, ...params: middleware[]): void; 250 | use(pathOrMiddleware: string | middleware, ...params: middleware[]): void { 251 | if (typeof pathOrMiddleware == "string") { 252 | this._middlewares.push({ 253 | regexp: pathOrMiddleware !== "*" 254 | ? pathToRegexp(pathOrMiddleware, [], { 255 | sensitive: true, 256 | strict: false, 257 | end: false 258 | }) 259 | : null, 260 | match: pathOrMiddleware === "*" 261 | ? starMatch 262 | : match(pathOrMiddleware, { encode: encodeURI, decode: decodeURIComponent, end: false }), 263 | handlers: params, 264 | fast_star: pathOrMiddleware === "*", 265 | mountPath: pathOrMiddleware, 266 | }); 267 | } else { 268 | this._middlewares.push({ 269 | regexp: null, 270 | match: starMatch, 271 | handlers: [pathOrMiddleware, ...params], 272 | fast_star: true, 273 | mountPath: null 274 | }); 275 | } 276 | } 277 | } 278 | 279 | type ServerOptions = { 280 | key: string | Buffer | Array; 281 | cert: string | Buffer | Array; 282 | titanEnabled?: boolean 283 | } 284 | 285 | export default function GeminiServer({ key, cert, titanEnabled = false }: ServerOptions): Server { 286 | if (!key || !cert) { 287 | throw new Error("Must specify key and cert"); 288 | } 289 | return new Server(key, cert, titanEnabled); 290 | } 291 | 292 | export { default as Request } from "./Request"; 293 | export { default as TitanRequest } from "./TitanRequest"; 294 | export { default as Response } from "./Response"; 295 | export { titanMiddleware, middleware, NextFunction } from "./middleware"; 296 | export { status } from "./status"; 297 | 298 | import { redirect, requireInput, requireCert } from './middleware'; 299 | GeminiServer.redirect = redirect; 300 | GeminiServer.requireInput = requireInput; 301 | GeminiServer.requireCert = requireCert; 302 | GeminiServer.serveStatic = serveStatic; 303 | -------------------------------------------------------------------------------- /lib/middleware.ts: -------------------------------------------------------------------------------- 1 | import Request from "./Request"; 2 | import TitanRequest from "./TitanRequest"; 3 | import Response from "./Response"; 4 | import {promises as fs} from 'fs'; 5 | import path from 'path'; 6 | 7 | export type NextFunction = () => void; 8 | 9 | export type middleware = ( 10 | req: R, 11 | res: Response, 12 | next: NextFunction, 13 | ) => void; 14 | 15 | export type titanMiddleware = middleware; 16 | 17 | // export type middleware = geminiMiddleware | titanMiddleware; 18 | 19 | export function redirect(url: string): middleware { 20 | return function (req: Request, res: Response) { 21 | res.redirect(url); 22 | }; 23 | }; 24 | export function requireInput(prompt: string = "Input requested"): middleware { 25 | return function (req: Request, res: Response, next: NextFunction) { 26 | if (!req.query) { 27 | res.input(prompt); 28 | } else { 29 | next(); 30 | } 31 | }; 32 | }; 33 | 34 | export let requireCert: middleware = function (req: Request, res: Response, next: NextFunction) { 35 | if (!req.fingerprint) { 36 | res.certify(); 37 | } else { 38 | next(); 39 | } 40 | }; 41 | 42 | type serveStaticOptions = {index?: boolean, indexExtensions?: string[], redirectOnDirectory?: boolean} 43 | 44 | //TODO: make async, check for malicious paths 45 | export function serveStatic(basePath: string, opts?: serveStaticOptions) : middleware { 46 | let options = { index: true, indexExtensions: ['.gemini', '.gmi'], redirectOnDirectory: true, ...opts }; // apply default options 47 | return async function (req, res, next){ 48 | if (req.path != null && !/^[a-zA-Z0-9_\.\/-]+$/.test(req.path)) { 49 | res.error(59, "Forbidden characters in path"); 50 | return; 51 | } 52 | const filePath = req.path?.replace(req.baseUrl, '') || '/'; 53 | const safeSuffix = path.normalize(filePath).replace(/^(\.\.(\/|\\|$))+/, ''); 54 | let fullPath = path.join(basePath, safeSuffix); 55 | try { 56 | let stat = await fs.stat(fullPath); 57 | if (stat.isDirectory()) { 58 | if (!filePath.endsWith('/')) { 59 | if (options.redirectOnDirectory) { 60 | res.redirect(req.path + '/'); 61 | return; 62 | } else { 63 | throw Error("Not a file but a directory"); 64 | } 65 | } 66 | if (options.index) { 67 | let extension = -1; 68 | for(let i = 0; i < options.indexExtensions.length; i++) { 69 | try { 70 | let file = fullPath + 'index' + options.indexExtensions[i]; 71 | await fs.access(file); 72 | extension = i; 73 | break; 74 | } catch(ex) { 75 | } 76 | } 77 | if (extension !== -1) { 78 | res.file(fullPath + 'index' + options.indexExtensions[extension]); 79 | return; 80 | } 81 | } 82 | } 83 | if (stat.isFile()) { 84 | res.file(fullPath); 85 | return; 86 | } 87 | } catch (_e) { 88 | res.status(51); 89 | next(); 90 | } 91 | }; 92 | }; 93 | -------------------------------------------------------------------------------- /lib/status.ts: -------------------------------------------------------------------------------- 1 | export type status = 2 | | 10 //input 3 | | 11 //sensitive input 4 | | 20 //success 5 | | 30 //redirect - temporary 6 | | 31 //redirect - permanent 7 | | 40 //temporary failure 8 | | 41 //server unavailable 9 | | 42 //CGI error 10 | | 43 //proxy error 11 | | 44 //slow down 12 | | 50 //permanent failure 13 | | 51 //not found 14 | | 52 //gone 15 | | 53 //proxy request refused 16 | | 59 //bad request 17 | | 60 //client certificate required 18 | | 61 //client certificate not authorized 19 | | 62; //client certificate not valid 20 | -------------------------------------------------------------------------------- /makeKeys: -------------------------------------------------------------------------------- 1 | openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=localhost' 2 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gemini-server", 3 | "version": "2.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "gemini-server", 9 | "version": "1.8.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "mime": "^2.4.6", 13 | "path-to-regexp": "^6.2.0", 14 | "truncate-utf8-bytes": "^1.0.2" 15 | }, 16 | "devDependencies": { 17 | "@types/mime": "^2.0.3", 18 | "@types/node": "^17.0.19", 19 | "@types/truncate-utf8-bytes": "^1.0.0", 20 | "typescript": "^4.5.4" 21 | } 22 | }, 23 | "node_modules/@types/mime": { 24 | "version": "2.0.3", 25 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", 26 | "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==", 27 | "dev": true 28 | }, 29 | "node_modules/@types/node": { 30 | "version": "17.0.19", 31 | "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.19.tgz", 32 | "integrity": "sha512-PfeQhvcMR4cPFVuYfBN4ifG7p9c+Dlh3yUZR6k+5yQK7wX3gDgVxBly4/WkBRs9x4dmcy1TVl08SY67wwtEvmA==", 33 | "dev": true 34 | }, 35 | "node_modules/@types/truncate-utf8-bytes": { 36 | "version": "1.0.0", 37 | "resolved": "https://registry.npmjs.org/@types/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.0.tgz", 38 | "integrity": "sha512-pa3icl1RawLUUKmQE1h0G381EpWMU8bTnZT4YF3Ey9D7VI4fL1BQvgZ8HKbYok9kKXeUSCr/E3vnscW8Ck3QKg==", 39 | "dev": true 40 | }, 41 | "node_modules/mime": { 42 | "version": "2.4.6", 43 | "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", 44 | "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", 45 | "bin": { 46 | "mime": "cli.js" 47 | }, 48 | "engines": { 49 | "node": ">=4.0.0" 50 | } 51 | }, 52 | "node_modules/path-to-regexp": { 53 | "version": "6.2.0", 54 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.0.tgz", 55 | "integrity": "sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg==" 56 | }, 57 | "node_modules/truncate-utf8-bytes": { 58 | "version": "1.0.2", 59 | "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", 60 | "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=", 61 | "dependencies": { 62 | "utf8-byte-length": "^1.0.1" 63 | } 64 | }, 65 | "node_modules/typescript": { 66 | "version": "4.5.4", 67 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", 68 | "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", 69 | "dev": true, 70 | "bin": { 71 | "tsc": "bin/tsc", 72 | "tsserver": "bin/tsserver" 73 | }, 74 | "engines": { 75 | "node": ">=4.2.0" 76 | } 77 | }, 78 | "node_modules/utf8-byte-length": { 79 | "version": "1.0.4", 80 | "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", 81 | "integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=" 82 | } 83 | }, 84 | "dependencies": { 85 | "@types/mime": { 86 | "version": "2.0.3", 87 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", 88 | "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==", 89 | "dev": true 90 | }, 91 | "@types/node": { 92 | "version": "17.0.19", 93 | "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.19.tgz", 94 | "integrity": "sha512-PfeQhvcMR4cPFVuYfBN4ifG7p9c+Dlh3yUZR6k+5yQK7wX3gDgVxBly4/WkBRs9x4dmcy1TVl08SY67wwtEvmA==", 95 | "dev": true 96 | }, 97 | "@types/truncate-utf8-bytes": { 98 | "version": "1.0.0", 99 | "resolved": "https://registry.npmjs.org/@types/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.0.tgz", 100 | "integrity": "sha512-pa3icl1RawLUUKmQE1h0G381EpWMU8bTnZT4YF3Ey9D7VI4fL1BQvgZ8HKbYok9kKXeUSCr/E3vnscW8Ck3QKg==", 101 | "dev": true 102 | }, 103 | "mime": { 104 | "version": "2.4.6", 105 | "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", 106 | "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" 107 | }, 108 | "path-to-regexp": { 109 | "version": "6.2.0", 110 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.0.tgz", 111 | "integrity": "sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg==" 112 | }, 113 | "truncate-utf8-bytes": { 114 | "version": "1.0.2", 115 | "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", 116 | "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=", 117 | "requires": { 118 | "utf8-byte-length": "^1.0.1" 119 | } 120 | }, 121 | "typescript": { 122 | "version": "4.5.4", 123 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", 124 | "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", 125 | "dev": true 126 | }, 127 | "utf8-byte-length": { 128 | "version": "1.0.4", 129 | "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", 130 | "integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=" 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gemini-server", 3 | "version": "2.1.1", 4 | "description": "", 5 | "main": "./dist/lib/index.js", 6 | "types": "./dist/lib/index.d.ts", 7 | "files": [ 8 | "./dist" 9 | ], 10 | "scripts": { 11 | "prepublish": "npm run build", 12 | "build": "tsc", 13 | "test": "npm run build && node dist/examples/example.js", 14 | "clean": "rm -r dist" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/jgkaplan/gemini-server.git" 19 | }, 20 | "author": "Joshua Kaplan", 21 | "license": "ISC", 22 | "dependencies": { 23 | "mime": "^2.4.6", 24 | "path-to-regexp": "^6.2.0", 25 | "truncate-utf8-bytes": "^1.0.2" 26 | }, 27 | "devDependencies": { 28 | "@types/mime": "^2.0.3", 29 | "@types/node": "^17.0.19", 30 | "@types/truncate-utf8-bytes": "^1.0.0", 31 | "typescript": "^4.5.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": [ 5 | "es6" 6 | ], 7 | "baseUrl": "./", 8 | "typeRoots": [ 9 | "node_modules/@types" 10 | ], 11 | "declaration": true, 12 | "outDir": "./dist", 13 | "allowUnreachableCode": false, 14 | "exactOptionalPropertyTypes": true, 15 | "noImplicitOverride": true, 16 | "noImplicitReturns": true, 17 | "strict": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "esModuleInterop": true 20 | }, 21 | "include": [ 22 | "lib/*", 23 | "examples/example.ts" 24 | ], 25 | "exclude": [ 26 | "build/**/*", 27 | "node_modules", 28 | "dist" 29 | ] 30 | } --------------------------------------------------------------------------------