forked from sloum/bombadillo
Merge branch 'tofu' of sloum/bombadillo into develop
Adds the following: - Storing/retrieving of TLS certs from servers in `.bombadillo.ini` - `purge` command is added to clear out stale certificates - Bombadillo now validates that certs match the ones on file - Bombadillo now validates certificates are within valid window - Bombadillo now validates that certificate hostnames match the host that is requested - Logic to get a new certificate (if one is presented) when an expired certificate is still present - Logic to get a certificate if none is present
This commit is contained in:
commit
66acf6102f
|
@ -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"
|
||||
|
|
39
client.go
39
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
|
||||
|
@ -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 {
|
||||
|
@ -472,7 +494,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()
|
||||
|
@ -551,7 +573,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
|
||||
|
@ -832,12 +854,13 @@ 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)
|
||||
if err != nil {
|
||||
c.SetMessage(err.Error(), true)
|
||||
c.DrawMessage()
|
||||
return
|
||||
}
|
||||
go saveConfig()
|
||||
switch capsule.Status {
|
||||
case 1:
|
||||
c.search("", u.Full, capsule.Content)
|
||||
|
@ -955,7 +978,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
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
170
gemini/gemini.go
170
gemini/gemini.go
|
@ -1,28 +1,138 @@
|
|||
package gemini
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
// "tildegit.org/sloum/mailcap"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
||||
type Capsule struct {
|
||||
MimeMaj string
|
||||
MimeMin string
|
||||
Status int
|
||||
Content string
|
||||
Links []string
|
||||
Links []string
|
||||
}
|
||||
|
||||
|
||||
type TofuDigest struct {
|
||||
certs map[string]string
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------\\
|
||||
// + + + R E C E I V E R S + + + \\
|
||||
//--------------------------------------------------\\
|
||||
|
||||
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 %q", host)
|
||||
}
|
||||
|
||||
func (t *TofuDigest) Add(host, hash string) {
|
||||
t.certs[strings.ToLower(host)] = hash
|
||||
}
|
||||
|
||||
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 string, cState *tls.ConnectionState) error {
|
||||
host = strings.ToLower(host)
|
||||
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
|
||||
}
|
||||
|
||||
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 fmt.Errorf("No valid certificates were offered by host %q", host)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
@ -41,6 +151,38 @@ func Retrieve(host, port, resource string) (string, error) {
|
|||
|
||||
defer conn.Close()
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
if td.Exists(host) {
|
||||
// See if we have a matching cert
|
||||
err := td.Match(host, &connState)
|
||||
if err != nil && err.Error() != "EXP" {
|
||||
// If there is no match and it isnt because of an expiration
|
||||
// just return the error
|
||||
return "", err
|
||||
} 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
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
} 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"
|
||||
|
||||
_, err = conn.Write([]byte(send))
|
||||
|
@ -56,8 +198,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
|
||||
}
|
||||
|
@ -103,9 +245,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
|
||||
}
|
||||
|
@ -226,8 +368,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)}
|
||||
}
|
||||
|
|
11
main.go
11
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
|
||||
}
|
||||
|
||||
|
|
39
url.go
39
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(`^((?P<scheme>gopher|telnet|http|https|gemini):\/\/)?(?P<host>[\w\-\.\d]+)(?::(?P<port>\d+)?)?(?:/(?P<type>[01345679gIhisp])?)?(?P<resource>.*)?$`)
|
||||
re := regexp.MustCompile(`^((?P<scheme>[a-zA-Z]+):\/\/)?(?P<host>[\w\-\.\d]+)(?::(?P<port>\d+)?)?(?:/(?P<type>[01345679gIhisp])?)?(?P<resource>.*)?$`)
|
||||
match := re.FindStringSubmatch(u)
|
||||
|
||||
if valid := re.MatchString(u); !valid {
|
||||
|
@ -59,14 +59,16 @@ func MakeUrl(u string) (Url, error) {
|
|||
}
|
||||
}
|
||||
|
||||
if out.Scheme == "" {
|
||||
out.Scheme = "gopher"
|
||||
}
|
||||
|
||||
if out.Host == "" {
|
||||
return out, fmt.Errorf("no host")
|
||||
}
|
||||
|
||||
out.Scheme = strings.ToLower(out.Scheme)
|
||||
|
||||
if out.Scheme == "" {
|
||||
out.Scheme = "gopher"
|
||||
}
|
||||
|
||||
if out.Scheme == "gopher" && out.Port == "" {
|
||||
out.Port = "70"
|
||||
} else if out.Scheme == "http" && out.Port == "" {
|
||||
|
@ -75,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
|
||||
|
@ -101,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
|
||||
|
|
Loading…
Reference in New Issue