1
0
Fork 0
mirror of https://git.sr.ht/~adnano/go-gemini synced 2024-06-02 14:46:05 +02:00
go-gemini/client.go

249 lines
6.5 KiB
Go
Raw Normal View History

2020-10-24 21:15:32 +02:00
package gemini
2020-09-22 04:09:50 +02:00
import (
2020-09-24 06:30:21 +02:00
"bufio"
2020-11-25 20:16:51 +01:00
"bytes"
2020-09-22 04:09:50 +02:00
"crypto/tls"
2020-09-26 01:53:50 +02:00
"crypto/x509"
2020-11-05 21:27:12 +01:00
"errors"
2020-10-28 00:21:33 +01:00
"net"
2020-10-28 03:12:10 +01:00
"net/url"
2020-10-28 21:02:04 +01:00
"path"
2020-10-28 18:40:25 +01:00
"strings"
2020-11-24 22:28:58 +01:00
"sync"
2020-11-01 01:55:56 +01:00
"time"
2020-09-22 04:09:50 +02:00
)
2020-10-28 18:40:25 +01:00
// Client is a Gemini client.
2020-11-24 22:28:58 +01:00
//
// Clients are safe for concurrent use by multiple goroutines.
2020-09-26 05:06:54 +02:00
type Client struct {
2020-10-28 18:40:25 +01:00
// KnownHosts is a list of known hosts.
KnownHosts KnownHostsFile
2020-09-26 05:06:54 +02:00
2020-10-28 18:40:25 +01:00
// Certificates stores client-side certificates.
Certificates CertificateDir
2020-09-26 21:14:34 +02:00
2020-11-01 01:55:56 +01:00
// 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
// InsecureSkipTrust specifies whether the client should trust
2020-11-01 03:50:42 +01:00
// any certificate it receives without checking KnownHosts
2020-11-01 03:45:21 +01:00
// or calling TrustCertificate.
// Use with caution.
InsecureSkipTrust bool
2020-11-01 03:45:21 +01:00
2020-10-28 18:40:25 +01:00
// GetInput is called to retrieve input when the server requests it.
// If GetInput is nil or returns false, no input will be sent and
// the response will be returned.
GetInput func(prompt string, sensitive bool) (input string, ok bool)
// CheckRedirect determines whether to follow a redirect.
2020-11-05 21:44:01 +01:00
// If CheckRedirect is nil, redirects will not be followed.
2020-10-28 03:12:10 +01:00
CheckRedirect func(req *Request, via []*Request) error
2020-10-28 18:40:25 +01:00
// CreateCertificate is called to generate a certificate upon
// the request of a server.
// If CreateCertificate is nil or the returned error is not nil,
// the request will not be sent again and the response will be returned.
CreateCertificate func(scope, path string) (tls.Certificate, error)
2020-10-28 18:40:25 +01:00
// TrustCertificate is called to determine whether the client
// should trust a certificate it has not seen before.
2020-11-01 04:05:31 +01:00
// If TrustCertificate is nil, the certificate will not be trusted
// and the connection will be aborted.
//
// If TrustCertificate returns TrustOnce, the certificate will be added
// to the client's list of known hosts.
// If TrustCertificate returns TrustAlways, the certificate will also be
// written to the known hosts file.
TrustCertificate func(hostname string, cert *x509.Certificate) Trust
2020-11-24 22:28:58 +01:00
mu sync.Mutex
2020-09-26 01:53:50 +02:00
}
2020-10-28 00:21:33 +01:00
// 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) {
2020-11-25 03:49:24 +01:00
c.mu.Lock()
defer c.mu.Unlock()
2020-10-28 03:12:10 +01:00
return c.do(req, nil)
}
func (c *Client) do(req *Request, via []*Request) (*Response, error) {
2020-09-26 01:53:50 +02:00
// Connect to the host
config := &tls.Config{
InsecureSkipVerify: true,
2020-09-26 06:31:16 +02:00
MinVersion: tls.VersionTLS12,
2020-10-28 18:40:25 +01:00
GetClientCertificate: func(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) {
return c.getClientCertificate(req)
2020-09-26 21:14:34 +02:00
},
VerifyConnection: func(cs tls.ConnectionState) error {
2020-10-28 18:40:25 +01:00
return c.verifyConnection(req, cs)
2020-09-26 01:53:50 +02:00
},
}
conn, err := tls.Dial("tcp", req.Host, config)
if err != nil {
return nil, err
}
2020-11-01 01:55:56 +01:00
// Set connection deadline
if d := c.Timeout; d != 0 {
conn.SetDeadline(time.Now().Add(d))
2020-11-01 01:55:56 +01:00
}
2020-09-26 01:53:50 +02:00
// Write the request
w := bufio.NewWriter(conn)
req.write(w)
if err := w.Flush(); err != nil {
return nil, err
}
// Read the response
resp := &Response{}
2020-10-28 00:16:55 +01:00
if err := resp.read(conn); err != nil {
2020-09-26 01:53:50 +02:00
return nil, err
}
2020-11-06 17:18:58 +01:00
resp.Request = req
2020-10-28 18:40:25 +01:00
// Store connection state
2020-09-28 01:56:33 +02:00
resp.TLS = conn.ConnectionState()
2020-10-28 18:40:25 +01:00
switch {
case resp.Status == StatusCertificateRequired:
// Check to see if a certificate was already provided to prevent an infinite loop
if req.Certificate != nil {
return resp, nil
}
2020-10-28 18:40:25 +01:00
hostname, path := req.URL.Hostname(), strings.TrimSuffix(req.URL.Path, "/")
if c.CreateCertificate != nil {
cert, err := c.CreateCertificate(hostname, path)
if err != nil {
return resp, err
}
2020-10-28 18:40:25 +01:00
c.Certificates.Add(hostname+path, cert)
c.Certificates.Write(hostname+path, cert)
req.Certificate = &cert
2020-10-28 18:40:25 +01:00
return c.do(req, via)
}
2020-11-05 05:46:05 +01:00
return resp, nil
2020-10-28 18:40:25 +01:00
case resp.Status.Class() == StatusClassInput:
if c.GetInput != nil {
input, ok := c.GetInput(resp.Meta, resp.Status == StatusSensitiveInput)
if ok {
req.URL.ForceQuery = true
req.URL.RawQuery = url.QueryEscape(input)
return c.do(req, via)
}
}
2020-11-05 05:46:05 +01:00
return resp, nil
2020-10-28 18:40:25 +01:00
case resp.Status.Class() == StatusClassRedirect:
2020-10-28 03:12:10 +01:00
if via == nil {
via = []*Request{}
}
via = append(via, req)
target, err := url.Parse(resp.Meta)
if err != nil {
return resp, err
}
2020-11-08 05:43:07 +01:00
target = req.URL.ResolveReference(target)
2020-11-05 05:46:05 +01:00
redirect := NewRequestFromURL(target)
2020-10-28 03:12:10 +01:00
if c.CheckRedirect != nil {
if err := c.CheckRedirect(redirect, via); err != nil {
return resp, err
}
2020-11-05 21:44:01 +01:00
return c.do(redirect, via)
2020-10-28 03:12:10 +01:00
}
}
2020-10-28 04:35:22 +01:00
2020-09-26 01:53:50 +02:00
return resp, nil
2020-09-24 06:30:21 +02:00
}
2020-10-28 00:21:33 +01:00
2020-10-28 18:40:25 +01:00
func (c *Client) getClientCertificate(req *Request) (*tls.Certificate, error) {
// Request certificates have the highest precedence
if req.Certificate != nil {
return req.Certificate, nil
}
2020-10-28 21:02:04 +01:00
// Search recursively for the certificate
scope := req.URL.Hostname() + strings.TrimSuffix(req.URL.Path, "/")
for {
2020-11-05 21:27:12 +01:00
cert, ok := c.Certificates.Lookup(scope)
if ok {
// Ensure that the certificate is not expired
if cert.Leaf != nil && !time.Now().After(cert.Leaf.NotAfter) {
// Store the certificate
req.Certificate = &cert
return &cert, nil
}
2020-10-28 21:02:04 +01:00
break
}
scope = path.Dir(scope)
if scope == "." {
break
}
2020-10-28 18:40:25 +01:00
}
2020-10-28 21:02:04 +01:00
2020-10-28 18:40:25 +01:00
return &tls.Certificate{}, 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
}
if c.InsecureSkipTrust {
2020-11-01 03:45:21 +01:00
return nil
}
2020-11-06 04:30:13 +01:00
// Check the known hosts
2020-11-05 21:27:12 +01:00
knownHost, ok := c.KnownHosts.Lookup(hostname)
2020-11-25 20:16:51 +01:00
if !ok || !time.Now().Before(knownHost.Expires) {
2020-11-06 04:30:13 +01:00
// See if the client trusts the certificate
if c.TrustCertificate != nil {
switch c.TrustCertificate(hostname, cert) {
case TrustOnce:
2020-11-09 18:04:53 +01:00
fingerprint := NewFingerprint(cert.Raw, cert.NotAfter)
c.KnownHosts.Add(hostname, fingerprint)
2020-11-06 04:30:13 +01:00
return nil
case TrustAlways:
2020-11-09 18:04:53 +01:00
fingerprint := NewFingerprint(cert.Raw, cert.NotAfter)
c.KnownHosts.Add(hostname, fingerprint)
c.KnownHosts.Write(hostname, fingerprint)
2020-11-06 04:30:13 +01:00
return nil
}
2020-11-05 21:27:12 +01:00
}
2020-11-06 04:30:13 +01:00
return errors.New("gemini: certificate not trusted")
2020-11-05 21:27:12 +01:00
}
2020-11-09 18:04:53 +01:00
fingerprint := NewFingerprint(cert.Raw, cert.NotAfter)
2020-11-25 20:20:31 +01:00
if bytes.Equal(knownHost.Raw, fingerprint.Raw) {
2020-11-06 04:30:13 +01:00
return nil
2020-10-28 00:21:33 +01:00
}
2020-11-06 04:30:13 +01:00
return errors.New("gemini: fingerprint does not match")
2020-10-28 00:21:33 +01:00
}