aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdnan Maolood <[email protected]>2020-10-28 14:59:45 -0400
committerAdnan Maolood <[email protected]>2020-10-28 15:03:54 -0400
commit7f0b1fa8a14d495edf6522fe9cb26cf14b69cb9b (patch)
tree99a2342cc3e29ffc0edbb8cdadc0c511075b70f3
parentFix examples/cert.go (diff)
downloadgo-gemini-7f0b1fa8a14d495edf6522fe9cb26cf14b69cb9b.tar.xz
go-gemini-7f0b1fa8a14d495edf6522fe9cb26cf14b69cb9b.zip
Refactor server certificates
-rw-r--r--examples/auth.go2
-rw-r--r--examples/server.go88
-rw-r--r--mux.go210
-rw-r--r--server.go250
4 files changed, 265 insertions, 285 deletions
diff --git a/examples/auth.go b/examples/auth.go
index eb7b129..4d61ea2 100644
--- a/examples/auth.go
+++ b/examples/auth.go
@@ -41,7 +41,7 @@ func main() {
mux.HandleFunc("/logout", logout)
var server gemini.Server
- if err := server.CertificateStore.Load("/var/lib/gemini/certs"); err != nil {
+ if err := server.Certificates.Load("/var/lib/gemini/certs"); err != nil {
log.Fatal(err)
}
server.Register("localhost", &mux)
diff --git a/examples/server.go b/examples/server.go
index 11ea9ed..6c29cdd 100644
--- a/examples/server.go
+++ b/examples/server.go
@@ -3,54 +3,38 @@
package main
import (
- "bytes"
+ "crypto"
"crypto/tls"
"crypto/x509"
"encoding/pem"
+ "io"
"log"
"os"
"time"
- gmi "git.sr.ht/~adnano/go-gemini"
+ "git.sr.ht/~adnano/go-gemini"
)
func main() {
- var server gmi.Server
- if err := server.CertificateStore.Load("/var/lib/gemini/certs"); err != nil {
+ var server gemini.Server
+ if err := server.Certificates.Load("/var/lib/gemini/certs"); err != nil {
log.Fatal(err)
}
- server.GetCertificate = func(hostname string, store *gmi.CertificateStore) *tls.Certificate {
- cert, err := store.Lookup(hostname)
- if err != nil {
- switch err {
- case gmi.ErrCertificateExpired:
- // Generate a new certificate if the current one is expired.
- log.Print("Old certificate expired, creating new one")
- fallthrough
- case gmi.ErrCertificateUnknown:
- // Generate a certificate if one does not exist.
- cert, err := gmi.CreateCertificate(gmi.CertificateOptions{
- DNSNames: []string{hostname},
- Duration: time.Hour,
- })
- if err != nil {
- // Failed to generate new certificate, abort
- return nil
- }
- // Store and return the new certificate
- err = writeCertificate("/var/lib/gemini/certs/"+hostname, cert)
- if err != nil {
- return nil
- }
- store.Add(hostname, cert)
- return &cert
- }
+ server.CreateCertificate = func(hostname string) (tls.Certificate, error) {
+ fmt.Println("Generating certificate for", hostname)
+ cert, err := gemini.CreateCertificate(gemini.CertificateOptions{
+ DNSNames: []string{hostname},
+ Duration: time.Minute, // for testing purposes
+ })
+ if err == nil {
+ // Write the new certificate to disk
+ err = writeCertificate("/var/lib/gemini/certs/"+hostname, cert)
}
- return cert
+ return cert, err
}
- var mux gmi.ServeMux
- mux.Handle("/", gmi.FileServer(gmi.Dir("/var/www")))
+ var mux gemini.ServeMux
+ mux.Handle("/", gemini.FileServer(gemini.Dir("/var/www")))
server.Register("localhost", &mux)
if err := server.ListenAndServe(); err != nil {
@@ -61,22 +45,13 @@ func main() {
// writeCertificate writes the provided certificate and private key
// to path.crt and path.key respectively.
func writeCertificate(path string, cert tls.Certificate) error {
- crt, err := marshalX509Certificate(cert.Leaf.Raw)
- if err != nil {
- return err
- }
- key, err := marshalPrivateKey(cert.PrivateKey)
- if err != nil {
- return err
- }
-
// Write the certificate
crtPath := path + ".crt"
crtOut, err := os.OpenFile(crtPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
- if _, err := crtOut.Write(crt); err != nil {
+ if err := marshalX509Certificate(crtOut, cert.Leaf.Raw); err != nil {
return err
}
@@ -86,30 +61,19 @@ func writeCertificate(path string, cert tls.Certificate) error {
if err != nil {
return err
}
- if _, err := keyOut.Write(key); err != nil {
- return err
- }
- return nil
+ return marshalPrivateKey(keyOut, cert.PrivateKey)
}
-// marshalX509Certificate returns a PEM-encoded version of the given raw certificate.
-func marshalX509Certificate(cert []byte) ([]byte, error) {
- var b bytes.Buffer
- if err := pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: cert}); err != nil {
- return nil, err
- }
- return b.Bytes(), nil
+// marshalX509Certificate writes a PEM-encoded version of the given certificate.
+func marshalX509Certificate(w io.Writer, cert []byte) error {
+ return pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: cert})
}
-// marshalPrivateKey returns PEM encoded versions of the given certificate and private key.
-func marshalPrivateKey(priv interface{}) ([]byte, error) {
- var b bytes.Buffer
+// marshalPrivateKey writes a PEM-encoded version of the given private key.
+func marshalPrivateKey(w io.Writer, priv crypto.PrivateKey) error {
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
- return nil, err
- }
- if err := pem.Encode(&b, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
- return nil, err
+ return err
}
- return b.Bytes(), nil
+ return pem.Encode(w, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes})
}
diff --git a/mux.go b/mux.go
new file mode 100644
index 0000000..47aa618
--- /dev/null
+++ b/mux.go
@@ -0,0 +1,210 @@
+package gemini
+
+import (
+ "net/url"
+ "path"
+ "sort"
+ "strings"
+ "sync"
+)
+
+// The following code is modified from the net/http package.
+
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// ServeMux is a Gemini request multiplexer.
+// It matches the URL of each incoming request against a list of registered
+// patterns and calls the handler for the pattern that
+// most closely matches the URL.
+//
+// Patterns name fixed, rooted paths, like "/favicon.ico",
+// or rooted subtrees, like "/images/" (note the trailing slash).
+// Longer patterns take precedence over shorter ones, so that
+// if there are handlers registered for both "/images/"
+// and "/images/thumbnails/", the latter handler will be
+// called for paths beginning "/images/thumbnails/" and the
+// former will receive requests for any other paths in the
+// "/images/" subtree.
+//
+// Note that since a pattern ending in a slash names a rooted subtree,
+// the pattern "/" matches all paths not matched by other registered
+// patterns, not just the URL with Path == "/".
+//
+// If a subtree has been registered and a request is received naming the
+// subtree root without its trailing slash, ServeMux redirects that
+// request to the subtree root (adding the trailing slash). This behavior can
+// be overridden with a separate registration for the path without
+// the trailing slash. For example, registering "/images/" causes ServeMux
+// to redirect a request for "/images" to "/images/", unless "/images" has
+// been registered separately.
+//
+// ServeMux also takes care of sanitizing the URL request path and
+// redirecting any request containing . or .. elements or repeated slashes
+// to an equivalent, cleaner URL.
+type ServeMux struct {
+ mu sync.RWMutex
+ m map[string]muxEntry
+ es []muxEntry // slice of entries sorted from longest to shortest.
+}
+
+type muxEntry struct {
+ r Responder
+ pattern string
+}
+
+// cleanPath returns the canonical path for p, eliminating . and .. elements.
+func cleanPath(p string) string {
+ if p == "" {
+ return "/"
+ }
+ if p[0] != '/' {
+ p = "/" + p
+ }
+ np := path.Clean(p)
+ // path.Clean removes trailing slash except for root;
+ // put the trailing slash back if necessary.
+ if p[len(p)-1] == '/' && np != "/" {
+ // Fast path for common case of p being the string we want:
+ if len(p) == len(np)+1 && strings.HasPrefix(p, np) {
+ np = p
+ } else {
+ np += "/"
+ }
+ }
+ return np
+}
+
+// Find a handler on a handler map given a path string.
+// Most-specific (longest) pattern wins.
+func (mux *ServeMux) match(path string) Responder {
+ // Check for exact match first.
+ v, ok := mux.m[path]
+ if ok {
+ 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.r
+ }
+ }
+ return nil
+}
+
+// redirectToPathSlash determines if the given path needs appending "/" to it.
+// This occurs when a handler for path + "/" was already registered, but
+// not for path itself. If the path needs appending to, it creates a new
+// URL, setting the path to u.Path + "/" and returning true to indicate so.
+func (mux *ServeMux) redirectToPathSlash(path string, u *url.URL) (*url.URL, bool) {
+ mux.mu.RLock()
+ shouldRedirect := mux.shouldRedirectRLocked(path)
+ mux.mu.RUnlock()
+ if !shouldRedirect {
+ return u, false
+ }
+ path = path + "/"
+ u = &url.URL{Path: path, RawQuery: u.RawQuery}
+ return u, true
+}
+
+// shouldRedirectRLocked reports whether the given path and host should be redirected to
+// path+"/". This should happen if a handler is registered for path+"/" but
+// not path -- see comments at ServeMux.
+func (mux *ServeMux) shouldRedirectRLocked(path string) bool {
+ if _, exist := mux.m[path]; exist {
+ return false
+ }
+
+ n := len(path)
+ if n == 0 {
+ return false
+ }
+ if _, exist := mux.m[path+"/"]; exist {
+ return path[n-1] != '/'
+ }
+
+ return false
+}
+
+// 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 {
+ Redirect(w, u.String())
+ return
+ }
+
+ if path != r.URL.Path {
+ u := *r.URL
+ u.Path = path
+ Redirect(w, u.String())
+ return
+ }
+
+ mux.mu.RLock()
+ defer mux.mu.RUnlock()
+
+ resp := mux.match(path)
+ if resp == nil {
+ w.WriteStatus(StatusNotFound)
+ return
+ }
+ resp.Respond(w, r)
+}
+
+// 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("gemini: invalid pattern")
+ }
+ if responder == nil {
+ panic("gemini: nil responder")
+ }
+ if _, exist := mux.m[pattern]; exist {
+ panic("gemini: multiple registrations for " + pattern)
+ }
+
+ if mux.m == nil {
+ mux.m = make(map[string]muxEntry)
+ }
+ e := muxEntry{responder, pattern}
+ mux.m[pattern] = e
+ if pattern[len(pattern)-1] == '/' {
+ mux.es = appendSorted(mux.es, e)
+ }
+}
+
+func appendSorted(es []muxEntry, e muxEntry) []muxEntry {
+ n := len(es)
+ i := sort.Search(n, func(i int) bool {
+ return len(es[i].pattern) < len(e.pattern)
+ })
+ if i == n {
+ return append(es, e)
+ }
+ // we now know that i points at where we want to insert
+ es = append(es, muxEntry{}) // try to grow the slice in place, any entry works.
+ copy(es[i+1:], es[i:]) // move shorter entries down
+ es[i] = e
+ return es
+}
+
+// HandleFunc registers the responder function for the given pattern.
+func (mux *ServeMux) HandleFunc(pattern string, responder func(*ResponseWriter, *Request)) {
+ if responder == nil {
+ panic("gemini: nil responder")
+ }
+ mux.Handle(pattern, ResponderFunc(responder))
+}
diff --git a/server.go b/server.go
index 65e62b5..f991f08 100644
--- a/server.go
+++ b/server.go
@@ -7,11 +7,8 @@ import (
"log"
"net"
"net/url"
- "path"
- "sort"
"strconv"
"strings"
- "sync"
"time"
)
@@ -21,13 +18,12 @@ type Server struct {
// If Addr is empty, the server will listen on the address ":1965".
Addr string
- // CertificateStore contains the certificates used by the server.
- CertificateStore CertificateStore
+ // Certificates contains the certificates used by the server.
+ Certificates CertificateStore
- // GetCertificate, if not nil, will be called to retrieve the certificate
- // to use for a given hostname.
- // If the certificate is nil, the connection will be aborted.
- GetCertificate func(hostname string, store *CertificateStore) *tls.Certificate
+ // CreateCertificate, if not nil, will be called to create a new certificate
+ // if the current one is expired or missing.
+ CreateCertificate func(hostname string) (tls.Certificate, error)
// registered responders
responders map[responderKey]Responder
@@ -69,6 +65,9 @@ func (s *Server) Register(pattern string, responder Responder) {
key.wildcard = true
}
+ if _, ok := s.responders[key]; ok {
+ panic("gemini: multiple registrations for " + pattern)
+ }
s.responders[key] = responder
}
@@ -90,18 +89,11 @@ func (s *Server) ListenAndServe() error {
}
defer ln.Close()
- config := &tls.Config{
- ClientAuth: tls.RequestClientCert,
- MinVersion: tls.VersionTLS12,
- GetCertificate: func(h *tls.ClientHelloInfo) (*tls.Certificate, error) {
- if s.GetCertificate != nil {
- return s.GetCertificate(h.ServerName, &s.CertificateStore), nil
- }
- return s.CertificateStore.Lookup(h.ServerName)
- },
- }
- tlsListener := tls.NewListener(ln, config)
- return s.Serve(tlsListener)
+ return s.Serve(tls.NewListener(ln, &tls.Config{
+ ClientAuth: tls.RequestClientCert,
+ MinVersion: tls.VersionTLS12,
+ GetCertificate: s.getCertificate,
+ }))
}
// Serve listens for requests on the provided listener.
@@ -135,6 +127,21 @@ func (s *Server) Serve(l net.Listener) error {
}
}
+func (s *Server) getCertificate(h *tls.ClientHelloInfo) (*tls.Certificate, error) {
+ cert, err := s.Certificates.Lookup(h.ServerName)
+ switch err {
+ case ErrCertificateExpired, ErrCertificateUnknown:
+ if s.CreateCertificate != nil {
+ cert, err := s.CreateCertificate(h.ServerName)
+ if err == nil {
+ s.Certificates.Add(h.ServerName, cert)
+ }
+ return &cert, err
+ }
+ }
+ return cert, err
+}
+
// respond responds to a connection.
func (s *Server) respond(conn net.Conn) {
r := bufio.NewReader(conn)
@@ -317,204 +324,3 @@ type ResponderFunc func(*ResponseWriter, *Request)
func (f ResponderFunc) Respond(w *ResponseWriter, r *Request) {
f(w, r)
}
-
-// The following code is modified from the net/http package.
-
-// Copyright 2009 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// ServeMux is a Gemini request multiplexer.
-// It matches the URL of each incoming request against a list of registered
-// patterns and calls the handler for the pattern that
-// most closely matches the URL.
-//
-// Patterns name fixed, rooted paths, like "/favicon.ico",
-// or rooted subtrees, like "/images/" (note the trailing slash).
-// Longer patterns take precedence over shorter ones, so that
-// if there are handlers registered for both "/images/"
-// and "/images/thumbnails/", the latter handler will be
-// called for paths beginning "/images/thumbnails/" and the
-// former will receive requests for any other paths in the
-// "/images/" subtree.
-//
-// Note that since a pattern ending in a slash names a rooted subtree,
-// the pattern "/" matches all paths not matched by other registered
-// patterns, not just the URL with Path == "/".
-//
-// If a subtree has been registered and a request is received naming the
-// subtree root without its trailing slash, ServeMux redirects that
-// request to the subtree root (adding the trailing slash). This behavior can
-// be overridden with a separate registration for the path without
-// the trailing slash. For example, registering "/images/" causes ServeMux
-// to redirect a request for "/images" to "/images/", unless "/images" has
-// been registered separately.
-//
-// ServeMux also takes care of sanitizing the URL request path and
-// redirecting any request containing . or .. elements or repeated slashes
-// to an equivalent, cleaner URL.
-type ServeMux struct {
- mu sync.RWMutex
- m map[string]muxEntry
- es []muxEntry // slice of entries sorted from longest to shortest.
-}
-
-type muxEntry struct {
- r Responder
- pattern string
-}
-
-// cleanPath returns the canonical path for p, eliminating . and .. elements.
-func cleanPath(p string) string {
- if p == "" {
- return "/"
- }
- if p[0] != '/' {
- p = "/" + p
- }
- np := path.Clean(p)
- // path.Clean removes trailing slash except for root;
- // put the trailing slash back if necessary.
- if p[len(p)-1] == '/' && np != "/" {
- // Fast path for common case of p being the string we want:
- if len(p) == len(np)+1 && strings.HasPrefix(p, np) {
- np = p
- } else {
- np += "/"
- }
- }
- return np
-}
-
-// Find a handler on a handler map given a path string.
-// Most-specific (longest) pattern wins.
-func (mux *ServeMux) match(path string) Responder {
- // Check for exact match first.
- v, ok := mux.m[path]
- if ok {
- 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.r
- }
- }
- return nil
-}
-
-// redirectToPathSlash determines if the given path needs appending "/" to it.
-// This occurs when a handler for path + "/" was already registered, but
-// not for path itself. If the path needs appending to, it creates a new
-// URL, setting the path to u.Path + "/" and returning true to indicate so.
-func (mux *ServeMux) redirectToPathSlash(path string, u *url.URL) (*url.URL, bool) {
- mux.mu.RLock()
- shouldRedirect := mux.shouldRedirectRLocked(path)
- mux.mu.RUnlock()
- if !shouldRedirect {
- return u, false
- }
- path = path + "/"
- u = &url.URL{Path: path, RawQuery: u.RawQuery}
- return u, true
-}
-
-// shouldRedirectRLocked reports whether the given path and host should be redirected to
-// path+"/". This should happen if a handler is registered for path+"/" but
-// not path -- see comments at ServeMux.
-func (mux *ServeMux) shouldRedirectRLocked(path string) bool {
- if _, exist := mux.m[path]; exist {
- return false
- }
-
- n := len(path)
- if n == 0 {
- return false
- }
- if _, exist := mux.m[path+"/"]; exist {
- return path[n-1] != '/'
- }
-
- return false
-}
-
-// 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 {
- Redirect(w, u.String())
- return
- }
-
- if path != r.URL.Path {
- u := *r.URL
- u.Path = path
- Redirect(w, u.String())
- return
- }
-
- mux.mu.RLock()
- defer mux.mu.RUnlock()
-
- resp := mux.match(path)
- if resp == nil {
- w.WriteStatus(StatusNotFound)
- return
- }
- resp.Respond(w, r)
-}
-
-// 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("gemini: invalid pattern")
- }
- if responder == nil {
- panic("gemini: nil responder")
- }
- if _, exist := mux.m[pattern]; exist {
- panic("gemini: multiple registrations for " + pattern)
- }
-
- if mux.m == nil {
- mux.m = make(map[string]muxEntry)
- }
- e := muxEntry{responder, pattern}
- mux.m[pattern] = e
- if pattern[len(pattern)-1] == '/' {
- mux.es = appendSorted(mux.es, e)
- }
-}
-
-func appendSorted(es []muxEntry, e muxEntry) []muxEntry {
- n := len(es)
- i := sort.Search(n, func(i int) bool {
- return len(es[i].pattern) < len(e.pattern)
- })
- if i == n {
- return append(es, e)
- }
- // we now know that i points at where we want to insert
- es = append(es, muxEntry{}) // try to grow the slice in place, any entry works.
- copy(es[i+1:], es[i:]) // move shorter entries down
- es[i] = e
- return es
-}
-
-// HandleFunc registers the responder function for the given pattern.
-func (mux *ServeMux) HandleFunc(pattern string, responder func(*ResponseWriter, *Request)) {
- if responder == nil {
- panic("gemini: nil responder")
- }
- mux.Handle(pattern, ResponderFunc(responder))
-}