1
0
Fork 0
mirror of https://git.sr.ht/~adnano/go-gemini synced 2024-05-12 14:36:06 +02:00
This commit is contained in:
Adnan Maolood 2020-10-21 17:07:28 -04:00
parent aeff8a051c
commit 9506f69f1a
5 changed files with 213 additions and 254 deletions

165
client.go
View File

@ -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
View File

@ -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
View 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
View 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
View File

@ -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))
}