diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..1b02251 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,30 @@ +on: [push, pull_request] +name: Test + +jobs: + test: + strategy: + matrix: + go-version: ['1.13', '1.14', '1.15'] + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + - name: Install make on Windows + if: matrix.os == 'windows-latest' + run: choco install make + - name: Checkout code + uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Test + run: | + go test -race ./... + make diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6af09e9..0000000 --- a/.travis.yml +++ /dev/null @@ -1,35 +0,0 @@ -language: go - -go: - #- "1.11" # Debian Stable golang version, fails - see below - #- "1.12" # Also fails due to progressbar Millisecond requirement - - "1.13" - - "1.14" - - "1.15" - -os: - - linux - - osx - - windows - -before_install: - - if [ "$TRAVIS_OS_NAME" = "windows" ]; then choco install make; fi - -script: - - go test -race ./... - - make - -env: - GO111MODULE=on - -cache: - directories: - - $GOCACHE - - $GOPATH/pkg/mod - -# TODO: GitHub Releases deploy - -notifications: - email: - on_success: never - on_failure: always diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d4592c..4547b95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,23 +5,38 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- Updated [go-gemini](https://github.com/makeworld-the-better-one/go-gemini) to v0.9.1 to support CN-only wildcard certs +- Preformatted text is now grey by default + + +## [1.6.0] - 2020-11-04 ### Added +- **Support client certificates** through config (#112) - `ansi` config setting, to disable ANSI colors in pages (#79, #86) - Edit current URL with e (#87) - If `emoji_favicons` is enabled, new bookmarks will have the domain's favicon prepended (#69, #90) - The `BROWSER` env var is now also checked when opening web links on Unix (#93) +- More accurate error messages based on server response code ### Changed - Disabling the `color` config setting also disables ANSI colors in pages (#79, #86) - Updated [go-isemoji](https://github.com/makeworld-the-better-one/go-isemoji) to v1.1.0 to support Emoji 13.1 for favicons - The web browser code doesn't check for Xorg anymore, just display variables (#93) - Bookmarks can be made to non-gemini URLs (#94) +- Remove pointless directory fallbacks (#101) +- Don't load page from cache when redirected to it (#114) ### Fixed - XDG user dir file is parsed instead of looking for XDG env vars (#97, #100) +- Support paths with spaces in HTTP browser config setting (#77) +- Clicking "Change" on an existing bookmark without changing the text no longer removes it (#91) +- Display HTTP Error if "Open In Portal" fails (#81) +- Support ANSI color codes again, but only in preformatted blocks (#59) +- Make the `..` command work lke it used to in v1.4.0 -## [v1.5.0] - 2020-09-01 +## [1.5.0] - 2020-09-01 ### Added - **Proxy support** - see the `[proxies]` section in the config (#66, #80) - **Emoji favicons** can now be seen if `emoji_favicons` is enabled in the config (#62) diff --git a/README.md b/README.md index 5c27aff..844ed66 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@
Image modified from: amphora by Alvaro Cabrera from the Noun Project
-[![travis build status](https://img.shields.io/travis/com/makeworld-the-better-one/amfora)](https://https://travis-ci.com/github/makeworld-the-better-one/amfora) +[![travis build status](https://img.shields.io/travis/com/makeworld-the-better-one/amfora/master?label=master)](https://travis-ci.com/github/makeworld-the-better-one/amfora) [![go reportcard](https://goreportcard.com/badge/github.com/makeworld-the-better-one/amfora)](https://goreportcard.com/report/github.com/makeworld-the-better-one/amfora) [![license GPLv3](https://img.shields.io/github/license/makeworld-the-better-one/amfora)](https://www.gnu.org/licenses/gpl-3.0.en.html) @@ -42,10 +42,10 @@ Make sure to click "Watch" > "Releases only" in the top right to get notified ab ### Arch Linux -Arch Linux users can install Amfora from AUR. It has the package name `amfora`, and is maintained by @pboyd. +Arch Linux users can install Amfora using pacman. ``` -yay -S amfora +sudo pacman -S amfora ``` ### Homebrew @@ -121,15 +121,15 @@ Features in *italics* are in the master branch, but not in the latest release. - Disabled by default, enable in config - [x] Proxying - Schemes like Gopher or HTTP can be proxied through a Gemini server +- [x] Client certificate support + - [ ] Full client certificate UX within the client + - Create transient and permanent certs within the client, per domain + - Manage and browse them + - Similar to [Kristall](https://github.com/MasterQ32/kristall) + - https://lists.orbitalfox.eu/archives/gemini/2020/001400.html - [x] *Subscribe to RSS and Atom feeds and display them* - Subscribing to page changes, similar to how Spacewalk works, will also be supported - [ ] Stream support -- [ ] Full client certificate UX within the client - - Create transient and permanent certs within the client, per domain - - Manage and browse them - - Similar to [Kristall](https://github.com/MasterQ32/kristall) - - https://lists.orbitalfox.eu/archives/gemini/2020/001400.html -- [ ] Stream support - [ ] Table of contents for pages - [ ] History browser @@ -138,10 +138,19 @@ The config file is written in the intuitive [TOML](https://github.com/toml-lang/ On Windows, the file is in `%APPDATA%\amfora\config.toml`, which usually expands to `C:\Users\\AppData\Roaming\amfora\config.toml`. +## Client Certificates + +Amfora has early support for client certs. Eventually Amfora will be able to generate them itself, but for you can do it by using OpenSSL: + +```shell +openssl req -new -subj "/CN=username" -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -days 1825 -nodes -out cert.pem -keyout key.pem +``` + +This will create a certificate and key file, that can be renamed and moved as you like. See the configuration section above for how to edit your config file to tell Amfora about them. + ## Known Bugs - Pasting on Windows is truncated, the full paste content won't be added. ([#43](https://github.com/makeworld-the-better-one/amfora/issues/43)) -- ANSI codes aren't displaying properly ([#59](https://github.com/makeworld-the-better-one/amfora/issues/59)) You can also check out [all the issues with the bug label](https://github.com/makeworld-the-better-one/amfora/issues?q=is%3Aopen+is%3Aissue+label%3Abug). diff --git a/THANKS.md b/THANKS.md new file mode 100644 index 0000000..0e093f4 --- /dev/null +++ b/THANKS.md @@ -0,0 +1,13 @@ +# THANKS + +Thank you to the following contributors, who have helped make Amfora great. FOSS projects are a community effort, and we would be worse off without you. + +- Sotiris Papatheodorou (@sotpapathe) +- Chloe Kudryavtsev (@CosmicToast) +- Adrian Hesketh (@a-h) +- Jansen Price (@sumpygump) +- Alex Wennerberg (@alexwennerberg) +- Timur Ismagilov (@bouncepaw) +- Matt Caroll (@ohiolab) +- Patryk Niedźwiedziński (@pniedzwiedzinski) +- Trevor Slocum (@tsclocum) \ No newline at end of file diff --git a/amfora.go b/amfora.go index 25e6887..c63eeca 100644 --- a/amfora.go +++ b/amfora.go @@ -10,7 +10,7 @@ import ( ) var ( - version = "1.5.0" + version = "v1.6.0" commit = "unknown" builtBy = "unknown" ) diff --git a/client/client.go b/client/client.go index 4bc1557..2ec65d5 100644 --- a/client/client.go +++ b/client/client.go @@ -2,23 +2,74 @@ package client import ( + "io/ioutil" "net" "net/url" "github.com/makeworld-the-better-one/go-gemini" + "github.com/mitchellh/go-homedir" + "github.com/spf13/viper" ) +var certCache = make(map[string][][]byte) + +func clientCert(host string) ([]byte, []byte) { + if cert := certCache[host]; cert != nil { + return cert[0], cert[1] + } + + // Expand paths starting with ~/ + certPath, err := homedir.Expand(viper.GetString("auth.certs." + host)) + if err != nil { + certPath = viper.GetString("auth.certs." + host) + } + keyPath, err := homedir.Expand(viper.GetString("auth.keys." + host)) + if err != nil { + keyPath = viper.GetString("auth.keys." + host) + } + if certPath == "" && keyPath == "" { + certCache[host] = [][]byte{nil, nil} + return nil, nil + } + + cert, err := ioutil.ReadFile(certPath) + if err != nil { + certCache[host] = [][]byte{nil, nil} + return nil, nil + } + key, err := ioutil.ReadFile(keyPath) + if err != nil { + certCache[host] = [][]byte{nil, nil} + return nil, nil + } + + certCache[host] = [][]byte{cert, key} + return cert, key +} + +// HasClientCert returns whether or not a client certificate exists for a host. +func HasClientCert(host string) bool { + cert, _ := clientCert(host) + return cert != nil +} + // Fetch returns response data and an error. // The error text is human friendly and should be displayed. func Fetch(u string) (*gemini.Response, error) { + parsed, _ := url.Parse(u) + cert, key := clientCert(parsed.Host) - res, err := gemini.Fetch(u) + var res *gemini.Response + var err error + if cert != nil { + res, err = gemini.FetchWithCert(u, cert, key) + } else { + res, err = gemini.Fetch(u) + } if err != nil { return nil, err } - parsed, _ := url.Parse(u) - ok := handleTofu(parsed.Hostname(), parsed.Port(), res.Cert) if !ok { return res, ErrTofu @@ -29,7 +80,16 @@ func Fetch(u string) (*gemini.Response, error) { // FetchWithProxy is the same as Fetch, but uses a proxy. func FetchWithProxy(proxyHostname, proxyPort, u string) (*gemini.Response, error) { - res, err := gemini.FetchWithHost(net.JoinHostPort(proxyHostname, proxyPort), u) + parsed, _ := url.Parse(u) + cert, key := clientCert(parsed.Host) + + var res *gemini.Response + var err error + if cert != nil { + res, err = gemini.FetchWithHostAndCert(net.JoinHostPort(proxyHostname, proxyPort), u, cert, key) + } else { + res, err = gemini.FetchWithHost(net.JoinHostPort(proxyHostname, proxyPort), u) + } if err != nil { return nil, err } diff --git a/config/config.go b/config/config.go index 01746c7..c68a62b 100644 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,7 @@ // Package config initializes all files required for Amfora, even those used by // other packages. It also reads in the config file and initializes a Viper and // the theme +//nolint:golint,goerr113 package config import ( @@ -40,12 +41,13 @@ var bkmkPath string var DownloadsDir string // Feeds - var FeedJSON io.ReadCloser var feedDir string var FeedPath string -//nolint:golint,goerr113 +// Command for opening HTTP(S) URLs in the browser, from "a-general.http" in config. +var HTTPCommand []string + func Init() error { // *** Set paths *** @@ -69,12 +71,7 @@ func Init() error { configDir = amforaAppData } else { // Unix / POSIX system - if basedir.ConfigHome == "" { - // Default to ~/.config/amfora - configDir = filepath.Join(home, ".config", "amfora") - } else { - configDir = filepath.Join(basedir.ConfigHome, "amfora") - } + configDir = filepath.Join(basedir.ConfigHome, "amfora") } configPath = filepath.Join(configDir, "config.toml") @@ -91,12 +88,7 @@ func Init() error { tofuDBDir = amforaAppData } else { // XDG cache dir on POSIX systems - if basedir.CacheHome == "" { - // Default to ~/.cache/amfora - tofuDBDir = filepath.Join(home, ".cache", "amfora") - } else { - tofuDBDir = filepath.Join(basedir.CacheHome, "amfora") - } + tofuDBDir = filepath.Join(basedir.CacheHome, "amfora") } tofuDBPath = filepath.Join(tofuDBDir, "tofu.toml") @@ -106,12 +98,7 @@ func Init() error { bkmkDir = amforaAppData } else { // XDG data dir on POSIX systems - if basedir.DataHome == "" { - // Default to ~/.local/share/amfora - bkmkDir = filepath.Join(home, ".local", "share", "amfora") - } else { - bkmkDir = filepath.Join(basedir.DataHome, "amfora") - } + bkmkDir = filepath.Join(basedir.DataHome, "amfora") } bkmkPath = filepath.Join(bkmkDir, "bookmarks.toml") @@ -282,5 +269,14 @@ func Init() error { cview.Styles.PrimitiveBackgroundColor = GetColor("bg") } // Otherwise it's black by default + // Parse HTTP command + HTTPCommand = viper.GetStringSlice("a-general.http") + if len(HTTPCommand) == 0 { + // Not a string array, interpret as a string instead + // Split on spaces to maintain compatibility with old versions + // The new better way to is to just define a string array in config + HTTPCommand = strings.Fields(viper.GetString("a-general.http")) + } + return nil } diff --git a/config/default.go b/config/default.go index 11cc1ef..6329539 100644 --- a/config/default.go +++ b/config/default.go @@ -21,10 +21,20 @@ home = "gemini://gemini.circumlunar.space" # If set to false, a prompt will be shown before following redirects. auto_redirect = false -# What command to run to open a HTTP(S) URL. Set to "default" to try to guess the browser, -# or set to "off" to not open HTTP(S) URLs. +# What command to run to open a HTTP(S) URL. +# Set to "default" to try to guess the browser, or set to "off" to not open HTTP(S) URLs. # If a command is set, than the URL will be added (in quotes) to the end of the command. -# A space will be prepended if necessary. +# A space will be prepended to the URL. +# +# The best to define a command is using a string array. +# Examples: +# http = ["firefox"] +# http = ["custom-browser", "--flag", "--option=2"] +# http = ["/path/with spaces/in it/firefox"] +# +# Using just a string will also work, but it is deprecated, +# and will degrade if you use paths with spaces. + http = "default" # Any URL that will accept a query string can be put here @@ -33,7 +43,7 @@ search = "gemini://gus.guru/search" # Whether colors will be used in the terminal color = true -# Whether ANSI codes from the page content should be rendered +# Whether ANSI color codes from the page content should be rendered ansi = true # Whether to replace list asterisks with unicode bullets @@ -59,6 +69,20 @@ page_max_time = 10 emoji_favicons = false +[auth] +# Authentication settings + +[auth.certs] +# Client certificates +# Set domain name equal to path to client cert +# "example.com" = "mycert.crt" + +[auth.keys] +# Client certificate keys +# Set domain name equal to path to key for the client cert above +# "example.com" = "mycert.key" + + [keybindings] # In the future there will be more settings here. diff --git a/config/theme.go b/config/theme.go index 7de75f9..6559864 100644 --- a/config/theme.go +++ b/config/theme.go @@ -58,7 +58,7 @@ var theme = map[string]tcell.Color{ "link_number": tcell.ColorSilver, "regular_text": tcell.ColorWhite, "quote_text": tcell.ColorWhite, - "preformatted_text": tcell.ColorWhite, + "preformatted_text": tcell.ColorGrey, "list_text": tcell.ColorWhite, } diff --git a/contrib/themes/nord.toml b/contrib/themes/nord.toml new file mode 100644 index 0000000..ec2bf7b --- /dev/null +++ b/contrib/themes/nord.toml @@ -0,0 +1,111 @@ +[theme] +# This section is for changing the COLORS used in Amfora. +# These colors only apply if 'color' is enabled above. +# Colors can be set using a W3C color name, or a hex value such as "#ffffff". + +# Note that not all colors will work on terminals that do not have truecolor support. +# If you want to stick to the standard 16 or 256 colors, you can get +# a list of those here: https://jonasjacek.github.io/colors/ +# DO NOT use the names from that site, just the hex codes. + +# Definitions: +# bg = background +# fg = foreground +# dl = download +# btn = button +# hdg = heading +# bkmk = bookmark +# modal = a popup window/box in the middle of the screen + +# EXAMPLES: +# hdg_1 = "green" +# hdg_2 = "#5f0000" + +# Available keys to set: + +# bg: background for pages, tab row, app in general +# tab_num: The number/highlight of the tabs at the top +# tab_divider: The color of the divider character between tab numbers: | +# bottombar_label: The color of the prompt that appears when you press space +# bottombar_text: The color of the text you type +# bottombar_bg +bg = "#2e3440" +fg = "#eceff4" +tab_num = "#88c0d0" +tab_divider = "#eceff4" +bottombar_bg = "#3b4252" +bottombar_text = "#eceff4" +bottombar_label = "#88c0d0" + +# hdg_1 +# hdg_2 +# hdg_3 +# amfora_link: A link that Amfora supports viewing. For now this is only gemini:// +# foreign_link: HTTP(S), Gopher, etc +# link_number: The silver number that appears to the left of a link +# regular_text: Normal gemini text, and plaintext documents +# quote_text +# preformatted_text +# list_text +hdg_1 = "#5e81ac" +hdg_2 = "#81a1c1" +hdg_3 = "#8fbcbb" +amfora_link = "#88c0d0" +foreign_link = "#b48ead" +link_number = "#a3be8c" +regular_text = "#eceff4" +quote_text = "#8fbcbb" +preformatted_text = "#eceff4" +list_text = "#eceff4" + +# btn_bg: The bg color for all modal buttons +# btn_text: The text color for all modal buttons +btn_bg = "#4c566a" +btn_text = "#eceff4" + +# dl_choice_modal_bg +# dl_choice_modal_text +# dl_modal_bg +# dl_modal_text +# info_modal_bg +# info_modal_text +# error_modal_bg +# error_modal_text +# yesno_modal_bg +# yesno_modal_text +# tofu_modal_bg +# tofu_modal_text + +dl_choice_modal_bg = "#3b4252" +dl_choice_modal_text = "#eceff4" +dl_modal_bg = "#3b4252" +dl_modal_text = "#eceff4" +info_modal_bg = "#3b4252" +info_modal_text = "#eceff4" +error_modal_bg = "#bf616a" +error_modal_text = "#2e3440" +yesno_modal_bg = "#3b4252" +yesno_modal_text = "#eceff4" +tofu_modal_bg = "#3b4252" +tofu_modal_text = "#eceff4" + +# input_modal_bg +# input_modal_text +# input_modal_field_bg: The bg of the input field, where you type the text +# input_modal_field_text: The color of the text you type +input_modal_bg = "#3b4252" +input_modal_text = "#eceff4" +input_modal_field_bg = "#4c566a" +input_modal_field_text ="#eceff4" + +# bkmk_modal_bg +# bkmk_modal_text +# bkmk_modal_label +# bkmk_modal_field_bg +# bkmk_modal_field_text + +bkmk_modal_bg = "#3b4252" +bkmk_modal_text = "#eceff4" +bkmk_modal_label = "#88c0d0" +bkmk_modal_field_bg = "#4c566a" +bkmk_modal_field_text = "#eceff4" diff --git a/default-config.toml b/default-config.toml index 4242bb1..9ac0d21 100644 --- a/default-config.toml +++ b/default-config.toml @@ -18,10 +18,20 @@ home = "gemini://gemini.circumlunar.space" # If set to false, a prompt will be shown before following redirects. auto_redirect = false -# What command to run to open a HTTP(S) URL. Set to "default" to try to guess the browser, -# or set to "off" to not open HTTP(S) URLs. +# What command to run to open a HTTP(S) URL. +# Set to "default" to try to guess the browser, or set to "off" to not open HTTP(S) URLs. # If a command is set, than the URL will be added (in quotes) to the end of the command. -# A space will be prepended if necessary. +# A space will be prepended to the URL. +# +# The best to define a command is using a string array. +# Examples: +# http = ["firefox"] +# http = ["custom-browser", "--flag", "--option=2"] +# http = ["/path/with spaces/in it/firefox"] +# +# Using just a string will also work, but it is deprecated, +# and will degrade if you use paths with spaces. + http = "default" # Any URL that will accept a query string can be put here @@ -30,7 +40,7 @@ search = "gemini://gus.guru/search" # Whether colors will be used in the terminal color = true -# Whether ANSI codes from the page content should be rendered +# Whether ANSI color codes from the page content should be rendered ansi = true # Whether to replace list asterisks with unicode bullets @@ -56,6 +66,20 @@ page_max_time = 10 emoji_favicons = false +[auth] +# Authentication settings + +[auth.certs] +# Client certificates +# Set domain name equal to path to client cert +# "example.com" = "mycert.crt" + +[auth.keys] +# Client certificate keys +# Set domain name equal to path to key for the client cert above +# "example.com" = "mycert.key" + + [keybindings] # In the future there will be more settings here. diff --git a/display/bookmarks.go b/display/bookmarks.go index 017c19d..8b0fe9a 100644 --- a/display/bookmarks.go +++ b/display/bookmarks.go @@ -88,7 +88,7 @@ func openBkmkModal(name string, exists bool, favicon string) (string, int) { if favicon != "" && !exists { name = favicon + " " + name } - bkmkModalText = "" + bkmkModalText = name bkmkModal.GetForm().AddInputField("Name: ", name, 0, nil, func(text string) { // Store for use later diff --git a/display/display.go b/display/display.go index 0f08676..d2b6921 100644 --- a/display/display.go +++ b/display/display.go @@ -133,6 +133,13 @@ func Init() { // This shouldn't occur return } + + if query == ".." && tabs[tab].page.URL[len(tabs[tab].page.URL)-1] != '/' { + // Support what ".." used to work like + // If on /dir/doc.gmi, got to /dir/ + query = "./" + } + target, err := current.Parse(query) if err != nil { // Invalid relative url diff --git a/display/download.go b/display/download.go index a150166..6a30b4e 100644 --- a/display/download.go +++ b/display/download.go @@ -117,10 +117,12 @@ func dlChoice(text, u string, resp *gemini.Response) { portalURL = parsed.String() + "%3F" + query } portalURL = strings.TrimPrefix(portalURL, "gemini://") + "?raw=1" - handleHTTP("https://portal.mozz.us/gemini/"+portalURL, false) - tabPages.SwitchToPage(strconv.Itoa(curTab)) - App.SetFocus(tabs[curTab].view) - App.Draw() + ok := handleHTTP("https://portal.mozz.us/gemini/"+portalURL, false) + if ok { + tabPages.SwitchToPage(strconv.Itoa(curTab)) + App.SetFocus(tabs[curTab].view) + App.Draw() + } return } tabPages.SwitchToPage(strconv.Itoa(curTab)) diff --git a/display/private.go b/display/private.go index d2a341b..8686fd7 100644 --- a/display/private.go +++ b/display/private.go @@ -20,7 +20,6 @@ import ( "github.com/makeworld-the-better-one/go-gemini" "github.com/makeworld-the-better-one/go-isemoji" "github.com/spf13/viper" - "gitlab.com/tslocum/cview" ) // This file contains the functions that aren't part of the public API. @@ -145,26 +144,42 @@ func setPage(t *tab, p *structs.Page) { // handleHTTP is used by handleURL. // It opens HTTP links and displays Info and Error modals. -func handleHTTP(u string, showInfo bool) { - switch strings.TrimSpace(viper.GetString("a-general.http")) { - case "", "off": - Error("HTTP Error", "Opening HTTP URLs is turned off.") - case "default": - s, err := webbrowser.Open(u) - if err != nil { - Error("Webbrowser Error", err.Error()) - } else if showInfo { - Info(s) - } - default: - // The config has a custom command to execute for HTTP URLs - fields := strings.Fields(viper.GetString("a-general.http")) - err := exec.Command(fields[0], append(fields[1:], u)...).Start() - if err != nil { - Error("HTTP Error", "Error executing custom browser command: "+err.Error()) +// Returns false if there was an error. +func handleHTTP(u string, showInfo bool) bool { + if len(config.HTTPCommand) == 1 { + // Possibly a non-command + + switch strings.TrimSpace(config.HTTPCommand[0]) { + case "", "off": + Error("HTTP Error", "Opening HTTP URLs is turned off.") + return false + case "default": + s, err := webbrowser.Open(u) + if err != nil { + Error("Webbrowser Error", err.Error()) + return false + } + if showInfo { + Info(s) + } + return true } } + + // Custom command + var err error = nil + if len(config.HTTPCommand) > 1 { + err = exec.Command(config.HTTPCommand[0], append(config.HTTPCommand[1:], u)...).Start() + } else { + err = exec.Command(config.HTTPCommand[0], u).Start() + } + if err != nil { + Error("HTTP Error", "Error executing custom browser command: "+err.Error()) + return false + } + App.Draw() + return true } // handleOther is used by handleURL. @@ -367,11 +382,14 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { // Gemini URL, or one with a Gemini proxy available - // Load page from cache if possible - page, ok := cache.GetPage(u) - if ok { - setPage(t, page) - return ret(u, true) + // Load page from cache if it exists, + // and this isn't a page that was redirected to by the server (indicates dynamic content) + if numRedirects == 0 { + page, ok := cache.GetPage(u) + if ok { + setPage(t, page) + return ret(u, true) + } } // Otherwise download it bottomBar.SetText("Loading...") @@ -449,7 +467,12 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { } page.Width = termW - go cache.AddPage(page) + + if !client.HasClientCert(parsed.Host) { + // Don't cache pages with client certs + go cache.AddPage(page) + } + setPage(t, page) return ret(u, true) } @@ -457,8 +480,8 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { // Could be a non 20 (or 21) status code, or a different kind of document // Handle each status code - switch gemini.SimplifyStatus(res.Status) { - case 10: + switch res.Status { + case 10, 11: userInput, ok := Input(res.Meta) if ok { // Make another request with the query string added @@ -471,7 +494,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { return ret(handleURL(t, parsed.String(), 0)) } return ret("", false) - case 30: + case 30, 31: parsedMeta, err := url.Parse(res.Meta) if err != nil { Error("Redirect Error", "Invalid URL: "+err.Error()) @@ -497,15 +520,46 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) { } return ret("", false) case 40: - Error("Temporary Failure", cview.Escape(res.Meta)) + Error("Temporary Failure", escapeMeta(res.Meta)) + return ret("", false) + case 41: + Error("Server Unavailable", escapeMeta(res.Meta)) + return ret("", false) + case 42: + Error("CGI Error", escapeMeta(res.Meta)) + return ret("", false) + case 43: + Error("Proxy Failure", escapeMeta(res.Meta)) + return ret("", false) + case 44: + Error("Slow Down", "You should wait "+escapeMeta(res.Meta)+" seconds before making another request.") return ret("", false) case 50: - Error("Permanent Failure", cview.Escape(res.Meta)) + Error("Permanent Failure", escapeMeta(res.Meta)) + return ret("", false) + case 51: + Error("Not Found", escapeMeta(res.Meta)) + return ret("", false) + case 52: + Error("Gone", escapeMeta(res.Meta)) + return ret("", false) + case 53: + Error("Proxy Request Refused", escapeMeta(res.Meta)) + return ret("", false) + case 59: + Error("Bad Request", escapeMeta(res.Meta)) return ret("", false) case 60: - Info("The server requested a certificate. Cert handling is coming to Amfora soon!") + Error("Client Certificate Required", escapeMeta(res.Meta)) + return ret("", false) + case 61: + Error("Certificate Not Authorised", escapeMeta(res.Meta)) + return ret("", false) + case 62: + Error("Certificate Not Valid", escapeMeta(res.Meta)) return ret("", false) } + // Status code 20, but not a document that can be displayed go dlChoice("That file could not be displayed. What would you like to do?", u, res) return ret("", false) diff --git a/display/util.go b/display/util.go index e03fdd6..4ea8e91 100644 --- a/display/util.go +++ b/display/util.go @@ -3,12 +3,19 @@ package display import ( "errors" "net/url" + "strings" "github.com/spf13/viper" + "gitlab.com/tslocum/cview" ) // This file contains funcs that are small, self-contained utilities. +// escapeMeta santizes a META string for use within a cview modal. +func escapeMeta(meta string) string { + return cview.Escape(strings.ReplaceAll(meta, "\n", "")) +} + // isValidTab indicates whether the passed tab is still being used, even if it's not currently displayed. func isValidTab(t *tab) bool { tempTabs := tabs diff --git a/go.mod b/go.mod index bc129f0..4925424 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/gdamore/tcell v1.3.1-0.20200608133353-cb1e5d6fa606 github.com/google/go-cmp v0.5.0 // indirect - github.com/makeworld-the-better-one/go-gemini v0.8.4 + github.com/makeworld-the-better-one/go-gemini v0.9.1 github.com/makeworld-the-better-one/go-isemoji v1.1.0 github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20200710151429-125743e22b4f github.com/mitchellh/go-homedir v1.1.0 diff --git a/go.sum b/go.sum index d8f5d5d..4affab9 100644 --- a/go.sum +++ b/go.sum @@ -130,8 +130,8 @@ github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tW github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/makeworld-the-better-one/go-gemini v0.8.4 h1:ntsQ9HnlJCmC9PDqXp/f1SCALjBMwh69BbT4BhFRFaw= -github.com/makeworld-the-better-one/go-gemini v0.8.4/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4= +github.com/makeworld-the-better-one/go-gemini v0.9.1 h1:/Vc6Y4Y1aOi4lZIBA1wDe+4N2xAI8EQ0CIjip2NUQkk= +github.com/makeworld-the-better-one/go-gemini v0.9.1/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4= github.com/makeworld-the-better-one/go-isemoji v1.1.0 h1:wZBHOKB5zAIgaU2vaWnXFDDhatebB8TySrNVxjVV84g= github.com/makeworld-the-better-one/go-isemoji v1.1.0/go.mod h1:FBjkPl9rr0G4vlZCc+Mr+QcnOfGCTbGWYW8/1sp06I0= github.com/makeworld-the-better-one/progressbar/v3 v3.3.5-0.20200710151429-125743e22b4f h1:YEUlTs5gb35UlBLTgqrub9axWTYB3d7/8TxrkJDZpRI= diff --git a/renderer/renderer.go b/renderer/renderer.go index 3b1e6eb..8f41fdd 100644 --- a/renderer/renderer.go +++ b/renderer/renderer.go @@ -80,17 +80,6 @@ func wrapLine(line string, width int, prefix, suffix string, includeFirst bool) return ret } -// tagLines splits a string into lines and adds a the given -// string to the start and another to the end. -// It is used for adding cview color tags. -func tagLines(s, start, end string) string { - lines := strings.Split(s, "\n") - for i := range lines { - lines[i] = start + lines[i] + end - } - return strings.Join(lines, "\n") -} - // convertRegularGemini converts non-preformatted blocks of text/gemini // into a cview-compatible format. // Since this only works on non-preformatted blocks, RenderGemini @@ -283,11 +272,6 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (string, // If it's not a gemini:// page, set this to true. func RenderGemini(s string, width, leftMargin int, proxied bool) (string, []string) { s = cview.Escape(s) - if viper.GetBool("a-general.color") && viper.GetBool("a-general.ansi") { - s = cview.TranslateANSI(s) - } else { - s = ansiRegex.ReplaceAllString(s, "") - } lines := strings.Split(s, "\n") @@ -302,13 +286,22 @@ func RenderGemini(s string, width, leftMargin int, proxied bool) (string, []stri if pre { // In a preformatted block, so add the text as is // Don't add the current line with backticks - rendered += tagLines( - buf, - fmt.Sprintf("[%s]", config.GetColorString("preformatted_text")), - "[-]", - ) + + // Support ANSI color codes in preformatted blocks - see #59 + if viper.GetBool("a-general.color") && viper.GetBool("a-general.ansi") { + buf = cview.TranslateANSI(buf) + } else { + buf = ansiRegex.ReplaceAllString(buf, "") + } + + rendered += fmt.Sprintf("[%s]", config.GetColorString("preformatted_text")) + + buf + "[-]" } else { // Not preformatted, regular text + + // ANSI not allowed in regular text - see #59 + buf = ansiRegex.ReplaceAllString(buf, "") + ren, lks := convertRegularGemini(buf, len(links), width, proxied) links = append(links, lks...) rendered += ren @@ -323,10 +316,21 @@ func RenderGemini(s string, width, leftMargin int, proxied bool) (string, []stri // Gone through all the lines, but there still is likely a block in the buffer if pre { // File ended without closing the preformatted block - rendered += buf + // Same code as in the loop above + + if viper.GetBool("a-general.color") && viper.GetBool("a-general.ansi") { + buf = cview.TranslateANSI(buf) + } else { + buf = ansiRegex.ReplaceAllString(buf, "") + } + rendered += fmt.Sprintf("[%s]", config.GetColorString("preformatted_text")) + + buf + "[-]" } else { // Not preformatted, regular text // Same code as in the loop above + + buf = ansiRegex.ReplaceAllString(buf, "") + ren, lks := convertRegularGemini(buf, len(links), width, proxied) links = append(links, lks...) rendered += ren