Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
520d0a7fb1 | ||
|
|
bf185e4091 | ||
|
|
8101fbe473 | ||
|
|
b76080c863 | ||
|
|
53390dad6b | ||
|
|
cec1f118fb | ||
|
|
95716296b4 | ||
|
|
1490bf6a75 | ||
|
|
610c6fc533 | ||
|
|
01670647d2 | ||
|
|
5b3194695f |
76
cert.go
76
cert.go
@@ -2,12 +2,14 @@ package gemini
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
|
"crypto/elliptic"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"log"
|
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
@@ -26,7 +28,7 @@ type CertificateStore struct {
|
|||||||
|
|
||||||
// Add adds a certificate for the given scope to the store.
|
// Add adds a certificate for the given scope to the store.
|
||||||
// It tries to parse the certificate if it is not already parsed.
|
// It tries to parse the certificate if it is not already parsed.
|
||||||
func (c *CertificateStore) Add(scope string, cert tls.Certificate) {
|
func (c *CertificateStore) Add(scope string, cert tls.Certificate) error {
|
||||||
if c.store == nil {
|
if c.store == nil {
|
||||||
c.store = map[string]tls.Certificate{}
|
c.store = map[string]tls.Certificate{}
|
||||||
}
|
}
|
||||||
@@ -39,27 +41,20 @@ func (c *CertificateStore) Add(scope string, cert tls.Certificate) {
|
|||||||
}
|
}
|
||||||
if c.dir {
|
if c.dir {
|
||||||
// Write certificates
|
// Write certificates
|
||||||
log.Printf("gemini: Writing certificate for %s to %s", scope, c.path)
|
|
||||||
certPath := filepath.Join(c.path, scope+".crt")
|
certPath := filepath.Join(c.path, scope+".crt")
|
||||||
keyPath := filepath.Join(c.path, scope+".key")
|
keyPath := filepath.Join(c.path, scope+".key")
|
||||||
if err := WriteCertificate(cert, certPath, keyPath); err != nil {
|
if err := WriteCertificate(cert, certPath, keyPath); err != nil {
|
||||||
log.Printf("gemini: Failed to write certificate for %s: %s", scope, err)
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.store[scope] = cert
|
c.store[scope] = cert
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lookup returns the certificate for the given scope.
|
// Lookup returns the certificate for the given scope.
|
||||||
func (c *CertificateStore) Lookup(scope string) (*tls.Certificate, error) {
|
func (c *CertificateStore) Lookup(scope string) (tls.Certificate, bool) {
|
||||||
cert, ok := c.store[scope]
|
cert, ok := c.store[scope]
|
||||||
if !ok {
|
return cert, ok
|
||||||
return nil, ErrCertificateNotFound
|
|
||||||
}
|
|
||||||
// Ensure that the certificate is not expired
|
|
||||||
if cert.Leaf != nil && cert.Leaf.NotAfter.Before(time.Now()) {
|
|
||||||
return &cert, ErrCertificateExpired
|
|
||||||
}
|
|
||||||
return &cert, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load loads certificates from the given path.
|
// Load loads certificates from the given path.
|
||||||
@@ -87,11 +82,31 @@ func (c *CertificateStore) Load(path string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CertificateOptions configures how a certificate is created.
|
// CertificateOptions configures the creation of a certificate.
|
||||||
type CertificateOptions struct {
|
type CertificateOptions struct {
|
||||||
|
// Subject Alternate Name values.
|
||||||
|
// Should contain the IP addresses that the certificate is valid for.
|
||||||
IPAddresses []net.IP
|
IPAddresses []net.IP
|
||||||
DNSNames []string
|
|
||||||
Duration time.Duration
|
// Subject Alternate Name values.
|
||||||
|
// Should contain the DNS names that this certificate is valid for.
|
||||||
|
// E.g. example.com, *.example.com
|
||||||
|
DNSNames []string
|
||||||
|
|
||||||
|
// Subject specifies the certificate Subject.
|
||||||
|
//
|
||||||
|
// Subject.CommonName can contain the DNS name that this certificate
|
||||||
|
// is valid for. Server certificates should specify both a Subject
|
||||||
|
// and a Subject Alternate Name.
|
||||||
|
Subject pkix.Name
|
||||||
|
|
||||||
|
// Duration specifies the amount of time that the certificate is valid for.
|
||||||
|
Duration time.Duration
|
||||||
|
|
||||||
|
// Ed25519 specifies whether to generate an Ed25519 key pair.
|
||||||
|
// If false, an ECDSA key will be generated instead.
|
||||||
|
// Ed25519 is not as widely supported as ECDSA.
|
||||||
|
Ed25519 bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateCertificate creates a new TLS certificate.
|
// CreateCertificate creates a new TLS certificate.
|
||||||
@@ -109,15 +124,27 @@ func CreateCertificate(options CertificateOptions) (tls.Certificate, error) {
|
|||||||
|
|
||||||
// newX509KeyPair creates and returns a new certificate and private key.
|
// newX509KeyPair creates and returns a new certificate and private key.
|
||||||
func newX509KeyPair(options CertificateOptions) (*x509.Certificate, crypto.PrivateKey, error) {
|
func newX509KeyPair(options CertificateOptions) (*x509.Certificate, crypto.PrivateKey, error) {
|
||||||
// Generate an ED25519 private key
|
var pub crypto.PublicKey
|
||||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
var priv crypto.PrivateKey
|
||||||
if err != nil {
|
if options.Ed25519 {
|
||||||
return nil, nil, err
|
// Generate an Ed25519 private key
|
||||||
|
var err error
|
||||||
|
pub, priv, err = ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Generate an ECDSA private key
|
||||||
|
private, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
priv = private
|
||||||
|
pub = &private.PublicKey
|
||||||
}
|
}
|
||||||
public := priv.Public()
|
|
||||||
|
|
||||||
// ED25519 keys should have the DigitalSignature KeyUsage bits set
|
// ECDSA and Ed25519 keys should have the DigitalSignature KeyUsage bits
|
||||||
// in the x509.Certificate template
|
// set in the x509.Certificate template
|
||||||
keyUsage := x509.KeyUsageDigitalSignature
|
keyUsage := x509.KeyUsageDigitalSignature
|
||||||
|
|
||||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||||
@@ -138,9 +165,10 @@ func newX509KeyPair(options CertificateOptions) (*x509.Certificate, crypto.Priva
|
|||||||
BasicConstraintsValid: true,
|
BasicConstraintsValid: true,
|
||||||
IPAddresses: options.IPAddresses,
|
IPAddresses: options.IPAddresses,
|
||||||
DNSNames: options.DNSNames,
|
DNSNames: options.DNSNames,
|
||||||
|
Subject: options.Subject,
|
||||||
}
|
}
|
||||||
|
|
||||||
crt, err := x509.CreateCertificate(rand.Reader, &template, &template, public, priv)
|
crt, err := x509.CreateCertificate(rand.Reader, &template, &template, pub, priv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
72
client.go
72
client.go
@@ -4,6 +4,7 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
@@ -39,8 +40,7 @@ type Client struct {
|
|||||||
GetInput func(prompt string, sensitive bool) (input string, ok bool)
|
GetInput func(prompt string, sensitive bool) (input string, ok bool)
|
||||||
|
|
||||||
// CheckRedirect determines whether to follow a redirect.
|
// CheckRedirect determines whether to follow a redirect.
|
||||||
// If CheckRedirect is nil, a default policy of no more than 5 consecutive
|
// If CheckRedirect is nil, redirects will not be followed.
|
||||||
// redirects will be enforced.
|
|
||||||
CheckRedirect func(req *Request, via []*Request) error
|
CheckRedirect func(req *Request, via []*Request) error
|
||||||
|
|
||||||
// CreateCertificate is called to generate a certificate upon
|
// CreateCertificate is called to generate a certificate upon
|
||||||
@@ -125,9 +125,10 @@ func (c *Client) do(req *Request, via []*Request) (*Response, error) {
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
c.Certificates.Add(hostname+path, cert)
|
c.Certificates.Add(hostname+path, cert)
|
||||||
|
req.Certificate = &cert
|
||||||
return c.do(req, via)
|
return c.do(req, via)
|
||||||
}
|
}
|
||||||
return resp, ErrCertificateRequired
|
return resp, nil
|
||||||
|
|
||||||
case resp.Status.Class() == StatusClassInput:
|
case resp.Status.Class() == StatusClassInput:
|
||||||
if c.GetInput != nil {
|
if c.GetInput != nil {
|
||||||
@@ -138,7 +139,7 @@ func (c *Client) do(req *Request, via []*Request) (*Response, error) {
|
|||||||
return c.do(req, via)
|
return c.do(req, via)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return resp, ErrInputRequired
|
return resp, nil
|
||||||
|
|
||||||
case resp.Status.Class() == StatusClassRedirect:
|
case resp.Status.Class() == StatusClassRedirect:
|
||||||
if via == nil {
|
if via == nil {
|
||||||
@@ -150,21 +151,19 @@ func (c *Client) do(req *Request, via []*Request) (*Response, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
target = req.URL.ResolveReference(target)
|
target = req.URL.ResolveReference(target)
|
||||||
redirect, err := NewRequestFromURL(target)
|
if target.Scheme != "" && target.Scheme != "gemini" {
|
||||||
if err != nil {
|
return resp, nil
|
||||||
return resp, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
redirect := NewRequestFromURL(target)
|
||||||
if c.CheckRedirect != nil {
|
if c.CheckRedirect != nil {
|
||||||
if err := c.CheckRedirect(redirect, via); err != nil {
|
if err := c.CheckRedirect(redirect, via); err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
} else if len(via) > 5 {
|
return c.do(redirect, via)
|
||||||
// Default policy of no more than 5 redirects
|
|
||||||
return resp, ErrTooManyRedirects
|
|
||||||
}
|
}
|
||||||
return c.do(redirect, via)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.Request = req
|
resp.Request = req
|
||||||
@@ -180,13 +179,14 @@ func (c *Client) getClientCertificate(req *Request) (*tls.Certificate, error) {
|
|||||||
// Search recursively for the certificate
|
// Search recursively for the certificate
|
||||||
scope := req.URL.Hostname() + strings.TrimSuffix(req.URL.Path, "/")
|
scope := req.URL.Hostname() + strings.TrimSuffix(req.URL.Path, "/")
|
||||||
for {
|
for {
|
||||||
cert, err := c.Certificates.Lookup(scope)
|
cert, ok := c.Certificates.Lookup(scope)
|
||||||
if err == nil {
|
if ok {
|
||||||
// Store the certificate
|
// Ensure that the certificate is not expired
|
||||||
req.Certificate = cert
|
if cert.Leaf != nil && !time.Now().After(cert.Leaf.NotAfter) {
|
||||||
return cert, err
|
// Store the certificate
|
||||||
}
|
req.Certificate = &cert
|
||||||
if err == ErrCertificateExpired {
|
return &cert, nil
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
scope = path.Dir(scope)
|
scope = path.Dir(scope)
|
||||||
@@ -214,21 +214,27 @@ func (c *Client) verifyConnection(req *Request, cs tls.ConnectionState) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Check the known hosts
|
// Check the known hosts
|
||||||
err := c.KnownHosts.Lookup(hostname, cert)
|
knownHost, ok := c.KnownHosts.Lookup(hostname)
|
||||||
switch err {
|
if ok && time.Now().After(cert.NotAfter) {
|
||||||
case ErrCertificateExpired, ErrCertificateNotFound:
|
// Not expired
|
||||||
// See if the client trusts the certificate
|
fingerprint := NewFingerprint(cert)
|
||||||
if c.TrustCertificate != nil {
|
if knownHost.Hex != fingerprint.Hex {
|
||||||
switch c.TrustCertificate(hostname, cert) {
|
return errors.New("gemini: fingerprint does not match")
|
||||||
case TrustOnce:
|
|
||||||
c.KnownHosts.AddTemporary(hostname, cert)
|
|
||||||
return nil
|
|
||||||
case TrustAlways:
|
|
||||||
c.KnownHosts.Add(hostname, cert)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ErrCertificateNotTrusted
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
|
||||||
|
// Unknown certificate
|
||||||
|
// See if the client trusts the certificate
|
||||||
|
if c.TrustCertificate != nil {
|
||||||
|
switch c.TrustCertificate(hostname, cert) {
|
||||||
|
case TrustOnce:
|
||||||
|
c.KnownHosts.AddTemporary(hostname, cert)
|
||||||
|
return nil
|
||||||
|
case TrustAlways:
|
||||||
|
c.KnownHosts.Add(hostname, cert)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.New("gemini: certificate not trusted")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
@@ -48,6 +49,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
server.CreateCertificate = func(hostname string) (tls.Certificate, error) {
|
server.CreateCertificate = func(hostname string) (tls.Certificate, error) {
|
||||||
return gemini.CreateCertificate(gemini.CertificateOptions{
|
return gemini.CreateCertificate(gemini.CertificateOptions{
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: hostname,
|
||||||
|
},
|
||||||
DNSNames: []string{hostname},
|
DNSNames: []string{hostname},
|
||||||
Duration: time.Hour,
|
Duration: time.Hour,
|
||||||
})
|
})
|
||||||
@@ -60,8 +64,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getSession(cert *x509.Certificate) (*session, bool) {
|
func getSession(cert *x509.Certificate) (*session, bool) {
|
||||||
fingerprint := gemini.Fingerprint(cert)
|
fingerprint := gemini.NewFingerprint(cert)
|
||||||
session, ok := sessions[fingerprint]
|
session, ok := sessions[fingerprint.Hex]
|
||||||
return session, ok
|
return session, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,8 +79,8 @@ func login(w *gemini.ResponseWriter, r *gemini.Request) {
|
|||||||
w.WriteHeader(gemini.StatusInput, "Username")
|
w.WriteHeader(gemini.StatusInput, "Username")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fingerprint := gemini.Fingerprint(r.Certificate.Leaf)
|
fingerprint := gemini.NewFingerprint(r.Certificate.Leaf)
|
||||||
sessions[fingerprint] = &session{
|
sessions[fingerprint.Hex] = &session{
|
||||||
username: username,
|
username: username,
|
||||||
}
|
}
|
||||||
w.WriteHeader(gemini.StatusRedirect, "/password")
|
w.WriteHeader(gemini.StatusRedirect, "/password")
|
||||||
@@ -103,7 +107,7 @@ func loginPassword(w *gemini.ResponseWriter, r *gemini.Request) {
|
|||||||
session.authorized = true
|
session.authorized = true
|
||||||
w.WriteHeader(gemini.StatusRedirect, "/profile")
|
w.WriteHeader(gemini.StatusRedirect, "/profile")
|
||||||
} else {
|
} else {
|
||||||
w.WriteHeader(gemini.StatusSensitiveInput, "Wrong password. Try again")
|
w.WriteHeader(gemini.StatusSensitiveInput, "Password")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,9 +116,10 @@ func logout(w *gemini.ResponseWriter, r *gemini.Request) {
|
|||||||
w.WriteStatus(gemini.StatusCertificateRequired)
|
w.WriteStatus(gemini.StatusCertificateRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fingerprint := gemini.Fingerprint(r.Certificate.Leaf)
|
fingerprint := gemini.NewFingerprint(r.Certificate.Leaf)
|
||||||
delete(sessions, fingerprint)
|
delete(sessions, fingerprint.Hex)
|
||||||
fmt.Fprintln(w, "Successfully logged out.")
|
fmt.Fprintln(w, "Successfully logged out.")
|
||||||
|
fmt.Fprintln(w, "=> / Index")
|
||||||
}
|
}
|
||||||
|
|
||||||
func profile(w *gemini.ResponseWriter, r *gemini.Request) {
|
func profile(w *gemini.ResponseWriter, r *gemini.Request) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/x509/pkix"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
@@ -22,6 +23,9 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
options := gemini.CertificateOptions{
|
options := gemini.CertificateOptions{
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: host,
|
||||||
|
},
|
||||||
DNSNames: []string{host},
|
DNSNames: []string{host},
|
||||||
Duration: duration,
|
Duration: duration,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ func init() {
|
|||||||
client.Timeout = 30 * time.Second
|
client.Timeout = 30 * time.Second
|
||||||
client.KnownHosts.LoadDefault()
|
client.KnownHosts.LoadDefault()
|
||||||
client.TrustCertificate = func(hostname string, cert *x509.Certificate) gemini.Trust {
|
client.TrustCertificate = func(hostname string, cert *x509.Certificate) gemini.Trust {
|
||||||
fmt.Printf(trustPrompt, hostname, gemini.Fingerprint(cert))
|
fingerprint := gemini.NewFingerprint(cert)
|
||||||
|
fmt.Printf(trustPrompt, hostname, fingerprint.Hex)
|
||||||
scanner.Scan()
|
scanner.Scan()
|
||||||
switch scanner.Text() {
|
switch scanner.Text() {
|
||||||
case "t":
|
case "t":
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"crypto/x509/pkix"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -19,6 +20,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
server.CreateCertificate = func(hostname string) (tls.Certificate, error) {
|
server.CreateCertificate = func(hostname string) (tls.Certificate, error) {
|
||||||
return gemini.CreateCertificate(gemini.CertificateOptions{
|
return gemini.CreateCertificate(gemini.CertificateOptions{
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: hostname,
|
||||||
|
},
|
||||||
DNSNames: []string{hostname},
|
DNSNames: []string{hostname},
|
||||||
Duration: time.Minute, // for testing purposes
|
Duration: time.Minute, // for testing purposes
|
||||||
})
|
})
|
||||||
|
|||||||
4
fs.go
4
fs.go
@@ -96,9 +96,9 @@ func openFile(p string) (File, error) {
|
|||||||
if stat.Mode().IsRegular() {
|
if stat.Mode().IsRegular() {
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
return nil, ErrNotAFile
|
return nil, os.ErrNotExist
|
||||||
} else if !stat.Mode().IsRegular() {
|
} else if !stat.Mode().IsRegular() {
|
||||||
return nil, ErrNotAFile
|
return nil, os.ErrNotExist
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return f, nil
|
return f, nil
|
||||||
|
|||||||
14
gemini.go
14
gemini.go
@@ -9,17 +9,9 @@ var crlf = []byte("\r\n")
|
|||||||
|
|
||||||
// Errors.
|
// Errors.
|
||||||
var (
|
var (
|
||||||
ErrInvalidURL = errors.New("gemini: invalid URL")
|
ErrInvalidURL = errors.New("gemini: invalid URL")
|
||||||
ErrInvalidResponse = errors.New("gemini: invalid response")
|
ErrInvalidResponse = errors.New("gemini: invalid response")
|
||||||
ErrCertificateExpired = errors.New("gemini: certificate expired")
|
ErrBodyNotAllowed = errors.New("gemini: response body not allowed")
|
||||||
ErrCertificateNotFound = errors.New("gemini: certificate not found")
|
|
||||||
ErrCertificateNotTrusted = errors.New("gemini: certificate not trusted")
|
|
||||||
ErrCertificateRequired = errors.New("gemini: certificate required")
|
|
||||||
ErrNotAFile = errors.New("gemini: not a file")
|
|
||||||
ErrNotAGeminiURL = errors.New("gemini: not a Gemini URL")
|
|
||||||
ErrBodyNotAllowed = errors.New("gemini: response status code does not allow for body")
|
|
||||||
ErrTooManyRedirects = errors.New("gemini: too many redirects")
|
|
||||||
ErrInputRequired = errors.New("gemini: input required")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// defaultClient is the default client. It is used by Get and Do.
|
// defaultClient is the default client. It is used by Get and Do.
|
||||||
|
|||||||
@@ -41,15 +41,12 @@ func NewRequest(rawurl string) (*Request, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return NewRequestFromURL(u)
|
return NewRequestFromURL(u), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRequestFromURL returns a new request for the given URL.
|
// NewRequestFromURL returns a new request for the given URL.
|
||||||
// The host is inferred from the URL.
|
// The host is inferred from the URL.
|
||||||
func NewRequestFromURL(url *url.URL) (*Request, error) {
|
func NewRequestFromURL(url *url.URL) *Request {
|
||||||
if url.Scheme != "" && url.Scheme != "gemini" {
|
|
||||||
return nil, ErrNotAGeminiURL
|
|
||||||
}
|
|
||||||
host := url.Host
|
host := url.Host
|
||||||
if url.Port() == "" {
|
if url.Port() == "" {
|
||||||
host += ":1965"
|
host += ":1965"
|
||||||
@@ -57,7 +54,7 @@ func NewRequestFromURL(url *url.URL) (*Request, error) {
|
|||||||
return &Request{
|
return &Request{
|
||||||
URL: url,
|
URL: url,
|
||||||
Host: host,
|
Host: host,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// write writes the Gemini request to the provided buffered writer.
|
// write writes the Gemini request to the provided buffered writer.
|
||||||
|
|||||||
32
server.go
32
server.go
@@ -3,6 +3,7 @@ package gemini
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -31,6 +32,11 @@ type Server struct {
|
|||||||
// if the current one is expired or missing.
|
// if the current one is expired or missing.
|
||||||
CreateCertificate func(hostname string) (tls.Certificate, error)
|
CreateCertificate func(hostname string) (tls.Certificate, error)
|
||||||
|
|
||||||
|
// ErrorLog specifies an optional logger for errors accepting connections
|
||||||
|
// and file system errors.
|
||||||
|
// If nil, logging is done via the log package's standard logger.
|
||||||
|
ErrorLog *log.Logger
|
||||||
|
|
||||||
// registered responders
|
// registered responders
|
||||||
responders map[responderKey]Responder
|
responders map[responderKey]Responder
|
||||||
hosts map[string]bool
|
hosts map[string]bool
|
||||||
@@ -117,7 +123,7 @@ func (s *Server) Serve(l net.Listener) error {
|
|||||||
if max := 1 * time.Second; tempDelay > max {
|
if max := 1 * time.Second; tempDelay > max {
|
||||||
tempDelay = max
|
tempDelay = max
|
||||||
}
|
}
|
||||||
log.Printf("gemini: Accept error: %v; retrying in %v", err, tempDelay)
|
s.logf("gemini: Accept error: %v; retrying in %v", err, tempDelay)
|
||||||
time.Sleep(tempDelay)
|
time.Sleep(tempDelay)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -145,22 +151,24 @@ func (s *Server) getCertificate(h *tls.ClientHelloInfo) (*tls.Certificate, error
|
|||||||
|
|
||||||
func (s *Server) getCertificateFor(hostname string) (*tls.Certificate, error) {
|
func (s *Server) getCertificateFor(hostname string) (*tls.Certificate, error) {
|
||||||
if _, ok := s.hosts[hostname]; !ok {
|
if _, ok := s.hosts[hostname]; !ok {
|
||||||
return nil, ErrCertificateNotFound
|
return nil, errors.New("hostname not registered")
|
||||||
}
|
}
|
||||||
cert, err := s.Certificates.Lookup(hostname)
|
|
||||||
|
|
||||||
switch err {
|
// Generate a new certificate if it is missing or expired
|
||||||
case ErrCertificateNotFound, ErrCertificateExpired:
|
cert, ok := s.Certificates.Lookup(hostname)
|
||||||
|
if !ok || cert.Leaf != nil && !time.Now().After(cert.Leaf.NotAfter) {
|
||||||
if s.CreateCertificate != nil {
|
if s.CreateCertificate != nil {
|
||||||
cert, err := s.CreateCertificate(hostname)
|
cert, err := s.CreateCertificate(hostname)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
s.Certificates.Add(hostname, cert)
|
if err := s.Certificates.Add(hostname, cert); err != nil {
|
||||||
|
s.logf("gemini: Failed to add new certificate for %s: %s", hostname, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &cert, err
|
return &cert, err
|
||||||
}
|
}
|
||||||
|
return nil, errors.New("no certificate")
|
||||||
}
|
}
|
||||||
|
return &cert, nil
|
||||||
return cert, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// respond responds to a connection.
|
// respond responds to a connection.
|
||||||
@@ -241,6 +249,14 @@ func (s *Server) responder(r *Request) Responder {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) logf(format string, args ...interface{}) {
|
||||||
|
if s.ErrorLog != nil {
|
||||||
|
s.ErrorLog.Printf(format, args...)
|
||||||
|
} else {
|
||||||
|
log.Printf(format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ResponseWriter is used by a Gemini handler to construct a Gemini response.
|
// ResponseWriter is used by a Gemini handler to construct a Gemini response.
|
||||||
type ResponseWriter struct {
|
type ResponseWriter struct {
|
||||||
b *bufio.Writer
|
b *bufio.Writer
|
||||||
|
|||||||
77
tofu.go
77
tofu.go
@@ -8,9 +8,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Trust represents the trustworthiness of a certificate.
|
// Trust represents the trustworthiness of a certificate.
|
||||||
@@ -25,7 +23,7 @@ const (
|
|||||||
// KnownHosts represents a list of known hosts.
|
// KnownHosts represents a list of known hosts.
|
||||||
// The zero value for KnownHosts is an empty list ready to use.
|
// The zero value for KnownHosts is an empty list ready to use.
|
||||||
type KnownHosts struct {
|
type KnownHosts struct {
|
||||||
hosts map[string]certInfo
|
hosts map[string]Fingerprint
|
||||||
file *os.File
|
file *os.File
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,53 +78,34 @@ func (k *KnownHosts) AddTemporary(hostname string, cert *x509.Certificate) {
|
|||||||
|
|
||||||
func (k *KnownHosts) add(hostname string, cert *x509.Certificate, write bool) {
|
func (k *KnownHosts) add(hostname string, cert *x509.Certificate, write bool) {
|
||||||
if k.hosts == nil {
|
if k.hosts == nil {
|
||||||
k.hosts = map[string]certInfo{}
|
k.hosts = map[string]Fingerprint{}
|
||||||
}
|
}
|
||||||
info := certInfo{
|
fingerprint := NewFingerprint(cert)
|
||||||
Algorithm: "SHA-512",
|
k.hosts[hostname] = fingerprint
|
||||||
Fingerprint: Fingerprint(cert),
|
|
||||||
Expires: cert.NotAfter.Unix(),
|
|
||||||
}
|
|
||||||
k.hosts[hostname] = info
|
|
||||||
// Append to the file
|
// Append to the file
|
||||||
if write && k.file != nil {
|
if write && k.file != nil {
|
||||||
appendKnownHost(k.file, hostname, info)
|
appendKnownHost(k.file, hostname, fingerprint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lookup looks for the provided certificate in the list of known hosts.
|
// Lookup returns the fingerprint of the certificate corresponding to
|
||||||
// If the hostname is not in the list, Lookup returns ErrCertificateNotFound.
|
// the given hostname.
|
||||||
// If the fingerprint doesn't match, Lookup returns ErrCertificateNotTrusted.
|
func (k *KnownHosts) Lookup(hostname string) (Fingerprint, bool) {
|
||||||
// Otherwise, Lookup returns nil.
|
c, ok := k.hosts[hostname]
|
||||||
func (k *KnownHosts) Lookup(hostname string, cert *x509.Certificate) error {
|
return c, ok
|
||||||
now := time.Now().Unix()
|
|
||||||
fingerprint := Fingerprint(cert)
|
|
||||||
if c, ok := k.hosts[hostname]; ok {
|
|
||||||
if c.Expires <= now {
|
|
||||||
// Certificate is expired
|
|
||||||
return ErrCertificateExpired
|
|
||||||
}
|
|
||||||
if c.Fingerprint != fingerprint {
|
|
||||||
// Fingerprint does not match
|
|
||||||
return ErrCertificateNotTrusted
|
|
||||||
}
|
|
||||||
// Certificate is found
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return ErrCertificateNotFound
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse parses the provided reader and adds the parsed known hosts to the list.
|
// Parse parses the provided reader and adds the parsed known hosts to the list.
|
||||||
// Invalid lines are ignored.
|
// Invalid lines are ignored.
|
||||||
func (k *KnownHosts) Parse(r io.Reader) {
|
func (k *KnownHosts) Parse(r io.Reader) {
|
||||||
if k.hosts == nil {
|
if k.hosts == nil {
|
||||||
k.hosts = map[string]certInfo{}
|
k.hosts = map[string]Fingerprint{}
|
||||||
}
|
}
|
||||||
scanner := bufio.NewScanner(r)
|
scanner := bufio.NewScanner(r)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
text := scanner.Text()
|
text := scanner.Text()
|
||||||
parts := strings.Split(text, " ")
|
parts := strings.Split(text, " ")
|
||||||
if len(parts) < 4 {
|
if len(parts) < 3 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,15 +115,10 @@ func (k *KnownHosts) Parse(r io.Reader) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fingerprint := parts[2]
|
fingerprint := parts[2]
|
||||||
expires, err := strconv.ParseInt(parts[3], 10, 0)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
k.hosts[hostname] = certInfo{
|
k.hosts[hostname] = Fingerprint{
|
||||||
Algorithm: algorithm,
|
Algorithm: algorithm,
|
||||||
Fingerprint: fingerprint,
|
Hex: fingerprint,
|
||||||
Expires: expires,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,18 +130,18 @@ func (k *KnownHosts) Write(w io.Writer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type certInfo struct {
|
func appendKnownHost(w io.Writer, hostname string, f Fingerprint) (int, error) {
|
||||||
Algorithm string // fingerprint algorithm e.g. SHA-512
|
return fmt.Fprintf(w, "%s %s %s\n", hostname, f.Algorithm, f.Hex)
|
||||||
Fingerprint string // fingerprint in hexadecimal, with ':' between each octet
|
|
||||||
Expires int64 // unix time of certificate notAfter date
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendKnownHost(w io.Writer, hostname string, c certInfo) (int, error) {
|
// Fingerprint represents a fingerprint using a certain algorithm.
|
||||||
return fmt.Fprintf(w, "%s %s %s %d\n", hostname, c.Algorithm, c.Fingerprint, c.Expires)
|
type Fingerprint struct {
|
||||||
|
Algorithm string // fingerprint algorithm e.g. SHA-512
|
||||||
|
Hex string // fingerprint in hexadecimal, with ':' between each octet
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fingerprint returns the SHA-512 fingerprint of the provided certificate.
|
// NewFingerprint returns the SHA-512 fingerprint of the provided certificate.
|
||||||
func Fingerprint(cert *x509.Certificate) string {
|
func NewFingerprint(cert *x509.Certificate) Fingerprint {
|
||||||
sum512 := sha512.Sum512(cert.Raw)
|
sum512 := sha512.Sum512(cert.Raw)
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
for i, f := range sum512 {
|
for i, f := range sum512 {
|
||||||
@@ -176,7 +150,10 @@ func Fingerprint(cert *x509.Certificate) string {
|
|||||||
}
|
}
|
||||||
fmt.Fprintf(&b, "%02X", f)
|
fmt.Fprintf(&b, "%02X", f)
|
||||||
}
|
}
|
||||||
return b.String()
|
return Fingerprint{
|
||||||
|
Algorithm: "SHA-512",
|
||||||
|
Hex: b.String(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultKnownHostsPath returns the default known_hosts path.
|
// defaultKnownHostsPath returns the default known_hosts path.
|
||||||
|
|||||||
Reference in New Issue
Block a user