12 Commits

Author SHA1 Message Date
Adnan Maolood
f6b0443a62 Update KnownHosts documentation 2020-11-09 13:57:30 -05:00
Adnan Maolood
3dee6dcff3 Add (*CertificateStore).Write function 2020-11-09 13:54:15 -05:00
Adnan Maolood
85f8e84bd5 Rename (*ResponseWriter).SetMimetype to SetMediaType 2020-11-09 13:44:42 -05:00
Adnan Maolood
9338681256 Add (*KnownHosts).SetOutput function 2020-11-09 12:26:08 -05:00
Adnan Maolood
f2a1510375 Move documentation to gemini.go 2020-11-09 12:07:49 -05:00
Adnan Maolood
46cbcfcaa4 Remove top-level Get and Do functions 2020-11-09 12:04:53 -05:00
Adnan Maolood
76dfe257f1 Remove (*KnownHosts).LoadDefault function 2020-11-09 09:28:44 -05:00
Adnan Maolood
5332dc6280 Don't guarantee that (*Response).Body is always non-nil 2020-11-08 18:38:08 -05:00
Adnan Maolood
6b3cf1314b Fix relative redirects 2020-11-07 23:43:07 -05:00
Adnan Maolood
fe92db1e9c Allow redirects to non-gemini schemes 2020-11-06 11:18:58 -05:00
Adnan Maolood
ff6c95930b Fix TOFU 2020-11-05 22:30:13 -05:00
Adnan Maolood
a5712c7705 Don't check if certificate is expired 2020-11-05 18:35:25 -05:00
10 changed files with 164 additions and 221 deletions

15
cert.go
View File

@@ -28,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) error { func (c *CertificateStore) Add(scope string, cert tls.Certificate) {
if c.store == nil { if c.store == nil {
c.store = map[string]tls.Certificate{} c.store = map[string]tls.Certificate{}
} }
@@ -39,15 +39,18 @@ func (c *CertificateStore) Add(scope string, cert tls.Certificate) error {
cert.Leaf = parsed cert.Leaf = parsed
} }
} }
c.store[scope] = cert
}
// Write writes the provided certificate to the certificate directory.
func (c *CertificateStore) Write(scope string, cert tls.Certificate) error {
if c.dir { if c.dir {
// Write certificates
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 {
return err return err
} }
} }
c.store[scope] = cert
return nil return nil
} }
@@ -82,6 +85,12 @@ func (c *CertificateStore) Load(path string) error {
return nil return nil
} }
// SetOutput sets the directory that new certificates will be written to.
func (c *CertificateStore) SetOutput(path string) {
c.dir = true
c.path = path
}
// CertificateOptions configures the creation of a certificate. // CertificateOptions configures the creation of a certificate.
type CertificateOptions struct { type CertificateOptions struct {
// Subject Alternate Name values. // Subject Alternate Name values.

View File

@@ -47,7 +47,7 @@ type Client struct {
// the request of a server. // the request of a server.
// If CreateCertificate is nil or the returned error is not nil, // If CreateCertificate is nil or the returned error is not nil,
// the request will not be sent again and the response will be returned. // the request will not be sent again and the response will be returned.
CreateCertificate func(hostname, path string) (tls.Certificate, error) CreateCertificate func(scope, path string) (tls.Certificate, error)
// TrustCertificate is called to determine whether the client // TrustCertificate is called to determine whether the client
// should trust a certificate it has not seen before. // should trust a certificate it has not seen before.
@@ -108,6 +108,7 @@ func (c *Client) do(req *Request, via []*Request) (*Response, error) {
if err := resp.read(conn); err != nil { if err := resp.read(conn); err != nil {
return nil, err return nil, err
} }
resp.Request = req
// Store connection state // Store connection state
resp.TLS = conn.ConnectionState() resp.TLS = conn.ConnectionState()
@@ -151,11 +152,7 @@ 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)
if target.Scheme != "" && target.Scheme != "gemini" {
return resp, nil
}
redirect := NewRequestFromURL(target) redirect := NewRequestFromURL(target)
if c.CheckRedirect != nil { if c.CheckRedirect != nil {
@@ -166,7 +163,6 @@ func (c *Client) do(req *Request, via []*Request) (*Response, error) {
} }
} }
resp.Request = req
return resp, nil return resp, nil
} }
@@ -213,28 +209,30 @@ func (c *Client) verifyConnection(req *Request, cs tls.ConnectionState) error {
if c.InsecureSkipTrust { if c.InsecureSkipTrust {
return nil return nil
} }
// Check the known hosts // Check the known hosts
knownHost, ok := c.KnownHosts.Lookup(hostname) knownHost, ok := c.KnownHosts.Lookup(hostname)
if ok && time.Now().After(cert.NotAfter) { if !ok || time.Now().Unix() >= knownHost.Expires {
// 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:
fingerprint := NewFingerprint(cert.Raw, cert.NotAfter)
c.KnownHosts.Add(hostname, fingerprint)
return nil
case TrustAlways:
fingerprint := NewFingerprint(cert.Raw, cert.NotAfter)
c.KnownHosts.Add(hostname, fingerprint)
c.KnownHosts.Write(hostname, fingerprint)
return nil
}
} }
return nil return errors.New("gemini: certificate not trusted")
} }
// Unknown certificate fingerprint := NewFingerprint(cert.Raw, cert.NotAfter)
// See if the client trusts the certificate if knownHost.Hex == fingerprint.Hex {
if c.TrustCertificate != nil { return 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") return errors.New("gemini: fingerprint does not match")
} }

55
doc.go
View File

@@ -1,55 +0,0 @@
/*
Package gemini implements the Gemini protocol.
Get makes a Gemini request:
resp, err := gemini.Get("gemini://example.com")
if err != nil {
// handle error
}
defer resp.Body.Close()
// ...
For control over client behavior, create a Client:
client := &gemini.Client{}
resp, err := client.Get("gemini://example.com")
if err != nil {
// handle error
}
// ...
Server is a Gemini server.
server := &gemini.Server{
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
Servers should be configured with certificates:
err := server.Certificates.Load("/var/lib/gemini/certs")
if err != nil {
// handle error
}
Servers can accept requests for multiple hosts and schemes:
server.RegisterFunc("example.com", func(w *gemini.ResponseWriter, r *gemini.Request) {
fmt.Fprint(w, "Welcome to example.com")
})
server.RegisterFunc("example.org", func(w *gemini.ResponseWriter, r *gemini.Request) {
fmt.Fprint(w, "Welcome to example.org")
})
server.RegisterFunc("http://example.net", func(w *gemini.ResponseWriter, r *gemini.Request) {
fmt.Fprint(w, "Proxied content from http://example.net")
})
To start the server, call ListenAndServe:
err := server.ListenAndServe()
if err != nil {
// handle error
}
*/
package gemini

View File

@@ -64,7 +64,7 @@ func main() {
} }
func getSession(cert *x509.Certificate) (*session, bool) { func getSession(cert *x509.Certificate) (*session, bool) {
fingerprint := gemini.NewFingerprint(cert) fingerprint := gemini.NewFingerprint(cert.Raw, cert.NotAfter)
session, ok := sessions[fingerprint.Hex] session, ok := sessions[fingerprint.Hex]
return session, ok return session, ok
} }
@@ -79,7 +79,8 @@ func login(w *gemini.ResponseWriter, r *gemini.Request) {
w.WriteHeader(gemini.StatusInput, "Username") w.WriteHeader(gemini.StatusInput, "Username")
return return
} }
fingerprint := gemini.NewFingerprint(r.Certificate.Leaf) cert := r.Certificate.Leaf
fingerprint := gemini.NewFingerprint(cert.Raw, cert.NotAfter)
sessions[fingerprint.Hex] = &session{ sessions[fingerprint.Hex] = &session{
username: username, username: username,
} }
@@ -116,7 +117,8 @@ func logout(w *gemini.ResponseWriter, r *gemini.Request) {
w.WriteStatus(gemini.StatusCertificateRequired) w.WriteStatus(gemini.StatusCertificateRequired)
return return
} }
fingerprint := gemini.NewFingerprint(r.Certificate.Leaf) cert := r.Certificate.Leaf
fingerprint := gemini.NewFingerprint(cert.Raw, cert.NotAfter)
delete(sessions, fingerprint.Hex) delete(sessions, fingerprint.Hex)
fmt.Fprintln(w, "Successfully logged out.") fmt.Fprintln(w, "Successfully logged out.")
fmt.Fprintln(w, "=> / Index") fmt.Fprintln(w, "=> / Index")

View File

@@ -10,9 +10,11 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
"path/filepath"
"time" "time"
"git.sr.ht/~adnano/go-gemini" "git.sr.ht/~adnano/go-gemini"
"git.sr.ht/~adnano/go-xdg"
) )
const trustPrompt = `The certificate offered by %s is of unknown trust. Its fingerprint is: const trustPrompt = `The certificate offered by %s is of unknown trust. Its fingerprint is:
@@ -31,9 +33,9 @@ var (
func init() { func init() {
client.Timeout = 30 * time.Second client.Timeout = 30 * time.Second
client.KnownHosts.LoadDefault() client.KnownHosts.Load(filepath.Join(xdg.DataHome(), "gemini", "known_hosts"))
client.TrustCertificate = func(hostname string, cert *x509.Certificate) gemini.Trust { client.TrustCertificate = func(hostname string, cert *x509.Certificate) gemini.Trust {
fingerprint := gemini.NewFingerprint(cert) fingerprint := gemini.NewFingerprint(cert.Raw, cert.NotAfter)
fmt.Printf(trustPrompt, hostname, fingerprint.Hex) fmt.Printf(trustPrompt, hostname, fingerprint.Hex)
scanner.Scan() scanner.Scan()
switch scanner.Text() { switch scanner.Text() {
@@ -80,9 +82,9 @@ func main() {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
defer resp.Body.Close()
if resp.Status.Class() == gemini.StatusClassSuccess { if resp.Status.Class() == gemini.StatusClassSuccess {
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

4
fs.go
View File

@@ -33,7 +33,7 @@ func (fsh fsHandler) Respond(w *ResponseWriter, r *Request) {
// Detect mimetype // Detect mimetype
ext := path.Ext(p) ext := path.Ext(p)
mimetype := mime.TypeByExtension(ext) mimetype := mime.TypeByExtension(ext)
w.SetMimetype(mimetype) w.SetMediaType(mimetype)
// Copy file to response writer // Copy file to response writer
io.Copy(w, f) io.Copy(w, f)
} }
@@ -72,7 +72,7 @@ func ServeFile(w *ResponseWriter, fs FS, name string) {
// Detect mimetype // Detect mimetype
ext := path.Ext(name) ext := path.Ext(name)
mimetype := mime.TypeByExtension(ext) mimetype := mime.TypeByExtension(ext)
w.SetMimetype(mimetype) w.SetMediaType(mimetype)
// Copy file to response writer // Copy file to response writer
io.Copy(w, f) io.Copy(w, f)
} }

View File

@@ -1,8 +1,56 @@
/*
Package gemini implements the Gemini protocol.
Client is a Gemini client.
client := &gemini.Client{}
resp, err := client.Get("gemini://example.com")
if err != nil {
// handle error
}
if resp.Status.Class() == gemini.StatusClassSucess {
defer resp.Body.Close()
// ...
}
// ...
Server is a Gemini server.
server := &gemini.Server{
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
Servers should be configured with certificates:
err := server.Certificates.Load("/var/lib/gemini/certs")
if err != nil {
// handle error
}
Servers can accept requests for multiple hosts and schemes:
server.RegisterFunc("example.com", func(w *gemini.ResponseWriter, r *gemini.Request) {
fmt.Fprint(w, "Welcome to example.com")
})
server.RegisterFunc("example.org", func(w *gemini.ResponseWriter, r *gemini.Request) {
fmt.Fprint(w, "Welcome to example.org")
})
server.RegisterFunc("http://example.net", func(w *gemini.ResponseWriter, r *gemini.Request) {
fmt.Fprint(w, "Proxied content from http://example.net")
})
To start the server, call ListenAndServe:
err := server.ListenAndServe()
if err != nil {
// handle error
}
*/
package gemini package gemini
import ( import (
"errors" "errors"
"sync"
) )
var crlf = []byte("\r\n") var crlf = []byte("\r\n")
@@ -13,26 +61,3 @@ var (
ErrInvalidResponse = errors.New("gemini: invalid response") ErrInvalidResponse = errors.New("gemini: invalid response")
ErrBodyNotAllowed = errors.New("gemini: response body not allowed") ErrBodyNotAllowed = errors.New("gemini: response body not allowed")
) )
// defaultClient is the default client. It is used by Get and Do.
var defaultClient Client
// Get performs a Gemini request for the given url.
func Get(url string) (*Response, error) {
setupDefaultClientOnce()
return defaultClient.Get(url)
}
// Do performs a Gemini request and returns a Gemini response.
func Do(req *Request) (*Response, error) {
setupDefaultClientOnce()
return defaultClient.Do(req)
}
var defaultClientOnce sync.Once
func setupDefaultClientOnce() {
defaultClientOnce.Do(func() {
defaultClient.KnownHosts.LoadDefault()
})
}

View File

@@ -2,10 +2,8 @@ package gemini
import ( import (
"bufio" "bufio"
"bytes"
"crypto/tls" "crypto/tls"
"io" "io"
"io/ioutil"
"strconv" "strconv"
) )
@@ -21,7 +19,6 @@ type Response struct {
Meta string Meta string
// Body contains the response body for successful responses. // Body contains the response body for successful responses.
// Body is guaranteed to be non-nil.
Body io.ReadCloser Body io.ReadCloser
// Request is the request that was sent to obtain this response. // Request is the request that was sent to obtain this response.
@@ -86,8 +83,6 @@ func (resp *Response) read(rc io.ReadCloser) error {
if resp.Status.Class() == StatusClassSuccess { if resp.Status.Class() == StatusClassSuccess {
resp.Body = newReadCloserBody(br, rc) resp.Body = newReadCloserBody(br, rc)
} else {
resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{}))
} }
return nil return nil
} }

View File

@@ -156,12 +156,13 @@ func (s *Server) getCertificateFor(hostname string) (*tls.Certificate, error) {
// Generate a new certificate if it is missing or expired // Generate a new certificate if it is missing or expired
cert, ok := s.Certificates.Lookup(hostname) cert, ok := s.Certificates.Lookup(hostname)
if !ok || cert.Leaf != nil && !time.Now().After(cert.Leaf.NotAfter) { if !ok || cert.Leaf != nil && cert.Leaf.NotAfter.Before(time.Now()) {
if s.CreateCertificate != nil { if s.CreateCertificate != nil {
cert, err := s.CreateCertificate(hostname) cert, err := s.CreateCertificate(hostname)
if err == nil { if err == nil {
if err := s.Certificates.Add(hostname, cert); err != nil { s.Certificates.Add(hostname, cert)
s.logf("gemini: Failed to add new certificate for %s: %s", hostname, err) if err := s.Certificates.Write(hostname, cert); err != nil {
s.logf("gemini: Failed to write new certificate for %s: %s", hostname, err)
} }
} }
return &cert, err return &cert, err
@@ -262,7 +263,7 @@ type ResponseWriter struct {
b *bufio.Writer b *bufio.Writer
bodyAllowed bool bodyAllowed bool
wroteHeader bool wroteHeader bool
mimetype string mediatype string
} }
func newResponseWriter(conn net.Conn) *ResponseWriter { func newResponseWriter(conn net.Conn) *ResponseWriter {
@@ -301,10 +302,10 @@ func (w *ResponseWriter) WriteStatus(status Status) {
w.WriteHeader(status, status.Message()) w.WriteHeader(status, status.Message())
} }
// SetMimetype sets the mimetype that will be written for a successful response. // SetMediaType sets the media type that will be written for a successful response.
// If the mimetype is not set, it will default to "text/gemini". // If the mimetype is not set, it will default to "text/gemini".
func (w *ResponseWriter) SetMimetype(mimetype string) { func (w *ResponseWriter) SetMediaType(mediatype string) {
w.mimetype = mimetype w.mediatype = mediatype
} }
// Write writes the response body. // Write writes the response body.
@@ -315,11 +316,11 @@ func (w *ResponseWriter) SetMimetype(mimetype string) {
// with StatusSuccess and the mimetype set in SetMimetype. // with StatusSuccess and the mimetype set in SetMimetype.
func (w *ResponseWriter) Write(b []byte) (int, error) { func (w *ResponseWriter) Write(b []byte) (int, error) {
if !w.wroteHeader { if !w.wroteHeader {
mimetype := w.mimetype mediatype := w.mediatype
if mimetype == "" { if mediatype == "" {
mimetype = "text/gemini" mediatype = "text/gemini"
} }
w.WriteHeader(StatusSuccess, mimetype) w.WriteHeader(StatusSuccess, mediatype)
} }
if !w.bodyAllowed { if !w.bodyAllowed {
return 0, ErrBodyNotAllowed return 0, ErrBodyNotAllowed

148
tofu.go
View File

@@ -3,12 +3,12 @@ package gemini
import ( import (
"bufio" "bufio"
"crypto/sha512" "crypto/sha512"
"crypto/x509"
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath" "strconv"
"strings" "strings"
"time"
) )
// Trust represents the trustworthiness of a certificate. // Trust represents the trustworthiness of a certificate.
@@ -24,31 +24,55 @@ const (
// 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]Fingerprint hosts map[string]Fingerprint
file *os.File out io.Writer
} }
// LoadDefault loads the known hosts from the default known hosts path, which is // SetOutput sets the output to which new known hosts will be written to.
// $XDG_DATA_HOME/gemini/known_hosts. func (k *KnownHosts) SetOutput(w io.Writer) {
// It creates the path and any of its parent directories if they do not exist. k.out = w
// KnownHosts will append to the file whenever a certificate is added. }
func (k *KnownHosts) LoadDefault() error {
path, err := defaultKnownHostsPath() // Add adds a known host to the list of known hosts.
if err != nil { func (k *KnownHosts) Add(hostname string, fingerprint Fingerprint) {
return err if k.hosts == nil {
k.hosts = map[string]Fingerprint{}
} }
return k.Load(path) k.hosts[hostname] = fingerprint
} }
// Load loads the known hosts from the provided path. // Lookup returns the fingerprint of the certificate corresponding to
// It creates the path and any of its parent directories if they do not exist. // the given hostname.
// KnownHosts will append to the file whenever a certificate is added. func (k *KnownHosts) Lookup(hostname string) (Fingerprint, bool) {
func (k *KnownHosts) Load(path string) error { c, ok := k.hosts[hostname]
if dir := filepath.Dir(path); dir != "." { return c, ok
err := os.MkdirAll(dir, 0755) }
if err != nil {
// Write writes a known hosts entry to the configured output.
func (k *KnownHosts) Write(hostname string, fingerprint Fingerprint) {
if k.out != nil {
k.writeKnownHost(k.out, hostname, fingerprint)
}
}
// WriteAll writes all of the known hosts to the provided io.Writer.
func (k *KnownHosts) WriteAll(w io.Writer) error {
for h, c := range k.hosts {
if _, err := k.writeKnownHost(w, h, c); err != nil {
return err return err
} }
} }
return nil
}
// writeKnownHost writes a known host to the provided io.Writer.
func (k *KnownHosts) writeKnownHost(w io.Writer, hostname string, f Fingerprint) (int, error) {
return fmt.Fprintf(w, "%s %s %s %d\n", hostname, f.Algorithm, f.Hex, f.Expires)
}
// Load loads the known hosts from the provided path.
// It creates the file if it does not exist.
// New known hosts will be appended to the file.
func (k *KnownHosts) Load(path string) error {
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDONLY, 0644) f, err := os.OpenFile(path, os.O_CREATE|os.O_RDONLY, 0644)
if err != nil { if err != nil {
return err return err
@@ -60,43 +84,12 @@ func (k *KnownHosts) Load(path string) error {
if err != nil { if err != nil {
return err return err
} }
k.file = f k.out = f
return nil return nil
} }
// Add adds a certificate to the list of known hosts.
// If KnownHosts was loaded from a file, Add will append to the file.
func (k *KnownHosts) Add(hostname string, cert *x509.Certificate) {
k.add(hostname, cert, true)
}
// AddTemporary adds a certificate to the list of known hosts
// without writing it to the known hosts file.
func (k *KnownHosts) AddTemporary(hostname string, cert *x509.Certificate) {
k.add(hostname, cert, false)
}
func (k *KnownHosts) add(hostname string, cert *x509.Certificate, write bool) {
if k.hosts == nil {
k.hosts = map[string]Fingerprint{}
}
fingerprint := NewFingerprint(cert)
k.hosts[hostname] = fingerprint
// Append to the file
if write && k.file != nil {
appendKnownHost(k.file, hostname, fingerprint)
}
}
// Lookup returns the fingerprint of the certificate corresponding to
// the given hostname.
func (k *KnownHosts) Lookup(hostname string) (Fingerprint, bool) {
c, ok := k.hosts[hostname]
return c, ok
}
// 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 entries 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]Fingerprint{} k.hosts = map[string]Fingerprint{}
@@ -105,7 +98,7 @@ func (k *KnownHosts) Parse(r io.Reader) {
for scanner.Scan() { for scanner.Scan() {
text := scanner.Text() text := scanner.Text()
parts := strings.Split(text, " ") parts := strings.Split(text, " ")
if len(parts) < 3 { if len(parts) < 4 {
continue continue
} }
@@ -116,33 +109,29 @@ func (k *KnownHosts) Parse(r io.Reader) {
} }
fingerprint := parts[2] fingerprint := parts[2]
expires, err := strconv.ParseInt(parts[3], 10, 0)
if err != nil {
continue
}
k.hosts[hostname] = Fingerprint{ k.hosts[hostname] = Fingerprint{
Algorithm: algorithm, Algorithm: algorithm,
Hex: fingerprint, Hex: fingerprint,
Expires: expires,
} }
} }
} }
// Write writes the known hosts to the provided io.Writer.
func (k *KnownHosts) Write(w io.Writer) {
for h, c := range k.hosts {
appendKnownHost(w, h, c)
}
}
func appendKnownHost(w io.Writer, hostname string, f Fingerprint) (int, error) {
return fmt.Fprintf(w, "%s %s %s\n", hostname, f.Algorithm, f.Hex)
}
// Fingerprint represents a fingerprint using a certain algorithm. // Fingerprint represents a fingerprint using a certain algorithm.
type Fingerprint struct { type Fingerprint struct {
Algorithm string // fingerprint algorithm e.g. SHA-512 Algorithm string // fingerprint algorithm e.g. SHA-512
Hex string // fingerprint in hexadecimal, with ':' between each octet Hex string // fingerprint in hexadecimal, with ':' between each octet
Expires int64 // unix time of the fingerprint expiration date
} }
// NewFingerprint returns the SHA-512 fingerprint of the provided certificate. // NewFingerprint returns the SHA-512 fingerprint of the provided raw data.
func NewFingerprint(cert *x509.Certificate) Fingerprint { func NewFingerprint(raw []byte, expires time.Time) Fingerprint {
sum512 := sha512.Sum512(cert.Raw) sum512 := sha512.Sum512(raw)
var b strings.Builder var b strings.Builder
for i, f := range sum512 { for i, f := range sum512 {
if i > 0 { if i > 0 {
@@ -153,29 +142,6 @@ func NewFingerprint(cert *x509.Certificate) Fingerprint {
return Fingerprint{ return Fingerprint{
Algorithm: "SHA-512", Algorithm: "SHA-512",
Hex: b.String(), Hex: b.String(),
Expires: expires.Unix(),
} }
} }
// defaultKnownHostsPath returns the default known_hosts path.
// The default path is $XDG_DATA_HOME/gemini/known_hosts
func defaultKnownHostsPath() (string, error) {
dataDir, err := userDataDir()
if err != nil {
return "", err
}
return filepath.Join(dataDir, "gemini", "known_hosts"), nil
}
// userDataDir returns the user data directory.
func userDataDir() (string, error) {
dataDir, ok := os.LookupEnv("XDG_DATA_HOME")
if ok {
return dataDir, nil
}
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".local", "share"), nil
}