gemini

package module
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 20 Imported by: 0

README

Gemini

Yet another library for Gemini servers. See the documentation of this library, or read more about the Gemini specifications.

Examples

Look at the examples in the public documentation, or browse the example code.

License

This project is licensed under the MIT License.

Documentation

Overview

Package gemini implements a Gemini server and exposes an interface that should be familiar to the users of the `net/http` package.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Values added in v1.1.0

func Values(ctx context.Context) map[string]string

Values returns the path values indexed by their placeholders, if any. If the current path does not define any placeholder, Values returns nil.

Types

type Handler

type Handler interface {
	Handle(ctx context.Context, w ResponseWriter, r *Request)
}

Handler handles client requests and produces responses.

If a handler doesn't write a response header or response data, the server automatically sends a StatusSuccess response with media type "text/gemini" and no content.

While a handler should deal with its own panics, the server intercepts and swallows a panic thrown by a handler. Unless a response has already been sent to the client, a panic results in a StatusCGIError response.

The context passed to a handler is an empty, background context with no cancellation semantics defined by this library.

type HandlerFunc

type HandlerFunc func(ctx context.Context, w ResponseWriter, r *Request)

HandlerFunc is an implementation of Handler using a plain function.

func (HandlerFunc) Handle

func (f HandlerFunc) Handle(ctx context.Context, w ResponseWriter, r *Request)

Handle implements Handler.

type Middleware added in v1.1.0

type Middleware func(Handler) Handler

A Middleware wraps a handler and implements cross-cutting concerns.

type Mux added in v1.1.0

type Mux struct {
	// contains filtered or unexported fields
}

Mux is a Handler that delegates to other registered handlers based on the requested path. If none of the registered handlers matches the request path, the mux responds with StatusNotFound.

A mux must be fully initialized before it's used. After serving the first request, the Mux is considered frozen and it will panic if a caller tries to change its configuration. Configuring a mux is not concurrency-safe.

Example
package main

import (
	"context"
	"fmt"
	"log/slog"
	"time"

	"github.com/francescomari/gemini"
)

func main() {
	var m gemini.Mux

	// Use a middleware to track the duration of each incoming request.

	m.Use(func(h gemini.Handler) gemini.Handler {
		return gemini.HandlerFunc(func(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request) {
			start := time.Now()
			h.Handle(ctx, w, r)
			slog.Info("request complete", slog.String("path", r.Path), slog.Any("duration", time.Since(start)))
		})
	})

	// The root with a wildcard (/*) acts as a fallback when no other handler
	// could be found.

	m.On("/", gemini.HandlerFunc(func(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request) {
		fmt.Fprint(w, "Index page\r\n")
	}))

	m.On("/*", gemini.HandlerFunc(func(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request) {
		w.WriteHeader(gemini.StatusNotFound, "")
	}))

	// Use sub-muxes to group handlers sharing a common path prefix. Handlers can
	// be registered at parameterized paths.

	m.Sub("/users", func(m *gemini.Mux) {
		m.On("/", gemini.HandlerFunc(func(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request) {
			// Show a list of users.
		}))
		m.On("/{id}", gemini.HandlerFunc(func(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request) {
			values := gemini.Values(ctx)

			if values["id"] == "self" {
				// Show the authenticated user's profile.
			} else {
				// Show the profile of some other user.
			}
		}))
	})
}

func (*Mux) Handle added in v1.1.0

func (m *Mux) Handle(ctx context.Context, w ResponseWriter, r *Request)

Handle implements Handler.

As soon as the first request is served, this enters in a frozen state. Changing the configuration of the mux from this point onward will panic.

func (*Mux) On added in v1.1.0

func (m *Mux) On(path string, h Handler)

On registers a Handler for the provided path. The path can contain placeholders like `{foo}` that will be resolved against the corresponding component of the request path. The handler can retrieve the values of the placeholder using Values.

Matching a request path is a single linear pass over its components, with no backtracking. At each component, a literal match takes precedence over a placeholder, and a placeholder takes precedence over a wildcard, but this choice is final: if it doesn't lead to a registered handler by the end of the path, the request is not matched, even if a different choice at an earlier component would have led to one. For example, if both `/users/{uid}` and `/users/1/edit` are registered, a request to `/users/1` is not matched, because the literal component `1` takes precedence over the placeholder `{uid}` and does not itself lead to a registered handler. It's the caller's responsibility to register paths that don't rely on backtracking to be matched correctly.

If the registered path ends with a slash (`/`), the handler is invoked for the registered path itself, but not for the same path without the trailing slash. For example, a handler registered for `/foo/` is called for requests to `/foo/`, but not for `/foo`.

If a path ends with a wildcard (`*`) as its last component, the path matches any request path that starts with the prefix preceding the wildcard and has at least one more path component after it. The prefix itself, with or without a trailing slash, is not matched by the wildcard. For example, a handler registered for `/foo/*` is called for requests to `/foo/bar` and `/foo/bar/baz`, but not for `/foo` or `/foo/`. When multiple paths ending with a wildcard are registered, if a request could not be matched exactly to a handler, the request is matched to the longest matching, wildcard-terminated path. For example, assume that `/*` and `/foo/*` are registered. A request to `/foo/bar` is matched by the handler registered at `/foo/*`, but a request to `/bar` is matched by the handler registered at `/*`.

On panics if the mux is frozen, if the path is not absolute, if the handler is nil, if the path was already registered, or if registering the path would otherwise result in an inconsistent configuration for this mux.

func (*Mux) Sub added in v1.1.0

func (m *Mux) Sub(path string, configure func(m *Mux))

Sub creates a sub-mux based on this mux and configures the sub-mux by calling the configure function.

A sub-mux allows grouping together routes with a common path prefix. If a sub-mux is created at `/foo`, registering `/` in the sub-mux is equivalent to registering `/foo/` in the parent mux. Similarly, registering `/bar` in the sub-mux is equivalent to registering `/foo/bar` in the parent mux.

The middleware registered in the sub-mux applies only to the routes registered in that sub-mux. The middleware registered in a parent mux applies to every handler in every sub-mux created (directly or transitively) from that parent mux. Sub-muxes can be arbitrarily nested. The middleware registered in a parent mux runs before the middleware registered in a sub-mux.

Sub-muxes must not be used to serve requests. A sub-mux panics if it's asked to serve a request.

Sub panics if the mux is frozen, or if the path is not absolute.

func (*Mux) Use added in v1.1.0

func (m *Mux) Use(middleware Middleware)

Use registers a middleware for every Handler registered on this mux. The order of registration is respected. If middleware A is registered before middleware B, then A is invoked before B when processing a request, and both A and B are invoked before each handler.

Use panics if the mux is frozen, or if the middleware is nil.

type Request

type Request struct {
	// The URL scheme.
	Scheme string
	// The URL host. It's never empty.
	Host string
	// The URL port. It can be zero if the request didn't specify a port, in
	// which case the client expects the default port for the gemini scheme
	// to be used.
	Port int
	// The URL path. It's never empty. If the client didn't include a path in
	// the request, or if the path normalizes to nothing, Path is /.
	//
	// Paths are normalized by removing single-dot components and resolving
	// double-dot components.
	//
	// A trailing / in the original URL path is preserved.
	Path string
	// The query component of the URL. It can be the empty string if not provided.
	// The query is automatically URL-decoded. If the query contains CR-LF
	// sequences, they are normalized to individual LF.
	Query string
	// The client certificate. It can be nil if the client did not provide a
	// certificate.
	//
	// If the client provides a certificate, the server always validates the
	// not-before, not-after, and signature of the provided certificate.
	// Self-signed certificates are expected and encouraged.
	Cert *x509.Certificate
	// The client's remote address. Mostly for logging purposes.
	RemoteAddr string
}

Request contains the URL requested by the client.

type ResponseRecorder added in v1.1.0

type ResponseRecorder struct {
	Wrapped    ResponseWriter
	StatusCode StatusCode
	Meta       string
	Written    int
}

ResponseRecorder records the status code, meta, and amount of bytes written by a handler. Optionally, a ResponseRecorder can wrap a ResponseWriter. When wrapping a ResponseWriter, calls to the WriteHeader and Write are dispatched to the wrapped ResponseWriter.

func (*ResponseRecorder) Write added in v1.1.0

func (r *ResponseRecorder) Write(p []byte) (int, error)

Write implements ResponseWriter.

func (*ResponseRecorder) WriteHeader added in v1.1.0

func (r *ResponseRecorder) WriteHeader(statusCode StatusCode, meta string)

WriteHeader implements ResponseWriter.

type ResponseWriter

type ResponseWriter interface {
	io.Writer

	// WriteHeader writes the response header.
	//
	// If response data is written without calling this method, WriteHeader is
	// automatically called with StatusSuccess and a media type
	// of "text/gemini".
	//
	// Calling this method multiple times, or calling this method after
	// response data is written, has no effect.
	//
	// WriteHeader panics if trying to generate a header that does not conform
	// to the specification. For example, StatusSuccess always requires a
	// media type, and StatusInputExpected a prompt.
	WriteHeader(statusCode StatusCode, meta string)
}

ResponseWriter generates the server response. A ResponseWriter can't be safely used by multiple goroutines.

type Server

type Server struct {
	// The server certificate. Self-signed certificates are supported and
	// encouraged. This field is mandatory.
	Cert tls.Certificate
	// The handler for requests sent to this server. If not provided, the
	// server responds with StatusNotFound to every request that would
	// otherwise be processed by the handler.
	Handler Handler
	// Timeout sets the timeout for each I/O operation performed while serving a
	// connection, be it a read or a write. If not specified, no timeout is set.
	Timeout time.Duration
	// ReadTimeout sets the timeout for each read operation performed while
	// serving a connection. If not specified, no read timeout is set. If both
	// Timeout and ReadTimeout are specified, ReadTimeout overrides Timeout.
	ReadTimeout time.Duration
	// WriteTimeout sets the timeout for each write operation performed while
	// serving a connection. If not specified, no write timeout is set. If both
	// Timeout and WriteTimeout are specified, WriteTimeout overrides Timeout.
	WriteTimeout time.Duration
}

Server is a Gemini server. A Server can be safely used by multiple goroutines.

Example (ReadWriteTimeouts)
package main

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"os"
	"time"

	"github.com/francescomari/gemini"
)

func main() {
	cert, err := tls.LoadX509KeyPair(os.Getenv("TLS_CERT"), os.Getenv("TLS_KEY"))
	if err != nil {
		panic(fmt.Sprintf("load X.509 key pair: %v", err))
	}

	listener, err := net.Listen("tcp", ":1965")
	if err != nil {
		panic(fmt.Sprintf("listen: %v", err))
	}

	handler := gemini.HandlerFunc(func(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request) {
		fmt.Fprint(w, "=> https://github.com/francescomari/gemini Yet another Gemini server\r\n")
	})

	// Configure separate timeouts for read and write operations.

	server := gemini.Server{
		Cert:         cert,
		Handler:      handler,
		ReadTimeout:  2 * time.Second,
		WriteTimeout: 3 * time.Second,
	}

	if err := server.Serve(listener); err != nil {
		panic(fmt.Sprintf("serve: %v", err))
	}
}
Example (Run)
package main

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"os"

	"github.com/francescomari/gemini"
)

func main() {
	cert, err := tls.LoadX509KeyPair(os.Getenv("TLS_CERT"), os.Getenv("TLS_KEY"))
	if err != nil {
		panic(fmt.Sprintf("load X.509 key pair: %v", err))
	}

	listener, err := net.Listen("tcp", ":1965")
	if err != nil {
		panic(fmt.Sprintf("listen: %v", err))
	}

	handler := gemini.HandlerFunc(func(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request) {
		fmt.Fprint(w, "=> https://github.com/francescomari/gemini Yet another Gemini server\r\n")
	})

	server := gemini.Server{
		Cert:    cert,
		Handler: handler,
	}

	if err := server.Serve(listener); err != nil {
		panic(fmt.Sprintf("serve: %v", err))
	}
}
Example (Timeout)
package main

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"os"
	"time"

	"github.com/francescomari/gemini"
)

func main() {
	cert, err := tls.LoadX509KeyPair(os.Getenv("TLS_CERT"), os.Getenv("TLS_KEY"))
	if err != nil {
		panic(fmt.Sprintf("load X.509 key pair: %v", err))
	}

	listener, err := net.Listen("tcp", ":1965")
	if err != nil {
		panic(fmt.Sprintf("listen: %v", err))
	}

	handler := gemini.HandlerFunc(func(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request) {
		fmt.Fprint(w, "=> https://github.com/francescomari/gemini Yet another Gemini server\r\n")
	})

	// Configure a single timeout for both read and write operations.

	server := gemini.Server{
		Cert:    cert,
		Handler: handler,
		Timeout: time.Second,
	}

	if err := server.Serve(listener); err != nil {
		panic(fmt.Sprintf("serve: %v", err))
	}
}

func (*Server) Serve

func (s *Server) Serve(listener net.Listener) error

Serve serves Gemini clients connecting to this server. Serve blocks until accepting a connection fails. Before returning, Serve waits until every in-flight request is processed and returns the error returned by the listener. Serve always returns an error.

type StatusCode

type StatusCode int

StatusCode is one of the available status codes.

const (
	StatusInputExpected            StatusCode = 10
	StatusSensitiveInput           StatusCode = 11
	StatusSuccess                  StatusCode = 20
	StatusTemporaryRedirection     StatusCode = 30
	StatusPermanentRedirection     StatusCode = 31
	StatusTemporaryFailure         StatusCode = 40
	StatusServerUnavailable        StatusCode = 41
	StatusCGIError                 StatusCode = 42
	StatusProxyError               StatusCode = 43
	StatusSlowDown                 StatusCode = 44
	StatusPermanentFailure         StatusCode = 50
	StatusNotFound                 StatusCode = 51
	StatusGone                     StatusCode = 52
	StatusProxyRequestRefused      StatusCode = 53
	StatusBadRequest               StatusCode = 59
	StatusCertificateRequired      StatusCode = 60
	StatusCertificateNotAuthorized StatusCode = 61
	StatusCertificateNotValid      StatusCode = 62
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL