Add (*Client).Get function

This commit is contained in:
Adnan Maolood 2020-10-27 19:21:33 -04:00
parent 12a9deb1a6
commit 239ec885f7
6 changed files with 137 additions and 149 deletions

View File

@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"net"
) )
// Client represents a Gemini client. // Client represents a Gemini client.
@ -28,8 +29,17 @@ type Client struct {
TrustCertificate func(hostname string, cert *x509.Certificate, knownHosts *KnownHosts) error TrustCertificate func(hostname string, cert *x509.Certificate, knownHosts *KnownHosts) error
} }
// Send sends a Gemini request and returns a Gemini response. // Get performs a Gemini request for the given url.
func (c *Client) Send(req *Request) (*Response, error) { 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) {
// Connect to the host // Connect to the host
config := &tls.Config{ config := &tls.Config{
InsecureSkipVerify: true, InsecureSkipVerify: true,
@ -92,9 +102,18 @@ func (c *Client) Send(req *Request) (*Response, error) {
if c.GetCertificate != nil { if c.GetCertificate != nil {
if cert := c.GetCertificate(hostname(req.Host), &c.CertificateStore); cert != nil { if cert := c.GetCertificate(hostname(req.Host), &c.CertificateStore); cert != nil {
req.Certificate = cert req.Certificate = cert
return c.Send(req) return c.Do(req)
} }
} }
} }
return resp, nil 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
}

24
doc.go
View File

@ -1,26 +1,34 @@
/* /*
Package gemini implements the Gemini protocol. Package gemini implements the Gemini protocol.
Send makes a Gemini request with the default client: Get makes a Gemini request:
req := gemini.NewRequest("gemini://example.com") resp, err := gemini.Get("gemini://example.com")
resp, err := gemini.Send(req)
if err != nil { if err != nil {
// handle error // handle error
} }
// ... // ...
For control over client behavior, create a custom Client: The client must close the response body when finished with it:
resp, err := gemini.Get("gemini://example.com")
if err != nil {
// handle error
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
// ...
For control over client behavior, create a Client:
var client gemini.Client var client gemini.Client
resp, err := client.Send(req) resp, err := client.Get("gemini://example.com")
if err != nil { if err != nil {
// handle error // handle error
} }
// ... // ...
The default client loads known hosts from "$XDG_DATA_HOME/gemini/known_hosts". Clients can load their own list of known hosts:
Custom clients can load their own list of known hosts:
err := client.KnownHosts.Load("path/to/my/known_hosts") err := client.KnownHosts.Load("path/to/my/known_hosts")
if err != nil { if err != nil {
@ -33,7 +41,7 @@ Clients can control when to trust certificates with TrustCertificate:
return knownHosts.Lookup(hostname, cert) return knownHosts.Lookup(hostname, cert)
} }
If a server responds with StatusCertificateRequired, the default client will generate a certificate and resend the request with it. Custom clients can do so in GetCertificate: Clients can control what to do when a server requests a certificate:
client.GetCertificate = func(hostname string, store *gemini.CertificateStore) *tls.Certificate { client.GetCertificate = func(hostname string, store *gemini.CertificateStore) *tls.Certificate {
// If the certificate is in the store, return it // If the certificate is in the store, return it

View File

@ -68,7 +68,7 @@ func init() {
// sendRequest sends a request to the given URL. // sendRequest sends a request to the given URL.
func sendRequest(req *gmi.Request) error { func sendRequest(req *gmi.Request) error {
resp, err := client.Send(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return err return err
} }
@ -149,19 +149,8 @@ func main() {
os.Exit(1) os.Exit(1)
} }
var host string
if len(os.Args) >= 3 {
host = os.Args[2]
}
url := os.Args[1] url := os.Args[1]
var req *gmi.Request req, err := gmi.NewRequest(url)
var err error
if host != "" {
req, err = gmi.NewRequestTo(url, host)
} else {
req, err = gmi.NewRequest(url)
}
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)

119
gemini.go
View File

@ -8,82 +8,7 @@ import (
"time" "time"
) )
// Status codes. var crlf = []byte("\r\n")
type Status int
const (
StatusInput Status = 10
StatusSensitiveInput Status = 11
StatusSuccess Status = 20
StatusRedirect Status = 30
StatusRedirectPermanent Status = 31
StatusTemporaryFailure Status = 40
StatusServerUnavailable Status = 41
StatusCGIError Status = 42
StatusProxyError Status = 43
StatusSlowDown Status = 44
StatusPermanentFailure Status = 50
StatusNotFound Status = 51
StatusGone Status = 52
StatusProxyRequestRefused Status = 53
StatusBadRequest Status = 59
StatusCertificateRequired Status = 60
StatusCertificateNotAuthorized Status = 61
StatusCertificateNotValid Status = 62
)
// Class returns the status class for this status code.
func (s Status) Class() StatusClass {
return StatusClass(s / 10)
}
// StatusMessage returns the status message corresponding to the provided
// status code.
// StatusMessage returns an empty string for input, successs, and redirect
// status codes.
func (s Status) Message() string {
switch s {
case StatusTemporaryFailure:
return "TemporaryFailure"
case StatusServerUnavailable:
return "Server unavailable"
case StatusCGIError:
return "CGI error"
case StatusProxyError:
return "Proxy error"
case StatusSlowDown:
return "Slow down"
case StatusPermanentFailure:
return "PermanentFailure"
case StatusNotFound:
return "Not found"
case StatusGone:
return "Gone"
case StatusProxyRequestRefused:
return "Proxy request refused"
case StatusBadRequest:
return "Bad request"
case StatusCertificateRequired:
return "Certificate required"
case StatusCertificateNotAuthorized:
return "Certificate not authorized"
case StatusCertificateNotValid:
return "Certificate not valid"
}
return ""
}
// Status code categories.
type StatusClass int
const (
StatusClassInput StatusClass = 1
StatusClassSuccess StatusClass = 2
StatusClassRedirect StatusClass = 3
StatusClassTemporaryFailure StatusClass = 4
StatusClassPermanentFailure StatusClass = 5
StatusClassCertificateRequired StatusClass = 6
)
// Errors. // Errors.
var ( var (
@ -96,48 +21,42 @@ var (
ErrBodyNotAllowed = errors.New("gemini: response status code does not allow for body") ErrBodyNotAllowed = errors.New("gemini: response status code does not allow for body")
) )
// DefaultClient is the default client. It is used by Send. // DefaultClient is the default client. It is used by Get and Do.
// //
// On the first request, DefaultClient will load the default list of known hosts. // On the first request, DefaultClient loads the default list of known hosts.
var DefaultClient Client var DefaultClient Client
var ( // Get performs a Gemini request for the given url.
crlf = []byte("\r\n") //
) // Get is a wrapper around DefaultClient.Get.
func Get(url string) (*Response, error) {
return DefaultClient.Get(url)
}
// Do performs a Gemini request and returns a Gemini response.
//
// Do is a wrapper around DefaultClient.Do.
func Do(req *Request) (*Response, error) {
return DefaultClient.Do(req)
}
var defaultClientOnce sync.Once
func init() { func init() {
DefaultClient.TrustCertificate = func(hostname string, cert *x509.Certificate, knownHosts *KnownHosts) error { DefaultClient.TrustCertificate = func(hostname string, cert *x509.Certificate, knownHosts *KnownHosts) error {
// Load the hosts only once. This is so that the hosts don't have to be loaded defaultClientOnce.Do(func() { knownHosts.LoadDefault() })
// for those using their own clients.
setupDefaultClientOnce.Do(setupDefaultClient)
return knownHosts.Lookup(hostname, cert) return knownHosts.Lookup(hostname, cert)
} }
DefaultClient.GetCertificate = func(hostname string, store *CertificateStore) *tls.Certificate { DefaultClient.GetCertificate = func(hostname string, store *CertificateStore) *tls.Certificate {
// If the certificate is in the store, return it
if cert, err := store.Lookup(hostname); err == nil { if cert, err := store.Lookup(hostname); err == nil {
return cert return cert
} }
// Otherwise, generate a certificate
duration := time.Hour duration := time.Hour
cert, err := NewCertificate(hostname, duration) cert, err := NewCertificate(hostname, duration)
if err != nil { if err != nil {
return nil return nil
} }
// Store and return the certificate
store.Add(hostname, cert) store.Add(hostname, cert)
return &cert return &cert
} }
} }
var setupDefaultClientOnce sync.Once
func setupDefaultClient() {
DefaultClient.KnownHosts.LoadDefault()
}
// Send sends a Gemini request and returns a Gemini response.
//
// Send is a wrapper around DefaultClient.Send.
func Send(req *Request) (*Response, error) {
return DefaultClient.Send(req)
}

View File

@ -33,15 +33,6 @@ type Request struct {
TLS tls.ConnectionState TLS tls.ConnectionState
} }
// hostname returns the host without the port.
func hostname(host string) string {
hostname, _, err := net.SplitHostPort(host)
if err != nil {
return host
}
return hostname
}
// NewRequest returns a new request. The host is inferred from the URL. // NewRequest returns a new request. The host is inferred from the URL.
func NewRequest(rawurl string) (*Request, error) { func NewRequest(rawurl string) (*Request, error) {
u, err := url.Parse(rawurl) u, err := url.Parse(rawurl)
@ -54,29 +45,13 @@ func NewRequest(rawurl string) (*Request, error) {
// NewRequestFromURL returns a new request for the given URL. // NewRequestFromURL returns a new request for the given URL.
// The host is inferred from the URL. // The host is inferred from the URL.
func NewRequestFromURL(url *url.URL) (*Request, error) { func NewRequestFromURL(url *url.URL) (*Request, error) {
// If there is no port, use the default port of 1965
host := url.Host host := url.Host
if url.Port() == "" { if url.Port() == "" {
host += ":1965" host += ":1965"
} }
return &Request{ return &Request{
Host: host,
URL: url, URL: url,
}, nil
}
// NewRequestTo returns a new request for the provided URL to the provided host.
// The host must contain a port.
func NewRequestTo(rawurl, host string) (*Request, error) {
u, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
return &Request{
Host: host, Host: host,
URL: u,
}, nil }, nil
} }

78
status.go Normal file
View File

@ -0,0 +1,78 @@
package gemini
// Status codes.
type Status int
const (
StatusInput Status = 10
StatusSensitiveInput Status = 11
StatusSuccess Status = 20
StatusRedirect Status = 30
StatusRedirectPermanent Status = 31
StatusTemporaryFailure Status = 40
StatusServerUnavailable Status = 41
StatusCGIError Status = 42
StatusProxyError Status = 43
StatusSlowDown Status = 44
StatusPermanentFailure Status = 50
StatusNotFound Status = 51
StatusGone Status = 52
StatusProxyRequestRefused Status = 53
StatusBadRequest Status = 59
StatusCertificateRequired Status = 60
StatusCertificateNotAuthorized Status = 61
StatusCertificateNotValid Status = 62
)
// Class returns the status class for this status code.
func (s Status) Class() StatusClass {
return StatusClass(s / 10)
}
// StatusMessage returns the status message corresponding to the provided
// status code.
// StatusMessage returns an empty string for input, successs, and redirect
// status codes.
func (s Status) Message() string {
switch s {
case StatusTemporaryFailure:
return "TemporaryFailure"
case StatusServerUnavailable:
return "Server unavailable"
case StatusCGIError:
return "CGI error"
case StatusProxyError:
return "Proxy error"
case StatusSlowDown:
return "Slow down"
case StatusPermanentFailure:
return "PermanentFailure"
case StatusNotFound:
return "Not found"
case StatusGone:
return "Gone"
case StatusProxyRequestRefused:
return "Proxy request refused"
case StatusBadRequest:
return "Bad request"
case StatusCertificateRequired:
return "Certificate required"
case StatusCertificateNotAuthorized:
return "Certificate not authorized"
case StatusCertificateNotValid:
return "Certificate not valid"
}
return ""
}
// Status code categories.
type StatusClass int
const (
StatusClassInput StatusClass = 1
StatusClassSuccess StatusClass = 2
StatusClassRedirect StatusClass = 3
StatusClassTemporaryFailure StatusClass = 4
StatusClassPermanentFailure StatusClass = 5
StatusClassCertificateRequired StatusClass = 6
)