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,12 +111,77 @@ type Response struct { TLS tls.ConnectionState } -// Do sends a Gemini request and returns a Gemini response. -func Do(req *Request) (*Response, error) { +// 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 err + } + status, err := strconv.Atoi(string(statusB)) + if err != nil { + return err + } + resp.Status = status + + // Read one space + if b, err := r.ReadByte(); err != nil { + return err + } else if b != ' ' { + return ErrProtocol + } + + // Read the meta + meta, err := r.ReadString('\r') + if err != nil { + 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 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 + if status/10 == StatusClassSuccess { + var err error + resp.Body, err = ioutil.ReadAll(r) + if err != nil { + return err + } + } + return 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 { @@ -134,66 +190,19 @@ func Do(req *Request) (*Response, error) { 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 + // Read the response + resp := &Response{} r := bufio.NewReader(conn) - statusB := make([]byte, 2) - if _, err := r.Read(statusB); err != nil { + // Store connection information + resp.TLS = conn.ConnectionState() + if err := resp.read(r); err != nil { return nil, err } - status, err := strconv.Atoi(string(statusB)) - if err != nil { - return nil, err - } - - // Read one space - if b, err := r.ReadByte(); err != nil { - return nil, err - } else if b != ' ' { - return nil, 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 - } - - // 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 - } - - // Read response body - var body []byte - if status/10 == StatusClassSuccess { - var err error - body, err = ioutil.ReadAll(r) - if err != nil { - return nil, err - } - } - - return &Response{ - Status: status, - Meta: meta, - Body: body, - TLS: conn.ConnectionState(), - }, nil + 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)