Compare commits

...

38 Commits

Author SHA1 Message Date
asdf 7dc68a18bf Corrected several issues identified during review 2020-09-14 06:17:54 +10:00
asdf 86ba01b1d9 Updated branch, reduced to implementing improved error messages 2020-09-11 14:30:17 +10:00
asdf 568de7d3b3 Merge branch 'develop' into improve-command-error-messages 2020-09-10 14:49:00 +10:00
Sloom Sloum Sluom IV 97b74ea767 Merge pull request 'Release 2.3.2 into develop' (#185) from release2.3.2 into develop
Reviewed-on: sloum/bombadillo#185
2020-08-08 21:31:59 +00:00
Sloom Sloum Sluom IV 7f3705fba6 Adds more help documentation 2020-07-16 22:05:37 -07:00
Sloom Sloum Sluom IV 0e2b80626e Merge pull request 'Removes support for client certificates in Bombadillo' (#181) from remove-client-certs into release2.3.2
Reviewed-on: sloum/bombadillo#181
2020-07-10 00:46:48 -04:00
Sloom Sloum Sluom IV 99fb1ce20b Solves merge conflicts with target 2020-07-09 21:45:58 -07:00
Sloom Sloum Sluom IV a346d78c7b Merge pull request 'Updates `handleGemini` to allow for handling relative redirects' (#182) from fixes-relative-redirects into release2.3.2
Reviewed-on: sloum/bombadillo#182
2020-07-10 00:38:28 -04:00
Sloom Sloum Sluom IV 44dc3f5029 Merge branch 'release2.3.2' into fixes-relative-redirects 2020-07-10 00:38:17 -04:00
Sloom Sloum Sluom IV f8a9465eaf Merge pull request 'Uses 'linux' OpenInBrowser settings for any non-darwin and non-windows os' (#180) from fixes-browser-opening-for-bsd into release2.3.2
Reviewed-on: sloum/bombadillo#180
2020-07-10 00:37:33 -04:00
Sloom Sloum Sluom IV 6308d3e761 Merge branch 'release2.3.2' into fixes-browser-opening-for-bsd 2020-07-10 00:37:18 -04:00
Sloom Sloum Sluom IV 3dcd0c5e13 Merge pull request 'Adds user configurable timeout to gopher and gemini connections (as single value)' (#178) from timeout-setting into release2.3.2
Reviewed-on: sloum/bombadillo#178
2020-07-10 00:37:01 -04:00
Sloom Sloum Sluom IV 3d85b43732 Merge branch 'release2.3.2' into timeout-setting 2020-07-10 00:36:34 -04:00
Sloom Sloum Sluom IV 688a0b3ef2 Merge branch 'release2.3.2' into remove-client-certs 2020-07-03 18:04:39 -04:00
sloum 328d16a191 Removes additional cert references and improves error messaging 2020-07-03 15:04:16 -07:00
Sloom Sloum Sluom IV 953ae8e28f Merge pull request 'Adds default mime per gemini spec' (#183) from mime-fix into release2.3.2
Reviewed-on: sloum/bombadillo#183
2020-07-03 17:59:00 -04:00
sloum df4f09a6bb Adds default mime per gemini spec 2020-07-03 14:56:12 -07:00
Sloom Sloum Sluom IV d7a65e6679 Allows for relative redirects 2020-07-02 14:47:38 -07:00
Sloom Sloum Sluom IV b6207c7b96 Merge branch 'release2.3.2' of https://tildegit.org/sloum/bombadillo into remove-client-certs 2020-07-02 11:51:37 -07:00
Sloom Sloum Sluom IV 21e87706aa Removes gemini client certificate support from Bombadillo 2020-07-02 11:51:07 -07:00
Sloom Sloum Sluom IV 51cdb686f6 Adds more help pages 2020-07-02 11:37:00 -07:00
Sloom Sloum Sluom IV e3a0292638 Removes unneeded build tags from darwin and windows files 2020-07-02 10:19:53 -07:00
Sloom Sloum Sluom IV 4ac6c8c2e5 Removes unneeded file and simplifies gui mode opening 2020-07-02 10:08:36 -07:00
Sloom Sloum Sluom IV df3b03c661 Merge branch 'release2.3.2' into timeout-setting 2020-07-01 22:39:01 -04:00
Sloom Sloum Sluom IV 6b2ccc5ad4 Merge pull request 'Line wrap correction' (#179) from line-wrap-correction into release2.3.2
This is approved/merging in now :) For historical conversation on this PR please see: #175
2020-07-01 22:38:37 -04:00
Sloom Sloum Sluom IV baf7932ab5 Merge branch 'release2.3.2' into line-wrap-correction 2020-07-01 22:37:23 -04:00
Sloom Sloum Sluom IV 81457d8406 increments version for release 2020-06-29 22:21:43 -07:00
Sloom Sloum Sluom IV 185c62f272 Adds timeout config option 2020-06-29 22:19:14 -07:00
Sloom Sloum Sluom IV 80bdbb642d Adds timeout to tls for gemini and increases gopher timeout 2020-06-29 21:45:24 -07:00
sloum 55a31fa8df Adds more help documentation 2020-06-29 21:18:20 -07:00
Sloom Sloum Sluom IV 8baf53d32b Adds more help sections 2020-06-23 20:31:32 -07:00
asdf d2c8af2a08 Page content now renders to the entire terminal width 2020-06-23 14:56:10 +10:00
Sloom Sloum Sluom IV 8b7e52bd5a Adds help files to Makefile and adds more help content 2020-06-21 21:04:40 -07:00
Sloom Sloum Sluom IV 975a3e34fd Adds more help files 2020-06-21 15:53:31 -07:00
Sloom Sloum Sluom IV 3ffd878ad6 Working help requests! 2020-06-21 10:16:05 -07:00
Sloom Sloum Sluom IV 4a297490c1 Adds better error messaging for commands and starts the help documentation 2020-06-20 22:33:59 -07:00
asdf 57b3afd4d7 Add supporting unit tests for WrapContent 2020-06-21 14:58:51 +10:00
asdf 980f236d84 Corrects wrapped lines to ensure they fit the required terminal width 2020-06-21 14:24:12 +10:00
14 changed files with 218 additions and 104 deletions

View File

@ -1 +1 @@
2.3.1
2.3.2

View File

@ -33,7 +33,7 @@ Gopher is the default protocol for \fBbombadillo\fP. Any textual item types will
.TP
.B
gemini
Gemini is supported, but as a new protocol with an incomplete specification, features may change over time. At present Bombadillo supports TLS with a trust on first use certificate pinning system (similar to SSH). Client certificates are also supported as a configurable option. Gemini maps and other text types are rendered in the browser and non-text types will be downloaded.
Gemini is supported, but as a new protocol with an incomplete specification, features may change over time. At present Bombadillo supports TLS with a trust on first use certificate pinning system (similar to SSH). Gemini maps and other text types are rendered in the browser and non-text types will be downloaded.
.TP
.B
finger
@ -257,14 +257,8 @@ theme
Can toggle between visual modes. Valid values are \fInormal\fP, \fIcolor\fP, and \fIinverse\fP. When set to inverse, the normal mode colors are inverted. Both normal and inverse modes filter out terminal escape sequences. When set to color, Bombadillo will render terminal escape sequences representing colors when it finds them in documents.
.TP
.B
tlscertificate
A path to a tls certificate file on a user's local filesystem. Defaults to NULL. Both \fItlscertificate\fP and \fItlskey\fP must be set for client certificates to work in gemini.
.TP
.B
tlskey
A path to a tls key that pairs with the tlscertificate setting, on a user's local filesystem. Defaults to NULL. Both \fItlskey\fP and \fItlscertificate\fP must be set for client certificates to work in gemini.
.TP
.B
timeout
The number of seconds after which connections to gopher or gemini servers should time out if the server has not responded.
webmode
Controls behavior when following web links. The following values are valid: \fInone\fP will disable following web links, \fIgui\fP will have the browser attempt to open web links in a user's default graphical web browser; \fIlynx\fP, \fIw3m\fP, and \fIelinks\fP will have the browser attempt to use the selected terminal web browser to handle the rendering of web pages and will display the pages directly in Bombadillo.

View File

@ -122,8 +122,8 @@ func (c *client) Draw() {
} else {
for i := 0; i < c.Height-3; i++ {
if i < len(pageContent) {
screen.WriteString(pageContent[i])
screen.WriteString("\033[0K")
screen.WriteString(pageContent[i])
screen.WriteString("\n")
} else {
screen.WriteString("\033[0K")
@ -346,21 +346,16 @@ func (c *client) simpleCommand(action string) {
case "HELP", "?":
go c.Visit(helplocation)
default:
c.SetMessage(fmt.Sprintf("Unknown action %q", action), true)
c.SetMessage(syntaxErrorMessage(action), true)
c.DrawMessage()
}
}
func (c *client) doCommand(action string, values []string) {
if length := len(values); length != 1 {
c.SetMessage(fmt.Sprintf("Expected 1 argument, received %d", len(values)), true)
c.DrawMessage()
return
}
switch action {
case "CHECK", "C":
case "C", "CHECK":
c.displayConfigValue(values[0])
c.DrawMessage()
case "PURGE", "P":
err := c.Certs.Purge(values[0])
if err != nil {
@ -405,24 +400,22 @@ func (c *client) doCommand(action string, values []string) {
c.saveFile(u, fn)
default:
c.SetMessage(fmt.Sprintf("Unknown action %q", action), true)
c.SetMessage(syntaxErrorMessage(action), true)
c.DrawMessage()
}
}
func (c *client) doCommandAs(action string, values []string) {
if len(values) < 2 {
c.SetMessage(fmt.Sprintf("Expected 2+ arguments, received %d", len(values)), true)
c.DrawMessage()
return
}
if values[0] == "." {
values[0] = c.PageState.History[c.PageState.Position].Location.Full
}
switch action {
case "ADD", "A":
if len(values) < 2 {
c.SetMessage(syntaxErrorMessage(action), true)
c.DrawMessage()
return
}
if values[0] == "." {
values[0] = c.PageState.History[c.PageState.Position].Location.Full
}
msg, err := c.BookMarks.Add(values)
if err != nil {
c.SetMessage(err.Error(), true)
@ -441,8 +434,18 @@ func (c *client) doCommandAs(action string, values []string) {
c.Draw()
}
case "SEARCH":
if len(values) < 2 {
c.SetMessage(syntaxErrorMessage(action), true)
c.DrawMessage()
return
}
c.search(strings.Join(values, " "), "", "")
case "SET", "S":
if len(values) < 2 {
c.SetMessage(syntaxErrorMessage(action), true)
c.DrawMessage()
return
}
if _, ok := c.Options[values[0]]; ok {
val := strings.Join(values[1:], " ")
if !validateOpt(values[0], val) {
@ -451,10 +454,10 @@ func (c *client) doCommandAs(action string, values []string) {
return
}
c.Options[values[0]] = lowerCaseOpt(values[0], val)
if values[0] == "tlskey" || values[0] == "tlscertificate" {
c.Certs.LoadCertificate(c.Options["tlscertificate"], c.Options["tlskey"])
} else if values[0] == "geminiblocks" {
if values[0] == "geminiblocks" {
gemini.BlockBehavior = c.Options[values[0]]
} else if values[0] == "timeout" {
updateTimeouts(c.Options[values[0]])
} else if values[0] == "configlocation" {
c.SetMessage("Cannot set READ ONLY setting 'configlocation'", true)
c.DrawMessage()
@ -473,7 +476,7 @@ func (c *client) doCommandAs(action string, values []string) {
c.SetMessage(fmt.Sprintf("Unable to set %s, it does not exist", values[0]), true)
c.DrawMessage()
default:
c.SetMessage(fmt.Sprintf("Unknown command structure"), true)
c.SetMessage(syntaxErrorMessage(action), true)
c.DrawMessage()
}
}
@ -523,7 +526,7 @@ func (c *client) doLinkCommandAs(action, target string, values []string) {
out = append(out, values...)
c.doCommandAs(action, out)
default:
c.SetMessage(fmt.Sprintf("Unknown command structure"), true)
c.SetMessage(syntaxErrorMessage(action), true)
c.DrawMessage()
}
}
@ -655,7 +658,7 @@ func (c *client) doLinkCommand(action, target string) {
}
c.saveFile(u, fn)
default:
c.SetMessage("Unknown command structure", true)
c.SetMessage(syntaxErrorMessage(action), true)
c.DrawMessage()
}
@ -999,6 +1002,13 @@ func (c *client) handleGemini(u Url) {
if strings.Replace(lowerRedirect, lowerOriginal, "", 1) == "/" {
c.Visit(capsule.Content)
} else {
if !strings.Contains(capsule.Content, "://") {
lnk, lnkErr := gemini.HandleRelativeUrl(capsule.Content, u.Full)
if lnkErr == nil {
capsule.Content = lnk
}
}
c.SetMessage(fmt.Sprintf("Follow redirect (y/n): %s?", capsule.Content), false)
c.DrawMessage()
ch := cui.Getch()
@ -1195,3 +1205,23 @@ func findAvailableFileName(fpath, fname string) (string, error) {
return savePath, nil
}
func syntaxErrorMessage(action string) string {
if val, ok := ERRS[action]; ok {
return fmt.Sprintf("Incorrect syntax. Try: %s", val)
}
return fmt.Sprintf("Unknown command %q", action)
}
func updateTimeouts(timeoutString string) error {
sec, err := strconv.Atoi(timeoutString)
if err != nil {
return err
}
timeout := time.Duration(sec) * time.Second
gopher.Timeout = timeout
gemini.TlsTimeout = timeout
return nil
}

View File

@ -54,9 +54,8 @@ var defaultOptions = map[string]string{
"showimages": "true",
"telnetcommand": "telnet",
"theme": "normal", // "normal", "inverted", "color"
"tlscertificate": "",
"tlskey": "",
"webmode": "none", // "none", "gui", "lynx", "w3m", "elinks"
"timeout": "15", // connection timeout for gopher/gemini in seconds
"webmode": "none", // "none", "gui", "lynx", "w3m", "elinks"
}
// homePath will return the path to your home directory as a string

View File

@ -6,6 +6,7 @@ import (
"crypto/tls"
"fmt"
"io/ioutil"
"net"
"net/url"
"strconv"
"strings"
@ -22,24 +23,15 @@ type Capsule struct {
type TofuDigest struct {
certs map[string]string
ClientCert tls.Certificate
}
var BlockBehavior = "block"
var BlockBehavior string = "block"
var TlsTimeout time.Duration = time.Duration(15) * time.Second
//------------------------------------------------\\
// + + + R E C E I V E R S + + + \\
//--------------------------------------------------\\
func (t *TofuDigest) LoadCertificate(cert, key string) {
certificate, err := tls.LoadX509KeyPair(cert, key)
if err != nil {
t.ClientCert = tls.Certificate{}
return
}
t.ClientCert = certificate
}
func (t *TofuDigest) Purge(host string) error {
host = strings.ToLower(host)
if host == "*" {
@ -185,11 +177,7 @@ func Retrieve(host, port, resource string, td *TofuDigest) (string, error) {
InsecureSkipVerify: true,
}
conf.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
return &td.ClientCert, nil
}
conn, err := tls.Dial("tcp", addr, conf)
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: TlsTimeout}, "tcp", addr, conf)
if err != nil {
return "", fmt.Errorf("TLS Dial Error: %s", err.Error())
}
@ -282,7 +270,7 @@ func Fetch(host, port, resource string, td *TofuDigest) ([]byte, error) {
case 5:
return make([]byte, 0), fmt.Errorf("[5] Permanent Failure.")
case 6:
return make([]byte, 0), fmt.Errorf("[6] Client Certificate Required")
return make([]byte, 0), fmt.Errorf("[6] Client Certificate Required (Unsupported)")
default:
return make([]byte, 0), fmt.Errorf("Invalid response status from server")
}
@ -331,6 +319,9 @@ func Visit(host, port, resource string, td *TofuDigest) (Capsule, error) {
case 2:
mimeAndCharset := strings.Split(header[1], ";")
meta = mimeAndCharset[0]
if meta == "" {
meta = "text/gemini"
}
minMajMime := strings.Split(meta, "/")
if len(minMajMime) < 2 {
return capsule, fmt.Errorf("Improperly formatted mimetype received from server")
@ -359,7 +350,7 @@ func Visit(host, port, resource string, td *TofuDigest) (Capsule, error) {
case 5:
return capsule, fmt.Errorf("[5] Permanent Failure. %s", header[1])
case 6:
return capsule, fmt.Errorf("[6] Client Certificate Required")
return capsule, fmt.Errorf("[6] Client Certificate Required (Unsupported)")
default:
return capsule, fmt.Errorf("Invalid response status from server")
}
@ -399,7 +390,7 @@ func parseGemini(b, currentUrl string) (string, []string) {
}
if strings.Index(link, "://") < 0 {
link, _ = handleRelativeUrl(link, currentUrl)
link, _ = HandleRelativeUrl(link, currentUrl)
}
links = append(links, link)
@ -418,7 +409,7 @@ func parseGemini(b, currentUrl string) (string, []string) {
}
// handleRelativeUrl provides link completion
func handleRelativeUrl(relLink, current string) (string, error) {
func HandleRelativeUrl(relLink, current string) (string, error) {
base, err := url.Parse(current)
if err != nil {
return relLink, err
@ -444,5 +435,5 @@ func MakeCapsule() Capsule {
}
func MakeTofuDigest() TofuDigest {
return TofuDigest{make(map[string]string), tls.Certificate{}}
return TofuDigest{make(map[string]string)}
}

View File

@ -38,6 +38,8 @@ var types = map[string]string{
"T": "TEL",
}
var Timeout time.Duration = time.Duration(15) * time.Second
//------------------------------------------------\\
// + + + F U N C T I O N S + + + \\
//--------------------------------------------------\\
@ -49,7 +51,6 @@ var types = map[string]string{
// be better.
func Retrieve(host, port, resource string) ([]byte, error) {
nullRes := make([]byte, 0)
timeOut := time.Duration(5) * time.Second
if host == "" || port == "" {
return nullRes, errors.New("Incomplete request url")
@ -57,7 +58,7 @@ func Retrieve(host, port, resource string) ([]byte, error) {
addr := host + ":" + port
conn, err := net.DialTimeout("tcp", addr, timeOut)
conn, err := net.DialTimeout("tcp", addr, Timeout)
if err != nil {
return nullRes, err
}

28
help.go Normal file
View File

@ -0,0 +1,28 @@
package main
// ERRS maps commands to their syntax error message
var ERRS = map[string]string{
"A": "`a [target] [name...]`",
"ADD": "`add [target] [name...]`",
"D": "`d [bookmark-id]`",
"DELETE": "`delete [bookmark-id]`",
"B": "`b [[bookmark-id]]`",
"BOOKMARKS": "`bookmarks [[bookmark-id]]`",
"C": "`c [link_id]` or `check [setting]`",
"CHECK": "`check [link_id]` or `check [setting]`",
"H": "`h`",
"HOME": "`home`",
"P": "`p [host]`",
"PURGE": "`purge [host]`",
"Q": "`q`",
"QUIT": "`quit`",
"R": "`r`",
"RELOAD": "`reload`",
"SEARCH": "`search [[keyword(s)...]]`",
"S": "`s [setting] [value]`",
"SET": "`set [setting] [value]`",
"W": "`w [target]`",
"WRITE": "`write [target]`",
"?": "`?`",
"HELP": "`help`",
}

View File

@ -1,4 +1,4 @@
// +build darwin
// This will build for osx without a build tag based on the filename
package http

View File

@ -1,30 +0,0 @@
// +build linux
package http
import (
"fmt"
"os"
"os/exec"
)
// OpenInBrowser checks for the presence of a display server
// and environment variables indicating a gui is present. If found
// then xdg-open is called on a url to open said url in the default
// gui web browser for the system
func OpenInBrowser(url string) (string, error) {
disp := os.Getenv("DISPLAY")
wayland := os.Getenv("WAYLAND_DISPLAY")
_, err := exec.LookPath("Xorg")
if disp == "" && wayland == "" && err != nil {
return "", fmt.Errorf("No gui is available, check 'webmode' setting")
}
// Use start rather than run or output in order
// to release the process and not block
err = exec.Command("xdg-open", url).Start()
if err != nil {
return "", err
}
return "Opened in system default web browser", nil
}

View File

@ -1,11 +1,30 @@
// +build !linux
// +build !darwin
// +build !windows
// +build !darwin,!windows
package http
import "fmt"
import (
"fmt"
"os"
"os/exec"
)
// OpenInBrowser checks for the presence of a display server
// and environment variables indicating a gui is present. If found
// then xdg-open is called on a url to open said url in the default
// gui web browser for the system
func OpenInBrowser(url string) (string, error) {
return "", fmt.Errorf("Unsupported os for 'webmode' 'gui' setting")
disp := os.Getenv("DISPLAY")
wayland := os.Getenv("WAYLAND_DISPLAY")
_, err := exec.LookPath("Xorg")
if disp == "" && wayland == "" && err != nil {
return "", fmt.Errorf("No gui is available, check 'webmode' setting")
}
// Use start rather than run or output in order
// to release the process and not block
err = exec.Command("xdg-open", url).Start()
if err != nil {
return "", err
}
return "Opened in system default web browser", nil
}

View File

@ -1,5 +1,5 @@
// +build windows
// This will only build for windows based on the filename
// no build tag required
package http
import "os/exec"

15
main.go
View File

@ -82,6 +82,14 @@ func validateOpt(opt, val string) bool {
}
return false
}
if opt == "timeout" {
_, err := strconv.Atoi(val)
if err != nil {
return false
}
}
return true
}
@ -126,6 +134,8 @@ func loadConfig() {
bombadillo.Options[lowerkey] = v.Value
if lowerkey == "geminiblocks" {
gemini.BlockBehavior = v.Value
} else if lowerkey == "timeout" {
updateTimeouts(v.Value)
}
} else {
bombadillo.Options[lowerkey] = defaultOptions[lowerkey]
@ -143,8 +153,8 @@ func loadConfig() {
if len(vals) < 2 {
continue
}
ts, err := strconv.ParseInt(vals[1], 10, 64)
now := time.Now()
ts, err := strconv.ParseInt(vals[1], 10, 64)
if err != nil || now.Unix() > ts {
continue
}
@ -158,9 +168,6 @@ func loadConfig() {
func initClient() {
bombadillo = MakeClient(" ((( Bombadillo ))) ")
loadConfig()
if bombadillo.Options["tlscertificate"] != "" && bombadillo.Options["tlskey"] != "" {
bombadillo.Certs.LoadCertificate(bombadillo.Options["tlscertificate"], bombadillo.Options["tlskey"])
}
}
// In the event of specific signals, ensure the display is shown correctly.

View File

@ -130,6 +130,7 @@ func (p *Page) WrapContent(width int, color bool) {
counter += len(spacer)
}
content.WriteRune(ch)
counter++
}
}
}

74
page_test.go Normal file
View File

@ -0,0 +1,74 @@
package main
import (
"reflect"
"testing"
)
func Test_WrapContent_Wrapped_Line_Length(t *testing.T) {
type fields struct {
WrappedContent []string
RawContent string
Links []string
Location Url
ScrollPosition int
FoundLinkLines []int
SearchTerm string
SearchIndex int
FileType string
WrapWidth int
Color bool
}
type args struct {
width int
color bool
}
// create a Url for use by the MakePage function
url, _ := MakeUrl("gemini://rawtext.club")
tests := []struct {
name string
input string
expects []string
args args
}{
{
"Short line that doesn't wrap",
"0123456789\n",
[]string{
"0123456789",
"",
},
args{
10,
false,
},
},
{
"Long line wrapped to 10 columns",
"0123456789 123456789 123456789 123456789 123456789\n",
[]string{
"0123456789",
" 123456789",
" 123456789",
" 123456789",
" 123456789",
"",
},
args{
10,
false,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := MakePage(url, tt.input, []string{""})
p.WrapContent(tt.args.width-1, tt.args.color)
if !reflect.DeepEqual(p.WrappedContent, tt.expects) {
t.Errorf("Test failed - %s\nexpects %s\nactual %s", tt.name, tt.expects, p.WrappedContent)
}
})
}
}