Refactor
This commit is contained in:
parent
aeff8a051c
commit
9506f69f1a
165
client.go
165
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.
|
||||
|
6
doc.go
6
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")
|
||||
})
|
||||
|
||||
|
92
request.go
Normal file
92
request.go
Normal file
@ -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
|
||||
}
|
85
response.go
Normal file
85
response.go
Normal file
@ -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
|
||||
}
|
119
server.go
119
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))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user