go-gemini/client.go

278 lines
6.1 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-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"
"io"
2020-10-27 17:21:33 -06:00
"net"
"net/url"
2020-10-31 18:55:56 -06:00
"time"
2021-02-21 07:43:23 -07:00
"unicode/utf8"
"golang.org/x/net/idna"
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
// 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 17:53:50 -06:00
}
2021-02-14 15:11:05 -07:00
// Get sends a Gemini request for the given URL.
2021-02-22 19:35:02 -07:00
// If the provided context is canceled or times out, the request
// is aborted and the context's error is returned.
2021-02-14 15:11:05 -07:00
//
// 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(ctx context.Context, url string) (*Response, error) {
2020-10-27 17:21:33 -06:00
req, err := NewRequest(url)
if err != nil {
return nil, err
}
return c.Do(ctx, req)
2020-10-27 17:21:33 -06:00
}
2021-02-22 19:35:02 -07:00
// Do sends a Gemini request and returns a Gemini response.
// If the provided context is canceled or times out, the request
// is aborted and the context's error is returned.
2021-02-14 15:11:05 -07:00
//
// 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(ctx context.Context, req *Request) (*Response, error) {
if ctx == nil {
panic("nil context")
2021-02-14 17:02:34 -07:00
}
// Punycode request URL host
host, port := splitHostPort(req.URL.Host)
punycode, err := punycodeHostname(host)
2021-02-14 17:02:34 -07:00
if err != nil {
2021-02-09 14:55:14 -07:00
return nil, err
2021-02-14 17:02:34 -07:00
}
if host != punycode {
host = punycode
2021-02-14 17:02:34 -07:00
// Copy the URL and update the host
u := new(url.URL)
*u = *req.URL
u.Host = net.JoinHostPort(host, port)
2021-02-09 14:55:14 -07:00
// Use the new URL in the request so that the server gets
// the punycoded hostname
req = &Request{
URL: u,
}
2021-02-09 14:55:14 -07:00
}
2021-02-14 17:02:34 -07:00
// Use request host if provided
if req.Host != "" {
host, port = splitHostPort(req.Host)
host, err = punycodeHostname(host)
2021-02-14 17:02:34 -07:00
if err != nil {
return nil, err
}
2020-11-27 15:45:15 -07:00
}
addr := net.JoinHostPort(host, port)
2020-09-25 17:53:50 -06:00
// Connect to the host
conn, err := c.dialContext(ctx, "tcp", addr)
if err != nil {
return nil, err
}
// Setup TLS
conn = tls.Client(conn, &tls.Config{
2020-09-25 17:53:50 -06:00
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(cs, host)
2020-09-25 17:53:50 -06:00
},
ServerName: host,
})
2020-09-25 17:53:50 -06:00
2021-02-20 22:20:42 -07:00
type result struct {
resp *Response
err error
}
res := make(chan result, 1)
go func() {
2021-02-23 14:01:29 -07:00
resp, err := c.do(ctx, conn, req)
2021-02-20 22:20:42 -07:00
res <- result{resp, err}
}()
select {
case <-ctx.Done():
conn.Close()
return nil, ctx.Err()
case r := <-res:
return r.resp, r.err
}
}
2021-02-23 14:01:29 -07:00
func (c *Client) do(ctx context.Context, conn net.Conn, req *Request) (*Response, error) {
ctx, cancel := context.WithCancel(ctx)
done := ctx.Done()
w := &contextWriter{
ctx: ctx,
done: done,
cancel: cancel,
wc: conn,
}
rc := &contextReader{
ctx: ctx,
done: done,
cancel: cancel,
rc: conn,
}
2020-09-25 17:53:50 -06:00
// Write the request
if err := req.Write(w); err != nil {
2021-02-20 22:20:42 -07:00
return nil, err
}
2020-09-25 17:53:50 -06:00
// Read the response
resp, err := ReadResponse(rc)
if err != nil {
2021-02-20 22:20:42 -07:00
return nil, err
}
2021-02-23 14:36:17 -07:00
resp.conn = conn
2021-02-20 22:20:42 -07:00
return 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{}).DialContext(ctx, network, addr)
2020-09-23 22:30:21 -06:00
}
2020-10-27 17:21:33 -06:00
func (c *Client) verifyConnection(cs tls.ConnectionState, hostname string) error {
2020-10-28 11:40:25 -06:00
cert := cs.PeerCertificates[0]
// Verify hostname
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) {
2021-02-21 07:27:12 -07:00
return ErrCertificateExpired
2020-12-19 11:43:47 -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
}
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
}
2021-02-21 07:43:23 -07:00
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)
}
type contextReader struct {
ctx context.Context
done <-chan struct{}
cancel func()
rc io.ReadCloser
}
func (r *contextReader) Read(p []byte) (int, error) {
select {
case <-r.done:
r.rc.Close()
return 0, r.ctx.Err()
default:
}
n, err := r.rc.Read(p)
if err != nil {
r.cancel()
}
return n, err
}
func (r *contextReader) Close() error {
r.cancel()
return r.rc.Close()
}
type contextWriter struct {
ctx context.Context
done <-chan struct{}
cancel func()
wc io.WriteCloser
}
func (w *contextWriter) Write(b []byte) (int, error) {
select {
case <-w.done:
w.wc.Close()
return 0, w.ctx.Err()
default:
}
n, err := w.wc.Write(b)
if err != nil {
w.cancel()
}
return n, err
}
func (w *contextWriter) Close() error {
w.cancel()
return w.wc.Close()
}