aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdnan Maolood <[email protected]>2020-12-17 19:50:26 -0500
committerAdnan Maolood <[email protected]>2020-12-17 19:50:26 -0500
commit28c5c857dc32bf16b5345ae6ed158e79b26a83cf (patch)
treee06bc884709be7a5666ffa7b2b2225db36d91ad0
parentAllow Request.Context to be nil (diff)
downloadgo-gemini-28c5c857dc32bf16b5345ae6ed158e79b26a83cf.tar.xz
go-gemini-28c5c857dc32bf16b5345ae6ed158e79b26a83cf.zip
Decouple Client from KnownHostsFile
-rw-r--r--client.go132
-rw-r--r--examples/client.go55
-rw-r--r--tofu.go9
3 files changed, 81 insertions, 115 deletions
diff --git a/client.go b/client.go
index 6821ded..8e9e808 100644
--- a/client.go
+++ b/client.go
@@ -5,7 +5,6 @@ import (
"context"
"crypto/tls"
"crypto/x509"
- "errors"
"net"
"net/url"
"strings"
@@ -13,25 +12,19 @@ import (
)
// Client is a Gemini client.
-//
-// Clients are safe for concurrent use by multiple goroutines.
type Client struct {
- // KnownHosts is a list of known hosts.
- KnownHosts KnownHostsFile
-
- // 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
+ // 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.
+ TrustCertificate func(hostname string, cert *x509.Certificate) error
- // InsecureSkipTrust specifies whether the client should trust
- // any certificate it receives without checking KnownHosts
- // or calling TrustCertificate.
- // Use with caution.
- InsecureSkipTrust bool
+ // GetCertificate is called to retrieve a certificate upon
+ // the request of a server.
+ // If GetCertificate is nil or the returned error is not nil,
+ // the request will not be sent again and the response will be returned.
+ GetCertificate func(scope, path string) (tls.Certificate, error)
// GetInput is called to retrieve input when the server requests it.
// If GetInput is nil or returns false, no input will be sent and
@@ -42,25 +35,16 @@ type Client struct {
// If CheckRedirect is nil, redirects will not be followed.
CheckRedirect func(req *Request, via []*Request) error
- // GetCertificate is called to retrieve a certificate upon
- // the request of a server.
- // If GetCertificate is nil or the returned error is not nil,
- // the request will not be sent again and the response will be returned.
- GetCertificate func(scope, path string) (tls.Certificate, error)
-
- // TrustCertificate is called to determine whether the client
- // should trust a certificate it has not seen before.
- // If TrustCertificate is nil, the certificate will not be trusted
- // and the connection will be aborted.
+ // 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.
//
- // 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
+ // A Timeout of zero means no timeout.
+ Timeout time.Duration
}
-// Get performs a Gemini request for the given url.
+// Get performs a Gemini request for the given URL.
func (c *Client) Get(url string) (*Response, error) {
req, err := NewRequest(url)
if err != nil {
@@ -130,34 +114,39 @@ func (c *Client) do(req *Request, via []*Request) (*Response, error) {
switch {
case resp.Status == StatusCertificateRequired:
+ if c.GetCertificate == nil {
+ break
+ }
+
// Check to see if a certificate was already provided to prevent an infinite loop
if req.Certificate != nil {
- return resp, nil
+ break
}
-
hostname, path := req.URL.Hostname(), strings.TrimSuffix(req.URL.Path, "/")
- if c.GetCertificate != nil {
- cert, err := c.GetCertificate(hostname, path)
- if err != nil {
- return resp, err
- }
- req.Certificate = &cert
- return c.do(req, via)
+ cert, err := c.GetCertificate(hostname, path)
+ if err != nil {
+ return resp, err
}
- return resp, nil
+ req.Certificate = &cert
+ return c.do(req, via)
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 = QueryEscape(input)
- return c.do(req, via)
- }
+ if c.GetInput == nil {
+ break
+ }
+
+ input, ok := c.GetInput(resp.Meta, resp.Status == StatusSensitiveInput)
+ if ok {
+ req.URL.ForceQuery = true
+ req.URL.RawQuery = QueryEscape(input)
+ return c.do(req, via)
}
- return resp, nil
case resp.Status.Class() == StatusClassRedirect:
+ if c.CheckRedirect == nil {
+ break
+ }
+
if via == nil {
via = []*Request{}
}
@@ -171,12 +160,10 @@ func (c *Client) do(req *Request, via []*Request) (*Response, error) {
redirect := NewRequestFromURL(target)
redirect.Context = req.Context
- if c.CheckRedirect != nil {
- if err := c.CheckRedirect(redirect, via); err != nil {
- return resp, err
- }
- return c.do(redirect, via)
+ if err := c.CheckRedirect(redirect, via); err != nil {
+ return resp, err
}
+ return c.do(redirect, via)
}
return resp, nil
@@ -194,33 +181,10 @@ func (c *Client) verifyConnection(req *Request, cs tls.ConnectionState) error {
if err := verifyHostname(cert, hostname); err != nil {
return err
}
- if c.InsecureSkipTrust {
- return nil
- }
-
- // Check the known hosts
- knownHost, ok := c.KnownHosts.Lookup(hostname)
- if !ok || !time.Now().Before(knownHost.Expires) {
- // See if the client trusts the certificate
- if c.TrustCertificate != nil {
- switch c.TrustCertificate(hostname, cert) {
- case TrustOnce:
- fingerprint := NewFingerprint(cert.Raw, cert.NotAfter)
- c.KnownHosts.Add(hostname, fingerprint)
- return nil
- case TrustAlways:
- fingerprint := NewFingerprint(cert.Raw, cert.NotAfter)
- c.KnownHosts.Add(hostname, fingerprint)
- c.KnownHosts.Write(hostname, fingerprint)
- return nil
- }
- }
- return errors.New("gemini: certificate not trusted")
- }
- fingerprint := NewFingerprint(cert.Raw, cert.NotAfter)
- if knownHost.Hex == fingerprint.Hex {
- return nil
+ // See if the client trusts the certificate
+ if c.TrustCertificate != nil {
+ return c.TrustCertificate(hostname, cert)
}
- return errors.New("gemini: fingerprint does not match")
+ return nil
}
diff --git a/examples/client.go b/examples/client.go
index 818f29c..1c98bf5 100644
--- a/examples/client.go
+++ b/examples/client.go
@@ -5,6 +5,7 @@ package main
import (
"bufio"
"crypto/x509"
+ "errors"
"fmt"
"io/ioutil"
"log"
@@ -25,43 +26,52 @@ Otherwise, this should be safe to trust.
[t]rust always; trust [o]nce; [a]bort
=> `
-var (
- scanner = bufio.NewScanner(os.Stdin)
- client = &gemini.Client{}
-)
+func main() {
+ if len(os.Args) < 2 {
+ fmt.Printf("usage: %s <url> [host]", os.Args[0])
+ os.Exit(1)
+ }
+
+ // Load known hosts file
+ var knownHosts gemini.KnownHostsFile
+ if err := knownHosts.Load(filepath.Join(xdg.DataHome(), "gemini", "known_hosts")); err != nil {
+ log.Println(err)
+ }
+
+ scanner := bufio.NewScanner(os.Stdin)
+
+ var client gemini.Client
+ client.TrustCertificate = func(hostname string, cert *x509.Certificate) error {
+ knownHost, ok := knownHosts.Lookup(hostname)
+ if ok && time.Now().Before(knownHost.Expires) {
+ // Certificate is in known hosts file and is not expired
+ return nil
+ }
-func init() {
- client.Timeout = 30 * time.Second
- client.KnownHosts.Load(filepath.Join(xdg.DataHome(), "gemini", "known_hosts"))
- client.TrustCertificate = func(hostname string, cert *x509.Certificate) gemini.Trust {
fingerprint := gemini.NewFingerprint(cert.Raw, cert.NotAfter)
fmt.Printf(trustPrompt, hostname, fingerprint.Hex)
scanner.Scan()
switch scanner.Text() {
case "t":
- return gemini.TrustAlways
+ knownHosts.Add(hostname, fingerprint)
+ knownHosts.Write(hostname, fingerprint)
+ return nil
case "o":
- return gemini.TrustOnce
+ knownHosts.Add(hostname, fingerprint)
+ return nil
default:
- return gemini.TrustNone
+ return errors.New("certificate not trusted")
}
}
client.GetInput = func(prompt string, sensitive bool) (string, bool) {
- fmt.Printf("%s: ", prompt)
+ fmt.Printf("%s ", prompt)
scanner.Scan()
return scanner.Text(), true
}
-}
-
-func main() {
- if len(os.Args) < 2 {
- fmt.Printf("usage: %s gemini://... [host]", os.Args[0])
- os.Exit(1)
- }
+ // Do the request
url := os.Args[1]
req, err := gemini.NewRequest(url)
-
if err != nil {
fmt.Println(err)
os.Exit(1)
@@ -69,13 +79,13 @@ func main() {
if len(os.Args) == 3 {
req.Host = os.Args[2]
}
-
resp, err := client.Do(req)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
+ // Handle response
if resp.Status.Class() == gemini.StatusClassSuccess {
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
@@ -84,6 +94,7 @@ func main() {
}
fmt.Print(string(body))
} else {
- fmt.Printf("request failed: %d %s: %s", resp.Status, resp.Status.Message(), resp.Meta)
+ fmt.Printf("%d %s: %s\n", resp.Status, resp.Status.Message(), resp.Meta)
+ os.Exit(1)
}
}
diff --git a/tofu.go b/tofu.go
index 68f2a96..9e93ac2 100644
--- a/tofu.go
+++ b/tofu.go
@@ -12,15 +12,6 @@ import (
"time"
)
-// Trust represents the trustworthiness of a certificate.
-type Trust int
-
-const (
- TrustNone Trust = iota // The certificate is not trusted.
- TrustOnce // The certificate is trusted once.
- TrustAlways // The certificate is trusted always.
-)
-
// KnownHosts maps hosts to fingerprints.
type KnownHosts map[string]Fingerprint