#!/usr/bin/env bash # bollux: a bash gemini client or whatever # Author: Case Duckworth # License: MIT # Version: -0.8 set -euo pipefail ### constants ### PRGN="${0##*/}" # program name DLDR="${BOLLUX_DOWNDIR:-.}" # where to download LOGL="${BOLLUX_LOGLEVEL:-3}" # log level MAXR="${BOLLUX_MAXREDIR:-5}" # max redirects PORT="${BOLLUX_PORT:-1965}" # port number PROT="${BOLLUX_PROTO:-gemini}" # protocol RDRS=0 # redirects # LOGLEVELS: # 0 - application fatal error # 1 - application warning # 2 - response error # 3 - response logging # 4 - application logging # 5 - diagnostic ### utility functions ### # a better echo put() { printf '%s\n' "$*"; } # conditionally log events to stderr # lower = more important log() { # log [LEVEL] [<] MESSAGE case "$1" in [0-5]) lvl="$1" shift ;; *) lvl=4 ;; esac output="$*" if ((lvl < LOGL)); then if (($# == 0)); then while IFS= read -r line; do output="$output${output:+$'\n'}$line" done fi printf '\e[3%dm%s\e[0m:\t%s\n' "$((lvl + 1))" "$PRGN" "$output" >&2 fi } # halt and catch fire die() { # die [EXIT-CODE] MESSAGE case "$1" in [0-9]*) ec="$1" shift ;; *) ec=1 ;; esac log 0 "$*" exit "$ec" } # fail if something isn't installed require() { hash "$1" 2>/dev/null || die 127 "Requirement '$1' not found."; } # trim a string trim() { sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'; } # stubs for when things aren't implemented (fully) NOT_IMPLEMENTED() { die 200 "NOT IMPLEMENTED!!!"; } NOT_FULLY_IMPLEMENTED() { log 1 "NOT FULLY IMPLEMENTED!!!"; } ### gemini ### # normalize a gemini address # example.com => gemini://example.com/ _address() { # _address URL addr="$1" [[ "$addr" != *://* ]] && addr="$PROT://$addr" trim <<<"$addr" } # return only the server part from an address, with the port added # gemini://example.com/path/to/file => example.com:1965 _server() { serv="$(_address "$1")" # normalize first serv="${serv#*://}" serv="${serv%%/*}" if [[ "$serv" != *:* ]]; then serv="$serv:$PORT" fi trim <<<"$serv" } # request a gemini page # by default, extract the server from the url request() { # request [-s SERVER] URL case "$1" in -s) serv="$(_server "$2")" addr="$(_address "$3")" ;; *) serv="$(_server "$1")" addr="$(_address "$1")" ;; esac log 5 "serv: $serv" log 5 "addr: $addr" t="$(mktemp)" sslcmd=(openssl s_client -crlf -ign_eof -quiet -connect "$serv") log "${sslcmd[@]}" "${sslcmd[@]}" <<<"$addr" 2>"$t" ((LOGL > 4)) && cat "$t" rm "$t" } # handle the response # cf. gemini://gemini.circumlunar.space/docs/spec-spec.txt handle() { # handle URL < RESPONSE url="$(_address "$1")" resp="$(cat)" head="$(sed 1q <<<"$resp")" body="$(sed 1d <<<"$resp")" code="$(awk '{print $1}' <<<"$head")" meta="$(awk '{for(i=2;i<=NF;i++)printf "%s ",$i;printf "\n"}' <<<"$head")" log 5 "[$code] $meta" case "$code" in 1*) # INPUT log 3 "Input" put "$meta" read -rep "? " bollux "$url?$REPLY" ;; 2*) # SUCCESS log 3 "Success" case "$code" in 20) log 5 "- OK" ;; 21) log 5 "- End of client certificate session" ;; *) log 2 "- Unknown response code: '$code'." ;; esac display <<<"$body" ;; 3*) # REDIRECT log 3 "Redirecting" case "$code" in 30) log 5 "- Temporary" ;; 31) log 5 "- Permanent" ;; *) log 2 "- Unknown response code: '$code'." ;; esac ((RDRS+=1)) ((RDRS > MAXR)) && die "$code" "Too many redirects!" bollux "$meta" ;; 4*) # TEMPORARY FAILURE log 2 "Temporary failure" case "$code" in 41) log 5 "- Server unavailable" ;; 42) log 5 "- CGI error" ;; 43) log 5 "- Proxy error" ;; 44) log 5 "- Rate limited" ;; *) log 2 "- Unknown response code: '$code'." ;; esac exit "$code" ;; 5*) # PERMANENT FAILURE log 2 "Permanent failure" case "$code" in 51) log 5 "- Not found" ;; 52) log 5 "- No longer available" ;; 53) log 5 "- Proxy request refused" ;; 59) log 5 "- Bad request" ;; *) log 2 "- Unknown response code: '$code'." ;; esac exit "$code" ;; 6*) # CLIENT CERT REQUIRED log 2 "Client certificate required" case "$code" in 61) log 5 "- Transient cert requested" ;; 62) log 5 "- Authorized cert required" ;; 63) log 5 "- Cert not accepted" ;; 64) log 5 "- Future cert rejected" ;; 65) log 5 "- Expired cert rejected" ;; *) log 2 "- Unknown response code: '$code'." ;; esac exit "$code" ;; *) # ??? die "$code" "Unknown response code: '$code'." ;; esac } # display the page display() { # display <<< STRING # normalize line endings to "\n" awk 'BEGIN{RS=""}{gsub("\r\n?","\n");print}' # TODO: use less with linking and stuff # less -R -p'^=>' +g # lesskey: # l /=>\n # highlight links # o pipe \n open_url # open the link on the top line # u shell select_url % # shows a selection prompt for all urls (on screen? file?) # Q exit 1 # for one of these, show a selection prompt for urls # q exit 0 # for the other, just quit ### # also look into the prompt, the filename, and input preprocessor # ($LESSOPEN, $LESSCLOSE) } ### main entry point ### bollux() { if (($# == 1)); then url="$1" else read -rp "GO> " url fi log 5 "url : $url" log 5 "addr: $(_address "$url")" log 5 "serv: $(_server "$url")" request "$url" | handle "$url" } if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then # requirements here -- so they're only checked once require openssl bollux "$@" fi