Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6b0443a62 | ||
|
|
3dee6dcff3 | ||
|
|
85f8e84bd5 | ||
|
|
9338681256 | ||
|
|
f2a1510375 | ||
|
|
46cbcfcaa4 | ||
|
|
76dfe257f1 | ||
|
|
5332dc6280 | ||
|
|
6b3cf1314b | ||
|
|
fe92db1e9c | ||
|
|
ff6c95930b | ||
|
|
a5712c7705 |
15
cert.go
15
cert.go
@@ -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.
|
||||||
|
|||||||
46
client.go
46
client.go
@@ -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
55
doc.go
@@ -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
|
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
4
fs.go
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
73
gemini.go
73
gemini.go
@@ -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()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
23
server.go
23
server.go
@@ -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
148
tofu.go
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user