go-gemini/client.go

185 lines
4.5 KiB
Go
Raw Normal View History

2020-10-24 13:15:32 -06:00
package gemini
2020-09-21 20:09:50 -06:00
import (
2020-09-23 22:30:21 -06:00
"bufio"
2020-12-17 15:16:55 -07:00
"context"
2020-09-21 20:09:50 -06:00
"crypto/tls"
2020-09-25 17:53:50 -06:00
"crypto/x509"
2020-12-19 11:43:47 -07:00
"errors"
"fmt"
2020-10-27 17:21:33 -06:00
"net"
2020-10-31 18:55:56 -06:00
"time"
2020-09-21 20:09:50 -06:00
)
2021-02-14 13:50:41 -07:00
// A Client is a Gemini client. Its zero value is a usable client.
2020-09-25 21:06:54 -06:00
type Client struct {
2020-12-17 17:50:26 -07:00
// 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.
2021-01-14 20:34:12 -07:00
//
2021-01-25 10:02:09 -07:00
// See the tofu submodule for an implementation of trust on first use.
2020-12-17 17:50:26 -07:00
TrustCertificate func(hostname string, cert *x509.Certificate) error
2020-10-31 18:55:56 -06:00
2020-12-17 17:50:26 -07: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
2021-02-14 13:50:41 -07:00
// Get or Do return and will interrupt reading of the Response.Body.
2020-10-31 21:05:31 -06:00
//
2020-12-17 17:50:26 -07:00
// A Timeout of zero means no timeout.
Timeout time.Duration
2020-09-25 17:53:50 -06:00
}
2021-02-14 15:11:05 -07:00
// 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.
2020-10-27 17:21:33 -06:00
func (c *Client) Get(url string) (*Response, error) {
req, err := NewRequest(url)
if err != nil {
return nil, err
}
return c.Do(req)
}
2021-02-14 15:11:05 -07:00
// 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.
2020-10-27 17:21:33 -06:00
func (c *Client) Do(req *Request) (*Response, error) {
2021-02-09 14:55:14 -07:00
// Punycode request URL
if punycode, err := punycodeHost(req.URL.Host); err != nil {
return nil, err
} else {
// Make a copy of the request
_req := *req
req = &_req
_url := *req.URL
req.URL = &_url
// Set the host
req.URL.Host = punycode
}
// Extract hostname and punycode it
hostname, port, err := net.SplitHostPort(req.Host)
if err != nil {
return nil, err
}
punycode, err := punycodeHostname(hostname)
if err != nil {
return nil, err
2020-11-27 15:45:15 -07:00
}
2020-09-25 17:53:50 -06:00
// Connect to the host
config := &tls.Config{
InsecureSkipVerify: true,
2020-09-25 22:31:16 -06:00
MinVersion: tls.VersionTLS12,
2020-10-28 11:40:25 -06:00
GetClientCertificate: func(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) {
2020-12-17 14:46:16 -07:00
if req.Certificate != nil {
return req.Certificate, nil
}
return &tls.Certificate{}, nil
2020-09-26 13:14:34 -06:00
},
VerifyConnection: func(cs tls.ConnectionState) error {
return c.verifyConnection(hostname, punycode, cs)
2020-09-25 17:53:50 -06:00
},
ServerName: punycode,
2020-09-25 17:53:50 -06:00
}
2020-12-17 15:16:55 -07:00
ctx := req.Context
if ctx == nil {
ctx = context.Background()
}
start := time.Now()
dialer := net.Dialer{
Timeout: c.Timeout,
}
address := net.JoinHostPort(punycode, port)
netConn, err := dialer.DialContext(ctx, "tcp", address)
2020-09-25 17:53:50 -06:00
if err != nil {
return nil, err
}
2020-11-25 22:42:25 -07:00
conn := tls.Client(netConn, config)
2020-10-31 18:55:56 -06:00
// Set connection deadline
2020-12-17 22:35:08 -07:00
if c.Timeout != 0 {
err := conn.SetDeadline(start.Add(c.Timeout))
if err != nil {
2021-01-25 10:02:09 -07:00
return nil, fmt.Errorf("failed to set connection deadline: %w", err)
}
2020-10-31 18:55:56 -06:00
}
2020-09-25 17:53:50 -06:00
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) {
2020-09-25 17:53:50 -06:00
// Write the request
w := bufio.NewWriter(conn)
err := req.Write(w)
if err != nil {
2021-01-25 10:02:09 -07:00
return nil, fmt.Errorf("failed to write request: %w", err)
}
2020-09-25 17:53:50 -06:00
if err := w.Flush(); err != nil {
return nil, err
}
// Read the response
resp, err := ReadResponse(conn)
if err != nil {
2020-09-25 17:53:50 -06:00
return nil, err
}
2020-09-25 17:53:50 -06:00
return resp, nil
2020-09-23 22:30:21 -06:00
}
2020-10-27 17:21:33 -06:00
func (c *Client) verifyConnection(hostname, punycode string, cs tls.ConnectionState) error {
2020-10-28 11:40:25 -06:00
cert := cs.PeerCertificates[0]
// Try punycode and then hostname
if err := verifyHostname(cert, punycode); err != nil {
if err := verifyHostname(cert, hostname); err != nil {
return err
}
2020-10-28 11:40:25 -06:00
}
2020-12-19 11:43:47 -07:00
// Check expiration date
if !time.Now().Before(cert.NotAfter) {
return errors.New("gemini: certificate expired")
}
2020-11-05 13:27:12 -07:00
2020-12-17 17:50:26 -07:00
// See if the client trusts the certificate
if c.TrustCertificate != nil {
return c.TrustCertificate(hostname, cert)
2020-10-27 17:21:33 -06:00
}
2020-12-17 17:50:26 -07:00
return nil
2020-10-27 17:21:33 -06:00
}