gemini

package module
v0.0.69 Latest Latest
Warning

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

Go to latest
Published: Aug 6, 2023 License: MIT Imports: 20 Imported by: 15

README

Gemini

Applications and libraries for building applications on Gemini (see https://gemini.circumlunar.space/).

Gemini CLI

Run a server
gemini serve --domain=example.com --certFile=a.crt --keyFile=a.key --path=.
Request content

curl for Gemini.

gemini request --insecure --verbose gemini://example.com/pass

Gemini Server Docker image

Run a server with Docker
docker run \
    -v /path_to_your_cert_files:/certs \
    -e PORT=1965 \
    -e DOMAIN=localhost \
    -v /path_to_your_content:/content \
    -p 1965:1965 \
    adrianhesketh/gemini:latest

Quick start

Check out https://github.com/a-h/gemini/releases for the latest version of the gemini command line tool to run locally, or use Docker:

# Create a server certificate.
openssl ecparam -genkey -name secp384r1 -out server.key
openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
# Make a Gemini file.
mkdir content
echo "# Hello, World!" > content/index.gmi
# Run the container.
docker pull adrianhesketh/gemini:latest
docker run -v `pwd`:/certs -e PORT=1965 -e DOMAIN=localhost -v `pwd`/content:/content -p 1965:1965 adrianhesketh/gemini:latest

Libraries

Serve

Use gemini.Server / gemini.ListenAndServe to build your own custom servers.

Supports hosting multiple Gemini servers on a single IP address.

These are used to build a Gemini application that supports dynamic content.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/a-h/gemini"
	"github.com/a-h/gemini/mux"
)

func main() {
	// Create the handlers for a domain (a.gemini).
	okHandler := gemini.HandlerFunc(func(w gemini.ResponseWriter, r *gemini.Request) {
		w.Write([]byte("OK"))
	})

	helloHandler := gemini.HandlerFunc(func(w gemini.ResponseWriter, r *gemini.Request) {
		w.Write([]byte("# Hello, user!\n"))
		if r.Certificate.ID == "" {
			w.Write([]byte("You're not authenticated"))
			return
		}
		w.Write([]byte(fmt.Sprintf("Certificate: %v\n", r.Certificate.ID)))
	})

	// Create a router for gemini://a.gemini/require_cert and gemini://a.gemini/public
	routerA := mux.NewMux()
	// Let's make /require_cert require the client to be authenticated.
	routerA.AddRoute("/require_cert", gemini.RequireCertificateHandler(helloHandler, nil))
	routerA.AddRoute("/public", okHandler)

	// Create a file system handler gemini://b.gemini/{path}
	handlerB := gemini.FileSystemHandler(gemini.Dir("./content"))

	// Set up the domain handlers.
	ctx := context.Background()
	a, err := gemini.NewDomainHandler("a.gemini", "a.crt", "a.key", routerA)
	if err != nil {
		log.Fatal("error creating domain handler A:", err)
	}
	b, err := gemini.NewDomainHandler("b.gemini", "b.crt", "b.key", handlerB)
	if err != nil {
		log.Fatal("error creating domain handler B:", err)
	}

	// Start the server for two domains (a.gemini / b.gemini).
	err = gemini.ListenAndServe(ctx, ":1965", a, b)
	if err != nil {
		log.Fatal("error:", err)
	}
}
Route

Use github.com/a-h/gemini/mux to provide routing between Gemini handlers and extract variables from URL paths.

Built-in utility handlers
  • RequireCertificateHandler a handler that ensures that users present certificates.
  • FileSystemHandler to support hosting static content.
Gemini client
client := gemini.NewClient()

// Make a request to the server without accepting its certificate.
r, certificates, authenticated, ok, err := client.Request("gemini://a.gemini/require_cert")
if err != nil {
	log.Printf("Request failed: %v", err)
	return
}

Configure allowed server certificates for trust-on-first-use certificate support:

client.AddAlllowedCertificateForHost("a.gemini", "3082016c3081f3020900d4c7c9907518eb61300a06082a8648ce3d0403023020310b30090603550406130267623111300f06035504030c08612e67656d696e69301e170d3230303832303139303330335a170d3330303831383139303330335a3020310b30090603550406130267623111300f06035504030c08612e67656d696e693076301006072a8648ce3d020106052b8104002203620004ae5cabe01f708d8f9423725df49601e1a033a1b51eb73cd3a8a9853011346127cbfedb57c4bd14ad6000ccb2f748d32b2a2b817b1860781d937e7666680874876fb4a9a91c44e2cf8c9804d40f6e7122f6c92a1884b62bd9f0749cca4e12cfa8300a06082a8648ce3d0403020368003065023100ae447eb9455e9ca1f02f013390d2c4029a7f29732cf6e29787b53b6435904d622f47f3b1fbffe60a284dbd4cddd6ef580230518dcb0355d5c3d880357128972c630ca90a915f1eb417a7ea0e4518a72dfc8a76c9b50c51d56f6a6835c4dfa989b72be3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")

Tasks

test

Test the project.

go test ./... -short
test-integration

Integration test the project.

go test ./...
build

Build the CLI.

go build -o gemini ./cmd/main.go
build-docker

Build the Docker image.

docker build . -t adrianhesketh/gemini
build-snapshot

Build a snapshot release using goreleaser.

goreleaser build --snapshot --rm-dist
serve-local-tests

Run a local Gemini server.

echo add '127.0.0.1       a-h.gemini' to your /etc/hosts file
openssl ecparam -genkey -name secp384r1 -out server.key
openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 -subj "/C=/ST=/L=/O=/OU=/CN=a-h.gemini"
go run ./cmd/main.go serve --domain=a-h.gemini --certFile=server.crt --keyFile=server.key --path=./tests
release

Push a release to Github.

if [ "${GITHUB_TOKEN}" == "" ]; then echo "Set the GITHUB_TOKEN environment variable"; fi
./push-tag.sh
goreleaser --clean

Documentation

Index

Constants

View Source
const (
	CodeInput                          Code = "10"
	CodeInputSensitive                      = "11"
	CodeSuccess                             = "20"
	CodeRedirect                            = "30"
	CodeRedirectTemporary                   = CodeRedirect
	CodeRedirectPermanent                   = "31"
	CodeTemporaryFailure                    = "40"
	CodeServerUnavailable                   = "41"
	CodeCGIError                            = "42"
	CodeProxyError                          = "43"
	CodeSlowDown                            = "44"
	CodePermanentFailure                    = "50"
	CodeNotFound                            = "51"
	CodeGone                                = "52"
	CodeProxyRequestRefused                 = "53"
	CodeBadRequest                          = "59"
	CodeClientCertificateRequired           = "60"
	CodeClientCertificateNotAuthorised      = "61"
	CodeClientCertificateNotValid           = "62"
)
View Source
const DefaultMIMEType = "text/gemini; charset=utf-8"

DefaultMIMEType for Gemini responses.

Variables

View Source
var ErrCannotWriteBodyWithoutSuccessCode = errors.New("gemini: cannot write body without success code")
View Source
var ErrCrLfNotFoundWithinMaxLength = errors.New("gemini: invalid header - CRLF not found within maximum length")

ErrCrLfNotFoundWithinMaxLength is returned if the Gemini server returns an invalid response.

View Source
var ErrHeaderAlreadyWritten = errors.New("gemini: header already written")

ErrHeaderAlreadyWritten is returned by SetHeader when the Gemini header has already been written to the response.

View Source
var ErrInvalidCode = errors.New("gemini: invalid code")

ErrInvalidCode is returned if the Gemini server returns an invalid code.

View Source
var ErrInvalidMeta = errors.New("gemini: invalid meta")

ErrInvalidMeta is returned if the Gemini server returns an invalid meta value.

View Source
var ErrInvalidStatus = errors.New("gemini: server status did not match the expected format")

ErrInvalidStatus is returned if the Gemini request did not match the expected format.

View Source
var ErrServerClosed = errors.New("gemini: server closed")

ErrServerClosed is returned when a server is attempted to start up when it's already shutting down.

Functions

func AuthoriserAllowAll

func AuthoriserAllowAll(id, key string) bool

AuthoriserAllowAll allows any authenticated user to access the handler.

func BadRequest

func BadRequest(w ResponseWriter, r *Request)

BadRequest responds with a 59 status.

func IsErrorCode

func IsErrorCode(code Code) bool

IsErrorCode returns true if the code is invalid, or starts with 4, 5 or 6.

func ListenAndServe

func ListenAndServe(ctx context.Context, addr string, domains ...*DomainHandler) (err error)

ListenAndServe starts up a new server to handle multiple domains with a specific certFile, keyFile and handler.

func NotFound

func NotFound(w ResponseWriter, r *Request)

NotFound responds with a 51 status.

Types

type Certificate

type Certificate struct {
	// ID is the base64-encoded SHA256 hash of the key.
	ID string
	// Key is the user public key in PKIX, ASN.1 DER form.
	Key string
	// Error is an error message related to any failures in handling the client certificate.
	Error string
}

Certificate information provided to the server by the client.

type Client

type Client struct {

	// Insecure mode does not check the hash of remote certificates.
	Insecure     bool
	WriteTimeout time.Duration
	ReadTimeout  time.Duration
	// contains filtered or unexported fields
}

Client for Gemini requests.

func NewClient

func NewClient() *Client

NewClient creates a new gemini client.

func (*Client) AddClientCertificate

func (client *Client) AddClientCertificate(prefix string, cert tls.Certificate)

AddClientCertificate adds a certificate to use when the URL prefix is encountered.

func (*Client) AddServerCertificate

func (client *Client) AddServerCertificate(host, certificateHash string)

AddServerCertificate allows the client to connect to a domain based on its hash.

func (*Client) GetCertificate

func (client *Client) GetCertificate(u *url.URL) (cert tls.Certificate, ok bool)

GetCertificate returns a certificate to use for the given URL, if one exists.

func (*Client) Request

func (client *Client) Request(ctx context.Context, u string) (resp *Response, certificates []string, authenticated, ok bool, err error)

Request a response from a given Gemini URL.

func (*Client) RequestConn

func (client *Client) RequestConn(ctx context.Context, conn net.Conn, u *url.URL) (resp *Response, err error)

RequestConn uses a given connection to make the request. This allows for insecure requests to be made. net.Dial("tcp", "localhost:1965")

func (*Client) RequestNoTLS

func (client *Client) RequestNoTLS(ctx context.Context, u *url.URL) (resp *Response, err error)

RequestNoTLS carries out a request without TLS enabled.

func (*Client) RequestURL

func (client *Client) RequestURL(ctx context.Context, u *url.URL) (resp *Response, certificates []string, authenticated, ok bool, err error)

RequestURL requests a response from a parsed URL. ok returns true if a matching server certificate is found (i.e. the server is OK).

type Code

type Code string

Code returned as part of the Gemini response (see https://gemini.circumlunar.space/docs/specification.html).

type Dir

type Dir string

func (Dir) Open

func (d Dir) Open(name string) (File, error)

Open implements FileSystem using os.Open, opening files for reading rooted and relative to the directory d.

type DomainHandler

type DomainHandler struct {
	ServerName string
	KeyPair    tls.Certificate
	Handler    Handler
}

DomainHandler handles incoming requests for the ServerName using the provided KeyPair certificate and Handler to process the request.

func NewDomainHandler

func NewDomainHandler(serverName string, cert tls.Certificate, handler Handler) *DomainHandler

NewDomainHandler creates a new handler to listen for Gemini requests using TLS. The cert can be generated by the github.com/a-h/gemini/cert.Generate package, or can generated using openssl: keyFile:

openssl ecparam -genkey -name secp384r1 -out server.key

certFile:

openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650

func NewDomainHandlerFromFiles

func NewDomainHandlerFromFiles(serverName, certFile, keyFile string, handler Handler) (*DomainHandler, error)

NewDomainHandlerFromFiles creates a new handler to listen for Gemini requests using TLS. certFile / keyFile are links to the X509 keypair. This can be generated using openssl: keyFile:

openssl ecparam -genkey -name secp384r1 -out server.key

certFile:

openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650

type File

type File interface {
	io.Closer
	io.Reader
	Readdir(count int) ([]os.FileInfo, error)
	Stat() (os.FileInfo, error)
}

A File is returned by a FileSystem's Open method and can be served by the FileServer implementation.

The methods should behave the same as those on an *os.File.

type FileSystem

type FileSystem interface {
	Open(name string) (File, error)
}

A FileSystem implements access to a collection of named files. The elements in a file path are separated by slash ('/', U+002F) characters, regardless of host operating system convention.

type Handler

type Handler interface {
	ServeGemini(w ResponseWriter, r *Request)
}

Handler of Gemini content.

func BadRequestHandler

func BadRequestHandler() Handler

BadRequestHandler creates a handler that returns a bad request code (59).

func DirectoryListingHandler

func DirectoryListingHandler(path string, f File) Handler

func FileContentHandler

func FileContentHandler(name string, f File) Handler

func FileSystemHandler

func FileSystemHandler(fs FileSystem) Handler

func NotFoundHandler

func NotFoundHandler() Handler

NotFoundHandler creates a handler that returns not found.

func RedirectPermanentHandler

func RedirectPermanentHandler(to string) Handler

RedirectPermanentHandler returns a handler which returns a permanent redirect.

func RedirectTemporaryHandler

func RedirectTemporaryHandler(to string) Handler

RedirectTemporaryHandler returns a temporary redirection.

func RequireCertificateHandler

func RequireCertificateHandler(h Handler, authoriser func(certID, certKey string) bool) Handler

RequireCertificateHandler returns a handler that enforces authentication on h. authoriser can be set to limit which users can access h. If authoriser is nil, authoriser is set to AuthoriserAllowAll which allows any authenticated user to access the handler.

func RequireInputHandler added in v0.0.66

func RequireInputHandler(h Handler, prompt string) Handler

RequireInputHandler returns a handler that enforces all incoming requests have a populated querystring. `prompt` is returned as response META if input is not provided.

func StripPrefixHandler

func StripPrefixHandler(prefix string, h Handler) Handler

StripPrefixHandler strips a prefix from the incoming URL and passes the strippe URL to h.

type HandlerFunc

type HandlerFunc func(ResponseWriter, *Request)

HandlerFunc handles a Gemini request and returns a response.

func (HandlerFunc) ServeGemini

func (f HandlerFunc) ServeGemini(w ResponseWriter, r *Request)

ServeGemini implements the Handler interface.

type Header struct {
	Code Code
	Meta string
}

type Request

type Request struct {
	Context     context.Context
	URL         *url.URL
	Certificate Certificate
}

Request from the client. A Gemini request contains only the URL, the Certificates field is populated by the TLS certificates presented by the client.

type Response

type Response struct {
	Header *Header
	Body   io.ReadCloser
}

Response from the Gemini server.

func NewResponse

func NewResponse(r io.ReadCloser) (resp *Response, err error)

NewResponse parses the server response.

func Record

func Record(r *Request, handler Handler) (resp *Response, err error)

Record a Gemini handler request in memory and return the response.

type ResponseWriter

type ResponseWriter interface {
	io.Writer
	SetHeader(code Code, meta string) error
}

ResponseWriter used by handlers to send a response to the client.

type Server

type Server struct {
	Context         context.Context
	Addr            string
	Insecure        bool
	DomainToHandler map[string]*DomainHandler
	ReadTimeout     time.Duration
	WriteTimeout    time.Duration
	HandlerTimeout  time.Duration
}

Server hosts Gemini content.

func NewServer

func NewServer(ctx context.Context, addr string, domainToHandler map[string]*DomainHandler) *Server

NewServer creates a new Gemini server. addr is in the form "<optional_ip>:<port>", e.g. ":1965". If left empty, it will default to ":1965". domainToHandler is a map of the server name (domain) to the certificate key pair and the Gemini handler used to serve content.

func (*Server) ListenAndServe

func (srv *Server) ListenAndServe() error

Set the server listening on the specified port.

type Writer

type Writer struct {
	Code          string
	Writer        io.Writer
	WrittenHeader int
	WrittenBody   int64
}

Writer passed to Gemini handlers.

func NewWriter

func NewWriter(w io.Writer) *Writer

NewWriter creates a new Gemini writer.

func (*Writer) SetHeader

func (gw *Writer) SetHeader(code Code, meta string) (err error)

func (*Writer) Write

func (gw *Writer) Write(p []byte) (n int, err error)

Directories

Path Synopsis
example

Jump to

Keyboard shortcuts

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