2 # bollux: a bash gemini client or whatever
3 # Author: Case Duckworth <acdw@acdw.net>
8 PRGN="${0##*/}" # program name
9 DLDR="${BOLLUX_DOWNDIR:-.}" # where to download
10 LOGL="${BOLLUX_LOGLEVEL:-3}" # log level
11 MAXR="${BOLLUX_MAXREDIR:-5}" # max redirects
12 PORT="${BOLLUX_PORT:-1965}" # port number
13 PROT="${BOLLUX_PROTO:-gemini}" # protocol
17 # 0 - application fatal error
18 # 1 - application warning
20 # 3 - response logging
21 # 4 - application logging
24 ### utility functions ###
26 put() { printf '%s\n' "$*" >&3; }
28 # conditionally log events to stderr
29 # lower = more important
30 log() { # log [LEVEL] [<] MESSAGE
44 if ((lvl < LOGL)); then
46 while IFS= read -r line; do
47 output="$output${output:+$'\n'}$line"
50 printf '\e[34m%s\e[0m:\t%s\n' "$PRGN" "$output" >&2
55 die() { # die [EXIT-CODE] MESSAGE
68 # ask the user for input
69 ask() { # ask PROMPT [READ_OPT...]
72 read -e -r -u 3 -p "$prompt> " "$@"
75 # fail if something isn't installed
76 require() { hash "$1" 2>/dev/null || die 127 "Requirement '$1' not found."; }
79 trim() { sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'; }
81 # stubs for when things aren't implemented (fully)
82 NOT_IMPLEMENTED() { die 200 "NOT IMPLEMENTED!!!"; }
83 NOT_FULLY_IMPLEMENTED() { log 1 "NOT FULLY IMPLEMENTED!!!"; }
86 # normalize a gemini address
87 # example.com => gemini://example.com/
88 _address() { # _address URL
91 [[ "$addr" != *://* ]] && addr="$PROT://$addr"
95 # return only the server part from an address, with the port added
96 # gemini://example.com/path/to/file => example.com:1965
98 serv="$(_address "$1")" # normalize first
101 if [[ "$serv" != *:* ]]; then
107 # request a gemini page
108 # by default, extract the server from the url
109 request() { # request [-s SERVER] URL
112 serv="$(_server "$2")"
113 addr="$(_address "$3")"
116 serv="$(_server "$1")"
117 addr="$(_address "$1")"
124 sslcmd=(openssl s_client -crlf -ign_eof -quiet -connect "$serv")
126 "${sslcmd[@]}" <<<"$addr" 2>/dev/null
129 # handle the response
130 # cf. gemini://gemini.circumlunar.space/docs/spec-spec.txt
131 handle() { # handle URL < RESPONSE
133 while read -r head; do
134 break # wait to read the first line
136 code="$(awk '{print $1}' <<<"$head")"
137 meta="$(awk '{for(i=2;i<=NF;i++)printf "%s ",$i;printf "\n"}' <<<"$head")"
139 log 5 "[$code] $meta"
152 21) log 5 "- End of client certificate session" ;;
153 *) log 2 "- Unknown response code: '$code'." ;;
160 30) log 5 "- Temporary" ;;
161 31) log 5 "- Permanent" ;;
162 *) log 2 "- Unknown response code: '$code'." ;;
165 ((RDRS > MAXR)) && die "$code" "Too many redirects!"
168 4*) # TEMPORARY FAILURE
169 log 2 "Temporary failure"
171 41) log 5 "- Server unavailable" ;;
172 42) log 5 "- CGI error" ;;
173 43) log 5 "- Proxy error" ;;
174 44) log 5 "- Rate limited" ;;
175 *) log 2 "- Unknown response code: '$code'." ;;
179 5*) # PERMANENT FAILURE
180 log 2 "Permanent failure"
182 51) log 5 "- Not found" ;;
183 52) log 5 "- No longer available" ;;
184 53) log 5 "- Proxy request refused" ;;
185 59) log 5 "- Bad request" ;;
186 *) log 2 "- Unknown response code: '$code'." ;;
190 6*) # CLIENT CERT REQUIRED
191 log 2 "Client certificate required"
193 61) log 5 "- Transient cert requested" ;;
194 62) log 5 "- Authorized cert required" ;;
195 63) log 5 "- Cert not accepted" ;;
196 64) log 5 "- Future cert rejected" ;;
197 65) log 5 "- Expired cert rejected" ;;
198 *) log 2 "- Unknown response code: '$code'." ;;
203 die "$code" "Unknown response code: '$code'."
209 display() { # display MIMETYPE < DOCUMENT
213 # normalize line endings to "\n"
214 # awk 'BEGIN{RS=""}{gsub(/\r\n?/,"\n");print}'
216 # TODO: use less with linking and stuff
219 # l /=>\n # highlight links
220 # o pipe \n open_url # open the link on the top line
221 # u shell select_url % # shows a selection prompt for all urls (on screen? file?)
222 # Q exit 1 # for one of these, show a selection prompt for urls
223 # q exit 0 # for the other, just quit
225 # also look into the prompt, the filename, and input preprocessor
226 # ($LESSOPEN, $LESSCLOSE)
234 download() { # download URL < FILE
236 dd status=progress >"$tn"
237 fn="$DLDR/${URL##*/}"
238 if [[ -f "$fn" ]]; then
241 if mv "$tn" "$fn"; then
244 log 0 "Error saving '$fn'."
250 ### main entry point ###
252 # use &3 for user input
263 request "$URL" | handle "$URL"
268 trap bollux_cleanup INT QUIT TERM EXIT
276 if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
277 set -euo pipefail # strict mode
278 # requirements here -- so they're only checked once