66 Commits

Author SHA1 Message Date
Adnan Maolood
dfa37aaeb8 client: Don't try to verify unicode hostname 2021-02-16 11:27:53 -05:00
Adnan Maolood
7c1a5184c9 Update examples/auth.go 2021-02-16 11:26:09 -05:00
Adnan Maolood
779be8b95b request: Allow User in URLs 2021-02-16 00:55:56 -05:00
Adnan Maolood
2157b35c0b Add build status badge to README.md 2021-02-16 00:07:01 -05:00
Adnan Maolood
1cb31e2d65 Add build manifest 2021-02-16 00:05:10 -05:00
Adnan Maolood
1d6cbddc5b server: Prevent adding Listeners after Close
Check done after calling trackListener to prevent the listener from
being registered after the server is closed.
2021-02-15 20:19:44 -05:00
Adnan Maolood
a05fa6d6bd server: Avoid creating a new Listener after Close 2021-02-15 20:16:32 -05:00
Adnan Maolood
f158bb5f1d server: Use separate mutex for handlers 2021-02-15 20:05:47 -05:00
Adnan Maolood
ec269c5c9d Add some tests 2021-02-15 19:20:37 -05:00
Adnan Maolood
bf4959a8ba Return ErrInvalidResponse on error reading status
Return ErrInvalidResponse when unable to read the response status code
instead of returning the error from strconv.
2021-02-15 19:18:23 -05:00
Adnan Maolood
19678ef934 Remove NewRequestFromURL method
Use a Request struct literal instead.
2021-02-15 17:23:56 -05:00
Adnan Maolood
5a784693ef server: Rename responder to handler 2021-02-15 01:15:23 -05:00
Adnan Maolood
2c7f8273e9 server: Recover from ServeGemini panics 2021-02-15 00:36:33 -05:00
Adnan Maolood
96a84ddd38 request: Don't read more than 1026 bytes 2021-02-15 00:16:21 -05:00
Adnan Maolood
3f2d540579 server: Implement Close and Shutdown methods 2021-02-14 23:58:33 -05:00
Adnan Maolood
92e7a309c6 Tweak returned error for requests that are too long
Return ErrInvalidRequest instead of ErrInvalidURL in Request.Write.
2021-02-14 23:33:18 -05:00
Adnan Maolood
c5ccbf023a fs: Refactor 2021-02-14 19:50:38 -05:00
Adnan Maolood
ff06e50df5 status: Update documentation 2021-02-14 19:28:29 -05:00
Adnan Maolood
5ec8dea1ba fs: Update documentation 2021-02-14 19:27:56 -05:00
Adnan Maolood
46e10da3a8 Make Request.Host optional 2021-02-14 19:02:34 -05:00
Adnan Maolood
41eec39a1d Update examples/client.go 2021-02-14 18:59:33 -05:00
Adnan Maolood
198a0b31c8 Remove faulty status code check in ReadResponse 2021-02-14 18:57:13 -05:00
Adnan Maolood
6f7c183662 server: Don't populate Request.Certificate field
Handlers should instead use the certificate provided in Request.TLS.
2021-02-14 17:34:57 -05:00
Adnan Maolood
20e1b14108 Update Client documentation 2021-02-14 17:11:05 -05:00
Adnan Maolood
0c303588a4 Update Response documentation 2021-02-14 16:23:38 -05:00
Adnan Maolood
37e5686764 Remove StatusClass* constants
Re-use the existing Status* constants and adjust StatusClass to return a
valid Status.
2021-02-14 16:01:39 -05:00
Adnan Maolood
7c703e95de Update documentation 2021-02-14 15:50:41 -05:00
Adnan Maolood
595b0d0490 server: Populate Request.RemoteAddr field 2021-02-13 21:10:19 -05:00
Adnan Maolood
d2c70a33d5 client: Punycode request URL 2021-02-09 16:55:14 -05:00
Adnan Maolood
79e0296bed client: Support IDNs
Convert IDNs to punycode before performing DNS lookups.
2021-02-09 15:59:47 -05:00
Adnan Maolood
f0e9150663 Add Gemini specification version to README.md 2021-02-09 15:50:54 -05:00
Adnan Maolood
f4b80ef305 Update documentation 2021-02-09 10:00:04 -05:00
Adnan Maolood
0e3b61ed00 examples/client: Fix opening of known hosts file 2021-02-09 09:48:51 -05:00
Adnan Maolood
f6824bd813 Make ResponseWriter an interface 2021-02-09 09:46:18 -05:00
Adnan Maolood
5ef5824d6f Use plain integers to represent status codes 2021-02-09 09:46:13 -05:00
Adnan Maolood
9bfb007581 Update README.md 2021-02-08 12:53:37 -05:00
Adnan Maolood
7910ed433b Rename Responder to Handler 2021-02-08 12:50:52 -05:00
Adnan Maolood
29f2b3738d Make TLS field nil for unencrypted connections 2021-02-08 12:32:49 -05:00
Adnan Maolood
1f39cab063 Remove unused field 2021-02-08 12:30:53 -05:00
Adnan Maolood
62960266ac tofu: Implement PersistentHosts 2021-01-25 12:11:59 -05:00
Adnan Maolood
3efa17f6fb Update examples 2021-01-25 10:59:50 -05:00
Adnan Maolood
9e89b93bab server: Allow handling any hostname with "*"
Allow registering a responder with the special pattern "*" to handle any
hostname.
2021-01-25 10:55:40 -05:00
Adnan Maolood
31de8d49b0 Guarantee that (*Response).Body is not nil 2021-01-15 15:18:00 -05:00
Adnan Maolood
2b17f3d8eb fs: Remove unused import 2021-01-14 22:45:09 -05:00
Adnan Maolood
f36a1c5c87 client: Add note about TOFU 2021-01-14 22:34:12 -05:00
Adnan Maolood
af61c1b60a fs: Update comments 2021-01-14 22:27:56 -05:00
Adnan Maolood
ad18ae601c fs: Don't panic on mime.AddExtensionType error
It's probably best not to panic if this fails.
2021-01-14 22:25:09 -05:00
Adnan Maolood
8473f3b9d4 fs: Update comments 2021-01-14 22:24:26 -05:00
Adnan Maolood
06c53cc5b1 server: Rename Register to Handle 2021-01-14 22:12:07 -05:00
Adnan Maolood
4b643523fb Update examples 2021-01-14 21:23:13 -05:00
Adnan Maolood
79a4dfd43f certificate: Add Dir.Entries function 2021-01-14 21:19:27 -05:00
Adnan Maolood
14d89f304a Move cert.go to a subpackage 2021-01-14 20:42:12 -05:00
Adnan Maolood
7a00539f75 tofu: Fix example 2021-01-14 19:57:52 -05:00
Adnan Maolood
a0adc42c95 tofu: Update documentation 2021-01-14 19:56:04 -05:00
Adnan Maolood
da8af5dbcb tofu: Update documentation 2021-01-14 19:40:19 -05:00
Adnan Maolood
ced6b06d76 Update examples/auth.go 2021-01-14 19:04:11 -05:00
Adnan Maolood
4a0f8e5e73 tofu: Rename KnownHosts.Hosts to Entries 2021-01-14 18:52:43 -05:00
Adnan Maolood
e701ceff71 Add KnownHosts.Hosts function 2021-01-14 18:50:03 -05:00
Adnan Maolood
1a3974b3a3 Update examples/client.go 2021-01-14 17:28:03 -05:00
Adnan Maolood
3fd55c5cee tofu: Add KnownHosts.Load function 2021-01-14 17:09:31 -05:00
Adnan Maolood
6f11910dff tofu: Add NewHostsFile function 2021-01-14 16:54:38 -05:00
Adnan Maolood
da3e9ac0fe tofu: Protect HostWriter with a mutex 2021-01-14 16:35:54 -05:00
Adnan Maolood
9fe837ffac tofu: Refactor known hosts
This commit introduces the KnownHosts struct, whose purpose is simply to
store known hosts entries. The HostWriter struct is now in charge of
appending hosts to files, and the two are not dependent on each other.
Users are now responsible for opening the known hosts file and closing
it when they are finished with it.
2021-01-14 16:26:43 -05:00
Adnan Maolood
4b8bb16a3d tofu: Rename KnownHost to Host 2021-01-14 14:15:08 -05:00
Hugo Wetterberg
95aff9c573 tofu: Refactor
This commit changes underlying file handling and known hosts parsing.

A known hosts file opened through Load() never closed the underlying
file. During known hosts parsing most errors were unchecked, or just
led to the line being skipped.

I removed the KnownHosts type, which didn't really have a role after
the refactor. The embedding of KnownHosts in KnownHosts file has been
removed as it also leaked the map unprotected by the mutex.

The Fingerprint type is now KnownHost and has taken over the
responsibility of marshalling and unmarshalling.

SetOutput now takes a WriteCloser so that we can close the underlying
writer when it's replaced, or when it's explicitly closed through the
new Close() function.

KnownHostsFile.Add() now also writes the known host to the output if
set. I think that makes sense expectation-wise for the type.

Turned WriteAll() into WriteTo() to conform with the io.WriterTo
interface.

Load() is now Open() to better reflect the fact that a file is opened,
and kept open. It can now also return errors from the parsing process.

The parser does a lot more error checking, and this might be an area
where I've changed a desired behaviour as invalid entries no longer
are ignored, but aborts the parsing process. That could be changed to
a warning, or some kind of parsing feedback.

I added KnownHostsFile.TOFU() to fill the developer experience gap
that was left after the client no longer knows about
KnownHostsFile. It implements a basic non-interactive TOFU flow.
2021-01-14 13:48:57 -05:00
Hugo Wetterberg
de042e4724 client: set the client timout on the dialer, close connection on err
Client.Timout isn't respected for the dial. Requests will hang on dial
until OS-level timouts kick in unless there is a Request.Context with
a deadline. We also fail to close the connection on errors.

This change sets the client timeout as the dialer timeout so that it
will be respected. It also ensures that we close the connection if we
fail to make the request.
2021-01-13 17:13:56 -05:00
23 changed files with 1416 additions and 532 deletions

9
.build.yml Normal file
View File

@@ -0,0 +1,9 @@
image: alpine/edge
packages:
- go
sources:
- https://git.sr.ht/~adnano/go-gemini
tasks:
- test: |
cd go-gemini
go test ./...

View File

@@ -1,10 +1,12 @@
# go-gemini
[![godocs.io](https://godocs.io/git.sr.ht/~adnano/go-gemini?status.svg)](https://godocs.io/git.sr.ht/~adnano/go-gemini)
[![godocs.io](https://godocs.io/git.sr.ht/~adnano/go-gemini?status.svg)](https://godocs.io/git.sr.ht/~adnano/go-gemini) [![builds.sr.ht status](https://builds.sr.ht/~adnano/go-gemini.svg)](https://builds.sr.ht/~adnano/go-gemini?)
Package gemini implements the [Gemini protocol](https://gemini.circumlunar.space) in Go.
It aims to provide an API similar to that of net/http to make it easy to develop Gemini clients and servers.
It provides an API similar to that of net/http to make it easy to develop Gemini clients and servers.
Compatible with version v0.14.3 of the Gemini specification.
## Usage

View File

@@ -1,4 +1,5 @@
package gemini
// Package certificate provides utility functions for TLS certificates.
package certificate
import (
"crypto"
@@ -19,27 +20,23 @@ import (
"time"
)
// CertificateDir maps certificate scopes to certificates.
type CertificateStore map[string]tls.Certificate
// CertificateDir represents a certificate store optionally loaded from a directory.
// The zero value of CertificateDir is an empty store ready to use.
// Dir represents a directory of certificates.
// The zero value for Dir is an empty directory ready to use.
//
// CertificateDir is safe for concurrent use by multiple goroutines.
type CertificateDir struct {
CertificateStore
dir bool
path string
mu sync.RWMutex
// Dir is safe for concurrent use by multiple goroutines.
type Dir struct {
certs map[string]tls.Certificate
path *string
mu sync.RWMutex
}
// Add adds a certificate for the given scope to the store.
// Add adds a certificate for the given scope to the directory.
// It tries to parse the certificate if it is not already parsed.
func (c *CertificateDir) Add(scope string, cert tls.Certificate) {
c.mu.Lock()
defer c.mu.Unlock()
if c.CertificateStore == nil {
c.CertificateStore = CertificateStore{}
func (d *Dir) Add(scope string, cert tls.Certificate) error {
d.mu.Lock()
defer d.mu.Unlock()
if d.certs == nil {
d.certs = map[string]tls.Certificate{}
}
// Parse certificate if not already parsed
if cert.Leaf == nil {
@@ -48,40 +45,45 @@ func (c *CertificateDir) Add(scope string, cert tls.Certificate) {
cert.Leaf = parsed
}
}
c.CertificateStore[scope] = cert
}
// Write writes the provided certificate to the certificate directory.
func (c *CertificateDir) Write(scope string, cert tls.Certificate) error {
c.mu.RLock()
defer c.mu.RUnlock()
if c.dir {
if d.path != nil {
// Escape slash character
scope = strings.ReplaceAll(scope, "/", ":")
certPath := filepath.Join(c.path, scope+".crt")
keyPath := filepath.Join(c.path, scope+".key")
if err := WriteCertificate(cert, certPath, keyPath); err != nil {
certPath := filepath.Join(*d.path, scope+".crt")
keyPath := filepath.Join(*d.path, scope+".key")
if err := Write(cert, certPath, keyPath); err != nil {
return err
}
}
d.certs[scope] = cert
return nil
}
// Lookup returns the certificate for the given scope.
func (c *CertificateDir) Lookup(scope string) (tls.Certificate, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
cert, ok := c.CertificateStore[scope]
// Lookup returns the certificate for the provided scope.
func (d *Dir) Lookup(scope string) (tls.Certificate, bool) {
d.mu.RLock()
defer d.mu.RUnlock()
cert, ok := d.certs[scope]
return cert, ok
}
// Load loads certificates from the given path.
// The path should lead to a directory containing certificates and private keys
// in the form scope.crt and scope.key.
// For example, the hostname "localhost" would have the corresponding files
// localhost.crt (certificate) and localhost.key (private key).
// New certificates will be written to this directory.
func (c *CertificateDir) Load(path string) error {
// Entries returns a map of hostnames to certificates.
func (d *Dir) Entries() map[string]tls.Certificate {
certs := map[string]tls.Certificate{}
for key := range d.certs {
certs[key] = d.certs[key]
}
return certs
}
// Load loads certificates from the provided path.
// Add will write certificates to this path.
//
// The directory should contain certificates and private keys
// named scope.crt and scope.key respectively, where scope is
// the scope of the certificate.
func (d *Dir) Load(path string) error {
matches, err := filepath.Glob(filepath.Join(path, "*.crt"))
if err != nil {
return err
@@ -95,31 +97,31 @@ func (c *CertificateDir) Load(path string) error {
scope := strings.TrimSuffix(filepath.Base(crtPath), ".crt")
// Unescape slash character
scope = strings.ReplaceAll(scope, ":", "/")
c.Add(scope, cert)
d.Add(scope, cert)
}
c.SetDir(path)
d.SetPath(path)
return nil
}
// SetDir sets the directory that new certificates will be written to.
func (c *CertificateDir) SetDir(path string) {
c.mu.Lock()
defer c.mu.Unlock()
c.dir = true
c.path = path
// SetPath sets the directory path.
// Add will write certificates to this path.
func (d *Dir) SetPath(path string) {
d.mu.Lock()
defer d.mu.Unlock()
d.path = &path
}
// CertificateOptions configures the creation of a certificate.
type CertificateOptions struct {
// Subject Alternate Name values.
// Should contain the IP addresses that the certificate is valid for.
IPAddresses []net.IP
// CreateOptions configures the creation of a TLS certificate.
type CreateOptions struct {
// Subject Alternate Name values.
// Should contain the DNS names that this certificate is valid for.
// E.g. example.com, *.example.com
DNSNames []string
// Subject Alternate Name values.
// Should contain the IP addresses that the certificate is valid for.
IPAddresses []net.IP
// Subject specifies the certificate Subject.
//
// Subject.CommonName can contain the DNS name that this certificate
@@ -136,8 +138,8 @@ type CertificateOptions struct {
Ed25519 bool
}
// CreateCertificate creates a new TLS certificate.
func CreateCertificate(options CertificateOptions) (tls.Certificate, error) {
// Create creates a new TLS certificate.
func Create(options CreateOptions) (tls.Certificate, error) {
crt, priv, err := newX509KeyPair(options)
if err != nil {
return tls.Certificate{}, err
@@ -150,7 +152,7 @@ func CreateCertificate(options CertificateOptions) (tls.Certificate, error) {
}
// newX509KeyPair creates and returns a new certificate and private key.
func newX509KeyPair(options CertificateOptions) (*x509.Certificate, crypto.PrivateKey, error) {
func newX509KeyPair(options CreateOptions) (*x509.Certificate, crypto.PrivateKey, error) {
var pub crypto.PublicKey
var priv crypto.PrivateKey
if options.Ed25519 {
@@ -206,9 +208,9 @@ func newX509KeyPair(options CertificateOptions) (*x509.Certificate, crypto.Priva
return cert, priv, nil
}
// WriteCertificate writes the provided certificate and private key
// Write writes the provided certificate and its private key
// to certPath and keyPath respectively.
func WriteCertificate(cert tls.Certificate, certPath, keyPath string) error {
func Write(cert tls.Certificate, certPath, keyPath string) error {
certOut, err := os.OpenFile(certPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err

125
client.go
View File

@@ -8,29 +8,38 @@ import (
"errors"
"fmt"
"net"
"strings"
"time"
)
// Client is a Gemini client.
// A Client is a Gemini client. Its zero value is a usable client.
type Client struct {
// TrustCertificate is called to determine whether the client
// should trust the certificate provided by the server.
// If TrustCertificate is nil, the client will accept any certificate.
// If the returned error is not nil, the certificate will not be trusted
// and the request will be aborted.
//
// See the tofu submodule for an implementation of trust on first use.
TrustCertificate func(hostname string, cert *x509.Certificate) error
// Timeout specifies a time limit for requests made by this
// Client. The timeout includes connection time and reading
// the response body. The timer remains running after
// Get and Do return and will interrupt reading of the Response.Body.
// Get or Do return and will interrupt reading of the Response.Body.
//
// A Timeout of zero means no timeout.
Timeout time.Duration
}
// Get performs a Gemini request for the given URL.
// Get sends a Gemini request for the given URL.
//
// An error is returned if there was a Gemini protocol error.
// A non-2x status code doesn't cause an error.
//
// If the returned error is nil, the Response will contain a non-nil Body
// which the user is expected to close.
//
// For more control over requests, use NewRequest and Client.Do.
func (c *Client) Get(url string) (*Response, error) {
req, err := NewRequest(url)
if err != nil {
@@ -39,14 +48,54 @@ func (c *Client) Get(url string) (*Response, error) {
return c.Do(req)
}
// Do performs a Gemini request and returns a Gemini response.
// Do sends a Gemini request and returns a Gemini response, following
// policy as configured on the client.
//
// An error is returned if there was a Gemini protocol error.
// A non-2x status code doesn't cause an error.
//
// If the returned error is nil, the Response will contain a non-nil Body
// which the user is expected to close.
//
// Generally Get will be used instead of Do.
func (c *Client) Do(req *Request) (*Response, error) {
// Extract hostname
colonPos := strings.LastIndex(req.Host, ":")
if colonPos == -1 {
colonPos = len(req.Host)
// Punycode request URL host
hostname, port, err := net.SplitHostPort(req.URL.Host)
if err != nil {
// Likely no port
hostname = req.URL.Host
port = "1965"
}
punycode, err := punycodeHostname(hostname)
if err != nil {
return nil, err
}
if hostname != punycode {
hostname = punycode
// Make a copy of the request
_req := *req
req = &_req
_url := *req.URL
req.URL = &_url
// Set the host
req.URL.Host = net.JoinHostPort(hostname, port)
}
// Use request host if provided
if req.Host != "" {
hostname, port, err = net.SplitHostPort(req.Host)
if err != nil {
// Port is required
return nil, err
}
// Punycode hostname
hostname, err = punycodeHostname(hostname)
if err != nil {
return nil, err
}
}
hostname := req.Host[:colonPos]
// Connect to the host
config := &tls.Config{
@@ -59,36 +108,60 @@ func (c *Client) Do(req *Request) (*Response, error) {
return &tls.Certificate{}, nil
},
VerifyConnection: func(cs tls.ConnectionState) error {
return c.verifyConnection(req, cs)
return c.verifyConnection(hostname, punycode, cs)
},
ServerName: hostname,
}
// Set connection context
ctx := req.Context
if ctx == nil {
ctx = context.Background()
}
netConn, err := (&net.Dialer{}).DialContext(ctx, "tcp", req.Host)
start := time.Now()
dialer := net.Dialer{
Timeout: c.Timeout,
}
address := net.JoinHostPort(hostname, port)
netConn, err := dialer.DialContext(ctx, "tcp", address)
if err != nil {
return nil, err
}
conn := tls.Client(netConn, config)
// Set connection deadline
if c.Timeout != 0 {
err := conn.SetDeadline(time.Now().Add(c.Timeout))
err := conn.SetDeadline(start.Add(c.Timeout))
if err != nil {
return nil, fmt.Errorf(
"failed to set connection deadline: %w", err)
return nil, fmt.Errorf("failed to set connection deadline: %w", err)
}
}
resp, err := c.do(conn, req)
if err != nil {
// If we fail to perform the request/response we have
// to take responsibility for closing the connection.
_ = conn.Close()
return nil, err
}
// Store connection state
state := conn.ConnectionState()
resp.TLS = &state
return resp, nil
}
func (c *Client) do(conn *tls.Conn, req *Request) (*Response, error) {
// Write the request
w := bufio.NewWriter(conn)
err = req.Write(w)
err := req.Write(w)
if err != nil {
return nil, fmt.Errorf(
"failed to write request data: %w", err)
return nil, fmt.Errorf("failed to write request: %w", err)
}
if err := w.Flush(); err != nil {
@@ -100,22 +173,14 @@ func (c *Client) Do(req *Request) (*Response, error) {
if err != nil {
return nil, err
}
// Store connection state
resp.TLS = conn.ConnectionState()
return resp, nil
}
func (c *Client) verifyConnection(req *Request, cs tls.ConnectionState) error {
// Verify the hostname
var hostname string
if host, _, err := net.SplitHostPort(req.Host); err == nil {
hostname = host
} else {
hostname = req.Host
}
func (c *Client) verifyConnection(hostname, punycode string, cs tls.ConnectionState) error {
cert := cs.PeerCertificates[0]
if err := verifyHostname(cert, hostname); err != nil {
// Verify punycoded hostname
if err := verifyHostname(cert, punycode); err != nil {
return err
}
// Check expiration date

11
doc.go
View File

@@ -8,10 +8,7 @@ Client is a Gemini client.
if err != nil {
// handle error
}
if resp.Body != nil {
defer resp.Body.Close()
// ...
}
defer resp.Body.Close()
// ...
Server is a Gemini server.
@@ -30,13 +27,13 @@ Servers should be configured with certificates:
Servers can accept requests for multiple hosts and schemes:
server.RegisterFunc("example.com", func(w *gemini.ResponseWriter, r *gemini.Request) {
server.RegisterFunc("example.com", func(w gemini.ResponseWriter, r *gemini.Request) {
fmt.Fprint(w, "Welcome to example.com")
})
server.RegisterFunc("example.org", func(w *gemini.ResponseWriter, r *gemini.Request) {
server.RegisterFunc("example.org", func(w gemini.ResponseWriter, r *gemini.Request) {
fmt.Fprint(w, "Welcome to example.org")
})
server.RegisterFunc("http://example.net", func(w *gemini.ResponseWriter, r *gemini.Request) {
server.RegisterFunc("http://example.net", func(w gemini.ResponseWriter, r *gemini.Request) {
fmt.Fprint(w, "Proxied content from http://example.net")
})

View File

@@ -12,6 +12,7 @@ import (
"time"
"git.sr.ht/~adnano/go-gemini"
"git.sr.ht/~adnano/go-gemini/certificate"
)
type User struct {
@@ -32,8 +33,8 @@ func main() {
if err := server.Certificates.Load("/var/lib/gemini/certs"); err != nil {
log.Fatal(err)
}
server.CreateCertificate = func(hostname string) (tls.Certificate, error) {
return gemini.CreateCertificate(gemini.CertificateOptions{
server.GetCertificate = func(hostname string) (tls.Certificate, error) {
return certificate.Create(certificate.CreateOptions{
Subject: pkix.Name{
CommonName: hostname,
},
@@ -41,7 +42,7 @@ func main() {
Duration: time.Hour,
})
}
server.Register("localhost", &mux)
server.Handle("localhost", &mux)
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
@@ -53,12 +54,12 @@ func fingerprint(cert *x509.Certificate) string {
return string(b[:])
}
func profile(w *gemini.ResponseWriter, r *gemini.Request) {
if r.Certificate == nil {
w.WriteStatus(gemini.StatusCertificateRequired)
func profile(w gemini.ResponseWriter, r *gemini.Request) {
if len(r.TLS.PeerCertificates) == 0 {
w.Status(gemini.StatusCertificateRequired)
return
}
fingerprint := fingerprint(r.Certificate.Leaf)
fingerprint := fingerprint(r.TLS.PeerCertificates[0])
user, ok := users[fingerprint]
if !ok {
user = &User{}
@@ -68,23 +69,23 @@ func profile(w *gemini.ResponseWriter, r *gemini.Request) {
fmt.Fprintln(w, "=> /username Change username")
}
func changeUsername(w *gemini.ResponseWriter, r *gemini.Request) {
if r.Certificate == nil {
w.WriteStatus(gemini.StatusCertificateRequired)
func changeUsername(w gemini.ResponseWriter, r *gemini.Request) {
if len(r.TLS.PeerCertificates) == 0 {
w.Status(gemini.StatusCertificateRequired)
return
}
username, err := gemini.QueryUnescape(r.URL.RawQuery)
if err != nil || username == "" {
w.WriteHeader(gemini.StatusInput, "Username")
w.Header(gemini.StatusInput, "Username")
return
}
fingerprint := fingerprint(r.Certificate.Leaf)
fingerprint := fingerprint(r.TLS.PeerCertificates[0])
user, ok := users[fingerprint]
if !ok {
user = &User{}
users[fingerprint] = user
}
user.Name = username
w.WriteHeader(gemini.StatusRedirect, "/")
w.Header(gemini.StatusRedirect, "/")
}

View File

@@ -11,7 +11,7 @@ import (
"os"
"time"
"git.sr.ht/~adnano/go-gemini"
"git.sr.ht/~adnano/go-gemini/certificate"
)
func main() {
@@ -24,20 +24,20 @@ func main() {
if err != nil {
log.Fatal(err)
}
options := gemini.CertificateOptions{
options := certificate.CreateOptions{
Subject: pkix.Name{
CommonName: host,
},
DNSNames: []string{host},
Duration: duration,
}
cert, err := gemini.CreateCertificate(options)
cert, err := certificate.Create(options)
if err != nil {
log.Fatal(err)
}
certPath := host + ".crt"
keyPath := host + ".key"
if err := gemini.WriteCertificate(cert, certPath, keyPath); err != nil {
if err := certificate.Write(cert, certPath, keyPath); err != nil {
log.Fatal(err)
}
}

View File

@@ -6,6 +6,7 @@ package main
import (
"bufio"
"bytes"
"crypto/x509"
"errors"
"fmt"
@@ -22,8 +23,9 @@ import (
)
var (
hosts tofu.KnownHostsFile
scanner *bufio.Scanner
hosts tofu.KnownHosts
hostsfile *tofu.HostWriter
scanner *bufio.Scanner
)
func init() {
@@ -31,7 +33,12 @@ func init() {
path := filepath.Join(xdg.DataHome(), "gemini", "known_hosts")
err := hosts.Load(path)
if err != nil {
log.Println(err)
log.Fatal(err)
}
hostsfile, err = tofu.OpenHostsFile(path)
if err != nil {
log.Fatal(err)
}
scanner = bufio.NewScanner(os.Stdin)
@@ -47,25 +54,26 @@ Otherwise, this should be safe to trust.
=> `
func trustCertificate(hostname string, cert *x509.Certificate) error {
fingerprint := tofu.NewFingerprint(cert.Raw, cert.NotAfter)
host := tofu.NewHost(hostname, cert.Raw, cert.NotAfter)
knownHost, ok := hosts.Lookup(hostname)
if ok && time.Now().Before(knownHost.Expires) {
// Check fingerprint
if knownHost.Hex == fingerprint.Hex {
if bytes.Equal(knownHost.Fingerprint, host.Fingerprint) {
return nil
}
return errors.New("error: fingerprint does not match!")
}
fmt.Printf(trustPrompt, hostname, fingerprint.Hex)
fmt.Printf(trustPrompt, hostname, host.Fingerprint)
scanner.Scan()
switch scanner.Text() {
case "t":
hosts.Add(hostname, fingerprint)
hosts.Write(hostname, fingerprint)
hosts.Add(host)
hostsfile.WriteHost(host)
return nil
case "o":
hosts.Add(hostname, fingerprint)
hosts.Add(host)
return nil
default:
return errors.New("certificate not trusted")
@@ -87,8 +95,8 @@ func do(req *gemini.Request, via []*gemini.Request) (*gemini.Response, error) {
return resp, err
}
switch resp.Status.Class() {
case gemini.StatusClassInput:
switch gemini.StatusClass(resp.Status) {
case gemini.StatusInput:
input, ok := getInput(resp.Meta, resp.Status == gemini.StatusSensitiveInput)
if !ok {
break
@@ -97,7 +105,7 @@ func do(req *gemini.Request, via []*gemini.Request) (*gemini.Response, error) {
req.URL.RawQuery = gemini.QueryEscape(input)
return do(req, via)
case gemini.StatusClassRedirect:
case gemini.StatusRedirect:
via = append(via, req)
if len(via) > 5 {
return resp, errors.New("too many redirects")
@@ -137,10 +145,10 @@ func main() {
fmt.Println(err)
os.Exit(1)
}
defer resp.Body.Close()
// Handle response
if resp.Status.Class() == gemini.StatusClassSuccess {
defer resp.Body.Close()
if gemini.StatusClass(resp.Status) == gemini.StatusSuccess {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)

View File

@@ -11,6 +11,7 @@ import (
"time"
"git.sr.ht/~adnano/go-gemini"
"git.sr.ht/~adnano/go-gemini/certificate"
)
func main() {
@@ -20,8 +21,8 @@ func main() {
if err := server.Certificates.Load("/var/lib/gemini/certs"); err != nil {
log.Fatal(err)
}
server.CreateCertificate = func(hostname string) (tls.Certificate, error) {
return gemini.CreateCertificate(gemini.CertificateOptions{
server.GetCertificate = func(hostname string) (tls.Certificate, error) {
return certificate.Create(certificate.CreateOptions{
Subject: pkix.Name{
CommonName: hostname,
},
@@ -33,7 +34,7 @@ func main() {
var mux gemini.ServeMux
mux.Handle("/", gemini.FileServer(gemini.Dir("/var/www")))
server.Register("localhost", &mux)
server.Handle("localhost", &mux)
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}

View File

@@ -13,6 +13,7 @@ import (
"time"
"git.sr.ht/~adnano/go-gemini"
"git.sr.ht/~adnano/go-gemini/certificate"
)
func main() {
@@ -20,8 +21,8 @@ func main() {
if err := server.Certificates.Load("/var/lib/gemini/certs"); err != nil {
log.Fatal(err)
}
server.CreateCertificate = func(hostname string) (tls.Certificate, error) {
return gemini.CreateCertificate(gemini.CertificateOptions{
server.GetCertificate = func(hostname string) (tls.Certificate, error) {
return certificate.Create(certificate.CreateOptions{
Subject: pkix.Name{
CommonName: hostname,
},
@@ -30,14 +31,14 @@ func main() {
})
}
server.RegisterFunc("localhost", stream)
server.HandleFunc("localhost", stream)
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
// stream writes an infinite stream to w.
func stream(w *gemini.ResponseWriter, r *gemini.Request) {
func stream(w gemini.ResponseWriter, r *gemini.Request) {
ch := make(chan string)
ctx, cancel := context.WithCancel(context.Background())

142
fs.go
View File

@@ -1,7 +1,6 @@
package gemini
import (
"fmt"
"io"
"mime"
"os"
@@ -10,67 +9,79 @@ import (
func init() {
// Add Gemini mime types
if err := mime.AddExtensionType(".gmi", "text/gemini"); err != nil {
panic(fmt.Errorf("failed to register .gmi extension mimetype: %w", err))
}
if err := mime.AddExtensionType(".gemini", "text/gemini"); err != nil {
panic(fmt.Errorf("failed to register .gemini extension mimetype: %w", err))
}
mime.AddExtensionType(".gmi", "text/gemini")
mime.AddExtensionType(".gemini", "text/gemini")
}
// FileServer takes a filesystem and returns a Responder which uses that filesystem.
// The returned Responder sanitizes paths before handling them.
func FileServer(fsys FS) Responder {
return fsHandler{fsys}
}
type fsHandler struct {
FS
}
func (fsh fsHandler) Respond(w *ResponseWriter, r *Request) {
p := path.Clean(r.URL.Path)
f, err := fsh.Open(p)
if err != nil {
w.Status(StatusNotFound)
return
}
// Detect mimetype
ext := path.Ext(p)
mimetype := mime.TypeByExtension(ext)
w.Meta(mimetype)
// Copy file to response writer
_, _ = io.Copy(w, f)
}
// TODO: replace with io/fs.FS when available
type FS interface {
// 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 FileSystem interface {
Open(name string) (File, error)
}
// TODO: replace with io/fs.File when available
// 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 File interface {
Stat() (os.FileInfo, error)
Read([]byte) (int, error)
Close() error
}
// Dir implements FS using the native filesystem restricted to a specific directory.
// A Dir implements FileSystem using the native file system restricted
// to a specific directory tree.
//
// While the FileSystem.Open method takes '/'-separated paths, a Dir's string
// value is a filename on the native file system, not a URL, so it is separated
// by filepath.Separator, which isn't necessarily '/'.
//
// Note that Dir could expose sensitive files and directories. Dir will follow
// symlinks pointing out of the directory tree, which can be especially
// dangerous if serving from a directory in which users are able to create
// arbitrary symlinks. Dir will also allow access to files and directories
// starting with a period, which could expose sensitive directories like .git
// or sensitive files like .htpasswd. To exclude files with a leading period,
// remove the files/directories from the server or create a custom FileSystem
// implementation.
//
// An empty Dir is treated as ".".
type Dir string
// Open tries to open the file with the given name.
// If the file is a directory, it tries to open the index file in that directory.
// Open implements FileSystem using os.Open, opening files for reading
// rooted and relative to the directory d.
func (d Dir) Open(name string) (File, error) {
p := path.Join(string(d), name)
return openFile(p)
return os.Open(path.Join(string(d), name))
}
// FileServer returns a handler that serves Gemini requests with the contents
// of the provided file system.
//
// To use the operating system's file system implementation, use gemini.Dir:
//
// gemini.FileServer(gemini.Dir("/tmp"))
func FileServer(fsys FileSystem) Handler {
return fileServer{fsys}
}
type fileServer struct {
FileSystem
}
func (fs fileServer) ServeGemini(w ResponseWriter, r *Request) {
ServeFile(w, fs, r.URL.Path)
}
// ServeFile responds to the request with the contents of the named file
// or directory.
// TODO: Use io/fs.FS when available.
func ServeFile(w *ResponseWriter, fs FS, name string) {
f, err := fs.Open(name)
//
// If the provided file or directory name is a relative path, it is interpreted
// relative to the current directory and may ascend to parent directories. If
// the provided name is constructed from user input, it should be sanitized
// before calling ServeFile.
func ServeFile(w ResponseWriter, fsys FileSystem, name string) {
f, err := openFile(fsys, name)
if err != nil {
w.Status(StatusNotFound)
return
@@ -83,29 +94,34 @@ func ServeFile(w *ResponseWriter, fs FS, name string) {
_, _ = io.Copy(w, f)
}
func openFile(p string) (File, error) {
f, err := os.OpenFile(p, os.O_RDONLY, 0644)
func openFile(fsys FileSystem, name string) (File, error) {
f, err := fsys.Open(name)
if err != nil {
return nil, err
}
if stat, err := f.Stat(); err == nil {
if stat.IsDir() {
f, err := os.Open(path.Join(p, "index.gmi"))
if err != nil {
return nil, err
}
stat, err := f.Stat()
if err != nil {
return nil, err
}
if stat.Mode().IsRegular() {
return f, nil
}
return nil, os.ErrNotExist
} else if !stat.Mode().IsRegular() {
return nil, os.ErrNotExist
stat, err := f.Stat()
if err != nil {
return nil, err
}
if stat.Mode().IsRegular() {
return f, nil
}
if stat.IsDir() {
// Try opening index.gmi
f, err := fsys.Open(path.Join(name, "index.gmi"))
if err != nil {
return nil, err
}
stat, err := f.Stat()
if err != nil {
return nil, err
}
if stat.Mode().IsRegular() {
return f, nil
}
}
return f, nil
return nil, os.ErrNotExist
}

View File

@@ -11,5 +11,18 @@ var (
ErrInvalidURL = errors.New("gemini: invalid URL")
ErrInvalidRequest = errors.New("gemini: invalid request")
ErrInvalidResponse = errors.New("gemini: invalid response")
ErrBodyNotAllowed = errors.New("gemini: response body not allowed")
// ErrBodyNotAllowed is returned by ResponseWriter.Write calls
// when the response status code does not permit a body.
ErrBodyNotAllowed = errors.New("gemini: response status code does not allow body")
// ErrServerClosed is returned by the Server's Serve and ListenAndServe
// methods after a call to Shutdown or Close.
ErrServerClosed = errors.New("gemini: server closed")
// ErrAbortHandler is a sentinel panic value to abort a handler.
// While any panic from ServeGemini aborts the response to the client,
// panicking with ErrAbortHandler also suppresses logging of a stack
// trace to the server's error log.
ErrAbortHandler = errors.New("net/http: abort Handler")
)

2
go.mod
View File

@@ -1,3 +1,5 @@
module git.sr.ht/~adnano/go-gemini
go 1.15
require golang.org/x/net v0.0.0-20210119194325-5f4716e94777

7
go.sum Normal file
View File

@@ -0,0 +1,7 @@
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

32
mux.go
View File

@@ -50,7 +50,7 @@ type ServeMux struct {
}
type muxEntry struct {
r Responder
r Handler
pattern string
}
@@ -78,7 +78,7 @@ func cleanPath(p string) string {
// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
func (mux *ServeMux) match(path string) Responder {
func (mux *ServeMux) match(path string) Handler {
// Check for exact match first.
v, ok := mux.m[path]
if ok {
@@ -130,9 +130,9 @@ func (mux *ServeMux) shouldRedirectRLocked(path string) bool {
return false
}
// Respond dispatches the request to the responder whose
// ServeGemini dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux) Respond(w *ResponseWriter, r *Request) {
func (mux *ServeMux) ServeGemini(w ResponseWriter, r *Request) {
path := cleanPath(r.URL.Path)
// If the given path is /tree and its handler is not registered,
@@ -157,20 +157,20 @@ func (mux *ServeMux) Respond(w *ResponseWriter, r *Request) {
w.Status(StatusNotFound)
return
}
resp.Respond(w, r)
resp.ServeGemini(w, r)
}
// Handle registers the responder for the given pattern.
// If a responder already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, responder Responder) {
// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()
if pattern == "" {
panic("gemini: invalid pattern")
}
if responder == nil {
panic("gemini: nil responder")
if handler == nil {
panic("gemini: nil handler")
}
if _, exist := mux.m[pattern]; exist {
panic("gemini: multiple registrations for " + pattern)
@@ -179,7 +179,7 @@ func (mux *ServeMux) Handle(pattern string, responder Responder) {
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
e := muxEntry{responder, pattern}
e := muxEntry{handler, pattern}
mux.m[pattern] = e
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e)
@@ -201,10 +201,10 @@ func appendSorted(es []muxEntry, e muxEntry) []muxEntry {
return es
}
// HandleFunc registers the responder function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, responder func(*ResponseWriter, *Request)) {
if responder == nil {
panic("gemini: nil responder")
// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("gemini: nil handler")
}
mux.Handle(pattern, ResponderFunc(responder))
mux.Handle(pattern, HandlerFunc(handler))
}

28
punycode.go Normal file
View File

@@ -0,0 +1,28 @@
package gemini
import (
"net"
"unicode/utf8"
"golang.org/x/net/idna"
)
func isASCII(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] >= utf8.RuneSelf {
return false
}
}
return true
}
// punycodeHostname returns the punycoded version of hostname.
func punycodeHostname(hostname string) (string, error) {
if net.ParseIP(hostname) != nil {
return hostname, nil
}
if isASCII(hostname) {
return hostname, nil
}
return idna.Lookup.ToASCII(hostname)
}

View File

@@ -9,69 +9,79 @@ import (
"net/url"
)
// Request represents a Gemini request.
// A Request represents a Gemini request received by a server or to be sent
// by a client.
//
// The field semantics differ slightly between client and server usage.
type Request struct {
// URL specifies the URL being requested.
// URL specifies the URL being requested (for server
// requests) or the URL to access (for client requests).
URL *url.URL
// For client requests, Host specifies the host on which the URL is sought.
// Host must contain a port.
//
// This field is ignored by the server.
// For client requests, Host optionally specifies the server to
// connect to. It must be of the form "host:port".
// If empty, the value of URL.Host is used.
// For international domain names, Host may be in Punycode or
// Unicode form. Use golang.org/x/net/idna to convert it to
// either format if needed.
// This field is ignored by the Gemini server.
Host string
// Certificate specifies the TLS certificate to use for the request.
//
// On the server side, if the client provided a certificate then
// Certificate.Leaf is guaranteed to be non-nil.
// For client requests, Certificate optionally specifies the
// TLS certificate to present to the other side of the connection.
// This field is ignored by the Gemini server.
Certificate *tls.Certificate
// RemoteAddr allows servers and other software to record the network
// address that sent the request.
//
// This field is ignored by the client.
// RemoteAddr allows Gemini servers and other software to record
// the network address that sent the request, usually for
// logging. This field is not filled in by ReadRequest and
// has no defined format. The Gemini server in this package
// sets RemoteAddr to an "IP:port" address before invoking a
// handler.
// This field is ignored by the Gemini client.
RemoteAddr net.Addr
// TLS allows servers and other software to record information about the TLS
// connection on which the request was received.
//
// This field is ignored by the client.
TLS tls.ConnectionState
// TLS allows Gemini servers and other software to record
// information about the TLS connection on which the request
// was received. This field is not filled in by ReadRequest.
// The Gemini server in this package sets the field for
// TLS-enabled connections before invoking a handler;
// otherwise it leaves the field nil.
// This field is ignored by the Gemini client.
TLS *tls.ConnectionState
// Context specifies the context to use for client requests.
// Context specifies the context to use for outgoing requests.
// The context controls the entire lifetime of a request and its
// response: obtaining a connection, sending the request, and
// reading the response header and body.
// If Context is nil, the background context will be used.
// This field is ignored by the Gemini server.
Context context.Context
}
// NewRequest returns a new request. The host is inferred from the URL.
// NewRequest returns a new request.
//
// The returned Request is suitable for use with Client.Do.
//
// Callers should be careful that the URL query is properly escaped.
// See the documentation for QueryEscape for more information.
func NewRequest(rawurl string) (*Request, error) {
u, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
return NewRequestFromURL(u), nil
return &Request{URL: u}, nil
}
// NewRequestFromURL returns a new request for the given URL.
// The host is inferred from the URL.
// ReadRequest reads and parses an incoming request from r.
//
// Callers should be careful that the URL query is properly escaped.
// See the documentation for QueryEscape for more information.
func NewRequestFromURL(url *url.URL) *Request {
host := url.Host
if url.Port() == "" {
host += ":1965"
}
return &Request{
URL: url,
Host: host,
}
}
// ReadRequest reads a Gemini request from the provided io.Reader
// ReadRequest is a low-level function and should only be used
// for specialized applications; most code should use the Server
// to read requests and handle them via the Handler interface.
func ReadRequest(r io.Reader) (*Request, error) {
// Read URL
br := bufio.NewReader(r)
r = io.LimitReader(r, 1026)
br := bufio.NewReaderSize(r, 1026)
rawurl, err := br.ReadString('\r')
if err != nil {
return nil, err
@@ -92,19 +102,15 @@ func ReadRequest(r io.Reader) (*Request, error) {
if err != nil {
return nil, err
}
if u.User != nil {
// User is not allowed
return nil, ErrInvalidURL
}
return &Request{URL: u}, nil
}
// Write writes the Gemini request to the provided buffered writer.
// Write writes a Gemini request in wire format.
// This method consults the request URL only.
func (r *Request) Write(w *bufio.Writer) error {
url := r.URL.String()
// User is invalid
if r.URL.User != nil || len(url) > 1024 {
return ErrInvalidURL
if len(url) > 1024 {
return ErrInvalidRequest
}
if _, err := w.WriteString(url); err != nil {
return err

132
request_test.go Normal file
View File

@@ -0,0 +1,132 @@
package gemini
import (
"bufio"
"io"
"net/url"
"strings"
"testing"
)
// 1024 bytes
const maxURL = "gemini://example.net/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
func TestReadRequest(t *testing.T) {
tests := []struct {
Raw string
URL *url.URL
Err error
}{
{
Raw: "gemini://example.com\r\n",
URL: &url.URL{
Scheme: "gemini",
Host: "example.com",
},
},
{
Raw: "http://example.org/path/?query#fragment\r\n",
URL: &url.URL{
Scheme: "http",
Host: "example.org",
Path: "/path/",
RawQuery: "query",
Fragment: "fragment",
},
},
{
Raw: "\r\n",
URL: &url.URL{},
},
{
Raw: "gemini://example.com\n",
Err: io.EOF,
},
{
Raw: "gemini://example.com",
Err: io.EOF,
},
{
// 1030 bytes
Raw: maxURL + "xxxxxx",
Err: io.EOF,
},
{
// 1027 bytes
Raw: maxURL + "x" + "\r\n",
Err: io.EOF,
},
{
// 1024 bytes
Raw: maxURL[:len(maxURL)-2] + "\r\n",
URL: &url.URL{
Scheme: "gemini",
Host: "example.net",
Path: maxURL[len("gemini://example.net") : len(maxURL)-2],
},
},
}
for _, test := range tests {
t.Logf("%#v", test.Raw)
req, err := ReadRequest(strings.NewReader(test.Raw))
if err != test.Err {
t.Errorf("expected err = %v, got %v", test.Err, err)
}
if req == nil && test.URL != nil {
t.Errorf("expected url = %s, got nil", test.URL)
} else if req != nil && test.URL == nil {
t.Errorf("expected req = nil, got %v", req)
} else if req != nil && *req.URL != *test.URL {
t.Errorf("expected url = %v, got %v", *test.URL, *req.URL)
}
}
}
func newRequest(rawurl string) *Request {
req, err := NewRequest(rawurl)
if err != nil {
panic(err)
}
return req
}
func TestWriteRequest(t *testing.T) {
tests := []struct {
Req *Request
Raw string
Err error
}{
{
Req: newRequest("gemini://example.com"),
Raw: "gemini://example.com\r\n",
},
{
Req: newRequest("gemini://example.com/path/?query#fragment"),
Raw: "gemini://example.com/path/?query#fragment\r\n",
},
{
Req: newRequest(maxURL),
Raw: maxURL + "\r\n",
},
{
Req: newRequest(maxURL + "x"),
Err: ErrInvalidRequest,
},
}
for _, test := range tests {
t.Logf("%s", test.Req.URL)
var b strings.Builder
bw := bufio.NewWriter(&b)
err := test.Req.Write(bw)
if err != test.Err {
t.Errorf("expected err = %v, got %v", test.Err, err)
}
bw.Flush()
got := b.String()
if got != test.Raw {
t.Errorf("expected %#v, got %#v", test.Raw, got)
}
}
}

View File

@@ -7,10 +7,14 @@ import (
"strconv"
)
// Response is a Gemini response.
// Response represents the response from a Gemini request.
//
// The Client returns Responses from servers once the response
// header has been received. The response body is streamed on demand
// as the Body field is read.
type Response struct {
// Status contains the response status code.
Status Status
Status int
// Meta contains more information related to the response status.
// For successful responses, Meta should contain the media type of the response.
@@ -18,12 +22,21 @@ type Response struct {
// Meta should not be longer than 1024 bytes.
Meta string
// Body contains the response body for successful responses.
// Body represents the response body.
//
// The response body is streamed on demand as the Body field
// is read. If the network connection fails or the server
// terminates the response, Body.Read calls return an error.
//
// The Gemini client guarantees that Body is always
// non-nil, even on responses without a body or responses with
// a zero-length body. It is the caller's responsibility to
// close Body.
Body io.ReadCloser
// TLS contains information about the TLS connection on which the response
// was received.
TLS tls.ConnectionState
// TLS contains information about the TLS connection on which the
// response was received. It is nil for unencrypted responses.
TLS *tls.ConnectionState
}
// ReadResponse reads a Gemini response from the provided io.ReadCloser.
@@ -38,16 +51,9 @@ func ReadResponse(rc io.ReadCloser) (*Response, error) {
}
status, err := strconv.Atoi(string(statusB))
if err != nil {
return nil, err
}
resp.Status = Status(status)
// Disregard invalid status codes
const minStatus, maxStatus = 1, 6
statusClass := resp.Status.Class()
if statusClass < minStatus || statusClass > maxStatus {
return nil, ErrInvalidResponse
}
resp.Status = status
// Read one space
if b, err := br.ReadByte(); err != nil {
@@ -68,7 +74,7 @@ func ReadResponse(rc io.ReadCloser) (*Response, error) {
return nil, ErrInvalidResponse
}
// Default mime type of text/gemini; charset=utf-8
if statusClass == StatusClassSuccess && meta == "" {
if StatusClass(status) == StatusSuccess && meta == "" {
meta = "text/gemini; charset=utf-8"
}
resp.Meta = meta
@@ -80,14 +86,25 @@ func ReadResponse(rc io.ReadCloser) (*Response, error) {
return nil, ErrInvalidResponse
}
if resp.Status.Class() == StatusClassSuccess {
if StatusClass(status) == StatusSuccess {
resp.Body = newReadCloserBody(br, rc)
} else {
resp.Body = nopReadCloser{}
rc.Close()
}
return resp, nil
}
type nopReadCloser struct{}
func (nopReadCloser) Read(p []byte) (int, error) {
return 0, io.EOF
}
func (nopReadCloser) Close() error {
return nil
}
type readCloserBody struct {
br *bufio.Reader // used until empty
io.ReadCloser
@@ -115,52 +132,68 @@ func (b *readCloserBody) Read(p []byte) (n int, err error) {
return b.ReadCloser.Read(p)
}
// ResponseWriter is used to construct a Gemini response.
type ResponseWriter struct {
// A ResponseWriter interface is used by a Gemini handler
// to construct a Gemini response.
type ResponseWriter interface {
// Header sets the response header.
Header(status int, meta string)
// Status sets the response status code.
// It also sets the response meta to Meta(status).
Status(status int)
// Meta sets the response meta.
//
// For successful responses, meta should contain the media type of the response.
// For failure responses, meta should contain a short description of the failure.
// The response meta should not be greater than 1024 bytes.
Meta(meta string)
// Write writes data to the connection as part of the response body.
// If the response status does not allow for a response body, Write returns
// ErrBodyNotAllowed.
//
// Write writes the response header if it has not already been written.
// It writes a successful status code if one is not set.
Write([]byte) (int, error)
// Flush writes any buffered data to the underlying io.Writer.
//
// Flush writes the response header if it has not already been written.
// It writes a failure status code if one is not set.
Flush() error
}
type responseWriter struct {
b *bufio.Writer
status Status
status int
meta string
setHeader bool
wroteHeader bool
bodyAllowed bool
}
// NewResponseWriter returns a ResponseWriter that uses the provided io.Writer.
func NewResponseWriter(w io.Writer) *ResponseWriter {
return &ResponseWriter{
func NewResponseWriter(w io.Writer) ResponseWriter {
return &responseWriter{
b: bufio.NewWriter(w),
}
}
// Header sets the response header.
func (w *ResponseWriter) Header(status Status, meta string) {
func (w *responseWriter) Header(status int, meta string) {
w.status = status
w.meta = meta
}
// Status sets the response status code.
// It also sets the response meta to status.Meta().
func (w *ResponseWriter) Status(status Status) {
func (w *responseWriter) Status(status int) {
w.status = status
w.meta = status.Meta()
w.meta = Meta(status)
}
// Meta sets the response meta.
//
// For successful responses, meta should contain the media type of the response.
// For failure responses, meta should contain a short description of the failure.
// The response meta should not be greater than 1024 bytes.
func (w *ResponseWriter) Meta(meta string) {
func (w *responseWriter) Meta(meta string) {
w.meta = meta
}
// Write writes data to the connection as part of the response body.
// If the response status does not allow for a response body, Write returns
// ErrBodyNotAllowed.
//
// Write writes the response header if it has not already been written.
// It writes a successful status code if one is not set.
func (w *ResponseWriter) Write(b []byte) (int, error) {
func (w *responseWriter) Write(b []byte) (int, error) {
if !w.wroteHeader {
w.writeHeader(StatusSuccess)
}
@@ -170,14 +203,14 @@ func (w *ResponseWriter) Write(b []byte) (int, error) {
return w.b.Write(b)
}
func (w *ResponseWriter) writeHeader(defaultStatus Status) {
func (w *responseWriter) writeHeader(defaultStatus int) {
status := w.status
if status == 0 {
status = defaultStatus
}
meta := w.meta
if status.Class() == StatusClassSuccess {
if StatusClass(status) == StatusSuccess {
w.bodyAllowed = true
if meta == "" {
@@ -185,18 +218,14 @@ func (w *ResponseWriter) writeHeader(defaultStatus Status) {
}
}
w.b.WriteString(strconv.Itoa(int(status)))
w.b.WriteString(strconv.Itoa(status))
w.b.WriteByte(' ')
w.b.WriteString(meta)
w.b.Write(crlf)
w.wroteHeader = true
}
// Flush writes any buffered data to the underlying io.Writer.
//
// Flush writes the response header if it has not already been written.
// It writes a failure status code if one is not set.
func (w *ResponseWriter) Flush() error {
func (w *responseWriter) Flush() error {
if !w.wroteHeader {
w.writeHeader(StatusTemporaryFailure)
}

104
response_test.go Normal file
View File

@@ -0,0 +1,104 @@
package gemini
import (
"io"
"io/ioutil"
"strings"
"testing"
)
func TestReadResponse(t *testing.T) {
tests := []struct {
Raw string
Status int
Meta string
Body string
Err error
}{
{
Raw: "20 text/gemini\r\nHello, world!\nWelcome to my capsule.",
Status: 20,
Meta: "text/gemini",
Body: "Hello, world!\nWelcome to my capsule.",
},
{
Raw: "10 Search query\r\n",
Status: 10,
Meta: "Search query",
},
{
Raw: "30 /redirect\r\n",
Status: 30,
Meta: "/redirect",
},
{
Raw: "31 /redirect\r\nThis body is ignored.",
Status: 31,
Meta: "/redirect",
},
{
Raw: "99 Unknown status code\r\n",
Status: 99,
Meta: "Unknown status code",
},
{
Raw: "\r\n",
Err: ErrInvalidResponse,
},
{
Raw: "\n",
Err: ErrInvalidResponse,
},
{
Raw: "1 Bad response\r\n",
Err: ErrInvalidResponse,
},
{
Raw: "",
Err: io.EOF,
},
{
Raw: "10 Search query",
Err: io.EOF,
},
{
Raw: "20 text/gemini\nHello, world!",
Err: io.EOF,
},
{
Raw: "20 text/gemini\rHello, world!",
Err: ErrInvalidResponse,
},
{
Raw: "20 text/gemini\r",
Err: io.EOF,
},
{
Raw: "abcdefghijklmnopqrstuvwxyz",
Err: ErrInvalidResponse,
},
}
for _, test := range tests {
t.Logf("%#v", test.Raw)
resp, err := ReadResponse(ioutil.NopCloser(strings.NewReader(test.Raw)))
if err != test.Err {
t.Errorf("expected err = %v, got %v", test.Err, err)
}
if test.Err != nil {
// No response
continue
}
if resp.Status != test.Status {
t.Errorf("expected status = %d, got %d", test.Status, resp.Status)
}
if resp.Meta != test.Meta {
t.Errorf("expected meta = %s, got %s", test.Meta, resp.Meta)
}
b, _ := ioutil.ReadAll(resp.Body)
body := string(b)
if body != test.Body {
t.Errorf("expected body = %#v, got %#v", test.Body, body)
}
}
}

382
server.go
View File

@@ -1,68 +1,93 @@
package gemini
import (
"context"
"crypto/tls"
"errors"
"log"
"net"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
"git.sr.ht/~adnano/go-gemini/certificate"
)
// Server is a Gemini server.
// A Server defines parameters for running a Gemini server. The zero value for
// Server is a valid configuration.
type Server struct {
// Addr specifies the address that the server should listen on.
// If Addr is empty, the server will listen on the address ":1965".
// Addr optionally specifies the TCP address for the server to listen on,
// in the form "host:port". If empty, ":1965" (port 1965) is used.
// See net.Dial for details of the address format.
Addr string
// ReadTimeout is the maximum duration for reading a request.
// ReadTimeout is the maximum duration for reading the entire
// request.
//
// A ReadTimeout of zero means no timeout.
ReadTimeout time.Duration
// WriteTimeout is the maximum duration before timing out
// writes of the response.
//
// A WriteTimeout of zero means no timeout.
WriteTimeout time.Duration
// Certificates contains the certificates used by the server.
Certificates CertificateDir
// Certificates contains one or more certificates to present to the
// other side of the connection.
Certificates certificate.Dir
// CreateCertificate, if not nil, will be called to create a new certificate
// GetCertificate, if not nil, will be called to retrieve a new certificate
// if the current one is expired or missing.
CreateCertificate func(hostname string) (tls.Certificate, error)
GetCertificate func(hostname string) (tls.Certificate, error)
// ErrorLog specifies an optional logger for errors accepting connections
// and file system errors.
// ErrorLog specifies an optional logger for errors accepting connections,
// unexpected behavior from handlers, and underlying file system errors.
// If nil, logging is done via the log package's standard logger.
ErrorLog *log.Logger
// registered responders
responders map[responderKey]Responder
hosts map[string]bool
// registered handlers
handlers map[handlerKey]Handler
hosts map[string]bool
hmu sync.Mutex
listeners map[*net.Listener]struct{}
conns map[*net.Conn]struct{}
done int32
mu sync.Mutex
}
type responderKey struct {
type handlerKey struct {
scheme string
hostname string
}
// Register registers a responder for the given pattern.
// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
//
// Patterns must be in the form of "hostname" or "scheme://hostname".
// The pattern must be in the form of "hostname" or "scheme://hostname".
// If no scheme is specified, a scheme of "gemini://" is implied.
// Wildcard patterns are supported (e.g. "*.example.com").
func (s *Server) Register(pattern string, responder Responder) {
// To handle any hostname, use the wildcard pattern "*".
func (srv *Server) Handle(pattern string, handler Handler) {
srv.hmu.Lock()
defer srv.hmu.Unlock()
if pattern == "" {
panic("gemini: invalid pattern")
}
if responder == nil {
panic("gemini: nil responder")
if handler == nil {
panic("gemini: nil handler")
}
if s.responders == nil {
s.responders = map[responderKey]Responder{}
s.hosts = map[string]bool{}
if srv.handlers == nil {
srv.handlers = map[handlerKey]Handler{}
srv.hosts = map[string]bool{}
}
split := strings.SplitN(pattern, "://", 2)
var key responderKey
var key handlerKey
if len(split) == 2 {
key.scheme = split[0]
key.hostname = split[1]
@@ -71,21 +96,32 @@ func (s *Server) Register(pattern string, responder Responder) {
key.hostname = split[0]
}
if _, ok := s.responders[key]; ok {
if _, ok := srv.handlers[key]; ok {
panic("gemini: multiple registrations for " + pattern)
}
s.responders[key] = responder
s.hosts[key.hostname] = true
srv.handlers[key] = handler
srv.hosts[key.hostname] = true
}
// RegisterFunc registers a responder function for the given pattern.
func (s *Server) RegisterFunc(pattern string, responder func(*ResponseWriter, *Request)) {
s.Register(pattern, ResponderFunc(responder))
// HandleFunc registers the handler function for the given pattern.
func (srv *Server) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
srv.Handle(pattern, HandlerFunc(handler))
}
// ListenAndServe listens for requests at the server's configured address.
func (s *Server) ListenAndServe() error {
addr := s.Addr
// ListenAndServe listens on the TCP network address srv.Addr and then calls
// Serve to handle requests on incoming connections.
//
// If srv.Addr is blank, ":1965" is used.
//
// ListenAndServe always returns a non-nil error. After Shutdown or Close, the
// returned error is ErrServerClosed.
func (srv *Server) ListenAndServe() error {
if atomic.LoadInt32(&srv.done) == 1 {
return ErrServerClosed
}
addr := srv.Addr
if addr == "" {
addr = ":1965"
}
@@ -96,20 +132,52 @@ func (s *Server) ListenAndServe() error {
}
defer ln.Close()
return s.Serve(tls.NewListener(ln, &tls.Config{
return srv.Serve(tls.NewListener(ln, &tls.Config{
ClientAuth: tls.RequestClientCert,
MinVersion: tls.VersionTLS12,
GetCertificate: s.getCertificate,
GetCertificate: srv.getCertificate,
}))
}
// Serve listens for requests on the provided listener.
func (s *Server) Serve(l net.Listener) error {
func (srv *Server) trackListener(l *net.Listener) {
srv.mu.Lock()
defer srv.mu.Unlock()
if srv.listeners == nil {
srv.listeners = make(map[*net.Listener]struct{})
}
srv.listeners[l] = struct{}{}
}
func (srv *Server) deleteListener(l *net.Listener) {
srv.mu.Lock()
defer srv.mu.Unlock()
delete(srv.listeners, l)
}
// Serve accepts incoming connections on the Listener l, creating a new
// service goroutine for each. The service goroutines read requests and
// then calls the appropriate Handler to reply to them.
//
// Serve always returns a non-nil error and closes l. After Shutdown or Close,
// the returned error is ErrServerClosed.
func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
srv.trackListener(&l)
defer srv.deleteListener(&l)
if atomic.LoadInt32(&srv.done) == 1 {
return ErrServerClosed
}
var tempDelay time.Duration // how long to sleep on accept failure
for {
rw, err := l.Accept()
if err != nil {
if atomic.LoadInt32(&srv.done) == 1 {
return ErrServerClosed
}
// If this is a temporary error, sleep
if ne, ok := err.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
@@ -120,7 +188,7 @@ func (s *Server) Serve(l net.Listener) error {
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
s.logf("gemini: Accept error: %v; retrying in %v", err, tempDelay)
srv.logf("gemini: Accept error: %v; retrying in %v", err, tempDelay)
time.Sleep(tempDelay)
continue
}
@@ -130,118 +198,264 @@ func (s *Server) Serve(l net.Listener) error {
}
tempDelay = 0
go s.respond(rw)
go srv.respond(rw)
}
}
func (s *Server) getCertificate(h *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := s.getCertificateFor(h.ServerName)
func (srv *Server) closeListenersLocked() error {
var err error
for ln := range srv.listeners {
if cerr := (*ln).Close(); cerr != nil && err == nil {
err = cerr
}
delete(srv.listeners, ln)
}
return err
}
// Close immediately closes all active net.Listeners and connections.
// For a graceful shutdown, use Shutdown.
//
// Close returns any error returned from closing the Server's
// underlying Listener(s).
func (srv *Server) Close() error {
srv.mu.Lock()
defer srv.mu.Unlock()
if !atomic.CompareAndSwapInt32(&srv.done, 0, 1) {
return ErrServerClosed
}
err := srv.closeListenersLocked()
// Close active connections
for conn := range srv.conns {
(*conn).Close()
delete(srv.conns, conn)
}
return err
}
func (srv *Server) numConns() int {
srv.mu.Lock()
defer srv.mu.Unlock()
return len(srv.conns)
}
// shutdownPollInterval is how often we poll for quiescence
// during Server.Shutdown. This is lower during tests, to
// speed up tests.
// Ideally we could find a solution that doesn't involve polling,
// but which also doesn't have a high runtime cost (and doesn't
// involve any contentious mutexes), but that is left as an
// exercise for the reader.
var shutdownPollInterval = 500 * time.Millisecond
// Shutdown gracefully shuts down the server without interrupting any
// active connections. Shutdown works by first closing all open
// listeners and then waiting indefinitely for connections
// to close and then shut down.
// If the provided context expires before the shutdown is complete,
// Shutdown returns the context's error, otherwise it returns any
// error returned from closing the Server's underlying Listener(s).
//
// When Shutdown is called, Serve, ListenAndServe, and
// ListenAndServeTLS immediately return ErrServerClosed. Make sure the
// program doesn't exit and waits instead for Shutdown to return.
//
// Once Shutdown has been called on a server, it may not be reused;
// future calls to methods such as Serve will return ErrServerClosed.
func (srv *Server) Shutdown(ctx context.Context) error {
if !atomic.CompareAndSwapInt32(&srv.done, 0, 1) {
return ErrServerClosed
}
srv.mu.Lock()
err := srv.closeListenersLocked()
srv.mu.Unlock()
// Wait for active connections to close
ticker := time.NewTicker(shutdownPollInterval)
defer ticker.Stop()
for {
if srv.numConns() == 0 {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
}
}
// getCertificate retrieves a certificate for the given client hello.
func (srv *Server) getCertificate(h *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := srv.lookupCertificate(h.ServerName, h.ServerName)
if err != nil {
// Try wildcard
wildcard := strings.SplitN(h.ServerName, ".", 2)
if len(wildcard) == 2 {
cert, err = s.getCertificateFor("*." + wildcard[1])
// Use the wildcard pattern as the hostname.
hostname := "*." + wildcard[1]
cert, err = srv.lookupCertificate(hostname, hostname)
}
// Try "*" wildcard
if err != nil {
// Use the server name as the hostname
// since "*" is not a valid hostname.
cert, err = srv.lookupCertificate("*", h.ServerName)
}
}
return cert, err
}
func (s *Server) getCertificateFor(hostname string) (*tls.Certificate, error) {
if _, ok := s.hosts[hostname]; !ok {
// lookupCertificate retrieves the certificate for the given hostname,
// if and only if the provided pattern is registered.
// If no certificate is found in the certificate store or the certificate
// is expired, it calls GetCertificate to retrieve a new certificate.
func (srv *Server) lookupCertificate(pattern, hostname string) (*tls.Certificate, error) {
srv.hmu.Lock()
_, ok := srv.hosts[pattern]
srv.hmu.Unlock()
if !ok {
return nil, errors.New("hostname not registered")
}
// Generate a new certificate if it is missing or expired
cert, ok := s.Certificates.Lookup(hostname)
cert, ok := srv.Certificates.Lookup(hostname)
if !ok || cert.Leaf != nil && cert.Leaf.NotAfter.Before(time.Now()) {
if s.CreateCertificate != nil {
cert, err := s.CreateCertificate(hostname)
if srv.GetCertificate != nil {
cert, err := srv.GetCertificate(hostname)
if err == nil {
s.Certificates.Add(hostname, cert)
if err := s.Certificates.Write(hostname, cert); err != nil {
s.logf("gemini: Failed to write new certificate for %s: %s", hostname, err)
if err := srv.Certificates.Add(hostname, cert); err != nil {
srv.logf("gemini: Failed to write new certificate for %s: %s", hostname, err)
}
}
return &cert, err
}
return nil, errors.New("no certificate")
}
return &cert, nil
}
// respond responds to a connection.
func (s *Server) respond(conn net.Conn) {
defer conn.Close()
if d := s.ReadTimeout; d != 0 {
_ = conn.SetReadDeadline(time.Now().Add(d))
func (srv *Server) trackConn(conn *net.Conn) {
srv.mu.Lock()
defer srv.mu.Unlock()
if srv.conns == nil {
srv.conns = make(map[*net.Conn]struct{})
}
if d := s.WriteTimeout; d != 0 {
_ = conn.SetWriteDeadline(time.Now().Add(d))
srv.conns[conn] = struct{}{}
}
func (srv *Server) deleteConn(conn *net.Conn) {
srv.mu.Lock()
defer srv.mu.Unlock()
delete(srv.conns, conn)
}
// respond responds to a connection.
func (srv *Server) respond(conn net.Conn) {
defer conn.Close()
defer func() {
if err := recover(); err != nil && err != ErrAbortHandler {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
srv.logf("gemini: panic serving %v: %v\n%s", conn.RemoteAddr(), err, buf)
}
}()
srv.trackConn(&conn)
defer srv.deleteConn(&conn)
if d := srv.ReadTimeout; d != 0 {
conn.SetReadDeadline(time.Now().Add(d))
}
if d := srv.WriteTimeout; d != 0 {
conn.SetWriteDeadline(time.Now().Add(d))
}
w := NewResponseWriter(conn)
defer func() {
_ = w.Flush()
}()
req, err := ReadRequest(conn)
if err != nil {
w.Status(StatusBadRequest)
w.Flush()
return
}
// Store information about the TLS connection
if tlsConn, ok := conn.(*tls.Conn); ok {
req.TLS = tlsConn.ConnectionState()
if len(req.TLS.PeerCertificates) > 0 {
peerCert := req.TLS.PeerCertificates[0]
// Store the TLS certificate
req.Certificate = &tls.Certificate{
Certificate: [][]byte{peerCert.Raw},
Leaf: peerCert,
}
}
state := tlsConn.ConnectionState()
req.TLS = &state
}
resp := s.responder(req)
if resp == nil {
// Store remote address
req.RemoteAddr = conn.RemoteAddr()
h := srv.handler(req)
if h == nil {
w.Status(StatusNotFound)
w.Flush()
return
}
resp.Respond(w, req)
h.ServeGemini(w, req)
w.Flush()
}
func (s *Server) responder(r *Request) Responder {
if h, ok := s.responders[responderKey{r.URL.Scheme, r.URL.Hostname()}]; ok {
func (srv *Server) handler(r *Request) Handler {
srv.hmu.Lock()
defer srv.hmu.Unlock()
if h, ok := srv.handlers[handlerKey{r.URL.Scheme, r.URL.Hostname()}]; ok {
return h
}
wildcard := strings.SplitN(r.URL.Hostname(), ".", 2)
if len(wildcard) == 2 {
if h, ok := s.responders[responderKey{r.URL.Scheme, "*." + wildcard[1]}]; ok {
if h, ok := srv.handlers[handlerKey{r.URL.Scheme, "*." + wildcard[1]}]; ok {
return h
}
}
if h, ok := srv.handlers[handlerKey{r.URL.Scheme, "*"}]; ok {
return h
}
return nil
}
func (s *Server) logf(format string, args ...interface{}) {
if s.ErrorLog != nil {
s.ErrorLog.Printf(format, args...)
func (srv *Server) logf(format string, args ...interface{}) {
if srv.ErrorLog != nil {
srv.ErrorLog.Printf(format, args...)
} else {
log.Printf(format, args...)
}
}
// A Responder responds to a Gemini request.
type Responder interface {
// Respond accepts a Request and constructs a Response.
Respond(*ResponseWriter, *Request)
// A Handler responds to a Gemini request.
//
// ServeGemini should write the response header and data to the ResponseWriter
// and then return. Returning signals that the request is finished; it is not
// valid to use the ResponseWriter after or concurrently with the completion
// of the ServeGemini call.
//
// Handlers should not modify the provided Request.
//
// If ServeGemini panics, the server (the caller of ServeGemini) assumes that
// the effect of the panic was isolated to the active request. It recovers
// the panic, logs a stack trace to the server error log, and closes the
// network connection. To abort a handler so the client sees an interrupted
// response but the server doesn't log an error, panic with the value
// ErrAbortHandler.
type Handler interface {
ServeGemini(ResponseWriter, *Request)
}
// ResponderFunc is a wrapper around a bare function that implements Responder.
type ResponderFunc func(*ResponseWriter, *Request)
// The HandlerFunc type is an adapter to allow the use of ordinary functions
// as Gemini handlers. If f is a function with the appropriate signature,
// HandlerFunc(f) is a Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)
func (f ResponderFunc) Respond(w *ResponseWriter, r *Request) {
// ServeGemini calls f(w, r).
func (f HandlerFunc) ServeGemini(w ResponseWriter, r *Request) {
f(w, r)
}

View File

@@ -1,51 +1,39 @@
package gemini
// Status codes.
type Status int
// Gemini status codes.
const (
StatusInput Status = 10
StatusSensitiveInput Status = 11
StatusSuccess Status = 20
StatusRedirect Status = 30
StatusPermanentRedirect Status = 31
StatusTemporaryFailure Status = 40
StatusServerUnavailable Status = 41
StatusCGIError Status = 42
StatusProxyError Status = 43
StatusSlowDown Status = 44
StatusPermanentFailure Status = 50
StatusNotFound Status = 51
StatusGone Status = 52
StatusProxyRequestRefused Status = 53
StatusBadRequest Status = 59
StatusCertificateRequired Status = 60
StatusCertificateNotAuthorized Status = 61
StatusCertificateNotValid Status = 62
StatusInput = 10
StatusSensitiveInput = 11
StatusSuccess = 20
StatusRedirect = 30
StatusPermanentRedirect = 31
StatusTemporaryFailure = 40
StatusServerUnavailable = 41
StatusCGIError = 42
StatusProxyError = 43
StatusSlowDown = 44
StatusPermanentFailure = 50
StatusNotFound = 51
StatusGone = 52
StatusProxyRequestRefused = 53
StatusBadRequest = 59
StatusCertificateRequired = 60
StatusCertificateNotAuthorized = 61
StatusCertificateNotValid = 62
)
// Status code categories.
type StatusClass int
const (
StatusClassInput StatusClass = 1
StatusClassSuccess StatusClass = 2
StatusClassRedirect StatusClass = 3
StatusClassTemporaryFailure StatusClass = 4
StatusClassPermanentFailure StatusClass = 5
StatusClassCertificateRequired StatusClass = 6
)
// Class returns the status class for this status code.
func (s Status) Class() StatusClass {
return StatusClass(s / 10)
// StatusClass returns the status class for this status code.
// 1x becomes 10, 2x becomes 20, and so on.
func StatusClass(status int) int {
return (status / 10) * 10
}
// Meta returns a description of the status code appropriate for use in a response.
// Meta returns a description of the provided status code appropriate
// for use in a response.
//
// Meta returns an empty string for input, success, and redirect status codes.
func (s Status) Meta() string {
switch s {
func Meta(status int) string {
switch status {
case StatusTemporaryFailure:
return "Temporary failure"
case StatusServerUnavailable:

View File

@@ -3,156 +3,415 @@ package tofu
import (
"bufio"
"bytes"
"crypto/sha512"
"crypto/x509"
"errors"
"fmt"
"io"
"os"
"sort"
"strconv"
"strings"
"sync"
"time"
)
// KnownHosts maps hosts to fingerprints.
type KnownHosts map[string]Fingerprint
// KnownHostsFile represents a list of known hosts optionally loaded from a file.
// The zero value for KnownHostsFile represents an empty list ready to use.
// KnownHosts represents a list of known hosts.
// The zero value for KnownHosts represents an empty list ready to use.
//
// KnownHostsFile is safe for concurrent use by multiple goroutines.
type KnownHostsFile struct {
KnownHosts
out io.Writer
mu sync.RWMutex
// KnownHosts is safe for concurrent use by multiple goroutines.
type KnownHosts struct {
hosts map[string]Host
mu sync.RWMutex
}
// SetOutput sets the output to which new known hosts will be written to.
func (k *KnownHostsFile) SetOutput(w io.Writer) {
// Add adds a host to the list of known hosts.
func (k *KnownHosts) Add(h Host) {
k.mu.Lock()
defer k.mu.Unlock()
k.out = w
}
// Add adds a known host to the list of known hosts.
func (k *KnownHostsFile) Add(hostname string, fingerprint Fingerprint) {
k.mu.Lock()
defer k.mu.Unlock()
if k.KnownHosts == nil {
k.KnownHosts = KnownHosts{}
if k.hosts == nil {
k.hosts = map[string]Host{}
}
k.KnownHosts[hostname] = fingerprint
k.hosts[h.Hostname] = h
}
// Lookup returns the fingerprint of the certificate corresponding to
// the given hostname.
func (k *KnownHostsFile) Lookup(hostname string) (Fingerprint, bool) {
// Lookup returns the known host entry corresponding to the given hostname.
func (k *KnownHosts) Lookup(hostname string) (Host, bool) {
k.mu.RLock()
defer k.mu.RUnlock()
c, ok := k.KnownHosts[hostname]
c, ok := k.hosts[hostname]
return c, ok
}
// Write writes a known hosts entry to the configured output.
func (k *KnownHostsFile) Write(hostname string, fingerprint Fingerprint) error {
// Entries returns the known host entries sorted by hostname.
func (k *KnownHosts) Entries() []Host {
keys := make([]string, 0, len(k.hosts))
for key := range k.hosts {
keys = append(keys, key)
}
sort.Strings(keys)
hosts := make([]Host, 0, len(k.hosts))
for _, key := range keys {
hosts = append(hosts, k.hosts[key])
}
return hosts
}
// WriteTo writes the list of known hosts to the provided io.Writer.
func (k *KnownHosts) WriteTo(w io.Writer) (int64, error) {
k.mu.RLock()
defer k.mu.RUnlock()
if k.out != nil {
_, err := k.writeKnownHost(k.out, hostname, fingerprint)
var written int
bw := bufio.NewWriter(w)
for _, h := range k.hosts {
n, err := bw.WriteString(h.String())
written += n
if err != nil {
return fmt.Errorf("failed to write to known host file: %w", err)
return int64(written), err
}
bw.WriteByte('\n')
written += 1
}
return nil
return int64(written), bw.Flush()
}
// WriteAll writes all of the known hosts to the provided io.Writer.
func (k *KnownHostsFile) WriteAll(w io.Writer) error {
k.mu.RLock()
defer k.mu.RUnlock()
for h, c := range k.KnownHosts {
if _, err := k.writeKnownHost(w, h, c); err != nil {
return err
}
}
return nil
}
// writeKnownHost writes a known host to the provided io.Writer.
func (k *KnownHostsFile) writeKnownHost(w io.Writer, hostname string, f Fingerprint) (int, error) {
return fmt.Fprintf(w, "%s %s %s %d\n", hostname, f.Algorithm, f.Hex, f.Expires.Unix())
}
// Load loads the known hosts from the provided path.
// It creates the file if it does not exist.
// New known hosts will be appended to the file.
func (k *KnownHostsFile) Load(path string) error {
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644)
// Load loads the known hosts entries from the provided path.
func (k *KnownHosts) Load(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
k.Parse(f)
k.SetOutput(f)
return nil
defer f.Close()
return k.Parse(f)
}
// Parse parses the provided reader and adds the parsed known hosts to the list.
// Parse parses the provided io.Reader and adds the parsed hosts to the list.
// Invalid entries are ignored.
func (k *KnownHostsFile) Parse(r io.Reader) {
//
// For more control over errors encountered during parsing, use bufio.Scanner
// in combination with ParseHost. For example:
//
// var knownHosts tofu.KnownHosts
// scanner := bufio.NewScanner(r)
// for scanner.Scan() {
// host, err := tofu.ParseHost(scanner.Bytes())
// if err != nil {
// // handle error
// } else {
// knownHosts.Add(host)
// }
// }
// err := scanner.Err()
// if err != nil {
// // handle error
// }
//
func (k *KnownHosts) Parse(r io.Reader) error {
k.mu.Lock()
defer k.mu.Unlock()
if k.KnownHosts == nil {
k.KnownHosts = map[string]Fingerprint{}
if k.hosts == nil {
k.hosts = map[string]Host{}
}
scanner := bufio.NewScanner(r)
for scanner.Scan() {
text := scanner.Text()
parts := strings.Split(text, " ")
if len(parts) < 4 {
text := scanner.Bytes()
if len(text) == 0 {
continue
}
hostname := parts[0]
algorithm := parts[1]
if algorithm != "SHA-512" {
continue
}
hex := parts[2]
unix, err := strconv.ParseInt(parts[3], 10, 0)
h, err := ParseHost(text)
if err != nil {
continue
}
expires := time.Unix(unix, 0)
k.KnownHosts[hostname] = Fingerprint{
Algorithm: algorithm,
Hex: hex,
Expires: expires,
}
k.hosts[h.Hostname] = h
}
return scanner.Err()
}
// TOFU implements basic trust on first use.
//
// If the host is not on file, it is added to the list.
// If the host on file is expired, a new entry is added to the list.
// If the fingerprint does not match the one on file, an error is returned.
func (k *KnownHosts) TOFU(hostname string, cert *x509.Certificate) error {
host := NewHost(hostname, cert.Raw, cert.NotAfter)
knownHost, ok := k.Lookup(hostname)
if !ok || time.Now().After(knownHost.Expires) {
k.Add(host)
return nil
}
// Check fingerprint
if !bytes.Equal(knownHost.Fingerprint, host.Fingerprint) {
return fmt.Errorf("fingerprint for %q does not match", hostname)
}
return nil
}
// HostWriter writes host entries to an io.WriteCloser.
//
// HostWriter is safe for concurrent use by multiple goroutines.
type HostWriter struct {
bw *bufio.Writer
cl io.Closer
mu sync.Mutex
}
// NewHostWriter returns a new host writer that writes to
// the provided io.WriteCloser.
func NewHostWriter(w io.WriteCloser) *HostWriter {
return &HostWriter{
bw: bufio.NewWriter(w),
cl: w,
}
}
// Fingerprint represents a fingerprint using a certain algorithm.
type Fingerprint struct {
Algorithm string // fingerprint algorithm e.g. SHA-512
Hex string // fingerprint in hexadecimal, with ':' between each octet
Expires time.Time // unix time of the fingerprint expiration date
// OpenHostsFile returns a new host writer that appends to the file at the given path.
// The file is created if it does not exist.
func OpenHostsFile(path string) (*HostWriter, error) {
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return nil, err
}
return NewHostWriter(f), nil
}
// NewFingerprint returns the SHA-512 fingerprint of the provided raw data.
func NewFingerprint(raw []byte, expires time.Time) Fingerprint {
sum512 := sha512.Sum512(raw)
// WriteHost writes the host to the underlying io.Writer.
func (h *HostWriter) WriteHost(host Host) error {
h.mu.Lock()
defer h.mu.Unlock()
h.bw.WriteString(host.String())
h.bw.WriteByte('\n')
if err := h.bw.Flush(); err != nil {
return fmt.Errorf("failed to write host: %w", err)
}
return nil
}
// Close closes the underlying io.Closer.
func (h *HostWriter) Close() error {
h.mu.Lock()
defer h.mu.Unlock()
return h.cl.Close()
}
// PersistentHosts represents a persistent set of known hosts.
type PersistentHosts struct {
hosts *KnownHosts
writer *HostWriter
}
// NewPersistentHosts returns a new persistent set of known hosts.
func NewPersistentHosts(hosts *KnownHosts, writer *HostWriter) *PersistentHosts {
return &PersistentHosts{
hosts,
writer,
}
}
// LoadPersistentHosts loads persistent hosts from the file at the given path.
func LoadPersistentHosts(path string) (*PersistentHosts, error) {
hosts := &KnownHosts{}
if err := hosts.Load(path); err != nil {
return nil, err
}
writer, err := OpenHostsFile(path)
if err != nil {
return nil, err
}
return &PersistentHosts{
hosts,
writer,
}, nil
}
// Add adds a host to the list of known hosts.
// It returns an error if the host could not be persisted.
func (p *PersistentHosts) Add(h Host) error {
err := p.writer.WriteHost(h)
if err != nil {
return fmt.Errorf("failed to persist host: %w", err)
}
p.hosts.Add(h)
return nil
}
// Lookup returns the known host entry corresponding to the given hostname.
func (p *PersistentHosts) Lookup(hostname string) (Host, bool) {
return p.hosts.Lookup(hostname)
}
// Entries returns the known host entries sorted by hostname.
func (p *PersistentHosts) Entries() []Host {
return p.hosts.Entries()
}
// TOFU implements trust on first use with a persistent set of known hosts.
//
// If the host is not on file, it is added to the list.
// If the host on file is expired, a new entry is added to the list.
// If the fingerprint does not match the one on file, an error is returned.
func (p *PersistentHosts) TOFU(hostname string, cert *x509.Certificate) error {
host := NewHost(hostname, cert.Raw, cert.NotAfter)
knownHost, ok := p.Lookup(hostname)
if !ok || time.Now().After(knownHost.Expires) {
return p.Add(host)
}
// Check fingerprint
if !bytes.Equal(knownHost.Fingerprint, host.Fingerprint) {
return fmt.Errorf("fingerprint for %q does not match", hostname)
}
return nil
}
// Close closes the underlying HostWriter.
func (p *PersistentHosts) Close() error {
return p.writer.Close()
}
// Host represents a host entry with a fingerprint using a certain algorithm.
type Host struct {
Hostname string // hostname
Algorithm string // fingerprint algorithm e.g. SHA-512
Fingerprint Fingerprint // fingerprint
Expires time.Time // unix time of the fingerprint expiration date
}
// NewHost returns a new host with a SHA-512 fingerprint of
// the provided raw data.
func NewHost(hostname string, raw []byte, expires time.Time) Host {
sum := sha512.Sum512(raw)
return Host{
Hostname: hostname,
Algorithm: "SHA-512",
Fingerprint: sum[:],
Expires: expires,
}
}
// ParseHost parses a host from the provided text.
func ParseHost(text []byte) (Host, error) {
var h Host
err := h.UnmarshalText(text)
return h, err
}
// String returns a string representation of the host.
func (h Host) String() string {
var b strings.Builder
for i, f := range sum512 {
if i > 0 {
b.WriteByte(':')
}
fmt.Fprintf(&b, "%02X", f)
}
return Fingerprint{
Algorithm: "SHA-512",
Hex: b.String(),
Expires: expires,
}
b.WriteString(h.Hostname)
b.WriteByte(' ')
b.WriteString(h.Algorithm)
b.WriteByte(' ')
b.WriteString(h.Fingerprint.String())
b.WriteByte(' ')
b.WriteString(strconv.FormatInt(h.Expires.Unix(), 10))
return b.String()
}
// UnmarshalText unmarshals the host from the provided text.
func (h *Host) UnmarshalText(text []byte) error {
const format = "hostname algorithm hex-fingerprint expiry-unix-ts"
parts := bytes.Split(text, []byte(" "))
if len(parts) != 4 {
return fmt.Errorf("expected the format %q", format)
}
if len(parts[0]) == 0 {
return errors.New("empty hostname")
}
h.Hostname = string(parts[0])
algorithm := string(parts[1])
if algorithm != "SHA-512" {
return fmt.Errorf("unsupported algorithm %q", algorithm)
}
h.Algorithm = algorithm
fingerprint := make([]byte, 0, sha512.Size)
scanner := bufio.NewScanner(bytes.NewReader(parts[2]))
scanner.Split(scanFingerprint)
for scanner.Scan() {
b, err := strconv.ParseUint(scanner.Text(), 16, 8)
if err != nil {
return fmt.Errorf("failed to parse fingerprint hash: %w", err)
}
fingerprint = append(fingerprint, byte(b))
}
if len(fingerprint) != sha512.Size {
return fmt.Errorf("invalid fingerprint size %d, expected %d",
len(fingerprint), sha512.Size)
}
h.Fingerprint = fingerprint
unix, err := strconv.ParseInt(string(parts[3]), 10, 0)
if err != nil {
return fmt.Errorf("invalid unix timestamp: %w", err)
}
h.Expires = time.Unix(unix, 0)
return nil
}
func scanFingerprint(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, ':'); i >= 0 {
// We have a full newline-terminated line.
return i + 1, data[0:i], nil
}
// If we're at EOF, we have a final, non-terminated hex byte
if atEOF {
return len(data), data, nil
}
// Request more data.
return 0, nil, nil
}
// Fingerprint represents a fingerprint.
type Fingerprint []byte
// String returns a string representation of the fingerprint.
func (f Fingerprint) String() string {
var sb strings.Builder
for i, b := range f {
if i > 0 {
sb.WriteByte(':')
}
fmt.Fprintf(&sb, "%02X", b)
}
return sb.String()
}