2020-10-24 19:15:32 +00:00
|
|
|
package gemini
|
2020-09-22 02:09:50 +00:00
|
|
|
|
|
|
|
import (
|
2020-12-17 22:16:55 +00:00
|
|
|
"context"
|
2020-09-22 02:09:50 +00:00
|
|
|
"crypto/tls"
|
2020-09-25 23:53:50 +00:00
|
|
|
"crypto/x509"
|
2020-12-19 18:43:47 +00:00
|
|
|
"errors"
|
2020-10-27 23:21:33 +00:00
|
|
|
"net"
|
2021-02-20 18:37:08 +00:00
|
|
|
"net/url"
|
2020-11-01 00:55:56 +00:00
|
|
|
"time"
|
2020-09-22 02:09:50 +00:00
|
|
|
)
|
|
|
|
|
2021-02-14 20:50:41 +00:00
|
|
|
// A Client is a Gemini client. Its zero value is a usable client.
|
2020-09-26 03:06:54 +00:00
|
|
|
type Client struct {
|
2020-12-18 00:50:26 +00: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-15 03:34:12 +00:00
|
|
|
//
|
2021-01-25 17:02:09 +00:00
|
|
|
// See the tofu submodule for an implementation of trust on first use.
|
2020-12-18 00:50:26 +00:00
|
|
|
TrustCertificate func(hostname string, cert *x509.Certificate) error
|
2020-11-01 00:55:56 +00:00
|
|
|
|
2020-12-18 00:50:26 +00: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 20:50:41 +00:00
|
|
|
// Get or Do return and will interrupt reading of the Response.Body.
|
2020-11-01 03:05:31 +00:00
|
|
|
//
|
2020-12-18 00:50:26 +00:00
|
|
|
// A Timeout of zero means no timeout.
|
|
|
|
Timeout time.Duration
|
2021-02-20 18:37:08 +00:00
|
|
|
|
|
|
|
// DialContext specifies the dial function for creating TCP connections.
|
|
|
|
// If DialContext is nil, the client dials using package net.
|
|
|
|
DialContext func(ctx context.Context, network, addr string) (net.Conn, error)
|
2020-09-25 23:53:50 +00:00
|
|
|
}
|
|
|
|
|
2021-02-14 22:11:05 +00: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.
|
2021-02-20 18:37:08 +00:00
|
|
|
func (c *Client) Get(ctx context.Context, url string) (*Response, error) {
|
2020-10-27 23:21:33 +00:00
|
|
|
req, err := NewRequest(url)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-02-20 18:37:08 +00:00
|
|
|
return c.Do(ctx, req)
|
2020-10-27 23:21:33 +00:00
|
|
|
}
|
|
|
|
|
2021-02-14 22:11:05 +00: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.
|
2021-02-20 18:37:08 +00:00
|
|
|
func (c *Client) Do(ctx context.Context, req *Request) (*Response, error) {
|
|
|
|
if ctx == nil {
|
|
|
|
panic("nil context")
|
2021-02-15 00:02:34 +00:00
|
|
|
}
|
2021-02-20 18:37:08 +00:00
|
|
|
|
|
|
|
// Punycode request URL host
|
|
|
|
host, port := splitHostPort(req.URL.Host)
|
|
|
|
punycode, err := punycodeHostname(host)
|
2021-02-15 00:02:34 +00:00
|
|
|
if err != nil {
|
2021-02-09 21:55:14 +00:00
|
|
|
return nil, err
|
2021-02-15 00:02:34 +00:00
|
|
|
}
|
2021-02-20 18:37:08 +00:00
|
|
|
if host != punycode {
|
|
|
|
host = punycode
|
2021-02-15 00:02:34 +00:00
|
|
|
|
2021-02-09 21:55:14 +00:00
|
|
|
// Make a copy of the request
|
2021-02-20 18:37:08 +00:00
|
|
|
r2 := new(Request)
|
|
|
|
*r2 = *req
|
|
|
|
r2.URL = new(url.URL)
|
|
|
|
*r2.URL = *req.URL
|
|
|
|
req = r2
|
2021-02-09 21:55:14 +00:00
|
|
|
|
|
|
|
// Set the host
|
2021-02-20 18:37:08 +00:00
|
|
|
req.URL.Host = net.JoinHostPort(host, port)
|
2021-02-09 21:55:14 +00:00
|
|
|
}
|
|
|
|
|
2021-02-15 00:02:34 +00:00
|
|
|
// Use request host if provided
|
|
|
|
if req.Host != "" {
|
2021-02-20 18:37:08 +00:00
|
|
|
host, port = splitHostPort(req.Host)
|
|
|
|
host, err = punycodeHostname(host)
|
2021-02-15 00:02:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-11-27 22:45:15 +00:00
|
|
|
}
|
|
|
|
|
2021-02-20 18:37:08 +00:00
|
|
|
addr := net.JoinHostPort(host, port)
|
|
|
|
|
2020-09-25 23:53:50 +00:00
|
|
|
// Connect to the host
|
2021-02-20 18:37:08 +00:00
|
|
|
start := time.Now()
|
|
|
|
conn, err := c.dialContext(ctx, "tcp", addr)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set the connection deadline
|
|
|
|
if c.Timeout != 0 {
|
|
|
|
conn.SetDeadline(start.Add(c.Timeout))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Setup TLS
|
|
|
|
conn = tls.Client(conn, &tls.Config{
|
2020-09-25 23:53:50 +00:00
|
|
|
InsecureSkipVerify: true,
|
2020-09-26 04:31:16 +00:00
|
|
|
MinVersion: tls.VersionTLS12,
|
2020-10-28 17:40:25 +00:00
|
|
|
GetClientCertificate: func(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
2020-12-17 21:46:16 +00:00
|
|
|
if req.Certificate != nil {
|
|
|
|
return req.Certificate, nil
|
|
|
|
}
|
|
|
|
return &tls.Certificate{}, nil
|
2020-09-26 19:14:34 +00:00
|
|
|
},
|
2020-10-13 20:44:46 +00:00
|
|
|
VerifyConnection: func(cs tls.ConnectionState) error {
|
2021-02-20 18:37:08 +00:00
|
|
|
return c.verifyConnection(cs, host)
|
2020-09-25 23:53:50 +00:00
|
|
|
},
|
2021-02-20 18:37:08 +00:00
|
|
|
ServerName: host,
|
|
|
|
})
|
2020-09-25 23:53:50 +00:00
|
|
|
|
2021-02-20 18:37:08 +00:00
|
|
|
res := make(chan result, 1)
|
|
|
|
go func() {
|
|
|
|
res <- c.do(conn, req)
|
|
|
|
}()
|
2021-01-13 21:30:08 +00:00
|
|
|
|
2021-02-20 18:37:08 +00:00
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
conn.Close()
|
|
|
|
return nil, ctx.Err()
|
|
|
|
case r := <-res:
|
|
|
|
return r.resp, r.err
|
2021-01-13 21:30:08 +00:00
|
|
|
}
|
2021-02-20 18:37:08 +00:00
|
|
|
}
|
2021-01-13 21:30:08 +00:00
|
|
|
|
2021-02-20 18:37:08 +00:00
|
|
|
type result struct {
|
|
|
|
resp *Response
|
|
|
|
err error
|
2021-01-13 21:30:08 +00:00
|
|
|
}
|
|
|
|
|
2021-02-20 18:37:08 +00:00
|
|
|
func (c *Client) do(conn net.Conn, req *Request) result {
|
2020-09-25 23:53:50 +00:00
|
|
|
// Write the request
|
2021-02-20 18:37:08 +00:00
|
|
|
if err := req.Write(conn); err != nil {
|
|
|
|
return result{nil, err}
|
2021-01-07 22:08:50 +00:00
|
|
|
}
|
|
|
|
|
2020-09-25 23:53:50 +00:00
|
|
|
// Read the response
|
2020-12-18 06:41:14 +00:00
|
|
|
resp, err := ReadResponse(conn)
|
|
|
|
if err != nil {
|
2021-02-20 18:37:08 +00:00
|
|
|
return result{nil, err}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Store TLS connection state
|
|
|
|
if tlsConn, ok := conn.(*tls.Conn); ok {
|
|
|
|
state := tlsConn.ConnectionState()
|
|
|
|
resp.TLS = &state
|
2020-09-25 23:53:50 +00:00
|
|
|
}
|
2020-09-28 03:58:45 +00:00
|
|
|
|
2021-02-20 18:37:08 +00:00
|
|
|
return result{resp, nil}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) dialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
|
|
if c.DialContext != nil {
|
|
|
|
return c.DialContext(ctx, network, addr)
|
|
|
|
}
|
|
|
|
return (&net.Dialer{
|
|
|
|
Timeout: c.Timeout,
|
|
|
|
}).DialContext(ctx, network, addr)
|
2020-09-24 04:30:21 +00:00
|
|
|
}
|
2020-10-27 23:21:33 +00:00
|
|
|
|
2021-02-20 18:37:08 +00:00
|
|
|
func (c *Client) verifyConnection(cs tls.ConnectionState, hostname string) error {
|
2020-10-28 17:40:25 +00:00
|
|
|
cert := cs.PeerCertificates[0]
|
2021-02-20 18:37:08 +00:00
|
|
|
// Verify hostname
|
|
|
|
if err := verifyHostname(cert, hostname); err != nil {
|
2021-02-16 16:27:51 +00:00
|
|
|
return err
|
2020-10-28 17:40:25 +00:00
|
|
|
}
|
2020-12-19 18:43:47 +00:00
|
|
|
// Check expiration date
|
|
|
|
if !time.Now().Before(cert.NotAfter) {
|
|
|
|
return errors.New("gemini: certificate expired")
|
|
|
|
}
|
2020-12-18 00:50:26 +00:00
|
|
|
// See if the client trusts the certificate
|
|
|
|
if c.TrustCertificate != nil {
|
|
|
|
return c.TrustCertificate(hostname, cert)
|
2020-10-27 23:21:33 +00:00
|
|
|
}
|
2020-12-18 00:50:26 +00:00
|
|
|
return nil
|
2020-10-27 23:21:33 +00:00
|
|
|
}
|
2021-02-20 18:37:08 +00:00
|
|
|
|
|
|
|
func splitHostPort(hostport string) (host, port string) {
|
|
|
|
var err error
|
|
|
|
host, port, err = net.SplitHostPort(hostport)
|
|
|
|
if err != nil {
|
|
|
|
// Likely no port
|
|
|
|
host = hostport
|
|
|
|
port = "1965"
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|