diff --git a/README.md b/README.md index 638115e..f98a442 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ [![GoDoc](https://godoc.org/git.sr.ht/~adnano/go-gemini?status.svg)](https://godoc.org/git.sr.ht/~adnano/go-gemini) -`go-gemini` implements the [Gemini protocol](https://gemini.circumlunar.space) in -Go. +`go-gemini` implements the [Gemini protocol](https://gemini.circumlunar.space) +in Go. It aims to provide an API similar to that of `net/http` to make it easy to develop Gemini clients and servers. diff --git a/client.go b/client.go index 7585dfc..ecd91ee 100644 --- a/client.go +++ b/client.go @@ -3,18 +3,11 @@ package gemini import ( "bytes" "crypto/tls" - "errors" "io/ioutil" - "net/url" "strconv" "strings" ) -var ( - ErrProtocol = errors.New("Protocol error") - ErrInvalidURL = errors.New("Invalid URL") -) - // Client is a Gemini client. type Client struct{} @@ -44,55 +37,6 @@ func (c *Client) ProxyRequest(host, url string) (*Response, error) { return c.Do(req) } -// 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.Certificates = append(req.Certificates, cert) -// -type Request struct { - Host string // host or host:port - URL *url.URL // the requested URL - Certificates []tls.Certificate // client certificates -} - -// NewRequest returns a new request. The host is inferred from the provided url. -func NewRequest(rawurl string) (*Request, error) { - u, err := url.Parse(rawurl) - if err != nil { - return nil, err - } - - // Ignore UserInfo if present - u.User = nil - - return &Request{ - Host: u.Host, - URL: u, - }, nil -} - -// NewProxyRequest makes a new request using the provided host. -func NewProxyRequest(host, rawurl string) (*Request, error) { - u, err := url.Parse(rawurl) - if err != nil { - return nil, err - } - - // Ignore UserInfo if present - u.User = nil - - return &Request{ - Host: host, - URL: u, - }, nil -} - // Do sends a Gemini request and returns a Gemini response. func (c *Client) Do(req *Request) (*Response, error) { host := req.Host @@ -113,8 +57,7 @@ func (c *Client) Do(req *Request) (*Response, error) { defer conn.Close() // Write the request - request := req.URL.String() + "\r\n" - if _, err := conn.Write([]byte(request)); err != nil { + if err := req.Write(conn); err != nil { return nil, err } diff --git a/gemini.go b/gemini.go new file mode 100644 index 0000000..c39474e --- /dev/null +++ b/gemini.go @@ -0,0 +1,132 @@ +// Package gemini implements the Gemini protocol. +package gemini + +import ( + "crypto/tls" + "errors" + "io" + "net/url" + "strconv" +) + +// Status codes. +const ( + StatusInput = 10 + StatusSensitiveInput = 11 + StatusSuccess = 20 + StatusRedirectTemporary = 30 + StatusRedirectPermanent = 31 + StatusTemporaryFailure = 40 + StatusServerUnavailable = 41 + StatusCGIError = 42 + StatusProxyError = 43 + StatusSlowDown = 44 + StatusPermanentFailure = 50 + StatusNotFound = 51 + StatusGone = 52 + StatusProxyRequestRefused = 53 + StatusBadRequest = 59 + StatusClientCertificateRequired = 60 + StatusCertificateNotAuthorised = 61 + StatusCertificateNotValid = 62 +) + +// Status code categories. +const ( + StatusClassInput = 1 + StatusClassSuccess = 2 + StatusClassRedirect = 3 + StatusClassTemporaryFailure = 4 + StatusClassPermanentFailure = 5 + StatusClassClientCertificateRequired = 6 +) + +// Errors. +var ( + ErrProtocol = errors.New("Protocol error") + ErrInvalidURL = errors.New("Invalid URL") +) + +// 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.Certificates = append(req.Certificates, cert) +// +type Request struct { + Host string // host or host:port + URL *url.URL // the requested URL + Certificates []tls.Certificate // client certificates +} + +// NewRequest returns a new request. The host is inferred from the provided url. +func NewRequest(rawurl string) (*Request, error) { + u, err := url.Parse(rawurl) + if err != nil { + return nil, err + } + + // UserInfo is invalid + if u.User != nil { + return nil, ErrInvalidURL + } + + return &Request{ + Host: u.Host, + URL: u, + }, nil +} + +// NewProxyRequest returns a new request using the provided host. +func NewProxyRequest(host, rawurl string) (*Request, error) { + u, err := url.Parse(rawurl) + if err != nil { + return nil, err + } + + // UserInfo is invalid + if u.User != nil { + return nil, ErrInvalidURL + } + + return &Request{ + Host: host, + URL: u, + }, nil +} + +// Write writes the Gemini request to the provided io.Writer. +func (r *Request) Write(w io.Writer) error { + request := r.URL.String() + "\r\n" + _, err := w.Write([]byte(request)) + return err +} + +// Response is a Gemini response. +type Response struct { + Status int + Meta string + Body []byte +} + +// Write writes the Gemini response header and body to the provided io.Writer. +func (r *Response) Write(w io.Writer) error { + header := strconv.Itoa(r.Status) + " " + r.Meta + "\r\n" + if _, err := w.Write([]byte(header)); err != nil { + return err + } + + // Only write the response body on success + if r.Status/10 == StatusClassSuccess { + if _, err := w.Write(r.Body); err != nil { + return err + } + } + + return nil +} diff --git a/server.go b/server.go index ad93738..3de09df 100644 --- a/server.go +++ b/server.go @@ -3,63 +3,11 @@ package gemini import ( "crypto/tls" "crypto/x509" - "io" "net" "net/url" - "strconv" "strings" ) -// Status codes. -const ( - StatusInput = 10 - StatusSensitiveInput = 11 - StatusSuccess = 20 - StatusRedirectTemporary = 30 - StatusRedirectPermanent = 31 - StatusTemporaryFailure = 40 - StatusServerUnavailable = 41 - StatusCGIError = 42 - StatusProxyError = 43 - StatusSlowDown = 44 - StatusPermanentFailure = 50 - StatusNotFound = 51 - StatusGone = 52 - StatusProxyRequestRefused = 53 - StatusBadRequest = 59 - StatusClientCertificateRequired = 60 - StatusCertificateNotAuthorised = 61 - StatusCertificateNotValid = 62 -) - -// Status code categories. -const ( - StatusClassInput = 1 - StatusClassSuccess = 2 - StatusClassRedirect = 3 - StatusClassTemporaryFailure = 4 - StatusClassPermanentFailure = 5 - StatusClassClientCertificateRequired = 6 -) - -// Response is a Gemini response. -type Response struct { - Status int - Meta string - Body []byte -} - -// 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 the response body on success - if r.Status/10 == StatusClassSuccess { - w.Write(r.Body) - } -} - // Server is a Gemini server. type Server struct { Addr string @@ -128,7 +76,7 @@ func (s *Server) Serve(ln net.Listener) error { type RequestInfo struct { URL *url.URL // the requested URL Certificates []*x509.Certificate // client certificates - RemoteAddr net.Addr + RemoteAddr net.Addr // client remote address } // A Handler responds to a Gemini request. @@ -163,7 +111,7 @@ func (m *Mux) match(url *url.URL) Handler { return nil } -// Handle registers a handler for the given pattern. +// Handle registers a Handler for the given pattern. func (m *Mux) Handle(pattern string, handler Handler) { url, err := url.Parse(pattern) if err != nil {