Fix annoying indentation
Quicker funcs for obtaining URL params from ctx
Add config option to guess omitted .gmi extension
Houston is an Express-like Gemini server, written in Go. Primarily because I want to. Not because it’s necessary. There are plenty other Gemini servers that have all important functionality covered, and several are written in Go themselves. I just want to make my own.
That said, this aims to be lightweight and easy to use. This was initially developed to suite my own purposes, for my own capsules, as I wanted a server that I understood. Not somebody else’s.
My initial goal for Houston was to serve static files. It’s moved beyond that already. Here are my goals for the project:
Easiest way to learn, for me, is by reading an example. Here you go:
package main
import (
"strconv"
"git.sr.ht/~seanld/houston"
)
func main() {
r := houston.BlankRouter()
// Serve static files from directory `./static` at URL `/`.
// For example:
// * Request to `/` yields file `./static/index.gmi`
// * Request to `/something/example.txt` yields file `./static/something/example.txt`
r.Sandbox("/", "static")
// You can handle requests more dynamically and programatically
// by using `Router.Handle()`, which takes a callback function.
// The callback is given a `Context` instance, which contains
// information about the client and the request that can be
// operated on however you want.
r.Handle("/input", func(ctx houston.Context) {
ctx.InputAndDo("Guess a number from 1 to 10", func(s string) {
asInt, err := strconv.ParseInt(s, 10, 32)
if err != nil {
ctx.BadRequest("Please enter an integer!")
return
}
if asInt == 3 {
ctx.SendString("text/plain", "You got it right!")
} else {
ctx.SendString("text/plain", "Sorry, you're incorrect.")
}
})
})
newServer := houston.NewServer(&r, &houston.ServerConfig{
// Set the server up with a TLS certificate and key.
// Self-signed is acceptable and normal in Gemini.
CertificatePath: "cert/localhost.crt",
KeyPath: "cert/localhost.key",
// If you're trying to put your server into production,
// you'll want to specify a hostname different from
// `localhost`. Port defaults to 1965.
Hostname: "0.0.0.0",
Port: 1965,
// You can enable connection logging with Houston.
// Toggle the boolean flag, and give it the log file's
// path, and it will record connections to your capsule.
EnableLog: true,
LogFilePath: "houston.log",
// Houston comes with a rate-limiter (token-bucket algorithm),
// and can be enabled and configured. By default `MaxRate` and
// `BucketSize` are set to 2. These are good defaults for most.
EnableLimiting: true,
MaxRate: 2,
BucketSize: 2,
})
// With our router's URL endpoints set up, and the server
// options configured, the server can be started up. Just SIGTERM
// to stop it (Ctrl+C for most machines/terminals).
newServer.Start()
}
Router
structs are used by Server
structs to provide functionality for handling request-to-response. Routers
can have Route
and Sandbox
instances. They can be added to a router by doing Router.Handle(url, func (net.Conn) {})
or Router.Sandbox(url, sandboxDirPath)
.
Route
instances connect a URL path to a function that is executed when it’s visited.
Sandbox
instances connect a URL path to a local directory that holds static files. For example, if you connect /hello
to local dir /hello-static
, and /hello-static
has a file named index.gmi
in it, and someone visits /hello
, it will attempt to load the file /hello-static/index.gmi
. Or any other file specified from that dir.
Context
instances provide the URL of a connection, the actual net.Conn
of the connection, some methods for conveniently sending responses, and other features.