From 9506f69f1aef8722f06bb47d65f99ff0bffb7347 Mon Sep 17 00:00:00 2001 From: Adnan Maolood Date: Wed, 21 Oct 2020 17:07:28 -0400 Subject: [PATCH] Refactor --- client.go | 165 ---------------------------------------------------- doc.go | 6 +- request.go | 92 +++++++++++++++++++++++++++++ response.go | 85 +++++++++++++++++++++++++++ server.go | 119 +++++++++++-------------------------- 5 files changed, 213 insertions(+), 254 deletions(-) create mode 100644 request.go create mode 100644 response.go diff --git a/client.go b/client.go index abf9a57..a513a0a 100644 --- a/client.go +++ b/client.go @@ -4,173 +4,8 @@ import ( "bufio" "crypto/tls" "crypto/x509" - "io/ioutil" - "net" - "net/url" - "strconv" ) -// Request represents a Gemini request. -type Request struct { - // URL specifies the URL being requested. - URL *url.URL - - // For client requests, Host specifies the host on which the URL is sought. - // Host must contain a port. - // This field is ignored by the server. - Host string - - // Certificate specifies the TLS certificate to use for the request. - // Request certificates take precedence over client certificates. - // This field is ignored by the server. - Certificate *tls.Certificate - - // RemoteAddr allows servers and other software to record the network - // address that sent the request. - // This field is ignored by the client. - RemoteAddr net.Addr - - // TLS allows servers and other software to record information about the TLS - // connection on which the request was received. - // This field is ignored by the client. - TLS tls.ConnectionState -} - -// hostname returns the host without the port. -func hostname(host string) string { - hostname, _, err := net.SplitHostPort(host) - if err != nil { - return host - } - return hostname -} - -// 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 - } - - // If there is no port, use the default port of 1965 - host := u.Host - if u.Port() == "" { - host += ":1965" - } - - return &Request{ - Host: host, - URL: u, - }, nil -} - -// NewRequestTo returns a new request for the provided URL to the provided host. -// The host must contain a port. -func NewRequestTo(rawurl, host string) (*Request, error) { - u, err := url.Parse(rawurl) - if err != nil { - return nil, err - } - - return &Request{ - Host: host, - URL: u, - }, nil -} - -// write writes the Gemini request to the provided buffered writer. -func (r *Request) write(w *bufio.Writer) error { - url := r.URL.String() - // User is invalid - if r.URL.User != nil || len(url) > 1024 { - return ErrInvalidURL - } - if _, err := w.WriteString(url); err != nil { - return err - } - if _, err := w.Write(crlf); err != nil { - return err - } - return nil -} - -// Response is a Gemini response. -type Response struct { - // Status represents the response status. - Status int - - // Meta contains more information related to the response status. - // For successful responses, Meta should contain the mimetype of the response. - // For failure responses, Meta should contain a short description of the failure. - // Meta should not be longer than 1024 bytes. - Meta string - - // Body contains the response body. - Body []byte - - // TLS contains information about the TLS connection on which the response - // was received. - TLS tls.ConnectionState -} - -// 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 - - // Disregard invalid status codes - const minStatus, maxStatus = 1, 6 - statusClass := status / 10 - if statusClass < minStatus || statusClass > maxStatus { - return ErrInvalidResponse - } - - // Read one space - if b, err := r.ReadByte(); err != nil { - return err - } else if b != ' ' { - return ErrInvalidResponse - } - - // 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 ErrInvalidResponse - } - resp.Meta = meta - - // Read terminating newline - if b, err := r.ReadByte(); err != nil { - return err - } else if b != '\n' { - return ErrInvalidResponse - } - - // 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 { // KnownHosts is a list of known hosts that the client trusts. diff --git a/doc.go b/doc.go index 7f7e62d..84dea50 100644 --- a/doc.go +++ b/doc.go @@ -64,13 +64,13 @@ Servers must be configured with certificates: Servers can accept requests for multiple hosts and schemes: - server.HandleFunc("example.com", func(w *gmi.ResponseWriter, r *gmi.Request) { + server.RegisterFunc("example.com", func(w *gmi.ResponseWriter, r *gmi.Request) { fmt.Fprint(w, "Welcome to example.com") }) - server.HandleFunc("example.org", func(w *gmi.ResponseWriter, r *gmi.Request) { + server.RegisterFunc("example.org", func(w *gmi.ResponseWriter, r *gmi.Request) { fmt.Fprint(w, "Welcome to example.org") }) - server.HandleFunc("http://example.net", func(w *gmi.ResponseWriter, r *gmi.Request) { + server.RegisterFunc("http://example.net", func(w *gmi.ResponseWriter, r *gmi.Request) { fmt.Fprint(w, "Proxied content from http://example.net") }) diff --git a/request.go b/request.go new file mode 100644 index 0000000..7b2d209 --- /dev/null +++ b/request.go @@ -0,0 +1,92 @@ +package gmi + +import ( + "bufio" + "crypto/tls" + "net" + "net/url" +) + +// Request represents a Gemini request. +type Request struct { + // URL specifies the URL being requested. + URL *url.URL + + // For client requests, Host specifies the host on which the URL is sought. + // Host must contain a port. + // This field is ignored by the server. + Host string + + // Certificate specifies the TLS certificate to use for the request. + // Request certificates take precedence over client certificates. + // This field is ignored by the server. + Certificate *tls.Certificate + + // RemoteAddr allows servers and other software to record the network + // address that sent the request. + // This field is ignored by the client. + RemoteAddr net.Addr + + // TLS allows servers and other software to record information about the TLS + // connection on which the request was received. + // This field is ignored by the client. + TLS tls.ConnectionState +} + +// hostname returns the host without the port. +func hostname(host string) string { + hostname, _, err := net.SplitHostPort(host) + if err != nil { + return host + } + return hostname +} + +// 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 + } + + // If there is no port, use the default port of 1965 + host := u.Host + if u.Port() == "" { + host += ":1965" + } + + return &Request{ + Host: host, + URL: u, + }, nil +} + +// NewRequestTo returns a new request for the provided URL to the provided host. +// The host must contain a port. +func NewRequestTo(rawurl, host string) (*Request, error) { + u, err := url.Parse(rawurl) + if err != nil { + return nil, err + } + + return &Request{ + Host: host, + URL: u, + }, nil +} + +// write writes the Gemini request to the provided buffered writer. +func (r *Request) write(w *bufio.Writer) error { + url := r.URL.String() + // User is invalid + if r.URL.User != nil || len(url) > 1024 { + return ErrInvalidURL + } + if _, err := w.WriteString(url); err != nil { + return err + } + if _, err := w.Write(crlf); err != nil { + return err + } + return nil +} diff --git a/response.go b/response.go new file mode 100644 index 0000000..975425a --- /dev/null +++ b/response.go @@ -0,0 +1,85 @@ +package gmi + +import ( + "bufio" + "crypto/tls" + "io/ioutil" + "strconv" +) + +// Response is a Gemini response. +type Response struct { + // Status represents the response status. + Status int + + // Meta contains more information related to the response status. + // For successful responses, Meta should contain the mimetype of the response. + // For failure responses, Meta should contain a short description of the failure. + // Meta should not be longer than 1024 bytes. + Meta string + + // Body contains the response body. + Body []byte + + // TLS contains information about the TLS connection on which the response + // was received. + TLS tls.ConnectionState +} + +// 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 + + // Disregard invalid status codes + const minStatus, maxStatus = 1, 6 + statusClass := status / 10 + if statusClass < minStatus || statusClass > maxStatus { + return ErrInvalidResponse + } + + // Read one space + if b, err := r.ReadByte(); err != nil { + return err + } else if b != ' ' { + return ErrInvalidResponse + } + + // 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 ErrInvalidResponse + } + resp.Meta = meta + + // Read terminating newline + if b, err := r.ReadByte(); err != nil { + return err + } else if b != '\n' { + return ErrInvalidResponse + } + + // Read response body + if status/10 == StatusClassSuccess { + var err error + resp.Body, err = ioutil.ReadAll(r) + if err != nil { + return err + } + } + return nil +} diff --git a/server.go b/server.go index 7995160..f978d82 100644 --- a/server.go +++ b/server.go @@ -184,7 +184,7 @@ func (s *Server) responder(r *Request) Responder { return h } } - return NotFoundHandler() + return NotFoundResponder() } // ResponseWriter is used by a Gemini handler to construct a Gemini response. @@ -313,35 +313,19 @@ func Redirect(w *ResponseWriter, r *Request, url string) { w.WriteHeader(StatusRedirect, url) } -// RedirectHandler returns a simple handler that responds to each request with -// a redirect to the given URL. -func RedirectHandler(url string) Responder { - return ResponderFunc(func(w *ResponseWriter, r *Request) { - Redirect(w, r, url) - }) -} - // PermanentRedirect replies to the request with a permanent redirect to the given URL. func PermanentRedirect(w *ResponseWriter, r *Request, url string) { w.WriteHeader(StatusRedirectPermanent, url) } -// PermanentRedirectHandler returns a simple handler that responds to each request with -// a redirect to the given URL. -func PermanentRedirectHandler(url string) Responder { - return ResponderFunc(func(w *ResponseWriter, r *Request) { - PermanentRedirect(w, r, url) - }) -} - // NotFound replies to the request with the NotFound status code. func NotFound(w *ResponseWriter, r *Request) { w.WriteHeader(StatusNotFound, "Not found") } -// NotFoundHandler returns a simple handler that responds to each request with +// NotFoundResponder returns a simple responder that responds to each request with // the status code NotFound. -func NotFoundHandler() Responder { +func NotFoundResponder() Responder { return ResponderFunc(NotFound) } @@ -350,12 +334,6 @@ func Gone(w *ResponseWriter, r *Request) { w.WriteHeader(StatusGone, "Gone") } -// GoneHandler returns a simple handler that responds to each request with -// the status code Gone. -func GoneHandler() Responder { - return ResponderFunc(Gone) -} - // CertificateRequired responds to the request with the CertificateRequired // status code. func CertificateRequired(w *ResponseWriter, r *Request) { @@ -379,15 +357,6 @@ func WithCertificate(w *ResponseWriter, r *Request, f func(*x509.Certificate)) { f(cert) } -// CertificateHandler returns a simple handler that requests a certificate from -// clients if they did not provide one, and calls f with the first certificate -// if they did. -func CertificateHandler(f func(*x509.Certificate)) Responder { - return ResponderFunc(func(w *ResponseWriter, r *Request) { - WithCertificate(w, r, f) - }) -} - // ResponderFunc is a wrapper around a bare function that implements Handler. type ResponderFunc func(*ResponseWriter, *Request) @@ -437,7 +406,7 @@ type ServeMux struct { } type muxEntry struct { - h Responder + r Responder pattern string } @@ -465,21 +434,21 @@ func cleanPath(p string) string { // Find a handler on a handler map given a path string. // Most-specific (longest) pattern wins. -func (mux *ServeMux) match(path string) (h Responder, pattern string) { +func (mux *ServeMux) match(path string) Responder { // Check for exact match first. v, ok := mux.m[path] if ok { - return v.h, v.pattern + return v.r } // Check for longest valid match. mux.es contains all patterns // that end in / sorted from longest to shortest. for _, e := range mux.es { if strings.HasPrefix(path, e.pattern) { - return e.h, e.pattern + return e.r } } - return nil, "" + return nil } // redirectToPathSlash determines if the given path needs appending "/" to it. @@ -517,69 +486,47 @@ func (mux *ServeMux) shouldRedirectRLocked(path string) bool { return false } -// Handler returns the handler to use for the given request. -// It consults r.URL.Path. It always returns a non-nil handler. -// If the path is not in its canonical form, the -// handler will be an internally-generated handler that redirects -// to the canonical path. If the host contains a port, it is ignored -// when matching handlers. -// -// Handler also returns the registered pattern that matches the -// request or, in the case of internally-generated redirects, -// the pattern that will match after following the redirect. -// -// If there is no registered handler that applies to the request, -// Handler returns a "not found" handler and an empty pattern. -func (mux *ServeMux) Handler(r *Request) (h Responder, pattern string) { +// Respond dispatches the request to the responder whose +// pattern most closely matches the request URL. +func (mux *ServeMux) Respond(w *ResponseWriter, r *Request) { path := cleanPath(r.URL.Path) // If the given path is /tree and its handler is not registered, // redirect for /tree/. if u, ok := mux.redirectToPathSlash(path, r.URL); ok { - return RedirectHandler(u.String()), u.Path + Redirect(w, r, u.String()) + return } if path != r.URL.Path { - _, pattern = mux.handler(path) - url := *r.URL - url.Path = path - return RedirectHandler(url.String()), pattern + u := *r.URL + u.Path = path + Redirect(w, r, u.String()) + return } - return mux.handler(r.URL.Path) -} - -// handler is the main implementation of Handler. -// The path is known to be in canonical form. -func (mux *ServeMux) handler(path string) (h Responder, pattern string) { mux.mu.RLock() defer mux.mu.RUnlock() - h, pattern = mux.match(path) - if h == nil { - h, pattern = NotFoundHandler(), "" + resp := mux.match(path) + if resp == nil { + NotFound(w, r) + return } - return + resp.Respond(w, r) } -// Respond dispatches the request to the handler whose -// pattern most closely matches the request URL. -func (mux *ServeMux) Respond(w *ResponseWriter, r *Request) { - h, _ := mux.Handler(r) - h.Respond(w, r) -} - -// Handle registers the handler for the given pattern. -// If a handler already exists for pattern, Handle panics. -func (mux *ServeMux) Handle(pattern string, handler Responder) { +// Handle registers the responder for the given pattern. +// If a responder already exists for pattern, Handle panics. +func (mux *ServeMux) Handle(pattern string, responder Responder) { mux.mu.Lock() defer mux.mu.Unlock() if pattern == "" { panic("gmi: invalid pattern") } - if handler == nil { - panic("gmi: nil handler") + if responder == nil { + panic("gmi: nil responder") } if _, exist := mux.m[pattern]; exist { panic("gmi: multiple registrations for " + pattern) @@ -588,7 +535,7 @@ func (mux *ServeMux) Handle(pattern string, handler Responder) { if mux.m == nil { mux.m = make(map[string]muxEntry) } - e := muxEntry{h: handler, pattern: pattern} + e := muxEntry{responder, pattern} mux.m[pattern] = e if pattern[len(pattern)-1] == '/' { mux.es = appendSorted(mux.es, e) @@ -610,10 +557,10 @@ func appendSorted(es []muxEntry, e muxEntry) []muxEntry { return es } -// HandleFunc registers the handler function for the given pattern. -func (mux *ServeMux) HandleFunc(pattern string, handler func(*ResponseWriter, *Request)) { - if handler == nil { - panic("gmi: nil handler") +// HandleFunc registers the responder function for the given pattern. +func (mux *ServeMux) HandleFunc(pattern string, responder func(*ResponseWriter, *Request)) { + if responder == nil { + panic("gmi: nil responder") } - mux.Handle(pattern, ResponderFunc(handler)) + mux.Handle(pattern, ResponderFunc(responder)) }