1
0
Fork 0
mirror of https://git.sr.ht/~adnano/go-gemini synced 2024-05-13 19:46:07 +02:00

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"
"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
}

24
doc.go
View File

@ -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

View File

@ -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)

119
gemini.go
View File

@ -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)
}

View File

@ -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
}

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
)