aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoradnano <[email protected]>2020-09-25 19:53:50 -0400
committeradnano <[email protected]>2020-09-25 19:53:50 -0400
commit53d84882ea0c33a12e1e009d58003ecde3a5aa98 (patch)
tree563f2729b2626ba72d552f38608946f919e8e21a
parentReorganize (diff)
downloadgo-gemini-53d84882ea0c33a12e1e009d58003ecde3a5aa98.tar.xz
go-gemini-53d84882ea0c33a12e1e009d58003ecde3a5aa98.zip
Implement configurable Client
-rw-r--r--README.md7
-rw-r--r--client.go127
-rw-r--r--examples/client/client.go19
3 files changed, 88 insertions, 65 deletions
diff --git a/README.md b/README.md
index cd49501..56712e9 100644
--- a/README.md
+++ b/README.md
@@ -33,11 +33,10 @@ A quick overview of the Gemini protocol:
The way this is implemented in this package is like so:
1. Client makes a request with `NewRequest`. The client then sends the request
- with `Do(*Request) (*Response, error)`.
+ with `Send(*Request) (*Response, error)`.
2. Server recieves the request and constructs a response.
The server calls the `Serve(*ResponseWriter, *Request)` method on the
`Handler` field. The handler writes the response. The server then closes
the connection.
-5. Client recieves the response as a `*Response`. The client then handles the
- response. The client can now verify the certificate of the server using a
- Trust-On-First-Use method.
+3. Client recieves the response as a `*Response`. The client then handles the
+ response.
diff --git a/client.go b/client.go
index d12f51c..4a4f32c 100644
--- a/client.go
+++ b/client.go
@@ -4,6 +4,7 @@ package gemini
import (
"bufio"
"crypto/tls"
+ "crypto/x509"
"errors"
"io/ioutil"
"net"
@@ -17,17 +18,7 @@ var (
ErrInvalidURL = errors.New("gemini: requested URL is invalid")
)
-// Request is a Gemini request.
-//
-// A Request can optionally be configured with a client certificate. Example:
-//
-// req := NewRequest(url)
-// cert, err := tls.LoadX509KeyPair("client.crt", "client.key")
-// if err != nil {
-// panic(err)
-// }
-// req.TLSConfig.Certificates = append(req.TLSConfig.Certificates, cert)
-//
+// Request represents a Gemini request.
type Request struct {
// URL specifies the URL being requested.
URL *url.URL
@@ -37,7 +28,7 @@ type Request struct {
// This field is ignored by the server.
Host string
- // The certificate to use for the request.
+ // Certificate specifies the TLS certificate to use for the request.
Certificate tls.Certificate
// RemoteAddr allows servers and other software to record the network
@@ -120,80 +111,98 @@ type Response struct {
TLS tls.ConnectionState
}
-// Do sends a Gemini request and returns a Gemini response.
-func Do(req *Request) (*Response, error) {
- // Connect to the host
- config := &tls.Config{
- InsecureSkipVerify: true,
- Certificates: []tls.Certificate{req.Certificate},
- }
- conn, err := tls.Dial("tcp", req.Host, config)
- if err != nil {
- return nil, err
- }
- defer conn.Close()
-
- // Write the request
- // TODO: Is buffered I/O necessary here?
- w := bufio.NewWriter(conn)
- req.write(w)
- if err := w.Flush(); err != nil {
- return nil, err
- }
-
- // Read the response status
- r := bufio.NewReader(conn)
+// read reads a Gemini response from the provided buffered reader.
+func (resp *Response) read(r *bufio.Reader) error {
+ // Read the status
statusB := make([]byte, 2)
if _, err := r.Read(statusB); err != nil {
- return nil, err
+ return err
}
status, err := strconv.Atoi(string(statusB))
if err != nil {
- return nil, err
+ return err
}
+ resp.Status = status
// Read one space
if b, err := r.ReadByte(); err != nil {
- return nil, err
+ return err
} else if b != ' ' {
- return nil, ErrProtocol
+ return ErrProtocol
}
// Read the meta
meta, err := r.ReadString('\r')
if err != nil {
- return nil, err
- }
-
- // Read terminating newline
- if b, err := r.ReadByte(); err != nil {
- return nil, err
- } else if b != '\n' {
- return nil, ErrProtocol
+ return err
}
-
// Trim carriage return
meta = meta[:len(meta)-1]
-
// Ensure meta is less than or equal to 1024 bytes
if len(meta) > 1024 {
- return nil, ErrProtocol
+ return ErrProtocol
+ }
+ resp.Meta = meta
+
+ // Read terminating newline
+ if b, err := r.ReadByte(); err != nil {
+ return err
+ } else if b != '\n' {
+ return ErrProtocol
}
// Read response body
- var body []byte
if status/10 == StatusClassSuccess {
var err error
- body, err = ioutil.ReadAll(r)
+ resp.Body, err = ioutil.ReadAll(r)
if err != nil {
- return nil, err
+ return err
}
}
+ return nil
+}
- return &Response{
- Status: status,
- Meta: meta,
- Body: body,
- TLS: conn.ConnectionState(),
- }, nil
+// Client represents a Gemini client.
+type Client struct {
+ // VerifyCertificate, if not nil, will be called to verify the server certificate.
+ // If error is not nil, the connection will be aborted.
+ VerifyCertificate func(cert *x509.Certificate) error
+}
+
+// Send sends a Gemini request and returns a Gemini response.
+func (c *Client) Send(req *Request) (*Response, error) {
+ // Connect to the host
+ config := &tls.Config{
+ InsecureSkipVerify: true,
+ Certificates: []tls.Certificate{req.Certificate},
+ VerifyPeerCertificate: func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
+ cert, err := x509.ParseCertificate(rawCerts[0])
+ if err != nil {
+ return err
+ }
+ return c.VerifyCertificate(cert)
+ },
+ }
+ conn, err := tls.Dial("tcp", req.Host, config)
+ if err != nil {
+ return nil, err
+ }
+ defer conn.Close()
+
+ // Write the request
+ w := bufio.NewWriter(conn)
+ req.write(w)
+ if err := w.Flush(); err != nil {
+ return nil, err
+ }
+
+ // Read the response
+ resp := &Response{}
+ r := bufio.NewReader(conn)
+ // Store connection information
+ resp.TLS = conn.ConnectionState()
+ if err := resp.read(r); err != nil {
+ return nil, err
+ }
+ return resp, nil
}
diff --git a/examples/client/client.go b/examples/client/client.go
index a90af18..353359c 100644
--- a/examples/client/client.go
+++ b/examples/client/client.go
@@ -5,6 +5,7 @@ package main
import (
"bufio"
"crypto/tls"
+ "crypto/x509"
"fmt"
"log"
"os"
@@ -12,7 +13,18 @@ import (
"git.sr.ht/~adnano/go-gemini"
)
-var cert tls.Certificate
+var (
+ client = &gemini.Client{
+ VerifyCertificate: func(cert *x509.Certificate) error {
+ // if gemini.Fingerprint(cert) != expected {
+ // return errors.New("invalid server certificate")
+ // }
+ return nil
+ },
+ }
+
+ cert tls.Certificate
+)
func init() {
// Configure a client side certificate.
@@ -35,11 +47,14 @@ func makeRequest(url string) {
log.Fatal(err)
}
req.Certificate = cert
- resp, err := gemini.Do(req)
+
+ resp, err := client.Send(req)
if err != nil {
log.Fatal(err)
}
+ fmt.Println(gemini.Fingerprint(resp.TLS.PeerCertificates[0]))
+
fmt.Println("Status code:", resp.Status)
fmt.Println("Meta:", resp.Meta)