From 5e68d81a6346bdd89f19243a0e8318fc2fad4d2e Mon Sep 17 00:00:00 2001 From: sloumdrone Date: Tue, 24 Sep 2019 21:23:44 -0700 Subject: [PATCH 1/8] Starts building out a tofu system for certificate pinning --- gemini/gemini.go | 66 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/gemini/gemini.go b/gemini/gemini.go index 4d8b044..39cda6d 100644 --- a/gemini/gemini.go +++ b/gemini/gemini.go @@ -6,10 +6,10 @@ import ( "io/ioutil" "strconv" "strings" - - // "tildegit.org/sloum/mailcap" + "time" ) + type Capsule struct { MimeMaj string MimeMin string @@ -18,6 +18,50 @@ type Capsule struct { Links []string } +type TofuDigest struct { + db map[string][]map[string]string +} + +//------------------------------------------------\\ +// + + + R E C E I V E R S + + + \\ +//--------------------------------------------------\\ + +func (t *TofuDigest) Remove(host string, indexToRemove int) error { + if _, ok := t.db[host]; ok { + if indexToRemove < 0 || indexToRemove >= len(t.db[host]) { + return fmt.Errorf("Invalid index") + } else if len(t.db[host]) > indexToRemove { + t.db[host] = append(t.db[host][:indexToRemove], t.db[host][indexToRemove+1:]...) + } else if len(t.db[host]) - 1 == indexToRemove { + t.db[host] = t.db[host][:indexToRemove] + } + return nil + } + return fmt.Errorf("Invalid host") +} + +func (t *TofuDigest) Add(host, hash string, start, end int64) { + s := strconv.FormatInt(start, 10) + e := strconv.FormatInt(end, 10) + added := strconv.FormatInt(time.Now().Unix(), 10) + entry := map[string]string{"hash": hash, "start": s, "end": e, "added": added} + t.db[host] = append(t.db[host], entry) +} + +// Removes all entries that are expired +func (t *TofuDigest) Clean() { + now := time.Now() + for host, slice := range t.db { + for index, entry := range slice { + intFromStringTime, err := strconv.ParseInt(entry["end"], 10, 64) + if err != nil || now.After(time.Unix(intFromStringTime, 0)) { + t.Remove(host, index) + } + } + } +} + + //------------------------------------------------\\ // + + + F U N C T I O N S + + + \\ //--------------------------------------------------\\ @@ -41,6 +85,24 @@ func Retrieve(host, port, resource string) (string, error) { defer conn.Close() + // Verify that the handshake ahs completed and that + // the hostname on the certificate(s) from the server + // is the hostname we have requested + connState := conn.ConnectionState() + if connState.HandshakeComplete { + if len(connState.PeerCertificates) > 0 { + for _, cert := range connState.PeerCertificates { + if err = cert.VerifyHostname(host); err == nil { + break + } + } + if err != nil { + return "", err + } + } + } + + send := "gemini://" + addr + "/" + resource + "\r\n" _, err = conn.Write([]byte(send)) From 7896858facaa28343b96d1ed7bea5b1f44c0a209 Mon Sep 17 00:00:00 2001 From: sloumdrone Date: Thu, 26 Sep 2019 18:30:44 -0700 Subject: [PATCH 2/8] Updated url regex to allow for any scheme --- url.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/url.go b/url.go index faedbbb..343cdbb 100644 --- a/url.go +++ b/url.go @@ -37,7 +37,7 @@ type Url struct { // an error (or nil). func MakeUrl(u string) (Url, error) { var out Url - re := regexp.MustCompile(`^((?Pgopher|telnet|http|https|gemini):\/\/)?(?P[\w\-\.\d]+)(?::(?P\d+)?)?(?:/(?P[01345679gIhisp])?)?(?P.*)?$`) + re := regexp.MustCompile(`^((?P[a-zA-Z]+):\/\/)?(?P[\w\-\.\d]+)(?::(?P\d+)?)?(?:/(?P[01345679gIhisp])?)?(?P.*)?$`) match := re.FindStringSubmatch(u) if valid := re.MatchString(u); !valid { @@ -58,6 +58,7 @@ func MakeUrl(u string) (Url, error) { out.Resource = match[i] } } + out.Scheme = strings.ToLower(out.Scheme) if out.Scheme == "" { out.Scheme = "gopher" From 4c92870790f00308206d1c8e3977a60f41371000 Mon Sep 17 00:00:00 2001 From: sloumdrone Date: Thu, 26 Sep 2019 18:40:14 -0700 Subject: [PATCH 3/8] Cleans up url struct creation a bit --- url.go | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/url.go b/url.go index 343cdbb..73b5226 100644 --- a/url.go +++ b/url.go @@ -58,16 +58,17 @@ func MakeUrl(u string) (Url, error) { out.Resource = match[i] } } + + if out.Host == "" { + return out, fmt.Errorf("no host") + } + out.Scheme = strings.ToLower(out.Scheme) if out.Scheme == "" { out.Scheme = "gopher" } - if out.Host == "" { - return out, fmt.Errorf("no host") - } - if out.Scheme == "gopher" && out.Port == "" { out.Port = "70" } else if out.Scheme == "http" && out.Port == "" { @@ -76,21 +77,20 @@ func MakeUrl(u string) (Url, error) { out.Port = "443" } else if out.Scheme == "gemini" && out.Port == "" { out.Port = "1965" - } - - if out.Scheme == "gopher" && out.Mime == "" { - out.Mime = "1" - } - - if out.Mime == "" && (out.Resource == "" || out.Resource == "/") && out.Scheme == "gopher" { - out.Mime = "1" - } - - if out.Mime == "7" && strings.Contains(out.Resource, "\t") { - out.Mime = "1" + } else if out.Scheme == "telnet" && out.Port == "" { + out.Port = "23" } if out.Scheme == "gopher" { + if out.Mime == "" { + out.Mime = "1" + } + if out.Resource == "" || out.Resource == "/" { + out.Mime = "1" + } + if out.Mime == "7" && strings.Contains(out.Resource, "\t") { + out.Mime = "1" + } switch out.Mime { case "1", "0", "h", "7": out.DownloadOnly = false @@ -102,10 +102,6 @@ func MakeUrl(u string) (Url, error) { out.Mime = "" } - if out.Scheme == "http" || out.Scheme == "https" { - out.Mime = "" - } - out.Full = out.Scheme + "://" + out.Host + ":" + out.Port + "/" + out.Mime + out.Resource return out, nil From bef32b7ff56f60e6d1a7e24d326f49f710787755 Mon Sep 17 00:00:00 2001 From: sloumdrone Date: Thu, 26 Sep 2019 22:08:57 -0700 Subject: [PATCH 4/8] Adds basic tofu certificate pinning --- bookmarks.go | 2 +- client.go | 11 ++-- config/parser.go | 6 +- gemini/gemini.go | 143 ++++++++++++++++++++++++++++++++++------------- main.go | 11 +++- 5 files changed, 123 insertions(+), 50 deletions(-) diff --git a/bookmarks.go b/bookmarks.go index cb2b773..3e320c4 100644 --- a/bookmarks.go +++ b/bookmarks.go @@ -61,7 +61,7 @@ func (b *Bookmarks) ToggleFocused() { } func (b Bookmarks) IniDump() string { - if len(b.Titles) < 0 { + if len(b.Titles) < 1 { return "" } out := "[BOOKMARKS]\n" diff --git a/client.go b/client.go index 54eb354..05bee18 100644 --- a/client.go +++ b/client.go @@ -16,7 +16,6 @@ import ( "tildegit.org/sloum/bombadillo/gopher" "tildegit.org/sloum/bombadillo/http" "tildegit.org/sloum/bombadillo/telnet" - // "tildegit.org/sloum/mailcap" ) //------------------------------------------------\\ @@ -33,6 +32,7 @@ type client struct { BookMarks Bookmarks TopBar Headbar FootBar Footbar + Certs gemini.TofuDigest } @@ -154,7 +154,7 @@ func (c *client) TakeControlInput() { c.ClearMessage() c.Scroll(-1) case 'q', 'Q': - // quite bombadillo + // quit bombadillo cui.Exit() case 'g': // scroll to top @@ -472,7 +472,7 @@ func (c *client) saveFile(u Url, name string) { case "gopher": file, err = gopher.Retrieve(u.Host, u.Port, u.Resource) case "gemini": - file, err = gemini.Fetch(u.Host, u.Port, u.Resource) + file, err = gemini.Fetch(u.Host, u.Port, u.Resource, &c.Certs) default: c.SetMessage(fmt.Sprintf("Saving files over %s is not supported", u.Scheme), true) c.DrawMessage() @@ -832,7 +832,8 @@ func (c *client) Visit(url string) { c.Draw() } case "gemini": - capsule, err := gemini.Visit(u.Host, u.Port, u.Resource) + capsule, err := gemini.Visit(u.Host, u.Port, u.Resource, &c.Certs) + go saveConfig() if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() @@ -955,7 +956,7 @@ func (c *client) Visit(url string) { //--------------------------------------------------\\ func MakeClient(name string) *client { - c := client{0, 0, defaultOptions, "", false, MakePages(), MakeBookmarks(), MakeHeadbar(name), MakeFootbar()} + c := client{0, 0, defaultOptions, "", false, MakePages(), MakeBookmarks(), MakeHeadbar(name), MakeFootbar(), gemini.MakeTofuDigest()} return &c } diff --git a/config/parser.go b/config/parser.go index 038c889..780bfa7 100644 --- a/config/parser.go +++ b/config/parser.go @@ -24,8 +24,8 @@ type Config struct { Bookmarks struct { Titles, Links []string } - Colors []KeyValue Settings []KeyValue + Certs []KeyValue } type KeyValue struct { @@ -90,8 +90,8 @@ func (p *Parser) Parse() (Config, error) { case "BOOKMARKS": c.Bookmarks.Titles = append(c.Bookmarks.Titles, keyval.Value) c.Bookmarks.Links = append(c.Bookmarks.Links, keyval.Key) - case "COLORS": - c.Colors = append(c.Colors, keyval) + case "CERTS": + c.Certs = append(c.Certs, keyval) case "SETTINGS": c.Settings = append(c.Settings, keyval) } diff --git a/gemini/gemini.go b/gemini/gemini.go index 39cda6d..adce235 100644 --- a/gemini/gemini.go +++ b/gemini/gemini.go @@ -1,6 +1,8 @@ package gemini import ( + "bytes" + "crypto/sha1" "crypto/tls" "fmt" "io/ioutil" @@ -15,58 +17,78 @@ type Capsule struct { MimeMin string Status int Content string - Links []string + Links []string } + type TofuDigest struct { - db map[string][]map[string]string + certs map[string]string } + //------------------------------------------------\\ // + + + R E C E I V E R S + + + \\ //--------------------------------------------------\\ -func (t *TofuDigest) Remove(host string, indexToRemove int) error { - if _, ok := t.db[host]; ok { - if indexToRemove < 0 || indexToRemove >= len(t.db[host]) { - return fmt.Errorf("Invalid index") - } else if len(t.db[host]) > indexToRemove { - t.db[host] = append(t.db[host][:indexToRemove], t.db[host][indexToRemove+1:]...) - } else if len(t.db[host]) - 1 == indexToRemove { - t.db[host] = t.db[host][:indexToRemove] - } +func (t *TofuDigest) Remove(host string) error { + if _, ok := t.certs[strings.ToLower(host)]; ok { + delete(t.certs, host) return nil } return fmt.Errorf("Invalid host") } -func (t *TofuDigest) Add(host, hash string, start, end int64) { - s := strconv.FormatInt(start, 10) - e := strconv.FormatInt(end, 10) - added := strconv.FormatInt(time.Now().Unix(), 10) - entry := map[string]string{"hash": hash, "start": s, "end": e, "added": added} - t.db[host] = append(t.db[host], entry) +func (t *TofuDigest) Add(host, hash string) { + t.certs[strings.ToLower(host)] = hash } -// Removes all entries that are expired -func (t *TofuDigest) Clean() { - now := time.Now() - for host, slice := range t.db { - for index, entry := range slice { - intFromStringTime, err := strconv.ParseInt(entry["end"], 10, 64) - if err != nil || now.After(time.Unix(intFromStringTime, 0)) { - t.Remove(host, index) - } - } +func (t *TofuDigest) Exists(host string) bool { + if _, ok := t.certs[strings.ToLower(host)]; ok { + return true } + return false } +func (t *TofuDigest) Find(host string) (string, error) { + if hash, ok := t.certs[strings.ToLower(host)]; ok { + return hash, nil + } + return "", fmt.Errorf("Invalid hostname, no key saved") +} + +func (t *TofuDigest) Match(host, hash string) bool { + host = strings.ToLower(host) + if _, ok := t.certs[host]; !ok { + return false + } + if t.certs[host] == hash { + return true + } + return false +} + +func (t *TofuDigest) IniDump() string { + if len(t.certs) < 1 { + return "" + } + var out strings.Builder + out.WriteString("[CERTS]\n") + for k, v := range t.certs { + out.WriteString(k) + out.WriteString("=") + out.WriteString(v) + out.WriteString("\n") + } + return out.String() +} + + //------------------------------------------------\\ // + + + F U N C T I O N S + + + \\ //--------------------------------------------------\\ -func Retrieve(host, port, resource string) (string, error) { +func Retrieve(host, port, resource string, td *TofuDigest) (string, error) { if host == "" || port == "" { return "", fmt.Errorf("Incomplete request url") } @@ -83,25 +105,54 @@ func Retrieve(host, port, resource string) (string, error) { return "", err } + now := time.Now() + defer conn.Close() // Verify that the handshake ahs completed and that // the hostname on the certificate(s) from the server // is the hostname we have requested connState := conn.ConnectionState() - if connState.HandshakeComplete { - if len(connState.PeerCertificates) > 0 { - for _, cert := range connState.PeerCertificates { - if err = cert.VerifyHostname(host); err == nil { - break - } + if len(connState.PeerCertificates) < 0 { + return "", fmt.Errorf("Insecure, no certificates offered by server") + } + hostCertExists := td.Exists(host) + matched := false + + for _, cert := range connState.PeerCertificates { + if hostCertExists { + if td.Match(host, hashCert(cert.Raw)) { + matched = true + if now.Before(cert.NotBefore) { + return "", fmt.Errorf("Server certificate error: certificate not valid yet") + } + + if now.After(cert.NotAfter) { + return "", fmt.Errorf("Server certificate error: certificate expired") + } + + if err = cert.VerifyHostname(host); err != nil { + return "", fmt.Errorf("Server certificate error: %s", err) + } + break + } + } else { + if now.Before(cert.NotBefore) || now.After(cert.NotAfter) { + continue } - if err != nil { - return "", err + + if err = cert.VerifyHostname(host); err != nil { + return "", fmt.Errorf("Server certificate error: %s", err) } + + td.Add(host, hashCert(cert.Raw)) + matched = true } } + if !matched { + return "", fmt.Errorf("Server certificate error: No matching certificate provided") + } send := "gemini://" + addr + "/" + resource + "\r\n" @@ -118,8 +169,8 @@ func Retrieve(host, port, resource string) (string, error) { return string(result), nil } -func Fetch(host, port, resource string) ([]byte, error) { - rawResp, err := Retrieve(host, port, resource) +func Fetch(host, port, resource string, td *TofuDigest) ([]byte, error) { + rawResp, err := Retrieve(host, port, resource, td) if err != nil { return make([]byte, 0), err } @@ -165,9 +216,9 @@ func Fetch(host, port, resource string) ([]byte, error) { } -func Visit(host, port, resource string) (Capsule, error) { +func Visit(host, port, resource string, td *TofuDigest) (Capsule, error) { capsule := MakeCapsule() - rawResp, err := Retrieve(host, port, resource) + rawResp, err := Retrieve(host, port, resource, td) if err != nil { return capsule, err } @@ -288,8 +339,20 @@ func handleRelativeUrl(u, root, current string) string { return fmt.Sprintf("%s%s", current, u) } +func hashCert(cert []byte) string { + hash := sha1.Sum(cert) + hex := make([][]byte, len(hash)) + for i, data := range hash { + hex[i] = []byte(fmt.Sprintf("%02X", data)) + } + return fmt.Sprintf("%s", string(bytes.Join(hex, []byte("-")))) +} + func MakeCapsule() Capsule { return Capsule{"", "", 0, "", make([]string, 0, 5)} } +func MakeTofuDigest() TofuDigest { + return TofuDigest{make(map[string]string)} +} diff --git a/main.go b/main.go index 57d00b8..41f3be3 100644 --- a/main.go +++ b/main.go @@ -24,6 +24,7 @@ import ( "os" "strings" + _ "tildegit.org/sloum/bombadillo/gemini" "tildegit.org/sloum/bombadillo/config" "tildegit.org/sloum/bombadillo/cui" "tildegit.org/sloum/mailcap" @@ -39,8 +40,8 @@ var mc *mailcap.Mailcap func saveConfig() error { var opts strings.Builder bkmrks := bombadillo.BookMarks.IniDump() + certs := bombadillo.Certs.IniDump() - opts.WriteString(bkmrks) opts.WriteString("\n[SETTINGS]\n") for k, v := range bombadillo.Options { if k == "theme" && v != "normal" && v != "inverse" { @@ -53,6 +54,10 @@ func saveConfig() error { opts.WriteRune('\n') } + opts.WriteString(bkmrks) + + opts.WriteString(certs) + return ioutil.WriteFile(bombadillo.Options["configlocation"] + "/.bombadillo.ini", []byte(opts.String()), 0644) } @@ -122,6 +127,10 @@ func loadConfig() error { bombadillo.BookMarks.Add([]string{v, settings.Bookmarks.Links[i]}) } + for _, v := range settings.Certs { + bombadillo.Certs.Add(v.Key, v.Value) + } + return nil } From 6c52299c7af25c5502f91cf7a1e78648453e5eda Mon Sep 17 00:00:00 2001 From: sloumdrone Date: Fri, 27 Sep 2019 19:19:23 -0700 Subject: [PATCH 5/8] Fixes a few logical issues and order of op --- client.go | 2 +- gemini/gemini.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index 05bee18..241bbc4 100644 --- a/client.go +++ b/client.go @@ -833,12 +833,12 @@ func (c *client) Visit(url string) { } case "gemini": capsule, err := gemini.Visit(u.Host, u.Port, u.Resource, &c.Certs) - go saveConfig() if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() return } + go saveConfig() switch capsule.Status { case 1: c.search("", u.Full, capsule.Content) diff --git a/gemini/gemini.go b/gemini/gemini.go index adce235..4e03492 100644 --- a/gemini/gemini.go +++ b/gemini/gemini.go @@ -113,7 +113,7 @@ func Retrieve(host, port, resource string, td *TofuDigest) (string, error) { // the hostname on the certificate(s) from the server // is the hostname we have requested connState := conn.ConnectionState() - if len(connState.PeerCertificates) < 0 { + if len(connState.PeerCertificates) < 1 { return "", fmt.Errorf("Insecure, no certificates offered by server") } hostCertExists := td.Exists(host) From bd004d74c21f2161ce13f3b7d6126c3523c7ae69 Mon Sep 17 00:00:00 2001 From: sloumdrone Date: Sat, 28 Sep 2019 10:20:23 -0700 Subject: [PATCH 6/8] Refactors TOFU approach --- client.go | 2 +- gemini/gemini.go | 114 ++++++++++++++++++++++++++++------------------- 2 files changed, 70 insertions(+), 46 deletions(-) diff --git a/client.go b/client.go index 241bbc4..3f84350 100644 --- a/client.go +++ b/client.go @@ -551,7 +551,7 @@ func (c *client) doLinkCommand(action, target string) { num -= 1 links := c.PageState.History[c.PageState.Position].Links - if num >= len(links) || num < 0 { + if num >= len(links) || num < 1 { c.SetMessage(fmt.Sprintf("Invalid link id: %s", target), true) c.DrawMessage() return diff --git a/gemini/gemini.go b/gemini/gemini.go index 4e03492..ef4ee15 100644 --- a/gemini/gemini.go +++ b/gemini/gemini.go @@ -56,15 +56,55 @@ func (t *TofuDigest) Find(host string) (string, error) { return "", fmt.Errorf("Invalid hostname, no key saved") } -func (t *TofuDigest) Match(host, hash string) bool { +func (t *TofuDigest) Match(host string, cState *tls.ConnectionState) error { host = strings.ToLower(host) - if _, ok := t.certs[host]; !ok { - return false + now := time.Now() + + for _, cert := range cState.PeerCertificates { + if t.certs[host] != hashCert(cert.Raw) { + continue + } + + if now.Before(cert.NotBefore) { + return fmt.Errorf("Certificate is not valid yet") + } + + if now.After(cert.NotAfter) { + return fmt.Errorf("EXP") + } + + if err := cert.VerifyHostname(host); err != nil { + return fmt.Errorf("Certificate error: %s", err) + } + + return nil } - if t.certs[host] == hash { - return true + + return fmt.Errorf("No matching certificate was found for host %q", host) +} + +func (t *TofuDigest) newCert(host string, cState *tls.ConnectionState) error { + host = strings.ToLower(host) + now := time.Now() + + for _, cert := range cState.PeerCertificates { + if now.Before(cert.NotBefore) { + continue + } + + if now.After(cert.NotAfter) { + continue + } + + if err := cert.VerifyHostname(host); err != nil { + continue + } + + t.Add(host, hashCert(cert.Raw)) + return nil } - return false + + return fmt.Errorf("No valid certificates were offered by host %q", host) } func (t *TofuDigest) IniDump() string { @@ -105,53 +145,37 @@ func Retrieve(host, port, resource string, td *TofuDigest) (string, error) { return "", err } - now := time.Now() - defer conn.Close() - // Verify that the handshake ahs completed and that - // the hostname on the certificate(s) from the server - // is the hostname we have requested connState := conn.ConnectionState() + + // Begin TOFU screening... + + // If no certificates are offered, bail out if len(connState.PeerCertificates) < 1 { return "", fmt.Errorf("Insecure, no certificates offered by server") } - hostCertExists := td.Exists(host) - matched := false - for _, cert := range connState.PeerCertificates { - if hostCertExists { - if td.Match(host, hashCert(cert.Raw)) { - matched = true - if now.Before(cert.NotBefore) { - return "", fmt.Errorf("Server certificate error: certificate not valid yet") - } - - if now.After(cert.NotAfter) { - return "", fmt.Errorf("Server certificate error: certificate expired") - } - - if err = cert.VerifyHostname(host); err != nil { - return "", fmt.Errorf("Server certificate error: %s", err) - } - break - } - } else { - if now.Before(cert.NotBefore) || now.After(cert.NotAfter) { - continue + if td.Exists(host) { + err := td.Match(host, &connState) + if err != nil && err.Error() != "EXP" { + // On any error other than EXP (expired), return the error + return "", err + } else if err.Error() == "EXP" { + // If the certificate we had was expired, check if they have + // offered a new valid cert and update the certificate + err := td.newCert(host, &connState) + if err != nil { + // If there are no valid certs to offer, let the client know + return "", err } - - if err = cert.VerifyHostname(host); err != nil { - return "", fmt.Errorf("Server certificate error: %s", err) - } - - td.Add(host, hashCert(cert.Raw)) - matched = true } - } - - if !matched { - return "", fmt.Errorf("Server certificate error: No matching certificate provided") + } else { + err = td.newCert(host, &connState) + if err != nil { + // If there are no valid certs to offer, let the client know + return "", err + } } send := "gemini://" + addr + "/" + resource + "\r\n" @@ -345,7 +369,7 @@ func hashCert(cert []byte) string { for i, data := range hash { hex[i] = []byte(fmt.Sprintf("%02X", data)) } - return fmt.Sprintf("%s", string(bytes.Join(hex, []byte("-")))) + return fmt.Sprintf("%s", string(bytes.Join(hex, []byte(":")))) } From 2fd2d6c722561818896f413edf0e6160b8a5af8e Mon Sep 17 00:00:00 2001 From: sloumdrone Date: Sat, 28 Sep 2019 16:29:42 -0700 Subject: [PATCH 7/8] Added command for user purging of certificates, and fixed multiword search --- client.go | 26 ++++++++++++++++++++++++-- cmdparse/lexer.go | 8 +++++--- gemini/gemini.go | 10 +++++++--- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/client.go b/client.go index 3f84350..a8b7b2f 100644 --- a/client.go +++ b/client.go @@ -279,6 +279,8 @@ func (c *client) simpleCommand(action string) { case "B", "BOOKMARKS": c.BookMarks.ToggleOpen() c.Draw() + case "R", "REFRESH": + // TODO build refresh code case "SEARCH": c.search("", "", "?") case "HELP", "?": @@ -299,8 +301,27 @@ func (c *client) doCommand(action string, values []string) { switch action { case "CHECK", "C": c.displayConfigValue(values[0]) + case "PURGE", "P": + err := c.Certs.Purge(values[0]) + if err != nil { + c.SetMessage(err.Error(), true) + c.DrawMessage() + return + } + if values[0] == "*" { + c.SetMessage("All certificates have been purged", false) + c.DrawMessage() + } else { + c.SetMessage(fmt.Sprintf("The certificate for %q has been purged", strings.ToLower(values[0])), false) + c.DrawMessage() + } + err = saveConfig() + if err != nil { + c.SetMessage("Error saving purge to file", true) + c.DrawMessage() + } case "SEARCH": - c.search(strings.Join(values, " "), "", "") + c.search(values[0], "", "") case "WRITE", "W": if values[0] == "." { values[0] = c.PageState.History[c.PageState.Position].Location.Full @@ -360,7 +381,8 @@ func (c *client) doCommandAs(action string, values []string) { if c.BookMarks.IsOpen { c.Draw() } - + case "SEARCH": + c.search(strings.Join(values, " "), "", "") case "WRITE", "W": u, err := MakeUrl(values[0]) if err != nil { diff --git a/cmdparse/lexer.go b/cmdparse/lexer.go index 673d7dc..529bb2a 100644 --- a/cmdparse/lexer.go +++ b/cmdparse/lexer.go @@ -68,9 +68,11 @@ func (s *scanner) scanText() Token { capInput := strings.ToUpper(buf.String()) switch capInput { - case "DELETE", "ADD", "WRITE", "SET", "RECALL", "R", "SEARCH", - "W", "A", "D", "S", "Q", "QUIT", "B", "BOOKMARKS", "H", - "HOME", "?", "HELP", "C", "CHECK": + case "D", "DELETE", "A", "ADD","W", "WRITE", + "S", "SET", "R", "REFRESH", "SEARCH", + "Q", "QUIT", "B", "BOOKMARKS", "H", + "HOME", "?", "HELP", "C", "CHECK", + "P", "PURGE": return Token{Action, capInput} } diff --git a/gemini/gemini.go b/gemini/gemini.go index ef4ee15..878f6b7 100644 --- a/gemini/gemini.go +++ b/gemini/gemini.go @@ -30,12 +30,16 @@ type TofuDigest struct { // + + + R E C E I V E R S + + + \\ //--------------------------------------------------\\ -func (t *TofuDigest) Remove(host string) error { - if _, ok := t.certs[strings.ToLower(host)]; ok { +func (t *TofuDigest) Purge(host string) error { + host = strings.ToLower(host) + if host == "*" { + t.certs = make(map[string]string) + return nil + } else if _, ok := t.certs[strings.ToLower(host)]; ok { delete(t.certs, host) return nil } - return fmt.Errorf("Invalid host") + return fmt.Errorf("Invalid host %q", host) } func (t *TofuDigest) Add(host, hash string) { From 8edf886488e3d01a007ed27229bc3055487be61c Mon Sep 17 00:00:00 2001 From: sloumdrone Date: Sat, 28 Sep 2019 16:58:42 -0700 Subject: [PATCH 8/8] Fixes a logical isssue where I was checking an errors value, but the error may have been nil and therefore had no value --- gemini/gemini.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gemini/gemini.go b/gemini/gemini.go index 878f6b7..fc0d3dc 100644 --- a/gemini/gemini.go +++ b/gemini/gemini.go @@ -161,13 +161,14 @@ func Retrieve(host, port, resource string, td *TofuDigest) (string, error) { } if td.Exists(host) { + // See if we have a matching cert err := td.Match(host, &connState) if err != nil && err.Error() != "EXP" { - // On any error other than EXP (expired), return the error + // If there is no match and it isnt because of an expiration + // just return the error return "", err - } else if err.Error() == "EXP" { - // If the certificate we had was expired, check if they have - // offered a new valid cert and update the certificate + } else if err != nil { + // The cert expired, see if they are offering one that is valid... err := td.newCert(host, &connState) if err != nil { // If there are no valid certs to offer, let the client know