1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
|
package gemini
import (
"bufio"
"crypto/tls"
"crypto/x509"
"net"
"net/url"
)
// Client represents a Gemini client.
type Client struct {
// KnownHosts is a list of known hosts that the client trusts.
KnownHosts KnownHosts
// CertificateStore maps hostnames to certificates.
// It is used to determine which certificate to use when the server requests
// a certificate.
CertificateStore ClientCertificateStore
// CheckRedirect, if not nil, will be called to determine whether
// to follow a redirect.
// If CheckRedirect is nil, a default policy of no more than 5 consecutive
// redirects will be enforced.
CheckRedirect func(req *Request, via []*Request) error
// GetCertificate, if not nil, will be called when a server requests a certificate.
// The returned certificate will be used when sending the request again.
// If the certificate is nil, the request will not be sent again and
// the response will be returned.
GetCertificate func(req *Request, store *ClientCertificateStore) *tls.Certificate
// TrustCertificate, if not nil, will be called to determine whether the
// client should trust the given certificate.
// If error is not nil, the connection will be aborted.
TrustCertificate func(hostname string, cert *x509.Certificate, knownHosts *KnownHosts) error
}
// 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) {
return c.do(req, nil)
}
func (c *Client) do(req *Request, via []*Request) (*Response, error) {
// Connect to the host
config := &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
GetClientCertificate: func(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
// Request certificates take precedence over client certificates
if req.Certificate != nil {
return req.Certificate, nil
}
// If we have already stored the certificate, return it
if cert, err := c.CertificateStore.Lookup(hostname(req.Host), req.URL.Path); err == nil {
return cert, nil
}
return &tls.Certificate{}, nil
},
VerifyConnection: func(cs tls.ConnectionState) error {
cert := cs.PeerCertificates[0]
// Verify the hostname
if err := verifyHostname(cert, hostname(req.Host)); err != nil {
return err
}
// Check that the client trusts the certificate
if c.TrustCertificate == nil {
if err := c.KnownHosts.Lookup(hostname(req.Host), cert); err != nil {
return err
}
} else if err := c.TrustCertificate(hostname(req.Host), cert, &c.KnownHosts); err != nil {
return err
}
return nil
},
}
conn, err := tls.Dial("tcp", req.Host, config)
if err != nil {
return nil, err
}
// Write the request
w := bufio.NewWriter(conn)
req.write(w)
if err := w.Flush(); err != nil {
return nil, err
}
// Read the response
resp := &Response{}
if err := resp.read(conn); err != nil {
return nil, err
}
// Store connection information
resp.TLS = conn.ConnectionState()
// Resend the request with a certificate if the server responded
// with CertificateRequired
if resp.Status == StatusCertificateRequired {
// Check to see if a certificate was already provided to prevent an infinite loop
if req.Certificate != nil {
return resp, nil
}
if c.GetCertificate != nil {
if cert := c.GetCertificate(req, &c.CertificateStore); cert != nil {
req.Certificate = cert
return c.Do(req)
}
}
} else if resp.Status.Class() == StatusClassRedirect {
if via == nil {
via = []*Request{}
}
via = append(via, req)
target, err := url.Parse(resp.Meta)
if err != nil {
return resp, err
}
target = req.URL.ResolveReference(target)
redirect, err := NewRequestFromURL(target)
if err != nil {
return resp, err
}
if c.CheckRedirect != nil {
if err := c.CheckRedirect(redirect, via); err != nil {
return resp, err
}
} else if len(via) > 5 {
// Default policy of no more than 5 redirects
return resp, ErrTooManyRedirects
}
return c.do(redirect, via)
}
return resp, nil
}
// hostname returns the host without the port.
func hostname(host string) string {
hostname, _, err := net.SplitHostPort(host)
if err != nil {
return host
}
return hostname
}
|