From b28d00f8f04c2b2a71ba49468939c76a5d71c3b2 Mon Sep 17 00:00:00 2001 From: Case Duckworth Date: Thu, 4 Jun 2020 20:14:05 -0500 Subject: Add gopher support; clean up --- bollux | 375 +++++++++++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 271 insertions(+), 104 deletions(-) diff --git a/bollux b/bollux index c8a6175..9f5e2bb 100755 --- a/bollux +++ b/bollux @@ -2,14 +2,11 @@ # bollux: a bash gemini client # Author: Case Duckworth # License: MIT -# Version: 0.3 +# Version: 0.4.0 # Program information PRGN="${0##*/}" -VRSN=0.3 -# State -REDIRECTS=0 -set -f +VRSN=0.4.0 bollux_usage() { cat <&2 '\e[%sm%s:\e[0m\t%s\n' "$fmt" "$PRGN" "$*" } # main entry point bollux() { - run bollux_config - run bollux_args "$@" - run history_init + run bollux_config # TODO: figure out better config method + run bollux_args "$@" # and argument parsing + run bollux_init if [[ ! "${BOLLUX_URL:+x}" ]]; then run prompt GO BOLLUX_URL @@ -105,6 +105,7 @@ bollux_config() { fi ## behavior + : "${BOLLUX_TIMEOUT:=30}" # connection timeout : "${BOLLUX_MAXREDIR:=5}" # max redirects : "${BOLLUX_PORT:=1965}" # port number : "${BOLLUX_PROTO:=gemini}" # default protocol @@ -142,12 +143,12 @@ set_title() { } prompt() { # prompt [-u] PROMPT [READ_ARGS...] - read_cmd=(read -e -r) + local read_cmd=(read -e -r) if [[ "$1" == "-u" ]]; then read_cmd+=(-i "$BOLLUX_URL") shift fi - prompt="$1" + local prompt="$1" shift read_cmd+=(-p "$prompt> ") "${read_cmd[@]}" /dev/null; then + run "${proto}_request" "$url" + else + log d "No request handler for '$proto'; trying gemini" + run gemini_request "$url" + fi + } | run normalize | + { + if declare -Fp "${proto}_response" >/dev/null; then + run "${proto}_response" "$url" + else + log d "No response handler for '$proto'; handling raw response" + raw_response + fi + } } transform_resource() { # transform_resource BASE_URL REFERENCE_URL - declare -A R B T # reference, base url, target + local -A R B T # reference, base url, target eval "$(run parse_url B "$1")" eval "$(run parse_url R "$2")" # A non-strict parser may ignore a scheme in the reference @@ -223,7 +236,7 @@ transform_resource() { # transform_resource BASE_URL REFERENCE_URL fi isdefined R[fragment] && T[fragment]="${R[fragment]}" # cf. 5.3 -- recomposition - local r="" + local r isdefined "T[scheme]" && r="$r${T[scheme]}:" # remove the port from the authority isdefined "T[authority]" && r="$r//${T[authority]%:*}" @@ -235,9 +248,9 @@ transform_resource() { # transform_resource BASE_URL REFERENCE_URL merge_paths() { # 5.2.3 # shellcheck disable=2034 - B_authority="$1" - B_path="$2" - R_path="$3" + local B_authority="$1" + local B_path="$2" + local R_path="$3" # if R_path is empty, get rid of // in B_path if [[ -z "$R_path" ]]; then printf '%s\n' "${B_path//\/\//\//}" @@ -258,8 +271,7 @@ merge_paths() { # 5.2.3 remove_dot_segments() { # 5.2.4 local input="$1" - local output= - # ^/\.(/|$) - BASH_REMATCH[0] + local output while [[ "$input" ]]; do if [[ "$input" =~ ^\.\.?/ ]]; then input="${input#${BASH_REMATCH[0]}}" @@ -306,40 +318,54 @@ parse_url() { # eval "$(split_url NAME STRING)" => NAME[...] isdefined() { [[ "${!1+x}" ]]; } # isdefined NAME # is a NAME defined AND empty? isempty() { [[ ! "${!1-x}" ]]; } # isempty NAME +# split a string -- see pure bash bible +split() { # split STRING DELIMITER + local -a arr + IFS=$'\n' read -d "" -ra arr <<<"${1//$2/$'\n'}" + printf '%s\n' "${arr[@]}" +} + +# GEMINI +# https://gemini.circumlunar.space/docs/spec-spec.txt +gemini_request() { + local url port server + local ssl_cmd + url="$1" + port=1965 + server="${url#*://}" + server="${server%%/*}" -request_url() { - local server="$1" - local port="$2" - local url="$3" - - # support for TLS v1.3 and v1.2 ssl_cmd=(openssl s_client -crlf -quiet -connect "$server:$port") - ssl_cmd+=(-servername "$server") # SNI - ssl_cmd_tls1_2=("${ssl_cmd[@]}" -tls1_2) - ssl_cmd_tls1_3=("${ssl_cmd[@]}" -tls1_3) + # disable old TLS/SSL versions (thanks makeworld!) + ssl_cmd+=(-no_ssl3 -no_tls1 -no_tls1_1) # always try to connect with TLS v1.3 first - run "${ssl_cmd_tls1_3[@]}" <<<"$url" 2>/dev/null || - run "${ssl_cmd_tls1_2[@]}" <<<"$url" 2>/dev/null + run "${ssl_cmd[@]}" <<<"$url" 2>/dev/null } -handle_response() { - local URL="$1" code meta +gemini_response() { + local url code meta + local title + url="$1" + + # we need a loop here so it waits for the first line + while read -t "3" -r code meta || + { (($? > 128)) && die 99 "Timeout."; }; do + break + done - read -r code meta log d "[$code] $meta" case "$code" in - 1*) + 1*) # input REDIRECTS=0 - run history_append "$URL" "$meta" run prompt "$meta" run blastoff "?$REPLY" ;; - 2*) + 2*) # OK REDIRECTS=0 # read ahead to find a title - pretitle= + local pretitle while read -r; do pretitle="$pretitle$REPLY"$'\n' if [[ "$REPLY" =~ ^#[[:space:]]*(.*) ]]; then @@ -347,59 +373,194 @@ handle_response() { break fi done - run history_append "$URL" "${title:-}" + run history_append "$url" "${title:-}" + # read the body out and pipe it to display { printf '%s' "$pretitle" - while read -r; do - printf '%s\n' "$REPLY" - done - } | run display "$meta" + passthru + } | run display "$meta" "${title:-}" ;; - 3*) + 3*) # redirect ((REDIRECTS += 1)) if ((REDIRECTS > BOLLUX_MAXREDIR)); then die $((100 + code)) "Too many redirects!" fi - run blastoff "$meta" + run blastoff "$meta" # TODO: confirm redirect ;; - 4*) + 4*) # temporary error REDIRECTS=0 - die "$((100 + code))" "Temporary error: $code" + die "$((100 + code))" "Temporary error [$code]: $meta" ;; - 5*) + 5*) # permanent error REDIRECTS=0 - die "$((100 + code))" "Permanent error: $code" + die "$((100 + code))" "Permanent error [$code]: $meta" ;; - 6*) + 6*) # certificate error REDIRECTS=0 - die "$((100 + code))" "Certificate error: $code" + log d "Not implemented: Client certificates" + # TODO: recheck the speck + die "$((100 + code))" "[$code] $meta" ;; *) [[ -z "${code-}" ]] && die 100 "Empty response code." - die "$((100 + code)) Unknown response code: $code." + die "$((100 + code))" "Unknown response code: $code." + ;; + esac +} + +# GOPHER +# https://tools.ietf.org/html/rfc1436 protocol +# https://tools.ietf.org/html/rfc4266 url +gopher_request() { + local url server port type path + url="$1" + port=70 + + # RFC 4266 + [[ "$url" =~ gopher://([^/?#:]*)(:([0-9]+))?(/((.))?(/?.*))?$ ]] + server="${BASH_REMATCH[1]}" + port="${BASH_REMATCH[3]:-70}" + type="${BASH_REMATCH[6]:-1}" + path="${BASH_REMATCH[7]}" + + log d "URL='$url' SERVER='$server' TYPE='$type' PATH='$path'" + + exec 9<>"/dev/tcp/$server/$port" + printf '%s\r\n' "$path" >&9 + passthru <&9 +} + +gopher_response() { + local url pre type cur_server + pre=false + url="$1" + # RFC 4266 + [[ "$url" =~ gopher://([^/?#:]*)(:([0-9]+))?(/((.))?(/?.*))?$ ]] + cur_server="${BASH_REMATCH[1]}" + type="${BASH_REMATCH[6]:-1}" + + run history_append "$url" "" # TODO: get the title ?? + + log d "TYPE='$type'" + + case "$type" in + 0) # text + run display text/plain + ;; + 1) # menu + run gopher_convert | run display text/gemini + ;; + 3) # failure + die 203 "GOPHER: failed" + ;; + 7) # search + die 207 "Not implemented" + ;; + *) # something else + die "$((200 + ${type:-0}))" "Not implemented" ;; esac } -display() { +passthru() { + while IFS= read -r; do + printf '%s\n' "$REPLY" + done +} + +gopher_convert() { + local type label path server port regex + # cf. https://github.com/jamestomasino/dotfiles-minimal/blob/master/bin/gophermap2gemini.awk + while IFS= read -r; do + printf -v regex '(.)([^\t]*)(\t([^\t]*)\t([^\t]*)\t([^\t]*))?' + if [[ "$REPLY" =~ $regex ]]; then + type="${BASH_REMATCH[1]}" + label="${BASH_REMATCH[2]}" + path="${BASH_REMATCH[4]:-/}" + server="${BASH_REMATCH[5]:-$cur_server}" + port="${BASH_REMATCH[6]}" + else + log e "CAN'T PARSE LINE" + printf '%s\n' "$REPLY" + continue + fi + case "$type" in + .) # end of file + printf '.\n' + break + ;; + i) # label + case "$label" in + '#'* | '*'[[:space:]]*) + if $pre; then + printf '%s\n' '```' + pre=false + fi + ;; + *) + if ! $pre; then + printf '%s\n' '```' + pre=true + fi + ;; + esac + printf '%s\n' "$label" + ;; + h) # html link + if $pre; then + printf '%s\n' '```' + pre=false + fi + printf '=> %s %s\n' "${path:4}" "$label" + ;; + T) # telnet link + if $pre; then + printf '%s\n' '```' + pre=false + fi + printf '=> telnet://%s:%s/%s%s %s\n' \ + "$server" "$port" "$type" "$path" "$label" + ;; + *) # other type + if $pre; then + printf '%s\n' '```' + pre=false + fi + printf '=> gopher://%s:%s/%s%s %s\n' \ + "$server" "$port" "$type" "$path" "$label" + ;; + esac + done + if $pre; then + printf '%s\n' '```' + fi + # close the connection + exec 9<&- + exec 9>&- +} + +display() { # display METADATA [TITLE] + local -a less_cmd + local i mime charset # split header line local -a hdr - local i mime charset h - IFS=$'\n' read -d "" -ra hdr <<<"${1//;/$'\n'}" + IFS=';' read -ra hdr <<<"$1" + # title is optional but nice looking + local title + if (($# == 2)); then + title="$2" + fi mime="$(trim "${hdr[0],,}")" for ((i = 1; i <= "${#hdr[@]}"; i++)); do - h="$(trim "${hdr[$i]}")" + h="${hdr[$i]}" case "$h" in - charset=*) charset="${h#charset=}" ;; - # add mime-extensions here + *charset=*) charset="${h#*=}" ;; esac done [[ -z "$mime" ]] && mime="text/gemini" - if [[ -z "$charset" ]]; then - charset="utf-8" - fi + [[ -z "$charset" ]] && charset="utf-8" log debug "mime='$mime'; charset='$charset'" @@ -409,28 +570,25 @@ display() { less_cmd=(less -R) mklesskey "$BOLLUX_LESSKEY" && less_cmd+=(-k "$BOLLUX_LESSKEY") less_cmd+=( - -Pm'bollux$' + -Pm"$title${title:+ - }bollux$" -PM'o\:open, g\:goto, [\:back, ]\:forward, r\:refresh$' - -M + -m ) - submime="${mime#*/}" - if declare -F | grep -q "$submime"; then - log d "typeset_$submime" - { - iconv -f "${charset^^}" -t "UTF-8" | - tee "$BOLLUX_PAGESRC" | - run "typeset_$submime" | - run "${less_cmd[@]}" && bollux_quit - } || run handle_keypress "$?" + local typeset + local submime="${mime#*/}" + if declare -Fp "typeset_$submime" >/dev/null; then + typeset="typeset_$submime" else - log "cat" - { - iconv -f "${charset^^}" -t "UTF-8" | - tee "$BOLLUX_PAGESRC" | - run "${less_cmd[@]}" && bollux_quit - } || run handle_keypress "$?" + typeset="passthru" fi + + { + run iconv -f "${charset^^}" -t "UTF-8" | + run tee "$BOLLUX_PAGESRC" | + run "$typeset" | + run "${less_cmd[@]}" && bollux_quit + } || run handle_keypress "$?" ;; *) run download "$BOLLUX_URL" ;; esac @@ -450,7 +608,7 @@ mklesskey() { END } -normalize_crlf() { +normalize() { shopt -s extglob while IFS= read -r; do printf '%s\n' "${REPLY//$'\r'?($'\n')/}" @@ -480,7 +638,7 @@ typeset_gemini() { while IFS= read -r; do case "$REPLY" in - '```' | '```'*) + '```'*) if $pre; then pre=false else @@ -493,12 +651,8 @@ typeset_gemini() { gemini_link "$REPLY" $pre "$ln" ;; '#'*) gemini_header "$REPLY" $pre ;; - '*'*) - if [[ "$REPLY" =~ ^\*[[:space:]]+ ]]; then - gemini_list "$REPLY" $pre - else - gemini_text "$REPLY" $pre - fi + '*'[[:space:]]*) + gemini_list "$REPLY" $pre ;; *) gemini_text "$REPLY" $pre ;; esac @@ -607,8 +761,8 @@ handle_keypress() { run select_url "$BOLLUX_PAGESRC" ;; 49) # g - goto a url -- input a new url - prompt GO URL - run blastoff -u "$URL" + prompt GO + run blastoff -u "$REPLY" ;; 50) # [ - back in the history run history_back || { @@ -626,10 +780,11 @@ handle_keypress() { run blastoff "$BOLLUX_URL" ;; 53) # G - goto a url (pre-filled with current) - prompt -u GO URL - run blastoff -u "$URL" + prompt -u GO + run blastoff -u "$REPLY" ;; - *) # 53-57 -- still available for binding + *) # 54-57 -- still available for binding + die "$?" "less(1) error" ;; esac } @@ -640,6 +795,7 @@ select_url() { select u in "${MAPFILE[@]}"; do case "$REPLY" in q) bollux_quit ;; + [^0-9]*) run blastoff -u "$REPLY" && break ;; esac run blastoff "${u%%[[:space:]]*}" && break done >"$BOLLUX_HISTFILE" @@ -699,6 +867,7 @@ history_back() { fi run blastoff "${HISTORY[$HN]}" } + history_forward() { log d "HN=$HN" if ((HN >= ${#HISTORY[@]})); then @@ -711,6 +880,4 @@ history_forward() { if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then run bollux "$@" -else - BOLLUX_LOGLEVEL=DEBUG fi -- cgit 1.4.1-21-gabe81