From e20b8a0a5e8474b14b0c6702ffc1fdc7df76756e Mon Sep 17 00:00:00 2001 From: adnano Date: Mon, 21 Sep 2020 17:23:51 -0400 Subject: [PATCH] Add examples --- README.md | 97 +++------------------------------------ client.go | 21 +++++---- example/client/client.go | 54 ++++++++++++++++++++++ example/server/.gitignore | 2 + example/server/server.go | 41 +++++++++++++++++ server.go | 25 ++++------ util.go | 16 +++++++ 7 files changed, 140 insertions(+), 116 deletions(-) create mode 100644 example/client/client.go create mode 100644 example/server/.gitignore create mode 100644 example/server/server.go create mode 100644 util.go diff --git a/README.md b/README.md index 7800c2f..a055361 100644 --- a/README.md +++ b/README.md @@ -3,98 +3,13 @@ `go-gemini` implements the [Gemini protocol](https://gemini.circumlunar.space) in Go. -It aims to provide an interface similar to that of `net/http` to make it easy -to develop Gemini clients and servers. +It aims to provide an API similar to that of `net/http` to make it easy to +develop Gemini clients and servers. -## Usage +## Examples -First generate TLS keys for your server to use. +See `examples/client` and `examples/server` for an example client and server. -```sh -openssl genrsa -out server.key 2048 -openssl ecparam -genkey -name secp384r1 -out server.key -openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 -``` +To run the examples: -Next, import and use `go-gemini`. Here is a simple server: - -```go -import ( - "git.sr.ht/~adnano/go-gemini" -) - -func main() { - config := &tls.Config{} - cert, err := tls.LoadX509KeyPair("server.crt", "server.key") - if err != nil { - log.Fatal(err) - } - config.Certificates = append(config.Certificates, cert) - - mux := &gemini.Mux{} - mux.HandleFunc("/", func(url *url.URL) *gemini.Response { - return &gemini.Response{ - Status: gemini.StatusSuccess, - Meta: "text/gemini", - Body: []byte("You requested " + url.String()), - } - }) - - server := gemini.Server{ - TLSConfig: config, - Handler: mux, - } - server.ListenAndServe() -} -``` - -And a simple client: - -```go -import ( - "git.sr.ht/~adnano/go-gemini" -) - -var client gemini.Client - -func makeRequest(url string) { - resp, err := client.Get(url) - if err != nil { - log.Fatal(err) - } - - fmt.Println("Status code:", resp.Status) - fmt.Println("Meta:", resp.Meta) - - switch resp.Status / 10 { - case gemini.StatusClassInput: - scanner := bufio.NewScanner(os.Stdin) - fmt.Printf("%s: ", resp.Meta) - scanner.Scan() - query := scanner.Text() - makeRequest(url + "?" + query) - return - case gemini.StatusClassSuccess: - fmt.Print("Body:\n", string(resp.Body)) - case gemini.StatusClassRedirect: - log.Print("Redirecting to ", resp.Meta) - makeRequest(resp.Meta) - return - case gemini.StatusClassTemporaryFailure: - log.Fatal("Temporary failure") - case gemini.StatusClassPermanentFailure: - log.Fatal("Permanent failure") - case gemini.StatusClassClientCertificateRequired: - log.Fatal("Client certificate required") - default: - log.Fatal("Protocol error: invalid status code") - } -} - -func main() { - if len(os.Args) < 2 { - log.Fatalf("usage: %s gemini://...", os.Args[0]) - } - makeRequest(os.Args[1]) -} -``` + go run -tags=example ./example/server diff --git a/client.go b/client.go index b44c8fb..9609ac5 100644 --- a/client.go +++ b/client.go @@ -1,7 +1,6 @@ package gemini import ( - "bufio" "crypto/tls" "errors" "io/ioutil" @@ -11,15 +10,16 @@ import ( ) var ( - ProtocolError = errors.New("Protocol error") + ErrProtocol = errors.New("Protocol error") ) // Client is a Gemini client. type Client struct { - TLSConfig tls.Config + TLSConfig *tls.Config // TODO: Client certificate support } -func (c *Client) Get(url string) (*Response, error) { +// Request makes a request for the provided URL. The host is inferred from the URL. +func (c *Client) Request(url string) (*Response, error) { req, err := NewRequest(url) if err != nil { return nil, err @@ -27,7 +27,8 @@ func (c *Client) Get(url string) (*Response, error) { return c.Do(req) } -func (c *Client) GetProxy(host, url string) (*Response, error) { +// ProxyRequest requests the provided URL from the provided host. +func (c *Client) ProxyRequest(host, url string) (*Response, error) { req, err := NewProxyRequest(host, url) if err != nil { return nil, err @@ -73,6 +74,7 @@ func NewProxyRequest(host, rawurl string) (*Request, error) { }, nil } +// Do completes a request. func (c *Client) Do(req *Request) (*Response, error) { host := req.Host if strings.LastIndex(host, ":") == -1 { @@ -113,13 +115,14 @@ func (c *Client) Do(req *Request) (*Response, error) { return nil, err } if space[0] != ' ' { - return nil, ProtocolError + return nil, ErrProtocol } // Read the meta - scanner := bufio.NewScanner(conn) - scanner.Scan() - meta := scanner.Text() + meta, err := readLine(conn) + if err != nil { + return nil, err + } // Read the response body body, err := ioutil.ReadAll(conn) diff --git a/example/client/client.go b/example/client/client.go new file mode 100644 index 0000000..5b6fde2 --- /dev/null +++ b/example/client/client.go @@ -0,0 +1,54 @@ +// +build example + +package main + +import ( + "bufio" + "fmt" + "git.sr.ht/~adnano/go-gemini" + "log" + "os" +) + +var client gemini.Client + +func makeRequest(url string) { + resp, err := client.Request(url) + if err != nil { + log.Fatal(err) + } + + fmt.Println("Status code:", resp.Status) + fmt.Println("Meta:", resp.Meta) + + switch resp.Status / 10 { + case gemini.StatusClassInput: + scanner := bufio.NewScanner(os.Stdin) + fmt.Printf("%s: ", resp.Meta) + scanner.Scan() + query := scanner.Text() + makeRequest(url + "?" + query) + return + case gemini.StatusClassSuccess: + fmt.Print("Body:\n", string(resp.Body)) + case gemini.StatusClassRedirect: + log.Print("Redirecting to ", resp.Meta) + makeRequest(resp.Meta) + return + case gemini.StatusClassTemporaryFailure: + log.Fatal("Temporary failure") + case gemini.StatusClassPermanentFailure: + log.Fatal("Permanent failure") + case gemini.StatusClassClientCertificateRequired: + log.Fatal("Client Certificate Required") + default: + log.Fatal("Protocol Error") + } +} + +func main() { + if len(os.Args) < 2 { + log.Fatalf("usage: %s gemini://...", os.Args[0]) + } + makeRequest(os.Args[1]) +} diff --git a/example/server/.gitignore b/example/server/.gitignore new file mode 100644 index 0000000..10cdeb2 --- /dev/null +++ b/example/server/.gitignore @@ -0,0 +1,2 @@ +server.crt +server.key diff --git a/example/server/server.go b/example/server/server.go new file mode 100644 index 0000000..3f0a3e3 --- /dev/null +++ b/example/server/server.go @@ -0,0 +1,41 @@ +// +build example + +package main + +import ( + "crypto/tls" + "git.sr.ht/~adnano/go-gemini" + "log" + "net/url" +) + +func main() { + // Load a TLS key pair. + // To generate a TLS key pair, run: + // + // openssl genrsa -out server.key 2048 + // openssl ecparam -genkey -name secp384r1 -out server.key + // openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 + // + config := &tls.Config{} + cert, err := tls.LoadX509KeyPair("example/server/server.crt", "example/server/server.key") + if err != nil { + log.Fatal(err) + } + config.Certificates = append(config.Certificates, cert) + + mux := &gemini.Mux{} + mux.HandleFunc("/", func(url *url.URL) *gemini.Response { + return &gemini.Response{ + Status: gemini.StatusSuccess, + Meta: "text/gemini", + Body: []byte("You requested " + url.String()), + } + }) + + server := gemini.Server{ + TLSConfig: config, + Handler: mux, + } + server.ListenAndServe() +} diff --git a/server.go b/server.go index 24c6a15..785add3 100644 --- a/server.go +++ b/server.go @@ -1,7 +1,6 @@ package gemini import ( - "bufio" "crypto/tls" "io" "net" @@ -49,12 +48,12 @@ type Response struct { Body []byte } -// Write writes the Response to the provided io.Writer. +// Write writes the Gemini response header and body to the provided io.Writer. func (r *Response) Write(w io.Writer) { header := strconv.Itoa(r.Status) + " " + r.Meta + "\r\n" w.Write([]byte(header)) - // Only write response body on success + // Only write the response body on success if r.Status/10 == StatusClassSuccess { w.Write(r.Body) } @@ -67,7 +66,7 @@ type Server struct { Handler Handler } -// ListenAndServer listens on the given address and serves. +// ListenAndServe listens for requests at the server's configured address. func (s *Server) ListenAndServe() error { addr := s.Addr if addr == "" { @@ -108,8 +107,8 @@ func (s *Server) Serve(ln net.Listener) error { // Handler handles a url with a response. type Handler interface { - // Serve accepts a url, as that is the only information that the client - // provides in a request. + // Serve accepts a url, as that is the only information that is provided in + // a Gemini request. Serve(*url.URL) *Response } @@ -139,6 +138,7 @@ func (m *Mux) match(url *url.URL) Handler { return nil } +// Handle registers a handler for the given pattern. func (m *Mux) Handle(pattern string, handler Handler) { url, err := url.Parse(pattern) if err != nil { @@ -152,11 +152,13 @@ func (m *Mux) Handle(pattern string, handler Handler) { }) } +// HandleFunc registers a HandlerFunc for the given pattern. func (m *Mux) HandleFunc(pattern string, handlerFunc func(url *url.URL) *Response) { handler := HandlerFunc(handlerFunc) m.Handle(pattern, handler) } +// Serve responds to the request with the appropriate handler. func (m *Mux) Serve(url *url.URL) *Response { h := m.match(url) if h == nil { @@ -168,18 +170,9 @@ func (m *Mux) Serve(url *url.URL) *Response { return h.Serve(url) } +// A wrapper around a bare function that implements Handler. type HandlerFunc func(url *url.URL) *Response func (f HandlerFunc) Serve(url *url.URL) *Response { return f(url) } - -// readLine reads a line. -func readLine(r io.Reader) (string, error) { - scanner := bufio.NewScanner(r) - scanner.Scan() - if err := scanner.Err(); err != nil { - return "", err - } - return scanner.Text(), nil -} diff --git a/util.go b/util.go new file mode 100644 index 0000000..efaa9c8 --- /dev/null +++ b/util.go @@ -0,0 +1,16 @@ +package gemini + +import ( + "bufio" + "io" +) + +// readLine reads a line. +func readLine(r io.Reader) (string, error) { + scanner := bufio.NewScanner(r) + scanner.Scan() + if err := scanner.Err(); err != nil { + return "", err + } + return scanner.Text(), nil +}