Add (*Client).Get function
This commit is contained in:
parent
12a9deb1a6
commit
239ec885f7
25
client.go
25
client.go
@ -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
24
doc.go
@ -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
|
||||||
|
@ -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
119
gemini.go
@ -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)
|
|
||||||
}
|
|
||||||
|
25
request.go
25
request.go
@ -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
78
status.go
Normal 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
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user