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 ¶
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
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.
}
}))
})
}
Output:
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
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
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))
}
}
Output:
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))
}
}
Output:
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))
}
}
Output:
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 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 )