forked from sloum/bombadillo
Compare commits
26 Commits
master
...
release2.3
Author | SHA1 | Date |
---|---|---|
Sloom Sloum Sluom IV | 0e2b80626e | |
Sloom Sloum Sluom IV | 99fb1ce20b | |
Sloom Sloum Sluom IV | a346d78c7b | |
Sloom Sloum Sluom IV | 44dc3f5029 | |
Sloom Sloum Sluom IV | f8a9465eaf | |
Sloom Sloum Sluom IV | 6308d3e761 | |
Sloom Sloum Sluom IV | 3dcd0c5e13 | |
Sloom Sloum Sluom IV | 3d85b43732 | |
Sloom Sloum Sluom IV | 688a0b3ef2 | |
sloum | 328d16a191 | |
Sloom Sloum Sluom IV | 953ae8e28f | |
sloum | df4f09a6bb | |
Sloom Sloum Sluom IV | d7a65e6679 | |
Sloom Sloum Sluom IV | b6207c7b96 | |
Sloom Sloum Sluom IV | 21e87706aa | |
Sloom Sloum Sluom IV | e3a0292638 | |
Sloom Sloum Sluom IV | 4ac6c8c2e5 | |
Sloom Sloum Sluom IV | df3b03c661 | |
Sloom Sloum Sluom IV | 6b2ccc5ad4 | |
Sloom Sloum Sluom IV | baf7932ab5 | |
Sloom Sloum Sluom IV | 81457d8406 | |
Sloom Sloum Sluom IV | 185c62f272 | |
Sloom Sloum Sluom IV | 80bdbb642d | |
asdf | d2c8af2a08 | |
asdf | 57b3afd4d7 | |
asdf | 980f236d84 |
12
bombadillo.1
12
bombadillo.1
|
@ -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.
|
||||
|
||||
|
|
28
client.go
28
client.go
|
@ -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")
|
||||
|
@ -451,10 +451,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()
|
||||
|
@ -999,6 +999,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 +1202,16 @@ func findAvailableFileName(fpath, fname string) (string, error) {
|
|||
|
||||
return savePath, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -54,8 +54,7 @@ var defaultOptions = map[string]string{
|
|||
"showimages": "true",
|
||||
"telnetcommand": "telnet",
|
||||
"theme": "normal", // "normal", "inverted", "color"
|
||||
"tlscertificate": "",
|
||||
"tlskey": "",
|
||||
"timeout": "15", // connection timeout for gopher/gemini in seconds
|
||||
"webmode": "none", // "none", "gui", "lynx", "w3m", "elinks"
|
||||
}
|
||||
|
||||
|
|
|
@ -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)}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// +build darwin
|
||||
// This will build for osx without a build tag based on the filename
|
||||
|
||||
package http
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
15
main.go
|
@ -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.
|
||||
|
|
1
page.go
1
page.go
|
@ -130,6 +130,7 @@ func (p *Page) WrapContent(width int, color bool) {
|
|||
counter += len(spacer)
|
||||
}
|
||||
content.WriteRune(ch)
|
||||
counter++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue