Implement configurable Client
This commit is contained in:
		
							parent
							
								
									2f43c191cc
								
							
						
					
					
						commit
						53d84882ea
					
				@ -33,11 +33,10 @@ A quick overview of the Gemini protocol:
 | 
				
			|||||||
The way this is implemented in this package is like so:
 | 
					The way this is implemented in this package is like so:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1. Client makes a request with `NewRequest`. The client then sends the request
 | 
					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.
 | 
					2. Server recieves the request and constructs a response.
 | 
				
			||||||
	The server calls the `Serve(*ResponseWriter, *Request)` method on the
 | 
						The server calls the `Serve(*ResponseWriter, *Request)` method on the
 | 
				
			||||||
	`Handler` field. The handler writes the response. The server then closes
 | 
						`Handler` field. The handler writes the response. The server then closes
 | 
				
			||||||
	the connection.
 | 
						the connection.
 | 
				
			||||||
5. Client recieves the response as a `*Response`. The client then handles the
 | 
					3. 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
 | 
						response.
 | 
				
			||||||
	Trust-On-First-Use method.
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										143
									
								
								client.go
									
									
									
									
									
								
							
							
						
						
									
										143
									
								
								client.go
									
									
									
									
									
								
							@ -4,6 +4,7 @@ package gemini
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"bufio"
 | 
						"bufio"
 | 
				
			||||||
	"crypto/tls"
 | 
						"crypto/tls"
 | 
				
			||||||
 | 
						"crypto/x509"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"io/ioutil"
 | 
						"io/ioutil"
 | 
				
			||||||
	"net"
 | 
						"net"
 | 
				
			||||||
@ -17,17 +18,7 @@ var (
 | 
				
			|||||||
	ErrInvalidURL = errors.New("gemini: requested URL is invalid")
 | 
						ErrInvalidURL = errors.New("gemini: requested URL is invalid")
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Request is a Gemini request.
 | 
					// Request represents 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)
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
type Request struct {
 | 
					type Request struct {
 | 
				
			||||||
	// URL specifies the URL being requested.
 | 
						// URL specifies the URL being requested.
 | 
				
			||||||
	URL *url.URL
 | 
						URL *url.URL
 | 
				
			||||||
@ -37,7 +28,7 @@ type Request struct {
 | 
				
			|||||||
	// This field is ignored by the server.
 | 
						// This field is ignored by the server.
 | 
				
			||||||
	Host string
 | 
						Host string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// The certificate to use for the request.
 | 
						// Certificate specifies the TLS certificate to use for the request.
 | 
				
			||||||
	Certificate tls.Certificate
 | 
						Certificate tls.Certificate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// RemoteAddr allows servers and other software to record the network
 | 
						// RemoteAddr allows servers and other software to record the network
 | 
				
			||||||
@ -120,12 +111,77 @@ type Response struct {
 | 
				
			|||||||
	TLS tls.ConnectionState
 | 
						TLS tls.ConnectionState
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Do sends a Gemini request and returns a Gemini response.
 | 
					// read reads a Gemini response from the provided buffered reader.
 | 
				
			||||||
func Do(req *Request) (*Response, error) {
 | 
					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
 | 
						// Connect to the host
 | 
				
			||||||
	config := &tls.Config{
 | 
						config := &tls.Config{
 | 
				
			||||||
		InsecureSkipVerify: true,
 | 
							InsecureSkipVerify: true,
 | 
				
			||||||
		Certificates:       []tls.Certificate{req.Certificate},
 | 
							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)
 | 
						conn, err := tls.Dial("tcp", req.Host, config)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@ -134,66 +190,19 @@ func Do(req *Request) (*Response, error) {
 | 
				
			|||||||
	defer conn.Close()
 | 
						defer conn.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Write the request
 | 
						// Write the request
 | 
				
			||||||
	// TODO: Is buffered I/O necessary here?
 | 
					 | 
				
			||||||
	w := bufio.NewWriter(conn)
 | 
						w := bufio.NewWriter(conn)
 | 
				
			||||||
	req.write(w)
 | 
						req.write(w)
 | 
				
			||||||
	if err := w.Flush(); err != nil {
 | 
						if err := w.Flush(); err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Read the response status
 | 
						// Read the response
 | 
				
			||||||
 | 
						resp := &Response{}
 | 
				
			||||||
	r := bufio.NewReader(conn)
 | 
						r := bufio.NewReader(conn)
 | 
				
			||||||
	statusB := make([]byte, 2)
 | 
						// Store connection information
 | 
				
			||||||
	if _, err := r.Read(statusB); err != nil {
 | 
						resp.TLS = conn.ConnectionState()
 | 
				
			||||||
 | 
						if err := resp.read(r); err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	status, err := strconv.Atoi(string(statusB))
 | 
						return resp, nil
 | 
				
			||||||
	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
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ package main
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"bufio"
 | 
						"bufio"
 | 
				
			||||||
	"crypto/tls"
 | 
						"crypto/tls"
 | 
				
			||||||
 | 
						"crypto/x509"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
@ -12,7 +13,18 @@ import (
 | 
				
			|||||||
	"git.sr.ht/~adnano/go-gemini"
 | 
						"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() {
 | 
					func init() {
 | 
				
			||||||
	// Configure a client side certificate.
 | 
						// Configure a client side certificate.
 | 
				
			||||||
@ -35,11 +47,14 @@ func makeRequest(url string) {
 | 
				
			|||||||
		log.Fatal(err)
 | 
							log.Fatal(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	req.Certificate = cert
 | 
						req.Certificate = cert
 | 
				
			||||||
	resp, err := gemini.Do(req)
 | 
					
 | 
				
			||||||
 | 
						resp, err := client.Send(req)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Fatal(err)
 | 
							log.Fatal(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fmt.Println(gemini.Fingerprint(resp.TLS.PeerCertificates[0]))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fmt.Println("Status code:", resp.Status)
 | 
						fmt.Println("Status code:", resp.Status)
 | 
				
			||||||
	fmt.Println("Meta:", resp.Meta)
 | 
						fmt.Println("Meta:", resp.Meta)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user