gemini

package module
v0.0.2 Latest Latest
Warning

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

Go to latest
Published: Jul 4, 2023 License: MIT Imports: 20 Imported by: 1

README

# gemini
A Go server implementation for the Gemini protocol.

## About
Head over to the go.pkg.dev documentation for examples.
=> https://pkg.go.dev/source.community/ckaznocha/gemini package documentation on pkg.go.dev

=> https://gemini.circumlunar.space/ Learn more about Gemini at gemini.circumlunar.space

Gemini is not HTTP and as such it isn't a goal if this package to replicate Go's net/http package faithfully. Instead the goal is to provide a Gemini server package that is both idiomatic and ergonomic for the things that make Gemini special.

This package is intended do provide everything needed to build spec compliant servers.  Properly configured servers cerated using this package will pass the michael-lazar/gemini-diagnostics tests.
=> https://github.com/michael-lazar/gemini-diagnostics michael-lazar/gemini-diagnostics

### Who's using this package?
You can check out a few servers using this package in the wild!

This list is not intended to be exhaustive.

=> gemini://source.community/ source.community - A git hosting service
=> gemini://clifton.kaznocha.net clifton.kaznocha.net - Clifton's Capsule

## TODO
* add Titan protocol support

## Contributing
The source code is hosted on source.community which doesn't yet support PRs so contributing is tricky. More info TBD.

## License
See LICENSE file

Documentation

Overview

Package gemini provides a server implementation for the Gemini protocol.

This package should feel familiar to people comfortable with Go's net/http however it differs where it makes sense.

Handlers receive a context directly rather than attaching it to a request.

There is no support for starting a server without TLS.

The ResponseWriter provides methods for handing different response types rather than a single abstract response.

An example of a server using this package. The server greets a user, accepting input from either a prompt or a client cert. The example includes a graceful shutdown, log handler, and a shutdown func:

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"os/signal"

	"source.community/ckaznocha/gemini"
)

func greetHandler(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request) {
	var name string

	switch {
	case r.Subject != nil:
		name = r.Subject.CommonName
	case r.URI.Query != "":
		name = r.URI.RawQuery
	default:
		w.Input(ctx, "What is your name?", false)

		return
	}

	fmt.Fprintf(w.Success(ctx, ""), "Hello, %s!", name)
}

func main() {
	logger := log.New(
		os.Stdout,
		"[Gemini Example] ",
		log.LstdFlags|log.LUTC|log.Lmicroseconds|log.Lmsgprefix|log.Lshortfile,
	)

	logger.Println("Server starting")

	mux := gemini.NewServeMux()

	mux.HandleFunc("/greet", greetHandler)

	s := &gemini.Server{
		Handler: mux,
		LogHandler: func(message string, isError bool) {
			logger.Printf("gemini server: %s", message)
		},
	}

	s.RegisterOnShutdown(func() {
		s.LogHandler("shutting down", false)
	})

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	logger.Println("Server started")

	go func() {
		defer cancel()

		s.LogHandler("starting", false)
		err := s.ListenAndServeTLS("", "testdata/cert.pem", "testdata/key.pem")
		s.LogHandler(fmt.Sprintf("exited: %s\n", err), true)
	}()

	ctx, stop := signal.NotifyContext(ctx, os.Interrupt)
	defer stop()

	<-ctx.Done()

	logger.Println("Shutdown starting")
	defer logger.Println("Shutdown complete")

	if err := s.Shutdown(context.Background()); err != nil {
		logger.Printf("Error during shutdown: %s\n", err)
	}
}

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// Listener sentinels.
	ErrClosingListeners  = errors.New("error closing listeners")
	ErrClosingListener   = errors.New("error closing listener")
	ErrOpeningConnection = errors.New("error opening connection")

	// Request sentinels.
	ErrMaxRequestLengthExceeded = fmt.Errorf(
		"the request length exceeded the max size of %d bytes",
		maxRequestLength,
	)
	ErrRequestRead = errors.New("unable to read request")

	// Server sentinels.
	ErrServerShutdown = errors.New("server shutdown")
	ErrStartingServer = errors.New("unable to start server")
	ErrServing        = errors.New("unable to serve requests")
	ErrIO             = errors.New("IO error")

	// URL sentinels.
	ErrMalformedURI = errors.New("malformed URI")
)

Sentinel errors.

Functions

This section is empty.

Types

type Handler

type Handler interface {
	ServeGemini(context.Context, ResponseWriter, *Request)
}

Handler is an interface that handles Gemini incoming requests.

type HandlerFunc

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

HandlerFunc is an adapter which allows a function to be used as a Handler.

func (HandlerFunc) ServeGemini

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

ServeGemini implements the Hander interface for a HandlerFunc.

type Request

type Request struct {
	Subject    *pkix.Name
	URI        *URI
	RemoteAddr string
}

Request is a Gemini request.

func ReadRequest

func ReadRequest(b *bufio.Reader) (*Request, error)

ReadRequest reads and parses a Gemini request from buffer. It returns an error if the request could not be read or was malformed.

type ResponseWriter

type ResponseWriter interface {
	Failure(ctx context.Context, code StatusCode, msg string)
	Input(ctx context.Context, prompt string, isSensitive bool)
	Redirect(ctx context.Context, redirectURL string, isPermanant bool)
	Success(ctx context.Context, mimeType string) io.Writer
}

ResponseWriter is interface to interact with a Gemini response. Calling any of its meethods after one has already been called or after Handler.ServeGemini has returned is a no-op.

type ServeMux

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

ServeMux is a Gemini request multiplexer. It will match requests to handlers based on the URI path. The longest match will be the one returned. Patterns ending with `/` will be matched exactly. Patterns without a trailing `/` will be treated as a prefix match.

func NewServeMux

func NewServeMux() *ServeMux

NewServeMux returns a new ServeMux ready to be used.

func (*ServeMux) Handle

func (sm *ServeMux) Handle(pattern string, handler Handler)

Handle adds a new pattern/Handler pair to the ServeMux.

func (*ServeMux) HandleFunc

func (sm *ServeMux) HandleFunc(pattern string, handler func(context.Context, ResponseWriter, *Request))

HandleFunc adds a new pattern/HandleFunc pair to the ServeMux.

func (*ServeMux) Handler

func (sm *ServeMux) Handler(r *Request) (h Handler, pattern string)

Handler looks up a matching Handler based on a Request. It returns the patter that matched in addition to the Hander.

func (*ServeMux) ServeGemini

func (sm *ServeMux) ServeGemini(ctx context.Context, w ResponseWriter, r *Request)

ServeGemini implements the Handler interface.

type Server

type Server struct {
	// Handler is a handler that is called each time a new network requests is
	// received. Unlike Go's net/http panics in handlers will not be recovered
	// automatically.
	Handler Handler

	// LogHandler is an optional function that allows a custom logger to be
	// hooked into the server. Erroneous logs will be passed in with `isError`
	// set to true.
	LogHandler func(message string, isError bool)

	// BaseContext is a optional function that takes a listener and returns a
	// context. The context returned by BaseContext will be used to create all
	// other contexts in the request lifecycle.
	BaseContext func(net.Listener) context.Context

	// ConnContext is an optional function that takes a context and a net.Conn
	// and returns a context. Like BaseContext, the context returned by
	// ConnContext will be used to create all contexts in the request lifecycle
	// after the connection has been created.
	ConnContext func(ctx context.Context, c net.Conn) context.Context

	// TLSConfig is the TLS config to use for the server.
	TLSConfig *tls.Config
	// contains filtered or unexported fields
}

Server serves network requests using the Gemini protocol.

func ServerFromCtx

func ServerFromCtx(ctx context.Context) (*Server, bool)

ServerFromCtx extracts a server from a context if present. If a server is not present on the context the returned bool will be false.

func (*Server) ListenAndServeTLS

func (s *Server) ListenAndServeTLS(addr, certFile, keyFile string) error

ListenAndServeTLS creates a listener and starts the server. If certFile and keyFile are non-empty strings the key pair will be loaded and used.

Example
package main

import (
	"context"
	"fmt"
	"log"

	"source.community/ckaznocha/gemini"
)

func main() {
	s := &gemini.Server{
		Handler: gemini.HandlerFunc(func(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request) {
			fmt.Fprintln(w.Success(ctx, ""), "Hello, TLS!")
		}),
	}

	// One can use generate_cert.go in crypto/tls to generate cert.pem and key.pem.
	log.Printf("About to listen the default port. Go to gemini://127.0.0.1/")

	err := s.ListenAndServeTLS("", "cert.pem", "key.pem")
	log.Fatal(err)
}
Output:

func (*Server) RegisterOnShutdown

func (s *Server) RegisterOnShutdown(f func())

RegisterOnShutdown adds a function which will be called when the server shuts down. RegisterOnShutdown can be called more than once to stack functions.

func (*Server) Serve

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

Serve start a server using the provided listener. The listener should support TLS.

func (*Server) ServeTLS

func (s *Server) ServeTLS(l net.Listener, certFile, keyFile string) error

ServeTLS starts a server with the provided listener, wrapping it in a TLS listener. If certFile and keyFile are non-empty strings the key pair will be loaded and used.

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

Shutdown shuts the server down gracefully. The shutdown will stop waiting for requests to finish if the context cancels.

type StatusCode

type StatusCode uint8

StatusCode represents a Gemini status code. Gemini status codes are two digit values. See `Project Gemini - Speculative specification - Section 3.2`.

const (
	// 1x - INPUT
	StatusInput          StatusCode = 10 // INPUT
	StatusSensitiveInput StatusCode = 11 // SENSITIVE INPUT

	// 2x - SUCCESS
	StatusSuccess StatusCode = 20 // SUCCESS

	// 3x - REDIRECT
	StatusTemporaryRedirect StatusCode = 30 // REDIRECT - TEMPORARY
	StatusPermanentRedirect StatusCode = 31 // REDIRECT - PERMANENT

	// 4x - TEMPORARY FAILURE
	StatusTemporaryFailure StatusCode = 40 // TEMPORARY FAILURE
	StatusServerFailure    StatusCode = 41 // SERVER UNAVAILABLE
	StatusCGIError         StatusCode = 42 // CGI ERROR
	StatusProxyError       StatusCode = 43 // PROXY ERROR
	StatusSlowDown         StatusCode = 44 // SLOW DOWN

	// 5x - PERMANENT FAILURE
	StatusPermanentFailure    StatusCode = 50 // PERMANENT FAILURE
	StatusNotFound            StatusCode = 51 // NOT FOUND
	StatusGone                StatusCode = 52 // GONE
	StatusProxyRequestRefused StatusCode = 53 // PROXY REQUEST REFUSED
	StatusBadRequest          StatusCode = 59 // BAD REQUEST

	// 6x - CLIENT CERTIFICATE REQUIRED
	StatusClientCertificateRequired StatusCode = 60 // CLIENT CERTIFICATE REQUIRED
	StatusCertificateNotAuthorised  StatusCode = 61 // CERTIFICATE NOT AUTHORIZED
	StatusCertificateNotValid       StatusCode = 62 // CERTIFICATE NOT VALID
)

The status codes as defined in `Project Gemini - Speculative specification - Appendix 1. Full two digit status codes`.

func (StatusCode) Description

func (c StatusCode) Description() string

Description returns the description of the code as described in `Project Gemini - Speculative specification`. Some but not all of the descriptions may be appropriate to return to a client as a failure description.

func (StatusCode) MarshalText

func (c StatusCode) MarshalText() ([]byte, error)

MarshalText implements the encoding.TextMarshaler interface. It returns the the code as an ASCII byte slice. It returns "00" if the code is invalid.

func (StatusCode) String

func (i StatusCode) String() string

func (StatusCode) ToCategory

func (c StatusCode) ToCategory() StatusCodeCategory

ToCategory returns which category the status code belongs to.

type StatusCodeCategory

type StatusCodeCategory uint8

StatusCodeCategory identifies a class of Gemini status codes.

const (
	StatusCategoryInput                     StatusCodeCategory = 1 // INPUT
	StatusCategorySuccess                   StatusCodeCategory = 2 // SUCCESS
	StatusCategoryRedirect                  StatusCodeCategory = 3 // REDIRECT
	StatusCategoryTemporaryFailure          StatusCodeCategory = 4 // TEMPORARY FAILURE
	StatusCategoryPermanentFailure          StatusCodeCategory = 5 // PERMANENT FAILURE
	StatusCategoryClientCertificateRequired StatusCodeCategory = 6 // CLIENT CERTIFICATE REQUIRED

	StatusCategoryUndefined    StatusCodeCategory = 0 // UNDEFINED
	StatusCategoryUndefinedX   StatusCodeCategory = 7 // UNDEFINED
	StatusCategoryUndefinedXX  StatusCodeCategory = 8 // UNDEFINED
	StatusCategoryUndefinedXXX StatusCodeCategory = 9 // UNDEFINED
)

The status code categories as defined in `Project Gemini - Speculative specification - Section 3.2`.

func (StatusCodeCategory) String

func (i StatusCodeCategory) String() string

func (StatusCodeCategory) ToCode

func (c StatusCodeCategory) ToCode() StatusCode

ToCode converts a Categoy to the base code for that category.

type URI

type URI struct {
	Host     string
	Port     string
	Path     string
	Fragment string

	// Query is the decoded query section. The original query is stored in
	// RawQuery.
	Query string

	// RawQuery is the original query section as received by the server. Use
	// this in the event you need to parse a query section into a url.Values.
	RawQuery string
	// contains filtered or unexported fields
}

URI represents a Gemini URI.

Resources hosted via Gemini are identified using URIs with the scheme "gemini". This scheme is syntactically compatible with the generic URI syntax defined in RFC 3986, but does not support all components of the generic syntax. In particular, the authority component is allowed and required, but its userinfo subcomponent is NOT allowed. The host subcomponent is required. The port subcomponent is optional, with a default value of 1965. The path, query and fragment components are allowed and have no special meanings beyond those defined by the generic syntax. Spaces in gemini URIs should be encoded as %20, not +.

func ParseRequestURI

func ParseRequestURI(rawURI string) (*URI, error)

ParseRequestURI parses a raw URI string into a Gemini URI. It returns an error if the URI is invalid.

func (*URI) String

func (u *URI) String() string

Directories

Path Synopsis
Package geminitest provides a test server and functions to help write tests for gemini servers.
Package geminitest provides a test server and functions to help write tests for gemini servers.

Jump to

Keyboard shortcuts

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