From 239ec885f7ff4c82195527543fe9fa79186ee405 Mon Sep 17 00:00:00 2001 From: Adnan Maolood Date: Tue, 27 Oct 2020 19:21:33 -0400 Subject: [PATCH] Add (*Client).Get function --- client.go | 25 ++++++++-- doc.go | 24 ++++++--- examples/client.go | 15 +----- gemini.go | 119 ++++++++------------------------------------- request.go | 25 ---------- status.go | 78 +++++++++++++++++++++++++++++ 6 files changed, 137 insertions(+), 149 deletions(-) create mode 100644 status.go diff --git a/client.go b/client.go index 03242e5..e48fe22 100644 --- a/client.go +++ b/client.go @@ -4,6 +4,7 @@ import ( "bufio" "crypto/tls" "crypto/x509" + "net" ) // Client represents a Gemini client. @@ -28,8 +29,17 @@ type Client struct { TrustCertificate func(hostname string, cert *x509.Certificate, knownHosts *KnownHosts) error } -// Send sends a Gemini request and returns a Gemini response. -func (c *Client) Send(req *Request) (*Response, 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) { // Connect to the host config := &tls.Config{ InsecureSkipVerify: true, @@ -92,9 +102,18 @@ func (c *Client) Send(req *Request) (*Response, error) { if c.GetCertificate != nil { if cert := c.GetCertificate(hostname(req.Host), &c.CertificateStore); cert != nil { req.Certificate = cert - return c.Send(req) + return c.Do(req) } } } 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 +} diff --git a/doc.go b/doc.go index 9d519a0..3c97595 100644 --- a/doc.go +++ b/doc.go @@ -1,26 +1,34 @@ /* 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.Send(req) + resp, err := gemini.Get("gemini://example.com") if err != nil { // 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 - resp, err := client.Send(req) + resp, err := client.Get("gemini://example.com") if err != nil { // handle error } // ... -The default client loads known hosts from "$XDG_DATA_HOME/gemini/known_hosts". -Custom clients can load their own list of known hosts: +Clients can load their own list of known hosts: err := client.KnownHosts.Load("path/to/my/known_hosts") if err != nil { @@ -33,7 +41,7 @@ Clients can control when to trust certificates with TrustCertificate: 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 { // If the certificate is in the store, return it diff --git a/examples/client.go b/examples/client.go index fa88dcf..16608ca 100644 --- a/examples/client.go +++ b/examples/client.go @@ -68,7 +68,7 @@ func init() { // sendRequest sends a request to the given URL. func sendRequest(req *gmi.Request) error { - resp, err := client.Send(req) + resp, err := client.Do(req) if err != nil { return err } @@ -149,19 +149,8 @@ func main() { os.Exit(1) } - var host string - if len(os.Args) >= 3 { - host = os.Args[2] - } - url := os.Args[1] - var req *gmi.Request - var err error - if host != "" { - req, err = gmi.NewRequestTo(url, host) - } else { - req, err = gmi.NewRequest(url) - } + req, err := gmi.NewRequest(url) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/gemini.go b/gemini.go index 3faa420..045a05d 100644 --- a/gemini.go +++ b/gemini.go @@ -8,82 +8,7 @@ import ( "time" ) -// 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 -) +var crlf = []byte("\r\n") // Errors. var ( @@ -96,48 +21,42 @@ var ( 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 ( - crlf = []byte("\r\n") -) +// Get performs a Gemini request for the given url. +// +// 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() { 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 - // for those using their own clients. - setupDefaultClientOnce.Do(setupDefaultClient) + defaultClientOnce.Do(func() { knownHosts.LoadDefault() }) return knownHosts.Lookup(hostname, cert) } 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 { return cert } - // Otherwise, generate a certificate duration := time.Hour cert, err := NewCertificate(hostname, duration) if err != nil { return nil } - // Store and return the certificate store.Add(hostname, 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) -} diff --git a/request.go b/request.go index 80b20be..1b43aa7 100644 --- a/request.go +++ b/request.go @@ -33,15 +33,6 @@ type Request struct { 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. func NewRequest(rawurl string) (*Request, error) { u, err := url.Parse(rawurl) @@ -54,29 +45,13 @@ func NewRequest(rawurl string) (*Request, error) { // NewRequestFromURL returns a new request for the given URL. // The host is inferred from the URL. func NewRequestFromURL(url *url.URL) (*Request, error) { - // If there is no port, use the default port of 1965 host := url.Host if url.Port() == "" { host += ":1965" } - return &Request{ - Host: host, 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, - URL: u, }, nil } diff --git a/status.go b/status.go new file mode 100644 index 0000000..43fe609 --- /dev/null +++ b/status.go @@ -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 +)