mirror of
https://git.sr.ht/~adnano/go-gemini
synced 2025-05-10 23:45:24 +02:00
Error handling is currently missing is a couple of places. Most of them are i/o related. This change adds checks, an therefore sometimes also has to change function signatures by adding an error return value. In the case of the response writer the status and meta handling is changed and this also breaks the API. In some places where we don't have any reasonable I've added assignment to a blank identifier to make it clear that we're ignoring an error. text: read the Err() that can be set by the scanner. client: check if conn.SetDeadline() returns an error. client: check if req.Write() returns an error. fs: panic if mime type registration fails. server: stop performing i/o in Header/Status functions By deferring the actual header write to the first Write() or Flush() call we don't have to do any error handling in Header() or Status(). As Server.respond() now defers a ResponseWriter.Flush() instead of directly flushing the underlying bufio.Writer this has the added benefit of ensuring that we always write a header to the client, even if the responder is a complete NOOP. tofu: return an error if we fail to write to the known hosts writer.
132 lines
3.1 KiB
Go
132 lines
3.1 KiB
Go
package gemini
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Client is a Gemini 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.
|
|
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.
|
|
//
|
|
// A Timeout of zero means no timeout.
|
|
Timeout time.Duration
|
|
}
|
|
|
|
// Get performs a Gemini request for the given URL.
|
|
func (c *Client) Get(url string) (*Response, error) {
|
|
req, err := NewRequest(url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c.Do(req)
|
|
}
|
|
|
|
// Do performs a Gemini request and returns a Gemini response.
|
|
func (c *Client) Do(req *Request) (*Response, error) {
|
|
// Extract hostname
|
|
colonPos := strings.LastIndex(req.Host, ":")
|
|
if colonPos == -1 {
|
|
colonPos = len(req.Host)
|
|
}
|
|
hostname := req.Host[:colonPos]
|
|
|
|
// Connect to the host
|
|
config := &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
MinVersion: tls.VersionTLS12,
|
|
GetClientCertificate: func(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
|
if req.Certificate != nil {
|
|
return req.Certificate, nil
|
|
}
|
|
return &tls.Certificate{}, nil
|
|
},
|
|
VerifyConnection: func(cs tls.ConnectionState) error {
|
|
return c.verifyConnection(req, cs)
|
|
},
|
|
ServerName: hostname,
|
|
}
|
|
// Set connection context
|
|
ctx := req.Context
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
netConn, err := (&net.Dialer{}).DialContext(ctx, "tcp", req.Host)
|
|
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))
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"failed to set connection deadline: %w", err)
|
|
}
|
|
}
|
|
|
|
// Write the request
|
|
w := bufio.NewWriter(conn)
|
|
|
|
err = req.Write(w)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"failed to write request data: %w", err)
|
|
}
|
|
|
|
if err := w.Flush(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Read the response
|
|
resp, err := ReadResponse(conn)
|
|
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
|
|
}
|
|
cert := cs.PeerCertificates[0]
|
|
if err := verifyHostname(cert, hostname); err != nil {
|
|
return err
|
|
}
|
|
// Check expiration date
|
|
if !time.Now().Before(cert.NotAfter) {
|
|
return errors.New("gemini: certificate expired")
|
|
}
|
|
|
|
// See if the client trusts the certificate
|
|
if c.TrustCertificate != nil {
|
|
return c.TrustCertificate(hostname, cert)
|
|
}
|
|
return nil
|
|
}
|